Merge branch 'feat/ngs2-system-reset-option' into feat/ngs2-system-option-codes

This commit is contained in:
Federico Marras committed 2026-10-22 15:32:04 +02:00
commit 845000aaf8
535 files changed
+21404 -6453

No files matched your search

+59
View File
@@ -0,0 +1,59 @@
name: Bug report
description: A title, homebrew or test stops, crashes or renders wrongly.
labels: [bug]
body:
- type: markdown
attributes:
value: |
Search open and closed issues and pull requests for the error text first, and comment there if it is already reported.
Game bring-up progress belongs in [Discussions](https://github.com/boykopovar/AnyPS5/discussions/categories/game-bring-up), one thread per title.
A slow scene or a change in speed goes into the [Performance report](https://github.com/boykopovar/AnyPS5/issues/new?template=performance.yml) form, which asks for a [performance trace](https://github.com/boykopovar/AnyPS5/blob/main/docs/dev/BUILD.md#performance-trace) and its `tools/trace_digest.py` digest.
- type: input
id: summary
attributes:
label: What happens
description: One sentence, with the exact error text if there is one.
validations:
required: true
- type: input
id: title
attributes:
label: Title, test or build step
description: Title ID and content version (for example PPSAxxxxx 01.000.000), a homebrew with its source, the ctest name, or the build step that fails.
validations:
required: true
- type: input
id: commit
attributes:
label: AnyPS5 commit
description: The `main` commit that was built, or the release that was used, and any pull requests merged on top.
validations:
required: true
- type: input
id: system
attributes:
label: System
description: OS, GPU and driver version, and the compiler if you built it yourself.
placeholder: Arch Linux, GCC 16.2.1, RTX 5080 (NVIDIA 615.78.08)
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to reproduce
description: The commands that were run, environment variables included, and how long it takes to stop.
validations:
required: true
- type: textarea
id: log
attributes:
label: Log
description: The lines around the stop.
render: text
validations:
required: true
- type: textarea
id: notes
attributes:
label: What was checked
description: Other devices or commits tried, and whether it is the same every run. Attach the full log and screenshots here.
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: true
contact_links:
- name: Game bring-up
url: https://github.com/boykopovar/AnyPS5/discussions/categories/game-bring-up
about: Progress of one title, one thread per title ID
- name: Question
url: https://github.com/boykopovar/AnyPS5/discussions/categories/q-a
about: Building, converting and running
+3 -4
View File
@@ -18,15 +18,14 @@ jobs:
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.base.sha }}
ref: ${{ github.event.pull_request.base.ref }}
fetch-depth: 0
persist-credentials: false
- name: Fetch pull request objects without checking out its code
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: git fetch --no-tags origin "$HEAD_SHA"
- name: Check conventions with the trusted base script
- name: Check conventions with the current base branch script
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: python3 -I tools/check_conventions.py --base "$BASE_SHA" --head "$HEAD_SHA"
run: python3 -I tools/check_conventions.py --base HEAD --head "$HEAD_SHA"
+117
View File
@@ -0,0 +1,117 @@
name: Windows MSYS2
on:
schedule:
- cron: '23 8 * * *'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: windows-msys2-${{ github.ref }}
cancel-in-progress: true
env:
CONFIGURE: >-
-G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON
-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++
-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
jobs:
windows-msys2:
runs-on: windows-latest
timeout-minutes: 45
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.sha }}
persist-credentials: false
- id: submodules
name: Key the submodules on their commits
run: |
echo "key=$(git ls-files -s | awk '$1 == "160000" { print $2, $4 }' | cat - .gitmodules | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT"
echo "ffmpeg=$(git rev-parse HEAD:3rdparty/ffmpeg-core)" >> "$GITHUB_OUTPUT"
- id: submodule-cache
uses: actions/cache/restore@v6
with:
path: ${{ runner.temp }}/submodule-gitdirs.tar
key: submodule-gitdirs-${{ runner.os }}-${{ steps.submodules.outputs.key }}
- name: Check out the submodules
run: |
if [ -f "$RUNNER_TEMP/submodule-gitdirs.tar" ]; then tar --force-local -xf "$RUNNER_TEMP/submodule-gitdirs.tar"; fi
git submodule update --init --recursive --depth 1 --jobs 8
- if: steps.submodule-cache.outputs.cache-hit != 'true'
run: tar --force-local -cf "$RUNNER_TEMP/submodule-gitdirs.tar" .git/modules
- if: steps.submodule-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v6
continue-on-error: true
with:
path: ${{ runner.temp }}/submodule-gitdirs.tar
key: submodule-gitdirs-${{ runner.os }}-${{ steps.submodules.outputs.key }}
- name: Use the CA bundle of Git for Windows
run: |
for prefix in ucrt64 mingw64; do
bundle="C:/Program Files/Git/$prefix/etc/ssl/certs/ca-bundle.crt"
if [ -f "$bundle" ]; then echo "SSL_CERT_FILE=$bundle" >> "$GITHUB_ENV"; exit 0; fi
done
echo "::error::No CA bundle found in Git for Windows"; exit 1
- id: msys2
uses: msys2/setup-msys2@v2
with:
msystem: UCRT64
update: true
install: >-
mingw-w64-ucrt-x86_64-gcc
mingw-w64-ucrt-x86_64-cmake
mingw-w64-ucrt-x86_64-ninja
- shell: pwsh
env:
MSYS2_LOCATION: ${{ steps.msys2.outputs.msys2-location }}
run: Add-Content -LiteralPath $env:GITHUB_PATH -Value "$env:MSYS2_LOCATION\ucrt64\bin"
- uses: hendrikmuhs/[email protected]
with:
key: windows-msys2
save: ${{ github.ref == 'refs/heads/main' }}
evict-old-files: job
- id: ffmpeg-cache
uses: actions/cache/restore@v6
with:
path: build/externals
key: ffmpeg-${{ runner.os }}-${{ steps.submodules.outputs.ffmpeg }}
- shell: cmd
run: cmake -S . -B build %CONFIGURE% "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown"
- if: steps.ffmpeg-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v6
continue-on-error: true
with:
path: build/externals
key: ${{ steps.ffmpeg-cache.outputs.cache-primary-key }}
- name: Check that every test executable is registered with ctest
shell: cmd
run: python tools/check_test_registration.py build
- shell: cmd
run: cmake --build build
- shell: cmd
run: cmake --build build --target libs
- shell: cmd
working-directory: build
run: ctest --output-on-failure --timeout 120 --parallel 4 --output-log ctest.log --output-junit ctest.xml
- run: python tools/check_skips.py build/ctest.xml --job windows
- name: Report tests slower than 30 s
if: ${{ !cancelled() }}
run: |
[ -f build/ctest.log ] || exit 0
awk '/Test +#[0-9]+: / && $NF == "sec" { for (i = 1; i <= NF; i++) if ($i ~ /^#[0-9]+:$/) name = $(i + 1); t = $(NF - 1) + 0; if (t > max[name]) max[name] = t }
END { for (name in max) if (max[name] > 30) printf "::warning title=Slow test::%s took %.1f s on %s; tests must finish in 30 s (CONTRIBUTING.md)\n", name, max[name], ENVIRON["RUNNER_OS"] }' build/ctest.log
- uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
name: windows-msys2-tests
path: |
build/ctest.log
build/ctest.xml
if-no-files-found: ignore
+1 -1
View File
@@ -42,7 +42,7 @@ This manual run also builds and tests the relinker on Linux, Windows and macOS w
With `--field full=false`, the manual run skips the full builds and runs only the relinker matrix on Linux, Windows and macOS, if the pull request changes a file the relinker-only build uses, such as the Python tools and their tests in `tools/`.
The Conventions check runs automatically for every contributor, including first-time contributors. It uses the base branch checker and reads pull request Git objects without executing pull request code. The Conventions check runs on every pull request and fails when a rule on this page is broken. It accepts code comments only when the pull request also changes [technical debt](docs/dev/TechnicalDebt.md), the index or a file in `docs/dev/debt/`, and only UTF-8 text files. Run it locally before pushing:
The Conventions check runs automatically for every contributor, including first-time contributors. It uses the checker from the current tip of the base branch, so rules added after a pull request was opened apply to its next run, and reads pull request Git objects without executing pull request code. The Conventions check runs on every pull request and fails when a rule on this page is broken. It accepts code comments only when the pull request also changes [technical debt](docs/dev/TechnicalDebt.md), the index or a file in `docs/dev/debt/`, and only UTF-8 text files. Run it locally before pushing:
```
python3 tools/check_conventions.py --base origin/main
@@ -19,6 +19,10 @@ struct Header {
std::uint32_t width;
std::uint32_t height;
std::uint32_t channels;
std::uint8_t componentIds[4]{};
std::uint8_t samplingFactors[4]{};
bool progressive = false;
bool app0BeforeFrame = false;
};
enum class Sampling {
+27 -1
View File
@@ -126,7 +126,33 @@ std::optional<Header> ParseHeader(std::span<const std::uint8_t> jpeg) {
int channels = 0;
if (!stbi_info_from_memory(jpeg.data(), static_cast<int>(jpeg.size()), &width, &height, &channels)) return std::nullopt;
if (width <= 0 || height <= 0 || channels <= 0) return std::nullopt;
return Header{static_cast<std::uint32_t>(width), static_cast<std::uint32_t>(height), static_cast<std::uint32_t>(channels)};
Header header{static_cast<std::uint32_t>(width), static_cast<std::uint32_t>(height), static_cast<std::uint32_t>(channels)};
std::size_t offset = 2;
while (offset < jpeg.size()) {
if (jpeg[offset++] != 0xFF) return std::nullopt;
while (offset < jpeg.size() && jpeg[offset] == 0xFF) ++offset;
if (offset == jpeg.size()) return std::nullopt;
const std::uint8_t marker = jpeg[offset++];
if (marker == 0xD9 || marker == 0xDA) return std::nullopt;
if (marker == 0x01 || (marker >= 0xD0 && marker <= 0xD8)) continue;
if (jpeg.size() - offset < 2) return std::nullopt;
const std::size_t length = (static_cast<std::size_t>(jpeg[offset]) << 8) | jpeg[offset + 1];
if (length < 2 || length > jpeg.size() - offset) return std::nullopt;
if (marker == 0xE0) header.app0BeforeFrame = true;
if (marker == 0xC0 || marker == 0xC1 || marker == 0xC2) {
if (length < 8) return std::nullopt;
header.channels = jpeg[offset + 7];
if (header.channels == 0 || header.channels > 4 || length != 8 + 3 * header.channels) return std::nullopt;
header.progressive = marker == 0xC2;
for (std::uint32_t i = 0; i < header.channels; ++i) {
header.componentIds[i] = jpeg[offset + 8 + 3 * i];
header.samplingFactors[i] = jpeg[offset + 9 + 3 * i];
}
return header;
}
offset += length;
}
return std::nullopt;
}
std::optional<Image> Decode(std::span<const std::uint8_t> jpeg) {
+34 -6
View File
@@ -39,9 +39,16 @@ if(MINGW)
check_cxx_source_compiles("#ifdef __USING_SJLJ_EXCEPTIONS__\n#error\n#endif\nint main() { return 0; }" ANYPS5_MINGW_DWARF_EXCEPTIONS)
unset(CMAKE_REQUIRED_FLAGS)
if(NOT ANYPS5_MINGW_DWARF_EXCEPTIONS)
message(FATAL_ERROR "${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION} selects SjLj exceptions with -fno-asynchronous-unwind-tables; the libraries need DWARF unwinding. Use the MinGW-w64 toolchain listed in docs/dev/BUILD.md.")
check_cxx_source_compiles("#if !defined(__SEH__) || defined(__USING_SJLJ_EXCEPTIONS__)\n#error\n#endif\n[[gnu::sysv_abi, gnu::noinline]] void Throw() { throw 1; }\n[[gnu::sysv_abi, gnu::noinline]] int Catch() { try { Throw(); } catch (int value) { return value; } return 0; }\nint main() { return Catch() != 1; }" ANYPS5_MINGW_SYSV_SEH_EXCEPTIONS)
if(NOT ANYPS5_MINGW_SYSV_SEH_EXCEPTIONS)
message(FATAL_ERROR "The Windows libraries require DWARF exceptions or GCC with System V ABI SEH support. Use the toolchains listed in docs/dev/BUILD.md.")
endif()
add_library(windows_seh_personalities OBJECT ${CMAKE_CURRENT_SOURCE_DIR}/prx/libc/src/specifics/windows/SehPersonalities.cpp)
target_link_options(windows_seh_personalities INTERFACE LINKER:--undefined=LibcSehPersonalities_nid_no_patch)
link_libraries(windows_seh_personalities)
else()
add_compile_options(-fno-asynchronous-unwind-tables)
endif()
add_compile_options(-fno-asynchronous-unwind-tables)
endif()
function(configure_windows_unwind target)
@@ -50,7 +57,7 @@ function(configure_windows_unwind target)
message(FATAL_ERROR "Windows DWARF unwinding requires objcopy")
endif()
add_custom_command(TARGET ${target} POST_BUILD
COMMAND "${CMAKE_OBJCOPY}" --rename-section .eh_frame=.ehfram "$<TARGET_FILE:${target}>"
COMMAND "${CMAKE_OBJCOPY}" --rename-section .eh_frame=.ehfram --rename-section .eh_fram=.ehfram "$<TARGET_FILE:${target}>"
VERBATIM
)
endif()
@@ -186,6 +193,8 @@ foreach(LIB_DIR ${LIBS_DIRS})
endif()
endforeach()
target_link_libraries(libSceUsbStorageDialog PRIVATE libSceUserService)
if(APS5_ENABLE_TRACY)
set_target_properties(TracyClient PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${UNPATCHED_DIR}
@@ -300,12 +309,18 @@ if(BUILD_TESTING)
target_link_libraries(guest_kernel_sys_tests PRIVATE libkernel_sys libkernel libc)
configure_windows_unwind(guest_kernel_sys_tests)
add_test(NAME guest_kernel_sys COMMAND guest_kernel_sys_tests)
add_test_executable(guest_kernel_sys_thread_defaults_tests tests/GuestKernelSysThreadDefaults.cpp)
target_include_directories(guest_kernel_sys_thread_defaults_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_kernel_sys_thread_defaults_tests PRIVATE libkernel_sys libkernel libc)
configure_windows_unwind(guest_kernel_sys_thread_defaults_tests)
add_test(NAME guest_kernel_sys_thread_defaults COMMAND guest_kernel_sys_thread_defaults_tests)
set_tests_properties(guest_kernel_sys_thread_defaults PROPERTIES TIMEOUT 20)
if(Python3_Interpreter_FOUND)
add_test(NAME nid_verneed COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_verneed.py $<TARGET_FILE:nid_patcher>)
endif()
if(WIN32)
set_tests_properties(guest_kernel_sys PROPERTIES ENVIRONMENT_MODIFICATION
set_tests_properties(guest_kernel_sys guest_kernel_sys_thread_defaults PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel_sys>")
endif()
if(APPLE)
@@ -325,6 +340,9 @@ if(BUILD_TESTING)
if(WIN32)
add_test_executable(undeliverable_host_exception_tests tests/UndeliverableHostException.cpp)
if(MINGW)
target_compile_options(undeliverable_host_exception_tests PRIVATE -fno-asynchronous-unwind-tables -fno-exceptions)
endif()
add_dependencies(undeliverable_host_exception_tests libc)
add_test(NAME undeliverable_host_exception COMMAND undeliverable_host_exception_tests $<TARGET_FILE:libc>)
set_tests_properties(undeliverable_host_exception PROPERTIES
@@ -360,6 +378,16 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceVoice>")
endif()
add_test_executable(guest_gnm_neo_retail_tests tests/GuestGnmNeoRetail.cpp)
target_include_directories(guest_gnm_neo_retail_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_gnm_neo_retail_tests PRIVATE libSceGnmDriverForNeoMode libc)
configure_windows_unwind(guest_gnm_neo_retail_tests)
add_test(NAME guest_gnm_neo_retail COMMAND guest_gnm_neo_retail_tests)
if(WIN32)
set_tests_properties(guest_gnm_neo_retail PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceGnmDriverForNeoMode>")
endif()
add_test_executable(guest_move_tests tests/GuestMove.cpp)
target_include_directories(guest_move_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_move_tests PRIVATE libSceMove libc)
@@ -382,7 +410,7 @@ if(BUILD_TESTING)
add_test_executable(guest_offline_imports_tests tests/GuestOfflineImports.cpp)
target_include_directories(guest_offline_imports_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_offline_imports_tests PRIVATE libSceHttp libSceNpEntitlementAccess libSceRudp libc)
target_link_libraries(guest_offline_imports_tests PRIVATE libSceHttp libSceNpEntitlementAccess libSceRudp libSceVideoRecordingP libc)
configure_windows_unwind(guest_offline_imports_tests)
add_test(NAME guest_offline_imports COMMAND guest_offline_imports_tests)
if(Python3_Interpreter_FOUND)
@@ -406,7 +434,7 @@ if(BUILD_TESTING)
add_test_executable(guest_usb_storage_dialog_tests tests/GuestUsbStorageDialog.cpp)
target_include_directories(guest_usb_storage_dialog_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_usb_storage_dialog_tests PRIVATE libSceUsbStorageDialog libc)
target_link_libraries(guest_usb_storage_dialog_tests PRIVATE libSceUsbStorageDialog libSceUserService libc)
configure_windows_unwind(guest_usb_storage_dialog_tests)
add_test(NAME guest_usb_storage_dialog COMMAND guest_usb_storage_dialog_tests)
if(WIN32)
+192 -21
View File
@@ -333,7 +333,7 @@ struct VaList {
};
#pragma pack(pop)
using FiberEntry = void* (*)(std::uint64_t, void*);
using FiberEntry = void (APS5_VABI*)(std::uint64_t argOnInitialize, std::uint64_t argOnRun);
struct FiberOptParam {
std::uint32_t magic;
@@ -432,6 +432,48 @@ struct AcmBatchError {
std::uint32_t reserved[8];
};
struct AcmReverbBuffer {
void* samples;
const void* blockMetadata;
};
struct AcmReverbInput {
std::uint32_t blockSize;
std::uint32_t blockCount;
std::uint32_t channelCount;
std::uint32_t sampleFormat;
std::uint32_t currentBlock;
std::uint32_t availableBlocks;
const AcmReverbBuffer* const* history;
const void* const* inputChannels;
std::uint32_t pendingBlocks;
};
struct AcmReverbFilter {
std::uint32_t blockSize;
std::uint32_t blockCount;
std::uint32_t channelCount;
std::uint32_t sampleFormat;
const void* blockMetadata;
const AcmReverbBuffer* const* channels;
};
struct AcmReverbOutput {
std::uint32_t blockSize;
std::uint32_t channelCount;
void* const* samples;
void* const* workspace;
float gain;
float previousGain;
void* const* previousSamples;
};
static_assert(sizeof(AcmReverbBuffer) == 16);
static_assert(offsetof(AcmReverbInput, history) == 24);
static_assert(offsetof(AcmReverbInput, pendingBlocks) == 40);
static_assert(sizeof(AcmReverbFilter) == 32);
static_assert(offsetof(AcmReverbOutput, previousSamples) == 32);
struct AcmBatchInfo {
void* buffer;
std::size_t offset;
@@ -933,13 +975,60 @@ struct AudiodecM4aacInfo {
std::int32_t i_result;
};
struct VoiceEvent {
std::int32_t type;
std::uint32_t reserved;
void* userData;
union {
struct {
std::uint16_t count;
std::uint16_t ports[7];
} dataReady;
struct {
std::uint16_t port;
std::uint8_t state;
std::uint8_t reserved[13];
} deviceState;
};
};
using VoiceEventHandler = void (APS5_VABI*)(const VoiceEvent* event);
struct VoiceThreadsParams {
std::uint32_t mode;
std::int32_t firstPriority;
std::uint64_t firstAffinity;
std::int32_t secondPriority;
std::uint32_t reserved20;
std::uint64_t secondAffinity;
std::int32_t thirdPriority;
std::uint32_t reserved36;
std::uint64_t thirdAffinity;
};
static_assert(sizeof(VoiceEvent) == 32);
static_assert(offsetof(VoiceEvent, userData) == 8);
static_assert(offsetof(VoiceEvent, dataReady) == 16);
static_assert(offsetof(VoiceEvent, deviceState) == 16);
static_assert(sizeof(VoiceThreadsParams) == 48);
static_assert(offsetof(VoiceThreadsParams, firstAffinity) == 8);
static_assert(offsetof(VoiceThreadsParams, secondPriority) == 16);
static_assert(offsetof(VoiceThreadsParams, secondAffinity) == 24);
static_assert(offsetof(VoiceThreadsParams, thirdPriority) == 32);
static_assert(offsetof(VoiceThreadsParams, thirdAffinity) == 40);
struct VoiceInitParam {
std::int32_t app_type;
std::uint64_t on_event;
VoiceEventHandler on_event;
void* user_data;
std::uint8_t reserved[32 - sizeof(std::int32_t) - sizeof(std::uint64_t) - sizeof(void*)];
std::uint8_t reserved[16];
};
static_assert(sizeof(VoiceInitParam) == 40);
static_assert(offsetof(VoiceInitParam, on_event) == 8);
static_assert(offsetof(VoiceInitParam, user_data) == 16);
static_assert(offsetof(VoiceInitParam, reserved) == 24);
struct VoicePortParam {
std::int32_t port_type;
std::uint16_t threshold;
@@ -962,14 +1051,30 @@ struct VoicePortInfo {
std::uint16_t reserved;
};
struct VoiceResourceInfo {
std::uint16_t max_voice_input_ports;
std::uint16_t max_voice_output_ports;
std::uint16_t max_device_input_ports;
std::uint16_t max_device_output_ports;
std::uint16_t max_ports;
struct NpAuthClientId;
struct NpAuthAuthorizedAppCodeParameter {
std::uint64_t size;
std::int32_t userId;
const NpAuthClientId* clientId;
const char* scope;
std::uint32_t option;
};
static_assert(sizeof(NpAuthAuthorizedAppCodeParameter) == 40);
static_assert(offsetof(NpAuthAuthorizedAppCodeParameter, clientId) == 16);
static_assert(offsetof(NpAuthAuthorizedAppCodeParameter, scope) == 24);
static_assert(offsetof(NpAuthAuthorizedAppCodeParameter, option) == 32);
struct VoiceResourceInfo {
std::uint16_t maxVoiceInputPorts;
std::uint16_t maxVoiceOutputPorts;
std::uint16_t maxDeviceInputPorts;
std::uint16_t maxDeviceOutputPorts;
};
static_assert(sizeof(VoiceResourceInfo) == 8);
struct VoiceStartParam {
void* container;
std::uint32_t mem_size;
@@ -1155,8 +1260,8 @@ struct ImeKeycode {
std::uint64_t timestamp;
};
using ImeTextFilter = std::int32_t (APS5_VABI *)(char16_t* out_text, std::uint32_t* out_text_length, const char16_t* source_text, std::uint32_t source_text_length);
using ImeExtKeyboardFilter = int (APS5_VABI *)(const ImeKeycode* source_keycode, std::uint16_t* out_keycode, std::uint32_t* out_status, void* reserved);
using ImeTextFilter = std::int32_t (APS5_VABI*)(char16_t* outText, std::uint32_t* outTextLength, const char16_t* sourceText, std::uint32_t sourceTextLength);
using ImeExtKeyboardFilter = int (APS5_VABI*)(const ImeKeycode* sourceKeycode, std::uint16_t* outKeycode, std::uint32_t* outStatus, void* reserved);
struct ImeExtendedParam {
std::uint32_t option;
@@ -1318,7 +1423,12 @@ struct NetEpollEvent {
NetEpollData data;
};
struct NetCtlNatInfo { std::uint8_t opaque[128]; };
struct NetCtlNatInfo {
std::uint32_t size;
std::uint8_t opaque[12];
};
static_assert(sizeof(NetCtlNatInfo) == 16);
union NetCtlInfo { std::uint8_t opaque[256]; };
@@ -1333,10 +1443,10 @@ using NetCtlCallback = void (APS5_VABI *)(int, void*);
struct HttpEpoll;
using HttpEpollHandle = HttpEpoll*;
using HttpsCallback = int (*)(int, unsigned int, void* const*, int, void*);
using HttpRedirectCallback = int (*)(int, std::int32_t, std::int32_t*, const char*, void*);
using HttpCookieRecvCallback = int (*)(int, const char*, const char*, std::uint64_t, void*);
using HttpAuthInfoCallback = int (*)(int, int, const char*, char*, char*, int, std::uint8_t**, std::uint64_t*, int*, void*);
using HttpsCallback = int (APS5_VABI*)(int, unsigned int, void* const*, int, void*);
using HttpRedirectCallback = int (APS5_VABI*)(int, std::int32_t, std::int32_t*, const char*, void*);
using HttpCookieRecvCallback = int (APS5_VABI*)(int, const char*, const char*, std::uint64_t, void*);
using HttpAuthInfoCallback = int (APS5_VABI*)(int, int, const char*, char*, char*, int, std::uint8_t**, std::uint64_t*, int*, void*);
struct HttpNBEvent { std::uint8_t opaque[64]; };
@@ -1395,6 +1505,24 @@ struct NpUnifiedEntitlementLabel {
char padding[3];
};
struct NpServiceEntitlementLabel {
char data[7];
std::uint8_t reserved[13];
};
struct NpEntitlementAccessTransactionId {
char data[65];
std::uint8_t reserved[7];
};
struct NpEntitlementAccessListParameter;
struct NpEntitlementAccessUnifiedInfo;
struct NpEntitlementAccessServiceInfo;
static_assert(sizeof(NpUnifiedEntitlementLabel) == 20);
static_assert(sizeof(NpServiceEntitlementLabel) == 20);
static_assert(sizeof(NpEntitlementAccessTransactionId) == 72);
struct NpEntitlementAccessInitParam { char reserved[32]; };
struct NpEntitlementAccessBootParam { char reserved[32]; };
@@ -1690,6 +1818,34 @@ struct AppContentBootParam {
struct AppContentMountPoint { char data[16]; };
using ContentExportMallocCallback = void* (APS5_VABI *)(std::size_t size, void* userData);
using ContentExportFreeCallback = void (APS5_VABI *)(void* memory, void* userData);
using ContentExportDataCallback = std::int32_t (APS5_VABI *)(const void** data, std::size_t* size, void* userData);
struct ContentExportInitParam {
ContentExportMallocCallback mallocFunc;
ContentExportFreeCallback freeFunc;
void* userData;
};
static_assert(sizeof(ContentExportInitParam) == 24);
static_assert(offsetof(ContentExportInitParam, freeFunc) == 8);
static_assert(offsetof(ContentExportInitParam, userData) == 16);
struct ContentExportSfParam {
std::size_t size;
const char* metadata;
const std::int64_t* contentIds;
std::uint32_t contentCount;
std::uint32_t option;
};
static_assert(sizeof(ContentExportSfParam) == 32);
static_assert(offsetof(ContentExportSfParam, metadata) == 8);
static_assert(offsetof(ContentExportSfParam, contentIds) == 16);
static_assert(offsetof(ContentExportSfParam, contentCount) == 24);
static_assert(offsetof(ContentExportSfParam, option) == 28);
struct ContentExportInitParam2 {
void* malloc_func;
void* free_func;
@@ -1897,11 +2053,22 @@ union PlayGoOptionalChunk {
};
struct PlayGoDialogResult {
std::int32_t errorCode;
std::int32_t result;
std::int32_t reserved[9];
std::uint8_t reserved[32];
};
using RudpEventHandler = void (*)(int ctx_id, int event_id, int error_code, void* arg);
struct RudpPollEvent {
std::int32_t contextId;
std::uint16_t events;
std::uint16_t returnedEvents;
};
static_assert(sizeof(RudpPollEvent) == 8);
static_assert(offsetof(RudpPollEvent, events) == 4);
static_assert(offsetof(RudpPollEvent, returnedEvents) == 6);
using RudpEventHandler = void (APS5_VABI*)(int ctxId, int eventId, int errorCode, void* arg);
struct SystemServiceStatus {
std::int32_t event_num = 0;
@@ -1998,12 +2165,16 @@ struct UserServiceGamePresets {
std::uint32_t audio_language;
};
struct UltMutexOptParam {
std::uint32_t reserved_header[2];
struct alignas(8) UltMutexOptParam {
std::uint32_t header[3];
std::uint32_t buildVersion;
std::uint32_t attribute;
std::uint32_t reserved0;
std::uint32_t reserved[27];
};
static_assert(sizeof(UltMutexOptParam) == 128);
static_assert(offsetof(UltMutexOptParam, attribute) == 16);
struct UltUlthreadRuntimeOptParam { std::uint8_t bytes[128]; };
using UltUlthreadEntry = std::int32_t (APS5_VABI *)(std::uint64_t);
+115 -4
View File
@@ -79,19 +79,130 @@ int APS5_VABI sceAcmContextDestroy(AcmContextId context) {
return 0;
}
int APS5_VABI sceAcm_ConvReverb_SharedInput(void) {
int APS5_VABI sceAcm_ConvReverb_SharedInput(AcmBatchInfo* batch, std::uint32_t mode, AcmReverbInput* input, std::uint32_t outputCount, const AcmReverbFilter* const* filters, const float* gains, const AcmReverbOutput* const* outputs) {
return 0;
}
int APS5_VABI sceAcm_FFT(void) {
int APS5_VABI sceAcm_FFT(AcmBatchInfo* batch, std::int32_t transformSize, std::uint32_t transformCount, std::int32_t inputFormat, const void* const* inputs, std::int32_t outputFormat, void* const* outputs, std::int32_t flags) {
if (!batch || transformCount == 0 || transformCount > 0x7FFFFFFF || !inputs || !outputs || (flags & 3) == 3 || inputFormat < 0 || inputFormat > 1 || outputFormat < 0 || outputFormat > 1) return SCE_ACM_ERROR_BAD_POINTER;
if (transformSize != 512 && transformSize != 1024 && transformSize != 2048) return SCE_ACM_ERROR_BAD_POINTER;
const bool realInput = (flags & 1) != 0;
if (transformSize == 2048 && !realInput) return SCE_ACM_ERROR_BAD_POINTER;
const std::uint32_t inputBytes = transformSize * (inputFormat == 1 ? 2 : 4);
const std::uint32_t outputBytes = transformSize * (outputFormat == 1 ? 2 : 4);
const std::uint32_t inputBlocks = (inputBytes >> 9) >> (realInput ? 1 : 0);
const std::uint32_t outputBlocks = outputBytes >> 9;
std::uint32_t transformCommand;
std::uint32_t inputCommand;
std::uint32_t outputCommand;
std::uint32_t setupCommand;
std::uint32_t sizeCode;
if (transformSize == 512) {
sizeCode = 4;
transformCommand = realInput ? 7 : 11;
inputCommand = realInput ? 2 * inputFormat : 9 + inputFormat;
outputCommand = 4 | outputFormat;
setupCommand = realInput ? 6 : 12;
} else if (transformSize == 1024) {
sizeCode = 5;
transformCommand = realInput ? 8 : 13;
inputCommand = realInput ? 2 * inputFormat : 14 | inputFormat;
outputCommand = 5 | (2 * outputFormat);
setupCommand = realInput ? 10 : 12;
} else {
sizeCode = 6;
transformCommand = 6;
inputCommand = 2 * inputFormat;
outputCommand = 4 | outputFormat;
setupCommand = 8;
}
auto* buffer = static_cast<std::byte*>(batch->buffer);
auto offset = batch->offset;
const auto capacity = batch->buffer_size;
const auto appendWord = [&](std::uint64_t value) {
if (offset > capacity || capacity - offset < sizeof(value)) return false;
std::memcpy(buffer + offset, &value, sizeof(value));
offset += sizeof(value);
return true;
};
std::uint64_t header = static_cast<std::uint64_t>(sizeCode) << 8;
if (!(flags & 8)) {
const auto encodedCount = transformCount < 64000 ? transformCount : 64000;
header |= (std::uint64_t{1} << 16) | (static_cast<std::uint64_t>(transformSize) << 8) | (static_cast<std::uint64_t>(flags & 6) << 19) | (static_cast<std::uint64_t>(flags & 1) << 23) | (std::uint64_t{1} << (inputFormat == 1 ? 24 : 26)) | (std::uint64_t{1} << (outputFormat == 1 ? 25 : 27)) | (static_cast<std::uint64_t>(encodedCount) << 28);
}
constexpr int noSpace = static_cast<int>(0x81940009);
constexpr int badAlignment = static_cast<int>(0x8194000A);
if (!appendWord(header) || !appendWord(10 | (setupCommand << 8))) return noSpace;
for (std::uint32_t index = 0; index < transformCount; ++index) {
const auto inputIndex = static_cast<std::size_t>(index) * ((flags & 2) ? 2 : 1);
const auto input = reinterpret_cast<std::uintptr_t>(inputs[inputIndex]);
const auto secondInput = (flags & 2) ? reinterpret_cast<std::uintptr_t>(inputs[inputIndex + 1]) : 0;
if (!(flags & 2) && !input) return SCE_ACM_ERROR_BAD_POINTER;
if ((input | secondInput) & 127) return badAlignment;
const auto inputRecordSize = (flags & 2) ? 24u : 16u;
if (offset > capacity || capacity - offset < inputRecordSize) return noSpace;
appendWord(((flags & 2) ? 7 : 6) | (static_cast<std::uint64_t>(inputBlocks) << 32) | ((flags & 2) ? static_cast<std::uint64_t>(inputBytes / 2) << 48 : 0));
appendWord(input);
if (flags & 2) appendWord(secondInput);
if (!appendWord(10 | (inputCommand << 8)) || !appendWord(10 | (transformCommand << 8)) || !appendWord(10 | (outputCommand << 8))) return noSpace;
const auto outputIndex = static_cast<std::size_t>(index) * ((flags & 4) ? 2 : 1);
const auto output = reinterpret_cast<std::uintptr_t>(outputs[outputIndex]);
const auto secondOutput = (flags & 4) ? reinterpret_cast<std::uintptr_t>(outputs[outputIndex + 1]) : 0;
if ((output | secondOutput) & 127) return badAlignment;
const auto outputRecordSize = (flags & 4) ? 24u : 16u;
if (offset > capacity || capacity - offset < outputRecordSize) return noSpace;
appendWord(((flags & 4) ? 9 : 8) | (static_cast<std::uint64_t>(outputBlocks) << 32) | ((flags & 4) ? static_cast<std::uint64_t>(outputBytes / 2) << 48 : 0));
appendWord(output);
if (flags & 4) appendWord(secondOutput);
}
batch->offset = offset;
return 0;
}
int APS5_VABI sceAcm_Panner(void) {
int APS5_VABI sceAcm_Panner(AcmBatchInfo* batch, std::uint32_t sourceCount, const void* const* sources, std::uint32_t sourceMode, std::uint32_t outputMode, std::uint32_t channelCount, const void* const* sourceParameters, const void* const* sourceMatrices, const void* const* inputChannels, void* const* outputChannels) {
if (!batch || sourceCount == 0 || sourceCount > 65535 || sourceMode > 2 || outputMode > 1 || channelCount == 0 || channelCount > 36 || !sources || !sourceParameters || !sourceMatrices || !outputChannels) return SCE_ACM_ERROR_BAD_POINTER;
std::uintptr_t alignment = reinterpret_cast<std::uintptr_t>(sources) | reinterpret_cast<std::uintptr_t>(sourceParameters) | reinterpret_cast<std::uintptr_t>(sourceMatrices) | reinterpret_cast<std::uintptr_t>(inputChannels) | reinterpret_cast<std::uintptr_t>(outputChannels);
for (std::uint32_t index = 0; index < sourceCount; ++index) {
if (!sources[index] || !sourceParameters[index] || !sourceMatrices[index]) return SCE_ACM_ERROR_BAD_POINTER;
std::uint32_t parameterMode;
std::memcpy(&parameterMode, sourceParameters[index], sizeof(parameterMode));
if (parameterMode > 1) return SCE_ACM_ERROR_BAD_POINTER;
alignment |= reinterpret_cast<std::uintptr_t>(sources[index]) | reinterpret_cast<std::uintptr_t>(sourceParameters[index]) | reinterpret_cast<std::uintptr_t>(sourceMatrices[index]);
}
for (std::uint32_t index = 0; index < channelCount; ++index) {
if (!outputChannels[index]) return SCE_ACM_ERROR_BAD_POINTER;
alignment |= reinterpret_cast<std::uintptr_t>(outputChannels[index]);
if (inputChannels) alignment |= reinterpret_cast<std::uintptr_t>(inputChannels[index]);
}
if (alignment & 127) return static_cast<int>(0x8194000A);
const auto offset = batch->offset;
const auto capacity = batch->buffer_size;
if (offset > capacity || capacity - offset < 8) return static_cast<int>(0x81940009);
auto* buffer = static_cast<std::byte*>(batch->buffer);
const auto writeWord = [buffer](std::size_t position, std::uint64_t value) { std::memcpy(buffer + position, &value, sizeof(value)); };
const auto encodedCount = sourceCount < 255 ? sourceCount : 255;
writeWord(offset, (static_cast<std::uint64_t>(channelCount) << 24) | (static_cast<std::uint64_t>(encodedCount) << 16) | (std::uint64_t{1} << (sourceMode + 32)) | (std::uint64_t{1} << (outputMode + 35)) | 0x300);
if (capacity < 128 || capacity - offset < 152) return static_cast<int>(0x81940009);
const auto descriptorOffset = capacity - 128;
std::memset(buffer + descriptorOffset, 0, 128);
writeWord(descriptorOffset, sourceCount);
writeWord(descriptorOffset + 8, reinterpret_cast<std::uintptr_t>(inputChannels));
writeWord(descriptorOffset + 16, reinterpret_cast<std::uintptr_t>(outputChannels));
writeWord(descriptorOffset + 24, reinterpret_cast<std::uintptr_t>(sources));
writeWord(descriptorOffset + 32, sourceMode | (static_cast<std::uint64_t>(outputMode) << 32));
writeWord(descriptorOffset + 40, channelCount);
writeWord(descriptorOffset + 48, reinterpret_cast<std::uintptr_t>(sourceParameters));
writeWord(descriptorOffset + 56, reinterpret_cast<std::uintptr_t>(sourceMatrices));
writeWord(offset + 8, 0x8000000000000D);
writeWord(offset + 16, reinterpret_cast<std::uintptr_t>(buffer + descriptorOffset));
if (capacity - offset < 160) return static_cast<int>(0x81940009);
writeWord(offset + 24, 10);
batch->offset = offset + 32;
batch->buffer_size = descriptorOffset;
return 0;
}
int APS5_VABI sceAcmBatchJobPriority(void) {
int APS5_VABI sceAcmBatchJobPriority(AcmBatchInfo* batch, std::uint32_t priority) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
@@ -21,7 +21,7 @@ std::uint32_t* APS5_VABI sceAgcDcbGetLodStats(CommandBuffer* buf, std::uint8_t c
return Agc::Command::Emit(buf, 0x10u, {bufferSizeInBytes, static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u), control}, __func__);
}
std::uint32_t APS5_VABI sceAgcDcbBeginOcclusionQueryGetSize() {
std::uint32_t APS5_VABI sceAgcDcbBeginOcclusionQueryGetSize(std::int32_t queryType) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
+7 -18
View File
@@ -13,20 +13,12 @@ namespace {
constexpr std::uint64_t semaphoreMemoryAlignment = 0x4000u;
constexpr std::uint64_t semaphoreLabelBytes = 32u;
constexpr std::uint64_t semaphoreCounterBytes = 8u;
std::mutex semaphoreMutex;
std::uint8_t* semaphoreMemory = nullptr;
std::uint64_t semaphoreBytes = 0;
bool hasOnlyZeroLabelTails(const std::uint8_t* memory, std::uint64_t sizeInBytes) {
for (std::uint64_t label = 0; label < sizeInBytes; label += semaphoreLabelBytes) {
for (std::uint64_t byte = semaphoreCounterBytes; byte < semaphoreLabelBytes; ++byte) {
if (memory[label + byte] != 0) return false;
}
}
return true;
}
}
@@ -35,15 +27,12 @@ extern "C" {
int APS5_VABI sceAgcSetAmmSemaphoreMemory(void* memory, std::uint64_t sizeInBytes) {
const auto address = reinterpret_cast<std::uintptr_t>(memory);
std::lock_guard lock(semaphoreMutex);
Agc::Command::Require(semaphoreMemory == nullptr, __func__, "the AMM semaphore memory is already registered; the console's error is unknown");
if (semaphoreBytes != 0) return static_cast<int>(0x8a6c0048u);
if (((address | sizeInBytes) & (semaphoreMemoryAlignment - 1u)) != 0) return static_cast<int>(0x8a6c0002u);
Agc::Command::Require(memory != nullptr && sizeInBytes != 0, __func__, "null or empty AMM semaphore memory; the console's error is unknown");
Agc::Command::Require(((address | sizeInBytes) & (semaphoreMemoryAlignment - 1u)) == 0, __func__, "AMM semaphore memory or size is not 16 KiB aligned; the console's error is unknown");
Agc::Command::Require(sizeInBytes - 1u <= std::numeric_limits<std::uintptr_t>::max() - address, __func__, "AMM semaphore memory wraps the address space");
auto* labels = static_cast<std::uint8_t*>(memory);
Agc::Command::Require(hasOnlyZeroLabelTails(labels, sizeInBytes), __func__, "AMM semaphore labels hold data past their first 8 bytes; whether registration clears it is unknown");
for (std::uint64_t label = 0; label < sizeInBytes; label += semaphoreLabelBytes) {
std::memset(labels + label, 0, semaphoreCounterBytes);
}
std::memset(labels, 0, sizeInBytes);
semaphoreMemory = labels;
semaphoreBytes = sizeInBytes;
return 0;
@@ -51,9 +40,9 @@ int APS5_VABI sceAgcSetAmmSemaphoreMemory(void* memory, std::uint64_t sizeInByte
int APS5_VABI sceAgcGetSemaphoreLabel(std::uint32_t index, void** label) {
std::lock_guard lock(semaphoreMutex);
Agc::Command::Require(semaphoreMemory != nullptr, __func__, "no AMM semaphore memory is registered; the console's error is unknown");
Agc::Command::Require(index < semaphoreBytes / semaphoreLabelBytes, __func__, "the semaphore label lies outside the AMM semaphore memory; the console's error is unknown");
Agc::Command::Require(label != nullptr, __func__, "null semaphore label output; the console's error is unknown");
if (semaphoreBytes == 0) return static_cast<int>(0x8a6c0049u);
if (index >= semaphoreBytes / semaphoreLabelBytes) return static_cast<int>(0x8a6c000bu);
Agc::Command::Require(label != nullptr, __func__, "null semaphore label output");
*label = semaphoreMemory + static_cast<std::uint64_t>(index) * semaphoreLabelBytes;
return 0;
}
@@ -35,7 +35,6 @@ constexpr std::size_t FusedCodeAlignment = 256;
constexpr char TrailerMagic[8] = {'b', 'a', 'r', 'e', 'f', 'o', 'o', 't'};
constexpr std::size_t TrailerCodeBytesOffset = sizeof(TrailerMagic) + 3 * sizeof(std::uint32_t);
constexpr std::uint32_t Rsrc1VgprMask = 0x3fu;
constexpr std::uint32_t Rsrc1SgprMask = 0xfu << 6u;
constexpr std::uint32_t Rsrc2OffchipLdsEnable = 1u << 18u;
constexpr std::uint32_t Rsrc2UserSgprMask = (0x1fu << 1u) | (1u << 27u);
@@ -121,15 +120,19 @@ std::uint32_t FrontProgramBytes(const char* function, const Shader* front) {
return static_cast<std::uint32_t>((end - 1) * 4);
}
std::uint32_t MergeRsrc1(std::uint32_t back, std::uint32_t front) {
const auto vgprs = std::max(back & Rsrc1VgprMask, front & Rsrc1VgprMask);
const auto sgprs = std::max(back & Rsrc1SgprMask, front & Rsrc1SgprMask);
return (back & ~(Rsrc1VgprMask | Rsrc1SgprMask)) | vgprs | sgprs;
}
std::uint32_t MergeRsrc2(const char* function, std::uint32_t back, std::uint32_t front) {
if ((front & ~(Rsrc2UserSgprMask | Rsrc2OffchipLdsEnable) & ~back) != 0) Fail(function, "front half requires hardware state the back half does not enable");
return (back & ~Rsrc2UserSgprMask) | (front & (Rsrc2UserSgprMask | Rsrc2OffchipLdsEnable));
std::array<std::uint32_t, 2> mergeGeometryResources(std::uint32_t frontRsrc1, std::uint32_t frontRsrc2, std::uint32_t backRsrc1, std::uint32_t backRsrc2) {
const auto frontVgprs = ((frontRsrc1 & Rsrc1VgprMask) + 1u) * 4u;
const auto backVgprs = ((backRsrc1 & Rsrc1VgprMask) + 1u) * 4u;
const auto frontTotal = frontVgprs + (frontRsrc2 >> 28u) * 8u;
const auto backTotal = backVgprs + (backRsrc2 >> 28u) * 8u;
const auto maxTotal = std::max(frontTotal, backTotal);
const auto shared = std::max(frontVgprs, backVgprs) >= maxTotal ? 0u : (maxTotal - std::min(frontTotal, backTotal) + 7u) / 64u;
const auto vgprs = std::max(frontRsrc1 & Rsrc1VgprMask, backRsrc1 & Rsrc1VgprMask);
const auto rsrc1Field = std::max((frontRsrc1 >> 29u) & 3u, (backRsrc1 >> 29u) & 3u);
const auto rsrc2Field = std::max((frontRsrc2 >> 16u) & 3u, (backRsrc2 >> 16u) & 3u);
const auto rsrc1 = (backRsrc1 & ~(Rsrc1VgprMask | (3u << 29u))) | vgprs | (rsrc1Field << 29u);
const auto rsrc2 = (backRsrc2 & ~(0xf0000000u | (3u << 16u) | Rsrc2UserSgprMask | Rsrc2OffchipLdsEnable)) | ((shared & 15u) << 28u) | (rsrc2Field << 16u) | (frontRsrc2 & (Rsrc2UserSgprMask | Rsrc2OffchipLdsEnable));
return {rsrc1, rsrc2};
}
const ShaderRegister* FindHalfRegister(const Shader* shader, std::uint32_t offset) {
@@ -149,10 +152,14 @@ GeometryLayout ComputeGeometryLayout(const char* function, const Shader* front,
const auto* frontRsrc1 = FindHalfRegister(front, SPI_SHADER_PGM_RSRC1_GS);
const auto* frontRsrc2 = FindHalfRegister(front, SPI_SHADER_PGM_RSRC2_GS);
if (frontRsrc1 == nullptr || frontRsrc2 == nullptr) Fail(function, "front half has no GS resource registers");
const auto* backRsrc1 = FindHalfRegister(back, SPI_SHADER_PGM_RSRC1_GS);
const auto* backRsrc2 = FindHalfRegister(back, SPI_SHADER_PGM_RSRC2_GS);
if (backRsrc1 == nullptr || backRsrc2 == nullptr) Fail(function, "back half has no GS resource registers");
const auto resources = mergeGeometryResources(frontRsrc1->value, frontRsrc2->value, backRsrc1->value, backRsrc2->value);
layout.sh.assign(back->sh_registers, back->sh_registers + back->num_sh_registers);
for (auto& reg : layout.sh) {
if (reg.offset == SPI_SHADER_PGM_RSRC1_GS) reg.value = MergeRsrc1(reg.value, frontRsrc1->value);
else if (reg.offset == SPI_SHADER_PGM_RSRC2_GS) reg.value = MergeRsrc2(function, reg.value, frontRsrc2->value);
if (reg.offset == SPI_SHADER_PGM_RSRC1_GS) reg.value = resources[0];
else if (reg.offset == SPI_SHADER_PGM_RSRC2_GS) reg.value = resources[1];
}
for (std::uint32_t i = 0; i < front->num_sh_registers; ++i) {
const auto& reg = front->sh_registers[i];
@@ -45,34 +45,29 @@ extern "C" {
int APS5_VABI sceAgcWaitRegMemPatchAddress(std::uint32_t* cmd, const volatile void* address) {
auto* wait = Agc::Command::ValidateWait(cmd, __func__);
const auto guestAddress = reinterpret_cast<std::uintptr_t>(address);
const auto alignment = ((wait[0] >> 8u) & 0xffu) == 0x3cu ? 4u : 8u;
Agc::Command::CheckAddress(guestAddress, alignment, __func__);
Agc::Command::CheckBits(guestAddress, 0xffffffffffffull, __func__);
cmd[2] = (cmd[2] & 0xffff0000u) | static_cast<std::uint32_t>(guestAddress >> 32u);
const auto lowMask = ((wait[0] >> 8u) & 0xffu) == 0x3cu ? 3u : 7u;
cmd[2] = (cmd[2] & 0xffff0000u) | (static_cast<std::uint32_t>(guestAddress >> 32u) & 0xffffu);
cmd[3] = static_cast<std::uint32_t>(guestAddress);
wait[2] = static_cast<std::uint32_t>(guestAddress);
wait[3] = static_cast<std::uint32_t>(guestAddress >> 32u);
wait[2] = (static_cast<std::uint32_t>(guestAddress) & ~lowMask) | (wait[2] & lowMask);
wait[3] = (static_cast<std::uint32_t>(guestAddress >> 32u) & 0x3ffffu) | (wait[3] & 0xfffc0000u);
return 0;
}
int APS5_VABI sceAgcWaitRegMemPatchReference(std::uint32_t* cmd, std::uint64_t reference) {
int APS5_VABI sceAgcWaitRegMemPatchReference(std::uint32_t* cmd, std::uint32_t reference) {
auto* wait = Agc::Command::ValidateWait(cmd, __func__);
Agc::Command::CheckBits(reference, 0xffffffffu, __func__);
wait[4] = static_cast<std::uint32_t>(reference);
return 0;
}
int APS5_VABI sceAgcWaitRegMemPatchMask(std::uint32_t* cmd, std::uint64_t mask) {
int APS5_VABI sceAgcWaitRegMemPatchMask(std::uint32_t* cmd, std::uint32_t mask) {
auto* wait = Agc::Command::ValidateWait(cmd, __func__);
Agc::Command::CheckBits(mask, 0xffffffffu, __func__);
wait[((wait[0] >> 8u) & 0xffu) == 0x3cu ? 5 : 6] = static_cast<std::uint32_t>(mask);
return 0;
}
int APS5_VABI sceAgcWaitRegMemPatchCompareFunction(std::uint32_t* cmd, std::uint8_t compareFunction) {
auto* wait = Agc::Command::ValidateWait(cmd, __func__);
Agc::Command::Require(compareFunction <= 6, __func__, "invalid wait comparison");
wait[1] = (wait[1] & ~7u) | compareFunction;
wait[1] = (wait[1] & ~7u) | (compareFunction & 7u);
return 0;
}
+8 -8
View File
@@ -5,42 +5,42 @@
extern "C" {
int APS5_VABI sceAgcAcbPushMarkerSpan() {
std::uint32_t* APS5_VABI sceAgcAcbPushMarkerSpan(CommandBuffer* buffer, const char* text, std::uint32_t length, std::uint32_t color) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcAcbSetMarkerSpan() {
std::uint32_t* APS5_VABI sceAgcAcbSetMarkerSpan(CommandBuffer* buffer, const char* text, std::uint32_t length, std::uint32_t color) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcAcquireMemSetEngine() {
int APS5_VABI sceAgcAcquireMemSetEngine(std::uint32_t* packet, std::uint8_t engine) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDcbPushMarkerSpan() {
std::uint32_t* APS5_VABI sceAgcDcbPushMarkerSpan(CommandBuffer* buffer, const char* text, std::uint32_t length, std::uint32_t color) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDcbSetMarkerSpan() {
std::uint32_t* APS5_VABI sceAgcDcbSetMarkerSpan(CommandBuffer* buffer, const char* text, std::uint32_t length, std::uint32_t color) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcSetSemaphoreMemory() {
int APS5_VABI sceAgcSetSemaphoreMemory(void* memory, std::uint64_t size) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcCbMemsetExclusive() {
std::uint32_t* APS5_VABI sceAgcCbMemsetExclusive(CommandBuffer* buffer, void* destination, const void* pattern, std::uint64_t size) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcSetShaderInstrumentation() {
int APS5_VABI sceAgcSetShaderInstrumentation(std::uint32_t flags) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
+19 -7
View File
@@ -28,8 +28,10 @@ extern "C" int APS5_VABI sceAgcInit_0090(std::uint32_t* state, std::uint32_t ver
extern "C" std::uint32_t* APS5_VABI sceAgcDcbDrawIndexAuto(CommandBuffer* buf, std::uint32_t indexCount, std::uint64_t modifier);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbDrawIndexIndirect(CommandBuffer* buf, std::uint32_t dataOffsetInBytes, std::uint64_t modifier);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbDrawIndexIndirectMulti(CommandBuffer* buf, std::uint32_t dataOffsetInBytes, std::uint32_t countIndirect, std::uint32_t maxCountOrCount, const volatile void* countAddress, std::uint32_t strideInBytes, std::uint64_t modifier);
extern "C" int APS5_VABI sceAgcWaitRegMemPatchReference(std::uint32_t* cmd, std::uint64_t reference);
extern "C" int APS5_VABI sceAgcWaitRegMemPatchMask(std::uint32_t* cmd, std::uint64_t mask);
extern "C" int APS5_VABI sceAgcWaitRegMemPatchAddress(std::uint32_t* cmd, const volatile void* address);
extern "C" int APS5_VABI sceAgcWaitRegMemPatchCompareFunction(std::uint32_t* cmd, std::uint8_t compareFunction);
extern "C" int APS5_VABI sceAgcWaitRegMemPatchReference(std::uint32_t* cmd, std::uint32_t reference);
extern "C" int APS5_VABI sceAgcWaitRegMemPatchMask(std::uint32_t* cmd, std::uint32_t mask);
extern "C" int APS5_VABI sceAgcGetDataPacketPayloadAddress_0090(std::uint32_t** addr, std::uint32_t* cmd, int type);
extern "C" std::uint32_t* APS5_VABI sceAgcCbSetShRegisterRangeDirect(CommandBuffer* buf, std::uint32_t offset, const std::uint32_t* values, std::uint32_t numValues);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbContextStateOp_0100(CommandBuffer* buf, std::uint32_t operation);
@@ -531,18 +533,28 @@ void testMemory() {
check(packet[8] == 0x55667788u && packet[9] == 0x11223344u && packet[12] == 2u, "wait reference or poll interval mismatch");
sceAgcWaitRegMemPatchReference(packet, 7);
check(packet[8] == 7 && packet[9] == 0x11223344u, "reference patch changed the high word");
const auto before = storage.words;
expectFailure([&] { sceAgcWaitRegMemPatchReference(packet, 0x100000000ull); });
check(storage.words == before, "invalid memory operation modified packet memory");
auto* truncated = Agc::Command::WriteWait(&storage.buffer, 0, 3, 0, 0, &value, 0x100000000ull, 0xffffffff00000001ull, 32, __func__);
check(truncated[8] == 0u && truncated[9] == 1u, "32-bit wait did not keep the low halves of the reference and mask");
sceAgcWaitRegMemPatchMask(packet, 0x0f0f0f0fu);
check(packet[10] == 0x0f0f0f0fu && packet[11] == 0xffffffffu && packet[8] == 7, "64-bit mask patch changed the wrong word");
sceAgcWaitRegMemPatchMask(truncated, 0xff00u);
check(truncated[9] == 0xff00u && truncated[8] == 0u && truncated[10] == 2u, "32-bit mask patch changed the wrong word");
for (auto* waitPacket : {packet, truncated}) {
const auto lowMask = waitPacket == packet ? 7u : 3u;
waitPacket[6] |= lowMask;
waitPacket[7] = 0xa5fc0000u;
const auto tag = waitPacket[2] & 0xffff0000u;
const std::uintptr_t address = 0x1236abcd12345679ull;
check(sceAgcWaitRegMemPatchAddress(waitPacket, reinterpret_cast<const volatile void*>(address)) == 0, "wait address patch failed");
check(waitPacket[2] == (tag | 0xabcdu) && waitPacket[3] == 0x12345679u, "wait wrapper address patch mismatch");
check(waitPacket[6] == ((0x12345679u & ~lowMask) | lowMask) && waitPacket[7] == 0xa5feabcdu, "wait address patch did not preserve control bits");
for (std::uint32_t comparison = 0; comparison <= 255; ++comparison) {
const auto control = waitPacket[5] & ~7u;
check(sceAgcWaitRegMemPatchCompareFunction(waitPacket, static_cast<std::uint8_t>(comparison)) == 0, "wait comparison patch failed");
check(waitPacket[5] == (control | (comparison & 7u)), "wait comparison patch changed control bits");
}
}
const auto beforeMask = storage.words;
expectFailure([&] { sceAgcWaitRegMemPatchMask(packet, 0x100000000ull); });
expectFailure([&] { sceAgcWaitRegMemPatchMask(truncated, 0x100000000ull); });
expectFailure([&] { sceAgcWaitRegMemPatchMask(packet + 4, 1); });
check(storage.words == beforeMask, "invalid mask patch modified packet memory");
}
+11 -22
View File
@@ -70,8 +70,8 @@ void fillStorage() {
void testUnregistered() {
std::array<void*, 3> labels{storage.get(), storage.get(), storage.get()};
expectFailure([&] { sceAgcGetSemaphoreLabel(0, &labels[1]); });
expectFailure([] { sceAgcGetSemaphoreLabel(0, nullptr); });
check(sceAgcGetSemaphoreLabel(0, &labels[1]) == static_cast<int>(0x8a6c0049u), "unregistered label error mismatch");
check(sceAgcGetSemaphoreLabel(0, nullptr) == static_cast<int>(0x8a6c0049u), "unregistered null label error mismatch");
check(labels[0] == storage.get() && labels[1] == storage.get() && labels[2] == storage.get(), "a rejected semaphore label changed the output");
}
@@ -80,10 +80,10 @@ void testRegistrationRejections() {
expectFailure([] { sceAgcSetAmmSemaphoreMemory(nullptr, memoryBytes); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(nullptr, 0); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), 0); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory() + 1, memoryBytes); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory() + labelBytes, blockBytes); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), blockBytes - 1u); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), blockBytes + labelBytes); });
check(sceAgcSetAmmSemaphoreMemory(memory() + 1, memoryBytes) == static_cast<int>(0x8a6c0002u), "unaligned semaphore memory error mismatch");
check(sceAgcSetAmmSemaphoreMemory(memory() + labelBytes, blockBytes) == static_cast<int>(0x8a6c0002u), "unaligned semaphore memory error mismatch");
check(sceAgcSetAmmSemaphoreMemory(memory(), blockBytes - 1u) == static_cast<int>(0x8a6c0002u), "unaligned semaphore memory error mismatch");
check(sceAgcSetAmmSemaphoreMemory(memory(), blockBytes + labelBytes) == static_cast<int>(0x8a6c0002u), "unaligned semaphore memory error mismatch");
expectFailure([] {
const auto top = std::numeric_limits<std::uintptr_t>::max() & ~static_cast<std::uintptr_t>(blockBytes - 1u);
sceAgcSetAmmSemaphoreMemory(reinterpret_cast<void*>(top), memoryBytes);
@@ -92,18 +92,8 @@ void testRegistrationRejections() {
testUnregistered();
}
void testUnsettledClearing() {
for (const std::uint64_t offset : {5u * labelBytes + counterBytes, memoryBytes - 1u}) {
memory()[offset] = 0x77u;
const auto before = snapshot();
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes); });
checkUnchanged(before, "a registration with unsettled clearing changed memory");
testUnregistered();
memory()[offset] = 0;
}
}
void testRegistration() {
std::fill(memory(), memory() + memoryBytes, std::uint8_t{0x77});
const auto before = snapshot();
check(sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes) == 0, "registering the AMM semaphore memory failed");
check(std::all_of(memory(), memory() + memoryBytes, [](std::uint8_t byte) { return byte == 0; }), "registration did not clear the semaphore counters");
@@ -123,7 +113,7 @@ void testLabels() {
void testLabelRejections() {
for (const std::uint32_t index : {labelCount, labelCount + 1u, std::numeric_limits<std::uint32_t>::max()}) {
void* label = storage.get();
expectFailure([&] { sceAgcGetSemaphoreLabel(index, &label); });
check(sceAgcGetSemaphoreLabel(index, &label) == static_cast<int>(0x8a6c000bu), "out-of-range label error mismatch");
check(label == storage.get(), "a rejected semaphore label changed the output");
}
expectFailure([] { sceAgcGetSemaphoreLabel(0, nullptr); });
@@ -132,9 +122,9 @@ void testLabelRejections() {
void testRepeatedRegistration() {
std::fill(memory(), memory() + memoryBytes, std::uint8_t{0x3c});
const auto before = snapshot();
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(storage.get(), blockBytes); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(nullptr, 0); });
check(sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes) == static_cast<int>(0x8a6c0048u), "repeated registration error mismatch");
check(sceAgcSetAmmSemaphoreMemory(storage.get(), blockBytes) == static_cast<int>(0x8a6c0048u), "repeated registration error mismatch");
check(sceAgcSetAmmSemaphoreMemory(nullptr, 0) == static_cast<int>(0x8a6c0048u), "repeated registration error mismatch");
checkUnchanged(before, "a repeated registration changed memory");
void* label = nullptr;
check(sceAgcGetSemaphoreLabel(labelCount - 1u, &label) == 0 && label == memory() + memoryBytes - labelBytes, "a repeated registration replaced the semaphore memory");
@@ -198,7 +188,6 @@ int main() {
fillStorage();
testUnregistered();
testRegistrationRejections();
testUnsettledClearing();
testRegistration();
testLabels();
testMemSemaphore();
+35 -7
View File
@@ -147,7 +147,7 @@ void testFusion(std::size_t misalignment, std::uint32_t terminator = SSetpcS6) {
const auto* frontLo = find(fused, 0x0C8u);
check(backLo != nullptr && backLo->value == static_cast<std::uint32_t>((base + 256) >> 8u), "back program address was not patched");
check(frontLo != nullptr && frontLo->value == static_cast<std::uint32_t>(base >> 8u), "fused program address was not patched");
check(find(fused, 0x08Au)->value == ((1u << 24u) | (2u << 6u) | 5u), "RSRC1 register counts were not merged");
check(find(fused, 0x08Au)->value == ((1u << 24u) | (1u << 6u) | 5u), "RSRC1 register counts were not merged");
check(find(fused, 0x08Bu)->value == ((6u << 1u) | (1u << 7u) | (1u << 15u)), "RSRC2 user SGPRs were not taken from the front half");
check(find(fused, 0x0B2u) != nullptr && find(fused, 0x0B2u)->value == 0x55u, "front-only SH register was lost");
check(find(fused, 0x080u) == nullptr, "front program checksum was kept");
@@ -208,7 +208,7 @@ void testOffchipLdsEnable() {
Shader fused{};
check(sceAgcFuseShaderHalves_0200(&fused, &halves.front, &halves.back, aligned(storage, 256)) == 0, "offchip LDS enable rejected by fusion");
const auto* resource = find(fused, 0x08bu);
check(resource != nullptr && resource->value == ((6u << 1u) | (1u << 7u) | (1u << 15u) | frontEnable | backEnable), "fusion lost offchip LDS enable or changed the back-half state or front user SGPRs");
check(resource != nullptr && resource->value == ((6u << 1u) | (1u << 7u) | (1u << 15u) | frontEnable), "fusion did not take offchip LDS enable and user SGPRs from the front half");
}
}
Halves reported;
@@ -222,6 +222,38 @@ void testOffchipLdsEnable() {
check(find(fused, 0x08bu)->value == 0x00af0004u, "reported shader resources did not retain the front offchip LDS enable");
}
void testResourceMerging() {
struct Case {
std::uint32_t front1;
std::uint32_t front2;
std::uint32_t back1;
std::uint32_t back2;
std::uint32_t expected1;
std::uint32_t expected2;
};
const Case cases[] = {
{0u, 0x80000000u, 0u, 0u, 0u, 0x10000000u},
{1u, 0xf0000000u, 3u, 0u, 3u, 0x10000000u},
{0u, 0xf0000000u, 0u, 0xf0000000u, 0u, 0u},
{0x400003c1u, 0x0006000cu, 0x61000043u, 0x00018001u, 0x61000043u, 0x0006800du},
{0x60000007u, 0x00010000u, 0x21000041u, 0x00020000u, 0x61000047u, 0x00020000u}
};
for (const auto& entry : cases) {
Halves halves;
halves.frontSh[1].value = entry.front1;
halves.frontSh[2].value = entry.front2;
halves.backSh[2].value = entry.back1;
halves.backSh[3].value = entry.back2;
SizeAlign size{};
check(sceAgcGetFusedShaderSize_0080(&size, &halves.front, &halves.back) == 0, "resource merge size query failed");
std::vector<std::uint8_t> scratch(size.m_size);
Shader fused{};
check(sceAgcFuseShaderHalves_0200(&fused, &halves.front, &halves.back, scratch.data()) == 0, "resource merge failed");
check(find(fused, 0x08au)->value == entry.expected1, "resource register 1 merge mismatch");
check(find(fused, 0x08bu)->value == entry.expected2, "resource register 2 merge mismatch");
}
}
void testRejections() {
Halves halves;
SizeAlign size{};
@@ -249,11 +281,6 @@ void testRejections() {
noTrailer.frontCode[5] = 0;
expectFailure([&] { sceAgcGetFusedShaderSize_0080(&size, &noTrailer.front, &noTrailer.back); });
}
{
Halves hardware;
hardware.frontSh[2].value |= 1u << 16u;
expectFailure([&] { sceAgcGetFusedShaderSize_0080(&size, &hardware.front, &hardware.back); });
}
{
Halves constants;
constants.back.embedded_constant_buffer_size_dqw = 1;
@@ -305,6 +332,7 @@ int main() {
testFusion(0, SSwappcNullS6);
testScratchReuse();
testOffchipLdsEnable();
testResourceMerging();
testRejections();
testHullHalves();
LibcRunShutdown_nid_postfix();
+52 -13
View File
@@ -771,15 +771,6 @@ if(BUILD_TESTING)
add_test(NAME agc_pipeline_link_optimization COMMAND agc_pipeline_link_optimization_tests)
endif()
add_test_executable(agc_robust_buffer_access_tests EXCLUDE_FROM_ALL tests/RobustBufferAccess.cpp)
target_include_directories(agc_robust_buffer_access_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR})
set_target_properties(agc_robust_buffer_access_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_robust_buffer_access_tests)
if(BUILD_TESTING)
set_target_properties(agc_robust_buffer_access_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_robust_buffer_access COMMAND agc_robust_buffer_access_tests)
endif()
add_test_executable(agc_draw_inputs_in_place_driver_tests EXCLUDE_FROM_ALL tests/DrawInputsInPlace.cpp)
target_include_directories(agc_draw_inputs_in_place_driver_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR})
set_target_properties(agc_draw_inputs_in_place_driver_tests PROPERTIES CXX_EXTENSIONS OFF)
@@ -1465,9 +1456,9 @@ if(BUILD_TESTING)
endif()
if(WIN32)
add_test(NAME agc_reimport_bench COMMAND agc_driver_recorder_tests --reimport-bench)
add_test(NAME agc_trimmed_import COMMAND agc_driver_recorder_tests --trimmed-import-only)
set_tests_properties(agc_reimport_bench agc_trimmed_import PROPERTIES SKIP_RETURN_CODE 77)
set_tests_properties(agc_reimport_bench agc_trimmed_import PROPERTIES ENVIRONMENT_MODIFICATION
add_test(NAME agc_host_import_cut_lifetime COMMAND agc_driver_recorder_tests --host-import-cut-only)
set_tests_properties(agc_reimport_bench agc_host_import_cut_lifetime PROPERTIES SKIP_RETURN_CODE 77)
set_tests_properties(agc_reimport_bench agc_host_import_cut_lifetime PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_unchanged_cpu_stamps COMMAND agc_driver_recorder_tests --unchanged-cpu-stamps-only)
@@ -1476,6 +1467,25 @@ if(BUILD_TESTING)
set_tests_properties(agc_unchanged_cpu_stamps PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
foreach(mode IN ITEMS map ring shared uncached)
add_test(NAME agc_collect_mapping_history_${mode} COMMAND agc_driver_recorder_tests --collect-mapping-history-only)
set_tests_properties(agc_collect_mapping_history_${mode} PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_collect_mapping_history_${mode} PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endforeach()
set_tests_properties(agc_collect_mapping_history_ring PROPERTIES ENVIRONMENT "APS5_NO_EPOCH_COLLECT_MAP=1")
set_tests_properties(agc_collect_mapping_history_shared PROPERTIES ENVIRONMENT "APS5_SHARED_COLLECT_MEMO=1")
set_tests_properties(agc_collect_mapping_history_uncached PROPERTIES ENVIRONMENT "APS5_NO_COLLECT_MEMO=1")
if(WIN32)
foreach(mode IN ITEMS cached uncached)
add_test(NAME agc_hint_mapping_history_${mode} COMMAND agc_driver_recorder_tests --hint-mapping-history-only)
set_tests_properties(agc_hint_mapping_history_${mode} PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30
ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endforeach()
set_tests_properties(agc_hint_mapping_history_uncached PROPERTIES ENVIRONMENT "APS5_NO_COLLECT_MEMO=1")
endif()
if(WIN32)
add_test(NAME agc_partial_watch_restoration COMMAND agc_driver_recorder_tests --partial-watch-restoration-only)
set_tests_properties(agc_partial_watch_restoration PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT_MODIFICATION
@@ -1580,6 +1590,8 @@ if(BUILD_TESTING)
set_tests_properties(agc_sparse_run_import_copied PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
add_test(NAME agc_unit_shadow COMMAND agc_driver_recorder_tests --unit-shadow-only)
set_tests_properties(agc_unit_shadow PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000")
add_test(NAME agc_idle_publish_wait COMMAND agc_driver_recorder_tests --idle-publish-wait-only)
set_tests_properties(agc_idle_publish_wait PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000;APS5_WRITE_WATCH_IMPORTS=watch")
if(WIN32)
add_test(NAME agc_storage_cpu_snapshot COMMAND agc_driver_recorder_tests --storage-cpu-snapshot-only)
set_tests_properties(agc_storage_cpu_snapshot PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 SKIP_RETURN_CODE 77
@@ -1613,7 +1625,7 @@ if(BUILD_TESTING)
add_test(NAME agc_metadata_pass COMMAND agc_driver_recorder_tests --metadata-pass-only)
set_tests_properties(agc_metadata_pass PROPERTIES SKIP_RETURN_CODE 77)
if(WIN32)
set_tests_properties(agc_recorder_read_list agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_sparse_run_import agc_sparse_run_import_copied agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_draw_snapshot_epoch agc_multisample_guest_access agc_draw_inputs_in_place agc_draw_inputs_copied agc_resident_present agc_slice_dirty agc_queued_write_groups agc_metadata_pass agc_tiled_resolve PROPERTIES ENVIRONMENT_MODIFICATION
set_tests_properties(agc_recorder_read_list agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_sparse_run_import agc_sparse_run_import_copied agc_unit_shadow agc_idle_publish_wait agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_draw_snapshot_epoch agc_multisample_guest_access agc_draw_inputs_in_place agc_draw_inputs_copied agc_resident_present agc_slice_dirty agc_queued_write_groups agc_metadata_pass agc_tiled_resolve PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
add_test(NAME agc_host_import_second_mapping COMMAND agc_driver_recorder_tests --host-import-second-mapping-only)
set_tests_properties(agc_host_import_second_mapping PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT_MODIFICATION
@@ -1647,6 +1659,13 @@ if(BUILD_TESTING)
endif()
set_property(TEST agc_driver_host_import_lifetime APPEND PROPERTY ENVIRONMENT_MODIFICATION
"APS5_RELEASE_UNDER_LOCK=unset:" "APS5_RELEASE_ON_UNLOCK=unset:")
if(WIN32)
add_test(NAME agc_driver_host_import_lifetime_other_family COMMAND agc_driver_recorder_tests --host-import-lifetime-other-family-only)
set_tests_properties(agc_driver_host_import_lifetime_other_family PROPERTIES
ENVIRONMENT "APS5_PIN_WAIT_MS=2000" SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
set_property(TEST agc_driver_host_import_lifetime_other_family APPEND PROPERTY ENVIRONMENT_MODIFICATION
"APS5_RELEASE_UNDER_LOCK=unset:" "APS5_RELEASE_ON_UNLOCK=unset:")
endif()
endif()
add_test_executable(agc_recorder_kept_bytes_tests EXCLUDE_FROM_ALL tests/RecorderKeptBytes.cpp)
@@ -2760,6 +2779,13 @@ if(BUILD_TESTING)
endif()
endforeach()
endforeach()
add_test(NAME agc_driver_array_layer_target_cpu COMMAND agc_driver_array_layer_target_tests)
set_tests_properties(agc_driver_array_layer_target_cpu PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_HOST_IMPORT=1" TIMEOUT 30 SKIP_RETURN_CODE 77)
if(WIN32)
set_tests_properties(agc_driver_array_layer_target_cpu PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
foreach(preparedMode IN ITEMS indirect uncached debug debug_failure deferred deferred_undecodable registered_workgroup registered_workgroup_sync abi_deferred_compute abi_deferred_pixel abi_deferred_stages abi_deferred_rectangle abi_deferred_undecodable_state abi_deferred_registration_failure abi_deferred_vertex_inputs abi_deferred_rectangle_pairs lazy lazy_sync)
set(preparedTest "agc_driver_prepared_shaders_${preparedMode}")
if(preparedMode MATCHES "^abi_deferred_")
@@ -3339,6 +3365,19 @@ if(BUILD_TESTING)
endif()
endif()
add_test_executable(agc_gcn_decode_tests EXCLUDE_FROM_ALL tests/GcnDecode.cpp)
target_link_libraries(agc_gcn_decode_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_gcn_decode_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_gcn_decode_tests)
if(BUILD_TESTING)
set_target_properties(agc_gcn_decode_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_gcn_decode COMMAND agc_gcn_decode_tests)
if(WIN32)
set_tests_properties(agc_gcn_decode PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_test_executable(agc_pixel_position_export_tests EXCLUDE_FROM_ALL tests/PixelPositionExport.cpp)
target_link_libraries(agc_pixel_position_export_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_pixel_position_export_tests PROPERTIES CXX_EXTENSIONS OFF)
@@ -86,6 +86,7 @@ private:
friend class SampledReadScope;
friend struct PendingView;
friend struct PacketTimer;
friend struct DriverTestAccess;
void stop();
Driver();
void rethrowFailure() const;
@@ -84,6 +84,7 @@ bool UnchangedCommittedSince(std::span<const std::pair<std::uint64_t, std::uint6
// Returns the generation the blocks were stamped with (0 when the arena is not write-watched):
// UnchangedSince(range, it) holds until the next store over the range.
std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes);
std::uint64_t MarkPossiblyWritten(std::uint64_t address, std::size_t bytes);
std::uint64_t StoreOwnBytes(std::uint64_t address, std::size_t bytes, const std::function<void()>& store);
std::uint64_t StoreOwnBytes(std::uint64_t address, std::span<const std::byte> source);
bool StoredOver(std::uint64_t address, std::size_t bytes, std::uint64_t generation);
@@ -1,14 +0,0 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_ROBUSTBUFFERACCESS_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_ROBUSTBUFFERACCESS_HPP
#include <vulkan/vulkan.h>
namespace AgcDriver {
inline bool RobustBufferAccess2Usable(VkDriverId driver) {
return driver != VK_DRIVER_ID_AMD_PROPRIETARY;
}
}
#endif
@@ -17,6 +17,7 @@
#include <memory>
#include <optional>
#include <string>
#include <utility>
namespace AgcDriver {
@@ -54,6 +55,7 @@ public:
VulkanDevice& operator=(const VulkanDevice&) = delete;
std::string DeviceName() const;
std::string DriverInfo() const;
bool SupportsImageFormat(VkFormat format, VkImageUsageFlags usage) const;
bool CalibrateClock(double& gpuNs, std::chrono::steady_clock::time_point& cpu) const;
ShaderRecompiler::SpirvTarget Target() const;
Graphics::SampleOrder HostSampleOrder(const Graphics::State& graphics) const;
@@ -272,7 +274,7 @@ public:
// `recipe`, when given, receives the Recipe a successful call built for its dispatch-cache
// variant (design_cpu_final M4): only when the resource cache served or took the object
// (reusable, cacheable) and recipes are on (APS5_NO_DISPATCH_RECIPE=1 builds none); else null.
void Dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::span<const Graphics::GuestMemorySnapshot> snapshots = {}, std::uint64_t programAddress = 0, std::shared_ptr<PreparedDispatch> prepared = nullptr, std::shared_ptr<const Recipe>* recipe = nullptr);
void Dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::span<const Graphics::GuestMemorySnapshot> snapshots = {}, std::uint64_t programAddress = 0, std::shared_ptr<PreparedDispatch> prepared = nullptr, std::shared_ptr<const Recipe>* recipe = nullptr, std::optional<std::pair<std::uint64_t, std::size_t>> htileClear = std::nullopt);
struct IndirectOutcome {
int cpuReason;
double argumentReadMs;
@@ -356,7 +358,7 @@ private:
ShaderRecompiler::SpirvTarget buildTarget() const;
Graphics::Context buildContext() const;
// Body of Dispatch and DispatchIndirect: `arguments` 0 dispatches x, y, z groups.
IndirectOutcome dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::uint64_t programAddress, std::shared_ptr<PreparedDispatch> prepared, std::shared_ptr<const Recipe>* recipe, const std::array<std::uint32_t, 3>& threadGroup = {});
IndirectOutcome dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::uint64_t programAddress, std::shared_ptr<PreparedDispatch> prepared, std::shared_ptr<const Recipe>* recipe, const std::array<std::uint32_t, 3>& threadGroup = {}, std::optional<std::pair<std::uint64_t, std::size_t>> htileClear = std::nullopt);
// The stage-A pre-sync over `surfaces` (see PrepareDispatch): the serial waited for, 0 none.
std::uint64_t presync(std::span<const Graphics::PresyncSurface> surfaces);
std::shared_ptr<ComputePipelineObjects> computePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span<const std::uint32_t> layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress);
@@ -16,7 +16,9 @@
#include "Optimization/ResourceProgram.hpp"
#include <algorithm>
#include <cstdlib>
#include <optional>
#include <stdexcept>
#include <utility>
namespace AgcDriver::DriverDetail {
@@ -119,6 +121,7 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
}
const bool pipelineCandidate = !resolving && !fromAhead && DispatchItemsEnabled() && DrawPipeline::Active() && indirectArguments == 0 && deferredLabels().labels.empty() && !matchesCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute) && !MatchesStreamCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute);
const bool partialLastGroup = partialGroups != std::array<std::uint32_t, 3>{};
std::optional<std::pair<std::uint64_t, std::size_t>> htileClearCompletion;
if (resolving) {
if (indirectArguments == 0 && Graphics::MatchMultisampledSingleClear(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads)) return;
if (!partialLastGroup && matchesFillKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute)) return;
@@ -139,7 +142,8 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
if (const auto htile = Graphics::HtileDepthClearAddress(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads); htile != 0) {
const auto threads = (packet[4] & 0x20u) != 0 ? static_cast<std::uint64_t>(packet[1]) : static_cast<std::uint64_t>(packet[1]) * compute.numThreads[0];
const auto nativeBytes = Graphics::HtileDepthClearBytes_nid_no_patch(std::span(snapshot.code).subspan(codeOffset), userData, compute, packet, indirectArguments);
Graphics::NoteHtileDepthClear(htile, static_cast<std::size_t>(threads * 16u), nativeBytes);
const auto bytes = static_cast<std::size_t>(threads * 16u);
if (Graphics::NoteHtileDepthClear(htile, bytes, nativeBytes)) htileClearCompletion = std::pair{htile, bytes};
}
if (!partialLastGroup && indirectArguments == 0 && copyBuffer(queue, submission.queue, packet, std::span(snapshot.code).subspan(codeOffset), userData, compute, localDevice, address)) {
pendingDispatchPhases().outcome = DispatchOutcome::CopyHle;
@@ -462,18 +466,18 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
const auto epoch = DrawPipeline::EpochToken().load(std::memory_order_relaxed);
const auto queueId = submission.queue;
itemCommits.fetch_add(1, std::memory_order_relaxed);
DrawPipeline::Queue0().Enqueue([this, localDevice = std::move(localDevice), result = compiledResult, snapshots = std::move(snapshots), liveWords = std::move(liveWords), groups, address, queueId, recipe, keepVariant, dataHit, epoch] {
DrawPipeline::Queue0().Enqueue([this, localDevice = std::move(localDevice), result = compiledResult, snapshots = std::move(snapshots), liveWords = std::move(liveWords), groups, address, queueId, recipe, keepVariant, dataHit, epoch, htileClearCompletion] {
DrawPipeline::FollowEpoch(epoch);
GuestMemory::SetCurrentPacket(0x15, queueId);
const auto& compiledCommit = *result;
const bool noteWrites = writeEvidenceEnabled() || traceCapSync();
try {
std::shared_ptr<RecipeHit> recipeHit = localDevice->PrepareRecipe(recipe, false);
std::shared_ptr<RecipeHit> recipeHit = htileClearCompletion.has_value() ? nullptr : localDevice->PrepareRecipe(recipe, false);
std::shared_ptr<const Recipe> builtRecipe;
bool writersNoted = false;
for (;;) {
std::shared_ptr<PreparedDispatch> prepared;
if (recipeHit == nullptr || VulkanDevice::VerifyRecipes()) prepared = localDevice->PrepareDispatch(compiledCommit, snapshots);
if (recipeHit == nullptr || VulkanDevice::VerifyRecipes() || htileClearCompletion.has_value()) prepared = localDevice->PrepareDispatch(compiledCommit, snapshots);
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Dispatch);
std::lock_guard gpuLock(GuestMemory::GpuMutex());
recordLabelsForPacket(localDevice.get(), queueId);
@@ -491,7 +495,7 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
continue;
}
} else {
localDevice->Dispatch(compiledCommit, groups[0], groups[1], groups[2], snapshots, address, std::move(prepared), &builtRecipe);
localDevice->Dispatch(compiledCommit, groups[0], groups[1], groups[2], snapshots, address, std::move(prepared), htileClearCompletion.has_value() ? nullptr : &builtRecipe, htileClearCompletion);
}
break;
}
@@ -517,7 +521,7 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
std::shared_ptr<RecipeHit> recipeHit;
if (cached && keepVariant != nullptr && !stampValidate()) {
recipeHit = localDevice->PrepareRecipe(keepVariant->recipe.load(std::memory_order_acquire), indirectArguments != 0);
recipeHit = htileClearCompletion.has_value() ? nullptr : localDevice->PrepareRecipe(keepVariant->recipe.load(std::memory_order_acquire), indirectArguments != 0);
phaseTiming.Phase(PhaseRecipePrecheck);
}
@@ -595,7 +599,7 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
const auto outcome = localDevice->DispatchIndirect(compiled, indirectArguments, snapshots, address, std::move(prepared), attachTo != nullptr ? &builtRecipe : nullptr, threadGroup);
countIndirect(outcome.cpuReason, outcome.argumentReadMs);
} else {
localDevice->Dispatch(compiled, groups[0], groups[1], groups[2], snapshots, address, std::move(prepared), attachTo != nullptr ? &builtRecipe : nullptr);
localDevice->Dispatch(compiled, groups[0], groups[1], groups[2], snapshots, address, std::move(prepared), htileClearCompletion.has_value() ? nullptr : (attachTo != nullptr ? &builtRecipe : nullptr), htileClearCompletion);
}
} catch (const std::exception& error) {
if (rethrow(error)) return;
@@ -224,13 +224,17 @@ Driver::Driver() {
void Driver::WaitIdle() {
require(!onWorkerThread(), "worker cannot wait for itself");
std::unique_lock lock(mutex);
const auto target = accepted;
++idleWaiters;
changed.wait(lock, [&] { return failure != nullptr || stopping || completed >= target; });
--idleWaiters;
rethrowFailure();
checkStopping();
{
std::unique_lock lock(mutex);
const auto target = accepted;
++idleWaiters;
changed.wait(lock, [&] { return failure != nullptr || stopping || completed >= target; });
--idleWaiters;
rethrowFailure();
checkStopping();
}
std::lock_guard gpuLock(GuestMemory::GpuMutex());
if (const auto localDevice = device.Load()) localDevice->WaitIdle();
}
void Driver::CheckFailure() {
@@ -181,10 +181,11 @@ std::vector<std::pair<std::uint64_t, std::uint64_t>> QueuedDrawReadRanges(const
}
if (stages.empty() || stages.front().program == nullptr) return ranges;
for (const auto& attribute : stages.front().program->vertexAttributes) {
if (Graphics::NullVertexDescriptor(attribute) || Graphics::VertexFetchOutOfRange(attribute)) continue;
if (Graphics::NullVertexDescriptor(attribute)) continue;
const auto address = attribute.resource.fields[0] | (static_cast<std::uint64_t>(attribute.resource.fields[1] & 0xffffu) << 32u);
std::uint64_t bytes = 0;
try {
if (Graphics::VertexFetchOutOfRange(attribute)) continue;
bytes = Graphics::VertexBufferExtent(attribute);
} catch (const std::runtime_error&) {
continue;
@@ -813,18 +813,33 @@ constexpr std::size_t WritePagesPerBlock = WriteBlockBytes / WritePageBytes;
enum class StampKind : std::uint8_t { Cpu, Driver, ImportWindow, Fresh };
constexpr std::array<std::byte, WritePageBytes> ZeroPage{};
std::uint64_t HashPage(const std::byte* page) {
constexpr std::uint64_t prime1 = 0x9E3779B185EBCA87ull;
constexpr std::uint64_t prime2 = 0xC2B2AE3D27D4EB4Full;
constexpr std::uint64_t prime3 = 0x165667B19E3779F9ull;
constexpr std::uint64_t prime4 = 0x85EBCA77C2B2AE63ull;
const auto round = [](std::uint64_t accumulator, std::uint64_t word) { return std::rotl(accumulator + word * prime2, 31) * prime1; };
std::array<std::uint64_t, 4> lanes{prime1 + prime2, prime2, 0, 0 - prime1};
for (std::size_t offset = 0; offset < WritePageBytes; offset += 4 * sizeof(std::uint64_t)) {
std::array<std::uint64_t, 4> words;
std::memcpy(words.data(), page + offset, sizeof(words));
for (std::size_t lane = 0; lane < 4; ++lane) lanes[lane] = round(lanes[lane], words[lane]);
}
std::uint64_t hash = std::rotl(lanes[0], 1) + std::rotl(lanes[1], 7) + std::rotl(lanes[2], 12) + std::rotl(lanes[3], 18);
for (const auto lane : lanes) hash = (hash ^ round(0, lane)) * prime1 + prime4;
hash ^= hash >> 33;
hash *= prime2;
hash ^= hash >> 29;
hash *= prime3;
return hash ^ (hash >> 32);
}
struct WindowCopy {
static constexpr std::uint32_t Zero = std::numeric_limits<std::uint32_t>::max();
std::uint64_t first;
std::span<const std::byte> bytes;
std::span<const std::uint32_t> slots;
std::span<const std::uint64_t> hashes;
bool Holds(std::uintptr_t page) const {
const auto slot = slots[(page - first) / WritePageBytes];
const auto* expected = slot == Zero ? ZeroPage.data() : bytes.data() + static_cast<std::size_t>(slot) * WritePageBytes;
return std::memcmp(reinterpret_cast<const void*>(page), expected, WritePageBytes) == 0;
return HashPage(reinterpret_cast<const std::byte*>(page)) == hashes[(page - first) / WritePageBytes];
}
};
@@ -873,6 +888,7 @@ struct WriteTracker {
std::uintptr_t end = 0;
std::uint64_t epoch = 0;
std::uint64_t unwatched = 0;
std::uint64_t mapping = 0;
};
std::array<Memo, 256> memo{};
std::size_t nextMemo = 0;
@@ -955,8 +971,10 @@ struct WriteTracker {
struct DriverPieces {
std::uint32_t whole = 0;
std::uint32_t dropped = 0;
std::uint32_t next = 0;
std::array<DriverPiece, 4> pieces{};
std::uint32_t possibleWhole = 0;
std::uint32_t possibleDropped = 0;
std::array<DriverPiece, 4> possiblePieces{};
bool covers(std::uint32_t begin, std::uint32_t end, std::uint64_t generation) const {
if (generation == 0 || begin >= end || dropped > generation) return false;
@@ -1009,7 +1027,7 @@ struct WriteTracker {
#endif
}
void stampWatchedPart(std::uint64_t address, std::uint64_t end, std::uint32_t stampGeneration) {
void stampWatchedPart(std::uint64_t address, std::uint64_t end, std::uint32_t stampGeneration, bool complete) {
#ifdef _WIN32
address = std::max<std::uint64_t>(address, base);
end = std::min<std::uint64_t>(end, base + size);
@@ -1023,24 +1041,45 @@ struct WriteTracker {
const auto stop = std::min<std::uint64_t>(end, blockBegin(block) + WriteBlockBytes);
if (!blockPartWatched(first, stop)) continue;
stamp(block, stampGeneration, StampKind::Driver);
noteDriverStore(block, address, end, stampGeneration);
noteDriverStore(block, address, end, stampGeneration, complete);
}
}
void noteDriverStore(std::uint64_t block, std::uint64_t address, std::uint64_t end, std::uint32_t stampGeneration) {
void noteDriverStore(std::uint64_t block, std::uint64_t address, std::uint64_t end, std::uint32_t stampGeneration, bool complete) {
const auto begin = blockBegin(block);
const auto from = static_cast<std::uint32_t>(std::max(address, begin) - begin);
const auto to = static_cast<std::uint32_t>(std::min<std::uint64_t>(end, begin + WriteBlockBytes) - begin);
if (from == 0 && to == WriteBlockBytes) {
auto& entry = driverPieces[block];
entry = DriverPieces{};
entry.whole = stampGeneration;
if (complete) {
entry = DriverPieces{};
entry.whole = stampGeneration;
} else {
entry.possibleWhole = stampGeneration;
entry.possibleDropped = 0;
entry.possiblePieces = {};
}
return;
}
auto& entry = driverPieces[block];
auto& slot = entry.pieces[entry.next++ % entry.pieces.size()];
entry.dropped = std::max(entry.dropped, slot.generation);
slot = {stampGeneration, from, to};
auto& pieces = complete ? entry.pieces : entry.possiblePieces;
auto& dropped = complete ? entry.dropped : entry.possibleDropped;
DriverPiece* contained = nullptr;
for (auto& piece : pieces) {
if (piece.generation == 0 || piece.begin < from || piece.end > to) continue;
if (contained == nullptr) contained = &piece;
else piece = {};
}
if (contained != nullptr) {
*contained = {stampGeneration, from, to};
return;
}
auto* oldest = &pieces.front();
for (auto& piece : pieces) {
if (piece.generation < oldest->generation) oldest = &piece;
}
dropped = std::max(dropped, oldest->generation);
*oldest = {stampGeneration, from, to};
}
Leaf& leafFor(std::uint64_t block) {
@@ -1151,10 +1190,13 @@ struct WriteTracker {
continue;
}
const auto& entry = found->second;
if (entry.whole > generation || entry.dropped > generation) return true;
if (entry.whole > generation || entry.dropped > generation || entry.possibleWhole > generation || entry.possibleDropped > generation) return true;
for (const auto& piece : entry.pieces) {
if (piece.generation > generation && piece.begin < to && from < piece.end) return true;
}
for (const auto& piece : entry.possiblePieces) {
if (piece.generation > generation && piece.begin < to && from < piece.end) return true;
}
}
return false;
}
@@ -1348,19 +1390,20 @@ std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoi
// must not differ between them.
const auto epoch = currentCollectEpoch();
const auto unwatched = unwatchSerial.load(std::memory_order_acquire);
const auto mapping = GuestAllocations::GuestAllocationsGeneration_nid_postfix();
const bool useMemo = memoized && collectMemoEnabled() && bytes != 0;
if (useMemo && !sharedCollectMemo()) {
// An entry exists only for a completed walk of an in-arena range, so the tracker is
// initialized and watched; nothing below the lock needs asking.
if (epochCollectMapEnabled()) {
const auto& map = epochCollectMap(epoch);
if (const auto found = map.ranges.find(first); found != map.ranges.end() && found->second.unwatched == unwatched && stop <= found->second.end) {
if (const auto found = map.ranges.find(first); found != map.ranges.end() && found->second.unwatched == unwatched && found->second.mapping == mapping && stop <= found->second.end) {
collectMemoHits.fetch_add(1, std::memory_order_relaxed);
return tracker.generation.load(std::memory_order_relaxed);
}
}
for (const auto& entry : threadCollectMemo.entries) {
if (entry.epoch == epoch && entry.unwatched == unwatched && entry.begin <= first && stop <= entry.end) {
if (entry.epoch == epoch && entry.unwatched == unwatched && entry.mapping == mapping && entry.begin <= first && stop <= entry.end) {
// The current generation, not the memoized one: blocks stamped since (MarkWritten, an
// overlapping collect) were written before this caller reads, and an older value would
// make every later UnchangedSince fail until the epoch ends.
@@ -1375,7 +1418,7 @@ std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoi
auto cursor = first;
if (useMemo && sharedCollectMemo()) {
for (const auto& entry : tracker.memo) {
if (entry.epoch == epoch && entry.unwatched == unwatchSerial.load(std::memory_order_relaxed) && entry.begin <= cursor && stop <= entry.end) {
if (entry.epoch == epoch && entry.unwatched == unwatchSerial.load(std::memory_order_relaxed) && entry.mapping == mapping && entry.begin <= cursor && stop <= entry.end) {
collectMemoHits.fetch_add(1, std::memory_order_relaxed);
return tracker.generation;
}
@@ -1396,12 +1439,12 @@ std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoi
// Only a completed walk is remembered; a failed one (uncommitted pages) returned 0 above.
if (collectMemoEnabled()) {
const auto serial = unwatchSerial.load(std::memory_order_relaxed);
if (sharedCollectMemo()) tracker.memo[tracker.nextMemo++ % tracker.memo.size()] = {first, stop, epoch, serial};
if (sharedCollectMemo()) tracker.memo[tracker.nextMemo++ % tracker.memo.size()] = {first, stop, epoch, serial, mapping};
else {
threadCollectMemo.entries[threadCollectMemo.next++ % threadCollectMemo.entries.size()] = {first, stop, epoch, serial};
threadCollectMemo.entries[threadCollectMemo.next++ % threadCollectMemo.entries.size()] = {first, stop, epoch, serial, mapping};
if (epochCollectMapEnabled()) {
auto& entry = epochCollectMap(epoch).ranges[first];
if (entry.unwatched != serial || entry.end < stop) entry = {first, stop, epoch, serial};
if (entry.unwatched != serial || entry.mapping != mapping || entry.end < stop) entry = {first, stop, epoch, serial, mapping};
}
}
}
@@ -1567,23 +1610,16 @@ bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function
begin = end;
}
}
std::vector<std::uint32_t> slots(static_cast<std::size_t>((stop - first) / page), WindowCopy::Zero);
std::size_t kept = 0;
std::vector<std::uint64_t> hashes(static_cast<std::size_t>((stop - first) / page), 0);
for (const auto& [begin, end] : spans) {
CheckRange(reinterpret_cast<const void*>(begin), static_cast<std::size_t>(end - begin), 1, false);
for (auto cursor = begin; cursor < end; cursor += page) {
if (std::memcmp(reinterpret_cast<const void*>(cursor), ZeroPage.data(), page) != 0) slots[static_cast<std::size_t>((cursor - first) / page)] = static_cast<std::uint32_t>(kept++);
hashes[static_cast<std::size_t>((cursor - first) / page)] = HashPage(reinterpret_cast<const std::byte*>(cursor));
}
}
std::vector<std::byte> saved;
saved.reserve(kept * page);
const auto* source = reinterpret_cast<const std::byte*>(first);
for (std::size_t index = 0; index < slots.size(); ++index) {
if (slots[index] != WindowCopy::Zero) saved.insert(saved.end(), source + index * page, source + (index + 1) * page);
}
std::erase_if(spans, [&](const auto& span) { return !walkWrites(tracker, span.first, span.second, StampKind::Cpu, true); });
if (!runImport()) return false;
const WindowCopy window{first, saved, slots};
const WindowCopy window{first, hashes};
auto unwatchedFrom = first;
for (const auto& [begin, end] : spans) {
if (!walkWrites(tracker, begin, end, StampKind::Cpu, true, &window)) continue;
@@ -1661,12 +1697,12 @@ std::uint64_t storeOwn(std::uint64_t address, std::size_t bytes, const std::func
++tracker.generation;
if (!tracker.watched || !tracker.covers(stored.first, static_cast<std::size_t>(stored.second - stored.first))) {
tracker.noteUnstampedStore(stored.first, stored.second);
if (tracker.watched) tracker.stampWatchedPart(stored.first, stored.second, tracker.generation);
if (tracker.watched) tracker.stampWatchedPart(stored.first, stored.second, tracker.generation, true);
return 0;
}
for (auto block = tracker.blockOf(stored.first); block <= tracker.blockOf(stored.second - 1); ++block) {
tracker.stamp(block, tracker.generation, StampKind::Driver);
tracker.noteDriverStore(block, stored.first, stored.second, tracker.generation);
tracker.noteDriverStore(block, stored.first, stored.second, tracker.generation, true);
}
return tracker.generation;
};
@@ -1699,7 +1735,7 @@ std::uint64_t StoreOwnBytes(std::uint64_t address, std::span<const std::byte> so
});
}
std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) {
static std::uint64_t markWritten(std::uint64_t address, std::size_t bytes, bool complete) {
auto& tracker = Tracker();
auto lock = lockTracker(tracker);
tracker.initialize();
@@ -1707,7 +1743,7 @@ std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) {
++tracker.generation;
if (!tracker.watched || !tracker.covers(address, bytes)) {
tracker.noteUnstampedStore(address, address + bytes);
if (tracker.watched) tracker.stampWatchedPart(address, address + bytes, tracker.generation);
if (tracker.watched) tracker.stampWatchedPart(address, address + bytes, tracker.generation, complete);
return 0;
}
{
@@ -1715,11 +1751,11 @@ std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) {
const auto blockStop = (address + bytes + WriteBlockBytes - 1) & ~(std::uint64_t{WriteBlockBytes} - 1);
StampRuns runs{tracker, StampKind::Fresh};
#ifdef _WIN32
const bool complete = collectWindowsWrites(runs, blockFirst, blockStop, false);
const bool collected = collectWindowsWrites(runs, blockFirst, blockStop, false);
#else
const bool complete = GuestWriteWatch::GuestWriteWatchCollectFresh_nid_postfix(static_cast<std::uintptr_t>(blockFirst), static_cast<std::size_t>(blockStop - blockFirst), &stampWrittenRun, &runs);
const bool collected = GuestWriteWatch::GuestWriteWatchCollectFresh_nid_postfix(static_cast<std::uintptr_t>(blockFirst), static_cast<std::size_t>(blockStop - blockFirst), &stampWrittenRun, &runs);
#endif
if (!complete) {
if (!collected) {
lock.unlock();
beforeUnwatch(blockFirst, static_cast<std::size_t>(blockStop - blockFirst));
lock.lock();
@@ -1733,11 +1769,19 @@ std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) {
const auto last = tracker.blockOf(address + bytes - 1);
for (auto block = first; block <= last; ++block) {
tracker.stamp(block, tracker.generation, StampKind::Driver);
tracker.noteDriverStore(block, address, address + bytes, tracker.generation);
tracker.noteDriverStore(block, address, address + bytes, tracker.generation, complete);
}
return tracker.generation;
}
std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) {
return markWritten(address, bytes, true);
}
std::uint64_t MarkPossiblyWritten(std::uint64_t address, std::size_t bytes) {
return markWritten(address, bytes, false);
}
bool StoredOver(std::uint64_t address, std::size_t bytes, std::uint64_t generation) {
auto& tracker = Tracker();
const auto lock = lockTracker(tracker);
@@ -1827,15 +1871,16 @@ std::uint64_t UntrackedHint(std::uint64_t address, std::size_t bytes) {
tracker.initialize();
if (!tracker.watched || !tracker.hinted(first, stop)) return 0;
const auto unwatched = unwatchSerial.load(std::memory_order_relaxed);
const auto mapping = GuestAllocations::GuestAllocationsGeneration_nid_postfix();
const bool memoized = collectMemoEnabled() && std::any_of(threadHintMemo.entries.begin(), threadHintMemo.entries.end(), [&](const WriteTracker::Memo& entry) {
return entry.epoch == epoch && entry.unwatched == unwatched && entry.begin <= first && stop <= entry.end;
return entry.epoch == epoch && entry.unwatched == unwatched && entry.mapping == mapping && entry.begin <= first && stop <= entry.end;
});
if (memoized) {
collectMemoHits.fetch_add(1, std::memory_order_relaxed);
} else {
const TimedAccess timed(CounterCollect, bytes);
if (!walkHints(tracker, first, stop)) return 0;
if (collectMemoEnabled()) threadHintMemo.entries[threadHintMemo.next++ % threadHintMemo.entries.size()] = {first, stop, epoch, unwatched};
if (collectMemoEnabled()) threadHintMemo.entries[threadHintMemo.next++ % threadHintMemo.entries.size()] = {first, stop, epoch, unwatched, mapping};
}
return tracker.newestHint(first, stop);
#else
@@ -18,7 +18,6 @@
#include "prx/libSceAgcDriver/Execution/include/Float32FlushControl.hpp"
#include "prx/libSceAgcDriver/Execution/include/PipelineLinkOptimization.hpp"
#include "prx/libSceAgcDriver/Execution/include/DrawInputsInPlace.hpp"
#include "prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp"
#include "prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp"
#include "prx/libSceAgcDriver/Execution/include/SubgroupClock.hpp"
#include "prx/libSceAgcDriver/Execution/include/ShaderDeviceProfile.hpp"
@@ -157,20 +156,22 @@ public:
std::lock_guard lock(mutex);
stopping = true;
}
changed.notify_one();
changed.notify_all();
if (worker.joinable()) worker.join();
}
void Enqueue(int index, std::uint32_t width, std::uint32_t height, std::uint32_t scale, bool screenshot, std::vector<std::byte> pixels) {
std::lock_guard lock(mutex);
std::unique_lock lock(mutex);
require(!stopping, "frame dump writer has stopped");
require(pixels.size() <= maxBytes - queuedBytes, "frame dump queue exceeded 64 MiB");
if (!screenshot && !frameDumpOutput.Enabled_nid_no_patch()) return;
require(pixels.size() <= maxBytes, "a frame dump exceeds the 64 MiB queue");
changed.wait(lock, [&] { return stopping || pixels.size() <= maxBytes - queuedBytes; });
require(!stopping, "frame dump writer has stopped");
if (!worker.joinable()) worker = std::thread([this] { run(); });
const auto bytes = pixels.size();
pending.push_back({index, width, height, scale, screenshot, std::move(pixels)});
queuedBytes += bytes;
changed.notify_one();
changed.notify_all();
}
bool Idle() {
@@ -203,6 +204,7 @@ private:
else WriteFrameBmp(frame.index, frame.width, frame.height, frame.pixels, frame.scale);
std::lock_guard lock(mutex);
queuedBytes -= frame.pixels.size();
changed.notify_all();
}
}
} catch (const std::exception& error) {
@@ -1945,14 +1947,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
VkPhysicalDeviceFeatures2 queried{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &robustness2};
state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2")(selected, &queried);
}
VkDriverId robustnessDriver = VK_DRIVER_ID_MAX_ENUM;
if (hasExtension(VK_KHR_DRIVER_PROPERTIES_EXTENSION_NAME)) {
VkPhysicalDeviceDriverProperties driver{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES};
VkPhysicalDeviceProperties2 driverProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &driver};
state->InstanceFunction<PFN_vkGetPhysicalDeviceProperties2>("vkGetPhysicalDeviceProperties2")(selected, &driverProperties);
robustnessDriver = driver.driverID;
}
state->robustBufferAccess2 = robustness2.robustBufferAccess2 == VK_TRUE && enabled.robustBufferAccess == VK_TRUE && RobustBufferAccess2Usable(robustnessDriver);
state->robustBufferAccess2 = robustness2.robustBufferAccess2 == VK_TRUE && enabled.robustBufferAccess == VK_TRUE;
if (robustness2.nullDescriptor == VK_TRUE || state->robustBufferAccess2) {
const bool nullDescriptor = robustness2.nullDescriptor == VK_TRUE;
robustness2 = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT};
@@ -3222,8 +3217,9 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) {
static const bool noAdopt = std::getenv("APS5_NO_PRESENT_EPOCH") != nullptr;
if (!noAdopt) GuestMemory::AdoptLatestCollectEpoch();
GuestMemory::CollectWritesUncached(buffer.address, bytes);
if (!GuestMemory::UnchangedSince(buffer.address, bytes, resident->Generation()) || (buffer.dccAddress != 0 && ResidentKeysMoved(*resident))) {
resident->Refresh();
if (!GuestMemory::UnchangedSince(buffer.address, bytes, resident->GenerationFor(buffer.address, bytes)) || (buffer.dccAddress != 0 && ResidentKeysMoved(*resident))) {
if (resident->ImageLayers() > 1) resident->RefreshLayers(layer, 1);
else resident->Refresh();
++refreshedPresents;
}
++residentPresents;
@@ -3673,6 +3669,12 @@ std::uint32_t VulkanDevice::VendorId() const {
return state->properties.vendorID;
}
bool VulkanDevice::SupportsImageFormat(VkFormat format, VkImageUsageFlags usage) const {
VkImageFormatProperties properties{};
const auto query = state->InstanceFunction<PFN_vkGetPhysicalDeviceImageFormatProperties>("vkGetPhysicalDeviceImageFormatProperties");
return query(state->physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, 0, &properties) == VK_SUCCESS;
}
ShaderRecompiler::SpirvTarget VulkanDevice::Target() const {
require(state->shaderProfile != nullptr, "shader device profile is unavailable");
auto target = state->shaderProfile->Target();
@@ -4404,6 +4406,7 @@ struct RecordedDispatch {
// A template hit's revalidate time, charged to the [rescache] refresh accounting.
double revalidateMs;
DispatchTimer* timer;
std::optional<std::pair<std::uint64_t, std::size_t>> htileClear;
// Whether the data refresh recorded anything.
bool refreshed = false;
bool resourceKeyMatched = false;
@@ -4664,8 +4667,8 @@ std::shared_ptr<RecipeHit> VulkanDevice::PrepareRecipe(const std::shared_ptr<con
return hit;
}
void VulkanDevice::Dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::uint64_t programAddress, std::shared_ptr<PreparedDispatch> prepared, std::shared_ptr<const Recipe>* recipe) {
static_cast<void>(dispatch(shader, x, y, z, 0, snapshots, programAddress, std::move(prepared), recipe));
void VulkanDevice::Dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::uint64_t programAddress, std::shared_ptr<PreparedDispatch> prepared, std::shared_ptr<const Recipe>* recipe, std::optional<std::pair<std::uint64_t, std::size_t>> htileClear) {
static_cast<void>(dispatch(shader, x, y, z, 0, snapshots, programAddress, std::move(prepared), recipe, {}, htileClear));
}
VulkanDevice::IndirectOutcome VulkanDevice::DispatchIndirect(const ShaderRecompiler::RecompileResult& shader, std::uint64_t arguments, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::uint64_t programAddress, std::shared_ptr<PreparedDispatch> prepared, std::shared_ptr<const Recipe>* recipe, const std::array<std::uint32_t, 3>& threadGroup) {
@@ -4748,6 +4751,14 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) {
auto& resources = *record.resources;
const auto arguments = record.arguments;
const auto* argumentImport = record.argumentImport;
const auto* limit = state->properties.limits.maxComputeWorkGroupCount;
if (argumentImport == nullptr && record.shader->program->workgroupMemoryDwords != 0 && (record.x > limit[0] || record.y > limit[1] || record.z > limit[2])) {
char counted[128] = "";
if (record.cpuArguments != 0) std::snprintf(counted, sizeof(counted), " (read on the CPU from the indirect arguments at 0x%llx, cpuReason %d)", static_cast<unsigned long long>(record.cpuArguments), record.cpuReason);
char text[384];
std::snprintf(text, sizeof(text), "Vulkan dispatch: workgroup count %ux%ux%u%s exceeds device limits %ux%ux%u and a split aliases the device-memory LDS slots, whose base is computed from gl_NumWorkGroups of a single chunk", record.x, record.y, record.z, counted, limit[0], limit[1], limit[2]);
throw std::runtime_error(text);
}
// The record phase split (APS5_PROFILE_DRAW, rows "record: ..." of the [dispatch] totals):
// opening the batch, the keeps, the data refresh, the barriers with the bind and the dispatch
// itself, the pending-write notes and marks (MarkGpuWrites), and the completion registration.
@@ -4882,17 +4893,11 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) {
if (argumentImport != nullptr) {
context.Resolved(&Graphics::DeviceFunctions::cmdDispatchIndirect, "vkCmdDispatchIndirect")(commands, indirectBuffer, indirectOffset);
} else {
const auto* limit = state->properties.limits.maxComputeWorkGroupCount;
if (record.x <= limit[0] && record.y <= limit[1] && record.z <= limit[2]) {
context.Resolved(&Graphics::DeviceFunctions::cmdDispatch, "vkCmdDispatch")(commands, record.x, record.y, record.z);
} else {
char counted[128] = "";
if (record.cpuArguments != 0) std::snprintf(counted, sizeof(counted), " (read on the CPU from the indirect arguments at 0x%llx, cpuReason %d)", static_cast<unsigned long long>(record.cpuArguments), record.cpuReason);
if (record.shader->program->workgroupMemoryDwords != 0) {
char text[384];
std::snprintf(text, sizeof(text), "Vulkan dispatch: workgroup count %ux%ux%u%s exceeds device limits %ux%ux%u and a split aliases the device-memory LDS slots, whose base is computed from gl_NumWorkGroups of a single chunk", record.x, record.y, record.z, counted, limit[0], limit[1], limit[2]);
throw std::runtime_error(text);
}
const auto offsets = std::ranges::count_if(record.shader->program->specialization, [](const auto& constant) { return ShaderRecompiler::PipelineSpecialization::IsWorkgroupOffset(constant.id); });
if (offsets != ShaderRecompiler::PipelineSpecialization::WorkgroupOffsetWords) {
char text[320];
@@ -4924,21 +4929,27 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) {
recorder.EndGpuTiming(trailingTiming);
recorder.MarkCovered(dispatchedAccess);
recordStep(PhaseRecordBind);
if (record.htileClear.has_value()) Graphics::ArmHtileDepthClear(record.htileClear->first, record.htileClear->second);
resources.MarkGpuWrites(recorder);
recordStep(PhaseRecordMarks);
// Only copied written buffers (and BDA fault checks) need work once the GPU is done; without them
// the batch can signal its labels from the GPU.
if (resources.NeedsCompletion() && resources.Reusable()) {
const bool completionNeeded = resources.NeedsCompletion() || record.htileClear.has_value();
if (completionNeeded && resources.Reusable()) {
auto kept = record.resources;
recorder.OnComplete([kept] { kept->CheckFaults(); });
} else if (resources.NeedsCompletion()) {
recorder.OnComplete([kept, htileClear = record.htileClear] {
kept->CheckFaults();
if (htileClear.has_value()) Graphics::CompleteHtileDepthClear(htileClear->first, htileClear->second);
});
} else if (completionNeeded) {
// Listed for DispatchIndirect until the write-back ran (a non-reusable object, so once).
auto writers = state->copiedWriters;
auto kept = record.resources;
recorder.OnComplete([kept, writers] {
recorder.OnComplete([kept, writers, htileClear = record.htileClear] {
// Delisted before the write-back: one that fails must not keep indirect dispatches on the CPU.
writers->erase(std::remove(writers->begin(), writers->end(), kept), writers->end());
kept->WriteBackBuffers();
if (htileClear.has_value()) Graphics::CompleteHtileDepthClear(htileClear->first, htileClear->second);
});
// Listed after the registration: a throw there leaves nothing that would pin the CPU path forever.
if (resources.HasCopiedWrites()) writers->push_back(kept);
@@ -5069,7 +5080,7 @@ std::shared_ptr<ComputePipelineObjects> VulkanDevice::computePipeline(const Grap
return finish(std::move(objects), nullptr);
}
VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::uint64_t programAddress, std::shared_ptr<PreparedDispatch> prepared, std::shared_ptr<const Recipe>* recipeOut, const std::array<std::uint32_t, 3>& threadGroup) {
VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::uint64_t programAddress, std::shared_ptr<PreparedDispatch> prepared, std::shared_ptr<const Recipe>* recipeOut, const std::array<std::uint32_t, 3>& threadGroup, std::optional<std::pair<std::uint64_t, std::size_t>> htileClear) {
startJournalReplay();
for (const auto& binding : shader.bindings) {
if (binding.role != ShaderRecompiler::DescriptorRole::GuestBuffers) continue;
@@ -5321,7 +5332,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
// works; the batch is waited for where the guest expects results (labels, flips, CPU reads).
auto& recorder = *state->recorder;
IndirectOutcome outcome{0, 0};
RecordedDispatch record{&context, &shaders[0], resources, objects, pushStages, &pushBytes, x, y, z, arguments, nullptr, programAddress, fromCache && TemplateDataRefresh() ? RecordedDispatch::DataRefresh::Words : RecordedDispatch::DataRefresh::None, 0, revalidateMs, &timer};
RecordedDispatch record{&context, &shaders[0], resources, objects, pushStages, &pushBytes, x, y, z, arguments, nullptr, programAddress, fromCache && TemplateDataRefresh() ? RecordedDispatch::DataRefresh::Words : RecordedDispatch::DataRefresh::None, 0, revalidateMs, &timer, htileClear};
record.resourceKeyMatched = fromCache;
record.bindings = overlay;
record.threadGroup = threadGroup;
@@ -20,8 +20,15 @@ class StorageTexture;
VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target, bool depthWritten = true);
SampleOrderTarget DepthSurfaceOrderTarget(const Context& context, const DepthTarget& target);
DepthSampleLocations DepthSurfaceSampleLocations(const Context& context, const DepthTarget& target);
std::vector<std::pair<std::uint64_t, std::uint64_t>> FlushDepthPlanes(std::uint64_t address, std::size_t bytes);
enum class DepthPlaneScope { Overlapped, Whole };
std::vector<std::pair<std::uint64_t, std::uint64_t>> FlushDepthPlanes(std::uint64_t address, std::size_t bytes, DepthPlaneScope scope = DepthPlaneScope::Overlapped);
bool DepthPlanesStale();
struct DepthPlaneStoreStatistics {
std::uint64_t stores = 0;
std::uint64_t units = 0;
std::uint64_t bytes = 0;
};
DepthPlaneStoreStatistics DepthPlaneStoreCounts();
bool SampleLocationsCompatibleDepth(const Context& context, VkFormat format, std::uint32_t samples);
std::uint64_t DepthSurfacesSerial();
std::uint64_t DepthSurfacesReleased();
@@ -47,7 +54,9 @@ bool DepthSurfaceOverlaps(VkDevice device, std::uint64_t address, std::uint64_t
bool DepthSurfaceAt(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource);
std::uint64_t HtileDepthClearAddress(std::span<const std::uint32_t> code, std::span<const std::uint32_t> userData, const std::array<std::uint32_t, 3>& numThreads);
std::size_t HtileDepthClearBytes_nid_no_patch(std::span<const std::uint32_t> code, std::span<const std::uint32_t> userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, std::span<const std::uint32_t> packet, std::uint64_t indirectArguments);
void NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes, std::size_t nativeBytes = 0u);
bool NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes, std::size_t nativeBytes = 0u);
void ArmHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes);
void CompleteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes);
struct MemoryValueFill {
std::uint64_t address;
std::size_t bytes;
@@ -114,6 +114,7 @@ void SetHostImportDeferredSettles(bool enabled);
// that heap: the other half stays for the buffers, mirrors and textures that share it.
std::uint64_t HostImportBudget(const VkPhysicalDeviceMemoryProperties& memory, std::uint64_t physicalBytes);
std::uint64_t HostImportLiveBytes();
std::uint64_t HostImportBudgetRemaining(const Context& context);
struct HostImportSettleCounts {
std::uint64_t submitted = 0;
std::uint64_t waited = 0;
@@ -52,6 +52,7 @@ bool SpirvStoresThroughAddresses(std::span<const std::uint32_t> words);
// The cached storage image of a surface (render targets use it as their resident image); brought up
// to date with guest memory before it is returned.
std::shared_ptr<StorageTexture> CachedStorageSurface(const Context& context, const GuestTextureResource& resource);
std::shared_ptr<StorageTexture> CachedStorageLayers(const Context& context, const GuestTextureResource& resource, std::uint32_t firstLayer, std::uint32_t layerCount);
// Whether `image` is still the storage cache's image of its surface (what CachedStorageSurface
// would return); a true answer counts as a use for the cache's eviction order, as a lookup would.
bool StorageImageCached(const Context& context, const StorageTexture* image);
@@ -198,6 +198,10 @@ public:
// Deferred write-back (APS5_EAGER_WRITEBACK=1 stores at once instead).
void MarkDirty();
void MarkWholeWritten();
void MarkUnitsWritten(const std::vector<bool>& units);
std::vector<VkRect2D> UnitRects(const std::vector<bool>& units) const;
bool BlockUnits() const { return blockUnits; }
std::uint32_t TrackedUnits() const { return trackedLayers; }
void MarkSliceDirty(std::uint32_t slice, std::uint32_t count = 1);
void Flush();
bool Dirty() const;
@@ -350,6 +354,7 @@ public:
// validates textures copied from this image.
std::uint64_t Version() const { return version; }
std::uint64_t Generation() const { return generation; }
std::uint64_t GenerationFor(std::uint64_t address, std::uint64_t bytes) const;
// The DCC keys the content was uploaded under and the last scan of the surface's keys
// (ProvedClearKeys): Refresh's own key rule for the fast revalidation. Under
// GuestMemory::GpuMutex only, as Refresh is; never from a build's stage A.
@@ -364,6 +369,7 @@ public:
// was still current (nothing uploaded).
// Keeps the image current with guest memory (see GuestMemory::CollectWrites).
bool Refresh();
bool RefreshLayers(std::uint32_t first, std::uint32_t count);
static std::uint64_t SinglePassMoves();
static std::uint64_t SlabRetiles();
static std::uint64_t RefreshesProved();
@@ -380,9 +386,8 @@ private:
StorageTexture(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, std::uint32_t mipLevel);
// The regions of every array layer, or of the tracked layers `layers` selects.
std::vector<VkBufferImageCopy> CopyRegions(const std::vector<bool>* layers = nullptr) const;
// Uploads the surface, or only the tracked layers `layers` selects (the direct path; the others
// upload everything).
void upload(const std::vector<bool>* layers = nullptr);
std::vector<bool> cpuUploadLayers(const std::vector<bool>& units) const;
void captureGuestBytes(const std::vector<bool>* layers);
bool originalHolds(std::uint64_t address, std::size_t bytes) const;
bool compareUntracked(std::uint64_t address, std::size_t bytes, std::span<std::uint8_t> changed, bool memoize = false) const;
@@ -477,7 +482,10 @@ private:
// layer generation (the whole-surface fast check), `dirty` whether any layer is pending.
std::uint64_t layerBegin(std::uint32_t layer) const { return descriptor.baseAddress + static_cast<std::uint64_t>(layer) * trackedLayerBytes; }
bool anyLayerPending() const;
bool refresh(std::uint32_t firstLayer, std::uint32_t lastLayer);
void noteLayersBehind(std::uint32_t firstLayer, std::uint32_t lastLayer, const std::vector<bool>& outside);
void refreshGeneration();
std::uint64_t collectedLayerGeneration(std::uint32_t layer, std::uint64_t whole, bool uncached = false) const;
bool refreshProved();
void takeRefreshProof(bool aliased);
bool otherPendingOverlaps() const;
@@ -555,7 +563,13 @@ private:
std::uint64_t sliceLinearBytes = 0;
SurfaceGeometry geometry;
std::vector<std::byte> original;
mutable std::array<std::uint64_t, 5> comparedGuestBytes{};
struct ComparedRange {
std::uint64_t address = 0;
std::size_t bytes = 0;
std::array<std::uint64_t, 5> stamp{};
};
mutable std::array<ComparedRange, 4> comparedRanges{};
mutable std::uint32_t nextComparedRange = 0;
std::vector<std::byte> generationBaseline;
// DCC keys the image content was uploaded under: a fast-cleared surface starts as its clear value.
DccKeys uploadedKeys = DccKeys::Uncompressed;
@@ -581,6 +595,7 @@ private:
std::uint64_t trackedLayerBytes = 0;
std::vector<std::uint64_t> layerGeneration;
std::vector<bool> layerPending;
std::vector<bool> layerBehind;
bool blockUnits = false;
// Write-back hysteresis (block units, see writeBack): partial stores in the current window of
// presents, its first present, and the present until which every pending unit is stored.
@@ -230,14 +230,16 @@ inline std::uint32_t VertexBufferOutOfBoundsSelect(const ShaderRecompiler::Verte
inline bool VertexFetchOutOfRange(const ShaderRecompiler::VertexAttribute& attribute) {
const auto stride = (attribute.resource.fields[1] >> 16u) & 0x3fffu;
if (stride != 0 || NullVertexDescriptor(attribute)) return false;
const auto select = VertexBufferOutOfBoundsSelect(attribute);
return stride == 0 && (select == 2u || select == 3u) && attribute.resource.fields[2] == 0;
const auto records = attribute.resource.fields[2];
if (select == 0u) return true;
if (select == 3u) return records < DecodeVertexFormat(attribute).bytes;
return records == 0;
}
inline std::uint64_t ZeroStrideAvailableBytes(const ShaderRecompiler::VertexAttribute& attribute) {
const auto records = attribute.resource.fields[2];
if (VertexBufferOutOfBoundsSelect(attribute) == 2u && records != 0) return std::max<std::uint64_t>(records, DecodeVertexFormat(attribute).bytes);
return records;
return VertexFetchOutOfRange(attribute) ? 0u : DecodeVertexFormat(attribute).bytes;
}
inline std::size_t VertexBufferExtent(const ShaderRecompiler::VertexAttribute& attribute) {
@@ -261,7 +263,9 @@ inline std::size_t VertexBufferReadSize(const ShaderRecompiler::VertexAttribute&
Require(attribute.fetchIndex <= 1, "unsupported vertex fetch index");
const auto index = attribute.fetchIndex == 0 ? maxIndex : firstInstance + instances - 1u;
const auto bytes = DecodeVertexFormat(attribute).bytes;
Require(stride == 0 || VertexBufferOutOfBoundsSelect(attribute) <= 1u || index < records, "vertex fetch exceeds descriptor record count");
const auto select = VertexBufferOutOfBoundsSelect(attribute);
const auto recordsUnchecked = stride == 0 ? select != 1u : select <= 1u;
Require(recordsUnchecked || index < records, "vertex fetch exceeds descriptor record count");
const auto required = static_cast<std::uint64_t>(stride) * index + bytes;
Require(stride != 0 || required <= ZeroStrideAvailableBytes(attribute), "vertex fetch exceeds descriptor byte range");
Require(required <= std::numeric_limits<std::size_t>::max(), "vertex fetch exceeds addressable byte range");
@@ -347,7 +347,7 @@ void BdaResources::markWrittenPages() const {
Require(false, message.str());
}
}
GuestMemory::MarkWritten(begin, bytes);
GuestMemory::MarkPossiblyWritten(begin, bytes);
}), "invalid BDA written-page record");
for (std::uint32_t claim = 0; claim < count; ++claim) {
const auto slot = words[Abi::WrittenListWord + claim];
@@ -19,6 +19,7 @@
#include <limits>
#include <shared_mutex>
#include <cstdio>
#include <cstdlib>
#include <map>
#include <memory>
#include <mutex>
@@ -150,6 +151,21 @@ DepthClearCounters& depthClearCounters() {
return counters;
}
struct DepthPlaneStoreCounters {
std::atomic<std::uint64_t> stores{0};
std::atomic<std::uint64_t> units{0};
std::atomic<std::uint64_t> bytes{0};
};
DepthPlaneStoreCounters& depthPlaneStoreCounters() {
static DepthPlaneStoreCounters counters;
return counters;
}
bool anyUnit(const std::vector<bool>& units) {
return std::find(units.begin(), units.end(), true) != units.end();
}
thread_local DepthPlaneOverwrite* currentOverwrite = nullptr;
std::optional<DepthPlaneOverwrite> decodeOverwrite(const StorageImageClear& clear) {
@@ -322,6 +338,7 @@ public:
const auto clearLease = clearGeneration == 0u ? GuestAllocations::Lease{} : GuestAllocations::GuestAllocationsAcquireAll_nid_postfix();
Require(active->load(std::memory_order_acquire), "the native multisample depth owner was released");
Require(sampling.refusal.empty(), sampling.refusal);
Require(!clearPending, "native multisample depth clear has not been recorded");
for (const auto& range : sampling.initial) {
Require(GuestMemory::Accessible(reinterpret_cast<const void*>(range.address), range.bytes) && GuestMemory::Watched(range.address, range.bytes), "native multisample depth requires watched accessible guest memory");
if (const auto* recorder = RecorderFor(context)) Require(!recorder->PendingWriteOverlaps(range.address, range.bytes), "external GPU writes over native multisample depth are not implemented");
@@ -722,42 +739,166 @@ public:
else Recorder::CountBarriers(Recorder::CommandClass::Copy, 3);
}
std::shared_ptr<StorageTexture> StoreDepthPlane(const GuestTextureResource& resource) {
ApplyFastClear();
std::shared_ptr<StorageTexture> Mirror(const GuestTextureResource& resource) {
if (mirrorPlane.storage == nullptr) {
mirrorPlane.storage = StorageTexture::Create(context, *context.detiler, resource, 0);
++mirrorEpoch;
}
copyPlane(mirrorPlane, mirrorPlane.storage, false, true, true);
return mirrorPlane.storage;
}
bool SyncFromMemory(const GuestTextureResource& resource) {
bool StoresUnits(const StorageTexture& mirror) const {
return mirror.BlockUnits() && mirror.TrackedUnits() == staleUnits.size();
}
std::shared_ptr<StorageTexture> StoreDepthPlane(const GuestTextureResource& resource, std::vector<bool>& units) {
ApplyFastClear();
auto mirror = Mirror(resource);
auto& counters = depthPlaneStoreCounters();
++counters.stores;
const auto planeBytes = mirror->GuestBytes();
if (!StoresUnits(*mirror)) {
units.assign(units.size(), true);
copyPlane(mirrorPlane, mirror, false, true, true);
mirror->MarkWholeWritten();
counters.units += units.size();
counters.bytes += planeBytes;
return mirror;
}
const auto rects = mirror->UnitRects(units);
copyPlane(mirrorPlane, mirror, false, true, true, &rects);
mirror->MarkUnitsWritten(units);
for (std::size_t unit = 0; unit < units.size(); ++unit) {
if (!units[unit]) continue;
++counters.units;
counters.bytes += std::min<std::uint64_t>(unitBytes, planeBytes - unit * unitBytes);
}
return mirror;
}
void SizeUnits(std::uint64_t planeBytes) {
static const bool whole = std::getenv("APS5_DEPTH_PLANE_WHOLE_STORE") != nullptr;
const std::uint64_t bytes = !whole && target.address % 65536u == 0 ? std::uint64_t{65536} : std::max<std::uint64_t>(planeBytes, 1u);
const auto count = static_cast<std::size_t>(std::max<std::uint64_t>((planeBytes + bytes - 1u) / bytes, 1u));
if (unitBytes == bytes && staleUnits.size() == count) return;
unitBytes = bytes;
staleUnits.assign(count, false);
staleCount = 0;
unitGeneration.assign(count, 0);
}
bool AnyStale() const {
return staleCount != 0;
}
void MarkAllStale(std::uint64_t planeBytes) {
SizeUnits(planeBytes);
staleUnits.assign(staleUnits.size(), true);
staleCount = staleUnits.size();
}
void ClearStale() {
staleUnits.assign(staleUnits.size(), false);
staleCount = 0;
}
void ClearStale(const std::vector<bool>& units) {
for (std::size_t unit = 0; unit < units.size() && unit < staleUnits.size(); ++unit) {
if (!units[unit] || !staleUnits[unit]) continue;
staleUnits[unit] = false;
--staleCount;
}
}
std::vector<bool> StaleUnitsIn(std::uint64_t planeBytes, std::uint64_t begin, std::uint64_t end) const {
std::vector<bool> units(staleUnits.size(), false);
const auto plane = target.address;
if (units.empty() || unitBytes == 0 || begin >= end || begin >= plane + planeBytes || plane >= end) return units;
const auto from = std::max(begin, plane) - plane;
const auto to = std::min(end, plane + planeBytes) - plane;
const auto last = std::min<std::uint64_t>((to - 1u) / unitBytes, units.size() - 1u);
for (auto unit = from / unitBytes; unit <= last; ++unit) units[static_cast<std::size_t>(unit)] = staleUnits[static_cast<std::size_t>(unit)];
return units;
}
std::pair<std::uint64_t, std::uint64_t> UnitHull(const std::vector<bool>& units, std::uint64_t planeBytes) const {
std::uint64_t first = planeBytes;
std::uint64_t last = 0;
for (std::size_t unit = 0; unit < units.size(); ++unit) {
if (!units[unit]) continue;
first = std::min<std::uint64_t>(first, unit * unitBytes);
last = std::max<std::uint64_t>(last, std::min<std::uint64_t>(planeBytes, (unit + 1u) * unitBytes));
}
return {target.address + first, target.address + std::max(first, last)};
}
void NoteStored(const std::vector<bool>& units, std::uint64_t generation) {
for (std::size_t unit = 0; unit < units.size() && unit < unitGeneration.size(); ++unit) {
if (units[unit]) unitGeneration[unit] = generation;
}
}
std::vector<bool> WrittenUnits(const GuestTextureResource& resource, std::uint64_t& generation) {
const auto bytes = static_cast<std::size_t>(DescribeSurface(resource).guestBytes);
SizeUnits(bytes);
GuestMemory::CollectWrites(resource.baseAddress, bytes);
if (syncedGeneration == 0 || (pendingClear & VK_IMAGE_ASPECT_DEPTH_BIT) != 0 || !GuestMemory::WrittenSince(resource.baseAddress, bytes, syncedGeneration)) {
syncedGeneration = GuestMemory::TrackerGeneration();
return false;
generation = GuestMemory::TrackerGeneration();
std::vector<bool> changed(unitGeneration.size(), false);
if (unitGeneration.size() == 1u) {
changed[0] = GuestMemory::WrittenSince(resource.baseAddress, bytes, unitGeneration[0]);
return changed;
}
memoryStale = false;
if (mirrorPlane.storage == nullptr) {
mirrorPlane.storage = StorageTexture::Create(context, *context.detiler, resource, 0);
++mirrorEpoch;
const auto oldest = *std::min_element(unitGeneration.begin(), unitGeneration.end());
if (oldest != 0u && !GuestMemory::WrittenSince(resource.baseAddress, bytes, oldest)) return changed;
std::vector<std::uint8_t> stamped(unitGeneration.size(), GuestMemory::BlockUnchanged);
if (!GuestMemory::ChangedBlocks(resource.baseAddress, bytes, unitGeneration, stamped)) return changed;
for (std::size_t unit = 0; unit < changed.size(); ++unit) changed[unit] = unitGeneration[unit] != 0u && stamped[unit] == GuestMemory::BlockWritten;
return changed;
}
void SyncFromMemory(const GuestTextureResource& resource) {
std::uint64_t generation = 0;
const auto changed = WrittenUnits(resource, generation);
if ((pendingClear & VK_IMAGE_ASPECT_DEPTH_BIT) != 0 || !anyUnit(changed)) {
unitGeneration.assign(unitGeneration.size(), generation);
return;
}
ClearStale(changed);
{
struct Syncing {
bool& flag;
~Syncing() { flag = false; }
} guard{syncing};
syncing = true;
if (mirrorPlane.storage == nullptr) {
mirrorPlane.storage = StorageTexture::Create(context, *context.detiler, resource, 0);
++mirrorEpoch;
} else {
static_cast<void>(mirrorPlane.storage->Refresh());
}
}
unitGeneration.assign(unitGeneration.size(), generation);
const auto mirror = mirrorPlane.storage;
if (StoresUnits(*mirror)) {
const auto rects = mirror->UnitRects(changed);
copyPlane(mirrorPlane, mirror, false, false, true, &rects);
} else {
ClearStale();
copyPlane(mirrorPlane, mirror, false, false, true);
}
else static_cast<void>(mirrorPlane.storage->Refresh());
syncedGeneration = GuestMemory::TrackerGeneration();
copyPlane(mirrorPlane, mirrorPlane.storage, false, false, true);
written = ++depthWrites;
return true;
}
void DropMirror() {
mirrorPlane = {};
}
bool memoryStale = false;
std::vector<bool> staleUnits;
std::size_t staleCount = 0;
std::vector<std::uint64_t> unitGeneration;
std::uint64_t unitBytes = 0;
bool syncing = false;
std::uint64_t scannedEpoch = 0;
std::uint64_t syncedGeneration = 0;
const Context context;
const DepthTarget target;
@@ -770,6 +911,7 @@ public:
bool sampleable = true;
bool samplingActive = false;
DepthSamplingState sampling;
bool clearPending = false;
std::uint64_t clearGeneration = 0u;
std::shared_ptr<OwnedImage> owned;
std::shared_ptr<OwnedImage> sampleShadow;
@@ -968,33 +1110,52 @@ private:
return ResolveTextureFormat(resource.format) == (stencil ? VK_FORMAT_R8_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT);
}
void copyPlane(PlaneStorage& plane, const std::shared_ptr<StorageTexture>& storage, bool stencil, bool toStorage, bool ownBatch = false) {
void copyPlane(PlaneStorage& plane, const std::shared_ptr<StorageTexture>& storage, bool stencil, bool toStorage, bool ownBatch = false, const std::vector<VkRect2D>* rects = nullptr) {
if (rects != nullptr && rects->empty()) return;
auto* recorder = Recorder::Active();
Require(recorder != nullptr || ownBatch, "depth surface storage copies need an active recorder");
std::unique_ptr<CommandBatch> batch;
if (recorder == nullptr) batch = std::make_unique<CommandBatch>(context);
const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT;
const auto bytes = static_cast<std::size_t>(target.extent.width) * target.extent.height * (stencil ? 1u : d16 ? 2u : 4u);
const std::uint32_t texelBytes = stencil ? 1u : d16 ? 2u : 4u;
const auto bytes = static_cast<std::size_t>(target.extent.width) * target.extent.height * texelBytes;
if (plane.staging == nullptr) plane.staging = std::make_unique<DeviceBuffer>(context, bytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT);
const auto commands = recorder != nullptr ? recorder->Commands() : batch->Handle();
if (recorder != nullptr) recorder->Keep(storage);
const std::uint32_t timing = recorder != nullptr ? recorder->BeginGpuTiming(Recorder::CommandClass::Copy) : 0u;
VkBufferImageCopy planeRegion{0, 0, 0, {stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT, 0, 0, 1}, {0, 0, 0}, {target.extent.width, target.extent.height, 1}};
VkBufferImageCopy colorRegion = planeRegion;
colorRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
const VkBufferImageCopy planeRegion{0, 0, 0, {stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT, 0, 0, 1}, {0, 0, 0}, {target.extent.width, target.extent.height, 1}};
std::vector<VkBufferImageCopy> planeRegions;
std::size_t moved = bytes;
if (rects == nullptr) {
planeRegions.push_back(planeRegion);
} else {
moved = 0;
for (const auto& rect : *rects) {
auto region = planeRegion;
region.bufferOffset = (static_cast<VkDeviceSize>(rect.offset.y) * target.extent.width + static_cast<VkDeviceSize>(rect.offset.x)) * texelBytes;
region.bufferRowLength = target.extent.width;
region.imageOffset = {rect.offset.x, rect.offset.y, 0};
region.imageExtent = {rect.extent.width, rect.extent.height, 1};
planeRegions.push_back(region);
moved += static_cast<std::size_t>(rect.extent.width) * rect.extent.height * texelBytes;
}
}
auto colorRegions = planeRegions;
for (auto& region : colorRegions) region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
const auto count = static_cast<std::uint32_t>(planeRegions.size());
const auto sourceImage = toStorage ? image : storage->Image();
const auto destinationImage = toStorage ? storage->Image() : image;
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT);
context.Function<PFN_vkCmdCopyImageToBuffer>("vkCmdCopyImageToBuffer")(commands, sourceImage, VK_IMAGE_LAYOUT_GENERAL, plane.staging->Handle(), 1, toStorage ? &planeRegion : &colorRegion);
context.Function<PFN_vkCmdCopyImageToBuffer>("vkCmdCopyImageToBuffer")(commands, sourceImage, VK_IMAGE_LAYOUT_GENERAL, plane.staging->Handle(), count, toStorage ? planeRegions.data() : colorRegions.data());
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT);
context.Function<PFN_vkCmdCopyBufferToImage>("vkCmdCopyBufferToImage")(commands, plane.staging->Handle(), destinationImage, VK_IMAGE_LAYOUT_GENERAL, 1, toStorage ? &colorRegion : &planeRegion);
context.Function<PFN_vkCmdCopyBufferToImage>("vkCmdCopyBufferToImage")(commands, plane.staging->Handle(), destinationImage, VK_IMAGE_LAYOUT_GENERAL, count, toStorage ? colorRegions.data() : planeRegions.data());
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT);
if (batch) {
batch->SubmitAndWait();
return;
}
Recorder::CountBarriers(Recorder::CommandClass::Copy, 3);
recorder->EndGpuTiming(timing, bytes);
recorder->EndGpuTiming(timing, moved);
}
void release() noexcept {
@@ -1604,15 +1765,36 @@ void retire(DepthSurface& surface) {
for (auto* owners : {&depthOwners(), &stencilOwners()}) {
std::erase_if(*owners, [&](const auto& entry) { return entry.second.surface == &surface; });
}
if (!surface.memoryStale) return;
surface.memoryStale = false;
if (!surface.AnyStale()) return;
surface.ClearStale();
const auto plane = stalePlaneRange(surface);
if (std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& other) { return other->memoryStale && stalePlaneRange(*other) == plane; })) return;
if (std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& other) { return other->AnyStale() && stalePlaneRange(*other) == plane; })) return;
auto& planes = stalePlanes();
std::lock_guard lock(planes.mutex);
std::erase(planes.ranges, plane);
publishStaleHull(planes);
}
void retireStoringStale(DepthSurface& surface) {
if (!surface.AnyStale() || !mirrorable(surface.target)) {
retire(surface);
return;
}
const auto resource = mirrorResource(surface.target);
const auto planeBytes = DescribeSurface(resource).guestBytes;
std::uint64_t generation = 0;
const auto written = surface.WrittenUnits(resource, generation);
auto units = surface.staleUnits;
for (std::size_t unit = 0; unit < units.size() && unit < written.size(); ++unit) {
if (written[unit]) units[unit] = false;
}
std::shared_ptr<StorageTexture> mirror;
if (anyUnit(units) && (!anyUnit(written) || surface.StoresUnits(*surface.Mirror(resource)))) mirror = surface.StoreDepthPlane(resource, units);
retire(surface);
if (mirror == nullptr) return;
const auto [begin, end] = surface.UnitHull(units, planeBytes);
if (StorageTexture::FlushPending(begin, static_cast<std::size_t>(end - begin), nullptr, "depth plane store")) surface.NoteStored(units, GuestMemory::TrackerGeneration());
}
}
std::uint64_t DepthSurfacesSerial() {
@@ -1775,7 +1957,7 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target,
if (!created) surface->NoteHtileClear(VK_IMAGE_ASPECT_DEPTH_BIT);
}
if (!created) surface->NoteHtileClear(htileCleared);
if (mirrorable(target)) static_cast<void>(surface->SyncFromMemory(mirrorResource(target)));
if (mirrorable(target)) surface->SyncFromMemory(mirrorResource(target));
surface->ApplyFastClear();
if (drawnDepth != nullptr && drawnDepth != surface) drawnDepth->written = ++depthWrites;
surface->written = ++depthWrites;
@@ -1787,14 +1969,15 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target,
if (other.get() == surface || other->context.device != context.device || other->written >= surface->written || !mirrorable(other->target)) continue;
const auto otherPlane = stalePlaneRange(*other);
if (otherPlane == plane || otherPlane.first >= plane.second || plane.first >= otherPlane.second) continue;
other->memoryStale = false;
other->ClearStale();
other->DropMirror();
}
}
if ((depthWritten || cleared) && mirrorable(target)) {
surface->memoryStale = true;
const auto plane = target.address;
noteStalePlane(plane, plane + DescribeSurface(mirrorResource(target)).guestBytes);
const auto planeBytes = DescribeSurface(mirrorResource(target)).guestBytes;
surface->MarkAllStale(planeBytes);
noteStalePlane(plane, plane + planeBytes);
}
drawnDepth = surface;
return surface->view;
@@ -1831,52 +2014,78 @@ bool DepthPlanesStale() {
return planes.begin.load(std::memory_order_acquire) < planes.end.load(std::memory_order_acquire);
}
std::vector<std::pair<std::uint64_t, std::uint64_t>> FlushDepthPlanes(std::uint64_t address, std::size_t bytes) {
std::vector<std::pair<std::uint64_t, std::uint64_t>> FlushDepthPlanes(std::uint64_t address, std::size_t bytes, DepthPlaneScope scope) {
static thread_local bool flushing = false;
const auto end = bytes > std::numeric_limits<std::uint64_t>::max() - address ? std::numeric_limits<std::uint64_t>::max() : address + bytes;
if (flushing || !overlapsStalePlane(address, end)) return {};
if (scope == DepthPlaneScope::Overlapped) {
std::lock_guard lock(surfacesMutex());
const auto staleInRange = [&](const auto& surface) { return surface->AnyStale() && !surface->syncing && ((surface->pendingClear & VK_IMAGE_ASPECT_DEPTH_BIT) != 0 || anyUnit(surface->StaleUnitsIn(DescribeSurface(mirrorResource(surface->target)).guestBytes, address, end))); };
if (std::none_of(surfaces().begin(), surfaces().end(), staleInRange)) return {};
}
flushing = true;
struct Reset {
~Reset() { flushing = false; }
} reset;
std::vector<std::pair<DepthSurface*, std::shared_ptr<StorageTexture>>> stored;
struct Stored {
DepthSurface* surface;
std::shared_ptr<StorageTexture> mirror;
std::vector<bool> units;
std::uint64_t begin;
std::uint64_t end;
bool flushed;
};
std::vector<Stored> stored;
std::vector<std::pair<std::uint64_t, std::uint64_t>> ranges;
{
std::lock_guard gpu(GuestMemory::GpuMutex());
std::lock_guard lock(surfacesMutex());
std::vector<std::pair<std::uint64_t, std::uint64_t>> remaining;
for (const auto& surface : surfaces()) {
if (!surface->memoryStale) continue;
if (!surface->AnyStale()) continue;
const auto resource = mirrorResource(surface->target);
const auto plane = surface->target.address;
const auto planeBytes = DescribeSurface(resource).guestBytes;
if (plane >= end || address >= plane + planeBytes) {
remaining.emplace_back(plane, plane + planeBytes);
continue;
const bool overlapped = !surface->syncing && plane < end && address < plane + planeBytes;
if (overlapped && (surface->pendingClear & VK_IMAGE_ASPECT_DEPTH_BIT) != 0) surface->MarkAllStale(planeBytes);
if (overlapped && (scope == DepthPlaneScope::Whole || anyUnit(surface->StaleUnitsIn(planeBytes, address, end)))) {
surface->SyncFromMemory(resource);
auto units = scope == DepthPlaneScope::Whole ? surface->staleUnits : surface->StaleUnitsIn(planeBytes, address, end);
if (anyUnit(units)) {
auto mirror = surface->StoreDepthPlane(resource, units);
surface->ClearStale(units);
const auto hull = surface->UnitHull(units, planeBytes);
ranges.emplace_back(hull.first, hull.second - hull.first);
stored.push_back({surface.get(), std::move(mirror), std::move(units), hull.first, hull.second, false});
}
}
surface->memoryStale = false;
if (surface->SyncFromMemory(resource)) continue;
stored.emplace_back(surface.get(), surface->StoreDepthPlane(resource));
ranges.emplace_back(plane, planeBytes);
if (surface->AnyStale()) remaining.emplace_back(plane, plane + planeBytes);
}
auto& planes = stalePlanes();
std::lock_guard staleLock(planes.mutex);
planes.ranges = std::move(remaining);
publishStaleHull(planes);
}
for (const auto& entry : stored) entry.second->MarkWholeWritten();
for (const auto& [plane, planeBytes] : ranges) StorageTexture::FlushPending(plane, static_cast<std::size_t>(planeBytes), nullptr, "depth plane store");
for (auto& entry : stored) entry.flushed = StorageTexture::FlushPending(entry.begin, static_cast<std::size_t>(entry.end - entry.begin), nullptr, "depth plane store");
const auto generation = GuestMemory::TrackerGeneration();
std::lock_guard lock(surfacesMutex());
for (const auto& entry : stored) {
if (std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& candidate) { return candidate.get() == entry.first; })) {
entry.first->syncedGeneration = generation;
entry.first->NoteMemory();
}
if (!entry.flushed || std::none_of(surfaces().begin(), surfaces().end(), [&](const auto& candidate) { return candidate.get() == entry.surface; })) continue;
entry.surface->NoteStored(entry.units, generation);
entry.surface->NoteMemory();
}
return ranges;
}
DepthPlaneStoreStatistics DepthPlaneStoreCounts() {
const auto& counters = depthPlaneStoreCounters();
DepthPlaneStoreStatistics statistics;
statistics.stores = counters.stores.load();
statistics.units = counters.units.load();
statistics.bytes = counters.bytes.load();
return statistics;
}
std::uint64_t HtileDepthClearAddress(std::span<const std::uint32_t> code, std::span<const std::uint32_t> userData, const std::array<std::uint32_t, 3>& numThreads) {
static constexpr std::array<std::uint32_t, 17> htileMaskKernel{0xd7460000u, 0x04010c06u, 0x34000084u, 0xdc388000u, 0x04020000u, 0xbf8c3f70u, 0xd7710008u, 0x00120805u, 0xd7710009u, 0x00120a05u, 0xd771000au, 0x00120c05u, 0xd771000bu, 0x00120e05u, 0xdc788000u, 0x00020800u, 0xbf810000u};
if (userData.size() < 6 || numThreads[0] != 64 || numThreads[1] != 1 || numThreads[2] != 1) return 0;
@@ -1977,17 +2186,21 @@ std::size_t HtileDepthClearBytes_nid_no_patch(std::span<const std::uint32_t> cod
return address <= std::numeric_limits<std::uint64_t>::max() - bytes ? static_cast<std::size_t>(bytes) : 0u;
}
void NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes, std::size_t nativeBytes) {
bool NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes, std::size_t nativeBytes) {
std::lock_guard lock(surfacesMutex());
forgetHtileReads(htileAddress, bytes);
bool existing = false;
bool completionCandidate = false;
for (const auto& surface : surfaces()) {
if (!surface->active->load(std::memory_order_acquire)) continue;
if (surface->samplingActive && surface->target.htileAddress == htileAddress) {
const auto& metadata = surface->sampling.initial[1];
if (nativeBytes != bytes || nativeBytes < metadata.bytes || !GuestMemory::Accessible(reinterpret_cast<const void*>(metadata.address), metadata.bytes) || !GuestMemory::Watched(metadata.address, metadata.bytes)) surface->sampling.Refuse("native multisample depth incomplete or unproven fast-clear transition is not implemented");
else if (surface->clearGeneration != 0u) surface->sampling.Refuse("overlapping native multisample depth clear transitions are not implemented");
else surface->clearGeneration = GuestMemory::CollectWritesUncached(metadata.address, metadata.bytes);
else if (surface->clearPending || surface->clearGeneration != 0u) surface->sampling.Refuse("overlapping native multisample depth clear transitions are not implemented");
else {
surface->clearPending = true;
completionCandidate = true;
}
existing = true;
continue;
}
@@ -1997,6 +2210,30 @@ void NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes, std::siz
existing = true;
}
if (!existing) clearedHtiles()[htileAddress] = bytes;
return completionCandidate;
}
void ArmHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes) {
std::lock_guard lock(surfacesMutex());
for (const auto& surface : surfaces()) {
if (!surface->active->load(std::memory_order_acquire) || !surface->samplingActive || surface->target.htileAddress != htileAddress || !surface->clearPending) continue;
const auto& metadata = surface->sampling.initial[1];
if (bytes < metadata.bytes) continue;
surface->clearGeneration = GuestMemory::CollectWritesUncached(metadata.address, metadata.bytes);
surface->clearPending = false;
if (surface->clearGeneration == 0u) surface->sampling.Refuse("native multisample depth clear requires tracked HTILE generations");
}
}
void CompleteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes) {
std::lock_guard lock(surfacesMutex());
for (const auto& surface : surfaces()) {
if (!surface->active->load(std::memory_order_acquire) || !surface->samplingActive || surface->target.htileAddress != htileAddress || surface->clearGeneration == 0u) continue;
const auto& metadata = surface->sampling.initial[1];
if (bytes < metadata.bytes) continue;
Require(GuestMemory::UnchangedSinceCollected(metadata.address, metadata.bytes, surface->clearGeneration), "CPU writes raced native multisample depth clear");
GuestMemory::MarkWritten(metadata.address, metadata.bytes);
}
}
void ClearDepthSurfaces(VkDevice device) {
@@ -2053,7 +2290,7 @@ std::shared_ptr<Texture> DepthSurfaceTexture(const Context& context, std::span<c
const auto copyFormat = integer ? VK_FORMAT_R16_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT;
const bool planes = (format == copyFormat || depthBits) && resource.width == base.extent.width && resource.height == base.extent.height && resource.baseLevel == 0 && resource.lastLevel == 0 && !(resource.baseArray != 0 && (cube || !layered || resource.baseArray > resource.depthOrLastArray)) && !(cube && resource.depthOrLastArray != 0 && resource.depthOrLastArray != 5) && (resource.dimension == TextureDimension::k2D || resource.dimension == TextureDimension::k2DArray || cube);
if (!(sampled ? found->SampledAccepts(words, resource) : planes) && found->OverwrittenInMemory()) {
retire(*found);
retireStoringStale(*found);
return nullptr;
}
found->ApplyFastClear();
@@ -2148,8 +2148,66 @@ std::optional<std::string> KnownValidationFailure(const Context& context, std::s
return std::nullopt;
}
void Draw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots, std::shared_ptr<const DrawRecipe>* recipeOut) {
namespace {
struct TargetBinding {
ColorTarget color;
bool gpuTiling = false;
std::shared_ptr<StorageTexture> resident;
bool proxied = false;
std::unique_ptr<Buffer> transfer;
std::unique_ptr<Buffer> tiled;
std::unique_ptr<DeviceBuffer> tiledDevice;
std::unique_ptr<DeviceBuffer> linearDevice;
std::vector<std::byte> original;
TileMipLayout mip{};
std::unique_ptr<RenderTarget> target;
bool multisampled = false;
VkImage multisampleImage = VK_NULL_HANDLE;
std::shared_ptr<void> multisampleOwner;
std::unique_ptr<Buffer> dump;
};
struct PreparedDraw {
explicit PreparedDraw(bool profile) : timer(profile) {}
PreparedDraw(const PreparedDraw&) = delete;
PreparedDraw& operator=(const PreparedDraw&) = delete;
std::shared_ptr<const DrawRecipe>* recipeOut = nullptr;
std::vector<std::uint32_t> stencilReferences;
std::optional<PerformanceTimer> timing;
DrawTimer timer;
DrawOutcome outcome;
const ShaderResources::BuildTiming* built = nullptr;
double waitedBefore = 0;
double ownWaitedMs = 0;
DrawInputs inputs;
const Pm4::DrawParameters::IndirectDraw* args = nullptr;
int dumpLimit = 0;
std::vector<TargetBinding> targets;
std::vector<VkImageView> targetViews;
Recorder* recorder = nullptr;
ResolvedResources resolved;
IndirectRecord indirect;
std::vector<Pm4::DrawArguments> records;
bool completion = false;
bool listed = false;
bool recorded = false;
bool lean = false;
bool multisampled = false;
std::optional<State> masked;
std::vector<std::pair<std::uint32_t, std::shared_ptr<Pipeline>>> stencilReplay;
std::shared_ptr<Pipeline> pipeline;
std::vector<std::shared_ptr<StorageTexture>> owners;
std::vector<std::shared_ptr<void>> framebufferOwners;
std::shared_ptr<Framebuffer> framebuffer;
std::vector<SampleOrderTarget> reordered;
SampleOrderPass reorderPass;
std::optional<DepthSampleLocations> depthLocations;
};
bool prepareDraw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots, PreparedDraw& prepared) {
auto*& recipeOut = prepared.recipeOut;
auto& stencilReferences = prepared.stencilReferences;
for (const auto& shader : shaders) {
if (shader.program != nullptr && !shader.program->stencilReferences.empty()) {
Require(shader.stage == ShaderRecompiler::ShaderStage::Fragment && stencilReferences.empty(), "stencil replay requires exactly one fragment stage");
@@ -2174,58 +2232,35 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
}
if (state.quadList) recipeOut = nullptr;
if (context.recorder != nullptr) context.recorder->BoundKeptBytes();
PerformanceTimer timing("Graphics.Draw");
auto& timing = prepared.timing.emplace("Graphics.Draw");
// APS5_PROFILE_DRAW prints the time of each phase of the draw (microseconds) and the [draws] totals.
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
DrawTimer timer(profile);
auto& timer = prepared.timer;
timer = DrawTimer(profile);
// APS5_TRACE_DRAWS (together with APS5_PROFILE_DRAW; alone it only enables Driver.cpp's own
// [draw] lines) additionally prints one line per draw with its phases and, for synchronous
// draws, their inputs: per-draw string building at ~1400 draws per 10 s is itself a cost the
// totals must not carry.
static const bool traceDraws = std::getenv("APS5_TRACE_DRAWS") != nullptr;
DrawOutcome outcome;
const ShaderResources::BuildTiming* built = nullptr;
auto& outcome = prepared.outcome;
auto& built = prepared.built;
// The thread's GPU waits so far: the difference at the report, less the draw's own syncs
// (`ownWaitedMs`, sampled around them), is what this draw waited for through the flush hook
// (nested in the draw's hold, see DrawOutcome::hookWaitUs).
const auto waitedBefore = profile ? Recorder::ThreadWaitedMs() : 0.0;
double ownWaitedMs = 0;
const auto waitedBefore = prepared.waitedBefore = profile ? Recorder::ThreadWaitedMs() : 0.0;
auto& ownWaitedMs = prepared.ownWaitedMs;
const auto report = [&](const char* suffix) { reportDrawEnd(state, timer, built, outcome, waitedBefore, ownWaitedMs, suffix); };
auto inputs = prepareDrawInputs(context, state, draw, shaders, outcome, timer, nullptr);
if (inputs.nothing) return;
const auto* args = draw.indirect ? &*draw.indirect : nullptr;
auto& inputs = prepared.inputs;
inputs = prepareDrawInputs(context, state, draw, shaders, outcome, timer, nullptr);
if (inputs.nothing) return false;
const auto* args = prepared.args = draw.indirect ? &*draw.indirect : nullptr;
const auto indexBytes = inputs.indexBytes;
timing.Mark("validate");
timing.Mark("index_upload");
// One binding per color attachment. Tiled targets are detiled and retiled on the GPU in video memory;
// only changed guest blocks are stored back.
struct TargetBinding {
ColorTarget color;
bool gpuTiling = false;
// Resident target: the surface's cached storage image is attached directly and marked dirty
// afterwards, so nothing is copied in or out per draw.
std::shared_ptr<StorageTexture> resident;
bool proxied = false;
// Linear pixels converted on the CPU, for targets the GPU detiler does not handle.
std::unique_ptr<Buffer> transfer;
// Guest bytes in host memory, and their device-local copies for the detiler.
std::unique_ptr<Buffer> tiled;
std::unique_ptr<DeviceBuffer> tiledDevice;
std::unique_ptr<DeviceBuffer> linearDevice;
std::vector<std::byte> original;
TileMipLayout mip{};
std::unique_ptr<RenderTarget> target;
bool multisampled = false;
VkImage multisampleImage = VK_NULL_HANDLE;
std::shared_ptr<void> multisampleOwner;
// APS5_DUMP_TARGETS: the rendered linear pixels, read back for target_<address>_<n>.raw.
std::unique_ptr<Buffer> dump;
};
const int dumpLimit = DumpTargetLimit();
static std::mutex dumpMutex;
static std::map<std::uint64_t, int> dumped;
std::vector<TargetBinding> targets(state.colors.size());
std::vector<VkImageView> targetViews;
const int dumpLimit = prepared.dumpLimit = DumpTargetLimit();
auto& targets = prepared.targets;
targets.resize(state.colors.size());
auto& targetViews = prepared.targetViews;
for (std::size_t index = 0; index < state.colors.size(); ++index) {
auto& binding = targets[index];
binding.color = state.colors[index];
@@ -2240,7 +2275,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
}
APS5_LOG_OUT_DEBUG("Creating color target %zu address=0x%llx bytes=%llu extent=%ux%u", index, static_cast<unsigned long long>(color.address), static_cast<unsigned long long>(color.bytes), color.extent.width, color.extent.height);
const ColorTargetLayout colorLayout(color.extent.width, color.extent.height, color.tileMode, color.elementBytes);
static_cast<void>(FlushDepthPlanes(color.address, colorLayout.Bytes()));
static_cast<void>(FlushDepthPlanes(color.address, colorLayout.Bytes(), DepthPlaneScope::Whole));
RetireDepthSurfaces(context.device, color.address, colorLayout.Bytes());
constexpr VkBufferUsageFlags copies = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
timer.phase(PhaseSetup);
@@ -2264,7 +2299,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
const auto geometry = DescribeSurface(array);
for (std::uint32_t slice = 0; slice <= color.arrayLastSlice; ++slice) Require(geometry.GuestLayerOffset(slice) == slice * sliceBytes, "the array image's layer pitch differs from the color view's slice offset");
Require(color.surfaceAddress == color.arrayAddress + color.arraySlice * sliceBytes, "the array image's layer pitch differs from the color view's slice offset");
auto resident = CachedStorageSurface(context, array);
auto resident = CachedStorageLayers(context, array, color.arraySlice, color.layers);
Require(resident->Attachable(), "storage format cannot be a color attachment");
return resident;
}
@@ -2320,9 +2355,10 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
if (state.depth) targetViews.push_back(DepthSurfaceView(context, *state.depth, state.depthWrite));
timer.phase(PhasePrepare);
const bool recordDraws = RecordDraws();
auto* recorder = Recorder::Active();
auto* recorder = prepared.recorder = Recorder::Active();
bool recordable = recordDraws && recorder != nullptr && dumpLimit == 0 && std::all_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.resident != nullptr || binding.multisampled; });
auto resolved = resolveDrawResources(context, state, draw, shaders, snapshots, indexBytes, recordable, outcome, timer);
auto& resolved = prepared.resolved;
resolved = resolveDrawResources(context, state, draw, shaders, snapshots, indexBytes, recordable, outcome, timer);
auto& resources = resolved.resources;
if (!stencilReferences.empty() && resources->ChecksFaults()) recordable = false;
if (!stencilReferences.empty()) {
@@ -2362,9 +2398,9 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
// and records plain draws with the same per-dimension rules. The import pointer stays valid:
// refreshImports runs only under GuestMemory::GpuMutex, held here, and a retired import's
// buffer is kept while the recorder is busy.
IndirectRecord indirect;
auto& indirect = prepared.indirect;
indirect.args = args;
std::vector<Pm4::DrawArguments> records;
auto& records = prepared.records;
if (args != nullptr) {
static const bool gpuIndirectDraws = std::getenv("APS5_NO_GPU_INDIRECT_DRAW") == nullptr;
const auto decide = [&](std::uint64_t address, std::size_t bytes, const HostImport*& import) {
@@ -2385,7 +2421,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
const auto rangeBytes = args->RangeBytes();
if (rangeBytes == 0) {
report(" indirect draw without records");
return;
return false;
}
Require(rangeBytes <= std::numeric_limits<std::size_t>::max(), "indirect draw record range overflow");
if (!gpuIndirectDraws) indirect.path = IndirectDrawPath::Disabled;
@@ -2406,7 +2442,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
if (std::none_of(records.begin(), records.end(), [](const Pm4::DrawArguments& record) { return record.count != 0 && record.instances != 0; })) {
CountIndirectDraw(indirect.path, indirect.readMs);
report(" indirect draw with empty records");
return;
return false;
}
}
}
@@ -2431,14 +2467,14 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
static const bool syncCompletionDraws = std::getenv("APS5_SYNC_COMPLETION_DRAWS") != nullptr;
static const bool recorderSyncDraws = std::getenv("APS5_NO_RECORDER_SYNC_DRAWS") == nullptr;
static const bool recordCopiedDraws = std::getenv("APS5_NO_RECORD_COPIED_DRAWS") == nullptr;
const bool completion = resources->NeedsCompletion();
const bool completion = prepared.completion = resources->NeedsCompletion();
const bool copiedWrites = completion && resources->HasCopiedWrites();
// A lease only forces the wait when deferred release is off.
const bool lease = resources->HoldsLease() && SyncLeaseWork();
outcome.completion = completion;
outcome.recorded = recordable;
// Whether the copied write-back is listed in DrawCopiedWriters instead of waited for.
bool listed = false;
auto& listed = prepared.listed;
if (!recordable) {
outcome.reason = !recordDraws || dumpLimit != 0 ? SyncDisabled : recorder == nullptr ? SyncNoRecorder : SyncNotResident;
} else if (completion && (syncCompletionDraws || copiedWrites || lease)) {
@@ -2448,7 +2484,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
else if (recorderSyncDraws) outcome.waited = true;
else outcome.recorded = false;
}
const bool recorded = outcome.recorded;
const bool recorded = prepared.recorded = outcome.recorded;
// A build this recorded draw can share with later identical ones goes into the cache (a cache
// hit is reusable by construction, so `recorded` holds for it; one with completion work is
// never reusable).
@@ -2457,8 +2493,8 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
// aid: APS5_DRAW_TRANSITIONS=1 keeps the per-draw upload and download barriers, the layout
// transitions of every target and one pass per draw, as before.
static const bool drawTransitions = std::getenv("APS5_DRAW_TRANSITIONS") != nullptr;
const bool lean = recorded && !drawTransitions;
const bool multisampled = std::any_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.multisampled; });
const bool lean = prepared.lean = recorded && !drawTransitions;
const bool multisampled = prepared.multisampled = std::any_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.multisampled; });
Require(lean || !multisampled, "multisampled draws are only rendered as recorded draws");
for (const auto& binding : targets) {
if (binding.multisampled) Require(std::find(resources->MultisampleSources().begin(), resources->MultisampleSources().end(), binding.color.surfaceAddress) == resources->MultisampleSources().end(), "sampling a multisampled color target the draw renders to is not implemented");
@@ -2470,7 +2506,8 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
if (state.depth) RequireNoNativeDepthFeedback_nid_no_patch(context, *state.depth, resources->SampledTextures());
APS5_LOG_CHARS_OUT_DEBUG("Creating Pipeline");
// The state is copied only when a mask must change.
auto masked = maskedState(state, inputs.fragmentOutputs);
auto& masked = prepared.masked;
masked = maskedState(state, inputs.fragmentOutputs);
if (std::any_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.proxied; })) recipeOut = nullptr;
for (std::size_t index = 0; index < targets.size(); ++index) {
if (!targets[index].proxied) continue;
@@ -2485,7 +2522,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
const auto built = resources->SetLayouts();
Require(described.layouts.main == built.main && described.layouts.fragment == built.fragment && described.layouts.split == built.split && std::ranges::equal(described.mainKey, built.mainKey) && std::ranges::equal(described.fragmentKey, built.fragmentKey), "set layouts described from the shaders differ from the draw's");
}
std::vector<std::pair<std::uint32_t, std::shared_ptr<Pipeline>>> stencilReplay;
auto& stencilReplay = prepared.stencilReplay;
for (const auto reference : stencilReferences) {
auto selected = pipelineState;
selected.stencilReplayReference = reference;
@@ -2495,20 +2532,22 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
Require(!selectedPipeline->SplitsFaces() && !selectedPipeline->SplitsSecondChannel(), "stencil replay refuses pipeline face or channel splits");
stencilReplay.emplace_back(reference, std::move(selectedPipeline));
}
auto pipeline = stencilReplay.empty() ? CachedPipeline(context, pipelineState, inputs.VertexLayout(), *resources, shaders, lean ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) : stencilReplay.front().second;
auto& pipeline = prepared.pipeline;
pipeline = stencilReplay.empty() ? CachedPipeline(context, pipelineState, inputs.VertexLayout(), *resources, shaders, lean ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) : stencilReplay.front().second;
// Resident targets' views are stable while their storage image lives, so the framebuffer is
// reused with the pipeline; a per-draw RenderTarget gets a framebuffer of its own.
std::vector<std::shared_ptr<StorageTexture>> owners;
auto& owners = prepared.owners;
owners.reserve(targets.size());
std::vector<std::shared_ptr<void>> framebufferOwners;
auto& framebufferOwners = prepared.framebufferOwners;
framebufferOwners.reserve(targets.size());
for (const auto& binding : targets) {
owners.push_back(binding.resident);
framebufferOwners.push_back(binding.multisampled ? binding.multisampleOwner : binding.resident);
}
auto framebuffer = pipeline->AcquireFramebuffer(targetViews, framebufferOwners, state.renderExtent, state.renderLayers);
std::vector<SampleOrderTarget> reordered;
SampleOrderPass reorderPass;
auto& framebuffer = prepared.framebuffer;
framebuffer = pipeline->AcquireFramebuffer(targetViews, framebufferOwners, state.renderExtent, state.renderLayers);
auto& reordered = prepared.reordered;
auto& reorderPass = prepared.reorderPass;
if (pipeline->ReordersSamples()) {
reorderPass = SampleOrderPassFor(context, pipelineState, pipeline->HostSampleOrder());
for (const auto& step : PlanSampleOrder(context, state, pipeline->HostSampleOrder())) {
@@ -2517,9 +2556,50 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
Require(reordered.back().method == step.method, "a reordered attachment's surface disagrees with the planned way of putting its samples in order");
}
}
const auto depthLocations = passDepthLocations(context, pipelineState, *pipeline);
prepared.depthLocations = passDepthLocations(context, pipelineState, *pipeline);
timer.phase(PhasePipeline);
APS5_LOG_CHARS_OUT_DEBUG("Pipeline created");
return true;
}
void recordPreparedDraw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, PreparedDraw& prepared) {
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
static const bool traceDraws = std::getenv("APS5_TRACE_DRAWS") != nullptr;
static std::mutex dumpMutex;
static std::map<std::uint64_t, int> dumped;
auto& timing = *prepared.timing;
auto& timer = prepared.timer;
auto& outcome = prepared.outcome;
const auto* built = prepared.built;
const auto waitedBefore = prepared.waitedBefore;
auto& ownWaitedMs = prepared.ownWaitedMs;
const auto report = [&](const char* suffix) { reportDrawEnd(state, timer, built, outcome, waitedBefore, ownWaitedMs, suffix); };
auto* recipeOut = prepared.recipeOut;
const auto& stencilReferences = prepared.stencilReferences;
auto& inputs = prepared.inputs;
const auto* args = prepared.args;
const int dumpLimit = prepared.dumpLimit;
auto& targets = prepared.targets;
const auto& targetViews = prepared.targetViews;
auto* recorder = prepared.recorder;
auto& resolved = prepared.resolved;
auto& resources = resolved.resources;
const auto& contentKey = resolved.contentKey;
const bool cacheable = resolved.cacheable;
auto& indirect = prepared.indirect;
const bool completion = prepared.completion;
const bool listed = prepared.listed;
const bool recorded = prepared.recorded;
const bool lean = prepared.lean;
const bool multisampled = prepared.multisampled;
const auto& masked = prepared.masked;
const auto& stencilReplay = prepared.stencilReplay;
const auto& pipeline = prepared.pipeline;
const auto& owners = prepared.owners;
const auto& framebuffer = prepared.framebuffer;
const auto& reordered = prepared.reordered;
const auto& reorderPass = prepared.reorderPass;
const auto& depthLocations = prepared.depthLocations;
if (lean) {
RecordedDraw record;
record.recorder = recorder;
@@ -2844,6 +2924,15 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
APS5_LOG_CHARS_OUT_DEBUG("Draw finished");
}
}
void Draw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots, std::shared_ptr<const DrawRecipe>* recipeOut) {
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
PreparedDraw prepared(profile);
prepared.recipeOut = recipeOut;
if (prepareDraw(context, state, draw, shaders, snapshots, prepared)) recordPreparedDraw(context, state, draw, shaders, prepared);
}
DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots, const DrawRecipe& recipe) {
if (context.recorder != nullptr) context.recorder->BoundKeptBytes();
static_cast<void>(snapshots);
@@ -2888,7 +2977,8 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con
auto stored = recipe.targets[index].lock();
if (stored == nullptr || !StorageImageCached(context, stored.get()) || !StorageImageServesKeys(*stored, state.colors[index].dccAddress)) return miss(DrawRecipeMiss::TargetGone);
auto resident = refreshResidentTarget(context, state, state.colors[index], outcome, profile, [&] {
stored->Refresh();
if (state.colors[index].arrayLastSlice != 0) stored->RefreshLayers(state.colors[index].arraySlice, 1);
else stored->Refresh();
return stored;
});
timer.phase(PhaseReadTarget);
@@ -232,7 +232,6 @@ struct HostImports {
PFN_vkDestroyBuffer destroyBuffer = nullptr;
PFN_vkFreeMemory freeMemory = nullptr;
std::map<std::uint64_t, HostImport> imports;
std::map<std::uint64_t, HostImport> trimmed;
std::map<VkDevice, std::vector<std::shared_ptr<RetiredImport>>> parked;
std::uint64_t liveBytes = 0;
std::map<std::uint64_t, ImportFailure> failed;
@@ -300,7 +299,7 @@ SettleBatch* forgetSettle(HostImports& state, std::uint64_t base) {
SettleBatch* running = nullptr;
if (const auto settle = state.settles.find(found->second.device); found->second.phase == SettlePhase::Submitted && settle != state.settles.end()) {
auto& batch = settle->second.batches[found->second.batch];
if (settleBatchRunning(settle->second, settle->first, batch)) running = &batch;
if (batch.busy) running = &batch;
}
state.pendingSettles.erase(found);
return running;
@@ -427,6 +426,8 @@ bool releaseSettleRetired(HostImports& state, std::uint64_t begin, std::uint64_t
++state.settleUnmapWaits;
waitSettleBatch(settle, device, batch);
}
for (const auto alias : batch.aliases) settle.settleDestroyBuffer(device, alias, nullptr);
batch.aliases.clear();
for (auto& holder : batch.retired) released.push_back(std::move(holder));
batch.retired.clear();
}
@@ -458,64 +459,6 @@ void shareDmaBufImport(const Context& context, VkBufferCreateInfo& info, ImportF
}
#endif
void releaseTrimmed(HostImports& state, ImportNode node) {
state.liveBytes -= node.mapped().pinnedBytes;
auto holder = std::make_shared<RetiredImport>(state.device, state.destroyBuffer, state.freeMemory, std::move(node));
auto* recorder = Recorder::Active();
if (recorder != nullptr) recorder->KeepThroughHold(std::move(holder));
}
void dropTrimmed(HostImports& state, std::uint64_t begin, std::uint64_t end) {
for (auto it = state.trimmed.begin(); it != state.trimmed.end();) {
const auto next = std::next(it);
if (it->first < end && begin < it->first + it->second.bytes) releaseTrimmed(state, state.trimmed.extract(it));
it = next;
}
}
void retireOrTrim(VkDevice device, HostImports& state, std::map<std::uint64_t, HostImport>::iterator it, const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) {
const auto& entry = it->second;
const auto entryEnd = entry.base + entry.bytes;
std::uint64_t first = 0;
std::uint64_t last = 0;
if (begin <= entry.base && end > entry.base && end < entryEnd) {
first = end;
last = entryEnd;
} else if (begin > entry.base && begin < entryEnd && end >= entryEnd) {
first = entry.base;
last = begin;
} else if (begin > entry.base && begin < end && end < entryEnd) {
const bool tail = entryEnd - end >= begin - entry.base;
first = tail ? end : entry.base;
last = tail ? entryEnd : begin;
}
const bool plain = entry.alias == nullptr && !entry.dmaBuf && entry.chunk == nullptr && !entry.joined && entry.ranges.size() == 1 && entry.backingSerial != 0 && entry.memoryType != std::numeric_limits<std::uint32_t>::max() && entry.base == entry.range->address && entry.bytes == entry.range->bytes;
#ifdef _WIN32
const bool settling = state.pendingSettles.contains(it->first);
#else
const bool settling = false;
#endif
if (first == last || !plain || settling || state.trimmed.contains(first)) {
retireImport(device, state, it, lease, false);
return;
}
RetireShadow(device, entry, [&lease](std::uint64_t from, std::uint64_t to) { return runCovers(lease, from, to); });
++state.epoch;
auto node = state.imports.extract(it);
auto& kept = node.mapped();
std::shared_ptr<void> holder(nullptr, [device = state.device, destroy = state.destroyBuffer, buffer = std::exchange(kept.buffer, VK_NULL_HANDLE)](void*) { destroy(device, buffer, nullptr); });
kept.range.reset();
kept.ranges.clear();
kept.base = first;
kept.bytes = last - first;
kept.address = 0;
kept.serial = 0;
node.key() = first;
state.trimmed.insert(std::move(node));
auto* recorder = Recorder::Active();
if (recorder != nullptr && !recorder->Idle()) recorder->Keep(std::move(holder));
}
VkResult createImportBuffer(const Context& context, VkExternalMemoryHandleTypeFlagBits handleType, VkDeviceSize size, VkBuffer* buffer) {
const VkExternalMemoryBufferCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO, nullptr, static_cast<VkExternalMemoryHandleTypeFlags>(handleType)};
VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, &external};
@@ -1407,9 +1350,6 @@ bool sameRun(const Entry& entry, const ImportRun& run) {
bool wouldRetireImport(const HostImports& state, const ImportRun& run) {
if (run.ranges.empty()) return false;
for (const auto& [base, entry] : state.trimmed) {
if (base < run.base + run.bytes && run.base < base + entry.bytes) return true;
}
const auto next = state.imports.lower_bound(run.base);
if (next != state.imports.end()) {
if (next->first == run.base && sameRun(next->second, run)) return false;
@@ -1686,7 +1626,6 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import
if (sameRun(failed->second, run)) return nullptr;
failures.erase(failed);
}
dropTrimmed(state, base, base + bytes);
const auto budget = effectiveBudget(context, state);
const auto containing = std::find_if(run.ranges.begin(), run.ranges.end(), [address](const auto& range) { return address >= range->address && address < range->address + range->bytes; });
if (const auto remembered = state.evictingRefusals.find({base, bytes}); evict && remembered != state.evictingRefusals.end()) {
@@ -1920,46 +1859,6 @@ const GuestAllocations::Range* containingRange(const GuestAllocations::Lease& le
return range->readable && begin >= range->address && end <= range->address + range->bytes ? range.get() : nullptr;
}
bool reattachTrimmed(const Context& context, HostImports& state, HostImport& entry, const GuestAllocations::Lease& lease) {
const auto first = entry.base;
const auto last = entry.base + entry.bytes;
std::shared_ptr<const GuestAllocations::Range> kept;
for (auto it = std::lower_bound(lease.begin(), lease.end(), first, [](const auto& range, std::uint64_t value) { return range->address < value; }); it != lease.end() && (*it)->address < last; ++it) {
if ((*it)->backingSerial == entry.backingSerial && (*it)->address + (*it)->bytes <= last && (kept == nullptr || (*it)->bytes > kept->bytes)) kept = *it;
}
if (kept == nullptr) return false;
const auto next = state.imports.lower_bound(kept->address);
if (next != state.imports.end() && next->first < kept->address + kept->bytes) return false;
if (next != state.imports.begin() && std::prev(next)->second.base + std::prev(next)->second.bytes > kept->address) return false;
HostImport candidate{kept->address, kept->bytes, VK_NULL_HANDLE, VK_NULL_HANDLE, 0};
candidate.range = kept;
candidate.ranges = {kept};
if (!sameRange(candidate, lease)) return false;
const auto offset = kept->address - entry.memoryBase;
VkBuffer buffer = VK_NULL_HANDLE;
if (createImportBuffer(context, VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT, kept->bytes, &buffer) != VK_SUCCESS) return false;
const auto refuse = [&] {
context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer")(context.device, buffer, nullptr);
return false;
};
VkMemoryRequirements requirements{};
context.Function<PFN_vkGetBufferMemoryRequirements>("vkGetBufferMemoryRequirements")(context.device, buffer, &requirements);
if (((requirements.memoryTypeBits >> entry.memoryType) & 1u) == 0 || requirements.alignment == 0 || offset % requirements.alignment != 0 || requirements.size > entry.pinnedBytes - offset) return refuse();
if (context.Function<PFN_vkBindBufferMemory>("vkBindBufferMemory")(context.device, buffer, entry.memory, offset) != VK_SUCCESS) return refuse();
const VkBufferDeviceAddressInfo addressInfo{VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO, nullptr, buffer};
const auto address = context.Function<PFN_vkGetBufferDeviceAddressKHR>("vkGetBufferDeviceAddressKHR")(context.device, &addressInfo);
if (address == 0) return refuse();
entry.base = kept->address;
entry.bytes = kept->bytes;
entry.buffer = buffer;
entry.address = address;
entry.range = kept;
entry.ranges = {kept};
entry.serial = 0;
entry.lastUse = ++state.useCounter;
return true;
}
// Drops imports whose registered range changed or disappeared: their pages may no longer back the
// guest addresses. Walks the imports only when the registry changed since the last walk.
void refreshImports(const Context& context, HostImports& state, const GuestAllocations::Lease& lease) {
@@ -1967,17 +1866,15 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc
#ifdef _WIN32
drainSettles(state);
#endif
for (auto* held : {&state.imports, &state.trimmed}) {
while (!held->empty()) {
auto node = held->extract(held->begin());
if (state.device != VK_NULL_HANDLE && state.destroyBuffer != nullptr && state.freeMemory != nullptr) {
state.parked[state.device].push_back(std::make_shared<RetiredImport>(state.device, state.destroyBuffer, state.freeMemory, std::move(node)));
continue;
}
#ifdef _WIN32
GuestArena::GuestArenaUnmapAlias_nid_postfix(node.mapped().alias);
#endif
while (!state.imports.empty()) {
auto node = state.imports.extract(state.imports.begin());
if (state.device != VK_NULL_HANDLE && state.destroyBuffer != nullptr && state.freeMemory != nullptr) {
state.parked[state.device].push_back(std::make_shared<RetiredImport>(state.device, state.destroyBuffer, state.freeMemory, std::move(node)));
continue;
}
#ifdef _WIN32
GuestArena::GuestArenaUnmapAlias_nid_postfix(node.mapped().alias);
#endif
}
state.liveBytes = 0;
state.failed.clear();
@@ -2008,16 +1905,6 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc
};
std::erase_if(state.sparseRunEnds, [&](const auto& entry) { return !starts(entry.first); });
std::erase_if(state.sparseRunJoins, [&](const auto& entry) { return !starts(entry.first); });
while (!state.trimmed.empty()) {
auto node = state.trimmed.extract(state.trimmed.begin());
if (!reattachTrimmed(context, state, node.mapped(), lease)) {
releaseTrimmed(state, std::move(node));
continue;
}
node.key() = node.mapped().base;
++state.epoch;
state.imports.insert(std::move(node));
}
const auto budget = effectiveBudget(context, state);
for (auto it = state.imports.begin(); it != state.imports.end();) {
if (sameRange(it->second, lease, budget)) {
@@ -2857,6 +2744,7 @@ bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) {
bool synced = false;
bool drained = false;
bool retiredImport = false;
bool droppedAddressSpace = false;
// A range pinned only by the cached address space is released by dropping the cache's
// reference: no GPU wait, no device lock (the mutating thread may be a driver thread holding
// it). The registry rescans; a range still pinned by a build or a batch holding the space comes
@@ -2865,12 +2753,24 @@ bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) {
Spaces().droppedByWaiter.store(true, std::memory_order_relaxed);
Spaces().waiterDrops.fetch_add(1, std::memory_order_relaxed);
dropped.reset();
auto& state = Leases();
std::lock_guard lock(state.mutex);
++state.stats.contentionWaits;
++state.stats.cacheDrops;
state.stats.contentionMs += std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
return true;
droppedAddressSpace = true;
auto& imports = Imports();
const auto end = static_cast<std::uint64_t>(address) + bytes;
bool overlapsImport = false;
{
std::lock_guard importsLock(imports.mutex);
const auto next = imports.imports.lower_bound(address);
const auto previous = next == imports.imports.begin() ? imports.imports.end() : std::prev(next);
overlapsImport = (next != imports.imports.end() && next->first < end) || (previous != imports.imports.end() && previous->first + previous->second.bytes > address);
}
if (!overlapsImport) {
auto& state = Leases();
std::lock_guard lock(state.mutex);
++state.stats.contentionWaits;
++state.stats.cacheDrops;
state.stats.contentionMs += std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
return true;
}
}
if (Recorder::InCompletion()) throw std::runtime_error("guest allocation mutation cannot wait for an active GPU completion");
if (GuestMemory::GpuMutex().HeldByThisThread()) {
@@ -2895,7 +2795,7 @@ bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) {
continue;
}
const auto next = std::next(it);
retireOrTrim(imports.device, imports, it, lease, address, end);
retireImport(imports.device, imports, it, lease, false);
it = next;
retiredImport = true;
}
@@ -2961,7 +2861,7 @@ bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) {
continue;
}
const auto next = std::next(it);
retireOrTrim(imports.device, imports, it, lease, address, end);
retireImport(imports.device, imports, it, lease, false);
it = next;
retiredImport = true;
}
@@ -2980,6 +2880,7 @@ bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) {
auto& state = Leases();
std::lock_guard lock(state.mutex);
++state.stats.contentionWaits;
if (droppedAddressSpace) ++state.stats.cacheDrops;
if (synced) ++state.stats.contentionSyncs;
if (drained) ++state.stats.contentionDrains;
state.stats.contentionMs += std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
@@ -3141,9 +3042,7 @@ void ClearHostImports(VkDevice device) {
GuestArena::GuestArenaUnmapAlias_nid_postfix(entry.alias);
#endif
}
for (const auto& [address, entry] : state.trimmed) state.freeMemory(state.device, entry.memory, nullptr);
state.imports.clear();
state.trimmed.clear();
state.liveBytes = 0;
state.failed.clear();
state.failedJoins.clear();
@@ -3436,6 +3335,13 @@ std::uint64_t HostImportLiveBytes() {
return state.liveBytes;
}
std::uint64_t HostImportBudgetRemaining(const Context& context) {
auto& state = Imports();
std::lock_guard lock(state.mutex);
const auto budget = effectiveBudget(context, state);
return budget > state.liveBytes ? budget - state.liveBytes : 0;
}
const char* ImportRequesterName(ImportRequester requester) {
switch (requester) {
case ImportRequester::Map: return "map";
@@ -4773,7 +4679,7 @@ void GuestBufferMemory::UploadFinish(bool addressable) {
void GuestBufferMemory::copyRegion(Region& region, bool addressable) {
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
const auto bytes = region.end - region.begin;
const auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | (addressable ? VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT : 0u);
const auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | (addressable ? VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT : 0u);
std::uint64_t readBytes = 0;
bool inImport = false;
if (profile && context.hostImportAlignment != 0) {
@@ -5267,7 +5173,7 @@ void GuestBufferMemory::MarkDirectWrites() const {
const auto* found = owner(begin, end);
if (found == nullptr) continue;
const auto& region = *found;
if (region.direct != nullptr || (region.gpuCopy && region.copiedBack)) GuestMemory::MarkWritten(begin, static_cast<std::size_t>(end - begin));
if (region.direct != nullptr || (region.gpuCopy && region.copiedBack)) GuestMemory::MarkPossiblyWritten(begin, static_cast<std::size_t>(end - begin));
}
}
@@ -5355,7 +5261,7 @@ void GuestBufferMemory::WriteBack() {
if (region.direct == nullptr && !(region.gpuCopy && (region.copiedBack || region.deviceLocal)) && !(region.mirror != nullptr && !region.mirror->writable)) Recorder::NoteWrittenBack(begin, static_cast<std::size_t>(end - begin));
if (region.direct != nullptr) {
// The GPU wrote imported guest memory directly; page write watching did not see it.
GuestMemory::MarkWritten(begin, static_cast<std::size_t>(end - begin));
GuestMemory::MarkPossiblyWritten(begin, static_cast<std::size_t>(end - begin));
continue;
}
if (region.mirror != nullptr) {
@@ -5385,7 +5291,7 @@ void GuestBufferMemory::WriteBack() {
if (region.copiedBack) {
// The GPU copied the written sub-range back into the import; page write watching
// did not see it.
GuestMemory::MarkWritten(begin, static_cast<std::size_t>(end - begin));
GuestMemory::MarkPossiblyWritten(begin, static_cast<std::size_t>(end - begin));
continue;
}
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
@@ -122,7 +122,7 @@ void ValidateProvokingVertex(const Context& context, const State& state, std::sp
Require(context.provokingVertexLast, "last provoking vertex requires VK_EXT_provoking_vertex with provokingVertexLast enabled");
const bool meshPrimitives = state.stages.path == ShaderPath::Geometry && state.stages.mesh;
Require(!state.stages.tessellation && (meshPrimitives || (state.stages.path == ShaderPath::Vertex && !state.stages.mesh)), "last provoking vertex is unsupported for generated primitive pipelines");
Require(!meshPrimitives || (state.topology != VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP && state.topology != VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN), "last provoking vertex is unsupported for triangle strips or fans assembled for a geometry program");
Require(!meshPrimitives || state.topology != VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, "last provoking vertex is unsupported for triangle fans assembled for a geometry program");
Require(state.rectList || state.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, "last provoking vertex is unsupported for this primitive topology");
for (const auto& shader : shaders) {
if (shader.stage != ShaderRecompiler::ShaderStage::Fragment) continue;
@@ -461,7 +461,12 @@ private:
bool outermost;
};
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);
void refreshForView(StorageTexture& source, const GuestTextureResource& resource) {
if (resource.dimension == TextureDimension::k2D && source.ImageLayers() > 1) source.RefreshLayers(resource.baseArray, 1);
else source.Refresh();
}
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::optional<std::pair<std::uint32_t, std::uint32_t>> layers = std::nullopt);
std::uint64_t heldBytes(const StorageTexture& image) {
return image.BudgetBytes();
@@ -787,9 +792,9 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
// so a write the game made since the last walk is invisible until a collect stamps its page. The
// walk is memoized per packet, so a texture viewed from a storage image repeats it for free.
auto generation = GuestMemory::CollectWrites(address, bytes);
if (source != nullptr && !GuestMemory::UnchangedSince(address, bytes, source->Generation())) {
if (source != nullptr && !GuestMemory::UnchangedSince(address, bytes, source->GenerationFor(address, bytes))) {
// The CPU wrote the memory while results were pending: Refresh merges them the usual way.
source->Refresh();
refreshForView(*source, resource);
}
// Results stored to guest memory just now are stamped newer than `generation`; a snapshot read
// after them is current at the generation of a second (memoized) collect. The store marks the
@@ -857,7 +862,7 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
// written meanwhile is taken in by refreshing the image. A fast clear the image cannot
// see (keys, and no pending results to prefer) ends the view: a snapshot holds the clear.
if (!stagedFlushed && (source == nullptr || source == it->source) && (source != nullptr || *keys == DccKeys::Uncompressed) && StorageImageServesKeys(*it->source, resource.dccAddress)) {
if (!GuestMemory::UnchangedSince(address, bytes, it->source->Generation())) it->source->Refresh();
if (!GuestMemory::UnchangedSince(address, bytes, it->source->GenerationFor(address, bytes))) refreshForView(*it->source, resource);
if (it->texture->IsDepthCopy()) it->texture->RefreshDepthCopy();
it->keys = *keys;
touchTexture(cache, it);
@@ -1108,7 +1113,7 @@ static WrittenStorage WrittenStorageBytes(const GuestTextureResource& viewed, st
}
// `guestBytes` is the surface size when the caller described the surface already (0: described here).
std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& requested, std::uint32_t mip, std::uint64_t guestBytes, bool written) {
std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& requested, std::uint32_t mip, std::uint64_t guestBytes, bool written, std::optional<std::pair<std::uint32_t, std::uint32_t>> layers) {
const auto viewed = ArrayLayerWindow(requested, context.limits.maxImageArrayLayers);
if (viewed.baseAddress != requested.baseAddress) guestBytes = 0;
Require(viewed.dimension != TextureDimension::k2DMsaa, "storage image access to a multisampled texture is not implemented");
@@ -1123,7 +1128,7 @@ std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std
reportGuestAllocation(viewed, viewedBytes);
}
}
if (DepthPlanesStale() || written) static_cast<void>(FlushDepthPlanes(viewed.baseAddress, static_cast<std::size_t>(viewedBytes)));
if (DepthPlanesStale() || written) static_cast<void>(FlushDepthPlanes(viewed.baseAddress, static_cast<std::size_t>(viewedBytes), written ? DepthPlaneScope::Whole : DepthPlaneScope::Overlapped));
if (written && DepthSurfaceOverlaps(context.device, viewed.baseAddress, viewedBytes)) {
const auto storage = WrittenStorageBytes(viewed, mip);
RetireCoveredDepthSurfaces(context.device, storage.bytes, storage.extents);
@@ -1154,7 +1159,8 @@ std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std
it = cache.entries.end();
}
if (it != cache.entries.end()) {
it->texture->Refresh();
if (layers) it->texture->RefreshLayers(layers->first, layers->second);
else it->texture->Refresh();
cache.entries.splice(cache.entries.begin(), cache.entries, it);
texture = it->texture;
counters.storageHits.fetch_add(1, std::memory_order_relaxed);
@@ -1436,6 +1442,10 @@ std::shared_ptr<StorageTexture> CachedStorageSurface(const Context& context, con
return cachedStorageTexture(context, {}, resource, 0);
}
std::shared_ptr<StorageTexture> CachedStorageLayers(const Context& context, const GuestTextureResource& resource, std::uint32_t firstLayer, std::uint32_t layerCount) {
return cachedStorageTexture(context, {}, resource, 0, 0, false, std::pair{firstLayer, layerCount});
}
bool StorageImageServesKeys(const StorageTexture& image, std::uint64_t dccAddress) {
GuestTextureResource resource{};
resource.dccAddress = dccAddress;
@@ -3005,7 +3015,7 @@ bool ShaderResources::fastRevalidate(std::uint64_t serialBefore, std::span<const
}
if (source != nullptr) {
if (!source->Cached()) return fail(FastFail::Evicted);
queries.push_back({address, bytes, source->Generation()});
queries.push_back({address, bytes, source->GenerationFor(address, bytes)});
images.push_back(source);
} else {
queries.push_back({address, bytes, surface.generation});
@@ -3033,7 +3043,7 @@ bool ShaderResources::fastRevalidate(std::uint64_t serialBefore, std::span<const
}
if (!unchanged) query(address, address + bytes, image, nullptr, {i, true, false});
if (!image->Cached()) return fail(FastFail::Evicted);
queries.push_back({address, bytes, image->Generation()});
queries.push_back({address, bytes, image->GenerationFor(address, bytes)});
images.push_back(image);
}
if (!pending.empty()) {
@@ -3101,7 +3111,7 @@ bool ShaderResources::fastRevalidateEach() {
if (surface.source != nullptr) {
// The view follows the image: it needs the image current with guest memory, as the
// lookup's Refresh would make it.
if (!StorageImageCached(context, surface.source) || !GuestMemory::UnchangedSince(address, bytes, surface.source->Generation())) return false;
if (!StorageImageCached(context, surface.source) || !GuestMemory::UnchangedSince(address, bytes, surface.source->GenerationFor(address, bytes))) return false;
} else if (!GuestMemory::UnchangedSince(address, bytes, surface.generation)) {
return false;
}
@@ -3135,7 +3145,7 @@ bool ShaderResources::fastRevalidateEach() {
if (GuestMemory::CollectWrites(address, bytes) == 0) return false;
if (PendingStorageOverlaps(address, bytes, image)) return false;
if (!StorageImageCached(context, image)) return false;
if (!GuestMemory::UnchangedSince(address, bytes, image->Generation())) return false;
if (!GuestMemory::UnchangedSince(address, bytes, image->GenerationFor(address, bytes))) return false;
}
// Every check passed: snapshot textures are current at the collects issued before them. Storage
// images keep their own generation: nothing was stamped above it, so the blocks a later write
@@ -3169,9 +3179,9 @@ ShaderResources::OwnRefreshFallback ShaderResources::refreshOwnObjects(std::span
if (found != nullptr && !Texture::CanCopyFrom(*found, surface.resource)) found.reset();
if (found != nullptr) {
if (found != source) return OwnRefreshFallback::ForeignView;
if (!GuestMemory::UnchangedSince(address, bytes, source->Generation())) {
if (!GuestMemory::UnchangedSince(address, bytes, source->GenerationFor(address, bytes))) {
countOwnRefresh(false);
source->Refresh();
refreshForView(*source, surface.resource);
}
return OwnRefreshFallback::Count;
}
@@ -4579,7 +4589,7 @@ std::shared_ptr<Texture> ShaderResources::fastTexture(const ImageRecord& record)
// keys and stays valid while they are unchanged); a change is the full lookup's to judge.
if (keys != record.entryKeys) return nullptr;
if (record.source != nullptr) {
if (!StorageImageCached(context, record.source.get()) || !GuestMemory::UnchangedSince(address, bytes, record.source->Generation())) return nullptr;
if (!StorageImageCached(context, record.source.get()) || !GuestMemory::UnchangedSince(address, bytes, record.source->GenerationFor(address, bytes))) return nullptr;
if (record.resource.dccAddress != 0 && IsDccClear(record.source->FilledKeys())) return nullptr;
// Under fast-clear keys the view holds only while its image's results are still pending over
// the surface; flushed, the clear the image cannot see makes the lookup take a snapshot.
@@ -357,8 +357,9 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValEx
const bool depthClear = (read(cx, 0x000) & 1u) != 0;
const bool stencilClear = (read(cx, 0x000) & 2u) != 0;
const auto view = read(cx, 0x002);
zero(cx, 0x002, 0x3c000000u, "depth mips (DB_DEPTH_VIEW MIP_LEVEL)");
zero(cx, 0x010, 0x000f1000u, "partially resident or mipmapped depth (DB_Z_INFO)");
zero(cx, 0x010, 0x00001000u, "partially resident depth (DB_Z_INFO)");
const auto viewMip = (view >> 26u) & 0xfu;
const auto maxMip = (read(cx, 0x010) >> 16u) & 0xfu;
zero(cx, 0x011, 0x00001000u, "partially resident stencil (DB_STENCIL_INFO)");
const auto zFormat = read(cx, 0x010) & 3u;
const bool stencil = (read(cx, 0x011) & 1u) != 0;
@@ -396,7 +397,27 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValEx
}
const auto size = read(cx, 0x007);
depth.extent = {(size & 0x3fffu) + 1u, ((size >> 16u) & 0x3fffu) + 1u};
if (const auto slice = view & 0x1fffu; slice != 0) {
if (maxMip == 0 && viewMip != 0) Require(false, "depth view mip " + std::to_string(viewMip) + " on a surface without mips");
if (maxMip != 0) {
char message[320];
std::snprintf(message, sizeof(message), "depth view mip %u of %u at %ux%u (DB_Z_INFO 0x%08x, DB_DEPTH_VIEW 0x%08x)", viewMip, maxMip + 1u, depth.extent.width, depth.extent.height, read(cx, 0x010), view);
const std::string where(message);
Require(viewMip <= maxMip, "the " + where + " is beyond the surface's mips");
Require(depth.samples == 1u, "multisampled " + where + " is unsupported");
Require(depth.depthSwizzle == XorSwizzleMode(TextureTileMode::kZ64KBX), "the " + where + " uses a swizzle other than 64 KiB Z_X");
const auto depthMips = ComputeElementMipLayout(TextureTileMode::kZ64KBX, zFormat == 1 ? 2u : 4u, depth.extent.width, depth.extent.height, maxMip + 1u);
const auto stencilMips = ComputeElementMipLayout(TextureTileMode::kZ64KBX, 1u, depth.extent.width, depth.extent.height, maxMip + 1u);
Require(zFormat == 0 || !depthMips.at(viewMip).tail, "rendering the " + where + " into the packed mip tail is unsupported");
Require(!stencil || !stencilMips.at(viewMip).tail, "rendering the stencil of the " + where + " into the packed mip tail is unsupported");
Require((read(cx, 0x010) & (1u << 29u)) == 0, "HTILE for the " + where + " is unsupported");
if (const auto slice = view & 0x1fffu; slice != 0) {
if (depth.address != 0) depth.address += static_cast<std::uint64_t>(slice) * ComputeSurfaceSize(depthMips, 1u);
if (depth.stencilAddress != 0) depth.stencilAddress += static_cast<std::uint64_t>(slice) * ComputeSurfaceSize(stencilMips, 1u);
}
if (depth.address != 0) depth.address += depthMips.at(viewMip).tiledOffset;
if (depth.stencilAddress != 0) depth.stencilAddress += stencilMips.at(viewMip).tiledOffset;
depth.extent = {depthMips.at(viewMip).width, depthMips.at(viewMip).height};
} else if (const auto slice = view & 0x1fffu; slice != 0) {
if (depth.address != 0) depth.address += static_cast<std::uint64_t>(slice) * DepthSliceBytes(depth.extent, zFormat == 1 ? 2u : 4u);
if (depth.stencilAddress != 0) depth.stencilAddress += static_cast<std::uint64_t>(slice) * DepthSliceBytes(depth.extent, 1u);
}
@@ -2036,11 +2036,31 @@ LookupOutcomes& ThreadLookupOutcomes() {
}
bool StorageTexture::Refresh() {
return refresh(0, trackedLayers);
}
bool StorageTexture::RefreshLayers(std::uint32_t first, std::uint32_t count) {
if (count == 0 || geometry.thick || geometry.imageLayers <= 1 || first >= geometry.imageLayers || count > geometry.imageLayers - first || geometry.layerBytes == 0 || trackedLayerBytes == 0 || descriptor.dccAddress != 0) return Refresh();
const auto begin = geometry.GuestLayerOffset(first);
const auto end = geometry.GuestLayerOffset(first + count - 1) + geometry.layerBytes;
const auto firstLayer = static_cast<std::uint32_t>(begin / trackedLayerBytes);
const auto lastLayer = static_cast<std::uint32_t>(std::min<std::uint64_t>((end + trackedLayerBytes - 1) / trackedLayerBytes, trackedLayers));
if (firstLayer >= lastLayer || (firstLayer == 0 && lastLayer == trackedLayers)) return Refresh();
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
if (layerPending[layer] && (layer < firstLayer || layer >= lastLayer)) return Refresh();
}
if (pendingAlias() != nullptr) return Refresh();
return refresh(firstLayer, lastLayer);
}
bool StorageTexture::refresh(std::uint32_t firstLayer, std::uint32_t lastLayer) {
const bool profile = LookupOutcomes::Profiled();
auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
const bool scoped = firstLayer != 0 || lastLayer != trackedLayers;
CompletePendingCpuWrites(context, descriptor.baseAddress, static_cast<std::size_t>(guestBytes));
if (descriptor.dccAddress != 0) CompletePendingCpuWrites(context, descriptor.dccAddress, DccKeyBytes(guestBytes));
if (refreshProved()) {
layerBehind.clear();
NoteProved();
refreshesProved.fetch_add(1, std::memory_order_relaxed);
if (profile) LookupOutcomes::Add(LookupOutcomes::RefreshProved, start);
@@ -2062,7 +2082,7 @@ bool StorageTexture::Refresh() {
// generation, no results of this image's own there) or dead to its own write-back (tracked and
// stamped); an untracked unit is stored whole by that write-back, and a unit pending in both
// images takes the old order (store, then this image re-uploads it).
auto alias = pendingAlias();
auto alias = scoped ? std::shared_ptr<StorageTexture>{} : pendingAlias();
if (alias != nullptr && !(ProvedClearKeys(descriptor, guestBytes, keyProof) == DccKeys::Uncompressed && HostImportFor(context, descriptor.baseAddress, static_cast<std::size_t>(guestBytes)) != nullptr)) alias = nullptr;
if (alias != nullptr) {
std::vector<std::uint8_t> aliasStamped(trackedLayers);
@@ -2075,22 +2095,35 @@ bool StorageTexture::Refresh() {
}
}
}
const auto scopeBegin = scoped ? layerBegin(firstLayer) : descriptor.baseAddress;
const auto scopeBytes = scoped ? static_cast<std::size_t>(layerBegin(lastLayer - 1) + layerBytes(lastLayer - 1) - scopeBegin) : static_cast<std::size_t>(guestBytes);
// No publish: the upload below reads the stored units from the unit shadow itself.
handedOver.clear();
handedUnits.clear();
const auto handOverKeys = alias == nullptr ? clearHandOver() : std::nullopt;
const bool flushed = !handOverKeys.has_value() && FlushPending(descriptor.baseAddress, static_cast<std::size_t>(guestBytes), alias.get(), "storage refresh", PublishScope::None);
const auto handOverKeys = alias == nullptr && !scoped ? clearHandOver() : std::nullopt;
const bool flushed = !handOverKeys.has_value() && FlushPending(scopeBegin, scopeBytes, alias.get(), "storage refresh", PublishScope::None);
if (profile && flushed) start = LookupOutcomes::Add(LookupOutcomes::PendingFlush, start);
// `original` holds the guest bytes the image was last uploaded from or written back as; while the
// guest memory and the DCC keys still match, the image content is current. Pages nobody wrote
// since `generation` need no comparison.
const auto current = GuestMemory::CollectWrites(descriptor.baseAddress, static_cast<std::size_t>(guestBytes));
const auto scopedCurrent = current != 0 || !scoped ? current : GuestMemory::CollectWrites(scopeBegin, scopeBytes);
std::vector<std::uint64_t> collected;
if (scopedCurrent == 0) {
collected.resize(trackedLayers);
for (auto layer = firstLayer; layer < lastLayer; ++layer) collected[layer] = collectedLayerGeneration(layer, 0);
}
const auto currentFor = [&](std::uint32_t layer) {
if (scoped && (layer < firstLayer || layer >= lastLayer)) return current;
return scopedCurrent != 0 ? scopedCurrent : collected[layer];
};
// Per tracked layer: changed since its generation (a keys change makes every layer stale).
std::vector<bool> changed(trackedLayers, false);
// Per 64 KiB block of the surface (blockGenerations): stamped since its layer's generation, and
// by a CPU store; scanned once when anything changed.
std::vector<std::uint64_t> generations;
std::vector<std::uint8_t> stampedBlocks, cpuBlocks;
std::vector<bool> outside;
// Units taken from the alias's image instead of guest memory.
std::vector<bool> borrowed;
bool anyBorrowed = false;
@@ -2106,6 +2139,7 @@ bool StorageTexture::Refresh() {
const auto equalsOriginal = [&] {
// Named for the [hooksync] attribution: the compare goes through the flush hook.
const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::TextureCompare);
if (scoped) return GuestMemory::EqualsCommitted(scopeBegin, std::span<const std::byte>(original).subspan(static_cast<std::size_t>(scopeBegin - descriptor.baseAddress), scopeBytes));
return GuestMemory::EqualsCommitted(descriptor.baseAddress, original);
};
const auto scanBlocks = [&] {
@@ -2114,13 +2148,19 @@ bool StorageTexture::Refresh() {
cpuBlocks.assign(generations.size(), 0);
tracked = GuestMemory::ChangedBlocks(descriptor.baseAddress, static_cast<std::size_t>(guestBytes), generations, stampedBlocks, cpuBlocks);
if (!tracked) awaitStoredSave(nullptr);
compared = !tracked && compareUntracked(descriptor.baseAddress, static_cast<std::size_t>(guestBytes), stampedBlocks, true);
const auto firstBlock = static_cast<std::size_t>(scopeBegin / 65536 - descriptor.baseAddress / 65536);
const auto blocks = static_cast<std::size_t>((scopeBegin + scopeBytes - 1) / 65536 - scopeBegin / 65536 + 1);
compared = !tracked && (scoped ? compareUntracked(scopeBegin, scopeBytes, std::span(stampedBlocks).subspan(firstBlock, blocks), true) : compareUntracked(descriptor.baseAddress, static_cast<std::size_t>(guestBytes), stampedBlocks, true));
if (compared) {
for (const auto& [from, unitGeneration] : keptAtUnwatch) {
const auto unit = static_cast<std::uint32_t>((from - descriptor.baseAddress) / trackedLayerBytes);
if (keptAtUnwatchHolds(from, unit)) stampedBlocks[static_cast<std::size_t>(from / 65536 - descriptor.baseAddress / 65536)] = GuestMemory::BlockWritten;
}
cpuBlocks = stampedBlocks;
if (scoped) {
std::fill(stampedBlocks.begin(), stampedBlocks.begin() + static_cast<std::ptrdiff_t>(firstBlock), GuestMemory::BlockWritten);
std::fill(stampedBlocks.begin() + static_cast<std::ptrdiff_t>(firstBlock + blocks), stampedBlocks.end(), GuestMemory::BlockWritten);
}
}
};
const auto keysStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
@@ -2139,7 +2179,7 @@ bool StorageTexture::Refresh() {
scanBlocks();
for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) {
if (stampedBlocks[unit] == 0) {
layerGeneration[unit] = current;
layerGeneration[unit] = currentFor(unit);
continue;
}
changed[unit] = true;
@@ -2165,6 +2205,15 @@ bool StorageTexture::Refresh() {
}
unchanged = std::none_of(changed.begin(), changed.end(), [](bool value) { return value; });
}
if (scoped && !keysChanged) {
outside.assign(trackedLayers, false);
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
if ((layer >= firstLayer && layer < lastLayer) || (!changed[layer] && !compared)) continue;
outside[layer] = true;
changed[layer] = false;
}
unchanged = std::none_of(changed.begin(), changed.end(), [](bool value) { return value; });
}
for (std::size_t k = 0; k < cpuBlocks.size(); ++k) {
if (cpuBlocks[k] != 0 && (compared || (tracked && generations[k] != 0))) cpuWrote = true;
}
@@ -2175,12 +2224,11 @@ bool StorageTexture::Refresh() {
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
if (changed[layer] && layerPending[layer]) pendingChanged = true;
}
if (!unchanged && !keysChanged && !pendingChanged && originalHolds(descriptor.baseAddress, static_cast<std::size_t>(guestBytes)) && equalsOriginal()) {
if (!unchanged && !keysChanged && !pendingChanged && (scoped ? originalHolds(scopeBegin, scopeBytes) : originalHolds(descriptor.baseAddress, static_cast<std::size_t>(guestBytes))) && equalsOriginal()) {
unchanged = true;
stamped = false;
if (!scoped) outside.clear();
}
// Only the direct path uploads the selected layers alone (see upload); the others replace
// the whole image, so every pending layer's results are stored first.
direct = keys == DccKeys::Uncompressed && HostImportFor(context, descriptor.baseAddress, static_cast<std::size_t>(guestBytes)) != nullptr;
if (alias != nullptr && direct) {
// The alias's pending units that nothing stamped since its generation (a CPU store or a
@@ -2205,11 +2253,16 @@ bool StorageTexture::Refresh() {
}
}
}
const bool outsideChanged = std::any_of(outside.begin(), outside.end(), [](bool value) { return value; });
if (scoped) noteLayersBehind(firstLayer, lastLayer, outside);
else layerBehind.clear();
if (unchanged) {
++Profile().storageReused;
layerGeneration.assign(trackedLayers, current);
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
if (!outsideChanged || !outside[layer]) layerGeneration[layer] = currentFor(layer);
}
refreshGeneration();
takeRefreshProof(alias != nullptr);
if (layerBehind.empty()) takeRefreshProof(alias != nullptr);
if (profile) LookupOutcomes::Add(stamped ? LookupOutcomes::RefreshUnchanged : LookupOutcomes::RefreshCompared, start);
return true;
}
@@ -2259,6 +2312,7 @@ bool StorageTexture::Refresh() {
}
std::fprintf(stderr, "[dcc-keys] refresh 0x%llx+0x%llx keys %s -> %s (dirty %d, %zu pending units, %zu cpu-stamped units%s)\n", static_cast<unsigned long long>(descriptor.baseAddress), static_cast<unsigned long long>(guestBytes), DccKeysName(uploadedKeys), DccKeysName(keys), dirty ? 1 : 0, pendingUnits, stampedUnits, keyFlip ? ", flip keeps unstamped results" : "");
}
const bool selectedCpu = !direct && keys == DccKeys::Uncompressed && !cpuUploadLayers(changed).empty();
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
if (!layerPending[layer]) continue;
if (keyFlip && layerGeneration[layer] != 0 && stampedBlocks[layer] != GuestMemory::BlockWritten) {
@@ -2277,7 +2331,7 @@ bool StorageTexture::Refresh() {
dropped = true;
continue;
}
} else if (direct) {
} else if (direct || selectedCpu) {
continue;
}
stored[layer] = true;
@@ -2299,7 +2353,7 @@ bool StorageTexture::Refresh() {
uploadReason = "alias";
borrowUnits(*alias, borrowed);
for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) {
if (borrowed[unit]) layerGeneration[unit] = current;
if (borrowed[unit]) layerGeneration[unit] = currentFor(unit);
}
refreshGeneration();
}
@@ -2412,7 +2466,7 @@ void traceKeyStore(const char* path, const GuestTextureResource& descriptor, std
void StorageTexture::captureGuestBytes(const std::vector<bool>* layers) {
original.resize(static_cast<std::size_t>(guestBytes));
comparedGuestBytes = {};
comparedRanges = {};
std::vector<bool> selected(trackedLayers, true);
if (layers != nullptr) selected = *layers;
for (const auto& [begin, end] : unitRuns(selected)) GuestMemory::FlushGpuWrites(descriptor.baseAddress + begin, static_cast<std::size_t>(end - begin));
@@ -2449,11 +2503,10 @@ bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes,
Require(changed.size() == (end - 1) / blockBytes - first + 1, "texture comparison block count differs");
const auto* recorder = Recorder::Active();
const auto epoch = GuestMemory::CollectEpoch();
const bool whole = address == descriptor.baseAddress && bytes == guestBytes;
const auto hint = memoize && epoch != 0 && whole ? GuestMemory::UntrackedHint(address, bytes) : 0;
const auto hint = memoize && epoch != 0 ? GuestMemory::UntrackedHint(address, bytes) : 0;
const std::array<std::uint64_t, 5> stamp{hint != 0, hint != 0 ? hint : epoch, GuestMemory::NewestStamp(address, bytes), GuestMemory::ForgetSerial(), recorder != nullptr ? recorder->NewestWriteNote(address, bytes) : 0};
const bool cacheable = memoize && epoch != 0 && (stamp[3] & 1u) == 0 && whole;
if (cacheable && comparedGuestBytes == stamp) {
const bool cacheable = memoize && epoch != 0 && (stamp[3] & 1u) == 0;
if (cacheable && std::any_of(comparedRanges.begin(), comparedRanges.end(), [&](const ComparedRange& entry) { return entry.address == address && entry.bytes == bytes && entry.stamp == stamp; })) {
std::fill(changed.begin(), changed.end(), GuestMemory::BlockUnchanged);
return true;
}
@@ -2461,10 +2514,14 @@ bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes,
const auto saved = std::span(original).subspan(static_cast<std::size_t>(address - descriptor.baseAddress), bytes);
if (GuestMemory::EqualsCommittedUnsynced(address, saved)) {
std::fill(changed.begin(), changed.end(), GuestMemory::BlockUnchanged);
if (cacheable) comparedGuestBytes = stamp;
if (cacheable) {
auto found = std::find_if(comparedRanges.begin(), comparedRanges.end(), [&](const ComparedRange& entry) { return entry.address == address && entry.bytes == bytes; });
if (found == comparedRanges.end()) found = comparedRanges.begin() + nextComparedRange++ % comparedRanges.size();
*found = {address, bytes, stamp};
}
return true;
}
comparedGuestBytes = {};
comparedRanges = {};
for (std::size_t index = 0; index < changed.size(); ++index) {
const auto begin = std::max(address, (first + index) * blockBytes);
const auto stop = std::min(end, (first + index + 1) * blockBytes);
@@ -2476,6 +2533,25 @@ bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes,
constexpr std::uint64_t GenerationBaselineBytes = 1ull << 20;
std::vector<bool> StorageTexture::cpuUploadLayers(const std::vector<bool>& units) const {
if (descriptor.dimension != TextureDimension::k2DArray || descriptor.mipCount != 1 || descriptor.dccAddress != 0 || IsBlockCompressed(descriptor.format) || geometry.thick || arrayLayers <= 1 || geometry.imageLayers != arrayLayers || trackedLayerBytes == 0 || units.size() != trackedLayers) return {};
std::vector<bool> selected(arrayLayers, false);
for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) {
const auto begin = geometry.GuestLayerOffset(layer);
const auto end = begin + geometry.layerBytes;
if (end <= begin || end > guestBytes || begin % trackedLayerBytes != 0 || (end % trackedLayerBytes != 0 && end != guestBytes)) return {};
const auto first = static_cast<std::uint32_t>(begin / trackedLayerBytes);
const auto last = static_cast<std::uint32_t>((end + trackedLayerBytes - 1) / trackedLayerBytes);
if (first >= last || last > trackedLayers) return {};
selected[layer] = units[first];
for (auto unit = first + 1; unit < last; ++unit) {
if (units[unit] != selected[layer]) return {};
}
}
if (std::none_of(selected.begin(), selected.end(), [](bool value) { return value; }) || std::all_of(selected.begin(), selected.end(), [](bool value) { return value; })) return {};
return selected;
}
void StorageTexture::upload(const std::vector<bool>* layers) {
const bool profile = LookupOutcomes::Profiled();
const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
@@ -2484,8 +2560,6 @@ void StorageTexture::upload(const std::vector<bool>* layers) {
CaptureTrace::Log("upload image=%llx bytes=%llu generation=%llu reason=%s partial=%d", static_cast<unsigned long long>(descriptor.baseAddress), static_cast<unsigned long long>(guestBytes), static_cast<unsigned long long>(generation), uploadReason, layers != nullptr);
const auto elementBytes = BytesPerElement(descriptor.format);
const auto linearBytes = sliceLinearBytes * arrayLayers;
// A layer selection applies to the direct path alone (the others upload everything); it names
// array layers, which the tracked layers are when there are several.
if (layers != nullptr && ((!blockUnits && trackedLayers != arrayLayers) || std::all_of(layers->begin(), layers->end(), [](bool selected) { return selected; }))) layers = nullptr;
const auto now = GuestMemory::CollectWrites(descriptor.baseAddress, static_cast<std::size_t>(guestBytes));
const auto stampLayers = [&](bool selectedOnly) {
@@ -2497,7 +2571,7 @@ void StorageTexture::upload(const std::vector<bool>* layers) {
}
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
if (!whole && !(*layers)[layer]) continue;
layerGeneration[layer] = now;
layerGeneration[layer] = collectedLayerGeneration(layer, now);
if (whole || generationBaseline.size() != guestBytes) continue;
const auto offset = static_cast<std::size_t>(layerBegin(layer) - descriptor.baseAddress);
GuestMemory::ReadCommitted(layerBegin(layer), std::span<std::byte>(generationBaseline).subspan(offset, static_cast<std::size_t>(layerBytes(layer))));
@@ -2692,15 +2766,36 @@ void StorageTexture::upload(const std::vector<bool>* layers) {
if (profile) LookupOutcomes::Add(LookupOutcomes::UploadDirect, start);
return;
}
layerOriginal.assign(trackedLayers, true);
cancelStoredSaves();
forgetBorrowed(0, trackedLayers);
stampLayers(false);
original.resize(static_cast<std::size_t>(guestBytes));
GuestMemory::ReadCommitted(descriptor.baseAddress, original);
const auto cpuLayers = uploadedKeys == DccKeys::Uncompressed && layers != nullptr ? cpuUploadLayers(*layers) : std::vector<bool>{};
const bool selectedCpu = !cpuLayers.empty();
std::uint64_t uploadedBytes = guestBytes;
if (selectedCpu) {
captureGuestBytes(layers);
for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) {
if ((*layers)[unit]) forgetBorrowed(unit, 1);
}
stampLayers(true);
uploadedBytes = 0;
for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) {
if (cpuLayers[layer]) uploadedBytes += geometry.layerBytes;
}
} else {
layerOriginal.assign(trackedLayers, true);
cancelStoredSaves();
forgetBorrowed(0, trackedLayers);
stampLayers(false);
original.resize(static_cast<std::size_t>(guestBytes));
GuestMemory::ReadCommitted(descriptor.baseAddress, original);
}
{
Buffer staging(context, original.size(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
if (uploadedKeys == DccKeys::Uncompressed) std::memcpy(staging.Bytes().data(), original.data(), original.size());
if (selectedCpu) {
for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) {
if (!cpuLayers[layer]) continue;
const auto offset = geometry.GuestLayerOffset(layer);
std::memcpy(staging.Bytes().data() + offset, original.data() + offset, static_cast<std::size_t>(geometry.layerBytes));
}
} else if (uploadedKeys == DccKeys::Uncompressed) std::memcpy(staging.Bytes().data(), original.data(), original.size());
else ReadTextureSurface(descriptor, uploadedKeys, staging.Bytes().first(original.size()));
DeviceBuffer tiled(context, original.size(), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT);
DeviceBuffer linear(context, static_cast<std::size_t>(linearBytes), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
@@ -2708,9 +2803,16 @@ void StorageTexture::upload(const std::vector<bool>* layers) {
CommandBatch batch(context);
const auto commands = batch.Handle();
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT);
CopyBuffer(context, commands, staging.Handle(), 0, tiled.Handle(), 0, original.size());
if (selectedCpu) {
for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) {
if (!cpuLayers[layer]) continue;
const auto offset = geometry.GuestLayerOffset(layer);
CopyBuffer(context, commands, staging.Handle(), offset, tiled.Handle(), offset, geometry.layerBytes);
}
} else CopyBuffer(context, commands, staging.Handle(), 0, tiled.Handle(), 0, original.size());
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT);
for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) {
if (selectedCpu && !cpuLayers[layer]) continue;
for (std::uint32_t level = 0; level < mips.size(); ++level) {
if (!geometry.HasLayer(level, layer)) continue;
const auto& mip = mips[level];
@@ -2719,15 +2821,16 @@ void StorageTexture::upload(const std::vector<bool>* layers) {
}
const auto linearRead = WholeBufferBarrier(linear.Handle(), VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT);
VkImageMemoryBarrier toTransfer{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
toTransfer.srcAccessMask = selectedCpu ? VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT : 0u;
toTransfer.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
toTransfer.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
toTransfer.oldLayout = selectedCpu ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_UNDEFINED;
toTransfer.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
toTransfer.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toTransfer.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toTransfer.image = image;
toTransfer.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 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, &linearRead, 1, &toTransfer);
const auto regions = CopyRegions();
context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | (selectedCpu ? VK_PIPELINE_STAGE_ALL_COMMANDS_BIT : 0u), VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &linearRead, 1, &toTransfer);
const auto regions = CopyRegions(selectedCpu ? &cpuLayers : nullptr);
context.Function<PFN_vkCmdCopyBufferToImage>("vkCmdCopyBufferToImage")(commands, linear.Handle(), image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast<std::uint32_t>(regions.size()), regions.data());
VkImageMemoryBarrier toGeneral = toTransfer;
toGeneral.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
@@ -2739,7 +2842,7 @@ void StorageTexture::upload(const std::vector<bool>* layers) {
batch.SubmitAndWait();
APS5_LOG_CHARS_OUT("StorageTexture upload done");
}
countStorageUpload(2, guestBytes);
countStorageUpload(2, uploadedBytes);
++version;
if (profile) LookupOutcomes::Add(LookupOutcomes::UploadCpu, start);
}
@@ -2837,6 +2940,16 @@ std::vector<StorageTexture::SliceWindow> StorageTexture::sliceWindows(std::span<
return windows;
}
std::vector<VkRect2D> StorageTexture::UnitRects(const std::vector<bool>& units) const {
std::vector<VkRect2D> rects;
if (units.size() != trackedLayers) return rects;
const auto runs = unitRuns(units);
for (const auto& window : sliceWindows(runs)) {
for (const auto& region : window.regions) rects.push_back({{region.imageOffset.x, region.imageOffset.y}, {region.imageExtent.width, region.imageExtent.height}});
}
return rects;
}
std::vector<std::pair<std::uint64_t, std::uint64_t>> StorageTexture::uncoveredBytes(std::span<const std::pair<std::uint64_t, std::uint64_t>> runs) const {
std::vector<std::pair<std::uint64_t, std::uint64_t>> covered;
if (!geometry.thick) {
@@ -3568,6 +3681,51 @@ void StorageTexture::refreshGeneration() {
generation = *std::min_element(layerGeneration.begin(), layerGeneration.end());
}
std::uint64_t StorageTexture::GenerationFor(std::uint64_t address, std::uint64_t bytes) const {
if (layerBehind.empty() && generation != 0) return generation;
std::optional<std::uint64_t> oldest;
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
const auto begin = layerBegin(layer);
if (address >= begin + layerBytes(layer) || begin >= address + bytes) continue;
if (!layerBehind.empty() && layerBehind[layer]) return 0;
oldest = std::min(oldest.value_or(layerGeneration[layer]), layerGeneration[layer]);
}
return oldest.value_or(generation);
}
std::uint64_t StorageTexture::collectedLayerGeneration(std::uint32_t layer, std::uint64_t whole, bool uncached) const {
if (whole != 0 || trackedLayers <= 1) return whole;
const auto begin = layerBegin(layer);
const auto bytes = static_cast<std::size_t>(layerBytes(layer));
return uncached ? GuestMemory::CollectWritesUncached(begin, bytes) : GuestMemory::CollectWrites(begin, bytes);
}
void StorageTexture::noteLayersBehind(std::uint32_t firstLayer, std::uint32_t lastLayer, const std::vector<bool>& outside) {
if (layerBehind.size() != trackedLayers) layerBehind.assign(trackedLayers, false);
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
if (layer >= firstLayer && layer < lastLayer) layerBehind[layer] = false;
else if (layer < outside.size() && outside[layer]) layerBehind[layer] = true;
}
const auto layersEnd = layerBegin(lastLayer - 1) + layerBytes(lastLayer - 1);
const std::array<std::pair<std::uint64_t, std::uint64_t>, 2> ranges{{{descriptor.baseAddress, layerBegin(firstLayer)}, {layersEnd, descriptor.baseAddress + guestBytes}}};
for (const auto& [from, to] : ranges) {
if (to <= from) continue;
for (const auto& other : overlappingPending(from, static_cast<std::size_t>(to - from))) {
const bool whole = other->descriptor.dccAddress != 0 && other->uploadedKeys != DccKeys::Uncompressed;
for (std::uint32_t unit = 0; unit < other->trackedLayers; ++unit) {
if (!whole && !other->layerPending[unit]) continue;
const auto begin = std::max(from, other->layerBegin(unit));
const auto end = std::min(to, other->layerBegin(unit) + other->layerBytes(unit));
if (end <= begin) continue;
const auto first = static_cast<std::uint32_t>((begin - descriptor.baseAddress) / trackedLayerBytes);
const auto last = static_cast<std::uint32_t>(std::min<std::uint64_t>((end - descriptor.baseAddress + trackedLayerBytes - 1) / trackedLayerBytes, trackedLayers));
for (auto layer = first; layer < last; ++layer) layerBehind[layer] = true;
}
}
}
if (std::none_of(layerBehind.begin(), layerBehind.end(), [](bool behind) { return behind; })) layerBehind.clear();
}
void StorageTexture::markLayersPending(std::uint32_t first, std::uint32_t count) {
CaptureTrace::Log("image-write image=%llx first=%u count=%u generation=%llu", static_cast<unsigned long long>(descriptor.baseAddress), first, count, static_cast<unsigned long long>(generation));
++writes;
@@ -3674,6 +3832,18 @@ bool StorageTexture::pendingUnitInside(std::uint64_t address, std::size_t bytes)
return false;
}
namespace {
void waitForIdlePublish(const char* reason) {
if (reason == nullptr || std::strcmp(reason, "idle") != 0 || Recorder::InCompletion()) return;
if (auto* recorder = Recorder::Active()) {
Recorder::CountSync(0);
recorder->Sync();
}
}
}
bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, const StorageTexture* except, const char* reason, PublishScope scope, bool* published) {
static_cast<void>(FlushDepthPlanes(address, bytes));
if (published != nullptr) *published = false;
@@ -3767,6 +3937,7 @@ bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, cons
// import, so they are published under the same hold, after every store.
if (scope != PublishScope::None) {
const auto units = PublishShadow(address, bytes, scope, PublishReasonFor(reason));
if (units != 0) waitForIdlePublish(reason);
if (published != nullptr) *published = units != 0;
}
return true;
@@ -3831,7 +4002,7 @@ void StorageTexture::saveBeforeUnwatch() {
const auto runs = unitRuns(units);
if (runs.empty()) return;
original.resize(static_cast<std::size_t>(guestBytes));
comparedGuestBytes = {};
comparedRanges = {};
for (const auto& [begin, end] : runs) GuestMemory::ReadCommittedUnsynced(descriptor.baseAddress + begin, std::span(original).subspan(static_cast<std::size_t>(begin), static_cast<std::size_t>(end - begin)));
for (const auto& [begin, end] : runs) GuestMemory::CollectWritesUncached(descriptor.baseAddress + begin, static_cast<std::size_t>(end - begin));
constexpr std::uint64_t block = 65536;
@@ -3892,7 +4063,7 @@ void StorageTexture::applyStoredSave(std::uint64_t ticket, const std::vector<std
for (const auto unit : units) {
if (savedTicket[unit] != ticket) continue;
if (original.size() != guestBytes) original.resize(static_cast<std::size_t>(guestBytes));
comparedGuestBytes = {};
comparedRanges = {};
const auto offset = static_cast<std::size_t>(layerBegin(unit) - descriptor.baseAddress);
std::memcpy(original.data() + offset, stored.data() + offset, static_cast<std::size_t>(layerBytes(unit)));
layerOriginal[unit] = true;
@@ -3932,7 +4103,9 @@ void StorageTexture::awaitStoredSave(const std::vector<bool>* layers) {
bool StorageTexture::PublishShadowsOnly(std::uint64_t address, std::size_t bytes, PublishScope scope, const char* reason) {
if (scope == PublishScope::None || !AnyShadowedOverlaps(address, bytes)) return false;
std::lock_guard gpu(GuestMemory::GpuMutex());
return PublishShadow(address, bytes, scope, PublishReasonFor(reason)) != 0;
if (PublishShadow(address, bytes, scope, PublishReasonFor(reason)) == 0) return false;
waitForIdlePublish(reason);
return true;
}
namespace {
@@ -4529,7 +4702,8 @@ bool StorageTexture::clearByKeysFill(DccKeys keys, std::uint8_t key) {
filledKeys = DccKeys::Uncompressed;
keyProof = {};
forgetBorrowed(0, trackedLayers);
layerGeneration.assign(trackedLayers, GuestMemory::CollectWrites(descriptor.baseAddress, static_cast<std::size_t>(guestBytes)));
const auto now = GuestMemory::CollectWrites(descriptor.baseAddress, static_cast<std::size_t>(guestBytes));
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) layerGeneration[layer] = collectedLayerGeneration(layer, now);
refreshGeneration();
++version;
if (traceKeys) std::fprintf(stderr, "[dcc-keys] key fill %s clears 0x%llx+0x%llx at once (keys 0x%llx, %zu pending units dropped, dirty %d)\n", DccKeysName(keys), static_cast<unsigned long long>(descriptor.baseAddress), static_cast<unsigned long long>(guestBytes), static_cast<unsigned long long>(descriptor.dccAddress), droppedUnits, wasDirty ? 1 : 0);
@@ -4690,7 +4864,8 @@ bool StorageTexture::FillClear(std::span<const std::uint32_t, 4> pattern, std::u
return true;
}
if (!watched) captureGuestBytes(nullptr);
layerGeneration.assign(trackedLayers, GuestMemory::CollectWritesUncached(descriptor.baseAddress, static_cast<std::size_t>(guestBytes)));
const auto now = GuestMemory::CollectWritesUncached(descriptor.baseAddress, static_cast<std::size_t>(guestBytes));
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) layerGeneration[layer] = collectedLayerGeneration(layer, now, true);
refreshGeneration();
forgetBorrowed(0, trackedLayers);
MarkDirty();
@@ -4808,12 +4983,45 @@ void StorageTexture::MarkWholeWritten() {
captureGuestBytes(nullptr);
}
uploadedKeys = DccKeys::Uncompressed;
layerGeneration.assign(trackedLayers, GuestMemory::CollectWritesUncached(descriptor.baseAddress, static_cast<std::size_t>(guestBytes)));
const auto now = GuestMemory::CollectWritesUncached(descriptor.baseAddress, static_cast<std::size_t>(guestBytes));
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) layerGeneration[layer] = collectedLayerGeneration(layer, now, true);
refreshGeneration();
forgetBorrowed(0, trackedLayers);
MarkDirty();
}
void StorageTexture::MarkUnitsWritten(const std::vector<bool>& units) {
if (!blockUnits || descriptor.dccAddress != 0 || units.size() != trackedLayers || std::all_of(units.begin(), units.end(), [](bool unit) { return unit; })) {
MarkWholeWritten();
return;
}
const auto runs = unitRuns(units);
if (runs.empty()) return;
if (GuestMemory::Watched(descriptor.baseAddress, static_cast<std::size_t>(guestBytes))) {
for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) {
if (!units[unit]) continue;
layerOriginal[unit] = false;
cancelStoredSave(unit);
}
} else {
captureGuestBytes(&units);
}
uploadedKeys = DccKeys::Uncompressed;
for (const auto& [begin, end] : runs) {
const auto first = static_cast<std::uint32_t>(begin / trackedLayerBytes);
const auto count = static_cast<std::uint32_t>((end - begin + trackedLayerBytes - 1) / trackedLayerBytes);
const auto collected = GuestMemory::CollectWritesUncached(descriptor.baseAddress + begin, static_cast<std::size_t>(end - begin));
for (auto unit = first; unit < first + count; ++unit) layerGeneration[unit] = collected;
}
refreshGeneration();
for (const auto& [begin, end] : runs) {
const auto first = static_cast<std::uint32_t>(begin / trackedLayerBytes);
const auto count = static_cast<std::uint32_t>((end - begin + trackedLayerBytes - 1) / trackedLayerBytes);
forgetBorrowed(first, count);
markLayersPending(first, count);
}
}
// See AdjacentGenerationEnabled above.
std::vector<StorageTexture::Adjacent> StorageTexture::adjacentPendingUnchanged(std::uint64_t firstBlock, std::uint64_t lastBlock) const {
std::vector<Adjacent> adjacent;
@@ -5270,7 +5478,7 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
if (!advances && adjacent.empty() && shadowed.empty()) return;
const auto now = memoizedCollect ? GuestMemory::CollectWrites(firstStored, static_cast<std::size_t>(lastStored - firstStored)) : GuestMemory::CollectWritesUncached(firstStored, static_cast<std::size_t>(lastStored - firstStored));
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
if (layers[layer] && !skippedLayer[layer]) layerGeneration[layer] = now;
if (layers[layer] && !skippedLayer[layer]) layerGeneration[layer] = collectedLayerGeneration(layer, now, !memoizedCollect);
}
refreshGeneration();
advanceAdjacent(adjacent, now, firstBlock, lastBlock);
@@ -2813,7 +2813,7 @@ bool RunSparseBufferHoleTests(const Context& context, const BdaTestAccess& acces
std::memset(reservation.data + 2 * unit, 0x72, unit);
VkBuffer seeded = VK_NULL_HANDLE;
{
Buffer seed(context, copied, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
Buffer seed(context, copied, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
std::fill(seed.Bytes().begin(), seed.Bytes().end(), std::byte{0xa5});
seeded = seed.Handle();
}
@@ -0,0 +1,45 @@
#include "RdnaDecoder/RdnaInstructionDecoder.hpp"
#include <array>
#include <cstdint>
#include <cstdio>
#include <exception>
#include <span>
using namespace ShaderRecompiler;
namespace {
int failures = 0;
void Check(bool condition, const char* what) {
if (condition) return;
std::fprintf(stderr, "%s\n", what);
++failures;
}
RdnaInstruction Decode(std::span<const std::uint32_t> words) {
return DecodeGcnInstruction(0u, words, 0u);
}
void SetregImmKeepsItsLiteral() {
const std::array<std::uint32_t, 3> setreg{0xba80f801u, 0x12345678u, 0xbf810000u};
const auto instruction = Decode(setreg);
Check(instruction.op == RdnaOpcode::SSetregImm32B32, "s_setreg_imm32_b32 did not decode as its RDNA opcode");
Check(instruction.wordCount == 2u, "a GCN s_setreg_imm32_b32 is not two words");
Check(instruction.source0.value == 0x12345678u, "s_setreg_imm32_b32 lost its literal");
const std::array<std::uint32_t, 2> movk{0xb0001234u, 0xbf810000u};
Check(Decode(movk).wordCount == 1u, "s_movk_i32 took a literal word");
}
}
int main() {
try {
SetregImmKeepsItsLiteral();
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
return 1;
}
if (failures == 0) std::puts("GCN decode tests passed");
return failures == 0 ? 0 : 1;
}
@@ -19,6 +19,7 @@
#include "prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
#include "SceShaders.hpp"
#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp"
#include "ControlFlow/RequestSerializer.hpp"
@@ -1729,7 +1730,39 @@ void DisabledColorTests() {
queue.context[0x002] = 3;
const auto thirdSlice = AgcDriver::Graphics::DecodeState(queue);
Require(thirdSlice.depth && thirdSlice.depth->firstSlice == 3 && thirdSlice.depth->address == depthOnly.depth->address + 3u * AgcDriver::Graphics::DepthSliceBytes(depthOnly.depth->extent, 4u), "a depth view of slice 3 lost its slice or its Z offset");
queue.context[0x002] = 1u << 26u;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "on a surface without mips");
queue.context[0x002] = 0;
{
using AgcDriver::Graphics::ComputeElementMipLayout;
using AgcDriver::Graphics::TextureTileMode;
auto mipped = queue;
mipped.context[0x007] = 0x03ff03ff;
mipped.context[0x010] = 0x80000181u | (2u << 16u);
const auto mips = ComputeElementMipLayout(TextureTileMode::kZ64KBX, 2u, 1024u, 1024u, 3u);
Require(!mips.at(1).tail && mips.at(1).width == 512u && mips.at(1).height == 512u, "the 1024x1024 Z_X layout does not give a 512x512 second mip outside the tail");
const auto first = AgcDriver::Graphics::DecodeState(mipped);
Require(first.depth && first.depth->address == depthOnly.depth->address + mips.at(0).tiledOffset && first.depth->extent.width == 1024u && first.depth->extent.height == 1024u, "view mip 0 of a mip chain is not its first level");
mipped.context[0x002] = 1u << 26u;
const auto second = AgcDriver::Graphics::DecodeState(mipped);
Require(second.depth && second.depth->address == depthOnly.depth->address + mips.at(1).tiledOffset, "view mip 1 is not at the second level's offset in the Z_X mip chain");
Require(second.depth->extent.width == 512u && second.depth->extent.height == 512u, "view mip 1 does not render at the second level's extent");
mipped.context[0x002] = (1u << 26u) | 1u | (1u << 13u);
const auto sliced = AgcDriver::Graphics::DecodeState(mipped);
Require(sliced.depth && sliced.depth->address == second.depth->address + AgcDriver::Graphics::ComputeSurfaceSize(mips, 1u), "slice 1 of a mip chain does not step by the whole chain");
mipped.context[0x002] = 3u << 26u;
expectFailure([&] { AgcDriver::Graphics::DecodeState(mipped); }, "is beyond the surface's mips");
mipped.context[0x002] = 2u << 26u;
mipped.context[0x007] = 0x003f003f;
expectFailure([&] { AgcDriver::Graphics::DecodeState(mipped); }, "into the packed mip tail is unsupported");
mipped.context[0x007] = 0x03ff03ff;
mipped.context[0x002] = 1u << 26u;
mipped.context[0x010] |= 1u << 29u;
mipped.context[0x005] = 0x300;
expectFailure([&] { AgcDriver::Graphics::DecodeState(mipped); }, "HTILE for the depth view mip");
mipped.context[0x010] = (0x80000181u | (2u << 16u)) & ~(31u << 4u);
expectFailure([&] { AgcDriver::Graphics::DecodeState(mipped); }, "uses a swizzle other than 64 KiB Z_X");
}
for (const auto colorControl : {0x0u, 0xcc0000u}) {
queue = makeState();
queue.context[0x202] = colorControl;
@@ -9195,10 +9228,16 @@ void ProvokingVertexTests() {
meshDraw.topology = topology;
for (const auto& drawn : {std::span<const CompiledShader>{}, std::span<const CompiledShader>(flatShaders), std::span<const CompiledShader>(flatP10Shaders), std::span<const CompiledShader>(smoothShaders), std::span<const CompiledShader>(slotShaders)}) ValidateProvokingVertex(supported, meshDraw, drawn);
}
meshDraw.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP;
for (const auto& drawn : {std::span<const CompiledShader>{}, std::span<const CompiledShader>(flatShaders), std::span<const CompiledShader>(flatP10Shaders), std::span<const CompiledShader>(smoothShaders)}) ValidateProvokingVertex(supported, meshDraw, drawn);
fragment.fragmentParameters = {{0, 0, false, true, false, false, false}, {1, 1, false, true, false, false, false}, {2, 2, false, true, false, false, false}, {3, 3, false, true, false, false, false}};
ValidateProvokingVertex(supported, meshDraw, shaders);
expectFailure([&] { ValidateProvokingVertex(supported, meshDraw, slotShaders); }, "last provoking vertex with explicit per-vertex triangle-strip interpolation is unsupported");
meshDraw.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN;
expectFailure([&] { ValidateProvokingVertex(supported, meshDraw, flatShaders); }, "last provoking vertex is unsupported for triangle fans assembled for a geometry program");
for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN}) {
meshDraw.topology = topology;
expectFailure([&] { ValidateProvokingVertex(supported, meshDraw, flatShaders); }, "last provoking vertex is unsupported for triangle strips or fans assembled for a geometry program");
auto firstDraw = meshDraw;
firstDraw.topology = topology;
firstDraw.provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT;
ValidateProvokingVertex(supported, firstDraw, flatShaders);
}
@@ -9884,26 +9923,63 @@ void validationTests() {
attribute.resource.fields[3] = 77u << 12u;
attribute.fetchIndex = 1;
attribute.resource.fields[1] = 0;
attribute.resource.fields[2] = 16;
const auto zeroStride = [&](std::uint32_t select, std::uint32_t records) {
attribute.resource.fields[3] = (77u << 12u) | (select << 28u);
attribute.resource.fields[2] = records;
};
for (const auto records : {0u, 1u, 16u, 0xffffffffu}) {
zeroStride(0, records);
Require(AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 0 must read every zero-stride fetch as zero");
Require(AgcDriver::Graphics::ZeroStrideAvailableBytes(attribute) == 0, "OOB_SELECT 0 zero-stride fetch has bytes to read");
expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1); }, "byte range");
expectFailure([&] { AgcDriver::Graphics::VertexBufferExtent(attribute); }, "empty or oversized");
}
zeroStride(1, 16);
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2) == 16, "zero stride must repeat one value");
Require(AgcDriver::Graphics::VertexBufferReadOffset(attribute, 100, 7) == 0, "zero-stride input skipped its repeated value");
attribute.resource.fields[2] = 8;
expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1); }, "byte range");
Require(!AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "a structured zero-stride fetch was treated as out of range");
attribute.resource.fields[3] = (77u << 12u) | (2u << 28u);
attribute.resource.fields[2] = 1;
Require(AgcDriver::Graphics::VertexBufferExtent(attribute) == 16, "OOB_SELECT 1 extent must cover the whole fetch");
zeroStride(1, 0);
Require(AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 1 without records must read zero");
zeroStride(1, 8);
Require(!AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 1 counted NUM_RECORDS in bytes");
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 8) == 16, "OOB_SELECT 1 must read the whole element for every index below NUM_RECORDS");
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1, 7) == 16, "OOB_SELECT 1 must read the whole element for the last index below NUM_RECORDS");
expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 9); }, "record count");
expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1, 8); }, "record count");
zeroStride(1, 1);
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1) == 16, "OOB_SELECT 1 with one record must read the element at index 0");
expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 2); }, "record count");
attribute.fetchIndex = 0;
zeroStride(1, 3);
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 2, 1) == 16, "OOB_SELECT 1 must read the element for index 2 of 3 records");
expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 3, 1); }, "record count");
attribute.fetchIndex = 1;
zeroStride(2, 1);
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2) == 16, "OOB_SELECT 2 must read a zero-stride fetch whole while NUM_RECORDS is nonzero");
Require(AgcDriver::Graphics::VertexBufferExtent(attribute) == 16, "OOB_SELECT 2 extent must cover the whole fetch");
Require(!AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 2 with records was treated as out of range");
attribute.resource.fields[2] = 0;
zeroStride(2, 0);
Require(AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 2 without records must read zero");
attribute.resource.fields[3] = (77u << 12u) | (3u << 28u);
zeroStride(3, 0);
Require(AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 3 without records must read zero");
attribute.resource.fields[2] = 8;
Require(!AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 3 with records was treated as out of range");
expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1); }, "byte range");
attribute.resource.fields[2] = 16;
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1) == 16, "OOB_SELECT 3 must read a fetch inside the range");
for (const auto records : {1u, 8u, 15u}) {
zeroStride(3, records);
Require(AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 3 must read a fetch that only partly fits as zero");
expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1); }, "byte range");
}
for (const auto records : {16u, 17u, 0xffffffffu}) {
zeroStride(3, records);
Require(!AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 3 with the whole fetch in range was treated as out of range");
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2) == 16, "OOB_SELECT 3 must read a fetch inside the range, whatever the index");
Require(AgcDriver::Graphics::VertexBufferExtent(attribute) == 16, "OOB_SELECT 3 extent must cover only the fetch");
}
attribute.resource.fields[3] = (22u << 12u) | (3u << 28u);
attribute.resource.fields[2] = 4;
Require(!AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 3 compared NUM_RECORDS with the wrong fetch size");
attribute.resource.fields[2] = 3;
Require(AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 3 read a 4-byte fetch past 3 bytes");
attribute.resource.fields[3] = (113u << 12u) | (3u << 28u);
expectFailure([&] { AgcDriver::Graphics::VertexFetchOutOfRange(attribute); }, "unsupported vertex format");
attribute.resource.fields[3] = 113u << 12u;
expectFailure([&] { AgcDriver::Graphics::DecodeVertexFormat(attribute); }, "unsupported vertex format");
attribute.resource.fields[3] = 50u << 12u;
@@ -517,7 +517,7 @@ void partialUnmap(Fixture& fixture) {
{
GuestAllocations::Mutation mutation;
mutation.Unmap(middle, part, [&](const void* piece, std::size_t bytes, const void* original, bool last) {
Require(!imported->freed.load(), "an unmap inside a host import freed the memory of the side it keeps");
requireFreed(imported);
Require(!other->freed.load(), "partial unmap retired a disjoint host import");
Require(piece == middle && bytes == part && original == allocation.data && !last, "partial unmap changed the wrong guest extent");
});
@@ -525,9 +525,8 @@ void partialUnmap(Fixture& fixture) {
Require(!RegisteredReadableCovers(reinterpret_cast<std::uint64_t>(middle), part), "the middle of a partial unmap remains registered");
const auto low = fixture.Import(allocation.Address(), part);
const auto high = fixture.Import(allocation.Address() + 2 * part, part);
Require(high->memory == imported->memory && !imported->freed.load(), "the side kept from an unmap inside the host import was imported again");
Require(imported->freed.load(), "the side kept from an unmap inside the host import reused memory imported across the unmapped range");
Require(high->address == allocation.Address() + 2 * part && high->bytes == part, "the kept side of the host import does not cover its remaining range");
Require(low->memory != imported->memory, "the smaller side of an unmap inside the host import was not imported again");
Require(!low->freed.load() && !high->freed.load() && !other->freed.load(), "remaining partial mappings could not be imported");
{
GuestAllocations::Mutation mutation;
+195 -32
View File
@@ -117,6 +117,32 @@ void ReleaseWatched(void* block, std::size_t bytes) {
#endif
}
bool collectMappingHistoryTests() {
constexpr std::size_t bytes = 131072;
auto* memory = static_cast<std::uint8_t*>(AllocateWatched(bytes, 65536));
if (memory == nullptr) return false;
GuestAllocations::Mutation().Add(memory, bytes, true, true, false);
struct Cleanup {
std::uint8_t* memory;
~Cleanup() {
GuestAllocations::Mutation().Remove(memory);
ReleaseWatched(memory, bytes);
}
} cleanup{memory};
const auto address = reinterpret_cast<std::uint64_t>(memory);
std::memset(memory, 0x11, bytes);
AgcDriver::GuestMemory::BumpCollectEpoch();
const auto before = AgcDriver::GuestMemory::CollectWrites(address, bytes);
Require(before != 0, "the mapping fixture has no complete write history");
memory[4096] = 0x22;
GuestAllocations::Mutation().Remove(memory);
GuestAllocations::Mutation().Add(memory, bytes, true, true, false);
Require(AgcDriver::GuestMemory::CollectWrites(address, bytes) != 0, "the replacement mapping lost write tracking");
Require(!AgcDriver::GuestMemory::UnchangedSince(address, bytes, before), "a cached collect hid a CPU store across allocation replacement");
Require(memory[4096] == 0x22, "collecting the replacement mapping changed its CPU bytes");
return true;
}
bool partialWatchRestorationTests() {
#ifdef _WIN32
constexpr std::size_t bytes = 131072;
@@ -191,6 +217,41 @@ std::optional<std::size_t> selectDevice(std::span<const VkPhysicalDeviceProperti
return selected;
}
bool hintMappingHistoryTests() {
#ifdef _WIN32
constexpr std::size_t bytes = 131072;
auto* memory = static_cast<std::uint8_t*>(AllocateWatched(bytes, 65536));
if (memory == nullptr) return false;
GuestAllocations::Mutation().Add(memory, bytes, true, true, false);
struct Cleanup {
std::uint8_t* memory;
~Cleanup() {
GuestAllocations::Mutation().Remove(memory);
ReleaseWatched(memory, bytes);
}
} cleanup{memory};
const auto address = reinterpret_cast<std::uint64_t>(memory);
std::memset(memory, 0x11, bytes);
AgcDriver::GuestMemory::BumpCollectEpoch();
AgcDriver::GuestMemory::Unwatch(address, bytes);
const auto before = AgcDriver::GuestMemory::UntrackedHint(address, bytes);
Require(before != 0, "the hint mapping fixture has no write hint");
Require(AgcDriver::GuestMemory::UntrackedHint(address, bytes) == before, "an unchanged hint range advanced its history");
memory[4096] = 0x22;
GuestAllocations::Mutation().Remove(memory);
GuestAllocations::Mutation().Add(memory, bytes, true, true, false);
const auto after = AgcDriver::GuestMemory::UntrackedHint(address, bytes);
Require(after > before, "a cached hint walk hid a CPU store across allocation replacement");
Require(memory[4096] == 0x22 && memory[bytes - 4096] == 0x11, "collecting replacement hints changed guest bytes");
GuestAllocations::Mutation().Remove(memory);
GuestAllocations::Mutation().Add(memory, bytes, true, true, false);
Require(AgcDriver::GuestMemory::UntrackedHint(address, bytes) == after, "allocation replacement invented a CPU write hint");
return true;
#else
return false;
#endif
}
void deviceSelectionTests() {
const auto properties = [](const char* name, VkPhysicalDeviceType type) {
VkPhysicalDeviceProperties result{};
@@ -2505,6 +2566,12 @@ int reimportBench(const Device& device) {
constexpr std::size_t moves = 5;
constexpr std::size_t page = 4096;
constexpr std::size_t alignment = 2ull << 20u;
constexpr std::size_t requiredImportBytes = bytes + inside;
const auto availableImportBytes = HostImportBudgetRemaining(context);
if (availableImportBytes < requiredImportBytes) {
std::cout << "skipped, the re-import bench needs " << (requiredImportBytes >> 20u) << " MiB of simultaneous host imports; the current budget has " << (availableImportBytes >> 20u) << " MiB available\n";
return 77;
}
struct Release {
void* mapping = nullptr;
std::int64_t physical = -1;
@@ -2619,15 +2686,15 @@ int reimportBench(const Device& device) {
throw;
}
gpu.lock();
Require(importedAgain.empty(), "the kept side was imported again instead of trimmed after the " + importedAgain + " cuts");
std::printf("re-import bench: every cut at the head, at the tail and inside the range kept the larger side by trimming the first import\n");
Require(importedAgain == "head, tail, inside", "a cut kept memory imported across the changed host range instead of importing the remaining side again: " + importedAgain);
std::printf("re-import bench: every cut at the head, at the tail and inside the range retired the original import before importing the remaining side\n");
return 0;
}
int trimmedImportTests(const Device& device) {
int hostImportCutTests(const Device& device) {
const auto& context = device.GetContext();
if (context.hostImportAlignment == 0 || context.settleQueue == VK_NULL_HANDLE || !AgcDriver::GuestMemory::WriteWatched()) {
std::cout << "skipped, the trimmed import tests require host imports, a settle queue and write watching\n";
std::cout << "skipped, the host import cut tests require host imports, a settle queue and write watching\n";
return 77;
}
constexpr std::size_t bytes = 32ull << 20u;
@@ -2644,14 +2711,13 @@ int trimmedImportTests(const Device& device) {
std::size_t keptEnd;
std::size_t smaller;
std::size_t smallerEnd;
bool trimmed;
};
constexpr std::array<Case, 5> cases{{
{"a page made inaccessible inside the import, nearer its start", inside + guard, guard, false, inside + 2 * guard, bytes, 0, inside + guard, true},
{"a page made inaccessible inside the import, nearer its end", bytes - inside - 2 * guard, guard, false, 0, bytes - inside - 2 * guard, bytes - inside - guard, bytes, true},
{"an unmap of a 2 MiB run inside the import", inside, run, true, inside + run, bytes, 0, inside, true},
{"an unmap of the import's first 2 MiB run", 0, run, true, run, bytes, 0, 0, true},
{"an unmap of 1440 KiB at the start of the import's first 2 MiB run", 0, 1440ull << 10u, true, 1440ull << 10u, bytes, 0, 0, false},
{"a page made inaccessible inside the import, nearer its start", inside + guard, guard, false, inside + 2 * guard, bytes, 0, inside + guard},
{"a page made inaccessible inside the import, nearer its end", bytes - inside - 2 * guard, guard, false, 0, bytes - inside - 2 * guard, bytes - inside - guard, bytes},
{"an unmap of a 2 MiB run inside the import", inside, run, true, inside + run, bytes, 0, inside},
{"an unmap of the import's first 2 MiB run", 0, run, true, run, bytes, 0, 0},
{"an unmap of 1440 KiB at the start of the import's first 2 MiB run", 0, 1440ull << 10u, true, 1440ull << 10u, bytes, 0, 0},
}};
struct Backing {
void* mapping = nullptr;
@@ -2679,15 +2745,14 @@ int trimmedImportTests(const Device& device) {
const auto check = [&](const Case& test) {
const std::string name = test.name;
Backing backing;
Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, run, 0, &backing.physical) == 0, "allocate the trimmed import test backing");
Require(sceKernelMapDirectMemory(&backing.mapping, bytes, 0x33, 0, backing.physical, run) == 0, "map the trimmed import test backing");
Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, run, 0, &backing.physical) == 0, "allocate the host import cut test backing");
Require(sceKernelMapDirectMemory(&backing.mapping, bytes, 0x33, 0, backing.physical, run) == 0, "map the host import cut test backing");
const auto address = reinterpret_cast<std::uint64_t>(backing.mapping);
auto* memory = static_cast<std::byte*>(backing.mapping);
for (std::size_t offset = 0; offset < bytes; offset += page) std::memset(memory + offset, static_cast<int>(0x40u + (offset / page) % 64u), 64);
gpu.lock();
const auto* whole = HostImportFor(context, address, bytes);
Require(whole != nullptr && whole->base == address && whole->bytes == bytes && whole->alias == nullptr && !whole->unwatched, name + ": the range was not imported whole, settled and watched");
const auto original = whole->memory;
auto* cut = memory + test.offset;
gpu.unlock();
if (test.unmap) {
@@ -2702,15 +2767,13 @@ int trimmedImportTests(const Device& device) {
const auto end = address + test.keptEnd;
const auto* kept = HostImportFor(context, begin, static_cast<std::size_t>(end - begin));
Require(kept != nullptr && kept->base == begin && kept->base + kept->bytes == end, name + ": the larger side is not imported whole");
const auto keptMemory = kept->memory;
if (test.trimmed) Require(kept->memory == original, "the larger side of " + name + " was imported again instead of kept");
else Require(kept->memory != original, "the side of " + name + " whose 2 MiB run the unmap replaced is served from the import that pinned the run's old pages");
Require(kept->memoryBase == begin && kept->pinnedBytes == end - begin, "the side of " + name + " is served from the import that pinned the changed host range");
requireImportServesGuest(context, recorder, *kept, begin, end, ++salt, "the larger side of " + name);
if (test.smallerEnd != test.smaller) {
const auto* smaller = HostImportFor(context, address + test.smaller, test.smallerEnd - test.smaller);
Require(smaller != nullptr && smaller->base == address + test.smaller && smaller->bytes == test.smallerEnd - test.smaller && smaller->memory != original, "the smaller side of " + name + " is not imported apart from the larger one");
Require(smaller != nullptr && smaller->base == address + test.smaller && smaller->bytes == test.smallerEnd - test.smaller && smaller->memoryBase == address + test.smaller && smaller->pinnedBytes == test.smallerEnd - test.smaller, "the smaller side of " + name + " is not imported apart from the larger one");
requireImportServesGuest(context, recorder, *smaller, address + test.smaller, address + test.smallerEnd, ++salt, "the smaller side of " + name);
Require(HostImportFor(context, begin, static_cast<std::size_t>(end - begin)) == kept && kept->memory == original, "importing the smaller side of " + name + " dropped the larger one");
Require(HostImportFor(context, begin, static_cast<std::size_t>(end - begin)) == kept && kept->memoryBase == begin && kept->pinnedBytes == end - begin, "importing the smaller side of " + name + " replaced the safe import of the larger one");
}
if (test.unmap) {
gpu.unlock();
@@ -2722,26 +2785,25 @@ int trimmedImportTests(const Device& device) {
const auto from = reinterpret_cast<std::uint64_t>(cut);
const auto* remapped = HostImportFor(context, address, bytes);
Require(remapped != nullptr && remapped->base == address && remapped->bytes == bytes, name + ": the range mapped again is not imported whole");
Require(remapped->memory != keptMemory, "after " + name + " the pages mapped again are served from the import made before they were mapped again");
Require(remapped->memoryBase == address && remapped->pinnedBytes == bytes, "after " + name + " the pages mapped again are served from the import that pinned the old ones");
requireImportServesGuest(context, recorder, *remapped, from, from + test.length, ++salt, "the pages mapped again after " + name);
requireImportServesGuest(context, recorder, *remapped, begin, end, ++salt, "the larger side after " + name + " and a new mapping");
}
cleanup();
gpu.unlock();
std::cout << "trimmed import: " << name << ": the larger side was " << (test.trimmed ? "kept" : "imported again") << " and the GPU and the write watch see the guest's bytes" << (test.unmap ? ", also after the pages were mapped again" : "") << '\n';
std::cout << "host import after cut: " << name << ": the changed import was retired before the remaining range was imported, and the GPU and write watch see the guest's bytes" << (test.unmap ? ", also after the pages were mapped again" : "") << '\n';
};
const auto remapWhileTrimmed = [&] {
const std::string name = "an unmap and a new mapping of the kept side while the import waits trimmed";
const std::string name = "an unmap and a new mapping of the retained side while the old import is retired";
Backing backing;
Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, run, 0, &backing.physical) == 0, "allocate the trimmed import test backing");
Require(sceKernelMapDirectMemory(&backing.mapping, bytes, 0x33, 0, backing.physical, run) == 0, "map the trimmed import test backing");
Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, run, 0, &backing.physical) == 0, "allocate the host import cut test backing");
Require(sceKernelMapDirectMemory(&backing.mapping, bytes, 0x33, 0, backing.physical, run) == 0, "map the host import cut test backing");
const auto address = reinterpret_cast<std::uint64_t>(backing.mapping);
auto* memory = static_cast<std::byte*>(backing.mapping);
for (std::size_t offset = 0; offset < bytes; offset += page) std::memset(memory + offset, static_cast<int>(0x40u + (offset / page) % 64u), 64);
gpu.lock();
const auto* whole = HostImportFor(context, address, bytes);
Require(whole != nullptr && whole->base == address && whole->bytes == bytes && whole->alias == nullptr && !whole->unwatched, name + ": the range was not imported whole, settled and watched");
const auto original = whole->memory;
gpu.unlock();
Require(sceKernelMprotect(memory, run, 0x03) == 0, name + ": the protection of the head failed");
Require(sceKernelMunmap(memory + run, bytes - run) == 0, name + ": the unmap of the kept side failed");
@@ -2757,10 +2819,10 @@ int trimmedImportTests(const Device& device) {
const auto* remapped = HostImportFor(context, begin, static_cast<std::size_t>(end - begin));
Require(remapped != nullptr && remapped->base == begin && remapped->base + remapped->bytes == end, name + ": the pages mapped again are not imported whole");
requireImportServesGuest(context, recorder, *remapped, begin, end, ++salt, "the pages mapped again after " + name);
Require(remapped->memory != original, "after " + name + " the pages mapped again are served from the import that pinned the old ones");
Require(remapped->memoryBase == begin && remapped->pinnedBytes == end - begin, "after " + name + " the pages mapped again are served from the import that pinned the old ones");
cleanup();
gpu.unlock();
std::cout << "trimmed import: " << name << ": the pages mapped again were imported again and the GPU and the write watch see the guest's bytes\n";
std::cout << "host import cut: " << name << ": the pages mapped again were imported again and the GPU and the write watch see the guest's bytes\n";
};
gpu.unlock();
try {
@@ -2771,7 +2833,7 @@ int trimmedImportTests(const Device& device) {
throw;
}
gpu.lock();
std::cout << "Trimmed host import tests passed\n";
std::cout << "Host import cut lifetime tests passed\n";
return 0;
}
#endif
@@ -4677,6 +4739,79 @@ bool unitShadowTests(const Device& device, Recorder& recorder) {
return true;
}
void idlePublishWaitTests(const Device& device, Recorder& recorder) {
using namespace AgcDriver::GuestMemory;
const auto& context = device.GetContext();
constexpr std::uint64_t unit = 65536;
constexpr std::size_t bytes = 8 * unit;
void* block = AllocateWatched(bytes, 65536);
Require(block != nullptr, "unit shadows are on without write watching");
struct Release {
void* block;
~Release() { ReleaseWatched(block, bytes); }
} release{block};
std::memset(block, 0x11, bytes);
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;
std::uint64_t address;
~Unregister() {
{
GuestAllocations::Mutation mutation;
mutation.Remove(block);
}
HostImportFor(context, address, bytes);
}
} unregister{context, block, address};
const auto* import = HostImportFor(context, address, bytes);
if (import == nullptr || !Watched(address, bytes)) {
std::cout << "host import of the idle publish test block refused or not watched: idle publish wait not tested\n";
return;
}
recorder.Sync();
CollectWritesUncached(address, bytes);
auto* words = static_cast<std::uint8_t*>(block);
const auto copyBuffer = context.Function<PFN_vkCmdCopyBuffer>("vkCmdCopyBuffer");
const auto retile = [&](std::uint64_t begin, std::uint8_t value) {
auto destination = ShadowDestinationFor(context, *import, begin, begin + unit);
Require(destination.has_value() && destination->seedUnits.empty(), "a whole-unit retile piece has no destination");
auto pattern = std::make_shared<Buffer>(context, static_cast<std::size_t>(unit), VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
std::memset(pattern->Bytes().data(), value, pattern->Bytes().size());
const auto commands = recorder.Commands();
recorder.Keep(pattern);
recorder.Keep(destination->slab);
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT);
const VkBufferCopy piece{0, destination->offset, unit};
copyBuffer(commands, pattern->Handle(), destination->buffer, 1, &piece);
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT);
recorder.MarkCovered(VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT);
const ShadowedRange range{begin, begin + unit, destination->slab};
MarkShadowed(*import, std::span(&range, 1), TrackerGeneration());
Require(AnyShadowedOverlaps(begin, unit), "the retiled unit is not shadowed");
};
retile(address + 2 * unit, 0x22);
retile(address + 3 * unit, 0x33);
StorageTexture::FlushAllPending("idle");
Require(!AnyShadowedOverlaps(address + 2 * unit, static_cast<std::size_t>(2 * unit)), "the idle flush did not publish the shadowed units");
Require(!recorder.PendingWriteOverlaps(address + 2 * unit, static_cast<std::size_t>(2 * unit)), "the idle flush returned before its publish's copies completed");
words[2 * unit] = 0x44;
words[4 * unit - 1] = 0x45;
recorder.Sync();
Require(words[2 * unit] == 0x44 && words[4 * unit - 1] == 0x45, "a CPU store made after the idle flush returned was overwritten by its publish");
Require(words[2 * unit + 1] == 0x22 && words[3 * unit - 1] == 0x22 && words[3 * unit] == 0x33 && words[1 * unit] == 0x11 && words[4 * unit] == 0x11, "the idle publish's bytes are wrong");
retile(address + 5 * unit, 0x55);
StorageTexture::FlushAllPending("label");
Require(recorder.PendingWriteOverlaps(address + 5 * unit, unit), "a publish of a flush other than the idle one was waited");
recorder.Sync();
Require(words[5 * unit] == 0x55 && words[6 * unit - 1] == 0x55, "the label flush's publish did not land");
std::cout << "idle publish wait verified\n";
}
void storageRefreshTests(const Device& device, Recorder& recorder, bool watched) {
const auto& base = device.GetContext();
if (base.hostImportAlignment == 0) {
@@ -8866,8 +9001,8 @@ bool vertexTargetRangeTests(const Device& device, Recorder& recorder, bool impor
const std::array range{std::pair{address + unit, address + 2 * unit}};
Require(StorageTexture::AnyPendingOverlaps(range), "the color producer did not leave GPU results pending");
};
const auto attributeFor = [](std::uint64_t base, std::uint32_t stride, std::uint32_t fetchIndex) {
return ShaderRecompiler::VertexAttribute{0, 4, {{static_cast<std::uint32_t>(base), static_cast<std::uint32_t>(base >> 32u) | (stride << 16u), static_cast<std::uint32_t>(bytes / 16), 77u << 12u}}, fetchIndex};
const auto attributeFor = [](std::uint64_t base, std::uint32_t stride, std::uint32_t fetchIndex, std::uint32_t outOfBoundsSelect = 0) {
return ShaderRecompiler::VertexAttribute{0, 4, {{static_cast<std::uint32_t>(base), static_cast<std::uint32_t>(base >> 32u) | (stride << 16u), static_cast<std::uint32_t>(bytes / 16), (77u << 12u) | (outOfBoundsSelect << 28u)}}, fetchIndex};
};
const auto check = [&](std::uint32_t edge) {
std::vector<std::byte> texels(64 * 64 * 4);
@@ -8938,9 +9073,12 @@ bool vertexTargetRangeTests(const Device& device, Recorder& recorder, bool impor
reject([&] { Draw(context, consumer, indexed, shaders); }, "index buffer aliases the render target");
}
reject([&] { Draw(context, consumer, triangle, shaders); }, "vertex buffer aliases the render target");
vertex.vertexAttributes = {attributeFor(address, 0, fetchIndex)};
vertex.vertexAttributes = {attributeFor(address, 0, fetchIndex, 3)};
reject([&] { Draw(context, consumer, draw, shaders); }, "vertex buffer aliases the render target");
}
vertex.vertexAttributes = {attributeFor(address, 0, 0)};
Draw(context, consumer, triangle, shaders);
WaitForOptimizedPipelines(context.device);
std::cout << "Vertex range and pending color producer tests passed (" << (imported ? "imported" : "copied") << ")\n";
return true;
}
@@ -9945,6 +10083,16 @@ void Run(Context context) {
int main(int argc, char** argv) {
try {
if (argc == 2 && std::string_view(argv[1]) == "--collect-mapping-history-only") {
if (!collectMappingHistoryTests()) return 77;
std::cout << "collect mapping history tests passed\n";
return 0;
}
if (argc == 2 && std::string_view(argv[1]) == "--hint-mapping-history-only") {
if (!hintMappingHistoryTests()) return 77;
std::cout << "hint mapping history tests passed\n";
return 0;
}
if (argc == 2 && std::string_view(argv[1]) == "--partial-watch-restoration-only") {
if (!partialWatchRestorationTests()) return 77;
std::cout << "partial private write-watch restoration tests passed\n";
@@ -9962,7 +10110,7 @@ int main(int argc, char** argv) {
}
std::unique_ptr<Device> created;
try {
created = std::make_unique<Device>(argc == 2 && (std::string_view(argv[1]) == "--import-watch-other-family-only" || std::string_view(argv[1]) == "--deferred-settle-other-family-only"));
created = std::make_unique<Device>(argc == 2 && (std::string_view(argv[1]) == "--import-watch-other-family-only" || std::string_view(argv[1]) == "--deferred-settle-other-family-only" || std::string_view(argv[1]) == "--host-import-lifetime-other-family-only"));
} catch (const std::exception& error) {
if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw;
std::cout << "skipped, no usable Vulkan device: " << error.what() << '\n';
@@ -9987,6 +10135,13 @@ int main(int argc, char** argv) {
return 0;
}
#ifdef _WIN32
if (argc == 2 && std::string_view(argv[1]) == "--host-import-lifetime-other-family-only") {
if (device.GetContext().settleFamily == device.GetContext().queueFamily) {
std::cout << "skipped, no other queue family to settle imports on\n";
return 77;
}
return RunHostImportLifetimeTests(device.GetContext());
}
if (argc == 2 && (std::string_view(argv[1]) == "--deferred-settle-only" || std::string_view(argv[1]) == "--deferred-settle-other-family-only")) {
if (std::string_view(argv[1]) == "--deferred-settle-other-family-only" && device.GetContext().settleFamily == device.GetContext().queueFamily) {
std::cout << "skipped, no other queue family to settle imports on\n";
@@ -9999,7 +10154,7 @@ int main(int argc, char** argv) {
if (argc == 2 && std::string_view(argv[1]) == "--settle-retire-only") return settleRetireTests(device) ? 0 : 77;
if (argc == 2 && std::string_view(argv[1]) == "--settle-storage-image-only") return settleStorageImageTests(device) ? 0 : 77;
if (argc == 2 && std::string_view(argv[1]) == "--reimport-bench") return reimportBench(device);
if (argc == 2 && std::string_view(argv[1]) == "--trimmed-import-only") return trimmedImportTests(device);
if (argc == 2 && std::string_view(argv[1]) == "--host-import-cut-only") return hostImportCutTests(device);
#endif
if (argc == 2 && std::string_view(argv[1]) == "--host-import-retirement-only") {
const auto alignment = device.GetContext().hostImportAlignment;
@@ -10164,6 +10319,14 @@ int main(int argc, char** argv) {
parkedImportUnmapTests(device);
return 0;
}
if (argc == 2 && std::string_view(argv[1]) == "--idle-publish-wait-only") {
if (!AgcDriver::Graphics::UnitShadowEnabled() || device.GetContext().hostImportAlignment == 0) {
std::cout << "skipped, unit shadows or host imports are unavailable\n";
return 77;
}
idlePublishWaitTests(device, recorder);
return 0;
}
if (argc == 2 && std::string_view(argv[1]) == "--unit-shadow-only") {
if (!AgcDriver::Graphics::UnitShadowEnabled() || device.GetContext().hostImportAlignment == 0) {
std::cout << "skipped, unit shadows or host imports are unavailable\n";
@@ -1,33 +0,0 @@
#include "prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp"
#include <cstdio>
namespace {
struct Case {
VkDriverId driver;
bool usable;
};
constexpr Case Cases[]{
{VK_DRIVER_ID_AMD_PROPRIETARY, false},
{VK_DRIVER_ID_AMD_OPEN_SOURCE, true},
{VK_DRIVER_ID_MESA_RADV, true},
{VK_DRIVER_ID_NVIDIA_PROPRIETARY, true},
{VK_DRIVER_ID_INTEL_PROPRIETARY_WINDOWS, true},
{VK_DRIVER_ID_INTEL_OPEN_SOURCE_MESA, true},
{VK_DRIVER_ID_MESA_LLVMPIPE, true},
{VK_DRIVER_ID_MOLTENVK, true},
};
}
int main() {
int failures = 0;
for (const auto& item : Cases) {
if (AgcDriver::RobustBufferAccess2Usable(item.driver) != item.usable) {
std::fprintf(stderr, "driver %d: expected robustBufferAccess2 to be %s\n", static_cast<int>(item.driver), item.usable ? "usable" : "unusable");
++failures;
}
}
return failures == 0 ? 0 : 1;
}
@@ -3551,6 +3551,40 @@ void verifyWaterfallImageTable() {
require(!SrtWalker().ValidateRuntimeValue(planA, &firstOther), "waterfall image table: non-implied condition was accepted by validator");
}
void verifySplitWaterfallImageTable() {
using namespace ShaderRecompiler;
namespace Abi = ImageTableAbi;
static TableTexture texture;
alignas(256) static std::array<std::uint32_t, 0x3000> root{};
static std::array<std::uint32_t, 4> output{};
const auto outputV = TableV(output.data(), 0u, static_cast<std::uint32_t>(sizeof(output)));
std::copy(outputV.begin(), outputV.end(), root.begin() + 4);
const auto descriptor = TableT(texture.bytes.data(), TableFormat8888UNorm, TableType2D);
std::copy(descriptor.begin(), descriptor.end(), root.begin() + 0x40u / 4u);
const auto base = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(root.data()));
const std::array<std::uint32_t, 2> userData{static_cast<std::uint32_t>(base), static_cast<std::uint32_t>(base >> 32u)};
const auto code = [](std::uint32_t addend) {
return std::vector<std::uint32_t>{0xf4080700u, 0xfa000010u, 0xf4080800u, 0xfa000020u, 0x7da80086u, 0x7e220500u, 0xd7460001u, 0x00010b00u | (addend << 18u), 0x7e260501u, 0x8f128511u,
0xf4080900u, 0x24000040u, 0xf4080a00u, 0x26000040u, 0xf09c0f08u, 0x01090000u, 0xe0700000u, 0x80070000u, 0xbf810000u};
};
const auto split = code(0x90u);
auto request = TableRequest(split, userData);
request.shader.codeAddress = 0x3a000u;
const auto plan = GetResourcePlan(request);
require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u, "split waterfall image table: the T# was not planned as a table");
const auto& column = *plan->descriptorSources.at(plan->info.images[0].source).tableColumn;
require(column.address && column.stride == 32u && column.offset == 0x40u && column.dwordCount == 8u && column.foldedCarry == 16u, "split waterfall image table: the column is wrong");
const auto compiled = CompileTable(request);
const auto& snapshot = compiled.capture->snapshot.tables.tables.at(0);
require((snapshot.codes[0] & Abi::PoisonFlag) == 0u && snapshot.words.at(snapshot.codes[0]).dwords == descriptor, "split waterfall image table: record 0 was not read whole");
for (const std::uint32_t addend : {0xa0u, 0x88u}) {
const auto otherCode = code(addend);
auto other = TableRequest(otherCode, userData);
other.shader.codeAddress = 0x3b000u + addend * 0x100u;
expectFailure([&] { static_cast<void>(GetResourcePlan(other)); }, "GetImageResource dword 0 is not a valid runtime value", "split waterfall image table: a second half outside the record was accepted");
}
}
void verifyDescriptorPhis() {
using namespace ShaderRecompiler;
constexpr std::uint32_t Format8888UNorm = 56;
@@ -7622,6 +7656,7 @@ int main(int argc, char** argv) {
verifyChainedTableOffset();
verifyPointerChainImageTable();
verifyWaterfallImageTable();
verifySplitWaterfallImageTable();
std::cout << "Image table keys, maps, records and registration passed\n";
return 0;
}
@@ -7685,6 +7720,7 @@ int main(int argc, char** argv) {
verifyChainedTableOffset();
verifyPointerChainImageTable();
verifyWaterfallImageTable();
verifySplitWaterfallImageTable();
verifyDescriptorPhis();
verifyUniformTableImage();
verifyLaneSelectedDescriptors();
@@ -143,6 +143,25 @@ void repackedFormatTests() {
Require(TextureComponentChannel(guest, VK_COMPONENT_SWIZZLE_A) == VK_COMPONENT_SWIZZLE_A && TextureComponentChannel(guest, VK_COMPONENT_SWIZZLE_ZERO) == VK_COMPONENT_SWIZZLE_ZERO, "format 43 keeps alpha and constant selectors");
}
void gcnBufferFormatTests() {
using ShaderRecompiler::IrBufferFormat;
const auto& formats = ShaderRecompiler::GcnBufferFormats();
const auto gcn = [&](std::uint32_t data, std::uint32_t number) { return static_cast<IrBufferFormat>(formats[(data << 3u) | number]); };
std::array<std::uint32_t, 78> seen{};
for (const auto format : formats) ++seen[format];
for (std::uint32_t format = 1; format < seen.size(); ++format) Require(seen[format] == 1u, "unified buffer format " + std::to_string(format) + " must come from exactly one GCN format pair");
Require(gcn(0u, 4u) == IrBufferFormat::Invalid && gcn(15u, 7u) == IrBufferFormat::Invalid, "GCN DATA_FORMAT 0 and 15 have no buffer form");
Require(gcn(1u, 7u) == IrBufferFormat::Invalid && gcn(4u, 0u) == IrBufferFormat::Invalid, "8-bit float and 32-bit UNORM have no buffer form");
Require(gcn(1u, 0u) == IrBufferFormat::Format8UNorm && gcn(2u, 7u) == IrBufferFormat::Format16Float, "8 and 16-bit pairs must map");
Require(gcn(4u, 4u) == IrBufferFormat::Format32UInt && gcn(4u, 7u) == IrBufferFormat::Format32Float, "32-bit pairs must map");
Require(gcn(6u, 7u) == IrBufferFormat::Format11_11_10Float && gcn(7u, 7u) == IrBufferFormat::Format10_11_11Float, "GCN 10_11_11 and 11_11_10 must map by bit layout");
Require(gcn(8u, 0u) == IrBufferFormat::Format2_10_10_10UNorm && gcn(9u, 0u) == IrBufferFormat::Format10_10_10_2UNorm, "GCN 10_10_10_2 and 2_10_10_10 must map by bit layout");
Require(gcn(10u, 0u) == IrBufferFormat::Format8_8_8_8UNorm && gcn(14u, 7u) == IrBufferFormat::Format32_32_32_32Float, "four-channel pairs must map");
const std::uint32_t word3 = 0x00000facu | (7u << 12u) | (14u << 15u) | (7u << 27u);
Require(ShaderRecompiler::GcnBufferWord3(16u << 16u, word3) == (0x00000facu | (77u << 12u) | (1u << 28u)), "a strided GCN V# must check its index range");
Require(ShaderRecompiler::GcnBufferWord3(0u, word3) == (0x00000facu | (77u << 12u) | (3u << 28u)), "a GCN V# without a stride must check its byte range");
}
void convertedDccClearTests() {
alignas(64) std::array<std::uint8_t, 16> keys{};
GuestTextureResource resource{};
@@ -268,4 +287,5 @@ void RunTextureFormatTests() {
convertedDccClearTests();
singleDccKeysTests();
packedFloatDccClearTests();
gcnBufferFormatTests();
}
@@ -386,6 +386,29 @@ void RunTextureTilingTests() {
Require(dense.pitchBytes == 1024 && CoveredMipBytes(TextureTileMode::kLinear, 4, dense) == std::vector<std::pair<std::uint64_t, std::uint64_t>>{{0, 3072}}, "linear rows without padding are not one range");
}
{
const auto* equation = FindTextureSwizzleEquation(XorSwizzleMode(TextureTileMode::kZ64KBX), 4u);
Require(equation != nullptr, "missing the 64KB Z_X swizzle equation of 4-byte elements");
for (const auto mask : equation->bits) Require((mask & 0x00f80f80u) == 0u, "a 64KB Z_X address bit of a 4-byte element reads x or y bits past one 128x128 tile");
GuestTextureResource plane{};
plane.width = 3840;
plane.height = 2160;
plane.mipCount = 1;
plane.tileMode = TextureTileMode::kZ64KBX;
plane.dimension = TextureDimension::k2D;
plane.format = 22;
const auto geometry = DescribeSurface(plane);
const auto& mip = geometry.mips.front();
const auto block = ThinBlockLayout(TextureTileMode::kZ64KBX, 4u);
Require(block[0] == 65536u && block[1] == 128u && block[2] == 128u, "a 64KB Z_X tile of 4-byte elements is not 128x128");
Require(!mip.tail && mip.tiledOffset == 0u && mip.blocksPerRow == 30u && geometry.guestBytes == 510ull * 65536ull, "a 3840x2160 D32 Z_X plane is not 30x17 tiles");
const std::array<std::pair<std::uint32_t, std::uint32_t>, 8> samples{{{0, 0}, {127, 127}, {128, 0}, {3839, 0}, {0, 128}, {1000, 700}, {2047, 1151}, {3839, 2159}}};
for (const auto& [x, y] : samples) {
const auto offset = mip.tiledOffset + (static_cast<std::uint64_t>(y / block[2]) * mip.blocksPerRow + x / block[1]) * block[0] + equationOffset(*equation, x, y, 0);
Require(offset / 65536u == static_cast<std::uint64_t>(y >> 7u) * 30u + (x >> 7u), "element (" + std::to_string(x) + ", " + std::to_string(y) + ") of a 3840x2160 D32 Z_X plane lies outside the 64 KiB unit of its 128x128 tile");
}
}
reject([] { ComputeSurfaceSize({}, 1); }, "empty mip chain");
reject([] { ComputeSurfaceSize(ComputeMipLayout(TextureTileMode::kLinear, 1, 4, 4, 1), 0); }, "zero array layers");
@@ -151,10 +151,25 @@ void run() {
const auto zeroStride = live.TryGet(101, input);
Require(VertexFetchOutOfRange(input[0]), "zero records did not select zero fill");
input[0].resource.fields[2] = 64;
Require(live.TryGet(101, input) == zeroStride && !VertexFetchOutOfRange(input[0]) && VertexBufferExtent(input[0]) == 64, "cached zero-stride layout froze the live zero-fill decision");
Require(live.TryGet(101, input) == zeroStride && !VertexFetchOutOfRange(input[0]) && VertexBufferExtent(input[0]) == 16, "cached zero-stride layout froze the live zero-fill decision");
input[0].resource.fields[2] = 0;
Require(live.TryGet(101, input) == zeroStride && VertexFetchOutOfRange(input[0]), "record count zero reused a live fetch");
input[0] = attribute();
input[0].resource.fields[1] = 0;
input[0].resource.fields[3] |= 3u << 28u;
input[0].resource.fields[2] = 16;
const auto wholeFetch = live.TryGet(105, input);
for (const auto records : {0u, 8u, 15u}) {
input[0].resource.fields[2] = records;
Require(live.TryGet(105, input) == wholeFetch && VertexFetchOutOfRange(input[0]), "a fetch that only partly fits was not read as zero");
}
for (const auto records : {16u, 17u, 0xffffffffu}) {
input[0].resource.fields[2] = records;
Require(live.TryGet(105, input) == wholeFetch && !VertexFetchOutOfRange(input[0]) && VertexBufferExtent(input[0]) == 16, "a fetch inside the byte range was read as zero or sized by NUM_RECORDS");
}
input[0].resource.fields[3] &= ~(3u << 28u);
Require(VertexFetchOutOfRange(input[0]), "OOB_SELECT 0 read a zero-stride fetch");
input[0] = attribute();
input[0].formatComponents = 1;
const auto narrow = live.TryGet(102, input);
input[0].formatComponents = 2;
@@ -12,6 +12,9 @@
#include <sys/mman.h>
#include <unistd.h>
#endif
#ifdef __GLIBC__
#include <malloc.h>
#endif
#include <array>
#include <cstdint>
#include <cstring>
@@ -171,6 +174,21 @@ void CheckSharedBlock() {
if (!Watched(outside, unwatched.size())) Require(StoredOver(outside, unwatched.size(), TrackerGeneration()), "an unwatched range reads as not stored over");
}
void CheckStoredOverUsesPageEvidence() {
constexpr std::size_t Page = 4096;
auto* memory = static_cast<std::uint8_t*>(AllocateWatched(2 * Block));
const auto base = reinterpret_cast<std::uint64_t>(memory);
std::memset(memory, 0x11, 2 * Block);
const auto before = CollectWritesUncached(base, 2 * Block);
Require(before != 0, "the stored-over page fixture was not collected");
static_cast<volatile std::uint8_t*>(memory)[0] = 0x22;
const auto after = CollectWritesUncached(base, Block);
Require(after > before, "the page-local CPU write was not collected");
Require(StoredOver(base, Page, before), "a collected CPU write on a page was not reported as stored over");
Require(!StoredOver(base + Page, Page, before), "a page-local CPU write made an untouched page in the same block count as stored over");
}
void CheckStoredAllSince() {
constexpr std::size_t bytes = 4 * Block;
auto* memory = static_cast<std::uint8_t*>(AllocateWatched(bytes));
@@ -214,6 +232,39 @@ void CheckStoredAllSince() {
MarkWritten(base, 64);
Require(StoredAllSince_nid_no_patch(base, 64, before), "a complete non-block-aligned range store is absent");
before = TrackerGeneration();
MarkPossiblyWritten(base + 2048, 16);
Require(StoredOver(base + 2048, 16, before), "a possible driver write did not invalidate its exact range");
Require(!StoredOver(base + 2064, 16, before), "a possible driver write invalidated an adjacent range");
Require(!UnchangedSince(base + 2048, 16, before), "a possible driver write left its exact range unchanged");
Require(UnchangedSince(base + 2064, 16, before), "a possible driver write changed an adjacent range");
Require(!StoredAllSince_nid_no_patch(base + 2048, 16, before), "a possible driver write proved complete byte coverage");
for (unsigned repeat = 0; repeat < 6; ++repeat) MarkPossiblyWritten(base + 2048, 16);
Require(StoredOver(base + 2048, 16, before), "repeated possible writes lost their overlap evidence");
Require(!StoredOver(base + 2064, 16, before), "repeated possible writes invalidated an adjacent range");
Require(!StoredAllSince_nid_no_patch(base + 2048, 16, before), "repeated possible writes proved complete byte coverage");
before = TrackerGeneration();
constexpr unsigned PossibleIntervals = 8;
auto beforeNewest = before;
for (unsigned interval = 0; interval < PossibleIntervals; ++interval) {
beforeNewest = TrackerGeneration();
MarkPossiblyWritten(base + 4096 + static_cast<std::uint64_t>(interval) * 32, 16);
}
Require(StoredOver(base + 4096, 16, before), "an evicted possible-write interval was forgotten instead of marking its block");
Require(StoredOver(base + 4096 + 16, 16, before), "an evicted possible write did not mark the rest of its block");
Require(StoredOver(base + 4096 + (PossibleIntervals - 1) * 32, 16, beforeNewest), "the newest possible-write interval was evicted");
const auto beforeRecord = TrackerGeneration();
MarkPossiblyWritten(base + 4096, 16);
Require(StoredOver(base + 4096, 16, beforeRecord), "a possible-write interval was not retained after being re-recorded");
Require(!StoredAllSince_nid_no_patch(base + 4096, 16, before), "possible-write eviction or re-recording proved complete byte coverage");
before = TrackerGeneration();
MarkWritten(base + 2560, 16);
MarkPossiblyWritten(base, Block);
Require(StoredAllSince_nid_no_patch(base + 2560, 16, before), "a possible whole-block range erased an exact partial-store proof");
Require(!StoredAllSince_nid_no_patch(base, Block, before), "a possible whole-block range proved complete block coverage");
before = TrackerGeneration();
MarkWritten(base + 512, 16);
MarkWritten(base + 544, 16);
@@ -345,6 +396,42 @@ void CheckSharedBlockProofs() {
Require(!UnchangedSince(base + Block + 14 * page + 0x800, 16, beforeNeighbour), "a range in the page an own store dirtied reads as unchanged");
}
void CheckRepeatedPossibleStores() {
void* memory = AllocateWatched(3 * Block);
const auto base = reinterpret_cast<std::uint64_t>(memory);
const auto boundary = base + 2 * Block - 0x9000;
std::memset(memory, 0x11, 3 * Block);
const auto synced = CollectWrites(base, 3 * Block);
Require(synced != 0, "the test block is not collected");
const auto exact = TrackerGeneration();
MarkWritten(base + Block, 0x100);
for (std::uint64_t store = 0; store < 5; ++store) MarkPossiblyWritten(boundary + store * 0x1000, 0x100);
Require(StoredAllSince_nid_no_patch(base + Block, 0x100, exact), "possible writes beyond the ring forgot an exact store's coverage");
void* fresh = AllocateWatched(2 * Block);
const auto freshBase = reinterpret_cast<std::uint64_t>(fresh);
std::memset(fresh, 0x11, 2 * Block);
const auto freshSynced = CollectWrites(freshBase, 2 * Block);
const auto neighbour = freshBase + Block - 0x9000;
MarkPossiblyWritten(neighbour, 0x100);
const auto secondStored = MarkPossiblyWritten(neighbour + 0x1000, 0x100);
const auto thirdStored = MarkPossiblyWritten(neighbour + 0x2000, 0x100);
MarkPossiblyWritten(neighbour + 0x3000, 0x100);
const auto settled = TrackerGeneration();
MarkPossiblyWritten(neighbour, 0x100);
MarkPossiblyWritten(neighbour, 0x100);
MarkPossiblyWritten(neighbour + 0x5000, 0x100);
Require(!StoredOver(freshBase, static_cast<std::size_t>(neighbour - freshBase), secondStored), "a full possible ring evicted a newer piece instead of its oldest");
Require(StoredOver(freshBase, static_cast<std::size_t>(neighbour - freshBase), freshSynced), "an evicted possible write is not taken as a store over the rest of the block");
const auto partial = TrackerGeneration();
MarkPossiblyWritten(neighbour + 0x5080, 0x100);
Require(StoredOver(neighbour + 0x5000, 0x10, settled) && StoredOver(neighbour + 0x5100, 0x10, partial), "a possible write that overlaps an earlier piece without containing it replaced it");
Require(!StoredOver(freshBase, static_cast<std::size_t>(neighbour - freshBase), thirdStored), "a possible write that overlaps without containing evicted a newer piece instead of the oldest");
}
void CheckOwnStore() {
void* memory = AllocateWatched(2 * Block);
const auto base = reinterpret_cast<std::uint64_t>(memory);
@@ -1153,6 +1240,38 @@ void CheckImportWindowOverUnwatchedBlock() {
Require(after != 0 && !UnchangedSince(base, Block, after), "a store after an import window over an unwatched block was missed");
}
void CheckImportWindowFootprint() {
#ifdef __GLIBC__
constexpr std::size_t bytes = std::size_t{64} << 20;
void* memory = AllocateWatched(bytes);
const auto base = reinterpret_cast<std::uint64_t>(memory);
std::memset(memory, 0x5a, bytes);
Require(CollectWritesUncached(base, bytes) != 0, "the footprint import window block is not collected");
const auto heapBytes = [] {
const auto info = mallinfo2();
return info.uordblks + info.hblkhd;
};
const auto before = heapBytes();
std::size_t during = 0;
Require(ImportWatched(base, bytes, [&] {
during = heapBytes();
return true;
}), "the footprint import window import failed");
Require(Watched(base, bytes), "the footprint import window left its range unwatched");
const auto held = during > before ? during - before : 0;
Require(held <= bytes / 64, "an import window over " + std::to_string(bytes >> 20) + " MiB held " + std::to_string(held >> 20) + " MiB of host memory during the import: it copies the range instead of hashing its pages");
auto* plain = static_cast<volatile std::uint8_t*>(memory);
const auto synced = CollectWritesUncached(base, bytes);
Require(ImportWatched(base, bytes, [&] {
plain[0x100] = 0x5a;
plain[Block + 0x100] = 0x5b;
return true;
}), "the footprint import window re-import failed");
Require(UnchangedSince(base, Block, synced), "a page a hashed import window resolved without changing its bytes reads as written");
Require(WrittenSince(base + Block, Block, synced), "a store of new bytes within a hashed import window was missed");
#endif
}
void ProtectPage(std::uint64_t address, bool readable) {
#ifdef _WIN32
DWORD previous = 0;
@@ -1415,8 +1534,10 @@ int main() {
}
#endif
CheckSharedBlock();
CheckStoredOverUsesPageEvidence();
CheckStoredAllSince();
CheckSharedBlockProofs();
CheckRepeatedPossibleStores();
CheckOwnStore();
CheckUnwatch();
CheckNewestStamp();
@@ -1442,6 +1563,7 @@ int main() {
CheckImportWindowOtherThread();
CheckImportWindowOverUnwatchedBlock();
CheckStoreAcrossImports();
CheckImportWindowFootprint();
CheckUnwatchedImport();
CheckCommittedRanges();
CheckAdoptLatestEpoch();
@@ -68,6 +68,8 @@ alignas(256) std::array<std::uint32_t, 16> PixelCode{};
alignas(256) std::array<std::uint32_t, Threads> Output{};
void SampleSliceZero(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode);
void OverlappingArrays(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode, bool cpuWrite = false);
void OverlappingArrayLastSlice(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode);
std::uint64_t AddressOf(const void* data) {
return static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(data));
@@ -81,22 +83,24 @@ std::string Hex(std::uint32_t value) {
class GuestBlock {
public:
explicit GuestBlock(std::size_t registeredBytes = SurfaceBytes, std::size_t registeredOffset = 0) : registeredOffset(registeredOffset) {
explicit GuestBlock(std::size_t registeredBytes = SurfaceBytes, std::size_t registeredOffset = 0, bool commitAll = true) : registeredOffset(registeredOffset) {
#ifdef _WIN32
block = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, LayerBytes));
if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, SurfaceBytes, PAGE_READWRITE, SurfaceBytes);
if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block + (commitAll ? 0 : registeredOffset), commitAll ? SurfaceBytes : registeredBytes, PAGE_READWRITE, commitAll ? SurfaceBytes : registeredBytes);
#else
void* mapped = mmap(nullptr, SurfaceBytes + LayerBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
void* mapped = mmap(nullptr, SurfaceBytes + LayerBytes, commitAll ? PROT_READ | PROT_WRITE : PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (mapped != MAP_FAILED) {
const auto begin = reinterpret_cast<std::uintptr_t>(mapped);
const auto aligned = (begin + LayerBytes - 1) & ~(static_cast<std::uintptr_t>(LayerBytes) - 1);
if (aligned != begin) munmap(mapped, aligned - begin);
if (aligned + SurfaceBytes != begin + SurfaceBytes + LayerBytes) munmap(reinterpret_cast<void*>(aligned + SurfaceBytes), begin + LayerBytes - aligned);
block = reinterpret_cast<std::uint8_t*>(aligned);
if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, SurfaceBytes);
if (!commitAll) Require(mprotect(block + registeredOffset, registeredBytes, PROT_READ | PROT_WRITE) == 0, "cannot commit the active array slice");
if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block + (commitAll ? 0 : registeredOffset), commitAll ? SurfaceBytes : registeredBytes);
}
#endif
Require(block != nullptr, "array layer target: cannot allocate the guest block");
if (!commitAll) GuestAllocations::GuestAllocationsInvalidate_nid_postfix(reinterpret_cast<std::uintptr_t>(block), SurfaceBytes);
GuestAllocations::Mutation().Add(block + registeredOffset, registeredBytes, true, true, true);
}
@@ -170,10 +174,10 @@ void Load(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code,
Dispatch(device, code, userData);
}
void LoadLayer(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t layer, std::uint32_t swizzleMode) {
void LoadLayer(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t layer, std::uint32_t swizzleMode, std::uint32_t lastLayer = Layers - 1u) {
auto& code = LoadCodes[layer];
code = {0x7e020280, 0x7e040280u | layer, 0xf0001128, 0x00010300, 0x34080082, 0xbf8c3f70, 0xe0701000, 0x80000304, 0xbf810000};
Load(device, code, SurfaceDescriptor(surface, swizzleMode, Type2DArray, Layers - 1u));
Load(device, code, SurfaceDescriptor(surface, swizzleMode, Type2DArray, lastLayer));
}
void Sample2D(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t swizzleMode) {
@@ -187,23 +191,23 @@ void RequireOutput(std::uint32_t inside, std::uint32_t outside, const std::strin
}
}
AgcDriver::Graphics::ColorTarget DecodeTarget(std::uint64_t address, std::uint32_t slice, std::uint32_t swizzleMode, bool collapsed) {
AgcDriver::Graphics::ColorTarget DecodeTarget(std::uint64_t address, std::uint32_t slice, std::uint32_t swizzleMode, bool collapsed, std::uint32_t lastSlice) {
AgcDriver::Registers cx;
cx[0x318] = static_cast<std::uint32_t>(address >> 8u);
cx[0x31b] = slice | (slice << 13u);
cx[0x31c] = Color32FloatInfo;
cx[0x31d] = 0;
cx[0x3b0] = ((Side - 1u) << 14u) | (Side - 1u);
cx[0x3b8] = (TiledAttrib3 & ~(0x1fu << 14u)) | (swizzleMode << 14u) | (collapsed ? 0u : Layers - 1u);
cx[0x3b8] = (TiledAttrib3 & ~(0x1fu << 14u)) | (swizzleMode << 14u) | (collapsed ? 0u : lastSlice);
cx[0x390] = static_cast<std::uint32_t>(address >> 40u);
const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0);
Require(color.format == VK_FORMAT_R32_SFLOAT && color.bytes == LayerBytes && color.address == address + slice * LayerBytes, "the COLOR_32 float view did not decode as a 128x128 R32 slice of the array");
if (collapsed) Require(color.arrayAddress == 0 && color.arrayLastSlice == 0, "a collapsed slice view unexpectedly describes a whole array");
else Require(color.arrayAddress == address && color.arrayLastSlice == Layers - 1u && color.arraySlice == slice, "the color view did not keep its array and slice");
else Require(color.arrayAddress == address && color.arrayLastSlice == lastSlice && color.arraySlice == slice, "the color view did not keep its array and slice");
return color;
}
void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t slice, std::uint32_t swizzleMode, bool collapsed) {
void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t slice, std::uint32_t swizzleMode, bool collapsed, std::uint32_t lastSlice = Layers - 1u, std::uint32_t width = ScissorWidth) {
const auto target = device.Target();
constexpr std::uint32_t waveSize = 64;
std::size_t at = 0;
@@ -230,7 +234,7 @@ void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint
const auto vertexResult = ShaderRecompiler::Recompile(vertex);
const auto vertexPush = static_cast<std::uint32_t>(vertexResult.pushConstants.size());
const auto color = DecodeTarget(surface, slice, swizzleMode, collapsed);
const auto color = DecodeTarget(surface, slice, swizzleMode, collapsed, lastSlice);
VkPipelineColorBlendAttachmentState blend{};
blend.colorWriteMask = 0xf;
AgcDriver::Graphics::State state{};
@@ -242,7 +246,7 @@ void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
state.viewport = {0, static_cast<float>(Side), static_cast<float>(Side), -static_cast<float>(Side), 0, 1};
state.negativeOneToOne = false;
state.scissor = {{0, 0}, {ScissorWidth, Side}};
state.scissor = {{0, 0}, {width, Side}};
state.cullMode = VK_CULL_MODE_NONE;
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
state.blends = {blend};
@@ -296,6 +300,9 @@ void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t sw
RequireOutput(drawn, Initial, "slice " + std::to_string(slice) + " uploaded again from guest memory");
AgcDriver::Graphics::ClearCachedTextures(device.Device());
SampleSliceZero(device, texels, swizzleMode);
OverlappingArrays(device, texels, swizzleMode);
OverlappingArrays(device, texels, swizzleMode, true);
OverlappingArrayLastSlice(device, texels, swizzleMode);
}
void SampleSliceZero(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode) {
@@ -315,6 +322,32 @@ void SampleSliceZero(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std:
AgcDriver::Graphics::ClearCachedTextures(device.Device());
}
void OverlappingArrayLastSlice(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode) {
const auto neighbourAddress = AddressOf(texels);
const auto surface = neighbourAddress + LayerBytes;
const auto cpuValue = std::bit_cast<std::uint32_t>(0.125f);
const auto drawn = std::bit_cast<std::uint32_t>(Drawn);
std::fill_n(reinterpret_cast<std::uint32_t*>(texels), SurfaceBytes / sizeof(std::uint32_t), Initial);
AgcDriver::GuestMemory::CollectWrites(neighbourAddress, SurfaceBytes);
AgcDriver::GuestMemory::BumpCollectEpoch();
DrawSlice(device, surface, 1u, swizzleMode, false, 1u);
DrawSlice(device, neighbourAddress, 1u, swizzleMode, false, 1u, Side);
const auto neighbour = StorageTexture::FindPending(neighbourAddress, 2u * LayerBytes);
Require(neighbour != nullptr && neighbour->Descriptor().baseAddress == neighbourAddress, "the preceding target has no pending results");
std::fill_n(reinterpret_cast<std::uint32_t*>(texels + 2u * LayerBytes), LayerBytes / sizeof(std::uint32_t), cpuValue);
AgcDriver::GuestMemory::CollectWrites(surface + LayerBytes, LayerBytes);
AgcDriver::GuestMemory::BumpCollectEpoch();
DrawSlice(device, surface, 1u, swizzleMode, false, 1u);
Require(StorageTexture::FindPending(neighbourAddress, 2u * LayerBytes) == neighbour, "CPU refresh of the last slice stored the preceding target's results");
LoadLayer(device, surface, 1u, swizzleMode, 1u);
RequireOutput(drawn, cpuValue, "the refreshed last slice keeps the CPU's untouched pixels");
LoadLayer(device, surface, 0u, swizzleMode, 1u);
RequireOutput(drawn, drawn, "the first slice sees the preceding target's pending results");
StorageTexture::FlushPending(neighbourAddress, SurfaceBytes, nullptr, "test");
device.WaitIdle();
AgcDriver::Graphics::ClearCachedTextures(device.Device());
}
void UnregisteredUnusedLayer(AgcDriver::VulkanDevice& device, std::uint32_t slice) {
GuestBlock block(LayerBytes, slice * LayerBytes);
auto* texels = block.Data();
@@ -354,6 +387,75 @@ void RetiredArrayLayer(AgcDriver::VulkanDevice& device) {
Require(untouched, "CPU write-back stored results over a retired allocation");
}
void UncommittedUnusedLayers(AgcDriver::VulkanDevice& device, std::uint32_t slice) {
if (!AgcDriver::GuestMemory::WriteWatched()) return;
GuestBlock block(LayerBytes, slice * LayerBytes, false);
const auto surface = AddressOf(block.Data());
const auto active = surface + slice * LayerBytes;
auto* texels = reinterpret_cast<std::uint32_t*>(active);
std::fill_n(texels, LayerBytes / sizeof(std::uint32_t), Initial);
Require(AgcDriver::GuestMemory::CollectWrites(active, LayerBytes) != 0, "the committed array slice has no write history");
Require(AgcDriver::GuestMemory::CollectWritesUncached(surface, SurfaceBytes) == 0, "the uncommitted array fixture unexpectedly has complete backend write history");
Require(AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes) == 0, "the unused array slices unexpectedly have complete write history");
AgcDriver::GuestMemory::BumpCollectEpoch();
DrawSlice(device, surface, slice, TileR64KBX, false);
const auto pending = StorageTexture::FindPending(surface, SurfaceBytes);
Require(pending != nullptr, "the committed array slice has no pending draw results");
Require(pending->GenerationFor(active, LayerBytes) != 0, "an uncommitted unused slice erased the active slice's write history");
const auto version = pending->Version();
for (unsigned repeat = 0; repeat < 3; ++repeat) {
AgcDriver::GuestMemory::BumpCollectEpoch();
Require(pending->RefreshLayers(slice, 1), "an unchanged committed slice was uploaded again");
Require(pending->GenerationFor(active, LayerBytes) != 0 && pending->Version() == version, "repeated refresh erased the committed slice's write history");
Require(StorageTexture::FindPending(surface, SurfaceBytes) == pending, "refresh settled unchanged GPU results for an uncommitted unused slice");
}
const auto cpu = std::bit_cast<std::uint32_t>(0.125f);
std::fill_n(texels, LayerBytes / sizeof(std::uint32_t), cpu);
AgcDriver::GuestMemory::BumpCollectEpoch();
Require(!pending->RefreshLayers(slice, 1), "the active array slice ignored a genuine CPU write");
LoadLayer(device, surface, slice, TileR64KBX);
RequireOutput(cpu, cpu, "a committed array slice after a genuine CPU write");
DrawSlice(device, surface, slice, TileR64KBX, false);
StorageTexture::FlushPending(active, LayerBytes, nullptr, "test");
device.WaitIdle();
Require(std::count(texels, texels + LayerBytes / sizeof(std::uint32_t), std::bit_cast<std::uint32_t>(Drawn)) == ScissorWidth * Side, "write-back lost results in the committed array slice");
AgcDriver::Graphics::ClearCachedTextures(device.Device());
}
void OverlappingArrays(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode, bool cpuWrite) {
const auto surface = AddressOf(texels);
const auto next = surface + LayerBytes;
const auto drawn = std::bit_cast<std::uint32_t>(Drawn);
std::fill_n(reinterpret_cast<std::uint32_t*>(texels), SurfaceBytes / 4u, Initial);
AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes);
AgcDriver::GuestMemory::BumpCollectEpoch();
DrawSlice(device, surface, 0, swizzleMode, false, 1u);
DrawSlice(device, next, 0, swizzleMode, false, 1u, Side);
const auto neighbour = StorageTexture::FindPending(next, 2u * LayerBytes);
Require(neighbour != nullptr && neighbour->Descriptor().baseAddress == next && neighbour->Descriptor().dimension == TextureDimension::k2DArray, "the next target did not render into the image of its own array");
if (cpuWrite) {
std::fill_n(reinterpret_cast<std::uint32_t*>(texels), LayerBytes / sizeof(std::uint32_t), std::bit_cast<std::uint32_t>(0.125f));
AgcDriver::GuestMemory::CollectWrites(surface, LayerBytes);
AgcDriver::GuestMemory::BumpCollectEpoch();
}
DrawSlice(device, surface, 0, swizzleMode, false, 1u);
Require(StorageTexture::FindPending(next, 2u * LayerBytes) == neighbour, "a draw into slice 0 of the first array stored the next target's results, which only its unused second slice overlaps");
Sample2D(device, surface, swizzleMode);
RequireOutput(drawn, cpuWrite ? std::bit_cast<std::uint32_t>(0.125f) : Initial, "slice 0 of the first array sampled as a 2D texture after both draws");
Require(StorageTexture::FindPending(next, 2u * LayerBytes) == neighbour, "a 2D texture over slice 0 of the first array stored the next target's results, which only its unused second slice overlaps");
LoadLayer(device, next, 0, swizzleMode, 1u);
RequireOutput(drawn, drawn, "slice 0 of the next array after the draw into the first");
LoadLayer(device, surface, 1, swizzleMode, 1u);
RequireOutput(drawn, drawn, "slice 1 of the first array (the next target's slice 0) read while the next target's results were pending");
LoadLayer(device, surface, 0, swizzleMode, 1u);
RequireOutput(drawn, cpuWrite ? std::bit_cast<std::uint32_t>(0.125f) : Initial, "slice 0 of the first array after both draws");
LoadLayer(device, next, 1, swizzleMode, 1u);
RequireOutput(Initial, Initial, "slice 1 of the next array, which nothing drew");
StorageTexture::FlushPending(surface, SurfaceBytes, nullptr, "test");
device.WaitIdle();
AgcDriver::Graphics::ClearCachedTextures(device.Device());
}
}
int main() {
@@ -367,6 +469,8 @@ int main() {
UnregisteredUnusedLayer(*device, 0u);
UnregisteredUnusedLayer(*device, Layers - 1u);
RetiredArrayLayer(*device);
UncommittedUnusedLayers(*device, 0u);
UncommittedUnusedLayers(*device, Layers - 1u);
std::puts("array layer target tests passed");
return 0;
} catch (const std::exception& error) {
@@ -0,0 +1,234 @@
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp"
#include "prx/libc/include/GuestAllocations.hpp"
#include "prx/libc/include/GuestArena.hpp"
#include "prx/libc/include/GuestWriteWatch.hpp"
#include "DepthFastClearHarness.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 <cstddef>
#include <cstdint>
#include <cstdio>
#include <iostream>
#include <span>
#include <string>
#include <utility>
#include <vector>
namespace AgcDriver {
struct VulkanDeviceTestAccess {
static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); }
};
}
namespace {
using AgcDriver::Graphics::DepthPlaneStoreStatistics;
using DepthFastClearHarness::Compile;
using DepthFastClearHarness::Draw;
using DepthFastClearHarness::DrawOptions;
using DepthFastClearHarness::Require;
using DepthFastClearHarness::Surface;
constexpr std::uint32_t PlaneWidth = 384;
constexpr std::uint32_t PlaneHeight = 200;
constexpr VkExtent2D PlaneExtent{PlaneWidth, PlaneHeight};
constexpr std::size_t Block = 65536;
constexpr std::size_t Units = 6;
constexpr std::size_t PlaneBytes = Units * Block;
constexpr std::size_t UnitTexels = Block / sizeof(float);
constexpr std::size_t EdgeTexels = 128u * (PlaneHeight - 128u);
constexpr std::size_t PlaneTexels = static_cast<std::size_t>(PlaneWidth) * PlaneHeight;
constexpr VkExtent2D SmallExtent{64, 32};
constexpr std::size_t SmallColorBytes = static_cast<std::size_t>(SmallExtent.width) * SmallExtent.height * 4u;
constexpr std::uint64_t ReadBefore = 4096;
constexpr std::size_t ReadBytes = 7u * 1024u;
alignas(256) std::array<std::byte, PlaneTexels * 4u> Colors{};
void* AllocateBlocks(std::size_t bytes, bool watched) {
#ifdef _WIN32
void* block = nullptr;
if (watched) {
block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block);
if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes);
} else {
block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
}
Require(block != nullptr, "cannot allocate the depth plane blocks");
#else
void* raw = mmap(nullptr, bytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
Require(raw != MAP_FAILED, "cannot map the depth plane blocks");
const auto begin = reinterpret_cast<std::uintptr_t>(raw);
const auto aligned = (begin + Block - 1) & ~static_cast<std::uintptr_t>(Block - 1);
if (aligned != begin) munmap(raw, aligned - begin);
if (aligned + bytes != begin + bytes + Block) munmap(reinterpret_cast<void*>(aligned + bytes), begin + Block - aligned);
void* block = reinterpret_cast<void*>(aligned);
if (watched) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes);
#endif
GuestAllocations::Mutation().Add(block, bytes, true, true, true);
return block;
}
void Fill(float* plane, float value) {
std::fill(plane, plane + PlaneBytes / sizeof(float), value);
AgcDriver::GuestMemory::BumpCollectEpoch();
}
void FillUnit(float* plane, std::size_t unit, float value) {
std::fill(plane + unit * UnitTexels, plane + (unit + 1u) * UnitTexels, value);
AgcDriver::GuestMemory::BumpCollectEpoch();
}
std::size_t Texels(std::size_t unit) {
return unit < 3u ? UnitTexels : EdgeTexels;
}
void RequireUnit(const float* plane, std::size_t unit, float depth, std::size_t count, const std::string& what) {
const auto* first = plane + unit * UnitTexels;
const auto held = static_cast<std::size_t>(std::count(first, first + UnitTexels, depth));
Require(held == count, what + ": unit " + std::to_string(unit) + " holds " + std::to_string(held) + " texels of " + std::to_string(depth) + " instead of " + std::to_string(count));
}
DepthPlaneStoreStatistics Since(const DepthPlaneStoreStatistics& before) {
const auto now = AgcDriver::Graphics::DepthPlaneStoreCounts();
DepthPlaneStoreStatistics difference;
difference.stores = now.stores - before.stores;
difference.units = now.units - before.units;
difference.bytes = now.bytes - before.bytes;
return difference;
}
std::vector<std::pair<std::uint64_t, std::uint64_t>> ReadStart(AgcDriver::VulkanDevice& device, std::uint64_t base) {
auto stored = AgcDriver::Graphics::FlushDepthPlanes(base - ReadBefore, ReadBytes);
device.WaitIdle();
return stored;
}
}
int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
Require(AgcDriver::Graphics::DepthSliceBytes(PlaneExtent, 4u) == PlaneBytes, "the 384x200 depth plane is not six 64 KiB tiles");
auto* blocks = static_cast<std::byte*>(AllocateBlocks(Block + PlaneBytes, true));
auto* plane = reinterpret_cast<float*>(blocks + Block);
const auto base = reinterpret_cast<std::uintptr_t>(plane);
const auto shaders = Compile(*device);
const auto swizzle = AgcDriver::Graphics::XorSwizzleMode(AgcDriver::Graphics::TextureTileMode::kZ64KBX);
const Surface depth{base, 0, 0, false, VK_FORMAT_D32_SFLOAT, 0, 0, swizzle};
const auto draw = [&](const Surface& surface, const DrawOptions& options) { return Draw(*device, shaders, surface, options, PlaneExtent, Colors); };
const DrawOptions writeHalf{.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS};
Fill(plane, 1.0f);
Require(draw(depth, writeHalf) == PlaneTexels, "the draw writing depth 0.5 did not cover the plane");
Require(AgcDriver::Graphics::DepthPlanesStale(), "the draw writing depth left the plane unregistered");
auto before = AgcDriver::Graphics::DepthPlaneStoreCounts();
const auto first = ReadStart(*device, base);
auto stored = Since(before);
Require(first.size() == 1 && first[0].first == base && first[0].second == Block, "a read ending 3 KiB into the stale plane did not store exactly its first 64 KiB unit");
Require(stored.stores == 1 && stored.units == 1 && stored.bytes == Block, "a read ending 3 KiB into the stale plane did not count one store of one unit");
RequireUnit(plane, 0, 0.5f, UnitTexels, "after a read of the first unit");
for (std::size_t unit = 1; unit < Units; ++unit) RequireUnit(plane, unit, 1.0f, UnitTexels, "after a read of the first unit, a unit it does not overlap");
Require(AgcDriver::Graphics::DepthPlanesStale(), "a store of one unit left no stale plane");
before = AgcDriver::Graphics::DepthPlaneStoreCounts();
Require(ReadStart(*device, base).empty(), "the same read again stored the first unit again");
stored = Since(before);
Require(stored.stores == 0 && stored.units == 0 && stored.bytes == 0, "the same read again counted a store");
before = AgcDriver::Graphics::DepthPlaneStoreCounts();
const auto rest = AgcDriver::Graphics::FlushDepthPlanes(base, PlaneBytes);
device->WaitIdle();
stored = Since(before);
Require(rest.size() == 1 && rest[0].first == base + Block && rest[0].second == PlaneBytes - Block, "a read of the whole plane did not store the five units still stale");
Require(stored.stores == 1 && stored.units == Units - 1u && stored.bytes == PlaneBytes - Block, "a read of the whole plane did not count one store of five units");
for (std::size_t unit = 0; unit < Units; ++unit) RequireUnit(plane, unit, 0.5f, Texels(unit), "after a read of the whole plane");
for (std::size_t unit = 3; unit < Units; ++unit) RequireUnit(plane, unit, 1.0f, UnitTexels - EdgeTexels, "after a read of the whole plane, the padding below the last row");
Require(!AgcDriver::Graphics::DepthPlanesStale(), "a read of the whole plane left it stale");
if (!AgcDriver::GuestMemory::WriteWatched() || !AgcDriver::GuestMemory::Watched(base, PlaneBytes)) {
std::puts("guest memory is not write-watched here: guest writes into a partly stored plane are not exercised");
} else {
Fill(plane, 1.0f);
Require(draw(depth, writeHalf) == PlaneTexels, "the draw writing depth 0.5 over the reloaded plane did not cover it");
Require(!ReadStart(*device, base).empty(), "a read of the first unit of the drawn plane stored nothing");
FillUnit(plane, 4, 0.75f);
draw(depth, {.depthTest = false});
before = AgcDriver::Graphics::DepthPlaneStoreCounts();
Require(!AgcDriver::Graphics::FlushDepthPlanes(base, PlaneBytes).empty(), "a read of the whole plane after a guest write into unit 4 stored nothing");
device->WaitIdle();
stored = Since(before);
Require(stored.units == 4 && stored.bytes == 4u * Block, "a read of the whole plane after a guest write into unit 4 did not store the four units still stale");
for (const std::size_t unit : {0u, 1u, 2u, 3u, 5u}) RequireUnit(plane, unit, 0.5f, Texels(unit), "a guest write into unit 4 after unit 0 was stored, then a read of the whole plane");
RequireUnit(plane, 4, 0.75f, UnitTexels, "a guest write into unit 4 after unit 0 was stored, then a read of the whole plane, the written unit");
Require(draw(depth, {.minDepth = 0.6f, .maxDepth = 0.6f}) == EdgeTexels, "a draw at depth 0.6 did not pass LESS exactly over the unit the guest wrote 0.75 into");
Fill(plane, 1.0f);
Require(draw(depth, writeHalf) == PlaneTexels, "the draw writing depth 0.5 before a guest write and an unmatched view did not cover the plane");
Require(!ReadStart(*device, base).empty(), "a read of the first unit before a guest write and an unmatched view stored nothing");
FillUnit(plane, 2, 0.75f);
const std::array<std::uint32_t, 8> words{0, 0, 0, 0, PlaneWidth, PlaneHeight, 1, 0};
AgcDriver::Graphics::GuestTextureResource levels{};
levels.baseAddress = base;
levels.width = PlaneWidth;
levels.height = PlaneHeight;
levels.mipCount = 2;
levels.lastLevel = 1;
levels.dimension = AgcDriver::Graphics::TextureDimension::k2D;
levels.tileMode = AgcDriver::Graphics::TextureTileMode::kZ64KBX;
levels.format = 22;
Require(AgcDriver::Graphics::DepthSurfaceTexture(AgcDriver::VulkanDeviceTestAccess::ContextOf(*device), words, levels, {}) == nullptr, "a view with two levels of a plane the guest wrote was served by the depth surface");
device->WaitIdle();
Require(!AgcDriver::Graphics::DepthSurfaceAt(base), "a view with two levels of a plane the guest wrote left the depth surface live");
for (const std::size_t unit : {0u, 1u, 3u, 4u, 5u}) RequireUnit(plane, unit, 0.5f, Texels(unit), "the retire of a partly stored plane the guest wrote into");
RequireUnit(plane, 2, 0.75f, UnitTexels, "the retire of a partly stored plane the guest wrote into, the written unit");
}
Fill(plane, 1.0f);
Require(draw(depth, writeHalf) == PlaneTexels, "the draw writing depth 0.5 under a later color target did not cover the plane");
Require(!ReadStart(*device, base).empty(), "a read of the first unit before a color target stored nothing");
auto* smallPlane = AllocateBlocks(Block, true);
const Surface smallDepth{reinterpret_cast<std::uintptr_t>(smallPlane), 0, 0, false, VK_FORMAT_D32_SFLOAT, 0, 0, swizzle};
const std::span<std::byte> overLastUnit(reinterpret_cast<std::byte*>(plane + 5u * UnitTexels), SmallColorBytes);
Draw(*device, shaders, smallDepth, {.depthTest = false}, SmallExtent, overLastUnit);
Require(!AgcDriver::Graphics::DepthSurfaceAt(base), "a color target over the last unit left the depth surface live");
for (std::size_t unit = 0; unit < 5u; ++unit) RequireUnit(plane, unit, 0.5f, Texels(unit), "a color target over the last unit of a partly stored plane");
auto* looseBlocks = static_cast<std::byte*>(AllocateBlocks(Block + PlaneBytes, false));
auto* loose = reinterpret_cast<float*>(looseBlocks + Block);
const auto looseBase = reinterpret_cast<std::uintptr_t>(loose);
if (AgcDriver::GuestMemory::Watched(looseBase, PlaneBytes)) {
std::puts("memory outside the guest arena is write-watched here: a partial store over unwatched memory is not exercised");
} else {
const Surface unwatched{looseBase, 0, 0, false, VK_FORMAT_D32_SFLOAT, 0, 0, swizzle};
Fill(loose, 1.0f);
Require(draw(unwatched, writeHalf) == PlaneTexels, "the draw writing depth 0.5 into an unwatched plane did not cover it");
before = AgcDriver::Graphics::DepthPlaneStoreCounts();
Require(ReadStart(*device, looseBase).size() == 1, "a read of the first unit of an unwatched plane did not store it");
stored = Since(before);
Require(stored.units == 1 && stored.bytes == Block, "a read of the first unit of an unwatched plane did not count one unit");
RequireUnit(loose, 0, 0.5f, UnitTexels, "unwatched plane after a read of the first unit");
for (std::size_t unit = 1; unit < Units; ++unit) RequireUnit(loose, unit, 1.0f, UnitTexels, "unwatched plane after a read of the first unit, a unit it does not overlap");
Require(draw(unwatched, {.minDepth = 0.6f, .maxDepth = 0.6f}) == 0u, "a draw at depth 0.6 passed LESS over unwatched units still holding depth 0.5 on the GPU");
}
std::puts("depth plane partial store tests passed");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
@@ -27,6 +27,7 @@ constexpr std::uint32_t Side = 2;
constexpr std::uint32_t Format8888UNorm = 56;
constexpr std::uint32_t Format8UNorm = 1;
constexpr std::uint32_t Format8_8UNorm = 14;
constexpr std::uint32_t Format16UNorm = 7;
constexpr std::uint32_t Format1616UNorm = 23;
constexpr std::uint32_t Format9995Float = 132;
constexpr std::uint32_t Format8Srgb = 128;
@@ -178,45 +179,6 @@ void FillInput() {
Extra[0] = 0x00001f20u;
}
std::int32_t OffsetComponent(std::uint32_t tid, std::uint32_t component) {
const auto field = (Extra[tid * 3u] >> (component * 8u)) & 0x3fu;
return static_cast<std::int32_t>(field ^ 0x20u) - 0x20;
}
float ReferenceTexel(int x, int y, float reference, const Sampler& sampler) {
const std::uint32_t clamp = sampler.clamp;
const int size = static_cast<int>(Side);
if (clamp == ClampBorder && (x < 0 || x >= size || y < 0 || y >= size)) {
const float border = sampler.border == BorderWhite ? 1.0f : 0.0f;
return reference <= border ? 1.0f : 0.0f;
}
const auto address = [&](int value) {
if (clamp == ClampEdge) return std::clamp(value, 0, static_cast<int>(Side) - 1);
const int size = static_cast<int>(Side);
return ((value % size) + size) % size;
};
const float red = static_cast<float>(Red[address(y) * Side + address(x)]) / 255.0f;
return reference <= red ? 1.0f : 0.0f;
}
float Expected(std::uint32_t tid, const Sampler& sampler, bool offsets) {
const float u = Input[tid * 3u + 1u] * static_cast<float>(Side);
const float v = Input[tid * 3u + 2u] * static_cast<float>(Side);
const float reference = std::clamp(Input[tid * 3u + 0u], 0.0f, 1.0f);
const int offsetX = offsets ? OffsetComponent(tid, 0u) : 0;
const int offsetY = offsets ? OffsetComponent(tid, 1u) : 0;
if (sampler.filter == FilterPoint) return ReferenceTexel(static_cast<int>(std::floor(u)) + offsetX, static_cast<int>(std::floor(v)) + offsetY, reference, sampler);
const float cu = u - 0.5f;
const float cv = v - 0.5f;
const int x = static_cast<int>(std::floor(cu)) + offsetX;
const int y = static_cast<int>(std::floor(cv)) + offsetY;
const float a = cu - std::floor(cu);
const float b = cv - std::floor(cv);
const float top = ReferenceTexel(x, y, reference, sampler) * (1.0f - a) + ReferenceTexel(x + 1, y, reference, sampler) * a;
const float bottom = ReferenceTexel(x, y + 1, reference, sampler) * (1.0f - a) + ReferenceTexel(x + 1, y + 1, reference, sampler) * a;
return top * (1.0f - b) + bottom * b;
}
ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, std::span<const std::uint32_t> code = Code, bool useCache = false, bool nativeSampleOffsets = true, bool cube = false) {
std::vector<std::uint32_t> userData(24, 0u);
const auto input = cube ? BufferDescriptor(CubeInput.data(), static_cast<std::uint32_t>(sizeof(CubeInput))) : BufferDescriptor(Input.data(), static_cast<std::uint32_t>(sizeof(Input)));
@@ -254,9 +216,7 @@ std::uint64_t Run(AgcDriver::VulkanDevice& device, const Sampler& sampler, const
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 float expected = Expected(tid, sampler, offsets);
const float tolerance = sampler.filter == FilterPoint ? 0.0f : 1e-4f;
Require(std::fabs(Output[tid] - expected) <= tolerance, std::string(name) + ": thread " + std::to_string(tid) + " compared to " + std::to_string(Output[tid]) + ", expected " + std::to_string(expected));
Require(Output[tid] == 0.0f, std::string(name) + ": thread " + std::to_string(tid) + " compared to " + std::to_string(Output[tid]) + ", expected 0");
}
return result.variantId;
}
@@ -274,12 +234,11 @@ void RunChannels(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t>
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 float compared = Expected(tid, sampler, false);
for (std::uint32_t reg = 0; reg < 4u; ++reg) {
float expected = -1.0f;
if (reg < registers) {
const auto select = channels[firstChannel + reg];
expected = select == 0u ? 0.0f : select == 1u ? 1.0f : compared;
expected = select == 0u ? 0.0f : select == 1u ? 1.0f : 0.0f;
}
const float actual = Output[tid * 4u + reg];
Require(std::fabs(actual - expected) <= 1e-4f, std::string(name) + ": thread " + std::to_string(tid) + " register " + std::to_string(reg) + " returned " + std::to_string(actual) + ", expected " + std::to_string(expected));
@@ -304,7 +263,7 @@ void RunCube(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(CubeCode.data()));
device.WaitIdle();
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
Require(Output[tid] == 1.0f, std::string(name) + ": +X cube face comparison returned " + std::to_string(Output[tid]) + ", expected 1");
Require(Output[tid] == 0.0f, std::string(name) + ": +X cube face comparison returned " + std::to_string(Output[tid]) + ", expected 0");
}
}
@@ -314,7 +273,7 @@ bool BindsDepthCompare(const ShaderRecompiler::RecompileResult& result) {
});
}
void RunSrgbCubeZero(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, const char* name) {
void RunZeroCube(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, const char* name) {
FillCubeTexels();
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
CubeInput[tid * 4u + 0u] = 0.5f;
@@ -332,7 +291,7 @@ void RunSrgbCubeZero(AgcDriver::VulkanDevice& device, std::uint32_t format, cons
}
}
void RunSrgbZero(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, const char* name, bool useCache, std::uint64_t expectedVariant) {
void RunZero(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, const char* name, bool useCache, std::uint64_t expectedVariant) {
Output.fill(-1.0f);
const auto result = Compile(device, format, sampler, Code, useCache);
if (useCache) Require(result.cacheHit && result.variantId == expectedVariant, std::string(name) + " did not reuse the compiled comparison artifact");
@@ -366,25 +325,38 @@ int main() {
FillTexels();
FillInput();
RunCube(*device, {ClampEdge, FilterPoint}, "point cube, clamp to edge");
Reject(*device, Format1616UNorm, {ClampEdge, FilterBilinear}, "bilinear cube comparison requires DISABLE_CUBE_WRAP", CubeCode, true, true);
RunCube(*device, {ClampEdge, FilterBilinear}, "bilinear cube without DISABLE_CUBE_WRAP");
for (std::uint32_t function = 0; function < 8u; ++function) {
const auto name = "bilinear 8_8_8_8 srgb cube function " + std::to_string(function);
RunSrgbCubeZero(*device, Format8888Srgb, {ClampEdge, FilterBilinear, BorderBlack, 0u, 0u, false, function}, name.c_str());
RunZeroCube(*device, Format8888Srgb, {ClampEdge, FilterBilinear, BorderBlack, 0u, 0u, false, function}, name.c_str());
}
RunSrgbCubeZero(*device, Format8Srgb, {ClampEdge, FilterBilinear}, "bilinear 8 srgb cube without DISABLE_CUBE_WRAP");
RunSrgbCubeZero(*device, Format8_8Srgb, {ClampEdge, FilterBilinear}, "bilinear 8_8 srgb cube without DISABLE_CUBE_WRAP");
RunZeroCube(*device, Format8Srgb, {ClampEdge, FilterBilinear}, "bilinear 8 srgb cube without DISABLE_CUBE_WRAP");
RunZeroCube(*device, Format8_8Srgb, {ClampEdge, FilterBilinear}, "bilinear 8_8 srgb cube without DISABLE_CUBE_WRAP");
RunCube(*device, {ClampEdge, FilterBilinear, BorderBlack, 0u, 0u, true}, "bilinear cube with DISABLE_CUBE_WRAP");
Reject(*device, Format9995Float, {ClampEdge, FilterPoint}, "implemented only for float, unorm and snorm formats");
Reject(*device, Format9995Float, {ClampEdge, FilterPoint}, "implemented only for floating-point, UNORM and SNORM formats");
for (std::uint32_t function = 0; function < 8u; ++function) {
const auto name = "bilinear 8_8_8_8 unorm cube function " + std::to_string(function);
RunSrgbCubeZero(*device, Format8888UNorm, {ClampEdge, FilterBilinear, BorderBlack, 0u, 0u, false, function}, name.c_str());
RunZeroCube(*device, Format8888UNorm, {ClampEdge, FilterBilinear, BorderBlack, 0u, 0u, false, function}, name.c_str());
}
RunSrgbCubeZero(*device, Format8888UNorm, {ClampEdge, FilterAnisoBilinear, BorderBlack, 0u, 0u, false, GreaterEqual}, "anisotropic bilinear 8_8_8_8 unorm cube without DISABLE_CUBE_WRAP");
Reject(*device, Format1616UNorm, {ClampEdge, FilterAnisoBilinear}, "point or bilinear");
Reject(*device, Format1616UNorm, {ClampEdge, FilterAnisoPoint}, "point or bilinear");
RunZeroCube(*device, Format8888UNorm, {ClampEdge, FilterAnisoBilinear, BorderBlack, 0u, 0u, false, GreaterEqual}, "anisotropic bilinear 8_8_8_8 unorm cube without DISABLE_CUBE_WRAP");
RunZero(*device, Format1616UNorm, {ClampEdge, FilterAnisoBilinear}, "anisotropic bilinear 16_16 unorm", false, 0u);
RunZero(*device, Format1616UNorm, {ClampEdge, FilterAnisoPoint}, "anisotropic point 16_16 unorm", false, 0u);
FillTexels();
Require(BindsDepthCompare(Compile(*device, Format16UNorm, {ClampEdge, FilterPoint})), "an R16 unorm texture left the native comparison path");
Require(BindsDepthCompare(Compile(*device, Format32Float, {ClampEdge, FilterBilinear})), "an R32 float texture left the native comparison path");
Require(!BindsDepthCompare(Compile(*device, Format8888UNorm, {ClampEdge, FilterBilinear})), "a color texture kept a depth-compare binding");
for (const auto format : {1u, 2u, 8u, 13u, 14u, 15u, 23u, 24u, 29u, 36u, 50u, 51u, 56u, 57u, 65u, 66u, 71u, 77u}) {
if (format == 51u && !device->SupportsImageFormat(VK_FORMAT_A2B10G10R10_SNORM_PACK32, VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT)) {
std::puts("measured GFX10 color format 51 returns zero: skipped, Vulkan device does not support the image format");
continue;
}
const auto name = "measured GFX10 color format " + std::to_string(format) + " returns zero";
RunZero(*device, format, {ClampEdge, FilterPoint}, name.c_str(), false, 0u);
}
for (std::uint32_t function = 0; function < 8u; ++function) {
const auto name = "16_16 unorm compare function " + std::to_string(function) + " returns zero";
RunZero(*device, Format1616UNorm, {ClampEdge, FilterPoint, BorderBlack, 0u, 0u, false, function}, name.c_str(), false, 0u);
}
for (const bool useCache : {false, true}) {
const auto variant = useCache ? Run(*device, {ClampEdge, FilterPoint}, "cached UNORM before sRGB", false, true) : 0u;
if (useCache) Require(variant != 0, "the cached comparison has no compiled artifact identity");
@@ -392,7 +364,7 @@ int main() {
for (std::uint32_t function = 0; function < 8u; ++function) {
for (const auto filter : {FilterPoint, FilterBilinear}) {
const auto name = std::string(useCache ? "cached" : "uncached") + " sRGB format " + std::to_string(format) + " function " + std::to_string(function) + (filter == FilterPoint ? " point" : " bilinear");
RunSrgbZero(*device, format, {ClampEdge, filter, BorderBlack, 0u, 0u, false, function}, name.c_str(), useCache, variant);
RunZero(*device, format, {ClampEdge, filter, BorderBlack, 0u, 0u, false, function}, name.c_str(), useCache, variant);
}
}
}
@@ -403,10 +375,11 @@ int main() {
for (const auto filter : {FilterPoint, FilterBilinear, FilterAnisoBilinear}) {
if (format != Format8888UNorm && function != LessEqual) continue;
const auto name = "8-bit unorm format " + std::to_string(format) + " function " + std::to_string(function) + " filter " + std::to_string(filter);
RunSrgbZero(*device, format, {ClampEdge, filter, BorderBlack, 0u, 0u, false, function}, name.c_str(), false, 0u);
RunZero(*device, format, {ClampEdge, filter, BorderBlack, 0u, 0u, false, function}, name.c_str(), false, 0u);
}
}
}
RunZero(*device, Format1616UNorm, {ClampEdge, FilterPoint}, "16_16 unorm comparison returns zero", false, 0u);
Run(*device, {ClampEdge, FilterPoint}, "point, clamp to edge");
Run(*device, {ClampWrap, FilterPoint}, "point, wrap");
if (lodClamp) Run(*device, {ClampWrap, FilterPoint}, "point, wrap with negative offsets", true);
@@ -63,16 +63,16 @@ void* Map(std::size_t bytes = BlockBytes) {
return result;
}
void CheckPattern(const void* pointer) {
void CheckPattern(const void* pointer, std::size_t bytes = FillBytes) {
const auto* data = static_cast<const std::byte*>(pointer);
for (std::size_t offset = 0; offset < FillBytes; offset += sizeof(Pattern))
for (std::size_t offset = 0; offset < bytes; offset += sizeof(Pattern))
Require(std::memcmp(data + offset, Pattern.data(), sizeof(Pattern)) == 0, "guest mutation lost the pending GPU fill");
}
void Fill(AgcDriver::VulkanDevice& device, void* pointer) {
void Fill(AgcDriver::VulkanDevice& device, void* pointer, std::size_t bytes = FillBytes) {
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
Require(device.FillBuffer(Address(pointer), FillBytes, Pattern), "Vulkan device did not import the guest allocation");
Require(Imported(device, pointer), "GPU fill did not create a host import");
Require(device.FillBuffer(Address(pointer), bytes, Pattern), "Vulkan device did not import the guest allocation");
Require(Imported(device, pointer, bytes), "GPU fill did not create a host import");
}
void CheckMappings(AgcDriver::VulkanDevice& device) {
@@ -105,14 +105,25 @@ void CheckMappings(AgcDriver::VulkanDevice& device) {
Require(!Imported(device, mapping), "host import survived guest munmap");
mapping = Map(3 * BlockBytes);
Fill(device, mapping);
Fill(device, mapping, 3 * BlockBytes);
auto* middle = static_cast<std::uint8_t*>(mapping) + BlockBytes;
Require(mprotect_nid_postfix(middle, BlockBytes, 1) == 0, "guest partial protection failed");
Require(!Imported(device, mapping), "whole host import survived partial protection change");
CheckPattern(mapping);
Fill(device, mapping);
Require(!Imported(device, mapping, 3 * BlockBytes), "whole host import survived partial protection change");
CheckPattern(mapping, 3 * BlockBytes);
Require(munmap_nid_postfix(middle, BlockBytes) == 0, "guest middle unmap after protection change failed");
Require(!Imported(device, mapping, 3 * BlockBytes), "host import still covered the protected and unmapped middle range");
Require(munmap_nid_postfix(mapping, BlockBytes) == 0, "guest prefix unmap after protection change failed");
Require(munmap_nid_postfix(middle + BlockBytes, BlockBytes) == 0, "guest suffix unmap after protection change failed");
mapping = Map(3 * BlockBytes);
Fill(device, mapping, 3 * BlockBytes);
Require(Imported(device, mapping, 3 * BlockBytes), "whole host import did not cover the three-block mapping before middle unmap");
middle = static_cast<std::uint8_t*>(mapping) + BlockBytes;
Require(munmap_nid_postfix(middle, BlockBytes) == 0, "guest middle unmap failed");
Require(Imported(device, mapping), "unrelated prefix import was retired by middle unmap");
Require(!Imported(device, mapping, 3 * BlockBytes), "middle unmap left a host import spanning the unmapped range");
Require(!Imported(device, mapping), "middle unmap retained the prefix of the retired host import");
Fill(device, mapping);
Require(Imported(device, mapping), "the retained prefix was not imported again after middle unmap");
Require(munmap_nid_postfix(mapping, BlockBytes) == 0, "guest prefix unmap failed");
Require(!Imported(device, mapping), "prefix host import survived guest munmap");
Require(munmap_nid_postfix(middle + BlockBytes, BlockBytes) == 0, "guest suffix unmap failed");
@@ -30,7 +30,10 @@ constexpr std::uint32_t ClampEdge = 2;
alignas(256) std::array<std::uint32_t, Threads * 4> Input{};
alignas(256) std::array<float, Threads * 8> Output{};
alignas(65536) std::array<std::uint8_t, 4u << 20u> Texels{};
constexpr std::size_t TexelBytes = 4u << 20u;
constexpr std::size_t TexelAlignment = 65536;
std::array<std::uint8_t, TexelBytes + TexelAlignment - 1> TexelStorage{};
const std::span<std::uint8_t, TexelBytes> Texels{reinterpret_cast<std::uint8_t*>((reinterpret_cast<std::uintptr_t>(TexelStorage.data()) + TexelAlignment - 1) & ~(TexelAlignment - 1)), TexelBytes};
alignas(256) constexpr std::array<std::uint32_t, 15> Code{
0x34020084, 0xe0381000, 0x80000201, 0xbf8c3f70, 0xf09c0f20, 0x00820602, 0xf0001f20, 0x00020a04,
@@ -62,7 +65,7 @@ std::array<std::uint8_t, 4> Texel(std::uint32_t x, std::uint32_t guestLayer) {
}
void FillTexels() {
Texels.fill(0);
std::ranges::fill(Texels, std::uint8_t{0});
const auto surface = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor()));
const auto& mip = surface.mips.at(0);
Require(surface.layers == LastArray + 1u && surface.guestBytes <= Texels.size(), "the test array does not fit its storage");
@@ -128,6 +131,7 @@ void Run(AgcDriver::VulkanDevice& device, std::uint32_t baseArray = BaseArray) {
int main() {
try {
Require(reinterpret_cast<std::uintptr_t>(Texels.data()) % TexelAlignment == 0, "the test array is not 64 KiB aligned");
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
FillTexels();
@@ -11,6 +11,7 @@
#include <cstdio>
#include <exception>
#include <iostream>
#include <span>
#include <string>
#include <utility>
#include <vector>
@@ -23,9 +24,9 @@ constexpr std::uint32_t Width = 64;
constexpr std::uint32_t Height = 64;
using Image = std::array<std::byte, Width * Height * 4>;
alignas(256) Image Pixels{};
alignas(256) constexpr std::array<std::uint16_t, 4> Indices{0, 1, 2, 0};
alignas(256) constexpr std::array<std::array<float, 8>, 3> Vertices{{
{-0.9f, -0.8f, 0, 1, 1, 0, 0, 1}, {-0.1f, -0.8f, 0, 1, 0, 1, 0, 1}, {-0.5f, 0.8f, 0, 1, 0, 0, 1, 1}
alignas(256) constexpr std::array<std::uint16_t, 4> Indices{0, 1, 2, 3};
alignas(256) constexpr std::array<std::array<float, 8>, 4> Vertices{{
{-0.9f, -0.8f, 0, 1, 1, 0, 0, 1}, {-0.1f, -0.8f, 0, 1, 0, 1, 0, 1}, {-0.5f, 0.8f, 0, 1, 0, 0, 1, 1}, {0.3f, 0.8f, 0, 1, 1, 1, 0, 1}
}};
alignas(256) constexpr std::array<std::uint32_t, 7> FlatPixelCode{0xc8120002u, 0xc8160102u, 0xc81a0202u, 0xc81e0302u, 0xf800180fu, 0x07060504u, 0xbf810000u};
alignas(256) constexpr std::array<std::uint32_t, 11> SmoothPixelCode{0xc8100000u, 0xc8110001u, 0xc8140100u, 0xc8150101u, 0xc8180200u, 0xc8190201u, 0xc81c0300u, 0xc81d0301u, 0xf800180fu, 0x07060504u, 0xbf810000u};
@@ -64,7 +65,7 @@ void Write(std::vector<std::uint32_t>& commands, std::uint32_t opcode, std::uint
commands.insert(commands.end(), {0xc0010000u | (opcode << 8u), offset, value});
}
Image Draw(const std::uint32_t* pixelCode, bool flat, bool last) {
Image Draw(const std::uint32_t* pixelCode, bool flat, bool last, bool strip = false) {
Pixels.fill(std::byte{0});
const auto color = reinterpret_cast<std::uintptr_t>(Pixels.data());
const auto geometry = reinterpret_cast<std::uintptr_t>(GeometryCode.data());
@@ -98,13 +99,14 @@ Image Draw(const std::uint32_t* pixelCode, bool flat, bool last) {
{0x8, static_cast<std::uint32_t>(pixel >> 8u)}, {0x9, static_cast<std::uint32_t>(pixel >> 40u)}, {0xb, 0}
};
std::vector<std::uint32_t> commands;
Write(commands, 0x79, 0x242, 4);
Write(commands, 0x79, 0x242, strip ? 6u : 4u);
Write(commands, 0x79, 0x25b, (12u << 9u) | 4u);
for (const auto& [offset, value] : context) Write(commands, 0x69, offset, value);
for (const auto& [offset, value] : shaders) Write(commands, 0x76, offset, value);
commands.insert(commands.end(), {0xc0002a00u, 0u});
const auto address = reinterpret_cast<std::uintptr_t>(Indices.data());
commands.insert(commands.end(), {0xc0042700u, 3u, static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u), 3u, 0u});
const std::uint32_t count = strip ? 4u : 3u;
commands.insert(commands.end(), {0xc0042700u, count, static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u), count, 0u});
Packet packet{commands.data(), static_cast<std::uint32_t>(commands.size()), 0, {}};
Require(sceAgcDriverSubmitDcb(&packet) == 0, "mesh submission failed");
AgcDriverWaitIdle_nid_postfix();
@@ -132,6 +134,25 @@ void FlatInputs() {
}
}
constexpr std::array<std::pair<std::uint32_t, std::uint32_t>, 4> SecondInside{{{26, 48}, {32, 48}, {29, 44}, {29, 52}}};
void CheckFlat(const Image& image, std::span<const std::pair<std::uint32_t, std::uint32_t>> pixels, const std::array<unsigned, 4>& expected, const std::string& what) {
for (const auto& [x, y] : pixels) {
for (std::uint32_t channel = 0; channel < 4; ++channel) {
Require(Channel(image, x, y, channel) == expected[channel], what + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(Channel(image, x, y, channel)) + ", expected " + std::to_string(expected[channel]));
}
}
}
void StripFlatInputs() {
const auto last = Draw(FlatPixelCode.data(), true, true, true);
CheckFlat(last, Inside, {0u, 0u, 255u, 255u}, "triangle strip, last provoking vertex, triangle 0 (the color of vertex 2)");
CheckFlat(last, SecondInside, {255u, 255u, 0u, 255u}, "triangle strip, last provoking vertex, triangle 1 (the color of vertex 3)");
Require(Channel(last, 48, 48, 3) == 0, "triangle strip, last provoking vertex: the draw wrote outside its triangles");
const auto first = Draw(FlatPixelCode.data(), true, false, true);
CheckFlat(first, Inside, {255u, 0u, 0u, 255u}, "triangle strip, first provoking vertex, triangle 0 (the color of vertex 0)");
}
void SmoothInputs() {
const auto first = Draw(SmoothPixelCode.data(), false, false);
for (const auto& [x, y] : Inside) {
@@ -169,9 +190,10 @@ int main() {
Register(FlatPixelHeader, FlatPixelCode.data(), sizeof(FlatPixelCode), 1);
Register(SmoothPixelHeader, SmoothPixelCode.data(), sizeof(SmoothPixelCode), 1);
FlatInputs();
StripFlatInputs();
if (smooth) SmoothInputs();
AgcDriverShutdown_nid_postfix();
std::cout << "geometry-program triangles take flat inputs from vertex 0 with the first provoking vertex and from vertex 2 with the last" << (smooth ? ", and interpolate smooth inputs the same in both modes\n" : "; smooth inputs skipped without fragment shader barycentrics\n");
std::cout << "geometry-program triangles take flat inputs from vertex 0 with the first provoking vertex and from vertex 2 with the last, and the second triangle of a strip takes them from vertex 3 with the last" << (smooth ? ", and interpolate smooth inputs the same in both modes\n" : "; smooth inputs skipped without fragment shader barycentrics\n");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
@@ -25,7 +25,9 @@
#include <cstring>
#include <exception>
#include <mutex>
#include <optional>
#include <span>
#include <utility>
namespace AgcDriver {
@@ -153,10 +155,12 @@ int RunNativeClear(AgcDriver::VulkanDevice& device) {
device.WaitIdle();
target.clearDepth = 1.0f;
Require(DepthSurfaceView(context, target) == view, "changing the native clear value replaced its attachment");
NoteHtileDepthClear(target.htileAddress, metadataBytes, metadataBytes);
std::optional<std::pair<std::uint64_t, std::size_t>> htileClear;
if (NoteHtileDepthClear(target.htileAddress, metadataBytes, metadataBytes)) htileClear.emplace(target.htileAddress, metadataBytes);
Require(htileClear.has_value(), "the full BDA clear was not registered for completion");
const auto generation = Memory::TrackerGeneration();
userData[4] = 0xfffffff0u;
device.Dispatch(compile(), groups, 1u, 1u, {}, reinterpret_cast<std::uintptr_t>(code.data()));
device.Dispatch(compile(), groups, 1u, 1u, {}, reinterpret_cast<std::uintptr_t>(code.data()), nullptr, nullptr, htileClear);
device.WaitIdle();
for (std::size_t offset = 0; offset < BlockBytes; offset += 4u) {
std::uint32_t value = 0;
@@ -7,6 +7,9 @@
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
#include "prx/libc/include/GuestArena.hpp"
#include "prx/libc/include/GuestAllocations.hpp"
#include "prx/libc/include/GuestWriteWatch.hpp"
@@ -21,6 +24,7 @@
#include <cstdlib>
#include <cstring>
#include <memory>
#include <mutex>
#include <optional>
#include <span>
#include <string>
@@ -35,6 +39,12 @@
#include <sys/mman.h>
#endif
namespace AgcDriver::DriverDetail {
struct DriverTestAccess {
static std::shared_ptr<VulkanDevice> GetDevice() { return Driver::Get().device.Load(); }
};
}
namespace {
using namespace AgcDriver::Graphics;
@@ -137,7 +147,7 @@ private:
class TargetMemory {
public:
explicit TargetMemory(std::size_t bytes) : bytes(bytes) {
explicit TargetMemory(std::size_t bytes, bool registerAllocation = false) : bytes(bytes), registered(registerAllocation) {
#ifdef _WIN32
pointer = GuestArena::GuestArenaAllocate_nid_postfix(bytes, 65536u);
GuestArena::GuestArenaCommit_nid_postfix(pointer, bytes, PAGE_READWRITE, bytes);
@@ -153,8 +163,10 @@ public:
#endif
Require(pointer != nullptr, "multisample target fixture allocation failed");
std::memset(pointer, 0xa5, bytes);
if (registered) GuestAllocations::Mutation().Add(pointer, bytes, true, true, true);
}
~TargetMemory() {
if (registered) GuestAllocations::Mutation().Remove(pointer);
#ifdef _WIN32
GuestArena::GuestArenaReset_nid_postfix(pointer, bytes);
GuestArena::GuestArenaRelease_nid_postfix(pointer, bytes);
@@ -168,8 +180,49 @@ public:
private:
void* pointer = nullptr;
std::size_t bytes;
bool registered = false;
};
alignas(256) constexpr std::array<std::uint32_t, 17> HtileMaskKernel{
0xd7460000u, 0x04010c06u, 0x34000084u, 0xdc388000u, 0x04020000u, 0xbf8c3f70u, 0xd7710008u, 0x00120805u,
0xd7710009u, 0x00120a05u, 0xd771000au, 0x00120c05u, 0xd771000bu, 0x00120e05u, 0xdc788000u, 0x00020800u,
0xbf810000u,
};
std::vector<std::uint32_t> DriverPm4Packet(std::uint32_t opcode, std::initializer_list<std::uint32_t> body) {
std::vector<std::uint32_t> words{0xc0000000u | (static_cast<std::uint32_t>(body.size() - 1u) << 16u) | (opcode << 8u)};
words.insert(words.end(), body);
return words;
}
Context SubmitDriverHtileClear(std::uint64_t address, std::unique_lock<AgcDriver::GuestMemory::GpuMutexType>& gpuLock) {
const auto shader = reinterpret_cast<std::uintptr_t>(HtileMaskKernel.data());
std::vector<std::uint32_t> commands;
const auto append = [&commands](std::vector<std::uint32_t> packet) { commands.insert(commands.end(), packet.begin(), packet.end()); };
append(DriverPm4Packet(0x76u, {0x20cu, static_cast<std::uint32_t>(shader >> 8u)}));
append(DriverPm4Packet(0x76u, {0x20du, static_cast<std::uint32_t>(shader >> 40u) & 0xffu}));
append(DriverPm4Packet(0x76u, {0x207u, 64u}));
append(DriverPm4Packet(0x76u, {0x208u, 1u}));
append(DriverPm4Packet(0x76u, {0x209u, 1u}));
append(DriverPm4Packet(0x76u, {0x213u, (6u << 1u) | 0x80u}));
append(DriverPm4Packet(0x76u, {0x240u, 0u}));
append(DriverPm4Packet(0x76u, {0x241u, 0u}));
append(DriverPm4Packet(0x76u, {0x242u, static_cast<std::uint32_t>(address)}));
append(DriverPm4Packet(0x76u, {0x243u, static_cast<std::uint32_t>(address >> 32u)}));
append(DriverPm4Packet(0x76u, {0x244u, 0xfffc00f0u}));
append(DriverPm4Packet(0x76u, {0x245u, 0u}));
append(DriverPm4Packet(0x15u, {32u, 1u, 1u, 0x41u}));
Packet packet{commands.data(), static_cast<std::uint32_t>(commands.size()), 0u, {}};
Require(sceAgcDriverSubmitDcb(&packet) == 0, "native depth HTILE clear: PM4 submission failed");
gpuLock.unlock();
try { AgcDriverWaitIdle_nid_postfix(); }
catch (...) { gpuLock.lock(); throw; }
gpuLock.lock();
const auto driverDevice = AgcDriver::DriverDetail::DriverTestAccess::GetDevice();
Require(driverDevice != nullptr, "HTILE clear driver device is missing");
return driverDevice->graphicsContext();
}
template<typename Action>
void TargetRefuses(Action action, std::string_view expected) {
std::string message;
@@ -600,7 +653,7 @@ void CheckDrawFeedback() {
std::puts("Native depth feedback refused through the draw resource path");
}
enum class ClearFault { None, PartialStore, CpuWrite, DepthWrite, BadMetadata, UnprovenDispatch, PendingWrite, Overlap };
enum class ClearFault { None, PartialStore, CpuWrite, DepthWrite, BadMetadata, UnprovenDispatch, PendingWrite, Overlap, PossibleStore, CompletedPossibleStore, DriverDispatch, ImportWindowWrite };
void CheckClearDispatch() {
const std::array<std::uint32_t, 17> code{
@@ -609,7 +662,7 @@ void CheckClearDispatch() {
0xd771000a, 0x00120c05, 0xd771000b, 0x00120e05,
0xdc788000, 0x00020800, 0xbf810000,
};
const std::array<std::uint32_t, 6> userData{0u, 0u, 65536u, 0u, 0xf0u, 0xf0u};
const std::array<std::uint32_t, 6> userData{0u, 0u, 65536u, 0u, 0xfffc00f0u, 0u};
const ShaderRecompiler::ShaderComputeStageInfo compute{{64u, 1u, 1u}, 0u, {true, false, false}, false, 1u};
const std::array<std::uint32_t, 5> packet{0u, 32u, 1u, 1u, 1u};
Require(HtileDepthClearBytes_nid_no_patch(code, userData, compute, packet, 0u) == 32768u, "complete HTILE mask dispatch was not recognized");
@@ -649,10 +702,10 @@ void CheckClearDispatch() {
}
}
void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, bool one, bool clearing = false, std::optional<bool> depthOnlyOverride = std::nullopt, ClearFault fault = ClearFault::None, bool iterate256 = true) {
void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, bool one, bool clearing, std::optional<bool> depthOnlyOverride, ClearFault fault, std::unique_lock<AgcDriver::GuestMemory::GpuMutexType>& gpuLock, bool iterate256 = true) {
const VkExtent2D extent{5u, 3u};
const auto bytes = static_cast<std::size_t>(MultisampledDepthBytes_nid_no_patch(extent, format == VK_FORMAT_D16_UNORM ? 2u : 4u, fragments));
TargetMemory memory(bytes), metadata(65536u);
TargetMemory memory(bytes), metadata(65536u, fault == ClearFault::DriverDispatch);
const auto metadataBytes = HtileMetadataBytes(extent);
const bool depthOnly = depthOnlyOverride.value_or(one);
const std::uint32_t pattern = one ? (depthOnly ? 0xfffffff0u : 0xfffc00f0u) : 0u;
@@ -770,17 +823,57 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments,
target.clearDepth = clearOne ? 1.0f : 0.0f;
Require(DepthSurfaceView(context, target) == view, "updating the native clear value replaced its attachment");
const std::uint32_t clearPattern = clearOne ? (depthOnly ? 0xfffffff0u : 0xfffc00f0u) : 0u;
const auto nativeBytes = fault == ClearFault::UnprovenDispatch ? 0u : static_cast<std::size_t>(metadataBytes);
NoteHtileDepthClear(target.htileAddress, static_cast<std::size_t>(metadataBytes), nativeBytes);
if (fault == ClearFault::Overlap) NoteHtileDepthClear(target.htileAddress, static_cast<std::size_t>(metadataBytes), nativeBytes);
const auto storedBytes = fault == ClearFault::PartialStore ? 4u : static_cast<std::size_t>(metadataBytes);
AgcDriver::GuestMemory::StoreOwnBytes(target.htileAddress, storedBytes, [&] {
for (std::size_t i = 0u; i < storedBytes / 4u; ++i) std::memcpy(metadata.Data() + i * 4u, &clearPattern, 4u);
if (fault == ClearFault::BadMetadata) metadata.Data()[0] = std::byte{0x0f};
});
if (fault == ClearFault::CpuWrite) metadata.Data()[metadataBytes - 1u] ^= std::byte{0x01};
if (fault == ClearFault::DepthWrite) memory.Data()[bytes - 1u] ^= std::byte{0x01};
if (fault == ClearFault::PendingWrite) QueueTargetByteWrite(context, recorder, target.htileAddress, std::byte{0});
if (fault == ClearFault::DriverDispatch && clearOne) {
struct ShutdownDriver {
~ShutdownDriver() {
try { AgcDriverShutdown_nid_postfix(); } catch (...) {}
}
} shutdownDriver;
const auto generation = AgcDriver::GuestMemory::TrackerGeneration();
const auto driverContext = SubmitDriverHtileClear(target.htileAddress, gpuLock);
recorder.Activate();
Require(Recorder::Active() == &recorder, "driver idle did not restore the native depth fixture recorder");
if (!AgcDriver::GuestMemory::Watched(target.htileAddress, metadataBytes)) {
Require(PrepareImportWatch(driverContext) == ImportWatch::Unwatch,
"the HTILE metadata range lost write-watch coverage on a device whose host imports stay watched");
std::puts("skipped native HTILE write-coverage assertion: the write tracker does not cover the imported metadata range; the production depth-sampling path may still decline this clear");
TargetRefuses([&] { Require(DepthSurfaceHolds(context, DecodeTextureResource(words), texture.get()), "native depth owner was not retained"); },
"native multisample depth clear requires tracked HTILE generations");
return;
} else {
const bool storedAll = AgcDriver::GuestMemory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation);
Require(storedAll, "driver idle returned before recording the completed HTILE dispatch");
}
} else {
const auto nativeBytes = fault == ClearFault::UnprovenDispatch ? 0u : static_cast<std::size_t>(metadataBytes);
if (fault == ClearFault::PartialStore || fault == ClearFault::PossibleStore || fault == ClearFault::CompletedPossibleStore) {
for (std::size_t i = 0u; i < metadataBytes / 4u; ++i) std::memcpy(metadata.Data() + i * 4u, &clearPattern, 4u);
}
const bool completionCandidate = NoteHtileDepthClear(target.htileAddress, static_cast<std::size_t>(metadataBytes), nativeBytes);
if (fault == ClearFault::ImportWindowWrite) {
const auto beforeImport = AgcDriver::GuestMemory::TrackerGeneration();
Require(AgcDriver::GuestMemory::ImportWatched(target.htileAddress, static_cast<std::size_t>(metadataBytes), [&] {
metadata.Data()[0] ^= std::byte{0x01};
return true;
}), "HTILE host import simulation failed");
Require(AgcDriver::GuestMemory::CollectWritesUncached(target.htileAddress, static_cast<std::size_t>(metadataBytes)) > beforeImport, "HTILE import window did not stamp its changed page");
}
if (completionCandidate) ArmHtileDepthClear(target.htileAddress, static_cast<std::size_t>(metadataBytes));
if (fault == ClearFault::Overlap) NoteHtileDepthClear(target.htileAddress, static_cast<std::size_t>(metadataBytes), nativeBytes);
const auto storedBytes = fault == ClearFault::PartialStore ? 4u : static_cast<std::size_t>(metadataBytes);
if (fault == ClearFault::PossibleStore || fault == ClearFault::CompletedPossibleStore) {
const auto generation = AgcDriver::GuestMemory::TrackerGeneration();
AgcDriver::GuestMemory::MarkPossiblyWritten(target.htileAddress, metadataBytes);
Require(!AgcDriver::GuestMemory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation), "a possible HTILE range proved complete coverage");
if (fault == ClearFault::CompletedPossibleStore) CompleteHtileDepthClear(target.htileAddress, static_cast<std::size_t>(metadataBytes));
} else AgcDriver::GuestMemory::StoreOwnBytes(target.htileAddress, storedBytes, [&] {
for (std::size_t i = 0u; i < storedBytes / 4u; ++i) std::memcpy(metadata.Data() + i * 4u, &clearPattern, 4u);
if (fault == ClearFault::BadMetadata) metadata.Data()[0] = std::byte{0x0f};
});
if (fault == ClearFault::CpuWrite) metadata.Data()[metadataBytes - 1u] ^= std::byte{0x01};
if (fault == ClearFault::DepthWrite) memory.Data()[bytes - 1u] ^= std::byte{0x01};
if (fault == ClearFault::PendingWrite) QueueTargetByteWrite(context, recorder, target.htileAddress, std::byte{0});
}
Require(DepthSurfaceHolds(context, DecodeTextureResource(words), texture.get()), "completed native depth clear lost its sampled view");
recorder.Sync();
Require(DepthSurfaceView(context, target) == view && CachedSampledTexture(context, words) == texture, "completed native depth clear replaced an attachment or cached view");
@@ -1129,7 +1222,7 @@ int main(int argc, char** argv) {
}
CheckDrawFeedback();
const auto context = device->GetContext();
const std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
std::unique_lock lock(AgcDriver::GuestMemory::GpuMutex());
Require(DepthSupported(context, VK_FORMAT_D32_SFLOAT, 4u), "the native depth regression requires sampled D32 with four samples");
if (argc == 2 && std::string_view(argv[1]) == "--clear-refusals") {
for (const auto& [fault, expected] : std::array{
@@ -1137,10 +1230,11 @@ int main(int argc, char** argv) {
std::pair{ClearFault::CpuWrite, "CPU writes raced"},
std::pair{ClearFault::DepthWrite, "guest writes"},
std::pair{ClearFault::BadMetadata, "does not prove its clear value"},
std::pair{ClearFault::PossibleStore, "no complete tracked HTILE store"},
std::pair{ClearFault::UnprovenDispatch, "incomplete or unproven"},
std::pair{ClearFault::PendingWrite, "external GPU writes"},
std::pair{ClearFault::Overlap, "overlapping native multisample depth"}}) {
TargetRefuses([&] { CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, fault); }, expected);
TargetRefuses([&] { CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, fault, lock); }, expected);
std::printf("Native depth clear fault=%u refused: %s\n", static_cast<std::uint32_t>(fault), expected);
}
return 0;
@@ -1152,10 +1246,16 @@ int main(int argc, char** argv) {
for (const auto samples : {2u, 4u, 8u}) {
if (!DepthSupported(context, format, samples)) { unavailable += 4u; continue; }
for (const bool depthOnly : {false, true}) {
for (const bool one : {false, true}) { CheckDepthTarget(context, format, samples, one, true, depthOnly); ++checked; }
for (const bool one : {false, true}) { CheckDepthTarget(context, format, samples, one, true, depthOnly, ClearFault::None, lock); ++checked; }
}
}
}
CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::CompletedPossibleStore, lock);
++checked;
CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::ImportWindowWrite, lock);
++checked;
CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::DriverDispatch, lock);
++checked;
std::printf("Native depth completed clear cases checked=%u unavailable=%u\n", checked, unavailable);
return 0;
}
@@ -1176,7 +1276,7 @@ int main(int argc, char** argv) {
for (const auto samples : {2u, 4u, 8u}) {
if (!DepthSupported(context, format, samples)) { unavailable += 4u; continue; }
for (const bool one : {false, true}) {
for (const bool iterate256 : {true, false}) { CheckDepthTarget(context, format, samples, one, false, std::nullopt, ClearFault::None, iterate256); ++checked; }
for (const bool iterate256 : {true, false}) { CheckDepthTarget(context, format, samples, one, false, std::nullopt, ClearFault::None, lock, iterate256); ++checked; }
}
CheckHtileRouting(context, format, samples);
}
@@ -18,6 +18,7 @@
#include <exception>
#include <algorithm>
#include <iostream>
#include <limits>
#include <span>
#include <string>
#include <vector>
@@ -141,14 +142,17 @@ void CheckLdsSplitThrows(AgcDriver::VulkanDevice& device) {
request.useCache = false;
const auto result = ShaderRecompiler::Recompile(request);
Require(result.workgroupMemoryDwords != 0u, "oversized dispatch: the LDS shader did not take the device-memory path");
bool threw = false;
try {
device.Dispatch(result, 1, device.WorkGroupCountLimit()[1] + 1u, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
} catch (const std::runtime_error&) {
threw = true;
for (const auto z : {1u, std::numeric_limits<std::uint32_t>::max()}) {
bool threw = false;
try {
device.Dispatch(result, 1, device.WorkGroupCountLimit()[1] + 1u, z, {}, reinterpret_cast<std::uintptr_t>(code.data()));
} catch (const std::runtime_error& error) {
Require(std::string(error.what()).find("a split aliases the device-memory LDS slots") != std::string::npos, "oversized dispatch: wrong LDS split refusal: " + std::string(error.what()));
threw = true;
}
device.WaitIdle();
Require(threw, "oversized dispatch: a Y count over the limit with device-memory LDS did not throw");
}
device.WaitIdle();
Require(threw, "oversized dispatch: a Y count over the limit with device-memory LDS did not throw");
}
alignas(256) constexpr std::array<std::uint32_t, 6> GroupYCode{
@@ -17,6 +17,7 @@
#include <future>
#include <initializer_list>
#include <iostream>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
@@ -138,20 +139,34 @@ void Flush(const std::array<std::byte, 1024>& memory) {
}
struct Blocker {
std::atomic<bool> release{false};
struct State {
std::atomic<bool> release{false};
std::atomic<bool> done{false};
};
std::shared_ptr<State> state = std::make_shared<State>();
void Release() { state->release = true; }
void Enqueue() {
DrawPipeline::Queue0().Enqueue([this] {
while (!release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1));
std::lock_guard gpuLock(AgcDriver::GuestMemory::GpuMutex());
DrawPipeline::Queue0().Enqueue([state = state] {
while (!state->release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1));
{ std::lock_guard gpuLock(AgcDriver::GuestMemory::GpuMutex()); }
state->done = true;
}, {});
}
void Join() const {
const auto start = std::chrono::steady_clock::now();
while (!state->done.load()) {
Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(20), "the blocker never ran");
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
};
void SubmitBehind(Blocker& blocker, std::vector<std::uint32_t>& commands) {
void SubmitBehind(Blocker& blocker, std::vector<std::uint32_t>& commands, std::size_t queuedBefore) {
std::unique_lock gpuLock(AgcDriver::GuestMemory::GpuMutex());
Packet packet{commands.data(), static_cast<std::uint32_t>(commands.size()), 0, {}};
AgcDriver::Submit(&packet, 0);
std::this_thread::sleep_for(std::chrono::milliseconds(500));
const auto start = std::chrono::steady_clock::now();
while (DrawPipeline::Queue0().Outstanding() < queuedBefore && std::chrono::steady_clock::now() - start < std::chrono::seconds(60)) std::this_thread::sleep_for(std::chrono::milliseconds(1));
blocker.Enqueue();
}
@@ -190,19 +205,20 @@ void IndirectDrawsQueueBehindTheCommitter() {
AppendTarget(commands, SecondMemory, Clipped);
AppendIndirect(commands);
Blocker blocker;
SubmitBehind(blocker, commands);
SubmitBehind(blocker, commands, 3);
const auto start = std::chrono::steady_clock::now();
while (DrawPipeline::Queue0().Outstanding() < 3) {
if (std::chrono::steady_clock::now() - start > std::chrono::seconds(10)) {
const auto outstanding = DrawPipeline::Queue0().Outstanding();
blocker.release = true;
blocker.Release();
Finish();
Require(false, "the indirect draws drained the pipeline instead of queuing behind it (" + std::to_string(outstanding) + " items outstanding, expected the blocker and two draws)");
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
blocker.release = true;
blocker.Release();
Finish();
blocker.Join();
Flush(FirstMemory);
Flush(SecondMemory);
SameAsDirect(FirstMemory, "the first queued indirect draw");
@@ -219,12 +235,13 @@ void IndirectDrawWaitsForItsQueuedRecordWrite() {
AppendTarget(commands, WrittenMemory, Clipped);
AppendIndirect(commands);
Blocker blocker;
SubmitBehind(blocker, commands);
const auto drainsBefore = DrawPipeline::DrainRequestsByOpcode(0x100);
SubmitBehind(blocker, commands, 1);
const auto start = std::chrono::steady_clock::now();
while (DrawPipeline::Queue0().Outstanding() < 2 && std::chrono::steady_clock::now() - start < std::chrono::seconds(10)) std::this_thread::sleep_for(std::chrono::milliseconds(1));
std::this_thread::sleep_for(std::chrono::milliseconds(500));
blocker.release = true;
while (DrawPipeline::DrainRequestsByOpcode(0x100) == drainsBefore && std::chrono::steady_clock::now() - start < std::chrono::seconds(10)) std::this_thread::sleep_for(std::chrono::milliseconds(1));
blocker.Release();
Finish();
blocker.Join();
Flush(WrittenMemory);
Require(LateRecords[0] == 3, "the queued label did not store the record's vertex count");
SameAsDirect(WrittenMemory, "the indirect draw whose record a queued label wrote");
@@ -325,7 +325,7 @@ void Run(const Context& context, const CopyPipeline& pipeline, bool writable, co
std::memset(readback.Bytes().data(), 0xcc, BufferBytes);
VkBuffer primed = VK_NULL_HANDLE;
{
Buffer earlier(context, BufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
Buffer earlier(context, BufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
primed = earlier.Handle();
std::memset(earlier.Bytes().data(), 0xa5, BufferBytes);
}
@@ -142,14 +142,28 @@ int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
constexpr std::array<Case, 7> cases{{
{2u, 1u, true, "OOB_SELECT 2, one record"},
constexpr std::uint32_t Wzyx = 7u | (6u << 3u) | (1u << 6u) | (0u << 9u);
constexpr std::array<Case, 20> cases{{
{0u, 0u, false, "OOB_SELECT 0, no records"},
{0u, 1u, false, "OOB_SELECT 0, one record"},
{0u, 16u, false, "OOB_SELECT 0, records cover the element's bytes"},
{0u, 0xffffffffu, false, "OOB_SELECT 0, NUM_RECORDS 0xffffffff"},
{0u, 0xffffffffu, false, "OOB_SELECT 0, NUM_RECORDS 0xffffffff, selectors W Z 1 0", Wzyx},
{1u, 0u, false, "OOB_SELECT 1, no records"},
{1u, 3u, true, "OOB_SELECT 1, NUM_RECORDS counts the three vertices' records"},
{1u, 4u, true, "OOB_SELECT 1, NUM_RECORDS past the vertices"},
{1u, 0xffffffffu, true, "OOB_SELECT 1, NUM_RECORDS 0xffffffff"},
{1u, 3u, true, "OOB_SELECT 1, NUM_RECORDS 3, selectors W Z 1 0", Wzyx},
{2u, 0u, false, "OOB_SELECT 2, no records"},
{3u, 16u, true, "OOB_SELECT 3, whole element in range"},
{2u, 1u, true, "OOB_SELECT 2, one record"},
{2u, 16u, true, "OOB_SELECT 2, whole element in range"},
{3u, 0u, false, "OOB_SELECT 3, no records"},
{0u, 16u, true, "OOB_SELECT 0, whole element in range"},
{3u, 16u, true, "OOB_SELECT 3, selectors W Z 1 0", 7u | (6u << 3u) | (1u << 6u) | (0u << 9u)},
{3u, 0u, false, "OOB_SELECT 3, no records, selectors W Z 1 0", 7u | (6u << 3u) | (1u << 6u) | (0u << 9u)},
{3u, 8u, false, "OOB_SELECT 3, the element only partly fits"},
{3u, 15u, false, "OOB_SELECT 3, the element misses its last byte"},
{3u, 16u, true, "OOB_SELECT 3, whole element in range"},
{3u, 0xffffffffu, true, "OOB_SELECT 3, NUM_RECORDS 0xffffffff"},
{3u, 16u, true, "OOB_SELECT 3, selectors W Z 1 0", Wzyx},
{3u, 8u, false, "OOB_SELECT 3, partly fits, selectors W Z 1 0", Wzyx},
}};
for (const auto& value : cases) {
Draw(*device, value);
@@ -0,0 +1,15 @@
function(add_sce_ajm_library target)
set(ajmDir ${CMAKE_CURRENT_FUNCTION_LIST_DIR})
add_library(${target} SHARED EXCLUDE_FROM_ALL
${ajmDir}/Export.cpp
${ajmDir}/src/Ajm.cpp
${ARGN}
)
target_include_directories(${target} PRIVATE
${LIBS_INCLUDE_DIR}
${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src
${CMAKE_SOURCE_DIR}/3rdparty/vgmstream/src/coding/libs
)
target_link_libraries(${target} PRIVATE atrac9 atrac9_vibration ffmpeg libc libkernel)
configure_windows_unwind(${target})
endfunction()
+3 -12
View File
@@ -29,22 +29,13 @@ add_library(atrac9_vibration STATIC EXCLUDE_FROM_ALL
)
set_target_properties(atrac9_vibration PROPERTIES POSITION_INDEPENDENT_CODE ON C_VISIBILITY_PRESET hidden)
add_library(libSceAjm.native SHARED EXCLUDE_FROM_ALL
${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Ajm.cpp
)
target_include_directories(libSceAjm.native PRIVATE
${LIBS_INCLUDE_DIR}
${LIBATRAC9_DIR}
${VGMSTREAM_ATRAC9_DIR}
)
target_link_libraries(libSceAjm.native PRIVATE atrac9 atrac9_vibration ffmpeg libc libkernel)
configure_windows_unwind(libSceAjm.native)
include(${CMAKE_CURRENT_SOURCE_DIR}/AjmLibrary.cmake)
add_sce_ajm_library(libSceAjm.native)
if(BUILD_TESTING)
add_test_executable(atrac9_gradient_tests tests/Atrac9Gradient.c ${LIBATRAC9_DIR}/bit_reader.c)
target_include_directories(atrac9_gradient_tests PRIVATE ${LIBATRAC9_PATCHED_DIR} ${LIBATRAC9_DIR})
target_link_libraries(atrac9_gradient_tests PRIVATE atrac9)
target_compile_options(atrac9_gradient_tests PRIVATE -ffunction-sections -fdata-sections)
if(APPLE)
target_link_options(atrac9_gradient_tests PRIVATE LINKER:-dead_strip)
+33 -2
View File
@@ -12,6 +12,37 @@ static void AjmStub(const char* name) {
NotImplemented_nid_no_patch(name);
}
struct WVorbisHeaderPacketParameter {
std::size_t size;
std::int32_t channels;
std::uint32_t sampleRate;
std::int32_t bytesPerSecond;
std::int32_t shortBlockExponent;
std::int32_t longBlockExponent;
std::uint32_t reserved;
const void* setupPacket;
std::size_t setupPacketSize;
void* output;
std::size_t outputCapacity;
std::size_t outputSize;
};
struct WVorbisSetupPacketParameter {
std::size_t size;
std::int32_t channels;
std::uint32_t reserved;
const void* input;
std::size_t inputSize;
void* output;
std::size_t outputCapacity;
std::size_t outputSize;
};
static_assert(sizeof(WVorbisHeaderPacketParameter) == 72);
static_assert(offsetof(WVorbisHeaderPacketParameter, setupPacket) == 32);
static_assert(sizeof(WVorbisSetupPacketParameter) == 56);
static_assert(offsetof(WVorbisSetupPacketParameter, input) == 16);
extern "C" {
int APS5_VABI sceAjmBatchJobEncode(AjmBatchInfo* info, uint32_t instance, const void* pcm_input, size_t pcm_input_size, void* bitstream_output, size_t bitstream_output_size, void* result) {
@@ -32,12 +63,12 @@ const char* APS5_VABI sceAjmStrError(int error) {
return nullptr;
}
int APS5_VABI sceAjmDecWVorbisCreateHeaderPacket(void) {
int APS5_VABI sceAjmDecWVorbisCreateHeaderPacket(WVorbisHeaderPacketParameter* parameter) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAjmDecWVorbisCreateSetupPacket(void) {
int APS5_VABI sceAjmDecWVorbisCreateSetupPacket(WVorbisSetupPacketParameter* parameter) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
+2
View File
@@ -0,0 +1,2 @@
include(${CMAKE_CURRENT_SOURCE_DIR}/../libSceAjm.native/AjmLibrary.cmake)
add_sce_ajm_library(libSceAjm ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp)
+14 -1
View File
@@ -5,6 +5,19 @@
extern "C" {
APS5_DUMMY_FUN
int APS5_VABI sceAjmBatchJobControlBufferRa() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAjmBatchJobRunSplitBufferRa() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAjmBatchStartBuffer() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
}
-2
View File
@@ -96,7 +96,6 @@ static int AppendNop(Apr::CommandBufferObject* buffer, std::uint32_t dwords, con
if (bytes > RemainingCommandBytes(*buffer)) return SCE_KERNEL_ERROR_EBUSY;
const Apr::CommandHeader header{Apr::Opcode::Nop, static_cast<std::uint32_t>(bytes)};
std::uint8_t* destination = buffer->base + buffer->offset;
std::memset(destination, 0, bytes);
std::memcpy(destination, &header, sizeof(header));
if (data) std::memcpy(destination + sizeof(header), data, std::uint64_t{dwords} * 4u);
buffer->offset += static_cast<std::uint32_t>(bytes);
@@ -432,7 +431,6 @@ int APS5_VABI sceAmprCommandBufferNop(Apr::CommandBufferObject* buffer, std::uin
int APS5_VABI sceAmprCommandBufferNopWithData(Apr::CommandBufferObject* buffer, std::uint32_t dwords, const std::uint32_t* data) {
if (dwords > 15u) return SCE_KERNEL_ERROR_EINVAL;
if (dwords != 0u && !data) throw std::invalid_argument("sceAmprCommandBufferNopWithData: null data");
return AppendNop(buffer, dwords, data);
}
+56 -13
View File
@@ -3,14 +3,17 @@
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include "prx/libc/include/RemoveTree.hpp"
#include "prx/libc/include/ApplicationHeap.hpp"
#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp"
#include <algorithm>
#include <atomic>
#include <cstring>
#include <filesystem>
#include <mutex>
#include <stdexcept>
#include <string>
static constexpr int SCE_APP_CONTENT_ERROR_NOT_INITIALIZED = static_cast<int>(0x80D90001);
static constexpr int SCE_APP_CONTENT_ERROR_PARAMETER = static_cast<int>(0x80D90002);
static constexpr int SCE_APP_CONTENT_ERROR_BUSY = static_cast<int>(0x80D90003);
static constexpr int SCE_APP_CONTENT_ERROR_NOT_MOUNTED = static_cast<int>(0x80D90004);
@@ -30,6 +33,20 @@ struct AppContentAddcontInfo {
namespace {
std::atomic_bool initialized{false};
std::mutex initializationMutex;
std::uint32_t compiledSdkVersion() {
const auto* process = static_cast<const std::byte*>(ApplicationProcessParameters_nid_no_patch());
std::uint32_t version;
std::memcpy(&version, process + 16, sizeof(version));
return version;
}
bool validEntitlementLabel(const NpUnifiedEntitlementLabel& label, std::uint32_t minimumSdkVersion) {
return compiledSdkVersion() < minimumSdkVersion || std::all_of(std::begin(label.padding), std::end(label.padding), [](char value) { return value == 0; });
}
struct TemporaryData {
std::mutex mutex;
const std::filesystem::path directory = ResolvePath_nid_no_patch(TEMPORARY_MOUNT_POINT);
@@ -57,30 +74,37 @@ static void ClearDirectory(const std::filesystem::path& directory) {
extern "C" {
int APS5_VABI sceAppContentAddcontMount(uint32_t service_label, const NpUnifiedEntitlementLabel* entitlement_label, AppContentMountPoint* mount_point) {
if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED;
(void)service_label;
if (!entitlement_label || !mount_point) return SCE_APP_CONTENT_ERROR_PARAMETER;
if (!validEntitlementLabel(*entitlement_label, 0x01500000)) return SCE_APP_CONTENT_ERROR_PARAMETER;
return SCE_APP_CONTENT_ERROR_NOT_FOUND;
}
int APS5_VABI sceAppContentAddcontUnmount(const AppContentMountPoint* mount_point) {
if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED;
if (!mount_point) return SCE_APP_CONTENT_ERROR_PARAMETER;
return SCE_APP_CONTENT_ERROR_NOT_FOUND;
}
int APS5_VABI sceAppContentGetAddcontInfo(uint32_t service_label, const NpUnifiedEntitlementLabel* entitlement_label, AppContentAddcontInfo* info) {
if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED;
(void)service_label;
if (!entitlement_label || !info) return SCE_APP_CONTENT_ERROR_PARAMETER;
if (!validEntitlementLabel(*entitlement_label, 0x01500000)) return SCE_APP_CONTENT_ERROR_PARAMETER;
return SCE_APP_CONTENT_ERROR_DRM_NO_ENTITLEMENT;
}
int APS5_VABI sceAppContentGetAddcontInfoList(uint32_t service_label, AppContentAddcontInfo* list, uint32_t list_num, uint32_t* hit_num) {
(void)service_label;
if ((!list || list_num == 0) && !hit_num) return SCE_APP_CONTENT_ERROR_PARAMETER;
if (hit_num) *hit_num = 0;
int APS5_VABI sceAppContentGetAddcontInfoList(uint32_t serviceLabel, AppContentAddcontInfo* list, uint32_t listNum, uint32_t* hitNum) {
if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED;
(void)serviceLabel;
if (!hitNum) return SCE_APP_CONTENT_ERROR_PARAMETER;
*hitNum = 0;
return 0;
}
int APS5_VABI sceAppContentAppParamGetInt(uint32_t param_id, int32_t* value) {
if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED;
if (!value) return SCE_APP_CONTENT_ERROR_PARAMETER;
switch (param_id) {
case APPPARAM_ID_SKU_FLAG:
@@ -95,6 +119,7 @@ int APS5_VABI sceAppContentAppParamGetInt(uint32_t param_id, int32_t* value) {
}
int APS5_VABI sceAppContentDownloadDataFormat(const AppContentMountPoint* mount_point) {
if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED;
if (!mount_point) return SCE_APP_CONTENT_ERROR_PARAMETER;
if (std::strncmp(mount_point->data, DOWNLOAD_MOUNT_POINT, sizeof(mount_point->data)) != 0) return SCE_APP_CONTENT_ERROR_NOT_MOUNTED;
const auto directory = ResolvePath_nid_no_patch(DOWNLOAD_MOUNT_POINT);
@@ -103,12 +128,13 @@ int APS5_VABI sceAppContentDownloadDataFormat(const AppContentMountPoint* mount_
return 0;
}
int APS5_VABI sceAppContentDownloadDataGetAvailableSpaceKb(const AppContentMountPoint* mount_point, size_t* available_space_kb) {
if (!mount_point || !available_space_kb) return SCE_APP_CONTENT_ERROR_PARAMETER;
if (std::strncmp(mount_point->data, DOWNLOAD_MOUNT_POINT, sizeof(mount_point->data)) != 0) return SCE_APP_CONTENT_ERROR_NOT_MOUNTED;
int APS5_VABI sceAppContentDownloadDataGetAvailableSpaceKb(const AppContentMountPoint* mountPoint, size_t* availableSpaceKb) {
if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED;
if (!mountPoint || !availableSpaceKb) return SCE_APP_CONTENT_ERROR_PARAMETER;
if (std::strncmp(mountPoint->data, DOWNLOAD_MOUNT_POINT, sizeof(mountPoint->data)) != 0) return SCE_APP_CONTENT_ERROR_NOT_MOUNTED;
const std::uint64_t quotaKb = GetAppDownloadDataSizeMiB_nid_postfix() * 1024u;
if (quotaKb == 0) {
*available_space_kb = 0;
*availableSpaceKb = 0;
return 0;
}
const auto directory = ResolvePath_nid_no_patch(DOWNLOAD_MOUNT_POINT);
@@ -118,19 +144,27 @@ int APS5_VABI sceAppContentDownloadDataGetAvailableSpaceKb(const AppContentMount
if (entry.is_regular_file()) usedKb += (entry.file_size() + 1023u) / 1024u;
}
const std::uint64_t hostKb = std::filesystem::space(directory).available / 1024u;
*available_space_kb = static_cast<size_t>(std::min(quotaKb - std::min(quotaKb, usedKb), hostKb));
*availableSpaceKb = static_cast<size_t>(std::min(quotaKb - std::min(quotaKb, usedKb), hostKb));
return 0;
}
int APS5_VABI sceAppContentInitialize(const AppContentInitParam* init_param, AppContentBootParam* boot_param) {
(void)init_param;
if (!boot_param) return SCE_APP_CONTENT_ERROR_PARAMETER;
int APS5_VABI sceAppContentInitialize(const AppContentInitParam* initParam, AppContentBootParam* bootParam) {
std::lock_guard lock(initializationMutex);
const auto sdkVersion = compiledSdkVersion();
if (sdkVersion >= 0x01500000) {
if (initialized.load()) return SCE_APP_CONTENT_ERROR_BUSY;
if (initParam == nullptr || bootParam == nullptr) return SCE_APP_CONTENT_ERROR_PARAMETER;
if (!std::all_of(std::begin(initParam->reserved), std::end(initParam->reserved), [](char value) { return value == 0; }) || !std::all_of(std::begin(bootParam->reserved1), std::end(bootParam->reserved1), [](char value) { return value == 0; }) || !std::all_of(std::begin(bootParam->reserved2), std::end(bootParam->reserved2), [](char value) { return value == 0; })) return SCE_APP_CONTENT_ERROR_PARAMETER;
}
if (bootParam == nullptr) return SCE_APP_CONTENT_ERROR_PARAMETER;
Temporary();
std::memset(boot_param, 0, sizeof(*boot_param));
std::memset(bootParam, 0, sizeof(*bootParam));
initialized = true;
return 0;
}
int APS5_VABI sceAppContentTemporaryDataFormat(const AppContentMountPoint* mountPoint) {
if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED;
if (!mountPoint) return SCE_APP_CONTENT_ERROR_PARAMETER;
auto& state = Temporary();
std::lock_guard lock(state.mutex);
@@ -140,6 +174,7 @@ int APS5_VABI sceAppContentTemporaryDataFormat(const AppContentMountPoint* mount
}
int APS5_VABI sceAppContentTemporaryDataGetAvailableSpaceKb(const AppContentMountPoint* mountPoint, size_t* availableSpaceKb) {
if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED;
if (!mountPoint || !availableSpaceKb) return SCE_APP_CONTENT_ERROR_PARAMETER;
auto& state = Temporary();
std::lock_guard lock(state.mutex);
@@ -149,6 +184,7 @@ int APS5_VABI sceAppContentTemporaryDataGetAvailableSpaceKb(const AppContentMoun
}
int APS5_VABI sceAppContentTemporaryDataMount2(uint32_t option, AppContentMountPoint* mountPoint) {
if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED;
if (!mountPoint || option > TEMPORARY_DATA_OPTION_FORMAT) return SCE_APP_CONTENT_ERROR_PARAMETER;
auto& state = Temporary();
std::lock_guard lock(state.mutex);
@@ -163,6 +199,7 @@ int APS5_VABI sceAppContentTemporaryDataMount2(uint32_t option, AppContentMountP
}
int APS5_VABI sceAppContentTemporaryDataUnmount(const AppContentMountPoint* mountPoint) {
if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED;
if (!mountPoint) return SCE_APP_CONTENT_ERROR_PARAMETER;
auto& state = Temporary();
std::lock_guard lock(state.mutex);
@@ -174,20 +211,26 @@ int APS5_VABI sceAppContentTemporaryDataUnmount(const AppContentMountPoint* moun
int APS5_VABI sceAppContentAddcontEnqueueDownload(uint32_t service_label, const NpUnifiedEntitlementLabel* entitlement_label) {
if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED;
(void)service_label;
if (!entitlement_label) return SCE_APP_CONTENT_ERROR_PARAMETER;
if (!validEntitlementLabel(*entitlement_label, 0x01500000)) return SCE_APP_CONTENT_ERROR_PARAMETER;
return SCE_APP_CONTENT_ERROR_NOT_FOUND;
}
int APS5_VABI sceAppContentAddcontDelete(uint32_t service_label, const NpUnifiedEntitlementLabel* entitlement_label) {
if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED;
(void)service_label;
if (!entitlement_label) return SCE_APP_CONTENT_ERROR_PARAMETER;
if (!validEntitlementLabel(*entitlement_label, 0x03500000)) return SCE_APP_CONTENT_ERROR_PARAMETER;
return SCE_APP_CONTENT_ERROR_NOT_FOUND;
}
int APS5_VABI sceAppContentGetAddcontDownloadProgress(uint32_t service_label, const NpUnifiedEntitlementLabel* entitlement_label, void* progress) {
if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED;
(void)service_label;
if (!entitlement_label || !progress) return SCE_APP_CONTENT_ERROR_PARAMETER;
if (!validEntitlementLabel(*entitlement_label, 0x03500000)) return SCE_APP_CONTENT_ERROR_PARAMETER;
return SCE_APP_CONTENT_ERROR_NOT_FOUND;
}
int APS5_VABI sceAppContentAddcontEnqueueDownloadByEntitlemetId(void) {
+11 -9
View File
@@ -3,6 +3,8 @@
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
struct AppInstallProgressInfo;
extern "C" {
int APS5_VABI sceAppInstUtilInitialize(void) {
@@ -25,7 +27,7 @@ int APS5_VABI sceAppInstUtilAppIsInInstalling(int32_t* installing) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAppInstUtilAppIsInUpdating() {
int APS5_VABI sceAppInstUtilAppIsInUpdating(const char* titleId, bool* updating) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
@@ -33,11 +35,11 @@ int APS5_VABI sceAppInstUtilAppExists(const char* titleId, int32_t* exists) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAppInstUtilAppGetSize() {
int APS5_VABI sceAppInstUtilAppGetSize(const char* titleId, std::uint64_t* size) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAppInstUtilAppInstallPkg() {
int APS5_VABI sceAppInstUtilAppInstallPkg(const char* packagePath, std::uint64_t serviceArgument) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
@@ -53,11 +55,11 @@ int APS5_VABI sceAppInstUtilAppUnInstall(const char* titleId) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAppInstUtilAppUnInstallAddcont() {
int APS5_VABI sceAppInstUtilAppUnInstallAddcont(const char* titleId, const char* contentLabel) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAppInstUtilAppUnInstallPat() {
int APS5_VABI sceAppInstUtilAppUnInstallPat(const char* titleId) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
@@ -65,19 +67,19 @@ int APS5_VABI sceAppInstUtilAppUnInstallTheme() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAppInstUtilAppUnInstallTypes() {
int APS5_VABI sceAppInstUtilAppUnInstallTypes(const char* titleId, std::uint32_t types) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAppInstUtilGetInstallProgressInfo() {
int APS5_VABI sceAppInstUtilGetInstallProgressInfo(const char* titleId, AppInstallProgressInfo* info) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAppInstUtilGetInstallProgress() {
int APS5_VABI sceAppInstUtilGetInstallProgress(const char* titleId, void* progress) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAppInstUtilGetPrimaryAppSlot() {
int APS5_VABI sceAppInstUtilGetPrimaryAppSlot(const char* titleId, void* slot) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
+5 -10
View File
@@ -199,22 +199,17 @@ int APS5_VABI sceAudio3dPortGetQueueLevel(uint32_t port_id, uint32_t* queue_leve
return 0;
}
int APS5_VABI sceAudio3dPortGetAttributesSupported(uint32_t port_id, uint32_t* attributes, uint32_t* num_attributes) {
int APS5_VABI sceAudio3dPortGetAttributesSupported(uint32_t portId, uint32_t* attributes, uint32_t* numAttributes) {
std::lock_guard lock(g_mutex);
if (num_attributes == nullptr) return AUDIO3D_ERROR_INVALID_PARAMETER;
if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT;
if (!PortIsOpen(portId)) return AUDIO3D_ERROR_INVALID_PORT;
if (numAttributes == nullptr) return AUDIO3D_ERROR_INVALID_PARAMETER;
constexpr std::uint32_t supported[]{
AUDIO3D_ATTRIBUTE_LATE_REVERB_LEVEL,
AUDIO3D_ATTRIBUTE_DOWNMIX_SPREAD_RADIUS,
AUDIO3D_ATTRIBUTE_DOWNMIX_SPREAD_HEIGHT_AWARE,
};
if (attributes == nullptr) {
*num_attributes = std::size(supported);
return 0;
}
const std::uint32_t count = std::min<std::uint32_t>(*num_attributes, std::size(supported));
std::copy_n(std::begin(supported), count, attributes);
*num_attributes = count;
*numAttributes = std::size(supported);
if (attributes != nullptr) std::copy(std::begin(supported), std::end(supported), attributes);
return 0;
}
+10 -10
View File
@@ -17,12 +17,12 @@ constexpr int AUDIO_IN_ERROR_INVALID_HANDLE = static_cast<int>(0x80260101);
constexpr int AUDIO_IN_ERROR_INVALID_SIZE = static_cast<int>(0x80260102);
constexpr int AUDIO_IN_ERROR_INVALID_FREQ = static_cast<int>(0x80260103);
constexpr int AUDIO_IN_ERROR_INVALID_TYPE = static_cast<int>(0x80260104);
constexpr int AUDIO_IN_ERROR_INVALID_POINTER = static_cast<int>(0x80260105);
constexpr int AUDIO_IN_ERROR_INVALID_PARAM = static_cast<int>(0x80260106);
constexpr int AUDIO_IN_ERROR_PORT_FULL = static_cast<int>(0x80260107);
constexpr int AUDIO_IN_ERROR_BUSY = static_cast<int>(0x8026010A);
constexpr int AUDIO_IN_SILENT_STATE_DEVICE_NONE = 1;
constexpr std::uint32_t MAX_QUEUED_BLOCKS = 4;
constexpr std::uint32_t MAX_ASYNC_LEN = 384;
using Clock = std::chrono::steady_clock;
@@ -175,18 +175,18 @@ int32_t APS5_VABI sceAudioInClose(int32_t handle) {
return 0;
}
int32_t APS5_VABI sceAudioInHqOpen(int32_t user_id, uint32_t type, uint32_t index, uint32_t len, uint32_t freq, uint32_t param) {
(void)user_id;
if (const int error = checkArguments(type, index, len, freq); error != 0) return error;
if (len != 128) return AUDIO_IN_ERROR_INVALID_SIZE;
int32_t APS5_VABI sceAudioInHqOpen(int32_t userId, uint32_t type, uint32_t index, uint32_t len, uint32_t freq, uint32_t param) {
if (freq != 48000) return AUDIO_IN_ERROR_INVALID_FREQ;
if (len != 128) return AUDIO_IN_ERROR_INVALID_SIZE;
if (param != 2) return AUDIO_IN_ERROR_INVALID_PARAM;
if (const int error = checkArguments(type, index, len, freq); error != 0) return error;
return openPort(len, freq, param);
}
int32_t APS5_VABI sceAudioInAsyncOpen(int32_t user_id, uint32_t type, uint32_t index, uint32_t len, uint32_t freq, uint32_t param) {
(void)user_id;
if (const int error = checkArguments(type, index, len, freq); error != 0 && error != AUDIO_IN_ERROR_INVALID_SIZE) return error;
if (len != 128 && len != 256 && len != MAX_ASYNC_LEN) return AUDIO_IN_ERROR_INVALID_SIZE;
int32_t APS5_VABI sceAudioInAsyncOpen(int32_t userId, uint32_t type, uint32_t index, uint32_t len, uint32_t freq, uint32_t param) {
if (freq != 48000) return AUDIO_IN_ERROR_INVALID_FREQ;
if (len != 128) return AUDIO_IN_ERROR_INVALID_SIZE;
if (const int error = checkArguments(type, index, len, freq); error != 0) return error;
return openPort(len, freq, param);
}
@@ -197,7 +197,7 @@ int32_t APS5_VABI sceAudioInIsPadEmbeddedMic(int32_t handle) {
}
int32_t APS5_VABI sceAudioInGetGain(int32_t handle, float* gain) {
if (!gain) throw std::runtime_error("sceAudioInGetGain: null gain");
if (!gain) return AUDIO_IN_ERROR_INVALID_POINTER;
*gain = 1.0f;
std::lock_guard lock(g_mutex);
return find(handle) ? 0 : AUDIO_IN_ERROR_INVALID_HANDLE;
+13 -27
View File
@@ -40,7 +40,7 @@ static constexpr std::uint64_t DRAIN_TIMEOUT_US = 200000;
static constexpr std::uint64_t DRAIN_SLEEP_US = 1000;
static constexpr int MONITOR_TYPE_HDMI = 1;
static constexpr std::uint32_t HDMI_MONITOR_INFO_SIZE = 0x180;
static constexpr std::uint32_t EX_MONITOR_INFO_MIN_SIZE = 0xA0;
static constexpr std::uint32_t EX_MONITOR_INFO_SIZE = 0x400;
static constexpr std::uint32_t EX_OPEN_MODE_MAX = 10;
static constexpr std::uint32_t EX_OPEN_MODES = 0x61F;
static constexpr std::uint32_t EX_CONFIGURE_MODE_DEFAULT = 0xFF;
@@ -599,7 +599,7 @@ int APS5_VABI sceAudioOutGetPortState(int handle, AudioOutPortState* state) {
return 0;
}
int APS5_VABI sceAudioOutDeviceIdOpen() {
int APS5_VABI sceAudioOutDeviceIdOpen(std::uint64_t deviceId, std::int32_t type, std::int32_t index, std::uint32_t length, std::int32_t frequency, std::uint32_t parameter) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
@@ -635,15 +635,10 @@ int APS5_VABI sceAudioOutSysConfigureOutput(int type, std::uint64_t flags, std::
}
int APS5_VABI sceAudioOutSysGetHdmiMonitorInfo(int type, void* info, std::uint32_t size) {
if (type != MONITOR_TYPE_HDMI) {
throw std::runtime_error("sceAudioOutSysGetHdmiMonitorInfo: monitor type " + std::to_string(type) + " not supported");
}
if (size != HDMI_MONITOR_INFO_SIZE) {
throw std::runtime_error("sceAudioOutSysGetHdmiMonitorInfo: size " + std::to_string(size) + " not supported");
}
if (info == nullptr) {
return -2144993276;
}
if (info == nullptr) return static_cast<int>(0x80260004);
if (size != HDMI_MONITOR_INFO_SIZE) return static_cast<int>(0x80260006);
if (type != MONITOR_TYPE_HDMI && type != 0x10) return static_cast<int>(0x8026000C);
if (type == 0x10) throw std::runtime_error("sceAudioOutSysGetHdmiMonitorInfo: monitor type 16 not supported");
std::memset(info, 0, HDMI_MONITOR_INFO_SIZE);
return 0;
}
@@ -656,20 +651,17 @@ int APS5_VABI sceAudioOutSysOpen(int userId, std::uint32_t mode) {
return allocatePort(PORT_TYPE_SYS, SYS_PORT_SAMPLES, SYS_PORT_FRAME_RATE, Format::S16_8Ch);
}
int APS5_VABI sceAudioOutSetMixLevelPadSpk(int handle, int mixLevel) {
int APS5_VABI sceAudioOutSetMixLevelPadSpk(int handle, std::uint32_t mixLevel) {
std::lock_guard<std::mutex> lock(g_mutex);
Port* port = getPort(handle);
if (port == nullptr) {
return -2144993277;
}
if (port->type != PORT_TYPE_PADSPK) {
return -2144993270;
}
if (mixLevel > DEFAULT_VOLUME) {
return -2144993260;
}
if (mixLevel < 0) {
throw std::runtime_error("sceAudioOutSetMixLevelPadSpk: negative mix level not supported");
if (port->type != PORT_TYPE_PADSPK) {
return -2144993270;
}
port->mixLevel = mixLevel;
return 0;
@@ -706,16 +698,10 @@ int APS5_VABI sceAudioOutExConfigureOutput(int control, std::uint64_t flags, std
}
int APS5_VABI sceAudioOutExGetMonitorInfo(int type, int index, void* info, std::uint32_t size) {
if (type != 0 || index != 0) {
throw std::runtime_error("sceAudioOutExGetMonitorInfo: monitor " + std::to_string(type) + ", " + std::to_string(index) + " not supported");
}
if (size < EX_MONITOR_INFO_MIN_SIZE) {
throw std::runtime_error("sceAudioOutExGetMonitorInfo: size " + std::to_string(size) + " not supported");
}
if (info == nullptr) {
return -2144993276;
}
std::memset(info, 0, size);
if (type != 0) return static_cast<int>(0x80260003);
if (info == nullptr) return static_cast<int>(0x80260004);
if (size != EX_MONITOR_INFO_SIZE) return static_cast<int>(0x80260006);
std::memset(info, 0, EX_MONITOR_INFO_SIZE);
return 0;
}
@@ -16,8 +16,8 @@ int APS5_VABI sceAudioOut2Initialize(void) {
}
int APS5_VABI sceAudioOut2Set3DLatency(int userId, std::uint32_t output) {
(void)userId;
if (output > MAX_SUPPORTED_3D_OUTPUT) return SCE_AUDIO_OUT2_ERROR_INVALID_PARAM;
if (userId != 0 && (userId != 0xFF || output == 0)) return SCE_AUDIO_OUT2_ERROR_INVALID_PARAM;
return 0;
}
@@ -32,13 +32,13 @@ int APS5_VABI sceAudioOut2UserDestroy(AudioOut2UserHandle handle) {
}
int APS5_VABI sceAudioOut2UserGetSupportedAttributes(AudioOut2UserHandle handle, uint32_t* contextAttributes, uint32_t* portAttributes) {
if (!contextAttributes || !portAttributes)
throw std::runtime_error("sceAudioOut2UserGetSupportedAttributes: null output");
if (handle == 0 || handle == std::numeric_limits<AudioOut2UserHandle>::max()) return static_cast<int>(0x80268010);
if (!contextAttributes && !portAttributes) return static_cast<int>(0x80268001);
std::lock_guard lock(g_usersLock);
if (!g_users.contains(handle))
throw std::runtime_error("sceAudioOut2UserGetSupportedAttributes: unknown user handle");
*contextAttributes = 0;
*portAttributes = (1u << 0) | (1u << 1);
if (contextAttributes) *contextAttributes = 0;
if (portAttributes) *portAttributes = (1u << 0) | (1u << 1);
return 0;
}
@@ -53,11 +53,14 @@ constexpr int errorMasteringInvalidApiParam = static_cast<int>(0x80268201);
constexpr int errorMasteringInvalidStatesId = static_cast<int>(0x80268204);
void TestSet3DLatency() {
Require(sceAudioOut2Set3DLatency(systemUser, 0) == 0, "output 0 for the system user must be accepted");
Require(sceAudioOut2Set3DLatency(systemUser, 0) == errorInvalidParam, "output 0 for selector 255 must be refused");
for (std::uint32_t output = 0; output <= 2; ++output) {
Require(sceAudioOut2Set3DLatency(0, output) == 0, "selector 0 must accept each supported output");
}
Require(sceAudioOut2Set3DLatency(systemUser, 1) == 0, "output 1 for the system user must be accepted");
Require(sceAudioOut2Set3DLatency(systemUser, 2) == 0, "output 2 for the system user must be accepted");
Require(sceAudioOut2Set3DLatency(systemUser, 2) == 0, "output 2 must be accepted again");
Require(sceAudioOut2Set3DLatency(user, 1) == 0, "output 1 for another user must be accepted");
Require(sceAudioOut2Set3DLatency(user, 1) == errorInvalidParam, "a user id is not a valid latency selector");
Require(sceAudioOut2Set3DLatency(systemUser, 3) == errorInvalidParam, "output 3 must be refused");
Require(sceAudioOut2Set3DLatency(systemUser, 0xFFFFFFFFu) == errorInvalidParam, "a large output must be refused");
}
@@ -77,11 +77,17 @@ void TestUserAttributes() {
}
Require(threw && context.value == 0xFFFFFFFFu && port.value == 0xFFFFFFFFu);
};
rejects(first, true, false);
rejects(first, false, true);
rejects(first, true, true);
rejects(0, false, false);
rejects(std::numeric_limits<AudioOut2UserHandle>::max(), false, false);
context.value = port.value = 0xFFFFFFFFu;
Require(sceAudioOut2UserGetSupportedAttributes(first, nullptr, &port.value) == 0);
Require(context.value == 0xFFFFFFFFu && port.value == 3);
Require(sceAudioOut2UserGetSupportedAttributes(first, &context.value, nullptr) == 0 && context.value == 0);
context.value = port.value = 0xFFFFFFFFu;
Require(sceAudioOut2UserGetSupportedAttributes(first, nullptr, nullptr) == static_cast<int>(0x80268001));
for (const AudioOut2UserHandle handle : {AudioOut2UserHandle{0}, std::numeric_limits<AudioOut2UserHandle>::max()}) {
Require(sceAudioOut2UserGetSupportedAttributes(handle, &context.value, &port.value) == static_cast<int>(0x80268010));
Require(sceAudioOut2UserGetSupportedAttributes(handle, nullptr, nullptr) == static_cast<int>(0x80268010));
Require(context.value == 0xFFFFFFFFu && port.value == 0xFFFFFFFFu);
}
Require(sceAudioOut2UserDestroy(first) == 0);
rejects(first, false, false);
std::thread([&] {
@@ -5,8 +5,6 @@
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <stdexcept>
#include <string>
extern "C" {
int APS5_VABI sceAudioOutExGetMonitorInfo(int, int, void*, std::uint32_t);
@@ -21,28 +19,16 @@ static void Require(bool value, const char* message) {
namespace {
constexpr std::uint32_t titleSize = 0x400;
constexpr std::uint32_t minimumSize = 0xA0;
constexpr std::size_t descriptorCountOffset = 0x98;
constexpr std::size_t guardSize = 16;
constexpr std::uint8_t untouched = 0xA5;
constexpr int invalidPointer = static_cast<int>(0x80260004);
bool Throws(int type, int index, std::uint32_t size, const std::string& expected) {
std::array<std::uint8_t, titleSize> info{};
try {
sceAudioOutExGetMonitorInfo(type, index, info.data(), size);
} catch (const std::runtime_error& error) {
if (std::string(error.what()) == expected) return true;
std::fprintf(stderr, "threw: %s\n", error.what());
return false;
}
return false;
}
void CheckEmpty(std::uint32_t size) {
void CheckEmpty(int index) {
constexpr auto size = titleSize;
std::array<std::uint8_t, titleSize + guardSize> buffer{};
buffer.fill(untouched);
Require(sceAudioOutExGetMonitorInfo(0, 0, buffer.data(), size) == 0, "the monitor query must succeed");
Require(sceAudioOutExGetMonitorInfo(0, index, buffer.data(), size) == 0, "the monitor query must succeed");
for (std::uint32_t i = 0; i < size; i++) {
Require(buffer[i] == 0, "the monitor info must describe a sink without audio descriptors");
}
@@ -57,15 +43,14 @@ void CheckEmpty(std::uint32_t size) {
}
int main() {
CheckEmpty(titleSize);
CheckEmpty(minimumSize);
Require(sceAudioOutExGetMonitorInfo(0, 0, nullptr, titleSize) == invalidPointer,
"a null buffer must report an invalid pointer");
Require(Throws(0, 0, minimumSize - 1, "sceAudioOutExGetMonitorInfo: size 159 not supported"),
"a buffer without room for the descriptor count must throw");
Require(Throws(1, 0, titleSize, "sceAudioOutExGetMonitorInfo: monitor 1, 0 not supported"),
"a nonzero first argument must throw");
Require(Throws(0, 1, titleSize, "sceAudioOutExGetMonitorInfo: monitor 0, 1 not supported"),
"a nonzero second argument must throw");
for (const int index : {0, 1, -1, 100}) CheckEmpty(index);
std::array<std::uint8_t, titleSize + guardSize> buffer;
buffer.fill(untouched);
for (const std::uint32_t size : {0u, 0xA0u, titleSize - 1, titleSize + 1, 0xffffffffu}) {
Require(sceAudioOutExGetMonitorInfo(0, 0, buffer.data(), size) == static_cast<int>(0x80260006), "exact size required");
Require(sceAudioOutExGetMonitorInfo(0, 0, nullptr, size) == invalidPointer, "pointer checked before size");
Require(sceAudioOutExGetMonitorInfo(1, 0, nullptr, size) == static_cast<int>(0x80260003), "type checked before pointer");
for (const auto value : buffer) Require(value == untouched, "failure preserves output");
}
return 0;
}
@@ -16,7 +16,7 @@ int APS5_VABI sceAudioOutOpen(int, int, int, std::uint32_t, std::uint32_t, std::
int APS5_VABI sceAudioOutClose(int);
int APS5_VABI sceAudioOutOutput(int, const void*);
int APS5_VABI sceAudioOutSetVolume(int, std::uint32_t, int*);
int APS5_VABI sceAudioOutSetMixLevelPadSpk(int, int);
int APS5_VABI sceAudioOutSetMixLevelPadSpk(int, std::uint32_t);
}
static void Require(bool value) { if (!value) std::abort(); }
@@ -153,7 +153,7 @@ void TestOtherPortType() {
Require(Play(portTypeMain, formatS16Mono, block, [](int) {}) == block);
Require(Play(portTypeMain, formatS16Mono, block, [](int handle) {
Require(sceAudioOutSetMixLevelPadSpk(handle, unity / 2) == invalidPortType);
Require(sceAudioOutSetMixLevelPadSpk(handle, unity + 1) == invalidPortType);
Require(sceAudioOutSetMixLevelPadSpk(handle, unity + 1) == invalidMixLevel);
}) == block);
}
@@ -164,7 +164,7 @@ void TestRejectedLevels() {
closed = handle;
Require(sceAudioOutSetMixLevelPadSpk(handle, unity / 2) == 0);
Require(sceAudioOutSetMixLevelPadSpk(handle, unity + 1) == invalidMixLevel);
Require(ThrowsRuntimeError([handle] { sceAudioOutSetMixLevelPadSpk(handle, -1); }));
Require(sceAudioOutSetMixLevelPadSpk(handle, UINT32_MAX) == invalidMixLevel);
Require(sceAudioOutSetMixLevelPadSpk(0, unity) == invalidPort);
Require(sceAudioOutSetMixLevelPadSpk(handle + 1, unity) == invalidPort);
}) == Scaled(block, unity / 2));
@@ -59,13 +59,14 @@ int main() {
Require(sceAudioOutSysGetHdmiMonitorInfo(monitorTypeHdmi, nullptr, infoSize) == invalidPointer,
"a null buffer must report an invalid pointer");
Require(Throws(monitorTypeHdmi, infoSize - 1,
"sceAudioOutSysGetHdmiMonitorInfo: size 383 not supported"), "a short buffer must throw");
Require(Throws(monitorTypeHdmi, infoSize + 1,
"sceAudioOutSysGetHdmiMonitorInfo: size 385 not supported"), "a long buffer must throw");
buffer.fill(untouched);
for (const std::uint32_t size : {0u, infoSize - 1, infoSize + 1, 0xffffffffu}) {
Require(sceAudioOutSysGetHdmiMonitorInfo(monitorTypeHdmi, buffer.data(), size) == static_cast<int>(0x80260006), "exact size required");
Require(sceAudioOutSysGetHdmiMonitorInfo(0, nullptr, size) == invalidPointer, "pointer checked before size and type");
for (const auto value : buffer) Require(value == untouched, "failure preserves output");
}
for (const int type : {0, 2, -1, 17}) Require(sceAudioOutSysGetHdmiMonitorInfo(type, buffer.data(), infoSize) == static_cast<int>(0x8026000C), "unsupported type");
Require(Throws(0x10, infoSize,
"sceAudioOutSysGetHdmiMonitorInfo: monitor type 16 not supported"), "monitor type 0x10 must throw");
Require(Throws(0, infoSize,
"sceAudioOutSysGetHdmiMonitorInfo: monitor type 0 not supported"), "monitor type 0 must throw");
"sceAudioOutSysGetHdmiMonitorInfo: monitor type 16 not supported"), "unimplemented device query");
return 0;
}
@@ -9,6 +9,10 @@
namespace AudioPropagation {
inline constexpr std::int32_t ErrorInvalidArgument = static_cast<std::int32_t>(0x8A700001);
inline constexpr std::int32_t ErrorInvalidHandle = static_cast<std::int32_t>(0x8A700002);
inline constexpr std::int32_t ErrorNullPointer = static_cast<std::int32_t>(0x8A700003);
inline constexpr std::uint32_t MaterialId = 0x010107d1;
inline constexpr std::size_t MaterialSize = 0x40;
inline constexpr std::uint32_t SystemMemoryId = 0x010107d4;
@@ -23,6 +27,107 @@ inline constexpr std::size_t PortalParamsSize = 0x60;
inline constexpr std::uint32_t PathEntryId = 0x010107d9;
inline constexpr std::size_t PathEntrySize = 0x28;
inline constexpr std::uint32_t SystemOptionsId = 0x010107d5;
inline constexpr std::size_t SystemOptionsSize = 56;
struct SystemOptions {
AudioPropagationStructDescriptor desc;
std::uint32_t maxSources;
std::uint32_t maxMaterials;
std::uint32_t parameter24;
std::uint32_t reflectionOrder;
std::uint32_t frameSamples;
float propagationSpeed;
float maxPathLength;
std::uint32_t flags;
std::uint8_t opaque[8];
};
static_assert(sizeof(SystemOptions) == SystemOptionsSize);
static_assert(offsetof(SystemOptions, frameSamples) == 32);
static_assert(offsetof(SystemOptions, flags) == 44);
struct SourceInfo {
AudioPropagationHandle system;
float position[3];
std::uint32_t reserved20;
float direction[3];
std::uint32_t reserved36;
std::uint32_t attribute2Mode;
float attribute2Values[3];
float attribute3Values[4];
std::uint32_t pathCapacity;
std::uint32_t parameter76;
};
struct SystemInfo {
void* cpuMemory;
std::size_t cpuMemorySize;
std::uint32_t index;
std::uint32_t frameSamples;
std::uint32_t parameter24;
std::uint32_t maxSources;
std::uint32_t maxMaterials;
std::uint32_t pathCapacityPerSource;
float propagationSpeed;
std::uint32_t reserved44;
};
static_assert(sizeof(SourceInfo) == 80);
static_assert(offsetof(SourceInfo, position) == 8);
static_assert(offsetof(SourceInfo, direction) == 24);
static_assert(offsetof(SourceInfo, attribute2Mode) == 40);
static_assert(offsetof(SourceInfo, attribute2Values) == 44);
static_assert(offsetof(SourceInfo, attribute3Values) == 56);
static_assert(offsetof(SourceInfo, pathCapacity) == 72);
static_assert(offsetof(SourceInfo, parameter76) == 76);
static_assert(sizeof(SystemInfo) == 48);
static_assert(offsetof(SystemInfo, cpuMemorySize) == 8);
static_assert(offsetof(SystemInfo, index) == 16);
static_assert(offsetof(SystemInfo, frameSamples) == 20);
static_assert(offsetof(SystemInfo, parameter24) == 24);
static_assert(offsetof(SystemInfo, maxSources) == 28);
static_assert(offsetof(SystemInfo, maxMaterials) == 32);
static_assert(offsetof(SystemInfo, pathCapacityPerSource) == 36);
static_assert(offsetof(SystemInfo, propagationSpeed) == 40);
struct ReportRegistration;
struct ReportArguments {
std::uint32_t generalOffset;
std::uint32_t floatingOffset;
void* overflowArguments;
void* savedRegisters;
};
struct ReportInfo {
std::uint64_t size;
AudioPropagationHandle handle;
std::uint32_t category;
std::int32_t result;
std::int32_t operation;
std::uint32_t reserved;
const char* function;
const char* format;
ReportArguments arguments;
};
using ReportHandler = void (APS5_VABI*)(const ReportInfo* report, void* userData);
static_assert(sizeof(ReportArguments) == 24);
static_assert(offsetof(ReportArguments, generalOffset) == 0);
static_assert(offsetof(ReportArguments, floatingOffset) == 4);
static_assert(offsetof(ReportArguments, overflowArguments) == 8);
static_assert(offsetof(ReportArguments, savedRegisters) == 16);
static_assert(sizeof(ReportInfo) == 72);
static_assert(offsetof(ReportInfo, size) == 0);
static_assert(offsetof(ReportInfo, handle) == 8);
static_assert(offsetof(ReportInfo, category) == 16);
static_assert(offsetof(ReportInfo, result) == 20);
static_assert(offsetof(ReportInfo, operation) == 24);
static_assert(offsetof(ReportInfo, function) == 32);
static_assert(offsetof(ReportInfo, format) == 40);
static_assert(offsetof(ReportInfo, arguments) == 48);
struct Attribute {
std::uint32_t id;
std::uint32_t reserved0;
@@ -32,13 +137,38 @@ struct Attribute {
std::uint32_t reserved2;
};
struct PathPoint {
AudioPropagationStructDescriptor desc;
AudioPropagationHandle room;
float gain;
float position[3];
std::byte opaque[8];
};
struct AudioPath {
AudioPropagationStructDescriptor desc;
PathPoint points[4];
std::uint32_t pointCount;
std::uint32_t flags;
std::byte opaque[8];
};
static_assert(sizeof(PathPoint) == 48);
static_assert(offsetof(PathPoint, room) == 16);
static_assert(offsetof(PathPoint, gain) == 24);
static_assert(offsetof(PathPoint, position) == 28);
static_assert(sizeof(AudioPath) == 224);
static_assert(offsetof(AudioPath, points) == 16);
static_assert(offsetof(AudioPath, pointCount) == 208);
static_assert(offsetof(AudioPath, flags) == 212);
struct RenderInfo {
AudioPropagationStructDescriptor desc;
AudioPropagationHandle source;
void* output;
float* output;
std::size_t outputSize;
std::uint32_t format;
std::uint32_t reserved;
std::uint32_t channelCount;
};
struct PortalParams {
@@ -53,6 +183,7 @@ static_assert(sizeof(Attribute) == 0x20);
static_assert(sizeof(RenderInfo) == RenderInfoSize);
static_assert(offsetof(RenderInfo, source) == 0x10);
static_assert(offsetof(RenderInfo, format) == 0x28);
static_assert(offsetof(RenderInfo, channelCount) == 0x2c);
static_assert(sizeof(PortalParams) == PortalParamsSize);
static_assert(offsetof(PortalParams, rooms) == 0x50);
@@ -60,8 +191,8 @@ static_assert(offsetof(PortalParams, rooms) == 0x50);
extern "C" {
std::int32_t APS5_VABI sceAudioPropagationSystemQueryMemory(const void* options, AudioPropagationSystemMemory* outMemory);
std::int32_t APS5_VABI sceAudioPropagationSystemCreate(const void* options, const AudioPropagationSystemMemory* memory, AudioPropagationHandle* outSystem);
std::int32_t APS5_VABI sceAudioPropagationSystemQueryMemory(const AudioPropagation::SystemOptions* options, AudioPropagationSystemMemory* outMemory);
std::int32_t APS5_VABI sceAudioPropagationSystemCreate(const AudioPropagation::SystemOptions* options, const AudioPropagationSystemMemory* memory, AudioPropagationHandle* outSystem);
std::int32_t APS5_VABI sceAudioPropagationSystemDestroy(AudioPropagationHandle system);
std::int32_t APS5_VABI sceAudioPropagationSystemSetAttributes(AudioPropagationHandle system, const AudioPropagation::Attribute* attributes, std::uint32_t count);
std::int32_t APS5_VABI sceAudioPropagationSystemRegisterMaterial(AudioPropagationHandle system, const AudioPropagationStructDescriptor* material, AudioPropagationHandle* outMaterial);
@@ -79,10 +210,10 @@ std::int32_t APS5_VABI sceAudioPropagationSourceSetAttributes(AudioPropagationHa
std::int32_t APS5_VABI sceAudioPropagationSourceGetAudioPathCount(AudioPropagationHandle source, std::uint32_t* count);
std::int32_t APS5_VABI sceAudioPropagationSourceGetAudioPath(AudioPropagationHandle source, std::uint32_t index, AudioPropagationHandle* outPath);
std::int32_t APS5_VABI sceAudioPropagationSourceGetRays(AudioPropagationHandle source, void* rays, std::uint32_t* count);
std::int32_t APS5_VABI sceAudioPropagationSourceCalculateAudioPaths(AudioPropagationHandle source, const void* rays, std::uint32_t rayCount, std::uint32_t flags, void* paths, std::uint32_t pathCount);
std::int32_t APS5_VABI sceAudioPropagationSourceCalculateAudioPaths(AudioPropagationHandle source, const void* rays, std::uint32_t rayCount, std::uint32_t reflectionOrder, void* paths, std::uint32_t pathCount);
std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPaths(AudioPropagationHandle source, const void* entries, std::uint32_t count);
std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPath(AudioPropagationHandle path, const void* data, float gain);
std::int32_t APS5_VABI sceAudioPropagationSourceRender(AudioPropagationHandle system, const AudioPropagation::RenderInfo* infos, std::uint32_t count);
std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPath(AudioPropagationHandle path, const AudioPropagation::AudioPath* data, float gain);
std::int32_t APS5_VABI sceAudioPropagationSourceRender(AudioPropagationHandle system, AudioPropagation::RenderInfo* infos, std::uint32_t count);
}
+66 -31
View File
@@ -2,6 +2,7 @@
#include "prx/libc/include/General.hpp"
#include <cstdint>
#include <cmath>
#include <cstring>
#include <mutex>
#include <stdexcept>
@@ -22,7 +23,8 @@ constexpr std::uint64_t SystemMagic = [] {
struct SystemState {
std::uint64_t magic;
std::uint64_t reserved;
std::uint32_t frameSamples;
std::uint32_t maxSources;
};
enum class Kind { Room, Portal, Source, Material };
@@ -115,23 +117,49 @@ void requireNoPropagation(bool none, const char* function) {
extern "C" {
std::int32_t APS5_VABI sceAudioPropagationSystemQueryMemory(const void* options, AudioPropagationSystemMemory* outMemory) {
require(options != nullptr, __func__);
requireDescriptor(outMemory != nullptr ? &outMemory->desc : nullptr, SystemMemoryId, SystemMemorySize, __func__);
outMemory->size_cpu_mem = sizeof(SystemState);
std::int32_t APS5_VABI sceAudioPropagationSystemQueryMemory(const SystemOptions* options, AudioPropagationSystemMemory* outMemory) {
if (!options || !outMemory) return ErrorNullPointer;
if (outMemory->desc.id != SystemMemoryId || outMemory->desc.size != SystemMemorySize) return static_cast<std::int32_t>(0x8A700007);
outMemory->p_cpu_mem = nullptr;
outMemory->size_cpu_mem = 0;
outMemory->p_gpu_mem = nullptr;
outMemory->size_gpu_mem = 0;
if (options->desc.id != SystemOptionsId || options->desc.size != SystemOptionsSize) return static_cast<std::int32_t>(0x8A700007);
if (options->maxSources == 0 || options->maxSources > 64 || options->maxMaterials == 0 || options->maxMaterials > 64 || options->parameter24 != 6 || options->reflectionOrder == 0 || options->reflectionOrder > 2) return ErrorInvalidArgument;
const auto frameSamples = options->frameSamples;
if (frameSamples != 256 && frameSamples != 512 && frameSamples != 1024 && frameSamples != 2048 && frameSamples != 4096) return ErrorInvalidArgument;
if (!(options->propagationSpeed > 0x1p-23f) || options->propagationSpeed > 1372.0f || !(options->maxPathLength > 0x1p-23f) || options->maxPathLength > 1372.0f || options->flags > 31 || ((options->flags & 1) && frameSamples != 512)) return ErrorInvalidArgument;
if (options->flags & 1) throw std::runtime_error(std::string(__func__) + ": GPU memory requirements depend on an unsupported native runtime query");
const std::uint64_t sources = options->maxSources;
const std::uint64_t paths = sources * (options->reflectionOrder == 1 ? 6 : 36);
const float delaySeconds = options->maxPathLength / options->propagationSpeed;
const float delaySamples = delaySeconds * 48000.0f;
const auto delayCapacity = static_cast<std::uint64_t>(std::ceil(delaySamples));
std::uint64_t bytes = sources * 1024 + paths * (32 * (options->reflectionOrder + 2) + 8928);
bytes += frameSamples == 256 ? 2048 : 4 * frameSamples;
bytes += 4 * delayCapacity * sources;
bytes += bytes & 15;
bytes += 160 * paths + 12352 + 4208 * options->maxMaterials;
if (frameSamples == 256) bytes += sources * ((options->flags & 2) ? 36896 : 16416);
if (options->flags & 4) bytes += 2000656;
if (options->flags & 8) bytes += 288 * paths;
if (options->flags & 16) bytes += 144 * paths;
outMemory->size_cpu_mem = bytes + 5368 - ((bytes & 15) ^ 8);
return 0;
}
std::int32_t APS5_VABI sceAudioPropagationSystemCreate(const void* options, const AudioPropagationSystemMemory* memory, AudioPropagationHandle* outSystem) {
std::int32_t APS5_VABI sceAudioPropagationSystemCreate(const SystemOptions* options, const AudioPropagationSystemMemory* memory, AudioPropagationHandle* outSystem) {
require(options != nullptr && outSystem != nullptr, __func__);
requireDescriptor(memory != nullptr ? &memory->desc : nullptr, SystemMemoryId, SystemMemorySize, __func__);
auto requiredMemory = *memory;
const auto result = sceAudioPropagationSystemQueryMemory(options, &requiredMemory);
if (result != 0) return result;
const auto address = reinterpret_cast<std::uintptr_t>(memory->p_cpu_mem);
require(address != 0 && address % alignof(SystemState) == 0 && memory->size_cpu_mem >= sizeof(SystemState) && memory->size_gpu_mem == 0, __func__);
require(address != 0 && address % alignof(SystemState) == 0 && memory->size_cpu_mem >= requiredMemory.size_cpu_mem && memory->size_gpu_mem == 0, __func__);
auto& state = registry();
std::lock_guard lock(state.mutex);
require(!state.systems.contains(address), __func__);
*static_cast<SystemState*>(memory->p_cpu_mem) = SystemState{SystemMagic, 0};
*static_cast<SystemState*>(memory->p_cpu_mem) = SystemState{SystemMagic, options->frameSamples, options->maxSources};
state.systems.insert(address);
*outSystem = address;
return 0;
@@ -258,9 +286,11 @@ std::int32_t APS5_VABI sceAudioPropagationSourceGetAudioPathCount(AudioPropagati
std::int32_t APS5_VABI sceAudioPropagationSourceGetAudioPath(AudioPropagationHandle source, std::uint32_t index, AudioPropagationHandle* outPath) {
auto& state = registry();
std::lock_guard lock(state.mutex);
requireObject(state, source, Kind::Source, __func__);
static_cast<void>(outPath);
throw std::invalid_argument(std::string(__func__) + ": index " + std::to_string(index) + " out of range (0 audio paths)");
const auto found = state.objects.find(source);
if (found == state.objects.end() || found->second.kind != Kind::Source || !liveSystem(state, found->second.system)) return ErrorInvalidHandle;
if (!outPath) return ErrorNullPointer;
static_cast<void>(index);
return ErrorInvalidArgument;
}
std::int32_t APS5_VABI sceAudioPropagationSourceGetRays(AudioPropagationHandle source, void* rays, std::uint32_t* count) {
@@ -272,15 +302,14 @@ std::int32_t APS5_VABI sceAudioPropagationSourceGetRays(AudioPropagationHandle s
return 0;
}
std::int32_t APS5_VABI sceAudioPropagationSourceCalculateAudioPaths(AudioPropagationHandle source, const void* rays, std::uint32_t rayCount, std::uint32_t flags, void* paths, std::uint32_t pathCount) {
std::int32_t APS5_VABI sceAudioPropagationSourceCalculateAudioPaths(AudioPropagationHandle source, const void* rays, std::uint32_t rayCount, std::uint32_t reflectionOrder, void* paths, std::uint32_t pathCount) {
auto& state = registry();
std::lock_guard lock(state.mutex);
requireObject(state, source, Kind::Source, __func__);
static_cast<void>(rays);
static_cast<void>(flags);
static_cast<void>(paths);
requireNoPropagation(rayCount == 0 && pathCount == 0, __func__);
return 0;
const auto found = state.objects.find(source);
if (found == state.objects.end() || found->second.kind != Kind::Source || !liveSystem(state, found->second.system)) return ErrorInvalidHandle;
if (!rays || !paths) return ErrorNullPointer;
if (rayCount != 6 || reflectionOrder == 0 || pathCount == 0) return ErrorInvalidArgument;
throw std::runtime_error(__func__);
}
std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPaths(AudioPropagationHandle source, const void* entries, std::uint32_t count) {
@@ -292,7 +321,7 @@ std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPaths(AudioPropagationHa
return 0;
}
std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPath(AudioPropagationHandle path, const void* data, float gain) {
std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPath(AudioPropagationHandle path, const AudioPath* data, float gain) {
static_cast<void>(path);
static_cast<void>(data);
static_cast<void>(gain);
@@ -300,47 +329,53 @@ std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPath(AudioPropagationHan
return 0;
}
std::int32_t APS5_VABI sceAudioPropagationSourceRender(AudioPropagationHandle system, const RenderInfo* infos, std::uint32_t count) {
std::int32_t APS5_VABI sceAudioPropagationSourceRender(AudioPropagationHandle system, RenderInfo* infos, std::uint32_t count) {
require(infos != nullptr && count != 0, __func__);
auto& state = registry();
std::lock_guard lock(state.mutex);
requireSystem(state, system, __func__);
const auto& configuration = *reinterpret_cast<const SystemState*>(system);
require(count <= configuration.maxSources, __func__);
const auto requiredBytes = static_cast<std::size_t>(configuration.frameSamples) * 16 * sizeof(float);
for (std::uint32_t index = 0; index < count; ++index) {
const auto& info = infos[index];
requireDescriptor(&info.desc, RenderInfoId, RenderInfoSize, __func__);
require(requireObject(state, info.source, Kind::Source, __func__).system == system, __func__);
require(info.output != nullptr && info.outputSize != 0, __func__);
require(info.output != nullptr && info.outputSize >= requiredBytes, __func__);
if (info.format != RenderFormat) throw std::runtime_error(std::string(__func__) + ": render format " + std::to_string(info.format) + " is unknown");
}
for (std::uint32_t index = 0; index < count; ++index) {
infos[index].channelCount = 0;
}
return 0;
}
int APS5_VABI sceAudioPropagationPathGetNumPoints() {
int APS5_VABI sceAudioPropagationPathGetNumPoints(AudioPropagationHandle path, std::uint32_t* count) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAudioPropagationReportApi() {
int APS5_VABI sceAudioPropagationReportApi(std::uint32_t category, AudioPropagationHandle handle, std::int32_t result, std::int32_t operation, const char* function, const char* format, ...) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAudioPropagationResetAttributes() {
int APS5_VABI sceAudioPropagationResetAttributes(Attribute* attributes, std::uint32_t count) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAudioPropagationSourceQueryInfo() {
int APS5_VABI sceAudioPropagationSourceQueryInfo(AudioPropagationHandle source, SourceInfo* info) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAudioPropagationSystemLock() {
int APS5_VABI sceAudioPropagationSystemLock(AudioPropagationHandle system) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAudioPropagationSystemQueryInfo() {
int APS5_VABI sceAudioPropagationSystemQueryInfo(AudioPropagationHandle system, SystemInfo* info, std::size_t size) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
@@ -395,22 +430,22 @@ int APS5_VABI sceAudioPropagationSystemOptionInit() {
return 0;
}
int APS5_VABI sceAudioPropagationReportRegisterHandler() {
int APS5_VABI sceAudioPropagationReportRegisterHandler(std::uint32_t category, ReportHandler handler, void* userData, ReportRegistration** registration) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAudioPropagationReportUnregisterHandler() {
int APS5_VABI sceAudioPropagationReportUnregisterHandler(ReportRegistration* registration) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAudioPropagationSystemEnumHandles() {
int APS5_VABI sceAudioPropagationSystemEnumHandles(AudioPropagationHandle* handles, std::uint32_t capacity) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAudioPropagationSystemUnlock() {
int APS5_VABI sceAudioPropagationSystemUnlock(AudioPropagationHandle system) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
+14 -4
View File
@@ -1,6 +1,8 @@
#include <cstdint>
#include <cstddef>
#include <array>
#include <cstring>
#include <stdexcept>
#include <memory>
#include <mutex>
#include "SceTypes.hpp"
@@ -88,7 +90,7 @@ std::int32_t validateCtrl(const AudiodecCtrl* ctrl, std::uint32_t codecType) {
if (infoSize != sizeof(AudiodecMp3Info)) return ERROR_INVALID_BSI_INFO_SIZE;
break;
default:
if (paramSize < sizeof(AudiodecParamM4aac)) return ERROR_INVALID_PARAM_SIZE;
if (paramSize != sizeof(AudiodecParamM4aac) && paramSize != 28 && paramSize != 32) return ERROR_INVALID_PARAM_SIZE;
if (infoSize != sizeof(AudiodecM4aacInfo)) return ERROR_INVALID_BSI_INFO_SIZE;
break;
}
@@ -133,12 +135,20 @@ void fillMp3Info(AudiodecMp3Info* info, const Audiodec::Mp3Header& header) {
}
std::int32_t createAac(const AudiodecParamM4aac* param, AudiodecM4aacInfo* info, Instance& instance) {
std::uint32_t extensionMode = 0;
std::uint32_t outputMode = 0;
const auto* bytes = reinterpret_cast<const std::byte*>(param);
if (param->ui_size >= 28) std::memcpy(&extensionMode, bytes + 24, sizeof(extensionMode));
if (extensionMode > 1) return static_cast<std::int32_t>(0x807F0305);
if (param->ui_size == 32) std::memcpy(&outputMode, bytes + 28, sizeof(outputMode));
if (outputMode > 2) return static_cast<std::int32_t>(0x807F0306);
if (extensionMode != 0 || outputMode != 0) throw std::runtime_error("M4AAC extended decoding modes are not implemented");
if (param->ui_config_number != M4AAC_CONFIG_ADTS && param->ui_config_number != M4AAC_CONFIG_RAW) return ERROR_M4AAC_INVALID_CONFIG_NUMBER;
const std::uint32_t sampleRate = Audiodec::AacSampleRate(param->ui_sampling_freq_index);
if (param->ui_config_number == M4AAC_CONFIG_RAW && sampleRate == 0) return ERROR_M4AAC_INVALID_SAMPLING_FREQ;
if (param->ui_max_channels > 8) return ERROR_M4AAC_INVALID_MAX_CHANNELS;
if (param->ui_enable_heaac > 1) return ERROR_M4AAC_INVALID_ENABLE_HEAAC;
const std::uint32_t channels = param->ui_max_channels == 0 ? 2 : param->ui_max_channels;
if (param->ui_max_channels != 1 && param->ui_max_channels != 2 && param->ui_max_channels != 6 && param->ui_max_channels != 8) return ERROR_M4AAC_INVALID_MAX_CHANNELS;
if (param->ui_enable_heaac > 1 || (param->ui_enable_heaac == 1 && param->ui_max_channels > 2)) return ERROR_M4AAC_INVALID_ENABLE_HEAAC;
const std::uint32_t channels = param->ui_max_channels;
instance.decoder = Audiodec::CreateAac(param->i_bw_pcm, param->ui_config_number == M4AAC_CONFIG_ADTS, param->ui_sampling_freq_index, channels, param->ui_enable_heaac == 1);
instance.channels = channels;
instance.sampleRate = sampleRate;
@@ -3,17 +3,37 @@
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
struct AutoMounterUsbDeviceInfo {
char deviceName[128];
std::byte metadata[22];
std::byte data0[256];
std::byte padding[2];
std::uint64_t data0Size;
std::byte data1[256];
std::uint64_t data1Size;
std::byte data2[256];
std::uint64_t data2Size;
};
static_assert(sizeof(AutoMounterUsbDeviceInfo) == 944);
static_assert(offsetof(AutoMounterUsbDeviceInfo, data0) == 150);
static_assert(offsetof(AutoMounterUsbDeviceInfo, data0Size) == 408);
static_assert(offsetof(AutoMounterUsbDeviceInfo, data1) == 416);
static_assert(offsetof(AutoMounterUsbDeviceInfo, data1Size) == 672);
static_assert(offsetof(AutoMounterUsbDeviceInfo, data2) == 680);
static_assert(offsetof(AutoMounterUsbDeviceInfo, data2Size) == 936);
extern "C" {
int APS5_VABI sceAutoMounterClientGetUsbDeviceInfo() {
int APS5_VABI sceAutoMounterClientGetUsbDeviceInfo(const char* deviceName, AutoMounterUsbDeviceInfo* info) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAutoMounterClientGetUsbDeviceList() {
int APS5_VABI sceAutoMounterClientGetUsbDeviceList(char (*deviceNames)[64], std::int32_t* count) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAutoMounterClientInit() {
int APS5_VABI sceAutoMounterClientInit(PthreadAttr* threadAttr) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
+3 -4
View File
@@ -175,10 +175,9 @@ int APS5_VABI sceAvPlayerStart(AvPlayerInternal* h) {
return ToPlayer(h)->Start();
}
int APS5_VABI sceAvPlayerStartEx(AvPlayerInternal* h, const void* start_info_ex) {
(void)start_info_ex;
if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS;
return ToPlayer(h)->Start();
int APS5_VABI sceAvPlayerStartEx(AvPlayerInternal* handle, const StartInfo* startInfo) {
if (handle == nullptr || startInfo == nullptr || startInfo->size != sizeof(*startInfo)) return SCE_AVPLAYER_ERROR_INVALID_PARAMS;
return ToPlayer(handle)->Start(startInfo->startTime);
}
int APS5_VABI sceAvPlayerStop(AvPlayerInternal* h) {
@@ -51,6 +51,13 @@ public:
virtual void OnError() = 0;
};
struct StartInfo {
std::uint64_t size;
std::uint64_t startTime;
};
static_assert(sizeof(StartInfo) == 16);
struct SourceSettings {
AvPlayerMemAllocator memory;
AvPlayerFileReplacement file;
@@ -66,7 +73,7 @@ public:
virtual bool EnableStream(std::uint32_t index) = 0;
virtual bool DisableStream(std::uint32_t index) = 0;
virtual bool ChangeStream(std::uint32_t from, std::uint32_t to) = 0;
virtual int Start() = 0;
virtual int Start(std::uint64_t startTime) = 0;
virtual void Stop() = 0;
virtual void Pause() = 0;
virtual void Resume() = 0;
@@ -103,7 +110,7 @@ public:
int EnableStream(std::uint32_t index);
int DisableStream(std::uint32_t index);
int ChangeStream(std::uint32_t from, std::uint32_t to);
int Start();
int Start(std::uint64_t startTime = 0);
int Stop();
int Pause();
int Resume();
@@ -136,7 +143,7 @@ private:
void autoStart();
void checkEndOfFile();
bool readyLocked() const;
int startLocked();
int startLocked(std::uint64_t startTime = 0);
int stopLocked();
AvPlayerFileReplacement file{};
+4 -4
View File
@@ -265,15 +265,15 @@ int Player::ChangeStream(std::uint32_t from, std::uint32_t to) {
return SCE_OK;
}
int Player::Start() {
int Player::Start(std::uint64_t startTime) {
std::lock_guard lock(mutex);
return startLocked();
return startLocked(startTime);
}
int Player::startLocked() {
int Player::startLocked(std::uint64_t startTime) {
if (!source) return SCE_AVPLAYER_ERROR_OPERATION_FAILED;
if (state != State::Ready && state != State::Stop && stopLocked() != SCE_OK) return SCE_AVPLAYER_ERROR_OPERATION_FAILED;
const int result = source->Start();
const int result = source->Start(startTime);
if (result != SCE_OK) {
previous = state;
state = State::Error;
+3 -2
View File
@@ -327,7 +327,8 @@ public:
return true;
}
int Start() override {
int Start(std::uint64_t startTime) override {
if (duration != 0 && startTime > duration) return SCE_AVPLAYER_ERROR_OPERATION_FAILED;
if (video.stream < 0 && audio.stream < 0) return SCE_AVPLAYER_ERROR_OPERATION_FAILED;
for (auto* decoder : {&video, &audio}) {
if (decoder->stream < 0) continue;
@@ -338,7 +339,7 @@ public:
}
}
addVideoDecodeAheadBuffers();
const auto start = startOffset;
const auto start = startTime != 0 ? startTime : startOffset;
startOffset = 0;
seek(start);
{
+52 -16
View File
@@ -3,9 +3,45 @@
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
struct BgftInitParam {
void* heap;
uint64_t heapSize;
};
struct BgftProgress;
struct BgftStorageTaskParam {
std::uint32_t value0;
std::uint32_t value4;
const char* contentId;
const char* text16;
std::byte unused24[8];
const char* text32;
const char* text40;
const char* text48;
std::uint32_t value56;
std::byte padding60[4];
const char* text64;
const char* text72;
const char* text80;
const char* text88;
std::uint64_t value96;
std::uint32_t value104;
};
static_assert(sizeof(BgftStorageTaskParam) == 112);
static_assert(offsetof(BgftStorageTaskParam, contentId) == 8);
static_assert(offsetof(BgftStorageTaskParam, text32) == 32);
static_assert(offsetof(BgftStorageTaskParam, value56) == 56);
static_assert(offsetof(BgftStorageTaskParam, text64) == 64);
static_assert(offsetof(BgftStorageTaskParam, value96) == 96);
static_assert(offsetof(BgftStorageTaskParam, value104) == 104);
static_assert(sizeof(BgftInitParam) == 16);
static_assert(offsetof(BgftInitParam, heapSize) == 8);
extern "C" {
int APS5_VABI sceBgftServiceInit(void* params) {
int APS5_VABI sceBgftServiceInit(const BgftInitParam* params) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
@@ -29,11 +65,11 @@ int APS5_VABI sceBgftServiceDownloadStopTaskAll() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceBgftServiceDownloadFindActivePatchTask() {
int APS5_VABI sceBgftServiceDownloadFindActivePatchTask(const char* contentId, int32_t* taskId) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceBgftServiceDownloadFindActivePupTask() {
int APS5_VABI sceBgftServiceDownloadFindActivePupTask(int32_t* taskId) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
@@ -41,7 +77,7 @@ int APS5_VABI sceBgftServiceDownloadFindTaskByContentId(const char* contentId, i
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceBgftServiceDownloadGetProgress(int32_t taskId, void* progress) {
int APS5_VABI sceBgftServiceDownloadGetProgress(int32_t taskId, BgftProgress* progress) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
@@ -61,15 +97,15 @@ int APS5_VABI sceBgftServiceDownloadStopTask(int32_t taskId) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceBgftServiceIntDownloadFindActiveGameAndGameAcTask() {
int APS5_VABI sceBgftServiceIntDownloadFindActiveGameAndGameAcTask(const char* contentId, int32_t* taskId) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceBgftServiceIntDownloadFindActiveTask() {
int APS5_VABI sceBgftServiceIntDownloadFindActiveTask(const char* contentId, int32_t subType, int32_t* taskId) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceBgftServiceIntDownloadGetGameAndGameAcProgress() {
int APS5_VABI sceBgftServiceIntDownloadGetGameAndGameAcProgress(const char* contentId, BgftProgress* progress) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
@@ -77,11 +113,11 @@ int APS5_VABI sceBgftServiceIntDownloadGetPatchProgress() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceBgftServiceIntDownloadGetPlayGoProgress() {
int APS5_VABI sceBgftServiceIntDownloadGetPlayGoProgress(const char* contentId, BgftProgress* progress) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceBgftServiceIntDownloadGetProgress() {
int APS5_VABI sceBgftServiceIntDownloadGetProgress(int32_t taskId, BgftProgress* progress) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
@@ -89,31 +125,31 @@ int APS5_VABI sceBgftServiceIntDownloadGetTaskInfo() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceBgftServiceIntDownloadPauseTask() {
int APS5_VABI sceBgftServiceIntDownloadPauseTask(int32_t taskId) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceBgftServiceIntDownloadRegisterTaskByStorageEx() {
int APS5_VABI sceBgftServiceIntDownloadRegisterTaskByStorageEx(const BgftStorageTaskParam* params, int32_t* taskId) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceBgftServiceIntDownloadResumeTask() {
int APS5_VABI sceBgftServiceIntDownloadResumeTask(int32_t taskId) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceBgftServiceIntDownloadStopTask() {
int APS5_VABI sceBgftServiceIntDownloadStopTask(int32_t taskId) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceBgftServiceIntDownloadStartTask() {
int APS5_VABI sceBgftServiceIntDownloadStartTask(int32_t taskId) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceBgftServiceIntDownloadUnregisterTask() {
int APS5_VABI sceBgftServiceIntDownloadUnregisterTask(int32_t taskId) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceBgftServiceIntInit() {
int APS5_VABI sceBgftServiceIntInit(const BgftInitParam* params) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
+83 -15
View File
@@ -4,47 +4,115 @@
#include "prx/libc/include/General.hpp"
#include <string>
#include <mutex>
struct BluetoothHidThreadParam {
std::int32_t priority;
std::uint32_t padding;
std::uint64_t affinityMask;
std::uint32_t reserved;
std::uint32_t tailPadding;
};
struct BluetoothHidParam {
const BluetoothHidThreadParam* threadParam;
std::uint32_t reserved;
std::uint32_t padding;
};
struct BluetoothHidEvent {
std::uint64_t deviceHandle;
std::uint16_t vendorId;
std::uint16_t productId;
std::uint8_t type;
std::uint8_t status;
std::uint16_t reportSize;
std::uint8_t reserved[8];
std::uint8_t report[78];
std::uint8_t padding[2];
};
struct BluetoothHidDeviceInfo;
struct BluetoothHidDeviceName;
using BluetoothHidCallback = void (APS5_VABI *)(const BluetoothHidEvent*, std::int32_t, void*);
static_assert(sizeof(BluetoothHidThreadParam) == 24);
static_assert(offsetof(BluetoothHidThreadParam, affinityMask) == 8);
static_assert(offsetof(BluetoothHidThreadParam, reserved) == 16);
static_assert(sizeof(BluetoothHidParam) == 16);
static_assert(sizeof(BluetoothHidEvent) == 104);
static_assert(offsetof(BluetoothHidEvent, reportSize) == 14);
static_assert(offsetof(BluetoothHidEvent, report) == 24);
namespace {
std::mutex stateMutex;
bool initialized = false;
bool callbackRegistered = false;
constexpr std::int32_t InvalidArgument = static_cast<std::int32_t>(0x81540001u);
constexpr std::int32_t NotInitialized = static_cast<std::int32_t>(0x81540004u);
constexpr std::int32_t NoCallback = static_cast<std::int32_t>(0x81540006u);
}
extern "C" {
std::int32_t APS5_VABI sceBluetoothHidInit() {
std::lock_guard lock(stateMutex);
initialized = true;
return 0;
}
std::int32_t APS5_VABI sceBluetoothHidRegisterCallback(void* callback, std::int32_t flags, void* param) {
(void)flags;
(void)param;
if (callback == nullptr) throw std::invalid_argument(std::string(__func__) + ": null callback");
std::int32_t APS5_VABI sceBluetoothHidRegisterCallback(BluetoothHidCallback callback, void* userData, const BluetoothHidParam* param) {
std::lock_guard lock(stateMutex);
if (!initialized) return NotInitialized;
if (callback == nullptr) return InvalidArgument;
if (param != nullptr) {
if (param->reserved != 0) return InvalidArgument;
if (param->threadParam != nullptr) {
if (param->threadParam->reserved != 0) return InvalidArgument;
throw std::runtime_error("sceBluetoothHidRegisterCallback: custom worker scheduling is not implemented");
}
}
(void)userData;
callbackRegistered = true;
return 0;
}
std::int32_t APS5_VABI sceBluetoothHidUnregisterCallback() {
std::lock_guard lock(stateMutex);
if (!initialized) return NotInitialized;
if (!callbackRegistered) return NoCallback;
callbackRegistered = false;
return 0;
}
std::int32_t APS5_VABI sceBluetoothHidRegisterDevice(std::uint16_t vendorId, std::uint16_t productId) {
std::lock_guard lock(stateMutex);
if (!initialized) return NotInitialized;
if (!callbackRegistered) return NoCallback;
(void)vendorId;
(void)productId;
return 0;
}
std::int32_t APS5_VABI sceBluetoothHidUnregisterDevice(std::uint16_t vendorId, std::uint16_t productId) {
std::lock_guard lock(stateMutex);
if (!initialized) return NotInitialized;
(void)vendorId;
(void)productId;
return 0;
}
int APS5_VABI sceBluetoothHidGetDeviceName() {
int APS5_VABI sceBluetoothHidGetDeviceName(std::uint64_t deviceHandle, BluetoothHidDeviceName* name) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceBluetoothHidGetInputReport() {
int APS5_VABI sceBluetoothHidGetInputReport(std::uint64_t deviceHandle, std::uint8_t reportId) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceBluetoothHidSetOutputReport() {
int APS5_VABI sceBluetoothHidSetOutputReport(std::uint64_t deviceHandle, std::uint8_t reportId, const void* data, std::size_t size) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
@@ -53,42 +121,42 @@ int APS5_VABI sceBluetoothHidDebugGetVersion() {
return 0x1e;
}
int APS5_VABI sceBluetoothHidDisconnectDevice() {
int APS5_VABI sceBluetoothHidDisconnectDevice(std::int32_t deviceId) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceBluetoothHidGetDeviceInfo() {
int APS5_VABI sceBluetoothHidGetDeviceInfo(std::int32_t deviceId, BluetoothHidDeviceInfo* info) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceBluetoothHidGetFeatureReport() {
int APS5_VABI sceBluetoothHidGetFeatureReport(std::uint64_t deviceHandle, std::uint8_t reportId) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceBluetoothHidGetReportDescriptor() {
int APS5_VABI sceBluetoothHidGetReportDescriptor(std::uint64_t deviceHandle, void* descriptor, std::size_t size) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceBluetoothHidInterruptOutput() {
int APS5_VABI sceBluetoothHidInterruptOutput(std::uint64_t deviceHandle, std::uint8_t reportId, const void* data, std::size_t size) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceBluetoothHidParamInitialize() {
int APS5_VABI sceBluetoothHidParamInitialize(BluetoothHidParam* param, const BluetoothHidThreadParam* threadParam) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceBluetoothHidSetFeatureReport() {
int APS5_VABI sceBluetoothHidSetFeatureReport(std::uint64_t deviceHandle, std::uint8_t reportId, const void* data, std::size_t size) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceBluetoothHidThreadParamInitialize() {
int APS5_VABI sceBluetoothHidThreadParamInitialize(BluetoothHidThreadParam* param, std::int32_t priority, std::uint64_t affinityMask) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
+2 -1
View File
@@ -1,11 +1,12 @@
add_library(libSceCdlgG2P SHARED EXCLUDE_FROM_ALL Export.cpp)
target_include_directories(libSceCdlgG2P PRIVATE ${LIBS_INCLUDE_DIR})
configure_windows_unwind(libSceCdlgG2P)
target_link_libraries(libSceCdlgG2P PRIVATE libSceUserService)
if(BUILD_TESTING)
add_test_executable(guest_g2p_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestG2PDialog.cpp)
target_include_directories(guest_g2p_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_g2p_dialog_tests PRIVATE libSceCdlgG2P)
target_link_libraries(guest_g2p_dialog_tests PRIVATE libSceCdlgG2P libSceUserService)
add_test(NAME guest_g2p_dialog COMMAND guest_g2p_dialog_tests)
if(WIN32)
set_tests_properties(guest_g2p_dialog PROPERTIES ENVIRONMENT_MODIFICATION
+36 -27
View File
@@ -1,10 +1,14 @@
#include "SceTypes.hpp"
#include <atomic>
#include <cstdint>
#include <cstddef>
#include <cstring>
#include "SceTypes.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include "prx/libSceCommonDialog/CommonDialog.hpp"
#include "prx/libc/include/General.hpp"
extern "C" int APS5_VABI sceUserServiceGetLoginUserIdList(UserServiceLoginUserIdList* users);
namespace {
constexpr int COMMON_DIALOG_STATUS_NONE = 0;
@@ -12,9 +16,10 @@ constexpr int COMMON_DIALOG_STATUS_INITIALIZED = 1;
constexpr int COMMON_DIALOG_STATUS_RUNNING = 2;
constexpr int COMMON_DIALOG_STATUS_FINISHED = 3;
constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast<int>(0x80B80003u);
constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x80B80004u);
constexpr int COMMON_DIALOG_ERROR_INVALID_ARGUMENT = static_cast<int>(0x80B8000Au);
constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast<int>(0x80B80005u);
constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast<int>(0x80B80007u);
constexpr int COMMON_DIALOG_ERROR_NOT_RUNNING = static_cast<int>(0x80B8000Bu);
constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast<int>(0x80B8000Du);
constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1;
@@ -25,47 +30,51 @@ std::atomic<int> g_status{COMMON_DIALOG_STATUS_NONE};
extern "C" {
int APS5_VABI sceG2PDialogInitialize(void) {
int expected = COMMON_DIALOG_STATUS_NONE;
if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_INITIALIZED)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED;
return 0;
int expected = COMMON_DIALOG_STATUS_NONE;
if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_INITIALIZED)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED;
return 0;
}
int APS5_VABI sceG2PDialogOpen(const void* param) {
const int status = g_status.load();
if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED;
if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY;
if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL;
g_status = COMMON_DIALOG_STATUS_FINISHED;
return 0;
int APS5_VABI sceG2PDialogOpen(const G2PDialogParam* param) {
if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL;
const int status = g_status.load();
if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED;
if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY;
if (!ValidateCommonDialogBaseParam(param->base) || param->size != sizeof(*param) || param->userId == -1 || !std::all_of(std::begin(param->reserved), std::end(param->reserved), [](std::uint8_t value) { return value == 0; })) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT;
UserServiceLoginUserIdList users{};
if (sceUserServiceGetLoginUserIdList(&users) != 0 || std::none_of(std::begin(users.user_id), std::end(users.user_id), [param](std::int32_t userId) { return userId != -1 && userId == param->userId; })) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT;
g_status = COMMON_DIALOG_STATUS_FINISHED;
return 0;
}
int APS5_VABI sceG2PDialogUpdateStatus(void) {
return g_status.load();
return g_status.load();
}
int APS5_VABI sceG2PDialogGetStatus(void) {
return g_status.load();
return g_status.load();
}
int APS5_VABI sceG2PDialogGetResult(void* result) {
const int status = g_status.load();
if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED;
if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL;
if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED;
const std::int32_t canceled = COMMON_DIALOG_RESULT_USER_CANCELED;
std::memcpy(result, &canceled, sizeof(canceled));
return 0;
int APS5_VABI sceG2PDialogGetResult(CommonDialogResultPrefix* result) {
if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL;
const int status = g_status.load();
if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED;
if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED;
result->result = COMMON_DIALOG_RESULT_USER_CANCELED;
return 0;
}
int APS5_VABI sceG2PDialogClose(void) {
if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED;
g_status = COMMON_DIALOG_STATUS_FINISHED;
return 0;
const int status = g_status.load();
if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED;
if (status != COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_NOT_RUNNING;
g_status = COMMON_DIALOG_STATUS_FINISHED;
return 0;
}
int APS5_VABI sceG2PDialogTerminate(void) {
if (g_status.exchange(COMMON_DIALOG_STATUS_NONE) == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED;
return 0;
if (g_status.exchange(COMMON_DIALOG_STATUS_NONE) == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED;
return 0;
}
}
@@ -1,6 +1,7 @@
add_library(libSceCdlgPlayerReview SHARED EXCLUDE_FROM_ALL Export.cpp)
target_include_directories(libSceCdlgPlayerReview PRIVATE ${LIBS_INCLUDE_DIR})
configure_windows_unwind(libSceCdlgPlayerReview)
target_link_libraries(libSceCdlgPlayerReview PRIVATE libSceUserService)
if(MINGW)
foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static)
@@ -11,7 +12,7 @@ endif()
if(BUILD_TESTING)
add_test_executable(guest_player_review_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestPlayerReviewDialog.cpp)
target_include_directories(guest_player_review_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_player_review_dialog_tests PRIVATE libSceCdlgPlayerReview)
target_link_libraries(guest_player_review_dialog_tests PRIVATE libSceCdlgPlayerReview libSceUserService)
add_test(NAME guest_player_review_dialog COMMAND guest_player_review_dialog_tests)
if(WIN32)
set_tests_properties(guest_player_review_dialog PROPERTIES ENVIRONMENT_MODIFICATION
@@ -1,8 +1,12 @@
#include "SceTypes.hpp"
#include <atomic>
#include <cstdint>
#include "SceTypes.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include "prx/libSceCommonDialog/CommonDialog.hpp"
#include "prx/libc/include/General.hpp"
extern "C" int APS5_VABI sceUserServiceGetLoginUserIdList(UserServiceLoginUserIdList* users);
namespace {
std::atomic<int> g_status{0};
@@ -10,8 +14,8 @@ constexpr int COMMON_DIALOG_STATUS_NONE = 0;
constexpr int COMMON_DIALOG_STATUS_RUNNING = 2;
constexpr int COMMON_DIALOG_STATUS_FINISHED = 3;
constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1;
constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x80B80004u);
constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast<int>(0x80B80003u);
constexpr int COMMON_DIALOG_ERROR_INVALID_ARGUMENT = static_cast<int>(0x80B8000Au);
constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast<int>(0x80B80005u);
constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast<int>(0x80B80007u);
constexpr int COMMON_DIALOG_ERROR_NOT_RUNNING = static_cast<int>(0x80B8000Bu);
@@ -35,17 +39,22 @@ int APS5_VABI scePlayerReviewDialogTerminate(void) {
return COMMON_DIALOG_ERROR_NOT_RUNNING;
}
int APS5_VABI scePlayerReviewDialogOpen(const void* param) {
int APS5_VABI scePlayerReviewDialogOpen(const PlayerReviewDialogParam* param) {
if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL;
const int status = g_status.load();
if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED;
if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY;
if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL;
if (!ValidateCommonDialogBaseParam(param->base) || param->size != sizeof(*param) || param->userId == -1 || !std::all_of(std::begin(param->reserved), std::end(param->reserved), [](std::uint8_t value) { return value == 0; }) || param->mode > 1) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT;
UserServiceLoginUserIdList users{};
if (sceUserServiceGetLoginUserIdList(&users) != 0 || std::none_of(std::begin(users.user_id), std::end(users.user_id), [param](std::int32_t userId) { return userId != -1 && userId == param->userId; })) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT;
g_status = COMMON_DIALOG_STATUS_FINISHED;
return 0;
}
int APS5_VABI scePlayerReviewDialogClose(void) {
if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED;
const int status = g_status.load();
if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED;
if (status != COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_NOT_RUNNING;
g_status = COMMON_DIALOG_STATUS_FINISHED;
return 0;
}
@@ -58,13 +67,13 @@ int APS5_VABI scePlayerReviewDialogUpdateStatus(void) {
return g_status.load();
}
int APS5_VABI scePlayerReviewDialogGetResult(void* result) {
int APS5_VABI scePlayerReviewDialogGetResult(CommonDialogResultPrefix* result) {
if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL;
const int status = g_status.load();
if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED;
if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL;
if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED;
*static_cast<std::int32_t*>(result) = COMMON_DIALOG_RESULT_USER_CANCELED;
return 0;
result->result = COMMON_DIALOG_RESULT_USER_CANCELED;
return COMMON_DIALOG_RESULT_USER_CANCELED;
}
}
@@ -1,6 +1,252 @@
#ifndef CORE_LIBS_PRX_LIBSCECOMMONDIALOG_COMMONDIALOG_HPP
#define CORE_LIBS_PRX_LIBSCECOMMONDIALOG_COMMONDIALOG_HPP
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <iterator>
struct CommonDialogBaseParam {
std::uint64_t size;
std::uint8_t reserved[36];
std::uint32_t magic;
};
struct NpAuthAuthorizedAppDialogParam {
CommonDialogBaseParam base;
std::uint64_t size;
std::int32_t userId;
std::uint32_t padding60;
const char* application;
const char* text;
std::uint32_t mode;
std::uint32_t padding84;
std::uint8_t reserved[32];
};
static_assert(sizeof(NpAuthAuthorizedAppDialogParam) == 120);
static_assert(offsetof(NpAuthAuthorizedAppDialogParam, application) == 64);
static_assert(offsetof(NpAuthAuthorizedAppDialogParam, text) == 72);
static_assert(offsetof(NpAuthAuthorizedAppDialogParam, mode) == 80);
static_assert(offsetof(NpAuthAuthorizedAppDialogParam, reserved) == 88);
struct VrSetupDialogParam {
std::uint64_t size;
std::uint8_t reserved8;
std::uint8_t reserved9;
std::uint8_t data[46];
};
static_assert(sizeof(VrSetupDialogParam) == 56);
struct UsbStorageDialogParam {
CommonDialogBaseParam base;
std::uint64_t size;
char path[1024];
std::int32_t userId;
std::uint32_t padding;
};
struct UsbStorageDialogResult {
std::uint64_t size;
std::int32_t result;
std::int32_t status;
std::uint8_t reserved[64];
};
static_assert(offsetof(UsbStorageDialogParam, path) == 56);
static_assert(offsetof(UsbStorageDialogParam, userId) == 1080);
static_assert(sizeof(UsbStorageDialogResult) == 80);
static_assert(offsetof(UsbStorageDialogResult, result) == 8);
static_assert(offsetof(UsbStorageDialogResult, status) == 12);
struct LoginDialogParam {
std::uint32_t size;
std::uint32_t mode;
std::int32_t userIds[9];
std::uint32_t reserved[5];
};
struct LoginDialogResult {
std::int32_t result;
std::int32_t errorCode;
std::uint8_t data[8];
};
static_assert(sizeof(LoginDialogParam) == 64);
static_assert(offsetof(LoginDialogParam, userIds) == 8);
static_assert(offsetof(LoginDialogParam, reserved) == 44);
static_assert(sizeof(LoginDialogResult) == 16);
static_assert(offsetof(LoginDialogResult, errorCode) == 4);
struct NetCtlApDialogParam {
CommonDialogBaseParam base;
std::uint64_t size;
std::uint32_t value56;
std::uint32_t reserved60;
char option[9];
std::uint8_t reserved[31];
};
struct NetCtlApDialogResult {
std::int32_t result;
std::uint8_t reserved[36];
};
static_assert(sizeof(NetCtlApDialogParam) == 104);
static_assert(offsetof(NetCtlApDialogParam, option) == 64);
static_assert(offsetof(NetCtlApDialogParam, reserved) == 73);
static_assert(sizeof(NetCtlApDialogResult) == 40);
struct G2PDialogParam {
CommonDialogBaseParam base;
std::int32_t userId;
std::uint32_t padding;
std::uint64_t size;
std::uint8_t reserved[64];
};
struct PlayerReviewDialogParam {
CommonDialogBaseParam base;
std::uint64_t size;
std::int32_t userId;
std::uint8_t target[40];
std::uint32_t mode;
std::uint8_t reserved[64];
};
struct PlayerSelectionDialogPlayer {
std::uint64_t accountId;
std::uint32_t option0;
std::uint32_t option1;
std::uint32_t mode;
std::uint8_t reserved[64];
std::uint32_t padding;
};
struct PlayerSelectionDialogParam {
CommonDialogBaseParam base;
std::uint64_t size;
std::int32_t userId;
char title[64];
std::uint32_t maxPlayers;
std::uint32_t mode;
std::uint32_t padding0;
const PlayerSelectionDialogPlayer* players;
std::uint32_t playerCount;
std::uint8_t reserved[64];
std::uint32_t padding1;
};
struct PlayerSelectionDialogResult {
std::int32_t result;
std::uint32_t option;
std::uint64_t* accountIds;
std::uint32_t count;
std::uint32_t padding;
};
static_assert(sizeof(PlayerSelectionDialogPlayer) == 88);
static_assert(offsetof(PlayerSelectionDialogPlayer, reserved) == 20);
static_assert(sizeof(PlayerSelectionDialogParam) == 216);
static_assert(offsetof(PlayerSelectionDialogParam, maxPlayers) == 124);
static_assert(offsetof(PlayerSelectionDialogParam, players) == 136);
static_assert(offsetof(PlayerSelectionDialogParam, reserved) == 148);
static_assert(sizeof(PlayerSelectionDialogResult) == 24);
static_assert(offsetof(PlayerSelectionDialogResult, accountIds) == 8);
struct NpCommerceDialogResult {
std::int32_t result;
std::uint8_t authorized;
std::uint8_t padding[3];
std::uint64_t userData;
std::uint8_t reserved[32];
};
static_assert(sizeof(NpCommerceDialogResult) == 48);
static_assert(offsetof(NpCommerceDialogResult, userData) == 8);
struct NpCommerceDialogParam {
CommonDialogBaseParam base;
std::uint32_t size;
std::int32_t userId;
std::uint32_t mode;
std::uint32_t option;
const char* const* products;
std::uint32_t productCount;
std::uint32_t padding;
std::uint64_t argument;
std::uint64_t userData;
std::uint8_t reserved[32];
};
static_assert(sizeof(NpCommerceDialogParam) == 128);
static_assert(offsetof(NpCommerceDialogParam, products) == 64);
static_assert(offsetof(NpCommerceDialogParam, userData) == 88);
struct NpCommerceDialogParam2 {
CommonDialogBaseParam base;
std::uint32_t size;
std::int32_t userId;
std::uint32_t mode;
std::uint32_t option;
std::uint32_t contentMode;
std::uint32_t padding0;
const char* const* products;
std::uint32_t productCount;
std::uint32_t padding1;
std::uint64_t argument;
std::uint64_t userData;
std::uint8_t reserved[32];
};
static_assert(sizeof(NpCommerceDialogParam2) == 136);
static_assert(offsetof(NpCommerceDialogParam2, products) == 72);
static_assert(offsetof(NpCommerceDialogParam2, userData) == 96);
struct WebBrowserDialogExtendedResult {
std::uint64_t size;
std::uint32_t type;
char content[2048];
std::uint8_t reserved0[4];
char* extendedContent;
std::uint32_t extendedCapacity;
std::uint8_t reserved1[20];
};
struct WebBrowserDialogResult {
std::int32_t result;
std::uint32_t padding;
WebBrowserDialogExtendedResult* extended;
std::uint8_t reserved[240];
};
static_assert(sizeof(WebBrowserDialogExtendedResult) == 2096);
static_assert(offsetof(WebBrowserDialogExtendedResult, extendedContent) == 2064);
static_assert(sizeof(WebBrowserDialogResult) == 256);
struct WebBrowserDialogAllowList {
std::uint64_t size;
const char* addresses[20];
std::uint8_t reserved[256];
};
static_assert(sizeof(WebBrowserDialogAllowList) == 424);
struct CommonDialogResultPrefix {
std::uint8_t untouched[8];
std::int32_t result;
};
static_assert(sizeof(CommonDialogBaseParam) == 48);
static_assert(sizeof(G2PDialogParam) == 128);
static_assert(sizeof(PlayerReviewDialogParam) == 168);
static_assert(offsetof(CommonDialogResultPrefix, result) == 8);
inline bool ValidateCommonDialogBaseParam(const CommonDialogBaseParam& param) {
return param.size == sizeof(param) && param.magic == static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(&param)) - 1060003575u && std::all_of(std::begin(param.reserved), std::end(param.reserved), [](std::uint8_t value) { return value == 0; });
}
constexpr int COMMON_DIALOG_OK = 0;
constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x80B80004);
+11 -10
View File
@@ -9,32 +9,33 @@
namespace {
std::atomic<bool> g_initialized{false};
void RequireInitialized(const char* function) {
if (!g_initialized.load()) throw std::logic_error(std::string(function) + ": not initialized");
}
constexpr int InvalidArgument = static_cast<int>(0x809D5001u);
constexpr int NotInitialized = static_cast<int>(0x809D5002u);
constexpr int AlreadyInitialized = static_cast<int>(0x809D5003u);
}
extern "C" {
int APS5_VABI sceContentDeleteInitialize(const ContentDeleteInitParam* init_param) {
if (init_param == nullptr) APS5_INVALID_ARG_EX;
int APS5_VABI sceContentDeleteInitialize(const ContentDeleteInitParam* initParam) {
if (initParam == nullptr || initParam->heap_size != 0x4000) return InvalidArgument;
bool expected = false;
if (!g_initialized.compare_exchange_strong(expected, true)) throw std::logic_error(std::string(__func__) + ": already initialized");
if (!g_initialized.compare_exchange_strong(expected, true)) return AlreadyInitialized;
return 0;
}
int APS5_VABI sceContentDeleteByPath(const char* contentPath) {
if (!g_initialized.load()) return NotInitialized;
if (contentPath == nullptr) APS5_INVALID_ARG_EX;
RequireInitialized(__func__);
if (*contentPath == 0) return InvalidArgument;
throw std::logic_error(std::string(__func__) + ": " + contentPath + " is not in the host content library, which is empty, and the console's error for it is unknown");
}
int APS5_VABI sceContentDeleteById(std::int64_t contentId) {
RequireInitialized(__func__);
if (!g_initialized.load()) return NotInitialized;
throw std::logic_error(std::string(__func__) + ": content id " + std::to_string(contentId) + " is not in the host content library, which is empty, and the console's error for it is unknown");
}
int APS5_VABI sceContentDeleteByIdListForSf(void) {
int APS5_VABI sceContentDeleteByIdListForSf(const std::int64_t* contentIds, std::size_t count) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
@@ -56,7 +57,7 @@ int APS5_VABI sceContentDeleteMemoryTerminate(void) {
int APS5_VABI sceContentDeleteTerminate(void) {
bool expected = true;
if (!g_initialized.compare_exchange_strong(expected, false)) throw std::logic_error(std::string(__func__) + ": not initialized");
if (!g_initialized.compare_exchange_strong(expected, false)) return NotInitialized;
return 0;
}
+26 -25
View File
@@ -3,18 +3,20 @@
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include <atomic>
#include <limits>
#include <mutex>
#include <set>
#include <stdexcept>
#include <string>
namespace {
constexpr std::size_t MaxExecution = 10;
constexpr std::int32_t MaxExecution = 10;
constexpr int ErrorInvalidState = static_cast<int>(0x809D3002u);
constexpr int ErrorBusy = static_cast<int>(0x809D3003u);
constexpr int ErrorNotInitialized = static_cast<int>(0x809D3004u);
constexpr int ErrorExecutionMax = static_cast<int>(0x809D3018u);
std::atomic<bool> g_initialized{false};
std::mutex g_sessionMutex;
std::set<std::int32_t> g_sessions;
std::int32_t g_lastSessionId = 0;
void RequireSession(const char* function, std::int32_t expId) {
if (!g_initialized.load()) throw std::logic_error(std::string(function) + ": not initialized");
@@ -40,22 +42,22 @@ int APS5_VABI sceContentExportFinish(std::int32_t expId) {
return 0;
}
int APS5_VABI sceContentExportFromData(void) {
int APS5_VABI sceContentExportFromData(std::int32_t expId, const ContentExportParam* param, std::size_t contentLength, ContentExportDataCallback callback, void* userData, char* path, std::size_t pathBufferLength) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceContentExportFromFile(void) {
int APS5_VABI sceContentExportFromFile(std::int32_t expId, const ContentExportParam* param, const char* sourcePath, char* path, std::size_t pathBufferLength) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceContentExportFromFileWithThumbnail(void) {
int APS5_VABI sceContentExportFromFileWithThumbnail(std::int32_t expId, const ContentExportParam* param, const char* sourcePath, const char* thumbnailPath, char* path, std::size_t pathBufferLength) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceContentExportAddToFavorites(void) {
int APS5_VABI sceContentExportAddToFavorites(std::int64_t contentId) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
@@ -75,22 +77,22 @@ int APS5_VABI sceContentExportFrom(void) {
return 0;
}
int APS5_VABI sceContentExportFromFileForSf(void) {
int APS5_VABI sceContentExportFromFileForSf(std::int32_t expId, const ContentExportParam* param, const ContentExportSfParam* sfParam, const char* sourcePath, const char* thumbnailPath, char* path, std::size_t pathBufferLength) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceContentExportFromFileWithContentIdList(void) {
int APS5_VABI sceContentExportFromFileWithContentIdList(std::int32_t expId, const ContentExportParam* param, const char* sourcePath, const char* thumbnailPath, const std::int64_t* contentIds, std::uint32_t contentCount, char* path, std::size_t pathBufferLength) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceContentExportFromFileWithTitleIdList(void) {
int APS5_VABI sceContentExportFromFileWithTitleIdList(std::int32_t expId, const ContentExportParam* param, const char* sourcePath, const char* thumbnailPath, const char* titleIdList, char* path, std::size_t pathBufferLength) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceContentExportInit(void) {
int APS5_VABI sceContentExportInit(const ContentExportInitParam* param) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
@@ -100,44 +102,43 @@ int APS5_VABI sceContentExportMalloc(void) {
return 0;
}
int APS5_VABI sceContentExportRemoveFromFavorites(void) {
int APS5_VABI sceContentExportRemoveFromFavorites(std::int64_t contentId) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceContentExportRenameContent(void) {
int APS5_VABI sceContentExportRenameContent(std::int64_t contentId, const char* name) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceContentExportValidateContents(void) {
int APS5_VABI sceContentExportValidateContents(const std::int64_t* contentIds, std::uint32_t count) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceContentExportStart(void) {
std::lock_guard lock(g_sessionMutex);
if (!g_initialized.load()) throw std::logic_error(std::string(__func__) + ": not initialized");
if (g_sessions.size() >= MaxExecution) throw std::runtime_error(std::string(__func__) + ": behaviour beyond 10 live export sessions is unknown");
if (g_lastSessionId == std::numeric_limits<std::int32_t>::max()) throw std::runtime_error(std::string(__func__) + ": export session ids exhausted");
g_sessions.insert(++g_lastSessionId);
return g_lastSessionId;
if (!g_initialized.load()) return ErrorNotInitialized;
for (std::int32_t expId = 0; expId < MaxExecution; ++expId) {
if (g_sessions.insert(expId).second) return expId;
}
return ErrorExecutionMax;
}
int APS5_VABI sceContentExportGetProgress(std::int32_t expId) {
std::lock_guard lock(g_sessionMutex);
RequireSession(__func__, expId);
throw std::runtime_error(std::string(__func__) + ": progress of a session with no running export is unknown");
throw std::runtime_error(std::string(__func__) + ": progress requires the calling title SDK compatibility mode");
}
int APS5_VABI sceContentExportCancel(std::int32_t expId) {
std::lock_guard lock(g_sessionMutex);
RequireSession(__func__, expId);
throw std::runtime_error(std::string(__func__) + ": cancelling a session with no running export is unknown");
return ErrorInvalidState;
}
int APS5_VABI sceContentExportFromDataWithThumbnail(std::int32_t expId, const ContentExportParam* param, const ContentExportCallbackParam* contentCallbackParam,
const char* thumbnailType, const ContentExportCallbackParam* thumbnailCallbackParam, char* path, std::size_t pathBufferLength) {
int APS5_VABI sceContentExportFromDataWithThumbnail(std::int32_t expId, const ContentExportParam* param, const ContentExportCallbackParam* contentCallbackParam, const char* thumbnailType, const ContentExportCallbackParam* thumbnailCallbackParam, char* path, std::size_t pathBufferLength) {
std::lock_guard lock(g_sessionMutex);
RequireSession(__func__, expId);
throw std::runtime_error(std::string(__func__) + ": the host has no media library to export into");
@@ -145,9 +146,9 @@ int APS5_VABI sceContentExportFromDataWithThumbnail(std::int32_t expId, const Co
int APS5_VABI sceContentExportTerm(void) {
std::lock_guard lock(g_sessionMutex);
if (!g_sessions.empty()) throw std::runtime_error(std::string(__func__) + ": terminating with live export sessions is unknown");
if (!g_sessions.empty()) return ErrorBusy;
bool expected = true;
if (!g_initialized.compare_exchange_strong(expected, false)) throw std::logic_error(std::string(__func__) + ": not initialized");
if (!g_initialized.compare_exchange_strong(expected, false)) return ErrorNotInitialized;
return 0;
}
+17 -13
View File
@@ -10,6 +10,8 @@ namespace {
std::atomic<bool> g_initialized{false};
constexpr int ErrorNotInitialized = static_cast<int>(0x809D1001u);
constexpr int ErrorAlreadyInitialized = static_cast<int>(0x809D1002u);
constexpr int ErrorInvalidArgument = static_cast<int>(0x809D1003u);
constexpr int ErrorLimitTooBig = static_cast<int>(0x809D100Au);
constexpr std::uint32_t MaxSearchLimit = 92;
constexpr std::int64_t LibraryUpdateId = 0;
@@ -63,42 +65,44 @@ void ValidateOrderBy(const char* funcName, const ContentSearchContentOrderByCond
extern "C" {
int APS5_VABI sceContentSearchInit(const ContentSearchInitParam* init_param) {
if (init_param == nullptr) APS5_INVALID_ARG_EX;
if (g_initialized.load()) return ErrorAlreadyInitialized;
if (init_param == nullptr || init_param->memory_size == 0 || init_param->memory_size % 16384 != 0) return ErrorInvalidArgument;
bool expected = false;
if (!g_initialized.compare_exchange_strong(expected, true)) throw std::logic_error(std::string(__func__) + ": already initialized");
if (!g_initialized.compare_exchange_strong(expected, true)) return ErrorAlreadyInitialized;
return 0;
}
int APS5_VABI sceContentSearchCloseMetadata(std::int32_t metadataId) {
if (!g_initialized.load()) return ErrorNotInitialized;
ThrowUnknownMetadataId(__func__, metadataId);
return ErrorInvalidArgument;
}
int APS5_VABI sceContentSearchGetMetadataFieldInfo(std::int32_t metadataId, const char* field, std::int32_t* metadataType, std::int32_t* size) {
if (!g_initialized.load()) return ErrorNotInitialized;
if (field == nullptr || metadataType == nullptr || size == nullptr) APS5_INVALID_ARG_EX;
if (field == nullptr || metadataType == nullptr || size == nullptr) return ErrorInvalidArgument;
ThrowUnknownMetadataId(__func__, metadataId);
}
int APS5_VABI sceContentSearchGetMetadataValue(std::int32_t metadataId, const char* field, void* value) {
if (!g_initialized.load()) return ErrorNotInitialized;
if (field == nullptr || value == nullptr) APS5_INVALID_ARG_EX;
if (field == nullptr || value == nullptr) return ErrorInvalidArgument;
ThrowUnknownMetadataId(__func__, metadataId);
}
int APS5_VABI sceContentSearchOpenMetadata(const char* filePath, std::int32_t* metadataId) {
if (!g_initialized.load()) return ErrorNotInitialized;
if (filePath == nullptr || metadataId == nullptr) APS5_INVALID_ARG_EX;
if (filePath == nullptr || metadataId == nullptr) return ErrorInvalidArgument;
throw std::logic_error(std::string(__func__) + ": " + filePath + " is not in the host content library, which is empty, and the console's error for it is unknown");
}
int APS5_VABI sceContentSearchSearchContent(const ContentSearchContentColumnSet* columnSet, std::uint32_t columnSetLength, const ContentSearchContentOrderByCondition* orderByConditions, std::uint32_t orderByConditionsLength, std::uint32_t offset, std::uint32_t limit, std::int64_t* numOfContent, void* infos, std::int64_t* lastUpdateId) {
(void)offset;
if (!g_initialized.load()) return ErrorNotInitialized;
if (numOfContent == nullptr || lastUpdateId == nullptr || (infos == nullptr && limit != 0)) APS5_INVALID_ARG_EX;
if ((columnSet == nullptr) != (columnSetLength == 0) || (orderByConditions == nullptr) != (orderByConditionsLength == 0) || limit == 0) return ErrorInvalidArgument;
if (limit > MaxSearchLimit) return ErrorLimitTooBig;
if (numOfContent == nullptr || lastUpdateId == nullptr || infos == nullptr) return ErrorInvalidArgument;
ValidateColumnSets(__func__, columnSet, columnSetLength);
ValidateOrderBy(__func__, orderByConditions, orderByConditionsLength);
if (limit > MaxSearchLimit) return ErrorLimitTooBig;
*numOfContent = 0;
*lastUpdateId = LibraryUpdateId;
return 0;
@@ -106,20 +110,20 @@ int APS5_VABI sceContentSearchSearchContent(const ContentSearchContentColumnSet*
int APS5_VABI sceContentSearchGetContentLastUpdateId(std::int64_t* lastUpdateId) {
if (!g_initialized.load()) return ErrorNotInitialized;
if (lastUpdateId == nullptr) APS5_INVALID_ARG_EX;
if (lastUpdateId == nullptr) return ErrorInvalidArgument;
*lastUpdateId = LibraryUpdateId;
return 0;
}
int APS5_VABI sceContentSearchOpenMetadataByContentId(std::int64_t contentId, std::int32_t* metadataId) {
if (!g_initialized.load()) return ErrorNotInitialized;
if (metadataId == nullptr) APS5_INVALID_ARG_EX;
if (metadataId == nullptr) return ErrorInvalidArgument;
throw std::logic_error(std::string(__func__) + ": content id " + std::to_string(contentId) + " is not in the host content library, which is empty, and the console's error for it is unknown");
}
int APS5_VABI sceContentSearchGetTotalContentSize(const ContentSearchContentColumnSet* columnSet, std::uint32_t columnSetLength, std::int64_t* size, std::int64_t* lastUpdateId) {
if (!g_initialized.load()) return ErrorNotInitialized;
if (size == nullptr || lastUpdateId == nullptr) APS5_INVALID_ARG_EX;
if ((columnSet == nullptr) != (columnSetLength == 0) || size == nullptr || lastUpdateId == nullptr) return ErrorInvalidArgument;
ValidateColumnSets(__func__, columnSet, columnSetLength);
*size = 0;
*lastUpdateId = LibraryUpdateId;
@@ -128,7 +132,7 @@ int APS5_VABI sceContentSearchGetTotalContentSize(const ContentSearchContentColu
int APS5_VABI sceContentSearchGetNumOfContent(const ContentSearchContentColumnSet* columnSet, std::uint32_t columnSetLength, std::int64_t* numOfContent, std::int64_t* lastUpdateId) {
if (!g_initialized.load()) return ErrorNotInitialized;
if (numOfContent == nullptr || lastUpdateId == nullptr) APS5_INVALID_ARG_EX;
if ((columnSet == nullptr) != (columnSetLength == 0) || numOfContent == nullptr || lastUpdateId == nullptr) return ErrorInvalidArgument;
ValidateColumnSets(__func__, columnSet, columnSetLength);
*numOfContent = 0;
*lastUpdateId = LibraryUpdateId;
@@ -137,7 +141,7 @@ int APS5_VABI sceContentSearchGetNumOfContent(const ContentSearchContentColumnSe
int APS5_VABI sceContentSearchTerm(void) {
bool expected = true;
if (!g_initialized.compare_exchange_strong(expected, false)) throw std::logic_error(std::string(__func__) + ": not initialized");
if (!g_initialized.compare_exchange_strong(expected, false)) return ErrorNotInitialized;
return 0;
}
Loaded 100 of 535 files, more files were not shown because too many files have changed in this diff. Show more