mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-27 00:28:14 +00:00
Merge origin/main into fix/net-send-nosignal
This commit is contained in:
commit
98c0bc2d4e
595 files changed
+29193
-2390
No files matched your search
@@ -421,7 +421,7 @@ jobs:
|
||||
|
||||
linux:
|
||||
needs: eligibility
|
||||
if: ${{ (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.linux)) && needs.eligibility.outputs.full == 'true' }}
|
||||
if: ${{ !github.event.repository.fork && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.linux)) && needs.eligibility.outputs.full == 'true' }}
|
||||
runs-on: [self-hosted, linux, x64, anyps5]
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
name: Input mapper
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- '.github/workflows/input-mapper.yml'
|
||||
- 'tools/input-mapper/**'
|
||||
- 'core/libs/prx/libScePad/include/**'
|
||||
- 'core/libs/prx/libSceVideoOut/src/InputMapping.cpp'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
input-mapper:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '22'
|
||||
package-manager-cache: false
|
||||
- run: npm --prefix tools/input-mapper test
|
||||
@@ -18,6 +18,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
progress:
|
||||
if: ${{ !github.event.repository.fork }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
|
||||
@@ -33,7 +33,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
pages:
|
||||
if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success'
|
||||
if: ${{ !github.event.repository.fork && (github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success') }}
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: github-pages
|
||||
|
||||
@@ -36,4 +36,4 @@ jobs:
|
||||
node --check demo/web/output-directory.mjs
|
||||
node --check demo/web/worker.mjs
|
||||
- name: Test browser converter helpers
|
||||
run: node --test demo/web/tests/batch.test.mjs demo/web/tests/output-directory.test.mjs
|
||||
run: node --test demo/web/tests/app.test.mjs demo/web/tests/batch.test.mjs demo/web/tests/output-directory.test.mjs
|
||||
+21
-5
@@ -136,10 +136,17 @@ if(BUILD_TESTING)
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/test_shader_cache_native_paths.py
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/ShaderCacheVersion.cmake ${CMAKE_COMMAND})
|
||||
set_tests_properties(shader_cache_native_paths PROPERTIES TIMEOUT 20)
|
||||
add_test(NAME shader_cache_runtime_sources COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_shader_cache_runtime_sources.py
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler ${CMAKE_COMMAND})
|
||||
set_tests_properties(shader_cache_runtime_sources PROPERTIES TIMEOUT 180)
|
||||
foreach(shard RANGE 0 6)
|
||||
if(shard EQUAL 0)
|
||||
set(shader_cache_sources_test_name shader_cache_runtime_sources)
|
||||
else()
|
||||
set(shader_cache_sources_test_name shader_cache_runtime_sources_shard_${shard})
|
||||
endif()
|
||||
add_test(NAME ${shader_cache_sources_test_name} COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_shader_cache_runtime_sources.py
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler ${CMAKE_COMMAND} --shard ${shard}/7)
|
||||
set_tests_properties(${shader_cache_sources_test_name} PROPERTIES TIMEOUT 180)
|
||||
endforeach()
|
||||
add_test(NAME pr_overlap_encoding COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_encoding.py)
|
||||
add_test(NAME pr_overlap_dependencies COMMAND ${Python3_EXECUTABLE}
|
||||
@@ -164,6 +171,8 @@ if(BUILD_TESTING)
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_import_audit.py)
|
||||
add_test(NAME unwrap_self COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_unwrap_self.py)
|
||||
add_test(NAME audit_dump COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_audit_dump.py)
|
||||
foreach(shard RANGE 0 11)
|
||||
if(shard EQUAL 0)
|
||||
set(conventions_test_name conventions_function_bodies)
|
||||
@@ -252,6 +261,8 @@ if(BUILD_TESTING)
|
||||
endforeach()
|
||||
add_test(NAME trace_digest COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_trace_digest.py)
|
||||
add_test(NAME symbolize_samples COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_symbolize_samples.py)
|
||||
add_test(NAME conventions_bom_shebang COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_bom_shebang.py)
|
||||
set_tests_properties(conventions_bom_shebang PROPERTIES TIMEOUT 20)
|
||||
@@ -277,8 +288,13 @@ if(BUILD_TESTING)
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_alignment.py)
|
||||
add_test(NAME conventions_pr_template COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_pr_template.py)
|
||||
add_test(NAME check_compile_preprocessor COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_compile_preprocessor.py)
|
||||
set_tests_properties(check_compile_preprocessor PROPERTIES TIMEOUT 20)
|
||||
add_test(NAME check_compile COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_compile.py)
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_compile.py CheckCompileTests)
|
||||
add_test(NAME check_compile_outputs COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_compile.py CheckCompileOutputTests)
|
||||
add_test(NAME performance_compare COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_performance_compare.py)
|
||||
set_tests_properties(performance_compare PROPERTIES TIMEOUT 20)
|
||||
|
||||
@@ -269,6 +269,15 @@ if(BUILD_TESTING)
|
||||
set_tests_properties(guest_kernel_sys_sdk_version PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel_sys>")
|
||||
endif()
|
||||
add_test_executable(guest_kernel_sys_console_identity_tests tests/GuestKernelSysConsoleIdentity.cpp)
|
||||
target_include_directories(guest_kernel_sys_console_identity_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(guest_kernel_sys_console_identity_tests PRIVATE libkernel_sys libkernel libc)
|
||||
configure_windows_unwind(guest_kernel_sys_console_identity_tests)
|
||||
add_test(NAME guest_kernel_sys_console_identity COMMAND guest_kernel_sys_console_identity_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_kernel_sys_console_identity PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel_sys>")
|
||||
endif()
|
||||
add_test_executable(nid_buffer_validation_tests nid/tests/BufferValidation.cpp)
|
||||
target_include_directories(nid_buffer_validation_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/nid/include)
|
||||
target_compile_features(nid_buffer_validation_tests PRIVATE cxx_std_20)
|
||||
@@ -278,6 +287,32 @@ if(BUILD_TESTING)
|
||||
add_test(NAME nid_buffer_validation COMMAND nid_buffer_validation_tests)
|
||||
set_tests_properties(nid_buffer_validation PROPERTIES TIMEOUT 10)
|
||||
|
||||
add_test_executable(guest_dinkumware_fetch_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/GuestDinkumwareFetch.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nid/src/NidCompute.cpp ${CMAKE_CURRENT_SOURCE_DIR}/nid/src/Sha1.cpp)
|
||||
target_include_directories(guest_dinkumware_fetch_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/nid/include)
|
||||
target_link_libraries(guest_dinkumware_fetch_tests PRIVATE ${CMAKE_DL_LIBS})
|
||||
configure_windows_unwind(guest_dinkumware_fetch_tests)
|
||||
if(MINGW)
|
||||
target_link_libraries(guest_dinkumware_fetch_tests PRIVATE libc)
|
||||
endif()
|
||||
add_custom_target(guest_dinkumware_fetch_libraries DEPENDS ${LIBC_PATCHED})
|
||||
add_dependencies(guest_dinkumware_fetch_tests guest_dinkumware_fetch_libraries)
|
||||
foreach(operation IN ITEMS and or xor)
|
||||
foreach(width IN ITEMS 1 2 4)
|
||||
add_test(NAME guest_dinkumware_fetch_${operation}_${width} COMMAND guest_dinkumware_fetch_tests
|
||||
$<TARGET_FILE_DIR:libc> unpatched ${operation} ${width})
|
||||
add_test(NAME guest_dinkumware_fetch_${operation}_${width}_patched COMMAND guest_dinkumware_fetch_tests
|
||||
${PATCHED_DIR} patched ${operation} ${width})
|
||||
set_tests_properties(guest_dinkumware_fetch_${operation}_${width}
|
||||
guest_dinkumware_fetch_${operation}_${width}_patched PROPERTIES TIMEOUT 20)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_dinkumware_fetch_${operation}_${width} PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
set_tests_properties(guest_dinkumware_fetch_${operation}_${width}_patched PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:${PATCHED_DIR}")
|
||||
endif()
|
||||
endforeach()
|
||||
endforeach()
|
||||
add_test_executable(pe_import_first_tests allocator/tests/PeImportFirstTests.cpp allocator/PeImportFirst.cpp)
|
||||
target_include_directories(pe_import_first_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_compile_features(pe_import_first_tests PRIVATE cxx_std_20)
|
||||
@@ -372,6 +407,16 @@ if(BUILD_TESTING)
|
||||
configure_windows_unwind(guest_kernel_sys_constants_tests)
|
||||
add_test(NAME guest_kernel_sys_constants COMMAND guest_kernel_sys_constants_tests)
|
||||
set_tests_properties(guest_kernel_sys_constants PROPERTIES TIMEOUT 20)
|
||||
add_test_executable(guest_firmware_constant_returns_tests tests/GuestFirmwareConstantReturns.cpp)
|
||||
target_include_directories(guest_firmware_constant_returns_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(guest_firmware_constant_returns_tests PRIVATE libSceComposite libSceFsInternalForVsh libSceHttpCache libScePlayReady libSceSdma libSceSsl libSceVisionManager libmdbg_syscore libkernel libc)
|
||||
configure_windows_unwind(guest_firmware_constant_returns_tests)
|
||||
add_test(NAME guest_firmware_constant_returns COMMAND guest_firmware_constant_returns_tests)
|
||||
set_tests_properties(guest_firmware_constant_returns PROPERTIES TIMEOUT 20)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_firmware_constant_returns PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceComposite>;PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceFsInternalForVsh>;PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceHttpCache>;PATH=path_list_prepend:$<TARGET_FILE_DIR:libScePlayReady>;PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceSdma>;PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceSsl>;PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceVisionManager>;PATH=path_list_prepend:$<TARGET_FILE_DIR:libmdbg_syscore>")
|
||||
endif()
|
||||
if(Python3_Interpreter_FOUND)
|
||||
add_test(NAME nid_verneed COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_verneed.py $<TARGET_FILE:nid_patcher>)
|
||||
@@ -416,6 +461,18 @@ if(BUILD_TESTING)
|
||||
add_subdirectory(tests/write_watch)
|
||||
add_subdirectory(tests/guest_arena)
|
||||
endif()
|
||||
add_test_executable(guest_kernel_sys_pthread_np_tests tests/GuestKernelSysPthreadNp.cpp)
|
||||
target_include_directories(guest_kernel_sys_pthread_np_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(guest_kernel_sys_pthread_np_tests PRIVATE libkernel_sys libkernel libc)
|
||||
configure_windows_unwind(guest_kernel_sys_pthread_np_tests)
|
||||
foreach(pthreadNpMode IN ITEMS rwlockattr kind)
|
||||
add_test(NAME guest_kernel_sys_pthread_np_${pthreadNpMode} COMMAND guest_kernel_sys_pthread_np_tests ${pthreadNpMode})
|
||||
set_tests_properties(guest_kernel_sys_pthread_np_${pthreadNpMode} PROPERTIES TIMEOUT 20)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_kernel_sys_pthread_np_${pthreadNpMode} PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel_sys>")
|
||||
endif()
|
||||
endforeach()
|
||||
add_test_executable(guest_kernel_sys_module_internal_tests tests/GuestKernelSysModuleInternal.cpp)
|
||||
target_include_directories(guest_kernel_sys_module_internal_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(guest_kernel_sys_module_internal_tests PRIVATE libkernel_sys libkernel libc)
|
||||
@@ -435,6 +492,23 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
add_test(NAME windows_write_watch COMMAND windows_write_watch_tests)
|
||||
set_tests_properties(windows_write_watch PROPERTIES TIMEOUT 20)
|
||||
add_test_executable(windows_shared_map_cost_tests tests/WindowsSharedMapCost.cpp)
|
||||
target_include_directories(windows_shared_map_cost_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_compile_definitions(windows_shared_map_cost_tests PRIVATE _WIN32_WINNT=0x0A00)
|
||||
if(MINGW)
|
||||
target_compile_options(windows_shared_map_cost_tests PRIVATE -fasynchronous-unwind-tables)
|
||||
endif()
|
||||
add_test(NAME windows_shared_map_cost COMMAND windows_shared_map_cost_tests)
|
||||
set_tests_properties(windows_shared_map_cost PROPERTIES TIMEOUT 120)
|
||||
endif()
|
||||
add_test_executable(guest_kernel_sys_login_tests tests/GuestKernelSysLogin.cpp)
|
||||
target_include_directories(guest_kernel_sys_login_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(guest_kernel_sys_login_tests PRIVATE libkernel_sys libkernel libc)
|
||||
configure_windows_unwind(guest_kernel_sys_login_tests)
|
||||
add_test(NAME guest_kernel_sys_login COMMAND guest_kernel_sys_login_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_kernel_sys_login PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel_sys>")
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
@@ -517,6 +591,17 @@ if(BUILD_TESTING)
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceMove>")
|
||||
endif()
|
||||
|
||||
add_test_executable(guest_pthread_create_suspended_tests tests/GuestPthreadCreateSuspended.cpp)
|
||||
target_include_directories(guest_pthread_create_suspended_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(guest_pthread_create_suspended_tests PRIVATE libkernel_sys libkernel libc)
|
||||
configure_windows_unwind(guest_pthread_create_suspended_tests)
|
||||
add_test(NAME guest_pthread_create_suspended COMMAND guest_pthread_create_suspended_tests)
|
||||
set_tests_properties(guest_pthread_create_suspended PROPERTIES TIMEOUT 20)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_pthread_create_suspended PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel_sys>")
|
||||
endif()
|
||||
|
||||
add_test_executable(guest_np_party_tests tests/GuestNpParty.cpp)
|
||||
target_include_directories(guest_np_party_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(guest_np_party_tests PRIVATE libSceNpParty libc)
|
||||
@@ -887,6 +972,18 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
add_test(NAME MemoryPoolTest COMMAND MemoryPoolTest)
|
||||
|
||||
add_test_executable(guest_kernel_sys_swapcontext_tests tests/GuestKernelSysSwapcontext.cpp)
|
||||
target_include_directories(guest_kernel_sys_swapcontext_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_compile_options(guest_kernel_sys_swapcontext_tests PRIVATE -fno-omit-frame-pointer)
|
||||
target_link_libraries(guest_kernel_sys_swapcontext_tests PRIVATE libkernel_sys libSceLibcInternal libkernel libc)
|
||||
configure_windows_unwind(guest_kernel_sys_swapcontext_tests)
|
||||
add_test(NAME guest_kernel_sys_swapcontext COMMAND guest_kernel_sys_swapcontext_tests)
|
||||
set_tests_properties(guest_kernel_sys_swapcontext PROPERTIES TIMEOUT 20)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_kernel_sys_swapcontext PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel_sys>")
|
||||
endif()
|
||||
|
||||
add_test_executable(guest_acm_tests tests/GuestAcm.cpp)
|
||||
target_include_directories(guest_acm_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(guest_acm_tests PRIVATE libSceAcm libc)
|
||||
|
||||
@@ -1502,6 +1502,15 @@ struct SharePlayConnectionInfoA {
|
||||
std::int32_t visitorUserId;
|
||||
};
|
||||
static_assert(sizeof(SharePlayConnectionInfoA) == 72 && offsetof(SharePlayConnectionInfoA, hostAccountId) == 48 && offsetof(SharePlayConnectionInfoA, hostUserId) == 64);
|
||||
struct SharePlayConnectionInfo {
|
||||
std::int32_t status;
|
||||
std::int32_t mode;
|
||||
NpOnlineId hostOnlineId;
|
||||
NpOnlineId visitorOnlineId;
|
||||
std::int32_t hostUserId;
|
||||
std::int32_t visitorUserId;
|
||||
};
|
||||
static_assert(sizeof(SharePlayConnectionInfo) == 56 && offsetof(SharePlayConnectionInfo, hostUserId) == 48);
|
||||
struct NpId { NpOnlineId online_id; std::uint8_t opaque[4]; };
|
||||
struct NpCreateAsyncRequestParameter { std::uint8_t opaque[64]; };
|
||||
struct NpCheckPremiumParameter { std::uint8_t opaque[64]; };
|
||||
|
||||
@@ -22,6 +22,17 @@ struct Elf64_Ehdr {
|
||||
std::uint16_t e_shstrndx;
|
||||
};
|
||||
|
||||
struct Elf64_Phdr {
|
||||
std::uint32_t p_type;
|
||||
std::uint32_t p_flags;
|
||||
std::uint64_t p_offset;
|
||||
std::uint64_t p_vaddr;
|
||||
std::uint64_t p_paddr;
|
||||
std::uint64_t p_filesz;
|
||||
std::uint64_t p_memsz;
|
||||
std::uint64_t p_align;
|
||||
};
|
||||
|
||||
struct Elf64_Shdr {
|
||||
std::uint32_t sh_name;
|
||||
std::uint32_t sh_type;
|
||||
@@ -103,7 +114,7 @@ constexpr std::uint16_t kShnUndef = 0u;
|
||||
|
||||
class ElfNidPatcher final : public IBinaryPatcher {
|
||||
public:
|
||||
void PatchNids(std::vector<std::uint8_t>& binary, const std::string& libraryName, const std::unordered_set<std::string>& excludedExports) const override;
|
||||
void PatchNids(std::vector<std::uint8_t>& binary, const std::string& libraryName, const std::unordered_set<std::string>& excludedExports, const PatchOptions& options = {}) const override;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -8,11 +8,16 @@
|
||||
|
||||
namespace Nid {
|
||||
|
||||
struct PatchOptions {
|
||||
bool executable = false;
|
||||
bool onlySce = false;
|
||||
};
|
||||
|
||||
class IBinaryPatcher {
|
||||
public:
|
||||
virtual ~IBinaryPatcher() = default;
|
||||
|
||||
virtual void PatchNids(std::vector<std::uint8_t>& binary, const std::string& libraryName, const std::unordered_set<std::string>& excludedExports) const = 0;
|
||||
virtual void PatchNids(std::vector<std::uint8_t>& binary, const std::string& libraryName, const std::unordered_set<std::string>& excludedExports, const PatchOptions& options = {}) const = 0;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ std::vector<MachOExport> ReadMachOExports(const std::vector<std::uint8_t>& binar
|
||||
|
||||
class MachONidPatcher final : public IBinaryPatcher {
|
||||
public:
|
||||
void PatchNids(std::vector<std::uint8_t>& binary, const std::string& libraryName, const std::unordered_set<std::string>& excludedExports) const override;
|
||||
void PatchNids(std::vector<std::uint8_t>& binary, const std::string& libraryName, const std::unordered_set<std::string>& excludedExports, const PatchOptions& options = {}) const override;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef NID_NIDRESOLVER_HPP
|
||||
#define NID_NIDRESOLVER_HPP
|
||||
|
||||
#include <nid/IBinaryPatcher.hpp>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
@@ -8,6 +9,10 @@
|
||||
|
||||
namespace Nid {
|
||||
|
||||
bool IsSceImport(const std::string& name);
|
||||
|
||||
std::unordered_map<std::string, std::string> ResolveImportNids(const std::vector<std::string>& names, const std::unordered_set<std::string>& excluded, const PatchOptions& options);
|
||||
|
||||
std::string ResolveOneName(const std::string& funcName);
|
||||
|
||||
std::unordered_map<std::string, std::string> ResolveNids(const std::vector<std::string>& exportedNames, const std::string& libraryName, const std::unordered_set<std::string>& excludedExports);
|
||||
|
||||
@@ -39,7 +39,7 @@ struct PeSectionHeader {
|
||||
|
||||
class PeNidPatcher final : public IBinaryPatcher {
|
||||
public:
|
||||
void PatchNids(std::vector<std::uint8_t>& binary, const std::string& libraryName, const std::unordered_set<std::string>& excludedExports) const override;
|
||||
void PatchNids(std::vector<std::uint8_t>& binary, const std::string& libraryName, const std::unordered_set<std::string>& excludedExports, const PatchOptions& options = {}) const override;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -354,7 +354,7 @@ std::vector<std::uint32_t> CollectVerneedNameOffsets(const std::vector<std::uint
|
||||
|
||||
}
|
||||
|
||||
void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string& libraryName, const std::unordered_set<std::string>& excludedExports) const {
|
||||
void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string& libraryName, const std::unordered_set<std::string>& excludedExports, const PatchOptions& options) const {
|
||||
using namespace Internal;
|
||||
|
||||
if (elf.size() < sizeof(Elf64_Ehdr)) throw std::runtime_error("file too small");
|
||||
@@ -370,11 +370,13 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
|
||||
std::size_t sysvHashOffset = 0u, sysvHashSize = 0u;
|
||||
std::uint32_t sysvHashLink = 0u;
|
||||
std::size_t versymOffset = 0u;
|
||||
bool hasDynamic = false;
|
||||
std::uint32_t dynSymSectionIndex = 0u;
|
||||
std::uint32_t gnuHashSymOffset = 0u;
|
||||
for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) {
|
||||
const std::size_t shOffset = static_cast<std::size_t>(ehdr.e_shoff) + i * sizeof(Elf64_Shdr);
|
||||
const auto shdr = Read<Elf64_Shdr>(elf, shOffset);
|
||||
if (shdr.sh_type == kShtDynamic) hasDynamic = true;
|
||||
if (shdr.sh_type == kShtDynsym) {
|
||||
if (shdr.sh_entsize != sizeof(Elf64_Sym) || shdr.sh_size % sizeof(Elf64_Sym) || shdr.sh_link >= ehdr.e_shnum)
|
||||
throw std::runtime_error("invalid ELF dynsym layout");
|
||||
@@ -398,6 +400,19 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
|
||||
throw std::runtime_error("unsupported .gnu.version_d section: symbol version definitions cannot be preserved");
|
||||
}
|
||||
|
||||
if (options.executable && dynSymOffset == 0u && !hasDynamic) {
|
||||
if (ehdr.e_phnum) {
|
||||
if (!ehdr.e_phoff || ehdr.e_phentsize != sizeof(Elf64_Phdr) || ehdr.e_phnum == 0xffffu)
|
||||
throw std::runtime_error("unsupported ELF program header layout");
|
||||
RequireElfRange(elf, ehdr.e_phoff, std::uint64_t{ehdr.e_phnum} * sizeof(Elf64_Phdr), "program headers");
|
||||
for (std::uint16_t index = 0u; index < ehdr.e_phnum; ++index) {
|
||||
const auto segment = Read<Elf64_Phdr>(elf, ehdr.e_phoff + index * sizeof(Elf64_Phdr));
|
||||
if (segment.p_type == 2u)
|
||||
throw std::runtime_error("unsupported ELF PT_DYNAMIC without a .dynsym section");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (dynSymOffset == 0u) throw std::runtime_error("no .dynsym section");
|
||||
|
||||
const std::size_t symCount = dynSymSize / sizeof(Elf64_Sym);
|
||||
@@ -437,19 +452,26 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
|
||||
elf.begin() + static_cast<std::ptrdiff_t>(dynStrOffset + dynStrSize)
|
||||
);
|
||||
|
||||
std::vector<std::string> importedNames;
|
||||
std::vector<std::string> exportedNames;
|
||||
for (std::size_t i = 1u; i < symCount; ++i) {
|
||||
const std::size_t symOffset = dynSymOffset + i * sizeof(Elf64_Sym);
|
||||
const auto sym = Read<Elf64_Sym>(elf, symOffset);
|
||||
if (sym.st_name == 0u) continue;
|
||||
const std::uint8_t binding = sym.st_info >> 4u;
|
||||
if (binding == kStbLocal || sym.st_shndx == kShnUndef) continue;
|
||||
if (binding == kStbLocal) continue;
|
||||
if (sym.st_shndx == kShnUndef) {
|
||||
const auto name = ReadCStr(origDynStr, sym.st_name);
|
||||
if (!name.empty()) importedNames.push_back(name);
|
||||
continue;
|
||||
}
|
||||
const std::string symName = ReadCStr(origDynStr, sym.st_name);
|
||||
if (symName.empty()) continue;
|
||||
exportedNames.push_back(symName);
|
||||
}
|
||||
|
||||
const auto nidMap = ResolveNids(exportedNames, libraryName, excludedExports);
|
||||
const auto nidMap = options.executable ? std::unordered_map<std::string, std::string>{} : ResolveNids(exportedNames, libraryName, excludedExports);
|
||||
const auto importMap = ResolveImportNids(importedNames, excludedExports, options);
|
||||
|
||||
struct SymbolNameUse {
|
||||
std::size_t symIndex;
|
||||
@@ -481,14 +503,11 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
|
||||
} else if (binding != kStbLocal && sym.st_shndx == kShnUndef) {
|
||||
const bool isUnversioned = versymOffset == 0u ||
|
||||
(Read<std::uint16_t>(elf, versymOffset + i * sizeof(std::uint16_t)) <= 1u);
|
||||
const bool hasNidPostfix = symName.size() >= kNidPostfixLen &&
|
||||
symName.compare(symName.size() - kNidPostfixLen, kNidPostfixLen, kNidPostfix) == 0;
|
||||
const bool hasScePrefix = symName.size() >= 3u &&
|
||||
std::tolower(static_cast<unsigned char>(symName[0])) == 's' &&
|
||||
std::tolower(static_cast<unsigned char>(symName[1])) == 'c' &&
|
||||
std::tolower(static_cast<unsigned char>(symName[2])) == 'e';
|
||||
if (isUnversioned && (hasNidPostfix || hasScePrefix))
|
||||
newValue = ResolveOneName(symName);
|
||||
if (options.executable || isUnversioned) {
|
||||
newValue = importMap.at(symName);
|
||||
if (options.executable && newValue != symName && versymOffset != 0u)
|
||||
Write(elf, versymOffset + i * sizeof(std::uint16_t), std::uint16_t{1});
|
||||
}
|
||||
}
|
||||
|
||||
uses.push_back(SymbolNameUse{i, sym.st_name, newValue});
|
||||
@@ -538,11 +557,69 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
|
||||
for (const auto offset : preservedOffsets)
|
||||
preservedToNewOffset.emplace_back(offset, valueOffsets.at(ReadCStr(origDynStr, offset)));
|
||||
|
||||
if (newDynStr.size() > dynStrSize && options.executable) {
|
||||
if (ehdr.e_phentsize != sizeof(Elf64_Phdr) || ehdr.e_phnum == 0 || ehdr.e_phnum >= 0xfffdu)
|
||||
throw std::runtime_error("invalid ELF program header table for string growth");
|
||||
RequireElfRange(elf, ehdr.e_phoff, std::uint64_t{ehdr.e_phnum} * sizeof(Elf64_Phdr), "program headers");
|
||||
const auto align = [](std::uint64_t value) {
|
||||
if (value > UINT64_MAX - 4095u) throw std::runtime_error("ELF address overflow");
|
||||
return (value + 4095u) & ~std::uint64_t{4095u};
|
||||
};
|
||||
std::vector<Elf64_Phdr> segments;
|
||||
std::uint64_t virtualEnd = 0;
|
||||
bool hasPhdr = false;
|
||||
for (std::size_t index = 0; index < ehdr.e_phnum; ++index) {
|
||||
const auto segment = Read<Elf64_Phdr>(elf, ehdr.e_phoff + index * sizeof(Elf64_Phdr));
|
||||
segments.push_back(segment);
|
||||
hasPhdr |= segment.p_type == 6;
|
||||
if (segment.p_type != 1) continue;
|
||||
if (segment.p_memsz > UINT64_MAX - segment.p_vaddr) throw std::runtime_error("ELF segment address overflow");
|
||||
virtualEnd = std::max(virtualEnd, segment.p_vaddr + segment.p_memsz);
|
||||
}
|
||||
if (!virtualEnd) throw std::runtime_error("ELF executable has no load segment");
|
||||
if (!hasPhdr) segments.insert(segments.begin(), Elf64_Phdr{6, 4, 0, 0, 0, 0, 0, 8});
|
||||
const auto fileOffset = align(elf.size());
|
||||
const auto address = align(virtualEnd);
|
||||
const auto tableSize = (segments.size() + 1u) * sizeof(Elf64_Phdr);
|
||||
const auto segmentSize = tableSize + newDynStr.size();
|
||||
if (segmentSize > UINT64_MAX - address || segmentSize > SIZE_MAX - fileOffset) throw std::runtime_error("ELF string segment overflow");
|
||||
for (auto& segment : segments) {
|
||||
if (segment.p_type != 6) continue;
|
||||
segment = {6, 4, fileOffset, address, address, tableSize, tableSize, 8};
|
||||
}
|
||||
segments.push_back({1, 4, fileOffset, address, address, segmentSize, segmentSize, 4096});
|
||||
elf.resize(static_cast<std::size_t>(fileOffset + segmentSize), 0);
|
||||
for (std::size_t index = 0; index < segments.size(); ++index)
|
||||
Write(elf, fileOffset + index * sizeof(Elf64_Phdr), segments[index]);
|
||||
Write(elf, offsetof(Elf64_Ehdr, e_phoff), fileOffset);
|
||||
Write(elf, offsetof(Elf64_Ehdr, e_phnum), static_cast<std::uint16_t>(segments.size()));
|
||||
dynStrOffset = static_cast<std::size_t>(fileOffset + tableSize);
|
||||
dynStrSize = newDynStr.size();
|
||||
const auto stringAddress = address + tableSize;
|
||||
const auto sectionOffset = ehdr.e_shoff + dynStrSectionLink * sizeof(Elf64_Shdr);
|
||||
auto strings = Read<Elf64_Shdr>(elf, sectionOffset);
|
||||
strings.sh_offset = dynStrOffset;
|
||||
strings.sh_addr = stringAddress;
|
||||
strings.sh_size = dynStrSize;
|
||||
Write(elf, sectionOffset, strings);
|
||||
bool foundAddress = false;
|
||||
bool foundSize = false;
|
||||
for (std::size_t index = 0; index < ehdr.e_shnum; ++index) {
|
||||
const auto section = Read<Elf64_Shdr>(elf, ehdr.e_shoff + index * sizeof(Elf64_Shdr));
|
||||
if (section.sh_type != kShtDynamic) continue;
|
||||
RequireElfRange(elf, section.sh_offset, section.sh_size, "dynamic");
|
||||
for (std::uint64_t offset = 0; offset + sizeof(Elf64_Dyn) <= section.sh_size; offset += sizeof(Elf64_Dyn)) {
|
||||
auto entry = Read<Elf64_Dyn>(elf, section.sh_offset + offset);
|
||||
if (entry.d_tag == kDtNull) break;
|
||||
if (entry.d_tag == 5) { entry.d_val = stringAddress; foundAddress = true; }
|
||||
if (entry.d_tag == 10) { entry.d_val = dynStrSize; foundSize = true; }
|
||||
Write(elf, section.sh_offset + offset, entry);
|
||||
}
|
||||
}
|
||||
if (!foundAddress || !foundSize) throw std::runtime_error("ELF executable lacks DT_STRTAB or DT_STRSZ");
|
||||
}
|
||||
if (newDynStr.size() > dynStrSize) {
|
||||
throw std::runtime_error(
|
||||
"rebuilt .dynstr (" + std::to_string(newDynStr.size()) + " bytes) exceeds original section size (" +
|
||||
std::to_string(dynStrSize) + " bytes) - relinking with a larger section is required"
|
||||
);
|
||||
throw std::runtime_error("rebuilt .dynstr exceeds original section size; relinking with a larger section is required");
|
||||
}
|
||||
newDynStr.resize(dynStrSize, 0u);
|
||||
|
||||
|
||||
@@ -341,15 +341,6 @@ std::string StripUnderscore(const std::string& name) {
|
||||
return !name.empty() && name[0] == '_' ? name.substr(1) : name;
|
||||
}
|
||||
|
||||
bool IsGuestFacingImport(const std::string& name) {
|
||||
using namespace Internal;
|
||||
const bool hasNidPostfix = name.size() >= kNidPostfixLen && name.compare(name.size() - kNidPostfixLen, kNidPostfixLen, kNidPostfix) == 0;
|
||||
const bool hasScePrefix = name.size() >= 3u &&
|
||||
std::tolower(static_cast<unsigned char>(name[0])) == 's' &&
|
||||
std::tolower(static_cast<unsigned char>(name[1])) == 'c' &&
|
||||
std::tolower(static_cast<unsigned char>(name[2])) == 'e';
|
||||
return hasNidPostfix || hasScePrefix;
|
||||
}
|
||||
|
||||
std::vector<bool> DylibOrdinalsArePrx(const std::vector<std::uint8_t>& binary, const Image& image) {
|
||||
std::vector<bool> result;
|
||||
@@ -364,11 +355,48 @@ std::vector<bool> DylibOrdinalsArePrx(const std::vector<std::uint8_t>& binary, c
|
||||
return result;
|
||||
}
|
||||
|
||||
void PatchChainedImports(std::vector<std::uint8_t>& binary, const Image& image) {
|
||||
void WriteLinkeditData(std::vector<std::uint8_t>& binary, const Image& image, const TrieLocation& location, const std::vector<std::uint8_t>& trie) {
|
||||
const auto oldOffset = Read<std::uint32_t>(binary, location.OffsetField);
|
||||
const auto oldSize = Read<std::uint32_t>(binary, location.SizeField);
|
||||
if (trie.size() <= oldSize) {
|
||||
std::copy(trie.begin(), trie.end(), binary.begin() + oldOffset);
|
||||
std::fill(binary.begin() + oldOffset + static_cast<std::ptrdiff_t>(trie.size()), binary.begin() + oldOffset + oldSize, 0);
|
||||
Write(binary, location.SizeField, static_cast<std::uint32_t>(trie.size()));
|
||||
return;
|
||||
}
|
||||
std::optional<std::size_t> linkedit;
|
||||
for (const auto offset : image.Commands) {
|
||||
const auto cmd = Read<LoadCommand>(binary, offset).Cmd;
|
||||
if (cmd == kLcCodeSignature) throw std::runtime_error("signed Mach-O images cannot grow linkedit data");
|
||||
if (cmd != kLcSegment64) continue;
|
||||
const auto segment = Read<SegmentCommand64>(binary, offset);
|
||||
if (std::strncmp(segment.Name, "__LINKEDIT", sizeof(segment.Name)) == 0) linkedit = offset;
|
||||
}
|
||||
if (!linkedit) throw std::runtime_error("Mach-O image has no __LINKEDIT segment");
|
||||
auto segment = Read<SegmentCommand64>(binary, *linkedit);
|
||||
if (segment.FileOffset + segment.FileSize != binary.size()) throw std::runtime_error("__LINKEDIT does not end the file");
|
||||
const auto newOffset = (binary.size() + 7) & ~std::size_t{7};
|
||||
if (newOffset + trie.size() > 0xffffffffu) throw std::runtime_error("linkedit data offset overflow");
|
||||
binary.resize(newOffset, 0);
|
||||
binary.insert(binary.end(), trie.begin(), trie.end());
|
||||
std::fill(binary.begin() + oldOffset, binary.begin() + oldOffset + oldSize, 0);
|
||||
segment.FileSize = binary.size() - segment.FileOffset;
|
||||
segment.VmSize = std::max(segment.VmSize, (segment.FileSize + kPageSize - 1) & ~(kPageSize - 1));
|
||||
Write(binary, *linkedit, segment);
|
||||
Write(binary, location.OffsetField, static_cast<std::uint32_t>(newOffset));
|
||||
Write(binary, location.SizeField, static_cast<std::uint32_t>(trie.size()));
|
||||
}
|
||||
|
||||
void PatchChainedImports(std::vector<std::uint8_t>& binary, const Image& image, const std::unordered_set<std::string>& excluded, const PatchOptions& options) {
|
||||
std::size_t commandOffset = 0;
|
||||
std::optional<LinkeditDataCommand> fixups;
|
||||
for (const auto offset : image.Commands) {
|
||||
const auto cmd = Read<LoadCommand>(binary, offset).Cmd;
|
||||
if (cmd == kLcDyldChainedFixups) fixups = Read<LinkeditDataCommand>(binary, offset);
|
||||
if (cmd == kLcDyldChainedFixups) {
|
||||
fixups = Read<LinkeditDataCommand>(binary, offset);
|
||||
commandOffset = offset;
|
||||
}
|
||||
if (options.executable && cmd == kLcCodeSignature) throw std::runtime_error("remove the Mach-O code signature before patching and sign again afterwards");
|
||||
if ((cmd == kLcDyldInfo || cmd == kLcDyldInfoOnly)) {
|
||||
const auto info = Read<DyldInfoCommand>(binary, offset);
|
||||
if (info.BindSize != 0 || info.LazyBindSize != 0 || info.WeakBindSize != 0) throw std::runtime_error("Mach-O bind opcodes are not supported; link with chained fixups");
|
||||
@@ -400,61 +428,72 @@ void PatchChainedImports(std::vector<std::uint8_t>& binary, const Image& image)
|
||||
imports.push_back({raw >> 9, raw & 0xff});
|
||||
}
|
||||
}
|
||||
std::set<std::uint32_t> nameOffsets;
|
||||
for (const auto& item : imports) nameOffsets.insert(item.NameOffset);
|
||||
std::set<std::uint32_t> rewritten;
|
||||
std::vector<std::string> names;
|
||||
std::vector<std::string> originalNames;
|
||||
std::vector<bool> selected;
|
||||
for (const auto& item : imports) {
|
||||
const bool special = header.ImportsFormat == kImportFormatAddend64 ? item.Ordinal >= 0xfff0 : item.Ordinal >= 0xf0;
|
||||
if (item.Ordinal == 0 || special || item.Ordinal > prx.size() || !prx[item.Ordinal - 1]) continue;
|
||||
if (rewritten.contains(item.NameOffset)) continue;
|
||||
const auto at = symbols + item.NameOffset;
|
||||
if (at >= symbolsEnd) throw std::runtime_error("chained import name out of bounds");
|
||||
const auto name = Internal::ReadCStr(binary, at);
|
||||
if (at + name.size() >= symbolsEnd) throw std::runtime_error("unterminated chained import name");
|
||||
const auto bare = StripUnderscore(name);
|
||||
if (bare == name || !IsGuestFacingImport(bare)) continue;
|
||||
const auto patched = "_" + ResolveOneName(bare);
|
||||
if (patched.size() > name.size()) throw std::runtime_error("import NID longer than original name: " + name);
|
||||
const auto inside = nameOffsets.upper_bound(item.NameOffset);
|
||||
if (inside != nameOffsets.end() && *inside <= item.NameOffset + name.size()) throw std::runtime_error("chained import names share storage: " + name);
|
||||
std::memcpy(binary.data() + at, patched.data(), patched.size());
|
||||
std::fill(binary.begin() + static_cast<std::ptrdiff_t>(at + patched.size()), binary.begin() + static_cast<std::ptrdiff_t>(at + name.size() + 1), 0);
|
||||
rewritten.insert(item.NameOffset);
|
||||
const bool special = header.ImportsFormat == kImportFormatAddend64 ? item.Ordinal >= 0xfff0 : item.Ordinal >= 0xf0;
|
||||
const bool eligible = options.executable || (bare != name && item.Ordinal != 0 && !special && item.Ordinal <= prx.size() && prx[item.Ordinal - 1]);
|
||||
originalNames.push_back(name);
|
||||
selected.push_back(eligible);
|
||||
if (eligible) names.push_back(bare);
|
||||
}
|
||||
}
|
||||
|
||||
void WriteTrie(std::vector<std::uint8_t>& binary, const Image& image, const TrieLocation& location, const std::vector<std::uint8_t>& trie) {
|
||||
const auto oldOffset = Read<std::uint32_t>(binary, location.OffsetField);
|
||||
const auto oldSize = Read<std::uint32_t>(binary, location.SizeField);
|
||||
if (trie.size() <= oldSize) {
|
||||
std::copy(trie.begin(), trie.end(), binary.begin() + oldOffset);
|
||||
std::fill(binary.begin() + oldOffset + static_cast<std::ptrdiff_t>(trie.size()), binary.begin() + oldOffset + oldSize, 0);
|
||||
Write(binary, location.SizeField, static_cast<std::uint32_t>(trie.size()));
|
||||
const auto mapping = ResolveImportNids(names, excluded, options);
|
||||
if (!options.executable) {
|
||||
std::set<std::uint32_t> nameOffsets;
|
||||
for (const auto& item : imports) nameOffsets.insert(item.NameOffset);
|
||||
std::set<std::uint32_t> rewritten;
|
||||
for (const auto& item : imports) {
|
||||
const bool special = header.ImportsFormat == kImportFormatAddend64 ? item.Ordinal >= 0xfff0 : item.Ordinal >= 0xf0;
|
||||
if (item.Ordinal == 0 || special || item.Ordinal > prx.size() || !prx[item.Ordinal - 1]) continue;
|
||||
if (rewritten.contains(item.NameOffset)) continue;
|
||||
const auto at = symbols + item.NameOffset;
|
||||
if (at >= symbolsEnd) throw std::runtime_error("chained import name out of bounds");
|
||||
const auto name = Internal::ReadCStr(binary, at);
|
||||
if (at + name.size() >= symbolsEnd) throw std::runtime_error("unterminated chained import name");
|
||||
const auto bare = StripUnderscore(name);
|
||||
if (bare == name) continue;
|
||||
const auto patched = "_" + mapping.at(bare);
|
||||
if (patched.size() > name.size()) throw std::runtime_error("import NID longer than original name: " + name);
|
||||
const auto inside = nameOffsets.upper_bound(item.NameOffset);
|
||||
if (inside != nameOffsets.end() && *inside <= item.NameOffset + name.size()) throw std::runtime_error("chained import names share storage: " + name);
|
||||
std::memcpy(binary.data() + at, patched.data(), patched.size());
|
||||
std::fill(binary.begin() + static_cast<std::ptrdiff_t>(at + patched.size()), binary.begin() + static_cast<std::ptrdiff_t>(at + name.size() + 1), 0);
|
||||
rewritten.insert(item.NameOffset);
|
||||
}
|
||||
return;
|
||||
}
|
||||
std::optional<std::size_t> linkedit;
|
||||
for (const auto offset : image.Commands) {
|
||||
const auto cmd = Read<LoadCommand>(binary, offset).Cmd;
|
||||
if (cmd == kLcCodeSignature) throw std::runtime_error("signed Mach-O images cannot grow their export trie");
|
||||
if (cmd != kLcSegment64) continue;
|
||||
const auto segment = Read<SegmentCommand64>(binary, offset);
|
||||
if (std::strncmp(segment.Name, "__LINKEDIT", sizeof(segment.Name)) == 0) linkedit = offset;
|
||||
if (imports.empty()) return;
|
||||
std::vector<std::uint8_t> data(binary.begin() + base, binary.begin() + symbolsEnd);
|
||||
const auto newSymbols = data.size();
|
||||
if (newSymbols > 0xffffffffu) throw std::runtime_error("chained symbol offset overflow");
|
||||
Write(data, offsetof(ChainedFixupsHeader, SymbolsOffset), static_cast<std::uint32_t>(newSymbols));
|
||||
for (std::size_t index = 0; index < imports.size(); ++index) {
|
||||
const auto& original = originalNames[index];
|
||||
const auto name = selected[index] ? (original.starts_with("_") ? "_" : "") + mapping.at(StripUnderscore(original)) : original;
|
||||
const auto offset = data.size() - newSymbols;
|
||||
const auto at = header.ImportsOffset + index * entrySize;
|
||||
if (header.ImportsFormat == kImportFormatAddend64) {
|
||||
if (offset > 0xffffffffu) throw std::runtime_error("chained symbol offset overflow");
|
||||
const auto raw = Read<std::uint64_t>(data, at);
|
||||
Write(data, at, (raw & 0xffffffffu) | (std::uint64_t{offset} << 32));
|
||||
} else {
|
||||
if (offset > 0x7fffffu) throw std::runtime_error("chained symbol offset overflow");
|
||||
const auto raw = Read<std::uint32_t>(data, at);
|
||||
Write(data, at, (raw & 0x1ffu) | (static_cast<std::uint32_t>(offset) << 9));
|
||||
}
|
||||
data.insert(data.end(), name.begin(), name.end());
|
||||
data.push_back(0);
|
||||
}
|
||||
if (!linkedit) throw std::runtime_error("Mach-O image has no __LINKEDIT segment");
|
||||
auto segment = Read<SegmentCommand64>(binary, *linkedit);
|
||||
if (segment.FileOffset + segment.FileSize != binary.size()) throw std::runtime_error("__LINKEDIT does not end the file");
|
||||
const auto newOffset = (binary.size() + 7) & ~std::size_t{7};
|
||||
if (newOffset + trie.size() > 0xffffffffu) throw std::runtime_error("export trie offset overflow");
|
||||
binary.resize(newOffset, 0);
|
||||
binary.insert(binary.end(), trie.begin(), trie.end());
|
||||
std::fill(binary.begin() + oldOffset, binary.begin() + oldOffset + oldSize, 0);
|
||||
segment.FileSize = binary.size() - segment.FileOffset;
|
||||
segment.VmSize = std::max(segment.VmSize, (segment.FileSize + kPageSize - 1) & ~(kPageSize - 1));
|
||||
Write(binary, *linkedit, segment);
|
||||
Write(binary, location.OffsetField, static_cast<std::uint32_t>(newOffset));
|
||||
Write(binary, location.SizeField, static_cast<std::uint32_t>(trie.size()));
|
||||
WriteLinkeditData(binary, image, {commandOffset + 8, commandOffset + 12}, data);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
std::vector<MachOExport> ReadMachOExports(const std::vector<std::uint8_t>& binary) {
|
||||
@@ -464,8 +503,12 @@ std::vector<MachOExport> ReadMachOExports(const std::vector<std::uint8_t>& binar
|
||||
return ParseTrie(binary, Read<std::uint32_t>(binary, location->OffsetField), Read<std::uint32_t>(binary, location->SizeField));
|
||||
}
|
||||
|
||||
void MachONidPatcher::PatchNids(std::vector<std::uint8_t>& binary, const std::string& libraryName, const std::unordered_set<std::string>& excludedExports) const {
|
||||
void MachONidPatcher::PatchNids(std::vector<std::uint8_t>& binary, const std::string& libraryName, const std::unordered_set<std::string>& excludedExports, const PatchOptions& options) const {
|
||||
const auto image = ParseImage(binary);
|
||||
if (options.executable) {
|
||||
PatchChainedImports(binary, image, excludedExports, options);
|
||||
return;
|
||||
}
|
||||
const auto location = FindTrie(binary, image);
|
||||
if (!location) throw std::runtime_error("no export trie");
|
||||
auto exports = ParseTrie(binary, Read<std::uint32_t>(binary, location->OffsetField), Read<std::uint32_t>(binary, location->SizeField));
|
||||
@@ -486,8 +529,8 @@ void MachONidPatcher::PatchNids(std::vector<std::uint8_t>& binary, const std::st
|
||||
if (found == nidMap.end()) throw std::runtime_error("symbol not in nid map: " + name);
|
||||
exports[index].Name = "_" + found->second;
|
||||
}
|
||||
WriteTrie(binary, image, *location, TrieBuilder(exports).Build());
|
||||
PatchChainedImports(binary, ParseImage(binary));
|
||||
WriteLinkeditData(binary, image, *location, TrieBuilder(exports).Build());
|
||||
PatchChainedImports(binary, ParseImage(binary), excludedExports, options);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,21 +3,32 @@
|
||||
#include <nid/NidCompute.hpp>
|
||||
#include <nid/NidPatcherUtils.hpp>
|
||||
#include <cstdint>
|
||||
#include <cctype>
|
||||
#include <stdexcept>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace Nid {
|
||||
|
||||
bool IsSceImport(const std::string& name) {
|
||||
return name.ends_with(Internal::kNidPostfix) || (name.size() >= 3 &&
|
||||
std::tolower(static_cast<unsigned char>(name[0])) == 's' &&
|
||||
std::tolower(static_cast<unsigned char>(name[1])) == 'c' &&
|
||||
std::tolower(static_cast<unsigned char>(name[2])) == 'e');
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, std::string> ResolveImportNids(const std::vector<std::string>& names, const std::unordered_set<std::string>& excluded, const PatchOptions& options) {
|
||||
const std::unordered_set<std::string> unique(names.begin(), names.end());
|
||||
auto result = ResolveNids(std::vector<std::string>(unique.begin(), unique.end()), "", excluded);
|
||||
for (const auto& name : unique) {
|
||||
if ((!options.executable || options.onlySce) && !IsSceImport(name)) result.at(name) = name;
|
||||
else if (!options.executable) result.at(name) = ResolveOneName(name);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string ResolveOneName(const std::string& funcName) {
|
||||
using namespace Internal;
|
||||
|
||||
if (IsNidNoPatchCut(funcName))
|
||||
return StripNidNoPatchCut(funcName);
|
||||
if (IsNidNoPatch(funcName))
|
||||
return funcName;
|
||||
|
||||
return ComputeNid(StripNidPostfix(funcName), "");
|
||||
return ResolveNids({funcName}, "", {}).at(funcName);
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, std::string> ResolveNids(const std::vector<std::string>& exportedNames, const std::string& libraryName, const std::unordered_set<std::string>& excludedExports) {
|
||||
|
||||
+231
-158
@@ -36,7 +36,7 @@ std::size_t RvaToOffset(const std::vector<std::uint8_t>& pe, std::uint32_t rva,
|
||||
|
||||
}
|
||||
|
||||
void PeNidPatcher::PatchNids(std::vector<std::uint8_t>& pe, const std::string& libraryName, const std::unordered_set<std::string>& excludedExports) const {
|
||||
void PeNidPatcher::PatchNids(std::vector<std::uint8_t>& pe, const std::string& libraryName, const std::unordered_set<std::string>& excludedExports, const PatchOptions& options) const {
|
||||
using namespace Internal;
|
||||
|
||||
if (pe.size() < 0x40) throw std::runtime_error("file too small");
|
||||
@@ -54,7 +54,7 @@ void PeNidPatcher::PatchNids(std::vector<std::uint8_t>& pe, const std::string& l
|
||||
const std::size_t sectionTableOffset = coffHeaderOffset + 20u + sizeOfOptionalHeader;
|
||||
const auto symbolTable = Read<std::uint32_t>(pe, coffHeaderOffset + 8u);
|
||||
const auto symbolCount = Read<std::uint32_t>(pe, coffHeaderOffset + 12u);
|
||||
for (std::uint16_t i = 0; i < numberOfSections; ++i) {
|
||||
for (std::uint16_t i = 0; !options.executable && i < numberOfSections; ++i) {
|
||||
const auto offset = sectionTableOffset + i * sizeof(PeSectionHeader);
|
||||
auto section = Read<PeSectionHeader>(pe, offset);
|
||||
if (pe[offset] != '/' || !symbolTable) continue;
|
||||
@@ -79,159 +79,162 @@ void PeNidPatcher::PatchNids(std::vector<std::uint8_t>& pe, const std::string& l
|
||||
throw std::runtime_error("unsupported optional header magic");
|
||||
}
|
||||
|
||||
const auto exportDir = Read<PeDataDirectory>(pe, dataDirectoryOffset);
|
||||
if (exportDir.VirtualAddress == 0u) throw std::runtime_error("no export directory");
|
||||
if (!options.executable) {
|
||||
const auto exportDir = Read<PeDataDirectory>(pe, dataDirectoryOffset);
|
||||
if (exportDir.VirtualAddress == 0u) throw std::runtime_error("no export directory");
|
||||
|
||||
const std::size_t exportDirOffset = RvaToOffset(pe, exportDir.VirtualAddress, peHeaderOffset, numberOfSections, sizeOfOptionalHeader);
|
||||
const auto exportTable = Read<PeExportDirectory>(pe, exportDirOffset);
|
||||
const std::size_t exportDirOffset = RvaToOffset(pe, exportDir.VirtualAddress, peHeaderOffset, numberOfSections, sizeOfOptionalHeader);
|
||||
const auto exportTable = Read<PeExportDirectory>(pe, exportDirOffset);
|
||||
|
||||
if (exportTable.NumberOfNames == 0u) throw std::runtime_error("no exported names");
|
||||
if (exportTable.NumberOfNames == 0u) throw std::runtime_error("no exported names");
|
||||
|
||||
const std::size_t namesArrayOffset = RvaToOffset(pe, exportTable.AddressOfNames, peHeaderOffset, numberOfSections, sizeOfOptionalHeader);
|
||||
const std::size_t ordinalsArrayOffset = RvaToOffset(pe, exportTable.AddressOfNameOrdinals, peHeaderOffset, numberOfSections, sizeOfOptionalHeader);
|
||||
const std::size_t namesArrayOffset = RvaToOffset(pe, exportTable.AddressOfNames, peHeaderOffset, numberOfSections, sizeOfOptionalHeader);
|
||||
const std::size_t ordinalsArrayOffset = RvaToOffset(pe, exportTable.AddressOfNameOrdinals, peHeaderOffset, numberOfSections, sizeOfOptionalHeader);
|
||||
|
||||
std::vector<std::uint32_t> nameRvas(exportTable.NumberOfNames);
|
||||
std::vector<std::string> names(exportTable.NumberOfNames);
|
||||
for (std::uint32_t i = 0u; i < exportTable.NumberOfNames; ++i) {
|
||||
const auto nameRva = Read<std::uint32_t>(pe, namesArrayOffset + i * 4u);
|
||||
const std::size_t nameOffset = RvaToOffset(pe, nameRva, peHeaderOffset, numberOfSections, sizeOfOptionalHeader);
|
||||
const std::string name = ReadCStr(pe, nameOffset);
|
||||
if (name.empty()) throw std::runtime_error("empty exported name");
|
||||
nameRvas[i] = nameRva;
|
||||
names[i] = name;
|
||||
}
|
||||
|
||||
const auto nidMap = ResolveNids(names, libraryName, excludedExports);
|
||||
|
||||
const std::size_t edataSectionOffset = FindSectionOffsetByRva(pe, exportDir.VirtualAddress, peHeaderOffset, numberOfSections, sizeOfOptionalHeader);
|
||||
const auto edataSection = Read<PeSectionHeader>(pe, edataSectionOffset);
|
||||
|
||||
const auto fileAlignment = Read<std::uint32_t>(pe, optionalHeaderOffset + 36u);
|
||||
if (fileAlignment == 0u) throw std::runtime_error("invalid file alignment");
|
||||
|
||||
std::uint32_t stringsRegionStart = std::numeric_limits<std::uint32_t>::max();
|
||||
for (std::uint32_t i = 0u; i < exportTable.NumberOfNames; ++i) {
|
||||
const std::uint32_t rvaFromSection = nameRvas[i] - edataSection.VirtualAddress;
|
||||
if (rvaFromSection < stringsRegionStart) stringsRegionStart = rvaFromSection;
|
||||
}
|
||||
if (stringsRegionStart == std::numeric_limits<std::uint32_t>::max())
|
||||
throw std::runtime_error("could not determine strings region start");
|
||||
|
||||
std::uint32_t rawLimit = ((edataSection.SizeOfRawData + fileAlignment - 1u) / fileAlignment) * fileAlignment;
|
||||
const auto dllName = ReadCStr(pe, RvaToOffset(pe, exportTable.Name, peHeaderOffset, numberOfSections, sizeOfOptionalHeader));
|
||||
const bool repackDllName = exportTable.Name >= edataSection.VirtualAddress &&
|
||||
exportTable.Name - edataSection.VirtualAddress < rawLimit;
|
||||
if (repackDllName) stringsRegionStart = std::min(stringsRegionStart, exportTable.Name - edataSection.VirtualAddress);
|
||||
struct Forwarder {
|
||||
std::size_t EntryOffset;
|
||||
std::string Target;
|
||||
};
|
||||
std::vector<Forwarder> forwarders;
|
||||
std::vector<std::uint32_t> directRvas;
|
||||
const auto functionsArrayOffset = RvaToOffset(pe, exportTable.AddressOfFunctions, peHeaderOffset, numberOfSections, sizeOfOptionalHeader);
|
||||
for (std::uint32_t index = 0u; index < exportTable.NumberOfFunctions; ++index) {
|
||||
const auto entryOffset = functionsArrayOffset + index * sizeof(std::uint32_t);
|
||||
const auto rva = Read<std::uint32_t>(pe, entryOffset);
|
||||
if (rva < exportDir.VirtualAddress || std::uint64_t{rva} - exportDir.VirtualAddress >= exportDir.Size) {
|
||||
directRvas.push_back(rva);
|
||||
continue;
|
||||
std::vector<std::uint32_t> nameRvas(exportTable.NumberOfNames);
|
||||
std::vector<std::string> names(exportTable.NumberOfNames);
|
||||
for (std::uint32_t i = 0u; i < exportTable.NumberOfNames; ++i) {
|
||||
const auto nameRva = Read<std::uint32_t>(pe, namesArrayOffset + i * 4u);
|
||||
const std::size_t nameOffset = RvaToOffset(pe, nameRva, peHeaderOffset, numberOfSections, sizeOfOptionalHeader);
|
||||
const std::string name = ReadCStr(pe, nameOffset);
|
||||
if (name.empty()) throw std::runtime_error("empty exported name");
|
||||
nameRvas[i] = nameRva;
|
||||
names[i] = name;
|
||||
}
|
||||
const auto target = ReadCStr(pe, RvaToOffset(pe, rva, peHeaderOffset, numberOfSections, sizeOfOptionalHeader));
|
||||
if (target.size() + 1u > exportDir.Size - (rva - exportDir.VirtualAddress))
|
||||
throw std::runtime_error("forwarder string outside export directory");
|
||||
forwarders.push_back({entryOffset, target});
|
||||
stringsRegionStart = std::min(stringsRegionStart, rva - edataSection.VirtualAddress);
|
||||
}
|
||||
for (const auto rva : directRvas) {
|
||||
if (rva >= edataSection.VirtualAddress && rva - edataSection.VirtualAddress >= stringsRegionStart)
|
||||
rawLimit = std::min(rawLimit, rva - edataSection.VirtualAddress);
|
||||
}
|
||||
const auto reserveTable = [&](std::uint32_t rva, std::uint64_t size) {
|
||||
if (rva < edataSection.VirtualAddress || rva - edataSection.VirtualAddress >= rawLimit) return;
|
||||
const auto end = std::uint64_t{rva - edataSection.VirtualAddress} + size;
|
||||
if (end > rawLimit) throw std::runtime_error("export table outside edata section");
|
||||
stringsRegionStart = std::max(stringsRegionStart, static_cast<std::uint32_t>(end));
|
||||
};
|
||||
reserveTable(exportDir.VirtualAddress, sizeof(PeExportDirectory));
|
||||
reserveTable(exportTable.AddressOfFunctions, std::uint64_t{exportTable.NumberOfFunctions} * 4u);
|
||||
reserveTable(exportTable.AddressOfNames, std::uint64_t{exportTable.NumberOfNames} * 4u);
|
||||
reserveTable(exportTable.AddressOfNameOrdinals, std::uint64_t{exportTable.NumberOfNames} * 2u);
|
||||
|
||||
std::vector<std::string> finalNames(exportTable.NumberOfNames);
|
||||
for (std::uint32_t i = 0u; i < exportTable.NumberOfNames; ++i) {
|
||||
const auto it = nidMap.find(names[i]);
|
||||
if (it == nidMap.end()) throw std::runtime_error("symbol not in nid map: " + names[i]);
|
||||
finalNames[i] = it->second;
|
||||
}
|
||||
const auto nidMap = ResolveNids(names, libraryName, excludedExports);
|
||||
|
||||
std::vector<std::uint8_t> strings;
|
||||
std::unordered_map<std::string, std::uint32_t> stringRvas;
|
||||
const auto storeString = [&](const std::string& name) {
|
||||
const auto existing = stringRvas.find(name);
|
||||
if (existing != stringRvas.end()) return existing->second;
|
||||
const auto rva = edataSection.VirtualAddress + stringsRegionStart + static_cast<std::uint32_t>(strings.size());
|
||||
stringRvas.emplace(name, rva);
|
||||
strings.insert(strings.end(), name.begin(), name.end());
|
||||
strings.push_back(0u);
|
||||
return rva;
|
||||
};
|
||||
std::vector<std::uint32_t> forwarderRvas;
|
||||
for (const auto& forwarder : forwarders) forwarderRvas.push_back(storeString(forwarder.Target));
|
||||
if (!forwarders.empty() && (std::uint64_t{edataSection.VirtualAddress} + stringsRegionStart < exportDir.VirtualAddress ||
|
||||
std::uint64_t{edataSection.VirtualAddress} + stringsRegionStart + strings.size() > std::uint64_t{exportDir.VirtualAddress} + exportDir.Size))
|
||||
throw std::runtime_error("not enough export directory space to repack forwarders");
|
||||
const auto dllNameRva = repackDllName ? storeString(dllName) : exportTable.Name;
|
||||
std::vector<std::uint32_t> finalNameRvas;
|
||||
for (const auto& name : finalNames) finalNameRvas.push_back(storeString(name));
|
||||
const auto requiredSize = std::uint64_t{stringsRegionStart} + strings.size();
|
||||
if (requiredSize > rawLimit)
|
||||
throw std::runtime_error("not enough raw space in edata section to repack export names");
|
||||
const auto directoryEnd = std::max(std::uint64_t{exportDir.VirtualAddress} + exportDir.Size,
|
||||
std::uint64_t{edataSection.VirtualAddress} + requiredSize);
|
||||
for (const auto rva : directRvas) {
|
||||
if (rva != 0u && rva >= exportDir.VirtualAddress && std::uint64_t{rva} < directoryEnd)
|
||||
throw std::runtime_error("repacked export directory would cover a direct export");
|
||||
}
|
||||
const auto directorySize = directoryEnd - exportDir.VirtualAddress;
|
||||
if (directorySize > std::numeric_limits<std::uint32_t>::max()) throw std::runtime_error("export directory too large");
|
||||
const auto stringFileOffset = static_cast<std::size_t>(edataSection.PointerToRawData) + stringsRegionStart;
|
||||
if (stringFileOffset > pe.size() || strings.size() > pe.size() - stringFileOffset)
|
||||
throw std::runtime_error("repack write out of file bounds");
|
||||
std::memcpy(pe.data() + stringFileOffset, strings.data(), strings.size());
|
||||
for (std::size_t index = 0u; index < finalNameRvas.size(); ++index)
|
||||
Write(pe, namesArrayOffset + index * 4u, finalNameRvas[index]);
|
||||
for (std::size_t index = 0u; index < forwarders.size(); ++index)
|
||||
Write(pe, forwarders[index].EntryOffset, forwarderRvas[index]);
|
||||
Write(pe, exportDirOffset + offsetof(PeExportDirectory, Name), dllNameRva);
|
||||
Write(pe, dataDirectoryOffset + offsetof(PeDataDirectory, Size), static_cast<std::uint32_t>(directorySize));
|
||||
const std::size_t edataSectionOffset = FindSectionOffsetByRva(pe, exportDir.VirtualAddress, peHeaderOffset, numberOfSections, sizeOfOptionalHeader);
|
||||
const auto edataSection = Read<PeSectionHeader>(pe, edataSectionOffset);
|
||||
|
||||
const std::uint32_t newVirtualSize = std::max(edataSection.VirtualSize, static_cast<std::uint32_t>(requiredSize));
|
||||
auto patchedSection = edataSection;
|
||||
patchedSection.VirtualSize = newVirtualSize;
|
||||
Write(pe, edataSectionOffset, patchedSection);
|
||||
const auto fileAlignment = Read<std::uint32_t>(pe, optionalHeaderOffset + 36u);
|
||||
if (fileAlignment == 0u) throw std::runtime_error("invalid file alignment");
|
||||
|
||||
struct ExportName {
|
||||
std::string Name;
|
||||
std::uint32_t Rva;
|
||||
std::uint16_t Ordinal;
|
||||
};
|
||||
std::uint32_t stringsRegionStart = std::numeric_limits<std::uint32_t>::max();
|
||||
for (std::uint32_t i = 0u; i < exportTable.NumberOfNames; ++i) {
|
||||
const std::uint32_t rvaFromSection = nameRvas[i] - edataSection.VirtualAddress;
|
||||
if (rvaFromSection < stringsRegionStart) stringsRegionStart = rvaFromSection;
|
||||
}
|
||||
if (stringsRegionStart == std::numeric_limits<std::uint32_t>::max())
|
||||
throw std::runtime_error("could not determine strings region start");
|
||||
|
||||
std::uint32_t rawLimit = ((edataSection.SizeOfRawData + fileAlignment - 1u) / fileAlignment) * fileAlignment;
|
||||
const auto dllName = ReadCStr(pe, RvaToOffset(pe, exportTable.Name, peHeaderOffset, numberOfSections, sizeOfOptionalHeader));
|
||||
const bool repackDllName = exportTable.Name >= edataSection.VirtualAddress &&
|
||||
exportTable.Name - edataSection.VirtualAddress < rawLimit;
|
||||
if (repackDllName) stringsRegionStart = std::min(stringsRegionStart, exportTable.Name - edataSection.VirtualAddress);
|
||||
struct Forwarder {
|
||||
std::size_t EntryOffset;
|
||||
std::string Target;
|
||||
};
|
||||
std::vector<Forwarder> forwarders;
|
||||
std::vector<std::uint32_t> directRvas;
|
||||
const auto functionsArrayOffset = RvaToOffset(pe, exportTable.AddressOfFunctions, peHeaderOffset, numberOfSections, sizeOfOptionalHeader);
|
||||
for (std::uint32_t index = 0u; index < exportTable.NumberOfFunctions; ++index) {
|
||||
const auto entryOffset = functionsArrayOffset + index * sizeof(std::uint32_t);
|
||||
const auto rva = Read<std::uint32_t>(pe, entryOffset);
|
||||
if (rva < exportDir.VirtualAddress || std::uint64_t{rva} - exportDir.VirtualAddress >= exportDir.Size) {
|
||||
directRvas.push_back(rva);
|
||||
continue;
|
||||
}
|
||||
const auto target = ReadCStr(pe, RvaToOffset(pe, rva, peHeaderOffset, numberOfSections, sizeOfOptionalHeader));
|
||||
if (target.size() + 1u > exportDir.Size - (rva - exportDir.VirtualAddress))
|
||||
throw std::runtime_error("forwarder string outside export directory");
|
||||
forwarders.push_back({entryOffset, target});
|
||||
stringsRegionStart = std::min(stringsRegionStart, rva - edataSection.VirtualAddress);
|
||||
}
|
||||
for (const auto rva : directRvas) {
|
||||
if (rva >= edataSection.VirtualAddress && rva - edataSection.VirtualAddress >= stringsRegionStart)
|
||||
rawLimit = std::min(rawLimit, rva - edataSection.VirtualAddress);
|
||||
}
|
||||
const auto reserveTable = [&](std::uint32_t rva, std::uint64_t size) {
|
||||
if (rva < edataSection.VirtualAddress || rva - edataSection.VirtualAddress >= rawLimit) return;
|
||||
const auto end = std::uint64_t{rva - edataSection.VirtualAddress} + size;
|
||||
if (end > rawLimit) throw std::runtime_error("export table outside edata section");
|
||||
stringsRegionStart = std::max(stringsRegionStart, static_cast<std::uint32_t>(end));
|
||||
};
|
||||
reserveTable(exportDir.VirtualAddress, sizeof(PeExportDirectory));
|
||||
reserveTable(exportTable.AddressOfFunctions, std::uint64_t{exportTable.NumberOfFunctions} * 4u);
|
||||
reserveTable(exportTable.AddressOfNames, std::uint64_t{exportTable.NumberOfNames} * 4u);
|
||||
reserveTable(exportTable.AddressOfNameOrdinals, std::uint64_t{exportTable.NumberOfNames} * 2u);
|
||||
|
||||
std::vector<std::string> finalNames(exportTable.NumberOfNames);
|
||||
for (std::uint32_t i = 0u; i < exportTable.NumberOfNames; ++i) {
|
||||
const auto it = nidMap.find(names[i]);
|
||||
if (it == nidMap.end()) throw std::runtime_error("symbol not in nid map: " + names[i]);
|
||||
finalNames[i] = it->second;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> strings;
|
||||
std::unordered_map<std::string, std::uint32_t> stringRvas;
|
||||
const auto storeString = [&](const std::string& name) {
|
||||
const auto existing = stringRvas.find(name);
|
||||
if (existing != stringRvas.end()) return existing->second;
|
||||
const auto rva = edataSection.VirtualAddress + stringsRegionStart + static_cast<std::uint32_t>(strings.size());
|
||||
stringRvas.emplace(name, rva);
|
||||
strings.insert(strings.end(), name.begin(), name.end());
|
||||
strings.push_back(0u);
|
||||
return rva;
|
||||
};
|
||||
std::vector<std::uint32_t> forwarderRvas;
|
||||
for (const auto& forwarder : forwarders) forwarderRvas.push_back(storeString(forwarder.Target));
|
||||
if (!forwarders.empty() && (std::uint64_t{edataSection.VirtualAddress} + stringsRegionStart < exportDir.VirtualAddress ||
|
||||
std::uint64_t{edataSection.VirtualAddress} + stringsRegionStart + strings.size() > std::uint64_t{exportDir.VirtualAddress} + exportDir.Size))
|
||||
throw std::runtime_error("not enough export directory space to repack forwarders");
|
||||
const auto dllNameRva = repackDllName ? storeString(dllName) : exportTable.Name;
|
||||
std::vector<std::uint32_t> finalNameRvas;
|
||||
for (const auto& name : finalNames) finalNameRvas.push_back(storeString(name));
|
||||
const auto requiredSize = std::uint64_t{stringsRegionStart} + strings.size();
|
||||
if (requiredSize > rawLimit)
|
||||
throw std::runtime_error("not enough raw space in edata section to repack export names");
|
||||
const auto directoryEnd = std::max(std::uint64_t{exportDir.VirtualAddress} + exportDir.Size,
|
||||
std::uint64_t{edataSection.VirtualAddress} + requiredSize);
|
||||
for (const auto rva : directRvas) {
|
||||
if (rva != 0u && rva >= exportDir.VirtualAddress && std::uint64_t{rva} < directoryEnd)
|
||||
throw std::runtime_error("repacked export directory would cover a direct export");
|
||||
}
|
||||
const auto directorySize = directoryEnd - exportDir.VirtualAddress;
|
||||
if (directorySize > std::numeric_limits<std::uint32_t>::max()) throw std::runtime_error("export directory too large");
|
||||
const auto stringFileOffset = static_cast<std::size_t>(edataSection.PointerToRawData) + stringsRegionStart;
|
||||
if (stringFileOffset > pe.size() || strings.size() > pe.size() - stringFileOffset)
|
||||
throw std::runtime_error("repack write out of file bounds");
|
||||
std::memcpy(pe.data() + stringFileOffset, strings.data(), strings.size());
|
||||
for (std::size_t index = 0u; index < finalNameRvas.size(); ++index)
|
||||
Write(pe, namesArrayOffset + index * 4u, finalNameRvas[index]);
|
||||
for (std::size_t index = 0u; index < forwarders.size(); ++index)
|
||||
Write(pe, forwarders[index].EntryOffset, forwarderRvas[index]);
|
||||
Write(pe, exportDirOffset + offsetof(PeExportDirectory, Name), dllNameRva);
|
||||
Write(pe, dataDirectoryOffset + offsetof(PeDataDirectory, Size), static_cast<std::uint32_t>(directorySize));
|
||||
|
||||
const std::uint32_t newVirtualSize = std::max(edataSection.VirtualSize, static_cast<std::uint32_t>(requiredSize));
|
||||
auto patchedSection = edataSection;
|
||||
patchedSection.VirtualSize = newVirtualSize;
|
||||
Write(pe, edataSectionOffset, patchedSection);
|
||||
|
||||
struct ExportName {
|
||||
std::string Name;
|
||||
std::uint32_t Rva;
|
||||
std::uint16_t Ordinal;
|
||||
};
|
||||
|
||||
std::vector<ExportName> exportNames;
|
||||
exportNames.reserve(exportTable.NumberOfNames);
|
||||
for (std::size_t i = 0; i < exportTable.NumberOfNames; ++i) {
|
||||
const auto nameRva = Read<std::uint32_t>(pe, namesArrayOffset + i * sizeof(std::uint32_t));
|
||||
const auto ordinal = Read<std::uint16_t>(pe, ordinalsArrayOffset + i * sizeof(std::uint16_t));
|
||||
if (ordinal >= exportTable.NumberOfFunctions)
|
||||
throw std::runtime_error("export name ordinal out of bounds");
|
||||
const auto nameOffset = RvaToOffset(pe, nameRva, peHeaderOffset, numberOfSections, sizeOfOptionalHeader);
|
||||
exportNames.push_back({ReadCStr(pe, nameOffset), nameRva, ordinal});
|
||||
}
|
||||
std::sort(exportNames.begin(), exportNames.end(), [](const ExportName& left, const ExportName& right) { return left.Name < right.Name; });
|
||||
for (std::size_t i = 0; i < exportNames.size(); ++i) {
|
||||
if (i != 0 && exportNames[i - 1].Name == exportNames[i].Name)
|
||||
throw std::runtime_error("duplicate patched export name: " + exportNames[i].Name);
|
||||
Write(pe, namesArrayOffset + i * sizeof(std::uint32_t), exportNames[i].Rva);
|
||||
Write(pe, ordinalsArrayOffset + i * sizeof(std::uint16_t), exportNames[i].Ordinal);
|
||||
}
|
||||
|
||||
std::vector<ExportName> exportNames;
|
||||
exportNames.reserve(exportTable.NumberOfNames);
|
||||
for (std::size_t i = 0; i < exportTable.NumberOfNames; ++i) {
|
||||
const auto nameRva = Read<std::uint32_t>(pe, namesArrayOffset + i * sizeof(std::uint32_t));
|
||||
const auto ordinal = Read<std::uint16_t>(pe, ordinalsArrayOffset + i * sizeof(std::uint16_t));
|
||||
if (ordinal >= exportTable.NumberOfFunctions)
|
||||
throw std::runtime_error("export name ordinal out of bounds");
|
||||
const auto nameOffset = RvaToOffset(pe, nameRva, peHeaderOffset, numberOfSections, sizeOfOptionalHeader);
|
||||
exportNames.push_back({ReadCStr(pe, nameOffset), nameRva, ordinal});
|
||||
}
|
||||
std::sort(exportNames.begin(), exportNames.end(), [](const ExportName& left, const ExportName& right) { return left.Name < right.Name; });
|
||||
for (std::size_t i = 0; i < exportNames.size(); ++i) {
|
||||
if (i != 0 && exportNames[i - 1].Name == exportNames[i].Name)
|
||||
throw std::runtime_error("duplicate patched export name: " + exportNames[i].Name);
|
||||
Write(pe, namesArrayOffset + i * sizeof(std::uint32_t), exportNames[i].Rva);
|
||||
Write(pe, ordinalsArrayOffset + i * sizeof(std::uint16_t), exportNames[i].Ordinal);
|
||||
}
|
||||
|
||||
constexpr std::size_t kImportDirIndex = 1u;
|
||||
@@ -250,6 +253,20 @@ void PeNidPatcher::PatchNids(std::vector<std::uint8_t>& pe, const std::string& l
|
||||
std::uint32_t FirstThunk;
|
||||
};
|
||||
|
||||
if (options.executable) {
|
||||
for (const auto index : {4u, 11u, 13u}) {
|
||||
const auto directory = Read<PeDataDirectory>(pe, dataDirectoryOffset + index * sizeof(PeDataDirectory));
|
||||
if (directory.VirtualAddress || directory.Size) throw std::runtime_error("signed, bound or delay-loaded PE imports are not supported in --exec mode");
|
||||
}
|
||||
}
|
||||
struct ImportUse {
|
||||
std::size_t NameOffset;
|
||||
std::size_t LookupOffset;
|
||||
std::size_t AddressOffset;
|
||||
std::string Name;
|
||||
};
|
||||
std::vector<ImportUse> imports;
|
||||
std::vector<std::string> importNames;
|
||||
std::size_t importDescOffset = RvaToOffset(pe, importDir.VirtualAddress, peHeaderOffset, numberOfSections, sizeOfOptionalHeader);
|
||||
|
||||
while (true) {
|
||||
@@ -286,19 +303,9 @@ void PeNidPatcher::PatchNids(std::vector<std::uint8_t>& pe, const std::string& l
|
||||
|
||||
const std::string funcName = ReadCStr(pe, hintNameOffset + 2u);
|
||||
if (!funcName.empty()) {
|
||||
const bool hasNidPostfix = funcName.size() >= kNidPostfixLen &&
|
||||
funcName.compare(funcName.size() - kNidPostfixLen, kNidPostfixLen, kNidPostfix) == 0;
|
||||
const bool hasScePrefix = funcName.size() >= 3u &&
|
||||
std::tolower(static_cast<unsigned char>(funcName[0])) == 's' &&
|
||||
std::tolower(static_cast<unsigned char>(funcName[1])) == 'c' &&
|
||||
std::tolower(static_cast<unsigned char>(funcName[2])) == 'e';
|
||||
if (hasNidPostfix || hasScePrefix) {
|
||||
const std::string nid = ResolveOneName(funcName);
|
||||
if (nid.size() > funcName.size())
|
||||
throw std::runtime_error("import NID longer than original name: " + funcName);
|
||||
std::memcpy(pe.data() + hintNameOffset + 2u, nid.data(), nid.size());
|
||||
pe[hintNameOffset + 2u + nid.size()] = 0u;
|
||||
}
|
||||
const auto addressOffset = RvaToOffset(pe, desc.FirstThunk, peHeaderOffset, numberOfSections, sizeOfOptionalHeader) + thunkOffset - RvaToOffset(pe, thunkRva, peHeaderOffset, numberOfSections, sizeOfOptionalHeader);
|
||||
imports.push_back({hintNameOffset + 2u, thunkOffset, addressOffset, funcName});
|
||||
importNames.push_back(funcName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,6 +314,72 @@ void PeNidPatcher::PatchNids(std::vector<std::uint8_t>& pe, const std::string& l
|
||||
|
||||
importDescOffset += sizeof(PeImportDescriptor);
|
||||
}
|
||||
const auto importMap = ResolveImportNids(importNames, excludedExports, options);
|
||||
if (!options.executable) {
|
||||
for (const auto& item : imports) {
|
||||
const auto& name = importMap.at(item.Name);
|
||||
if (name.size() > item.Name.size()) throw std::runtime_error("import NID longer than original name: " + item.Name);
|
||||
std::memcpy(pe.data() + item.NameOffset, name.c_str(), name.size() + 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
std::vector<std::uint8_t> strings;
|
||||
std::unordered_map<std::string, std::uint32_t> stringOffsets;
|
||||
for (const auto& item : imports) {
|
||||
const auto& name = importMap.at(item.Name);
|
||||
if (name == item.Name || stringOffsets.contains(name)) continue;
|
||||
if (strings.size() & 1u) strings.push_back(0);
|
||||
if (strings.size() > 0xffffffffu - name.size() - 3u) throw std::runtime_error("PE import string table overflow");
|
||||
stringOffsets.emplace(name, static_cast<std::uint32_t>(strings.size()));
|
||||
strings.insert(strings.end(), {0, 0});
|
||||
strings.insert(strings.end(), name.begin(), name.end());
|
||||
strings.push_back(0);
|
||||
}
|
||||
if (strings.empty()) return;
|
||||
const auto fileAlignment = Read<std::uint32_t>(pe, optionalHeaderOffset + 36u);
|
||||
const auto sectionAlignment = Read<std::uint32_t>(pe, optionalHeaderOffset + 32u);
|
||||
const auto align = [](std::uint64_t value, std::uint32_t alignment) {
|
||||
if (!alignment || (alignment & (alignment - 1u))) throw std::runtime_error("invalid PE alignment");
|
||||
const auto result = (value + alignment - 1u) & ~std::uint64_t{alignment - 1u};
|
||||
if (result > 0xffffffffu) throw std::runtime_error("PE section overflow");
|
||||
return static_cast<std::uint32_t>(result);
|
||||
};
|
||||
const auto newHeader = sectionTableOffset + numberOfSections * sizeof(PeSectionHeader);
|
||||
auto headerLimit = std::size_t{Read<std::uint32_t>(pe, optionalHeaderOffset + 60u)};
|
||||
std::uint64_t virtualEnd = headerLimit;
|
||||
for (std::uint16_t index = 0; index < numberOfSections; ++index) {
|
||||
const auto section = Read<PeSectionHeader>(pe, sectionTableOffset + index * sizeof(PeSectionHeader));
|
||||
if (section.PointerToRawData) headerLimit = std::min(headerLimit, std::size_t{section.PointerToRawData});
|
||||
virtualEnd = std::max(virtualEnd, std::uint64_t{section.VirtualAddress} + std::max(section.VirtualSize, section.SizeOfRawData));
|
||||
}
|
||||
if (numberOfSections == 0xffffu || newHeader + sizeof(PeSectionHeader) > headerLimit || headerLimit > pe.size()) throw std::runtime_error("no room for PE import section header");
|
||||
PeSectionHeader section{};
|
||||
const char sectionName[] = ".nidimp";
|
||||
std::memcpy(section.Name, sectionName, sizeof(sectionName));
|
||||
section.VirtualSize = static_cast<std::uint32_t>(strings.size());
|
||||
section.VirtualAddress = align(virtualEnd, sectionAlignment);
|
||||
section.PointerToRawData = align(pe.size(), fileAlignment);
|
||||
section.SizeOfRawData = align(strings.size(), fileAlignment);
|
||||
section.Characteristics = 0x40000040u;
|
||||
const auto imageSize = align(std::uint64_t{section.VirtualAddress} + section.VirtualSize, sectionAlignment);
|
||||
const auto rawEnd = std::uint64_t{section.PointerToRawData} + section.SizeOfRawData;
|
||||
if (rawEnd > 0xffffffffu) throw std::runtime_error("PE file size overflow");
|
||||
pe.resize(static_cast<std::size_t>(rawEnd), 0);
|
||||
std::copy(strings.begin(), strings.end(), pe.begin() + section.PointerToRawData);
|
||||
Write(pe, newHeader, section);
|
||||
Write(pe, coffHeaderOffset + 2u, static_cast<std::uint16_t>(numberOfSections + 1u));
|
||||
Write(pe, optionalHeaderOffset + 56u, imageSize);
|
||||
Write(pe, optionalHeaderOffset + 64u, std::uint32_t{0});
|
||||
for (const auto& item : imports) {
|
||||
const auto& name = importMap.at(item.Name);
|
||||
if (name == item.Name) continue;
|
||||
const auto rva = section.VirtualAddress + stringOffsets.at(name);
|
||||
for (const auto offset : {item.LookupOffset, item.AddressOffset}) {
|
||||
if (magic == 0x20bu) Write(pe, offset, std::uint64_t{rva});
|
||||
else Write(pe, offset, rva);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -28,6 +28,7 @@ void _writeFile(const std::string& path, const std::vector<std::uint8_t>& data)
|
||||
int Run(int argc, char* argv[]) {
|
||||
if (argc < 3) {
|
||||
std::cerr << "Usage: nid_patcher <library_name> [--preserve-exports <unpatched-library> | --preserve-export-list <export-list>] <file> [file2 ...]\n"
|
||||
" nid_patcher --exec [--only-sce] [--preserve-exports <unpatched-library> | --preserve-export-list <export-list>] <file> [file2 ...]\n"
|
||||
" nid_patcher --write-export-list <unpatched-library> <export-list>\n";
|
||||
return 1;
|
||||
}
|
||||
@@ -46,14 +47,20 @@ int Run(int argc, char* argv[]) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const std::string libraryName = argv[1];
|
||||
Nid::PatchOptions options;
|
||||
options.executable = std::string(argv[1]) == "--exec";
|
||||
const std::string libraryName = options.executable ? "" : argv[1];
|
||||
std::unordered_set<std::string> excludedExports;
|
||||
std::vector<std::string> paths;
|
||||
try {
|
||||
if (!options.executable && libraryName.starts_with("--")) throw std::runtime_error("unknown option: " + libraryName);
|
||||
bool hasReference = false;
|
||||
for (int i = 2; i < argc; ++i) {
|
||||
const std::string argument = argv[i];
|
||||
if (argument == "--preserve-exports" || argument == "--preserve-export-list") {
|
||||
if (argument == "--only-sce") {
|
||||
if (!options.executable || options.onlySce) throw std::runtime_error("--only-sce requires --exec and may appear once");
|
||||
options.onlySce = true;
|
||||
} else if (argument == "--preserve-exports" || argument == "--preserve-export-list") {
|
||||
if (hasReference || i + 1 == argc) throw std::runtime_error(argument + " requires exactly one reference");
|
||||
hasReference = true;
|
||||
excludedExports = argument == "--preserve-exports" ? Nid::ReadExportExclusions(argv[++i]) : Nid::ReadExportList(argv[++i]);
|
||||
@@ -72,7 +79,7 @@ int Run(int argc, char* argv[]) {
|
||||
try {
|
||||
auto binary = _readFile(path);
|
||||
const auto patcher = Nid::MakePatcher(binary);
|
||||
patcher->PatchNids(binary, libraryName, excludedExports);
|
||||
patcher->PatchNids(binary, libraryName, excludedExports, options);
|
||||
_writeFile(path, binary);
|
||||
std::cout << "OK: " << path << "\n";
|
||||
} catch (const std::exception& e) {
|
||||
|
||||
@@ -263,7 +263,7 @@ def check_export_list(patcher, directory):
|
||||
def check_refusals(patcher, directory):
|
||||
growing = [("_ab", 0, 0x100), ("_cd", 0, 0x108)]
|
||||
result, _ = patch(patcher, directory, "signed.dylib", build_dylib(growing, [], signed=True))
|
||||
assert result.returncode != 0 and "signed Mach-O images cannot grow their export trie" in result.stdout + result.stderr, (result.stdout, result.stderr)
|
||||
assert result.returncode != 0 and "signed Mach-O images cannot grow linkedit data" in result.stdout + result.stderr, (result.stdout, result.stderr)
|
||||
result, _ = patch(patcher, directory, "opcodes.dylib", build_dylib([("_puts_nid_postfix", 0, 0x100)], [], bind_opcodes=True))
|
||||
assert result.returncode != 0 and "bind opcodes are not supported" in result.stdout + result.stderr, (result.stdout, result.stderr)
|
||||
result, _ = patch(patcher, directory, "long.dylib", build_dylib([("_puts_nid_postfix", 0, 0x100)], [("_sceXy", 1)]))
|
||||
|
||||
@@ -243,5 +243,203 @@ class ForwardingExportTests(unittest.TestCase):
|
||||
self.assertEqual(after, image)
|
||||
|
||||
|
||||
class ExecutableImportTests(unittest.TestCase):
|
||||
names = ["foo", "foo_nid_postfix", "z", "sceExample", "helper_nid_no_patch", "plain_nid_no_patch_cut", "SDL_Init", "scePreserved", "hostFunctionLongName", "alias_nid_disambig12_nid_postfix"]
|
||||
|
||||
def pe(self, bits=64):
|
||||
image = bytearray(2048)
|
||||
image[:2] = b"MZ"
|
||||
struct.pack_into("<I", image, 0x3c, 0x40)
|
||||
optional_size = 240 if bits == 64 else 224
|
||||
struct.pack_into("<IHHIIIHH", image, 0x40, 0x4550, 0x8664 if bits == 64 else 0x14c, 1, 0, 0, 0, optional_size, 0x22)
|
||||
struct.pack_into("<H", image, 0x58, 0x20b if bits == 64 else 0x10b)
|
||||
struct.pack_into("<II", image, 0x58 + 32, 4096, 512)
|
||||
struct.pack_into("<II", image, 0x58 + 56, 8192, 512)
|
||||
directory = 0x58 + (112 if bits == 64 else 96)
|
||||
struct.pack_into("<I", image, directory - 4, 16)
|
||||
struct.pack_into("<II", image, directory + 8, 0x1000, 40)
|
||||
struct.pack_into("<8sIIIIIIHHI", image, 0x58 + optional_size, b".idata", 1536, 0x1000, 1536, 512, 0, 0, 0, 0, 0xc0000040)
|
||||
struct.pack_into("<IIIII", image, 512, 0x1040, 0, 0, 0x1028, 0x10c0)
|
||||
image[552:561] = b"test.prx\0"
|
||||
offset = 0x380
|
||||
for index, name in enumerate(self.names):
|
||||
entry = name.encode() + b"\0"
|
||||
struct.pack_into("<H", image, offset, 0)
|
||||
image[offset + 2:offset + 2 + len(entry)] = entry
|
||||
for table in (576, 704):
|
||||
struct.pack_into("<Q" if bits == 64 else "<I", image, table + index * (bits // 8), offset - 512 + 0x1000)
|
||||
offset += len(entry) + 2
|
||||
return image
|
||||
|
||||
def elf(self):
|
||||
from test_symbol_versions import build_image, SHOFF
|
||||
entries = [(None, 1, 0, 0, 0)] + [(name, 1, 0, 2, 0) for name in self.names] + [("application_export", 1, 9, 2, 0x400100)]
|
||||
import test_symbol_versions
|
||||
from unittest.mock import patch
|
||||
with patch.object(test_symbol_versions, "DYNSYM_OFFSET", 0xa00):
|
||||
image = build_image(entries, False)
|
||||
struct.pack_into("<H", image, 16, 2)
|
||||
struct.pack_into("<Q", image, 32, 64)
|
||||
struct.pack_into("<HH", image, 54, 56, 2)
|
||||
struct.pack_into("<IIQQQQQQ", image, 64, 1, 5, 0, 0x400000, 0x400000, 4096, 8192, 4096)
|
||||
struct.pack_into("<IIQQQQQQ", image, 120, 2, 6, 0x700, 0x400700, 0x400700, 48, 48, 8)
|
||||
struct.pack_into("<H", image, 60, 5)
|
||||
struct.pack_into("<IIQQQQIIQQ", image, SHOFF + 4 * 64, 0, 6, 3, 0x400700, 0x700, 48, 2, 0, 8, 16)
|
||||
size = struct.unpack_from("<Q", image, SHOFF + 2 * 64 + 32)[0]
|
||||
struct.pack_into("<qQqQqQ", image, 0x700, 5, 0x400300, 10, size, 0, 0)
|
||||
return image
|
||||
|
||||
def macho(self):
|
||||
image = bytearray(build_dylib([("_application_export", 0, 0x100)], [("_" + name, 1 if index % 2 else 2) for index, name in enumerate(self.names)]))
|
||||
struct.pack_into("<I", image, 12, 2)
|
||||
return image
|
||||
|
||||
def read_names(self, image, kind):
|
||||
if kind == "elf":
|
||||
from test_symbol_versions import read_dynsym
|
||||
return [name for name, section, _ in read_dynsym(image) if name and section == 0]
|
||||
if kind == "macho":
|
||||
from test_macho_nids import chained_imports
|
||||
return [name[1:] for name, _ in chained_imports(image)]
|
||||
bits = 64 if kind == "pe64" else 32
|
||||
optional_size = 240 if bits == 64 else 224
|
||||
count = struct.unpack_from("<H", image, 0x46)[0]
|
||||
sections = [struct.unpack_from("<8sIIIIIIHHI", image, 0x58 + optional_size + index * 40) for index in range(count)]
|
||||
result = []
|
||||
for index in range(len(self.names)):
|
||||
rva = struct.unpack_from("<Q" if bits == 64 else "<I", image, 576 + index * (bits // 8))[0]
|
||||
self.assertEqual(rva, struct.unpack_from("<Q" if bits == 64 else "<I", image, 704 + index * (bits // 8))[0])
|
||||
section = next(s for s in sections if s[2] <= rva < s[2] + s[3])
|
||||
offset = section[4] + rva - section[2] + 2
|
||||
result.append(image[offset:image.index(0, offset)].decode())
|
||||
return result
|
||||
|
||||
def test_shared_import_rules_and_filter_in_all_formats(self):
|
||||
for kind, build in (("pe64", self.pe), ("pe32", lambda: self.pe(32)), ("elf", self.elf), ("macho", self.macho)):
|
||||
for only_sce in (False, True):
|
||||
with self.subTest(kind=kind, only_sce=only_sce), tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "arbitrary.extension"
|
||||
image = build()
|
||||
path.write_bytes(image)
|
||||
excluded = Path(directory) / "preserved.txt"
|
||||
excluded.write_bytes(b"scePreserved\n")
|
||||
args = [str(PATCHER), "--exec", "--preserve-export-list", str(excluded)]
|
||||
if only_sce:
|
||||
args.append("--only-sce")
|
||||
result = subprocess.run([*args, str(path)], capture_output=True, text=True, timeout=20)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
expected = ["foo", nid("foo"), nid("z"), nid("sceExample"), "helper_nid_no_patch", "plain", "SDL_Init", "scePreserved", nid("hostFunctionLongName"), nid("alias")]
|
||||
if only_sce:
|
||||
expected[2] = "z"
|
||||
expected[5] = "plain_nid_no_patch_cut"
|
||||
expected[8] = "hostFunctionLongName"
|
||||
self.assertCountEqual(self.read_names(path.read_bytes(), kind), expected)
|
||||
if kind == "macho":
|
||||
self.assertEqual(exports_of(path.read_bytes()), exports_of(image))
|
||||
if kind == "elf":
|
||||
from test_symbol_versions import read_dynsym
|
||||
self.assertIn(("application_export", 9, 0x400100), read_dynsym(path.read_bytes()))
|
||||
|
||||
def test_exec_accepts_macho_without_exports(self):
|
||||
from test_macho_nids import LC_DYLD_EXPORTS_TRIE, command_fields
|
||||
image = self.macho()
|
||||
command = command_fields(image)[LC_DYLD_EXPORTS_TRIE][0]
|
||||
struct.pack_into("<II", image, command + 8, 0, 0)
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "program"
|
||||
path.write_bytes(image)
|
||||
result = subprocess.run([str(PATCHER), "--exec", str(path)], capture_output=True, text=True, timeout=20)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
|
||||
def test_elf_without_sections_rejects_dynamic_segment_without_writing(self):
|
||||
image = self.elf()
|
||||
struct.pack_into("<Q", image, 40, 0)
|
||||
struct.pack_into("<HH", image, 60, 0, 0)
|
||||
for flags in ([], ["--only-sce"]):
|
||||
with self.subTest(flags=flags), tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "program"
|
||||
path.write_bytes(image)
|
||||
result = subprocess.run([str(PATCHER), "--exec", *flags, str(path)], capture_output=True, text=True, timeout=20)
|
||||
self.assertEqual(result.returncode, 2, result.stdout + result.stderr)
|
||||
self.assertIn("PT_DYNAMIC without a .dynsym section", result.stderr)
|
||||
self.assertEqual(path.read_bytes(), image)
|
||||
|
||||
def test_elf_without_sections_or_dynamic_segment_is_unchanged(self):
|
||||
image = self.elf()
|
||||
struct.pack_into("<Q", image, 40, 0)
|
||||
struct.pack_into("<HH", image, 60, 0, 0)
|
||||
struct.pack_into("<H", image, 56, 1)
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "program"
|
||||
path.write_bytes(image)
|
||||
result = subprocess.run([str(PATCHER), "--exec", str(path)], capture_output=True, text=True, timeout=20)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertEqual(path.read_bytes(), image)
|
||||
|
||||
def test_elf_short_imports_grow_without_overwriting_bss(self):
|
||||
from test_symbol_versions import sections
|
||||
self.names = ["a", "b", "c", "d"]
|
||||
image = self.elf()
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "program"
|
||||
path.write_bytes(image)
|
||||
result = subprocess.run([str(PATCHER), "--exec", str(path)], capture_output=True, text=True, timeout=20)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
after = path.read_bytes()
|
||||
self.assertCountEqual(self.read_names(after, "elf"), [nid(name) for name in self.names])
|
||||
offset = struct.unpack_from("<Q", after, 32)[0]
|
||||
count = struct.unpack_from("<H", after, 56)[0]
|
||||
segments = [struct.unpack_from("<IIQQQQQQ", after, offset + index * 56) for index in range(count)]
|
||||
original = next(segment for segment in segments if segment[0] == 1 and segment[3] == 0x400000)
|
||||
self.assertEqual(original[5:7], (4096, 8192))
|
||||
phdr = next(segment for segment in segments if segment[0] == 6)
|
||||
load = next(segment for segment in segments if segment[0] == 1 and segment[2] == offset)
|
||||
self.assertEqual(phdr[2:4], load[2:4])
|
||||
strings = next(section for section in sections(after) if section[1] == 3)
|
||||
self.assertEqual(struct.unpack_from("<Q", after, 0x708)[0], strings[3])
|
||||
self.assertEqual(struct.unpack_from("<Q", after, 0x718)[0], strings[5])
|
||||
self.assertGreater(strings[4], len(image))
|
||||
|
||||
def test_pe_own_exports_remain_unchanged(self):
|
||||
image = self.pe()
|
||||
struct.pack_into("<II", image, 0x58 + 112, 0x1500, 96)
|
||||
struct.pack_into("<IIHHIIIIIII", image, 0x700, 0, 0, 0, 0, 0x1540, 1, 1, 1, 0x1528, 0x152c, 0x1530)
|
||||
struct.pack_into("<IIH", image, 0x728, 0x1100, 0x1550, 0)
|
||||
image[0x740:0x748] = b"app.exe\0"
|
||||
image[0x750:0x75b] = b"own_export\0"
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "program"
|
||||
path.write_bytes(image)
|
||||
result = subprocess.run([str(PATCHER), "--exec", str(path)], capture_output=True, text=True, timeout=20)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(path.read_bytes()[0x700:0x760], image[0x700:0x760])
|
||||
|
||||
def test_unsupported_exec_layouts_fail_without_writing(self):
|
||||
signed = bytearray(build_dylib([], [("_sceExample", 1)], signed=True))
|
||||
struct.pack_into("<I", signed, 12, 2)
|
||||
pe = self.pe()
|
||||
struct.pack_into("<II", pe, 0x58 + 112 + 13 * 8, 0x1400, 32)
|
||||
full_headers = self.pe()
|
||||
struct.pack_into("<I", full_headers, 0x58 + 60, 0x58 + 240 + 40)
|
||||
for image, message in ((signed, "signature"), (pe, "delay-loaded"), (full_headers, "section header"), (b"invalid", "unrecognized")):
|
||||
with self.subTest(message=message), tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "program"
|
||||
path.write_bytes(image)
|
||||
result = subprocess.run([str(PATCHER), "--exec", str(path)], capture_output=True, text=True, timeout=20)
|
||||
self.assertEqual(result.returncode, 2)
|
||||
self.assertIn(message, result.stderr)
|
||||
self.assertEqual(path.read_bytes(), image)
|
||||
|
||||
def test_only_sce_requires_exec_and_failure_does_not_write(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "program"
|
||||
image = self.pe()
|
||||
path.write_bytes(image)
|
||||
for arguments in (["libtest", "--only-sce"], ["--only-sce"], ["--exec", "--only-sce", "--only-sce"]):
|
||||
result = subprocess.run([str(PATCHER), *arguments, str(path)], capture_output=True, timeout=20)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertEqual(path.read_bytes(), image)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -327,6 +327,7 @@ set(agcDriverSources
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PipelineCache.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PipelineJournal.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Resources.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/DeferredDestruction.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PlaceholderImages.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Recorder.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ParallelCompare.cpp
|
||||
@@ -1279,6 +1280,20 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_deferred_destruction_tests EXCLUDE_FROM_ALL tests/DeferredDestruction.cpp)
|
||||
target_include_directories(agc_deferred_destruction_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_deferred_destruction_tests PRIVATE libSceAgcDriver libc)
|
||||
set_target_properties(agc_deferred_destruction_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_deferred_destruction_tests)
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_deferred_destruction_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
|
||||
add_test(NAME agc_deferred_destruction COMMAND agc_deferred_destruction_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_deferred_destruction PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_device_memory_fallback_tests EXCLUDE_FROM_ALL tests/DeviceMemoryFallback.cpp)
|
||||
target_include_directories(agc_device_memory_fallback_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_device_memory_fallback_tests PRIVATE libSceAgcDriver libc)
|
||||
@@ -1572,8 +1587,12 @@ if(BUILD_TESTING)
|
||||
set_tests_properties(agc_pipeline_library_off PROPERTIES ENVIRONMENT "APS5_NO_GPL=1")
|
||||
add_test(NAME agc_pipeline_library_fast_only COMMAND agc_driver_recorder_tests --pipeline-library-only)
|
||||
set_tests_properties(agc_pipeline_library_fast_only PROPERTIES ENVIRONMENT "APS5_NO_PIPELINE_LTO=1" SKIP_RETURN_CODE 77)
|
||||
add_test(NAME agc_pipeline_library_lto_info COMMAND agc_driver_recorder_tests --pipeline-library-lto-info-only)
|
||||
add_test(NAME agc_pipeline_library_lto_info_fast_only COMMAND agc_driver_recorder_tests --pipeline-library-lto-info-only)
|
||||
set_tests_properties(agc_pipeline_library_lto_info agc_pipeline_library_lto_info_fast_only PROPERTIES SKIP_RETURN_CODE 77)
|
||||
set_tests_properties(agc_pipeline_library_lto_info_fast_only PROPERTIES ENVIRONMENT "APS5_NO_PIPELINE_LTO=1")
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_pipeline_library agc_pipeline_library_off agc_pipeline_library_fast_only PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
set_tests_properties(agc_pipeline_library agc_pipeline_library_off agc_pipeline_library_fast_only agc_pipeline_library_lto_info agc_pipeline_library_lto_info_fast_only PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
set_tests_properties(agc_pipeline_cache agc_pipeline_cache_file PROPERTIES SKIP_RETURN_CODE 77)
|
||||
@@ -2181,6 +2200,12 @@ if(BUILD_TESTING)
|
||||
set_tests_properties(agc_driver_address_passes_targets_split PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_driver_address_passes_draw_transitions COMMAND agc_driver_address_passes_tests --targets)
|
||||
set_tests_properties(agc_driver_address_passes_draw_transitions PROPERTIES ENVIRONMENT "APS5_NO_ADDRESS_READ_PASSES=1;APS5_DRAW_TRANSITIONS=1" SKIP_RETURN_CODE 77 TIMEOUT 60)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_address_passes_draw_transitions PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
set_target_properties(agc_driver_rcp_hardware_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
|
||||
add_test(NAME agc_driver_rcp_hardware_exact COMMAND agc_driver_rcp_hardware_tests)
|
||||
set_tests_properties(agc_driver_rcp_hardware_exact PROPERTIES ENVIRONMENT "APS5_EXACT_RCP=1;ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 60)
|
||||
@@ -2225,6 +2250,7 @@ endif()
|
||||
|
||||
target_link_libraries(agc_driver_rect_list_corner_tests PRIVATE shader_recompiler_agc_driver)
|
||||
target_link_libraries(agc_driver_queue_zero_late_interrupt_tests PRIVATE libkernel)
|
||||
target_link_libraries(agc_driver_linear_target_completion_points_tests PRIVATE libkernel)
|
||||
|
||||
target_link_libraries(agc_driver_storage_file_write_tests PRIVATE libkernel)
|
||||
|
||||
@@ -2239,6 +2265,16 @@ target_include_directories(agc_driver_image_table_cache_bounds_tests PRIVATE $<T
|
||||
target_include_directories(agc_driver_draw_cache_relocation_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_test(NAME agc_driver_linear_target_completion_points_sync_flip COMMAND agc_driver_linear_target_completion_points_tests --flip)
|
||||
set_tests_properties(agc_driver_linear_target_completion_points_sync_flip PROPERTIES
|
||||
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_SYNC_FLIP=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
add_test(NAME agc_driver_linear_target_completion_points_unpipelined COMMAND agc_driver_linear_target_completion_points_tests --label --interrupt)
|
||||
set_tests_properties(agc_driver_linear_target_completion_points_unpipelined PROPERTIES
|
||||
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=0" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_linear_target_completion_points_sync_flip agc_driver_linear_target_completion_points_unpipelined PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_submit_progress COMMAND agc_driver_submit_progress_tests)
|
||||
set_tests_properties(agc_submit_progress PROPERTIES
|
||||
ENVIRONMENT "APS5_SUBMIT_PROGRESS=1;ANYPS5_REQUIRE_VULKAN=1" TIMEOUT 30 SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
@@ -2369,6 +2405,8 @@ target_include_directories(agc_driver_color565_target_tests PRIVATE $<TARGET_PRO
|
||||
|
||||
target_include_directories(agc_driver_depth_range_remap_draw_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||
|
||||
target_include_directories(agc_driver_depth_bounds_emulation_draw_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||
|
||||
foreach(stage IN ITEMS vert frag)
|
||||
set(agcProvokingShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/ProvokingVertex.${stage})
|
||||
set(agcProvokingShaderSpv ${agcTestShaderGeneratedRoot}/ProvokingVertex_${stage}.spv)
|
||||
@@ -2736,6 +2774,11 @@ if(BUILD_TESTING)
|
||||
set_tests_properties(agc_pipeline_journal_long_paths PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
add_test(NAME agc_pipeline_library_clear_waits_for_build COMMAND agc_driver_graphics_tests --pipeline-library-clear-waits-for-build)
|
||||
add_test(NAME agc_pipeline_store_lazy_buffer_pool COMMAND agc_driver_graphics_tests --pipeline-store-lazy-buffer-pool)
|
||||
add_test(NAME agc_pipeline_store_growth COMMAND agc_driver_graphics_tests --pipeline-store-growth)
|
||||
add_test(NAME agc_pipeline_store_fixed_bound COMMAND agc_driver_graphics_tests --pipeline-store-growth)
|
||||
add_test(NAME agc_pipeline_store_replay_growth COMMAND agc_driver_graphics_tests --pipeline-store-replay-growth)
|
||||
set_tests_properties(agc_pipeline_store_growth agc_pipeline_store_fixed_bound agc_pipeline_store_replay_growth PROPERTIES TIMEOUT 60 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
set_tests_properties(agc_pipeline_store_fixed_bound PROPERTIES ENVIRONMENT "APS5_PIPELINE_STORE_ENTRIES=1024")
|
||||
set_tests_properties(agc_pipeline_journal PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
set_tests_properties(agc_pipeline_library_clear_waits_for_build PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
set_tests_properties(agc_pipeline_store_lazy_buffer_pool PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
@@ -2743,7 +2786,7 @@ if(BUILD_TESTING)
|
||||
add_test(NAME agc_pipeline_journal_extended_paths COMMAND agc_driver_graphics_tests --pipeline-journal-extended-paths)
|
||||
set_tests_properties(agc_pipeline_journal_extended_paths PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
set_tests_properties(agc_pipeline_journal_long_paths agc_pipeline_journal_extended_paths PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
set_tests_properties(agc_pipeline_journal agc_pipeline_library_clear_waits_for_build agc_pipeline_store_lazy_buffer_pool PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
set_tests_properties(agc_pipeline_journal agc_pipeline_library_clear_waits_for_build agc_pipeline_store_lazy_buffer_pool agc_pipeline_store_growth agc_pipeline_store_fixed_bound agc_pipeline_store_replay_growth PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
set_tests_properties(agc_driver_sparse_buffer_holes PROPERTIES TIMEOUT 30)
|
||||
if(WIN32)
|
||||
@@ -2875,12 +2918,16 @@ if(BUILD_TESTING)
|
||||
set_tests_properties(agc_structurizer_iteration_selection_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_ITERATION_SELECTION=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
add_test(NAME agc_shader_control_flow_iteration_selection_off COMMAND agc_shader_control_flow_tests)
|
||||
set_tests_properties(agc_shader_control_flow_iteration_selection_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_ITERATION_SELECTION=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
add_test(NAME agc_structurizer_dominator_tree_off COMMAND agc_structurizer_tests)
|
||||
set_tests_properties(agc_structurizer_dominator_tree_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_DOMINATOR_TREE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
add_test(NAME agc_shader_control_flow_dominator_tree_off COMMAND agc_shader_control_flow_tests)
|
||||
set_tests_properties(agc_shader_control_flow_dominator_tree_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_DOMINATOR_TREE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
add_test(NAME agc_structurizer_forced_fallback COMMAND agc_structurizer_tests)
|
||||
set_tests_properties(agc_structurizer_forced_fallback PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_FORCE_STRUCTURIZER_FALLBACK=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
add_test(NAME agc_shader_control_flow_forced_fallback COMMAND agc_shader_control_flow_tests)
|
||||
set_tests_properties(agc_shader_control_flow_forced_fallback PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_FORCE_STRUCTURIZER_FALLBACK=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_structurizer_intersection_off agc_structurizer_iteration_selection_off agc_shader_control_flow_iteration_selection_off agc_structurizer_forced_fallback agc_shader_control_flow_forced_fallback PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
set_tests_properties(agc_structurizer_intersection_off agc_structurizer_iteration_selection_off agc_shader_control_flow_iteration_selection_off agc_structurizer_dominator_tree_off agc_shader_control_flow_dominator_tree_off agc_structurizer_forced_fallback agc_shader_control_flow_forced_fallback PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_driver_gated_dispatch_capture_short_timeout COMMAND agc_driver_gated_dispatch_capture_tests)
|
||||
set_tests_properties(agc_driver_gated_dispatch_capture_short_timeout PROPERTIES
|
||||
@@ -3443,8 +3490,7 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
option(ANYPS5_RUN_QUARANTINED_FRAME_REPLAY "Build and register agc_driver_frame_replay on Windows" OFF)
|
||||
if(WIN32 AND ANYPS5_RUN_QUARANTINED_FRAME_REPLAY)
|
||||
if(WIN32)
|
||||
add_test_executable(agc_driver_frame_replay_tests tests/FrameReplay.cpp Execution/src/FrameCapture.cpp)
|
||||
target_include_directories(agc_driver_frame_replay_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_driver_frame_replay_tests PRIVATE libSceAgc libSceAgcDriver libkernel libc libzstd_static)
|
||||
@@ -3457,6 +3503,12 @@ if(BUILD_TESTING)
|
||||
TIMEOUT 120)
|
||||
set_tests_properties(agc_driver_frame_replay PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
add_test(NAME agc_driver_frame_replay_concurrent COMMAND agc_driver_frame_replay_tests $<TARGET_FILE:agc_frame_replay> concurrent)
|
||||
set_tests_properties(agc_driver_frame_replay_concurrent PROPERTIES
|
||||
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_FRAME_CAPTURE=${CMAKE_CURRENT_BINARY_DIR}/frame-replay-concurrent-capture;APS5_FRAME_CAPTURE_AT_FLIP=1;APS5_FRAME_CAPTURE_FRAMES=8"
|
||||
TIMEOUT 120)
|
||||
set_tests_properties(agc_driver_frame_replay_concurrent PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@@ -3980,6 +4032,6 @@ endif()
|
||||
|
||||
if(BUILD_TESTING)
|
||||
get_directory_property(agcRegisteredTests TESTS)
|
||||
list(FILTER agcRegisteredTests EXCLUDE REGEX "^(agc_pipeline_journal(_long_paths|_extended_paths)?|agc_driver_pipeline_journal)$")
|
||||
list(FILTER agcRegisteredTests EXCLUDE REGEX "^(agc_pipeline_journal(_long_paths|_extended_paths)?|agc_driver_pipeline_journal|agc_pipeline_store_replay_growth)$")
|
||||
set_property(TEST ${agcRegisteredTests} APPEND PROPERTY ENVIRONMENT "APS5_NO_PIPELINE_JOURNAL=1")
|
||||
endif()
|
||||
@@ -33,6 +33,8 @@ extern "C" void AgcDriverSuspendPoint_nid_postfix();
|
||||
extern "C" void AgcDriverSerialProgress_nid_postfix(std::uint64_t* accepted, std::uint64_t* completed);
|
||||
extern "C" std::size_t AgcDriverAwaitedWaits_nid_postfix(AgcDriver::AwaitedWait* waits, std::size_t capacity);
|
||||
extern "C" std::uint64_t AgcDriverDispatchesAdoptedAhead_nid_postfix();
|
||||
extern "C" std::uint64_t AgcDriverDispatchesRejectedAhead_nid_postfix();
|
||||
extern "C" void AgcDriverSetAheadAdoptHook_nid_postfix(void (*hook)());
|
||||
extern "C" void AgcDriverRegisterShader_nid_postfix(const Shader* shader);
|
||||
extern "C" void AgcDriverResolveShaderAbi_nid_postfix(const Shader* shader, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive);
|
||||
extern "C" void AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span<const Shader* const> stages, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive);
|
||||
|
||||
@@ -55,6 +55,7 @@ public:
|
||||
std::uint64_t DecodeDrains() const { return decodeDrains.load(std::memory_order_relaxed); }
|
||||
std::optional<std::uint64_t> PendingLabel(std::uint64_t address, std::size_t bytes);
|
||||
static bool HeldReads(std::uint64_t address, std::size_t bytes);
|
||||
static std::uint64_t HeldReadQueries();
|
||||
void ReleaseRunningReads();
|
||||
|
||||
static bool MetadataPassesPipelined();
|
||||
|
||||
@@ -59,6 +59,8 @@ public:
|
||||
void Shutdown();
|
||||
void WaitIdle();
|
||||
std::uint64_t DispatchesAdoptedAhead() const;
|
||||
std::uint64_t DispatchesRejectedAhead() const;
|
||||
void SetAheadAdoptHook(void (*hook)());
|
||||
void CheckFailure();
|
||||
void ReportFailure(std::exception_ptr error);
|
||||
void Submit(const Packet* packet, std::uint32_t queue, bool cpuFlip = false);
|
||||
@@ -317,7 +319,7 @@ private:
|
||||
std::chrono::steady_clock::time_point resolvedAt{};
|
||||
};
|
||||
struct GroupCaptureStats {
|
||||
std::uint64_t runs = 0, resolved = 0, resolvedHits = 0, adopted = 0, addressMismatches = 0, keyMismatches = 0, failures = 0, stale = 0, labelOverlaps = 0, writtenBefore = 0, deviceRejects = 0, argumentMismatches = 0;
|
||||
std::uint64_t runs = 0, resolved = 0, resolvedHits = 0, adopted = 0, addressMismatches = 0, keyMismatches = 0, memoryMismatches = 0, failures = 0, stale = 0, labelOverlaps = 0, writtenBefore = 0, deviceRejects = 0, argumentMismatches = 0;
|
||||
double resolveMs = 0, resolveMaxMs = 0, ageMs = 0, ageMaxMs = 0;
|
||||
std::chrono::steady_clock::time_point reported = std::chrono::steady_clock::now();
|
||||
};
|
||||
@@ -474,6 +476,8 @@ private:
|
||||
std::atomic<std::uint64_t> evidenceReads{0};
|
||||
std::atomic<std::uint64_t> evidenceValidations{0};
|
||||
std::atomic<std::uint64_t> dispatchesAdoptedAhead{0};
|
||||
std::atomic<std::uint64_t> dispatchesRejectedAhead{0};
|
||||
std::atomic<void (*)()> aheadAdoptHook{nullptr};
|
||||
|
||||
AgcDriver::Mutex writtenBuffersMutex;
|
||||
std::deque<WrittenBuffer> writtenBuffers;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
@@ -35,10 +36,10 @@ public:
|
||||
void ResolveGraphicsAbi(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType);
|
||||
void RegisterVideoOutput(std::uint32_t handle);
|
||||
void UnregisterVideoOutput(std::uint32_t handle);
|
||||
void Submit(const Packet& packet, std::uint32_t queue);
|
||||
void Flip(const FlipInfo& info);
|
||||
void Submit(const Packet& packet, std::uint32_t queue, const std::function<void()>& accept);
|
||||
void Flip(const FlipInfo& info, const std::function<void()>& accept);
|
||||
void ReserveEopFlip(const FlipInfo& info, std::uint32_t token);
|
||||
void Suspend();
|
||||
void Suspend(const std::function<void()>& accept);
|
||||
void PresentBuffer(const DisplayBuffer& buffer);
|
||||
void FrameBoundary();
|
||||
|
||||
@@ -62,7 +63,8 @@ private:
|
||||
};
|
||||
|
||||
FrameCall make(FrameCallKind kind);
|
||||
void record(FrameCall call, bool prologueCall, std::span<const Shader* const> shaders = {});
|
||||
void record(FrameCall call, bool prologueCall, std::span<const Shader* const> shaders = {}, const std::function<void()>& accept = {});
|
||||
void recordLocked(FrameCall call, bool prologueCall, std::span<const Shader* const> shaders);
|
||||
void emitDelta();
|
||||
void emitShaderDelta(std::span<const Shader* const> shaders);
|
||||
void writeDelta(std::span<const Span> spans, std::span<const std::uint64_t> hostPages, std::span<const Range> added = {});
|
||||
@@ -72,6 +74,7 @@ private:
|
||||
void snapshot(const Range& range);
|
||||
void writeReferenceFrames();
|
||||
bool abandonIfDiskLow();
|
||||
void stop(const char* reason);
|
||||
std::string header() const;
|
||||
std::vector<Range> committedRanges();
|
||||
std::vector<Range> newRanges();
|
||||
|
||||
@@ -63,7 +63,7 @@ void WriteChanged(std::uint64_t address, std::span<const std::byte> current, std
|
||||
bool WriteWatched();
|
||||
void Unwatch(std::uint64_t address, std::size_t bytes);
|
||||
void Rewatch(std::span<const std::pair<std::uint64_t, std::uint64_t>> ranges);
|
||||
bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function<bool()>& import, bool staysWatched = true);
|
||||
bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function<bool()>& import, bool staysWatched = true, bool importMayWriteGuest = true);
|
||||
void SetUnwatchHook(void (*hook)(std::uint64_t address, std::size_t bytes));
|
||||
#ifdef _WIN32
|
||||
using FreshCollectHook = bool (*)(std::uintptr_t address, std::size_t bytes, void (*written)(void*, std::uintptr_t, std::uintptr_t), void* context);
|
||||
|
||||
@@ -46,6 +46,8 @@ private:
|
||||
std::uint32_t sourceHeight = 0;
|
||||
VkImage sourceImage = VK_NULL_HANDLE;
|
||||
VkDeviceMemory sourceMemory = VK_NULL_HANDLE;
|
||||
VkDeviceSize sourceBytes = 0;
|
||||
std::uint32_t sourceMemoryType = 0;
|
||||
VkImage blackImage = VK_NULL_HANDLE;
|
||||
VkDeviceMemory blackMemory = VK_NULL_HANDLE;
|
||||
};
|
||||
|
||||
@@ -275,8 +275,8 @@ public:
|
||||
// blit-submit times after the flip packet; noted in-place reads checked
|
||||
// (APS5_FLIP_READ_CHECK=1) and found overwritten before the blit's fence.
|
||||
struct PresentStatistics {
|
||||
std::uint64_t presents, gpuUnread, batchesAheadOfBlit, batchesAfterFlip, readsChecked, readsOverwritten, acquireDrops;
|
||||
double gpuBusyMs, gpuGapMs, lastSubmitAfterFlipMs, blitSubmitAfterFlipMs;
|
||||
std::uint64_t presents, gpuUnread, batchesAheadOfBlit, batchesAfterFlip, readsChecked, readsOverwritten, acquireDrops, keyWriterWaits;
|
||||
double gpuBusyMs, gpuGapMs, lastSubmitAfterFlipMs, blitSubmitAfterFlipMs, keyWriterWaitMs;
|
||||
};
|
||||
static PresentStatistics PresentCounts();
|
||||
struct PresyncStatistics {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "Optimization/ResourceProgram.hpp"
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
@@ -57,6 +58,19 @@ void ReportDispatchItems(bool profile) {
|
||||
std::fprintf(stderr, "[dispatch-item] %llu commit items (%llu recipes rebuilt on the committer); drained instead, cumulative:%s\n", static_cast<unsigned long long>(itemCommits.load(std::memory_order_relaxed)), static_cast<unsigned long long>(itemRebuilds.load(std::memory_order_relaxed)), drained.c_str());
|
||||
}
|
||||
|
||||
bool CapturedCurrent(std::span<const ShaderRecompiler::MemoryRegion> captured, const ShaderSnapshot& code) {
|
||||
for (const auto& region : captured) {
|
||||
const auto address = region.guestAddress;
|
||||
const auto bytes = region.bytes.size();
|
||||
if (bytes == 0 || (address >= code.codeAddress && address < code.codeAddress + code.code.size() * sizeof(std::uint32_t)) || (!code.header.empty() && address >= code.headerAddress && address < code.headerAddress + code.header.size())) continue;
|
||||
if (Driver::QueryPendingSpan(address, bytes, {}) != ShaderMemory::PendingWrite::None || !GuestMemory::Accessible(reinterpret_cast<const void*>(address), bytes)) return false;
|
||||
GuestMemory::GuardRead(address, bytes);
|
||||
GuestMemory::FlushGpuWrites(address, bytes);
|
||||
if (std::memcmp(region.bytes.data(), reinterpret_cast<const void*>(address), bytes) != 0) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool Driver::pipelineDispatch() {
|
||||
@@ -281,6 +295,14 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
|
||||
fromAhead = false;
|
||||
++groupStats.keyMismatches;
|
||||
}
|
||||
if (fromAhead) {
|
||||
if (const auto hook = aheadAdoptHook.load(std::memory_order_acquire)) hook();
|
||||
if (!CapturedCurrent(ahead.captured, snapshot)) {
|
||||
fromAhead = false;
|
||||
++dispatchesRejectedAhead;
|
||||
++groupStats.memoryMismatches;
|
||||
}
|
||||
}
|
||||
phaseTiming.Phase(PhaseKey);
|
||||
timing.Mark("decode_key_device_setup");
|
||||
bool readsClear = false;
|
||||
|
||||
@@ -42,6 +42,14 @@ std::uint64_t Driver::DispatchesAdoptedAhead() const {
|
||||
return dispatchesAdoptedAhead.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
std::uint64_t Driver::DispatchesRejectedAhead() const {
|
||||
return dispatchesRejectedAhead.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void Driver::SetAheadAdoptHook(void (*hook)()) {
|
||||
aheadAdoptHook.store(hook, std::memory_order_release);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::size_t& PreviewedUntil() {
|
||||
@@ -326,9 +334,9 @@ void Driver::reportGroupCapture(std::uint32_t queue) {
|
||||
auto& stats = groupCaptureStats();
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now - stats.reported < std::chrono::seconds(10)) return;
|
||||
AgcDriver::ProfilePrint_nid_no_patch("[groupcap] queue 0x%x (10 s): %llu runs resolved %llu dispatches (%llu cache hits) in %.1f ms (max %.2f ms); %llu adopted %.2f ms after the resolve on average, max %.2f ms; mismatches: address %llu key %llu; %llu resolve failures, %llu stale, %llu label overlaps, %llu read an earlier dispatch's output, %llu rejected by the device, %llu dropped because the argument re-read differed\n",
|
||||
AgcDriver::ProfilePrint_nid_no_patch("[groupcap] queue 0x%x (10 s): %llu runs resolved %llu dispatches (%llu cache hits) in %.1f ms (max %.2f ms); %llu adopted %.2f ms after the resolve on average, max %.2f ms; mismatches: address %llu key %llu memory %llu; %llu resolve failures, %llu stale, %llu label overlaps, %llu read an earlier dispatch's output, %llu rejected by the device, %llu dropped because the argument re-read differed\n",
|
||||
queue, static_cast<unsigned long long>(stats.runs), static_cast<unsigned long long>(stats.resolved), static_cast<unsigned long long>(stats.resolvedHits), stats.resolveMs, stats.resolveMaxMs,
|
||||
static_cast<unsigned long long>(stats.adopted), stats.adopted != 0 ? stats.ageMs / static_cast<double>(stats.adopted) : 0.0, stats.ageMaxMs, static_cast<unsigned long long>(stats.addressMismatches), static_cast<unsigned long long>(stats.keyMismatches),
|
||||
static_cast<unsigned long long>(stats.adopted), stats.adopted != 0 ? stats.ageMs / static_cast<double>(stats.adopted) : 0.0, stats.ageMaxMs, static_cast<unsigned long long>(stats.addressMismatches), static_cast<unsigned long long>(stats.keyMismatches), static_cast<unsigned long long>(stats.memoryMismatches),
|
||||
static_cast<unsigned long long>(stats.failures), static_cast<unsigned long long>(stats.stale), static_cast<unsigned long long>(stats.labelOverlaps), static_cast<unsigned long long>(stats.writtenBefore), static_cast<unsigned long long>(stats.deviceRejects), static_cast<unsigned long long>(stats.argumentMismatches));
|
||||
stats = GroupCaptureStats{};
|
||||
stats.reported = now;
|
||||
|
||||
@@ -183,6 +183,7 @@ std::shared_ptr<DrawDecode> Driver::decodeDraw(const QueueState& queue, const Su
|
||||
product->pixel = Graphics::DecodePixelStageInfo(queue.context, Graphics::ExportMappings(product->state), Graphics::PixelProgramSkipped(queue));
|
||||
Graphics::NormalizeUnusedViewportDepth(product->state, product->pixel);
|
||||
Graphics::ApplyDepthRemap(context, product->state);
|
||||
Graphics::ApplyDepthBoundsEmulation(context, product->state, product->pixel);
|
||||
product->pixel.targetExportPacking = Graphics::ExportPackings(product->state);
|
||||
product->pixel.dualSourceBlend = product->state.dualSourceBlend;
|
||||
product->pixel.reversedBlend = product->state.reversedBlend;
|
||||
|
||||
@@ -482,6 +482,10 @@ bool DrawPipeline::HeldReads(std::uint64_t address, std::size_t bytes) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::uint64_t DrawPipeline::HeldReadQueries() {
|
||||
return heldReadQueries.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void DrawPipeline::ReleaseRunningReads() {
|
||||
std::lock_guard lock(mutex);
|
||||
if (!items.empty()) items.front().reads.clear();
|
||||
|
||||
@@ -19,9 +19,8 @@ void Submit(const Packet* packet, std::uint32_t queue) try {
|
||||
return;
|
||||
}
|
||||
GuestMemory::CheckRange(packet, sizeof(Packet), alignof(Packet));
|
||||
recorder->Submit(*packet, queue);
|
||||
const auto flips = driver.FlipSubmissions();
|
||||
driver.Submit(packet, queue);
|
||||
recorder->Submit(*packet, queue, [&] { driver.Submit(packet, queue); });
|
||||
if (driver.FlipSubmissions() != flips) recorder->FrameBoundary();
|
||||
} catch (const ProcessShutdown&) {
|
||||
LibcAwaitExit_nid_postfix();
|
||||
@@ -41,8 +40,12 @@ void RegisterShader(const Shader* shader) {
|
||||
}
|
||||
|
||||
void SuspendPoint() {
|
||||
if (auto* const recorder = FrameRecorder::Active()) recorder->Suspend();
|
||||
DriverDetail::Driver::Get().SuspendPoint();
|
||||
auto* const recorder = FrameRecorder::Active();
|
||||
if (recorder == nullptr) {
|
||||
DriverDetail::Driver::Get().SuspendPoint();
|
||||
return;
|
||||
}
|
||||
recorder->Suspend([] { DriverDetail::Driver::Get().SuspendPoint(); });
|
||||
}
|
||||
|
||||
void RegisterVideoOutput(std::uint32_t handle, const std::shared_ptr<IVideoOutput>& output) {
|
||||
@@ -146,9 +149,12 @@ extern "C" void AgcDriverSerialProgress_nid_postfix(std::uint64_t* accepted, std
|
||||
|
||||
extern "C" void AgcDriverSubmitFlip_nid_postfix(const AgcDriver::FlipInfo& info) try {
|
||||
auto* const recorder = AgcDriver::FrameRecorder::Active();
|
||||
if (recorder != nullptr) recorder->Flip(info);
|
||||
AgcDriver::DriverDetail::Driver::Get().SubmitFlip(info);
|
||||
if (recorder != nullptr) recorder->FrameBoundary();
|
||||
if (recorder == nullptr) {
|
||||
AgcDriver::DriverDetail::Driver::Get().SubmitFlip(info);
|
||||
return;
|
||||
}
|
||||
recorder->Flip(info, [&] { AgcDriver::DriverDetail::Driver::Get().SubmitFlip(info); });
|
||||
recorder->FrameBoundary();
|
||||
} catch (const ProcessShutdown&) {
|
||||
LibcAwaitExit_nid_postfix();
|
||||
}
|
||||
@@ -157,6 +163,14 @@ extern "C" std::uint64_t AgcDriverDispatchesAdoptedAhead_nid_postfix() {
|
||||
return AgcDriver::DriverDetail::Driver::Get().DispatchesAdoptedAhead();
|
||||
}
|
||||
|
||||
extern "C" std::uint64_t AgcDriverDispatchesRejectedAhead_nid_postfix() {
|
||||
return AgcDriver::DriverDetail::Driver::Get().DispatchesRejectedAhead();
|
||||
}
|
||||
|
||||
extern "C" void AgcDriverSetAheadAdoptHook_nid_postfix(void (*hook)()) {
|
||||
AgcDriver::DriverDetail::Driver::Get().SetAheadAdoptHook(hook);
|
||||
}
|
||||
|
||||
extern "C" void AgcDriverRegisterVideoOutput_nid_postfix(std::uint32_t handle, const std::shared_ptr<AgcDriver::IVideoOutput>& output) {
|
||||
AgcDriver::RegisterVideoOutput(handle, output);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
|
||||
#include "prx/libSceAgcDriver/Eq/include/Event.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
@@ -394,6 +395,7 @@ void Driver::execute(const Submission& submission) {
|
||||
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush);
|
||||
std::lock_guard gpuLock(GuestMemory::GpuMutex());
|
||||
const auto localDevice = device.Load();
|
||||
if (localDevice != nullptr) static_cast<void>(Graphics::StorageTexture::StoreDrawnLinear("flip"));
|
||||
|
||||
if (!recordLabelsForPacket(localDevice.get(), submission.queue) && localDevice != nullptr) localDevice->SubmitRecorded(submission.queue == 0);
|
||||
if (localDevice != nullptr) {
|
||||
|
||||
@@ -38,11 +38,12 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue
|
||||
std::lock_guard gpuLock(GuestMemory::GpuMutex());
|
||||
const auto localDevice = device.Load();
|
||||
recordDeferredLabels(localDevice.get(), submission.queue);
|
||||
const bool workOpen = Graphics::Recorder::RecordedWorkSinceSubmit() != 0;
|
||||
bool workOpen = Graphics::Recorder::RecordedWorkSinceSubmit() != 0;
|
||||
int reason = localDevice != nullptr ? 0 : 1;
|
||||
if (label.has_value()) {
|
||||
const auto bytes = label->Bytes();
|
||||
const auto stamp = ++eventSerial;
|
||||
if (localDevice != nullptr && submission.queue == 0 && Graphics::StorageTexture::StoreDrawnLinear("label") != 0) workOpen = true;
|
||||
reason = localDevice != nullptr ? localDevice->WriteLabelOnGpu(label->address, bytes, stamp, submission.queue) : 4;
|
||||
if (reason == 1) GuestMemory::Write(label->address, bytes, 4);
|
||||
if (reason == 0 || reason == 1 || reason == 5) {
|
||||
@@ -52,6 +53,7 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue
|
||||
countLabelOutcome(reason);
|
||||
++immediateLabels;
|
||||
} else {
|
||||
if (localDevice != nullptr && submission.queue == 0 && Graphics::StorageTexture::StoreDrawnLinear("interrupt") != 0) workOpen = true;
|
||||
if (localDevice != nullptr) Graphics::StorageTexture::StoreReadbackBeforeLabel("readback before interrupt");
|
||||
++noOpLabels;
|
||||
}
|
||||
@@ -249,6 +251,7 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue
|
||||
|
||||
recordDeferredLabels(draining.get(), submission.queue);
|
||||
if (draining != nullptr) {
|
||||
static_cast<void>(Graphics::StorageTexture::StoreDrawnLinear("drain"));
|
||||
if (unlockedDrain && draining->CanWaitUnlocked()) epoch = draining->SubmitAndEpoch();
|
||||
else draining->WaitIdle();
|
||||
}
|
||||
@@ -437,10 +440,11 @@ void Driver::commitLabel(std::uint32_t queue, std::uint64_t address, std::span<c
|
||||
std::lock_guard gpuLock(GuestMemory::GpuMutex());
|
||||
const auto localDevice = device.Load();
|
||||
recordDeferredLabels(localDevice.get(), queue);
|
||||
const bool workOpen = Graphics::Recorder::RecordedWorkSinceSubmit() != 0;
|
||||
bool workOpen = Graphics::Recorder::RecordedWorkSinceSubmit() != 0;
|
||||
int reason = localDevice != nullptr ? 0 : 1;
|
||||
if (!bytes.empty()) {
|
||||
const auto stamp = ++eventSerial;
|
||||
if (localDevice != nullptr && queue == 0 && Graphics::StorageTexture::StoreDrawnLinear("label") != 0) workOpen = true;
|
||||
reason = localDevice != nullptr ? localDevice->WriteLabelOnGpu(address, bytes, stamp, queue) : 4;
|
||||
if (reason != 0 && reason != 5 && reason != 6) {
|
||||
if (reason != 1 && localDevice != nullptr) localDevice->WaitIdle();
|
||||
@@ -455,6 +459,7 @@ void Driver::commitLabel(std::uint32_t queue, std::uint64_t address, std::span<c
|
||||
}
|
||||
if (endOfPipeInterrupt) {
|
||||
bool deferred = false;
|
||||
if (bytes.empty() && localDevice != nullptr && queue == 0 && Graphics::StorageTexture::StoreDrawnLinear("interrupt") != 0) workOpen = true;
|
||||
if (localDevice != nullptr && (reason == 0 || reason == 5 || reason == 6)) {
|
||||
deferred = localDevice->AfterRecordedWork([this, queue] { deliverGuestEopInterrupt(queue); }, queue == 0);
|
||||
if (deferred && workOpen) localDevice->SubmitRecorded(queue == 0);
|
||||
|
||||
@@ -206,7 +206,7 @@ void Driver::reportPresents(double waitedMs, std::size_t inFlight) {
|
||||
const auto flips = flipsCounted.load() - previousFlips;
|
||||
const double perFlip = flips != 0 ? 1.0 / static_cast<double>(flips) : 0.0;
|
||||
const auto serial = flipSerial.load();
|
||||
AgcDriver::ProfilePrint_nid_no_patch( "[present] %llu presents over 10 s (%llu dropped: no free swapchain image; %zu may trail on the GPU); %s p50 %.2f ms, p90 %.2f ms, max %.2f ms over %zu; per frame, presented or dropped: GPU busy %.1f ms, idle gaps %.1f ms (%llu batches without a completion record); per flip: %.1f batches recorded (%.1f unsignaled at the flip); per present: %.1f batches ahead of the blit, %.1f after it; after the flip packet: last submit %.1f ms, blit submit %.1f ms; in-place reads overwritten before execution: %llu of %llu checked\n", static_cast<unsigned long long>(presents), static_cast<unsigned long long>(dropped), inFlight, inFlight != 0 ? "inflight_wait" : "render_fence_wait", percentile(0.5), percentile(0.9), waits.empty() ? 0.0 : waits.back(), waits.size(), (counts.gpuBusyMs - previous.gpuBusyMs) * perFrame, (counts.gpuGapMs - previous.gpuGapMs) * perFrame, static_cast<unsigned long long>(counts.gpuUnread - previous.gpuUnread), static_cast<double>(serial - windowSerial) * perFlip, static_cast<double>(flipBatchesUnsignaled.load() - previousUnsignaled) * perFlip, static_cast<double>(counts.batchesAheadOfBlit - previous.batchesAheadOfBlit) * perPresent, static_cast<double>(counts.batchesAfterFlip - previous.batchesAfterFlip) * perPresent, (counts.lastSubmitAfterFlipMs - previous.lastSubmitAfterFlipMs) * perPresent, (counts.blitSubmitAfterFlipMs - previous.blitSubmitAfterFlipMs) * perPresent, static_cast<unsigned long long>(counts.readsOverwritten - previous.readsOverwritten), static_cast<unsigned long long>(counts.readsChecked - previous.readsChecked));
|
||||
AgcDriver::ProfilePrint_nid_no_patch( "[present] %llu presents over 10 s (%llu dropped: no free swapchain image; %zu may trail on the GPU); %s p50 %.2f ms, p90 %.2f ms, max %.2f ms over %zu; per frame, presented or dropped: GPU busy %.1f ms, idle gaps %.1f ms (%llu batches without a completion record); per flip: %.1f batches recorded (%.1f unsignaled at the flip); per present: %.1f batches ahead of the blit, %.1f after it; after the flip packet: last submit %.1f ms, blit submit %.1f ms; in-place reads overwritten before execution: %llu of %llu checked; waits for the display keys' writer outside the GPU mutex: %llu, %.1f ms\n", static_cast<unsigned long long>(presents), static_cast<unsigned long long>(dropped), inFlight, inFlight != 0 ? "inflight_wait" : "render_fence_wait", percentile(0.5), percentile(0.9), waits.empty() ? 0.0 : waits.back(), waits.size(), (counts.gpuBusyMs - previous.gpuBusyMs) * perFrame, (counts.gpuGapMs - previous.gpuGapMs) * perFrame, static_cast<unsigned long long>(counts.gpuUnread - previous.gpuUnread), static_cast<double>(serial - windowSerial) * perFlip, static_cast<double>(flipBatchesUnsignaled.load() - previousUnsignaled) * perFlip, static_cast<double>(counts.batchesAheadOfBlit - previous.batchesAheadOfBlit) * perPresent, static_cast<double>(counts.batchesAfterFlip - previous.batchesAfterFlip) * perPresent, (counts.lastSubmitAfterFlipMs - previous.lastSubmitAfterFlipMs) * perPresent, (counts.blitSubmitAfterFlipMs - previous.blitSubmitAfterFlipMs) * perPresent, static_cast<unsigned long long>(counts.readsOverwritten - previous.readsOverwritten), static_cast<unsigned long long>(counts.readsChecked - previous.readsChecked), static_cast<unsigned long long>(counts.keyWriterWaits - previous.keyWriterWaits), counts.keyWriterWaitMs - previous.keyWriterWaitMs);
|
||||
previous = counts;
|
||||
previousFlips = flipsCounted.load();
|
||||
previousUnsignaled = flipBatchesUnsignaled.load();
|
||||
|
||||
@@ -138,7 +138,7 @@ std::shared_ptr<const ShaderSnapshot> ReadRawShader(std::uint64_t address) {
|
||||
cache.erase(found);
|
||||
}
|
||||
const auto bytes = result->code.size() * sizeof(std::uint32_t);
|
||||
while (!cache.empty() && (cache.size() >= 64 || cacheBytes + bytes > 8 * 1024 * 1024)) {
|
||||
while (!cache.empty() && (cache.size() >= 4096 || cacheBytes + bytes > 64 * 1024 * 1024)) {
|
||||
cacheBytes -= cache.back()->code.size() * sizeof(std::uint32_t);
|
||||
cache.pop_back();
|
||||
}
|
||||
|
||||
@@ -163,20 +163,20 @@ void FrameRecorder::UnregisterVideoOutput(std::uint32_t handle) {
|
||||
record(std::move(call), true);
|
||||
}
|
||||
|
||||
void FrameRecorder::Submit(const Packet& packet, std::uint32_t queue) {
|
||||
void FrameRecorder::Submit(const Packet& packet, std::uint32_t queue, const std::function<void()>& accept) {
|
||||
auto call = make(FrameCallKind::Submit);
|
||||
call.queue = queue;
|
||||
call.address = pointer(packet.addr);
|
||||
call.size = packet.dw_num;
|
||||
call.flags = packet.flags;
|
||||
record(std::move(call), false);
|
||||
record(std::move(call), false, {}, accept);
|
||||
}
|
||||
|
||||
void FrameRecorder::Flip(const FlipInfo& info) {
|
||||
void FrameRecorder::Flip(const FlipInfo& info, const std::function<void()>& accept) {
|
||||
auto call = make(FrameCallKind::Flip);
|
||||
call.cpuFlip = true;
|
||||
call.values = {info.handle, flipValue(info.index), info.mode, flipValue(info.argument)};
|
||||
record(std::move(call), false);
|
||||
record(std::move(call), false, {}, accept);
|
||||
}
|
||||
|
||||
void FrameRecorder::ReserveEopFlip(const FlipInfo& info, std::uint32_t token) {
|
||||
@@ -198,8 +198,8 @@ void FrameRecorder::ReserveEopFlip(const FlipInfo& info, std::uint32_t token) {
|
||||
++recordedCalls;
|
||||
}
|
||||
|
||||
void FrameRecorder::Suspend() {
|
||||
record(make(FrameCallKind::Suspend), false);
|
||||
void FrameRecorder::Suspend(const std::function<void()>& accept) {
|
||||
record(make(FrameCallKind::Suspend), false, {}, accept);
|
||||
}
|
||||
|
||||
void FrameRecorder::PresentBuffer(const DisplayBuffer& buffer) {
|
||||
@@ -236,8 +236,13 @@ void FrameRecorder::writeReferenceFrames() {
|
||||
}
|
||||
}
|
||||
|
||||
void FrameRecorder::record(FrameCall call, bool prologueCall, std::span<const Shader* const> shaders) {
|
||||
void FrameRecorder::record(FrameCall call, bool prologueCall, std::span<const Shader* const> shaders, const std::function<void()>& accept) {
|
||||
std::lock_guard lock(mutex);
|
||||
recordLocked(std::move(call), prologueCall, shaders);
|
||||
if (accept) accept();
|
||||
}
|
||||
|
||||
void FrameRecorder::recordLocked(FrameCall call, bool prologueCall, std::span<const Shader* const> shaders) {
|
||||
if (state == State::Waiting) {
|
||||
if (!prologueCall) return;
|
||||
call.ordinal = ++ordinal;
|
||||
@@ -360,10 +365,23 @@ void FrameRecorder::emitShaderDelta(std::span<const Shader* const> shaders) {
|
||||
|
||||
void FrameRecorder::writeDelta(std::span<const Span> spans, std::span<const std::uint64_t> hostPages, std::span<const Range> added) {
|
||||
if (abandonIfDiskLow()) return;
|
||||
const auto [accepted, completed] = DriverDetail::Driver::Get().SerialProgress();
|
||||
std::vector<std::pair<std::uint64_t, std::uint64_t>> blocks;
|
||||
std::vector<std::pair<std::uint64_t, std::uint64_t>> written;
|
||||
for (const auto& span : spans) {
|
||||
GuestMemory::CollectWrittenBlocks(span.address, static_cast<std::size_t>(span.bytes), span.since, written);
|
||||
blocks.insert(blocks.end(), written.begin(), written.end());
|
||||
}
|
||||
auto& driver = DriverDetail::Driver::Get();
|
||||
auto [accepted, completed] = driver.SerialProgress();
|
||||
if (completed < accepted && (!added.empty() || !blocks.empty())) {
|
||||
if (!driver.WaitIdleFor(std::chrono::seconds(10))) {
|
||||
stop("stopped: the GPU work submitted before a memory delta did not finish within 10 s");
|
||||
return;
|
||||
}
|
||||
std::tie(accepted, completed) = driver.SerialProgress();
|
||||
}
|
||||
const auto group = deltaGroups + 1;
|
||||
bool any = false;
|
||||
std::vector<std::pair<std::uint64_t, std::uint64_t>> written;
|
||||
std::vector<std::byte> bytes;
|
||||
for (const auto& range : added) {
|
||||
writer->WriteRange(range.address, range.bytes, range.kind, range.protection, range.access);
|
||||
@@ -397,15 +415,12 @@ void FrameRecorder::writeDelta(std::span<const Span> spans, std::span<const std:
|
||||
deltaBytes += bytes.size();
|
||||
any = true;
|
||||
}
|
||||
for (const auto& span : spans) {
|
||||
GuestMemory::CollectWrittenBlocks(span.address, static_cast<std::size_t>(span.bytes), span.since, written);
|
||||
for (const auto& [begin, end] : written) {
|
||||
bytes.resize(static_cast<std::size_t>(end - begin));
|
||||
GuestMemory::ReadCommittedUnsynced(begin, bytes);
|
||||
writer->WriteDelta(group, completed, begin, bytes);
|
||||
deltaBytes += bytes.size();
|
||||
any = true;
|
||||
}
|
||||
for (const auto& [begin, end] : blocks) {
|
||||
bytes.resize(static_cast<std::size_t>(end - begin));
|
||||
GuestMemory::ReadCommittedUnsynced(begin, bytes);
|
||||
writer->WriteDelta(group, completed, begin, bytes);
|
||||
deltaBytes += bytes.size();
|
||||
any = true;
|
||||
}
|
||||
if (!any) return;
|
||||
deltaGroups = group;
|
||||
@@ -436,8 +451,13 @@ bool FrameRecorder::abandonIfDiskLow() {
|
||||
std::error_code error;
|
||||
const auto space = std::filesystem::space(directory, error);
|
||||
if (error || space.available >= MinimumFreeBytes) return false;
|
||||
warnings.push_back("stopped: less than 4 GiB free on the capture volume");
|
||||
std::fprintf(stderr, "[frame-capture] stopped: less than 4 GiB free on the capture volume\n");
|
||||
stop("stopped: less than 4 GiB free on the capture volume");
|
||||
return true;
|
||||
}
|
||||
|
||||
void FrameRecorder::stop(const char* reason) {
|
||||
warnings.push_back(reason);
|
||||
std::fprintf(stderr, "[frame-capture] %s\n", reason);
|
||||
try {
|
||||
writer->Finish(header());
|
||||
} catch (const std::exception& failure) {
|
||||
@@ -445,7 +465,6 @@ bool FrameRecorder::abandonIfDiskLow() {
|
||||
}
|
||||
writer.reset();
|
||||
state = State::Done;
|
||||
return true;
|
||||
}
|
||||
|
||||
void FrameRecorder::start() {
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <new>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
@@ -1770,7 +1771,7 @@ void Rewatch(std::span<const std::pair<std::uint64_t, std::uint64_t>> ranges) {
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function<bool()>& import, bool staysWatched) {
|
||||
bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function<bool()>& import, bool staysWatched, bool importMayWriteGuest) {
|
||||
if (bytes == 0 || bytes > std::numeric_limits<std::uint64_t>::max() - address) return import();
|
||||
if (!staysWatched) {
|
||||
constexpr std::uint64_t page = 4096;
|
||||
@@ -1790,7 +1791,11 @@ bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function
|
||||
const bool covered = tracker.covers(first, static_cast<std::size_t>(stop - first));
|
||||
const auto runImport = [&] {
|
||||
#ifdef __APPLE__
|
||||
const GuestArena::HostWrite open(reinterpret_cast<void*>(first), static_cast<std::size_t>(stop - first));
|
||||
std::optional<GuestArena::HostWrite> open;
|
||||
if (importMayWriteGuest) open.emplace(reinterpret_cast<void*>(first), static_cast<std::size_t>(stop - first));
|
||||
#endif
|
||||
#ifndef __APPLE__
|
||||
static_cast<void>(importMayWriteGuest);
|
||||
#endif
|
||||
return import();
|
||||
};
|
||||
@@ -1820,12 +1825,26 @@ bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function
|
||||
hashes[static_cast<std::size_t>((cursor - first) / page)] = HashPage(reinterpret_cast<const std::byte*>(cursor));
|
||||
}
|
||||
}
|
||||
std::erase_if(spans, [&](const auto& span) { return !walkWrites(tracker, span.first, span.second, StampKind::Cpu, true); });
|
||||
const auto saveBytes = [&](std::uint64_t begin, std::uint64_t end) {
|
||||
lock.Suspend();
|
||||
beforeUnwatch(begin, static_cast<std::size_t>(end - begin));
|
||||
lock.Resume();
|
||||
};
|
||||
std::vector<std::pair<std::uint64_t, std::uint64_t>> leaving;
|
||||
std::erase_if(spans, [&](const auto& span) {
|
||||
if (walkWrites(tracker, span.first, span.second, StampKind::Cpu, true)) return false;
|
||||
leaving.push_back(span);
|
||||
return true;
|
||||
});
|
||||
for (const auto& [begin, end] : leaving) saveBytes(begin, end);
|
||||
if (!runImport()) return false;
|
||||
const WindowCopy window{first, hashes};
|
||||
auto unwatchedFrom = first;
|
||||
for (const auto& [begin, end] : spans) {
|
||||
if (!walkWrites(tracker, begin, end, StampKind::Cpu, true, &window)) continue;
|
||||
if (!walkWrites(tracker, begin, end, StampKind::Cpu, true, &window)) {
|
||||
saveBytes(begin, end);
|
||||
continue;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
GuestArena::GuestArenaCompareResidentWhenShared_nid_no_patch(static_cast<std::uintptr_t>(begin), static_cast<std::size_t>(end - begin));
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/AspectFit.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
|
||||
|
||||
namespace AgcDriver {
|
||||
@@ -23,7 +24,7 @@ PresentationScaler::~PresentationScaler() {
|
||||
|
||||
void PresentationScaler::release() noexcept {
|
||||
if (sourceImage != VK_NULL_HANDLE) context.Function<PFN_vkDestroyImage>("vkDestroyImage")(context.device, sourceImage, nullptr);
|
||||
if (sourceMemory != VK_NULL_HANDLE) context.Function<PFN_vkFreeMemory>("vkFreeMemory")(context.device, sourceMemory, nullptr);
|
||||
if (sourceMemory != VK_NULL_HANDLE) Graphics::ReleaseImageMemory(context, sourceMemory, sourceBytes, sourceMemoryType);
|
||||
sourceImage = VK_NULL_HANDLE;
|
||||
sourceMemory = VK_NULL_HANDLE;
|
||||
sourceWidth = 0;
|
||||
@@ -50,8 +51,9 @@ void PresentationScaler::EnsureSourceImage(std::uint32_t width, std::uint32_t he
|
||||
context.Function<PFN_vkGetImageMemoryRequirements>("vkGetImageMemoryRequirements")(context.device, sourceImage, &requirements);
|
||||
VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
||||
allocation.allocationSize = requirements.size;
|
||||
allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
||||
Graphics::Check(context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &sourceMemory), "vkAllocateMemory presentation source");
|
||||
allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "presentation source");
|
||||
sourceMemory = Graphics::AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory presentation source", &sourceMemoryType, true);
|
||||
sourceBytes = requirements.size;
|
||||
Graphics::Check(context.Function<PFN_vkBindImageMemory>("vkBindImageMemory")(context.device, sourceImage, sourceMemory, 0), "vkBindImageMemory presentation source");
|
||||
sourceWidth = width;
|
||||
sourceHeight = height;
|
||||
|
||||
@@ -12,6 +12,12 @@
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#elif defined(__APPLE__)
|
||||
#include <mach/mach.h>
|
||||
#include <mach/mach_time.h>
|
||||
#include <mach/thread_policy.h>
|
||||
#include <pthread.h>
|
||||
#include <sys/qos.h>
|
||||
#else
|
||||
#include <dlfcn.h>
|
||||
#include <sched.h>
|
||||
@@ -42,7 +48,30 @@ void LogOnce(const char* role, const std::string& text) {
|
||||
std::fprintf(stderr, "[thread-priority] %s: %s\n", role, text.c_str());
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
#if defined(__APPLE__)
|
||||
|
||||
void Raise(const char* role) {
|
||||
if (RequestedMode() == ThreadPriorityMode::Realtime) {
|
||||
mach_timebase_info_data_t timebase{};
|
||||
mach_timebase_info(&timebase);
|
||||
const auto ticks = [&](std::uint64_t nanos) {
|
||||
return static_cast<std::uint32_t>(nanos * timebase.denom / timebase.numer);
|
||||
};
|
||||
thread_time_constraint_policy_data_t policy{ticks(1000000), ticks(500000), ticks(1000000), TRUE};
|
||||
const auto status = thread_policy_set(pthread_mach_thread_np(pthread_self()), THREAD_TIME_CONSTRAINT_POLICY,
|
||||
reinterpret_cast<thread_policy_t>(&policy), THREAD_TIME_CONSTRAINT_POLICY_COUNT);
|
||||
if (status != KERN_SUCCESS)
|
||||
throw std::runtime_error(std::string("APS5_THREAD_PRIORITY=rt: ") + role + " time-constraint policy refused (kern_return " + std::to_string(status) + ")");
|
||||
LogOnce(role, "Mach time-constraint policy");
|
||||
return;
|
||||
}
|
||||
const int error = pthread_set_qos_class_self_np(QOS_CLASS_USER_INTERACTIVE, 0);
|
||||
if (error != 0)
|
||||
throw std::runtime_error(std::string("APS5_THREAD_PRIORITY=high: ") + role + " user-interactive QoS refused: " + std::strerror(error));
|
||||
LogOnce(role, "user-interactive QoS");
|
||||
}
|
||||
|
||||
#elif !defined(_WIN32)
|
||||
|
||||
struct DBusConnection;
|
||||
struct DBusMessage;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "SpirvBackend/SpirvOptimizer.hpp"
|
||||
#endif
|
||||
#include "prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/DeferredDestruction.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/FloatFlushProbe.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
|
||||
@@ -369,6 +370,8 @@ struct VulkanDevice::State {
|
||||
void* markerMapping = nullptr;
|
||||
BufferMarkerLog markerLog;
|
||||
bool deviceLostHook = false;
|
||||
bool deferDestruction = false;
|
||||
PFN_vkGetDeviceProcAddr contextDeviceProc = nullptr;
|
||||
VkTimeDomainKHR hostTimeDomain = VK_TIME_DOMAIN_DEVICE_KHR;
|
||||
VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES};
|
||||
std::vector<std::uint32_t> capabilities{1};
|
||||
@@ -945,6 +948,7 @@ struct VulkanDevice::State {
|
||||
destroyPool(device, pool, nullptr);
|
||||
}
|
||||
if (settlePool != VK_NULL_HANDLE) destroyPool(device, settlePool, nullptr);
|
||||
Graphics::DestroyDeferredObjects(device);
|
||||
destroyDevice(device, nullptr);
|
||||
}
|
||||
if (instance != VK_NULL_HANDLE) {
|
||||
@@ -1283,6 +1287,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
|
||||
VkPhysicalDeviceProperties2 driverProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &driver};
|
||||
state->InstanceFunction<PFN_vkGetPhysicalDeviceProperties2>("vkGetPhysicalDeviceProperties2")(selected, &driverProperties);
|
||||
driverId = driver.driverID;
|
||||
state->deferDestruction = driver.driverID == VK_DRIVER_ID_MOLTENVK && std::getenv("APS5_NO_DEFERRED_DESTRUCTION") == nullptr;
|
||||
state->driverInfo = std::string(driver.driverName) + " " + driver.driverInfo;
|
||||
state->driverId = driver.driverID;
|
||||
state->individuallyFreeDescriptorSets = driver.driverID == VK_DRIVER_ID_MESA_LLVMPIPE;
|
||||
@@ -2095,6 +2100,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
|
||||
check(state->InstanceFunction<PFN_vkCreateDevice>("vkCreateDevice")(selected, &deviceInfo, nullptr, &state->device), "vkCreateDevice");
|
||||
state->shaderProfile = std::move(shaderProfile);
|
||||
if (compileOnly) return;
|
||||
if (state->deferDestruction) state->contextDeviceProc = Graphics::DeferDestructionToIdle(state->device, state->deviceProc);
|
||||
if (calibratedTimestamps != nullptr) {
|
||||
const bool khr = std::strcmp(calibratedTimestamps, VK_KHR_CALIBRATED_TIMESTAMPS_EXTENSION_NAME) == 0;
|
||||
const auto domains = reinterpret_cast<PFN_vkGetPhysicalDeviceCalibrateableTimeDomainsKHR>(state->instanceProc(state->instance, khr ? "vkGetPhysicalDeviceCalibrateableTimeDomainsKHR" : "vkGetPhysicalDeviceCalibrateableTimeDomainsEXT"));
|
||||
@@ -2362,6 +2368,7 @@ void VulkanDevice::PrepareForReplacement() {
|
||||
|
||||
void VulkanDevice::WaitIdle() {
|
||||
APS5_LOG_CHARS_OUT_DEBUG("VulkanDevice::WaitIdle begin");
|
||||
static_cast<void>(Graphics::StorageTexture::StoreDrawnLinear("wait idle"));
|
||||
RetirePresents(0);
|
||||
if (state->recorder) {
|
||||
// Announced with this call's return address: the [recorder] site table then names the
|
||||
@@ -2468,6 +2475,7 @@ bool VulkanDevice::StoresPendingOver(std::uint64_t address, std::size_t bytes) c
|
||||
int VulkanDevice::WriteLabelOnGpu(std::uint64_t address, std::span<const std::byte> bytes, std::uint64_t stamp, std::uint32_t queue, bool reapFirst) {
|
||||
static const bool storeAtLabel = Graphics::StorageTexture::StoreAtLabelRequested(std::getenv("APS5_STORE_AT_LABEL"));
|
||||
if (storeAtLabel) Graphics::StorageTexture::FlushAllPending("label");
|
||||
if (queue == 0) static_cast<void>(Graphics::StorageTexture::StoreDrawnLinear("label"));
|
||||
Graphics::StorageTexture::StoreReadbackBeforeLabel("readback before label");
|
||||
if (!state->recorder || bytes.empty() || bytes.size() > 65536 || address % 4 != 0 || bytes.size() % 4 != 0) return 4;
|
||||
auto& recorder = *state->recorder;
|
||||
@@ -3355,6 +3363,15 @@ FrameDumps& Dumps() {
|
||||
}
|
||||
|
||||
bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) {
|
||||
if (buffer.dccAddress != 0 && buffer.tilingMode == 0) {
|
||||
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
|
||||
const auto before = profile ? Graphics::Recorder::ThreadWaitedMs() : 0.0;
|
||||
state->recorder->SyncThrough(buffer.dccAddress, DisplayBufferKeyBytes(buffer), true);
|
||||
if (const auto waited = profile ? Graphics::Recorder::ThreadWaitedMs() - before : 0.0; waited > 0.0) {
|
||||
++presentCounters.keyWriterWaits;
|
||||
presentCounters.keyWriterWaitMs += waited;
|
||||
}
|
||||
}
|
||||
static const bool storeAtFlip = Graphics::StorageTexture::StoreAtFlipRequested(std::getenv("APS5_STORE_AT_FLIP"));
|
||||
if (storeAtFlip) Graphics::StorageTexture::FlushAllPending("flip");
|
||||
else Graphics::StorageTexture::FlushDumpSelected("flip");
|
||||
@@ -3975,7 +3992,7 @@ Graphics::Context VulkanDevice::buildContext() const {
|
||||
state->physical,
|
||||
state->queue,
|
||||
state->pool,
|
||||
state->deviceProc,
|
||||
state->contextDeviceProc != nullptr ? state->contextDeviceProc : state->deviceProc,
|
||||
state->InstanceFunction<PFN_vkGetPhysicalDeviceFormatProperties>("vkGetPhysicalDeviceFormatProperties"),
|
||||
state->InstanceFunction<PFN_vkGetPhysicalDeviceImageFormatProperties>("vkGetPhysicalDeviceImageFormatProperties"),
|
||||
state->memoryProperties,
|
||||
@@ -5021,6 +5038,7 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) {
|
||||
};
|
||||
if (recorder.HasQueuedKeyStores() && (resources.HoldsLease() || recorder.AnyQueuedKeyStore(touches))) recorder.FlushKeyStores();
|
||||
if (recorder.HasQueuedStores() && (resources.HoldsLease() || recorder.AnyQueuedStore(touches))) recorder.FlushStores();
|
||||
static_cast<void>(Graphics::StorageTexture::StoreDrawnLinearTouching("dispatch access", touches, resources.HoldsLease()));
|
||||
VkAccessFlags covered = 0;
|
||||
const auto commands = recorder.Commands(&covered);
|
||||
recordStep(PhaseRecordCommands);
|
||||
|
||||
@@ -163,18 +163,29 @@ struct Sampler {
|
||||
return frame;
|
||||
}
|
||||
|
||||
static void writeModules(std::FILE* file, const std::map<std::uint64_t, std::string>& loaded) {
|
||||
for (const auto& [base, name] : loaded) {
|
||||
const auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(base);
|
||||
const auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS*>(base + dos->e_lfanew);
|
||||
std::fprintf(file, "# module %s 0x%lx 0x%lx\n", name.c_str(), static_cast<unsigned long>(nt->OptionalHeader.SizeOfImage), static_cast<unsigned long>(nt->FileHeader.TimeDateStamp));
|
||||
}
|
||||
}
|
||||
|
||||
void write() {
|
||||
std::lock_guard lock(mutex);
|
||||
std::FILE* file = std::fopen(path.c_str(), "w");
|
||||
if (file == nullptr) return;
|
||||
std::fprintf(file, "# %llu samples\n", static_cast<unsigned long long>(samples));
|
||||
std::map<std::uint64_t, std::string> loaded;
|
||||
for (const auto& [address, count] : counts) {
|
||||
HMODULE module = nullptr;
|
||||
char name[MAX_PATH] = "?";
|
||||
if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast<LPCSTR>(address), &module)) GetModuleFileNameA(module, name, sizeof(name));
|
||||
const char* base = std::strrchr(name, '\\');
|
||||
if (module != nullptr) loaded.emplace(reinterpret_cast<std::uint64_t>(module), base ? base + 1 : name);
|
||||
std::fprintf(file, "%s 0x%llx %llu %llu\n", base ? base + 1 : name, static_cast<unsigned long long>(address - reinterpret_cast<std::uint64_t>(module)), static_cast<unsigned long long>(count.first), static_cast<unsigned long long>(count.second));
|
||||
}
|
||||
writeModules(file, loaded);
|
||||
std::fclose(file);
|
||||
if (keepStacks) writeStacks();
|
||||
}
|
||||
@@ -195,11 +206,7 @@ struct Sampler {
|
||||
const double tickUs = 1e6 / static_cast<double>(frequency.QuadPart);
|
||||
std::fprintf(file, "# samples=%llu depth=%d truncated=%llu suspended_us_mean=%.1f suspended_us_max=%.1f\n", static_cast<unsigned long long>(samples), depth, static_cast<unsigned long long>(stacks.Truncated()),
|
||||
samples != 0 ? static_cast<double>(suspendedTicks) * tickUs / static_cast<double>(samples) : 0.0, static_cast<double>(longestSuspendedTicks) * tickUs);
|
||||
for (const auto& [base, name] : loaded) {
|
||||
const auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(base);
|
||||
const auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS*>(base + dos->e_lfanew);
|
||||
std::fprintf(file, "# module %s 0x%lx 0x%lx\n", name.c_str(), static_cast<unsigned long>(nt->OptionalHeader.SizeOfImage), static_cast<unsigned long>(nt->FileHeader.TimeDateStamp));
|
||||
}
|
||||
writeModules(file, loaded);
|
||||
const auto text = stacks.Format([&](std::uint64_t address) { return names.at(address); });
|
||||
const bool written = std::fwrite(text.data(), 1, text.size(), file) == text.size();
|
||||
if (std::fclose(file) != 0 || !written) return;
|
||||
@@ -246,6 +253,7 @@ struct ProcessSampler {
|
||||
std::FILE* file = std::fopen(path.c_str(), "w");
|
||||
if (file == nullptr) return;
|
||||
std::fprintf(file, "# %llu rounds\n", static_cast<unsigned long long>(rounds));
|
||||
std::map<std::uint64_t, std::string> loaded;
|
||||
for (auto& [id, thread] : threads) {
|
||||
std::lock_guard lock(thread.mutex);
|
||||
std::vector<std::pair<std::uint64_t, std::pair<std::uint64_t, std::uint64_t>>> hot(thread.counts.begin(), thread.counts.end());
|
||||
@@ -260,9 +268,11 @@ struct ProcessSampler {
|
||||
char name[MAX_PATH] = "?";
|
||||
if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast<LPCSTR>(address), &module)) GetModuleFileNameA(module, name, sizeof(name));
|
||||
const char* base = std::strrchr(name, '\\');
|
||||
if (module != nullptr) loaded.emplace(reinterpret_cast<std::uint64_t>(module), base ? base + 1 : name);
|
||||
std::fprintf(file, " %s 0x%llx %llu %llu\n", base ? base + 1 : name, static_cast<unsigned long long>(address - reinterpret_cast<std::uint64_t>(module)), static_cast<unsigned long long>(hot[i].second.first), static_cast<unsigned long long>(hot[i].second.second));
|
||||
}
|
||||
}
|
||||
Sampler::writeModules(file, loaded);
|
||||
std::fclose(file);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_DEFERREDDESTRUCTION_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_DEFERREDDESTRUCTION_HPP
|
||||
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Context.hpp"
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
|
||||
namespace AgcDriver::Graphics {
|
||||
|
||||
PFN_vkGetDeviceProcAddr DeferDestructionToIdle(VkDevice device, PFN_vkGetDeviceProcAddr resolver);
|
||||
void DestroyDeferredAtIdle(const Context& context, std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now());
|
||||
void DestroyDeferredObjects(VkDevice device) noexcept;
|
||||
std::size_t DeferredObjects(VkDevice device);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -109,8 +109,10 @@ private:
|
||||
// stages and every pipeline state field. A configuration whose stage has no module gets a private
|
||||
// pipeline. A miss builds the pipeline outside the store's mutex (a concurrent build of the same
|
||||
// key keeps the first inserted) and records its recipe in the pipeline journal. Entries are
|
||||
// LRU-bounded (APS5_PIPELINE_STORE_ENTRIES, 1024) and only evicted once no recorded draw holds
|
||||
// them. Debug aid: APS5_NO_PIPELINE_CACHE=1 builds one per draw as before.
|
||||
// LRU-bounded and only evicted once no recorded draw holds them. The bound starts at 1024 and
|
||||
// doubles up to 4096 for a journal replay that does not fit and while pipelines it evicted keep
|
||||
// coming back; it never shrinks during a run. APS5_PIPELINE_STORE_ENTRIES fixes it. Debug aid:
|
||||
// APS5_NO_PIPELINE_CACHE=1 builds one per draw as before.
|
||||
std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
|
||||
// `created` reports whether this call built the pipeline (false: served from the store or private).
|
||||
std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, bool* created = nullptr);
|
||||
@@ -124,6 +126,8 @@ struct PipelineStoreCounters {
|
||||
std::uint64_t bound = 0;
|
||||
};
|
||||
PipelineStoreCounters PipelineStoreTotals();
|
||||
// Whether the store may still raise its bound (not fixed by APS5_PIPELINE_STORE_ENTRIES, below 4096).
|
||||
bool PipelineStoreCanGrow();
|
||||
// The pipeline store's key of a configuration (empty when the configuration gets a private pipeline).
|
||||
void BuildPipelineStoreKey(std::vector<std::byte>& key, const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout);
|
||||
struct DescribedSetLayouts {
|
||||
|
||||
@@ -183,6 +183,7 @@ public:
|
||||
// Whether the OPEN (unsubmitted) batch writes the range: a wait on such a range must submit it.
|
||||
bool OpenWriteOverlaps(std::uint64_t address, std::size_t bytes) const;
|
||||
std::uint64_t PendingWriterSerial(std::uint64_t address, std::size_t bytes) const;
|
||||
std::uint64_t PendingWriterSerial(std::uint64_t address, std::size_t bytes, std::span<const std::uint64_t> exceptNotes) const;
|
||||
// What SyncThrough(address, bytes) would wait for, for the [hooksync] attribution: the newest
|
||||
// batch whose noted range overlaps the access (the open batch counts first, with the serial it
|
||||
// will get), whether that batch's fence has already signaled (its GPU work is done, so the
|
||||
|
||||
@@ -32,6 +32,7 @@ void FlushCachedTextures(VkDevice device);
|
||||
void ClearCachedTextures(VkDevice device);
|
||||
std::uint64_t ReleaseTextureCaches(std::uint64_t bytes, bool newImage = false);
|
||||
std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory);
|
||||
std::uint64_t StorageTextureCeiling(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported);
|
||||
std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t sampledBytes, std::uint64_t storageBytes);
|
||||
std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes, std::uint64_t storageBytes = 0);
|
||||
bool SampledBudgetReportDue(std::uint64_t reported, std::uint64_t budget, std::chrono::steady_clock::duration sinceReport);
|
||||
|
||||
@@ -181,6 +181,7 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup =
|
||||
void ApplyPointSize(const State& state, ShaderRecompiler::ShaderVertexStageInfo& vertex);
|
||||
State DecodeState(const QueueState& queue, bool vertexWorkgroup = false);
|
||||
void ApplyDepthRemap(const Context& context, State& state);
|
||||
void ApplyDepthBoundsEmulation(const Context& context, State& state, const ShaderRecompiler::ShaderPixelStageInfo& pixel);
|
||||
void NormalizeUnusedViewportDepth(State& state, const ShaderRecompiler::ShaderPixelStageInfo& pixel);
|
||||
std::array<VkSampleLocationEXT, 32> SampleLocations(const State& state);
|
||||
std::array<std::uint8_t, 32> PixelSampleLocations(const State& state, const ShaderRecompiler::ShaderPixelStageInfo& pixel);
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <functional>
|
||||
#include <span>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
@@ -204,6 +205,7 @@ public:
|
||||
bool BlockUnits() const { return blockUnits; }
|
||||
std::uint32_t TrackedUnits() const { return trackedLayers; }
|
||||
void MarkSliceDirty(std::uint32_t slice, std::uint32_t count = 1);
|
||||
bool MarkDrawn(std::uint32_t slice, std::uint32_t count, VkRect2D scissor);
|
||||
void Flush();
|
||||
bool Dirty() const;
|
||||
// Stores every pending image overlapping the range, except `except`; returns whether any was.
|
||||
@@ -220,6 +222,8 @@ public:
|
||||
static constexpr std::uint64_t SmallStoreBytes = 64 * 1024;
|
||||
static std::size_t StoreSmallPending(const char* reason);
|
||||
static void StoreReadbackBeforeLabel(const char* reason);
|
||||
static std::size_t StoreDrawnLinear(const char* reason);
|
||||
static std::size_t StoreDrawnLinearTouching(const char* reason, const std::function<bool(std::uint64_t, std::uint64_t)>& touches, bool all);
|
||||
static bool AnyPending();
|
||||
static void FlushDumpSelected(const char* reason);
|
||||
static bool StoreAtFlipRequested(const char* value);
|
||||
@@ -482,6 +486,27 @@ private:
|
||||
// (`shadowed` receives those guest ranges, `imported` the pieces written to the import).
|
||||
std::uint64_t uploadWindows(const HostImport& import, std::span<const std::pair<std::uint64_t, std::uint64_t>> runs, bool discard = false);
|
||||
std::uint64_t writeBackWindows(std::span<const ImportSegment> segments, std::span<const std::pair<std::uint64_t, std::uint64_t>> keep, std::uint64_t firstStored, std::uint64_t lastStored, std::vector<ShadowedRange>& shadowed, std::vector<std::pair<std::uint64_t, std::uint64_t>>& imported, Buffer* save = nullptr);
|
||||
struct DrawnRect {
|
||||
std::uint32_t left = 0;
|
||||
std::uint32_t top = 0;
|
||||
std::uint32_t right = 0;
|
||||
std::uint32_t bottom = 0;
|
||||
};
|
||||
struct DrawnArea {
|
||||
bool drawn = false;
|
||||
bool rect = false;
|
||||
std::uint32_t left = 0;
|
||||
std::uint32_t top = 0;
|
||||
std::uint32_t right = 0;
|
||||
std::uint32_t bottom = 0;
|
||||
std::vector<DrawnRect> rects;
|
||||
};
|
||||
DrawnArea drawnArea() const;
|
||||
std::vector<std::pair<std::uint64_t, std::uint64_t>> drawnSpans(const DrawnArea& area, std::span<const std::pair<std::uint64_t, std::uint64_t>> keep) const;
|
||||
std::uint64_t storeDrawnRect(const DrawnArea& area, std::span<const ImportSegment> segments, std::span<const std::pair<std::uint64_t, std::uint64_t>> stores, const std::vector<std::uint32_t>& saveUnits);
|
||||
void addDrawnRect(DrawnRect rect);
|
||||
void forgetDrawn();
|
||||
void noteStore(std::uint64_t note);
|
||||
// Per-layer validity (thin array surfaces whose layers are 64 KiB-aligned guest slices; every
|
||||
// other surface is one tracked layer): a layer's generation is the write generation its guest
|
||||
// bytes were last known to match the image at, and a pending layer holds results (a clear, a
|
||||
@@ -518,6 +543,7 @@ private:
|
||||
// Per 64 KiB tracker block of the surface (block 0 holds the base address): the generation of
|
||||
// the tracked layer it belongs to, for GuestMemory::ChangedBlocks.
|
||||
void blockGenerations(std::vector<std::uint64_t>& generations) const;
|
||||
std::uint64_t unitBlockGeneration(std::uint32_t unit, std::uint64_t at) const;
|
||||
// Another image of exactly this surface under another storage format with results pending
|
||||
// (block units, uncompressed, still cached), if any: Refresh takes its pending units from its
|
||||
// image on the device (borrowUnits) instead of having them stored and re-read, and the alias
|
||||
@@ -602,6 +628,7 @@ private:
|
||||
std::uint32_t trackedLayers = 1;
|
||||
std::uint64_t trackedLayerBytes = 0;
|
||||
std::vector<std::uint64_t> layerGeneration;
|
||||
std::vector<std::uint64_t> completionKept;
|
||||
std::vector<bool> layerPending;
|
||||
std::vector<bool> layerBehind;
|
||||
bool blockUnits = false;
|
||||
@@ -641,6 +668,12 @@ private:
|
||||
VkFormat storageFormat = VK_FORMAT_UNDEFINED;
|
||||
// Results are on the GPU only (guarded by the pending-write registry lock).
|
||||
bool dirty = false;
|
||||
bool drawnListed = false;
|
||||
bool drawnWhole = false;
|
||||
bool drawingTarget = false;
|
||||
bool pendingFromOthers = false;
|
||||
std::vector<DrawnRect> drawnRects;
|
||||
std::vector<std::uint64_t> storeNotes;
|
||||
std::uint64_t version = 0;
|
||||
std::uint64_t writes = 0;
|
||||
// `original` holds the guest bytes; false after a GPU-side clear, which never read them.
|
||||
|
||||
@@ -75,7 +75,7 @@ struct SurfaceGeometry {
|
||||
};
|
||||
SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor);
|
||||
bool LevelsFitAllocation(const GuestTextureResource& surface, std::uint32_t levels);
|
||||
GuestTextureResource ViewedArrayWindow(const GuestTextureResource& viewed);
|
||||
GuestTextureResource ViewedArrayWindow(const GuestTextureResource& viewed, bool keysReadAsStored);
|
||||
GuestTextureResource ArrayLayerWindow(const GuestTextureResource& viewed, std::uint32_t maxArrayLayers);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
#include "prx/libSceAgcDriver/Graphics/include/DeferredDestruction.hpp"
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace AgcDriver::Graphics {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::size_t DrainObjects = 512;
|
||||
constexpr auto DrainAge = std::chrono::milliseconds(100);
|
||||
|
||||
struct Deferred {
|
||||
PFN_vkDestroyBuffer destroyBuffer = nullptr;
|
||||
PFN_vkFreeMemory freeMemory = nullptr;
|
||||
std::vector<VkBuffer> buffers;
|
||||
std::vector<VkDeviceMemory> memory;
|
||||
std::chrono::steady_clock::time_point oldest{};
|
||||
};
|
||||
|
||||
struct Registry {
|
||||
std::mutex mutex;
|
||||
std::unordered_map<VkDevice, Deferred> devices;
|
||||
std::atomic<PFN_vkGetDeviceProcAddr> resolver{nullptr};
|
||||
};
|
||||
|
||||
Registry& registry() {
|
||||
static auto& state = *new Registry();
|
||||
return state;
|
||||
}
|
||||
|
||||
void destroyAll(VkDevice device, const Deferred& deferred) noexcept {
|
||||
for (const auto buffer : deferred.buffers) deferred.destroyBuffer(device, buffer, nullptr);
|
||||
for (const auto memory : deferred.memory) deferred.freeMemory(device, memory, nullptr);
|
||||
}
|
||||
|
||||
template<typename THandle>
|
||||
bool defer(VkDevice device, THandle handle, std::vector<THandle> Deferred::*list) noexcept {
|
||||
try {
|
||||
auto& state = registry();
|
||||
std::lock_guard lock(state.mutex);
|
||||
const auto found = state.devices.find(device);
|
||||
if (found == state.devices.end()) return false;
|
||||
auto& deferred = found->second;
|
||||
if (deferred.buffers.empty() && deferred.memory.empty()) deferred.oldest = std::chrono::steady_clock::now();
|
||||
(deferred.*list).push_back(handle);
|
||||
return true;
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL deferredDestroyBuffer(VkDevice device, VkBuffer buffer, const VkAllocationCallbacks* allocator) {
|
||||
if (buffer == VK_NULL_HANDLE || defer(device, buffer, &Deferred::buffers)) return;
|
||||
reinterpret_cast<PFN_vkDestroyBuffer>(registry().resolver.load(std::memory_order_acquire)(device, "vkDestroyBuffer"))(device, buffer, allocator);
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL deferredFreeMemory(VkDevice device, VkDeviceMemory memory, const VkAllocationCallbacks* allocator) {
|
||||
if (memory == VK_NULL_HANDLE || defer(device, memory, &Deferred::memory)) return;
|
||||
reinterpret_cast<PFN_vkFreeMemory>(registry().resolver.load(std::memory_order_acquire)(device, "vkFreeMemory"))(device, memory, allocator);
|
||||
}
|
||||
|
||||
VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL deferringDeviceProc(VkDevice device, const char* name) {
|
||||
if (std::strcmp(name, "vkDestroyBuffer") == 0) return reinterpret_cast<PFN_vkVoidFunction>(&deferredDestroyBuffer);
|
||||
if (std::strcmp(name, "vkFreeMemory") == 0) return reinterpret_cast<PFN_vkVoidFunction>(&deferredFreeMemory);
|
||||
return registry().resolver.load(std::memory_order_acquire)(device, name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
PFN_vkGetDeviceProcAddr DeferDestructionToIdle(VkDevice device, PFN_vkGetDeviceProcAddr resolver) {
|
||||
if (device == VK_NULL_HANDLE || resolver == nullptr) throw std::invalid_argument("AGC graphics: deferred destruction needs a device and its resolver");
|
||||
Deferred deferred;
|
||||
deferred.destroyBuffer = reinterpret_cast<PFN_vkDestroyBuffer>(resolver(device, "vkDestroyBuffer"));
|
||||
deferred.freeMemory = reinterpret_cast<PFN_vkFreeMemory>(resolver(device, "vkFreeMemory"));
|
||||
if (deferred.destroyBuffer == nullptr || deferred.freeMemory == nullptr) throw std::runtime_error("AGC graphics: missing Vulkan function: vkDestroyBuffer or vkFreeMemory");
|
||||
auto& state = registry();
|
||||
std::lock_guard lock(state.mutex);
|
||||
const auto current = state.resolver.load(std::memory_order_acquire);
|
||||
if (current != nullptr && current != resolver) throw std::runtime_error("AGC graphics: deferred destruction found two different device resolvers");
|
||||
if (!state.devices.emplace(device, std::move(deferred)).second) throw std::runtime_error("AGC graphics: deferred destruction was set up twice for one device");
|
||||
state.resolver.store(resolver, std::memory_order_release);
|
||||
return &deferringDeviceProc;
|
||||
}
|
||||
|
||||
void DestroyDeferredAtIdle(const Context& context, std::chrono::steady_clock::time_point now) {
|
||||
auto& state = registry();
|
||||
{
|
||||
std::lock_guard lock(state.mutex);
|
||||
const auto found = state.devices.find(context.device);
|
||||
if (found == state.devices.end()) return;
|
||||
const auto objects = found->second.buffers.size() + found->second.memory.size();
|
||||
if (objects == 0 || (objects < DrainObjects && now - found->second.oldest < DrainAge)) return;
|
||||
}
|
||||
const auto idle = context.Function<PFN_vkQueueWaitIdle>("vkQueueWaitIdle")(context.queue);
|
||||
if (idle != VK_SUCCESS && idle != VK_ERROR_DEVICE_LOST) throw std::runtime_error("AGC graphics: vkQueueWaitIdle before destroying deferred objects: Vulkan result " + std::to_string(idle));
|
||||
Deferred taken;
|
||||
{
|
||||
std::lock_guard lock(state.mutex);
|
||||
const auto found = state.devices.find(context.device);
|
||||
if (found == state.devices.end()) return;
|
||||
taken.destroyBuffer = found->second.destroyBuffer;
|
||||
taken.freeMemory = found->second.freeMemory;
|
||||
taken.buffers.swap(found->second.buffers);
|
||||
taken.memory.swap(found->second.memory);
|
||||
}
|
||||
destroyAll(context.device, taken);
|
||||
}
|
||||
|
||||
void DestroyDeferredObjects(VkDevice device) noexcept {
|
||||
Deferred taken;
|
||||
{
|
||||
auto& state = registry();
|
||||
std::lock_guard lock(state.mutex);
|
||||
const auto found = state.devices.find(device);
|
||||
if (found == state.devices.end()) return;
|
||||
taken = std::move(found->second);
|
||||
state.devices.erase(found);
|
||||
}
|
||||
destroyAll(device, taken);
|
||||
}
|
||||
|
||||
std::size_t DeferredObjects(VkDevice device) {
|
||||
auto& state = registry();
|
||||
std::lock_guard lock(state.mutex);
|
||||
const auto found = state.devices.find(device);
|
||||
return found == state.devices.end() ? 0 : found->second.buffers.size() + found->second.memory.size();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -719,10 +719,30 @@ public:
|
||||
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT);
|
||||
context.Function<PFN_vkCmdCopyImageToBuffer>("vkCmdCopyImageToBuffer")(commands, storage->Image(), VK_IMAGE_LAYOUT_GENERAL, depthPlane.staging->Handle(), 1, &colorRegion);
|
||||
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT);
|
||||
context.Function<PFN_vkCmdCopyBufferToImage>("vkCmdCopyBufferToImage")(commands, depthPlane.staging->Handle(), image, VK_IMAGE_LAYOUT_GENERAL, 1, &depthRegion);
|
||||
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT);
|
||||
if (target.stencilAddress == 0) {
|
||||
VkImageMemoryBarrier discard{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
|
||||
discard.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
discard.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
discard.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||
discard.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
discard.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
discard.image = image;
|
||||
discard.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1};
|
||||
const auto barrier = context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier");
|
||||
barrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &discard);
|
||||
context.Function<PFN_vkCmdCopyBufferToImage>("vkCmdCopyBufferToImage")(commands, depthPlane.staging->Handle(), image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &depthRegion);
|
||||
VkImageMemoryBarrier toGeneral = discard;
|
||||
toGeneral.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
toGeneral.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
toGeneral.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||
toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL;
|
||||
barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral);
|
||||
} else {
|
||||
context.Function<PFN_vkCmdCopyBufferToImage>("vkCmdCopyBufferToImage")(commands, depthPlane.staging->Handle(), image, VK_IMAGE_LAYOUT_GENERAL, 1, &depthRegion);
|
||||
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT);
|
||||
}
|
||||
if (batch) batch->SubmitAndWait();
|
||||
else Recorder::CountBarriers(Recorder::CommandClass::Copy, 3);
|
||||
else Recorder::CountBarriers(Recorder::CommandClass::Copy, target.stencilAddress == 0 ? 4 : 3);
|
||||
}
|
||||
|
||||
std::shared_ptr<StorageTexture> Mirror(const GuestTextureResource& resource) {
|
||||
@@ -1227,8 +1247,10 @@ public:
|
||||
continue;
|
||||
}
|
||||
surface = {};
|
||||
const GuestLayer current{resource.baseAddress + geometry.GuestLayerOffset(layer), geometry.layerBytes, resource.tileMode, geometry.thick, GuestMemory::CollectWrites(resource.baseAddress + geometry.GuestLayerOffset(layer), static_cast<std::size_t>(geometry.layerBytes))};
|
||||
if (held.generation != 0 && held.address == current.address && held.bytes == current.bytes && held.tileMode == current.tileMode && held.thick == current.thick && !Recorder::SnapshotWriteOverlaps(current.address, static_cast<std::size_t>(current.bytes)) && GuestMemory::UnchangedSince(current.address, static_cast<std::size_t>(current.bytes), held.generation)) {
|
||||
GuestLayer current{resource.baseAddress + geometry.GuestLayerOffset(layer), geometry.layerBytes, resource.tileMode, geometry.thick, GuestMemory::CollectWrites(resource.baseAddress + geometry.GuestLayerOffset(layer), static_cast<std::size_t>(geometry.layerBytes))};
|
||||
current.uncommitted = current.generation == 0 && GuestMemory::CommittedRanges(current.address, static_cast<std::size_t>(current.bytes)).empty();
|
||||
const bool unchanged = current.uncommitted ? held.uncommitted : held.generation != 0 && GuestMemory::UnchangedSince(current.address, static_cast<std::size_t>(current.bytes), held.generation);
|
||||
if (held.address == current.address && held.bytes == current.bytes && held.tileMode == current.tileMode && held.thick == current.thick && !Recorder::SnapshotWriteOverlaps(current.address, static_cast<std::size_t>(current.bytes)) && unchanged) {
|
||||
held.generation = current.generation;
|
||||
continue;
|
||||
}
|
||||
@@ -1266,6 +1288,7 @@ private:
|
||||
TextureTileMode tileMode = TextureTileMode::kLinear;
|
||||
bool thick = false;
|
||||
std::uint64_t generation = 0;
|
||||
bool uncommitted = false;
|
||||
};
|
||||
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
|
||||
@@ -1921,7 +1921,7 @@ struct Kept {
|
||||
// The completion side of a recorded draw: the kept objects, the record check, the lease outcome,
|
||||
// the GPU write notes, the copied-buffer write-back (listed in DrawCopiedWriters or run as a
|
||||
// completion action) and the targets marked dirty.
|
||||
void keepRecordedDraw(Recorder& recorder, const std::shared_ptr<ShaderResources>& resources, std::shared_ptr<Pipeline> pipeline, std::shared_ptr<Framebuffer> framebuffer, DrawInputs& inputs, std::vector<std::shared_ptr<StorageTexture>> targets, std::vector<std::pair<std::uint32_t, std::uint32_t>> slices, std::unique_ptr<DeviceBuffer> scratch, std::function<void()> checkRecords, bool listed, bool completion, const DrawOutcome& outcome) {
|
||||
void keepRecordedDraw(Recorder& recorder, const std::shared_ptr<ShaderResources>& resources, std::shared_ptr<Pipeline> pipeline, std::shared_ptr<Framebuffer> framebuffer, DrawInputs& inputs, std::vector<std::shared_ptr<StorageTexture>> targets, std::vector<std::pair<std::uint32_t, std::uint32_t>> slices, VkRect2D scissor, std::unique_ptr<DeviceBuffer> scratch, std::function<void()> checkRecords, bool listed, bool completion, const DrawOutcome& outcome) {
|
||||
auto kept = std::make_shared<Kept>();
|
||||
kept->resources = resources;
|
||||
kept->pipeline = std::move(pipeline);
|
||||
@@ -1961,14 +1961,15 @@ void keepRecordedDraw(Recorder& recorder, const std::shared_ptr<ShaderResources>
|
||||
if (resources->Reusable()) recorder.OnComplete([resources] { resources->CheckFaults(); });
|
||||
else recorder.OnComplete([resources] { resources->WriteBackBuffers(); });
|
||||
}
|
||||
std::vector<bool> storeNow(residents.size(), false);
|
||||
for (std::size_t index = 0; index < residents.size(); ++index) {
|
||||
if (residents[index] == nullptr) continue;
|
||||
if (index < slices.size()) residents[index]->MarkSliceDirty(slices[index].first, slices[index].second);
|
||||
if (residents[index]->Descriptor().tileMode == TextureTileMode::kLinear) storeNow[index] = !residents[index]->MarkDrawn(index < slices.size() ? slices[index].first : 0u, index < slices.size() ? slices[index].second : 0u, scissor) || !residents[index]->Cached();
|
||||
else if (index < slices.size()) residents[index]->MarkSliceDirty(slices[index].first, slices[index].second);
|
||||
else residents[index]->MarkDirty();
|
||||
}
|
||||
for (const auto& resident : residents) {
|
||||
if (resident == nullptr || resident->Descriptor().tileMode != TextureTileMode::kLinear) continue;
|
||||
StorageTexture::FlushPending(resident->Descriptor().baseAddress, static_cast<std::size_t>(resident->GuestBytes()), nullptr, "linear target");
|
||||
for (std::size_t index = 0; index < residents.size(); ++index) {
|
||||
if (storeNow[index]) StorageTexture::FlushPending(residents[index]->Descriptor().baseAddress, static_cast<std::size_t>(residents[index]->GuestBytes()), nullptr, "linear target");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2195,7 +2196,6 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
|
||||
recorder->FlushStores();
|
||||
outcome.storesFlushed = true;
|
||||
}
|
||||
const auto drawBindings = resources.PrepareDrawBindings(*recorder, record.moved);
|
||||
const bool capture = CaptureInputsEnabled();
|
||||
const bool meshIndirect = state.stages.mesh && args != nullptr;
|
||||
const bool addressBased = resources.HoldsLease();
|
||||
@@ -2217,6 +2217,8 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
|
||||
}
|
||||
for (const auto image : record.multisampleImages) passAttachments.emplace_back(image, true);
|
||||
const bool unknownMemory = addressBased || resources.UsesAddressTable() || resources.HasCopiedWrites();
|
||||
static_cast<void>(StorageTexture::StoreDrawnLinearTouching("draw access", [&](std::uint64_t begin, std::uint64_t end) { return std::any_of(passReads.begin(), passReads.end(), [&](const auto& read) { return read.first < end && begin < read.second; }) || resources.WritesOverlap(begin, static_cast<std::size_t>(end - begin)); }, unknownMemory));
|
||||
const auto drawBindings = resources.PrepareDrawBindings(*recorder, record.moved);
|
||||
static const bool addressReadPasses = std::getenv("APS5_NO_ADDRESS_READ_PASSES") == nullptr;
|
||||
const bool readOnlyBda = addressReadPasses && resources.BdaReadOnly();
|
||||
outcome.addressDraw = addressBased;
|
||||
@@ -2361,7 +2363,7 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
|
||||
}
|
||||
recorder->LeaveRenderPassOpen(passKey, drawTiming, fenced, passAccess, std::move(storeProxies));
|
||||
timer.phase(PhaseRecord);
|
||||
keepRecordedDraw(*recorder, record.resources, std::move(record.pipeline), std::move(record.framebuffer), inputs, std::move(record.targets), std::move(record.targetSlices), std::move(scratch), std::move(checkRecords), record.listed, record.completion, outcome);
|
||||
keepRecordedDraw(*recorder, record.resources, std::move(record.pipeline), std::move(record.framebuffer), inputs, std::move(record.targets), std::move(record.targetSlices), state.scissor, std::move(scratch), std::move(checkRecords), record.listed, record.completion, outcome);
|
||||
timer.phase(PhaseKeep);
|
||||
if (record.waited) {
|
||||
// The wait the dispatch path makes for such work (source 3, "address-based"): the
|
||||
@@ -2952,6 +2954,14 @@ void recordPreparedDraw(const Context& context, const State& state, const Pm4::D
|
||||
report(outcome.waited ? (outcome.reason == SyncLease ? " recorded then waited (lease)" : " recorded then waited (copied writes)") : completion ? " recorded with completion" : " recorded");
|
||||
return;
|
||||
}
|
||||
const bool gpuIndirect = args != nullptr && indirect.path == IndirectDrawPath::Gpu;
|
||||
const auto touches = [&](std::uint64_t begin, std::uint64_t end) {
|
||||
const auto bytes = static_cast<std::size_t>(end - begin);
|
||||
if (resources->WritesOverlap(begin, bytes) || resources->ReadsOverlap(begin, bytes)) return true;
|
||||
if (!gpuIndirect) return false;
|
||||
return (begin < args->arguments + args->RangeBytes() && args->arguments < end) || (args->countIndirect && begin < args->countAddress + 4 && args->countAddress < end);
|
||||
};
|
||||
static_cast<void>(StorageTexture::StoreDrawnLinearTouching("draw access", [&](std::uint64_t begin, std::uint64_t end) { return touches(begin, end) || std::any_of(inputs.inPlaceReads.begin(), inputs.inPlaceReads.end(), [&](const auto& read) { return read.first < end && begin < read.second; }); }, resources->HoldsLease() || resources->UsesAddressTable() || resources->HasCopiedWrites()));
|
||||
APS5_LOG_CHARS_OUT_DEBUG("Creating CommandBatch");
|
||||
std::optional<CommandBatch> batch;
|
||||
if (!recorded) {
|
||||
@@ -2961,16 +2971,9 @@ void recordPreparedDraw(const Context& context, const State& state, const Pm4::D
|
||||
}
|
||||
using CommandClass = Recorder::CommandClass;
|
||||
const auto countBarrier = [&](std::uint32_t count = 1) { if (recorded) Recorder::CountBarriers(CommandClass::Draw, count); };
|
||||
const bool gpuIndirect = args != nullptr && indirect.path == IndirectDrawPath::Gpu;
|
||||
// A queued DCC key store over memory the draw writes or reads in place (unknown for an
|
||||
// address-based build), or over its GPU-side records, lands before it, as before a dispatch
|
||||
// (VulkanDevice::dispatch).
|
||||
const auto touches = [&](std::uint64_t begin, std::uint64_t end) {
|
||||
const auto bytes = static_cast<std::size_t>(end - begin);
|
||||
if (resources->WritesOverlap(begin, bytes) || resources->ReadsOverlap(begin, bytes)) return true;
|
||||
if (!gpuIndirect) return false;
|
||||
return (begin < args->arguments + args->RangeBytes() && args->arguments < end) || (args->countIndirect && begin < args->countAddress + 4 && args->countAddress < end);
|
||||
};
|
||||
if (recorded && recorder->HasQueuedKeyStores() && (resources->HoldsLease() || recorder->AnyQueuedKeyStore(touches))) recorder->FlushKeyStores();
|
||||
// A queued label store over such memory likewise (Recorder::RecordStore).
|
||||
if (recorded && recorder->HasQueuedStores() && (resources->HoldsLease() || recorder->AnyQueuedStore(touches))) recorder->FlushStores();
|
||||
@@ -3137,7 +3140,7 @@ void recordPreparedDraw(const Context& context, const State& state, const Pm4::D
|
||||
APS5_LOG_CHARS_OUT_DEBUG("SubmitAndWait");
|
||||
timer.phase(PhaseRecord);
|
||||
if (recorded) {
|
||||
keepRecordedDraw(*recorder, resources, pipeline, framebuffer, inputs, owners, [&] { std::vector<std::pair<std::uint32_t, std::uint32_t>> slices; for (const auto& binding : targets) slices.emplace_back(binding.color.arraySlice, binding.color.arrayLastSlice != 0 ? binding.color.layers : 1u); return slices; }(), std::move(scratch), std::move(checkRecords), listed, completion, outcome);
|
||||
keepRecordedDraw(*recorder, resources, pipeline, framebuffer, inputs, owners, [&] { std::vector<std::pair<std::uint32_t, std::uint32_t>> slices; for (const auto& binding : targets) slices.emplace_back(binding.color.arraySlice, binding.color.arrayLastSlice != 0 ? binding.color.layers : 1u); return slices; }(), state.scissor, std::move(scratch), std::move(checkRecords), listed, completion, outcome);
|
||||
timer.phase(PhaseKeep);
|
||||
if (outcome.waited) {
|
||||
// The wait the dispatch path makes for such work (source 3, "address-based"): the
|
||||
|
||||
@@ -69,6 +69,7 @@ struct ImageMirror {
|
||||
std::uint64_t bytes = 0;
|
||||
bool writable = false;
|
||||
bool heap = false;
|
||||
bool joinedHeap = false;
|
||||
bool sparse = false;
|
||||
std::uint64_t reserved = 0;
|
||||
std::vector<std::pair<std::uint64_t, std::uint64_t>> committed;
|
||||
@@ -1492,8 +1493,32 @@ bool evictingRefusalCurrent(const HostImports& state, const EvictingRefusal& ref
|
||||
return refusal.epoch == state.epoch && refusal.liveBytes == state.liveBytes && budget <= refusal.budget && refusal.window == importWindow.load(std::memory_order_relaxed) && state.useCounter < refusal.ready && (state.buildFloor <= refusal.floor || state.useCounter < refusal.readyHeld);
|
||||
}
|
||||
|
||||
void noteRetiredUnwatched(std::vector<std::pair<std::uint64_t, std::uint64_t>>& spans, const HostImport& entry, const GuestAllocations::Lease& lease) {
|
||||
if (!entry.unwatched) return;
|
||||
for (const auto& range : entry.ranges) {
|
||||
if (leasedRangeOwner(lease, range->address) != range) continue;
|
||||
const auto begin = std::max(range->address, entry.base);
|
||||
const auto end = std::min(range->address + range->bytes, entry.base + entry.bytes);
|
||||
if (begin < end) spans.emplace_back(begin, end);
|
||||
}
|
||||
}
|
||||
|
||||
void rewatchRetired(const HostImports& state, std::vector<std::pair<std::uint64_t, std::uint64_t>> spans) {
|
||||
std::erase_if(spans, [&](const auto& span) {
|
||||
auto it = state.imports.upper_bound(span.first);
|
||||
if (it != state.imports.begin()) --it;
|
||||
for (; it != state.imports.end() && it->first < span.second; ++it) {
|
||||
if (it->second.unwatched && span.first < it->first + it->second.bytes) return true;
|
||||
}
|
||||
const std::array<std::pair<std::uint64_t, std::uint64_t>, 1> one{{span}};
|
||||
return StorageTexture::AnyLiveOverlaps(one);
|
||||
});
|
||||
if (!spans.empty()) GuestMemory::Rewatch(spans);
|
||||
}
|
||||
|
||||
std::uint64_t retireLeastRecent(const Context& context, HostImports& state, const GuestAllocations::Lease& lease, std::uint64_t wanted) {
|
||||
std::uint64_t freed = 0;
|
||||
std::vector<std::pair<std::uint64_t, std::uint64_t>> unwatched;
|
||||
while (freed < wanted) {
|
||||
auto oldest = state.imports.end();
|
||||
for (auto it = state.imports.begin(); it != state.imports.end(); ++it) {
|
||||
@@ -1501,8 +1526,10 @@ std::uint64_t retireLeastRecent(const Context& context, HostImports& state, cons
|
||||
}
|
||||
if (oldest == state.imports.end()) break;
|
||||
freed += oldest->second.pinnedBytes;
|
||||
noteRetiredUnwatched(unwatched, oldest->second, lease);
|
||||
retireImport(context.device, state, oldest, lease);
|
||||
}
|
||||
rewatchRetired(state, std::move(unwatched));
|
||||
return freed;
|
||||
}
|
||||
|
||||
@@ -1949,7 +1976,7 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import
|
||||
GuestMemory::ImportWatched(base, bytes, [&] {
|
||||
step = createBackingImport(context, entry, backing, result);
|
||||
return step == nullptr;
|
||||
}, watchedDmaBuf);
|
||||
}, watchedDmaBuf, false);
|
||||
}
|
||||
if (step == nullptr && entry.dmaBuf && bytes >= settleFloorBytes && context.settleQueue != VK_NULL_HANDLE && context.settleQueueMutex != nullptr) {
|
||||
static const bool traceSettle = std::getenv("APS5_TRACE_HOST_IMPORT") != nullptr;
|
||||
@@ -2087,6 +2114,7 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc
|
||||
std::erase_if(state.sparseRunEnds, [&](const auto& entry) { return !starts(entry.first); });
|
||||
std::erase_if(state.sparseRunJoins, [&](const auto& entry) { return !starts(entry.first); });
|
||||
const auto budget = effectiveBudget(context, state);
|
||||
std::vector<std::pair<std::uint64_t, std::uint64_t>> unwatched;
|
||||
for (auto it = state.imports.begin(); it != state.imports.end();) {
|
||||
if (sameRange(it->second, lease, budget)) {
|
||||
if (it->second.unwatched) GuestMemory::Unwatch(it->first, it->second.bytes);
|
||||
@@ -2096,9 +2124,11 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc
|
||||
static const bool trace = std::getenv("APS5_TRACE_HOST_IMPORT") != nullptr;
|
||||
if (trace && leasedRangeOwner(lease, it->first, it->first + it->second.bytes) != nullptr) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx retired: the range was mapped again\n", static_cast<unsigned long long>(it->first), static_cast<unsigned long long>(it->second.bytes));
|
||||
const auto next = std::next(it);
|
||||
noteRetiredUnwatched(unwatched, it->second, lease);
|
||||
retireImport(context.device, state, it, lease);
|
||||
it = next;
|
||||
}
|
||||
rewatchRetired(state, std::move(unwatched));
|
||||
for (auto it = state.failed.begin(); it != state.failed.end();) it = sameRange(it->second, lease, budget) ? std::next(it) : state.failed.erase(it);
|
||||
for (auto it = state.failedJoins.begin(); it != state.failedJoins.end();) it = sameRun(it->second, joinedRun(lease, it->second.base, it->second.base + it->second.bytes)) ? std::next(it) : state.failedJoins.erase(it);
|
||||
for (auto it = state.failedBlocks.begin(); it != state.failedBlocks.end();) it = sameRun(it->second, exactRun(lease, it->first.first, it->first.second)) ? std::next(it) : state.failedBlocks.erase(it);
|
||||
@@ -2269,6 +2299,39 @@ void unlistMirror(ImageMirrors& state, ImageMirror& mirror) {
|
||||
mirror.listed = false;
|
||||
}
|
||||
|
||||
struct JoinedHeapRange {
|
||||
std::vector<std::weak_ptr<const GuestAllocations::Range>> members;
|
||||
std::shared_ptr<const GuestAllocations::Range> range;
|
||||
};
|
||||
|
||||
struct JoinedHeapRanges {
|
||||
std::mutex mutex;
|
||||
std::map<std::uint64_t, JoinedHeapRange> entries;
|
||||
};
|
||||
|
||||
JoinedHeapRanges& JoinedHeaps() {
|
||||
static JoinedHeapRanges ranges;
|
||||
return ranges;
|
||||
}
|
||||
|
||||
bool releaseExpiredJoinedHeaps() {
|
||||
std::vector<std::shared_ptr<const GuestAllocations::Range>> released;
|
||||
{
|
||||
auto& joined = JoinedHeaps();
|
||||
std::lock_guard lock(joined.mutex);
|
||||
for (auto it = joined.entries.begin(); it != joined.entries.end();) {
|
||||
const auto& members = it->second.members;
|
||||
if (std::none_of(members.begin(), members.end(), [](const auto& member) { return member.expired(); })) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
released.push_back(std::move(it->second.range));
|
||||
it = joined.entries.erase(it);
|
||||
}
|
||||
}
|
||||
return !released.empty();
|
||||
}
|
||||
|
||||
std::uint64_t blockBytes(const GuestRuns& runs) {
|
||||
constexpr std::uint64_t block = ResidentBuffer::BlockBytes;
|
||||
std::uint64_t blocks = 0;
|
||||
@@ -2783,7 +2846,7 @@ const char* heapCopyReason(bool overBudget, bool watched) {
|
||||
// The mirror for a leased image range: the existing one, or a new one when none exists or the
|
||||
// registered Range object changed (a protection change). Null when the range cannot be mirrored. An
|
||||
// existing writable mirror's blocks are appended to `blocks` for the caller's one compare of every
|
||||
std::shared_ptr<ImageMirror> acquireMirror(const Context& context, const std::shared_ptr<const GuestAllocations::Range>& range, std::vector<RefreshBlock>& blocks, bool heap, std::uint64_t begin = 0, std::uint64_t bytes = 0) {
|
||||
std::shared_ptr<ImageMirror> acquireMirror(const Context& context, const std::shared_ptr<const GuestAllocations::Range>& range, std::vector<RefreshBlock>& blocks, bool heap, std::uint64_t begin = 0, std::uint64_t bytes = 0, bool joinedHeap = false) {
|
||||
auto selected = *range;
|
||||
if (bytes != 0) {
|
||||
Require(begin >= range->address && bytes <= range->bytes && begin - range->address <= range->bytes - bytes, "heap mirror selection exceeds its registered range");
|
||||
@@ -2804,7 +2867,7 @@ std::shared_ptr<ImageMirror> acquireMirror(const Context& context, const std::sh
|
||||
}
|
||||
if (state.failed.contains(selected.address)) return nullptr;
|
||||
const auto found = state.entries.find(selected.address);
|
||||
if (found != state.entries.end() && sameRange(found->second->range, range) && found->second->reserved == selected.bytes && found->second->writable == range->writable && found->second->heap == heap) mirror = found->second;
|
||||
if (found != state.entries.end() && sameRange(found->second->range, range) && found->second->reserved == selected.bytes && found->second->writable == range->writable && found->second->heap == heap && found->second->joinedHeap == joinedHeap) mirror = found->second;
|
||||
}
|
||||
if (mirror != nullptr) {
|
||||
if (!mirror->sparse) {
|
||||
@@ -2827,6 +2890,7 @@ std::shared_ptr<ImageMirror> acquireMirror(const Context& context, const std::sh
|
||||
mirror->sparse = sparse;
|
||||
mirror->writable = range->writable;
|
||||
mirror->heap = heap;
|
||||
mirror->joinedHeap = joinedHeap;
|
||||
mirror->range = range;
|
||||
GuestRuns backed;
|
||||
if (heap) {
|
||||
@@ -2959,9 +3023,11 @@ std::shared_ptr<ImageMirror> findMirror(const Context& context, std::uint64_t be
|
||||
|
||||
// Drops mirrors whose registered range is gone (a later build made a new one for the replacement).
|
||||
void sweepMirrors() {
|
||||
const bool released = releaseExpiredJoinedHeaps();
|
||||
const auto space = Spaces().current.load();
|
||||
auto& state = Mirrors();
|
||||
std::lock_guard lock(state.mutex);
|
||||
if (released) ++state.version;
|
||||
if (space != nullptr && state.pinnedSpace == space->serial && state.pinnedVersion == state.version) return;
|
||||
++state.sweeps;
|
||||
bool pinned = space != nullptr;
|
||||
@@ -2985,6 +3051,14 @@ void sweepMirrors() {
|
||||
|
||||
void releaseImportedMirrors(const Context& context, std::span<const std::pair<std::uint64_t, std::uint64_t>> imported) {
|
||||
if (imported.empty()) return;
|
||||
std::vector<std::pair<std::uint64_t, std::uint64_t>> coverage(imported.begin(), imported.end());
|
||||
std::sort(coverage.begin(), coverage.end());
|
||||
std::size_t count = 0;
|
||||
for (const auto& interval : coverage) {
|
||||
if (count != 0 && interval.first <= coverage[count - 1].second) coverage[count - 1].second = std::max(coverage[count - 1].second, interval.second);
|
||||
else coverage[count++] = interval;
|
||||
}
|
||||
coverage.resize(count);
|
||||
std::vector<std::shared_ptr<ImageMirror>> released;
|
||||
auto& state = Mirrors();
|
||||
std::lock_guard lock(state.mutex);
|
||||
@@ -2998,6 +3072,13 @@ void releaseImportedMirrors(const Context& context, std::span<const std::pair<st
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
if (mirror->joinedHeap) {
|
||||
const auto covered = std::upper_bound(coverage.begin(), coverage.end(), mirror->base, [](std::uint64_t address, const auto& interval) { return address < interval.first; });
|
||||
if (covered == coverage.begin() || std::prev(covered)->second < mirror->base + mirror->reserved) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
++state.heapImported;
|
||||
state.heapImportedBytes += mirror->accounted;
|
||||
unlistMirror(state, *mirror);
|
||||
@@ -4388,15 +4469,40 @@ bool GuestBufferMemory::sparseHeapCandidate(const Region& region) {
|
||||
return region.sparseHeap && region.sparse && region.mirror == nullptr && region.direct == nullptr && (region.hostBacked || region.writable) && mirrorsEnabled() && sparseMirrorsEnabled();
|
||||
}
|
||||
|
||||
std::shared_ptr<const GuestAllocations::Range> joinedHeapRange(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) {
|
||||
const auto run = crossedRun(lease, begin, end);
|
||||
if (run.ranges.size() < 2 || std::any_of(run.ranges.begin(), run.ranges.end(), [](const auto& range) { return !range->releasable; })) return nullptr;
|
||||
releaseExpiredJoinedHeaps();
|
||||
auto& joined = JoinedHeaps();
|
||||
std::lock_guard lock(joined.mutex);
|
||||
auto& entry = joined.entries[run.base];
|
||||
bool same = entry.range != nullptr && entry.members.size() == run.ranges.size();
|
||||
for (std::size_t index = 0; same && index < run.ranges.size(); ++index) same = sameRange(entry.members[index], run.ranges[index]);
|
||||
if (!same) {
|
||||
const auto& first = *run.ranges.front();
|
||||
entry.members.assign(run.ranges.begin(), run.ranges.end());
|
||||
entry.range = std::make_shared<const GuestAllocations::Range>(GuestAllocations::Range{run.base, static_cast<std::size_t>(run.bytes), true, first.writable, first.allocationAddress, first.allocationBytes, true, first.gpu, first.backingSerial});
|
||||
}
|
||||
return entry.range;
|
||||
}
|
||||
|
||||
bool GuestBufferMemory::bindSparseMirror(Region& region, const GuestAllocations::Lease& lease) {
|
||||
const auto backedEnd = region.backed.empty() ? region.begin : region.backed.back().second;
|
||||
const auto neededEnd = std::min(region.end, std::max(backedEnd, std::max(region.lastView, region.begin) + 4u));
|
||||
const auto* range = containingRange(lease, region.begin, neededEnd);
|
||||
if (range == nullptr || !range->releasable) return false;
|
||||
std::shared_ptr<const GuestAllocations::Range> owner;
|
||||
bool joined = false;
|
||||
if (const auto* range = containingRange(lease, region.begin, neededEnd); range != nullptr) {
|
||||
if (!range->releasable) return false;
|
||||
owner = leasedRangeOwner(lease, range->address);
|
||||
} else {
|
||||
joined = true;
|
||||
owner = joinedHeapRange(lease, region.begin, neededEnd);
|
||||
if (owner == nullptr) return false;
|
||||
}
|
||||
std::vector<RefreshBlock> blocks;
|
||||
std::shared_ptr<ImageMirror> mirror;
|
||||
try {
|
||||
mirror = acquireMirror(context, leasedRangeOwner(lease, range->address), blocks, true);
|
||||
mirror = acquireMirror(context, owner, blocks, true, 0, 0, joined);
|
||||
} catch (const std::runtime_error& error) {
|
||||
static std::atomic<int> reported{0};
|
||||
if (reported.fetch_add(1, std::memory_order_relaxed) < 4) std::fprintf(stderr, "[gpu] %s; copying the sparse heap instead\n", error.what());
|
||||
|
||||
@@ -1127,6 +1127,14 @@ struct PipelineStore {
|
||||
std::uint64_t evicted = 0;
|
||||
PipelineStoreCounters totals;
|
||||
std::chrono::steady_clock::time_point lastReport = std::chrono::steady_clock::now();
|
||||
std::size_t capacity = 0;
|
||||
bool fixedCapacity = false;
|
||||
std::list<std::uint64_t> evictedOrder;
|
||||
std::unordered_map<std::uint64_t, std::list<std::uint64_t>::iterator> evictedKeys;
|
||||
std::size_t evictionsSinceCheck = 0;
|
||||
std::size_t returnsSinceCheck = 0;
|
||||
static constexpr std::size_t BaseEntries = 1024;
|
||||
static constexpr std::size_t MaximumEntries = 4096;
|
||||
};
|
||||
|
||||
// Never destroyed: the pipelines belong to a device that may already be gone when statics die, and
|
||||
@@ -1155,9 +1163,41 @@ std::list<PipelineStore::Entry>::iterator abandon(PipelineStore& store, std::lis
|
||||
return store.entries.erase(it);
|
||||
}
|
||||
|
||||
std::size_t storeBound() {
|
||||
static const auto bound = static_cast<std::size_t>(std::max<std::uint64_t>(1u, PipelineSwitch("APS5_PIPELINE_STORE_ENTRIES", 1024u)));
|
||||
return bound;
|
||||
std::size_t storeBound(PipelineStore& store) {
|
||||
if (store.capacity == 0) {
|
||||
const auto set = PipelineSwitch("APS5_PIPELINE_STORE_ENTRIES", 0u);
|
||||
store.fixedCapacity = set != 0;
|
||||
store.capacity = store.fixedCapacity ? static_cast<std::size_t>(set) : PipelineStore::BaseEntries;
|
||||
}
|
||||
return store.capacity;
|
||||
}
|
||||
|
||||
void noteStoreEviction(PipelineStore& store, std::uint64_t hash) {
|
||||
if (store.fixedCapacity) return;
|
||||
if (const auto found = store.evictedKeys.find(hash); found != store.evictedKeys.end()) store.evictedOrder.erase(found->second);
|
||||
store.evictedOrder.push_front(hash);
|
||||
store.evictedKeys[hash] = store.evictedOrder.begin();
|
||||
while (store.evictedOrder.size() > store.capacity) {
|
||||
store.evictedKeys.erase(store.evictedOrder.back());
|
||||
store.evictedOrder.pop_back();
|
||||
}
|
||||
if (++store.evictionsSinceCheck < store.capacity / 32u) return;
|
||||
const bool churning = store.returnsSinceCheck * 2u > store.evictionsSinceCheck;
|
||||
store.evictionsSinceCheck = 0;
|
||||
store.returnsSinceCheck = 0;
|
||||
if (!churning || store.capacity >= PipelineStore::MaximumEntries) return;
|
||||
const auto previous = store.capacity;
|
||||
store.capacity = std::min(store.capacity * 2u, PipelineStore::MaximumEntries);
|
||||
std::fprintf(stderr, "[pipecache] bound %zu -> %zu pipelines: pipelines it evicted keep coming back\n", previous, store.capacity);
|
||||
}
|
||||
|
||||
void noteStoreReturn(PipelineStore& store, std::uint64_t hash) {
|
||||
if (store.fixedCapacity) return;
|
||||
const auto found = store.evictedKeys.find(hash);
|
||||
if (found == store.evictedKeys.end()) return;
|
||||
store.evictedOrder.erase(found->second);
|
||||
store.evictedKeys.erase(found);
|
||||
++store.returnsSinceCheck;
|
||||
}
|
||||
|
||||
void reportPipelines(PipelineStore& store) {
|
||||
@@ -1229,12 +1269,12 @@ std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& st
|
||||
}
|
||||
++store.misses;
|
||||
++store.totals.misses;
|
||||
noteStoreReturn(store, hash);
|
||||
}
|
||||
// Built outside the mutex: a draw that finds its pipeline resident is not held behind another
|
||||
// thread's build (the journal replay builds here too), and a device's driver compiles in
|
||||
// parallel when several threads build at once.
|
||||
auto pipeline = std::make_shared<Pipeline>(context, state, vertexInput, resources, shaders, attachmentLayout);
|
||||
const auto bound = storeBound();
|
||||
const bool replaying = PipelineJournalReplaying();
|
||||
{
|
||||
std::lock_guard lock(store.mutex);
|
||||
@@ -1252,7 +1292,10 @@ std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& st
|
||||
}
|
||||
// A replayed pipeline enters at the least recently used end and never displaces a draw's:
|
||||
// the title's own pipelines evict it first, and a full store takes none.
|
||||
if (replaying && store.entries.size() >= bound) return pipeline;
|
||||
if (replaying && store.entries.size() >= storeBound(store)) {
|
||||
if (store.fixedCapacity || store.capacity >= PipelineStore::MaximumEntries) return pipeline;
|
||||
store.capacity = std::min(store.capacity * 2u, PipelineStore::MaximumEntries);
|
||||
}
|
||||
if (replaying) {
|
||||
store.entries.push_front({context.device, context.bufferPool, context.bufferPool != nullptr, hash, std::vector<std::byte>(key), pipeline});
|
||||
store.index[hash] = store.entries.begin();
|
||||
@@ -1260,12 +1303,14 @@ std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& st
|
||||
store.entries.push_back({context.device, context.bufferPool, context.bufferPool != nullptr, hash, std::vector<std::byte>(key), pipeline});
|
||||
store.index[hash] = std::prev(store.entries.end());
|
||||
}
|
||||
while (store.entries.size() > bound) {
|
||||
while (store.entries.size() > storeBound(store)) {
|
||||
// Only an entry no recorded draw still holds may go (Kept keeps its shared_ptr until the fence).
|
||||
const auto victim = std::find_if(store.entries.begin(), store.entries.end(), [](const PipelineStore::Entry& entry) { return entry.pipeline.use_count() == 1; });
|
||||
if (victim == store.entries.end()) break;
|
||||
const auto evictedHash = victim->hash;
|
||||
store.index.erase(victim->hash);
|
||||
store.entries.erase(victim);
|
||||
noteStoreEviction(store, evictedHash);
|
||||
++store.evicted;
|
||||
++store.totals.evicted;
|
||||
}
|
||||
@@ -1284,10 +1329,16 @@ PipelineStoreCounters PipelineStoreTotals() {
|
||||
std::lock_guard lock(store.mutex);
|
||||
auto totals = store.totals;
|
||||
totals.cached = store.entries.size();
|
||||
totals.bound = storeBound();
|
||||
totals.bound = storeBound(store);
|
||||
return totals;
|
||||
}
|
||||
|
||||
bool PipelineStoreCanGrow() {
|
||||
auto& store = Pipelines();
|
||||
std::lock_guard lock(store.mutex);
|
||||
return storeBound(store) < PipelineStore::MaximumEntries && !store.fixedCapacity;
|
||||
}
|
||||
|
||||
void ClearCachedPipelines(VkDevice device) {
|
||||
auto& store = Pipelines();
|
||||
std::lock_guard lock(store.mutex);
|
||||
@@ -1299,6 +1350,13 @@ void ClearCachedPipelines(VkDevice device) {
|
||||
store.index.erase(it->hash);
|
||||
it = store.entries.erase(it);
|
||||
}
|
||||
if (store.entries.empty() && !store.fixedCapacity) {
|
||||
store.capacity = 0;
|
||||
store.evictedOrder.clear();
|
||||
store.evictedKeys.clear();
|
||||
store.evictionsSinceCheck = 0;
|
||||
store.returnsSinceCheck = 0;
|
||||
}
|
||||
ClearPipelineLibraries(device);
|
||||
ReleasePixelFunctionLds(device);
|
||||
}
|
||||
|
||||
@@ -1352,7 +1352,7 @@ private:
|
||||
std::call_once(loadOnce, [&] {
|
||||
try {
|
||||
auto journal = Journal::Instance().Load();
|
||||
const auto limit = configured("APS5_PIPELINE_JOURNAL_ENTRIES", 1024u);
|
||||
const auto limit = configured("APS5_PIPELINE_JOURNAL_ENTRIES", 4096u);
|
||||
if (journal.entries.size() > limit) journal.entries.erase(journal.entries.begin(), journal.entries.end() - static_cast<std::ptrdiff_t>(limit));
|
||||
loaded = std::move(journal);
|
||||
} catch (const std::exception& error) {
|
||||
@@ -1438,7 +1438,7 @@ private:
|
||||
return;
|
||||
}
|
||||
const auto store = PipelineStoreTotals();
|
||||
if (store.cached >= store.bound) {
|
||||
if (store.cached >= store.bound && !PipelineStoreCanGrow()) {
|
||||
counters.skipped.fetch_add(1, std::memory_order_relaxed);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -185,7 +185,8 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip
|
||||
std::vector<VkPipeline> libraries;
|
||||
for (std::size_t part = 0; part < flags.size(); ++part) {
|
||||
if (part == 0 && mesh) continue;
|
||||
const auto key = Combined(*partKeys[part], *contextKeys[part]);
|
||||
auto key = Combined(*partKeys[part], *contextKeys[part]);
|
||||
key.push_back(static_cast<std::byte>(optimize));
|
||||
auto& cache = device.parts[part];
|
||||
if (const auto found = cache.find(key); found != cache.end()) {
|
||||
libraries.push_back(found->second);
|
||||
@@ -195,7 +196,7 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip
|
||||
VkGraphicsPipelineLibraryCreateInfoEXT library{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_LIBRARY_CREATE_INFO_EXT, flags[part] == VK_GRAPHICS_PIPELINE_LIBRARY_FRAGMENT_OUTPUT_INTERFACE_BIT_EXT ? &rendering : &shaderRendering};
|
||||
library.flags = flags[part];
|
||||
VkGraphicsPipelineCreateInfo create{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, &library};
|
||||
create.flags = VK_PIPELINE_CREATE_LIBRARY_BIT_KHR | VK_PIPELINE_CREATE_RETAIN_LINK_TIME_OPTIMIZATION_INFO_BIT_EXT;
|
||||
create.flags = optimize ? VK_PIPELINE_CREATE_LIBRARY_BIT_KHR | VK_PIPELINE_CREATE_RETAIN_LINK_TIME_OPTIMIZATION_INFO_BIT_EXT : VK_PIPELINE_CREATE_LIBRARY_BIT_KHR;
|
||||
create.pDynamicState = info.pDynamicState;
|
||||
create.layout = partLayouts[part];
|
||||
switch (flags[part]) {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/DeferredDestruction.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/shaders/MeshArguments_spv.h"
|
||||
#include "prx/libSceAgcDriver/Graphics/shaders/SampleCounter_spv.h"
|
||||
#include "prx/libSceAgcDriver/Graphics/shaders/SampleDumps_spv.h"
|
||||
@@ -3134,6 +3135,26 @@ std::uint64_t Recorder::PendingWriterSerial(std::uint64_t address, std::size_t b
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::uint64_t Recorder::PendingWriterSerial(std::uint64_t address, std::size_t bytes, std::span<const std::uint64_t> exceptNotes) const {
|
||||
if (exceptNotes.empty()) return PendingWriterSerial(address, bytes);
|
||||
if (bytes == 0) return 0;
|
||||
const auto end = address + bytes;
|
||||
const auto writes = [&](const Batch& batch) {
|
||||
for (std::size_t index = 0; index < batch.writes.size(); ++index) {
|
||||
const auto& [begin, finish] = batch.writes[index];
|
||||
if (address >= finish || begin >= end) continue;
|
||||
if (index < batch.writeNotes.size() && std::find(exceptNotes.begin(), exceptNotes.end(), batch.writeNotes[index]) != exceptNotes.end()) continue;
|
||||
return true;
|
||||
}
|
||||
return readOverlap(batch, address, end, ReadKind::AddressBased).has_value();
|
||||
};
|
||||
if (open != nullptr && writes(*open)) return OpenSerial();
|
||||
for (auto it = inFlight.rbegin(); it != inFlight.rend(); ++it) {
|
||||
if (writes(**it)) return (*it)->serial;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool Recorder::signaled(const Batch& batch) const {
|
||||
return batch.submitted && fenceStatus(batch.fence) == VK_SUCCESS;
|
||||
}
|
||||
@@ -3659,6 +3680,7 @@ void Recorder::Submit() {
|
||||
submission.pSignalSemaphores = &timeline;
|
||||
}
|
||||
if (hold != VK_NULL_HANDLE || timeline != VK_NULL_HANDLE) submission.pNext = &timelineInfo;
|
||||
DestroyDeferredAtIdle(context);
|
||||
const auto submitStart = std::chrono::steady_clock::now();
|
||||
const auto result = function(queueSubmit, "vkQueueSubmit")(context.queue, 1, &submission, batch->fence);
|
||||
if (result == VK_ERROR_DEVICE_LOST) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/DeferredDestruction.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
@@ -381,6 +382,7 @@ void CommandBatch::Submit() {
|
||||
submission.commandBufferCount = 1;
|
||||
submission.pCommandBuffers = &commands;
|
||||
timing.Mark("command_end");
|
||||
DestroyDeferredAtIdle(context);
|
||||
Check(context.Function<PFN_vkQueueSubmit>("vkQueueSubmit")(context.queue, 1, &submission, fence), "vkQueueSubmit graphics");
|
||||
pending = true;
|
||||
submitted = true;
|
||||
|
||||
@@ -624,10 +624,7 @@ std::uint64_t storageBudget(const Context& context, std::uint64_t required = 0)
|
||||
cached.device = context.physical;
|
||||
cached.read = now;
|
||||
cached.budget = StorageTextureBudget(context.memory, &reported, SampledTextureMemory(), storageCacheBytes());
|
||||
cached.ceiling = cached.budget;
|
||||
if (const auto heap = largestDeviceLocalHeap(context.memory); heap && reported.heapBudget[*heap] != 0) {
|
||||
cached.ceiling = std::max<std::uint64_t>(512ull << 20u, std::min<std::uint64_t>(TextureCacheBudget(context.memory), reported.heapBudget[*heap] / 4u));
|
||||
}
|
||||
cached.ceiling = StorageTextureCeiling(context.memory, &reported);
|
||||
};
|
||||
if (cached.device != context.physical || now - cached.read >= std::chrono::seconds(1)) read();
|
||||
const auto needed = std::min(required, cached.ceiling);
|
||||
@@ -752,7 +749,14 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
|
||||
normalized.format = unorm16;
|
||||
return cachedTexture(context, words, normalized, components, guestBytes, depthCompare);
|
||||
}
|
||||
if (const auto window = ViewedArrayWindow(resource); window.baseAddress != resource.baseAddress) {
|
||||
const auto viewedWindow = [&] {
|
||||
const auto window = ViewedArrayWindow(resource, false);
|
||||
if (window.baseAddress != resource.baseAddress || resource.dccAddress == 0) return window;
|
||||
if (ViewedArrayWindow(resource, true).baseAddress == resource.baseAddress) return window;
|
||||
if (TextureClearKeys(resource, guestBytes != 0 ? guestBytes : DescribeSurface(resource).guestBytes) != DccKeys::Uncompressed) return window;
|
||||
return ViewedArrayWindow(resource, true);
|
||||
};
|
||||
if (const auto window = viewedWindow(); window.baseAddress != resource.baseAddress) {
|
||||
static std::atomic<std::uint64_t> windows{0};
|
||||
const auto count = windows.fetch_add(1) + 1;
|
||||
if (count <= 5 || (count & (count - 1)) == 0) std::fprintf(stderr, "[gpu] sampled array view 0x%llx (%ux%u format %u, dimension %d, slices %u..%u) served from its viewed slices at 0x%llx; %llu such views\n", static_cast<unsigned long long>(resource.baseAddress), resource.width, resource.height, resource.format, static_cast<int>(resource.dimension), resource.baseArray, resource.depthOrLastArray, static_cast<unsigned long long>(window.baseAddress), static_cast<unsigned long long>(count));
|
||||
@@ -1185,9 +1189,9 @@ std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std
|
||||
const auto viewed = ArrayLayerWindow(requested, context.limits.maxImageArrayLayers);
|
||||
if (viewed.baseAddress != requested.baseAddress) guestBytes = 0;
|
||||
Require(viewed.dimension != TextureDimension::k2DMsaa, "storage image access to a multisampled texture is not implemented");
|
||||
RequireNoMultisampleTarget(viewed.baseAddress, guestBytes != 0 ? static_cast<std::size_t>(guestBytes) : 1u, "a storage image access");
|
||||
if (auto depth = DepthSurfaceStorage(context, words, viewed, mip)) return depth;
|
||||
const auto viewedBytes = guestBytes != 0 ? guestBytes : DescribeSurface(viewed).guestBytes;
|
||||
RequireNoMultisampleTarget(viewed.baseAddress, static_cast<std::size_t>(viewedBytes), "a storage image access");
|
||||
if (auto depth = DepthSurfaceStorage(context, words, viewed, mip)) return depth;
|
||||
if (viewedBytes >= (1ull << 30u)) {
|
||||
static std::atomic<std::uint64_t> large{0};
|
||||
const auto count = large.fetch_add(1) + 1;
|
||||
@@ -1283,14 +1287,19 @@ std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory)
|
||||
return std::max<std::uint64_t>(2048ull << 20u, heap ? memory.memoryHeaps[*heap].size / 4u : 0u);
|
||||
}
|
||||
|
||||
std::uint64_t StorageTextureCeiling(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported) {
|
||||
const auto heap = largestDeviceLocalHeap(memory);
|
||||
if (reported == nullptr || !heap || reported->heapBudget[*heap] == 0) return TextureCacheBudget(memory);
|
||||
return std::max<std::uint64_t>(512ull << 20u, std::min<std::uint64_t>(memory.memoryHeaps[*heap].size / 2u, reported->heapBudget[*heap] / 2u));
|
||||
}
|
||||
|
||||
std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t sampledBytes, std::uint64_t storageBytes) {
|
||||
const auto heap = largestDeviceLocalHeap(memory);
|
||||
if (reported == nullptr || !heap || reported->heapBudget[*heap] == 0) return TextureCacheBudget(memory);
|
||||
const auto budget = reported->heapBudget[*heap];
|
||||
const auto usage = reported->heapUsage[*heap];
|
||||
const auto reserved = usage - std::min<std::uint64_t>(usage, sampledBytes + storageBytes) + sampledBytes + budget / 8u;
|
||||
const auto cap = std::min<std::uint64_t>(memory.memoryHeaps[*heap].size / 2u, budget / 2u);
|
||||
return std::max<std::uint64_t>(512ull << 20u, std::min<std::uint64_t>(cap, budget > reserved ? budget - reserved : 0u));
|
||||
return std::min<std::uint64_t>(StorageTextureCeiling(memory, reported), std::max<std::uint64_t>(512ull << 20u, budget > reserved ? budget - reserved : 0u));
|
||||
}
|
||||
|
||||
std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes, std::uint64_t storageBytes) {
|
||||
@@ -2075,9 +2084,10 @@ void ShaderResources::buildPrepare(std::span<const CompiledShader> shaders, cons
|
||||
const bool classified = element < binding.bufferWritten.size() && element < binding.bufferRead.size() && element < binding.bufferAtomic.size();
|
||||
allocations[index].writeOnly = classified && allocations[index].guest && allocations[index].written && binding.bufferWritten[element] && !read && !atomic;
|
||||
const auto& push = shader.program->pushConstants;
|
||||
if (!push.empty()) {
|
||||
const auto pushDataBytes = shader.program->stencilReferences.empty() ? push.size() : shader.program->stencilReferenceDword * sizeof(std::uint32_t);
|
||||
if (pushDataBytes != 0u) {
|
||||
const auto position = shader.program->memoryOffsetDword * 4u + element;
|
||||
Require(position < push.size(), "guest buffer offset lies outside the shader's push constants");
|
||||
Require(position < pushDataBytes, "guest buffer offset lies outside the shader's push constants");
|
||||
allocations[index].pushByte = static_cast<std::int32_t>(shader.pushConstantOffset + position);
|
||||
} else {
|
||||
allocations[index].dataByte = shader.program->memoryOffsetDword * 4u + element;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
#include <algorithm>
|
||||
@@ -314,13 +315,33 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint
|
||||
auto fixed = compare == 0 || compare == 7 ? 0u : face.compareMask;
|
||||
std::uint32_t invertMask = 0;
|
||||
std::uint32_t inverted = 0;
|
||||
std::uint32_t logicMask = 0;
|
||||
const auto logic = [](std::uint32_t op) { return op == 10 || op == 11 || op == 12 || op == 15; };
|
||||
const auto convert = [&](std::uint32_t op, bool testFailed) {
|
||||
if (face.writeMask == 0) return VK_STENCIL_OP_KEEP;
|
||||
char detail[112];
|
||||
if (opValExport && ((op == 4 && !opValReference) || op == 5 || op == 6 || op == 8 || op == 9)) {
|
||||
if (opValExport && ((op == 4 && !opValReference) || (op >= 5 && op != 7))) {
|
||||
std::snprintf(detail, sizeof(detail), "AGC graphics: stencil operation %u with a pixel shader that exports the stencil operation value is unsupported", op);
|
||||
throw std::runtime_error(detail);
|
||||
}
|
||||
if (logic(op)) {
|
||||
const auto bits = (op == 11 || op == 12 ? opValue : ~opValue) & face.writeMask;
|
||||
if (bits == 0) return VK_STENCIL_OP_KEEP;
|
||||
if (logicMask != 0 && logicMask != bits) {
|
||||
std::snprintf(detail, sizeof(detail), "AGC graphics: stencil logic operations on write masks 0x%02x and 0x%02x are unsupported", logicMask, bits);
|
||||
throw std::runtime_error(detail);
|
||||
}
|
||||
logicMask = bits;
|
||||
if (op != 11) return op == 10 ? VK_STENCIL_OP_ZERO : VK_STENCIL_OP_INVERT;
|
||||
if (testValExport) throw std::runtime_error("AGC graphics: stencil OR is unsupported with a pixel shader that exports the stencil test value");
|
||||
if ((~reference & fixed & bits) != 0) {
|
||||
std::snprintf(detail, sizeof(detail), "AGC graphics: stencil OR of 0x%02x against test value 0x%02x (masks 0x%02x/0x%02x) is unsupported", opValue, test, face.compareMask, face.writeMask);
|
||||
throw std::runtime_error(detail);
|
||||
}
|
||||
reference |= bits;
|
||||
fixed |= bits;
|
||||
return VK_STENCIL_OP_REPLACE;
|
||||
}
|
||||
switch (op) {
|
||||
case 0: return VK_STENCIL_OP_KEEP;
|
||||
case 1: return VK_STENCIL_OP_ZERO;
|
||||
@@ -377,6 +398,15 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint
|
||||
if (inverted != active) throw std::runtime_error("AGC graphics: a stencil replacement drawn as an inversion beside another stencil operation is unsupported");
|
||||
face.writeMask = invertMask;
|
||||
}
|
||||
if (logicMask != 0) {
|
||||
const auto other = [&](std::uint32_t op) { return op != 0 && !logic(op); };
|
||||
if ((other(ops & 0xfu) || other((ops >> 4u) & 0xfu) || other((ops >> 8u) & 0xfu)) && logicMask != face.writeMask) {
|
||||
char detail[128];
|
||||
std::snprintf(detail, sizeof(detail), "AGC graphics: stencil operations 0x%03x combine a logic operation on write mask 0x%02x with write mask 0x%02x", ops & 0xfffu, logicMask, face.writeMask);
|
||||
throw std::runtime_error(detail);
|
||||
}
|
||||
face.writeMask = logicMask;
|
||||
}
|
||||
face.reference = reference;
|
||||
Require(!opValReference || compare == 0 || compare == 7 || face.compareMask == 0, "a stencil compare against the test value is unsupported with a pixel shader that exports the stencil operation value");
|
||||
return face;
|
||||
@@ -1326,6 +1356,27 @@ ReversedBlend TranslateReversedBlend(std::uint32_t blendControl, std::uint8_t co
|
||||
return {blend, routing->code};
|
||||
}
|
||||
|
||||
void ApplyDepthBoundsEmulation(const Context& context, State& state, const ShaderRecompiler::ShaderPixelStageInfo& pixel) {
|
||||
if (!state.depthBoundsTest || !state.depth || context.depthBounds) return;
|
||||
ValidateDepthBounds(context, state);
|
||||
Require(!context.depthRangeUnrestricted, "the depth bounds test without the depthBounds feature is emulated only where stored depth stays in [0, 1]");
|
||||
Require(state.depth->format == VK_FORMAT_D32_SFLOAT || state.depth->format == VK_FORMAT_D32_SFLOAT_S8_UINT, "the depth bounds test on a normalized depth target needs the depthBounds feature");
|
||||
Require(!state.depthTest && !state.depthWrite, "the depth bounds test with depth testing needs the depthBounds feature");
|
||||
Require(!pixel.posZ && !pixel.depthExportEnable, "the depth bounds test from a pixel program that reads or exports depth needs the depthBounds feature");
|
||||
const auto keepsOnFail = [](const VkStencilOpState& face) { return (face.writeMask & 0xffu) == 0 || (face.failOp == VK_STENCIL_OP_KEEP && face.depthFailOp == VK_STENCIL_OP_KEEP); };
|
||||
Require(!state.stencilTest || (keepsOnFail(state.stencilFront) && keepsOnFail(state.stencilBack)), "the depth bounds test with a stencil test that writes on a failed test needs the depthBounds feature");
|
||||
const bool upper = state.minDepthBounds <= 0.0f;
|
||||
Require(upper || state.maxDepthBounds >= 1.0f, "the depth bounds test with both bounds inside (0, 1) needs the depthBounds feature");
|
||||
const float bound = upper ? state.maxDepthBounds : state.minDepthBounds;
|
||||
state.depthBoundsTest = false;
|
||||
state.depthTest = true;
|
||||
state.depthCompare = upper ? VK_COMPARE_OP_GREATER_OR_EQUAL : VK_COMPARE_OP_LESS_OR_EQUAL;
|
||||
state.depthBias = false;
|
||||
state.depthBiasPerFace = false;
|
||||
state.viewport.minDepth = bound;
|
||||
state.viewport.maxDepth = bound;
|
||||
}
|
||||
|
||||
void ApplyDepthRemap(const Context& context, State& state) {
|
||||
const auto& viewport = state.viewport;
|
||||
const bool inRange = viewport.minDepth >= 0 && viewport.minDepth <= 1 && viewport.maxDepth >= 0 && viewport.maxDepth <= 1;
|
||||
|
||||
@@ -123,6 +123,7 @@ struct StorageTraffic {
|
||||
std::map<std::string, std::pair<std::uint64_t, std::uint64_t>> writeBacks;
|
||||
std::array<std::pair<std::uint64_t, std::uint64_t>, 4> uploads{};
|
||||
std::map<std::string, std::pair<std::uint64_t, std::uint64_t>> uploadReasons;
|
||||
std::map<std::string, std::pair<std::uint64_t, std::uint64_t>> drawnStores;
|
||||
std::uint64_t directWriteBacks = 0;
|
||||
std::chrono::steady_clock::time_point lastReport = std::chrono::steady_clock::now();
|
||||
};
|
||||
@@ -132,6 +133,14 @@ StorageTraffic& Traffic() {
|
||||
return traffic;
|
||||
}
|
||||
|
||||
struct DrawnPending {
|
||||
std::size_t images = 0;
|
||||
std::uint64_t bytes = 0;
|
||||
double oldestMs = 0;
|
||||
};
|
||||
|
||||
DrawnPending drawnPending();
|
||||
|
||||
void reportStorageTraffic(StorageTraffic& traffic) {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now - traffic.lastReport < std::chrono::seconds(10)) return;
|
||||
@@ -154,12 +163,19 @@ void reportStorageTraffic(StorageTraffic& traffic) {
|
||||
std::snprintf(text, sizeof(text), " %llu/%.1f", static_cast<unsigned long long>(totals.first), totals.second / 1048576.0);
|
||||
line += " " + reason + text;
|
||||
}
|
||||
std::string drawnLine;
|
||||
for (const auto& [reason, totals] : traffic.drawnStores) {
|
||||
std::snprintf(text, sizeof(text), " %llu/%.1f", static_cast<unsigned long long>(totals.first), totals.second / 1048576.0);
|
||||
drawnLine += " " + reason + text;
|
||||
}
|
||||
const auto drawn = drawnPending();
|
||||
const auto take = [](std::atomic<std::uint64_t>& counter) { return static_cast<unsigned long long>(counter.exchange(0, std::memory_order_relaxed)); };
|
||||
const auto partialUploadCount = take(partialUploads), partialWriteBackCount = take(partialWriteBacks);
|
||||
const auto uploadMiB = take(partialUploadBytes) / 1048576.0, writeBackMiB = take(partialWriteBackBytes) / 1048576.0;
|
||||
AgcDriver::ProfilePrint_nid_no_patch( "[storage] uploads by path (count/MiB, 10 s):%s; %llu write-backs GPU-direct, %llu stored nothing; block units: partial uploads %llu/%.1f, partial write-backs %llu/%.1f, %llu widened to every pending unit, units dropped %llu, superseded %llu, handed over at %llu clear-key refreshes (%llu units superseded), dropped unregistered %llu, kept over a key flip %llu%s, pretest skipped %llu, held for a queued draw read %llu, evicted stale %llu\n", line.c_str(), static_cast<unsigned long long>(traffic.directWriteBacks), take(emptyWriteBacks), partialUploadCount, uploadMiB, partialWriteBackCount, writeBackMiB, take(coalescedWriteBacks), take(unitsDropped), take(unitsSuperseded), take(clearHandOvers), take(handedOverSuperseded), take(unregisteredDropped), take(keyFlipKept), ShadowReport().c_str(), take(pretestSkipped), take(queuedReadHeld), take(staleEvicted));
|
||||
AgcDriver::ProfilePrint_nid_no_patch( "[storage] uploads by path (count/MiB, 10 s):%s; %llu write-backs GPU-direct, %llu stored nothing; block units: partial uploads %llu/%.1f, partial write-backs %llu/%.1f, %llu widened to every pending unit, units dropped %llu, superseded %llu, handed over at %llu clear-key refreshes (%llu units superseded), dropped unregistered %llu, kept over a key flip %llu%s, pretest skipped %llu, held for a queued draw read %llu, evicted stale %llu; drawn linear targets: stores by reason (count/MiB):%s, pending %zu images / %.1f MiB, oldest %.1f ms\n", line.c_str(), static_cast<unsigned long long>(traffic.directWriteBacks), take(emptyWriteBacks), partialUploadCount, uploadMiB, partialWriteBackCount, writeBackMiB, take(coalescedWriteBacks), take(unitsDropped), take(unitsSuperseded), take(clearHandOvers), take(handedOverSuperseded), take(unregisteredDropped), take(keyFlipKept), ShadowReport().c_str(), take(pretestSkipped), take(queuedReadHeld), take(staleEvicted), drawnLine.c_str(), drawn.images, drawn.bytes / 1048576.0, drawn.oldestMs);
|
||||
traffic.writeBacks.clear();
|
||||
traffic.uploadReasons.clear();
|
||||
traffic.drawnStores.clear();
|
||||
traffic.directWriteBacks = 0;
|
||||
}
|
||||
|
||||
@@ -175,7 +191,7 @@ void countStorageUpload(std::size_t path, std::uint64_t bytes) {
|
||||
reportStorageTraffic(traffic);
|
||||
}
|
||||
|
||||
void countStorageWriteBack(std::uint64_t bytes, bool direct) {
|
||||
void countStorageWriteBack(std::uint64_t bytes, bool direct, bool drawn = false) {
|
||||
if (!LookupOutcomes::Profiled()) return;
|
||||
auto& traffic = Traffic();
|
||||
std::lock_guard lock(traffic.mutex);
|
||||
@@ -183,6 +199,11 @@ void countStorageWriteBack(std::uint64_t bytes, bool direct) {
|
||||
++totals.first;
|
||||
totals.second += bytes;
|
||||
if (direct) ++traffic.directWriteBacks;
|
||||
if (drawn) {
|
||||
auto& stores = traffic.drawnStores[flushReason != nullptr ? flushReason : "other"];
|
||||
++stores.first;
|
||||
stores.second += bytes;
|
||||
}
|
||||
reportStorageTraffic(traffic);
|
||||
}
|
||||
|
||||
@@ -1538,6 +1559,13 @@ struct PendingWrites {
|
||||
// Images taken out of `textures` by a FlushPending still storing them (see adjacentPendingUnchanged).
|
||||
std::vector<StorageTexture*> flushing;
|
||||
std::atomic<std::size_t> flushingCount{0};
|
||||
struct Drawn {
|
||||
StorageTexture* texture;
|
||||
std::uint64_t bytes;
|
||||
std::chrono::steady_clock::time_point since;
|
||||
};
|
||||
std::vector<Drawn> drawn;
|
||||
std::atomic<std::size_t> drawnCount{0};
|
||||
};
|
||||
|
||||
PendingWrites& Pending() {
|
||||
@@ -1545,6 +1573,20 @@ PendingWrites& Pending() {
|
||||
return pending;
|
||||
}
|
||||
|
||||
DrawnPending drawnPending() {
|
||||
DrawnPending result;
|
||||
auto& pending = Pending();
|
||||
if (pending.drawnCount.load(std::memory_order_acquire) == 0) return result;
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
std::lock_guard lock(pending.mutex);
|
||||
result.images = pending.drawn.size();
|
||||
for (const auto& entry : pending.drawn) {
|
||||
result.bytes += entry.bytes;
|
||||
result.oldestMs = std::max(result.oldestMs, std::chrono::duration<double, std::milli>(now - entry.since).count());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// See StorageTexture::PendingSerial: bumped after every mutation of the registry above.
|
||||
std::atomic<std::uint64_t> pendingSerial{0};
|
||||
|
||||
@@ -3804,7 +3846,13 @@ void StorageTexture::markLayersPending(std::uint32_t first, std::uint32_t count)
|
||||
}
|
||||
}
|
||||
if (anyUnsaved && !GuestMemory::Watched(descriptor.baseAddress, static_cast<std::size_t>(guestBytes))) captureGuestBytes(&unsaved);
|
||||
for (std::uint32_t layer = first; layer < first + count; ++layer) layerPending[layer] = true;
|
||||
for (std::uint32_t layer = first; layer < first + count; ++layer) {
|
||||
if (!layerPending[layer] && !completionKept.empty()) {
|
||||
const auto end = layerBegin(layer) + layerBytes(layer);
|
||||
for (auto at = layerBegin(layer) & ~std::uint64_t{65535}; at < end; at += 65536) completionKept[static_cast<std::size_t>(at / 65536 - descriptor.baseAddress / 65536)] = 0;
|
||||
}
|
||||
layerPending[layer] = true;
|
||||
}
|
||||
// Results of this image now cover the alias's results borrowed into these units: this image
|
||||
// stores them, the alias no longer has to (its results, unchanged since the borrow, are in
|
||||
// this image's content). An alias written since keeps its own newer results pending.
|
||||
@@ -3827,6 +3875,10 @@ void StorageTexture::markLayersPending(std::uint32_t first, std::uint32_t count)
|
||||
}
|
||||
auto& pending = Pending();
|
||||
std::lock_guard lock(pending.mutex);
|
||||
if (!drawingTarget) {
|
||||
pendingFromOthers = true;
|
||||
if (drawnListed) drawnWhole = true;
|
||||
}
|
||||
// The active recorder installs a hook that also waits for recorded work; without one (tests),
|
||||
// pending storage results alone are flushed.
|
||||
if (Recorder::Active() == nullptr) {
|
||||
@@ -3851,6 +3903,7 @@ void StorageTexture::reconcilePending() {
|
||||
auto& pending = Pending();
|
||||
std::lock_guard lock(pending.mutex);
|
||||
const bool any = anyLayerPending();
|
||||
if (!any) forgetDrawn();
|
||||
if (any == dirty) return;
|
||||
dirty = any;
|
||||
if (any) pending.textures.push_back(this);
|
||||
@@ -3864,6 +3917,215 @@ bool StorageTexture::Dirty() const {
|
||||
return dirty;
|
||||
}
|
||||
|
||||
constexpr std::size_t MaxDrawnRects = 16;
|
||||
|
||||
bool StorageTexture::MarkDrawn(std::uint32_t slice, std::uint32_t count, VkRect2D scissor) {
|
||||
struct Drawing {
|
||||
bool& flag;
|
||||
bool previous;
|
||||
~Drawing() { flag = previous; }
|
||||
} drawing{drawingTarget, std::exchange(drawingTarget, true)};
|
||||
MarkSliceDirty(slice, count);
|
||||
const auto clip = [](std::int64_t value, std::uint32_t limit) { return static_cast<std::uint32_t>(std::clamp<std::int64_t>(value, 0, limit)); };
|
||||
const auto left = clip(scissor.offset.x, descriptor.width);
|
||||
const auto top = clip(scissor.offset.y, descriptor.height);
|
||||
const auto right = clip(std::int64_t{scissor.offset.x} + scissor.extent.width, descriptor.width);
|
||||
const auto bottom = clip(std::int64_t{scissor.offset.y} + scissor.extent.height, descriptor.height);
|
||||
const bool rectangle = proxyImage == VK_NULL_HANDLE && descriptor.tileMode == TextureTileMode::kLinear && !blockUnits && trackedLayers == 1 && descriptor.mipCount == 1 && arrayLayers == 1 && geometry.imageLayers == 1 && geometry.imageDepth == 1 && !geometry.thick && descriptor.dccAddress == 0;
|
||||
auto& pending = Pending();
|
||||
std::lock_guard lock(pending.mutex);
|
||||
if (!anyLayerPending()) return true;
|
||||
if (!drawnListed) {
|
||||
drawnListed = true;
|
||||
drawnWhole = false;
|
||||
drawnRects.clear();
|
||||
pending.drawn.push_back({this, guestBytes, std::chrono::steady_clock::now()});
|
||||
pending.drawnCount.store(pending.drawn.size(), std::memory_order_release);
|
||||
}
|
||||
if (!rectangle || pendingFromOthers) drawnWhole = true;
|
||||
if (!drawnWhole && right > left && bottom > top) addDrawnRect({left, top, right, bottom});
|
||||
return !drawnWhole && drawnRects.size() <= MaxDrawnRects;
|
||||
}
|
||||
|
||||
void StorageTexture::addDrawnRect(DrawnRect rect) {
|
||||
const auto inside = [](const DrawnRect& outer, const DrawnRect& inner) { return outer.left <= inner.left && outer.top <= inner.top && inner.right <= outer.right && inner.bottom <= outer.bottom; };
|
||||
if (std::any_of(drawnRects.begin(), drawnRects.end(), [&](const DrawnRect& kept) { return inside(kept, rect); })) return;
|
||||
for (auto kept = drawnRects.begin(); kept != drawnRects.end();) {
|
||||
const bool columns = kept->left == rect.left && kept->right == rect.right && kept->top <= rect.bottom && rect.top <= kept->bottom;
|
||||
const bool rows = kept->top == rect.top && kept->bottom == rect.bottom && kept->left <= rect.right && rect.left <= kept->right;
|
||||
if (!columns && !rows && !inside(rect, *kept)) {
|
||||
++kept;
|
||||
continue;
|
||||
}
|
||||
rect = {std::min(kept->left, rect.left), std::min(kept->top, rect.top), std::max(kept->right, rect.right), std::max(kept->bottom, rect.bottom)};
|
||||
drawnRects.erase(kept);
|
||||
kept = drawnRects.begin();
|
||||
}
|
||||
drawnRects.push_back(rect);
|
||||
}
|
||||
|
||||
void StorageTexture::forgetDrawn() {
|
||||
pendingFromOthers = false;
|
||||
if (!drawnListed) return;
|
||||
drawnListed = false;
|
||||
drawnWhole = false;
|
||||
drawnRects.clear();
|
||||
auto& pending = Pending();
|
||||
std::erase_if(pending.drawn, [this](const PendingWrites::Drawn& entry) { return entry.texture == this; });
|
||||
pending.drawnCount.store(pending.drawn.size(), std::memory_order_release);
|
||||
}
|
||||
|
||||
void StorageTexture::noteStore(std::uint64_t note) {
|
||||
if (note == 0) return;
|
||||
auto& pending = Pending();
|
||||
std::lock_guard lock(pending.mutex);
|
||||
storeNotes.push_back(note);
|
||||
if (storeNotes.size() > 8) storeNotes.erase(storeNotes.begin());
|
||||
}
|
||||
|
||||
StorageTexture::DrawnArea StorageTexture::drawnArea() const {
|
||||
auto& pending = Pending();
|
||||
std::lock_guard lock(pending.mutex);
|
||||
if (!drawnListed) return {};
|
||||
DrawnArea area{true, !drawnWhole && !pendingFromOthers};
|
||||
if (!area.rect || drawnRects.empty()) return area;
|
||||
area.left = area.top = std::numeric_limits<std::uint32_t>::max();
|
||||
for (const auto& rect : drawnRects) {
|
||||
area.left = std::min(area.left, rect.left);
|
||||
area.top = std::min(area.top, rect.top);
|
||||
area.right = std::max(area.right, rect.right);
|
||||
area.bottom = std::max(area.bottom, rect.bottom);
|
||||
}
|
||||
area.rects = drawnRects;
|
||||
return area;
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::uint64_t, std::uint64_t>> StorageTexture::drawnSpans(const DrawnArea& area, std::span<const std::pair<std::uint64_t, std::uint64_t>> keep) const {
|
||||
std::vector<std::pair<std::uint64_t, std::uint64_t>> spans;
|
||||
if (area.rects.empty()) return spans;
|
||||
const auto& mip = mips.front();
|
||||
const auto elementBytes = static_cast<std::uint64_t>(BytesPerElement(descriptor.format));
|
||||
const auto rowBase = descriptor.baseAddress + mip.tiledOffset;
|
||||
std::vector<std::pair<std::uint32_t, std::uint32_t>> columns;
|
||||
std::size_t next = 0;
|
||||
for (auto row = area.top; row < area.bottom; ++row) {
|
||||
columns.clear();
|
||||
for (const auto& rect : area.rects) {
|
||||
if (rect.top <= row && row < rect.bottom) columns.emplace_back(rect.left, rect.right);
|
||||
}
|
||||
std::sort(columns.begin(), columns.end());
|
||||
const auto rowStart = rowBase + std::uint64_t{row} * mip.pitchBytes;
|
||||
for (std::size_t first = 0; first < columns.size();) {
|
||||
auto right = columns[first].second;
|
||||
auto last = first + 1;
|
||||
for (; last < columns.size() && columns[last].first <= right; ++last) right = std::max(right, columns[last].second);
|
||||
const auto rowBegin = rowStart + columns[first].first * elementBytes;
|
||||
const auto rowEnd = rowStart + right * elementBytes;
|
||||
first = last;
|
||||
while (next < keep.size() && keep[next].second <= rowBegin) ++next;
|
||||
for (auto index = next; index < keep.size() && keep[index].first < rowEnd; ++index) {
|
||||
const auto from = std::max(rowBegin, keep[index].first);
|
||||
const auto to = std::min(rowEnd, keep[index].second);
|
||||
if (from < to) spans.emplace_back(from, to);
|
||||
}
|
||||
}
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
|
||||
std::uint64_t StorageTexture::storeDrawnRect(const DrawnArea& area, std::span<const ImportSegment> segments, std::span<const std::pair<std::uint64_t, std::uint64_t>> stores, const std::vector<std::uint32_t>& saveUnits) {
|
||||
const auto& mip = mips.front();
|
||||
const auto elementBytes = static_cast<std::uint64_t>(BytesPerElement(descriptor.format));
|
||||
const auto rowBase = descriptor.baseAddress + mip.tiledOffset;
|
||||
const auto width = area.right - area.left;
|
||||
const auto height = area.bottom - area.top;
|
||||
const auto rowBytes = std::uint64_t{width} * elementBytes;
|
||||
const auto hullBegin = stores.front().first;
|
||||
const auto hullBytes = static_cast<std::size_t>(stores.back().second - hullBegin);
|
||||
std::uint64_t storedBytes = 0;
|
||||
for (const auto& [from, to] : stores) storedBytes += to - from;
|
||||
auto scratch = std::make_shared<DeviceBuffer>(context, static_cast<std::size_t>(rowBytes * height), VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT);
|
||||
auto* recorder = Recorder::Active();
|
||||
std::unique_ptr<CommandBatch> batch;
|
||||
VkCommandBuffer commands = VK_NULL_HANDLE;
|
||||
auto timing = Recorder::NoTiming;
|
||||
if (recorder != nullptr) {
|
||||
recorder->FlushKeyStoresOverlapping(hullBegin, hullBytes);
|
||||
recorder->FlushStoresOverlapping(hullBegin, hullBytes);
|
||||
commands = recorder->Commands();
|
||||
timing = recorder->BeginGpuTiming(Recorder::CommandClass::StorageWriteBack);
|
||||
recorder->Keep(scratch, scratch->Size());
|
||||
if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self));
|
||||
Recorder::CountBarriers(Recorder::CommandClass::StorageWriteBack, 3);
|
||||
if (Recorder::BarrierValidate()) {
|
||||
const std::pair<VkImage, bool> read{image, false};
|
||||
recorder->NoteAccess(Recorder::CommandClass::StorageWriteBack, Recorder::Access{{}, stores, std::span(&read, 1), VK_PIPELINE_STAGE_TRANSFER_BIT});
|
||||
}
|
||||
} else {
|
||||
batch = std::make_unique<CommandBatch>(context);
|
||||
commands = batch->Handle();
|
||||
}
|
||||
VkImageMemoryBarrier toSource{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
|
||||
toSource.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
||||
toSource.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
||||
toSource.oldLayout = VK_IMAGE_LAYOUT_GENERAL;
|
||||
toSource.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
|
||||
toSource.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
toSource.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
toSource.image = image;
|
||||
toSource.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, descriptor.mipCount, 0, geometry.imageLayers};
|
||||
const VkMemoryBarrier importReady{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT};
|
||||
context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &importReady, 0, nullptr, 1, &toSource);
|
||||
VkBufferImageCopy region{};
|
||||
region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
||||
region.imageOffset = {static_cast<std::int32_t>(area.left), static_cast<std::int32_t>(area.top), 0};
|
||||
region.imageExtent = {width, height, 1u};
|
||||
context.Function<PFN_vkCmdCopyImageToBuffer>("vkCmdCopyImageToBuffer")(commands, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, scratch->Handle(), 1, ®ion);
|
||||
const auto scratchRead = WholeBufferBarrier(scratch->Handle(), VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT);
|
||||
context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &scratchRead, 0, nullptr);
|
||||
const auto scratchOffset = [&](std::uint64_t address) {
|
||||
const auto offset = address - rowBase;
|
||||
const auto row = offset / mip.pitchBytes;
|
||||
return (row - area.top) * rowBytes + offset - row * mip.pitchBytes - area.left * elementBytes;
|
||||
};
|
||||
const auto copyBuffer = context.Function<PFN_vkCmdCopyBuffer>("vkCmdCopyBuffer");
|
||||
for (const auto& segment : segments) {
|
||||
std::vector<VkBufferCopy> copies;
|
||||
for (const auto& [from, to] : stores) {
|
||||
const auto begin = std::max(from, segment.begin);
|
||||
const auto end = std::min(to, segment.end);
|
||||
if (begin < end) copies.push_back({scratchOffset(begin), HostImportOffset(*segment.import, begin), end - begin});
|
||||
}
|
||||
if (!copies.empty()) copyBuffer(commands, scratch->Handle(), segment.import->buffer, static_cast<std::uint32_t>(copies.size()), copies.data());
|
||||
}
|
||||
if (!saveUnits.empty()) {
|
||||
const auto saved = savedBytes->Bytes();
|
||||
for (const auto unit : saveUnits) {
|
||||
const auto offset = static_cast<std::size_t>(layerBegin(unit) - descriptor.baseAddress);
|
||||
std::memcpy(saved.data() + offset, original.data() + offset, static_cast<std::size_t>(layerBytes(unit)));
|
||||
}
|
||||
std::vector<VkBufferCopy> saves;
|
||||
for (const auto& [from, to] : stores) saves.push_back({scratchOffset(from), from - descriptor.baseAddress, to - from});
|
||||
copyBuffer(commands, scratch->Handle(), savedBytes->Handle(), static_cast<std::uint32_t>(saves.size()), saves.data());
|
||||
}
|
||||
VkImageMemoryBarrier backToGeneral = toSource;
|
||||
backToGeneral.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
||||
backToGeneral.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT;
|
||||
backToGeneral.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
|
||||
backToGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL;
|
||||
const VkMemoryBarrier stored{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_READ_BIT};
|
||||
context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &stored, 0, nullptr, 1, &backToGeneral);
|
||||
if (batch) {
|
||||
batch->SubmitAndWait();
|
||||
return storedBytes;
|
||||
}
|
||||
recorder->EndGpuTiming(timing, storedBytes);
|
||||
recorder->MarkShaderReadsCovered();
|
||||
recorder->NotePendingWrite(hullBegin, hullBytes);
|
||||
noteStore(recorder->LastWriteNote(hullBegin, hullBytes));
|
||||
return storedBytes;
|
||||
}
|
||||
|
||||
void StorageTexture::Flush() {
|
||||
{
|
||||
auto& live = Live();
|
||||
@@ -3914,13 +4176,38 @@ void waitForIdlePublish(const char* reason) {
|
||||
bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, const StorageTexture* except, const char* reason, PublishScope scope, bool* published, bool waited) {
|
||||
static_cast<void>(FlushDepthPlanes(address, bytes));
|
||||
if (published != nullptr) *published = false;
|
||||
const auto nothingToStore = [&] {
|
||||
auto& pending = Pending();
|
||||
if (pending.flushingCount.load(std::memory_order_acquire) != 0) return false;
|
||||
if (!pending.textures.SurelyClear(address, bytes)) {
|
||||
std::lock_guard lock(pending.mutex);
|
||||
const auto* exempt = refreshing;
|
||||
if (pending.textures.MayOverlap(address, bytes) && std::any_of(pending.textures.begin(), pending.textures.end(), [&](const StorageTexture* texture) { return texture != except && texture != exempt && texture->overlaps(address, bytes) && texture->pendingUnitInside(address, bytes); })) return false;
|
||||
if (pending.flushingCount.load(std::memory_order_acquire) != 0) return false;
|
||||
}
|
||||
return scope == PublishScope::None || !AnyShadowedOverlaps(address, bytes);
|
||||
};
|
||||
if (nothingToStore()) return false;
|
||||
if (AgcDriver::DriverDetail::DrawPipeline::HeldReads(address, bytes)) {
|
||||
queuedReadHeld.fetch_add(1, std::memory_order_relaxed);
|
||||
return false;
|
||||
}
|
||||
std::unique_lock<GuestMemory::GpuMutexType> drawnHold;
|
||||
if (auto& drawnPending = Pending(); drawnPending.drawnCount.load(std::memory_order_acquire) != 0 && !GuestMemory::GpuMutex().HeldByThisThread()) {
|
||||
bool overlap = false;
|
||||
{
|
||||
std::lock_guard lock(drawnPending.mutex);
|
||||
overlap = std::any_of(drawnPending.drawn.begin(), drawnPending.drawn.end(), [&](const PendingWrites::Drawn& entry) { return entry.texture->dirty && entry.texture->overlaps(address, bytes); });
|
||||
}
|
||||
if (overlap) {
|
||||
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush);
|
||||
drawnHold = std::unique_lock(GuestMemory::GpuMutex());
|
||||
}
|
||||
}
|
||||
// The images stay alive across the scan: their last owner may be a batch's kept list, which
|
||||
// another thread releases outside the GPU mutex once the batch completed.
|
||||
std::vector<std::shared_ptr<StorageTexture>> flush;
|
||||
std::vector<std::uint64_t> ownNotes;
|
||||
{
|
||||
auto& pending = Pending();
|
||||
std::lock_guard lock(pending.mutex);
|
||||
@@ -3948,8 +4235,12 @@ bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, cons
|
||||
for (const auto& texture : flush) pending.flushing.push_back(texture.get());
|
||||
pending.flushingCount.store(pending.flushing.size(), std::memory_order_release);
|
||||
if (!flush.empty()) BumpPendingSerial();
|
||||
for (const auto& texture : flush) {
|
||||
if (texture->drawnListed) ownNotes.insert(ownNotes.end(), texture->storeNotes.begin(), texture->storeNotes.end());
|
||||
}
|
||||
}
|
||||
if (flush.empty()) {
|
||||
if (drawnHold.owns_lock()) drawnHold.unlock();
|
||||
const bool stored = WaitForFlushing(address, bytes, except);
|
||||
// Units shadowed over the range (another image's results retiled into the import's unit
|
||||
// shadow, or an earlier flush's) still reach the import as the scope asks.
|
||||
@@ -3973,10 +4264,11 @@ bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, cons
|
||||
for (const auto& texture : flush) std::fprintf(stderr, "[flush] image 0x%llx+0x%llx for %s 0x%llx+0x%zx\n", static_cast<unsigned long long>(texture->descriptor.baseAddress), static_cast<unsigned long long>(texture->guestBytes), reason, static_cast<unsigned long long>(address), bytes);
|
||||
}
|
||||
std::lock_guard gpu(GuestMemory::GpuMutex());
|
||||
if (auto* recorder = Recorder::Active(); recorder != nullptr && !Recorder::InCompletion() && recorder->PendingWriterSerial(address, bytes) != 0) {
|
||||
if (!ownNotes.empty() && (flush.size() != 1 || std::any_of(flush.begin(), flush.end(), [](const std::shared_ptr<StorageTexture>& texture) { return ExistingHostImportFor(texture->context, texture->descriptor.baseAddress, static_cast<std::size_t>(texture->guestBytes)) == nullptr; }))) ownNotes.clear();
|
||||
if (auto* recorder = Recorder::Active(); recorder != nullptr && !Recorder::InCompletion() && recorder->PendingWriterSerial(address, bytes, ownNotes) != 0) {
|
||||
try {
|
||||
recorder->Reap();
|
||||
if (const auto writer = recorder->PendingWriterSerial(address, bytes); writer == recorder->OpenSerial()) {
|
||||
if (const auto writer = recorder->PendingWriterSerial(address, bytes, ownNotes); writer == recorder->OpenSerial()) {
|
||||
Recorder::CountSync(recorder->OpenWriteOverlaps(address, bytes) ? 1 : 3);
|
||||
recorder->Sync();
|
||||
} else if (writer != 0) {
|
||||
@@ -4086,7 +4378,8 @@ void StorageTexture::saveBeforeUnwatch() {
|
||||
const auto end = begin + layerBytes(unit);
|
||||
const auto spanBegin = begin & ~(block - 1);
|
||||
const auto spanBlocks = static_cast<std::size_t>((end - spanBegin + block - 1) / block);
|
||||
const std::vector<std::uint64_t> generations(spanBlocks, layerGeneration[unit]);
|
||||
std::vector<std::uint64_t> generations(spanBlocks);
|
||||
for (std::size_t k = 0; k < spanBlocks; ++k) generations[k] = unitBlockGeneration(unit, spanBegin + k * block);
|
||||
std::vector<std::uint8_t> changedBlocks(spanBlocks);
|
||||
if (!GuestMemory::ChangedBlocks(begin, static_cast<std::size_t>(end - begin), generations, changedBlocks)) continue;
|
||||
forgetKeptAtUnwatch(unit);
|
||||
@@ -4094,7 +4387,7 @@ void StorageTexture::saveBeforeUnwatch() {
|
||||
const auto from = std::max(at, begin);
|
||||
const auto to = std::min(at + block, end);
|
||||
const bool edge = from != at || to != at + block;
|
||||
if (changedBlocks[static_cast<std::size_t>((at - spanBegin) / block)] == GuestMemory::BlockWritten && (!edge || GuestMemory::StoredOver(from, static_cast<std::size_t>(to - from), layerGeneration[unit])) && !unchangedSinceBaseline(from, to)) keptAtUnwatch.emplace(from, layerGeneration[unit]);
|
||||
if (changedBlocks[static_cast<std::size_t>((at - spanBegin) / block)] == GuestMemory::BlockWritten && (!edge || GuestMemory::StoredOver(from, static_cast<std::size_t>(to - from), unitBlockGeneration(unit, at))) && !unchangedSinceBaseline(from, to)) keptAtUnwatch.emplace(from, layerGeneration[unit]);
|
||||
}
|
||||
layerOriginal[unit] = true;
|
||||
cancelStoredSave(unit);
|
||||
@@ -4258,6 +4551,49 @@ void StorageTexture::StoreReadbackBeforeLabel(const char* reason) {
|
||||
for (const auto& [address, bytes] : ranges) static_cast<void>(FlushPending(address, bytes, nullptr, reason));
|
||||
}
|
||||
|
||||
std::size_t StorageTexture::StoreDrawnLinear(const char* reason) {
|
||||
auto& pending = Pending();
|
||||
if (pending.drawnCount.load(std::memory_order_acquire) == 0 || Recorder::InCompletion()) return 0;
|
||||
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush);
|
||||
std::lock_guard gpu(GuestMemory::GpuMutex());
|
||||
std::vector<std::pair<std::uint64_t, std::size_t>> ranges;
|
||||
{
|
||||
std::lock_guard lock(pending.mutex);
|
||||
for (const auto& entry : pending.drawn) {
|
||||
if (entry.texture->dirty) ranges.emplace_back(entry.texture->descriptor.baseAddress, static_cast<std::size_t>(entry.texture->guestBytes));
|
||||
}
|
||||
}
|
||||
std::size_t stored = 0;
|
||||
for (const auto& [address, bytes] : ranges) {
|
||||
if (FlushPending(address, bytes, nullptr, reason)) ++stored;
|
||||
}
|
||||
return stored;
|
||||
}
|
||||
|
||||
std::size_t StorageTexture::StoreDrawnLinearTouching(const char* reason, const std::function<bool(std::uint64_t, std::uint64_t)>& touches, bool all) {
|
||||
auto& pending = Pending();
|
||||
if (pending.drawnCount.load(std::memory_order_acquire) == 0 || Recorder::InCompletion()) return 0;
|
||||
std::vector<std::pair<std::uint64_t, std::size_t>> ranges;
|
||||
{
|
||||
std::lock_guard lock(pending.mutex);
|
||||
for (const auto& entry : pending.drawn) {
|
||||
if (!entry.texture->dirty) continue;
|
||||
const auto begin = entry.texture->descriptor.baseAddress;
|
||||
const auto end = begin + entry.texture->guestBytes;
|
||||
if (!all && !touches(begin, end)) continue;
|
||||
ranges.emplace_back(begin, static_cast<std::size_t>(entry.texture->guestBytes));
|
||||
}
|
||||
}
|
||||
if (ranges.empty()) return 0;
|
||||
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush);
|
||||
std::lock_guard gpu(GuestMemory::GpuMutex());
|
||||
std::size_t stored = 0;
|
||||
for (const auto& [address, bytes] : ranges) {
|
||||
if (FlushPending(address, bytes, nullptr, reason)) ++stored;
|
||||
}
|
||||
return stored;
|
||||
}
|
||||
|
||||
bool StorageTexture::AnyPending() {
|
||||
auto& pending = Pending();
|
||||
std::lock_guard lock(pending.mutex);
|
||||
@@ -4324,37 +4660,96 @@ bool PendingStorageOverlaps(std::uint64_t address, std::size_t bytes, const Stor
|
||||
}
|
||||
|
||||
std::uint64_t StorageTexture::StampKeepingNewerResults(std::uint64_t address, std::size_t bytes, std::uint64_t recorded, const std::function<std::uint64_t()>& stamp) {
|
||||
std::vector<std::pair<std::shared_ptr<StorageTexture>, std::uint32_t>> kept;
|
||||
struct Candidate {
|
||||
std::shared_ptr<StorageTexture> texture;
|
||||
std::uint32_t unit;
|
||||
bool drawn;
|
||||
};
|
||||
struct Kept {
|
||||
std::shared_ptr<StorageTexture> texture;
|
||||
std::uint32_t unit;
|
||||
std::uint64_t from;
|
||||
std::uint64_t to;
|
||||
std::uint64_t since;
|
||||
bool whole;
|
||||
bool drawn;
|
||||
};
|
||||
std::vector<Candidate> candidates;
|
||||
if (recorded != 0 && bytes != 0) {
|
||||
auto& pending = Pending();
|
||||
std::lock_guard lock(pending.mutex);
|
||||
for (auto* texture : pending.textures) {
|
||||
if (!texture->blockUnits || !texture->overlaps(address, bytes)) continue;
|
||||
const auto consider = [&](StorageTexture* texture) {
|
||||
if ((!texture->blockUnits && !texture->drawnListed) || !texture->overlaps(address, bytes)) return;
|
||||
auto alive = texture->weak_from_this().lock();
|
||||
if (alive == nullptr) continue;
|
||||
if (alive == nullptr) return;
|
||||
for (std::uint32_t unit = 0; unit < texture->trackedLayers; ++unit) {
|
||||
const auto begin = texture->layerBegin(unit);
|
||||
const auto unitBytes = texture->layerBytes(unit);
|
||||
if (!texture->layerPending[unit] || begin >= address + bytes || address >= begin + unitBytes) continue;
|
||||
if (texture->layerGeneration[unit] >= recorded) kept.emplace_back(alive, unit);
|
||||
if (texture->layerGeneration[unit] >= recorded) candidates.push_back({alive, unit, texture->drawnListed});
|
||||
}
|
||||
};
|
||||
for (auto* texture : pending.textures) consider(texture);
|
||||
for (auto* texture : pending.flushing) consider(texture);
|
||||
}
|
||||
constexpr std::uint64_t block = 65536;
|
||||
std::vector<Kept> kept;
|
||||
for (const auto& [texture, unit, drawn] : candidates) {
|
||||
const auto begin = texture->layerBegin(unit);
|
||||
const auto end = begin + texture->layerBytes(unit);
|
||||
GuestMemory::CollectWritesUncached(begin, static_cast<std::size_t>(end - begin));
|
||||
if (GuestMemory::UnchangedSince(begin, static_cast<std::size_t>(end - begin), texture->layerGeneration[unit])) {
|
||||
kept.push_back({texture, unit, begin, end, texture->layerGeneration[unit], true, drawn});
|
||||
continue;
|
||||
}
|
||||
if (!drawn) continue;
|
||||
const auto first = std::max(begin, address) / block;
|
||||
const auto last = (std::min(end, address + bytes) - 1) / block;
|
||||
const auto from = std::max(begin, first * block);
|
||||
const auto to = std::min(end, (last + 1) * block);
|
||||
std::vector<std::uint64_t> generations(static_cast<std::size_t>(last - first + 1));
|
||||
for (std::size_t k = 0; k < generations.size(); ++k) generations[k] = texture->unitBlockGeneration(unit, (first + k) * block);
|
||||
std::vector<std::uint8_t> changed(generations.size());
|
||||
if (!GuestMemory::ChangedBlocks(from, static_cast<std::size_t>(to - from), generations, changed)) continue;
|
||||
for (std::size_t k = 0; k < generations.size(); ++k) {
|
||||
const auto at = (first + k) * block;
|
||||
const auto blockFrom = std::max(at, begin);
|
||||
const auto blockTo = std::min(at + block, end);
|
||||
const bool edge = blockFrom != at || blockTo != at + block;
|
||||
if (changed[k] == GuestMemory::BlockWritten && (!edge || !GuestMemory::UnchangedSince(blockFrom, static_cast<std::size_t>(blockTo - blockFrom), generations[k]))) continue;
|
||||
if (!kept.empty() && !kept.back().whole && kept.back().texture == texture && kept.back().unit == unit && kept.back().since == generations[k] && kept.back().to == blockFrom) kept.back().to = blockTo;
|
||||
else kept.push_back({texture, unit, blockFrom, blockTo, generations[k], false, true});
|
||||
}
|
||||
}
|
||||
std::erase_if(kept, [](const auto& entry) {
|
||||
const auto& [texture, unit] = entry;
|
||||
const auto begin = texture->layerBegin(unit);
|
||||
const auto unitBytes = static_cast<std::size_t>(texture->layerBytes(unit));
|
||||
GuestMemory::CollectWritesUncached(begin, unitBytes);
|
||||
return !GuestMemory::UnchangedSince(begin, unitBytes, texture->layerGeneration[unit]);
|
||||
});
|
||||
const auto generation = stamp();
|
||||
if (generation == 0 || kept.empty()) return generation;
|
||||
std::vector<Kept> confirmed;
|
||||
for (const auto& entry : kept) {
|
||||
if ((entry.whole && !entry.drawn) || GuestMemory::UnchangedSinceCollected(entry.from, static_cast<std::size_t>(entry.to - entry.from), entry.since)) {
|
||||
confirmed.push_back(entry);
|
||||
continue;
|
||||
}
|
||||
if (entry.from / block == (entry.to - 1) / block) continue;
|
||||
const auto last = (std::min(entry.to, address + bytes) - 1) / block;
|
||||
for (auto at = std::max(entry.from, address) / block * block; at <= last * block; at += block) {
|
||||
const auto from = std::max(at, entry.from);
|
||||
const auto to = std::min(at + block, entry.to);
|
||||
if (GuestMemory::UnchangedSinceCollected(from, static_cast<std::size_t>(to - from), entry.since)) confirmed.push_back({entry.texture, entry.unit, from, to, entry.since, false, true});
|
||||
}
|
||||
}
|
||||
auto& pending = Pending();
|
||||
std::lock_guard lock(pending.mutex);
|
||||
for (const auto& [texture, unit] : kept) {
|
||||
if (!texture->layerPending[unit]) continue;
|
||||
texture->layerGeneration[unit] = generation;
|
||||
texture->refreshGeneration();
|
||||
for (const auto& entry : confirmed) {
|
||||
auto& texture = *entry.texture;
|
||||
if (!texture.layerPending[entry.unit]) continue;
|
||||
if (entry.whole) {
|
||||
texture.layerGeneration[entry.unit] = generation;
|
||||
texture.refreshGeneration();
|
||||
continue;
|
||||
}
|
||||
const auto spanBegin = texture.descriptor.baseAddress / block;
|
||||
if (texture.completionKept.empty()) texture.completionKept.assign(static_cast<std::size_t>((texture.descriptor.baseAddress + texture.guestBytes - 1) / block - spanBegin + 1), 0);
|
||||
for (auto at = entry.from / block; at <= (entry.to - 1) / block; ++at) texture.completionKept[static_cast<std::size_t>(at - spanBegin)] = generation;
|
||||
}
|
||||
return generation;
|
||||
}
|
||||
@@ -4470,6 +4865,7 @@ bool StorageTexture::AccessKeptByCpu(std::uint64_t address, std::size_t bytes, s
|
||||
// store there would see, and the stamps only grow.
|
||||
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Hook);
|
||||
std::lock_guard gpu(GuestMemory::GpuMutex());
|
||||
if (std::any_of(overlapping.begin(), overlapping.end(), [](const std::shared_ptr<StorageTexture>& texture) { return texture->drawnArea().drawn; })) return false;
|
||||
if (!blocksKept(overlapping, address, bytes)) return false;
|
||||
const auto site = static_cast<std::uint8_t>(GuestMemory::CurrentReadSite());
|
||||
std::size_t dropped = 0;
|
||||
@@ -4601,6 +4997,7 @@ std::size_t StorageTexture::DiscardPendingInside(std::uint64_t address, std::siz
|
||||
texture->layerPending.assign(texture->trackedLayers, false);
|
||||
texture->layerOriginal.assign(texture->trackedLayers, false);
|
||||
texture->cancelStoredSaves();
|
||||
texture->forgetDrawn();
|
||||
it = pending.textures.erase(it);
|
||||
++discarded;
|
||||
} else if (texture != exempt && texture->blockUnits && texture->overlaps(address, bytes)) {
|
||||
@@ -5210,6 +5607,12 @@ void StorageTexture::blockGenerations(std::vector<std::uint64_t>& generations) c
|
||||
}
|
||||
}
|
||||
|
||||
std::uint64_t StorageTexture::unitBlockGeneration(std::uint32_t unit, std::uint64_t at) const {
|
||||
const auto unitGeneration = layerGeneration[unit];
|
||||
if (unitGeneration == 0 || !layerPending[unit] || completionKept.empty()) return unitGeneration;
|
||||
return std::max(unitGeneration, completionKept[static_cast<std::size_t>(at / 65536 - descriptor.baseAddress / 65536)]);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::atomic<bool>& ClearHandOverOn() {
|
||||
@@ -5506,10 +5909,12 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
|
||||
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
|
||||
if (!layers[layer]) continue;
|
||||
const auto end = layerBegin(layer) + layerBytes(layer);
|
||||
for (auto at = layerBegin(layer) & ~(block - 1); at < end; at += block) generations[static_cast<std::size_t>((at - spanBegin) / block)] = layerGeneration[layer];
|
||||
for (auto at = layerBegin(layer) & ~(block - 1); at < end; at += block) generations[static_cast<std::size_t>((at - spanBegin) / block)] = unitBlockGeneration(layer, at);
|
||||
}
|
||||
const auto drawn = drawnArea();
|
||||
std::vector<std::uint8_t> changedBlocks(spanBlocks);
|
||||
const bool tracked = GuestMemory::ChangedBlocks(firstStored, static_cast<std::size_t>(lastStored - firstStored), generations, changedBlocks);
|
||||
std::vector<std::uint8_t> cpuBlocks(drawn.drawn ? spanBlocks : 0);
|
||||
const bool tracked = GuestMemory::ChangedBlocks(firstStored, static_cast<std::size_t>(lastStored - firstStored), generations, changedBlocks, cpuBlocks);
|
||||
std::vector<bool> compared(trackedLayers, false);
|
||||
if (!tracked) {
|
||||
awaitStoredSave(&layers);
|
||||
@@ -5534,7 +5939,8 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
|
||||
if (!layers[layer]) continue;
|
||||
const auto begin = layerBegin(layer);
|
||||
const auto end = begin + layerBytes(layer);
|
||||
if (tracked ? layerGeneration[layer] == 0 : !compared[layer]) {
|
||||
const bool unknown = tracked ? layerGeneration[layer] == 0 : !compared[layer];
|
||||
if (unknown && !drawn.rect) {
|
||||
unsavedOutsideWatch = unsavedOutsideWatch || !tracked;
|
||||
keep.emplace_back(begin, end);
|
||||
continue;
|
||||
@@ -5544,12 +5950,16 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
|
||||
const auto to = std::min(at + block, end);
|
||||
if (from >= to) continue;
|
||||
const bool edge = from != at || to != at + block;
|
||||
if (keptAtUnwatchHolds(from, layer) || (changedBlocks[static_cast<std::size_t>((at - spanBegin) / block)] == GuestMemory::BlockWritten && (compared[layer] || !edge || GuestMemory::StoredOver(from, static_cast<std::size_t>(to - from), layerGeneration[layer])) && !unchangedSinceBaseline(from, to))) {
|
||||
const auto index = static_cast<std::size_t>((at - spanBegin) / block);
|
||||
const bool kept = keptAtUnwatchHolds(from, layer);
|
||||
const bool written = kept || unknown || (changedBlocks[index] == GuestMemory::BlockWritten && (compared[layer] || !edge || GuestMemory::StoredOver(from, static_cast<std::size_t>(to - from), unitBlockGeneration(layer, at))) && !unchangedSinceBaseline(from, to));
|
||||
if (written && (!drawn.rect || (tracked && !kept && !unknown && cpuBlocks[index] == 0))) {
|
||||
skippedAny = true;
|
||||
skippedLayer[layer] = true;
|
||||
++skipped;
|
||||
continue;
|
||||
}
|
||||
if (written && drawn.rect) skippedLayer[layer] = true;
|
||||
if (!keep.empty() && keep.back().second == from) keep.back().second = to;
|
||||
else keep.emplace_back(from, to);
|
||||
}
|
||||
@@ -5563,10 +5973,15 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
|
||||
const auto first = keep.empty() ? descriptor.baseAddress + guestBytes : keep.front().first;
|
||||
std::fprintf(stderr, "[flush] image 0x%llx+0x%llx keeps %zu CPU-written 64 KiB blocks (first stored byte at +0x%llx, %zu ranges stored, generation %llu)\n", static_cast<unsigned long long>(descriptor.baseAddress), static_cast<unsigned long long>(guestBytes), skipped, static_cast<unsigned long long>(first - descriptor.baseAddress), keep.size(), static_cast<unsigned long long>(generation));
|
||||
}
|
||||
std::vector<std::pair<std::uint64_t, std::uint64_t>> spans;
|
||||
if (drawn.rect) spans = drawnSpans(drawn, keep);
|
||||
const auto& stores = drawn.rect ? spans : keep;
|
||||
const auto stampBegin = drawn.rect && !stores.empty() ? stores.front().first : firstStored;
|
||||
const auto stampEnd = drawn.rect && !stores.empty() ? stores.back().second : lastStored;
|
||||
// Taken before this write-back stamps anything (see advanceAdjacent), with the 64 KiB block span
|
||||
// its stamps cover.
|
||||
const auto firstBlock = firstStored / block;
|
||||
const auto lastBlock = (lastStored - 1) / block;
|
||||
const auto firstBlock = stampBegin / block;
|
||||
const auto lastBlock = (stampEnd - 1) / block;
|
||||
const auto adjacent = adjacentPendingUnchanged(firstBlock, lastBlock);
|
||||
// After the store: the stored layers are in sync at a fresh generation unless blocks were kept
|
||||
// for the CPU (then the layer is stale there, keeps its old generation and re-uploads at its
|
||||
@@ -5603,7 +6018,7 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
|
||||
advanceAdjacent(adjacent, now, firstBlock, lastBlock);
|
||||
if (!shadowed.empty()) markShadowed(now);
|
||||
};
|
||||
if (keep.empty()) {
|
||||
if (stores.empty()) {
|
||||
// Every selected block was written by the CPU since the layer's generation: nothing to
|
||||
// store, the keys stay as they are, and the image is stale there (re-uploaded at its next
|
||||
// use, never from a matching `original`).
|
||||
@@ -5633,6 +6048,24 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
|
||||
++Profile().storageDirectWriteBacks;
|
||||
return;
|
||||
}
|
||||
if (drawn.rect) {
|
||||
PublishShadow(stampBegin, static_cast<std::size_t>(stampEnd - stampBegin), PublishScope::PartialUnits, PublishReason::Upload);
|
||||
const auto storedBytes = storeDrawnRect(drawn, segments, stores, saveUnits);
|
||||
countStorageWriteBack(storedBytes, true, true);
|
||||
if (profile) Profile().storageGpu += timer.lap();
|
||||
forgetStored();
|
||||
uploadedKeys = DccKeys::Uncompressed;
|
||||
std::vector<std::pair<std::uint64_t, std::uint64_t>> stamps;
|
||||
for (const auto& [from, to] : stores) {
|
||||
if (!stamps.empty() && from / block <= (stamps.back().second - 1) / block + 1) stamps.back().second = to;
|
||||
else stamps.emplace_back(from, to);
|
||||
}
|
||||
for (const auto& [from, to] : stamps) GuestMemory::MarkWritten(from, static_cast<std::size_t>(to - from));
|
||||
settle(true);
|
||||
commitStoredSave(saveUnits);
|
||||
++Profile().storageDirectWriteBacks;
|
||||
return;
|
||||
}
|
||||
// The whole-layer copies below overwrite the interior units of the span; the edge units
|
||||
// keep a neighbour's shadowed results in the rest of their bytes, which reach the import
|
||||
// first (this write-back's stamps then make those units stale).
|
||||
@@ -5735,8 +6168,9 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
|
||||
recorder->EndGpuTiming(timing, storedBytes);
|
||||
recorder->MarkShaderReadsCovered();
|
||||
recorder->NotePendingWrite(firstStored, static_cast<std::size_t>(lastStored - firstStored));
|
||||
if (drawn.drawn) noteStore(recorder->LastWriteNote(firstStored, static_cast<std::size_t>(lastStored - firstStored)));
|
||||
}
|
||||
countStorageWriteBack(storedBytes, true);
|
||||
countStorageWriteBack(storedBytes, true, drawn.drawn);
|
||||
if (profile) Profile().storageGpu += timer.lap();
|
||||
// The guest bytes now differ from `original`; other caches of the range see the write.
|
||||
forgetStored();
|
||||
@@ -5777,10 +6211,10 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
|
||||
overhang = covered >= end;
|
||||
}
|
||||
}
|
||||
const bool registered = std::all_of(keep.begin(), keep.end(), [](const auto& range) { return RegisteredReadableCovers(range.first, static_cast<std::size_t>(range.second - range.first)); });
|
||||
const bool registered = std::all_of(stores.begin(), stores.end(), [](const auto& range) { return RegisteredReadableCovers(range.first, static_cast<std::size_t>(range.second - range.first)); });
|
||||
if (unregisteredDrop && !overhang && !registered) {
|
||||
static std::atomic<int> reports{0};
|
||||
if (reports.fetch_add(1) < 4) std::fprintf(stderr, "[gpu] storage image 0x%llx+0x%llx: memory no longer registered; %zu pending ranges dropped\n", static_cast<unsigned long long>(descriptor.baseAddress), static_cast<unsigned long long>(guestBytes), keep.size());
|
||||
if (reports.fetch_add(1) < 4) std::fprintf(stderr, "[gpu] storage image 0x%llx+0x%llx: memory no longer registered; %zu pending ranges dropped\n", static_cast<unsigned long long>(descriptor.baseAddress), static_cast<unsigned long long>(guestBytes), stores.size());
|
||||
unregisteredDropped.fetch_add(1, std::memory_order_relaxed);
|
||||
forgetStored();
|
||||
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
|
||||
@@ -5797,6 +6231,10 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
|
||||
std::vector<bool> unsaved = layerOriginal;
|
||||
unsaved.flip();
|
||||
for (const auto& [begin, end] : unitRuns(unsaved)) {
|
||||
if (drawn.rect) {
|
||||
GuestMemory::ReadCommitted(descriptor.baseAddress + begin, std::span(original).subspan(static_cast<std::size_t>(begin), static_cast<std::size_t>(end - begin)));
|
||||
continue;
|
||||
}
|
||||
for (const auto& [from, to] : keep) {
|
||||
const auto first = std::max(descriptor.baseAddress + begin, from);
|
||||
const auto last = std::min(descriptor.baseAddress + end, to);
|
||||
@@ -5858,7 +6296,9 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
|
||||
APS5_LOG_CHARS_OUT("StorageTexture writeback submit");
|
||||
batch.SubmitAndWait();
|
||||
APS5_LOG_CHARS_OUT("StorageTexture writeback done");
|
||||
countStorageWriteBack(guestBytes, false);
|
||||
std::uint64_t storedBytes = 0;
|
||||
for (const auto& [from, to] : stores) storedBytes += to - from;
|
||||
countStorageWriteBack(drawn.rect ? storedBytes : guestBytes, false, drawn.drawn);
|
||||
if (profile) Profile().storageGpu += timer.lap();
|
||||
if (dump) {
|
||||
char name[64];
|
||||
@@ -5873,7 +6313,7 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
|
||||
// Store only the kept ranges' blocks that changed, so concurrent CPU writes to untouched texels
|
||||
// survive; `original` follows for the stored layers (the others' guest bytes are unchanged).
|
||||
const auto current = host.Bytes();
|
||||
for (const auto& [from, to] : keep) {
|
||||
for (const auto& [from, to] : stores) {
|
||||
const auto offset = static_cast<std::size_t>(from - descriptor.baseAddress);
|
||||
const auto length = static_cast<std::size_t>(to - from);
|
||||
GuestMemory::WriteChangedCommitted(from, current.subspan(offset, length), std::span<const std::byte>(original).subspan(offset, length));
|
||||
@@ -5909,6 +6349,7 @@ StorageTexture::~StorageTexture() {
|
||||
{
|
||||
auto& pending = Pending();
|
||||
std::lock_guard lock(pending.mutex);
|
||||
forgetDrawn();
|
||||
if (dirty) {
|
||||
dirty = false;
|
||||
pending.textures.remove(this);
|
||||
|
||||
@@ -466,23 +466,26 @@ bool LevelsFitAllocation(const GuestTextureResource& surface, std::uint32_t leve
|
||||
return true;
|
||||
}
|
||||
|
||||
GuestTextureResource ViewedArrayWindow(const GuestTextureResource& viewed) {
|
||||
GuestTextureResource ViewedArrayWindow(const GuestTextureResource& viewed, bool keysReadAsStored) {
|
||||
const bool layered = viewed.dimension == TextureDimension::k2DArray || viewed.dimension == TextureDimension::k1DArray || viewed.dimension == TextureDimension::kCube;
|
||||
if (!layered || viewed.baseArray == 0 || viewed.baseArray > viewed.depthOrLastArray || viewed.dccAddress != 0) return viewed;
|
||||
if (!layered || viewed.baseArray == 0 || viewed.baseArray > viewed.depthOrLastArray || (viewed.dccAddress != 0 && !keysReadAsStored)) return viewed;
|
||||
std::uint32_t sliceMask = 0;
|
||||
if (const auto xorMode = XorSwizzleMode(viewed.tileMode); xorMode != 0u) {
|
||||
const auto* equation = FindTextureSwizzleEquation(xorMode, BytesPerElement(viewed.format));
|
||||
if (equation == nullptr) return viewed;
|
||||
std::uint32_t sliceBits = 0;
|
||||
for (const auto bit : equation->bits) sliceBits |= bit >> 24u;
|
||||
const auto sliceMask = sliceBits == 0 ? 0u : (1u << std::bit_width(sliceBits)) - 1u;
|
||||
if ((viewed.baseArray & sliceMask) != 0) return viewed;
|
||||
sliceMask = sliceBits == 0 ? 0u : (1u << std::bit_width(sliceBits)) - 1u;
|
||||
}
|
||||
const auto first = viewed.baseArray & ~sliceMask;
|
||||
if (first == 0) return viewed;
|
||||
const auto geometry = DescribeSurface(viewed);
|
||||
if (geometry.thick || geometry.layers != viewed.depthOrLastArray + 1u) return viewed;
|
||||
auto window = viewed;
|
||||
window.baseAddress = viewed.baseAddress + geometry.GuestLayerOffset(viewed.baseArray);
|
||||
window.depthOrLastArray = viewed.depthOrLastArray - viewed.baseArray;
|
||||
window.baseArray = 0;
|
||||
window.baseAddress = viewed.baseAddress + geometry.GuestLayerOffset(first);
|
||||
window.depthOrLastArray = viewed.depthOrLastArray - first;
|
||||
window.baseArray = viewed.baseArray - first;
|
||||
window.dccAddress = 0;
|
||||
return window;
|
||||
}
|
||||
|
||||
|
||||
@@ -2360,6 +2360,64 @@ void importedHeapMirrorTests(const Context& context, const BdaTestAccess& access
|
||||
GuestArena::GuestArenaRelease_nid_postfix(block, bytes);
|
||||
}
|
||||
|
||||
void retiredImportWatchTests(const Context& context) {
|
||||
#ifdef _WIN32
|
||||
if (!GuestArena::GuestArenaAvailable_nid_postfix() || !GuestArena::GuestArenaWriteWatched_nid_postfix()) {
|
||||
std::cout << "guest arena unavailable or not write-watched: the write watch of retired imports not tested\n";
|
||||
return;
|
||||
}
|
||||
constexpr std::size_t piece = 65536;
|
||||
constexpr std::size_t bytes = 2 * piece;
|
||||
void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, bytes);
|
||||
GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes);
|
||||
std::memset(block, 0x5a, bytes);
|
||||
auto* guest = static_cast<std::uint8_t*>(block);
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(block);
|
||||
auto importing = context;
|
||||
importing.hostImportAlignment = piece;
|
||||
SetHostImportWindow(bytes);
|
||||
SetHostImportDeferredSettles(false);
|
||||
ClearHostImports(importing.device);
|
||||
const auto decided = PrepareImportWatch(importing);
|
||||
SetImportWatch(importing, ImportWatch::Unwatch);
|
||||
const auto registry = [&](std::size_t offset, bool add) {
|
||||
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
|
||||
if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, guest + offset, piece, true, true, true);
|
||||
else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, guest + offset);
|
||||
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
|
||||
};
|
||||
const auto build = [&] {
|
||||
GuestBufferMemory leased(importing);
|
||||
leased.AcquireRegistered();
|
||||
leased.Upload(true);
|
||||
leased.WriteBack();
|
||||
};
|
||||
registry(0, true);
|
||||
build();
|
||||
Require(HostImportCovers(importing, address, piece), "the first 64 KiB piece was not imported on its own");
|
||||
Require(!AgcDriver::GuestMemory::Watched(address, piece), "an unwatched 64 KiB import kept its piece in the write watch");
|
||||
registry(piece, true);
|
||||
SetHostImportBudget(4096);
|
||||
const auto before = MirrorCounters();
|
||||
build();
|
||||
Require(!HostImportCovers(importing, address, piece), "the piece's import outlived the run that grew past it, or a 128 KiB block fit a 4 KiB budget");
|
||||
Require(AgcDriver::GuestMemory::Watched(address, piece), "a 64 KiB piece whose import retired when its run grew into a block stayed outside the write watch");
|
||||
Require(MirrorCounters().heapMirrors == before.heapMirrors + 2, "a 64 KiB piece whose import retired when its run grew into a block was copied, not heap mirrored, once no import fit");
|
||||
registry(piece, false);
|
||||
registry(0, false);
|
||||
build();
|
||||
SetImportWatch(importing, decided);
|
||||
ClearHostImports(importing.device);
|
||||
SetHostImportBudget(0);
|
||||
SetHostImportWindow(0);
|
||||
SetHostImportDeferredSettles(true);
|
||||
GuestArena::GuestArenaReset_nid_postfix(block, bytes);
|
||||
GuestArena::GuestArenaRelease_nid_postfix(block, bytes);
|
||||
#else
|
||||
static_cast<void>(context);
|
||||
#endif
|
||||
}
|
||||
|
||||
void importedFreshTests(const Context& context) {
|
||||
#ifndef _WIN32
|
||||
if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) {
|
||||
@@ -3574,6 +3632,200 @@ void hostWriteWatchExclusionTests() {
|
||||
#endif
|
||||
}
|
||||
|
||||
void sparseJoinedHeapMirrorTests(const Context& context, const BdaTestAccess& access) {
|
||||
constexpr std::size_t half = 1u << 20u;
|
||||
constexpr std::size_t reserved = 2 * half;
|
||||
constexpr std::size_t page = 65536;
|
||||
#ifdef _WIN32
|
||||
if (!GuestArena::GuestArenaAvailable_nid_postfix() || !GuestArena::GuestArenaWriteWatched_nid_postfix()) {
|
||||
std::cout << "guest arena unavailable or not write-watched: sparse heap mirrors over adjacent ranges not tested\n";
|
||||
return;
|
||||
}
|
||||
void* block = GuestArena::GuestArenaAllocate_nid_postfix(reserved, page);
|
||||
auto* guest = static_cast<std::uint8_t*>(block);
|
||||
GuestArena::GuestArenaCommit_nid_postfix(guest, page, PAGE_READWRITE, page);
|
||||
GuestArena::GuestArenaCommit_nid_postfix(guest + half, page, PAGE_READWRITE, page);
|
||||
#else
|
||||
if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) {
|
||||
std::cout << "write watch unavailable: sparse heap mirrors over adjacent ranges not tested\n";
|
||||
return;
|
||||
}
|
||||
void* mapping = mmap(nullptr, reserved + page, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
Require(mapping != MAP_FAILED, "cannot reserve the sparse heap over adjacent ranges");
|
||||
auto* guest = reinterpret_cast<std::uint8_t*>((reinterpret_cast<std::uintptr_t>(mapping) + page - 1) & ~std::uintptr_t{page - 1});
|
||||
Require(mprotect(guest, page, PROT_READ | PROT_WRITE) == 0 && mprotect(guest + half, page, PROT_READ | PROT_WRITE) == 0, "cannot commit the sparse heap pages");
|
||||
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(guest, reserved);
|
||||
void* block = guest;
|
||||
#endif
|
||||
auto* second = guest + half;
|
||||
std::memset(guest, 0x5a, page);
|
||||
std::memset(second, 0x6b, page);
|
||||
const auto base = reinterpret_cast<std::uintptr_t>(block);
|
||||
const auto registry = [&](bool add) {
|
||||
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
|
||||
if (add) {
|
||||
GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, guest, half, true, true, true);
|
||||
GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, second, half, true, true, true);
|
||||
} else {
|
||||
GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, guest);
|
||||
GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, second);
|
||||
}
|
||||
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
|
||||
};
|
||||
const auto sweep = [&] {
|
||||
GuestBufferMemory memory(context);
|
||||
memory.AcquireRegistered();
|
||||
memory.Upload(true);
|
||||
memory.WriteBack();
|
||||
};
|
||||
sweep();
|
||||
registry(true);
|
||||
const auto bind = [&] {
|
||||
GuestBufferMemory memory(context);
|
||||
memory.AddWritable(base, reserved);
|
||||
memory.Upload(false);
|
||||
std::uint32_t adjustment = 0;
|
||||
const auto view = memory.Descriptor(base, reserved, adjustment);
|
||||
Require(adjustment == 0, "a sparse heap view over adjacent ranges is adjusted");
|
||||
const auto bytes = access.bytes(view.buffer);
|
||||
Require(view.range >= half + page && bytes[view.offset + page - 1] == std::byte{0x5a} && bytes[view.offset + page] == std::byte{} && bytes[view.offset + half] == std::byte{0x6b}, "a sparse heap over adjacent ranges is not bound to its committed extent");
|
||||
bytes[view.offset + half + 8] = std::byte{0x77};
|
||||
memory.WriteBack();
|
||||
return view;
|
||||
};
|
||||
const auto before = MirrorCounters();
|
||||
const auto first = bind();
|
||||
const auto made = MirrorCounters();
|
||||
Require(made.sparseBinds == before.sparseBinds + 1 && made.heapMirrors == before.heapMirrors + 1, "a sparse heap whose committed span crosses an adjacent range was not mirrored");
|
||||
Require(second[8] == 0x77, "a store to the adjacent range of a mirrored sparse heap was not written back");
|
||||
second[8] = 0x6b;
|
||||
const auto again = bind();
|
||||
Require(again.buffer == first.buffer && MirrorCounters().rebuilds == made.rebuilds && MirrorCounters().sparseBinds == made.sparseBinds + 1, "a sparse heap over unchanged adjacent ranges did not reuse its mirror");
|
||||
registry(false);
|
||||
sweep();
|
||||
Require(MirrorCounters().heapMirrors == before.heapMirrors && MirrorCounters().heapBytes == before.heapBytes, "the sparse heap mirror over adjacent ranges outlived its ranges");
|
||||
#ifdef _WIN32
|
||||
GuestArena::GuestArenaReset_nid_postfix(block, reserved);
|
||||
GuestArena::GuestArenaRelease_nid_postfix(block, reserved);
|
||||
#else
|
||||
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(guest, reserved);
|
||||
munmap(mapping, reserved + page);
|
||||
#endif
|
||||
}
|
||||
|
||||
void joinedHeapMirrorPartialImportTests(const Context& context, const BdaTestAccess& access) {
|
||||
constexpr std::size_t half = 1u << 20u;
|
||||
constexpr std::size_t committed = 2 * half;
|
||||
constexpr std::size_t reserved = 4 * half;
|
||||
constexpr std::size_t page = 65536;
|
||||
#ifdef _WIN32
|
||||
if (!GuestArena::GuestArenaAvailable_nid_postfix() || !GuestArena::GuestArenaWriteWatched_nid_postfix()) {
|
||||
std::cout << "guest arena unavailable or not write-watched: joined heap mirrors under a partial import not tested\n";
|
||||
return;
|
||||
}
|
||||
void* block = GuestArena::GuestArenaAllocate_nid_postfix(reserved, half);
|
||||
auto* guest = static_cast<std::uint8_t*>(block);
|
||||
GuestArena::GuestArenaCommit_nid_postfix(guest, committed, PAGE_READWRITE, page);
|
||||
#else
|
||||
if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) {
|
||||
std::cout << "write watch unavailable: joined heap mirrors under a partial import not tested\n";
|
||||
return;
|
||||
}
|
||||
void* mapping = mmap(nullptr, reserved + half, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
Require(mapping != MAP_FAILED, "cannot reserve the joined heap");
|
||||
auto* guest = reinterpret_cast<std::uint8_t*>((reinterpret_cast<std::uintptr_t>(mapping) + half - 1) & ~std::uintptr_t{half - 1});
|
||||
Require(mprotect(guest, committed, PROT_READ | PROT_WRITE) == 0, "cannot commit the joined heap ranges");
|
||||
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(guest, reserved);
|
||||
void* block = guest;
|
||||
#endif
|
||||
std::memset(guest, 0x3c, committed);
|
||||
const auto base = reinterpret_cast<std::uintptr_t>(block);
|
||||
const auto registry = [&](bool add) {
|
||||
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
|
||||
if (add) {
|
||||
GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, guest, half, true, true, true);
|
||||
GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, guest + half, half, true, true, true);
|
||||
} else {
|
||||
GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, guest);
|
||||
GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, guest + half);
|
||||
}
|
||||
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
|
||||
};
|
||||
const auto sweep = [&](const Context& with) {
|
||||
GuestBufferMemory memory(with);
|
||||
memory.AcquireRegistered();
|
||||
memory.Upload(true);
|
||||
memory.WriteBack();
|
||||
};
|
||||
const auto bind = [&] {
|
||||
GuestBufferMemory memory(context);
|
||||
memory.AddWritable(base, reserved);
|
||||
memory.Upload(false);
|
||||
std::uint32_t adjustment = 0;
|
||||
const auto view = memory.Descriptor(base, reserved, adjustment);
|
||||
Require(adjustment == 0 && access.bytes(view.buffer)[view.offset + half + 32] == std::byte{0x3c}, "a heap bound across two committed ranges did not read its second range");
|
||||
memory.WriteBack();
|
||||
return view;
|
||||
};
|
||||
sweep(context);
|
||||
registry(true);
|
||||
const auto before = MirrorCounters();
|
||||
const auto first = bind();
|
||||
const auto made = MirrorCounters();
|
||||
Require(made.heapMirrors == before.heapMirrors + 1 && made.sparseBinds == before.sparseBinds + 1, "a heap bound across two committed ranges was not mirrored over both");
|
||||
auto importing = context;
|
||||
importing.hostImportAlignment = page;
|
||||
SetHostImportWindow(half);
|
||||
SetHostImportRangeWindow(half);
|
||||
SetHostImportBudget(HostImportLiveBytes() + half);
|
||||
{
|
||||
GuestBufferMemory memory(importing);
|
||||
memory.AcquireRegistered();
|
||||
Require(ExistingHostImportFor(importing, base, half) != nullptr && ExistingHostImportFor(importing, base + half, half) == nullptr, "the joined heap partial-import fixture did not import exactly its first range");
|
||||
Require(HeapMirrorsWithin(base, base + half).mirrors == 1, "an address build that imported one range of a joined heap mirror released the mirror");
|
||||
memory.Upload(true);
|
||||
memory.WriteBack();
|
||||
}
|
||||
const auto partial = MirrorCounters();
|
||||
const auto again = bind();
|
||||
Require(again.buffer == first.buffer && MirrorCounters().rebuilds == partial.rebuilds, "a joined heap mirror was rebuilt after an import of one of its ranges");
|
||||
{
|
||||
GuestBufferMemory memory(importing);
|
||||
memory.AcquireRegistered();
|
||||
memory.AddWritable(base + half, 32);
|
||||
memory.Upload(true);
|
||||
if (ExistingHostImportFor(importing, base + half, 32) != nullptr) guest[half] = 0x71;
|
||||
else {
|
||||
std::uint32_t adjustment = 0;
|
||||
const auto view = memory.Descriptor(base + half, 32, adjustment);
|
||||
access.bytes(view.buffer)[view.offset + adjustment] = std::byte{0x71};
|
||||
}
|
||||
guest[half + 8] = 0x52;
|
||||
memory.WriteBack();
|
||||
}
|
||||
Require(guest[half] == 0x71 && guest[half + 8] == 0x52, "a joined heap address build lost GPU or genuine CPU stores");
|
||||
const auto refreshed = bind();
|
||||
Require(refreshed.buffer == first.buffer && access.bytes(refreshed.buffer)[refreshed.offset + half] == std::byte{0x71} && access.bytes(refreshed.buffer)[refreshed.offset + half + 8] == std::byte{0x52}, "the retained joined mirror did not refresh stores from an address build");
|
||||
SetHostImportBudget(HostImportLiveBytes() + committed);
|
||||
GuestAllocations::GuestAllocationsEnd_nid_postfix(GuestAllocations::GuestAllocationsBegin_nid_postfix());
|
||||
sweep(importing);
|
||||
Require(ExistingHostImportFor(importing, base, half) != nullptr && ExistingHostImportFor(importing, base + half, half) != nullptr, "the joined heap full-import fixture did not import both ranges");
|
||||
Require(HeapMirrorsWithin(base, base + committed).mirrors == 0, "imports covering every joined heap range left a heap mirror allocated");
|
||||
SetHostImportBudget(0);
|
||||
SetHostImportWindow(0);
|
||||
SetHostImportRangeWindow(std::uint64_t{64} << 20u);
|
||||
ClearHostImports(context.device);
|
||||
registry(false);
|
||||
sweep(context);
|
||||
#ifdef _WIN32
|
||||
GuestArena::GuestArenaReset_nid_postfix(block, reserved);
|
||||
GuestArena::GuestArenaRelease_nid_postfix(block, reserved);
|
||||
#else
|
||||
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(guest, reserved);
|
||||
munmap(mapping, reserved + half);
|
||||
#endif
|
||||
}
|
||||
|
||||
void sparseHeapMirrorTests(const Context& context, const BdaTestAccess& access) {
|
||||
#ifdef _WIN32
|
||||
if (!GuestArena::GuestArenaAvailable_nid_postfix() || !GuestArena::GuestArenaWriteWatched_nid_postfix()) {
|
||||
@@ -3974,6 +4226,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) {
|
||||
heapMirrorTests(context, access);
|
||||
partialHeapImportBudgetTests(context, access);
|
||||
importedHeapMirrorTests(context, access);
|
||||
retiredImportWatchTests(context);
|
||||
importWatchTests();
|
||||
importedFreshTests(context);
|
||||
gpuMappingTests(context);
|
||||
@@ -3982,6 +4235,8 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) {
|
||||
spaceCommittedPagesTests(context, access);
|
||||
hostWriteWatchExclusionTests();
|
||||
sparseHeapMirrorTests(context, access);
|
||||
sparseJoinedHeapMirrorTests(context, access);
|
||||
joinedHeapMirrorPartialImportTests(context, access);
|
||||
Require(AddressCopyOverflow({{0x1000, 0x3000, 0x2000, "uncommitted pages"}}, 0x2000).empty(), "copies within the limit were refused");
|
||||
const auto copies = AddressCopyOverflow({{0x1000, 0x2000, 0x1000, "not mirrored"}, {0x10000, 0x30000, 0x18000, "uncommitted pages"}}, 0x2000);
|
||||
Require(!copies.empty() && copies.find("0x10000+0x20000 (0.1 MiB committed, uncommitted pages)") < copies.find("0x1000+0x1000"), "the copy limit does not name the largest copy first");
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
#include "prx/libSceAgcDriver/Graphics/include/DeferredDestruction.hpp"
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace AgcDriver::Graphics;
|
||||
|
||||
namespace {
|
||||
|
||||
int failures = 0;
|
||||
|
||||
void Expect(bool condition, const std::string& what) {
|
||||
if (condition) return;
|
||||
std::fprintf(stderr, "FAIL: %s\n", what.c_str());
|
||||
++failures;
|
||||
}
|
||||
|
||||
std::vector<std::string> events;
|
||||
VkResult idleResult = VK_SUCCESS;
|
||||
|
||||
template<typename THandle>
|
||||
THandle Handle(std::uintptr_t value) {
|
||||
return reinterpret_cast<THandle>(value);
|
||||
}
|
||||
|
||||
std::string Name(const char* kind, const void* handle) {
|
||||
return std::string(kind) + std::to_string(reinterpret_cast<std::uintptr_t>(handle));
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL mockDestroyBuffer(VkDevice device, VkBuffer buffer, const VkAllocationCallbacks*) {
|
||||
events.push_back(Name("buffer ", reinterpret_cast<const void*>(buffer)) + Name(" on ", device));
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL mockFreeMemory(VkDevice device, VkDeviceMemory memory, const VkAllocationCallbacks*) {
|
||||
events.push_back(Name("memory ", reinterpret_cast<const void*>(memory)) + Name(" on ", device));
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL mockQueueWaitIdle(VkQueue queue) {
|
||||
events.push_back(Name("idle ", queue));
|
||||
return idleResult;
|
||||
}
|
||||
|
||||
VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL mockProc(VkDevice, const char* name) {
|
||||
if (std::strcmp(name, "vkDestroyBuffer") == 0) return reinterpret_cast<PFN_vkVoidFunction>(&mockDestroyBuffer);
|
||||
if (std::strcmp(name, "vkFreeMemory") == 0) return reinterpret_cast<PFN_vkVoidFunction>(&mockFreeMemory);
|
||||
if (std::strcmp(name, "vkQueueWaitIdle") == 0) return reinterpret_cast<PFN_vkVoidFunction>(&mockQueueWaitIdle);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Context ContextFor(VkDevice device, PFN_vkGetDeviceProcAddr proc) {
|
||||
Context context{};
|
||||
context.device = device;
|
||||
context.queue = Handle<VkQueue>(0x900);
|
||||
context.deviceProc = proc;
|
||||
return context;
|
||||
}
|
||||
|
||||
void DeferredUntilTheQueueIsIdle() {
|
||||
events.clear();
|
||||
const auto device = Handle<VkDevice>(0x100);
|
||||
const auto proc = DeferDestructionToIdle(device, mockProc);
|
||||
const auto context = ContextFor(device, proc);
|
||||
Expect(context.Function<PFN_vkQueueWaitIdle>("vkQueueWaitIdle") == &mockQueueWaitIdle, "a function other than vkDestroyBuffer and vkFreeMemory was not passed through");
|
||||
context.Function<PFN_vkFreeMemory>("vkFreeMemory")(device, Handle<VkDeviceMemory>(0x21), nullptr);
|
||||
context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer")(device, Handle<VkBuffer>(0x11), nullptr);
|
||||
context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer")(device, VK_NULL_HANDLE, nullptr);
|
||||
Expect(events.empty() && DeferredObjects(device) == 2, "a destroy or free was not deferred, or a null handle was queued");
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
DestroyDeferredAtIdle(context, start);
|
||||
Expect(events.empty() && DeferredObjects(device) == 2, "deferred objects younger than the drain age were destroyed, or the queue was waited for");
|
||||
DestroyDeferredAtIdle(context, start + std::chrono::seconds(1));
|
||||
const std::vector<std::string> drained{"idle 2304", "buffer 17 on 256", "memory 33 on 256"};
|
||||
Expect(events == drained && DeferredObjects(device) == 0, "old deferred objects were not destroyed after a queue wait, buffers before memory");
|
||||
events.clear();
|
||||
for (std::uintptr_t index = 0; index < 512; ++index) context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer")(device, Handle<VkBuffer>(0x1000 + index), nullptr);
|
||||
DestroyDeferredAtIdle(context, std::chrono::steady_clock::now());
|
||||
Expect(events.size() == 513 && events.front() == "idle 2304" && DeferredObjects(device) == 0, "512 young deferred objects were not destroyed after a queue wait");
|
||||
events.clear();
|
||||
idleResult = VK_ERROR_OUT_OF_HOST_MEMORY;
|
||||
context.Function<PFN_vkFreeMemory>("vkFreeMemory")(device, Handle<VkDeviceMemory>(0x22), nullptr);
|
||||
bool threw = false;
|
||||
try {
|
||||
DestroyDeferredAtIdle(context, std::chrono::steady_clock::now() + std::chrono::seconds(1));
|
||||
} catch (const std::exception&) {
|
||||
threw = true;
|
||||
}
|
||||
Expect(threw && DeferredObjects(device) == 1 && events.size() == 1, "a failed queue wait did not throw, or destroyed the deferred objects");
|
||||
idleResult = VK_SUCCESS;
|
||||
events.clear();
|
||||
DestroyDeferredObjects(device);
|
||||
Expect(events == std::vector<std::string>{"memory 34 on 256"} && DeferredObjects(device) == 0, "the deferred objects were not destroyed when the device went, or the queue was waited for");
|
||||
events.clear();
|
||||
context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer")(device, Handle<VkBuffer>(0x12), nullptr);
|
||||
Expect(events == std::vector<std::string>{"buffer 18 on 256"}, "a destroy after the device went was deferred");
|
||||
}
|
||||
|
||||
void OtherDevicesDestroyAtOnce() {
|
||||
events.clear();
|
||||
const auto device = Handle<VkDevice>(0x101);
|
||||
const auto proc = DeferDestructionToIdle(device, mockProc);
|
||||
const auto other = Handle<VkDevice>(0x102);
|
||||
reinterpret_cast<PFN_vkFreeMemory>(proc(other, "vkFreeMemory"))(other, Handle<VkDeviceMemory>(0x23), nullptr);
|
||||
Expect(events == std::vector<std::string>{"memory 35 on 258"} && DeferredObjects(other) == 0, "a free on a device without deferred destruction was deferred");
|
||||
bool threw = false;
|
||||
try {
|
||||
DeferDestructionToIdle(device, mockProc);
|
||||
} catch (const std::exception&) {
|
||||
threw = true;
|
||||
}
|
||||
Expect(threw, "deferred destruction was set up twice for one device");
|
||||
DestroyDeferredObjects(device);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
DeferredUntilTheQueueIsIdle();
|
||||
OtherDevicesDestroyAtOnce();
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "FAIL: %s\n", error.what());
|
||||
return 1;
|
||||
}
|
||||
if (failures != 0) return 1;
|
||||
std::puts("deferred destruction tests passed");
|
||||
return 0;
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
@@ -100,14 +101,18 @@ std::vector<std::byte> pattern(std::uint8_t seed) {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
void copyFrame(std::uint32_t* words, const Layout& layout, std::byte* target) {
|
||||
CommandBuffer buffer{words, words + CommandBytes / sizeof(std::uint32_t), words, words + CommandBytes / sizeof(std::uint32_t), nullptr, nullptr, 0};
|
||||
sceAgcDcbDmaData(&buffer, 0, DmaMemory, 0, at(target), DmaMemory, 0, at(layout.source), DataBytes, 0, 1, 0);
|
||||
sceAgcDcbWriteData(&buffer, GraphicsMemory, 0, at(layout.label), &Done, 1, 0, 1);
|
||||
void copy(std::uint32_t* words, std::size_t commandBytes, const std::byte* source, std::byte* target, std::uint32_t* label) {
|
||||
CommandBuffer buffer{words, words + commandBytes / sizeof(std::uint32_t), words, words + commandBytes / sizeof(std::uint32_t), nullptr, nullptr, 0};
|
||||
sceAgcDcbDmaData(&buffer, 0, DmaMemory, 0, at(target), DmaMemory, 0, at(source), DataBytes, 0, 1, 0);
|
||||
sceAgcDcbWriteData(&buffer, GraphicsMemory, 0, at(label), &Done, 1, 0, 1);
|
||||
Packet packet{words, static_cast<std::uint32_t>(buffer.cursor_up - words), 0, {}};
|
||||
check(sceAgcDriverSubmitDcb(&packet) == 0, "submission failed");
|
||||
}
|
||||
|
||||
void copyFrame(std::uint32_t* words, const Layout& layout, std::byte* target) {
|
||||
copy(words, CommandBytes, layout.source, target, layout.label);
|
||||
}
|
||||
|
||||
void flip() {
|
||||
AgcDriverSubmitFlip_nid_postfix(AgcDriver::FlipInfo{Handle, -1, 1, 0});
|
||||
}
|
||||
@@ -187,15 +192,75 @@ void testCaptureAndReplay(const std::filesystem::path& tool) {
|
||||
check(readFile(secondDump) == second, "the replay did not reproduce the second copy");
|
||||
}
|
||||
|
||||
void testConcurrentCopies(const std::filesystem::path& tool) {
|
||||
constexpr std::size_t Copies = 64;
|
||||
constexpr std::size_t Frames = 8;
|
||||
constexpr std::size_t CopyCommandBytes = 1024;
|
||||
const char* directory = std::getenv("APS5_FRAME_CAPTURE");
|
||||
check(directory != nullptr && *directory != '\0', "APS5_FRAME_CAPTURE is not set");
|
||||
const std::filesystem::path capture(directory);
|
||||
const auto layout = allocate();
|
||||
const auto other = allocate();
|
||||
auto* const otherSource = other.base + 128 * 1024;
|
||||
auto* const targets = otherSource + DataBytes;
|
||||
auto* const otherLabel = reinterpret_cast<std::uint32_t*>(other.base + 512 * 1024);
|
||||
const auto copied = pattern(57);
|
||||
std::memcpy(otherSource, copied.data(), copied.size());
|
||||
auto output = std::make_shared<Output>();
|
||||
AgcDriverRegisterVideoOutput_nid_postfix(Handle, output);
|
||||
|
||||
flip();
|
||||
std::thread copier([&] {
|
||||
for (std::size_t index = 0; index < Copies; ++index) {
|
||||
copy(reinterpret_cast<std::uint32_t*>(other.base + index * CopyCommandBytes), CopyCommandBytes, otherSource, targets + index * DataBytes, otherLabel);
|
||||
}
|
||||
});
|
||||
for (std::size_t frame = 0; frame + 1 < Frames; ++frame) {
|
||||
const auto bytes = pattern(static_cast<std::uint8_t>(frame));
|
||||
std::memcpy(layout.source, bytes.data(), bytes.size());
|
||||
copyFrame(layout.firstCommands, layout, layout.firstTarget);
|
||||
flip();
|
||||
}
|
||||
copier.join();
|
||||
flip();
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
|
||||
const FrameCaptureReader reader(capture);
|
||||
check(!reader.Truncated(), "the capture is truncated");
|
||||
std::uint64_t base = 0;
|
||||
std::size_t submits = 0, memory = 0;
|
||||
for (const auto& call : reader.Calls()) {
|
||||
if (call.kind == FrameCallKind::FrameStart) base = call.completed;
|
||||
if (call.kind == FrameCallKind::Memory) {
|
||||
++memory;
|
||||
check(call.completed >= base + submits, "a memory delta was read with " + std::to_string(base + submits - call.completed) + " of the " + std::to_string(submits) + " submissions recorded before it unfinished");
|
||||
}
|
||||
submits += call.kind == FrameCallKind::Submit;
|
||||
}
|
||||
check(submits == Copies + Frames - 1, "the capture holds " + std::to_string(submits) + " submits, not " + std::to_string(Copies + Frames - 1));
|
||||
check(memory != 0, "the capture holds no memory deltas");
|
||||
|
||||
const auto dump = capture / "targets.bin";
|
||||
const auto command = "\"\"" + tool.string() + "\" \"" + capture.string() + "\" --strict --dump " + std::to_string(at(targets)) + ":" + std::to_string(Copies * DataBytes) + ":\"" + dump.string() + "\"\"";
|
||||
const auto code = std::system(command.c_str());
|
||||
check(code == 0, "agc_frame_replay exited with " + std::to_string(code));
|
||||
const auto replayed = readFile(dump);
|
||||
check(replayed.size() == Copies * DataBytes, "the replay dumped " + std::to_string(replayed.size()) + " bytes of the copy targets");
|
||||
for (std::size_t index = 0; index < Copies; ++index) {
|
||||
check(std::memcmp(replayed.data() + index * DataBytes, copied.data(), DataBytes) == 0, "the replay did not reproduce copy " + std::to_string(index) + " of the second thread");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc != 2) {
|
||||
std::fprintf(stderr, "usage: agc_driver_frame_replay_tests <agc_frame_replay>\n");
|
||||
if (argc != 2 && !(argc == 3 && std::string(argv[2]) == "concurrent")) {
|
||||
std::fprintf(stderr, "usage: agc_driver_frame_replay_tests <agc_frame_replay> [concurrent]\n");
|
||||
return 2;
|
||||
}
|
||||
try {
|
||||
testCaptureAndReplay(argv[1]);
|
||||
if (argc == 3) testConcurrentCopies(argv[1]);
|
||||
else testCaptureAndReplay(argv[1]);
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "FAILED: %s\n", error.what());
|
||||
return 1;
|
||||
|
||||
@@ -4674,6 +4674,72 @@ void DepthRemapTests() {
|
||||
Require(floating.depthRemap.mode == 1 && floating.viewport.minDepth == 0 && floating.viewport.maxDepth == 1 && floating.depthClamp, "a written float depth target refused clipped clamp bounds");
|
||||
}
|
||||
|
||||
void DepthBoundsEmulationTests() {
|
||||
auto queue = makeState();
|
||||
queue.context[0x204] = 0;
|
||||
queue.context[0x113] = std::bit_cast<std::uint32_t>(0.5f);
|
||||
queue.context[0x114] = std::bit_cast<std::uint32_t>(0.5f);
|
||||
auto base = AgcDriver::Graphics::DecodeState(queue);
|
||||
base.depth = AgcDriver::Graphics::DepthTarget{0x10000, 0, {64, 64}, VK_FORMAT_D32_SFLOAT_S8_UINT, 0.0f, 0};
|
||||
base.depthTest = base.depthWrite = base.stencilTest = false;
|
||||
base.depthBoundsTest = true;
|
||||
base.depthBias = true;
|
||||
base.depthBiasConstant = 4.0f;
|
||||
const ShaderRecompiler::ShaderPixelStageInfo pixel{};
|
||||
const AgcDriver::Graphics::Context emulating{};
|
||||
const auto emulated = [&](float minBound, float maxBound) {
|
||||
auto state = base;
|
||||
state.minDepthBounds = minBound;
|
||||
state.maxDepthBounds = maxBound;
|
||||
AgcDriver::Graphics::ApplyDepthBoundsEmulation(emulating, state, pixel);
|
||||
Require(!state.depthBoundsTest && state.depthTest && !state.depthWrite && !state.depthBias && state.viewport.x == base.viewport.x && state.viewport.width == base.viewport.width && state.viewport.height == base.viewport.height, "an emulated depth bounds test did not become a read-only depth test over the same coverage");
|
||||
return state;
|
||||
};
|
||||
const auto upper = emulated(0.0f, 0.000125f);
|
||||
Require(upper.depthCompare == VK_COMPARE_OP_GREATER_OR_EQUAL && upper.viewport.minDepth == 0.000125f && upper.viewport.maxDepth == 0.000125f, "an upper depth bound was not tested as the bound at or above the stored depth");
|
||||
const auto lower = emulated(0.000125f, 1.0f);
|
||||
Require(lower.depthCompare == VK_COMPARE_OP_LESS_OR_EQUAL && lower.viewport.minDepth == 0.000125f && lower.viewport.maxDepth == 0.000125f, "a lower depth bound was not tested as the bound at or below the stored depth");
|
||||
const auto whole = emulated(0.0f, 1.0f);
|
||||
Require(whole.depthCompare == VK_COMPARE_OP_GREATER_OR_EQUAL && whole.viewport.minDepth == 1.0f && whole.viewport.maxDepth == 1.0f, "bounds covering [0, 1] were not tested against the upper bound");
|
||||
AgcDriver::Graphics::Context native{};
|
||||
native.depthBounds = true;
|
||||
auto kept = base;
|
||||
AgcDriver::Graphics::ApplyDepthBoundsEmulation(native, kept, pixel);
|
||||
Require(kept.depthBoundsTest && !kept.depthTest && kept.depthBias && kept.viewport.minDepth == base.viewport.minDepth, "a device with the depth bounds test had it emulated");
|
||||
auto absent = base;
|
||||
absent.depth.reset();
|
||||
AgcDriver::Graphics::ApplyDepthBoundsEmulation(emulating, absent, pixel);
|
||||
Require(absent.depthBoundsTest && !absent.depthTest, "a draw without a depth target had its depth bounds emulated");
|
||||
const auto refuses = [&](const char* expected, auto change) {
|
||||
auto state = base;
|
||||
state.minDepthBounds = 0.0f;
|
||||
state.maxDepthBounds = 0.5f;
|
||||
auto context = emulating;
|
||||
auto program = pixel;
|
||||
change(state, context, program);
|
||||
expectFailure([&] { AgcDriver::Graphics::ApplyDepthBoundsEmulation(context, state, program); }, expected);
|
||||
};
|
||||
refuses("both bounds inside", [](auto& state, auto&, auto&) { state.minDepthBounds = 0.25f; });
|
||||
refuses("with depth testing", [](auto& state, auto&, auto&) { state.depthTest = true; });
|
||||
refuses("reads or exports depth", [](auto&, auto&, auto& program) { program.posZ = true; });
|
||||
refuses("reads or exports depth", [](auto&, auto&, auto& program) { program.depthExportEnable = true; });
|
||||
refuses("normalized depth target", [](auto& state, auto&, auto&) { state.depth->format = VK_FORMAT_D16_UNORM; });
|
||||
refuses("stays in [0, 1]", [](auto&, auto& context, auto&) { context.depthRangeUnrestricted = true; });
|
||||
refuses("depth bounds outside [0, 1]", [](auto& state, auto&, auto&) { state.maxDepthBounds = 1.5f; });
|
||||
refuses("writes on a failed test", [](auto& state, auto&, auto&) {
|
||||
state.stencilTest = true;
|
||||
state.stencilFront.writeMask = 0xffu;
|
||||
state.stencilFront.failOp = VK_STENCIL_OP_REPLACE;
|
||||
});
|
||||
auto stencil = base;
|
||||
stencil.stencilTest = true;
|
||||
stencil.stencilFront.writeMask = stencil.stencilBack.writeMask = 0xffu;
|
||||
stencil.stencilFront.passOp = stencil.stencilBack.passOp = VK_STENCIL_OP_REPLACE;
|
||||
stencil.maxDepthBounds = 0.5f;
|
||||
AgcDriver::Graphics::ApplyDepthBoundsEmulation(emulating, stencil, pixel);
|
||||
Require(stencil.depthTest && stencil.stencilTest, "a stencil test that writes only on a pass refused the depth bounds emulation");
|
||||
}
|
||||
|
||||
void UnusedViewportDepthTests() {
|
||||
auto queue = makeState();
|
||||
queue.context[0x204] = 0;
|
||||
@@ -7099,6 +7165,27 @@ void storageTextureBudgetTests() {
|
||||
memory.memoryHeaps[0].flags = 0;
|
||||
memory.memoryHeaps[2].flags = 0;
|
||||
Require(StorageTextureBudget(memory, &reported, 0, 0) == 2 * GiB, "a device without a device-local heap does not keep 2 GiB of storage images");
|
||||
using AgcDriver::Graphics::StorageTextureCeiling;
|
||||
memory.memoryHeaps[0] = {256 * MiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT};
|
||||
memory.memoryHeaps[2] = {16 * GiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT};
|
||||
reported = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT};
|
||||
Require(StorageTextureCeiling(memory, nullptr) == 4 * GiB, "without VK_EXT_memory_budget the storage image cache ceiling is not a quarter of the 16 GiB heap");
|
||||
reported.heapBudget[2] = 6 * GiB;
|
||||
Require(StorageTextureCeiling(memory, &reported) == 3 * GiB, "the storage image cache ceiling on a 6 GiB budget is not half of the budget, the cap the cache may reach");
|
||||
reported.heapBudget[2] = 40 * GiB;
|
||||
Require(StorageTextureCeiling(memory, &reported) == 8 * GiB, "the storage image cache ceiling on a 40 GiB budget is not half of the 16 GiB heap");
|
||||
reported.heapBudget[2] = 768 * MiB;
|
||||
Require(StorageTextureCeiling(memory, &reported) == 512 * MiB, "the storage image cache ceiling on a 768 MiB budget does not keep the 512 MiB floor");
|
||||
for (std::uint64_t budget = 1 * GiB; budget <= 24 * GiB; budget += 512 * MiB) {
|
||||
reported.heapBudget[2] = budget;
|
||||
const auto ceiling = StorageTextureCeiling(memory, &reported);
|
||||
for (std::uint64_t used = 0; used <= budget; used += 256 * MiB) {
|
||||
reported.heapUsage[2] = used;
|
||||
Require(StorageTextureBudget(memory, &reported, 256 * MiB, 512 * MiB) <= ceiling, "the storage image cache budget exceeds the ceiling idle device buffers are reclaimed up to");
|
||||
}
|
||||
reported.heapUsage[2] = 0;
|
||||
Require(StorageTextureBudget(memory, &reported, 0, 0) == ceiling, "an idle heap does not give the storage image cache its full ceiling");
|
||||
}
|
||||
}
|
||||
|
||||
void sampledBudgetReportTests() {
|
||||
@@ -9148,7 +9235,7 @@ void stencilRefExportTests() {
|
||||
Require(AgcDriver::Graphics::DrawRejection(unused, false).empty(), "the precheck rejected STENCIL_OP_VAL_EXPORT_ENABLE without a stencil test in DB_SHADER_CONTROL " + std::to_string(shaderControl));
|
||||
Require(AgcDriver::Graphics::DecodePixelStageInfo(unused.context, IdentityExports).stencilRefExportEnable == ((shaderControl & 2u) != 0), "STENCIL_OP_VAL_EXPORT_ENABLE without a stencil test exported the stencil reference");
|
||||
const bool replaces = (shaderControl & 6u) == 4u;
|
||||
for (const auto [ops, writeMask, compare, accepted] : {std::tuple{0x000u, 0xffu, 7u, true}, std::tuple{0x737u, 0xffu, 7u, true}, std::tuple{0x110u, 0xffu, 7u, true}, std::tuple{0x050u, 0x00u, 7u, true}, std::tuple{0x040u, 0xffu, 7u, replaces}, std::tuple{0x40040u, 0xffu, 7u, replaces}, std::tuple{0x404u, 0x0fu, 0u, replaces}, std::tuple{0x040u, 0xffu, 2u, false}, std::tuple{0x340u, 0xffu, 7u, false}, std::tuple{0x042u, 0xffu, 7u, false}, std::tuple{0x050u, 0xffu, 7u, false}, std::tuple{0x600u, 0xffu, 7u, false}, std::tuple{0x008u, 0xffu, 7u, false}, std::tuple{0x900u, 0xffu, 7u, false}, std::tuple{0x5000u, 0xffu, 7u, false}}) {
|
||||
for (const auto [ops, writeMask, compare, accepted] : {std::tuple{0x000u, 0xffu, 7u, true}, std::tuple{0x737u, 0xffu, 7u, true}, std::tuple{0x110u, 0xffu, 7u, true}, std::tuple{0x050u, 0x00u, 7u, true}, std::tuple{0x040u, 0xffu, 7u, replaces}, std::tuple{0x40040u, 0xffu, 7u, replaces}, std::tuple{0x404u, 0x0fu, 0u, replaces}, std::tuple{0x040u, 0xffu, 2u, false}, std::tuple{0x340u, 0xffu, 7u, false}, std::tuple{0x042u, 0xffu, 7u, false}, std::tuple{0x050u, 0xffu, 7u, false}, std::tuple{0x600u, 0xffu, 7u, false}, std::tuple{0x008u, 0xffu, 7u, false}, std::tuple{0x900u, 0xffu, 7u, false}, std::tuple{0x5000u, 0xffu, 7u, false}, std::tuple{0x0c0u, 0xffu, 7u, false}, std::tuple{0x00bu, 0xffu, 7u, false}}) {
|
||||
auto stenciled = stencilRefExportQueue(shaderControl, zFormat);
|
||||
for (const auto [offset, word] : {std::pair{0x010u, 0x80000180u}, std::pair{0x011u, 0x20000181u}, std::pair{0x012u, 0x00001000u}, std::pair{0x013u, 0x00002000u}, std::pair{0x015u, 0x00002000u}, std::pair{0x007u, 0x003f003fu}, std::pair{0x000u, 0u}, std::pair{0x002u, 0u}, std::pair{0x00au, 0u}, std::pair{0x00bu, 0x3f800000u}, std::pair{0x200u, 0x007007b3u}}) stenciled.context[offset] = word;
|
||||
stenciled.context[0x10b] = ops;
|
||||
@@ -9458,6 +9545,99 @@ void stencilReferenceUnusedTests() {
|
||||
std::cout << "Unused stencil reference sample equivalence passed: " << accepted << " accepted states, " << countermodels << " dependent countermodels\n";
|
||||
}
|
||||
|
||||
void stencilLogicOperationTests() {
|
||||
const auto queueFor = [](std::uint32_t ops, std::uint32_t front, std::uint32_t back, std::uint32_t depthControl) {
|
||||
auto queue = makeState();
|
||||
for (const auto [offset, word] : {std::pair{0x000u, 0u}, std::pair{0x002u, 0u}, std::pair{0x007u, (1u << 16u) | 3u}, std::pair{0x00au, 0u}, std::pair{0x00bu, 0u}, std::pair{0x010u, 0x22900983u}, std::pair{0x011u, 0x20000181u}, std::pair{0x012u, 0x100u}, std::pair{0x014u, 0x100u}, std::pair{0x013u, 0x200u}, std::pair{0x015u, 0x200u}, std::pair{0x005u, 0x300u}, std::pair{0x01eu, 0x1u}}) queue.context[offset] = word;
|
||||
queue.context[0x10b] = ops;
|
||||
queue.context[0x10c] = front;
|
||||
queue.context[0x10d] = back;
|
||||
queue.context[0x200] = depthControl;
|
||||
return queue;
|
||||
};
|
||||
const auto refMaskOf = [](std::uint32_t test, std::uint32_t writeMask, std::uint32_t value) { return test | 0xff00u | (writeMask << 16u) | (value << 24u); };
|
||||
const auto apply = [](std::uint32_t op, std::uint32_t stencil, std::uint32_t value) {
|
||||
switch (op) {
|
||||
case 1: return 0u;
|
||||
case 10: return stencil & value;
|
||||
case 11: return stencil | value;
|
||||
case 12: return stencil ^ value;
|
||||
case 15: return ~(stencil ^ value);
|
||||
default: return stencil;
|
||||
}
|
||||
};
|
||||
const auto matches = [&](const AgcDriver::Graphics::State& state, const VkStencilOpState& face, std::uint32_t ops, std::uint32_t compare, std::uint32_t refMask, const std::string& name) {
|
||||
const auto test = refMask & 0xffu;
|
||||
const auto compareMask = (refMask >> 8u) & 0xffu;
|
||||
const auto writeMask = (refMask >> 16u) & 0xffu;
|
||||
const auto value = refMask >> 24u;
|
||||
for (std::uint32_t stencil = 0; stencil < 256u; ++stencil) {
|
||||
for (const bool depthPass : {false, true}) {
|
||||
const bool stencilPass = stencilComparison(static_cast<VkCompareOp>(compare), test & compareMask, stencil & compareMask);
|
||||
const auto op = (ops >> (!stencilPass ? 0u : !depthPass ? 8u : 4u)) & 0xfu;
|
||||
const auto expected = ((stencil & ~writeMask) | (apply(op, stencil, value) & writeMask)) & 0xffu;
|
||||
const auto [actual, passed] = stencilReferenceOutcome(state, face, face.reference, stencil, depthPass, true);
|
||||
Require(actual == expected && passed == (stencilPass && depthPass), name + ": stencil " + std::to_string(stencil) + (depthPass ? " passing" : " failing") + " the depth test became " + std::to_string(actual) + ", expected " + std::to_string(expected));
|
||||
}
|
||||
}
|
||||
};
|
||||
std::size_t accepted = 0;
|
||||
for (const auto op : {10u, 11u, 12u, 15u}) {
|
||||
for (const auto placement : {0x010u, 0x001u, 0x100u, 0x111u}) {
|
||||
for (const auto compare : {7u, 0u, 2u, 3u}) {
|
||||
for (const auto [test, writeMask, value] : {std::tuple{0x00u, 0xffu, 0x5au}, std::tuple{0xa5u, 0xffu, 0x21u}, std::tuple{0xa5u, 0x3cu, 0xffu}, std::tuple{0xffu, 0xf0u, 0x0fu}, std::tuple{0x5au, 0x81u, 0x00u}}) {
|
||||
const auto ops = op * placement;
|
||||
const auto refMask = refMaskOf(test, writeMask, value);
|
||||
const auto queue = queueFor(ops, refMask, refMask, 0x13u | (compare << 8u));
|
||||
const auto name = "stencil operation " + std::to_string(op) + " in DB_STENCIL_CONTROL " + std::to_string(ops) + " with compare " + std::to_string(compare) + " and DB_STENCILREFMASK " + std::to_string(refMask);
|
||||
const auto bits = (op == 11u || op == 12u ? value : ~value) & writeMask;
|
||||
if (op == 11u && compare != 0u && compare != 7u && (~test & bits) != 0u) {
|
||||
expectFailure([&] { static_cast<void>(AgcDriver::Graphics::DecodeState(queue)); }, "stencil OR of");
|
||||
continue;
|
||||
}
|
||||
const auto state = AgcDriver::Graphics::DecodeState(queue);
|
||||
Require(state.stencilTest && state.depthTest && (state.stencilFront.writeMask & 0xffu) == (bits != 0u ? bits : writeMask), "the write mask is not narrowed to the bits changed by " + name);
|
||||
matches(state, state.stencilFront, ops, compare, refMask, name);
|
||||
++accepted;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const auto [ops, refMask, name] : {std::tuple{0x0c1u, refMaskOf(0x80u, 0xffu, 0xffu), "XOR of every written bit beside ZERO"}, std::tuple{0x0cbu, refMaskOf(0x8fu, 0xffu, 0x0fu), "OR and XOR of the same bits"}, std::tuple{0x0fau, refMaskOf(0x80u, 0xffu, 0xf0u), "AND and XNOR of the same bits"}, std::tuple{0xb0bu, refMaskOf(0x30u, 0x3cu, 0xf0u), "OR on stencil fail and depth fail"}}) {
|
||||
const auto queue = queueFor(ops, refMask, refMask, 0x13u | (3u << 8u));
|
||||
const auto state = AgcDriver::Graphics::DecodeState(queue);
|
||||
matches(state, state.stencilFront, ops, 3u, refMask, name);
|
||||
++accepted;
|
||||
}
|
||||
{
|
||||
const auto front = refMaskOf(0x11u, 0xffu, 0x0fu);
|
||||
const auto back = refMaskOf(0x22u, 0x3cu, 0x30u);
|
||||
const auto queue = queueFor(0x0c0u | (0x0a0u << 12u), front, back, 0x93u | (7u << 8u) | (5u << 20u));
|
||||
const auto state = AgcDriver::Graphics::DecodeState(queue);
|
||||
Require(state.stencilFront.passOp == VK_STENCIL_OP_INVERT && state.stencilFront.writeMask == 0x0fu && state.stencilBack.passOp == VK_STENCIL_OP_ZERO && state.stencilBack.writeMask == 0x0cu, "front XOR and back AND did not keep their own write masks");
|
||||
matches(state, state.stencilFront, 0x0c0u, 7u, front, "front XOR beside a back AND");
|
||||
matches(state, state.stencilBack, 0x0a0u, 5u, back, "back AND beside a front XOR");
|
||||
accepted += 2;
|
||||
}
|
||||
for (const auto [ops, refMask, reason] : {std::tuple{0x0d0u, refMaskOf(0u, 0xffu, 0x0fu), "stencil operation 13 is unsupported"}, std::tuple{0x0e0u, refMaskOf(0u, 0xffu, 0x0fu), "stencil operation 14 is unsupported"}, std::tuple{0x0cau, refMaskOf(0u, 0xffu, 0x0fu), "stencil logic operations on write masks"}, std::tuple{0x0fcu, refMaskOf(0u, 0xffu, 0x0fu), "stencil logic operations on write masks"},std::tuple{0x0c1u, refMaskOf(0u, 0xffu, 0x0fu), "combine a logic operation"}, std::tuple{0x5c0u, refMaskOf(0u, 0xffu, 0x01u), "combine a logic operation"}}) {
|
||||
const auto queue = queueFor(ops, refMask, refMask, 0x13u | (7u << 8u));
|
||||
expectFailure([&] { static_cast<void>(AgcDriver::Graphics::DecodeState(queue)); }, reason);
|
||||
}
|
||||
for (const auto ops : {0x0c0u, 0x0b0u}) {
|
||||
auto exported = stencilRefExportQueue(0x12u, 9u);
|
||||
for (const auto [offset, word] : {std::pair{0x010u, 0x80000180u}, std::pair{0x011u, 0x20000181u}, std::pair{0x012u, 0x00001000u}, std::pair{0x013u, 0x00002000u}, std::pair{0x015u, 0x00002000u}, std::pair{0x007u, 0x003f003fu}, std::pair{0x000u, 0u}, std::pair{0x002u, 0u}, std::pair{0x00au, 0u}, std::pair{0x00bu, 0x3f800000u}, std::pair{0x200u, 0x007007b3u}}) exported.context[offset] = word;
|
||||
exported.context[0x10b] = ops;
|
||||
exported.context[0x10c] = exported.context[0x10d] = refMaskOf(0u, 0xffu, 0x0fu);
|
||||
if (ops == 0x0c0u) {
|
||||
const auto state = AgcDriver::Graphics::DecodeState(exported);
|
||||
Require(state.stencilFront.passOp == VK_STENCIL_OP_INVERT && state.stencilFront.writeMask == 0x0fu, "a stencil test value export changed a stencil XOR");
|
||||
} else {
|
||||
expectFailure([&] { static_cast<void>(AgcDriver::Graphics::DecodeState(exported)); }, "stencil OR is unsupported with a pixel shader that exports the stencil test value");
|
||||
}
|
||||
}
|
||||
std::cout << "Stencil logic operations matched AND, OR, XOR and XNOR under the write mask for " << accepted << " register states\n";
|
||||
}
|
||||
|
||||
std::set<std::string> bufferViews(std::span<const std::uint32_t> words) {
|
||||
std::set<std::string> views;
|
||||
for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) {
|
||||
@@ -11452,6 +11632,117 @@ void pipelineStoreLazyBufferPoolTests() {
|
||||
Require(mock.live == 0, "lazy buffer pool pipeline tests leaked Vulkan objects");
|
||||
}
|
||||
|
||||
// The pipeline store's bound without a GPU: 1536 configurations cycled through the default 1024
|
||||
// entries keep coming back after their eviction, so the bound doubles and a later pass finds every
|
||||
// pipeline resident; a bound set with APS5_PIPELINE_STORE_ENTRIES never moves.
|
||||
void pipelineStoreGrowthTests(bool fixed) {
|
||||
using namespace AgcDriver::Graphics;
|
||||
mock = MockVulkan{};
|
||||
auto context = mockContext();
|
||||
context.device = makeHandle<VkDevice>();
|
||||
context.graphicsPipelineLibrary = true;
|
||||
context.pipelineLinkTimeOptimization = false;
|
||||
context.limits.framebufferNoAttachmentsSampleCounts = VK_SAMPLE_COUNT_1_BIT;
|
||||
ShaderRecompiler::RecompileResult vertex, fragment;
|
||||
vertex.spirv = makeModule({});
|
||||
fragment.spirv = makeModule({.fragment = true});
|
||||
vertex.variantId = 301;
|
||||
fragment.variantId = 302;
|
||||
const std::array<CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, PipelinePushSlotBytes}}};
|
||||
std::array<std::uint32_t, 1> mainKey{41}, fragmentKey{42};
|
||||
const PipelineSetLayouts resources{makeHandle<VkDescriptorSetLayout>(), makeHandle<VkDescriptorSetLayout>(), true, mainKey, fragmentKey};
|
||||
constexpr std::uint32_t configurations = 1536;
|
||||
const auto pass = [&](std::uint32_t count) {
|
||||
std::uint32_t built = 0;
|
||||
for (std::uint32_t configuration = 0; configuration < count; ++configuration) {
|
||||
VertexInputLayout input;
|
||||
input.bindings.push_back({0, 4u * (configuration + 1u), VK_VERTEX_INPUT_RATE_VERTEX});
|
||||
bool created = false;
|
||||
static_cast<void>(CachedPipeline(context, State{}, input, resources, shaders, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, &created));
|
||||
built += created ? 1u : 0u;
|
||||
}
|
||||
return built;
|
||||
};
|
||||
std::uint32_t lastBuilt = 0;
|
||||
for (int round = 0; round < 6; ++round) lastBuilt = pass(configurations);
|
||||
const auto grown = PipelineStoreTotals();
|
||||
if (fixed) {
|
||||
Require(grown.bound == 1024, "a bound set with APS5_PIPELINE_STORE_ENTRIES changed");
|
||||
Require(lastBuilt == configurations, "a fixed 1024-entry bound kept pipelines of a 1536-configuration cycle");
|
||||
} else {
|
||||
Require(grown.bound == 2048, "the bound did not grow to 2048 while evicted pipelines kept coming back");
|
||||
Require(lastBuilt == 0, "a pass over 1536 configurations still rebuilt pipelines after the bound grew");
|
||||
}
|
||||
ClearCachedPipelines(context.device);
|
||||
Require(mock.live == 0, "pipeline store growth tests leaked Vulkan objects");
|
||||
}
|
||||
|
||||
// A journal longer than the store's starting bound: 1032 recorded pipelines replayed into an empty
|
||||
// store all stay resident (the replay grows the bound instead of dropping what does not fit).
|
||||
void pipelineStoreReplayGrowthTests() {
|
||||
using namespace AgcDriver::Graphics;
|
||||
const auto root = std::filesystem::temp_directory_path() / ("aps5-pipeline-store-replay-" + std::to_string(std::random_device{}()));
|
||||
std::filesystem::create_directories(root);
|
||||
struct Cleanup {
|
||||
std::filesystem::path directory;
|
||||
~Cleanup() {
|
||||
ShaderRecompiler::ShaderDiskCache::Shutdown();
|
||||
std::error_code error;
|
||||
std::filesystem::remove_all(directory, error);
|
||||
}
|
||||
} cleanup{root};
|
||||
#ifdef _WIN32
|
||||
_putenv_s("ANYPS5_NO_SHADER_CACHE", "0");
|
||||
_wputenv_s(L"ANYPS5_SHADER_CACHE_DIR", root.c_str());
|
||||
_putenv_s("APS5_PIPELINE_JOURNAL_THREADS", "1");
|
||||
#else
|
||||
setenv("ANYPS5_NO_SHADER_CACHE", "0", 1);
|
||||
setenv("ANYPS5_SHADER_CACHE_DIR", root.string().c_str(), 1);
|
||||
setenv("APS5_PIPELINE_JOURNAL_THREADS", "1", 1);
|
||||
#endif
|
||||
Require(PipelineJournalEnabled(), "the journal is disabled with a shader cache directory set");
|
||||
mock = MockVulkan{};
|
||||
auto context = mockContext();
|
||||
context.device = makeHandle<VkDevice>();
|
||||
context.graphicsPipelineLibrary = true;
|
||||
context.pipelineLinkTimeOptimization = false;
|
||||
context.limits.framebufferNoAttachmentsSampleCounts = VK_SAMPLE_COUNT_1_BIT;
|
||||
ShaderRecompiler::RecompileResult vertex, fragment;
|
||||
vertex.spirv = makeModule({});
|
||||
fragment.spirv = makeModule({.fragment = true});
|
||||
vertex.variantId = 401;
|
||||
fragment.variantId = 402;
|
||||
const std::array<CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, PipelinePushSlotBytes}}};
|
||||
auto cache = std::make_unique<DescriptorCache>(context);
|
||||
context.descriptorCache = cache.get();
|
||||
const auto layouts = DescribeSetLayouts(context, shaders);
|
||||
const auto resources = layouts.View();
|
||||
constexpr std::uint64_t fingerprint = 0x5eed'0000'0000'0002ull;
|
||||
{
|
||||
auto absent = StartPipelineJournalReplay(context, fingerprint, {});
|
||||
Require(absent != nullptr, "the first replay did not start");
|
||||
absent->Wait();
|
||||
}
|
||||
constexpr std::uint32_t configurations = 1032;
|
||||
for (std::uint32_t configuration = 0; configuration < configurations; ++configuration) {
|
||||
VertexInputLayout input;
|
||||
input.bindings.push_back({0, 4u * (configuration + 1u), VK_VERTEX_INPUT_RATE_VERTEX});
|
||||
static_cast<void>(CachedPipeline(context, State{}, input, resources, shaders));
|
||||
}
|
||||
FlushPipelineJournal();
|
||||
ClearCachedPipelines(context.device);
|
||||
Require(PipelineStoreTotals().cached == 0, "clearing the device left pipelines in the store");
|
||||
auto replay = StartPipelineJournalReplay(context, fingerprint, {});
|
||||
Require(replay != nullptr, "the replay did not start");
|
||||
replay->Wait();
|
||||
const auto totals = PipelineStoreTotals();
|
||||
Require(totals.cached == configurations, "a replay of " + std::to_string(configurations) + " recorded pipelines kept " + std::to_string(totals.cached) + " in the store (bound " + std::to_string(totals.bound) + ")");
|
||||
ClearCachedPipelines(context.device);
|
||||
context.descriptorCache = nullptr;
|
||||
cache.reset();
|
||||
Require(mock.live == 0, "pipeline store replay growth tests leaked Vulkan objects");
|
||||
}
|
||||
|
||||
void pipelineJournalTests(bool longPath = false, bool extended = false) {
|
||||
using namespace AgcDriver::Graphics;
|
||||
using Stage = ShaderRecompiler::ShaderStage;
|
||||
@@ -11883,6 +12174,16 @@ int main(int argc, char** argv) {
|
||||
std::cout << "pipeline library clear waits for active part builds\n";
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--pipeline-store-replay-growth") {
|
||||
pipelineStoreReplayGrowthTests();
|
||||
std::cout << "a replayed journal longer than the store bound stays resident\n";
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--pipeline-store-growth") {
|
||||
pipelineStoreGrowthTests(std::getenv("APS5_PIPELINE_STORE_ENTRIES") != nullptr);
|
||||
std::cout << "pipeline store bound follows the evicted pipelines that come back\n";
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--pipeline-store-lazy-buffer-pool") {
|
||||
pipelineStoreLazyBufferPoolTests();
|
||||
std::cout << "cached pipelines survive lazy buffer pool creation\n";
|
||||
@@ -12152,6 +12453,7 @@ int main(int argc, char** argv) {
|
||||
srgb8TargetTests();
|
||||
DepthClipTests();
|
||||
DepthRemapTests();
|
||||
DepthBoundsEmulationTests();
|
||||
UnusedViewportDepthTests();
|
||||
DepthStencilTests();
|
||||
OneDimensionalColorTests();
|
||||
@@ -12168,6 +12470,7 @@ int main(int argc, char** argv) {
|
||||
orderedPixelShaderTests();
|
||||
stencilRefExportTests();
|
||||
stencilReferenceUnusedTests();
|
||||
stencilLogicOperationTests();
|
||||
aliasedBufferViewTests();
|
||||
LineStripTests();
|
||||
ShaderModuleKeyTests();
|
||||
|
||||
@@ -32,7 +32,7 @@ struct LoadCase {
|
||||
std::uint32_t components = 1;
|
||||
};
|
||||
|
||||
std::vector<std::uint32_t> emitLoad(const LoadCase& test, bool active, std::uint32_t offset, std::uint32_t records, bool robust) {
|
||||
std::vector<std::uint32_t> emitLoad(const LoadCase& test, bool active, std::uint32_t offset, std::uint32_t records, bool robust, bool valid = true) {
|
||||
IrProgram program;
|
||||
program.SetWaveSize(32);
|
||||
program.Resources().stage = IrShaderStage::Compute;
|
||||
@@ -76,7 +76,8 @@ std::vector<std::uint32_t> emitLoad(const LoadCase& test, bool active, std::uint
|
||||
program.Metadata().bindings.pushDataStartDword = 0;
|
||||
program.Metadata().bindings.memoryOffsetCount = 1;
|
||||
auto& size = constant(records);
|
||||
auto& handle = emit(IrOpcode::GetBufferResource, IrType::BufferResource, {&zero, &zero, &size, &zero});
|
||||
auto& base = constant(valid ? 0x100u : 0u);
|
||||
auto& handle = emit(IrOpcode::GetBufferResource, IrType::BufferResource, {&base, &zero, &size, &zero});
|
||||
emit(test.opcode, test.type, {&handle, &zero, &address, &zero, &enabled});
|
||||
} else {
|
||||
if (test.kind == ResourceKind::Gds) program.Metadata().bindings.descriptors.push_back({DescriptorBindingKind::Gds, {0}});
|
||||
@@ -104,8 +105,8 @@ std::vector<std::uint32_t> emitLoad(const LoadCase& test, bool active, std::uint
|
||||
|
||||
class AccessTrace {
|
||||
public:
|
||||
AccessTrace(const std::vector<std::uint32_t>& module, std::uint32_t misalignment = 0, bool valid = true)
|
||||
: words(module), misalignment(misalignment), valid(valid) {
|
||||
AccessTrace(const std::vector<std::uint32_t>& module, std::uint32_t misalignment = 0)
|
||||
: words(module), misalignment(misalignment) {
|
||||
require(words.size() >= 5, "SPIR-V module header is missing");
|
||||
for (std::size_t offset = 5; offset < words.size();) {
|
||||
const auto count = words[offset] >> 16;
|
||||
@@ -206,7 +207,6 @@ private:
|
||||
const auto spec = specializations.at(id);
|
||||
if (spec == PipelineSpecialization::BufferBase) return 0;
|
||||
if (spec == PipelineSpecialization::BufferBase + 1) return 0x30001000;
|
||||
if (spec == PipelineSpecialization::BufferBase + 2) return valid;
|
||||
if (spec == PipelineSpecialization::BufferBase + 3) return misalignment;
|
||||
if (spec == PipelineSpecialization::PushDataOffset) return 0;
|
||||
throw std::runtime_error("unexpected specialization in access fixture");
|
||||
@@ -250,7 +250,6 @@ private:
|
||||
|
||||
const std::vector<std::uint32_t>& words;
|
||||
std::uint32_t misalignment;
|
||||
bool valid;
|
||||
std::uint32_t entryFunction = 0;
|
||||
std::map<std::uint32_t, std::span<const std::uint32_t>> definitions;
|
||||
std::map<std::uint32_t, std::string> names;
|
||||
@@ -281,8 +280,8 @@ std::uint32_t ldsLength(const std::vector<std::uint32_t>& words) {
|
||||
}
|
||||
|
||||
void expectReads(const LoadCase& test, bool robust, bool active, std::uint32_t offset, std::vector<std::uint32_t> expected, std::uint32_t records = 64, std::uint32_t misalignment = 0, bool valid = true) {
|
||||
const auto module = emitLoad(test, active, offset, records, robust);
|
||||
const auto actual = AccessTrace(module, misalignment, valid).Run();
|
||||
const auto module = emitLoad(test, active, offset, records, robust, valid);
|
||||
const auto actual = AccessTrace(module, misalignment).Run();
|
||||
if (actual != expected) {
|
||||
std::string detail = std::string(robust ? "robust" : "plain") + " guest reads:";
|
||||
for (const auto index : actual) detail += " " + std::to_string(index);
|
||||
|
||||
@@ -568,6 +568,64 @@ void residentPresentTests(const Context& context) {
|
||||
blitLetterboxMatchesClear(context);
|
||||
}
|
||||
|
||||
PFN_vkGetDeviceProcAddr realDeviceProc = nullptr;
|
||||
PFN_vkAllocateMemory realAllocateMemory = nullptr;
|
||||
const VkPhysicalDeviceMemoryProperties* refusingMemory = nullptr;
|
||||
std::uint32_t refusedAllocations = 0;
|
||||
std::uint32_t lastAllocatedType = UINT32_MAX;
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL refuseDeviceLocalAllocation(VkDevice device, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks* callbacks, VkDeviceMemory* memory) {
|
||||
const auto heap = refusingMemory->memoryTypes[info->memoryTypeIndex].heapIndex;
|
||||
if ((refusingMemory->memoryHeaps[heap].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0) {
|
||||
++refusedAllocations;
|
||||
return VK_ERROR_OUT_OF_DEVICE_MEMORY;
|
||||
}
|
||||
lastAllocatedType = info->memoryTypeIndex;
|
||||
return realAllocateMemory(device, info, callbacks, memory);
|
||||
}
|
||||
|
||||
VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL refusingDeviceProc(VkDevice device, const char* name) {
|
||||
if (std::strcmp(name, "vkAllocateMemory") == 0) return reinterpret_cast<PFN_vkVoidFunction>(&refuseDeviceLocalAllocation);
|
||||
return realDeviceProc(device, name);
|
||||
}
|
||||
|
||||
void sourceImageOutsideDeviceMemoryTests(const Context& base) {
|
||||
VkImageCreateInfo probeInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
||||
probeInfo.imageType = VK_IMAGE_TYPE_2D;
|
||||
probeInfo.format = VK_FORMAT_B8G8R8A8_UNORM;
|
||||
probeInfo.extent = {64u, 64u, 1u};
|
||||
probeInfo.mipLevels = 1;
|
||||
probeInfo.arrayLayers = 1;
|
||||
probeInfo.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
probeInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
probeInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
|
||||
VkImage probe = VK_NULL_HANDLE;
|
||||
Check(base.Function<PFN_vkCreateImage>("vkCreateImage")(base.device, &probeInfo, nullptr, &probe), "vkCreateImage probe");
|
||||
VkMemoryRequirements requirements{};
|
||||
base.Function<PFN_vkGetImageMemoryRequirements>("vkGetImageMemoryRequirements")(base.device, probe, &requirements);
|
||||
base.Function<PFN_vkDestroyImage>("vkDestroyImage")(base.device, probe, nullptr);
|
||||
if (!base.HostMemoryType(requirements.memoryTypeBits)) {
|
||||
std::printf("presentation source in host memory: skipped, the device offers no host memory type for the image\n");
|
||||
return;
|
||||
}
|
||||
auto context = base;
|
||||
context.bufferPool.reset();
|
||||
realDeviceProc = base.deviceProc;
|
||||
realAllocateMemory = base.Function<PFN_vkAllocateMemory>("vkAllocateMemory");
|
||||
refusingMemory = &context.memory;
|
||||
refusedAllocations = 0;
|
||||
lastAllocatedType = UINT32_MAX;
|
||||
context.deviceProc = refusingDeviceProc;
|
||||
{
|
||||
PresentationScaler scaler(context, VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_B8G8R8A8_UNORM);
|
||||
scaler.EnsureSourceImage(64u, 64u);
|
||||
Require(refusedAllocations != 0, "the presentation source never asked for device-local memory");
|
||||
Require(lastAllocatedType != UINT32_MAX && (context.memory.memoryHeaps[context.memory.memoryTypes[lastAllocatedType].heapIndex].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) == 0, "the presentation source was not placed in host memory when device-local memory was refused");
|
||||
Require(scaler.SourceWidth() == 64u && scaler.SourceHeight() == 64u, "the presentation source in host memory lost its extent");
|
||||
}
|
||||
context.bufferPool.reset();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
@@ -582,6 +640,7 @@ int main() {
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
residentPresentTests(device->GetContext());
|
||||
sourceImageOutsideDeviceMemoryTests(device->GetContext());
|
||||
std::cout << "resident present format, presentation pass and letterbox tests passed\n";
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
#endif
|
||||
#include <io.h>
|
||||
#include <windows.h>
|
||||
#include <psapi.h>
|
||||
#else
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
@@ -67,6 +68,7 @@
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <future>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
@@ -322,6 +324,7 @@ public:
|
||||
}
|
||||
return false;
|
||||
};
|
||||
memoryBudget = hasExtension(VK_EXT_MEMORY_BUDGET_EXTENSION_NAME);
|
||||
auto bytes = AgcDriver::QueryBdaByteFeatures(context.physical, function<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2"), available);
|
||||
auto address = AgcDriver::QueryBdaFeatures(context.physical, function<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2"), available);
|
||||
const auto queues = function<PFN_vkGetPhysicalDeviceQueueFamilyProperties>("vkGetPhysicalDeviceQueueFamilyProperties");
|
||||
@@ -516,6 +519,21 @@ public:
|
||||
return properties;
|
||||
}
|
||||
void WaitQueue() const { Check(context.Function<PFN_vkQueueWaitIdle>("vkQueueWaitIdle")(context.queue), "vkQueueWaitIdle"); }
|
||||
struct HeapUsage {
|
||||
std::uint64_t deviceLocal = 0;
|
||||
std::uint64_t host = 0;
|
||||
};
|
||||
std::optional<HeapUsage> MemoryUsage() const {
|
||||
if (!memoryBudget) return std::nullopt;
|
||||
VkPhysicalDeviceMemoryBudgetPropertiesEXT budget{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT};
|
||||
VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2, &budget};
|
||||
function<PFN_vkGetPhysicalDeviceMemoryProperties2>("vkGetPhysicalDeviceMemoryProperties2")(context.physical, &properties);
|
||||
HeapUsage usage;
|
||||
for (std::uint32_t heap = 0; heap < properties.memoryProperties.memoryHeapCount; ++heap) {
|
||||
((properties.memoryProperties.memoryHeaps[heap].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0 ? usage.deviceLocal : usage.host) += budget.heapUsage[heap];
|
||||
}
|
||||
return usage;
|
||||
}
|
||||
|
||||
private:
|
||||
template<typename TFunction>
|
||||
@@ -545,6 +563,7 @@ private:
|
||||
bool graphics = false;
|
||||
std::mutex settleMutex;
|
||||
bool nullDescriptors = false;
|
||||
bool memoryBudget = false;
|
||||
};
|
||||
|
||||
using Kind = Recorder::ReadKind;
|
||||
@@ -10112,6 +10131,98 @@ bool vertexTargetRangeTests(const Device& device, Recorder& recorder, bool impor
|
||||
return true;
|
||||
}
|
||||
|
||||
std::uint64_t processPrivateBytes() {
|
||||
#ifdef _WIN32
|
||||
PROCESS_MEMORY_COUNTERS_EX counters{};
|
||||
counters.cb = sizeof(counters);
|
||||
if (!K32GetProcessMemoryInfo(GetCurrentProcess(), reinterpret_cast<PROCESS_MEMORY_COUNTERS*>(&counters), sizeof(counters))) return 0;
|
||||
return counters.PrivateUsage;
|
||||
#else
|
||||
std::ifstream status("/proc/self/status");
|
||||
std::string line;
|
||||
while (std::getline(status, line)) {
|
||||
if (line.rfind("RssAnon:", 0) == 0) return std::stoull(line.substr(8)) * 1024u;
|
||||
}
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
// What one graphics pipeline the store keeps costs: `count` pipelines, each with its own fragment
|
||||
// module (so its own fragment shader library), built through CachedPipeline and held, after their
|
||||
// optimized relinks; process private bytes and VK_EXT_memory_budget heap usage before and after,
|
||||
// and after the store is cleared.
|
||||
int pipelineStoreMemoryBench(const Device& device, std::uint32_t count) {
|
||||
if (!device.Graphics() || count == 0) {
|
||||
std::cout << "skipped, the pipeline store memory bench requires a graphics queue and at least one pipeline\n";
|
||||
return 77;
|
||||
}
|
||||
std::lock_guard gpu(GpuMutex());
|
||||
const auto& context = device.GetContext();
|
||||
struct Cleanup {
|
||||
VkDevice device;
|
||||
~Cleanup() { ClearCachedPipelines(device); }
|
||||
} cleanup{context.device};
|
||||
ClearCachedPipelines(context.device);
|
||||
ShaderRecompiler::RecompileResult vertex;
|
||||
vertex.variantId = 21;
|
||||
vertex.spirv = std::vector<std::uint32_t>(std::begin(TRIANGLE_vert_SPV), std::end(TRIANGLE_vert_SPV));
|
||||
std::vector<ShaderRecompiler::RecompileResult> fragments(count + 1u);
|
||||
for (std::uint32_t index = 0; index <= count; ++index) {
|
||||
fragments[index].variantId = 1000u + index;
|
||||
fragments[index].spirv = std::vector<std::uint32_t>(std::begin(TRIANGLE_frag_SPV), std::end(TRIANGLE_frag_SPV));
|
||||
fragments[index].spirv[2] = 0x50535430u + index;
|
||||
}
|
||||
State state{};
|
||||
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
state.hasColorTarget = true;
|
||||
state.color.format = VK_FORMAT_R8G8B8A8_UNORM;
|
||||
state.colors.push_back(state.color);
|
||||
state.blend.colorWriteMask = 15;
|
||||
state.blends.push_back(state.blend);
|
||||
const std::array first{CompiledShader{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, CompiledShader{ShaderRecompiler::ShaderStage::Fragment, &fragments[count], 0}};
|
||||
ShaderResources resources(context, first, state.color, static_cast<std::uint32_t>(state.colors.size()), 0, 0);
|
||||
const VertexInputLayout input;
|
||||
std::vector<std::shared_ptr<Pipeline>> held;
|
||||
held.reserve(count + 1u);
|
||||
const auto build = [&](std::uint32_t index) {
|
||||
const std::array shaders{CompiledShader{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, CompiledShader{ShaderRecompiler::ShaderStage::Fragment, &fragments[index], 0}};
|
||||
held.push_back(CachedPipeline(context, state, input, resources, shaders, VK_IMAGE_LAYOUT_GENERAL));
|
||||
};
|
||||
struct Sample {
|
||||
std::uint64_t privateBytes;
|
||||
std::optional<Device::HeapUsage> heaps;
|
||||
};
|
||||
const auto sample = [&] {
|
||||
WaitForOptimizedPipelines(context.device);
|
||||
return Sample{processPrivateBytes(), device.MemoryUsage()};
|
||||
};
|
||||
build(count);
|
||||
const auto before = sample();
|
||||
const auto started = std::chrono::steady_clock::now();
|
||||
for (std::uint32_t index = 0; index < count; ++index) build(index);
|
||||
const auto built = std::chrono::steady_clock::now();
|
||||
const auto after = sample();
|
||||
const auto linked = std::chrono::steady_clock::now();
|
||||
const auto totals = PipelineStoreTotals();
|
||||
held.clear();
|
||||
ClearCachedPipelines(context.device);
|
||||
const auto cleared = sample();
|
||||
const auto mib = [](std::uint64_t later, std::uint64_t earlier) { return (static_cast<double>(later) - static_cast<double>(earlier)) / (1024.0 * 1024.0); };
|
||||
const auto perPipeline = [&](std::uint64_t later, std::uint64_t earlier) { return (static_cast<double>(later) - static_cast<double>(earlier)) / 1024.0 / count; };
|
||||
const char* linking = !context.graphicsPipelineLibrary ? "whole pipelines" : context.pipelineLinkTimeOptimization ? "libraries and optimized relinks" : "libraries, fast links only";
|
||||
std::cout << std::fixed << std::setprecision(1);
|
||||
std::cout << "Pipeline store memory: " << count << " graphics pipelines (one fragment module each, " << linking << "), " << totals.cached << " in the store, built in "
|
||||
<< std::chrono::duration<double>(built - started).count() << " s, relinked in " << std::chrono::duration<double>(linked - built).count() << " s\n";
|
||||
std::cout << " process private bytes: +" << mib(after.privateBytes, before.privateBytes) << " MiB = " << perPipeline(after.privateBytes, before.privateBytes) << " KiB per pipeline; after clearing the store " << mib(cleared.privateBytes, before.privateBytes) << " MiB above the start\n";
|
||||
if (before.heaps && after.heaps && cleared.heaps) {
|
||||
std::cout << " VK_EXT_memory_budget heap usage: device-local +" << mib(after.heaps->deviceLocal, before.heaps->deviceLocal) << " MiB = " << perPipeline(after.heaps->deviceLocal, before.heaps->deviceLocal) << " KiB per pipeline, host +" << mib(after.heaps->host, before.heaps->host) << " MiB = " << perPipeline(after.heaps->host, before.heaps->host)
|
||||
<< " KiB per pipeline; after clearing the store device-local " << mib(cleared.heaps->deviceLocal, before.heaps->deviceLocal) << " MiB, host " << mib(cleared.heaps->host, before.heaps->host) << " MiB above the start\n";
|
||||
} else {
|
||||
std::cout << " VK_EXT_memory_budget unavailable: no device heap usage\n";
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void pipelineCacheTests(const Device& device, bool benchmark) {
|
||||
std::thread establishThreadedProcess([] {});
|
||||
establishThreadedProcess.join();
|
||||
@@ -10947,6 +11058,97 @@ void denormalClearTests(const Device& device, Recorder& recorder) {
|
||||
recorder.Sync();
|
||||
}
|
||||
|
||||
class LibraryCreateFlags {
|
||||
public:
|
||||
explicit LibraryCreateFlags(const Context& context) : resolver(context.deviceProc), device(context.device) {
|
||||
create = function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines");
|
||||
active = this;
|
||||
}
|
||||
~LibraryCreateFlags() { active = nullptr; }
|
||||
LibraryCreateFlags(const LibraryCreateFlags&) = delete;
|
||||
LibraryCreateFlags& operator=(const LibraryCreateFlags&) = delete;
|
||||
PFN_vkGetDeviceProcAddr Resolver() const { return resolve; }
|
||||
std::uint32_t Libraries() const { return libraries.load(); }
|
||||
std::uint32_t Retaining() const { return retaining.load(); }
|
||||
|
||||
private:
|
||||
template<typename TFunction>
|
||||
TFunction function(const char* name) const { return reinterpret_cast<TFunction>(resolver(device, name)); }
|
||||
static VKAPI_ATTR VkResult VKAPI_CALL createPipelines(VkDevice device, VkPipelineCache cache, std::uint32_t count, const VkGraphicsPipelineCreateInfo* infos, const VkAllocationCallbacks* callbacks, VkPipeline* pipelines) {
|
||||
const auto result = active->create(device, cache, count, infos, callbacks, pipelines);
|
||||
for (std::uint32_t index = 0; index < count; ++index) {
|
||||
if (pipelines[index] == VK_NULL_HANDLE || (infos[index].flags & VK_PIPELINE_CREATE_LIBRARY_BIT_KHR) == 0) continue;
|
||||
active->libraries.fetch_add(1);
|
||||
if ((infos[index].flags & VK_PIPELINE_CREATE_RETAIN_LINK_TIME_OPTIMIZATION_INFO_BIT_EXT) != 0) active->retaining.fetch_add(1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
static VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL resolve(VkDevice device, const char* name) {
|
||||
if (std::strcmp(name, "vkCreateGraphicsPipelines") == 0) return reinterpret_cast<PFN_vkVoidFunction>(createPipelines);
|
||||
return active->resolver(device, name);
|
||||
}
|
||||
PFN_vkGetDeviceProcAddr resolver;
|
||||
VkDevice device;
|
||||
PFN_vkCreateGraphicsPipelines create = nullptr;
|
||||
std::atomic<std::uint32_t> libraries{0};
|
||||
std::atomic<std::uint32_t> retaining{0};
|
||||
static inline LibraryCreateFlags* active = nullptr;
|
||||
};
|
||||
|
||||
void pipelineLibraryLtoInfoTests(const Device& device) {
|
||||
std::lock_guard gpu(GpuMutex());
|
||||
const auto& context = device.GetContext();
|
||||
LibraryCreateFlags created(context);
|
||||
struct Cleanup {
|
||||
VkDevice device;
|
||||
~Cleanup() { ClearCachedPipelines(device); }
|
||||
} cleanup{context.device};
|
||||
ClearCachedPipelines(context.device);
|
||||
if (!context.graphicsPipelineLibrary) {
|
||||
std::cout << "graphics pipeline libraries unavailable or disabled: no library retains link-time optimization info\n";
|
||||
return;
|
||||
}
|
||||
ShaderRecompiler::RecompileResult vertex, fragment;
|
||||
vertex.variantId = 41;
|
||||
fragment.variantId = 42;
|
||||
vertex.spirv = std::vector<std::uint32_t>(std::begin(TRIANGLE_vert_SPV), std::end(TRIANGLE_vert_SPV));
|
||||
fragment.spirv = std::vector<std::uint32_t>(std::begin(TRIANGLE_frag_SPV), std::end(TRIANGLE_frag_SPV));
|
||||
const std::array shaders{CompiledShader{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, CompiledShader{ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}};
|
||||
State state{};
|
||||
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
state.hasColorTarget = true;
|
||||
state.color.format = VK_FORMAT_R8G8B8A8_UNORM;
|
||||
state.colors.push_back(state.color);
|
||||
state.blend.colorWriteMask = 15;
|
||||
state.blends.push_back(state.blend);
|
||||
ShaderResources resources(context, shaders, state.color, static_cast<std::uint32_t>(state.colors.size()), 0, 0);
|
||||
VertexInputLayout input;
|
||||
auto fast = context;
|
||||
fast.deviceProc = created.Resolver();
|
||||
fast.pipelineLinkTimeOptimization = false;
|
||||
auto optimizing = fast;
|
||||
optimizing.pipelineLinkTimeOptimization = true;
|
||||
const bool lto = std::getenv("APS5_NO_PIPELINE_LTO") == nullptr;
|
||||
const auto fastPipeline = CachedPipeline(fast, state, input, resources, shaders, VK_IMAGE_LAYOUT_GENERAL);
|
||||
const auto fastLibraries = created.Libraries();
|
||||
Require(fastLibraries != 0, "the pipeline was not linked from libraries created through the traced device");
|
||||
Require(created.Retaining() == 0, std::to_string(created.Retaining()) + " of the " + std::to_string(fastLibraries) + " libraries made for a context without optimized links were created to retain link-time optimization info");
|
||||
WaitForOptimizedPipelines(context.device);
|
||||
Require(!fastPipeline->Optimized(), "a pipeline of a context without optimized links was optimized in the background");
|
||||
const auto optimizedPipeline = CachedPipeline(optimizing, state, input, resources, shaders, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
|
||||
Require(optimizedPipeline != fastPipeline, "the two attachment layouts shared one cached pipeline");
|
||||
const auto libraries = created.Libraries() - fastLibraries;
|
||||
if (lto) {
|
||||
Require(libraries == fastLibraries, "a link that will be optimized made " + std::to_string(libraries) + " libraries, expected " + std::to_string(fastLibraries) + ": it reused libraries made without link-time optimization info");
|
||||
Require(created.Retaining() == libraries, std::to_string(created.Retaining()) + " of the " + std::to_string(libraries) + " libraries of a link that will be optimized were created to retain link-time optimization info");
|
||||
} else {
|
||||
Require(libraries == 0 && created.Retaining() == 0, "with APS5_NO_PIPELINE_LTO set, forcing pipelineLinkTimeOptimization changed the libraries or their create flags");
|
||||
}
|
||||
WaitForOptimizedPipelines(context.device);
|
||||
Require(optimizedPipeline->Optimized() == lto, lto ? "the pipeline linked from libraries that retain link-time optimization info was not optimized in the background" : "a pipeline was optimized with APS5_NO_PIPELINE_LTO set");
|
||||
std::cout << "Pipeline library link-time optimization info tests passed: " << fastLibraries << " libraries without the info, " << created.Retaining() << " with it\n";
|
||||
}
|
||||
|
||||
void pipelineLibraryTests(const Device& device) {
|
||||
std::lock_guard gpu(GpuMutex());
|
||||
const auto& context = device.GetContext();
|
||||
@@ -11327,10 +11529,15 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
return sparseRunImportTests(device, true) ? 0 : 77;
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--pipeline-library-lto-info-only") {
|
||||
pipelineLibraryLtoInfoTests(device);
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--pipeline-library-only") {
|
||||
pipelineLibraryTests(device);
|
||||
return 0;
|
||||
}
|
||||
if ((argc == 2 || argc == 3) && std::string_view(argv[1]) == "--benchmark-pipeline-store-memory") return pipelineStoreMemoryBench(device, argc == 3 ? static_cast<std::uint32_t>(std::stoul(argv[2])) : 4096u);
|
||||
if (argc == 2 && (std::string_view(argv[1]) == "--benchmark-pipeline-cache" || std::string_view(argv[1]) == "--pipeline-cache-only")) {
|
||||
pipelineCacheTests(device, std::string_view(argv[1]) == "--benchmark-pipeline-cache");
|
||||
return 0;
|
||||
|
||||
@@ -6833,6 +6833,12 @@ void verifyUnnormalizedSamplers() {
|
||||
const auto adjusted = recompile(reservedDword3(0xf0800f09u), userData(normalized));
|
||||
require(proven(adjusted, false), "sampler dword 3: an image_sample_a whose S# dword 3 holds only a ballot in its reserved bits was not compiled");
|
||||
expectFailure([&] { static_cast<void>(recompile(reservedDword3(0xf0800f08u), userData(normalized))); }, "GetSamplerResource dword 3 is not a valid runtime value", "sampler dword 3: an image_sample whose S# dword 3 holds a ballot was accepted");
|
||||
for (const auto& [adjust, plain, compiled, accepted] : {
|
||||
std::tuple{0xf1200109u, 0xf1200108u, "sampler dword 3: an image_gather4_c_a whose S# dword 3 holds only a ballot in its reserved bits was not compiled", "sampler dword 3: an image_gather4_c with a computed S# dword 3 was accepted"},
|
||||
std::tuple{0xf1600109u, 0xf1600108u, "sampler dword 3: an image_gather4_c_o_a whose S# dword 3 holds only a ballot in its reserved bits was not compiled", "sampler dword 3: an image_gather4_c_o with a computed S# dword 3 was accepted"}}) {
|
||||
require(proven(recompile(reservedDword3(adjust), imageData(normalized, Type2D, Format32Float, 0u)), false), compiled);
|
||||
expectFailure([&] { static_cast<void>(recompile(reservedDword3(plain), imageData(normalized, Type2D, Format32Float, 0u))); }, "GetSamplerResource dword 3 is not a valid runtime value", accepted);
|
||||
}
|
||||
reject(0xf09c0f18u, imageData(unnormalized, TypeCube, Format8888UNorm, 5u), "unnormalized guest sampler samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: a cube T# was accepted");
|
||||
reject(0xf09c0f10u, imageData(unnormalized, Type3D, Format8888UNorm, 3u), "unnormalized guest sampler samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: a 3D T# was accepted");
|
||||
reject(0xf09c0f28u, imageData(unnormalized, Type2DArray, Format8888UNorm, 5u), "unnormalized guest sampler samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: a 2D-array sample was accepted");
|
||||
|
||||
@@ -952,7 +952,8 @@ int main(int argc, char** argv) {
|
||||
});
|
||||
for (auto& reader : readers) reader.join();
|
||||
for (const auto& snapshot : concurrent) check(snapshot == branched, "concurrent raw compute readers lost snapshot reuse");
|
||||
alignas(256) std::array<std::array<std::uint32_t, 64>, 65> programs{};
|
||||
struct alignas(256) RawProgram : std::array<std::uint32_t, 64> {};
|
||||
std::vector<RawProgram> programs(4097);
|
||||
for (auto& program : programs) program[0] = 0xbf810000;
|
||||
const auto firstAddress = reinterpret_cast<std::uintptr_t>(programs.front().data());
|
||||
readers.clear();
|
||||
@@ -962,6 +963,9 @@ int main(int argc, char** argv) {
|
||||
for (auto& reader : readers) reader.join();
|
||||
for (const auto& snapshot : concurrent) check(snapshot == concurrent.front(), "concurrent raw compute misses duplicated snapshots");
|
||||
const auto evicted = AgcDriver::DriverDetail::ReadRawShader(firstAddress);
|
||||
for (std::size_t i = 1; i <= 400; ++i)
|
||||
AgcDriver::DriverDetail::ReadRawShader(reinterpret_cast<std::uintptr_t>(programs[i].data()));
|
||||
check(AgcDriver::DriverDetail::ReadRawShader(firstAddress) == evicted, "raw shader cache evicted a program while a title used only 401");
|
||||
for (std::size_t i = 1; i < programs.size(); ++i)
|
||||
AgcDriver::DriverDetail::ReadRawShader(reinterpret_cast<std::uintptr_t>(programs[i].data()));
|
||||
check(AgcDriver::DriverDetail::ReadRawShader(firstAddress) != evicted && evicted->code[0] == 0xbf810000,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "GraphicsTests.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
|
||||
@@ -424,26 +426,35 @@ void RunTextureTilingTests() {
|
||||
array.depthOrLastArray = 1536;
|
||||
array.baseArray = 1536;
|
||||
const auto layerBytes = DescribeSurface(array).layerBytes;
|
||||
const auto window = ViewedArrayWindow(array);
|
||||
const auto window = ViewedArrayWindow(array, false);
|
||||
Require(window.baseAddress == array.baseAddress + 1536ull * layerBytes && window.depthOrLastArray == 0 && window.baseArray == 0, "a sampled view of one slice at 1536 of an R_X array must start at that slice");
|
||||
Require(DescribeSurface(window).layerBytes == layerBytes && DescribeSurface(window).layers == 1, "a sampled array window must keep the slice layout");
|
||||
|
||||
auto unaligned = array;
|
||||
unaligned.baseArray = 1530;
|
||||
Require(ViewedArrayWindow(unaligned).baseAddress == array.baseAddress, "an R_X window at a slice the swizzle mixes in must keep the whole array");
|
||||
const auto mixed = ViewedArrayWindow(unaligned, false);
|
||||
Require(mixed.baseAddress == array.baseAddress + 1520ull * layerBytes && mixed.baseArray == 10 && mixed.depthOrLastArray == 16, "an R_X view at a slice the swizzle mixes in must start its window at the last slice before it with those bits clear");
|
||||
auto low = array;
|
||||
low.baseArray = 9;
|
||||
Require(ViewedArrayWindow(low, false).baseAddress == array.baseAddress, "an R_X view below the first slice with those bits clear keeps the whole array");
|
||||
|
||||
auto plain = array;
|
||||
plain.tileMode = TextureTileMode::kStandard64KB;
|
||||
plain.baseArray = 1530;
|
||||
Require(ViewedArrayWindow(plain).baseAddress == array.baseAddress + 1530ull * DescribeSurface(plain).layerBytes && ViewedArrayWindow(plain).depthOrLastArray == 6, "a non-XOR window may start at any slice");
|
||||
Require(ViewedArrayWindow(plain, false).baseAddress == array.baseAddress + 1530ull * DescribeSurface(plain).layerBytes && ViewedArrayWindow(plain, false).depthOrLastArray == 6, "a non-XOR window may start at any slice");
|
||||
|
||||
auto fromZero = array;
|
||||
fromZero.baseArray = 0;
|
||||
Require(ViewedArrayWindow(fromZero).baseAddress == array.baseAddress && ViewedArrayWindow(fromZero).depthOrLastArray == 1536, "a view from slice 0 keeps its descriptor");
|
||||
Require(ViewedArrayWindow(fromZero, false).baseAddress == array.baseAddress && ViewedArrayWindow(fromZero, false).depthOrLastArray == 1536, "a view from slice 0 keeps its descriptor");
|
||||
|
||||
auto compressed = array;
|
||||
compressed.dccAddress = 0x20000000ull;
|
||||
Require(ViewedArrayWindow(compressed).baseAddress == array.baseAddress, "a view with DCC metadata keeps its descriptor");
|
||||
Require(ViewedArrayWindow(compressed, false).baseAddress == array.baseAddress, "a view with DCC metadata keeps its descriptor");
|
||||
const auto stored = ViewedArrayWindow(compressed, true);
|
||||
Require(stored.baseAddress == window.baseAddress && stored.depthOrLastArray == 0 && stored.baseArray == 0 && stored.dccAddress == 0, "a view whose DCC keys read as stored is cut to its slices without metadata");
|
||||
auto compressedFromZero = compressed;
|
||||
compressedFromZero.baseArray = 0;
|
||||
Require(ViewedArrayWindow(compressedFromZero, true).baseAddress == array.baseAddress && ViewedArrayWindow(compressedFromZero, true).dccAddress == compressed.dccAddress, "a view with DCC metadata from slice 0 keeps its descriptor");
|
||||
|
||||
GuestTextureResource cube{};
|
||||
cube.baseAddress = 0x30000000ull;
|
||||
@@ -455,13 +466,45 @@ void RunTextureTilingTests() {
|
||||
cube.tileMode = TextureTileMode::kStandard4KB;
|
||||
cube.depthOrLastArray = 7232;
|
||||
cube.baseArray = 7227;
|
||||
const auto faces = ViewedArrayWindow(cube);
|
||||
const auto faces = ViewedArrayWindow(cube, false);
|
||||
Require(faces.baseAddress == cube.baseAddress + 7227ull * DescribeSurface(cube).layerBytes && faces.depthOrLastArray == 5 && faces.baseArray == 0 && faces.dimension == TextureDimension::kCube, "a cube view of six faces at 7227 must start at its first face");
|
||||
|
||||
auto compressedVolume = cube;
|
||||
compressedVolume.dimension = TextureDimension::k3D;
|
||||
compressedVolume.baseArray = 0;
|
||||
compressedVolume.dccAddress = 0x20000000ull;
|
||||
Require(ViewedArrayWindow(compressedVolume, true).dccAddress == compressedVolume.dccAddress, "a volume with DCC metadata keeps its descriptor");
|
||||
|
||||
auto volume = cube;
|
||||
volume.dimension = TextureDimension::k3D;
|
||||
volume.baseArray = 0;
|
||||
Require(ViewedArrayWindow(volume).baseAddress == cube.baseAddress, "a volume keeps its descriptor");
|
||||
Require(ViewedArrayWindow(volume, false).baseAddress == cube.baseAddress, "a volume keeps its descriptor");
|
||||
}
|
||||
|
||||
{
|
||||
const std::array<std::uint32_t, 8> words{0x027c0000u, 0xc1600000u, 0x00ffc0ffu, 0xd1800924u, 0x027cc27cu, 0xc1400000u, 0x00704018u, 0x91b00204u};
|
||||
const auto view = DecodeTextureResource(words);
|
||||
Require(view.baseAddress == 0x27c000000ull && view.dccAddress == 0x91b002040000ull && view.dimension == TextureDimension::k2DArray && view.baseArray == 636 && view.depthOrLastArray == 636 && view.width == 1024 && view.height == 1024 && view.format == 22, "the title's DCC array view decodes to slice 636 of 637");
|
||||
const auto whole = DescribeSurface(view);
|
||||
Require(whole.guestBytes == 2671771648ull, "the uncut DCC array view covers all 637 slices (" + std::to_string(whole.guestBytes) + " bytes)");
|
||||
Require(TextureClearKeys(view, whole.guestBytes) == DccKeys::Uncompressed, "unreadable DCC keys of the uncut view read the texels as stored");
|
||||
Require(ViewedArrayWindow(view, false).baseAddress == view.baseAddress, "a DCC view whose keys are not known to read as stored keeps its descriptor");
|
||||
const auto window = ViewedArrayWindow(view, true);
|
||||
const auto cut = DescribeSurface(window);
|
||||
Require(window.baseAddress == view.baseAddress + whole.GuestLayerOffset(624) && window.dccAddress == 0 && window.baseArray == 12 && window.depthOrLastArray == 12, "the window of a stored-keys DCC view starts at the slice-swizzle boundary below its slice without metadata");
|
||||
Require(cut.layers == 13 && cut.layerBytes == whole.layerBytes && window.baseAddress + cut.guestBytes == view.baseAddress + whole.guestBytes, "the window covers slices 624..636 (" + std::to_string(cut.guestBytes) + " bytes)");
|
||||
for (std::uint32_t slice = 0; slice < cut.layers; ++slice) Require(window.baseAddress + cut.GuestLayerOffset(slice) == view.baseAddress + whole.GuestLayerOffset(624 + slice), "a window slice lies where the uncut surface keeps it");
|
||||
const auto* equation = FindTextureSwizzleEquation(XorSwizzleMode(view.tileMode), 4);
|
||||
const auto block = ThinBlockLayout(view.tileMode, 4);
|
||||
Require(equation != nullptr && whole.mips.size() == 1 && !whole.mips[0].tail, "the title's DCC array is one untailed Z_X level");
|
||||
const auto texel = [&](const GuestTextureResource& surface, const SurfaceGeometry& geometry, std::uint32_t x, std::uint32_t y, std::uint32_t slice) {
|
||||
const auto& mip = geometry.mips[0];
|
||||
return surface.baseAddress + geometry.GuestLayerOffset(slice) + mip.tiledOffset + (static_cast<std::uint64_t>(y / block[2]) * mip.blocksPerRow + x / block[1]) * block[0] + equationOffset(*equation, x, y, slice);
|
||||
};
|
||||
for (std::uint32_t y = 0; y < 1024; y += 7) {
|
||||
for (std::uint32_t x = 0; x < 1024; x += 5) Require(texel(window, cut, x, y, window.baseArray) == texel(view, whole, x, y, view.baseArray), "a texel of the viewed slice detiles from the same guest byte in the window as in the uncut surface");
|
||||
}
|
||||
Require(TextureClearKeys(window, cut.guestBytes) == DccKeys::Uncompressed, "the window reads its texels as stored, as the uncut view does");
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -1506,7 +1506,7 @@ void CheckImportWindow() {
|
||||
bytes[0x100] = 0x11;
|
||||
bytes[Block + 0x2000] = 0x22;
|
||||
return true;
|
||||
}), "the import window import failed");
|
||||
}, true, false), "the import window import failed");
|
||||
Require(Watched(base, 3 * Block), "an import window left its range unwatched");
|
||||
const std::array<std::uint64_t, 3> generations{synced, synced, synced};
|
||||
std::array<std::uint8_t, 3> changed{};
|
||||
@@ -1718,6 +1718,80 @@ void CheckUnwatchedImport() {
|
||||
#endif
|
||||
}
|
||||
|
||||
struct HookCall {
|
||||
std::uint64_t address;
|
||||
std::size_t bytes;
|
||||
bool watched;
|
||||
};
|
||||
|
||||
std::vector<HookCall> hookCalls;
|
||||
std::uint64_t hookProbe = 0;
|
||||
std::size_t hookProbeBytes = 0;
|
||||
|
||||
void RecordHook(std::uint64_t address, std::size_t bytes) {
|
||||
hookCalls.push_back({address, bytes, Watched(hookProbe, hookProbeBytes)});
|
||||
}
|
||||
|
||||
struct RecordedHook {
|
||||
RecordedHook() {
|
||||
hookCalls.clear();
|
||||
SetUnwatchHook(&RecordHook);
|
||||
}
|
||||
~RecordedHook() { SetUnwatchHook(nullptr); }
|
||||
RecordedHook(const RecordedHook&) = delete;
|
||||
RecordedHook& operator=(const RecordedHook&) = delete;
|
||||
};
|
||||
|
||||
void RequireHookOver(std::uint64_t address, std::size_t bytes, const std::string& path) {
|
||||
Require(hookCalls.size() == 1, path + " did not run the unwatch hook once");
|
||||
const auto& call = hookCalls.front();
|
||||
Require(call.address <= address && address + bytes <= call.address + call.bytes, path + " gave the unwatch hook a range that misses its own");
|
||||
Require(call.watched, path + " ran the unwatch hook after its range left the write watch");
|
||||
}
|
||||
|
||||
void CheckUnwatchHooks() {
|
||||
void* memory = AllocateWatched(8 * Block);
|
||||
const auto base = reinterpret_cast<std::uint64_t>(memory);
|
||||
std::memset(memory, 0x11, 8 * Block);
|
||||
Require(CollectWritesUncached(base, 8 * Block) != 0, "the hooked block is not collected");
|
||||
const RecordedHook recorded;
|
||||
hookProbe = base;
|
||||
hookProbeBytes = Block;
|
||||
Unwatch(base, Block);
|
||||
RequireHookOver(base, Block, "Unwatch");
|
||||
Require(!Watched(base, Block), "Unwatch left its range watched");
|
||||
|
||||
hookCalls.clear();
|
||||
hookProbe = base + Block;
|
||||
bool hookedBeforeImport = false;
|
||||
Require(ImportWatched(base + Block, Block, [&] {
|
||||
hookedBeforeImport = hookCalls.size() == 1;
|
||||
return true;
|
||||
}, false), "an import that does not stay watched failed");
|
||||
RequireHookOver(base + Block, Block, "an import that does not stay watched");
|
||||
Require(hookedBeforeImport, "an import that does not stay watched ran the unwatch hook after the import");
|
||||
Require(!Watched(base + Block, Block), "an import that does not stay watched left its range watched");
|
||||
|
||||
hookCalls.clear();
|
||||
Require(ImportWatched(base + 2 * Block, Block, [] { return true; }), "an import that stays watched failed");
|
||||
Require(hookCalls.empty() && Watched(base + 2 * Block, Block), "an import whose page walk vouched for its pages ran the unwatch hook or gave up its range");
|
||||
|
||||
#ifndef _WIN32
|
||||
hookCalls.clear();
|
||||
const auto late = base + 4 * Block;
|
||||
hookProbe = late;
|
||||
bool hookedBeforeLoss = false;
|
||||
Require(ImportWatched(late, 3 * Block, [&] {
|
||||
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(reinterpret_cast<const void*>(late + 2 * Block), Block);
|
||||
hookedBeforeLoss = !hookCalls.empty();
|
||||
return true;
|
||||
}), "an import whose range lost a block during the import failed");
|
||||
RequireHookOver(late, 3 * Block, "an import whose page walk failed after the import");
|
||||
Require(!hookedBeforeLoss, "an import whose page walk failed after the import ran the unwatch hook before it");
|
||||
Require(!Watched(late, Block), "an import whose page walk failed after the import left its range watched");
|
||||
#endif
|
||||
}
|
||||
|
||||
class PartlyCommitted {
|
||||
public:
|
||||
static constexpr std::size_t Bytes = 4 * Block;
|
||||
@@ -1948,6 +2022,7 @@ int main() {
|
||||
CheckStoreAcrossImports();
|
||||
CheckImportWindowFootprint();
|
||||
CheckUnwatchedImport();
|
||||
CheckUnwatchHooks();
|
||||
CheckCommittedRanges();
|
||||
CheckAdoptLatestEpoch();
|
||||
} catch (const std::exception& error) {
|
||||
|
||||
@@ -141,6 +141,7 @@ struct Step {
|
||||
const std::uint32_t* pixel;
|
||||
std::uint64_t pointer;
|
||||
std::array<std::uint32_t, 4> descriptor{};
|
||||
std::uint64_t color = 0;
|
||||
};
|
||||
|
||||
void Submit(const GuestBlock& vertices, const GuestBlock& target, std::initializer_list<Step> steps, bool linear = false) {
|
||||
@@ -179,6 +180,10 @@ void Submit(const GuestBlock& vertices, const GuestBlock& target, std::initializ
|
||||
Append(commands, 0x76, {0x008, static_cast<std::uint32_t>(pixel >> 8u), static_cast<std::uint32_t>(pixel >> 40u)});
|
||||
Append(commands, 0x76, {0x00e, static_cast<std::uint32_t>(step.pointer), static_cast<std::uint32_t>(step.pointer >> 32u)});
|
||||
Append(commands, 0x76, {0x010, step.descriptor[0], step.descriptor[1], step.descriptor[2], step.descriptor[3]});
|
||||
if (step.color != 0) {
|
||||
Append(commands, 0x69, {0x318, static_cast<std::uint32_t>(step.color >> 8u)});
|
||||
Append(commands, 0x69, {0x390, static_cast<std::uint32_t>(step.color >> 40u)});
|
||||
}
|
||||
Append(commands, 0x2d, {3, 2});
|
||||
}
|
||||
Packet packet{commands.data(), static_cast<std::uint32_t>(commands.size()), 0, {}};
|
||||
@@ -285,6 +290,26 @@ void TargetReadAsWhenSplit(const GuestBlock& vertices, const GuestBlock& target,
|
||||
if (!linear && !split) Expect(after.passesContinued - before.passesContinued == 1, what + ": the reading draw did not continue the rendering draw's pass, so the case does not test it: " + Counts(before, after));
|
||||
}
|
||||
|
||||
void TargetReadSynchronously(const GuestBlock& vertices, const GuestBlock& target, const GuestBlock& unmapped) {
|
||||
Submit(vertices, target, {{PixelRead.data(), target.At(ColorOffset)}}, true);
|
||||
std::memset(target.Bytes(ColorOffset), 0, ColorBytes);
|
||||
std::memset(unmapped.Bytes(ColorOffset), 0, ColorBytes);
|
||||
const auto before = AddressDraws();
|
||||
const auto otherBefore = OtherDraws();
|
||||
Submit(vertices, target, {{PixelColorWord.data(), 0}, {PixelRead.data(), target.At(ColorOffset), {}, unmapped.At(ColorOffset)}}, true);
|
||||
const auto after = AddressDraws();
|
||||
const auto otherAfter = OtherDraws();
|
||||
const unsigned red = unmapped.Bytes(ColorOffset)[0];
|
||||
std::printf("a synchronous draw reading through the table the linear target the previous draw rendered read %s (red byte %u); recorded: %s, %s\n", red == 255u ? "that draw's color" : "the bytes before it", red, Counts(before, after).c_str(), Counts(otherBefore, otherAfter, "other draws").c_str());
|
||||
if (otherAfter.draws == otherBefore.draws) {
|
||||
std::puts("a synchronous draw reading through the table the linear target the previous draw rendered: skipped, that draw was not recorded because the linear target is not resident on this device, so the target was stored right after it");
|
||||
return;
|
||||
}
|
||||
Expect(otherAfter.draws - otherBefore.draws == 1, "the draw rendering the linear target was not recorded as one draw: " + Counts(otherBefore, otherAfter, "other draws"));
|
||||
Expect(after.draws == before.draws, "the draw reading through the table into an unregistered target was recorded, so the case does not test a synchronous draw: " + Counts(before, after));
|
||||
Expect(red == 255u, "a synchronous draw reading through the table the linear target the previous draw rendered read the bytes before it (red byte " + std::to_string(red) + "), not that draw's color");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
@@ -306,6 +331,7 @@ int main(int argc, char** argv) {
|
||||
GuestBlock readOnlyVertices;
|
||||
GuestBlock data;
|
||||
GuestBlock target;
|
||||
GuestBlock unmapped;
|
||||
FillVertices(readOnlyVertices);
|
||||
readOnlyVertices.Map(false);
|
||||
data.Map(true);
|
||||
@@ -320,6 +346,7 @@ int main(int argc, char** argv) {
|
||||
TargetReadAsWhenSplit(readOnlyVertices, target, false);
|
||||
TargetReadAsWhenSplit(readOnlyVertices, target, true);
|
||||
}
|
||||
if (only.empty() || only == "--synchronous-target-read") TargetReadSynchronously(readOnlyVertices, target, unmapped);
|
||||
AgcDriverShutdown_nid_postfix();
|
||||
std::puts("address passes: draws reading through the BDA table share a render pass, and a store through it still begins one");
|
||||
return 0;
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "DepthFastClearHarness.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
namespace Harness = DepthFastClearHarness;
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
struct Bounds {
|
||||
float min;
|
||||
float max;
|
||||
};
|
||||
|
||||
struct Case {
|
||||
float stored;
|
||||
Bounds bounds;
|
||||
bool covered;
|
||||
const char* what;
|
||||
};
|
||||
|
||||
alignas(4096) std::array<std::array<float, Harness::Width * Harness::Height>, 16> Depths{};
|
||||
alignas(256) std::array<std::array<std::array<float, 4>, 3>, 64> VertexSets{};
|
||||
std::size_t vertexSets = 0;
|
||||
|
||||
std::uint32_t Draw(AgcDriver::VulkanDevice& device, std::uint64_t depth, float z, std::optional<Bounds> bounds, bool emulate) {
|
||||
Require(vertexSets < VertexSets.size(), "the test ran out of vertex sets");
|
||||
auto& vertices = VertexSets[vertexSets++];
|
||||
vertices = {{{-1.0f, -1.0f, z, 1.0f}, {3.0f, -1.0f, z, 1.0f}, {-1.0f, 3.0f, z, 1.0f}}};
|
||||
Harness::Pixels.fill(std::byte{0});
|
||||
|
||||
AgcDriver::Graphics::State state{};
|
||||
state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt};
|
||||
state.color = {reinterpret_cast<std::uintptr_t>(Harness::Pixels.data()), {Harness::Width, Harness::Height}, VK_FORMAT_R8G8B8A8_UNORM, Harness::Pixels.size(), 0xe4u};
|
||||
state.colors = {state.color};
|
||||
state.hasColorTarget = true;
|
||||
state.depth = AgcDriver::Graphics::DepthTarget{depth, 0, {Harness::Width, Harness::Height}, VK_FORMAT_D32_SFLOAT, 1.0f, 0};
|
||||
state.depthTest = !bounds.has_value();
|
||||
state.depthWrite = !bounds.has_value();
|
||||
state.depthCompare = VK_COMPARE_OP_ALWAYS;
|
||||
if (bounds) {
|
||||
state.depthBoundsTest = true;
|
||||
state.minDepthBounds = bounds->min;
|
||||
state.maxDepthBounds = bounds->max;
|
||||
}
|
||||
state.renderExtent = {Harness::Width, Harness::Height};
|
||||
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
state.viewport = {0, static_cast<float>(Harness::Height), static_cast<float>(Harness::Width), -static_cast<float>(Harness::Height), 0.0f, 1.0f};
|
||||
state.negativeOneToOne = false;
|
||||
state.scissor = {{0, 0}, {Harness::Width, Harness::Height}};
|
||||
state.cullMode = VK_CULL_MODE_NONE;
|
||||
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
|
||||
state.blend.colorWriteMask = 15;
|
||||
state.blends = {state.blend};
|
||||
state.blendConstants = {};
|
||||
|
||||
ShaderRecompiler::ShaderPixelStageInfo pixel{};
|
||||
pixel.wave32 = true;
|
||||
pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY);
|
||||
pixel.posX = true;
|
||||
pixel.posY = true;
|
||||
pixel.targetOutputMode[0] = 9;
|
||||
pixel.targetExportMapping.fill(0xe4u);
|
||||
if (bounds && emulate) {
|
||||
auto context = device.graphicsContext();
|
||||
context.depthBounds = false;
|
||||
context.depthRangeUnrestricted = false;
|
||||
AgcDriver::Graphics::ApplyDepthBoundsEmulation(context, state, pixel);
|
||||
Require(!state.depthBoundsTest && state.depthTest && !state.depthWrite, "the depth bounds test was not emulated");
|
||||
}
|
||||
|
||||
auto target = device.Target();
|
||||
target.depthRangeRemap = false;
|
||||
std::vector<std::uint32_t> vertexUserData(4, 0u);
|
||||
const auto vertexBuffer = Harness::BufferDescriptor(vertices.data(), 16u, static_cast<std::uint32_t>(vertices.size()));
|
||||
std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin());
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> vertexMemory{{{reinterpret_cast<std::uintptr_t>(Harness::VertexCode.data()), std::as_bytes(std::span(Harness::VertexCode))}}};
|
||||
ShaderRecompiler::RecompileRequest vertex{
|
||||
{ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(Harness::VertexCode.data()), Harness::VertexCode, 0, {}},
|
||||
{32, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory},
|
||||
target,
|
||||
{0, 0, 0, 64}
|
||||
};
|
||||
vertex.useCache = false;
|
||||
const auto vertexResult = ShaderRecompiler::Recompile(vertex);
|
||||
const auto vertexPush = static_cast<std::uint32_t>(vertexResult.pushConstants.size());
|
||||
std::vector<std::uint32_t> pixelUserData(4, 0u);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{reinterpret_cast<std::uintptr_t>(Harness::PixelCode.data()), std::as_bytes(std::span(Harness::PixelCode))}}};
|
||||
ShaderRecompiler::RecompileRequest fragment{
|
||||
{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(Harness::PixelCode.data()), Harness::PixelCode, 0, {}},
|
||||
{32, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory},
|
||||
target,
|
||||
PixelPushLayout(vertexPush, target)
|
||||
};
|
||||
fragment.useCache = false;
|
||||
const auto pixelResult = ShaderRecompiler::Recompile(fragment);
|
||||
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{
|
||||
{ShaderStage::Vertex, &vertexResult, 0},
|
||||
{ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)}
|
||||
}};
|
||||
const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(vertices.size()), 0, 1, 0, false};
|
||||
device.Draw(state, draw, shaders);
|
||||
device.WaitIdle();
|
||||
std::uint32_t covered = 0;
|
||||
for (std::size_t index = 0; index < Harness::Pixels.size() / 4u; ++index) {
|
||||
if (std::to_integer<std::uint8_t>(Harness::Pixels[index * 4u]) == 255u) ++covered;
|
||||
}
|
||||
return covered;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
const auto context = device->graphicsContext();
|
||||
const float below = std::nextafter(0.25f, 0.0f);
|
||||
const float above = std::nextafter(0.25f, 1.0f);
|
||||
const std::array<Case, 12> cases{{
|
||||
{0.25f, {0.0f, 0.25f}, true, "stored 0.25, upper bound equal to it"},
|
||||
{0.25f, {0.0f, below}, false, "stored 0.25, upper bound one step below it"},
|
||||
{0.25f, {0.25f, 1.0f}, true, "stored 0.25, lower bound equal to it"},
|
||||
{0.25f, {above, 1.0f}, false, "stored 0.25, lower bound one step above it"},
|
||||
{0.000125f, {0.0f, 0.000125f}, true, "stored 0.000125 inside [0, 0.000125]"},
|
||||
{0.5f, {0.0f, 0.000125f}, false, "stored 0.5 outside [0, 0.000125]"},
|
||||
{0.5f, {0.000125f, 1.0f}, true, "stored 0.5 inside [0.000125, 1]"},
|
||||
{0.0f, {0.000125f, 1.0f}, false, "stored 0 outside [0.000125, 1]"},
|
||||
{0.0f, {0.0f, 1.0f}, true, "stored 0 inside [0, 1]"},
|
||||
{1.0f, {0.0f, 1.0f}, true, "stored 1 inside [0, 1]"},
|
||||
{1.0f, {0.0f, 0.5f}, false, "stored 1 outside [0, 0.5]"},
|
||||
{0.75f, {0.5f, 1.0f}, true, "stored 0.75 inside [0.5, 1]"},
|
||||
}};
|
||||
std::size_t compared = 0;
|
||||
for (std::size_t index = 0; index < cases.size(); ++index) {
|
||||
const auto& test = cases[index];
|
||||
const auto depth = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(Depths[index].data()));
|
||||
Require(Draw(*device, depth, test.stored, std::nullopt, false) == Harness::Covered, "the draw that stores the depth did not cover the target");
|
||||
const auto expected = test.covered ? Harness::Covered : 0u;
|
||||
for (const bool emulate : {true, false}) {
|
||||
if (!emulate && !context.depthBounds) continue;
|
||||
const auto passed = Draw(*device, depth, 0.5f, test.bounds, emulate);
|
||||
if (passed != expected) {
|
||||
char detail[256];
|
||||
std::snprintf(detail, sizeof(detail), "%s (%s): %u of %u pixels passed, expected %s", test.what, emulate ? "emulated" : "native", passed, Harness::Covered, test.covered ? "all" : "none");
|
||||
throw std::runtime_error(detail);
|
||||
}
|
||||
if (!emulate) ++compared;
|
||||
}
|
||||
}
|
||||
std::printf("depth bounds emulation draw tests passed (%zu cases, %zu compared with the native test)\n", cases.size(), compared);
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -223,6 +223,65 @@ private:
|
||||
std::uint8_t* block = nullptr;
|
||||
};
|
||||
|
||||
class SparseLayers {
|
||||
public:
|
||||
SparseLayers() {
|
||||
#ifdef _WIN32
|
||||
memory = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocateAtOrAbove_nid_postfix(GuestArena::GuestArenaHighestFree_nid_no_patch(BlockBytes, 65536), BlockBytes, 65536));
|
||||
#else
|
||||
constexpr std::uintptr_t alignment = 65536;
|
||||
void* raw = mmap(nullptr, BlockBytes + alignment, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
Require(raw != MAP_FAILED, "cannot reserve sparse depth layers");
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(raw);
|
||||
const auto aligned = (begin + alignment - 1) & ~(alignment - 1);
|
||||
if (aligned != begin) munmap(raw, aligned - begin);
|
||||
if (aligned + BlockBytes != begin + BlockBytes + alignment) munmap(reinterpret_cast<void*>(aligned + BlockBytes), begin + alignment - aligned);
|
||||
memory = reinterpret_cast<std::uint8_t*>(aligned);
|
||||
#endif
|
||||
Require(memory != nullptr, "cannot reserve sparse depth layers");
|
||||
}
|
||||
|
||||
~SparseLayers() {
|
||||
#ifdef _WIN32
|
||||
GuestArena::GuestArenaReset_nid_postfix(memory, BlockBytes);
|
||||
GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(), BlockBytes);
|
||||
GuestArena::GuestArenaRelease_nid_postfix(memory, BlockBytes);
|
||||
#else
|
||||
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory, BlockBytes);
|
||||
munmap(memory, BlockBytes);
|
||||
GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(), BlockBytes);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Commit(std::size_t offset, std::size_t bytes) {
|
||||
#ifdef _WIN32
|
||||
GuestArena::GuestArenaCommit_nid_postfix(memory + offset, bytes, PAGE_READWRITE, 65536);
|
||||
#else
|
||||
Require(mmap(memory + offset, bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) != MAP_FAILED, "cannot commit sparse depth layer pages");
|
||||
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(memory + offset, bytes);
|
||||
#endif
|
||||
GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address() + offset, bytes);
|
||||
}
|
||||
|
||||
void DecommitGuestLayer() {
|
||||
#ifdef _WIN32
|
||||
GuestArena::GuestArenaReset_nid_postfix(memory + LayerBytes, LayerBytes);
|
||||
#else
|
||||
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory + LayerBytes, LayerBytes);
|
||||
Require(mmap(memory + LayerBytes, LayerBytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) != MAP_FAILED, "cannot decommit sparse depth layer pages");
|
||||
#endif
|
||||
GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address() + LayerBytes, LayerBytes);
|
||||
}
|
||||
|
||||
std::uint8_t* Data() const { return memory; }
|
||||
std::uint64_t Address() const { return reinterpret_cast<std::uint64_t>(memory); }
|
||||
SparseLayers(const SparseLayers&) = delete;
|
||||
SparseLayers& operator=(const SparseLayers&) = delete;
|
||||
|
||||
private:
|
||||
std::uint8_t* memory = nullptr;
|
||||
};
|
||||
|
||||
std::atomic<std::uint64_t> watchedBegin{0};
|
||||
std::atomic<std::uint64_t> watchedEnd{0};
|
||||
std::atomic<std::uint32_t> watchedReads{0};
|
||||
@@ -308,6 +367,59 @@ void Run(const Context& context, const WatchedBlock& block) {
|
||||
AgcDriver::GuestMemory::SetFlushHook(nullptr);
|
||||
}
|
||||
|
||||
void SparseGuestLayers(const Context& context) {
|
||||
ClearDepthSurfaces(context.device);
|
||||
SparseLayers block;
|
||||
block.Commit(0, LayerBytes);
|
||||
FillLayer(block.Data(), 1.0f);
|
||||
const auto address = block.Address();
|
||||
DepthSurfaceView(context, DepthTarget{address, 0, {Side, Side}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0});
|
||||
const auto resource = ArrayOver(address);
|
||||
watchedBegin = address + LayerBytes;
|
||||
watchedEnd = address + BlockBytes;
|
||||
AgcDriver::GuestMemory::SetFlushHook(&CountReads);
|
||||
const auto sample = [&](const char* when, bool changed) {
|
||||
AgcDriver::GuestMemory::BumpCollectEpoch();
|
||||
watchedReads = 0;
|
||||
Require(DepthSurfaceTexture(context, words, resource, components) != nullptr, "sparse depth layers were not sampled");
|
||||
Require((watchedReads.load() != 0) == changed, std::string("sparse depth layer read mismatch ") + when);
|
||||
};
|
||||
try {
|
||||
sample("at first use", true);
|
||||
sample("with the guest layer still uncommitted", false);
|
||||
block.Commit(LayerBytes, 65536);
|
||||
const float value = 0.25f;
|
||||
std::memcpy(block.Data() + LayerBytes, &value, sizeof(value));
|
||||
sample("after committing part of the guest layer", true);
|
||||
block.Data()[LayerBytes + 64] = 1;
|
||||
sample("after a CPU write to the committed part", true);
|
||||
AgcDriver::GuestMemory::MarkWritten(address + LayerBytes + 128, 4);
|
||||
sample("after a driver store to the committed part", true);
|
||||
block.DecommitGuestLayer();
|
||||
sample("after decommitting the guest layer", true);
|
||||
sample("with the guest layer still decommitted", false);
|
||||
block.Commit(LayerBytes, 65536);
|
||||
sample("after committing the same pages again", true);
|
||||
block.DecommitGuestLayer();
|
||||
block.Commit(LayerBytes, 65536);
|
||||
sample("after replacing a mapping with the same committed range", true);
|
||||
block.DecommitGuestLayer();
|
||||
block.Commit(LayerBytes, LayerBytes);
|
||||
FillLayer(block.Data() + LayerBytes, 0.5f);
|
||||
sample("after fully committing the guest layer", true);
|
||||
sample("with the fully committed layer unchanged", false);
|
||||
block.DecommitGuestLayer();
|
||||
sample("after decommitting a fully committed guest layer", true);
|
||||
sample("with the fully committed layer now empty", false);
|
||||
} catch (...) {
|
||||
AgcDriver::GuestMemory::SetFlushHook(nullptr);
|
||||
ClearDepthSurfaces(context.device);
|
||||
throw;
|
||||
}
|
||||
AgcDriver::GuestMemory::SetFlushHook(nullptr);
|
||||
ClearDepthSurfaces(context.device);
|
||||
}
|
||||
|
||||
void RecordedUploads(const Context& context, const WatchedBlock& block) {
|
||||
const auto resource = ArrayOver(block.Address());
|
||||
Recorder recorder(context);
|
||||
@@ -418,6 +530,7 @@ int main() {
|
||||
RecordedUploads(context, block);
|
||||
RunCubeGather(context, block);
|
||||
RunCubeTiledSlice(context, block);
|
||||
SparseGuestLayers(context);
|
||||
}
|
||||
device.reset();
|
||||
std::puts("depth plane guest layer tests passed");
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Submit/include/Acb.hpp"
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <initializer_list>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
void Require(bool condition, const std::string& message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
alignas(256) std::array<std::uint32_t, 7> CounterCode{0xf4080200u, 0xfa000000u, 0xbf8cc07fu, 0x7e000281u, 0xe0c80000u, 0x80020000u, 0xbf810000u};
|
||||
alignas(256) std::array<std::uint32_t, 7> StoreCode{0xf4080200u, 0xfa000000u, 0xbf8cc07fu, 0x7e000202u, 0xe0700000u, 0x80020000u, 0xbf810000u};
|
||||
alignas(256) std::array<std::uint32_t, 15> SpinCode{0xf4080200u, 0xfa000000u, 0xbe840381u, 0x9304ff04u, 0x0019660du, 0x8004ff04u, 0x3c6ef35fu, 0x80828102u, 0xbf078002u, 0xbf85fff9u, 0xbf8cc07fu, 0x7e000204u, 0xe0700000u, 0x80020000u, 0xbf810000u};
|
||||
|
||||
constexpr std::uint32_t Compute = 0x20u;
|
||||
constexpr std::uint32_t OtherCompute = 0x28u;
|
||||
constexpr std::uint32_t SpinIterations = 1u << 20u;
|
||||
|
||||
alignas(256) std::array<std::uint32_t, 4> Stale{};
|
||||
alignas(256) std::array<std::uint32_t, 4> Fresh{};
|
||||
alignas(256) std::array<std::uint32_t, 4> Target{};
|
||||
alignas(256) std::array<std::uint32_t, 4> TargetDescriptor{};
|
||||
alignas(256) std::array<std::uint32_t, 4> Spun{};
|
||||
alignas(256) std::array<std::uint32_t, 4> SpunDescriptor{};
|
||||
alignas(256) volatile std::uint32_t Gate = 1;
|
||||
alignas(256) volatile std::uint32_t Done = 0;
|
||||
alignas(256) volatile std::uint32_t OtherDone = 0;
|
||||
|
||||
enum class Window { None, CpuStore, QueuedGpuStore };
|
||||
|
||||
std::atomic<Window> Armed{Window::None};
|
||||
std::uint32_t SpinDispatches = 16;
|
||||
std::vector<std::uint32_t> OtherWords;
|
||||
bool SawPending = false;
|
||||
|
||||
std::uint32_t Low(const volatile void* pointer) {
|
||||
return static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(pointer));
|
||||
}
|
||||
|
||||
std::uint32_t High(const volatile void* pointer) {
|
||||
return static_cast<std::uint32_t>(static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(pointer)) >> 32u);
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 4> Descriptor(const void* pointer, std::uint32_t bytes) {
|
||||
return {Low(pointer), High(pointer) & 0xffffu, bytes, 0x31016facu};
|
||||
}
|
||||
|
||||
void SetShaderRegister(std::vector<std::uint32_t>& words, std::uint32_t offset, std::uint32_t value) {
|
||||
words.insert(words.end(), {0xc0017600u, offset, value});
|
||||
}
|
||||
|
||||
void Program(std::vector<std::uint32_t>& words, const void* code, std::initializer_list<std::uint32_t> userData) {
|
||||
const auto address = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(code));
|
||||
SetShaderRegister(words, 0x20c, static_cast<std::uint32_t>(address >> 8u));
|
||||
SetShaderRegister(words, 0x20d, static_cast<std::uint32_t>(address >> 40u) & 0xffu);
|
||||
for (const std::uint32_t offset : {0x207u, 0x208u, 0x209u}) SetShaderRegister(words, offset, 1);
|
||||
SetShaderRegister(words, 0x212, 0);
|
||||
SetShaderRegister(words, 0x213, static_cast<std::uint32_t>(userData.size()) << 1u);
|
||||
std::uint32_t offset = 0x240;
|
||||
for (const auto value : userData) SetShaderRegister(words, offset++, value);
|
||||
}
|
||||
|
||||
void Dispatch(std::vector<std::uint32_t>& words, const void* code, std::initializer_list<std::uint32_t> userData) {
|
||||
Program(words, code, userData);
|
||||
words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x8041u});
|
||||
}
|
||||
|
||||
void WriteData(std::vector<std::uint32_t>& words, const volatile std::uint32_t* target, std::uint32_t value) {
|
||||
words.insert(words.end(), {0xc0033700u, 0x00100200u, Low(target), High(target), value});
|
||||
}
|
||||
|
||||
void Submit(std::uint32_t queue, std::vector<std::uint32_t>& words) {
|
||||
Packet packet{words.data(), static_cast<std::uint32_t>(words.size()), 0, {}};
|
||||
Require(sceAgcDriverSubmitAcb(queue, &packet) == 0, "compute submit failed");
|
||||
}
|
||||
|
||||
void QueueStoreAhead() {
|
||||
OtherWords.clear();
|
||||
for (std::uint32_t index = 0; index < SpinDispatches; ++index) Dispatch(OtherWords, SpinCode.data(), {Low(SpunDescriptor.data()), High(SpunDescriptor.data()), SpinIterations});
|
||||
Dispatch(OtherWords, StoreCode.data(), {Low(TargetDescriptor.data()), High(TargetDescriptor.data()), Low(Fresh.data())});
|
||||
WriteData(OtherWords, &OtherDone, 1);
|
||||
Submit(OtherCompute, OtherWords);
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
while (std::chrono::steady_clock::now() - start < std::chrono::seconds(5) && OtherDone == 0) {
|
||||
if (AgcDriver::DriverDetail::Driver::QueryPendingSpan(reinterpret_cast<std::uintptr_t>(Target.data()), sizeof(Target), {}) != AgcDriver::ShaderMemory::PendingWrite::None) {
|
||||
SawPending = true;
|
||||
return;
|
||||
}
|
||||
std::this_thread::yield();
|
||||
}
|
||||
}
|
||||
|
||||
void BeforeAdoption() {
|
||||
const auto window = Armed.exchange(Window::None);
|
||||
if (window == Window::CpuStore) reinterpret_cast<volatile std::uint32_t&>(Target[0]) = Low(Fresh.data());
|
||||
else if (window == Window::QueuedGpuStore) QueueStoreAhead();
|
||||
}
|
||||
|
||||
void WaitIdle() {
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
for (const auto* buffer : {&Stale, &Fresh, &Target}) AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast<std::uintptr_t>(buffer->data()), sizeof(*buffer));
|
||||
}
|
||||
|
||||
void Reset() {
|
||||
WaitIdle();
|
||||
Stale = {};
|
||||
Fresh = {};
|
||||
Target = Descriptor(Stale.data(), sizeof(Stale));
|
||||
Done = 0;
|
||||
OtherDone = 0;
|
||||
SawPending = false;
|
||||
}
|
||||
|
||||
void Gated(Window window) {
|
||||
Reset();
|
||||
std::vector<std::uint32_t> words;
|
||||
words.insert(words.end(), {0xc0053c00u, 0x13u, Low(&Gate), High(&Gate), 1u, 0xffffffffu, 0x19u});
|
||||
Dispatch(words, CounterCode.data(), {Low(Target.data()), High(Target.data())});
|
||||
WriteData(words, &Done, 1);
|
||||
Armed = window;
|
||||
Submit(Compute, words);
|
||||
WaitIdle();
|
||||
Armed = Window::None;
|
||||
Require(Done == 1, "the gated submission did not finish");
|
||||
}
|
||||
|
||||
std::string Counts() {
|
||||
return "(stale " + std::to_string(Stale[0]) + ", fresh " + std::to_string(Fresh[0]) + ")";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
device.reset();
|
||||
Require(High(Stale.data()) == High(Fresh.data()), "the stale and fresh buffers differ in their high address word");
|
||||
TargetDescriptor = Descriptor(Target.data(), sizeof(Target));
|
||||
SpunDescriptor = Descriptor(Spun.data(), sizeof(Spun));
|
||||
AgcDriverSetAheadAdoptHook_nid_postfix(&BeforeAdoption);
|
||||
Reset();
|
||||
std::vector<std::uint32_t> warmUp;
|
||||
Dispatch(warmUp, CounterCode.data(), {Low(Target.data()), High(Target.data())});
|
||||
WriteData(warmUp, &Done, 1);
|
||||
Submit(Compute, warmUp);
|
||||
std::vector<std::uint32_t> otherWarmUp;
|
||||
Dispatch(otherWarmUp, SpinCode.data(), {Low(SpunDescriptor.data()), High(SpunDescriptor.data()), SpinIterations});
|
||||
Dispatch(otherWarmUp, StoreCode.data(), {Low(TargetDescriptor.data()), High(TargetDescriptor.data()), Low(Fresh.data())});
|
||||
Submit(OtherCompute, otherWarmUp);
|
||||
WaitIdle();
|
||||
Require(Done == 1 && Target[0] == Low(Fresh.data()), "the warm-up dispatches did not run");
|
||||
|
||||
Gated(Window::None);
|
||||
Require(Stale[0] == 1 && Fresh[0] == 0, "the gated dispatch without a store in the window did not count into the stale buffer " + Counts());
|
||||
const auto adopted = AgcDriverDispatchesAdoptedAhead_nid_postfix();
|
||||
Require(adopted != 0, "the gated dispatch was not resolved ahead and adopted");
|
||||
|
||||
auto rejected = AgcDriverDispatchesRejectedAhead_nid_postfix();
|
||||
Gated(Window::CpuStore);
|
||||
Require(Fresh[0] == 1 && Stale[0] == 0, "a dispatch resolved ahead whose descriptor the CPU replaced before its adoption used the old descriptor " + Counts());
|
||||
Require(AgcDriverDispatchesRejectedAhead_nid_postfix() == rejected + 1, "the CPU store in the adoption window was not caught by the adoption check");
|
||||
std::cout << "cpu store before adoption: passed\n";
|
||||
|
||||
bool conclusive = false;
|
||||
for (int attempt = 0; attempt < 4 && !conclusive; ++attempt, SpinDispatches *= 4) {
|
||||
rejected = AgcDriverDispatchesRejectedAhead_nid_postfix();
|
||||
Gated(Window::QueuedGpuStore);
|
||||
conclusive = SawPending;
|
||||
if (!conclusive) {
|
||||
std::cout << "queued gpu store before adoption: the store finished before the adoption with " << SpinDispatches << " spin dispatches, retrying\n";
|
||||
continue;
|
||||
}
|
||||
Require(Fresh[0] == 1 && Stale[0] == 0, "a dispatch resolved ahead whose descriptor a GPU store queued on another queue replaces before its adoption used the old descriptor " + Counts());
|
||||
Require(AgcDriverDispatchesRejectedAhead_nid_postfix() == rejected + 1, "the queued GPU store in the adoption window was not caught by the adoption check");
|
||||
}
|
||||
AgcDriverSetAheadAdoptHook_nid_postfix(nullptr);
|
||||
if (!conclusive) {
|
||||
std::cout << "queued gpu store before adoption: never observed the store pending at the adoption\n";
|
||||
AgcDriverShutdown_nid_postfix();
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
std::cout << "queued gpu store before adoption: passed\n";
|
||||
std::cout << "dispatch ahead adopt check test passed\n";
|
||||
AgcDriverShutdown_nid_postfix();
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
try {
|
||||
AgcDriverShutdown_nid_postfix();
|
||||
} catch (const std::exception& shutdownError) {
|
||||
std::cerr << shutdownError.what() << '\n';
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,12 @@ alignas(256) constexpr std::array<std::uint32_t, 37> Code{
|
||||
0xe0781020, 0x80011203, 0xe0781030, 0x80011603, 0xbf810000,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 20> AccumulatorCode{
|
||||
0x34020084, 0x34060086, 0xe0381000, 0x80000401, 0xbf8c3f70, 0xcc13600a, 0x1c1a0b04, 0xcc13000b,
|
||||
0x1c1a0b04, 0xcc13440c, 0x1c1a0b04, 0xcc13400d, 0x9c1a0b04, 0xcc13440e, 0x9c1a0b04, 0xe0781000,
|
||||
0x80010a03, 0xe0701010, 0x80010e03, 0xbf810000,
|
||||
};
|
||||
|
||||
constexpr std::uint32_t Rows[32][4] = {
|
||||
{0x0001bc00u, 0x81bfacbau, 0xc541aa2fu, 0x00000000u},
|
||||
{0x01f61a79u, 0x3c000381u, 0xc1729fffu, 0x00000000u},
|
||||
@@ -101,6 +107,43 @@ constexpr std::uint32_t Expected[32][10] = {
|
||||
{0xbaf43c38u, 0xbaf43c38u, 0x3af43c38u, 0x3af43c38u, 0x3abe63ecu, 0xbaf43c38u, 0xc021e000u, 0x3fb50000u, 0xba90cd60u, 0x3abe63ecu}
|
||||
};
|
||||
|
||||
constexpr std::uint32_t AccumulatorExpected[32][5] = {
|
||||
{0xc541a901u, 0xc541a901u, 0x4541ab5du, 0x4541ab5du, 0xc541a901u},
|
||||
{0xc1729fdfu, 0xc1729fdfu, 0x4172a01fu, 0x4172a01fu, 0xc1729fdfu},
|
||||
{0x4187e315u, 0x4187e315u, 0x4187e315u, 0xc1882737u, 0xc1882737u},
|
||||
{0x3f7fdb0fu, 0x3f7fdb0fu, 0x3f7fdb0fu, 0xbf801279u, 0xbf801279u},
|
||||
{0xbe511687u, 0xbe511687u, 0xbe511687u, 0xbe5166f4u, 0xbe5166f4u},
|
||||
{0xc1e56f13u, 0xc1e56f13u, 0x41e55ba5u, 0x41e55ba5u, 0xc1e56f13u},
|
||||
{0x439a1228u, 0x439a1228u, 0x439a1228u, 0xc3a5d2acu, 0xc3a5d2acu},
|
||||
{0x37f5fb14u, 0x37f5fb14u, 0x37f5fc0cu, 0x37f5fc0cu, 0x37f5fb14u},
|
||||
{0x7fd0a000u, 0x7fd0a000u, 0x7fd0a000u, 0x7fd0a000u, 0x7fd0a000u},
|
||||
{0xffc8ab86u, 0xffc8ab86u, 0x7fc8ab86u, 0x7fc8ab86u, 0xffc8ab86u},
|
||||
{0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u},
|
||||
{0xffc00000u, 0xffc00000u, 0xffc00000u, 0xffc00000u, 0xffc00000u},
|
||||
{0xb974e480u, 0xb974e480u, 0xb974e480u, 0xb974e480u, 0xb974e480u},
|
||||
{0x7f800000u, 0x7f800000u, 0x7f800000u, 0xff800000u, 0xff800000u},
|
||||
{0xff800000u, 0xff800000u, 0xff800000u, 0xff800000u, 0xff800000u},
|
||||
{0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x80000000u},
|
||||
{0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u},
|
||||
{0x7fc42000u, 0x7fc42000u, 0x7fc42000u, 0x7fc42000u, 0x7fc42000u},
|
||||
{0xbf052ae0u, 0xbf052ae0u, 0xbf052ae0u, 0xbf052ae0u, 0xbf052ae0u},
|
||||
{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u},
|
||||
{0x5f834da7u, 0x5f834da7u, 0x5f834da7u, 0xdf834da7u, 0xdf834da7u},
|
||||
{0x7f7fffffu, 0x7f7fffffu, 0x7f7fffffu, 0xff7fffffu, 0xff7fffffu},
|
||||
{0x42750eeeu, 0x42750eeeu, 0x42750eeeu, 0xc2750eeeu, 0xc2750eeeu},
|
||||
{0x43380748u, 0x43380748u, 0x43380748u, 0xc3380746u, 0xc3380746u},
|
||||
{0xffc00000u, 0xffc00000u, 0xffc00000u, 0xffc00000u, 0xffc00000u},
|
||||
{0xf540cddbu, 0xf540cddbu, 0x7540cddbu, 0x7540cddbu, 0xf540cddbu},
|
||||
{0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u},
|
||||
{0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0xffe00000u, 0xffe00000u},
|
||||
{0x3cb7c720u, 0x3cb7c720u, 0x3cb7c720u, 0x3cb7c720u, 0x3cb7c720u},
|
||||
{0xbf9bd158u, 0xbf9bd158u, 0xbf9bd158u, 0xbf9bd158u, 0xbf9bd158u},
|
||||
{0x565f49ebu, 0x565f49ebu, 0x565f49ebu, 0xd65f49ebu, 0xd65f49ebu},
|
||||
{0xbaf43c38u, 0xbaf43c38u, 0xbaf43c38u, 0xbaf43c38u, 0xbaf43c38u}
|
||||
};
|
||||
|
||||
constexpr const char* AccumulatorNames[5] = {"v_dot2 op_sel src2", "v_dot2 op_sel_hi src2 cleared", "v_dot2 neg_hi src2", "v_dot2 neg_lo src2", "v_dot2 neg_lo and neg_hi src2"};
|
||||
|
||||
constexpr const char* Names[10] = {"v_dot2c", "v_dot2", "v_dot2 neg_lo src0/src2 neg_hi src1", "v_dot2 neg_lo src1 neg_hi src0", "v_dot2 op_sel src0", "v_dot2 clamp", "v_dot2c inline 1.0", "v_dot2c literal", "v_dot2c dpp quad_perm", "v_dot2 op_sel src1"};
|
||||
|
||||
void Fill(std::uint32_t tid, std::uint32_t* words) {
|
||||
@@ -140,21 +183,9 @@ auto Compile(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> cod
|
||||
return ShaderRecompiler::Recompile(request);
|
||||
}
|
||||
|
||||
void CheckRefused(AgcDriver::VulkanDevice& device, std::uint32_t word0, const std::string& what) {
|
||||
alignas(256) const std::array<std::uint32_t, 3> code{word0, 0x1c1a0b04u, 0xbf810000u};
|
||||
std::string refusal;
|
||||
try {
|
||||
static_cast<void>(Compile(device, code));
|
||||
} catch (const std::exception& error) {
|
||||
refusal = error.what();
|
||||
}
|
||||
Require(refusal.find("v_dot2_f32_f16 accumulator op_sel, op_sel_hi and neg_hi are not implemented") != std::string::npos, what + " was not refused");
|
||||
}
|
||||
|
||||
void Run(AgcDriver::VulkanDevice& device) {
|
||||
void Run(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code) {
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) Fill(tid, &Input[tid * Inputs]);
|
||||
Output.fill(0xdeadbeefu);
|
||||
const std::span<const std::uint32_t> code(Code);
|
||||
const auto result = Compile(device, code);
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
|
||||
device.WaitIdle();
|
||||
@@ -167,6 +198,13 @@ void Check() {
|
||||
}
|
||||
}
|
||||
|
||||
void CheckAccumulator() {
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
const std::uint32_t* out = &Output[tid * Results];
|
||||
for (std::uint32_t i = 0; i < 5; ++i) Expect(tid, out[i], AccumulatorExpected[tid][i], AccumulatorNames[i]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
@@ -177,11 +215,10 @@ int main() {
|
||||
std::puts("skipped, v_dot2_f32_f16 is computed in f64 and the device has no shaderFloat64");
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
Run(*device);
|
||||
Run(*device, Code);
|
||||
Check();
|
||||
CheckRefused(*device, 0xcc13600au, "op_sel on the accumulator");
|
||||
CheckRefused(*device, 0xcc13440au, "neg_hi on the accumulator");
|
||||
CheckRefused(*device, 0xcc13000au, "op_sel_hi cleared on the accumulator");
|
||||
Run(*device, AccumulatorCode);
|
||||
CheckAccumulator();
|
||||
std::puts("v_dot2_f32_f16 tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Threads = 32;
|
||||
constexpr std::uint32_t Width = 32;
|
||||
constexpr std::uint32_t Type1D = 8;
|
||||
constexpr std::uint32_t IdentitySwizzle = 0xfacu;
|
||||
constexpr std::size_t TexelBytes = 4096;
|
||||
constexpr std::size_t BlockBytes = 65536;
|
||||
|
||||
alignas(256) std::array<std::uint32_t, Threads * 4> Input{};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 17> StoreCode{
|
||||
0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0x7e3c0300, 0x7e3e0281, 0x7e400280, 0xf0201f28, 0x0001141e,
|
||||
0x7e3e0280, 0x7e4002c1, 0xf0201528, 0x0001141e, 0x7e400281, 0xf0201528, 0x0001141e, 0xbf810000,
|
||||
};
|
||||
|
||||
class GuestBlock {
|
||||
public:
|
||||
GuestBlock() {
|
||||
#ifdef _WIN32
|
||||
block = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
#else
|
||||
block = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, BlockBytes));
|
||||
#endif
|
||||
Require(block != nullptr, "image store line through plane array: cannot allocate the guest block");
|
||||
GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true);
|
||||
}
|
||||
|
||||
~GuestBlock() {
|
||||
GuestAllocations::Mutation().Remove(block);
|
||||
#ifdef _WIN32
|
||||
VirtualFree(block, 0, MEM_RELEASE);
|
||||
#else
|
||||
std::free(block);
|
||||
#endif
|
||||
}
|
||||
|
||||
GuestBlock(const GuestBlock&) = delete;
|
||||
GuestBlock& operator=(const GuestBlock&) = delete;
|
||||
|
||||
std::uint8_t* Data() { return block; }
|
||||
|
||||
private:
|
||||
std::uint8_t* block = nullptr;
|
||||
};
|
||||
|
||||
struct Format {
|
||||
const char* name;
|
||||
std::uint32_t format;
|
||||
std::uint32_t components;
|
||||
};
|
||||
|
||||
constexpr std::array<Format, 2> Formats{{
|
||||
{"32_SINT", 21u, 1u},
|
||||
{"32_32_32_32_SINT", 76u, 4u},
|
||||
}};
|
||||
|
||||
std::string Hex(std::uint32_t value) {
|
||||
char text[16];
|
||||
std::snprintf(text, sizeof(text), "0x%08x", value);
|
||||
return text;
|
||||
}
|
||||
|
||||
std::uint32_t Value(std::uint32_t tid, std::uint32_t index) {
|
||||
return (tid * 4u + index + 1u) * 0x9e3779b1u;
|
||||
}
|
||||
|
||||
std::uint64_t TexelOffset(const Format& format, std::uint32_t x) {
|
||||
const auto mip = AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, format.format, Width, 1u, 1u).at(0);
|
||||
return mip.tiledOffset + static_cast<std::uint64_t>(x) * format.components * 4u;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> Initial() {
|
||||
std::vector<std::uint8_t> bytes(TexelBytes);
|
||||
for (std::size_t index = 0; index < bytes.size(); ++index) bytes[index] = static_cast<std::uint8_t>(0x80u + 37u * index + 11u * (index >> 4u));
|
||||
return bytes;
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu};
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 8> TextureDescriptor(const void* data, std::uint32_t format) {
|
||||
const auto address = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(data));
|
||||
return {
|
||||
static_cast<std::uint32_t>(address >> 8u),
|
||||
static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (format << 20u) | (((Width - 1u) & 3u) << 30u),
|
||||
(Width - 1u) >> 2u,
|
||||
IdentitySwizzle | (Type1D << 28u),
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
};
|
||||
}
|
||||
|
||||
void Dispatch(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::span<const std::uint32_t> code, std::uint32_t format) {
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
for (std::uint32_t index = 0; index < 4u; ++index) Input[tid * 4u + index] = Value(tid, index);
|
||||
}
|
||||
const auto initial = Initial();
|
||||
std::copy(initial.begin(), initial.end(), texels);
|
||||
std::vector<std::uint32_t> userData(16, 0u);
|
||||
const auto buffer = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size() * 4u));
|
||||
const auto texture = TextureDescriptor(texels, format);
|
||||
std::copy(buffer.begin(), buffer.end(), userData.begin());
|
||||
std::copy(texture.begin(), texture.end(), userData.begin() + 4);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1};
|
||||
ShaderRecompiler::RecompileRequest request{
|
||||
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
|
||||
{32, 0, userData, compute, std::nullopt, std::nullopt, memory},
|
||||
device.Target(),
|
||||
{0, 0, 0, 128}
|
||||
};
|
||||
request.useCache = false;
|
||||
const auto result = ShaderRecompiler::Recompile(request);
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
|
||||
device.WaitIdle();
|
||||
AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast<std::uintptr_t>(texels), TexelBytes, nullptr, "test");
|
||||
device.WaitIdle();
|
||||
}
|
||||
|
||||
void CheckTexels(const std::uint8_t* texels, const Format& format) {
|
||||
const auto initial = Initial();
|
||||
for (std::uint32_t x = 0; x < Width; ++x) {
|
||||
const auto offset = TexelOffset(format, x);
|
||||
for (std::uint32_t component = 0; component < format.components; ++component) {
|
||||
std::uint32_t actual = 0;
|
||||
std::memcpy(&actual, texels + offset + component * 4u, 4u);
|
||||
const auto wanted = Value(x, component);
|
||||
Require(actual == wanted, std::string("image_store 2d_array through a 1D ") + format.name + " T#: texel " + std::to_string(x) + " component " + std::to_string(component) + " is " + Hex(actual) + ", expected the y = 1, slice = 0 store " + Hex(wanted) + " and the stores to slices 0xffffffff and 1 dropped");
|
||||
}
|
||||
}
|
||||
const auto end = TexelOffset(format, Width);
|
||||
Require(std::equal(texels + end, texels + TexelBytes, initial.begin() + static_cast<std::ptrdiff_t>(end)), std::string("image_store 2d_array through a 1D ") + format.name + " T#: bytes past the texture changed");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
GuestBlock block;
|
||||
auto* texels = block.Data();
|
||||
for (const auto& format : Formats) {
|
||||
Dispatch(*device, texels, StoreCode, format.format);
|
||||
CheckTexels(texels, format);
|
||||
}
|
||||
std::puts("image store line through plane array tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
#include "prx/libkernel/include/Api.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp"
|
||||
#include "prx/libSceAgcDriver/Eq/include/Event.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "prx/libc/include/GuestArena.hpp"
|
||||
#include "prx/libc/include/GuestWriteWatch.hpp"
|
||||
#include "prx/libkernel/Equeue/Equeue.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <bit>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <initializer_list>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
struct VulkanDeviceTestAccess {
|
||||
static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); }
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
|
||||
constexpr std::uint32_t Width = 256;
|
||||
constexpr std::uint32_t Height = 128;
|
||||
constexpr std::size_t Pitch = std::size_t{Width} * 4u;
|
||||
constexpr std::size_t TargetBytes = Pitch * Height;
|
||||
constexpr std::size_t BlockBytes = 65536;
|
||||
constexpr std::size_t LabelOffset = 3 * BlockBytes;
|
||||
constexpr std::uint32_t Untouched = 0x5a5a5a5au;
|
||||
constexpr std::uint32_t Drawn = 0xff0000ffu;
|
||||
constexpr std::uint32_t LabelValue = 0x600dcafeu;
|
||||
constexpr std::uint32_t OutputHandle = 9;
|
||||
constexpr std::uint32_t Rgba8UnormInfo = 0x28028u;
|
||||
constexpr std::uint32_t LinearAttrib3 = 0x09000000u;
|
||||
constexpr std::array<std::uint32_t, 4> Scissor{32, 16, 160, 80};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 20> VertexCode{
|
||||
0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u,
|
||||
0x7e060208u, 0x7e0802f2u, 0x7e0a02ffu, 0x44800000u, 0xf80000d1u, 0x00000005u, 0xf80008cfu, 0x04030201u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u,
|
||||
};
|
||||
alignas(256) constexpr std::array<std::uint32_t, 8> PixelCode{0x7e000200u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u};
|
||||
alignas(256) constexpr std::array<std::uint32_t, 9> FillCode{
|
||||
0xd7460004, 0x04010c08, 0x7e000204, 0x7e020205, 0x7e040206, 0x7e060207, 0xe01c2000, 0x80000004, 0xbf810000,
|
||||
};
|
||||
|
||||
alignas(4) std::atomic<std::uint32_t> Gate = 0;
|
||||
|
||||
struct Header {
|
||||
Shader shader{};
|
||||
ShaderUserData userData{};
|
||||
};
|
||||
|
||||
alignas(8) Header VertexHeader{};
|
||||
alignas(8) Header PixelHeader{};
|
||||
|
||||
void Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) {
|
||||
header.shader.file_header = 0x34333231u;
|
||||
header.shader.version = 0x18u;
|
||||
header.shader.header_size = sizeof(Header);
|
||||
header.shader.shader_size = static_cast<std::uint32_t>(bytes);
|
||||
header.shader.code = code;
|
||||
header.shader.type = type;
|
||||
header.shader.user_data = &header.userData;
|
||||
AgcDriverRegisterShader_nid_postfix(&header.shader);
|
||||
}
|
||||
|
||||
void Append(std::vector<std::uint32_t>& commands, std::uint32_t opcode, std::initializer_list<std::uint32_t> payload) {
|
||||
commands.push_back(0xc0000000u | (static_cast<std::uint32_t>(payload.size() - 1) << 16u) | (opcode << 8u));
|
||||
commands.insert(commands.end(), payload);
|
||||
}
|
||||
|
||||
void AppendDraw(std::vector<std::uint32_t>& commands, std::uint64_t target) {
|
||||
const auto vertex = reinterpret_cast<std::uintptr_t>(VertexCode.data());
|
||||
const auto pixel = reinterpret_cast<std::uintptr_t>(PixelCode.data());
|
||||
const auto extent = (Height << 16u) | Width;
|
||||
const std::vector<std::pair<std::uint32_t, std::uint32_t>> context{
|
||||
{0x2d5, 0x2000},
|
||||
{0x1b6, 0}, {0x207, 0x01200000}, {0x200, 0}, {0x203, 0x800},
|
||||
{0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0},
|
||||
{0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa},
|
||||
{0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff},
|
||||
{0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240},
|
||||
{0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010},
|
||||
{0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, Rgba8UnormInfo},
|
||||
{0x31b, 0}, {0x31d, 0}, {0x3b0, ((Width - 1u) << 14u) | (Height - 1u)},
|
||||
{0x3b8, LinearAttrib3}, {0x1e0, 0},
|
||||
{0xc, 0}, {0xd, extent},
|
||||
{0x81, 0x80000000}, {0x82, extent},
|
||||
{0x90, 0x80000000u | (Scissor[1] << 16u) | Scissor[0]}, {0x91, (Scissor[3] << 16u) | Scissor[2]},
|
||||
{0x94, 0x80000000}, {0x95, extent},
|
||||
{0x10f, std::bit_cast<std::uint32_t>(2.0f * Width)}, {0x110, std::bit_cast<std::uint32_t>(2.0f * Width)},
|
||||
{0x111, std::bit_cast<std::uint32_t>(2.0f * Height)}, {0x112, std::bit_cast<std::uint32_t>(2.0f * Height)},
|
||||
{0x113, std::bit_cast<std::uint32_t>(1.0f)}, {0x114, 0},
|
||||
{0xb4, 0}, {0xb5, std::bit_cast<std::uint32_t>(1.0f)},
|
||||
{0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0},
|
||||
{0x318, static_cast<std::uint32_t>(target >> 8u)}, {0x390, static_cast<std::uint32_t>(target >> 40u)},
|
||||
};
|
||||
const std::vector<std::pair<std::uint32_t, std::uint32_t>> shader{
|
||||
{0xc8, static_cast<std::uint32_t>(vertex >> 8u)}, {0xc9, static_cast<std::uint32_t>(vertex >> 40u)}, {0x8b, 1u << 1u}, {0x8c, 0},
|
||||
{0x008, static_cast<std::uint32_t>(pixel >> 8u)}, {0x009, static_cast<std::uint32_t>(pixel >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast<std::uint32_t>(1.0f)},
|
||||
};
|
||||
Append(commands, 0x79, {0x242, 4});
|
||||
Append(commands, 0x79, {0x24a, 0});
|
||||
for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value});
|
||||
for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value});
|
||||
Append(commands, 0x2d, {3, 2});
|
||||
}
|
||||
|
||||
void AppendGate(std::vector<std::uint32_t>& commands) {
|
||||
const auto gate = reinterpret_cast<std::uintptr_t>(&Gate);
|
||||
commands.insert(commands.end(), {0xc0053c00u, 0x13u, static_cast<std::uint32_t>(gate), static_cast<std::uint32_t>(gate >> 32u), 1u, 0xffffffffu, 0x19u});
|
||||
}
|
||||
|
||||
void Submit(std::vector<std::uint32_t>& commands) {
|
||||
Packet packet{commands.data(), static_cast<std::uint32_t>(commands.size()), 0, {}};
|
||||
AgcDriver::Submit(&packet, 0);
|
||||
}
|
||||
|
||||
class Region {
|
||||
public:
|
||||
Region(std::size_t bytes, bool watched) : bytes(bytes), arena(AgcDriver::GuestMemory::WriteWatched()) {
|
||||
#ifdef _WIN32
|
||||
if (arena) {
|
||||
data = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes));
|
||||
if (data != nullptr) GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
}
|
||||
#else
|
||||
if (arena) {
|
||||
void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
Require(raw != MAP_FAILED, "linear target completion points: cannot map the guest region");
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(raw);
|
||||
const auto aligned = (begin + BlockBytes - 1) & ~(static_cast<std::uintptr_t>(BlockBytes) - 1);
|
||||
if (aligned != begin) munmap(raw, aligned - begin);
|
||||
if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast<void*>(aligned + bytes), begin + BlockBytes - aligned);
|
||||
data = reinterpret_cast<std::uint8_t*>(aligned);
|
||||
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, bytes));
|
||||
}
|
||||
#endif
|
||||
Require(data != nullptr, "linear target completion points: cannot allocate the guest region");
|
||||
auto* words = reinterpret_cast<std::uint32_t*>(data);
|
||||
std::fill(words, words + bytes / 4u, Untouched);
|
||||
if (arena && !watched) {
|
||||
AgcDriver::GuestMemory::Unwatch(Address(), bytes);
|
||||
Require(!AgcDriver::GuestMemory::Watched(Address(), bytes), "linear target completion points: the region is still write-watched");
|
||||
}
|
||||
GuestAllocations::Mutation().Add(data, bytes, true, true, true);
|
||||
}
|
||||
~Region() {
|
||||
GuestAllocations::Mutation().Remove(data);
|
||||
#ifdef _WIN32
|
||||
if (arena) {
|
||||
GuestArena::GuestArenaReset_nid_postfix(data, bytes);
|
||||
GuestArena::GuestArenaRelease_nid_postfix(data, bytes);
|
||||
} else {
|
||||
VirtualFree(data, 0, MEM_RELEASE);
|
||||
}
|
||||
#else
|
||||
if (arena) {
|
||||
munmap(data, bytes);
|
||||
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
std::free(data);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
Region(const Region&) = delete;
|
||||
Region& operator=(const Region&) = delete;
|
||||
std::uint64_t Address() const { return reinterpret_cast<std::uintptr_t>(data); }
|
||||
std::uint32_t Word(std::size_t offset) const { return std::atomic_ref<std::uint32_t>(*reinterpret_cast<std::uint32_t*>(data + offset)).load(std::memory_order_acquire); }
|
||||
std::size_t bytes;
|
||||
|
||||
private:
|
||||
bool arena;
|
||||
std::uint8_t* data = nullptr;
|
||||
};
|
||||
|
||||
void Import(const Region& region) {
|
||||
const auto address = region.Address();
|
||||
const std::vector<std::uint32_t> userData{
|
||||
static_cast<std::uint32_t>(address),
|
||||
static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (16u << 16u),
|
||||
static_cast<std::uint32_t>(region.bytes / 16u),
|
||||
0x4bfacu,
|
||||
Untouched,
|
||||
Untouched,
|
||||
Untouched,
|
||||
Untouched,
|
||||
};
|
||||
std::vector<std::uint32_t> commands;
|
||||
const auto code = reinterpret_cast<std::uintptr_t>(FillCode.data());
|
||||
Append(commands, 0x76, {0x207, 64, 1, 1});
|
||||
Append(commands, 0x76, {0x20c, static_cast<std::uint32_t>(code >> 8u), static_cast<std::uint32_t>(code >> 40u)});
|
||||
Append(commands, 0x76, {0x213, (static_cast<std::uint32_t>(userData.size()) << 1u) | 0x80u});
|
||||
commands.push_back(0xc0007600u | (static_cast<std::uint32_t>(userData.size()) << 16u));
|
||||
commands.push_back(0x240u);
|
||||
commands.insert(commands.end(), userData.begin(), userData.end());
|
||||
commands.insert(commands.end(), {0xc0031500u, static_cast<std::uint32_t>(region.bytes / 1024u), 1, 1, 0x41u});
|
||||
Submit(commands);
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
}
|
||||
|
||||
bool InsideScissor(std::uint32_t x, std::uint32_t y) {
|
||||
return x >= Scissor[0] && x < Scissor[2] && y >= Scissor[1] && y < Scissor[3];
|
||||
}
|
||||
|
||||
std::string Mismatches(const Region& region) {
|
||||
std::size_t drawn = 0, outside = 0;
|
||||
for (std::uint32_t y = 0; y < Height; ++y) {
|
||||
for (std::uint32_t x = 0; x < Width; ++x) {
|
||||
const auto value = region.Word(y * Pitch + x * 4u);
|
||||
if (InsideScissor(x, y)) drawn += value == Drawn ? 0u : 1u;
|
||||
else outside += value == Untouched ? 0u : 1u;
|
||||
}
|
||||
}
|
||||
if (drawn == 0 && outside == 0) return {};
|
||||
return std::to_string(drawn) + " of the " + std::to_string((Scissor[2] - Scissor[0]) * (Scissor[3] - Scissor[1])) + " pixels inside the scissor do not hold the draw's color and " + std::to_string(outside) + " pixels outside it changed";
|
||||
}
|
||||
|
||||
bool Await(const std::function<bool()>& done) {
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
||||
while (!done()) {
|
||||
if (std::chrono::steady_clock::now() >= deadline) return false;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
class Request final : public AgcDriver::IFlipRequest {
|
||||
public:
|
||||
explicit Request(std::atomic<int>& ready) : ready(ready) {}
|
||||
void GpuReady(const std::shared_ptr<AgcDriver::FrameTiming>&) override { ++ready; }
|
||||
void Fail(std::exception_ptr) noexcept override {}
|
||||
private:
|
||||
std::atomic<int>& ready;
|
||||
};
|
||||
|
||||
class Output final : public AgcDriver::IVideoOutput {
|
||||
public:
|
||||
std::shared_ptr<AgcDriver::IFlipRequest> Reserve(const AgcDriver::FlipInfo&) override { return std::make_shared<Request>(ready); }
|
||||
void Fail(std::exception_ptr) noexcept override {}
|
||||
std::atomic<int> ready = 0;
|
||||
};
|
||||
|
||||
enum class Point { Flip, Label, Interrupt };
|
||||
|
||||
struct Events {
|
||||
KernelEqueue queue = 0;
|
||||
KernelEqueueRef owner;
|
||||
int tag = 0;
|
||||
};
|
||||
|
||||
std::string Run(Point point, bool watched, const Region& region, Output& output, Events& events) {
|
||||
const char* name = point == Point::Flip ? "flip" : point == Point::Label ? "label" : "end-of-pipe interrupt";
|
||||
const auto target = region.Address();
|
||||
Import(region);
|
||||
Gate.store(0, std::memory_order_release);
|
||||
std::vector<std::uint32_t> commands;
|
||||
AppendDraw(commands, target);
|
||||
if (point == Point::Flip) {
|
||||
commands.insert(commands.end(), {AgcDriver::FlipPacketHeader, OutputHandle, 0xfffffffeu, 1, 0, 0});
|
||||
} else if (point == Point::Label) {
|
||||
const auto label = target + LabelOffset;
|
||||
commands.insert(commands.end(), {0xc0064900u, 0x514u, 1u << 29u, static_cast<std::uint32_t>(label), static_cast<std::uint32_t>(label >> 32u), LabelValue, 0, 0});
|
||||
} else {
|
||||
commands.insert(commands.end(), {0xc0064900u, 0x06000528u, 2u << 24u, 0, 0, 0, 0, 0});
|
||||
}
|
||||
AppendGate(commands);
|
||||
const auto readyBefore = output.ready.load();
|
||||
if (point == Point::Interrupt) {
|
||||
std::array<KernelEvent, 4> stale{};
|
||||
while (events.owner->GetTriggeredEvents(stale.data(), static_cast<int>(stale.size())) > 0) {}
|
||||
}
|
||||
Submit(commands);
|
||||
const bool watchedAtStore = AgcDriver::GuestMemory::Watched(target, TargetBytes);
|
||||
std::string failure;
|
||||
if (point == Point::Flip) {
|
||||
const bool flipped = Await([&] { return output.ready.load() != readyBefore; });
|
||||
if (!flipped) failure = "the flip did not reach the GPU";
|
||||
else if (!Await([&] { return Mismatches(region).empty(); })) failure = "after the flip, while the queue waited: " + Mismatches(region);
|
||||
} else if (point == Point::Label) {
|
||||
if (!Await([&] { return region.Word(LabelOffset) == LabelValue; })) failure = "the label after the draw never landed";
|
||||
else if (const auto mismatches = Mismatches(region); !mismatches.empty()) failure = "when the label after the draw landed: " + mismatches;
|
||||
} else {
|
||||
std::array<KernelEvent, 1> delivered{};
|
||||
if (!Await([&] { return events.owner->GetTriggeredEvents(delivered.data(), 1) == 1; })) failure = "the end-of-pipe interrupt after the draw was not delivered";
|
||||
else if (delivered[0].udata != &events.tag) failure = "an unexpected event was delivered";
|
||||
else if (const auto mismatches = Mismatches(region); !mismatches.empty()) failure = "when the end-of-pipe interrupt after the draw was delivered: " + mismatches;
|
||||
}
|
||||
Gate.store(1, std::memory_order_release);
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
AgcDriver::Graphics::StorageTexture::FlushPending(target, region.bytes, nullptr, "test release");
|
||||
std::printf("linear target completion point: %s, %s memory (watched at the store: %d): %s\n", name, watched ? "write-watched" : "unwatched", watchedAtStore ? 1 : 0, failure.empty() ? "stored" : failure.c_str());
|
||||
if (failure.empty()) return {};
|
||||
return std::string(name) + (watched ? " (write-watched memory): " : " (unwatched memory): ") + failure;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
try {
|
||||
bool flips = argc == 1, labels = argc == 1, interrupts = argc == 1, watchedFlip = argc == 1;
|
||||
for (int index = 1; index < argc; ++index) {
|
||||
const std::string_view argument(argv[index]);
|
||||
if (argument == "--flip") flips = true;
|
||||
else if (argument == "--label") labels = true;
|
||||
else if (argument == "--interrupt") interrupts = true;
|
||||
else Require(false, "linear target completion points: unknown argument " + std::string(argument));
|
||||
}
|
||||
{
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
if (AgcDriver::VulkanDeviceTestAccess::ContextOf(*device).hostImportAlignment == 0) {
|
||||
std::puts("linear target completion point tests skipped: no host import on this device, so the target is never resident");
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
}
|
||||
std::vector<std::unique_ptr<Region>> regions;
|
||||
struct ShutdownGuard {
|
||||
~ShutdownGuard() { try { AgcDriverShutdown_nid_postfix(); } catch (...) {} }
|
||||
} shutdown;
|
||||
Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2);
|
||||
Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1);
|
||||
auto output = std::make_shared<Output>();
|
||||
AgcDriverRegisterVideoOutput_nid_postfix(OutputHandle, output);
|
||||
Events events;
|
||||
if (interrupts) {
|
||||
Require(sceKernelCreateEqueue(&events.queue, "AGC linear target") == 0, "linear target completion points: event queue creation failed");
|
||||
events.owner = EqueuePin_nid_postfix(events.queue);
|
||||
Require(sceAgcDriverAddEqEvent(events.queue, 0, &events.tag) == 0, "linear target completion points: graphics event registration failed");
|
||||
}
|
||||
std::vector<std::string> failures;
|
||||
const auto run = [&](Point point, bool watched) {
|
||||
regions.push_back(std::make_unique<Region>(watched ? 16 * BlockBytes : 4 * BlockBytes, watched));
|
||||
if (auto failure = Run(point, watched, *regions.back(), *output, events); !failure.empty()) failures.push_back(std::move(failure));
|
||||
};
|
||||
if (flips) run(Point::Flip, false);
|
||||
if (watchedFlip) run(Point::Flip, true);
|
||||
if (labels) run(Point::Label, false);
|
||||
if (interrupts) run(Point::Interrupt, false);
|
||||
if (interrupts) {
|
||||
Require(sceAgcDriverDeleteEqEvent(events.queue, 0) == 0, "linear target completion points: graphics event deletion failed");
|
||||
events.owner.reset();
|
||||
Require(sceKernelDeleteEqueue(events.queue) == 0, "linear target completion points: event queue deletion failed");
|
||||
}
|
||||
AgcDriverUnregisterVideoOutput_nid_postfix(OutputHandle, output);
|
||||
if (!failures.empty()) {
|
||||
std::string message = "a CPU read after a point where the guest can observe a draw's completion did not see the drawn linear target:";
|
||||
for (const auto& failure : failures) message += "\n " + failure;
|
||||
Require(false, message);
|
||||
}
|
||||
AgcDriverShutdown_nid_postfix();
|
||||
std::puts("linear target completion point tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "prx/libc/include/GuestArena.hpp"
|
||||
#include "prx/libc/include/GuestWriteWatch.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
struct VulkanDeviceTestAccess {
|
||||
static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); }
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using AgcDriver::Graphics::StorageTexture;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Width = 64;
|
||||
constexpr std::uint32_t Height = 16;
|
||||
constexpr std::size_t BlockBytes = 65536;
|
||||
constexpr std::size_t TargetBytes = std::size_t{Width} * Height * 16u;
|
||||
constexpr std::size_t LabelOffset = BlockBytes;
|
||||
constexpr std::uint32_t Rgba32FloatInfo = 0x8738u;
|
||||
constexpr std::uint32_t LinearAttrib3 = 0x09000000u;
|
||||
constexpr std::uint32_t Float32AbgrExport = 9;
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{
|
||||
0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000,
|
||||
};
|
||||
|
||||
constexpr std::array<std::array<float, 4>, 3> Triangle{{
|
||||
{-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f}
|
||||
}};
|
||||
|
||||
alignas(256) std::array<std::array<std::uint32_t, 16>, 2> Programs{};
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu};
|
||||
}
|
||||
|
||||
std::span<const std::uint32_t> Program(std::size_t index, bool blue) {
|
||||
auto& code = Programs.at(index);
|
||||
std::size_t at = 0;
|
||||
code[at++] = 0x36000000u | (20u << 17u) | 0xffu;
|
||||
code[at++] = 0x0000ffffu;
|
||||
code[at++] = 0x2c000000u | (21u << 17u) | 0x90u;
|
||||
code[at++] = 0x7e000c00u | (20u << 17u) | (256u + 20u);
|
||||
code[at++] = 0x7e000c00u | (21u << 17u) | (256u + 21u);
|
||||
code[at++] = (blue ? 0x7e0002f2u : 0x7e000280u) | (22u << 17u);
|
||||
code[at++] = 0x7e0002f2u | (23u << 17u);
|
||||
code[at++] = 0xf800180fu;
|
||||
code[at++] = 0x17161514u;
|
||||
code[at++] = 0xbf810000u;
|
||||
return std::span<const std::uint32_t>(code.data(), at);
|
||||
}
|
||||
|
||||
struct Block {
|
||||
explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) {
|
||||
#ifdef _WIN32
|
||||
if (watched) {
|
||||
data = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes));
|
||||
GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
}
|
||||
#else
|
||||
if (watched) {
|
||||
void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
Require(raw != MAP_FAILED, "cannot map the color target");
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(raw);
|
||||
const auto aligned = (begin + BlockBytes - 1) & ~(static_cast<std::uintptr_t>(BlockBytes) - 1);
|
||||
if (aligned != begin) munmap(raw, aligned - begin);
|
||||
if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast<void*>(aligned + bytes), begin + BlockBytes - aligned);
|
||||
data = reinterpret_cast<std::uint8_t*>(aligned);
|
||||
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, bytes));
|
||||
}
|
||||
#endif
|
||||
Require(data != nullptr, "cannot allocate the color target");
|
||||
Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched");
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Add(data, bytes, true, true, true);
|
||||
}
|
||||
~Block() {
|
||||
StorageTexture::FlushPending(Address(), bytes, nullptr, "test release");
|
||||
{
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Remove(data);
|
||||
}
|
||||
#ifdef _WIN32
|
||||
if (watched) {
|
||||
GuestArena::GuestArenaReset_nid_postfix(data, bytes);
|
||||
GuestArena::GuestArenaRelease_nid_postfix(data, bytes);
|
||||
} else {
|
||||
VirtualFree(data, 0, MEM_RELEASE);
|
||||
}
|
||||
#else
|
||||
if (watched) {
|
||||
munmap(data, bytes);
|
||||
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
std::free(data);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
Block(const Block&) = delete;
|
||||
Block& operator=(const Block&) = delete;
|
||||
std::uint64_t Address() const { return reinterpret_cast<std::uintptr_t>(data); }
|
||||
std::size_t bytes;
|
||||
bool watched;
|
||||
std::uint8_t* data = nullptr;
|
||||
};
|
||||
|
||||
AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) {
|
||||
AgcDriver::Registers cx;
|
||||
cx[0x318] = static_cast<std::uint32_t>(block.Address() >> 8u);
|
||||
cx[0x31b] = 0;
|
||||
cx[0x31c] = Rgba32FloatInfo;
|
||||
cx[0x31d] = 0;
|
||||
cx[0x323] = 0;
|
||||
cx[0x324] = 0;
|
||||
cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u);
|
||||
cx[0x3b8] = LinearAttrib3;
|
||||
cx[0x390] = static_cast<std::uint32_t>(block.Address() >> 40u);
|
||||
auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0);
|
||||
color.exportIndex = 0;
|
||||
return color;
|
||||
}
|
||||
|
||||
void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::span<const std::uint32_t> pixelCode, VkRect2D scissor) {
|
||||
const auto target = device.Target();
|
||||
constexpr std::uint32_t waveSize = 64;
|
||||
std::vector<std::uint32_t> vertexUserData(4, 0u);
|
||||
const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast<std::uint32_t>(Triangle.size()));
|
||||
std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin());
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> vertexMemory{{{reinterpret_cast<std::uintptr_t>(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}};
|
||||
ShaderRecompiler::RecompileRequest vertex{
|
||||
{ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(VertexCode.data()), VertexCode, 0, {}},
|
||||
{waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory},
|
||||
target,
|
||||
{0, 0, 0, 64}
|
||||
};
|
||||
vertex.useCache = false;
|
||||
const auto vertexResult = ShaderRecompiler::Recompile(vertex);
|
||||
const auto vertexPush = static_cast<std::uint32_t>(vertexResult.pushConstants.size());
|
||||
|
||||
ShaderRecompiler::ShaderPixelStageInfo pixel{};
|
||||
pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionFixedPoint);
|
||||
pixel.targetOutputMode.at(0) = Float32AbgrExport;
|
||||
pixel.targetExportMapping.fill(0xe4u);
|
||||
const std::vector<std::uint32_t> pixelUserData(8, 0u);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{reinterpret_cast<std::uintptr_t>(pixelCode.data()), std::as_bytes(pixelCode)}}};
|
||||
ShaderRecompiler::RecompileRequest fragment{
|
||||
{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(pixelCode.data()), pixelCode, 0, {}},
|
||||
{waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory},
|
||||
target,
|
||||
PixelPushLayout(vertexPush, target)
|
||||
};
|
||||
fragment.useCache = false;
|
||||
const auto pixelResult = ShaderRecompiler::Recompile(fragment);
|
||||
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{
|
||||
{ShaderStage::Vertex, &vertexResult, 0},
|
||||
{ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)}
|
||||
}};
|
||||
|
||||
VkPipelineColorBlendAttachmentState blend{};
|
||||
blend.blendEnable = VK_FALSE;
|
||||
blend.colorWriteMask = 0xf;
|
||||
AgcDriver::Graphics::State state{};
|
||||
state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt};
|
||||
state.colors = {color};
|
||||
state.color = color;
|
||||
state.hasColorTarget = true;
|
||||
state.renderExtent = {Width, Height};
|
||||
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
state.viewport = {0, static_cast<float>(Height), static_cast<float>(Width), -static_cast<float>(Height), 0, 1};
|
||||
state.negativeOneToOne = false;
|
||||
state.scissor = scissor;
|
||||
state.cullMode = VK_CULL_MODE_NONE;
|
||||
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
|
||||
state.blends = {blend};
|
||||
state.blend = blend;
|
||||
state.blendConstants = {};
|
||||
const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(Triangle.size()), 0, 1, 0, false};
|
||||
device.Draw(state, draw, shaders);
|
||||
}
|
||||
|
||||
std::array<float, 4> Texel(const std::uint8_t* target, std::uint32_t x, std::uint32_t y) {
|
||||
std::array<float, 4> texel{};
|
||||
std::memcpy(texel.data(), target + (static_cast<std::size_t>(y) * Width + x) * 16u, sizeof(texel));
|
||||
return texel;
|
||||
}
|
||||
|
||||
void Label(AgcDriver::VulkanDevice& device, const Block& block, std::uint32_t value, std::uint64_t submissions, bool watched, const char* what) {
|
||||
const auto bytes = std::as_bytes(std::span(&value, 1));
|
||||
const auto reason = device.WriteLabelOnGpu(block.Address() + LabelOffset, bytes, value, 0);
|
||||
Require(reason == 0, std::string(what) + ": the label was not recorded on the GPU (outcome " + std::to_string(reason) + ")");
|
||||
Require(StorageTexture::FindPending(block.Address(), TargetBytes) == nullptr, std::string(what) + ": the drawn linear target was still pending after a label the GPU writes");
|
||||
const auto* recorder = AgcDriver::Graphics::Recorder::Active();
|
||||
Require(!watched || (recorder != nullptr && recorder->Submissions() == submissions), std::string(what) + ": storing the drawn linear target before the label waited for recorded work (" + std::to_string(recorder != nullptr ? recorder->Submissions() - submissions : 0) + " submissions)");
|
||||
}
|
||||
|
||||
bool Expect(AgcDriver::VulkanDevice& device) {
|
||||
Block block(16 * BlockBytes);
|
||||
std::fill(block.data, block.data + block.bytes, std::uint8_t{0xff});
|
||||
const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device);
|
||||
if (AgcDriver::Graphics::HostImportFor(context, block.Address(), block.bytes) == nullptr) return false;
|
||||
const auto color = DecodeTarget(block);
|
||||
Require(color.tileMode == AgcDriver::Graphics::ColorTileMode::Linear && color.bytes == TargetBytes, "the target is not a linear R32G32B32A32 target");
|
||||
const auto red = Program(0, false);
|
||||
const auto blue = Program(1, true);
|
||||
const auto* recorder = AgcDriver::Graphics::Recorder::Active();
|
||||
Require(recorder != nullptr, "the device has no recorder");
|
||||
const bool watched = AgcDriver::GuestMemory::Watched(block.Address(), TargetBytes);
|
||||
if (!watched) std::puts("linear target completion stores: the target is outside the write watch, so a store may wait for the previous store's saved bytes; the submission checks are skipped");
|
||||
const auto pending = [&](const char* what) {
|
||||
Require(StorageTexture::FindPending(block.Address(), TargetBytes) != nullptr, std::string(what) + ": the drawn linear target was stored before any point where its completion is observable");
|
||||
};
|
||||
Draw(device, color, red, {{0, 0}, {Width, Height}});
|
||||
pending("after the first draw");
|
||||
const auto submissions = recorder->Submissions();
|
||||
Draw(device, color, blue, {{0, 0}, {Width / 2u, Height}});
|
||||
pending("after the second draw");
|
||||
Require(!watched || recorder->Submissions() == submissions, "the second draw into the linear target submitted recorded work");
|
||||
Draw(device, color, red, {{Width / 4u, Height / 4u}, {Width / 4u, Height / 2u}});
|
||||
pending("after the third draw");
|
||||
Require(!watched || recorder->Submissions() == submissions, "the third draw into the linear target submitted recorded work");
|
||||
Label(device, block, 1u, submissions, watched, "the first label");
|
||||
Draw(device, color, blue, {{Width / 2u, 0}, {Width / 2u, Height}});
|
||||
pending("after the fourth draw");
|
||||
Label(device, block, 2u, submissions, watched, "the second label");
|
||||
Draw(device, color, red, {{0, Height / 2u}, {Width, Height / 2u}});
|
||||
pending("after the fifth draw");
|
||||
Require(!watched || recorder->Submissions() == submissions, "the fifth draw into the linear target submitted recorded work");
|
||||
device.WaitIdle();
|
||||
Require(StorageTexture::FindPending(block.Address(), TargetBytes) == nullptr, "the drawn linear target was still pending after the device wait");
|
||||
std::vector<std::uint8_t> stored(TargetBytes);
|
||||
std::memcpy(stored.data(), block.data, TargetBytes);
|
||||
for (std::uint32_t y = 0; y < Height; ++y) {
|
||||
for (std::uint32_t x = 0; x < Width; ++x) {
|
||||
const bool lastRed = y >= Height / 2u;
|
||||
const bool lastBlue = !lastRed && x >= Width / 2u;
|
||||
const bool thirdRed = !lastRed && !lastBlue && x >= Width / 4u && x < Width / 2u && y >= Height / 4u;
|
||||
const bool secondBlue = !lastRed && !lastBlue && !thirdRed;
|
||||
const float z = lastRed || thirdRed ? 0.0f : secondBlue || lastBlue ? 1.0f : -1.0f;
|
||||
const std::array<float, 4> expected{static_cast<float>(x), static_cast<float>(y), z, 1.0f};
|
||||
const auto texel = Texel(stored.data(), x, y);
|
||||
Require(texel == expected, "after the device wait, raw guest read: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") holds (" + std::to_string(texel[0]) + ", " + std::to_string(texel[1]) + ", " + std::to_string(texel[2]) + ", " + std::to_string(texel[3]) + ")");
|
||||
}
|
||||
}
|
||||
std::uint32_t label = 0;
|
||||
std::memcpy(&label, block.data + LabelOffset, sizeof(label));
|
||||
Require(label == 2u, "the second label did not land: it holds " + std::to_string(label));
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
if (!Expect(*device)) {
|
||||
std::puts("linear target completion store test skipped: no host import on this device");
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
std::puts("linear target completion store tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "prx/libc/include/GuestArena.hpp"
|
||||
#include "prx/libc/include/GuestWriteWatch.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
struct VulkanDeviceTestAccess {
|
||||
static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); }
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Width = 64;
|
||||
constexpr std::uint32_t Height = 16;
|
||||
constexpr std::size_t BlockBytes = 65536;
|
||||
constexpr std::size_t TargetBytes = std::size_t{Width} * Height * 16u;
|
||||
constexpr std::size_t RowBytes = std::size_t{Width} * 16u;
|
||||
constexpr std::uint32_t Drawn = Width / 2u;
|
||||
constexpr std::uint32_t Rgba32FloatInfo = 0x8738u;
|
||||
constexpr std::uint32_t LinearAttrib3 = 0x09000000u;
|
||||
constexpr std::uint32_t Float32AbgrExport = 9;
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{
|
||||
0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000,
|
||||
};
|
||||
|
||||
constexpr std::array<std::array<float, 4>, 3> Triangle{{
|
||||
{-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f}
|
||||
}};
|
||||
|
||||
alignas(256) std::array<std::array<std::uint32_t, 16>, 2> Programs{};
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu};
|
||||
}
|
||||
|
||||
std::span<const std::uint32_t> Program(std::size_t index, bool blue) {
|
||||
auto& code = Programs.at(index);
|
||||
std::size_t at = 0;
|
||||
code[at++] = 0x36000000u | (20u << 17u) | 0xffu;
|
||||
code[at++] = 0x0000ffffu;
|
||||
code[at++] = 0x2c000000u | (21u << 17u) | 0x90u;
|
||||
code[at++] = 0x7e000c00u | (20u << 17u) | (256u + 20u);
|
||||
code[at++] = 0x7e000c00u | (21u << 17u) | (256u + 21u);
|
||||
code[at++] = (blue ? 0x7e0002f2u : 0x7e000280u) | (22u << 17u);
|
||||
code[at++] = 0x7e0002f2u | (23u << 17u);
|
||||
code[at++] = 0xf800180fu;
|
||||
code[at++] = 0x17161514u;
|
||||
code[at++] = 0xbf810000u;
|
||||
return std::span<const std::uint32_t>(code.data(), at);
|
||||
}
|
||||
|
||||
struct Block {
|
||||
Block(std::size_t bytes, bool stayWatched) : bytes(bytes), arena(AgcDriver::GuestMemory::WriteWatched()) {
|
||||
#ifdef _WIN32
|
||||
if (arena) {
|
||||
data = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes));
|
||||
GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
}
|
||||
#else
|
||||
if (arena) {
|
||||
void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
Require(raw != MAP_FAILED, "cannot map the color target");
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(raw);
|
||||
const auto aligned = (begin + BlockBytes - 1) & ~(static_cast<std::uintptr_t>(BlockBytes) - 1);
|
||||
if (aligned != begin) munmap(raw, aligned - begin);
|
||||
if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast<void*>(aligned + bytes), begin + BlockBytes - aligned);
|
||||
data = reinterpret_cast<std::uint8_t*>(aligned);
|
||||
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, bytes));
|
||||
}
|
||||
#endif
|
||||
Require(data != nullptr, "cannot allocate the color target");
|
||||
Require(!arena || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched");
|
||||
if (arena && !stayWatched) {
|
||||
AgcDriver::GuestMemory::Unwatch(Address(), bytes);
|
||||
Require(!AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is still write-watched");
|
||||
}
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Add(data, bytes, true, true, true);
|
||||
}
|
||||
~Block() {
|
||||
AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release");
|
||||
{
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Remove(data);
|
||||
}
|
||||
#ifdef _WIN32
|
||||
if (arena) {
|
||||
GuestArena::GuestArenaReset_nid_postfix(data, bytes);
|
||||
GuestArena::GuestArenaRelease_nid_postfix(data, bytes);
|
||||
} else {
|
||||
VirtualFree(data, 0, MEM_RELEASE);
|
||||
}
|
||||
#else
|
||||
if (arena) {
|
||||
munmap(data, bytes);
|
||||
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
std::free(data);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
Block(const Block&) = delete;
|
||||
Block& operator=(const Block&) = delete;
|
||||
std::uint64_t Address() const { return reinterpret_cast<std::uintptr_t>(data); }
|
||||
std::size_t bytes;
|
||||
bool arena;
|
||||
std::uint8_t* data = nullptr;
|
||||
};
|
||||
|
||||
AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) {
|
||||
AgcDriver::Registers cx;
|
||||
cx[0x318] = static_cast<std::uint32_t>(block.Address() >> 8u);
|
||||
cx[0x31b] = 0;
|
||||
cx[0x31c] = Rgba32FloatInfo;
|
||||
cx[0x31d] = 0;
|
||||
cx[0x323] = 0;
|
||||
cx[0x324] = 0;
|
||||
cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u);
|
||||
cx[0x3b8] = LinearAttrib3;
|
||||
cx[0x390] = static_cast<std::uint32_t>(block.Address() >> 40u);
|
||||
auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0);
|
||||
color.exportIndex = 0;
|
||||
return color;
|
||||
}
|
||||
|
||||
void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::span<const std::uint32_t> pixelCode, std::uint32_t scissorWidth) {
|
||||
const auto target = device.Target();
|
||||
constexpr std::uint32_t waveSize = 64;
|
||||
std::vector<std::uint32_t> vertexUserData(4, 0u);
|
||||
const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast<std::uint32_t>(Triangle.size()));
|
||||
std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin());
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> vertexMemory{{{reinterpret_cast<std::uintptr_t>(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}};
|
||||
ShaderRecompiler::RecompileRequest vertex{
|
||||
{ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(VertexCode.data()), VertexCode, 0, {}},
|
||||
{waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory},
|
||||
target,
|
||||
{0, 0, 0, 64}
|
||||
};
|
||||
vertex.useCache = false;
|
||||
const auto vertexResult = ShaderRecompiler::Recompile(vertex);
|
||||
const auto vertexPush = static_cast<std::uint32_t>(vertexResult.pushConstants.size());
|
||||
|
||||
ShaderRecompiler::ShaderPixelStageInfo pixel{};
|
||||
pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionFixedPoint);
|
||||
pixel.targetOutputMode.at(0) = Float32AbgrExport;
|
||||
pixel.targetExportMapping.fill(0xe4u);
|
||||
const std::vector<std::uint32_t> pixelUserData(8, 0u);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{reinterpret_cast<std::uintptr_t>(pixelCode.data()), std::as_bytes(pixelCode)}}};
|
||||
ShaderRecompiler::RecompileRequest fragment{
|
||||
{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(pixelCode.data()), pixelCode, 0, {}},
|
||||
{waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory},
|
||||
target,
|
||||
PixelPushLayout(vertexPush, target)
|
||||
};
|
||||
fragment.useCache = false;
|
||||
const auto pixelResult = ShaderRecompiler::Recompile(fragment);
|
||||
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{
|
||||
{ShaderStage::Vertex, &vertexResult, 0},
|
||||
{ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)}
|
||||
}};
|
||||
|
||||
VkPipelineColorBlendAttachmentState blend{};
|
||||
blend.blendEnable = VK_FALSE;
|
||||
blend.colorWriteMask = 0xf;
|
||||
AgcDriver::Graphics::State state{};
|
||||
state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt};
|
||||
state.colors = {color};
|
||||
state.color = color;
|
||||
state.hasColorTarget = true;
|
||||
state.renderExtent = {Width, Height};
|
||||
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
state.viewport = {0, static_cast<float>(Height), static_cast<float>(Width), -static_cast<float>(Height), 0, 1};
|
||||
state.negativeOneToOne = false;
|
||||
state.scissor = {{0, 0}, {scissorWidth, Height}};
|
||||
state.cullMode = VK_CULL_MODE_NONE;
|
||||
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
|
||||
state.blends = {blend};
|
||||
state.blend = blend;
|
||||
state.blendConstants = {};
|
||||
const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(Triangle.size()), 0, 1, 0, false};
|
||||
device.Draw(state, draw, shaders);
|
||||
}
|
||||
|
||||
std::array<float, 4> Texel(const std::uint8_t* target, std::uint32_t x, std::uint32_t y) {
|
||||
std::array<float, 4> texel{};
|
||||
std::memcpy(texel.data(), target + static_cast<std::size_t>(y) * RowBytes + static_cast<std::size_t>(x) * 16u, sizeof(texel));
|
||||
return texel;
|
||||
}
|
||||
|
||||
enum class Outcome { Kept, Lost, Skipped };
|
||||
|
||||
Outcome Run(AgcDriver::VulkanDevice& device, bool watched, bool seeded, bool hookRead) {
|
||||
const auto name = std::string(seeded ? "seeded" : "unseeded") + ", " + (watched ? "write-watched" : "unwatched") + " memory" + (hookRead ? ", driver read before the store" : "");
|
||||
Block block(watched ? std::size_t{16} * BlockBytes : BlockBytes, watched);
|
||||
std::fill(block.data, block.data + block.bytes, std::uint8_t{0xff});
|
||||
const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device);
|
||||
if (AgcDriver::Graphics::HostImportFor(context, block.Address(), block.bytes) == nullptr) return Outcome::Skipped;
|
||||
const auto color = DecodeTarget(block);
|
||||
Require(color.tileMode == AgcDriver::Graphics::ColorTileMode::Linear && color.bytes == TargetBytes, "the target is not a linear R32G32B32A32 target");
|
||||
Draw(device, color, Program(0, false), Width);
|
||||
const auto* recorder = AgcDriver::Graphics::Recorder::Active();
|
||||
Require(recorder != nullptr && !recorder->Idle(), "the draw into an imported linear target waited for the GPU instead of being recorded");
|
||||
device.WaitIdle();
|
||||
std::vector<std::uint8_t> stored(TargetBytes);
|
||||
std::memcpy(stored.data(), block.data, TargetBytes);
|
||||
for (std::uint32_t y = 0; y < Height; ++y) {
|
||||
for (std::uint32_t x = 0; x < Width; ++x) {
|
||||
const std::array<float, 4> expected{static_cast<float>(x), static_cast<float>(y), 0.0f, 1.0f};
|
||||
const auto texel = Texel(stored.data(), x, y);
|
||||
Require(texel == expected, name + ": after the first draw, raw guest read: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") holds (" + std::to_string(texel[0]) + ", " + std::to_string(texel[1]) + ", " + std::to_string(texel[2]) + ", " + std::to_string(texel[3]) + ")");
|
||||
}
|
||||
}
|
||||
if (seeded) {
|
||||
for (std::uint32_t y = 0; y < Height; ++y) std::memset(block.data + y * RowBytes, 0x5a, Drawn * 16u);
|
||||
}
|
||||
Draw(device, color, Program(1, true), Drawn);
|
||||
for (std::uint32_t y = 0; y < Height; ++y) std::memset(block.data + y * RowBytes + Drawn * 16u, 0x42, RowBytes - Drawn * 16u);
|
||||
const bool watchedAtStore = AgcDriver::GuestMemory::Watched(block.Address(), TargetBytes);
|
||||
if (hookRead) {
|
||||
std::vector<std::uint8_t> row(RowBytes);
|
||||
AgcDriver::GuestMemory::Read(block.Address(), std::as_writable_bytes(std::span(row)), 1);
|
||||
for (std::uint32_t x = 0; x < Drawn; ++x) {
|
||||
const std::array<float, 4> expected{static_cast<float>(x), 0.0f, 1.0f, 1.0f};
|
||||
const auto texel = Texel(row.data(), x, 0);
|
||||
Require(texel == expected, name + ": a driver read of row 0 before the store: pixel (" + std::to_string(x) + ", 0) holds (" + std::to_string(texel[0]) + ", " + std::to_string(texel[1]) + ", " + std::to_string(texel[2]) + ", " + std::to_string(texel[3]) + "), not the second draw's");
|
||||
}
|
||||
}
|
||||
device.WaitIdle();
|
||||
std::size_t wrongGpu = 0;
|
||||
std::size_t wrongCpu = 0;
|
||||
for (std::uint32_t y = 0; y < Height; ++y) {
|
||||
for (std::uint32_t x = 0; x < Drawn; ++x) {
|
||||
const std::array<float, 4> expected{static_cast<float>(x), static_cast<float>(y), 1.0f, 1.0f};
|
||||
if (Texel(block.data, x, y) != expected) ++wrongGpu;
|
||||
}
|
||||
auto* begin = block.data + y * RowBytes + Drawn * 16u;
|
||||
wrongCpu += static_cast<std::size_t>(std::count_if(begin, block.data + (y + 1u) * RowBytes, [](std::uint8_t byte) { return byte != 0x42u; }));
|
||||
}
|
||||
std::printf("linear target CPU writes, %s (watched at the store: %d): wrong GPU pixels %zu/%u, wrong CPU bytes %zu/%zu\n", name.c_str(), watchedAtStore ? 1 : 0, wrongGpu, Drawn * Height, wrongCpu, (RowBytes - Drawn * 16u) * Height);
|
||||
return wrongGpu == 0 && wrongCpu == 0 ? Outcome::Kept : Outcome::Lost;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
bool kept = true;
|
||||
bool ran = false;
|
||||
for (const bool watched : {true, false}) {
|
||||
for (const bool seeded : {false, true}) {
|
||||
for (const bool hookRead : {false, true}) {
|
||||
const auto outcome = Run(*device, watched, seeded, hookRead);
|
||||
ran = ran || outcome != Outcome::Skipped;
|
||||
kept = kept && outcome != Outcome::Lost;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!ran) {
|
||||
std::puts("linear target CPU write test skipped: no host import on this device");
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
Require(kept, "a drawn linear target's store lost the drawn half or the CPU's later stores to the other half");
|
||||
std::puts("linear target CPU write tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "prx/libc/include/GuestArena.hpp"
|
||||
#include "prx/libc/include/GuestWriteWatch.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <bit>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <functional>
|
||||
#include <initializer_list>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
struct VulkanDeviceTestAccess {
|
||||
static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); }
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
|
||||
constexpr std::uint32_t Width = 256;
|
||||
constexpr std::uint32_t Height = 128;
|
||||
constexpr std::size_t Pitch = std::size_t{Width} * 4u;
|
||||
constexpr std::size_t TargetBytes = Pitch * Height;
|
||||
constexpr std::size_t BlockBytes = 65536;
|
||||
constexpr std::size_t RegionBytes = 16 * BlockBytes;
|
||||
constexpr std::size_t LabelOffset = 3 * BlockBytes;
|
||||
constexpr std::uint32_t Untouched = 0x5a5a5a5au;
|
||||
constexpr std::uint32_t Filled = 0x9abcdef0u;
|
||||
constexpr std::uint32_t Drawn = 0xff0000ffu;
|
||||
constexpr std::uint32_t LabelValue = 0x600dcafeu;
|
||||
constexpr std::uint32_t CpuStored = 0x0badf00du;
|
||||
constexpr std::uint32_t Rgba8UnormInfo = 0x28028u;
|
||||
constexpr std::uint32_t LinearAttrib3 = 0x09000000u;
|
||||
constexpr std::uint32_t WaitEqual = 3;
|
||||
constexpr std::uint32_t WaitAtLeast = 5;
|
||||
constexpr std::array<std::array<std::uint32_t, 4>, 2> Scissors{{{16, 8, 128, 96}, {128, 32, 240, 120}}};
|
||||
constexpr std::array<std::uint32_t, 4> BlockZeroScissor{24, 8, 232, 56};
|
||||
constexpr std::size_t CpuStoreOffset = BlockBytes + 32 * Pitch + 64 * 4;
|
||||
static_assert(BlockZeroScissor[3] * Pitch <= BlockBytes);
|
||||
static_assert(CpuStoreOffset >= BlockBytes && CpuStoreOffset < TargetBytes);
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 20> VertexCode{
|
||||
0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u,
|
||||
0x7e060208u, 0x7e0802f2u, 0x7e0a02ffu, 0x44800000u, 0xf80000d1u, 0x00000005u, 0xf80008cfu, 0x04030201u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u,
|
||||
};
|
||||
alignas(256) constexpr std::array<std::uint32_t, 8> PixelCode{0x7e000200u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u};
|
||||
alignas(256) constexpr std::array<std::uint32_t, 9> FillCode{
|
||||
0xd7460004, 0x04010c08, 0x7e000204, 0x7e020205, 0x7e040206, 0x7e060207, 0xe01c2000, 0x80000004, 0xbf810000,
|
||||
};
|
||||
alignas(256) constexpr std::array<std::uint32_t, 11> PairFillCode{
|
||||
0xd7460004, 0x04010c0c, 0x7e000204, 0x7e020205, 0x7e040206, 0x7e060207, 0xe01c2000, 0x80000004, 0xe01c2000, 0x80020004, 0xbf810000,
|
||||
};
|
||||
|
||||
alignas(4) std::atomic<std::uint32_t> Gate = 0;
|
||||
|
||||
struct Header {
|
||||
Shader shader{};
|
||||
ShaderUserData userData{};
|
||||
};
|
||||
|
||||
alignas(8) Header VertexHeader{};
|
||||
alignas(8) Header PixelHeader{};
|
||||
|
||||
void Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) {
|
||||
header.shader.file_header = 0x34333231u;
|
||||
header.shader.version = 0x18u;
|
||||
header.shader.header_size = sizeof(Header);
|
||||
header.shader.shader_size = static_cast<std::uint32_t>(bytes);
|
||||
header.shader.code = code;
|
||||
header.shader.type = type;
|
||||
header.shader.user_data = &header.userData;
|
||||
AgcDriverRegisterShader_nid_postfix(&header.shader);
|
||||
}
|
||||
|
||||
void Append(std::vector<std::uint32_t>& commands, std::uint32_t opcode, std::initializer_list<std::uint32_t> payload) {
|
||||
commands.push_back(0xc0000000u | (static_cast<std::uint32_t>(payload.size() - 1) << 16u) | (opcode << 8u));
|
||||
commands.insert(commands.end(), payload);
|
||||
}
|
||||
|
||||
void AppendDraw(std::vector<std::uint32_t>& commands, std::uint64_t target, const std::array<std::uint32_t, 4>& scissor) {
|
||||
const auto vertex = reinterpret_cast<std::uintptr_t>(VertexCode.data());
|
||||
const auto pixel = reinterpret_cast<std::uintptr_t>(PixelCode.data());
|
||||
const auto extent = (Height << 16u) | Width;
|
||||
const std::vector<std::pair<std::uint32_t, std::uint32_t>> context{
|
||||
{0x2d5, 0x2000},
|
||||
{0x1b6, 0}, {0x207, 0x01200000}, {0x200, 0}, {0x203, 0x800},
|
||||
{0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0},
|
||||
{0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa},
|
||||
{0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff},
|
||||
{0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240},
|
||||
{0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010},
|
||||
{0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, Rgba8UnormInfo},
|
||||
{0x31b, 0}, {0x31d, 0}, {0x3b0, ((Width - 1u) << 14u) | (Height - 1u)},
|
||||
{0x3b8, LinearAttrib3}, {0x1e0, 0},
|
||||
{0xc, 0}, {0xd, extent},
|
||||
{0x81, 0x80000000}, {0x82, extent},
|
||||
{0x90, 0x80000000u | (scissor[1] << 16u) | scissor[0]}, {0x91, (scissor[3] << 16u) | scissor[2]},
|
||||
{0x94, 0x80000000}, {0x95, extent},
|
||||
{0x10f, std::bit_cast<std::uint32_t>(2.0f * Width)}, {0x110, std::bit_cast<std::uint32_t>(2.0f * Width)},
|
||||
{0x111, std::bit_cast<std::uint32_t>(2.0f * Height)}, {0x112, std::bit_cast<std::uint32_t>(2.0f * Height)},
|
||||
{0x113, std::bit_cast<std::uint32_t>(1.0f)}, {0x114, 0},
|
||||
{0xb4, 0}, {0xb5, std::bit_cast<std::uint32_t>(1.0f)},
|
||||
{0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0},
|
||||
{0x318, static_cast<std::uint32_t>(target >> 8u)}, {0x390, static_cast<std::uint32_t>(target >> 40u)},
|
||||
};
|
||||
const std::vector<std::pair<std::uint32_t, std::uint32_t>> shader{
|
||||
{0xc8, static_cast<std::uint32_t>(vertex >> 8u)}, {0xc9, static_cast<std::uint32_t>(vertex >> 40u)}, {0x8b, 1u << 1u}, {0x8c, 0},
|
||||
{0x008, static_cast<std::uint32_t>(pixel >> 8u)}, {0x009, static_cast<std::uint32_t>(pixel >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast<std::uint32_t>(1.0f)},
|
||||
};
|
||||
Append(commands, 0x79, {0x242, 4});
|
||||
Append(commands, 0x79, {0x24a, 0});
|
||||
for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value});
|
||||
for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value});
|
||||
Append(commands, 0x2d, {3, 2});
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(std::uint64_t address, std::size_t bytes) {
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (16u << 16u), static_cast<std::uint32_t>(bytes / 16u), 0x4bfacu};
|
||||
}
|
||||
|
||||
void AppendDispatch(std::vector<std::uint32_t>& commands, const std::uint32_t* program, const std::vector<std::uint32_t>& userData, std::uint32_t groups) {
|
||||
const auto code = reinterpret_cast<std::uintptr_t>(program);
|
||||
Append(commands, 0x76, {0x207, 64, 1, 1});
|
||||
Append(commands, 0x76, {0x20c, static_cast<std::uint32_t>(code >> 8u), static_cast<std::uint32_t>(code >> 40u)});
|
||||
Append(commands, 0x76, {0x213, (static_cast<std::uint32_t>(userData.size()) << 1u) | 0x80u});
|
||||
commands.push_back(0xc0007600u | (static_cast<std::uint32_t>(userData.size()) << 16u));
|
||||
commands.push_back(0x240u);
|
||||
commands.insert(commands.end(), userData.begin(), userData.end());
|
||||
commands.insert(commands.end(), {0xc0031500u, groups, 1, 1, 0x41u});
|
||||
}
|
||||
|
||||
void AppendFill(std::vector<std::uint32_t>& commands, std::uint64_t target, const std::uint32_t* copied, std::size_t bytes) {
|
||||
const auto direct = BufferDescriptor(target, bytes);
|
||||
const auto other = BufferDescriptor(reinterpret_cast<std::uintptr_t>(copied), bytes);
|
||||
std::vector<std::uint32_t> userData(direct.begin(), direct.end());
|
||||
userData.insert(userData.end(), {Filled, Filled, Filled, Filled});
|
||||
userData.insert(userData.end(), other.begin(), other.end());
|
||||
AppendDispatch(commands, PairFillCode.data(), userData, static_cast<std::uint32_t>(bytes / 1024u));
|
||||
}
|
||||
|
||||
void AppendLabel(std::vector<std::uint32_t>& commands, std::uint64_t label) {
|
||||
commands.insert(commands.end(), {0xc0064900u, 0x514u, 1u << 29u, static_cast<std::uint32_t>(label), static_cast<std::uint32_t>(label >> 32u), LabelValue, 0, 0});
|
||||
}
|
||||
|
||||
void AppendGate(std::vector<std::uint32_t>& commands, std::uint32_t function, std::uint32_t reference) {
|
||||
const auto gate = reinterpret_cast<std::uintptr_t>(&Gate);
|
||||
commands.insert(commands.end(), {0xc0053c00u, 0x10u | function, static_cast<std::uint32_t>(gate), static_cast<std::uint32_t>(gate >> 32u), reference, 0xffffffffu, 0x19u});
|
||||
}
|
||||
|
||||
void Submit(std::vector<std::uint32_t>& commands) {
|
||||
Packet packet{commands.data(), static_cast<std::uint32_t>(commands.size()), 0, {}};
|
||||
AgcDriver::Submit(&packet, 0);
|
||||
}
|
||||
|
||||
class Region {
|
||||
public:
|
||||
explicit Region(std::size_t bytes) : bytes(bytes), arena(AgcDriver::GuestMemory::WriteWatched()) {
|
||||
#ifdef _WIN32
|
||||
if (arena) {
|
||||
data = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes));
|
||||
if (data != nullptr) GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
}
|
||||
#else
|
||||
if (arena) {
|
||||
void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
Require(raw != MAP_FAILED, "linear target direct write completion: cannot map the guest region");
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(raw);
|
||||
const auto aligned = (begin + BlockBytes - 1) & ~(static_cast<std::uintptr_t>(BlockBytes) - 1);
|
||||
if (aligned != begin) munmap(raw, aligned - begin);
|
||||
if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast<void*>(aligned + bytes), begin + BlockBytes - aligned);
|
||||
data = reinterpret_cast<std::uint8_t*>(aligned);
|
||||
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, bytes));
|
||||
}
|
||||
#endif
|
||||
Require(data != nullptr, "linear target direct write completion: cannot allocate the guest region");
|
||||
auto* words = reinterpret_cast<std::uint32_t*>(data);
|
||||
std::fill(words, words + bytes / 4u, Untouched);
|
||||
GuestAllocations::Mutation().Add(data, bytes, true, true, true);
|
||||
}
|
||||
~Region() {
|
||||
GuestAllocations::Mutation().Remove(data);
|
||||
#ifdef _WIN32
|
||||
if (arena) {
|
||||
GuestArena::GuestArenaReset_nid_postfix(data, bytes);
|
||||
GuestArena::GuestArenaRelease_nid_postfix(data, bytes);
|
||||
} else {
|
||||
VirtualFree(data, 0, MEM_RELEASE);
|
||||
}
|
||||
#else
|
||||
if (arena) {
|
||||
munmap(data, bytes);
|
||||
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
std::free(data);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
Region(const Region&) = delete;
|
||||
Region& operator=(const Region&) = delete;
|
||||
std::uint64_t Address() const { return reinterpret_cast<std::uintptr_t>(data); }
|
||||
std::uint32_t Word(std::size_t offset) const { return std::atomic_ref<std::uint32_t>(*reinterpret_cast<std::uint32_t*>(data + offset)).load(std::memory_order_acquire); }
|
||||
void Put(std::size_t offset, std::uint32_t value) const { std::atomic_ref<std::uint32_t>(*reinterpret_cast<std::uint32_t*>(data + offset)).store(value, std::memory_order_release); }
|
||||
std::size_t bytes;
|
||||
|
||||
private:
|
||||
bool arena;
|
||||
std::uint8_t* data = nullptr;
|
||||
};
|
||||
|
||||
void Import(const Region& region) {
|
||||
const auto buffer = BufferDescriptor(region.Address(), region.bytes);
|
||||
std::vector<std::uint32_t> userData(buffer.begin(), buffer.end());
|
||||
userData.insert(userData.end(), {Untouched, Untouched, Untouched, Untouched});
|
||||
std::vector<std::uint32_t> commands;
|
||||
AppendDispatch(commands, FillCode.data(), userData, static_cast<std::uint32_t>(region.bytes / 1024u));
|
||||
Submit(commands);
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
}
|
||||
|
||||
bool Covered(std::uint32_t x, std::uint32_t y) {
|
||||
return std::any_of(Scissors.begin(), Scissors.end(), [&](const std::array<std::uint32_t, 4>& scissor) { return x >= scissor[0] && x < scissor[2] && y >= scissor[1] && y < scissor[3]; });
|
||||
}
|
||||
|
||||
std::string Mismatches(const Region& region, bool drawsLast) {
|
||||
std::size_t covered = 0, wrongCovered = 0, filledCovered = 0, wrongElsewhere = 0, drawnElsewhere = 0;
|
||||
for (std::uint32_t y = 0; y < Height; ++y) {
|
||||
for (std::uint32_t x = 0; x < Width; ++x) {
|
||||
const auto value = region.Word(y * Pitch + x * 4u);
|
||||
if (drawsLast && Covered(x, y)) {
|
||||
++covered;
|
||||
wrongCovered += value == Drawn ? 0u : 1u;
|
||||
filledCovered += value == Filled ? 1u : 0u;
|
||||
} else {
|
||||
wrongElsewhere += value == Filled ? 0u : 1u;
|
||||
drawnElsewhere += value == Drawn ? 1u : 0u;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (wrongCovered == 0 && wrongElsewhere == 0) return {};
|
||||
const auto elsewhere = std::to_string(std::size_t{Width} * Height - covered);
|
||||
if (!drawsLast) return std::to_string(wrongElsewhere) + " of the " + elsewhere + " pixels do not hold the fill's pattern (" + std::to_string(drawnElsewhere) + " hold the draws' color)";
|
||||
return std::to_string(wrongCovered) + " of the " + std::to_string(covered) + " pixels the draws covered do not hold their color (" + std::to_string(filledCovered) + " hold the fill's pattern) and " + std::to_string(wrongElsewhere) + " of the " + elsewhere + " pixels outside them do not hold the fill's pattern";
|
||||
}
|
||||
|
||||
std::string Hex(std::uint32_t value) {
|
||||
char text[16];
|
||||
std::snprintf(text, sizeof(text), "0x%08x", value);
|
||||
return text;
|
||||
}
|
||||
|
||||
std::string CpuStoreMismatches(const Region& region) {
|
||||
std::size_t covered = 0, wrongCovered = 0, filledCovered = 0, others = 0, wrongOthers = 0;
|
||||
for (std::uint32_t y = 0; y < Height; ++y) {
|
||||
for (std::uint32_t x = 0; x < Width; ++x) {
|
||||
const auto offset = y * Pitch + x * 4u;
|
||||
if (offset == CpuStoreOffset) continue;
|
||||
const auto value = region.Word(offset);
|
||||
if (x >= BlockZeroScissor[0] && x < BlockZeroScissor[2] && y >= BlockZeroScissor[1] && y < BlockZeroScissor[3]) {
|
||||
++covered;
|
||||
wrongCovered += value == Drawn ? 0u : 1u;
|
||||
filledCovered += value == Filled ? 1u : 0u;
|
||||
} else {
|
||||
++others;
|
||||
wrongOthers += value == (offset < BlockBytes ? Filled : Untouched) ? 0u : 1u;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::vector<std::string> parts;
|
||||
if (wrongCovered != 0) parts.push_back(std::to_string(wrongCovered) + " of the " + std::to_string(covered) + " pixels the draw covered in block 0 do not hold its color (" + std::to_string(filledCovered) + " hold the fill's pattern)");
|
||||
if (const auto stored = region.Word(CpuStoreOffset); stored != CpuStored) parts.push_back("the CPU store in block 1 holds " + Hex(stored) + ", not " + Hex(CpuStored));
|
||||
if (wrongOthers != 0) parts.push_back(std::to_string(wrongOthers) + " of the " + std::to_string(others) + " other pixels do not hold the fill's pattern in block 0 or the untouched pattern in block 1");
|
||||
std::string text;
|
||||
for (const auto& part : parts) text += (text.empty() ? "" : "; ") + part;
|
||||
return text;
|
||||
}
|
||||
|
||||
bool Await(const std::function<bool()>& done) {
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
||||
while (!done()) {
|
||||
if (std::chrono::steady_clock::now() >= deadline) return false;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
enum class Order { FillFirst, DrawsFirst };
|
||||
|
||||
bool Run(Order order, const Region& region, std::vector<std::string>& failures) {
|
||||
const bool fillFirst = order == Order::FillFirst;
|
||||
const char* name = fillFirst ? "fill, then draws" : "draws, then fill";
|
||||
const auto target = region.Address();
|
||||
Import(region);
|
||||
if (!AgcDriver::GuestMemory::Watched(target, TargetBytes)) return false;
|
||||
std::vector<std::uint32_t> copied(TargetBytes / 4u + 4u, Untouched);
|
||||
GuestAllocations::Mutation().Add(copied.data(), TargetBytes + 16u, true, true, true);
|
||||
struct Unregister {
|
||||
void* data;
|
||||
~Unregister() { GuestAllocations::Mutation().Remove(data); }
|
||||
} unregister{copied.data()};
|
||||
Gate.store(0, std::memory_order_release);
|
||||
std::vector<std::uint32_t> commands;
|
||||
if (fillFirst) AppendFill(commands, target, copied.data(), TargetBytes);
|
||||
for (const auto& scissor : Scissors) AppendDraw(commands, target, scissor);
|
||||
if (!fillFirst) AppendFill(commands, target, copied.data(), TargetBytes);
|
||||
AppendLabel(commands, target + LabelOffset);
|
||||
if (fillFirst) AppendGate(commands, WaitEqual, 1);
|
||||
Submit(commands);
|
||||
std::string failure;
|
||||
if (fillFirst) {
|
||||
if (!Await([&] { return region.Word(LabelOffset) == LabelValue; })) failure = "the label after the draws never landed";
|
||||
else if (const auto mismatches = Mismatches(region, true); !mismatches.empty()) failure = "when the label after the draws landed: " + mismatches;
|
||||
Gate.store(1, std::memory_order_release);
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
} else {
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
if (region.Word(LabelOffset) != LabelValue) failure = "the label after the fill had not landed by the device wait";
|
||||
else if (const auto mismatches = Mismatches(region, false); !mismatches.empty()) failure = "after the label and the device wait: " + mismatches;
|
||||
}
|
||||
AgcDriver::Graphics::StorageTexture::FlushPending(target, region.bytes, nullptr, "test release");
|
||||
std::printf("linear target direct write completion, %s: %s\n", name, failure.empty() ? "the later writer's values" : failure.c_str());
|
||||
if (!failure.empty()) failures.push_back(std::string(name) + ": " + failure);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RunCpuStore(bool watched, const Region& region, std::vector<std::string>& failures) {
|
||||
const std::string name = std::string("fill, draw in block 0, CPU store in block 1, ") + (watched ? "write-watched" : "unwatched") + " memory";
|
||||
const auto target = region.Address();
|
||||
Import(region);
|
||||
if (watched && !AgcDriver::GuestMemory::Watched(target, TargetBytes)) return false;
|
||||
if (!watched) {
|
||||
AgcDriver::GuestMemory::Unwatch(target, region.bytes);
|
||||
Require(!AgcDriver::GuestMemory::Watched(target, region.bytes), "linear target direct write completion: the region is still write-watched");
|
||||
}
|
||||
std::vector<std::uint32_t> copied(TargetBytes / 4u + 4u, Untouched);
|
||||
GuestAllocations::Mutation().Add(copied.data(), TargetBytes + 16u, true, true, true);
|
||||
struct Unregister {
|
||||
void* data;
|
||||
~Unregister() { GuestAllocations::Mutation().Remove(data); }
|
||||
} unregister{copied.data()};
|
||||
Gate.store(0, std::memory_order_release);
|
||||
std::vector<std::uint32_t> commands;
|
||||
AppendFill(commands, target, copied.data(), BlockBytes);
|
||||
AppendDraw(commands, target, BlockZeroScissor);
|
||||
AppendGate(commands, WaitAtLeast, 1);
|
||||
AppendLabel(commands, target + LabelOffset);
|
||||
AppendGate(commands, WaitAtLeast, 2);
|
||||
Submit(commands);
|
||||
const auto completed = [&] { return std::atomic_ref<std::uint32_t>(copied[0]).load(std::memory_order_acquire) == Filled; };
|
||||
bool pendingAtStore = false;
|
||||
const bool recorded = Await([&] {
|
||||
std::unique_lock hold(AgcDriver::GuestMemory::GpuMutex());
|
||||
if (AgcDriver::Graphics::StorageTexture::FindLive(target, TargetBytes) == nullptr) return false;
|
||||
pendingAtStore = !completed();
|
||||
region.Put(CpuStoreOffset, CpuStored);
|
||||
return true;
|
||||
});
|
||||
Gate.store(1, std::memory_order_release);
|
||||
std::string failure;
|
||||
bool completedAtLabel = false;
|
||||
if (!recorded) failure = "the draw into block 0 never made the target's image";
|
||||
else if (!Await([&] { return region.Word(LabelOffset) == LabelValue; })) failure = "the label after the draw never landed";
|
||||
else {
|
||||
completedAtLabel = completed();
|
||||
failure = CpuStoreMismatches(region);
|
||||
if (!failure.empty()) failure = "when the label after the draw landed: " + failure;
|
||||
if (watched && (!pendingAtStore || !completedAtLabel)) failure += std::string(failure.empty() ? "" : "; ") + (pendingAtStore ? "the fill's completion had not run when the label landed" : "the fill's completion had run before the CPU store") + ", so the ordering under test did not occur";
|
||||
}
|
||||
const bool watchedAtLabel = AgcDriver::GuestMemory::Watched(target, TargetBytes);
|
||||
Gate.store(2, std::memory_order_release);
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
AgcDriver::Graphics::StorageTexture::FlushPending(target, region.bytes, nullptr, "test release");
|
||||
std::printf("linear target direct write completion, %s (watched at the label: %d, completion pending at the CPU store: %d, run by the label: %d): %s\n", name.c_str(), watchedAtLabel ? 1 : 0, pendingAtStore ? 1 : 0, completedAtLabel ? 1 : 0, failure.empty() ? "the draw's color and the CPU store" : failure.c_str());
|
||||
if (!failure.empty()) failures.push_back(name + ": " + failure);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
{
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
if (AgcDriver::VulkanDeviceTestAccess::ContextOf(*device).hostImportAlignment == 0) {
|
||||
std::puts("linear target direct write completion tests skipped, host imports are unavailable on this device, so the target is never resident");
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
}
|
||||
std::vector<std::unique_ptr<Region>> regions;
|
||||
struct ShutdownGuard {
|
||||
~ShutdownGuard() { try { AgcDriverShutdown_nid_postfix(); } catch (...) {} }
|
||||
} shutdown;
|
||||
Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2);
|
||||
Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1);
|
||||
const auto unwatchedImports = [] {
|
||||
std::puts("linear target direct write completion tests skipped, the device does not keep host imports write-watched, so no completion stamp over the target is tracked");
|
||||
return VulkanTestSkipped;
|
||||
};
|
||||
std::vector<std::string> failures;
|
||||
for (const auto order : {Order::FillFirst, Order::DrawsFirst}) {
|
||||
regions.push_back(std::make_unique<Region>(RegionBytes));
|
||||
if (!Run(order, *regions.back(), failures)) return unwatchedImports();
|
||||
}
|
||||
for (const bool watched : {true, false}) {
|
||||
regions.push_back(std::make_unique<Region>(RegionBytes));
|
||||
if (!RunCpuStore(watched, *regions.back(), failures)) return unwatchedImports();
|
||||
}
|
||||
if (!failures.empty()) {
|
||||
std::string message = "linear target direct write completion: at a completion point, the memory of a resident linear target did not hold the later of a direct buffer write with completion work and draws into it, or lost a CPU store made outside the draws:";
|
||||
for (const auto& failure : failures) message += "\n " + failure;
|
||||
Require(false, message);
|
||||
}
|
||||
AgcDriverShutdown_nid_postfix();
|
||||
std::puts("linear target direct write completion tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "prx/libc/include/GuestArena.hpp"
|
||||
#include "prx/libc/include/GuestWriteWatch.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
struct VulkanDeviceTestAccess {
|
||||
static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); }
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using AgcDriver::Graphics::StorageTexture;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Width = 64;
|
||||
constexpr std::uint32_t Height = 16;
|
||||
constexpr std::uint32_t Threads = 64;
|
||||
constexpr std::size_t BlockBytes = 65536;
|
||||
constexpr std::size_t TargetBytes = std::size_t{Width} * Height * 16u;
|
||||
constexpr std::uint32_t Rgba32FloatInfo = 0x8738u;
|
||||
constexpr std::uint32_t LinearAttrib3 = 0x09000000u;
|
||||
constexpr std::uint32_t Float32AbgrExport = 9;
|
||||
constexpr std::uint32_t ReadX = 10;
|
||||
constexpr std::uint32_t ReadY = 6;
|
||||
constexpr std::uint32_t Drawn = 0x3f800000u;
|
||||
constexpr std::uint32_t Unwritten = 0xdeadbeefu;
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{
|
||||
0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 7> CopyCode{0x7e000280u, 0xdc308000u, 0x01000000u, 0xbf8c3f70u, 0xdc708000u, 0x00020100u, 0xbf810000u};
|
||||
|
||||
constexpr std::array<std::array<float, 4>, 3> Triangle{{
|
||||
{-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f}
|
||||
}};
|
||||
|
||||
alignas(256) std::array<std::array<std::uint32_t, 16>, 2> Programs{};
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu};
|
||||
}
|
||||
|
||||
std::span<const std::uint32_t> Program(std::size_t index, bool blue) {
|
||||
auto& code = Programs.at(index);
|
||||
std::size_t at = 0;
|
||||
code[at++] = 0x36000000u | (20u << 17u) | 0xffu;
|
||||
code[at++] = 0x0000ffffu;
|
||||
code[at++] = 0x2c000000u | (21u << 17u) | 0x90u;
|
||||
code[at++] = 0x7e000c00u | (20u << 17u) | (256u + 20u);
|
||||
code[at++] = 0x7e000c00u | (21u << 17u) | (256u + 21u);
|
||||
code[at++] = (blue ? 0x7e0002f2u : 0x7e000280u) | (22u << 17u);
|
||||
code[at++] = 0x7e0002f2u | (23u << 17u);
|
||||
code[at++] = 0xf800180fu;
|
||||
code[at++] = 0x17161514u;
|
||||
code[at++] = 0xbf810000u;
|
||||
return std::span<const std::uint32_t>(code.data(), at);
|
||||
}
|
||||
|
||||
struct Block {
|
||||
explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) {
|
||||
#ifdef _WIN32
|
||||
if (watched) {
|
||||
data = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes));
|
||||
GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
}
|
||||
#else
|
||||
if (watched) {
|
||||
void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
Require(raw != MAP_FAILED, "cannot map a guest block");
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(raw);
|
||||
const auto aligned = (begin + BlockBytes - 1) & ~(static_cast<std::uintptr_t>(BlockBytes) - 1);
|
||||
if (aligned != begin) munmap(raw, aligned - begin);
|
||||
if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast<void*>(aligned + bytes), begin + BlockBytes - aligned);
|
||||
data = reinterpret_cast<std::uint8_t*>(aligned);
|
||||
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, bytes));
|
||||
}
|
||||
#endif
|
||||
Require(data != nullptr, "cannot allocate a guest block");
|
||||
Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "a guest block is not write-watched");
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Add(data, bytes, true, true, true);
|
||||
}
|
||||
~Block() {
|
||||
StorageTexture::FlushPending(Address(), bytes, nullptr, "test release");
|
||||
{
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Remove(data);
|
||||
}
|
||||
#ifdef _WIN32
|
||||
if (watched) {
|
||||
GuestArena::GuestArenaReset_nid_postfix(data, bytes);
|
||||
GuestArena::GuestArenaRelease_nid_postfix(data, bytes);
|
||||
} else {
|
||||
VirtualFree(data, 0, MEM_RELEASE);
|
||||
}
|
||||
#else
|
||||
if (watched) {
|
||||
munmap(data, bytes);
|
||||
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
std::free(data);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
Block(const Block&) = delete;
|
||||
Block& operator=(const Block&) = delete;
|
||||
std::uint64_t Address() const { return reinterpret_cast<std::uintptr_t>(data); }
|
||||
std::size_t bytes;
|
||||
bool watched;
|
||||
std::uint8_t* data = nullptr;
|
||||
};
|
||||
|
||||
AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) {
|
||||
AgcDriver::Registers cx;
|
||||
cx[0x318] = static_cast<std::uint32_t>(block.Address() >> 8u);
|
||||
cx[0x31b] = 0;
|
||||
cx[0x31c] = Rgba32FloatInfo;
|
||||
cx[0x31d] = 0;
|
||||
cx[0x323] = 0;
|
||||
cx[0x324] = 0;
|
||||
cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u);
|
||||
cx[0x3b8] = LinearAttrib3;
|
||||
cx[0x390] = static_cast<std::uint32_t>(block.Address() >> 40u);
|
||||
auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0);
|
||||
color.exportIndex = 0;
|
||||
return color;
|
||||
}
|
||||
|
||||
void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::span<const std::uint32_t> pixelCode, VkRect2D scissor) {
|
||||
const auto target = device.Target();
|
||||
constexpr std::uint32_t waveSize = 64;
|
||||
std::vector<std::uint32_t> vertexUserData(4, 0u);
|
||||
const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast<std::uint32_t>(Triangle.size()));
|
||||
std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin());
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> vertexMemory{{{reinterpret_cast<std::uintptr_t>(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}};
|
||||
ShaderRecompiler::RecompileRequest vertex{
|
||||
{ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(VertexCode.data()), VertexCode, 0, {}},
|
||||
{waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory},
|
||||
target,
|
||||
{0, 0, 0, 64}
|
||||
};
|
||||
vertex.useCache = false;
|
||||
const auto vertexResult = ShaderRecompiler::Recompile(vertex);
|
||||
const auto vertexPush = static_cast<std::uint32_t>(vertexResult.pushConstants.size());
|
||||
|
||||
ShaderRecompiler::ShaderPixelStageInfo pixel{};
|
||||
pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionFixedPoint);
|
||||
pixel.targetOutputMode.at(0) = Float32AbgrExport;
|
||||
pixel.targetExportMapping.fill(0xe4u);
|
||||
const std::vector<std::uint32_t> pixelUserData(8, 0u);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{reinterpret_cast<std::uintptr_t>(pixelCode.data()), std::as_bytes(pixelCode)}}};
|
||||
ShaderRecompiler::RecompileRequest fragment{
|
||||
{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(pixelCode.data()), pixelCode, 0, {}},
|
||||
{waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory},
|
||||
target,
|
||||
PixelPushLayout(vertexPush, target)
|
||||
};
|
||||
fragment.useCache = false;
|
||||
const auto pixelResult = ShaderRecompiler::Recompile(fragment);
|
||||
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{
|
||||
{ShaderStage::Vertex, &vertexResult, 0},
|
||||
{ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)}
|
||||
}};
|
||||
|
||||
VkPipelineColorBlendAttachmentState blend{};
|
||||
blend.blendEnable = VK_FALSE;
|
||||
blend.colorWriteMask = 0xf;
|
||||
AgcDriver::Graphics::State state{};
|
||||
state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt};
|
||||
state.colors = {color};
|
||||
state.color = color;
|
||||
state.hasColorTarget = true;
|
||||
state.renderExtent = {Width, Height};
|
||||
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
state.viewport = {0, static_cast<float>(Height), static_cast<float>(Width), -static_cast<float>(Height), 0, 1};
|
||||
state.negativeOneToOne = false;
|
||||
state.scissor = scissor;
|
||||
state.cullMode = VK_CULL_MODE_NONE;
|
||||
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
|
||||
state.blends = {blend};
|
||||
state.blend = blend;
|
||||
state.blendConstants = {};
|
||||
const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(Triangle.size()), 0, 1, 0, false};
|
||||
device.Draw(state, draw, shaders);
|
||||
}
|
||||
|
||||
void Dispatch(AgcDriver::VulkanDevice& device, std::uint64_t source, std::uint64_t destination) {
|
||||
constexpr std::uint32_t waveSize = 64;
|
||||
const std::array<std::uint32_t, 4> userData{static_cast<std::uint32_t>(source), static_cast<std::uint32_t>(source >> 32u), static_cast<std::uint32_t>(destination), static_cast<std::uint32_t>(destination >> 32u)};
|
||||
const std::span<const std::uint32_t> code(CopyCode);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1};
|
||||
ShaderRecompiler::RecompileRequest request{
|
||||
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
|
||||
{waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory},
|
||||
device.Target(),
|
||||
{0, 0, 0, 128}
|
||||
};
|
||||
request.useCache = false;
|
||||
const auto result = ShaderRecompiler::Recompile(request);
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
|
||||
}
|
||||
|
||||
std::string Hex(std::uint32_t value) {
|
||||
char text[16];
|
||||
std::snprintf(text, sizeof(text), "0x%08x", value);
|
||||
return text;
|
||||
}
|
||||
|
||||
std::uint32_t Dword(const std::uint8_t* bytes) {
|
||||
std::uint32_t value = 0;
|
||||
std::memcpy(&value, bytes, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
bool Expect(AgcDriver::VulkanDevice& device) {
|
||||
const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device);
|
||||
if (!context.bufferDeviceAddress) {
|
||||
std::puts("skipped, the device lacks bufferDeviceAddress");
|
||||
return false;
|
||||
}
|
||||
Block block(16 * BlockBytes);
|
||||
std::fill(block.data, block.data + block.bytes, std::uint8_t{0});
|
||||
Block output(BlockBytes);
|
||||
for (std::size_t offset = 0; offset < output.bytes; offset += sizeof(Unwritten)) std::memcpy(output.data + offset, &Unwritten, sizeof(Unwritten));
|
||||
if (AgcDriver::Graphics::HostImportFor(context, block.Address(), block.bytes) == nullptr) {
|
||||
std::puts("skipped, host imports are unavailable");
|
||||
return false;
|
||||
}
|
||||
const auto color = DecodeTarget(block);
|
||||
Require(color.tileMode == AgcDriver::Graphics::ColorTileMode::Linear && color.bytes == TargetBytes, "the target is not a linear R32G32B32A32 target");
|
||||
const auto* recorder = AgcDriver::Graphics::Recorder::Active();
|
||||
Require(recorder != nullptr, "the device has no recorder");
|
||||
Draw(device, color, Program(0, false), {{0, 0}, {Width, Height}});
|
||||
device.WaitIdle();
|
||||
Require(StorageTexture::FindPending(block.Address(), TargetBytes) == nullptr, "the linear target was still pending after the device wait");
|
||||
const auto blueOffset = (static_cast<std::size_t>(ReadY) * Width + ReadX) * 16u + 8u;
|
||||
Require(Dword(block.data + blueOffset) == 0u, "the first draw left " + Hex(Dword(block.data + blueOffset)) + " in the blue channel of pixel (" + std::to_string(ReadX) + ", " + std::to_string(ReadY) + ")");
|
||||
Draw(device, color, Program(1, true), {{8, 4}, {16, 8}});
|
||||
Require(recorder->Recording(), "the draw into the linear target was not left in the open batch");
|
||||
Dispatch(device, block.Address() + blueOffset, output.Address());
|
||||
device.WaitIdle();
|
||||
const auto read = Dword(output.data);
|
||||
Require(read == Drawn, "the address-based dispatch recorded after the draw read " + Hex(read) + " from the blue channel of pixel (" + std::to_string(ReadX) + ", " + std::to_string(ReadY) + "), not the drawn " + Hex(Drawn));
|
||||
Require(Dword(block.data + blueOffset) == Drawn, "the second draw left " + Hex(Dword(block.data + blueOffset)) + " in the blue channel of pixel (" + std::to_string(ReadX) + ", " + std::to_string(ReadY) + ")");
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
if (!Expect(*device)) return VulkanTestSkipped;
|
||||
std::puts("linear target dispatch access tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "prx/libc/include/GuestArena.hpp"
|
||||
#include "prx/libc/include/GuestWriteWatch.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
struct VulkanDeviceTestAccess {
|
||||
static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); }
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using AgcDriver::Graphics::StorageTexture;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Width = 64;
|
||||
constexpr std::uint32_t Height = 16;
|
||||
constexpr std::size_t BlockBytes = 65536;
|
||||
constexpr std::size_t TargetBytes = std::size_t{Width} * Height * 16u;
|
||||
constexpr std::size_t CopyTargetOffset = 0x40000;
|
||||
constexpr std::size_t FirstSourceOffset = 0x80000;
|
||||
constexpr std::size_t SecondSourceOffset = 0x90000;
|
||||
constexpr std::size_t PointedOffset = 0xa0000;
|
||||
constexpr std::uint32_t Rgba32FloatInfo = 0x8738u;
|
||||
constexpr std::uint32_t LinearAttrib3 = 0x09000000u;
|
||||
constexpr std::uint32_t Float32AbgrExport = 9;
|
||||
constexpr std::uint32_t FirstSource = 0x3e800000u;
|
||||
constexpr std::uint32_t SecondSource = 0x3f400000u;
|
||||
constexpr std::uint32_t Pointed = 0x3f800000u;
|
||||
constexpr std::uint32_t Unwritten = 0xdeadbeefu;
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{
|
||||
0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 7> PixelOpaque{0x7e100280u, 0x7e120280u, 0x7e140280u, 0x7e1602f2u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u};
|
||||
alignas(256) constexpr std::array<std::uint32_t, 11> PixelRead{0x7e1a0280u, 0xdc308000u, 0x0c02000du, 0xbf8c3f70u, 0x7e10030cu, 0x7e120280u, 0x7e140280u, 0x7e160280u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u};
|
||||
alignas(256) constexpr std::array<std::uint32_t, 13> PixelCopy{0x7e040280u, 0xe0301000u, 0x80010102u, 0xbf8c3f70u, 0xe0701000u, 0x80020102u, 0x7e100301u, 0x7e120280u, 0x7e140280u, 0x7e1602f2u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u};
|
||||
|
||||
constexpr std::array<std::array<float, 4>, 3> Triangle{{
|
||||
{-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f}
|
||||
}};
|
||||
|
||||
alignas(256) std::array<std::uint32_t, 4> FirstOutput{};
|
||||
alignas(256) std::array<std::uint32_t, 4> SecondOutput{};
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu};
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 4> RawBuffer(std::uint64_t address, std::uint32_t bytes) {
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu};
|
||||
}
|
||||
|
||||
struct Block {
|
||||
explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) {
|
||||
#ifdef _WIN32
|
||||
if (watched) {
|
||||
data = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes));
|
||||
GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
}
|
||||
#else
|
||||
if (watched) {
|
||||
void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
Require(raw != MAP_FAILED, "cannot map the color targets");
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(raw);
|
||||
const auto aligned = (begin + BlockBytes - 1) & ~(static_cast<std::uintptr_t>(BlockBytes) - 1);
|
||||
if (aligned != begin) munmap(raw, aligned - begin);
|
||||
if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast<void*>(aligned + bytes), begin + BlockBytes - aligned);
|
||||
data = reinterpret_cast<std::uint8_t*>(aligned);
|
||||
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, bytes));
|
||||
}
|
||||
#endif
|
||||
Require(data != nullptr, "cannot allocate the color targets");
|
||||
Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color targets are not write-watched");
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Add(data, bytes, true, true, true);
|
||||
}
|
||||
~Block() {
|
||||
StorageTexture::FlushPending(Address(), bytes, nullptr, "test release");
|
||||
{
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Remove(data);
|
||||
}
|
||||
#ifdef _WIN32
|
||||
if (watched) {
|
||||
GuestArena::GuestArenaReset_nid_postfix(data, bytes);
|
||||
GuestArena::GuestArenaRelease_nid_postfix(data, bytes);
|
||||
} else {
|
||||
VirtualFree(data, 0, MEM_RELEASE);
|
||||
}
|
||||
#else
|
||||
if (watched) {
|
||||
munmap(data, bytes);
|
||||
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
std::free(data);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
Block(const Block&) = delete;
|
||||
Block& operator=(const Block&) = delete;
|
||||
std::uint64_t Address() const { return reinterpret_cast<std::uintptr_t>(data); }
|
||||
std::size_t bytes;
|
||||
bool watched;
|
||||
std::uint8_t* data = nullptr;
|
||||
};
|
||||
|
||||
AgcDriver::Graphics::ColorTarget DecodeTarget(std::uint64_t address) {
|
||||
AgcDriver::Registers cx;
|
||||
cx[0x318] = static_cast<std::uint32_t>(address >> 8u);
|
||||
cx[0x31b] = 0;
|
||||
cx[0x31c] = Rgba32FloatInfo;
|
||||
cx[0x31d] = 0;
|
||||
cx[0x323] = 0;
|
||||
cx[0x324] = 0;
|
||||
cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u);
|
||||
cx[0x3b8] = LinearAttrib3;
|
||||
cx[0x390] = static_cast<std::uint32_t>(address >> 40u);
|
||||
auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0);
|
||||
color.exportIndex = 0;
|
||||
return color;
|
||||
}
|
||||
|
||||
void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::span<const std::uint32_t> pixelCode, std::span<const std::uint32_t> pixelUserData, VkRect2D scissor) {
|
||||
const auto target = device.Target();
|
||||
constexpr std::uint32_t waveSize = 64;
|
||||
std::vector<std::uint32_t> vertexUserData(4, 0u);
|
||||
const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast<std::uint32_t>(Triangle.size()));
|
||||
std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin());
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> vertexMemory{{{reinterpret_cast<std::uintptr_t>(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}};
|
||||
ShaderRecompiler::RecompileRequest vertex{
|
||||
{ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(VertexCode.data()), VertexCode, 0, {}},
|
||||
{waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory},
|
||||
target,
|
||||
{0, 0, 0, 64}
|
||||
};
|
||||
vertex.useCache = false;
|
||||
const auto vertexResult = ShaderRecompiler::Recompile(vertex);
|
||||
const auto vertexPush = static_cast<std::uint32_t>(vertexResult.pushConstants.size());
|
||||
|
||||
ShaderRecompiler::ShaderPixelStageInfo pixel{};
|
||||
pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionFixedPoint);
|
||||
pixel.targetOutputMode.at(0) = Float32AbgrExport;
|
||||
pixel.targetExportMapping.fill(0xe4u);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{reinterpret_cast<std::uintptr_t>(pixelCode.data()), std::as_bytes(pixelCode)}}};
|
||||
ShaderRecompiler::RecompileRequest fragment{
|
||||
{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(pixelCode.data()), pixelCode, 0, {}},
|
||||
{waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory},
|
||||
target,
|
||||
PixelPushLayout(vertexPush, target)
|
||||
};
|
||||
fragment.useCache = false;
|
||||
const auto pixelResult = ShaderRecompiler::Recompile(fragment);
|
||||
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{
|
||||
{ShaderStage::Vertex, &vertexResult, 0},
|
||||
{ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)}
|
||||
}};
|
||||
|
||||
VkPipelineColorBlendAttachmentState blend{};
|
||||
blend.blendEnable = VK_FALSE;
|
||||
blend.colorWriteMask = 0xf;
|
||||
AgcDriver::Graphics::State state{};
|
||||
state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt};
|
||||
state.colors = {color};
|
||||
state.color = color;
|
||||
state.hasColorTarget = true;
|
||||
state.renderExtent = {Width, Height};
|
||||
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
state.viewport = {0, static_cast<float>(Height), static_cast<float>(Width), -static_cast<float>(Height), 0, 1};
|
||||
state.negativeOneToOne = false;
|
||||
state.scissor = scissor;
|
||||
state.cullMode = VK_CULL_MODE_NONE;
|
||||
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
|
||||
state.blends = {blend};
|
||||
state.blend = blend;
|
||||
state.blendConstants = {};
|
||||
const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(Triangle.size()), 0, 1, 0, false};
|
||||
device.Draw(state, draw, shaders);
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 12> CopyUserData(std::uint64_t source, const std::array<std::uint32_t, 4>& output) {
|
||||
std::array<std::uint32_t, 12> userData{};
|
||||
const auto read = RawBuffer(source, 16u);
|
||||
const auto written = RawBuffer(reinterpret_cast<std::uintptr_t>(output.data()), 16u);
|
||||
std::copy(read.begin(), read.end(), userData.begin() + 4);
|
||||
std::copy(written.begin(), written.end(), userData.begin() + 8);
|
||||
return userData;
|
||||
}
|
||||
|
||||
std::array<float, 4> Texel(const std::uint8_t* target, std::uint32_t x, std::uint32_t y) {
|
||||
std::array<float, 4> texel{};
|
||||
std::memcpy(texel.data(), target + (static_cast<std::size_t>(y) * Width + x) * 16u, sizeof(texel));
|
||||
return texel;
|
||||
}
|
||||
|
||||
std::string Hex(std::uint32_t value) {
|
||||
char text[16];
|
||||
std::snprintf(text, sizeof(text), "0x%08x", value);
|
||||
return text;
|
||||
}
|
||||
|
||||
std::string Describe(const std::array<float, 4>& texel) {
|
||||
return "(" + std::to_string(texel[0]) + ", " + std::to_string(texel[1]) + ", " + std::to_string(texel[2]) + ", " + std::to_string(texel[3]) + ")";
|
||||
}
|
||||
|
||||
void ExpectTexels(const std::uint8_t* target, const char* what, std::uint32_t firstX, std::uint32_t lastX, const std::array<float, 4>& expected) {
|
||||
for (std::uint32_t y = 0; y < Height; ++y) {
|
||||
for (std::uint32_t x = firstX; x < lastX; ++x) {
|
||||
const auto texel = Texel(target, x, y);
|
||||
Require(texel == expected, std::string(what) + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") holds " + Describe(texel) + ", expected " + Describe(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Expect(AgcDriver::VulkanDevice& device, bool unwatch) {
|
||||
const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device);
|
||||
if (!context.bufferDeviceAddress) {
|
||||
std::puts("skipped, the device lacks bufferDeviceAddress");
|
||||
return false;
|
||||
}
|
||||
Block block(16 * BlockBytes);
|
||||
std::fill(block.data, block.data + block.bytes, std::uint8_t{0});
|
||||
const auto put = [&](std::size_t offset, std::uint32_t value, std::size_t dwords) {
|
||||
for (std::size_t dword = 0; dword < dwords; ++dword) std::memcpy(block.data + offset + dword * 4u, &value, sizeof(value));
|
||||
};
|
||||
put(FirstSourceOffset, FirstSource, 4);
|
||||
put(SecondSourceOffset, SecondSource, 4);
|
||||
put(PointedOffset, Pointed, 1);
|
||||
FirstOutput.fill(Unwritten);
|
||||
SecondOutput.fill(Unwritten);
|
||||
if (AgcDriver::Graphics::HostImportFor(context, block.Address(), block.bytes) == nullptr) {
|
||||
std::puts("skipped, host imports are unavailable");
|
||||
return false;
|
||||
}
|
||||
const auto drawn = DecodeTarget(block.Address());
|
||||
const auto copied = DecodeTarget(block.Address() + CopyTargetOffset);
|
||||
Require(drawn.tileMode == AgcDriver::Graphics::ColorTileMode::Linear && drawn.bytes == TargetBytes && copied.tileMode == AgcDriver::Graphics::ColorTileMode::Linear && copied.bytes == TargetBytes, "the targets are not linear R32G32B32A32 targets");
|
||||
auto* recorder = AgcDriver::Graphics::Recorder::Active();
|
||||
Require(recorder != nullptr && context.descriptorCache != nullptr, "the device has no recorder or descriptor cache");
|
||||
const auto pending = [](std::uint64_t address) { return StorageTexture::FindPending(address, TargetBytes) != nullptr; };
|
||||
const VkRect2D whole{{0, 0}, {Width, Height}};
|
||||
Draw(device, drawn, PixelOpaque, {}, whole);
|
||||
Draw(device, copied, PixelOpaque, {}, whole);
|
||||
device.WaitIdle();
|
||||
Require(!pending(drawn.address) && !pending(copied.address), "a linear target was still pending after the device wait");
|
||||
if (unwatch) AgcDriver::GuestMemory::Unwatch(block.Address(), block.bytes);
|
||||
const bool watched = AgcDriver::GuestMemory::Watched(copied.address, TargetBytes);
|
||||
Require(!unwatch || !watched, "the copied target is still write-watched after its block was taken out of the write watch");
|
||||
const auto submitted = recorder->Submissions();
|
||||
const auto pointed = block.Address() + PointedOffset;
|
||||
const std::array<std::uint32_t, 4> pointer{0u, 0u, static_cast<std::uint32_t>(pointed), static_cast<std::uint32_t>(pointed >> 32u)};
|
||||
Draw(device, drawn, PixelRead, pointer, whole);
|
||||
Require(pending(drawn.address), "the address-based draw did not leave its linear target pending");
|
||||
Require(recorder->Recording() && recorder->Submissions() == submitted, "the address-based draw did not stay in the open batch");
|
||||
const auto setsBefore = context.descriptorCache->Counters().sets;
|
||||
const auto firstCopy = CopyUserData(block.Address() + FirstSourceOffset, FirstOutput);
|
||||
Draw(device, copied, PixelCopy, firstCopy, {{0, 0}, {Width / 2u, Height}});
|
||||
Require(!pending(drawn.address), "the draw writing a copied buffer did not store the drawn linear target first");
|
||||
Require(pending(copied.address), "the first copying draw did not leave its linear target pending");
|
||||
Require(recorder->Recording() && recorder->Submissions() == submitted + 1, "storing the drawn linear target did not submit the batch the address-based draw read it in (" + std::to_string(recorder->Submissions() - submitted) + " submissions)");
|
||||
const auto setsBetween = context.descriptorCache->Counters().sets;
|
||||
const auto secondCopy = CopyUserData(block.Address() + SecondSourceOffset, SecondOutput);
|
||||
Draw(device, copied, PixelCopy, secondCopy, {{static_cast<std::int32_t>(Width / 2u), 0}, {Width / 2u, Height}});
|
||||
if (watched) Require(recorder->Recording() && recorder->Submissions() == submitted + 1, "the second copying draw submitted recorded work");
|
||||
else Require(recorder->Submissions() == submitted + 2, "the second copying draw into a stored target outside the write watch did not submit once to capture its bytes (" + std::to_string(recorder->Submissions() - submitted - 1) + " submissions)");
|
||||
const auto setsAfter = context.descriptorCache->Counters().sets;
|
||||
device.WaitIdle();
|
||||
Require(FirstOutput[0] == FirstSource, "the first copying draw's output holds " + Hex(FirstOutput[0]) + ", not its source's " + Hex(FirstSource));
|
||||
Require(SecondOutput[0] == SecondSource, "the second copying draw's output holds " + Hex(SecondOutput[0]) + ", not its source's " + Hex(SecondSource));
|
||||
ExpectTexels(block.data + CopyTargetOffset, "the first copying draw's half", 0, Width / 2u, {0.25f, 0.0f, 0.0f, 1.0f});
|
||||
ExpectTexels(block.data + CopyTargetOffset, "the second copying draw's half", Width / 2u, Width, {0.75f, 0.0f, 0.0f, 1.0f});
|
||||
ExpectTexels(block.data, "the address-based draw's target", 0, Width, {1.0f, 0.0f, 0.0f, 0.0f});
|
||||
Require(setsAfter - setsBetween == setsBetween - setsBefore, "the second copying draw allocated " + std::to_string(setsAfter - setsBetween) + " descriptor sets, the first " + std::to_string(setsBetween - setsBefore));
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
#ifdef _WIN32
|
||||
_putenv_s("APS5_RELEASE_ON_UNLOCK", "1");
|
||||
#else
|
||||
setenv("APS5_RELEASE_ON_UNLOCK", "1", 1);
|
||||
#endif
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
if (!Expect(*device, false)) return VulkanTestSkipped;
|
||||
if (!Expect(*device, true)) return VulkanTestSkipped;
|
||||
std::puts("linear target draw binding tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "prx/libc/include/GuestArena.hpp"
|
||||
#include "prx/libc/include/GuestWriteWatch.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
struct VulkanDeviceTestAccess {
|
||||
static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); }
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::size_t BlockBytes = 65536;
|
||||
constexpr std::size_t RegionBytes = 8 * BlockBytes;
|
||||
constexpr std::uint32_t Rgba32FloatInfo = 0x8738u;
|
||||
constexpr std::uint32_t LinearAttrib3 = 0x09000000u;
|
||||
constexpr std::uint32_t Float32AbgrExport = 9;
|
||||
constexpr std::uint8_t Initial = 0x3f;
|
||||
constexpr std::uint8_t Sentinel = 0x37;
|
||||
constexpr std::uint8_t Gap = 0x29;
|
||||
constexpr std::size_t GapBytes = 0x100;
|
||||
|
||||
struct Surface {
|
||||
std::size_t offset;
|
||||
std::uint32_t width;
|
||||
std::uint32_t height;
|
||||
std::size_t RowBytes() const { return std::size_t{width} * 16u; }
|
||||
std::size_t Bytes() const { return RowBytes() * height; }
|
||||
};
|
||||
|
||||
constexpr Surface Tall{0, 64, 256};
|
||||
constexpr Surface Shifted{6 * BlockBytes + GapBytes, 64, 16};
|
||||
constexpr VkRect2D Seed{{63, 255}, {1, 1}};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{
|
||||
0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000,
|
||||
};
|
||||
|
||||
constexpr std::array<std::array<float, 4>, 3> Triangle{{
|
||||
{-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f}
|
||||
}};
|
||||
|
||||
alignas(256) std::array<std::array<std::uint32_t, 16>, 2> Programs{};
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu};
|
||||
}
|
||||
|
||||
std::span<const std::uint32_t> Program(std::size_t index, bool blue) {
|
||||
auto& code = Programs.at(index);
|
||||
std::size_t at = 0;
|
||||
code[at++] = 0x36000000u | (20u << 17u) | 0xffu;
|
||||
code[at++] = 0x0000ffffu;
|
||||
code[at++] = 0x2c000000u | (21u << 17u) | 0x90u;
|
||||
code[at++] = 0x7e000c00u | (20u << 17u) | (256u + 20u);
|
||||
code[at++] = 0x7e000c00u | (21u << 17u) | (256u + 21u);
|
||||
code[at++] = (blue ? 0x7e0002f2u : 0x7e000280u) | (22u << 17u);
|
||||
code[at++] = 0x7e0002f2u | (23u << 17u);
|
||||
code[at++] = 0xf800180fu;
|
||||
code[at++] = 0x17161514u;
|
||||
code[at++] = 0xbf810000u;
|
||||
return std::span<const std::uint32_t>(code.data(), at);
|
||||
}
|
||||
|
||||
struct Block {
|
||||
explicit Block(std::size_t bytes) : bytes(bytes), arena(AgcDriver::GuestMemory::WriteWatched()) {
|
||||
#ifdef _WIN32
|
||||
if (arena) {
|
||||
data = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes));
|
||||
GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
}
|
||||
#else
|
||||
if (arena) {
|
||||
void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
Require(raw != MAP_FAILED, "cannot map the color targets");
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(raw);
|
||||
const auto aligned = (begin + BlockBytes - 1) & ~(static_cast<std::uintptr_t>(BlockBytes) - 1);
|
||||
if (aligned != begin) munmap(raw, aligned - begin);
|
||||
if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast<void*>(aligned + bytes), begin + BlockBytes - aligned);
|
||||
data = reinterpret_cast<std::uint8_t*>(aligned);
|
||||
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, bytes));
|
||||
}
|
||||
#endif
|
||||
Require(data != nullptr, "cannot allocate the color targets");
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Add(data, bytes, true, true, true);
|
||||
}
|
||||
~Block() {
|
||||
AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release");
|
||||
{
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Remove(data);
|
||||
}
|
||||
#ifdef _WIN32
|
||||
if (arena) {
|
||||
GuestArena::GuestArenaReset_nid_postfix(data, bytes);
|
||||
GuestArena::GuestArenaRelease_nid_postfix(data, bytes);
|
||||
} else {
|
||||
VirtualFree(data, 0, MEM_RELEASE);
|
||||
}
|
||||
#else
|
||||
if (arena) {
|
||||
munmap(data, bytes);
|
||||
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
std::free(data);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
Block(const Block&) = delete;
|
||||
Block& operator=(const Block&) = delete;
|
||||
std::uint64_t Address() const { return reinterpret_cast<std::uintptr_t>(data); }
|
||||
std::size_t bytes;
|
||||
bool arena;
|
||||
std::uint8_t* data = nullptr;
|
||||
};
|
||||
|
||||
AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, const Surface& surface) {
|
||||
const auto address = block.Address() + surface.offset;
|
||||
AgcDriver::Registers cx;
|
||||
cx[0x318] = static_cast<std::uint32_t>(address >> 8u);
|
||||
cx[0x31b] = 0;
|
||||
cx[0x31c] = Rgba32FloatInfo;
|
||||
cx[0x31d] = 0;
|
||||
cx[0x323] = 0;
|
||||
cx[0x324] = 0;
|
||||
cx[0x3b0] = ((surface.width - 1u) << 14u) | (surface.height - 1u);
|
||||
cx[0x3b8] = LinearAttrib3;
|
||||
cx[0x390] = static_cast<std::uint32_t>(address >> 40u);
|
||||
auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0);
|
||||
color.exportIndex = 0;
|
||||
return color;
|
||||
}
|
||||
|
||||
void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, const Surface& surface, std::span<const std::uint32_t> pixelCode, VkRect2D scissor) {
|
||||
const auto target = device.Target();
|
||||
constexpr std::uint32_t waveSize = 64;
|
||||
std::vector<std::uint32_t> vertexUserData(4, 0u);
|
||||
const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast<std::uint32_t>(Triangle.size()));
|
||||
std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin());
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> vertexMemory{{{reinterpret_cast<std::uintptr_t>(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}};
|
||||
ShaderRecompiler::RecompileRequest vertex{
|
||||
{ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(VertexCode.data()), VertexCode, 0, {}},
|
||||
{waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory},
|
||||
target,
|
||||
{0, 0, 0, 64}
|
||||
};
|
||||
vertex.useCache = false;
|
||||
const auto vertexResult = ShaderRecompiler::Recompile(vertex);
|
||||
const auto vertexPush = static_cast<std::uint32_t>(vertexResult.pushConstants.size());
|
||||
|
||||
ShaderRecompiler::ShaderPixelStageInfo pixel{};
|
||||
pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionFixedPoint);
|
||||
pixel.targetOutputMode.at(0) = Float32AbgrExport;
|
||||
pixel.targetExportMapping.fill(0xe4u);
|
||||
const std::vector<std::uint32_t> pixelUserData(8, 0u);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{reinterpret_cast<std::uintptr_t>(pixelCode.data()), std::as_bytes(pixelCode)}}};
|
||||
ShaderRecompiler::RecompileRequest fragment{
|
||||
{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(pixelCode.data()), pixelCode, 0, {}},
|
||||
{waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory},
|
||||
target,
|
||||
PixelPushLayout(vertexPush, target)
|
||||
};
|
||||
fragment.useCache = false;
|
||||
const auto pixelResult = ShaderRecompiler::Recompile(fragment);
|
||||
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{
|
||||
{ShaderStage::Vertex, &vertexResult, 0},
|
||||
{ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)}
|
||||
}};
|
||||
|
||||
VkPipelineColorBlendAttachmentState blend{};
|
||||
blend.blendEnable = VK_FALSE;
|
||||
blend.colorWriteMask = 0xf;
|
||||
AgcDriver::Graphics::State state{};
|
||||
state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt};
|
||||
state.colors = {color};
|
||||
state.color = color;
|
||||
state.hasColorTarget = true;
|
||||
state.renderExtent = {surface.width, surface.height};
|
||||
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
state.viewport = {0, static_cast<float>(surface.height), static_cast<float>(surface.width), -static_cast<float>(surface.height), 0, 1};
|
||||
state.negativeOneToOne = false;
|
||||
state.scissor = scissor;
|
||||
state.cullMode = VK_CULL_MODE_NONE;
|
||||
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
|
||||
state.blends = {blend};
|
||||
state.blend = blend;
|
||||
state.blendConstants = {};
|
||||
const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(Triangle.size()), 0, 1, 0, false};
|
||||
device.Draw(state, draw, shaders);
|
||||
}
|
||||
|
||||
bool Inside(const VkRect2D& rect, std::uint32_t x, std::uint32_t y) {
|
||||
return x >= static_cast<std::uint32_t>(rect.offset.x) && x < static_cast<std::uint32_t>(rect.offset.x) + rect.extent.width && y >= static_cast<std::uint32_t>(rect.offset.y) && y < static_cast<std::uint32_t>(rect.offset.y) + rect.extent.height;
|
||||
}
|
||||
|
||||
std::uint8_t* Pixel(const Block& block, const Surface& surface, std::uint32_t x, std::uint32_t y) {
|
||||
return block.data + surface.offset + static_cast<std::size_t>(y) * surface.RowBytes() + static_cast<std::size_t>(x) * 16u;
|
||||
}
|
||||
|
||||
bool Holds(const std::uint8_t* pixel, std::uint8_t value) {
|
||||
return std::all_of(pixel, pixel + 16, [&](std::uint8_t byte) { return byte == value; });
|
||||
}
|
||||
|
||||
bool HoldsOutput(const std::uint8_t* pixel, std::uint32_t x, std::uint32_t y, float z) {
|
||||
std::array<float, 4> texel{};
|
||||
std::memcpy(texel.data(), pixel, sizeof(texel));
|
||||
return texel == std::array<float, 4>{static_cast<float>(x), static_cast<float>(y), z, 1.0f};
|
||||
}
|
||||
|
||||
bool Expect(AgcDriver::VulkanDevice& device) {
|
||||
Block block(RegionBytes);
|
||||
std::fill(block.data, block.data + block.bytes, Initial);
|
||||
const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device);
|
||||
if (AgcDriver::Graphics::HostImportFor(context, block.Address(), block.bytes) == nullptr) return false;
|
||||
const auto tall = DecodeTarget(block, Tall);
|
||||
const auto shifted = DecodeTarget(block, Shifted);
|
||||
Require(tall.tileMode == AgcDriver::Graphics::ColorTileMode::Linear && tall.bytes == Tall.Bytes() && shifted.bytes == Shifted.Bytes(), "the targets are not linear R32G32B32A32 targets");
|
||||
constexpr VkRect2D first{{8, 4}, {16, 16}};
|
||||
constexpr VkRect2D second{{16, 12}, {24, 16}};
|
||||
constexpr VkRect2D whole{{0, 0}, {Shifted.width, Shifted.height}};
|
||||
Draw(device, tall, Tall, Program(0, false), Seed);
|
||||
device.WaitIdle();
|
||||
Draw(device, tall, Tall, Program(0, false), first);
|
||||
Draw(device, tall, Tall, Program(1, true), second);
|
||||
Draw(device, shifted, Shifted, Program(0, false), whole);
|
||||
const std::array<std::array<std::uint32_t, 2>, 3> sentinels{{{50, 10}, {20, 40}, {30, 200}}};
|
||||
const std::array<std::array<std::uint32_t, 2>, 2> between{{{30, 6}, {10, 25}}};
|
||||
for (const auto& [x, y] : sentinels) std::memset(Pixel(block, Tall, x, y), Sentinel, 16);
|
||||
for (const auto& [x, y] : between) std::memset(Pixel(block, Tall, x, y), Sentinel, 16);
|
||||
Require((block.Address() + Shifted.offset) / BlockBytes * BlockBytes == block.Address() + Shifted.offset - GapBytes, "the shifted target does not start past a 64 KiB boundary");
|
||||
std::memset(block.data + Shifted.offset - GapBytes, Gap, GapBytes);
|
||||
device.WaitIdle();
|
||||
std::size_t wrongDrawn = 0, wrongSentinels = 0, wrongBetween = 0, wrongUntouched = 0, wrongShifted = 0;
|
||||
for (std::uint32_t y = 0; y < Tall.height; ++y) {
|
||||
for (std::uint32_t x = 0; x < Tall.width; ++x) {
|
||||
const auto* pixel = Pixel(block, Tall, x, y);
|
||||
const auto at = [&](const auto& point) { return point[0] == x && point[1] == y; };
|
||||
if (std::any_of(sentinels.begin(), sentinels.end(), at)) wrongSentinels += Holds(pixel, Sentinel) ? 0u : 1u;
|
||||
else if (std::any_of(between.begin(), between.end(), at)) wrongBetween += Holds(pixel, Sentinel) ? 0u : 1u;
|
||||
else if (Inside(second, x, y)) wrongDrawn += HoldsOutput(pixel, x, y, 1.0f) ? 0u : 1u;
|
||||
else if (Inside(first, x, y) || Inside(Seed, x, y)) wrongDrawn += HoldsOutput(pixel, x, y, 0.0f) ? 0u : 1u;
|
||||
else wrongUntouched += Holds(pixel, Initial) ? 0u : 1u;
|
||||
}
|
||||
}
|
||||
for (std::uint32_t y = 0; y < Shifted.height; ++y) {
|
||||
for (std::uint32_t x = 0; x < Shifted.width; ++x) wrongShifted += HoldsOutput(Pixel(block, Shifted, x, y), x, y, 0.0f) ? 0u : 1u;
|
||||
}
|
||||
const auto gapLost = static_cast<std::size_t>(std::count_if(block.data + Shifted.offset - GapBytes, block.data + Shifted.offset, [](std::uint8_t byte) { return byte != Gap; }));
|
||||
std::printf("linear target rectangle bound: wrong drawn pixels %zu, lost sentinels %zu/%zu, lost sentinels between the rectangles %zu/%zu, changed untouched pixels %zu, wrong shifted-target pixels %zu, lost gap bytes %zu/%zu\n", wrongDrawn, wrongSentinels, sentinels.size(), wrongBetween, between.size(), wrongUntouched, wrongShifted, gapLost, GapBytes);
|
||||
Require(wrongDrawn == 0 && wrongShifted == 0, "a drawn linear target's store lost pixels its draws wrote");
|
||||
Require(wrongSentinels == 0 && gapLost == 0, "a drawn linear target's store overwrote CPU stores outside the drawn rectangles");
|
||||
Require(wrongBetween == 0, "a drawn linear target's store overwrote CPU stores between two draws' scissor rectangles");
|
||||
Require(wrongUntouched == 0, "a drawn linear target's store changed pixels no draw and no CPU store wrote");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ExpectManyRows(AgcDriver::VulkanDevice& device) {
|
||||
Block block(RegionBytes);
|
||||
std::fill(block.data, block.data + block.bytes, Initial);
|
||||
const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device);
|
||||
if (AgcDriver::Graphics::HostImportFor(context, block.Address(), block.bytes) == nullptr) return false;
|
||||
const auto tall = DecodeTarget(block, Tall);
|
||||
constexpr std::uint32_t Rows = 17;
|
||||
constexpr std::uint32_t Spacing = 15;
|
||||
Draw(device, tall, Tall, Program(0, false), Seed);
|
||||
device.WaitIdle();
|
||||
for (std::uint32_t row = 0; row < Rows; ++row) Draw(device, tall, Tall, Program(0, false), VkRect2D{{8, static_cast<std::int32_t>(row * Spacing)}, {16, 1}});
|
||||
constexpr std::array<std::uint32_t, 2> between{10, 70};
|
||||
std::memset(Pixel(block, Tall, between[0], between[1]), Sentinel, 16);
|
||||
device.WaitIdle();
|
||||
std::size_t wrongDrawn = 0, wrongUntouched = 0;
|
||||
for (std::uint32_t y = 0; y < Tall.height; ++y) {
|
||||
const bool drawnRow = y % Spacing == 0 && y / Spacing < Rows;
|
||||
for (std::uint32_t x = 0; x < Tall.width; ++x) {
|
||||
if (x == between[0] && y == between[1]) continue;
|
||||
const auto* pixel = Pixel(block, Tall, x, y);
|
||||
if ((drawnRow && x >= 8 && x < 24) || Inside(Seed, x, y)) wrongDrawn += HoldsOutput(pixel, x, y, 0.0f) ? 0u : 1u;
|
||||
else wrongUntouched += Holds(pixel, Initial) ? 0u : 1u;
|
||||
}
|
||||
}
|
||||
const bool kept = Holds(Pixel(block, Tall, between[0], between[1]), Sentinel);
|
||||
std::printf("linear target rectangle bound, %u one-row scissors: wrong drawn pixels %zu, sentinel between the rows %s, changed untouched pixels %zu\n", Rows, wrongDrawn, kept ? "kept" : "lost", wrongUntouched);
|
||||
Require(wrongDrawn == 0, "a drawn linear target's store lost pixels its one-row draws wrote");
|
||||
Require(kept, "a drawn linear target's store overwrote a CPU store between two one-row scissors");
|
||||
Require(wrongUntouched == 0, "a drawn linear target's store changed pixels no one-row draw and no CPU store wrote");
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
if (!Expect(*device) || !ExpectManyRows(*device)) {
|
||||
std::puts("linear target rectangle bound test skipped: no host import on this device");
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
std::puts("linear target rectangle bound tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -704,6 +704,48 @@ void CheckDepthAttachmentAlias() {
|
||||
std::puts("native depth attachment alias changes layout while retaining held GPU views");
|
||||
}
|
||||
|
||||
void CheckStorageAliasRetirement() {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
Require(device != nullptr, "native depth storage alias requires Vulkan");
|
||||
const auto context = device->graphicsContext();
|
||||
constexpr VkExtent2D extent{5u, 3u};
|
||||
constexpr std::uint32_t samples = 4u;
|
||||
constexpr std::size_t prefix = 65536u;
|
||||
const auto bytes = static_cast<std::size_t>(MultisampledDepthBytes_nid_no_patch(extent, 4u, samples));
|
||||
TargetMemory memory(prefix + bytes), metadata(65536u);
|
||||
std::memset(memory.Data(), 0x3c, prefix + bytes);
|
||||
std::memset(metadata.Data(), 0, 65536u);
|
||||
struct Cleanup {
|
||||
AgcDriver::VulkanDevice& device;
|
||||
~Cleanup() { device.WaitIdle(); ClearCachedTextures(device.Device()); ClearDepthSurfaces(device.Device()); }
|
||||
} cleanup{*device};
|
||||
GuestTextureResource alias{};
|
||||
alias.baseAddress = memory.Address();
|
||||
alias.width = 256u;
|
||||
alias.height = static_cast<std::uint32_t>((prefix + bytes) / 1024u);
|
||||
alias.mipCount = alias.allocatedMipCount = 1u;
|
||||
alias.dimension = TextureDimension::k2D;
|
||||
alias.tileMode = TextureTileMode::kLinear;
|
||||
alias.format = 22u;
|
||||
alias.dstSelX = 0u;
|
||||
alias.dstSelY = alias.dstSelZ = 4u;
|
||||
alias.dstSelW = 5u;
|
||||
auto cached = CachedStorageSurface(context, alias);
|
||||
Require(cached != nullptr, "storage alias was not cached before native depth creation");
|
||||
DepthTarget depth{memory.Address() + prefix, 0u, extent, VK_FORMAT_D32_SFLOAT, 0.0f, 0u, metadata.Address(), false, samples};
|
||||
depth.depthSwizzle = 24u;
|
||||
depth.iterate256 = depth.htilePipeAligned = depth.htileDepthOnly = true;
|
||||
Require(DepthSurfaceView(context, depth) != VK_NULL_HANDLE && DepthSurfaceAt(depth.address), "storage alias fixture has no native depth owner");
|
||||
auto disjoint = alias;
|
||||
disjoint.height = static_cast<std::uint32_t>(prefix / 1024u);
|
||||
Require(CachedStorageSurface(context, disjoint) != nullptr, "disjoint storage alias was not created");
|
||||
Require(DepthSurfaceAt(depth.address), "storage access ending at native depth retired a disjoint owner");
|
||||
Require(CachedStorageSurface(context, alias) != nullptr, "overlapping cached storage alias was not returned");
|
||||
Require(!DepthSurfaceAt(depth.address), "a cached storage view kept a native depth owner inside its range");
|
||||
for (std::size_t offset = 0u; offset < prefix + bytes; ++offset) Require(memory.Data()[offset] == std::byte{0x3c}, "retiring unwritten native depth changed guest bytes");
|
||||
std::puts("cached storage span retires only intersecting native depth owners");
|
||||
}
|
||||
|
||||
enum class ClearFault { None, PartialStore, CpuWrite, DepthWrite, BadMetadata, UnprovenDispatch, PendingWrite, Overlap, PossibleStore, CompletedPossibleStore, DriverDispatch, ImportWindowWrite };
|
||||
|
||||
void CheckClearDispatch() {
|
||||
@@ -1337,6 +1379,7 @@ int main(int argc, char** argv) {
|
||||
CheckDrawFeedback();
|
||||
const auto context = device->GetContext();
|
||||
if (argc == 1) CheckDepthAttachmentAlias();
|
||||
if (argc == 1) CheckStorageAliasRetirement();
|
||||
std::unique_lock lock(AgcDriver::GuestMemory::GpuMutex());
|
||||
Require(DepthSupported(context, VK_FORMAT_D32_SFLOAT, 4u), "the native depth regression requires sampled D32 with four samples");
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--clear-refusals") {
|
||||
|
||||
@@ -64,6 +64,8 @@ struct PipelineTrace {
|
||||
std::mutex mutex;
|
||||
std::condition_variable released;
|
||||
bool allowOptimization = false;
|
||||
std::uint32_t libraries = 0;
|
||||
std::uint32_t retaining = 0;
|
||||
std::vector<VkPipeline> fast;
|
||||
std::vector<VkPipeline> optimized;
|
||||
VkCommandBuffer boundCommands = VK_NULL_HANDLE;
|
||||
@@ -90,7 +92,12 @@ VKAPI_ATTR VkResult VKAPI_CALL CreatePipelines(VkDevice device, VkPipelineCache
|
||||
const auto result = Trace->create(device, cache, count, infos, allocator, pipelines);
|
||||
std::lock_guard lock(Trace->mutex);
|
||||
for (std::uint32_t index = 0; index < count; ++index) {
|
||||
if (pipelines[index] == VK_NULL_HANDLE || (infos[index].flags & VK_PIPELINE_CREATE_LIBRARY_BIT_KHR) != 0) continue;
|
||||
if (pipelines[index] == VK_NULL_HANDLE) continue;
|
||||
if ((infos[index].flags & VK_PIPELINE_CREATE_LIBRARY_BIT_KHR) != 0) {
|
||||
++Trace->libraries;
|
||||
if ((infos[index].flags & VK_PIPELINE_CREATE_RETAIN_LINK_TIME_OPTIMIZATION_INFO_BIT_EXT) != 0) ++Trace->retaining;
|
||||
continue;
|
||||
}
|
||||
auto& handles = (infos[index].flags & VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT) != 0 ? Trace->optimized : Trace->fast;
|
||||
handles.push_back(pipelines[index]);
|
||||
}
|
||||
@@ -217,12 +224,17 @@ void Run(AgcDriver::VulkanDevice& device, Context context, PipelineTrace& trace,
|
||||
CheckPixels(masked, phase);
|
||||
};
|
||||
render(false, "fast link");
|
||||
{
|
||||
std::lock_guard lock(trace.mutex);
|
||||
Require(trace.libraries != 0, "the readback pipeline's libraries were not created through the traced device");
|
||||
Require(trace.retaining == (optimize ? trace.libraries : 0u), std::to_string(trace.retaining) + " of the " + std::to_string(trace.libraries) + " libraries were created to retain link-time optimization info, expected " + (optimize ? "all of them" : "none, since no optimized link will be made"));
|
||||
}
|
||||
trace.ReleaseOptimization();
|
||||
WaitForOptimizedPipelines(context.device);
|
||||
Require(PipelineLibraryCountersOf(context.device).linked == 1, "the readback pipeline was not linked from graphics libraries");
|
||||
Require(trace.optimized.size() == (optimize ? 1u : 0u), "the optimized pipeline creation count differs from the requested mode");
|
||||
render(optimize, "completed link");
|
||||
std::printf("pipeline readback passed: fast draws %u, optimized draws %u, optimized creations %zu\n", optimize ? 1u : 2u, optimize ? 1u : 0u, trace.optimized.size());
|
||||
std::printf("pipeline readback passed: fast draws %u, optimized draws %u, optimized creations %zu, libraries %u (%u retaining link-time optimization info)\n", optimize ? 1u : 2u, optimize ? 1u : 0u, trace.optimized.size(), trace.libraries, trace.retaining);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#endif
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Threads = 32;
|
||||
constexpr std::uint32_t Side = 128;
|
||||
constexpr std::uint64_t DisplayFormat = 0x8000000000000000ull;
|
||||
constexpr std::size_t SurfaceBytes = 65536;
|
||||
constexpr std::size_t KeysOffset = SurfaceBytes;
|
||||
constexpr std::size_t KeysCapacity = 32768;
|
||||
constexpr std::size_t CopiedKeys = 256;
|
||||
constexpr std::size_t SourceKeysOffset = KeysOffset + KeysCapacity;
|
||||
constexpr std::size_t BlockBytes = 2 * 65536;
|
||||
constexpr std::uint8_t Uncompressed = 0xff;
|
||||
constexpr std::uint8_t Cleared = 0x00;
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 7> CopyKeysCode{
|
||||
0x34020083, 0xe0341000, 0x80000201, 0xbf8c3f70, 0xe0741000, 0x80010201, 0xbf810000,
|
||||
};
|
||||
|
||||
std::uint64_t AddressOf(const void* data) {
|
||||
return static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(data));
|
||||
}
|
||||
|
||||
class GuestBlock {
|
||||
public:
|
||||
GuestBlock() {
|
||||
#ifdef _WIN32
|
||||
block = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
#else
|
||||
block = static_cast<std::uint8_t*>(std::aligned_alloc(65536, BlockBytes));
|
||||
#endif
|
||||
Require(block != nullptr, "present key writer wait: cannot allocate the guest block");
|
||||
GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true);
|
||||
}
|
||||
|
||||
~GuestBlock() {
|
||||
GuestAllocations::Mutation().Remove(block);
|
||||
#ifdef _WIN32
|
||||
VirtualFree(block, 0, MEM_RELEASE);
|
||||
#else
|
||||
std::free(block);
|
||||
#endif
|
||||
}
|
||||
|
||||
GuestBlock(const GuestBlock&) = delete;
|
||||
GuestBlock& operator=(const GuestBlock&) = delete;
|
||||
|
||||
std::uint8_t* Data() { return block; }
|
||||
|
||||
private:
|
||||
std::uint8_t* block = nullptr;
|
||||
};
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
|
||||
const auto address = AddressOf(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu};
|
||||
}
|
||||
|
||||
ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData) {
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1};
|
||||
ShaderRecompiler::RecompileRequest request{
|
||||
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
|
||||
{32, 0, userData, compute, std::nullopt, std::nullopt, memory},
|
||||
device.Target(),
|
||||
{0, 0, 0, 128}
|
||||
};
|
||||
request.useCache = false;
|
||||
return ShaderRecompiler::Recompile(request);
|
||||
}
|
||||
|
||||
struct Scenario {
|
||||
std::uint8_t* surface;
|
||||
std::uint8_t* keys;
|
||||
std::uint8_t* source;
|
||||
std::size_t keyBytes;
|
||||
AgcDriver::DisplayBuffer buffer;
|
||||
std::vector<std::uint32_t> copyData;
|
||||
};
|
||||
|
||||
Scenario Prepare(std::uint8_t* block) {
|
||||
Scenario scenario{block, block + KeysOffset, block + SourceKeysOffset, 0, {AddressOf(block), DisplayFormat, Side, Side}, std::vector<std::uint32_t>(16, 0u)};
|
||||
scenario.buffer.dccAddress = AddressOf(scenario.keys);
|
||||
scenario.keyBytes = AgcDriver::DisplayBufferKeyBytes(scenario.buffer);
|
||||
Require(AgcDriver::DisplayBufferSize(scenario.buffer) <= SurfaceBytes && scenario.keyBytes >= CopiedKeys && scenario.keyBytes <= KeysCapacity, "present key writer wait: the display buffer does not fit the guest block");
|
||||
const auto source = BufferDescriptor(scenario.source, CopiedKeys);
|
||||
const auto destination = BufferDescriptor(scenario.keys, CopiedKeys);
|
||||
std::copy(source.begin(), source.end(), scenario.copyData.begin());
|
||||
std::copy(destination.begin(), destination.end(), scenario.copyData.begin() + 4);
|
||||
return scenario;
|
||||
}
|
||||
|
||||
void Reset(const Scenario& scenario) {
|
||||
std::memset(scenario.surface, 0x55, SurfaceBytes);
|
||||
std::memset(scenario.keys, Uncompressed, scenario.keyBytes);
|
||||
std::memset(scenario.keys, Cleared, CopiedKeys);
|
||||
std::memset(scenario.source, Uncompressed, CopiedKeys);
|
||||
AgcDriver::GuestMemory::BumpCollectEpoch();
|
||||
}
|
||||
|
||||
std::string PresentFailure(AgcDriver::VulkanDevice& device, const AgcDriver::DisplayBuffer& buffer) {
|
||||
try {
|
||||
device.PresentDisplayBuffer(buffer);
|
||||
} catch (const std::exception& error) {
|
||||
return error.what();
|
||||
}
|
||||
return "the headless present succeeded";
|
||||
}
|
||||
|
||||
const std::string NoSwapchain = "Vulkan presentation: device has no swapchain";
|
||||
|
||||
bool CanCopyKeys(AgcDriver::VulkanDevice& device, const Scenario& scenario) {
|
||||
const std::array<std::uint32_t, 4> pattern{0xffffffffu, 0xffffffffu, 0xffffffffu, 0xffffffffu};
|
||||
std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex());
|
||||
const bool filled = device.FillBuffer(AddressOf(scenario.keys), CopiedKeys, pattern);
|
||||
device.WaitIdle();
|
||||
return filled;
|
||||
}
|
||||
|
||||
void RunPendingWriter(AgcDriver::VulkanDevice& device, const Scenario& scenario) {
|
||||
Reset(scenario);
|
||||
const auto copy = Compile(device, CopyKeysCode, scenario.copyData);
|
||||
std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex());
|
||||
Require(AgcDriver::GuestMemory::GpuMutex().DepthOnThisThread() == 1, "present key writer wait: the presenter's hold is not the outermost");
|
||||
device.Dispatch(copy, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(CopyKeysCode.data()));
|
||||
Require(AgcDriver::Graphics::DccKeyWritersPending(scenario.buffer.dccAddress, scenario.keyBytes), "a recorded kernel that copies data over the display keys is not seen as a pending key writer");
|
||||
const auto holds = AgcDriver::GuestMemory::GpuHoldSerial();
|
||||
const auto failure = PresentFailure(device, scenario.buffer);
|
||||
const auto acquired = AgcDriver::GuestMemory::GpuHoldSerial() - holds;
|
||||
Require(failure == NoSwapchain, "a present behind a pending writer of the display keys stopped before the swapchain: " + failure);
|
||||
Require(acquired != 0, "the presenter synchronized with the display keys' writer without releasing the GPU mutex");
|
||||
Require(!AgcDriver::Graphics::DccKeyWritersPending(scenario.buffer.dccAddress, scenario.keyBytes), "the display keys' writer is still pending after the present");
|
||||
Require(scenario.keys[0] == Uncompressed && scenario.keys[CopiedKeys - 1] == Uncompressed, "the present read the display keys before their writer landed");
|
||||
}
|
||||
|
||||
void RunNoWriter(AgcDriver::VulkanDevice& device, const Scenario& scenario) {
|
||||
std::memset(scenario.keys, Uncompressed, scenario.keyBytes);
|
||||
AgcDriver::GuestMemory::BumpCollectEpoch();
|
||||
std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex());
|
||||
device.WaitIdle();
|
||||
Require(!AgcDriver::Graphics::DccKeyWritersPending(scenario.buffer.dccAddress, scenario.keyBytes), "the idle device still reports a pending writer of the display keys");
|
||||
const auto holds = AgcDriver::GuestMemory::GpuHoldSerial();
|
||||
const auto failure = PresentFailure(device, scenario.buffer);
|
||||
const auto acquired = AgcDriver::GuestMemory::GpuHoldSerial() - holds;
|
||||
Require(failure == NoSwapchain, "a present with no pending writer of the display keys stopped before the swapchain: " + failure);
|
||||
Require(acquired == 0, "a present with no pending writer of the display keys released the GPU mutex");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
if (!device->CanWaitUnlocked()) {
|
||||
std::puts("skipped, the device has no timeline semaphores: the presenter cannot wait without the GPU mutex");
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
GuestBlock block;
|
||||
const auto scenario = Prepare(block.Data());
|
||||
if (!CanCopyKeys(*device, scenario)) {
|
||||
std::puts("skipped, host imports are unavailable: the key writer of the test is a copied writer");
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
RunPendingWriter(*device, scenario);
|
||||
RunNoWriter(*device, scenario);
|
||||
std::puts("present key writer wait tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -371,6 +371,11 @@ int main() {
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
Require(AgcDriver::Graphics::StorageTexture::AnyPending(), "queued index read: the image write left no pending result to order against");
|
||||
Require(IndicesIntact(), "queued index read: the image's results reached guest memory before any consumer asked");
|
||||
const auto copiedAddress = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(Copied));
|
||||
const auto heldQueries = AgcDriver::DriverDetail::DrawPipeline::HeldReadQueries();
|
||||
Require(!AgcDriver::Graphics::StorageTexture::FlushPending(copiedAddress, PoolBytes), "queued index read: a flush over memory without pending results stored something");
|
||||
Require(AgcDriver::DriverDetail::DrawPipeline::HeldReadQueries() == heldQueries, "queued index read: a flush with nothing to store asked the draw pipeline for held reads");
|
||||
Require(IndicesIntact(), "queued index read: a flush over other memory stored the image's results");
|
||||
GateUntilQueued();
|
||||
std::fill_n(ColorMemory, ColorBytes, std::byte{0});
|
||||
Submit(Draw(Vertices.data(), Vertices.size()), "the indexed draw");
|
||||
|
||||
@@ -17,20 +17,50 @@ using ShaderRecompiler::ShaderStage;
|
||||
constexpr std::uint32_t Threads = 64;
|
||||
constexpr std::uint32_t Inputs = 4;
|
||||
constexpr std::uint32_t Results = 4;
|
||||
constexpr std::uint32_t Selectors = 7;
|
||||
alignas(256) std::array<std::uint32_t, Threads * Inputs> Input{};
|
||||
alignas(256) std::array<std::uint32_t, Threads * Results> Output{};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 30> SdwaCode{
|
||||
constexpr std::array<std::uint32_t, Results> ModifierWords{14, 16, 18, 20};
|
||||
|
||||
alignas(256) std::array<std::uint32_t, 30> SdwaCode{
|
||||
0x34020082, 0x34060082, 0xe0302000, 0x80000401,
|
||||
0x7e1402ff, 0xabcd1234, 0x7e1602ff, 0xabcd1234, 0x7e1802ff, 0xabcd1234, 0x7e1a02ff, 0xabcd1234,
|
||||
0xbf8c3f70, 0x7e1424f9, 0x00040604, 0x7e1624f9, 0x00050604, 0x7e1826f9, 0x000d0604, 0x7e1a28f9, 0x000b0604,
|
||||
0xbf8c3f70, 0x7e1422f9, 0x00060604, 0x7e1624f9, 0x00060604, 0x7e1826f9, 0x00060604, 0x7e1a28f9, 0x00060604,
|
||||
0xe0702000, 0x80010a03, 0xe0702004, 0x80010b03, 0xe0702008, 0x80010c03, 0xe070200c, 0x80010d03,
|
||||
0xbf810000,
|
||||
};
|
||||
|
||||
const std::array<const char*, Selectors> SelectorNames{"BYTE_0", "BYTE_1", "BYTE_2", "BYTE_3", "WORD_0", "WORD_1", "DWORD"};
|
||||
|
||||
struct SelectVector {
|
||||
std::uint32_t value;
|
||||
std::array<std::uint32_t, Selectors> zeroExtended;
|
||||
std::array<std::uint32_t, Selectors> signExtended;
|
||||
};
|
||||
|
||||
constexpr std::array<SelectVector, 16> Vectors{{
|
||||
{0x00000000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
|
||||
{0xffffffffu, {0x000000ffu, 0x000000ffu, 0x000000ffu, 0x000000ffu, 0x0000ffffu, 0x0000ffffu, 0xffffffffu}, {0xffffffffu, 0xffffffffu, 0xffffffffu, 0xffffffffu, 0xffffffffu, 0xffffffffu, 0xffffffffu}},
|
||||
{0x12345678u, {0x00000078u, 0x00000056u, 0x00000034u, 0x00000012u, 0x00005678u, 0x00001234u, 0x12345678u}, {0x00000078u, 0x00000056u, 0x00000034u, 0x00000012u, 0x00005678u, 0x00001234u, 0x12345678u}},
|
||||
{0x80808080u, {0x00000080u, 0x00000080u, 0x00000080u, 0x00000080u, 0x00008080u, 0x00008080u, 0x80808080u}, {0xffffff80u, 0xffffff80u, 0xffffff80u, 0xffffff80u, 0xffff8080u, 0xffff8080u, 0x80808080u}},
|
||||
{0x7f7f7f7fu, {0x0000007fu, 0x0000007fu, 0x0000007fu, 0x0000007fu, 0x00007f7fu, 0x00007f7fu, 0x7f7f7f7fu}, {0x0000007fu, 0x0000007fu, 0x0000007fu, 0x0000007fu, 0x00007f7fu, 0x00007f7fu, 0x7f7f7f7fu}},
|
||||
{0x8182a3f4u, {0x000000f4u, 0x000000a3u, 0x00000082u, 0x00000081u, 0x0000a3f4u, 0x00008182u, 0x8182a3f4u}, {0xfffffff4u, 0xffffffa3u, 0xffffff82u, 0xffffff81u, 0xffffa3f4u, 0xffff8182u, 0x8182a3f4u}},
|
||||
{0x7f00ff80u, {0x00000080u, 0x000000ffu, 0x00000000u, 0x0000007fu, 0x0000ff80u, 0x00007f00u, 0x7f00ff80u}, {0xffffff80u, 0xffffffffu, 0x00000000u, 0x0000007fu, 0xffffff80u, 0x00007f00u, 0x7f00ff80u}},
|
||||
{0x00008000u, {0x00000000u, 0x00000080u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00000000u, 0x00008000u}, {0x00000000u, 0xffffff80u, 0x00000000u, 0x00000000u, 0xffff8000u, 0x00000000u, 0x00008000u}},
|
||||
{0x0000007fu, {0x0000007fu, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000007fu, 0x00000000u, 0x0000007fu}, {0x0000007fu, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000007fu, 0x00000000u, 0x0000007fu}},
|
||||
{0x00000080u, {0x00000080u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000080u, 0x00000000u, 0x00000080u}, {0xffffff80u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000080u, 0x00000000u, 0x00000080u}},
|
||||
{0x80000000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000080u, 0x00000000u, 0x00008000u, 0x80000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0xffffff80u, 0x00000000u, 0xffff8000u, 0x80000000u}},
|
||||
{0x00800000u, {0x00000000u, 0x00000000u, 0x00000080u, 0x00000000u, 0x00000000u, 0x00000080u, 0x00800000u}, {0x00000000u, 0x00000000u, 0xffffff80u, 0x00000000u, 0x00000000u, 0x00000080u, 0x00800000u}},
|
||||
{0x0000ff00u, {0x00000000u, 0x000000ffu, 0x00000000u, 0x00000000u, 0x0000ff00u, 0x00000000u, 0x0000ff00u}, {0x00000000u, 0xffffffffu, 0x00000000u, 0x00000000u, 0xffffff00u, 0x00000000u, 0x0000ff00u}},
|
||||
{0xdeadbeefu, {0x000000efu, 0x000000beu, 0x000000adu, 0x000000deu, 0x0000beefu, 0x0000deadu, 0xdeadbeefu}, {0xffffffefu, 0xffffffbeu, 0xffffffadu, 0xffffffdeu, 0xffffbeefu, 0xffffdeadu, 0xdeadbeefu}},
|
||||
{0x01020304u, {0x00000004u, 0x00000003u, 0x00000002u, 0x00000001u, 0x00000304u, 0x00000102u, 0x01020304u}, {0x00000004u, 0x00000003u, 0x00000002u, 0x00000001u, 0x00000304u, 0x00000102u, 0x01020304u}},
|
||||
{0xf0e0d0c0u, {0x000000c0u, 0x000000d0u, 0x000000e0u, 0x000000f0u, 0x0000d0c0u, 0x0000f0e0u, 0xf0e0d0c0u}, {0xffffffc0u, 0xffffffd0u, 0xffffffe0u, 0xfffffff0u, 0xffffd0c0u, 0xfffff0e0u, 0xf0e0d0c0u}},
|
||||
}};
|
||||
|
||||
void FillInput() {
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
Input[tid * Inputs] = tid | ((tid + 64u) << 8u) | ((255u - tid) << 16u) | (((tid * 3u + 128u) & 0xffu) << 24u);
|
||||
Input[tid * Inputs] = Vectors[tid % Vectors.size()].value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +79,10 @@ std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t co
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (4u << 16u), count, 0x11016facu};
|
||||
}
|
||||
|
||||
void Run(AgcDriver::VulkanDevice& device) {
|
||||
void Run(AgcDriver::VulkanDevice& device, std::uint32_t selector, bool signExtend) {
|
||||
for (const auto word : ModifierWords) {
|
||||
SdwaCode[word] = 0x00000604u | (selector << 16u) | (signExtend ? 1u << 19u : 0u);
|
||||
}
|
||||
Output.fill(0xdeadbeefu);
|
||||
std::vector<std::uint32_t> userData(8, 0u);
|
||||
const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size()));
|
||||
@@ -71,18 +104,16 @@ void Run(AgcDriver::VulkanDevice& device) {
|
||||
device.WaitIdle();
|
||||
}
|
||||
|
||||
void Check() {
|
||||
void Check(std::uint32_t selector, bool signExtend) {
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
const auto source = Input[tid * Inputs];
|
||||
const auto& vector = Vectors[tid % Vectors.size()];
|
||||
const auto selected = signExtend ? vector.signExtended[selector] : vector.zeroExtended[selector];
|
||||
const auto* out = &Output[tid * Results];
|
||||
const auto where = [&](std::uint32_t result) { return "sdwa cvt f32 ubyte: thread " + std::to_string(tid) + " result " + std::to_string(result) + " is " + Hex(out[result]); };
|
||||
const auto byte1 = FloatBits((source >> 8u) & 0xffu);
|
||||
const auto byte3 = FloatBits(source >> 24u);
|
||||
const auto signFill = FloatBits((source & 0x80000000u) != 0u ? 0xffu : 0u);
|
||||
Require(out[0] == byte1, where(0) + ", expected " + Hex(byte1) + ": v_cvt_f32_ubyte1 src0_sel:WORD_0 must convert bits 15:8");
|
||||
Require(out[1] == byte3, where(1) + ", expected " + Hex(byte3) + ": v_cvt_f32_ubyte1 src0_sel:WORD_1 must convert bits 31:24");
|
||||
Require(out[2] == signFill, where(2) + ", expected " + Hex(signFill) + ": v_cvt_f32_ubyte2 src0_sel:WORD_1 src0_sext must convert the sign extension of the high word");
|
||||
Require(out[3] == signFill, where(3) + ", expected " + Hex(signFill) + ": v_cvt_f32_ubyte3 src0_sel:BYTE_3 src0_sext must convert the sign extension of the top byte");
|
||||
for (std::uint32_t byte = 0; byte < Results; ++byte) {
|
||||
const auto expected = FloatBits((selected >> (byte * 8u)) & 0xffu);
|
||||
Require(out[byte] == expected, "sdwa v_cvt_f32_ubyte" + std::to_string(byte) + " src0_sel:" + SelectorNames[selector] + (signExtend ? " src0_sext" : "") + " of " +
|
||||
Hex(vector.value) + " is " + Hex(out[byte]) + ", expected " + Hex(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,8 +124,12 @@ int main() {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
FillInput();
|
||||
Run(*device);
|
||||
Check();
|
||||
for (const bool signExtend : {false, true}) {
|
||||
for (std::uint32_t selector = 0; selector < Selectors; ++selector) {
|
||||
Run(*device, selector, signExtend);
|
||||
Check(selector, signExtend);
|
||||
}
|
||||
}
|
||||
std::puts("sdwa cvt f32 ubyte tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Width = 64;
|
||||
constexpr std::uint32_t Height = 16;
|
||||
constexpr std::uint32_t WaveSize = 64;
|
||||
constexpr std::uint32_t PlainControl = 0x50u;
|
||||
constexpr std::byte Kept{0x40};
|
||||
alignas(256) std::array<std::byte, Width * Height * 4> Pixels{};
|
||||
alignas(256) std::array<std::byte, 128 * 128 * 4> DepthMemory{};
|
||||
alignas(256) std::array<std::byte, 256 * 256> StencilMemory{};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{
|
||||
0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 4> PixelCode{
|
||||
0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::array<float, 4>, 3> Vertices{{
|
||||
{-1.0f, -1.0f, 0.25f, 1.0f}, {3.0f, -1.0f, 0.25f, 1.0f}, {-1.0f, 3.0f, 0.25f, 1.0f},
|
||||
}};
|
||||
|
||||
struct Shaders {
|
||||
ShaderRecompiler::RecompileResult vertex;
|
||||
ShaderRecompiler::RecompileResult pixel;
|
||||
std::uint32_t pixelPush = 0;
|
||||
};
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu};
|
||||
}
|
||||
|
||||
ShaderRecompiler::ShaderPixelStageInfo PixelInfo() {
|
||||
const AgcDriver::Registers context{
|
||||
{0x1b3, 0x700u}, {0x1b4, 0x700u}, {0x1b6, 0u}, {0x1c5, 9u}, {0x203, PlainControl},
|
||||
};
|
||||
return AgcDriver::Graphics::DecodePixelStageInfo(context, {0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u});
|
||||
}
|
||||
|
||||
Shaders Compile(AgcDriver::VulkanDevice& device) {
|
||||
const auto target = device.Target();
|
||||
std::vector<std::uint32_t> vertexUserData(4, 0u);
|
||||
const auto vertexBuffer = BufferDescriptor(Vertices.data(), 16u, static_cast<std::uint32_t>(Vertices.size()));
|
||||
std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin());
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> vertexMemory{{{reinterpret_cast<std::uintptr_t>(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}};
|
||||
ShaderRecompiler::RecompileRequest vertex{
|
||||
{ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(VertexCode.data()), VertexCode, 0, {}},
|
||||
{WaveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory},
|
||||
target,
|
||||
{0, 0, 0, 64}
|
||||
};
|
||||
vertex.useCache = false;
|
||||
Shaders shaders{ShaderRecompiler::Recompile(vertex), {}, 0};
|
||||
const auto vertexPush = static_cast<std::uint32_t>(shaders.vertex.pushConstants.size());
|
||||
std::vector<std::uint32_t> pixelUserData(8, 0u);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{reinterpret_cast<std::uintptr_t>(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}};
|
||||
ShaderRecompiler::RecompileRequest fragment{
|
||||
{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(PixelCode.data()), PixelCode, 0, {}},
|
||||
{WaveSize, 0, pixelUserData, std::nullopt, PixelInfo(), std::nullopt, pixelMemory},
|
||||
target,
|
||||
PixelPushLayout(vertexPush, target)
|
||||
};
|
||||
fragment.useCache = false;
|
||||
shaders.pixel = ShaderRecompiler::Recompile(fragment);
|
||||
shaders.pixelPush = PixelPushOffset(vertexPush, target);
|
||||
return shaders;
|
||||
}
|
||||
|
||||
AgcDriver::QueueState StencilQueue(std::uint32_t ops, std::uint32_t refMask, std::uint32_t depthControl) {
|
||||
AgcDriver::QueueState queue;
|
||||
queue.userConfig[0x242] = 4;
|
||||
queue.context = {
|
||||
{0x2d5, 0x2000},
|
||||
{0x1b6, 0}, {0x1b8, 0}, {0x207, 0}, {0x203, 0x800},
|
||||
{0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0},
|
||||
{0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa},
|
||||
{0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff},
|
||||
{0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240},
|
||||
{0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010},
|
||||
{0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028},
|
||||
{0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u},
|
||||
{0x3b8, 0x9000000}, {0x1e0, 0},
|
||||
{0xc, 0}, {0xd, 0x40040},
|
||||
{0x81, 0x80000000}, {0x82, 0x40040},
|
||||
{0x90, 0x80000000}, {0x91, 0x40040},
|
||||
{0x94, 0x80000000}, {0x95, 0x40040},
|
||||
{0x000, 0}, {0x002, 0}, {0x007, (1u << 16u) | 3u}, {0x00a, 0}, {0x00b, 0}, {0x010, 0x22900983u}, {0x011, 0x20000181u},
|
||||
{0x012, 0x100}, {0x014, 0x100}, {0x013, 0x200}, {0x015, 0x200}, {0x005, 0x300}, {0x01e, 0x1},
|
||||
{0x10b, ops}, {0x10c, refMask}, {0x10d, refMask}, {0x200, depthControl}
|
||||
};
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(Pixels.data());
|
||||
queue.context[0x318] = static_cast<std::uint32_t>(address >> 8u);
|
||||
queue.context[0x390] = static_cast<std::uint32_t>(address >> 40u);
|
||||
queue.context[0x10f] = std::bit_cast<std::uint32_t>(32.0f);
|
||||
queue.context[0x110] = std::bit_cast<std::uint32_t>(32.0f);
|
||||
queue.context[0x111] = std::bit_cast<std::uint32_t>(-2.0f);
|
||||
queue.context[0x112] = std::bit_cast<std::uint32_t>(2.0f);
|
||||
queue.context[0x113] = std::bit_cast<std::uint32_t>(1.0f);
|
||||
queue.context[0x114] = 0;
|
||||
queue.context[0xb4] = 0;
|
||||
queue.context[0xb5] = std::bit_cast<std::uint32_t>(1.0f);
|
||||
queue.shader[0x008] = 0x100;
|
||||
queue.shader[0x009] = 0;
|
||||
return queue;
|
||||
}
|
||||
|
||||
void Draw(AgcDriver::VulkanDevice& device, const Shaders& shaders, const VkStencilOpState& stencil) {
|
||||
Pixels.fill(Kept);
|
||||
const std::array<AgcDriver::Graphics::CompiledShader, 2> stages{{
|
||||
{ShaderStage::Vertex, &shaders.vertex, 0},
|
||||
{ShaderStage::Fragment, &shaders.pixel, shaders.pixelPush}
|
||||
}};
|
||||
AgcDriver::Graphics::State state{};
|
||||
state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, WaveSize, WaveSize, std::nullopt, std::nullopt};
|
||||
state.color = {reinterpret_cast<std::uintptr_t>(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u};
|
||||
state.colors = {state.color};
|
||||
state.hasColorTarget = true;
|
||||
state.depth = AgcDriver::Graphics::DepthTarget{reinterpret_cast<std::uintptr_t>(DepthMemory.data()), reinterpret_cast<std::uintptr_t>(StencilMemory.data()), {Width, Height}, VK_FORMAT_D32_SFLOAT_S8_UINT, 1.0f, 0};
|
||||
state.depthTest = false;
|
||||
state.depthCompare = VK_COMPARE_OP_ALWAYS;
|
||||
state.stencilTest = true;
|
||||
state.stencilFront = stencil;
|
||||
state.stencilBack = stencil;
|
||||
state.renderExtent = {Width, Height};
|
||||
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
state.viewport = {0, static_cast<float>(Height), static_cast<float>(Width), -static_cast<float>(Height), 0, 1};
|
||||
state.negativeOneToOne = false;
|
||||
state.scissor = {{0, 0}, {Width, Height}};
|
||||
state.cullMode = VK_CULL_MODE_NONE;
|
||||
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
|
||||
state.blend.colorWriteMask = 15;
|
||||
state.blends = {state.blend};
|
||||
state.blendConstants = {};
|
||||
device.Draw(state, AgcDriver::Pm4::DrawParameters{0, 3u, 0, 1u, 0, false}, stages);
|
||||
device.WaitIdle();
|
||||
}
|
||||
|
||||
std::uint32_t Expected(std::uint32_t op, std::uint32_t stored, std::uint32_t value, std::uint32_t writeMask) {
|
||||
const auto result = op == 10u ? stored & value : op == 11u ? stored | value : op == 12u ? stored ^ value : ~(stored ^ value);
|
||||
return ((stored & ~writeMask) | (result & writeMask)) & 0xffu;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
const auto shaders = Compile(*device);
|
||||
const auto replace = [](std::uint32_t value) { return VkStencilOpState{VK_STENCIL_OP_REPLACE, VK_STENCIL_OP_REPLACE, VK_STENCIL_OP_REPLACE, VK_COMPARE_OP_ALWAYS, 0xffu, 0xffu, value}; };
|
||||
const auto equal = [](std::uint32_t value) { return VkStencilOpState{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_EQUAL, 0xffu, 0u, value}; };
|
||||
std::size_t checked = 0;
|
||||
for (const auto stored : {0xa5u, 0x3cu}) {
|
||||
for (const auto op : {10u, 11u, 12u, 15u}) {
|
||||
for (const auto [writeMask, value] : {std::pair{0xffu, 0x3cu}, std::pair{0x0fu, 0x96u}, std::pair{0xf6u, 0xffu}}) {
|
||||
for (const auto [ops, compare] : {std::pair{op << 4u, 7u}, std::pair{op, 0u}}) {
|
||||
const auto refMask = 0xff00u | (writeMask << 16u) | (value << 24u);
|
||||
const auto face = AgcDriver::Graphics::DecodeState(StencilQueue(ops, refMask, 1u | (compare << 8u))).stencilFront;
|
||||
const auto expected = Expected(op, stored, value, writeMask);
|
||||
const auto name = "stencil operation " + std::to_string(op) + " in DB_STENCIL_CONTROL " + std::to_string(ops) + " with DB_STENCILREFMASK " + std::to_string(refMask) + " on stored value " + std::to_string(stored);
|
||||
Draw(*device, shaders, replace(stored));
|
||||
Draw(*device, shaders, face);
|
||||
Draw(*device, shaders, equal(expected));
|
||||
for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) {
|
||||
Require(Pixels[pixel * 4u] == std::byte{255}, name + ": pixel " + std::to_string(pixel) + " does not hold the expected stencil value " + std::to_string(expected));
|
||||
}
|
||||
Draw(*device, shaders, equal(expected ^ 0x80u));
|
||||
Require(std::ranges::all_of(std::span(Pixels).first(4), [](std::byte value) { return value == Kept; }), name + ": the stencil value also equals " + std::to_string(expected ^ 0x80u));
|
||||
++checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
std::printf("stencil logic operation tests passed: %zu states\n", checked);
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -88,7 +88,7 @@ ShaderRecompiler::ShaderPixelStageInfo PixelInfo(std::uint32_t shaderControl) {
|
||||
return AgcDriver::Graphics::DecodePixelStageInfo(context, {0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u});
|
||||
}
|
||||
|
||||
void Draw(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> pixelCode, std::uint32_t shaderControl, const VkStencilOpState& stencil, std::uint32_t vertices = 3u, std::uint32_t instances = 1u, bool depthFail = false, const std::function<void(AgcDriver::Graphics::State&)>& alter = {}, const std::function<void(AgcDriver::Pm4::DrawParameters&)>& alterDraw = {}, std::span<const std::array<float, 4>> vertexData = Vertices, std::span<const std::uint32_t> vertexCode = VertexCode, std::span<const std::uint32_t> pixelData = {}, std::span<const ShaderRecompiler::MemoryRegion> pixelSrt = {}, const std::function<void(ShaderRecompiler::RecompileResult&)>& alterVertex = {}) {
|
||||
void Draw(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> pixelCode, std::uint32_t shaderControl, const VkStencilOpState& stencil, std::uint32_t vertices = 3u, std::uint32_t instances = 1u, bool depthFail = false, const std::function<void(AgcDriver::Graphics::State&)>& alter = {}, const std::function<void(AgcDriver::Pm4::DrawParameters&)>& alterDraw = {}, std::span<const std::array<float, 4>> vertexData = Vertices, std::span<const std::uint32_t> vertexCode = VertexCode, std::span<const std::uint32_t> pixelData = {}, std::span<const ShaderRecompiler::MemoryRegion> pixelSrt = {}, const std::function<void(ShaderRecompiler::RecompileResult&)>& alterVertex = {}, std::uint32_t pixelPushBytes = 0u) {
|
||||
Pixels.fill(Kept);
|
||||
auto target = device.Target();
|
||||
std::vector<std::uint32_t> capabilities(target.supportedCapabilities.begin(), target.supportedCapabilities.end());
|
||||
@@ -113,11 +113,13 @@ void Draw(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> pixelC
|
||||
auto pixelUserData = pixelData.empty() ? std::vector<std::uint32_t>(8, 0u) : std::vector<std::uint32_t>(pixelData.begin(), pixelData.end());
|
||||
std::vector<ShaderRecompiler::MemoryRegion> pixelMemory{{reinterpret_cast<std::uintptr_t>(pixelCode.data()), std::as_bytes(pixelCode)}};
|
||||
pixelMemory.insert(pixelMemory.end(), pixelSrt.begin(), pixelSrt.end());
|
||||
auto pixelLayout = PixelPushLayout(vertexPush, target);
|
||||
if (pixelPushBytes != 0u) pixelLayout.pushConstantSizeBytes = pixelPushBytes;
|
||||
ShaderRecompiler::RecompileRequest fragment{
|
||||
{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(pixelCode.data()), pixelCode, 0, {}},
|
||||
{WaveSize, 0, pixelUserData, std::nullopt, PixelInfo(shaderControl), std::nullopt, pixelMemory},
|
||||
target,
|
||||
PixelPushLayout(vertexPush, target)
|
||||
pixelLayout
|
||||
};
|
||||
fragment.useCache = false;
|
||||
const auto pixelResult = ShaderRecompiler::Recompile(fragment);
|
||||
@@ -350,6 +352,14 @@ int main() {
|
||||
const std::array<std::uint32_t, 1> colorData{0x3f800000u};
|
||||
Draw(*device, userColor, exported, replace(), 3u, 1u, false, {}, {}, Vertices, VertexCode, colorData);
|
||||
Check(all, "dynamic reference preserves the adjacent shader-data push payload");
|
||||
auto bufferColor = std::vector<std::uint32_t>(alternating.begin(), alternating.end());
|
||||
*std::find(bufferColor.begin(), bufferColor.end(), 0x7e0e02f2u) = 0x7e0e0208u;
|
||||
bufferColor.insert(bufferColor.begin(), {0xf4200200u, 0xfa000000u, 0xbf8cc07fu});
|
||||
alignas(256) const std::array<std::uint32_t, 4> colorBuffer{0x3f800000u, 0u, 0u, 0u};
|
||||
const auto colorDescriptor = BufferDescriptor(colorBuffer.data(), 4u, static_cast<std::uint32_t>(colorBuffer.size()));
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> colorMemory{{{reinterpret_cast<std::uintptr_t>(colorBuffer.data()), std::as_bytes(std::span(colorBuffer))}}};
|
||||
Draw(*device, bufferColor, exported, replace(), 3u, 1u, false, {}, {}, Vertices, VertexCode, colorDescriptor, colorMemory, {}, sizeof(std::uint32_t));
|
||||
Check(all, "dynamic reference keeps guest buffer offsets in shader data outside the push constants");
|
||||
auto guarded = std::vector<std::uint32_t>(alternating.begin(), alternating.end());
|
||||
*std::find(guarded.begin(), guarded.end(), 0x7e0e02f2u) = 0x7e0e020cu;
|
||||
guarded.insert(guarded.begin(), {0xf4040280u, 0xfa000018u, 0xf4000305u, 0xfa000000u, 0xbf8cc07fu});
|
||||
@@ -369,6 +379,29 @@ int main() {
|
||||
catch (const std::exception& error) { faultThrown = std::string(error.what()).find("BDA") != std::string::npos; }
|
||||
Require(faultThrown, "poisoned readonly SRT replay did not throw a host fault");
|
||||
Require(std::ranges::all_of(Pixels, [](std::byte value) { return value == Kept; }), "poisoned replay published guest color results before its fault");
|
||||
alignas(256) std::array<std::uint32_t, Width + 1u> columnValues{};
|
||||
alignas(256) std::array<float, Width> columnTexels{};
|
||||
columnValues[0] = 0x2fu;
|
||||
for (std::uint32_t x = 0; x < Width; ++x) {
|
||||
columnValues[x + 1u] = 0x30u + x;
|
||||
columnTexels[x] = 1.0f;
|
||||
}
|
||||
constexpr std::array<std::uint32_t, 15> bufferImageCode{
|
||||
0x7e060f00u, 0x7e080280u, 0xf0001108u, 0x00010503u, 0xe0302000u, 0x80000603u, 0xe0302000u, 0x80030803u,
|
||||
0xbf8c3f70u, 0x7e0e02f2u, 0xf8000082u, 0x00000600u, 0xf800180fu, 0x07050805u, 0xbf810000u,
|
||||
};
|
||||
const auto columnBuffer = BufferDescriptor(columnValues.data() + 1, 4u, Width);
|
||||
const auto alignedBuffer = BufferDescriptor(columnValues.data(), 4u, Width + 1u);
|
||||
const auto texelAddress = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(columnTexels.data()));
|
||||
const std::array<std::uint32_t, 8> columnTexture{static_cast<std::uint32_t>(texelAddress >> 8u), static_cast<std::uint32_t>((texelAddress >> 40u) & 0xffu) | (22u << 20u) | (((Width - 1u) & 3u) << 30u), (Width - 1u) >> 2u, 0xfacu | (9u << 28u), 0u, 0u, 0u, 0u};
|
||||
std::vector<std::uint32_t> bufferImageData(columnBuffer.begin(), columnBuffer.end());
|
||||
bufferImageData.insert(bufferImageData.end(), columnTexture.begin(), columnTexture.end());
|
||||
bufferImageData.insert(bufferImageData.end(), alignedBuffer.begin(), alignedBuffer.end());
|
||||
Draw(*device, PlainPixelCode, plain, replace(0u));
|
||||
Draw(*device, bufferImageCode, exported, replace(), 3u, 1u, false, {}, {}, Vertices, VertexCode, bufferImageData);
|
||||
Check(all, "a replayed export beside an image draws the image's texels");
|
||||
Draw(*device, PlainPixelCode, plain, equal(0x35u));
|
||||
Check([](std::uint32_t x, std::uint32_t) { return x == 5u; }, "a replayed export beside an image reads its guest buffer at a 4-byte offset");
|
||||
std::puts("stencil replay tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
|
||||
@@ -2,7 +2,7 @@ enable_language(C)
|
||||
|
||||
set(LIBATRAC9_DIR ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src)
|
||||
set(LIBATRAC9_PATCHED_DIR ${CMAKE_CURRENT_BINARY_DIR}/atrac9)
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${LIBATRAC9_DIR}/unpack.c ${LIBATRAC9_DIR}/bit_reader.c ${LIBATRAC9_DIR}/decinit.c)
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${LIBATRAC9_DIR}/unpack.c ${LIBATRAC9_DIR}/bit_reader.c ${LIBATRAC9_DIR}/decinit.c ${LIBATRAC9_DIR}/utility.c)
|
||||
include(${CMAKE_CURRENT_SOURCE_DIR}/PatchLibAtrac9.cmake)
|
||||
add_library(atrac9 STATIC EXCLUDE_FROM_ALL
|
||||
${LIBATRAC9_DIR}/band_extension.c
|
||||
@@ -17,7 +17,7 @@ add_library(atrac9 STATIC EXCLUDE_FROM_ALL
|
||||
${LIBATRAC9_DIR}/scale_factors.c
|
||||
${LIBATRAC9_DIR}/tables.c
|
||||
${LIBATRAC9_PATCHED_DIR}/unpack.c
|
||||
${LIBATRAC9_DIR}/utility.c
|
||||
${LIBATRAC9_PATCHED_DIR}/utility.c
|
||||
)
|
||||
target_include_directories(atrac9 PRIVATE ${LIBATRAC9_DIR})
|
||||
set_target_properties(atrac9 PROPERTIES POSITION_INDEPENDENT_CODE ON)
|
||||
@@ -36,6 +36,9 @@ if(BUILD_TESTING)
|
||||
add_test_executable(atrac9_gradient_tests tests/Atrac9Gradient.c ${LIBATRAC9_DIR}/bit_reader.c)
|
||||
target_include_directories(atrac9_gradient_tests PRIVATE ${LIBATRAC9_PATCHED_DIR} ${LIBATRAC9_DIR})
|
||||
target_link_libraries(atrac9_gradient_tests PRIVATE atrac9)
|
||||
if(NOT WIN32)
|
||||
target_link_libraries(atrac9_gradient_tests PRIVATE m)
|
||||
endif()
|
||||
target_compile_options(atrac9_gradient_tests PRIVATE -ffunction-sections -fdata-sections)
|
||||
if(APPLE)
|
||||
target_link_options(atrac9_gradient_tests PRIVATE LINKER:-dead_strip)
|
||||
|
||||
@@ -28,3 +28,21 @@ if(libatrac9ConfigGuardPosition EQUAL -1)
|
||||
endif()
|
||||
string(REPLACE "${libatrac9OldConfigGuard}" "${libatrac9NewConfigGuard}" libatrac9Decinit "${libatrac9Decinit}")
|
||||
file(WRITE ${LIBATRAC9_PATCHED_DIR}/decinit.c "${libatrac9Decinit}")
|
||||
|
||||
file(READ ${LIBATRAC9_DIR}/utility.c libatrac9Utility)
|
||||
set(libatrac9OldReverse "\treturn value >> (32 - bitCount);")
|
||||
set(libatrac9NewReverse "\treturn bitCount ? value >> (32 - bitCount) : 0u;")
|
||||
string(FIND "${libatrac9Utility}" "${libatrac9OldReverse}" libatrac9ReversePosition)
|
||||
if(libatrac9ReversePosition EQUAL -1)
|
||||
message(FATAL_ERROR "LibAtrac9 BitReverse32 has changed; review the local shift-by-32 guard")
|
||||
endif()
|
||||
string(REPLACE "${libatrac9OldReverse}" "${libatrac9NewReverse}" libatrac9Utility "${libatrac9Utility}")
|
||||
|
||||
set(libatrac9OldSignExtend "\treturn (value << shift) >> shift;")
|
||||
set(libatrac9NewSignExtend "\treturn (int)((unsigned int)value << shift) >> shift;")
|
||||
string(FIND "${libatrac9Utility}" "${libatrac9OldSignExtend}" libatrac9SignExtendPosition)
|
||||
if(libatrac9SignExtendPosition EQUAL -1)
|
||||
message(FATAL_ERROR "LibAtrac9 SignExtend32 has changed; review the local signed-shift guard")
|
||||
endif()
|
||||
string(REPLACE "${libatrac9OldSignExtend}" "${libatrac9NewSignExtend}" libatrac9Utility "${libatrac9Utility}")
|
||||
file(WRITE ${LIBATRAC9_PATCHED_DIR}/utility.c "${libatrac9Utility}")
|
||||
@@ -70,5 +70,9 @@ int APS5_VABI sceAjmBatchStart(uint32_t context, const AjmBatchInfo* info, int p
|
||||
int APS5_VABI sceAjmBatchWait(uint32_t context, uint32_t batch, uint32_t timeout, AjmBatchError* error);
|
||||
int APS5_VABI sceAjmBatchCancel(uint32_t context, uint32_t batch);
|
||||
int APS5_VABI sceAjmBatchErrorDump(const AjmBatchInfo* info, AjmBatchError* error);
|
||||
void* APS5_VABI sceAjmBatchJobControlBufferRa(void* position, uint32_t instance, uint64_t flags, const void* sideband_input, size_t sideband_input_size, void* sideband_output, size_t sideband_output_size, void* return_address);
|
||||
void* APS5_VABI sceAjmBatchJobRunBufferRa(void* position, uint32_t instance, uint64_t flags, const void* data_input, size_t data_input_size, void* data_output, size_t data_output_size, void* sideband_output, size_t sideband_output_size, void* return_address);
|
||||
void* APS5_VABI sceAjmBatchJobRunSplitBufferRa(void* position, uint32_t instance, uint64_t flags, const AjmBuffer* input_buffers, size_t input_buffers_num, const AjmBuffer* output_buffers, size_t output_buffers_num, void* sideband_output, size_t sideband_output_size, void* return_address);
|
||||
int APS5_VABI sceAjmBatchStartBuffer(uint32_t context, const uint8_t* batch, uint32_t batch_size, int priority, AjmBatchError* error, uint32_t* batch_id);
|
||||
|
||||
}
|
||||
@@ -36,6 +36,8 @@ constexpr int SCE_AJM_ERROR_INVALID_INSTANCE = static_cast<int>(0x80930003);
|
||||
constexpr int SCE_AJM_ERROR_INVALID_BATCH = static_cast<int>(0x80930004);
|
||||
constexpr int SCE_AJM_ERROR_INVALID_PARAMETER = static_cast<int>(0x80930005);
|
||||
constexpr int SCE_AJM_ERROR_OUT_OF_RESOURCES = static_cast<int>(0x80930007);
|
||||
constexpr int SCE_AJM_ERROR_MALFORMED_BATCH = static_cast<int>(0x80930011);
|
||||
constexpr int SCE_AJM_ERROR_INVALID_ADDRESS = static_cast<int>(0x80930016);
|
||||
|
||||
constexpr std::int32_t AJM_RESULT_NOT_INITIALIZED = 0x00000001;
|
||||
constexpr std::int32_t AJM_RESULT_INVALID_DATA = 0x00000002;
|
||||
@@ -216,6 +218,7 @@ struct JobHeader {
|
||||
std::uint32_t inputCount;
|
||||
std::uint32_t outputCount;
|
||||
std::uint8_t parameters[16];
|
||||
bool console;
|
||||
};
|
||||
|
||||
struct PackedJob {
|
||||
@@ -691,9 +694,17 @@ void EmitFrame(Instance& instance, PcmOutputs& outputs, const std::uint8_t* pcm,
|
||||
ResamplerProduce(instance, outputs, channels, encoding, sampleBytes);
|
||||
}
|
||||
|
||||
// A PS4 reports an ATRAC9 stream's bitrate from its superframe: 448-byte superframes of 4 frames of 256
|
||||
// samples at 48 kHz gave 168000 (Knowledge is Power: Decades, every run job's format sideband).
|
||||
std::uint32_t At9Bitrate(const Instance& instance) {
|
||||
const auto samples = static_cast<std::uint64_t>(instance.info.framesInSuperframe) * static_cast<std::uint64_t>(instance.info.frameSamples);
|
||||
if (samples == 0) return 0;
|
||||
return static_cast<std::uint32_t>(static_cast<std::uint64_t>(instance.info.superframeSize) * 8u * static_cast<std::uint64_t>(instance.info.samplingRate) / samples);
|
||||
}
|
||||
|
||||
SidebandFormat CurrentFormat(const Instance& instance) {
|
||||
switch (instance.codec) {
|
||||
case CODEC_AT9: return {static_cast<std::uint32_t>(instance.info.channels), ChannelMask(static_cast<std::size_t>(instance.info.channels)), static_cast<std::uint32_t>(instance.info.samplingRate), PcmEncoding(instance), 0, 0};
|
||||
case CODEC_AT9: return {static_cast<std::uint32_t>(instance.info.channels), ChannelMask(static_cast<std::size_t>(instance.info.channels)), static_cast<std::uint32_t>(instance.info.samplingRate), PcmEncoding(instance), At9Bitrate(instance), 0};
|
||||
case CODEC_MP3: return {instance.mp3Channels, ChannelMask(instance.mp3Channels), instance.mp3SampleRate, PcmEncoding(instance), instance.mp3Bitrate, 0};
|
||||
case CODEC_OPUS: return {instance.opusChannels, ChannelMask(instance.opusChannels), instance.opusSampleRate, PcmEncoding(instance), 0, 0};
|
||||
case CODEC_M4AAC: return {instance.aacChannels, ChannelMask(instance.aacChannels), instance.aacSampleRate, PcmEncoding(instance), instance.aacBitrate, 0};
|
||||
@@ -768,12 +779,22 @@ void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c
|
||||
if (segmentEnded()) break;
|
||||
if (instance.superframeRemaining == 0) consumed += RiffDataOffset(input.data() + consumed, input.size() - consumed);
|
||||
const std::size_t needed = instance.superframeRemaining == 0 ? superframeSize : instance.superframeRemaining;
|
||||
const bool roomForFrame = Resampling(instance) ? pcmOutputs.Room() >= channels * sampleBytes : pcmOutputs.Room() >= frameBytes;
|
||||
// A PS4 reports which side ran out (probe runs of a title's ATRAC9 stream through libSceAjm's
|
||||
// builders): PARTIAL_INPUT when the input ran out, on a superframe boundary or not, with room
|
||||
// left; NOT_ENOUGH_ROOM when the room ran out with input left, after however many frames (and
|
||||
// with none decoded for a zero room); both (0x18) when the input ended just as the output
|
||||
// filled, which is every Wwise run job. A run without input (a format query) reports 0.
|
||||
// Resampled runs were not measured and keep the earlier rule: a trailing partial superframe is
|
||||
// partial input, a zero-output run is out of room.
|
||||
const bool measured = !Resampling(instance);
|
||||
if (input.size() - consumed < needed) {
|
||||
if (input.size() != consumed) result |= AJM_RESULT_PARTIAL_INPUT;
|
||||
if (input.size() != consumed || (measured && !input.empty())) result |= AJM_RESULT_PARTIAL_INPUT;
|
||||
if (measured && !input.empty() && !roomForFrame) result |= AJM_RESULT_NOT_ENOUGH_ROOM;
|
||||
break;
|
||||
}
|
||||
if (Resampling(instance) ? pcmOutputs.Room() < channels * sampleBytes : pcmOutputs.Room() < frameBytes) {
|
||||
if (frames == 0 && pcmOutputs.produced == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM;
|
||||
if (!roomForFrame) {
|
||||
if (measured || (frames == 0 && pcmOutputs.produced == 0)) result |= AJM_RESULT_NOT_ENOUGH_ROOM;
|
||||
break;
|
||||
}
|
||||
if (instance.superframeRemaining == 0) {
|
||||
@@ -797,6 +818,7 @@ void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c
|
||||
++frames;
|
||||
|
||||
EmitFrame(instance, pcmOutputs, pcm.data(), frameSamples, channels, sampleBytes);
|
||||
if (job.console && (job.flags & RUN_MULTIPLE_FRAMES) == 0) break;
|
||||
}
|
||||
if (segmentEnded()) instance.segmentSamples = 0;
|
||||
|
||||
@@ -1692,6 +1714,169 @@ void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* out
|
||||
}
|
||||
}
|
||||
|
||||
int RegisterBatch(std::uint32_t context, AjmBatchError* error, std::uint32_t* batch) {
|
||||
if (error) std::memset(error, 0, sizeof(*error));
|
||||
const auto id = g_nextBatch.fetch_add(1, std::memory_order_relaxed);
|
||||
{
|
||||
std::lock_guard lock(g_batchLock);
|
||||
g_batches.emplace(id, context);
|
||||
}
|
||||
*batch = id;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The PS4 batch buffer (sceAjmBatchStartBuffer) is not private: the PS4 SDK builds it with inline code
|
||||
// in the title, so it has the console's layout, taken from shadPS4's reimplementation of the PS4
|
||||
// library. It is a run of chunks, each starting with a 32-bit word that holds the chunk kind in bits
|
||||
// 0-5 and a payload in bits 6-25. A job chunk has the instance as payload and, in a second word, the
|
||||
// byte count of the chunks after it that describe the job; an inline chunk holds that many bytes of data
|
||||
// a job's control buffer may point to. A job's flags chunk has flags bits 32-51 as payload and bits 0-31
|
||||
// in its second word; its buffer chunks have a 32-bit size and a 64-bit address.
|
||||
enum class ConsoleChunk : std::uint32_t {
|
||||
Job = 0,
|
||||
InputRunBuffer = 1,
|
||||
InputControlBuffer = 2,
|
||||
ControlFlags = 3,
|
||||
RunFlags = 4,
|
||||
ReturnAddress = 6,
|
||||
InlineBuffer = 7,
|
||||
OutputRunBuffer = 17,
|
||||
OutputControlBuffer = 18,
|
||||
};
|
||||
|
||||
constexpr std::uint32_t CONSOLE_STATISTICS_INSTANCE = 0x80000;
|
||||
constexpr std::uint64_t CONSOLE_FLAGS_VERSION_AND_CODEC = 0x7FF;
|
||||
|
||||
// The low header word as the PS4's libSceAjm builders write it (a trace of 32 545 batches a Wwise title
|
||||
// started on a PS4): bits 0-3 the kind, bit 4 the output direction of a buffer chunk
|
||||
// (0x11 output run, 0x12 output control), bits 6-25 the job's instance or the flags' bits 32-51. The
|
||||
// builders leave bits 5, 26, 27 and 31 as they were, and a title that reuses its batch memory (Wwise
|
||||
// does) carries the previous batch's bytes there: a 6-bit kind read job headers as 16, 32 or 48 and run
|
||||
// flags as 20, 36 or 52, and a 26-bit payload read flags bit 52 set.
|
||||
ConsoleChunk ConsoleChunkKind(std::uint32_t word) {
|
||||
const std::uint32_t kind = word & 0xFu;
|
||||
return static_cast<ConsoleChunk>(kind == 1 || kind == 2 ? kind | (word & 0x10u) : kind);
|
||||
}
|
||||
|
||||
std::uint32_t ConsoleChunkPayload(std::uint32_t word) {
|
||||
return (word >> 6) & 0xFFFFFu;
|
||||
}
|
||||
|
||||
std::uint8_t* PutConsoleChunk(std::uint8_t* at, ConsoleChunk kind, std::uint32_t payload, std::uint32_t word) {
|
||||
const std::uint32_t words[2] = {static_cast<std::uint32_t>(kind) | ((payload & 0xFFFFFu) << 6), word};
|
||||
std::memcpy(at, words, sizeof(words));
|
||||
return at + sizeof(words);
|
||||
}
|
||||
|
||||
std::uint8_t* PutConsoleFlags(std::uint8_t* at, ConsoleChunk kind, std::uint64_t flags) {
|
||||
return PutConsoleChunk(at, kind, static_cast<std::uint32_t>(flags >> 32), static_cast<std::uint32_t>(flags));
|
||||
}
|
||||
|
||||
std::uint8_t* PutConsoleBuffer(std::uint8_t* at, ConsoleChunk kind, const void* address, std::size_t size) {
|
||||
at = PutConsoleChunk(at, kind, 0, static_cast<std::uint32_t>(size));
|
||||
std::memcpy(at, &address, sizeof(address));
|
||||
return at + sizeof(address);
|
||||
}
|
||||
|
||||
std::uint8_t* BeginConsoleJob(std::uint8_t* job, const void* returnAddress) {
|
||||
std::uint8_t* at = job + 2 * sizeof(std::uint32_t);
|
||||
return returnAddress ? PutConsoleBuffer(at, ConsoleChunk::ReturnAddress, returnAddress, 0) : at;
|
||||
}
|
||||
|
||||
void* EndConsoleJob(std::uint8_t* job, std::uint8_t* end, std::uint32_t instance) {
|
||||
PutConsoleChunk(job, ConsoleChunk::Job, instance, static_cast<std::uint32_t>(end - job) - 2 * sizeof(std::uint32_t));
|
||||
return end;
|
||||
}
|
||||
|
||||
struct ConsoleJob {
|
||||
JobHeader header;
|
||||
std::vector<AjmBuffer> buffers;
|
||||
};
|
||||
|
||||
// What sceAjmBatchStartBuffer reports in the AjmBatchError when it refuses a batch: the error, the job
|
||||
// chunk, the offending chunk's offset in the job and the job's return address, as a PS4 fills them for a
|
||||
// buffer chunk with a null address and a nonzero size (INVALID_ADDRESS, nothing of the batch run). The
|
||||
// other refusals here report MALFORMED_BATCH the same way; they were not measured.
|
||||
struct ConsoleRefusal {
|
||||
std::int32_t code = SCE_AJM_ERROR_MALFORMED_BATCH;
|
||||
const std::uint8_t* job = nullptr;
|
||||
std::uint32_t offset = 0;
|
||||
const void* returnAddress = nullptr;
|
||||
};
|
||||
|
||||
// Reads one console job: the chunks between the job header at `job` and `end`. A buffer chunk of size
|
||||
// zero is dropped whatever its address, as a PS4 does. Returns false with the refusal filled for a null
|
||||
// address with a nonzero size, for chunks that overrun the job and for a job without flags.
|
||||
bool ReadConsoleJob(std::uint32_t instance, const std::uint8_t* job, const std::uint8_t* end, ConsoleJob& out, ConsoleRefusal& refusal) {
|
||||
out.header = JobHeader{};
|
||||
out.header.instance = instance;
|
||||
out.header.console = true;
|
||||
std::vector<AjmBuffer> inputs;
|
||||
std::vector<AjmBuffer> outputs;
|
||||
AjmBuffer controlInput{};
|
||||
AjmBuffer controlOutput{};
|
||||
ConsoleChunk flagsKind = ConsoleChunk::Job;
|
||||
refusal = ConsoleRefusal{};
|
||||
refusal.job = job;
|
||||
const std::uint8_t* at = job + 2 * sizeof(std::uint32_t);
|
||||
while (at < end) {
|
||||
refusal.offset = static_cast<std::uint32_t>(at - job);
|
||||
if (end - at < 8) return false;
|
||||
std::uint32_t words[2];
|
||||
std::memcpy(words, at, sizeof(words));
|
||||
const auto kind = ConsoleChunkKind(words[0]);
|
||||
if (kind == ConsoleChunk::ControlFlags || kind == ConsoleChunk::RunFlags) {
|
||||
if (flagsKind != ConsoleChunk::Job) return false;
|
||||
flagsKind = kind;
|
||||
out.header.flags = (std::uint64_t{ConsoleChunkPayload(words[0])} << 32) | words[1];
|
||||
at += sizeof(words);
|
||||
continue;
|
||||
}
|
||||
if (kind != ConsoleChunk::InputRunBuffer && kind != ConsoleChunk::InputControlBuffer && kind != ConsoleChunk::ReturnAddress && kind != ConsoleChunk::OutputRunBuffer && kind != ConsoleChunk::OutputControlBuffer) {
|
||||
NotImplemented_nid_no_patch(("sceAjmBatchStartBuffer (job chunk kind " + std::to_string(static_cast<unsigned>(kind)) + ")").c_str());
|
||||
}
|
||||
if (end - at < 16) return false;
|
||||
AjmBuffer buffer{nullptr, words[1]};
|
||||
std::memcpy(&buffer.ptr, at + sizeof(words), sizeof(buffer.ptr));
|
||||
at += 16;
|
||||
if (kind == ConsoleChunk::ReturnAddress) {
|
||||
refusal.returnAddress = buffer.ptr;
|
||||
continue;
|
||||
}
|
||||
if (buffer.size == 0) continue;
|
||||
if (!buffer.ptr) {
|
||||
refusal.code = SCE_AJM_ERROR_INVALID_ADDRESS;
|
||||
return false;
|
||||
}
|
||||
if (kind == ConsoleChunk::InputRunBuffer) inputs.push_back(buffer);
|
||||
if (kind == ConsoleChunk::OutputRunBuffer) outputs.push_back(buffer);
|
||||
if (kind == ConsoleChunk::InputControlBuffer) controlInput = buffer;
|
||||
if (kind == ConsoleChunk::OutputControlBuffer) controlOutput = buffer;
|
||||
}
|
||||
refusal.offset = static_cast<std::uint32_t>(end - job);
|
||||
if (flagsKind == ConsoleChunk::Job) return false;
|
||||
JobHeader& header = out.header;
|
||||
header.sideband = controlOutput.ptr;
|
||||
header.sidebandSize = controlOutput.size;
|
||||
if (instance == CONSOLE_STATISTICS_INSTANCE) {
|
||||
header.kind = JobKind::GetStatistics;
|
||||
} else if (flagsKind == ConsoleChunk::ControlFlags) {
|
||||
constexpr std::uint64_t implemented = CONSOLE_FLAGS_VERSION_AND_CODEC | CONTROL_RESET | CONTROL_INITIALIZE | SIDEBAND_GAPLESS_DECODE;
|
||||
if ((header.flags & ~implemented) != 0) NotImplemented_nid_no_patch("sceAjmBatchStartBuffer (control job flags other than RESET, INITIALIZE and SIDEBAND_GAPLESS_DECODE)");
|
||||
if (!inputs.empty() || !outputs.empty()) NotImplemented_nid_no_patch("sceAjmBatchStartBuffer (control job with run buffers)");
|
||||
header.kind = JobKind::Control;
|
||||
if (controlInput.size) inputs.push_back(controlInput);
|
||||
} else {
|
||||
if (controlInput.size) NotImplemented_nid_no_patch("sceAjmBatchStartBuffer (run job with an input control buffer)");
|
||||
header.kind = JobKind::Run;
|
||||
}
|
||||
header.inputCount = static_cast<std::uint32_t>(inputs.size());
|
||||
header.outputCount = static_cast<std::uint32_t>(outputs.size());
|
||||
AJM_TRACE("[ajm] console job: instance %u, %s flags 0x%llx, %u input and %u output buffers, control input %llu and output %llu bytes\n", instance, flagsKind == ConsoleChunk::ControlFlags ? "control" : "run", static_cast<unsigned long long>(header.flags), header.inputCount, header.outputCount, static_cast<unsigned long long>(controlInput.size), static_cast<unsigned long long>(controlOutput.size));
|
||||
out.buffers = std::move(inputs);
|
||||
out.buffers.insert(out.buffers.end(), outputs.begin(), outputs.end());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
@@ -1893,14 +2078,19 @@ int APS5_VABI sceAjmBatchJobGetStatistics(AjmBatchInfo* info, float interval, vo
|
||||
int APS5_VABI sceAjmBatchStart(uint32_t context, const AjmBatchInfo* info, int priority, AjmBatchError* error, uint32_t* batch) {
|
||||
if (context == 0 || context >= g_nextContext.load(std::memory_order_relaxed)) return SCE_AJM_ERROR_INVALID_CONTEXT;
|
||||
(void)priority;
|
||||
if (!info || !batch) return SCE_AJM_ERROR_INVALID_PARAMETER;
|
||||
if (!info || !batch || !info->p_buffer || info->offset > info->size) return SCE_AJM_ERROR_INVALID_PARAMETER;
|
||||
AJM_TRACE("[ajm] batch start: context %u, priority %d, %llu of %llu buffer bytes used\n", context, priority, static_cast<unsigned long long>(info->offset), static_cast<unsigned long long>(info->size));
|
||||
const auto* cursor = static_cast<const std::uint8_t*>(info->p_buffer);
|
||||
const auto* end = cursor + info->offset;
|
||||
auto remaining = info->offset;
|
||||
std::vector<AjmBuffer> buffers;
|
||||
while (cursor < end) {
|
||||
while (remaining != 0) {
|
||||
if (remaining < sizeof(PackedJob)) NotImplemented_nid_no_patch("AJM batch contains a truncated private job header");
|
||||
PackedJob record;
|
||||
std::memcpy(&record, cursor, sizeof(record));
|
||||
const std::size_t bufferCount = static_cast<std::size_t>(record.inputCount) + record.outputCount;
|
||||
const std::size_t parameterBytes = (static_cast<std::size_t>(record.parameterSize) + 7u) & ~std::size_t{7};
|
||||
const std::size_t requiredBytes = sizeof(PackedJob) + bufferCount * sizeof(AjmBuffer) + parameterBytes;
|
||||
if (record.parameterSize > sizeof(JobHeader::parameters) || record.bytes < requiredBytes || record.bytes > remaining) NotImplemented_nid_no_patch("AJM batch contains an invalid private job size");
|
||||
JobHeader job{};
|
||||
job.kind = static_cast<JobKind>(record.kind);
|
||||
job.bytes = record.bytes;
|
||||
@@ -1915,16 +2105,10 @@ int APS5_VABI sceAjmBatchStart(uint32_t context, const AjmBatchInfo* info, int p
|
||||
if (!buffers.empty()) std::memcpy(buffers.data(), cursor + sizeof(record), buffers.size() * sizeof(AjmBuffer));
|
||||
if (record.parameterSize) std::memcpy(job.parameters, cursor + sizeof(record) + buffers.size() * sizeof(AjmBuffer), record.parameterSize);
|
||||
Execute(job, buffers.data(), buffers.data() + job.inputCount);
|
||||
cursor += record.bytes;
|
||||
remaining -= record.bytes;
|
||||
if (remaining != 0) cursor += record.bytes;
|
||||
}
|
||||
if (error) std::memset(error, 0, sizeof(*error));
|
||||
const auto id = g_nextBatch.fetch_add(1, std::memory_order_relaxed);
|
||||
{
|
||||
std::lock_guard lock(g_batchLock);
|
||||
g_batches.emplace(id, context);
|
||||
}
|
||||
*batch = id;
|
||||
return 0;
|
||||
return RegisterBatch(context, error, batch);
|
||||
}
|
||||
|
||||
int APS5_VABI sceAjmBatchWait(uint32_t context, uint32_t batch, uint32_t timeout, AjmBatchError* error) {
|
||||
@@ -1953,4 +2137,69 @@ int APS5_VABI sceAjmBatchErrorDump(const AjmBatchInfo* info, AjmBatchError* erro
|
||||
return 0;
|
||||
}
|
||||
|
||||
void* APS5_VABI sceAjmBatchJobControlBufferRa(void* position, uint32_t instance, uint64_t flags, const void* sideband_input, size_t sideband_input_size, void* sideband_output, size_t sideband_output_size, void* return_address) {
|
||||
auto* job = static_cast<std::uint8_t*>(position);
|
||||
auto* at = BeginConsoleJob(job, return_address);
|
||||
at = PutConsoleBuffer(at, ConsoleChunk::InputControlBuffer, sideband_input, sideband_input_size);
|
||||
at = PutConsoleFlags(at, ConsoleChunk::ControlFlags, flags);
|
||||
at = PutConsoleBuffer(at, ConsoleChunk::OutputControlBuffer, sideband_output, sideband_output_size);
|
||||
return EndConsoleJob(job, at, instance);
|
||||
}
|
||||
|
||||
void* APS5_VABI sceAjmBatchJobRunSplitBufferRa(void* position, uint32_t instance, uint64_t flags, const AjmBuffer* input_buffers, size_t input_buffers_num, const AjmBuffer* output_buffers, size_t output_buffers_num, void* sideband_output, size_t sideband_output_size, void* return_address) {
|
||||
if ((input_buffers_num && !input_buffers) || (output_buffers_num && !output_buffers)) NotImplemented_nid_no_patch("AJM batch job with a null buffer list and a nonzero buffer count");
|
||||
auto* job = static_cast<std::uint8_t*>(position);
|
||||
auto* at = BeginConsoleJob(job, return_address);
|
||||
for (std::size_t index = 0; index < input_buffers_num; ++index) at = PutConsoleBuffer(at, ConsoleChunk::InputRunBuffer, input_buffers[index].ptr, input_buffers[index].size);
|
||||
at = PutConsoleFlags(at, ConsoleChunk::RunFlags, flags);
|
||||
for (std::size_t index = 0; index < output_buffers_num; ++index) at = PutConsoleBuffer(at, ConsoleChunk::OutputRunBuffer, output_buffers[index].ptr, output_buffers[index].size);
|
||||
at = PutConsoleBuffer(at, ConsoleChunk::OutputControlBuffer, sideband_output, sideband_output_size);
|
||||
return EndConsoleJob(job, at, instance);
|
||||
}
|
||||
|
||||
void* APS5_VABI sceAjmBatchJobRunBufferRa(void* position, uint32_t instance, uint64_t flags, const void* data_input, size_t data_input_size, void* data_output, size_t data_output_size, void* sideband_output, size_t sideband_output_size, void* return_address) {
|
||||
const AjmBuffer input{const_cast<void*>(data_input), data_input_size};
|
||||
const AjmBuffer output{data_output, data_output_size};
|
||||
return sceAjmBatchJobRunSplitBufferRa(position, instance, flags, &input, 1, &output, 1, sideband_output, sideband_output_size, return_address);
|
||||
}
|
||||
|
||||
int APS5_VABI sceAjmBatchStartBuffer(uint32_t context, const uint8_t* batch, uint32_t batch_size, int priority, AjmBatchError* error, uint32_t* batch_id) {
|
||||
if (context == 0 || context >= g_nextContext.load(std::memory_order_relaxed)) return SCE_AJM_ERROR_INVALID_CONTEXT;
|
||||
if (!batch_id || !batch) return SCE_AJM_ERROR_INVALID_PARAMETER;
|
||||
AJM_TRACE("[ajm] batch start buffer: context %u, priority %d, %u bytes\n", context, priority, batch_size);
|
||||
std::vector<ConsoleJob> jobs;
|
||||
ConsoleRefusal refusal;
|
||||
const std::uint8_t* at = batch;
|
||||
const std::uint8_t* end = batch + batch_size;
|
||||
bool accepted = batch_size % 8 == 0;
|
||||
while (accepted && at < end) {
|
||||
refusal = ConsoleRefusal{};
|
||||
refusal.job = at;
|
||||
if (end - at < 8) {
|
||||
accepted = false;
|
||||
break;
|
||||
}
|
||||
std::uint32_t words[2];
|
||||
std::memcpy(words, at, sizeof(words));
|
||||
const std::uint8_t* body = at + sizeof(words);
|
||||
if (words[1] > static_cast<std::size_t>(end - body)) {
|
||||
accepted = false;
|
||||
break;
|
||||
}
|
||||
const auto kind = ConsoleChunkKind(words[0]);
|
||||
if (kind == ConsoleChunk::Job) {
|
||||
accepted = ReadConsoleJob(ConsoleChunkPayload(words[0]), at, body + words[1], jobs.emplace_back(), refusal);
|
||||
} else if (kind != ConsoleChunk::InlineBuffer) {
|
||||
NotImplemented_nid_no_patch(("sceAjmBatchStartBuffer (batch chunk kind " + std::to_string(static_cast<unsigned>(kind)) + ")").c_str());
|
||||
}
|
||||
at = body + words[1];
|
||||
}
|
||||
if (!accepted) {
|
||||
if (error) *error = AjmBatchError{refusal.code, refusal.job, refusal.offset, refusal.returnAddress};
|
||||
return SCE_AJM_ERROR_MALFORMED_BATCH;
|
||||
}
|
||||
for (const ConsoleJob& job : jobs) Execute(job.header, job.buffers.data(), job.buffers.data() + job.header.inputCount);
|
||||
return RegisterBatch(context, error, batch_id);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,19 +6,6 @@
|
||||
|
||||
extern "C" {
|
||||
|
||||
int APS5_VABI sceAjmBatchJobControlBufferRa() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAjmBatchJobRunSplitBufferRa() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAjmBatchStartBuffer() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
APS5_DUMMY_FUN
|
||||
|
||||
}
|
||||
@@ -4,8 +4,4 @@
|
||||
|
||||
extern "C" {
|
||||
|
||||
int APS5_VABI sceAjmBatchJobControlBufferRa();
|
||||
int APS5_VABI sceAjmBatchJobRunSplitBufferRa();
|
||||
int APS5_VABI sceAjmBatchStartBuffer();
|
||||
|
||||
}
|
||||
Loaded 100 of 595 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user