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:
Yunus Shaikh committed 2026-10-18 11:33:00 -07:00
commit d0325058f7
200 files changed
+9036 -471

No files matched your search

+40
View File
@@ -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
+11
View File
@@ -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
View File
@@ -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.
![anyps5_collage1](https://github.com/user-attachments/assets/ed2f0b2d-33d3-46d7-ab88-767ca872fc99)
+1
View File
@@ -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)
+1 -1
View File
@@ -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*;
+1 -1
View File
@@ -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) {
+13
View File
@@ -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;
}
}
+89 -3
View File
@@ -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 = &registry.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), &parameters);
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, &copy);
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, &copy);
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, &region);
region.srcOffset = second->snapshots[0].offset;
region.dstOffset = elementBytes;
copy(commands, second->snapshots[0].buffer->Handle(), downloaded->Handle(), 1, &region);
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();
+85 -15
View File
@@ -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
+13 -14
View File
@@ -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) {
+1 -1
View File
@@ -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
+33 -5
View File
@@ -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) {
+42 -10
View File
@@ -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) {
+61 -7
View File
@@ -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();
}
+1 -1
View File
@@ -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");
}
+33 -1
View File
@@ -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()
+35
View File
@@ -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;
+1
View File
@@ -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') {
+2 -2
View File
@@ -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) {
+2 -1
View File
@@ -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;
+19 -18
View File
@@ -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;
}
+70 -26
View File
@@ -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) {
+54 -11
View File
@@ -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)
+13
View File
@@ -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); }
+17 -1
View File
@@ -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;
+4
View File
@@ -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;
}
+34 -3
View File
@@ -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