mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-28 00:35:47 +00:00
chore(libc): refresh feupdateenv and preserve debt entries
This commit is contained in:
599 files changed
+24825
-6719
No files matched your search
@@ -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.
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -38,6 +38,8 @@ SDL-mapped game controllers are supported, including analog sticks and triggers.
|
||||
|
||||
This project is intended for interoperability, research, preservation, and compatibility purposes. It does not include, distribute, or require copyrighted software, firmware, cryptographic keys, or proprietary libraries. Users are responsible for ensuring that any binaries used with this project are obtained and used in accordance with applicable laws and their respective license terms.
|
||||
|
||||
AnyPS5 is an independent project and is not affiliated with, authorized or endorsed by Sony Interactive Entertainment. "PlayStation" and "PS5" are trademarks of Sony Interactive Entertainment Inc.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the GNU General Public License version 2 only.
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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(¶meterMode, 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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -79,6 +79,18 @@ foreach(agcReadbackKind IN ITEMS Uint Depth)
|
||||
list(APPEND agcReadbackHeaders ${agcReadbackHeader})
|
||||
endforeach()
|
||||
|
||||
set(agcMultisampleUploadSpv ${agcTextureDetileGeneratedDir}/MultisampleUpload.spv)
|
||||
set(agcMultisampleUploadHeader ${agcTextureDetileGeneratedDir}/MultisampleUpload_spv.h)
|
||||
add_custom_command(
|
||||
OUTPUT "${agcMultisampleUploadHeader}"
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}"
|
||||
COMMAND "$<TARGET_FILE:glslang-standalone>" -V --target-env vulkan1.1 -o "${agcMultisampleUploadSpv}" "${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/MultisampleUpload.comp"
|
||||
COMMAND ${CMAKE_COMMAND} -DINPUT=${agcMultisampleUploadSpv} -DOUTPUT=${agcMultisampleUploadHeader}
|
||||
-DSYMBOL=MULTISAMPLE_UPLOAD_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake"
|
||||
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/MultisampleUpload.comp" glslang-standalone
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
set(agcMeshArgumentsShaderSpv ${agcTextureDetileGeneratedDir}/MeshArguments.spv)
|
||||
set(agcMeshArgumentsShaderHeader ${agcTextureDetileGeneratedDir}/MeshArguments_spv.h)
|
||||
add_custom_command(
|
||||
@@ -334,8 +346,10 @@ set(agcDriverSources
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/TextureFormat.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/DccMetadata.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/TextureTiling.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/MultisampleTextureLayout.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/TextureDetiler.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/MultisampleReadback.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/MultisampleUpload.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Texture.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PassHazards.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/SampleOrder.cpp
|
||||
@@ -345,6 +359,7 @@ set(agcDriverSources
|
||||
${agcSampleOrderShaderHeaders}
|
||||
${agcTextureDetileShaderHeader}
|
||||
${agcReadbackHeaders}
|
||||
${agcMultisampleUploadHeader}
|
||||
${agcCmaskClearShaderHeader}
|
||||
${agcDepthSampleShaderHeaders}
|
||||
${agcMeshArgumentsShaderHeader}
|
||||
@@ -756,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)
|
||||
@@ -1450,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)
|
||||
@@ -1461,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
|
||||
@@ -1565,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
|
||||
@@ -1598,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
|
||||
@@ -1626,12 +1653,27 @@ if(BUILD_TESTING)
|
||||
ENVIRONMENT "APS5_PIN_WAIT_MS=2000" SKIP_RETURN_CODE 77 TIMEOUT 30)
|
||||
set_tests_properties(agc_driver_unit_shadow_budget PROPERTIES
|
||||
ENVIRONMENT "APS5_PIN_WAIT_MS=2000;APS5_UNIT_SHADOW_MIB=8" SKIP_RETURN_CODE 77 TIMEOUT 30)
|
||||
add_test(NAME agc_recorder_completion_labels COMMAND agc_driver_recorder_tests --completion-labels-only)
|
||||
set_tests_properties(agc_recorder_completion_labels PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
|
||||
add_test(NAME agc_recorder_cmask COMMAND agc_driver_recorder_tests --cmask-only)
|
||||
set_tests_properties(agc_recorder_cmask PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
|
||||
add_test(NAME agc_recorder_cube COMMAND agc_driver_recorder_tests --cube-only)
|
||||
set_tests_properties(agc_recorder_cube PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_host_import_lifetime agc_driver_host_import_mutation_race agc_driver_unit_shadow agc_driver_unit_shadow_budget PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
set_tests_properties(agc_recorder_completion_labels agc_recorder_cmask agc_recorder_cube PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
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)
|
||||
@@ -1846,6 +1888,28 @@ endif()
|
||||
set_target_properties(agc_driver_graphics_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_driver_graphics_tests)
|
||||
|
||||
add_test_executable(agc_driver_msaa_layout_tests EXCLUDE_FROM_ALL
|
||||
tests/MultisampleTextureLayout.cpp
|
||||
Graphics/src/MultisampleTextureLayout.cpp
|
||||
)
|
||||
target_include_directories(agc_driver_msaa_layout_tests PRIVATE
|
||||
${LIBS_INCLUDE_DIR}
|
||||
${CMAKE_SOURCE_DIR}/core/shader/recompiler
|
||||
${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}
|
||||
)
|
||||
target_link_libraries(agc_driver_msaa_layout_tests PRIVATE libc)
|
||||
set_target_properties(agc_driver_msaa_layout_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_driver_msaa_layout_tests)
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_driver_msaa_layout_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
|
||||
add_test(NAME agc_driver_msaa_layout COMMAND agc_driver_msaa_layout_tests)
|
||||
set_tests_properties(agc_driver_msaa_layout PROPERTIES TIMEOUT 20)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_msaa_layout PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
foreach(agcVertexRangeShader IN ITEMS VertexRange.vert VertexRangeProducer.frag)
|
||||
string(REPLACE "." "_" agcVertexRangeName "${agcVertexRangeShader}")
|
||||
string(TOUPPER "${agcVertexRangeName}_SPV" agcVertexRangeSymbol)
|
||||
@@ -1987,9 +2051,14 @@ if(BUILD_TESTING)
|
||||
add_test(NAME agc_driver_store_pending_identity COMMAND agc_driver_store_over_untracked_tests --pending-identity)
|
||||
set_tests_properties(agc_driver_store_pending_identity PROPERTIES
|
||||
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
add_test(NAME agc_driver_store_idle_results_wake COMMAND agc_driver_store_idle_results_tests --idle-epoch-wake)
|
||||
set_tests_properties(agc_driver_store_idle_results_wake PROPERTIES
|
||||
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_store_pending_identity PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
set_tests_properties(agc_driver_store_idle_results_wake PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@@ -2272,7 +2341,7 @@ if(BUILD_TESTING)
|
||||
set_tests_properties(agc_driver_queued_raw_shader PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=8" TIMEOUT 30)
|
||||
add_test(NAME agc_driver_partially_resident_pending_stores COMMAND agc_driver_partially_resident_snapshot_tests --pending-stores)
|
||||
set_tests_properties(agc_driver_multisample_readback PROPERTIES TIMEOUT 20)
|
||||
foreach(agcReadbackCase IN ITEMS uint8 uint16 uint64 uint128 depth)
|
||||
foreach(agcReadbackCase IN ITEMS uint8 uint16 uint64 uint128 depth upload retile depth_retile)
|
||||
add_test(NAME agc_driver_multisample_readback_${agcReadbackCase} COMMAND agc_driver_multisample_readback_tests ${agcReadbackCase})
|
||||
set_tests_properties(agc_driver_multisample_readback_${agcReadbackCase} PROPERTIES TIMEOUT 20 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
if(WIN32)
|
||||
@@ -2723,6 +2792,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_")
|
||||
@@ -2764,15 +2840,16 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
endforeach()
|
||||
add_test(NAME agc_driver_shutdown_threads_joined COMMAND agc_driver_shutdown_threads_tests --joined)
|
||||
add_test(NAME agc_driver_shutdown_threads_queued_prebuilds COMMAND agc_driver_shutdown_threads_tests --queued-prebuilds)
|
||||
add_test(NAME agc_driver_shutdown_threads_compare_helpers COMMAND agc_driver_shutdown_threads_tests --compare-helpers)
|
||||
add_test(NAME agc_driver_shutdown_threads_draw_committer COMMAND agc_driver_shutdown_threads_tests --draw-committer)
|
||||
add_test(NAME agc_driver_shutdown_threads_shader_cache_writer COMMAND agc_driver_shutdown_threads_tests --shader-cache-writer)
|
||||
add_test(NAME agc_driver_shutdown_threads_late_shader_cache COMMAND agc_driver_shutdown_threads_tests --late-shader-cache)
|
||||
add_test(NAME agc_driver_shutdown_threads_async_log_held COMMAND agc_driver_shutdown_threads_tests --async-log-held)
|
||||
add_test(NAME agc_driver_shutdown_threads_exit_after_start COMMAND agc_driver_shutdown_threads_tests --exit-after-start)
|
||||
set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache agc_driver_shutdown_threads_async_log_held agc_driver_shutdown_threads_exit_after_start PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30)
|
||||
set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_queued_prebuilds agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache agc_driver_shutdown_threads_async_log_held agc_driver_shutdown_threads_exit_after_start PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache agc_driver_shutdown_threads_async_log_held agc_driver_shutdown_threads_exit_after_start PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_queued_prebuilds agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache agc_driver_shutdown_threads_async_log_held agc_driver_shutdown_threads_exit_after_start PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_driver_compute_user_data_calls_registered COMMAND agc_driver_compute_user_data_calls_tests --registered)
|
||||
set_tests_properties(agc_driver_compute_user_data_calls_registered PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
@@ -3302,6 +3379,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;
|
||||
|
||||
@@ -487,7 +487,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
|
||||
results.push_back(ShaderRecompiler::BuildBarycentricGeometryShader(*programResults[0], *programResults[1], localDevice->Target(), geometryLimits));
|
||||
stages.insert(stages.begin() + 1, Graphics::CompiledShader{Stage::Geometry, &results.back(), 0});
|
||||
} else {
|
||||
require(graphics.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST && !graphics.primitiveRestart, "a pixel shader that reads barycentrics without VK_KHR_fragment_shader_barycentric or geometry shaders needs a triangle list without primitive restart");
|
||||
require((graphics.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || graphics.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP) && !graphics.primitiveRestart, "a pixel shader that reads barycentrics without VK_KHR_fragment_shader_barycentric or geometry shaders needs a triangle list or strip without primitive restart");
|
||||
auto patches = ShaderRecompiler::BuildBarycentricTessellationShaders(*programResults[0], *programResults[1], localDevice->Target(), localDevice->MaxFragmentInputComponents(), graphics.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT ? 2u : 0u);
|
||||
const auto first = results.size();
|
||||
results.push_back(std::move(patches.control));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1146,9 +1146,8 @@ public:
|
||||
Closing() = true;
|
||||
pools = {Existing(), ExistingPrebuild()};
|
||||
}
|
||||
for (auto* pool : pools) {
|
||||
if (pool != nullptr) pool->Close();
|
||||
}
|
||||
if (pools[0] != nullptr) pools[0]->Close(false);
|
||||
if (pools[1] != nullptr) pools[1]->Close(true);
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -1204,11 +1203,17 @@ public:
|
||||
idle.wait(lock, [&] { return unfinished.empty() || *unfinished.begin() > ticket; });
|
||||
}
|
||||
|
||||
void Close() {
|
||||
void Close(bool dropQueued) {
|
||||
std::vector<Worker> joined;
|
||||
std::deque<Task> dropped;
|
||||
{
|
||||
std::unique_lock lock(mutex);
|
||||
closed = true;
|
||||
if (dropQueued) {
|
||||
dropped.swap(tasks);
|
||||
for (const auto& task : dropped) unfinished.erase(task.sequence);
|
||||
idle.notify_all();
|
||||
}
|
||||
idle.wait(lock, [&] { return tasks.empty() && running == 0; });
|
||||
joined.swap(workers);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
@@ -316,6 +318,7 @@ struct VulkanDevice::State {
|
||||
bool geometryShader = false;
|
||||
bool sampleRateShading = false;
|
||||
bool storageImageMultisample = false;
|
||||
bool storageImageWriteWithoutFormat = false;
|
||||
bool shaderClipDistance = false;
|
||||
bool shaderCullDistance = false;
|
||||
bool shaderClock = false;
|
||||
@@ -342,6 +345,7 @@ struct VulkanDevice::State {
|
||||
bool imageInt64Atomics = false;
|
||||
bool bufferInt64Atomics = false;
|
||||
bool viewportIndexLayer = false;
|
||||
bool imageFormatList = false;
|
||||
bool primitiveListRestart = false;
|
||||
bool depthClipControl = false;
|
||||
bool graphicsPipelineLibrary = false;
|
||||
@@ -1313,6 +1317,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
|
||||
state->spirvExtensions.push_back("SPV_EXT_shader_viewport_index_layer");
|
||||
state->viewportIndexLayer = true;
|
||||
}
|
||||
if (hasExtension(VK_KHR_IMAGE_FORMAT_LIST_EXTENSION_NAME)) {
|
||||
deviceExtensions.push_back(VK_KHR_IMAGE_FORMAT_LIST_EXTENSION_NAME);
|
||||
state->imageFormatList = true;
|
||||
}
|
||||
if (state->demoteToHelperInvocation) {
|
||||
deviceExtensions.push_back(VK_EXT_SHADER_DEMOTE_TO_HELPER_INVOCATION_EXTENSION_NAME);
|
||||
state->capabilities.push_back(spv::CapabilityDemoteToHelperInvocation);
|
||||
@@ -1693,6 +1701,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
|
||||
if (enabled.shaderStorageImageMultisample) state->capabilities.push_back(spv::CapabilityStorageImageMultisample);
|
||||
state->capabilities.push_back(spv::CapabilityImageMSArray);
|
||||
enabled.shaderStorageImageWriteWithoutFormat = available.shaderStorageImageWriteWithoutFormat;
|
||||
state->storageImageWriteWithoutFormat = enabled.shaderStorageImageWriteWithoutFormat == VK_TRUE;
|
||||
enabled.shaderStorageImageReadWithoutFormat = available.shaderStorageImageReadWithoutFormat;
|
||||
// Gathers with non-constant offsets (ImageGatherExtended).
|
||||
enabled.shaderImageGatherExtended = available.shaderImageGatherExtended;
|
||||
@@ -1943,14 +1952,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};
|
||||
@@ -3220,8 +3222,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;
|
||||
@@ -3671,6 +3674,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();
|
||||
@@ -3833,6 +3842,7 @@ Graphics::Context VulkanDevice::buildContext() const {
|
||||
context.fillModeNonSolid = state->fillModeNonSolid;
|
||||
context.maintenance5 = state->maintenance5;
|
||||
context.storageImageMultisample = state->storageImageMultisample;
|
||||
context.storageImageWriteWithoutFormat = state->storageImageWriteWithoutFormat;
|
||||
context.shaderClipDistance = state->shaderClipDistance;
|
||||
context.shaderCullDistance = state->shaderCullDistance;
|
||||
context.shaderTerminateInvocation = state->shaderTerminateInvocation;
|
||||
@@ -3840,6 +3850,7 @@ Graphics::Context VulkanDevice::buildContext() const {
|
||||
context.bufferInt64Atomics = state->bufferInt64Atomics;
|
||||
context.placeholderImages = state->placeholderImages.get();
|
||||
context.viewportIndexLayer = state->viewportIndexLayer;
|
||||
context.imageFormatList = state->imageFormatList;
|
||||
context.demoteToHelperInvocation = state->demoteToHelperInvocation;
|
||||
context.earlyFragmentSampleMaskTestBeforeSampleCounting = state->earlyFragmentSampleMaskTestBeforeSampleCounting;
|
||||
context.primitiveListRestart = state->primitiveListRestart;
|
||||
@@ -4401,6 +4412,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;
|
||||
@@ -4661,8 +4673,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) {
|
||||
@@ -4745,6 +4757,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.
|
||||
@@ -4879,17 +4899,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];
|
||||
@@ -4921,21 +4935,31 @@ 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);
|
||||
if (record.htileClear.has_value()) {
|
||||
Graphics::QueueHtileDepthClear(record.htileClear->first, record.htileClear->second);
|
||||
recorder.NotePendingWrite(record.htileClear->first, record.htileClear->second);
|
||||
}
|
||||
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);
|
||||
@@ -5066,7 +5090,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;
|
||||
@@ -5318,7 +5342,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;
|
||||
|
||||
@@ -241,6 +241,8 @@ struct Context {
|
||||
bool fillModeNonSolid = false;
|
||||
bool maintenance5 = false;
|
||||
bool storageImageMultisample = false;
|
||||
bool storageImageWriteWithoutFormat = false;
|
||||
bool imageFormatList = false;
|
||||
bool shaderClipDistance = false;
|
||||
bool shaderCullDistance = false;
|
||||
bool nullDescriptors = false;
|
||||
|
||||
@@ -20,8 +20,16 @@ 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();
|
||||
void NoteDepthSurfaceWrite(const Context& context, const DepthTarget& target);
|
||||
bool SampleLocationsCompatibleDepth(const Context& context, VkFormat format, std::uint32_t samples);
|
||||
std::uint64_t DepthSurfacesSerial();
|
||||
std::uint64_t DepthSurfacesReleased();
|
||||
@@ -47,7 +55,10 @@ 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 QueueHtileDepthClear(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;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_MULTISAMPLESWIZZLEEQUATIONS_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_MULTISAMPLESWIZZLEEQUATIONS_HPP
|
||||
|
||||
#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp"
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
namespace AgcDriver::Graphics {
|
||||
|
||||
struct MultisampleSwizzleEquation {
|
||||
TextureTileMode tileMode;
|
||||
std::uint32_t elementBytes;
|
||||
std::uint32_t fragments;
|
||||
std::array<std::uint64_t, 16> bits;
|
||||
};
|
||||
|
||||
inline constexpr MultisampleSwizzleEquation kMultisampleSwizzleEquations[] = {
|
||||
{TextureTileMode::kZ64KBX, 1u, 2u, {0x0001000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000000000004ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000100000ull, 0x0000000000400000ull, 0x0000000000000040ull, 0x0000000000800000ull}},
|
||||
{TextureTileMode::kZ64KBX, 2u, 2u, {0x0000000000000000ull, 0x0001000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000000000004ull, 0x0000000000040000ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0000000000400000ull, 0x0000000000000040ull}},
|
||||
{TextureTileMode::kZ64KBX, 4u, 2u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0001000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000000000004ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0000000000400000ull, 0x0000000000840000ull}},
|
||||
{TextureTileMode::kZ64KBX, 1u, 4u, {0x0001000000000000ull, 0x0002000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000000000004ull, 0x0000000000040000ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0000000000400000ull, 0x0000000000000040ull}},
|
||||
{TextureTileMode::kZ64KBX, 2u, 4u, {0x0000000000000000ull, 0x0001000000000000ull, 0x0002000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000000000004ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000000008ull, 0x0000000000100000ull, 0x0000000000000040ull, 0x0000000000440000ull}},
|
||||
{TextureTileMode::kZ64KBX, 4u, 4u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0001000000000000ull, 0x0002000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0000000000440000ull, 0x0000000000800004ull}},
|
||||
{TextureTileMode::kZ64KBX, 1u, 8u, {0x0001000000000000ull, 0x0002000000000000ull, 0x0004000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000000000004ull, 0x0000000400080008ull, 0x0000000200100010ull, 0x0000000100200040ull, 0x0000000800400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0000000000400000ull, 0x0000000000840000ull}},
|
||||
{TextureTileMode::kZ64KBX, 2u, 8u, {0x0000000000000000ull, 0x0001000000000000ull, 0x0002000000000000ull, 0x0004000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000400080008ull, 0x0000000200100010ull, 0x0000000100200040ull, 0x0000000800400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0000000000440000ull, 0x0000000000800004ull}},
|
||||
{TextureTileMode::kZ64KBX, 4u, 8u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0001000000000000ull, 0x0002000000000000ull, 0x0004000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000400080008ull, 0x0000000200100010ull, 0x0000000100200040ull, 0x0000000000440020ull, 0x0000000000000008ull, 0x0000000000100000ull, 0x0000000000400004ull, 0x0000000000820000ull}},
|
||||
{TextureTileMode::kR64KBX, 1u, 2u, {0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000020000ull, 0x0000000000010000ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000000100000ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000400000ull, 0x0000000000000040ull, 0x0000000000800000ull, 0x0001000001000000ull}},
|
||||
{TextureTileMode::kR64KBX, 2u, 2u, {0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000010000ull, 0x0000000000020000ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000100000ull, 0x0000000000000040ull, 0x0000000000400000ull, 0x0001000000800000ull}},
|
||||
{TextureTileMode::kR64KBX, 4u, 2u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000010000ull, 0x0000000000020000ull, 0x0000000000040000ull, 0x0000000000000004ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0000000000400000ull, 0x0001000000800000ull}},
|
||||
{TextureTileMode::kR64KBX, 8u, 2u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000020000ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000000100000ull, 0x0001000000400000ull}},
|
||||
{TextureTileMode::kR64KBX, 16u, 2u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000400080008ull, 0x0000000200100010ull, 0x0000000100200040ull, 0x0001000000400020ull, 0x0000000000040000ull, 0x0000000000000004ull, 0x0000000000080000ull, 0x0000000000000010ull}},
|
||||
{TextureTileMode::kR64KBX, 1u, 4u, {0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000020000ull, 0x0000000000010000ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000000100000ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000400000ull, 0x0000000000000040ull, 0x0001000000800000ull, 0x0002000001000000ull}},
|
||||
{TextureTileMode::kR64KBX, 2u, 4u, {0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000010000ull, 0x0000000000020000ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000100000ull, 0x0000000000000040ull, 0x0001000000400000ull, 0x0002000000800000ull}},
|
||||
{TextureTileMode::kR64KBX, 4u, 4u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000010000ull, 0x0000000000020000ull, 0x0000000000040000ull, 0x0000000000000004ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0001000000400000ull, 0x0002000000800000ull}},
|
||||
{TextureTileMode::kR64KBX, 8u, 4u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000020000ull, 0x0000000400080008ull, 0x0000000200100010ull, 0x0002000000200040ull, 0x0000000100400020ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000000100000ull, 0x0001000000400000ull}},
|
||||
{TextureTileMode::kR64KBX, 16u, 4u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000200080008ull, 0x0000000100100010ull, 0x0002000000200040ull, 0x0001000000400020ull, 0x0000000000040000ull, 0x0000000000000004ull, 0x0000000000080000ull, 0x0000000000000010ull}},
|
||||
{TextureTileMode::kR64KBX, 1u, 8u, {0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000020000ull, 0x0000000000010000ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000000100000ull, 0x0000000400080008ull, 0x0000000200100010ull, 0x0000000100200040ull, 0x0000000800400020ull, 0x0000000000400000ull, 0x0001000000800000ull, 0x0002000001000000ull, 0x0004000002000000ull}},
|
||||
{TextureTileMode::kR64KBX, 2u, 8u, {0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000010000ull, 0x0000000000020000ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000400080008ull, 0x0000000200100010ull, 0x0000000100200040ull, 0x0000000800400020ull, 0x0000000000100000ull, 0x0001000000400000ull, 0x0002000000800000ull, 0x0004000001000000ull}},
|
||||
{TextureTileMode::kR64KBX, 4u, 8u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000010000ull, 0x0000000000020000ull, 0x0000000000040000ull, 0x0000000000000004ull, 0x0000000400080008ull, 0x0000000200100010ull, 0x0000000100200040ull, 0x0004000000400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0001000000400000ull, 0x0002000000800000ull}},
|
||||
{TextureTileMode::kR64KBX, 8u, 8u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000020000ull, 0x0000000200080008ull, 0x0000000100100010ull, 0x0004000000200040ull, 0x0002000000400020ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000000100000ull, 0x0001000000400000ull}},
|
||||
{TextureTileMode::kR64KBX, 16u, 8u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000200080008ull, 0x0000000100100010ull, 0x0004000000200040ull, 0x0002000000400020ull, 0x0000000000000004ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0001000000400000ull}},
|
||||
};
|
||||
|
||||
constexpr const MultisampleSwizzleEquation* FindMultisampleSwizzleEquation(TextureTileMode tileMode, std::uint32_t elementBytes, std::uint32_t fragments) {
|
||||
for (const auto& equation : kMultisampleSwizzleEquations) {
|
||||
if (equation.tileMode == tileMode && equation.elementBytes == elementBytes && equation.fragments == fragments) return &equation;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,54 @@
|
||||
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_MULTISAMPLETEXTURELAYOUT_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_MULTISAMPLETEXTURELAYOUT_HPP
|
||||
|
||||
#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp"
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
namespace AgcDriver::Graphics {
|
||||
|
||||
class MultisampleTextureLayout {
|
||||
public:
|
||||
static constexpr std::uint32_t Alignment = 65536u;
|
||||
|
||||
MultisampleTextureLayout(TextureTileMode tileMode, std::uint32_t elementBytes, std::uint32_t fragments, std::uint32_t width, std::uint32_t height, std::uint32_t layers, std::uint32_t pipeBankXor = 0);
|
||||
std::uint64_t Bytes() const { return sliceBytes * layers; }
|
||||
std::uint64_t SliceBytes() const { return sliceBytes; }
|
||||
std::uint64_t LinearBytes() const { return static_cast<std::uint64_t>(width) * height * layers * fragments * elementBytes; }
|
||||
std::uint32_t Pitch() const { return pitch; }
|
||||
std::uint32_t PaddedHeight() const { return paddedHeight; }
|
||||
std::uint32_t BlockWidth() const { return blockWidth; }
|
||||
std::uint32_t BlockHeight() const { return blockHeight; }
|
||||
const std::array<std::uint32_t, 16>& InverseBlockEquation() const { return inverseBlockEquation; }
|
||||
std::uint64_t Offset(std::uint32_t x, std::uint32_t y, std::uint32_t layer, std::uint32_t fragment) const;
|
||||
void Detile(std::span<const std::byte> source, std::span<std::byte> destination) const;
|
||||
void Tile(std::span<const std::byte> source, std::span<std::byte> destination) const;
|
||||
|
||||
private:
|
||||
std::uint64_t offset(std::uint32_t x, std::uint32_t y, std::uint32_t layer, std::uint32_t fragment) const;
|
||||
void transfer(std::span<const std::byte> source, std::span<std::byte> destination, bool retile) const;
|
||||
|
||||
std::uint32_t elementBytes;
|
||||
std::uint32_t fragments;
|
||||
std::uint32_t width;
|
||||
std::uint32_t height;
|
||||
std::uint32_t layers;
|
||||
std::uint32_t pipeBankXor;
|
||||
std::uint32_t blockWidth;
|
||||
std::uint32_t blockHeight;
|
||||
std::uint32_t pitch;
|
||||
std::uint32_t paddedHeight;
|
||||
std::uint64_t sliceBytes;
|
||||
std::array<std::uint32_t, 16> inverseBlockEquation;
|
||||
std::vector<std::uint32_t> xOffsets;
|
||||
std::vector<std::uint32_t> yOffsets;
|
||||
std::vector<std::uint32_t> layerOffsets;
|
||||
std::vector<std::uint32_t> fragmentOffsets;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -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.
|
||||
|
||||
@@ -57,9 +57,11 @@ namespace AgcDriver::Graphics {
|
||||
std::uint32_t CmaskErrors();
|
||||
void DispatchMultisampleReadback(VkCommandBuffer commands, VkFormat format, VkBuffer destination, std::uint64_t offset, std::uint64_t bytes, VkImageView arrayView, std::uint32_t width, std::uint32_t height, std::uint32_t layers, std::uint32_t fragments, std::uint32_t firstElement, std::uint32_t elements);
|
||||
void DispatchDepthSample(VkCommandBuffer commands, VkImageView depth, bool multisampled, VkImageView target, std::uint32_t width, std::uint32_t height, std::uint32_t sample);
|
||||
void DispatchMultisampleImage(VkCommandBuffer commands, std::uint32_t elementBytes, VkBuffer source, std::uint64_t offset, std::uint64_t bytes, VkImageView arrayView, std::uint32_t width, std::uint32_t height, std::uint32_t layers, std::uint32_t fragments, std::uint32_t firstElement, std::uint32_t elements);
|
||||
|
||||
private:
|
||||
VkPipeline multisampleReadbackPipeline(std::uint32_t elementBytes, bool depth);
|
||||
VkPipeline multisampleUploadPipeline(std::uint32_t elementBytes);
|
||||
VkPipeline pipeline(TextureTileMode tileMode, std::uint32_t elementBytes, bool retile, bool thick, bool image = false);
|
||||
void release() noexcept;
|
||||
struct DescriptorRing {
|
||||
@@ -100,6 +102,7 @@ namespace AgcDriver::Graphics {
|
||||
std::array<VkShaderModule, 2> readbackModules{};
|
||||
std::vector<std::pair<std::uint32_t, VkPipeline>> readbackPipelines;
|
||||
DescriptorRing readbackDescriptorSets;
|
||||
VkShaderModule multisampleUploadModule = VK_NULL_HANDLE;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
#version 450
|
||||
#extension GL_EXT_shader_image_load_formatted : require
|
||||
|
||||
layout(constant_id = 0) const uint ELEMENT_BYTES = 4u;
|
||||
layout(local_size_x = 64) in;
|
||||
layout(set = 0, binding = 0, std430) readonly buffer Input { uint data[]; } sourceBuffer;
|
||||
layout(set = 0, binding = 1) uniform uimage2DMSArray destinationImage;
|
||||
layout(push_constant) uniform Push {
|
||||
uint width;
|
||||
uint height;
|
||||
uint layers;
|
||||
uint fragments;
|
||||
uint firstElement;
|
||||
uint elements;
|
||||
uint rowThreads;
|
||||
} params;
|
||||
|
||||
void main() {
|
||||
uint index = gl_GlobalInvocationID.y * params.rowThreads + gl_GlobalInvocationID.x;
|
||||
if (index >= params.elements) return;
|
||||
uint element = params.firstElement + index;
|
||||
uint pixel = element / params.fragments;
|
||||
ivec3 coordinate = ivec3(pixel % params.width, (pixel / params.width) % params.height, pixel / (params.width * params.height));
|
||||
uint address = index * ELEMENT_BYTES;
|
||||
uvec4 value = uvec4(0u);
|
||||
if (ELEMENT_BYTES < 4u) {
|
||||
value.x = (sourceBuffer.data[address >> 2u] >> ((address & 3u) * 8u)) & (ELEMENT_BYTES == 1u ? 255u : 65535u);
|
||||
} else {
|
||||
for (uint component = 0u; component < ELEMENT_BYTES / 4u; ++component) value[component] = sourceBuffer.data[(address >> 2u) + component];
|
||||
}
|
||||
imageStore(destinationImage, coordinate, int(element % params.fragments), value);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -57,6 +58,7 @@ struct DepthGuard {
|
||||
std::size_t bytes;
|
||||
std::shared_ptr<std::atomic<bool>> active;
|
||||
std::vector<GuestMemory::UnchangedQuery> live;
|
||||
std::shared_ptr<std::atomic<bool>> hostWritten;
|
||||
};
|
||||
|
||||
std::shared_mutex& depthGuardMutex() {
|
||||
@@ -150,6 +152,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 +339,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");
|
||||
@@ -475,6 +493,7 @@ public:
|
||||
|
||||
void CopyAspectFrom(const DepthSurface& source, VkImageAspectFlagBits aspect) {
|
||||
ForgetClear(aspect);
|
||||
NoteHostWrite();
|
||||
auto* recorder = RecorderFor(context);
|
||||
std::unique_ptr<CommandBatch> batch;
|
||||
if (recorder == nullptr) batch = std::make_unique<CommandBatch>(context);
|
||||
@@ -524,7 +543,12 @@ public:
|
||||
return std::any_of(textures.begin(), textures.end(), [](const auto& entry) { return entry.second.use_count() > 1; });
|
||||
}
|
||||
|
||||
void NoteHostWrite() {
|
||||
hostWritten->store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
std::uint64_t written = ++depthWrites;
|
||||
std::shared_ptr<std::atomic<bool>> hostWritten = std::make_shared<std::atomic<bool>>(false);
|
||||
bool retired = false;
|
||||
|
||||
bool Overlaps(std::uint64_t address, std::uint64_t bytes) const {
|
||||
@@ -674,6 +698,7 @@ public:
|
||||
if (plane.storage.get() == &candidate) {
|
||||
if (stencil || !StoreOverwrite(candidate)) copyPlane(plane, plane.storage, stencil, false);
|
||||
written = ++depthWrites;
|
||||
NoteHostWrite();
|
||||
return true;
|
||||
}
|
||||
if (std::none_of(plane.replaced.begin(), plane.replaced.end(), [&](const auto& old) { return old.lock().get() == &candidate; })) continue;
|
||||
@@ -691,6 +716,7 @@ public:
|
||||
const auto source = std::const_pointer_cast<StorageTexture>(candidate.shared_from_this());
|
||||
copyPlane(plane, source, stencil, false);
|
||||
written = ++depthWrites;
|
||||
NoteHostWrite();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -722,42 +748,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,7 +920,9 @@ public:
|
||||
bool sampleable = true;
|
||||
bool samplingActive = false;
|
||||
DepthSamplingState sampling;
|
||||
bool clearPending = false;
|
||||
std::uint64_t clearGeneration = 0u;
|
||||
bool clearCompletionQueued = false;
|
||||
std::shared_ptr<OwnedImage> owned;
|
||||
std::shared_ptr<OwnedImage> sampleShadow;
|
||||
bool useSampleShadow = false;
|
||||
@@ -968,33 +1120,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 {
|
||||
@@ -1368,6 +1539,21 @@ DepthSurface* planeSurface(VkDevice device, std::uint64_t address) {
|
||||
return found == list.rend() ? nullptr : found->get();
|
||||
}
|
||||
|
||||
void settlePendingHtileClear(const Context& context, std::uint64_t address) {
|
||||
if (Recorder::InCompletion()) return;
|
||||
auto* recorder = RecorderFor(context);
|
||||
if (recorder == nullptr) return;
|
||||
std::pair<std::uint64_t, std::size_t> metadata;
|
||||
{
|
||||
std::lock_guard lock(surfacesMutex());
|
||||
const auto* surface = planeSurface(context.device, address);
|
||||
if (surface == nullptr || !surface->samplingActive || surface->clearGeneration == 0u || !surface->clearCompletionQueued || !surface->sampling.refusal.empty()) return;
|
||||
const auto& range = surface->sampling.initial[1];
|
||||
metadata = {range.address, range.bytes};
|
||||
}
|
||||
if (recorder->PendingWriteOverlaps(metadata.first, metadata.second)) recorder->SyncThrough(metadata.first, metadata.second);
|
||||
}
|
||||
|
||||
std::map<std::uint64_t, std::size_t>& clearedHtiles() {
|
||||
static auto* cleared = new std::map<std::uint64_t, std::size_t>();
|
||||
return *cleared;
|
||||
@@ -1604,15 +1790,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() {
|
||||
@@ -1646,8 +1853,28 @@ std::uint64_t MultisampledDepthBytes_nid_no_patch(VkExtent2D extent, std::uint32
|
||||
void RequireNoResidentDepthRead_nid_no_patch(std::uint64_t address, std::size_t bytes) {
|
||||
if (bytes == 0u || !anyDepthGuard().load(std::memory_order_acquire)) return;
|
||||
Require(address <= std::numeric_limits<std::uint64_t>::max() - bytes, "native depth read range overflows");
|
||||
std::shared_lock lock(depthGuardMutex());
|
||||
for (const auto& range : depthGuards()) Require(address >= range.address + range.bytes || range.address >= address + bytes, "sample-preserving guest publication of native multisample depth is not implemented");
|
||||
const auto overlaps = [&](const DepthGuard& range) { return address < range.address + range.bytes && range.address < address + bytes; };
|
||||
{
|
||||
std::shared_lock lock(depthGuardMutex());
|
||||
if (std::ranges::none_of(depthGuards(), overlaps)) return;
|
||||
}
|
||||
std::unique_lock lock(depthGuardMutex());
|
||||
for (const auto& range : depthGuards()) {
|
||||
if (overlaps(range)) Require(!range.hostWritten->load(std::memory_order_acquire), "sample-preserving guest publication of native multisample depth is not implemented");
|
||||
}
|
||||
for (const auto& range : depthGuards()) {
|
||||
if (overlaps(range)) range.active->store(false, std::memory_order_release);
|
||||
}
|
||||
std::erase_if(depthGuards(), [](const auto& range) { return !range.active->load(std::memory_order_acquire); });
|
||||
anyDepthGuard().store(!depthGuards().empty(), std::memory_order_release);
|
||||
}
|
||||
|
||||
void NoteDepthSurfaceWrite(const Context& context, const DepthTarget& target) {
|
||||
if (target.samples == 1u || !anyDepthGuard().load(std::memory_order_acquire)) return;
|
||||
std::lock_guard lock(surfacesMutex());
|
||||
for (const auto& surface : surfaces()) {
|
||||
if (surface->active->load(std::memory_order_acquire) && surface->context.device == context.device && sameSurface(surface->target, target)) surface->NoteHostWrite();
|
||||
}
|
||||
}
|
||||
|
||||
bool HasResidentDepthGuards_nid_no_patch() {
|
||||
@@ -1720,6 +1947,7 @@ float HostDepthBiasConstant(const Context& context, VkFormat format, float const
|
||||
VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target, bool depthWritten) {
|
||||
const auto htileCleared = RoutesToNativeDepthOwner(target) ? VkImageAspectFlags{0} : storeBackClearedHtile(context, target);
|
||||
std::lock_guard gpu(GuestMemory::GpuMutex());
|
||||
settlePendingHtileClear(context, target.address);
|
||||
std::unique_lock lock(surfacesMutex());
|
||||
const bool cleared = takeClearedHtile(target);
|
||||
DepthSurface* surface = nullptr;
|
||||
@@ -1756,7 +1984,7 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target,
|
||||
auto& fresh = *surfaces().back();
|
||||
if (target.samples != 1u && fresh.sampleable && fresh.sampling.refusal.empty()) {
|
||||
std::unique_lock guardLock(depthGuardMutex());
|
||||
for (const auto& range : fresh.sampling.initial) depthGuards().push_back({context.device, range.address, range.bytes, fresh.active, {{range.address, range.bytes, 0u}}});
|
||||
for (const auto& range : fresh.sampling.initial) depthGuards().push_back({context.device, range.address, range.bytes, fresh.active, {{range.address, range.bytes, 0u}}, fresh.hostWritten});
|
||||
anyDepthGuard().store(true, std::memory_order_release);
|
||||
InstallResidentImageReadGuard_nid_no_patch();
|
||||
fresh.samplingActive = true;
|
||||
@@ -1775,7 +2003,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 +2015,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 +2060,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 +2232,22 @@ 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;
|
||||
surface->clearCompletionQueued = false;
|
||||
completionCandidate = true;
|
||||
}
|
||||
existing = true;
|
||||
continue;
|
||||
}
|
||||
@@ -1997,6 +2257,39 @@ 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 QueueHtileDepthClear(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 || !surface->sampling.refusal.empty()) continue;
|
||||
if (bytes >= surface->sampling.initial[1].bytes) surface->clearCompletionQueued = true;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
surface->clearCompletionQueued = false;
|
||||
}
|
||||
}
|
||||
|
||||
void ClearDepthSurfaces(VkDevice device) {
|
||||
@@ -2024,6 +2317,7 @@ std::shared_ptr<Texture> DepthSurfaceTexture(const Context& context, std::span<c
|
||||
if (std::none_of(surfaces().begin(), surfaces().end(), held)) return nullptr;
|
||||
}
|
||||
std::lock_guard gpu(GuestMemory::GpuMutex());
|
||||
settlePendingHtileClear(context, resource.baseAddress);
|
||||
std::lock_guard lock(surfacesMutex());
|
||||
auto* found = planeSurface(context.device, resource.baseAddress);
|
||||
if (found == nullptr) return nullptr;
|
||||
@@ -2053,7 +2347,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();
|
||||
@@ -2086,6 +2380,7 @@ bool DepthSurfaceHolds(const Context& context, const GuestTextureResource& resou
|
||||
if ((resource.dimension != TextureDimension::k2D && resource.dimension != TextureDimension::k2DMsaa) || resource.baseArray != 0) return false;
|
||||
std::lock_guard gpu(GuestMemory::GpuMutex());
|
||||
const auto address = resource.baseAddress;
|
||||
settlePendingHtileClear(context, address);
|
||||
std::lock_guard lock(surfacesMutex());
|
||||
auto* found = planeSurface(context.device, address);
|
||||
if (found == nullptr || !found->Holds(texture)) return false;
|
||||
@@ -2247,7 +2542,10 @@ bool DepthSurfaceStoreBack(const Context& context, const StorageTexture& storage
|
||||
if (std::get<0>(key) != context.device || entry->storage.get() != &storage) continue;
|
||||
const auto slices = layeredSlices(context, *entry);
|
||||
entry->Copy(slices, false);
|
||||
for (const auto& slice : slices) slice.surface->written = ++depthWrites;
|
||||
for (const auto& slice : slices) {
|
||||
slice.surface->written = ++depthWrites;
|
||||
slice.surface->NoteHostWrite();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
for (const auto& surface : surfaces()) {
|
||||
|
||||
@@ -81,6 +81,10 @@ GuestTextureResource ArraySurfaceForTarget(const ColorTarget& color) {
|
||||
|
||||
namespace {
|
||||
|
||||
bool writesDepthTarget(const State& state) {
|
||||
return state.depthWrite || (state.stencilTest && (state.stencilFront.writeMask != 0u || state.stencilBack.writeMask != 0u));
|
||||
}
|
||||
|
||||
std::size_t colorKeyCount(const ColorTarget& color, std::size_t bytes) {
|
||||
if (color.mipCount != 1 || color.depth != 1 || !color.dccPipeAligned) return DccKeyBytes(bytes);
|
||||
return DccKeyCount(ColorTextureTileMode(color.tileMode), color.elementBytes, color.extent.width, color.extent.height, bytes);
|
||||
@@ -1043,9 +1047,14 @@ struct DrawInputs {
|
||||
VkPipelineStageFlags shaderStages = 0;
|
||||
std::uint32_t meshGroups = 0;
|
||||
std::vector<std::uint32_t> restartTable;
|
||||
std::uint32_t expandedIndexCount = 0;
|
||||
const VertexInputLayout& VertexLayout() const { return retainedVertexInput != nullptr ? *retainedVertexInput : vertexInput; }
|
||||
};
|
||||
|
||||
bool StripPatches(const State& state, std::span<const CompiledShader> shaders) {
|
||||
return state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP && BarycentricPatches(state, shaders);
|
||||
}
|
||||
|
||||
std::shared_ptr<Buffer> zeroVertexBuffer(const Context& context, const ShaderRecompiler::VertexAttribute& attribute) {
|
||||
auto zero = std::make_shared<Buffer>(context, DecodeVertexFormat(attribute).bytes, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
|
||||
std::fill(zero->Bytes().begin(), zero->Bytes().end(), std::byte{0});
|
||||
@@ -1129,19 +1138,58 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P
|
||||
ValidateSampleMask(context, state, shaders);
|
||||
timer.phase(PhaseValidate);
|
||||
inputs.maxIndex = draw.indexed ? 0u : draw.firstVertex + draw.indexCount - 1u;
|
||||
if (state.quadList) {
|
||||
Require(!draw.indirect, "indirect quad-list draw is unsupported");
|
||||
const std::uint32_t quadCount = draw.indexCount / 4u;
|
||||
if (quadCount == 0u || draw.instanceCount == 0u) {
|
||||
if (state.quadList || StripPatches(state, shaders)) {
|
||||
Require(!draw.indirect, state.quadList ? "indirect quad-list draw is unsupported" : "an indirect triangle strip through emulated barycentric patches is unsupported");
|
||||
const std::uint32_t primitives = state.quadList ? draw.indexCount / 4u : draw.indexCount >= 3u ? draw.indexCount - 2u : 0u;
|
||||
if (primitives == 0u || draw.instanceCount == 0u) {
|
||||
inputs.nothing = true;
|
||||
return inputs;
|
||||
}
|
||||
const std::uint32_t outputIndexCount = quadCount * 6u;
|
||||
const std::size_t quadIndexBytes = static_cast<std::size_t>(outputIndexCount) * sizeof(std::uint32_t);
|
||||
auto indexCopy = std::make_shared<Buffer>(context, quadIndexBytes, VK_BUFFER_USAGE_INDEX_BUFFER_BIT);
|
||||
const std::uint32_t corners = state.quadList ? 6u : 3u;
|
||||
const std::uint32_t outputIndexCount = primitives * corners;
|
||||
const std::size_t expandedIndexBytes = static_cast<std::size_t>(outputIndexCount) * sizeof(std::uint32_t);
|
||||
auto indexCopy = std::make_shared<Buffer>(context, expandedIndexBytes, VK_BUFFER_USAGE_INDEX_BUFFER_BIT);
|
||||
auto* outIndices = reinterpret_cast<std::uint32_t*>(indexCopy->Bytes().data());
|
||||
std::uint32_t maxIdx = 0;
|
||||
const bool lastProvoking = state.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT;
|
||||
const auto expand = [&](const auto& source) {
|
||||
for (std::uint32_t primitive = 0; primitive < primitives; ++primitive) {
|
||||
auto* out = outIndices + static_cast<std::size_t>(primitive) * corners;
|
||||
if (state.quadList) {
|
||||
const auto i0 = source(primitive * 4u + 0u);
|
||||
const auto i1 = source(primitive * 4u + 1u);
|
||||
const auto i2 = source(primitive * 4u + 2u);
|
||||
const auto i3 = source(primitive * 4u + 3u);
|
||||
if (lastProvoking) {
|
||||
out[0] = i0;
|
||||
out[1] = i1;
|
||||
out[2] = i3;
|
||||
out[3] = i1;
|
||||
out[4] = i2;
|
||||
out[5] = i3;
|
||||
} else {
|
||||
out[0] = i0;
|
||||
out[1] = i1;
|
||||
out[2] = i2;
|
||||
out[3] = i0;
|
||||
out[4] = i2;
|
||||
out[5] = i3;
|
||||
}
|
||||
} else {
|
||||
const std::uint32_t odd = primitive & 1u;
|
||||
if (lastProvoking) {
|
||||
out[0] = source(primitive + odd);
|
||||
out[1] = source(primitive + 1u - odd);
|
||||
out[2] = source(primitive + 2u);
|
||||
} else {
|
||||
out[0] = source(primitive);
|
||||
out[1] = source(primitive + 1u + odd);
|
||||
out[2] = source(primitive + 2u - odd);
|
||||
}
|
||||
}
|
||||
for (std::uint32_t corner = 0; corner < corners; ++corner) maxIdx = std::max(maxIdx, out[corner]);
|
||||
}
|
||||
};
|
||||
if (draw.indexed) {
|
||||
GuestMemory::FlushGpuWrites(draw.indexAddress, static_cast<std::size_t>(indexBytes));
|
||||
GuestMemory::CheckRange(reinterpret_cast<const void*>(draw.indexAddress), static_cast<std::size_t>(indexBytes), draw.indexSize);
|
||||
@@ -1156,59 +1204,18 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P
|
||||
return v;
|
||||
}
|
||||
};
|
||||
for (std::uint32_t q = 0; q < quadCount; ++q) {
|
||||
const auto i0 = readIndex(q * 4u + 0u);
|
||||
const auto i1 = readIndex(q * 4u + 1u);
|
||||
const auto i2 = readIndex(q * 4u + 2u);
|
||||
const auto i3 = readIndex(q * 4u + 3u);
|
||||
maxIdx = std::max({maxIdx, i0, i1, i2, i3});
|
||||
if (lastProvoking) {
|
||||
outIndices[q * 6u + 0u] = i0;
|
||||
outIndices[q * 6u + 1u] = i1;
|
||||
outIndices[q * 6u + 2u] = i3;
|
||||
outIndices[q * 6u + 3u] = i1;
|
||||
outIndices[q * 6u + 4u] = i2;
|
||||
outIndices[q * 6u + 5u] = i3;
|
||||
} else {
|
||||
outIndices[q * 6u + 0u] = i0;
|
||||
outIndices[q * 6u + 1u] = i1;
|
||||
outIndices[q * 6u + 2u] = i2;
|
||||
outIndices[q * 6u + 3u] = i0;
|
||||
outIndices[q * 6u + 4u] = i2;
|
||||
outIndices[q * 6u + 5u] = i3;
|
||||
}
|
||||
}
|
||||
expand(readIndex);
|
||||
Require(draw.firstVertex <= std::numeric_limits<std::uint32_t>::max() - maxIdx, "indexed draw vertex range overflow");
|
||||
inputs.maxIndex = maxIdx + draw.firstVertex;
|
||||
} else {
|
||||
maxIdx = quadCount * 4u - 1u;
|
||||
Require(draw.firstVertex <= std::numeric_limits<std::uint32_t>::max() - maxIdx, "auto draw vertex range overflow");
|
||||
for (std::uint32_t q = 0; q < quadCount; ++q) {
|
||||
const auto i0 = q * 4u + 0u;
|
||||
const auto i1 = q * 4u + 1u;
|
||||
const auto i2 = q * 4u + 2u;
|
||||
const auto i3 = q * 4u + 3u;
|
||||
if (lastProvoking) {
|
||||
outIndices[q * 6u + 0u] = i0;
|
||||
outIndices[q * 6u + 1u] = i1;
|
||||
outIndices[q * 6u + 2u] = i3;
|
||||
outIndices[q * 6u + 3u] = i1;
|
||||
outIndices[q * 6u + 4u] = i2;
|
||||
outIndices[q * 6u + 5u] = i3;
|
||||
} else {
|
||||
outIndices[q * 6u + 0u] = i0;
|
||||
outIndices[q * 6u + 1u] = i1;
|
||||
outIndices[q * 6u + 2u] = i2;
|
||||
outIndices[q * 6u + 3u] = i0;
|
||||
outIndices[q * 6u + 4u] = i2;
|
||||
outIndices[q * 6u + 5u] = i3;
|
||||
}
|
||||
}
|
||||
inputs.maxIndex = maxIdx + draw.firstVertex;
|
||||
const std::uint32_t lastVertex = state.quadList ? primitives * 4u - 1u : primitives + 1u;
|
||||
Require(draw.firstVertex <= std::numeric_limits<std::uint32_t>::max() - lastVertex, "auto draw vertex range overflow");
|
||||
expand([](std::uint32_t idx) { return idx; });
|
||||
}
|
||||
inputs.maxIndex = maxIdx + draw.firstVertex;
|
||||
Require(inputs.maxIndex <= context.limits.maxDrawIndexedIndexValue, "index exceeds the device's indexed draw limit");
|
||||
inputs.indexHandle = indexCopy->Handle();
|
||||
inputs.indexOffset = 0;
|
||||
inputs.expandedIndexCount = outputIndexCount;
|
||||
inputs.indices = std::move(indexCopy);
|
||||
} else if (draw.indexed) {
|
||||
const bool listTopology = state.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
@@ -1491,9 +1498,9 @@ void recordDrawCommands(const Context& context, VkCommandBuffer commands, const
|
||||
return;
|
||||
}
|
||||
if (!inputs.vertexHandles.empty()) context.Resolved(&DeviceFunctions::cmdBindVertexBuffers, "vkCmdBindVertexBuffers")(commands, 0, static_cast<std::uint32_t>(inputs.vertexHandles.size()), inputs.vertexHandles.data(), inputs.vertexOffsets.data());
|
||||
if (state.quadList) {
|
||||
if (inputs.expandedIndexCount != 0u) {
|
||||
context.Resolved(&DeviceFunctions::cmdBindIndexBuffer, "vkCmdBindIndexBuffer")(commands, inputs.indexHandle, inputs.indexOffset, VK_INDEX_TYPE_UINT32);
|
||||
context.Resolved(&DeviceFunctions::cmdDrawIndexed, "vkCmdDrawIndexed")(commands, (draw.indexCount / 4u) * 6u, draw.instanceCount, 0, static_cast<std::int32_t>(draw.firstVertex), draw.firstInstance);
|
||||
context.Resolved(&DeviceFunctions::cmdDrawIndexed, "vkCmdDrawIndexed")(commands, inputs.expandedIndexCount, draw.instanceCount, 0, static_cast<std::int32_t>(draw.firstVertex), draw.firstInstance);
|
||||
return;
|
||||
}
|
||||
if (draw.indexed) context.Resolved(&DeviceFunctions::cmdBindIndexBuffer, "vkCmdBindIndexBuffer")(commands, inputs.indexHandle, inputs.indexOffset, draw.indexSize == 2 ? VK_INDEX_TYPE_UINT16 : VK_INDEX_TYPE_UINT32);
|
||||
@@ -2148,8 +2155,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");
|
||||
@@ -2172,60 +2237,37 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
|
||||
Require(state.samples == 1u && (!state.depth || HasStencil(state.depth->format)), "stencil replay requires single-sample rendering and an eight-bit stencil format");
|
||||
recipeOut = nullptr;
|
||||
}
|
||||
if (state.quadList) recipeOut = nullptr;
|
||||
if (state.quadList || StripPatches(state, shaders)) 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 +2282,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 +2306,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;
|
||||
}
|
||||
@@ -2317,12 +2359,16 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
|
||||
binding.target = std::make_unique<RenderTarget>(context, color, state.blends.at(color.exportIndex).blendEnable != 0);
|
||||
targetViews.push_back(binding.target->View());
|
||||
}
|
||||
if (state.depth) targetViews.push_back(DepthSurfaceView(context, *state.depth, state.depthWrite));
|
||||
if (state.depth) {
|
||||
targetViews.push_back(DepthSurfaceView(context, *state.depth, state.depthWrite));
|
||||
if (writesDepthTarget(state)) NoteDepthSurfaceWrite(context, *state.depth);
|
||||
}
|
||||
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 +2408,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 +2431,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 +2452,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 +2477,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 +2494,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 +2503,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 +2516,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 +2532,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 +2542,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 +2566,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 +2934,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);
|
||||
@@ -2866,7 +2965,7 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con
|
||||
};
|
||||
Require(!draw.indirect, "a draw recipe covers direct draws only");
|
||||
auto* recorder = Recorder::Active();
|
||||
if (state.quadList || !RecordDraws() || recorder == nullptr || DumpTargetLimit() != 0) return miss(DrawRecipeMiss::NotRecordable);
|
||||
if (state.quadList || StripPatches(state, shaders) || !RecordDraws() || recorder == nullptr || DumpTargetLimit() != 0) return miss(DrawRecipeMiss::NotRecordable);
|
||||
if (recipe.hasDepth != state.depth.has_value()) return miss(DrawRecipeMiss::TargetGone);
|
||||
const std::size_t expectedViews = state.colors.size() + (recipe.hasDepth ? 1u : 0u);
|
||||
Require(recipe.targets.size() == state.colors.size() && recipe.targetViews.size() == expectedViews, "draw recipe targets do not match the state");
|
||||
@@ -2888,7 +2987,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);
|
||||
@@ -2898,6 +2998,7 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con
|
||||
if (recipe.hasDepth) {
|
||||
timer.phase(PhaseSetup);
|
||||
const auto depthView = DepthSurfaceView(context, *state.depth, state.depthWrite);
|
||||
if (writesDepthTarget(state)) NoteDepthSurfaceWrite(context, *state.depth);
|
||||
timer.phase(PhaseReadTarget);
|
||||
if (recipe.targetViews.back() != depthView) return miss(DrawRecipeMiss::TargetGone);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
@@ -4636,12 +4542,6 @@ void GuestBufferMemory::UploadFinish(bool addressable) {
|
||||
if (profile) refreshUs += microsecondsSince(refreshStart);
|
||||
continue;
|
||||
}
|
||||
if (sparseHeapCandidate(region)) {
|
||||
const auto refreshStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
|
||||
const bool bound = bindSparseMirror(region, importRanges());
|
||||
if (profile) refreshUs += microsecondsSince(refreshStart);
|
||||
if (bound) continue;
|
||||
}
|
||||
bool copyOnGpu = false;
|
||||
if (context.hostImportAlignment != 0) {
|
||||
auto& state = Imports();
|
||||
@@ -4728,6 +4628,12 @@ void GuestBufferMemory::UploadFinish(bool addressable) {
|
||||
gpuCopies.push_back(®ion);
|
||||
continue;
|
||||
}
|
||||
if (sparseHeapCandidate(region)) {
|
||||
const auto refreshStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
|
||||
const bool bound = bindSparseMirror(region, importRanges());
|
||||
if (profile) refreshUs += microsecondsSince(refreshStart);
|
||||
if (bound) continue;
|
||||
}
|
||||
if (region.deviceLocal) {
|
||||
// The staging shadow UploadPrepare made cannot take the CPU copy (no host mapping): the
|
||||
// region falls back to a host buffer of its own.
|
||||
@@ -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;
|
||||
|
||||
@@ -49,6 +49,16 @@ bool sameTarget(const ColorTarget& a, const ColorTarget& b) {
|
||||
return a.surfaceAddress == b.surfaceAddress && a.format == b.format && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.samples == b.samples;
|
||||
}
|
||||
|
||||
constexpr VkImageUsageFlags twinViewUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
||||
|
||||
VkFormat colorTwinFormat(const Context& context, VkFormat format) {
|
||||
if (const auto srgb = SrgbTwinFormat(context, format); srgb != VK_FORMAT_UNDEFINED) return srgb;
|
||||
for (const auto unorm : {VK_FORMAT_R8_UNORM, VK_FORMAT_R8G8_UNORM, VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_A8B8G8R8_UNORM_PACK32}) {
|
||||
if (SrgbTwinFormat(context, unorm) == format) return unorm;
|
||||
}
|
||||
return VK_FORMAT_UNDEFINED;
|
||||
}
|
||||
|
||||
bool recreated(const ColorTarget& resident, const ColorTarget& use) {
|
||||
return use.dccAddress != 0 && use.pipeBankXor != resident.pipeBankXor && use.address == resident.address && use.dccAddress == resident.dccAddress && use.dccControl == resident.dccControl && use.dccAlphaOnMsb == resident.dccAlphaOnMsb && use.dccPipeAligned == resident.dccPipeAligned && use.cmaskAddress == resident.cmaskAddress && use.cmaskBytes == resident.cmaskBytes && use.tileMode == resident.tileMode && use.elementBytes == resident.elementBytes;
|
||||
}
|
||||
@@ -101,6 +111,13 @@ public:
|
||||
return context.imageFormatProperties(context.physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, formatUsage, flags, &properties) == VK_SUCCESS && (properties.sampleCounts & samples) != 0;
|
||||
};
|
||||
if (storageFormat != VK_FORMAT_UNDEFINED && (!context.storageImageMultisample || !storageSupports(storageFormat, VK_IMAGE_USAGE_STORAGE_BIT, 0u) || !storageSupports(target.format, usage | VK_IMAGE_USAGE_STORAGE_BIT, storageFlags))) storageFormat = VK_FORMAT_UNDEFINED;
|
||||
twinFormat = colorTwinFormat(context, target.format);
|
||||
if (twinFormat != VK_FORMAT_UNDEFINED) {
|
||||
VkFormatProperties twinProperties{};
|
||||
context.formatProperties(context.physical, twinFormat, &twinProperties);
|
||||
const VkImageCreateFlags twinFlags = orderFlags | VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
||||
if ((twinProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) == 0 || !storageSupports(target.format, usage, twinFlags) || !storageSupports(twinFormat, twinViewUsage, twinFlags)) twinFormat = VK_FORMAT_UNDEFINED;
|
||||
}
|
||||
try {
|
||||
const auto created = allocate(usage, orderFlags);
|
||||
image = created.image;
|
||||
@@ -135,6 +152,16 @@ public:
|
||||
try {
|
||||
VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
||||
info.flags = flags;
|
||||
const std::array<VkFormat, 2> viewFormats{target.format, twinFormat};
|
||||
VkImageFormatListCreateInfo formatList{VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO};
|
||||
if (twinFormat != VK_FORMAT_UNDEFINED) {
|
||||
if ((flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0 && context.imageFormatList) {
|
||||
formatList.viewFormatCount = static_cast<std::uint32_t>(viewFormats.size());
|
||||
formatList.pViewFormats = viewFormats.data();
|
||||
info.pNext = &formatList;
|
||||
}
|
||||
info.flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
||||
}
|
||||
info.imageType = VK_IMAGE_TYPE_2D;
|
||||
info.format = target.format;
|
||||
info.extent = {target.extent.width, target.extent.height, 1};
|
||||
@@ -203,6 +230,7 @@ public:
|
||||
Recorder::CountBarriers(Recorder::CommandClass::Draw, 3);
|
||||
for (auto& [key, texture] : textures) retiredTextures.push_back(std::move(texture));
|
||||
textures.clear();
|
||||
retired.push_back({VK_NULL_HANDLE, VK_NULL_HANDLE, std::exchange(twinView, VK_NULL_HANDLE)});
|
||||
retired.push_back({image, memory, view});
|
||||
image = storable.image;
|
||||
memory = storable.memory;
|
||||
@@ -336,14 +364,39 @@ public:
|
||||
return {shared_from_this(), texture.get()};
|
||||
}
|
||||
|
||||
VkRenderPass ResolvePass() {
|
||||
VkImageView View(VkFormat format) {
|
||||
if (format == target.format) return view;
|
||||
Require(twinFormat != VK_FORMAT_UNDEFINED && format == twinFormat, "a multisampled color target is only viewed in its own format or its sRGB twin");
|
||||
if (twinView == VK_NULL_HANDLE) {
|
||||
VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
|
||||
viewInfo.image = image;
|
||||
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
viewInfo.format = twinFormat;
|
||||
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
VkImageViewUsageCreateInfo viewUsage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO};
|
||||
viewUsage.usage = twinViewUsage;
|
||||
viewInfo.pNext = &viewUsage;
|
||||
Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &twinView), "vkCreateImageView multisampled target twin");
|
||||
}
|
||||
return twinView;
|
||||
}
|
||||
|
||||
bool Serves(const ColorTarget& use) const {
|
||||
if (use.format == target.format) return sameTarget(target, use);
|
||||
auto twin = use;
|
||||
twin.format = target.format;
|
||||
return twinFormat != VK_FORMAT_UNDEFINED && use.format == twinFormat && sameTarget(target, twin);
|
||||
}
|
||||
|
||||
VkRenderPass ResolvePass(VkFormat format) {
|
||||
auto& resolvePass = resolvePasses[format];
|
||||
if (resolvePass != VK_NULL_HANDLE) return resolvePass;
|
||||
std::array<VkAttachmentDescription, 2> attachments{};
|
||||
attachments[0].format = target.format;
|
||||
attachments[0].format = format;
|
||||
attachments[0].samples = static_cast<VkSampleCountFlagBits>(target.samples);
|
||||
attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
|
||||
attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||||
attachments[1].format = target.format;
|
||||
attachments[1].format = format;
|
||||
attachments[1].samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
||||
attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||||
@@ -376,7 +429,7 @@ public:
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VkDeviceMemory memory = VK_NULL_HANDLE;
|
||||
VkImageView view = VK_NULL_HANDLE;
|
||||
VkRenderPass resolvePass = VK_NULL_HANDLE;
|
||||
std::map<VkFormat, VkRenderPass> resolvePasses;
|
||||
MultisampledCmask cmask;
|
||||
MultisampledColorFill colorFill;
|
||||
bool fromTexture = false;
|
||||
@@ -391,6 +444,8 @@ private:
|
||||
VkImageUsageFlags usage = 0;
|
||||
VkFormat storageFormat = VK_FORMAT_UNDEFINED;
|
||||
VkImageCreateFlags storageFlags = 0;
|
||||
VkFormat twinFormat = VK_FORMAT_UNDEFINED;
|
||||
VkImageView twinView = VK_NULL_HANDLE;
|
||||
std::vector<Allocation> retired;
|
||||
VkImageView storageView = VK_NULL_HANDLE;
|
||||
bool storageWritten = false;
|
||||
@@ -487,8 +542,9 @@ private:
|
||||
void release() noexcept {
|
||||
textures.clear();
|
||||
retiredTextures.clear();
|
||||
if (resolvePass) context.Function<PFN_vkDestroyRenderPass>("vkDestroyRenderPass")(context.device, resolvePass, nullptr);
|
||||
for (const auto& [format, resolvePass] : resolvePasses) context.Function<PFN_vkDestroyRenderPass>("vkDestroyRenderPass")(context.device, resolvePass, nullptr);
|
||||
if (orderView) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, orderView, nullptr);
|
||||
if (twinView) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, twinView, nullptr);
|
||||
if (storageView) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, storageView, nullptr);
|
||||
destroy({image, memory, view});
|
||||
for (const auto& allocation : retired) destroy(allocation);
|
||||
@@ -610,11 +666,13 @@ void discardRetiredTargets() {
|
||||
std::erase_if(targets(), [](const auto& target) { return !target->active->load(std::memory_order_acquire); });
|
||||
}
|
||||
|
||||
MultisampleTarget& acquire(const Context& context, const ColorTarget& use) {
|
||||
MultisampleTarget& acquire(const Context& context, const ColorTarget& requested) {
|
||||
discardRetiredTargets();
|
||||
for (const auto& target : targets()) {
|
||||
if (!target->active->load(std::memory_order_acquire)) continue;
|
||||
if (target->context.device != context.device || !sameTarget(target->target, use)) continue;
|
||||
if (target->context.device != context.device || !target->Serves(requested)) continue;
|
||||
auto use = requested;
|
||||
use.format = target->target.format;
|
||||
if (recreated(target->target, use)) {
|
||||
{
|
||||
std::unique_lock lock(guardedMutex());
|
||||
@@ -636,6 +694,7 @@ MultisampleTarget& acquire(const Context& context, const ColorTarget& use) {
|
||||
target->ApplyFastClear(use);
|
||||
return *target;
|
||||
}
|
||||
const auto& use = requested;
|
||||
auto target = std::make_shared<MultisampleTarget>(context, use);
|
||||
target->Keep();
|
||||
targets().push_back(std::move(target));
|
||||
@@ -956,9 +1015,9 @@ SampleOrderTarget MultisampleTargetOrderTarget(const Context& context, const Col
|
||||
|
||||
VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target, VkImage* image) {
|
||||
std::lock_guard lock(targetsMutex());
|
||||
const auto& acquired = acquire(context, target);
|
||||
auto& acquired = acquire(context, target);
|
||||
if (image != nullptr) *image = acquired.image;
|
||||
return acquired.view;
|
||||
return acquired.View(target.format);
|
||||
}
|
||||
|
||||
VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target, std::shared_ptr<void>& owner, VkImage* image) {
|
||||
@@ -966,7 +1025,7 @@ VkImageView MultisampleTargetView(const Context& context, const ColorTarget& tar
|
||||
auto& acquired = acquire(context, target);
|
||||
owner = acquired.shared_from_this();
|
||||
if (image != nullptr) *image = acquired.image;
|
||||
return acquired.view;
|
||||
return acquired.View(target.format);
|
||||
}
|
||||
|
||||
void KeepMultisampleTarget_nid_no_patch(const std::shared_ptr<void>& owner) {
|
||||
@@ -1052,12 +1111,12 @@ void ResolveMultisampleTarget(const Context& context, const ColorTarget& source,
|
||||
std::lock_guard lock(targetsMutex());
|
||||
auto& target = acquire(context, source);
|
||||
target.Keep();
|
||||
const std::array<VkImageView, 2> views{target.view, destinationView};
|
||||
auto framebuffer = std::make_shared<Framebuffer>(context, target.ResolvePass(), views, source.extent);
|
||||
const std::array<VkImageView, 2> views{target.View(source.format), destinationView};
|
||||
auto framebuffer = std::make_shared<Framebuffer>(context, target.ResolvePass(source.format), views, source.extent);
|
||||
recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) {
|
||||
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT);
|
||||
VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
|
||||
begin.renderPass = target.ResolvePass();
|
||||
begin.renderPass = target.ResolvePass(source.format);
|
||||
begin.framebuffer = framebuffer->Handle();
|
||||
begin.renderArea = {{0, 0}, source.extent};
|
||||
context.Resolved(&DeviceFunctions::cmdBeginRenderPass, "vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE);
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTextureLayout.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/MultisampleSwizzleEquations.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Context.hpp"
|
||||
#include <bit>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
|
||||
namespace AgcDriver::Graphics {
|
||||
namespace {
|
||||
|
||||
std::array<std::uint32_t, 16> inverseBlock(const MultisampleSwizzleEquation& equation, std::uint32_t byteBits, std::uint32_t fragmentBits, std::uint32_t widthBits) {
|
||||
std::array<std::uint32_t, 16> rows{}, inverse{};
|
||||
for (std::uint32_t input = 0; input < 16u; ++input) {
|
||||
std::uint32_t address = 0u;
|
||||
if (input < byteBits) address = 1u << input;
|
||||
else {
|
||||
const auto coordinateBit = input < byteBits + fragmentBits ? 48u + input - byteBits : input < byteBits + fragmentBits + widthBits ? input - byteBits - fragmentBits : 16u + input - byteBits - fragmentBits - widthBits;
|
||||
const auto coordinate = std::uint64_t{1} << coordinateBit;
|
||||
for (std::uint32_t output = 0; output < 16u; ++output) address |= static_cast<std::uint32_t>(std::popcount(coordinate & equation.bits[output]) & 1u) << output;
|
||||
}
|
||||
for (std::uint32_t output = 0; output < 16u; ++output) rows[output] |= ((address >> output) & 1u) << input;
|
||||
inverse[input] = 1u << input;
|
||||
}
|
||||
for (std::uint32_t input = 0; input < 16u; ++input) {
|
||||
auto pivot = input;
|
||||
while (pivot < 16u && (rows[pivot] & (1u << input)) == 0u) ++pivot;
|
||||
Require(pivot < 16u, "multisample block address equation is not invertible");
|
||||
std::swap(rows[input], rows[pivot]);
|
||||
std::swap(inverse[input], inverse[pivot]);
|
||||
for (std::uint32_t row = 0; row < 16u; ++row) {
|
||||
if (row == input || (rows[row] & (1u << input)) == 0u) continue;
|
||||
rows[row] ^= rows[input];
|
||||
inverse[row] ^= inverse[input];
|
||||
}
|
||||
}
|
||||
return inverse;
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> axisOffsets(const MultisampleSwizzleEquation& equation, std::uint32_t count, std::uint32_t shift) {
|
||||
std::vector<std::uint32_t> offsets(count);
|
||||
for (std::uint32_t value = 0; value < count; ++value) {
|
||||
const auto coordinate = static_cast<std::uint64_t>(value) << shift;
|
||||
for (std::uint32_t bit = 0; bit < equation.bits.size(); ++bit) {
|
||||
offsets[value] |= static_cast<std::uint32_t>(std::popcount(coordinate & equation.bits[bit]) & 1u) << bit;
|
||||
}
|
||||
}
|
||||
return offsets;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MultisampleTextureLayout::MultisampleTextureLayout(TextureTileMode tileMode, std::uint32_t elementBytes, std::uint32_t fragments, std::uint32_t width, std::uint32_t height, std::uint32_t layers, std::uint32_t pipeBankXor) : elementBytes(elementBytes), fragments(fragments), width(width), height(height), layers(layers), pipeBankXor(pipeBankXor) {
|
||||
Require(width != 0 && height != 0 && width <= 16384u && height <= 16384u && layers != 0 && layers <= 8192u, "invalid multisampled surface extent or layer count");
|
||||
Require(std::has_single_bit(elementBytes) && elementBytes <= 16u, "unsupported multisampled surface element size");
|
||||
Require(fragments == 2u || fragments == 4u || fragments == 8u, "unsupported multisampled surface fragment count");
|
||||
Require((pipeBankXor & ~0xcf00u) == 0, "multisampled surface pipe/bank XOR uses bits outside its 16-pipe swizzle");
|
||||
Require(tileMode != TextureTileMode::kZ64KBX || elementBytes <= 4u, "multisampled Z surface element size exceeds 32 bits");
|
||||
const auto* equation = FindMultisampleSwizzleEquation(tileMode, elementBytes, fragments);
|
||||
Require(equation != nullptr, "unsupported multisampled surface tile mode");
|
||||
|
||||
const auto fragmentBits = static_cast<std::uint32_t>(std::countr_zero(fragments));
|
||||
const auto elementBits = 16u - static_cast<std::uint32_t>(std::countr_zero(elementBytes)) - fragmentBits;
|
||||
const auto widthBits = (elementBits + ((fragmentBits & 1u) == 0 ? 1u : 0u)) / 2u;
|
||||
blockWidth = 1u << widthBits;
|
||||
blockHeight = 1u << (elementBits - widthBits);
|
||||
inverseBlockEquation = inverseBlock(*equation, static_cast<std::uint32_t>(std::countr_zero(elementBytes)), fragmentBits, widthBits);
|
||||
pitch = (width + blockWidth - 1u) / blockWidth * blockWidth;
|
||||
paddedHeight = (height + blockHeight - 1u) / blockHeight * blockHeight;
|
||||
sliceBytes = static_cast<std::uint64_t>(pitch) * paddedHeight * fragments * elementBytes;
|
||||
Require(sliceBytes <= std::numeric_limits<std::uint64_t>::max() / layers, "multisampled surface size overflows");
|
||||
xOffsets = axisOffsets(*equation, width, 0u);
|
||||
yOffsets = axisOffsets(*equation, height, 16u);
|
||||
layerOffsets = axisOffsets(*equation, layers, 32u);
|
||||
fragmentOffsets = axisOffsets(*equation, fragments, 48u);
|
||||
}
|
||||
|
||||
std::uint64_t MultisampleTextureLayout::offset(std::uint32_t x, std::uint32_t y, std::uint32_t layer, std::uint32_t fragment) const {
|
||||
const auto block = static_cast<std::uint64_t>(y / blockHeight) * (pitch / blockWidth) + x / blockWidth;
|
||||
const auto withinBlock = xOffsets[x] ^ yOffsets[y] ^ layerOffsets[layer] ^ fragmentOffsets[fragment] ^ pipeBankXor;
|
||||
return static_cast<std::uint64_t>(layer) * sliceBytes + block * Alignment + withinBlock;
|
||||
}
|
||||
|
||||
std::uint64_t MultisampleTextureLayout::Offset(std::uint32_t x, std::uint32_t y, std::uint32_t layer, std::uint32_t fragment) const {
|
||||
Require(x < width && y < height && layer < layers && fragment < fragments, "multisampled surface coordinate is outside its extent");
|
||||
return offset(x, y, layer, fragment);
|
||||
}
|
||||
|
||||
void MultisampleTextureLayout::transfer(std::span<const std::byte> source, std::span<std::byte> destination, bool retile) const {
|
||||
const auto sourceBytes = retile ? LinearBytes() : Bytes();
|
||||
const auto destinationBytes = retile ? Bytes() : LinearBytes();
|
||||
Require(source.size() >= sourceBytes && destination.size() >= destinationBytes, "multisampled surface transfer buffer is too small");
|
||||
const auto sourceAddress = reinterpret_cast<std::uintptr_t>(source.data());
|
||||
const auto destinationAddress = reinterpret_cast<std::uintptr_t>(destination.data());
|
||||
Require(sourceAddress <= std::numeric_limits<std::uintptr_t>::max() - sourceBytes && destinationAddress <= std::numeric_limits<std::uintptr_t>::max() - destinationBytes, "multisampled surface transfer range overflows");
|
||||
Require(sourceAddress + sourceBytes <= destinationAddress || destinationAddress + destinationBytes <= sourceAddress, "multisampled surface transfer buffers overlap");
|
||||
|
||||
std::uint64_t linear = 0;
|
||||
for (std::uint32_t layer = 0; layer < layers; ++layer) {
|
||||
for (std::uint32_t y = 0; y < height; ++y) {
|
||||
for (std::uint32_t x = 0; x < width; ++x) {
|
||||
for (std::uint32_t fragment = 0; fragment < fragments; ++fragment) {
|
||||
const auto tiled = offset(x, y, layer, fragment);
|
||||
std::memcpy(destination.data() + (retile ? tiled : linear), source.data() + (retile ? linear : tiled), elementBytes);
|
||||
linear += elementBytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MultisampleTextureLayout::Detile(std::span<const std::byte> source, std::span<std::byte> destination) const {
|
||||
transfer(source, destination, false);
|
||||
}
|
||||
|
||||
void MultisampleTextureLayout::Tile(std::span<const std::byte> source, std::span<std::byte> destination) const {
|
||||
transfer(source, destination, true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/shaders/MultisampleUpload_spv.h"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <limits>
|
||||
|
||||
namespace AgcDriver::Graphics {
|
||||
|
||||
VkPipeline TextureDetiler::multisampleUploadPipeline(std::uint32_t elementBytes) {
|
||||
const auto key = (1u << 19u) | elementBytes;
|
||||
for (const auto& entry : pipelines) if (entry.first == key) return entry.second;
|
||||
if (multisampleUploadModule == VK_NULL_HANDLE) {
|
||||
VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
||||
info.codeSize = sizeof(MULTISAMPLE_UPLOAD_SPV);
|
||||
info.pCode = MULTISAMPLE_UPLOAD_SPV;
|
||||
Check(context.Function<PFN_vkCreateShaderModule>("vkCreateShaderModule")(context.device, &info, nullptr, &multisampleUploadModule), "vkCreateShaderModule multisample upload");
|
||||
}
|
||||
const VkSpecializationMapEntry entry{0u, 0u, sizeof(elementBytes)};
|
||||
const VkSpecializationInfo specialization{1u, &entry, sizeof(elementBytes), &elementBytes};
|
||||
VkComputePipelineCreateInfo info{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO};
|
||||
info.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO};
|
||||
info.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT;
|
||||
info.stage.module = multisampleUploadModule;
|
||||
info.stage.pName = "main";
|
||||
info.stage.pSpecializationInfo = &specialization;
|
||||
info.layout = imagePipelineLayout;
|
||||
pipelines.reserve(pipelines.size() + 1u);
|
||||
VkPipeline result = VK_NULL_HANDLE;
|
||||
{
|
||||
auto pipelineCacheLock = context.LockPipelineCacheForUse();
|
||||
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1u, &info, nullptr, &result), "vkCreateComputePipelines multisample upload");
|
||||
}
|
||||
pipelines.emplace_back(key, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
void TextureDetiler::DispatchMultisampleImage(VkCommandBuffer commands, std::uint32_t elementBytes, VkBuffer source, std::uint64_t offset, std::uint64_t bytes, VkImageView arrayView, std::uint32_t width, std::uint32_t height, std::uint32_t layers, std::uint32_t fragments, std::uint32_t firstElement, std::uint32_t elements) {
|
||||
Require(context.storageImageMultisample && context.storageImageWriteWithoutFormat, "device does not enable multisampled storage image writes without format");
|
||||
Require(commands != VK_NULL_HANDLE && source != VK_NULL_HANDLE && arrayView != VK_NULL_HANDLE, "multisample upload requires commands, buffer and array view");
|
||||
Require(std::has_single_bit(elementBytes) && elementBytes <= 16u, "multisample upload element size is unsupported");
|
||||
Require(width != 0u && width <= 16384u && height != 0u && height <= 16384u && layers != 0u && layers <= 8192u, "multisample upload extent is unsupported");
|
||||
Require(fragments == 2u || fragments == 4u || fragments == 8u, "multisample upload fragment count is unsupported");
|
||||
const auto total = static_cast<std::uint64_t>(width) * height * layers * fragments;
|
||||
Require(total <= std::numeric_limits<std::uint32_t>::max() && elements != 0u && static_cast<std::uint64_t>(firstElement) + elements <= total, "multisample upload element range is invalid");
|
||||
const auto required = (static_cast<std::uint64_t>(elements) * elementBytes + 3u) & ~3ull;
|
||||
const auto alignment = std::max<std::uint64_t>(4u, context.limits.minStorageBufferOffsetAlignment);
|
||||
Require(offset % alignment == 0u && bytes >= required && bytes <= context.limits.maxStorageBufferRange && bytes % 4u == 0u && offset <= std::numeric_limits<std::uint64_t>::max() - bytes, "multisample upload buffer range is invalid");
|
||||
Require(context.limits.maxComputeWorkGroupInvocations >= 64u && context.limits.maxComputeWorkGroupSize[0] >= 64u && context.limits.maxComputeWorkGroupSize[1] >= 1u && context.limits.maxComputeWorkGroupSize[2] >= 1u && context.limits.maxComputeWorkGroupCount[2] >= 1u, "device does not support the multisample upload workgroup");
|
||||
const auto groups = (static_cast<std::uint64_t>(elements) + 63u) / 64u;
|
||||
const auto groupsX = std::min<std::uint64_t>(groups, context.limits.maxComputeWorkGroupCount[0]);
|
||||
Require(groupsX != 0u, "device has no multisample upload workgroups");
|
||||
const auto groupsY = (groups + groupsX - 1u) / groupsX;
|
||||
Require(groupsY <= context.limits.maxComputeWorkGroupCount[1] && groupsX * groupsY * 64u <= std::numeric_limits<std::uint32_t>::max(), "multisample upload dispatch exceeds the device limits");
|
||||
const auto target = multisampleUploadPipeline(elementBytes);
|
||||
const auto set = allocateSet(true);
|
||||
const VkDescriptorBufferInfo buffer{source, offset, bytes};
|
||||
const VkDescriptorImageInfo image{VK_NULL_HANDLE, arrayView, VK_IMAGE_LAYOUT_GENERAL};
|
||||
const std::array<VkWriteDescriptorSet, 2> writes{{{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, set, 0u, 0u, 1u, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, nullptr, &buffer, nullptr}, {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, set, 1u, 0u, 1u, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, &image, nullptr, nullptr}}};
|
||||
context.Function<PFN_vkUpdateDescriptorSets>("vkUpdateDescriptorSets")(context.device, 2u, writes.data(), 0u, nullptr);
|
||||
context.Function<PFN_vkCmdBindPipeline>("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, target);
|
||||
context.Function<PFN_vkCmdBindDescriptorSets>("vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, imagePipelineLayout, 0u, 1u, &set, 0u, nullptr);
|
||||
const std::array<std::uint32_t, 7> push{width, height, layers, fragments, firstElement, elements, static_cast<std::uint32_t>(groupsX * 64u)};
|
||||
context.Function<PFN_vkCmdPushConstants>("vkCmdPushConstants")(commands, imagePipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0u, sizeof(push), push.data());
|
||||
context.Function<PFN_vkCmdDispatch>("vkCmdDispatch")(commands, static_cast<std::uint32_t>(groupsX), static_cast<std::uint32_t>(groupsY), 1u);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -112,6 +112,10 @@ void ValidateProvokingVertex(const Context& context, const State& state, std::sp
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (const auto& shader : shaders) {
|
||||
if (shader.stage != ShaderRecompiler::ShaderStage::Fragment || shader.program == nullptr) continue;
|
||||
Require(std::none_of(shader.program->fragmentParameters.begin(), shader.program->fragmentParameters.end(), [](const ShaderRecompiler::FragmentParameter& parameter) { return parameter.flatP20; }), "v_interp_mov p20 of a flat input with the last provoking vertex is not measured");
|
||||
}
|
||||
if (BarycentricPatches(state, shaders)) {
|
||||
for (const auto& shader : shaders) {
|
||||
if (shader.stage != ShaderRecompiler::ShaderStage::Fragment || shader.program == nullptr) continue;
|
||||
@@ -122,7 +126,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;
|
||||
@@ -345,7 +349,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
|
||||
ValidateProvokingVertex(context, state, shaders);
|
||||
if (state.rectList) Require(context.tessellationShader && context.limits.maxTessellationPatchSize >= 4, "rect-list requires tessellation with four output control points");
|
||||
const bool barycentricPatches = BarycentricPatches(state, shaders);
|
||||
if (barycentricPatches) Require(context.tessellationShader && state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST && !state.primitiveRestart, "emulated barycentrics through tessellation require a triangle list without primitive restart");
|
||||
if (barycentricPatches) Require(context.tessellationShader && (state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP) && !state.primitiveRestart, "emulated barycentrics through tessellation require a triangle list or strip without primitive restart");
|
||||
if (std::any_of(shaders.begin(), shaders.end(), [](const CompiledShader& shader) { return shader.stage == ShaderRecompiler::ShaderStage::Geometry; })) Require(context.geometryShader, "device does not support geometry shaders");
|
||||
if (state.stages.tessellation) {
|
||||
Require(context.tessellationShader, "device does not support tessellation shaders");
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -659,7 +659,7 @@ std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders,
|
||||
const bool geometry = state.stages.path == ShaderPath::Vertex && !state.rectList && shaders.size() == 3u;
|
||||
const bool patches = BarycentricPatches(state, shaders) && shaders.back().program != nullptr && shaders.back().program->barycentricEmulation.active;
|
||||
Require(geometry || shaders.size() == (tessellation || state.rectList || patches ? 4u : 2u), "incorrect graphics stage count");
|
||||
if (patches) Require(state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, "emulated barycentrics through tessellation require a triangle list");
|
||||
if (patches) Require(state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, "emulated barycentrics through tessellation require a triangle list or strip");
|
||||
if (geometry) {
|
||||
Require(shaders.back().program != nullptr && shaders.back().program->barycentricEmulation.active, "a geometry stage is generated only for emulated barycentrics");
|
||||
Require(state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, "emulated barycentrics require triangle primitives");
|
||||
|
||||
@@ -281,7 +281,9 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint
|
||||
const auto opValue = refMask >> 24u;
|
||||
auto reference = test;
|
||||
auto fixed = compare == 0 || compare == 7 ? 0u : face.compareMask;
|
||||
const auto convert = [&](std::uint32_t op) {
|
||||
std::uint32_t invertMask = 0;
|
||||
std::uint32_t inverted = 0;
|
||||
const auto convert = [&](std::uint32_t op, bool testFailed) {
|
||||
if (face.writeMask == 0) return VK_STENCIL_OP_KEEP;
|
||||
char detail[112];
|
||||
if (opValExport && ((op == 4 && !opValReference) || op == 5 || op == 6 || op == 8 || op == 9)) {
|
||||
@@ -304,6 +306,16 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint
|
||||
}
|
||||
const auto value = op == 2 ? 0xffu : op == 3 ? test : opValue;
|
||||
if (((reference ^ value) & fixed & face.writeMask) != 0) {
|
||||
const auto compared = test & face.compareMask;
|
||||
const bool passKnown = compare == 2 || (compare == 3 && compared == face.compareMask) || (compare == 6 && compared == 0u);
|
||||
const bool failKnown = compare == 5 || (compare == 1 && compared == 0u) || (compare == 4 && compared == face.compareMask);
|
||||
const bool testValueKnown = testFailed ? failKnown : passKnown;
|
||||
const auto flipped = (test ^ value) & face.writeMask;
|
||||
if (testValueKnown && (face.writeMask & ~face.compareMask) == 0 && (invertMask == 0 || invertMask == flipped)) {
|
||||
invertMask = flipped;
|
||||
++inverted;
|
||||
return VK_STENCIL_OP_INVERT;
|
||||
}
|
||||
std::snprintf(detail, sizeof(detail), "AGC graphics: stencil replacement 0x%02x against test value 0x%02x (masks 0x%02x/0x%02x) is unsupported", value, test, face.compareMask, face.writeMask);
|
||||
throw std::runtime_error(detail);
|
||||
}
|
||||
@@ -326,9 +338,14 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint
|
||||
throw std::runtime_error(detail);
|
||||
}
|
||||
};
|
||||
face.failOp = convert(ops & 0xfu);
|
||||
face.passOp = convert((ops >> 4u) & 0xfu);
|
||||
face.depthFailOp = convert((ops >> 8u) & 0xfu);
|
||||
face.failOp = convert(ops & 0xfu, true);
|
||||
face.passOp = convert((ops >> 4u) & 0xfu, false);
|
||||
face.depthFailOp = convert((ops >> 8u) & 0xfu, false);
|
||||
if (invertMask != 0) {
|
||||
const auto active = ((ops & 0xfu) != 0u) + (((ops >> 4u) & 0xfu) != 0u) + (((ops >> 8u) & 0xfu) != 0u);
|
||||
if (inverted != active) throw std::runtime_error("AGC graphics: a stencil replacement drawn as an inversion beside another stencil operation is unsupported");
|
||||
face.writeMask = invertMask;
|
||||
}
|
||||
face.reference = reference;
|
||||
Require(!opValReference || compare == 0 || compare == 7 || face.compareMask == 0, "a stencil compare against the test value is unsupported with a pixel shader that exports the stencil operation value");
|
||||
return face;
|
||||
@@ -338,13 +355,14 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValEx
|
||||
const bool opValReference = opValExport && StencilOperationValueReplaces(cx);
|
||||
zero(cx, 0x000, 0x0000101cu, "depth copy, resummarize or decompress draws (DB_RENDER_CONTROL)");
|
||||
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;
|
||||
const bool stencilClear = stencil && (read(cx, 0x000) & 2u) != 0;
|
||||
Require(zFormat != 2, "Z_24 depth is unsupported");
|
||||
if (zFormat == 0) depthControl &= ~6u;
|
||||
if (!stencil) depthControl &= ~1u;
|
||||
@@ -354,7 +372,7 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValEx
|
||||
};
|
||||
const bool depthReadOnly = (view & 0x01000000u) != 0;
|
||||
const bool stencilReadOnly = (view & 0x02000000u) != 0;
|
||||
Require(!stencilClear || (stencil && !stencilReadOnly), "stencil clear requires a writable stencil plane");
|
||||
Require(!stencilClear || !stencilReadOnly, "stencil clear requires a writable stencil plane");
|
||||
Require(!stencilClear || !testValExport, "stencil clear with a pixel shader that exports the stencil test value is unsupported");
|
||||
Require(!depthClear || (zFormat != 0 && !depthReadOnly), "depth clear requires a writable depth plane");
|
||||
DepthTarget depth{};
|
||||
@@ -379,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);
|
||||
}
|
||||
@@ -891,8 +929,8 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number,
|
||||
if (number == unorm) return {VK_FORMAT_R32_UINT, 4, 0xe4u, ShaderRecompiler::ColorExportPacking::Unorm10_11_11};
|
||||
return {VK_FORMAT_B10G11R11_UFLOAT_PACK32, 4};
|
||||
case 7:
|
||||
if (swap != 2 || number != floating) return fail();
|
||||
return {VK_FORMAT_R32_UINT, 4, 0xc6u, ShaderRecompiler::ColorExportPacking::Float11_11_10};
|
||||
if ((swap != 0 && swap != 2) || number != floating) return fail();
|
||||
return {VK_FORMAT_R32_UINT, 4, static_cast<std::uint8_t>(swap == 2 ? 0xc6u : 0xe4u), ShaderRecompiler::ColorExportPacking::Float11_11_10};
|
||||
case 9:
|
||||
// COLOR_2_10_10_10 keeps red in the low bits, the Vulkan A2B10G10R10 packing.
|
||||
if (number != unorm) return fail();
|
||||
@@ -1296,8 +1334,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
|
||||
result.sampleLocationGrid = false;
|
||||
}
|
||||
if (result.samples > 1 && (read(cx, 0x292) & 1u) == 0) {
|
||||
const bool filled = result.topology != VK_PRIMITIVE_TOPOLOGY_POINT_LIST && result.topology != VK_PRIMITIVE_TOPOLOGY_LINE_LIST && result.topology != VK_PRIMITIVE_TOPOLOGY_LINE_STRIP;
|
||||
Require(filled && !result.alphaToCoverage, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points, lines or alpha-to-coverage");
|
||||
Require(RasterPrimitiveOf(result) != ShaderRecompiler::RasterPrimitive::Point && !result.alphaToCoverage, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points or alpha-to-coverage");
|
||||
if (const auto eqaa = find(cx, 0x201); result.depth && eqaa != cx.end() && (eqaa->second & 0x0f0c0000u) != 0) throw std::runtime_error(zeroMessage(0x201, eqaa->second, "a multisampled depth target rasterized without MSAA_ENABLE with Z interpolation or overrasterization (DB_EQAA)"));
|
||||
if (const auto control = find(cx, 0x203); result.depth && control != cx.end() && (control->second & 0x100u) != 0) throw std::runtime_error(zeroMessage(0x203, control->second, "a multisampled depth target rasterized without MSAA_ENABLE with a sample-mask export"));
|
||||
result.customSampleLocations = true;
|
||||
@@ -2011,10 +2048,11 @@ std::string DrawRejection(const QueueState& queue, bool indexed) {
|
||||
if (value(cx, 0x200, word)) {
|
||||
const auto renderControl = find(cx, 0x000);
|
||||
const bool depthClear = renderControl != cx.end() && (renderControl->second & 1u) != 0;
|
||||
const bool stencilClear = renderControl != cx.end() && (renderControl->second & 2u) != 0;
|
||||
std::uint32_t stencilInfo = 0;
|
||||
const bool stencilClear = renderControl != cx.end() && (renderControl->second & 2u) != 0 && !(value(cx, 0x011, stencilInfo) && (stencilInfo & 1u) == 0);
|
||||
if (stencilClear && !depthPlanesAbsent(cx)) {
|
||||
std::uint32_t stencil = 0, view = 0;
|
||||
if ((value(cx, 0x011, stencil) && (stencil & 1u) == 0) || (value(cx, 0x002, view) && (view & 0x02000000u) != 0)) return require(false, "stencil clear requires a writable stencil plane");
|
||||
std::uint32_t view = 0;
|
||||
if (value(cx, 0x002, view) && (view & 0x02000000u) != 0) return require(false, "stencil clear requires a writable stencil plane");
|
||||
}
|
||||
if (depthClear && !depthPlanesAbsent(cx)) {
|
||||
std::uint32_t z = 0, view = 0;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -110,6 +110,7 @@ void TextureDetiler::release() noexcept {
|
||||
for (const auto pool : readbackDescriptorSets.pools) context.Function<PFN_vkDestroyDescriptorPool>("vkDestroyDescriptorPool")(context.device, pool, nullptr);
|
||||
for (const auto& entry : readbackPipelines) context.Function<PFN_vkDestroyPipeline>("vkDestroyPipeline")(context.device, entry.second, nullptr);
|
||||
for (const auto shader : readbackModules) if (shader) context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, shader, nullptr);
|
||||
if (multisampleUploadModule) context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, multisampleUploadModule, nullptr);
|
||||
if (readbackPipelineLayout) context.Function<PFN_vkDestroyPipelineLayout>("vkDestroyPipelineLayout")(context.device, readbackPipelineLayout, nullptr);
|
||||
if (readbackDescriptorLayout) context.Function<PFN_vkDestroyDescriptorSetLayout>("vkDestroyDescriptorSetLayout")(context.device, readbackDescriptorLayout, nullptr);
|
||||
for (const auto pool : cmaskDescriptorSets.pools) context.Function<PFN_vkDestroyDescriptorPool>("vkDestroyDescriptorPool")(context.device, pool, nullptr);
|
||||
|
||||
@@ -2227,6 +2227,95 @@ void importWatchTests() {
|
||||
#endif
|
||||
}
|
||||
|
||||
void sparseImportedHeapTests(const Context& context, const BdaTestAccess& access) {
|
||||
#ifndef _WIN32
|
||||
if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) {
|
||||
std::cout << "write watch unavailable: sparse imported heaps not tested\n";
|
||||
return;
|
||||
}
|
||||
constexpr std::size_t bytes = 65536;
|
||||
void* raw = mmap(nullptr, 4 * bytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
Require(raw != MAP_FAILED, "cannot reserve the sparse imported heap fixture");
|
||||
const auto base = (reinterpret_cast<std::uintptr_t>(raw) + bytes - 1) & ~(bytes - 1);
|
||||
auto* guest = reinterpret_cast<std::uint8_t*>(base);
|
||||
Require(mprotect(guest, bytes, PROT_READ | PROT_WRITE) == 0, "cannot commit the sparse imported heap prefix");
|
||||
std::memset(guest, 0x57, bytes);
|
||||
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(guest, bytes);
|
||||
auto importing = context;
|
||||
importing.hostImportAlignment = bytes;
|
||||
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
|
||||
GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, guest, bytes, true, true, true);
|
||||
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
|
||||
const auto* imported = HostImportFor(importing, base, bytes);
|
||||
Require(imported != nullptr, "the sparse heap prefix was not imported");
|
||||
const auto before = MirrorCounters();
|
||||
for (unsigned index = 0; index < 3; ++index) {
|
||||
{
|
||||
GuestBufferMemory described(importing);
|
||||
described.AddReadable(base, 2 * bytes);
|
||||
described.Upload(false);
|
||||
std::uint32_t adjustment = 0;
|
||||
const auto view = described.Descriptor(base, 2 * bytes, adjustment);
|
||||
Require(view.buffer == imported->buffer && view.range == bytes && adjustment == 0, "the sparse descriptor did not bind its imported prefix");
|
||||
Require(described.CopiedBytes() == 0 && MirrorCounters().rebuilds == before.rebuilds, "the sparse imported descriptor rebuilt or copied its heap mirror");
|
||||
reject([&] { static_cast<void>(described.Descriptor(base + bytes + 16, 4, adjustment)); }, "starts past its copy");
|
||||
described.WriteBack();
|
||||
}
|
||||
{
|
||||
GuestBufferMemory addressed(importing);
|
||||
addressed.AcquireRegistered();
|
||||
addressed.Upload(true);
|
||||
Require(MirrorCounters().rebuilds == before.rebuilds && MirrorCounters().heapMirrors == before.heapMirrors, "alternating descriptor and address builds rebuilt the imported heap mirror");
|
||||
addressed.WriteBack();
|
||||
}
|
||||
}
|
||||
{
|
||||
auto misaligned = importing;
|
||||
misaligned.limits.minStorageBufferOffsetAlignment = 256;
|
||||
std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex());
|
||||
Recorder recorder(misaligned);
|
||||
recorder.Activate();
|
||||
GuestBufferMemory memory(misaligned);
|
||||
memory.AddReadable(base + 16, 2 * bytes - 16);
|
||||
memory.Upload(false);
|
||||
std::uint32_t adjustment = 0;
|
||||
const auto view = memory.Descriptor(base + 16, 2 * bytes - 16, adjustment);
|
||||
Require(view.buffer != imported->buffer, "a misaligned sparse descriptor bound its import in place");
|
||||
Require(view.range != 0 && view.range <= bytes && memory.CopiedBytes() <= bytes && access.bytes(view.buffer)[view.offset + adjustment] == std::byte{0x57}, "a misaligned sparse descriptor does not serve its committed prefix alone");
|
||||
memory.WriteBack();
|
||||
}
|
||||
{
|
||||
GuestBufferMemory written(importing);
|
||||
written.AddWritable(base, 2 * bytes);
|
||||
written.Upload(false);
|
||||
std::uint32_t adjustment = 0;
|
||||
const auto view = written.Descriptor(base, 2 * bytes, adjustment);
|
||||
Require(view.buffer != imported->buffer && view.range == bytes && MirrorCounters().rebuilds == before.rebuilds + 1, "a sparse writable descriptor bound a shortened import");
|
||||
access.bytes(view.buffer)[view.offset] = std::byte{0x68};
|
||||
guest[8] = 0x79;
|
||||
written.WriteBack();
|
||||
Require(guest[0] == 0x68 && guest[8] == 0x79, "the sparse writable fallback lost a GPU store or rolled back a CPU store");
|
||||
}
|
||||
mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
|
||||
GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, guest);
|
||||
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
|
||||
Require(HostImportFor(importing, base, bytes) == nullptr, "the sparse heap import outlived its range");
|
||||
{
|
||||
GuestBufferMemory sweep(importing);
|
||||
sweep.AcquireRegistered();
|
||||
sweep.Upload(true);
|
||||
sweep.WriteBack();
|
||||
}
|
||||
Require(MirrorCounters().heapMirrors == before.heapMirrors, "the sparse imported heap fixture leaked its mirror");
|
||||
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(guest, bytes);
|
||||
munmap(raw, 4 * bytes);
|
||||
std::cout << "Sparse imported heap tests passed\n";
|
||||
#else
|
||||
static_cast<void>(context);
|
||||
static_cast<void>(access);
|
||||
#endif
|
||||
}
|
||||
|
||||
void heapMirrorTests(const Context& context, const BdaTestAccess& access) {
|
||||
if (!GuestArena::GuestArenaAvailable_nid_postfix() || !GuestArena::GuestArenaWriteWatched_nid_postfix()) {
|
||||
std::cout << "guest arena unavailable or not write-watched: heap mirrors not tested\n";
|
||||
@@ -2724,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();
|
||||
}
|
||||
@@ -3439,6 +3528,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) {
|
||||
importReportFloorTests(context, access, true);
|
||||
importReportFloorTests(context, access, false);
|
||||
importReportRefusalMemoryTests(context, access);
|
||||
sparseImportedHeapTests(context, access);
|
||||
heapMirrorTests(context, access);
|
||||
importedHeapMirrorTests(context, access);
|
||||
importWatchTests();
|
||||
|
||||
@@ -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"
|
||||
@@ -765,10 +766,12 @@ void msaaEnableClearOneSampleTargetTests() {
|
||||
expectFailure([&] { DecodeState(queue); }, refused);
|
||||
queue = drawC();
|
||||
queue.context[0x2dc] = 0x0000aa01u;
|
||||
expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE are unsupported for points, lines");
|
||||
queue = drawC();
|
||||
queue.userConfig[0x242] = 2;
|
||||
expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE are unsupported for points, lines");
|
||||
expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE are unsupported for points or alpha-to-coverage");
|
||||
for (const auto primitive : {2u, 3u}) {
|
||||
queue = drawC();
|
||||
queue.userConfig[0x242] = primitive;
|
||||
requireOneSample(queue, "a line list or strip into PPSA18216's draw C target");
|
||||
}
|
||||
const auto twoTargets = [&](std::uint32_t first, std::uint32_t second) {
|
||||
auto pair = drawC();
|
||||
for (const auto offset : {0x318u, 0x31bu, 0x31cu}) pair.context[offset + 0xfu] = pair.context.at(offset);
|
||||
@@ -853,9 +856,11 @@ void msaaEnableClearOneSampleTargetTests() {
|
||||
queue = depthTarget(color);
|
||||
queue.context[0x2dc] = 0x0000aa01u;
|
||||
expectFailure([&] { DecodeState(queue); }, depthRefused);
|
||||
queue = depthTarget(color);
|
||||
queue.userConfig[0x242] = 2;
|
||||
expectFailure([&] { DecodeState(queue); }, depthRefused);
|
||||
for (const auto primitive : {2u, 3u}) {
|
||||
queue = depthTarget(color);
|
||||
queue.userConfig[0x242] = primitive;
|
||||
expectFailure([&] { DecodeState(queue); }, depthRefused);
|
||||
}
|
||||
for (const auto eqaa : {0x00050002u, 0x00090002u, 0x01010002u, 0x08010002u}) {
|
||||
queue = depthTarget(color);
|
||||
queue.context[0x201] = eqaa;
|
||||
@@ -1729,7 +1734,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;
|
||||
@@ -2262,7 +2299,41 @@ void DepthStencilTests() {
|
||||
queue.context[0x10b] = 0x40;
|
||||
queue.context[0x10c] = 0x05ffff02;
|
||||
queue.context[0x200] = 0x00200211;
|
||||
state = AgcDriver::Graphics::DecodeState(queue);
|
||||
Require(state.stencilFront.passOp == VK_STENCIL_OP_INVERT && state.stencilFront.failOp == VK_STENCIL_OP_KEEP && state.stencilFront.depthFailOp == VK_STENCIL_OP_KEEP && state.stencilFront.compareOp == VK_COMPARE_OP_EQUAL, "an equal test replaced by another value is not drawn as an inversion");
|
||||
Require(state.stencilFront.reference == 2 && state.stencilFront.compareMask == 0xff && state.stencilFront.writeMask == 7, "an inverted replacement does not keep the test value and write only the bits that differ from it");
|
||||
queue.context[0x10b] = 0x440;
|
||||
state = AgcDriver::Graphics::DecodeState(queue);
|
||||
Require(state.stencilFront.passOp == VK_STENCIL_OP_INVERT && state.stencilFront.depthFailOp == VK_STENCIL_OP_INVERT && state.stencilFront.writeMask == 7, "a replacement on pass and depth fail is not drawn as one inversion");
|
||||
queue.context[0x10b] = 0x4;
|
||||
queue.context[0x200] = 0x00200511;
|
||||
state = AgcDriver::Graphics::DecodeState(queue);
|
||||
Require(state.stencilFront.failOp == VK_STENCIL_OP_INVERT && state.stencilFront.passOp == VK_STENCIL_OP_KEEP && state.stencilFront.writeMask == 7, "a not-equal test that fails is not replaced as an inversion");
|
||||
queue.context[0x10c] = 0x40ffffff;
|
||||
queue.context[0x10b] = 0x40;
|
||||
queue.context[0x200] = 0x00200311;
|
||||
state = AgcDriver::Graphics::DecodeState(queue);
|
||||
Require(state.stencilFront.passOp == VK_STENCIL_OP_INVERT && state.stencilFront.writeMask == 0xbf && state.stencilFront.reference == 0xff, "a less-or-equal test against 0xff, which passes only on 0xff, is not replaced as an inversion");
|
||||
queue.context[0x10b] = 0x4;
|
||||
queue.context[0x200] = 0x00200411;
|
||||
state = AgcDriver::Graphics::DecodeState(queue);
|
||||
Require(state.stencilFront.failOp == VK_STENCIL_OP_INVERT && state.stencilFront.writeMask == 0xbf, "a greater test against 0xff, which fails only on 0xff, is not replaced as an inversion");
|
||||
queue.context[0x10c] = 0x40ffff7f;
|
||||
queue.context[0x10b] = 0x40;
|
||||
queue.context[0x200] = 0x00200311;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "stencil replacement");
|
||||
queue.context[0x10c] = 0x05ffff02;
|
||||
queue.context[0x200] = 0x00200511;
|
||||
queue.context[0x10b] = 0x40;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "stencil replacement");
|
||||
queue.context[0x200] = 0x00200111;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "stencil replacement");
|
||||
queue.context[0x200] = 0x00200211;
|
||||
queue.context[0x10c] = 0x05ff0f02;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "stencil replacement");
|
||||
queue.context[0x10c] = 0x05ffff02;
|
||||
queue.context[0x10b] = 0x41;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "beside another stencil operation");
|
||||
queue.context[0x10b] = 0x50;
|
||||
queue.context[0x200] = 0x00700711;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "stencil add/subtract");
|
||||
@@ -2329,10 +2400,12 @@ void DepthStencilTests() {
|
||||
queue.context[0x000] = 0x22u | control;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "DB_RENDER_CONTROL");
|
||||
}
|
||||
queue.context[0x000] = 0x22;
|
||||
queue.context[0x000] = 0x23;
|
||||
queue.context[0x011] = 0;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "writable stencil plane");
|
||||
Require(AgcDriver::Graphics::DrawRejection(queue, false).find("writable stencil plane") != std::string::npos, "precheck accepted clear without a stencil plane");
|
||||
state = AgcDriver::Graphics::DecodeState(queue);
|
||||
Require(state.depthWrite && state.depthCompare == VK_COMPARE_OP_ALWAYS && !state.stencilTest && state.colors.size() == 2, "a depth and stencil clear without a stencil plane does not clear depth alone");
|
||||
Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "precheck rejected a stencil clear without a stencil plane");
|
||||
queue.context[0x000] = 0x22;
|
||||
queue.context[0x010] = 0;
|
||||
state = AgcDriver::Graphics::DecodeState(queue);
|
||||
Require(!state.depth && !state.depthTest && !state.depthWrite && !state.stencilTest && state.colors.size() == 2, "clear flags without depth/stencil planes changed color-only rendering");
|
||||
@@ -3282,6 +3355,41 @@ void msaaEnableClearDepthTests() {
|
||||
state = DecodeState(queue);
|
||||
Require(state.colors.empty(), "a depth-only 4x draw without MSAA_ENABLE decoded a color target");
|
||||
requireCentered(state, "a 4x depth target rasterized without MSAA_ENABLE");
|
||||
for (const auto primitive : {2u, 3u}) {
|
||||
queue = drawB();
|
||||
queue.userConfig[0x242] = primitive;
|
||||
state = DecodeState(queue);
|
||||
Require(state.topology == (primitive == 2u ? VK_PRIMITIVE_TOPOLOGY_LINE_LIST : VK_PRIMITIVE_TOPOLOGY_LINE_STRIP) && state.colors.size() == 1 && state.colors[0].samples == 4, "PPSA18216's line list or strip into its 4x depth and color pair lost its topology or color target");
|
||||
requireCentered(state, "a line list or strip into PPSA18216's 4x depth and color pair without MSAA_ENABLE");
|
||||
Require(!state.sampleLocationGrid && !state.perSampleShading, "a line list or strip without MSAA_ENABLE kept a sample location grid or sample shading");
|
||||
Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck refused a line list or strip into PPSA18216's 4x depth and color pair");
|
||||
}
|
||||
queue = drawB();
|
||||
queue.userConfig[0x242] = 1;
|
||||
queue.context[0x2d5] = 0x2030;
|
||||
queue.userConfig[0x25b] = 0x6633;
|
||||
queue.context[0x1ff] = 255;
|
||||
queue.context[0x2ce] = 5;
|
||||
queue.context[0x29b] = 2;
|
||||
queue.context[0x2ab] = 4;
|
||||
queue.shader[0x8a] = 3u << 29u;
|
||||
queue.shader[0x8b] = 3u << 16u;
|
||||
state = DecodeState(queue);
|
||||
Require(state.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST && state.stages.mesh && state.stages.mesh->outputPrimitiveSize == 3u && AgcDriver::Graphics::RasterPrimitiveOf(state) == ShaderRecompiler::RasterPrimitive::Triangle && state.colors.size() == 1 && state.colors[0].samples == 4, "a point list into a geometry program that outputs triangle strips into PPSA18216's 4x depth and color pair did not decode as triangles");
|
||||
requireCentered(state, "triangles a geometry program makes from a point list into PPSA18216's 4x depth and color pair without MSAA_ENABLE");
|
||||
for (const auto primitive : {4u, 2u, 1u}) {
|
||||
queue = drawB();
|
||||
queue.userConfig[0x242] = primitive;
|
||||
queue.context[0x2d5] = 0x2030;
|
||||
queue.userConfig[0x25b] = 0x6633;
|
||||
queue.context[0x1ff] = 255;
|
||||
queue.context[0x2ce] = 5;
|
||||
queue.context[0x29b] = 0;
|
||||
queue.context[0x2ab] = 4;
|
||||
queue.shader[0x8a] = 3u << 29u;
|
||||
queue.shader[0x8b] = 3u << 16u;
|
||||
expectFailure([&] { DecodeState(queue); }, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points or alpha-to-coverage");
|
||||
}
|
||||
for (const auto eqaa : {0x00050002u, 0x00090002u, 0x01010002u, 0x08010002u}) {
|
||||
queue = drawB();
|
||||
queue.context[0x201] = eqaa;
|
||||
@@ -3293,7 +3401,7 @@ void msaaEnableClearDepthTests() {
|
||||
expectFailure([&] { DecodeState(queue); }, "a multisampled depth target rasterized without MSAA_ENABLE with a sample-mask export");
|
||||
queue = drawB();
|
||||
queue.context[0x2dc] = 0x0000aa01u;
|
||||
expectFailure([&] { DecodeState(queue); }, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points, lines or alpha-to-coverage");
|
||||
expectFailure([&] { DecodeState(queue); }, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points or alpha-to-coverage");
|
||||
queue = drawB();
|
||||
queue.context[0x010] = 0xa2900987;
|
||||
expectFailure([&] { DecodeState(queue); }, "the depth target's sample count differs from the rasterizer's");
|
||||
@@ -3600,7 +3708,7 @@ void multisampleTests() {
|
||||
const auto aliased = AgcDriver::Graphics::SampleLocations(state);
|
||||
for (std::uint32_t sample = 0; sample < state.samples; ++sample) Require(aliased[sample].x == 0.5f && aliased[sample].y == 0.5f, "a 2x target rasterized without MSAA_ENABLE did not place every sample at the pixel center");
|
||||
queue.context[0x2dc] = 0x0000aa01u;
|
||||
expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE are unsupported for points, lines or alpha-to-coverage");
|
||||
expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE are unsupported for points or alpha-to-coverage");
|
||||
queue = multisampled();
|
||||
queue.context[0x292] = 2;
|
||||
queue.context[0x302] = 0x44cc;
|
||||
@@ -7860,9 +7968,11 @@ void barycentricEmulationTests() {
|
||||
Require(BarycentricPatches(state, patchShaders) && !BarycentricPatches(state, shaders), "generated tessellation stages were not recognised as barycentric patches");
|
||||
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
ValidateShaders(patchShaders, state, subgroup, false);
|
||||
for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, VK_PRIMITIVE_TOPOLOGY_POINT_LIST}) {
|
||||
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP;
|
||||
ValidateShaders(patchShaders, state, subgroup, false);
|
||||
for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, VK_PRIMITIVE_TOPOLOGY_POINT_LIST}) {
|
||||
state.topology = topology;
|
||||
expectFailure([&] { ValidateShaders(patchShaders, state, subgroup, false); }, "require a triangle list");
|
||||
expectFailure([&] { ValidateShaders(patchShaders, state, subgroup, false); }, "require a triangle list or strip");
|
||||
}
|
||||
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
const std::array<CompiledShader, 4> plainPatchShaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::TessellationControl, &patches.control, 0}, {ShaderStage::TessellationEvaluation, &patches.evaluation, 0}, {ShaderStage::Fragment, &plain, 0}}};
|
||||
@@ -9102,6 +9212,14 @@ void ProvokingVertexTests() {
|
||||
const std::array<CompiledShader, 1> flatP10Shaders{{{ShaderRecompiler::ShaderStage::Fragment, &flatP10Pixel, 0}}};
|
||||
ValidateProvokingVertex(supported, last, flatP10Shaders);
|
||||
expectFailure([&] { ValidateProvokingVertex(supported, first, flatP10Shaders); }, "v_interp_mov p10 of a flat input with the first provoking vertex is not measured");
|
||||
const std::array<std::uint32_t, 7> flatP20Color{0xc8120001u, 0xc8160101u, 0xc81a0201u, 0xc81e0301u, 0xf800180fu, 0x07060504u, 0xbf810000u};
|
||||
const auto flatP20Pixel = recompilePixel({0x400u}, flatP20Color);
|
||||
const auto p20Inputs = locatedInputs(flatP20Pixel.spirv.Words());
|
||||
Require(p20Inputs.size() == 1 && p20Inputs[0].location == 0 && p20Inputs[0].flat && !p20Inputs[0].perVertex, "flat v_interp_mov P20 did not compile to a flat input");
|
||||
Require(flatP20Pixel.fragmentParameters.size() == 1 && flatP20Pixel.fragmentParameters[0].flatP20 && !flatP20Pixel.fragmentParameters[0].flatP10 && !flatP20Pixel.fragmentParameters[0].perVertex && !flatP10Pixel.fragmentParameters[0].flatP20, "flat v_interp_mov P20 was not recorded apart from P10");
|
||||
const std::array<CompiledShader, 1> flatP20Shaders{{{ShaderRecompiler::ShaderStage::Fragment, &flatP20Pixel, 0}}};
|
||||
ValidateProvokingVertex(supported, first, flatP20Shaders);
|
||||
expectFailure([&] { ValidateProvokingVertex(supported, last, flatP20Shaders); }, "v_interp_mov p20 of a flat input with the last provoking vertex is not measured");
|
||||
auto lastStrip = last;
|
||||
lastStrip.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP;
|
||||
auto lastList = last;
|
||||
@@ -9161,10 +9279,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);
|
||||
}
|
||||
@@ -9460,7 +9584,12 @@ void float11_11_10TargetTests() {
|
||||
auto changed = queue;
|
||||
changed.context[0x31c] &= ~0x40000u;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "rounding (ROUND_MODE 0) 11_11_10 float color targets");
|
||||
for (const auto swap : {0u, 1u, 3u}) {
|
||||
changed = queue;
|
||||
changed.context[0x31c] &= ~0x1800u;
|
||||
const auto standard = AgcDriver::Graphics::DecodeState(changed);
|
||||
Require(standard.color.format == VK_FORMAT_R32_UINT && standard.color.componentMapping == 0xe4u && standard.color.packing == ColorExportPacking::Float11_11_10, "an 11_11_10 float target with the standard component order did not decode");
|
||||
Require(AgcDriver::Graphics::ExportMappings(standard)[0] == 0xe4u, "export 0 reordered its components into the standard-order 11_11_10 float target");
|
||||
for (const auto swap : {1u, 3u}) {
|
||||
changed = queue;
|
||||
changed.context[0x31c] = (changed.context[0x31c] & ~0x1800u) | (swap << 11u);
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "unsupported color format 7 number type 7 component swap " + std::to_string(swap));
|
||||
@@ -9850,26 +9979,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;
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTextureLayout.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Context.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
using namespace AgcDriver::Graphics;
|
||||
|
||||
namespace {
|
||||
|
||||
struct ReferenceCase {
|
||||
TextureTileMode tileMode;
|
||||
std::uint32_t elementBytes;
|
||||
std::uint32_t fragments;
|
||||
std::uint32_t blockWidth;
|
||||
std::uint32_t blockHeight;
|
||||
std::uint32_t pitch;
|
||||
std::uint32_t paddedHeight;
|
||||
std::uint64_t sliceBytes;
|
||||
std::array<std::uint64_t, 7> addresses;
|
||||
};
|
||||
|
||||
constexpr std::array<std::array<std::uint32_t, 3>, 7> kCoordinates{{
|
||||
{0u, 0u, 0u}, {128u, 66u, 16u}, {64u, 32u, 1u}, {32u, 64u, 2u},
|
||||
{63u, 31u, 7u}, {127u, 65u, 15u}, {1u, 1u, 3u}
|
||||
}};
|
||||
|
||||
constexpr ReferenceCase kReferences[] = {
|
||||
{TextureTileMode::kR64KBX, 1u, 2u, 128u, 256u, 256u, 256u, 131072ull, {{0ull, 2201608ull, 141312ull, 300032ull, 919295ull, 2013271ull, 396305ull}}},
|
||||
{TextureTileMode::kR64KBX, 1u, 4u, 128u, 128u, 256u, 128u, 131072ull, {{0ull, 2185224ull, 174080ull, 316416ull, 919295ull, 1996887ull, 429073ull}}},
|
||||
{TextureTileMode::kR64KBX, 1u, 8u, 64u, 128u, 192u, 128u, 196608ull, {{0ull, 3291144ull, 279552ull, 422400ull, 1413119ull, 3061847ull, 640529ull}}},
|
||||
{TextureTileMode::kR64KBX, 2u, 2u, 128u, 128u, 256u, 128u, 131072ull, {{0ull, 2213920ull, 141312ull, 312320ull, 923390ull, 2025630ull, 396306ull}}},
|
||||
{TextureTileMode::kR64KBX, 2u, 4u, 128u, 64u, 256u, 128u, 262144ull, {{0ull, 4392992ull, 305152ull, 689152ull, 1840894ull, 4073630ull, 822290ull}}},
|
||||
{TextureTileMode::kR64KBX, 2u, 8u, 64u, 64u, 192u, 128u, 393216ull, {{0ull, 6621216ull, 476160ull, 999936ull, 2793470ull, 6195358ull, 1230354ull}}},
|
||||
{TextureTileMode::kR64KBX, 4u, 2u, 64u, 128u, 192u, 128u, 196608ull, {{0ull, 3328032ull, 264192ull, 443392ull, 1390332ull, 3074204ull, 592916ull}}},
|
||||
{TextureTileMode::kR64KBX, 4u, 4u, 64u, 64u, 192u, 128u, 393216ull, {{0ull, 6621216ull, 493568ull, 1016832ull, 2766588ull, 6170780ull, 1215508ull}}},
|
||||
{TextureTileMode::kR64KBX, 4u, 8u, 32u, 64u, 160u, 128u, 655360ull, {{0ull, 11077664ull, 820224ull, 1737216ull, 4667388ull, 10365084ull, 2002452ull}}},
|
||||
{TextureTileMode::kR64KBX, 8u, 2u, 64u, 64u, 192u, 128u, 393216ull, {{0ull, 6621312ull, 460800ull, 984064ull, 2782968ull, 6170744ull, 1182744ull}}},
|
||||
{TextureTileMode::kR64KBX, 8u, 4u, 64u, 32u, 192u, 96u, 589824ull, {{0ull, 9963648ull, 855040ull, 1574400ull, 4158456ull, 9317496ull, 1773080ull}}},
|
||||
{TextureTileMode::kR64KBX, 8u, 8u, 32u, 32u, 160u, 96u, 983040ull, {{0ull, 16648320ull, 1444352ull, 2689280ull, 6979576ull, 15605880ull, 2952984ull}}},
|
||||
{TextureTileMode::kR64KBX, 16u, 2u, 32u, 64u, 160u, 128u, 655360ull, {{0ull, 11075712ull, 787456ull, 1706496ull, 4718576ull, 10397808ull, 1967664ull}}},
|
||||
{TextureTileMode::kR64KBX, 16u, 4u, 32u, 32u, 160u, 96u, 983040ull, {{0ull, 16646272ull, 1443328ull, 2690304ull, 7011312ull, 15641712ull, 2950960ull}}},
|
||||
{TextureTileMode::kR64KBX, 16u, 8u, 16u, 32u, 144u, 96u, 1769472ull, {{0ull, 30017664ull, 2624000ull, 4851968ull, 12615664ull, 28201072ull, 5312304ull}}},
|
||||
{TextureTileMode::kZ64KBX, 1u, 2u, 128u, 256u, 256u, 256u, 131072ull, {{0ull, 2172945ull, 149504ull, 271361ull, 923390ull, 1992879ull, 396294ull}}},
|
||||
{TextureTileMode::kZ64KBX, 1u, 4u, 128u, 128u, 256u, 128u, 131072ull, {{0ull, 2181153ull, 165890ull, 279555ull, 931580ull, 2025565ull, 396302ull}}},
|
||||
{TextureTileMode::kZ64KBX, 1u, 8u, 64u, 128u, 192u, 128u, 196608ull, {{0ull, 3295297ull, 263170ull, 410115ull, 1425404ull, 3041469ull, 591390ull}}},
|
||||
{TextureTileMode::kZ64KBX, 2u, 2u, 128u, 128u, 256u, 128u, 131072ull, {{0ull, 2181154ull, 165888ull, 279554ull, 931580ull, 2025566ull, 396300ull}}},
|
||||
{TextureTileMode::kZ64KBX, 2u, 4u, 128u, 64u, 256u, 128u, 262144ull, {{0ull, 4425794ull, 280580ull, 689158ull, 1881848ull, 4118714ull, 789532ull}}},
|
||||
{TextureTileMode::kZ64KBX, 2u, 8u, 64u, 64u, 192u, 128u, 393216ull, {{0ull, 6637698ull, 459780ull, 999942ull, 2818040ull, 6219898ull, 1181244ull}}},
|
||||
{TextureTileMode::kZ64KBX, 4u, 2u, 64u, 128u, 192u, 128u, 196608ull, {{0ull, 3295300ull, 264192ull, 410628ull, 1423096ull, 3041468ull, 592920ull}}},
|
||||
{TextureTileMode::kZ64KBX, 4u, 4u, 64u, 64u, 192u, 128u, 393216ull, {{0ull, 6637700ull, 460808ull, 1000460ull, 2815728ull, 6219892ull, 1182776ull}}},
|
||||
{TextureTileMode::kZ64KBX, 4u, 8u, 32u, 64u, 160u, 128u, 655360ull, {{0ull, 11126788ull, 787464ull, 1720844ull, 4716528ull, 10359028ull, 1967736ull}}},
|
||||
};
|
||||
|
||||
template<typename TFunction>
|
||||
void requireRefusal(TFunction action) {
|
||||
bool refused = false;
|
||||
try { action(); } catch (const std::exception&) { refused = true; }
|
||||
Require(refused, "multisampled layout accepted an unsupported operation");
|
||||
}
|
||||
|
||||
void checkReference(const ReferenceCase& reference) {
|
||||
MultisampleTextureLayout layout(reference.tileMode, reference.elementBytes, reference.fragments, 129u, 67u, 17u);
|
||||
Require(layout.BlockWidth() == reference.blockWidth && layout.BlockHeight() == reference.blockHeight, "multisampled block extent differs from AMD reference");
|
||||
Require(layout.Pitch() == reference.pitch && layout.PaddedHeight() == reference.paddedHeight, "multisampled padded extent differs from AMD reference");
|
||||
Require(layout.SliceBytes() == reference.sliceBytes && layout.Bytes() == reference.sliceBytes * 17u, "multisampled allocation differs from AMD reference");
|
||||
Require(layout.LinearBytes() == 129ull * 67u * 17u * reference.fragments * reference.elementBytes, "multisampled linear region omits fragments or layers");
|
||||
for (std::uint32_t n = 0; n < kCoordinates.size(); ++n) {
|
||||
const auto& coordinate = kCoordinates[n];
|
||||
const auto address = layout.Offset(coordinate[0], coordinate[1], coordinate[2], n % reference.fragments);
|
||||
Require(address == reference.addresses[n], "multisampled fragment address differs from AMD reference");
|
||||
for (auto xorBytes : {0x100u, 0x800u, 0x4000u, 0x8000u, 0xcf00u}) {
|
||||
MultisampleTextureLayout swizzled(reference.tileMode, reference.elementBytes, reference.fragments, 129u, 67u, 17u, xorBytes);
|
||||
const auto expected = (address & ~65535ull) | ((address & 65535ull) ^ xorBytes);
|
||||
Require(swizzled.Offset(coordinate[0], coordinate[1], coordinate[2], n % reference.fragments) == expected, "multisampled pipe/bank XOR moves a fragment outside its tile");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void checkTransfer(const ReferenceCase& reference) {
|
||||
MultisampleTextureLayout layout(reference.tileMode, reference.elementBytes, reference.fragments, 129u, 67u, 3u, 0x8100u);
|
||||
std::vector<std::byte> source(layout.LinearBytes() + 32u, std::byte{0xa5});
|
||||
auto linear = std::span(source).subspan(16u, layout.LinearBytes());
|
||||
for (std::size_t n = 0; n < linear.size(); ++n) linear[n] = static_cast<std::byte>((n * 13u + n / 251u) & 255u);
|
||||
const auto savedSource = source;
|
||||
std::vector<std::byte> tiled(layout.Bytes() + 32u, std::byte{0xa5});
|
||||
auto guest = std::span(tiled).subspan(16u, layout.Bytes());
|
||||
layout.Tile(linear, guest);
|
||||
Require(source == savedSource, "multisampled retile changed its source");
|
||||
std::vector<bool> covered(guest.size());
|
||||
std::size_t position = 0;
|
||||
for (std::uint32_t layer = 0; layer < 3u; ++layer) {
|
||||
for (std::uint32_t y = 0; y < 67u; ++y) {
|
||||
for (std::uint32_t x = 0; x < 129u; ++x) {
|
||||
for (std::uint32_t fragment = 0; fragment < reference.fragments; ++fragment) {
|
||||
const auto address = layout.Offset(x, y, layer, fragment);
|
||||
for (std::uint32_t channel = 0; channel < reference.elementBytes; ++channel) {
|
||||
Require(address + channel < guest.size() && !covered[address + channel], "multisampled element addresses overlap or exceed the allocation");
|
||||
covered[address + channel] = true;
|
||||
Require(guest[address + channel] == linear[position++], "multisampled retile changed element bytes");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (std::size_t n = 0; n < guest.size(); ++n) Require(covered[n] || guest[n] == std::byte{0xa5}, "multisampled retile changed tile padding");
|
||||
Require(std::all_of(tiled.begin(), tiled.begin() + 16u, [](auto value) { return value == std::byte{0xa5}; }) && std::all_of(tiled.end() - 16u, tiled.end(), [](auto value) { return value == std::byte{0xa5}; }), "multisampled retile changed guard bytes");
|
||||
const auto savedTiled = tiled;
|
||||
std::vector<std::byte> restored(source.size(), std::byte{0xa5});
|
||||
layout.Detile(guest, std::span(restored).subspan(16u, layout.LinearBytes()));
|
||||
Require(restored == source && tiled == savedTiled, "multisampled detile changed element, source or guard bytes");
|
||||
}
|
||||
|
||||
void checkInverse(const ReferenceCase& reference) {
|
||||
const auto byteBits = static_cast<std::uint32_t>(std::countr_zero(reference.elementBytes));
|
||||
const auto fragmentBits = static_cast<std::uint32_t>(std::countr_zero(reference.fragments));
|
||||
const auto widthBits = static_cast<std::uint32_t>(std::countr_zero(reference.blockWidth));
|
||||
for (const auto xorBytes : {0u, 0x4000u, 0x8000u, 0xcf00u}) {
|
||||
const MultisampleTextureLayout layout(reference.tileMode, reference.elementBytes, reference.fragments, reference.blockWidth * 4u, reference.blockHeight * 4u, 8u, xorBytes);
|
||||
for (const auto origin : {std::array{0u, 0u, 0u}, std::array{reference.blockWidth * 2u, reference.blockHeight * 3u, 7u}}) {
|
||||
const auto base = static_cast<std::uint32_t>(layout.Offset(origin[0], origin[1], origin[2], 0u) & 65535u);
|
||||
const auto step = xorBytes == 0u && origin[2] == 0u ? 1u : 257u;
|
||||
for (std::uint32_t packed = 0u; packed < 65536u; packed += step) {
|
||||
const auto byte = packed & (reference.elementBytes - 1u);
|
||||
const auto fragment = (packed >> byteBits) & (reference.fragments - 1u);
|
||||
const auto x = (packed >> (byteBits + fragmentBits)) & (reference.blockWidth - 1u);
|
||||
const auto y = packed >> (byteBits + fragmentBits + widthBits);
|
||||
const auto physical = static_cast<std::uint32_t>((layout.Offset(origin[0] + x, origin[1] + y, origin[2], fragment) + byte) & 65535u) ^ base;
|
||||
std::uint32_t restored = 0u;
|
||||
for (std::uint32_t bit = 0; bit < 16u; ++bit) restored |= static_cast<std::uint32_t>(std::popcount(physical & layout.InverseBlockEquation()[bit]) & 1u) << bit;
|
||||
Require(restored == packed, "multisampled inverse equation changed a byte, fragment or pixel coordinate");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void checkRefusals() {
|
||||
for (const auto& extent : {std::array{0u, 3u, 1u}, std::array{5u, 0u, 1u}, std::array{16385u, 3u, 1u}, std::array{5u, 16385u, 1u}, std::array{5u, 3u, 0u}, std::array{5u, 3u, 8193u}}) {
|
||||
requireRefusal([&] { MultisampleTextureLayout invalid(TextureTileMode::kR64KBX, 8u, 8u, extent[0], extent[1], extent[2]); });
|
||||
}
|
||||
for (auto elementBytes : {0u, 3u, 32u}) requireRefusal([&] { MultisampleTextureLayout invalid(TextureTileMode::kR64KBX, elementBytes, 8u, 5u, 3u, 1u); });
|
||||
for (auto fragments : {0u, 1u, 3u, 16u}) requireRefusal([&] { MultisampleTextureLayout invalid(TextureTileMode::kR64KBX, 8u, fragments, 5u, 3u, 1u); });
|
||||
for (auto tileMode : {TextureTileMode::kLinear, TextureTileMode::kStandard4KB, TextureTileMode::kStandard64KB, TextureTileMode::kS64KBX, TextureTileMode::kD64KBX}) requireRefusal([&] { MultisampleTextureLayout invalid(tileMode, 8u, 8u, 5u, 3u, 1u); });
|
||||
for (auto elementBytes : {8u, 16u}) requireRefusal([&] { MultisampleTextureLayout invalid(TextureTileMode::kZ64KBX, elementBytes, 8u, 5u, 3u, 1u); });
|
||||
for (auto xorBytes : {1u, 255u, 0x1000u, 0x2000u, 0x10000u}) requireRefusal([&] { MultisampleTextureLayout invalid(TextureTileMode::kR64KBX, 8u, 8u, 5u, 3u, 1u, xorBytes); });
|
||||
|
||||
MultisampleTextureLayout layout(TextureTileMode::kR64KBX, 8u, 8u, 5u, 3u, 2u);
|
||||
for (const auto& coordinate : {std::array{5u, 0u, 0u, 0u}, std::array{0u, 3u, 0u, 0u}, std::array{0u, 0u, 2u, 0u}, std::array{0u, 0u, 0u, 8u}}) requireRefusal([&] { layout.Offset(coordinate[0], coordinate[1], coordinate[2], coordinate[3]); });
|
||||
std::vector<std::byte> source(layout.Bytes(), std::byte{0xa5}), destination(layout.Bytes(), std::byte{0x5a});
|
||||
const auto savedSource = source, savedDestination = destination;
|
||||
requireRefusal([&] { layout.Detile(std::span(source).first(layout.Bytes() - 1u), destination); });
|
||||
requireRefusal([&] { layout.Detile(source, std::span(destination).first(layout.LinearBytes() - 1u)); });
|
||||
requireRefusal([&] { layout.Tile(std::span(source).first(layout.LinearBytes() - 1u), destination); });
|
||||
requireRefusal([&] { layout.Tile(source, std::span(destination).first(layout.Bytes() - 1u)); });
|
||||
requireRefusal([&] { layout.Detile(source, source); });
|
||||
requireRefusal([&] { layout.Tile(source, source); });
|
||||
requireRefusal([&] { layout.Detile(source, std::span(source).subspan(8u)); });
|
||||
Require(source == savedSource && destination == savedDestination, "refused multisampled transfer changed a buffer");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
for (const auto& reference : kReferences) {
|
||||
checkReference(reference);
|
||||
checkTransfer(reference);
|
||||
checkInverse(reference);
|
||||
}
|
||||
checkRefusals();
|
||||
std::cout << "Multisampled texture layout reference, fragment, layer, padding and refusal tests passed\n";
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
#include "IntermediateRepresentation/IrBuilder.hpp"
|
||||
#include "IntermediateRepresentation/IrProgram.hpp"
|
||||
#include "Optimization/ShaderStageInputInfo.hpp"
|
||||
#include "Optimization/TessellationLowering.hpp"
|
||||
#include "SpirvBackend/SpirvEmitter.hpp"
|
||||
#include <spirv/unified1/spirv.hpp>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
using namespace ShaderRecompiler;
|
||||
|
||||
@@ -135,6 +140,52 @@ bool RefusedBecause(IrProgram& program, const char* reason) {
|
||||
return !outcome.lowered && outcome.reason.find(reason) != std::string::npos;
|
||||
}
|
||||
|
||||
std::uint32_t EmittedEvaluationControlPoint(std::uint32_t address) {
|
||||
constexpr std::uint32_t ControlStride = 16u;
|
||||
IrProgram program;
|
||||
IrBuilder builder{program};
|
||||
program.SetWaveSize(64);
|
||||
program.Resources().stage = IrShaderStage::TessellationEvaluation;
|
||||
program.Resources().srtPlanComplete = true;
|
||||
program.Resources().resourceTrackingComplete = true;
|
||||
program.Metadata().shaderInfoComplete = true;
|
||||
program.Metadata().bindingLayoutComplete = true;
|
||||
program.Metadata().tessellationControlStride = ControlStride;
|
||||
auto& block = program.CreateBlock();
|
||||
program.SetEntryBlock(block);
|
||||
program.BlockOrder().push_back(&block);
|
||||
BlockInfo info;
|
||||
info.id = block.Id();
|
||||
program.Metadata().blockInfo.push_back(info);
|
||||
builder.SetInsertionPoint(block);
|
||||
auto& read = builder.Emit(IrOpcode::GetTessellationAttribute, IrType::U32, {&builder.Constant(static_cast<std::uint32_t>(TessellationAttribute::EvaluationInput)), &builder.Constant(address), &builder.ConstantBool(true)});
|
||||
static_cast<void>(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&read}));
|
||||
ShaderVertexInputInfo vertex;
|
||||
vertex.tess.inputControlPoints = InputControlPoints;
|
||||
vertex.tess.outputControlPoints = InputControlPoints;
|
||||
vertex.tess.domain = 1u;
|
||||
vertex.tess.partitioning = 2u;
|
||||
vertex.tess.outputTopology = 2u;
|
||||
ShaderStageInputInfo inputs;
|
||||
inputs.vertex = &vertex;
|
||||
CompiledBindingLayout bindings;
|
||||
bindings.layout = program.Metadata().bindings;
|
||||
SpirvTargetOptions target{};
|
||||
target.vulkanVersion = 0x00401000;
|
||||
target.spirvVersion = 0x00010300;
|
||||
target.subgroupSize = 64;
|
||||
const auto words = SpirvEmitter().Emit(program, inputs, bindings, target);
|
||||
std::unordered_map<std::uint32_t, std::uint32_t> constants;
|
||||
for (std::size_t at = 5; at < words.size(); at += words[at] >> 16u) {
|
||||
const auto count = words[at] >> 16u;
|
||||
if (count == 0u || at + count > words.size()) throw std::runtime_error("the domain program compiled to a malformed SPIR-V instruction");
|
||||
const auto opcode = words[at] & 0xffffu;
|
||||
if (opcode == spv::OpConstant && count == 4u) constants[words[at + 2u]] = words[at + 3u];
|
||||
if (opcode == spv::OpAccessChain && count == 7u) return constants.at(words[at + 4u]);
|
||||
}
|
||||
throw std::runtime_error("the domain program reads no control point attribute");
|
||||
}
|
||||
|
||||
bool run(const char* name, const std::function<bool()>& body) {
|
||||
bool passed = false;
|
||||
try {
|
||||
@@ -206,5 +257,9 @@ int main() {
|
||||
return patch->Opcode() == IrOpcode::IMul32 && patch->Argument(1)->Resolve()->ImmediateU32() == PatchStride;
|
||||
});
|
||||
|
||||
passed &= run("a domain read at an immediate address past the last control point reads the last control point", [] {
|
||||
return EmittedEvaluationControlPoint(InputControlPoints * 16u) == InputControlPoints - 1u;
|
||||
});
|
||||
|
||||
return passed ? 0 : 1;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
struct VulkanDeviceTestAccess {
|
||||
static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); }
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Side = 16;
|
||||
constexpr std::uint32_t Samples = 4;
|
||||
constexpr std::uint32_t SampleShift = 2;
|
||||
constexpr std::size_t BlockBytes = 65536;
|
||||
constexpr std::uint32_t WaveSize = 64;
|
||||
constexpr std::uint32_t Float16x4Info = 0x0730u;
|
||||
constexpr std::uint32_t MultisampledAttrib = (SampleShift << 12u) | (SampleShift << 15u);
|
||||
constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u;
|
||||
constexpr std::uint32_t AaConfig4x = 0x0020c002u;
|
||||
constexpr std::uint32_t MsaaEnableClear = 2;
|
||||
constexpr std::uint32_t StandardLocations4x = 0x622ae6aeu;
|
||||
constexpr std::uint32_t Format16161616Float = 71;
|
||||
constexpr std::uint32_t Type2DMsaa = 14;
|
||||
constexpr std::uint32_t TileRenderTarget = 0x1b;
|
||||
constexpr std::uint32_t Threads = 32;
|
||||
constexpr std::uint32_t Words = 32;
|
||||
constexpr std::uint32_t LoadResult = 12;
|
||||
constexpr std::array<std::uint32_t, 2> Primitives{2u, 3u};
|
||||
constexpr std::array<float, 4> Drawn{0.25f, 0.5f, 0.125f, 1.0f};
|
||||
constexpr std::array<std::byte, 4> DrawnBytes{std::byte{64}, std::byte{128}, std::byte{32}, std::byte{255}};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{
|
||||
0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 11> PixelCode{
|
||||
0x7e0802ff, 0x3e800000, 0x7e0a02ff, 0x3f000000, 0x7e0c02ff, 0x3e000000, 0x7e0e02ff, 0x3f800000, 0xf800180f, 0x07060504, 0xbf810000,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 11> Unorm8PixelCode{
|
||||
0x7e0802ff, std::bit_cast<std::uint32_t>(64.0f / 255.0f), 0x7e0a02ff, std::bit_cast<std::uint32_t>(128.0f / 255.0f), 0x7e0c02ff, std::bit_cast<std::uint32_t>(32.0f / 255.0f), 0x7e0e02ff, std::bit_cast<std::uint32_t>(1.0f), 0xf800180f, 0x07060504, 0xbf810000,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 26> LoadCode{
|
||||
0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c,
|
||||
0x80000501, 0xe0301010, 0x80000601, 0xe0301014, 0x80000701, 0xbf8c3f70, 0xf0001f30, 0x00011005,
|
||||
0xbf8c3f70, 0xe0701030, 0x80001001, 0xe0701034, 0x80001101, 0xe0701038, 0x80001201, 0xe070103c,
|
||||
0x80001301, 0xbf810000,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::array<float, 4>, 4> Lines{{
|
||||
{-0.828125f, -0.4375f, 0.5f, 1.0f}, {0.90625f, 0.140625f, 0.5f, 1.0f},
|
||||
{-0.265625f, 0.90625f, 0.5f, 1.0f}, {0.34375f, -0.921875f, 0.5f, 1.0f},
|
||||
}};
|
||||
|
||||
alignas(256) std::array<std::uint32_t, Threads * Words> Buffer{};
|
||||
alignas(256) std::array<std::array<std::byte, 4096>, Primitives.size()> LinePixels{};
|
||||
|
||||
struct Block {
|
||||
explicit Block(std::size_t bytes) : bytes(bytes) {
|
||||
#ifdef _WIN32
|
||||
data = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
#else
|
||||
data = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, bytes));
|
||||
#endif
|
||||
Require(data != nullptr, "cannot allocate the multisampled color target");
|
||||
std::memset(data, 0, bytes);
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Add(data, bytes, true, true, true);
|
||||
}
|
||||
~Block() {
|
||||
{
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Remove(data);
|
||||
}
|
||||
#ifdef _WIN32
|
||||
VirtualFree(data, 0, MEM_RELEASE);
|
||||
#else
|
||||
std::free(data);
|
||||
#endif
|
||||
}
|
||||
Block(const Block&) = delete;
|
||||
Block& operator=(const Block&) = delete;
|
||||
std::uint64_t Address() const { return reinterpret_cast<std::uintptr_t>(data); }
|
||||
std::size_t bytes;
|
||||
std::uint8_t* data = nullptr;
|
||||
};
|
||||
|
||||
AgcDriver::QueueState LineQueue(const Block& block, std::uint32_t primitive) {
|
||||
const auto half = std::bit_cast<std::uint32_t>(static_cast<float>(Side) / 2.0f);
|
||||
const auto extent = (Side << 16u) | Side;
|
||||
AgcDriver::QueueState queue;
|
||||
queue.userConfig[0x242] = primitive;
|
||||
queue.context = {
|
||||
{0x2d5, 0x2000}, {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800},
|
||||
{0x2dc, 0xaa00}, {0x2f8, AaConfig4x}, {0x292, MsaaEnableClear}, {0x293, 0},
|
||||
{0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa},
|
||||
{0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff},
|
||||
{0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240},
|
||||
{0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010},
|
||||
{0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4},
|
||||
{0x318, static_cast<std::uint32_t>(block.Address() >> 8u)}, {0x390, static_cast<std::uint32_t>(block.Address() >> 40u)},
|
||||
{0x31b, 0}, {0x31c, Float16x4Info}, {0x31d, MultisampledAttrib},
|
||||
{0x3b0, ((Side - 1u) << 14u) | (Side - 1u)}, {0x3b8, TiledAttrib3}, {0x1e0, 0},
|
||||
{0xc, 0}, {0xd, extent}, {0x81, 0x80000000}, {0x82, extent},
|
||||
{0x90, 0x80000000}, {0x91, extent}, {0x94, 0x80000000}, {0x95, extent},
|
||||
{0x10f, half}, {0x110, half}, {0x111, std::bit_cast<std::uint32_t>(-static_cast<float>(Side) / 2.0f)}, {0x112, half},
|
||||
{0x113, std::bit_cast<std::uint32_t>(1.0f)}, {0x114, 0}, {0xb4, 0}, {0xb5, std::bit_cast<std::uint32_t>(1.0f)}
|
||||
};
|
||||
for (std::uint32_t offset = 0x2fe; offset <= 0x30d; ++offset) queue.context[offset] = (offset - 0x2feu) % 4u == 0 ? StandardLocations4x : 0u;
|
||||
queue.shader[0x008] = 0x100;
|
||||
queue.shader[0x009] = 0;
|
||||
return queue;
|
||||
}
|
||||
|
||||
AgcDriver::Graphics::State DecodeLines(const Block& block, std::uint32_t primitive) {
|
||||
const auto state = AgcDriver::Graphics::DecodeState(LineQueue(block, primitive));
|
||||
const auto& color = state.color;
|
||||
Require(state.topology == (primitive == 2u ? VK_PRIMITIVE_TOPOLOGY_LINE_LIST : VK_PRIMITIVE_TOPOLOGY_LINE_STRIP), "VGT_PRIMITIVE_TYPE " + std::to_string(primitive) + " did not decode as a line list or strip");
|
||||
Require(state.colors.size() == 1 && color.address == block.Address() && color.format == VK_FORMAT_R16G16B16A16_SFLOAT && color.samples == Samples && color.extent.width == Side && color.extent.height == Side, "the target did not decode as a 16x16 RGBA16F surface of 4 samples");
|
||||
Require(state.samples == Samples && !state.depth && !state.perSampleShading && !state.alphaToCoverage, "the lines did not decode as a 4x draw without depth, sample shading or alpha-to-coverage");
|
||||
const auto locations = AgcDriver::Graphics::SampleLocations(state);
|
||||
for (std::uint32_t sample = 0; sample < Samples; ++sample) Require(locations[sample].x == 0.5f && locations[sample].y == 0.5f, "the decoded 4x line state does not put every sample at the pixel center");
|
||||
Require(state.viewport.x == 0.0f && state.viewport.y == static_cast<float>(Side) && state.viewport.width == static_cast<float>(Side) && state.viewport.height == -static_cast<float>(Side) && state.scissor.offset.x == 0 && state.scissor.offset.y == 0 && state.scissor.extent.width == Side && state.scissor.extent.height == Side, "the decoded viewport or scissor does not cover the 16x16 target");
|
||||
return state;
|
||||
}
|
||||
|
||||
AgcDriver::Graphics::State OneSample(AgcDriver::Graphics::State state, std::span<std::byte> pixels, const AgcDriver::Graphics::ColorTargetLayout& linear) {
|
||||
state.color = {reinterpret_cast<std::uintptr_t>(pixels.data()), {Side, Side}, VK_FORMAT_R8G8B8A8_UNORM, linear.Bytes(), 0xe4u};
|
||||
state.colors = {state.color};
|
||||
state.samples = 1;
|
||||
state.customSampleLocations = false;
|
||||
state.sampleLocations.fill(0);
|
||||
state.sampleLocationGrid = false;
|
||||
return state;
|
||||
}
|
||||
|
||||
void DrawLines(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::State& state, std::span<const std::uint32_t> pixelCode) {
|
||||
const auto target = device.Target();
|
||||
std::vector<std::uint32_t> vertexUserData(4, 0u);
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(Lines.data());
|
||||
const std::array<std::uint32_t, 4> vertexBuffer{static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (16u << 16u), static_cast<std::uint32_t>(Lines.size()), 0x01016facu};
|
||||
std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin());
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> vertexMemory{{{reinterpret_cast<std::uintptr_t>(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}};
|
||||
ShaderRecompiler::RecompileRequest vertex{
|
||||
{ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(VertexCode.data()), VertexCode, 0, {}},
|
||||
{WaveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory},
|
||||
target,
|
||||
{0, 0, 0, 64}
|
||||
};
|
||||
vertex.useCache = false;
|
||||
const auto vertexResult = ShaderRecompiler::Recompile(vertex);
|
||||
const auto vertexPush = static_cast<std::uint32_t>(vertexResult.pushConstants.size());
|
||||
ShaderRecompiler::ShaderPixelStageInfo pixel{};
|
||||
pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY);
|
||||
pixel.posX = true;
|
||||
pixel.posY = true;
|
||||
pixel.targetOutputMode[0] = 9;
|
||||
pixel.targetExportMapping.fill(0xe4u);
|
||||
const std::vector<std::uint32_t> pixelUserData(8, 0u);
|
||||
const auto codeAddress = reinterpret_cast<std::uintptr_t>(pixelCode.data());
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}};
|
||||
ShaderRecompiler::RecompileRequest fragment{
|
||||
{ShaderStage::Fragment, codeAddress, pixelCode, 0, {}},
|
||||
{WaveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory},
|
||||
target,
|
||||
PixelPushLayout(vertexPush, target)
|
||||
};
|
||||
fragment.useCache = false;
|
||||
const auto pixelResult = ShaderRecompiler::Recompile(fragment);
|
||||
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{
|
||||
{ShaderStage::Vertex, &vertexResult, 0},
|
||||
{ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)}
|
||||
}};
|
||||
const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(Lines.size()), 0, 1, 0, false};
|
||||
device.Draw(state, draw, shaders);
|
||||
device.WaitIdle();
|
||||
}
|
||||
|
||||
std::vector<std::array<float, 4>> LoadSamples(AgcDriver::VulkanDevice& device, const Block& block) {
|
||||
const auto address = block.Address();
|
||||
const std::array<std::uint32_t, 8> texture{
|
||||
static_cast<std::uint32_t>(address >> 8u),
|
||||
static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (Format16161616Float << 20u) | (((Side - 1u) & 3u) << 30u),
|
||||
((Side - 1u) >> 2u) | ((Side - 1u) << 14u),
|
||||
0xfacu | (SampleShift << 16u) | (TileRenderTarget << 20u) | (Type2DMsaa << 28u),
|
||||
0u,
|
||||
SampleShift << 4u,
|
||||
0u,
|
||||
0u,
|
||||
};
|
||||
const auto bufferAddress = reinterpret_cast<std::uintptr_t>(Buffer.data());
|
||||
const std::array<std::uint32_t, 4> buffer{static_cast<std::uint32_t>(bufferAddress), static_cast<std::uint32_t>((bufferAddress >> 32u) & 0xffffu), static_cast<std::uint32_t>(Buffer.size() * 4u), 0x31016facu};
|
||||
std::vector<std::uint32_t> userData(16, 0u);
|
||||
std::copy(buffer.begin(), buffer.end(), userData.begin());
|
||||
std::copy(texture.begin(), texture.end(), userData.begin() + 4);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(LoadCode.data()), std::as_bytes(std::span(LoadCode))}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1};
|
||||
ShaderRecompiler::RecompileRequest request{
|
||||
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(LoadCode.data()), LoadCode, 0, {}},
|
||||
{32, 0, userData, compute, std::nullopt, std::nullopt, memory},
|
||||
device.Target(),
|
||||
{0, 0, 0, 128}
|
||||
};
|
||||
request.useCache = false;
|
||||
const auto program = ShaderRecompiler::Recompile(request);
|
||||
std::vector<std::array<float, 4>> loaded(Side * Side * Samples);
|
||||
for (std::uint32_t first = 0; first < loaded.size(); first += Threads) {
|
||||
Buffer.fill(0xdeadbeefu);
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
const auto index = first + tid;
|
||||
const auto pixel = index / Samples;
|
||||
const std::array<std::uint32_t, 6> words{0u, 0u, 0u, pixel % Side, pixel / Side, index % Samples};
|
||||
std::copy(words.begin(), words.end(), &Buffer[tid * Words]);
|
||||
}
|
||||
device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(LoadCode.data()));
|
||||
device.WaitIdle();
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
for (std::uint32_t component = 0; component < 4u; ++component) loaded[first + tid][component] = std::bit_cast<float>(Buffer[tid * Words + LoadResult + component]);
|
||||
}
|
||||
}
|
||||
return loaded;
|
||||
}
|
||||
|
||||
bool NearEndpoint(std::uint32_t x, std::uint32_t y) {
|
||||
return std::any_of(Lines.begin(), Lines.end(), [&](const std::array<float, 4>& vertex) {
|
||||
const double px = (static_cast<double>(vertex[0]) + 1.0) * 0.5 * Side;
|
||||
const double py = Side - (static_cast<double>(vertex[1]) + 1.0) * 0.5 * Side;
|
||||
return std::fabs(x + 0.5 - px) < 1.5 && std::fabs(y + 0.5 - py) < 1.5;
|
||||
});
|
||||
}
|
||||
|
||||
void CompareLines(AgcDriver::VulkanDevice& device, const Block& block, std::uint32_t primitive, std::span<std::byte> pixels) {
|
||||
const auto decoded = DecodeLines(block, primitive);
|
||||
DrawLines(device, decoded, PixelCode);
|
||||
const auto loaded = LoadSamples(device, block);
|
||||
const AgcDriver::Graphics::ColorTargetLayout linear(Side, Side, AgcDriver::Graphics::ColorTileMode::Linear, 4u);
|
||||
Require(linear.Bytes() <= pixels.size(), "the one-sample line target does not fit its memory");
|
||||
std::fill(pixels.begin(), pixels.end(), std::byte{0});
|
||||
DrawLines(device, OneSample(decoded, pixels, linear), Unorm8PixelCode);
|
||||
std::uint32_t covered = 0;
|
||||
std::uint32_t single = 0;
|
||||
std::uint32_t compared = 0;
|
||||
std::uint32_t differing = 0;
|
||||
for (std::uint32_t y = 0; y < Side; ++y) {
|
||||
for (std::uint32_t x = 0; x < Side; ++x) {
|
||||
char where[80];
|
||||
std::snprintf(where, sizeof(where), "VGT_PRIMITIVE_TYPE %u: pixel (%u, %u)", primitive, x, y);
|
||||
std::uint32_t drawn = 0;
|
||||
for (std::uint32_t sample = 0; sample < Samples; ++sample) {
|
||||
const auto& texel = loaded[(y * Side + x) * Samples + sample];
|
||||
Require(texel == Drawn || texel == std::array<float, 4>{}, std::string(where) + " of the 4x target holds neither the line color nor the cleared value");
|
||||
drawn += texel == Drawn ? 1u : 0u;
|
||||
}
|
||||
Require(drawn == 0u || drawn == Samples, std::string(where) + " has " + std::to_string(drawn) + " of its 4 samples covered by a line with every sample at the pixel center");
|
||||
const auto bytes = pixels.begin() + static_cast<std::ptrdiff_t>(linear.Offset(x, y));
|
||||
const bool oneSample = std::equal(DrawnBytes.begin(), DrawnBytes.end(), bytes);
|
||||
Require(oneSample || std::all_of(bytes, bytes + 4, [](std::byte value) { return value == std::byte{0}; }), std::string(where) + " of the one-sample target holds neither the line color nor zero");
|
||||
covered += drawn == Samples ? 1u : 0u;
|
||||
single += oneSample ? 1u : 0u;
|
||||
if (NearEndpoint(x, y)) continue;
|
||||
++compared;
|
||||
differing += (drawn == Samples) != oneSample ? 1u : 0u;
|
||||
Require(!oneSample || drawn == Samples, std::string(where) + " is drawn by the one-sample line but not covered in the 4x target");
|
||||
}
|
||||
}
|
||||
Require(covered >= 20u && covered <= 2u * single && compared >= 200u, "VGT_PRIMITIVE_TYPE " + std::to_string(primitive) + ": the 4x lines covered " + std::to_string(covered) + " pixels and the one-sample lines " + std::to_string(single) + ", with " + std::to_string(compared) + " pixels compared");
|
||||
std::printf("VGT_PRIMITIVE_TYPE %u: 4x lines with every sample at the pixel center covered %u pixels in full and none in part; the one-sample lines drew %u; %u of the %u pixels away from the endpoints differ\n", primitive, covered, single, differing, compared);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
if (!AgcDriver::VulkanDeviceTestAccess::ContextOf(*device).storageImageMultisample) {
|
||||
std::puts("skipped, the device has no shaderStorageImageMultisample to load the samples of a 4x target");
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
if ((AgcDriver::VulkanDeviceTestAccess::ContextOf(*device).sampleLocationSampleCounts & Samples) == 0u) {
|
||||
std::puts("skipped, the device has no VK_EXT_sample_locations at 4 samples to put every sample at the pixel center");
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
Block listTarget(4u * BlockBytes);
|
||||
Block stripTarget(4u * BlockBytes);
|
||||
struct ReleaseDevice {
|
||||
std::unique_ptr<AgcDriver::VulkanDevice>& value;
|
||||
~ReleaseDevice() { value.reset(); }
|
||||
} releaseDevice{device};
|
||||
const std::array<const Block*, Primitives.size()> targets{&listTarget, &stripTarget};
|
||||
for (std::size_t index = 0; index < Primitives.size(); ++index) CompareLines(*device, *targets[index], Primitives[index], LinePixels[index]);
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTextureLayout.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include "MsaaReadbackVertex_spv.h"
|
||||
#include "MsaaReadbackUint_spv.h"
|
||||
@@ -69,8 +70,8 @@ void CheckSupportSelection() {
|
||||
static_assert(!SurfaceFits(VkImageFormatProperties{}, 4u));
|
||||
}
|
||||
|
||||
bool SurfaceSupported(const Context& context, VkFormat format, std::uint32_t samples, std::uint32_t width = 17u, std::uint32_t height = 5u, std::uint32_t layers = 2u) {
|
||||
const VkImageUsageFlags usage = VK_IMAGE_USAGE_SAMPLED_BIT | (format == VK_FORMAT_D32_SFLOAT ? VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT : VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT);
|
||||
bool SurfaceSupported(const Context& context, VkFormat format, std::uint32_t samples, std::uint32_t width = 17u, std::uint32_t height = 5u, std::uint32_t layers = 2u, bool writable = false) {
|
||||
const VkImageUsageFlags usage = VK_IMAGE_USAGE_SAMPLED_BIT | (format == VK_FORMAT_D32_SFLOAT ? VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT : VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) | (writable ? VK_IMAGE_USAGE_STORAGE_BIT : 0u);
|
||||
VkImageFormatProperties supported{};
|
||||
const auto result = context.imageFormatProperties(context.physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, 0u, &supported);
|
||||
if (result != VK_ERROR_FORMAT_NOT_SUPPORTED) Check(result, "readback fixture format query");
|
||||
@@ -97,11 +98,11 @@ struct Surface {
|
||||
std::vector<VkImageView> views;
|
||||
std::vector<VkFramebuffer> framebuffers;
|
||||
|
||||
Surface(const Context& context, VkFormat format, std::uint32_t elementBytes, std::uint32_t samples, std::uint32_t width = 17u, std::uint32_t height = 5u, std::uint32_t layers = 2u) : context(context), format(format), elementBytes(elementBytes), samples(samples), width(width), height(height), layers(layers) {
|
||||
Surface(const Context& context, VkFormat format, std::uint32_t elementBytes, std::uint32_t samples, std::uint32_t width = 17u, std::uint32_t height = 5u, std::uint32_t layers = 2u, bool writable = false) : context(context), format(format), elementBytes(elementBytes), samples(samples), width(width), height(height), layers(layers) {
|
||||
try {
|
||||
const bool depth = format == VK_FORMAT_D32_SFLOAT;
|
||||
const auto aspect = depth ? VK_IMAGE_ASPECT_DEPTH_BIT : VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
const VkImageUsageFlags usage = VK_IMAGE_USAGE_SAMPLED_BIT | (depth ? VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT : VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT);
|
||||
const VkImageUsageFlags usage = VK_IMAGE_USAGE_SAMPLED_BIT | (depth ? VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT : VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) | (writable ? VK_IMAGE_USAGE_STORAGE_BIT : 0u);
|
||||
VkImageFormatProperties supported{};
|
||||
Check(context.imageFormatProperties(context.physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, 0u, &supported), "readback fixture format query");
|
||||
Require(SurfaceFits(supported, samples, width, height, layers), "readback fixture format does not support its sample count or extent");
|
||||
@@ -275,15 +276,15 @@ struct Surface {
|
||||
const auto layer = pixel / (width * height);
|
||||
const auto value = x + y * width + layer * width * height + sample * 31u;
|
||||
std::array<std::uint32_t, 4> words{0x91352700u ^ value, 0x45219b00u ^ (value * 3u), 0xc7391d00u ^ (value * 5u), 0x6b43e100u ^ (value * 7u)};
|
||||
if (format == VK_FORMAT_D32_SFLOAT) words[0] = std::bit_cast<std::uint32_t>(0.125f + static_cast<float>(element + 1u) / 65536.0f);
|
||||
if (format == VK_FORMAT_D32_SFLOAT) words[0] = std::bit_cast<std::uint32_t>(0.125f + static_cast<float>(element + 1u) / 1048576.0f);
|
||||
std::memcpy(result.data() + static_cast<std::size_t>(index) * elementBytes, words.data(), elementBytes);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
void CheckReadback(TextureDetiler& reader, const Surface& surface, std::uint32_t first, std::uint32_t elements, std::uint32_t repetitions, std::size_t extraBytes = 0u) {
|
||||
const auto expected = surface.Expected(first, elements);
|
||||
std::vector<std::byte> CheckReadback(TextureDetiler& reader, const Surface& surface, std::uint32_t first, std::uint32_t elements, std::uint32_t repetitions, std::size_t extraBytes = 0u, std::span<const std::byte> supplied = {}) {
|
||||
const auto expected = supplied.empty() ? surface.Expected(first, elements) : std::vector<std::byte>(supplied.begin(), supplied.end());
|
||||
const auto bytes = ((expected.size() + 3u) & ~std::size_t{3u}) + extraBytes;
|
||||
const auto alignment = std::max<std::size_t>(4u, surface.context.limits.minStorageBufferOffsetAlignment);
|
||||
const auto offset = (16u + alignment - 1u) / alignment * alignment;
|
||||
@@ -291,7 +292,7 @@ void CheckReadback(TextureDetiler& reader, const Surface& surface, std::uint32_t
|
||||
std::fill(output.Bytes().begin(), output.Bytes().end(), std::byte{0xa5});
|
||||
reader.BeginBatch();
|
||||
CommandBatch batch(surface.context);
|
||||
RecordMemoryBarrier(surface.context, batch.Handle(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT);
|
||||
RecordMemoryBarrier(surface.context, batch.Handle(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT);
|
||||
for (std::uint32_t repeat = 0u; repeat < repetitions; ++repeat) {
|
||||
reader.DispatchMultisampleReadback(batch.Handle(), surface.format, output.Handle(), offset, bytes, surface.arrayView, surface.width, surface.height, surface.layers, surface.samples, first, elements);
|
||||
if (repeat + 1u < repetitions) RecordMemoryBarrier(surface.context, batch.Handle(), VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT);
|
||||
@@ -304,6 +305,61 @@ void CheckReadback(TextureDetiler& reader, const Surface& surface, std::uint32_t
|
||||
if (mismatch.first != expected.end()) std::printf("readback mismatch: format=%u first=%u byte=%zu expected=0x%02x actual=0x%02x\n", static_cast<std::uint32_t>(surface.format), first, static_cast<std::size_t>(mismatch.first - expected.begin()), std::to_integer<unsigned>(*mismatch.first), std::to_integer<unsigned>(*mismatch.second));
|
||||
Require(std::equal(expected.begin(), expected.end(), data.begin() + offset), "multisample readback changed a rendered sample, component, layer or pixel");
|
||||
for (std::size_t index = 0u; index < data.size(); ++index) Require((index >= offset && index < offset + expected.size()) || data[index] == std::byte{0xa5}, "multisample readback changed a buffer sentinel or packed tail");
|
||||
return {data.begin() + offset, data.begin() + offset + expected.size()};
|
||||
}
|
||||
|
||||
void CheckRetile(TextureDetiler& reader, const Surface& surface) {
|
||||
const auto elements = surface.width * surface.height * surface.layers * surface.samples;
|
||||
const auto linear = CheckReadback(reader, surface, 0u, elements, 1u);
|
||||
const auto expected = surface.Expected(0u, elements);
|
||||
constexpr std::array<std::array<std::uint32_t, 3>, 7> coordinates{{
|
||||
{0u, 0u, 0u}, {128u, 66u, 16u}, {64u, 32u, 1u}, {32u, 64u, 2u},
|
||||
{63u, 31u, 7u}, {127u, 65u, 15u}, {1u, 1u, 3u}
|
||||
}};
|
||||
constexpr std::array<TextureTileMode, 2> modes{TextureTileMode::kR64KBX, TextureTileMode::kZ64KBX};
|
||||
constexpr std::array<std::array<std::uint64_t, 7>, 2> references{{
|
||||
{0ull, 6621216ull, 493568ull, 1016832ull, 2766588ull, 6170780ull, 1215508ull},
|
||||
{0ull, 6637700ull, 460808ull, 1000460ull, 2815728ull, 6219892ull, 1182776ull}
|
||||
}};
|
||||
for (std::size_t mode = 0u; mode < modes.size(); ++mode) {
|
||||
for (const auto pipeBankXor : {0u, 0x8100u}) {
|
||||
const MultisampleTextureLayout layout(modes[mode], surface.elementBytes, surface.samples, surface.width, surface.height, surface.layers, pipeBankXor);
|
||||
std::vector<std::byte> tiled(static_cast<std::size_t>(layout.Bytes()) + 32u, std::byte{0xa5});
|
||||
auto payload = std::span(tiled).subspan(16u, layout.Bytes());
|
||||
layout.Tile(linear, payload);
|
||||
std::vector<bool> covered(payload.size());
|
||||
std::size_t position = 0u;
|
||||
for (std::uint32_t layer = 0u; layer < surface.layers; ++layer) {
|
||||
for (std::uint32_t y = 0u; y < surface.height; ++y) {
|
||||
for (std::uint32_t x = 0u; x < surface.width; ++x) {
|
||||
for (std::uint32_t fragment = 0u; fragment < surface.samples; ++fragment) {
|
||||
const auto address = layout.Offset(x, y, layer, fragment);
|
||||
Require(address + surface.elementBytes <= payload.size(), "readback retile exceeds its allocation");
|
||||
for (std::uint32_t byte = 0u; byte < surface.elementBytes; ++byte) {
|
||||
Require(!covered[address + byte], "readback retile elements overlap");
|
||||
covered[address + byte] = true;
|
||||
Require(payload[address + byte] == expected[position++], "readback and retile disagree on layer, pixel or fragment order");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (std::size_t index = 0u; index < covered.size(); ++index) Require(covered[index] || payload[index] == std::byte{0xa5}, "readback retile changed tile padding");
|
||||
for (std::size_t index = 0u; index < coordinates.size(); ++index) {
|
||||
const auto& coordinate = coordinates[index];
|
||||
const auto fragment = static_cast<std::uint32_t>(index) % surface.samples;
|
||||
const auto source = (((coordinate[2] * surface.height + coordinate[1]) * surface.width + coordinate[0]) * surface.samples + fragment) * surface.elementBytes;
|
||||
const auto reference = references[mode][index];
|
||||
const auto address = (reference & ~65535ull) | ((reference & 65535ull) ^ pipeBankXor);
|
||||
Require(address + surface.elementBytes <= payload.size() && std::equal(expected.begin() + source, expected.begin() + source + surface.elementBytes, payload.begin() + address), "rendered readback differs at an AMD reference fragment address");
|
||||
}
|
||||
std::vector<std::byte> restored(expected.size());
|
||||
layout.Detile(payload, restored);
|
||||
Require(restored == expected && linear == expected, "readback retile roundtrip changed rendered element bits");
|
||||
for (std::size_t index = 0u; index < tiled.size(); ++index) Require((index >= 16u && index < 16u + payload.size()) || tiled[index] == std::byte{0xa5}, "readback retile changed an allocation sentinel");
|
||||
std::printf("multisample readback retile: format=%u mode=%s xor=0x%x samples=%u layers=%u elements=%u reference-points=7 padding/sentinels/roundtrip=passed\n", static_cast<std::uint32_t>(surface.format), mode == 0u ? "R_X" : "Z_X", pipeBankXor, surface.samples, surface.layers, elements);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template<typename TAction>
|
||||
@@ -331,6 +387,106 @@ void CheckRefusals(TextureDetiler& reader, const Surface& surface) {
|
||||
std::puts("multisample readback invalid-input refusals passed");
|
||||
}
|
||||
|
||||
std::vector<std::byte> Upload(TextureDetiler& transfer, TextureDetiler& reader, const Surface& surface, std::vector<std::byte>& expected, std::uint32_t first, std::uint32_t elements, std::uint32_t repetitions = 1u, std::span<const std::byte> supplied = {}) {
|
||||
const auto bytes = static_cast<std::size_t>(elements) * surface.elementBytes;
|
||||
const auto padded = (bytes + 3u) & ~std::size_t{3};
|
||||
const auto alignment = std::max<std::size_t>({4u, surface.elementBytes, surface.context.limits.minStorageBufferOffsetAlignment});
|
||||
const auto offset = (16u + alignment - 1u) / alignment * alignment;
|
||||
Buffer source(surface.context, offset + padded + 16u, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
|
||||
std::fill(source.Bytes().begin(), source.Bytes().end(), std::byte{0x5a});
|
||||
Require(supplied.empty() || supplied.size() == bytes, "multisample upload fixture source size mismatch");
|
||||
for (std::size_t index = 0; index < bytes; ++index) source.Bytes()[offset + index] = supplied.empty() ? static_cast<std::byte>((index * 29u + first * 17u + elements * 7u) & 255u) : supplied[index];
|
||||
const std::vector<std::byte> retained(source.Bytes().begin(), source.Bytes().end());
|
||||
std::copy_n(retained.begin() + offset, bytes, expected.begin() + static_cast<std::size_t>(first) * surface.elementBytes);
|
||||
const auto stride = 256u / alignment * alignment;
|
||||
Require(stride != 0u, "upload fixture cannot hold an aligned chunk");
|
||||
transfer.BeginBatch();
|
||||
CommandBatch batch(surface.context);
|
||||
RecordMemoryBarrier(surface.context, batch.Handle(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT);
|
||||
for (std::uint32_t repeat = 0; repeat < repetitions; ++repeat) {
|
||||
for (std::size_t done = 0; done < bytes; done += stride) {
|
||||
const auto count = std::min(stride, bytes - done);
|
||||
transfer.DispatchMultisampleImage(batch.Handle(), surface.elementBytes, source.Handle(), offset + done, (count + 3u) & ~std::size_t{3}, surface.arrayView, surface.width, surface.height, surface.layers, surface.samples, first + static_cast<std::uint32_t>(done / surface.elementBytes), static_cast<std::uint32_t>(count / surface.elementBytes));
|
||||
}
|
||||
RecordMemoryBarrier(surface.context, batch.Handle(), VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT);
|
||||
}
|
||||
batch.SubmitAndWait();
|
||||
source.Invalidate();
|
||||
Require(std::equal(retained.begin(), retained.end(), source.Bytes().begin()), "multisample upload changed source bytes or sentinels");
|
||||
return CheckReadback(reader, surface, 0u, surface.width * surface.height * surface.layers * surface.samples, 1u, 0u, expected);
|
||||
}
|
||||
|
||||
void CheckUploadRefusals(TextureDetiler& transfer, const Surface& surface) {
|
||||
Buffer source(surface.context, 256u, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
|
||||
CommandBatch batch(surface.context);
|
||||
const auto call = [&](std::uint32_t bytes, std::uint32_t fragments, std::uint32_t first, std::uint32_t elements, std::uint64_t offset = 0u, std::uint64_t range = 4u) {
|
||||
transfer.DispatchMultisampleImage(batch.Handle(), bytes, source.Handle(), offset, range, surface.arrayView, 1u, 1u, 1u, fragments, first, elements);
|
||||
};
|
||||
for (const auto bytes : {0u, 3u, 32u}) Refused([&] { call(bytes, 4u, 0u, 1u); }, "element size is unsupported");
|
||||
for (const auto fragments : {0u, 1u, 3u, 16u}) Refused([&] { call(4u, fragments, 0u, 1u); }, "fragment count is unsupported");
|
||||
for (const auto range : {std::array{0u, 0u}, std::array{4u, 1u}, std::array{3u, 2u}, std::array{0xffffffffu, 2u}}) Refused([&] { call(4u, 4u, range[0], range[1]); }, "element range is invalid");
|
||||
for (const auto range : {std::array<std::uint64_t, 2>{1u, 4u}, std::array<std::uint64_t, 2>{0u, 0u}, std::array<std::uint64_t, 2>{0u, 3u}, std::array<std::uint64_t, 2>{0u, 260u}, std::array<std::uint64_t, 2>{~std::uint64_t{0} - 3u, 8u}}) Refused([&] { call(4u, 4u, 0u, 1u, range[0], range[1]); }, "buffer range is invalid");
|
||||
Refused([&] { transfer.DispatchMultisampleImage(VK_NULL_HANDLE, 4u, source.Handle(), 0u, 4u, surface.arrayView, 1u, 1u, 1u, 4u, 0u, 1u); }, "requires commands, buffer and array view");
|
||||
Refused([&] { transfer.DispatchMultisampleImage(batch.Handle(), 4u, VK_NULL_HANDLE, 0u, 4u, surface.arrayView, 1u, 1u, 1u, 4u, 0u, 1u); }, "requires commands, buffer and array view");
|
||||
Refused([&] { transfer.DispatchMultisampleImage(batch.Handle(), 4u, source.Handle(), 0u, 4u, VK_NULL_HANDLE, 1u, 1u, 1u, 4u, 0u, 1u); }, "requires commands, buffer and array view");
|
||||
for (const auto extent : {std::array{0u, 1u, 1u}, std::array{1u, 0u, 1u}, std::array{1u, 1u, 0u}, std::array{16385u, 1u, 1u}, std::array{1u, 16385u, 1u}, std::array{1u, 1u, 8193u}}) Refused([&] { transfer.DispatchMultisampleImage(batch.Handle(), 4u, source.Handle(), 0u, 4u, surface.arrayView, extent[0], extent[1], extent[2], 4u, 0u, 1u); }, "extent is unsupported");
|
||||
Refused([&] { transfer.DispatchMultisampleImage(batch.Handle(), 4u, source.Handle(), 0u, 4u, surface.arrayView, 16384u, 16384u, 8192u, 8u, 0u, 1u); }, "element range is invalid");
|
||||
for (const bool multisample : {false, true}) {
|
||||
auto unsupported = surface.context;
|
||||
if (multisample) unsupported.storageImageMultisample = false;
|
||||
else unsupported.storageImageWriteWithoutFormat = false;
|
||||
TextureDetiler unavailable(unsupported);
|
||||
Refused([&] { unavailable.DispatchMultisampleImage(batch.Handle(), 4u, source.Handle(), 0u, 4u, surface.arrayView, 1u, 1u, 1u, 4u, 0u, 1u); }, "does not enable multisampled storage image writes without format");
|
||||
}
|
||||
}
|
||||
|
||||
void CheckUpload(const Context& context) {
|
||||
Require(context.storageImageMultisample && context.storageImageWriteWithoutFormat, "upload fixture requires enabled multisampled formatless storage writes");
|
||||
auto limited = context;
|
||||
limited.limits.maxComputeWorkGroupCount[0] = 1u;
|
||||
limited.limits.maxStorageBufferRange = 256u;
|
||||
TextureDetiler transfer(limited);
|
||||
TextureDetiler reader(context);
|
||||
std::uint32_t exercised = 0u;
|
||||
for (const auto bytes : {1u, 2u, 4u, 8u, 16u}) {
|
||||
const auto format = TextureDetiler::ImageElementFormat(bytes);
|
||||
for (const auto samples : {2u, 4u, 8u}) {
|
||||
if (!SurfaceSupported(context, format, samples, 17u, 5u, 2u, true)) continue;
|
||||
Surface surface(context, format, bytes, samples, 17u, 5u, 2u, true);
|
||||
surface.Render();
|
||||
const auto elements = surface.width * surface.height * surface.layers * samples;
|
||||
auto expected = surface.Expected(0u, elements);
|
||||
Upload(transfer, reader, surface, expected, 3u, 1u);
|
||||
Upload(transfer, reader, surface, expected, 1u, elements - 3u);
|
||||
Upload(transfer, reader, surface, expected, 0u, elements);
|
||||
Upload(transfer, reader, surface, expected, elements - 1u, 1u, samples == 4u ? 65u : 1u);
|
||||
if (bytes == 4u && samples == 4u) CheckUploadRefusals(transfer, surface);
|
||||
std::printf("multisample upload: bytes=%u samples=%u layers=2 full/partial/packed/source-sentinels/chunks/reuse=passed\n", bytes, samples);
|
||||
++exercised;
|
||||
}
|
||||
}
|
||||
Require(exercised != 0u, "no supported multisample upload subcase was exercised");
|
||||
const MultisampleTextureLayout active(TextureTileMode::kR64KBX, 8u, 4u, 17u, 5u, 2u);
|
||||
if (SurfaceSupported(context, VK_FORMAT_R32G32_UINT, 4u, active.Pitch(), active.PaddedHeight(), 2u, true)) {
|
||||
for (const auto xorValue : {0u, 0x8100u}) {
|
||||
const MultisampleTextureLayout layout(TextureTileMode::kR64KBX, 8u, 4u, active.Pitch(), active.PaddedHeight(), 2u, xorValue);
|
||||
std::vector<std::byte> tiled(static_cast<std::size_t>(layout.Bytes()));
|
||||
for (std::size_t byte = 0; byte < tiled.size(); ++byte) tiled[byte] = static_cast<std::byte>((byte * 37u + (byte >> 8u) * 13u + (byte >> 15u) * 71u + (byte >> 16u) * 101u) & 255u);
|
||||
std::vector<std::byte> linear(static_cast<std::size_t>(layout.LinearBytes()));
|
||||
layout.Detile(tiled, linear);
|
||||
Surface surface(context, VK_FORMAT_R32G32_UINT, 8u, 4u, layout.Pitch(), layout.PaddedHeight(), 2u, true);
|
||||
surface.Render();
|
||||
const auto elements = surface.width * surface.height * surface.layers * surface.samples;
|
||||
auto expected = surface.Expected(0u, elements);
|
||||
const auto actual = Upload(transfer, reader, surface, expected, 0u, elements, 1u, linear);
|
||||
std::vector<std::byte> retiled(tiled.size(), std::byte{0xa5});
|
||||
layout.Tile(actual, retiled);
|
||||
Require(retiled == tiled, "multisample padded initialization changed color bytes, fragments, layers, XOR or padding");
|
||||
std::printf("multisample padded upload: active=17x5 backing=%ux%u layers=2 fragments=4 bytes=%zu xor=0x%x exact roundtrip passed\n", surface.width, surface.height, retiled.size(), xorValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
@@ -343,8 +499,8 @@ int main(int argc, char** argv) {
|
||||
else if (mode == "uint16") { format = VK_FORMAT_R16_UINT; bytes = 2u; }
|
||||
else if (mode == "uint64") { format = VK_FORMAT_R32G32_UINT; bytes = 8u; }
|
||||
else if (mode == "uint128") { format = VK_FORMAT_R32G32B32A32_UINT; bytes = 16u; }
|
||||
else Require(mode == "uint32" || mode == "depth", "unknown multisample readback test mode");
|
||||
if (mode == "depth") format = VK_FORMAT_D32_SFLOAT;
|
||||
else Require(mode == "uint32" || mode == "depth" || mode == "upload" || mode == "retile" || mode == "depth_retile", "unknown multisample readback test mode");
|
||||
if (mode == "depth" || mode == "depth_retile") format = VK_FORMAT_D32_SFLOAT;
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex());
|
||||
@@ -357,9 +513,28 @@ int main(int argc, char** argv) {
|
||||
std::puts("skipped, the device does not support per-sample shading");
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
if (mode == "upload") {
|
||||
if (!context.storageImageMultisample || !context.storageImageWriteWithoutFormat) {
|
||||
std::puts("skipped, the device does not enable multisampled formatless storage writes");
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
CheckUpload(context);
|
||||
Require(checkedPipelineCacheUses != 0u, "upload fixture did not check pipeline cache use");
|
||||
std::printf("multisample upload guarded pipeline creation checks=%u passed\n", checkedPipelineCacheUses);
|
||||
return 0;
|
||||
}
|
||||
auto limited = context;
|
||||
limited.limits.maxComputeWorkGroupCount[0] = 1u;
|
||||
TextureDetiler reader(limited);
|
||||
if (mode == "retile" || mode == "depth_retile") {
|
||||
if (!SurfaceSupported(context, format, 4u, 129u, 67u, 17u)) return VulkanTestSkipped;
|
||||
Surface surface(context, format, bytes, 4u, 129u, 67u, 17u);
|
||||
surface.Render();
|
||||
CheckRetile(reader, surface);
|
||||
Require(checkedPipelineCacheUses != 0u, "retile fixture did not check pipeline cache use");
|
||||
std::printf("multisample retile guarded pipeline creation checks=%u passed\n", checkedPipelineCacheUses);
|
||||
return 0;
|
||||
}
|
||||
std::uint32_t checked = 0u;
|
||||
for (const auto samples : {2u, 4u, 8u}) {
|
||||
if (!SurfaceSupported(context, format, samples)) continue;
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#else
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
@@ -66,6 +67,15 @@ PFN_vkDestroyImageView realDestroyImageView = nullptr;
|
||||
PFN_vkCmdClearColorImage realClearColorImage = nullptr;
|
||||
PFN_vkCmdCopyImage realCopyImage = nullptr;
|
||||
|
||||
[[noreturn]] void FailViewOutlivesImage() {
|
||||
std::fputs("an MSAA image would be destroyed before one of its views; stopping\n", stderr);
|
||||
std::fflush(stderr);
|
||||
#ifdef _WIN32
|
||||
TerminateProcess(GetCurrentProcess(), 1);
|
||||
#endif
|
||||
std::_Exit(1);
|
||||
}
|
||||
|
||||
[[noreturn]] void FailPendingImage() {
|
||||
std::fputs("an unsubmitted MSAA image would be destroyed; stopping without submitting it\n", stderr);
|
||||
std::fflush(stderr);
|
||||
@@ -104,6 +114,7 @@ VKAPI_ATTR void VKAPI_CALL CountDestroyImage(VkDevice device, VkImage image, con
|
||||
for (const auto& [pending, generation] : pendingImages) {
|
||||
if (pending == image && images.at(image).generation == generation) FailPendingImage();
|
||||
}
|
||||
if (std::any_of(views.begin(), views.end(), [&](const auto& entry) { return entry.second == image; })) FailViewOutlivesImage();
|
||||
realDestroyImage(device, image, callbacks);
|
||||
images.erase(image);
|
||||
++destroyed;
|
||||
@@ -514,6 +525,37 @@ public:
|
||||
AwaitImages(0, "promoted MSAA targets were not fully released");
|
||||
}
|
||||
|
||||
void StorageTwin() {
|
||||
GuestBlock block;
|
||||
const auto target = Target(block);
|
||||
auto twin = target;
|
||||
twin.format = VK_FORMAT_R8G8B8A8_SRGB;
|
||||
const auto resource = Resource(block);
|
||||
VkImage renderedImage = VK_NULL_HANDLE, twinImage = VK_NULL_HANDLE, promotedImage = VK_NULL_HANDLE, promotedTwinImage = VK_NULL_HANDLE;
|
||||
Image rendered{}, promoted{};
|
||||
VkImageView twinView = VK_NULL_HANDLE;
|
||||
{
|
||||
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
|
||||
rendered = ImageFor(MultisampleTargetView(context, target), &renderedImage);
|
||||
twinView = MultisampleTargetView(context, twin);
|
||||
ImageFor(twinView, &twinImage);
|
||||
Require(twinImage == renderedImage, "an sRGB use of an MSAA surface rendered as UNORM does not reach its samples");
|
||||
}
|
||||
Sync();
|
||||
{
|
||||
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
|
||||
promoted = ImageFor(MultisampleTargetStorageView(context, resource), &promotedImage);
|
||||
Require(promoted.generation != rendered.generation, "MSAA storage promotion kept the rendered image");
|
||||
const auto promotedTwin = MultisampleTargetView(context, twin);
|
||||
ImageFor(promotedTwin, &promotedTwinImage);
|
||||
Require(promotedTwinImage == promotedImage, "after storage promotion an sRGB use of the MSAA surface still views the retired image");
|
||||
}
|
||||
Sync();
|
||||
block.Unmap();
|
||||
Clear();
|
||||
AwaitImages(0, "MSAA targets with twin views were not fully released");
|
||||
}
|
||||
|
||||
private:
|
||||
AgcDriver::VulkanDevice& device;
|
||||
Context context;
|
||||
@@ -533,6 +575,7 @@ int main() {
|
||||
run.SampledRemap();
|
||||
run.StorageRemap();
|
||||
run.OrderRemap();
|
||||
if (context.storageImageMultisample) run.StorageTwin();
|
||||
std::puts("retired MSAA allocations stay bounded, recorded clears and storage promotions retain their images, sampled aliases release their parents after the last batch, and sample-order targets keep their retired images until their batch completes");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "prx/libc/include/Shutdown.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <initializer_list>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
struct VulkanDeviceTestAccess {
|
||||
static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); }
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::ColorTarget;
|
||||
using AgcDriver::Graphics::Require;
|
||||
|
||||
constexpr std::size_t BlockBytes = 65536;
|
||||
constexpr std::uint32_t Side = 64;
|
||||
|
||||
class GuestBlock {
|
||||
public:
|
||||
GuestBlock() {
|
||||
#ifdef _WIN32
|
||||
block = static_cast<std::byte*>(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
#else
|
||||
block = static_cast<std::byte*>(std::aligned_alloc(BlockBytes, BlockBytes));
|
||||
#endif
|
||||
Require(block != nullptr, "multisampled sRGB twin: cannot allocate a guest block");
|
||||
std::memset(block, 0, BlockBytes);
|
||||
GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true);
|
||||
}
|
||||
|
||||
GuestBlock(const GuestBlock&) = delete;
|
||||
GuestBlock& operator=(const GuestBlock&) = delete;
|
||||
|
||||
std::uint64_t Address() const { return static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(block)); }
|
||||
|
||||
private:
|
||||
std::byte* block = nullptr;
|
||||
};
|
||||
|
||||
GuestBlock& NewBlock() {
|
||||
static auto* blocks = new std::deque<GuestBlock>();
|
||||
return blocks->emplace_back();
|
||||
}
|
||||
|
||||
ColorTarget MultisampledTarget(const GuestBlock& color, VkFormat format) {
|
||||
ColorTarget target{};
|
||||
target.address = color.Address();
|
||||
target.surfaceAddress = target.address;
|
||||
target.extent = {Side, Side};
|
||||
target.surfaceExtent = target.extent;
|
||||
target.format = format;
|
||||
target.tileMode = AgcDriver::Graphics::ColorTileMode::RenderTarget;
|
||||
target.elementBytes = 4;
|
||||
target.bytes = BlockBytes / 4;
|
||||
target.samples = 4;
|
||||
return target;
|
||||
}
|
||||
|
||||
struct Acquired {
|
||||
VkImageView view = VK_NULL_HANDLE;
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
std::shared_ptr<void> owner;
|
||||
};
|
||||
|
||||
Acquired Acquire(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::Context& context, const ColorTarget& target) {
|
||||
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
|
||||
Acquired acquired;
|
||||
acquired.view = AgcDriver::Graphics::MultisampleTargetView(context, target, acquired.owner, &acquired.image);
|
||||
Require(acquired.view != VK_NULL_HANDLE && acquired.image != VK_NULL_HANDLE && acquired.owner != nullptr, "the multisampled target has no view");
|
||||
device.SubmitRecorded(true);
|
||||
device.WaitIdle();
|
||||
return acquired;
|
||||
}
|
||||
|
||||
std::array<std::byte, 4> UniformTexel(const AgcDriver::Graphics::Context& context, VkImage multisampled, VkFormat format) {
|
||||
VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
||||
info.imageType = VK_IMAGE_TYPE_2D;
|
||||
info.format = format;
|
||||
info.extent = {Side, Side, 1};
|
||||
info.mipLevels = 1;
|
||||
info.arrayLayers = 1;
|
||||
info.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
info.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
info.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
|
||||
info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
VkImage resolved = VK_NULL_HANDLE;
|
||||
AgcDriver::Graphics::Check(context.Function<PFN_vkCreateImage>("vkCreateImage")(context.device, &info, nullptr, &resolved), "vkCreateImage resolved texels");
|
||||
VkMemoryRequirements requirements{};
|
||||
context.Function<PFN_vkGetImageMemoryRequirements>("vkGetImageMemoryRequirements")(context.device, resolved, &requirements);
|
||||
VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
||||
allocation.allocationSize = requirements.size;
|
||||
allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
||||
VkDeviceMemory memory = VK_NULL_HANDLE;
|
||||
AgcDriver::Graphics::Check(context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory resolved texels");
|
||||
AgcDriver::Graphics::Check(context.Function<PFN_vkBindImageMemory>("vkBindImageMemory")(context.device, resolved, memory, 0), "vkBindImageMemory resolved texels");
|
||||
AgcDriver::Graphics::Buffer readback(context, static_cast<std::size_t>(Side) * Side * 4u, VK_BUFFER_USAGE_TRANSFER_DST_BIT);
|
||||
{
|
||||
AgcDriver::Graphics::CommandBatch batch(context);
|
||||
const auto commands = batch.Handle();
|
||||
std::array<VkImageMemoryBarrier, 2> barriers{};
|
||||
for (auto& barrier : barriers) {
|
||||
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
||||
barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
}
|
||||
barriers[0].image = multisampled;
|
||||
barriers[0].srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
barriers[0].dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
||||
barriers[0].oldLayout = barriers[0].newLayout = VK_IMAGE_LAYOUT_GENERAL;
|
||||
barriers[1].image = resolved;
|
||||
barriers[1].dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
barriers[1].oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
barriers[1].newLayout = VK_IMAGE_LAYOUT_GENERAL;
|
||||
context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, static_cast<std::uint32_t>(barriers.size()), barriers.data());
|
||||
VkImageResolve region{};
|
||||
region.srcSubresource = region.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
||||
region.extent = {Side, Side, 1};
|
||||
context.Function<PFN_vkCmdResolveImage>("vkCmdResolveImage")(commands, multisampled, VK_IMAGE_LAYOUT_GENERAL, resolved, VK_IMAGE_LAYOUT_GENERAL, 1, ®ion);
|
||||
VkMemoryBarrier toCopy{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
toCopy.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
toCopy.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
||||
context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &toCopy, 0, nullptr, 0, nullptr);
|
||||
VkBufferImageCopy copy{};
|
||||
copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
||||
copy.imageExtent = {Side, Side, 1};
|
||||
context.Function<PFN_vkCmdCopyImageToBuffer>("vkCmdCopyImageToBuffer")(commands, resolved, VK_IMAGE_LAYOUT_GENERAL, readback.Handle(), 1, ©);
|
||||
VkMemoryBarrier toHost{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
toHost.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
toHost.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
|
||||
context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &toHost, 0, nullptr, 0, nullptr);
|
||||
batch.SubmitAndWait();
|
||||
}
|
||||
readback.Invalidate();
|
||||
const auto bytes = readback.Bytes();
|
||||
std::array<std::byte, 4> texel{};
|
||||
std::copy_n(bytes.begin(), texel.size(), texel.begin());
|
||||
for (std::size_t offset = 0; offset < bytes.size(); offset += texel.size()) Require(std::equal(texel.begin(), texel.end(), bytes.begin() + static_cast<std::ptrdiff_t>(offset)), "a cleared multisampled target holds different texels");
|
||||
context.Function<PFN_vkDestroyImage>("vkDestroyImage")(context.device, resolved, nullptr);
|
||||
context.Function<PFN_vkFreeMemory>("vkFreeMemory")(context.device, memory, nullptr);
|
||||
return texel;
|
||||
}
|
||||
|
||||
std::array<std::byte, 4> Bytes(std::uint32_t pattern) {
|
||||
std::array<std::byte, 4> bytes{};
|
||||
std::memcpy(bytes.data(), &pattern, bytes.size());
|
||||
return bytes;
|
||||
}
|
||||
|
||||
void FillTakenThroughTwin(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::Context& context, VkFormat created, VkFormat used, std::uint32_t pattern) {
|
||||
auto& color = NewBlock();
|
||||
const auto target = MultisampledTarget(color, created);
|
||||
const auto rendered = Acquire(device, context, target);
|
||||
const auto bytes = static_cast<std::size_t>(AgcDriver::Graphics::MultisampledColorBytes(target));
|
||||
Require(AgcDriver::Graphics::MultisampledColorFillAbsorbs(color.Address(), bytes, pattern), "the color fill is not taken as the target's clear in its creation format");
|
||||
AgcDriver::Graphics::NoteColorMetadataFill(color.Address(), bytes, pattern);
|
||||
const auto twin = Acquire(device, context, MultisampledTarget(color, used));
|
||||
Require(twin.image == rendered.image, "the twin use of the filled target does not reach its samples");
|
||||
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
|
||||
Require(UniformTexel(context, twin.image, created) == Bytes(pattern), "a fill taken as a clear through the twin format does not store the guest's bytes");
|
||||
}
|
||||
|
||||
bool TwinFormatsShareTheirSamples(AgcDriver::VulkanDevice& device) {
|
||||
const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device);
|
||||
if ((context.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0) {
|
||||
std::cout << "skipped, the device has no 4x color targets\n";
|
||||
return false;
|
||||
}
|
||||
struct ClearTargets {
|
||||
AgcDriver::VulkanDevice& device;
|
||||
VkDevice handle;
|
||||
~ClearTargets() {
|
||||
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
|
||||
device.SubmitRecorded(true);
|
||||
device.WaitIdle();
|
||||
AgcDriver::Graphics::ClearMultisampleTargets(handle);
|
||||
}
|
||||
} clearTargets{device, context.device};
|
||||
for (const auto [first, second] : {std::pair{VK_FORMAT_B8G8R8A8_SRGB, VK_FORMAT_B8G8R8A8_UNORM}, std::pair{VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_R8G8B8A8_SRGB}}) {
|
||||
if (AgcDriver::Graphics::SrgbTwinFormat(context, first) != second && AgcDriver::Graphics::SrgbTwinFormat(context, second) != first) continue;
|
||||
auto& color = NewBlock();
|
||||
const auto rendered = Acquire(device, context, MultisampledTarget(color, first));
|
||||
const auto twin = Acquire(device, context, MultisampledTarget(color, second));
|
||||
Require(twin.image == rendered.image && twin.owner == rendered.owner, "a use of a multisampled surface in its sRGB twin format does not reach the samples rendered in the other format");
|
||||
Require(twin.view != rendered.view, "a use of a multisampled surface in its sRGB twin format is given the view of the other format");
|
||||
const auto again = Acquire(device, context, MultisampledTarget(color, first));
|
||||
Require(again.image == rendered.image && again.view == rendered.view, "returning to the first format does not give back the first format's view");
|
||||
}
|
||||
if (AgcDriver::Graphics::SrgbTwinFormat(context, VK_FORMAT_R8G8B8A8_UNORM) == VK_FORMAT_R8G8B8A8_SRGB) {
|
||||
FillTakenThroughTwin(device, context, VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_R8G8B8A8_SRGB, 0x80808080u);
|
||||
FillTakenThroughTwin(device, context, VK_FORMAT_R8G8B8A8_SRGB, VK_FORMAT_R8G8B8A8_UNORM, 0xff0000ffu);
|
||||
}
|
||||
auto& other = NewBlock();
|
||||
const auto unorm = Acquire(device, context, MultisampledTarget(other, VK_FORMAT_R8G8B8A8_UNORM));
|
||||
const auto floating = Acquire(device, context, MultisampledTarget(other, VK_FORMAT_R16G16_SFLOAT));
|
||||
Require(floating.image != unorm.image, "a multisampled surface used in an unrelated format of the same size shares the other format's samples");
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
const bool tested = TwinFormatsShareTheirSamples(*device);
|
||||
LibcRunShutdown_nid_postfix();
|
||||
if (!tested) return VulkanTestSkipped;
|
||||
std::cout << "multisampled sRGB twin tests passed\n";
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
try {
|
||||
LibcRunShutdown_nid_postfix();
|
||||
} catch (...) {
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,9 @@
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <utility>
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
@@ -93,7 +95,9 @@ private:
|
||||
#endif
|
||||
};
|
||||
|
||||
int RunNativeClear(AgcDriver::VulkanDevice& device) {
|
||||
enum class ClearConsumer { Owner, Texture, Attachment };
|
||||
|
||||
int RunNativeClear(AgcDriver::VulkanDevice& device, ClearConsumer consumer) {
|
||||
using namespace ShaderRecompiler;
|
||||
using namespace AgcDriver::Graphics;
|
||||
const std::lock_guard lock(Memory::GpuMutex());
|
||||
@@ -153,10 +157,28 @@ 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);
|
||||
std::puts("Native full BDA clear: checking sampled owner before explicit WaitIdle");
|
||||
Require(context.recorder->PendingWriteOverlaps(target.htileAddress, metadataBytes), "the recognized HTILE clear is missing its pending write range");
|
||||
switch (consumer) {
|
||||
case ClearConsumer::Owner:
|
||||
Require(DepthSurfaceHolds(context, DecodeTextureResource(words), sampled.get()), "a pending full BDA clear lost its native sampled owner");
|
||||
break;
|
||||
case ClearConsumer::Texture:
|
||||
Require(CachedSampledTexture(context, words) == sampled, "a pending full BDA clear lost its native sampled view");
|
||||
break;
|
||||
case ClearConsumer::Attachment:
|
||||
Require(DepthSurfaceView(context, target) == view, "a pending full BDA clear lost its native attachment");
|
||||
break;
|
||||
}
|
||||
std::printf("Native full BDA clear: consumer=%u settled the recognized clear before WaitIdle\n", static_cast<unsigned>(consumer));
|
||||
Require(!context.recorder->PendingWriteOverlaps(target.htileAddress, metadataBytes), "the native HTILE consumer did not settle its clear's pending write");
|
||||
Require(Memory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation), "the native HTILE consumer did not complete its recognized clear");
|
||||
device.WaitIdle();
|
||||
for (std::size_t offset = 0; offset < BlockBytes; offset += 4u) {
|
||||
std::uint32_t value = 0;
|
||||
@@ -164,6 +186,7 @@ int RunNativeClear(AgcDriver::VulkanDevice& device) {
|
||||
Require(value == (offset < metadataBytes ? 0xfffffff0u : 0u), "the native GPU clear did not write its exact metadata range");
|
||||
}
|
||||
std::printf("Native full BDA clear: exact %zu-byte metadata readback passed; complete=%d\n", metadataBytes, Memory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation));
|
||||
Require(Memory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation), "the completed recognized native clear did not record exact coverage");
|
||||
Require(CachedSampledTexture(context, words) == sampled, "a completed full BDA clear lost its native sampled view");
|
||||
Require(DepthSurfaceView(context, target) == view, "a completed full BDA clear replaced its native attachment");
|
||||
device.WaitIdle();
|
||||
@@ -178,7 +201,11 @@ int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
return RunNativeClear(*device);
|
||||
for (const auto consumer : {ClearConsumer::Owner, ClearConsumer::Texture, ClearConsumer::Attachment}) {
|
||||
const auto result = RunNativeClear(*device, consumer);
|
||||
if (result != 0) return result;
|
||||
}
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "%s\n", error.what());
|
||||
return 1;
|
||||
|
||||
@@ -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;
|
||||
@@ -548,7 +601,8 @@ void CheckDrawFeedback() {
|
||||
Require(device != nullptr, "native depth draw feedback requires Vulkan");
|
||||
constexpr VkExtent2D extent{5u, 3u};
|
||||
constexpr std::uint32_t samples = 4u;
|
||||
TargetMemory memory(static_cast<std::size_t>(MultisampledDepthBytes_nid_no_patch(extent, 4u, samples))), metadata(65536u);
|
||||
const auto depthBytes = static_cast<std::size_t>(MultisampledDepthBytes_nid_no_patch(extent, 4u, samples));
|
||||
TargetMemory memory(depthBytes), metadata(65536u);
|
||||
std::memset(metadata.Data(), 0, 65536u);
|
||||
DepthTarget depth{memory.Address(), 0u, extent, VK_FORMAT_D32_SFLOAT, 0.0f, 0u, metadata.Address(), false, samples};
|
||||
depth.depthSwizzle = 24u;
|
||||
@@ -581,6 +635,9 @@ void CheckDrawFeedback() {
|
||||
const std::array<CompiledShader, 2> initial{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0u}, {ShaderRecompiler::ShaderStage::Fragment, &producer, pixelOffset}}};
|
||||
device->Draw(state, draw, initial);
|
||||
device->WaitIdle();
|
||||
std::array<std::byte, 1> probe{};
|
||||
TargetRefuses([&] { AgcDriver::GuestMemory::Read(depth.address, probe); }, "sample-preserving guest publication");
|
||||
Require(DepthSurfaceAt(depth.address), "a refused guest read retired a native depth surface a draw wrote");
|
||||
reader.spirv = DepthFeedbackFragmentShader(binding, true);
|
||||
reader.variantId = 3u;
|
||||
const auto words = DepthSampleWords(depth);
|
||||
@@ -597,10 +654,25 @@ void CheckDrawFeedback() {
|
||||
const std::array<CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0u}, {ShaderRecompiler::ShaderStage::Fragment, &reader, pixelOffset}}};
|
||||
TargetRefuses([&] { device->Draw(state, draw, shaders); }, "sampling a native multisampled depth target the draw attaches");
|
||||
device->WaitIdle();
|
||||
std::puts("Native depth feedback refused through the draw resource path");
|
||||
TargetMemory testedMemory(depthBytes), testedMetadata(65536u);
|
||||
std::memset(testedMetadata.Data(), 0, 65536u);
|
||||
const auto guestByte = [](std::size_t offset) { return static_cast<std::byte>(offset * 7u + 3u); };
|
||||
for (std::size_t offset = 0u; offset < depthBytes; ++offset) testedMemory.Data()[offset] = guestByte(offset);
|
||||
auto tested = depth;
|
||||
tested.address = testedMemory.Address();
|
||||
tested.htileAddress = testedMetadata.Address();
|
||||
state.depth = tested;
|
||||
state.depthWrite = false;
|
||||
device->Draw(state, draw, initial);
|
||||
device->WaitIdle();
|
||||
Require(DepthSurfaceAt(tested.address), "a depth-test-only draw left no resident native depth surface");
|
||||
AgcDriver::GuestMemory::Read(tested.address + depthBytes - 1u, probe);
|
||||
Require(probe[0] == guestByte(depthBytes - 1u), "a guest read after a depth-test-only draw did not return its guest bytes");
|
||||
Require(!DepthSurfaceAt(tested.address), "a guest read left a native depth surface only depth-tested resident");
|
||||
std::puts("Native depth feedback refused through the draw resource path; a written surface refused and a depth-tested one retired on guest read");
|
||||
}
|
||||
|
||||
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 +681,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 +721,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;
|
||||
@@ -679,6 +751,7 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments,
|
||||
}
|
||||
const auto view = DepthSurfaceView(context, target);
|
||||
Require(DepthSurfaceView(context, target) == view, "native depth attachment did not reuse its image");
|
||||
NoteDepthSurfaceWrite(context, target);
|
||||
std::array<std::byte, 1> beforeSampling{};
|
||||
TargetRefuses([&] { AgcDriver::GuestMemory::Read(target.address + bytes - 1u, beforeSampling); }, "sample-preserving guest publication");
|
||||
TargetRefuses([&] { RequireNoMultisampleTarget(target.htileAddress + metadataBytes - 1u, 1u, "a DMA copy"); }, "sample-preserving guest publication");
|
||||
@@ -770,17 +843,65 @@ 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});
|
||||
std::string pendingRefusal;
|
||||
try { CachedSampledTexture(context, words); }
|
||||
catch (const std::exception& error) { pendingRefusal = error.what(); }
|
||||
const bool refused = pendingRefusal.find("external GPU writes") != std::string::npos;
|
||||
if (!refused) std::fprintf(stderr, "Pending HTILE texture refusal: %s\n", pendingRefusal.c_str());
|
||||
Require(refused, "an unrecognized pending HTILE write was accepted or refused for another reason");
|
||||
}
|
||||
}
|
||||
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");
|
||||
@@ -843,6 +964,59 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments,
|
||||
std::printf("Native depth format=%u samples=%u clear=%u iterate256=%u: imported, rasterized twice, sampled and refused conflicts\n", static_cast<std::uint32_t>(format), fragments, one, iterate256);
|
||||
}
|
||||
|
||||
void CheckUnwrittenGuestAccess(Context context, VkFormat format, std::uint32_t fragments) {
|
||||
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));
|
||||
const auto metadataBytes = static_cast<std::size_t>(HtileMetadataBytes(extent));
|
||||
TargetMemory memory(bytes), metadata(65536u), unrelated(65536u);
|
||||
const auto guestByte = [](std::size_t offset) { return static_cast<std::byte>(offset * 7u + 3u); };
|
||||
for (std::size_t offset = 0u; offset < bytes; ++offset) memory.Data()[offset] = guestByte(offset);
|
||||
std::memset(metadata.Data(), 0, metadataBytes);
|
||||
DepthTarget target{memory.Address(), 0u, extent, format, 0.0f, 0u, metadata.Address(), false, fragments};
|
||||
target.depthSwizzle = 24u;
|
||||
target.iterate256 = target.htilePipeAligned = target.htileDepthOnly = true;
|
||||
Recorder recorder(context);
|
||||
recorder.Activate();
|
||||
struct Cleanup {
|
||||
Recorder& recorder;
|
||||
Context context;
|
||||
~Cleanup() { recorder.Sync(); ClearCachedTextures(context.device); ClearDepthSurfaces(context.device); }
|
||||
} cleanup{recorder, context};
|
||||
const auto resident = [&] { return HasResidentDepthGuards_nid_no_patch() && DepthSurfaceAt(target.address); };
|
||||
auto view = DepthSurfaceView(context, target);
|
||||
Require(view != VK_NULL_HANDLE && resident(), "unwritten native depth fixture has no resident owner");
|
||||
auto otherDevice = context;
|
||||
otherDevice.device = VK_NULL_HANDLE;
|
||||
NoteDepthSurfaceWrite(otherDevice, target);
|
||||
auto unattached = target;
|
||||
unattached.address += 65536u;
|
||||
NoteDepthSurfaceWrite(context, unattached);
|
||||
std::array<std::byte, 1> probe{};
|
||||
AgcDriver::GuestMemory::Read(unrelated.Address(), probe);
|
||||
Require(resident(), "an unrelated guest read retired a native depth surface");
|
||||
for (const bool readMetadata : {false, true}) {
|
||||
if (readMetadata) RequireNoMultisampleTarget(target.htileAddress + metadataBytes - 1u, 1u, "a DMA copy");
|
||||
else {
|
||||
AgcDriver::GuestMemory::Read(target.address + bytes - 1u, probe);
|
||||
Require(probe[0] == guestByte(bytes - 1u), "a guest read over an unwritten native depth surface did not return its guest bytes");
|
||||
}
|
||||
Require(!resident(), "a guest access left an unwritten native depth surface resident");
|
||||
RequireNoResidentDepthRead_nid_no_patch(target.address, bytes);
|
||||
RequireNoResidentDepthRead_nid_no_patch(target.htileAddress, metadataBytes);
|
||||
recorder.Sync();
|
||||
const auto rebound = DepthSurfaceView(context, target);
|
||||
Require(rebound != VK_NULL_HANDLE && rebound != view && resident(), "rebinding after a guest access did not import a new native depth surface");
|
||||
view = rebound;
|
||||
}
|
||||
NoteDepthSurfaceWrite(context, target);
|
||||
TargetRefuses([&] { AgcDriver::GuestMemory::Read(target.address, probe); }, "sample-preserving guest publication");
|
||||
TargetRefuses([&] { RequireNoMultisampleTarget(target.htileAddress, 1u, "a DMA copy"); }, "sample-preserving guest publication");
|
||||
Require(resident(), "a refused guest access retired a written native depth surface");
|
||||
for (std::size_t offset = 0u; offset < bytes; ++offset) Require(memory.Data()[offset] == guestByte(offset), "retiring an unwritten native depth surface changed its guest bytes");
|
||||
Require(std::all_of(metadata.Data(), metadata.Data() + metadataBytes, [](std::byte value) { return value == std::byte{0}; }), "retiring an unwritten native depth surface changed its HTILE");
|
||||
std::printf("Native depth format=%u samples=%u: guest access retired the unwritten surface and refused the written one\n", static_cast<std::uint32_t>(format), fragments);
|
||||
}
|
||||
|
||||
void CheckHtileRouting(Context context, VkFormat format, std::uint32_t fragments) {
|
||||
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));
|
||||
@@ -1031,6 +1205,7 @@ void CheckRetirement(Context context, VkFormat format, std::uint32_t samples, st
|
||||
const auto words = DepthSampleWords(target);
|
||||
const auto resource = DecodeTextureResource(words);
|
||||
const auto firstView = DepthSurfaceView(context, target);
|
||||
NoteDepthSurfaceWrite(context, target);
|
||||
auto firstTexture = DepthSurfaceTexture(context, words, resource, VkComponentMapping{});
|
||||
Require(firstTexture != nullptr, "retirement fixture has no native sampled view");
|
||||
ColorTarget color{colorMemory.Address(), extent, VK_FORMAT_R8G8B8A8_UNORM, extent.width * extent.height * 4u, 0u};
|
||||
@@ -1129,7 +1304,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 +1312,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 +1328,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,9 +1358,10 @@ 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);
|
||||
CheckUnwrittenGuestAccess(context, format, samples);
|
||||
}
|
||||
}
|
||||
CheckEligibility(context);
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -45,6 +45,7 @@ constexpr std::uint32_t Height = 128;
|
||||
constexpr std::uint32_t ScissorWidth = 128;
|
||||
constexpr std::size_t BlockBytes = 65536;
|
||||
constexpr std::uint32_t Float11_11_10Info = 0x4171cu;
|
||||
constexpr std::uint32_t StandardOrderInfo = Float11_11_10Info & ~0x1800u;
|
||||
constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u;
|
||||
constexpr std::uint32_t LinearAttrib3 = 0x09000000u;
|
||||
constexpr std::uint32_t Float32Export = 9;
|
||||
@@ -58,7 +59,7 @@ constexpr std::array<std::array<float, 4>, 3> Triangle{{
|
||||
{-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f}
|
||||
}};
|
||||
|
||||
alignas(256) std::array<std::array<std::uint32_t, 64>, 32> Programs{};
|
||||
alignas(256) std::array<std::array<std::uint32_t, 64>, 64> Programs{};
|
||||
std::size_t programCount = 0;
|
||||
|
||||
std::uint32_t SmallFloat(float value, std::uint32_t mantissaBits) {
|
||||
@@ -78,7 +79,8 @@ std::uint32_t SmallFloat(float value, std::uint32_t mantissaBits) {
|
||||
return (static_cast<std::uint32_t>(exponent - 1 + 15) << mantissaBits) | static_cast<std::uint32_t>(std::floor((fraction * 2.0 - 1.0) * scale));
|
||||
}
|
||||
|
||||
std::uint32_t Packed(std::array<float, 3> rgb) {
|
||||
std::uint32_t Packed(std::array<float, 3> rgb, bool standardOrder) {
|
||||
if (standardOrder) return SmallFloat(rgb[0], 5) | (SmallFloat(rgb[1], 6) << 10u) | (SmallFloat(rgb[2], 6) << 21u);
|
||||
return SmallFloat(rgb[2], 5) | (SmallFloat(rgb[1], 6) << 10u) | (SmallFloat(rgb[0], 6) << 21u);
|
||||
}
|
||||
|
||||
@@ -170,17 +172,17 @@ struct Block {
|
||||
std::uint8_t* data = nullptr;
|
||||
};
|
||||
|
||||
AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t attrib3) {
|
||||
AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t attrib3, bool standardOrder) {
|
||||
AgcDriver::Registers cx;
|
||||
cx[0x318] = static_cast<std::uint32_t>(block.Address() >> 8u);
|
||||
cx[0x31b] = 0;
|
||||
cx[0x31c] = Float11_11_10Info;
|
||||
cx[0x31c] = standardOrder ? StandardOrderInfo : Float11_11_10Info;
|
||||
cx[0x31d] = 0;
|
||||
cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u);
|
||||
cx[0x3b8] = attrib3;
|
||||
cx[0x390] = static_cast<std::uint32_t>(block.Address() >> 40u);
|
||||
auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0);
|
||||
Require(color.format == VK_FORMAT_R32_UINT && color.elementBytes == 4 && color.componentMapping == 0xc6u && color.packing == ShaderRecompiler::ColorExportPacking::Float11_11_10 && color.bytes == block.bytes, "the 11_11_10 float target did not decode as a packed R32_UINT surface");
|
||||
Require(color.format == VK_FORMAT_R32_UINT && color.elementBytes == 4 && color.componentMapping == (standardOrder ? 0xe4u : 0xc6u) && color.packing == ShaderRecompiler::ColorExportPacking::Float11_11_10 && color.bytes == block.bytes, "the 11_11_10 float target did not decode as a packed R32_UINT surface");
|
||||
return color;
|
||||
}
|
||||
|
||||
@@ -290,10 +292,10 @@ std::string Describe(std::array<float, 3> rgb) {
|
||||
return text;
|
||||
}
|
||||
|
||||
void TargetTests(AgcDriver::VulkanDevice& device, Block& block, std::uint32_t attrib3, ColorTileMode mode, const char* kind) {
|
||||
void TargetTests(AgcDriver::VulkanDevice& device, Block& block, std::uint32_t attrib3, ColorTileMode mode, const char* kind, bool standardOrder) {
|
||||
const ColorTargetLayout layout(Width, Height, mode, 4);
|
||||
Require(layout.Bytes() == block.bytes, "the test surface size disagrees with its layout");
|
||||
const auto color = DecodeTarget(block, attrib3);
|
||||
const auto color = DecodeTarget(block, attrib3, standardOrder);
|
||||
auto before = Fill(block);
|
||||
const float nan = std::numeric_limits<float>::quiet_NaN();
|
||||
const float infinity = std::numeric_limits<float>::infinity();
|
||||
@@ -311,12 +313,12 @@ void TargetTests(AgcDriver::VulkanDevice& device, Block& block, std::uint32_t at
|
||||
}};
|
||||
for (const auto& rgb : cases) {
|
||||
Draw(device, color, Float32Export, Float32Program(rgb));
|
||||
ExpectSurface(device, block, layout, before, Packed(rgb), std::string("a 32_ABGR export of ") + Describe(rgb) + " into a " + kind + " 11_11_10 float target");
|
||||
ExpectSurface(device, block, layout, before, Packed(rgb, standardOrder), std::string("a 32_ABGR export of ") + Describe(rgb) + " into a " + kind + " 11_11_10 float target");
|
||||
before = ReadBack(device, block);
|
||||
}
|
||||
const std::array<float, 3> halves{0.5f, 0.75f, 0.125f};
|
||||
Draw(device, color, Float16Export, Float16Program(halves));
|
||||
ExpectSurface(device, block, layout, before, Packed(halves), std::string("an FP16_ABGR export of ") + Describe(halves) + " into a " + kind + " 11_11_10 float target");
|
||||
ExpectSurface(device, block, layout, before, Packed(halves, standardOrder), std::string("an FP16_ABGR export of ") + Describe(halves) + " into a " + kind + " 11_11_10 float target");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -327,8 +329,10 @@ int main() {
|
||||
if (!device) return VulkanTestSkipped;
|
||||
Block tiled(ColorTargetLayout(Width, Height, ColorTileMode::RenderTarget, 4).Bytes());
|
||||
Block linear(ColorTargetLayout(Width, Height, ColorTileMode::Linear, 4).Bytes());
|
||||
TargetTests(*device, tiled, TiledAttrib3, ColorTileMode::RenderTarget, "tiled");
|
||||
TargetTests(*device, linear, LinearAttrib3, ColorTileMode::Linear, "linear");
|
||||
for (const bool standardOrder : {false, true}) {
|
||||
TargetTests(*device, tiled, TiledAttrib3, ColorTileMode::RenderTarget, standardOrder ? "tiled standard-order" : "tiled", standardOrder);
|
||||
TargetTests(*device, linear, LinearAttrib3, ColorTileMode::Linear, standardOrder ? "linear standard-order" : "linear", standardOrder);
|
||||
}
|
||||
std::puts("11_11_10 float color target tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -57,6 +57,14 @@ std::array<std::array<float, 4>, 3> ClipPositions() {
|
||||
return positions;
|
||||
}
|
||||
|
||||
constexpr std::array<Vertex, 4> StripCorners{{{-0.8f, -0.8f, 1.0f}, {0.8f, -0.8f, 1.0f}, {-0.8f, 0.8f, 1.0f}, {0.8f, 0.8f, 1.0f}}};
|
||||
|
||||
std::array<std::array<float, 4>, 4> StripPositions() {
|
||||
std::array<std::array<float, 4>, 4> positions{};
|
||||
for (std::size_t i = 0; i < StripCorners.size(); ++i) positions[i] = {StripCorners[i].x, StripCorners[i].y, 0.5f, 1.0f};
|
||||
return positions;
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu};
|
||||
@@ -67,11 +75,13 @@ enum class Emulation {
|
||||
Tessellation,
|
||||
};
|
||||
|
||||
ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, Emulation emulation, std::span<const std::uint32_t> pixelCode, std::uint32_t inputs, VkCullModeFlags cullMode = VK_CULL_MODE_NONE, bool flat = false, VkProvokingVertexModeEXT provoking = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT) {
|
||||
ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, Emulation emulation, std::span<const std::uint32_t> pixelCode, std::uint32_t inputs, VkCullModeFlags cullMode = VK_CULL_MODE_NONE, bool flat = false, VkProvokingVertexModeEXT provoking = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT, bool strip = false) {
|
||||
Pixels.fill(Unwritten);
|
||||
auto target = device.Target();
|
||||
target.fragmentShaderBarycentricEnabled = false;
|
||||
static const auto positions = ClipPositions();
|
||||
static const auto trianglePositions = ClipPositions();
|
||||
static const auto stripPositions = StripPositions();
|
||||
const auto positions = strip ? std::span<const std::array<float, 4>>(stripPositions) : std::span<const std::array<float, 4>>(trianglePositions);
|
||||
|
||||
std::vector<std::uint32_t> vertexUserData(4, 0u);
|
||||
const auto vertexBuffer = BufferDescriptor(positions.data(), 16u, static_cast<std::uint32_t>(positions.size()));
|
||||
@@ -125,7 +135,7 @@ ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, Emulatio
|
||||
state.colors = {state.color};
|
||||
state.hasColorTarget = true;
|
||||
state.renderExtent = {Width, Height};
|
||||
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
state.topology = strip ? VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP : VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
state.viewport = {0, static_cast<float>(Height), static_cast<float>(Width), -static_cast<float>(Height), 0, 1};
|
||||
state.negativeOneToOne = false;
|
||||
state.scissor = {{0, 0}, {Width, Height}};
|
||||
@@ -282,6 +292,87 @@ void CheckWinding(AgcDriver::VulkanDevice& device, Emulation emulation, const st
|
||||
}
|
||||
}
|
||||
|
||||
struct StripPixel {
|
||||
bool inside;
|
||||
std::array<std::size_t, 3> order;
|
||||
std::array<double, 3> weights;
|
||||
};
|
||||
|
||||
StripPixel StripBarycentrics(std::uint32_t x, std::uint32_t y, bool snapped) {
|
||||
std::array<std::array<double, 2>, 4> window{};
|
||||
for (std::size_t i = 0; i < StripCorners.size(); ++i) {
|
||||
window[i] = {(StripCorners[i].x + 1.0) * Width / 2.0, (1.0 - StripCorners[i].y) * Height / 2.0};
|
||||
if (snapped) {
|
||||
for (auto& coordinate : window[i]) coordinate = std::round(coordinate * 256.0) / 256.0;
|
||||
}
|
||||
}
|
||||
const double px = x + 0.5;
|
||||
const double py = y + 0.5;
|
||||
const auto edge = [](const std::array<double, 2>& a, const std::array<double, 2>& b, double cx, double cy) { return (b[0] - a[0]) * (cy - a[1]) - (b[1] - a[1]) * (cx - a[0]); };
|
||||
for (const std::array<std::size_t, 3> order : {std::array<std::size_t, 3>{0, 1, 2}, std::array<std::size_t, 3>{1, 3, 2}}) {
|
||||
const auto& a = window[order[0]];
|
||||
const auto& b = window[order[1]];
|
||||
const auto& c = window[order[2]];
|
||||
const double area = edge(a, b, c[0], c[1]);
|
||||
const std::array<double, 3> weights{edge(b, c, px, py) / area, edge(c, a, px, py) / area, edge(a, b, px, py) / area};
|
||||
if (std::min({weights[0], weights[1], weights[2]}) > 0.02) return {true, order, weights};
|
||||
}
|
||||
return {false, {}, {}};
|
||||
}
|
||||
|
||||
void CheckStrip(AgcDriver::VulkanDevice& device, Emulation emulation, const std::string& name) {
|
||||
Draw(device, emulation, BarycentricPixelCode, PixelInputBit(PixelInput::LinearCenter), VK_CULL_MODE_NONE, false, VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT, true);
|
||||
std::array<std::uint32_t, 2> checked{};
|
||||
for (std::uint32_t y = 0; y < Height; ++y) {
|
||||
for (std::uint32_t x = 0; x < Width; ++x) {
|
||||
const auto expected = StripBarycentrics(x, y, false);
|
||||
if (!expected.inside) continue;
|
||||
const auto snapped = StripBarycentrics(x, y, true);
|
||||
for (std::uint32_t channel = 0; channel < 4; ++channel) {
|
||||
const auto reference = static_cast<float>(expected.weights[1u + (channel & 1u)]);
|
||||
const auto snappedReference = snapped.inside && snapped.order == expected.order ? static_cast<float>(snapped.weights[1u + (channel & 1u)]) : reference;
|
||||
Require(MatchesEither(Pixel(x, y, channel), reference, snappedReference), name + " triangle strip: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(Pixel(x, y, channel)) + ", expected " + std::to_string(reference));
|
||||
}
|
||||
++checked[expected.order[0]];
|
||||
}
|
||||
}
|
||||
Require(checked[0] > 200u && checked[1] > 200u, name + " triangle strip: too few pixels of each triangle were checked");
|
||||
static const auto positions = StripPositions();
|
||||
Draw(device, emulation, VertexValuePixelCode, PixelInputBit(PixelInput::PerspectiveCenter), VK_CULL_MODE_NONE, false, VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT, true);
|
||||
for (std::uint32_t y = 0; y < Height; ++y) {
|
||||
for (std::uint32_t x = 0; x < Width; ++x) {
|
||||
const auto expected = StripBarycentrics(x, y, false);
|
||||
if (!expected.inside) continue;
|
||||
const auto first = positions[expected.order[0]][0];
|
||||
const std::array<float, 3> values{positions[expected.order[1]][0] - first, positions[expected.order[2]][0] - first, first};
|
||||
for (std::uint32_t channel = 0; channel < 3; ++channel) {
|
||||
Require(Pixel(x, y, channel) == values[channel], name + " triangle strip v_interp_mov: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(Pixel(x, y, channel)) + ", expected " + std::to_string(values[channel]));
|
||||
}
|
||||
}
|
||||
}
|
||||
std::printf("barycentric emulation through the %s passed for a triangle strip: %u and %u pixels of its even and odd triangle\n", name.c_str(), checked[0], checked[1]);
|
||||
}
|
||||
|
||||
void CheckStripFlatLast(AgcDriver::VulkanDevice& device, Emulation emulation, const std::string& name) {
|
||||
Draw(device, emulation, FlatBarycentricPixelCode, PixelInputBit(PixelInput::PerspectiveCenter), VK_CULL_MODE_NONE, true, VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT, true);
|
||||
const auto positions = StripPositions();
|
||||
std::array<std::uint32_t, 2> checked{};
|
||||
for (std::uint32_t y = 0; y < Height; ++y) {
|
||||
for (std::uint32_t x = 0; x < Width; ++x) {
|
||||
const auto expected = StripBarycentrics(x, y, false);
|
||||
if (!expected.inside) continue;
|
||||
const auto triangle = expected.order[0];
|
||||
const auto provoking = triangle == 0u ? 2u : 3u;
|
||||
for (std::uint32_t channel = 2; channel < 4; ++channel) {
|
||||
Require(Pixel(x, y, channel) == positions[provoking][0], name + " flat last-provoking strip: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") flat channel " + std::to_string(channel) + " is " + std::to_string(Pixel(x, y, channel)) + ", expected vertex " + std::to_string(provoking) + " value " + std::to_string(positions[provoking][0]));
|
||||
}
|
||||
++checked[triangle];
|
||||
}
|
||||
}
|
||||
Require(checked[0] > 200u && checked[1] > 200u, name + " flat last-provoking strip: too few pixels of each triangle were checked");
|
||||
std::printf("flat last-provoking strip through %s passed: %u and %u pixels of its even and odd triangle\n", name.c_str(), checked[0], checked[1]);
|
||||
}
|
||||
|
||||
void RunEmulation(AgcDriver::VulkanDevice& device, Emulation emulation) {
|
||||
const std::string name = emulation == Emulation::Geometry ? "geometry stage" : "tessellation stages";
|
||||
const auto perspective = PixelInputBit(PixelInput::PerspectiveCenter);
|
||||
@@ -305,8 +396,10 @@ void RunEmulation(AgcDriver::VulkanDevice& device, Emulation emulation) {
|
||||
if (emulation == Emulation::Tessellation) {
|
||||
Draw(device, emulation, FlatBarycentricPixelCode, perspective, VK_CULL_MODE_NONE, true, VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT);
|
||||
CheckFlat(name + " flat input, last provoking vertex", 2u);
|
||||
CheckStripFlatLast(device, emulation, name);
|
||||
}
|
||||
std::printf("barycentric emulation through the %s passed: %u perspective and %u linear I/J pixels, raw vertex values, host interpolation, face culling and flat inputs\n", name.c_str(), smooth, noPerspective);
|
||||
CheckStrip(device, emulation, name);
|
||||
std::printf("barycentric emulation through the %s passed: %u perspective and %u linear I/J pixels, raw vertex values, host interpolation, face culling, flat inputs and triangle strips\n", name.c_str(), smooth, noPerspective);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -76,6 +76,41 @@ void ClosingJoinsPreparation() {
|
||||
Require(ranHere && RegistrationPreparationThreads() == 0, "a closed preparation pool restarted after being joined");
|
||||
}
|
||||
|
||||
void ClosingDropsQueuedPrebuilds() {
|
||||
using namespace AgcDriver::DriverDetail;
|
||||
std::promise<void> started;
|
||||
auto running = started.get_future();
|
||||
std::promise<void> gate;
|
||||
auto opened = gate.get_future().share();
|
||||
bool queuedRan = false;
|
||||
SubmitLibraryPrebuild([&] { started.set_value(); opened.wait(); });
|
||||
for (int index = 0; index < 3; ++index) SubmitLibraryPrebuild([&] { queuedRan = true; });
|
||||
running.wait();
|
||||
auto entering = std::make_shared<std::promise<void>>();
|
||||
auto entered = entering->get_future();
|
||||
auto awaited = std::make_shared<std::promise<void>>();
|
||||
auto woken = awaited->get_future();
|
||||
std::thread([entering, awaited] {
|
||||
entering->set_value();
|
||||
AwaitLibraryPrebuilds();
|
||||
awaited->set_value();
|
||||
}).detach();
|
||||
entered.wait();
|
||||
const bool waiting = woken.wait_for(std::chrono::milliseconds(20)) == std::future_status::timeout;
|
||||
auto closing = std::async(std::launch::async, [] { CloseRegistrationPreparation(); });
|
||||
const bool waited = closing.wait_for(std::chrono::milliseconds(20)) == std::future_status::timeout;
|
||||
gate.set_value();
|
||||
const bool returned = closing.wait_for(std::chrono::seconds(10)) == std::future_status::ready;
|
||||
const bool wokeUp = woken.wait_for(std::chrono::seconds(10)) == std::future_status::ready;
|
||||
closing.get();
|
||||
Require(waiting, "a waiter for the library prebuilds returned while one was still running");
|
||||
Require(waited, "closing returned while a library prebuild was still running");
|
||||
Require(returned, "closing did not return once the running library prebuild ended");
|
||||
Require(wokeUp, "a waiter for the library prebuilds did not return after closing dropped the queued ones");
|
||||
Require(!queuedRan, "closing ran the queued library prebuilds");
|
||||
Require(RegistrationPreparationThreads() == 0, "closing the library prebuilds left their thread running");
|
||||
}
|
||||
|
||||
void StoppingJoinsCompareHelpers() {
|
||||
using namespace AgcDriver::Graphics;
|
||||
const std::vector<std::byte> first(std::size_t{2} << 20u, std::byte{1});
|
||||
@@ -168,6 +203,7 @@ int main(int argc, char** argv) {
|
||||
try {
|
||||
const std::string_view mode = argc > 1 ? argv[1] : "";
|
||||
if (mode == "--joined") ClosingJoinsPreparation();
|
||||
else if (mode == "--queued-prebuilds") ClosingDropsQueuedPrebuilds();
|
||||
else if (mode == "--compare-helpers") StoppingJoinsCompareHelpers();
|
||||
else if (mode == "--draw-committer") ShutdownJoinsDrawCommitter();
|
||||
else if (mode == "--shader-cache-writer") ShutdownJoinsShaderCacheWriter();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <exception>
|
||||
#include <initializer_list>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
|
||||
constexpr std::byte Kept{0x40};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 16> VertexCode{
|
||||
0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u,
|
||||
0x7e060280u, 0x7e0802f2u, 0xf80008cfu, 0x04030201u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u,
|
||||
};
|
||||
alignas(256) constexpr std::array<std::uint32_t, 4> WhitePixelCode{0x7e0e02f2u, 0xf800180fu, 0x07070707u, 0xbf810000u};
|
||||
alignas(256) std::array<std::byte, 1024> ColorMemory{};
|
||||
alignas(256) std::array<std::byte, 65536> DepthMemory{};
|
||||
alignas(256) std::array<std::byte, 65536> StencilMemory{};
|
||||
|
||||
struct Header {
|
||||
Shader shader{};
|
||||
ShaderUserData userData{};
|
||||
std::array<ShaderRegister, 4> registers{};
|
||||
};
|
||||
|
||||
alignas(8) Header VertexHeader{};
|
||||
alignas(8) Header PixelHeader{};
|
||||
|
||||
void Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) {
|
||||
header.shader.file_header = 0x34333231u;
|
||||
header.shader.version = 0x18u;
|
||||
header.shader.header_size = sizeof(Header);
|
||||
header.shader.shader_size = static_cast<std::uint32_t>(bytes);
|
||||
header.shader.code = code;
|
||||
header.shader.type = type;
|
||||
header.shader.user_data = &header.userData;
|
||||
if (type == 1) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(code);
|
||||
header.registers = {{{0x008, static_cast<std::uint32_t>(address >> 8u)}, {0x009, static_cast<std::uint32_t>(address >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast<std::uint32_t>(1.0f)}}};
|
||||
header.shader.sh_registers = header.registers.data();
|
||||
header.shader.num_sh_registers = header.registers.size();
|
||||
}
|
||||
AgcDriverRegisterShader_nid_postfix(&header.shader);
|
||||
}
|
||||
|
||||
void Append(std::vector<std::uint32_t>& commands, std::uint32_t opcode, std::initializer_list<std::uint32_t> payload) {
|
||||
commands.push_back(0xc0000000u | (static_cast<std::uint32_t>(payload.size() - 1) << 16u) | (opcode << 8u));
|
||||
commands.insert(commands.end(), payload);
|
||||
}
|
||||
|
||||
struct StencilState {
|
||||
std::uint32_t depthControl;
|
||||
std::uint32_t operations;
|
||||
std::uint32_t refMask;
|
||||
};
|
||||
|
||||
std::vector<std::uint32_t> Draw(const StencilState& stencilState) {
|
||||
const auto vertex = reinterpret_cast<std::uintptr_t>(VertexCode.data());
|
||||
const auto pixel = reinterpret_cast<std::uintptr_t>(WhitePixelCode.data());
|
||||
const auto color = reinterpret_cast<std::uintptr_t>(ColorMemory.data());
|
||||
const auto depth = reinterpret_cast<std::uintptr_t>(DepthMemory.data());
|
||||
const auto stencil = reinterpret_cast<std::uintptr_t>(StencilMemory.data());
|
||||
const std::vector<std::pair<std::uint32_t, std::uint32_t>> context{
|
||||
{0x2d5, 0x2000},
|
||||
{0x1b3, 0}, {0x1b4, 0}, {0x1b6, 0}, {0x207, 0}, {0x200, stencilState.depthControl}, {0x203, 0x800},
|
||||
{0x2dc, 0xaa00}, {0x2f8, 0}, {0x293, 0},
|
||||
{0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa},
|
||||
{0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff},
|
||||
{0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240},
|
||||
{0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010},
|
||||
{0x1c4, 9}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028},
|
||||
{0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u},
|
||||
{0x3b8, 0x9000000}, {0x1e0, 0},
|
||||
{0xc, 0}, {0xd, 0x40040},
|
||||
{0x81, 0x80000000}, {0x82, 0x40040},
|
||||
{0x90, 0x80000000}, {0x91, 0x40040},
|
||||
{0x94, 0x80000000}, {0x95, 0x40040},
|
||||
{0x10f, std::bit_cast<std::uint32_t>(32.0f)}, {0x110, std::bit_cast<std::uint32_t>(32.0f)},
|
||||
{0x111, std::bit_cast<std::uint32_t>(-2.0f)}, {0x112, std::bit_cast<std::uint32_t>(2.0f)},
|
||||
{0x113, std::bit_cast<std::uint32_t>(1.0f)}, {0x114, 0},
|
||||
{0xb4, 0}, {0xb5, std::bit_cast<std::uint32_t>(1.0f)},
|
||||
{0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0},
|
||||
{0x318, static_cast<std::uint32_t>(color >> 8u)}, {0x390, static_cast<std::uint32_t>(color >> 40u)}, {0x292, 2},
|
||||
{0x000, 0}, {0x002, 0}, {0x007, (3u << 16u) | 63u},
|
||||
{0x008, 0}, {0x009, std::bit_cast<std::uint32_t>(1.0f)}, {0x00a, 0}, {0x00b, 0},
|
||||
{0x010, 3}, {0x011, 1},
|
||||
{0x012, static_cast<std::uint32_t>(depth >> 8u)}, {0x014, static_cast<std::uint32_t>(depth >> 8u)},
|
||||
{0x013, static_cast<std::uint32_t>(stencil >> 8u)}, {0x015, static_cast<std::uint32_t>(stencil >> 8u)},
|
||||
{0x01a, static_cast<std::uint32_t>(depth >> 40u)}, {0x01c, static_cast<std::uint32_t>(depth >> 40u)},
|
||||
{0x01b, static_cast<std::uint32_t>(stencil >> 40u)}, {0x01d, static_cast<std::uint32_t>(stencil >> 40u)},
|
||||
{0x10b, stencilState.operations}, {0x10c, stencilState.refMask}, {0x10d, stencilState.refMask},
|
||||
};
|
||||
const std::vector<std::pair<std::uint32_t, std::uint32_t>> shader{
|
||||
{0xc8, static_cast<std::uint32_t>(vertex >> 8u)}, {0xc9, static_cast<std::uint32_t>(vertex >> 40u)}, {0x8b, 1u << 1u}, {0x8c, 0},
|
||||
{0x008, static_cast<std::uint32_t>(pixel >> 8u)}, {0x009, static_cast<std::uint32_t>(pixel >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast<std::uint32_t>(1.0f)},
|
||||
};
|
||||
std::vector<std::uint32_t> commands;
|
||||
Append(commands, 0x79, {0x242, 4});
|
||||
for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value});
|
||||
for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value});
|
||||
Append(commands, 0x2d, {3, 2});
|
||||
return commands;
|
||||
}
|
||||
|
||||
void Submit(const StencilState& stencilState) {
|
||||
ColorMemory.fill(Kept);
|
||||
auto commands = Draw(stencilState);
|
||||
Packet packet{commands.data(), static_cast<std::uint32_t>(commands.size()), 0, {}};
|
||||
Require(sceAgcDriverSubmitDcb(&packet) == 0, "the stencil draw submission failed");
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
}
|
||||
|
||||
constexpr std::uint32_t StencilEnable = 1u;
|
||||
constexpr std::uint32_t Equal = 2u << 8u;
|
||||
constexpr std::uint32_t LessEqual = 3u << 8u;
|
||||
constexpr std::uint32_t Greater = 4u << 8u;
|
||||
constexpr std::uint32_t NotEqual = 5u << 8u;
|
||||
constexpr std::uint32_t Always = 7u << 8u;
|
||||
|
||||
std::uint32_t RefMask(std::uint32_t test, std::uint32_t compareMask, std::uint32_t writeMask, std::uint32_t opValue) {
|
||||
return test | (compareMask << 8u) | (writeMask << 16u) | (opValue << 24u);
|
||||
}
|
||||
|
||||
std::array<std::byte, 1024> Fill(std::uint32_t value) {
|
||||
Submit({StencilEnable | Always, 0x30u, RefMask(value, 0xff, 0xff, 0)});
|
||||
const auto coverage = ColorMemory;
|
||||
Require(std::count(coverage.begin(), coverage.end(), Kept) != static_cast<std::ptrdiff_t>(coverage.size()), "the stencil fill covered no pixels");
|
||||
return coverage;
|
||||
}
|
||||
|
||||
void Expect(const std::array<std::byte, 1024>& coverage, std::uint32_t value, const std::string& what) {
|
||||
Submit({StencilEnable | Equal, 0u, RefMask(value, 0xff, 0, 0)});
|
||||
std::size_t drawn = 0;
|
||||
for (std::size_t pixel = 0; pixel < coverage.size(); pixel += 4u) {
|
||||
const bool covered = coverage[pixel] != Kept;
|
||||
drawn += covered;
|
||||
for (std::size_t channel = 0; channel < 4u; ++channel) Require(ColorMemory[pixel + channel] == (covered ? std::byte{255} : Kept), what + ": the stencil is not " + std::to_string(value) + " exactly where the draw covered");
|
||||
}
|
||||
Require(drawn != 0, what + ": the probe covered no pixels");
|
||||
}
|
||||
|
||||
void Case(const std::string& what, std::uint32_t initial, const StencilState& stencilState, std::uint32_t expected, bool testPasses = true) {
|
||||
const auto coverage = Fill(initial);
|
||||
Submit(stencilState);
|
||||
if (testPasses) Require(ColorMemory == coverage, what + ": the draw under test did not color exactly the filled pixels");
|
||||
else Require(std::all_of(ColorMemory.begin(), ColorMemory.end(), [](auto value) { return value == Kept; }), what + ": a draw whose stencil test fails wrote color");
|
||||
Expect(coverage, expected, what);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
{
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
}
|
||||
Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2);
|
||||
Registered(PixelHeader, WhitePixelCode.data(), sizeof(WhitePixelCode), 1);
|
||||
const std::array<ShaderRegister, 3> pixelContext{{{0x203, 0x800}, {0x1c4, 9}, {0x1b6, 0}}};
|
||||
AgcDriverResolveShaderAbi_nid_postfix(&PixelHeader.shader, pixelContext, {});
|
||||
Case("an equal test replaced by the operation value", 0xff, {StencilEnable | Equal, 0x40u, RefMask(0xff, 0xff, 0xff, 0x40)}, 0x40);
|
||||
Case("an equal test replaced by the operation value on pass and depth fail", 0xff, {StencilEnable | Equal, 0x440u, RefMask(0xff, 0xff, 0xff, 0x40)}, 0x40);
|
||||
Case("an equal test replaced by 0xff", 0x25, {StencilEnable | Equal, 0x20u, RefMask(0x25, 0xff, 0xff, 0)}, 0xff);
|
||||
Case("an equal test replaced through a partial write mask", 0xff, {StencilEnable | Equal, 0x40u, RefMask(0xff, 0xff, 0xf0, 0x40)}, 0x4f);
|
||||
Case("a failed not-equal test replaced by the operation value", 0xff, {StencilEnable | NotEqual, 0x4u, RefMask(0xff, 0xff, 0xff, 0x40)}, 0x40, false);
|
||||
Case("a less-or-equal test against 0xff replaced by the operation value", 0xff, {StencilEnable | LessEqual, 0x40u, RefMask(0xff, 0xff, 0xff, 0x40)}, 0x40);
|
||||
Case("a failed greater test against 0xff replaced by the operation value", 0xff, {StencilEnable | Greater, 0x4u, RefMask(0xff, 0xff, 0xff, 0x40)}, 0x40, false);
|
||||
AgcDriverShutdown_nid_postfix();
|
||||
std::puts("stencil replacements with a known stencil value passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
try { AgcDriverShutdown_nid_postfix(); } catch (...) {}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -946,6 +947,28 @@ void RequireMergedHullWithoutSubgroups(const ShaderRecompiler::SpirvTarget& targ
|
||||
}
|
||||
}
|
||||
|
||||
void RequireMergedHullConstantIndices(const ShaderRecompiler::SpirvTarget& target) {
|
||||
std::uint32_t cursor = 0;
|
||||
const auto hull = Compile(target, ShaderStage::TessellationControl, MergedHullCode, cursor, MergedTriangles);
|
||||
const auto words = hull.spirv.Words();
|
||||
std::unordered_set<std::uint32_t> constants;
|
||||
std::unordered_set<std::uint32_t> folded;
|
||||
for (std::size_t at = 5; at + 1u < words.size(); at += words[at] >> 16u) {
|
||||
const auto count = words[at] >> 16u;
|
||||
Require(count != 0u && at + count <= words.size(), "the merged hull program compiled to a malformed SPIR-V instruction");
|
||||
const auto opcode = words[at] & 0xffffu;
|
||||
if (opcode == spv::OpConstant && count >= 3u) constants.insert(words[at + 2u]);
|
||||
const bool integer = opcode == spv::OpIAdd || opcode == spv::OpIMul || opcode == spv::OpUDiv || opcode == spv::OpUMod || opcode == spv::OpShiftRightLogical || opcode == spv::OpBitwiseAnd;
|
||||
const auto operand = [&](std::uint32_t id) { return constants.contains(id) || folded.contains(id); };
|
||||
if (integer && count == 5u && operand(words[at + 3u]) && operand(words[at + 4u])) folded.insert(words[at + 2u]);
|
||||
if (opcode == spv::OpAccessChain) {
|
||||
for (std::uint32_t index = 4u; index < count; ++index) {
|
||||
Require(!folded.contains(words[at + index]), "the merged hull program indexes a tessellation attribute with arithmetic on constants instead of a constant, which AMD's Windows driver reads wrongly");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RequireRegisteredMergedStages() {
|
||||
const auto low = [](const auto& code) { return static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(code.data()) >> 8u); };
|
||||
const auto high = [](const auto& code) { return static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(code.data()) >> 40u); };
|
||||
@@ -1003,6 +1026,7 @@ int main() {
|
||||
ExpectRefusal(target, ShaderStage::TessellationControl, MergedHullOutsidePatch, "reads LDS outside its patch's control point records", MergedTriangles);
|
||||
ExpectRefusal(target, ShaderStage::TessellationControl, MergedHullReadLane, "another lane's register", MergedTriangles);
|
||||
RequireMergedHullWithoutSubgroups(target);
|
||||
RequireMergedHullConstantIndices(target);
|
||||
RequirePatches(*device, target, MergedHullCode, "merged hull program", MergedTriangles, MergedHullCode);
|
||||
RequirePatches(*device, target, MergedHullRegisters, "merged hull program storing its local part's registers", MergedTriangles, MergedHullRegisters);
|
||||
RequireCounterClockwise(*device, target);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -10,7 +10,7 @@ void main() {
|
||||
uint y = uint(gl_FragCoord.y);
|
||||
#if READ_DEPTH
|
||||
uint element = (x + y * params.width + params.layer * params.width * params.height) * params.samples + uint(gl_SampleID);
|
||||
gl_FragDepth = 0.125 + float(element + 1u) / 65536.0;
|
||||
gl_FragDepth = 0.125 + float(element + 1u) / 1048576.0;
|
||||
#else
|
||||
uint value = x + y * params.width + params.layer * params.width * params.height + uint(gl_SampleID) * 31u;
|
||||
color = uvec4(0x91352700u ^ value, 0x45219b00u ^ (value * 3u), 0xc7391d00u ^ (value * 5u), 0x6b43e100u ^ (value * 7u));
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
include(${CMAKE_CURRENT_SOURCE_DIR}/../libSceAjm.native/AjmLibrary.cmake)
|
||||
add_sce_ajm_library(libSceAjm ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp)
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -847,11 +845,6 @@ int APS5_VABI sceAmprAmmWaitCommandBufferCompletion(std::uint32_t id) {
|
||||
return AmmSubmitted_nid_no_patch(id) ? 0 : SCE_KERNEL_ERROR_ESRCH;
|
||||
}
|
||||
|
||||
int APS5_VABI _ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void APS5_VABI _Unlock_shared_ptr_spin_lock_nid_postfix(void) {
|
||||
sharedPtrSpinLock.clear(std::memory_order_release);
|
||||
}
|
||||
@@ -860,21 +853,11 @@ int APS5_VABI _Cnd_init_with_name_nid_postfix(void** handle, const char*) {
|
||||
return _Cnd_init_nid_postfix(handle);
|
||||
}
|
||||
|
||||
int APS5_VABI _ZTVSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI _ZNKSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE3getES3_S3_RSt8ios_baseRNSt5_IosbIiE8_IostateEP2tmPKcSE__nid_postfix(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE7_GetcatEPPKNSt6locale5facetEPKS5__nid_postfix(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Loaded 100 of 599 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user