mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-27 00:28:14 +00:00
Merge main into fix/http-status-line-measured-ps5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
commit
5fd2403725
470 files changed
+18492
-2605
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
|
||||
@@ -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
|
||||
@@ -52,3 +52,9 @@
|
||||
[submodule "3rdparty/vgmstream"]
|
||||
path = 3rdparty/vgmstream
|
||||
url = https://github.com/vgmstream/vgmstream.git
|
||||
[submodule "3rdparty/ogg"]
|
||||
path = 3rdparty/ogg
|
||||
url = https://github.com/xiph/ogg.git
|
||||
[submodule "3rdparty/vorbis"]
|
||||
path = 3rdparty/vorbis
|
||||
url = https://github.com/xiph/vorbis.git
|
||||
+1
Submodule 3rdparty/ogg added at be05b13e98.
+1
Submodule 3rdparty/vorbis added at 0657aee69d.
@@ -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()
|
||||
@@ -211,7 +218,7 @@ if(TARGET mimalloc)
|
||||
target_compile_features(pe_import_first PRIVATE cxx_std_20)
|
||||
if(MINGW)
|
||||
target_compile_options(pe_import_first PRIVATE -fasynchronous-unwind-tables)
|
||||
target_link_options(pe_import_first PRIVATE -static -static-libgcc -static-libstdc++)
|
||||
target_link_options(pe_import_first PRIVATE -municode -static -static-libgcc -static-libstdc++)
|
||||
endif()
|
||||
add_custom_command(
|
||||
OUTPUT ${PATCHED_DIR}/mimalloc.dll
|
||||
@@ -302,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)
|
||||
@@ -327,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
|
||||
@@ -362,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)
|
||||
@@ -595,6 +621,16 @@ if(BUILD_TESTING)
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpCppWebApi>")
|
||||
endif()
|
||||
|
||||
add_test_executable(guest_np_cpp_web_api_signed_out_tests tests/GuestNpCppWebApiSignedOut.cpp)
|
||||
target_include_directories(guest_np_cpp_web_api_signed_out_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(guest_np_cpp_web_api_signed_out_tests PRIVATE libSceNpCppWebApi libc)
|
||||
configure_windows_unwind(guest_np_cpp_web_api_signed_out_tests)
|
||||
add_test(NAME guest_np_cpp_web_api_signed_out COMMAND guest_np_cpp_web_api_signed_out_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_np_cpp_web_api_signed_out PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpCppWebApi>")
|
||||
endif()
|
||||
|
||||
add_test_executable(savedata_core_write_replacement_tests tests/SaveDataWriteReplacement.cpp)
|
||||
target_include_directories(savedata_core_write_replacement_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(savedata_core_write_replacement_tests PRIVATE libSceSaveData)
|
||||
@@ -687,9 +723,11 @@ if(BUILD_TESTING)
|
||||
|
||||
add_test_executable(guest_vorbis_dec_tests tests/GuestVorbisDec.cpp)
|
||||
target_include_directories(guest_vorbis_dec_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(guest_vorbis_dec_tests PRIVATE libSceVorbisDec libc)
|
||||
target_compile_definitions(guest_vorbis_dec_tests PRIVATE OV_EXCLUDE_STATIC_CALLBACKS)
|
||||
target_link_libraries(guest_vorbis_dec_tests PRIVATE libSceVorbisDec vorbisenc vorbisfile vorbis ogg libc)
|
||||
configure_windows_unwind(guest_vorbis_dec_tests)
|
||||
add_test(NAME guest_vorbis_dec COMMAND guest_vorbis_dec_tests)
|
||||
set_tests_properties(guest_vorbis_dec PROPERTIES TIMEOUT 30)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_vorbis_dec PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceVorbisDec>")
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <stdexcept>
|
||||
@@ -78,16 +79,16 @@ void MoveImportFirst(std::vector<unsigned char>& bytes, const std::string& dll)
|
||||
for (std::size_t index = 0; index < descriptors.size(); ++index) std::memcpy(bytes.data() + directory + index * DescriptorBytes, descriptors[index].data(), DescriptorBytes);
|
||||
}
|
||||
|
||||
void MoveImportFirstInFile(const std::string& path, const std::string& dll) {
|
||||
void MoveImportFirstInFile(const std::filesystem::path& path, const std::string& dll) {
|
||||
std::vector<unsigned char> bytes;
|
||||
{
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
if (!input) throw std::runtime_error("cannot open " + path);
|
||||
if (!input) throw std::runtime_error("cannot open " + path.string());
|
||||
bytes.assign(std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>());
|
||||
}
|
||||
MoveImportFirst(bytes, dll);
|
||||
std::ofstream output(path, std::ios::binary | std::ios::trunc);
|
||||
if (!output || !output.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()))) throw std::runtime_error("cannot write " + path);
|
||||
if (!output || !output.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()))) throw std::runtime_error("cannot write " + path.string());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
#ifndef CORE_LIBS_ALLOCATOR_PEIMPORTFIRST_HPP
|
||||
#define CORE_LIBS_ALLOCATOR_PEIMPORTFIRST_HPP
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Allocator {
|
||||
|
||||
void MoveImportFirst(std::vector<unsigned char>& image, const std::string& dll);
|
||||
void MoveImportFirstInFile(const std::string& path, const std::string& dll);
|
||||
void MoveImportFirstInFile(const std::filesystem::path& path, const std::string& dll);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -2,17 +2,37 @@
|
||||
|
||||
#include <cstdio>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc != 3) {
|
||||
std::fprintf(stderr, "usage: pe_import_first <file> <dll>\n");
|
||||
return 2;
|
||||
}
|
||||
namespace {
|
||||
|
||||
int Usage() {
|
||||
std::fprintf(stderr, "usage: pe_import_first <file> <dll>\n");
|
||||
return 2;
|
||||
}
|
||||
|
||||
int Run(const std::filesystem::path& file, const std::string& dll) {
|
||||
try {
|
||||
Allocator::MoveImportFirstInFile(argv[1], argv[2]);
|
||||
Allocator::MoveImportFirstInFile(file, dll);
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "pe_import_first: %s: %s\n", argv[1], error.what());
|
||||
std::fprintf(stderr, "pe_import_first: %s: %s\n", file.string().c_str(), error.what());
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
int wmain(int argc, wchar_t** argv) {
|
||||
if (argc != 3) return Usage();
|
||||
const auto dll = std::filesystem::path(argv[2]).u8string();
|
||||
return Run(argv[1], std::string(dll.begin(), dll.end()));
|
||||
}
|
||||
#else
|
||||
int main(int argc, char** argv) {
|
||||
if (argc != 3) return Usage();
|
||||
return Run(argv[1], argv[2]);
|
||||
}
|
||||
#endif
|
||||
@@ -113,7 +113,7 @@ void movesTheNamedImportFirst() {
|
||||
void movesTheImportInAFile(const std::filesystem::path& directory) {
|
||||
const auto path = directory / "moved.dll";
|
||||
writeFile(path, image({"kernel32.dll", "mimalloc-redirect.dll"}));
|
||||
Allocator::MoveImportFirstInFile(path.string(), "mimalloc-redirect.dll");
|
||||
Allocator::MoveImportFirstInFile(path, "mimalloc-redirect.dll");
|
||||
const auto moved = readFile(path);
|
||||
require(importName(moved, 0) == "mimalloc-redirect.dll" && importName(moved, 1) == "kernel32.dll", "the file's import did not move first");
|
||||
}
|
||||
@@ -122,7 +122,7 @@ void failsWithoutTouchingTheFile(const std::filesystem::path& directory, const s
|
||||
const auto path = directory / (what + ".dll");
|
||||
writeFile(path, bytes);
|
||||
try {
|
||||
Allocator::MoveImportFirstInFile(path.string(), "mimalloc-redirect.dll");
|
||||
Allocator::MoveImportFirstInFile(path, "mimalloc-redirect.dll");
|
||||
} catch (const std::exception& error) {
|
||||
require(std::string(error.what()).find(expected) != std::string::npos, what + ": failed with another error: " + error.what());
|
||||
require(readFile(path) == bytes, what + ": the file changed although the move failed");
|
||||
@@ -134,7 +134,8 @@ void failsWithoutTouchingTheFile(const std::filesystem::path& directory, const s
|
||||
}
|
||||
|
||||
int main() {
|
||||
const auto directory = std::filesystem::temp_directory_path() / ("aps5-pe-import-first-" + std::to_string(std::random_device{}()));
|
||||
auto directory = std::filesystem::temp_directory_path() / std::filesystem::path(u8"aps5-pe-import-first-a\u00e7\u00e3o-\u65e5\u672c-");
|
||||
directory += std::to_string(std::random_device{}());
|
||||
try {
|
||||
std::filesystem::create_directories(directory);
|
||||
movesTheNamedImportFirst();
|
||||
|
||||
@@ -267,6 +267,19 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_instrumentation_query_size_tests EXCLUDE_FROM_ALL tests/InstrumentationQuerySize.cpp)
|
||||
target_include_directories(agc_instrumentation_query_size_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_instrumentation_query_size_tests PRIVATE libSceAgc libc)
|
||||
configure_windows_unwind(agc_instrumentation_query_size_tests)
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_instrumentation_query_size_tests PROPERTIES EXCLUDE_FROM_ALL FALSE)
|
||||
add_test(NAME agc_instrumentation_query_size_tests COMMAND agc_instrumentation_query_size_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_instrumentation_query_size_tests PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_context_state_size_tests EXCLUDE_FROM_ALL tests/ContextStateSize.cpp)
|
||||
target_include_directories(agc_context_state_size_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_context_state_size_tests PRIVATE libSceAgc libc)
|
||||
|
||||
@@ -22,13 +22,12 @@ std::uint32_t* APS5_VABI sceAgcDcbGetLodStats(CommandBuffer* buf, std::uint8_t c
|
||||
}
|
||||
|
||||
std::uint32_t APS5_VABI sceAgcDcbBeginOcclusionQueryGetSize(std::int32_t queryType) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
if (queryType == 1) return 0x120u;
|
||||
return queryType == 0 ? 0x10u : 0u;
|
||||
}
|
||||
|
||||
std::uint32_t APS5_VABI sceAgcDcbEndOcclusionQueryGetSize() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
return 0x10u;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +1,16 @@
|
||||
#include "prx/libSceAgc/Misc/include/Debug.hpp"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include "SceTypes.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
std::atomic<std::uint32_t> shaderInstrumentation{0};
|
||||
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
int APS5_VABI sceAgcDebugRaiseException(std::uint32_t exceptionId) {
|
||||
@@ -11,4 +18,13 @@ int APS5_VABI sceAgcDebugRaiseException(std::uint32_t exceptionId) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcSetShaderInstrumentation(std::uint32_t flags) {
|
||||
shaderInstrumentation.store(flags, std::memory_order_relaxed);
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::uint32_t APS5_VABI sceAgcGetShaderInstrumentation() {
|
||||
return shaderInstrumentation.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -53,12 +53,9 @@ std::uint32_t CreateInterpolantValueSplitF16(std::uint32_t psWord, const ShaderS
|
||||
: CreateInterpolantValue(psWord, gsSemantic);
|
||||
}
|
||||
|
||||
std::uint32_t UpdateInterpolantValue(std::uint32_t psWord, const ShaderSemantic* gsSemantic) {
|
||||
const std::uint32_t f16 = (psWord >> 20u) & 0x3u;
|
||||
if (f16 != 0 && f16 != InterpolantF16HiOnly && ((psWord >> 28u) & 0x3u) != ((psWord >> 30u) & 0x3u)) {
|
||||
throw std::runtime_error(std::string(__func__) + ": DEFAULT_VAL of an f16 input with different low and high defaults is unknown");
|
||||
}
|
||||
return CreateInterpolantValueSplitF16(psWord, gsSemantic);
|
||||
std::uint32_t CreateInterpolantValueFlatDefault(std::uint32_t psWord, const ShaderSemantic* gsSemantic) {
|
||||
const std::uint32_t value = CreateInterpolantValue(psWord, gsSemantic);
|
||||
return ((psWord & 0x00700000u) == 0x00400000u) ? (value | 0x00000020u) : value;
|
||||
}
|
||||
|
||||
int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* gs, const Shader* ps, CreateInterpolantValueFn createValue, bool fillIdentityTail) {
|
||||
@@ -132,7 +129,15 @@ int APS5_VABI sceAgcCreateInterpolantMapping_10000(ShaderRegister* regs, const S
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcUpdateInterpolantMapping_10000(ShaderRegister* regs, const Shader* gs, const Shader* ps) {
|
||||
return CreateInterpolantMapping(__func__, regs, gs, ps, UpdateInterpolantValue, false);
|
||||
return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValueSplitF16, false);
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcCreateInterpolantMapping_nid_postfix(ShaderRegister* regs, const Shader* gs, const Shader* ps) {
|
||||
return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValueFlatDefault, true);
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcUpdateInterpolantMapping_nid_postfix(ShaderRegister* regs, const Shader* gs, const Shader* ps) {
|
||||
return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValueFlatDefault, false);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -40,14 +40,4 @@ std::uint32_t* APS5_VABI sceAgcCbMemsetExclusive(CommandBuffer* buffer, void* de
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcSetShaderInstrumentation(std::uint32_t flags) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcGetShaderInstrumentation() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#include "prx/libc/include/general/VabiMacros.hpp"
|
||||
#include "prx/libc/include/Shutdown.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <stdexcept>
|
||||
|
||||
extern "C" {
|
||||
int APS5_VABI sceAgcSetShaderInstrumentation(std::uint32_t);
|
||||
std::uint32_t APS5_VABI sceAgcGetShaderInstrumentation();
|
||||
std::uint32_t APS5_VABI sceAgcDcbBeginOcclusionQueryGetSize(std::int32_t);
|
||||
std::uint32_t APS5_VABI sceAgcDcbEndOcclusionQueryGetSize();
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
void check(bool condition, const char* message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
void testShaderInstrumentation() {
|
||||
check(sceAgcGetShaderInstrumentation() == 0, "shader instrumentation starts cleared");
|
||||
check(sceAgcSetShaderInstrumentation(0x80000005u) == 0, "setting shader instrumentation failed");
|
||||
check(sceAgcGetShaderInstrumentation() == 0x80000005u, "shader instrumentation flags were not kept");
|
||||
check(sceAgcSetShaderInstrumentation(0) == 0 && sceAgcGetShaderInstrumentation() == 0, "shader instrumentation was not cleared");
|
||||
}
|
||||
|
||||
void testOcclusionQuerySizes() {
|
||||
check(sceAgcDcbBeginOcclusionQueryGetSize(0) == 0x10u, "begin occlusion query size for type 0");
|
||||
check(sceAgcDcbBeginOcclusionQueryGetSize(1) == 0x120u, "begin occlusion query size for type 1");
|
||||
check(sceAgcDcbBeginOcclusionQueryGetSize(2) == 0, "begin occlusion query size for an unknown type");
|
||||
check(sceAgcDcbEndOcclusionQueryGetSize() == 0x10u, "end occlusion query size");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
testShaderInstrumentation();
|
||||
testOcclusionQuerySizes();
|
||||
LibcRunShutdown_nid_postfix();
|
||||
std::puts("AGC shader instrumentation and occlusion query size tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "%s\n", error.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,8 @@
|
||||
extern "C" int APS5_VABI sceAgcCreateInterpolantMapping_0100(ShaderRegister* regs, const Shader* gs, const Shader* ps);
|
||||
extern "C" int APS5_VABI sceAgcCreateInterpolantMapping_10000(ShaderRegister* regs, const Shader* gs, const Shader* ps);
|
||||
extern "C" int APS5_VABI sceAgcUpdateInterpolantMapping_10000(ShaderRegister* regs, const Shader* gs, const Shader* ps);
|
||||
extern "C" int APS5_VABI sceAgcCreateInterpolantMapping_nid_postfix(ShaderRegister* regs, const Shader* gs, const Shader* ps);
|
||||
extern "C" int APS5_VABI sceAgcUpdateInterpolantMapping_nid_postfix(ShaderRegister* regs, const Shader* gs, const Shader* ps);
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -158,13 +160,46 @@ void testUpdateMapping() {
|
||||
check(std::memcmp(create.data(), update.data(), written) == 0, "update differs from the split f16 mapping");
|
||||
checkIdentity(create, static_cast<std::uint32_t>(inputs.size()), "unused interpolants are not the identity");
|
||||
|
||||
for (const std::uint32_t index : {1u, 3u}) {
|
||||
inputs[index].default_value = 0;
|
||||
update = filled();
|
||||
expectFailure([&] { sceAgcUpdateInterpolantMapping_10000(update.data(), &gs, &ps); });
|
||||
check(std::memcmp(update.data(), untouched.data(), sizeof(update)) == 0, "rejected update wrote registers");
|
||||
inputs[index].default_value = inputs[index].default_value_hi;
|
||||
inputs[1].default_value = 0;
|
||||
inputs[3].default_value = 0;
|
||||
update = filled();
|
||||
check(sceAgcUpdateInterpolantMapping_10000(update.data(), &gs, &ps) == 0, "update with split f16 defaults failed");
|
||||
check(update[1].value == 0x01380007u && update[3].value == 0x03680409u, "f16 DEFAULT_VAL does not come from the low default");
|
||||
}
|
||||
|
||||
void testUnversionedMapping() {
|
||||
std::array<ShaderSemantic, 5> inputs{
|
||||
semantic(1, 0, 2), semantic(2, 0, 2), semantic(5, 0, 2), semantic(3, 0, 0), semantic(4, 0, 3)};
|
||||
inputs[0].is_flat_shaded = 1;
|
||||
inputs[0].default_value = 1;
|
||||
inputs[0].default_value_hi = 3;
|
||||
inputs[1].default_value_hi = 1;
|
||||
inputs[2].is_flat_shaded = 1;
|
||||
inputs[2].default_value_hi = 2;
|
||||
inputs[3].is_flat_shaded = 1;
|
||||
inputs[3].default_value = 2;
|
||||
std::array<ShaderSemantic, 4> outputs{semantic(1, 7, 3), semantic(2, 4, 1), semantic(3, 9, 0), semantic(4, 2, 1)};
|
||||
Shader gs{};
|
||||
gs.output_semantics = outputs.data();
|
||||
gs.num_output_semantics = static_cast<std::uint16_t>(outputs.size());
|
||||
Shader ps{};
|
||||
ps.input_semantics = inputs.data();
|
||||
ps.num_input_semantics = static_cast<std::uint32_t>(inputs.size());
|
||||
const std::array<std::uint32_t, 5> flatDefault{0x02680527u, 0x02380024u, 0x02580020u, 0x00000629u, 0x03180002u};
|
||||
const std::size_t written = inputs.size() * sizeof(ShaderRegister);
|
||||
const auto untouched = filled();
|
||||
|
||||
auto create = filled();
|
||||
check(sceAgcCreateInterpolantMapping_nid_postfix(create.data(), &gs, &ps) == 0, "unversioned mapping failed");
|
||||
for (std::uint32_t i = 0; i < inputs.size(); ++i) {
|
||||
check(create[i].offset == ShaderRegs::SPI_PS_INPUT_CNTL_0 + i && create[i].value == flatDefault[i], "unversioned mapping changed");
|
||||
}
|
||||
checkIdentity(create, static_cast<std::uint32_t>(inputs.size()), "unused interpolants are not the identity");
|
||||
|
||||
auto update = filled();
|
||||
check(sceAgcUpdateInterpolantMapping_nid_postfix(update.data(), &gs, &ps) == 0, "unversioned update failed");
|
||||
check(std::memcmp(update.data(), create.data(), written) == 0, "unversioned update differs from its mapping");
|
||||
check(std::memcmp(update.data() + inputs.size(), untouched.data(), sizeof(update) - written) == 0, "unversioned update wrote past its inputs");
|
||||
}
|
||||
|
||||
void testRejections() {
|
||||
@@ -208,8 +243,9 @@ int main() {
|
||||
testIdentity();
|
||||
testMapping();
|
||||
testUpdateMapping();
|
||||
testUnversionedMapping();
|
||||
testRejections();
|
||||
check(mappings == 7u && links == 5u, "mapping preparation call count changed");
|
||||
check(mappings == 10u && links == 8u, "mapping preparation call count changed");
|
||||
LibcRunShutdown_nid_postfix();
|
||||
std::puts("AGC interpolant mapping tests passed");
|
||||
return 0;
|
||||
|
||||
@@ -433,6 +433,16 @@ if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
endif()
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_test_executable(agc_driver_unsupported_diagnostics_tests tests/UnsupportedDiagnostics.cpp)
|
||||
target_include_directories(agc_driver_unsupported_diagnostics_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_driver_unsupported_diagnostics_tests PRIVATE libSceAgcDriver)
|
||||
configure_windows_unwind(agc_driver_unsupported_diagnostics_tests)
|
||||
add_test(NAME agc_driver_unsupported_diagnostics COMMAND agc_driver_unsupported_diagnostics_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_unsupported_diagnostics PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceAgcDriver>")
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_driver_draw_program_trace_tests
|
||||
tests/DrawProgramTrace.cpp
|
||||
Execution/src/Driver/Draw/DrawProgramTrace.cpp
|
||||
@@ -459,6 +469,16 @@ if(WIN32)
|
||||
endif()
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_test_executable(agc_driver_nop_data_packet_tests tests/NopDataWritePacket.cpp)
|
||||
target_include_directories(agc_driver_nop_data_packet_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_driver_nop_data_packet_tests PRIVATE libSceAgcDriver)
|
||||
configure_windows_unwind(agc_driver_nop_data_packet_tests)
|
||||
add_test(NAME agc_driver_nop_data_packet COMMAND agc_driver_nop_data_packet_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_nop_data_packet PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_frame_dump_io_tests tests/FrameDumpIo.cpp)
|
||||
target_include_directories(agc_frame_dump_io_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_frame_dump_io_tests PRIVATE libc)
|
||||
@@ -1653,9 +1673,17 @@ 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:")
|
||||
@@ -2043,13 +2071,23 @@ 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()
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_test(NAME agc_driver_native_fmask COMMAND agc_driver_sample_multisample_target_tests --native-fmask)
|
||||
set_tests_properties(agc_driver_native_fmask PROPERTIES TIMEOUT 30 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_native_fmask PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_driver_native_msaa_vertex_input COMMAND agc_driver_sample_multisample_target_tests --native-vertex-input)
|
||||
set_tests_properties(agc_driver_native_msaa_vertex_input PROPERTIES
|
||||
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
@@ -2827,15 +2865,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}")
|
||||
@@ -3365,6 +3404,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);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
@@ -273,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;
|
||||
@@ -357,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));
|
||||
|
||||
@@ -263,7 +263,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) {
|
||||
if (x.binary.stage != y.binary.stage || x.binary.codeAddress != y.binary.codeAddress || x.userDataBase != y.userDataBase || x.firstUserSgpr != y.firstUserSgpr || x.userData != y.userData || x.snapshot != y.snapshot || x.codeOffset != y.codeOffset || x.tailContinuation != y.tailContinuation) return false;
|
||||
}
|
||||
if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.reversedBlend != q.reversedBlend || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive || p.ditheredAlphaToCoverage != q.ditheredAlphaToCoverage || p.quadPixelMask != q.quadPixelMask || p.quadSampleMask != q.quadSampleMask || p.iteratesSamples != q.iteratesSamples || p.positionLocation != q.positionLocation || p.sampleLocations != q.sampleLocations || p.sampleOrder != q.sampleOrder) return false;
|
||||
if (s.samples != t.samples || s.mergedCoverage != t.mergedCoverage || s.quadSampleMask != t.quadSampleMask || s.pixelEarlyZ != t.pixelEarlyZ) return false;
|
||||
if (s.samples != t.samples || s.mergedCoverage != t.mergedCoverage || s.truncatedCoverage != t.truncatedCoverage || s.quadSampleMask != t.quadSampleMask || s.pixelEarlyZ != t.pixelEarlyZ) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -971,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;
|
||||
@@ -1025,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);
|
||||
@@ -1039,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) {
|
||||
@@ -1167,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;
|
||||
}
|
||||
@@ -1671,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;
|
||||
};
|
||||
@@ -1709,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();
|
||||
@@ -1717,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;
|
||||
}
|
||||
{
|
||||
@@ -1725,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();
|
||||
@@ -1743,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);
|
||||
|
||||
@@ -16,8 +16,12 @@ namespace {
|
||||
std::atomic<bool> requested{false};
|
||||
|
||||
std::filesystem::path directory() {
|
||||
#ifdef _WIN32
|
||||
const wchar_t* configured = _wgetenv(L"ANYPS5_SCREENSHOTS");
|
||||
#else
|
||||
const char* configured = std::getenv("ANYPS5_SCREENSHOTS");
|
||||
if (configured != nullptr && configured[0] != '\0') return configured;
|
||||
#endif
|
||||
if (configured != nullptr && configured[0] != 0) return configured;
|
||||
const std::unique_ptr<char, decltype(&SDL_free)> basePath(SDL_GetBasePath(), SDL_free);
|
||||
if (!basePath) throw std::runtime_error(std::string("Screenshot: cannot locate the executable: ") + SDL_GetError());
|
||||
return std::filesystem::path(reinterpret_cast<const char8_t*>(basePath.get())) / "anyps5-screenshots";
|
||||
|
||||
@@ -156,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() {
|
||||
@@ -202,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) {
|
||||
@@ -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;
|
||||
@@ -382,6 +386,8 @@ struct VulkanDevice::State {
|
||||
bool samplerFilterMinmax = false;
|
||||
bool nonSeamlessCubeMap = false;
|
||||
VkSampleCountFlags mergedCoverageSampleCounts = 0;
|
||||
VkSampleCountFlags truncatedCoverageSampleCounts = 0;
|
||||
bool mixedAttachmentSamples = false;
|
||||
bool fragmentShaderPixelInterlock = false;
|
||||
bool conservativeRasterization = false;
|
||||
bool provokingVertexLast = false;
|
||||
@@ -1313,6 +1319,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);
|
||||
@@ -1507,12 +1517,17 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
|
||||
check(combinations(selected, &count, supported.data()), "vkGetPhysicalDeviceSupportedFramebufferMixedSamplesCombinationsNV");
|
||||
for (const auto& combination : supported) {
|
||||
if (combination.coverageReductionMode == VK_COVERAGE_REDUCTION_MODE_MERGE_NV && combination.colorSamples == VK_SAMPLE_COUNT_1_BIT && combination.depthStencilSamples == 0) state->mergedCoverageSampleCounts |= combination.rasterizationSamples;
|
||||
if (combination.coverageReductionMode == VK_COVERAGE_REDUCTION_MODE_TRUNCATE_NV && combination.colorSamples == VK_SAMPLE_COUNT_1_BIT && combination.depthStencilSamples == combination.rasterizationSamples) state->truncatedCoverageSampleCounts |= combination.rasterizationSamples;
|
||||
}
|
||||
}
|
||||
if (state->mergedCoverageSampleCounts != 0) {
|
||||
if (state->mergedCoverageSampleCounts != 0 || state->truncatedCoverageSampleCounts != 0) {
|
||||
deviceExtensions.push_back(VK_NV_FRAMEBUFFER_MIXED_SAMPLES_EXTENSION_NAME);
|
||||
deviceExtensions.push_back(VK_NV_COVERAGE_REDUCTION_MODE_EXTENSION_NAME);
|
||||
}
|
||||
} else if (hasExtension(VK_AMD_MIXED_ATTACHMENT_SAMPLES_EXTENSION_NAME)) {
|
||||
state->mixedAttachmentSamples = true;
|
||||
state->truncatedCoverageSampleCounts = state->properties.limits.framebufferDepthSampleCounts & state->properties.limits.framebufferColorSampleCounts & ~static_cast<VkSampleCountFlags>(VK_SAMPLE_COUNT_1_BIT);
|
||||
deviceExtensions.push_back(VK_AMD_MIXED_ATTACHMENT_SAMPLES_EXTENSION_NAME);
|
||||
}
|
||||
coverageReductionFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_COVERAGE_REDUCTION_MODE_FEATURES_NV};
|
||||
coverageReductionFeatures.coverageReductionMode = VK_TRUE;
|
||||
@@ -1803,7 +1818,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
|
||||
nonSeamlessCubeFeatures.pNext = const_cast<void*>(deviceInfo.pNext);
|
||||
deviceInfo.pNext = &nonSeamlessCubeFeatures;
|
||||
}
|
||||
if (state->mergedCoverageSampleCounts != 0) {
|
||||
if (!state->mixedAttachmentSamples && (state->mergedCoverageSampleCounts != 0 || state->truncatedCoverageSampleCounts != 0)) {
|
||||
coverageReductionFeatures.pNext = const_cast<void*>(deviceInfo.pNext);
|
||||
deviceInfo.pNext = &coverageReductionFeatures;
|
||||
}
|
||||
@@ -3806,6 +3821,8 @@ Graphics::Context VulkanDevice::buildContext() const {
|
||||
context.samplerFilterMinmax = state->samplerFilterMinmax;
|
||||
context.nonSeamlessCubeMap = state->nonSeamlessCubeMap;
|
||||
context.mergedCoverageSampleCounts = state->mergedCoverageSampleCounts;
|
||||
context.truncatedCoverageSampleCounts = state->truncatedCoverageSampleCounts;
|
||||
context.mixedAttachmentSamples = state->mixedAttachmentSamples;
|
||||
context.conservativeRasterization = state->conservativeRasterization;
|
||||
context.meshWave32 = state->meshWave32;
|
||||
context.provokingVertexLast = state->provokingVertexLast;
|
||||
@@ -3842,6 +3859,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;
|
||||
@@ -4403,6 +4421,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;
|
||||
@@ -4663,8 +4682,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) {
|
||||
@@ -4925,21 +4944,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);
|
||||
@@ -5070,7 +5099,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;
|
||||
@@ -5322,7 +5351,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;
|
||||
|
||||
@@ -213,6 +213,8 @@ struct Context {
|
||||
bool samplerFilterMinmax = false;
|
||||
bool nonSeamlessCubeMap = false;
|
||||
VkSampleCountFlags mergedCoverageSampleCounts = 0;
|
||||
VkSampleCountFlags truncatedCoverageSampleCounts = 0;
|
||||
bool mixedAttachmentSamples = false;
|
||||
bool conservativeRasterization = false;
|
||||
bool meshWave32 = false;
|
||||
VkBuffer emptyBuffer = VK_NULL_HANDLE;
|
||||
@@ -242,6 +244,7 @@ struct Context {
|
||||
bool maintenance5 = false;
|
||||
bool storageImageMultisample = false;
|
||||
bool storageImageWriteWithoutFormat = false;
|
||||
bool imageFormatList = false;
|
||||
bool shaderClipDistance = false;
|
||||
bool shaderCullDistance = false;
|
||||
bool nullDescriptors = false;
|
||||
|
||||
@@ -29,6 +29,7 @@ struct DepthPlaneStoreStatistics {
|
||||
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();
|
||||
@@ -54,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;
|
||||
|
||||
@@ -90,6 +90,7 @@ struct GuestTextureResource {
|
||||
std::uint32_t minLod = 0;
|
||||
std::uint32_t allocatedMipCount = 0;
|
||||
std::uint32_t samples = 1;
|
||||
std::uint64_t fmaskCmaskAddress = 0;
|
||||
};
|
||||
|
||||
float EffectiveMinLod(const GuestTextureResource& resource);
|
||||
|
||||
@@ -76,6 +76,7 @@ bool MultisampleTargetStorable(const Context& context, const ColorTarget& target
|
||||
void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView);
|
||||
bool MultisampledColorFillAbsorbs(std::uint64_t address, std::size_t bytes, std::uint32_t pattern);
|
||||
void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern);
|
||||
std::shared_ptr<Texture> MultisampleTargetFmaskTexture_nid_no_patch(const Context& context, const GuestTextureResource& resource, VkComponentMapping components);
|
||||
void ClearMultisampleTargets(VkDevice device);
|
||||
void InstallResidentImageReadGuard_nid_no_patch();
|
||||
void UpdateResidentImageUnmapObserver_nid_no_patch();
|
||||
|
||||
@@ -68,6 +68,8 @@ struct ColorTarget {
|
||||
std::uint32_t pipeBankXor = 0;
|
||||
std::uint32_t samples = 1;
|
||||
std::uint32_t dccControl = 0;
|
||||
std::uint64_t fmaskAddress = 0;
|
||||
std::uint32_t fmaskPipeBankXor = 0;
|
||||
};
|
||||
|
||||
struct DepthTarget {
|
||||
@@ -140,6 +142,7 @@ struct State {
|
||||
bool ditheredAlphaToCoverage = false;
|
||||
bool perSampleShading = false;
|
||||
bool mergedCoverage = false;
|
||||
bool truncatedCoverage = false;
|
||||
VkConservativeRasterizationModeEXT conservativeRasterization = VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT;
|
||||
VkRect2D scissor;
|
||||
VkCullModeFlags cullMode;
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -58,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() {
|
||||
@@ -338,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");
|
||||
@@ -491,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);
|
||||
@@ -540,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 {
|
||||
@@ -690,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;
|
||||
@@ -707,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;
|
||||
@@ -910,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;
|
||||
@@ -1527,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;
|
||||
@@ -1826,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() {
|
||||
@@ -1900,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;
|
||||
@@ -1936,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;
|
||||
@@ -2184,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;
|
||||
}
|
||||
@@ -2204,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) {
|
||||
@@ -2231,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;
|
||||
@@ -2293,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;
|
||||
@@ -2454,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()) {
|
||||
|
||||
@@ -55,6 +55,7 @@ std::uint64_t DrawRenderPassKey(const Context& context, const State& state, std:
|
||||
mix(state.renderExtent.height);
|
||||
mix(state.samples);
|
||||
mix(state.mergedCoverage);
|
||||
mix(state.truncatedCoverage);
|
||||
mix(state.customSampleLocations);
|
||||
if (state.customSampleLocations) {
|
||||
mix(state.sampleLocationGrid);
|
||||
@@ -81,6 +82,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 +1048,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 +1139,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 +1205,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 +1499,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 +2156,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 +2238,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];
|
||||
@@ -2317,12 +2360,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 +2409,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 +2432,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 +2453,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 +2478,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 +2495,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 +2504,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 +2517,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 +2533,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 +2543,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 +2567,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 +2935,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 +2966,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");
|
||||
@@ -2899,6 +2999,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);
|
||||
}
|
||||
|
||||
@@ -4679,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) {
|
||||
@@ -5173,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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5261,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) {
|
||||
@@ -5291,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;
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp"
|
||||
#include "RdnaDecoder/RdnaDescriptorFormat.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
@@ -50,6 +52,45 @@ TextureDimension resolveDimension(std::uint32_t raw) {
|
||||
}
|
||||
}
|
||||
|
||||
bool ZeroTail(std::span<const std::uint32_t> words) {
|
||||
return words[4] == 0u && words[5] == 0u && words[6] == 0u && words[7] == 0u;
|
||||
}
|
||||
|
||||
bool SameLevels(const TileMipLayout& a, const TileMipLayout& b) {
|
||||
return a.tiledOffset == b.tiledOffset && a.tiledSize == b.tiledSize && a.width == b.width && a.height == b.height && a.blocksPerRow == b.blocksPerRow && a.pitchBytes == b.pitchBytes && a.tail == b.tail && a.tailX == b.tailX && a.tailY == b.tailY;
|
||||
}
|
||||
|
||||
std::optional<std::array<std::uint32_t, 8>> R128ResidentLevelWords(std::span<const std::uint32_t> words) {
|
||||
const auto baseLevel = (words[3] >> 12u) & 0xfu;
|
||||
const auto lastLevel = (words[3] >> 16u) & 0xfu;
|
||||
const auto typeRaw = (words[3] >> 28u) & 0xfu;
|
||||
if (baseLevel == 0u || baseLevel > lastLevel || (typeRaw != 8u && typeRaw != 9u)) return std::nullopt;
|
||||
const std::array<std::uint32_t, 8> chainWords{words[0], words[1], words[2], words[3], 0u, lastLevel << 4u, 0u, 0u};
|
||||
const auto width = (((words[1] >> 30u) & 0x3u) | ((words[2] & 0xfffu) << 2u)) + 1u;
|
||||
const auto height = ((words[2] >> 14u) & 0x3fffu) + 1u;
|
||||
const auto minLod = (words[1] >> 8u) & 0xfffu;
|
||||
const auto widthMinus1 = std::max(width >> baseLevel, 1u) - 1u;
|
||||
const auto heightMinus1 = std::max(height >> baseLevel, 1u) - 1u;
|
||||
const auto rebasedMinLod = minLod > baseLevel * 256u ? minLod - baseLevel * 256u : 0u;
|
||||
const auto levels = lastLevel - baseLevel;
|
||||
auto rebased = chainWords;
|
||||
rebased[1] = (words[1] & ~((0xfffu << 8u) | (0x3u << 30u))) | (rebasedMinLod << 8u) | ((widthMinus1 & 0x3u) << 30u);
|
||||
rebased[2] = (words[2] & ~(0xfffu | (0x3fffu << 14u))) | ((widthMinus1 >> 2u) & 0xfffu) | ((heightMinus1 & 0x3fffu) << 14u);
|
||||
rebased[3] = (words[3] & ~(0xffu << 12u)) | (levels << 16u);
|
||||
rebased[5] = levels << 4u;
|
||||
try {
|
||||
const auto chain = DescribeSurface(DecodeTextureResource(chainWords));
|
||||
const auto view = DescribeSurface(DecodeTextureResource(rebased));
|
||||
if (chain.mips.size() != lastLevel + 1u || view.mips.size() != levels + 1u || chain.layers != view.layers) return std::nullopt;
|
||||
for (std::uint32_t level = baseLevel; level <= lastLevel; ++level) {
|
||||
if (!SameLevels(chain.mips[level], view.mips[level - baseLevel])) return std::nullopt;
|
||||
}
|
||||
} catch (const std::exception&) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return rebased;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool IsNullTextureResource(std::span<const std::uint32_t> words) {
|
||||
@@ -140,7 +181,8 @@ GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words)
|
||||
const bool fmask = ShaderRecompiler::IsFmaskTextureFormat(static_cast<ShaderRecompiler::IrBufferFormat>(format));
|
||||
if (fmask) {
|
||||
Require(format == static_cast<std::uint32_t>(ShaderRecompiler::IrBufferFormat::Fmask32_S8_F8), "guest FMASK format is not implemented");
|
||||
Require(!metaCompress && !writeCompress, "FMASK CMASK compression is not implemented");
|
||||
Require(!writeCompress && (!metaCompress || metaAddr != 0u), "FMASK CMASK compression is not implemented without readable metadata");
|
||||
Require(!metaCompress || metaPipeAligned, "FMASK CMASK metadata must be pipe aligned");
|
||||
Require((typeRaw == 9u || typeRaw == 13u) && tileModeRaw == 24u && baseLevel == 0u && lastLevel == 0u && maxMip == 0u, "FMASK reads require a single-mip 2D or 2D-array Z_64KB_X surface");
|
||||
for (const auto selector : {dstSelX, dstSelY, dstSelZ, dstSelW}) Require(selector == 0u || selector == 1u || selector == 4u, "FMASK destination channel selector is not implemented");
|
||||
}
|
||||
@@ -229,14 +271,19 @@ GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words)
|
||||
result.dstSelZ = static_cast<std::uint8_t>(dstSelZ);
|
||||
result.dstSelW = static_cast<std::uint8_t>(dstSelW);
|
||||
result.bcSwizzle = static_cast<std::uint8_t>(bcSwizzle);
|
||||
result.dccAddress = metaCompress ? metaAddr << 8u : 0u;
|
||||
result.dccAddress = metaCompress && !fmask ? metaAddr << 8u : 0u;
|
||||
result.fmaskCmaskAddress = metaCompress && fmask ? metaAddr << 8u : 0u;
|
||||
result.dccAlphaOnMsb = dccAlphaPos;
|
||||
result.dccPipeAligned = metaPipeAligned;
|
||||
result.iterate256 = iterate256;
|
||||
result.minLod = minLod;
|
||||
result.samples = samples;
|
||||
if (baseLevel > maxMip) {
|
||||
Require(LevelsFitAllocation(result, lastLevel + 1u), "guest texture descriptor starts past the surface's last mip level at levels that would move the surface's own" + describe());
|
||||
const bool fits = LevelsFitAllocation(result, lastLevel + 1u);
|
||||
if (!fits && ZeroTail(words)) {
|
||||
if (const auto resident = R128ResidentLevelWords(words)) return DecodeTextureResource(*resident);
|
||||
}
|
||||
Require(fits, "guest texture descriptor starts past the surface's last mip level at levels that would move the surface's own" + describe());
|
||||
result.mipCount = lastLevel + 1u;
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp"
|
||||
#include "IntermediateRepresentation/IrMetadata/BufferFormat.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include <algorithm>
|
||||
@@ -45,10 +47,57 @@ std::uint64_t collectBlocks(std::uint64_t address, std::size_t bytes) {
|
||||
return generation != 0 ? generation : GuestMemory::CollectWritesUncached(address, bytes);
|
||||
}
|
||||
|
||||
GuestTextureResource fmaskSnapshotResource(const GuestTextureResource& resource) {
|
||||
Require(resource.format == static_cast<std::uint32_t>(ShaderRecompiler::IrBufferFormat::Format32UInt) && resource.fmaskCmaskAddress != 0u, "FMASK image format read semantics are not implemented");
|
||||
Require(resource.dimension == TextureDimension::k2D || resource.dimension == TextureDimension::k2DArray, "FMASK image dimension is not implemented");
|
||||
Require(resource.tileMode == TextureTileMode::kZ64KBX && (resource.pipeBankXor & ~0xcf00u) == 0u, "FMASK image swizzle or pipe/bank XOR is not implemented");
|
||||
Require(resource.width != 0u && resource.width <= 16384u && resource.height != 0u && resource.height <= 16384u && resource.depthOrLastArray < 8192u && resource.baseArray <= resource.depthOrLastArray, "invalid FMASK image extent or layer range");
|
||||
Require(resource.dimension == TextureDimension::k2DArray || (resource.depthOrLastArray == 0u && resource.baseArray == 0u), "FMASK 2D image has an array range");
|
||||
Require(resource.mipCount == 1u && resource.allocatedMipCount == 1u && resource.baseLevel == 0u && resource.lastLevel == 0u && resource.minLod == 0u, "FMASK image mip fields are not implemented");
|
||||
Require(resource.fmaskCmaskAddress == 0u || resource.dccPipeAligned, "FMASK image CMASK must be pipe aligned");
|
||||
for (const auto selector : {resource.dstSelX, resource.dstSelY, resource.dstSelZ, resource.dstSelW}) Require(selector == 0u || selector == 1u || selector == 4u, "FMASK image selects an unverified channel");
|
||||
auto snapshot = resource;
|
||||
snapshot.format = static_cast<std::uint32_t>(ShaderRecompiler::IrBufferFormat::Format32UInt);
|
||||
snapshot.tileMode = TextureTileMode::kLinear;
|
||||
snapshot.pipeBankXor = 0u;
|
||||
snapshot.dccAddress = 0u;
|
||||
snapshot.fmaskCmaskAddress = 0u;
|
||||
snapshot.dccAlphaOnMsb = false;
|
||||
snapshot.dccPipeAligned = false;
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
GuestTextureResource fmaskResource(const ColorTarget& target) {
|
||||
GuestTextureResource resource{};
|
||||
resource.baseAddress = target.fmaskAddress;
|
||||
resource.width = target.extent.width;
|
||||
resource.height = target.extent.height;
|
||||
resource.mipCount = 1u;
|
||||
resource.allocatedMipCount = 1u;
|
||||
resource.tileMode = TextureTileMode::kZ64KBX;
|
||||
resource.dimension = TextureDimension::k2D;
|
||||
resource.format = static_cast<std::uint32_t>(ShaderRecompiler::IrBufferFormat::Format32UInt);
|
||||
resource.dstSelX = 4u;
|
||||
resource.pipeBankXor = target.fmaskPipeBankXor;
|
||||
resource.fmaskCmaskAddress = target.cmaskAddress;
|
||||
resource.dccPipeAligned = true;
|
||||
return resource;
|
||||
}
|
||||
|
||||
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 +150,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 +191,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 +269,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;
|
||||
@@ -212,6 +279,28 @@ public:
|
||||
recorder.Keep(shared_from_this(), static_cast<std::size_t>(storable.bytes));
|
||||
}
|
||||
|
||||
std::shared_ptr<Texture> FmaskSampled(const GuestTextureResource& resource, VkComponentMapping components) {
|
||||
const auto snapshot = fmaskSnapshotResource(resource);
|
||||
Require(target.samples == 8u && target.dccAddress != 0u && fmaskRange.has_value(), "native FMASK sampling requires a tracked eight-fragment DCC target");
|
||||
Require(target.tileMode == ColorTileMode::RenderTarget && target.depth == 1u && target.depthSlice == 0u && target.layers == 1u && target.arrayLastSlice == 0u && target.arraySlice == 0u && target.mip == 0u && target.mipCount == 1u, "native FMASK target layout is not implemented");
|
||||
Require(target.fmaskAddress == resource.baseAddress && target.fmaskPipeBankXor == resource.pipeBankXor && target.cmaskAddress == resource.fmaskCmaskAddress, "native FMASK descriptor does not match its resident target");
|
||||
Require(target.extent.width == resource.width && target.extent.height == resource.height && resource.baseArray == 0u && resource.depthOrLastArray == 0u, "native FMASK extent or subresource does not match its resident target");
|
||||
auto* recorder = Recorder::Active();
|
||||
Require(recorder != nullptr && recorder->Device() == context.device && !Recorder::InCompletion(), "native FMASK sampling requires its device's active recorder outside completion");
|
||||
ApplyFastClear(target);
|
||||
Keep();
|
||||
const std::array<std::uint32_t, 5> key{static_cast<std::uint32_t>(components.r), static_cast<std::uint32_t>(components.g), static_cast<std::uint32_t>(components.b), static_cast<std::uint32_t>(components.a), static_cast<std::uint32_t>(resource.dimension)};
|
||||
auto& texture = fmaskTextures[key];
|
||||
if (texture == nullptr) {
|
||||
const auto geometry = DescribeSurface(snapshot);
|
||||
std::vector<std::byte> bytes(geometry.guestBytes);
|
||||
constexpr std::uint32_t expanded = 0x76543210u;
|
||||
for (std::size_t offset = 0; offset < bytes.size(); offset += sizeof(expanded)) std::memcpy(bytes.data() + offset, &expanded, sizeof(expanded));
|
||||
texture = std::make_unique<Texture>(context, *context.detiler, snapshot, components, bytes);
|
||||
}
|
||||
return {shared_from_this(), texture.get()};
|
||||
}
|
||||
|
||||
void Keep() {
|
||||
if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->Device() == context.device) {
|
||||
recorder->Keep(shared_from_this());
|
||||
@@ -241,6 +330,7 @@ public:
|
||||
colorFill = MultisampledColorFill(use);
|
||||
fromTexture = false;
|
||||
dccRanges.clear();
|
||||
fmaskRange.reset();
|
||||
metadataValid = std::make_shared<std::atomic<bool>>(true);
|
||||
const auto initial = initialDccClear(use);
|
||||
recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) {
|
||||
@@ -336,14 +426,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 +491,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,10 +506,14 @@ 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;
|
||||
VkDeviceSize allocationBytes = 0;
|
||||
std::optional<GuestMemory::UnchangedQuery> fmaskRange;
|
||||
std::map<std::array<std::uint32_t, 5>, std::unique_ptr<Texture>> fmaskTextures;
|
||||
std::map<std::array<std::uint32_t, 5>, std::shared_ptr<Texture>> textures;
|
||||
std::vector<std::shared_ptr<Texture>> retiredTextures;
|
||||
std::vector<GuestMemory::UnchangedQuery> dccRanges;
|
||||
@@ -450,18 +569,29 @@ private:
|
||||
Require(a.address >= b.address + b.bytes || b.address >= a.address + a.bytes, "overlapping multisampled DCC color or metadata ranges are not implemented");
|
||||
}
|
||||
}
|
||||
if (use.samples == 8u && use.fmaskAddress != 0u && use.cmaskAddress != 0u) {
|
||||
const auto resource = fmaskResource(use);
|
||||
static_cast<void>(fmaskSnapshotResource(resource));
|
||||
fmaskRange = GuestMemory::UnchangedQuery{use.fmaskAddress, static_cast<std::size_t>(DescribeSurface(resource).guestBytes), 0};
|
||||
auto& range = *fmaskRange;
|
||||
GuestMemory::CheckRange(reinterpret_cast<const void*>(range.address), range.bytes, 65536u);
|
||||
Require(GuestMemory::Watched(range.address, range.bytes), "native FMASK contents require watched guest memory");
|
||||
range.generation = GuestMemory::CollectWritesUncached(range.address, range.bytes);
|
||||
Require(range.generation != 0, "native FMASK contents have no write generation");
|
||||
for (const auto& other : dccRanges) Require(range.address >= other.address + other.bytes || other.address >= range.address + range.bytes, "native FMASK overlaps its color or metadata planes");
|
||||
}
|
||||
std::vector<std::byte> dcc(dccRanges[1].bytes);
|
||||
GuestMemory::Read(use.dccAddress, dcc);
|
||||
std::vector<std::byte> cmask(use.cmaskBytes);
|
||||
if (!cmask.empty()) GuestMemory::Read(use.cmaskAddress, cmask);
|
||||
const auto clear = MultisampledDccClear(use, dcc, cmask);
|
||||
Require(GuestMemory::UnchangedSinceAll(dccRanges), "initial multisampled DCC contents changed during import");
|
||||
Require(GuestMemory::UnchangedSinceAll(dccRanges) && (!fmaskRange || GuestMemory::UnchangedSinceAll(std::span(&*fmaskRange, 1u))), "initial multisampled DCC contents changed during import");
|
||||
return clear;
|
||||
}
|
||||
|
||||
void requireDccRange(bool valid, const GuestMemory::UnchangedQuery& range, const char* reason) const {
|
||||
if (valid) return;
|
||||
const auto* plane = range.address == target.address ? "color" : range.address == target.dccAddress ? "DCC" : "CMASK";
|
||||
const auto* plane = range.address == target.address ? "color" : range.address == target.dccAddress ? "DCC" : range.address == target.fmaskAddress ? "FMASK" : "CMASK";
|
||||
char text[384];
|
||||
std::snprintf(text, sizeof(text), "%s (%s range 0x%llx+0x%zx, %u samples)", reason, plane, static_cast<unsigned long long>(range.address), range.bytes, target.samples);
|
||||
Require(false, text);
|
||||
@@ -482,13 +612,23 @@ private:
|
||||
requireDccRange(GuestMemory::UnchangedSince(range.address, range.bytes, range.generation) || !GuestMemory::StoredOver(range.address, range.bytes, range.generation), range, "writes to a resident multisampled DCC target or its metadata are not implemented");
|
||||
if (auto* recorder = Recorder::Active(); recorder != nullptr) requireDccRange(!recorder->PendingWriteOverlaps(range.address, range.bytes), range, "pending writes to a resident multisampled DCC target or its metadata are not implemented");
|
||||
}
|
||||
if (fmaskRange) {
|
||||
Require(use.fmaskAddress == target.fmaskAddress && use.fmaskPipeBankXor == target.fmaskPipeBankXor, "native FMASK target layout changed");
|
||||
const auto& range = *fmaskRange;
|
||||
requireDccRange(GuestMemory::Watched(range.address, range.bytes), range, "native FMASK sampling requires watched guest memory");
|
||||
GuestMemory::CollectWritesUncached(range.address, range.bytes);
|
||||
requireDccRange(GuestMemory::UnchangedSince(range.address, range.bytes, range.generation) || !GuestMemory::StoredOver(range.address, range.bytes, range.generation), range, "writes to a resident native FMASK are not implemented");
|
||||
if (auto* recorder = Recorder::Active(); recorder != nullptr) requireDccRange(!recorder->PendingWriteOverlaps(range.address, range.bytes), range, "pending writes to a resident native FMASK are not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
void release() noexcept {
|
||||
fmaskTextures.clear();
|
||||
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);
|
||||
@@ -591,6 +731,7 @@ void guard(const Context& context, const ColorTarget& target, const std::shared_
|
||||
if (target.dccAddress != 0) {
|
||||
guarded().push_back({context.device, target.dccAddress, MultisampledDccBytes(target), target.samples, active, metadataValid});
|
||||
if (target.cmaskAddress != 0) guarded().push_back({context.device, target.cmaskAddress, target.cmaskBytes, target.samples, active, metadataValid});
|
||||
if (target.samples == 8u && target.fmaskAddress != 0u && target.cmaskAddress != 0u) guarded().push_back({context.device, target.fmaskAddress, DescribeSurface(fmaskResource(target)).guestBytes, target.samples, active, metadataValid});
|
||||
}
|
||||
anyGuarded().store(true, std::memory_order_release);
|
||||
InstallResidentImageReadGuard_nid_no_patch();
|
||||
@@ -610,11 +751,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 +779,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 +1100,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 +1110,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 +1196,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);
|
||||
@@ -1107,6 +1251,20 @@ void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<Texture> MultisampleTargetFmaskTexture_nid_no_patch(const Context& context, const GuestTextureResource& resource, VkComponentMapping components) {
|
||||
std::lock_guard lock(targetsMutex());
|
||||
discardRetiredTargets();
|
||||
std::shared_ptr<MultisampleTarget> owner;
|
||||
for (const auto& candidate : targets()) {
|
||||
if (!candidate->active->load(std::memory_order_acquire) || candidate->context.device != context.device) continue;
|
||||
const auto& target = candidate->target;
|
||||
if (target.fmaskAddress != resource.baseAddress && (resource.fmaskCmaskAddress == 0u || target.cmaskAddress != resource.fmaskCmaskAddress)) continue;
|
||||
Require(owner == nullptr, "FMASK descriptor refers to more than one resident target");
|
||||
owner = candidate;
|
||||
}
|
||||
return owner != nullptr ? owner->FmaskSampled(resource, components) : nullptr;
|
||||
}
|
||||
|
||||
void ClearMultisampleTargets(VkDevice device) {
|
||||
std::lock_guard lock(targetsMutex());
|
||||
std::erase_if(targets(), [&](const auto& target) { return target->context.device == device; });
|
||||
|
||||
@@ -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;
|
||||
@@ -259,6 +263,8 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State&
|
||||
appendKey(keys.fragmentOutput, state.perSampleShading);
|
||||
appendKey(keys.fragmentShader, state.mergedCoverage);
|
||||
appendKey(keys.fragmentOutput, state.mergedCoverage);
|
||||
appendKey(keys.fragmentShader, state.truncatedCoverage);
|
||||
appendKey(keys.fragmentOutput, state.truncatedCoverage);
|
||||
const auto sampleLocationsEnabled = ValidateSampleLocations(context, state);
|
||||
appendKey(keys.fragmentShader, sampleLocationsEnabled);
|
||||
appendKey(keys.fragmentOutput, sampleLocationsEnabled);
|
||||
@@ -341,11 +347,12 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
|
||||
Require(!state.negativeOneToOne || context.depthClipControl, "negative-one-to-one depth clipping requires VK_EXT_depth_clip_control with depthClipControl enabled");
|
||||
Require(!state.perSampleShading || context.sampleRateShading, "per-sample pixel shading requires the sampleRateShading device feature");
|
||||
Require(!state.mergedCoverage || (context.mergedCoverageSampleCounts & state.samples) != 0, "an overrasterized draw (a multisampled rasterizer into one-sample color targets) requires VK_NV_framebuffer_mixed_samples with merged coverage reduction");
|
||||
Require(!state.truncatedCoverage || (context.truncatedCoverageSampleCounts & state.samples) != 0, "one-sample color targets beside a multisampled depth target require VK_AMD_mixed_attachment_samples or VK_NV_framebuffer_mixed_samples with truncated coverage reduction");
|
||||
Require(state.conservativeRasterization == VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT || context.conservativeRasterization, "conservative rasterization requires VK_EXT_conservative_rasterization with at most 1/256 pixel of overestimation and degenerate triangles rasterized");
|
||||
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");
|
||||
@@ -403,7 +410,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
|
||||
for (std::uint32_t index = 0; index < state.colors.size(); ++index) {
|
||||
VkAttachmentDescription color{};
|
||||
color.format = state.colors[index].format;
|
||||
color.samples = state.mergedCoverage ? VK_SAMPLE_COUNT_1_BIT : static_cast<VkSampleCountFlagBits>(state.samples);
|
||||
color.samples = state.mergedCoverage || state.truncatedCoverage ? VK_SAMPLE_COUNT_1_BIT : static_cast<VkSampleCountFlagBits>(state.samples);
|
||||
color.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
|
||||
color.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||||
color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
||||
@@ -440,7 +447,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
|
||||
VkAttachmentSampleCountInfoNV attachmentSamples{VK_STRUCTURE_TYPE_ATTACHMENT_SAMPLE_COUNT_INFO_NV};
|
||||
attachmentSamples.colorAttachmentCount = static_cast<std::uint32_t>(mergedColorSamples.size());
|
||||
attachmentSamples.pColorAttachmentSamples = mergedColorSamples.empty() ? nullptr : mergedColorSamples.data();
|
||||
if (state.mergedCoverage) rendering.pNext = &attachmentSamples;
|
||||
if (state.truncatedCoverage) attachmentSamples.depthStencilAttachmentSamples = static_cast<VkSampleCountFlagBits>(state.samples);
|
||||
if (state.mergedCoverage || state.truncatedCoverage) rendering.pNext = &attachmentSamples;
|
||||
if (state.depth) {
|
||||
rendering.depthAttachmentFormat = HostDepthFormat(context, state.depth->format);
|
||||
if (HasStencil(state.depth->format)) rendering.stencilAttachmentFormat = rendering.depthAttachmentFormat;
|
||||
@@ -514,8 +522,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
|
||||
sampleLocations.sampleLocationsInfo = {VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT, nullptr, samples.rasterizationSamples, grid, state.samples * grid.width * grid.height, locations.data()};
|
||||
if (sampleLocationsEnabled) samples.pNext = &sampleLocations;
|
||||
VkPipelineCoverageReductionStateCreateInfoNV coverageReduction{VK_STRUCTURE_TYPE_PIPELINE_COVERAGE_REDUCTION_STATE_CREATE_INFO_NV};
|
||||
coverageReduction.coverageReductionMode = VK_COVERAGE_REDUCTION_MODE_MERGE_NV;
|
||||
if (state.mergedCoverage) {
|
||||
coverageReduction.coverageReductionMode = state.truncatedCoverage ? VK_COVERAGE_REDUCTION_MODE_TRUNCATE_NV : VK_COVERAGE_REDUCTION_MODE_MERGE_NV;
|
||||
if (state.mergedCoverage || (state.truncatedCoverage && !context.mixedAttachmentSamples)) {
|
||||
coverageReduction.pNext = samples.pNext;
|
||||
samples.pNext = &coverageReduction;
|
||||
}
|
||||
@@ -893,6 +901,7 @@ void pipelineKey(std::vector<std::byte>& key, const Context& context, const Stat
|
||||
append(key, state.samples);
|
||||
append(key, state.perSampleShading);
|
||||
append(key, state.mergedCoverage);
|
||||
append(key, state.truncatedCoverage);
|
||||
append(key, state.customSampleLocations);
|
||||
if (state.customSampleLocations) {
|
||||
append(key, state.sampleLocationGrid);
|
||||
|
||||
@@ -35,7 +35,7 @@ static_assert(sizeof(VkStencilOpState) == 28, "VkStencilOpState changed: update
|
||||
static_assert(sizeof(VkVertexInputBindingDescription) == 12, "VkVertexInputBindingDescription changed: update the recipe codec");
|
||||
static_assert(sizeof(VkVertexInputAttributeDescription) == 16, "VkVertexInputAttributeDescription changed: update the recipe codec");
|
||||
#if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__)
|
||||
static_assert(sizeof(State) == 784, "State changed: update CanonicalPipelineState and the recipe codec");
|
||||
static_assert(sizeof(State) == 808, "State changed: update CanonicalPipelineState and the recipe codec");
|
||||
static_assert(sizeof(ShaderStages) == 112, "ShaderStages changed: update the recipe codec");
|
||||
static_assert(sizeof(ShaderRecompiler::MeshConfiguration) == 40, "MeshConfiguration changed: update the recipe codec");
|
||||
static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "TessellationConfiguration changed: update the recipe codec");
|
||||
@@ -44,7 +44,7 @@ static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "Tessel
|
||||
constexpr std::uint32_t FileMagic = 0x4c4a5041u;
|
||||
// Bumped when the recipe codec, CanonicalPipelineState or the store key change in a way the
|
||||
// layout guards above do not catch (a field renumbered or reinterpreted at the same size).
|
||||
constexpr std::uint32_t FileFormat = 6u;
|
||||
constexpr std::uint32_t FileFormat = 7u;
|
||||
constexpr std::uint32_t ModuleRecord = 1u;
|
||||
constexpr std::uint32_t GraphicsRecord = 2u;
|
||||
constexpr std::uint32_t ComputeRecord = 3u;
|
||||
@@ -217,6 +217,7 @@ void encodeState(Writer& writer, const State& state) {
|
||||
writer.Bool(state.sampleLocationGrid);
|
||||
writer.Bool(state.perSampleShading);
|
||||
writer.Bool(state.mergedCoverage);
|
||||
writer.Bool(state.truncatedCoverage);
|
||||
for (const auto mask : state.quadSampleMask) writer.Raw(mask);
|
||||
writer.U32(state.depthRemap.mode);
|
||||
}
|
||||
@@ -278,6 +279,7 @@ void decodeState(Reader& reader, State& state) {
|
||||
state.sampleLocationGrid = reader.Bool();
|
||||
state.perSampleShading = reader.Bool();
|
||||
state.mergedCoverage = reader.Bool();
|
||||
state.truncatedCoverage = reader.Bool();
|
||||
for (auto& mask : state.quadSampleMask) mask = reader.Raw<std::uint16_t>();
|
||||
state.depthRemap.mode = reader.U32();
|
||||
}
|
||||
@@ -1051,6 +1053,7 @@ State CanonicalPipelineState(const State& state) {
|
||||
canonical.sampleLocationGrid = state.sampleLocationGrid;
|
||||
canonical.perSampleShading = state.perSampleShading;
|
||||
canonical.mergedCoverage = state.mergedCoverage;
|
||||
canonical.truncatedCoverage = state.truncatedCoverage;
|
||||
canonical.quadSampleMask = state.quadSampleMask;
|
||||
canonical.depthRemap.mode = state.depthRemap.mode;
|
||||
return canonical;
|
||||
|
||||
@@ -255,10 +255,24 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip
|
||||
}
|
||||
if (partsOnly) return VK_NULL_HANDLE;
|
||||
PerformanceTimer timing("Vulkan.GraphicsPipelineLink");
|
||||
std::optional<std::pair<std::vector<VkSampleCountFlagBits>, VkSampleCountFlagBits>> sampleCounts;
|
||||
for (auto* next = static_cast<const VkBaseInStructure*>(rendering.pNext); next != nullptr; next = next->pNext) {
|
||||
if (next->sType != VK_STRUCTURE_TYPE_ATTACHMENT_SAMPLE_COUNT_INFO_NV) continue;
|
||||
const auto& counts = *reinterpret_cast<const VkAttachmentSampleCountInfoNV*>(next);
|
||||
sampleCounts.emplace(std::vector<VkSampleCountFlagBits>(counts.pColorAttachmentSamples, counts.pColorAttachmentSamples + counts.colorAttachmentCount), counts.depthStencilAttachmentSamples);
|
||||
}
|
||||
const auto attachmentSamples = [](const std::pair<std::vector<VkSampleCountFlagBits>, VkSampleCountFlagBits>& counts) {
|
||||
return VkAttachmentSampleCountInfoNV{VK_STRUCTURE_TYPE_ATTACHMENT_SAMPLE_COUNT_INFO_NV, nullptr, static_cast<std::uint32_t>(counts.first.size()), counts.first.empty() ? nullptr : counts.first.data(), counts.second};
|
||||
};
|
||||
auto linkedSamples = sampleCounts ? attachmentSamples(*sampleCounts) : VkAttachmentSampleCountInfoNV{};
|
||||
VkPipelineLibraryCreateInfoKHR linked{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR};
|
||||
linked.libraryCount = static_cast<std::uint32_t>(libraries.size());
|
||||
linked.pLibraries = libraries.data();
|
||||
VkGraphicsPipelineCreateInfo create{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, &linked};
|
||||
if (sampleCounts) {
|
||||
linkedSamples.pNext = create.pNext;
|
||||
create.pNext = &linkedSamples;
|
||||
}
|
||||
create.layout = info.layout;
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
lock.unlock();
|
||||
@@ -277,12 +291,17 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip
|
||||
const auto cache = context.pipelineCache;
|
||||
const auto cacheSynchronization = context.pipelineCacheSynchronization;
|
||||
const auto libraryLayout = fullLayout;
|
||||
std::function<void()> relink = [slot, createPipelines, cache, cacheSynchronization, libraryLayout, libraries, &store, devicePointer = &device] {
|
||||
std::function<void()> relink = [slot, createPipelines, cache, cacheSynchronization, libraryLayout, libraries, sampleCounts, attachmentSamples, &store, devicePointer = &device] {
|
||||
if (slot->released.load()) return;
|
||||
auto linkedSamples = sampleCounts ? attachmentSamples(*sampleCounts) : VkAttachmentSampleCountInfoNV{};
|
||||
VkPipelineLibraryCreateInfoKHR linked{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR};
|
||||
linked.libraryCount = static_cast<std::uint32_t>(libraries.size());
|
||||
linked.pLibraries = libraries.data();
|
||||
VkGraphicsPipelineCreateInfo create{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, &linked};
|
||||
if (sampleCounts) {
|
||||
linkedSamples.pNext = create.pNext;
|
||||
create.pNext = &linkedSamples;
|
||||
}
|
||||
create.flags = VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT;
|
||||
create.layout = libraryLayout;
|
||||
VkPipeline result = VK_NULL_HANDLE;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "ThreadOwned.hpp"
|
||||
#include "prx/libc/include/HostThreadLocal.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
|
||||
@@ -682,6 +683,14 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
|
||||
logLookup({target.get(), resource, 0, DccKeys::Uncompressed, 0, nullptr, true});
|
||||
return target;
|
||||
}
|
||||
if (resource.fmaskCmaskAddress != 0u) {
|
||||
Require(!depthCompare, "FMASK depth comparison sampling is not implemented");
|
||||
Require(resource.baseArray == 0u && resource.depthOrLastArray == 0u, "native FMASK array sampling is not implemented");
|
||||
CompletePendingCpuWrites(context, resource.fmaskCmaskAddress, CmaskBytes(resource.width, resource.height));
|
||||
auto native = MultisampleTargetFmaskTexture_nid_no_patch(context, resource, components);
|
||||
Require(native != nullptr, "compressed FMASK sampling without a matching resident native target is not implemented");
|
||||
return native;
|
||||
}
|
||||
CaptureTrace::Log("sampled-lookup address=%llx width=%u height=%u dcc=%llx", static_cast<unsigned long long>(resource.baseAddress), resource.width, resource.height, static_cast<unsigned long long>(resource.dccAddress));
|
||||
RequireNoMultisampleTarget(resource.baseAddress, guestBytes != 0 ? static_cast<std::size_t>(guestBytes) : 1u, "a texture read");
|
||||
if (auto* recorder = Recorder::Active(); recorder != nullptr) recorder->BoundKeptBytes();
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -355,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;
|
||||
@@ -371,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{};
|
||||
@@ -396,7 +397,27 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValEx
|
||||
}
|
||||
const auto size = read(cx, 0x007);
|
||||
depth.extent = {(size & 0x3fffu) + 1u, ((size >> 16u) & 0x3fffu) + 1u};
|
||||
if (const auto slice = view & 0x1fffu; slice != 0) {
|
||||
if (maxMip == 0 && viewMip != 0) Require(false, "depth view mip " + std::to_string(viewMip) + " on a surface without mips");
|
||||
if (maxMip != 0) {
|
||||
char message[320];
|
||||
std::snprintf(message, sizeof(message), "depth view mip %u of %u at %ux%u (DB_Z_INFO 0x%08x, DB_DEPTH_VIEW 0x%08x)", viewMip, maxMip + 1u, depth.extent.width, depth.extent.height, read(cx, 0x010), view);
|
||||
const std::string where(message);
|
||||
Require(viewMip <= maxMip, "the " + where + " is beyond the surface's mips");
|
||||
Require(depth.samples == 1u, "multisampled " + where + " is unsupported");
|
||||
Require(depth.depthSwizzle == XorSwizzleMode(TextureTileMode::kZ64KBX), "the " + where + " uses a swizzle other than 64 KiB Z_X");
|
||||
const auto depthMips = ComputeElementMipLayout(TextureTileMode::kZ64KBX, zFormat == 1 ? 2u : 4u, depth.extent.width, depth.extent.height, maxMip + 1u);
|
||||
const auto stencilMips = ComputeElementMipLayout(TextureTileMode::kZ64KBX, 1u, depth.extent.width, depth.extent.height, maxMip + 1u);
|
||||
Require(zFormat == 0 || !depthMips.at(viewMip).tail, "rendering the " + where + " into the packed mip tail is unsupported");
|
||||
Require(!stencil || !stencilMips.at(viewMip).tail, "rendering the stencil of the " + where + " into the packed mip tail is unsupported");
|
||||
Require((read(cx, 0x010) & (1u << 29u)) == 0, "HTILE for the " + where + " is unsupported");
|
||||
if (const auto slice = view & 0x1fffu; slice != 0) {
|
||||
if (depth.address != 0) depth.address += static_cast<std::uint64_t>(slice) * ComputeSurfaceSize(depthMips, 1u);
|
||||
if (depth.stencilAddress != 0) depth.stencilAddress += static_cast<std::uint64_t>(slice) * ComputeSurfaceSize(stencilMips, 1u);
|
||||
}
|
||||
if (depth.address != 0) depth.address += depthMips.at(viewMip).tiledOffset;
|
||||
if (depth.stencilAddress != 0) depth.stencilAddress += stencilMips.at(viewMip).tiledOffset;
|
||||
depth.extent = {depthMips.at(viewMip).width, depthMips.at(viewMip).height};
|
||||
} else if (const auto slice = view & 0x1fffu; slice != 0) {
|
||||
if (depth.address != 0) depth.address += static_cast<std::uint64_t>(slice) * DepthSliceBytes(depth.extent, zFormat == 1 ? 2u : 4u);
|
||||
if (depth.stencilAddress != 0) depth.stencilAddress += static_cast<std::uint64_t>(slice) * DepthSliceBytes(depth.extent, 1u);
|
||||
}
|
||||
@@ -908,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();
|
||||
@@ -1313,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;
|
||||
@@ -1415,8 +1435,10 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
|
||||
}
|
||||
const bool merged = color.samples == 1u && !result.depth && !result.perSampleShading && (result.colors.empty() || result.mergedCoverage) && overrasterized(cx, result.samples);
|
||||
if (!merged && color.samples == 1 && result.colors.empty() && !result.depth && rasterizesSampleZeroOnly(cx, result)) rasterizeSampleZero(result);
|
||||
Require((color.samples == result.samples && !result.mergedCoverage) || merged, "a color target's sample count differs from the rasterizer's");
|
||||
const bool truncated = color.samples == 1u && result.samples > 1u && result.depth && result.depth->samples == result.samples && (result.colors.empty() || result.truncatedCoverage) && !overrasterized(cx, result.samples);
|
||||
Require((color.samples == result.samples && !result.mergedCoverage && !result.truncatedCoverage) || merged || truncated, "a color target's sample count differs from the rasterizer's");
|
||||
result.mergedCoverage = merged;
|
||||
result.truncatedCoverage = truncated;
|
||||
APS5_LOG_OUT_DEBUG("Color %u address=0x%llx extent=%ux%u bytes=%llu VkFormat=%u", slot, static_cast<unsigned long long>(color.address), color.extent.width, color.extent.height, static_cast<unsigned long long>(color.bytes), static_cast<unsigned>(color.format));
|
||||
if (result.colors.empty()) {
|
||||
result.renderExtent = color.extent;
|
||||
@@ -1856,6 +1878,14 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo
|
||||
if (color.samples == 1) GuestMemory::CheckRange(reinterpret_cast<const void*>(cmask), totalCmaskBytes, CmaskLayout::Alignment, true);
|
||||
}
|
||||
color.cmaskFastClear = (info & 0x2000u) != 0;
|
||||
if (const auto fmask = find(cx, 0x321 + stride); color.samples > 1u && fmask != cx.end()) {
|
||||
const auto high = find(cx, 0x3a0 + slot);
|
||||
const auto fmaskHigh = high == cx.end() ? 0u : high->second;
|
||||
Require((fmaskHigh & ~0xffu) == 0u, "invalid FMASK address extension");
|
||||
const auto address = (static_cast<std::uint64_t>(fmaskHigh) << 40u) | (static_cast<std::uint64_t>(fmask->second) << 8u);
|
||||
color.fmaskPipeBankXor = static_cast<std::uint32_t>(address & 0xffffu);
|
||||
color.fmaskAddress = address - color.fmaskPipeBankXor;
|
||||
}
|
||||
}
|
||||
if ((info & 0x10000000u) != 0) {
|
||||
if (maxMip == 0) {
|
||||
@@ -2028,10 +2058,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;
|
||||
|
||||
@@ -5,6 +5,12 @@
|
||||
#include "SceTypes.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int CAPTURE_UNAVAILABLE = static_cast<int>(0x8A6C1000);
|
||||
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
bool APS5_VABI sceAgcDriverIsCaptureInProgress(void) {
|
||||
@@ -21,18 +27,15 @@ bool APS5_VABI sceAgcDriverIsSubmitValidationEnabled(void) {
|
||||
|
||||
int APS5_VABI sceAgcDriverRequestCaptureStart(const char* path) {
|
||||
(void)path;
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
return CAPTURE_UNAVAILABLE;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcDriverRequestCaptureStop() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
return CAPTURE_UNAVAILABLE;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcDriverTriggerCapture() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
return CAPTURE_UNAVAILABLE;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -57,6 +57,17 @@ int APS5_VABI sceAgcDriverGetHsOffchipParam(uint16_t* first, uint16_t* second) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcDriverNopDataWritePacket(void* destination, uint32_t address, const void* data, uint32_t bytes) {
|
||||
const auto alignedBytes = (static_cast<std::uint64_t>(bytes) + 3) & ~std::uint64_t{3};
|
||||
const auto totalBytes = alignedBytes + 8;
|
||||
std::memset(destination, 0, static_cast<std::size_t>(totalBytes));
|
||||
const auto header = (static_cast<std::uint32_t>(totalBytes) << 14) + 0x3ffe0000U | 0xc0001000U;
|
||||
std::memcpy(destination, &header, sizeof(header));
|
||||
std::memcpy(static_cast<std::byte*>(destination) + 4, &address, sizeof(address));
|
||||
if (data != nullptr && bytes != 0) std::memcpy(static_cast<std::byte*>(destination) + 8, data, bytes);
|
||||
return data == nullptr || bytes == 0 ? 2 : static_cast<int>(totalBytes / 4);
|
||||
}
|
||||
|
||||
|
||||
int APS5_VABI sceAgcDriverSetValidationDisabled() {
|
||||
return DEBUG_UNAVAILABLE;
|
||||
@@ -154,9 +165,8 @@ int APS5_VABI sceAgcDriverAcquireHDRScopesACQ() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcDriverGetCoredumpAddress() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
void* APS5_VABI sceAgcDriverGetCoredumpAddress() {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcDriverHp3dLockEnd() {
|
||||
@@ -174,19 +184,13 @@ int APS5_VABI sceAgcDriverIsEsvmProcess() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcDriverNopDataWritePacket() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcDriverReleaseHDRScopesACQ() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcDriverSetTargetRingForDiag() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
return DRIVER_FEATURE_UNAVAILABLE;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcDriverSetValueToSharedGds() {
|
||||
|
||||
@@ -2813,7 +2813,7 @@ bool RunSparseBufferHoleTests(const Context& context, const BdaTestAccess& acces
|
||||
std::memset(reservation.data + 2 * unit, 0x72, unit);
|
||||
VkBuffer seeded = VK_NULL_HANDLE;
|
||||
{
|
||||
Buffer seed(context, copied, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
|
||||
Buffer seed(context, copied, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
|
||||
std::fill(seed.Bytes().begin(), seed.Bytes().end(), std::byte{0xa5});
|
||||
seeded = seed.Handle();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
#include "RdnaDecoder/RdnaInstructionDecoder.hpp"
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <exception>
|
||||
#include <span>
|
||||
|
||||
using namespace ShaderRecompiler;
|
||||
|
||||
namespace {
|
||||
|
||||
int failures = 0;
|
||||
|
||||
void Check(bool condition, const char* what) {
|
||||
if (condition) return;
|
||||
std::fprintf(stderr, "%s\n", what);
|
||||
++failures;
|
||||
}
|
||||
|
||||
RdnaInstruction Decode(std::span<const std::uint32_t> words) {
|
||||
return DecodeGcnInstruction(0u, words, 0u);
|
||||
}
|
||||
|
||||
void SetregImmKeepsItsLiteral() {
|
||||
const std::array<std::uint32_t, 3> setreg{0xba80f801u, 0x12345678u, 0xbf810000u};
|
||||
const auto instruction = Decode(setreg);
|
||||
Check(instruction.op == RdnaOpcode::SSetregImm32B32, "s_setreg_imm32_b32 did not decode as its RDNA opcode");
|
||||
Check(instruction.wordCount == 2u, "a GCN s_setreg_imm32_b32 is not two words");
|
||||
Check(instruction.source0.value == 0x12345678u, "s_setreg_imm32_b32 lost its literal");
|
||||
const std::array<std::uint32_t, 2> movk{0xb0001234u, 0xbf810000u};
|
||||
Check(Decode(movk).wordCount == 1u, "s_movk_i32 took a literal word");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
SetregImmKeepsItsLiteral();
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "%s\n", error.what());
|
||||
return 1;
|
||||
}
|
||||
if (failures == 0) std::puts("GCN decode tests passed");
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
|
||||
#include "SceShaders.hpp"
|
||||
#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp"
|
||||
#include "ControlFlow/RequestSerializer.hpp"
|
||||
@@ -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;
|
||||
@@ -2363,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");
|
||||
@@ -3316,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;
|
||||
@@ -3327,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");
|
||||
@@ -3634,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;
|
||||
@@ -4077,6 +4151,29 @@ void multisampledDepthTests() {
|
||||
Require(!DecodeState(queue).perSampleShading, "PS_ITER_SAMPLE requested sample shading for a single-sample scan");
|
||||
queue = singleSampleScan();
|
||||
queue.context[0x31d] = 0;
|
||||
state = DecodeState(queue);
|
||||
Require(state.truncatedCoverage && !state.mergedCoverage && state.colors.size() == 1 && state.colors[0].samples == 1, "a single-sample scan into a 4x depth target and a one-sample color target did not take the color from sample 0");
|
||||
requireCentered(state, "a single-sample scan into a 4x depth target and a one-sample color target");
|
||||
const auto truncatedTwoSample = [&] {
|
||||
auto queue = singleSampleScan();
|
||||
queue.context[0x2f8] = 0x00100001;
|
||||
queue.context[0x292] = 3;
|
||||
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0xcc44;
|
||||
queue.context[0x010] = 0x22900987;
|
||||
queue.context[0x31d] = 0;
|
||||
return queue;
|
||||
};
|
||||
queue = truncatedTwoSample();
|
||||
state = DecodeState(queue);
|
||||
Require(state.samples == 2 && state.depth && state.depth->samples == 2 && state.colors.size() == 1 && state.colors[0].samples == 1 && state.truncatedCoverage && !state.mergedCoverage && !state.perSampleShading, "a one-sample color target beside a 2x depth target under a 2x rasterizer with MSAA_ENABLE (PPSA02225's registers) did not take the color from sample 0");
|
||||
Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck refused a one-sample color target beside a 2x depth target");
|
||||
queue = truncatedTwoSample();
|
||||
queue.context[0x293] = 0x10000u;
|
||||
queue.context[0x1b3] = queue.context[0x1b4] = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveSample) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::FrontFace);
|
||||
state = DecodeState(queue);
|
||||
Require(state.truncatedCoverage && state.perSampleShading && state.samples == 2 && state.colors[0].samples == 1, "PS_ITER_SAMPLE into a one-sample color target beside a 2x depth target (PPSA02225's registers) did not run per sample with sample 0's color");
|
||||
queue = truncatedTwoSample();
|
||||
queue.context[0x31d] = 0x12000;
|
||||
expectFailure([&] { DecodeState(queue); }, "a color target's sample count differs from the rasterizer's");
|
||||
queue = singleSampleScan();
|
||||
queue.context[0x010] = 0x22900983;
|
||||
@@ -7894,9 +7991,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}}};
|
||||
@@ -9136,6 +9235,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;
|
||||
@@ -9500,7 +9607,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));
|
||||
@@ -10686,6 +10798,39 @@ void pipelineJournalTests(bool longPath = false, bool extended = false) {
|
||||
const PipelineSetLayouts decodedLayouts{layouts.layouts.main, layouts.layouts.fragment, decoded.splitSets, decoded.mainLayoutKey, decoded.fragmentLayoutKey};
|
||||
BuildPipelineStoreKey(replayed, context, decoded.state, decoded.vertexInput, decodedLayouts, shaders, decoded.attachmentLayout);
|
||||
Require(!original.empty() && original == canonical && original == replayed, "the canonical or the decoded state keys another pipeline");
|
||||
auto fmaskState = state;
|
||||
fmaskState.samples = 8;
|
||||
fmaskState.color.samples = 8;
|
||||
fmaskState.color.fmaskAddress = 0x30000;
|
||||
fmaskState.color.fmaskPipeBankXor = 0x1200;
|
||||
for (auto& target : fmaskState.colors) {
|
||||
target.samples = 8;
|
||||
target.fmaskAddress = 0x40000;
|
||||
target.fmaskPipeBankXor = 0x2300;
|
||||
}
|
||||
const auto fmaskRecipe = MakeGraphicsPipelineRecipe(fmaskState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL);
|
||||
Require(fmaskRecipe.state.color.fmaskAddress == 0 && fmaskRecipe.state.color.fmaskPipeBankXor == 0, "the canonical state kept the primary target's FMASK location");
|
||||
for (const auto& canonicalColor : fmaskRecipe.state.colors) {
|
||||
Require(canonicalColor.fmaskAddress == 0 && canonicalColor.fmaskPipeBankXor == 0, "the canonical state kept an attachment's FMASK location");
|
||||
}
|
||||
const auto fmaskBytes = EncodeGraphicsPipelineRecipe(fmaskRecipe);
|
||||
GraphicsPipelineRecipe fmaskDecoded;
|
||||
Require(DecodeGraphicsPipelineRecipe(fmaskBytes, fmaskDecoded) && EncodeGraphicsPipelineRecipe(fmaskDecoded) == fmaskBytes, "the FMASK pipeline recipe did not round-trip byte for byte");
|
||||
std::vector<std::byte> fmaskKey, fmaskReplayedKey;
|
||||
BuildPipelineStoreKey(fmaskKey, context, fmaskState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL);
|
||||
BuildPipelineStoreKey(fmaskReplayedKey, context, fmaskDecoded.state, fmaskDecoded.vertexInput, layouts.View(), shaders, fmaskDecoded.attachmentLayout);
|
||||
Require(fmaskKey != original && fmaskKey == fmaskReplayedKey, "the decoded FMASK state keys another pipeline");
|
||||
auto relocatedFmask = fmaskState;
|
||||
relocatedFmask.color.fmaskAddress += 0x10000;
|
||||
relocatedFmask.color.fmaskPipeBankXor ^= 0x100;
|
||||
for (auto& target : relocatedFmask.colors) {
|
||||
target.fmaskAddress += 0x20000;
|
||||
target.fmaskPipeBankXor ^= 0x200;
|
||||
}
|
||||
std::vector<std::byte> relocatedFmaskKey;
|
||||
BuildPipelineStoreKey(relocatedFmaskKey, context, relocatedFmask, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL);
|
||||
Require(relocatedFmaskKey == fmaskKey, "the FMASK resource location changed the pipeline key");
|
||||
Require(EncodeGraphicsPipelineRecipe(MakeGraphicsPipelineRecipe(relocatedFmask, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL)) == fmaskBytes, "the FMASK resource location changed the journal recipe");
|
||||
auto gridState = state;
|
||||
gridState.samples = 2;
|
||||
gridState.customSampleLocations = true;
|
||||
|
||||
@@ -95,6 +95,52 @@ void rejectFields(const Fields& f, std::string_view reason) {
|
||||
|
||||
}
|
||||
|
||||
static void RunR128TextureTests() {
|
||||
constexpr std::array<std::uint32_t, 8> streamed{0xa0c18800u, 0xcad00000u, 0x01ffc1ffu, 0x905b2facu, 0u, 0u, 0u, 0u};
|
||||
const auto view = DecodeTextureResource(streamed);
|
||||
Require(view.baseAddress == 0xa0c1880000ull && view.format == 173u && view.tileMode == TextureTileMode::kStandard4KB && view.width == 512u && view.height == 512u && view.baseLevel == 0u && view.lastLevel == 9u && view.mipCount == 10u && view.allocatedMipCount == 10u && view.minLod == 0u,
|
||||
"a 128-bit BC3 T# (2048x2048, levels 2..11, words 4-7 zero) must read as the 512x512 chain of its 10 resident levels, got " + std::to_string(view.width) + "x" + std::to_string(view.height) + " levels " + std::to_string(view.baseLevel) + ".." + std::to_string(view.lastLevel) + " of " + std::to_string(view.mipCount));
|
||||
auto chainWords = streamed;
|
||||
chainWords[3] &= ~(0xfu << 12u);
|
||||
chainWords[5] = 11u << 4u;
|
||||
const auto full = DecodeTextureResource(chainWords);
|
||||
Require(full.width == 2048u && full.mipCount == 12u, "the same surface with MAX_MIP 11 must be the 12-level 2048x2048 chain");
|
||||
const auto chain = DescribeSurface(full);
|
||||
const auto resident = DescribeSurface(view);
|
||||
Require(chain.mips.size() == 12u && resident.mips.size() == 10u, "the chain and the resident levels must have 12 and 10 levels");
|
||||
for (std::uint32_t level = 0; level < 10u; ++level) {
|
||||
const auto& a = chain.mips[level + 2u];
|
||||
const auto& b = resident.mips[level];
|
||||
Require(a.tiledOffset == b.tiledOffset && a.tiledSize == b.tiledSize && a.width == b.width && a.height == b.height && a.blocksPerRow == b.blocksPerRow && a.tail == b.tail && a.tailX == b.tailX && a.tailY == b.tailY, "resident level " + std::to_string(level) + " must be level " + std::to_string(level + 2u) + " of the 12-level chain");
|
||||
}
|
||||
Require(resident.guestBytes <= chain.mips[1].tiledOffset && chain.mips[0].tiledOffset > chain.mips[1].tiledOffset, "the resident levels must end before the chain's levels 1 and 0, which come last in a GFX10 swizzled chain");
|
||||
auto levelZero = streamed;
|
||||
levelZero[3] &= ~(0xfu << 12u);
|
||||
const auto single = DecodeTextureResource(levelZero);
|
||||
Require(single.mipCount == 1u && single.lastLevel == 0u, "a T# with words 4-7 zero that starts at level 0 must stay one level");
|
||||
auto tailed = streamed;
|
||||
tailed[5] = 4u << 20u;
|
||||
reject([&] { DecodeTextureResource(tailed); }, "would move the surface's own");
|
||||
|
||||
constexpr std::array<std::uint32_t, 8> tall{0xa0c1de00u, 0xcad00000u, 0x01ffc0ffu, 0x905b4facu, 0u, 0u, 0u, 0u};
|
||||
const auto tallView = DecodeTextureResource(tall);
|
||||
Require(tallView.width == 64u && tallView.height == 128u && tallView.lastLevel == 7u && tallView.mipCount == 8u, "a 128-bit 1024x2048 T# of levels 4..11 must read as its 64x128 chain of 8 levels");
|
||||
|
||||
Fields clamped;
|
||||
clamped.base40 = 0x55ea000ull;
|
||||
clamped.format = 20;
|
||||
clamped.width = 1024;
|
||||
clamped.height = 1024;
|
||||
clamped.tileModeRaw = 0x1b;
|
||||
clamped.baseLevel = 2;
|
||||
clamped.lastLevel = 10;
|
||||
clamped.minLod = 2u * 256u + 64u;
|
||||
const auto clampedView = DecodeTextureResource(pack(clamped));
|
||||
Require(clampedView.width == 256u && clampedView.mipCount == 9u && clampedView.minLod == 64u, "reading a 128-bit T# from its resident levels must move its MIN_LOD clamp with the base level");
|
||||
clamped.minLod = 256u;
|
||||
Require(DecodeTextureResource(pack(clamped)).minLod == 0u, "a MIN_LOD clamp below the base level must vanish when read from the resident levels");
|
||||
}
|
||||
|
||||
void RunGuestTextureResourceTests() {
|
||||
Fields base;
|
||||
const auto baseWords = pack(base);
|
||||
@@ -123,6 +169,15 @@ void RunGuestTextureResourceTests() {
|
||||
fmask.dstSelY = fmask.dstSelZ = fmask.dstSelW = 0u;
|
||||
const auto fmaskResource = DecodeTextureResource(pack(fmask));
|
||||
Require(fmaskResource.format == 20u && fmaskResource.dimension == TextureDimension::k2D && fmaskResource.tileMode == TextureTileMode::kZ64KBX, "uncompressed 32-bit FMASK did not use an integer Z surface");
|
||||
auto compressedFmask = fmask;
|
||||
compressedFmask.metaCompress = compressedFmask.metaPipeAligned = true;
|
||||
compressedFmask.metaAddr = 0x123456789aull;
|
||||
const auto compressedResource = DecodeTextureResource(pack(compressedFmask));
|
||||
Require(compressedResource.fmaskCmaskAddress == compressedFmask.metaAddr << 8u && compressedResource.dccAddress == 0u && compressedResource.dccPipeAligned, "compressed FMASK CMASK was interpreted as color DCC");
|
||||
compressedFmask.metaPipeAligned = false;
|
||||
rejectFields(compressedFmask, "FMASK CMASK metadata must be pipe aligned");
|
||||
compressedFmask.metaPipeAligned = compressedFmask.writeCompress = true;
|
||||
rejectFields(compressedFmask, "FMASK CMASK compression is not implemented");
|
||||
Fields fmaskArray = fmask;
|
||||
fmaskArray.typeRaw = 13u;
|
||||
fmaskArray.depth = 2u;
|
||||
@@ -620,6 +675,7 @@ void RunGuestTextureResourceTests() {
|
||||
pastUntailed.maxMip = 0;
|
||||
pastUntailed.baseLevel = 1;
|
||||
pastUntailed.lastLevel = 1;
|
||||
pastUntailed.perfMod = 4;
|
||||
rejectFields(pastUntailed, "would move the surface's own");
|
||||
auto untailedChain = tailView;
|
||||
untailedChain.mipCount = 1;
|
||||
@@ -627,6 +683,8 @@ void RunGuestTextureResourceTests() {
|
||||
untailedChain.mipCount = 2;
|
||||
Require(untailedAllocated == 0xff0000u && DescribeSurface(untailedChain).guestBytes == 0x1470000u, "a second level must grow a 1920x1080 64 bpp SW_64KB_R_X surface from addrlib's 0xff0000 to 0x1470000 bytes");
|
||||
|
||||
RunR128TextureTests();
|
||||
|
||||
std::array<std::uint32_t, 4> shortWords{};
|
||||
reject([&] { DecodeTextureResource(shortWords); }, "8 dwords");
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#include "SceTypes.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
extern "C" int APS5_VABI sceAgcDriverNopDataWritePacket(void* destination, uint32_t address, const void* data, uint32_t bytes);
|
||||
|
||||
int main() {
|
||||
std::array<std::byte, 24> storage;
|
||||
storage.fill(std::byte{0xaa});
|
||||
const std::array<std::byte, 5> data{std::byte{0x11}, std::byte{0x22}, std::byte{0x33}, std::byte{0x44}, std::byte{0x55}};
|
||||
const auto words = sceAgcDriverNopDataWritePacket(storage.data() + 4, 0x12345678, data.data(), static_cast<std::uint32_t>(data.size()));
|
||||
std::uint32_t header = 0;
|
||||
std::uint32_t address = 0;
|
||||
std::memcpy(&header, storage.data() + 4, sizeof(header));
|
||||
std::memcpy(&address, storage.data() + 8, sizeof(address));
|
||||
if (words != 4 || header != 0xc0021000U || address != 0x12345678U) return 1;
|
||||
if (!std::equal(data.begin(), data.end(), storage.begin() + 12)) return 2;
|
||||
if (storage[17] != std::byte{0} || storage[18] != std::byte{0} || storage[19] != std::byte{0}) return 3;
|
||||
if (storage[3] != std::byte{0xaa} || storage[20] != std::byte{0xaa}) return 4;
|
||||
|
||||
storage.fill(std::byte{0xaa});
|
||||
const auto emptyWords = sceAgcDriverNopDataWritePacket(storage.data() + 4, 0x87654321, nullptr, 5);
|
||||
std::memcpy(&header, storage.data() + 4, sizeof(header));
|
||||
std::memcpy(&address, storage.data() + 8, sizeof(address));
|
||||
if (emptyWords != 2 || header != 0xc0021000U || address != 0x87654321U) return 5;
|
||||
if (storage[12] != std::byte{0} || storage[19] != std::byte{0} || storage[20] != std::byte{0xaa}) return 6;
|
||||
|
||||
storage.fill(std::byte{0xaa});
|
||||
const auto zeroBytesWords = sceAgcDriverNopDataWritePacket(storage.data() + 4, 0x01020304, data.data(), 0);
|
||||
std::memcpy(&header, storage.data() + 4, sizeof(header));
|
||||
std::memcpy(&address, storage.data() + 8, sizeof(address));
|
||||
if (zeroBytesWords != 2 || header != 0xc0001000U || address != 0x01020304U) return 7;
|
||||
if (storage[12] != std::byte{0xaa}) return 8;
|
||||
return 0;
|
||||
}
|
||||
@@ -4804,11 +4804,14 @@ void idlePublishWaitTests(const Device& device, Recorder& recorder) {
|
||||
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");
|
||||
words[5 * unit] = 0x11;
|
||||
CollectWritesUncached(address, bytes);
|
||||
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");
|
||||
const bool pending = recorder.PendingWriteOverlaps(address + 5 * unit, unit);
|
||||
recorder.Sync();
|
||||
Require(words[5 * unit] == 0x55 && words[6 * unit - 1] == 0x55, "the label flush's publish did not land");
|
||||
Require(words[5 * unit] == 0x55 && words[6 * unit - 1] == 0x55, "the label flush's publish did not land: the unit read as written after its retile, so the publish dropped it as stale");
|
||||
Require(pending, "a publish of a flush other than the idle one was waited");
|
||||
std::cout << "idle publish wait verified\n";
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_SAVEDEXECMASKLOOP_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_SAVEDEXECMASKLOOP_HPP
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace AgcTest {
|
||||
|
||||
inline std::vector<std::uint32_t> SavedExecMaskLoop(std::uint32_t rounds) {
|
||||
std::vector<std::uint32_t> code{0xbea2047eu, 0xbea40380u};
|
||||
for (std::uint32_t step = 0; step < rounds; ++step) code.insert(code.end(), {0xbefe0422u, 0x7c080080u, 0x8aa26a7eu});
|
||||
const auto branch = 0xbf850000u | static_cast<std::uint16_t>(2 - static_cast<int>(code.size() + 3));
|
||||
code.insert(code.end(), {0x80248124u, 0xbf0a8424u, branch, 0xbefe0422u});
|
||||
return code;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -21,9 +21,9 @@ void require(bool condition, const char* message) {
|
||||
|
||||
void setScreenshotDirectory(const std::filesystem::path& directory) {
|
||||
#ifdef _WIN32
|
||||
require(_putenv_s("ANYPS5_SCREENSHOTS", directory.string().c_str()) == 0, "set ANYPS5_SCREENSHOTS");
|
||||
require(_wputenv_s(L"ANYPS5_SCREENSHOTS", directory.wstring().c_str()) == 0, "set ANYPS5_SCREENSHOTS");
|
||||
#else
|
||||
require(::setenv("ANYPS5_SCREENSHOTS", directory.string().c_str(), 1) == 0, "set ANYPS5_SCREENSHOTS");
|
||||
require(::setenv("ANYPS5_SCREENSHOTS", directory.c_str(), 1) == 0, "set ANYPS5_SCREENSHOTS");
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ void rejectsInvalidFrames() {
|
||||
|
||||
int main() try {
|
||||
const auto root = std::filesystem::temp_directory_path() / ("anyps5_screenshot-" + std::to_string(std::random_device{}()));
|
||||
const auto directory = root / "nested";
|
||||
const auto directory = root / std::filesystem::path(u8"nested-a\u00e7\u00e3o-\u65e5\u672c");
|
||||
std::filesystem::remove_all(root);
|
||||
setScreenshotDirectory(directory);
|
||||
requestsMerge();
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "IntermediateRepresentation/IrBuilder.hpp"
|
||||
#include "IntermediateRepresentation/IrProgram.hpp"
|
||||
#include "SpirvBackend/SpirvAnalysis.hpp"
|
||||
#include "SavedExecMaskLoop.hpp"
|
||||
#if ANYPS5_ENABLE_SPIRV_TOOLS
|
||||
#include "SpirvBackend/SpirvOptimizer.hpp"
|
||||
#endif
|
||||
@@ -4088,6 +4089,18 @@ void verifyHalfWaveReduction() {
|
||||
{0u, 0x38000000u, 0x88000000u, spv::OpGroupNonUniformBitwiseOr, "half-wave reduction: the wave64 OR scan did not read lane 31 as a subgroup OR on 32 lanes"},
|
||||
}};
|
||||
for (const auto& reduction : scans) require(reductions(pixel(scan(reduction.identity, reduction.vector, reduction.scalar), 32u, true), reduction.reduce) == 1u, reduction.message);
|
||||
auto loopScan = scan(0xffffffffu, 0x26000000u, 0x83800000u);
|
||||
const auto savedMaskLoop = AgcTest::SavedExecMaskLoop(8u);
|
||||
loopScan.insert(loopScan.begin(), savedMaskLoop.begin(), savedMaskLoop.end());
|
||||
auto helperLoop = loopScan;
|
||||
helperLoop[0] = 0xbea204c1u;
|
||||
expectFailure([&] { static_cast<void>(pixel(helperLoop, 32u, true)); }, "v_readlane_b32 of lane 63 is outside the 32-lane host subgroup", "half-wave reduction: a loop initialized with helper lanes was proven to exclude them");
|
||||
auto upperHelperLoop = loopScan;
|
||||
upperHelperLoop[0] = 0xbea2037eu;
|
||||
upperHelperLoop.insert(upperHelperLoop.begin() + 1, 0xbea303c1u);
|
||||
expectFailure([&] { static_cast<void>(pixel(upperHelperLoop, 32u, true)); }, "v_readlane_b32 of lane 63 is outside the 32-lane host subgroup", "half-wave reduction: the low-word loop proof was reused for helper-inclusive upper lanes");
|
||||
require(reductions(pixel(loopScan, 32u, true), spv::OpGroupNonUniformUMin) == 1u, "half-wave reduction: a loop-carried saved EXEC mask exhausted the entry-lane proof on 32 lanes");
|
||||
require(halves(pixel(loopScan, 64u, true), spv::OpGroupNonUniformUMin, 0xffffffffu), "half-wave reduction: a loop-carried saved EXEC mask did not preserve the two half-wave UMin reductions");
|
||||
auto comparedScan = scan(0u, 0x38000000u, 0x88000000u);
|
||||
*std::find(comparedScan.begin(), comparedScan.end(), 0xbe98047eu) = 0x8a986a7eu;
|
||||
comparedScan.insert(comparedScan.begin(), 0x7c080080u);
|
||||
@@ -7587,6 +7600,24 @@ void verifyDynamicStorageMipChain() {
|
||||
require(ResourceMaterializer::TryRuntimeImageMode(image, allLevels, modes).mode.has_value(), "a dynamic-mip storage image with sixteen levels was rejected");
|
||||
}
|
||||
|
||||
void verifyArrayVolumeStoreMode() {
|
||||
using namespace ShaderRecompiler;
|
||||
const DescriptorValue descriptor{{0x11800100u, 0xc3800000u, 0x0003c003u, 0xd0900facu, 0u, 0x00700000u, 0u, 0u}, 8u};
|
||||
ImageResource store{};
|
||||
store.resourceClass = ImageResourceClass::Storage;
|
||||
store.dimension = RdnaImageDimension::Dim3D;
|
||||
store.written = true;
|
||||
store.fmaskCompatible = false;
|
||||
const auto modes = ResourceMaterializer::RuntimeImageModes(store);
|
||||
require(modes[ResourceMaterializer::RuntimeImageMode(store, descriptor, modes)].dimension == RdnaImageDimension::Dim2DArray, "3D image store on a 2D-array T# did not select an array runtime mode");
|
||||
store.read = true;
|
||||
expectFailure([&] { static_cast<void>(ResourceMaterializer::RuntimeImageMode(store, descriptor, ResourceMaterializer::RuntimeImageModes(store))); }, "incompatible with the static runtime image interface", "3D image load and store on a 2D-array T# was accepted");
|
||||
store.read = false;
|
||||
store.atomic = true;
|
||||
const DescriptorValue uintDescriptor{{0x11800100u, 0xc1400000u, 0x0003c003u, 0xd0900facu, 0u, 0x00700000u, 0u, 0u}, 8u};
|
||||
expectFailure([&] { static_cast<void>(ResourceMaterializer::RuntimeImageMode(store, uintDescriptor, ResourceMaterializer::RuntimeImageModes(store))); }, "incompatible with the static runtime image interface", "3D image atomic on a 2D-array T# was accepted");
|
||||
}
|
||||
|
||||
void verifySrgbDepthCompareModes() {
|
||||
using namespace ShaderRecompiler;
|
||||
for (const auto decodeFormats : {0u, 1u, 2u, 3u}) {
|
||||
@@ -7761,6 +7792,7 @@ int main(int argc, char** argv) {
|
||||
verifyFmaskImageMode();
|
||||
verifyCubeDepthCompareMode();
|
||||
verifyVolumeArrayLoadMode();
|
||||
verifyArrayVolumeStoreMode();
|
||||
verifyVolumeBiasTableRejection();
|
||||
verifyDynamicStorageMipChain();
|
||||
verifySrgbDepthCompareModes();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#include "SceTypes.hpp"
|
||||
|
||||
extern "C" void* APS5_VABI sceAgcDriverGetCoredumpAddress();
|
||||
extern "C" int APS5_VABI sceAgcDriverSetTargetRingForDiag();
|
||||
|
||||
int main() {
|
||||
if (sceAgcDriverGetCoredumpAddress() != nullptr) return 1;
|
||||
if (sceAgcDriverSetTargetRingForDiag() != static_cast<int>(0x8A6D0001)) return 2;
|
||||
return 0;
|
||||
}
|
||||
@@ -174,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));
|
||||
@@ -217,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);
|
||||
@@ -348,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);
|
||||
@@ -1450,8 +1534,10 @@ int main() {
|
||||
}
|
||||
#endif
|
||||
CheckSharedBlock();
|
||||
CheckStoredOverUsesPageEvidence();
|
||||
CheckStoredAllSince();
|
||||
CheckSharedBlockProofs();
|
||||
CheckRepeatedPossibleStores();
|
||||
CheckOwnStore();
|
||||
CheckUnwatch();
|
||||
CheckNewestStamp();
|
||||
|
||||
@@ -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");
|
||||
@@ -117,7 +120,7 @@ void Run(AgcDriver::VulkanDevice& device, std::uint32_t baseArray = BaseArray) {
|
||||
device.WaitIdle();
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
const float u = std::bit_cast<float>(Input[tid * 4u + 0u]);
|
||||
const float layer = std::clamp(std::nearbyint(std::bit_cast<float>(Input[tid * 4u + 1u])), 0.0f, static_cast<float>(Layers - 1u));
|
||||
const float layer = std::clamp(std::nearbyint(std::bit_cast<float>(Input[tid * 4u + 1u])), 0.0f, static_cast<float>(LastArray - baseArray));
|
||||
const auto x = static_cast<std::uint32_t>(std::clamp(static_cast<int>(std::floor(u * static_cast<float>(Width))), 0, static_cast<int>(Width) - 1));
|
||||
Expect(tid, "image_sample_lz on a window of a 1D array past maxImageArrayLayers", &Output[tid * 8u], Texel(x, baseArray + static_cast<std::uint32_t>(layer)));
|
||||
Expect(tid, "image_load on a window of a 1D array past maxImageArrayLayers", &Output[tid * 8u + 4u], Texel(Input[tid * 4u + 2u], baseArray + Input[tid * 4u + 3u]));
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Threads = 32;
|
||||
constexpr std::uint32_t Size = 4;
|
||||
constexpr std::uint32_t Depth = 2;
|
||||
constexpr std::uint32_t Format32UInt = 20;
|
||||
constexpr std::uint32_t Type2DArray = 13;
|
||||
constexpr std::uint32_t IdentitySwizzle = 0xfacu;
|
||||
constexpr std::size_t TexelBytes = 16384;
|
||||
constexpr std::size_t BlockBytes = 65536;
|
||||
|
||||
alignas(256) std::array<std::uint32_t, Threads * 4> Input{};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 10> StoreCode{
|
||||
0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0xf0201110, 0x00011714, 0x7e2c0282, 0xf0201110,
|
||||
0x00011714, 0xbf810000,
|
||||
};
|
||||
|
||||
class GuestBlock {
|
||||
public:
|
||||
GuestBlock() {
|
||||
#ifdef _WIN32
|
||||
block = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
#else
|
||||
block = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, BlockBytes));
|
||||
#endif
|
||||
Require(block != nullptr, "image store volume through array: cannot allocate the guest block");
|
||||
GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true);
|
||||
}
|
||||
|
||||
~GuestBlock() {
|
||||
GuestAllocations::Mutation().Remove(block);
|
||||
#ifdef _WIN32
|
||||
VirtualFree(block, 0, MEM_RELEASE);
|
||||
#else
|
||||
std::free(block);
|
||||
#endif
|
||||
}
|
||||
|
||||
GuestBlock(const GuestBlock&) = delete;
|
||||
GuestBlock& operator=(const GuestBlock&) = delete;
|
||||
|
||||
std::uint8_t* Data() { return block; }
|
||||
|
||||
private:
|
||||
std::uint8_t* block = nullptr;
|
||||
};
|
||||
|
||||
std::string Hex(std::uint32_t value) {
|
||||
char text[16];
|
||||
std::snprintf(text, sizeof(text), "0x%08x", value);
|
||||
return text;
|
||||
}
|
||||
|
||||
std::uint32_t Value(std::uint32_t tid) {
|
||||
return (tid + 1u) * 0x9e3779b1u;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> Initial() {
|
||||
std::vector<std::uint8_t> bytes(TexelBytes);
|
||||
for (std::size_t index = 0; index < bytes.size(); ++index) bytes[index] = static_cast<std::uint8_t>(0x80u + 37u * index + 11u * (index >> 4u));
|
||||
return bytes;
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu};
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 8> ArrayDescriptor(const void* data) {
|
||||
const auto address = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(data));
|
||||
return {
|
||||
static_cast<std::uint32_t>(address >> 8u),
|
||||
static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((Size - 1u) & 3u) << 30u),
|
||||
((Size - 1u) >> 2u) | ((Size - 1u) << 14u),
|
||||
IdentitySwizzle | (Type2DArray << 28u),
|
||||
Depth - 1u,
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
};
|
||||
}
|
||||
|
||||
void Dispatch(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::size_t bytes) {
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
const std::array<std::uint32_t, 4> words{tid % Size, (tid / Size) % Size, tid / (Size * Size), Value(tid)};
|
||||
std::copy(words.begin(), words.end(), Input.begin() + tid * 4u);
|
||||
}
|
||||
const auto initial = Initial();
|
||||
std::copy(initial.begin(), initial.end(), texels);
|
||||
std::vector<std::uint32_t> userData(16, 0u);
|
||||
const auto buffer = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size() * 4u));
|
||||
const auto texture = ArrayDescriptor(texels);
|
||||
std::copy(buffer.begin(), buffer.end(), userData.begin());
|
||||
std::copy(texture.begin(), texture.end(), userData.begin() + 4);
|
||||
const std::span<const std::uint32_t> code(StoreCode);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1};
|
||||
ShaderRecompiler::RecompileRequest request{
|
||||
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
|
||||
{32, 0, userData, compute, std::nullopt, std::nullopt, memory},
|
||||
device.Target(),
|
||||
{0, 0, 0, 128}
|
||||
};
|
||||
request.useCache = false;
|
||||
const auto result = ShaderRecompiler::Recompile(request);
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
|
||||
device.WaitIdle();
|
||||
AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast<std::uintptr_t>(texels), bytes, nullptr, "test");
|
||||
device.WaitIdle();
|
||||
}
|
||||
|
||||
void Check(const std::uint8_t* texels) {
|
||||
const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(ArrayDescriptor(texels)));
|
||||
const auto& mip = geometry.mips.at(0);
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
const auto x = tid % Size;
|
||||
const auto y = (tid / Size) % Size;
|
||||
const auto z = tid / (Size * Size);
|
||||
std::uint32_t actual = 0;
|
||||
std::memcpy(&actual, texels + geometry.GuestLayerOffset(z) + mip.tiledOffset + static_cast<std::uint64_t>(y) * mip.pitchBytes + x * 4u, 4u);
|
||||
Require(actual == Value(tid), "image_store 3d through a 2D-array T#: texel (" + std::to_string(x) + ", " + std::to_string(y) + ", " + std::to_string(z) + ") is " + Hex(actual) + ", expected " + Hex(Value(tid)));
|
||||
}
|
||||
const auto initial = Initial();
|
||||
Require(std::equal(texels + geometry.guestBytes, texels + TexelBytes, initial.begin() + static_cast<std::ptrdiff_t>(geometry.guestBytes)), "image_store 3d through a 2D-array T#: a slice past the array changed bytes after it");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
GuestBlock block;
|
||||
auto* texels = block.Data();
|
||||
const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(ArrayDescriptor(texels)));
|
||||
Require(geometry.guestBytes <= TexelBytes, "image store volume through array: the array does not fit the texel storage");
|
||||
Dispatch(*device, texels, geometry.guestBytes);
|
||||
Check(texels);
|
||||
std::puts("image store volume through array tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <cstdlib>
|
||||
#include <future>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
@@ -75,16 +76,29 @@ void AppendLabel(std::vector<std::uint32_t>& words, std::uint32_t* target, std::
|
||||
}
|
||||
|
||||
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>();
|
||||
std::uint32_t* target = nullptr;
|
||||
~Blocker() { release = true; }
|
||||
~Blocker() { Release(); }
|
||||
void Release() { state->release = true; }
|
||||
void Enqueue(std::vector<DrawPipeline::Range> writes = {}) {
|
||||
DrawPipeline::Queue0().Enqueue([this] {
|
||||
while (!release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
DrawPipeline::Queue0().Enqueue([state = state, target = target] {
|
||||
while (!state->release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
if (target != nullptr) std::atomic_ref<std::uint32_t>(*target).store(1);
|
||||
std::lock_guard gpuLock(AgcDriver::GuestMemory::GpuMutex());
|
||||
{ std::lock_guard gpuLock(AgcDriver::GuestMemory::GpuMutex()); }
|
||||
state->done = true;
|
||||
}, std::move(writes));
|
||||
}
|
||||
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>& words) {
|
||||
@@ -107,7 +121,7 @@ void ReleaseWhen(Blocker& blocker, std::size_t reached, std::size_t passed) {
|
||||
Require(now - start < std::chrono::seconds(20), "the queue worker never reached the fill");
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
blocker.release = true;
|
||||
blocker.Release();
|
||||
}
|
||||
|
||||
void Finish() {
|
||||
@@ -118,6 +132,11 @@ void Finish() {
|
||||
std::_Exit(1);
|
||||
}
|
||||
idle.get();
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
while (DrawPipeline::Queue0().Outstanding() != 0) {
|
||||
Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(20), "the draw pipeline kept an item after the queue went idle");
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast<std::uintptr_t>(Output.data()), sizeof(Output));
|
||||
AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast<std::uintptr_t>(Scratch.data()), sizeof(Scratch));
|
||||
}
|
||||
@@ -135,6 +154,7 @@ void InputsAfterQueuedLabels() {
|
||||
SubmitBehind(blocker, words);
|
||||
ReleaseWhen(blocker, 4, 5);
|
||||
Finish();
|
||||
blocker.Join();
|
||||
for (std::size_t i = 0; i < Output.size(); ++i) {
|
||||
const auto expected = i < 128 ? 0xababababu : 0xcafebabeu;
|
||||
Require(Output[i] == expected, "a fill read its control or pattern before the queued labels that write them, at " + std::to_string(i));
|
||||
@@ -156,6 +176,7 @@ void FillIntoFullPipeline() {
|
||||
SubmitBehind(blocker, words);
|
||||
ReleaseWhen(blocker, depth, depth + 1);
|
||||
Finish();
|
||||
blocker.Join();
|
||||
for (std::size_t i = 0; i + 2 < depth; ++i) Require(Labels[i] == i + 1, "a queued label was not stored");
|
||||
for (std::size_t i = 0; i < Output.size(); ++i) Require(Output[i] == (i < 256 ? 0x5a5a5a5au : 0xcafebabeu), "a fill behind a full draw pipeline did not land at " + std::to_string(i));
|
||||
}
|
||||
@@ -180,8 +201,9 @@ void SatisfiedWaitKeepsUnrelatedQueuedWork() {
|
||||
while (DrawPipeline::Queue0().Outstanding() < 2 && std::chrono::steady_clock::now() < deadline)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
const bool keptQueued = DrawPipeline::Queue0().Outstanding() >= 2;
|
||||
blocker.release = true;
|
||||
blocker.Release();
|
||||
Finish();
|
||||
blocker.Join();
|
||||
Require(keptQueued, "a satisfied WAIT_REG_MEM drained unrelated queued work");
|
||||
Require(marker == 1, "a label after a satisfied WAIT_REG_MEM did not execute");
|
||||
}
|
||||
@@ -211,11 +233,12 @@ void SatisfiedWaitDrainsOverlappingQueuedWork() {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
const bool drained = DrawPipeline::DrainRequestsByOpcode(0x3c) > drainsBefore;
|
||||
const bool labelQueuedEarly = DrawPipeline::Queue0().Outstanding() >= 2;
|
||||
blocker.release = true;
|
||||
blocker.Release();
|
||||
Require(submitted.wait_for(std::chrono::seconds(20)) == std::future_status::ready,
|
||||
"WAIT_REG_MEM submission did not finish after the overlapping write");
|
||||
submitted.get();
|
||||
Finish();
|
||||
blocker.Join();
|
||||
Require(drained && !labelQueuedEarly, "a satisfied WAIT_REG_MEM skipped an overlapping queued write");
|
||||
Require(marker == 1, "a label after an overlapping WAIT_REG_MEM did not execute");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
@@ -30,6 +31,14 @@
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
struct VulkanDeviceTestAccess {
|
||||
static Graphics::Context Context(const VulkanDevice& device) { return device.graphicsContext(); }
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
@@ -54,7 +63,12 @@ constexpr std::array<std::array<float, 4>, 3> HalfTriangle{{
|
||||
{-1.0f, -1.0f, 0.5f, 1.0f}, {1.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 1.0f, 0.5f, 1.0f}
|
||||
}};
|
||||
|
||||
alignas(256) std::array<std::array<std::uint32_t, 16>, 4> Programs{};
|
||||
constexpr std::array<std::array<float, 4>, 3> OtherHalfTriangle{{
|
||||
{1.0f, -1.0f, 0.5f, 1.0f}, {1.0f, 1.0f, 0.5f, 1.0f}, {-1.0f, 1.0f, 0.5f, 1.0f}
|
||||
}};
|
||||
|
||||
alignas(256) std::array<std::array<std::uint32_t, 16>, 8> Programs{};
|
||||
alignas(256) std::array<std::byte, 256 * 1024> DepthMemory{};
|
||||
std::size_t programCount = 0;
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) {
|
||||
@@ -76,6 +90,22 @@ std::span<const std::uint32_t> Program(std::uint32_t coverageVgpr) {
|
||||
return std::span<const std::uint32_t>(code.data(), at);
|
||||
}
|
||||
|
||||
std::span<const std::uint32_t> SampleIndexProgram() {
|
||||
Require(programCount < Programs.size(), "too many test programs");
|
||||
auto& code = Programs[programCount++];
|
||||
std::size_t at = 0;
|
||||
code[at++] = 0xd5480018u;
|
||||
code[at++] = 0x02111100u;
|
||||
code[at++] = 0x7e000c00u | (20u << 17u) | (256u + 24u);
|
||||
code[at++] = 0x7e000280u | (21u << 17u);
|
||||
code[at++] = 0x7e000280u | (22u << 17u);
|
||||
code[at++] = 0x7e0002f2u | (23u << 17u);
|
||||
code[at++] = 0xf800180fu;
|
||||
code[at++] = 0x17161514u;
|
||||
code[at++] = 0xbf810000u;
|
||||
return std::span<const std::uint32_t>(code.data(), at);
|
||||
}
|
||||
|
||||
struct Block {
|
||||
explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) {
|
||||
#ifdef _WIN32
|
||||
@@ -150,7 +180,7 @@ AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t
|
||||
return color;
|
||||
}
|
||||
|
||||
void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::uint32_t inputAddr, std::span<const std::uint32_t> pixelCode, std::span<const std::array<float, 4>, 3> triangle = Triangle, std::uint32_t rasterSamples = 1) {
|
||||
void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::uint32_t inputAddr, std::span<const std::uint32_t> pixelCode, std::span<const std::array<float, 4>, 3> triangle = Triangle, std::uint32_t rasterSamples = 1, const AgcDriver::Graphics::DepthTarget* depth = nullptr, bool perSample = false) {
|
||||
const auto target = device.Target();
|
||||
constexpr std::uint32_t waveSize = 64;
|
||||
std::vector<std::uint32_t> vertexUserData(4, 0u);
|
||||
@@ -171,6 +201,8 @@ void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarge
|
||||
pixel.inputAddr = inputAddr;
|
||||
pixel.ancillary = (inputAddr & ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary)) != 0u;
|
||||
pixel.hasPerspectiveCenterVgpr = (inputAddr & ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCenter)) != 0u;
|
||||
pixel.sampleShading = perSample;
|
||||
pixel.iteratesSamples = perSample;
|
||||
pixel.targetOutputMode.at(0) = Float32AbgrExport;
|
||||
pixel.targetExportMapping.fill(0xe4u);
|
||||
const std::vector<std::uint32_t> pixelUserData(8, 0u);
|
||||
@@ -207,7 +239,15 @@ void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarge
|
||||
state.blend = blend;
|
||||
state.blendConstants = {};
|
||||
state.samples = rasterSamples;
|
||||
state.mergedCoverage = rasterSamples > 1;
|
||||
state.mergedCoverage = rasterSamples > 1 && depth == nullptr;
|
||||
state.truncatedCoverage = rasterSamples > 1 && depth != nullptr;
|
||||
state.perSampleShading = perSample;
|
||||
if (depth != nullptr) {
|
||||
state.depth = *depth;
|
||||
state.depthTest = true;
|
||||
state.depthWrite = false;
|
||||
state.depthCompare = VK_COMPARE_OP_ALWAYS;
|
||||
}
|
||||
const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(triangle.size()), 0, 1, 0, false};
|
||||
device.Draw(state, draw, shaders);
|
||||
}
|
||||
@@ -273,6 +313,67 @@ bool ExpectMerged(AgcDriver::VulkanDevice& device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string ExpectTruncated(AgcDriver::VulkanDevice& device) {
|
||||
if ((AgcDriver::VulkanDeviceTestAccess::Context(device).limits.framebufferDepthSampleCounts & VK_SAMPLE_COUNT_2_BIT) == 0) return "the device has no two-sample depth targets (framebufferDepthSampleCounts)";
|
||||
constexpr std::size_t bytes = std::size_t{Width} * Height * 16u;
|
||||
AgcDriver::Graphics::DepthTarget depth{reinterpret_cast<std::uintptr_t>(DepthMemory.data()), 0u, {Width, Height}, VK_FORMAT_D32_SFLOAT, 0.0f, 0};
|
||||
depth.samples = 2;
|
||||
std::array<std::uint32_t, 4> written{};
|
||||
std::uint32_t untouched = 0;
|
||||
for (const auto* triangle : {&HalfTriangle, &OtherHalfTriangle}) {
|
||||
Block block(bytes);
|
||||
std::fill(block.data, block.data + bytes, std::uint8_t{0xff});
|
||||
const auto color = DecodeTarget(block, Rgba32FloatInfo);
|
||||
try {
|
||||
Draw(device, color, ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::SampleCoverage), Program(0u), *triangle, 2u, &depth);
|
||||
} catch (const std::exception& error) {
|
||||
if (std::string_view(error.what()).find("truncated coverage reduction") == std::string_view::npos) throw;
|
||||
return "the device lacks VK_AMD_mixed_attachment_samples and VK_NV_framebuffer_mixed_samples with truncated coverage reduction";
|
||||
}
|
||||
const auto stored = ReadBack(device, block.Address(), bytes);
|
||||
for (std::uint32_t y = 0; y < Height; ++y) {
|
||||
for (std::uint32_t x = 0; x < Width; ++x) {
|
||||
const auto* texel = stored.data() + (static_cast<std::size_t>(y) * Width + x) * 16u;
|
||||
if (std::all_of(texel, texel + 16, [](std::uint8_t byte) { return byte == 0xffu; })) {
|
||||
++untouched;
|
||||
continue;
|
||||
}
|
||||
std::array<float, 4> value{};
|
||||
std::memcpy(value.data(), texel, sizeof(value));
|
||||
const auto mask = static_cast<std::uint32_t>(value[0]);
|
||||
Require(value[0] == static_cast<float>(mask) && mask >= 1u && mask <= 3u && value[1] == 0.0f && value[2] == 0.0f && value[3] == 1.0f, "truncated coverage: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") holds (" + std::to_string(value[0]) + ", " + std::to_string(value[1]) + ", " + std::to_string(value[2]) + ", " + std::to_string(value[3]) + ")");
|
||||
++written[mask];
|
||||
}
|
||||
}
|
||||
}
|
||||
Require(written[2] == 0, "truncated coverage: " + std::to_string(written[2]) + " pixels that cover only sample 1 were written into the one-sample target");
|
||||
Require(written[1] != 0 && written[3] != 0 && untouched != 0, "truncated coverage: two half-covering triangles wrote " + std::to_string(written[3]) + " fully covered and " + std::to_string(written[1]) + " sample-0-only pixels and left " + std::to_string(untouched));
|
||||
std::uint32_t perSampleWritten = 0;
|
||||
std::uint32_t perSampleUntouched = 0;
|
||||
for (const auto* triangle : {&HalfTriangle, &OtherHalfTriangle}) {
|
||||
Block block(bytes);
|
||||
std::fill(block.data, block.data + bytes, std::uint8_t{0xff});
|
||||
const auto color = DecodeTarget(block, Rgba32FloatInfo);
|
||||
Draw(device, color, ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary), SampleIndexProgram(), *triangle, 2u, &depth, true);
|
||||
const auto stored = ReadBack(device, block.Address(), bytes);
|
||||
for (std::uint32_t y = 0; y < Height; ++y) {
|
||||
for (std::uint32_t x = 0; x < Width; ++x) {
|
||||
const auto* texel = stored.data() + (static_cast<std::size_t>(y) * Width + x) * 16u;
|
||||
if (std::all_of(texel, texel + 16, [](std::uint8_t byte) { return byte == 0xffu; })) {
|
||||
++perSampleUntouched;
|
||||
continue;
|
||||
}
|
||||
std::array<float, 4> value{};
|
||||
std::memcpy(value.data(), texel, sizeof(value));
|
||||
Require(value == std::array<float, 4>{0.0f, 0.0f, 0.0f, 1.0f}, "truncated coverage per sample: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") holds (" + std::to_string(value[0]) + ", " + std::to_string(value[1]) + ", " + std::to_string(value[2]) + ", " + std::to_string(value[3]) + "), not sample 0's output");
|
||||
++perSampleWritten;
|
||||
}
|
||||
}
|
||||
}
|
||||
Require(perSampleWritten == written[1] + written[3] && perSampleUntouched == untouched, "truncated coverage per sample: the draws wrote " + std::to_string(perSampleWritten) + " pixels where one invocation per pixel wrote " + std::to_string(written[1] + written[3]));
|
||||
return {};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
@@ -285,6 +386,7 @@ int main() {
|
||||
Expect(*device, PixelInputBit(PixelInput::PerspectiveCenter) | PixelInputBit(PixelInput::SampleCoverage), 2u, "SAMPLE_COVERAGE after the perspective center");
|
||||
Expect(*device, PixelInputBit(PixelInput::Ancillary) | PixelInputBit(PixelInput::SampleCoverage), 1u, "SAMPLE_COVERAGE after ANCILLARY");
|
||||
if (!ExpectMerged(*device)) std::puts("merged coverage not tested: the device lacks VK_NV_framebuffer_mixed_samples with merged coverage reduction");
|
||||
if (const auto skipped = ExpectTruncated(*device); !skipped.empty()) std::printf("truncated coverage not tested: %s\n", skipped.c_str());
|
||||
std::puts("pixel sample coverage tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "prx/libc/include/GuestArena.hpp"
|
||||
#include "prx/libc/include/GuestWriteWatch.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "Optimization/ResourceProgram.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
@@ -17,6 +20,8 @@
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -120,14 +125,31 @@ constexpr std::array<std::array<float, 4>, 3> Triangle{{
|
||||
alignas(256) std::array<std::uint32_t, Threads * Words> Buffer{};
|
||||
|
||||
struct Block {
|
||||
explicit Block(std::size_t bytes) : bytes(bytes) {
|
||||
explicit Block(std::size_t bytes, bool watched = false) : bytes(bytes), watched(watched) {
|
||||
#ifdef _WIN32
|
||||
data = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
if (watched) {
|
||||
data = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes));
|
||||
GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
}
|
||||
#else
|
||||
data = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, bytes));
|
||||
if (watched) {
|
||||
void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
Require(raw != MAP_FAILED, "cannot map the color target");
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(raw);
|
||||
const auto aligned = (begin + BlockBytes - 1) & ~(static_cast<std::uintptr_t>(BlockBytes) - 1);
|
||||
if (aligned != begin) munmap(raw, aligned - begin);
|
||||
if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast<void*>(aligned + bytes), begin + BlockBytes - aligned);
|
||||
data = reinterpret_cast<std::uint8_t*>(aligned);
|
||||
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, bytes));
|
||||
}
|
||||
#endif
|
||||
Require(data != nullptr, "cannot allocate the multisampled color target");
|
||||
Require(data != nullptr, "cannot allocate the color target");
|
||||
std::memset(data, 0, bytes);
|
||||
Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched");
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Add(data, bytes, true, true, true);
|
||||
}
|
||||
@@ -137,15 +159,26 @@ struct Block {
|
||||
mutation.Remove(data);
|
||||
}
|
||||
#ifdef _WIN32
|
||||
VirtualFree(data, 0, MEM_RELEASE);
|
||||
if (watched) {
|
||||
GuestArena::GuestArenaReset_nid_postfix(data, bytes);
|
||||
GuestArena::GuestArenaRelease_nid_postfix(data, bytes);
|
||||
} else {
|
||||
VirtualFree(data, 0, MEM_RELEASE);
|
||||
}
|
||||
#else
|
||||
std::free(data);
|
||||
if (watched) {
|
||||
munmap(data, bytes);
|
||||
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
std::free(data);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
Block(const Block&) = delete;
|
||||
Block& operator=(const Block&) = delete;
|
||||
std::uint64_t Address() const { return reinterpret_cast<std::uintptr_t>(data); }
|
||||
std::size_t bytes;
|
||||
bool watched;
|
||||
std::uint8_t* data = nullptr;
|
||||
};
|
||||
|
||||
@@ -214,7 +247,7 @@ AgcDriver::Graphics::State TargetState(const AgcDriver::Graphics::ColorTarget& c
|
||||
state.blends = {blend};
|
||||
state.blend = blend;
|
||||
state.blendConstants = {};
|
||||
state.samples = Samples;
|
||||
state.samples = color.samples;
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -349,19 +382,19 @@ void RequireStoreRefused(AgcDriver::VulkanDevice& device, const std::array<std::
|
||||
Require(refusal.find(expected) != std::string::npos, what + " was not refused: " + refusal);
|
||||
}
|
||||
|
||||
std::vector<std::array<float, 4>> RunLoad(AgcDriver::VulkanDevice& device, const std::array<std::uint32_t, 8>& texture, bool arrayed) {
|
||||
std::vector<std::array<float, 4>> RunLoad(AgcDriver::VulkanDevice& device, const std::array<std::uint32_t, 8>& texture, bool arrayed, std::uint32_t fragments) {
|
||||
std::vector<std::uint32_t> userData;
|
||||
const auto program = CompileLoad(device, texture, userData, arrayed);
|
||||
std::vector<std::array<float, 4>> loaded(Side * Side * Samples);
|
||||
std::vector<std::array<float, 4>> loaded(Side * Side * fragments);
|
||||
const auto codeAddress = reinterpret_cast<std::uintptr_t>((arrayed ? ArrayLoadCode : LoadCode).data());
|
||||
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 auto pixel = index / fragments;
|
||||
const std::array<std::uint32_t, 6> words = arrayed
|
||||
? std::array<std::uint32_t, 6>{0u, 0u, pixel % Side, pixel / Side, 0u, index % Samples}
|
||||
: std::array<std::uint32_t, 6>{0u, 0u, 0u, pixel % Side, pixel / Side, index % Samples};
|
||||
? std::array<std::uint32_t, 6>{0u, 0u, pixel % Side, pixel / Side, 0u, index % fragments}
|
||||
: std::array<std::uint32_t, 6>{0u, 0u, 0u, pixel % Side, pixel / Side, index % fragments};
|
||||
std::copy(words.begin(), words.end(), &Buffer[tid * Words]);
|
||||
}
|
||||
device.Dispatch(program, 1, 1, 1, {}, codeAddress);
|
||||
@@ -373,9 +406,9 @@ std::vector<std::array<float, 4>> RunLoad(AgcDriver::VulkanDevice& device, const
|
||||
return loaded;
|
||||
}
|
||||
|
||||
std::vector<std::array<float, 4>> LoadSamples(AgcDriver::VulkanDevice& device, const std::array<std::uint32_t, 8>& texture) {
|
||||
auto loaded = RunLoad(device, texture, false);
|
||||
const auto loadedArray = RunLoad(device, texture, true);
|
||||
std::vector<std::array<float, 4>> LoadSamples(AgcDriver::VulkanDevice& device, const std::array<std::uint32_t, 8>& texture, std::uint32_t fragments = Samples) {
|
||||
auto loaded = RunLoad(device, texture, false, fragments);
|
||||
const auto loadedArray = RunLoad(device, texture, true, fragments);
|
||||
Require(loadedArray == loaded, "Dim2DMsaaArray image_load disagreed with Dim2DMsaa image_load on the multisampled color target");
|
||||
return loaded;
|
||||
}
|
||||
@@ -643,12 +676,210 @@ int NativeVertexInput(std::unique_ptr<AgcDriver::VulkanDevice>& device) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 10> NativeFmaskCode{0x34020084u, 0xe0381000u, 0x80000201u, 0xbf8c3f70u, 0xf0001f08u, 0x00020602u, 0xbf8c3f70u, 0xe0781000u, 0x80010601u, 0xbf810000u};
|
||||
alignas(256) std::array<std::uint32_t, Threads * 4u> NativeFmaskInput{};
|
||||
alignas(256) std::array<std::uint32_t, Threads * 4u + 4u> NativeFmaskOutput{};
|
||||
|
||||
std::array<std::uint32_t, 8> NativeFmaskDescriptor(const Block& fmask, const Block& cmask, std::uint32_t pipeXor) {
|
||||
const auto address = fmask.Address() | pipeXor;
|
||||
const auto metadata = cmask.Address();
|
||||
return {static_cast<std::uint32_t>(address >> 8u), static_cast<std::uint32_t>(address >> 40u) | (166u << 20u) | (((Side - 1u) & 3u) << 30u), ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), 4u | (1u << 3u) | (4u << 6u) | (24u << 20u) | (9u << 28u), 0u, 0u, (1u << 19u) | (1u << 21u) | (static_cast<std::uint32_t>(metadata >> 8u) << 24u), static_cast<std::uint32_t>(metadata >> 16u)};
|
||||
}
|
||||
|
||||
ShaderRecompiler::RecompileResult CompileNativeFmask(AgcDriver::VulkanDevice& device, const std::array<std::uint32_t, 8>& image, bool registration) {
|
||||
std::vector<std::uint32_t> userData(16u);
|
||||
const auto buffer = [](const void* data, std::uint32_t bytes) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(data);
|
||||
return std::array<std::uint32_t, 4>{static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u) & 0xffffu, bytes, 0x31016facu};
|
||||
};
|
||||
const auto input = buffer(NativeFmaskInput.data(), sizeof(NativeFmaskInput));
|
||||
const auto output = buffer(NativeFmaskOutput.data(), Threads * 16u);
|
||||
std::copy(input.begin(), input.end(), userData.begin());
|
||||
std::copy(output.begin(), output.end(), userData.begin() + 4u);
|
||||
std::copy(image.begin(), image.end(), userData.begin() + 8u);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(NativeFmaskCode.data()), std::as_bytes(std::span(NativeFmaskCode))}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1u, 1u}, 0u, {false, false, false}, false, 1u};
|
||||
auto target = device.Target();
|
||||
target.registrationModules = registration;
|
||||
ShaderRecompiler::RecompileRequest request{{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(NativeFmaskCode.data()), NativeFmaskCode, 0u, {}}, {32u, 0u, userData, compute, std::nullopt, std::nullopt, memory}, target, {0u, 0u, 0u, 128u}};
|
||||
request.useCache = false;
|
||||
return ShaderRecompiler::Recompile(request);
|
||||
}
|
||||
|
||||
void DispatchNativeFmask(AgcDriver::VulkanDevice& device, const std::array<std::uint32_t, 8>& image, bool registration) {
|
||||
device.Dispatch(CompileNativeFmask(device, image, registration), 1u, 1u, 1u);
|
||||
device.WaitIdle();
|
||||
}
|
||||
|
||||
void NativeFmask(std::unique_ptr<AgcDriver::VulkanDevice>& device) {
|
||||
using namespace AgcDriver::Graphics;
|
||||
Block colorMemory(4u * BlockBytes, true);
|
||||
Block dcc(BlockBytes, true);
|
||||
Block cmask(BlockBytes, true);
|
||||
Block fmask(BlockBytes, true);
|
||||
Block unrelated(BlockBytes, true);
|
||||
struct ReleaseDevice {
|
||||
std::unique_ptr<AgcDriver::VulkanDevice>& value;
|
||||
~ReleaseDevice() { value.reset(); }
|
||||
} releaseDevice{device};
|
||||
constexpr std::uint32_t sentinel = 0xdeadbeefu;
|
||||
for (const auto pipeXor : {0u, 0xf00u}) {
|
||||
std::memset(colorMemory.data, 0, colorMemory.bytes);
|
||||
std::memset(dcc.data, 0, dcc.bytes);
|
||||
std::memset(cmask.data, 0xcc, cmask.bytes);
|
||||
std::memset(fmask.data, 0x11, fmask.bytes);
|
||||
AgcDriver::Registers cx{{0x318, static_cast<std::uint32_t>(colorMemory.Address() >> 8u)}, {0x31b, 0u}, {0x31c, Float16x4Info | 0x10006000u}, {0x31d, (3u << 12u) | (3u << 15u)}, {0x31e, 0u}, {0x31f, static_cast<std::uint32_t>(cmask.Address() >> 8u)}, {0x321, static_cast<std::uint32_t>((fmask.Address() | pipeXor) >> 8u)}, {0x325, static_cast<std::uint32_t>(dcc.Address() >> 8u)}, {0x390, static_cast<std::uint32_t>(colorMemory.Address() >> 40u)}, {0x398, static_cast<std::uint32_t>(cmask.Address() >> 40u)}, {0x3a0, static_cast<std::uint32_t>(fmask.Address() >> 40u)}, {0x3a8, static_cast<std::uint32_t>(dcc.Address() >> 40u)}, {0x3b0, ((Side - 1u) << 14u) | (Side - 1u)}, {0x3b8, TiledAttrib3}};
|
||||
const auto color = DecodeColorBuffer(cx, 0u);
|
||||
Require(color.samples == 8u && color.cmaskAddress == cmask.Address(), "native FMASK fixture lost its decoded target association");
|
||||
std::array<std::array<float, 4>, 8> expected{};
|
||||
for (std::uint32_t sample = 0u; sample < 8u; ++sample) {
|
||||
expected[sample] = {static_cast<float>(sample + 1u) / 8.0f, 0.25f, 0.5f, 1.0f};
|
||||
auto pixel = PixelCode;
|
||||
pixel[1] = std::bit_cast<std::uint32_t>(expected[sample][0]);
|
||||
pixel[3] = std::bit_cast<std::uint32_t>(expected[sample][1]);
|
||||
pixel[5] = std::bit_cast<std::uint32_t>(expected[sample][2]);
|
||||
pixel[7] = std::bit_cast<std::uint32_t>(expected[sample][3]);
|
||||
auto state = TargetState(color);
|
||||
state.quadSampleMask.fill(static_cast<std::uint16_t>(1u << sample));
|
||||
Draw(*device, state, 0u, 2u, pixel);
|
||||
}
|
||||
const auto native = LoadSamples(*device, TargetDescriptor(colorMemory, 3u), 8u);
|
||||
std::vector<std::uint32_t> covered;
|
||||
for (std::uint32_t pixel = 0; pixel < Side * Side; ++pixel) {
|
||||
bool all = true;
|
||||
for (std::size_t sample = 0; sample < 8u; ++sample) all = all && native[pixel * 8u + sample] == expected[sample];
|
||||
if (all) covered.push_back(pixel);
|
||||
}
|
||||
Require(!covered.empty(), "native FMASK fixture did not render eight distinct sample colors");
|
||||
for (std::uint32_t lane = 0; lane < Threads; ++lane) {
|
||||
const auto pixel = covered[lane % covered.size()];
|
||||
NativeFmaskInput[lane * 4u] = pixel % Side;
|
||||
NativeFmaskInput[lane * 4u + 1u] = pixel / Side;
|
||||
NativeFmaskInput[lane * 4u + 2u] = 0u;
|
||||
NativeFmaskInput[lane * 4u + 3u] = 0u;
|
||||
}
|
||||
const auto image = NativeFmaskDescriptor(fmask, cmask, pipeXor);
|
||||
const auto check = [&] {
|
||||
for (std::uint32_t lane = 0; lane < Threads; ++lane) {
|
||||
const std::array<std::uint32_t, 4> mapping{0x76543210u, 1u, 0x76543210u, 0u};
|
||||
for (std::size_t component = 0; component < 4u; ++component) Require(NativeFmaskOutput[lane * 4u + component] == mapping[component], "native FMASK load did not preserve its expanded sample indices and selectors");
|
||||
for (std::uint32_t sample = 0; sample < 8u; ++sample) {
|
||||
const auto fragment = NativeFmaskOutput[lane * 4u] >> (4u * sample) & 15u;
|
||||
const auto pixel = NativeFmaskInput[lane * 4u + 1u] * Side + NativeFmaskInput[lane * 4u];
|
||||
Require(native[pixel * 8u + fragment] == expected[sample], "native FMASK remapped a sampled color to another fragment");
|
||||
}
|
||||
}
|
||||
Require(std::all_of(NativeFmaskOutput.begin() + Threads * 4u, NativeFmaskOutput.end(), [](auto word) { return word == sentinel; }), "native FMASK wrote beyond its output");
|
||||
};
|
||||
for (const auto registration : {false, true}) {
|
||||
for (std::uint32_t repeat = 0; repeat < 2u; ++repeat) {
|
||||
NativeFmaskOutput.fill(sentinel);
|
||||
DispatchNativeFmask(*device, image, registration);
|
||||
check();
|
||||
}
|
||||
const auto program = CompileNativeFmask(*device, image, registration);
|
||||
const auto codeAddress = reinterpret_cast<std::uintptr_t>(NativeFmaskCode.data());
|
||||
std::shared_ptr<const AgcDriver::Recipe> recipe;
|
||||
NativeFmaskOutput.fill(sentinel);
|
||||
device->Dispatch(program, 1u, 1u, 1u, {}, codeAddress, nullptr, &recipe);
|
||||
device->WaitIdle();
|
||||
check();
|
||||
Require(recipe == nullptr, "native FMASK emitted a reusable recipe without an ownership proof");
|
||||
const auto prepared = device->PrepareDispatch(program, {}, Threads, codeAddress);
|
||||
Require(prepared != nullptr, "native FMASK did not prepare a deferred dispatch");
|
||||
NativeFmaskOutput.fill(sentinel);
|
||||
device->Dispatch(program, 1u, 1u, 1u, {}, codeAddress, prepared);
|
||||
device->WaitIdle();
|
||||
check();
|
||||
}
|
||||
Require(LoadSamples(*device, TargetDescriptor(colorMemory, 3u), 8u) == native, "FMASK sampling changed the native samples");
|
||||
Require(std::all_of(fmask.data, fmask.data + fmask.bytes, [](auto byte) { return byte == 0x11u; }) && std::all_of(cmask.data, cmask.data + cmask.bytes, [](auto byte) { return byte == 0xccu; }), "native FMASK sampling published or changed guest metadata");
|
||||
const auto refused = [&](const auto& descriptor, std::string_view reason) {
|
||||
NativeFmaskOutput.fill(sentinel);
|
||||
std::string error;
|
||||
try { DispatchNativeFmask(*device, descriptor, false); } catch (const std::runtime_error& e) { error = e.what(); }
|
||||
Require(error.find(reason) != std::string::npos, "native FMASK missing refusal: " + error);
|
||||
Require(std::all_of(NativeFmaskOutput.begin(), NativeFmaskOutput.end(), [sentinel](auto word) { return word == sentinel; }), "rejected native FMASK wrote output");
|
||||
};
|
||||
refused(NativeFmaskDescriptor(unrelated, cmask, pipeXor), "descriptor does not match");
|
||||
refused(NativeFmaskDescriptor(fmask, unrelated, pipeXor), "descriptor does not match");
|
||||
refused(NativeFmaskDescriptor(fmask, cmask, pipeXor ^ 0x100u), "descriptor does not match");
|
||||
auto resized = image;
|
||||
resized[2] += 1u;
|
||||
refused(resized, "extent or subresource does not match");
|
||||
const auto deferredProgram = CompileNativeFmask(*device, image, false);
|
||||
const auto codeAddress = reinterpret_cast<std::uintptr_t>(NativeFmaskCode.data());
|
||||
const auto preparedBeforeStore = device->PrepareDispatch(deferredProgram, {}, Threads, codeAddress);
|
||||
Require(preparedBeforeStore != nullptr, "native FMASK did not prepare its metadata invalidation probe");
|
||||
std::array<std::byte, 4> same{};
|
||||
same.fill(std::byte{0x11});
|
||||
AgcDriver::GuestMemory::Write(fmask.Address(), same);
|
||||
refused(image, "writes to a resident native FMASK");
|
||||
NativeFmaskOutput.fill(sentinel);
|
||||
std::string deferredError;
|
||||
try {
|
||||
device->Dispatch(deferredProgram, 1u, 1u, 1u, {}, codeAddress, preparedBeforeStore);
|
||||
device->WaitIdle();
|
||||
} catch (const std::runtime_error& error) {
|
||||
deferredError = error.what();
|
||||
}
|
||||
Require(deferredError.find("writes to a resident native FMASK") != std::string::npos, "deferred native FMASK missed its metadata store: " + deferredError);
|
||||
Require(std::all_of(NativeFmaskOutput.begin(), NativeFmaskOutput.end(), [](auto word) { return word == sentinel; }), "rejected deferred native FMASK wrote output");
|
||||
device->SubmitRecorded(true);
|
||||
device->WaitIdle();
|
||||
ClearMultisampleTargets(device->Device());
|
||||
ClearCachedTextures(device->Device());
|
||||
cx[0x31c] &= ~0x2000u;
|
||||
cx[0x31f] = 0u;
|
||||
cx[0x398] = 0u;
|
||||
const auto withoutCmask = DecodeColorBuffer(cx, 0u);
|
||||
Require(withoutCmask.cmaskAddress == 0u, "the no-CMASK control retained metadata");
|
||||
Draw(*device, TargetState(withoutCmask));
|
||||
const auto uncompressed = LoadSamples(*device, TargetDescriptor(colorMemory, 3u), 8u);
|
||||
Require(std::find(uncompressed.begin(), uncompressed.end(), Drawn) != uncompressed.end(), "tracking native FMASK broke ordinary DCC rendering without CMASK");
|
||||
device->SubmitRecorded(true);
|
||||
device->WaitIdle();
|
||||
ClearMultisampleTargets(device->Device());
|
||||
ClearCachedTextures(device->Device());
|
||||
Draw(*device, TargetState(color));
|
||||
auto recreated = color;
|
||||
recreated.pipeBankXor ^= 0x4000u;
|
||||
recreated.fmaskAddress = 0u;
|
||||
recreated.fmaskPipeBankXor = 0u;
|
||||
Draw(*device, TargetState(recreated));
|
||||
std::array<std::byte, 4> formerFmask{};
|
||||
formerFmask.fill(std::byte{0x22});
|
||||
AgcDriver::GuestMemory::Write(fmask.Address(), formerFmask);
|
||||
Draw(*device, TargetState(recreated));
|
||||
auto recreatedDescriptor = TargetDescriptor(colorMemory, 3u);
|
||||
recreatedDescriptor[0] |= recreated.pipeBankXor >> 8u;
|
||||
const auto recreatedSamples = LoadSamples(*device, recreatedDescriptor, 8u);
|
||||
Require(std::find(recreatedSamples.begin(), recreatedSamples.end(), Drawn) != recreatedSamples.end(), "former FMASK metadata affected the recreated native target");
|
||||
device->SubmitRecorded(true);
|
||||
device->WaitIdle();
|
||||
ClearMultisampleTargets(device->Device());
|
||||
ClearCachedTextures(device->Device());
|
||||
std::printf("native FMASK: XOR=0x%x, eight distinct samples, both registration modes, cached reuse, association/extent/write refusals and unchanged guest metadata passed\n", pipeXor);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
try {
|
||||
auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--native-fmask") {
|
||||
{
|
||||
const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device);
|
||||
if (!context.storageImageMultisample || (context.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_8_BIT) == 0) {
|
||||
std::puts("native FMASK skipped, the device does not render or store 8-sample color");
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
}
|
||||
NativeFmask(device);
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--native-vertex-input") return NativeVertexInput(device);
|
||||
Block block(4u * BlockBytes);
|
||||
Block other(BlockBytes);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
@@ -71,6 +62,11 @@ if(BUILD_TESTING)
|
||||
target_link_libraries(guest_ajm_vibration_tests PRIVATE libSceAjm.native libc)
|
||||
configure_windows_unwind(guest_ajm_vibration_tests)
|
||||
add_test(NAME guest_ajm_vibration COMMAND guest_ajm_vibration_tests)
|
||||
add_test_executable(guest_ajm_wvorbis_tests ${LIBS_INCLUDE_DIR}/tests/GuestAjmWVorbis.cpp)
|
||||
target_include_directories(guest_ajm_wvorbis_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_ajm_wvorbis_tests PRIVATE libSceAjm.native libc)
|
||||
configure_windows_unwind(guest_ajm_wvorbis_tests)
|
||||
add_test(NAME guest_ajm_wvorbis COMMAND guest_ajm_wvorbis_tests)
|
||||
if(UNIX)
|
||||
add_test_executable(guest_ajm_teardown_tests ${LIBS_INCLUDE_DIR}/tests/GuestAjmTeardown.cpp)
|
||||
target_include_directories(guest_ajm_teardown_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
@@ -79,7 +75,7 @@ if(BUILD_TESTING)
|
||||
add_test(NAME guest_ajm_teardown COMMAND guest_ajm_teardown_tests $<TARGET_FILE:libSceAjm.native>)
|
||||
endif()
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_ajm guest_ajm_vibration PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
set_tests_properties(guest_ajm guest_ajm_vibration guest_ajm_wvorbis PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceAjm.native>")
|
||||
endif()
|
||||
endif()
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "prx/libc/include/General.hpp"
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
// Entry points this library does not implement: reported before the unimplemented-call exception so an
|
||||
// APS5_TRACE_AJM run shows which one a title reached.
|
||||
@@ -43,6 +44,38 @@ static_assert(offsetof(WVorbisHeaderPacketParameter, setupPacket) == 32);
|
||||
static_assert(sizeof(WVorbisSetupPacketParameter) == 56);
|
||||
static_assert(offsetof(WVorbisSetupPacketParameter, input) == 16);
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int AJM_WVORBIS_ERROR_INVALID_PARAMETER = static_cast<int>(0x8A6E0001);
|
||||
constexpr int AJM_WVORBIS_ERROR_INVALID_SIZE = static_cast<int>(0x8A6E0002);
|
||||
constexpr int AJM_WVORBIS_ERROR_OUTPUT_TOO_SMALL = static_cast<int>(0x8A6E0003);
|
||||
constexpr int AJM_WVORBIS_ERROR_INVALID_SETUP = static_cast<int>(0x8A6E0004);
|
||||
constexpr std::size_t WVORBIS_LENGTH_TABLE_SIZE = 10;
|
||||
constexpr std::size_t VORBIS_IDENTIFICATION_SIZE = 30;
|
||||
constexpr std::size_t VORBIS_COMMENT_SIZE = 16;
|
||||
constexpr std::size_t WVORBIS_SETUP_MIN_SIZE = 9;
|
||||
constexpr std::size_t WVORBIS_SETUP_PART1_LENGTH_OFFSET = 7;
|
||||
constexpr std::int32_t VORBIS_BLOCK_SIZE_MIN = 64;
|
||||
constexpr std::int32_t VORBIS_BLOCK_SIZE_MAX = 8192;
|
||||
constexpr std::uint8_t VORBIS_MAGIC[] {'v', 'o', 'r', 'b', 'i', 's'};
|
||||
|
||||
std::int32_t WVorbisBlockSize(std::int32_t log2) {
|
||||
return static_cast<std::int32_t>(1u << (static_cast<std::uint32_t>(log2) & 31u));
|
||||
}
|
||||
|
||||
std::uint8_t* PutLittle(std::uint8_t* out, std::uint32_t value, std::size_t bytes) {
|
||||
for (std::size_t i = 0; i < bytes; ++i) out[i] = static_cast<std::uint8_t>(value >> (8 * i));
|
||||
return out + bytes;
|
||||
}
|
||||
|
||||
std::uint8_t* PutVorbisHeaderStart(std::uint8_t* out, std::uint8_t type) {
|
||||
*out++ = type;
|
||||
std::memcpy(out, VORBIS_MAGIC, sizeof(VORBIS_MAGIC));
|
||||
return out + sizeof(VORBIS_MAGIC);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -63,8 +96,45 @@ const char* APS5_VABI sceAjmStrError(int error) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAjmDecWVorbisCreateHeaderPacket(WVorbisHeaderPacketParameter* parameter) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
int APS5_VABI sceAjmDecWVorbisCreateHeaderPacket(WVorbisHeaderPacketParameter* param) {
|
||||
if (param == nullptr) return AJM_WVORBIS_ERROR_INVALID_PARAMETER;
|
||||
if (param->size != sizeof(WVorbisHeaderPacketParameter)) return AJM_WVORBIS_ERROR_INVALID_SIZE;
|
||||
if (param->bytesPerSecond < 0) return AJM_WVORBIS_ERROR_INVALID_PARAMETER;
|
||||
if (WVorbisBlockSize(param->shortBlockExponent) < VORBIS_BLOCK_SIZE_MIN) return AJM_WVORBIS_ERROR_INVALID_PARAMETER;
|
||||
if (param->longBlockExponent < param->shortBlockExponent || WVorbisBlockSize(param->longBlockExponent) > VORBIS_BLOCK_SIZE_MAX)
|
||||
return AJM_WVORBIS_ERROR_INVALID_PARAMETER;
|
||||
if (param->setupPacket == nullptr) return AJM_WVORBIS_ERROR_INVALID_PARAMETER;
|
||||
if (param->setupPacketSize < WVORBIS_SETUP_MIN_SIZE) return AJM_WVORBIS_ERROR_INVALID_SETUP;
|
||||
if (param->output == nullptr) return AJM_WVORBIS_ERROR_INVALID_PARAMETER;
|
||||
constexpr std::size_t headers = WVORBIS_LENGTH_TABLE_SIZE + VORBIS_IDENTIFICATION_SIZE + VORBIS_COMMENT_SIZE;
|
||||
if (param->outputCapacity < headers || param->setupPacketSize > param->outputCapacity - headers) return AJM_WVORBIS_ERROR_OUTPUT_TOO_SMALL;
|
||||
|
||||
const auto* setup = static_cast<const std::uint8_t*>(param->setupPacket);
|
||||
const auto part1 = static_cast<std::uint16_t>(setup[WVORBIS_SETUP_PART1_LENGTH_OFFSET] | setup[WVORBIS_SETUP_PART1_LENGTH_OFFSET + 1] << 8);
|
||||
auto* out = static_cast<std::uint8_t*>(param->output);
|
||||
out = PutLittle(out, 0, 2);
|
||||
out = PutLittle(out, VORBIS_IDENTIFICATION_SIZE, 2);
|
||||
out = PutLittle(out, VORBIS_COMMENT_SIZE, 2);
|
||||
out = PutLittle(out, part1, 2);
|
||||
out = PutLittle(out, static_cast<std::uint16_t>(param->setupPacketSize) - part1, 2);
|
||||
|
||||
out = PutVorbisHeaderStart(out, 1);
|
||||
out = PutLittle(out, 0, 4);
|
||||
out = PutLittle(out, static_cast<std::uint32_t>(param->channels), 1);
|
||||
out = PutLittle(out, param->sampleRate, 4);
|
||||
out = PutLittle(out, 0, 4);
|
||||
out = PutLittle(out, static_cast<std::uint32_t>(param->bytesPerSecond) * 8u, 4);
|
||||
out = PutLittle(out, 0, 4);
|
||||
*out++ = static_cast<std::uint8_t>((param->shortBlockExponent & 0xF) | (param->longBlockExponent & 0xF) << 4);
|
||||
*out++ = 1;
|
||||
|
||||
out = PutVorbisHeaderStart(out, 3);
|
||||
out = PutLittle(out, 0, 4);
|
||||
out = PutLittle(out, 0, 4);
|
||||
*out++ = 1;
|
||||
|
||||
std::memcpy(out, setup, param->setupPacketSize);
|
||||
param->outputSize = headers + param->setupPacketSize;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -851,14 +851,15 @@ std::uint32_t ReadBigEndian(const std::uint8_t* data, std::size_t bytes) {
|
||||
|
||||
int ParseMp3Header(const std::uint8_t* stream, std::uint32_t streamSize, int parseOfl, AjmDecMp3ParseFrame* frame);
|
||||
|
||||
void ParseMp3Ofl(const std::uint8_t* stream, std::uint32_t streamSize, std::uint32_t header, AjmDecMp3ParseFrame* frame) {
|
||||
void ParseMp3Ofl(const std::uint8_t* stream, std::uint32_t streamSize, std::uint32_t header, AjmDecMp3ParseFrame* frame, int parseOfl) {
|
||||
const std::size_t end = std::min<std::size_t>(streamSize, frame->frame_size);
|
||||
if (end <= 4) return;
|
||||
const std::size_t available = end - 4;
|
||||
const std::size_t sideInfoStart = 4 + (((header >> 16u) & 1u) == 0 ? 2u : 0u);
|
||||
if (end <= sideInfoStart) return;
|
||||
const std::size_t available = end - sideInfoStart;
|
||||
std::size_t bit = 0;
|
||||
const auto readBits = [&](std::uint32_t count) {
|
||||
std::uint32_t value = 0;
|
||||
for (std::uint32_t i = 0; i < count; ++i, ++bit) value = (value << 1u) | (bit / 8 < available ? (stream[4 + bit / 8] >> (7 - bit % 8)) & 1u : 0u);
|
||||
for (std::uint32_t i = 0; i < count; ++i, ++bit) value = (value << 1u) | (bit / 8 < available ? (stream[sideInfoStart + bit / 8] >> (7 - bit % 8)) & 1u : 0u);
|
||||
return value;
|
||||
};
|
||||
const bool mpeg1 = ((header >> 19u) & 3u) == 3u;
|
||||
@@ -872,11 +873,20 @@ void ParseMp3Ofl(const std::uint8_t* stream, std::uint32_t streamSize, std::uint
|
||||
}
|
||||
}
|
||||
bit += mainDataBits;
|
||||
std::size_t at = 4 + std::min<std::size_t>((bit + 7) / 8, available);
|
||||
std::size_t at = sideInfoStart + std::min<std::size_t>((bit + 7) / 8, available);
|
||||
|
||||
if (at + 8 <= end && (std::memcmp(stream + at, "Xing", 4) == 0 || std::memcmp(stream + at, "Info", 4) == 0)) {
|
||||
const std::uint8_t flags = stream[at + 7];
|
||||
std::size_t field = at + 8;
|
||||
std::size_t tagAt = at;
|
||||
if (tagAt + 8 > end || (std::memcmp(stream + tagAt, "Xing", 4) != 0 && std::memcmp(stream + tagAt, "Info", 4) != 0)) {
|
||||
const std::size_t fixedOffset = mpeg1 ? (mono ? 21u : 36u) : (mono ? 13u : 21u);
|
||||
tagAt = 0;
|
||||
if (fixedOffset + 8 <= end && (std::memcmp(stream + fixedOffset, "Xing", 4) == 0 || std::memcmp(stream + fixedOffset, "Info", 4) == 0)) {
|
||||
tagAt = fixedOffset;
|
||||
}
|
||||
}
|
||||
|
||||
if (tagAt != 0) {
|
||||
const std::uint8_t flags = stream[tagAt + 7];
|
||||
std::size_t field = tagAt + 8;
|
||||
std::uint32_t frames = 0;
|
||||
if (flags & 0x01u) {
|
||||
if (field + 4 > end) return;
|
||||
@@ -903,30 +913,34 @@ void ParseMp3Ofl(const std::uint8_t* stream, std::uint32_t streamSize, std::uint
|
||||
frame->encoder_delay = frame->samples_per_channel + delay + 529;
|
||||
frame->ofl_type = MP3_OFL_LAME;
|
||||
}
|
||||
} else if (at != 36 && end >= 40 && std::memcmp(stream + 36, "VBRI", 4) == 0) {
|
||||
NotImplemented_nid_no_patch("sceAjmDecMp3ParseFrame (VBRI header outside MPEG-1 stereo)");
|
||||
} else if (at + 26 <= end && std::memcmp(stream + at, "VBRI", 4) == 0) {
|
||||
frame->encoder_delay = ReadBigEndian(stream + at + 6, 2);
|
||||
return;
|
||||
}
|
||||
|
||||
const std::size_t vbriAt = end >= 40 && std::memcmp(stream + 36, "VBRI", 4) == 0 ? 36 : at;
|
||||
if (vbriAt + 26 <= end && std::memcmp(stream + vbriAt, "VBRI", 4) == 0) {
|
||||
frame->encoder_delay = ReadBigEndian(stream + vbriAt + 6, 2);
|
||||
frame->ofl_type = MP3_OFL_VBRI;
|
||||
if (frame->frame_size <= streamSize) {
|
||||
if (parseOfl == 1 && frame->frame_size <= streamSize) {
|
||||
AjmDecMp3ParseFrame next{};
|
||||
if (ParseMp3Header(stream + frame->frame_size, streamSize - static_cast<std::uint32_t>(frame->frame_size), 1, &next) == 0 && next.ofl_type == MP3_OFL_FGH) {
|
||||
if (ParseMp3Header(stream + frame->frame_size, streamSize - static_cast<std::uint32_t>(frame->frame_size), 2, &next) == 0 && next.ofl_type == MP3_OFL_FGH) {
|
||||
frame->encoder_delay += next.encoder_delay;
|
||||
frame->total_samples = next.total_samples;
|
||||
frame->ofl_type = MP3_OFL_VBRI_AND_FGH;
|
||||
}
|
||||
}
|
||||
} else if (at + 10 <= end) {
|
||||
while (at + 9 < end && stream[at] != 0xB4u) ++at;
|
||||
if (at + 9 >= end) return;
|
||||
std::uint8_t crc = 0xFF;
|
||||
for (std::size_t i = 0; i < 9; ++i) {
|
||||
for (int shift = 7; shift >= 0; --shift) {
|
||||
const bool same = ((crc >> 7u) & 1u) == ((stream[at + i] >> shift) & 1u);
|
||||
crc = static_cast<std::uint8_t>(same ? (crc << 1u) ^ 0x45u : crc << 1u);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
while (at + 9 < end && stream[at] != 0xB4u) ++at;
|
||||
if (at + 9 >= end) return;
|
||||
std::uint8_t crc = 0xFF;
|
||||
for (std::size_t i = 0; i < 9; ++i) {
|
||||
for (int shift = 7; shift >= 0; --shift) {
|
||||
const bool same = ((crc >> 7u) & 1u) == ((stream[at + i] >> shift) & 1u);
|
||||
crc = static_cast<std::uint8_t>(same ? (crc << 1u) ^ 0x45u : crc << 1u);
|
||||
}
|
||||
if (stream[at + 9] != crc) return;
|
||||
}
|
||||
if (stream[at + 9] == crc) {
|
||||
frame->encoder_delay = ReadBigEndian(stream + at + 1, 2);
|
||||
frame->total_samples = ReadBigEndian(stream + at + 3, 4);
|
||||
frame->ofl_type = MP3_OFL_FGH;
|
||||
@@ -944,13 +958,13 @@ int ParseMp3Header(const std::uint8_t* stream, std::uint32_t streamSize, int par
|
||||
};
|
||||
const std::uint32_t header = (std::uint32_t{stream[0]} << 24u) | (std::uint32_t{stream[1]} << 16u) | (std::uint32_t{stream[2]} << 8u) | stream[3];
|
||||
const std::uint32_t version = (header >> 19u) & 3u;
|
||||
const std::uint32_t layer = (header >> 17u) & 3u;
|
||||
const std::uint32_t sampleRate = sampleRates[version][(header >> 10u) & 3u];
|
||||
const std::uint32_t bitrate = kbps[version][(header >> 12u) & 15u] * 1000u;
|
||||
const bool valid = (header >> 21u) == 0x7FFu && sampleRate != 0 && bitrate != 0;
|
||||
const bool valid = (header >> 21u) == 0x7FFu && layer != 0 && sampleRate != 0 && bitrate != 0;
|
||||
AJM_TRACE("[ajm] parse mp3 frame %02x %02x %02x %02x (%u bytes, parse ofl %d) -> %s, %u Hz, %u bps\n", stream[0], stream[1], stream[2], stream[3], streamSize, parseOfl, valid ? "ok" : "invalid", sampleRate, bitrate);
|
||||
if (!valid) return SCE_AJM_ERROR_INVALID_PARAMETER;
|
||||
if (parseOfl && ((header >> 17u) & 3u) != 1u) NotImplemented_nid_no_patch("sceAjmDecMp3ParseFrame (original file length lookup outside layer III)");
|
||||
if (parseOfl && ((header >> 16u) & 1u) == 0) NotImplemented_nid_no_patch("sceAjmDecMp3ParseFrame (original file length lookup in a CRC-protected frame)");
|
||||
if (parseOfl && layer != 1u) return SCE_AJM_ERROR_INVALID_PARAMETER;
|
||||
const bool mpeg1 = version == 3;
|
||||
frame->frame_size = (mpeg1 ? 144u : 72u) * bitrate / sampleRate + ((header >> 9u) & 1u);
|
||||
frame->num_channels = ((header >> 6u) & 3u) == 3u ? 1u : 2u;
|
||||
@@ -961,7 +975,7 @@ int ParseMp3Header(const std::uint8_t* stream, std::uint32_t streamSize, int par
|
||||
frame->num_frames = 0;
|
||||
frame->total_samples = 0;
|
||||
frame->ofl_type = MP3_OFL_NONE;
|
||||
if (parseOfl) ParseMp3Ofl(stream, streamSize, header, frame);
|
||||
if (parseOfl) ParseMp3Ofl(stream, streamSize, header, frame, parseOfl);
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -845,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);
|
||||
}
|
||||
@@ -858,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;
|
||||
|
||||
@@ -313,9 +313,8 @@ int APS5_VABI sceAppContentRequestPatchInstall(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAppContentTemporaryDataMount(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
int APS5_VABI sceAppContentTemporaryDataMount(AppContentMountPoint* mountPoint) {
|
||||
return sceAppContentTemporaryDataMount2(TEMPORARY_DATA_OPTION_FORMAT, mountPoint);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -22,6 +22,15 @@ if(BUILD_TESTING)
|
||||
set_tests_properties(guest_audio3d_port PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceAudio3d>")
|
||||
endif()
|
||||
add_test_executable(guest_audio3d_speaker_array_tests ${LIBS_INCLUDE_DIR}/tests/GuestAudio3dSpeakerArray.cpp)
|
||||
target_include_directories(guest_audio3d_speaker_array_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_audio3d_speaker_array_tests PRIVATE libSceAudio3d libSceAudioOut libc)
|
||||
configure_windows_unwind(guest_audio3d_speaker_array_tests)
|
||||
add_test(NAME guest_audio3d_speaker_array COMMAND guest_audio3d_speaker_array_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_audio3d_speaker_array PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceAudio3d>")
|
||||
endif()
|
||||
add_test_executable(guest_audio3d_mixer_tests ${LIBS_INCLUDE_DIR}/tests/GuestAudio3dMixer.cpp)
|
||||
target_include_directories(guest_audio3d_mixer_tests PRIVATE
|
||||
${LIBS_INCLUDE_DIR} ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
@@ -472,29 +472,42 @@ int APS5_VABI sceAudio3dReportUnregisterHandler(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudio3dGetSpeakerArrayMemorySize(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
size_t APS5_VABI sceAudioOut2GetSpeakerArrayMemorySize(uint32_t num_speakers, uint8_t is_3d, uint8_t is_ambisonics);
|
||||
int APS5_VABI sceAudioOut2SpeakerArrayCreate(AudioOut2SpeakerArrayHandle* handle, const void* vbap_params, const void* ambi_params);
|
||||
int APS5_VABI sceAudioOut2SpeakerArrayDestroy(AudioOut2SpeakerArrayHandle handle);
|
||||
int APS5_VABI sceAudioOut2GetSpeakerArrayCoefficients(AudioOut2SpeakerArrayHandle handle, AudioOut2Position pos, float spread, float* coefficients, uint32_t num_coefficients, uint8_t height_aware, float downmix_spread_radius);
|
||||
|
||||
struct Audio3dSpeakerArrayParameters {
|
||||
const AudioOut2Position* positions;
|
||||
std::uint32_t numSpeakers;
|
||||
std::uint8_t is3d;
|
||||
void* memory;
|
||||
std::size_t memorySize;
|
||||
};
|
||||
|
||||
static_assert(sizeof(Audio3dSpeakerArrayParameters) == 0x20);
|
||||
|
||||
size_t APS5_VABI sceAudio3dGetSpeakerArrayMemorySize(uint32_t num_speakers, uint8_t is_3d) {
|
||||
return sceAudioOut2GetSpeakerArrayMemorySize(num_speakers, is_3d, 0);
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudio3dCreateSpeakerArray(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
int APS5_VABI sceAudio3dCreateSpeakerArray(AudioOut2SpeakerArrayHandle* handle, const Audio3dSpeakerArrayParameters* parameters) {
|
||||
if (!handle || !parameters) return AUDIO3D_ERROR_INVALID_PARAMETER;
|
||||
unsigned char extended[0x28] = {};
|
||||
std::memcpy(extended, parameters, sizeof(*parameters));
|
||||
return sceAudioOut2SpeakerArrayCreate(handle, extended, nullptr);
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudio3dDeleteSpeakerArray(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
int APS5_VABI sceAudio3dDeleteSpeakerArray(AudioOut2SpeakerArrayHandle handle) {
|
||||
return sceAudioOut2SpeakerArrayDestroy(handle);
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudio3dGetSpeakerArrayMixCoefficients(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
int APS5_VABI sceAudio3dGetSpeakerArrayMixCoefficients(AudioOut2SpeakerArrayHandle handle, AudioOut2Position pos, float spread, float* coefficients, uint32_t num_coefficients) {
|
||||
return sceAudioOut2GetSpeakerArrayCoefficients(handle, pos, spread, coefficients, num_coefficients, 0, 0.1f);
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudio3dGetSpeakerArrayMixCoefficients2(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
int APS5_VABI sceAudio3dGetSpeakerArrayMixCoefficients2(AudioOut2SpeakerArrayHandle handle, AudioOut2Position pos, float spread, float* coefficients, uint32_t num_coefficients, uint8_t height_aware, float downmix_spread_radius) {
|
||||
return sceAudioOut2GetSpeakerArrayCoefficients(handle, pos, spread, coefficients, num_coefficients, height_aware, downmix_spread_radius);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -191,9 +191,9 @@ int32_t APS5_VABI sceAudioInAsyncOpen(int32_t userId, uint32_t type, uint32_t in
|
||||
}
|
||||
|
||||
int32_t APS5_VABI sceAudioInIsPadEmbeddedMic(int32_t handle) {
|
||||
(void)handle;
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
std::lock_guard lock(g_mutex);
|
||||
if (!find(handle)) return AUDIO_IN_ERROR_INVALID_HANDLE;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int32_t APS5_VABI sceAudioInGetGain(int32_t handle, float* gain) {
|
||||
@@ -258,14 +258,14 @@ int APS5_VABI sceAudioInGetHandleStatusInfo(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioInOpenEx(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
int APS5_VABI sceAudioInOpenEx(int user_id, uint32_t type, uint32_t index, uint32_t extra, uint32_t len, uint32_t freq, uint32_t param) {
|
||||
(void)extra;
|
||||
return sceAudioInOpen(user_id, type, index, len, freq, param);
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioInHqOpenEx(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
int APS5_VABI sceAudioInHqOpenEx(int user_id, uint32_t type, uint32_t index, uint32_t extra, uint32_t len, uint32_t freq, uint32_t param) {
|
||||
(void)extra;
|
||||
return sceAudioInOpen(user_id, type, index, len, freq, param);
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioInDeviceOpenEx(void) {
|
||||
@@ -289,12 +289,10 @@ int APS5_VABI sceAudioInSetSparkSideTone(void) {
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioInSetCompressorPreGain(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioInSetNoiseGateThreshold(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,21 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
add_test_executable(audio_out_buffer_size_tests tests/AudioOutBufferSize.cpp src/AudioOut.cpp src/AudioOut2PadMix.cpp src/AudioOutShutdown.cpp)
|
||||
target_compile_definitions(audio_out_buffer_size_tests PRIVATE SDL_MAIN_HANDLED)
|
||||
target_include_directories(audio_out_buffer_size_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR})
|
||||
target_link_libraries(audio_out_buffer_size_tests PRIVATE libc libkernel libScePad)
|
||||
configure_windows_unwind(audio_out_buffer_size_tests)
|
||||
foreach(bufferSizeMode IN ITEMS pass_through wrap mix threshold)
|
||||
set(bufferSizeTest "audio_out_buffer_size_${bufferSizeMode}")
|
||||
add_test(NAME ${bufferSizeTest} COMMAND audio_out_buffer_size_tests ${bufferSizeMode})
|
||||
set_tests_properties(${bufferSizeTest} PROPERTIES TIMEOUT 5)
|
||||
if(WIN32)
|
||||
set_tests_properties(${bufferSizeTest} PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
add_test_executable(audio_out_vibration_tests tests/AudioOutVibration.cpp)
|
||||
target_include_directories(audio_out_vibration_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(audio_out_vibration_tests PRIVATE libSceAudioOut libScePad libc)
|
||||
|
||||
@@ -22,6 +22,10 @@ int APS5_VABI sceAudioOut2LoInit(void) {
|
||||
return sceAudioOut2Initialize();
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2LoTerminate(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2LoContextQueryMemory(const AudioOut2ContextParam* params, size_t* memory_size) {
|
||||
return sceAudioOut2ContextQueryMemory(params, memory_size);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,11 @@ int APS5_VABI sceAudioOut2MasteringInit(uint32_t flags) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2MasteringInitEx(uint32_t flags) {
|
||||
if ((flags & 0x7FFFFFFFu) != 0) NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2MasteringTerm(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ extern "C" {
|
||||
|
||||
int APS5_VABI sceAudioOut2SpeakerArrayCreate(AudioOut2SpeakerArrayHandle* handle, const void* vbap_params, const void* ambi_params) {
|
||||
const auto* params = static_cast<const GuestVbapParams*>(vbap_params);
|
||||
if (handle == nullptr || params == nullptr || ambi_params == nullptr || params->positions == nullptr ||
|
||||
if (handle == nullptr || params == nullptr || params->positions == nullptr ||
|
||||
params->numSpeakers == 0 || params->numSpeakers > MaxSpeakers || params->memory == nullptr ||
|
||||
params->memorySize < sizeof(SpeakerArray) || reinterpret_cast<std::uintptr_t>(params->memory) % alignof(SpeakerArray) != 0) {
|
||||
APS5_INVALID_ARG_EX;
|
||||
|
||||
@@ -15,6 +15,11 @@ int APS5_VABI sceAudioOut2Initialize(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2InitializeEx(std::uint32_t param) {
|
||||
(void)param;
|
||||
return sceAudioOut2Initialize();
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2Set3DLatency(int userId, std::uint32_t output) {
|
||||
if (output > MAX_SUPPORTED_3D_OUTPUT) return SCE_AUDIO_OUT2_ERROR_INVALID_PARAM;
|
||||
if (userId != 0 && (userId != 0xFF || output == 0)) return SCE_AUDIO_OUT2_ERROR_INVALID_PARAM;
|
||||
|
||||
@@ -365,11 +365,6 @@ int APS5_VABI sceAudioOut2GetSystemStateEx(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2MasteringInitEx(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2SetDebugHeadphoneMode(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
@@ -380,11 +375,6 @@ int APS5_VABI sceAudioOut2DebugStateCtrl(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2LoTerminate(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2ContextBedWrite(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
@@ -395,11 +385,6 @@ int APS5_VABI sceAudioOut2PortCreateEx(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2InitializeEx(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2Terminate(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
|
||||
extern "C" {
|
||||
int APS5_VABI sceAudioOut2Initialize();
|
||||
int APS5_VABI sceAudioOut2InitializeEx(std::uint32_t);
|
||||
int APS5_VABI sceAudioOut2LoInit();
|
||||
int APS5_VABI sceAudioOut2LoTerminate();
|
||||
int APS5_VABI sceAudioOut2Set3DLatency(int, std::uint32_t);
|
||||
int APS5_VABI sceAudioOut2MasteringInit(std::uint32_t);
|
||||
int APS5_VABI sceAudioOut2MasteringInitEx(std::uint32_t);
|
||||
int APS5_VABI sceAudioOut2MasteringTerm();
|
||||
int APS5_VABI sceAudioOut2MasteringSetParam(const void*, std::uint32_t, std::uint32_t);
|
||||
int APS5_VABI sceAudioOut2MasteringGetState(void*, std::uint32_t, std::uint64_t);
|
||||
@@ -68,6 +70,10 @@ void TestSet3DLatency() {
|
||||
void TestMasteringInit() {
|
||||
Require(sceAudioOut2MasteringInit(0) == 0, "flags 0 must be accepted");
|
||||
Require(ThrowsRuntimeError([] { sceAudioOut2MasteringInit(1); }), "flags 1 must throw");
|
||||
Require(sceAudioOut2MasteringInitEx(0) == 0, "InitEx flags 0 must be accepted");
|
||||
Require(sceAudioOut2MasteringInitEx(0x80000000u) == 0, "InitEx flag bit 31 must be accepted");
|
||||
Require(ThrowsRuntimeError([] { sceAudioOut2MasteringInitEx(1); }), "InitEx flag bit 0 must throw");
|
||||
Require(ThrowsRuntimeError([] { sceAudioOut2MasteringInitEx(0x40000000u); }), "InitEx flag bit 30 must throw");
|
||||
}
|
||||
|
||||
void TestMasteringTerm() {
|
||||
@@ -125,7 +131,9 @@ void TestEnableChat() {
|
||||
void TestLowLatencyService() {
|
||||
Require(sceAudioOut2LoInit() == 0, "starting the low-latency service must be accepted");
|
||||
Require(sceAudioOut2LoInit() == 0, "starting the low-latency service again must be accepted");
|
||||
Require(ThrowsRuntimeError([] { sceAudioOut2LoTerminate(); }), "stopping the low-latency service must throw");
|
||||
Require(sceAudioOut2LoTerminate() == 0, "stopping the low-latency service returns 0");
|
||||
Require(sceAudioOut2LoTerminate() == 0, "stopping a stopped low-latency service returns 0");
|
||||
Require(sceAudioOut2InitializeEx(0) == 0 && sceAudioOut2InitializeEx(1) == 0, "the extended initialization succeeds");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
#include "SDL.h"
|
||||
#include "prx/libSceAudioOut/include/AudioOutSdl.hpp"
|
||||
#include "prx/libc/include/general/VabiMacros.hpp"
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
extern "C" {
|
||||
int APS5_VABI sceAudioOutOpen(int, int, int, std::uint32_t, std::uint32_t, std::uint32_t);
|
||||
int APS5_VABI sceAudioOutClose(int);
|
||||
int APS5_VABI sceAudioOutOutput(int, const void*);
|
||||
}
|
||||
|
||||
namespace {
|
||||
constexpr SDL_AudioDeviceID device = 7;
|
||||
int opened = 0;
|
||||
int closed = 0;
|
||||
int queued = 0;
|
||||
std::uint32_t queuedBytes = 0;
|
||||
const void* supplied = nullptr;
|
||||
std::uint32_t suppliedBytes = 0;
|
||||
|
||||
void Require(bool value, const char* message) {
|
||||
if (!value) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
void RunPassThrough() {
|
||||
std::vector<float> samples(512u * 8u);
|
||||
for (std::size_t i = 0; i < samples.size(); ++i) samples[i] = static_cast<float>(i % 97u) / 97.0f;
|
||||
const int handle = sceAudioOutOpen(0, 0, 0, 512, 48000, 5);
|
||||
Require(handle > 0 && opened == 1, "a 512-frame F32 8-channel port did not open its device");
|
||||
supplied = samples.data();
|
||||
suppliedBytes = static_cast<std::uint32_t>(samples.size() * sizeof(float));
|
||||
Require(sceAudioOutOutput(handle, samples.data()) == 512, "a 512-frame output did not return 512");
|
||||
Require(queued == 1 && queuedBytes == 16384, "a 512-frame F32 8-channel output did not queue 16384 bytes");
|
||||
Require(sceAudioOutClose(handle) == 0 && closed == 1, "the port did not close its device");
|
||||
}
|
||||
|
||||
void RunOverflow(std::uint32_t frames, std::uint32_t format, std::uint32_t frameBytes) {
|
||||
std::vector<std::uint8_t> frame(frameBytes);
|
||||
const int handle = sceAudioOutOpen(0, 0, 0, frames, 48000, format);
|
||||
Require(handle > 0 && opened == 1, "a port with an oversized length did not open its device");
|
||||
supplied = frame.data();
|
||||
suppliedBytes = frameBytes;
|
||||
bool threw = false;
|
||||
try {
|
||||
static_cast<void>(sceAudioOutOutput(handle, frame.data()));
|
||||
} catch (const std::runtime_error&) {
|
||||
threw = true;
|
||||
}
|
||||
Require(threw, "an output whose buffer size overflows did not throw");
|
||||
Require(queued == 0, "an output whose buffer size overflows queued audio");
|
||||
Require(sceAudioOutClose(handle) == 0 && closed == 1, "the port did not close after the refused output");
|
||||
}
|
||||
}
|
||||
|
||||
int InitializeSdlAudio_nid_no_patch() { return 0; }
|
||||
extern "C" Uint32 SDLCALL SDL_WasInit(Uint32) { return SDL_INIT_AUDIO; }
|
||||
extern "C" int SDLCALL SDL_GetNumAudioDevices(int) { return 1; }
|
||||
extern "C" const char* SDLCALL SDL_GetAudioDeviceName(int, int) { return "Main speakers"; }
|
||||
extern "C" int SDLCALL SDL_GetAudioDeviceSpec(int, int, SDL_AudioSpec* spec) { *spec = {}; spec->channels = 2; return 0; }
|
||||
extern "C" const char* SDLCALL SDL_GetCurrentAudioDriver() { return "dummy"; }
|
||||
extern "C" const char* SDLCALL SDL_GetError() { return "test error"; }
|
||||
extern "C" SDL_AudioDeviceID SDLCALL SDL_OpenAudioDevice(const char* name, int capture, const SDL_AudioSpec* desired, SDL_AudioSpec* obtained, int) {
|
||||
Require(name == nullptr && capture == 0 && desired->callback == nullptr, "the main port did not open the default device");
|
||||
++opened;
|
||||
*obtained = *desired;
|
||||
return device;
|
||||
}
|
||||
extern "C" void SDLCALL SDL_PauseAudioDevice(SDL_AudioDeviceID id, int) { Require(id == device, "wrong device paused"); }
|
||||
extern "C" Uint32 SDLCALL SDL_GetQueuedAudioSize(SDL_AudioDeviceID id) { Require(id == device, "wrong queue queried"); return 0; }
|
||||
extern "C" int SDLCALL SDL_QueueAudio(SDL_AudioDeviceID id, const void* data, Uint32 bytes) {
|
||||
Require(id == device, "wrong queue fed");
|
||||
++queued;
|
||||
queuedBytes = bytes;
|
||||
if (bytes <= suppliedBytes) Require(std::memcmp(data, supplied, bytes) == 0, "the queued audio is not the supplied samples");
|
||||
return 0;
|
||||
}
|
||||
extern "C" void SDLCALL SDL_ClearQueuedAudio(SDL_AudioDeviceID id) { Require(id == device, "wrong queue cleared"); }
|
||||
extern "C" void SDLCALL SDL_CloseAudioDevice(SDL_AudioDeviceID id) { Require(id == device, "wrong device closed"); ++closed; }
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
try {
|
||||
Require(argc == 2, "invalid test arguments");
|
||||
const std::string_view mode = argv[1];
|
||||
if (mode == "pass_through") RunPassThrough();
|
||||
else if (mode == "wrap") RunOverflow(0x80000000u, 5, 8u * sizeof(float));
|
||||
else if (mode == "mix") RunOverflow(0x80000000u, 7, 8u * sizeof(float));
|
||||
else if (mode == "threshold") RunOverflow(0x40000000u, 0, sizeof(std::int16_t));
|
||||
else Require(false, "invalid buffer size mode");
|
||||
std::puts("audio out buffer size tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "%s\n", error.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ configure_windows_unwind(libSceAudioPropagation)
|
||||
if(BUILD_TESTING)
|
||||
add_test_executable(guest_audio_propagation_tests ${LIBS_INCLUDE_DIR}/tests/GuestAudioPropagation.cpp)
|
||||
target_include_directories(guest_audio_propagation_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_audio_propagation_tests PRIVATE libSceAudioPropagation libc)
|
||||
target_link_libraries(guest_audio_propagation_tests PRIVATE libSceAudioPropagation libc ${CMAKE_DL_LIBS})
|
||||
configure_windows_unwind(guest_audio_propagation_tests)
|
||||
add_test(NAME guest_audio_propagation COMMAND guest_audio_propagation_tests)
|
||||
if(WIN32)
|
||||
|
||||
@@ -380,56 +380,6 @@ int APS5_VABI sceAudioPropagationSystemQueryInfo(AudioPropagationHandle system,
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioPropagationAudioPathInit() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioPropagationAudioPathPointInit() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioPropagationMaterialInit() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioPropagationPortalSettingsInit() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioPropagationRayInit() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioPropagationSourceRenderInfoInit() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioPropagationSourceSetAudioPathsParamInit() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioPropagationSystem() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioPropagationSystemMemoryInit() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioPropagationSystemOptionInit() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioPropagationReportRegisterHandler(std::uint32_t category, ReportHandler handler, void* userData, ReportRegistration** registration) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
|
||||
@@ -21,10 +21,10 @@ constexpr int CES_ERROR_UNASSIGNED_CODE = static_cast<int>(0x805C0020);
|
||||
constexpr int CES_ERROR_INVALID_DST_BUFFER = static_cast<int>(0x805C0030);
|
||||
constexpr int CES_ERROR_DST_BUFFER_END = static_cast<int>(0x805C0031);
|
||||
|
||||
constexpr uint8_t PROFILES[3] = {0, 1, 2};
|
||||
constexpr const uint8_t* CP1252_PROFILE = &PROFILES[0];
|
||||
constexpr const uint8_t* CP1251_PROFILE = &PROFILES[1];
|
||||
constexpr const uint8_t* CP1250_PROFILE = &PROFILES[2];
|
||||
constexpr uint8_t PROFILES[3][8] = {{0}, {1}, {2}};
|
||||
constexpr const uint8_t* CP1252_PROFILE = PROFILES[0];
|
||||
constexpr const uint8_t* CP1251_PROFILE = PROFILES[1];
|
||||
constexpr const uint8_t* CP1250_PROFILE = PROFILES[2];
|
||||
|
||||
constexpr uint16_t CP1252_HIGH[32] = {
|
||||
0x20AC, 0x0081, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0x008D, 0x017D, 0x008F,
|
||||
|
||||
Loaded 100 of 470 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user