mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-22 00:22:57 -05:00
Merge main into perf/shader-image-table-reuse-v3-pr
#7527's selected-record limit test reads a record's descriptor words through the table snapshot's shared words list; this branch keeps a snapshot's words per column, so the test reads them from its column instead.
This commit is contained in:
commit
d0325058f7
200 files changed
+9036
-471
No files matched your search
@@ -0,0 +1,40 @@
|
||||
name: Pages progress
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- core/libs/prx/**
|
||||
- core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write
|
||||
pages: read
|
||||
|
||||
concurrency:
|
||||
group: pages-progress
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
progress:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Deploy Pages if the progress changed
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
run: |
|
||||
python3 tools/progress.py "$RUNNER_TEMP/progress"
|
||||
site=$(gh api "repos/$GH_REPO/pages" --jq .html_url)
|
||||
if curl -fsSL "${site%/}/progress.json?$GITHUB_SHA" -o "$RUNNER_TEMP/deployed.json" &&
|
||||
cmp -s "$RUNNER_TEMP/deployed.json" "$RUNNER_TEMP/progress/progress.json"; then
|
||||
echo "Progress unchanged, Pages not deployed" >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
gh workflow run pages.yml --ref main
|
||||
echo "Progress changed, Pages deployment dispatched" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
@@ -3,6 +3,17 @@ name: Pages
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- .github/workflows/pages.yml
|
||||
- CMakeLists.txt
|
||||
- core/relinker/**
|
||||
- demo/web/**
|
||||
- tools/build_web.py
|
||||
- tools/nid_names.py
|
||||
- tools/progress.py
|
||||
- tools/progress_history.py
|
||||
- tools/rdna_isa.txt
|
||||
- tools/test_web.py
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
Tool for automatic executables porting to Linux and Windows.
|
||||
|
||||
The relinker-only build runs on macOS, including Apple silicon.
|
||||
|
||||
[**Web converter**](https://boykopovar.github.io/AnyPS5/) - files stay on your device.
|
||||
|
||||

|
||||
|
||||
@@ -272,6 +272,7 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
if(APPLE)
|
||||
add_subdirectory(tests/write_watch)
|
||||
add_subdirectory(tests/guest_arena)
|
||||
endif()
|
||||
if(WIN32)
|
||||
add_test_executable(windows_write_watch_tests tests/WindowsWriteWatch.cpp)
|
||||
|
||||
@@ -1288,7 +1288,7 @@ struct NetCtlIfStat {
|
||||
std::uint32_t reserved[8];
|
||||
};
|
||||
|
||||
using NetCtlCallback = void (*)(int, void*);
|
||||
using NetCtlCallback = void (APS5_VABI *)(int, void*);
|
||||
|
||||
struct HttpEpoll;
|
||||
using HttpEpollHandle = HttpEpoll*;
|
||||
|
||||
@@ -43,7 +43,7 @@ std::uint32_t APS5_VABI sceAgcAcbAtomicMemGetSize() {
|
||||
}
|
||||
|
||||
std::uint32_t APS5_VABI sceAgcAcbAtomicGdsGetSize() {
|
||||
return 36;
|
||||
return 44;
|
||||
}
|
||||
|
||||
std::uint32_t* APS5_VABI sceAgcAcbPrimeUtcl2(CommandBuffer* buf, const volatile void* address, std::uint32_t sizeInBytes) {
|
||||
|
||||
@@ -81,6 +81,19 @@ if(BUILD_TESTING)
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_atomic_gds_size_tests EXCLUDE_FROM_ALL tests/AtomicGdsSize.cpp)
|
||||
target_include_directories(agc_atomic_gds_size_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_atomic_gds_size_tests PRIVATE libSceAgc libc)
|
||||
configure_windows_unwind(agc_atomic_gds_size_tests)
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_atomic_gds_size_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
|
||||
add_test(NAME agc_atomic_gds_size COMMAND agc_atomic_gds_size_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_atomic_gds_size PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
configure_windows_unwind(libSceAgc)
|
||||
|
||||
add_test_executable(agc_platform_tests EXCLUDE_FROM_ALL tests/Platform.cpp)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
#include "prx/libSceAgc/Command/include/Packet.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
|
||||
extern "C" std::uint32_t APS5_VABI sceAgcAcbAtomicGdsGetSize();
|
||||
extern "C" std::uint32_t* APS5_VABI sceAgcAcbAtomicGds_0900(CommandBuffer* buf, std::uint32_t control, std::uint32_t mode,
|
||||
std::uint32_t gdsOp, std::uint32_t src, std::uint16_t offset, std::uint16_t index, std::uint32_t mask,
|
||||
std::uint32_t data, std::uint64_t compareData, std::uint64_t extraData);
|
||||
|
||||
int main() {
|
||||
constexpr std::uint32_t expectedSize = 11u * sizeof(std::uint32_t);
|
||||
if (sceAgcAcbAtomicGdsGetSize() != expectedSize) {
|
||||
std::fprintf(stderr, "ACB atomic GDS size does not match its 11-dword packet\n");
|
||||
return 1;
|
||||
}
|
||||
std::array<std::uint32_t, 64> words{};
|
||||
CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0};
|
||||
const auto* packet = sceAgcAcbAtomicGds_0900(&buffer, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
|
||||
if (packet != words.data() || buffer.cursor_up != words.data() + expectedSize / sizeof(std::uint32_t)) {
|
||||
std::fprintf(stderr, "ACB atomic GDS builder wrote a different size than its size accessor\n");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -138,6 +138,23 @@ foreach(agcPresentationStage IN ITEMS Vert Frag)
|
||||
)
|
||||
list(APPEND agcPresentationShaderHeaders ${agcPresentationShaderHeader})
|
||||
endforeach()
|
||||
foreach(agcOverlayStage IN ITEMS Vert Frag)
|
||||
string(TOLOWER "${agcOverlayStage}" agcOverlayExtension)
|
||||
string(TOUPPER "${agcOverlayStage}" agcOverlaySymbol)
|
||||
set(agcOverlayShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/PerformanceOverlay.${agcOverlayExtension})
|
||||
set(agcOverlayShaderSpv ${agcTextureDetileGeneratedDir}/PerformanceOverlay${agcOverlayStage}.spv)
|
||||
set(agcOverlayShaderHeader ${agcTextureDetileGeneratedDir}/PerformanceOverlay${agcOverlayStage}_spv.h)
|
||||
add_custom_command(
|
||||
OUTPUT "${agcOverlayShaderHeader}"
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}"
|
||||
COMMAND "$<TARGET_FILE:glslang-standalone>" -V --target-env vulkan1.1 -o "${agcOverlayShaderSpv}" "${agcOverlayShaderSource}"
|
||||
COMMAND ${CMAKE_COMMAND} -DINPUT=${agcOverlayShaderSpv} -DOUTPUT=${agcOverlayShaderHeader}
|
||||
-DSYMBOL=PERFORMANCE_OVERLAY_${agcOverlaySymbol}_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake"
|
||||
DEPENDS "${agcOverlayShaderSource}" glslang-standalone
|
||||
VERBATIM
|
||||
)
|
||||
list(APPEND agcPresentationShaderHeaders ${agcOverlayShaderHeader})
|
||||
endforeach()
|
||||
|
||||
set(agcDriverSources
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Unimplemented.cpp
|
||||
@@ -212,6 +229,7 @@ set(agcDriverSources
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/AspectFit.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PresentationScaler.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PresentationPass.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PerformanceOverlay.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Draw.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/DepthSurface.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/MultisampleTarget.cpp
|
||||
@@ -1413,6 +1431,51 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_performance_overlay_statistics_tests EXCLUDE_FROM_ALL
|
||||
tests/PerformanceOverlayStatistics.cpp
|
||||
)
|
||||
target_include_directories(agc_performance_overlay_statistics_tests PRIVATE
|
||||
${LIBS_INCLUDE_DIR}
|
||||
${CMAKE_SOURCE_DIR}/core/shader/recompiler
|
||||
${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}
|
||||
)
|
||||
target_link_libraries(agc_performance_overlay_statistics_tests PRIVATE
|
||||
libSceAgcDriver
|
||||
libc
|
||||
)
|
||||
set_target_properties(agc_performance_overlay_statistics_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_performance_overlay_statistics_tests)
|
||||
|
||||
add_test_executable(agc_performance_overlay_tests EXCLUDE_FROM_ALL
|
||||
tests/PerformanceOverlay.cpp
|
||||
)
|
||||
target_include_directories(agc_performance_overlay_tests PRIVATE
|
||||
${LIBS_INCLUDE_DIR}
|
||||
${CMAKE_SOURCE_DIR}/core/shader/recompiler
|
||||
${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}
|
||||
)
|
||||
target_link_libraries(agc_performance_overlay_tests PRIVATE
|
||||
libSceAgcDriver
|
||||
SDL2-static
|
||||
libc
|
||||
)
|
||||
set_target_properties(agc_performance_overlay_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_performance_overlay_tests)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_performance_overlay_statistics_tests agc_performance_overlay_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
|
||||
add_test(NAME agc_performance_overlay_statistics COMMAND agc_performance_overlay_statistics_tests)
|
||||
add_test(NAME agc_performance_overlay COMMAND agc_performance_overlay_tests)
|
||||
add_test(NAME agc_performance_overlay_device_selection COMMAND agc_performance_overlay_tests device-selection)
|
||||
add_test(NAME agc_performance_overlay_device_selection_cpu COMMAND agc_performance_overlay_tests device-selection-cpu)
|
||||
set_tests_properties(agc_performance_overlay agc_performance_overlay_device_selection PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
|
||||
set_tests_properties(agc_performance_overlay_device_selection_cpu PROPERTIES TIMEOUT 30)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_performance_overlay_statistics agc_performance_overlay agc_performance_overlay_device_selection agc_performance_overlay_device_selection_cpu PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_driver_pm4_tests EXCLUDE_FROM_ALL
|
||||
tests/Pm4.cpp
|
||||
)
|
||||
@@ -1718,8 +1781,10 @@ if(BUILD_TESTING)
|
||||
endforeach()
|
||||
add_test(NAME agc_driver_readonly_resummarize COMMAND agc_driver_tests --readonly-resummarize)
|
||||
set_tests_properties(agc_driver_readonly_resummarize PROPERTIES TIMEOUT 10)
|
||||
add_test(NAME agc_driver_eop_interrupts COMMAND agc_driver_tests --eop-interrupts)
|
||||
set_tests_properties(agc_driver_eop_interrupts PROPERTIES TIMEOUT 60)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_readonly_resummarize PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
set_tests_properties(agc_driver_readonly_resummarize agc_driver_eop_interrupts PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_shader_mode_register COMMAND agc_shader_mode_register_tests)
|
||||
@@ -1729,6 +1794,11 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
add_test(NAME agc_shader_bindless COMMAND agc_shader_memory_tests --bindless)
|
||||
set_tests_properties(agc_shader_bindless PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
add_test(NAME agc_shader_selected_records_image_limit COMMAND agc_shader_memory_tests --selected-records-image-limit)
|
||||
add_test(NAME agc_shader_selected_records_pair_limit COMMAND agc_shader_memory_tests --selected-records-pair-limit)
|
||||
add_test(NAME agc_shader_selected_records_direct_image_limit COMMAND agc_shader_memory_tests --selected-records-direct-image-limit)
|
||||
add_test(NAME agc_shader_selected_records_direct_pair_limit COMMAND agc_shader_memory_tests --selected-records-direct-pair-limit)
|
||||
set_tests_properties(agc_shader_selected_records_image_limit agc_shader_selected_records_pair_limit agc_shader_selected_records_direct_image_limit agc_shader_selected_records_direct_pair_limit PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
add_test(NAME agc_shader_srt_read_patching COMMAND agc_shader_memory_tests --srt-read-patching)
|
||||
set_tests_properties(agc_shader_srt_read_patching PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
add_test(NAME agc_shader_table_snapshot_reuse COMMAND agc_shader_memory_tests --table-snapshot-reuse)
|
||||
@@ -1758,7 +1828,7 @@ if(BUILD_TESTING)
|
||||
add_test(NAME agc_shader_entry_lane_proof_budget COMMAND agc_shader_memory_tests --entry-lane-proof-budget)
|
||||
set_tests_properties(agc_shader_entry_lane_proof_budget PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_shader_bindless agc_shader_srt_read_patching agc_shader_type_zero_image_materialization agc_shader_frontend_pair agc_shader_frontend_pair_off agc_shader_layout_pair agc_shader_layout_pair_off agc_shader_entry_lane_proof_budget PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
set_tests_properties(agc_shader_bindless agc_shader_selected_records_image_limit agc_shader_selected_records_pair_limit agc_shader_selected_records_direct_image_limit agc_shader_selected_records_direct_pair_limit agc_shader_srt_read_patching agc_shader_type_zero_image_materialization agc_shader_frontend_pair agc_shader_frontend_pair_off agc_shader_layout_pair agc_shader_layout_pair_off agc_shader_entry_lane_proof_budget PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
foreach(agcTest IN ITEMS agc_vertex_input_tests agc_shared_write_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_ir_reclaim_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_tests agc_scalar_select_mask_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_shader_disk_cache_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests agc_formatted_buffer_byte_stores_tests ${agcExecutionTests} agc_fma_mix_once_tests)
|
||||
if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests" OR agcTest STREQUAL "agc_driver_pipeline_optimized_readback_tests")
|
||||
@@ -1841,6 +1911,14 @@ if(BUILD_TESTING)
|
||||
set_tests_properties(agc_driver_emulated_color_compare_srgb_decode PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_driver_image_srgb_sample_copy_forced COMMAND agc_driver_image_srgb_sample_copy_tests)
|
||||
set_tests_properties(agc_driver_image_srgb_sample_copy_forced PROPERTIES
|
||||
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_SRGB_SHADER_DECODE=copy"
|
||||
SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_image_srgb_sample_copy_forced PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_shared_write_partial_collect COMMAND agc_shared_write_tests --partial-collect)
|
||||
set_tests_properties(agc_shared_write_partial_collect PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30)
|
||||
if(WIN32)
|
||||
@@ -1971,14 +2049,22 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
endforeach()
|
||||
set_tests_properties(agc_driver_storage_cache_budget PROPERTIES TIMEOUT 30)
|
||||
add_test(NAME agc_driver_storage_cache_retained_batch COMMAND agc_driver_storage_cache_budget_tests --retained-batch)
|
||||
set_tests_properties(agc_driver_storage_cache_retained_batch PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_storage_cache_retained_batch PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_driver_sampled_cache_budget_memory COMMAND agc_driver_sampled_cache_budget_tests memory)
|
||||
set_tests_properties(agc_driver_sampled_cache_budget_memory PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77)
|
||||
add_test(NAME agc_driver_sampled_cache_budget_without_storage_image_multisample COMMAND agc_driver_sampled_cache_budget_tests without-storage-image-multisample)
|
||||
set_tests_properties(agc_driver_sampled_cache_budget_without_storage_image_multisample PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30)
|
||||
add_test(NAME agc_driver_resource_cache_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-admission)
|
||||
add_test(NAME agc_driver_resource_cache_oversized_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-oversized)
|
||||
add_test(NAME agc_driver_resource_cache_storage_clear COMMAND agc_driver_sampled_cache_budget_tests resource-clear)
|
||||
set_tests_properties(agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_sampled_cache_budget_memory agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
set_tests_properties(agc_driver_sampled_cache_budget_memory agc_driver_sampled_cache_budget_without_storage_image_multisample agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
# The driver draw test again with the draw key off: the draw cache then keys on the decoded vertex
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#include <cstdint>
|
||||
#include "SceTypes.hpp"
|
||||
|
||||
void AgcDriverDeliverEopInterrupt(std::uint32_t queue);
|
||||
bool AgcDriverDeliverEopInterrupt(std::uint32_t queue);
|
||||
|
||||
extern "C" int APS5_VABI sceAgcDriverAddEqEvent(KernelEqueue eq, int id, void* udata);
|
||||
extern "C" int APS5_VABI sceAgcDriverDeleteEqEvent(KernelEqueue eq, int id);
|
||||
|
||||
@@ -25,17 +25,20 @@ std::vector<Registration> g_registrations;
|
||||
|
||||
}
|
||||
|
||||
void AgcDriverDeliverEopInterrupt(std::uint32_t queue) {
|
||||
bool AgcDriverDeliverEopInterrupt(std::uint32_t queue) {
|
||||
std::vector<Registration> registrations;
|
||||
{
|
||||
std::lock_guard lock(g_mutex);
|
||||
registrations = g_registrations;
|
||||
}
|
||||
bool delivered = false;
|
||||
for (const auto& registration : registrations) {
|
||||
if (registration.id == static_cast<int>(queue)) {
|
||||
EqueueTriggerEvent_nid_postfix(registration.eq, static_cast<uintptr_t>(registration.id), EvfiltGraphicsCore, nullptr);
|
||||
delivered = true;
|
||||
}
|
||||
}
|
||||
return delivered;
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
@@ -317,6 +317,8 @@ private:
|
||||
FrameTiming* frameTiming();
|
||||
FrameTiming* includeTimingSubmission(const Submission& submission, bool firstSegment);
|
||||
void execute(const Submission& submission);
|
||||
void deliverGuestEopInterrupt(std::uint32_t queue);
|
||||
void deliverFlipEopInterrupt(std::uint32_t queue);
|
||||
void markCompleted(std::uint64_t serial);
|
||||
static const std::atomic<std::uint64_t>*& workerQueued();
|
||||
void reapCompletionLabels();
|
||||
@@ -334,6 +336,9 @@ private:
|
||||
std::atomic<std::uint64_t> flipSerial{0};
|
||||
std::atomic<std::uint64_t> flipBatchesUnsignaled{0};
|
||||
|
||||
std::mutex eopSelfSignalMutex;
|
||||
std::unordered_map<std::uint32_t, std::uint32_t> eopSelfSignalFlips;
|
||||
|
||||
alignas(64) std::atomic<int> packetsInFlight{0};
|
||||
std::atomic<std::uint64_t> packetsDone{0};
|
||||
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PERFORMANCEOVERLAY_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PERFORMANCEOVERLAY_HPP
|
||||
|
||||
#define VK_NO_PROTOTYPES
|
||||
#include <vulkan/vulkan.h>
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Context.hpp"
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
namespace Graphics {
|
||||
class Buffer;
|
||||
}
|
||||
|
||||
enum class OverlayCorner : std::uint8_t { TopLeft, TopRight, BottomLeft, BottomRight };
|
||||
|
||||
struct OverlaySettings {
|
||||
bool visible = false;
|
||||
OverlayCorner corner = OverlayCorner::TopLeft;
|
||||
};
|
||||
|
||||
OverlaySettings ParseOverlaySettings(const char* enabled, const char* position);
|
||||
|
||||
class PerformanceCounters {
|
||||
public:
|
||||
static void AddGpuNanoseconds(std::uint64_t nanoseconds);
|
||||
static void AddDriverNanoseconds(std::uint64_t nanoseconds);
|
||||
static void CountPipeline();
|
||||
static std::uint64_t GpuNanoseconds();
|
||||
static std::uint64_t DriverNanoseconds();
|
||||
static std::uint64_t Pipelines();
|
||||
static bool GpuEnabled();
|
||||
static bool DriverEnabled();
|
||||
};
|
||||
|
||||
class PacketCounters {
|
||||
public:
|
||||
struct Totals {
|
||||
std::uint64_t draws = 0;
|
||||
std::uint64_t dispatches = 0;
|
||||
};
|
||||
|
||||
static PacketCounters& ThisThread();
|
||||
static Totals Sum();
|
||||
|
||||
void CountDraw() { draws.store(draws.load(std::memory_order_relaxed) + 1, std::memory_order_relaxed); }
|
||||
void CountDispatch() { dispatches.store(dispatches.load(std::memory_order_relaxed) + 1, std::memory_order_relaxed); }
|
||||
|
||||
private:
|
||||
alignas(64) std::atomic<std::uint64_t> draws{0};
|
||||
std::atomic<std::uint64_t> dispatches{0};
|
||||
};
|
||||
|
||||
struct OverlayCounters {
|
||||
std::uint64_t gpuNanoseconds = 0;
|
||||
std::uint64_t driverNanoseconds = 0;
|
||||
std::uint64_t draws = 0;
|
||||
std::uint64_t dispatches = 0;
|
||||
std::uint64_t pipelines = 0;
|
||||
};
|
||||
|
||||
class OverlayStatistics {
|
||||
public:
|
||||
using Clock = std::chrono::steady_clock;
|
||||
static constexpr std::size_t FrameHistory = 120;
|
||||
static constexpr Clock::duration Window = std::chrono::seconds(1);
|
||||
|
||||
struct Range {
|
||||
double minimum = 0;
|
||||
double average = 0;
|
||||
double maximum = 0;
|
||||
};
|
||||
|
||||
void AddPresent(Clock::time_point at, std::uint64_t gpuNanoseconds, std::uint64_t driverNanoseconds);
|
||||
void AddPresent(Clock::time_point at, const OverlayCounters& counters);
|
||||
void Reset();
|
||||
double Fps() const;
|
||||
double LastFrameMs() const;
|
||||
std::vector<double> FrameTimes() const;
|
||||
Range FrameTimeRange() const;
|
||||
double GpuMsPerFrame() const;
|
||||
double DriverMsPerFrame() const;
|
||||
double DrawsPerFrame() const;
|
||||
double DispatchesPerFrame() const;
|
||||
std::uint64_t PipelinesInWindow() const;
|
||||
OverlayCounters Latest() const;
|
||||
|
||||
private:
|
||||
struct Sample {
|
||||
Clock::time_point at;
|
||||
OverlayCounters counters;
|
||||
};
|
||||
|
||||
double perFrame(std::uint64_t OverlayCounters::*counter) const;
|
||||
|
||||
std::deque<Sample> window;
|
||||
std::optional<Sample> previous;
|
||||
std::array<double, FrameHistory> frameTimes{};
|
||||
std::size_t frameCount = 0;
|
||||
std::size_t frameNext = 0;
|
||||
};
|
||||
|
||||
struct OverlayRect {
|
||||
std::int32_t x = 0;
|
||||
std::int32_t y = 0;
|
||||
std::int32_t width = 0;
|
||||
std::int32_t height = 0;
|
||||
std::uint32_t color = 0;
|
||||
};
|
||||
|
||||
enum class OverlayColor : std::uint32_t { Background = 0, Text = 1, Graph = 2 };
|
||||
inline constexpr std::size_t OverlayColorCount = 3;
|
||||
|
||||
struct OverlayContent {
|
||||
std::vector<std::string> above;
|
||||
std::vector<double> frameTimes;
|
||||
std::vector<std::string> below;
|
||||
};
|
||||
|
||||
struct OverlayMemory {
|
||||
std::uint64_t used = 0;
|
||||
std::uint64_t budget = 0;
|
||||
};
|
||||
|
||||
struct OverlayDetails {
|
||||
std::optional<OverlayMemory> vram;
|
||||
std::optional<std::uint64_t> ram;
|
||||
VkExtent2D display{};
|
||||
VkExtent2D window{};
|
||||
};
|
||||
|
||||
struct OverlayLayout {
|
||||
OverlayRect bounds;
|
||||
std::vector<OverlayRect> rects;
|
||||
std::int32_t scale = 1;
|
||||
};
|
||||
|
||||
struct OverlayMetrics {
|
||||
static constexpr std::int32_t GlyphWidth = 5;
|
||||
static constexpr std::int32_t GlyphHeight = 7;
|
||||
static constexpr std::int32_t Advance = 6;
|
||||
static constexpr std::int32_t LinePitch = 9;
|
||||
static constexpr std::int32_t Columns = 27;
|
||||
static constexpr std::int32_t Padding = 3;
|
||||
static constexpr std::int32_t Margin = 4;
|
||||
static constexpr std::int32_t GraphHeight = 20;
|
||||
static constexpr std::int32_t GraphGap = 2;
|
||||
static constexpr std::int32_t InnerWidth = Columns * Advance - 1;
|
||||
};
|
||||
|
||||
const std::array<std::uint8_t, OverlayMetrics::GlyphHeight>& OverlayGlyph(char character);
|
||||
std::string FormatOverlayValue(double value);
|
||||
std::string FormatOverlayRate(double value);
|
||||
std::string FormatOverlayCount(double value);
|
||||
std::string FormatOverlayGigabytes(std::uint64_t bytes);
|
||||
OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, bool driver);
|
||||
OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, bool driver, const OverlayDetails& details);
|
||||
bool OverlaySampleDue(std::optional<OverlayStatistics::Clock::time_point> sampledAt, OverlayStatistics::Clock::time_point now);
|
||||
std::optional<OverlayMemory> ReadVramBudget(const Graphics::Context& context);
|
||||
std::optional<std::uint64_t> ReadProcessPrivateBytes();
|
||||
std::int32_t OverlayScale(VkExtent2D extent, const OverlayContent& content);
|
||||
OverlayLayout LayOutOverlay(const OverlayContent& content, VkExtent2D extent, OverlayCorner corner);
|
||||
std::array<std::array<float, 4>, OverlayColorCount> OverlayPalette(VkFormat format, VkColorSpaceKHR colorSpace);
|
||||
|
||||
class PerformanceOverlay {
|
||||
public:
|
||||
PerformanceOverlay(const Graphics::Context& context, std::size_t slots);
|
||||
~PerformanceOverlay();
|
||||
PerformanceOverlay(const PerformanceOverlay&) = delete;
|
||||
PerformanceOverlay& operator=(const PerformanceOverlay&) = delete;
|
||||
|
||||
static bool Visible();
|
||||
static void Toggle();
|
||||
static OverlayCorner Corner();
|
||||
|
||||
void NotePresent(OverlayStatistics::Clock::time_point at, VkExtent2D display);
|
||||
const OverlayStatistics& Statistics() const { return statistics; }
|
||||
const OverlayDetails& Details() const { return details; }
|
||||
OverlayRect Record(VkCommandBuffer commands, std::size_t slot, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout);
|
||||
OverlayRect Record(VkCommandBuffer commands, std::size_t slot, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout, const OverlayContent& content, OverlayCorner corner);
|
||||
void Release(std::size_t slot) noexcept;
|
||||
|
||||
private:
|
||||
struct Slot {
|
||||
std::unique_ptr<Graphics::Buffer> rects;
|
||||
std::size_t capacity = 0;
|
||||
std::vector<std::pair<VkImageView, VkFramebuffer>> transient;
|
||||
};
|
||||
|
||||
VkRenderPass renderPass(VkFormat format, VkImageLayout initialLayout, VkImageLayout finalLayout);
|
||||
VkPipeline pipeline(VkFormat format, VkRenderPass compatible);
|
||||
void releaseTransient(Slot& slot) noexcept;
|
||||
void release() noexcept;
|
||||
void restartIfToggled();
|
||||
|
||||
Graphics::Context context;
|
||||
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
|
||||
VkShaderModule vertex = VK_NULL_HANDLE;
|
||||
VkShaderModule fragment = VK_NULL_HANDLE;
|
||||
std::map<std::tuple<VkFormat, VkImageLayout, VkImageLayout>, VkRenderPass> renderPasses;
|
||||
std::map<VkFormat, VkPipeline> pipelines;
|
||||
std::vector<Slot> slots;
|
||||
OverlayStatistics statistics;
|
||||
OverlayDetails details;
|
||||
std::optional<OverlayStatistics::Clock::time_point> sampledAt;
|
||||
std::uint64_t toggles = 0;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -29,6 +29,7 @@ extern "C" void AgcDriverAttachWindow_nid_postfix(const AgcDriver::PresentationW
|
||||
extern "C" void AgcDriverPresentClear_nid_postfix(const AgcDriver::PresentationWindow& window, bool opaque, void (*gpuReady)(void*), void* context);
|
||||
extern "C" void AgcDriverPresentBuffer_nid_postfix(const AgcDriver::PresentationWindow& window, const AgcDriver::DisplayBuffer& buffer, void (*gpuReady)(void*), void* context);
|
||||
extern "C" void AgcDriverReleaseWindow_nid_postfix(void* window);
|
||||
extern "C" void AgcDriverTogglePerformanceOverlay_nid_postfix();
|
||||
extern "C" void AgcDriverReportFailure_nid_postfix(std::exception_ptr error);
|
||||
|
||||
#endif
|
||||
@@ -13,10 +13,13 @@
|
||||
#include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "ShaderDiskCache.hpp"
|
||||
#include "prx/libc/include/GuestArena.hpp"
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cerrno>
|
||||
#include <exception>
|
||||
#include <stdexcept>
|
||||
#include <system_error>
|
||||
#include <thread>
|
||||
#ifdef _WIN32
|
||||
@@ -26,6 +29,7 @@
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <pthread.h>
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
|
||||
namespace AgcDriver::DriverDetail {
|
||||
@@ -48,6 +52,45 @@ void PrintFailure(std::exception_ptr error) {
|
||||
std::fflush(stderr);
|
||||
}
|
||||
|
||||
constexpr std::uint64_t DriverMemoryAddress = 0xfe0000000ull;
|
||||
constexpr std::size_t DriverMemoryBytes = 2u << 20u;
|
||||
constexpr std::uint64_t FsTableAddress = 0xfe0040000ull;
|
||||
constexpr std::uint64_t FsTableSlotBytes = 16u;
|
||||
constexpr std::uint64_t FsTableSlots = 6u;
|
||||
constexpr std::uint64_t FsRoutineRecords = 8u;
|
||||
constexpr std::uint64_t FsRoutineRecordBytes = 16u;
|
||||
constexpr std::uint64_t FsRoutineTableBytes = FsRoutineRecords * FsRoutineRecordBytes;
|
||||
constexpr std::uint64_t FsRoutineTableAddress = FsTableAddress + FsTableSlots * FsTableSlotBytes;
|
||||
constexpr std::uint64_t FsRoutineAddress = FsRoutineTableAddress + FsRoutineTableBytes;
|
||||
constexpr std::uint32_t ScalarSetpcB64LinkS14 = 0xbe80200eu;
|
||||
constexpr std::uint32_t ScalarEndProgram = 0xbf810000u;
|
||||
constexpr std::uint64_t FsTableDword3 = 0x5204u;
|
||||
|
||||
void MapDriverMemory() {
|
||||
void* mapped;
|
||||
#ifdef _WIN32
|
||||
mapped = reinterpret_cast<void*>(DriverMemoryAddress);
|
||||
GuestArena::GuestArenaCommit_nid_postfix(mapped, DriverMemoryBytes, PAGE_READWRITE, DriverMemoryBytes);
|
||||
#else
|
||||
mapped = mmap(reinterpret_cast<void*>(DriverMemoryAddress), DriverMemoryBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0);
|
||||
if (mapped == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "map the AGC driver memory");
|
||||
#endif
|
||||
if (reinterpret_cast<std::uint64_t>(mapped) != DriverMemoryAddress) throw std::runtime_error("the AGC driver memory was not mapped at its fixed address");
|
||||
auto* routineTable = reinterpret_cast<std::uint64_t*>(FsRoutineTableAddress);
|
||||
for (std::uint64_t record = 0; record < FsRoutineRecords; ++record) {
|
||||
routineTable[record * 2u] = FsRoutineAddress;
|
||||
routineTable[record * 2u + 1u] = 0u;
|
||||
}
|
||||
auto* routine = reinterpret_cast<std::uint32_t*>(FsRoutineAddress);
|
||||
routine[0] = ScalarSetpcB64LinkS14;
|
||||
routine[1] = ScalarEndProgram;
|
||||
auto* slot = reinterpret_cast<std::uint64_t*>(FsTableAddress);
|
||||
slot[0] = (FsRoutineTableAddress & 0xffffffffffffull) | 16ull << 48u;
|
||||
slot[1] = FsTableDword3 << 32u | FsRoutineRecords;
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Add(mapped, DriverMemoryBytes, true, true, true);
|
||||
}
|
||||
|
||||
bool StackHoldsImport() {
|
||||
#ifdef _WIN32
|
||||
ULONG_PTR low = 0;
|
||||
@@ -137,6 +180,8 @@ void Driver::stop() {
|
||||
|
||||
Driver::Driver() {
|
||||
InstallAsyncLog_nid_no_patch();
|
||||
static const bool driverMemory = (MapDriverMemory(), true);
|
||||
static_cast<void>(driverMemory);
|
||||
static_cast<void>(GuestMemory::TrackerGeneration());
|
||||
ShaderMemory::SetWaitedMsProvider(&Graphics::Recorder::ThreadWaitedMs);
|
||||
try {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
|
||||
#include "prx/libSceAgcDriver/Eq/include/Event.hpp"
|
||||
@@ -18,20 +19,38 @@ namespace AgcDriver::DriverDetail {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::uint32_t eopSelfSignalFlipLimit = 4;
|
||||
|
||||
bool orderedFills() {
|
||||
static const bool ordered = std::getenv("APS5_PIPELINE_DRAIN_FILLS") == nullptr;
|
||||
return ordered;
|
||||
}
|
||||
|
||||
bool pipelineDma() {
|
||||
static const bool enabled = std::getenv("APS5_PIPELINE_DMA") != nullptr;
|
||||
return enabled;
|
||||
}
|
||||
|
||||
bool keepsDrawPipeline(std::span<const std::uint32_t> packet, std::uint32_t header, std::uint32_t opcode) {
|
||||
if (header == FlipPacketHeader || header == RenderingWaitPacketHeader) return false;
|
||||
if (Pm4::DrawOpcode(opcode)) return true;
|
||||
switch (opcode) {
|
||||
case 0x11: case 0x12: case 0x13: case 0x26: case 0x2a: case 0x2f: case 0x42: case 0x58: case 0x59: case 0x69: case 0x76: case 0x79: case 0x7a: case 0x81: return true;
|
||||
case 0x11: case 0x12: case 0x13: case 0x20: case 0x26: case 0x2a: case 0x2f: case 0x42: case 0x58: case 0x59: case 0x69: case 0x76: case 0x79: case 0x7a: case 0x81: return true;
|
||||
case 0x46: return packet.size() >= 2 && (packet[1] & 0x3fu) != 0x39u;
|
||||
case 0x15: return orderedFills();
|
||||
case 0x63: case 0x64: case 0x9f:
|
||||
return packet.size() >= 5 && !DrawPipeline::Queue0().Overlaps(packet[1] | (static_cast<std::uint64_t>(packet[2]) << 32u), static_cast<std::size_t>(packet[4]) * 8);
|
||||
case 0x50: {
|
||||
if (!pipelineDma() || packet.size() < 7) return false;
|
||||
const auto destination = Pm4::DmaDestination(packet);
|
||||
if (destination == Pm4::DmaSelectGds || destination == Pm4::DmaDestinationNowhere) return true;
|
||||
const auto bytes = Pm4::DmaBytes(packet);
|
||||
if (bytes == 0) return true;
|
||||
const auto source = Pm4::DmaSource(packet);
|
||||
if ((source == 0 || source == 3) && DrawPipeline::Queue0().Overlaps(packet[2] | (static_cast<std::uint64_t>(packet[3]) << 32u), bytes)) return false;
|
||||
const auto address = packet[4] | (static_cast<std::uint64_t>(packet[5]) << 32u);
|
||||
return !DrawPipeline::Queue0().Overlaps(address, bytes);
|
||||
}
|
||||
case 0x10: {
|
||||
const auto operation = (header >> 2u) & 0x3fu;
|
||||
return operation == 0 || operation == 0x09 || operation == 0x0b || operation == 0x0c || operation == 0x1a;
|
||||
@@ -42,6 +61,24 @@ bool keepsDrawPipeline(std::span<const std::uint32_t> packet, std::uint32_t head
|
||||
|
||||
}
|
||||
|
||||
void Driver::deliverGuestEopInterrupt(std::uint32_t queue) {
|
||||
if (!AgcDriverDeliverEopInterrupt(queue)) return;
|
||||
std::lock_guard<std::mutex> lock(eopSelfSignalMutex);
|
||||
eopSelfSignalFlips[queue] = 0;
|
||||
}
|
||||
|
||||
void Driver::deliverFlipEopInterrupt(std::uint32_t queue) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(eopSelfSignalMutex);
|
||||
const auto latched = eopSelfSignalFlips.find(queue);
|
||||
if (latched != eopSelfSignalFlips.end()) {
|
||||
if (++latched->second <= eopSelfSignalFlipLimit) return;
|
||||
eopSelfSignalFlips.erase(latched);
|
||||
}
|
||||
}
|
||||
AgcDriverDeliverEopInterrupt(queue);
|
||||
}
|
||||
|
||||
template <typename TWork>
|
||||
void Driver::timed(double WorkerProfile::*bucket, TWork&& work) {
|
||||
static thread_local WorkerProfile profile;
|
||||
@@ -54,6 +91,7 @@ void Driver::timed(double WorkerProfile::*bucket, TWork&& work) {
|
||||
profile.reported = end;
|
||||
AgcDriver::ProfilePrint_nid_no_patch( "[gpu] worker at %.0f s: dispatch %.1f s, draw %.1f s, wait %.1f s\n", std::chrono::duration<double>(end - profile.start).count(), profile.dispatchMs / 1000, profile.drawMs / 1000, profile.waitMs / 1000);
|
||||
}
|
||||
if (report && bucket != &WorkerProfile::waitMs) PerformanceCounters::AddDriverNanoseconds(static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(end - begin).count()));
|
||||
}
|
||||
|
||||
void Driver::execute(const Submission& submission) {
|
||||
@@ -149,6 +187,7 @@ void Driver::execute(const Submission& submission) {
|
||||
thread_local PacketProfile* packetProfileSlot = nullptr;
|
||||
auto& packetProfile = ShaderRecompiler::ThreadOwned(packetProfileSlot);
|
||||
++packetProfile.submissions;
|
||||
auto& packetCounters = PacketCounters::ThisThread();
|
||||
|
||||
static const bool epochEach = std::getenv("APS5_SUBMISSION_EPOCH_EACH") != nullptr;
|
||||
if (epochEach || submission.enqueuedAt == std::chrono::steady_clock::time_point{} || submission.enqueuedAt > lastEpochBump()) {
|
||||
@@ -273,7 +312,7 @@ void Driver::execute(const Submission& submission) {
|
||||
Pm4::Execute(packet, queue);
|
||||
if (const auto label = Pm4::DecodeLabelWrite(packet)) noteLabelStore(label->address, label->Bytes(), ++eventSerial, submission.queue);
|
||||
}
|
||||
if (endOfPipeInterrupt && !interruptDeferred) AgcDriverDeliverEopInterrupt(submission.queue);
|
||||
if (endOfPipeInterrupt && !interruptDeferred) deliverGuestEopInterrupt(submission.queue);
|
||||
}
|
||||
if (deviceUse.owns_lock()) deviceUse.unlock();
|
||||
CheckFailure();
|
||||
@@ -289,10 +328,10 @@ void Driver::execute(const Submission& submission) {
|
||||
if (localDevice != nullptr) {
|
||||
localDevice->FlipBatches(batchesAtFlip, unsignaledAtFlip);
|
||||
const auto queueId = submission.queue;
|
||||
flipInterruptDeferred = localDevice->AfterRecordedWork([queueId] { AgcDriverDeliverEopInterrupt(queueId); }, false);
|
||||
flipInterruptDeferred = localDevice->AfterRecordedWork([this, queueId] { deliverFlipEopInterrupt(queueId); }, false);
|
||||
}
|
||||
}
|
||||
if (!flipInterruptDeferred) AgcDriverDeliverEopInterrupt(submission.queue);
|
||||
if (!flipInterruptDeferred) deliverFlipEopInterrupt(submission.queue);
|
||||
++flipsCounted;
|
||||
if (batchesAtFlip != 0) flipSerial = batchesAtFlip;
|
||||
flipBatchesUnsignaled += unsignaledAtFlip;
|
||||
@@ -310,6 +349,7 @@ void Driver::execute(const Submission& submission) {
|
||||
flipHold.Release();
|
||||
submission.flips.at(cursor)->GpuReady(frame);
|
||||
} else if (opcode == 0x15 || opcode == 0x16) {
|
||||
packetCounters.CountDispatch();
|
||||
landPendingWaits(submission.queue);
|
||||
currentPacketOffset() = cursor;
|
||||
if (gateOpened()) {
|
||||
@@ -343,6 +383,7 @@ void Driver::execute(const Submission& submission) {
|
||||
}
|
||||
} else if (drawPacket) {
|
||||
landPendingWaits(submission.queue);
|
||||
packetCounters.CountDraw();
|
||||
bool drawn = false;
|
||||
timed(&WorkerProfile::drawMs, [&] {
|
||||
static const bool traceDraws = std::getenv("APS5_TRACE_DRAWS") != nullptr;
|
||||
@@ -393,7 +434,7 @@ void Driver::execute(const Submission& submission) {
|
||||
if (const auto label = Pm4::DecodeLabelWrite(packet)) noteLabelStore(label->address, label->Bytes(), ++eventSerial, submission.queue);
|
||||
}
|
||||
}
|
||||
if (endOfPipeInterrupt && !interruptDeferred) AgcDriverDeliverEopInterrupt(submission.queue);
|
||||
if (endOfPipeInterrupt && !interruptDeferred) deliverGuestEopInterrupt(submission.queue);
|
||||
}
|
||||
if (drawPacket || (sampleDump && wroteOnGpu)) Graphics::Recorder::CountRecordedWork();
|
||||
timing.Mark(header == RenderingWaitPacketHeader ? "rendering_wait" : opcode == 0x3c || opcode == 0x93 ? "memory_wait" : opcode == 0x15 || opcode == 0x16 ? "dispatch" : drawPacket ? "draw" : "execute");
|
||||
|
||||
@@ -54,7 +54,7 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue
|
||||
}
|
||||
if (reason == 0 || reason == 5) {
|
||||
const auto queueId = submission.queue;
|
||||
interruptDeferred = localDevice->AfterRecordedWork([queueId] { AgcDriverDeliverEopInterrupt(queueId); }, submission.queue == 0);
|
||||
interruptDeferred = localDevice->AfterRecordedWork([this, queueId] { deliverGuestEopInterrupt(queueId); }, submission.queue == 0);
|
||||
if (interruptDeferred && workOpen) localDevice->SubmitRecorded(submission.queue == 0);
|
||||
wroteOnGpu = true;
|
||||
} else if (reason == 1) {
|
||||
@@ -272,6 +272,21 @@ bool Driver::enqueueDmaPacket(std::span<const std::uint32_t> packet, std::uint32
|
||||
if (!enabled || opcode != 0x50 || queue != 0 || packet.size() < 7 || !DrawPipeline::Active() || !deferredLabels().labels.empty()) return false;
|
||||
const auto source = ((packet[1] >> 29u) & 3u) | ((packet[6] >> 24u) & 4u) | ((packet[6] >> 25u) & 8u);
|
||||
constexpr std::size_t immediateLimit = std::size_t{16} << 20u;
|
||||
constexpr std::size_t gpuStoreLimit = 65536;
|
||||
if (source == 2 && Pm4::DmaBytes(packet) > gpuStoreLimit && Pm4::DmaDestination(packet) != Pm4::DmaSelectGds) {
|
||||
const auto fillAddress = packet[4] | (static_cast<std::uint64_t>(packet[5]) << 32u);
|
||||
const auto fillBytes = Pm4::DmaBytes(packet);
|
||||
const auto pattern = packet[2];
|
||||
if (fillAddress % 16 != 0 || fillBytes % 16 != 0 || !GuestMemory::Accessible(reinterpret_cast<const void*>(fillAddress), fillBytes, true) || !device.Load()) return false;
|
||||
DrawPipeline::Queue0().Enqueue([this, queue, fillAddress, fillBytes, pattern] {
|
||||
GuestMemory::SetCurrentPacket(0x50, queue);
|
||||
if (const auto localDevice = device.Load()) {
|
||||
storeBufferFill(queue, fillAddress, fillBytes, {pattern, pattern, pattern, pattern}, localDevice, false, false);
|
||||
++dmaFills;
|
||||
}
|
||||
}, {{fillAddress, fillAddress + fillBytes}});
|
||||
return true;
|
||||
}
|
||||
if (source == 2) {
|
||||
const auto store = Pm4::ResolveStore(packet, state, immediateLimit);
|
||||
if (!store.has_value() || store->Bytes().empty() || !GuestMemory::Accessible(reinterpret_cast<const void*>(store->address), store->Bytes().size(), true)) return false;
|
||||
@@ -305,6 +320,7 @@ bool Driver::enqueueDmaPacket(std::span<const std::uint32_t> packet, std::uint32
|
||||
recordDeferredLabels(localDevice.get(), queue);
|
||||
const auto outcome = localDevice->CopyBuffer(copy.destination, copy.source, copy.bytes, 0, std::numeric_limits<std::size_t>::max(), 0, 0, queue, [](std::span<const std::byte>, std::uint64_t) {});
|
||||
if (outcome.path == 1 || outcome.path == 3) return;
|
||||
if (outcome.path == 2 && !Graphics::RegisteredReadableCovers(copy.destination, copy.bytes) && !localDevice->StoresPendingOver(copy.destination, copy.bytes)) ++copiesToHostMemory;
|
||||
}
|
||||
}
|
||||
std::vector<std::byte> bytes(copy.bytes);
|
||||
@@ -397,10 +413,10 @@ void Driver::commitLabel(std::uint32_t queue, std::uint64_t address, std::span<c
|
||||
if (endOfPipeInterrupt) {
|
||||
bool deferred = false;
|
||||
if (localDevice != nullptr && (reason == 0 || reason == 5 || reason == 6)) {
|
||||
deferred = localDevice->AfterRecordedWork([queue] { AgcDriverDeliverEopInterrupt(queue); }, queue == 0);
|
||||
deferred = localDevice->AfterRecordedWork([this, queue] { deliverGuestEopInterrupt(queue); }, queue == 0);
|
||||
if (deferred && workOpen) localDevice->SubmitRecorded(queue == 0);
|
||||
}
|
||||
if (!deferred) AgcDriverDeliverEopInterrupt(queue);
|
||||
if (!deferred) deliverGuestEopInterrupt(queue);
|
||||
}
|
||||
submitDueAfterCommit(localDevice.get());
|
||||
}
|
||||
|
||||
@@ -652,7 +652,7 @@ std::vector<ShaderRecompiler::TableCallee> ReadTableCallees(const ShaderRecompil
|
||||
constexpr std::uint64_t limitBytes = 64u << 20u;
|
||||
constexpr std::size_t limitCallees = 64u;
|
||||
const auto slot = call.userDataRegister - request.context.userDataBaseRegister;
|
||||
const auto pointer = static_cast<std::uint64_t>(request.context.userData[slot]) | static_cast<std::uint64_t>(request.context.userData[slot + 1u]) << 32u;
|
||||
const auto pointer = call.constantPointer ? *call.constantPointer : static_cast<std::uint64_t>(request.context.userData[slot]) | static_cast<std::uint64_t>(request.context.userData[slot + 1u]) << 32u;
|
||||
const auto words = ReadGuestBytes(pointer + call.descriptorOffset, 16u, "call table descriptor");
|
||||
ShaderRecompiler::ShaderBufferResource descriptor{};
|
||||
std::memcpy(descriptor.fields.data(), words.data(), words.size());
|
||||
@@ -901,7 +901,13 @@ std::unique_ptr<RegisteredPreparation> PlanRegistered(const ShaderSnapshot& snap
|
||||
} else {
|
||||
const auto routing = RegisterValue(state.context, 0x2d5);
|
||||
wave = (routing & 0x00400000u) != 0 ? 32u : 64u;
|
||||
if (registration && stage == Stage::Vertex && (routing & 0x02002024u) == 0x2000u && ProgramUsesWorkgroup(snapshot, codeOffset)) return nullptr;
|
||||
const bool unroutedBarrier = routing == 0 && std::ranges::any_of(decoded.instructions, [](const auto& instruction) {
|
||||
return instruction.op == ShaderRecompiler::RdnaOpcode::SBarrier;
|
||||
});
|
||||
if (registration && stage == Stage::Vertex && (((routing & 0x02002024u) == 0x2000u && ProgramUsesWorkgroup(snapshot, codeOffset)) || unroutedBarrier)) {
|
||||
if (unroutedBarrier && deferred != nullptr) return defer("its s_barrier depends on draw routing");
|
||||
return nullptr;
|
||||
}
|
||||
if ((routing & 0x20u) != 0 || stage == Stage::Mesh || (routing & 4u) != 0 || ((routing >> 3u) & 3u) == 1u) {
|
||||
if (registration) return {};
|
||||
auto stageState = state;
|
||||
|
||||
@@ -0,0 +1,791 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Presentation.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/PqToSdr.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlayFrag_spv.h"
|
||||
#include "prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlayVert_spv.h"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
|
||||
#include "prx/libc/include/HostThreadLocal.hpp"
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <initializer_list>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
#ifdef _WIN32
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#include <psapi.h>
|
||||
#endif
|
||||
|
||||
namespace AgcDriver {
|
||||
namespace {
|
||||
|
||||
static_assert(sizeof(OverlayRect) == 20, "overlay rectangles are read as one ivec4 and one uint per instance");
|
||||
|
||||
struct OverlayParameters {
|
||||
std::array<std::array<float, 4>, OverlayColorCount> palette;
|
||||
std::array<float, 2> extent;
|
||||
};
|
||||
|
||||
std::atomic<std::uint64_t> toggleCount{0};
|
||||
std::atomic<std::uint64_t> gpuTotal{0};
|
||||
std::atomic<std::uint64_t> driverTotal{0};
|
||||
std::atomic<std::uint64_t> pipelineTotal{0};
|
||||
|
||||
struct PacketCounterRegistry {
|
||||
std::mutex mutex;
|
||||
std::deque<PacketCounters> counters;
|
||||
std::vector<PacketCounters*> unused;
|
||||
};
|
||||
|
||||
PacketCounterRegistry& packetCounterRegistry() {
|
||||
static auto* registry = new PacketCounterRegistry;
|
||||
return *registry;
|
||||
}
|
||||
|
||||
struct PacketCounterLease {
|
||||
PacketCounters* counters = nullptr;
|
||||
~PacketCounterLease() {
|
||||
if (counters == nullptr) return;
|
||||
auto& registry = packetCounterRegistry();
|
||||
std::lock_guard lock(registry.mutex);
|
||||
registry.unused.push_back(counters);
|
||||
}
|
||||
};
|
||||
|
||||
constexpr double GigabyteBytes = 1024.0 * 1024.0 * 1024.0;
|
||||
|
||||
const OverlaySettings& settings() {
|
||||
static const OverlaySettings parsed = ParseOverlaySettings(std::getenv("ANYPS5_OVERLAY"), std::getenv("ANYPS5_OVERLAY_POS"));
|
||||
return parsed;
|
||||
}
|
||||
|
||||
std::int32_t panelWidth(std::int32_t scale) {
|
||||
return (OverlayMetrics::InnerWidth + 2 * OverlayMetrics::Padding) * scale;
|
||||
}
|
||||
|
||||
std::int32_t panelHeight(const OverlayContent& content, std::int32_t scale) {
|
||||
const auto lines = static_cast<std::int32_t>(content.above.size() + content.below.size());
|
||||
const auto glyphGap = OverlayMetrics::LinePitch - OverlayMetrics::GlyphHeight;
|
||||
return (2 * OverlayMetrics::Padding + lines * OverlayMetrics::LinePitch + OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap - glyphGap) * scale;
|
||||
}
|
||||
|
||||
bool fits(VkExtent2D extent, const OverlayContent& content, std::int32_t scale) {
|
||||
const auto margins = 2 * OverlayMetrics::Margin * scale;
|
||||
return panelWidth(scale) + margins <= static_cast<std::int32_t>(extent.width) && panelHeight(content, scale) + margins <= static_cast<std::int32_t>(extent.height);
|
||||
}
|
||||
|
||||
float srgbDecode(float value) {
|
||||
return value <= 0.04045f ? value / 12.92f : std::pow((value + 0.055f) / 1.055f, 2.4f);
|
||||
}
|
||||
|
||||
float pqEncode(float nits) {
|
||||
constexpr double m1 = 2610.0 / 16384.0;
|
||||
constexpr double m2 = 2523.0 / 4096.0 * 128.0;
|
||||
constexpr double c1 = 3424.0 / 4096.0;
|
||||
constexpr double c2 = 2413.0 / 4096.0 * 32.0;
|
||||
constexpr double c3 = 2392.0 / 4096.0 * 32.0;
|
||||
const double power = std::pow(std::clamp(static_cast<double>(nits) / 10000.0, 0.0, 1.0), m1);
|
||||
return static_cast<float>(std::pow((c1 + c2 * power) / (1.0 + c3 * power), m2));
|
||||
}
|
||||
|
||||
bool srgbFormat(VkFormat format) {
|
||||
return format == VK_FORMAT_B8G8R8A8_SRGB || format == VK_FORMAT_R8G8B8A8_SRGB || format == VK_FORMAT_A8B8G8R8_SRGB_PACK32;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
OverlaySettings ParseOverlaySettings(const char* enabled, const char* position) {
|
||||
OverlaySettings parsed;
|
||||
const std::string_view visible = enabled != nullptr ? enabled : "";
|
||||
if (visible == "1") parsed.visible = true;
|
||||
else if (!visible.empty() && visible != "0") throw std::runtime_error("ANYPS5_OVERLAY must be 1 or 0, not '" + std::string(visible) + "'");
|
||||
const std::string_view corner = position != nullptr ? position : "";
|
||||
if (corner.empty() || corner == "tl") parsed.corner = OverlayCorner::TopLeft;
|
||||
else if (corner == "tr") parsed.corner = OverlayCorner::TopRight;
|
||||
else if (corner == "bl") parsed.corner = OverlayCorner::BottomLeft;
|
||||
else if (corner == "br") parsed.corner = OverlayCorner::BottomRight;
|
||||
else throw std::runtime_error("ANYPS5_OVERLAY_POS must be tl, tr, bl or br, not '" + std::string(corner) + "'");
|
||||
return parsed;
|
||||
}
|
||||
|
||||
void PerformanceCounters::AddGpuNanoseconds(std::uint64_t nanoseconds) {
|
||||
gpuTotal.fetch_add(nanoseconds, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void PerformanceCounters::AddDriverNanoseconds(std::uint64_t nanoseconds) {
|
||||
driverTotal.fetch_add(nanoseconds, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void PerformanceCounters::CountPipeline() {
|
||||
pipelineTotal.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
std::uint64_t PerformanceCounters::GpuNanoseconds() {
|
||||
return gpuTotal.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
std::uint64_t PerformanceCounters::DriverNanoseconds() {
|
||||
return driverTotal.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
std::uint64_t PerformanceCounters::Pipelines() {
|
||||
return pipelineTotal.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
PacketCounters& PacketCounters::ThisThread() {
|
||||
auto& lease = HostThreadLocal<PacketCounterLease, PacketCounterLease>();
|
||||
if (lease.counters == nullptr) {
|
||||
auto& registry = packetCounterRegistry();
|
||||
std::lock_guard lock(registry.mutex);
|
||||
if (registry.unused.empty()) {
|
||||
lease.counters = ®istry.counters.emplace_back();
|
||||
} else {
|
||||
lease.counters = registry.unused.back();
|
||||
registry.unused.pop_back();
|
||||
}
|
||||
}
|
||||
return *lease.counters;
|
||||
}
|
||||
|
||||
PacketCounters::Totals PacketCounters::Sum() {
|
||||
auto& registry = packetCounterRegistry();
|
||||
std::lock_guard lock(registry.mutex);
|
||||
Totals totals;
|
||||
for (const auto& counters : registry.counters) {
|
||||
totals.draws += counters.draws.load(std::memory_order_relaxed);
|
||||
totals.dispatches += counters.dispatches.load(std::memory_order_relaxed);
|
||||
}
|
||||
return totals;
|
||||
}
|
||||
|
||||
bool PerformanceCounters::GpuEnabled() {
|
||||
return Graphics::Recorder::GpuTimingEnabled();
|
||||
}
|
||||
|
||||
bool PerformanceCounters::DriverEnabled() {
|
||||
static const bool enabled = std::getenv("APS5_PROFILE_DRAW") != nullptr;
|
||||
return enabled;
|
||||
}
|
||||
|
||||
void OverlayStatistics::AddPresent(Clock::time_point at, std::uint64_t gpuNanoseconds, std::uint64_t driverNanoseconds) {
|
||||
AddPresent(at, OverlayCounters{gpuNanoseconds, driverNanoseconds});
|
||||
}
|
||||
|
||||
void OverlayStatistics::AddPresent(Clock::time_point at, const OverlayCounters& counters) {
|
||||
if (!window.empty()) {
|
||||
Graphics::Require(at >= window.back().at, "presents reached the performance overlay out of order");
|
||||
frameTimes[frameNext] = std::chrono::duration<double, std::milli>(at - window.back().at).count();
|
||||
frameNext = (frameNext + 1) % FrameHistory;
|
||||
frameCount = std::min(frameCount + 1, FrameHistory);
|
||||
previous = window.back();
|
||||
}
|
||||
window.push_back({at, counters});
|
||||
while (at - window.front().at > Window) window.pop_front();
|
||||
}
|
||||
|
||||
void OverlayStatistics::Reset() {
|
||||
window.clear();
|
||||
previous.reset();
|
||||
frameTimes = {};
|
||||
frameCount = 0;
|
||||
frameNext = 0;
|
||||
}
|
||||
|
||||
double OverlayStatistics::Fps() const {
|
||||
if (window.size() < 2) {
|
||||
const auto frameMs = LastFrameMs();
|
||||
return frameMs > 0 ? 1000.0 / frameMs : 0;
|
||||
}
|
||||
const auto seconds = std::chrono::duration<double>(window.back().at - window.front().at).count();
|
||||
return seconds > 0 ? static_cast<double>(window.size() - 1) / seconds : 0;
|
||||
}
|
||||
|
||||
double OverlayStatistics::LastFrameMs() const {
|
||||
return frameCount == 0 ? 0 : frameTimes[(frameNext + FrameHistory - 1) % FrameHistory];
|
||||
}
|
||||
|
||||
std::vector<double> OverlayStatistics::FrameTimes() const {
|
||||
std::vector<double> ordered;
|
||||
ordered.reserve(frameCount);
|
||||
for (std::size_t i = 0; i < frameCount; ++i) ordered.push_back(frameTimes[(frameNext + FrameHistory - frameCount + i) % FrameHistory]);
|
||||
return ordered;
|
||||
}
|
||||
|
||||
OverlayStatistics::Range OverlayStatistics::FrameTimeRange() const {
|
||||
const auto times = FrameTimes();
|
||||
if (times.empty()) return {};
|
||||
Range range{times.front(), 0, times.front()};
|
||||
double sum = 0;
|
||||
for (const auto time : times) {
|
||||
range.minimum = std::min(range.minimum, time);
|
||||
range.maximum = std::max(range.maximum, time);
|
||||
sum += time;
|
||||
}
|
||||
range.average = sum / static_cast<double>(times.size());
|
||||
return range;
|
||||
}
|
||||
|
||||
double OverlayStatistics::perFrame(std::uint64_t OverlayCounters::*counter) const {
|
||||
const bool windowed = window.size() >= 2;
|
||||
if (!windowed && !previous) return 0;
|
||||
const auto first = windowed ? window.front().counters.*counter : previous->counters.*counter;
|
||||
const auto last = window.back().counters.*counter;
|
||||
const auto frames = windowed ? window.size() - 1 : std::size_t{1};
|
||||
return last >= first ? static_cast<double>(last - first) / static_cast<double>(frames) : 0;
|
||||
}
|
||||
|
||||
double OverlayStatistics::GpuMsPerFrame() const {
|
||||
return perFrame(&OverlayCounters::gpuNanoseconds) / 1e6;
|
||||
}
|
||||
|
||||
double OverlayStatistics::DriverMsPerFrame() const {
|
||||
return perFrame(&OverlayCounters::driverNanoseconds) / 1e6;
|
||||
}
|
||||
|
||||
double OverlayStatistics::DrawsPerFrame() const {
|
||||
return perFrame(&OverlayCounters::draws);
|
||||
}
|
||||
|
||||
double OverlayStatistics::DispatchesPerFrame() const {
|
||||
return perFrame(&OverlayCounters::dispatches);
|
||||
}
|
||||
|
||||
std::uint64_t OverlayStatistics::PipelinesInWindow() const {
|
||||
if (window.empty()) return 0;
|
||||
const auto first = window.size() < 2 && previous ? previous->counters.pipelines : window.front().counters.pipelines;
|
||||
const auto last = window.back().counters.pipelines;
|
||||
return last >= first ? last - first : 0;
|
||||
}
|
||||
|
||||
OverlayCounters OverlayStatistics::Latest() const {
|
||||
return window.empty() ? OverlayCounters{} : window.back().counters;
|
||||
}
|
||||
|
||||
const std::array<std::uint8_t, OverlayMetrics::GlyphHeight>& OverlayGlyph(char character) {
|
||||
using Glyph = std::array<std::uint8_t, OverlayMetrics::GlyphHeight>;
|
||||
static constexpr Glyph space{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
static constexpr std::array<Glyph, 10> digits{{
|
||||
{0x0e, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0e},
|
||||
{0x04, 0x0c, 0x04, 0x04, 0x04, 0x04, 0x0e},
|
||||
{0x0e, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1f},
|
||||
{0x1f, 0x02, 0x04, 0x02, 0x01, 0x11, 0x0e},
|
||||
{0x02, 0x06, 0x0a, 0x12, 0x1f, 0x02, 0x02},
|
||||
{0x1f, 0x10, 0x1e, 0x01, 0x01, 0x11, 0x0e},
|
||||
{0x06, 0x08, 0x10, 0x1e, 0x11, 0x11, 0x0e},
|
||||
{0x1f, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08},
|
||||
{0x0e, 0x11, 0x11, 0x0e, 0x11, 0x11, 0x0e},
|
||||
{0x0e, 0x11, 0x11, 0x0f, 0x01, 0x02, 0x0c},
|
||||
}};
|
||||
static constexpr Glyph period{0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x0c};
|
||||
static constexpr Glyph colon{0x00, 0x0c, 0x0c, 0x00, 0x0c, 0x0c, 0x00};
|
||||
static constexpr Glyph slash{0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x00};
|
||||
static constexpr Glyph a{0x0e, 0x11, 0x11, 0x11, 0x1f, 0x11, 0x11};
|
||||
static constexpr Glyph b{0x1e, 0x11, 0x11, 0x1e, 0x11, 0x11, 0x1e};
|
||||
static constexpr Glyph c{0x0e, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0e};
|
||||
static constexpr Glyph d{0x1c, 0x12, 0x11, 0x11, 0x11, 0x12, 0x1c};
|
||||
static constexpr Glyph e{0x1f, 0x10, 0x10, 0x1e, 0x10, 0x10, 0x1f};
|
||||
static constexpr Glyph f{0x1f, 0x10, 0x10, 0x1e, 0x10, 0x10, 0x10};
|
||||
static constexpr Glyph g{0x0e, 0x11, 0x10, 0x17, 0x11, 0x11, 0x0f};
|
||||
static constexpr Glyph h{0x11, 0x11, 0x11, 0x1f, 0x11, 0x11, 0x11};
|
||||
static constexpr Glyph i{0x0e, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0e};
|
||||
static constexpr Glyph k{0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11};
|
||||
static constexpr Glyph l{0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1f};
|
||||
static constexpr Glyph m{0x11, 0x1b, 0x15, 0x15, 0x11, 0x11, 0x11};
|
||||
static constexpr Glyph n{0x11, 0x11, 0x19, 0x15, 0x13, 0x11, 0x11};
|
||||
static constexpr Glyph o{0x0e, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0e};
|
||||
static constexpr Glyph p{0x1e, 0x11, 0x11, 0x1e, 0x10, 0x10, 0x10};
|
||||
static constexpr Glyph r{0x1e, 0x11, 0x11, 0x1e, 0x14, 0x12, 0x11};
|
||||
static constexpr Glyph s{0x0f, 0x10, 0x10, 0x0e, 0x01, 0x01, 0x1e};
|
||||
static constexpr Glyph t{0x1f, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04};
|
||||
static constexpr Glyph u{0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0e};
|
||||
static constexpr Glyph v{0x11, 0x11, 0x11, 0x11, 0x11, 0x0a, 0x04};
|
||||
static constexpr Glyph w{0x11, 0x11, 0x11, 0x15, 0x15, 0x15, 0x0a};
|
||||
static constexpr Glyph x{0x11, 0x11, 0x0a, 0x04, 0x0a, 0x11, 0x11};
|
||||
if (character >= '0' && character <= '9') return digits[static_cast<std::size_t>(character - '0')];
|
||||
switch (character) {
|
||||
case ' ': return space;
|
||||
case '.': return period;
|
||||
case ':': return colon;
|
||||
case '/': return slash;
|
||||
case 'A': return a;
|
||||
case 'B': return b;
|
||||
case 'C': return c;
|
||||
case 'D': return d;
|
||||
case 'E': return e;
|
||||
case 'F': return f;
|
||||
case 'G': return g;
|
||||
case 'H': return h;
|
||||
case 'I': return i;
|
||||
case 'K': return k;
|
||||
case 'L': return l;
|
||||
case 'M': return m;
|
||||
case 'N': return n;
|
||||
case 'O': return o;
|
||||
case 'P': return p;
|
||||
case 'R': return r;
|
||||
case 'S': return s;
|
||||
case 'T': return t;
|
||||
case 'U': return u;
|
||||
case 'V': return v;
|
||||
case 'W': return w;
|
||||
case 'X': return x;
|
||||
default: throw std::runtime_error(std::string("the performance overlay font has no glyph for '") + character + "'");
|
||||
}
|
||||
}
|
||||
|
||||
std::string FormatOverlayValue(double value) {
|
||||
const double clamped = std::isfinite(value) ? std::clamp(value, 0.0, 9999.0) : 0.0;
|
||||
char text[16];
|
||||
std::snprintf(text, sizeof(text), clamped < 99.95 ? "%.1f" : "%.0f", clamped);
|
||||
return text;
|
||||
}
|
||||
|
||||
std::string FormatOverlayRate(double value) {
|
||||
if (!std::isfinite(value) || value <= 0 || value >= 0.995) return FormatOverlayValue(value);
|
||||
char text[16];
|
||||
std::snprintf(text, sizeof(text), "%.2f", value);
|
||||
return text;
|
||||
}
|
||||
|
||||
std::string FormatOverlayCount(double value) {
|
||||
const long long rounded = std::isfinite(value) && value > 0 ? std::llround(std::min(value, 1e12)) : 0;
|
||||
char text[24];
|
||||
if (rounded < 10000) std::snprintf(text, sizeof(text), "%lld", rounded);
|
||||
else if (rounded < 1000000) std::snprintf(text, sizeof(text), "%lldK", rounded / 1000);
|
||||
else std::snprintf(text, sizeof(text), "%lldM", std::min<long long>(rounded / 1000000, 999));
|
||||
return text;
|
||||
}
|
||||
|
||||
std::string FormatOverlayGigabytes(std::uint64_t bytes) {
|
||||
return FormatOverlayValue(static_cast<double>(bytes) / GigabyteBytes);
|
||||
}
|
||||
|
||||
OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, bool driver) {
|
||||
return ComposeOverlay(statistics, gpu, driver, OverlayDetails{});
|
||||
}
|
||||
|
||||
OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, bool driver, const OverlayDetails& details) {
|
||||
OverlayContent content;
|
||||
if (gpu || driver) content.above.push_back("PROFILED");
|
||||
content.above.push_back("FPS: " + FormatOverlayRate(statistics.Fps()));
|
||||
content.above.push_back("FRAME: " + FormatOverlayValue(statistics.LastFrameMs()) + " MS");
|
||||
content.frameTimes = statistics.FrameTimes();
|
||||
const auto range = statistics.FrameTimeRange();
|
||||
content.below.push_back("MIN/AVG/MAX: " + FormatOverlayValue(range.minimum) + "/" + FormatOverlayValue(range.average) + "/" + FormatOverlayValue(range.maximum));
|
||||
if (gpu) content.below.push_back("GPU: " + FormatOverlayValue(statistics.GpuMsPerFrame()) + " MS");
|
||||
if (driver) content.below.push_back("DRIVER: " + FormatOverlayValue(statistics.DriverMsPerFrame()) + " MS");
|
||||
const auto latest = statistics.Latest();
|
||||
if (latest.draws != 0 || latest.dispatches != 0) content.below.push_back("DRAWS: " + FormatOverlayCount(statistics.DrawsPerFrame()) + " DISPATCH: " + FormatOverlayCount(statistics.DispatchesPerFrame()));
|
||||
if (latest.pipelines != 0) content.below.push_back("SHADERS: " + FormatOverlayCount(static_cast<double>(statistics.PipelinesInWindow())) + " TOTAL: " + FormatOverlayCount(static_cast<double>(latest.pipelines)));
|
||||
if (details.vram) content.below.push_back("VRAM: " + FormatOverlayGigabytes(details.vram->used) + "/" + FormatOverlayGigabytes(details.vram->budget) + " GB");
|
||||
if (details.ram) content.below.push_back("RAM: " + FormatOverlayGigabytes(*details.ram) + " GB");
|
||||
if (details.display.width != 0 && details.display.height != 0 && details.window.width != 0 && details.window.height != 0) {
|
||||
const auto size = [](VkExtent2D extent) { return std::to_string(extent.width) + "X" + std::to_string(extent.height); };
|
||||
for (const std::string_view separator : {" / ", "/"}) {
|
||||
auto line = "RES: " + size(details.display) + std::string(separator) + size(details.window);
|
||||
if (line.size() > static_cast<std::size_t>(OverlayMetrics::Columns)) continue;
|
||||
content.below.push_back(std::move(line));
|
||||
break;
|
||||
}
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
bool OverlaySampleDue(std::optional<OverlayStatistics::Clock::time_point> sampledAt, OverlayStatistics::Clock::time_point now) {
|
||||
return !sampledAt || now - *sampledAt >= std::chrono::seconds(1);
|
||||
}
|
||||
|
||||
std::optional<OverlayMemory> ReadVramBudget(const Graphics::Context& context) {
|
||||
if (context.memoryProperties2 == nullptr || context.physical == VK_NULL_HANDLE) return std::nullopt;
|
||||
const auto heap = Graphics::LargestDeviceLocalHeap(context.memory);
|
||||
if (!heap) return std::nullopt;
|
||||
VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT};
|
||||
VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2, &reported};
|
||||
context.memoryProperties2(context.physical, &properties);
|
||||
if (reported.heapBudget[*heap] == 0) return std::nullopt;
|
||||
return OverlayMemory{reported.heapUsage[*heap], reported.heapBudget[*heap]};
|
||||
}
|
||||
|
||||
std::optional<std::uint64_t> ReadProcessPrivateBytes() {
|
||||
#ifdef _WIN32
|
||||
PROCESS_MEMORY_COUNTERS_EX counters{};
|
||||
counters.cb = sizeof(counters);
|
||||
if (K32GetProcessMemoryInfo(GetCurrentProcess(), reinterpret_cast<PROCESS_MEMORY_COUNTERS*>(&counters), sizeof(counters)) == 0) return std::nullopt;
|
||||
return static_cast<std::uint64_t>(counters.PrivateUsage);
|
||||
#else
|
||||
std::FILE* status = std::fopen("/proc/self/status", "r");
|
||||
if (status == nullptr) return std::nullopt;
|
||||
std::optional<std::uint64_t> anonymousKb;
|
||||
std::uint64_t swapKb = 0;
|
||||
char line[256];
|
||||
while (std::fgets(line, sizeof(line), status) != nullptr) {
|
||||
unsigned long long value = 0;
|
||||
if (std::sscanf(line, "RssAnon: %llu kB", &value) == 1) anonymousKb = value;
|
||||
else if (std::sscanf(line, "VmSwap: %llu kB", &value) == 1) swapKb = value;
|
||||
}
|
||||
std::fclose(status);
|
||||
if (!anonymousKb) return std::nullopt;
|
||||
return (*anonymousKb + swapKb) * 1024u;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::int32_t OverlayScale(VkExtent2D extent, const OverlayContent& content) {
|
||||
auto scale = std::max<std::int32_t>(1, static_cast<std::int32_t>((extent.height + 240) / 480));
|
||||
while (scale > 1 && !fits(extent, content, scale)) --scale;
|
||||
return scale;
|
||||
}
|
||||
|
||||
OverlayLayout LayOutOverlay(const OverlayContent& content, VkExtent2D extent, OverlayCorner corner) {
|
||||
Graphics::Require(extent.width != 0 && extent.height != 0, "the performance overlay needs a destination");
|
||||
Graphics::Require(content.frameTimes.size() <= OverlayStatistics::FrameHistory, "the performance overlay graph holds at most 120 frames");
|
||||
OverlayLayout layout;
|
||||
const auto scale = OverlayScale(extent, content);
|
||||
layout.scale = scale;
|
||||
const auto width = panelWidth(scale);
|
||||
const auto height = panelHeight(content, scale);
|
||||
const auto margin = OverlayMetrics::Margin * scale;
|
||||
const bool right = corner == OverlayCorner::TopRight || corner == OverlayCorner::BottomRight;
|
||||
const bool bottom = corner == OverlayCorner::BottomLeft || corner == OverlayCorner::BottomRight;
|
||||
const auto left = std::max(0, right ? static_cast<std::int32_t>(extent.width) - margin - width : margin);
|
||||
const auto top = std::max(0, bottom ? static_cast<std::int32_t>(extent.height) - margin - height : margin);
|
||||
layout.bounds = {left, top, width, height, static_cast<std::uint32_t>(OverlayColor::Background)};
|
||||
layout.rects.push_back(layout.bounds);
|
||||
const auto text = [&](const std::string& line, std::int32_t row) {
|
||||
Graphics::Require(line.size() <= static_cast<std::size_t>(OverlayMetrics::Columns), "a performance overlay line is wider than the panel");
|
||||
for (std::size_t column = 0; column < line.size(); ++column) {
|
||||
const auto& glyph = OverlayGlyph(line[column]);
|
||||
const auto glyphLeft = left + (OverlayMetrics::Padding + static_cast<std::int32_t>(column) * OverlayMetrics::Advance) * scale;
|
||||
for (std::int32_t y = 0; y < OverlayMetrics::GlyphHeight; ++y) {
|
||||
const auto bits = glyph[static_cast<std::size_t>(y)];
|
||||
std::int32_t x = 0;
|
||||
while (x < OverlayMetrics::GlyphWidth) {
|
||||
if ((bits & (0x10u >> x)) == 0) {
|
||||
++x;
|
||||
continue;
|
||||
}
|
||||
auto end = x;
|
||||
while (end < OverlayMetrics::GlyphWidth && (bits & (0x10u >> end)) != 0) ++end;
|
||||
layout.rects.push_back({glyphLeft + x * scale, top + (row + y) * scale, (end - x) * scale, scale, static_cast<std::uint32_t>(OverlayColor::Text)});
|
||||
x = end;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
auto row = OverlayMetrics::Padding;
|
||||
for (const auto& line : content.above) {
|
||||
text(line, row);
|
||||
row += OverlayMetrics::LinePitch;
|
||||
}
|
||||
const auto graphBottom = top + (row + OverlayMetrics::GraphHeight) * scale;
|
||||
if (!content.frameTimes.empty()) {
|
||||
double maximum = 0;
|
||||
double sum = 0;
|
||||
for (const auto time : content.frameTimes) {
|
||||
maximum = std::max(maximum, time);
|
||||
sum += time;
|
||||
}
|
||||
const double fullScale = std::max(maximum, 2.0 * sum / static_cast<double>(content.frameTimes.size()));
|
||||
const auto first = static_cast<std::int32_t>(OverlayStatistics::FrameHistory - content.frameTimes.size());
|
||||
for (std::size_t i = 0; i < content.frameTimes.size() && fullScale > 0; ++i) {
|
||||
const auto pixels = std::clamp(static_cast<std::int32_t>(std::lround(content.frameTimes[i] / fullScale * OverlayMetrics::GraphHeight * scale)), 1, OverlayMetrics::GraphHeight * scale);
|
||||
layout.rects.push_back({left + (OverlayMetrics::Padding + first + static_cast<std::int32_t>(i)) * scale, graphBottom - pixels, scale, pixels, static_cast<std::uint32_t>(OverlayColor::Graph)});
|
||||
}
|
||||
}
|
||||
row += OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap;
|
||||
for (const auto& line : content.below) {
|
||||
text(line, row);
|
||||
row += OverlayMetrics::LinePitch;
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
|
||||
std::array<std::array<float, 4>, OverlayColorCount> OverlayPalette(VkFormat format, VkColorSpaceKHR colorSpace) {
|
||||
std::array<std::array<float, 4>, OverlayColorCount> palette{{{0.0f, 0.0f, 0.0f, 0.6f}, {1.0f, 1.0f, 1.0f, 1.0f}, {0.35f, 0.8f, 0.35f, 1.0f}}};
|
||||
if (colorSpace == VK_COLOR_SPACE_HDR10_ST2084_EXT) {
|
||||
for (auto& color : palette) {
|
||||
const float red = srgbDecode(color[0]);
|
||||
const float green = srgbDecode(color[1]);
|
||||
const float blue = srgbDecode(color[2]);
|
||||
const std::array<float, 3> bt2020{
|
||||
0.6274f * red + 0.3293f * green + 0.0433f * blue,
|
||||
0.0691f * red + 0.9195f * green + 0.0114f * blue,
|
||||
0.0164f * red + 0.0880f * green + 0.8956f * blue};
|
||||
for (std::size_t channel = 0; channel < 3; ++channel) color[channel] = pqEncode(bt2020[channel] * PqReferenceWhiteNits);
|
||||
}
|
||||
} else if (srgbFormat(format)) {
|
||||
for (auto& color : palette) {
|
||||
for (std::size_t channel = 0; channel < 3; ++channel) color[channel] = srgbDecode(color[channel]);
|
||||
}
|
||||
}
|
||||
return palette;
|
||||
}
|
||||
|
||||
PerformanceOverlay::PerformanceOverlay(const Graphics::Context& context, std::size_t slotCount) : context(context), slots(slotCount) {
|
||||
Graphics::Require(slotCount != 0, "the performance overlay needs at least one slot");
|
||||
try {
|
||||
const VkPushConstantRange push{VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(OverlayParameters)};
|
||||
VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
|
||||
layoutInfo.pushConstantRangeCount = 1;
|
||||
layoutInfo.pPushConstantRanges = &push;
|
||||
Graphics::Check(context.Function<PFN_vkCreatePipelineLayout>("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &pipelineLayout), "vkCreatePipelineLayout performance overlay");
|
||||
const auto createModule = context.Function<PFN_vkCreateShaderModule>("vkCreateShaderModule");
|
||||
VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
||||
moduleInfo.codeSize = sizeof(PERFORMANCE_OVERLAY_VERT_SPV);
|
||||
moduleInfo.pCode = PERFORMANCE_OVERLAY_VERT_SPV;
|
||||
Graphics::Check(createModule(context.device, &moduleInfo, nullptr, &vertex), "vkCreateShaderModule performance overlay vertex");
|
||||
moduleInfo.codeSize = sizeof(PERFORMANCE_OVERLAY_FRAG_SPV);
|
||||
moduleInfo.pCode = PERFORMANCE_OVERLAY_FRAG_SPV;
|
||||
Graphics::Check(createModule(context.device, &moduleInfo, nullptr, &fragment), "vkCreateShaderModule performance overlay fragment");
|
||||
} catch (...) {
|
||||
release();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
PerformanceOverlay::~PerformanceOverlay() {
|
||||
release();
|
||||
}
|
||||
|
||||
void PerformanceOverlay::releaseTransient(Slot& slot) noexcept {
|
||||
for (const auto& [view, framebuffer] : slot.transient) {
|
||||
if (framebuffer) context.Function<PFN_vkDestroyFramebuffer>("vkDestroyFramebuffer")(context.device, framebuffer, nullptr);
|
||||
if (view) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, view, nullptr);
|
||||
}
|
||||
slot.transient.clear();
|
||||
}
|
||||
|
||||
void PerformanceOverlay::Release(std::size_t index) noexcept {
|
||||
if (index < slots.size()) releaseTransient(slots[index]);
|
||||
}
|
||||
|
||||
void PerformanceOverlay::release() noexcept {
|
||||
for (auto& slot : slots) {
|
||||
releaseTransient(slot);
|
||||
slot.rects.reset();
|
||||
slot.capacity = 0;
|
||||
}
|
||||
for (const auto& [format, entry] : pipelines) context.Function<PFN_vkDestroyPipeline>("vkDestroyPipeline")(context.device, entry, nullptr);
|
||||
pipelines.clear();
|
||||
for (const auto& [key, entry] : renderPasses) context.Function<PFN_vkDestroyRenderPass>("vkDestroyRenderPass")(context.device, entry, nullptr);
|
||||
renderPasses.clear();
|
||||
if (fragment) context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, fragment, nullptr);
|
||||
if (vertex) context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, vertex, nullptr);
|
||||
if (pipelineLayout) context.Function<PFN_vkDestroyPipelineLayout>("vkDestroyPipelineLayout")(context.device, pipelineLayout, nullptr);
|
||||
fragment = vertex = VK_NULL_HANDLE;
|
||||
pipelineLayout = VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
bool PerformanceOverlay::Visible() {
|
||||
return settings().visible != ((toggleCount.load(std::memory_order_relaxed) & 1u) != 0);
|
||||
}
|
||||
|
||||
void PerformanceOverlay::Toggle() {
|
||||
toggleCount.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
OverlayCorner PerformanceOverlay::Corner() {
|
||||
return settings().corner;
|
||||
}
|
||||
|
||||
void PerformanceOverlay::NotePresent(OverlayStatistics::Clock::time_point at, VkExtent2D display) {
|
||||
restartIfToggled();
|
||||
if (OverlaySampleDue(sampledAt, at)) {
|
||||
details.vram = ReadVramBudget(context);
|
||||
details.ram = ReadProcessPrivateBytes();
|
||||
sampledAt = at;
|
||||
}
|
||||
details.display = display;
|
||||
const auto packets = PacketCounters::Sum();
|
||||
statistics.AddPresent(at, OverlayCounters{PerformanceCounters::GpuEnabled() ? PerformanceCounters::GpuNanoseconds() : 0, PerformanceCounters::DriverEnabled() ? PerformanceCounters::DriverNanoseconds() : 0, packets.draws, packets.dispatches, PerformanceCounters::Pipelines()});
|
||||
}
|
||||
|
||||
void PerformanceOverlay::restartIfToggled() {
|
||||
const auto current = toggleCount.load(std::memory_order_relaxed);
|
||||
if (current == toggles) return;
|
||||
statistics.Reset();
|
||||
details = {};
|
||||
sampledAt.reset();
|
||||
toggles = current;
|
||||
}
|
||||
|
||||
VkRenderPass PerformanceOverlay::renderPass(VkFormat format, VkImageLayout initialLayout, VkImageLayout finalLayout) {
|
||||
const auto key = std::tuple{format, initialLayout, finalLayout};
|
||||
if (const auto found = renderPasses.find(key); found != renderPasses.end()) return found->second;
|
||||
VkFormatProperties properties{};
|
||||
context.formatProperties(context.physical, format, &properties);
|
||||
Graphics::Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT) != 0, "the performance overlay cannot blend into this format");
|
||||
VkAttachmentDescription attachment{};
|
||||
attachment.format = format;
|
||||
attachment.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
attachment.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
|
||||
attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||||
attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
||||
attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
||||
attachment.initialLayout = initialLayout;
|
||||
attachment.finalLayout = finalLayout;
|
||||
const VkAttachmentReference reference{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
|
||||
VkSubpassDescription subpass{};
|
||||
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
||||
subpass.colorAttachmentCount = 1;
|
||||
subpass.pColorAttachments = &reference;
|
||||
const bool transfer = finalLayout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
|
||||
const VkSubpassDependency dependencies[2] = {
|
||||
{VK_SUBPASS_EXTERNAL, 0, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, 0},
|
||||
{0, VK_SUBPASS_EXTERNAL, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, transfer ? VK_PIPELINE_STAGE_TRANSFER_BIT : VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, transfer ? VK_ACCESS_TRANSFER_READ_BIT : VkAccessFlags{0}, 0},
|
||||
};
|
||||
VkRenderPassCreateInfo info{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
|
||||
info.attachmentCount = 1;
|
||||
info.pAttachments = &attachment;
|
||||
info.subpassCount = 1;
|
||||
info.pSubpasses = &subpass;
|
||||
info.dependencyCount = 2;
|
||||
info.pDependencies = dependencies;
|
||||
VkRenderPass created = VK_NULL_HANDLE;
|
||||
Graphics::Check(context.Function<PFN_vkCreateRenderPass>("vkCreateRenderPass")(context.device, &info, nullptr, &created), "vkCreateRenderPass performance overlay");
|
||||
return renderPasses.emplace(key, created).first->second;
|
||||
}
|
||||
|
||||
VkPipeline PerformanceOverlay::pipeline(VkFormat format, VkRenderPass compatible) {
|
||||
if (const auto found = pipelines.find(format); found != pipelines.end()) return found->second;
|
||||
VkPipelineShaderStageCreateInfo stages[2]{{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}, {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}};
|
||||
stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
|
||||
stages[0].module = vertex;
|
||||
stages[0].pName = "main";
|
||||
stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
|
||||
stages[1].module = fragment;
|
||||
stages[1].pName = "main";
|
||||
const VkVertexInputBindingDescription binding{0, sizeof(OverlayRect), VK_VERTEX_INPUT_RATE_INSTANCE};
|
||||
const VkVertexInputAttributeDescription attributes[2] = {{0, 0, VK_FORMAT_R32G32B32A32_SINT, 0}, {1, 0, VK_FORMAT_R32_UINT, 16}};
|
||||
VkPipelineVertexInputStateCreateInfo vertexInput{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO};
|
||||
vertexInput.vertexBindingDescriptionCount = 1;
|
||||
vertexInput.pVertexBindingDescriptions = &binding;
|
||||
vertexInput.vertexAttributeDescriptionCount = 2;
|
||||
vertexInput.pVertexAttributeDescriptions = attributes;
|
||||
VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};
|
||||
assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP;
|
||||
VkPipelineViewportStateCreateInfo viewport{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
|
||||
viewport.viewportCount = 1;
|
||||
viewport.scissorCount = 1;
|
||||
VkPipelineRasterizationStateCreateInfo rasterization{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
|
||||
rasterization.polygonMode = VK_POLYGON_MODE_FILL;
|
||||
rasterization.cullMode = VK_CULL_MODE_NONE;
|
||||
rasterization.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
|
||||
rasterization.lineWidth = 1.0f;
|
||||
VkPipelineMultisampleStateCreateInfo multisample{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
|
||||
multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
|
||||
VkPipelineColorBlendAttachmentState blendAttachment{};
|
||||
blendAttachment.blendEnable = VK_TRUE;
|
||||
blendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
|
||||
blendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
|
||||
blendAttachment.colorBlendOp = VK_BLEND_OP_ADD;
|
||||
blendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
|
||||
blendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
|
||||
blendAttachment.alphaBlendOp = VK_BLEND_OP_ADD;
|
||||
blendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
|
||||
VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
|
||||
blend.attachmentCount = 1;
|
||||
blend.pAttachments = &blendAttachment;
|
||||
const VkDynamicState dynamicStates[2] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
|
||||
VkPipelineDynamicStateCreateInfo dynamic{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO};
|
||||
dynamic.dynamicStateCount = 2;
|
||||
dynamic.pDynamicStates = dynamicStates;
|
||||
VkGraphicsPipelineCreateInfo info{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
|
||||
info.stageCount = 2;
|
||||
info.pStages = stages;
|
||||
info.pVertexInputState = &vertexInput;
|
||||
info.pInputAssemblyState = &assembly;
|
||||
info.pViewportState = &viewport;
|
||||
info.pRasterizationState = &rasterization;
|
||||
info.pMultisampleState = &multisample;
|
||||
info.pColorBlendState = &blend;
|
||||
info.pDynamicState = &dynamic;
|
||||
info.layout = pipelineLayout;
|
||||
info.renderPass = compatible;
|
||||
VkPipeline created = VK_NULL_HANDLE;
|
||||
Graphics::Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, VK_NULL_HANDLE, 1, &info, nullptr, &created), "vkCreateGraphicsPipelines performance overlay");
|
||||
return pipelines.emplace(format, created).first->second;
|
||||
}
|
||||
|
||||
OverlayRect PerformanceOverlay::Record(VkCommandBuffer commands, std::size_t slot, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout) {
|
||||
auto shown = details;
|
||||
shown.window = extent;
|
||||
return Record(commands, slot, destination, format, colorSpace, extent, initialLayout, finalLayout, ComposeOverlay(statistics, PerformanceCounters::GpuEnabled(), PerformanceCounters::DriverEnabled(), shown), Corner());
|
||||
}
|
||||
|
||||
OverlayRect PerformanceOverlay::Record(VkCommandBuffer commands, std::size_t index, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout, const OverlayContent& content, OverlayCorner corner) {
|
||||
Graphics::Require(index < slots.size(), "performance overlay slot is out of range");
|
||||
Graphics::Require(destination != VK_NULL_HANDLE && extent.width != 0 && extent.height != 0, "the performance overlay destination is unavailable");
|
||||
auto& slot = slots[index];
|
||||
releaseTransient(slot);
|
||||
const auto layout = LayOutOverlay(content, extent, corner);
|
||||
if (slot.capacity < layout.rects.size()) {
|
||||
const auto capacity = std::max<std::size_t>(layout.rects.size(), 1024);
|
||||
slot.rects = std::make_unique<Graphics::Buffer>(context, capacity * sizeof(OverlayRect), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
|
||||
slot.capacity = capacity;
|
||||
}
|
||||
std::memcpy(slot.rects->Bytes().data(), layout.rects.data(), layout.rects.size() * sizeof(OverlayRect));
|
||||
const auto pass = renderPass(format, initialLayout, finalLayout);
|
||||
const auto drawing = pipeline(format, pass);
|
||||
VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
|
||||
viewInfo.image = destination;
|
||||
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
viewInfo.format = format;
|
||||
viewInfo.components = {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY};
|
||||
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
VkImageViewUsageCreateInfo usageInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO};
|
||||
usageInfo.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
|
||||
for (const auto layout : {initialLayout, finalLayout}) {
|
||||
if (layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) usageInfo.usage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
||||
if (layout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) usageInfo.usage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
|
||||
}
|
||||
viewInfo.pNext = &usageInfo;
|
||||
VkImageView view = VK_NULL_HANDLE;
|
||||
Graphics::Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView performance overlay");
|
||||
VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
|
||||
framebufferInfo.renderPass = pass;
|
||||
framebufferInfo.attachmentCount = 1;
|
||||
framebufferInfo.pAttachments = &view;
|
||||
framebufferInfo.width = extent.width;
|
||||
framebufferInfo.height = extent.height;
|
||||
framebufferInfo.layers = 1;
|
||||
VkFramebuffer framebuffer = VK_NULL_HANDLE;
|
||||
const auto created = context.Function<PFN_vkCreateFramebuffer>("vkCreateFramebuffer")(context.device, &framebufferInfo, nullptr, &framebuffer);
|
||||
slot.transient.emplace_back(view, framebuffer);
|
||||
Graphics::Check(created, "vkCreateFramebuffer performance overlay");
|
||||
const auto right = std::min<std::int32_t>(layout.bounds.x + layout.bounds.width, static_cast<std::int32_t>(extent.width));
|
||||
const auto bottom = std::min<std::int32_t>(layout.bounds.y + layout.bounds.height, static_cast<std::int32_t>(extent.height));
|
||||
const VkRect2D area{{layout.bounds.x, layout.bounds.y}, {static_cast<std::uint32_t>(std::max(1, right - layout.bounds.x)), static_cast<std::uint32_t>(std::max(1, bottom - layout.bounds.y))}};
|
||||
VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
|
||||
begin.renderPass = pass;
|
||||
begin.framebuffer = framebuffer;
|
||||
begin.renderArea = area;
|
||||
context.Function<PFN_vkCmdBeginRenderPass>("vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE);
|
||||
context.Function<PFN_vkCmdBindPipeline>("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, drawing);
|
||||
const VkViewport viewport{0.0f, 0.0f, static_cast<float>(extent.width), static_cast<float>(extent.height), 0.0f, 1.0f};
|
||||
context.Function<PFN_vkCmdSetViewport>("vkCmdSetViewport")(commands, 0, 1, &viewport);
|
||||
context.Function<PFN_vkCmdSetScissor>("vkCmdSetScissor")(commands, 0, 1, &area);
|
||||
const OverlayParameters parameters{OverlayPalette(format, colorSpace), {static_cast<float>(extent.width), static_cast<float>(extent.height)}};
|
||||
context.Function<PFN_vkCmdPushConstants>("vkCmdPushConstants")(commands, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(parameters), ¶meters);
|
||||
const VkBuffer buffer = slot.rects->Handle();
|
||||
const VkDeviceSize offset = 0;
|
||||
context.Function<PFN_vkCmdBindVertexBuffers>("vkCmdBindVertexBuffers")(commands, 0, 1, &buffer, &offset);
|
||||
context.Function<PFN_vkCmdDraw>("vkCmdDraw")(commands, 4, static_cast<std::uint32_t>(layout.rects.size()), 0, 0);
|
||||
context.Function<PFN_vkCmdEndRenderPass>("vkCmdEndRenderPass")(commands);
|
||||
return layout.bounds;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" void AgcDriverTogglePerformanceOverlay_nid_postfix() {
|
||||
AgcDriver::PerformanceOverlay::Toggle();
|
||||
}
|
||||
@@ -378,9 +378,11 @@ void Validate(std::span<const std::uint32_t> packet, std::uint32_t queue) {
|
||||
require((packet[1] & 0x7fffffffu) == 0 && (packet[2] & 0x7fffffffu) == 0, "CONTEXT_CONTROL register loading and shadowing are not implemented");
|
||||
break;
|
||||
case 0x46: {
|
||||
require((packet[1] & ~0x73fu) == 0, "unsupported EVENT_WRITE flags or reserved bits");
|
||||
const auto eventType = packet[1] & 0x3fu;
|
||||
const auto eventIndex = (packet[1] >> 8u) & 7u;
|
||||
const auto eventIndex = (packet[1] >> 8u) & 0xfu;
|
||||
const auto streamOutStatistics = (eventType >= 0x01u && eventType <= 0x03u) || eventType == 0x20u;
|
||||
require(!streamOutStatistics || (eventIndex != 3 && (eventIndex < 8 || eventIndex > 11)), "stream-out statistics sampling to memory is not implemented");
|
||||
require((packet[1] & ~0x73fu) == 0, "unsupported EVENT_WRITE flags or reserved bits");
|
||||
switch (eventType) {
|
||||
case 0x07: case 0x0f: case 0x10:
|
||||
size(2);
|
||||
@@ -401,6 +403,11 @@ void Validate(std::span<const std::uint32_t> packet, std::uint32_t queue) {
|
||||
size(2);
|
||||
require(eventIndex == 0, "invalid VGT_FLUSH event index");
|
||||
break;
|
||||
case 0x01: case 0x02: case 0x03: case 0x20:
|
||||
graphics();
|
||||
size(2);
|
||||
require(eventIndex == 0, "invalid stream-out statistics event index");
|
||||
break;
|
||||
case 0x39:
|
||||
graphics();
|
||||
size(4);
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/SubgroupClock.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/ShaderDeviceProfile.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/PresentMode.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/PresentationPass.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
|
||||
@@ -118,6 +119,7 @@ void CreateComputePipeline(VkDevice device, PFN_vkCreateComputePipelines create,
|
||||
info.layout = layout;
|
||||
auto cacheLock = synchronization != nullptr ? synchronization->LockForPipelineUse() : Graphics::PipelineCacheSynchronization::PipelineUseLock{};
|
||||
check(create(device, cache, 1, &info, nullptr, pipeline), "vkCreateComputePipelines");
|
||||
PerformanceCounters::CountPipeline();
|
||||
}
|
||||
|
||||
bool CaptureComputePipeline(std::size_t words) {
|
||||
@@ -324,6 +326,7 @@ struct VulkanDevice::State {
|
||||
bool pipelineExecutableInfo = false;
|
||||
bool maintenance8 = false;
|
||||
std::uint32_t srgbDecodeFormats = 0;
|
||||
std::uint32_t srgbCopyFormats = 0;
|
||||
bool storageImageWithoutFormat = false;
|
||||
VkSampleCountFlags sampleLocationSampleCounts = 0;
|
||||
VkSampleCountFlags sampleLocationGridSampleCounts = 0;
|
||||
@@ -407,6 +410,7 @@ struct VulkanDevice::State {
|
||||
VkPhysicalDeviceMeshShaderPropertiesEXT meshLimits{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_PROPERTIES_EXT};
|
||||
std::unique_ptr<PresentationScaler> scaler;
|
||||
std::unique_ptr<PresentationPass> presentation;
|
||||
std::unique_ptr<PerformanceOverlay> overlay;
|
||||
VkFormat surfaceFormat = VK_FORMAT_B8G8R8A8_UNORM;
|
||||
VkColorSpaceKHR surfaceColorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR;
|
||||
bool swapchainColorSpaces = false;
|
||||
@@ -766,6 +770,7 @@ struct VulkanDevice::State {
|
||||
textureCache.reset();
|
||||
detiler.reset();
|
||||
colorTransfer.reset();
|
||||
overlay.reset();
|
||||
presentation.reset();
|
||||
scaler.reset();
|
||||
hdrScaler.reset();
|
||||
@@ -1111,6 +1116,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
|
||||
state->properties = properties.properties;
|
||||
state->physical = selected;
|
||||
state->srgbDecodeFormats = Graphics::SrgbDecodeFormats(state->InstanceFunction<PFN_vkGetPhysicalDeviceFormatProperties>("vkGetPhysicalDeviceFormatProperties"), selected);
|
||||
state->srgbCopyFormats = Graphics::SrgbCopyFormats(state->InstanceFunction<PFN_vkGetPhysicalDeviceFormatProperties>("vkGetPhysicalDeviceFormatProperties"), selected, state->srgbDecodeFormats);
|
||||
if ((state->subgroup.supportedOperations & VK_SUBGROUP_FEATURE_BASIC_BIT) != 0) {
|
||||
state->capabilities.push_back(spv::CapabilityGroupNonUniform);
|
||||
if ((state->subgroup.supportedOperations & VK_SUBGROUP_FEATURE_BALLOT_BIT) != 0) state->capabilities.push_back(spv::CapabilityGroupNonUniformBallot);
|
||||
@@ -2110,6 +2116,7 @@ void VulkanDevice::StoreIdleResults(bool reapFirst) {
|
||||
if (!Graphics::StorageTexture::AnyPending()) return;
|
||||
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush);
|
||||
std::lock_guard gpuLock(GuestMemory::GpuMutex());
|
||||
const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix();
|
||||
Graphics::StorageTexture::FlushAllPending("idle");
|
||||
SubmitRecorded(reapFirst);
|
||||
}
|
||||
@@ -3169,6 +3176,7 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu
|
||||
if (state->presentSlots[state->presentCursor].inFlight) RetirePresents(state->presentSlots.size() - 1);
|
||||
auto& slot = state->presentSlots[state->presentCursor];
|
||||
require(!slot.inFlight, "presentation slot is still in flight");
|
||||
const bool overlay = PerformanceOverlay::Visible();
|
||||
if (display != nullptr && resident == nullptr) {
|
||||
static_cast<void>(DisplayBufferSize(*display));
|
||||
state->colorTransfer->Upload(display->address, width, height, Graphics::ColorTileMode::RenderTarget, DisplayTexelBytes(display->pixelFormat));
|
||||
@@ -3260,7 +3268,7 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu
|
||||
}
|
||||
if (drawn) {
|
||||
state->presentation->Bind(slotIndex, resident->Image(), width, height, display->pixelFormat, whiteNits, residentLayer);
|
||||
state->presentation->RecordPresent(commands, slotIndex, barrier.image, state->surfaceFormat, state->surfaceColorSpace, state->extent, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
|
||||
state->presentation->RecordPresent(commands, slotIndex, barrier.image, state->surfaceFormat, state->surfaceColorSpace, state->extent, overlay ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
|
||||
} else {
|
||||
scaler->RecordLetterbox(commands, width, height, barrier.image, state->extent.width, state->extent.height);
|
||||
if (direct) PresentationScaler::RecordBlitFrom(graphicsContext(), commands, resident->Image(), VK_IMAGE_LAYOUT_GENERAL, width, height, residentFilter, barrier.image, state->extent.width, state->extent.height, residentLayer);
|
||||
@@ -3298,7 +3306,11 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu
|
||||
Graphics::Recorder::CountBarriers(CommandClass::PresentBlit);
|
||||
}
|
||||
}
|
||||
if (!drawn) {
|
||||
if (overlay) {
|
||||
if (!state->overlay) state->overlay = std::make_unique<PerformanceOverlay>(graphicsContext(), state->presentSlots.size());
|
||||
state->overlay->NotePresent(std::chrono::steady_clock::now(), clearOnly ? VkExtent2D{} : VkExtent2D{width, height});
|
||||
state->overlay->Record(commands, static_cast<std::size_t>(&slot - state->presentSlots.data()), barrier.image, state->surfaceFormat, state->surfaceColorSpace, state->extent, drawn ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
|
||||
} else if (!drawn) {
|
||||
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
barrier.dstAccessMask = 0;
|
||||
barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||
@@ -3508,7 +3520,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const {
|
||||
target.fixedPushSlots = state->graphicsPipelineLibrary;
|
||||
target.registrationModules = state->graphicsPipelineLibrary;
|
||||
target.robustBufferAccess2 = state->robustBufferAccess2;
|
||||
target.srgbDecodeFormats = state->srgbDecodeFormats;
|
||||
target.srgbDecodeFormats = state->srgbDecodeFormats & ~state->srgbCopyFormats;
|
||||
target.vertexSubgroups = (state->subgroup.supportedStages & VK_SHADER_STAGE_VERTEX_BIT) != 0;
|
||||
if (state->meshShader) {
|
||||
const auto& mesh = state->meshLimits;
|
||||
@@ -3649,6 +3661,7 @@ Graphics::Context VulkanDevice::buildContext() const {
|
||||
context.imageViewMinLod = state->imageViewMinLod;
|
||||
context.pipelineExecutableInfo = state->pipelineExecutableInfo;
|
||||
context.srgbDecodeFormats = state->srgbDecodeFormats;
|
||||
context.srgbCopyFormats = state->srgbCopyFormats;
|
||||
context.sampleLocationSampleCounts = state->sampleLocationSampleCounts;
|
||||
context.sampleLocationGridSampleCounts = state->sampleLocationGridSampleCounts;
|
||||
context.memoryProperties2 = state->memoryBudget ? state->InstanceFunction<PFN_vkGetPhysicalDeviceMemoryProperties2>("vkGetPhysicalDeviceMemoryProperties2") : nullptr;
|
||||
@@ -4556,6 +4569,7 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) {
|
||||
const auto commands = recorder.Commands(&covered);
|
||||
recordStep(PhaseRecordCommands);
|
||||
recorder.Keep(record.objects);
|
||||
if (record.resources != nullptr) record.resources->KeepStorageImageBytes(recorder);
|
||||
recorder.Keep(record.resources);
|
||||
if (record.resources != nullptr) recorder.KeepBytes(record.resources.get(), record.resources->CopiedBytes());
|
||||
if (record.bindings != nullptr) {
|
||||
@@ -4863,7 +4877,10 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
|
||||
static const bool deferEviction = std::getenv("APS5_NO_DEFERRED_EVICTION") == nullptr;
|
||||
std::vector<std::shared_ptr<Graphics::ShaderResources>> evicted;
|
||||
state->resourceCache.Insert(contentKey, resources, deferEviction ? &evicted : nullptr);
|
||||
for (auto& object : evicted) state->recorder->Keep(std::move(object));
|
||||
for (auto& object : evicted) {
|
||||
object->KeepStorageImageBytes(*state->recorder);
|
||||
state->recorder->Keep(std::move(object));
|
||||
}
|
||||
if (profile) {
|
||||
timer.add(PhaseResourcesInsert, std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - insertStart).count());
|
||||
if (arguments != 0) d.indirectHold.phaseCounts[PhaseResourcesInsert] += evicted.size();
|
||||
@@ -4961,6 +4978,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
|
||||
state->resourceCache.Remove(contentKey, cached.get());
|
||||
++d.cacheInvalidated;
|
||||
// Off the mutex with its batch, as an evicted entry (insert()).
|
||||
cached->KeepStorageImageBytes(*state->recorder);
|
||||
state->recorder->Keep(std::move(cached));
|
||||
}
|
||||
if (profile) {
|
||||
@@ -5139,6 +5157,7 @@ RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResu
|
||||
// ordinary path runs with the prepared find (C10: the template goes to the batch).
|
||||
if (verify->cached.get() != hit->resources.get()) {
|
||||
counters.verifyReplaced.fetch_add(1, std::memory_order_relaxed);
|
||||
hit->resources->KeepStorageImageBytes(*state->recorder);
|
||||
state->recorder->Keep(std::move(hit->resources));
|
||||
return RecipeOutcome::Rebuild;
|
||||
}
|
||||
@@ -5175,6 +5194,7 @@ RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResu
|
||||
state->resourceCache.Remove(recipe.key, hit->resources.get());
|
||||
++d.cacheInvalidated;
|
||||
// Off the mutex with its batch, as an evicted entry.
|
||||
hit->resources->KeepStorageImageBytes(*state->recorder);
|
||||
state->recorder->Keep(std::move(hit->resources));
|
||||
counters.rebuilds[static_cast<std::size_t>(report.failure)].fetch_add(1, std::memory_order_relaxed);
|
||||
return RecipeOutcome::Rebuild;
|
||||
|
||||
@@ -229,6 +229,7 @@ struct Context {
|
||||
bool graphicsPipelineLibrary = false;
|
||||
bool pipelineLinkTimeOptimization = true;
|
||||
std::uint32_t srgbDecodeFormats = 0;
|
||||
std::uint32_t srgbCopyFormats = 0;
|
||||
bool provokingVertexLast = false;
|
||||
bool provokingVertexModePerPipeline = false;
|
||||
bool singlePassStorage = false;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "RuntimeAbi.hpp"
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace AgcDriver::Graphics {
|
||||
|
||||
@@ -51,6 +52,12 @@ constexpr PlaceholderImageShape PlaceholderImageShapeOf(std::uint32_t heap) {
|
||||
samples[dimension]};
|
||||
}
|
||||
|
||||
constexpr std::optional<PlaceholderImageShape> PlaceholderImageShapeForDevice(std::uint32_t heap, bool storageImageMultisample) {
|
||||
const auto shape = PlaceholderImageShapeOf(heap);
|
||||
if (!storageImageMultisample && (shape.usage & VK_IMAGE_USAGE_STORAGE_BIT) != 0 && shape.samples != VK_SAMPLE_COUNT_1_BIT) return std::nullopt;
|
||||
return shape;
|
||||
}
|
||||
|
||||
class PlaceholderImages {
|
||||
public:
|
||||
explicit PlaceholderImages(const Context& context);
|
||||
|
||||
@@ -102,10 +102,17 @@ public:
|
||||
static constexpr std::size_t UnwrittenSnapshotEntries = 4096;
|
||||
static constexpr std::size_t DrawInputBudget = std::size_t{1024} << 20u;
|
||||
static constexpr std::size_t DrawInputEntries = 16384;
|
||||
std::shared_ptr<Buffer> UnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array<std::uint64_t, 3>& stamp);
|
||||
void NoteUnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array<std::uint64_t, 3>& stamp, std::shared_ptr<Buffer> buffer);
|
||||
std::shared_ptr<Buffer> UnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array<std::uint64_t, 3>& stamp, VkDeviceSize* offset = nullptr);
|
||||
void NoteUnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array<std::uint64_t, 3>& stamp, std::shared_ptr<Buffer> buffer, VkDeviceSize offset = 0);
|
||||
std::shared_ptr<Buffer> ReusableDrawSnapshot(std::uint64_t address, std::size_t bytes, SnapshotUse use = SnapshotUse::Storage, std::uint32_t* derived = nullptr, std::uint64_t generation = 0);
|
||||
void KeepDrawSnapshot(std::uint64_t address, std::size_t bytes, std::uint64_t generation, std::uint64_t registryGeneration, std::shared_ptr<Buffer> buffer, SnapshotUse use = SnapshotUse::Storage, std::uint32_t derived = 0);
|
||||
static constexpr std::size_t DrawUploadChunk = std::size_t{1} << 20u;
|
||||
struct UploadSlice {
|
||||
std::shared_ptr<Buffer> buffer;
|
||||
VkDeviceSize offset = 0;
|
||||
std::byte* bytes = nullptr;
|
||||
};
|
||||
UploadSlice DrawUpload(std::size_t bytes);
|
||||
static constexpr std::size_t IndexRangeEntries = 16384;
|
||||
static constexpr std::size_t KeptIndexRangeIndices = 4096;
|
||||
bool KnownHighestIndex(std::uint64_t address, std::size_t bytes, SnapshotUse use, std::optional<std::uint32_t>& highest);
|
||||
@@ -812,6 +819,8 @@ private:
|
||||
std::uint64_t submissions = 0;
|
||||
std::size_t inFlightKeptBytes = 0;
|
||||
std::uint64_t writeNoteCount = 0;
|
||||
std::shared_ptr<Buffer> drawUpload;
|
||||
VkDeviceSize drawUploadUsed = 0;
|
||||
// Command buffers and fences of completed batches, reused by later ones (hundreds of batches per
|
||||
// frame would otherwise allocate and free their objects each time).
|
||||
std::vector<std::pair<VkCommandBuffer, VkFence>> spare;
|
||||
@@ -836,6 +845,7 @@ private:
|
||||
struct UnwrittenSnapshot {
|
||||
std::size_t bytes;
|
||||
std::shared_ptr<Buffer> buffer;
|
||||
VkDeviceSize offset = 0;
|
||||
};
|
||||
std::unordered_map<std::uint64_t, UnwrittenSnapshot> unwrittenSnapshots;
|
||||
std::array<std::uint64_t, 3> unwrittenStamp{};
|
||||
|
||||
@@ -34,6 +34,7 @@ std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory)
|
||||
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);
|
||||
bool SampledBudgetReportDue(std::uint64_t reported, std::uint64_t budget, std::chrono::steady_clock::duration sinceReport);
|
||||
std::optional<std::uint32_t> LargestDeviceLocalHeap(const VkPhysicalDeviceMemoryProperties& memory);
|
||||
std::uint64_t StorageTextureCacheBudget(const Context& context);
|
||||
struct TextureCacheUse {
|
||||
std::size_t sampledEntries = 0;
|
||||
@@ -181,6 +182,9 @@ public:
|
||||
struct Snapshot {
|
||||
std::uint64_t address;
|
||||
std::shared_ptr<Buffer> buffer;
|
||||
VkDeviceSize offset = 0;
|
||||
std::size_t bytes = 0;
|
||||
std::span<std::byte> Bytes() const { return buffer->Bytes().subspan(static_cast<std::size_t>(offset), bytes); }
|
||||
};
|
||||
DescriptorCache* cache = nullptr;
|
||||
VkDescriptorSetLayout layout = VK_NULL_HANDLE;
|
||||
@@ -207,6 +211,7 @@ public:
|
||||
static const char* MoveRefusalName(MoveRefusal refusal);
|
||||
std::optional<std::vector<MovedBuffer>> MovedDispatchBuffers(const CompiledShader& shader, Recorder* recorder, std::size_t maxBytes, MoveProof& proof) const;
|
||||
static std::size_t MaxDrawSnapshotBytes();
|
||||
static std::size_t DrawUploadLimit();
|
||||
void WriteBack();
|
||||
// Deferred completion: MarkGpuWrites registers the results the recorded work leaves on the GPU
|
||||
// (storage images stay there; buffer ranges are noted so CPU reads wait); WriteBackBuffers runs
|
||||
@@ -233,6 +238,7 @@ public:
|
||||
std::vector<std::pair<std::uint64_t, std::uint64_t>> DeviceReads() const { return guestMemory.DeviceReads(); }
|
||||
bool ReadsImage(const StorageTexture* image) const;
|
||||
bool HasCachedStorageImages() const;
|
||||
bool HoldsReleasedTexture() const;
|
||||
std::uint32_t TableElements() const { return tableElements[0] + tableElements[1]; }
|
||||
const std::vector<std::uint32_t>& LayoutKey() const { return layoutKey; }
|
||||
const std::vector<std::uint32_t>& FragmentLayoutKey() const { return fragmentLayoutKey; }
|
||||
@@ -261,6 +267,7 @@ public:
|
||||
// ranges are fixed by the build), so a Revalidate's collects on the same worker are memo hits.
|
||||
void PrecollectSurfaces() const;
|
||||
bool Reusable() const { return reusable; }
|
||||
void KeepStorageImageBytes(Recorder& recorder) const;
|
||||
const std::vector<std::uint64_t>& MultisampleSources() const { return multisampleSources; }
|
||||
static bool NeverReusable(std::span<const CompiledShader> shaders);
|
||||
static std::uint64_t DepthGuardRefusals();
|
||||
@@ -692,6 +699,7 @@ public:
|
||||
private:
|
||||
void erase(const Key& key);
|
||||
void noteMiss(const Key& key);
|
||||
void dropReleasedTextures(std::vector<std::shared_ptr<ShaderResources>>* evicted);
|
||||
void noteEvicted(const Key& key);
|
||||
void noteReturn(const Key& key);
|
||||
void noteUse(const Key& key);
|
||||
|
||||
@@ -66,6 +66,9 @@ public:
|
||||
Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D, std::uint32_t baseLayer = 0, std::shared_ptr<OwnedImage> owner = {});
|
||||
struct DepthCopyTag {};
|
||||
Texture(const Context& context, const std::shared_ptr<StorageTexture>& source, const GuestTextureResource& descriptor, VkComponentMapping components, DepthCopyTag);
|
||||
struct SrgbExpandTag {};
|
||||
Texture(const Context& context, const std::shared_ptr<StorageTexture>& source, const GuestTextureResource& descriptor, VkComponentMapping components, SrgbExpandTag);
|
||||
static bool CanSrgbExpandFrom(const Context& context, const StorageTexture& source, const GuestTextureResource& descriptor);
|
||||
static VkFormat DepthCopyFormat(VkFormat colorFormat);
|
||||
static bool CanDepthCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor);
|
||||
struct BlockCopyTag {};
|
||||
@@ -113,10 +116,13 @@ public:
|
||||
PerUseCopy CopyKind() const { return perUseCopy; }
|
||||
bool RefreshedPerUse() const { return perUseCopy == PerUseCopy::RemadePerUse; }
|
||||
void MarkRefreshedPerUse() { perUseCopy = PerUseCopy::RemadePerUse; }
|
||||
bool Released() const { return released.load(std::memory_order_acquire); }
|
||||
void MarkReleased() { released.store(true, std::memory_order_release); }
|
||||
|
||||
private:
|
||||
void release() noexcept;
|
||||
bool refreshBlockCopy();
|
||||
bool refreshSrgbExpand();
|
||||
void createFirstLayerView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo);
|
||||
void prepareBaseLevelView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo);
|
||||
void prepareSrgbView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo);
|
||||
@@ -146,7 +152,10 @@ private:
|
||||
std::shared_ptr<StorageTexture> storageSource;
|
||||
std::unique_ptr<CommandBatch> upload;
|
||||
PerUseCopy perUseCopy = PerUseCopy::None;
|
||||
std::atomic<bool> released{false};
|
||||
bool copiedOnce = false;
|
||||
bool srgbExpand = false;
|
||||
GuestTextureResource sourceDescriptor{};
|
||||
std::uint64_t copiedVersion = 0;
|
||||
VkFormat depthFormat = VK_FORMAT_UNDEFINED;
|
||||
VkFormat blockFormat = VK_FORMAT_UNDEFINED;
|
||||
@@ -356,6 +365,7 @@ public:
|
||||
static std::uint64_t RefreshesProved();
|
||||
static std::uint64_t UntrackedCompares();
|
||||
std::uint64_t GuestBytes() const;
|
||||
std::uint64_t BudgetBytes() const;
|
||||
std::size_t OriginalSnapshotBytes() const { return original.capacity(); }
|
||||
VkDeviceSize AllocationBytes() const { return memoryBytes; }
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ VkFormat ResolveTextureFormat(std::uint32_t guestFormat);
|
||||
VkComponentSwizzle TextureComponentChannel(std::uint32_t guestFormat, VkComponentSwizzle component);
|
||||
std::uint32_t SrgbDecodeFormats(PFN_vkGetPhysicalDeviceFormatProperties formatProperties, VkPhysicalDevice physical);
|
||||
VkFormat SampledTextureFormat(const Context& context, std::uint32_t guestFormat);
|
||||
std::uint32_t SrgbCopyFormats(PFN_vkGetPhysicalDeviceFormatProperties formatProperties, VkPhysicalDevice physical, std::uint32_t decodeFormats);
|
||||
bool SrgbExpandsToRgba8(const Context& context, std::uint32_t guestFormat);
|
||||
bool IsSrgbTextureFormat(std::uint32_t guestFormat);
|
||||
std::optional<std::uint32_t> FindGuestTextureFormat(VkFormat format, std::uint32_t elementBytes);
|
||||
std::optional<std::uint32_t> FindGuestColorTargetFormat(VkFormat format, std::uint32_t elementBytes);
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
#version 450
|
||||
|
||||
layout(push_constant) uniform Parameters {
|
||||
vec4 palette[3];
|
||||
vec2 extent;
|
||||
} parameters;
|
||||
layout(location = 0) flat in uint paletteIndex;
|
||||
layout(location = 0) out vec4 color;
|
||||
|
||||
void main() {
|
||||
color = paletteIndex == 1u ? parameters.palette[1] : paletteIndex == 2u ? parameters.palette[2] : parameters.palette[0];
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#version 450
|
||||
|
||||
layout(location = 0) in ivec4 area;
|
||||
layout(location = 1) in uint color;
|
||||
layout(push_constant) uniform Parameters {
|
||||
vec4 palette[3];
|
||||
vec2 extent;
|
||||
} parameters;
|
||||
layout(location = 0) flat out uint paletteIndex;
|
||||
|
||||
void main() {
|
||||
vec2 corner = vec2(float(gl_VertexIndex & 1), float((gl_VertexIndex >> 1) & 1));
|
||||
vec2 pixel = vec2(area.xy) + corner * vec2(area.zw);
|
||||
gl_Position = vec4(pixel / parameters.extent * 2.0 - 1.0, 0.0, 1.0);
|
||||
paletteIndex = color;
|
||||
}
|
||||
@@ -194,6 +194,7 @@ BdaResources::BdaResources(const Context& context, const GuestBufferMemory& memo
|
||||
++it;
|
||||
}
|
||||
}
|
||||
Require(ShaderRecompiler::BdaAbi::RangesOrdered(ranges.data(), ranges.size()), "BDA table ranges do not ascend by begin");
|
||||
for (const auto& range : ranges) Require((range.permissions & ShaderRecompiler::BdaAbi::Write) == 0 || range.end <= ShaderRecompiler::BdaAbi::WrittenAddressLimit, "a writable BDA range ends past the addresses the written-page record can hold");
|
||||
table = std::make_shared<Buffer>(context, tableBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
|
||||
const ShaderRecompiler::BdaAbi::Header header{ShaderRecompiler::BdaAbi::Version, static_cast<std::uint32_t>(ranges.size()), sizeof(ShaderRecompiler::BdaAbi::Range), 0};
|
||||
|
||||
@@ -257,6 +257,7 @@ void recordCmaskClear(const Context& context, Recorder& recorder, const HostImpo
|
||||
if (AnyShadowedOverlaps(address, bytes)) PublishShadow(address, bytes, PublishScope::Whole, PublishReason::Keys);
|
||||
recorder.FlushKeyStoresOverlapping(address, bytes);
|
||||
const auto commands = recorder.Commands();
|
||||
if (resident != nullptr && !resident->Cached()) recorder.KeepBytes(resident.get(), static_cast<std::size_t>(resident->BudgetBytes()));
|
||||
recorder.Keep(resident);
|
||||
context.detiler->DispatchCmaskClear(commands, import.buffer, HostImportOffset(import, address), bytes, view, color.extent.width, color.extent.height, color.elementBytes, color.clearWords, color.dccAddress == 0);
|
||||
recorder.NotePendingWrite(address, bytes);
|
||||
@@ -1544,6 +1545,10 @@ void keepRecordedDraw(Recorder& recorder, const std::shared_ptr<ShaderResources>
|
||||
kept->scratch = std::move(scratch);
|
||||
kept->targets = std::move(targets);
|
||||
const auto& residents = kept->targets;
|
||||
resources->KeepStorageImageBytes(recorder);
|
||||
for (const auto& target : residents) {
|
||||
if (target != nullptr && !target->Cached()) recorder.KeepBytes(target.get(), static_cast<std::size_t>(target->BudgetBytes()));
|
||||
}
|
||||
recorder.Keep(kept);
|
||||
if (resources != nullptr) recorder.KeepBytes(resources.get(), resources->CopiedBytes());
|
||||
if (checkRecords) recorder.OnComplete(std::move(checkRecords));
|
||||
@@ -1702,15 +1707,15 @@ void captureInputs(const Context& context, Recorder& recorder, VkCommandBuffer c
|
||||
};
|
||||
if (bindings != nullptr) {
|
||||
for (const auto& snapshot : bindings->snapshots) {
|
||||
const auto bytes = snapshot.buffer->Bytes();
|
||||
addSample(snapshot.address, bytes.size(), snapshot.buffer->Handle(), 0, bytes.data());
|
||||
const auto bytes = snapshot.Bytes();
|
||||
addSample(snapshot.address, bytes.size(), snapshot.buffer->Handle(), snapshot.offset, bytes.data());
|
||||
}
|
||||
}
|
||||
for (const auto& [begin, end] : resources.InPlaceReads()) {
|
||||
Require(end >= begin, "invalid capture input range");
|
||||
const auto bytes = static_cast<std::size_t>(end - begin);
|
||||
if (bytes == 0 || bytes > 512) continue;
|
||||
if (bindings != nullptr && std::any_of(bindings->snapshots.begin(), bindings->snapshots.end(), [&](const auto& snapshot) { return snapshot.address < end && begin < snapshot.address + snapshot.buffer->Bytes().size(); })) continue;
|
||||
if (bindings != nullptr && std::any_of(bindings->snapshots.begin(), bindings->snapshots.end(), [&](const auto& snapshot) { return snapshot.address < end && begin < snapshot.address + snapshot.bytes; })) continue;
|
||||
if (resources.WritesOverlap(begin, bytes) || recorder.PendingWriteOverlaps(begin, bytes)) {
|
||||
CaptureTrace::Log("input-skip draw=%llu batch=%llu address=%llx bytes=%zu reason=gpu-writer", draw, batch, static_cast<unsigned long long>(begin), bytes);
|
||||
continue;
|
||||
@@ -2678,6 +2683,7 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con
|
||||
if (!proved) {
|
||||
SharedResourceCache().Remove(recipe.key, resources.get());
|
||||
countCache(&DrawProfile::cacheInvalidated);
|
||||
resources->KeepStorageImageBytes(*recorder);
|
||||
recorder->Keep(std::move(resources));
|
||||
timer.phase(PhaseLookup);
|
||||
return miss(DrawRecipeMiss::Proof);
|
||||
|
||||
@@ -1400,7 +1400,10 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s
|
||||
if (fresh && end != 0) {
|
||||
if (const auto* current = findImport(state, address, end)) return current;
|
||||
} else if (!fresh) {
|
||||
if (const auto* current = findImport(state, address, address + 1); current != nullptr && current->joined && sameRange(*current, lease)) return current;
|
||||
if (const auto* current = peekImport(state, address, address + 1); current != nullptr && current->joined && sameRange(*current, lease)) {
|
||||
current->lastUse = ++state.useCounter;
|
||||
return current;
|
||||
}
|
||||
}
|
||||
auto run = importRun(lease, address);
|
||||
// Without an end the caller views the whole run through the import (AcquireRegistered): a window that holds the
|
||||
@@ -1431,7 +1434,10 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import
|
||||
failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(run.ranges.begin(), run.ranges.end())});
|
||||
return nullptr;
|
||||
}
|
||||
if (const auto found = state.imports.find(base); found != state.imports.end() && sameRun(found->second, run)) return &found->second;
|
||||
if (const auto found = state.imports.find(base); found != state.imports.end() && sameRun(found->second, run)) {
|
||||
found->second.lastUse = ++state.useCounter;
|
||||
return &found->second;
|
||||
}
|
||||
if (alignment == 0 || base % alignment != 0 || bytes % alignment != 0) return nullptr;
|
||||
if (const auto failed = failures.find(base); failed != failures.end()) {
|
||||
if (sameRun(failed->second, run)) return nullptr;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp"
|
||||
@@ -566,6 +567,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
|
||||
for (const auto module : _modules) context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, module, nullptr);
|
||||
_modules.clear();
|
||||
timing.Mark("create");
|
||||
PerformanceCounters::CountPipeline();
|
||||
LogPipelineStatistics_nid_no_patch(context, pipeline);
|
||||
} catch (...) {
|
||||
release();
|
||||
|
||||
@@ -21,18 +21,18 @@ PlaceholderImages::PlaceholderImages(const Context& context) : context(context)
|
||||
try {
|
||||
std::vector<VkImageMemoryBarrier> toClear;
|
||||
for (std::uint32_t index = 0; index < images.size(); ++index) {
|
||||
const auto shape = PlaceholderImageShapeOf(ShaderRecompiler::RuntimeAbi::FirstImageBinding + index);
|
||||
if (!Supported(context, shape)) continue;
|
||||
const auto shape = PlaceholderImageShapeForDevice(ShaderRecompiler::RuntimeAbi::FirstImageBinding + index, context.storageImageMultisample);
|
||||
if (!shape || !Supported(context, *shape)) continue;
|
||||
auto& image = images[index];
|
||||
VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
||||
info.imageType = shape.type;
|
||||
info.format = shape.format;
|
||||
info.imageType = shape->type;
|
||||
info.format = shape->format;
|
||||
info.extent = {1, 1, 1};
|
||||
info.mipLevels = 1;
|
||||
info.arrayLayers = 1;
|
||||
info.samples = shape.samples;
|
||||
info.samples = shape->samples;
|
||||
info.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
info.usage = shape.usage;
|
||||
info.usage = shape->usage;
|
||||
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||
info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
Check(context.Function<PFN_vkCreateImage>("vkCreateImage")(context.device, &info, nullptr, &image.image), "vkCreateImage placeholder");
|
||||
@@ -45,9 +45,9 @@ PlaceholderImages::PlaceholderImages(const Context& context) : context(context)
|
||||
Check(context.Function<PFN_vkBindImageMemory>("vkBindImageMemory")(context.device, image.image, image.memory, 0), "vkBindImageMemory placeholder");
|
||||
VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
|
||||
viewInfo.image = image.image;
|
||||
viewInfo.viewType = shape.viewType;
|
||||
viewInfo.format = shape.format;
|
||||
viewInfo.subresourceRange = {shape.aspect, 0, 1, 0, 1};
|
||||
viewInfo.viewType = shape->viewType;
|
||||
viewInfo.format = shape->format;
|
||||
viewInfo.subresourceRange = {shape->aspect, 0, 1, 0, 1};
|
||||
Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &image.view), "vkCreateImageView placeholder");
|
||||
VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
|
||||
barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "prx/libSceAgcDriver/Graphics/shaders/SampleCounter_spv.h"
|
||||
#include "prx/libSceAgcDriver/Graphics/shaders/SampleDumps_spv.h"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Pm4Opcodes.hpp"
|
||||
#include "prx/libc/include/CpuTopology.hpp"
|
||||
@@ -2414,6 +2415,7 @@ void Recorder::readGpuTiming(Batch& batch) {
|
||||
batch.gpuEndNs = static_cast<double>(stamps[i * 2 + 1]) * period;
|
||||
PerformanceTrace::GpuRange("batch", batch.gpuStartNs, batch.gpuEndNs, batch.traceId);
|
||||
timingBatchMs += ns / 1e6;
|
||||
AgcDriver::PerformanceCounters::AddGpuNanoseconds(static_cast<std::uint64_t>(ns));
|
||||
continue;
|
||||
}
|
||||
const auto key = batch.timedKeys[i];
|
||||
@@ -2540,6 +2542,17 @@ void Recorder::eraseDrawSnapshot(std::map<DrawSnapshotKey, DrawSnapshot>::iterat
|
||||
drawSnapshots.erase(entry);
|
||||
}
|
||||
|
||||
Recorder::UploadSlice Recorder::DrawUpload(std::size_t bytes) {
|
||||
const VkDeviceSize alignment = std::max<VkDeviceSize>(context.limits.minStorageBufferOffsetAlignment, 16);
|
||||
auto offset = (drawUploadUsed + alignment - 1) & ~(alignment - 1);
|
||||
if (drawUpload == nullptr || offset + bytes > drawUpload->Bytes().size()) {
|
||||
drawUpload = std::make_shared<Buffer>(context, std::max(DrawUploadChunk, bytes), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
|
||||
offset = 0;
|
||||
}
|
||||
drawUploadUsed = offset + bytes;
|
||||
return {drawUpload, offset, drawUpload->Bytes().data() + offset};
|
||||
}
|
||||
|
||||
void Recorder::retireUnwrittenSnapshots(const std::array<std::uint64_t, 3>& stamp) {
|
||||
if (stamp == unwrittenStamp) return;
|
||||
unwrittenSnapshots.clear();
|
||||
@@ -2547,21 +2560,22 @@ void Recorder::retireUnwrittenSnapshots(const std::array<std::uint64_t, 3>& stam
|
||||
unwrittenStamp = stamp;
|
||||
}
|
||||
|
||||
std::shared_ptr<Buffer> Recorder::UnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array<std::uint64_t, 3>& stamp) {
|
||||
std::shared_ptr<Buffer> Recorder::UnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array<std::uint64_t, 3>& stamp, VkDeviceSize* offset) {
|
||||
retireUnwrittenSnapshots(stamp);
|
||||
const auto found = unwrittenSnapshots.find(address);
|
||||
if (found == unwrittenSnapshots.end() || found->second.bytes != bytes) return {};
|
||||
if (offset != nullptr) *offset = found->second.offset;
|
||||
return found->second.buffer;
|
||||
}
|
||||
|
||||
void Recorder::NoteUnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array<std::uint64_t, 3>& stamp, std::shared_ptr<Buffer> buffer) {
|
||||
void Recorder::NoteUnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array<std::uint64_t, 3>& stamp, std::shared_ptr<Buffer> buffer, VkDeviceSize offset) {
|
||||
retireUnwrittenSnapshots(stamp);
|
||||
if (const auto found = unwrittenSnapshots.find(address); found != unwrittenSnapshots.end()) {
|
||||
unwrittenBytes -= found->second.bytes;
|
||||
unwrittenSnapshots.erase(found);
|
||||
}
|
||||
if (unwrittenSnapshots.size() >= UnwrittenSnapshotEntries || bytes > DrawSnapshotBudget - unwrittenBytes) return;
|
||||
unwrittenSnapshots.emplace(address, UnwrittenSnapshot{bytes, std::move(buffer)});
|
||||
unwrittenSnapshots.emplace(address, UnwrittenSnapshot{bytes, std::move(buffer), offset});
|
||||
unwrittenBytes += bytes;
|
||||
}
|
||||
|
||||
|
||||
@@ -194,6 +194,16 @@ void noteUncached(const std::shared_ptr<StorageTexture>& image) {
|
||||
uncached.any.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
std::atomic<bool>& TexturesReleased() {
|
||||
static std::atomic<bool> released{false};
|
||||
return released;
|
||||
}
|
||||
|
||||
void noteReleased(const std::shared_ptr<Texture>& texture) {
|
||||
texture->MarkReleased();
|
||||
TexturesReleased().store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
bool SamplerCacheDisabled() {
|
||||
static const bool disabled = std::getenv("APS5_NO_SAMPLER_CACHE") != nullptr;
|
||||
return disabled;
|
||||
@@ -225,6 +235,7 @@ void eraseTexture(TextureCache& cache, std::list<CachedTexture>::iterator it) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (it->texture.use_count() > 1) noteReleased(it->texture);
|
||||
cache.index.erase(it->key);
|
||||
cache.entries.erase(it);
|
||||
++cache.erasures;
|
||||
@@ -424,7 +435,7 @@ void logLookup(const LookupRecord& record) {
|
||||
std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, std::uint32_t mip, std::uint64_t guestBytes = 0, bool written = false);
|
||||
|
||||
std::uint64_t heldBytes(const StorageTexture& image) {
|
||||
return std::max<std::uint64_t>(image.AllocationBytes(), image.GuestBytes());
|
||||
return image.BudgetBytes();
|
||||
}
|
||||
|
||||
bool MetadataMoved(const StorageTexture& image, const GuestTextureResource& resource) {
|
||||
@@ -639,10 +650,11 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
|
||||
// the GPU supplies the texture by a view of it; anything else needs those results in guest
|
||||
// memory first.
|
||||
const bool depthCopy = depthCompare && guestBytes >= DepthCopyMinBytes() && Texture::DepthCopyFormat(ResolveTextureFormat(resource.format)) != VK_FORMAT_UNDEFINED && resource.dimension != TextureDimension::k3D;
|
||||
const bool srgbExpand = !depthCompare && SrgbExpandsToRgba8(context, resource.format);
|
||||
auto source = StorageTexture::FindPending(address, guestBytes);
|
||||
const bool blockCopy = source != nullptr && !depthCompare && Texture::CanBlockCopyFrom(*source, resource);
|
||||
const bool blockCopy = source != nullptr && !depthCompare && !srgbExpand && Texture::CanBlockCopyFrom(*source, resource);
|
||||
const bool repackCopy = source != nullptr && !depthCompare && Texture::CanRepackCopyFrom(*source, resource);
|
||||
if (source != nullptr && !blockCopy && !repackCopy && (depthCompare ? !depthCopy || !Texture::CanDepthCopyFrom(*source, resource) : !Texture::CanCopyFrom(*source, resource))) source.reset();
|
||||
if (source != nullptr && !blockCopy && !repackCopy && (depthCompare ? !depthCopy || !Texture::CanDepthCopyFrom(*source, resource) : srgbExpand ? !Texture::CanSrgbExpandFrom(context, *source, resource) : !Texture::CanCopyFrom(*source, resource))) source.reset();
|
||||
if (source != nullptr && MetadataMoved(*source, resource)) source.reset();
|
||||
std::optional<DccKeys> keys;
|
||||
bool clearThroughKeys = false;
|
||||
@@ -674,6 +686,7 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
|
||||
if (*keys == DccKeys::Uncompressed) {
|
||||
source = sampledStorageSource(context, resource, guestBytes);
|
||||
if (source != nullptr && depthCompare && !Texture::CanDepthCopyFrom(*source, resource)) source.reset();
|
||||
if (source != nullptr && srgbExpand && !Texture::CanSrgbExpandFrom(context, *source, resource)) source.reset();
|
||||
} else if (!depthCompare && ClearedViewEnabled() && StorageClearAvailable(context, resource.format, *keys)) {
|
||||
// A fast-cleared surface is viewed as well, through its image cleared on the GPU (the
|
||||
// lookup's Refresh, see StorageTexture::upload), when the image's own descriptor names
|
||||
@@ -734,6 +747,11 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
|
||||
texture->RefreshDepthCopy();
|
||||
return texture;
|
||||
}
|
||||
if (source != nullptr && srgbExpand) {
|
||||
auto texture = std::make_shared<Texture>(context, source, resource, components, Texture::SrgbExpandTag{});
|
||||
texture->RefreshDepthCopy();
|
||||
return texture;
|
||||
}
|
||||
if (source != nullptr) return std::make_shared<Texture>(context, source, resource, components);
|
||||
std::vector<std::byte> snapshot(bytes);
|
||||
ReadTextureSurface(resource, *keys, snapshot);
|
||||
@@ -816,6 +834,9 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
|
||||
} else if (IsRepackedTextureFormat(resource.format)) {
|
||||
entry.texture = std::make_shared<Texture>(context, source, resource, components, Texture::RepackCopyTag{});
|
||||
entry.texture->RefreshDepthCopy();
|
||||
} else if (srgbExpand) {
|
||||
entry.texture = std::make_shared<Texture>(context, source, resource, components, Texture::SrgbExpandTag{});
|
||||
entry.texture->RefreshDepthCopy();
|
||||
} else {
|
||||
entry.texture = std::make_shared<Texture>(context, source, resource, components);
|
||||
}
|
||||
@@ -840,6 +861,7 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
|
||||
const auto budget = sampledBudget(context, cache);
|
||||
auto texture = entry.texture;
|
||||
if (!admitTexture(cache, key, entry.accounted, budget)) {
|
||||
noteReleased(texture);
|
||||
logLookup({texture.get(), resource, guestBytes, *keys, generation, source.get()});
|
||||
reportCacheCounts(cache, budget);
|
||||
reportTextureCounters();
|
||||
@@ -1036,7 +1058,11 @@ std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std
|
||||
std::vector<std::shared_ptr<ShaderResources>> resources;
|
||||
SharedResourceCache().RemoveImage(image.get(), &resources);
|
||||
if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->Recording()) {
|
||||
for (auto& resource : resources) recorder->Keep(std::move(resource));
|
||||
if (image.use_count() > 1) recorder->KeepBytes(image.get(), static_cast<std::size_t>(heldBytes(*image)));
|
||||
for (auto& resource : resources) {
|
||||
resource->KeepStorageImageBytes(*recorder);
|
||||
recorder->Keep(std::move(resource));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!evicted.empty()) {
|
||||
@@ -1078,6 +1104,10 @@ bool SampledBudgetReportDue(std::uint64_t reported, std::uint64_t budget, std::c
|
||||
return change > reported / 10u && sinceReport >= std::chrono::seconds(10);
|
||||
}
|
||||
|
||||
std::optional<std::uint32_t> LargestDeviceLocalHeap(const VkPhysicalDeviceMemoryProperties& memory) {
|
||||
return largestDeviceLocalHeap(memory);
|
||||
}
|
||||
|
||||
std::uint64_t StorageTextureCacheBudget(const Context& context) {
|
||||
return storageBudget(context);
|
||||
}
|
||||
@@ -1156,7 +1186,10 @@ void ClearCachedTextures(VkDevice device) {
|
||||
std::vector<std::shared_ptr<ShaderResources>> resources;
|
||||
SharedResourceCache().RemoveImage(image.get(), &resources);
|
||||
if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->Recording()) {
|
||||
for (auto& resource : resources) recorder->Keep(std::move(resource));
|
||||
for (auto& resource : resources) {
|
||||
resource->KeepStorageImageBytes(*recorder);
|
||||
recorder->Keep(std::move(resource));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1333,6 +1366,16 @@ bool SameAsPreviousStorageElement(const ShaderRecompiler::DescriptorBinding& bin
|
||||
|
||||
ShaderResources::ShaderResources(const Context& context, const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment, const ColorTarget& target, std::uint64_t indexAddress, std::size_t indexBytes) : ShaderResources(context, std::array<CompiledShader, 2>{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, static_cast<std::uint32_t>(vertex.pushConstants.size())}}}, target, 1, indexAddress, indexBytes) {}
|
||||
|
||||
void ShaderResources::KeepStorageImageBytes(Recorder& recorder) const {
|
||||
const auto keep = [&](const std::shared_ptr<StorageTexture>& image) {
|
||||
if (image != nullptr && !image->Cached()) recorder.KeepBytes(image.get(), static_cast<std::size_t>(image->BudgetBytes()));
|
||||
};
|
||||
for (const auto& image : storageTextures) keep(image);
|
||||
for (const auto& texture : textures) {
|
||||
if (texture != nullptr) keep(texture->SharedStorageSource());
|
||||
}
|
||||
}
|
||||
|
||||
ShaderResources::ShaderResources(const Context& context, std::span<const CompiledShader> shaders, const ColorTarget& target, std::uint32_t colorAttachments, std::uint64_t indexAddress, std::size_t indexBytes, std::span<const GuestMemorySnapshot> snapshots) : context(context), guestMemory(context) {
|
||||
drawBuild = true;
|
||||
prepareAddressBindings(shaders, snapshots);
|
||||
@@ -2381,6 +2424,7 @@ void VisitContentKey(const CompiledShader& shader, bool dataWords, bool movableB
|
||||
packBits(binding.imageAtomic);
|
||||
packBits(binding.samplerUnnormalized);
|
||||
packBits(binding.samplerIntegerBorder);
|
||||
packBits(binding.samplerCoordinatesIgnored);
|
||||
packBits(binding.imageUnnormalized);
|
||||
packBits(binding.imageUnnormalizedBaseLevel);
|
||||
// Read-only elements are bound without a write set: an object built for one written set
|
||||
@@ -2402,11 +2446,11 @@ std::vector<std::uint32_t> ShaderResources::ContentKey(const CompiledShader& sha
|
||||
words += count;
|
||||
};
|
||||
for (const auto& binding : shader.program->bindings) {
|
||||
addWords(19);
|
||||
addWords(20);
|
||||
addWords(binding.imageSamplers.size());
|
||||
addWords(binding.imageShapes.size());
|
||||
if ((dataWords && !movableBuffers) || !DataRole(binding.role)) addWords(binding.guestDescriptor.size());
|
||||
for (const auto* bits : {&binding.imageWritten, &binding.samplerDepthCompare, &binding.imageDepthCompare, &binding.imageAtomic, &binding.samplerUnnormalized, &binding.samplerIntegerBorder, &binding.imageUnnormalized, &binding.imageUnnormalizedBaseLevel, &binding.bufferWritten}) addWords(bits->size() / 32u + (bits->size() % 32u != 0 ? 1u : 0u));
|
||||
for (const auto* bits : {&binding.imageWritten, &binding.samplerDepthCompare, &binding.imageDepthCompare, &binding.imageAtomic, &binding.samplerUnnormalized, &binding.samplerIntegerBorder, &binding.samplerCoordinatesIgnored, &binding.imageUnnormalized, &binding.imageUnnormalizedBaseLevel, &binding.bufferWritten}) addWords(bits->size() / 32u + (bits->size() % 32u != 0 ? 1u : 0u));
|
||||
}
|
||||
key.reserve(words);
|
||||
VisitContentKey(shader, dataWords, movableBuffers, [&](std::uint32_t word) { key.push_back(word); });
|
||||
@@ -3538,7 +3582,8 @@ void ResourceCache::noteMiss(const Key& key) {
|
||||
|
||||
void ResourceCache::Insert(const Key& key, std::shared_ptr<ShaderResources> resources, std::vector<std::shared_ptr<ShaderResources>>* evicted) {
|
||||
std::lock_guard lock(mutex);
|
||||
if (!resources->HasCachedStorageImages()) return;
|
||||
dropReleasedTextures(evicted);
|
||||
if (!resources->HasCachedStorageImages() || resources->HoldsReleasedTexture()) return;
|
||||
if (const auto found = index.find(key); found != index.end()) {
|
||||
if (evicted != nullptr) evicted->push_back(std::move(found->second->second));
|
||||
entries.erase(found->second);
|
||||
@@ -3597,6 +3642,19 @@ void ResourceCache::RemoveImage(const StorageTexture* image, std::vector<std::sh
|
||||
}
|
||||
}
|
||||
|
||||
void ResourceCache::dropReleasedTextures(std::vector<std::shared_ptr<ShaderResources>>* evicted) {
|
||||
if (!TexturesReleased().exchange(false, std::memory_order_acq_rel)) return;
|
||||
for (auto it = entries.begin(); it != entries.end();) {
|
||||
if (!it->second->HoldsReleasedTexture()) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
if (evicted != nullptr) evicted->push_back(std::move(it->second));
|
||||
index.erase(it->first);
|
||||
it = entries.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
bool ResourceCache::Touch(const Key& key) {
|
||||
resourceCacheTouches.fetch_add(1, std::memory_order_relaxed);
|
||||
std::lock_guard lock(mutex);
|
||||
@@ -4137,7 +4195,14 @@ void ShaderResources::addImageBinding(const ShaderRecompiler::DescriptorBinding&
|
||||
Require(binding.samplerDepthCompare.size() == binding.count, "guest sampler binding is missing depth comparison metadata");
|
||||
Require(binding.samplerUnnormalized.empty() || binding.samplerUnnormalized.size() == binding.count, "guest sampler binding has unnormalized coordinate metadata of another size");
|
||||
for (std::uint32_t element = 0; element < binding.count; ++element) {
|
||||
const auto words = std::span<const std::uint32_t>(binding.guestDescriptor).subspan(static_cast<std::size_t>(element) * elementWords, elementWords);
|
||||
auto guestWords = std::span<const std::uint32_t>(binding.guestDescriptor).subspan(static_cast<std::size_t>(element) * elementWords, elementWords);
|
||||
std::array<std::uint32_t, 4> normalizedWords{};
|
||||
if (element < binding.samplerCoordinatesIgnored.size() && binding.samplerCoordinatesIgnored[element]) {
|
||||
std::copy(guestWords.begin(), guestWords.end(), normalizedWords.begin());
|
||||
normalizedWords[0] &= ~(1u << 15u);
|
||||
guestWords = normalizedWords;
|
||||
}
|
||||
const auto words = guestWords;
|
||||
const bool compareEnable = binding.samplerDepthCompare.at(element);
|
||||
const bool unnormalized = element < binding.samplerUnnormalized.size() && binding.samplerUnnormalized[element];
|
||||
const bool integerBorder = element < binding.samplerIntegerBorder.size() && binding.samplerIntegerBorder[element];
|
||||
@@ -5030,10 +5095,19 @@ std::size_t ShaderResources::MaxDrawSnapshotBytes() {
|
||||
return static_cast<std::size_t>(mib * 1024ull * 1024ull);
|
||||
}
|
||||
|
||||
std::size_t ShaderResources::DrawUploadLimit() {
|
||||
static const std::size_t limit = [] {
|
||||
const char* value = std::getenv("APS5_DRAW_UPLOAD_MAX");
|
||||
return value != nullptr ? static_cast<std::size_t>(std::strtoull(value, nullptr, 10)) : std::size_t{4096};
|
||||
}();
|
||||
return limit;
|
||||
}
|
||||
|
||||
std::shared_ptr<ShaderResources::DrawBindings> ShaderResources::PrepareDrawBindings(Recorder& recorder, std::span<const MovedBuffer> moved, bool movedOnly) const {
|
||||
if ((_set == VK_NULL_HANDLE && fragment.set == VK_NULL_HANDLE) || usesBda) return {};
|
||||
const auto reads = guestMemory.InPlaceReads();
|
||||
const auto maxSnapshotBytes = maxDrawSnapshotBytes;
|
||||
const auto uploadLimit = DrawUploadLimit();
|
||||
std::size_t totalSnapshotBytes = 0;
|
||||
auto result = std::make_shared<DrawBindings>();
|
||||
thread_local std::vector<std::size_t>* selectedSlot = nullptr;
|
||||
@@ -5061,7 +5135,7 @@ std::shared_ptr<ShaderResources::DrawBindings> ShaderResources::PrepareDrawBindi
|
||||
else movedWordBuffers[movedWordBufferNext++ % MovedWordBufferSlots] = std::move(entry);
|
||||
}
|
||||
selected.push_back(index);
|
||||
result->snapshots.push_back({0, std::move(buffer)});
|
||||
result->snapshots.push_back({0, std::move(buffer), 0, override->size});
|
||||
continue;
|
||||
}
|
||||
std::uint64_t address = item.address;
|
||||
@@ -5082,28 +5156,38 @@ std::shared_ptr<ShaderResources::DrawBindings> ShaderResources::PrepareDrawBindi
|
||||
stamp = {GuestMemory::TrackerGeneration(), GuestMemory::CollectEpoch(), registryGeneration};
|
||||
stamped = true;
|
||||
}
|
||||
auto buffer = recorder.UnwrittenDrawSnapshot(begin, bytes, stamp);
|
||||
VkDeviceSize offset = 0;
|
||||
auto buffer = recorder.UnwrittenDrawSnapshot(begin, bytes, stamp, &offset);
|
||||
static const bool verifySnapshots = std::getenv("APS5_VERIFY_DRAW_SNAPSHOTS") != nullptr;
|
||||
if (verifySnapshots && buffer != nullptr) {
|
||||
static std::atomic<std::uint64_t> checked{0}, differing{0};
|
||||
const auto fast = buffer->Bytes().subspan(static_cast<std::size_t>(offset), bytes);
|
||||
const auto slow = recorder.ReusableDrawSnapshot(begin, bytes, Recorder::SnapshotUse::Storage, nullptr, GuestMemory::CollectWrites(begin, bytes));
|
||||
const bool same = slow == buffer || (slow != nullptr && slow->Bytes().size() == buffer->Bytes().size() && std::memcmp(slow->Bytes().data(), buffer->Bytes().data(), buffer->Bytes().size()) == 0) || (slow == nullptr && std::memcmp(buffer->Bytes().data(), reinterpret_cast<const void*>(begin), bytes) == 0);
|
||||
const bool same = slow == buffer || (slow != nullptr && slow->Bytes().size() == fast.size() && std::memcmp(slow->Bytes().data(), fast.data(), fast.size()) == 0) || (slow == nullptr && std::memcmp(fast.data(), reinterpret_cast<const void*>(begin), bytes) == 0);
|
||||
if (!same) differing.fetch_add(1);
|
||||
if (checked.fetch_add(1) % 200000 == 199999) std::fprintf(stderr, "[draw-snapshots] verify: %llu fast answers checked, %llu differing%c", static_cast<unsigned long long>(checked.load()), static_cast<unsigned long long>(differing.load()), 10);
|
||||
}
|
||||
if (buffer == nullptr) {
|
||||
const auto generation = GuestMemory::CollectWrites(begin, bytes);
|
||||
buffer = recorder.ReusableDrawSnapshot(begin, bytes, Recorder::SnapshotUse::Storage, nullptr, generation);
|
||||
if (buffer == nullptr) {
|
||||
buffer = std::make_shared<Buffer>(context, bytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
|
||||
std::memcpy(buffer->Bytes().data(), reinterpret_cast<const void*>(begin), bytes);
|
||||
recorder.KeepDrawSnapshot(begin, bytes, generation, registryGeneration, buffer);
|
||||
const bool uploaded = bytes != 0 && bytes <= uploadLimit;
|
||||
if (uploaded) {
|
||||
auto slice = recorder.DrawUpload(bytes);
|
||||
std::memcpy(slice.bytes, reinterpret_cast<const void*>(begin), bytes);
|
||||
buffer = std::move(slice.buffer);
|
||||
offset = slice.offset;
|
||||
} else {
|
||||
const auto generation = GuestMemory::CollectWrites(begin, bytes);
|
||||
buffer = recorder.ReusableDrawSnapshot(begin, bytes, Recorder::SnapshotUse::Storage, nullptr, generation);
|
||||
if (buffer == nullptr) {
|
||||
buffer = std::make_shared<Buffer>(context, bytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
|
||||
std::memcpy(buffer->Bytes().data(), reinterpret_cast<const void*>(begin), bytes);
|
||||
recorder.KeepDrawSnapshot(begin, bytes, generation, registryGeneration, buffer);
|
||||
}
|
||||
}
|
||||
if (GuestMemory::TrackerGeneration() == stamp[0]) recorder.NoteUnwrittenDrawSnapshot(begin, bytes, stamp, buffer);
|
||||
else stamped = false;
|
||||
if (GuestMemory::TrackerGeneration() != stamp[0]) stamped = false;
|
||||
else if (!uploaded || stamp[1] != 0) recorder.NoteUnwrittenDrawSnapshot(begin, bytes, stamp, buffer, offset);
|
||||
}
|
||||
selected.push_back(index);
|
||||
result->snapshots.push_back({begin, std::move(buffer)});
|
||||
result->snapshots.push_back({begin, std::move(buffer), offset, bytes});
|
||||
totalSnapshotBytes += bytes;
|
||||
CaptureTrace::Log("draw-snapshot batch=%llu address=%llx bytes=%zu", static_cast<unsigned long long>(recorder.Submissions() + 1), static_cast<unsigned long long>(begin), bytes);
|
||||
}
|
||||
@@ -5152,7 +5236,7 @@ std::shared_ptr<ShaderResources::DrawBindings> ShaderResources::PrepareDrawBindi
|
||||
const auto snapshotIndex = snapshotIndices[binding.allocations[element]];
|
||||
if (snapshotIndex == selected.size()) continue;
|
||||
const auto& snapshot = result->snapshots[snapshotIndex];
|
||||
buffers[offset + element] = {snapshot.buffer->Handle(), 0, snapshot.buffer->Bytes().size()};
|
||||
buffers[offset + element] = {snapshot.buffer->Handle(), snapshot.offset, snapshot.bytes};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5255,6 +5339,10 @@ bool ShaderResources::HasCachedStorageImages() const {
|
||||
return std::none_of(textures.begin(), textures.end(), [](const auto& texture) { return texture != nullptr && texture->StorageSource() != nullptr && !texture->StorageSource()->Cached(); });
|
||||
}
|
||||
|
||||
bool ShaderResources::HoldsReleasedTexture() const {
|
||||
return std::any_of(textures.begin(), textures.end(), [](const auto& texture) { return texture != nullptr && texture->Released(); });
|
||||
}
|
||||
|
||||
bool ShaderResources::ReadsImage(const StorageTexture* image) const {
|
||||
if (image == nullptr) return false;
|
||||
for (const auto& texture : textures) {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <span>
|
||||
#include <array>
|
||||
#include <sstream>
|
||||
#include <string_view>
|
||||
|
||||
namespace AgcDriver::Graphics {
|
||||
namespace {
|
||||
@@ -50,6 +51,25 @@ std::string zeroMessage(std::uint32_t offset, std::uint32_t value, const char* n
|
||||
return std::string(name) + " is unsupported" + detail;
|
||||
}
|
||||
|
||||
std::string tessellationMessage(std::uint32_t config, std::uint32_t parameters, const ShaderRecompiler::TessellationConfiguration& tess) {
|
||||
static constexpr std::array<std::string_view, 3> Types{"TESS_ISOLINE", "TESS_TRIANGLE", "TESS_QUAD"};
|
||||
static constexpr std::array<std::string_view, 4> Partitionings{"PART_INTEGER", "PART_POW2", "PART_FRAC_ODD", "PART_FRAC_EVEN"};
|
||||
static constexpr std::array<std::string_view, 4> Topologies{"OUTPUT_POINT", "OUTPUT_LINE", "OUTPUT_TRIANGLE_CW", "OUTPUT_TRIANGLE_CCW"};
|
||||
std::ostringstream message;
|
||||
const auto field = [&](std::string_view name, std::uint32_t value, std::span<const std::string_view> names) {
|
||||
message << name << '=' << value;
|
||||
if (value < names.size()) message << ' ' << names[value];
|
||||
};
|
||||
message << "VGT_TF_PARAM=0x" << std::hex << parameters << std::dec << " (";
|
||||
field("TYPE", tess.domain, Types);
|
||||
message << ", ";
|
||||
field("PARTITIONING", tess.partitioning, Partitionings);
|
||||
message << ", ";
|
||||
field("TOPOLOGY", tess.outputTopology, Topologies);
|
||||
message << "), VGT_LS_HS_CONFIG=0x" << std::hex << config << std::dec << " (NUM_PATCHES=" << tess.patchesPerGroup << ", HS_NUM_INPUT_CP=" << tess.inputControlPoints << ", HS_NUM_OUTPUT_CP=" << tess.outputControlPoints << "): only triangle and quad domains with integer, fractional-even or fractional-odd partitioning and triangle output are supported by the reference path";
|
||||
return message.str();
|
||||
}
|
||||
|
||||
void zero(const Registers& registers, std::uint32_t offset, std::uint32_t mask, const char* name, RegisterBank bank = RegisterBank::Context) {
|
||||
const auto value = read(registers, offset, bank);
|
||||
if ((value & mask) != 0) throw std::runtime_error(zeroMessage(offset, value, name));
|
||||
@@ -777,6 +797,7 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number,
|
||||
const auto mapping = static_cast<std::uint8_t>(alternate ? 0xecu : 0xe4u);
|
||||
if (number == unorm) return {VK_FORMAT_R8G8_UNORM, 2, mapping};
|
||||
if (number == snorm) return {VK_FORMAT_R8G8_SNORM, 2, mapping};
|
||||
if (number == uint) return {VK_FORMAT_R8G8_UINT, 2, mapping};
|
||||
return fail();
|
||||
}
|
||||
case 4:
|
||||
@@ -915,7 +936,7 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) {
|
||||
validate(tess.inputControlPoints != 0 && tess.inputControlPoints <= 32 && tess.outputControlPoints != 0 && tess.outputControlPoints <= 32, "invalid tessellation control-point counts");
|
||||
validate(tess.patchesPerGroup != 0, "VGT_LS_HS_CONFIG.NUM_PATCHES is zero");
|
||||
if ((parameters & ~0x000600ffu) != 0) Require(false, zeroMessage(0x2db, parameters, "VGT_TF_PARAM donut, factor detection, memory type or reserved state"));
|
||||
validate((tess.domain == 1 || tess.domain == 2) && (tess.partitioning == 0 || tess.partitioning == 2 || tess.partitioning == 3) && (tess.outputTopology == 2 || tess.outputTopology == 3), "only triangle and quad domains with integer, fractional-even or fractional-odd partitioning and triangle output are supported by the reference path");
|
||||
if (!((tess.domain == 1 || tess.domain == 2) && (tess.partitioning == 0 || tess.partitioning == 2 || tess.partitioning == 3) && (tess.outputTopology == 2 || tess.outputTopology == 3))) validate(false, tessellationMessage(config, parameters, tess).c_str());
|
||||
result.minTessellationLevel = readFloat(queue.context, 0x287);
|
||||
result.maxTessellationLevel = readFloat(queue.context, 0x286);
|
||||
Require(result.minTessellationLevel >= 0.0f && result.minTessellationLevel <= 63.0f && result.minTessellationLevel <= result.maxTessellationLevel && result.maxTessellationLevel >= 1.0f, "tessellation level clamps need 0 <= VGT_HOS_MIN_TESS_LEVEL <= 63, VGT_HOS_MAX_TESS_LEVEL >= 1 and the minimum at most the maximum");
|
||||
@@ -1096,7 +1117,9 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
|
||||
zero(cx, 0x292, ScanModeMask & ~1u, "scan conversion mode");
|
||||
if (result.samples > 1 && (read(cx, 0x292) & 1u) == 0) {
|
||||
const bool filled = result.topology != VK_PRIMITIVE_TOPOLOGY_POINT_LIST && result.topology != VK_PRIMITIVE_TOPOLOGY_LINE_LIST && result.topology != VK_PRIMITIVE_TOPOLOGY_LINE_STRIP;
|
||||
Require(filled && !result.depth && !result.alphaToCoverage, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points, lines, a depth target or alpha-to-coverage");
|
||||
Require(filled && !result.alphaToCoverage, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points, lines or alpha-to-coverage");
|
||||
if (const auto eqaa = find(cx, 0x201); result.depth && eqaa != cx.end() && (eqaa->second & 0x0f0c0000u) != 0) throw std::runtime_error(zeroMessage(0x201, eqaa->second, "a multisampled depth target rasterized without MSAA_ENABLE with Z interpolation or overrasterization (DB_EQAA)"));
|
||||
if (const auto control = find(cx, 0x203); result.depth && control != cx.end() && (control->second & 0x100u) != 0) throw std::runtime_error(zeroMessage(0x203, control->second, "a multisampled depth target rasterized without MSAA_ENABLE with a sample-mask export"));
|
||||
result.customSampleLocations = true;
|
||||
result.sampleLocations.fill(0);
|
||||
result.sampleLocationGrid = false;
|
||||
|
||||
@@ -64,6 +64,12 @@ TextureProfile& Profile() {
|
||||
return profile;
|
||||
}
|
||||
|
||||
void keepStorageTexture(Recorder& recorder, std::shared_ptr<StorageTexture> texture) {
|
||||
if (texture == nullptr) return;
|
||||
if (!texture->Cached()) recorder.KeepBytes(texture.get(), static_cast<std::size_t>(texture->BudgetBytes()));
|
||||
recorder.Keep(std::move(texture));
|
||||
}
|
||||
|
||||
// APS5_PROFILE_DRAW, every 10 s on the [storage] line: storage image bytes uploaded by path
|
||||
// (recorded DCC clear, direct detile from the import, CPU) and written back by the reason of the
|
||||
// FlushPending that forced the write-back (the other callers name themselves: "refresh", "cache
|
||||
@@ -232,6 +238,53 @@ void ChainMinLod(const Context& context, const GuestTextureResource& descriptor,
|
||||
viewInfo.pNext = &minLod;
|
||||
}
|
||||
|
||||
struct ExpandImage {
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
std::shared_ptr<OwnedImage> owned;
|
||||
VkDeviceSize bytes = 0;
|
||||
};
|
||||
|
||||
ExpandImage CreateExpandImage(const Context& context, const GuestTextureResource& descriptor, std::uint32_t depth, std::uint32_t layers) {
|
||||
ExpandImage result;
|
||||
VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
||||
imageInfo.flags = VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | (descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u);
|
||||
imageInfo.imageType = ImageTypeFor(descriptor.dimension);
|
||||
imageInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
|
||||
imageInfo.extent = {descriptor.width, descriptor.height, depth};
|
||||
imageInfo.mipLevels = descriptor.mipCount;
|
||||
imageInfo.arrayLayers = layers;
|
||||
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
||||
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
Check(context.Function<PFN_vkCreateImage>("vkCreateImage")(context.device, &imageInfo, nullptr, &result.image), "vkCreateImage sRGB copy");
|
||||
result.owned = std::make_shared<OwnedImage>(context, result.image, VK_NULL_HANDLE);
|
||||
VkMemoryRequirements requirements{};
|
||||
context.Function<PFN_vkGetImageMemoryRequirements>("vkGetImageMemoryRequirements")(context.device, result.image, &requirements);
|
||||
VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
||||
allocation.allocationSize = requirements.size;
|
||||
allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "texture");
|
||||
Check(context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &result.owned->memory), "vkAllocateMemory sRGB copy");
|
||||
Check(context.Function<PFN_vkBindImageMemory>("vkBindImageMemory")(context.device, result.image, result.owned->memory, 0), "vkBindImageMemory sRGB copy");
|
||||
result.bytes = requirements.size;
|
||||
return result;
|
||||
}
|
||||
|
||||
void RecordExpandBlit(const Context& context, VkCommandBuffer commands, VkImage source, VkImageLayout sourceLayout, VkImage target, const GuestTextureResource& descriptor, std::uint32_t depth, std::uint32_t layers) {
|
||||
std::vector<VkImageBlit> regions;
|
||||
for (std::uint32_t level = 0; level < descriptor.mipCount; ++level) {
|
||||
const VkOffset3D end{static_cast<std::int32_t>(std::max(descriptor.width >> level, 1u)), static_cast<std::int32_t>(std::max(descriptor.height >> level, 1u)), static_cast<std::int32_t>(descriptor.dimension == TextureDimension::k3D ? std::max(depth >> level, 1u) : 1u)};
|
||||
VkImageBlit region{};
|
||||
region.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, 0, layers};
|
||||
region.srcOffsets[1] = end;
|
||||
region.dstSubresource = region.srcSubresource;
|
||||
region.dstOffsets[1] = end;
|
||||
regions.push_back(region);
|
||||
}
|
||||
context.Function<PFN_vkCmdBlitImage>("vkCmdBlitImage")(commands, source, sourceLayout, target, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast<std::uint32_t>(regions.size()), regions.data(), VK_FILTER_NEAREST);
|
||||
}
|
||||
|
||||
std::atomic<std::uint64_t>& SampledMemoryCounter() {
|
||||
static std::atomic<std::uint64_t> bytes{0};
|
||||
return bytes;
|
||||
@@ -249,6 +302,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex
|
||||
PhaseTimer timer;
|
||||
try {
|
||||
const auto colorFormat = SampledTextureFormat(context, descriptor.format);
|
||||
const bool srgbExpand = !depthCompare && SrgbExpandsToRgba8(context, descriptor.format);
|
||||
Require(!depthCompare || colorFormat == VK_FORMAT_R32_SFLOAT || colorFormat == VK_FORMAT_R16_UNORM, "comparison sampling requires an R32 float or R16 unorm depth texture");
|
||||
Require(!depthCompare || descriptor.dimension != TextureDimension::k3D, "comparison sampling does not support 3D depth textures");
|
||||
const auto vkFormat = depthCompare ? (colorFormat == VK_FORMAT_R32_SFLOAT ? VK_FORMAT_D32_SFLOAT : VK_FORMAT_D16_UNORM) : colorFormat;
|
||||
@@ -281,7 +335,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex
|
||||
imageInfo.arrayLayers = geometry.imageLayers;
|
||||
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
||||
imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | (srgbExpand ? VK_IMAGE_USAGE_TRANSFER_SRC_BIT : 0u);
|
||||
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
Check(context.Function<PFN_vkCreateImage>("vkCreateImage")(context.device, &imageInfo, nullptr, &image), "vkCreateImage");
|
||||
@@ -296,6 +350,16 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex
|
||||
owned->memory = GetBufferPool(context)->AllocateMemory(allocation, "vkAllocateMemory texture");
|
||||
Check(context.Function<PFN_vkBindImageMemory>("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory");
|
||||
|
||||
const VkImage uploadImage = image;
|
||||
std::shared_ptr<OwnedImage> uploadOwned;
|
||||
if (srgbExpand) {
|
||||
auto expanded = CreateExpandImage(context, descriptor, geometry.imageDepth, geometry.imageLayers);
|
||||
uploadOwned = std::move(owned);
|
||||
owned = std::move(expanded.owned);
|
||||
image = expanded.image;
|
||||
allocationBytes = expanded.bytes;
|
||||
}
|
||||
|
||||
{
|
||||
// Debug aid: APS5_DUMP_TEXTURE=<hex addresses, comma separated> saves the detiled first mip
|
||||
// of those sampled textures on their first 8 uploads as texture_<address>_<n>.raw (u32
|
||||
@@ -334,6 +398,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex
|
||||
recorder->Keep(tiled, static_cast<std::size_t>(guestBytes));
|
||||
recorder->Keep(linear, static_cast<std::size_t>(linearBytes));
|
||||
recorder->Keep(owned);
|
||||
if (uploadOwned != nullptr) recorder->Keep(uploadOwned);
|
||||
} else {
|
||||
batch = std::make_unique<CommandBatch>(context);
|
||||
commands = batch->Handle();
|
||||
@@ -379,7 +444,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex
|
||||
toTransferDst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||
toTransferDst.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
toTransferDst.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
toTransferDst.image = image;
|
||||
toTransferDst.image = uploadImage;
|
||||
toTransferDst.subresourceRange = {aspect, 0, descriptor.mipCount, 0, geometry.imageLayers};
|
||||
context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &linearReadBarrier, 1, &toTransferDst);
|
||||
|
||||
@@ -400,7 +465,19 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex
|
||||
regions.push_back(region);
|
||||
}
|
||||
}
|
||||
context.Function<PFN_vkCmdCopyBufferToImage>("vkCmdCopyBufferToImage")(commands, linear->Handle(), image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast<std::uint32_t>(regions.size()), regions.data());
|
||||
context.Function<PFN_vkCmdCopyBufferToImage>("vkCmdCopyBufferToImage")(commands, linear->Handle(), uploadImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast<std::uint32_t>(regions.size()), regions.data());
|
||||
if (srgbExpand) {
|
||||
std::array<VkImageMemoryBarrier, 2> toBlit{};
|
||||
toBlit[0] = toTransferDst;
|
||||
toBlit[0].srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
toBlit[0].dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
||||
toBlit[0].oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||
toBlit[0].newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
|
||||
toBlit[1] = toTransferDst;
|
||||
toBlit[1].image = image;
|
||||
context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 2, toBlit.data());
|
||||
RecordExpandBlit(context, commands, uploadImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, image, descriptor, geometry.imageDepth, geometry.imageLayers);
|
||||
}
|
||||
std::unique_ptr<Buffer> dump;
|
||||
if (dumpWanted) {
|
||||
dump = std::make_unique<Buffer>(context, static_cast<std::size_t>(mips[0].linearSize), VK_BUFFER_USAGE_TRANSFER_DST_BIT);
|
||||
@@ -451,8 +528,8 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex
|
||||
VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
|
||||
viewInfo.image = image;
|
||||
viewInfo.viewType = ViewTypeFor(descriptor.dimension, viewLayerCount);
|
||||
viewInfo.format = vkFormat;
|
||||
viewFormat = vkFormat;
|
||||
viewInfo.format = srgbExpand ? VK_FORMAT_R8G8B8A8_SRGB : vkFormat;
|
||||
viewFormat = viewInfo.format;
|
||||
viewInfo.components = depthCompare ? VkComponentMapping{} : components;
|
||||
viewInfo.subresourceRange = {aspect, descriptor.baseLevel, viewLevelCount, descriptor.baseArray, viewLayerCount};
|
||||
VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT};
|
||||
@@ -687,10 +764,96 @@ Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>&
|
||||
}
|
||||
}
|
||||
|
||||
bool Texture::CanSrgbExpandFrom(const Context& context, const StorageTexture& source, const GuestTextureResource& descriptor) {
|
||||
if (!SrgbExpandsToRgba8(context, descriptor.format) || !CanCopyFrom(source, descriptor) || descriptor.dimension == TextureDimension::k3D) return false;
|
||||
if (!StorageFormatAvailable(context, source.Descriptor().format) || StorageFormatForGuest(context, source.Descriptor().format) != SampledTextureFormat(context, descriptor.format)) return false;
|
||||
const auto geometry = DescribeSurface(descriptor);
|
||||
return geometry.imageDepth == 1 && source.ImageDepth() == 1 && source.ImageLayers() >= geometry.imageLayers && source.Descriptor().mipCount >= descriptor.mipCount;
|
||||
}
|
||||
|
||||
Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>& source, const GuestTextureResource& descriptor, VkComponentMapping components, SrgbExpandTag) : context(context), storageSource(source), perUseCopy(PerUseCopy::RefreshedOnReuse), srgbExpand(true) {
|
||||
try {
|
||||
Require(source != nullptr && CanSrgbExpandFrom(context, *source, descriptor), "storage image cannot serve an sRGB copy of the sampled texture");
|
||||
const auto geometry = DescribeSurface(descriptor);
|
||||
copyWidth = descriptor.width;
|
||||
copyHeight = descriptor.height;
|
||||
copyLevels = descriptor.mipCount;
|
||||
copyLayers = geometry.imageLayers;
|
||||
APS5_LOG_OUT("Texture address=0x%llx %ux%u mips=%u layers=%u sRGB copy of storage image (guest format %u)", static_cast<unsigned long long>(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, copyLayers, descriptor.format);
|
||||
auto expanded = CreateExpandImage(context, descriptor, 1u, copyLayers);
|
||||
image = expanded.image;
|
||||
owned = std::move(expanded.owned);
|
||||
allocationBytes = expanded.bytes;
|
||||
|
||||
const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u;
|
||||
const auto viewLayerCount = geometry.imageLayers - descriptor.baseArray;
|
||||
if (descriptor.dimension == TextureDimension::kCube) {
|
||||
Require(viewLayerCount % 6u == 0, "guest cube texture view does not contain a multiple of 6 array slices");
|
||||
}
|
||||
VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
|
||||
viewInfo.image = image;
|
||||
viewInfo.viewType = ViewTypeFor(descriptor.dimension, viewLayerCount);
|
||||
viewInfo.format = VK_FORMAT_R8G8B8A8_SRGB;
|
||||
viewFormat = viewInfo.format;
|
||||
viewInfo.components = components;
|
||||
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, descriptor.baseLevel, viewLevelCount, descriptor.baseArray, viewLayerCount};
|
||||
VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT};
|
||||
ChainMinLod(context, descriptor, viewInfo, minLod);
|
||||
Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView sRGB copy");
|
||||
viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount};
|
||||
createFirstLayerView(descriptor, viewInfo);
|
||||
prepareBaseLevelView(descriptor, viewInfo);
|
||||
sourceDescriptor = descriptor;
|
||||
countedBytes = allocationBytes;
|
||||
SampledMemoryCounter().fetch_add(countedBytes, std::memory_order_relaxed);
|
||||
} catch (...) {
|
||||
release();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
bool Texture::refreshSrgbExpand() {
|
||||
auto* recorder = Recorder::Active();
|
||||
std::unique_ptr<CommandBatch> batch;
|
||||
VkCommandBuffer commands = VK_NULL_HANDLE;
|
||||
if (recorder != nullptr) {
|
||||
commands = recorder->Commands();
|
||||
recorder->Keep(owned);
|
||||
recorder->Keep(storageSource);
|
||||
} else {
|
||||
batch = std::make_unique<CommandBatch>(context);
|
||||
commands = batch->Handle();
|
||||
}
|
||||
const auto barrier = context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier");
|
||||
const VkMemoryBarrier produced{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT};
|
||||
VkImageMemoryBarrier toTransferDst{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
|
||||
toTransferDst.srcAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
||||
toTransferDst.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
toTransferDst.oldLayout = copiedOnce ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
toTransferDst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||
toTransferDst.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
toTransferDst.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
toTransferDst.image = image;
|
||||
toTransferDst.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, copyLevels, 0, copyLayers};
|
||||
barrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &produced, 0, nullptr, 1, &toTransferDst);
|
||||
RecordExpandBlit(context, commands, storageSource->Image(), VK_IMAGE_LAYOUT_GENERAL, image, sourceDescriptor, 1u, copyLayers);
|
||||
VkImageMemoryBarrier toShaderRead = toTransferDst;
|
||||
toShaderRead.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
toShaderRead.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
||||
toShaderRead.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||
toShaderRead.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &toShaderRead);
|
||||
if (batch) batch->SubmitAndWait();
|
||||
copiedOnce = true;
|
||||
copiedVersion = storageSource->Version();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Texture::RefreshDepthCopy() {
|
||||
if (perUseCopy != PerUseCopy::RefreshedOnReuse || storageSource == nullptr) return false;
|
||||
if (copiedOnce && storageSource->Version() == copiedVersion) return false;
|
||||
if (blockFormat != VK_FORMAT_UNDEFINED) return refreshBlockCopy();
|
||||
if (srgbExpand) return refreshSrgbExpand();
|
||||
std::uint64_t linearBytes = 0;
|
||||
for (std::uint32_t level = 0; level < copyLevels; ++level) linearBytes += static_cast<std::uint64_t>(std::max(copyWidth >> level, 1u)) * std::max(copyHeight >> level, 1u) * copyElementBytes * copyLayers;
|
||||
auto linear = std::make_shared<DeviceBuffer>(context, static_cast<std::size_t>(linearBytes), VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | (repackCopy ? VK_BUFFER_USAGE_STORAGE_BUFFER_BIT : 0u));
|
||||
@@ -2328,7 +2491,7 @@ void StorageTexture::upload(const std::vector<bool>* layers) {
|
||||
if (recorder != nullptr) {
|
||||
commands = recorder->Commands();
|
||||
timing = recorder->BeginGpuTiming(Recorder::CommandClass::DccClear);
|
||||
if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self));
|
||||
if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self));
|
||||
++Profile().storageRecordedClears;
|
||||
Recorder::CountBarriers(Recorder::CommandClass::DccClear, 2);
|
||||
if (Recorder::BarrierValidate()) {
|
||||
@@ -2420,7 +2583,7 @@ void StorageTexture::upload(const std::vector<bool>* layers) {
|
||||
timing = recorder->BeginGpuTiming(Recorder::CommandClass::StorageUpload);
|
||||
if (linear) recorder->Keep(linear, linear->Size());
|
||||
// The image itself must outlive the recorded copy: the cache may evict it right after.
|
||||
if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self));
|
||||
if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self));
|
||||
// The detile reads the tiled bytes from the import when the batch runs.
|
||||
recorder->NotePendingRead(descriptor.baseAddress, static_cast<std::size_t>(guestBytes), Recorder::ReadKind::StorageUpload);
|
||||
Recorder::CountBarriers(Recorder::CommandClass::StorageUpload, direct ? 2 : 3);
|
||||
@@ -2848,7 +3011,7 @@ std::uint64_t StorageTexture::uploadWindows(const HostImport& import, std::span<
|
||||
for (const auto& source : sources) {
|
||||
if (source.slab != nullptr) recorder->Keep(source.slab);
|
||||
}
|
||||
if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self));
|
||||
if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self));
|
||||
if (!reads.empty()) recorder->NotePendingReads(reads, Recorder::ReadKind::StorageUpload);
|
||||
Recorder::CountBarriers(Recorder::CommandClass::StorageUpload, direct ? 2 : 3);
|
||||
if (Recorder::BarrierValidate()) {
|
||||
@@ -3052,7 +3215,7 @@ std::uint64_t StorageTexture::writeBackWindows(std::span<const ImportSegment> se
|
||||
recorder->Keep(tiledScratch, tiledScratch->Size());
|
||||
for (const auto& pieces : slabPieces) recorder->Keep(pieces.slab);
|
||||
for (const auto& slab : padding.slabs) recorder->Keep(slab);
|
||||
if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self));
|
||||
if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self));
|
||||
Recorder::CountBarriers(Recorder::CommandClass::StorageWriteBack, direct ? 3 : 4);
|
||||
if (Recorder::BarrierValidate()) {
|
||||
const std::pair<VkImage, bool> read{image, false};
|
||||
@@ -4127,7 +4290,7 @@ bool StorageTexture::clearByKeysFill(DccKeys keys, std::uint8_t key) {
|
||||
}
|
||||
const auto commands = recorder->Commands();
|
||||
const auto timing = recorder->BeginGpuTiming(Recorder::CommandClass::DccClear);
|
||||
if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self));
|
||||
if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self));
|
||||
Recorder::CountBarriers(Recorder::CommandClass::DccClear, 2);
|
||||
if (Recorder::BarrierValidate()) {
|
||||
const std::pair<VkImage, bool> clearedImage{image, true};
|
||||
@@ -4262,7 +4425,7 @@ bool StorageTexture::FillClear(std::span<const std::uint32_t, 4> pattern, std::u
|
||||
const auto commands = recorder->Commands();
|
||||
const auto timing = recorder->BeginGpuTiming(Recorder::CommandClass::FillClear);
|
||||
Recorder::CountBarriers(Recorder::CommandClass::FillClear, 2);
|
||||
if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self));
|
||||
if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self));
|
||||
if (Recorder::BarrierValidate()) {
|
||||
const std::pair<VkImage, bool> cleared{image, true};
|
||||
recorder->NoteAccess(Recorder::CommandClass::FillClear, Recorder::Access{{}, {}, std::span(&cleared, 1), VK_PIPELINE_STAGE_TRANSFER_BIT});
|
||||
@@ -4389,8 +4552,8 @@ bool StorageTexture::CopyFrom(StorageTexture& source, const char*& refusal) {
|
||||
const auto commands = recorder->Commands();
|
||||
const auto timing = recorder->BeginGpuTiming(Recorder::CommandClass::Copy);
|
||||
Recorder::CountBarriers(Recorder::CommandClass::Copy, 2);
|
||||
if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self));
|
||||
if (auto other = source.weak_from_this().lock()) recorder->Keep(std::move(other));
|
||||
if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self));
|
||||
if (auto other = source.weak_from_this().lock()) keepStorageTexture(*recorder, std::move(other));
|
||||
if (Recorder::BarrierValidate()) {
|
||||
const std::array<std::pair<VkImage, bool>, 2> images{{{source.image, false}, {image, true}}};
|
||||
recorder->NoteAccess(Recorder::CommandClass::Copy, Recorder::Access{{}, {}, images, VK_PIPELINE_STAGE_TRANSFER_BIT});
|
||||
@@ -4567,8 +4730,8 @@ std::uint64_t StorageTexture::borrowUnits(StorageTexture& source, const std::vec
|
||||
if (recorder != nullptr) {
|
||||
commands = recorder->Commands();
|
||||
timing = recorder->BeginGpuTiming(Recorder::CommandClass::StorageUpload);
|
||||
if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self));
|
||||
if (auto other = source.weak_from_this().lock()) recorder->Keep(std::move(other));
|
||||
if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self));
|
||||
if (auto other = source.weak_from_this().lock()) keepStorageTexture(*recorder, std::move(other));
|
||||
Recorder::CountBarriers(Recorder::CommandClass::StorageUpload, 2);
|
||||
if (Recorder::BarrierValidate()) {
|
||||
const std::array<std::pair<VkImage, bool>, 2> images{{{source.image, false}, {image, true}}};
|
||||
@@ -4869,7 +5032,7 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
|
||||
recorder->Keep(tiledScratch, tiledScratch->Size());
|
||||
for (const auto& slab : padding.slabs) recorder->Keep(slab);
|
||||
// The image itself must outlive the recorded retile: the cache may evict it right after.
|
||||
if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self));
|
||||
if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self));
|
||||
Recorder::CountBarriers(Recorder::CommandClass::StorageWriteBack, padding.copies.empty() ? 4 : 5);
|
||||
if (Recorder::BarrierValidate()) {
|
||||
const std::pair<VkImage, bool> read{image, false};
|
||||
@@ -5148,6 +5311,10 @@ std::uint64_t StorageTexture::GuestBytes() const {
|
||||
return guestBytes;
|
||||
}
|
||||
|
||||
std::uint64_t StorageTexture::BudgetBytes() const {
|
||||
return std::max<std::uint64_t>(AllocationBytes(), GuestBytes());
|
||||
}
|
||||
|
||||
VkImageView StorageTexture::View() const {
|
||||
return view;
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ VkFormat ResolveTextureFormat(std::uint32_t guestFormat) {
|
||||
|
||||
std::uint32_t SrgbDecodeFormats(PFN_vkGetPhysicalDeviceFormatProperties formatProperties, VkPhysicalDevice physical) {
|
||||
const char* forced = std::getenv("APS5_SRGB_SHADER_DECODE");
|
||||
const bool always = forced != nullptr && std::strcmp(forced, "1") == 0;
|
||||
const bool always = forced != nullptr && (std::strcmp(forced, "1") == 0 || std::strcmp(forced, "copy") == 0);
|
||||
const auto sampled = [&](ShaderRecompiler::IrBufferFormat format) {
|
||||
VkFormatProperties properties{};
|
||||
formatProperties(physical, ResolveTextureFormat(static_cast<std::uint32_t>(format)), &properties);
|
||||
@@ -137,6 +137,26 @@ VkFormat SampledTextureFormat(const Context& context, std::uint32_t guestFormat)
|
||||
return ResolveTextureFormat(static_cast<std::uint32_t>(ShaderRecompiler::SrgbUnormFormat(format)));
|
||||
}
|
||||
|
||||
std::uint32_t SrgbCopyFormats(PFN_vkGetPhysicalDeviceFormatProperties formatProperties, VkPhysicalDevice physical, std::uint32_t decodeFormats) {
|
||||
const char* forced = std::getenv("APS5_SRGB_SHADER_DECODE");
|
||||
if (forced != nullptr && std::strcmp(forced, "1") == 0) return 0u;
|
||||
VkFormatProperties target{};
|
||||
formatProperties(physical, VK_FORMAT_R8G8B8A8_UNORM, &target);
|
||||
if ((target.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_DST_BIT) == 0) return 0u;
|
||||
std::uint32_t formats = 0;
|
||||
for (const auto format : {ShaderRecompiler::IrBufferFormat::Format8Srgb, ShaderRecompiler::IrBufferFormat::Format8_8Srgb}) {
|
||||
if ((decodeFormats & ShaderRecompiler::SrgbDecodeBit(format)) == 0u) continue;
|
||||
VkFormatProperties source{};
|
||||
formatProperties(physical, ResolveTextureFormat(static_cast<std::uint32_t>(ShaderRecompiler::SrgbUnormFormat(format))), &source);
|
||||
if ((source.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_SRC_BIT) != 0) formats |= ShaderRecompiler::SrgbDecodeBit(format);
|
||||
}
|
||||
return formats;
|
||||
}
|
||||
|
||||
bool SrgbExpandsToRgba8(const Context& context, std::uint32_t guestFormat) {
|
||||
return (context.srgbCopyFormats & ShaderRecompiler::SrgbDecodeBit(static_cast<ShaderRecompiler::IrBufferFormat>(guestFormat))) != 0u;
|
||||
}
|
||||
|
||||
bool IsSrgbTextureFormat(std::uint32_t guestFormat) {
|
||||
switch (findFormatEntry(guestFormat).vkFormat) {
|
||||
case VK_FORMAT_R8_SRGB:
|
||||
|
||||
@@ -383,6 +383,13 @@ ThickLayout ComputeThickLayout(TextureTileMode tileMode, std::uint32_t format, s
|
||||
|
||||
SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor) {
|
||||
SurfaceGeometry geometry;
|
||||
if (descriptor.samples != 1u && descriptor.tileMode != TextureTileMode::kLinear) {
|
||||
Require(descriptor.samples == 2u || descriptor.samples == 4u || descriptor.samples == 8u, "multisampled surface fragment count is not implemented");
|
||||
Require(descriptor.dimension == TextureDimension::k2DMsaa && descriptor.baseArray == 0u && descriptor.depthOrLastArray == 0u, "multisampled surface dimension or array range is not implemented");
|
||||
Require(descriptor.mipCount == 1u && descriptor.allocatedMipCount == 1u && descriptor.baseLevel == 0u && descriptor.lastLevel == 0u, "multisampled surface mip levels are not implemented");
|
||||
Require(descriptor.width != 0u && descriptor.width <= 16384u && descriptor.height != 0u && descriptor.height <= 16384u, "invalid multisampled surface extent");
|
||||
Require(!IsBlockCompressed(descriptor.format), "multisampled block-compressed surface is not implemented");
|
||||
}
|
||||
if (descriptor.dimension == TextureDimension::k3D && (descriptor.tileMode == TextureTileMode::kZ64KBX || descriptor.tileMode == TextureTileMode::kR64KBX)) {
|
||||
const auto depth = descriptor.depthOrLastArray + 1u;
|
||||
geometry.mips = ComputeMipLayout(descriptor.tileMode, descriptor.format, descriptor.width, descriptor.height, descriptor.mipCount);
|
||||
@@ -406,6 +413,29 @@ SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor) {
|
||||
geometry.layerBytes = thick.slabBytes;
|
||||
return geometry;
|
||||
}
|
||||
if (descriptor.dimension == TextureDimension::k2DMsaa && descriptor.tileMode != TextureTileMode::kLinear) {
|
||||
Require(descriptor.samples > 1u && std::has_single_bit(descriptor.samples), "multisampled texture has an invalid sample count");
|
||||
const auto bytesPerElement = BytesPerElement(descriptor.format);
|
||||
Require(std::has_single_bit(bytesPerElement), "multisampled texture format has an unsupported element size");
|
||||
const auto block = GetBlockLayout(descriptor.tileMode, bytesPerElement);
|
||||
Require(block.blockSize == 4096u || block.blockSize == 65536u, "multisampled surface requires 4 KiB or 64 KiB blocks");
|
||||
const auto log2Samples = static_cast<std::uint32_t>(std::countr_zero(descriptor.samples));
|
||||
const auto log2Pixels = static_cast<std::uint32_t>(std::countr_zero(block.blockSize)) - static_cast<std::uint32_t>(std::countr_zero(bytesPerElement)) - log2Samples;
|
||||
const auto log2Width = (log2Pixels + ((log2Samples & 1u) == 0u ? 1u : 0u)) / 2u;
|
||||
const auto blockWidth = 1u << log2Width;
|
||||
const auto blockHeight = 1u << (log2Pixels - log2Width);
|
||||
const auto align = [](std::uint32_t extent, std::uint32_t blockDimension) {
|
||||
return (static_cast<std::uint64_t>(extent) + blockDimension - 1u) / blockDimension * blockDimension;
|
||||
};
|
||||
geometry.guestBytes = align(descriptor.width, blockWidth) * align(descriptor.height, blockHeight) * bytesPerElement * descriptor.samples;
|
||||
geometry.layerBytes = geometry.guestBytes;
|
||||
geometry.layers = 1;
|
||||
geometry.imageLayers = 1;
|
||||
geometry.imageDepth = 1;
|
||||
geometry.sliceLinearBytes = static_cast<std::uint64_t>(descriptor.width) * descriptor.height * descriptor.samples * bytesPerElement;
|
||||
geometry.mips.push_back({0u, geometry.guestBytes, 0u, geometry.sliceLinearBytes, descriptor.width, descriptor.height, static_cast<std::uint32_t>(align(descriptor.width, blockWidth) / blockWidth), descriptor.width * descriptor.samples * bytesPerElement, false, 0u, 0u});
|
||||
return geometry;
|
||||
}
|
||||
geometry.mips = ComputeMipLayout(descriptor.tileMode, descriptor.format, descriptor.width, descriptor.height, descriptor.mipCount);
|
||||
const bool layered = descriptor.dimension == TextureDimension::k2DArray || descriptor.dimension == TextureDimension::k1DArray || descriptor.dimension == TextureDimension::kCube;
|
||||
geometry.layers = layered ? descriptor.depthOrLastArray + 1u : 1u;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <array>
|
||||
#include <initializer_list>
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -181,4 +182,8 @@ void RunBdaContractTests() {
|
||||
invalid[0] = invalid[0] == 'A' ? 'B' : 'A';
|
||||
reject([&] { static_cast<void>(serializer.Deserialize(invalid)); }, "request signature");
|
||||
reject([&] { static_cast<void>(serializer.Deserialize("NVNQQWMAAAA=")); }, "serialization version");
|
||||
const auto ordered = [](std::initializer_list<BdaAbi::Range> ranges) { return BdaAbi::RangesOrdered(ranges.begin(), ranges.size()); };
|
||||
AgcDriver::Graphics::Require(ordered({}) && ordered({{0x1000, 0x2000, 1, 1, 0}, {0x2000, 0x2000, 1, 1, 0}, {0x2000, 0x3000, 1, 1, 0}, {0x4000, 0x5000, 1, 1, 0}}), "ascending BDA ranges were refused");
|
||||
AgcDriver::Graphics::Require(ordered({{0x1000, 0x3000, 1, 1, 0}, {0x2f00, 0x3100, 1, 1, 0}}), "BDA ranges ascending by begin and overlapping were refused");
|
||||
AgcDriver::Graphics::Require(!ordered({{0x4000, 0x5000, 1, 1, 0}, {0x1000, 0x2000, 1, 1, 0}}), "unsorted BDA ranges were accepted");
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <cstring>
|
||||
#include <initializer_list>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
@@ -213,6 +214,170 @@ static void RunBdaDwordWriteTests(const Context& context) {
|
||||
write(guest + 0x7c, 2, 8, static_cast<Abi::FaultReason>(0), 64);
|
||||
}
|
||||
|
||||
static void RunBdaAnchorTests(const Context& context) {
|
||||
namespace Abi = ShaderRecompiler::BdaAbi;
|
||||
constexpr std::uint64_t guest = 0x7fff12340001ULL;
|
||||
constexpr std::uint64_t limit = 0xffffffff00000000ULL;
|
||||
constexpr std::uint64_t highest = 0xffffffffffffff00ULL;
|
||||
constexpr std::uint32_t lanes = 64;
|
||||
const auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT;
|
||||
Buffer first(context, 3, usage);
|
||||
Buffer second(context, 2, usage);
|
||||
Buffer words(context, 16, usage);
|
||||
Buffer shifted(context, 20, usage);
|
||||
Buffer large(context, 1024, usage);
|
||||
Buffer top(context, 16, usage);
|
||||
Buffer above(context, 16, usage);
|
||||
Buffer overlap(context, 0x200, usage);
|
||||
std::uint32_t seed = 0x11;
|
||||
for (auto* buffer : {&first, &second, &words, &shifted, &large, &top, &above, &overlap}) {
|
||||
for (std::size_t index = 0; index < buffer->Bytes().size(); ++index) buffer->Bytes()[index] = std::byte(static_cast<std::uint8_t>(seed + index * 7u));
|
||||
seed += 0x33;
|
||||
}
|
||||
for (std::uint32_t index = 0; index < 16u; ++index) words.Bytes()[index] = std::byte(static_cast<std::uint8_t>(0xa0u + index));
|
||||
for (std::uint32_t index = 0; index < 20u; ++index) shifted.Bytes()[index] = std::byte(static_cast<std::uint8_t>(0xc0u + index));
|
||||
std::vector<Abi::Range> ranges{{guest, guest + 3, first.DeviceAddress(), Abi::Read, 0},
|
||||
{guest + 3, guest + 5, second.DeviceAddress(), Abi::Read, 0},
|
||||
{guest + 0x100, guest + 0x110, words.DeviceAddress(), Abi::Read, 0},
|
||||
{guest + 0x200, guest + 0x210, shifted.DeviceAddress() + 1u, Abi::Read, 0},
|
||||
{guest + 0x400, guest + 0x800, large.DeviceAddress(), Abi::Read, 0},
|
||||
{guest + 0x700, guest + 0x900, overlap.DeviceAddress(), Abi::Read, 0},
|
||||
{limit - 0x10, limit, top.DeviceAddress(), Abi::Read, 0},
|
||||
{highest, highest + 0x10, above.DeviceAddress(), Abi::Read, 0}};
|
||||
Require(Abi::RangesOrdered(ranges.data(), ranges.size()), "the BDA anchor test table does not ascend by begin");
|
||||
Buffer table(context, sizeof(Abi::Header) + ranges.size() * sizeof(Abi::Range), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
|
||||
Buffer fault(context, sizeof(Abi::Fault), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
|
||||
Buffer output(context, (BdaAnchoredReadInputWord + 3u + lanes) * sizeof(std::uint32_t), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
|
||||
struct Outcome {
|
||||
Abi::Fault fault{};
|
||||
std::array<std::uint32_t, BdaAnchoredReadInputWord> values{};
|
||||
};
|
||||
std::uint32_t count = static_cast<std::uint32_t>(ranges.size());
|
||||
std::uint32_t permissions = Abi::Read;
|
||||
const auto run = [&](Pipeline& pipeline, std::uint32_t read, std::uint64_t anchor, const std::vector<std::uint32_t>& lows) {
|
||||
auto copy = ranges;
|
||||
if (copy.size() > 2u) copy[2].permissions = permissions;
|
||||
const Abi::Header header{Abi::Version, count, sizeof(Abi::Range), 0};
|
||||
std::memcpy(table.Bytes().data(), &header, sizeof(header));
|
||||
std::memcpy(table.Bytes().data() + sizeof(header), copy.data(), copy.size() * sizeof(Abi::Range));
|
||||
std::memset(fault.Bytes().data(), 0, fault.Bytes().size());
|
||||
std::memset(output.Bytes().data(), 0xa5, BdaAnchoredReadInputWord * sizeof(std::uint32_t));
|
||||
std::vector<std::uint32_t> inputs{static_cast<std::uint32_t>(anchor), static_cast<std::uint32_t>(anchor >> 32u), read};
|
||||
inputs.insert(inputs.end(), lows.begin(), lows.end());
|
||||
std::memcpy(output.Bytes().data() + BdaAnchoredReadInputWord * sizeof(std::uint32_t), inputs.data(), inputs.size() * sizeof(std::uint32_t));
|
||||
pipeline.Run(1);
|
||||
Outcome outcome;
|
||||
std::memcpy(&outcome.fault, fault.Bytes().data(), sizeof(outcome.fault));
|
||||
std::memcpy(outcome.values.data(), output.Bytes().data(), sizeof(outcome.values));
|
||||
return outcome;
|
||||
};
|
||||
struct Programs {
|
||||
Programs(const Context& context, const BdaAnchoredReadProgram& reads, const std::array<Buffer*, 3>& buffers) : reads(reads), plain(context, MakeBdaAnchoredReadTestShader(reads, false), buffers), anchored(context, MakeBdaAnchoredReadTestShader(reads, true), buffers) {}
|
||||
BdaAnchoredReadProgram reads;
|
||||
Pipeline plain;
|
||||
Pipeline anchored;
|
||||
};
|
||||
const auto build = [&](const BdaAnchoredReadProgram& reads) { return std::make_unique<Programs>(context, reads, std::array<Buffer*, 3>{&table, &fault, &output}); };
|
||||
const auto compare = [&](Programs& programs, std::initializer_list<std::uint64_t> anchors, const std::vector<std::uint32_t>& lows) {
|
||||
std::vector<std::vector<std::uint32_t>> batches;
|
||||
if (programs.reads.lanes == 1u) {
|
||||
for (const auto low : lows) batches.push_back({low});
|
||||
} else {
|
||||
batches.push_back(lows);
|
||||
batches.back().resize(programs.reads.lanes, lows.back());
|
||||
}
|
||||
for (std::uint32_t index = 0; index < programs.reads.reads.size(); ++index) {
|
||||
const auto& read = programs.reads.reads[index];
|
||||
for (const auto anchor : anchors) {
|
||||
for (const auto& batch : batches) {
|
||||
const auto expected = run(programs.plain, index, anchor, batch);
|
||||
const auto actual = run(programs.anchored, index, anchor, batch);
|
||||
const bool faultsMatch = programs.reads.lanes == 1u ? std::memcmp(&expected.fault, &actual.fault, sizeof(expected.fault)) == 0 : expected.fault.state == actual.fault.state;
|
||||
Require(faultsMatch && expected.values == actual.values, "an anchored BDA read differs from the same read through the lookups at anchor 0x" + std::to_string(anchor) + " + " + std::to_string(batch[0]) + " + (" + std::to_string(read.immediate) + "), " + std::to_string(read.bits) + " bits, " + std::to_string(read.dwords) + " dwords");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
std::vector<BdaAnchoredRead> reads;
|
||||
for (const std::int32_t immediate : {0, 4, -1, -8, 0x7ffffff0, std::numeric_limits<std::int32_t>::min()}) {
|
||||
for (const auto& [bits, dwords, used] : std::initializer_list<std::array<std::uint32_t, 3>>{{8, 1, 1}, {16, 1, 1}, {32, 1, 1}, {32, 4, 15}, {32, 4, 10}, {32, 3, 4}}) reads.push_back({immediate, bits, dwords, used});
|
||||
}
|
||||
const auto anchors = {guest + 0x100, guest + 0x108, guest, guest + 3, guest + 0x200, guest - 1, guest + 0x110, guest + 0x400, guest + 0x6f0, guest + 0x700, guest + 0x800, limit - 0x10, limit - 4, highest, highest + 8, std::numeric_limits<std::uint64_t>::max() - 2};
|
||||
std::vector<std::uint32_t> lows{0u, 1u, 2u, 3u, 4u, 8u, 0xcu, 0xdu, 0xeu, 0xfu, 0x10u, 0x14u, 0x100u, 0x108u, 0x10cu, 0x1fcu, 0x200u, 0x20eu, 0x3f0u, 0x3fcu, 0x400u, 0x7fffu, 0x7fffffffu, 0x80000000u, 0xfffffff0u, 0xfffffffcu, 0xffffffffu};
|
||||
for (std::uint32_t lane = 0; lane < 32u; ++lane) lows.push_back(lane * 0x24u);
|
||||
std::vector<std::uint32_t> strided;
|
||||
for (std::uint32_t lane = 0; lane < lanes; ++lane) strided.push_back(lane * 16u);
|
||||
auto continuing = build({reads, false, false, false, lanes});
|
||||
compare(*continuing, anchors, lows);
|
||||
compare(*continuing, {guest + 0x400}, strided);
|
||||
auto elected = build({reads, false, true, false, lanes});
|
||||
compare(*elected, anchors, lows);
|
||||
compare(*elected, {guest + 0x400}, strided);
|
||||
auto coherent = build({{reads[0], reads[2], reads[9], reads[16], reads[22]}, false, false, true, lanes});
|
||||
compare(*coherent, anchors, lows);
|
||||
auto stopping = build({reads, true, true, false, 1});
|
||||
compare(*stopping, anchors, {0u, 0x80000000u, 0xfffffffcu});
|
||||
const auto check = [&](std::uint32_t read, std::uint64_t anchor, std::uint32_t low) { return run(stopping->anchored, read, anchor, {low}); };
|
||||
auto direct = std::make_unique<Pipeline>(context, MakeBdaAnchoredReadTestShader({reads, true, false, false, 1, true}, true), std::array<Buffer*, 3>{&table, &fault, &output});
|
||||
const auto dword = [](Buffer& buffer, std::size_t offset) {
|
||||
std::uint32_t value = 0;
|
||||
std::memcpy(&value, buffer.Bytes().data() + offset, sizeof(value));
|
||||
return value;
|
||||
};
|
||||
const auto loads = [&](std::uint32_t read, std::uint64_t anchor, std::uint32_t low, std::initializer_list<std::uint32_t> values) {
|
||||
const auto outcome = run(*direct, read, anchor, {low});
|
||||
Require(outcome.fault.state == Abi::FaultState::Empty && std::equal(values.begin(), values.end(), outcome.values.begin()), "a read the anchor holds did not load directly at anchor 0x" + std::to_string(anchor) + " + " + std::to_string(low) + ", read " + std::to_string(read));
|
||||
};
|
||||
const auto misses = [&](std::uint32_t read, std::uint64_t anchor, std::uint32_t low) {
|
||||
const auto outcome = run(*direct, read, anchor, {low});
|
||||
Require(outcome.fault.state == Abi::FaultState::Empty && outcome.values[0] == BdaAnchoredReadSentinel, "a read the anchor does not hold did not take the lookup path at anchor 0x" + std::to_string(anchor) + " + " + std::to_string(low) + ", read " + std::to_string(read));
|
||||
};
|
||||
loads(2, guest + 0x100, 0, {dword(words, 0)});
|
||||
loads(2, guest + 0x100, 0xc, {dword(words, 0xc)});
|
||||
loads(3, guest + 0x100, 0, {dword(words, 0), dword(words, 4), dword(words, 8), dword(words, 0xc)});
|
||||
loads(0, guest + 0x200, 0xd, {0xceu});
|
||||
loads(0, guest + 3, 1, {static_cast<std::uint32_t>(second.Bytes()[1])});
|
||||
loads(1, guest, 1, {static_cast<std::uint32_t>(first.Bytes()[1]) | static_cast<std::uint32_t>(first.Bytes()[2]) << 8u});
|
||||
loads(2, guest + 0x400, 0x2fc, {dword(large, 0x2fc)});
|
||||
loads(2, guest + 0x700, 0x1fc, {dword(overlap, 0x1fc)});
|
||||
loads(2, limit - 0x10, 0xc, {dword(top, 0xc)});
|
||||
misses(2, guest + 0x100, 0xd);
|
||||
misses(3, guest + 0x100, 4);
|
||||
misses(2, guest + 0x200, 0);
|
||||
misses(1, guest + 2, 0);
|
||||
misses(0, guest + 3, 2);
|
||||
misses(2, guest + 0x400, 0x300);
|
||||
misses(2, guest + 0x400, 0x2fd);
|
||||
misses(2, limit - 0x10, 0xd);
|
||||
misses(2, highest, 0);
|
||||
misses(2, guest - 1, 1);
|
||||
const auto wide = check(3, guest + 0x100, 0);
|
||||
Require(wide.fault.state == Abi::FaultState::Empty && wide.values[0] == 0xa3a2a1a0u && wide.values[1] == 0xa7a6a5a4u && wide.values[2] == 0xabaaa9a8u && wide.values[3] == 0xafaeadacu, "an anchored span read returned wrong data");
|
||||
const auto some = check(4, guest + 0x100, 0);
|
||||
Require(some.fault.state == Abi::FaultState::Empty && some.values[0] == 0u && some.values[1] == 0xa7a6a5a4u && some.values[2] == 0u && some.values[3] == 0xafaeadacu, "an anchored read of some dwords returned wrong data");
|
||||
const auto odd = check(2, guest + 0x200, 0);
|
||||
Require(odd.fault.state == Abi::FaultState::Empty && odd.values[0] == 0xc4c3c2c1u, "an anchored read of an odd device address returned wrong data");
|
||||
const auto byte = check(0, guest + 0x200, 0xd);
|
||||
Require(byte.fault.state == Abi::FaultState::Empty && byte.values[0] == 0xceu, "an anchored byte read returned wrong data");
|
||||
const auto below = check(20, guest + 0x108, 4);
|
||||
Require(below.fault.state == Abi::FaultState::Empty && below.values[0] == 0xa7a6a5a4u, "an anchored read below its base returned wrong data");
|
||||
const auto unmapped = check(2, guest + 0x100, 0x10);
|
||||
Require(unmapped.fault.state == Abi::FaultState::Ready && unmapped.fault.reason == Abi::FaultReason::Unmapped && unmapped.fault.address == guest + 0x110 && unmapped.fault.instruction == 0x1234 && unmapped.values[0] == 0xa5a5a5a5u, "an anchored read past its range did not fault");
|
||||
permissions = 0;
|
||||
misses(2, guest + 0x100, 0);
|
||||
compare(*stopping, {guest + 0x100}, {0u, 4u});
|
||||
compare(*elected, {guest + 0x100}, lows);
|
||||
permissions = Abi::Read;
|
||||
count = 3;
|
||||
misses(2, guest + 0x100, 0);
|
||||
misses(0, guest, 0);
|
||||
compare(*stopping, {guest + 0x100, guest}, {0u, 4u});
|
||||
compare(*elected, {guest + 0x100, guest}, lows);
|
||||
ranges = {{0, limit, large.DeviceAddress(), Abi::Read, 0}};
|
||||
count = 1;
|
||||
compare(*stopping, {limit - 1}, {0xffffffffu});
|
||||
}
|
||||
|
||||
void RunBdaExecutionTests(const Context& context) {
|
||||
namespace Abi = ShaderRecompiler::BdaAbi;
|
||||
Buffer first(context, 3, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT);
|
||||
@@ -333,6 +498,7 @@ void RunBdaExecutionTests(const Context& context) {
|
||||
ranges[0].permissions = 0;
|
||||
run(guest, 8, 0, Abi::FaultReason::Permission);
|
||||
RunBdaDwordWriteTests(context);
|
||||
RunBdaAnchorTests(context);
|
||||
}
|
||||
|
||||
void RunBdaStoreExecutionTests(const Context& context) {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <new>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -739,6 +740,151 @@ void importWholeRangeOverWindowTests(const Context& context) {
|
||||
leased.WriteBack();
|
||||
}
|
||||
|
||||
struct ObservedHostImport {
|
||||
const void* pointer;
|
||||
VkDeviceSize bytes;
|
||||
};
|
||||
|
||||
class ImportAllocationProbe {
|
||||
public:
|
||||
explicit ImportAllocationProbe(Context& context) : context(context), original(context.deviceProc) {
|
||||
Require(active == nullptr && original != nullptr, "invalid import allocation probe scope");
|
||||
active = this;
|
||||
context.deviceProc = Resolve;
|
||||
}
|
||||
~ImportAllocationProbe() {
|
||||
context.deviceProc = original;
|
||||
active = nullptr;
|
||||
}
|
||||
ImportAllocationProbe(const ImportAllocationProbe&) = delete;
|
||||
ImportAllocationProbe& operator=(const ImportAllocationProbe&) = delete;
|
||||
|
||||
std::vector<ObservedHostImport> calls;
|
||||
|
||||
private:
|
||||
static PFN_vkVoidFunction VKAPI_CALL Resolve(VkDevice device, const char* name) {
|
||||
Require(active != nullptr, "import allocation probe escaped its scope");
|
||||
if (std::strcmp(name, "vkAllocateMemory") == 0) return reinterpret_cast<PFN_vkVoidFunction>(Allocate);
|
||||
return active->original(device, name);
|
||||
}
|
||||
static VkResult VKAPI_CALL Allocate(VkDevice device, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks* allocator, VkDeviceMemory* memory) {
|
||||
Require(active != nullptr, "import allocation callback escaped its scope");
|
||||
for (const auto* next = static_cast<const VkBaseInStructure*>(info->pNext); next != nullptr; next = next->pNext) {
|
||||
if (next->sType != VK_STRUCTURE_TYPE_IMPORT_MEMORY_HOST_POINTER_INFO_EXT) continue;
|
||||
const auto* imported = reinterpret_cast<const VkImportMemoryHostPointerInfoEXT*>(next);
|
||||
active->calls.push_back({imported->pHostPointer, info->allocationSize});
|
||||
}
|
||||
const auto allocate = reinterpret_cast<PFN_vkAllocateMemory>(active->original(device, "vkAllocateMemory"));
|
||||
Require(allocate != nullptr, "import allocation probe has no original allocator");
|
||||
return allocate(device, info, allocator, memory);
|
||||
}
|
||||
|
||||
Context& context;
|
||||
PFN_vkGetDeviceProcAddr original;
|
||||
inline static thread_local ImportAllocationProbe* active = nullptr;
|
||||
};
|
||||
|
||||
class UnrelatedImportProbeRange {
|
||||
public:
|
||||
UnrelatedImportProbeRange() {
|
||||
GuestAllocations::Mutation().Add(storage.data(), storage.size(), true, false, false);
|
||||
}
|
||||
~UnrelatedImportProbeRange() {
|
||||
GuestAllocations::Mutation().Remove(storage.data());
|
||||
}
|
||||
UnrelatedImportProbeRange(const UnrelatedImportProbeRange&) = delete;
|
||||
UnrelatedImportProbeRange& operator=(const UnrelatedImportProbeRange&) = delete;
|
||||
|
||||
private:
|
||||
std::array<std::byte, 64> storage{};
|
||||
};
|
||||
|
||||
void importStaleProbeTests(const Context& context, const BdaTestAccess& access) {
|
||||
for (const auto scenario : {0u, 1u, 2u}) {
|
||||
const bool held = scenario == 1u;
|
||||
const bool exactWhole = scenario == 2u;
|
||||
WindowedRange range(context, 2 * WindowedRange::piece, 2);
|
||||
ImportAllocationProbe probe(range.importing);
|
||||
const auto first = range.First();
|
||||
auto* guest = reinterpret_cast<std::byte*>(first);
|
||||
for (std::size_t offset = 17u; offset < range.bytes; offset += 4096u) guest[offset] = static_cast<std::byte>((offset / 4096u) & 0xffu);
|
||||
const std::vector<std::byte> originalGuest(guest, guest + range.bytes);
|
||||
const auto* window = HostImportFor(range.importing, first, exactWhole ? range.bytes : 32u);
|
||||
Require(window != nullptr && !window->joined && window->base == first && window->bytes == (exactWhole ? range.bytes : range.window), "the stale-probe fixture did not create its ordinary import extent");
|
||||
const auto originalBuffer = window->buffer;
|
||||
const auto originalUse = window->lastUse;
|
||||
const auto originalSerial = HostImportSerial(range.importing, first, 32u, false);
|
||||
auto mockContents = access.bytes(window->buffer);
|
||||
Require(mockContents.size() == window->bytes, "the mock import buffer has an incorrect extent");
|
||||
std::copy_n(originalGuest.begin(), window->bytes, mockContents.begin());
|
||||
std::uint32_t scratch = 0x12345678u;
|
||||
{
|
||||
GuestBufferMemory copied(range.importing);
|
||||
copied.AddReadable(reinterpret_cast<std::uintptr_t>(&scratch), sizeof(scratch));
|
||||
copied.Upload(false);
|
||||
copied.WriteBack();
|
||||
}
|
||||
Require(window->lastUse == originalUse && HostImportSerial(range.importing, first, 32u, false) == originalSerial, "an unrelated copied build touched or replaced the prefix window");
|
||||
GuestBufferMemory reader(range.importing);
|
||||
VkDescriptorBufferInfo heldView{};
|
||||
std::uint32_t heldAdjustment = 0u;
|
||||
if (held) {
|
||||
reader.AddReadable(first + 16u, 32u);
|
||||
reader.Upload(false);
|
||||
heldView = reader.Descriptor(first + 16u, 32u, heldAdjustment);
|
||||
Require(heldView.buffer == originalBuffer, "the held control did not bind the prefix window");
|
||||
}
|
||||
GuestAllocations::Mapped mapped;
|
||||
{
|
||||
const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix();
|
||||
for (const auto& owner : lease) if (owner->address == first && owner->bytes == range.bytes) mapped.push_back(owner);
|
||||
}
|
||||
Require(mapped.size() == 1u, "the stale-probe fixture lost its registered range");
|
||||
const auto originalOwner = mapped.front().lock();
|
||||
const auto generation = GuestAllocations::GuestAllocationsGeneration_nid_postfix();
|
||||
UnrelatedImportProbeRange unrelated;
|
||||
Require(GuestAllocations::GuestAllocationsGeneration_nid_postfix() != generation && HostImportsIdentity(range.importing).device == VK_NULL_HANDLE,
|
||||
"the unrelated registry change did not make imports stale");
|
||||
const auto attempts = access.allocationAttempts();
|
||||
probe.calls.clear();
|
||||
{
|
||||
std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex());
|
||||
Require(ImportMappedRanges(range.importing, mapped, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), false), "the current device did not admit the map-time import pass");
|
||||
}
|
||||
Require(mapped.front().lock() == originalOwner, "an unrelated registry change replaced the import's memory owner");
|
||||
if (held) {
|
||||
Require(access.allocationAttempts() == attempts && probe.calls.empty(), "a stale probe retired an import held by an actual descriptor");
|
||||
const auto* kept = HostImportFor(range.importing, first + 16u, 32u);
|
||||
Require(kept != nullptr && kept->buffer == originalBuffer && kept->bytes == range.window && HostImportSerial(range.importing, first, 32u, false) == originalSerial,
|
||||
"the actual held import lost its buffer, extent or serial");
|
||||
std::uint32_t adjustment = 0u;
|
||||
const auto view = reader.Descriptor(first + 16u, 32u, adjustment);
|
||||
Require(view.buffer == heldView.buffer && view.offset == heldView.offset && view.range == heldView.range && adjustment == heldAdjustment,
|
||||
"a map-time stale probe invalidated a descriptor that still uses its import");
|
||||
const auto contents = access.bytes(view.buffer);
|
||||
Require(view.offset + adjustment + 32u <= contents.size() && std::memcmp(contents.data() + view.offset + adjustment, reinterpret_cast<const void*>(first + 16u), 32u) == 0,
|
||||
"the protected descriptor no longer reads the registered bytes");
|
||||
reader.WriteBack();
|
||||
} else if (exactWhole) {
|
||||
Require(access.allocationAttempts() == attempts && probe.calls.empty() && window->lastUse > originalUse,
|
||||
"accepting the stale exact whole import reallocated it or did not protect its returned pointer");
|
||||
const auto* kept = HostImportFor(range.importing, first, range.bytes);
|
||||
Require(kept != nullptr && kept->buffer == originalBuffer && kept->ranges.size() == 1u && kept->ranges.front() == originalOwner && HostImportSerial(range.importing, first, range.bytes, false) == originalSerial,
|
||||
"accepting the exact whole import changed its native or registered identity");
|
||||
} else {
|
||||
Require(access.allocationAttempts() == attempts + 1u, "a discarded stale probe made an unheld prefix window block the whole import");
|
||||
const auto* whole = HostImportFor(range.importing, first, range.bytes);
|
||||
Require(whole != nullptr && whole->base == first && whole->bytes == range.bytes && whole->ranges.size() == 1u && whole->ranges.front() == originalOwner,
|
||||
"the stale-probe repair did not retain the exact registered owner over the whole import");
|
||||
Require(HostImportSerial(range.importing, first, range.bytes, false) != originalSerial, "replacing the unheld window reused its old import identity");
|
||||
Require(probe.calls.size() == 1u && probe.calls.front().pointer == reinterpret_cast<const void*>(first) && probe.calls.front().bytes == range.bytes,
|
||||
"the replacement native import did not reference the exact whole registered memory");
|
||||
Require(access.bytes(whole->buffer).size() == range.bytes, "the replacement import buffer does not cover its registered extent");
|
||||
}
|
||||
Require(scratch == 0x12345678u && std::equal(originalGuest.begin(), originalGuest.end(), guest), "the import probe changed guest bytes or unrelated copied data");
|
||||
}
|
||||
}
|
||||
|
||||
// An address-based build whose whole-range import does not fit the budget leaves the range's window alone: before,
|
||||
// every such build retired the window and the next access imported it again.
|
||||
void importRefusedWholeRangeTests(const Context& context) {
|
||||
@@ -2068,6 +2214,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) {
|
||||
importCoverageHintBudgetTests(context, access);
|
||||
failedImportJoinProbeBudgetTests(context, access);
|
||||
importWholeRangeOverWindowTests(context);
|
||||
importStaleProbeTests(context, access);
|
||||
importRefusedWholeRangeTests(context);
|
||||
importNeighbouringEdgesTests(context);
|
||||
importAtMapOverBudgetTests(context);
|
||||
|
||||
@@ -133,3 +133,94 @@ std::vector<std::uint32_t> MakeBdaWriteTestShader(std::uint64_t address, std::ui
|
||||
#endif
|
||||
return shader;
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> MakeBdaAnchoredReadTestShader(const BdaAnchoredReadProgram& reads, bool anchored) {
|
||||
using namespace ShaderRecompiler;
|
||||
IrProgram program;
|
||||
program.Resources().stage = IrShaderStage::Compute;
|
||||
program.Info().usesDma = true;
|
||||
if (reads.coherent) {
|
||||
MemoryInfo memory;
|
||||
memory.coherent = true;
|
||||
program.Resources().memoryInfo = {memory};
|
||||
}
|
||||
SpirvEmitterState state(program, {});
|
||||
EmitBaseHeader(state.module, program);
|
||||
if (reads.elect) {
|
||||
state.module.EmitCapability(spv::CapabilityGroupNonUniform);
|
||||
state.module.EmitCapability(spv::CapabilityGroupNonUniformBallot);
|
||||
state.requirements.subgroupBallot = true;
|
||||
}
|
||||
const auto define = [&](std::uint32_t binding) {
|
||||
const auto variable = state.module.DefineGlobalVariable(TypeStorageBufferPointer(state), spv::StorageClassStorageBuffer);
|
||||
state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationDescriptorSet, 0u);
|
||||
state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationBinding, binding);
|
||||
return variable;
|
||||
};
|
||||
state.bdaPagetableVariable = define(0);
|
||||
state.faultBufferVariable = define(1);
|
||||
const auto output = define(2);
|
||||
std::vector<std::uint32_t> interfaceVariables{state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassInput, TypeU32(state)), spv::StorageClassInput)};
|
||||
state.module.AddAnnotation(spv::OpDecorate, interfaceVariables[0], spv::DecorationBuiltIn, spv::BuiltInLocalInvocationIndex);
|
||||
state.bdaStopsInvocations = reads.stops;
|
||||
DefineGetBdaPointer(state);
|
||||
if (anchored) DefineBdaAnchorFunction(state);
|
||||
const auto main = state.module.AllocateId();
|
||||
state.module.AddFunction(spv::OpFunction, TypeVoid(state), main, spv::FunctionControlMaskNone, TypeFunction(state));
|
||||
EmitLabel(state, state.module.AllocateId());
|
||||
SpirvValueEmitContext ctx(state);
|
||||
const auto u32 = TypeU32(state);
|
||||
const auto u64 = TypeScalarU64(state);
|
||||
const auto input = [&](std::uint32_t word) {
|
||||
const auto value = state.module.AllocateId();
|
||||
state.module.AddFunction(spv::OpLoad, u32, value, BdaWord(state, output, word));
|
||||
return value;
|
||||
};
|
||||
const auto base = Binary(state, spv::OpBitwiseOr, u64, Unary(state, spv::OpUConvert, u64, input(ConstantU32(state, BdaAnchoredReadInputWord))), Binary(state, spv::OpShiftLeftLogical, u64, Unary(state, spv::OpUConvert, u64, input(ConstantU32(state, BdaAnchoredReadInputWord + 1u))), BdaConstant(state, 32u)));
|
||||
SpirvEmitterState::BdaAnchor anchor{};
|
||||
if (anchored) EmitBdaAnchorRange(state, anchor, base);
|
||||
const auto selector = input(ConstantU32(state, BdaAnchoredReadInputWord + 2u));
|
||||
const auto laneIndex = state.module.AllocateId();
|
||||
state.module.AddFunction(spv::OpLoad, u32, laneIndex, interfaceVariables[0]);
|
||||
const auto low = input(Binary(state, spv::OpIAdd, u32, laneIndex, ConstantU32(state, BdaAnchoredReadInputWord + 3u)));
|
||||
const auto slot = Binary(state, spv::OpIMul, u32, laneIndex, ConstantU32(state, 4u));
|
||||
IrBuilder ir(program);
|
||||
for (std::uint32_t index = 0; index < reads.reads.size(); ++index) {
|
||||
const auto& read = reads.reads[index];
|
||||
const auto opcode = read.bits == 8u ? IrOpcode::LoadAddressU8 : read.bits == 16u ? IrOpcode::LoadAddressU16 : read.dwords == 2u ? IrOpcode::LoadAddressU32x2 : read.dwords == 3u ? IrOpcode::LoadAddressU32x3 : read.dwords == 4u ? IrOpcode::LoadAddressU32x4 : IrOpcode::LoadAddressU32;
|
||||
const auto type = read.bits != 32u || read.dwords == 1u ? IrType::U32 : read.dwords == 2u ? IrType::U32x2 : read.dwords == 3u ? IrType::U32x3 : IrType::U32x4;
|
||||
auto& instruction = program.CreateValue(opcode, type);
|
||||
MemoryFlags flags{};
|
||||
flags.pc = 0x1234;
|
||||
instruction.SetFlags(flags);
|
||||
if (read.bits == 32u && read.dwords > 1u) {
|
||||
const auto extract = read.dwords == 2u ? IrOpcode::CompositeExtractU32x2 : read.dwords == 3u ? IrOpcode::CompositeExtractU32x3 : IrOpcode::CompositeExtractU32x4;
|
||||
for (std::uint32_t dword = 0; dword < read.dwords; ++dword) {
|
||||
if ((read.used & (1u << dword)) == 0u) continue;
|
||||
auto& user = program.CreateValue(extract, IrType::U32);
|
||||
user.AddArgument(&instruction);
|
||||
user.AddArgument(&ir.Constant(dword));
|
||||
}
|
||||
}
|
||||
EmitIfCondition(state, Binary(state, spv::OpIEqual, TypeBool(state), selector, ConstantU32(state, index)), [&] {
|
||||
const auto lookup = [&] {
|
||||
if (reads.sentinel) {
|
||||
const auto sentinel = ConstantU32(state, BdaAnchoredReadSentinel);
|
||||
return std::array<std::uint32_t, 4>{sentinel, sentinel, sentinel, sentinel};
|
||||
}
|
||||
const auto address = AddBdaAddress(ctx, instruction, base, Unary(state, spv::OpUConvert, u64, low), false);
|
||||
if (read.bits == 32u) return EmitBdaDwordReads(ctx, instruction, address, static_cast<std::uint32_t>(read.immediate), read.dwords);
|
||||
return std::array<std::uint32_t, 4>{EmitBdaRead(ctx, instruction, AddBdaImmediate(ctx, instruction, address, read.immediate), read.bits)};
|
||||
};
|
||||
const auto values = anchored ? EmitAnchoredBdaRead(state, instruction, anchor, low, read.immediate, read.bits, read.dwords, 0u, lookup) : lookup();
|
||||
for (std::uint32_t dword = 0; dword < (read.bits == 32u ? read.dwords : 1u); ++dword) {
|
||||
state.module.AddFunction(spv::OpStore, BdaWord(state, output, Binary(state, spv::OpIAdd, u32, slot, ConstantU32(state, dword))), values[dword]);
|
||||
}
|
||||
});
|
||||
}
|
||||
state.module.AddFunction(spv::OpReturn);
|
||||
state.module.AddFunction(spv::OpFunctionEnd);
|
||||
state.module.AddExecutionMode(main, spv::ExecutionModeLocalSize, reads.lanes, 1u, 1u);
|
||||
state.module.EmitEntryPoint(spv::ExecutionModelGLCompute, main, "main", interfaceVariables);
|
||||
return state.module.Finalize();
|
||||
}
|
||||
@@ -11,6 +11,23 @@ std::vector<std::uint32_t> MakeBdaSpanReadTestShader(std::uint64_t address, std:
|
||||
std::vector<std::uint32_t> MakeBdaDwordWriteTestShader(std::uint64_t address, std::uint32_t dwords, const std::uint32_t* values);
|
||||
std::vector<std::uint32_t> MakeBdaStoreTestShader(std::uint64_t address, std::uint32_t value, std::uint32_t bits, bool coherent);
|
||||
std::vector<std::uint32_t> MakeBdaWriteTestShader(std::uint64_t address, std::uint32_t value, std::uint32_t bits, bool coherent);
|
||||
inline constexpr std::uint32_t BdaAnchoredReadInputWord = 256;
|
||||
struct BdaAnchoredRead {
|
||||
std::int32_t immediate = 0;
|
||||
std::uint32_t bits = 32;
|
||||
std::uint32_t dwords = 1;
|
||||
std::uint32_t used = 1;
|
||||
};
|
||||
struct BdaAnchoredReadProgram {
|
||||
std::vector<BdaAnchoredRead> reads;
|
||||
bool stops = true;
|
||||
bool elect = false;
|
||||
bool coherent = false;
|
||||
std::uint32_t lanes = 1;
|
||||
bool sentinel = false;
|
||||
};
|
||||
inline constexpr std::uint32_t BdaAnchoredReadSentinel = 0xdeadbeefu;
|
||||
std::vector<std::uint32_t> MakeBdaAnchoredReadTestShader(const BdaAnchoredReadProgram& reads, bool anchored);
|
||||
void RunBdaStoreExecutionTests(const AgcDriver::Graphics::Context& context);
|
||||
void RunBdaExecutionTests(const AgcDriver::Graphics::Context& context);
|
||||
void RunBdaContractTests();
|
||||
|
||||
@@ -995,6 +995,29 @@ void ShaderStageTests() {
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register");
|
||||
}
|
||||
|
||||
void UnsupportedTessellationFieldTests() {
|
||||
auto queue = makeState();
|
||||
queue.context[0x2d5] = 0x0200210d;
|
||||
queue.userConfig[0x242] = 9;
|
||||
queue.userConfig[0x25b] = 0x1fe55;
|
||||
queue.context[0x286] = 0x42800000;
|
||||
queue.context[0x287] = 0x3f800000;
|
||||
for (const auto [config, parameters, fields] : {
|
||||
std::tuple{0xc355u, 0x40045u, "VGT_TF_PARAM=0x40045 (TYPE=1 TESS_TRIANGLE, PARTITIONING=1 PART_POW2, TOPOLOGY=2 OUTPUT_TRIANGLE_CW), VGT_LS_HS_CONFIG=0xc355 (NUM_PATCHES=85, HS_NUM_INPUT_CP=3, HS_NUM_OUTPUT_CP=3)"},
|
||||
std::tuple{0x1033fu, 0x40066u, "VGT_TF_PARAM=0x40066 (TYPE=2 TESS_QUAD, PARTITIONING=1 PART_POW2, TOPOLOGY=3 OUTPUT_TRIANGLE_CCW), VGT_LS_HS_CONFIG=0x1033f (NUM_PATCHES=63, HS_NUM_INPUT_CP=3, HS_NUM_OUTPUT_CP=4)"},
|
||||
std::tuple{0xc355u, 0x40048u, "VGT_TF_PARAM=0x40048 (TYPE=0 TESS_ISOLINE, PARTITIONING=2 PART_FRAC_ODD, TOPOLOGY=2 OUTPUT_TRIANGLE_CW)"},
|
||||
std::tuple{0xc355u, 0x40029u, "(TYPE=1 TESS_TRIANGLE, PARTITIONING=2 PART_FRAC_ODD, TOPOLOGY=1 OUTPUT_LINE)"},
|
||||
std::tuple{0xc355u, 0x4000du, "(TYPE=1 TESS_TRIANGLE, PARTITIONING=3 PART_FRAC_EVEN, TOPOLOGY=0 OUTPUT_POINT)"},
|
||||
std::tuple{0xc355u, 0x4004bu, "(TYPE=3, PARTITIONING=2 PART_FRAC_ODD, TOPOLOGY=2 OUTPUT_TRIANGLE_CW)"},
|
||||
std::tuple{0xc355u, 0x400f1u, "(TYPE=1 TESS_TRIANGLE, PARTITIONING=4, TOPOLOGY=7)"}}) {
|
||||
queue.context[0x2d6] = config;
|
||||
queue.context[0x2db] = parameters;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "VGT_SHADER_STAGES_EN=0x200210d: VGT_TF_PARAM=0x");
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, fields);
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "): only triangle and quad domains with integer, fractional-even or fractional-odd partitioning and triangle output are supported by the reference path");
|
||||
}
|
||||
}
|
||||
|
||||
void NggVertexWorkgroupTests() {
|
||||
auto queue = makeState();
|
||||
queue.context[0x2d5] = 0x12010;
|
||||
@@ -2764,6 +2787,17 @@ void cmaskTests() {
|
||||
Require(texels(0x5a5a5a5au), "a refused pass changed the texels of a DCC target");
|
||||
}
|
||||
|
||||
void uint8x2TargetTests() {
|
||||
auto queue = makeState();
|
||||
queue.context[0x1c5] = 7;
|
||||
queue.context[0x31c] = (queue.context[0x31c] & ~0x1ffcu) | (3u << 2u) | (4u << 8u);
|
||||
const auto state = AgcDriver::Graphics::DecodeState(queue);
|
||||
Require(state.color.format == VK_FORMAT_R8G8_UINT && state.color.elementBytes == 2 && state.color.componentMapping == 0xe4u && state.color.uintExport, "an 8_8 UINT color target was rejected");
|
||||
queue.context[0x31c] = (queue.context[0x31c] & ~0x1800u) | (1u << 11u);
|
||||
const auto alt = AgcDriver::Graphics::DecodeState(queue);
|
||||
Require(alt.color.format == VK_FORMAT_R8G8_UINT && alt.color.componentMapping == 0xecu, "an 8_8 UINT SWAP_ALT color target was rejected");
|
||||
}
|
||||
|
||||
void uint8x4TargetTests() {
|
||||
auto queue = makeState();
|
||||
queue.context[0x1c5] = 7;
|
||||
@@ -2805,6 +2839,78 @@ VKAPI_ATTR VkResult VKAPI_CALL sampleLocationDepthProperties(VkPhysicalDevice, V
|
||||
return depthSampleResult;
|
||||
}
|
||||
|
||||
void msaaEnableClearDepthTests() {
|
||||
using AgcDriver::Graphics::DecodeState;
|
||||
const auto drawB = [] {
|
||||
auto queue = makeState();
|
||||
queue.context[0x2f8] = 0x0020c002;
|
||||
queue.context[0x292] = 2;
|
||||
for (std::uint32_t offset = 0x2fe; offset <= 0x30d; ++offset) queue.context[offset] = (offset - 0x2feu) % 4u == 0 ? 0x622ae6aeu : 0u;
|
||||
queue.context[0x31c] = 0x8028;
|
||||
queue.context[0x31d] = 0x12000;
|
||||
queue.context[0x3b8] = 0x0dc00000u;
|
||||
queue.context[0x1b3] = 2;
|
||||
queue.context[0x1b4] = 2;
|
||||
queue.context[0x000] = 0;
|
||||
queue.context[0x002] = 0;
|
||||
queue.context[0x007] = (3u << 16u) | 63u;
|
||||
queue.context[0x00a] = 0;
|
||||
queue.context[0x00b] = 0;
|
||||
queue.context[0x010] = 0xa290098b;
|
||||
queue.context[0x011] = 0x20000180;
|
||||
for (const auto offset : {0x012u, 0x014u}) queue.context[offset] = 0x100;
|
||||
queue.context[0x200] = 0x16;
|
||||
queue.context[0x201] = 0x00010002;
|
||||
return queue;
|
||||
};
|
||||
const auto requireCentered = [](const AgcDriver::Graphics::State& state, const std::string& what) {
|
||||
Require(state.samples == 4 && state.depth && state.depth->samples == 4 && state.customSampleLocations, what + " did not keep the 4x depth target");
|
||||
const auto locations = AgcDriver::Graphics::SampleLocations(state);
|
||||
for (std::uint32_t sample = 0; sample < state.samples; ++sample) Require(locations[sample].x == 0.5f && locations[sample].y == 0.5f, what + " did not place every sample at the pixel center");
|
||||
};
|
||||
auto queue = drawB();
|
||||
queue.context[0x200] = 0x711;
|
||||
queue.context[0x011] = 0x20000181;
|
||||
for (const auto offset : {0x013u, 0x015u}) queue.context[offset] = 0x200;
|
||||
queue.context[0x10b] = 0x333;
|
||||
queue.context[0x10c] = 0x00ff0000;
|
||||
queue.context[0x10d] = 0;
|
||||
auto state = DecodeState(queue);
|
||||
Require(state.colors.size() == 1 && state.colors[0].samples == 4, "PPSA18216's draw B lost its 4x color target");
|
||||
Require(state.stencilTest && !state.depthTest && state.depth && state.depth->format == VK_FORMAT_D32_SFLOAT_S8_UINT && state.stencilFront.compareOp == VK_COMPARE_OP_ALWAYS, "PPSA18216's draw B (DB_DEPTH_CONTROL 0x711) lost its 4x stencil plane");
|
||||
Require(state.stencilFront.passOp == VK_STENCIL_OP_REPLACE && state.stencilFront.failOp == VK_STENCIL_OP_REPLACE && state.stencilFront.depthFailOp == VK_STENCIL_OP_REPLACE && state.stencilFront.writeMask == 0xffu && state.stencilFront.reference == 0u, "PPSA18216's draw B did not decode as a stencil clear to 0");
|
||||
requireCentered(state, "a 4x depth and stencil target rasterized without MSAA_ENABLE (PPSA18216's draw B)");
|
||||
Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck refused PPSA18216's draw B");
|
||||
queue.context[0x200] = 0x710;
|
||||
state = DecodeState(queue);
|
||||
Require(!state.depth && state.samples == 4 && state.colors.size() == 1, "draw B with DB_DEPTH_CONTROL 0x710 bound a depth target");
|
||||
queue.context[0x201] = 0x00050002u;
|
||||
Require(!DecodeState(queue).depth, "DB_EQAA Z interpolation refused a 4x draw without a depth target");
|
||||
queue = drawB();
|
||||
state = DecodeState(queue);
|
||||
Require(state.depthTest && state.colors.size() == 1, "a depth-tested 4x depth and color pair lost a target");
|
||||
requireCentered(state, "a depth-tested 4x depth and color pair rasterized without MSAA_ENABLE");
|
||||
queue.context[0x8e] = 0;
|
||||
state = DecodeState(queue);
|
||||
Require(state.colors.empty(), "a depth-only 4x draw without MSAA_ENABLE decoded a color target");
|
||||
requireCentered(state, "a 4x depth target rasterized without MSAA_ENABLE");
|
||||
for (const auto eqaa : {0x00050002u, 0x00090002u, 0x01010002u, 0x08010002u}) {
|
||||
queue = drawB();
|
||||
queue.context[0x201] = eqaa;
|
||||
expectFailure([&] { DecodeState(queue); }, "a multisampled depth target rasterized without MSAA_ENABLE with Z interpolation or overrasterization (DB_EQAA)");
|
||||
}
|
||||
queue = drawB();
|
||||
queue.context[0x1c4] = 9;
|
||||
queue.context[0x203] |= 0x100u;
|
||||
expectFailure([&] { DecodeState(queue); }, "a multisampled depth target rasterized without MSAA_ENABLE with a sample-mask export");
|
||||
queue = drawB();
|
||||
queue.context[0x2dc] = 0x0000aa01u;
|
||||
expectFailure([&] { DecodeState(queue); }, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points, lines or alpha-to-coverage");
|
||||
queue = drawB();
|
||||
queue.context[0x010] = 0xa2900987;
|
||||
expectFailure([&] { DecodeState(queue); }, "the depth target's sample count differs from the rasterizer's");
|
||||
}
|
||||
|
||||
void multisampleTests() {
|
||||
using AgcDriver::Graphics::ColorMetadataPass;
|
||||
using AgcDriver::Graphics::DecodeColorBuffer;
|
||||
@@ -3065,7 +3171,7 @@ void multisampleTests() {
|
||||
const auto aliased = AgcDriver::Graphics::SampleLocations(state);
|
||||
for (std::uint32_t sample = 0; sample < state.samples; ++sample) Require(aliased[sample].x == 0.5f && aliased[sample].y == 0.5f, "a 2x target rasterized without MSAA_ENABLE did not place every sample at the pixel center");
|
||||
queue.context[0x2dc] = 0x0000aa01u;
|
||||
expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE are unsupported for points, lines, a depth target or alpha-to-coverage");
|
||||
expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE are unsupported for points, lines or alpha-to-coverage");
|
||||
queue = multisampled();
|
||||
queue.context[0x292] = 2;
|
||||
queue.context[0x302] = 0x44cc;
|
||||
@@ -8799,7 +8905,9 @@ int main(int argc, char** argv) {
|
||||
metadataPassTests();
|
||||
cmaskTests();
|
||||
uint16ExportTests();
|
||||
uint8x2TargetTests();
|
||||
uint8x4TargetTests();
|
||||
msaaEnableClearDepthTests();
|
||||
multisampleTests();
|
||||
MsaaEnableSingleSampleTests();
|
||||
multisampledDepthTests();
|
||||
@@ -8809,6 +8917,7 @@ int main(int argc, char** argv) {
|
||||
depthCopyFormatTests();
|
||||
quadPixelMaskTests();
|
||||
ShaderStageTests();
|
||||
UnsupportedTessellationFieldTests();
|
||||
NggVertexWorkgroupTests();
|
||||
inactiveVertexPointSizeTests();
|
||||
PolygonPrimitiveTests();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <string>
|
||||
@@ -270,6 +271,89 @@ void RunGuestTextureResourceTests() {
|
||||
const std::array<std::uint32_t, 8> canvas{0x021f2000u, 0x03800000u, 0x804a004au, 0xe1b20facu, 0x00000000u, 0x00400020u, 0x00000000u, 0x00000000u};
|
||||
result = DecodeTextureResource(canvas);
|
||||
Require(result.dimension == TextureDimension::k2DMsaa && result.samples == 4 && result.baseAddress == 0x21f200000ull && result.width == 297 && result.height == 297 && result.format == 56 && result.tileMode == TextureTileMode::kR64KBX, "the 4x canvas descriptor of PPSA99006 decoded incorrectly");
|
||||
Require(DescribeSurface(result).guestBytes == 0x190000, "4x render-target geometry must include fragment-aware padding for the PPSA99006 canvas");
|
||||
const auto canvasGeometry = DescribeSurface(result);
|
||||
const std::array<std::uint32_t, 8> sonic{0x405fc080u, 0xc4700000u, 0x821bc3bfu, 0xe1b30facu, 0x00000000u, 0x00400030u, 0x406b0000u, 0x0040819fu};
|
||||
const auto sonicResource = DecodeTextureResource(sonic);
|
||||
const auto sonicGeometry = DescribeSurface(sonicResource);
|
||||
Require(sonicResource.width == 3840u && sonicResource.height == 2160u && sonicResource.samples == 8u && sonicResource.format == 71u, "the PPSA99902 multisampled color descriptor decoded incorrectly");
|
||||
Require(sonicGeometry.guestBytes == 0x1fe00000ull, "the PPSA99902 8x surface footprint omits fragments: " + std::to_string(sonicGeometry.guestBytes));
|
||||
Require(canvasGeometry.guestBytes == 0x190000u, "the 297x297 4x surface footprint does not use fragment-aware blocks");
|
||||
auto singleSample = sonicResource;
|
||||
singleSample.samples = 1u;
|
||||
singleSample.dimension = TextureDimension::k2D;
|
||||
Require(DescribeSurface(singleSample).guestBytes == 0x3fc0000u, "single-sample surface geometry changed");
|
||||
struct FootprintCase {
|
||||
TextureTileMode tile;
|
||||
VkFormat format;
|
||||
std::uint32_t elementBytes, width, height, samples, blocksPerRow;
|
||||
std::uint64_t bytes;
|
||||
};
|
||||
const std::array footprints{
|
||||
FootprintCase{TextureTileMode::kR64KBX, VK_FORMAT_R8_UINT, 1u, 129u, 65u, 2u, 2u, 0x20000u},
|
||||
FootprintCase{TextureTileMode::kR64KBX, VK_FORMAT_R16_UINT, 2u, 65u, 129u, 4u, 1u, 0x30000u},
|
||||
FootprintCase{TextureTileMode::kR64KBX, VK_FORMAT_R32_UINT, 4u, 129u, 65u, 2u, 3u, 0x30000u},
|
||||
FootprintCase{TextureTileMode::kR64KBX, VK_FORMAT_R32_UINT, 4u, 65u, 129u, 2u, 2u, 0x40000u},
|
||||
FootprintCase{TextureTileMode::kR64KBX, VK_FORMAT_R32_UINT, 4u, 297u, 297u, 4u, 5u, 0x190000u},
|
||||
FootprintCase{TextureTileMode::kR64KBX, VK_FORMAT_R32_UINT, 4u, 65u, 129u, 8u, 3u, 0x90000u},
|
||||
FootprintCase{TextureTileMode::kR64KBX, VK_FORMAT_R32G32_SFLOAT, 8u, 3840u, 2160u, 8u, 120u, 0x1fe00000u},
|
||||
FootprintCase{TextureTileMode::kR64KBX, VK_FORMAT_R32G32B32A32_UINT, 16u, 16384u, 16384u, 8u, 1024u, 0x800000000ull},
|
||||
FootprintCase{TextureTileMode::kZ64KBX, VK_FORMAT_R16_UNORM, 2u, 129u, 65u, 2u, 2u, 0x20000u},
|
||||
FootprintCase{TextureTileMode::kZ64KBX, VK_FORMAT_R32_SFLOAT, 4u, 65u, 129u, 8u, 3u, 0x90000u},
|
||||
FootprintCase{TextureTileMode::kStandard4KB, VK_FORMAT_R32_UINT, 4u, 17u, 33u, 4u, 2u, 0x6000u},
|
||||
FootprintCase{TextureTileMode::kStandard64KB, VK_FORMAT_R32_UINT, 4u, 129u, 65u, 2u, 3u, 0x30000u}
|
||||
};
|
||||
for (const auto& test : footprints) {
|
||||
auto resource = sonicResource;
|
||||
const auto format = FindGuestTextureFormat(test.format, test.elementBytes);
|
||||
Require(format.has_value(), "the multisample footprint test format has no guest encoding");
|
||||
resource.format = format.value();
|
||||
resource.width = test.width;
|
||||
resource.height = test.height;
|
||||
resource.samples = test.samples;
|
||||
resource.tileMode = test.tile;
|
||||
const auto geometry = DescribeSurface(resource);
|
||||
const auto linearBytes = static_cast<std::uint64_t>(test.width) * test.height * test.samples * test.elementBytes;
|
||||
Require(geometry.guestBytes == test.bytes && geometry.layerBytes == test.bytes && geometry.sliceLinearBytes == linearBytes, "multisampled surface footprint or linear size disagrees with the reference");
|
||||
Require(geometry.layers == 1u && geometry.imageLayers == 1u && geometry.imageDepth == 1u && !geometry.thick && geometry.blockDepth == 1u && geometry.mips.size() == 1u, "multisampled surface geometry is not one thin 2D level");
|
||||
const auto& mip = geometry.mips.front();
|
||||
Require(mip.tiledOffset == 0u && mip.tiledSize == test.bytes && mip.linearOffset == 0u && mip.linearSize == linearBytes && mip.width == test.width && mip.height == test.height && mip.blocksPerRow == test.blocksPerRow && mip.pitchBytes == test.width * test.samples * test.elementBytes && !mip.tail && mip.tailX == 0u && mip.tailY == 0u, "multisampled mip geometry lost its sample pitch or block layout");
|
||||
}
|
||||
auto unsupportedSurface = sonicResource;
|
||||
for (const auto samples : {0u, 3u, 16u}) {
|
||||
unsupportedSurface.samples = samples;
|
||||
reject([&] { DescribeSurface(unsupportedSurface); }, "fragment count");
|
||||
}
|
||||
unsupportedSurface = sonicResource;
|
||||
unsupportedSurface.mipCount = 2u;
|
||||
reject([&] { DescribeSurface(unsupportedSurface); }, "mip levels");
|
||||
unsupportedSurface = sonicResource;
|
||||
unsupportedSurface.dimension = TextureDimension::k2DArray;
|
||||
reject([&] { DescribeSurface(unsupportedSurface); }, "dimension or array");
|
||||
unsupportedSurface = sonicResource;
|
||||
unsupportedSurface.tileMode = TextureTileMode::kStandard256B;
|
||||
reject([&] { DescribeSurface(unsupportedSurface); }, "4 KiB or 64 KiB");
|
||||
for (const auto format : {56u, 71u}) {
|
||||
for (const auto samples : {2u, 4u, 8u}) {
|
||||
for (const auto extent : {std::array{1u, 1u}, std::array{17u, 5u}, std::array{297u, 297u}}) {
|
||||
auto linear = sonicResource;
|
||||
linear.tileMode = TextureTileMode::kLinear;
|
||||
linear.format = format;
|
||||
linear.samples = samples;
|
||||
linear.width = extent[0];
|
||||
linear.height = extent[1];
|
||||
auto single = linear;
|
||||
single.samples = 1u;
|
||||
single.dimension = TextureDimension::k2D;
|
||||
const auto expected = DescribeSurface(single);
|
||||
const auto actual = DescribeSurface(linear);
|
||||
Require(actual.guestBytes == expected.guestBytes && actual.layerBytes == expected.layerBytes && actual.sliceLinearBytes == expected.sliceLinearBytes && actual.mips.size() == expected.mips.size(), "linear multisampled descriptors must retain the existing geometry path");
|
||||
const auto& mip = actual.mips.front();
|
||||
const auto& baseline = expected.mips.front();
|
||||
Require(mip.tiledSize == baseline.tiledSize && mip.linearSize == baseline.linearSize && mip.pitchBytes == baseline.pitchBytes && mip.blocksPerRow == baseline.blocksPerRow, "linear multisampled descriptors changed existing mip geometry");
|
||||
}
|
||||
}
|
||||
}
|
||||
Fields fewerFragments = multisampled;
|
||||
fewerFragments.maxMip = 1;
|
||||
rejectFields(fewerFragments, "same sample count");
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
@@ -56,12 +57,31 @@ std::uint64_t workgroupBytes(std::span<const std::uint32_t> words) {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
RecompileResult compile(std::uint32_t waveSize, std::uint32_t limit) {
|
||||
std::set<std::uint32_t> compareExchangeWidths(std::span<const std::uint32_t> words) {
|
||||
std::map<std::uint32_t, std::uint32_t> widths;
|
||||
std::set<std::uint32_t> found;
|
||||
std::uint32_t faultFunction = 0;
|
||||
bool inFaultFunction = false;
|
||||
for (std::size_t offset = 5; offset < words.size();) {
|
||||
const auto count = words[offset] >> 16u;
|
||||
require(count != 0u && count <= words.size() - offset, "malformed SPIR-V instruction");
|
||||
const auto instruction = words.subspan(offset, count);
|
||||
const auto opcode = static_cast<spv::Op>(instruction[0] & 0xffffu);
|
||||
if (opcode == spv::OpName && std::string_view(reinterpret_cast<const char*>(instruction.data() + 2)) == "record_bda_fault") faultFunction = instruction[1];
|
||||
if (opcode == spv::OpFunction) inFaultFunction = instruction[2] == faultFunction;
|
||||
if (opcode == spv::OpTypeInt) widths.emplace(instruction[1], instruction[2]);
|
||||
if (opcode == spv::OpAtomicCompareExchange && !inFaultFunction) found.insert(widths.at(instruction[1]));
|
||||
offset += count;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
RecompileResult compile(std::uint32_t waveSize, std::uint32_t limit, bool int64Atomics) {
|
||||
static constexpr std::array<std::uint32_t, 8> code{
|
||||
0x7e020280u, 0x7e0402c1u, 0x7e060280u, 0x7e080281u,
|
||||
0xd9000000u, 0x00000201u, 0xbf8cc07fu, 0xbf810000u
|
||||
};
|
||||
static constexpr std::array<std::uint32_t, 7> capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess};
|
||||
static constexpr std::array<std::uint32_t, 8> capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityInt64Atomics};
|
||||
static constexpr std::array<std::string_view, 2> extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"};
|
||||
static constexpr std::array<std::uint32_t, 4> userData{};
|
||||
RecompileRequest request{};
|
||||
@@ -73,7 +93,7 @@ RecompileResult compile(std::uint32_t waveSize, std::uint32_t limit) {
|
||||
request.target.spirvVersion = 0x00010600u;
|
||||
request.target.subgroupSize = 32;
|
||||
request.target.bdaAbiVersion = 1;
|
||||
request.target.supportedCapabilities = capabilities;
|
||||
request.target.supportedCapabilities = std::span(capabilities).first(int64Atomics ? 8u : 7u);
|
||||
request.target.supportedExtensions = extensions;
|
||||
request.target.maxWorkgroupSize = {1024u, 1024u, 64u};
|
||||
request.target.maxWorkgroupInvocations = 1024;
|
||||
@@ -83,13 +103,18 @@ RecompileResult compile(std::uint32_t waveSize, std::uint32_t limit) {
|
||||
return Recompile(request);
|
||||
}
|
||||
|
||||
void verify(std::uint32_t waveSize, std::uint32_t limit) {
|
||||
const auto result = compile(waveSize, limit);
|
||||
void verify(std::uint32_t waveSize, std::uint32_t limit, bool int64Atomics) {
|
||||
const auto result = compile(waveSize, limit, int64Atomics);
|
||||
require(!result.spirv.empty(), "LDS atomic program did not compile");
|
||||
const auto declared = workgroupBytes(result.spirv.Words());
|
||||
require(declared <= limit, "declared " + std::to_string(declared) + " Workgroup bytes exceeds limit " + std::to_string(limit));
|
||||
require(result.workgroupMemoryDwords == (limit < 68u ? 17u : 0u), "incorrect device-memory LDS stride at limit " + std::to_string(limit));
|
||||
require(result.workgroupMemoryDwords == (limit < 68u ? 18u : 0u), "incorrect device-memory LDS stride at limit " + std::to_string(limit));
|
||||
if (limit >= 68u) require(declared != 0u, "in-budget LDS unexpectedly lost its Workgroup allocation");
|
||||
const auto widths = compareExchangeWidths(result.spirv.Words());
|
||||
const bool native = int64Atomics && limit < 68u;
|
||||
require(widths.contains(64u) == native, native ? "device-memory LDS did not use a 64-bit compare exchange" : "a 64-bit compare exchange appeared without device-memory LDS and shaderBufferInt64Atomics");
|
||||
require(widths.contains(32u) != native, native ? "device-memory LDS with shaderBufferInt64Atomics kept the workgroup lock" : "the 64-bit LDS atomic lost its workgroup lock");
|
||||
require(result.bdaAbiVersion == (limit < 68u ? 1u : 0u), "the fault buffer for a misaligned 64-bit LDS atomic does not follow device-memory LDS at limit " + std::to_string(limit));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -98,14 +123,16 @@ int main() {
|
||||
unsigned failures = 0;
|
||||
for (const auto waveSize : {32u, 64u}) {
|
||||
for (const auto limit : {64u, 68u, 72u}) {
|
||||
const auto name = "wave" + std::to_string(waveSize) + " limit" + std::to_string(limit);
|
||||
try {
|
||||
verify(waveSize, limit);
|
||||
std::cout << "LDS lock budget: " << name << " passed\n";
|
||||
} catch (const std::exception& error) {
|
||||
++failures;
|
||||
const std::string message(error.what());
|
||||
std::cerr << "LDS lock budget: " << name << ": " << message.substr(0, message.find("RecompileRequest:")) << '\n';
|
||||
for (const bool int64Atomics : {false, true}) {
|
||||
const auto name = "wave" + std::to_string(waveSize) + " limit" + std::to_string(limit) + (int64Atomics ? " int64 atomics" : "");
|
||||
try {
|
||||
verify(waveSize, limit, int64Atomics);
|
||||
std::cout << "LDS lock budget: " << name << " passed\n";
|
||||
} catch (const std::exception& error) {
|
||||
++failures;
|
||||
const std::string message(error.what());
|
||||
std::cerr << "LDS lock budget: " << name << ": " << message.substr(0, message.find("RecompileRequest:")) << '\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,724 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/PresentationPass.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/ToneMapLuminance.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
|
||||
#include <SDL_loadso.h>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace AgcDriver::Graphics;
|
||||
using AgcDriver::OverlayContent;
|
||||
using AgcDriver::OverlayCorner;
|
||||
using AgcDriver::OverlayMetrics;
|
||||
using AgcDriver::OverlayRect;
|
||||
using AgcDriver::PerformanceOverlay;
|
||||
using AgcDriver::PresentationPass;
|
||||
|
||||
constexpr int VulkanTestSkipped = 77;
|
||||
constexpr std::uint64_t Bgra8 = 0x8000000000000000ull;
|
||||
|
||||
std::optional<std::size_t> selectDevice(std::span<const VkPhysicalDeviceProperties> devices, std::string_view request) {
|
||||
const auto rankDeviceType = [](VkPhysicalDeviceType type) {
|
||||
switch (type) {
|
||||
case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: return 3;
|
||||
case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: return 2;
|
||||
case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: return 1;
|
||||
default: return 0;
|
||||
}
|
||||
};
|
||||
std::optional<std::size_t> selected;
|
||||
int selectedRank = -1;
|
||||
for (std::size_t index = 0; index < devices.size(); ++index) {
|
||||
const auto& candidate = devices[index];
|
||||
if (candidate.apiVersion < VK_API_VERSION_1_1) continue;
|
||||
const std::string_view name(candidate.deviceName);
|
||||
if (!request.empty() && std::search(name.begin(), name.end(), request.begin(), request.end(), [](char left, char right) {
|
||||
return std::tolower(static_cast<unsigned char>(left)) == std::tolower(static_cast<unsigned char>(right));
|
||||
}) == name.end()) continue;
|
||||
const int rank = request.empty() ? rankDeviceType(candidate.deviceType) : 4;
|
||||
if (rank <= selectedRank) continue;
|
||||
selected = index;
|
||||
selectedRank = rank;
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
void deviceSelectionTests() {
|
||||
const auto properties = [](const char* name, VkPhysicalDeviceType type) {
|
||||
VkPhysicalDeviceProperties result{};
|
||||
result.apiVersion = VK_API_VERSION_1_1;
|
||||
result.deviceType = type;
|
||||
std::memcpy(result.deviceName, name, std::strlen(name) + 1u);
|
||||
return result;
|
||||
};
|
||||
std::array devices{
|
||||
properties("AMD Radeon Graphics", VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU),
|
||||
properties("NVIDIA GeForce RTX 5080", VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU),
|
||||
properties("Virtual GPU", VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU),
|
||||
properties("CPU", VK_PHYSICAL_DEVICE_TYPE_CPU),
|
||||
};
|
||||
Require(!selectDevice({}, {}).has_value(), "an empty device list selected a device");
|
||||
Require(selectDevice(devices, {}) == 1u, "default selection did not prefer the discrete GPU");
|
||||
Require(selectDevice(devices, "AMD") == 0u, "the AMD request did not select the integrated GPU");
|
||||
Require(selectDevice(devices, "NVIDIA") == 1u, "the NVIDIA request did not select the discrete GPU");
|
||||
Require(selectDevice(devices, "aMd rAdEoN") == 0u, "a requested integrated GPU lost to an unrelated discrete GPU");
|
||||
Require(selectDevice(devices, "rtx 5080") == 1u, "a partial GPU name did not match without regard to case");
|
||||
Require(selectDevice(devices, "Virtual") == 2u, "a requested virtual GPU was not selected");
|
||||
Require(selectDevice(devices, "cpu") == 3u, "a requested CPU device was not selected");
|
||||
Require(!selectDevice(devices, "missing").has_value(), "an unmatched request selected another GPU");
|
||||
Require(!selectDevice(devices, "NVIDIA GeForce RTX 5080 with a longer name").has_value(), "a request longer than the device name matched");
|
||||
Require(selectDevice(std::span(devices).subspan(2), {}) == 0u, "default selection did not prefer the virtual GPU to the CPU");
|
||||
devices[1].apiVersion = VK_API_VERSION_1_0;
|
||||
Require(selectDevice(devices, {}) == 0u, "default selection did not prefer the integrated GPU to the virtual GPU");
|
||||
Require(!selectDevice(devices, "NVIDIA").has_value(), "a requested device without Vulkan 1.1 was selected");
|
||||
devices[1] = devices[0];
|
||||
Require(selectDevice(devices, {}) == 0u && selectDevice(devices, "AMD") == 0u, "equally ranked matches did not retain enumeration order");
|
||||
for (auto& device : devices) device.apiVersion = VK_API_VERSION_1_0;
|
||||
Require(!selectDevice(devices, {}).has_value(), "a list without Vulkan 1.1 selected a device");
|
||||
std::puts("performance-overlay device selection CPU tests passed");
|
||||
}
|
||||
|
||||
class Device {
|
||||
public:
|
||||
Device() {
|
||||
#ifdef _WIN32
|
||||
library = SDL_LoadObject("vulkan-1.dll");
|
||||
#else
|
||||
library = SDL_LoadObject("libvulkan.so.1");
|
||||
#endif
|
||||
Require(library != nullptr, "cannot load Vulkan");
|
||||
try {
|
||||
instanceProc = reinterpret_cast<PFN_vkGetInstanceProcAddr>(SDL_LoadFunction(library, "vkGetInstanceProcAddr"));
|
||||
Require(instanceProc != nullptr, "missing Vulkan instance resolver");
|
||||
VkApplicationInfo application{VK_STRUCTURE_TYPE_APPLICATION_INFO};
|
||||
application.apiVersion = VK_API_VERSION_1_1;
|
||||
VkInstanceCreateInfo info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO};
|
||||
info.pApplicationInfo = &application;
|
||||
Check(function<PFN_vkCreateInstance>("vkCreateInstance")(&info, nullptr, &instance), "vkCreateInstance");
|
||||
std::uint32_t count = 0;
|
||||
const auto enumerate = function<PFN_vkEnumeratePhysicalDevices>("vkEnumeratePhysicalDevices");
|
||||
Check(enumerate(instance, &count, nullptr), "vkEnumeratePhysicalDevices");
|
||||
Require(count != 0, "no Vulkan device");
|
||||
std::vector<VkPhysicalDevice> devices(count);
|
||||
Check(enumerate(instance, &count, devices.data()), "vkEnumeratePhysicalDevices");
|
||||
const auto physicalProperties = function<PFN_vkGetPhysicalDeviceProperties>("vkGetPhysicalDeviceProperties");
|
||||
std::vector<VkPhysicalDeviceProperties> deviceProperties(devices.size());
|
||||
for (std::size_t index = 0; index < devices.size(); ++index) physicalProperties(devices[index], &deviceProperties[index]);
|
||||
const char* requestedName = std::getenv("ANYPS5_GPU");
|
||||
const auto selectedIndex = selectDevice(deviceProperties, requestedName != nullptr ? requestedName : "");
|
||||
Require(selectedIndex.has_value(), requestedName != nullptr && *requestedName != '\0' ? std::string("no Vulkan 1.1 device matching ANYPS5_GPU=\"") + requestedName + "\"" : "no Vulkan 1.1 device");
|
||||
context.physical = devices[*selectedIndex];
|
||||
const auto queues = function<PFN_vkGetPhysicalDeviceQueueFamilyProperties>("vkGetPhysicalDeviceQueueFamilyProperties");
|
||||
queues(context.physical, &count, nullptr);
|
||||
std::vector<VkQueueFamilyProperties> families(count);
|
||||
queues(context.physical, &count, families.data());
|
||||
std::uint32_t family = 0;
|
||||
while (family < count && (families[family].queueFlags & VK_QUEUE_GRAPHICS_BIT) == 0) ++family;
|
||||
Require(family < count, "no Vulkan graphics queue");
|
||||
timestampBits = families[family].timestampValidBits;
|
||||
const auto enumerateExtensions = function<PFN_vkEnumerateDeviceExtensionProperties>("vkEnumerateDeviceExtensionProperties");
|
||||
Check(enumerateExtensions(context.physical, nullptr, &count, nullptr), "vkEnumerateDeviceExtensionProperties");
|
||||
std::vector<VkExtensionProperties> extensions(count);
|
||||
Check(enumerateExtensions(context.physical, nullptr, &count, extensions.data()), "vkEnumerateDeviceExtensionProperties");
|
||||
memoryBudget = std::any_of(extensions.begin(), extensions.end(), [](const VkExtensionProperties& extension) { return std::string(extension.extensionName) == VK_EXT_MEMORY_BUDGET_EXTENSION_NAME; });
|
||||
const char* const budgetExtension = VK_EXT_MEMORY_BUDGET_EXTENSION_NAME;
|
||||
const float priority = 1;
|
||||
VkDeviceQueueCreateInfo queue{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO};
|
||||
queue.queueFamilyIndex = family;
|
||||
queue.queueCount = 1;
|
||||
queue.pQueuePriorities = &priority;
|
||||
VkDeviceCreateInfo device{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO};
|
||||
device.queueCreateInfoCount = 1;
|
||||
device.pQueueCreateInfos = &queue;
|
||||
device.enabledExtensionCount = memoryBudget ? 1 : 0;
|
||||
device.ppEnabledExtensionNames = memoryBudget ? &budgetExtension : nullptr;
|
||||
Check(function<PFN_vkCreateDevice>("vkCreateDevice")(context.physical, &device, nullptr, &context.device), "vkCreateDevice");
|
||||
if (memoryBudget) context.memoryProperties2 = function<PFN_vkGetPhysicalDeviceMemoryProperties2>("vkGetPhysicalDeviceMemoryProperties2");
|
||||
context.deviceProc = function<PFN_vkGetDeviceProcAddr>("vkGetDeviceProcAddr");
|
||||
function<PFN_vkGetPhysicalDeviceMemoryProperties>("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory);
|
||||
VkPhysicalDeviceProperties properties{};
|
||||
function<PFN_vkGetPhysicalDeviceProperties>("vkGetPhysicalDeviceProperties")(context.physical, &properties);
|
||||
context.limits = properties.limits;
|
||||
name = properties.deviceName;
|
||||
context.formatProperties = function<PFN_vkGetPhysicalDeviceFormatProperties>("vkGetPhysicalDeviceFormatProperties");
|
||||
context.imageFormatProperties = function<PFN_vkGetPhysicalDeviceImageFormatProperties>("vkGetPhysicalDeviceImageFormatProperties");
|
||||
context.Function<PFN_vkGetDeviceQueue>("vkGetDeviceQueue")(context.device, family, 0, &context.queue);
|
||||
VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
|
||||
pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
|
||||
pool.queueFamilyIndex = family;
|
||||
Check(context.Function<PFN_vkCreateCommandPool>("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool");
|
||||
} catch (...) {
|
||||
release();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
~Device() { release(); }
|
||||
const Context& GetContext() const { return context; }
|
||||
const std::string& Name() const { return name; }
|
||||
std::uint32_t TimestampBits() const { return timestampBits; }
|
||||
bool MemoryBudget() const { return memoryBudget; }
|
||||
|
||||
void VerifySelection() const {
|
||||
VkPhysicalDeviceProperties properties{};
|
||||
function<PFN_vkGetPhysicalDeviceProperties>("vkGetPhysicalDeviceProperties")(context.physical, &properties);
|
||||
std::cout << "Selected GPU name=" << properties.deviceName << " vendor=" << properties.vendorID << " device=" << properties.deviceID << '\n';
|
||||
const char* request = std::getenv("ANYPS5_GPU");
|
||||
Require(selectDevice(std::span(&properties, 1), request != nullptr ? request : "").has_value(), "the performance-overlay fixture physical device does not match ANYPS5_GPU");
|
||||
}
|
||||
|
||||
private:
|
||||
template<typename TFunction>
|
||||
TFunction function(const char* functionName) const {
|
||||
const auto result = reinterpret_cast<TFunction>(instanceProc(instance, functionName));
|
||||
Require(result != nullptr, functionName);
|
||||
return result;
|
||||
}
|
||||
|
||||
void release() noexcept {
|
||||
if (context.pool != VK_NULL_HANDLE) context.Function<PFN_vkDestroyCommandPool>("vkDestroyCommandPool")(context.device, context.pool, nullptr);
|
||||
context.bufferPool.reset();
|
||||
if (context.device != VK_NULL_HANDLE) function<PFN_vkDestroyDevice>("vkDestroyDevice")(context.device, nullptr);
|
||||
if (instance != VK_NULL_HANDLE) function<PFN_vkDestroyInstance>("vkDestroyInstance")(instance, nullptr);
|
||||
if (library != nullptr) SDL_UnloadObject(library);
|
||||
}
|
||||
|
||||
void* library = nullptr;
|
||||
PFN_vkGetInstanceProcAddr instanceProc = nullptr;
|
||||
VkInstance instance = VK_NULL_HANDLE;
|
||||
Context context{};
|
||||
std::string name;
|
||||
std::uint32_t timestampBits = 0;
|
||||
bool memoryBudget = false;
|
||||
};
|
||||
|
||||
class Image {
|
||||
public:
|
||||
Image(const Context& context, VkFormat format, VkExtent2D extent, VkImageUsageFlags usage, VkImageCreateFlags flags = 0) : context(context), extent(extent) {
|
||||
VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
||||
info.flags = flags;
|
||||
info.imageType = VK_IMAGE_TYPE_2D;
|
||||
info.format = format;
|
||||
info.extent = {extent.width, extent.height, 1};
|
||||
info.mipLevels = 1;
|
||||
info.arrayLayers = 1;
|
||||
info.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
info.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
info.usage = usage;
|
||||
info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
Check(context.Function<PFN_vkCreateImage>("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage performance overlay test");
|
||||
VkMemoryRequirements requirements{};
|
||||
context.Function<PFN_vkGetImageMemoryRequirements>("vkGetImageMemoryRequirements")(context.device, image, &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);
|
||||
const auto allocated = context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &memory);
|
||||
if (allocated != VK_SUCCESS) context.Function<PFN_vkDestroyImage>("vkDestroyImage")(context.device, image, nullptr);
|
||||
Check(allocated, "vkAllocateMemory performance overlay test");
|
||||
Check(context.Function<PFN_vkBindImageMemory>("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory performance overlay test");
|
||||
}
|
||||
~Image() {
|
||||
context.Function<PFN_vkDestroyImage>("vkDestroyImage")(context.device, image, nullptr);
|
||||
context.Function<PFN_vkFreeMemory>("vkFreeMemory")(context.device, memory, nullptr);
|
||||
}
|
||||
Image(const Image&) = delete;
|
||||
Image& operator=(const Image&) = delete;
|
||||
VkImage Handle() const { return image; }
|
||||
VkExtent2D Extent() const { return extent; }
|
||||
|
||||
private:
|
||||
const Context& context;
|
||||
VkExtent2D extent;
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VkDeviceMemory memory = VK_NULL_HANDLE;
|
||||
};
|
||||
|
||||
void transition(const Context& context, VkCommandBuffer commands, VkImage image, VkImageLayout from, VkImageLayout to, VkPipelineStageFlags sourceStage, VkPipelineStageFlags destinationStage, VkAccessFlags sourceAccess, VkAccessFlags destinationAccess) {
|
||||
VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
|
||||
barrier.srcAccessMask = sourceAccess;
|
||||
barrier.dstAccessMask = destinationAccess;
|
||||
barrier.oldLayout = from;
|
||||
barrier.newLayout = to;
|
||||
barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
barrier.image = image;
|
||||
barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, sourceStage, destinationStage, 0, 0, nullptr, 0, nullptr, 1, &barrier);
|
||||
}
|
||||
|
||||
void readImage(const Context& context, VkCommandBuffer commands, const Image& image, Buffer& destination) {
|
||||
VkBufferImageCopy copy{};
|
||||
copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
||||
copy.imageExtent = {image.Extent().width, image.Extent().height, 1};
|
||||
context.Function<PFN_vkCmdCopyImageToBuffer>("vkCmdCopyImageToBuffer")(commands, image.Handle(), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destination.Handle(), 1, ©);
|
||||
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT);
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> wordsOf(Buffer& buffer, VkExtent2D extent) {
|
||||
std::vector<std::uint32_t> words(static_cast<std::size_t>(extent.width) * extent.height);
|
||||
std::memcpy(words.data(), buffer.Bytes().data(), words.size() * sizeof(std::uint32_t));
|
||||
return words;
|
||||
}
|
||||
|
||||
void setEnvironment(const char* name, const char* value) {
|
||||
#ifdef _WIN32
|
||||
_putenv_s(name, value);
|
||||
#else
|
||||
setenv(name, value, 1);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool inside(const OverlayRect& rect, std::int32_t x, std::int32_t y) {
|
||||
return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height;
|
||||
}
|
||||
|
||||
OverlayContent sixtyFps() {
|
||||
AgcDriver::OverlayStatistics statistics;
|
||||
const auto start = AgcDriver::OverlayStatistics::Clock::time_point{} + std::chrono::hours(1);
|
||||
for (int i = 0; i <= 60; ++i) statistics.AddPresent(start + std::chrono::nanoseconds(16'666'667) * i, 0, 0);
|
||||
auto content = AgcDriver::ComposeOverlay(statistics, false, false);
|
||||
Require(content.above.front() == "FPS: 60.0", "60 presents a second compose as '" + content.above.front() + "'");
|
||||
return content;
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> clearAndOverlay(const Context& context, PerformanceOverlay* overlay, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, const VkClearColorValue& clear, const OverlayContent& content, OverlayCorner corner, OverlayRect* bounds) {
|
||||
Image destination(context, format, extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT);
|
||||
Buffer readback(context, static_cast<std::size_t>(extent.width) * extent.height * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT);
|
||||
{
|
||||
CommandBatch batch(context);
|
||||
const auto commands = batch.Handle();
|
||||
transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT);
|
||||
const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
context.Function<PFN_vkCmdClearColorImage>("vkCmdClearColorImage")(commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clear, 1, &range);
|
||||
if (overlay != nullptr) {
|
||||
*bounds = overlay->Record(commands, 0, destination.Handle(), format, colorSpace, extent, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, content, corner);
|
||||
} else {
|
||||
transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT);
|
||||
}
|
||||
readImage(context, commands, destination, readback);
|
||||
batch.SubmitAndWait();
|
||||
}
|
||||
if (overlay != nullptr) overlay->Release(0);
|
||||
return wordsOf(readback, extent);
|
||||
}
|
||||
|
||||
void onlyInside(const std::vector<std::uint32_t>& reference, const std::vector<std::uint32_t>& drawn, VkExtent2D extent, const OverlayRect& bounds, const std::string& what, bool everyPixel = true) {
|
||||
std::size_t changed = 0;
|
||||
for (std::int32_t y = 0; y < static_cast<std::int32_t>(extent.height); ++y) {
|
||||
for (std::int32_t x = 0; x < static_cast<std::int32_t>(extent.width); ++x) {
|
||||
const auto index = static_cast<std::size_t>(y) * extent.width + static_cast<std::size_t>(x);
|
||||
if (!inside(bounds, x, y)) {
|
||||
Require(drawn[index] == reference[index], what + ": the overlay changed pixel " + std::to_string(x) + "," + std::to_string(y) + " outside its panel");
|
||||
continue;
|
||||
}
|
||||
if (drawn[index] != reference[index]) ++changed;
|
||||
else Require(!everyPixel, what + ": the overlay left pixel " + std::to_string(x) + "," + std::to_string(y) + " of its panel unchanged");
|
||||
}
|
||||
}
|
||||
Require(changed != 0, what + ": the overlay changed nothing");
|
||||
}
|
||||
|
||||
int channel(std::uint32_t word, unsigned shift) {
|
||||
return static_cast<int>((word >> shift) & 0xffu);
|
||||
}
|
||||
|
||||
void requireTextDrawn(const std::vector<std::uint32_t>& drawn, VkExtent2D extent, const OverlayRect& bounds, std::int32_t scale, std::uint32_t frameWord, const std::string& line, std::int32_t row, std::size_t first = 0, std::size_t count = std::string::npos) {
|
||||
const auto end = count == std::string::npos ? line.size() : std::min(line.size(), first + count);
|
||||
for (std::size_t index = first; index < end; ++index) {
|
||||
const auto& glyph = AgcDriver::OverlayGlyph(line[index]);
|
||||
const auto column = static_cast<std::int32_t>(index);
|
||||
for (std::int32_t gy = 0; gy < OverlayMetrics::GlyphHeight; ++gy) {
|
||||
for (std::int32_t gx = 0; gx < OverlayMetrics::Advance; ++gx) {
|
||||
const bool set = gx < OverlayMetrics::GlyphWidth && (glyph[static_cast<std::size_t>(gy)] & (0x10u >> gx)) != 0;
|
||||
for (std::int32_t py = 0; py < scale; ++py) {
|
||||
for (std::int32_t px = 0; px < scale; ++px) {
|
||||
const auto x = bounds.x + (OverlayMetrics::Padding + column * OverlayMetrics::Advance + gx) * scale + px;
|
||||
const auto y = bounds.y + (row + gy) * scale + py;
|
||||
const auto word = drawn[static_cast<std::size_t>(y) * extent.width + static_cast<std::size_t>(x)];
|
||||
const auto where = std::string("'") + line[index] + "' of '" + line + "' glyph pixel " + std::to_string(gx) + "," + std::to_string(gy);
|
||||
Require(channel(word, 24) == channel(frameWord, 24), where + " changed the alpha");
|
||||
if (set) {
|
||||
Require((word & 0x00ffffffu) == 0x00ffffffu, where + " is not white text: 0x" + std::to_string(word));
|
||||
continue;
|
||||
}
|
||||
for (const unsigned shift : {0u, 8u, 16u}) {
|
||||
const int expected = static_cast<int>(std::lround(channel(frameWord, shift) * 0.4));
|
||||
Require(std::abs(channel(word, shift) - expected) <= 1, where + " is not the panel over the frame: " + std::to_string(channel(word, shift)) + " for " + std::to_string(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AgcDriver::OverlayDetails measuredDetails(const Context& context, VkExtent2D display, VkExtent2D window) {
|
||||
AgcDriver::OverlayDetails details;
|
||||
details.vram = AgcDriver::ReadVramBudget(context);
|
||||
details.ram = AgcDriver::ReadProcessPrivateBytes();
|
||||
details.display = display;
|
||||
details.window = window;
|
||||
return details;
|
||||
}
|
||||
|
||||
AgcDriver::OverlayDetails fixedDetails(VkExtent2D display, VkExtent2D window) {
|
||||
constexpr double GiB = 1024.0 * 1024.0 * 1024.0;
|
||||
AgcDriver::OverlayDetails details;
|
||||
details.vram = AgcDriver::OverlayMemory{static_cast<std::uint64_t>(6.1 * GiB), static_cast<std::uint64_t>(7.8 * GiB)};
|
||||
details.ram = static_cast<std::uint64_t>(3.2 * GiB);
|
||||
details.display = display;
|
||||
details.window = window;
|
||||
return details;
|
||||
}
|
||||
|
||||
OverlayContent fullPanel(const AgcDriver::OverlayDetails& details) {
|
||||
AgcDriver::OverlayStatistics statistics;
|
||||
const auto start = AgcDriver::OverlayStatistics::Clock::time_point{} + std::chrono::hours(1);
|
||||
for (std::uint64_t i = 0; i <= 60; ++i) {
|
||||
AgcDriver::OverlayCounters counters;
|
||||
counters.gpuNanoseconds = i * 4'200'000u;
|
||||
counters.driverNanoseconds = i * 1'300'000u;
|
||||
counters.draws = i * 1240;
|
||||
counters.dispatches = i * 85;
|
||||
counters.pipelines = i < 50 ? 1500 : 1503;
|
||||
statistics.AddPresent(start + std::chrono::nanoseconds(16'666'667) * i, counters);
|
||||
}
|
||||
auto content = AgcDriver::ComposeOverlay(statistics, true, true, details);
|
||||
Require(content.above.size() == 3 && content.above[0] == "PROFILED" && content.above[1] == "FPS: 60.0" && content.below.size() == 5u + (details.vram ? 1u : 0u) + (details.ram ? 1u : 0u) + (details.display.width != 0 ? 1u : 0u), "the full panel composes as '" + content.above.front() + "' with " + std::to_string(content.below.size()) + " lines below its graph");
|
||||
return content;
|
||||
}
|
||||
|
||||
void sdrOverlayTest(const Context& context, PerformanceOverlay& overlay) {
|
||||
const VkExtent2D extent{1280, 720};
|
||||
VkClearColorValue clear{};
|
||||
clear.float32[0] = 0.75f;
|
||||
clear.float32[1] = 0.5f;
|
||||
clear.float32[2] = 0.25f;
|
||||
clear.float32[3] = 1.0f;
|
||||
const auto content = sixtyFps();
|
||||
OverlayRect bounds{};
|
||||
const auto reference = clearAndOverlay(context, nullptr, VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, clear, content, OverlayCorner::TopLeft, nullptr);
|
||||
const auto drawn = clearAndOverlay(context, &overlay, VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, clear, content, OverlayCorner::TopLeft, &bounds);
|
||||
Require(bounds.x == AgcDriver::LayOutOverlay(content, extent, OverlayCorner::TopLeft).bounds.x && bounds.width > 0 && bounds.height > 0, "the overlay drew another panel than its layout");
|
||||
onlyInside(reference, drawn, extent, bounds, "SDR 1280x720");
|
||||
const auto scale = AgcDriver::OverlayScale(extent, content);
|
||||
const std::string digits = "60.0";
|
||||
const auto firstColumn = content.above.front().find(digits);
|
||||
Require(firstColumn != std::string::npos, "the FPS line has no '60.0'");
|
||||
requireTextDrawn(drawn, extent, bounds, scale, reference.front(), content.above.front(), OverlayMetrics::Padding, firstColumn, digits.size());
|
||||
std::printf("SDR overlay: panel %dx%d at %d,%d (scale %d), FPS digits drawn bit for bit\n", bounds.width, bounds.height, bounds.x, bounds.y, scale);
|
||||
}
|
||||
|
||||
void hdrOverlayTest(const Context& context, PerformanceOverlay& overlay) {
|
||||
const auto format = VK_FORMAT_A2B10G10R10_UNORM_PACK32;
|
||||
VkFormatProperties properties{};
|
||||
context.formatProperties(context.physical, format, &properties);
|
||||
if ((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT) == 0) {
|
||||
std::printf("A2B10G10R10 cannot be blended into here; HDR10 overlay not exercised\n");
|
||||
return;
|
||||
}
|
||||
const VkExtent2D extent{1920, 1080};
|
||||
VkClearColorValue clear{};
|
||||
clear.float32[0] = 0.4f;
|
||||
clear.float32[1] = 0.5f;
|
||||
clear.float32[2] = 0.6f;
|
||||
clear.float32[3] = 1.0f;
|
||||
const auto content = sixtyFps();
|
||||
OverlayRect bounds{};
|
||||
const auto reference = clearAndOverlay(context, nullptr, format, VK_COLOR_SPACE_HDR10_ST2084_EXT, extent, clear, content, OverlayCorner::BottomRight, nullptr);
|
||||
const auto drawn = clearAndOverlay(context, &overlay, format, VK_COLOR_SPACE_HDR10_ST2084_EXT, extent, clear, content, OverlayCorner::BottomRight, &bounds);
|
||||
onlyInside(reference, drawn, extent, bounds, "HDR10 1920x1080");
|
||||
const auto scale = AgcDriver::OverlayScale(extent, content);
|
||||
const auto& six = AgcDriver::OverlayGlyph('6');
|
||||
const auto column = static_cast<std::int32_t>(content.above.front().find('6'));
|
||||
std::size_t checked = 0;
|
||||
for (std::int32_t gy = 0; gy < OverlayMetrics::GlyphHeight; ++gy) {
|
||||
for (std::int32_t gx = 0; gx < OverlayMetrics::GlyphWidth; ++gx) {
|
||||
if ((six[static_cast<std::size_t>(gy)] & (0x10u >> gx)) == 0) continue;
|
||||
const auto x = bounds.x + (OverlayMetrics::Padding + column * OverlayMetrics::Advance + gx) * scale + scale / 2;
|
||||
const auto y = bounds.y + (OverlayMetrics::Padding + gy) * scale + scale / 2;
|
||||
const auto word = drawn[static_cast<std::size_t>(y) * extent.width + static_cast<std::size_t>(x)];
|
||||
for (const unsigned shift : {0u, 10u, 20u}) {
|
||||
const int code = static_cast<int>((word >> shift) & 0x3ffu);
|
||||
Require(std::abs(code - 594) <= 1, "HDR10 text is not at 203 nits: PQ code " + std::to_string(code) + " for 594");
|
||||
}
|
||||
Require((word >> 30u) == (reference.front() >> 30u), "HDR10 text changed the alpha");
|
||||
++checked;
|
||||
}
|
||||
}
|
||||
Require(checked != 0, "no HDR10 text pixel was checked");
|
||||
std::printf("HDR10 overlay: text at PQ code 594 (203 nits), nothing changed outside the %dx%d panel\n", bounds.width, bounds.height);
|
||||
}
|
||||
|
||||
void cornerTest(const Context& context, PerformanceOverlay& overlay) {
|
||||
const VkExtent2D extent{640, 360};
|
||||
VkClearColorValue clear{};
|
||||
clear.float32[1] = 1.0f;
|
||||
clear.float32[3] = 1.0f;
|
||||
const auto content = sixtyFps();
|
||||
const auto reference = clearAndOverlay(context, nullptr, VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, clear, content, OverlayCorner::TopLeft, nullptr);
|
||||
for (const auto corner : {OverlayCorner::TopLeft, OverlayCorner::TopRight, OverlayCorner::BottomLeft, OverlayCorner::BottomRight}) {
|
||||
OverlayRect bounds{};
|
||||
const auto drawn = clearAndOverlay(context, &overlay, VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, clear, content, corner, &bounds);
|
||||
const auto expected = AgcDriver::LayOutOverlay(content, extent, corner).bounds;
|
||||
Require(bounds.x == expected.x && bounds.y == expected.y && bounds.width == expected.width && bounds.height == expected.height, "the drawn panel is not where the layout puts it");
|
||||
onlyInside(reference, drawn, extent, bounds, "corner " + std::to_string(static_cast<int>(corner)));
|
||||
}
|
||||
std::printf("overlay corners: tl, tr, bl and br drawn where the layout puts them\n");
|
||||
}
|
||||
|
||||
void fullPanelTest(const Device& device, PerformanceOverlay& overlay) {
|
||||
const auto& context = device.GetContext();
|
||||
const VkExtent2D extent{1920, 1080};
|
||||
auto details = measuredDetails(context, extent, extent);
|
||||
Require(details.ram.has_value(), "the process's private memory cannot be read");
|
||||
Require(details.vram.has_value() == device.MemoryBudget(), device.MemoryBudget() ? "VK_EXT_memory_budget gives no device-local budget" : "VRAM is read without VK_EXT_memory_budget");
|
||||
auto withoutBudget = context;
|
||||
withoutBudget.memoryProperties2 = nullptr;
|
||||
Require(!AgcDriver::ReadVramBudget(withoutBudget).has_value(), "VRAM is read without vkGetPhysicalDeviceMemoryProperties2");
|
||||
VkClearColorValue clear{};
|
||||
clear.float32[0] = 0.75f;
|
||||
clear.float32[1] = 0.5f;
|
||||
clear.float32[2] = 0.25f;
|
||||
clear.float32[3] = 1.0f;
|
||||
const auto content = fullPanel(details);
|
||||
OverlayRect bounds{};
|
||||
const auto reference = clearAndOverlay(context, nullptr, VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, clear, content, OverlayCorner::TopLeft, nullptr);
|
||||
const auto drawn = clearAndOverlay(context, &overlay, VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, clear, content, OverlayCorner::TopLeft, &bounds);
|
||||
const auto layout = AgcDriver::LayOutOverlay(content, extent, OverlayCorner::TopLeft);
|
||||
Require(bounds.x == layout.bounds.x && bounds.y == layout.bounds.y && bounds.width == layout.bounds.width && bounds.height == layout.bounds.height, "the full panel is not drawn where its layout puts it");
|
||||
onlyInside(reference, drawn, extent, bounds, "full panel 1920x1080");
|
||||
std::int32_t row = OverlayMetrics::Padding;
|
||||
for (const auto& line : content.above) {
|
||||
requireTextDrawn(drawn, extent, bounds, layout.scale, reference.front(), line, row);
|
||||
row += OverlayMetrics::LinePitch;
|
||||
}
|
||||
row += OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap;
|
||||
for (const auto& line : content.below) {
|
||||
requireTextDrawn(drawn, extent, bounds, layout.scale, reference.front(), line, row);
|
||||
row += OverlayMetrics::LinePitch;
|
||||
}
|
||||
std::printf("full panel at 1920x1080: %dx%d at %d,%d (scale %d, %.1f%% of the height), every line drawn bit for bit, nothing changed outside it:\n", bounds.width, bounds.height, bounds.x, bounds.y, layout.scale, 100.0 * bounds.height / extent.height);
|
||||
for (const auto& line : content.above) std::printf(" %s\n", line.c_str());
|
||||
std::printf(" (graph of %zu frame times)\n", content.frameTimes.size());
|
||||
for (const auto& line : content.below) std::printf(" %s\n", line.c_str());
|
||||
}
|
||||
|
||||
void notePresentTest(const Device& device) {
|
||||
const auto& context = device.GetContext();
|
||||
const VkExtent2D extent{1280, 720};
|
||||
PerformanceOverlay overlay(context, 1);
|
||||
overlay.NotePresent(AgcDriver::OverlayStatistics::Clock::now(), VkExtent2D{640, 360});
|
||||
const auto& details = overlay.Details();
|
||||
Require(details.ram.has_value() && details.vram.has_value() == device.MemoryBudget() && details.display.width == 640 && details.display.height == 360, "a shown frame did not sample the memory or keep the display buffer size");
|
||||
Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT);
|
||||
OverlayRect bounds{};
|
||||
{
|
||||
CommandBatch batch(context);
|
||||
const auto commands = batch.Handle();
|
||||
transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT);
|
||||
bounds = overlay.Record(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
|
||||
batch.SubmitAndWait();
|
||||
}
|
||||
overlay.Release(0);
|
||||
auto shown = details;
|
||||
shown.window = extent;
|
||||
const auto expected = AgcDriver::ComposeOverlay(overlay.Statistics(), AgcDriver::PerformanceCounters::GpuEnabled(), AgcDriver::PerformanceCounters::DriverEnabled(), shown);
|
||||
Require(expected.below.back() == "RES: 640X360 / 1280X720", "the RES line reads '" + expected.below.back() + "'");
|
||||
const auto layout = AgcDriver::LayOutOverlay(expected, extent, PerformanceOverlay::Corner());
|
||||
Require(bounds.height == layout.bounds.height && bounds.width == layout.bounds.width, "a shown frame's panel does not hold the sampled VRAM, RAM and RES lines");
|
||||
std::printf("shown frame: memory sampled, %zu lines below the graph, the last '%s'\n", expected.below.size(), expected.below.back().c_str());
|
||||
}
|
||||
|
||||
class Presenter {
|
||||
public:
|
||||
explicit Presenter(const Context& context) : context(context), pass(context, 1), overlay(context, 1), source(context, VK_FORMAT_B8G8R8A8_UNORM, {256, 144}, VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT) {
|
||||
Buffer upload(context, static_cast<std::size_t>(256) * 144 * 4, VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
|
||||
std::uint32_t seed = 0x2545f491u;
|
||||
for (auto& byte : upload.Bytes()) {
|
||||
seed = seed * 1664525u + 1013904223u;
|
||||
byte = static_cast<std::byte>(seed >> 24u);
|
||||
}
|
||||
CommandBatch batch(context);
|
||||
const auto commands = batch.Handle();
|
||||
transition(context, commands, source.Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT);
|
||||
VkBufferImageCopy copy{};
|
||||
copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
||||
copy.imageExtent = {256, 144, 1};
|
||||
context.Function<PFN_vkCmdCopyBufferToImage>("vkCmdCopyBufferToImage")(commands, upload.Handle(), source.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©);
|
||||
transition(context, commands, source.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT);
|
||||
batch.SubmitAndWait();
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> Drawn(OverlayRect* bounds) {
|
||||
Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, Extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT);
|
||||
Buffer readback(context, static_cast<std::size_t>(Extent.width) * Extent.height * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT);
|
||||
const bool visible = PerformanceOverlay::Visible();
|
||||
pass.Bind(0, source.Handle(), 256, 144, Bgra8, AgcDriver::SdrToneMapLuminanceNits);
|
||||
{
|
||||
CommandBatch batch(context);
|
||||
const auto commands = batch.Handle();
|
||||
pass.RecordPresent(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, Extent, visible ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
|
||||
if (visible) *bounds = overlay.Record(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, Extent, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, fullPanel(fixedDetails({256, 144}, Extent)), OverlayCorner::TopLeft);
|
||||
readImage(context, commands, destination, readback);
|
||||
batch.SubmitAndWait();
|
||||
}
|
||||
pass.Release(0);
|
||||
overlay.Release(0);
|
||||
return wordsOf(readback, Extent);
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> Blitted(OverlayRect* bounds) {
|
||||
Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, Extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT);
|
||||
Buffer readback(context, static_cast<std::size_t>(Extent.width) * Extent.height * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT);
|
||||
const bool visible = PerformanceOverlay::Visible();
|
||||
{
|
||||
CommandBatch batch(context);
|
||||
const auto commands = batch.Handle();
|
||||
transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT);
|
||||
transition(context, commands, source.Handle(), VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_READ_BIT);
|
||||
VkClearColorValue black{};
|
||||
black.float32[3] = 1.0f;
|
||||
const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
context.Function<PFN_vkCmdClearColorImage>("vkCmdClearColorImage")(commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &range);
|
||||
transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT);
|
||||
AgcDriver::PresentationScaler::RecordBlitFrom(context, commands, source.Handle(), VK_IMAGE_LAYOUT_GENERAL, 256, 144, VK_FILTER_LINEAR, destination.Handle(), Extent.width, Extent.height);
|
||||
if (visible) {
|
||||
*bounds = overlay.Record(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, Extent, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, fullPanel(fixedDetails({256, 144}, Extent)), OverlayCorner::TopLeft);
|
||||
} else {
|
||||
transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT);
|
||||
}
|
||||
transition(context, commands, source.Handle(), VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_SHADER_READ_BIT);
|
||||
readImage(context, commands, destination, readback);
|
||||
batch.SubmitAndWait();
|
||||
}
|
||||
overlay.Release(0);
|
||||
return wordsOf(readback, Extent);
|
||||
}
|
||||
|
||||
static constexpr VkExtent2D Extent{400, 300};
|
||||
|
||||
private:
|
||||
const Context& context;
|
||||
PresentationPass pass;
|
||||
PerformanceOverlay overlay;
|
||||
Image source;
|
||||
};
|
||||
|
||||
void disabledIsUnchangedTest(const Context& context) {
|
||||
Require(!PerformanceOverlay::Visible(), "the performance overlay is shown without ANYPS5_OVERLAY=1");
|
||||
Presenter presenter(context);
|
||||
OverlayRect unused{};
|
||||
const auto drawnBefore = presenter.Drawn(&unused);
|
||||
const auto blittedBefore = presenter.Blitted(&unused);
|
||||
PerformanceOverlay::Toggle();
|
||||
Require(PerformanceOverlay::Visible(), "F10 does not show the performance overlay");
|
||||
OverlayRect bounds{};
|
||||
onlyInside(drawnBefore, presenter.Drawn(&bounds), Presenter::Extent, bounds, "presentation pass with the overlay", false);
|
||||
onlyInside(blittedBefore, presenter.Blitted(&bounds), Presenter::Extent, bounds, "blit with the overlay", false);
|
||||
PerformanceOverlay::Toggle();
|
||||
Require(!PerformanceOverlay::Visible(), "F10 does not hide the performance overlay again");
|
||||
Require(presenter.Drawn(&unused) == drawnBefore, "the presentation pass output differs after the overlay was hidden");
|
||||
Require(presenter.Blitted(&unused) == blittedBefore, "the blit output differs after the overlay was hidden");
|
||||
std::printf("overlay hidden: presentation pass and blit output bit-identical before and after showing it\n");
|
||||
}
|
||||
|
||||
void costReport(const Device& device, PerformanceOverlay& overlay) {
|
||||
const auto& context = device.GetContext();
|
||||
if (device.TimestampBits() == 0) {
|
||||
std::printf("the graphics queue has no timestamps; overlay cost not measured\n");
|
||||
return;
|
||||
}
|
||||
VkQueryPoolCreateInfo info{VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO};
|
||||
info.queryType = VK_QUERY_TYPE_TIMESTAMP;
|
||||
info.queryCount = 2;
|
||||
VkQueryPool queries = VK_NULL_HANDLE;
|
||||
Check(context.Function<PFN_vkCreateQueryPool>("vkCreateQueryPool")(context.device, &info, nullptr, &queries), "vkCreateQueryPool overlay cost");
|
||||
const auto sampled = std::chrono::steady_clock::now();
|
||||
for (int run = 0; run < 100; ++run) static_cast<void>(AgcDriver::ReadVramBudget(context));
|
||||
const auto vramUs = std::chrono::duration<double, std::micro>(std::chrono::steady_clock::now() - sampled).count() / 100;
|
||||
std::printf("VRAM budget read in %.1f us (%s), once a second while the overlay is shown\n", vramUs, device.MemoryBudget() ? "VK_EXT_memory_budget" : "no VK_EXT_memory_budget");
|
||||
for (const VkExtent2D extent : {VkExtent2D{1920, 1080}, VkExtent2D{3840, 2160}}) {
|
||||
Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT);
|
||||
const auto content = fullPanel(fixedDetails(extent, extent));
|
||||
std::vector<double> gpuMs;
|
||||
std::vector<double> cpuMs;
|
||||
for (int run = 0; run < 12; ++run) {
|
||||
CommandBatch batch(context);
|
||||
const auto commands = batch.Handle();
|
||||
context.Function<PFN_vkCmdResetQueryPool>("vkCmdResetQueryPool")(commands, queries, 0, 2);
|
||||
transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT);
|
||||
context.Function<PFN_vkCmdWriteTimestamp>("vkCmdWriteTimestamp")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, queries, 0);
|
||||
const auto recordStart = std::chrono::steady_clock::now();
|
||||
static_cast<void>(overlay.Record(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, content, OverlayCorner::TopLeft));
|
||||
cpuMs.push_back(std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - recordStart).count());
|
||||
context.Function<PFN_vkCmdWriteTimestamp>("vkCmdWriteTimestamp")(commands, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, queries, 1);
|
||||
batch.SubmitAndWait();
|
||||
overlay.Release(0);
|
||||
std::array<std::uint64_t, 2> stamps{};
|
||||
Check(context.Function<PFN_vkGetQueryPoolResults>("vkGetQueryPoolResults")(context.device, queries, 0, 2, sizeof(stamps), stamps.data(), sizeof(std::uint64_t), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT), "vkGetQueryPoolResults overlay cost");
|
||||
if (run >= 2) gpuMs.push_back(static_cast<double>(stamps[1] - stamps[0]) * context.limits.timestampPeriod / 1e6);
|
||||
}
|
||||
std::sort(gpuMs.begin(), gpuMs.end());
|
||||
std::sort(cpuMs.begin(), cpuMs.end());
|
||||
std::printf("full panel cost on %s at %ux%u: GPU median %.3f ms (min %.3f, max %.3f), recording median %.3f ms\n", device.Name().c_str(), extent.width, extent.height, gpuMs[gpuMs.size() / 2], gpuMs.front(), gpuMs.back(), cpuMs[cpuMs.size() / 2]);
|
||||
}
|
||||
context.Function<PFN_vkDestroyQueryPool>("vkDestroyQueryPool")(context.device, queries, nullptr);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
try {
|
||||
const std::string_view mode = argc > 1 ? argv[1] : "";
|
||||
Require(argc <= 2 && (mode.empty() || mode == "device-selection" || mode == "device-selection-cpu"), "unknown performance-overlay test mode");
|
||||
if (mode == "device-selection-cpu") {
|
||||
deviceSelectionTests();
|
||||
return 0;
|
||||
}
|
||||
setEnvironment("ANYPS5_OVERLAY", "");
|
||||
setEnvironment("ANYPS5_OVERLAY_POS", "");
|
||||
std::unique_ptr<Device> device;
|
||||
try {
|
||||
device = std::make_unique<Device>();
|
||||
} catch (const std::exception& error) {
|
||||
const char* requestedName = std::getenv("ANYPS5_GPU");
|
||||
if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr || (requestedName != nullptr && *requestedName != '\0')) throw;
|
||||
std::printf("skipped, no usable Vulkan device: %s\n", error.what());
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
device->VerifySelection();
|
||||
if (mode == "device-selection") return 0;
|
||||
const auto& context = device->GetContext();
|
||||
{
|
||||
PerformanceOverlay overlay(context, 1);
|
||||
sdrOverlayTest(context, overlay);
|
||||
hdrOverlayTest(context, overlay);
|
||||
cornerTest(context, overlay);
|
||||
fullPanelTest(*device, overlay);
|
||||
costReport(*device, overlay);
|
||||
}
|
||||
notePresentTest(*device);
|
||||
disabledIsUnchangedTest(context);
|
||||
std::cout << "performance overlay tests passed\n";
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,500 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp"
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::OverlayContent;
|
||||
using AgcDriver::OverlayCorner;
|
||||
using AgcDriver::OverlayMetrics;
|
||||
using AgcDriver::OverlayRect;
|
||||
using AgcDriver::OverlayStatistics;
|
||||
using Clock = OverlayStatistics::Clock;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
constexpr Clock::time_point Start = Clock::time_point{} + std::chrono::hours(1);
|
||||
|
||||
void require(bool condition, const std::string& reason) {
|
||||
if (!condition) throw std::runtime_error(reason);
|
||||
}
|
||||
|
||||
bool within(double value, double expected, double tolerance = 1e-9) {
|
||||
return std::abs(value - expected) <= tolerance;
|
||||
}
|
||||
|
||||
void fpsWindowTests() {
|
||||
OverlayStatistics statistics;
|
||||
require(statistics.Fps() == 0 && statistics.LastFrameMs() == 0 && statistics.FrameTimes().empty(), "a new overlay shows statistics");
|
||||
statistics.AddPresent(Start, 0, 0);
|
||||
require(statistics.Fps() == 0, "one present reads as a frame rate");
|
||||
for (int i = 1; i <= 120; ++i) statistics.AddPresent(Start + i * 10ms, 0, 0);
|
||||
require(within(statistics.Fps(), 100.0), "a present every 10 ms reads as " + std::to_string(statistics.Fps()) + " FPS, not 100");
|
||||
statistics.AddPresent(Start + 1700ms, 0, 0);
|
||||
require(within(statistics.Fps(), 51.0), "after a 500 ms stall the last second holds 52 presents, read as " + std::to_string(statistics.Fps()) + " FPS, not 51");
|
||||
statistics.AddPresent(Start + 3700ms, 0, 0);
|
||||
require(within(statistics.Fps(), 0.5), "after a 2 s stall the last second holds one present, read as " + std::to_string(statistics.Fps()) + " FPS, not the last frame's 0.5");
|
||||
statistics.AddPresent(Start + 3750ms, 0, 0);
|
||||
require(within(statistics.Fps(), 20.0), "two presents 50 ms apart read as " + std::to_string(statistics.Fps()) + " FPS, not 20");
|
||||
bool refused = false;
|
||||
try {
|
||||
statistics.AddPresent(Start + 3740ms, 0, 0);
|
||||
} catch (const std::exception&) {
|
||||
refused = true;
|
||||
}
|
||||
require(refused, "a present earlier than the previous one was accepted");
|
||||
statistics.Reset();
|
||||
require(statistics.Fps() == 0 && statistics.LastFrameMs() == 0 && statistics.FrameTimes().empty(), "a reset kept statistics");
|
||||
}
|
||||
|
||||
void frameRingTests() {
|
||||
OverlayStatistics statistics;
|
||||
auto at = Start;
|
||||
statistics.AddPresent(at, 0, 0);
|
||||
for (int i = 1; i <= 4; ++i) {
|
||||
at += std::chrono::milliseconds(i);
|
||||
statistics.AddPresent(at, 0, 0);
|
||||
}
|
||||
const auto partial = statistics.FrameTimes();
|
||||
require(partial.size() == 4 && within(partial[0], 1) && within(partial[3], 4), "the frame times of a short run are not in present order");
|
||||
for (int i = 5; i <= 150; ++i) {
|
||||
at += std::chrono::milliseconds(i);
|
||||
statistics.AddPresent(at, 0, 0);
|
||||
}
|
||||
const auto times = statistics.FrameTimes();
|
||||
require(times.size() == OverlayStatistics::FrameHistory, "the ring holds " + std::to_string(times.size()) + " frames, not 120");
|
||||
for (std::size_t i = 0; i < times.size(); ++i) require(within(times[i], 31.0 + static_cast<double>(i)), "frame " + std::to_string(i) + " of the ring is " + std::to_string(times[i]) + " ms");
|
||||
require(within(statistics.LastFrameMs(), 150), "the last frame is not the newest");
|
||||
const auto range = statistics.FrameTimeRange();
|
||||
require(within(range.minimum, 31) && within(range.maximum, 150) && within(range.average, 90.5), "the graph range of 31..150 ms reads as " + std::to_string(range.minimum) + "/" + std::to_string(range.average) + "/" + std::to_string(range.maximum));
|
||||
}
|
||||
|
||||
void counterTests() {
|
||||
OverlayStatistics statistics;
|
||||
constexpr std::uint64_t Earlier = 1'000'000'000u;
|
||||
for (std::uint64_t i = 0; i <= 60; ++i) statistics.AddPresent(Start + i * 16ms, Earlier + i * 4'000'000u, Earlier + i * 1'500'000u);
|
||||
require(within(statistics.GpuMsPerFrame(), 4.0) && within(statistics.DriverMsPerFrame(), 1.5), "4 ms of GPU and 1.5 ms of driver time per frame read as " + std::to_string(statistics.GpuMsPerFrame()) + " and " + std::to_string(statistics.DriverMsPerFrame()));
|
||||
statistics.AddPresent(Start + 61 * 16ms, 5, 5);
|
||||
require(statistics.GpuMsPerFrame() == 0 && statistics.DriverMsPerFrame() == 0, "a counter that went back is read as time");
|
||||
}
|
||||
|
||||
void formatTests() {
|
||||
const std::pair<double, const char*> cases[] = {{59.94, "59.9"}, {0, "0.0"}, {99.94, "99.9"}, {99.96, "100"}, {144.4, "144"}, {12345, "9999"}, {-3, "0.0"}, {std::nan(""), "0.0"}};
|
||||
for (const auto& [value, expected] : cases) require(AgcDriver::FormatOverlayValue(value) == expected, std::to_string(value) + " is shown as '" + AgcDriver::FormatOverlayValue(value) + "', not '" + expected + "'");
|
||||
const std::pair<double, const char*> rates[] = {{0.5, "0.50"}, {1000.0 / 12000.0, "0.08"}, {0.05, "0.05"}, {0.004, "0.00"}, {0.994, "0.99"}, {0.996, "1.0"}, {1.5, "1.5"}, {59.94, "59.9"}, {144.4, "144"}, {12345, "9999"}, {0, "0.0"}, {-1, "0.0"}, {std::nan(""), "0.0"}};
|
||||
for (const auto& [value, expected] : rates) require(AgcDriver::FormatOverlayRate(value) == expected, "a rate of " + std::to_string(value) + " is shown as '" + AgcDriver::FormatOverlayRate(value) + "', not '" + expected + "'");
|
||||
}
|
||||
|
||||
void lowRateTests() {
|
||||
OverlayStatistics slow;
|
||||
constexpr std::uint64_t Gpu[] = {30'000'000u, 45'000'000u, 20'000'000u};
|
||||
constexpr std::uint64_t Driver[] = {12'000'000u, 20'000'000u, 8'000'000u};
|
||||
std::uint64_t gpuTotal = 0;
|
||||
std::uint64_t driverTotal = 0;
|
||||
slow.AddPresent(Start, 0, 0);
|
||||
require(slow.Fps() == 0 && slow.GpuMsPerFrame() == 0 && slow.DriverMsPerFrame() == 0, "a single present reads as a frame");
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
gpuTotal += Gpu[i];
|
||||
driverTotal += Driver[i];
|
||||
slow.AddPresent(Start + (i + 1) * 2s, gpuTotal, driverTotal);
|
||||
require(within(slow.Fps(), 0.5) && within(slow.LastFrameMs(), 2000), "a present every 2 s reads as " + std::to_string(slow.Fps()) + " FPS and " + std::to_string(slow.LastFrameMs()) + " ms, not 0.5 and 2000");
|
||||
require(within(slow.GpuMsPerFrame(), static_cast<double>(Gpu[i]) / 1e6) && within(slow.DriverMsPerFrame(), static_cast<double>(Driver[i]) / 1e6), "frame " + std::to_string(i + 1) + " every 2 s reads " + std::to_string(slow.GpuMsPerFrame()) + " ms of GPU and " + std::to_string(slow.DriverMsPerFrame()) + " ms of driver time, not its own");
|
||||
}
|
||||
const auto everyTwoSeconds = AgcDriver::ComposeOverlay(slow, true, true);
|
||||
require(everyTwoSeconds.above == std::vector<std::string>{"PROFILED", "FPS: 0.50", "FRAME: 2000 MS"}, "a present every 2 s shows " + std::to_string(everyTwoSeconds.above.size()) + " lines above the graph, the last '" + everyTwoSeconds.above.back() + "'");
|
||||
require(everyTwoSeconds.below[0] == "MIN/AVG/MAX: 2000/2000/2000" && everyTwoSeconds.below[1] == "GPU: 20.0 MS" && everyTwoSeconds.below[2] == "DRIVER: 8.0 MS", "a present every 2 s shows '" + everyTwoSeconds.below[0] + "', '" + everyTwoSeconds.below[1] + "' and '" + everyTwoSeconds.below[2] + "'");
|
||||
|
||||
OverlayStatistics glacial;
|
||||
glacial.AddPresent(Start, 0, 0);
|
||||
for (int i = 1; i <= 5; ++i) glacial.AddPresent(Start + i * 12s, static_cast<std::uint64_t>(i) * 250'000'000u, static_cast<std::uint64_t>(i) * 12'500'000'000u);
|
||||
require(within(glacial.Fps(), 1000.0 / 12000.0) && within(glacial.LastFrameMs(), 12000) && within(glacial.GpuMsPerFrame(), 250) && within(glacial.DriverMsPerFrame(), 12500), "a present every 12 s reads as " + std::to_string(glacial.Fps()) + " FPS, " + std::to_string(glacial.LastFrameMs()) + " ms, " + std::to_string(glacial.GpuMsPerFrame()) + " and " + std::to_string(glacial.DriverMsPerFrame()) + " ms");
|
||||
const auto everyTwelveSeconds = AgcDriver::ComposeOverlay(glacial, true, true);
|
||||
require(everyTwelveSeconds.above == std::vector<std::string>{"PROFILED", "FPS: 0.08", "FRAME: 9999 MS"}, "a present every 12 s shows " + std::to_string(everyTwelveSeconds.above.size()) + " lines above the graph, the last '" + everyTwelveSeconds.above.back() + "'");
|
||||
require(everyTwelveSeconds.below[0] == "MIN/AVG/MAX: 9999/9999/9999" && everyTwelveSeconds.below[1] == "GPU: 250 MS" && everyTwelveSeconds.below[2] == "DRIVER: 9999 MS", "a present every 12 s shows '" + everyTwelveSeconds.below[0] + "', '" + everyTwelveSeconds.below[1] + "' and '" + everyTwelveSeconds.below[2] + "'");
|
||||
require(everyTwelveSeconds.frameTimes == std::vector<double>(5, 12000.0), "the graph does not hold the five 12 s frames");
|
||||
const auto layout = AgcDriver::LayOutOverlay(everyTwelveSeconds, {1920, 1080}, OverlayCorner::TopLeft);
|
||||
std::size_t bars = 0;
|
||||
for (const auto& rect : layout.rects) {
|
||||
if (rect.color != static_cast<std::uint32_t>(AgcDriver::OverlayColor::Graph)) continue;
|
||||
++bars;
|
||||
require(rect.height == OverlayMetrics::GraphHeight * layout.scale / 2, "steady 12 s frames do not reach half the graph");
|
||||
}
|
||||
require(bars == 5, "the graph has " + std::to_string(bars) + " bars for five 12 s frames");
|
||||
|
||||
OverlayStatistics rising;
|
||||
auto at = Start;
|
||||
std::uint64_t total = 0;
|
||||
rising.AddPresent(at, 0, 0);
|
||||
const auto step = [&](Clock::duration interval, std::uint64_t gpuNanoseconds) {
|
||||
at += interval;
|
||||
total += gpuNanoseconds;
|
||||
rising.AddPresent(at, total, 0);
|
||||
};
|
||||
const auto check = [&](double fps, double gpuMs, const char* shown, const std::string& when) {
|
||||
require(within(rising.Fps(), fps) && within(rising.GpuMsPerFrame(), gpuMs) && AgcDriver::FormatOverlayRate(rising.Fps()) == shown, when + " reads as " + std::to_string(rising.Fps()) + " FPS ('" + AgcDriver::FormatOverlayRate(rising.Fps()) + "') and " + std::to_string(rising.GpuMsPerFrame()) + " ms of GPU time, not " + std::to_string(fps) + " ('" + shown + "') and " + std::to_string(gpuMs));
|
||||
};
|
||||
step(2s, 40'000'000u);
|
||||
check(0.5, 40, "0.50", "a 2 s frame");
|
||||
step(1250ms, 30'000'000u);
|
||||
check(0.8, 30, "0.80", "a 1.25 s frame");
|
||||
step(1001ms, 20'000'000u);
|
||||
check(1000.0 / 1001.0, 20, "1.0", "a 1.001 s frame, the last second holding one present");
|
||||
step(1000ms, 10'000'000u);
|
||||
check(1.0, 10, "1.0", "a 1 s frame, the last second holding two presents");
|
||||
step(750ms, 6'000'000u);
|
||||
check(1.0 / 0.75, 6, "1.3", "a 750 ms frame");
|
||||
for (int i = 0; i < 8; ++i) step(250ms, i % 2 == 0 ? 2'000'000u : 6'000'000u);
|
||||
check(4.0, 4.0, "4.0", "four presents a second, alternating 2 and 6 ms of GPU time");
|
||||
step(3s, 9'000'000u);
|
||||
check(1.0 / 3.0, 9, "0.33", "a 3 s frame after the fast ones");
|
||||
}
|
||||
|
||||
void settingsTests() {
|
||||
const auto hidden = AgcDriver::ParseOverlaySettings(nullptr, nullptr);
|
||||
require(!hidden.visible && hidden.corner == OverlayCorner::TopLeft, "the overlay is not off and top-left by default");
|
||||
require(!AgcDriver::ParseOverlaySettings("", "").visible && !AgcDriver::ParseOverlaySettings("0", nullptr).visible, "an empty or 0 ANYPS5_OVERLAY shows the overlay");
|
||||
require(AgcDriver::ParseOverlaySettings("1", nullptr).visible, "ANYPS5_OVERLAY=1 does not show the overlay");
|
||||
const std::pair<const char*, OverlayCorner> corners[] = {{"tl", OverlayCorner::TopLeft}, {"tr", OverlayCorner::TopRight}, {"bl", OverlayCorner::BottomLeft}, {"br", OverlayCorner::BottomRight}};
|
||||
for (const auto& [name, corner] : corners) require(AgcDriver::ParseOverlaySettings("1", name).corner == corner, std::string("ANYPS5_OVERLAY_POS=") + name + " picks another corner");
|
||||
const auto refused = [](const char* enabled, const char* position, const char* variable) {
|
||||
try {
|
||||
static_cast<void>(AgcDriver::ParseOverlaySettings(enabled, position));
|
||||
} catch (const std::exception& error) {
|
||||
return std::string(error.what()).find(variable) != std::string::npos;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
require(refused("yes", nullptr, "ANYPS5_OVERLAY") && refused("2", nullptr, "ANYPS5_OVERLAY"), "an ANYPS5_OVERLAY value other than 1 or 0 was accepted or not named");
|
||||
require(refused("1", "center", "ANYPS5_OVERLAY_POS") && refused(nullptr, "TL", "ANYPS5_OVERLAY_POS"), "an unknown ANYPS5_OVERLAY_POS was accepted or not named");
|
||||
}
|
||||
|
||||
void composeTests() {
|
||||
OverlayStatistics statistics;
|
||||
for (std::uint64_t i = 0; i <= 100; ++i) statistics.AddPresent(Start + i * 10ms, i * 2'000'000u, i * 500'000u);
|
||||
const auto plain = AgcDriver::ComposeOverlay(statistics, false, false);
|
||||
require(plain.above.size() == 2 && plain.above[0] == "FPS: 100" && plain.above[1] == "FRAME: 10.0 MS", "the frame lines read '" + plain.above[0] + "' and '" + plain.above[1] + "'");
|
||||
require(plain.below.size() == 1 && plain.below[0] == "MIN/AVG/MAX: 10.0/10.0/10.0", "the graph line reads '" + plain.below[0] + "'");
|
||||
require(plain.frameTimes.size() == 100, "the graph does not hold every frame time");
|
||||
const auto profiled = AgcDriver::ComposeOverlay(statistics, true, true);
|
||||
require(profiled.below.size() == 3 && profiled.below[1] == "GPU: 2.0 MS" && profiled.below[2] == "DRIVER: 0.5 MS", "the profiled lines read '" + profiled.below[1] + "' and '" + profiled.below[2] + "'");
|
||||
std::vector<std::string> marked{"PROFILED"};
|
||||
marked.insert(marked.end(), plain.above.begin(), plain.above.end());
|
||||
require(profiled.above == marked && AgcDriver::ComposeOverlay(statistics, true, false).above == marked && AgcDriver::ComposeOverlay(statistics, false, true).above == marked, "a profiled panel does not start with PROFILED above the unprofiled lines: it starts with '" + profiled.above.front() + "'");
|
||||
require(AgcDriver::ComposeOverlay(statistics, true, false).below.size() == 2 && AgcDriver::ComposeOverlay(statistics, false, true).below[1].rfind("DRIVER", 0) == 0, "a line is shown without its source");
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> coverage(const AgcDriver::OverlayLayout& layout, std::uint32_t color) {
|
||||
const auto& bounds = layout.bounds;
|
||||
std::vector<std::uint8_t> covered(static_cast<std::size_t>(bounds.width) * static_cast<std::size_t>(bounds.height), 0);
|
||||
for (const auto& rect : layout.rects) {
|
||||
if (rect.color != color) continue;
|
||||
for (std::int32_t y = rect.y; y < rect.y + rect.height; ++y) {
|
||||
for (std::int32_t x = rect.x; x < rect.x + rect.width; ++x) ++covered[static_cast<std::size_t>(y - bounds.y) * static_cast<std::size_t>(bounds.width) + static_cast<std::size_t>(x - bounds.x)];
|
||||
}
|
||||
}
|
||||
return covered;
|
||||
}
|
||||
|
||||
void requireLineDrawn(const AgcDriver::OverlayLayout& layout, const std::vector<std::uint8_t>& text, const std::string& line, std::int32_t top) {
|
||||
const auto s = layout.scale;
|
||||
for (std::size_t column = 0; column < line.size(); ++column) {
|
||||
const auto& glyph = AgcDriver::OverlayGlyph(line[column]);
|
||||
for (std::int32_t gy = 0; gy < OverlayMetrics::GlyphHeight; ++gy) {
|
||||
for (std::int32_t gx = 0; gx < OverlayMetrics::Advance; ++gx) {
|
||||
const bool set = gx < OverlayMetrics::GlyphWidth && (glyph[static_cast<std::size_t>(gy)] & (0x10u >> gx)) != 0;
|
||||
for (std::int32_t py = 0; py < s; ++py) {
|
||||
for (std::int32_t px = 0; px < s; ++px) {
|
||||
const auto x = (OverlayMetrics::Padding + static_cast<std::int32_t>(column) * OverlayMetrics::Advance + gx) * s + px;
|
||||
const auto y = (top + gy) * s + py;
|
||||
require(text[static_cast<std::size_t>(y) * static_cast<std::size_t>(layout.bounds.width) + static_cast<std::size_t>(x)] == (set ? 1 : 0), "'" + std::string(1, line[column]) + "' of '" + line + "' is drawn wrong at glyph pixel " + std::to_string(gx) + "," + std::to_string(gy));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void layoutTests() {
|
||||
OverlayContent content{{"FPS: 60.0", "FRAME: 16.7 MS"}, {}, {"MIN/AVG/MAX: 16.7/16.7/16.7"}};
|
||||
for (int i = 0; i < 40; ++i) content.frameTimes.push_back(i == 39 ? 50.0 : 10.0);
|
||||
const std::pair<VkExtent2D, std::int32_t> scales[] = {{{1280, 720}, 2}, {{1920, 1080}, 2}, {{2560, 1440}, 3}, {{3840, 2160}, 5}, {{320, 180}, 1}, {{2560, 720}, 2}};
|
||||
for (const auto& [extent, scale] : scales) require(AgcDriver::OverlayScale(extent, content) == scale, std::to_string(extent.width) + "x" + std::to_string(extent.height) + " draws the overlay at scale " + std::to_string(AgcDriver::OverlayScale(extent, content)) + ", not " + std::to_string(scale));
|
||||
|
||||
const VkExtent2D extent{1920, 1080};
|
||||
const auto layout = AgcDriver::LayOutOverlay(content, extent, OverlayCorner::TopLeft);
|
||||
const auto s = layout.scale;
|
||||
const auto margin = OverlayMetrics::Margin * s;
|
||||
require(layout.bounds.x == margin && layout.bounds.y == margin && layout.bounds.width == (OverlayMetrics::InnerWidth + 2 * OverlayMetrics::Padding) * s, "the top-left panel is not at the margin");
|
||||
require(layout.rects.front().x == layout.bounds.x && layout.rects.front().width == layout.bounds.width && layout.rects.front().color == static_cast<std::uint32_t>(AgcDriver::OverlayColor::Background), "the background is not drawn first over the whole panel");
|
||||
for (const auto& rect : layout.rects) {
|
||||
require(rect.width > 0 && rect.height > 0 && rect.x >= layout.bounds.x && rect.y >= layout.bounds.y && rect.x + rect.width <= layout.bounds.x + layout.bounds.width && rect.y + rect.height <= layout.bounds.y + layout.bounds.height, "a rectangle leaves the panel");
|
||||
}
|
||||
|
||||
const auto text = coverage(layout, static_cast<std::uint32_t>(AgcDriver::OverlayColor::Text));
|
||||
const auto lineTop = [&](std::size_t line) { return OverlayMetrics::Padding + static_cast<std::int32_t>(line) * OverlayMetrics::LinePitch; };
|
||||
requireLineDrawn(layout, text, content.above[0], lineTop(0));
|
||||
requireLineDrawn(layout, text, content.above[1], lineTop(1));
|
||||
requireLineDrawn(layout, text, content.below[0], lineTop(2) + OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap);
|
||||
|
||||
std::vector<OverlayRect> bars;
|
||||
for (const auto& rect : layout.rects) {
|
||||
if (rect.color == static_cast<std::uint32_t>(AgcDriver::OverlayColor::Graph)) bars.push_back(rect);
|
||||
}
|
||||
require(bars.size() == content.frameTimes.size(), "the graph has " + std::to_string(bars.size()) + " bars for 40 frames");
|
||||
const auto graphBottom = layout.bounds.y + (lineTop(2) + OverlayMetrics::GraphHeight) * s;
|
||||
const auto full = OverlayMetrics::GraphHeight * s;
|
||||
require(bars.back().x == layout.bounds.x + (OverlayMetrics::Padding + static_cast<std::int32_t>(OverlayStatistics::FrameHistory) - 1) * s, "the newest frame is not at the right end of the graph");
|
||||
for (std::size_t i = 0; i + 1 < bars.size(); ++i) require(bars[i + 1].x == bars[i].x + s && bars[i].width == s, "the graph bars are not side by side");
|
||||
require(bars.back().height == full && bars.back().y + bars.back().height == graphBottom, "the slowest frame does not fill the graph");
|
||||
require(bars.front().height == static_cast<std::int32_t>(std::lround(10.0 / 50.0 * full)) && bars.front().y + bars.front().height == graphBottom, "a 10 ms frame is not a fifth of a 50 ms one");
|
||||
OverlayContent steady = content;
|
||||
std::fill(steady.frameTimes.begin(), steady.frameTimes.end(), 16.0);
|
||||
for (const auto& rect : AgcDriver::LayOutOverlay(steady, extent, OverlayCorner::TopLeft).rects) {
|
||||
if (rect.color == static_cast<std::uint32_t>(AgcDriver::OverlayColor::Graph)) require(rect.height == full / 2, "steady frames do not reach half the graph");
|
||||
}
|
||||
|
||||
const std::pair<OverlayCorner, std::pair<bool, bool>> corners[] = {{OverlayCorner::TopRight, {true, false}}, {OverlayCorner::BottomLeft, {false, true}}, {OverlayCorner::BottomRight, {true, true}}};
|
||||
for (const auto& [corner, sides] : corners) {
|
||||
const auto placed = AgcDriver::LayOutOverlay(content, extent, corner).bounds;
|
||||
const auto x = sides.first ? static_cast<std::int32_t>(extent.width) - margin - placed.width : margin;
|
||||
const auto y = sides.second ? static_cast<std::int32_t>(extent.height) - margin - placed.height : margin;
|
||||
require(placed.x == x && placed.y == y && placed.width == layout.bounds.width && placed.height == layout.bounds.height, "a corner places the panel elsewhere");
|
||||
}
|
||||
OverlayContent profiled = content;
|
||||
profiled.below.push_back("GPU: 2.0 MS");
|
||||
require(AgcDriver::LayOutOverlay(profiled, extent, OverlayCorner::TopLeft).bounds.height == layout.bounds.height + OverlayMetrics::LinePitch * s, "a profiled line does not add one line to the panel");
|
||||
|
||||
bool refused = false;
|
||||
try {
|
||||
static_cast<void>(AgcDriver::OverlayGlyph('q'));
|
||||
} catch (const std::exception&) {
|
||||
refused = true;
|
||||
}
|
||||
require(refused, "a character without a glyph was drawn");
|
||||
}
|
||||
|
||||
constexpr std::uint64_t GiB = 1ull << 30u;
|
||||
|
||||
OverlayStatistics countedStatistics() {
|
||||
OverlayStatistics statistics;
|
||||
for (std::uint64_t i = 0; i <= 60; ++i) {
|
||||
AgcDriver::OverlayCounters counters;
|
||||
counters.gpuNanoseconds = i * 4'000'000u;
|
||||
counters.driverNanoseconds = i * 1'500'000u;
|
||||
counters.draws = 5000 + i * 1240;
|
||||
counters.dispatches = 70 + i * 85;
|
||||
counters.pipelines = i < 50 ? 1500 : 1503;
|
||||
statistics.AddPresent(Start + i * 16ms, counters);
|
||||
}
|
||||
return statistics;
|
||||
}
|
||||
|
||||
AgcDriver::OverlayDetails fullDetails() {
|
||||
AgcDriver::OverlayDetails details;
|
||||
details.vram = AgcDriver::OverlayMemory{static_cast<std::uint64_t>(6.1 * GiB), static_cast<std::uint64_t>(7.8 * GiB)};
|
||||
details.ram = static_cast<std::uint64_t>(3.2 * GiB);
|
||||
details.display = {1920, 1080};
|
||||
details.window = {2560, 1440};
|
||||
return details;
|
||||
}
|
||||
|
||||
void packetStatisticsTests() {
|
||||
const auto statistics = countedStatistics();
|
||||
require(within(statistics.DrawsPerFrame(), 1240) && within(statistics.DispatchesPerFrame(), 85), "1240 draws and 85 dispatches per frame read as " + std::to_string(statistics.DrawsPerFrame()) + " and " + std::to_string(statistics.DispatchesPerFrame()));
|
||||
require(statistics.PipelinesInWindow() == 3 && statistics.Latest().pipelines == 1503, "3 pipelines created within the last second of 1503 read as " + std::to_string(statistics.PipelinesInWindow()) + " of " + std::to_string(statistics.Latest().pipelines));
|
||||
require(within(statistics.GpuMsPerFrame(), 4.0) && within(statistics.DriverMsPerFrame(), 1.5), "the time counters changed with the packet counters");
|
||||
auto stalled = statistics;
|
||||
auto counters = stalled.Latest();
|
||||
counters.draws += 900;
|
||||
counters.dispatches += 40;
|
||||
counters.pipelines += 7;
|
||||
stalled.AddPresent(Start + 3000ms, counters);
|
||||
require(within(stalled.DrawsPerFrame(), 900) && within(stalled.DispatchesPerFrame(), 40) && stalled.PipelinesInWindow() == 7 && stalled.Latest().pipelines == 1510, "a present alone in its second reads as " + std::to_string(stalled.DrawsPerFrame()) + " draws, " + std::to_string(stalled.DispatchesPerFrame()) + " dispatches and " + std::to_string(stalled.PipelinesInWindow()) + " pipelines, not the last frame's 900, 40 and 7");
|
||||
auto restarted = statistics;
|
||||
counters = restarted.Latest();
|
||||
counters.draws = 3;
|
||||
restarted.AddPresent(Start + 61 * 16ms, counters);
|
||||
require(restarted.DrawsPerFrame() == 0, "a draw counter that went back is read as draws");
|
||||
OverlayStatistics empty;
|
||||
require(empty.PipelinesInWindow() == 0 && empty.Latest().draws == 0, "an overlay without presents shows packet counts");
|
||||
}
|
||||
|
||||
void packetCounterTests() {
|
||||
const auto before = AgcDriver::PacketCounters::Sum();
|
||||
auto& counters = AgcDriver::PacketCounters::ThisThread();
|
||||
require(&counters == &AgcDriver::PacketCounters::ThisThread(), "one thread got two packet counters");
|
||||
for (int i = 0; i < 1240; ++i) counters.CountDraw();
|
||||
for (int i = 0; i < 85; ++i) counters.CountDispatch();
|
||||
bool separate = false;
|
||||
std::thread worker([&] {
|
||||
auto& own = AgcDriver::PacketCounters::ThisThread();
|
||||
separate = &own != &counters;
|
||||
for (int i = 0; i < 1000; ++i) own.CountDraw();
|
||||
own.CountDispatch();
|
||||
});
|
||||
worker.join();
|
||||
require(separate, "two workers share packet counters");
|
||||
const auto after = AgcDriver::PacketCounters::Sum();
|
||||
require(after.draws - before.draws == 2240 && after.dispatches - before.dispatches == 86, "two workers' 2240 draws and 86 dispatches sum to " + std::to_string(after.draws - before.draws) + " and " + std::to_string(after.dispatches - before.dispatches));
|
||||
std::thread next([] { AgcDriver::PacketCounters::ThisThread().CountDraw(); });
|
||||
next.join();
|
||||
require(AgcDriver::PacketCounters::Sum().draws == after.draws + 1, "the counts of a worker that ended were lost, or a new worker restarted them");
|
||||
}
|
||||
|
||||
void countFormatTests() {
|
||||
const std::pair<double, const char*> counts[] = {{0, "0"}, {0.4, "0"}, {0.5, "1"}, {85, "85"}, {1239.6, "1240"}, {9999, "9999"}, {9999.5, "10K"}, {12345, "12K"}, {999999, "999K"}, {1e6, "1M"}, {2.5e9, "999M"}, {-3, "0"}, {std::nan(""), "0"}, {INFINITY, "0"}};
|
||||
for (const auto& [value, expected] : counts) require(AgcDriver::FormatOverlayCount(value) == expected, std::to_string(value) + " is counted as '" + AgcDriver::FormatOverlayCount(value) + "', not '" + expected + "'");
|
||||
const std::pair<std::uint64_t, const char*> sizes[] = {{0, "0.0"}, {static_cast<std::uint64_t>(6.1 * GiB), "6.1"}, {static_cast<std::uint64_t>(7.8 * GiB), "7.8"}, {GiB / 2, "0.5"}, {1536 * GiB, "1536"}, {20000 * GiB, "9999"}};
|
||||
for (const auto& [bytes, expected] : sizes) require(AgcDriver::FormatOverlayGigabytes(bytes) == expected, std::to_string(bytes) + " bytes are shown as '" + AgcDriver::FormatOverlayGigabytes(bytes) + "' GB, not '" + expected + "'");
|
||||
}
|
||||
|
||||
void detailComposeTests() {
|
||||
const auto statistics = countedStatistics();
|
||||
const auto full = AgcDriver::ComposeOverlay(statistics, true, true, fullDetails());
|
||||
const std::vector<std::string> expected{"MIN/AVG/MAX: 16.0/16.0/16.0", "GPU: 4.0 MS", "DRIVER: 1.5 MS", "DRAWS: 1240 DISPATCH: 85", "SHADERS: 3 TOTAL: 1503", "VRAM: 6.1/7.8 GB", "RAM: 3.2 GB", "RES: 1920X1080 / 2560X1440"};
|
||||
require(full.below == expected, "the full panel's lines below the graph read '" + (full.below.empty() ? std::string() : full.below.back()) + "' last, of " + std::to_string(full.below.size()));
|
||||
for (const auto& line : full.below) require(line.size() <= static_cast<std::size_t>(OverlayMetrics::Columns), "'" + line + "' is wider than the panel");
|
||||
|
||||
OverlayStatistics uncounted;
|
||||
for (int i = 0; i <= 60; ++i) uncounted.AddPresent(Start + i * 16ms, 0, 0);
|
||||
const auto plain = AgcDriver::ComposeOverlay(uncounted, false, false);
|
||||
const auto none = AgcDriver::ComposeOverlay(uncounted, false, false, AgcDriver::OverlayDetails{});
|
||||
require(none.above == plain.above && none.below == plain.below && none.below.size() == 1, "a panel without packets, pipelines, memory or a display buffer shows more than the first lines");
|
||||
|
||||
auto details = fullDetails();
|
||||
details.vram.reset();
|
||||
details.ram.reset();
|
||||
details.display = {};
|
||||
const auto partial = AgcDriver::ComposeOverlay(statistics, false, false, details);
|
||||
require(partial.below.size() == 3 && partial.below.back().rfind("SHADERS", 0) == 0, "a line is shown without its data: the last line is '" + partial.below.back() + "'");
|
||||
|
||||
details = fullDetails();
|
||||
details.display = {3840, 2160};
|
||||
details.window = {16384, 16384};
|
||||
require(AgcDriver::ComposeOverlay(uncounted, false, false, details).below.back() == "RES: 3840X2160/16384X16384", "a long RES line is not written without spaces");
|
||||
details.display = {12345, 12345};
|
||||
details.window = {12345, 12345};
|
||||
require(AgcDriver::ComposeOverlay(uncounted, false, false, details).below.back() == "RAM: 3.2 GB", "a RES line wider than the panel is shown");
|
||||
details = fullDetails();
|
||||
details.window = {};
|
||||
require(AgcDriver::ComposeOverlay(uncounted, false, false, details).below.back() == "RAM: 3.2 GB", "RES is shown without a window size");
|
||||
auto large = countedStatistics();
|
||||
auto counters = large.Latest();
|
||||
counters.draws += 12345 * 2;
|
||||
counters.pipelines = 123456;
|
||||
large.AddPresent(Start + 62 * 16ms, counters);
|
||||
const auto busy = AgcDriver::ComposeOverlay(large, false, false);
|
||||
require(busy.below.size() == 3 && busy.below[2] == "SHADERS: 121K TOTAL: 123K", "large counts are not shortened: '" + busy.below.back() + "'");
|
||||
}
|
||||
|
||||
void fullPanelLayoutTests() {
|
||||
auto content = AgcDriver::ComposeOverlay(countedStatistics(), true, true, fullDetails());
|
||||
content.below[3] = "DRAWS: 9999 DISPATCH: 12K";
|
||||
content.below[4] = "SHADERS: 999M TOTAL: 9999";
|
||||
const VkExtent2D hd{1920, 1080};
|
||||
const auto layout = AgcDriver::LayOutOverlay(content, hd, OverlayCorner::TopLeft);
|
||||
require(layout.scale == 2 && layout.bounds.height * 3 <= static_cast<std::int32_t>(hd.height), "the full panel at 1080p is " + std::to_string(layout.bounds.height) + " pixels tall at scale " + std::to_string(layout.scale) + ", more than a third of the window");
|
||||
const auto text = coverage(layout, static_cast<std::uint32_t>(AgcDriver::OverlayColor::Text));
|
||||
std::int32_t top = OverlayMetrics::Padding;
|
||||
for (const auto& line : content.above) {
|
||||
requireLineDrawn(layout, text, line, top);
|
||||
top += OverlayMetrics::LinePitch;
|
||||
}
|
||||
top += OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap;
|
||||
for (const auto& line : content.below) {
|
||||
requireLineDrawn(layout, text, line, top);
|
||||
top += OverlayMetrics::LinePitch;
|
||||
}
|
||||
require(layout.bounds.height == (top - OverlayMetrics::LinePitch + OverlayMetrics::GlyphHeight + OverlayMetrics::Padding) * layout.scale, "the panel does not end one padding below its last line");
|
||||
const std::pair<VkExtent2D, std::int32_t> scales[] = {{{1280, 720}, 2}, {{2560, 1440}, 3}, {{3840, 2160}, 5}, {{320, 180}, 1}};
|
||||
for (const auto& [extent, scale] : scales) {
|
||||
const auto placed = AgcDriver::LayOutOverlay(content, extent, OverlayCorner::BottomRight);
|
||||
require(placed.scale == scale && placed.bounds.x >= 0 && placed.bounds.y >= 0 && placed.bounds.y + placed.bounds.height <= static_cast<std::int32_t>(extent.height), "the full panel does not fit " + std::to_string(extent.width) + "x" + std::to_string(extent.height) + " at scale " + std::to_string(scale));
|
||||
}
|
||||
const std::string added = "BCHKLOTW";
|
||||
for (const char letter : added) {
|
||||
const auto& glyph = AgcDriver::OverlayGlyph(letter);
|
||||
require(std::any_of(glyph.begin(), glyph.end(), [](std::uint8_t row) { return row != 0; }) && std::all_of(glyph.begin(), glyph.end(), [](std::uint8_t row) { return row <= 0x1f; }), std::string("the glyph of '") + letter + "' is empty or wider than 5 pixels");
|
||||
for (const char other : std::string("0123456789ADEFGIMNPRSUVX") + added) {
|
||||
if (other != letter) require(glyph != AgcDriver::OverlayGlyph(other), std::string("'") + letter + "' and '" + other + "' have the same glyph");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void sampleTests() {
|
||||
require(AgcDriver::OverlaySampleDue(std::nullopt, Start), "the first frame is not sampled");
|
||||
require(!AgcDriver::OverlaySampleDue(Start, Start + 999ms) && AgcDriver::OverlaySampleDue(Start, Start + 1000ms), "memory is sampled more than once a second, or not after a second");
|
||||
static std::uint8_t* volatile touched = nullptr;
|
||||
const auto before = AgcDriver::ReadProcessPrivateBytes();
|
||||
require(before.has_value() && *before != 0, "the process's private memory cannot be read");
|
||||
constexpr std::size_t Bytes = std::size_t{64} << 20u;
|
||||
const auto block = std::make_unique<std::uint8_t[]>(Bytes);
|
||||
touched = block.get();
|
||||
std::memset(touched, 0x5a, Bytes);
|
||||
const auto after = AgcDriver::ReadProcessPrivateBytes();
|
||||
require(after.has_value() && *after >= *before + Bytes * 3 / 4, "touching 64 MiB raised the private memory from " + std::to_string(*before) + " to " + std::to_string(after.value_or(0)) + " bytes");
|
||||
require(touched[Bytes - 1] == 0x5a, "the touched block was not written");
|
||||
}
|
||||
|
||||
void costReport() {
|
||||
auto& counters = AgcDriver::PacketCounters::ThisThread();
|
||||
constexpr int Increments = 20'000'000;
|
||||
const auto counted = Clock::now();
|
||||
for (int i = 0; i < Increments; ++i) counters.CountDraw();
|
||||
const auto countNs = std::chrono::duration<double, std::nano>(Clock::now() - counted).count() / Increments;
|
||||
const auto summed = Clock::now();
|
||||
for (int i = 0; i < 1000; ++i) static_cast<void>(AgcDriver::PacketCounters::Sum());
|
||||
const auto sumUs = std::chrono::duration<double, std::micro>(Clock::now() - summed).count() / 1000;
|
||||
const auto sampled = Clock::now();
|
||||
for (int i = 0; i < 100; ++i) static_cast<void>(AgcDriver::ReadProcessPrivateBytes());
|
||||
const auto ramUs = std::chrono::duration<double, std::micro>(Clock::now() - sampled).count() / 100;
|
||||
std::printf("hidden cost: %.2f ns per counted draw or dispatch packet; shown: %.2f us to sum the packet counters per frame, %.1f us to read the private memory once a second\n", countNs, sumUs, ramUs);
|
||||
}
|
||||
|
||||
void paletteTests() {
|
||||
const auto sdr = AgcDriver::OverlayPalette(VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR);
|
||||
require(sdr[1] == std::array<float, 4>{1.0f, 1.0f, 1.0f, 1.0f} && sdr[0][3] > 0.0f && sdr[0][3] < 1.0f, "the SDR palette is not opaque white text over a translucent panel");
|
||||
const auto srgb = AgcDriver::OverlayPalette(VK_FORMAT_B8G8R8A8_SRGB, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR);
|
||||
require(within(srgb[2][1], std::pow((0.8 + 0.055) / 1.055, 2.4), 1e-5) && srgb[1][0] == 1.0f, "an sRGB swapchain gets encoded colors");
|
||||
const auto hdr = AgcDriver::OverlayPalette(VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_COLOR_SPACE_HDR10_ST2084_EXT);
|
||||
for (std::size_t channel = 0; channel < 3; ++channel) require(within(hdr[1][channel], 0.5807, 0.0015), "HDR10 white text is not at 203 nits (PQ " + std::to_string(hdr[1][channel]) + ")");
|
||||
require(std::lround(hdr[0][0] * 1023.0f) == 0 && hdr[0][3] == sdr[0][3], "the HDR10 panel is not black at the same opacity");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
fpsWindowTests();
|
||||
frameRingTests();
|
||||
counterTests();
|
||||
formatTests();
|
||||
lowRateTests();
|
||||
settingsTests();
|
||||
composeTests();
|
||||
layoutTests();
|
||||
paletteTests();
|
||||
packetStatisticsTests();
|
||||
packetCounterTests();
|
||||
countFormatTests();
|
||||
detailComposeTests();
|
||||
fullPanelLayoutTests();
|
||||
sampleTests();
|
||||
costReport();
|
||||
std::cout << "performance overlay statistics, layout, palette, packet, pipeline, memory and resolution tests passed\n";
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,8 @@ void CheckEveryHeap() {
|
||||
continue;
|
||||
}
|
||||
const auto shape = AgcDriver::Graphics::PlaceholderImageShapeOf(heap);
|
||||
const auto withoutStorageMsaa = AgcDriver::Graphics::PlaceholderImageShapeForDevice(heap, false);
|
||||
const auto withStorageMsaa = AgcDriver::Graphics::PlaceholderImageShapeForDevice(heap, true);
|
||||
const auto where = " for heap " + std::to_string(heap);
|
||||
const bool storage = resourceClass == ImageResourceClass::Storage;
|
||||
const bool multisampled = dimension == RdnaImageDimension::Dim2DMsaa || dimension == RdnaImageDimension::Dim2DMsaaArray;
|
||||
@@ -73,6 +75,10 @@ void CheckEveryHeap() {
|
||||
Require(shape.format == Format(image), "placeholder format disagrees with the numeric class" + where);
|
||||
Require(shape.viewType == ViewType(dimension), "placeholder view type disagrees with the dimension" + where);
|
||||
Require((shape.samples != VK_SAMPLE_COUNT_1_BIT) == multisampled, "placeholder sample count disagrees with multisampling" + where);
|
||||
Require(withStorageMsaa.has_value(), "an enabled device lost a placeholder image shape" + where);
|
||||
Require(withoutStorageMsaa.has_value() == !(storage && multisampled), "a device without storage-image MSAA exposes an unsupported placeholder" + where);
|
||||
if (withoutStorageMsaa) Require(withoutStorageMsaa->samples == shape.samples && withoutStorageMsaa->viewType == shape.viewType && withoutStorageMsaa->usage == shape.usage,
|
||||
"a device-specific placeholder changed the typed heap shape" + where);
|
||||
Require(((shape.features & VK_FORMAT_FEATURE_STORAGE_IMAGE_ATOMIC_BIT) != 0) == atomic, "placeholder format features disagree with atomics" + where);
|
||||
heaps.insert(heap);
|
||||
}
|
||||
|
||||
@@ -805,6 +805,22 @@ void testEventWrite() {
|
||||
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x24}), 0x20); }, "compute queue");
|
||||
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x424}), 0); }, "VGT_FLUSH event index");
|
||||
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x1e, 0x1000, 0}), 0); }, "event type 30");
|
||||
for (const auto eventType : {0x01u, 0x02u, 0x03u, 0x20u}) {
|
||||
AgcDriver::Pm4::Validate(makePacket(0x46, {eventType}), 0);
|
||||
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {eventType}), 0x20); }, "compute queue");
|
||||
for (std::uint32_t index = 1; index < 8; ++index) {
|
||||
if (index == 3) continue;
|
||||
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {(index << 8u) | eventType}), 0); }, "stream-out statistics event index");
|
||||
}
|
||||
for (const auto index : {3u, 8u, 9u, 10u, 11u}) {
|
||||
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {(index << 8u) | eventType, 0x1000, 0x2}), 0); }, "stream-out statistics sampling to memory is not implemented");
|
||||
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {(index << 8u) | eventType}), 0); }, "stream-out statistics sampling to memory is not implemented");
|
||||
}
|
||||
for (const auto index : {12u, 13u, 14u, 15u}) {
|
||||
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {(index << 8u) | eventType}), 0); }, "reserved bits");
|
||||
}
|
||||
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {eventType, 0x1000, 0x2}), 0); }, "packet size");
|
||||
}
|
||||
}
|
||||
|
||||
void testAcquireMem() {
|
||||
@@ -909,6 +925,8 @@ void testDriverSubmission() {
|
||||
makePacket(0x46, {0x2a}),
|
||||
makePacket(0x46, {0x72c}),
|
||||
makePacket(0x46, {0x2e}),
|
||||
makePacket(0x46, {0x01}),
|
||||
makePacket(0x46, {0x20}),
|
||||
makePacket(0x58, {0x02007fc0, 0, 0, 0, 0, 10, 0x200}),
|
||||
makePacket(0x58, {0x00800000, 0xffffffff, 0, 0, 0, 10}),
|
||||
makePacket(0x83, {0, 1, low(destination.data()), high(destination.data())})
|
||||
|
||||
@@ -2531,8 +2531,9 @@ void resourceReadTests(const Device& device, Recorder& recorder) {
|
||||
const auto commands = snapshotRecorder.Commands();
|
||||
RecordMemoryBarrier(snapshotContext, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT);
|
||||
const auto copy = snapshotContext.Function<PFN_vkCmdCopyBuffer>("vkCmdCopyBuffer");
|
||||
VkBufferCopy region{0, 0, elementBytes};
|
||||
VkBufferCopy region{first->snapshots[0].offset, 0, elementBytes};
|
||||
copy(commands, first->snapshots[0].buffer->Handle(), downloaded->Handle(), 1, ®ion);
|
||||
region.srcOffset = second->snapshots[0].offset;
|
||||
region.dstOffset = elementBytes;
|
||||
copy(commands, second->snapshots[0].buffer->Handle(), downloaded->Handle(), 1, ®ion);
|
||||
RecordMemoryBarrier(snapshotContext, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT);
|
||||
@@ -2878,7 +2879,7 @@ void misalignedRegionTests(const Device& device, Recorder& recorder) {
|
||||
const auto check = [&] {
|
||||
const auto bindings = resources.PrepareDrawBindings(regionRecorder);
|
||||
Require(bindings != nullptr && bindings->snapshots.size() == 1, "the read-only misaligned draw input was not snapshotted");
|
||||
const auto contents = bindings->snapshots.front().buffer->Bytes();
|
||||
const auto contents = bindings->snapshots.front().Bytes();
|
||||
Require(bindings->snapshots.front().address == view - adjustment && contents.size() == adjustment + elementBytes, "the draw snapshot does not start at the aligned offset below the view");
|
||||
Require(std::memcmp(contents.data() + adjustment, guest + offset, elementBytes) == 0, "the shader's patched offset into the draw snapshot misses the view's bytes");
|
||||
};
|
||||
@@ -2963,9 +2964,9 @@ void misalignedSnapshotTests(const Device& device, Recorder& recorder) {
|
||||
Require(push[0] == std::byte{0} && adjustment == offset % alignment, "the inner view's shader offset is not its distance from the binding");
|
||||
const auto bindings = resources.PrepareDrawBindings(snapshotRecorder);
|
||||
Require(bindings != nullptr && bindings->snapshots.size() == 2, "read-only draw inputs were not snapshotted");
|
||||
const auto outerContents = bindings->snapshots[0].buffer->Bytes();
|
||||
const auto outerContents = bindings->snapshots[0].Bytes();
|
||||
Require(outerContents.size() >= outerBytes && std::memcmp(outerContents.data(), guest + outer, outerBytes) == 0, "an aligned draw snapshot misses its view's bytes");
|
||||
const auto contents = bindings->snapshots[1].buffer->Bytes();
|
||||
const auto contents = bindings->snapshots[1].Bytes();
|
||||
Require(contents.size() >= adjustment + elementBytes, "a draw snapshot ends before the bytes the shader reads");
|
||||
Require(std::memcmp(contents.data() + adjustment, guest + offset, elementBytes) == 0, "the shader's offset into a draw snapshot misses the view's bytes");
|
||||
snapshotRecorder.Sync();
|
||||
@@ -3185,7 +3186,7 @@ void drawSnapshotReuseTests(const Device& device, Recorder& recorder) {
|
||||
return;
|
||||
}
|
||||
const auto element = address + 4096;
|
||||
constexpr std::size_t elementBytes = 1024;
|
||||
constexpr std::size_t elementBytes = 8192;
|
||||
ShaderRecompiler::RecompileResult program;
|
||||
ShaderRecompiler::DescriptorBinding binding;
|
||||
binding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer;
|
||||
@@ -3635,6 +3636,99 @@ void unwrittenSnapshotTests(const Device& device) {
|
||||
for (std::size_t entry = 0; entry < Recorder::UnwrittenSnapshotEntries; ++entry) cache.NoteUnwrittenDrawSnapshot(address + entry * 16, 1, next, buffer);
|
||||
cache.NoteUnwrittenDrawSnapshot(address + Recorder::UnwrittenSnapshotEntries * 16, 1, next, buffer);
|
||||
Require(cache.UnwrittenDrawSnapshot(address, 1, next) == buffer && cache.UnwrittenDrawSnapshot(address + Recorder::UnwrittenSnapshotEntries * 16, 1, next) == nullptr, "unwritten snapshots grew past the entry cap");
|
||||
const std::array<std::uint64_t, 3> later{1, 3, 1};
|
||||
cache.NoteUnwrittenDrawSnapshot(address, 16, later, buffer, 256);
|
||||
VkDeviceSize offset = 0;
|
||||
Require(cache.UnwrittenDrawSnapshot(address, 16, later, &offset) == buffer && offset == 256, "an unwritten snapshot lost its offset in its buffer");
|
||||
}
|
||||
|
||||
void drawSnapshotUploadTests(const Device& device, Recorder& recorder) {
|
||||
const auto& context = device.GetContext();
|
||||
const auto limit = ShaderResources::DrawUploadLimit();
|
||||
if (limit < 256 || limit > 16384) {
|
||||
std::cout << "draw upload limit outside 256..16384 bytes: draw snapshot uploads not tested\n";
|
||||
return;
|
||||
}
|
||||
constexpr std::size_t bytes = 65536;
|
||||
void* block = context.hostImportAlignment != 0 ? AllocateWatched(bytes, 65536) : nullptr;
|
||||
if (block == nullptr) {
|
||||
std::cout << "host imports or write watching unavailable: draw snapshot uploads not tested\n";
|
||||
return;
|
||||
}
|
||||
struct Release {
|
||||
void* block;
|
||||
~Release() { ReleaseWatched(block, bytes); }
|
||||
} release{block};
|
||||
for (std::size_t index = 0; index < bytes; ++index) static_cast<std::uint8_t*>(block)[index] = static_cast<std::uint8_t>(index * 7u + 1u);
|
||||
const auto address = reinterpret_cast<std::uint64_t>(block);
|
||||
{
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Add(block, bytes, true, true, true);
|
||||
}
|
||||
struct Unregister {
|
||||
const Context& context;
|
||||
void* block;
|
||||
~Unregister() {
|
||||
{
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Remove(block);
|
||||
}
|
||||
HostImportFor(context, reinterpret_cast<std::uint64_t>(block), bytes);
|
||||
}
|
||||
} unregister{context, block};
|
||||
if (HostImportFor(context, address, bytes) == nullptr) {
|
||||
std::cout << "host import of the watched block refused: draw snapshot uploads not tested\n";
|
||||
return;
|
||||
}
|
||||
const std::array<std::uint64_t, 3> elements{address + 4096, address + 8192, address + 32768};
|
||||
const std::array<std::size_t, 3> sizes{256, limit, limit + 256};
|
||||
ShaderRecompiler::RecompileResult program;
|
||||
for (std::uint32_t index = 0; index < elements.size(); ++index) {
|
||||
ShaderRecompiler::DescriptorBinding binding;
|
||||
binding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer;
|
||||
binding.role = ShaderRecompiler::DescriptorRole::GuestBuffers;
|
||||
binding.descriptorSet = 0;
|
||||
binding.binding = index;
|
||||
binding.count = 1;
|
||||
binding.guestDescriptor = {static_cast<std::uint32_t>(elements[index]), static_cast<std::uint32_t>(elements[index] >> 32u) & 0xffffu, static_cast<std::uint32_t>(sizes[index]), 0x31000000u};
|
||||
binding.bufferWritten = {false};
|
||||
program.bindings.push_back(binding);
|
||||
}
|
||||
const CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0};
|
||||
{
|
||||
auto snapshotContext = context;
|
||||
DescriptorCache cache(snapshotContext);
|
||||
snapshotContext.descriptorCache = &cache;
|
||||
Recorder snapshotRecorder(snapshotContext);
|
||||
snapshotRecorder.Activate();
|
||||
ShaderResources resources(snapshotContext, compute);
|
||||
const auto alignment = std::max<VkDeviceSize>(snapshotContext.limits.minStorageBufferOffsetAlignment, 16);
|
||||
const auto bindings = resources.PrepareDrawBindings(snapshotRecorder);
|
||||
Require(bindings != nullptr && bindings->snapshots.size() == elements.size(), "read-only draw inputs were not snapshotted");
|
||||
for (std::size_t index = 0; index < elements.size(); ++index) {
|
||||
const auto& snapshot = bindings->snapshots[index];
|
||||
Require(snapshot.address == elements[index] && snapshot.bytes == sizes[index] && snapshot.offset % alignment == 0, "a draw snapshot is not an aligned copy of its input");
|
||||
Require(std::memcmp(snapshot.Bytes().data(), reinterpret_cast<const void*>(elements[index]), sizes[index]) == 0, "a draw snapshot does not hold the guest bytes of its draw");
|
||||
}
|
||||
Require(bindings->snapshots[0].buffer == bindings->snapshots[1].buffer && bindings->snapshots[0].offset != bindings->snapshots[1].offset, "draw inputs up to the upload limit do not share the recorder's upload chunk");
|
||||
Require(bindings->snapshots[2].buffer != bindings->snapshots[0].buffer && bindings->snapshots[2].offset == 0, "a draw input above the upload limit was copied into the upload chunk");
|
||||
const auto earlier = bindings->snapshots[0].Bytes();
|
||||
const std::vector<std::byte> before(earlier.begin(), earlier.end());
|
||||
std::memset(reinterpret_cast<void*>(elements[0]), 0x5a, sizes[0]);
|
||||
const auto again = resources.PrepareDrawBindings(snapshotRecorder);
|
||||
Require(again != nullptr && again->snapshots.size() == elements.size(), "read-only draw inputs were not snapshotted again");
|
||||
const auto copied = again->snapshots[0].Bytes();
|
||||
Require(again->snapshots[0].buffer != bindings->snapshots[0].buffer || again->snapshots[0].offset != bindings->snapshots[0].offset, "a draw input copied again took the slice an earlier draw reads");
|
||||
Require(std::all_of(copied.begin(), copied.end(), [](std::byte value) { return value == std::byte{0x5a}; }), "a draw snapshot copied again misses the CPU store");
|
||||
Require(std::equal(before.begin(), before.end(), earlier.begin()), "copying a draw input again changed the slice an earlier draw reads");
|
||||
const auto first = snapshotRecorder.DrawUpload(16);
|
||||
const auto second = snapshotRecorder.DrawUpload(16);
|
||||
Require(second.buffer == first.buffer && second.offset >= first.offset + 16 && second.offset % alignment == 0, "upload slices overlap or are not aligned");
|
||||
const auto whole = snapshotRecorder.DrawUpload(Recorder::DrawUploadChunk);
|
||||
Require(whole.buffer != second.buffer && whole.offset == 0, "a full upload chunk was not replaced");
|
||||
snapshotRecorder.Sync();
|
||||
}
|
||||
recorder.Activate();
|
||||
}
|
||||
|
||||
void drawInputReuseTests(const Device& device, Recorder& recorder) {
|
||||
@@ -9022,6 +9116,7 @@ int main(int argc, char** argv) {
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--draw-snapshot-cap-only") {
|
||||
drawSnapshotCapTests(device, recorder);
|
||||
unwrittenSnapshotTests(device);
|
||||
drawSnapshotUploadTests(device, recorder);
|
||||
std::cout << "Draw snapshot cap tests passed\n";
|
||||
return 0;
|
||||
}
|
||||
@@ -9071,6 +9166,7 @@ int main(int argc, char** argv) {
|
||||
drawSnapshotCapTests(device, recorder);
|
||||
drawSnapshotEvictionTests(device);
|
||||
unwrittenSnapshotTests(device);
|
||||
drawSnapshotUploadTests(device, recorder);
|
||||
drawInputReuseTests(device, recorder);
|
||||
drawInputInPlaceTests(device, recorder);
|
||||
RunResidentPresentTests(device.GetContext());
|
||||
|
||||
@@ -84,7 +84,7 @@ void removeCacheDirectory(const std::filesystem::path& directory) {
|
||||
}
|
||||
|
||||
bool sameBinding(const DescriptorBinding& left, const DescriptorBinding& right) {
|
||||
return left.kind == right.kind && left.role == right.role && left.descriptorSet == right.descriptorSet && left.binding == right.binding && left.count == right.count && left.guestDescriptor == right.guestDescriptor && left.readOnly == right.readOnly && left.imageShape == right.imageShape && left.samplerDepthCompare == right.samplerDepthCompare && left.imageWritten == right.imageWritten && left.imageDepthCompare == right.imageDepthCompare && left.imageAtomic == right.imageAtomic && left.imageShapes == right.imageShapes && left.bufferAtomic == right.bufferAtomic && left.bufferWritten == right.bufferWritten && left.bufferRead == right.bufferRead && left.samplerUnnormalized == right.samplerUnnormalized && left.samplerIntegerBorder == right.samplerIntegerBorder && left.imageUnnormalized == right.imageUnnormalized && left.imageUnnormalizedBaseLevel == right.imageUnnormalizedBaseLevel && left.imageSamplers == right.imageSamplers;
|
||||
return left.kind == right.kind && left.role == right.role && left.descriptorSet == right.descriptorSet && left.binding == right.binding && left.count == right.count && left.guestDescriptor == right.guestDescriptor && left.readOnly == right.readOnly && left.imageShape == right.imageShape && left.samplerDepthCompare == right.samplerDepthCompare && left.imageWritten == right.imageWritten && left.imageDepthCompare == right.imageDepthCompare && left.imageAtomic == right.imageAtomic && left.imageShapes == right.imageShapes && left.bufferAtomic == right.bufferAtomic && left.bufferWritten == right.bufferWritten && left.bufferRead == right.bufferRead && left.samplerUnnormalized == right.samplerUnnormalized && left.samplerIntegerBorder == right.samplerIntegerBorder && left.samplerCoordinatesIgnored == right.samplerCoordinatesIgnored && left.imageUnnormalized == right.imageUnnormalized && left.imageUnnormalizedBaseLevel == right.imageUnnormalizedBaseLevel && left.imageSamplers == right.imageSamplers;
|
||||
}
|
||||
|
||||
bool sameBindings(const std::vector<DescriptorBinding>& left, const std::vector<DescriptorBinding>& right) {
|
||||
@@ -176,6 +176,7 @@ DescriptorBinding sampleBinding(std::uint32_t seed) {
|
||||
binding.bufferWritten = {false, false, true, true, false};
|
||||
binding.samplerUnnormalized = {false, true, true};
|
||||
binding.samplerIntegerBorder = {true, false, seed % 2 == 1};
|
||||
binding.samplerCoordinatesIgnored = {false, seed % 2 == 0, true};
|
||||
binding.imageUnnormalized = {true, false, seed % 2 == 0};
|
||||
binding.imageUnnormalizedBaseLevel = {false, true, seed % 3 == 0};
|
||||
binding.imageSamplers = {0x5u, 0u, 0x80000000u};
|
||||
|
||||
@@ -2615,6 +2615,116 @@ void verifyImageTableUnkeyedAccess() {
|
||||
}
|
||||
}
|
||||
|
||||
struct TableManyGuest {
|
||||
std::array<std::array<std::uint32_t, 8>, 128> heap{};
|
||||
std::array<std::uint32_t, 4> output{};
|
||||
std::array<std::uint32_t, 128> srt{};
|
||||
};
|
||||
|
||||
constexpr std::uint32_t TableManyDirectWord = 96u;
|
||||
constexpr std::uint32_t TableManyExtraSamplerWord = 112u;
|
||||
|
||||
std::vector<std::uint32_t> TableManyKeyCode(std::uint32_t first, std::uint32_t second, bool twoSamplers, std::uint32_t direct = 0u, bool extraSampler = false) {
|
||||
std::vector<std::uint32_t> code{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0xf4080a00u, 0xfa000040u, 0x7e100280u};
|
||||
for (std::uint32_t image = 0; image < direct; image++) code.insert(code.end(), {0xf40c0000u | ((44u + image * 8u) << 6u), 0xfa000000u | ((TableManyDirectWord + image * 8u) * 4u)});
|
||||
if (extraSampler) code.insert(code.end(), {0xf4080f00u, 0xfa000000u | (TableManyExtraSamplerWord * 4u)});
|
||||
for (std::uint32_t read = 0; read < first + second; read++) {
|
||||
code.insert(code.end(), {0xf4000400u, 0xfa000050u + read * 4u, 0x8f108510u, read < first ? 0xf42c0502u : 0xf42c0514u, 0x20000000u, 0xf09c0f08u, 0x00450400u, 0x06100908u});
|
||||
if (twoSamplers) code.insert(code.end(), {0xf09c0f08u, 0x00650400u, 0x06100908u});
|
||||
}
|
||||
for (std::uint32_t image = 0; image < direct; image++) {
|
||||
const auto resource = ((44u + image * 8u) / 4u) << 16u;
|
||||
code.insert(code.end(), {0xf09c0f08u, 0x00400400u | resource, 0x06100908u});
|
||||
if (twoSamplers) code.insert(code.end(), {0xf09c0f08u, 0x00600400u | resource, 0x06100908u});
|
||||
if (extraSampler && image + 1u == direct) code.insert(code.end(), {0xf09c0f08u, 0x01e00400u | resource, 0x06100908u});
|
||||
}
|
||||
code.insert(code.end(), {0xe0700000u, 0x80070800u, 0xbf810000u});
|
||||
return code;
|
||||
}
|
||||
|
||||
void verifySelectedRecordsWithinLimits(bool pairs, bool direct) {
|
||||
using namespace ShaderRecompiler;
|
||||
namespace Abi = ImageTableAbi;
|
||||
static TableTexture textures[4];
|
||||
alignas(4096) static TableManyGuest guest;
|
||||
auto& heap = guest.heap;
|
||||
for (std::uint32_t record = 0; record < heap.size(); record++) heap[record] = TableT(textures[record % 4u].bytes.data(), TableFormat8888UNorm, TableType2D, record + 1u);
|
||||
const auto heapV = TableV(heap.data(), 32u, static_cast<std::uint32_t>(heap.size()));
|
||||
const auto outputV = TableV(guest.output.data(), 0u, 16u);
|
||||
guest.srt = {};
|
||||
std::copy(heapV.begin(), heapV.end(), guest.srt.begin());
|
||||
guest.srt[8] = 0x92u;
|
||||
std::copy(outputV.begin(), outputV.end(), guest.srt.begin() + 12);
|
||||
std::copy(heapV.begin(), heapV.end(), guest.srt.begin() + 16);
|
||||
for (std::uint32_t read = 0; read + 20u < guest.srt.size(); read++) guest.srt[20u + read] = read + 2u;
|
||||
for (std::uint32_t image = 0; image < 2u; image++) {
|
||||
const auto direct = TableT(textures[image].bytes.data(), TableFormat8888UNorm, TableType2D, 200u + image);
|
||||
std::copy(direct.begin(), direct.end(), guest.srt.begin() + TableManyDirectWord + image * 8u);
|
||||
}
|
||||
guest.srt[TableManyExtraSamplerWord] = 0x91u;
|
||||
const auto address = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(guest.srt.data()));
|
||||
const std::array<std::uint32_t, 2> userData{static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u)};
|
||||
const auto columnOf = [](const IrResourcePlan& plan, const ImageResource& image) { return *plan.descriptorSources.at(image.source).tableColumn; };
|
||||
const auto selectedTables = [&](const IrResourcePlan& plan) {
|
||||
return static_cast<std::uint32_t>(std::count_if(plan.info.images.begin(), plan.info.images.end(), [&](const ImageResource& image) { return image.table != NoTable && columnOf(plan, image).selected; }));
|
||||
};
|
||||
const std::string name = pairs ? "selected records within the pair limit" : "selected records within the image limit";
|
||||
const std::uint32_t limit = pairs ? ShaderInfo::MaxSampledPairs / 2u : ShaderInfo::MaxImages;
|
||||
const auto outside = Abi::PoisonCode(static_cast<std::uint32_t>(Abi::PoisonReason::OutsideDomain));
|
||||
if (direct) {
|
||||
const auto directImages = [&](const IrResourcePlan& plan) {
|
||||
return static_cast<std::uint32_t>(std::count_if(plan.info.images.begin(), plan.info.images.end(), [](const ImageResource& image) { return image.table == NoTable; }));
|
||||
};
|
||||
const auto reads = limit - 1u;
|
||||
const auto fittingDirectCode = TableManyKeyCode(reads, 0u, pairs, 1u);
|
||||
const auto fittingDirect = GetResourcePlan(TableRequest(fittingDirectCode, userData));
|
||||
const auto describe = [&](const IrResourcePlan& plan) {
|
||||
return std::to_string(plan.info.images.size()) + " images (" + std::to_string(selectedTables(plan)) + " selected tables, " + std::to_string(directImages(plan)) + " direct), " + std::to_string(plan.info.sampledPairs.size()) + " pairs";
|
||||
};
|
||||
require(fittingDirect->info.images.size() == limit && selectedTables(*fittingDirect) == reads && directImages(*fittingDirect) == 1u && fittingDirect->info.sampledPairs.size() == (pairs ? 2u : 1u) * limit, (name + ": selected reads and a direct image that fit lost the per-key split").c_str());
|
||||
std::cout << name << ": " << reads << " selected reads and 1 direct image" << (pairs ? ", each with 2 samplers" : "") << ": " << describe(*fittingDirect) << '\n';
|
||||
const auto pastDirectCode = TableManyKeyCode(reads, 0u, pairs, pairs ? 1u : 2u, pairs);
|
||||
const auto pastDirectRequest = TableRequest(pastDirectCode, userData);
|
||||
const auto pastDirect = GetResourcePlan(pastDirectRequest);
|
||||
const auto mergedDirect = std::find_if(pastDirect->info.images.begin(), pastDirect->info.images.end(), [](const ImageResource& image) { return image.table != NoTable; });
|
||||
require(pastDirect->info.images.size() == (pairs ? 2u : 3u) && selectedTables(*pastDirect) == 0u && directImages(*pastDirect) == (pairs ? 1u : 2u) && pastDirect->info.sampledPairs.size() == (pairs ? 5u : 3u) && mergedDirect != pastDirect->info.images.end(), (name + ": selected reads that the direct " + (pairs ? "sampler" : "images") + " push past the limit were not merged into one table").c_str());
|
||||
const auto mergedColumn = columnOf(*pastDirect, *mergedDirect);
|
||||
require(!mergedColumn.selected && mergedColumn.selection == 0u && !mergedColumn.unkeyed && mergedColumn.keyDomains.size() == reads, (name + ": the table merged beside the direct " + (pairs ? "sampler" : "images") + " is not the union of the keys").c_str());
|
||||
const auto compiledDirect = CompileTable(pastDirectRequest);
|
||||
const auto& directTables = compiledDirect.capture->snapshot.tables;
|
||||
require(directTables.tables.size() == 1u, (name + ": the snapshot beside the direct " + (pairs ? "sampler" : "images") + " does not hold one table").c_str());
|
||||
const auto& directSnapshot = directTables.tables[mergedDirect->table];
|
||||
for (const auto key : {2u, 2u + reads / 2u, 1u + reads}) {
|
||||
require(key < directSnapshot.codes.size() && (directSnapshot.codes[key] & Abi::PoisonFlag) == 0u && directSnapshot.words.at(directSnapshot.codes[key]).dwords == heap[key], (name + ": a key beside the direct " + (pairs ? "sampler" : "images") + " does not bind its record").c_str());
|
||||
}
|
||||
require(directSnapshot.codes.at(1) == outside && directSnapshot.codes.at(reads + 2u) == outside, (name + ": a key no read selects was bound beside the direct " + (pairs ? "sampler" : "images")).c_str());
|
||||
std::cout << name << ": " << reads << " selected reads and " << (pairs ? "1 direct image with a third sampler" : "2 direct images") << ": " << describe(*pastDirect) << ", the reads merged into one table with " << mergedColumn.keyDomains.size() << " key domains\n";
|
||||
return;
|
||||
}
|
||||
const auto fitting = TableManyKeyCode(limit, 0u, pairs);
|
||||
const auto atLimit = GetResourcePlan(TableRequest(fitting, userData));
|
||||
require(atLimit->info.images.size() == limit && selectedTables(*atLimit) == limit && atLimit->info.sampledPairs.size() == (pairs ? 2u : 1u) * limit, (name + ": reads that fit lost the per-key split").c_str());
|
||||
const auto past = TableManyKeyCode(limit + 1u, 0u, pairs);
|
||||
const auto request = TableRequest(past, userData);
|
||||
const auto plan = GetResourcePlan(request);
|
||||
require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u && plan->info.sampledPairs.size() == (pairs ? 2u : 1u), (name + ": reads past the limit were not merged into one table").c_str());
|
||||
const auto merged = columnOf(*plan, plan->info.images[0]);
|
||||
require(!merged.selected && merged.selection == 0u && !merged.unkeyed && merged.keyDomains.size() == limit + 1u, (name + ": the merged table is not the union of the keys").c_str());
|
||||
const auto compiled = CompileTable(request);
|
||||
const auto& tables = compiled.capture->snapshot.tables;
|
||||
require(tables.tables.size() == 1u, (name + ": the snapshot does not hold one table").c_str());
|
||||
const auto& snapshot = tables.tables[0];
|
||||
for (const auto key : {2u, 2u + limit / 2u, 2u + limit}) {
|
||||
require(key < snapshot.codes.size() && (snapshot.codes[key] & Abi::PoisonFlag) == 0u && snapshot.words.at(snapshot.codes[key]).dwords == heap[key], (name + ": a key does not bind its record").c_str());
|
||||
}
|
||||
require(snapshot.codes.at(1) == outside && snapshot.codes.at(limit + 3u) == outside, (name + ": a key no read selects was bound").c_str());
|
||||
if (pairs) return;
|
||||
const auto split = TableManyKeyCode(40u, 30u, false);
|
||||
const auto twoHeaps = GetResourcePlan(TableRequest(split, userData));
|
||||
const auto unselected = std::find_if(twoHeaps->info.images.begin(), twoHeaps->info.images.end(), [&](const ImageResource& image) { return !columnOf(*twoHeaps, image).selected; });
|
||||
require(twoHeaps->info.images.size() == 31u && selectedTables(*twoHeaps) == 30u && unselected != twoHeaps->info.images.end() && columnOf(*twoHeaps, *unselected).keyDomains.size() == 40u, (name + ": the larger heap was not merged first, or the smaller one lost its split").c_str());
|
||||
}
|
||||
|
||||
void verifyImageTableSrtKey() {
|
||||
using namespace ShaderRecompiler;
|
||||
namespace Abi = ImageTableAbi;
|
||||
@@ -5323,7 +5433,7 @@ void verifySpecializedBufferAtomicBindings() {
|
||||
}
|
||||
require(!physical, "specialized buffer atomic: the module addresses physical storage without an address access");
|
||||
require(int64, "specialized buffer atomic: the fault address is 64-bit and the module does not declare Int64");
|
||||
require(faultFunction != 0 && faultCalls == 1, "specialized buffer atomic: an unaligned atomic does not record exactly one fault");
|
||||
require(faultCalls == 0, "specialized buffer atomic: a 32-bit atomic records an unaligned fault instead of acting on its dword");
|
||||
const std::array<std::uint32_t, 1> shaderOnly{spv::CapabilityShader};
|
||||
expectFailure([&] { static_cast<void>(compile(shaderOnly)); }, "buffer atomic fault addresses require shaderInt64", "specialized buffer atomic: the program compiled without shaderInt64");
|
||||
}
|
||||
@@ -5696,6 +5806,47 @@ void verifyUnnormalizedSamplers() {
|
||||
require(proven(pixelOffsetNormalized, false) && implicitMasks.size() == 1u && (implicitMasks[0] & (spv::ImageOperandsConstOffsetMask | spv::ImageOperandsOffsetMask)) != 0u, "unnormalized texel offsets: a normalized pixel image_sample_o is not an implicit-LOD sample with its offset");
|
||||
}
|
||||
|
||||
void verifyNullVolumeUnnormalizedSampler() {
|
||||
using namespace ShaderRecompiler;
|
||||
ImageResource volume{};
|
||||
volume.resourceClass = ImageResourceClass::Sampled;
|
||||
volume.numericClass = IrTextureNumericClass::Float;
|
||||
volume.dimension = RdnaImageDimension::Dim3D;
|
||||
volume.read = true;
|
||||
ImageResource plane = volume;
|
||||
plane.dimension = RdnaImageDimension::Dim2D;
|
||||
SamplerResource sampler{};
|
||||
sampler.uses = SamplerUseExplicitLod;
|
||||
const DescriptorValue nullDescriptor{{0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u}, 8u};
|
||||
const DescriptorValue volumeDescriptor{{0x00001000u, 0x03800000u, 0x0000c000u, 0xa0000facu, 3u, 0u, 0u, 0u}, 8u};
|
||||
const DescriptorValue planeDescriptor{{0x00002000u, 0x03800000u, 0x0000c000u, 0x90000facu, 0u, 0u, 0u, 0u}, 8u};
|
||||
const DescriptorValue unnormalized{{0x00008092u, 0x00fff000u, 0x02500000u, 0u}, 4u};
|
||||
const auto populate = [&](std::vector<ImageResource> images, std::vector<DescriptorValue> descriptors) {
|
||||
ShaderInfo info;
|
||||
info.images = images;
|
||||
info.samplers = {sampler};
|
||||
for (std::uint32_t index = 0; index < images.size(); ++index) {
|
||||
info.sampledPairs.push_back({index, 0u, 0u});
|
||||
info.runtimeImageModes.push_back(ResourceMaterializer::RuntimeImageModes(images[index]));
|
||||
}
|
||||
ResourceSnapshot snapshot;
|
||||
snapshot.images = descriptors;
|
||||
snapshot.samplers = {unnormalized};
|
||||
BindingAllocationResult allocation;
|
||||
for (std::uint32_t index = 0; index < images.size(); ++index) allocation.layout.descriptors.push_back({DescriptorBindingForImage(images[index]), {index}});
|
||||
allocation.layout.descriptors.push_back({DescriptorBindingKind::Samplers, {0u}});
|
||||
DescriptorBindingBuilder{}.Populate(allocation, info, IrShaderStage::Compute, 0u, snapshot, {});
|
||||
return allocation.bindings.back();
|
||||
};
|
||||
const auto nullOnly = populate({volume}, {nullDescriptor});
|
||||
require(nullOnly.samplerUnnormalized == std::vector<bool>{false} && nullOnly.samplerCoordinatesIgnored == std::vector<bool>{true}, "unnormalized samplers: an S# paired only with a null 3D T# was refused or kept its unnormalized coordinates");
|
||||
const DescriptorValue zeroSwizzle{{0x00003000u, 0x00100000u, 0u, 0xa0000000u, 0u, 0u, 0u, 0u}, 8u};
|
||||
const auto constantAndNull = populate({volume, volume}, {zeroSwizzle, nullDescriptor});
|
||||
require(constantAndNull.samplerUnnormalized == std::vector<bool>{false} && constantAndNull.samplerCoordinatesIgnored == std::vector<bool>{true}, "unnormalized samplers: an S# paired with a null 3D T# and a 3D T# whose channels all select constants was refused or kept its unnormalized coordinates");
|
||||
expectFailure([&] { static_cast<void>(populate({volume}, {volumeDescriptor})); }, "samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: a 3D T# was accepted");
|
||||
expectFailure([&] { static_cast<void>(populate({volume, plane}, {nullDescriptor, planeDescriptor})); }, "samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: an S# shared by a null 3D T# and a 2D T# was accepted");
|
||||
}
|
||||
|
||||
void verifyUnusedUnnormalizedSampler() {
|
||||
using namespace ShaderRecompiler;
|
||||
ImageResource image{};
|
||||
@@ -6004,6 +6155,102 @@ void verifyBdaReadFallbackFunctions() {
|
||||
}
|
||||
}
|
||||
|
||||
void verifyBdaAnchors() {
|
||||
using namespace ShaderRecompiler;
|
||||
const std::array<std::uint32_t, 3> capabilities{spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess};
|
||||
const std::array<std::string_view, 2> extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"};
|
||||
alignas(256) static std::array<std::uint32_t, 32> output{};
|
||||
const auto outputAddress = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(output.data()));
|
||||
const std::array<std::uint32_t, 8> userData{0x10000000u, 0u, 0u, 0u, static_cast<std::uint32_t>(outputAddress), static_cast<std::uint32_t>((outputAddress >> 32u) & 0xffffu), 128u, 0x01016facu};
|
||||
const auto compile = [&](std::uint32_t scalarBase, std::uint32_t stride) {
|
||||
std::vector<std::uint32_t> code{0x7e020200u, 0x7e040201u};
|
||||
for (std::uint32_t load = 0; load < scalarBase; ++load) {
|
||||
code.push_back(0xdc308000u | (stride * load + 4u));
|
||||
code.push_back(((3u + load) << 24u) | 0x00040000u);
|
||||
}
|
||||
code.push_back(0xdc308000u | 4u);
|
||||
code.push_back(((3u + scalarBase) << 24u) | 0x007d0001u);
|
||||
code.push_back(0xbf8c3f70u);
|
||||
for (std::uint32_t load = 1; load <= scalarBase; ++load) code.push_back(0x4a060103u | ((3u + load) << 9u));
|
||||
code.insert(code.end(), {0xe0700000u, 0x80010300u, 0xbf810000u});
|
||||
RecompileRequest request{};
|
||||
request.shader = {ShaderStage::Compute, 0x60000u, code, 0, {}};
|
||||
request.context.waveSize = 32;
|
||||
request.context.userDataBaseRegister = 0;
|
||||
request.context.userData = userData;
|
||||
request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u};
|
||||
request.target.vulkanVersion = 0x00401000u;
|
||||
request.target.spirvVersion = 0x00010300u;
|
||||
request.target.subgroupSize = 32;
|
||||
request.target.bdaAbiVersion = BdaAbi::Version;
|
||||
request.target.supportedCapabilities = capabilities;
|
||||
request.target.supportedExtensions = extensions;
|
||||
request.target.fragmentShaderBarycentricEnabled = false;
|
||||
request.layout.pushConstantSizeBytes = 128;
|
||||
request.useCache = false;
|
||||
return Recompile(request).spirv;
|
||||
};
|
||||
for (const std::uint32_t stride : {4u, 32u}) {
|
||||
for (const std::uint32_t loads : {0u, 1u, 2u, 4u}) {
|
||||
const bool anchored = stride == 32u && loads >= 2u;
|
||||
const auto words = compile(loads, stride);
|
||||
std::map<std::uint32_t, std::string> names;
|
||||
std::string function;
|
||||
std::map<std::string, std::size_t> mainCalls;
|
||||
std::size_t anchorFunctions = 0;
|
||||
std::size_t branchesBeforeAnchor = 0;
|
||||
std::size_t directLoads = 0;
|
||||
for (std::size_t cursor = 5; cursor < words.size();) {
|
||||
const auto length = words[cursor] >> 16u;
|
||||
require(length != 0 && length <= words.size() - cursor, "BDA anchors: truncated SPIR-V instruction");
|
||||
const auto op = words[cursor] & 0xffffu;
|
||||
if (op == spv::OpName) names[words[cursor + 1]] = reinterpret_cast<const char*>(&words[cursor + 2]);
|
||||
if (op == spv::OpFunction) {
|
||||
function = names[words[cursor + 2]];
|
||||
if (function == "bda_anchor_range") ++anchorFunctions;
|
||||
}
|
||||
if (op == spv::OpFunctionCall && function == "main") {
|
||||
const auto& callee = names[words[cursor + 3]];
|
||||
if (callee == "bda_anchor_range") require(mainCalls.empty(), "BDA anchors: the anchor is searched after a read");
|
||||
++mainCalls[callee.starts_with("read_bda_span") ? std::string("read_bda_span") : callee];
|
||||
}
|
||||
if (op == spv::OpBranchConditional && function == "main" && mainCalls["bda_anchor_range"] == 0u) ++branchesBeforeAnchor;
|
||||
if (op == spv::OpLoad && function == "main" && length > 4u && words[cursor + 4] == spv::MemoryAccessAlignedMask && words[cursor + 5] == 4u) ++directLoads;
|
||||
cursor += length;
|
||||
}
|
||||
const auto spans = loads == 0u ? 1u : stride == 4u ? 2u : loads + 1u;
|
||||
require(anchorFunctions == (anchored ? 1u : 0u), "BDA anchors: the anchor search is defined without anchored reads, or twice");
|
||||
require(mainCalls["bda_anchor_range"] == (anchored ? 1u : 0u), "BDA anchors: the reads of one user data base do not share one anchor");
|
||||
require(!anchored || branchesBeforeAnchor == 0u, "BDA anchors: the anchor is not searched at the program's entry");
|
||||
require(mainCalls["read_bda_span"] == spans, "BDA anchors: a read lost its lookup path");
|
||||
require(directLoads == (anchored ? loads : 0u), "BDA anchors: an anchored read does not load its dwords directly");
|
||||
}
|
||||
}
|
||||
RecompileRequest plain{};
|
||||
const std::vector<std::uint32_t> endOnly{0xbf810000u};
|
||||
plain.shader = {ShaderStage::Compute, 0x60000u, endOnly, 0, {}};
|
||||
plain.context.waveSize = 32;
|
||||
plain.context.userDataBaseRegister = 0;
|
||||
plain.context.userData = userData;
|
||||
plain.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u};
|
||||
plain.target.vulkanVersion = 0x00401000u;
|
||||
plain.target.spirvVersion = 0x00010300u;
|
||||
plain.target.subgroupSize = 32;
|
||||
plain.target.bdaAbiVersion = BdaAbi::Version;
|
||||
plain.target.fragmentShaderBarycentricEnabled = false;
|
||||
plain.layout.pushConstantSizeBytes = 128;
|
||||
plain.useCache = false;
|
||||
const auto plainWords = Recompile(plain).spirv;
|
||||
for (std::size_t cursor = 5; cursor < plainWords.size();) {
|
||||
const auto length = plainWords[cursor] >> 16u;
|
||||
require(length != 0 && length <= plainWords.size() - cursor, "BDA anchors: truncated SPIR-V instruction");
|
||||
const auto op = plainWords[cursor] & 0xffffu;
|
||||
require(!(op == spv::OpTypeInt && plainWords[cursor + 2] == 64u), "BDA anchors: a program without anchors declares a 64-bit integer type");
|
||||
require(!(op == spv::OpCapability && plainWords[cursor + 1] == spv::CapabilityInt64), "BDA anchors: a program without anchors requires Int64");
|
||||
cursor += length;
|
||||
}
|
||||
}
|
||||
|
||||
void verifyFunctionLdsBound() {
|
||||
using namespace ShaderRecompiler;
|
||||
const auto build = [](const auto& body) {
|
||||
@@ -6671,6 +6918,11 @@ void verifySrgbDepthCompareModes() {
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
try {
|
||||
#ifdef _WIN32
|
||||
_putenv_s("ANYPS5_NO_SHADER_CACHE", "1");
|
||||
#else
|
||||
setenv("ANYPS5_NO_SHADER_CACHE", "1", 1);
|
||||
#endif
|
||||
using namespace ShaderRecompiler;
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--quad-mask-no-demote") {
|
||||
verifyQuadMaskNeighbourReads();
|
||||
@@ -6724,6 +6976,26 @@ int main(int argc, char** argv) {
|
||||
verifyTableEdgeStore();
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--selected-records-image-limit") {
|
||||
verifySelectedRecordsWithinLimits(false, false);
|
||||
std::cout << "Selected image-table records past the image limit merge back into one table per heap\n";
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--selected-records-pair-limit") {
|
||||
verifySelectedRecordsWithinLimits(true, false);
|
||||
std::cout << "Selected image-table records past the sampled pair limit merge back into one table per heap\n";
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--selected-records-direct-image-limit") {
|
||||
verifySelectedRecordsWithinLimits(false, true);
|
||||
std::cout << "Direct images count toward the image limit beside selected image-table records\n";
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--selected-records-direct-pair-limit") {
|
||||
verifySelectedRecordsWithinLimits(true, true);
|
||||
std::cout << "Direct samplers count toward the sampled pair limit beside selected image-table records\n";
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--srt-read-patching") {
|
||||
verifySrtReadPatching();
|
||||
std::cout << "SRT read patching replaces repeated and multiple uses\n";
|
||||
@@ -6779,9 +7051,11 @@ int main(int argc, char** argv) {
|
||||
verifyCoherentBufferAccesses();
|
||||
verifyUnnormalizedSamplers();
|
||||
verifyUnusedUnnormalizedSampler();
|
||||
verifyNullVolumeUnnormalizedSampler();
|
||||
verifyWaveUniformValues();
|
||||
verifyTwoLaneUniformValues();
|
||||
verifyBdaReadFallbackFunctions();
|
||||
verifyBdaAnchors();
|
||||
verifyFunctionLdsBound();
|
||||
verifyFunctionLdsUpdates();
|
||||
verifySrtReadPatching();
|
||||
|
||||
@@ -195,11 +195,26 @@ void testEndOfPipeInterrupts() {
|
||||
check(sceAgcDriverAddEqEvent(eq, 0, &graphicsTag) == 0, "graphics event registration failed");
|
||||
check(sceAgcDriverAddEqEvent(eq, 0x20, &computeTag) == 0, "compute event registration failed");
|
||||
expectFailure([] { sceAgcDriverAddEqEvent(0, 0, nullptr); });
|
||||
struct Output final : AgcDriver::IVideoOutput, AgcDriver::IFlipRequest, std::enable_shared_from_this<Output> {
|
||||
std::atomic<int> ready{0};
|
||||
std::shared_ptr<AgcDriver::IFlipRequest> Reserve(const AgcDriver::FlipInfo&) override { return shared_from_this(); }
|
||||
void GpuReady(const std::shared_ptr<AgcDriver::FrameTiming>&) override { ready.fetch_add(1); }
|
||||
void Fail(std::exception_ptr error) noexcept override { if (!error) std::terminate(); }
|
||||
};
|
||||
const auto output = std::make_shared<Output>();
|
||||
AgcDriverRegisterVideoOutput_nid_postfix(7, output);
|
||||
std::array<std::uint32_t, 6> flip{AgcDriver::FlipPacketHeader, 7, 0xfffffffeu, 1, 0, 0};
|
||||
Packet flipPacket{flip.data(), static_cast<std::uint32_t>(flip.size()), 0, {}};
|
||||
std::array<KernelEvent, 2> events{};
|
||||
check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "flip-only queue submit failed");
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
check(output->ready.load() == 1, "flip-only queue flip was not handed to its output");
|
||||
check(owner->GetTriggeredEvents(events.data(), 2) == 1 && events[0].udata == &graphicsTag && events[0].data == 1 && sceAgcDriverGetEqEventType(events.data()) == 0, "flip on a queue that never requested a release interrupt did not raise its event");
|
||||
check(owner->GetTriggeredEvents(events.data(), 2) == 0, "flip-only queue event was not cleared");
|
||||
std::array<std::uint32_t, 8> words{0xc0064900, 0, 1u << 24u, 0, 0, 0, 0, 0};
|
||||
Packet packet{words.data(), static_cast<std::uint32_t>(words.size()), 0, {}};
|
||||
check(sceAgcDriverSubmitDcb(&packet) == 0 && sceAgcDriverSubmitDcb(&packet) == 0, "interrupt submit failed");
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
std::array<KernelEvent, 2> events{};
|
||||
check(owner->GetTriggeredEvents(events.data(), 2) == 1, "graphics end-of-pipe interrupt was not delivered to its queue only");
|
||||
check(events[0].filter == -14 && events[0].udata == &graphicsTag && events[0].data == 2 && sceAgcDriverGetEqEventType(events.data()) == 0, "graphics end-of-pipe event encoding is wrong");
|
||||
check(sceAgcDriverGetEqContextId(events.data()) == 0, "graphics end-of-pipe event reports another context");
|
||||
@@ -212,21 +227,39 @@ void testEndOfPipeInterrupts() {
|
||||
check(sceAgcDriverSubmitDcb(&packet) == 0, "plain release submit failed");
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
check(owner->GetTriggeredEvents(events.data(), 2) == 0, "release without INT_SEL raised an interrupt");
|
||||
struct Output final : AgcDriver::IVideoOutput, AgcDriver::IFlipRequest, std::enable_shared_from_this<Output> {
|
||||
std::atomic<int> ready{0};
|
||||
std::shared_ptr<AgcDriver::IFlipRequest> Reserve(const AgcDriver::FlipInfo&) override { return shared_from_this(); }
|
||||
void GpuReady(const std::shared_ptr<AgcDriver::FrameTiming>&) override { ready.fetch_add(1); }
|
||||
void Fail(std::exception_ptr error) noexcept override { if (!error) std::terminate(); }
|
||||
};
|
||||
const auto output = std::make_shared<Output>();
|
||||
AgcDriverRegisterVideoOutput_nid_postfix(7, output);
|
||||
std::array<std::uint32_t, 6> flip{AgcDriver::FlipPacketHeader, 7, 0xfffffffeu, 1, 0, 0};
|
||||
Packet flipPacket{flip.data(), static_cast<std::uint32_t>(flip.size()), 0, {}};
|
||||
check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "flip submit failed");
|
||||
check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "flip after a release interrupt submit failed");
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
check(output->ready.load() == 1, "flip was not handed to its output");
|
||||
check(owner->GetTriggeredEvents(events.data(), 2) == 1 && events[0].udata == &graphicsTag && events[0].data == 1 && sceAgcDriverGetEqEventType(events.data()) == 0, "flip did not raise the graphics end-of-pipe event");
|
||||
check(owner->GetTriggeredEvents(events.data(), 2) == 0, "flip event was not cleared");
|
||||
check(output->ready.load() == 2, "flip after a release interrupt was not handed to its output");
|
||||
check(owner->GetTriggeredEvents(events.data(), 2) == 0, "flip on a queue that already raised its own release interrupt raised a second event");
|
||||
words[2] = 1u << 24u;
|
||||
check(sceAgcDriverSubmitDcb(&packet) == 0, "release before sustained flips submit failed");
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
check(owner->GetTriggeredEvents(events.data(), 2) == 1, "a release interrupt did not deliver on a queue whose flip event is suppressed");
|
||||
std::uint32_t sustainedFlips = 0;
|
||||
std::uint32_t flipsUntilEvent = 0;
|
||||
while (sustainedFlips < 8 && flipsUntilEvent == 0) {
|
||||
check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "sustained flip submit failed");
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
++sustainedFlips;
|
||||
check(output->ready.load() == static_cast<int>(2 + sustainedFlips), "sustained flip was not handed to its output");
|
||||
if (owner->GetTriggeredEvents(events.data(), 2) != 0) flipsUntilEvent = sustainedFlips;
|
||||
}
|
||||
check(flipsUntilEvent != 0, "a flip never raised an event again on a queue whose own release interrupts stopped");
|
||||
check(flipsUntilEvent > 2, "a flip raised an event within two flips of the queue's own release interrupt");
|
||||
check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "flip after the latch cleared submit failed");
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
check(output->ready.load() == static_cast<int>(3 + sustainedFlips), "flip after the latch cleared was not handed to its output");
|
||||
check(owner->GetTriggeredEvents(events.data(), 2) == 1, "a flip did not raise an event after the latch cleared");
|
||||
words[2] = 1u << 24u;
|
||||
check(sceAgcDriverSubmitDcb(&packet) == 0, "release after a cleared latch submit failed");
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
check(owner->GetTriggeredEvents(events.data(), 2) == 1, "a release interrupt did not deliver after the latch cleared");
|
||||
for (int index = 0; index < 2; ++index) {
|
||||
check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "flip paired with a release interrupt submit failed");
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
check(output->ready.load() == static_cast<int>(4 + sustainedFlips + index), "flip paired with a release interrupt was not handed to its output");
|
||||
check(owner->GetTriggeredEvents(events.data(), 2) == 0, "a flip raised an event within two flips of the queue's own release interrupt");
|
||||
}
|
||||
AgcDriverUnregisterVideoOutput_nid_postfix(7, output);
|
||||
alignas(8) static volatile std::uint64_t label = 0;
|
||||
const auto labelAddress = reinterpret_cast<std::uintptr_t>(&label);
|
||||
@@ -784,6 +817,36 @@ void testRegisteredVertexWorkgroupRouting() {
|
||||
check(expectFailure([&] { static_cast<void>(VertexWorkgroupRequired(queue, registry)); }).find("reserved graphics program address") != std::string::npos, "reserved NGG program address bits were accepted");
|
||||
}
|
||||
|
||||
void testRegisteredFastLaunchWorkgroupVertex() {
|
||||
alignas(256) static const std::array<std::uint32_t, 6> code{
|
||||
0xd8340000u, 0x00000100u, 0xbf8a0000u, 0xf80008cfu, 0x00000000u, 0xbf810000u,
|
||||
};
|
||||
struct Header {
|
||||
Shader shader{};
|
||||
ShaderUserData userData{};
|
||||
std::array<ShaderRegister, 4> registers{};
|
||||
};
|
||||
alignas(8) static Header header{};
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(code.data());
|
||||
header.shader.file_header = 0x34333231u;
|
||||
header.shader.version = 0x18u;
|
||||
header.shader.header_size = sizeof(Header);
|
||||
header.shader.shader_size = sizeof(code);
|
||||
header.shader.code = code.data();
|
||||
header.shader.type = 2u;
|
||||
header.shader.user_data = &header.userData;
|
||||
header.registers = {{{0x0c8u, static_cast<std::uint32_t>(address >> 8u)}, {0x0c9u, static_cast<std::uint32_t>(address >> 40u)}, {0x08au, 0x022c0245u}, {0x08bu, 0x0009002au}}};
|
||||
header.shader.sh_registers = header.registers.data();
|
||||
header.shader.num_sh_registers = header.registers.size();
|
||||
AgcDriverRegisterShader_nid_postfix(&header.shader);
|
||||
const std::array<const Shader*, 1> stages{&header.shader};
|
||||
const std::array<ShaderRegister, 6> context{{
|
||||
{0x2d5u, 0x00482030u}, {0x1ffu, 32u}, {0x2ceu, 32u}, {0x2d3u, 64u}, {0x29bu, 2u}, {0x2abu, 4u},
|
||||
}};
|
||||
const std::array<ShaderRegister, 2> primitive{{{0x242u, 1u}, {0x25bu, 0x201u}}};
|
||||
AgcDriverResolveGraphicsStagesAbi_nid_postfix(stages, context, primitive);
|
||||
}
|
||||
|
||||
void testWorkerFailure() {
|
||||
std::array<std::uint32_t, 5> words{0xc0031500, 1, 1, 1, 0x41};
|
||||
Packet packet{words.data(), static_cast<std::uint32_t>(words.size()), 0, {}};
|
||||
@@ -804,6 +867,12 @@ void testWorkerFailure() {
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
try {
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--eop-interrupts") {
|
||||
testEndOfPipeInterrupts();
|
||||
LibcRunShutdown_nid_postfix();
|
||||
std::puts("AGC end-of-pipe interrupt queue mode tests passed");
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--readonly-resummarize") {
|
||||
testReadOnlyResummarize();
|
||||
LibcRunShutdown_nid_postfix();
|
||||
@@ -886,6 +955,7 @@ int main(int argc, char** argv) {
|
||||
testProgramWorkgroupUse();
|
||||
testVertexTailSnapshots();
|
||||
testRegisteredVertexWorkgroupRouting();
|
||||
testRegisteredFastLaunchWorkgroupVertex();
|
||||
testEvents();
|
||||
testValidation();
|
||||
testClearState();
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.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 <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Threads = 64;
|
||||
constexpr std::size_t BlockBytes = 65536;
|
||||
constexpr std::uint32_t InputDwords = 1024;
|
||||
constexpr std::uint32_t Fill = 0xcdcdcdcdu;
|
||||
constexpr std::uint32_t OutputBase = 0x8000;
|
||||
constexpr std::uint32_t OutputDwords = 8;
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 33> Code{
|
||||
0x34040082, 0x34060085, 0x4a0606ff, OutputBase,
|
||||
0xdc308010, 0x04000002, 0xdc308014, 0x05000002, 0xdc308018, 0x06000002,
|
||||
0xdc308403, 0x07000002,
|
||||
0xf4000100, 0xfa000203, 0xf4000140, 0xfa0002f6,
|
||||
0xbf8c3f70, 0xbf8cc07f, 0x7e100204, 0x7e120205,
|
||||
0xdc708000, 0x00000403, 0xdc708004, 0x00000503, 0xdc708008, 0x00000603, 0xdc70800c, 0x00000703,
|
||||
0xdc708010, 0x00000803, 0xdc708014, 0x00000903, 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, "BDA anchor reads: 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::string Hex(std::uint32_t value) {
|
||||
char text[16];
|
||||
std::snprintf(text, sizeof(text), "0x%08x", value);
|
||||
return text;
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> Initial() {
|
||||
std::vector<std::uint32_t> memory(BlockBytes / 4u, Fill);
|
||||
for (std::uint32_t dword = 0; dword < InputDwords; ++dword) memory[dword] = 0x61000000u + dword * 0x00010307u;
|
||||
return memory;
|
||||
}
|
||||
|
||||
std::uint32_t Bytes(const std::vector<std::uint32_t>& memory, std::uint32_t address) {
|
||||
std::uint32_t value = 0;
|
||||
for (std::uint32_t byte = 0; byte < 4u; ++byte) {
|
||||
const std::uint32_t at = address + byte;
|
||||
value |= ((memory[at / 4u] >> (at % 4u * 8u)) & 0xffu) << (byte * 8u);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> Expected() {
|
||||
const auto initial = Initial();
|
||||
auto memory = initial;
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
auto* out = &memory[OutputBase / 4u + tid * OutputDwords];
|
||||
out[0] = Bytes(initial, tid * 4u + 0x10u);
|
||||
out[1] = Bytes(initial, tid * 4u + 0x14u);
|
||||
out[2] = Bytes(initial, tid * 4u + 0x18u);
|
||||
out[3] = Bytes(initial, tid * 4u + 0x403u);
|
||||
out[4] = Bytes(initial, 0x200u);
|
||||
out[5] = Bytes(initial, 0x2f4u);
|
||||
}
|
||||
return memory;
|
||||
}
|
||||
|
||||
ShaderRecompiler::RecompileResult Recompile(std::uint32_t waveSize, std::span<const std::uint32_t> userData, std::span<const std::byte> guest, const ShaderRecompiler::SpirvTarget& target) {
|
||||
const std::span<const std::uint32_t> code(Code);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 2> regions{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}, {reinterpret_cast<std::uintptr_t>(guest.data()), guest}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {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, regions},
|
||||
target,
|
||||
{0, 0, 0, 128}
|
||||
};
|
||||
request.useCache = false;
|
||||
return ShaderRecompiler::Recompile(request);
|
||||
}
|
||||
|
||||
bool SearchesAnchor(const ShaderRecompiler::SharedSpirv& spirv) {
|
||||
const std::string_view text(reinterpret_cast<const char*>(spirv.data()), spirv.size() * sizeof(std::uint32_t));
|
||||
return text.find("bda_anchor_range") != std::string_view::npos;
|
||||
}
|
||||
|
||||
void Run(AgcDriver::VulkanDevice& device, GuestBlock& guest, std::uint32_t waveSize, const ShaderRecompiler::SpirvTarget& target, const std::string& name) {
|
||||
auto memory = Initial();
|
||||
std::memcpy(guest.Data(), memory.data(), BlockBytes);
|
||||
const auto address = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(guest.Data()));
|
||||
const std::vector<std::uint32_t> userData{static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u)};
|
||||
const auto result = Recompile(waveSize, userData, std::span<const std::byte>(reinterpret_cast<const std::byte*>(guest.Data()), InputDwords * sizeof(std::uint32_t)), target);
|
||||
Require(SearchesAnchor(result.spirv), "BDA anchor reads: " + name + " reads of one user-data base do not share an anchor");
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(Code.data()));
|
||||
device.WaitIdle();
|
||||
std::memcpy(memory.data(), guest.Data(), BlockBytes);
|
||||
const auto expected = Expected();
|
||||
for (std::size_t dword = 0; dword < memory.size(); ++dword) {
|
||||
Require(memory[dword] == expected[dword], "BDA anchor reads: " + name + " byte " + std::to_string(dword * 4u) + " is " + Hex(memory[dword]) + ", expected " + Hex(expected[dword]));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
GuestBlock guest;
|
||||
const auto native = device->Target();
|
||||
const auto split = device->ComputeTarget(32);
|
||||
Run(*device, guest, 32, native, "wave32");
|
||||
Run(*device, guest, 64, native, "wave64");
|
||||
if (split.subgroupSize != native.subgroupSize) Run(*device, guest, 64, split, "wave64 split");
|
||||
std::puts("BDA anchor read tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Threads = 32;
|
||||
constexpr std::uint32_t Sentinel = 0x5eed0001u;
|
||||
constexpr std::size_t OffsetWord = 2;
|
||||
alignas(256) std::array<std::uint32_t, Threads + 1> Buffer{};
|
||||
alignas(256) std::array<std::uint32_t, Threads> Output{};
|
||||
|
||||
alignas(256) auto Code = std::to_array<std::uint32_t>({
|
||||
0x34020082,
|
||||
0x7e040281,
|
||||
0xe0c85000, 0x80000201,
|
||||
0xbf8c3f70,
|
||||
0xe0701000, 0x80010201,
|
||||
0xbf810000,
|
||||
});
|
||||
|
||||
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};
|
||||
}
|
||||
|
||||
void Run(AgcDriver::VulkanDevice& device, std::uint32_t offset) {
|
||||
for (std::uint32_t lane = 0; lane < Threads; ++lane) Buffer[lane] = lane * 100u;
|
||||
Buffer[Threads] = Sentinel;
|
||||
Output.fill(0xdeadbeefu);
|
||||
Code[OffsetWord] = 0xe0c85000u | offset;
|
||||
std::vector<std::uint32_t> userData(8, 0u);
|
||||
const auto buffer = BufferDescriptor(Buffer.data(), static_cast<std::uint32_t>(Buffer.size() * 4u));
|
||||
const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size() * 4u));
|
||||
std::copy(buffer.begin(), buffer.end(), userData.begin());
|
||||
std::copy(output.begin(), output.end(), userData.begin() + 4);
|
||||
const std::span<const std::uint32_t> code(Code);
|
||||
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();
|
||||
}
|
||||
|
||||
void Check(std::uint32_t offset) {
|
||||
const auto what = "buffer_atomic_add at byte offset " + std::to_string(offset) + ": lane ";
|
||||
for (std::uint32_t lane = 0; lane < Threads; ++lane) {
|
||||
Require(Output[lane] == lane * 100u, what + std::to_string(lane) + " returned " + std::to_string(Output[lane]) + ", expected " + std::to_string(lane * 100u));
|
||||
Require(Buffer[lane] == lane * 100u + 1u, what + std::to_string(lane) + " left dword " + std::to_string(Buffer[lane]) + ", expected " + std::to_string(lane * 100u + 1u));
|
||||
}
|
||||
Require(Buffer[Threads] == Sentinel, what + "the dword after the last lane changed");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
if (!TargetHasCapability(device->Target(), spv::CapabilityInt64)) {
|
||||
std::puts("skipped, the device has no shaderInt64");
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
for (std::uint32_t offset = 0; offset < 4u; ++offset) {
|
||||
Run(*device, offset);
|
||||
Check(offset);
|
||||
}
|
||||
std::puts("buffer atomic unaligned tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -244,9 +244,13 @@ void RunAtomic(AgcDriver::VulkanDevice& device, std::uint32_t misalignment) {
|
||||
for (std::uint32_t lane = 0; lane < Lanes; ++lane) Require(Output[128u + lane] == Fill, run + ": lane " + std::to_string(lane) + " returned " + Hex(Output[128u + lane]) + ", expected " + Hex(Fill));
|
||||
}
|
||||
|
||||
void RunAtomicFault(AgcDriver::VulkanDevice& device) {
|
||||
void RunAtomicContainingDword(AgcDriver::VulkanDevice& device) {
|
||||
DispatchAtomic(device, 2u, 0u);
|
||||
for (std::uint32_t word = 0; word < Output.size(); ++word) Require(Output[word] == Fill, "buffer unaligned base atomic, view +2, offset lane * 4: dword " + std::to_string(word) + " is " + Hex(Output[word]) + ", but an atomic off a DWORD boundary faults the wave on RDNA2");
|
||||
for (std::uint32_t word = 0; word < 128u; ++word) {
|
||||
const auto expected = word >= 1u && word < Lanes ? Fill + word + 1u : Fill;
|
||||
Require(Output[word] == expected, "buffer unaligned base atomic, view +2, offset lane * 4: dword " + std::to_string(word) + " is " + Hex(Output[word]) + ", expected " + Hex(expected) + ": a 32-bit atomic off a DWORD boundary acts on its containing dword");
|
||||
}
|
||||
for (std::uint32_t lane = 0; lane < Lanes; ++lane) Require(Output[128u + lane] == Fill, "buffer unaligned base atomic, view +2, offset lane * 4: lane " + std::to_string(lane) + " returned " + Hex(Output[128u + lane]) + ", expected " + Hex(Fill));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -272,7 +276,7 @@ int main() {
|
||||
RunFormattedFault(*device, "tbuffer_load_format_x 32_UINT", Load32Code, view);
|
||||
RunFormattedFault(*device, "tbuffer_load_format_x 16_16_UINT", Load16_16Code, view);
|
||||
}
|
||||
RunAtomicFault(*device);
|
||||
RunAtomicContainingDword(*device);
|
||||
std::puts("buffer unaligned base tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "SceShaders.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
|
||||
constexpr std::uint32_t Threads = 64;
|
||||
constexpr std::uint32_t Untouched = 0xcafebabeu;
|
||||
constexpr std::uint32_t Marker = 0x55u;
|
||||
constexpr std::uint64_t FsTable = 0xfe0040000ull;
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 32> Caller{
|
||||
0x34040082u, 0x7e0202ffu, 0x00000055u, 0xbeea03ffu, 0xe0040000u, 0xbeeb038fu, 0xf4080235u, 0xfa000000u, 0xbf8cc07fu, 0xf4240384u,
|
||||
0xfa000020u, 0xbf8cc07fu, 0xbf130e80u, 0xbf840006u, 0xf4240404u, 0xfa000028u, 0xbf8cc07fu, 0xbe8e210eu, 0xe0701000u, 0x80010102u,
|
||||
0xbf810000u, 0xbf9f0000u};
|
||||
alignas(256) std::array<std::uint32_t, Threads> Output{};
|
||||
|
||||
std::vector<std::uint32_t> Commands() {
|
||||
std::vector<std::uint32_t> words;
|
||||
const auto registers = [&](std::uint32_t first, std::span<const std::uint32_t> values) {
|
||||
words.push_back(0xc0007600u | (static_cast<std::uint32_t>(values.size()) << 16u));
|
||||
words.push_back(first);
|
||||
words.insert(words.end(), values.begin(), values.end());
|
||||
};
|
||||
const std::array<std::uint32_t, 3> shape{Threads, 1, 1};
|
||||
registers(0x207, shape);
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(Caller.data());
|
||||
const std::array<std::uint32_t, 2> program{static_cast<std::uint32_t>(address >> 8u), static_cast<std::uint32_t>(address >> 40u)};
|
||||
registers(0x20c, program);
|
||||
const std::array<std::uint32_t, 1> resource{8u << 1u};
|
||||
registers(0x213, resource);
|
||||
const auto output = reinterpret_cast<std::uintptr_t>(Output.data());
|
||||
const std::array<std::uint32_t, 8> data{0u, 0u, 0u, 0u, static_cast<std::uint32_t>(output), static_cast<std::uint32_t>((output >> 32u) & 0xffffu),
|
||||
static_cast<std::uint32_t>(sizeof(Output)), 0x31016facu};
|
||||
registers(0x240, data);
|
||||
words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x41u});
|
||||
return words;
|
||||
}
|
||||
|
||||
std::string Dispatch() {
|
||||
Output.fill(Untouched);
|
||||
const auto words = Commands();
|
||||
Packet packet{const_cast<std::uint32_t*>(words.data()), static_cast<std::uint32_t>(words.size()), 0, {}};
|
||||
std::string failure;
|
||||
try {
|
||||
AgcDriver::Submit(&packet, 0);
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
} catch (const std::exception& error) {
|
||||
failure = error.what();
|
||||
}
|
||||
AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast<std::uintptr_t>(Output.data()), sizeof(Output));
|
||||
return failure;
|
||||
}
|
||||
|
||||
void CheckTable() {
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
Require(AgcDriver::GuestMemory::Accessible(reinterpret_cast<const void*>(FsTable), 0x60u), "the FS table at 0xfe0040000 is not mapped");
|
||||
std::array<std::uint64_t, 12> slots{};
|
||||
std::memcpy(slots.data(), reinterpret_cast<const void*>(FsTable), sizeof(slots));
|
||||
const auto records = slots[0] & 0xffffffffffffull;
|
||||
Require(slots[0] >> 48u == 16u, "FS table slot 0 does not have a stride of 16");
|
||||
Require(slots[1] == 0x0000520400000008ull, "FS table slot 0 does not describe eight records with dword3 0x5204");
|
||||
for (std::size_t slot = 2; slot < slots.size(); ++slot) Require(slots[slot] == 0u, "an FS table slot other than the first is not zero");
|
||||
Require(records % 16u == 0u && AgcDriver::GuestMemory::Accessible(reinterpret_cast<const void*>(records), 128u), "the FS table records are not readable");
|
||||
std::array<std::uint64_t, 16> table{};
|
||||
std::memcpy(table.data(), reinterpret_cast<const void*>(records), sizeof(table));
|
||||
for (std::size_t record = 0; record < 8u; ++record) {
|
||||
Require(table[record * 2u] != 0u && table[record * 2u] == table[2u] && table[record * 2u + 1u] == 0u, "an FS table record does not hold the routine and a zero");
|
||||
}
|
||||
std::array<std::uint32_t, 2> routine{};
|
||||
Require(AgcDriver::GuestMemory::Accessible(reinterpret_cast<const void*>(table[2]), sizeof(routine)), "the FS routine is not readable");
|
||||
std::memcpy(routine.data(), reinterpret_cast<const void*>(table[2]), sizeof(routine));
|
||||
Require(routine[0] == 0xbe80200eu, "the FS routine does not start with s_setpc_b64 s[14:15]");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
{
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
}
|
||||
CheckTable();
|
||||
const auto failure = Dispatch();
|
||||
Require(failure.find("not statically resolvable") == std::string::npos, "the call through the FS table did not link: " + failure);
|
||||
Require(failure.empty(), "the dispatch failed: " + failure);
|
||||
for (std::uint32_t lane = 0; lane < Threads; ++lane) {
|
||||
Require(Output[lane] == Marker, "lane " + std::to_string(lane) + " is " + std::to_string(Output[lane]) + " after the call through the FS table, expected " + std::to_string(Marker));
|
||||
}
|
||||
AgcDriverShutdown_nid_postfix();
|
||||
std::puts("compute FS table call tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
try {
|
||||
AgcDriverShutdown_nid_postfix();
|
||||
} catch (const std::exception&) {
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.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 <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <span>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using AgcDriver::Graphics::StorageTexture;
|
||||
constexpr std::uint32_t Side = 256;
|
||||
constexpr std::uint32_t Format32UInt = 20;
|
||||
constexpr std::uint32_t TileR64KBX = 0x1b;
|
||||
constexpr std::uint32_t Type2D = 9;
|
||||
constexpr std::size_t Block = 0x10000;
|
||||
constexpr std::size_t SurfaceBytes = 4 * Block;
|
||||
constexpr std::uint32_t Untouched = 0xdeadbeefu;
|
||||
constexpr std::uint32_t Stored = 0x12345678u;
|
||||
constexpr std::uint32_t StoredAgain = 0x0badf00du;
|
||||
constexpr auto IdleStoreWindow = std::chrono::milliseconds(500);
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 7> StoreCode{
|
||||
0x7e040300, 0x7e060280, 0x7e0202ff, Stored, 0xf0200108, 0x00020102, 0xbf810000,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 7> StoreAgainCode{
|
||||
0x7e040300, 0x7e060280, 0x7e0202ff, StoredAgain, 0xf0200108, 0x00020102, 0xbf810000,
|
||||
};
|
||||
|
||||
std::uint64_t AddressOf(const void* data) {
|
||||
return static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(data));
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 8> TextureDescriptor(const std::uint32_t* texels) {
|
||||
const auto address = AddressOf(texels);
|
||||
return {
|
||||
static_cast<std::uint32_t>(address >> 8u),
|
||||
static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((Side - 1u) & 3u) << 30u),
|
||||
((Side - 1u) >> 2u) | ((Side - 1u) << 14u),
|
||||
0xfacu | (TileR64KBX << 20u) | (Type2D << 28u),
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
};
|
||||
}
|
||||
|
||||
void Store(const std::uint32_t* texels, std::span<const std::uint32_t> code) {
|
||||
std::array<std::uint32_t, 16> userData{};
|
||||
const auto texture = TextureDescriptor(texels);
|
||||
std::copy(texture.begin(), texture.end(), userData.begin() + 8);
|
||||
std::vector<std::uint32_t> words;
|
||||
const auto registers = [&](const std::uint32_t first, const std::span<const std::uint32_t> values) {
|
||||
words.push_back(0xc0007600u | (static_cast<std::uint32_t>(values.size()) << 16u));
|
||||
words.push_back(first);
|
||||
words.insert(words.end(), values.begin(), values.end());
|
||||
};
|
||||
const std::array<std::uint32_t, 3> shape{8, 1, 1};
|
||||
registers(0x207, shape);
|
||||
const auto address = AddressOf(code.data());
|
||||
const std::array<std::uint32_t, 2> program{static_cast<std::uint32_t>(address >> 8u), static_cast<std::uint32_t>(address >> 40u)};
|
||||
registers(0x20c, program);
|
||||
const std::array<std::uint32_t, 1> layout{static_cast<std::uint32_t>(userData.size()) << 1u};
|
||||
registers(0x213, layout);
|
||||
registers(0x240, userData);
|
||||
words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x41u});
|
||||
Packet packet{words.data(), static_cast<std::uint32_t>(words.size()), 0, {}};
|
||||
AgcDriver::Submit(&packet, 0);
|
||||
}
|
||||
|
||||
std::uint32_t* MapSurface() {
|
||||
#ifdef _WIN32
|
||||
auto* texels = static_cast<std::uint32_t*>(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, Block));
|
||||
if (texels != nullptr) GuestArena::GuestArenaCommit_nid_postfix(texels, SurfaceBytes, PAGE_READWRITE, SurfaceBytes);
|
||||
return texels;
|
||||
#else
|
||||
void* mapped = mmap(nullptr, SurfaceBytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
if (mapped == MAP_FAILED) return nullptr;
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(mapped);
|
||||
const auto aligned = (begin + Block - 1) & ~std::uintptr_t{Block - 1};
|
||||
if (aligned != begin) munmap(mapped, aligned - begin);
|
||||
if (aligned != begin + Block) munmap(reinterpret_cast<void*>(aligned + SurfaceBytes), begin + Block - aligned);
|
||||
auto* texels = reinterpret_cast<std::uint32_t*>(aligned);
|
||||
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(texels, SurfaceBytes);
|
||||
return texels;
|
||||
#endif
|
||||
}
|
||||
|
||||
void ReleaseSurface(std::uint32_t* texels) {
|
||||
#ifdef _WIN32
|
||||
GuestArena::GuestArenaReset_nid_postfix(texels, SurfaceBytes);
|
||||
GuestArena::GuestArenaRelease_nid_postfix(texels, SurfaceBytes);
|
||||
#else
|
||||
munmap(texels, SurfaceBytes);
|
||||
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(texels, SurfaceBytes);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Holds(const std::uint32_t* texels, std::uint32_t value) {
|
||||
const auto* words = static_cast<const volatile std::uint32_t*>(texels);
|
||||
for (std::size_t index = 0; index < SurfaceBytes / 4u; ++index) {
|
||||
if (words[index] == value) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool HoldLive(std::unique_lock<AgcDriver::GuestMemory::GpuMutexType>& hold, const std::uint32_t* texels, std::uint32_t result) {
|
||||
const auto address = AddressOf(texels);
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10);
|
||||
while (true) {
|
||||
hold.lock();
|
||||
const std::array<std::pair<std::uint64_t, std::uint64_t>, 1> range{{{address, address + SurfaceBytes}}};
|
||||
if (StorageTexture::FindLive(address, SurfaceBytes) != nullptr && StorageTexture::AnyPendingOverlaps(range)) return true;
|
||||
hold.unlock();
|
||||
if (Holds(texels, result)) return false;
|
||||
Require(std::chrono::steady_clock::now() < deadline, "idle store over unmap: the result neither became pending nor reached guest memory");
|
||||
std::this_thread::yield();
|
||||
}
|
||||
}
|
||||
|
||||
enum class Outcome { Settled, Missed, NotRun };
|
||||
|
||||
Outcome Run() {
|
||||
auto* texels = MapSurface();
|
||||
Require(texels != nullptr, "idle store over unmap: cannot allocate the guest surface");
|
||||
std::fill(texels, texels + SurfaceBytes / 4u, Untouched);
|
||||
GuestAllocations::Mutation().Add(texels, SurfaceBytes, true, true, true);
|
||||
bool mapped = true;
|
||||
struct Unmapped {
|
||||
std::uint32_t* texels;
|
||||
bool& mapped;
|
||||
~Unmapped() {
|
||||
if (!mapped) return;
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
GuestAllocations::Mutation().Remove(texels);
|
||||
ReleaseSurface(texels);
|
||||
}
|
||||
} unmapped{texels, mapped};
|
||||
Require(AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(texels))).guestBytes == SurfaceBytes, "idle store over unmap: unexpected surface size");
|
||||
const auto address = AddressOf(texels);
|
||||
if (!AgcDriver::GuestMemory::Watched(address, SurfaceBytes)) {
|
||||
std::printf("skipped, the surface is not write-watched\n");
|
||||
return Outcome::NotRun;
|
||||
}
|
||||
AgcDriver::GuestMemory::Unwatch(address, SurfaceBytes);
|
||||
Require(!AgcDriver::GuestMemory::Watched(address, SurfaceBytes), "idle store over unmap: the surface is still write-watched");
|
||||
std::unique_lock hold(AgcDriver::GuestMemory::GpuMutex(), std::defer_lock);
|
||||
Store(texels, StoreCode);
|
||||
if (!HoldLive(hold, texels, Stored)) return Outcome::Missed;
|
||||
StorageTexture::FlushAllPending("idle store over unmap fixture");
|
||||
hold.unlock();
|
||||
{
|
||||
std::vector<std::byte> first(SurfaceBytes);
|
||||
AgcDriver::GuestMemory::Read(address, first);
|
||||
}
|
||||
Require(static_cast<const volatile std::uint32_t*>(texels)[0] == Stored, "idle store over unmap: the first result was not stored");
|
||||
hold.lock();
|
||||
const auto image = StorageTexture::FindLive(address, SurfaceBytes);
|
||||
const bool saved = image != nullptr && image->GuestSnapshotValid();
|
||||
hold.unlock();
|
||||
Require(image != nullptr, "idle store over unmap: the surface has no storage image");
|
||||
if (!saved) {
|
||||
std::printf("skipped, the first store left no saved guest bytes to compare with\n");
|
||||
return Outcome::NotRun;
|
||||
}
|
||||
Store(texels, StoreAgainCode);
|
||||
if (!HoldLive(hold, texels, StoredAgain)) return Outcome::Missed;
|
||||
{
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Unmap(texels, SurfaceBytes, [](const void* piece, std::size_t, const void*, bool) {
|
||||
ReleaseSurface(static_cast<std::uint32_t*>(const_cast<void*>(piece)));
|
||||
});
|
||||
mapped = false;
|
||||
hold.unlock();
|
||||
std::this_thread::sleep_for(IdleStoreWindow);
|
||||
Require(StorageTexture::AnyPending(), "idle store over unmap: the queue's idle store ran while a guest unmap of the surface was in progress");
|
||||
}
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10);
|
||||
while (StorageTexture::AnyPending() && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
Require(!StorageTexture::AnyPending(), "idle store over unmap: the queue's idle store did not settle the results of the unmapped surface");
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
return Outcome::Settled;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
{
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
}
|
||||
struct ShutdownGuard {
|
||||
~ShutdownGuard() { try { AgcDriverShutdown_nid_postfix(); } catch (...) {} }
|
||||
} shutdown;
|
||||
auto outcome = Outcome::Missed;
|
||||
for (std::uint32_t attempt = 0; attempt < 64u && outcome == Outcome::Missed; ++attempt) outcome = Run();
|
||||
Require(outcome != Outcome::Missed, "idle store over unmap: the result was never held pending");
|
||||
if (outcome == Outcome::NotRun) return VulkanTestSkipped;
|
||||
std::puts("idle store over unmap tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.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 <cmath>
|
||||
#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 Size = 4;
|
||||
constexpr std::uint32_t Type2D = 9;
|
||||
constexpr std::uint32_t Srgb8 = 128;
|
||||
constexpr std::uint32_t Srgb8_8 = 129;
|
||||
constexpr std::uint32_t SwizzleX = 0xfacu;
|
||||
constexpr std::uint32_t SwizzleYAsX = 0xfadu;
|
||||
constexpr std::uint32_t FilterPoint = 0;
|
||||
constexpr std::uint32_t FilterBilinear = 1;
|
||||
constexpr std::array<std::uint8_t, Size> Codes{0, 255, 128, 64};
|
||||
constexpr std::size_t BlockBytes = 2u << 20u;
|
||||
constexpr std::uint32_t StoreSize = 1024;
|
||||
|
||||
alignas(256) std::array<float, Threads * 3> Input{};
|
||||
alignas(256) std::array<float, Threads * 4> Output{};
|
||||
alignas(4096) std::array<std::uint8_t, 4096> Texels{};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 13> SampleCode{
|
||||
0x1614008c, 0xe03c1000, 0x8000010a, 0xbf8c3f70, 0xf0900108, 0x00820401, 0xbf8c3f70,
|
||||
0x34160082, 0xe0701000, 0x8001040b, 0xbf810000, 0xbf810000, 0xbf810000,
|
||||
};
|
||||
|
||||
alignas(256) std::array<std::uint32_t, 11> StoreCode{
|
||||
0x7e280d00, 0x102828ff, 0x3d000000, 0x062a28ff, 0x41000000, 0x7e3c0300, 0x7e3e0280, 0xf0201108, 0x0001141e, 0xbf810000, 0xbf810000,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 13> GatherCode{
|
||||
0x1614008c, 0xe03c1000, 0x8000010a, 0xbf8c3f70, 0xf11c0108, 0x00820401, 0xbf8c3f70,
|
||||
0x34160084, 0xe0781000, 0x8001040b, 0xbf810000, 0xbf810000, 0xbf810000,
|
||||
};
|
||||
|
||||
|
||||
class GuestBlock {
|
||||
public:
|
||||
GuestBlock() {
|
||||
#ifdef _WIN32
|
||||
block = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536));
|
||||
if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, PAGE_READWRITE, BlockBytes);
|
||||
#else
|
||||
constexpr std::uintptr_t alignment = 65536;
|
||||
void* mapped = mmap(nullptr, BlockBytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
if (mapped != MAP_FAILED) {
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(mapped);
|
||||
const auto aligned = (begin + alignment - 1) & ~(alignment - 1);
|
||||
if (aligned != begin) munmap(mapped, aligned - begin);
|
||||
if (aligned + BlockBytes != begin + BlockBytes + alignment) munmap(reinterpret_cast<void*>(aligned + BlockBytes), begin + alignment - aligned);
|
||||
block = reinterpret_cast<std::uint8_t*>(aligned);
|
||||
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, BlockBytes);
|
||||
}
|
||||
#endif
|
||||
Require(block != nullptr, "image sRGB sample copy: cannot allocate the guest block");
|
||||
Require(AgcDriver::GuestMemory::Watched(reinterpret_cast<std::uintptr_t>(block), BlockBytes), "image sRGB sample copy: the guest block is not write-watched");
|
||||
std::memset(block, 0, BlockBytes);
|
||||
GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true);
|
||||
AgcDriver::GuestMemory::CollectWritesUncached(reinterpret_cast<std::uintptr_t>(block), BlockBytes);
|
||||
}
|
||||
|
||||
~GuestBlock() {
|
||||
GuestAllocations::Mutation().Remove(block);
|
||||
#ifdef _WIN32
|
||||
GuestArena::GuestArenaReset_nid_postfix(block, BlockBytes);
|
||||
GuestArena::GuestArenaRelease_nid_postfix(block, BlockBytes);
|
||||
#else
|
||||
munmap(block, BlockBytes);
|
||||
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, BlockBytes);
|
||||
#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 = 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(std::uint32_t format, std::uint32_t swizzle, const void* data = Texels.data(), std::uint32_t extent = Size) {
|
||||
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) | (((extent - 1u) & 3u) << 30u),
|
||||
((extent - 1u) >> 2u) | ((extent - 1u) << 14u),
|
||||
swizzle | (Type2D << 28u),
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
};
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 4> SamplerDescriptor(std::uint32_t filter) {
|
||||
return {0x92u, 0x00fff000u, (filter << 20u) | (filter << 22u) | (1u << 24u), 0u};
|
||||
}
|
||||
|
||||
double Decode(std::uint32_t code) {
|
||||
const double encoded = code / 255.0;
|
||||
return encoded <= 0.04045 ? encoded / 12.92 : std::pow((encoded + 0.055) / 1.055, 2.4);
|
||||
}
|
||||
|
||||
std::uint32_t Code(std::uint32_t format, bool second, std::uint32_t x) {
|
||||
const std::uint32_t code = Codes.at(std::min(x, Size - 1u));
|
||||
return second && format == Srgb8_8 ? 255u - code : code;
|
||||
}
|
||||
|
||||
void FillTexels(std::uint32_t format) {
|
||||
const std::uint32_t bytes = format == Srgb8 ? 1u : 2u;
|
||||
const auto mips = AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, format, Size, Size, 1);
|
||||
Require(AgcDriver::Graphics::ComputeSurfaceSize(mips, 1) <= Texels.size(), "image sRGB sample copy: the texture does not fit the texel storage");
|
||||
Texels.fill(0);
|
||||
for (std::uint32_t y = 0; y < Size; ++y) {
|
||||
for (std::uint32_t x = 0; x < Size; ++x) {
|
||||
auto* texel = &Texels[mips[0].tiledOffset + static_cast<std::uint64_t>(y) * mips[0].pitchBytes + x * bytes];
|
||||
texel[0] = static_cast<std::uint8_t>(Code(format, false, x));
|
||||
if (bytes == 2u) texel[1] = static_cast<std::uint8_t>(Code(format, true, x));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FillInput(bool gather, bool between) {
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
const std::uint32_t base = tid % (Size - 1u);
|
||||
float u = (static_cast<float>(tid % Size) + 0.5f) / static_cast<float>(Size);
|
||||
if (gather) u = (static_cast<float>(base) + 0.6f) / static_cast<float>(Size);
|
||||
else if (between) u = static_cast<float>(base + 1u) / static_cast<float>(Size);
|
||||
Input[tid * 3u + 0u] = u;
|
||||
Input[tid * 3u + 1u] = 0.6f / static_cast<float>(Size);
|
||||
Input[tid * 3u + 2u] = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
bool Close(double actual, double expected) {
|
||||
return std::fabs(actual - expected) <= 0.01 + std::fabs(expected) * 0.04;
|
||||
}
|
||||
|
||||
void Store(AgcDriver::VulkanDevice& device, std::uint8_t* texels, float scale) {
|
||||
std::vector<std::uint32_t> userData(16, 0u);
|
||||
const auto texture = TextureDescriptor(Srgb8, SwizzleX, texels, StoreSize);
|
||||
std::copy(texture.begin(), texture.end(), userData.begin() + 4);
|
||||
std::uint32_t bits = 0;
|
||||
std::memcpy(&bits, &scale, sizeof(bits));
|
||||
StoreCode[2] = bits;
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(StoreCode.data()), std::as_bytes(std::span<const std::uint32_t>(StoreCode))}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1};
|
||||
ShaderRecompiler::RecompileRequest request{
|
||||
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(StoreCode.data()), std::span<const std::uint32_t>(StoreCode), 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>(StoreCode.data()));
|
||||
device.WaitIdle();
|
||||
}
|
||||
|
||||
void CheckStored(AgcDriver::VulkanDevice& device, std::uint8_t* texels, float scale, const std::string& what) {
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
Input[tid * 3u + 0u] = (static_cast<float>(tid) + 0.5f) / static_cast<float>(StoreSize);
|
||||
Input[tid * 3u + 1u] = 0.5f / static_cast<float>(StoreSize);
|
||||
Input[tid * 3u + 2u] = 0.0f;
|
||||
}
|
||||
std::vector<std::uint32_t> userData(28, 0u);
|
||||
const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(sizeof(Input)));
|
||||
const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(sizeof(Output)));
|
||||
const auto texture = TextureDescriptor(Srgb8, SwizzleX, texels, StoreSize);
|
||||
const auto sampler = SamplerDescriptor(FilterPoint);
|
||||
std::copy(input.begin(), input.end(), userData.begin());
|
||||
std::copy(output.begin(), output.end(), userData.begin() + 4);
|
||||
std::copy(texture.begin(), texture.end(), userData.begin() + 8);
|
||||
std::copy(sampler.begin(), sampler.end(), userData.begin() + 16);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(SampleCode.data()), std::as_bytes(std::span<const std::uint32_t>(SampleCode))}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1};
|
||||
ShaderRecompiler::RecompileRequest request{
|
||||
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(SampleCode.data()), std::span<const std::uint32_t>(SampleCode), 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);
|
||||
Output.fill(-1.0f);
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(SampleCode.data()));
|
||||
device.WaitIdle();
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
const double code = std::round(std::min(1.0, static_cast<double>(tid) * scale) * 255.0);
|
||||
const double expected = Decode(static_cast<std::uint32_t>(code));
|
||||
Require(Close(Output[tid], expected), what + ": thread " + std::to_string(tid) + " sampled " + std::to_string(Output[tid]) + ", expected " + std::to_string(expected));
|
||||
}
|
||||
}
|
||||
|
||||
void CheckStorageSource(AgcDriver::VulkanDevice& device) {
|
||||
GuestBlock block;
|
||||
auto* texels = block.Data();
|
||||
const auto surface = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(Srgb8, SwizzleX, texels, StoreSize)));
|
||||
Require(surface.guestBytes >= (1u << 20u) && surface.guestBytes <= BlockBytes, "8_SRGB storage source: the surface does not fit the guest block or is below the storage image floor");
|
||||
AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast<std::uintptr_t>(texels), surface.guestBytes, nullptr, "test");
|
||||
AgcDriver::Graphics::FlushCachedTextures(device.Device());
|
||||
Store(device, texels, 1.0f / 32.0f);
|
||||
CheckStored(device, texels, 1.0f / 32.0f, "8_SRGB sample of a pending storage image");
|
||||
const auto before = AgcDriver::Graphics::StorageTexture::FindPending(reinterpret_cast<std::uintptr_t>(texels), surface.guestBytes);
|
||||
Require(before != nullptr, "8_SRGB storage source: no storage image holds the surface's results");
|
||||
const auto version = before->Version();
|
||||
Store(device, texels, 1.0f / 8.0f);
|
||||
const auto after = AgcDriver::Graphics::StorageTexture::FindPending(reinterpret_cast<std::uintptr_t>(texels), surface.guestBytes);
|
||||
Require(after != nullptr && after->Version() > version, "8_SRGB storage source: the second store did not advance the storage image");
|
||||
CheckStored(device, texels, 1.0f / 8.0f, "8_SRGB sample of a pending storage image after a second store");
|
||||
}
|
||||
|
||||
std::string ForcedMode() {
|
||||
const char* forced = std::getenv("APS5_SRGB_SHADER_DECODE");
|
||||
return forced == nullptr ? std::string{} : std::string{forced};
|
||||
}
|
||||
|
||||
void VKAPI_PTR NativeSrgbFormatProperties(VkPhysicalDevice, VkFormat, VkFormatProperties* properties) {
|
||||
*properties = {};
|
||||
properties->optimalTilingFeatures = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT | VK_FORMAT_FEATURE_BLIT_SRC_BIT | VK_FORMAT_FEATURE_BLIT_DST_BIT;
|
||||
}
|
||||
|
||||
void VKAPI_PTR NoSrgbFormatProperties(VkPhysicalDevice, VkFormat format, VkFormatProperties* properties) {
|
||||
*properties = {};
|
||||
if (format == VK_FORMAT_R8_SRGB || format == VK_FORMAT_R8G8_SRGB) return;
|
||||
properties->optimalTilingFeatures = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT | VK_FORMAT_FEATURE_BLIT_SRC_BIT | VK_FORMAT_FEATURE_BLIT_DST_BIT;
|
||||
}
|
||||
|
||||
std::uint32_t SrgbBits() {
|
||||
return AgcDriver::Graphics::SrgbDecodeFormats(NoSrgbFormatProperties, VK_NULL_HANDLE);
|
||||
}
|
||||
|
||||
void CheckSelection() {
|
||||
const auto mode = ForcedMode();
|
||||
Require(SrgbBits() != 0u, "a device that cannot sample R8_SRGB or R8G8_SRGB selected no sRGB decode formats");
|
||||
const auto decode = AgcDriver::Graphics::SrgbDecodeFormats(NativeSrgbFormatProperties, VK_NULL_HANDLE);
|
||||
const auto copy = AgcDriver::Graphics::SrgbCopyFormats(NativeSrgbFormatProperties, VK_NULL_HANDLE, decode);
|
||||
const std::uint32_t expected = mode == "copy" ? SrgbBits() : 0u;
|
||||
Require(copy == expected, "a device that samples R8_SRGB and R8G8_SRGB selected sRGB copy formats " + std::to_string(copy) + " with APS5_SRGB_SHADER_DECODE='" + mode + "', expected " + std::to_string(expected));
|
||||
}
|
||||
|
||||
void Check(AgcDriver::VulkanDevice& device, std::uint32_t format, bool second, std::uint32_t filter, bool gather, const std::string& what) {
|
||||
FillTexels(format);
|
||||
const bool between = filter == FilterBilinear;
|
||||
FillInput(gather, between);
|
||||
const auto& code = gather ? GatherCode : SampleCode;
|
||||
std::vector<std::uint32_t> userData(28, 0u);
|
||||
const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(sizeof(Input)));
|
||||
const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(sizeof(Output)));
|
||||
const auto texture = TextureDescriptor(format, second ? SwizzleYAsX : SwizzleX);
|
||||
const auto sampler = SamplerDescriptor(filter);
|
||||
std::copy(input.begin(), input.end(), userData.begin());
|
||||
std::copy(output.begin(), output.end(), userData.begin() + 4);
|
||||
std::copy(texture.begin(), texture.end(), userData.begin() + 8);
|
||||
std::copy(sampler.begin(), sampler.end(), userData.begin() + 16);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(std::span<const std::uint32_t>(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()), std::span<const std::uint32_t>(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);
|
||||
Output.fill(-1.0f);
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
|
||||
device.WaitIdle();
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
const std::uint32_t base = tid % (Size - 1u);
|
||||
if (gather) {
|
||||
const double low = Decode(Code(format, second, base));
|
||||
const double high = Decode(Code(format, second, base + 1u));
|
||||
const std::array<double, 4> expected{low, high, high, low};
|
||||
for (std::uint32_t component = 0; component < 4u; ++component) {
|
||||
const double actual = Output[tid * 4u + component];
|
||||
Require(Close(actual, expected[component]), what + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " gathered " + std::to_string(actual) + ", expected " + std::to_string(expected[component]));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
double expected = Decode(Code(format, second, tid % Size));
|
||||
if (between) expected = (Decode(Code(format, second, base)) + Decode(Code(format, second, base + 1u))) / 2.0;
|
||||
const double actual = Output[tid];
|
||||
Require(Close(actual, expected), what + ": thread " + std::to_string(tid) + " sampled " + std::to_string(actual) + ", expected " + std::to_string(expected));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
CheckSelection();
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
if (ForcedMode() == "copy") Require((device->Target().srgbDecodeFormats & SrgbBits()) == 0u, "APS5_SRGB_SHADER_DECODE=copy: the device cannot blit R8 or RG8 to RGBA8, so 8 and 8_8 sRGB samples have no copy");
|
||||
for (const auto format : {Srgb8, Srgb8_8}) {
|
||||
const std::string name = format == Srgb8 ? "8_SRGB" : "8_8_SRGB";
|
||||
Check(*device, format, false, FilterPoint, false, name + " point sample");
|
||||
Check(*device, format, false, FilterBilinear, false, name + " bilinear sample, decoded before filtering");
|
||||
Check(*device, format, false, FilterPoint, true, name + " gather4");
|
||||
if (format == Srgb8_8) {
|
||||
Check(*device, format, true, FilterPoint, false, name + " point sample of the second channel");
|
||||
Check(*device, format, true, FilterBilinear, false, name + " bilinear sample of the second channel, decoded before filtering");
|
||||
Check(*device, format, true, FilterPoint, true, name + " gather4 of the second channel");
|
||||
}
|
||||
}
|
||||
CheckStorageSource(*device);
|
||||
std::puts("image sRGB sample copy tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -148,7 +148,7 @@ void Run(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, const ShaderRe
|
||||
Require(within.workgroupMemoryDwords == 0u, "lds beyond the device limit " + run + ": LDS below the device limit left workgroup memory");
|
||||
fits(within, "LDS below the device limit");
|
||||
const auto atLimit = Compile(waveSize, limit, target);
|
||||
Require(atLimit.workgroupMemoryDwords == limit / 4u + 1u, "lds beyond the device limit " + run + ": LDS at the device limit gave " + std::to_string(atLimit.workgroupMemoryDwords) + " device memory dwords per workgroup");
|
||||
Require(atLimit.workgroupMemoryDwords == limit / 4u + 2u, "lds beyond the device limit " + run + ": LDS at the device limit gave " + std::to_string(atLimit.workgroupMemoryDwords) + " device memory dwords per workgroup");
|
||||
fits(atLimit, "LDS at the device limit");
|
||||
const auto bytes = limit + AboveLimitBytes;
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
@@ -159,7 +159,7 @@ void Run(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, const ShaderRe
|
||||
}
|
||||
Output.fill(Untouched);
|
||||
const auto result = Compile(waveSize, bytes, target);
|
||||
Require(result.workgroupMemoryDwords == bytes / 4u + 1u, "lds beyond the device limit " + run + ": " + std::to_string(bytes) + " bytes of LDS gave " + std::to_string(result.workgroupMemoryDwords) + " device memory dwords per workgroup");
|
||||
Require(result.workgroupMemoryDwords == bytes / 4u + 2u, "lds beyond the device limit " + run + ": " + std::to_string(bytes) + " bytes of LDS gave " + std::to_string(result.workgroupMemoryDwords) + " device memory dwords per workgroup");
|
||||
fits(result, "LDS above the device limit");
|
||||
if (indirect) {
|
||||
IndirectGroups = {groupsX, groupsY, groupsZ, 0u};
|
||||
|
||||
@@ -14,12 +14,61 @@
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#include <io.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
class StderrCapture {
|
||||
public:
|
||||
StderrCapture() {
|
||||
std::fflush(stderr);
|
||||
file = std::tmpfile();
|
||||
Require(file != nullptr, "cannot open a temporary file for stderr");
|
||||
#ifdef _WIN32
|
||||
saved = _dup(_fileno(stderr));
|
||||
_dup2(_fileno(file), _fileno(stderr));
|
||||
#else
|
||||
saved = dup(fileno(stderr));
|
||||
dup2(fileno(file), fileno(stderr));
|
||||
#endif
|
||||
}
|
||||
std::string Finish() {
|
||||
std::fflush(stderr);
|
||||
#ifdef _WIN32
|
||||
_dup2(saved, _fileno(stderr));
|
||||
_close(saved);
|
||||
#else
|
||||
dup2(saved, fileno(stderr));
|
||||
close(saved);
|
||||
#endif
|
||||
std::rewind(file);
|
||||
std::string text;
|
||||
char buffer[4096];
|
||||
for (std::size_t read = 0; (read = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, read);
|
||||
std::fclose(file);
|
||||
std::fwrite(text.data(), 1, text.size(), stderr);
|
||||
return text;
|
||||
}
|
||||
|
||||
private:
|
||||
std::FILE* file = nullptr;
|
||||
int saved = -1;
|
||||
};
|
||||
|
||||
constexpr std::uint32_t Threads = 256;
|
||||
constexpr std::uint32_t Iterations = 16;
|
||||
constexpr std::uint32_t Repeats = 32;
|
||||
@@ -167,6 +216,32 @@ alignas(256) constexpr auto LoadedParityCode = Unrolled<1>(
|
||||
0xbf810000,
|
||||
}));
|
||||
|
||||
alignas(256) constexpr auto MisalignedCode = Unrolled<1>(
|
||||
std::to_array<std::uint32_t>({
|
||||
0x34100083,
|
||||
0x7e020280,
|
||||
0x7e0402c1,
|
||||
0x7e060280,
|
||||
0x7e080281,
|
||||
0x7e0c0280,
|
||||
0x7e0e0280,
|
||||
0xd9340000, 0x00000601,
|
||||
0xbf8cc07f,
|
||||
0xbf8a0000,
|
||||
}),
|
||||
std::to_array<std::uint32_t>({
|
||||
0xd9000004, 0x00000201,
|
||||
0xbf8cc07f,
|
||||
}),
|
||||
std::to_array<std::uint32_t>({
|
||||
0xbf8cc07f,
|
||||
0xbf8a0000,
|
||||
0xd9d80000, 0x0a000001,
|
||||
0xbf8cc07f,
|
||||
0xe0741000, 0x80010a08,
|
||||
0xbf810000,
|
||||
}));
|
||||
|
||||
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};
|
||||
@@ -213,7 +288,7 @@ std::uint64_t WorkgroupBytes(std::span<const std::uint32_t> words) {
|
||||
return total;
|
||||
}
|
||||
|
||||
void Run(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, const char* name, std::uint64_t operations, std::uint32_t repeats, std::uint32_t ldsDwords = LdsDwords, std::optional<std::uint32_t> expectedDeviceDwords = std::nullopt) {
|
||||
ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, std::uint32_t ldsDwords) {
|
||||
std::vector<std::uint32_t> userData(8, 0u);
|
||||
const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size() * 4u));
|
||||
const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size() * 4u));
|
||||
@@ -228,10 +303,14 @@ void Run(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, c
|
||||
{0, 0, 0, 128}
|
||||
};
|
||||
request.useCache = false;
|
||||
const auto result = ShaderRecompiler::Recompile(request);
|
||||
return ShaderRecompiler::Recompile(request);
|
||||
}
|
||||
|
||||
void Run(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, const char* name, std::uint64_t operations, std::uint32_t repeats, std::uint32_t ldsDwords = LdsDwords, std::optional<std::uint32_t> expectedDeviceDwords = std::nullopt) {
|
||||
const auto result = Compile(device, code, ldsDwords);
|
||||
if (expectedDeviceDwords) {
|
||||
const auto bytes = WorkgroupBytes(result.spirv.Words());
|
||||
const auto limit = request.target.maxWorkgroupSharedMemoryBytes;
|
||||
const auto limit = device.Target().maxWorkgroupSharedMemoryBytes;
|
||||
Require(bytes <= limit, "lds lock budget: guest LDS and lock exceed the device limit before dispatch");
|
||||
Require(result.workgroupMemoryDwords == *expectedDeviceDwords, "lds lock budget: unexpected device-backed LDS allocation before dispatch");
|
||||
if (*expectedDeviceDwords == 0u) Require(bytes == limit, "lds lock budget: exact-fit case did not retain its lock");
|
||||
@@ -246,6 +325,21 @@ void Run(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, c
|
||||
if (expectedDeviceDwords) std::printf("lds lock budget: %s GPU readback verified across 256 lanes\n", name);
|
||||
}
|
||||
|
||||
void Misaligned(AgcDriver::VulkanDevice& device, std::uint32_t ldsDwords) {
|
||||
const auto result = Compile(device, MisalignedCode, ldsDwords);
|
||||
StderrCapture capture;
|
||||
try {
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(MisalignedCode.data()));
|
||||
device.WaitIdle();
|
||||
} catch (...) {
|
||||
static_cast<void>(capture.Finish());
|
||||
throw;
|
||||
}
|
||||
const auto log = capture.Finish();
|
||||
Require(log.find("BDA access failed: address=0x4 instruction=0x2c bytes=8 ") != std::string::npos && log.find("reason=7") != std::string::npos, "lds lock budget: misaligned ds_add_u64 on device-backed LDS did not fault");
|
||||
std::puts("lds lock budget: misaligned ds_add_u64 faulted");
|
||||
}
|
||||
|
||||
void LockBudget(AgcDriver::VulkanDevice& device) {
|
||||
const auto target = device.Target();
|
||||
const auto limit = target.maxWorkgroupSharedMemoryBytes;
|
||||
@@ -253,7 +347,9 @@ void LockBudget(AgcDriver::VulkanDevice& device) {
|
||||
if (std::getenv("ANYPS5_REQUIRE_ROBUST2") != nullptr) Require(target.robustBufferAccess2, "lds lock budget: robustBufferAccess2 was not enabled");
|
||||
Require(limit >= 32u && limit % 4u == 0u, "lds lock budget: unexpected device shared-memory limit");
|
||||
const auto operations = static_cast<std::uint64_t>(Threads) * Iterations;
|
||||
Run(device, UniformCode, "limit", operations, 1, limit / 4u, limit / 4u + 1u);
|
||||
Run(device, UniformCode, "limit", operations, 1, limit / 4u, (limit / 4u + 2u) & ~1u);
|
||||
const auto& capabilities = target.supportedCapabilities;
|
||||
if (std::find(capabilities.begin(), capabilities.end(), static_cast<std::uint32_t>(spv::CapabilityInt64Atomics)) != capabilities.end()) Misaligned(device, limit / 4u);
|
||||
Run(device, UniformCode, "limit-4", operations, 1, (limit - 4u) / 4u, 0u);
|
||||
std::puts("lds lock budget tests passed");
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp"
|
||||
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
|
||||
#include "prx/libc/include/Shutdown.hpp"
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
@@ -10,10 +13,13 @@
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::DriverDetail::DrawPipeline;
|
||||
|
||||
void check(bool condition, const std::string& what) {
|
||||
if (!condition) throw std::runtime_error("pipelined DMA: " + what);
|
||||
}
|
||||
@@ -22,11 +28,47 @@ std::array<std::uint32_t, 7> DmaData(bool immediate, std::uint64_t source, std::
|
||||
return {0xc0055000u, immediate ? (2u << 29u) : 0u, immediate ? data : static_cast<std::uint32_t>(source), immediate ? 0u : static_cast<std::uint32_t>(source >> 32u), static_cast<std::uint32_t>(destination), static_cast<std::uint32_t>(destination >> 32u), bytes};
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 7> DmaSelect(std::uint32_t destinationSelect, std::uint64_t source, std::uint64_t destination, std::uint32_t bytes) {
|
||||
return {0xc0055000u, destinationSelect << 20u, 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), bytes};
|
||||
}
|
||||
|
||||
template <std::size_t Count>
|
||||
void Append(std::vector<std::uint32_t>& words, const std::array<std::uint32_t, Count>& packet) {
|
||||
words.insert(words.end(), packet.begin(), packet.end());
|
||||
}
|
||||
|
||||
void Submit(std::vector<std::uint32_t>& words) {
|
||||
Packet packet{words.data(), static_cast<std::uint32_t>(words.size()), 0, {}};
|
||||
check(sceAgcDriverSubmitDcb(&packet) == 0, "submission failed");
|
||||
}
|
||||
|
||||
struct Gate {
|
||||
std::atomic<bool> release{false};
|
||||
~Gate() { release.store(true, std::memory_order_release); }
|
||||
};
|
||||
|
||||
void Hold(Gate& gate, std::uint64_t address, std::size_t bytes) {
|
||||
DrawPipeline::Queue0().Enqueue([&gate] {
|
||||
while (!gate.release.load(std::memory_order_acquire)) std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}, {{address, address + bytes}});
|
||||
}
|
||||
|
||||
void ExpectDrains(std::vector<std::uint32_t>& words, std::uint32_t opcode, Gate& gate, const char* what) {
|
||||
const auto before = DrawPipeline::DrainRequestsByOpcode(opcode);
|
||||
std::thread releaser([&] {
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10);
|
||||
while (DrawPipeline::DrainRequestsByOpcode(opcode) == before && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
gate.release.store(true, std::memory_order_release);
|
||||
});
|
||||
Submit(words);
|
||||
releaser.join();
|
||||
check(DrawPipeline::DrainRequestsByOpcode(opcode) == before + 1, what);
|
||||
}
|
||||
|
||||
void ExpectNoDrain(std::uint32_t opcode, std::uint64_t before, const char* what) {
|
||||
check(DrawPipeline::DrainRequestsByOpcode(opcode) == before, what);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
@@ -40,8 +82,12 @@ int main() {
|
||||
try {
|
||||
alignas(256) static std::array<std::uint32_t, 1024> first{};
|
||||
alignas(256) static std::array<std::uint32_t, 1024> second{};
|
||||
alignas(256) static std::array<std::uint32_t, 4> pending{};
|
||||
alignas(256) static std::array<std::uint32_t, 4> elsewhere{};
|
||||
first.fill(0);
|
||||
second.fill(0);
|
||||
pending.fill(0);
|
||||
elsewhere.fill(0);
|
||||
using namespace AgcDriver::DriverDetail;
|
||||
const auto drainedBefore = storesDrained.load();
|
||||
const auto cpuBefore = storesOnCpu.load() + storesOnGpu.load() + storesBehindCompletions.load();
|
||||
@@ -57,6 +103,69 @@ int main() {
|
||||
const auto stores = storesOnCpu.load() + storesOnGpu.load() + storesBehindCompletions.load() - cpuBefore;
|
||||
check(stores == 2, "the DMA_DATA packets were not committed (" + std::to_string(stores) + " committed stores)");
|
||||
check(storesDrained.load() == drainedBefore, "a DMA_DATA packet still drained");
|
||||
|
||||
const auto source = reinterpret_cast<std::uint64_t>(first.data());
|
||||
const auto held = reinterpret_cast<std::uint64_t>(pending.data());
|
||||
const auto other = reinterpret_cast<std::uint64_t>(elsewhere.data());
|
||||
{
|
||||
Gate gate;
|
||||
Hold(gate, held, sizeof(pending));
|
||||
const auto dmaBefore = DrawPipeline::DrainRequestsByOpcode(0x50);
|
||||
const auto predicationBefore = DrawPipeline::DrainRequestsByOpcode(0x20);
|
||||
const auto nopBefore = DrawPipeline::DrainRequestsByOpcode(0x10);
|
||||
words.clear();
|
||||
Append(words, DmaSelect(2, source, 0, 64));
|
||||
Append(words, DmaData(false, source, 0, other, 0));
|
||||
Append(words, DmaSelect(1, source, 0, 4));
|
||||
Append(words, DmaData(false, source, 0, other, 2));
|
||||
Append(words, std::array<std::uint32_t, 4>{0xc0022000u, 0x31100u, static_cast<std::uint32_t>(held), static_cast<std::uint32_t>(held >> 32u)});
|
||||
Append(words, std::array<std::uint32_t, 2>{0xc000102cu, 0x00636261u});
|
||||
Append(words, std::array<std::uint32_t, 2>{0xc0001030u, 0u});
|
||||
Submit(words);
|
||||
ExpectNoDrain(0x50, dmaBefore, "a prefetch, a zero-byte copy, a GDS copy or an unaligned copy off the pending range drained");
|
||||
ExpectNoDrain(0x20, predicationBefore, "SET_PREDICATION drained the pipeline");
|
||||
ExpectNoDrain(0x10, nopBefore, "a marker push or pop drained the pipeline");
|
||||
}
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
|
||||
{
|
||||
Gate gate;
|
||||
Hold(gate, held, sizeof(pending));
|
||||
words.clear();
|
||||
Append(words, DmaData(false, source, 0, held, 2));
|
||||
ExpectDrains(words, 0x50, gate, "a copy onto an uncommitted pipeline write did not drain");
|
||||
}
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
|
||||
{
|
||||
Gate gate;
|
||||
Hold(gate, held, sizeof(pending));
|
||||
words.clear();
|
||||
Append(words, DmaData(false, held, 0, other, 2));
|
||||
ExpectDrains(words, 0x50, gate, "an unaligned copy of an uncommitted pipeline write did not drain");
|
||||
}
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
|
||||
{
|
||||
Gate gate;
|
||||
Hold(gate, held, sizeof(pending));
|
||||
words.clear();
|
||||
Append(words, std::array<std::uint32_t, 4>{0xc0022000u, 0x31100u, static_cast<std::uint32_t>(held), static_cast<std::uint32_t>(held >> 32u)});
|
||||
Append(words, std::array<std::uint32_t, 2>{0xc0001301u, 32u});
|
||||
ExpectDrains(words, 0x13, gate, "a predicated packet whose predicate overlaps an uncommitted write did not drain");
|
||||
}
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
|
||||
{
|
||||
Gate gate;
|
||||
Hold(gate, held, sizeof(pending));
|
||||
const auto resetBefore = DrawPipeline::DrainRequestsByOpcode(0x10);
|
||||
words.clear();
|
||||
Append(words, std::array<std::uint32_t, 2>{0xc0001024u, 0u});
|
||||
Submit(words);
|
||||
ExpectNoDrain(0x10, resetBefore, "DISPATCH_RESET drained the pipeline");
|
||||
}
|
||||
|
||||
LibcRunShutdown_nid_postfix();
|
||||
std::puts("pipelined DMA tests passed");
|
||||
return 0;
|
||||
|
||||
@@ -50,7 +50,7 @@ constexpr std::uint64_t Ceiling = Budget + Budget / 4u;
|
||||
|
||||
class Device {
|
||||
public:
|
||||
explicit Device(bool resourceImages = false) {
|
||||
explicit Device(bool enableStorageImageMultisample = true) {
|
||||
#ifdef _WIN32
|
||||
library = SDL_LoadObject("vulkan-1.dll");
|
||||
#else
|
||||
@@ -96,13 +96,12 @@ public:
|
||||
queue.queueFamilyIndex = family;
|
||||
queue.queueCount = 1;
|
||||
queue.pQueuePriorities = &priority;
|
||||
VkPhysicalDeviceFeatures supported{};
|
||||
function<PFN_vkGetPhysicalDeviceFeatures>("vkGetPhysicalDeviceFeatures")(context.physical, &supported);
|
||||
VkPhysicalDeviceFeatures enabled{};
|
||||
enabled.shaderInt64 = VK_TRUE;
|
||||
if (resourceImages) {
|
||||
VkPhysicalDeviceFeatures supported{};
|
||||
function<PFN_vkGetPhysicalDeviceFeatures>("vkGetPhysicalDeviceFeatures")(context.physical, &supported);
|
||||
enabled.shaderStorageImageMultisample = supported.shaderStorageImageMultisample;
|
||||
}
|
||||
enabled.shaderStorageImageMultisample = enableStorageImageMultisample && supported.shaderStorageImageMultisample;
|
||||
context.storageImageMultisample = enabled.shaderStorageImageMultisample == VK_TRUE;
|
||||
address.pNext = &bytes;
|
||||
std::vector<const char*> extensionsEnabled{VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME, VK_KHR_8BIT_STORAGE_EXTENSION_NAME};
|
||||
if (hasExtension(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME)) {
|
||||
@@ -469,6 +468,48 @@ void heldTests(const Context& context) {
|
||||
Require(TextureCacheUsage().sampledEntries == capacity && TextureCacheUsage().sampledBytes <= Budget, "released textures were not evicted down to the budget");
|
||||
}
|
||||
|
||||
void releasedTests(Context context) {
|
||||
PlaceholderImages placeholders(context);
|
||||
context.placeholderImages = &placeholders;
|
||||
TextureCache textures(context);
|
||||
context.textureCache = &textures;
|
||||
ClearCachedTextures(context.device);
|
||||
auto& cache = SharedResourceCache();
|
||||
cache.Clear();
|
||||
try {
|
||||
Block block(context);
|
||||
auto pinned = ImageResources(context, block, 0, 1, true);
|
||||
Require(pinned->SampledTextures().size() == 1 && !pinned->SampledTextures().front()->ViewsStorageImage(), "the pinned descriptor set does not hold a sampled snapshot");
|
||||
const std::weak_ptr<Texture> texture = pinned->SampledTextures().front();
|
||||
const std::weak_ptr<ShaderResources> pinnedSet = pinned;
|
||||
cache.Insert({1}, pinned);
|
||||
Require(cache.Find({1}) == pinned, "the resource cache rejected a descriptor set whose texture is cached");
|
||||
pinned.reset();
|
||||
ClearCachedTextures(context.device);
|
||||
Require(!texture.expired() && texture.lock()->Released(), "the sampled texture cache dropped a texture the resource cache holds without marking it released");
|
||||
auto live = ImageResources(context, block, 1, 1, true);
|
||||
std::vector<std::shared_ptr<ShaderResources>> displaced;
|
||||
cache.Insert({2}, live, &displaced);
|
||||
Require(cache.Find({1}) == nullptr && cache.Find({2}) == live && cache.Size() == 1, "the resource cache kept an entry holding a released texture");
|
||||
Require(displaced.size() == 1 && displaced.front() == pinnedSet.lock(), "the entry holding a released texture was not handed back to the caller");
|
||||
displaced.clear();
|
||||
Require(pinnedSet.expired() && texture.expired(), "the resource cache kept a released texture alive");
|
||||
auto stale = ImageResources(context, block, 2, 1, true);
|
||||
ClearCachedTextures(context.device);
|
||||
Require(stale->SampledTextures().front()->Released(), "a held texture the sampled cache dropped is not marked released");
|
||||
cache.Insert({3}, stale, &displaced);
|
||||
Require(cache.Find({3}) == nullptr, "a descriptor set holding a released texture entered the resource cache");
|
||||
Require(cache.Size() == 0 && displaced.size() == 1 && displaced.front() == live, "the live entry whose texture was released stayed in the resource cache");
|
||||
displaced.clear();
|
||||
} catch (...) {
|
||||
cache.Clear();
|
||||
throw;
|
||||
}
|
||||
cache.Clear();
|
||||
ClearCachedTextures(context.device);
|
||||
std::puts("resource cache released texture tests passed");
|
||||
}
|
||||
|
||||
void rescanTests(const Context& context) {
|
||||
constexpr std::uint32_t SmallSide = 128;
|
||||
constexpr std::size_t SmallBytes = std::size_t{SmallSide} * SmallSide * 4u;
|
||||
@@ -678,6 +719,7 @@ int main(int argc, char** argv) {
|
||||
const bool memory = argc > 1 && std::string(argv[1]) == "memory";
|
||||
const std::string mode = argc > 1 ? argv[1] : "";
|
||||
const bool resources = mode == "resource-admission" || mode == "resource-oversized" || mode == "resource-clear";
|
||||
const bool disableStorageImageMultisample = mode == "without-storage-image-multisample";
|
||||
#ifdef _WIN32
|
||||
if (!memory) _putenv_s("APS5_TEXTURE_CACHE_MIB", "1");
|
||||
#else
|
||||
@@ -685,13 +727,28 @@ int main(int argc, char** argv) {
|
||||
#endif
|
||||
std::unique_ptr<Device> device;
|
||||
try {
|
||||
device = std::make_unique<Device>(resources);
|
||||
device = std::make_unique<Device>(!disableStorageImageMultisample);
|
||||
} catch (const std::exception& error) {
|
||||
if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw;
|
||||
std::printf("skipped, no usable Vulkan device: %s\n", error.what());
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
auto context = device->GetContext();
|
||||
if (disableStorageImageMultisample) {
|
||||
PlaceholderImages placeholders(context);
|
||||
for (std::uint32_t heap = ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding;
|
||||
heap < ShaderRecompiler::RuntimeAbi::FirstImageBinding + ShaderRecompiler::RuntimeAbi::ImageBindingCount; ++heap) {
|
||||
const auto shape = PlaceholderImageShapeOf(heap);
|
||||
if (shape.samples == VK_SAMPLE_COUNT_1_BIT) continue;
|
||||
bool refused = false;
|
||||
try {
|
||||
placeholders.View(heap);
|
||||
} catch (const std::runtime_error&) {
|
||||
refused = true;
|
||||
}
|
||||
Require(refused, "a device without storage-image MSAA returned a placeholder for typed multisample storage heap " + std::to_string(heap));
|
||||
}
|
||||
}
|
||||
TextureDetiler detiler(context);
|
||||
context.detiler = &detiler;
|
||||
if (resources) {
|
||||
@@ -713,6 +770,7 @@ int main(int argc, char** argv) {
|
||||
reportedBudgetTests(context);
|
||||
snapshotTests(context);
|
||||
heldTests(context);
|
||||
releasedTests(context);
|
||||
rescanTests(context);
|
||||
viewEvictionTests(context);
|
||||
viewTests(context);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
@@ -10,6 +12,7 @@
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -17,6 +20,7 @@
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using AgcDriver::Graphics::Recorder;
|
||||
using AgcDriver::Graphics::StorageTexture;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
@@ -51,8 +55,7 @@ std::array<std::uint32_t, 8> TextureDescriptor(std::uint64_t address) {
|
||||
};
|
||||
}
|
||||
|
||||
std::shared_ptr<StorageTexture> Store(AgcDriver::VulkanDevice& device, std::uint64_t base, std::uint32_t surface, bool flush = true) {
|
||||
const auto address = base + surface * Stride;
|
||||
void Record(AgcDriver::VulkanDevice& device, std::uint64_t address) {
|
||||
std::vector<std::uint32_t> userData(16, 0u);
|
||||
const auto texture = TextureDescriptor(address);
|
||||
std::copy(texture.begin(), texture.end(), userData.begin() + 4);
|
||||
@@ -67,7 +70,13 @@ std::shared_ptr<StorageTexture> Store(AgcDriver::VulkanDevice& device, std::uint
|
||||
};
|
||||
request.useCache = true;
|
||||
const auto result = ShaderRecompiler::Recompile(request);
|
||||
std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex());
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
|
||||
}
|
||||
|
||||
std::shared_ptr<StorageTexture> Store(AgcDriver::VulkanDevice& device, std::uint64_t base, std::uint32_t surface, bool flush = true) {
|
||||
const auto address = base + surface * Stride;
|
||||
Record(device, address);
|
||||
device.WaitIdle();
|
||||
if (!flush) return StorageTexture::FindLive(address, SurfaceBytes);
|
||||
StorageTexture::FlushPending(address, static_cast<std::size_t>(SurfaceBytes), nullptr, "test");
|
||||
@@ -75,10 +84,57 @@ std::shared_ptr<StorageTexture> Store(AgcDriver::VulkanDevice& device, std::uint
|
||||
return StorageTexture::FindLive(address, SurfaceBytes);
|
||||
}
|
||||
|
||||
int RetainedBatch() {
|
||||
#ifdef _WIN32
|
||||
Require(_putenv_s("APS5_TEXTURE_CACHE_MIB", "32") == 0, "cannot set the test cache budget");
|
||||
Require(_putenv_s("APS5_NO_RESOURCE_CACHE", "1") == 0, "cannot disable the resource cache for the retained-batch test");
|
||||
#else
|
||||
Require(setenv("APS5_TEXTURE_CACHE_MIB", "32", 1) == 0, "cannot set the test cache budget");
|
||||
Require(setenv("APS5_NO_RESOURCE_CACHE", "1", 1) == 0, "cannot disable the resource cache for the retained-batch test");
|
||||
#endif
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
std::vector<std::uint32_t> storage((SurfaceBytes + MaxSurfaces * Stride + Stride) / 4u, 0u);
|
||||
auto* texels = reinterpret_cast<std::uint32_t*>((reinterpret_cast<std::uintptr_t>(storage.data()) + Stride - 1u) & ~std::uintptr_t{Stride - 1u});
|
||||
const auto base = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(texels));
|
||||
const auto blockBytes = static_cast<std::size_t>(SurfaceBytes + (MaxSurfaces - 1u) * Stride);
|
||||
{
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Add(texels, blockBytes, true, true, true);
|
||||
}
|
||||
const auto release = [&] {
|
||||
device->WaitIdle();
|
||||
AgcDriver::Graphics::ClearCachedTextures(device->Device());
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Remove(texels);
|
||||
};
|
||||
try {
|
||||
Record(*device, base);
|
||||
auto retained = StorageTexture::FindLive(base, SurfaceBytes);
|
||||
Require(retained != nullptr, "surface 0 has no cached storage image");
|
||||
const auto retainedBytes = retained->BudgetBytes();
|
||||
auto* recorder = Recorder::Active();
|
||||
Require(recorder != nullptr && recorder->Recording(), "the surface dispatch was not left in an open batch");
|
||||
const auto beforeEviction = recorder->OpenKeptBytes();
|
||||
const auto capacity = static_cast<std::uint32_t>(TestBudget / retainedBytes);
|
||||
Require(capacity + 1u < MaxSurfaces, "the eviction set exceeds the test storage");
|
||||
retained.reset();
|
||||
for (std::uint32_t surface = 1; surface <= capacity; ++surface) Record(*device, base + surface * Stride);
|
||||
Require(StorageTexture::FindLive(base, SurfaceBytes) == nullptr, "surface 0 remained cached after the storage budget was exceeded");
|
||||
Require(recorder->OpenKeptBytes() >= beforeEviction + retainedBytes, "an evicted storage image held by a batch without a shared resource-cache entry was not charged to that batch");
|
||||
} catch (...) {
|
||||
release();
|
||||
throw;
|
||||
}
|
||||
release();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main() {
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
try {
|
||||
if (argc == 2 && std::string(argv[1]) == "--retained-batch") return RetainedBatch();
|
||||
#ifdef _WIN32
|
||||
Require(_putenv_s("APS5_TEXTURE_CACHE_MIB", "32") == 0, "cannot set the test cache budget");
|
||||
#else
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <condition_variable>
|
||||
@@ -198,7 +199,6 @@ private:
|
||||
std::uint32_t outstanding = 0;
|
||||
std::size_t cursor = 0;
|
||||
std::deque<Item> items;
|
||||
std::mutex waiters;
|
||||
std::condition_variable passed;
|
||||
Label label{};
|
||||
std::uint64_t labelSeen = 0;
|
||||
@@ -221,7 +221,6 @@ public:
|
||||
struct Manager {
|
||||
std::mutex lock;
|
||||
std::condition_variable work;
|
||||
std::condition_variable nodeFreed;
|
||||
bool running = false;
|
||||
std::uint32_t blockedWaiters = 0;
|
||||
std::uint32_t freeNodes = 0;
|
||||
@@ -251,7 +250,6 @@ bool TakeNode(Manager& manager) {
|
||||
|
||||
void ReturnNodes(Manager& manager, std::uint32_t count) {
|
||||
manager.freeNodes += count;
|
||||
manager.nodeFreed.notify_all();
|
||||
}
|
||||
|
||||
int Sequence::Latch(int code) {
|
||||
@@ -303,8 +301,8 @@ void Sequence::Advance() {
|
||||
continue;
|
||||
}
|
||||
bool* const waiter = item.passed;
|
||||
const auto retired = static_cast<std::uint32_t>(cursor) + 1;
|
||||
items.erase(items.begin(), items.begin() + static_cast<std::ptrdiff_t>(retired));
|
||||
const auto retired = static_cast<std::uint32_t>(cursor);
|
||||
items.erase(items.begin(), items.begin() + static_cast<std::ptrdiff_t>(retired + 1));
|
||||
cursor = 0;
|
||||
commands.clear();
|
||||
batchStarted = false;
|
||||
@@ -439,7 +437,8 @@ int Sequence::Wait() {
|
||||
int Sequence::WaitAndClose(std::uint32_t index) {
|
||||
std::unique_lock guard(manager->lock);
|
||||
const int result = WaitLocked(guard);
|
||||
ReturnNodes(*manager, static_cast<std::uint32_t>(items.size()) + 1);
|
||||
const auto remaining = static_cast<std::uint32_t>(std::count_if(items.begin(), items.end(), [](const Item& item) { return item.type != ItemType::Wait; }));
|
||||
ReturnNodes(*manager, remaining + 1);
|
||||
items.clear();
|
||||
commands.clear();
|
||||
cursor = 0;
|
||||
@@ -457,13 +456,7 @@ int Sequence::WaitLocked(std::unique_lock<std::mutex>& guard) {
|
||||
} else if (submitted.waits++ >= limits.waits) {
|
||||
result = Latch(SequenceLimitExceeded);
|
||||
}
|
||||
if (manager->freeNodes == 0 && result == 0) result = Latch(SequenceNoNodes);
|
||||
++manager->blockedWaiters;
|
||||
manager->nodeFreed.wait(guard, [this] { return manager->freeNodes != 0; });
|
||||
--manager->freeNodes;
|
||||
guard.unlock();
|
||||
const std::lock_guard serial(waiters);
|
||||
guard.lock();
|
||||
bool done = false;
|
||||
items.push_back({ItemType::Wait, this, nullptr, nullptr, 0, 0, 0, &done});
|
||||
Advance();
|
||||
@@ -700,10 +693,16 @@ void APS5_VABI _ZN3sce3Job10JobManagerC2Ev(void* self) {
|
||||
Implementation(self) = nullptr;
|
||||
}
|
||||
|
||||
void APS5_VABI _ZN3sce3Job10JobManagerD1Ev(void*) {
|
||||
int APS5_VABI _ZN3sce3Job10JobManager8shutdownEv(void* self);
|
||||
|
||||
void APS5_VABI _ZN3sce3Job10JobManagerD1Ev(void* self) {
|
||||
if (Implementation(self) != nullptr) {
|
||||
static_cast<void>(_ZN3sce3Job10JobManager8shutdownEv(self));
|
||||
}
|
||||
}
|
||||
|
||||
void APS5_VABI _ZN3sce3Job10JobManagerD2Ev(void*) {
|
||||
void APS5_VABI _ZN3sce3Job10JobManagerD2Ev(void* self) {
|
||||
_ZN3sce3Job10JobManagerD1Ev(self);
|
||||
}
|
||||
|
||||
std::uint64_t APS5_VABI _ZN3sce3Job10JobManager27calculateRequiredMemorySizeEPKNS1_21MemorySizeQueryParamsE(const MemorySizeQueryParams* params) {
|
||||
|
||||
@@ -30,7 +30,7 @@ if(BUILD_TESTING)
|
||||
target_include_directories(guest_socket_host_write_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_socket_host_write_tests PRIVATE libSceNet libkernel libc)
|
||||
configure_windows_unwind(guest_socket_host_write_tests)
|
||||
foreach(api IN ITEMS recv recvfrom read sceKernelRead sceNetRecv sceNetRecvfrom)
|
||||
foreach(api IN ITEMS recv recvfrom read sceKernelRead sceNetRecv sceNetRecvfrom sceNetRecvmsg)
|
||||
add_test(NAME guest_socket_host_write_${api}_udp COMMAND guest_socket_host_write_tests ${api})
|
||||
add_test(NAME guest_socket_host_write_${api}_tcp COMMAND guest_socket_host_write_tests ${api} tcp)
|
||||
set_tests_properties(guest_socket_host_write_${api}_udp guest_socket_host_write_${api}_tcp
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include <cstdio>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
@@ -36,6 +37,7 @@
|
||||
#include "prx/libc/include/General.hpp"
|
||||
#include "prx/libc/include/GuestArena.hpp"
|
||||
#include "prx/libkernel/Socket/include/SocketPoll.hpp"
|
||||
#include "prx/libkernel/Socket/include/SocketRuntime.hpp"
|
||||
|
||||
// Guest socket descriptors retain PS5 semantics while their transport is backed by host sockets.
|
||||
|
||||
@@ -763,14 +765,23 @@ int APS5_VABI sceNetSocket(const char* name, int family, int type, int protocol)
|
||||
return fd;
|
||||
}
|
||||
|
||||
extern "C" int APS5_VABI close_nid_postfix(int d);
|
||||
|
||||
int APS5_VABI sceNetSocketClose(int s) {
|
||||
std::shared_ptr<NativeSocketHandle> native;
|
||||
bool found = false;
|
||||
{
|
||||
std::lock_guard lk(g_mutex);
|
||||
const auto it = g_socks.find(s);
|
||||
if (it == g_socks.end()) return fail(NET_EBADF);
|
||||
native = it->second.native;
|
||||
g_socks.erase(it);
|
||||
if (it != g_socks.end()) {
|
||||
native = it->second.native;
|
||||
g_socks.erase(it);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
if (!GuestSocketIsOpen_nid_no_patch(s)) return fail(NET_EBADF);
|
||||
return close_nid_postfix(s) == 0 ? 0 : fail(*__error_nid_postfix());
|
||||
}
|
||||
KernelReleaseSocketNumber_nid_no_patch(s);
|
||||
if (native) {
|
||||
@@ -1089,6 +1100,14 @@ int64_t APS5_VABI sceNetRecvmsg(int s, NetMsghdr* msg, int flags) {
|
||||
if (it == g_socks.end()) return fail(NET_EBADF);
|
||||
socket = it->second;
|
||||
}
|
||||
const GuestArena::HostWrite messageOutput(msg, sizeof(*msg));
|
||||
if (!messageOutput.Open()) return fail(NET_EFAULT);
|
||||
std::deque<GuestArena::HostWrite> destinations;
|
||||
for (int i = 0; i < msg->iov_length; ++i) {
|
||||
const auto& entry = msg->iov[i];
|
||||
if (entry.length == 0) continue;
|
||||
if (!destinations.emplace_back(entry.base, static_cast<std::size_t>(entry.length)).Open()) return fail(NET_EFAULT);
|
||||
}
|
||||
const bool datagram = socket.type != NET_SOCK_STREAM;
|
||||
#ifdef _WIN32
|
||||
if (const int wait = wait_for_local_address(s, socket, total == 0)) {
|
||||
@@ -1152,13 +1171,22 @@ int APS5_VABI sceNetShutdown(int s, int how) {
|
||||
return error == 0 ? 0 : fail(error);
|
||||
}
|
||||
|
||||
extern "C" int APS5_VABI setsockopt_nid_postfix(int descriptor, int level, int option, const void* value, std::uint32_t length);
|
||||
|
||||
int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval, uint32_t optlen) {
|
||||
Sock socket;
|
||||
bool found = false;
|
||||
{
|
||||
std::lock_guard lk(g_mutex);
|
||||
auto it = g_socks.find(s);
|
||||
if (it == g_socks.end()) return fail(NET_EBADF);
|
||||
socket = it->second;
|
||||
if (it != g_socks.end()) {
|
||||
socket = it->second;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
if (!GuestSocketIsOpen_nid_no_patch(s)) return fail(NET_EBADF);
|
||||
return setsockopt_nid_postfix(s, level, optname, optval, optlen) == 0 ? 0 : fail(*__error_nid_postfix());
|
||||
}
|
||||
if (level != NET_SOL_SOCKET) return fail(NET_EOPNOTSUPP);
|
||||
if (optname == NET_SO_RCVTIMEO || optname == NET_SO_SNDTIMEO) {
|
||||
|
||||
@@ -38,6 +38,8 @@ static constexpr int SCE_NET_CTL_ERROR_INVALID_ADDR = static_cast<int>(0x8041210
|
||||
static constexpr int SCE_NET_CTL_ERROR_NOT_CONNECTED = static_cast<int>(0x80412108);
|
||||
static constexpr int NET_CTL_STATE_DISCONNECTED = 0;
|
||||
static constexpr int NET_CTL_STATE_IPOBTAINED = 3;
|
||||
static constexpr int NET_CTL_EVENT_DISCONNECTED = 1;
|
||||
static constexpr int NET_CTL_EVENT_IPOBTAINED = 3;
|
||||
static constexpr int NET_CTL_INFO_DEVICE = 1;
|
||||
static constexpr int NET_CTL_INFO_ETHER_ADDR = 2;
|
||||
static constexpr int NET_CTL_INFO_MTU = 3;
|
||||
@@ -61,16 +63,22 @@ static constexpr std::uint8_t NET_CTL_ETHER_ADDR[6] = {0x02, 0x50, 0x53, 0x35, 0
|
||||
static constexpr std::size_t NET_CTL_IPV4_ADDR_STR_LEN = 16;
|
||||
static constexpr int MAX_CALLBACKS = 8;
|
||||
|
||||
static std::mutex g_callbackLock;
|
||||
static NetCtlCallback g_callbacks[MAX_CALLBACKS] = {};
|
||||
static NetCtlCallback g_callbacksV6[MAX_CALLBACKS] = {};
|
||||
struct CallbackSlot {
|
||||
NetCtlCallback func = nullptr;
|
||||
void* arg = nullptr;
|
||||
int lastEvent = 0;
|
||||
};
|
||||
|
||||
static int RegisterCallback(NetCtlCallback (&callbacks)[MAX_CALLBACKS], NetCtlCallback func, int* cid) {
|
||||
static std::mutex g_callbackLock;
|
||||
static CallbackSlot g_callbacks[MAX_CALLBACKS] = {};
|
||||
static CallbackSlot g_callbacksV6[MAX_CALLBACKS] = {};
|
||||
|
||||
static int RegisterCallback(CallbackSlot (&callbacks)[MAX_CALLBACKS], NetCtlCallback func, void* arg, int* cid) {
|
||||
if (!func || !cid) return SCE_NET_CTL_ERROR_INVALID_ADDR;
|
||||
std::lock_guard lock(g_callbackLock);
|
||||
for (int index = 0; index < MAX_CALLBACKS; ++index) {
|
||||
if (!callbacks[index]) {
|
||||
callbacks[index] = func;
|
||||
if (!callbacks[index].func) {
|
||||
callbacks[index] = {func, arg, 0};
|
||||
*cid = index;
|
||||
return 0;
|
||||
}
|
||||
@@ -78,13 +86,21 @@ static int RegisterCallback(NetCtlCallback (&callbacks)[MAX_CALLBACKS], NetCtlCa
|
||||
return SCE_NET_CTL_ERROR_CALLBACK_MAX;
|
||||
}
|
||||
|
||||
static int UnregisterCallback(NetCtlCallback (&callbacks)[MAX_CALLBACKS], int cid) {
|
||||
static int UnregisterCallback(CallbackSlot (&callbacks)[MAX_CALLBACKS], int cid) {
|
||||
if (cid < 0 || cid >= MAX_CALLBACKS) return SCE_NET_CTL_ERROR_INVALID_ID;
|
||||
std::lock_guard lock(g_callbackLock);
|
||||
callbacks[cid] = nullptr;
|
||||
callbacks[cid] = {};
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool HasIpv4Callback() {
|
||||
std::lock_guard lock(g_callbackLock);
|
||||
for (const auto& slot : g_callbacks) {
|
||||
if (slot.func) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool HostHasAddress(int family) {
|
||||
#ifdef _WIN32
|
||||
static const int socket_status = [] {
|
||||
@@ -324,6 +340,19 @@ static void HostTraffic(std::uint64_t& sent, std::uint64_t& received) {
|
||||
extern "C" {
|
||||
|
||||
int APS5_VABI sceNetCtlCheckCallback(void) {
|
||||
if (!HasIpv4Callback()) return 0;
|
||||
const int event = HostHasAddress(AF_UNSPEC) ? NET_CTL_EVENT_IPOBTAINED : NET_CTL_EVENT_DISCONNECTED;
|
||||
CallbackSlot due[MAX_CALLBACKS];
|
||||
int count = 0;
|
||||
{
|
||||
std::lock_guard lock(g_callbackLock);
|
||||
for (auto& slot : g_callbacks) {
|
||||
if (!slot.func || slot.lastEvent == event) continue;
|
||||
slot.lastEvent = event;
|
||||
due[count++] = slot;
|
||||
}
|
||||
}
|
||||
for (int index = 0; index < count; ++index) due[index].func(event, due[index].arg);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -390,14 +419,17 @@ int APS5_VABI sceNetCtlInit(void) {
|
||||
}
|
||||
|
||||
int APS5_VABI sceNetCtlRegisterCallback(NetCtlCallback func, void* arg, int* cid) {
|
||||
return RegisterCallback(g_callbacks, func, cid);
|
||||
return RegisterCallback(g_callbacks, func, arg, cid);
|
||||
}
|
||||
|
||||
int APS5_VABI sceNetCtlRegisterCallbackV6(NetCtlCallback func, void* arg, int* cid) {
|
||||
return RegisterCallback(g_callbacksV6, func, cid);
|
||||
return RegisterCallback(g_callbacksV6, func, arg, cid);
|
||||
}
|
||||
|
||||
void APS5_VABI sceNetCtlTerm(void) {
|
||||
std::lock_guard lock(g_callbackLock);
|
||||
for (auto& slot : g_callbacks) slot = {};
|
||||
for (auto& slot : g_callbacksV6) slot = {};
|
||||
}
|
||||
|
||||
int APS5_VABI sceNetCtlUnregisterCallback(int cid) {
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#include <cstddef>
|
||||
#include "SceTypes.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
#include "prx/libSceUserService/UserService.hpp"
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <unordered_set>
|
||||
@@ -13,6 +15,7 @@ static constexpr int SCE_NP_ERROR_USER_NOT_FOUND = static_cast<int>(0x80550007);
|
||||
static constexpr int SCE_NP_ERROR_CALLBACK_ALREADY_REGISTERED = static_cast<int>(0x80550008);
|
||||
static constexpr int SCE_NP_ERROR_CALLBACK_NOT_REGISTERED = static_cast<int>(0x80550009);
|
||||
static constexpr int SCE_NP_ERROR_REQUEST_NOT_FOUND = static_cast<int>(0x80550014);
|
||||
static constexpr int SCE_NP_ERROR_CALLBACK_MAX = static_cast<int>(0x8055001D);
|
||||
static constexpr uint32_t NP_STATE_SIGNED_OUT = 1;
|
||||
static constexpr int NP_POLL_ASYNC_FINISHED = 0;
|
||||
static constexpr uint32_t NP_REACHABILITY_STATE_UNAVAILABLE = 0;
|
||||
@@ -32,6 +35,27 @@ int AddRequest() {
|
||||
liveRequests.insert(reqId);
|
||||
return reqId;
|
||||
}
|
||||
|
||||
using NpStateCallbackA = void (APS5_VABI *)(int userId, uint32_t state, void* userdata);
|
||||
using NpStateCallback = void (APS5_VABI *)(int userId, uint32_t state, NpId* npId, void* userdata);
|
||||
|
||||
constexpr int NP_STATE_CALLBACK_MAX = 8;
|
||||
|
||||
struct StateCallbackA {
|
||||
NpStateCallbackA callback = nullptr;
|
||||
void* userdata = nullptr;
|
||||
bool pending = false;
|
||||
};
|
||||
|
||||
struct StateCallback {
|
||||
NpStateCallback callback = nullptr;
|
||||
void* userdata = nullptr;
|
||||
bool pending = false;
|
||||
};
|
||||
|
||||
std::mutex stateMutex;
|
||||
std::array<StateCallbackA, NP_STATE_CALLBACK_MAX> stateCallbacksA;
|
||||
StateCallback stateCallback;
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
@@ -42,6 +66,19 @@ int APS5_VABI sceNpAbortRequest(int req_id) {
|
||||
}
|
||||
|
||||
int APS5_VABI sceNpCheckCallback(void) {
|
||||
std::array<StateCallbackA, NP_STATE_CALLBACK_MAX> callbacksA;
|
||||
StateCallback legacy;
|
||||
{
|
||||
std::lock_guard lock(stateMutex);
|
||||
callbacksA = stateCallbacksA;
|
||||
legacy = stateCallback;
|
||||
for (auto& entry : stateCallbacksA) entry.pending = false;
|
||||
stateCallback.pending = false;
|
||||
}
|
||||
if (legacy.pending) legacy.callback(USER_SERVICE_INITIAL_USER_ID, NP_STATE_SIGNED_OUT, nullptr, legacy.userdata);
|
||||
for (const auto& entry : callbacksA) {
|
||||
if (entry.pending) entry.callback(USER_SERVICE_INITIAL_USER_ID, NP_STATE_SIGNED_OUT, entry.userdata);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -172,9 +209,11 @@ int APS5_VABI sceNpRegisterPremiumEventCallback(void* callback, void* userdata)
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceNpRegisterStateCallback(void* callback, void* userdata) {
|
||||
(void)userdata;
|
||||
int APS5_VABI sceNpRegisterStateCallback(NpStateCallback callback, void* userdata) {
|
||||
if (!callback) return SCE_NP_ERROR_INVALID_ARGUMENT;
|
||||
std::lock_guard lock(stateMutex);
|
||||
if (stateCallback.callback) return SCE_NP_ERROR_CALLBACK_ALREADY_REGISTERED;
|
||||
stateCallback = {callback, userdata, true};
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -208,6 +247,9 @@ int APS5_VABI sceNpSetTimeout(int reqId, int32_t resolveRetry, uint32_t resolveT
|
||||
}
|
||||
|
||||
int APS5_VABI sceNpUnregisterStateCallback(void) {
|
||||
std::lock_guard lock(stateMutex);
|
||||
if (!stateCallback.callback) return SCE_NP_ERROR_CALLBACK_NOT_REGISTERED;
|
||||
stateCallback = {};
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -223,14 +265,26 @@ int APS5_VABI sceNpNotifyPremiumFeature(const void* param) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceNpRegisterStateCallbackA(void* callback, void* userdata) {
|
||||
(void)userdata;
|
||||
int APS5_VABI sceNpRegisterStateCallbackA(NpStateCallbackA callback, void* userdata) {
|
||||
if (!callback) return SCE_NP_ERROR_INVALID_ARGUMENT;
|
||||
return 1;
|
||||
std::lock_guard lock(stateMutex);
|
||||
for (const auto& entry : stateCallbacksA) {
|
||||
if (entry.callback == callback) return SCE_NP_ERROR_CALLBACK_ALREADY_REGISTERED;
|
||||
}
|
||||
for (int index = 0; index < NP_STATE_CALLBACK_MAX; ++index) {
|
||||
if (stateCallbacksA[index].callback) continue;
|
||||
stateCallbacksA[index] = {callback, userdata, true};
|
||||
return index + 1;
|
||||
}
|
||||
return SCE_NP_ERROR_CALLBACK_MAX;
|
||||
}
|
||||
|
||||
int APS5_VABI sceNpUnregisterStateCallbackA(int callback_id) {
|
||||
(void)callback_id;
|
||||
int APS5_VABI sceNpUnregisterStateCallbackA(int callbackId) {
|
||||
if (callbackId < 1 || callbackId > NP_STATE_CALLBACK_MAX) return SCE_NP_ERROR_INVALID_ARGUMENT;
|
||||
std::lock_guard lock(stateMutex);
|
||||
auto& entry = stateCallbacksA[callbackId - 1];
|
||||
if (!entry.callback) return SCE_NP_ERROR_CALLBACK_NOT_REGISTERED;
|
||||
entry = {};
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ struct InputConfiguration {
|
||||
|
||||
inline constexpr std::array InputMapping{
|
||||
InputBinding{SDL_SCANCODE_F11, MouseButton::None, InputControl::ToggleFullscreen},
|
||||
InputBinding{SDL_SCANCODE_F10, MouseButton::None, InputControl::TogglePerformanceOverlay},
|
||||
InputBinding{SDL_SCANCODE_RETURN, MouseButton::None, InputControl::Button, PadButton::Cross},
|
||||
InputBinding{SDL_SCANCODE_SPACE, MouseButton::None, InputControl::Button, PadButton::Cross},
|
||||
InputBinding{SDL_SCANCODE_ESCAPE, MouseButton::None, InputControl::Button, PadButton::Options},
|
||||
|
||||
@@ -36,6 +36,7 @@ private:
|
||||
|
||||
struct InputRequests {
|
||||
bool toggleFullscreen = false;
|
||||
bool togglePerformanceOverlay = false;
|
||||
bool toggleMouse = false;
|
||||
bool releaseMouse = false;
|
||||
};
|
||||
|
||||
@@ -20,7 +20,8 @@ enum class InputControl {
|
||||
TouchLeft,
|
||||
TouchRight,
|
||||
ToggleMouse,
|
||||
ToggleFullscreen
|
||||
ToggleFullscreen,
|
||||
TogglePerformanceOverlay
|
||||
};
|
||||
|
||||
enum class PadButton : std::uint32_t {
|
||||
|
||||
@@ -71,6 +71,16 @@ if(BUILD_TESTING)
|
||||
set_tests_properties(guest_video_out_high_frame_rate PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test_executable(guest_video_out_cursor_tests ${LIBS_INCLUDE_DIR}/tests/GuestVideoOutCursor.cpp)
|
||||
target_include_directories(guest_video_out_cursor_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_video_out_cursor_tests PRIVATE libSceVideoOut libkernel libc)
|
||||
configure_windows_unwind(guest_video_out_cursor_tests)
|
||||
add_test(NAME guest_video_out_cursor COMMAND guest_video_out_cursor_tests)
|
||||
set_tests_properties(guest_video_out_cursor PROPERTIES TIMEOUT 20)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_video_out_cursor PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
set(ANYPS5_VIDEO_OUT_TEST_DRIVER "offscreen" CACHE STRING
|
||||
"SDL video driver for VideoOut flip tests")
|
||||
|
||||
|
||||
@@ -17,6 +17,26 @@ int APS5_VABI sceVideoOutClearFlipMaster() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceVideoOutCursorEnable(int32_t, int32_t, const void*) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceVideoOutCursorDisable(int32_t, int32_t) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceVideoOutCursorSetImageAddress(int32_t, int32_t, const void*) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceVideoOutCursorSetPosition(int32_t, int32_t, uint32_t, uint32_t) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceVideoOutAdjustColor_(int handle, const VideoOutColorSettings* settings, uint32_t settings_size) {
|
||||
if (settings_size < sizeof(VideoOutColorSettings)) throw std::runtime_error("sceVideoOutAdjustColor_: VIDEO_OUT_ERROR_INVALID_VALUE");
|
||||
return sceVideoOutAdjustColor(handle, settings);
|
||||
|
||||
@@ -52,7 +52,8 @@ constexpr auto actions = std::array{
|
||||
InputAction{"TouchLeft", Pad::InputControl::TouchLeft, Pad::PadButton::None},
|
||||
InputAction{"TouchRight", Pad::InputControl::TouchRight, Pad::PadButton::None},
|
||||
InputAction{"ToggleMouse", Pad::InputControl::ToggleMouse, Pad::PadButton::None},
|
||||
InputAction{"ToggleFullscreen", Pad::InputControl::ToggleFullscreen, Pad::PadButton::None}
|
||||
InputAction{"ToggleFullscreen", Pad::InputControl::ToggleFullscreen, Pad::PadButton::None},
|
||||
InputAction{"TogglePerformanceOverlay", Pad::InputControl::TogglePerformanceOverlay, Pad::PadButton::None}
|
||||
};
|
||||
|
||||
std::string upper(std::string_view value) {
|
||||
@@ -111,6 +112,9 @@ Pad::InputBinding parseBinding(const InputAction& action, std::string_view sourc
|
||||
if (action.control == Pad::InputControl::ToggleFullscreen) {
|
||||
throw std::runtime_error("ToggleFullscreen can only use a keyboard key");
|
||||
}
|
||||
if (action.control == Pad::InputControl::TogglePerformanceOverlay) {
|
||||
throw std::runtime_error("TogglePerformanceOverlay can only use a keyboard key");
|
||||
}
|
||||
return {SDL_SCANCODE_UNKNOWN, parseMouseButton(value), action.control, action.button, 0};
|
||||
}
|
||||
if (type == "WHEEL") {
|
||||
|
||||
@@ -152,6 +152,7 @@ Pad::InputRequests Pad::KeyboardMouse::HandleEvent(const SDL_Event& event, Clock
|
||||
: binding.mouseButton != MouseButton::None && binding.mouseButton == static_cast<MouseButton>(event.button.button);
|
||||
if (!matches) continue;
|
||||
if (binding.control == InputControl::ToggleFullscreen && keyboard && down && !pressed[index]) requests.toggleFullscreen = true;
|
||||
if (binding.control == InputControl::TogglePerformanceOverlay && keyboard && down && !pressed[index]) requests.togglePerformanceOverlay = true;
|
||||
if (binding.control == InputControl::ToggleMouse && down && !pressed[index]) requests.toggleMouse = true;
|
||||
pressed[index] = down;
|
||||
}
|
||||
@@ -183,6 +184,7 @@ PadInputState Pad::KeyboardMouse::Compose(const PadInputState& controller) const
|
||||
case InputControl::TouchRight: state.touchRight = true; break;
|
||||
case InputControl::ToggleMouse: break;
|
||||
case InputControl::ToggleFullscreen: break;
|
||||
case InputControl::TogglePerformanceOverlay: break;
|
||||
}
|
||||
}
|
||||
for (std::size_t axis = 0; axis < state.sticks.size(); ++axis) {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "prx/libSceVideoOut/include/PadInput.hpp"
|
||||
#include "prx/libSceMouse/include/MouseState.hpp"
|
||||
#include "prx/libSceVideoOut/include/DisplayWindow.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Presentation.hpp"
|
||||
#include "prx/libScePad/include/PadState.hpp"
|
||||
#include "prx/libScePad/include/PadInputTypes.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
@@ -228,6 +229,7 @@ void PadInput::HandleEvent(const SDL_Event& event, DisplayWindow& window) {
|
||||
keyboardMouse.SetTitleReadsMouse(MouseIsOpen_nid_no_patch());
|
||||
const auto requests = keyboardMouse.HandleEvent(event, std::chrono::steady_clock::now());
|
||||
if (requests.toggleFullscreen && window.Handle() != nullptr && event.key.windowID == SDL_GetWindowID(window.Handle())) window.ToggleFullscreen();
|
||||
if (requests.togglePerformanceOverlay && window.Handle() != nullptr && event.key.windowID == SDL_GetWindowID(window.Handle())) AgcDriverTogglePerformanceOverlay_nid_postfix();
|
||||
if (requests.toggleMouse) setMouseMode(!keyboardMouse.MouseMode());
|
||||
if (event.type != SDL_MOUSEMOTION) publish();
|
||||
}
|
||||
|
||||
@@ -681,7 +681,7 @@ void testReleaseVblank() {
|
||||
const bool done = cfg->vblankCond.wait_for(lock, std::chrono::seconds(15), [&] { return cfg->failure || cfg->flipStatus.count == 1; });
|
||||
if (cfg->failure) std::rethrow_exception(cfg->failure);
|
||||
check(done, "the flip did not complete");
|
||||
check(cfg->lastFlipVblank >= 3 && cfg->vblankStatus.count >= cfg->lastFlipVblank + 3, "the next flip is not paced from the vblank this flip was released at");
|
||||
check(cfg->lastFlipVblank >= 3, "the first flip was released before its configured interval");
|
||||
}
|
||||
sceVideoOutUnregisterBuffers(handle, 0);
|
||||
sceVideoOutClose(handle);
|
||||
|
||||
@@ -75,6 +75,23 @@ void settingsReject() {
|
||||
require(rejects("MouseSensitivity = 1\nMouseSensitivity = 2\n"), "a setting given twice is rejected");
|
||||
}
|
||||
|
||||
void performanceOverlayBinding() {
|
||||
useConfig("Cross = KEY:F\n");
|
||||
bool onF10 = false;
|
||||
for (const auto& binding : Pad::LoadInputMapping().bindings) onF10 = onF10 || (binding.control == Pad::InputControl::TogglePerformanceOverlay && binding.key == SDL_SCANCODE_F10);
|
||||
require(onF10, "F10 toggles the performance overlay by default");
|
||||
useConfig("togglePerformanceOverlay = KEY:F9\n");
|
||||
bool onF9 = false;
|
||||
bool stillOnF10 = false;
|
||||
for (const auto& binding : Pad::LoadInputMapping().bindings) {
|
||||
if (binding.control != Pad::InputControl::TogglePerformanceOverlay) continue;
|
||||
onF9 = onF9 || binding.key == SDL_SCANCODE_F9;
|
||||
stillOnF10 = stillOnF10 || binding.key == SDL_SCANCODE_F10;
|
||||
}
|
||||
require(onF9 && !stillOnF10, "the performance overlay toggle moves to another key");
|
||||
require(rejects("TogglePerformanceOverlay = MOUSE:Left\n"), "the performance overlay toggle refuses a mouse button");
|
||||
}
|
||||
|
||||
void mouseLookDefault() {
|
||||
useConfig("Cross = KEY:F\n");
|
||||
const auto settings = Pad::LoadInputMapping().settings;
|
||||
@@ -180,6 +197,7 @@ int main() try {
|
||||
settingsDefault();
|
||||
settingsParse();
|
||||
settingsReject();
|
||||
performanceOverlayBinding();
|
||||
mouseLookDefault();
|
||||
mouseLookParse();
|
||||
mouseLookReject();
|
||||
|
||||
@@ -284,6 +284,9 @@ void testToggleRequests() {
|
||||
check(keyboardMouse.HandleEvent(PadSimulation::ButtonEvent(SDL_BUTTON_MIDDLE, true), now).toggleMouse, "middle click asks to toggle the mouse");
|
||||
check(!keyboardMouse.HandleEvent(PadSimulation::ButtonEvent(SDL_BUTTON_MIDDLE, false), now).toggleMouse, "releasing it asks for nothing");
|
||||
check(keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_F11, true), now).toggleFullscreen, "F11 asks to toggle fullscreen");
|
||||
const auto overlay = keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_F10, true), now);
|
||||
check(overlay.togglePerformanceOverlay && !overlay.toggleFullscreen, "F10 asks to toggle the performance overlay");
|
||||
check(!keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_F10, false), now).togglePerformanceOverlay, "releasing F10 asks for nothing");
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -343,6 +343,16 @@ if(BUILD_TESTING)
|
||||
set_tests_properties(guest_path_case PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test_executable(guest_path_drive_component_tests ${LIBS_INCLUDE_DIR}/tests/GuestPathDriveComponent.cpp)
|
||||
target_include_directories(guest_path_drive_component_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_path_drive_component_tests PRIVATE libc)
|
||||
configure_windows_unwind(guest_path_drive_component_tests)
|
||||
add_test(NAME guest_path_drive_component COMMAND guest_path_drive_component_tests)
|
||||
set_tests_properties(guest_path_drive_component PROPERTIES TIMEOUT 20)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_path_drive_component PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test_executable(guest_directory_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectory.cpp)
|
||||
target_include_directories(guest_directory_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_directory_tests PRIVATE libc)
|
||||
@@ -463,8 +473,9 @@ if(BUILD_TESTING)
|
||||
configure_windows_unwind(guest_standard_stream_tests)
|
||||
add_test(NAME guest_standard_streams COMMAND guest_standard_stream_tests)
|
||||
add_test(NAME guest_standard_streams_closed_stdout COMMAND guest_standard_stream_tests --closed-stdout)
|
||||
add_test(NAME guest_standard_streams_exclusive_stdout COMMAND guest_standard_stream_tests --exclusive-stdout)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_standard_streams guest_standard_streams_closed_stdout PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
set_tests_properties(guest_standard_streams guest_standard_streams_closed_stdout guest_standard_streams_exclusive_stdout PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test_executable(file_streams_tests ${LIBS_INCLUDE_DIR}/tests/FileStreams.cpp)
|
||||
@@ -771,6 +782,18 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(guest_libc_cnd_init_default_name_tests EXCLUDE_FROM_ALL ${LIBS_INCLUDE_DIR}/tests/GuestLibcCndInitDefaultName.cpp)
|
||||
target_include_directories(guest_libc_cnd_init_default_name_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_libc_cnd_init_default_name_tests PRIVATE libc)
|
||||
configure_windows_unwind(guest_libc_cnd_init_default_name_tests)
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(guest_libc_cnd_init_default_name_tests PROPERTIES EXCLUDE_FROM_ALL FALSE)
|
||||
add_test(NAME guest_libc_cnd_init_default_name COMMAND guest_libc_cnd_init_default_name_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_libc_cnd_init_default_name PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(guest_libc_cnd_timed_wait_tests EXCLUDE_FROM_ALL ${LIBS_INCLUDE_DIR}/tests/GuestLibcCndTimedWait.cpp)
|
||||
target_include_directories(guest_libc_cnd_timed_wait_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_libc_cnd_timed_wait_tests PRIVATE libc)
|
||||
@@ -837,5 +860,14 @@ if(MINGW)
|
||||
ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>"
|
||||
PASS_REGULAR_EXPRESSION "kept failure report ran[^~]*aborting[^~]*SIGABRT. on thread[^~]*all threads:"
|
||||
FAIL_REGULAR_EXPRESSION "kept failure report ran[^~]*kept failure report ran")
|
||||
add_test(NAME windows_crash_stack_words COMMAND windows_backtrace_report_tests fault)
|
||||
set_tests_properties(windows_crash_stack_words PROPERTIES TIMEOUT 20
|
||||
ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>;APS5_CRASH_STACK_BYTES=set:2048"
|
||||
PASS_REGULAR_EXPRESSION "FATAL: unhandled exception 0xc0000005[^~]*stack words from rsp[^~]*data at stack words that point outside the stack[^~]*c0ffee0000000000 c0ffee0000000001 c0ffee0000000002 c0ffee0000000003 c0ffee0000000004 c0ffee0000000005 c0ffee0000000006 c0ffee0000000007")
|
||||
add_test(NAME windows_crash_without_stack_words COMMAND windows_backtrace_report_tests fault)
|
||||
set_tests_properties(windows_crash_without_stack_words PROPERTIES TIMEOUT 20
|
||||
ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>;APS5_CRASH_STACK_BYTES=unset:"
|
||||
PASS_REGULAR_EXPRESSION "FATAL: unhandled exception 0xc0000005"
|
||||
FAIL_REGULAR_EXPRESSION "stack words from rsp;data at stack words")
|
||||
endif()
|
||||
endif()
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_FILESTREAM_HPP
|
||||
#define CORE_LIBS_PRX_LIBC_INCLUDE_FILESTREAM_HPP
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <stdexcept>
|
||||
@@ -15,7 +16,10 @@
|
||||
#include "prx/libc/include/FileLock.hpp"
|
||||
extern "C" void* HostStandardHandle_nid_no_patch(int descriptor);
|
||||
extern "C" void KeepStandardHandle_nid_no_patch(int descriptor, void* handle);
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
extern "C" int DuplicateHostDescriptor_nid_no_patch(int descriptor);
|
||||
|
||||
struct GuestFilePrefix {
|
||||
unsigned char* position = nullptr;
|
||||
@@ -80,6 +84,31 @@ public:
|
||||
}
|
||||
|
||||
GuestFilePrefix& GuestState() { return _guest; }
|
||||
bool IsHostStandard() const {
|
||||
return _handle == stdin || _handle == stdout || _handle == stderr;
|
||||
}
|
||||
|
||||
void Detach() {
|
||||
_handle = nullptr;
|
||||
_guest.flags = 0;
|
||||
_guest.descriptor = -1;
|
||||
}
|
||||
|
||||
void RestoreHostStandard(std::FILE* stream, int kept) {
|
||||
const int error = errno;
|
||||
#ifdef _WIN32
|
||||
const bool restored = _handle || (::_wfreopen(L"NUL", stream == stdin ? L"r" : L"w", stream) && ::_dup2(kept, ::_fileno(stream)) == 0);
|
||||
::_unlock_file(stream);
|
||||
::_close(kept);
|
||||
#else
|
||||
const bool restored = _handle || (std::freopen("/dev/null", stream == stdin ? "r" : "w", stream) && ::dup2(kept, ::fileno(stream)) >= 0);
|
||||
::funlockfile(stream);
|
||||
::close(kept);
|
||||
#endif
|
||||
if (!restored) throw std::runtime_error("FileStream: cannot restore the host standard stream");
|
||||
errno = error;
|
||||
}
|
||||
|
||||
bool Reopen(const std::filesystem::path& filename, const char* mode) {
|
||||
auto* previous = GetHandle();
|
||||
_guest = {};
|
||||
@@ -90,11 +119,17 @@ public:
|
||||
const int replaced = previous ? _fileno(previous) : -1;
|
||||
auto* const standard = HostStandardHandle_nid_no_patch(replaced);
|
||||
GuestFileLockForget_nid_no_patch(replaced);
|
||||
const int kept = IsHostStandard() ? DuplicateHostDescriptor_nid_no_patch(replaced) : -1;
|
||||
if (kept >= 0) ::_lock_file(previous);
|
||||
_handle = ::_wfreopen(filename.c_str(), wideMode.c_str(), previous);
|
||||
if (kept >= 0) RestoreHostStandard(previous, kept);
|
||||
KeepStandardHandle_nid_no_patch(replaced, standard);
|
||||
#else
|
||||
if (::fileno(previous) == 2) KeepHostStandardError_nid_no_patch();
|
||||
const int kept = IsHostStandard() ? DuplicateHostDescriptor_nid_no_patch(::fileno(previous)) : -1;
|
||||
if (kept >= 0) ::flockfile(previous);
|
||||
_handle = std::freopen(filename.c_str(), mode, previous);
|
||||
if (kept >= 0) RestoreHostStandard(previous, kept);
|
||||
#endif
|
||||
if (!_handle) return false;
|
||||
_guest.flags = 0x10;
|
||||
|
||||
@@ -35,6 +35,7 @@ extern "C" void RecordWrittenPath_nid_no_patch(const std::filesystem::path& path
|
||||
extern "C" std::vector<std::filesystem::path> WrittenPaths_nid_no_patch();
|
||||
extern "C" void SyncWrittenPaths_nid_no_patch();
|
||||
extern "C" void BlockPathAlias_nid_no_patch(const char* guestPrefix);
|
||||
extern "C" int DuplicateHostDescriptor_nid_no_patch(int descriptor);
|
||||
#ifdef _WIN32
|
||||
extern "C" int WindowsOpenError_nid_no_patch(unsigned long error);
|
||||
extern "C" void* HostStandardHandle_nid_no_patch(int descriptor);
|
||||
|
||||
@@ -193,11 +193,10 @@ inline std::string FormatPointer(const std::string& spec, const void* pointer) {
|
||||
for (unsigned long long rest = value; rest != 0; rest >>= 4) digits.insert(digits.begin(), "0123456789abcdef"[rest & 15]);
|
||||
if (digits.empty() && !(hasPrecision && precision == 0)) digits = "0";
|
||||
if (digits.size() < precision) digits.insert(0, precision - digits.size(), '0');
|
||||
const std::size_t used = digits.size() + 2;
|
||||
const std::size_t padding = width > used ? width - used : 0;
|
||||
if (left) return "0x" + digits + std::string(padding, ' ');
|
||||
if (zero && !hasPrecision) return "0x" + std::string(padding, '0') + digits;
|
||||
return std::string(padding, ' ') + "0x" + digits;
|
||||
const std::size_t padding = width > digits.size() ? width - digits.size() : 0;
|
||||
if (left) return digits + std::string(padding, ' ');
|
||||
if (zero && !hasPrecision) return std::string(padding, '0') + digits;
|
||||
return std::string(padding, ' ') + digits;
|
||||
}
|
||||
|
||||
constexpr size_t maximumArgumentPosition = 99;
|
||||
@@ -380,7 +379,7 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo
|
||||
}
|
||||
}
|
||||
output.Value(spec + 's', utf8.c_str());
|
||||
} else if (conversion == 'p' && length.empty()) {
|
||||
} else if (conversion == 'p' && (length.empty() || length == "l")) {
|
||||
const std::string text = FormatPointer(spec, args.Next<void*>());
|
||||
output.Append(text.data(), text.size());
|
||||
} else if (conversion == 'm') {
|
||||
|
||||
@@ -22,8 +22,8 @@ struct Archive {
|
||||
};
|
||||
|
||||
Archive& State() {
|
||||
static Archive archive;
|
||||
return archive;
|
||||
static auto* archive = new Archive;
|
||||
return *archive;
|
||||
}
|
||||
|
||||
bool SameComponent(std::string_view left, std::string_view right) {
|
||||
|
||||
@@ -283,7 +283,8 @@ FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode
|
||||
std::filesystem::path path;
|
||||
if (*filename) {
|
||||
if (const int error = ResolvePathError_nid_no_patch(filename, path)) {
|
||||
stream->Close();
|
||||
if (stream->IsHostStandard()) stream->Detach();
|
||||
else stream->Close();
|
||||
if (stream->IsDynamic()) delete stream;
|
||||
errno = error;
|
||||
return nullptr;
|
||||
|
||||
@@ -31,13 +31,14 @@ int __isoc99_vfscanf(std::FILE*, const char*, std::va_list);
|
||||
namespace {
|
||||
|
||||
#ifndef _WIN32
|
||||
bool HasGuestWideFormat(const char* format) {
|
||||
bool RequiresGuestFormatting(const char* format) {
|
||||
if (format == nullptr) return false;
|
||||
while ((format = std::strchr(format, '%')) != nullptr) {
|
||||
++format;
|
||||
if (*format == '%') { ++format; continue; }
|
||||
while (*format && std::strchr("-+ #0'0123456789.*$", *format)) ++format;
|
||||
if (*format == 'S' || *format == 'C' || *format == 'm' || (*format == 'l' && (format[1] == 's' || format[1] == 'c'))) return true;
|
||||
if (*format == 'S' || *format == 'C' || *format == 'm' || *format == 'p' ||
|
||||
(*format == 'l' && (format[1] == 's' || format[1] == 'c' || format[1] == 'p'))) return true;
|
||||
if (*format) ++format;
|
||||
}
|
||||
return false;
|
||||
@@ -222,15 +223,15 @@ int APS5_VABI vasprintf_nid_postfix(char** destination, const char* format, VaLi
|
||||
const char* source = text.c_str();
|
||||
#else
|
||||
std::string guestText;
|
||||
const bool guestWide = HasGuestWideFormat(format);
|
||||
const bool guestFormatting = RequiresGuestFormatting(format);
|
||||
char* text = nullptr;
|
||||
std::va_list copy;
|
||||
va_copy(copy, *reinterpret_cast<std::va_list*>(args));
|
||||
const int count = guestWide ? LibcDetail::FormatWindows(nullptr, 0, format, args, &guestText) : ::vasprintf(&text, format, copy);
|
||||
const int count = guestFormatting ? LibcDetail::FormatWindows(nullptr, 0, format, args, &guestText) : ::vasprintf(&text, format, copy);
|
||||
va_end(copy);
|
||||
std::unique_ptr<char, decltype(&std::free)> owner(text, std::free);
|
||||
if (count < 0) return -1;
|
||||
const char* source = guestWide ? guestText.c_str() : text;
|
||||
const char* source = guestFormatting ? guestText.c_str() : text;
|
||||
#endif
|
||||
auto* output = static_cast<char*>(ApplicationHeapAllocate_nid_no_patch(static_cast<size_t>(count) + 1));
|
||||
if (!output) {
|
||||
@@ -255,7 +256,7 @@ int APS5_VABI vfprintf_nid_postfix(FileStream* stream, const char* format, VaLis
|
||||
const int result = std::fwrite(buffer.data(), 1, static_cast<size_t>(count), native) ==
|
||||
static_cast<size_t>(count) ? count : -1;
|
||||
#else
|
||||
if (HasGuestWideFormat(format)) {
|
||||
if (RequiresGuestFormatting(format)) {
|
||||
std::string buffer;
|
||||
const int count = LibcDetail::FormatWindows(nullptr, 0, format, args, &buffer);
|
||||
const int result = std::fwrite(buffer.data(), 1, static_cast<size_t>(count), native) == static_cast<size_t>(count) ? count : -1;
|
||||
@@ -349,7 +350,7 @@ int APS5_VABI printf_nid_postfix(const char* format, ...) {
|
||||
if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; }
|
||||
std::va_list args;
|
||||
va_start(args, format);
|
||||
const int result = HasGuestWideFormat(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args);
|
||||
const int result = RequiresGuestFormatting(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args);
|
||||
va_end(args);
|
||||
return result;
|
||||
}
|
||||
@@ -358,7 +359,7 @@ int APS5_VABI libc_printf_nid_postfix(const char* format, ...) {
|
||||
if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; }
|
||||
std::va_list args;
|
||||
va_start(args, format);
|
||||
const int result = HasGuestWideFormat(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args);
|
||||
const int result = RequiresGuestFormatting(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args);
|
||||
va_end(args);
|
||||
return result;
|
||||
}
|
||||
@@ -366,7 +367,7 @@ int APS5_VABI libc_printf_nid_postfix(const char* format, ...) {
|
||||
int APS5_VABI snprintf_nid_postfix(char* buffer, size_t size, const char* format, ...) {
|
||||
std::va_list args;
|
||||
va_start(args, format);
|
||||
const int result = HasGuestWideFormat(format) ? LibcDetail::FormatWindows(buffer, size, format, args) : std::vsnprintf(buffer, size, format, args);
|
||||
const int result = RequiresGuestFormatting(format) ? LibcDetail::FormatWindows(buffer, size, format, args) : std::vsnprintf(buffer, size, format, args);
|
||||
va_end(args);
|
||||
return result;
|
||||
}
|
||||
@@ -374,7 +375,7 @@ int APS5_VABI snprintf_nid_postfix(char* buffer, size_t size, const char* format
|
||||
int APS5_VABI sprintf_nid_postfix(char* buffer, const char* format, ...) {
|
||||
std::va_list args;
|
||||
va_start(args, format);
|
||||
const int result = HasGuestWideFormat(format) ? LibcDetail::FormatWindows(buffer, SIZE_MAX, format, args) : std::vsprintf(buffer, format, args);
|
||||
const int result = RequiresGuestFormatting(format) ? LibcDetail::FormatWindows(buffer, SIZE_MAX, format, args) : std::vsprintf(buffer, format, args);
|
||||
va_end(args);
|
||||
return result;
|
||||
}
|
||||
@@ -441,7 +442,7 @@ int APS5_VABI vprintf_nid_postfix(const char* str, VaList* c) {
|
||||
#ifdef _WIN32
|
||||
return LibcDetail::PrintWindows(str, c);
|
||||
#else
|
||||
if (HasGuestWideFormat(str)) return LibcDetail::PrintWindows(str, c);
|
||||
if (RequiresGuestFormatting(str)) return LibcDetail::PrintWindows(str, c);
|
||||
std::va_list copy;
|
||||
va_copy(copy, *reinterpret_cast<std::va_list*>(c));
|
||||
const int result = std::vprintf(str, copy);
|
||||
@@ -454,7 +455,7 @@ int APS5_VABI vsprintf_nid_postfix(char* str, const char* format, VaList* args)
|
||||
#ifdef _WIN32
|
||||
return LibcDetail::FormatWindows(str, SIZE_MAX, format, args);
|
||||
#else
|
||||
if (HasGuestWideFormat(format)) return LibcDetail::FormatWindows(str, SIZE_MAX, format, args);
|
||||
if (RequiresGuestFormatting(format)) return LibcDetail::FormatWindows(str, SIZE_MAX, format, args);
|
||||
std::va_list copy;
|
||||
va_copy(copy, *reinterpret_cast<std::va_list*>(args));
|
||||
const int result = std::vsprintf(str, format, copy);
|
||||
@@ -467,7 +468,7 @@ int APS5_VABI vsnprintf_nid_postfix(char* str, size_t size, const char* format,
|
||||
#ifdef _WIN32
|
||||
return LibcDetail::FormatWindows(str, size, format, c);
|
||||
#else
|
||||
if (HasGuestWideFormat(format)) return LibcDetail::FormatWindows(str, size, format, c);
|
||||
if (RequiresGuestFormatting(format)) return LibcDetail::FormatWindows(str, size, format, c);
|
||||
std::va_list copy;
|
||||
va_copy(copy, *reinterpret_cast<std::va_list*>(c));
|
||||
const int result = std::vsnprintf(str, size, format, copy);
|
||||
@@ -512,7 +513,7 @@ int APS5_VABI vsscanf_s_nid_postfix(const char* buffer, const char* format, VaLi
|
||||
}
|
||||
|
||||
int APS5_VABI vsnprintf_s_nid_postfix(char* buffer, size_t size, const char* format, VaList* args) {
|
||||
if (HasGuestWideFormat(format)) return LibcDetail::FormatWindows(buffer, size, format, args);
|
||||
if (RequiresGuestFormatting(format)) return LibcDetail::FormatWindows(buffer, size, format, args);
|
||||
std::va_list copy;
|
||||
va_copy(copy, *reinterpret_cast<std::va_list*>(args));
|
||||
const int result = std::vsnprintf(buffer, size, format, copy);
|
||||
@@ -521,7 +522,7 @@ int APS5_VABI vsnprintf_s_nid_postfix(char* buffer, size_t size, const char* for
|
||||
}
|
||||
|
||||
int APS5_VABI vsprintf_s_nid_postfix(char* buffer, size_t size, const char* format, VaList* args) {
|
||||
if (HasGuestWideFormat(format)) return LibcDetail::FormatWindows(buffer, size, format, args);
|
||||
if (RequiresGuestFormatting(format)) return LibcDetail::FormatWindows(buffer, size, format, args);
|
||||
std::va_list copy;
|
||||
va_copy(copy, *reinterpret_cast<std::va_list*>(args));
|
||||
const int result = std::vsnprintf(buffer, size, format, copy);
|
||||
@@ -532,7 +533,7 @@ int APS5_VABI vsprintf_s_nid_postfix(char* buffer, size_t size, const char* form
|
||||
int APS5_VABI sprintf_s_nid_postfix(char* buffer, size_t size, const char* format, ...) {
|
||||
std::va_list args;
|
||||
va_start(args, format);
|
||||
const int result = HasGuestWideFormat(format) ? LibcDetail::FormatWindows(buffer, size, format, args) : std::vsnprintf(buffer, size, format, args);
|
||||
const int result = RequiresGuestFormatting(format) ? LibcDetail::FormatWindows(buffer, size, format, args) : std::vsnprintf(buffer, size, format, args);
|
||||
va_end(args);
|
||||
return result;
|
||||
}
|
||||
@@ -540,7 +541,7 @@ int APS5_VABI sprintf_s_nid_postfix(char* buffer, size_t size, const char* forma
|
||||
int APS5_VABI snprintf_s_nid_postfix(char* buffer, size_t size, const char* format, ...) {
|
||||
std::va_list args;
|
||||
va_start(args, format);
|
||||
const int result = HasGuestWideFormat(format) ? LibcDetail::FormatWindows(buffer, size, format, args) : std::vsnprintf(buffer, size, format, args);
|
||||
const int result = RequiresGuestFormatting(format) ? LibcDetail::FormatWindows(buffer, size, format, args) : std::vsnprintf(buffer, size, format, args);
|
||||
va_end(args);
|
||||
return result;
|
||||
}
|
||||
@@ -548,7 +549,7 @@ int APS5_VABI snprintf_s_nid_postfix(char* buffer, size_t size, const char* form
|
||||
int APS5_VABI printf_s_nid_postfix(const char* format, ...) {
|
||||
std::va_list args;
|
||||
va_start(args, format);
|
||||
const int result = HasGuestWideFormat(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args);
|
||||
const int result = RequiresGuestFormatting(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args);
|
||||
va_end(args);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -111,12 +111,39 @@ std::filesystem::path PathOrRelinkedModule(const std::filesystem::path& path) {
|
||||
return RelinkedModulePath_nid_no_patch(path);
|
||||
}
|
||||
|
||||
std::filesystem::path ResolveHostPath(std::filesystem::path root, const std::filesystem::path& relative, bool module = false) {
|
||||
const auto direct = root / relative;
|
||||
std::vector<std::string> SplitNames(std::string_view text) {
|
||||
std::vector<std::string> names;
|
||||
while (!text.empty()) {
|
||||
const auto separator = text.find('/');
|
||||
names.emplace_back(text.substr(0, separator));
|
||||
if (separator == std::string_view::npos) break;
|
||||
text.remove_prefix(separator + 1);
|
||||
if (text.empty()) names.emplace_back();
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
std::string JoinNames(const std::vector<std::string>& names) {
|
||||
std::string joined;
|
||||
for (std::size_t i = 0; i < names.size(); ++i) {
|
||||
if (i != 0) joined += '/';
|
||||
joined += names[i];
|
||||
}
|
||||
return joined;
|
||||
}
|
||||
|
||||
std::filesystem::path Child(std::filesystem::path parent, const std::string& name) {
|
||||
parent /= "";
|
||||
parent += name;
|
||||
return parent;
|
||||
}
|
||||
|
||||
std::filesystem::path ResolveHostPath(std::filesystem::path root, const std::vector<std::string>& relative, bool module = false) {
|
||||
const auto direct = Child(root, JoinNames(relative));
|
||||
std::error_code error;
|
||||
if (std::filesystem::exists(direct, error)) return direct;
|
||||
for (auto part = relative.begin(); part != relative.end(); ++part) {
|
||||
const auto candidate = root / *part;
|
||||
const auto candidate = Child(root, *part);
|
||||
const auto status = std::filesystem::symlink_status(candidate, error);
|
||||
if (std::filesystem::exists(status) || (error && error != std::errc::no_such_file_or_directory)) {
|
||||
root = candidate;
|
||||
@@ -124,7 +151,7 @@ std::filesystem::path ResolveHostPath(std::filesystem::path root, const std::fil
|
||||
}
|
||||
const bool last = module && std::next(part) == relative.end();
|
||||
const auto relinked = RelinkedModuleName(candidate);
|
||||
NameMatch names[] = {{part->string()}, {relinked.filename().string()}};
|
||||
NameMatch names[] = {{*part}, {relinked.filename().string()}};
|
||||
error = MatchNames(root, std::span(names, last ? 2 : 1));
|
||||
if (names[0].ambiguous) throw std::runtime_error("Ambiguous case-insensitive guest path: " + candidate.string());
|
||||
const bool matched = !error && names[0].path;
|
||||
@@ -152,12 +179,12 @@ std::optional<std::filesystem::path> ResolveAlias(const std::string& guestPath,
|
||||
}
|
||||
#ifndef _WIN32
|
||||
if (guestPath.back() == '/') {
|
||||
auto result = ResolveHostPath(host, relative.substr(prefix.size() + 1));
|
||||
auto result = ResolveHostPath(host, SplitNames(std::string_view(relative).substr(prefix.size() + 1)));
|
||||
result /= "";
|
||||
return module ? PathOrRelinkedModule(result) : result;
|
||||
}
|
||||
#endif
|
||||
return ResolveHostPath(host, relative.substr(prefix.size() + 1), module).make_preferred();
|
||||
return ResolveHostPath(host, SplitNames(std::string_view(relative).substr(prefix.size() + 1)), module).make_preferred();
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -171,8 +198,8 @@ WorkingDirectory& Directories() {
|
||||
static auto* state = new WorkingDirectory;
|
||||
return *state;
|
||||
}
|
||||
std::optional<std::filesystem::path> ProvidedPath(const WorkingDirectory& state, const std::filesystem::path& guest) {
|
||||
const auto relative = TrimSlashes(guest.relative_path().generic_string().c_str());
|
||||
std::optional<std::filesystem::path> ProvidedPath(const WorkingDirectory& state, const std::vector<std::string>& names) {
|
||||
const auto relative = TrimSlashes(JoinNames(names).c_str());
|
||||
PathProvider provide = nullptr;
|
||||
{
|
||||
auto& aliases = Aliases();
|
||||
@@ -182,20 +209,20 @@ std::optional<std::filesystem::path> ProvidedPath(const WorkingDirectory& state,
|
||||
}
|
||||
}
|
||||
if (provide == nullptr) return std::nullopt;
|
||||
const auto supplied = ResolveHostPath(state.root, guest.relative_path());
|
||||
const auto supplied = ResolveHostPath(state.root, names);
|
||||
std::error_code error;
|
||||
if (std::filesystem::exists(supplied, error) || error) return supplied;
|
||||
auto provided = provide(state.root);
|
||||
if (provided.empty()) return std::nullopt;
|
||||
return provided.make_preferred();
|
||||
}
|
||||
std::filesystem::path HostPath(WorkingDirectory& state, const std::filesystem::path& guest, bool module = false) {
|
||||
if (auto aliased = ResolveAlias(guest.relative_path().generic_string(), module)) return *aliased;
|
||||
if (auto provided = ProvidedPath(state, guest)) return *provided;
|
||||
return ResolveHostPath(state.root, guest.relative_path(), module).make_preferred();
|
||||
std::filesystem::path HostPath(WorkingDirectory& state, const std::vector<std::string>& names, bool module = false) {
|
||||
if (auto aliased = ResolveAlias(JoinNames(names), module)) return *aliased;
|
||||
if (auto provided = ProvidedPath(state, names)) return *provided;
|
||||
return ResolveHostPath(state.root, names, module).make_preferred();
|
||||
}
|
||||
void RequireHostDirectory(WorkingDirectory& state, const std::filesystem::path& guest) {
|
||||
const auto host = HostPath(state, guest);
|
||||
void RequireHostDirectory(WorkingDirectory& state, const std::vector<std::string>& names) {
|
||||
const auto host = HostPath(state, names);
|
||||
std::error_code error;
|
||||
const auto status = std::filesystem::status(host, error);
|
||||
if (std::filesystem::is_directory(status)) return;
|
||||
@@ -213,25 +240,32 @@ void RequireHostDirectory(WorkingDirectory& state, const std::filesystem::path&
|
||||
#endif
|
||||
throw std::filesystem::filesystem_error("Guest path component is not a directory", host, error);
|
||||
}
|
||||
std::vector<std::string> WithTrailingSeparator(std::vector<std::string> names, bool trailing) {
|
||||
if (trailing && !names.empty()) names.emplace_back();
|
||||
return names;
|
||||
}
|
||||
std::filesystem::path Resolve(WorkingDirectory& state, const char* path, bool module = false, bool archived = false) {
|
||||
std::string text(path);
|
||||
for (auto& character : text) if (character == '\\') character = '/';
|
||||
std::filesystem::path input(text);
|
||||
#ifdef _WIN32
|
||||
// Preserve the existing ability to pass explicit native drive paths.
|
||||
if (input.has_root_name()) return module ? PathOrRelinkedModule(input) : input;
|
||||
if (std::filesystem::path input(text); input.has_root_name()) return module ? PathOrRelinkedModule(input) : input;
|
||||
#endif
|
||||
const auto current = std::filesystem::path("/") / state.current.lexically_relative(state.root);
|
||||
const auto relative = (input.is_absolute() ? input : current / input).relative_path();
|
||||
std::filesystem::path guest("/");
|
||||
for (auto part = relative.begin(); part != relative.end(); ++part) {
|
||||
if (*part == "..") {
|
||||
const auto parent = guest.lexically_normal();
|
||||
if (parent.has_relative_path() && !(archived && AppArchiveHasDirectory_nid_no_patch(HostPath(state, parent)))) RequireHostDirectory(state, parent);
|
||||
if (text.empty() || text.front() != '/') text = state.current.lexically_relative(state.root).generic_string() + "/" + text;
|
||||
std::vector<std::string> names;
|
||||
bool trailing = true;
|
||||
const auto parts = SplitNames(text);
|
||||
for (std::size_t i = 0; i < parts.size(); ++i) {
|
||||
const auto& name = parts[i];
|
||||
if (name.empty() && i + 1 != parts.size()) continue;
|
||||
if (name == ".." && !names.empty()) {
|
||||
if (!(archived && AppArchiveHasDirectory_nid_no_patch(HostPath(state, names)))) RequireHostDirectory(state, WithTrailingSeparator(names, trailing));
|
||||
names.pop_back();
|
||||
}
|
||||
guest /= *part;
|
||||
trailing = name.empty() || name == "." || name == "..";
|
||||
if (!trailing) names.push_back(name);
|
||||
}
|
||||
return HostPath(state, guest.lexically_normal(), module);
|
||||
return HostPath(state, WithTrailingSeparator(std::move(names), trailing), module);
|
||||
}
|
||||
#ifdef _WIN32
|
||||
using WriteMark = FILETIME;
|
||||
@@ -356,6 +390,10 @@ extern "C" int KeepHostStandardError_nid_no_patch() {
|
||||
extern "C" int HostStandardError_nid_no_patch() {
|
||||
return g_hostStandardError.load();
|
||||
}
|
||||
|
||||
extern "C" int DuplicateHostDescriptor_nid_no_patch(int descriptor) {
|
||||
return ::fcntl(descriptor, F_DUPFD_CLOEXEC, 3);
|
||||
}
|
||||
#endif
|
||||
|
||||
extern "C" void AddPathAlias_nid_no_patch(const char* guestPrefix, const char* hostPath) {
|
||||
@@ -612,6 +650,12 @@ extern "C" void KeepStandardHandle_nid_no_patch(int descriptor, void* handle) {
|
||||
if (handle != nullptr && !SetStdHandle(StandardHandleId(descriptor), handle))
|
||||
throw std::runtime_error("Cannot keep the host standard handle of descriptor " + std::to_string(descriptor));
|
||||
}
|
||||
extern "C" int DuplicateHostDescriptor_nid_no_patch(int descriptor) {
|
||||
const auto previous = _set_thread_local_invalid_parameter_handler([](const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {});
|
||||
const int kept = ::_dup(descriptor);
|
||||
_set_thread_local_invalid_parameter_handler(previous);
|
||||
return kept;
|
||||
}
|
||||
#endif
|
||||
|
||||
extern "C" [[noreturn]] void NotImplemented_nid_no_patch(const char* funcName) {
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
#include <string>
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
#ifdef __APPLE__
|
||||
#include <mach/mach.h>
|
||||
#include <mach/mach_vm.h>
|
||||
#endif
|
||||
#include <cstdio>
|
||||
#include <iterator>
|
||||
#include <map>
|
||||
@@ -148,8 +152,8 @@ public:
|
||||
|
||||
private:
|
||||
Arena() {
|
||||
#if defined(__linux__)
|
||||
SeedLinuxUsedRanges();
|
||||
#if defined(__linux__) || defined(__APPLE__)
|
||||
SeedHostUsedRanges();
|
||||
#elif defined(_WIN32)
|
||||
_writeWatched = std::getenv("APS5_NO_WRITE_WATCH") == nullptr;
|
||||
SYSTEM_INFO system{};
|
||||
@@ -182,7 +186,8 @@ private:
|
||||
}
|
||||
|
||||
#if defined(__linux__)
|
||||
void SeedLinuxUsedRanges() {
|
||||
static std::vector<std::pair<std::uintptr_t, std::uintptr_t>> HostMappings() {
|
||||
std::vector<std::pair<std::uintptr_t, std::uintptr_t>> mappings;
|
||||
std::ifstream maps("/proc/self/maps");
|
||||
if (!maps.is_open()) throw std::runtime_error("guest arena: cannot open /proc/self/maps");
|
||||
std::string line;
|
||||
@@ -191,6 +196,48 @@ private:
|
||||
std::uintptr_t begin = 0, end = 0;
|
||||
char dash = 0;
|
||||
if (!(fields >> std::hex >> begin >> dash >> end) || dash != '-' || end <= begin) throw std::runtime_error("guest arena: malformed /proc/self/maps line: " + line);
|
||||
mappings.emplace_back(begin, end);
|
||||
}
|
||||
return mappings;
|
||||
}
|
||||
|
||||
static int ReserveNoReplace(std::uintptr_t address, std::size_t bytes) {
|
||||
void* const wanted = reinterpret_cast<void*>(address);
|
||||
void* const placed = mmap(wanted, bytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED_NOREPLACE, -1, 0);
|
||||
if (placed == wanted) return 0;
|
||||
if (placed == MAP_FAILED) return errno;
|
||||
munmap(placed, bytes);
|
||||
return EINVAL;
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
static std::vector<std::pair<std::uintptr_t, std::uintptr_t>> HostMappings() {
|
||||
std::vector<std::pair<std::uintptr_t, std::uintptr_t>> mappings;
|
||||
mach_vm_address_t address = ArenaStart;
|
||||
while (address < ApplicationAreaEnd) {
|
||||
mach_vm_size_t size = 0;
|
||||
vm_region_basic_info_data_64_t info{};
|
||||
mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64;
|
||||
mach_port_t object = MACH_PORT_NULL;
|
||||
const kern_return_t result = mach_vm_region(mach_task_self(), &address, &size, VM_REGION_BASIC_INFO_64, reinterpret_cast<vm_region_info_t>(&info), &count, &object);
|
||||
if (result == KERN_INVALID_ADDRESS) break;
|
||||
if (result != KERN_SUCCESS) throw std::runtime_error("guest arena: mach_vm_region failed");
|
||||
mappings.emplace_back(static_cast<std::uintptr_t>(address), static_cast<std::uintptr_t>(address + size));
|
||||
address += size;
|
||||
}
|
||||
return mappings;
|
||||
}
|
||||
|
||||
static int ReserveNoReplace(std::uintptr_t address, std::size_t bytes) {
|
||||
mach_vm_address_t wanted = address;
|
||||
const kern_return_t result = mach_vm_map(mach_task_self(), &wanted, bytes, 0, VM_FLAGS_FIXED, MEMORY_OBJECT_NULL, 0, FALSE, VM_PROT_NONE, VM_PROT_ALL, VM_INHERIT_DEFAULT);
|
||||
if (result == KERN_SUCCESS) return 0;
|
||||
return result == KERN_NO_SPACE ? EEXIST : ENOMEM;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(__linux__) || defined(__APPLE__)
|
||||
void SeedHostUsedRanges() {
|
||||
for (const auto& [begin, end] : HostMappings()) {
|
||||
if (end <= ArenaStart || begin >= ApplicationAreaEnd) continue;
|
||||
const auto first = std::max(begin, ArenaStart);
|
||||
const auto last = std::min(end, ApplicationAreaEnd);
|
||||
@@ -208,12 +255,8 @@ private:
|
||||
constexpr std::uintptr_t kChunk = 1ULL << 30;
|
||||
for (std::uintptr_t cursor = from; cursor < to; cursor += kChunk) {
|
||||
const auto end = std::min(cursor + kChunk, to);
|
||||
void* const wanted = reinterpret_cast<void*>(cursor);
|
||||
void* const placed = mmap(wanted, end - cursor, PROT_NONE,
|
||||
MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED_NOREPLACE, -1, 0);
|
||||
if (placed == wanted) continue;
|
||||
const int failed = placed == MAP_FAILED ? errno : EINVAL;
|
||||
if (placed != MAP_FAILED) munmap(placed, end - cursor);
|
||||
const int failed = ReserveNoReplace(cursor, end - cursor);
|
||||
if (failed == 0) continue;
|
||||
if (failed != EEXIST) {
|
||||
char message[128];
|
||||
std::snprintf(message, sizeof(message), "guest arena: cannot reserve 0x%llx+0x%llx",
|
||||
@@ -428,7 +471,7 @@ void GuestArenaForgetSection_nid_postfix(void* section) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(__linux__)
|
||||
#if defined(__linux__) || defined(__APPLE__)
|
||||
namespace {
|
||||
int PosixProtection(std::uint32_t windowsProtection) {
|
||||
switch (windowsProtection) {
|
||||
@@ -449,7 +492,7 @@ void* PlaceAt(void* pointer, std::size_t bytes, std::uint32_t protection) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(__linux__)
|
||||
#if defined(__linux__) || defined(__APPLE__)
|
||||
void GuestArenaSetProtection_nid_postfix(std::uintptr_t address, std::size_t bytes, std::uint32_t protection) {
|
||||
if (!Arena::Get().Contains(reinterpret_cast<void*>(address), bytes)) throw OutsideArena("set protection", reinterpret_cast<void*>(address), bytes);
|
||||
if (mprotect(reinterpret_cast<void*>(address), bytes, PosixProtection(protection)) != 0)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <cstdint>
|
||||
#include <cerrno>
|
||||
#include <cfenv>
|
||||
#include <climits>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstdint>
|
||||
@@ -319,6 +320,8 @@ long double APS5_VABI fminl_nid_postfix(long double x, long double y) { return G
|
||||
double APS5_VABI fmax_nid_postfix(double x, double y) { return GuestFmax(x, y); }
|
||||
float APS5_VABI fmaxf_nid_postfix(float x, float y) { return GuestFmax(x, y); }
|
||||
long double APS5_VABI fmaxl_nid_postfix(long double x, long double y) { return GuestFmax(x, y); }
|
||||
double APS5_VABI fdim_nid_postfix(double x, double y) { return std::fdim(x, y); }
|
||||
float APS5_VABI fdimf_nid_postfix(float x, float y) { return std::fdim(x, y); }
|
||||
long double APS5_VABI acosl_nid_postfix(long double x) { return std::acos(x); }
|
||||
long double APS5_VABI acoshl_nid_postfix(long double x) { return std::acosh(x); }
|
||||
long double APS5_VABI asinhl_nid_postfix(long double x) { return std::asinh(x); }
|
||||
@@ -545,6 +548,16 @@ long double APS5_VABI powl_nid_postfix(long double base, long double exp) { retu
|
||||
long double APS5_VABI ldexpl_nid_postfix(long double x, int exp) { return std::ldexp(x, exp); }
|
||||
long double APS5_VABI truncl_nid_postfix(long double x) { return std::trunc(x); }
|
||||
double APS5_VABI logb_nid_postfix(double x) { return std::logb(x); }
|
||||
int APS5_VABI ilogb_nid_postfix(double x) {
|
||||
if (x == 0.0) {
|
||||
return INT_MIN;
|
||||
}
|
||||
if (!std::isfinite(x)) {
|
||||
return INT_MAX;
|
||||
}
|
||||
return std::ilogb(x);
|
||||
}
|
||||
int APS5_VABI ilogbf_nid_postfix(float x) { return ilogb_nid_postfix(static_cast<double>(x)); }
|
||||
double APS5_VABI remainder_nid_postfix(double x, double y) { return std::remainder(x, y); }
|
||||
float APS5_VABI _FSinh_nid_postfix(float x, float y) { return y * std::sinh(x); }
|
||||
float APS5_VABI _FCosh_nid_postfix(float x, float y) { return y * std::cosh(x); }
|
||||
|
||||
@@ -7,10 +7,26 @@
|
||||
#ifdef _WIN32
|
||||
#include <io.h>
|
||||
#else
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace { FileStream input{stdin}; }
|
||||
namespace {
|
||||
#ifndef _WIN32
|
||||
const bool standardDescriptorsOpen = [] {
|
||||
for (int descriptor = 0; descriptor <= 2; ++descriptor) {
|
||||
if (::fcntl(descriptor, F_GETFD) != -1 || errno != EBADF) continue;
|
||||
const int opened = ::open("/dev/null", descriptor == 0 ? O_RDONLY : O_WRONLY);
|
||||
if (opened > descriptor) {
|
||||
::dup2(opened, descriptor);
|
||||
::close(opened);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}();
|
||||
#endif
|
||||
FileStream input{stdin};
|
||||
}
|
||||
extern "C" {
|
||||
FileStream* __stdinp_nid_postfix = &input;
|
||||
FileStream* __stdoutp_nid_postfix = &_Stdout_nid_postfix;
|
||||
|
||||
@@ -187,6 +187,10 @@ int APS5_VABI _Cnd_init_nid_postfix(void** handle) {
|
||||
return threadSuccess;
|
||||
}
|
||||
|
||||
int APS5_VABI _Cnd_init_with_default_name_override_nid_postfix(void** handle, const char*) {
|
||||
return _Cnd_init_nid_postfix(handle);
|
||||
}
|
||||
|
||||
void APS5_VABI _Cnd_destroy_nid_postfix(void** handle) {
|
||||
if (!handle)
|
||||
throw std::invalid_argument("_Cnd_destroy: null condition handle pointer");
|
||||
|
||||
@@ -256,6 +256,58 @@ bool ReadWord(std::uint64_t address, std::uint64_t* value) {
|
||||
return ReadCode(address, reinterpret_cast<std::uint8_t*>(value), sizeof(*value)) == sizeof(*value);
|
||||
}
|
||||
|
||||
std::uint64_t EnvironmentNumber(const char* name) {
|
||||
char text[32];
|
||||
const DWORD length = GetEnvironmentVariableA(name, text, sizeof(text));
|
||||
if (length == 0 || length >= sizeof(text)) return 0;
|
||||
return std::strtoull(text, nullptr, 0);
|
||||
}
|
||||
|
||||
void ReportStackWords(std::uint64_t rsp) {
|
||||
const auto bytes = std::min<std::uint64_t>(EnvironmentNumber("APS5_CRASH_STACK_BYTES"), std::uint64_t{1} << 20);
|
||||
if (bytes == 0) return;
|
||||
const auto* tib = reinterpret_cast<const NT_TIB*>(NtCurrentTeb());
|
||||
const auto stackLow = reinterpret_cast<std::uint64_t>(tib->StackLimit);
|
||||
const auto stackHigh = reinterpret_cast<std::uint64_t>(tib->StackBase);
|
||||
const auto first = rsp & ~std::uint64_t{7};
|
||||
Report(" stack words from rsp 0x%016llx (APS5_CRASH_STACK_BYTES):\n", static_cast<unsigned long long>(rsp));
|
||||
const auto limit = std::min(first + bytes, stackHigh);
|
||||
std::uint64_t end = first;
|
||||
std::uint64_t readablePage = 0;
|
||||
for (std::uint64_t at = first; at < limit; at += 32) {
|
||||
std::uint64_t words[4] = {};
|
||||
int count = 0;
|
||||
for (; count < 4; ++count) {
|
||||
const auto slot = at + static_cast<std::uint64_t>(count) * 8;
|
||||
const auto page = slot & ~std::uint64_t{0xfff};
|
||||
if (page != readablePage) {
|
||||
if (!IsReadable(slot)) break;
|
||||
readablePage = page;
|
||||
}
|
||||
words[count] = *reinterpret_cast<const std::uint64_t*>(slot);
|
||||
}
|
||||
if (count == 0) break;
|
||||
static char text[192];
|
||||
int used = std::snprintf(text, sizeof(text), " +0x%04llx:", static_cast<unsigned long long>(at - first));
|
||||
for (int k = 0; k < count && used > 0 && used < static_cast<int>(sizeof(text)); ++k) used += std::snprintf(text + used, sizeof(text) - static_cast<std::size_t>(used), " %016llx", static_cast<unsigned long long>(words[k]));
|
||||
Report("%s\n", text);
|
||||
end = at + static_cast<std::uint64_t>(count) * 8;
|
||||
if (count < 4) break;
|
||||
}
|
||||
static std::uint64_t seen[256];
|
||||
std::size_t seenCount = 0;
|
||||
Report(" data at stack words that point outside the stack (8 words each):\n");
|
||||
for (std::uint64_t slot = first; slot + 8 <= end && seenCount < std::size(seen); slot += 8) {
|
||||
const auto value = *reinterpret_cast<const std::uint64_t*>(slot);
|
||||
if (value < 0x10000 || (value & 7) != 0 || value > 0x7fffffffffffull || (value >= stackLow && value < stackHigh)) continue;
|
||||
if (std::find(seen, seen + seenCount, value) != seen + seenCount) continue;
|
||||
if (IsExecutable(value) || !IsReadable(value) || !IsReadable(value + 0x38)) continue;
|
||||
seen[seenCount++] = value;
|
||||
const auto* words = reinterpret_cast<const std::uint64_t*>(value);
|
||||
Report(" [+0x%04llx] 0x%016llx: %016llx %016llx %016llx %016llx %016llx %016llx %016llx %016llx\n", static_cast<unsigned long long>(slot - first), static_cast<unsigned long long>(value), words[0], words[1], words[2], words[3], words[4], words[5], words[6], words[7]);
|
||||
}
|
||||
}
|
||||
|
||||
void ReportUndeliverableException(const EXCEPTION_POINTERS* info, std::uint64_t frame) {
|
||||
const auto* record = info->ExceptionRecord;
|
||||
char type[256] = "of unknown type";
|
||||
@@ -377,7 +429,8 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) {
|
||||
frame = next;
|
||||
}
|
||||
ReportStackCandidates(context->Rsp, 16, 0x2000);
|
||||
if (context->Rip == 0 || !IsExecutable(context->Rip)) {
|
||||
ReportStackWords(context->Rsp);
|
||||
if (context->Rip == 0 ||!IsExecutable(context->Rip)) {
|
||||
// A jump into nothing usually follows a return through a clobbered frame; the frames that just
|
||||
// returned are still below rsp.
|
||||
Report(" recently popped return address candidates:\n");
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
#ifndef CORE_LIBS_PRX_LIBKERNEL_APPMETADATA_INCLUDE_PARAMSFOPARSER_HPP
|
||||
#define CORE_LIBS_PRX_LIBKERNEL_APPMETADATA_INCLUDE_PARAMSFOPARSER_HPP
|
||||
|
||||
#include "prx/libkernel/AppMetadata/include/ParamJsonParser.hpp"
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
ParsedParamJson parseParamSfo(const std::filesystem::path& paramSfoPath);
|
||||
|
||||
#endif
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp"
|
||||
#include "prx/libkernel/AppMetadata/include/ParamJsonParser.hpp"
|
||||
#include "prx/libkernel/AppMetadata/include/ParamSfoParser.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -25,6 +26,7 @@ std::int32_t g_userDefinedParams[4] = {};
|
||||
std::vector<std::uint8_t> g_iconBytes;
|
||||
|
||||
constexpr const char* AppMetadataParamJsonGuestPath = "/app0/sce_sys/param.json";
|
||||
constexpr const char* AppMetadataParamSfoGuestPath = "/app0/sce_sys/param.sfo";
|
||||
constexpr const char* AppMetadataIconGuestPath = "/app0/sce_sys/icon0.png";
|
||||
|
||||
void copyToFixedBuffer(char* destination, std::size_t destinationSize, const std::string& source) {
|
||||
@@ -33,12 +35,16 @@ void copyToFixedBuffer(char* destination, std::size_t destinationSize, const std
|
||||
std::memcpy(destination, source.c_str(), source.size() + 1);
|
||||
}
|
||||
|
||||
ParsedParamJson parseParams(const std::filesystem::path& paramJson, const std::filesystem::path& paramSfo) {
|
||||
if (std::filesystem::exists(paramJson)) return parseParamJson(paramJson);
|
||||
if (std::filesystem::exists(paramSfo)) return parseParamSfo(paramSfo);
|
||||
throw std::runtime_error("param.json or param.sfo not found");
|
||||
}
|
||||
|
||||
void ensureTitleLoaded() {
|
||||
std::lock_guard lock(g_metadataMutex);
|
||||
if (g_titleLoaded) return;
|
||||
const auto resolvedPath = ResolvePath_nid_no_patch(AppMetadataParamJsonGuestPath);
|
||||
if (!std::filesystem::exists(resolvedPath)) throw std::runtime_error("param.json not found");
|
||||
const auto parsed = parseParamJson(resolvedPath);
|
||||
const auto parsed = parseParams(ResolvePath_nid_no_patch(AppMetadataParamJsonGuestPath), ResolvePath_nid_no_patch(AppMetadataParamSfoGuestPath));
|
||||
copyToFixedBuffer(g_title, sizeof(g_title), parsed.title);
|
||||
copyToFixedBuffer(g_titleId, sizeof(g_titleId), parsed.titleId);
|
||||
g_downloadDataSizeMiB = parsed.downloadDataSizeMiB;
|
||||
@@ -50,8 +56,9 @@ void ensureTitleLoaded() {
|
||||
const bool g_downloadDataMounted = [] {
|
||||
try {
|
||||
const auto run = std::filesystem::current_path();
|
||||
const auto paramJson = run / "app0" / "sce_sys" / "param.json";
|
||||
if (!std::filesystem::exists(paramJson) || parseParamJson(paramJson).downloadDataSizeMiB == 0) return false;
|
||||
const auto metadata = run / "app0" / "sce_sys";
|
||||
if (!std::filesystem::exists(metadata / "param.json") && !std::filesystem::exists(metadata / "param.sfo")) return false;
|
||||
if (parseParams(metadata / "param.json", metadata / "param.sfo").downloadDataSizeMiB == 0) return false;
|
||||
std::filesystem::create_directories(run / "download0");
|
||||
return true;
|
||||
} catch (const std::exception&) {
|
||||
@@ -62,7 +69,8 @@ const bool g_downloadDataMounted = [] {
|
||||
const bool g_dataMounted = [] {
|
||||
try {
|
||||
const auto run = std::filesystem::current_path();
|
||||
if (!std::filesystem::exists(run / "app0" / "sce_sys" / "param.json")) return false;
|
||||
const auto metadata = run / "app0" / "sce_sys";
|
||||
if (!std::filesystem::exists(metadata / "param.json") && !std::filesystem::exists(metadata / "param.sfo")) return false;
|
||||
std::filesystem::create_directories(run / "data");
|
||||
return true;
|
||||
} catch (const std::exception&) {
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
#include "prx/libkernel/AppMetadata/include/ParamSfoParser.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::uint16_t SfoUtf8 = 0x0204;
|
||||
constexpr std::uint16_t SfoInteger = 0x0404;
|
||||
constexpr std::size_t HeaderSize = 20;
|
||||
constexpr std::size_t EntrySize = 16;
|
||||
|
||||
std::uint16_t readU16(const std::vector<std::uint8_t>& bytes, std::size_t offset) {
|
||||
std::uint16_t value;
|
||||
std::memcpy(&value, bytes.data() + offset, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
std::uint32_t readU32(const std::vector<std::uint8_t>& bytes, std::size_t offset) {
|
||||
std::uint32_t value;
|
||||
std::memcpy(&value, bytes.data() + offset, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ParsedParamJson parseParamSfo(const std::filesystem::path& paramSfoPath) {
|
||||
std::ifstream file(paramSfoPath, std::ios::binary);
|
||||
if (!file.is_open()) throw std::runtime_error("failed to open param.sfo");
|
||||
const std::vector<std::uint8_t> bytes((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||
if (bytes.size() < HeaderSize || std::memcmp(bytes.data(), "\0PSF", 4) != 0) throw std::runtime_error("param.sfo has no PSF header");
|
||||
const std::uint64_t keyTable = readU32(bytes, 8);
|
||||
const std::uint64_t dataTable = readU32(bytes, 12);
|
||||
const std::uint64_t count = readU32(bytes, 16);
|
||||
if (count > (bytes.size() - HeaderSize) / EntrySize) throw std::runtime_error("param.sfo tables are out of bounds");
|
||||
std::set<std::string> keys;
|
||||
std::map<std::string, std::string> strings;
|
||||
std::map<std::string, std::int32_t> integers;
|
||||
for (std::uint64_t index = 0; index < count; ++index) {
|
||||
const auto entry = HeaderSize + index * EntrySize;
|
||||
const auto keyOffset = keyTable + readU16(bytes, entry);
|
||||
const auto format = readU16(bytes, entry + 2);
|
||||
const std::uint64_t length = readU32(bytes, entry + 4);
|
||||
const auto dataOffset = dataTable + readU32(bytes, entry + 12);
|
||||
if (keyOffset >= bytes.size() || dataOffset > bytes.size() || length > bytes.size() - dataOffset) throw std::runtime_error("param.sfo entry is out of bounds");
|
||||
const auto keyEnd = std::find(bytes.begin() + static_cast<std::ptrdiff_t>(keyOffset), bytes.end(), std::uint8_t{0});
|
||||
if (keyEnd == bytes.end()) throw std::runtime_error("param.sfo key is not terminated");
|
||||
const std::string key(bytes.begin() + static_cast<std::ptrdiff_t>(keyOffset), keyEnd);
|
||||
if (!keys.insert(key).second) throw std::runtime_error("param.sfo repeats " + key);
|
||||
if (format == SfoUtf8) {
|
||||
const auto begin = bytes.begin() + static_cast<std::ptrdiff_t>(dataOffset);
|
||||
const auto end = std::find(begin, begin + static_cast<std::ptrdiff_t>(length), std::uint8_t{0});
|
||||
if (end == begin + static_cast<std::ptrdiff_t>(length)) throw std::runtime_error("param.sfo " + key + " is not a terminated string");
|
||||
strings.emplace(key, std::string(begin, end));
|
||||
} else if (format == SfoInteger) {
|
||||
if (length != 4) throw std::runtime_error("param.sfo " + key + " is not a 32-bit integer");
|
||||
integers.emplace(key, static_cast<std::int32_t>(readU32(bytes, dataOffset)));
|
||||
}
|
||||
}
|
||||
const auto text = [&](const char* key) {
|
||||
const auto found = strings.find(key);
|
||||
if (found == strings.end() || found->second.empty()) throw std::runtime_error(std::string("param.sfo is missing ") + key);
|
||||
return found->second;
|
||||
};
|
||||
ParsedParamJson result;
|
||||
result.title = text("TITLE");
|
||||
result.titleId = text("TITLE_ID");
|
||||
if (const auto size = integers.find("DOWNLOAD_DATA_SIZE"); size != integers.end()) {
|
||||
if (size->second < 0) throw std::runtime_error("param.sfo DOWNLOAD_DATA_SIZE is negative");
|
||||
result.downloadDataSizeMiB = static_cast<std::uint64_t>(size->second);
|
||||
}
|
||||
for (int index = 0; index < 4; ++index) {
|
||||
const auto param = integers.find("USER_DEFINED_PARAM_" + std::to_string(index + 1));
|
||||
if (param != integers.end()) result.userDefinedParams[index] = param->second;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -76,6 +76,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Time/TimedWait.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/AppMetadata/src/AppMetadata.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/AppMetadata/src/ParamJsonParser.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/AppMetadata/src/ParamSfoParser.cpp
|
||||
)
|
||||
|
||||
target_include_directories(libkernel PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
@@ -105,12 +106,12 @@ if(BUILD_TESTING)
|
||||
target_include_directories(download_data_mount_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(download_data_mount_tests PRIVATE libkernel libc)
|
||||
configure_windows_unwind(download_data_mount_tests)
|
||||
foreach(downloadDataCase IN ITEMS declared:256 none:0 invalid:256 missing:0)
|
||||
foreach(downloadDataCase IN ITEMS declared:256 none:0 invalid:256 missing:0 sfo:0)
|
||||
string(REPLACE ":" ";" downloadDataCase ${downloadDataCase})
|
||||
list(GET downloadDataCase 0 downloadDataName)
|
||||
list(GET downloadDataCase 1 downloadDataSize)
|
||||
set(downloadDataRun ${CMAKE_CURRENT_BINARY_DIR}/download_data_mount_${downloadDataName})
|
||||
if(downloadDataName STREQUAL "missing")
|
||||
if(downloadDataName STREQUAL "missing" OR downloadDataName STREQUAL "sfo")
|
||||
file(MAKE_DIRECTORY ${downloadDataRun})
|
||||
elseif(downloadDataName STREQUAL "invalid")
|
||||
file(WRITE ${downloadDataRun}/app0/sce_sys/param.json "{\"downloadDataSize\":${downloadDataSize}}")
|
||||
@@ -135,6 +136,15 @@ if(BUILD_TESTING)
|
||||
set_tests_properties(param_json_parser PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
|
||||
endif()
|
||||
add_test_executable(param_sfo_parser_tests ${LIBS_INCLUDE_DIR}/tests/ParamSfoParser.cpp)
|
||||
target_include_directories(param_sfo_parser_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(param_sfo_parser_tests PRIVATE libkernel libc)
|
||||
configure_windows_unwind(param_sfo_parser_tests)
|
||||
add_test(NAME param_sfo_parser COMMAND param_sfo_parser_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(param_sfo_parser PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
|
||||
endif()
|
||||
add_test_executable(guest_posix_sem_tests ${LIBS_INCLUDE_DIR}/tests/GuestPosixSem.cpp)
|
||||
target_include_directories(guest_posix_sem_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_posix_sem_tests PRIVATE libkernel libc)
|
||||
@@ -275,6 +285,11 @@ if(BUILD_TESTING)
|
||||
target_link_libraries(guest_host_standard_error_tests PRIVATE libkernel libc)
|
||||
add_test(NAME guest_host_standard_error COMMAND guest_host_standard_error_tests)
|
||||
set_tests_properties(guest_host_standard_error PROPERTIES TIMEOUT 10)
|
||||
add_test_executable(guest_closed_standard_descriptors_tests ${LIBS_INCLUDE_DIR}/tests/GuestClosedStandardDescriptors.cpp)
|
||||
target_include_directories(guest_closed_standard_descriptors_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_closed_standard_descriptors_tests PRIVATE libkernel libc)
|
||||
add_test(NAME guest_closed_standard_descriptors COMMAND guest_closed_standard_descriptors_tests)
|
||||
set_tests_properties(guest_closed_standard_descriptors PROPERTIES TIMEOUT 10)
|
||||
endif()
|
||||
add_test_executable(guest_truncate_tests ${LIBS_INCLUDE_DIR}/tests/GuestTruncate.cpp)
|
||||
target_include_directories(guest_truncate_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
@@ -420,6 +435,10 @@ if(BUILD_TESTING)
|
||||
add_test_executable(guest_thread_self_tests ${LIBS_INCLUDE_DIR}/tests/GuestThreadSelf.cpp)
|
||||
target_include_directories(guest_thread_self_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_thread_self_tests PRIVATE libkernel libc)
|
||||
if(WIN32)
|
||||
target_compile_definitions(guest_thread_self_tests PRIVATE _WIN32_WINNT=0x0602)
|
||||
target_link_options(guest_thread_self_tests PRIVATE "SHELL:-Xlinker --stack=0x210000,0x200000")
|
||||
endif()
|
||||
configure_windows_unwind(guest_thread_self_tests)
|
||||
add_test(NAME guest_thread_self COMMAND guest_thread_self_tests)
|
||||
set_tests_properties(guest_thread_self PROPERTIES TIMEOUT 20)
|
||||
@@ -681,8 +700,20 @@ if(BUILD_TESTING)
|
||||
configure_windows_unwind(guest_semaphore_tests)
|
||||
add_test(NAME guest_semaphore COMMAND guest_semaphore_tests)
|
||||
set_tests_properties(guest_semaphore PROPERTIES TIMEOUT 20)
|
||||
if(Python3_Interpreter_FOUND)
|
||||
add_test_executable(semaphore_trace_tests
|
||||
${LIBS_INCLUDE_DIR}/tests/SemaphoreTrace.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Semaphore/src/Semaphore.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Time/TimedWait.cpp)
|
||||
target_include_directories(semaphore_trace_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
find_package(Threads REQUIRED)
|
||||
target_link_libraries(semaphore_trace_tests PRIVATE Threads::Threads libc)
|
||||
add_test(NAME guest_semaphore_trace COMMAND ${Python3_EXECUTABLE}
|
||||
${LIBS_INCLUDE_DIR}/tests/test_semaphore_trace.py $<TARGET_FILE:semaphore_trace_tests>)
|
||||
set_tests_properties(guest_semaphore_trace PROPERTIES TIMEOUT 20)
|
||||
endif()
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_semaphore PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
set_tests_properties(guest_semaphore guest_semaphore_trace PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
|
||||
endif()
|
||||
add_test_executable(guest_pthread_timed_locks_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadTimedLocks.cpp)
|
||||
|
||||
Loaded 100 of 200 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user