Compare commits

..

No commits in common. "main" and "feat/web-relinker-ui" have entirely different histories.

498 changed files with 2025 additions and 23499 deletions

View File

@ -14,5 +14,4 @@
- [ ] Generic behaviour, not specific to one title
- [ ] Depends on: <!-- #PR, or none -->
- [ ] Unblocks: <!-- title ID and Game bring-up discussion, e.g. PPSA01325 #3232, or none -->
- [ ] GPUs tested: <!-- for AGC, shader recompiler or VideoOut changes: vendor, model, driver and OS of each GPU the GPU tests ran on; otherwise n/a -->
- [ ] AI-assisted: <!-- yes / no -->

View File

@ -12,11 +12,6 @@ on:
description: Pull request number to build
required: true
type: string
linux:
description: Also build and test on Linux
required: false
default: true
type: boolean
permissions:
contents: read
@ -108,7 +103,7 @@ jobs:
}
const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number, per_page: 100 });
const paths = files.flatMap((file) => [file.filename, file.previous_filename].filter(Boolean));
const touches = (...prefixes) => files.length >= 3000 ||
const touches = (...prefixes) => manual || files.length >= 3000 ||
paths.some((path) => prefixes.some((prefix) => prefix.endsWith('/') ? path.startsWith(prefix) : path === prefix));
const workflow = '.github/workflows/build.yml';
const libc = 'core/libs/prx/libc/';
@ -219,7 +214,7 @@ jobs:
linux:
needs: eligibility
if: ${{ (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.linux)) && needs.eligibility.outputs.full == 'true' }}
if: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && needs.eligibility.outputs.full == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
@ -246,15 +241,6 @@ jobs:
evict-old-files: job
- name: Install lavapipe and the SDL audio and XRandR development files
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libvulkan1 mesa-vulkan-drivers libpipewire-0.3-dev libpulse-dev libasound2-dev libxrandr-dev
- id: mesa
run: |
echo "version=$(dpkg-query -W -f='${Version}' mesa-vulkan-drivers)" >> "$GITHUB_OUTPUT"
echo "cpu=$(grep -m1 '^flags' /proc/cpuinfo | sha256sum | cut -c1-12)" >> "$GITHUB_OUTPUT"
- uses: actions/cache/restore@v6
with:
path: .mesa-shader-cache
key: mesa-${{ steps.mesa.outputs.version }}-${{ steps.mesa.outputs.cpu }}-${{ github.sha }}
restore-keys: mesa-${{ steps.mesa.outputs.version }}-${{ steps.mesa.outputs.cpu }}-
- run: cmake -S . -B build $CONFIGURE -DANYPS5_ENABLE_SPIRV_TOOLS=ON
- name: Check that every test executable is registered with ctest
run: python3 tools/check_test_registration.py build
@ -264,12 +250,6 @@ jobs:
env:
ANYPS5_REQUIRE_VULKAN: 1
GALLIUM_OVERRIDE_CPU_CAPS: avx
MESA_SHADER_CACHE_DIR: ${{ github.workspace }}/.mesa-shader-cache
- if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && !cancelled() }}
uses: actions/cache/save@v6
with:
path: .mesa-shader-cache
key: mesa-${{ steps.mesa.outputs.version }}-${{ steps.mesa.outputs.cpu }}-${{ github.sha }}
- run: python3 tools/check_skips.py build/ctest.xml
- name: Report tests slower than 30 s
if: ${{ !cancelled() }}
@ -324,7 +304,7 @@ jobs:
save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
evict-old-files: job
- shell: cmd
run: cmake -S . -B build %CONFIGURE% "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown"
run: cmake -S . -B build %CONFIGURE%
- name: Check that every test executable is registered with ctest
shell: cmd
run: python tools/check_test_registration.py build

View File

@ -68,14 +68,6 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler)
add_test(NAME shader_cache_file_read COMMAND shader_cache_file_read_tests)
set_tests_properties(shader_cache_file_read PROPERTIES TIMEOUT 20)
add_test_executable(request_serializer_tests
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/RequestSerializerTests.cpp
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp)
target_include_directories(request_serializer_tests PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/ControlFlow/include)
add_test(NAME request_serializer COMMAND request_serializer_tests)
set_tests_properties(request_serializer PROPERTIES TIMEOUT 20)
find_package(Python3 QUIET COMPONENTS Interpreter)
if(Python3_Interpreter_FOUND)
add_test(NAME pr_overlap_encoding COMMAND ${Python3_EXECUTABLE}
@ -92,10 +84,6 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_encoding.py)
add_test(NAME progress_render_sliver COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_render_sliver.py)
add_test(NAME progress_wrappers COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_wrappers.py)
add_test(NAME progress_read_encoding COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_read_encoding.py)
add_test(NAME import_audit COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_import_audit.py)
add_test(NAME conventions_function_bodies COMMAND ${Python3_EXECUTABLE}
@ -112,16 +100,12 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_stubgen.py)
add_test(NAME debt_split COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_debt_split.py)
add_test(NAME stubgen_stdout_newlines COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_stubgen_stdout_newlines.py)
add_test(NAME hardware_oracle COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/hw-oracle/test_hw_oracle.py)
add_test(NAME pr_overlap_exports COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_exports.py)
add_test(NAME pr_overlap_wrapped_exports COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_wrapped_exports.py)
add_test(NAME pr_overlap_own_branch COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_own_branch.py)
add_test(NAME nid_database_cache COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_nid_names.py)
add_test(NAME release_package COMMAND ${Python3_EXECUTABLE}
@ -131,10 +115,6 @@ if(BUILD_TESTING)
set_tests_properties(pad_input PROPERTIES TIMEOUT 20)
add_test(NAME trace_digest COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_trace_digest.py)
add_test(NAME conventions_body_bom COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_body_bom.py)
add_test(NAME conventions_agent_files COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_agent_files.py)
endif()
endif()

View File

@ -60,7 +60,6 @@ The [pull request template](.github/pull_request_template.md) is the checklist f
- The PR overlap comment lists open pull requests that change the same lines or exports; agree with their authors which one goes first and declare it with `Depends on`.
- Run the tests before opening the pull request and describe what was tested (OS, title or homebrew).
- A pull request that claims a change in speed shows the summary lines of `main` and of the change, from the same machine and scene, and attaches the traces, captured as the [Performance report](.github/ISSUE_TEMPLATE/performance.yml) issue form describes. A slow scene is reported with the same form.
- Changes to the AGC driver, the shader recompiler or VideoOut name each GPU the GPU tests ran on (vendor, model, driver, OS) on the `GPUs tested` checklist line. CI runs these tests only on lavapipe, and hardware drivers fail where lavapipe and other vendors pass, so a reviewer can ask for a run on another vendor before merging.
- Investigation notes, reports, screenshots and logs go in the pull request, not in the repository. Images for documentation go in the [gist](https://gist.github.com/boykopovar/0e53f2e1426f29ecd41e3b51540b8a90) comments.
- Say whether the change was written with AI assistance. The author of the pull request is responsible for every line of it.

View File

@ -2,9 +2,7 @@
Tool for automatic executables porting to Linux and Windows.
[**Web converter**](https://boykopovar.github.io/AnyPS5/) - files stay on your device.
![anyps5_collage1](https://github.com/user-attachments/assets/0f8e13e8-1cb7-4c69-8075-39978e138994)
[**Online converter**](https://boykopovar.github.io/AnyPS5/) - files stay on your device.
[Discord](https://discord.gg/BHFztBPUe)

View File

@ -25,7 +25,7 @@ target_include_directories(nid_patcher PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/nid/i
target_compile_features(nid_patcher PRIVATE cxx_std_20)
if(MINGW)
target_link_options(nid_patcher PRIVATE -municode -static -static-libgcc -static-libstdc++)
target_link_options(nid_patcher PRIVATE -static -static-libgcc -static-libstdc++)
include(CheckCXXSourceCompiles)
set(CMAKE_REQUIRED_FLAGS -fno-asynchronous-unwind-tables)
check_cxx_source_compiles("#ifdef __USING_SJLJ_EXCEPTIONS__\n#error\n#endif\nint main() { return 0; }" ANYPS5_MINGW_DWARF_EXCEPTIONS)
@ -179,49 +179,23 @@ if(BUILD_TESTING)
set_tests_properties(guest_posix_scalar_io PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
endif()
add_test_executable(nid_buffer_validation_tests nid/tests/BufferValidation.cpp)
target_include_directories(nid_buffer_validation_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/nid/include)
target_compile_features(nid_buffer_validation_tests PRIVATE cxx_std_20)
if(MINGW)
target_compile_options(nid_buffer_validation_tests PRIVATE -fasynchronous-unwind-tables)
endif()
add_test(NAME nid_buffer_validation COMMAND nid_buffer_validation_tests)
set_tests_properties(nid_buffer_validation PROPERTIES TIMEOUT 10)
find_package(Python3 QUIET COMPONENTS Interpreter)
if(Python3_Interpreter_FOUND)
add_test(NAME nid_gnu_hash COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_gnu_hash.py $<TARGET_FILE:nid_patcher>)
add_test(NAME nid_patcher_macho COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_macho_nids.py $<TARGET_FILE:nid_patcher>)
add_test(NAME nid_patcher_native_paths COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_native_paths.py $<TARGET_FILE:nid_patcher>)
set_tests_properties(nid_patcher_native_paths PROPERTIES TIMEOUT 20)
endif()
add_test_executable(patched_libraries_tests tests/PatchedLibraries.cpp)
add_dependencies(patched_libraries_tests libs)
if(Python3_Interpreter_FOUND)
add_test(NAME nid_pe_references COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_pe_references.py $<TARGET_FILE:nid_patcher>)
endif()
if(MINGW)
target_compile_options(patched_libraries_tests PRIVATE -fasynchronous-unwind-tables)
target_link_options(patched_libraries_tests PRIVATE -static -static-libgcc -static-libstdc++)
endif()
add_test(NAME patched_libraries COMMAND patched_libraries_tests ${PATCHED_DIR})
if(Python3_Interpreter_FOUND)
add_test(NAME nid_elf_layout COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_elf_layout.py $<TARGET_FILE:nid_patcher>)
endif()
add_test_executable(guest_kernel_sys_tests tests/GuestKernelSys.cpp)
target_include_directories(guest_kernel_sys_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
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)
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
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel_sys>")
@ -245,10 +219,6 @@ if(BUILD_TESTING)
target_link_libraries(guest_offline_imports_tests PRIVATE libSceHttp libSceNpEntitlementAccess libSceRudp libc)
configure_windows_unwind(guest_offline_imports_tests)
add_test(NAME guest_offline_imports COMMAND guest_offline_imports_tests)
if(Python3_Interpreter_FOUND)
add_test(NAME nid_shared_strings COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_shared_strings.py $<TARGET_FILE:nid_patcher>)
endif()
if(WIN32)
set_tests_properties(guest_offline_imports PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceHttp>")
@ -441,16 +411,6 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libScePosixForWebKit>")
endif()
add_test_executable(MemoryPoolTest
tests/MemoryPoolTest.cpp
prx/libkernel/DirectMemory/MemoryPool.cpp
)
target_include_directories(MemoryPoolTest PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
if(MINGW)
target_compile_options(MemoryPoolTest PRIVATE -fasynchronous-unwind-tables)
endif()
add_test(NAME MemoryPoolTest COMMAND MemoryPoolTest)
add_test_executable(guest_acm_tests tests/GuestAcm.cpp)
target_include_directories(guest_acm_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_acm_tests PRIVATE libSceAcm libc)

View File

@ -1,15 +0,0 @@
#ifndef NID_NATIVEPATH_HPP
#define NID_NATIVEPATH_HPP
#include <filesystem>
#include <string>
namespace Nid::Internal {
inline std::filesystem::path NativePath(const std::string& path) {
return std::filesystem::path(std::u8string(path.begin(), path.end()));
}
}
#endif

View File

@ -61,7 +61,7 @@ inline std::string StripNidPostfix(const std::string& name) {
template<typename T>
T Read(const std::vector<std::uint8_t>& buf, std::size_t offset) {
if (offset > buf.size() || sizeof(T) > buf.size() - offset) throw std::runtime_error("read out of bounds");
if (offset + sizeof(T) > buf.size()) throw std::runtime_error("read out of bounds");
T v;
std::memcpy(&v, buf.data() + offset, sizeof(T));
return v;
@ -69,7 +69,7 @@ T Read(const std::vector<std::uint8_t>& buf, std::size_t offset) {
template<typename T>
void Write(std::vector<std::uint8_t>& buf, std::size_t offset, const T& v) {
if (offset > buf.size() || sizeof(T) > buf.size() - offset) throw std::runtime_error("write out of bounds");
if (offset + sizeof(T) > buf.size()) throw std::runtime_error("write out of bounds");
std::memcpy(buf.data() + offset, &v, sizeof(T));
}
@ -78,7 +78,6 @@ inline std::string ReadCStr(const std::vector<std::uint8_t>& buf, std::size_t of
std::string s;
while (offset < buf.size() && buf[offset] != 0u)
s.push_back(static_cast<char>(buf[offset++]));
if (offset == buf.size()) throw std::runtime_error("unterminated cstr");
return s;
}

View File

@ -15,11 +15,6 @@ namespace Nid {
namespace {
void RequireElfRange(const std::vector<std::uint8_t>& elf, std::uint64_t offset, std::uint64_t size, const char* table) {
if (offset > elf.size() || size > elf.size() - offset)
throw std::runtime_error(std::string("invalid ELF ") + table + " range");
}
std::uint32_t GnuHash(const std::string& name) {
std::uint32_t h = 5381u;
for (const unsigned char c : name)
@ -41,18 +36,6 @@ GnuHashLayout ReadGnuHashLayout(const std::vector<std::uint8_t>& elf, std::size_
return layout;
}
void ValidateGnuHash(const std::vector<std::uint8_t>& elf, std::size_t offset, std::size_t size, std::size_t symCount) {
if (offset > elf.size() || size > elf.size() - offset || size < 16u)
throw std::runtime_error("invalid gnu.hash section range");
const auto layout = ReadGnuHashLayout(elf, offset);
if (layout.SymOffset > symCount || !layout.BloomSize || (layout.BloomSize & (layout.BloomSize - 1u)) ||
layout.BloomShift >= 32u || (!layout.NBuckets && layout.SymOffset != symCount))
throw std::runtime_error("invalid gnu.hash geometry");
const std::uint64_t required = 16u + std::uint64_t{layout.BloomSize} * 8u +
std::uint64_t{layout.NBuckets} * 4u + (symCount - layout.SymOffset) * 4u;
if (required > size) throw std::runtime_error("gnu.hash section too small to rebuild in place");
}
void ReorderDynSymForGnuHash(
std::vector<std::uint8_t>& elf,
std::size_t dynSymOffset,
@ -221,65 +204,49 @@ void RemapDynamicNeededOffsets(
}
}
struct VersionNameField {
std::size_t Offset;
std::uint32_t Name;
};
std::vector<VersionNameField> ReadVersionNameFields(const std::vector<std::uint8_t>& elf, const Elf64_Ehdr& ehdr) {
using namespace Internal;
std::vector<VersionNameField> fields;
for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) {
const auto section = Read<Elf64_Shdr>(elf, static_cast<std::size_t>(ehdr.e_shoff) + i * sizeof(Elf64_Shdr));
if (section.sh_type != kShtGnuVerneed || section.sh_size == 0u) continue;
if (section.sh_offset > elf.size() || section.sh_size > elf.size() - section.sh_offset)
throw std::runtime_error("invalid .gnu.version_r section range");
const auto location = [&](std::uint64_t relative, std::size_t size) {
if (relative > section.sh_size || size > section.sh_size - relative)
throw std::runtime_error(".gnu.version_r record outside section");
return static_cast<std::size_t>(section.sh_offset + relative);
};
const auto advance = [&](std::uint64_t current, std::uint32_t step, std::size_t size) {
if (step < size || step > section.sh_size - current)
throw std::runtime_error("invalid .gnu.version_r record link");
return current + step;
};
std::uint64_t current = 0u;
while (true) {
const auto offset = location(current, sizeof(Elf64_Verneed));
const auto need = Read<Elf64_Verneed>(elf, offset);
if (need.vn_version != 1u) throw std::runtime_error("unsupported .gnu.version_r structure version");
fields.push_back({offset + offsetof(Elf64_Verneed, vn_file), need.vn_file});
if (need.vn_cnt != 0u) {
auto auxiliary = advance(current, need.vn_aux, sizeof(Elf64_Verneed));
for (std::uint16_t index = 0u; index < need.vn_cnt; ++index) {
const auto auxOffset = location(auxiliary, sizeof(Elf64_Vernaux));
const auto aux = Read<Elf64_Vernaux>(elf, auxOffset);
fields.push_back({auxOffset + offsetof(Elf64_Vernaux, vna_name), aux.vna_name});
if (index + 1u == need.vn_cnt) {
if (aux.vna_next != 0u) throw std::runtime_error(".gnu.version_r auxiliary count mismatch");
} else {
auxiliary = advance(auxiliary, aux.vna_next, sizeof(Elf64_Vernaux));
}
}
}
if (need.vn_next == 0u) break;
current = advance(current, need.vn_next, sizeof(Elf64_Verneed));
}
}
return fields;
}
void RemapVerneedOffsets(
std::vector<std::uint8_t>& elf,
const Elf64_Ehdr& ehdr,
const std::vector<std::pair<std::uint32_t, std::uint32_t>>& oldToNewOffset
) {
using namespace Internal;
for (const auto& field : ReadVersionNameFields(elf, ehdr)) {
const auto mapped = std::find_if(oldToNewOffset.begin(), oldToNewOffset.end(),
[&](const auto& entry) { return entry.first == field.Name; });
if (mapped != oldToNewOffset.end()) Write(elf, field.Offset, mapped->second);
for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) {
const std::size_t shOffset = static_cast<std::size_t>(ehdr.e_shoff) + i * sizeof(Elf64_Shdr);
const auto shdr = Read<Elf64_Shdr>(elf, shOffset);
if (shdr.sh_type != kShtGnuVerneed) continue;
const std::size_t verneedOffset = static_cast<std::size_t>(shdr.sh_offset);
std::size_t entryOffset = verneedOffset;
while (true) {
const auto verneed = Read<Elf64_Verneed>(elf, entryOffset);
const auto mappedFile = std::find_if(
oldToNewOffset.begin(), oldToNewOffset.end(),
[&](const auto& entry) { return entry.first == verneed.vn_file; }
);
if (mappedFile != oldToNewOffset.end())
Write(elf, entryOffset + offsetof(Elf64_Verneed, vn_file), mappedFile->second);
std::size_t auxOffset = entryOffset + verneed.vn_aux;
for (std::uint16_t a = 0u; a < verneed.vn_cnt; ++a) {
const auto vernaux = Read<Elf64_Vernaux>(elf, auxOffset);
const auto mappedName = std::find_if(
oldToNewOffset.begin(), oldToNewOffset.end(),
[&](const auto& entry) { return entry.first == vernaux.vna_name; }
);
if (mappedName != oldToNewOffset.end())
Write(elf, auxOffset + offsetof(Elf64_Vernaux, vna_name), mappedName->second);
if (vernaux.vna_next == 0u) break;
auxOffset += vernaux.vna_next;
}
if (verneed.vn_next == 0u) break;
entryOffset += verneed.vn_next;
}
}
}
@ -308,8 +275,35 @@ std::vector<std::uint32_t> CollectDynamicNameOffsets(const std::vector<std::uint
}
std::vector<std::uint32_t> CollectVerneedNameOffsets(const std::vector<std::uint8_t>& elf, const Elf64_Ehdr& ehdr) {
using namespace Internal;
std::vector<std::uint32_t> offsets;
for (const auto& field : ReadVersionNameFields(elf, ehdr)) offsets.push_back(field.Name);
for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) {
const std::size_t shOffset = static_cast<std::size_t>(ehdr.e_shoff) + i * sizeof(Elf64_Shdr);
const auto shdr = Read<Elf64_Shdr>(elf, shOffset);
if (shdr.sh_type != kShtGnuVerneed) continue;
std::size_t entryOffset = static_cast<std::size_t>(shdr.sh_offset);
while (true) {
const auto verneed = Read<Elf64_Verneed>(elf, entryOffset);
offsets.push_back(verneed.vn_file);
std::size_t auxOffset = entryOffset + verneed.vn_aux;
for (std::uint16_t a = 0u; a < verneed.vn_cnt; ++a) {
const auto vernaux = Read<Elf64_Vernaux>(elf, auxOffset);
offsets.push_back(vernaux.vna_name);
if (vernaux.vna_next == 0u) break;
auxOffset += vernaux.vna_next;
}
if (verneed.vn_next == 0u) break;
entryOffset += verneed.vn_next;
}
}
return offsets;
}
@ -322,9 +316,6 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
if (elf[4] != 2) throw std::runtime_error("not ELF64");
const auto ehdr = Read<Elf64_Ehdr>(elf, 0u);
if (ehdr.e_ident[5] != 1 || ehdr.e_shentsize != sizeof(Elf64_Shdr))
throw std::runtime_error("unsupported ELF byte order or section header size");
RequireElfRange(elf, ehdr.e_shoff, std::uint64_t{ehdr.e_shnum} * sizeof(Elf64_Shdr), "section table");
std::size_t dynSymOffset = 0u, dynSymSize = 0u;
std::size_t gnuHashOffset = 0u, gnuHashSize = 0u;
@ -335,9 +326,6 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
const std::size_t shOffset = static_cast<std::size_t>(ehdr.e_shoff) + i * sizeof(Elf64_Shdr);
const auto shdr = Read<Elf64_Shdr>(elf, shOffset);
if (shdr.sh_type == kShtDynsym) {
if (shdr.sh_entsize != sizeof(Elf64_Sym) || shdr.sh_size % sizeof(Elf64_Sym) || shdr.sh_link >= ehdr.e_shnum)
throw std::runtime_error("invalid ELF dynsym layout");
RequireElfRange(elf, shdr.sh_offset, shdr.sh_size, "dynsym");
dynSymOffset = static_cast<std::size_t>(shdr.sh_offset);
dynSymSize = static_cast<std::size_t>(shdr.sh_size);
dynSymSectionIndex = i;
@ -345,6 +333,7 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
if (shdr.sh_type == kShtGnuHash) {
gnuHashOffset = static_cast<std::size_t>(shdr.sh_offset);
gnuHashSize = static_cast<std::size_t>(shdr.sh_size);
gnuHashSymOffset = Read<std::uint32_t>(elf, gnuHashOffset + 4u);
}
if (shdr.sh_type == kShtGnuVersym)
versymOffset = static_cast<std::size_t>(shdr.sh_offset);
@ -356,10 +345,6 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
const std::size_t symCount = dynSymSize / sizeof(Elf64_Sym);
if (symCount == 0u) throw std::runtime_error(".dynsym is empty");
if (gnuHashOffset != 0u) {
ValidateGnuHash(elf, gnuHashOffset, gnuHashSize, symCount);
gnuHashSymOffset = Read<std::uint32_t>(elf, gnuHashOffset + 4u);
}
const std::size_t dynStrSectionLink = [&]() -> std::size_t {
for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) {
@ -374,8 +359,6 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
{
const std::size_t shOffset = static_cast<std::size_t>(ehdr.e_shoff) + dynStrSectionLink * sizeof(Elf64_Shdr);
const auto shdr = Read<Elf64_Shdr>(elf, shOffset);
if (shdr.sh_type != 3) throw std::runtime_error("invalid ELF dynstr section type");
RequireElfRange(elf, shdr.sh_offset, shdr.sh_size, "dynstr");
dynStrOffset = static_cast<std::size_t>(shdr.sh_offset);
dynStrSize = static_cast<std::size_t>(shdr.sh_size);
}
@ -444,7 +427,7 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
const auto addName = [&](std::uint32_t oldOffset, std::string value) {
const auto existing = std::find_if(
names.begin(), names.end(),
[&](const auto& entry) { return entry.first == oldOffset && entry.second == value; }
[&](const auto& entry) { return entry.first == oldOffset; }
);
if (existing == names.end()) names.emplace_back(oldOffset, std::move(value));
};
@ -480,9 +463,8 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
newDynStr.push_back(0u);
}
std::vector<std::pair<std::uint32_t, std::uint32_t>> preservedToNewOffset;
for (const auto offset : preservedOffsets)
preservedToNewOffset.emplace_back(offset, valueOffsets.at(ReadCStr(origDynStr, offset)));
std::vector<std::pair<std::uint32_t, std::uint32_t>> oldToNewOffset;
for (const auto& name : names) oldToNewOffset.emplace_back(name.first, valueOffsets.at(name.second));
if (newDynStr.size() > dynStrSize) {
throw std::runtime_error(
@ -493,15 +475,25 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
newDynStr.resize(dynStrSize, 0u);
std::vector<std::string> newSymbolNames(symCount);
for (const auto& use : uses) {
newSymbolNames[use.symIndex] = use.newValue;
Write(elf, dynSymOffset + use.symIndex * sizeof(Elf64_Sym) + offsetof(Elf64_Sym, st_name), valueOffsets.at(use.newValue));
for (std::size_t i = 1u; i < symCount; ++i) {
const std::size_t symOffset = dynSymOffset + i * sizeof(Elf64_Sym);
const auto sym = Read<Elf64_Sym>(elf, symOffset);
if (sym.st_name == 0u) continue;
const auto mapped = std::find_if(
oldToNewOffset.begin(), oldToNewOffset.end(),
[&](const auto& entry) { return entry.first == sym.st_name; }
);
if (mapped == oldToNewOffset.end()) continue;
newSymbolNames[i] = ReadCStr(newDynStr, mapped->second);
Write(elf, symOffset + offsetof(Elf64_Sym, st_name), mapped->second);
}
std::memcpy(elf.data() + dynStrOffset, newDynStr.data(), newDynStr.size());
RemapDynamicNeededOffsets(elf, ehdr, preservedToNewOffset);
RemapVerneedOffsets(elf, ehdr, preservedToNewOffset);
RemapDynamicNeededOffsets(elf, ehdr, oldToNewOffset);
RemapVerneedOffsets(elf, ehdr, oldToNewOffset);
if (gnuHashOffset != 0u) {
const auto bucketCount = Read<std::uint32_t>(elf, gnuHashOffset);

View File

@ -3,7 +3,6 @@
#include <nid/PeNidPatcher.hpp>
#include <nid/MachONidPatcher.hpp>
#include <nid/NidPatcherUtils.hpp>
#include <nid/NativePath.hpp>
#include <bit>
#include <cstring>
#include <fstream>
@ -50,12 +49,7 @@ std::unordered_set<std::string> ReadPeExports(const std::vector<std::uint8_t>& b
if (!directory.VirtualAddress || directory.Size < sizeof(PeExportDirectory)) throw std::runtime_error("missing reference PE exports");
const auto sections = optional + optionalSize;
RequireRange(binary, sections, std::uint64_t{sectionCount} * sizeof(PeSectionHeader));
const auto headersSize = ReadValue<std::uint32_t>(binary, optional + 60);
const auto mapRva = [&](std::uint32_t rva, std::uint64_t size) -> std::uint64_t {
if (rva < headersSize && size <= headersSize - rva) {
RequireRange(binary, rva, size);
return rva;
}
for (std::uint16_t index = 0; index < sectionCount; ++index) {
const auto section = ReadValue<PeSectionHeader>(binary, sections + std::uint64_t{index} * sizeof(PeSectionHeader));
if (rva < section.VirtualAddress) continue;
@ -70,20 +64,14 @@ std::unordered_set<std::string> ReadPeExports(const std::vector<std::uint8_t>& b
const auto table = ReadValue<PeExportDirectory>(binary, mapRva(directory.VirtualAddress, sizeof(PeExportDirectory)));
if (!table.NumberOfNames) throw std::runtime_error("reference PE has no named exports");
const auto names = mapRva(table.AddressOfNames, std::uint64_t{table.NumberOfNames} * 4);
const auto ordinals = mapRva(table.AddressOfNameOrdinals, std::uint64_t{table.NumberOfNames} * 2);
const auto functions = mapRva(table.AddressOfFunctions, std::uint64_t{table.NumberOfFunctions} * 4);
std::unordered_set<std::string> result;
for (std::uint32_t index = 0; index < table.NumberOfNames; ++index) {
const auto ordinal = ReadValue<std::uint16_t>(binary, ordinals + std::uint64_t{index} * 2);
if (ordinal >= table.NumberOfFunctions) throw std::runtime_error("invalid reference PE export ordinal");
if (!ReadValue<std::uint32_t>(binary, functions + std::uint64_t{ordinal} * 4)) continue;
const auto rva = ReadValue<std::uint32_t>(binary, names + std::uint64_t{index} * 4);
const auto offset = mapRva(rva, 1);
auto name = ReadName(binary, offset, binary.size() - offset);
mapRva(rva, name.size() + 1);
if (!result.insert(name).second) throw std::runtime_error("duplicate reference export: " + name);
}
if (result.empty()) throw std::runtime_error("reference PE has no named exports");
return result;
}
@ -122,7 +110,7 @@ std::string NormalizeExportName(const std::string& name) {
std::unordered_set<std::string> ReadExportExclusions(const std::string& path) {
if constexpr (std::endian::native != std::endian::little) throw std::runtime_error("export references require a little-endian host");
std::ifstream input(Internal::NativePath(path), std::ios::binary | std::ios::ate);
std::ifstream input(path, std::ios::binary | std::ios::ate);
if (!input) throw std::runtime_error("cannot open export reference: " + path);
const auto size = input.tellg();
if (size <= 0) throw std::runtime_error("empty or unreadable export reference: " + path);

View File

@ -1,6 +1,5 @@
#include <nid/BinaryPatcherFactory.hpp>
#include <nid/ExportExclusions.hpp>
#include <nid/NativePath.hpp>
#include <fstream>
#include <iostream>
#include <stdexcept>
@ -10,20 +9,20 @@
namespace {
std::vector<std::uint8_t> _readFile(const std::string& path) {
std::ifstream f(Nid::Internal::NativePath(path), std::ios::binary);
std::ifstream f(path, std::ios::binary);
if (!f) throw std::runtime_error("cannot open: " + path);
return {std::istreambuf_iterator<char>(f), {}};
}
void _writeFile(const std::string& path, const std::vector<std::uint8_t>& data) {
std::ofstream f(Nid::Internal::NativePath(path), std::ios::binary);
std::ofstream f(path, std::ios::binary);
if (!f) throw std::runtime_error("cannot write: " + path);
f.write(reinterpret_cast<const char*>(data.data()), static_cast<std::streamsize>(data.size()));
}
}
int Run(int argc, char* argv[]) {
int main(int argc, char* argv[]) {
if (argc < 3) {
std::cerr << "Usage: nid_patcher <library_name> [--preserve-exports <unpatched-library>] <file> [file2 ...]\n";
return 1;
@ -66,27 +65,3 @@ int Run(int argc, char* argv[]) {
return 0;
}
#ifdef _WIN32
int wmain(int argc, wchar_t* argv[]) {
try {
std::vector<std::string> values;
values.reserve(static_cast<std::size_t>(argc));
for (int index = 0; index < argc; ++index) {
const auto utf8 = std::filesystem::path(argv[index]).u8string();
values.emplace_back(utf8.begin(), utf8.end());
}
std::vector<char*> arguments;
arguments.reserve(values.size());
for (auto& value : values) arguments.push_back(value.data());
return Run(argc, arguments.data());
} catch (const std::exception& error) {
std::cerr << "FAIL: " << error.what() << '\n';
return 2;
}
}
#else
int main(int argc, char* argv[]) {
return Run(argc, argv);
}
#endif

View File

@ -1,60 +0,0 @@
#include <nid/NidPatcherUtils.hpp>
#include <cstdint>
#include <iostream>
#include <limits>
#include <stdexcept>
#include <vector>
namespace {
void Require(bool condition) {
if (!condition) throw std::runtime_error("NID buffer validation failed");
}
template<typename TAction>
void RequireRejected(TAction action) {
try { action(); }
catch (const std::runtime_error&) { return; }
throw std::runtime_error("Malformed NID buffer was accepted");
}
}
int main() {
try {
using namespace Nid::Internal;
const std::vector<std::uint8_t> unterminated{'n', 'a', 'm', 'e'};
RequireRejected([&] { ReadCStr(unterminated, 0); });
RequireRejected([&] { ReadCStr(unterminated, 3); });
const std::vector<std::uint8_t> strings{'a', 0, 'b', 'c', 0};
Require(ReadCStr(strings, 0) == "a");
Require(ReadCStr(strings, 1).empty());
Require(ReadCStr(strings, 2) == "bc");
Require(ReadCStr(strings, 4).empty());
RequireRejected([&] { ReadCStr(strings, strings.size()); });
RequireRejected([&] { ReadCStr({}, 0); });
std::vector<std::uint8_t> bytes(8, 0xa5);
const std::uint64_t value = 0x1020304050607080;
Write(bytes, 0, value);
Require(Read<std::uint64_t>(bytes, 0) == value);
Write<std::uint32_t>(bytes, 4, 0x12345678);
Require(Read<std::uint32_t>(bytes, 4) == 0x12345678);
const auto before = bytes;
for (const auto offset : {std::size_t{5}, std::size_t{8}, std::size_t{9},
std::numeric_limits<std::size_t>::max(), std::numeric_limits<std::size_t>::max() - 1,
std::numeric_limits<std::size_t>::max() - 3}) {
RequireRejected([&] { Read<std::uint32_t>(bytes, offset); });
RequireRejected([&] { Write<std::uint32_t>(bytes, offset, 0); });
Require(bytes == before);
}
RequireRejected([&] { Read<std::uint64_t>(bytes, 1); });
std::vector<std::uint8_t> empty;
RequireRejected([&] { Read<std::uint8_t>(empty, 0); });
RequireRejected([&] { Write<std::uint8_t>(empty, 0, 0); });
std::cout << "NID buffer validation passed\n";
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}

View File

@ -1,48 +0,0 @@
import struct
import sys
import unittest
from pathlib import Path
from test_symbol_versions import build_image, symbols, patch, SHOFF
class ElfLayoutTests(unittest.TestCase):
def test_declared_layout_must_match_supported_tables(self):
changes = ((5, "B", 2), (0x3a, "H", 32),
(SHOFF + 64 + 56, "Q", 8),
(SHOFF + 64 + 32, "Q", len(symbols(False)) * 24 + 1),
(SHOFF + 128 + 4, "I", 1))
for offset, kind, value in changes:
with self.subTest(offset=offset, value=value):
image = build_image(symbols(False), False)
struct.pack_into("<" + kind, image, offset, value)
self.reject(image)
def test_table_ranges_and_links_are_checked(self):
changes = ((0x28, "Q", 0x10000),
(SHOFF + 64 + 24, "Q", 0xff0),
(SHOFF + 64 + 40, "I", 100),
(SHOFF + 128 + 24, "Q", 0xff0),
(SHOFF + 128 + 32, "Q", 0xffffffffffffffff))
for offset, kind, value in changes:
with self.subTest(offset=offset, value=value):
image = build_image(symbols(False), False)
struct.pack_into("<" + kind, image, offset, value)
self.reject(image)
def reject(self, image):
result, after = patch(PATCHER, bytes(image), "layout.so")
self.assertEqual(result.returncode, 2, (result.returncode, result.stderr))
self.assertRegex(result.stderr.decode(), "ELF|dynsym|dynstr")
self.assertEqual(after, image)
def test_valid_supported_layout_still_patches(self):
image = build_image(symbols(False), False)
result, after = patch(PATCHER, bytes(image), "valid-layout.so")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertNotEqual(after, image)
if __name__ == "__main__":
PATCHER = Path(sys.argv.pop(1)).resolve()
unittest.main()

View File

@ -1,75 +0,0 @@
import struct
import sys
import unittest
from pathlib import Path
from test_symbol_versions import build_image, symbols, patch, HASH_OFFSET, SHOFF, read_dynsym, gnu_hash
class GnuHashTests(unittest.TestCase):
def image(self, buckets=4, bloom=1, shift=6, symoffset=3):
image = build_image(symbols(False), False)
count = len(symbols(False))
struct.pack_into("<IIII", image, HASH_OFFSET, buckets, symoffset, bloom, shift)
size = 16 + 8 * bloom + 4 * buckets + 4 * max(0, count - symoffset)
struct.pack_into("<Q", image, SHOFF + 3 * 64 + 32, size)
return image
def test_non_power_of_two_bloom_is_rejected(self):
self.reject(self.image(bloom=3))
def test_hash_shift_outside_word_is_rejected(self):
self.reject(self.image(shift=32))
def test_unsafe_geometry_is_rejected(self):
for fields in ({"bloom": 0}, {"buckets": 0}, {"symoffset": 100}):
with self.subTest(fields=fields):
self.reject(self.image(**fields))
def test_declared_section_extent_is_checked(self):
for size in (12, 16, 0x10000):
with self.subTest(size=size):
image = self.image()
struct.pack_into("<Q", image, SHOFF + 3 * 64 + 32, size)
self.reject(image)
def reject(self, image):
result, after = patch(PATCHER, bytes(image), "geometry.so")
self.assertEqual(result.returncode, 2, (result.returncode, result.stderr))
self.assertIn(b"gnu.hash", result.stderr)
self.assertEqual(after, image)
def test_rebuilt_hash_finds_every_export(self):
image = self.image(bloom=2)
result, after = patch(PATCHER, image, "valid.so")
self.assertEqual(result.returncode, 0, result.stderr)
entries = read_dynsym(after)
buckets, first, bloom, shift = struct.unpack_from("<4I", after, HASH_OFFSET)
bucket_offset = HASH_OFFSET + 16 + bloom * 8
chain_offset = bucket_offset + buckets * 4
for index in range(first, len(entries)):
name = entries[index][0]
hashed = gnu_hash(name)
word = struct.unpack_from("<Q", after, HASH_OFFSET + 16 + ((hashed // 64) % bloom) * 8)[0]
self.assertTrue(word & (1 << (hashed % 64)))
self.assertTrue(word & (1 << ((hashed >> shift) % 64)))
start = struct.unpack_from("<I", after, bucket_offset + (hashed % buckets) * 4)[0]
found = False
for candidate in range(start, len(entries)):
value = struct.unpack_from("<I", after, chain_offset + (candidate - first) * 4)[0]
if (value | 1) == (hashed | 1) and entries[candidate][0] == name:
found = True
if value & 1:
break
self.assertTrue(found, name)
def test_empty_hash_with_no_buckets_is_supported(self):
image = self.image(buckets=0, symoffset=len(symbols(False)))
result, after = patch(PATCHER, image, "empty.so")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(struct.unpack_from("<I", after, HASH_OFFSET)[0], 0)
if __name__ == "__main__":
PATCHER = Path(sys.argv.pop(1)).resolve()
unittest.main()

View File

@ -1,56 +0,0 @@
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from test_macho_nids import build_dylib, exports_of, nid
PATCHER = Path(sys.argv.pop(1)).resolve()
class NativePathTests(unittest.TestCase):
def invoke(self, *arguments):
return subprocess.run([str(PATCHER), "libtest", *map(str, arguments)],
capture_output=True, encoding="utf-8", errors="replace", timeout=10)
def test_non_ascii_directory_and_filename_patch_the_intended_file(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary) / "\u6e2c\u8a66"
root.mkdir()
file = root / "\u043c\u043e\u0434\u0443\u043b\u044c.prx"
file.write_bytes(build_dylib([("_puts_nid_postfix", 0, 0x100)], []))
result = self.invoke(file)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(exports_of(file.read_bytes()), {"_" + nid("puts"): (0, 0x100)})
self.assertIn(str(file), result.stdout)
self.assertEqual(list(root.iterdir()), [file])
def test_non_ascii_export_reference_preserves_the_named_export(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
reference = root / "\u6e2c\u8a66-reference.prx"
original = build_dylib([("_puts", 0, 0x100)], [])
reference.write_bytes(original)
file = root / "target.prx"
file.write_bytes(build_dylib([("_puts_nid_postfix", 0, 0x100), ("_exit_nid_postfix", 0, 0x108)], []))
result = self.invoke("--preserve-exports", reference, file)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(exports_of(file.read_bytes()), {"_puts_nid_postfix": (0, 0x100), "_" + nid("exit"): (0, 0x108)})
self.assertEqual(reference.read_bytes(), original)
def test_missing_unicode_input_does_not_modify_another_file(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
existing = root / "existing.prx"
existing.write_bytes(b"unchanged")
missing = root / "\u6e2c\u8a66-missing.prx"
result = self.invoke(missing)
self.assertEqual(result.returncode, 2)
self.assertIn(str(missing), result.stderr)
self.assertFalse(missing.exists())
self.assertEqual(existing.read_bytes(), b"unchanged")
if __name__ == "__main__":
unittest.main()

View File

@ -1,69 +0,0 @@
import struct
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from test_macho_nids import build_dylib, exports_of, nid
def reference(*, headers=False, address=0x2000, ordinal=0):
image = bytearray(1280)
image[:2] = b"MZ"
struct.pack_into("<I", image, 0x3c, 0x40)
struct.pack_into("<IHHIIIHH", image, 0x40, 0x4550, 0x8664, 2, 0, 0, 0, 240, 0x2022)
optional = 0x58
struct.pack_into("<H", image, optional, 0x20b)
struct.pack_into("<I", image, optional + 60, 512)
struct.pack_into("<I", image, optional + 108, 16)
start, rva = (416, 416) if headers else (512, 0x1000)
struct.pack_into("<II", image, optional + 112, rva, 80)
for index, (name, va, offset, size) in enumerate(((b".edata", 0x1000, 512, 256), (b".text", 0x2000, 768, 512))):
struct.pack_into("<8sIIIIIIHHI", image, optional + 240 + index * 40,
name, size, va, size, offset, 0, 0, 0, 0, 0x40000040)
struct.pack_into("<IIHHIIIIIII", image, start, 0, 0, 0, 0, rva + 50, 1, 1, 1, rva + 40, rva + 44, rva + 48)
struct.pack_into("<IIH", image, start + 40, address, rva + 50, ordinal)
name = b"puts_nid_postfix\0"
image[start + 50:start + 50 + len(name)] = name
return image
class ReferenceTests(unittest.TestCase):
def patch_with_reference(self, data):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
source, ref = root / "target.dylib", root / "reference.bin"
original = build_dylib([("_puts_nid_postfix", 0, 0x100), ("_exit_nid_postfix", 0, 0x108)], [])
source.write_bytes(original)
ref.write_bytes(data)
result = subprocess.run([str(PATCHER), "libc", str(source), "--preserve-exports", str(ref)], capture_output=True, text=True)
self.assertEqual(ref.read_bytes(), data)
return result, source.read_bytes(), original
def test_header_resident_export_tables(self):
result, image, _ = self.patch_with_reference(reference(headers=True))
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(exports_of(image), {"_puts_nid_postfix": (0, 0x100), "_" + nid("exit"): (0, 0x108)})
def test_section_resident_export_tables(self):
result, image, _ = self.patch_with_reference(reference())
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn("_puts_nid_postfix", exports_of(image))
def test_null_function_slot_does_not_preserve_a_name(self):
result, image, original = self.patch_with_reference(reference(address=0))
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn("no named exports", result.stdout + result.stderr)
self.assertEqual(image, original)
def test_invalid_name_ordinal_is_rejected_without_changing_target(self):
result, image, original = self.patch_with_reference(reference(ordinal=1))
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn("ordinal", result.stdout + result.stderr)
self.assertEqual(image, original)
if __name__ == "__main__":
PATCHER = Path(sys.argv.pop(1)).resolve()
unittest.main()

View File

@ -1,58 +0,0 @@
import struct
import sys
import unittest
from pathlib import Path
from test_symbol_versions import build_image, symbols, patch, SHOFF, DYNSYM_OFFSET, read_dynsym, sections, DEFINITIONS
class SharedStringTests(unittest.TestCase):
def test_dynamic_names_keep_original_string_when_export_shares_offset(self):
for tag in (1, 14, 15, 29):
with self.subTest(tag=tag):
data = build_image(symbols(False), False)
name_offset = struct.unpack_from("<I", data, DYNSYM_OFFSET + 3 * 24)[0]
struct.pack_into("<H", data, 0x3c, 5)
struct.pack_into("<IIQQQQIIQQ", data, SHOFF + 4 * 64, 0, 6, 2, 0, 0x580, 32, 2, 0, 8, 16)
struct.pack_into("<qQqQ", data, 0x580, tag, name_offset, 0, 0)
result, after = patch(PATCHER, bytes(data), "shared-dynamic.so")
self.assertEqual(result.returncode, 0, result.stderr)
strings = sections(after)[2]
new_offset = struct.unpack_from("<Q", after, 0x588)[0]
start = strings[4] + new_offset
self.assertEqual(after[start:after.index(0, start)].decode(), DEFINITIONS[0][0])
renamed = next(entry[0] for entry in read_dynsym(after) if entry[2] == DEFINITIONS[0][2])
self.assertNotEqual(renamed, DEFINITIONS[0][0])
def test_version_requirement_strings_keep_original_names(self):
data = build_image(symbols(False), False)
name_offset = struct.unpack_from("<I", data, DYNSYM_OFFSET + 3 * 24)[0]
struct.pack_into("<H", data, 0x3c, 5)
struct.pack_into("<IIQQQQIIQQ", data, SHOFF + 4 * 64, 0, 0x6ffffffe, 2, 0, 0x580, 32, 2, 1, 8, 0)
struct.pack_into("<HHIII", data, 0x580, 1, 1, name_offset, 16, 0)
struct.pack_into("<IHHII", data, 0x590, 0, 0, 2, name_offset, 0)
result, after = patch(PATCHER, bytes(data), "shared-version.so")
self.assertEqual(result.returncode, 0, result.stderr)
strings = sections(after)[2]
for field in (0x584, 0x598):
offset = struct.unpack_from("<I", after, field)[0]
start = strings[4] + offset
self.assertEqual(after[start:after.index(0, start)].decode(), DEFINITIONS[0][0])
renamed = next(entry[0] for entry in read_dynsym(after) if entry[2] == DEFINITIONS[0][2])
self.assertNotEqual(renamed, DEFINITIONS[0][0])
def test_local_symbol_keeps_name_shared_with_renamed_global(self):
entries = symbols(False)
entries[1] = (DEFINITIONS[0][0], 1, 9, 2, 0x1500)
data = build_image(entries, False)
data[DYNSYM_OFFSET + 24 + 4] = 2
result, after = patch(PATCHER, bytes(data), "shared-local.so")
self.assertEqual(result.returncode, 0, result.stderr)
names = {value: name for name, _, value in read_dynsym(after)}
self.assertEqual(names[0x1500], DEFINITIONS[0][0])
self.assertNotEqual(names[DEFINITIONS[0][2]], DEFINITIONS[0][0])
if __name__ == "__main__":
PATCHER = Path(sys.argv.pop(1)).resolve()
unittest.main()

View File

@ -1,76 +0,0 @@
import struct
import sys
import unittest
from pathlib import Path
from test_symbol_versions import build_image, symbols, patch, SHOFF, DYNSTR_OFFSET, sections
VERNEED = 0x580
def image(*, auxiliary=16, count=1, size=32, version=1, auxnext=0, nextrecord=0):
data = build_image(symbols(False), False)
strings = sections(data)[2]
filename = strings[5]
names = b"libfixture.so\0FIXTURE_1\0"
data[DYNSTR_OFFSET + filename:DYNSTR_OFFSET + filename + len(names)] = names
struct.pack_into("<Q", data, SHOFF + 2 * 64 + 32, filename + len(names))
struct.pack_into("<H", data, 0x3c, 5)
struct.pack_into("<IIQQQQIIQQ", data, SHOFF + 4 * 64, 0, 0x6ffffffe, 2, 0, VERNEED, size, 2, 1, 8, 0)
struct.pack_into("<HHIII", data, VERNEED, version, count, filename, auxiliary, nextrecord)
struct.pack_into("<IHHII", data, VERNEED + auxiliary, 0, 0, 2, filename + len(b"libfixture.so\0"), auxnext)
return data
class VersionRequirementsTests(unittest.TestCase):
def test_auxiliary_record_cannot_leave_declared_section(self):
self.reject(image(auxiliary=32))
def test_auxiliary_count_must_match_chain(self):
self.reject(image(count=2))
def test_unsupported_structure_version_is_rejected(self):
self.reject(image(version=2))
def test_invalid_forward_links_and_section_ranges_are_rejected(self):
for fields in ({"auxnext": 16}, {"nextrecord": 64}, {"size": 15},
{"size": 0x10000}, {"auxiliary": 8}, {"count": 2, "auxnext": 8}):
with self.subTest(fields=fields):
self.reject(image(**fields))
def test_valid_multiple_auxiliaries_and_requirements(self):
data = image(count=2, size=80, auxnext=16, nextrecord=48)
original_name = struct.unpack_from("<I", data, VERNEED + 24)[0]
original_file = struct.unpack_from("<I", data, VERNEED + 4)[0]
struct.pack_into("<IHHII", data, VERNEED + 32, 0, 0, 3, original_name, 0)
struct.pack_into("<HHIII", data, VERNEED + 48, 1, 1, original_file, 16, 0)
struct.pack_into("<IHHII", data, VERNEED + 64, 0, 0, 4, original_name, 0)
result, after = patch(PATCHER, bytes(data), "multiple-versions.so")
self.assertEqual(result.returncode, 0, result.stderr)
strings = sections(after)[2]
for position, expected in ((4, b"libfixture.so"), (24, b"FIXTURE_1"),
(40, b"FIXTURE_1"), (52, b"libfixture.so"), (72, b"FIXTURE_1")):
offset = struct.unpack_from("<I", after, VERNEED + position)[0]
start = strings[4] + offset
self.assertEqual(after[start:after.index(0, start)], expected)
def reject(self, data):
result, after = patch(PATCHER, bytes(data), "versions.so")
self.assertEqual(result.returncode, 2, (result.returncode, result.stderr))
self.assertIn(b"version_r", result.stderr)
self.assertEqual(after, data)
def test_valid_names_are_preserved_when_strings_are_rebuilt(self):
data = image()
result, after = patch(PATCHER, bytes(data), "valid-versions.so")
self.assertEqual(result.returncode, 0, result.stderr)
strings = sections(after)[2]
for position, expected in ((VERNEED + 4, b"libfixture.so"), (VERNEED + 16 + 8, b"FIXTURE_1")):
offset = struct.unpack_from("<I", after, position)[0]
start = strings[4] + offset
self.assertEqual(after[start:after.index(0, start)], expected)
if __name__ == "__main__":
PATCHER = Path(sys.argv.pop(1)).resolve()
unittest.main()

View File

@ -33,7 +33,6 @@ add_library(libSceAgc SHARED EXCLUDE_FROM_ALL
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/PacketInfo.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/Platform.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/RegisterDefaults.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/Semaphore.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/ShaderFusion.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/Suspend.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Patch/src/CondExec.cpp
@ -96,19 +95,6 @@ if(BUILD_TESTING)
endif()
endif()
add_test_executable(agc_semaphore_tests EXCLUDE_FROM_ALL tests/Semaphore.cpp)
target_include_directories(agc_semaphore_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_semaphore_tests PRIVATE libSceAgc libc)
configure_windows_unwind(agc_semaphore_tests)
if(BUILD_TESTING)
set_target_properties(agc_semaphore_tests PROPERTIES EXCLUDE_FROM_ALL FALSE)
add_test(NAME agc_semaphore_tests COMMAND agc_semaphore_tests)
if(WIN32)
set_tests_properties(agc_semaphore_tests PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_test_executable(agc_packet_info_tests EXCLUDE_FROM_ALL tests/PacketInfo.cpp)
target_include_directories(agc_packet_info_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_packet_info_tests PRIVATE libSceAgc libc)

View File

@ -36,6 +36,7 @@ std::uint32_t* APS5_VABI sceAgcDcbEventWrite(CommandBuffer* buf, std::uint8_t ev
Agc::Command::CheckAddress(guestAddress, 8, __func__);
return Agc::Command::Emit(buf, 0x46u, {0x100u | eventType, static_cast<std::uint32_t>(guestAddress), static_cast<std::uint32_t>(guestAddress >> 32u)}, __func__);
}
Agc::Command::Require(address == nullptr, __func__, "this event does not use an address");
const auto eventIndex = eventType == 7 || eventType == 15 || eventType == 16 ? 0x400u : 0u;
return Agc::Command::Emit(buf, 0x46u, {eventIndex | eventType}, __func__);
}

View File

@ -1,4 +0,0 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_SEMAPHORE_HPP
#define CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_SEMAPHORE_HPP
#endif

View File

@ -1,61 +0,0 @@
#include "prx/libSceAgc/Misc/include/Semaphore.hpp"
#include "prx/libSceAgc/Command/include/Packet.hpp"
#include <cstdint>
#include <cstddef>
#include <cstring>
#include <limits>
#include <mutex>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
namespace {
constexpr std::uint64_t semaphoreMemoryAlignment = 0x4000u;
constexpr std::uint64_t semaphoreLabelBytes = 32u;
constexpr std::uint64_t semaphoreCounterBytes = 8u;
std::mutex semaphoreMutex;
std::uint8_t* semaphoreMemory = nullptr;
std::uint64_t semaphoreBytes = 0;
bool hasOnlyZeroLabelTails(const std::uint8_t* memory, std::uint64_t sizeInBytes) {
for (std::uint64_t label = 0; label < sizeInBytes; label += semaphoreLabelBytes) {
for (std::uint64_t byte = semaphoreCounterBytes; byte < semaphoreLabelBytes; ++byte) {
if (memory[label + byte] != 0) return false;
}
}
return true;
}
}
extern "C" {
int APS5_VABI sceAgcSetAmmSemaphoreMemory(void* memory, std::uint64_t sizeInBytes) {
const auto address = reinterpret_cast<std::uintptr_t>(memory);
std::lock_guard lock(semaphoreMutex);
Agc::Command::Require(semaphoreMemory == nullptr, __func__, "the AMM semaphore memory is already registered; the console's error is unknown");
Agc::Command::Require(memory != nullptr && sizeInBytes != 0, __func__, "null or empty AMM semaphore memory; the console's error is unknown");
Agc::Command::Require(((address | sizeInBytes) & (semaphoreMemoryAlignment - 1u)) == 0, __func__, "AMM semaphore memory or size is not 16 KiB aligned; the console's error is unknown");
Agc::Command::Require(sizeInBytes - 1u <= std::numeric_limits<std::uintptr_t>::max() - address, __func__, "AMM semaphore memory wraps the address space");
auto* labels = static_cast<std::uint8_t*>(memory);
Agc::Command::Require(hasOnlyZeroLabelTails(labels, sizeInBytes), __func__, "AMM semaphore labels hold data past their first 8 bytes; whether registration clears it is unknown");
for (std::uint64_t label = 0; label < sizeInBytes; label += semaphoreLabelBytes) {
std::memset(labels + label, 0, semaphoreCounterBytes);
}
semaphoreMemory = labels;
semaphoreBytes = sizeInBytes;
return 0;
}
int APS5_VABI sceAgcGetSemaphoreLabel(std::uint32_t index, void** label) {
std::lock_guard lock(semaphoreMutex);
Agc::Command::Require(semaphoreMemory != nullptr, __func__, "no AMM semaphore memory is registered; the console's error is unknown");
Agc::Command::Require(index < semaphoreBytes / semaphoreLabelBytes, __func__, "the semaphore label lies outside the AMM semaphore memory; the console's error is unknown");
Agc::Command::Require(label != nullptr, __func__, "null semaphore label output; the console's error is unknown");
*label = semaphoreMemory + static_cast<std::uint64_t>(index) * semaphoreLabelBytes;
return 0;
}
}

View File

@ -35,6 +35,16 @@ int APS5_VABI sceAgcGetDefaultCxStateFlat() {
return 0;
}
int APS5_VABI sceAgcGetSemaphoreLabel() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcSetAmmSemaphoreMemory() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcSetSemaphoreMemory() {
NotImplemented_nid_no_patch(__func__);
return 0;

View File

@ -10,8 +10,6 @@
#include <cstring>
#include <string>
extern "C" std::uint32_t* APS5_VABI sceAgcDcbEventWrite(CommandBuffer* buf, std::uint8_t eventType, const volatile void* address);
extern "C" std::uint64_t APS5_VABI sceAgcDcbEventWriteGetSize(std::uint8_t eventType);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbResetQueue(CommandBuffer* buf, std::uint32_t op, std::uint32_t state);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbClearState(CommandBuffer* buf, std::uint32_t command);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetFlip(CommandBuffer* buf, std::uint32_t handle, std::int32_t index, std::uint32_t mode, std::int64_t argument);
@ -643,47 +641,6 @@ void testDefaults() {
expectFailure([] { Agc::Command::GetRegisterDefaults(0xffffffffu, true, __func__); });
}
void testEventWrite() {
Storage storage;
const auto* samplePacket = sceAgcDcbEventWrite(&storage.buffer, 0x38, reinterpret_cast<const void*>(0x1000));
check(samplePacket == storage.words.data(), "incorrect event 0x38 return pointer");
check(storage.words[0] == 0xc0024600u, "incorrect event 0x38 packet header");
check(storage.words[1] == (0x100u | 0x38u), "incorrect event 0x38 first payload word");
check(storage.words[2] == 0x1000u, "incorrect event 0x38 low address word");
check(storage.words[3] == 0u, "incorrect event 0x38 high address word");
check(sceAgcDcbEventWriteGetSize(0x38) == 16, "incorrect event 0x38 size");
check(sceAgcDcbEventWriteGetSize(0x39) == 16, "incorrect event 0x39 size");
Storage sample39;
sceAgcDcbEventWrite(&sample39.buffer, 0x39, reinterpret_cast<const void*>(0x2008));
check(sample39.words[0] == 0xc0024600u && sample39.words[1] == (0x100u | 0x39u) && sample39.words[2] == 0x2008u && sample39.words[3] == 0u, "incorrect event 0x39 packet");
expectFailure([&] { sceAgcDcbEventWrite(&storage.buffer, 0x38, reinterpret_cast<const void*>(0x1001)); });
expectFailure([&] { sceAgcDcbEventWrite(&storage.buffer, 0x40, nullptr); });
Storage partialFlush;
sceAgcDcbEventWrite(&partialFlush.buffer, 7, nullptr);
check(partialFlush.words[0] == 0xc0004600u && partialFlush.words[1] == 0x407u, "incorrect event 7 partial flush packet");
check(sceAgcDcbEventWriteGetSize(7) == 8, "incorrect event 7 size");
Storage partialFlush15;
sceAgcDcbEventWrite(&partialFlush15.buffer, 15, nullptr);
check(partialFlush15.words[0] == 0xc0004600u && partialFlush15.words[1] == 0x40fu, "incorrect event 15 partial flush packet");
Storage partialFlush16;
sceAgcDcbEventWrite(&partialFlush16.buffer, 16, nullptr);
check(partialFlush16.words[0] == 0xc0004600u && partialFlush16.words[1] == 0x410u, "incorrect event 16 partial flush packet");
Storage cacheFlushNull;
sceAgcDcbEventWrite(&cacheFlushNull.buffer, 0x16, nullptr);
check(cacheFlushNull.words[0] == 0xc0004600u && cacheFlushNull.words[1] == 0x16u, "incorrect event 0x16 null address packet");
check(sceAgcDcbEventWriteGetSize(0x16) == 8, "incorrect event 0x16 size");
Storage cacheFlushWithAddress;
sceAgcDcbEventWrite(&cacheFlushWithAddress.buffer, 0x16, reinterpret_cast<const void*>(0x2d2a10000ull));
check(cacheFlushWithAddress.words[0] == 0xc0004600u && cacheFlushWithAddress.words[1] == 0x16u, "incorrect event 0x16 with address packet");
}
}
int main(int argc, char** argv) {
@ -695,7 +652,6 @@ int main(int argc, char** argv) {
return 0;
}
testPackets();
testEventWrite();
testNop();
testClearState();
testIndexedIndirectDraws();

View File

@ -1,164 +0,0 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include "prx/libc/include/Shutdown.hpp"
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstdio>
#include <limits>
#include <memory>
#include <new>
#include <stdexcept>
#include <vector>
extern "C" int APS5_VABI sceAgcSetAmmSemaphoreMemory(void* memory, std::uint64_t sizeInBytes);
extern "C" int APS5_VABI sceAgcGetSemaphoreLabel(std::uint32_t index, void** label);
namespace {
constexpr std::uint64_t blockBytes = 0x4000u;
constexpr std::uint64_t labelBytes = 32u;
constexpr std::uint64_t counterBytes = 8u;
constexpr std::uint64_t memoryBytes = 2u * blockBytes;
constexpr std::uint64_t storageBytes = 4u * blockBytes;
constexpr std::uint32_t labelCount = static_cast<std::uint32_t>(memoryBytes / labelBytes);
constexpr std::uint8_t guardByte = 0xa5u;
constexpr std::uint8_t counterByte = 0x5au;
struct AlignedRelease {
void operator()(std::uint8_t* memory) const { ::operator delete(memory, std::align_val_t{blockBytes}); }
};
std::unique_ptr<std::uint8_t, AlignedRelease> storage;
void check(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
template <typename TAction>
void expectFailure(TAction action) {
try {
action();
} catch (const std::runtime_error& error) {
check(error.what()[0] != '\0', "empty exception message");
return;
}
throw std::runtime_error("expected an exception");
}
std::uint8_t* memory() {
return storage.get() + blockBytes;
}
std::vector<std::uint8_t> snapshot() {
return {storage.get(), storage.get() + storageBytes};
}
void checkUnchanged(const std::vector<std::uint8_t>& before, const char* message) {
check(std::equal(before.begin(), before.end(), storage.get()), message);
}
void fillStorage() {
std::fill(storage.get(), storage.get() + storageBytes, guardByte);
for (std::uint64_t label = 0; label < memoryBytes; label += labelBytes) {
std::fill(memory() + label, memory() + label + counterBytes, counterByte);
std::fill(memory() + label + counterBytes, memory() + label + labelBytes, std::uint8_t{0});
}
}
void testUnregistered() {
std::array<void*, 3> labels{storage.get(), storage.get(), storage.get()};
expectFailure([&] { sceAgcGetSemaphoreLabel(0, &labels[1]); });
expectFailure([] { sceAgcGetSemaphoreLabel(0, nullptr); });
check(labels[0] == storage.get() && labels[1] == storage.get() && labels[2] == storage.get(), "a rejected semaphore label changed the output");
}
void testRegistrationRejections() {
const auto before = snapshot();
expectFailure([] { sceAgcSetAmmSemaphoreMemory(nullptr, memoryBytes); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(nullptr, 0); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), 0); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory() + 1, memoryBytes); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory() + labelBytes, blockBytes); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), blockBytes - 1u); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), blockBytes + labelBytes); });
expectFailure([] {
const auto top = std::numeric_limits<std::uintptr_t>::max() & ~static_cast<std::uintptr_t>(blockBytes - 1u);
sceAgcSetAmmSemaphoreMemory(reinterpret_cast<void*>(top), memoryBytes);
});
checkUnchanged(before, "a rejected registration changed memory");
testUnregistered();
}
void testUnsettledClearing() {
for (const std::uint64_t offset : {5u * labelBytes + counterBytes, memoryBytes - 1u}) {
memory()[offset] = 0x77u;
const auto before = snapshot();
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes); });
checkUnchanged(before, "a registration with unsettled clearing changed memory");
testUnregistered();
memory()[offset] = 0;
}
}
void testRegistration() {
const auto before = snapshot();
check(sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes) == 0, "registering the AMM semaphore memory failed");
check(std::all_of(memory(), memory() + memoryBytes, [](std::uint8_t byte) { return byte == 0; }), "registration did not clear the semaphore counters");
check(std::equal(before.begin(), before.begin() + blockBytes, storage.get()), "registration wrote before the semaphore memory");
check(std::equal(before.begin() + blockBytes + memoryBytes, before.end(), memory() + memoryBytes), "registration wrote past the semaphore memory");
}
void testLabels() {
for (const std::uint32_t index : {0u, 1u, 7u, labelCount / 2u, labelCount - 1u}) {
std::array<void*, 3> labels{storage.get(), nullptr, storage.get()};
check(sceAgcGetSemaphoreLabel(index, &labels[1]) == 0, "a semaphore label lookup failed");
check(labels[1] == memory() + index * labelBytes, "incorrect semaphore label address");
check(labels[0] == storage.get() && labels[2] == storage.get(), "a semaphore label lookup wrote past its output");
}
}
void testLabelRejections() {
for (const std::uint32_t index : {labelCount, labelCount + 1u, std::numeric_limits<std::uint32_t>::max()}) {
void* label = storage.get();
expectFailure([&] { sceAgcGetSemaphoreLabel(index, &label); });
check(label == storage.get(), "a rejected semaphore label changed the output");
}
expectFailure([] { sceAgcGetSemaphoreLabel(0, nullptr); });
}
void testRepeatedRegistration() {
std::fill(memory(), memory() + memoryBytes, std::uint8_t{0x3c});
const auto before = snapshot();
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(storage.get(), blockBytes); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(nullptr, 0); });
checkUnchanged(before, "a repeated registration changed memory");
void* label = nullptr;
check(sceAgcGetSemaphoreLabel(labelCount - 1u, &label) == 0 && label == memory() + memoryBytes - labelBytes, "a repeated registration replaced the semaphore memory");
}
}
int main() {
try {
storage.reset(static_cast<std::uint8_t*>(::operator new(storageBytes, std::align_val_t{blockBytes})));
fillStorage();
testUnregistered();
testRegistrationRejections();
testUnsettledClearing();
testRegistration();
testLabels();
testLabelRejections();
testRepeatedRegistration();
storage.reset();
LibcRunShutdown_nid_postfix();
std::puts("AGC semaphore tests passed");
return 0;
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
try { LibcRunShutdown_nid_postfix(); }
catch (const std::exception& shutdown) { std::fprintf(stderr, "shutdown: %s\n", shutdown.what()); }
return 1;
}
}

View File

@ -325,29 +325,6 @@ foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static)
string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}")
endforeach()
add_test_executable(agc_driver_register_key_tests EXCLUDE_FROM_ALL tests/RegisterKey.cpp)
target_include_directories(agc_driver_register_key_tests PRIVATE
${LIBS_INCLUDE_DIR}
${CMAKE_SOURCE_DIR}/core/shader/recompiler
${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}
)
target_link_libraries(agc_driver_register_key_tests PRIVATE
libSceAgcDriver
libc
libkernel
)
set_target_properties(agc_driver_register_key_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_driver_register_key_tests)
if(BUILD_TESTING)
set_target_properties(agc_driver_register_key_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_driver_register_key COMMAND agc_driver_register_key_tests)
set_tests_properties(agc_driver_register_key PROPERTIES TIMEOUT 30 ENVIRONMENT "APS5_VERIFY_REGISTER_KEYS=1")
if(WIN32)
set_tests_properties(agc_driver_register_key PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_test_executable(agc_driver_tests EXCLUDE_FROM_ALL
tests/Submit.cpp
)
@ -566,11 +543,6 @@ target_link_libraries(agc_masked_select_tests PRIVATE shader_recompiler_agc_driv
set_target_properties(agc_masked_select_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_masked_select_tests)
add_test_executable(agc_ir_reclaim_tests EXCLUDE_FROM_ALL tests/IrReclaim.cpp)
target_link_libraries(agc_ir_reclaim_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_ir_reclaim_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_ir_reclaim_tests)
add_test_executable(agc_vector_window_tests EXCLUDE_FROM_ALL tests/VectorWindow.cpp)
target_link_libraries(agc_vector_window_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_vector_window_tests PROPERTIES CXX_EXTENSIONS OFF)
@ -733,11 +705,6 @@ if(BUILD_TESTING)
endif()
endif()
add_test_executable(agc_formatted_buffer_byte_stores_tests EXCLUDE_FROM_ALL tests/FormattedBufferByteStores.cpp)
target_link_libraries(agc_formatted_buffer_byte_stores_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_formatted_buffer_byte_stores_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_formatted_buffer_byte_stores_tests)
add_test_executable(agc_shader_disk_cache_tests EXCLUDE_FROM_ALL tests/ShaderDiskCache.cpp)
target_include_directories(agc_shader_disk_cache_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_shader_disk_cache_tests PRIVATE shader_recompiler_agc_driver libc)
@ -867,19 +834,6 @@ target_link_libraries(agc_driver_cmask_fast_clear_tests PRIVATE
set_target_properties(agc_driver_cmask_fast_clear_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_driver_cmask_fast_clear_tests)
set(agcBufferUpdateSource ${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/BufferUpdate.comp)
set(agcBufferUpdateSpv ${agcTestShaderGeneratedRoot}/BufferUpdate.spv)
set(agcBufferUpdateHeader ${agcTestShaderGeneratedRoot}/BufferUpdate_spv.h)
add_custom_command(
OUTPUT "${agcBufferUpdateHeader}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTestShaderGeneratedRoot}"
COMMAND "$<TARGET_FILE:glslang-standalone>" -V --target-env vulkan1.1 -o "${agcBufferUpdateSpv}" "${agcBufferUpdateSource}"
COMMAND ${CMAKE_COMMAND} -DINPUT=${agcBufferUpdateSpv} -DOUTPUT=${agcBufferUpdateHeader}
-DSYMBOL=BUFFER_UPDATE_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake"
DEPENDS "${agcBufferUpdateSource}" glslang-standalone
VERBATIM
)
add_test_executable(agc_driver_recorder_tests EXCLUDE_FROM_ALL
tests/Recorder.cpp
tests/ResidentPresent.cpp
@ -887,7 +841,6 @@ add_test_executable(agc_driver_recorder_tests EXCLUDE_FROM_ALL
${agcSampleLodShaderHeader}
${agcSampleArrayShaderHeader}
${agcCoverShaderHeaders}
${agcBufferUpdateHeader}
)
target_include_directories(agc_driver_recorder_tests PRIVATE
@ -955,12 +908,6 @@ if(BUILD_TESTING)
set_tests_properties(agc_storage_written_buffer_order PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_copied_direct_ordering COMMAND agc_driver_recorder_tests --copied-direct-only)
set_tests_properties(agc_copied_direct_ordering PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_copied_direct_ordering PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_host_import_unmap COMMAND agc_driver_recorder_tests --host-import-unmap-only)
set_tests_properties(agc_host_import_unmap PROPERTIES SKIP_RETURN_CODE 77)
add_test(NAME agc_parked_import_unmap COMMAND agc_driver_recorder_tests --parked-import-unmap-only)
@ -969,11 +916,6 @@ if(BUILD_TESTING)
set_tests_properties(agc_host_import_retirement PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
add_test(NAME agc_unit_shadow COMMAND agc_driver_recorder_tests --unit-shadow-only)
set_tests_properties(agc_unit_shadow PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000")
if(WIN32)
add_test(NAME agc_storage_cpu_snapshot COMMAND agc_driver_recorder_tests --storage-cpu-snapshot-only)
set_tests_properties(agc_storage_cpu_snapshot PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 SKIP_RETURN_CODE 77
ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_descriptor_pool_recycling COMMAND agc_driver_recorder_tests --descriptor-pool-only)
set_tests_properties(agc_descriptor_pool_recycling PROPERTIES SKIP_RETURN_CODE 77)
add_test(NAME agc_draw_snapshot_cap COMMAND agc_driver_recorder_tests --draw-snapshot-cap-only)
@ -985,9 +927,6 @@ if(BUILD_TESTING)
if(WIN32)
set_tests_properties(agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_multisample_guest_access agc_draw_inputs_in_place PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
add_test(NAME agc_host_import_second_mapping COMMAND agc_driver_recorder_tests --host-import-second-mapping-only)
set_tests_properties(agc_host_import_second_mapping PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
@ -1048,11 +987,47 @@ add_test_executable(agc_driver_graphics_tests EXCLUDE_FROM_ALL
tests/GuestSamplerResource.cpp
tests/TextureDetiler.cpp
tests/DepthSurfaceReuse.cpp
Graphics/src/Draw.cpp
Graphics/src/DepthSurface.cpp
Graphics/src/MultisampleTarget.cpp
Graphics/src/Pipeline.cpp
Graphics/src/PipelineLibrary.cpp
Graphics/src/State.cpp
Graphics/src/ColorTargetLayout.cpp
Graphics/src/ColorTargetTransfer.cpp
Graphics/src/Resources.cpp
Graphics/src/PlaceholderImages.cpp
Graphics/src/ShaderResources.cpp
Graphics/src/ShaderAddressResources.cpp
Graphics/src/GuestBufferMemory.cpp
Graphics/src/BdaResources.cpp
Graphics/src/BufferAddress.cpp
Graphics/src/ShaderValidation.cpp
Graphics/src/GuestTextureResource.cpp
Graphics/src/GuestSamplerResource.cpp
Graphics/src/TextureFormat.cpp
Graphics/src/DccMetadata.cpp
Graphics/src/TextureTiling.cpp
Graphics/src/TextureDetiler.cpp
Graphics/src/Texture.cpp
Graphics/src/PassHazards.cpp
Graphics/src/UnitShadow.cpp
Graphics/src/Sampler.cpp
Graphics/src/Recorder.cpp
Graphics/src/ParallelCompare.cpp
Graphics/src/ShaderInputState.cpp
${agcTextureDetileShaderHeader}
${agcCmaskClearShaderHeader}
${agcMeshArgumentsShaderHeader}
${agcTextureDetileImageShaderHeader}
${agcSampleCounterShaderHeader}
${agcSampleDumpsShaderHeader}
Execution/src/GuestMemory.cpp
Execution/src/Pm4.cpp
)
target_include_directories(agc_driver_graphics_tests PRIVATE
${LIBS_INCLUDE_DIR}
$<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>
${agcTextureDetileGeneratedRoot}
${CMAKE_SOURCE_DIR}/core/shader/recompiler
${CMAKE_SOURCE_DIR}/core/shader/recompiler/IntermediateRepresentation/include
@ -1063,7 +1038,7 @@ target_include_directories(agc_driver_graphics_tests PRIVATE
)
target_link_libraries(agc_driver_graphics_tests PRIVATE
libSceAgcDriver
shader_recompiler_agc_driver
libc
libkernel
)
@ -1071,71 +1046,45 @@ target_link_libraries(agc_driver_graphics_tests PRIVATE
set_target_properties(agc_driver_graphics_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_driver_graphics_tests)
foreach(agcVertexRangeShader IN ITEMS VertexRange.vert VertexRangeProducer.frag)
string(REPLACE "." "_" agcVertexRangeName "${agcVertexRangeShader}")
string(TOUPPER "${agcVertexRangeName}_SPV" agcVertexRangeSymbol)
set(agcVertexRangeSpv ${agcTestShaderGeneratedRoot}/${agcVertexRangeName}.spv)
set(agcVertexRangeHeader ${agcTestShaderGeneratedRoot}/${agcVertexRangeName}_spv.h)
add_custom_command(
OUTPUT "${agcVertexRangeHeader}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTestShaderGeneratedRoot}"
COMMAND "$<TARGET_FILE:glslang-standalone>" -V --target-env vulkan1.1 -o "${agcVertexRangeSpv}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/${agcVertexRangeShader}"
COMMAND ${CMAKE_COMMAND} -DINPUT=${agcVertexRangeSpv} -DOUTPUT=${agcVertexRangeHeader}
-DSYMBOL=${agcVertexRangeSymbol} -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake"
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/${agcVertexRangeShader}" glslang-standalone
VERBATIM
)
list(APPEND agcVertexRangeHeaders ${agcVertexRangeHeader})
endforeach()
target_sources(agc_driver_recorder_tests PRIVATE ${agcVertexRangeHeaders})
if(BUILD_TESTING)
add_test(NAME agc_driver_vertex_target_range_copied COMMAND agc_driver_recorder_tests --vertex-target-range-copied)
add_test(NAME agc_driver_vertex_target_range_imported COMMAND agc_driver_recorder_tests --vertex-target-range-imported)
set_tests_properties(agc_driver_vertex_target_range_copied agc_driver_vertex_target_range_imported PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_driver_vertex_target_range_copied agc_driver_vertex_target_range_imported PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_test_executable(agc_driver_bda_device_tests EXCLUDE_FROM_ALL
tests/BdaDevice.cpp
tests/BdaShader.cpp
tests/BdaExecution.cpp
tests/BdaContracts.cpp
Execution/src/BdaFeatures.cpp
Graphics/src/Resources.cpp
Graphics/src/PlaceholderImages.cpp
Graphics/src/BufferAddress.cpp
)
target_include_directories(agc_driver_bda_device_tests PRIVATE
${LIBS_INCLUDE_DIR}
$<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>
${CMAKE_SOURCE_DIR}/core/shader/recompiler
${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}
)
target_link_libraries(agc_driver_bda_device_tests PRIVATE libSceAgcDriver SDL2-static libc)
target_link_libraries(agc_driver_bda_device_tests PRIVATE SDL2-static libc shader_recompiler_agc_driver)
set_target_properties(agc_driver_bda_device_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_driver_bda_device_tests)
add_test_executable(agc_driver_mesh_tests EXCLUDE_FROM_ALL tests/Mesh.cpp)
add_test_executable(agc_driver_mesh_tests EXCLUDE_FROM_ALL
tests/Mesh.cpp
${agcDriverSources}
)
target_include_directories(agc_driver_mesh_tests PRIVATE
${LIBS_INCLUDE_DIR}
${CMAKE_SOURCE_DIR}/core/shader/recompiler
${agcTextureDetileGeneratedRoot}
${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}
${ANYPS5_SPIRV_HEADERS_INCLUDE_DIR}
)
target_link_libraries(agc_driver_mesh_tests PRIVATE libSceAgcDriver SDL2-static libc)
target_link_libraries(agc_driver_mesh_tests PRIVATE shader_recompiler_agc_driver SDL2-static libc libkernel jpeg_turbo ${CMAKE_DL_LIBS})
add_dependencies(agc_driver_mesh_tests agc_trace_revision)
set_target_properties(agc_driver_mesh_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_driver_mesh_tests)
set(ANYPS5_AGC_EXECUTION_TESTS "" CACHE STRING "tests/execution sources to build, by file name without extension; the others build a stub that reports a skip. Empty builds all")
file(GLOB agcExecutionTestSources CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/tests/execution/*.cpp)
set(agcExecutionTests)
foreach(agcExecutionTestSource IN LISTS agcExecutionTestSources)
get_filename_component(agcExecutionTestName ${agcExecutionTestSource} NAME_WE)
if(ANYPS5_AGC_EXECUTION_TESTS AND NOT agcExecutionTestName IN_LIST ANYPS5_AGC_EXECUTION_TESTS)
set(agcExecutionTestSource ${CMAKE_CURRENT_SOURCE_DIR}/tests/ExecutionTestNotBuilt.cpp)
endif()
string(REGEX REPLACE "([a-z0-9])([A-Z])" "\\1_\\2" agcExecutionTestName "${agcExecutionTestName}")
string(TOLOWER "agc_driver_${agcExecutionTestName}_tests" agcExecutionTest)
add_test_executable(${agcExecutionTest} EXCLUDE_FROM_ALL ${agcExecutionTestSource})
@ -1150,11 +1099,6 @@ foreach(agcExecutionTestSource IN LISTS agcExecutionTestSources)
configure_windows_unwind(${agcExecutionTest})
list(APPEND agcExecutionTests ${agcExecutionTest})
endforeach()
foreach(agcExecutionTestName IN LISTS ANYPS5_AGC_EXECUTION_TESTS)
if(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/execution/${agcExecutionTestName}.cpp)
message(FATAL_ERROR "ANYPS5_AGC_EXECUTION_TESTS names ${agcExecutionTestName}, which has no source in tests/execution")
endif()
endforeach()
target_link_libraries(agc_driver_gpu_map_on_fiber_tests PRIVATE libSceFiber)
@ -1225,7 +1169,7 @@ if(BUILD_TESTING)
if(WIN32)
set_tests_properties(agc_shader_bindless agc_shader_srt_read_patching agc_shader_frontend_pair agc_shader_frontend_pair_off PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
foreach(agcTest IN ITEMS agc_shared_write_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_ir_reclaim_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_tests agc_scalar_select_mask_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_shader_disk_cache_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests agc_formatted_buffer_byte_stores_tests ${agcExecutionTests})
foreach(agcTest IN ITEMS agc_shared_write_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_tests agc_scalar_select_mask_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_shader_disk_cache_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests ${agcExecutionTests})
if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests" OR agcTest STREQUAL "agc_driver_pipeline_optimized_readback_tests")
continue()
endif()
@ -1238,11 +1182,6 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endforeach()
add_test(NAME agc_driver_partially_resident_pending_stores COMMAND agc_driver_partially_resident_snapshot_tests --pending-stores)
set_tests_properties(agc_driver_partially_resident_pending_stores 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_partially_resident_pending_stores PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_sparse_buffer_holes COMMAND agc_driver_graphics_tests --sparse-buffer-holes)
set_tests_properties(agc_sparse_buffer_holes PROPERTIES TIMEOUT 30)
set_tests_properties(agc_driver_sparse_buffer_holes PROPERTIES TIMEOUT 30)
@ -1288,25 +1227,11 @@ if(BUILD_TESTING)
if(WIN32)
set_tests_properties(agc_vector_window_stage_inputs PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_layered_volume_vertex COMMAND agc_driver_layered_volume_draw_tests --vertex-only)
add_test(NAME agc_driver_layered_volume_vertex_no_gpl COMMAND agc_driver_layered_volume_draw_tests --vertex-only)
set_tests_properties(agc_driver_layered_volume_vertex agc_driver_layered_volume_vertex_no_gpl PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
set_property(TEST agc_driver_layered_volume_vertex_no_gpl APPEND PROPERTY ENVIRONMENT "APS5_NO_GPL=1")
if(WIN32)
set_tests_properties(agc_driver_layered_volume_vertex agc_driver_layered_volume_vertex_no_gpl PROPERTIES
ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_f64_denorm_preserve_spirv COMMAND agc_driver_f64_fma_subnormals_tests --verify-spv-denorm-mode)
set_tests_properties(agc_driver_f64_denorm_preserve_spirv PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(agc_driver_f64_denorm_preserve_spirv PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_f16_to_f32_unpack_spirv COMMAND agc_driver_f16_to_f32_unpack_tests --verify-spirv)
set_tests_properties(agc_driver_f16_to_f32_unpack_spirv PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(agc_driver_f16_to_f32_unpack_spirv PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_structurizer_intersection_off COMMAND agc_structurizer_tests)
set_tests_properties(agc_structurizer_intersection_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_DOM_INTERSECTION=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
add_test(NAME agc_structurizer_iteration_selection_off COMMAND agc_structurizer_tests)
@ -1363,10 +1288,8 @@ if(BUILD_TESTING)
add_test(NAME agc_driver_pm4_failure COMMAND agc_driver_pm4_tests failure)
set_tests_properties(agc_driver_pm4_failure PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77)
add_test(NAME agc_descriptor_pool_reset_failure COMMAND agc_driver_graphics_tests --descriptor-pool-reset-failure)
add_test(NAME agc_driver_address_space_teardown COMMAND agc_driver_graphics_tests --address-space-teardown)
set_tests_properties(agc_driver_address_space_teardown PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(agc_driver_pm4_failure agc_descriptor_pool_reset_failure agc_driver_address_space_teardown PROPERTIES ENVIRONMENT_MODIFICATION
set_tests_properties(agc_driver_pm4_failure agc_descriptor_pool_reset_failure PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
if(NOT WIN32)
@ -1580,11 +1503,8 @@ if(BUILD_TESTING)
configure_windows_unwind(agc_compute_pipeline_capture_tests)
add_test(NAME agc_compute_descriptor_layout_flags COMMAND agc_driver_graphics_tests --descriptor-layout-flags)
set_tests_properties(agc_compute_descriptor_layout_flags PROPERTIES TIMEOUT 30)
add_test(NAME agc_driver_covered_centroid_state COMMAND agc_driver_graphics_tests --covered-centroid-state)
set_tests_properties(agc_driver_covered_centroid_state PROPERTIES TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_compute_descriptor_layout_flags PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
set_tests_properties(agc_driver_covered_centroid_state PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_compute_pipeline_capture COMMAND agc_compute_pipeline_capture_tests)
set_tests_properties(agc_compute_pipeline_capture PROPERTIES TIMEOUT 30)
@ -1889,21 +1809,6 @@ add_test_executable(agc_position_invariance_tests EXCLUDE_FROM_ALL tests/Positio
target_link_libraries(agc_position_invariance_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_position_invariance_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_position_invariance_tests)
add_test_executable(agc_vertex_layer_export_tests EXCLUDE_FROM_ALL tests/VertexLayerExport.cpp)
target_link_libraries(agc_vertex_layer_export_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_vertex_layer_export_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_vertex_layer_export_tests)
if(BUILD_TESTING)
set_target_properties(agc_vertex_layer_export_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_vertex_layer_export COMMAND agc_vertex_layer_export_tests)
set_tests_properties(agc_vertex_layer_export PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1")
if(WIN32)
set_tests_properties(agc_vertex_layer_export PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
if(BUILD_TESTING)
set_target_properties(agc_position_invariance_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_position_invariance COMMAND agc_position_invariance_tests)

View File

@ -8,8 +8,6 @@ namespace AgcDriver::DriverDetail {
double TraceMs();
std::uint64_t SkippedShader();
void require(bool condition, const char* reason);
std::uint32_t readRegister(const Registers& registers, std::uint32_t offset);

View File

@ -75,19 +75,6 @@ struct DrawEntryCounters {
std::chrono::steady_clock::time_point lastReport = std::chrono::steady_clock::now();
};
class DrawKeySightings {
public:
static constexpr std::size_t Ways = 4;
explicit DrawKeySightings(std::size_t keys);
bool SeenBefore(std::uint64_t key);
private:
std::vector<std::uint64_t> keys;
std::vector<std::uint8_t> filled;
std::vector<std::uint8_t> next;
std::size_t bucketMask = 0;
};
enum class DrawVerdict { Drawn, Nothing, Rejected };
struct StageCapture {

View File

@ -1,12 +0,0 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_DRIVER_DRAW_DRAWREGISTERKEY_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_DRIVER_DRAW_DRAWREGISTERKEY_HPP
#include "prx/libSceAgcDriver/Execution/include/QueueState.hpp"
namespace AgcDriver::DriverDetail {
std::uint64_t RegisterStateKey(const QueueState& queue, bool allUserWords = false, bool incremental = true);
}
#endif

View File

@ -115,7 +115,6 @@ private:
static std::size_t drawCacheEntries();
void accountDrawVariant(const DispatchVariant& variant, bool added);
void insertDrawEntry(std::uint64_t key, std::vector<std::shared_ptr<DispatchVariant>>& fresh, std::shared_ptr<const DrawDecode> decode);
bool admitDrawKey(std::uint64_t key);
void eraseDrawEntry(std::uint64_t key, const std::shared_ptr<DrawEntry>& expected);
std::shared_ptr<const DrawRecipe> findDrawRecipe(std::uint64_t key, const std::vector<std::shared_ptr<DispatchVariant>>& stages);
void attachDrawRecipe(std::uint64_t key, const std::vector<std::shared_ptr<DispatchVariant>>& stages, std::shared_ptr<const DrawRecipe> recipe);
@ -311,7 +310,6 @@ private:
std::list<std::uint64_t> drawOrder;
std::mutex drawCacheMutex;
std::uint64_t drawCacheHits = 0, drawCacheEvictions = 0, drawCacheVariants = 0, drawCacheVariantBytes = 0;
std::optional<DrawKeySightings> drawKeySightings;
DrawEntryCounters drawEntryCounters;

View File

@ -2,8 +2,6 @@
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_MEMORY_DWORDPATTERNFILL_HPP
#include "Recompiler.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include <array>
#include <cstddef>
#include <cstdint>
@ -20,10 +18,6 @@ struct DwordPatternFill {
};
std::optional<DwordPatternFill> MatchDwordPatternFill(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute);
std::optional<DwordPatternFill> MatchZeroDwordFill(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute);
std::optional<Graphics::MemoryValueFill> MatchMemoryValueBufferFill(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute);
bool IsByteFill(const std::array<std::uint32_t, 4>& pattern);
std::optional<Graphics::DccKeys> KeysFillMemo(const std::array<std::uint32_t, 4>& pattern, bool coversStorageKeys, bool memoryValueFill);
}

View File

@ -67,7 +67,6 @@ struct PreparedShaders : PreparedShaderState {
std::mutex mutex;
std::condition_variable settled;
std::exception_ptr failure;
std::atomic<bool> demanded{false};
bool pending = false;
};
@ -108,8 +107,7 @@ std::optional<ShaderRecompiler::ShaderFloatMode> RegisteredFloatMode(const Shade
std::shared_ptr<const ShaderSnapshot> RegisteredHeader(const ShaderRegistry& registry, const Shader* shader);
std::shared_ptr<const ShaderSnapshot> RegisteredProgram(const ShaderRegistry& registry, std::uint64_t address, std::initializer_list<std::uint8_t> types);
void PublishRegisteredShader(std::shared_ptr<ShaderRegistry>& registry, const std::shared_ptr<const ShaderSnapshot>& snapshot);
void SubmitRegistrationPreparation(std::function<void()> task, const PreparedShaders* prepared = nullptr);
void PrioritizeRegistrationPreparation(PreparedShaders& prepared);
void SubmitRegistrationPreparation(std::function<void()> task);
void SubmitLibraryPrebuild(std::function<void()> task);
void CloseRegistrationPreparation();

View File

@ -72,8 +72,6 @@ struct UnchangedQuery {
};
bool WrittenSince(std::uint64_t address, std::size_t bytes, std::uint64_t generation);
bool UnchangedSinceAll(std::span<const UnchangedQuery> queries);
std::uint64_t CollectCommittedWrites(std::uint64_t address, std::size_t bytes, std::vector<std::pair<std::uint64_t, std::uint64_t>>& ranges);
bool UnchangedCommittedSince(std::span<const std::pair<std::uint64_t, std::uint64_t>> ranges, std::uint64_t generation);
// 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);

View File

@ -9,7 +9,6 @@
#include <stdexcept>
#include <utility>
#include <map>
#include <memory>
#include <array>
#include <optional>
#include <span>
@ -24,29 +23,6 @@ public:
using mapped_type = std::uint32_t;
using value_type = std::pair<const std::uint32_t, std::uint32_t>;
class Mask {
public:
explicit Mask(std::vector<std::uint64_t> bits) : bits(std::move(bits)) {}
bool Contains(std::uint32_t offset) const { return offset / 64 < bits.size() && ((bits[offset / 64] >> (offset % 64)) & 1u) != 0; }
private:
std::vector<std::uint64_t> bits;
};
class Reference {
public:
Reference(Registers& owner, std::uint32_t offset) : owner(owner), offset(offset) {}
operator std::uint32_t() const { return owner.values[offset]; }
Reference& operator=(std::uint32_t value) { owner.insert_or_assign(offset, value); return *this; }
Reference& operator=(const Reference& other) { return *this = static_cast<std::uint32_t>(other); }
Reference& operator|=(std::uint32_t value) { return *this = static_cast<std::uint32_t>(*this) | value; }
Reference& operator&=(std::uint32_t value) { return *this = static_cast<std::uint32_t>(*this) & value; }
Reference& operator^=(std::uint32_t value) { return *this = static_cast<std::uint32_t>(*this) ^ value; }
Reference& operator+=(std::uint32_t value) { return *this = static_cast<std::uint32_t>(*this) + value; }
private:
Registers& owner;
std::uint32_t offset;
};
class const_iterator {
public:
using iterator_category = std::forward_iterator_tag;
@ -81,18 +57,6 @@ public:
using iterator = const_iterator;
Registers() = default;
Registers(const Registers&) = default;
Registers& operator=(const Registers&) = default;
Registers(Registers&& other) noexcept : values(std::move(other.values)), present(std::move(other.present)), entries(std::exchange(other.entries, 0)), mask(std::move(other.mask)), fingerprint(std::exchange(other.fingerprint, 0)) {}
Registers& operator=(Registers&& other) noexcept {
if (this == &other) return *this;
values = std::move(other.values);
present = std::move(other.present);
entries = std::exchange(other.entries, 0);
mask = std::move(other.mask);
fingerprint = std::exchange(other.fingerprint, 0);
return *this;
}
Registers(std::initializer_list<std::pair<std::uint32_t, std::uint32_t>> entries) {
for (const auto& [offset, value] : entries) emplace(offset, value);
}
@ -119,33 +83,27 @@ public:
values.clear();
present.clear();
entries = 0;
fingerprint = 0;
}
std::pair<const_iterator, bool> emplace(std::uint32_t offset, std::uint32_t value) {
if (contains(offset)) return {const_iterator{this, offset}, false};
mark(offset) = value;
changeFingerprint(offset, value);
return {const_iterator{this, offset}, true};
}
std::pair<const_iterator, bool> insert_or_assign(std::uint32_t offset, std::uint32_t value) {
if (contains(offset)) {
if (values[offset] == value) return {const_iterator{this, offset}, false};
changeFingerprint(offset, values[offset]);
values[offset] = value;
changeFingerprint(offset, value);
return {const_iterator{this, offset}, false};
}
mark(offset) = value;
changeFingerprint(offset, value);
return {const_iterator{this, offset}, true};
}
Reference operator[](std::uint32_t offset) {
if (!contains(offset)) emplace(offset, 0);
return {*this, offset};
std::uint32_t& operator[](std::uint32_t offset) {
if (contains(offset)) return values[offset];
return mark(offset) = 0;
}
Reference at(std::uint32_t offset) {
std::uint32_t& at(std::uint32_t offset) {
if (!contains(offset)) throw std::out_of_range("register is not set");
return {*this, offset};
return values[offset];
}
const std::uint32_t& at(std::uint32_t offset) const {
if (!contains(offset)) throw std::out_of_range("register is not set");
@ -153,7 +111,6 @@ public:
}
std::size_t erase(std::uint32_t offset) {
if (!contains(offset)) return 0;
changeFingerprint(offset, values[offset]);
present[offset / 64] &= ~(std::uint64_t{1} << (offset % 64));
--entries;
return 1;
@ -166,31 +123,8 @@ public:
}
return a == end() && b == other.end();
}
std::uint64_t Fingerprint(const std::shared_ptr<const Mask>& selected) const {
if (mask != selected) {
fingerprint = RecomputeFingerprint(*selected);
mask = selected;
}
return fingerprint;
}
std::uint64_t RecomputeFingerprint(const Mask& selected) const {
std::uint64_t result = 0;
for (const auto [offset, value] : *this) {
if (selected.Contains(offset)) result ^= contribution(offset, value);
}
return result;
}
private:
static std::uint64_t contribution(std::uint32_t offset, std::uint32_t value) {
auto hash = ((static_cast<std::uint64_t>(offset) << 32u) | value) + 0x9e3779b97f4a7c15ull;
hash = (hash ^ (hash >> 30u)) * 0xbf58476d1ce4e5b9ull;
hash = (hash ^ (hash >> 27u)) * 0x94d049bb133111ebull;
return hash ^ (hash >> 31u);
}
void changeFingerprint(std::uint32_t offset, std::uint32_t value) {
if (mask != nullptr && mask->Contains(offset)) fingerprint ^= contribution(offset, value);
}
static constexpr std::size_t End = ~std::size_t{0};
std::uint32_t& mark(std::uint32_t offset) {
if (offset >= values.size()) {
@ -213,8 +147,6 @@ private:
std::vector<std::uint32_t> values;
std::vector<std::uint64_t> present;
std::size_t entries = 0;
mutable std::shared_ptr<const Mask> mask;
mutable std::uint64_t fingerprint = 0;
};
inline Registers BuildInitialContextRegisters() {
@ -240,7 +172,6 @@ inline Registers BuildInitialContextRegisters() {
result.emplace(0x31c + 0xfu * i, 0);
}
for (std::uint32_t i = 0; i < 16; ++i) {
result.emplace(0x2fe + i, 0);
result.emplace(0x94 + 2 * i, 0x80000000);
result.emplace(0x95 + 2 * i, 0x40004000);
result.emplace(0xb4 + 2 * i, 0);

View File

@ -84,7 +84,6 @@ struct DrawRecipe {
std::optional<Graphics::State> masked;
std::set<std::uint32_t> fragmentOutputs;
VkPipelineStageFlags shaderStages = 0;
bool hasDepth = false;
};
}

View File

@ -222,7 +222,7 @@ public:
// blit-submit times after the flip packet; noted in-place reads checked
// (APS5_FLIP_READ_CHECK=1) and found overwritten before the blit's fence.
struct PresentStatistics {
std::uint64_t presents, gpuUnread, batchesAheadOfBlit, batchesAfterFlip, readsChecked, readsOverwritten, acquireDrops;
std::uint64_t presents, gpuUnread, batchesAheadOfBlit, batchesAfterFlip, readsChecked, readsOverwritten;
double gpuBusyMs, gpuGapMs, lastSubmitAfterFlipMs, blitSubmitAfterFlipMs;
};
static PresentStatistics PresentCounts();

View File

@ -1,6 +1,5 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include <cstdlib>
#include <stdexcept>
namespace AgcDriver::DriverDetail {
@ -10,11 +9,6 @@ double TraceMs() {
return std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - origin).count();
}
std::uint64_t SkippedShader() {
static const std::uint64_t skipped = [] { const char* text = std::getenv("APS5_SKIP_SHADER"); return text ? std::strtoull(text, nullptr, 16) : 0ull; }();
return skipped;
}
void require(bool condition, const char* reason) {
if (!condition) {
throw std::runtime_error(std::string("AGC driver: ") + reason);

View File

@ -16,7 +16,6 @@ namespace AgcDriver::DriverDetail {
void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet, const Submission& submission, std::uint64_t indirectArguments) {
PerformanceTimer timing("Driver.Dispatch");
const auto address = (static_cast<std::uint64_t>(readRegister(queue.shader, 0x20c)) << 8u) | (static_cast<std::uint64_t>(readRegister(queue.shader, 0x20d) & 0xffu) << 40u);
if (address == SkippedShader() && address != 0) return;
auto registeredShader = RegisteredProgram(*submission.shaders, address, {0});
if (!registeredShader) registeredShader = ReadRawShader(address);
const auto& snapshot = *registeredShader;

View File

@ -4,7 +4,6 @@
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
#include "Optimization/ResourceProgram.hpp"
#include <algorithm>
#include <cstdlib>
namespace AgcDriver::DriverDetail {
@ -95,7 +94,6 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
const auto& graphics = decode->state;
auto pixel = decode->pixel;
std::vector<DrawProgram> programs = decode->programs;
if (const auto skipped = SkippedShader(); skipped != 0 && std::any_of(programs.begin(), programs.end(), [skipped](const DrawProgram& program) { return program.binary.codeAddress == skipped; })) return drawn();
const auto setMeshIndexBuffer = [&](const Pm4::DrawParameters& parameters) {
if (!graphics.stages.mesh) return;
auto& words = programs.front().userData;
@ -147,7 +145,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
if (program.binary.stage == Stage::Fragment || roles[i] == Role::GeometryBack) return;
decodeReads[i].clear();
vertexInfos[i] = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, &decodeReads[i], false, 8u - program.firstUserSgpr);
if (vertexInfos[i]) vertexInfos[i]->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, readRegister(queue.context, 0x207)) | graphics.layerExports;
if (vertexInfos[i]) vertexInfos[i]->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, readRegister(queue.context, 0x207));
};
if (!registerKey) {
for (std::size_t i = 0; i < programs.size(); ++i) decodeVertexInfo(i);

View File

@ -1,7 +1,6 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp"
#include <algorithm>
#include <bit>
#include <cstdlib>
namespace AgcDriver::DriverDetail {
@ -57,36 +56,6 @@ void Driver::accountDrawVariant(const DispatchVariant& variant, bool added) {
}
}
DrawKeySightings::DrawKeySightings(std::size_t keyCount) {
Graphics::Require(keyCount >= Ways, "draw key sightings need at least one bucket");
const auto buckets = std::bit_ceil(keyCount / Ways);
keys.assign(buckets * Ways, 0);
filled.assign(buckets, 0);
next.assign(buckets, 0);
bucketMask = buckets - 1;
}
bool DrawKeySightings::SeenBefore(std::uint64_t key) {
auto mixed = key;
mixed = (mixed ^ (mixed >> 30u)) * 0xbf58476d1ce4e5b9ull;
mixed = (mixed ^ (mixed >> 27u)) * 0x94d049bb133111ebull;
mixed ^= mixed >> 31u;
const auto bucket = static_cast<std::size_t>(mixed) & bucketMask;
auto* ways = keys.data() + bucket * Ways;
if (std::find(ways, ways + filled[bucket], key) != ways + filled[bucket]) return true;
ways[next[bucket]] = key;
next[bucket] = static_cast<std::uint8_t>((next[bucket] + 1u) % Ways);
if (filled[bucket] < Ways) ++filled[bucket];
return false;
}
bool Driver::admitDrawKey(std::uint64_t key) {
std::lock_guard cacheLock(drawCacheMutex);
if (drawCache.contains(key)) return true;
if (!drawKeySightings) drawKeySightings.emplace(drawCacheEntries() * DrawKeySightings::Ways);
return drawKeySightings->SeenBefore(key);
}
void Driver::insertDrawEntry(std::uint64_t key, std::vector<std::shared_ptr<DispatchVariant>>& fresh, std::shared_ptr<const DrawDecode> decode) {
std::lock_guard cacheLock(drawCacheMutex);
auto& counters = drawEntryCounters;

View File

@ -20,7 +20,7 @@ ShaderRecompiler::RecompileResult Driver::materializeDrawStage(std::size_t i, st
program.binary,
{waveSize, program.firstUserSgpr, program.userData, std::nullopt, program.binary.stage == Stage::Fragment ? std::optional(pixel) : std::nullopt, vertexInfos[i], memory, RegisteredFloatMode(*program.snapshot)},
localDevice->Target(),
{0, 0, pushOffset, (graphics.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : graphics.stages.tessellation && pushOffset < Graphics::PipelinePushSlotBytes ? ShaderRecompiler::TessellationLevelPushOffsetBytes : Graphics::PipelinePushSlotBytes) - pushOffset % Graphics::PipelinePushSlotBytes},
{0, 0, pushOffset, (graphics.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : Graphics::PipelinePushSlotBytes) - pushOffset % Graphics::PipelinePushSlotBytes},
ShaderRecompiler::GraphicsCompileContext{program.firstUserSgpr, linked, graphics.stages.mesh, graphics.stages.tessellation, {drawParameters.indexAddress, drawParameters.indexCount, drawParameters.indexSize, drawParameters.instanceCount}}
};
const auto waitedBefore = traceCapSync() || profile ? Graphics::Recorder::ThreadWaitedMs() : 0.0;
@ -83,7 +83,7 @@ ShaderRecompiler::RecompileResult Driver::materializeDrawStage(std::size_t i, st
}
void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawParameters& drawParameters, const std::optional<Graphics::IndirectDrawPath>& indirectCpu, const std::vector<DrawProgram>& programs, const std::vector<StageCapture>& stageCaptures, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, const std::vector<std::vector<Graphics::DecodeRead>>& decodeReads, bool verifyHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, std::vector<std::shared_ptr<DispatchVariant>>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr<const DrawDecode>& decode, DrawPhaseTiming& phaseTiming) {
if (useDrawEntries && !drawHit && !(drawParameters.indirect && indirectCpu) && (verifyHit || admitDrawKey(drawKey))) {
if (useDrawEntries && !drawHit && !(drawParameters.indirect && indirectCpu)) {
phaseTiming.Phase(DrawRowVectors);
std::uint64_t unstable = 0, mismatches = 0;
for (std::size_t i = 0; i < programs.size(); ++i) {

View File

@ -116,7 +116,6 @@ std::shared_ptr<DrawDecode> Driver::decodeDraw(const QueueState& queue, const Su
product->pixel.dualSourceBlend = product->state.dualSourceBlend;
product->pixel.alphaToCoverage = product->state.alphaToCoverage;
if (product->pixel.ancillary) product->pixel.rasterPrimitive = Graphics::RasterPrimitiveOf(product->state);
if (product->pixel.ancillary) product->pixel.layerExported = product->state.layerExports != 0;
return product;
}

View File

@ -26,7 +26,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr<Vulk
mix(graphics.stages.tessellation.has_value());
if (graphics.stages.tessellation) {
const auto& tess = *graphics.stages.tessellation;
for (const auto value : {tess.inputControlPoints, tess.outputControlPoints, tess.domain, tess.partitioning, tess.outputTopology, tess.patchesPerGroup}) mix(value);
for (const auto value : {tess.inputControlPoints, tess.outputControlPoints, tess.domain, tess.partitioning, tess.outputTopology}) mix(value);
}
mix(graphics.rectList);
mix(programs.size());

View File

@ -1,5 +1,4 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp"
#include <algorithm>
#include <array>
#include <cstdlib>
@ -7,69 +6,36 @@
namespace AgcDriver::DriverDetail {
namespace {
bool runtimeRegister(std::uint32_t offset) {
return offset - 0x00cu < 32u || offset - 0x08cu < 32u || offset - 0x10cu < 32u || offset == 0x082u || offset == 0x083u || offset == 0x102u || offset == 0x103u;
}
const auto& registerMasks() {
static const auto masks = [] {
std::array<std::vector<std::uint64_t>, 3> bits;
for (const auto& range : Graphics::DrawKeyRegisters) {
auto& bank = bits[static_cast<std::size_t>(range.bank)];
bank.resize(std::max(bank.size(), (static_cast<std::size_t>(range.first) + range.count + 63) / 64));
for (auto offset = range.first; offset < range.first + range.count; ++offset) {
if (range.bank == Graphics::RegisterBank::Shader && runtimeRegister(offset)) continue;
bank[offset / 64] |= std::uint64_t{1} << (offset % 64);
}
}
std::array<std::shared_ptr<const Registers::Mask>, 3> result;
for (std::size_t i = 0; i < bits.size(); ++i) result[i] = std::make_shared<const Registers::Mask>(std::move(bits[i]));
return result;
}();
return masks;
}
}
std::uint64_t RegisterStateKey(const QueueState& queue, bool allUserWords, bool incremental) {
const auto& masks = registerMasks();
const auto fingerprint = [&](const Registers& registers, Graphics::RegisterBank bank) {
const auto& mask = masks[static_cast<std::size_t>(bank)];
return incremental ? registers.Fingerprint(mask) : registers.RecomputeFingerprint(*mask);
};
auto shape = (0xcbf29ce484222325ull ^ fingerprint(queue.context, Graphics::RegisterBank::Context)) * 0x100000001b3ull;
shape = (shape ^ fingerprint(queue.shader, Graphics::RegisterBank::Shader)) * 0x100000001b3ull;
shape = (shape ^ fingerprint(queue.userConfig, Graphics::RegisterBank::UserConfig)) * 0x100000001b3ull;
auto exact = shape;
const auto words = [&](std::uint32_t base, std::uint32_t count) {
for (auto it = queue.shader.lower_bound(base); it != queue.shader.end() && it->first < base + count; ++it) {
exact = (exact ^ it->first) * 0x100000001b3ull;
exact = (exact ^ it->second) * 0x100000001b3ull;
}
};
for (const auto base : {0x00cu, 0x08cu, 0x10cu}) {
const auto resources = queue.shader.find(base - 1);
const auto count = resources == queue.shader.end() ? 0u : ((resources->second >> 1u) & 0x1fu) | (((resources->second >> 27u) & 1u) << 5u);
words(base, allUserWords ? 32u : std::min(count, 32u));
}
words(0x082u, 2);
words(0x102u, 2);
return exact;
}
std::uint64_t Driver::drawRegisterKey(const QueueState& queue, const ShaderRegistry& registry, std::uint64_t deviceSerial) {
static const bool allUserWords = std::getenv("APS5_DRAW_KEY_ALL_USER_WORDS") != nullptr;
static const bool verify = std::getenv("APS5_VERIFY_REGISTER_KEYS") != nullptr;
auto key = RegisterStateKey(queue, allUserWords);
if (verify && key != RegisterStateKey(queue, allUserWords, false)) throw std::runtime_error("incremental register fingerprint disagrees with full state");
std::uint64_t key = 0xcbf29ce484222325ull;
const auto mix = [&](std::uint64_t value) {
key ^= value;
key *= 0x100000001b3ull;
};
const auto userEnd = [&](std::uint32_t base) {
if (allUserWords) return base + 32u;
const auto resources = queue.shader.find(base - 1);
if (resources == queue.shader.end()) return base;
const auto count = ((resources->second >> 1u) & 0x1fu) | (((resources->second >> 27u) & 1u) << 5u);
return base + std::min(count, 32u);
};
const std::array<std::pair<std::uint32_t, std::uint32_t>, 3> users{{{0x00cu, userEnd(0x00cu)}, {0x08cu, userEnd(0x08cu)}, {0x10cu, userEnd(0x10cu)}}};
const auto unread = [&](std::uint32_t offset) {
return std::any_of(users.begin(), users.end(), [&](const auto& user) { return offset >= user.second && offset < user.first + 32u; });
};
mix(deviceSerial);
for (const auto& range : Graphics::DrawKeyRegisters) {
const auto& bank = range.bank == Graphics::RegisterBank::Context ? queue.context : range.bank == Graphics::RegisterBank::Shader ? queue.shader : queue.userConfig;
mix((static_cast<std::uint64_t>(range.bank) << 32u) | range.first);
const auto end = range.first + range.count;
const bool shader = range.bank == Graphics::RegisterBank::Shader;
for (auto it = bank.lower_bound(range.first); it != bank.end() && it->first < end; ++it) {
if (shader && unread(it->first)) continue;
mix(it->first);
mix(it->second);
}
}
for (const auto base : {0x008u, 0x088u, 0x0c8u, 0x108u, 0x148u}) {
const auto low = queue.shader.find(base);
const auto high = queue.shader.find(base + 1);
@ -118,7 +84,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) {
const auto sameTess = [](const std::optional<ShaderRecompiler::TessellationConfiguration>& x, const std::optional<ShaderRecompiler::TessellationConfiguration>& y) {
if (x.has_value() != y.has_value()) return false;
if (!x) return true;
return x->inputControlPoints == y->inputControlPoints && x->outputControlPoints == y->outputControlPoints && x->domain == y->domain && x->partitioning == y->partitioning && x->outputTopology == y->outputTopology && x->patchesPerGroup == y->patchesPerGroup;
return x->inputControlPoints == y->inputControlPoints && x->outputControlPoints == y->outputControlPoints && x->domain == y->domain && x->partitioning == y->partitioning && x->outputTopology == y->outputTopology;
};
if (s.stages.path != t.stages.path || s.stages.registerValue != t.stages.registerValue || s.stages.vertexWaveSize != t.stages.vertexWaveSize || s.stages.fragmentWaveSize != t.stages.fragmentWaveSize || !sameMesh(s.stages.mesh, t.stages.mesh) || !sameTess(s.stages.tessellation, t.stages.tessellation)) return false;
if (!sameColor(s.color, t.color) || s.colors.size() != t.colors.size() || s.blends.size() != t.blends.size()) return false;

View File

@ -14,22 +14,13 @@
namespace AgcDriver::DriverDetail {
namespace {
bool matchesBufferPatternFillKernel(std::span<const std::uint32_t> code) {
static constexpr std::array<std::uint32_t, 12> kernel{0xd7460004u, 0x04010c08u, 0xf4280302u, 0xfa000000u, 0xbf8cc07fu, 0x7e00020cu, 0x7e02020du, 0x7e04020eu, 0x7e06020fu, 0xe01c2000u, 0x80000004u, 0xbf810000u};
return code.size() >= kernel.size() && std::equal(kernel.begin(), kernel.end(), code.begin());
}
}
bool Driver::matchesFillKernel(std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) {
static const bool enabled = std::getenv("APS5_NO_FILL_HLE") == nullptr;
static const bool patternEnabled = std::getenv("APS5_NO_PATTERN_FILL_HLE") == nullptr;
if (!enabled || userData.size() < 8 || compute.numThreads[0] != 64 || compute.numThreads[1] != 1 || compute.numThreads[2] != 1) return false;
if (patternEnabled && MatchesPatternFillKernel(code, userData, compute)) return true;
static constexpr std::array<std::uint32_t, 9> fillKernel{0xd7460004u, 0x04010c08u, 0x7e000204u, 0x7e020205u, 0x7e040206u, 0x7e060207u, 0xe01c2000u, 0x80000004u, 0xbf810000u};
if (!matchesBufferPatternFillKernel(code) && (code.size() < fillKernel.size() || !std::equal(fillKernel.begin(), fillKernel.end(), code.begin()))) return false;
if (code.size() < fillKernel.size() || !std::equal(fillKernel.begin(), fillKernel.end(), code.begin())) return false;
const auto stride = (userData[1] >> 16u) & 0x3fffu;
const bool swizzled = ((userData[1] >> 31u) & 1u) != 0;
@ -69,50 +60,6 @@ std::optional<DwordPatternFill> MatchDwordPatternFill(std::span<const std::uint3
return fill;
}
std::optional<DwordPatternFill> MatchZeroDwordFill(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) {
static const bool enabled = std::getenv("APS5_NO_FILL_HLE") == nullptr;
if (!enabled || userData.size() != 4 || packet.size() < 5 || !compute.groupIdEnable[0] || compute.numThreads[0] != 64 || compute.numThreads[1] != 1 || compute.numThreads[2] != 1) return std::nullopt;
static constexpr std::array<std::uint32_t, 6> kernel{0xd7460000u, 0x04010c04u, 0x7e020280u, 0xe0102000u, 0x80000100u, 0xbf810000u};
if (code.size() < kernel.size() || !std::equal(kernel.begin(), kernel.end(), code.begin())) return std::nullopt;
const auto stride = (userData[1] >> 16u) & 0x3fffu;
const bool swizzled = ((userData[1] >> 31u) & 1u) != 0;
const bool addTid = ((userData[3] >> 23u) & 1u) != 0;
const auto outOfBounds = (userData[3] >> 28u) & 3u;
const auto format = (userData[3] >> 12u) & 0x7fu;
const auto type = userData[3] >> 30u;
if (type != 0 || stride != 4 || swizzled || addTid || outOfBounds != 0 || format < 20u || format > 22u || (packet[4] & 0x20u) != 0 || packet[2] != 1 || packet[3] != 1) return std::nullopt;
const auto dwords = std::min<std::uint64_t>(static_cast<std::uint64_t>(packet[1]) * 64u, userData[2]);
const auto base = userData[0] | (static_cast<std::uint64_t>(userData[1] & 0xffffu) << 32u);
if (dwords == 0 || dwords % 4u != 0 || base % 16u != 0) return std::nullopt;
return DwordPatternFill{base, static_cast<std::size_t>(dwords * 4u), {}};
}
std::optional<Graphics::MemoryValueFill> MatchMemoryValueBufferFill(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) {
static const bool enabled = std::getenv("APS5_NO_FILL_HLE") == nullptr;
if (!enabled || userData.size() != 8 || packet.size() < 5 || (packet[4] & 0x20u) != 0 || !compute.groupIdEnable[0]) return std::nullopt;
const auto fill = Graphics::MatchMemoryValueFill(code, userData, compute.numThreads, {packet[1], packet[2], packet[3]});
if (!fill) return std::nullopt;
if (fill->address % 16u != 0 || fill->bytes % 16u != 0) return std::nullopt;
return fill;
}
bool IsByteFill(const std::array<std::uint32_t, 4>& pattern) {
return std::all_of(pattern.begin(), pattern.end(), [&](std::uint32_t word) { return word == (pattern[0] & 0xffu) * 0x01010101u; });
}
std::optional<Graphics::DccKeys> KeysFillMemo(const std::array<std::uint32_t, 4>& pattern, bool coversStorageKeys, bool memoryValueFill) {
if (!IsByteFill(pattern) || !(coversStorageKeys || memoryValueFill)) return std::nullopt;
switch (pattern[0] & 0xffu) {
case 0x00: return Graphics::DccKeys::Clear0000;
case 0x40: return Graphics::DccKeys::Clear0001;
case 0x80: return Graphics::DccKeys::Clear1110;
case 0xc0: return Graphics::DccKeys::Clear1111;
case 0x20: return Graphics::DccKeys::ClearRegister;
case 0xff: return Graphics::DccKeys::Uncompressed;
default: return std::nullopt;
}
}
bool Driver::fillClearEnabled() {
static const bool enabled = std::getenv("APS5_NO_FILL_CLEAR") == nullptr;
return enabled;
@ -178,11 +125,8 @@ void Driver::fillClearCount(const Graphics::StorageTexture::FillCoverage& covera
}
bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, const std::shared_ptr<VulkanDevice>& localDevice) {
auto dwordFill = MatchDwordPatternFill(packet, code, userData, compute);
if (!dwordFill) dwordFill = MatchZeroDwordFill(packet, code, userData, compute);
std::optional<Graphics::MemoryValueFill> memoryFill;
if (!dwordFill) memoryFill = MatchMemoryValueBufferFill(packet, code, userData, compute);
if (!dwordFill && !memoryFill && !matchesFillKernel(code, userData, compute)) return false;
const auto dwordFill = MatchDwordPatternFill(packet, code, userData, compute);
if (!dwordFill && !matchesFillKernel(code, userData, compute)) return false;
std::uint64_t base;
std::size_t bytes;
std::array<std::uint32_t, 4> pattern;
@ -191,15 +135,6 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span<cons
base = dwordFill->base;
bytes = dwordFill->bytes;
pattern = dwordFill->pattern;
} else if (memoryFill) {
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Fill);
inputLock.lock();
recordLabelsForPacket(localDevice.get(), queueId);
if (!GuestMemory::Accessible(reinterpret_cast<const void*>(memoryFill->valueAddress), 4)) return false;
GuestMemory::Read(memoryFill->valueAddress, std::as_writable_bytes(std::span(pattern).first(1)), 4);
std::fill(pattern.begin() + 1, pattern.end(), pattern[0]);
base = memoryFill->address;
bytes = memoryFill->bytes;
} else if (MatchesPatternFillKernel(code, userData, compute)) {
const auto range = DecodePatternFillRange(userData, packet);
if (!range) return false;
@ -231,16 +166,7 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span<cons
const auto records = std::min<std::uint64_t>(static_cast<std::uint64_t>(groups[0]) * 64u, numRecords);
base = userData[0] | (static_cast<std::uint64_t>(userData[1] & 0xffffu) << 32u);
bytes = static_cast<std::size_t>(records * 16u);
if (matchesBufferPatternFillKernel(code)) {
const auto source = userData[4] | (static_cast<std::uint64_t>(userData[5] & 0xffffu) << 32u);
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Fill);
inputLock.lock();
recordLabelsForPacket(localDevice.get(), queueId);
if (!GuestMemory::Accessible(reinterpret_cast<const void*>(source), sizeof(pattern))) return false;
GuestMemory::Read(source, std::as_writable_bytes(std::span(pattern)), 4);
} else {
pattern = {userData[4], userData[5], userData[6], userData[7]};
}
pattern = {userData[4], userData[5], userData[6], userData[7]};
}
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
static std::atomic<std::uint64_t> fills{0}, filledBytes{0}, cpuFills{0};
@ -277,7 +203,7 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span<cons
static const bool traceKeys = std::getenv("APS5_TRACE_DCC_KEYS") != nullptr;
if (traceKeys && coverage.cover == Graphics::StorageTexture::FillCover::Keys) std::fprintf(stderr, "[dcc-keys] title fills keys 0x%llx+0x%zx with %08x %08x %08x %08x (queue 0x%x)\n", static_cast<unsigned long long>(base), bytes, pattern[0], pattern[1], pattern[2], pattern[3], queueId);
const bool uniformKeysFill = coverage.cover == Graphics::StorageTexture::FillCover::Keys && IsByteFill(pattern);
const bool uniformKeysFill = coverage.cover == Graphics::StorageTexture::FillCover::Keys && std::all_of(pattern.begin(), pattern.end(), [&](std::uint32_t word) { return word == (pattern[0] & 0xffu) * 0x01010101u; });
if (uniformKeysFill) {
Graphics::StorageTexture::NoteKeysFill(base, bytes, static_cast<std::uint8_t>(pattern[0]));
Graphics::StorageTexture::ClearByKeysFill(base, bytes, static_cast<std::uint8_t>(pattern[0]));
@ -310,8 +236,18 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span<cons
phase(FillFlush);
const bool stored = cleared || localDevice->FillBuffer(base, bytes, pattern);
phase(FillDevice);
if (stored && !cleared) {
if (const auto filled = KeysFillMemo(pattern, uniformKeysFill, memoryFill.has_value())) Graphics::NoteKeysFillOnGpu(base, bytes, *filled);
if (uniformKeysFill && stored && !cleared) {
Graphics::DccKeys filled = Graphics::DccKeys::Mixed;
switch (pattern[0] & 0xffu) {
case 0x00: filled = Graphics::DccKeys::Clear0000; break;
case 0x40: filled = Graphics::DccKeys::Clear0001; break;
case 0x80: filled = Graphics::DccKeys::Clear1110; break;
case 0xc0: filled = Graphics::DccKeys::Clear1111; break;
case 0x20: filled = Graphics::DccKeys::ClearRegister; break;
case 0xff: filled = Graphics::DccKeys::Uncompressed; break;
default: break;
}
if (filled != Graphics::DccKeys::Mixed) Graphics::NoteKeysFillOnGpu(base, bytes, filled);
}
if (profile && stored && !cleared) {
const bool uniform = pattern[0] == pattern[1] && pattern[1] == pattern[2] && pattern[2] == pattern[3];

View File

@ -205,7 +205,6 @@ void Driver::execute(const Submission& submission) {
} else if (header == FlipPacketHeader) {
CheckFailure();
std::uint64_t batchesAtFlip = 0, unsignaledAtFlip = 0;
bool flipInterruptDeferred = false;
if (!drains) {
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush);
@ -213,13 +212,8 @@ void Driver::execute(const Submission& submission) {
const auto localDevice = device.Load();
if (!recordLabelsForPacket(localDevice.get(), submission.queue) && localDevice != nullptr) localDevice->SubmitRecorded(submission.queue == 0);
if (localDevice != nullptr) {
localDevice->FlipBatches(batchesAtFlip, unsignaledAtFlip);
const auto queueId = submission.queue;
flipInterruptDeferred = localDevice->AfterRecordedWork([queueId] { AgcDriverDeliverEopInterrupt(queueId); }, false);
}
if (localDevice != nullptr) localDevice->FlipBatches(batchesAtFlip, unsignaledAtFlip);
}
if (!flipInterruptDeferred) AgcDriverDeliverEopInterrupt(submission.queue);
++flipsCounted;
if (batchesAtFlip != 0) flipSerial = batchesAtFlip;
flipBatchesUnsignaled += unsignaledAtFlip;

View File

@ -154,12 +154,10 @@ void Driver::reportPresents(double waitedMs, std::size_t inFlight) {
const auto counts = VulkanDevice::PresentCounts();
const auto presents = counts.presents - previous.presents;
const double perPresent = presents != 0 ? 1.0 / static_cast<double>(presents) : 0.0;
const auto dropped = counts.acquireDrops - previous.acquireDrops;
const double perFrame = presents + dropped != 0 ? 1.0 / static_cast<double>(presents + dropped) : 0.0;
const auto flips = flipsCounted.load() - previousFlips;
const double perFlip = flips != 0 ? 1.0 / static_cast<double>(flips) : 0.0;
const auto serial = flipSerial.load();
AgcDriver::ProfilePrint_nid_no_patch( "[present] %llu presents over 10 s (%llu dropped: no free swapchain image; %zu may trail on the GPU); %s p50 %.2f ms, p90 %.2f ms, max %.2f ms over %zu; per frame, presented or dropped: GPU busy %.1f ms, idle gaps %.1f ms (%llu batches without a completion record); per flip: %.1f batches recorded (%.1f unsignaled at the flip); per present: %.1f batches ahead of the blit, %.1f after it; after the flip packet: last submit %.1f ms, blit submit %.1f ms; in-place reads overwritten before execution: %llu of %llu checked\n", static_cast<unsigned long long>(presents), static_cast<unsigned long long>(dropped), inFlight, inFlight != 0 ? "inflight_wait" : "render_fence_wait", percentile(0.5), percentile(0.9), waits.empty() ? 0.0 : waits.back(), waits.size(), (counts.gpuBusyMs - previous.gpuBusyMs) * perFrame, (counts.gpuGapMs - previous.gpuGapMs) * perFrame, static_cast<unsigned long long>(counts.gpuUnread - previous.gpuUnread), static_cast<double>(serial - windowSerial) * perFlip, static_cast<double>(flipBatchesUnsignaled.load() - previousUnsignaled) * perFlip, static_cast<double>(counts.batchesAheadOfBlit - previous.batchesAheadOfBlit) * perPresent, static_cast<double>(counts.batchesAfterFlip - previous.batchesAfterFlip) * perPresent, (counts.lastSubmitAfterFlipMs - previous.lastSubmitAfterFlipMs) * perPresent, (counts.blitSubmitAfterFlipMs - previous.blitSubmitAfterFlipMs) * perPresent, static_cast<unsigned long long>(counts.readsOverwritten - previous.readsOverwritten), static_cast<unsigned long long>(counts.readsChecked - previous.readsChecked));
AgcDriver::ProfilePrint_nid_no_patch( "[present] %llu presents over 10 s (%zu may trail on the GPU); %s p50 %.2f ms, p90 %.2f ms, max %.2f ms over %zu; per present: GPU busy %.1f ms, idle gaps %.1f ms (%llu batches without a completion record); per flip: %.1f batches recorded (%.1f unsignaled at the flip); per present: %.1f batches ahead of the blit, %.1f after it; after the flip packet: last submit %.1f ms, blit submit %.1f ms; in-place reads overwritten before execution: %llu of %llu checked\n", static_cast<unsigned long long>(presents), inFlight, inFlight != 0 ? "inflight_wait" : "render_fence_wait", percentile(0.5), percentile(0.9), waits.empty() ? 0.0 : waits.back(), waits.size(), (counts.gpuBusyMs - previous.gpuBusyMs) * perPresent, (counts.gpuGapMs - previous.gpuGapMs) * perPresent, static_cast<unsigned long long>(counts.gpuUnread - previous.gpuUnread), static_cast<double>(serial - windowSerial) * perFlip, static_cast<double>(flipBatchesUnsignaled.load() - previousUnsignaled) * perFlip, static_cast<double>(counts.batchesAheadOfBlit - previous.batchesAheadOfBlit) * perPresent, static_cast<double>(counts.batchesAfterFlip - previous.batchesAfterFlip) * perPresent, (counts.lastSubmitAfterFlipMs - previous.lastSubmitAfterFlipMs) * perPresent, (counts.blitSubmitAfterFlipMs - previous.blitSubmitAfterFlipMs) * perPresent, static_cast<unsigned long long>(counts.readsOverwritten - previous.readsOverwritten), static_cast<unsigned long long>(counts.readsChecked - previous.readsChecked));
previous = counts;
previousFlips = flipsCounted.load();
previousUnsignaled = flipBatchesUnsignaled.load();

View File

@ -141,7 +141,6 @@ bool RawGraphicsDrawn() {
namespace {
void AwaitRegisteredPreparation(PreparedShaders& prepared, std::unique_lock<std::mutex>& lock) {
if (prepared.pending) PrioritizeRegistrationPreparation(prepared);
prepared.settled.wait(lock, [&] { return !prepared.pending; });
if (prepared.failure != nullptr) std::rethrow_exception(prepared.failure);
}
@ -399,10 +398,6 @@ void ReportPreparedAtUse(const ShaderSnapshot& snapshot, const ShaderRecompiler:
APS5_LOG_ERR("The null pixel program has no wave%u artifact for this draw; preparing it at draw", request.context.waveSize);
return;
}
if (request.context.vertex && (request.context.vertex->paClVsOutCntl & (1u << 18u)) != 0) {
APS5_LOG_OUT_DEBUG("Shader 0x%llx is drawn into a layered target; preparing its layered variant at draw", static_cast<unsigned long long>(snapshot.codeAddress));
return;
}
if (!snapshot.header.empty()) {
if (!snapshot.prepared->deferred) APS5_LOG_ERR("Shader 0x%llx stage %u has no artifact prepared at registration for the state of this draw or dispatch; preparing it at use", static_cast<unsigned long long>(request.shader.codeAddress), static_cast<std::uint32_t>(request.shader.stage));
return;
@ -714,7 +709,6 @@ std::unique_ptr<RegisteredPreparation> PlanRegistered(const ShaderSnapshot& snap
plan->memory = {{{snapshot.codeAddress, std::as_bytes(std::span(snapshot.code))}, {snapshot.headerAddress, snapshot.header}}};
auto& request = plan->request.emplace(ShaderRecompiler::RecompileRequest{{stage, address, code, snapshot.headerAddress, snapshot.header}, {wave, firstUser, plan->userData, compute, pixel, vertex, plan->memory, RegisteredFloatMode(snapshot)}, stage == Stage::Compute ? device.ComputeTarget(wave) : device.Target(), {0, 0, 0, 128}, graphics});
if (graphics && graphics->mesh) request.layout.pushConstantSizeBytes = ShaderRecompiler::MeshDrawPushOffsetBytes;
if (graphics && graphics->tessellation) request.layout.pushConstantSizeBytes = ShaderRecompiler::TessellationLevelPushOffsetBytes;
if (pixel && device.GraphicsPipelineLibraries()) request.layout.pushConstantOffsetBytes = Graphics::FixedPushOffset(Stage::Fragment);
return plan;
}
@ -765,18 +759,10 @@ std::vector<PreparedShaders::Entry> PrepareRegistered(const ShaderSnapshot& snap
class RegistrationPreparePool {
public:
static RegistrationPreparePool& Instance() {
static auto* pool = [] {
auto* created = new RegistrationPreparePool(std::max(2u, std::thread::hardware_concurrency() / 2u));
PreparationSlot().store(created);
return created;
}();
static auto* pool = new RegistrationPreparePool(std::max(2u, std::thread::hardware_concurrency() / 2u));
return *pool;
}
static RegistrationPreparePool* Existing() {
return PreparationSlot().load();
}
static RegistrationPreparePool& Prebuild() {
static auto* pool = [] {
const char* text = std::getenv("APS5_PREBUILD_THREADS");
@ -792,11 +778,6 @@ public:
}
private:
static std::atomic<RegistrationPreparePool*>& PreparationSlot() {
static std::atomic<RegistrationPreparePool*> slot{nullptr};
return slot;
}
static std::atomic<RegistrationPreparePool*>& PrebuildSlot() {
static std::atomic<RegistrationPreparePool*> slot{nullptr};
return slot;
@ -804,12 +785,11 @@ private:
public:
void Submit(std::function<void()> task, const PreparedShaders* prepared = nullptr) {
void Submit(std::function<void()> task) {
{
std::unique_lock lock(mutex);
if (threads != 0 && !closed) {
if (prepared != nullptr && prepared->demanded.load()) tasks.push_front({std::move(task), prepared});
else tasks.push_back({std::move(task), prepared});
tasks.push_back(std::move(task));
lock.unlock();
ready.notify_one();
return;
@ -818,14 +798,6 @@ public:
task();
}
void Prioritize(const PreparedShaders& prepared) {
std::lock_guard lock(mutex);
const auto found = std::ranges::find_if(tasks, [&](const auto& task) { return task.prepared == &prepared; });
if (found == tasks.end()) return;
if (std::getenv("APS5_TRACE_PREPARATION_WAIT") != nullptr) std::fprintf(stderr, "[shader-prepare] demanded queued task at position %zu of %zu\n", static_cast<std::size_t>(std::distance(tasks.begin(), found)), tasks.size());
std::rotate(tasks.begin(), found, std::next(found));
}
void Close() {
std::unique_lock lock(mutex);
closed = true;
@ -851,7 +823,7 @@ private:
auto& pool = *static_cast<RegistrationPreparePool*>(argument);
if (PerformanceTrace::Configured()) PerformanceTrace::BindThread("shader prepare", TraceThread::DefaultCapacity / 8);
for (;;) {
Task task;
std::function<void()> task;
{
std::unique_lock lock(pool.mutex);
pool.ready.wait(lock, [&] { return !pool.tasks.empty(); });
@ -859,8 +831,8 @@ private:
pool.tasks.pop_front();
++pool.running;
}
task.function();
task = {};
task();
task = nullptr;
{
std::lock_guard lock(pool.mutex);
--pool.running;
@ -870,15 +842,10 @@ private:
return nullptr;
}
struct Task {
std::function<void()> function;
const PreparedShaders* prepared = nullptr;
};
std::mutex mutex;
std::condition_variable ready;
std::condition_variable idle;
std::deque<Task> tasks;
std::deque<std::function<void()>> tasks;
unsigned threads = 0;
unsigned running = 0;
bool closed = false;
@ -890,7 +857,6 @@ bool AsyncRegistrationPrepare() {
}
void PrepareRegisteredInBackground(std::shared_ptr<const ShaderSnapshot> snapshot, std::shared_ptr<RegisteredPreparation> plan) {
const auto* preparedKey = snapshot->prepared.get();
SubmitRegistrationPreparation([snapshot = std::move(snapshot), plan = std::move(plan)] {
PerformanceContext timingContext(FrameTiming::Preparation());
std::vector<PreparedShaders::Entry> entries;
@ -912,18 +878,13 @@ void PrepareRegisteredInBackground(std::shared_ptr<const ShaderSnapshot> snapsho
prepared.pending = false;
}
prepared.settled.notify_all();
}, preparedKey);
});
}
}
void SubmitRegistrationPreparation(std::function<void()> task, const PreparedShaders* prepared) {
RegistrationPreparePool::Instance().Submit(std::move(task), prepared);
}
void PrioritizeRegistrationPreparation(PreparedShaders& prepared) {
prepared.demanded.store(true);
if (auto* pool = RegistrationPreparePool::Existing()) pool->Prioritize(prepared);
void SubmitRegistrationPreparation(std::function<void()> task) {
RegistrationPreparePool::Instance().Submit(std::move(task));
}
void SubmitLibraryPrebuild(std::function<void()> task) {
@ -962,7 +923,7 @@ std::vector<PreparedGraphicsStage> PrepareGraphicsStages(const DrawDecode& decod
std::vector<PreparedGraphicsStage> prepared;
std::uint32_t pushOffset = 0;
const bool fixedSlots = target.fixedPushSlots;
const auto capacity = decoded.state.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : decoded.state.stages.tessellation ? ShaderRecompiler::TessellationLevelPushOffsetBytes : Graphics::PipelinePushSlotBytes;
const auto capacity = decoded.state.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : Graphics::PipelinePushSlotBytes;
for (std::size_t index = 0; index < decoded.programs.size(); ++index) {
if (decoded.roles[index] == ShaderRecompiler::ProgramRole::GeometryBack) continue;
const auto& program = decoded.programs[index];
@ -970,7 +931,7 @@ std::vector<PreparedGraphicsStage> PrepareGraphicsStages(const DrawDecode& decod
const auto wave = fragment ? decoded.state.stages.fragmentWaveSize : decoded.state.stages.vertexWaveSize;
std::optional<ShaderRecompiler::ShaderVertexStageInfo> vertex;
if (!fragment) vertex = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, nullptr, true, 8u - program.firstUserSgpr);
if (vertex) vertex->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, decoded.state.paClVsOutCntl) | decoded.state.layerExports;
if (vertex) vertex->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, decoded.state.paClVsOutCntl);
pushOffset = Graphics::StagePushOffset(pushOffset, program.binary.stage, fixedSlots);
const auto offset = pushOffset;
const auto room = (fixedSlots && fragment ? Graphics::PipelinePushSlotBytes : capacity) - pushOffset % Graphics::PipelinePushSlotBytes;

View File

@ -1212,14 +1212,7 @@ bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function
saved.assign(source, source + (stop - first));
}
const bool before = covered && walkWrites(tracker, first, stop, StampKind::Cpu, true);
bool imported = false;
{
#ifdef __APPLE__
const GuestArena::HostWrite open(reinterpret_cast<void*>(first), static_cast<std::size_t>(stop - first));
#endif
imported = import();
}
if (!imported) return false;
if (!import()) return false;
if (!staysWatched) {
if (!before || !walkWrites(tracker, first, stop, StampKind::ImportWindow, true)) unwatchLocked(tracker, first, static_cast<std::size_t>(stop - first));
unwatchLocked(tracker, address, bytes);
@ -1270,30 +1263,6 @@ bool UnchangedSinceAll(std::span<const UnchangedQuery> queries) {
return true;
}
std::uint64_t CollectCommittedWrites(std::uint64_t address, std::size_t bytes, std::vector<std::pair<std::uint64_t, std::uint64_t>>& ranges) {
ranges.clear();
if (bytes == 0 || bytes > std::numeric_limits<std::uint64_t>::max() - address || !WriteWatched()) return 0;
auto committed = CommittedRanges(address, bytes);
auto generation = TrackerGeneration();
for (const auto& [begin, end] : committed) {
generation = CollectWrites(begin, static_cast<std::size_t>(end - begin));
if (generation == 0) return 0;
}
ranges = std::move(committed);
return generation;
}
bool UnchangedCommittedSince(std::span<const std::pair<std::uint64_t, std::uint64_t>> ranges, std::uint64_t generation) {
auto& tracker = Tracker();
const auto lock = lockTracker(tracker);
tracker.initialize();
if (!tracker.watched || generation == 0) return false;
for (const auto& [begin, end] : ranges) {
if (end <= begin || !tracker.covers(begin, static_cast<std::size_t>(end - begin)) || tracker.newestStamp(begin, end) > generation) return false;
}
return true;
}
std::uint64_t storeOwn(std::uint64_t address, std::size_t bytes, const std::function<std::pair<std::uint64_t, std::uint64_t>()>& store) {
if (bytes == 0) return 0;
const auto stampStored = [](WriteTracker& tracker, std::pair<std::uint64_t, std::uint64_t> stored) -> std::uint64_t {

View File

@ -94,7 +94,7 @@ bool CoreVulkanCapability(std::uint32_t capability) {
}
}
void CreateComputePipeline(VkDevice device, PFN_vkCreateComputePipelines create, VkPipelineCache cache, VkShaderModule module, VkPipelineLayout layout, VkPipelineCreateFlags flags, VkPipelineShaderStageCreateFlags stageFlags, const VkSpecializationInfo* specialization, std::uint32_t subgroupSize, VkPipeline* pipeline, Graphics::PipelineCacheSynchronization* synchronization = nullptr) {
void CreateComputePipeline(VkDevice device, PFN_vkCreateComputePipelines create, VkPipelineCache cache, VkShaderModule module, VkPipelineLayout layout, VkPipelineCreateFlags flags, VkPipelineShaderStageCreateFlags stageFlags, const VkSpecializationInfo* specialization, std::uint32_t subgroupSize, VkPipeline* pipeline) {
VkComputePipelineCreateInfo info{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO};
info.flags = flags;
info.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
@ -107,7 +107,6 @@ void CreateComputePipeline(VkDevice device, PFN_vkCreateComputePipelines create,
subgroup.requiredSubgroupSize = subgroupSize;
if (subgroupSize != 0) info.stage.pNext = &subgroup;
info.layout = layout;
auto cacheLock = synchronization != nullptr ? synchronization->LockForPipelineUse() : Graphics::PipelineCacheSynchronization::PipelineUseLock{};
check(create(device, cache, 1, &info, nullptr, pipeline), "vkCreateComputePipelines");
}
@ -274,7 +273,6 @@ struct VulkanDevice::State {
bool shaderClock = false;
bool narrowSubgroupClock = false;
bool float64DenormPreserve = false;
bool float16DenormPreserve = false;
// VK_EXT_descriptor_indexing with non-uniform image array indexing (bindless image tables in
// graphics stages, and compute workgroups wider than a wave).
bool descriptorIndexing = false;
@ -285,7 +283,6 @@ struct VulkanDevice::State {
VkPhysicalDeviceDescriptorIndexingPropertiesEXT descriptorIndexingProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_PROPERTIES_EXT};
bool imageInt64Atomics = false;
bool bufferInt64Atomics = false;
bool viewportIndexLayer = false;
bool primitiveListRestart = false;
bool depthClipControl = false;
bool graphicsPipelineLibrary = false;
@ -1152,12 +1149,6 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
state->imageInt64Atomics = true;
state->spirvExtensions.push_back("SPV_EXT_shader_image_int64");
}
if (hasExtension(VK_EXT_SHADER_VIEWPORT_INDEX_LAYER_EXTENSION_NAME)) {
deviceExtensions.push_back(VK_EXT_SHADER_VIEWPORT_INDEX_LAYER_EXTENSION_NAME);
state->capabilities.push_back(spv::CapabilityShaderViewportIndexLayerEXT);
state->spirvExtensions.push_back("SPV_EXT_shader_viewport_index_layer");
state->viewportIndexLayer = true;
}
deviceExtensions.push_back(VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME);
state->capabilities.push_back(spv::CapabilitySignedZeroInfNanPreserve);
state->spirvExtensions.push_back("SPV_KHR_float_controls");
@ -1176,9 +1167,6 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
state->capabilities.push_back(spv::CapabilityRoundingModeRTE);
state->capabilities.push_back(spv::CapabilityDenormPreserve);
}
state->float16DenormPreserve = floatControls.shaderDenormPreserveFloat16 == VK_TRUE &&
floatControls.denormBehaviorIndependence == VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL;
if (state->float16DenormPreserve && std::find(state->capabilities.begin(), state->capabilities.end(), static_cast<std::uint32_t>(spv::CapabilityDenormPreserve)) == state->capabilities.end()) state->capabilities.push_back(spv::CapabilityDenormPreserve);
deviceExtensions.push_back(VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME);
deviceExtensions.push_back(VK_KHR_8BIT_STORAGE_EXTENSION_NAME);
state->capabilities.push_back(4448);
@ -2177,7 +2165,7 @@ VulkanDevice::CopyOutcome VulkanDevice::CopyBuffer(std::uint64_t destination, st
// a completion-deferred label): it lands only when that batch is reaped, whatever its fence.
const auto completionStoreOverlaps = [&](std::uint64_t address) {
if (alwaysSync || state->CopiedWriterOverlaps(address, bytes)) return true;
return Graphics::Recorder::PendingCompletionLabels() != 0 && recorder.CompletionLabelIn(address, bytes);
return Graphics::Recorder::PendingCompletionLabels() != 0 && recorder.PendingLabelIn(address, bytes);
};
// The CPU decision (see the header): pure queries, in the order that makes the first failing
// test the reason; a signaled writer of the source is fine (its in-place stores are final in
@ -2570,23 +2558,9 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) {
static const bool storeAtFlip = Graphics::StorageTexture::StoreAtFlipRequested(std::getenv("APS5_STORE_AT_FLIP"));
if (storeAtFlip) Graphics::StorageTexture::FlushAllPending("flip");
else Graphics::StorageTexture::FlushDumpSelected("flip");
auto& dumps = Dumps();
if (buffer.tilingMode == 1) {
const auto pixels = ReadDisplayBuffer(buffer);
bool dumpFrame = false;
if (dumps.dumped < dumps.limit && ++dumps.presents % static_cast<std::uint64_t>(dumps.every) == 0) {
if (dumps.cpu) {
WriteFrameBmp(dumps.dumped++, buffer.width, buffer.height, pixels, DumpScale());
} else {
state->nextDumpIndex = dumps.dumped++;
dumpFrame = true;
}
}
if (!present(buffer.width, buffer.height, true, pixels, nullptr, nullptr, VK_FILTER_LINEAR, dumpFrame)) {
if (dumpFrame) --dumps.dumped;
return false;
}
return true;
return present(buffer.width, buffer.height, true, pixels);
}
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
// The display buffer is usually a resident render target whose results are still on the GPU: it
@ -2625,6 +2599,7 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) {
}
}
const auto cleared = buffer.dccAddress != 0 && resident == nullptr ? CompressedClearPixel(buffer, bytes) : std::nullopt;
auto& dumps = Dumps();
bool dumpFrame = false;
if (dumps.dumped < dumps.limit && ++dumps.presents % static_cast<std::uint64_t>(dumps.every) == 0) {
if (dumps.cpu) {
@ -2667,15 +2642,11 @@ bool VulkanDevice::AcquireImage() {
std::uint32_t index = 0;
timing.Mark("fence_reset");
// An out-of-date swapchain (the window changed) drops this frame; the next Resize recreates it.
const auto acquired = state->DeviceFunction<PFN_vkAcquireNextImageKHR>("vkAcquireNextImageKHR")(state->device, state->swapchain, 0, VK_NULL_HANDLE, state->acquireFence, &index);
const auto acquired = state->DeviceFunction<PFN_vkAcquireNextImageKHR>("vkAcquireNextImageKHR")(state->device, state->swapchain, 5'000'000'000ULL, VK_NULL_HANDLE, state->acquireFence, &index);
if (acquired == VK_ERROR_OUT_OF_DATE_KHR) {
state->extent = {0, 0};
return false;
}
if (acquired == VK_NOT_READY || acquired == VK_TIMEOUT) {
++presentCounters.acquireDrops;
return false;
}
if (acquired != VK_SUBOPTIMAL_KHR) check(acquired, "vkAcquireNextImageKHR");
timing.Mark("acquire_image");
APS5_LOG_OUT_DEBUG("vkAcquireNextImageKHR index=%u imageCount=%zu", index, state->images.size());
@ -3052,7 +3023,6 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const {
target.nonConstantImageOffsets = state->maintenance8;
target.narrowSubgroupClock = state->narrowSubgroupClock;
target.float64DenormPreserve = state->float64DenormPreserve;
target.float16DenormPreserve = state->float16DenormPreserve;
target.fixedPushSlots = state->graphicsPipelineLibrary;
target.registrationModules = state->graphicsPipelineLibrary;
target.srgbDecodeFormats = state->srgbDecodeFormats;
@ -3134,7 +3104,6 @@ Graphics::Context VulkanDevice::buildContext() const {
state->depthClamp,
state->hostImportAlignment
};
context.pipelineCacheSynchronization = state->pipelineCache ? &state->pipelineCache->Synchronization() : nullptr;
context.dmaBufImport = state->dmaBufImport;
context.recorder = state->recorder.get();
context.descriptorCache = state->descriptorCache.get();
@ -3167,7 +3136,6 @@ Graphics::Context VulkanDevice::buildContext() const {
context.nullDescriptors = state->shaderProfile != nullptr && state->shaderProfile->NullDescriptors();
context.bufferInt64Atomics = state->bufferInt64Atomics;
context.placeholderImages = state->placeholderImages.get();
context.viewportIndexLayer = state->viewportIndexLayer;
context.primitiveListRestart = state->primitiveListRestart;
context.imageViewMinLod = state->imageViewMinLod;
context.pipelineExecutableInfo = state->pipelineExecutableInfo;
@ -4290,7 +4258,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
std::fprintf(stderr, "[compute-capture] %s\n", path.string().c_str());
}
if (profile && shader.spirv.size() > 100000) std::fprintf(stderr, "[dispatch] creating a pipeline for %zu SPIR-V words (program 0x%llx)\n", shader.spirv.size(), static_cast<unsigned long long>(programAddress));
CreateComputePipeline(state->device, state->DeviceFunction<PFN_vkCreateComputePipelines>("vkCreateComputePipelines"), context.pipelineCache, objects->module, objects->layout, pipelineFlags, 0, specialization.Info(), subgroupSize, &objects->pipeline, context.pipelineCacheSynchronization);
CreateComputePipeline(state->device, state->DeviceFunction<PFN_vkCreateComputePipelines>("vkCreateComputePipelines"), context.pipelineCache, objects->module, objects->layout, pipelineFlags, 0, specialization.Info(), subgroupSize, &objects->pipeline);
objects->destroyModule(objects->device, objects->module, nullptr);
objects->module = VK_NULL_HANDLE;
Graphics::LogPipelineStatistics_nid_no_patch(context, objects->pipeline);

View File

@ -9,8 +9,6 @@
#include <cstdint>
#include <cstdio>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <stdexcept>
#include <string>
#include <vector>
@ -30,19 +28,6 @@ class SamplerCache;
class ShaderResources;
class PlaceholderImages;
class PipelineCacheSynchronization {
public:
using PipelineUseLock = std::shared_lock<std::shared_mutex>;
using SnapshotLock = std::unique_lock<std::shared_mutex>;
PipelineUseLock LockForPipelineUse() const { return PipelineUseLock(mutex); }
SnapshotLock LockForSnapshot() const { return SnapshotLock(mutex); }
SnapshotLock TryLockForSnapshot() const { return SnapshotLock(mutex, std::try_to_lock); }
private:
mutable std::shared_mutex mutex;
};
inline void Require(bool condition, const std::string& reason) {
if (!condition) throw std::runtime_error("AGC graphics: " + reason);
}
@ -180,7 +165,6 @@ struct Context {
bool nullDescriptors = false;
bool bufferInt64Atomics = false;
const PlaceholderImages* placeholderImages = nullptr;
bool viewportIndexLayer = false;
bool primitiveListRestart = false;
bool imageViewMinLod = false;
bool pipelineExecutableInfo = false;
@ -191,11 +175,6 @@ struct Context {
bool provokingVertexModePerPipeline = false;
bool singlePassStorage = false;
VkSampleCountFlags sampleLocationSampleCounts = 0;
PipelineCacheSynchronization* pipelineCacheSynchronization = nullptr;
PipelineCacheSynchronization::PipelineUseLock LockPipelineCacheForUse() const {
return pipelineCacheSynchronization != nullptr ? pipelineCacheSynchronization->LockForPipelineUse() : PipelineCacheSynchronization::PipelineUseLock{};
}
template<typename TFunction>
TFunction Function(const char* name) const {

View File

@ -33,9 +33,7 @@ struct HostImport {
std::uint64_t serial = 0;
bool unwatched = false;
bool dmaBuf = false;
bool joined = false;
std::shared_ptr<void> chunk;
mutable std::uint64_t lastUse = 0;
};
enum class ImportWatch : std::uint8_t { Watch, Unwatch };
@ -60,10 +58,6 @@ void SetImportWatch(const Context& context, ImportWatch watch);
// the import's buffer.
const HostImport* HostImportFor(const Context& context, std::uint64_t address, std::size_t bytes);
void SetHostImportWindow(std::uint64_t bytes);
// Tests: the window of one large registered range (64 MiB) and the import budget (0: the configured one).
void SetHostImportRangeWindow(std::uint64_t bytes);
void SetHostImportBudget(std::uint64_t bytes);
std::uint64_t HostImportLiveBytes();
bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& ranges, std::uint64_t generation, bool adoptDevice);
// Whether an existing import covers [address, address + bytes), without reconciling the imports
// with the registry or making one (HostImportFor may take a registry lease): a hint for choices
@ -384,7 +378,6 @@ private:
// Import registry epoch when `direct` pointers were taken at acquire time; they are reused while
// no import was destroyed since.
std::uint64_t importsEpoch = 0;
std::uint64_t importFloor = 0;
std::vector<Region> regions;
// Whether `regions` is in ascending address order (true right after AcquireRegistered, whose
// regions follow the registry's order), so AddSnapshot can search instead of scanning.

View File

@ -15,7 +15,7 @@ void LogPipelineStatistics_nid_no_patch(const Context& context, VkPipeline pipel
// the batch completes, so a recorded draw keeps the object (Recorder::Keep) like its pipeline.
class Framebuffer {
public:
Framebuffer(const Context& context, VkRenderPass renderPass, std::span<const VkImageView> targets, VkExtent2D extent, std::uint32_t layers = 1);
Framebuffer(const Context& context, VkRenderPass renderPass, std::span<const VkImageView> targets, VkExtent2D extent);
~Framebuffer();
Framebuffer(const Framebuffer&) = delete;
Framebuffer& operator=(const Framebuffer&) = delete;
@ -47,7 +47,7 @@ public:
Pipeline(const Pipeline&) = delete;
Pipeline& operator=(const Pipeline&) = delete;
VkPipelineLayout Layout() const;
std::shared_ptr<Framebuffer> AcquireFramebuffer(std::span<const VkImageView> targets, std::span<const std::shared_ptr<StorageTexture>> owners, VkExtent2D extent, std::uint32_t layers = 1, std::span<const std::uint8_t> persistent = {});
std::shared_ptr<Framebuffer> AcquireFramebuffer(std::span<const VkImageView> targets, std::span<const std::shared_ptr<StorageTexture>> owners, VkExtent2D extent, std::span<const std::uint8_t> persistent = {});
// Begins the render pass on the framebuffer, binds the pipeline and sets its dynamic state.
void Begin(VkCommandBuffer commands, const Framebuffer& framebuffer, VkExtent2D extent, const State& state) const;
// The same inside a render pass another pipeline of the same attachments began (compatible by
@ -68,7 +68,6 @@ private:
std::vector<std::weak_ptr<StorageTexture>> owners;
std::vector<bool> owned;
VkExtent2D extent;
std::uint32_t layers = 1;
std::shared_ptr<Framebuffer> framebuffer;
};
void release() noexcept;
@ -124,7 +123,7 @@ bool ValidateSampleLocations(const Context& context, const State& state);
void ValidateShaderPair(const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment);
bool ShadersWriteMemory(std::span<const CompiledShader> shaders);
// Returns the color attachment locations the pixel shader writes.
std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false, bool imageInt64Atomics = false, bool geometryShader = false, bool sampleRateShading = false, bool bufferInt64Atomics = false, bool shaderClipDistance = false, bool shaderCullDistance = false, bool viewportIndexLayer = false);
std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false, bool imageInt64Atomics = false, bool geometryShader = false, bool sampleRateShading = false, bool bufferInt64Atomics = false, bool shaderClipDistance = false, bool shaderCullDistance = false);
}

View File

@ -21,7 +21,6 @@ public:
PipelineCache(const PipelineCache&) = delete;
PipelineCache& operator=(const PipelineCache&) = delete;
VkPipelineCache Handle() const { return cache; }
PipelineCacheSynchronization& Synchronization() { return synchronization; }
private:
void load(std::vector<std::byte>& initialData);
@ -31,7 +30,6 @@ private:
Context context;
VkPhysicalDeviceProperties properties{};
VkPipelineCache cache = VK_NULL_HANDLE;
PipelineCacheSynchronization synchronization;
std::filesystem::path path;
std::size_t savedBytes = 0;
std::mutex mutex;

View File

@ -66,7 +66,7 @@ private:
};
void RequireFilterMinmax(const Context& context, VkFormat format, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers);
bool ForcesDegammaOn(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers);
void RequireDegammaFormat(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers);
void RequireBorderSwizzle(std::uint32_t bcSwizzle, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers);
void RequireNonSeamlessCube(bool cube, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers);

View File

@ -38,7 +38,6 @@ TextureCacheUse TextureCacheUsage();
std::uint64_t SampledTextureCacheBudget(const Context& context);
std::shared_ptr<Texture> CachedSampledTexture(const Context& context, std::span<const std::uint32_t> words);
bool SampledTexturesShareEntry(std::span<const std::uint32_t> first, std::span<const std::uint32_t> second);
void CompletePendingCpuWrites(const Context& context, std::uint64_t address, std::size_t bytes);
// The cached storage image of a surface (render targets use it as their resident image); brought up
// to date with guest memory before it is returned.
@ -433,7 +432,6 @@ private:
Context context;
std::vector<std::uint32_t> layoutKey;
GuestBufferMemory guestMemory;
std::size_t maxDrawSnapshotBytes = MaxDrawSnapshotBytes();
std::unique_ptr<BdaResources> bda;
bool usesBda = false;
bool usesFaultBuffer = false;
@ -467,7 +465,6 @@ private:
std::vector<std::shared_ptr<Texture>> textures;
std::vector<bool> textureFirstLayer;
std::vector<bool> textureBaseLevel;
std::vector<bool> textureSrgb;
std::vector<std::shared_ptr<StorageTexture>> storageTextures;
std::vector<std::uint32_t> storageMips;
std::vector<std::uint64_t> storageKeys;

View File

@ -73,7 +73,7 @@ inline VkShaderStageFlags PushConstantStages(std::span<const CompiledShader> sha
VkShaderStageFlags result = 0;
for (const auto& shader : shaders) {
Require(shader.program != nullptr, "missing compiled shader");
if (!shader.program->pushConstants.empty() || shader.stage == ShaderRecompiler::ShaderStage::Mesh || shader.stage == ShaderRecompiler::ShaderStage::TessellationControl) result |= VulkanStage(shader.stage);
if (!shader.program->pushConstants.empty() || shader.stage == ShaderRecompiler::ShaderStage::Mesh) result |= VulkanStage(shader.stage);
}
return result;
}

View File

@ -3,7 +3,6 @@
#include "prx/libSceAgcDriver/Graphics/include/Context.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp"
#include "prx/libSceAgcDriver/Execution/include/QueueState.hpp"
#include <vector>
#include <array>
@ -29,8 +28,6 @@ struct ShaderStages {
std::uint32_t fragmentWaveSize;
std::optional<ShaderRecompiler::MeshConfiguration> mesh;
std::optional<ShaderRecompiler::TessellationConfiguration> tessellation;
float minTessellationLevel = 1.0f;
float maxTessellationLevel = 64.0f;
};
struct ColorTarget {
@ -61,7 +58,6 @@ struct ColorTarget {
std::uint64_t arrayAddress = 0;
std::uint32_t arrayLastSlice = 0;
std::uint32_t arraySlice = 0;
std::uint32_t layers = 1;
std::uint32_t exportIndex = 0;
bool uintExport = false;
std::uint32_t pipeBankXor = 0;
@ -106,8 +102,6 @@ struct State {
bool rectList = false;
bool dualSourceBlend = false;
VkExtent2D renderExtent;
std::uint32_t renderLayers = 1;
std::uint32_t layerExports = 0;
VkPrimitiveTopology topology;
bool primitiveRestart = false;
VkViewport viewport;
@ -140,7 +134,6 @@ ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state);
ColorTarget DecodeColorBuffer(const Registers& context, std::uint32_t slot);
std::size_t CmaskBytes(std::uint32_t width, std::uint32_t height);
std::uint32_t ColorWriteMask(const Registers& context);
GuestTextureResource SurfaceForTarget(const ColorTarget& color);
struct ColorMetadataPass {
enum class Mode { EliminateFastClear, DccDecompress, Resolve };

View File

@ -66,9 +66,6 @@ public:
Texture(const Context& context, const std::shared_ptr<StorageTexture>& source, const GuestTextureResource& descriptor, VkComponentMapping components, DepthCopyTag);
static VkFormat DepthCopyFormat(VkFormat colorFormat);
static bool CanDepthCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor);
struct BlockCopyTag {};
Texture(const Context& context, const std::shared_ptr<StorageTexture>& source, const GuestTextureResource& descriptor, VkComponentMapping components, BlockCopyTag);
static bool CanBlockCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor);
bool IsDepthCopy() const { return perUseCopy == PerUseCopy::RefreshedOnReuse; }
bool RefreshDepthCopy();
struct DepthCopyCounts {
@ -84,8 +81,6 @@ public:
VkImageView View() const;
VkImageView FirstLayerView() const { return firstLayerView; }
VkImageView BaseLevelView() const;
bool HasSrgbView() const { return srgbInfo.has_value(); }
VkImageView SrgbView(bool firstLayer, bool baseLevel) const;
struct ViewRange {
VkImageViewType type;
std::uint32_t levels;
@ -111,10 +106,8 @@ public:
private:
void release() noexcept;
bool refreshBlockCopy();
void createFirstLayerView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo);
void prepareBaseLevelView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo);
void prepareSrgbView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo);
// Held by value: cached textures outlive the Context of the draw that created them.
Context context;
@ -129,10 +122,6 @@ private:
std::optional<VkImageViewCreateInfo> baseLevelInfo;
mutable std::mutex baseLevelMutex;
mutable VkImageView baseLevelView = VK_NULL_HANDLE;
std::optional<VkImageViewCreateInfo> srgbInfo;
float srgbMinLod = 0.0f;
mutable std::mutex srgbMutex;
mutable std::array<VkImageView, 3> srgbViews{};
VkImageLayout layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
VkDeviceSize allocationBytes = 0;
VkDeviceSize countedBytes = 0;
@ -144,12 +133,10 @@ private:
bool copiedOnce = false;
std::uint64_t copiedVersion = 0;
VkFormat depthFormat = VK_FORMAT_UNDEFINED;
VkFormat blockFormat = VK_FORMAT_UNDEFINED;
std::uint32_t copyWidth = 0, copyHeight = 0, copyLevels = 0, copyLayers = 0, copyElementBytes = 0;
};
VkFormat AttachmentProxyFormat(const Context& context, VkFormat format);
VkFormat SrgbTwinFormat(const Context& context, VkFormat format);
// A guest texture a shader writes through a storage image. It is uploaded like a sampled texture;
// after the GPU work completes its results are stored to guest memory (retiled, changed bytes only),
@ -175,7 +162,7 @@ public:
// Render targets live in the same images: draws attach mip 0 through a view of the color
// buffer's format and mark the image dirty like a storage write.
bool Attachable() const { return attachable; }
VkImageView AttachmentView(VkFormat format, std::uint32_t mip = 0, std::uint32_t depthSlice = 0, std::uint32_t layers = 1, std::uint32_t layer = 0);
VkImageView AttachmentView(VkFormat format, std::uint32_t mip = 0, std::uint32_t depthSlice = 0, std::uint32_t layer = 0);
VkImageView AttachmentProxyView();
void RecordAttachmentProxyLoad(VkCommandBuffer commands, VkImageLayout attachmentLayout) const;
void RecordAttachmentProxyStore(VkCommandBuffer commands, VkImageLayout attachmentLayout) const;
@ -329,7 +316,7 @@ public:
// GuestMemory::GpuMutex only, as Refresh is; never from a build's stage A.
DccKeys UploadedKeys() const { return uploadedKeys; }
DccKeys FilledKeys() const { return filledKeys; }
bool GuestSnapshotValid() const { return originalHolds(descriptor.baseAddress, static_cast<std::size_t>(guestBytes)); }
bool GuestSnapshotValid() const { return originalValid; }
DccKeyProof& KeyProof() const { return keyProof; }
DccRangeProof& TargetKeyProof() const { return targetKeyProof; }
DccKeys ProvedKeys() const;
@ -341,7 +328,6 @@ public:
static std::uint64_t SinglePassMoves();
static std::uint64_t RefreshesProved();
std::uint64_t GuestBytes() const;
std::size_t OriginalSnapshotBytes() const { return original.capacity(); }
VkDeviceSize AllocationBytes() const { return memoryBytes; }
private:
@ -352,7 +338,6 @@ private:
// upload everything).
void upload(const std::vector<bool>* layers = nullptr);
void captureGuestBytes(const std::vector<bool>* layers);
bool originalHolds(std::uint64_t address, std::size_t bytes) const;
bool compareUntracked(std::uint64_t address, std::size_t bytes, std::span<std::uint8_t> changed, bool memoize = false) const;
// Stores the pending tracked layers overlapping [address, address + bytes) to guest memory; in
// each, 64 KiB blocks the CPU wrote since the layer's generation keep the CPU's bytes. Block
@ -550,7 +535,7 @@ private:
std::map<std::uint32_t, VkImageView> elementLayerViews;
std::int8_t singlePassState = -1;
bool attachable = false;
std::map<std::tuple<VkFormat, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t>, VkImageView> attachmentViews;
std::map<std::tuple<VkFormat, std::uint32_t, std::uint32_t, std::uint32_t>, VkImageView> attachmentViews;
VkImage proxyImage = VK_NULL_HANDLE;
VkDeviceMemory proxyMemory = VK_NULL_HANDLE;
VkImageView proxyView = VK_NULL_HANDLE;
@ -559,7 +544,7 @@ private:
bool dirty = false;
std::uint64_t version = 0;
// `original` holds the guest bytes; false after a GPU-side clear, which never read them.
std::vector<bool> layerOriginal;
bool originalValid = true;
// The storage cache let the image go (Flush): a lookup makes a new image of the surface, so
// no fill may put results into this one (guarded by the live-image registry lock).
bool released = false;

View File

@ -180,13 +180,6 @@ inline std::size_t VertexBufferReadSize(const ShaderRecompiler::VertexAttribute&
return static_cast<std::size_t>(required);
}
inline std::size_t VertexBufferReadOffset(const ShaderRecompiler::VertexAttribute& attribute, std::uint32_t firstVertex, std::uint32_t firstInstance) {
Require(attribute.fetchIndex <= 1, "unsupported vertex fetch index");
const auto stride = (attribute.resource.fields[1] >> 16u) & 0x3fffu;
const auto index = attribute.fetchIndex == 0 ? firstVertex : firstInstance;
return static_cast<std::size_t>(static_cast<std::uint64_t>(stride) * index);
}
struct VertexFetch {
std::uint64_t begin;
std::uint64_t end;

View File

@ -29,10 +29,10 @@ void main() {
uint nibble = ((y / 512u) * push.blocksPerRow + x / 1024u) * 8192u + offset;
uint byteAddress = push.base + nibble / 2u;
uint code = (cmask.words[byteAddress / 4u] >> ((byteAddress % 4u) * 8u + (nibble % 2u) * 4u)) & 0xfu;
if (code != 0u) return;
if (push.write != 0u) {
if (code == 0xfu) return;
if (code == 0u && push.write != 0u) {
imageStore(target, ivec2(pixel), uvec4(push.clear0, push.clear1, 0u, 0u));
return;
}
if ((pixel.x & 7u) == 0u && (pixel.y & 7u) == 0u) atomicOr(errors.codes, 1u);
if ((pixel.x & 7u) == 0u && (pixel.y & 7u) == 0u) atomicOr(errors.codes, 1u << code);
}

View File

@ -356,7 +356,6 @@ bool LayoutFor(VkFormat format, Layout& layout) {
case VK_FORMAT_R16G16_UINT: layout = {2, {16, 16}, Kind::Uint}; return true;
case VK_FORMAT_R16G16_SINT: layout = {2, {16, 16}, Kind::Sint}; return true;
case VK_FORMAT_R16G16_SFLOAT: layout = {2, {16, 16}, Kind::Float}; return true;
case VK_FORMAT_B10G11R11_UFLOAT_PACK32: layout = {3, {11, 11, 10}, Kind::Float}; return true;
case VK_FORMAT_A2B10G10R10_UNORM_PACK32: case VK_FORMAT_A2R10G10B10_UNORM_PACK32: layout = {4, {10, 10, 10, 2}, Kind::Unorm}; return true;
case VK_FORMAT_A2B10G10R10_UINT_PACK32: layout = {4, {10, 10, 10, 2}, Kind::Uint}; return true;
case VK_FORMAT_R8G8B8A8_UNORM: case VK_FORMAT_R8G8B8A8_SRGB: case VK_FORMAT_B8G8R8A8_UNORM: case VK_FORMAT_B8G8R8A8_SRGB: layout = {4, {8, 8, 8, 8}, Kind::Unorm}; return true;
@ -383,14 +382,7 @@ std::uint64_t One(Kind kind, std::uint32_t bits) {
switch (kind) {
case Kind::Unorm: case Kind::Uint: return bits >= 64 ? ~0ull : (1ull << bits) - 1u;
case Kind::Snorm: case Kind::Sint: return (1ull << (bits - 1u)) - 1u;
case Kind::Float:
switch (bits) {
case 10: return 0x1e0u;
case 11: return 0x3c0u;
case 16: return 0x3c00u;
case 32: return 0x3f800000u;
default: throw std::runtime_error("AGC graphics: DCC clear of a " + std::to_string(bits) + "-bit float channel is not implemented");
}
case Kind::Float: return bits == 16 ? 0x3c00u : 0x3f800000u;
}
return 0;
}
@ -468,9 +460,7 @@ DccKeys readDccKeys(std::uint64_t metaAddress, std::size_t count, bool& memoized
DccKeys textureClearKeys(const GuestTextureResource& resource, std::uint64_t guestBytes, bool& memoized) {
memoized = false;
if (resource.dccAddress == 0) return DccKeys::Uncompressed;
DccKeys keys;
if (const auto known = WaitForKeyWriters(resource, guestBytes)) keys = *known;
else keys = readDccKeys(resource.dccAddress, DccKeyBytes(guestBytes), memoized);
const auto keys = readDccKeys(resource.dccAddress, DccKeyBytes(guestBytes), memoized);
if (keys == DccKeys::Uncompressed || (keys == DccKeys::ClearSingle && SingleKeysReadable(resource))) return keys;
if (IsConvertedTextureFormat(resource.format) && (keys == DccKeys::Clear0001 || keys == DccKeys::Clear1110)) throw std::runtime_error(std::string("AGC graphics: DCC clear code ") + DccKeysName(keys) + " of converted texture format " + std::to_string(resource.format) + " is not implemented");
std::byte probe[16]{};

View File

@ -153,17 +153,18 @@ public:
float clearDepth = 0.0f;
std::uint8_t clearStencil = 0;
bool StorageViewMatches(std::span<const std::uint32_t> words, const GuestTextureResource& resource) const {
if (resource.width != target.extent.width || resource.height != target.extent.height) return false;
std::shared_ptr<StorageTexture> Storage(std::span<const std::uint32_t> words, const GuestTextureResource& resource, std::uint32_t mip) {
if (resource.width != target.extent.width || resource.height != target.extent.height) return nullptr;
const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress;
const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT;
const auto expected = stencil ? VK_FORMAT_R8_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT;
const bool depthBits = !stencil && words.size() >= 4 && ShaderRecompiler::DepthBitsTextureWidth(words[1], words[3]) == (d16 ? 16u : 32u);
return ResolveTextureFormat(resource.format) == expected || depthBits;
}
void RequireStorable(const GuestTextureResource& resource, std::uint32_t mip) const {
const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress;
if (ResolveTextureFormat(resource.format) != expected && !depthBits) return nullptr;
if (resource.dimension != TextureDimension::k2D) {
char text[256];
std::snprintf(text, sizeof(text), "AGC graphics: storage image access to depth/stencil surface 0x%llx is not implemented (storage=%ux%u guest format=%u mip=%u dimension=%d tile=%d)", static_cast<unsigned long long>(resource.baseAddress), resource.width, resource.height, resource.format, mip, static_cast<int>(resource.dimension), static_cast<int>(resource.tileMode));
throw std::runtime_error(text);
}
Require(target.samples == 1, "storage image access to a multisampled depth surface is not implemented");
if (!stencil && hostFormat != target.format) {
char text[224];
@ -175,21 +176,9 @@ public:
std::snprintf(text, sizeof(text), "AGC graphics: storage image access to level %u of the %s plane of depth surface 0x%llx is not implemented", mip, stencil ? "stencil" : "depth", static_cast<unsigned long long>(target.address));
throw std::runtime_error(text);
}
}
std::shared_ptr<StorageTexture> Storage(std::span<const std::uint32_t> words, const GuestTextureResource& resource, std::uint32_t mip) {
if (!StorageViewMatches(words, resource)) return nullptr;
const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress;
const bool oneLayer = resource.dimension == TextureDimension::k2DArray && resource.baseArray == 0 && resource.depthOrLastArray == 0;
if (resource.dimension != TextureDimension::k2D && !oneLayer) {
char text[256];
std::snprintf(text, sizeof(text), "AGC graphics: storage image access to depth/stencil surface 0x%llx is not implemented (storage=%ux%u guest format=%u mip=%u dimension=%d tile=%d)", static_cast<unsigned long long>(resource.baseAddress), resource.width, resource.height, resource.format, mip, static_cast<int>(resource.dimension), static_cast<int>(resource.tileMode));
throw std::runtime_error(text);
}
RequireStorable(resource, mip);
ApplyFastClear();
auto& plane = stencil ? stencilPlane : depthPlane;
if (plane.storage == nullptr || plane.storage->Descriptor().format != resource.format || plane.storage->Descriptor().tileMode != resource.tileMode || plane.storage->Descriptor().baseAddress != resource.baseAddress || plane.storage->Descriptor().dimension != resource.dimension) {
if (plane.storage == nullptr || plane.storage->Descriptor().format != resource.format || plane.storage->Descriptor().tileMode != resource.tileMode || plane.storage->Descriptor().baseAddress != resource.baseAddress) {
std::erase_if(plane.replaced, [](const auto& old) { return old.expired(); });
if (plane.storage != nullptr) plane.replaced.push_back(plane.storage);
plane.storage = StorageTexture::Create(context, *context.detiler, resource, 0);
@ -449,82 +438,6 @@ std::map<std::tuple<VkDevice, std::uint64_t, std::uint32_t, std::uint32_t, VkFor
return *copies;
}
struct LayeredSlice {
DepthSurface* surface;
std::uint32_t layer;
};
class LayeredDepthStorage {
public:
LayeredDepthStorage(const Context& context, const GuestTextureResource& resource, const DepthTarget& base, std::uint32_t bytesPerTexel, std::uint32_t layers) : context(context), base(base), extent(base.extent), bytesPerTexel(bytesPerTexel), layers(layers) {
const auto bytes = DescribeSurface(resource).guestBytes;
StorageTexture::FlushPending(resource.baseAddress, static_cast<std::size_t>(bytes), nullptr, "storage image creation", PublishScope::None);
storage = StorageTexture::Create(context, *context.detiler, resource, 0);
Require(storage->ImageLayers() == layers, "a layered depth storage view's image does not hold its layers");
staging = std::make_unique<DeviceBuffer>(context, static_cast<std::size_t>(sliceBytes()) * layers, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT);
}
void Copy(std::span<const LayeredSlice> slices, bool toStorage) {
if (slices.empty()) return;
auto* recorder = Recorder::Active();
Require(recorder != nullptr, "depth surface storage copies need an active recorder");
const auto commands = recorder->Commands();
recorder->Keep(storage);
const auto timing = recorder->BeginGpuTiming(Recorder::CommandClass::Copy);
std::vector<VkBufferImageCopy> storageRegions;
const auto region = [&](std::uint32_t layer, VkImageAspectFlags aspect, std::uint32_t imageLayer) {
VkBufferImageCopy copy{};
copy.bufferOffset = sliceBytes() * layer;
copy.imageSubresource = {aspect, 0, imageLayer, 1};
copy.imageExtent = {extent.width, extent.height, 1};
return copy;
};
for (const auto& slice : slices) storageRegions.push_back(region(slice.layer, VK_IMAGE_ASPECT_COLOR_BIT, slice.layer));
const auto copyToBuffer = context.Function<PFN_vkCmdCopyImageToBuffer>("vkCmdCopyImageToBuffer");
const auto copyToImage = context.Function<PFN_vkCmdCopyBufferToImage>("vkCmdCopyBufferToImage");
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT);
if (toStorage) {
for (const auto& slice : slices) {
const auto copy = region(slice.layer, VK_IMAGE_ASPECT_DEPTH_BIT, 0);
copyToBuffer(commands, slice.surface->image, VK_IMAGE_LAYOUT_GENERAL, staging->Handle(), 1, &copy);
}
} else {
copyToBuffer(commands, storage->Image(), VK_IMAGE_LAYOUT_GENERAL, staging->Handle(), static_cast<std::uint32_t>(storageRegions.size()), storageRegions.data());
}
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT);
if (toStorage) {
copyToImage(commands, staging->Handle(), storage->Image(), VK_IMAGE_LAYOUT_GENERAL, static_cast<std::uint32_t>(storageRegions.size()), storageRegions.data());
} else {
for (const auto& slice : slices) {
const auto copy = region(slice.layer, VK_IMAGE_ASPECT_DEPTH_BIT, 0);
copyToImage(commands, staging->Handle(), slice.surface->image, VK_IMAGE_LAYOUT_GENERAL, 1, &copy);
}
}
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT);
Recorder::CountBarriers(Recorder::CommandClass::Copy, 3);
recorder->EndGpuTiming(timing, sliceBytes() * slices.size());
}
const Context context;
const DepthTarget base;
const VkExtent2D extent;
const std::uint32_t bytesPerTexel;
const std::uint32_t layers;
std::shared_ptr<StorageTexture> storage;
private:
std::unique_ptr<DeviceBuffer> staging;
VkDeviceSize sliceBytes() const {
return static_cast<VkDeviceSize>(extent.width) * extent.height * bytesPerTexel;
}
};
std::map<std::tuple<VkDevice, std::uint64_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, int, int>, std::unique_ptr<LayeredDepthStorage>>& layeredStorages() {
static auto* storages = new std::map<std::tuple<VkDevice, std::uint64_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, int, int>, std::unique_ptr<LayeredDepthStorage>>();
return *storages;
}
bool sameSurface(const DepthTarget& a, const DepthTarget& b) {
return a.address == b.address && a.stencilAddress == b.stencilAddress && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.format == b.format && a.samples == b.samples;
}
@ -661,7 +574,6 @@ void ClearDepthSurfaces(VkDevice device) {
drawnDepth = nullptr;
std::erase_if(surfaces(), [&](const auto& surface) { return surface->context.device == device; });
std::erase_if(planeCopies(), [&](const auto& entry) { return std::get<0>(entry.first) == device; });
std::erase_if(layeredStorages(), [&](const auto& entry) { return std::get<0>(entry.first) == device; });
}
std::shared_ptr<Texture> DepthSurfaceTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, VkComponentMapping components) {
@ -769,71 +681,17 @@ bool DepthSurfaceAt(const Context& context, std::span<const std::uint32_t> words
return ResolveTextureFormat(resource.format) == expected || depthBits;
}
namespace {
std::vector<LayeredSlice> layeredSlices(const Context& context, const LayeredDepthStorage& storage) {
const auto& base = storage.base;
std::vector<LayeredSlice> slices;
const auto& list = surfaces();
for (std::uint32_t layer = 0; layer < storage.layers; ++layer) {
const auto address = base.address + static_cast<std::uint64_t>(layer) * DepthSliceBytes(base.extent, storage.bytesPerTexel);
const auto slice = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { return surface->context.device == context.device && surface->target.address == address && surface->target.extent.width == base.extent.width && surface->target.extent.height == base.extent.height && surface->target.format == base.format && surface->target.samples == 1; });
if (slice != list.rend()) slices.push_back({slice->get(), layer});
}
return slices;
}
std::shared_ptr<StorageTexture> layeredDepthStorage(const Context& context, DepthSurface& surface, const GuestTextureResource& resource, std::uint32_t mip) {
const auto& target = surface.target;
const auto layers = resource.depthOrLastArray + 1u;
const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT;
const auto bytesPerTexel = d16 ? 2u : 4u;
if (resource.baseArray != 0) {
char text[256];
std::snprintf(text, sizeof(text), "AGC graphics: storage image access to the depth planes of depth surface 0x%llx through a 2D array view of slices %u-%u is not implemented", static_cast<unsigned long long>(target.address), resource.baseArray, resource.depthOrLastArray);
throw std::runtime_error(text);
}
surface.RequireStorable(resource, mip);
const auto geometry = DescribeSurface(resource);
if (layers > 1 && geometry.GuestLayerOffset(1) != DepthSliceBytes(target.extent, bytesPerTexel)) {
char text[256];
std::snprintf(text, sizeof(text), "AGC graphics: storage image access to the depth planes of depth surface 0x%llx with a layer pitch of 0x%llx bytes, not the depth slice size 0x%llx, is not implemented", static_cast<unsigned long long>(target.address), static_cast<unsigned long long>(geometry.GuestLayerOffset(1)), static_cast<unsigned long long>(DepthSliceBytes(target.extent, bytesPerTexel)));
throw std::runtime_error(text);
}
auto& entry = layeredStorages()[{context.device, resource.baseAddress, resource.width, resource.height, layers, resource.format, static_cast<int>(resource.tileMode), static_cast<int>(resource.dimension)}];
if (entry == nullptr) entry = std::make_unique<LayeredDepthStorage>(context, resource, target, bytesPerTexel, layers);
else static_cast<void>(entry->storage->Refresh());
const auto slices = layeredSlices(context, *entry);
for (const auto& slice : slices) slice.surface->ApplyFastClear();
entry->Copy(slices, true);
return entry->storage;
}
}
std::shared_ptr<StorageTexture> DepthSurfaceStorage(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, std::uint32_t mip) {
std::lock_guard lock(surfacesMutex());
const auto& list = surfaces();
const auto found = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) {
return surface->context.device == context.device && (surface->target.address == resource.baseAddress || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == resource.baseAddress));
});
if (found == list.rend()) return nullptr;
auto& surface = **found;
const bool stencil = surface.target.stencilAddress != 0 && surface.target.stencilAddress == resource.baseAddress;
if (stencil || resource.dimension != TextureDimension::k2DArray || (resource.baseArray == 0 && resource.depthOrLastArray == 0)) return surface.Storage(words, resource, mip);
if (!surface.StorageViewMatches(words, resource)) return nullptr;
return layeredDepthStorage(context, surface, resource, mip);
return found == list.rend() ? nullptr : (*found)->Storage(words, resource, mip);
}
bool DepthSurfaceStoreBack(const Context& context, const StorageTexture& storage) {
std::lock_guard lock(surfacesMutex());
for (auto& [key, entry] : layeredStorages()) {
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;
return true;
}
for (const auto& surface : surfaces()) {
if (surface->context.device == context.device && surface->StoreBack(storage)) return true;
}

View File

@ -61,6 +61,38 @@ std::uint64_t DrawRenderPassKey(const Context& context, const State& state, std:
namespace {
std::uint32_t GuestFormatFor(VkFormat format, std::uint32_t elementBytes) {
if (const auto guest = FindGuestColorTargetFormat(format, elementBytes)) return *guest;
throw std::runtime_error("AGC graphics: no guest texture format matches the color buffer format " + std::to_string(static_cast<int>(format)));
}
// The color buffer as a single-mip 2D surface descriptor (tile mode SW_64KB_R_X).
GuestTextureResource SurfaceForTarget(const ColorTarget& color) {
Require(color.tileMode != ColorTileMode::Linear, "linear color targets are not resident");
const bool chain = color.mipCount > 1;
GuestTextureResource surface{};
surface.baseAddress = chain ? color.surfaceAddress : color.address;
surface.pipeBankXor = color.pipeBankXor;
surface.width = chain ? color.surfaceExtent.width : color.extent.width;
surface.height = chain ? color.surfaceExtent.height : color.extent.height;
surface.depthOrLastArray = color.depth - 1u;
surface.baseArray = 0;
surface.mipCount = color.mipCount;
surface.baseLevel = 0;
surface.lastLevel = color.mipCount - 1;
surface.tileMode = ColorTextureTileMode(color.tileMode);
surface.dimension = color.depth > 1 ? TextureDimension::k3D : TextureDimension::k2D;
surface.format = GuestFormatFor(color.format, color.elementBytes);
surface.dstSelX = 4;
surface.dstSelY = 5;
surface.dstSelZ = 6;
surface.dstSelW = 7;
surface.dccAddress = color.dccAddress;
surface.dccAlphaOnMsb = color.dccAlphaOnMsb;
surface.dccPipeAligned = color.dccPipeAligned;
return surface;
}
GuestTextureResource ArraySurfaceForTarget(const ColorTarget& color) {
Require(color.arrayLastSlice != 0, "the color target is not a view of a 2D array");
auto surface = SurfaceForTarget(color);
@ -189,8 +221,20 @@ void proveCmask(std::uint64_t address, std::size_t bytes, std::uint64_t generati
else cmaskProofs()[address] = {bytes, generation};
}
std::string hexAddress(std::uint64_t address) {
char text[24];
std::snprintf(text, sizeof(text), "0x%llx", static_cast<unsigned long long>(address));
return text;
}
void requireCmaskCodes(std::uint32_t codes) {
if (codes == 0) return;
Require((codes & 1u) == 0, "a CMASK fast clear of a DCC color target, whose clear belongs in its DCC keys, was recorded");
std::string listed;
for (std::uint32_t code = 1; code < 15; ++code) {
if ((codes >> code) & 1u) listed += (listed.empty() ? "" : ", ") + std::to_string(code);
}
Require(false, "CMASK codes " + listed + " were recorded, which are not a single-sample fast-clear code (0 cleared, 15 expanded)");
}
CmaskState readCmask(const ColorTarget& color, const CmaskLayout& layout, std::vector<std::uint32_t>& cleared) {
@ -220,6 +264,7 @@ CmaskState readCmask(const ColorTarget& color, const CmaskLayout& layout, std::v
const auto nibble = layout.Nibble(tileX, tileY);
const auto code = (std::to_integer<std::uint32_t>(mask[nibble / 2u]) >> ((nibble % 2u) * 4u)) & 0xfu;
if (code == 0u) cleared.push_back(tileY * layout.TilesX() + tileX);
else Require(code == 0xfu, "CMASK code " + std::to_string(code) + " of color target " + hexAddress(color.address) + " is not a single-sample fast-clear code (0 cleared, 15 expanded)");
}
}
if (cleared.empty()) return CmaskState::Expanded;
@ -643,7 +688,6 @@ bool ValidationKey(const Context& context, std::span<const CompiledShader> shade
add(context.fragmentShaderBarycentric);
add(context.shaderClipDistance);
add(context.shaderCullDistance);
add(context.viewportIndexLayer);
add(state.paClVsOutCntl);
add(state.stages.mesh.has_value());
if (state.stages.mesh) {
@ -665,7 +709,6 @@ bool ValidationKey(const Context& context, std::span<const CompiledShader> shade
add(tessellation.domain);
add(tessellation.partitioning);
add(tessellation.outputTopology);
add(tessellation.patchesPerGroup);
}
}
return keyed;
@ -697,7 +740,7 @@ std::set<std::uint32_t> CachedFragmentOutputs(const Context& context, std::span<
}
std::set<std::uint32_t> outputs;
try {
outputs = ValidateShaders(shaders, state, context.subgroup, context.fragmentShaderBarycentric, context.descriptorIndexing, context.imageInt64Atomics, context.geometryShader, context.sampleRateShading, context.bufferInt64Atomics, context.shaderClipDistance, context.shaderCullDistance, context.viewportIndexLayer);
outputs = ValidateShaders(shaders, state, context.subgroup, context.fragmentShaderBarycentric, context.descriptorIndexing, context.imageInt64Atomics, context.geometryShader, context.sampleRateShading, context.bufferInt64Atomics, context.shaderClipDistance, context.shaderCullDistance);
} catch (const std::exception& error) {
if (keyed) {
std::lock_guard lock(validationMutex());
@ -1037,7 +1080,6 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P
const auto& mesh = *state.stages.mesh;
const auto inputSize = mesh.inputPrimitive == 1 ? 1u : mesh.inputPrimitive == 2 ? 2u : 3u;
Require(draw.indexCount >= inputSize && mesh.primitivesPerGroup != 0, "mesh draw contains no complete primitive");
Require(((state.stages.registerValue >> 19u) & 3u) == 0 || !draw.indexed, "indexed fast-launch draws are unsupported");
if (args == nullptr) {
const auto step = mesh.inputPrimitive == 5 || mesh.inputPrimitive == 6 ? 1u : inputSize;
const auto primitives = (draw.indexCount - inputSize) / step + 1u;
@ -1137,8 +1179,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P
const auto bytes = args != nullptr ? VertexBufferExtent(attribute) : VertexBufferReadSize(attribute, inputs.maxIndex, draw.instanceCount, draw.firstInstance);
const auto& fields = attribute.resource.fields;
const auto address = fields[0] | (static_cast<std::uint64_t>(fields[1] & 0xffffu) << 32u);
const auto firstByte = args == nullptr ? VertexBufferReadOffset(attribute, draw.firstVertex, draw.firstInstance) : 0;
Require(!state.hasColorTarget || address + bytes <= state.color.address || state.color.address + state.color.bytes <= address + firstByte, "vertex buffer aliases the render target");
Require(!state.hasColorTarget || address + bytes <= state.color.address || state.color.address + state.color.bytes <= address, "vertex buffer aliases the render target");
const auto stride = (fields[1] >> 16u) & 0x3fffu;
fetchOf[i] = fetches.size();
fetches.push_back({address, address + bytes, stride, attribute.fetchIndex, DecodeVertexFormat(attribute).alignment});
@ -1549,12 +1590,6 @@ void pushDrawConstants(const Pipeline& pipeline, VkCommandBuffer commands, const
resources.PatchPushConstants(block);
stages = PushConstantStages(shaders);
}
if (state.stages.tessellation) {
for (const auto& shader : shaders) Require(shader.program->pushConstants.empty() || shader.pushConstantOffset >= PipelinePushSlotBytes || shader.pushConstantOffset + shader.program->pushConstants.size() <= ShaderRecompiler::TessellationLevelPushOffsetBytes, "a stage's push constants overlap the tessellation levels");
const std::array<float, ShaderRecompiler::TessellationLevelPushBytes / 4> levels{state.stages.minTessellationLevel, state.stages.maxTessellationLevel};
static_assert(ShaderRecompiler::TessellationLevelPushOffsetBytes + ShaderRecompiler::TessellationLevelPushBytes == PipelinePushSlotBytes);
std::memcpy(block.data() + ShaderRecompiler::TessellationLevelPushOffsetBytes, levels.data(), sizeof(levels));
}
if (!state.stages.mesh) {
pipeline.PushConstants(commands, stages, block);
return;
@ -1933,7 +1968,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
if (binding.resident != nullptr) {
timer.phase(PhaseReadTarget);
binding.proxied = AttachmentProxyFormat(context, color.format) != VK_FORMAT_UNDEFINED;
targetViews.push_back(binding.proxied ? binding.resident->AttachmentProxyView() : binding.resident->AttachmentView(color.format, color.mip, color.depthSlice, color.layers, color.arraySlice));
targetViews.push_back(binding.proxied ? binding.resident->AttachmentProxyView() : binding.resident->AttachmentView(color.format, color.mip, color.depthSlice, color.arraySlice));
continue;
}
materializeCmaskClear(context, color, nullptr);
@ -2121,7 +2156,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
owners.push_back(binding.resident);
persistent.push_back(binding.multisampled ? 1u : 0u);
}
auto framebuffer = pipeline->AcquireFramebuffer(targetViews, owners, state.renderExtent, state.renderLayers, persistent);
auto framebuffer = pipeline->AcquireFramebuffer(targetViews, owners, state.renderExtent, persistent);
timer.phase(PhasePipeline);
APS5_LOG_CHARS_OUT_DEBUG("Pipeline created");
if (lean) {
@ -2145,7 +2180,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
// only a template the resource cache serves under this content key (reusable: no lease,
// no copied writes, every direct region import- or mirror-served) of a direct draw, so a
// hit's proof is the template's ProveCurrent and nothing needs completion work.
if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && resolved.moved.empty() && !multisampled) {
if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && !state.depth && resolved.moved.empty() && !multisampled) {
auto recipe = std::make_shared<DrawRecipe>();
recipe->device = context.device;
recipe->templateRef = resources;
@ -2164,7 +2199,6 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
recipe->masked = masked;
recipe->fragmentOutputs = inputs.fragmentOutputs;
recipe->shaderStages = inputs.shaderStages;
recipe->hasDepth = state.depth.has_value();
*recipeOut = std::move(recipe);
}
timing.Mark("draw_and_resource_release");
@ -2442,9 +2476,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 (!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");
Require(recipe.targets.size() == state.colors.size() && recipe.targetViews.size() == state.colors.size(), "draw recipe targets do not match the state");
auto inputs = prepareDrawInputs(context, state, draw, shaders, outcome, timer, &recipe);
if (inputs.nothing) {
result.recorded = true;
@ -2470,12 +2502,6 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con
if (resident == nullptr) return miss(DrawRecipeMiss::TargetGone);
targets.push_back(std::move(resident));
}
if (recipe.hasDepth) {
timer.phase(PhaseSetup);
const auto depthView = DepthSurfaceView(context, *state.depth);
timer.phase(PhaseReadTarget);
if (recipe.targetViews.back() != depthView) return miss(DrawRecipeMiss::TargetGone);
}
timer.phase(PhasePrepare);
// The template's proof (rules R6/R7): ProveCurrent, T1 included, the alias checks the trimmed
// key leaves to a hit repeated; a failure removes the template from the cache (the batch keeps
@ -2506,7 +2532,7 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con
auto pipeline = recipe.pipeline.lock();
if (pipeline == nullptr) return miss(DrawRecipeMiss::ObjectsGone);
auto framebuffer = recipe.framebuffer.lock();
if (framebuffer == nullptr) framebuffer = pipeline->AcquireFramebuffer(recipe.targetViews, targets, state.renderExtent, state.renderLayers);
if (framebuffer == nullptr) framebuffer = pipeline->AcquireFramebuffer(recipe.targetViews, targets, state.renderExtent);
timer.phase(PhasePipeline);
const auto recordStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
RecordedDraw record;

View File

@ -35,10 +35,7 @@ GpuColorTransfer::GpuColorTransfer(const Context& context) : context(context) {
pipelineInfo.stage.module = module;
pipelineInfo.stage.pName = "main";
pipelineInfo.layout = pipelineLayout;
{
auto pipelineCacheLock = context.LockPipelineCacheForUse();
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateComputePipelines color transfer");
}
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateComputePipelines color transfer");
context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, module, nullptr);
module = VK_NULL_HANDLE;
const VkDescriptorPoolSize size{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 3};

View File

@ -4,7 +4,6 @@
#include "prx/libSceAgcDriver/Graphics/include/BdaResources.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
#include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp"
@ -162,7 +161,6 @@ struct HostImports {
std::map<VkDevice, std::vector<std::shared_ptr<RetiredImport>>> parked;
std::uint64_t liveBytes = 0;
std::map<std::uint64_t, ImportFailure> failed;
std::map<std::uint64_t, ImportFailure> failedJoins;
std::set<std::uint64_t> unaliasedSpans;
// Registry generation the imports were last reconciled with.
std::uint64_t refreshedGeneration = 0;
@ -172,8 +170,6 @@ struct HostImports {
VkDevice watchDevice = VK_NULL_HANDLE;
bool unwatchImports = false;
bool unwatchDmaBufImports = false;
std::uint64_t useCounter = 0;
std::uint64_t buildFloor = 0;
};
HostImports& Imports() {
@ -214,13 +210,12 @@ struct RetiredImport {
const GuestAllocations::Range* containingRange(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end);
bool runCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end);
bool joinedCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end);
void retireImport(VkDevice device, HostImports& state, std::map<std::uint64_t, HostImport>::iterator it, const GuestAllocations::Lease& lease, bool hold = true) {
// Results shadowed for the import reach its (old) buffer first where the memory is still a
// readable registered range (the import retires because its registration vanished or changed
// size); the holder below outlives the batch that copies them.
RetireShadow(device, it->second, [&lease, joined = it->second.joined](std::uint64_t begin, std::uint64_t end) { return runCovers(lease, begin, end) || (joined && joinedCovers(lease, begin, end)); });
RetireShadow(device, it->second, [&lease](std::uint64_t begin, std::uint64_t end) { return runCovers(lease, begin, end); });
++state.epoch;
state.liveBytes -= it->second.bytes;
auto holder = std::make_shared<RetiredImport>(state.device, state.destroyBuffer, state.freeMemory, state.imports.extract(it));
@ -584,13 +579,6 @@ std::shared_ptr<const GuestAllocations::Range> leasedRangeOwner(const GuestAlloc
return found != lease.end() && (*found)->address == base ? *found : nullptr;
}
std::shared_ptr<const GuestAllocations::Range> leasedRangeOwner(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) {
const auto found = std::upper_bound(lease.begin(), lease.end(), begin, [](std::uint64_t value, const auto& range) { return value < range->address; });
if (found == lease.begin()) return nullptr;
const auto& range = *std::prev(found);
return begin >= range->address && end <= range->address + range->bytes ? range : nullptr;
}
struct ImportRun {
std::uint64_t base = 0;
std::uint64_t bytes = 0;
@ -642,34 +630,6 @@ bool runCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::u
return !run.ranges.empty() && end <= run.base + run.bytes;
}
ImportRun crossedRun(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) {
auto last = leasedIndexContaining(lease, begin);
if (last == lease.size()) return {};
const auto first = last;
while (lease[last]->address + lease[last]->bytes < end) {
if (last + 1 == lease.size() || !continuesRun(*lease[last], *lease[last + 1])) return {};
++last;
}
if (first == last) return {};
ImportRun run{lease[first]->address, lease[last]->address + lease[last]->bytes - lease[first]->address, {}};
run.ranges.assign(lease.begin() + static_cast<std::ptrdiff_t>(first), lease.begin() + static_cast<std::ptrdiff_t>(last) + 1);
return run;
}
ImportRun joinedRun(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) {
const auto window = importWindow.load(std::memory_order_relaxed);
if (window == 0) return {};
auto run = crossedRun(lease, begin, end);
if (run.ranges.empty()) return run;
if (std::all_of(run.ranges.begin(), run.ranges.end(), [window](const auto& range) { return range->bytes > window; })) return run;
if (begin == run.base && end == run.base + run.bytes) return run;
return {};
}
bool joinedCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) {
return !crossedRun(lease, begin, end).ranges.empty();
}
template<typename Entry>
bool sameRun(const Entry& entry, const ImportRun& run) {
if (run.base != entry.base || run.bytes != entry.bytes || run.ranges.size() != entry.ranges.size()) return false;
@ -692,173 +652,45 @@ bool wouldRetireImport(const HostImports& state, const ImportRun& run) {
}
bool sameRange(const HostImport& entry, const GuestAllocations::Lease& lease) {
if (entry.ranges.size() == 1 && !entry.joined && entry.bytes < entry.ranges.front()->bytes) {
const auto current = leasedRangeOwner(lease, entry.base, entry.base + entry.bytes);
return current != nullptr && !entry.ranges.front().owner_before(current) && !current.owner_before(entry.ranges.front());
}
return sameRun(entry, entry.joined ? joinedRun(lease, entry.base, entry.base + entry.bytes) : importRun(lease, entry.base));
}
bool pointerHeld(const HostImports& state, const HostImport& entry) {
return entry.lastUse >= state.buildFloor;
}
bool evictable(const HostImports& state, const HostImport& entry) {
constexpr std::uint64_t protectedUses = 256;
return !pointerHeld(state, entry) && entry.lastUse + protectedUses < state.useCounter;
}
std::uint64_t retireLeastRecent(const Context& context, HostImports& state, const GuestAllocations::Lease& lease, std::uint64_t wanted) {
std::uint64_t freed = 0;
while (freed < wanted) {
auto oldest = state.imports.end();
for (auto it = state.imports.begin(); it != state.imports.end(); ++it) {
if (evictable(state, it->second) && (oldest == state.imports.end() || it->second.lastUse < oldest->second.lastUse)) oldest = it;
}
if (oldest == state.imports.end()) break;
freed += oldest->second.bytes;
retireImport(context.device, state, oldest, lease);
}
return freed;
}
std::uint64_t physicalMemoryBytes() {
#ifdef _WIN32
MEMORYSTATUSEX status{};
status.dwLength = sizeof(status);
return GlobalMemoryStatusEx(&status) ? status.ullTotalPhys : 0;
#else
const auto pages = sysconf(_SC_PHYS_PAGES);
const auto pageSize = sysconf(_SC_PAGESIZE);
return pages > 0 && pageSize > 0 ? static_cast<std::uint64_t>(pages) * static_cast<std::uint64_t>(pageSize) : 0;
#endif
}
std::atomic<std::uint64_t> budgetOverride{0};
// Pinned imports count against the driver's system memory heap, which imports and type-3 mirrors share; past it
// ordinary host allocations fail. The budget is APS5_HOST_IMPORT_MIB, else the larger of 6 GiB and half the physical
// memory, at most the largest host heap less 2 GiB.
std::uint64_t importBudget(const Context& context) {
if (const auto set = budgetOverride.load(std::memory_order_relaxed); set != 0) return set;
static const std::uint64_t configured = []() -> std::uint64_t {
const char* value = std::getenv("APS5_HOST_IMPORT_MIB");
return value != nullptr ? std::strtoull(value, nullptr, 10) << 20u : 0u;
}();
if (configured != 0) return configured;
std::uint64_t hostHeap = 0;
for (std::uint32_t index = 0; index < context.memory.memoryHeapCount; ++index) {
if ((context.memory.memoryHeaps[index].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) == 0) hostHeap = std::max<std::uint64_t>(hostHeap, context.memory.memoryHeaps[index].size);
}
const auto fromMemory = std::max<std::uint64_t>(6144ull << 20u, physicalMemoryBytes() / 2);
constexpr std::uint64_t reserve = 2048ull << 20u;
return hostHeap > reserve ? std::min(fromMemory, hostHeap - reserve) : fromMemory;
}
std::atomic<std::uint64_t> rangeImportWindow{64ull << 20u};
// The import of an access to a registered range larger than the range window: the cells the access touches, grown to
// the union of the range's imports it overlaps, so accesses across neighbouring cell edges share one import instead of
// replacing each other's.
ImportRun windowRun(const HostImports& state, ImportRun run, std::uint64_t begin, std::uint64_t end, std::uint64_t alignment) {
const auto window = rangeImportWindow.load(std::memory_order_relaxed);
if (run.ranges.size() != 1 || window == 0 || alignment == 0 || window % alignment != 0) return run;
const auto& range = *run.ranges.front();
if (range.bytes <= window || begin < range.address || end > range.address + range.bytes || end <= begin) return run;
const auto lastCell = range.bytes / window - 1;
const auto cell = [&](std::uint64_t address) { return std::min((address - range.address) / window, lastCell); };
auto windowBegin = range.address + cell(begin) * window;
const auto endCell = cell(end - 1);
auto windowEnd = endCell == lastCell ? range.address + range.bytes : range.address + (endCell + 1) * window;
for (bool grown = true; grown;) {
grown = false;
auto it = state.imports.upper_bound(windowBegin);
if (it != state.imports.begin()) --it;
for (; it != state.imports.end() && it->first < windowEnd; ++it) {
const auto importEnd = it->second.base + it->second.bytes;
if (importEnd <= windowBegin || it->second.base < range.address || importEnd > range.address + range.bytes) continue;
if (it->second.base < windowBegin || importEnd > windowEnd) grown = true;
windowBegin = std::min(windowBegin, it->second.base);
windowEnd = std::max(windowEnd, importEnd);
}
}
if (windowBegin % alignment != 0 || (windowEnd - windowBegin) % alignment != 0) return run;
run.base = windowBegin;
run.bytes = windowEnd - windowBegin;
return run;
return sameRun(entry, importRun(lease, entry.base));
}
const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint64_t end);
bool importsStale(const Context& context, const HostImports& state);
const HostImport* importRunAt(const Context& context, HostImports& state, ImportRun run, bool joined, std::uint64_t address, const GuestAllocations::Lease& lease, std::uint64_t end, bool evict);
// The bytes retireLeastRecent could free outside [begin, end).
std::uint64_t evictableBytes(const HostImports& state, std::uint64_t begin, std::uint64_t end) {
std::uint64_t bytes = 0;
for (const auto& [base, entry] : state.imports) {
if ((entry.base + entry.bytes <= begin || entry.base >= end) && evictable(state, entry)) bytes += entry.bytes;
}
return bytes;
}
const HostImport* importAllocation(const Context& context, HostImports& state, std::uint64_t address, const GuestAllocations::Lease& lease, std::uint64_t end = 0, bool evict = true) {
const bool fresh = !importsStale(context, state);
if (fresh && end != 0) {
if (const auto* current = findImport(state, address, end)) return current;
} else if (!fresh) {
if (const auto* current = findImport(state, address, address + 1); current != nullptr && current->joined && sameRange(*current, lease)) return current;
const HostImport* importAllocation(const Context& context, HostImports& state, std::uint64_t address, const GuestAllocations::Lease& lease) {
if (!importsStale(context, state)) {
if (const auto* current = findImport(state, address, address + 1)) return current;
}
auto run = importRun(lease, address);
// Without an end the caller views the whole run through the import (AcquireRegistered): a window that holds the
// first byte is shorter than that, so only an import of the whole run is a hit; a window is replaced below.
if (fresh && end == 0) {
if (const auto* current = findImport(state, address, run.ranges.empty() ? address + 1 : run.base + run.bytes)) return current;
}
if (end != 0) run = windowRun(state, std::move(run), address, end, context.hostImportAlignment);
if (run.ranges.empty()) return nullptr;
return importRunAt(context, state, std::move(run), false, address, lease, end, evict);
}
const HostImport* importJoined(const Context& context, HostImports& state, std::uint64_t begin, std::uint64_t end, const GuestAllocations::Lease& lease) {
if (const auto* low = findImport(state, begin, begin + 1); low != nullptr && low->joined) begin = low->base;
if (const auto* high = findImport(state, end - 1, end); high != nullptr && high->joined) end = high->base + high->bytes;
auto run = joinedRun(lease, begin, end);
if (run.ranges.empty()) return nullptr;
if (const auto failed = state.failedJoins.find(run.base); failed != state.failedJoins.end() && sameRun(failed->second, run)) return nullptr;
return importRunAt(context, state, std::move(run), true, begin, lease, end, true);
}
const HostImport* importRunAt(const Context& context, HostImports& state, ImportRun run, bool joined, std::uint64_t address, const GuestAllocations::Lease& lease, std::uint64_t end, bool evict) {
const auto base = run.base;
const auto bytes = run.bytes;
auto& failures = joined ? state.failedJoins : state.failed;
if (const auto found = state.imports.find(base); found != state.imports.end()) {
if (sameRun(found->second, run)) return &found->second;
retireImport(context.device, state, found, lease);
}
for (auto it = state.imports.lower_bound(base); it != state.imports.end() && it->first < base + bytes;) {
const auto next = std::next(it);
retireImport(context.device, state, it, lease);
it = next;
}
if (auto it = state.imports.lower_bound(base); it != state.imports.begin() && std::prev(it)->second.base + std::prev(it)->second.bytes > base) retireImport(context.device, state, std::prev(it), lease);
const auto alignment = context.hostImportAlignment;
if (joined && (alignment == 0 || base % alignment != 0 || bytes % alignment != 0)) {
failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(run.ranges.begin(), run.ranges.end())});
return nullptr;
}
if (const auto found = state.imports.find(base); found != state.imports.end() && sameRun(found->second, run)) return &found->second;
if (alignment == 0 || base % alignment != 0 || bytes % alignment != 0) return nullptr;
if (const auto failed = failures.find(base); failed != failures.end()) {
if (const auto failed = state.failed.find(base); failed != state.failed.end()) {
if (sameRun(failed->second, run)) return nullptr;
failures.erase(failed);
state.failed.erase(failed);
}
// The imports this one replaces overlap it; none may be held by a build, and the import must fit the budget (with
// what may be evicted) before any of them is retired.
auto first = state.imports.lower_bound(base);
if (first != state.imports.begin() && std::prev(first)->second.base + std::prev(first)->second.bytes > base) --first;
std::uint64_t replaced = 0;
// A join may replace imports this build already looked up: it runs before any region confirms an import
// (UploadFinish's pre-pass re-resolves every pointer after it), and a retired import stays alive through the
// hold (KeepThroughHold). Refusing it there would keep the run copied for good.
const auto held = [&](const HostImport& entry) { return !joined && pointerHeld(state, entry); };
for (auto it = first; it != state.imports.end() && it->first < base + bytes; ++it) {
if (held(it->second)) return nullptr;
replaced += it->second.bytes;
}
const auto budget = importBudget(context);
const auto live = state.liveBytes - replaced;
if (live + bytes > budget && (!evict || live + bytes - evictableBytes(state, base, base + bytes) > budget)) {
// Pinned imports count against the driver's system memory budget; past it ordinary host
// allocations fail, so imports stop at APS5_HOST_IMPORT_MIB (default 6 GiB, which covers the
// registered memory of address-based shaders; past it they copy gigabytes per dispatch).
static const std::uint64_t budget = [] {
const char* value = std::getenv("APS5_HOST_IMPORT_MIB");
return (value ? std::strtoull(value, nullptr, 10) : 6144ull) << 20u;
}();
const auto live = state.liveBytes;
if (live + bytes > budget) {
// A refused import turns every later use of the range into CPU copies, so say so.
static std::uint64_t refused = 0, refusedBytes = 0;
static auto lastReport = std::chrono::steady_clock::now() - std::chrono::seconds(60);
@ -866,16 +698,10 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import
refusedBytes += bytes;
if (std::chrono::steady_clock::now() - lastReport > std::chrono::seconds(10)) {
lastReport = std::chrono::steady_clock::now();
std::fprintf(stderr, "[gpu] host import budget: %llu MiB live of %llu MiB (APS5_HOST_IMPORT_MIB); %llu imports (%llu MiB) refused so far, last 0x%llx+0x%llx\n", static_cast<unsigned long long>(state.liveBytes >> 20u), static_cast<unsigned long long>(budget >> 20u), static_cast<unsigned long long>(refused), static_cast<unsigned long long>(refusedBytes >> 20u), static_cast<unsigned long long>(base), static_cast<unsigned long long>(bytes));
std::fprintf(stderr, "[gpu] host import budget: %llu MiB live of %llu MiB (APS5_HOST_IMPORT_MIB); %llu imports (%llu MiB) refused so far, last 0x%llx+0x%llx\n", static_cast<unsigned long long>(live >> 20u), static_cast<unsigned long long>(budget >> 20u), static_cast<unsigned long long>(refused), static_cast<unsigned long long>(refusedBytes >> 20u), static_cast<unsigned long long>(base), static_cast<unsigned long long>(bytes));
}
return nullptr;
}
for (auto it = first; it != state.imports.end() && it->first < base + bytes;) {
const auto next = std::next(it);
retireImport(context.device, state, it, lease);
it = next;
}
if (state.liveBytes + bytes > budget) retireLeastRecent(context, state, lease, state.liveBytes + bytes - budget);
HostImport entry{base, bytes, VK_NULL_HANDLE, VK_NULL_HANDLE, 0};
entry.range = run.ranges.front();
entry.ranges = std::move(run.ranges);
@ -887,7 +713,7 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import
for (std::uint64_t cursor = base; cursor < base + bytes;) {
MEMORY_BASIC_INFORMATION info{};
if (VirtualQuery(reinterpret_cast<const void*>(cursor), &info, sizeof(info)) == 0 || info.State != MEM_COMMIT || (info.Protect & (PAGE_NOACCESS | PAGE_GUARD)) != 0) {
failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
state.failed.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
return nullptr;
}
const auto protection = info.Protect & 0xffu;
@ -899,7 +725,7 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import
cursor = reinterpret_cast<std::uint64_t>(info.BaseAddress) + info.RegionSize;
}
if (!writable && readOnly) {
failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
state.failed.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
return nullptr;
}
if (!writable) {
@ -912,17 +738,13 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import
entry.alias = GuestArena::GuestArenaMapAlias_nid_postfix(static_cast<std::uintptr_t>(base), static_cast<std::size_t>(bytes));
} catch (const std::runtime_error&) {
if (entry.ranges.size() == 1) throw;
if (joined) {
failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
return nullptr;
}
state.unaliasedSpans.insert(base);
return importAllocation(context, state, address, lease, end, evict);
return importAllocation(context, state, address, lease);
}
}
#else
if (!GuestMemory::Accessible(reinterpret_cast<const void*>(base), static_cast<std::size_t>(bytes), true)) {
failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
state.failed.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
return nullptr;
}
#endif
@ -953,7 +775,7 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import
#ifdef _WIN32
GuestArena::GuestArenaUnmapAlias_nid_postfix(entry.alias);
#endif
failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
state.failed.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())});
std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx failed at %s (%d); falling back to copies\n", static_cast<unsigned long long>(base), static_cast<unsigned long long>(bytes), step, static_cast<int>(result));
#ifdef _WIN32
static const bool trace = std::getenv("APS5_TRACE_HOST_IMPORT") != nullptr;
@ -983,12 +805,16 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import
importedBytes += bytes;
static const bool trace = std::getenv("APS5_TRACE_HOST_IMPORT") != nullptr;
state.liveBytes += bytes;
if (trace) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx ok (%zu live, %.1f MiB live, %.1f MiB ever)%s\n", static_cast<unsigned long long>(base), static_cast<unsigned long long>(bytes), state.imports.size() + 1, state.liveBytes / 1048576.0, importedBytes / 1048576.0, joined ? ", joined at bind" : "");
entry.joined = joined;
entry.lastUse = ++state.useCounter;
if (trace) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx ok (%zu live, %.1f MiB live, %.1f MiB ever)\n", static_cast<unsigned long long>(base), static_cast<unsigned long long>(bytes), state.imports.size() + 1, state.liveBytes / 1048576.0, importedBytes / 1048576.0);
return &state.imports.emplace(base, entry).first->second;
}
// The lease is in address order (it is built from the registry map), so a range is found by binary search.
const GuestAllocations::Range* leasedRangeAt(const GuestAllocations::Lease& lease, std::uint64_t base) {
const auto found = std::lower_bound(lease.begin(), lease.end(), base, [](const auto& range, std::uint64_t value) { return range->address < value; });
return found != lease.end() && (*found)->address == base ? found->get() : nullptr;
}
// The readable registered range containing [begin, end), or null.
const GuestAllocations::Range* containingRange(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) {
const auto found = std::upper_bound(lease.begin(), lease.end(), begin, [](std::uint64_t value, const auto& range) { return value < range->address; });
@ -1013,7 +839,6 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc
}
state.liveBytes = 0;
state.failed.clear();
state.failedJoins.clear();
state.device = context.device;
state.destroyBuffer = context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer");
state.freeMemory = context.Function<PFN_vkFreeMemory>("vkFreeMemory");
@ -1026,19 +851,19 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc
if (generation == state.refreshedGeneration) return;
state.refreshedGeneration = generation;
for (auto it = state.imports.begin(); it != state.imports.end();) {
if (sameRange(it->second, lease)) {
const auto* range = leasedRangeAt(lease, it->first);
if (range != nullptr && sameRange(it->second, lease)) {
if (it->second.unwatched) GuestMemory::Unwatch(it->first, it->second.bytes);
++it;
continue;
}
static const bool trace = std::getenv("APS5_TRACE_HOST_IMPORT") != nullptr;
if (trace && leasedRangeOwner(lease, it->first, it->first + it->second.bytes) != nullptr) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx retired: the range was mapped again\n", static_cast<unsigned long long>(it->first), static_cast<unsigned long long>(it->second.bytes));
if (trace && range != nullptr && range->bytes == it->second.bytes) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx retired: the range was mapped again\n", static_cast<unsigned long long>(it->first), static_cast<unsigned long long>(it->second.bytes));
const auto next = std::next(it);
retireImport(context.device, state, it, lease);
it = next;
}
for (auto it = state.failed.begin(); it != state.failed.end();) it = sameRun(it->second, importRun(lease, it->first)) ? std::next(it) : state.failed.erase(it);
for (auto it = state.failedJoins.begin(); it != state.failedJoins.end();) it = sameRun(it->second, joinedRun(lease, it->second.base, it->second.base + it->second.bytes)) ? std::next(it) : state.failedJoins.erase(it);
}
const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint64_t end) {
@ -1046,15 +871,7 @@ const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint6
if (found == state.imports.begin()) return nullptr;
--found;
const auto& entry = found->second;
if (begin < entry.base || end > entry.base + entry.bytes) return nullptr;
entry.lastUse = ++state.useCounter;
return &entry;
}
const HostImport* importAccess(const Context& context, HostImports& state, std::uint64_t begin, std::uint64_t end, const GuestAllocations::Lease& lease) {
if (!runCovers(lease, begin, end)) return findImport(state, begin, end);
const auto* entry = importAllocation(context, state, begin, lease, end);
return entry != nullptr && findImport(state, begin, end) == entry ? entry : nullptr;
return begin >= entry.base && end <= entry.base + entry.bytes ? &entry : nullptr;
}
// Whether the imports need reconciling before a lookup: the registry changed since the last walk.
@ -1967,10 +1784,6 @@ MirrorStats MirrorCounters() {
}
void ClearImageMirrors(VkDevice device) {
auto space = Spaces().current.load();
if (space != nullptr && space->device == device) {
Spaces().current.compare_exchange_strong(space, std::shared_ptr<const GuestBufferMemory::AddressSpace>{});
}
auto& state = Mirrors();
std::map<std::uint64_t, std::shared_ptr<ImageMirror>> entries;
{
@ -1981,6 +1794,7 @@ void ClearImageMirrors(VkDevice device) {
state.heapBytes = 0;
state.device = VK_NULL_HANDLE;
}
Spaces().current.store(nullptr);
}
void ClearHostImports(VkDevice device) {
@ -1998,7 +1812,6 @@ void ClearHostImports(VkDevice device) {
state.imports.clear();
state.liveBytes = 0;
state.failed.clear();
state.failedJoins.clear();
state.device = VK_NULL_HANDLE;
state.refreshedGeneration = 0;
++state.epoch;
@ -2187,20 +2000,6 @@ void SetHostImportWindow(std::uint64_t bytes) {
importWindow.store(bytes, std::memory_order_relaxed);
}
void SetHostImportRangeWindow(std::uint64_t bytes) {
rangeImportWindow.store(bytes, std::memory_order_relaxed);
}
void SetHostImportBudget(std::uint64_t bytes) {
budgetOverride.store(bytes, std::memory_order_relaxed);
}
std::uint64_t HostImportLiveBytes() {
auto& state = Imports();
std::lock_guard lock(state.mutex);
return state.liveBytes;
}
const HostImport* HostImportFor(const Context& context, std::uint64_t address, std::size_t bytes) {
if (context.hostImportAlignment == 0 || bytes == 0 || bytes > std::numeric_limits<std::uint64_t>::max() - address) return nullptr;
ensurePinWaiter();
@ -2208,14 +2007,13 @@ const HostImport* HostImportFor(const Context& context, std::uint64_t address, s
std::lock_guard lock(state.mutex);
// A hit is only valid while the registry has not changed since the imports were reconciled.
if (!importsStale(context, state)) {
if (const auto* entry = findImport(state, address, address + bytes)) {
entry->lastUse = ++state.useCounter;
return entry;
}
if (const auto* entry = findImport(state, address, address + bytes)) return entry;
}
const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix();
refreshImports(context, state, lease);
return importAccess(context, state, address, address + bytes, lease);
if (!runCovers(lease, address, address + bytes)) return nullptr;
const auto* entry = importAllocation(context, state, address, lease);
return entry != nullptr && findImport(state, address, address + bytes) == entry ? entry : nullptr;
}
bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& ranges, std::uint64_t generation, bool adoptDevice) {
@ -2238,7 +2036,7 @@ bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped&
if (range == nullptr || leasedRangeOwner(current, range->address) != range) continue;
try {
if (!GuestMemory::GpuMutex().HeldByThisThread() && wouldRetireImport(state, importRun(current, range->address))) return false;
if (importAllocation(context, state, range->address, current, 0, false) == nullptr && GuestAllocations::GuestAllocationsGeneration_nid_postfix() != generation) state.failed.erase(importRun(current, range->address).base);
if (importAllocation(context, state, range->address, current) == nullptr && GuestAllocations::GuestAllocationsGeneration_nid_postfix() != generation) state.failed.erase(importRun(current, range->address).base);
} catch (const std::exception& error) {
std::fprintf(stderr, "[gpu] import of GPU memory mapped at 0x%llx+0x%zx failed: %s\n", static_cast<unsigned long long>(range->address), range->bytes, error.what());
}
@ -2338,13 +2136,6 @@ void GuestBufferMemory::AcquireRegistered() {
else if (current->generation != generation) spaces.rebuiltGeneration.fetch_add(1, std::memory_order_relaxed);
else if (current->importsEpoch != state.epoch) spaces.rebuiltEpoch.fetch_add(1, std::memory_order_relaxed);
else hit = true;
importFloor = state.useCounter + 1;
if (hit) {
const auto stamp = ++state.useCounter;
for (const auto& region : current->base) {
if (region.direct != nullptr) region.direct->lastUse = stamp;
}
}
}
if (hit) {
for (auto* mirror : current->sparseMirrors) {
@ -2383,8 +2174,6 @@ void GuestBufferMemory::AcquireRegistered() {
auto& state = Imports();
std::lock_guard lock(state.mutex);
if (importable) refreshImports(context, state, lease);
if (importFloor == 0) importFloor = state.useCounter + 1;
state.buildFloor = importFloor;
// Taken before the pass: the imports it takes are the registry's ones (just reconciled, so
// none is retired by a make below), and a mirror's preparation may sync the recorder, whose
// completions can retire an import already taken; that moves the epoch, and Upload then
@ -2406,7 +2195,7 @@ void GuestBufferMemory::AcquireRegistered() {
if (importable) {
auto& state = Imports();
std::lock_guard lock(state.mutex);
entry = importAllocation(context, state, range->address, lease, 0, false);
entry = importAllocation(context, state, range->address, lease);
}
lap(timing.importsUs, at);
if (entry != nullptr) {
@ -2468,20 +2257,6 @@ void GuestBufferMemory::AcquireRegistered() {
publish = false;
spaces.unpublished.fetch_add(1, std::memory_order_relaxed);
} else {
std::size_t kept = 0;
for (std::size_t index = 0; index < regions.size(); ++index) {
auto& region = regions[index];
if (kept != 0) {
auto& previous = regions[kept - 1];
if (region.direct != nullptr && previous.direct == region.direct && previous.end == region.begin && previous.writable == region.writable && region.mirror == nullptr && previous.mirror == nullptr && !region.sparse && !previous.sparse && region.end <= region.direct->base + region.direct->bytes) {
previous.end = region.end;
continue;
}
}
if (kept != index) regions[kept] = std::move(region);
++kept;
}
regions.erase(regions.begin() + static_cast<std::ptrdiff_t>(kept), regions.end());
for (const auto& region : regions) {
if (region.direct != nullptr || region.mirror != nullptr) ranges.push_back(addressRange(region));
}
@ -3055,7 +2830,6 @@ void GuestBufferMemory::UploadPrepare(bool addressable) {
if (context.hostImportAlignment != 0) {
auto& state = Imports();
std::lock_guard lock(state.mutex);
if (importFloor == 0) importFloor = state.useCounter + 1;
const bool stale = importsStale(context, state);
for (auto& region : regions) {
if (region.mirror != nullptr) continue;
@ -3130,19 +2904,10 @@ void GuestBufferMemory::UploadPrepare(bool addressable) {
void GuestBufferMemory::UploadFinish(bool addressable) {
Require(prepared && !uploaded, "guest memory upload was not prepared");
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
const auto started = std::chrono::steady_clock::now();
for (const auto& region : regions) CompletePendingCpuWrites(context, region.begin, static_cast<std::size_t>(region.end - region.begin));
if (space != nullptr) {
for (const auto& region : space->base) CompletePendingCpuWrites(context, region.begin, static_cast<std::size_t>(region.end - region.begin));
}
uploaded = true;
for (const auto& [begin, end] : writes) StorageTexture::FlushPending(begin, static_cast<std::size_t>(end - begin), nullptr, "written buffer range");
if (context.hostImportAlignment != 0) {
auto& state = Imports();
std::lock_guard lock(state.mutex);
state.buildFloor = importFloor != 0 ? importFloor : state.useCounter + 1;
}
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
const auto started = std::chrono::steady_clock::now();
// Registered ranges to import from. Address-based shaders hold their lease until write-back;
// descriptor-only uploads acquire one only when the registry changed or an import must be made
// (a lease copies every registered range's shared_ptr under the registry lock), so the guest can
@ -3172,17 +2937,6 @@ void GuestBufferMemory::UploadFinish(bool addressable) {
++Mirrors().subranges;
}
bool importsRefreshed = false;
if (context.hostImportAlignment != 0) {
auto& state = Imports();
std::lock_guard lock(state.mutex);
if (importsStale(context, state)) refreshImports(context, state, importRanges());
importsRefreshed = true;
for (const auto& region : regions) {
if (!region.pending || region.mirror != nullptr || region.direct != nullptr || sparseHeapCandidate(region)) continue;
if (findImport(state, region.begin, region.end) != nullptr || runCovers(importRanges(), region.begin, region.end)) continue;
static_cast<void>(importJoined(context, state, region.begin, region.end, importRanges()));
}
}
if (space != nullptr && context.hostImportAlignment != 0) {
// The space's regions were confirmed against the imports when it was built; one check that
// none was retired since (a mirror refresh or a flush between the stages can reconcile
@ -3240,7 +2994,7 @@ void GuestBufferMemory::UploadFinish(bool addressable) {
region.direct = nullptr;
if (entry == nullptr) entry = findImport(state, region.begin, region.end);
if (entry == nullptr) {
entry = importAccess(context, state, region.begin, region.end, importRanges());
if (runCovers(importRanges(), region.begin, region.end)) entry = importAllocation(context, state, region.begin, importRanges());
}
// A staged region (see stagingEligible) is copied out of the import even when aligned;
// without a recorder to record the copies it binds in place like any other.

View File

@ -5,7 +5,6 @@
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libc/include/GuestAllocations.hpp"
#include <algorithm>
#include <array>
#include <atomic>
@ -154,7 +153,6 @@ public:
}
Context context;
std::shared_ptr<std::atomic<bool>> active = std::make_shared<std::atomic<bool>>(true);
ColorTarget target;
VkImage image = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
@ -177,19 +175,10 @@ struct GuardedRange {
std::uint64_t address;
std::uint64_t bytes;
std::uint32_t samples;
std::shared_ptr<std::atomic<bool>> active;
};
class PrimedSharedMutex : public std::shared_mutex {
public:
PrimedSharedMutex() {
lock();
unlock();
}
};
std::shared_mutex& guardedMutex() {
static PrimedSharedMutex mutex;
static std::shared_mutex mutex;
return mutex;
}
@ -207,17 +196,7 @@ void guardReads(std::uint64_t address, std::size_t bytes) {
RequireNoMultisampleTarget(address, bytes, "a CPU read");
}
void retireUnmapped(std::uintptr_t address, std::size_t bytes) {
std::unique_lock lock(guardedMutex());
std::erase_if(guarded(), [&](const auto& range) {
if (range.address < address || range.address - address > bytes || range.bytes > bytes - (range.address - address)) return false;
range.active->store(false, std::memory_order_release);
return true;
});
anyGuarded().store(!guarded().empty(), std::memory_order_release);
}
void guard(const Context& context, const ColorTarget& target, const std::shared_ptr<std::atomic<bool>>& active) {
void guard(const Context& context, const ColorTarget& target) {
auto bytes = std::max<std::uint64_t>(target.bytes * target.samples, static_cast<std::uint64_t>(target.extent.width) * target.extent.height * target.elementBytes * target.samples);
if (target.tileMode == ColorTileMode::RenderTarget && std::has_single_bit(target.elementBytes) && std::has_single_bit(target.samples)) {
const auto log2Samples = std::countr_zero(target.samples);
@ -229,13 +208,10 @@ void guard(const Context& context, const ColorTarget& target, const std::shared_
bytes = align(target.extent.width, blockWidth) * align(target.extent.height, blockHeight) * target.elementBytes * target.samples;
}
std::unique_lock lock(guardedMutex());
guarded().push_back({context.device, target.address, bytes, target.samples, active});
guarded().push_back({context.device, target.address, bytes, target.samples});
anyGuarded().store(true, std::memory_order_release);
static std::once_flag installed;
std::call_once(installed, [] {
GuestMemory::SetReadGuard(&guardReads);
});
GuestAllocations::GuestAllocationsSetUnmapObserver_nid_postfix(&retireUnmapped);
std::call_once(installed, [] { GuestMemory::SetReadGuard(&guardReads); });
}
std::mutex& targetsMutex() {
@ -250,15 +226,14 @@ std::vector<std::unique_ptr<MultisampleTarget>>& targets() {
MultisampleTarget& acquire(const Context& context, const ColorTarget& use) {
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;
target->cmask.CheckAddress(use.cmaskAddress);
target->ApplyFastClear(use);
return *target;
}
targets().push_back(std::make_unique<MultisampleTarget>(context, use));
guard(context, use);
auto& created = *targets().back();
guard(context, use, created.active);
if (use.cmaskFastClear) created.cmask.SeedKeys(CurrentDccKeys(use.cmaskAddress, static_cast<std::uint64_t>(use.cmaskBytes) * 256u));
created.ApplyFastClear(use);
return created;
@ -405,7 +380,6 @@ void ResolveMultisampleTarget(const Context& context, const ColorTarget& source,
void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern) {
std::lock_guard lock(targetsMutex());
for (const auto& target : targets()) {
if (!target->active->load(std::memory_order_acquire)) continue;
target->colorFill.NoteFill(address, bytes, pattern);
const auto before = target->cmask.Keys();
target->cmask.NoteFill(address, bytes, pattern);
@ -421,7 +395,6 @@ void ClearMultisampleTargets(VkDevice device) {
std::unique_lock guardLock(guardedMutex());
std::erase_if(guarded(), [&](const auto& range) { return range.device == device; });
anyGuarded().store(!guarded().empty(), std::memory_order_release);
if (guarded().empty()) GuestAllocations::GuestAllocationsSetUnmapObserver_nid_postfix(nullptr);
}
void RequireNoMultisampleTarget(std::uint64_t address, std::size_t bytes, const char* access) {

View File

@ -69,11 +69,10 @@ bool HasStencil(VkFormat format) {
return format == VK_FORMAT_D16_UNORM_S8_UINT || format == VK_FORMAT_D24_UNORM_S8_UINT || format == VK_FORMAT_D32_SFLOAT_S8_UINT || format == VK_FORMAT_S8_UINT;
}
Framebuffer::Framebuffer(const Context& context, VkRenderPass renderPass, std::span<const VkImageView> targets, VkExtent2D extent, std::uint32_t layers) : context(context) {
Framebuffer::Framebuffer(const Context& context, VkRenderPass renderPass, std::span<const VkImageView> targets, VkExtent2D extent) : context(context) {
// Cached objects outlive their device's teardown; they must not keep its buffer pool alive past it.
this->context.bufferPool.reset();
Require(extent.width != 0 && extent.height != 0 && extent.width <= context.limits.maxFramebufferWidth && extent.height <= context.limits.maxFramebufferHeight, "framebuffer extent exceeds device limits");
Require(layers != 0 && layers <= context.limits.maxFramebufferLayers, "framebuffer layers exceed device limits");
views.assign(targets.begin(), targets.end());
if (renderPass == VK_NULL_HANDLE) return;
VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
@ -82,7 +81,7 @@ Framebuffer::Framebuffer(const Context& context, VkRenderPass renderPass, std::s
framebufferInfo.pAttachments = targets.empty() ? nullptr : targets.data();
framebufferInfo.width = extent.width;
framebufferInfo.height = extent.height;
framebufferInfo.layers = layers;
framebufferInfo.layers = 1;
Check(context.Function<PFN_vkCreateFramebuffer>("vkCreateFramebuffer")(context.device, &framebufferInfo, nullptr, &framebuffer), "vkCreateFramebuffer");
}
@ -499,18 +498,13 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
return;
}
if (libraries) pipeline = LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, false, _modules);
else {
auto pipelineCacheLock = context.LockPipelineCacheForUse();
Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines");
}
else Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines");
if (alternateAlpha) {
blend.pAttachments = secondChannels.data();
auto pipelineCacheLock = context.LockPipelineCacheForUse();
Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &secondChannel), "vkCreateGraphicsPipelines");
}
if (state.depthBiasPerFace) {
raster.cullMode = VK_CULL_MODE_FRONT_BIT;
auto pipelineCacheLock = context.LockPipelineCacheForUse();
Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &backFaces), "vkCreateGraphicsPipelines");
}
for (const auto module : _modules) context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, module, nullptr);
@ -577,7 +571,7 @@ VkPipelineLayout Pipeline::Layout() const {
return layout;
}
std::shared_ptr<Framebuffer> Pipeline::AcquireFramebuffer(std::span<const VkImageView> targets, std::span<const std::shared_ptr<StorageTexture>> owners, VkExtent2D extent, std::uint32_t layers, std::span<const std::uint8_t> persistent) {
std::shared_ptr<Framebuffer> Pipeline::AcquireFramebuffer(std::span<const VkImageView> targets, std::span<const std::shared_ptr<StorageTexture>> owners, VkExtent2D extent, std::span<const std::uint8_t> persistent) {
Require(targets.size() == attachments && owners.size() == colorAttachments, "render targets do not match the pipeline's attachments");
bool resident = true;
for (std::size_t i = 0; i < owners.size(); ++i) resident = resident && (owners[i] != nullptr || (i < persistent.size() && persistent[i] != 0));
@ -592,7 +586,7 @@ std::shared_ptr<Framebuffer> Pipeline::AcquireFramebuffer(std::span<const VkImag
return false;
});
for (auto it = framebuffers.begin(); it != framebuffers.end(); ++it) {
if (it->extent.width != extent.width || it->extent.height != extent.height || it->layers != layers || !std::equal(it->views.begin(), it->views.end(), targets.begin(), targets.end())) continue;
if (it->extent.width != extent.width || it->extent.height != extent.height || !std::equal(it->views.begin(), it->views.end(), targets.begin(), targets.end())) continue;
// View handles are recycled once a StorageTexture is destroyed, so the owners must be the
// very objects the views were made for.
bool same = true;
@ -602,7 +596,7 @@ std::shared_ptr<Framebuffer> Pipeline::AcquireFramebuffer(std::span<const VkImag
return framebuffers.back().framebuffer;
}
}
auto framebuffer = std::make_shared<Framebuffer>(context, renderPass, targets, extent, layers);
auto framebuffer = std::make_shared<Framebuffer>(context, renderPass, targets, extent);
if (!resident) return framebuffer;
// Beyond the bound the least recently used unreferenced entry goes.
constexpr std::size_t bound = 8;
@ -616,7 +610,6 @@ std::shared_ptr<Framebuffer> Pipeline::AcquireFramebuffer(std::span<const VkImag
entry.owners.assign(owners.begin(), owners.end());
for (const auto& owner : owners) entry.owned.push_back(owner != nullptr);
entry.extent = extent;
entry.layers = layers;
entry.framebuffer = framebuffer;
framebuffers.push_back(std::move(entry));
return framebuffer;
@ -643,7 +636,7 @@ void Pipeline::Begin(VkCommandBuffer commands, const Framebuffer& framebuffer, V
}
VkRenderingInfoKHR rendering{VK_STRUCTURE_TYPE_RENDERING_INFO_KHR};
rendering.renderArea = {{0, 0}, extent};
rendering.layerCount = state.renderLayers;
rendering.layerCount = 1;
rendering.colorAttachmentCount = static_cast<std::uint32_t>(colors.size());
rendering.pColorAttachments = colors.empty() ? nullptr : colors.data();
if (depthFormat != VK_FORMAT_UNDEFINED) rendering.pDepthAttachment = &depth;
@ -832,7 +825,6 @@ void pipelineKey(std::vector<std::byte>& key, const Context& context, const Stat
append(key, tessellation.domain);
append(key, tessellation.partitioning);
append(key, tessellation.outputTopology);
append(key, tessellation.patchesPerGroup);
}
}

View File

@ -106,15 +106,11 @@ void PipelineCache::load(std::vector<std::byte>& initialData) {
void PipelineCache::save(bool final) {
const auto getData = context.Function<PFN_vkGetPipelineCacheData>("vkGetPipelineCacheData");
std::size_t size = 0;
std::vector<std::byte> file;
{
auto synchronizationLock = synchronization.LockForSnapshot();
if (getData(context.device, cache, &size, nullptr) != VK_SUCCESS || size == 0 || size == savedBytes) return;
file.resize(sizeof(FileHeader) + size);
const auto result = getData(context.device, cache, &size, file.data() + sizeof(FileHeader));
if (result != VK_SUCCESS && result != VK_INCOMPLETE) return;
file.resize(sizeof(FileHeader) + size);
}
if (getData(context.device, cache, &size, nullptr) != VK_SUCCESS || size == 0 || size == savedBytes) return;
std::vector<std::byte> file(sizeof(FileHeader) + size);
const auto result = getData(context.device, cache, &size, file.data() + sizeof(FileHeader));
if (result != VK_SUCCESS && result != VK_INCOMPLETE) return;
file.resize(sizeof(FileHeader) + size);
const FileHeader header{FileMagic, FileFormat, size, ShaderRecompiler::HashBytes(std::span(file).subspan(sizeof(FileHeader)))};
std::memcpy(file.data(), &header, sizeof(header));
const auto started = std::chrono::steady_clock::now();

View File

@ -205,11 +205,7 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip
}
VkPipeline handle = VK_NULL_HANDLE;
if (partsOnly) lock.unlock();
VkResult created;
{
auto pipelineCacheLock = context.LockPipelineCacheForUse();
created = context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &handle);
}
const auto created = context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &handle);
if (partsOnly) lock.lock();
Check(created, "vkCreateGraphicsPipelines library");
if (const auto raced = cache.find(key); raced != cache.end()) {
@ -238,10 +234,7 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip
VkGraphicsPipelineCreateInfo create{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, &linked};
create.layout = info.layout;
VkPipeline pipeline = VK_NULL_HANDLE;
{
auto pipelineCacheLock = context.LockPipelineCacheForUse();
Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &pipeline), "vkCreateGraphicsPipelines link");
}
Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &pipeline), "vkCreateGraphicsPipelines link");
++device.counters.linked;
if (optimize && optimized != nullptr) {
auto slot = std::make_shared<OptimizedPipeline>();
@ -250,9 +243,8 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip
*optimized = slot;
const auto createPipelines = context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines");
const auto cache = context.pipelineCache;
const auto cacheSynchronization = context.pipelineCacheSynchronization;
const auto libraryLayout = fullLayout;
enqueue(store, context.device, [slot, createPipelines, cache, cacheSynchronization, libraryLayout, libraries, &store, devicePointer = &device] {
enqueue(store, context.device, [slot, createPipelines, cache, libraryLayout, libraries, &store, devicePointer = &device] {
if (slot->released.load()) return;
VkPipelineLibraryCreateInfoKHR linked{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR};
linked.libraryCount = static_cast<std::uint32_t>(libraries.size());
@ -261,11 +253,7 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip
create.flags = VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT;
create.layout = libraryLayout;
VkPipeline result = VK_NULL_HANDLE;
const auto pipelineCreationResult = [&] {
auto pipelineCacheLock = cacheSynchronization != nullptr ? cacheSynchronization->LockForPipelineUse() : PipelineCacheSynchronization::PipelineUseLock{};
return createPipelines(slot->device, cache, 1, &create, nullptr, &result);
}();
if (pipelineCreationResult != VK_SUCCESS) return;
if (createPipelines(slot->device, cache, 1, &create, nullptr, &result) != VK_SUCCESS) return;
slot->handle.store(result);
if (slot->released.load()) {
if (const auto handle = slot->handle.exchange(VK_NULL_HANDLE)) slot->destroy(slot->device, handle, nullptr);

View File

@ -1998,10 +1998,7 @@ bool Recorder::gpuSampleCounter() {
pipelineInfo.stage.module = module;
pipelineInfo.stage.pName = "main";
pipelineInfo.layout = sampleLayout;
{
auto pipelineCacheLock = context.LockPipelineCacheForUse();
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &samplePipeline), "vkCreateComputePipelines sample counter");
}
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &samplePipeline), "vkCreateComputePipelines sample counter");
context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, module, nullptr);
module = VK_NULL_HANDLE;
const VkPushConstantRange dumpPush{VK_SHADER_STAGE_COMPUTE_BIT, 0, SampleDumpPushBytes};
@ -2012,10 +2009,7 @@ bool Recorder::gpuSampleCounter() {
Check(context.Function<PFN_vkCreateShaderModule>("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule sample dumps");
pipelineInfo.stage.module = module;
pipelineInfo.layout = dumpLayout;
{
auto pipelineCacheLock = context.LockPipelineCacheForUse();
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &dumpPipeline), "vkCreateComputePipelines sample dumps");
}
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &dumpPipeline), "vkCreateComputePipelines sample dumps");
context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, module, nullptr);
module = VK_NULL_HANDLE;
} catch (const std::exception& error) {
@ -2292,10 +2286,7 @@ bool Recorder::RecordMeshArguments(VkCommandBuffer commands, VkDeviceAddress rec
pipelineInfo.stage.module = module;
pipelineInfo.stage.pName = "main";
pipelineInfo.layout = meshArgumentLayout;
{
auto pipelineCacheLock = context.LockPipelineCacheForUse();
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &meshArgumentPipeline), "vkCreateComputePipelines mesh arguments");
}
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &meshArgumentPipeline), "vkCreateComputePipelines mesh arguments");
context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, module, nullptr);
meshArgumentState = 1;
} catch (const std::exception& error) {
@ -3318,6 +3309,9 @@ void Recorder::SyncThrough(std::uint64_t address, std::size_t bytes, bool waitUn
// later batch's own ordering against this write-back is its recorder's business at record
// time (FillBuffer and DispatchIndirect consult copiedWriters; a later copied writer's
// write-back runs after this one in finish order; a label behind completions lands after).
// Known gap, not introduced here: a later batch writing the same range GPU-direct in place
// (a writable region bound in place, or a GPU copy-back) is not ordered against this
// write-back by the resource build; it went from deterministically stale to racy.
announcedSource = 4;
announcedSite = nullptr;
++holdCounters.completionSyncsSkipped;

View File

@ -113,12 +113,10 @@ namespace AgcDriver::Graphics {
return requiresNonSeamlessCube;
}
bool ForcesDegammaOn(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers) {
if (IsSrgbTextureFormat(guestFormat)) return false;
void RequireDegammaFormat(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers) {
for (std::uint32_t element = 0; element < 32u && element < samplers.size(); ++element) {
if (((samplerMask >> element) & 1u) != 0u && samplers[element]->ForcesDegamma()) return true;
if (((samplerMask >> element) & 1u) != 0u && samplers[element]->ForcesDegamma() && !IsSrgbTextureFormat(guestFormat)) Require(false, "a texture of guest format " + std::to_string(guestFormat) + ", which is not sRGB, is sampled through a sampler that forces sRGB decoding, which is not implemented");
}
return false;
}
void RequireBorderSwizzle(std::uint32_t bcSwizzle, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers) {

View File

@ -143,7 +143,6 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con
const bool depthExportEnable = (shaderControl & 0x1u) != 0;
const bool sampleMaskExportEnable = ((shaderControl >> 8u) & 0x1u) != 0;
const bool stencilRefExportEnable = ((shaderControl >> 1u) & 0x1u) != 0;
const bool depthBeforeShader = ((shaderControl >> 12u) & 0x1u) != 0;
const auto zOrder = (shaderControl >> 4u) & 0x3u;
const auto loaded = [&](PixelInput input) { return (activeInputs & PixelInputBit(input)) != 0; };
return ShaderRecompiler::ShaderPixelStageInfo{
@ -165,7 +164,7 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con
.pixelKillEnable = pixelKillEnable,
.depthExportEnable = depthExportEnable,
.sampleMaskExportEnable = sampleMaskExportEnable,
.earlyZ = depthBeforeShader || ((zOrder & 1u) != 0u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable && !stencilRefExportEnable),
.earlyZ = zOrder == 1u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable && !stencilRefExportEnable,
.executeOnNoop = ((shaderControl >> 10u) & 0x1u) != 0,
.conservativeZExport = static_cast<ShaderRecompiler::ConservativeZExport>(conservativeZExport),
.orderedPixelShader = ((shaderControl >> 16u) & 0x1u) != 0,

View File

@ -9,7 +9,6 @@
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include <algorithm>
#include <atomic>
#include <memory>
@ -111,7 +110,6 @@ struct CachedTexture {
std::uint64_t sourceVersion = 0;
std::uint64_t accounted = 0;
std::uint64_t present = 0;
std::optional<std::vector<std::pair<std::uint64_t, std::uint64_t>>> committed;
};
// Entries in use order (front = most recent) with a hash index by key: a lookup is O(1) and the
@ -555,8 +553,7 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
// memory first.
const bool depthCopy = depthCompare && guestBytes >= DepthCopyMinBytes() && Texture::DepthCopyFormat(ResolveTextureFormat(resource.format)) != VK_FORMAT_UNDEFINED && resource.dimension != TextureDimension::k3D;
auto source = StorageTexture::FindPending(address, guestBytes);
const bool blockCopy = source != nullptr && !depthCompare && Texture::CanBlockCopyFrom(*source, resource);
if (source != nullptr && !blockCopy && (depthCompare ? !depthCopy || !Texture::CanDepthCopyFrom(*source, resource) : !Texture::CanCopyFrom(*source, resource))) source.reset();
if (source != nullptr && (depthCompare ? !depthCopy || !Texture::CanDepthCopyFrom(*source, resource) : !Texture::CanCopyFrom(*source, resource))) source.reset();
if (source != nullptr && MetadataMoved(*source, resource)) source.reset();
std::optional<DccKeys> keys;
bool clearThroughKeys = false;
@ -628,25 +625,11 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
texture->RefreshDepthCopy();
return texture;
}
if (source != nullptr && blockCopy) {
auto texture = std::make_shared<Texture>(context, source, resource, components, Texture::BlockCopyTag{});
texture->RefreshDepthCopy();
return texture;
}
if (source != nullptr) return std::make_shared<Texture>(context, source, resource, components);
std::vector<std::byte> snapshot(bytes);
ReadTextureSurface(resource, *keys, snapshot);
return std::make_shared<Texture>(context, *context.detiler, resource, components, snapshot, depthCompare);
}
auto snapshotGeneration = generation;
std::optional<std::vector<std::pair<std::uint64_t, std::uint64_t>>> committed;
if (source == nullptr && generation == 0) {
std::vector<std::pair<std::uint64_t, std::uint64_t>> ranges;
if (const auto partial = GuestMemory::CollectCommittedWrites(address, bytes, ranges); partial != 0) {
snapshotGeneration = partial;
committed = std::move(ranges);
}
}
auto& cache = Textures();
const auto key = MakeTextureKey(context.device, words, components, depthCompare);
std::lock_guard lock(cache.mutex);
@ -675,13 +658,8 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::TextureCompare);
return GuestMemory::EqualsCommitted(address, it->bytes);
};
const auto unchanged = [&] {
if (it->committed != committed) return false;
return committed.has_value() ? !Recorder::SnapshotWriteOverlaps(address, it->bytes.size()) && GuestMemory::UnchangedCommittedSince(*committed, it->generation) : GuestMemory::UnchangedSince(address, it->bytes.size(), it->generation);
};
if ((*keys != DccKeys::Uncompressed && *keys != DccKeys::ClearSingle) || unchanged() || equalsCommitted()) {
it->generation = snapshotGeneration;
it->committed = std::move(committed);
if ((*keys != DccKeys::Uncompressed && *keys != DccKeys::ClearSingle) || GuestMemory::UnchangedSince(address, it->bytes.size(), it->generation) || equalsCommitted()) {
it->generation = generation;
touchTexture(cache, it);
logLookup({it->texture.get(), resource, guestBytes, *keys, generation, nullptr});
reportTextureCounters();
@ -692,17 +670,13 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
eraseTexture(cache, it);
++cache.counts.stale;
}
CachedTexture entry{key, address, std::vector<std::byte>(source != nullptr ? 0u : bytes), nullptr, *keys, snapshotGeneration};
entry.committed = std::move(committed);
CachedTexture entry{key, address, std::vector<std::byte>(source != nullptr ? 0u : bytes), nullptr, *keys, generation};
if (source != nullptr) {
entry.source = source;
entry.sourceVersion = source->Version();
if (depthCompare) {
entry.texture = std::make_shared<Texture>(context, source, resource, components, Texture::DepthCopyTag{});
entry.texture->RefreshDepthCopy();
} else if (blockCopy) {
entry.texture = std::make_shared<Texture>(context, source, resource, components, Texture::BlockCopyTag{});
entry.texture->RefreshDepthCopy();
} else {
entry.texture = std::make_shared<Texture>(context, source, resource, components);
}
@ -1642,7 +1616,7 @@ void ShaderResources::buildComplete() {
write.pImageInfo = images.data() + images.size();
for (const auto index : binding.imageAllocations) {
if (index == std::numeric_limits<std::size_t>::max()) images.push_back(absentImage(binding.layout.binding));
else images.push_back({VK_NULL_HANDLE, textureSrgb[index] ? textures[index]->SrgbView(textureFirstLayer[index], textureBaseLevel[index]) : textureBaseLevel[index] ? textures[index]->BaseLevelView() : textureFirstLayer[index] ? textures[index]->FirstLayerView() : textures[index]->View(), textures[index]->Layout()});
else images.push_back({VK_NULL_HANDLE, textureBaseLevel[index] ? textures[index]->BaseLevelView() : textureFirstLayer[index] ? textures[index]->FirstLayerView() : textures[index]->View(), textures[index]->Layout()});
}
Require(binding.imageAllocations.size() == binding.layout.descriptorCount, "descriptor allocations disagree with compact binding");
break;
@ -2314,39 +2288,13 @@ ShaderResources::OwnRefreshFallback ShaderResources::refreshOwnObjects(std::span
return OwnRefreshFallback::Count;
}
void CompletePendingCpuWrites(const Context& context, std::uint64_t address, std::size_t bytes) {
if (bytes == 0 || Recorder::InCompletion()) return;
auto* recorder = Recorder::Active();
if (recorder == nullptr) return;
const auto drawWriters = DrawCopiedWriters();
const bool labels = Recorder::PendingCompletionLabels() != 0;
if ((context.copiedWriters == nullptr || context.copiedWriters->empty()) && drawWriters->empty() && !labels) return;
if (!recorder->PendingWriteOverlaps(address, bytes)) return;
const auto overlaps = [&](const auto& writers) {
return std::any_of(writers.begin(), writers.end(), [&](const auto& writer) { return writer->WritesOverlap(address, bytes); });
};
if ((context.copiedWriters != nullptr && overlaps(*context.copiedWriters)) || overlaps(*drawWriters) || (labels && recorder->PendingLabelIn(address, bytes))) {
Recorder::CountSync(4);
recorder->SyncThrough(address, bytes);
}
}
bool ShaderResources::Revalidate(std::span<const CompiledShader> shaders, ProofReport* report) {
if (report != nullptr) *report = {ProofPath::Full, ProofFailure::Other};
if (!reusable || shaders.empty()) return false;
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
for (const auto& region : directRegions) CompletePendingCpuWrites(context, region.begin, static_cast<std::size_t>(region.end - region.begin));
for (const auto& range : describedRanges) {
CompletePendingCpuWrites(context, range.address, static_cast<std::size_t>(range.bytes));
if (range.dccAddress != 0) CompletePendingCpuWrites(context, range.dccAddress, DccKeyBytes(range.bytes));
}
for (const auto& image : storageTextures) {
CompletePendingCpuWrites(context, image->Descriptor().baseAddress, static_cast<std::size_t>(image->GuestBytes()));
if (image->Descriptor().dccAddress != 0) CompletePendingCpuWrites(context, image->Descriptor().dccAddress, DccKeyBytes(image->GuestBytes()));
}
// APS5_NO_FAST_REVALIDATE=1 always repeats the lookups.
static const bool noFast = std::getenv("APS5_NO_FAST_REVALIDATE") != nullptr;
const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
const auto finish = [&](bool fast, bool ok, ProofFailure failure = ProofFailure::Other) {
if (profile) countRevalidate(fast, ok, start);
if (report != nullptr) report->failure = ok ? ProofFailure::None : failure;
@ -3293,7 +3241,7 @@ std::shared_ptr<Texture> ShaderResources::fastTexture(const ImageRecord& record)
auto& cache = Textures();
std::lock_guard lock(cache.mutex);
const auto it = findSampledEntry(cache, context.device, record.words, record.components);
if (it == cache.entries.end() || it->texture != record.texture || it->committed.has_value()) return nullptr;
if (it == cache.entries.end() || it->texture != record.texture) return nullptr;
if (it->source == nullptr) it->generation = record.generation;
touchTexture(cache, it);
logLookup({record.texture.get(), record.resource, record.guestBytes, keys, record.source != nullptr ? 0 : record.generation, record.source.get()});
@ -3333,14 +3281,12 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind
continue;
}
const auto resource = record != nullptr && record->decoded ? record->resource : DecodeTextureResource(words);
const bool forcedSrgb = ForcesDegammaOn(resource.format, binding.imageSamplers[element], shaderSamplers);
RequireDegammaFormat(resource.format, binding.imageSamplers[element], shaderSamplers);
RequireNonSeamlessCube(resource.dimension == TextureDimension::kCube, binding.imageSamplers[element], shaderSamplers);
const bool firstLayer = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k2DArray;
if (!firstLayer && !MatchesGuestDimension(*binding.imageShape, resource.dimension)) throw std::runtime_error("AGC graphics: guest texture dimension disagrees with the shader's declared image shape (shape " + std::to_string(static_cast<int>(*binding.imageShape)) + ", dimension " + std::to_string(static_cast<int>(resource.dimension)) + ")");
const VkComponentMapping components = ViewComponents(resource);
const auto guestBytes = record != nullptr && record->decoded ? record->guestBytes : DescribeSurface(resource).guestBytes;
CompletePendingCpuWrites(context, resource.baseAddress, static_cast<std::size_t>(guestBytes));
if (resource.dccAddress != 0) CompletePendingCpuWrites(context, resource.dccAddress, DccKeyBytes(guestBytes));
std::shared_ptr<Texture> texture;
const bool depthCompare = !binding.imageDepthCompare.empty() && binding.imageDepthCompare.at(element);
if (previousSampled.texture != nullptr && previousSampled.depthCompare == depthCompare && words.size() == 8 && std::equal(words.begin(), words.end(), previousSampled.words.begin())) texture = previousSampled.texture;
@ -3360,7 +3306,6 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind
}
RequireFilterMinmax(context, texture->ViewFormat(), binding.imageSamplers[element], shaderSamplers);
RequireBorderSwizzle(resource.bcSwizzle, binding.imageSamplers[element], shaderSamplers);
if (forcedSrgb && !texture->HasSrgbView()) Require(false, "a texture of guest format " + std::to_string(resource.format) + ", which is not sRGB and has no sRGB view, is sampled through a sampler that forces sRGB decoding, which is not implemented");
if (!shaderSamplers.empty()) {
const bool singleLevel = DescriptorSingleLevel(words);
const auto firstSampler = static_cast<std::size_t>(shaderSamplers.data() - samplers.data());
@ -3378,7 +3323,6 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind
textures.push_back(std::move(texture));
textureFirstLayer.push_back(firstLayer);
textureBaseLevel.push_back(baseLevel);
textureSrgb.push_back(forcedSrgb);
describedRanges.push_back({"texture", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast<int>(resource.tileMode), resource.dccAddress});
item.imageAllocations.push_back(textures.size() - 1);
}
@ -3407,8 +3351,6 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind
const auto mip = std::min(resource.baseLevel + mipOffset, resource.mipCount - 1u);
Require(resource.minLod <= mip * 256u, "guest storage texture descriptor clamps its minimum LOD above the level it addresses, which is not implemented");
const auto guestBytes = record != nullptr && record->decoded ? record->guestBytes : DescribeSurface(resource).guestBytes;
CompletePendingCpuWrites(context, resource.baseAddress, static_cast<std::size_t>(guestBytes));
if (resource.dccAddress != 0) CompletePendingCpuWrites(context, resource.dccAddress, DccKeyBytes(guestBytes));
// The same surface as the previous element: its image was just looked up and refreshed.
if (sameAsPrevious && StorageDedupeEnabled()) storageTextures.push_back(storageTextures.back());
else storageTextures.push_back(cachedStorageTexture(context, words, resource, mip, guestBytes));
@ -3573,7 +3515,7 @@ std::size_t ShaderResources::MaxDrawSnapshotBytes() {
std::shared_ptr<ShaderResources::DrawBindings> ShaderResources::PrepareDrawBindings(Recorder& recorder, std::span<const MovedBuffer> moved) const {
if ((_set == VK_NULL_HANDLE && fragment.set == VK_NULL_HANDLE) || usesBda) return {};
const auto reads = guestMemory.InPlaceReads();
const auto maxSnapshotBytes = maxDrawSnapshotBytes;
const auto maxSnapshotBytes = MaxDrawSnapshotBytes();
std::size_t totalSnapshotBytes = 0;
auto result = std::make_shared<DrawBindings>();
std::vector<std::size_t> selected;

View File

@ -49,7 +49,6 @@ struct Module {
std::uint32_t cullDistances = 0;
bool primitiveIndices = false;
bool fragmentBarycentric = false;
bool layerOutput = false;
std::map<std::uint32_t, std::vector<std::uint32_t>> modes;
const std::vector<std::uint32_t>& Type(std::uint32_t id) const {
@ -135,14 +134,13 @@ struct Module {
return;
}
if (!input && value == spv::BuiltInPrimitiveTriangleIndicesEXT) primitiveIndices = true;
Require((input && ((value == spv::BuiltInWorkgroupId || value == spv::BuiltInLocalInvocationId || value == spv::BuiltInGlobalInvocationId || value == spv::BuiltInNumWorkgroups) && signature == "u32x3")) || (input && value == spv::BuiltInLocalInvocationIndex && signature == "u32") || (!input && value == spv::BuiltInPrimitiveTriangleIndicesEXT && signature == "u32x3") || (!input && value == spv::BuiltInCullPrimitiveEXT && signature == "bool") || (!input && value == spv::BuiltInLayer && signature == "u32"), "unsupported mesh built-in");
Require((input && ((value == spv::BuiltInWorkgroupId || value == spv::BuiltInLocalInvocationId || value == spv::BuiltInGlobalInvocationId || value == spv::BuiltInNumWorkgroups) && signature == "u32x3")) || (input && value == spv::BuiltInLocalInvocationIndex && signature == "u32") || (!input && value == spv::BuiltInPrimitiveTriangleIndicesEXT && signature == "u32x3") || (!input && value == spv::BuiltInCullPrimitiveEXT && signature == "bool"), "unsupported mesh built-in");
return;
}
const auto signature = Signature(type);
if (vertex && storage == spv::StorageClassInput) {
Require((value == spv::BuiltInVertexIndex || value == spv::BuiltInInstanceIndex) && signature == "i32", "unsupported vertex built-in input");
} else if (vertex && storage == spv::StorageClassOutput) {
if (value == spv::BuiltInLayer && layerOutput && (signature == "u32" || signature == "i32")) return;
Require(value == spv::BuiltInPosition && signature == "f32x4" && !position, "unsupported or duplicate vertex built-in output");
position = true;
} else {
@ -151,7 +149,7 @@ struct Module {
}
};
Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance, bool viewportIndexLayer) {
Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance) {
using Stage = ShaderRecompiler::ShaderStage;
Require(compiled.program != nullptr, "missing compiled shader");
const auto& shader = *compiled.program;
@ -201,11 +199,6 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
Require(fragmentShaderBarycentric, "device does not support enabled VK_KHR_fragment_shader_barycentric with fragmentShaderBarycentric");
module.fragmentBarycentric = true;
}
const bool isLayerCapability = capability == spv::CapabilityShaderViewportIndexLayerEXT;
if (isLayerCapability) {
Require(viewportIndexLayer, "device does not support enabled VK_EXT_shader_viewport_index_layer");
module.layerOutput = true;
}
VkSubgroupFeatureFlags subgroupOperations = 0;
switch (capability) {
case spv::CapabilityGroupNonUniform: subgroupOperations = VK_SUBGROUP_FEATURE_BASIC_BIT; break;
@ -302,8 +295,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
isFeatureCapability ||
isDescriptorIndexingCapability ||
((imageInt64Atomics || bufferInt64Atomics) && capability == spv::CapabilityInt64Atomics) ||
(imageInt64Atomics && capability == spv::CapabilityInt64ImageEXT) ||
isLayerCapability,
(imageInt64Atomics && capability == spv::CapabilityInt64ImageEXT),
std::string("SPIR-V requires unsupported device capability ") +
std::to_string(static_cast<std::uint32_t>(capability)));
@ -328,7 +320,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
Require(fragment && fragmentShaderBarycentric, "SPV_KHR_fragment_shader_barycentric requires enabled fragmentShaderBarycentric in a fragment shader");
break;
}
Require(extension == "SPV_KHR_float_controls" || (imageInt64Atomics && extension == "SPV_EXT_shader_image_int64") || (viewportIndexLayer && extension == "SPV_EXT_shader_viewport_index_layer") || (mesh && (extension == "SPV_EXT_mesh_shader" || extension == "SPV_KHR_physical_storage_buffer")) || (descriptorIndexing && extension == "SPV_EXT_descriptor_indexing") || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (extension == "SPV_KHR_physical_storage_buffer" || extension == "SPV_KHR_8bit_storage")), "unsupported SPIR-V extension");
Require(extension == "SPV_KHR_float_controls" || (imageInt64Atomics && extension == "SPV_EXT_shader_image_int64") || (mesh && (extension == "SPV_EXT_mesh_shader" || extension == "SPV_KHR_physical_storage_buffer")) || (descriptorIndexing && extension == "SPV_EXT_descriptor_indexing") || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (extension == "SPV_KHR_physical_storage_buffer" || extension == "SPV_KHR_8bit_storage")), "unsupported SPIR-V extension");
break;
}
case spv::OpDecorateId:
@ -441,12 +433,9 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
mode(spv::ExecutionModeOutputVertices, {state.rectList ? 4u : state.stages.tessellation->outputControlPoints});
Require(module.modes.size() == 1, "unsupported tessellation-control execution mode");
} else if (evaluation) {
Require(state.rectList || state.stages.tessellation.has_value(), "tessellation configuration is missing");
const bool quads = state.rectList || state.stages.tessellation->domain == 2u;
const bool counterClockwise = !state.rectList && state.stages.tessellation->outputTopology == 3u;
mode(quads ? spv::ExecutionModeQuads : spv::ExecutionModeTriangles, {});
mode(state.rectList || state.stages.tessellation->partitioning == 0u ? spv::ExecutionModeSpacingEqual : spv::ExecutionModeSpacingFractionalOdd, {});
mode(counterClockwise ? spv::ExecutionModeVertexOrderCcw : spv::ExecutionModeVertexOrderCw, {});
mode(state.rectList ? spv::ExecutionModeQuads : spv::ExecutionModeTriangles, {});
mode(state.rectList ? spv::ExecutionModeSpacingEqual : spv::ExecutionModeSpacingFractionalOdd, {});
mode(spv::ExecutionModeVertexOrderCw, {});
Require(module.modes.size() == 3, "unsupported tessellation-evaluation execution mode");
} else if (geometry) {
mode(spv::ExecutionModeTriangles, {});
@ -517,7 +506,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
}
}
} else if (variable.storage == spv::StorageClassPushConstant) {
Require(!push && (!shader.pushConstants.empty() || mesh || (control && !rectListControl)) && (type[0] & 0xffffu) == spv::OpTypeStruct, "invalid push constant interface");
Require(!push && (!shader.pushConstants.empty() || mesh) && (type[0] & 0xffffu) == spv::OpTypeStruct, "invalid push constant interface");
push = true;
Require(type.size() == 3 && module.decorations[typeId].block, "push constant variable must be a Block struct with exactly one member");
const auto offset = module.offsets.find({typeId, 0});
@ -585,7 +574,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
Require(shader.vertexAttributes.empty(), "vertex attribute metadata is invalid for this stage");
}
Require(descriptors.size() == shader.bindings.size(), "recompiler binding metadata contains undeclared resources");
Require(push == (!shader.pushConstants.empty() || mesh || (control && !rectListControl)), "recompiler push constant metadata disagrees with SPIR-V");
Require(push == (!shader.pushConstants.empty() || mesh), "recompiler push constant metadata disagrees with SPIR-V");
for (const auto id : module.interface) Require(module.variables.contains(id), "entry point interface contains an unknown variable");
if (mesh) Require(module.primitiveIndices, "mesh shader does not export primitive indices");
if (stage == Stage::Vertex || mesh || evaluation || geometry) Require(module.position, "vertex shader does not export position");
@ -594,7 +583,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
}
std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance, bool viewportIndexLayer) {
std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance) {
using Stage = ShaderRecompiler::ShaderStage;
const bool tessellation = state.stages.path == ShaderPath::Tessellation;
const bool mesh = state.stages.path == ShaderPath::Geometry;
@ -615,7 +604,7 @@ std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders,
Require(shaders[i].program != nullptr, "missing compiled shader");
Require(shaders[i].stage == expected, "graphics stage order disagrees");
for (const auto& binding : shaders[i].program->bindings) Require(binding.descriptorSet == ShaderRecompiler::RuntimeAbi::DescriptorSet || (shaders[i].stage == ShaderRecompiler::ShaderStage::Fragment && binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet), "graphics resource uses a descriptor set other than its stage's");
const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics, shaderClipDistance, shaderCullDistance, viewportIndexLayer);
const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics, shaderClipDistance, shaderCullDistance);
if (i != 0) {
for (const auto& [location, signature] : current.inputs) {
const auto output = previous.outputs.find(location);

View File

@ -2,9 +2,7 @@
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libc/include/General.hpp"
#include <algorithm>
@ -99,30 +97,10 @@ VkConservativeRasterizationModeEXT decodeConservativeRasterization(const QueueSt
// The register rules DecodeState and DrawRejection share (the precheck must reject exactly what
// DecodeState would): the masks whose set bits are unsupported, and the depth-control verdict.
// Render target index, viewport index and the misc export vector that carries them are accepted but
// not routed: color targets are single-layer, so layered draws land in layer 0.
constexpr std::uint32_t LayerExports = (1u << 18u) | (1u << 19u) | (1u << 21u) | (1u << 24u);
constexpr std::uint32_t ClipCullExports = 0xffffu | (1u << 22u) | (1u << 23u);
std::uint32_t inactiveVertexPointSize(const QueueState& queue) {
const auto stages = find(queue.context, 0x2d5);
const auto primitive = find(queue.userConfig, 0x242, RegisterBank::UserConfig);
const auto raster = find(queue.context, 0x205);
if (stages == queue.context.end() || primitive == queue.userConfig.end() || raster == queue.context.end()) return 0;
if ((stages->second & 0x24u) != 0) return 0;
if ((raster->second & 8u) != 0 && (raster->second & 0x7e0u) != 0x240u) return 0;
switch (primitive->second) {
case 2:
case 3:
case 4:
case 5:
case 6:
case 7:
case 17:
return 1u << 16u;
default:
return 0;
}
}
constexpr std::uint32_t DepthControlMask = ~0x007007f0u;
// EXEC_ON_HIER_FAIL / EXEC_ON_NOOP / EXEC_IF_OVERLAPPED (bits 9, 10, 17) only force the pixel shader
// to run, which it always does here.
@ -377,20 +355,9 @@ std::string singleSampleMsaaRejection(const QueueState& queue) {
return {};
}
constexpr std::uint32_t CoveredCentroidIsCenter = 1u << 29u;
constexpr std::uint32_t CentroidInputs = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCentroid) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::LinearCentroid);
bool coveredCentroidSupported(const QueueState& queue) {
const auto config = find(queue.context, 0x2f8);
if (config == queue.context.end() || (config->second & CoveredCentroidIsCenter) == 0 || PixelProgramSkipped(queue)) return true;
const auto ena = find(queue.context, 0x1b3);
const auto addr = find(queue.context, 0x1b4);
return ena != queue.context.end() && addr != queue.context.end() && (ena->second & addr->second & CentroidInputs) == 0;
}
std::uint32_t decodeSamples(const Registers& cx) {
const auto config = read(cx, 0x2f8);
zero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u | CoveredCentroidIsCenter), "coverage conversion or nonstandard multisampling");
zero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u), "coverage conversion or nonstandard multisampling");
const auto log2 = config & 7u;
if (log2 == 0) {
Require(((config >> 20u) & 7u) == 0, "exposed samples without multisampling are unsupported");
@ -646,22 +613,17 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) {
validate((value & ~0x0207ed0du) == 0 && (value & 0x02002000u) == 0x02002000u && (value & 3u) == 1u && ((value >> 3u) & 3u) == 1u, "unsupported tessellation routing");
const auto config = read(queue.context, 0x2d6);
const auto parameters = read(queue.context, 0x2db);
ShaderRecompiler::TessellationConfiguration tess{(config >> 8u) & 0x3fu, (config >> 14u) & 0x3fu, parameters & 3u, (parameters >> 2u) & 7u, (parameters >> 5u) & 7u, config & 0xffu};
ShaderRecompiler::TessellationConfiguration tess{(config >> 8u) & 0x3fu, (config >> 14u) & 0x3fu, parameters & 3u, (parameters >> 2u) & 7u, (parameters >> 5u) & 7u};
validate(tess.inputControlPoints != 0 && tess.inputControlPoints <= 32 && tess.outputControlPoints != 0 && tess.outputControlPoints <= 32, "invalid tessellation control-point counts");
validate(tess.patchesPerGroup != 0, "VGT_LS_HS_CONFIG.NUM_PATCHES is zero");
if ((parameters & ~0x000600ffu) != 0) Require(false, zeroMessage(0x2db, parameters, "VGT_TF_PARAM donut, factor detection, memory type or reserved state"));
validate((tess.domain == 1 || tess.domain == 2) && (tess.partitioning == 0 || tess.partitioning == 2) && (tess.outputTopology == 2 || tess.outputTopology == 3), "only triangle and quad domains with integer or fractional-odd partitioning and triangle output are supported by the reference path");
result.minTessellationLevel = readFloat(queue.context, 0x287);
result.maxTessellationLevel = readFloat(queue.context, 0x286);
Require(result.minTessellationLevel >= 0.0f && result.minTessellationLevel <= 63.0f && result.minTessellationLevel <= result.maxTessellationLevel && result.maxTessellationLevel >= 1.0f, "tessellation level clamps need 0 <= VGT_HOS_MIN_TESS_LEVEL <= 63, VGT_HOS_MAX_TESS_LEVEL >= 1 and the minimum at most the maximum");
validate(tess.domain == 1 && tess.partitioning == 2 && tess.outputTopology == 2, "only triangular, fractional-odd, clockwise tessellation is supported by the reference path");
Require(readFloat(queue.context, 0x287) <= 1.0f && readFloat(queue.context, 0x286) >= 63.0f, "tessellation level clamps inside the fractional-odd range [1, 63] are unsupported");
result.tessellation = tess;
} else {
const bool fastLaunch = ((value >> 19u) & 3u) == 1u;
validate((value & ~(fastLaunch ? 0x004fec30u : 0x0047ec30u)) == 0, "unsupported geometry routing, fast launch or wave-ID state");
validate((value & ~0x0047ec30u) == 0, "unsupported geometry routing, fast launch or wave-ID state");
const auto group = read(queue.userConfig, 0x25b, RegisterBank::UserConfig);
const auto vertices = (group >> 9u) & 0x1ffu;
const auto primitives = group & 0x1ffu;
validate(!fastLaunch || (!vertexWorkgroup && primitive == 1 && vertices == 1 && primitives == 1), "fast launch other than one point per subgroup is unsupported");
const auto maxVertices = read(queue.context, 0x1ff);
const auto verticesPerPrimitive = vertexWorkgroup ? 3u : read(queue.context, 0x2ce);
validate((primitive == 1 || primitive == 2 || primitive == 4 || primitive == 5 || primitive == 6) && read(queue.context, 0x29b) == 2 && verticesPerPrimitive >= 3, "unsupported geometry input or output assembly");
@ -672,14 +634,11 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) {
const auto groupPrimitives = std::min({primitives, (vertices - inputSize) / inputStep + 1u, outputLimit});
validate(groupPrimitives != 0, "geometry subgroup contains no primitives");
const auto resources = read(queue.shader, 0x8b, RegisterBank::Shader);
validate(vertexWorkgroup || fastLaunch || (((read(queue.shader, 0x8a, RegisterBank::Shader) >> 29u) & 3u) == 3 && ((resources >> 16u) & 3u) == 3), "unsupported geometry VGPR allocation");
validate(vertexWorkgroup || (((read(queue.shader, 0x8a, RegisterBank::Shader) >> 29u) & 3u) == 3 && ((resources >> 16u) & 3u) == 3), "unsupported geometry VGPR allocation");
const auto esgsItemSize = read(queue.context, 0x2ab);
validate(esgsItemSize != 0 && esgsItemSize * vertices <= 0xffffu, "invalid VGT_ESGS_RING_ITEMSIZE");
const auto amplification = fastLaunch ? read(queue.context, 0x2d3) & 0x1ffu : verticesPerPrimitive - 2u;
validate(amplification != 0, "fast launch subgroup amplifies to no primitives");
const auto maxPrimitives = primitives * amplification;
const auto threads = std::max({(groupPrimitives - 1u) * inputStep + inputSize, primitives, maxVertices, maxPrimitives});
result.mesh = ShaderRecompiler::MeshConfiguration{primitive, groupPrimitives, (groupPrimitives - 1u) * inputStep + inputSize, maxVertices, maxPrimitives, ((threads + result.vertexWaveSize - 1u) / result.vertexWaveSize) * result.vertexWaveSize, ((resources >> 19u) & 0xffu) * 128u, 0, esgsItemSize};
const auto threads = std::max({(groupPrimitives - 1u) * inputStep + inputSize, primitives, maxVertices, primitives * (verticesPerPrimitive - 2u)});
result.mesh = ShaderRecompiler::MeshConfiguration{primitive, groupPrimitives, (groupPrimitives - 1u) * inputStep + inputSize, maxVertices, primitives * (verticesPerPrimitive - 2u), ((threads + result.vertexWaveSize - 1u) / result.vertexWaveSize) * result.vertexWaveSize, ((resources >> 19u) & 0xffu) * 128u, 0, esgsItemSize};
}
return result;
}
@ -734,11 +693,17 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
}
APS5_LOG_OUT_DEBUG("Topology=%u", static_cast<unsigned>(result.topology));
result.primitiveRestart = read(queue.userConfig, 0x24b, RegisterBank::UserConfig) != 0 && !result.rectList && result.topology != VK_PRIMITIVE_TOPOLOGY_PATCH_LIST;
zero(cx, 0x207, ~(LayerExports | ClipCullExports | inactiveVertexPointSize(queue)), "layer, viewport or auxiliary vertex exports");
if ((read(cx, 0x207) & LayerExports) != 0) {
static bool reported = false;
if (!reported) {
reported = true;
std::fprintf(stderr, "[gpu] layer/viewport index vertex exports are ignored (PA_CL_VS_OUT_CNTL=0x%08x)\n", read(cx, 0x207));
}
}
zero(cx, 0x207, ~(LayerExports | ClipCullExports), "layer, viewport or auxiliary vertex exports");
Require(std::popcount(read(cx, 0x207) & 0xffffu) <= 8, "more than eight clip and cull distances are unsupported");
result.paClVsOutCntl = read(cx, 0x207);
result.samples = decodeSamples(cx);
Require(coveredCentroidSupported(queue), "COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs is not implemented");
decodeSampleLocations(cx, result);
{
const auto depthControl = read(cx, 0x200);
@ -852,12 +817,6 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
}
if (result.hasColorTarget) {
result.color = result.colors.front();
result.renderLayers = result.color.layers;
for (const auto& color : result.colors) Require(color.layers == result.renderLayers, "color targets with different layer counts are unsupported");
if (result.renderLayers > 1) {
Require(!result.depth, "layered rendering with a depth target is unsupported");
result.layerExports = read(cx, 0x207) & (1u << 18u);
}
if (result.depth) result.renderExtent = {std::min(result.renderExtent.width, result.depth->extent.width), std::min(result.renderExtent.height, result.depth->extent.height)};
} else if (result.depth) {
result.renderExtent = result.depth->extent;
@ -868,13 +827,6 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
APS5_LOG_OUT_DEBUG("Render extent from screen=%ux%u", result.renderExtent.width, result.renderExtent.height);
Require(result.renderExtent.width != 0 && result.renderExtent.height != 0, "empty framebuffer extent for a draw without color writes");
}
if ((read(cx, 0x207) & LayerExports & ~result.layerExports & ~(1u << 21u)) != 0) {
static bool reported = false;
if (!reported) {
reported = true;
std::fprintf(stderr, "[gpu] layer/viewport index vertex exports are ignored (PA_CL_VS_OUT_CNTL=0x%08x)\n", read(cx, 0x207));
}
}
const auto xs = readFloat(cx, 0x10f);
const auto xo = readFloat(cx, 0x110);
const auto ys = readFloat(cx, 0x111);
@ -920,10 +872,11 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
for (std::uint32_t component = 0; component < 4; ++component) {
if (((exportedMask >> ((mapping >> (2u * component)) & 3u)) & 1u) != 0) state.colorWriteMask |= 1u << component;
}
state.blendEnable = ((blend >> 30u) & 1u) != 0 && (read(cx, 0x31c + slot * 0xfu) & 0x10000u) == 0;
state.blendEnable = (blend >> 30u) & 1u;
if (state.blendEnable && (mapping == 0x1bu || mapping == 0x93u)) throw std::runtime_error("AGC graphics: blending into a color target with a reversed component order is not implemented");
Require(!state.blendEnable || !color.uintExport, "blending into an unsigned integer target is unsupported");
if (state.blendEnable) {
Require((read(cx, 0x31c + slot * 0xfu) & 0x10000u) == 0, "blend bypass conflicts with enabled blending");
state.srcColorBlendFactor = blendFactor(blend & 0x1fu);
state.dstColorBlendFactor = blendFactor((blend >> 8u) & 0x1fu);
state.colorBlendOp = blendOp((blend >> 5u) & 7u);
@ -1037,41 +990,6 @@ std::uint32_t ColorWriteMask(const Registers& context) {
return mask;
}
namespace {
std::uint32_t GuestFormatFor(VkFormat format, std::uint32_t elementBytes) {
if (const auto guest = FindGuestColorTargetFormat(format, elementBytes)) return *guest;
throw std::runtime_error("AGC graphics: no guest texture format matches the color buffer format " + std::to_string(static_cast<int>(format)));
}
}
GuestTextureResource SurfaceForTarget(const ColorTarget& color) {
Require(color.tileMode != ColorTileMode::Linear, "linear color targets are not resident");
const bool chain = color.mipCount > 1;
GuestTextureResource surface{};
surface.baseAddress = chain ? color.surfaceAddress : color.address;
surface.pipeBankXor = color.pipeBankXor;
surface.width = chain ? color.surfaceExtent.width : color.extent.width;
surface.height = chain ? color.surfaceExtent.height : color.extent.height;
surface.depthOrLastArray = color.depth - 1u;
surface.baseArray = 0;
surface.mipCount = color.mipCount;
surface.baseLevel = 0;
surface.lastLevel = color.mipCount - 1;
surface.tileMode = ColorTextureTileMode(color.tileMode);
surface.dimension = color.depth > 1 ? TextureDimension::k3D : TextureDimension::k2D;
surface.format = GuestFormatFor(color.format, color.elementBytes);
surface.dstSelX = 4;
surface.dstSelY = 5;
surface.dstSelZ = 6;
surface.dstSelW = 7;
surface.dccAddress = color.dccAddress;
surface.dccAlphaOnMsb = color.dccAlphaOnMsb;
surface.dccPipeAligned = color.dccPipeAligned;
return surface;
}
ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) {
const auto stride = slot * 0xfu;
ColorTarget color{};
@ -1108,16 +1026,16 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) {
Require(((attrib3 >> 24u) & 3u) != 0u || (attrib2 & 0x3fffu) == 0u, "1D color targets taller than one row are unsupported");
if (volume) {
color.depth = (attrib3 & 0x1fffu) + 1u;
Require(maxMip == 0, "mipmapped 3D color targets are unsupported");
Require(slice < color.depth && lastSlice >= slice, "the color view slices are outside the 3D surface");
Require(maxMip == 0 && (info & 0x10000000u) == 0, "mipmapped or DCC 3D color targets are unsupported");
Require(slice < color.depth, "the color view slice is beyond the 3D surface");
Require(lastSlice == slice, "color views of several 3D slices are unsupported");
color.depthSlice = slice;
color.layers = std::min(lastSlice + 1u, color.depth) - slice;
} else if ((attrib3 & 0x1fffu) != 0) {
Require(lastSlice <= (attrib3 & 0x1fffu), "the color view slice is beyond the array surface");
color.arrayLastSlice = attrib3 & 0x1fffu;
color.arraySlice = slice;
}
if (!volume && lastSlice != slice) {
if (lastSlice != slice) {
const auto exports = find(cx, 0x207);
Require(exports == cx.end() || (exports->second & (1u << 18u)) == 0, "layered draws into a color view of several array slices are unsupported");
}
@ -1145,17 +1063,7 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) {
}
Require(color.tileMode != ColorTileMode::D64KBX || (color.pipeBankXor & ~0xcf00u) == 0, "unsupported display color pipe/bank XOR column bits");
color.arrayAddress = color.arrayLastSlice != 0 ? color.surfaceAddress : 0;
if (slice != 0 && !volume) {
color.surfaceAddress += slice * ComputeSurfaceSize(ComputeElementMipLayout(ColorTextureTileMode(color.tileMode), color.elementBytes, color.surfaceExtent.width, color.surfaceExtent.height, color.mipCount), 1);
if (color.arrayLastSlice == 0 && ColorTileModeIsXor(color.tileMode)) {
const auto* equation = FindTextureSwizzleEquation(static_cast<std::uint32_t>(color.tileMode), color.elementBytes);
Require(equation != nullptr, "a sliced color target has no swizzle equation");
const auto coordinates = slice << 24u;
for (std::uint32_t bit = 0; bit < equation->bits.size(); ++bit) {
color.pipeBankXor ^= (std::popcount(equation->bits[bit] & coordinates) & 1u) << bit;
}
}
}
if (slice != 0 && !volume) color.surfaceAddress += slice * ComputeSurfaceSize(ComputeElementMipLayout(ColorTextureTileMode(color.tileMode), color.elementBytes, color.surfaceExtent.width, color.surfaceExtent.height, color.mipCount), 1);
color.address = color.surfaceAddress + mipOffset;
color.bytes = colorLayout.Bytes();
GuestMemory::CheckRange(reinterpret_cast<const void*>(color.address), color.bytes, colorLayout.Alignment(), true);
@ -1168,7 +1076,7 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) {
const auto clear = find(cx, 0x323 + word + stride);
color.clearWords[word] = clear == cx.end() ? 0u : clear->second;
}
if (((info & 0x2000u) != 0 || (color.samples > 1 && (info & 0x4000u) != 0)) && (color.samples > 1 || viewMip == 0)) {
if ((info & 0x2000u) != 0 || (color.samples > 1 && (info & 0x4000u) != 0)) {
Require(color.samples > 1 || (maxMip == 0 && !volume && slice == 0), "CMASK fast clears of a mipmapped, 3D or array color target are unsupported");
Require(((attrib3 >> 19u) & 0x1fu) == 0x18u && (attrib3 & 0x4000000u) != 0, "CMASK fast clears need pipe-aligned SW_64KB_Z_X metadata (CB_COLOR_ATTRIB3 FMASK_SW_MODE 24, CMASK_PIPE_ALIGNED)");
if (color.samples == 1) Require(color.elementBytes <= 8, "CMASK fast clears of texels over 64 bits are unsupported");
@ -1196,10 +1104,6 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) {
}
}
}
if (volume && (color.tileMode == ColorTileMode::RenderTarget || color.tileMode == ColorTileMode::Standard4KB)) {
color.bytes = DescribeSurface(SurfaceForTarget(color)).guestBytes;
GuestMemory::CheckRange(reinterpret_cast<const void*>(color.address), color.bytes, colorLayout.Alignment(), true);
}
return color;
}
@ -1216,18 +1120,7 @@ std::optional<ColorMetadataPass> DecodeColorMetadataPass(const QueueState& queue
if (mode != 2u && mode != 6u && !resolve) return std::nullopt;
ColorMetadataPass pass{resolve ? ColorMetadataPass::Mode::Resolve : mode == 2u ? ColorMetadataPass::Mode::EliminateFastClear : ColorMetadataPass::Mode::DccDecompress, {}, std::nullopt};
Require((control->second & ~0x70u) == 0xcc0000u, "CB metadata pass with a nonstandard ROP, dual quads disabled or degamma");
const auto value = [&](std::uint32_t offset) {
const auto found = find(cx, offset);
return found == cx.end() ? 0u : found->second;
};
const bool planes = !depthPlanesAbsent(cx);
const auto depthControl = planes ? read(cx, 0x200) : 0u;
const bool depthWork = (depthControl & 0xfu) != 0 && ((depthControl & 8u) != 0 || !depthPassThrough(depthControl));
if (depthWork || (planes && (read(cx, 0x0) & 0xfu) != 0)) {
char text[192];
std::snprintf(text, sizeof(text), "CB metadata pass with depth or stencil work (DB_DEPTH_CONTROL 0x%08x, DB_RENDER_CONTROL 0x%08x, DB_Z_INFO 0x%08x, DB_STENCIL_INFO 0x%08x)", value(0x200), value(0x0), value(0x010), value(0x011));
throw std::runtime_error(text);
}
Require(depthPlanesAbsent(cx) || ((read(cx, 0x200) & 0xfu) == 0 && (read(cx, 0x0) & 0xfu) == 0), "CB metadata pass with depth or stencil work");
const auto samples = resolve && rasterizerSamples ? decodeSamples(cx) : 1u;
if (!resolve) zero(cx, 0x2f8, ~0u, "multisampling or coverage conversion");
zero(cx, 0x80, ~0u, "window offset");
@ -1296,7 +1189,7 @@ std::string DrawRejection(const QueueState& queue, bool indexed) {
if (value(queue.userConfig, 0x242, primitive) && (primitive & 0x3fu) != 1 && (primitive & 0x3fu) != 2 && (primitive & 0x3fu) != 3 && (primitive & 0x3fu) != 4 && (primitive & 0x3fu) != 5 && (primitive & 0x3fu) != 6) return "AGC graphics: primitive restart is only supported for point, line and triangle topologies";
if (value(cx, 0x103, resetIndex) && (resetIndex & 0xffffu) != 0xffffu) return "AGC graphics: primitive restart index other than all ones is unsupported";
}
if (auto reason = nonzero(cx, 0x207, ~(LayerExports | ClipCullExports | inactiveVertexPointSize(queue)), "layer, viewport or auxiliary vertex exports"); !reason.empty()) return reason;
if (auto reason = nonzero(cx, 0x207, ~(LayerExports | ClipCullExports), "layer, viewport or auxiliary vertex exports"); !reason.empty()) return reason;
if (value(cx, 0x207, word) && std::popcount(word & 0xffffu) > 8) return require(false, "more than eight clip and cull distances are unsupported");
if (value(cx, 0x200, word)) {
const auto renderControl = find(cx, 0x000);
@ -1322,8 +1215,7 @@ std::string DrawRejection(const QueueState& queue, bool indexed) {
if (value(cx, 0x2dc, word) && word != AlphaToCoverageUndithered) {
if (auto reason = nonzero(cx, 0x2dc, AlphaToCoverageMask, "dithered or rounded alpha-to-coverage"); !reason.empty()) return reason;
}
if (auto reason = nonzero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u | CoveredCentroidIsCenter), "coverage conversion or nonstandard multisampling"); !reason.empty()) return reason;
if (!coveredCentroidSupported(queue)) return "AGC graphics: COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs is not implemented";
if (auto reason = nonzero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u), "coverage conversion or nonstandard multisampling"); !reason.empty()) return reason;
const bool multisampled = value(cx, 0x2f8, word) && (word & 7u) != 0;
if (auto reason = nonzero(cx, 0x292, ScanModeMask & ~1u, "scan conversion mode"); !reason.empty()) return reason;
if (!multisampled && value(cx, 0x292, word) && (word & 1u) != 0) {

View File

@ -11,7 +11,6 @@
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
#include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp"
#include <algorithm>
#include <array>
#include <bit>
@ -268,7 +267,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex
const auto linearBytes = sliceLinearBytes * arrayLayers;
VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
imageInfo.flags = (descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u) | (SrgbTwinFormat(context, vkFormat) != VK_FORMAT_UNDEFINED ? VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT : 0u);
imageInfo.flags = descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u;
imageInfo.imageType = ImageTypeFor(descriptor.dimension);
imageInfo.format = vkFormat;
imageInfo.extent = {descriptor.width, descriptor.height, geometry.imageDepth};
@ -447,7 +446,6 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex
viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount};
createFirstLayerView(descriptor, viewInfo);
prepareBaseLevelView(descriptor, viewInfo);
prepareSrgbView(descriptor, viewInfo);
if (profile) {
auto& totals = Profile();
totals.view += timer.lap();
@ -477,75 +475,6 @@ bool Texture::CanDepthCopyFrom(const StorageTexture& source, const GuestTextureR
return geometry.imageDepth == 1 && source.ImageDepth() == 1 && source.ImageLayers() >= geometry.imageLayers && source.Descriptor().mipCount >= descriptor.mipCount;
}
bool Texture::CanBlockCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor) {
const auto& from = source.Descriptor();
if (!IsBlockCompressed(descriptor.format) || IsBlockCompressed(from.format) || BytesPerElement(from.format) != BytesPerElement(descriptor.format)) return false;
if (descriptor.dimension != TextureDimension::k2D && descriptor.dimension != TextureDimension::k2DArray) return false;
if (descriptor.baseAddress != from.baseAddress || descriptor.pipeBankXor != from.pipeBankXor || descriptor.dimension != from.dimension || descriptor.tileMode != from.tileMode || descriptor.depthOrLastArray != from.depthOrLastArray || descriptor.mipCount > from.mipCount) return false;
for (std::uint32_t level = 0; level < descriptor.mipCount; ++level) {
const auto width = std::max(descriptor.width >> level, 1u);
const auto height = std::max(descriptor.height >> level, 1u);
if (width % 4u != 0 || height % 4u != 0 || std::max(from.width >> level, 1u) != width / 4u || std::max(from.height >> level, 1u) != height / 4u) return false;
}
const auto geometry = DescribeSurface(descriptor);
return geometry.imageDepth == 1 && source.ImageDepth() == 1 && source.ImageLayers() >= geometry.imageLayers;
}
Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>& source, const GuestTextureResource& descriptor, VkComponentMapping components, BlockCopyTag) : context(context), storageSource(source), perUseCopy(PerUseCopy::RefreshedOnReuse) {
try {
Require(source != nullptr && CanBlockCopyFrom(*source, descriptor), "storage image cannot serve a block copy of the sampled texture");
blockFormat = SampledTextureFormat(context, descriptor.format);
const auto geometry = DescribeSurface(descriptor);
copyWidth = descriptor.width;
copyHeight = descriptor.height;
copyLevels = descriptor.mipCount;
copyLayers = geometry.imageLayers;
copyElementBytes = BytesPerElement(descriptor.format);
APS5_LOG_OUT("Texture address=0x%llx %ux%u mips=%u layers=%u block copy of storage image (guest format %u -> vk %d)", static_cast<unsigned long long>(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, copyLayers, source->Descriptor().format, static_cast<int>(blockFormat));
VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
imageInfo.imageType = VK_IMAGE_TYPE_2D;
imageInfo.format = blockFormat;
imageInfo.extent = {descriptor.width, descriptor.height, 1u};
imageInfo.mipLevels = copyLevels;
imageInfo.arrayLayers = copyLayers;
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
Check(context.Function<PFN_vkCreateImage>("vkCreateImage")(context.device, &imageInfo, nullptr, &image), "vkCreateImage block copy");
owned = std::make_shared<OwnedImage>(context, image, VK_NULL_HANDLE);
VkMemoryRequirements requirements{};
context.Function<PFN_vkGetImageMemoryRequirements>("vkGetImageMemoryRequirements")(context.device, image, &requirements);
VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
allocation.allocationSize = requirements.size;
allocationBytes = requirements.size;
allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
Check(context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &owned->memory), "vkAllocateMemory block copy");
Check(context.Function<PFN_vkBindImageMemory>("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory block copy");
const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u;
const auto viewLayerCount = geometry.imageLayers - descriptor.baseArray;
VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
viewInfo.image = image;
viewInfo.viewType = ViewTypeFor(descriptor.dimension, viewLayerCount);
viewInfo.format = blockFormat;
viewFormat = blockFormat;
viewInfo.components = components;
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, descriptor.baseLevel, viewLevelCount, descriptor.baseArray, viewLayerCount};
VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT};
ChainMinLod(context, descriptor, viewInfo, minLod);
Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView block copy");
viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount};
createFirstLayerView(descriptor, viewInfo);
prepareBaseLevelView(descriptor, viewInfo);
} catch (...) {
release();
throw;
}
}
Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>& source, const GuestTextureResource& descriptor, VkComponentMapping components, DepthCopyTag) : context(context), storageSource(source), perUseCopy(PerUseCopy::RefreshedOnReuse) {
try {
Require(source != nullptr && CanDepthCopyFrom(*source, descriptor), "storage image cannot serve a depth copy of the sampled texture");
@ -608,7 +537,6 @@ Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>&
bool Texture::RefreshDepthCopy() {
if (perUseCopy != PerUseCopy::RefreshedOnReuse || storageSource == nullptr) return false;
if (copiedOnce && storageSource->Version() == copiedVersion) return false;
if (blockFormat != VK_FORMAT_UNDEFINED) return refreshBlockCopy();
std::uint64_t linearBytes = 0;
for (std::uint32_t level = 0; level < copyLevels; ++level) linearBytes += static_cast<std::uint64_t>(std::max(copyWidth >> level, 1u)) * std::max(copyHeight >> level, 1u) * copyElementBytes * copyLayers;
auto linear = std::make_shared<DeviceBuffer>(context, static_cast<std::size_t>(linearBytes), VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT);
@ -680,51 +608,6 @@ bool Texture::RefreshDepthCopy() {
return true;
}
bool Texture::refreshBlockCopy() {
std::vector<VkImageCopy> regions;
for (std::uint32_t level = 0; level < copyLevels; ++level) {
VkImageCopy region{};
region.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, 0, copyLayers};
region.dstSubresource = region.srcSubresource;
region.extent = {std::max(copyWidth >> level, 1u) / 4u, std::max(copyHeight >> level, 1u) / 4u, 1u};
regions.push_back(region);
}
auto* recorder = Recorder::Active();
std::unique_ptr<CommandBatch> batch;
VkCommandBuffer commands = VK_NULL_HANDLE;
if (recorder != nullptr) {
commands = recorder->Commands();
recorder->Keep(owned);
recorder->Keep(storageSource);
} else {
batch = std::make_unique<CommandBatch>(context);
commands = batch->Handle();
}
const auto barrier = context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier");
const VkMemoryBarrier produced{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT};
VkImageMemoryBarrier toTransferDst{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
toTransferDst.srcAccessMask = VK_ACCESS_SHADER_READ_BIT;
toTransferDst.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
toTransferDst.oldLayout = copiedOnce ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_UNDEFINED;
toTransferDst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
toTransferDst.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toTransferDst.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toTransferDst.image = image;
toTransferDst.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, copyLevels, 0, copyLayers};
barrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &produced, 0, nullptr, 1, &toTransferDst);
context.Function<PFN_vkCmdCopyImage>("vkCmdCopyImage")(commands, storageSource->Image(), VK_IMAGE_LAYOUT_GENERAL, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast<std::uint32_t>(regions.size()), regions.data());
VkImageMemoryBarrier toShaderRead = toTransferDst;
toShaderRead.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
toShaderRead.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
toShaderRead.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
toShaderRead.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &toShaderRead);
if (batch) batch->SubmitAndWait();
copiedOnce = true;
copiedVersion = storageSource->Version();
return true;
}
Texture::DepthCopyCounts Texture::TakeDepthCopyCounts() {
return {depthCopyTextures.exchange(0, std::memory_order_relaxed), depthCopyRefreshes.exchange(0, std::memory_order_relaxed)};
}
@ -765,7 +648,6 @@ Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>&
viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount};
createFirstLayerView(descriptor, viewInfo);
prepareBaseLevelView(descriptor, viewInfo);
prepareSrgbView(descriptor, viewInfo);
if (profile) {
auto& totals = Profile();
totals.view += timer.lap();
@ -818,44 +700,6 @@ void Texture::prepareBaseLevelView(const GuestTextureResource& descriptor, VkIma
baseLevelInfo = viewInfo;
}
void Texture::prepareSrgbView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo) {
const auto format = SrgbTwinFormat(context, viewInfo.format);
if (format == VK_FORMAT_UNDEFINED) return;
viewInfo.pNext = nullptr;
viewInfo.format = format;
srgbInfo = viewInfo;
srgbMinLod = EffectiveMinLod(descriptor);
}
VkImageView Texture::SrgbView(bool firstLayer, bool baseLevel) const {
Require(srgbInfo.has_value(), "AGC graphics: a texture of view format " + std::to_string(viewFormat) + " has no sRGB view for a sampler that forces sRGB decoding");
std::lock_guard lock(srgbMutex);
auto& slot = srgbViews[baseLevel ? 2u : firstLayer ? 1u : 0u];
if (slot != VK_NULL_HANDLE) return slot;
VkImageViewCreateInfo info = *srgbInfo;
VkImageViewUsageCreateInfo usage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO};
usage.usage = VK_IMAGE_USAGE_SAMPLED_BIT;
info.pNext = &usage;
VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT};
if (baseLevel) {
info.viewType = info.viewType == VK_IMAGE_VIEW_TYPE_1D || info.viewType == VK_IMAGE_VIEW_TYPE_1D_ARRAY ? VK_IMAGE_VIEW_TYPE_1D : VK_IMAGE_VIEW_TYPE_2D;
info.subresourceRange.levelCount = 1;
info.subresourceRange.layerCount = 1;
} else {
if (firstLayer) {
info.viewType = VK_IMAGE_VIEW_TYPE_2D;
info.subresourceRange.layerCount = 1;
}
if (srgbMinLod != 0.0f) {
minLod.minLod = srgbMinLod;
minLod.pNext = info.pNext;
info.pNext = &minLod;
}
}
Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &info, nullptr, &slot), "vkCreateImageView sRGB");
return slot;
}
VkImageView Texture::BaseLevelView() const {
Require(baseLevelInfo.has_value(), "a base-level view needs a mipmapped 1D, 2D or 2D array texture");
std::lock_guard lock(baseLevelMutex);
@ -871,10 +715,6 @@ void Texture::release() noexcept {
firstLayerView = VK_NULL_HANDLE;
if (baseLevelView) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, baseLevelView, nullptr);
baseLevelView = VK_NULL_HANDLE;
for (auto& srgbView : srgbViews) {
if (srgbView) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, srgbView, nullptr);
srgbView = VK_NULL_HANDLE;
}
// The image and its memory go with the last holder: this texture, or the batch still uploading it.
owned.reset();
image = VK_NULL_HANDLE;
@ -977,7 +817,7 @@ bool IntegerFormat(VkFormat format) {
switch (format) {
case VK_FORMAT_R8_UINT: case VK_FORMAT_R8_SINT: case VK_FORMAT_R8G8_UINT: case VK_FORMAT_R8G8_SINT:
case VK_FORMAT_R8G8B8A8_UINT: case VK_FORMAT_R8G8B8A8_SINT: case VK_FORMAT_B8G8R8A8_UINT: case VK_FORMAT_B8G8R8A8_SINT:
case VK_FORMAT_A8B8G8R8_UINT_PACK32: case VK_FORMAT_A8B8G8R8_SINT_PACK32: case VK_FORMAT_A2R10G10B10_UINT_PACK32: case VK_FORMAT_A2B10G10R10_UINT_PACK32: case VK_FORMAT_A2B10G10R10_SINT_PACK32:
case VK_FORMAT_A8B8G8R8_UINT_PACK32: case VK_FORMAT_A8B8G8R8_SINT_PACK32: case VK_FORMAT_A2R10G10B10_UINT_PACK32: case VK_FORMAT_A2B10G10R10_UINT_PACK32:
case VK_FORMAT_R16_UINT: case VK_FORMAT_R16_SINT: case VK_FORMAT_R16G16_UINT: case VK_FORMAT_R16G16_SINT:
case VK_FORMAT_R16G16B16A16_UINT: case VK_FORMAT_R16G16B16A16_SINT: case VK_FORMAT_R32_UINT: case VK_FORMAT_R32_SINT:
case VK_FORMAT_R32G32_UINT: case VK_FORMAT_R32G32_SINT: case VK_FORMAT_R32G32B32_UINT: case VK_FORMAT_R32G32B32_SINT:
@ -1011,23 +851,6 @@ bool StorageClearAvailable(const Context& context, std::uint32_t guestFormat, Dc
return IsDccClear(keys) && StorageFormatAvailable(context, guestFormat) && ClearColorFor(StorageFormatForGuest(context, guestFormat), keys, clear);
}
VkFormat SrgbTwinFormat(const Context& context, VkFormat format) {
const auto decoded = [&](ShaderRecompiler::IrBufferFormat srgb) { return (context.srgbDecodeFormats & ShaderRecompiler::SrgbDecodeBit(srgb)) != 0u; };
switch (format) {
case VK_FORMAT_R8_UNORM: return decoded(ShaderRecompiler::IrBufferFormat::Format8Srgb) ? VK_FORMAT_UNDEFINED : VK_FORMAT_R8_SRGB;
case VK_FORMAT_R8G8_UNORM: return decoded(ShaderRecompiler::IrBufferFormat::Format8_8Srgb) ? VK_FORMAT_UNDEFINED : VK_FORMAT_R8G8_SRGB;
case VK_FORMAT_R8G8B8A8_UNORM: return VK_FORMAT_R8G8B8A8_SRGB;
case VK_FORMAT_B8G8R8A8_UNORM: return VK_FORMAT_B8G8R8A8_SRGB;
case VK_FORMAT_A8B8G8R8_UNORM_PACK32: return VK_FORMAT_A8B8G8R8_SRGB_PACK32;
case VK_FORMAT_BC1_RGB_UNORM_BLOCK: return VK_FORMAT_BC1_RGB_SRGB_BLOCK;
case VK_FORMAT_BC1_RGBA_UNORM_BLOCK: return VK_FORMAT_BC1_RGBA_SRGB_BLOCK;
case VK_FORMAT_BC2_UNORM_BLOCK: return VK_FORMAT_BC2_SRGB_BLOCK;
case VK_FORMAT_BC3_UNORM_BLOCK: return VK_FORMAT_BC3_SRGB_BLOCK;
case VK_FORMAT_BC7_UNORM_BLOCK: return VK_FORMAT_BC7_SRGB_BLOCK;
default: return VK_FORMAT_UNDEFINED;
}
}
VkFormat AttachmentProxyFormat(const Context& context, VkFormat format) {
if (format != VK_FORMAT_R8_SRGB) return VK_FORMAT_UNDEFINED;
struct Table {
@ -1096,7 +919,6 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler,
trackedLayerBytes = blockUnits ? 65536 : guestBytes / trackedLayers;
layerGeneration.assign(trackedLayers, 0);
layerPending.assign(trackedLayers, false);
layerOriginal.assign(trackedLayers, true);
sliceLinearBytes = geometry.sliceLinearBytes;
const auto linearBytes = sliceLinearBytes * arrayLayers;
@ -1511,26 +1333,26 @@ VkImageView StorageTexture::elementLayerView(std::uint32_t level, std::uint32_t
return created;
}
VkImageView StorageTexture::AttachmentView(VkFormat format, std::uint32_t mip, std::uint32_t depthSlice, std::uint32_t layers, std::uint32_t layer) {
VkImageView StorageTexture::AttachmentView(VkFormat format, std::uint32_t mip, std::uint32_t depthSlice, std::uint32_t layer) {
Require(attachable, "storage image cannot be a color attachment");
Require(mip < descriptor.mipCount, "attachment mip exceeds the storage image");
const bool volume = descriptor.dimension == TextureDimension::k3D;
Require((depthSlice == 0 && layers == 1) || (volume && mip == 0 && layers != 0 && depthSlice + layers - 1u <= descriptor.depthOrLastArray), "attachment slice is outside the storage image");
Require(depthSlice == 0 || (volume && mip == 0 && depthSlice <= descriptor.depthOrLastArray), "attachment slice is outside the storage image");
Require(layer == 0 || (!volume && descriptor.dimension == TextureDimension::k2DArray && descriptor.baseArray + layer <= descriptor.depthOrLastArray), "attachment layer is outside the storage image");
const auto found = attachmentViews.find({format, mip, depthSlice, layers, layer});
const auto found = attachmentViews.find({format, mip, depthSlice, layer});
if (found != attachmentViews.end()) return found->second;
VkImageViewUsageCreateInfo usage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO};
usage.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
viewInfo.pNext = &usage;
viewInfo.image = image;
viewInfo.viewType = layers > 1 ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D;
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = format;
viewInfo.components = {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY};
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, mip, 1u, volume ? depthSlice : descriptor.baseArray + layer, layers};
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, mip, 1u, volume ? depthSlice : descriptor.baseArray + layer, 1u};
VkImageView created = VK_NULL_HANDLE;
Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &created), "vkCreateImageView attachment");
attachmentViews.emplace(std::tuple{format, mip, depthSlice, layers, layer}, created);
attachmentViews.emplace(std::tuple{format, mip, depthSlice, layer}, created);
return created;
}
@ -1708,8 +1530,6 @@ LookupOutcomes& ThreadLookupOutcomes() {
bool StorageTexture::Refresh() {
const bool profile = LookupOutcomes::Profiled();
auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
CompletePendingCpuWrites(context, descriptor.baseAddress, static_cast<std::size_t>(guestBytes));
if (descriptor.dccAddress != 0) CompletePendingCpuWrites(context, descriptor.dccAddress, DccKeyBytes(guestBytes));
if (refreshProved()) {
NoteProved();
refreshesProved.fetch_add(1, std::memory_order_relaxed);
@ -1835,7 +1655,7 @@ bool StorageTexture::Refresh() {
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
if (changed[layer] && layerPending[layer]) pendingChanged = true;
}
if (!unchanged && !keysChanged && !pendingChanged && originalHolds(descriptor.baseAddress, static_cast<std::size_t>(guestBytes)) && equalsOriginal()) {
if (!unchanged && !keysChanged && !pendingChanged && originalValid && equalsOriginal()) {
unchanged = true;
stamped = false;
}
@ -1878,7 +1698,7 @@ bool StorageTexture::Refresh() {
static const bool traceUpload = std::getenv("APS5_TRACE_UPLOAD") != nullptr;
if (traceUpload) {
const auto keys = TextureClearKeys(descriptor, guestBytes);
std::fprintf(stderr, "[upload] 0x%llx+0x%llx %ux%u mips %u: %s (keys %s -> %s, originalValid %d, dirty %d)\n", static_cast<unsigned long long>(descriptor.baseAddress), static_cast<unsigned long long>(guestBytes), descriptor.width, descriptor.height, descriptor.mipCount, keys != uploadedKeys ? "DCC keys changed" : "guest memory changed", DccKeysName(uploadedKeys), DccKeysName(keys), originalHolds(descriptor.baseAddress, static_cast<std::size_t>(guestBytes)) ? 1 : 0, dirty ? 1 : 0);
std::fprintf(stderr, "[upload] 0x%llx+0x%llx %ux%u mips %u: %s (keys %s -> %s, originalValid %d, dirty %d)\n", static_cast<unsigned long long>(descriptor.baseAddress), static_cast<unsigned long long>(guestBytes), descriptor.width, descriptor.height, descriptor.mipCount, keys != uploadedKeys ? "DCC keys changed" : "guest memory changed", DccKeysName(uploadedKeys), DccKeysName(keys), originalValid ? 1 : 0, dirty ? 1 : 0);
}
// Changed layers with results pending: the CPU wrote this memory while GPU results were
// pending, so as with an immediate write-back followed by the CPU write, its blocks win and the
@ -2063,8 +1883,8 @@ void traceKeyStore(const char* path, const GuestTextureResource& descriptor, std
}
void StorageTexture::captureGuestBytes(const std::vector<bool>* layers) {
original.resize(static_cast<std::size_t>(guestBytes));
comparedGuestBytes = {};
const bool complete = layers == nullptr || std::all_of(layers->begin(), layers->end(), [](bool selected) { return selected; });
std::vector<bool> selected(trackedLayers, true);
if (layers != nullptr) selected = *layers;
for (const auto& [begin, end] : unitRuns(selected)) GuestMemory::FlushGpuWrites(descriptor.baseAddress + begin, static_cast<std::size_t>(end - begin));
@ -2072,19 +1892,12 @@ void StorageTexture::captureGuestBytes(const std::vector<bool>* layers) {
if (!selected[layer]) continue;
const auto offset = static_cast<std::size_t>(layerBegin(layer) - descriptor.baseAddress);
GuestMemory::ReadCommittedUnsynced(layerBegin(layer), std::span(original).subspan(offset, static_cast<std::size_t>(layerBytes(layer))));
layerOriginal[layer] = true;
}
}
bool StorageTexture::originalHolds(std::uint64_t address, std::size_t bytes) const {
for (auto layer = (address - descriptor.baseAddress) / trackedLayerBytes; layer < trackedLayers && layer * trackedLayerBytes < address - descriptor.baseAddress + bytes; ++layer) {
if (!layerOriginal[static_cast<std::size_t>(layer)]) return false;
}
return true;
originalValid = originalValid || complete;
}
bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes, std::span<std::uint8_t> changed, bool memoize) const {
if (!originalHolds(address, bytes)) return false;
if (!originalValid) return false;
struct Exempt {
const StorageTexture* previous;
~Exempt() { refreshing = previous; }
@ -2093,7 +1906,7 @@ bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes,
Require(address >= descriptor.baseAddress && bytes <= guestBytes && address - descriptor.baseAddress <= guestBytes - bytes, "texture comparison exceeds its snapshot");
Require(original.size() == guestBytes, "texture comparison has no complete snapshot");
GuestMemory::FlushGpuWrites(address, bytes);
if (!originalHolds(address, bytes)) return false;
if (!originalValid) return false;
constexpr std::uint64_t blockBytes = 65536;
const auto end = address + bytes;
const auto first = address / blockBytes;
@ -2124,13 +1937,12 @@ bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes,
constexpr std::uint64_t GenerationBaselineBytes = 1ull << 20;
void StorageTexture::upload(const std::vector<bool>* layers) {
CaptureTrace::Log("upload image=%llx bytes=%llu generation=%llu reason=%s partial=%d", static_cast<unsigned long long>(descriptor.baseAddress), static_cast<unsigned long long>(guestBytes), static_cast<unsigned long long>(generation), uploadReason, layers != nullptr);
const bool profile = LookupOutcomes::Profiled();
const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
CompletePendingCpuWrites(context, descriptor.baseAddress, static_cast<std::size_t>(guestBytes));
if (descriptor.dccAddress != 0) CompletePendingCpuWrites(context, descriptor.dccAddress, DccKeyBytes(guestBytes));
CaptureTrace::Log("upload image=%llx bytes=%llu generation=%llu reason=%s partial=%d", static_cast<unsigned long long>(descriptor.baseAddress), static_cast<unsigned long long>(guestBytes), static_cast<unsigned long long>(generation), uploadReason, layers != nullptr);
const auto elementBytes = BytesPerElement(descriptor.format);
const auto linearBytes = sliceLinearBytes * arrayLayers;
original.resize(static_cast<std::size_t>(guestBytes));
// A layer selection applies to the direct path alone (the others upload everything); it names
// array layers, which the tracked layers are when there are several.
if (layers != nullptr && ((!blockUnits && trackedLayers != arrayLayers) || std::all_of(layers->begin(), layers->end(), [](bool selected) { return selected; }))) layers = nullptr;
@ -2160,7 +1972,7 @@ void StorageTexture::upload(const std::vector<bool>* layers) {
// batch of its own submitted the recorder's work first and waited for all of it, 25-40 ms
// under the GPU mutex at the movie stage. APS5_NO_RECORDED_CLEAR=1 waits as before.
static_cast<void>(HostImportFor(context, descriptor.baseAddress, static_cast<std::size_t>(guestBytes)));
if (GuestMemory::Watched(descriptor.baseAddress, static_cast<std::size_t>(guestBytes))) layerOriginal.assign(trackedLayers, false);
if (GuestMemory::Watched(descriptor.baseAddress, static_cast<std::size_t>(guestBytes))) originalValid = false;
else captureGuestBytes(nullptr);
forgetBorrowed(0, trackedLayers);
stampLayers(false);
@ -2209,7 +2021,7 @@ void StorageTexture::upload(const std::vector<bool>* layers) {
return;
}
if (const auto* import = uploadedKeys == DccKeys::Uncompressed ? HostImportFor(context, descriptor.baseAddress, static_cast<std::size_t>(guestBytes)) : nullptr) {
if (GuestMemory::Watched(descriptor.baseAddress, static_cast<std::size_t>(guestBytes))) layerOriginal.assign(trackedLayers, false);
if (GuestMemory::Watched(descriptor.baseAddress, static_cast<std::size_t>(guestBytes))) originalValid = false;
else captureGuestBytes(layers);
stampLayers(true);
// A whole-surface upload of a block-unit image goes through the windows too when a unit
@ -2331,10 +2143,9 @@ void StorageTexture::upload(const std::vector<bool>* layers) {
if (profile) LookupOutcomes::Add(LookupOutcomes::UploadDirect, start);
return;
}
layerOriginal.assign(trackedLayers, true);
originalValid = true;
forgetBorrowed(0, trackedLayers);
stampLayers(false);
original.resize(static_cast<std::size_t>(guestBytes));
GuestMemory::ReadCommitted(descriptor.baseAddress, original);
{
Buffer staging(context, original.size(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
@ -3412,10 +3223,9 @@ bool StorageTexture::evictStale(std::uint64_t address, std::size_t bytes, std::s
// As the empty write-back leaves the layers: no results pending, the generation unchanged (the
// stamps make their next Refresh re-upload), `original` no longer vouching for the bytes.
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
if (!selected(layer)) continue;
layerPending[layer] = false;
layerOriginal[layer] = false;
if (selected(layer)) layerPending[layer] = false;
}
originalValid = false;
if (!anyLayerPending()) {
++dropped;
staleEvicted.fetch_add(1, std::memory_order_relaxed);
@ -3475,7 +3285,7 @@ std::size_t StorageTexture::DiscardPendingInside(std::uint64_t address, std::siz
// touched the bytes a CPU-path upload copied).
texture->dirty = false;
texture->layerPending.assign(texture->trackedLayers, false);
texture->layerOriginal.assign(texture->trackedLayers, false);
texture->originalValid = false;
it = pending.textures.erase(it);
++discarded;
} else if (texture != exempt && texture->blockUnits && texture->overlaps(address, bytes)) {
@ -3487,11 +3297,13 @@ std::size_t StorageTexture::DiscardPendingInside(std::uint64_t address, std::siz
const auto unitBegin = texture->layerBegin(unit);
if (!texture->layerPending[unit] || unitBegin < address || unitBegin + texture->layerBytes(unit) > end) continue;
texture->layerPending[unit] = false;
texture->layerOriginal[unit] = false;
unitsDropped.fetch_add(1, std::memory_order_relaxed);
dropped = true;
}
if (dropped) ++discarded;
if (dropped) {
texture->originalValid = false;
++discarded;
}
if (dropped && !texture->anyLayerPending()) {
texture->dirty = false;
it = pending.textures.erase(it);
@ -3689,7 +3501,7 @@ bool StorageTexture::clearByKeysFill(DccKeys keys, std::uint8_t key) {
toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL;
context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral);
recorder->EndGpuTiming(timing, guestBytes);
layerOriginal.assign(trackedLayers, false);
originalValid = false;
uploadedKeys = keys;
filledKeys = DccKeys::Uncompressed;
keyProof = {};
@ -3782,7 +3594,7 @@ bool StorageTexture::FillClear(std::span<const std::uint32_t, 4> pattern, std::u
std::lock_guard lock(live.mutex);
for (auto* texture : live.textures) {
if (texture == this || texture->released || !texture->overlaps(begin, bytes)) continue;
texture->layerOriginal.assign(texture->trackedLayers, false);
texture->originalValid = false;
if (texture->descriptor.dccAddress != 0) aliasedKeys = true;
}
}
@ -3831,19 +3643,13 @@ bool StorageTexture::FillClear(std::span<const std::uint32_t, 4> pattern, std::u
// memo hit of this submission's epoch would not consume pages the game dirtied since that
// walk, the write-back's own walk would stamp them above this generation and keep their
// blocks, and the fill's pattern would be lost in them.
const bool watched = GuestMemory::Watched(descriptor.baseAddress, static_cast<std::size_t>(guestBytes));
if (watched) layerOriginal.assign(trackedLayers, false);
originalValid = false;
uploadedKeys = DccKeys::Uncompressed;
if (layer != WholeImage && trackedLayers > 1) {
// The cleared layer's units alone: their stamps above lie below this generation, the other
// units keep theirs (their pages are consumed by their own collects).
const auto first = blockUnits ? static_cast<std::uint32_t>((begin - descriptor.baseAddress) / trackedLayerBytes) : layer;
const auto count = blockUnits ? static_cast<std::uint32_t>(bytes / trackedLayerBytes) : 1u;
if (!watched) {
std::vector<bool> filled(trackedLayers, false);
std::fill_n(filled.begin() + first, count, true);
captureGuestBytes(&filled);
}
const auto now = GuestMemory::CollectWritesUncached(begin, bytes);
for (auto unit = first; unit < first + count; ++unit) layerGeneration[unit] = now;
refreshGeneration();
@ -3851,7 +3657,6 @@ bool StorageTexture::FillClear(std::span<const std::uint32_t, 4> pattern, std::u
markLayersPending(first, count);
return true;
}
if (!watched) captureGuestBytes(nullptr);
layerGeneration.assign(trackedLayers, GuestMemory::CollectWritesUncached(descriptor.baseAddress, static_cast<std::size_t>(guestBytes)));
refreshGeneration();
forgetBorrowed(0, trackedLayers);
@ -3916,7 +3721,7 @@ bool StorageTexture::CopyFrom(StorageTexture& source, const char*& refusal) {
auto& live = Live();
std::lock_guard lock(live.mutex);
for (auto* texture : live.textures) {
if (texture != this && !texture->released && texture->overlaps(descriptor.baseAddress, static_cast<std::size_t>(guestBytes))) texture->layerOriginal.assign(texture->trackedLayers, false);
if (texture != this && !texture->released && texture->overlaps(descriptor.baseAddress, static_cast<std::size_t>(guestBytes))) texture->originalValid = false;
}
}
GuestMemory::MarkWritten(descriptor.baseAddress, static_cast<std::size_t>(guestBytes));
@ -3967,8 +3772,7 @@ bool StorageTexture::CopyFrom(StorageTexture& source, const char*& refusal) {
// As after a clear: the image holds the surface, guest memory follows at the write-back (which
// keeps the 64 KiB blocks the CPU writes from now on; the collect is uncached for the reason
// given in FillClear).
if (GuestMemory::Watched(descriptor.baseAddress, static_cast<std::size_t>(guestBytes))) layerOriginal.assign(trackedLayers, false);
else captureGuestBytes(nullptr);
originalValid = false;
uploadedKeys = DccKeys::Uncompressed;
layerGeneration.assign(trackedLayers, GuestMemory::CollectWritesUncached(descriptor.baseAddress, static_cast<std::size_t>(guestBytes)));
refreshGeneration();
@ -4195,7 +3999,7 @@ bool StorageTexture::guestBytesSettled() const {
}
bool StorageTexture::unchangedSinceBaseline(std::uint64_t from, std::uint64_t to) const {
const auto& baseline = originalHolds(from, static_cast<std::size_t>(to - from)) ? original : generationBaseline;
const auto& baseline = originalValid ? original : generationBaseline;
if (baseline.size() != guestBytes) return false;
const auto offset = static_cast<std::size_t>(from - descriptor.baseAddress);
return GuestMemory::EqualsCommittedUnsynced(from, std::span<const std::byte>(baseline).subspan(offset, static_cast<std::size_t>(to - from)));
@ -4248,29 +4052,12 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
}
std::vector<std::uint8_t> changedBlocks(spanBlocks);
const bool tracked = GuestMemory::ChangedBlocks(firstStored, static_cast<std::size_t>(lastStored - firstStored), generations, changedBlocks);
std::vector<bool> compared(trackedLayers, false);
if (!tracked) {
std::vector<bool> saved(trackedLayers, false);
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) saved[layer] = layers[layer] && layerOriginal[layer];
for (const auto& [begin, end] : unitRuns(saved)) {
const auto from = descriptor.baseAddress + begin;
const auto last = descriptor.baseAddress + end - 1;
if (!compareUntracked(from, static_cast<std::size_t>(end - begin), std::span(changedBlocks).subspan(static_cast<std::size_t>((from - spanBegin) / block), static_cast<std::size_t>(last / block - from / block + 1)))) continue;
for (auto layer = static_cast<std::uint32_t>(begin / trackedLayerBytes); layer < trackedLayers && layerBegin(layer) <= last; ++layer) compared[layer] = true;
}
}
const auto forgetStored = [&] {
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
if (layers[layer]) layerOriginal[layer] = false;
}
};
bool unsavedOutsideWatch = false;
const bool compared = !tracked && compareUntracked(firstStored, static_cast<std::size_t>(lastStored - firstStored), changedBlocks);
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
if (!layers[layer]) continue;
const auto begin = layerBegin(layer);
const auto end = begin + layerBytes(layer);
if (tracked ? layerGeneration[layer] == 0 : !compared[layer]) {
unsavedOutsideWatch = unsavedOutsideWatch || !tracked;
if (!compared && (!tracked || layerGeneration[layer] == 0)) {
keep.emplace_back(begin, end);
continue;
}
@ -4279,7 +4066,7 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
const auto to = std::min(at + block, end);
if (from >= to) continue;
const bool edge = from != at || to != at + block;
if (changedBlocks[static_cast<std::size_t>((at - spanBegin) / block)] == GuestMemory::BlockWritten && (compared[layer] || !edge || GuestMemory::StoredOver(from, static_cast<std::size_t>(to - from), layerGeneration[layer])) && !unchangedSinceBaseline(from, to)) {
if (changedBlocks[static_cast<std::size_t>((at - spanBegin) / block)] == GuestMemory::BlockWritten && (compared || !edge || GuestMemory::StoredOver(from, static_cast<std::size_t>(to - from), layerGeneration[layer])) && !unchangedSinceBaseline(from, to)) {
skippedAny = true;
skippedLayer[layer] = true;
++skipped;
@ -4331,19 +4118,18 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
// store, the keys stay as they are, and the image is stale there (re-uploaded at its next
// use, never from a matching `original`).
emptyWriteBacks.fetch_add(1, std::memory_order_relaxed);
forgetStored();
originalValid = false;
settle(true);
return;
}
if (const auto* import = HostImportFor(context, descriptor.baseAddress, static_cast<std::size_t>(guestBytes))) {
Require(!unsavedOutsideWatch, "storing storage image results pending over memory outside the write watch without the guest bytes saved before them is not implemented");
if (blockUnits) {
// The kept blocks alone pass through the retiler (windows of their slices).
shadowImport = import;
const auto storedBytes = writeBackWindows(*import, keep, firstStored, lastStored, shadowed, imported);
countStorageWriteBack(storedBytes, true);
if (profile) Profile().storageGpu += timer.lap();
forgetStored();
originalValid = false;
traceKeyStore("block write-back", descriptor, guestBytes);
if (!IsDccClear(filledKeys) && uploadedKeys != DccKeys::Uncompressed) MarkDccUncompressed(context, descriptor.dccAddress, guestBytes, DccKeyCount(descriptor, guestBytes));
uploadedKeys = DccKeys::Uncompressed;
@ -4448,7 +4234,7 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
countStorageWriteBack(storedBytes, true);
if (profile) Profile().storageGpu += timer.lap();
// The guest bytes now differ from `original`; other caches of the range see the write.
forgetStored();
originalValid = false;
// The keys go the same way as the texels: a fill recorded behind the retile when the
// metadata is host-imported (no CPU wait for the title's key-writing kernels), else a CPU
// store (APS5_CPU_DCC_KEYS=1 keeps the CPU store; see DccMetadata.hpp).
@ -4472,21 +4258,18 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
static std::atomic<int> reports{0};
if (reports.fetch_add(1) < 4) std::fprintf(stderr, "[gpu] storage image 0x%llx+0x%llx: memory no longer registered; %zu pending ranges dropped\n", static_cast<unsigned long long>(descriptor.baseAddress), static_cast<unsigned long long>(guestBytes), keep.size());
unregisteredDropped.fetch_add(1, std::memory_order_relaxed);
forgetStored();
originalValid = false;
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
if (layers[layer]) layerPending[layer] = false;
}
return;
}
original.resize(static_cast<std::size_t>(guestBytes));
// The store below compares against the guest bytes, read now when `original` does not hold
// them (a GPU clear, a dropped or kept unit). Reading them says nothing about the image: a
// stale unit (dropped, or kept for the CPU) still differs from them, so `original` vouches for
// the content again only where it did before, or once every unit is stored.
Require(!unsavedOutsideWatch, "storing storage image results pending over memory outside the write watch without the guest bytes saved before them is not implemented");
std::vector<bool> unsaved = layerOriginal;
unsaved.flip();
for (const auto& [begin, end] : unitRuns(unsaved)) GuestMemory::ReadCommitted(descriptor.baseAddress + begin, std::span(original).subspan(static_cast<std::size_t>(begin), static_cast<std::size_t>(end - begin)));
const bool wasValid = originalValid;
if (!wasValid) GuestMemory::ReadCommitted(descriptor.baseAddress, original);
DeviceBuffer linear(context, static_cast<std::size_t>(sliceLinearBytes * arrayLayers), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT);
DeviceBuffer tiled(context, original.size(), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT);
Buffer host(context, original.size(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT);
@ -4570,9 +4353,7 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
uploadedKeys = DccKeys::Uncompressed;
// The store above is the only write to these pages, so `original` is current at a fresh
// generation, unless blocks were kept for the CPU: then the image is stale there.
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
if (layers[layer]) layerOriginal[layer] = !skippedLayer[layer];
}
originalValid = !skippedAny && (wasValid || std::all_of(layers.begin(), layers.end(), [](bool selected) { return selected; }));
// As in the GPU-direct path, but the store's memcpy dirtied this surface's pages in the write
// watch: a memoized collect would leave them for the next walk to stamp newer than this value
// (the shared edge block included, undoing the advance), so the walk is made here and

View File

@ -181,10 +181,7 @@ VkPipeline TextureDetiler::pipeline(TextureTileMode tileMode, std::uint32_t elem
createInfo.stage = stage;
createInfo.layout = image ? imagePipelineLayout : pipelineLayout;
VkPipeline result = VK_NULL_HANDLE;
{
auto pipelineCacheLock = context.LockPipelineCacheForUse();
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &createInfo, nullptr, &result), "vkCreateComputePipelines");
}
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &createInfo, nullptr, &result), "vkCreateComputePipelines");
pipelines.emplace_back(key, result);
return result;
}
@ -302,11 +299,7 @@ void TextureDetiler::DispatchCmaskClear(VkCommandBuffer commands, VkBuffer cmask
pipelineInfo.stage.module = cmaskModule;
pipelineInfo.stage.pName = "main";
pipelineInfo.layout = cmaskPipelineLayout;
VkResult created;
{
auto pipelineCacheLock = context.LockPipelineCacheForUse();
created = context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &cmaskPipeline);
}
const auto created = context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &cmaskPipeline);
context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, cmaskModule, nullptr);
Check(created, "vkCreateComputePipelines CMASK clear");
cmaskErrors = std::make_unique<Buffer>(context, 4, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);

View File

@ -42,9 +42,7 @@ constexpr FormatEntry kFormatLookup[] = {
{29, VK_FORMAT_R16G16_SFLOAT, 4, false},
{36, VK_FORMAT_B10G11R11_UFLOAT_PACK32, 4, false},
{50, VK_FORMAT_A2B10G10R10_UNORM_PACK32, 4, false},
{51, VK_FORMAT_A2B10G10R10_SNORM_PACK32, 4, false},
{54, VK_FORMAT_A2B10G10R10_UINT_PACK32, 4, false},
{55, VK_FORMAT_A2B10G10R10_SINT_PACK32, 4, false},
{56, VK_FORMAT_R8G8B8A8_UNORM, 4, false},
{57, VK_FORMAT_R8G8B8A8_SNORM, 4, false},
{60, VK_FORMAT_R8G8B8A8_UINT, 4, false},

View File

@ -10,25 +10,25 @@ add_custom_command(
VERBATIM
)
foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test)
foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests)
if(TARGET ${agcTarget})
target_sources(${agcTarget} PRIVATE Graphics/src/BufferPool.cpp)
endif()
endforeach()
foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test)
foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test agc_driver_graphics_tests agc_driver_mesh_tests)
if(TARGET ${agcTarget})
target_sources(${agcTarget} PRIVATE Graphics/src/TextureDetilerDescriptors.cpp Graphics/src/TextureCache.cpp)
endif()
endforeach()
foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test)
foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests)
if(TARGET ${agcTarget})
target_sources(${agcTarget} PRIVATE Graphics/src/GpuColorTransfer.cpp ${agcColorTransferHeader})
endif()
endforeach()
target_sources(agc_driver_bda_device_tests PRIVATE tests/ColorTransferTests.cpp)
target_sources(agc_driver_bda_device_tests PRIVATE tests/ColorTransferTests.cpp Graphics/src/ColorTargetLayout.cpp Graphics/src/TextureTiling.cpp Graphics/src/TextureFormat.cpp Execution/src/GuestMemory.cpp)
target_include_directories(agc_driver_bda_device_tests PRIVATE ${agcTextureDetileGeneratedRoot})
if(TARGET agc_driver_visual_test)

View File

@ -175,42 +175,6 @@ void failedImportRemapTests(const Context& context, const BdaTestAccess& access)
::operator delete(block, std::align_val_t{piece});
}
void spaceImportRunTests(const Context& context) {
constexpr std::size_t piece = 65536;
void* block = ::operator new(2 * piece, std::align_val_t{piece});
const auto first = reinterpret_cast<std::uintptr_t>(block);
std::memset(block, 0x3c, 2 * piece);
auto importing = context;
importing.hostImportAlignment = piece;
const auto change = [&](std::uint8_t* range, bool add) {
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, piece, true, false, true);
else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range);
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
};
auto* guest = static_cast<std::uint8_t*>(block);
change(guest, true);
change(guest + piece, true);
for (int build = 0; build < 2; ++build) {
const auto before = AddressSpaceCounters();
GuestBufferMemory leased(importing);
leased.AcquireRegistered();
leased.AddReadable(first + piece - 256, 512);
leased.Upload(true);
std::uint32_t adjustment = 0;
const auto view = leased.Descriptor(first + piece - 256, 512, adjustment);
Require(view.buffer != VK_NULL_HANDLE && view.range == 512 + adjustment, "a V# across one import's ranges has no descriptor");
const auto ranges = leased.AddressRanges();
Require(std::any_of(ranges.begin(), ranges.end(), [&](const auto& range) { return range.begin <= first && first + 2 * piece <= range.end; }), "one import's adjacent ranges are not one BDA table entry");
leased.WriteBack();
const auto after = AddressSpaceCounters();
if (after.enabled && build == 1) Require(after.hits == before.hits + 1 && after.dissolvedOverlap == before.dissolvedOverlap, "a V# across one import's ranges dissolved the cached address space");
}
change(guest, false);
change(guest + piece, false);
::operator delete(block, std::align_val_t{piece});
}
void importWindowTests(const Context& context) {
constexpr std::size_t piece = 65536;
constexpr std::size_t window = 2 * piece;
@ -243,237 +207,6 @@ void importWindowTests(const Context& context) {
::operator delete(block, std::align_val_t{window});
}
void importJoinTests(const Context& context) {
constexpr std::size_t piece = 65536;
constexpr std::size_t window = 2 * piece;
constexpr std::size_t large = 3 * piece;
void* block = ::operator new(3 * large + 5 * piece, std::align_val_t{window});
auto* guest = static_cast<std::uint8_t*>(block);
std::memset(guest, 0x11, 3 * large);
const auto first = reinterpret_cast<std::uintptr_t>(block);
const auto second = first + large;
const auto third = second + large;
auto importing = context;
importing.hostImportAlignment = piece;
SetHostImportWindow(window);
const auto change = [&](std::uint8_t* range, std::size_t bytes, bool add) {
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, bytes, true, false, true);
else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range);
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
};
const auto bind = [&](std::uintptr_t address, std::size_t bytes = 32) {
GuestBufferMemory plain(importing);
plain.AddReadable(address, bytes);
plain.Upload(false);
std::uint32_t adjustment = 0;
const auto view = plain.Descriptor(address, bytes, adjustment);
const auto copied = plain.CopiedBytes();
plain.WriteBack();
return std::pair{view.buffer, copied};
};
change(guest, large, true);
change(guest + large, large, true);
const auto* low = HostImportFor(importing, first, 16);
Require(low != nullptr && low->base == first && low->bytes == large, "a range larger than the import window was not imported alone");
const auto* high = HostImportFor(importing, second, 16);
Require(high != nullptr && high->base == second && high->bytes == large, "the next range larger than the import window was not imported alone");
Require(HostImportFor(importing, second - 16, 32) == nullptr, "a view across two imported ranges was imported before it was bound");
{
GuestBufferMemory leased(importing);
leased.AcquireRegistered();
leased.AddReadable(second - 16, 32);
leased.Upload(true);
const auto* joined = HostImportFor(importing, second - 16, 32);
Require(joined != nullptr && joined->base == first && joined->bytes == 2 * large, "a buffer bound across two adjacent ranges larger than the import window was not imported as one");
std::uint32_t adjustment = 0;
const auto crossing = leased.Descriptor(second - 16, 32, adjustment);
Require(crossing.buffer == joined->buffer && crossing.offset + adjustment == second - 16 - first, "a buffer bound across the joined ranges is not bound in place");
leased.WriteBack();
}
const auto joinedBuffer = HostImportFor(importing, first, 16)->buffer;
const auto [again, copied] = bind(second - 16);
Require(again == joinedBuffer && copied == 0, "a later bind across the joined ranges did not use the joined import");
auto* heap = guest + 3 * large + 2 * piece;
change(heap, piece, true);
change(heap + piece, piece, true);
Require(bind(reinterpret_cast<std::uintptr_t>(heap + piece) - 16).second != 0 && HostImportFor(importing, reinterpret_cast<std::uintptr_t>(heap + piece) - 16, 32) == nullptr, "two pieces no larger than the import window were joined");
const auto heapBase = reinterpret_cast<std::uintptr_t>(heap);
const auto [piecesBuffer, piecesCopied] = bind(heapBase, 2 * piece);
const auto* pieces = HostImportFor(importing, heapBase, 2 * piece);
Require(pieces != nullptr && pieces->base == heapBase && pieces->bytes == 2 * piece && piecesBuffer == pieces->buffer && piecesCopied == 0, "a buffer bound exactly over two pieces no larger than the import window was not imported as one");
change(heap + 2 * piece, piece, true);
static_cast<void>(bind(heapBase + 2 * piece - 16));
const auto* kept = HostImportFor(importing, first, 16);
Require(kept != nullptr && kept->buffer == joinedBuffer && kept->bytes == 2 * large, "a registry change elsewhere re-imported the joined ranges");
change(guest + 2 * large, large, true);
Require(HostImportFor(importing, first, 16)->buffer == joinedBuffer, "a range registered next to the joined ranges re-imported them");
const auto [grownBuffer, grownCopied] = bind(third - 16);
const auto* grown = HostImportFor(importing, third - 16, 32);
Require(grown != nullptr && grown->base == first && grown->bytes == 3 * large && grownBuffer == grown->buffer && grownCopied == 0, "a bind across the next range did not grow the joined import");
Require(bind(second - 16).first == grownBuffer && HostImportFor(importing, first, 16)->buffer == grownBuffer, "two joins sharing a range retired each other");
change(heap, piece, false);
change(heap + piece, piece, false);
change(heap + 2 * piece, piece, false);
change(guest + large, large, false);
const auto* alone = HostImportFor(importing, first, 16);
Require(alone != nullptr && alone->base == first && alone->bytes == large, "the joined import outlived one of its ranges");
change(guest, large, false);
change(guest + 2 * large, large, false);
Require(HostImportFor(importing, first, 16) == nullptr && HostImportFor(importing, third, 16) == nullptr, "the host imports outlived their ranges");
SetHostImportWindow(0);
::operator delete(block, std::align_val_t{window});
}
// One registered range of `cells` import windows of `window` bytes, with the window and the import budget set for the
// test and reset after it.
class WindowedRange {
public:
WindowedRange(const Context& context, std::size_t window, std::size_t cells, std::uint64_t budget = 0) : window(window), bytes(window * cells), importing(context) {
block = ::operator new(bytes, std::align_val_t{piece});
std::memset(block, 0x33, bytes);
importing.hostImportAlignment = piece;
SetHostImportRangeWindow(window);
SetHostImportBudget(budget);
change(true);
}
~WindowedRange() {
change(false);
SetHostImportBudget(0);
SetHostImportRangeWindow(std::uint64_t{64} << 20u);
::operator delete(block, std::align_val_t{piece});
}
WindowedRange(const WindowedRange&) = delete;
WindowedRange& operator=(const WindowedRange&) = delete;
std::uintptr_t First() const { return reinterpret_cast<std::uintptr_t>(block); }
static constexpr std::size_t piece = 65536;
const std::size_t window;
const std::size_t bytes;
Context importing;
private:
void change(bool add) {
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, bytes, true, false, true);
else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block);
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
}
void* block = nullptr;
};
// A registered range larger than the import window, its first window imported by an earlier access: an address-based
// build (AcquireRegistered) must import the whole range, so a view in its last window binds (Demon's Souls threw
// "guest buffer view ... exceeds its GPU owner" with the 64 MiB windows).
void importWholeRangeOverWindowTests(const Context& context) {
WindowedRange range(context, 2 * WindowedRange::piece, 2);
const auto first = range.First();
const auto* windowed = HostImportFor(range.importing, first + 16, 32);
Require(windowed != nullptr && windowed->base == first && windowed->bytes == range.window, "an access to the first window of a large range was not imported as that window");
GuestBufferMemory leased(range.importing);
leased.AcquireRegistered();
leased.Upload(true);
std::uint32_t adjustment = 0;
const auto view = leased.Descriptor(first + range.bytes - WindowedRange::piece + 16, 32, adjustment);
Require(view.range == 32 + adjustment, "a view in the last window of a large registered range has no descriptor");
const auto* whole = HostImportFor(range.importing, first, range.bytes);
Require(whole != nullptr && whole->base == first && whole->bytes == range.bytes && view.buffer == whole->buffer, "an address-based build was served by the range's first window, not an import of the whole range");
leased.WriteBack();
}
// An address-based build whose whole-range import does not fit the budget leaves the range's window alone: before,
// every such build retired the window and the next access imported it again.
void importRefusedWholeRangeTests(const Context& context) {
WindowedRange range(context, 2 * WindowedRange::piece, 4, 3 * 2 * WindowedRange::piece);
const auto first = range.First();
const auto* windowed = HostImportFor(range.importing, first + 16, 32);
Require(windowed != nullptr && windowed->bytes == range.window, "the first window of a range over the budget was not imported");
const auto buffer = windowed->buffer;
for (int build = 0; build < 3; ++build) {
GuestBufferMemory leased(range.importing);
leased.AcquireRegistered();
leased.Upload(true);
leased.WriteBack();
Require(HostImportCovers(range.importing, first + 16, 32), "an address-based build refused its whole-range import and retired the range's window (build " + std::to_string(build) + ")");
}
const auto* kept = HostImportFor(range.importing, first + 16, 32);
Require(kept != nullptr && kept->buffer == buffer, "the range's window was imported again after a refused whole-range import");
}
// Two uses across neighbouring cell edges of one range, each in a build of its own, share one import: the second grows
// the first's window to their union, and later rounds of both keep it. Before, each replaced the other's window at
// every use.
void importNeighbouringEdgesTests(const Context& context) {
WindowedRange range(context, 2 * WindowedRange::piece, 3);
const auto first = range.First();
const auto use = [&](std::uintptr_t address) {
GuestBufferMemory leased(range.importing);
leased.AddReadable(address, 32);
leased.Upload(false);
leased.WriteBack();
};
const auto acrossFirst = first + range.window - 16;
const auto acrossSecond = first + 2 * range.window - 16;
use(acrossFirst);
Require(HostImportCovers(range.importing, acrossFirst, 32), "a use across the first cell edge was not imported");
use(acrossSecond);
Require(HostImportCovers(range.importing, first, range.bytes), "a use across the second cell edge did not grow the first use's window to their union");
const auto serial = HostImportSerial(range.importing, first, range.bytes, false);
for (int round = 0; round < 10; ++round) {
use(acrossFirst);
use(acrossSecond);
Require(HostImportSerial(range.importing, first, range.bytes, false) == serial, "a use across a cell edge replaced the shared window (round " + std::to_string(round) + ")");
}
}
// A range mapped while the imports are at the budget is refused at map time without retiring an import: the map
// observer holds no GPU lock, so it must not evict (before, it evicted the least recently used import to fit).
void importAtMapOverBudgetTests(const Context& context) {
constexpr std::size_t bytes = 65536;
auto importing = context;
importing.hostImportAlignment = bytes;
void* kept = ::operator new(bytes, std::align_val_t{bytes});
void* used = ::operator new(bytes, std::align_val_t{bytes});
void* mapped = ::operator new(bytes, std::align_val_t{bytes});
const auto change = [&](void* block, bool add) {
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, bytes, true, false, true);
else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block);
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
};
change(kept, true);
change(used, true);
const auto keptAddress = reinterpret_cast<std::uintptr_t>(kept);
const auto usedAddress = reinterpret_cast<std::uintptr_t>(used);
Require(HostImportFor(importing, keptAddress, 16) != nullptr, "the import to keep was not made");
for (int use = 0; use < 300; ++use) Require(HostImportFor(importing, usedAddress, 16) != nullptr, "the import in use was not made");
{
GuestBufferMemory leased(importing);
leased.AddReadable(usedAddress, 16);
leased.Upload(false);
leased.WriteBack();
}
const auto live = HostImportLiveBytes();
SetHostImportBudget(live + bytes - 1);
change(mapped, true);
GuestAllocations::Mapped ranges;
{
const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix();
for (const auto& range : lease) {
if (range->address == reinterpret_cast<std::uintptr_t>(mapped)) ranges.push_back(range);
}
}
Require(ranges.size() == 1, "the mapped range is not registered");
static_cast<void>(ImportMappedRanges(importing, ranges, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), true));
SetHostImportBudget(0);
Require(HostImportLiveBytes() == live, "a range mapped at the budget retired an import at map time (an eviction without the GPU lock)");
Require(HostImportCovers(importing, keptAddress, 16) && !HostImportCovers(importing, reinterpret_cast<std::uintptr_t>(mapped), 16), "a range mapped at the budget was imported, or the import to keep was retired");
for (auto* block : {kept, used, mapped}) change(block, false);
for (auto* block : {kept, used, mapped}) ::operator delete(block, std::align_val_t{bytes});
}
ShaderRecompiler::DescriptorBinding binding(Role role, std::uint32_t slot) {
return {ShaderRecompiler::DescriptorKind::StorageBuffer, role, 0, slot, 1, {}, false};
}
@ -1469,13 +1202,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) {
importCrossingTests(context, access);
importSpanTests(context);
failedImportRemapTests(context, access);
spaceImportRunTests(context);
importWindowTests(context);
importJoinTests(context);
importWholeRangeOverWindowTests(context);
importRefusedWholeRangeTests(context);
importNeighbouringEdgesTests(context);
importAtMapOverBudgetTests(context);
heapMirrorTests(context, access);
importedHeapMirrorTests(context, access);
importWatchTests();

View File

@ -145,14 +145,22 @@ void run(AgcDriver::VulkanDevice& device, ColorTileMode mode, const std::string&
expect(device, color, [](std::uint32_t x, std::uint32_t y) { return y < 4 ? Drawn : y >= 8 && x >= 24 ? Clear : Stale; }, name + " fast-cleared bottom edge tiles under a draw of the top rows");
}
void unknownCode(AgcDriver::VulkanDevice& device) {
void invalidCode(AgcDriver::VulkanDevice& device) {
const auto color = target(ColorTileMode::RenderTarget);
prepare(color, [](std::uint32_t tileX, std::uint32_t tileY) { return tileX == 2 && tileY == 0; }, 3);
eliminate(device, color);
expect(device, color, [](std::uint32_t, std::uint32_t) { return Stale; }, "a tile with CMASK code 3 eliminated");
{
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
device.WaitIdle();
}
prepare(color, [](std::uint32_t, std::uint32_t) { return true; });
eliminate(device, color);
expect(device, color, [](std::uint32_t, std::uint32_t) { return Clear; }, "fast-cleared target eliminated after a CMASK code 3");
try {
eliminate(device, color);
} catch (const std::runtime_error& error) {
Require(std::string(error.what()).find("were recorded, which are not a single-sample fast-clear code") != std::string::npos, std::string("unexpected failure: ") + error.what());
return;
}
throw std::runtime_error("a CMASK code 3 read by the GPU pass did not throw");
}
}
@ -174,7 +182,7 @@ int main() {
}
run(*device, ColorTileMode::Linear, "linear");
run(*device, ColorTileMode::RenderTarget, "SW_64KB_R_X");
unknownCode(*device);
invalidCode(*device);
device->WaitIdle();
device.reset();
{

View File

@ -150,66 +150,6 @@ int main() {
return b.eliminate() == 1u && !clamped.HasUses() && product.HasUses();
});
passed &= run("a min, max, min3, max3 or med3 of flushed values needs no flush, one with an unknown operand does", [] {
Builder b;
const auto value = [&](IrValue& bits) -> IrValue& { return b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&bits}); };
const auto flushedResult = [&](IrOpcode opcode, std::initializer_list<IrValue*> arguments) -> IrValue& {
return b.flush(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&b.emit(opcode, IrType::F32, arguments)}));
};
auto& x = value(b.flush(b.unknown()));
auto& y = value(b.flush(b.unknown()));
auto& z = value(b.flush(b.unknown()));
auto& raw = value(b.unknown());
IrValue* removed[] = {
&flushedResult(IrOpcode::FPMin32, {&x, &y}),
&flushedResult(IrOpcode::FPMax32, {&y, &x}),
&flushedResult(IrOpcode::FPMinTri32, {&x, &y, &z}),
&flushedResult(IrOpcode::FPMaxTri32, {&z, &y, &x}),
&flushedResult(IrOpcode::FPMedTri32, {&x, &z, &y}),
};
auto& mixedPair = flushedResult(IrOpcode::FPMax32, {&x, &raw});
auto& mixedMedian = flushedResult(IrOpcode::FPMedTri32, {&x, &y, &raw});
for (auto* flushed : removed) b.keep(*flushed);
b.keep(mixedPair);
b.keep(mixedMedian);
const bool counted = b.eliminate() == 5u;
bool kept = false;
for (auto* flushed : removed) kept |= flushed->HasUses();
return counted && !kept && mixedPair.HasUses() && mixedMedian.HasUses();
});
passed &= run("a floor or ceil needs no flush, even of an unknown value", [] {
Builder b;
auto& raw = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&b.unknown()});
auto& floored = b.flush(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&b.emit(IrOpcode::FPFloor32, IrType::F32, {&raw})}));
auto& ceiled = b.flush(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&b.emit(IrOpcode::FPCeil32, IrType::F32, {&raw})}));
b.keep(floored);
b.keep(ceiled);
return b.eliminate() == 2u && !floored.HasUses() && !ceiled.HasUses();
});
passed &= run("a max carried around a loop without a flush keeps its flush and the search ends", [] {
Builder b;
IrBlock& head = *b.block;
IrBlock& body = b.newBlock();
IrBlock& exit = b.newBlock();
head.AddBranch(&body);
body.AddBranch(&body);
body.AddBranch(&exit);
auto& initial = b.flush(b.unknown());
b.block = &body;
auto& carried = b.program.CreateValue(IrOpcode::Phi, IrType::U32);
body.AppendInstruction(&carried);
auto& step = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&b.flush(b.unknown())});
auto& maximum = b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&b.emit(IrOpcode::FPMax32, IrType::F32, {&b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&carried}), &step})});
carried.AddPhiOperand(&head, &initial);
carried.AddPhiOperand(&body, &maximum);
auto& flushed = b.flush(maximum);
b.block = &exit;
b.keep(flushed);
return b.eliminate() == 0u && flushed.HasUses();
});
passed &= run("a phi of flushed values needs no flush", [] {
Builder b;
IrBlock& head = *b.block;

View File

@ -230,7 +230,6 @@ void RunDepthSurfaceReuseTests() {
context.memory.memoryTypes[0].propertyFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
context.limits.maxFramebufferWidth = 16384;
context.limits.maxFramebufferHeight = 16384;
context.limits.maxFramebufferLayers = 1;
TestD16StencilFallback(context);
constexpr std::uint64_t depth32 = 0x40000000;
constexpr std::uint64_t depth16 = 0x50000000;

View File

@ -1,6 +0,0 @@
#include <cstdio>
int main() {
std::puts("skipped, this build leaves the test out with ANYPS5_AGC_EXECUTION_TESTS");
return 77;
}

View File

@ -1,119 +0,0 @@
#include "BdaAbi.hpp"
#include "Recompiler.hpp"
#include <array>
#include <cstdio>
#include <exception>
#include <span>
#include <spirv/unified1/spirv.hpp>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>
namespace {
using namespace ShaderRecompiler;
void Check(bool condition, const std::string& message) {
if (!condition) throw std::runtime_error(message);
}
SpirvTarget Target(bool byteAccess) {
static constexpr std::array<std::uint32_t, 3> withBytes{
spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess,
};
static constexpr std::array<std::uint32_t, 2> withoutBytes{spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses};
static constexpr std::array<std::string_view, 4> extensions{
"SPV_EXT_descriptor_indexing", "SPV_KHR_8bit_storage", "SPV_KHR_float_controls", "SPV_KHR_physical_storage_buffer",
};
return {0x00401000u, 0x00010300u, 32, BdaAbi::Version, byteAccess ? std::span<const std::uint32_t>(withBytes) : std::span<const std::uint32_t>(withoutBytes), extensions, false,
{1024, 1024, 64}, 1024, 32768, {}, {}};
}
struct Counts {
std::size_t compareExchanges = 0;
std::size_t byteStores = 0;
std::size_t byteTypes = 0;
bool byteCapability = false;
bool byteBuffers = false;
};
Counts Count(std::span<const std::uint32_t> words) {
Check(words.size() >= 5 && words[0] == spv::MagicNumber, "formatted buffer byte stores: missing SPIR-V module");
Counts counts;
std::uint32_t byteType = 0;
std::vector<std::uint32_t> bytePointers;
for (std::size_t offset = 5; offset < words.size();) {
const auto count = words[offset] >> spv::WordCountShift;
Check(count != 0 && count <= words.size() - offset, "formatted buffer byte stores: malformed SPIR-V instruction");
const auto opcode = words[offset] & spv::OpCodeMask;
if (opcode == spv::OpAtomicCompareExchange) ++counts.compareExchanges;
if (opcode == spv::OpCapability && words[offset + 1] == spv::CapabilityStorageBuffer8BitAccess) counts.byteCapability = true;
if (opcode == spv::OpTypeInt && words[offset + 2] == 8u) {
++counts.byteTypes;
byteType = words[offset + 1];
}
if (opcode == spv::OpTypePointer && words[offset + 2] == spv::StorageClassStorageBuffer && words[offset + 3] == byteType && byteType != 0) bytePointers.push_back(words[offset + 1]);
if (opcode == spv::OpName) {
const auto name = std::string_view(reinterpret_cast<const char*>(&words[offset + 2]));
if (name == "buffers_u8") counts.byteBuffers = true;
}
offset += count;
}
std::vector<std::uint32_t> byteChains;
for (std::size_t offset = 5; offset < words.size();) {
const auto count = words[offset] >> spv::WordCountShift;
const auto opcode = words[offset] & spv::OpCodeMask;
if (opcode == spv::OpAccessChain) {
for (const auto pointer : bytePointers) {
if (words[offset + 1] == pointer) byteChains.push_back(words[offset + 2]);
}
}
if (opcode == spv::OpStore) {
for (const auto chain : byteChains) {
if (words[offset + 1] == chain) ++counts.byteStores;
}
}
offset += count;
}
return counts;
}
Counts Compile(bool byteAccess, bool vertex = false) {
std::vector<std::uint32_t> code{0xe0000000u, 0x80020a00u, 0xbf8c3f70u, 0xe0100000u, 0x80020a00u};
if (vertex) code.insert(code.end(), {0xf80008cfu, 0x03020100u});
code.push_back(0xbf810000u);
const std::span<const std::uint32_t> words(code);
const std::array<MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(words)}}};
const std::array<std::uint32_t, 12> userData{0u, 0u, 0u, 0u, 0x10000000u, 0u, 0x1000u, 0x31016facu, 0x20000000u, 0u, 0x1000u, 0x31038facu};
RecompileRequest request{};
request.shader = {vertex ? ShaderStage::Vertex : ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), words, 0, {}};
request.target = Target(byteAccess);
request.layout = {0, 0, 0, 128};
request.context.waveSize = 32;
request.context.userDataBaseRegister = 0;
request.context.userData = userData;
if (vertex) request.context.vertex = ShaderVertexStageInfo{};
else request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u};
request.context.memory = memory;
request.useCache = false;
return Count(Recompile(request).spirv.Words());
}
}
int main() {
try {
const auto bytes = Compile(true);
Check(bytes.compareExchanges == 0, "formatted buffer byte stores: a store still patches its dword with " + std::to_string(bytes.compareExchanges) + " compare-exchange loops");
Check(bytes.byteBuffers && bytes.byteCapability && bytes.byteTypes == 1, "formatted buffer byte stores: the byte view of the buffers is not declared under StorageBuffer8BitAccess");
Check(bytes.byteStores >= 4, "formatted buffer byte stores: the 8-bit components are stored with " + std::to_string(bytes.byteStores) + " byte stores, expected at least 4");
const auto dwords = Compile(false);
Check(dwords.compareExchanges > 0 && !dwords.byteBuffers && !dwords.byteCapability, "formatted buffer byte stores: without StorageBuffer8BitAccess the stores do not fall back to the dword patch");
std::printf("formatted buffer byte stores tests passed (%zu byte stores, %zu compare-exchange loops without the capability)\n", bytes.byteStores, dwords.compareExchanges);
return 0;
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
return 1;
}
}

View File

@ -7,7 +7,6 @@
#include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp"
@ -23,7 +22,6 @@
#include "BdaAbi.hpp"
#include "prx/libc/include/GuestAllocations.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include "prx/libc/include/GuestHeap.hpp"
#include <spirv/unified1/spirv.hpp>
#include <algorithm>
#include <array>
@ -53,7 +51,6 @@ using AgcDriver::Graphics::Require;
alignas(256) std::array<std::byte, 1024> colorMemory{};
alignas(256) std::array<std::byte, 2048> sliceMemory{};
alignas(256) std::array<std::uint8_t, 4> volumeKeyMemory{};
AgcDriver::QueueState makeState() {
AgcDriver::QueueState queue;
@ -101,90 +98,6 @@ void expectFailure(TAction action, std::string_view reason) {
throw std::runtime_error("expected graphics rejection: " + std::string(reason));
}
void inactiveVertexPointSizeTests() {
const auto pointSizeState = [] {
auto queue = makeState();
queue.context[0x1b3] = queue.context[0x1b4] = 2;
queue.context[0x207] = 0x01210000;
return queue;
};
for (const auto primitive : {2u, 4u, 5u, 6u, 7u, 17u}) {
auto queue = pointSizeState();
queue.userConfig[0x242] = primitive;
for (const auto raster : {0u, 0x240u, 0x248u}) {
queue.context[0x205] = raster;
const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false);
Require(rejection.empty(), "inactive vertex point size failed the draw precheck: " + rejection);
static_cast<void>(AgcDriver::Graphics::DecodeState(queue));
}
}
for (const auto raster : {0u, 0x240u, 0x248u}) {
auto queue = pointSizeState();
queue.userConfig[0x242] = 3;
queue.context[0x205] = raster;
const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false);
Require(rejection.empty(), "inactive line-strip point size failed the draw precheck: " + rejection);
const auto state = AgcDriver::Graphics::DecodeState(queue);
Require(state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP && !state.primitiveRestart, "inactive point size changed line-strip decoding");
Require(AgcDriver::Graphics::RasterPrimitiveOf(state) == ShaderRecompiler::RasterPrimitive::Line, "inactive point size changed the line primitive class");
queue.userConfig[0x24b] = 1;
queue.context[0x103] = 0xffffffffu;
const auto indexedRejection = AgcDriver::Graphics::DrawRejection(queue, true);
Require(indexedRejection.empty(), "inactive line-strip point size with primitive restart failed the draw precheck: " + indexedRejection);
const auto indexedState = AgcDriver::Graphics::DecodeState(queue);
Require(indexedState.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP && indexedState.primitiveRestart, "inactive point size changed indexed line-strip decoding");
}
const auto rejected = [](AgcDriver::QueueState queue) {
Require(AgcDriver::Graphics::DrawRejection(queue, false).find("auxiliary vertex exports") != std::string::npos, "unsupported vertex exports passed the draw precheck");
try {
static_cast<void>(AgcDriver::Graphics::DecodeState(queue));
} catch (const std::runtime_error&) {
return;
}
throw std::runtime_error("potentially active point size passed the state decoder");
};
for (const auto primitive : {1u, 9u}) {
auto queue = pointSizeState();
queue.userConfig[0x242] = primitive;
rejected(queue);
}
for (const auto stages : {0x2004u, 0x2020u}) {
auto queue = pointSizeState();
queue.context[0x2d5] = stages;
rejected(queue);
}
for (const auto raster : {8u, 0x208u, 0x48u}) {
auto queue = pointSizeState();
queue.context[0x205] = raster;
rejected(queue);
}
for (const auto flag : {1u << 17u, 1u << 20u}) {
auto queue = pointSizeState();
queue.context[0x207] = 0x01210000 | flag;
rejected(queue);
}
for (const auto distances : {1u, 0x100u, 0x00c000ffu, 0x00c0ff00u}) {
auto queue = pointSizeState();
queue.context[0x207] |= distances;
Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "inactive point size rejected supported clip/cull distances");
Require(AgcDriver::Graphics::DecodeState(queue).paClVsOutCntl == queue.context[0x207], "inactive point size discarded clip/cull distance state");
}
{
auto queue = pointSizeState();
queue.context[0x207] |= 0x004001ffu;
Require(AgcDriver::Graphics::DrawRejection(queue, false).find("more than eight") != std::string::npos, "inactive point size bypassed the clip/cull count limit");
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "more than eight clip and cull distances");
}
for (const auto offset : {0x2d5u, 0x205u}) {
auto queue = pointSizeState();
queue.context.erase(offset);
rejected(queue);
}
auto queue = pointSizeState();
queue.userConfig.erase(0x242);
rejected(queue);
}
void hdrComponentSwapTests() {
using namespace AgcDriver::Graphics;
for (const auto number : {0u, 1u, 4u, 7u}) {
@ -538,58 +451,6 @@ void hardwareScreenOffsetTests() {
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register");
}
void UnwrittenSampleLocationTests() {
using AgcDriver::Graphics::DecodeState;
using AgcDriver::Graphics::DrawRejection;
using AgcDriver::Graphics::SampleLocations;
const auto fresh = [](std::uint32_t config, std::uint32_t attributes, std::uint32_t scanMode = 3) {
auto written = makeState();
written.context[0x2f8] = config;
written.context[0x292] = scanMode;
written.context[0x3b8] = 0x0dc00000u;
written.context[0x31d] = attributes;
written.context[0x1b3] = 2;
written.context[0x1b4] = 2;
AgcDriver::QueueState queue;
queue.shader = written.shader;
queue.userConfig = written.userConfig;
written.context.ForEach([&](std::uint32_t offset, std::uint32_t value) { queue.context[offset] = value; });
return queue;
};
AgcDriver::QueueState initial;
for (std::uint32_t offset = 0x2fe; offset <= 0x30d; ++offset) Require(initial.context.contains(offset) && initial.context.at(offset) == 0, "an unwritten PA_SC_AA_SAMPLE_LOCS register did not hold its clear-state value 0");
for (const auto samples : {2u, 4u, 8u}) {
const auto log2 = static_cast<std::uint32_t>(std::countr_zero(samples));
auto queue = fresh((log2 << 20u) | log2, (log2 << 12u) | (log2 << 15u));
const auto state = DecodeState(queue);
Require(state.samples == samples && state.colors.size() == 1 && state.colors[0].samples == samples, "a multisampled draw with unwritten sample locations did not decode its sample count");
Require(state.customSampleLocations, "unwritten sample locations did not decode as the clear-state pattern");
for (std::uint32_t sample = 0; sample < samples; ++sample) Require(state.sampleLocations[sample] == 0, "an unwritten sample location was not at the pixel center");
const auto centered = SampleLocations(state);
for (std::uint32_t sample = 0; sample < samples; ++sample) Require(centered[sample].x == 0.5f && centered[sample].y == 0.5f, "unwritten sample locations did not place every sample at the pixel center");
const auto rejection = DrawRejection(queue, false);
Require(rejection.empty(), "the precheck refused a multisampled draw with unwritten sample locations: " + rejection);
}
auto queue = fresh(0x00200002, 0x12000);
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x622ae6ae;
auto state = DecodeState(queue);
Require(state.samples == 4 && !state.customSampleLocations, "explicit standard 4x sample locations changed");
queue = fresh(0x00200002, 0x12000);
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x22eea66a;
state = DecodeState(queue);
Require(state.samples == 4 && state.customSampleLocations, "explicit custom 4x sample locations changed");
const auto custom = SampleLocations(state);
const std::array<VkSampleLocationEXT, 4> expected{{{0.125f, 0.875f}, {0.875f, 0.125f}, {0.375f, 0.375f}, {0.625f, 0.625f}}};
for (std::size_t sample = 0; sample < expected.size(); ++sample) Require(custom[sample].x == expected[sample].x && custom[sample].y == expected[sample].y, "explicit custom 4x sample coordinates changed");
queue.ClearContext();
for (std::uint32_t offset = 0x2fe; offset <= 0x30d; ++offset) Require(queue.context.at(offset) == 0, "CLEAR_STATE did not reset PA_SC_AA_SAMPLE_LOCS");
expectFailure([&] { DecodeState(fresh(0x00400004, 0x12000)); }, "more than eight samples");
expectFailure([&] { DecodeState(fresh(0x00100002, 0x12000)); }, "exposing fewer samples");
queue = fresh(0, 0, 3);
state = DecodeState(queue);
Require(state.samples == 1 && DrawRejection(queue, false).empty(), "MSAA_ENABLE on a single-sample target with unwritten sample locations was refused");
}
void MsaaEnableSingleSampleTests() {
using AgcDriver::Graphics::DecodeState;
using AgcDriver::Graphics::DrawRejection;
@ -711,34 +572,6 @@ void ShaderStageTests() {
fan.userConfig[0x242] = 3;
expectFailure([&] { AgcDriver::Graphics::DecodeState(fan); }, "unsupported geometry input or output assembly");
}
{
auto fast = makeState();
fast.userConfig[0x242] = 1;
fast.context[0x2d5] = 0x482030;
fast.userConfig[0x25b] = 0x201;
fast.context[0x1ff] = 32;
fast.context[0x2ce] = 32;
fast.context[0x2d3] = 64;
fast.context[0x29b] = 2;
fast.context[0x2ab] = 4;
fast.shader[0x8a] = 0x022c0245;
fast.shader[0x8b] = 0x9002a;
const auto state = AgcDriver::Graphics::DecodeState(fast);
Require(state.stages.path == AgcDriver::Graphics::ShaderPath::Geometry && state.stages.vertexWaveSize == 32 && state.stages.mesh, "a fast-launch draw did not decode as wave32 geometry");
const auto& mesh = *state.stages.mesh;
Require(mesh.inputPrimitive == 1 && mesh.primitivesPerGroup == 1 && mesh.verticesPerGroup == 1 && mesh.maxVertices == 32 && mesh.maxPrimitives == 64 && mesh.threadsPerGroup == 64 && mesh.ldsSizeDwords == 128, "a fast-launch subgroup did not decode as one point amplified by PRIM_AMP_FACTOR");
fast.context[0x2d5] = 0x502030;
expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "unsupported geometry routing, fast launch or wave-ID state");
fast.context[0x2d5] = 0x482030;
fast.userConfig[0x25b] = 0x402;
expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "fast launch other than one point per subgroup");
fast.userConfig[0x25b] = 0x201;
fast.userConfig[0x242] = 4;
expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "fast launch other than one point per subgroup");
fast.userConfig[0x242] = 1;
fast.context[0x2d3] = 0;
expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "amplifies to no primitives");
}
queue.context[0x2ab] = 0;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "invalid VGT_ESGS_RING_ITEMSIZE");
queue.context[0x2ab] = 4;
@ -765,28 +598,17 @@ void ShaderStageTests() {
}
Require(stages.path == AgcDriver::Graphics::ShaderPath::Tessellation && stages.tessellation, "tessellation routing changed");
const auto& tessellation = *stages.tessellation;
Require(tessellation.inputControlPoints == 3 && tessellation.outputControlPoints == 3 && tessellation.domain == 1 && tessellation.partitioning == 2 && tessellation.outputTopology == 2 && stages.minTessellationLevel == 1.0f && stages.maxTessellationLevel == 64.0f && tessellation.patchesPerGroup == 85, "the passthrough triangle-patch configuration decoded differently");
for (const auto [parameters, reason] : {std::pair{0x44049u, "VGT_TF_PARAM"}, std::pair{0x140049u, "VGT_TF_PARAM"}, std::pair{0x2040049u, "VGT_TF_PARAM"}, std::pair{0x40059u, "fractional-odd partitioning"}, std::pair{0x4004du, "fractional-odd partitioning"}, std::pair{0x40045u, "fractional-odd partitioning"}, std::pair{0x40048u, "fractional-odd partitioning"}, std::pair{0x40029u, "fractional-odd partitioning"}, std::pair{0x4004bu, "fractional-odd partitioning"}}) {
Require(tessellation.inputControlPoints == 3 && tessellation.outputControlPoints == 3 && tessellation.domain == 1 && tessellation.partitioning == 2 && tessellation.outputTopology == 2, "the passthrough triangle-patch configuration decoded differently");
for (const auto [parameters, reason] : {std::pair{0x44049u, "VGT_TF_PARAM"}, std::pair{0x140049u, "VGT_TF_PARAM"}, std::pair{0x2040049u, "VGT_TF_PARAM"}, std::pair{0x40059u, "only triangular"}, std::pair{0x40069u, "only triangular"}, std::pair{0x4006au, "only triangular"}}) {
queue.context[0x2db] = parameters;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, reason);
}
queue.context[0x2db] = 0x40041;
Require(AgcDriver::Graphics::DecodeState(queue).stages.tessellation->partitioning == 0, "integer-partitioned triangle patches decoded differently");
queue.context[0x2d6] = 0x1043f;
queue.context[0x2db] = 0x4006a;
queue.context[0x286] = 0x41100000;
const auto quadStages = AgcDriver::Graphics::DecodeState(queue).stages;
const auto& quads = quadStages.tessellation;
Require(quads && quads->inputControlPoints == 4 && quads->outputControlPoints == 4 && quads->domain == 2 && quads->partitioning == 2 && quads->outputTopology == 3 && quadStages.minTessellationLevel == 1.0f && quadStages.maxTessellationLevel == 9.0f && quads->patchesPerGroup == 63, "counter-clockwise quad patches with HOS levels [1, 9] and 63 patches per group decoded differently");
queue.context[0x2db] = 0x40069;
Require(AgcDriver::Graphics::DecodeState(queue).stages.tessellation->outputTopology == 3, "counter-clockwise triangle patches decoded differently");
for (const auto [maximum, minimum, reason] : {std::tuple{0x3f800000u, 0x40000000u, "tessellation level clamps"}, std::tuple{0x41100000u, 0xbf800000u, "tessellation level clamps"}, std::tuple{0x3f000000u, 0x00000000u, "tessellation level clamps"}, std::tuple{0x42800000u, 0x42800000u, "tessellation level clamps"}, std::tuple{0x7fc00000u, 0x3f800000u, "non-finite register"}, std::tuple{0x7f800000u, 0x3f800000u, "non-finite register"}}) {
queue.context[0x2db] = 0x40049;
for (const auto [maximum, minimum] : {std::pair{0x41100000u, 0x3f800000u}, std::pair{0x42800000u, 0x40000000u}}) {
queue.context[0x286] = maximum;
queue.context[0x287] = minimum;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, reason);
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "tessellation level clamps");
}
queue.context[0x2d6] = 0xc355;
queue.context[0x2db] = 0x40049;
queue.context[0x286] = 0x42800000;
queue.context[0x287] = 0x3f800000;
queue.context[0x2d5] = 0x202d;
@ -794,8 +616,6 @@ void ShaderStageTests() {
queue.context[0x2d5] = 0x0200210d;
queue.context[0x2d6] = 0;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "control-point counts");
queue.context[0x2d6] = 0xc300;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "NUM_PATCHES is zero");
queue = makeState();
for (const auto value : {0x2003u, 0x2018u, 0x20c0u, 0x80002000u}) {
queue.context[0x2d5] = value;
@ -1144,16 +964,6 @@ void PixelInputLayoutTests() {
Require(!readsBuiltin(read, spv::BuiltInFragCoord) && !readsBuiltin(read, spv::BuiltInBaryCoordKHR), "an ADDR-only centroid pair was loaded");
}
void BlendBypassTests() {
auto queue = makeState();
for (std::uint32_t constant = 0x105; constant < 0x109; ++constant) queue.context[constant] = 0;
queue.context[0x1e0] = (1u << 30u) | 4u | (5u << 8u);
Require(AgcDriver::Graphics::DecodeState(queue).blends.at(0).blendEnable, "enabled blending was not decoded");
queue.context[0x31c] |= 0x10000u;
const auto bypassed = AgcDriver::Graphics::DecodeState(queue);
Require(!bypassed.blends.at(0).blendEnable && bypassed.blends.at(0).colorWriteMask != 0, "a target with BLEND_BYPASS still blends");
}
void OpaqueDestinationAlphaTests() {
auto queue = makeState();
for (std::uint32_t constant = 0x105; constant < 0x109; ++constant) queue.context[constant] = 0;
@ -1522,12 +1332,6 @@ void DepthStencilTests() {
Require(plainRange.color.address == plain.color.address && plainRange.color.bytes == plain.color.bytes && plainRange.color.arrayLastSlice == plain.color.arrayLastSlice, "a color view of slices 0 to 35 of a target with no array slices did not render into its only slice");
queue.context[0x31b] = 1u;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "ends before its first slice");
queue.context[0x31b] = 1u << 13u;
const auto firstSlice = AgcDriver::Graphics::DecodeState(queue);
Require(firstSlice.color.address == reinterpret_cast<std::uintptr_t>(colorMemory.data()), "a color view of several slices did not retain its first slice");
queue.context[0x207] = 1u << 18u;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "layered draws into a color view of several array slices");
queue.context[0x207] = 0;
const auto sliced = reinterpret_cast<std::uintptr_t>(sliceMemory.data());
queue.context[0x318] = static_cast<std::uint32_t>(sliced >> 8u);
queue.context[0x390] = static_cast<std::uint32_t>(sliced >> 40u);
@ -1551,35 +1355,13 @@ void DepthStencilTests() {
queue.context[0x31b] = 2u | (2u << 13u);
const auto volume = AgcDriver::Graphics::DecodeState(queue);
Require(volume.color.address == sliced && volume.color.depth == 4u && volume.color.depthSlice == 2u, "a color view of one 3D depth slice did not keep the surface address and select the slice");
Require(volume.color.layers == 1 && volume.renderLayers == 1 && volume.layerExports == 0, "a color view of one 3D depth slice became layered");
queue.context[0x31b] = 4u | (4u << 13u);
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "outside the 3D surface");
queue.context[0x31b] = 2u | (1u << 13u);
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "ends before its first slice");
queue.context[0x31b] = 1u | (3u << 13u);
queue.context[0x207] = (1u << 18u) | (1u << 21u) | (1u << 24u);
const auto layered = AgcDriver::Graphics::DecodeState(queue);
Require(layered.color.depthSlice == 1u && layered.color.layers == 3u && layered.renderLayers == 3u && layered.layerExports == (1u << 18u), "a color view of three 3D depth slices did not render three layers from the vertex layer export");
{
auto pointSize = queue;
pointSize.context[0x1b3] = pointSize.context[0x1b4] = 2;
Require(AgcDriver::Graphics::DrawRejection(pointSize, false).empty(), "layered volume test state failed the draw precheck");
pointSize.context[0x207] |= 1u << 16u;
const auto rejection = AgcDriver::Graphics::DrawRejection(pointSize, false);
Require(rejection.empty(), "inactive point size rejected a layered volume draw: " + rejection);
const auto decoded = AgcDriver::Graphics::DecodeState(pointSize);
Require(decoded.color.depthSlice == layered.color.depthSlice && decoded.color.layers == layered.color.layers && decoded.renderLayers == layered.renderLayers && decoded.layerExports == layered.layerExports, "inactive point size changed layered volume routing");
Require(decoded.paClVsOutCntl == pointSize.context[0x207], "inactive point size discarded the layered export state");
}
queue.context[0x31b] = 1u | (8u << 13u);
Require(AgcDriver::Graphics::DecodeState(queue).renderLayers == 3u, "a color view past the 3D surface was not clamped to its last slice");
queue.context[0x207] = 0;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "beyond the 3D surface");
queue.context[0x31b] = 2u | (3u << 13u);
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "several 3D slices");
queue.context[0x31b] = 0;
queue.context[0x31c] |= 0x10000000;
const auto volumeKeys = reinterpret_cast<std::uintptr_t>(volumeKeyMemory.data());
queue.context[0x325] = static_cast<std::uint32_t>(volumeKeys >> 8u);
queue.context[0x3a8] = static_cast<std::uint32_t>(volumeKeys >> 40u);
Require(AgcDriver::Graphics::DecodeState(queue).color.dccAddress == volumeKeys, "a DCC 3D color target lost its keys");
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "DCC 3D color targets");
}
void OneDimensionalColorTests() {
@ -1878,15 +1660,6 @@ void metadataPassTests() {
queue.context[0x0] = 0;
queue.context[0x200] = 0x70;
Require(DecodeColorMetadataPass(queue).has_value(), "an always-pass depth function without a test refused the pass");
auto bound = queue;
bound.context[0x010] = 0x3;
bound.context[0x011] = 0x1;
bound.context[0x200] = 0x72;
Require(DecodeColorMetadataPass(bound).has_value(), "an always-pass depth test without writes refused the pass");
bound.context[0x200] = 0x76;
expectFailure([&] { DecodeColorMetadataPass(bound); }, "depth or stencil work");
bound.context[0x200] = 0x7a;
expectFailure([&] { DecodeColorMetadataPass(bound); }, "depth or stencil work");
queue.context[0x91] = 0x40020;
expectFailure([&] { DecodeColorMetadataPass(queue); }, "over part of a color target");
queue.context[0x91] = 0x40040;
@ -2052,16 +1825,6 @@ void cmaskTests() {
auto mipmapped = queue;
mipmapped.context[0x3b0] |= 1u << 28u;
expectFailure([&] { DecodeColorBuffer(mipmapped.context, 0); }, "mipmapped, 3D or array");
constexpr std::size_t blockBytes = 65536u;
static std::vector<std::byte> mipStorage(16u * blockBytes);
const auto mipBase = (reinterpret_cast<std::uintptr_t>(mipStorage.data()) + blockBytes - 1u) / blockBytes * blockBytes;
auto higherMip = queue;
higherMip.context[0x318] = static_cast<std::uint32_t>(mipBase >> 8u);
higherMip.context[0x390] = static_cast<std::uint32_t>(mipBase >> 40u);
higherMip.context[0x3b0] = (4u << 28u) | (255u << 14u) | 127u;
higherMip.context[0x31b] = 1u << 26u;
const auto higherTarget = DecodeColorBuffer(higherMip.context, 0);
Require(!higherTarget.cmaskFastClear && higherTarget.cmaskAddress == 0, "a higher mip of a fast-clear target decoded a CMASK");
auto unaddressed = queue;
unaddressed.context[0x31f] = 0;
unaddressed.context[0x398] = 0;
@ -2133,8 +1896,8 @@ void cmaskTests() {
cmaskMemory.fill(0xff);
setTile(2, 0);
setTile(3, 3);
AgcDriver::Graphics::RunColorMetadataPass(context, *pass);
Require(tiles([](std::uint32_t tile) { return tile == 2; }) && cmaskIs(0xff), "a tile with a CMASK code other than 0 or 15 did not keep its texels");
expectFailure([&] { AgcDriver::Graphics::RunColorMetadataPass(context, *pass); }, "not a single-sample fast-clear code");
Require(texels(0x5a5a5a5au), "a refused pass changed the texels");
queue.context[0x31c] |= 0x10000000;
const auto keysAddress = reinterpret_cast<std::uintptr_t>(dccKeys.data());
@ -2310,54 +2073,6 @@ void multisampleTests() {
state = DecodeState(queue);
Require(state.samples == 8 && !state.customSampleLocations && state.colors.size() == 1 && state.colors[0].samples == 8, "the standard 8x pattern (sample 7 at (7, -7), PPSA99902's registers) did not decode as standard");
AgcDriver::Graphics::ValidateSampleLocations({}, state);
for (const auto log2 : {0u, 1u, 2u, 3u}) {
queue = log2 == 0 ? makeState() : multisampled();
queue.context[0x1b3] = 2u;
queue.context[0x1b4] = 2u;
queue.context[0x2f8] = (log2 << 20u) | log2 | 0x20000000u;
queue.context[0x31d] = (log2 << 12u) | (log2 << 15u);
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) {
queue.context[0x2fe + pixel * 4u] = log2 == 3 ? 0xbd153fd1u : log2 == 2 ? 0x622ae6aeu : 0xcc44u;
queue.context[0x2ff + pixel * 4u] = log2 == 3 ? 0x9773f95bu : 0u;
}
std::vector<AgcDriver::Graphics::RegisterRead> sampleReads;
AgcDriver::Graphics::RegisterReadLog() = &sampleReads;
const auto coveredCenter = DecodeState(queue);
const auto coveredRejection = AgcDriver::Graphics::DrawRejection(queue, false);
AgcDriver::Graphics::RegisterReadLog() = nullptr;
Require(coveredCenter.samples == (1u << log2) && !coveredCenter.customSampleLocations, "covered-centroid mode changed the sample count or locations without centroid inputs");
for (const auto read : sampleReads) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks a covered-centroid register");
Require(coveredRejection.empty(), "covered-centroid mode without centroid inputs was refused: " + coveredRejection);
for (const auto centroid : {4u, 64u, 68u}) {
queue.context[0x1b3] = 2u | centroid;
queue.context[0x1b4] = 2u | centroid;
expectFailure([&] { DecodeState(queue); }, "COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs");
Require(AgcDriver::Graphics::DrawRejection(queue, false).find("COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs") != std::string::npos, "the precheck accepted active covered-centroid interpolation");
queue.context[0x1b4] = 2u;
Require(DecodeState(queue).samples == (1u << log2) && AgcDriver::Graphics::DrawRejection(queue, false).empty(), "an unaddressed centroid input changed covered-centroid mode");
queue.context[0x1b3] = 2u;
queue.context[0x1b4] = 2u | centroid;
Require(DecodeState(queue).samples == (1u << log2) && AgcDriver::Graphics::DrawRejection(queue, false).empty(), "a disabled centroid input changed covered-centroid mode");
}
queue.context[0x1b3] = 2u;
queue.context[0x1b4] = 2u;
for (const auto offset : {0x1b3u, 0x1b4u}) {
queue.context.erase(offset);
expectFailure([&] { DecodeState(queue); }, "COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs");
Require(AgcDriver::Graphics::DrawRejection(queue, false).find("COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs") != std::string::npos, "the precheck accepted unspecified centroid inputs");
queue.context[offset] = 2u;
}
const auto withProgram = queue;
queue.shader[0x008] = 0;
queue.context[0x8e] = 0;
queue.context.erase(0x1b3);
queue.context.erase(0x1b4);
Require(DecodeState(queue).samples == (1u << log2) && AgcDriver::Graphics::DrawRejection(queue, false).empty(), "covered-centroid mode was refused with no pixel program");
queue = withProgram;
queue.context[0x2f8] |= 0x10u;
expectFailure([&] { DecodeState(queue); }, "coverage conversion or nonstandard multisampling");
Require(AgcDriver::Graphics::DrawRejection(queue, false).find("coverage conversion or nonstandard multisampling") != std::string::npos, "covered-centroid handling admitted unrelated AA bits");
}
queue = multisampled();
queue.context[0x31d] = 0x1000;
expectFailure([&] { DecodeState(queue); }, "fewer fragments than samples");
@ -2547,18 +2262,6 @@ void clipDistanceTests() {
Require(AgcDriver::Graphics::DrawRejection(queue, false).find("more than eight") != std::string::npos, "nine clip distances passed the precheck");
}
void framebufferLayerTests() {
AgcDriver::Graphics::Context context{};
context.limits.maxFramebufferWidth = 64;
context.limits.maxFramebufferHeight = 64;
context.limits.maxFramebufferLayers = 3;
AgcDriver::Graphics::Framebuffer accepted(context, VK_NULL_HANDLE, {}, {64, 64}, 3);
for (const VkRenderPass pass : std::array<VkRenderPass, 2>{VK_NULL_HANDLE, reinterpret_cast<VkRenderPass>(1)}) {
expectFailure([&] { AgcDriver::Graphics::Framebuffer rejected(context, pass, {}, {64, 64}, 0); }, "framebuffer layers exceed device limits");
expectFailure([&] { AgcDriver::Graphics::Framebuffer rejected(context, pass, {}, {64, 64}, 4); }, "framebuffer layers exceed device limits");
}
}
void DepthClipTests() {
auto queue = makeState();
const auto direct = AgcDriver::Graphics::DecodeState(queue);
@ -2636,7 +2339,7 @@ void AlphaToCoverageTests() {
Require(AgcDriver::Graphics::DrawRejection(queue, false).find("alpha-to-coverage") != std::string::npos, "the precheck accepted unsupported alpha-to-coverage");
}
queue.context[0x2dc] = 0xaa01;
const std::uint32_t exportFormat = queue.context.at(0x1c5);
const auto exportFormat = queue.context.at(0x1c5);
for (const auto format : {4u, 5u, 6u, 9u}) {
queue.context[0x1c5] = (exportFormat & ~0xfu) | format;
Require(AgcDriver::Graphics::DecodeState(queue).alphaToCoverage, "alpha-to-coverage rejected an MRT0 export format with a floating-point alpha");
@ -3036,32 +2739,6 @@ AgcDriver::Graphics::Context mockContext() {
return context;
}
void addressSpaceTeardownTests() {
#ifdef _WIN32
_putenv_s("APS5_NO_LEASE_MIRROR", "1");
#else
setenv("APS5_NO_LEASE_MIRROR", "1", 1);
#endif
auto context = mockContext();
context.device = makeHandle<VkDevice>();
context.bufferDeviceAddress = true;
context.limits.maxStorageBufferRange = 1u << 27;
void* block = GuestHeap::GuestHeapAlignForGpu_nid_postfix(16, 64);
const auto before = AgcDriver::Graphics::LeaseCounters().cacheDrops;
{
AgcDriver::Graphics::GuestBufferMemory memory(context);
memory.AcquireRegistered();
Require(memory.HoldsLease(), "address-based build holds no lease before device teardown");
memory.Upload(true);
memory.WriteBack();
Require(!memory.HoldsLease(), "completed address-based build kept its own lease");
}
AgcDriver::Graphics::ClearImageMirrors(context.device);
GuestHeap::GuestHeapFree_nid_postfix(block);
Require(AgcDriver::Graphics::LeaseCounters().cacheDrops == before, "device teardown left the cached address space pinning guest allocations");
Require(mock.live == 0, "address-space teardown test leaked Vulkan objects");
}
using Role = ShaderRecompiler::DescriptorRole;
using Kind = ShaderRecompiler::DescriptorKind;
@ -3695,7 +3372,6 @@ struct ModuleShape {
std::uint32_t floatControlsWidth = 0;
std::uint32_t clipDistanceLength = 0;
std::uint32_t cullDistanceLength = 0;
bool layerOutput = false;
};
void emit(std::vector<std::uint32_t>& out, spv::Op op, std::initializer_list<std::uint32_t> operands) {
@ -3734,14 +3410,6 @@ std::vector<std::uint32_t> makeModule(const ModuleShape& shape) {
emit(annotations, spv::OpDecorate, {parameter, spv::DecorationLocation, shape.parameterLocation});
extraInterface.push_back(parameter);
}
if (shape.layerOutput) {
const auto pointer = id();
const auto layer = id();
emit(declarations, spv::OpTypePointer, {pointer, spv::StorageClassOutput, uintType});
emit(declarations, spv::OpVariable, {pointer, layer, spv::StorageClassOutput});
emit(annotations, spv::OpDecorate, {layer, spv::DecorationBuiltIn, spv::BuiltInLayer});
extraInterface.push_back(layer);
}
if (shape.secondTarget) {
const auto target = id();
emit(declarations, spv::OpVariable, {outputPointer, target, spv::StorageClassOutput});
@ -3898,15 +3566,6 @@ std::vector<std::uint32_t> makeModule(const ModuleShape& shape) {
if (shape.layer) emit(words, spv::OpCapability, {spv::CapabilityGeometry});
if (shape.clipDistanceLength != 0) emit(words, spv::OpCapability, {spv::CapabilityClipDistance});
if (shape.cullDistanceLength != 0) emit(words, spv::OpCapability, {spv::CapabilityCullDistance});
if (shape.layerOutput) {
emit(words, spv::OpCapability, {spv::CapabilityShaderViewportIndexLayerEXT});
const std::string extension = "SPV_EXT_shader_viewport_index_layer";
const auto count = (extension.size() + 4) / 4;
words.push_back((static_cast<std::uint32_t>(count + 1) << 16u) | spv::OpExtension);
const auto start = words.size();
words.resize(start + count, 0);
for (std::size_t i = 0; i < extension.size(); ++i) words[start + i / 4] |= static_cast<std::uint32_t>(static_cast<unsigned char>(extension[i])) << ((i % 4) * 8);
}
if (shape.barycentric) {
emit(words, spv::OpCapability, {spv::CapabilityFragmentBarycentricKHR});
const std::string extension = "SPV_KHR_fragment_shader_barycentric";
@ -4389,41 +4048,6 @@ void conservativeZExportTests() {
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported fragment execution mode");
}
bool declaresEarlyFragmentTests(const ShaderRecompiler::RecompileRequest& request) {
const auto result = ShaderRecompiler::Recompile(request);
const auto& words = result.spirv.Words();
for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) {
if (static_cast<spv::Op>(words[at] & 0xffffu) == spv::OpExecutionMode && words[at + 2] == spv::ExecutionModeEarlyFragmentTests) return true;
}
return false;
}
void earlyFragmentTestsTests() {
const std::array<std::pair<std::uint32_t, bool>, 9> cases{{
{0x0000u, false},
{0x0020u, false},
{0x0010u, true},
{0x0030u, true},
{0x0050u, false},
{0x0011u, false},
{0x1000u, true},
{0x1050u, true},
{0x1031u, true}
}};
const ShaderRecompiler::RequestSerializer serializer;
for (const auto& [shaderControl, expected] : cases) {
const auto request = depthExportRequest(shaderControl);
const auto replayed = serializer.Deserialize(serializer.Serialize(request));
Require(declaresEarlyFragmentTests(request) == expected && declaresEarlyFragmentTests(replayed.request) == expected, "DB_SHADER_CONTROL " + std::to_string(shaderControl) + (expected ? " did not declare" : " declared") + " early fragment tests");
}
for (const auto shaderControl : {0x0010u, 0x1000u}) {
auto request = depthExportRequest(shaderControl);
request.context.pixel->alphaToCoverage = true;
const auto replayed = serializer.Deserialize(serializer.Serialize(request));
Require(!declaresEarlyFragmentTests(request) && !declaresEarlyFragmentTests(replayed.request), "DB_SHADER_CONTROL " + std::to_string(shaderControl) + " with alpha-to-coverage declared early fragment tests");
}
}
constexpr std::array<std::uint32_t, 5> OrderedPixelCode{0x7e0e02f2u, 0xbf900007u, 0xf800180fu, 0x07070707u, 0xbf810000u};
constexpr std::array<std::uint32_t, 12> OrderedBdaPixelCode{0x7e040f00u, 0x7e060280u, 0xdc208001u, 0x047d0002u, 0xdc308008u, 0x057d0002u, 0xbf8c3f70u, 0x4a080b04u, 0x7e0e0d04u, 0xf800180fu, 0x07070707u, 0xbf810000u};
@ -4627,7 +4251,6 @@ void stencilRefExportTests() {
Require(!AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck accepted STENCIL_OP_VAL_EXPORT_ENABLE in DB_SHADER_CONTROL " + std::to_string(shaderControl));
}
Require(!AgcDriver::Graphics::DecodePixelStageInfo(stencilRefExportQueue(0x50u, 2u).context, IdentityExports).stencilRefExportEnable, "a pixel stage without STENCIL_TEST_VAL_EXPORT_ENABLE exports the stencil reference");
Require(AgcDriver::Graphics::DecodePixelStageInfo(stencilRefExportQueue(0x1012u, 2u).context, IdentityExports).earlyZ, "DEPTH_BEFORE_SHADER with a stencil reference export did not get early tests");
const std::array<std::uint32_t, 2> stencilExport{spv::CapabilityShader, spv::CapabilityStencilExportEXT};
const std::array<std::uint32_t, 1> plain{spv::CapabilityShader};
const ShaderRecompiler::RequestSerializer serializer;
@ -5297,7 +4920,6 @@ void validationTests() {
vertex.vertexAttributes[0].components = 2;
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "metadata disagrees");
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 2, 1) == 80, "incorrect strided vertex range");
Require(AgcDriver::Graphics::VertexBufferReadOffset(attribute, 2, 7) == 64, "vertex range includes records before the first vertex");
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 3, 1) == 112, "an index past the records still spans its fetch");
{
auto wider = attribute;
@ -5320,7 +4942,6 @@ void validationTests() {
Require(AgcDriver::Graphics::ClampDrawIndices(std::as_writable_bytes(std::span(narrow)), 2, false, 4u) == 4u && narrow[0] == 4u && narrow[1] == 1u, "clamping 16-bit indices failed");
}
attribute.fetchIndex = 1;
Require(AgcDriver::Graphics::VertexBufferReadOffset(attribute, 2, 7) == 224, "instance range includes records before the first instance");
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2) == 48, "instance attributes used the vertex index");
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2, 1) == 80, "first instance was ignored");
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 2, 2) == 112, "a first instance past the records still spans its fetch");
@ -5338,7 +4959,6 @@ void validationTests() {
attribute.resource.fields[1] = 0;
attribute.resource.fields[2] = 16;
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2) == 16, "zero stride must repeat one value");
Require(AgcDriver::Graphics::VertexBufferReadOffset(attribute, 100, 7) == 0, "zero-stride input skipped its repeated value");
attribute.resource.fields[2] = 8;
expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1); }, "byte range");
Require(!AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "a structured zero-stride fetch was treated as out of range");
@ -5414,14 +5034,6 @@ void validationTests() {
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported device capability 12");
AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, true);
}
{
ShaderRecompiler::RecompileResult vertex;
vertex.spirv = makeModule({.layerOutput = true});
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}};
const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES};
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "VK_EXT_shader_viewport_index_layer");
AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, false, false, true);
}
for (const auto capability : {spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle}) {
ShaderRecompiler::RecompileResult vertex;
vertex.spirv = makeModule({});
@ -5777,21 +5389,6 @@ int main(int argc, char** argv) {
std::cout << "sparse buffer hole tests " << (passed ? "passed" : "failed") << '\n';
return passed ? 0 : 1;
}
if (argc == 2 && std::string_view(argv[1]) == "--address-space-teardown") {
addressSpaceTeardownTests();
std::cout << "address-space device teardown releases cached guest allocations\n";
return 0;
}
{
AgcDriver::Graphics::PipelineCacheSynchronization synchronization;
auto pipelineUse = synchronization.LockForPipelineUse();
bool snapshotAcquired = false;
std::thread snapshot([&] { snapshotAcquired = synchronization.TryLockForSnapshot().owns_lock(); });
snapshot.join();
Require(!snapshotAcquired, "pipeline cache snapshot overlapped pipeline creation");
pipelineUse.unlock();
Require(synchronization.TryLockForSnapshot().owns_lock(), "pipeline cache snapshot stayed blocked after pipeline creation finished");
}
if (argc == 2 && std::string_view(argv[1]) == "--descriptor-layout-flags") {
descriptorLayoutFlagTests();
std::cout << "Descriptor layout native and captured binding flags passed\n";
@ -5802,11 +5399,6 @@ int main(int argc, char** argv) {
std::cerr << "descriptor pool reset failure did not terminate\n";
return 1;
}
if (argc == 2 && std::string_view(argv[1]) == "--covered-centroid-state") {
multisampleTests();
std::cout << "Covered-centroid and multisample state checks passed\n";
return 0;
}
{
const AgcDriver::Graphics::Context context{};
const AgcDriver::Graphics::State state{};
@ -5853,7 +5445,6 @@ int main(int argc, char** argv) {
unorm10_11_11TargetTests();
DepthBoundsBiasTests();
conservativeZExportTests();
earlyFragmentTestsTests();
orderedPixelShaderTests();
stencilRefExportTests();
aliasedBufferViewTests();
@ -5864,7 +5455,6 @@ int main(int argc, char** argv) {
DisabledColorTests();
OpaqueDestinationAlphaTests();
Color1555TargetTests();
BlendBypassTests();
CompactedExportTests();
ReversedComponentOrderTests();
ShadersWriteMemoryTests();
@ -5876,11 +5466,8 @@ int main(int argc, char** argv) {
multisampleTests();
MsaaEnableSingleSampleTests();
clipDistanceTests();
framebufferLayerTests();
UnwrittenSampleLocationTests();
ShaderStageTests();
NggVertexWorkgroupTests();
inactiveVertexPointSizeTests();
TuningFieldTests();
PixelInputLayoutTests();
UnexportedPixelInputTests();

View File

@ -1,93 +0,0 @@
#include "IntermediateRepresentation/IrBuilder.hpp"
#include "IntermediateRepresentation/IrProgram.hpp"
#include "Optimization/DeadCodeEliminator.hpp"
#include <cstdio>
#include <memory>
#include <stdexcept>
#include <vector>
using namespace ShaderRecompiler;
int main() {
try {
IrProgram program;
auto& block = program.CreateBlock();
program.SetEntryBlock(block);
program.BlockOrder().push_back(&block);
IrBuilder builder(program);
builder.SetInsertionPoint(block);
auto& value = builder.Constant(42u);
IrSsaValues<NumScalarRegs> table;
const auto& readTable = table;
if (readTable[0] != nullptr || table.HasStorage()) throw std::runtime_error("an empty SSA table allocated storage during a read");
for (IrValue* entry : readTable) if (entry != nullptr) throw std::runtime_error("an empty SSA table contained a value");
if (table.HasStorage()) throw std::runtime_error("iterating an empty SSA table allocated storage");
table[3] = &value;
if (!table.HasStorage() || readTable[3] != &value || readTable[2] != nullptr) throw std::runtime_error("an SSA write lost zero initialization or its value");
table.fill(nullptr);
if (table.HasStorage() || readTable[3] != nullptr) throw std::runtime_error("clearing an SSA table retained construction storage");
const auto id = value.Id();
static_cast<void>(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&value}));
auto& identity = program.CreateValue(IrOpcode::Identity, IrType::U32);
identity.AddArgument(&value);
block.AppendInstruction(&identity);
DeadCodeEliminator{}.RemoveIdentities(program);
auto& first = program.CreateValue(IrOpcode::IAdd32, IrType::U32);
auto& second = program.CreateValue(IrOpcode::IAdd32, IrType::U32);
first.AddArgument(&second);
second.AddArgument(&first);
std::vector<IrValue*> pinned;
for (std::uint32_t number = 100u; number < 112u; ++number) pinned.push_back(&builder.Constant(number));
block.ssaScalarValues[0] = pinned[0];
block.ssaVectorValues[0] = pinned[1];
block.ssaThreadBitScalarValues[0] = pinned[2];
block.ssaScalarMaskTags[0] = pinned[3];
program.Metadata().dynamicReads.push_back(pinned[4]);
program.Metadata().blockInfo.emplace_back();
program.Metadata().blockInfo.back().condition = pinned[5];
program.Metadata().blockInfo.back().indirectTarget = pinned[6];
auto& resources = program.Resources();
resources.descriptorSources.emplace_back();
resources.descriptorSources.back().dwords[0] = pinned[7];
resources.srtReads.push_back({pinned[8], 0u});
resources.controlFlow.emplace_back();
resources.controlFlow.back().condition = pinned[9];
resources.uniformFill.values[0] = pinned[10];
auto external = std::make_unique<IrValue>(IrOpcode::Identity, IrType::U32, value.Id());
external->AddArgument(pinned[11]);
resources.valueStorage.push_back(std::move(external));
const auto removed = program.ReclaimUnusedValues();
if (removed != 3u) throw std::runtime_error("detached identity and cyclic garbage were not reclaimed");
if (value.Id() != id || value.ImmediateU32() != 42u || value.Uses().size() != 1u) throw std::runtime_error("a live instruction lost its value or retained an unused backlink");
for (std::size_t index = 0; index < pinned.size(); ++index) {
if (pinned[index]->ImmediateU32() != 100u + index) throw std::runtime_error("a metadata or resource-owned value was reclaimed");
}
if (program.ReclaimUnusedValues() != 0u) throw std::runtime_error("reclaiming an unchanged graph was not idempotent");
auto& unused = program.CreateValue(IrOpcode::IAdd32, IrType::U32);
IrValue untracked(IrOpcode::Identity, IrType::U32, value.Id());
untracked.AddArgument(&unused);
bool rejected = false;
try {
static_cast<void>(program.ReclaimUnusedValues());
} catch (const std::runtime_error&) {
rejected = true;
}
if (!rejected) throw std::runtime_error("an untracked external owner was accepted");
untracked.ReplaceArgument(0, nullptr);
program.Metadata().dynamicReads.clear();
if (program.ReclaimUnusedValues() != 2u) throw std::runtime_error("reclamation did not recover after an external-owner rejection");
if (builder.Constant(999u).Id() <= pinned.back()->Id()) throw std::runtime_error("reclamation reused a value ID");
IrProgram moved = std::move(program);
if (value.Id() != id || value.ImmediateU32() != 42u || moved.ReclaimUnusedValues() != 1u) throw std::runtime_error("moving a program invalidated its allocation owner");
IrProgram assigned;
static_cast<void>(assigned.CreateValue(IrOpcode::IAdd32, IrType::U32));
assigned = std::move(moved);
if (value.Id() != id || value.ImmediateU32() != 42u || assigned.ReclaimUnusedValues() != 0u) throw std::runtime_error("move assignment invalidated a live allocation owner");
if (assigned.CreateValue(IrOpcode::IAdd32, IrType::U32).Id() <= pinned.back()->Id()) throw std::runtime_error("a transferred allocation owner reused a value ID");
std::puts("IR reclamation ownership tests passed");
return 0;
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
return 1;
}
}

View File

@ -10,7 +10,6 @@
#include <cstring>
#include <iostream>
#include <new>
#include <span>
#include <string>
#include <vector>
@ -47,13 +46,6 @@ alignas(256) constexpr auto Wave32GeometryCode = [] {
return code;
}();
alignas(256) constexpr std::array<std::uint32_t, 25> FastLaunchCode{
0xd7650000, 0x000100c1, 0x7e000505, 0x93018300, 0x4a020001, 0xe0382000, 0x80020201, 0xe0382010,
0x80020601, 0xb07c1003, 0xbf900009, 0xbf8c3f70, 0x7d880083, 0xbefe036a, 0xf80008cf, 0x05040302,
0xf800020f, 0x09080706, 0x7d840080, 0xbefe036a, 0x7e1402ff, 0x00200400, 0xf8000941, 0x0000000a,
0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 7> PixelCode{
0xc8020002, 0xc8060102, 0xc80a0202, 0xc80e0302, 0xf800180f, 0x03020100, 0xbf810000,
};
@ -140,8 +132,6 @@ struct MeshDraw {
bool restart = false;
std::uint32_t waveSize = 64u;
VkCullModeFlags cull = VK_CULL_MODE_NONE;
std::span<const std::uint32_t> code = {};
std::uint32_t stagesRegister = 0x20u;
};
bool ReadsNumWorkgroups(const std::vector<std::uint32_t>& words) {
@ -170,7 +160,7 @@ void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) {
const auto index = AgcDriver::Graphics::MeshIndexBufferDescriptor(setup.draw);
std::copy(index.begin(), index.end(), userData.begin() + ShaderRecompiler::MeshIndexBufferUserWord);
std::copy(setup.vertexBuffer.begin(), setup.vertexBuffer.end(), userData.begin() + 8);
const std::span<const std::uint32_t> code = !setup.code.empty() ? setup.code : setup.waveSize == 32u ? std::span<const std::uint32_t>(Wave32GeometryCode) : std::span<const std::uint32_t>(GeometryCode);
const auto& code = setup.waveSize == 32u ? Wave32GeometryCode : GeometryCode;
const std::array<ShaderRecompiler::MemoryRegion, 1> geometryMemory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(std::span(code))}}};
ShaderRecompiler::RecompileRequest geometry{
{ShaderStage::Mesh, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
@ -204,7 +194,7 @@ void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) {
}};
AgcDriver::Graphics::State state{};
state.stages = {AgcDriver::Graphics::ShaderPath::Geometry, setup.stagesRegister, setup.waveSize, 64, setup.mesh, std::nullopt};
state.stages = {AgcDriver::Graphics::ShaderPath::Geometry, 0x20u, setup.waveSize, 64, setup.mesh, std::nullopt};
state.color = {reinterpret_cast<std::uintptr_t>(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u};
state.colors = {state.color};
state.hasColorTarget = true;
@ -225,7 +215,6 @@ void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) {
constexpr ShaderRecompiler::MeshConfiguration SmallSubgroup{4u, 4u, 12u, 12u, 4u, 64u, 1024u, 0u, 4u};
constexpr ShaderRecompiler::MeshConfiguration WideSubgroup{4u, 32u, 96u, 96u, 32u, 128u, 2048u, 0u, 4u};
constexpr ShaderRecompiler::MeshConfiguration FastLaunchSubgroup{1u, 1u, 1u, 3u, 1u, 32u, 128u, 0u, 4u};
std::size_t FanPixel(float angle, float radius) {
const auto x = static_cast<std::uint32_t>((radius * std::cos(angle) + 1.0f) * 0.5f * Width);
@ -355,24 +344,6 @@ int main() {
CheckTriangles("non-indexed wave32 triangle list");
}
ClearPixels();
DrawMesh(device, {FastLaunchSubgroup, {0, Triangles, 0, 1, 0, false}, VertexBufferDescriptor(Ordered.data(), static_cast<std::uint32_t>(Ordered.size())), ShaderRecompiler::MeshDrawPushOffsetBytes, false, 32u, VK_CULL_MODE_NONE, FastLaunchCode, 0x482030u});
CheckTriangles("fast-launch subgroups");
ClearPixels();
DrawMesh(device, {FastLaunchSubgroup, {0, Triangles / 2u, 0, 1, 0, false, Triangles / 4u}, VertexBufferDescriptor(Ordered.data(), static_cast<std::uint32_t>(Ordered.size())), ShaderRecompiler::MeshDrawPushOffsetBytes, false, 32u, VK_CULL_MODE_NONE, FastLaunchCode, 0x482030u});
for (std::uint32_t triangle = 0; triangle < Triangles; ++triangle) {
const bool drawn = triangle >= Triangles / 4u && triangle < Triangles / 4u + Triangles / 2u;
Require(PixelIs(CellPixel(triangle), drawn ? TriangleColor(triangle) : Background), "fast-launch subgroups from a start vertex: triangle " + std::to_string(triangle) + " pixel " + PixelText(CellPixel(triangle)));
}
bool fastLaunchIndexedRefused = false;
try {
DrawMesh(device, {FastLaunchSubgroup, {reinterpret_cast<std::uintptr_t>(Indices.data()), Triangles, 2, 1, 0, true}, VertexBufferDescriptor(Ordered.data(), static_cast<std::uint32_t>(Ordered.size())), ShaderRecompiler::MeshDrawPushOffsetBytes, false, 32u, VK_CULL_MODE_NONE, FastLaunchCode, 0x482030u});
} catch (const std::runtime_error& error) {
fastLaunchIndexedRefused = std::string(error.what()).find("indexed fast-launch draws are unsupported") != std::string::npos;
if (!fastLaunchIndexedRefused) throw;
}
Require(fastLaunchIndexedRefused, "an indexed fast-launch draw was drawn");
constexpr std::size_t recordBlockBytes = 65536;
auto* record = static_cast<std::uint32_t*>(::operator new(recordBlockBytes, std::align_val_t{65536}));
{

View File

@ -2,7 +2,6 @@
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp"
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
#include "prx/libSceAgcDriver/Submit/include/Acb.hpp"
#include "prx/libc/include/Shutdown.hpp"
@ -51,10 +50,8 @@ std::uint32_t low(const void* pointer) { return static_cast<std::uint32_t>(reint
std::uint32_t high(const void* pointer) { return static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(pointer) >> 32u); }
void execute(AgcDriver::QueueState& state, const std::vector<std::uint32_t>& packet) {
AgcDriver::DriverDetail::RegisterStateKey(state);
AgcDriver::Pm4::Validate(packet, 0);
AgcDriver::Pm4::Execute(packet, state);
check(AgcDriver::DriverDetail::RegisterStateKey(state) == AgcDriver::DriverDetail::RegisterStateKey(state, false, false), "PM4 changed registers without updating their fingerprint");
}
void testCatalog() {

View File

@ -27,13 +27,9 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include "ResidentPresent.hpp"
#include "SampleLod_spv.h"
#include "BufferUpdate_spv.h"
#include "SampleArray_spv.h"
#include "Cover_vert_spv.h"
#include "Cover_frag_spv.h"
#include "VertexRange_vert_spv.h"
#include "VertexRangeProducer_frag_spv.h"
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetTransfer.hpp"
#include <SDL_loadso.h>
#ifdef _WIN32
#ifndef NOMINMAX
@ -310,240 +306,6 @@ private:
using Kind = Recorder::ReadKind;
void copiedDirectOrderingTests(const Device& device, Recorder& recorder) {
auto context = device.GetContext();
context.nullDescriptors = device.NullDescriptors();
if (context.hostImportAlignment == 0 || !context.nullDescriptors || !AgcDriver::GuestMemory::WriteWatched()) {
std::cout << "Host imports, null descriptors or write watching unavailable: copied/direct ordering test skipped\n";
return;
}
recorder.Sync();
const auto bytes = static_cast<std::size_t>(std::max<VkDeviceSize>(131072, context.hostImportAlignment));
auto* block = static_cast<std::uint32_t*>(AllocateWatched(bytes, bytes));
Require(block != nullptr, "allocate copied/direct ordering memory");
const auto address = reinterpret_cast<std::uint64_t>(block);
std::vector<std::shared_ptr<ShaderResources>> writers;
context.copiedWriters = &writers;
struct Release {
const Context& context;
Recorder& recorder;
std::uint32_t* block;
std::size_t bytes;
std::vector<std::shared_ptr<ShaderResources>>& writers;
VkPipeline pipeline = VK_NULL_HANDLE;
VkPipelineLayout layout = VK_NULL_HANDLE;
VkShaderModule module = VK_NULL_HANDLE;
std::shared_ptr<ShaderResources> layoutOwner;
~Release() {
recorder.Sync();
writers.clear();
DrawCopiedWriters()->clear();
if (pipeline) context.Function<PFN_vkDestroyPipeline>("vkDestroyPipeline")(context.device, pipeline, nullptr);
if (layout) context.Function<PFN_vkDestroyPipelineLayout>("vkDestroyPipelineLayout")(context.device, layout, nullptr);
if (module) context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, module, nullptr);
{
GuestAllocations::Mutation mutation;
mutation.Remove(block);
}
static_cast<void>(HostImportFor(context, reinterpret_cast<std::uint64_t>(block), bytes));
recorder.Sync();
ReleaseWatched(block, bytes);
}
} release{context, recorder, block, bytes, writers};
{
GuestAllocations::Mutation mutation;
mutation.Add(block, bytes, true, true, true);
}
std::memset(block, 0, bytes);
Require(HostImportFor(context, address, bytes) != nullptr, "import copied/direct ordering memory");
ShaderRecompiler::RecompileResult program;
ShaderRecompiler::DescriptorBinding binding;
binding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer;
binding.role = ShaderRecompiler::DescriptorRole::GuestBuffers;
binding.descriptorSet = 0;
binding.binding = 0;
binding.count = 1;
binding.guestDescriptor = {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u) & 0xffffu, 1024, 0x31000000u};
binding.bufferWritten = {true};
program.bindings.push_back(binding);
const CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0};
auto direct = std::make_shared<ShaderResources>(context, compute);
release.layoutOwner = direct;
Require(!direct->HasCopiedWrites(), "ordering consumer did not bind directly");
const auto setLayout = direct->Layout();
const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(std::uint32_t)};
VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
layoutInfo.setLayoutCount = 1;
layoutInfo.pSetLayouts = &setLayout;
layoutInfo.pushConstantRangeCount = 1;
layoutInfo.pPushConstantRanges = &push;
Check(context.Function<PFN_vkCreatePipelineLayout>("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &release.layout), "ordering pipeline layout");
VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
moduleInfo.codeSize = sizeof(BUFFER_UPDATE_SPV);
moduleInfo.pCode = BUFFER_UPDATE_SPV;
Check(context.Function<PFN_vkCreateShaderModule>("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &release.module), "ordering shader module");
VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO};
pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0, VK_SHADER_STAGE_COMPUTE_BIT, release.module, "main", nullptr};
pipelineInfo.layout = release.layout;
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &release.pipeline), "ordering pipeline");
const auto update = [&](const std::shared_ptr<ShaderResources>& resources, std::uint32_t delta, std::uint32_t count = 1) {
const auto commands = recorder.Commands();
recorder.Keep(resources);
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_MEMORY_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT);
context.Function<PFN_vkCmdBindPipeline>("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, release.pipeline);
resources->Bind(commands, VK_PIPELINE_BIND_POINT_COMPUTE, release.layout);
context.Function<PFN_vkCmdPushConstants>("vkCmdPushConstants")(commands, release.layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(delta), &delta);
context.Function<PFN_vkCmdDispatch>("vkCmdDispatch")(commands, count, 1, 1);
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT | VK_ACCESS_HOST_READ_BIT);
resources->MarkGpuWrites(recorder);
};
for (int mode = 0; mode < 3; ++mode) {
*block = 10;
auto copiedContext = context;
copiedContext.hostImportAlignment = 0;
copiedContext.dmaBufImport = false;
auto copied = std::make_shared<ShaderResources>(copiedContext, compute);
Require(copied->HasCopiedWrites(), "ordering producer did not take the copied path");
auto& pending = mode == 2 ? *DrawCopiedWriters() : writers;
pending.push_back(copied);
update(copied, 1);
recorder.OnComplete([copied, &pending] {
copied->WriteBackBuffers();
std::erase(pending, copied);
});
Require(!recorder.Idle() && *block == 10, "copied producer was completed before the consumer test");
if (mode == 0) direct = std::make_shared<ShaderResources>(context, compute);
else Require(direct->Revalidate(std::span(&compute, 1)), "the imported template could not be revalidated");
update(direct, 2);
recorder.Sync();
Require(*block == 13, mode == 0 ? "fresh direct consumer ran before copied write-back" : "reused direct consumer ran before copied write-back");
}
*block = 20;
block[1024] = 30;
auto copiedContext = context;
copiedContext.hostImportAlignment = 0;
copiedContext.dmaBufImport = false;
auto copied = std::make_shared<ShaderResources>(copiedContext, compute);
writers.push_back(copied);
update(copied, 1);
recorder.OnComplete([copied, &writers] {
copied->WriteBackBuffers();
std::erase(writers, copied);
});
auto separateProgram = program;
separateProgram.bindings[0].guestDescriptor[0] += 4096;
const CompiledShader separateCompute{ShaderRecompiler::ShaderStage::Compute, &separateProgram, 0};
auto separate = std::make_shared<ShaderResources>(context, separateCompute);
Require(!writers.empty() && *block == 20, "a disjoint consumer waited for copied writes");
update(separate, 2);
recorder.Sync();
Require(*block == 21 && block[1024] == 32, "disjoint copied and direct writes changed each other");
const std::uint32_t label = 40;
static_cast<void>(recorder.Commands());
recorder.AfterCompletions(address, std::as_bytes(std::span(&label, 1)), 1000, 0, false);
Require(direct->Revalidate(std::span(&compute, 1)), "template reuse after a CPU completion label failed");
update(direct, 2);
recorder.Sync();
Require(*block == 42, "direct consumer preceded an overlapping CPU completion label");
update(direct, 1);
const auto submissions = recorder.Submissions();
Require(direct->Revalidate(std::span(&compute, 1)) && recorder.Submissions() == submissions, "ordinary direct work incurred a CPU write-back wait");
update(direct, 2);
recorder.Sync();
Require(*block == 45, "direct consumers lost GPU-ordered updates");
{
TextureDetiler detiler(context);
auto imageContext = context;
imageContext.detiler = &detiler;
TextureCache textureCache(imageContext);
imageContext.textureCache = &textureCache;
Buffer readback(context, 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT);
struct Clear {
const Context& context;
Recorder& recorder;
~Clear() { recorder.Sync(); ClearCachedTextures(context.device); }
} clear{context, recorder};
ShaderRecompiler::RecompileResult imageProgram;
ShaderRecompiler::DescriptorBinding imageBinding;
imageBinding.kind = ShaderRecompiler::DescriptorKind::SampledImage;
imageBinding.role = ShaderRecompiler::DescriptorRole::GuestImages;
imageBinding.descriptorSet = 0;
imageBinding.binding = ShaderRecompiler::RuntimeAbi::FirstImageBinding;
imageBinding.count = 1;
imageBinding.imageSamplers = {0};
imageBinding.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D;
imageBinding.guestDescriptor = {static_cast<std::uint32_t>(address >> 8), static_cast<std::uint32_t>(address >> 40) | (56u << 20) | (3u << 30), 15u | (63u << 14), 0x91b00fac, 0, 0, 0, 0};
imageProgram.bindings.push_back(imageBinding);
const CompiledShader imageCompute{ShaderRecompiler::ShaderStage::Compute, &imageProgram, 0};
auto sampled = std::make_shared<ShaderResources>(imageContext, imageCompute);
recorder.Sync();
for (int reuse = 0; reuse < 2; ++reuse) {
auto producer = std::make_shared<ShaderResources>(copiedContext, compute);
writers.push_back(producer);
update(producer, 1);
recorder.OnComplete([producer, &writers] {
producer->WriteBackBuffers();
std::erase(writers, producer);
});
if (reuse == 0) sampled = std::make_shared<ShaderResources>(imageContext, imageCompute);
else Require(sampled->Revalidate(std::span(&imageCompute, 1)), "sampled image template did not revalidate");
const auto images = sampled->StorageImages();
Require(images.size() == 1, "sampled image did not use imported texture storage");
const auto commands = recorder.Commands();
recorder.Keep(sampled);
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT);
VkBufferImageCopy copy{};
copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
copy.imageExtent = {1, 1, 1};
context.Function<PFN_vkCmdCopyImageToBuffer>("vkCmdCopyImageToBuffer")(commands, images.front().first, VK_IMAGE_LAYOUT_GENERAL, readback.Handle(), 1, &copy);
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT);
recorder.Sync();
std::uint32_t value = 0;
std::memcpy(&value, readback.Bytes().data(), sizeof(value));
Require(value == static_cast<std::uint32_t>(46 + reuse), "cached sampled image preceded a copied producer");
}
sampled.reset();
ClearCachedTextures(context.device);
const auto keys = address + 65536;
const auto keyBytes = DccKeyBytes(65536);
imageProgram.bindings[0].guestDescriptor[6] = (1u << 21) | (static_cast<std::uint32_t>((keys >> 8) & 0xffu) << 24);
imageProgram.bindings[0].guestDescriptor[7] = static_cast<std::uint32_t>(keys >> 16);
auto keyProgram = program;
keyProgram.bindings[0].guestDescriptor = {static_cast<std::uint32_t>(keys), static_cast<std::uint32_t>(keys >> 32u) & 0xffffu, static_cast<std::uint32_t>(keyBytes), 0x31000000u};
const CompiledShader keyCompute{ShaderRecompiler::ShaderStage::Compute, &keyProgram, 0};
for (int reuse = 0; reuse < 2; ++reuse) {
std::memset(reinterpret_cast<void*>(keys), 0, keyBytes);
AgcDriver::GuestMemory::MarkWritten(keys, keyBytes);
sampled = std::make_shared<ShaderResources>(imageContext, imageCompute);
recorder.Sync();
auto producer = std::make_shared<ShaderResources>(copiedContext, keyCompute);
writers.push_back(producer);
update(producer, 0x40404040, static_cast<std::uint32_t>(keyBytes / 4));
recorder.OnComplete([producer, &writers] {
producer->WriteBackBuffers();
std::erase(writers, producer);
});
if (reuse == 0) sampled = std::make_shared<ShaderResources>(imageContext, imageCompute);
else Require(sampled->Revalidate(std::span(&imageCompute, 1)), "metadata-only producer invalidated the sampled template");
const auto images = sampled->StorageImages();
Require(images.size() == 1, "metadata texture is not resident");
const auto commands = recorder.Commands();
recorder.Keep(sampled);
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT);
VkBufferImageCopy copy{};
copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
copy.imageExtent = {1, 1, 1};
context.Function<PFN_vkCmdCopyImageToBuffer>("vkCmdCopyImageToBuffer")(commands, images.front().first, VK_IMAGE_LAYOUT_GENERAL, readback.Handle(), 1, &copy);
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT);
recorder.Sync();
std::uint32_t value = 0;
std::memcpy(&value, readback.Bytes().data(), sizeof(value));
Require(value == 0xff000000, "cached sampled image ignored copied compression-metadata writes");
}
}
std::cout << "Copied producers precede fresh and reused direct consumers\n";
}
void readTrackingTests(const Device& device, Recorder& recorder) {
Require(Recorder::ReadTracking(), "read tracking is off (APS5_COPY_READ_TRACKING=0 set?)");
Require(recorder.Idle() && !recorder.PendingReadOverlaps(0x10000, 16), "a fresh recorder reports a read");
@ -1310,41 +1072,6 @@ void hostImportRetirementTests(const Device& device) {
Require(preservedEveryRecorder, "unlocked mapping changed recorder batches");
std::cout << "Host import retirement locking and GPU readback tests passed\n";
}
#ifdef _WIN32
void secondMappingImportTests(const Device& device) {
const auto& context = device.GetContext();
if (context.hostImportAlignment == 0) {
std::cout << "host imports unavailable: imports after a second mapping not tested\n";
return;
}
constexpr std::size_t bytes = 65536;
constexpr std::size_t page = 16384;
struct Release {
void* mapping = nullptr;
void* alias = nullptr;
std::int64_t physical = -1;
~Release() {
if (alias != nullptr) sceKernelMunmap(alias, page);
if (mapping != nullptr) sceKernelMunmap(mapping, bytes);
if (physical >= 0) sceKernelReleaseDirectMemory(physical, bytes);
}
} release;
Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, bytes, 0, &release.physical) == 0, "allocate the second mapping test backing");
Require(sceKernelMapDirectMemory(&release.mapping, bytes, 0x33, 0, release.physical, bytes) == 0, "map the second mapping test backing");
const auto address = reinterpret_cast<std::uint64_t>(release.mapping);
if (HostImportFor(context, address, bytes) == nullptr) {
std::cout << "host import of singly mapped direct memory refused: imports after a second mapping not tested\n";
return;
}
const auto first = HostImportSerial(context, address, bytes, true);
Require(first != 0 && HostImportSerial(context, address, bytes, true) == first, "singly mapped direct memory lost its import");
Require(sceKernelMapDirectMemory(&release.alias, page, 0x33, 0, release.physical, page) == 0, "map the first page a second time");
Require(HostImportFor(context, address, bytes) != nullptr, "direct memory was not imported again after a second mapping of its first page");
const auto second = HostImportSerial(context, address, bytes, true);
Require(second != 0 && second != first, "the import of direct memory whose pages were replaced was kept");
HostImportFor(context, address, bytes);
}
#endif
void movedMetadataTests(const Device& device, Recorder& recorder) {
const auto& base = device.GetContext();
@ -2269,19 +1996,6 @@ void drawSnapshotCapTests(const Device& device, Recorder& recorder) {
Require(inPlace[0] == std::byte{0x33} && std::all_of(inPlace, inPlace + largeBytes, [](std::byte value) { return value == std::byte{0x33}; }), "in-place bound input did not observe CPU write");
std::memset(block, 0x11, bytes);
snapshotRecorder.Sync();
#ifdef _WIN32
Require(_putenv_s("APS5_DRAW_SNAPSHOT_MAX_MIB", "64") == 0, "cannot raise snapshot cap");
#else
Require(setenv("APS5_DRAW_SNAPSHOT_MAX_MIB", "64", 1) == 0, "cannot raise snapshot cap");
#endif
const auto again = resources.PrepareDrawBindings(snapshotRecorder);
Require(again != nullptr && again->snapshots.size() == 1 && again->snapshots[0].address == smallAddress, "built resources read the draw snapshot cap again instead of keeping the one they were built with");
snapshotRecorder.Sync();
#ifdef _WIN32
Require(_putenv_s("APS5_DRAW_SNAPSHOT_MAX_MIB", "1") == 0, "cannot restore snapshot cap");
#else
Require(setenv("APS5_DRAW_SNAPSHOT_MAX_MIB", "1", 1) == 0, "cannot restore snapshot cap");
#endif
}
const auto firstAddress = address + 4096;
constexpr std::size_t firstBytes = 600 * 1024;
@ -5103,12 +4817,6 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) {
context.nullDescriptors = true;
TextureCache cache(context);
context.textureCache = &cache;
const auto clearTargets = [&] {
recorder.Submit();
device.WaitQueue();
recorder.Sync();
ClearMultisampleTargets(context.device);
};
const auto refused = [](const std::function<void()>& access, const std::string& expected, const std::string& what) {
std::string message;
try {
@ -5165,7 +4873,7 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) {
}, "a texture read", "a texture read");
refused([&] { AgcDriver::GuestMemory::Read(address + 3u * 65536u + 256u, texels); }, "a CPU read", "a CPU read in the padding of a later sample plane");
AgcDriver::GuestMemory::Read(address + 4u * 65536u, texels);
clearTargets();
ClearMultisampleTargets(context.device);
target.tileMode = ColorTileMode::RenderTarget;
{
std::lock_guard gpu(GpuMutex());
@ -5176,7 +4884,7 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) {
}
refused([&] { AgcDriver::GuestMemory::Read(address + targetBytes - texels.size(), texels); }, "a CPU read", "a CPU read inside the tiled multisampled target");
AgcDriver::GuestMemory::Read(address + targetBytes, texels);
clearTargets();
ClearMultisampleTargets(context.device);
if ((base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_2_BIT) != 0) {
target.extent = {128, 64};
target.surfaceExtent = target.extent;
@ -5190,7 +4898,7 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) {
}
refused([&] { AgcDriver::GuestMemory::Read(address + 2u * targetBytes - texels.size(), texels); }, "a CPU read", "a CPU read inside the 2x tile-height padding");
AgcDriver::GuestMemory::Read(address + 2u * targetBytes, texels);
clearTargets();
ClearMultisampleTargets(context.device);
}
if ((base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_8_BIT) != 0) {
target.extent = {64, 32};
@ -5205,48 +4913,9 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) {
}
refused([&] { AgcDriver::GuestMemory::Read(address + 2u * targetBytes - texels.size(), texels); }, "a CPU read", "a CPU read inside the 8x tile-height padding");
AgcDriver::GuestMemory::Read(address + 2u * targetBytes, texels);
clearTargets();
ClearMultisampleTargets(context.device);
}
AgcDriver::GuestMemory::Read(address + 256u, texels);
target.extent = {side, side};
target.surfaceExtent = target.extent;
target.samples = 4;
VkImageView previous = VK_NULL_HANDLE;
for (const bool removeAllocation : {false, true}) {
VkImageView view = VK_NULL_HANDLE;
{
std::lock_guard gpu(GpuMutex());
view = MultisampleTargetView(context, target);
Require(view != VK_NULL_HANDLE && view != previous, "a remapped allocation reused its retired multisampled image");
recorder.Submit();
device.WaitQueue();
recorder.Sync();
}
{
GuestAllocations::Mutation mutation;
mutation.Protect(block, bytes, true, true, true, [] {});
}
refused([&] { RequireNoMultisampleTarget(address, 64u, "a CPU read"); }, "a CPU read", "a read after protection changed");
bool failed = false;
try {
GuestAllocations::Mutation mutation;
mutation.Unmap(block, bytes, [](const void*, std::size_t, const void*, bool) { throw std::runtime_error("unmap failed"); });
} catch (const std::runtime_error&) {
failed = true;
}
Require(failed, "the failed host unmap did not throw");
refused([&] { RequireNoMultisampleTarget(address, 64u, "a CPU read"); }, "a CPU read", "a read after a failed unmap");
{
GuestAllocations::Mutation mutation;
if (removeAllocation) mutation.Remove(block);
else mutation.Unmap(block, bytes, [](const void*, std::size_t, const void*, bool) {});
mutation.Add(block, bytes, true, true, true);
}
RequireNoMultisampleTarget(address, bytes, "a texture read");
AgcDriver::GuestMemory::Read(address + 256u, texels);
previous = view;
}
clearTargets();
std::cout << "Multisampled target guest access tests passed\n";
}
@ -5615,165 +5284,6 @@ void atomicViewTests(const Device& device, Recorder& recorder) {
}
}
bool vertexTargetRangeTests(const Device& device, Recorder& recorder, bool imported) {
if (!device.Graphics() || (imported && device.GetContext().hostImportAlignment == 0)) return false;
constexpr std::size_t unit = 65536;
constexpr std::size_t bytes = unit * 3;
void* watched = AllocateWatched(bytes, unit);
std::vector<std::byte> fallback(watched == nullptr ? bytes + unit : 0);
auto* block = watched != nullptr ? static_cast<std::byte*>(watched) : reinterpret_cast<std::byte*>((reinterpret_cast<std::uintptr_t>(fallback.data()) + unit - 1) & ~(unit - 1));
struct Release {
void* watched;
~Release() { if (watched != nullptr) ReleaseWatched(watched, bytes); }
} release{watched};
std::fill_n(block, bytes, std::byte{0});
const auto address = reinterpret_cast<std::uint64_t>(block);
auto context = device.GetContext();
context.recorder = &recorder;
if (!imported) {
context.hostImportAlignment = 0;
context.dmaBufImport = false;
}
TextureDetiler detiler(context);
context.detiler = &detiler;
{
GuestAllocations::Mutation mutation;
mutation.Add(block, bytes, true, true, true);
}
struct Cleanup {
const Context& context;
Recorder& recorder;
void* block;
~Cleanup() {
recorder.Sync();
WaitForOptimizedPipelines(context.device);
SharedResourceCache().Clear();
ClearCachedTextures(context.device);
recorder.Sync();
ClearCachedPipelines(context.device);
{
GuestAllocations::Mutation mutation;
mutation.Remove(block);
}
HostImportFor(context, reinterpret_cast<std::uint64_t>(block), bytes);
}
} cleanup{context, recorder, block};
if (imported && HostImportFor(context, address, bytes) == nullptr) return false;
const auto shader = [](auto& words, std::uint64_t variant) {
ShaderRecompiler::RecompileResult program;
program.spirv = std::vector<std::uint32_t>(std::begin(words), std::end(words));
program.variantId = variant;
return program;
};
auto producerVertex = shader(COVER_VERT_SPV, 101);
auto producerPixel = shader(VERTEXRANGEPRODUCER_FRAG_SPV, 102);
auto vertex = shader(VERTEXRANGE_VERT_SPV, 103);
auto pixel = shader(COVER_FRAG_SPV, 104);
const std::array producerShaders{CompiledShader{ShaderRecompiler::ShaderStage::Vertex, &producerVertex, 0}, CompiledShader{ShaderRecompiler::ShaderStage::Fragment, &producerPixel, 0}};
const std::array shaders{CompiledShader{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, CompiledShader{ShaderRecompiler::ShaderStage::Fragment, &pixel, 0}};
const auto stateFor = [](std::uint64_t base, VkFormat format, std::uint32_t elementBytes) {
State state{};
state.stages.path = ShaderPath::Vertex;
state.color = {base, {64, 64}, format, unit, 0xe4u, ColorTileMode::RenderTarget, elementBytes};
state.colors = {state.color};
state.hasColorTarget = true;
state.renderExtent = {64, 64};
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
state.viewport = {0, 64, 64, -64, 0, 1};
state.scissor = {{0, 0}, {64, 64}};
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
state.blend.colorWriteMask = 15;
state.blends = {state.blend};
return state;
};
const auto producer = stateFor(address + unit, VK_FORMAT_R32G32B32A32_SFLOAT, 16);
const auto consumer = stateFor(address, VK_FORMAT_R8G8B8A8_UNORM, 4);
const AgcDriver::Pm4::DrawParameters triangle{0, 3, 0, 1, 0, false};
const auto produce = [&] {
Draw(context, producer, triangle, producerShaders);
WaitForOptimizedPipelines(context.device);
const std::array range{std::pair{address + unit, address + 2 * unit}};
Require(StorageTexture::AnyPendingOverlaps(range), "the color producer did not leave GPU results pending");
};
const auto attributeFor = [](std::uint64_t base, std::uint32_t stride, std::uint32_t fetchIndex) {
return ShaderRecompiler::VertexAttribute{0, 4, {{static_cast<std::uint32_t>(base), static_cast<std::uint32_t>(base >> 32u) | (stride << 16u), static_cast<std::uint32_t>(bytes / 16), 77u << 12u}}, fetchIndex};
};
const auto check = [&](std::uint32_t edge) {
std::vector<std::byte> texels(64 * 64 * 4);
ReadColorTarget(consumer.color, texels);
for (std::size_t y = 0; y < 64; ++y) {
for (std::size_t x = 0; x < 64; ++x) {
const auto expected = x < edge ? std::byte{0} : std::byte{255};
for (std::size_t channel = 0; channel < 4; ++channel) Require(texels[(y * 64 + x) * 4 + channel] == expected, "the draw did not consume the GPU-produced vertex offset");
}
}
};
const auto reject = [&](auto action, const char* expected) {
try { action(); }
catch (const std::runtime_error& error) {
Require(std::string(error.what()).find(expected) != std::string::npos, error.what());
return;
}
throw std::runtime_error("an actual render-target alias was accepted");
};
for (const std::uint32_t fetchIndex : {0u, 1u}) {
producerPixel = shader(VERTEXRANGEPRODUCER_FRAG_SPV, 102);
vertex.vertexAttributes = {attributeFor(address, 16, fetchIndex)};
auto draw = triangle;
if (fetchIndex == 0) draw.firstVertex = unit / 16;
else draw.firstInstance = unit / 16;
produce();
std::shared_ptr<const AgcDriver::DrawRecipe> recipe;
Draw(context, consumer, draw, shaders, {}, &recipe);
WaitForOptimizedPipelines(context.device);
const auto read = recorder.DescribePendingRead(address + unit, 16);
Require(read.has_value() == imported && (!read || read->kind == Recorder::ReadKind::DrawInput), "the vertex regression used the wrong copied/imported path");
check(16);
Require(recipe != nullptr, "the range regression did not create a draw recipe");
std::vector<std::byte> cleared(unit);
AgcDriver::GuestMemory::Write(address, cleared);
for (std::size_t offset = 5; offset < producerPixel.spirv.size();) {
const auto count = producerPixel.spirv[offset] >> 16u;
Require(count != 0, "invalid producer SPIR-V");
if ((producerPixel.spirv[offset] & 0xffffu) == 43u && count == 4 && producerPixel.spirv[offset + 3] == 0x3f000000u) producerPixel.spirv[offset + 3] = 0x3f400000u;
offset += count;
}
producerPixel.variantId = 105;
produce();
const auto hit = DrawWithRecipe(context, consumer, draw, shaders, {}, *recipe);
WaitForOptimizedPipelines(context.device);
Require(hit.recorded, "the vertex range regression did not reuse its draw recipe");
check(24);
vertex.vertexAttributes = {attributeFor(address + unit, 16, fetchIndex)};
Draw(context, consumer, triangle, shaders);
WaitForOptimizedPipelines(context.device);
check(24);
vertex.vertexAttributes = {attributeFor(address, 16, fetchIndex)};
auto indirect = draw;
indirect.indirect.emplace();
reject([&] { Draw(context, consumer, indirect, shaders); }, "vertex buffer aliases the render target");
if (fetchIndex == 0) {
const std::array<std::uint16_t, 3> indices{0, 1, 2};
AgcDriver::GuestMemory::Write(address + 2 * unit, std::as_bytes(std::span(indices)));
auto indexed = draw;
indexed.indexed = true;
indexed.indexSize = 2;
indexed.indexAddress = address + 2 * unit;
produce();
Draw(context, consumer, indexed, shaders);
WaitForOptimizedPipelines(context.device);
check(24);
indexed.indexAddress = address;
reject([&] { Draw(context, consumer, indexed, shaders); }, "index buffer aliases the render target");
}
reject([&] { Draw(context, consumer, triangle, shaders); }, "vertex buffer aliases the render target");
vertex.vertexAttributes = {attributeFor(address, 0, fetchIndex)};
reject([&] { Draw(context, consumer, draw, shaders); }, "vertex buffer aliases the render target");
}
std::cout << "Vertex range and pending color producer tests passed (" << (imported ? "imported" : "copied") << ")\n";
return true;
}
void pipelineCacheTests(const Device& device, bool benchmark) {
std::thread establishThreadedProcess([] {});
establishThreadedProcess.join();
@ -6206,148 +5716,6 @@ void pipelineLibraryTests(const Device& device) {
std::cout << "Pipeline library reuse tests passed\n";
}
#ifdef _WIN32
namespace StorageCpuSnapshotTest {
constexpr std::uint32_t Side = 256;
constexpr std::uint32_t Format32UInt = 20;
constexpr std::uint32_t TileR64KBX = 0x1b;
constexpr std::uint32_t Type2D = 9;
constexpr std::size_t SurfaceBytes = std::size_t{Side} * Side * 4u;
constexpr std::size_t BlockBytes = 12 * SurfaceBytes;
class Block {
public:
explicit Block(const Context& context, bool watched = false) : context(context), watched(watched) {
if (watched) {
memory = GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536);
const auto section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, BlockBytes, nullptr);
Require(section != nullptr, "cannot allocate the shared texture section");
try { GuestArena::GuestArenaMap_nid_postfix(memory, BlockBytes, section, 0, PAGE_READWRITE); }
catch (...) { CloseHandle(section); throw; }
CloseHandle(section);
} else memory = VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
Require(memory != nullptr, "cannot allocate the texture block");
std::memset(memory, 0x5a, BlockBytes);
GuestAllocations::Mutation().Add(memory, BlockBytes, true, true, true);
}
~Block() {
ClearCachedTextures(context.device);
GuestAllocations::Mutation().Remove(memory);
HostImportFor(context, Surface(0), BlockBytes);
if (watched) {
GuestArena::GuestArenaReset_nid_postfix(memory, BlockBytes);
GuestArena::GuestArenaRelease_nid_postfix(memory, BlockBytes);
} else VirtualFree(memory, 0, MEM_RELEASE);
}
std::uint64_t Surface(std::size_t surface) const { return reinterpret_cast<std::uint64_t>(memory) + surface * SurfaceBytes; }
private:
const Context& context;
bool watched;
void* memory = nullptr;
};
std::array<std::uint32_t, 8> Descriptor(std::uint64_t address, std::uint32_t side = Side) {
return {
static_cast<std::uint32_t>(address >> 8u),
static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((side - 1u) & 3u) << 30u),
((side - 1u) >> 2u) | ((side - 1u) << 14u),
0xfacu | (TileR64KBX << 20u) | (Type2D << 28u),
0u,
0u,
0u,
0u,
};
}
GuestTextureResource Resource(std::uint64_t address, std::uint32_t side = Side) {
const auto words = Descriptor(address, side);
return DecodeTextureResource(words);
}
void Run(Context context) {
TextureDetiler detiler(context);
context.detiler = &detiler;
for (const bool watched : {true, false}) {
Block block(context, watched);
const auto address = block.Surface(0);
Require(AgcDriver::GuestMemory::Watched(address, SurfaceBytes) == watched, "snapshot fixture has the wrong write tracking");
Require(HostImportFor(context, address, SurfaceBytes) != nullptr, "snapshot fixture requires a host import");
Require(AgcDriver::GuestMemory::Watched(address, SurfaceBytes) == watched, "host import changed the fixture's write tracking");
auto image = StorageTexture::Create(context, *context.detiler, Resource(address), 0);
const auto expectedBytes = watched ? 0u : SurfaceBytes;
Require(image->OriginalSnapshotBytes() == expectedBytes, "watched GPU-direct upload retained an unnecessary CPU surface snapshot");
const auto readPixel = [&] {
Buffer result(context, 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT);
CommandBatch batch(context);
RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT);
VkBufferImageCopy copy{};
copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
copy.imageExtent = {1, 1, 1};
context.Function<PFN_vkCmdCopyImageToBuffer>("vkCmdCopyImageToBuffer")(batch.Handle(), image->Image(), VK_IMAGE_LAYOUT_GENERAL, result.Handle(), 1, &copy);
RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT);
batch.SubmitAndWait();
result.Invalidate();
std::uint32_t pixel = 0;
std::memcpy(&pixel, result.Bytes().data(), sizeof(pixel));
return pixel;
};
Require(readPixel() == 0x5a5a5a5au, "snapshot upload returned incorrect GPU texels");
std::memset(reinterpret_cast<void*>(address), 0x6b, SurfaceBytes);
image->Refresh();
Require(readPixel() == 0x6b6b6b6bu, "snapshot refresh missed a CPU write");
Require(image->OriginalSnapshotBytes() == expectedBytes, "watched refresh allocated a CPU surface snapshot");
{
CommandBatch batch(context);
RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT);
VkClearColorValue clear{};
clear.uint32[0] = 0x7c7c7c7cu;
const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
context.Function<PFN_vkCmdClearColorImage>("vkCmdClearColorImage")(batch.Handle(), image->Image(), VK_IMAGE_LAYOUT_GENERAL, &clear, 1, &range);
batch.SubmitAndWait();
}
image->MarkDirty();
image->WriteBack();
std::uint32_t pixel = 0;
AgcDriver::GuestMemory::ReadCommitted(address, std::as_writable_bytes(std::span(&pixel, 1)));
Require(pixel == 0x7c7c7c7cu, "snapshot write-back lost GPU texels");
Require(image->OriginalSnapshotBytes() == expectedBytes, "GPU-direct write-back allocated a CPU surface snapshot");
if (watched) {
AgcDriver::GuestMemory::Unwatch(address, SurfaceBytes);
Require(!AgcDriver::GuestMemory::Watched(address, SurfaceBytes), "snapshot fixture is still write-watched");
std::memset(reinterpret_cast<void*>(address), 0x8d, SurfaceBytes);
image->Refresh();
Require(image->OriginalSnapshotBytes() == SurfaceBytes, "unwatched refresh did not allocate its comparison snapshot");
Require(readPixel() == 0x8d8d8d8du, "unwatched refresh returned incorrect GPU texels");
}
image.reset();
Context cpu = context;
cpu.hostImportAlignment = 0;
image = StorageTexture::Create(cpu, *context.detiler, Resource(address), 0);
Require(image->OriginalSnapshotBytes() == SurfaceBytes, "CPU upload has no guest-byte snapshot");
const auto expectedPixel = watched ? 0x8d8d8d8du : 0x7c7c7c7cu;
Require(readPixel() == expectedPixel, "CPU upload returned incorrect GPU texels");
{
CommandBatch batch(context);
VkClearColorValue clear{};
clear.uint32[0] = 0x9e9e9e9eu;
const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT);
context.Function<PFN_vkCmdClearColorImage>("vkCmdClearColorImage")(batch.Handle(), image->Image(), VK_IMAGE_LAYOUT_GENERAL, &clear, 1, &range);
batch.SubmitAndWait();
}
image->MarkDirty();
image->WriteBack();
AgcDriver::GuestMemory::ReadCommitted(address, std::as_writable_bytes(std::span(&pixel, 1)));
Require(pixel == 0x9e9e9e9eu, "CPU write-back lost guest texels");
std::printf("%s snapshot fixture: direct %zu bytes, CPU fallback %zu bytes\n", watched ? "watched" : "unwatched", expectedBytes, image->OriginalSnapshotBytes());
}
}
}
#endif
int main(int argc, char** argv) {
try {
std::unique_ptr<Device> created;
@ -6382,25 +5750,9 @@ int main(int argc, char** argv) {
return 0;
}
std::unique_lock gpu(GpuMutex());
#ifdef _WIN32
if (argc == 2 && std::string_view(argv[1]) == "--storage-cpu-snapshot-only") {
if (device.GetContext().hostImportAlignment == 0) {
Require(std::getenv("ANYPS5_REQUIRE_VULKAN") == nullptr, "storage snapshot test requires host imports");
std::cout << "skipped, host imports unavailable\n";
return 77;
}
StorageCpuSnapshotTest::Run(device.GetContext());
std::cout << "Storage CPU snapshot tests passed\n";
return 0;
}
#endif
std::cout << "host imports " << (PrepareImportWatch(device.GetContext()) == ImportWatch::Unwatch ? "are compared" : "stay watched") << '\n';
Recorder recorder(device.GetContext());
recorder.Activate();
if (argc == 2 && std::string_view(argv[1]) == "--copied-direct-only") {
copiedDirectOrderingTests(device, recorder);
return 0;
}
if (argc == 2 && (std::string_view(argv[1]) == "--storage-upload-lifetime-only" || std::string_view(argv[1]) == "--storage-upload-failure-only")) {
storageUploadLifetimeTests(device, recorder, std::string_view(argv[1]) == "--storage-upload-failure-only");
return 0;
@ -6480,16 +5832,6 @@ int main(int argc, char** argv) {
return 0;
}
if (argc == 2 && (std::string_view(argv[1]) == "--vertex-target-range-copied" || std::string_view(argv[1]) == "--vertex-target-range-imported")) {
return vertexTargetRangeTests(device, recorder, std::string_view(argv[1]) == "--vertex-target-range-imported") ? 0 : 77;
}
#ifdef _WIN32
if (argc == 2 && std::string_view(argv[1]) == "--host-import-second-mapping-only") {
secondMappingImportTests(device);
return 0;
}
#endif
readTrackingTests(device, recorder);
writeSettledTests(device, recorder);
completionCountTests(device, recorder);
@ -6520,9 +5862,6 @@ int main(int argc, char** argv) {
storeRunTests(device, recorder);
queuedWriteGroupTests();
remappedImportTests(device);
#ifdef _WIN32
secondMappingImportTests(device);
#endif
movedMetadataTests(device, recorder);
viewPastLastMipTests(device, recorder);
keysFillTests(device, recorder);
@ -6552,7 +5891,6 @@ int main(int argc, char** argv) {
cmaskPassTests(device, recorder);
pendingKeyStoreTests(device, recorder);
sampleDumpTests(device, recorder);
copiedDirectOrderingTests(device, recorder);
gpu.unlock();
hostImportUnmapTests(device, recorder);
drawSnapshotPatchTests(device);

View File

@ -1,140 +0,0 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp"
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
#include <algorithm>
#include <array>
#include <cstdio>
#include <random>
#include <stdexcept>
#include <type_traits>
using namespace AgcDriver;
using namespace AgcDriver::DriverDetail;
namespace {
void check(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
std::uint64_t baselineRegisterStateKey(const QueueState& queue, bool allUserWords = false) {
std::uint64_t key = 0xcbf29ce484222325ull;
const auto mix = [&](std::uint64_t value) {
key ^= value;
key *= 0x100000001b3ull;
};
const auto userEnd = [&](std::uint32_t base) {
if (allUserWords) return base + 32u;
const auto resources = queue.shader.find(base - 1);
if (resources == queue.shader.end()) return base;
const auto count = ((resources->second >> 1u) & 0x1fu) | (((resources->second >> 27u) & 1u) << 5u);
return base + std::min(count, 32u);
};
const std::array<std::pair<std::uint32_t, std::uint32_t>, 3> users{{{0x00cu, userEnd(0x00cu)}, {0x08cu, userEnd(0x08cu)}, {0x10cu, userEnd(0x10cu)}}};
const auto unread = [&](std::uint32_t offset) {
return std::any_of(users.begin(), users.end(), [&](const auto& user) { return offset >= user.second && offset < user.first + 32u; });
};
for (const auto& range : Graphics::DrawKeyRegisters) {
const auto& bank = range.bank == Graphics::RegisterBank::Context ? queue.context : range.bank == Graphics::RegisterBank::Shader ? queue.shader : queue.userConfig;
mix((static_cast<std::uint64_t>(range.bank) << 32u) | range.first);
const auto end = range.first + range.count;
const bool shader = range.bank == Graphics::RegisterBank::Shader;
for (auto it = bank.lower_bound(range.first); it != bank.end() && it->first < end; ++it) {
if (shader && unread(it->first)) continue;
mix(it->first);
mix(it->second);
}
}
return key;
}
void testFingerprints() {
static_assert(!std::is_convertible_v<Registers::Reference, std::uint32_t&>);
std::mt19937 random(42);
QueueState queue;
auto reference = baselineRegisterStateKey(queue);
auto tracked = RegisterStateKey(queue);
const auto compare = [&] {
const auto expected = baselineRegisterStateKey(queue);
const auto actual = RegisterStateKey(queue);
check(actual == RegisterStateKey(queue, false, false), "incremental fingerprint missed a mutation");
check(RegisterStateKey(queue, true) == RegisterStateKey(queue, true, false), "all-user fingerprint missed a mutation");
check((expected == reference) == (actual == tracked), "exact key changed its dependency set");
reference = expected;
tracked = actual;
};
for (const auto& range : Graphics::DrawKeyRegisters) {
auto& bank = range.bank == Graphics::RegisterBank::Context ? queue.context : range.bank == Graphics::RegisterBank::Shader ? queue.shader : queue.userConfig;
for (auto offset = range.first; offset < range.first + range.count; ++offset) {
bank.erase(offset); compare();
bank.emplace(offset, 0); compare();
bank.insert_or_assign(offset, 0); compare();
bank[offset] = random(); compare();
bank.at(offset) ^= 1; compare();
}
}
for (std::uint32_t i = 0; i < 30000; ++i) {
auto& bank = i % 3 == 0 ? queue.context : i % 3 == 1 ? queue.shader : queue.userConfig;
const auto offset = random() % 0x500;
switch (i % 11) {
case 0: bank.erase(offset); break;
case 1: bank.emplace(offset, random()); break;
case 2: bank[offset] |= random(); break;
case 3: bank[offset] &= random(); break;
case 6: bank[offset] += random(); break;
case 4: {
auto retained = bank[offset];
bank.insert_or_assign(0x1000, random());
compare();
retained = random();
break;
}
case 5: {
auto copy = bank;
copy.insert_or_assign(offset, random());
compare();
bank = copy;
break;
}
default: bank.insert_or_assign(offset, random()); break;
}
compare();
}
queue.shader[0x8b] = 0; compare();
queue.shader[0x8c] = random(); compare();
queue.shader[0x8b] = 2; compare();
queue.shader[0x8b] = 1u << 27u; compare();
queue.shader.erase(0x8b); compare();
queue.savedContext = queue.context;
queue.ClearContext(); compare();
queue.context = *queue.savedContext; compare();
auto moved = std::move(queue.context);
check(queue.context.empty(), "moved register bank retained its population");
queue.context.emplace(0x200, 1); compare();
queue.context = std::move(moved); compare();
queue.context = queue.context; compare();
queue.context.clear(); compare();
queue.context.emplace(0x200, 0); compare();
queue = QueueState{}; compare();
Registers registers{{0, 7}, {64, 9}};
auto low = std::make_shared<const Registers::Mask>(std::vector<std::uint64_t>{1});
auto high = std::make_shared<const Registers::Mask>(std::vector<std::uint64_t>{0, 1});
const auto lowHash = registers.Fingerprint(low);
check(registers.Fingerprint(high) != lowHash && registers.Fingerprint(low) == lowHash, "projection switch reused another mask's hash");
registers[64] = 8;
check(registers.Fingerprint(low) == lowHash && registers.Fingerprint(high) == registers.RecomputeFingerprint(*high), "projection switch lost a masked write");
}
}
int main() {
try {
testFingerprints();
std::puts("Register fingerprint dependencies and mutations passed");
return 0;
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
return 1;
}
}

View File

@ -7,9 +7,6 @@
#include "Optimization/DescriptorBindingBuilder.hpp"
#include "ShaderCacheDirectory.hpp"
#include "ControlFlow/RequestSerializer.hpp"
#include "Optimization/RequestMemoryView.hpp"
#include "Optimization/SrtWalker.hpp"
#include "IntermediateRepresentation/IrBlock.hpp"
#include <algorithm>
#include <array>
#include <chrono>
@ -21,10 +18,7 @@
#include <future>
#include <iostream>
#include <map>
#include <memory>
#include <optional>
#include <random>
#include <span>
#include <stdexcept>
#include <string>
#include <string_view>
@ -253,7 +247,6 @@ CompiledVariant sampleVariant() {
info.info.usesDma = true;
info.info.usesFaultBuffer = true;
info.info.rasterPrimitive = true;
info.info.layerExported = true;
info.bindings.pushDataStartDword = 2;
info.bindings.memoryOffsetDword = 1;
info.bindings.memoryOffsetCount = 5;
@ -422,7 +415,6 @@ void verifyKeySensitivity() {
changes("the sRGB formats decoded in the shader", [](SampleRequest& sample) { sample.request.target.srgbDecodeFormats = 2u; });
changes("the narrow subgroup clock", [](SampleRequest& sample) { sample.request.target.narrowSubgroupClock = true; });
changes("f64 denormal preservation", [](SampleRequest& sample) { sample.request.target.float64DenormPreserve = true; });
changes("f16 denormal preservation", [](SampleRequest& sample) { sample.request.target.float16DenormPreserve = true; });
changes("the workgroup size limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSize[2] = 128; });
changes("the invocation limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupInvocations = 512; });
changes("the shared memory limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSharedMemoryBytes = 32768; });
@ -1089,743 +1081,6 @@ void verifySpecializationLiveness() {
require(SpecializeSpirv(specialized) == specialized, "dead computation removal is not stable");
}
constexpr std::uint64_t PlanSrtAddress = 0x7f0000010000ull;
constexpr std::uint64_t PlanTableAddress = 0x7f0000020000ull;
constexpr std::uint64_t PlanMaterialAddress = 0x7f0000030000ull;
constexpr std::uint64_t PlanPayloadAddress = 0x7f0000040000ull;
constexpr std::uint64_t PlanPointerAddress = 0x7f0000048000ull;
constexpr std::uint64_t PlanOutputAddress = 0x7f0000050000ull;
constexpr std::uint64_t PlanTextureAddress = 0x7f0000060000ull;
constexpr std::uint64_t PlanUnmappedAddress = 0x7f00000f0000ull;
struct PlanFixture {
std::string name;
ShaderStage stage = ShaderStage::Compute;
std::uint64_t codeAddress = 0;
std::uint32_t waveSize = 64;
std::vector<std::uint32_t> code;
std::vector<std::uint32_t> userData;
std::vector<std::pair<std::uint64_t, std::vector<std::uint32_t>>> regions;
std::optional<ShaderPixelStageInfo> pixel;
std::vector<MemoryRegion> memory;
RecompileRequest Request(bool useCache) {
static constexpr std::array<std::uint32_t, 7> capabilities{spv::CapabilityShader, spv::CapabilityImageGatherExtended, spv::CapabilitySampledImageArrayDynamicIndexing, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityClipDistance};
static constexpr std::array<std::string_view, 2> extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"};
memory.clear();
for (const auto& [address, words] : regions) memory.push_back({address, std::as_bytes(std::span(words))});
RecompileRequest request{};
request.shader = {stage, codeAddress, code, 0, {}};
request.context.waveSize = waveSize;
request.context.userDataBaseRegister = 0;
request.context.userData = userData;
if (stage == ShaderStage::Compute) request.context.compute = ShaderComputeStageInfo{{waveSize, 1u, 1u}, 0u, {false, false, false}, false, 1u};
request.context.pixel = pixel;
request.context.memory = memory;
request.target.vulkanVersion = 0x00401000u;
request.target.spirvVersion = 0x00010300u;
request.target.subgroupSize = 32;
request.target.supportedCapabilities = capabilities;
request.target.supportedExtensions = extensions;
request.target.bdaAbiVersion = 1u;
request.layout.pushConstantSizeBytes = 128;
request.useCache = useCache;
return request;
}
};
std::vector<std::uint32_t> imageWords(std::uint64_t base) {
return {static_cast<std::uint32_t>(base >> 8u), static_cast<std::uint32_t>((base >> 40u) & 0xffu) | (56u << 20u) | (3u << 30u), 3u << 14u, 0xfacu | (9u << 28u), 0u, 0u, 0u, 0u};
}
std::vector<std::uint32_t> bufferWords(std::uint64_t base, std::uint32_t stride, std::uint32_t records, std::uint32_t format = 0xfacu) {
return {static_cast<std::uint32_t>(base), static_cast<std::uint32_t>((base >> 32u) & 0xffffu) | (stride << 16u), records, format};
}
void place(std::vector<std::uint32_t>& words, std::size_t offset, const std::vector<std::uint32_t>& values) {
std::copy(values.begin(), values.end(), words.begin() + static_cast<std::ptrdiff_t>(offset));
}
std::vector<PlanFixture> planFixtures() {
std::vector<PlanFixture> fixtures;
const std::vector<std::uint32_t> srtPointer{static_cast<std::uint32_t>(PlanSrtAddress), static_cast<std::uint32_t>(PlanSrtAddress >> 32u)};
const auto add = [&](std::string name, std::uint32_t waveSize, std::vector<std::uint32_t> code, std::vector<std::uint32_t> userData, std::vector<std::pair<std::uint64_t, std::vector<std::uint32_t>>> regions) {
PlanFixture fixture;
fixture.name = std::move(name);
fixture.codeAddress = 0x100000u + static_cast<std::uint64_t>(fixtures.size()) * 0x1000u;
fixture.waveSize = waveSize;
fixture.code = std::move(code);
fixture.userData = std::move(userData);
fixture.regions = std::move(regions);
fixtures.push_back(std::move(fixture));
};
add("user data buffer", 64, {0xe0700000u, 0x80000000u, 0xbf810000u}, bufferWords(PlanOutputAddress, 0u, 0x40u, 0x00027facu), {});
std::vector<std::uint32_t> guarded(32, 0u);
place(guarded, 0, imageWords(PlanTextureAddress));
place(guarded, 16, bufferWords(PlanOutputAddress, 0u, 16u));
guarded[20] = 1u;
add("guarded image", 32, {0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xbf068010u, 0xbf850006u, 0xf40c0200u, 0xfa000000u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, guarded}});
add("guarded image stores", 32, {0xf4000400u, 0xfa000050u, 0xbf8cc07fu, 0xbf068010u, 0xbf850006u, 0xf40c0100u, 0xfa000000u, 0xbf8cc07fu, 0xf0200f08u, 0x00010004u, 0xbf820005u, 0xf40c0100u, 0xfa000000u, 0xbf8cc07fu, 0xf0200f10u, 0x00010004u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, guarded}});
std::vector<std::uint32_t> heap(32, 0u);
place(heap, 0, imageWords(PlanTextureAddress));
place(heap, 8, imageWords(PlanTextureAddress + 0x100u));
place(heap, 24, imageWords(PlanTextureAddress));
const std::vector<std::uint32_t> materials{0u, 1u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 3u, 0u, 0u};
std::vector<std::uint32_t> tables(16, 0u);
place(tables, 0, bufferWords(PlanTableAddress, 32u, 4u));
place(tables, 8, bufferWords(PlanMaterialAddress, 16u, 3u));
place(tables, 12, bufferWords(PlanOutputAddress, 0u, 16u));
add("bindless material table", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x93109010u, 0xf4200406u, 0x20000004u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, tables}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}});
add("bindless whole table", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, tables}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}});
add("bindless sample in a loop latch", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xbea80380u, 0xf09c0f08u, 0x00450000u, 0x80288128u, 0xbf0a8428u, 0xbf85fffbu, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, tables}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}});
std::vector<std::uint32_t> phis(32, 0u);
place(phis, 0, imageWords(PlanTextureAddress));
place(phis, 12, {0x49u, 0u, 0x00500000u, 0u});
place(phis, 16, bufferWords(PlanOutputAddress, 0u, 16u));
phis[20] = 1u;
place(phis, 24, imageWords(PlanTextureAddress + 0x100u));
add("descriptor phi sampler", 32, {0xf40c0200u, 0xfa000000u, 0xf4000400u, 0xfa000050u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf4080500u, 0xfa000020u, 0xbf820002u, 0xf4080500u, 0xfa000030u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, phis}});
add("descriptor phi image", 32, {0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf40c0200u, 0xfa000000u, 0xbf820002u, 0xf40c0200u, 0xfa000060u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, phis}});
add("descriptor phi loop", 32, {0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xf40c0200u, 0xfa000000u, 0xbe910380u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf068011u, 0xbf850003u, 0xf40c0200u, 0xfa000000u, 0xbf820002u, 0xf40c0200u, 0xfa000060u, 0xbf8cc07fu, 0x80118111u, 0xbf0a8211u, 0xbf85fff0u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, phis}});
add("descriptor phi in sequence", 32, {0xf4000400u, 0xfa000050u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf40c0200u, 0xfa000000u, 0xbf820002u, 0xf40c0200u, 0xfa000060u, 0xbf068010u, 0xbf850003u, 0xf4080500u, 0xfa000020u, 0xbf820002u, 0xf4080500u, 0xfa000030u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, phis}});
std::vector<std::uint32_t> root(16, 0u);
place(root, 0, bufferWords(PlanOutputAddress, 0u, 256u, 0x30027facu));
root[4] = 1u;
root[6] = static_cast<std::uint32_t>(PlanPayloadAddress);
root[7] = static_cast<std::uint32_t>(PlanPayloadAddress >> 32u);
const std::vector<std::uint32_t> payload{0x3f800000u};
add("guarded nested read", 64, {0xf4080100u, 0xfa000000u, 0xf4000200u, 0xfa000010u, 0xf4040280u, 0xfa000018u, 0xbe8c03ffu, 0x11111111u, 0xbf8cc07fu, 0xbf068008u, 0xbf850003u, 0xf4000305u, 0xfa000000u, 0xbf8cc07fu, 0x7e02020cu, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, root}, {PlanPayloadAddress, payload}});
auto chained = root;
chained[6] = static_cast<std::uint32_t>(PlanPointerAddress);
chained[7] = static_cast<std::uint32_t>(PlanPointerAddress >> 32u);
add("guarded pointer chain", 64, {0xf4080100u, 0xfa000000u, 0xf4000200u, 0xfa000010u, 0xf4040280u, 0xfa000018u, 0xbe8c03ffu, 0x11111111u, 0xbf8cc07fu, 0xbf068008u, 0xbf850006u, 0xf4040385u, 0xfa000000u, 0xbf8cc07fu, 0xf4000307u, 0xfa000000u, 0xbf8cc07fu, 0x7e02020cu, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, chained}, {PlanPayloadAddress, payload}, {PlanPointerAddress, {static_cast<std::uint32_t>(PlanPayloadAddress), static_cast<std::uint32_t>(PlanPayloadAddress >> 32u)}}});
add("descriptor through a pointer", 64, {0xf4040280u, 0xfa000018u, 0xbf8cc07fu, 0xf4080105u, 0xfa000000u, 0xbf8cc07fu, 0x34040082u, 0xe0701000u, 0x80010002u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, chained}, {PlanPointerAddress, bufferWords(PlanOutputAddress, 0u, 256u, 0x30027facu)}});
add("descriptor invariant phi", 32, {0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf40c0200u, 0xfa000000u, 0xbf820002u, 0xf40c0200u, 0xfa000000u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, phis}});
PlanFixture fragment;
fragment.name = "fragment parameters";
fragment.stage = ShaderStage::Fragment;
fragment.codeAddress = 0x100000u + static_cast<std::uint64_t>(fixtures.size()) * 0x1000u;
fragment.code = {0xc8100000u, 0xc8110001u, 0xc8140402u, 0xc8150403u, 0xf800180fu, 0x05040504u, 0xbf810000u};
ShaderPixelStageInfo pixel{};
pixel.interpolatorCount = 2u;
pixel.interpolatorSettings[1] = 1u;
pixel.wave32 = true;
pixel.inputAddr = PixelInputBit(PixelInput::PerspectiveCenter) | PixelInputBit(PixelInput::LinearCenter);
pixel.hasPerspectiveCenterVgpr = true;
pixel.noPerspective = true;
pixel.targetOutputMode[0] = 9u;
pixel.targetExportMapping[0] = 0xe4u;
fragment.pixel = pixel;
fixtures.push_back(std::move(fragment));
return fixtures;
}
IrResourcePlan handBuiltPlan() {
IrResourcePlan plan;
std::uint32_t ids = 0;
const auto make = [&](IrOpcode opcode, IrType type) -> IrValue& {
plan.valueStorage.push_back(std::make_unique<IrValue>(opcode, type, ids++));
return *plan.valueStorage.back();
};
const auto constant = [&](std::uint32_t value) -> IrValue& {
auto& immediate = make(IrOpcode::Void, IrType::U32);
immediate.SetImmediateU32(value);
return immediate;
};
auto& resource = make(IrOpcode::GetSrtResource, IrType::SrtResource);
const auto readConst = [&](std::uint32_t slot) -> IrValue& {
auto& read = make(IrOpcode::ReadConst, IrType::U32);
read.AddArgument(&resource);
read.AddArgument(&constant(slot));
return read;
};
const auto userData = [&](std::uint32_t index) -> IrValue& {
auto& reg = make(IrOpcode::Void, IrType::ScalarReg);
reg.SetRegister({RegisterBank::Scalar, index});
auto& read = make(IrOpcode::GetUserData, IrType::U32);
read.AddArgument(&reg);
return read;
};
auto& handle = make(IrOpcode::CompositeConstructU64, IrType::U64);
handle.AddArgument(&userData(0));
handle.AddArgument(&userData(1));
auto& pointer = make(IrOpcode::LoadAddressU32, IrType::U32);
pointer.AddArgument(&handle);
pointer.AddArgument(&constant(8));
plan.blockStorage.push_back(std::make_unique<IrBlock>(3u));
plan.blockStorage.push_back(std::make_unique<IrBlock>(7u));
auto& phi = make(IrOpcode::Phi, IrType::U32);
phi.AddPhiOperand(plan.blockStorage[0].get(), &readConst(0));
phi.AddPhiOperand(plan.blockStorage[1].get(), &constant(0));
auto& condition = make(IrOpcode::Void, IrType::Bool);
condition.SetImmediateU64(1u);
condition.SetFlags(std::uint64_t{0x1234u});
plan.stage = IrShaderStage::Compute;
plan.shaderHash = 0x123456789abcull;
plan.userDataBase = 2u;
plan.userDataCount = 4u;
plan.srgbDecodeFormats = 5u;
MemoryInfo memory;
memory.kind = ResourceKind::Image;
memory.resource = 1u;
memory.dmask = 0xfu;
memory.imageDimension = RdnaImageDimension::Dim2D;
memory.gpuDescriptor = true;
memory.flushDenormals = true;
MemoryInfo every;
every.kind = ResourceKind::Buffer;
every.resource = 2u;
every.sampler = 3u;
every.offset = 0x14u;
every.secondaryOffset = 0x28u;
every.dmask = 0x5u;
every.dataDwords = 3u;
every.dataBits = 16u;
every.componentIndex = 1u;
every.componentCount = 2u;
every.dataFormat = 10u;
every.numberFormat = 7u;
every.imageSampleFlags = 0x21u;
every.imageDimension = RdnaImageDimension::Dim3D;
every.imageAddressComponents = 3u;
every.addressIsFull = true;
every.dataSigned = true;
every.typed = true;
every.formatted = true;
every.d16 = true;
every.imageHasMip = true;
every.imageR128 = true;
every.imagePacked = true;
every.imageByElements = 2u;
every.idxen = true;
every.offen = true;
every.planningOnly = true;
every.coherent = true;
every.gpuDescriptor = true;
every.flushDenormals = true;
plan.memoryInfo = {memory, every, MemoryInfo{}};
DescriptorSource buffer;
buffer.dwordCount = 4u;
buffer.dwords = {&readConst(1), &readConst(2), &phi, &pointer};
DescriptorSource table;
table.dwordCount = 8u;
table.dwords[0] = &readConst(3);
table.indirectImage = DescriptorSource::IndirectImage{3u, 2u, 16u, 4u, 2u, 32u, true};
DescriptorSource heap;
heap.dwordCount = 4u;
heap.dwords[1] = &readConst(6);
DescriptorSource material;
material.dwordCount = 4u;
material.dwords[2] = &readConst(7);
auto& wide = make(IrOpcode::Void, IrType::U64);
wide.SetImmediateU64(0x8000000000000001ull);
material.dwords[3] = &wide;
plan.descriptorSources = {buffer, table, heap, material};
plan.controlFlow = {{&condition, {1u, 2u}, {0u}, {}}, {nullptr, {2u}, {3u}, {0u}}, {nullptr, {}, {}, {}}};
plan.materializationSources = {0u, 2u};
plan.srtReads = {{&readConst(4), 2u}, {&pointer, 1u}};
plan.guardedSrtSlots = {1u};
plan.srtGuardOffset = 6u;
plan.cleanFlatSlots = {0u, 1u};
plan.pureFlatSlots = {1u, 0u};
plan.requiresSpecializationMemory = true;
plan.srtPlanComplete = true;
plan.resourceTrackingComplete = true;
plan.info.buffers = {BufferResource{}, BufferResource{2u, 0x40u, 0x100u, 0u, true, true, true, true, true, 0x7u, true, 4u}};
ImageResource image{};
image.source = 1u;
image.resourceClass = ImageResourceClass::Sampled;
image.dimension = RdnaImageDimension::Dim2D;
plan.info.images = {image};
ResourceMaterializer::PrepareImageModes(plan.info);
plan.uniformFill.fill = {UniformFillKind::Buffer, 1u, {4u, 8u, 12u}, 2u, 0x3f800000u};
auto& fillValue = readConst(5);
plan.uniformFill.values = {&fillValue, &fillValue, nullptr, nullptr};
return plan;
}
std::vector<std::byte> encodedPlan(const IrResourcePlan& plan) {
std::vector<std::byte> bytes;
require(ShaderDiskCache::EncodePlan(plan, bytes), "a self-contained plan does not encode");
return bytes;
}
bool samePlan(const IrResourcePlan& left, const IrResourcePlan& right) {
if (left.valueStorage.size() != right.valueStorage.size() || left.blockStorage.size() != right.blockStorage.size()) return false;
std::map<const IrValue*, std::size_t> leftValues;
std::map<const IrValue*, std::size_t> rightValues;
std::map<const IrBlock*, std::size_t> leftBlocks;
std::map<const IrBlock*, std::size_t> rightBlocks;
for (std::size_t index = 0; index < left.valueStorage.size(); ++index) {
leftValues.emplace(left.valueStorage[index].get(), index);
rightValues.emplace(right.valueStorage[index].get(), index);
}
for (std::size_t index = 0; index < left.blockStorage.size(); ++index) {
leftBlocks.emplace(left.blockStorage[index].get(), index);
rightBlocks.emplace(right.blockStorage[index].get(), index);
}
const auto sameValue = [&](const IrValue* a, const IrValue* b) {
if (a == nullptr || b == nullptr) return a == b;
const auto foundA = leftValues.find(a);
const auto foundB = rightValues.find(b);
return foundA != leftValues.end() && foundB != rightValues.end() && foundA->second == foundB->second;
};
const auto sameBlock = [&](const IrBlock* a, const IrBlock* b) {
const auto foundA = leftBlocks.find(a);
const auto foundB = rightBlocks.find(b);
return foundA != leftBlocks.end() && foundB != rightBlocks.end() && foundA->second == foundB->second;
};
for (std::size_t index = 0; index < left.blockStorage.size(); ++index) {
if (left.blockStorage[index]->Id() != right.blockStorage[index]->Id()) return false;
}
for (std::size_t index = 0; index < left.valueStorage.size(); ++index) {
const auto& a = *left.valueStorage[index];
const auto& b = *right.valueStorage[index];
if (a.Opcode() != b.Opcode() || a.Type() != b.Type() || a.Id() != b.Id() || a.Flags<std::uint64_t>() != b.Flags<std::uint64_t>() || !(a.Register() == b.Register()) || a.HasImmediate() != b.HasImmediate() || (a.HasImmediate() && a.ImmediateU64() != b.ImmediateU64())) return false;
if (a.ArgumentCount() != b.ArgumentCount() || a.IsPhi() != b.IsPhi() || a.PhiBlockCount() != b.PhiBlockCount()) return false;
for (std::size_t operand = 0; operand < a.ArgumentCount(); ++operand) {
if (!sameValue(a.Argument(operand), b.Argument(operand))) return false;
}
for (std::size_t operand = 0; operand < a.PhiBlockCount(); ++operand) {
if (!sameBlock(a.PhiBlock(operand), b.PhiBlock(operand))) return false;
}
}
if (left.stage != right.stage || left.shaderHash != right.shaderHash || left.userDataBase != right.userDataBase || left.userDataCount != right.userDataCount || left.srgbDecodeFormats != right.srgbDecodeFormats) return false;
if (left.memoryInfo != right.memoryInfo || left.descriptorSources.size() != right.descriptorSources.size() || left.controlFlow.size() != right.controlFlow.size() || left.srtReads.size() != right.srtReads.size()) return false;
for (std::size_t index = 0; index < left.descriptorSources.size(); ++index) {
const auto& a = left.descriptorSources[index];
const auto& b = right.descriptorSources[index];
if (a.dwordCount != b.dwordCount || a.indirectImage != b.indirectImage) return false;
for (std::size_t dword = 0; dword < a.dwords.size(); ++dword) {
if (!sameValue(a.dwords[dword], b.dwords[dword])) return false;
}
}
for (std::size_t index = 0; index < left.controlFlow.size(); ++index) {
const auto& a = left.controlFlow[index];
const auto& b = right.controlFlow[index];
if (!sameValue(a.condition, b.condition) || a.successors != b.successors || a.sources != b.sources || a.images != b.images) return false;
}
for (std::size_t index = 0; index < left.srtReads.size(); ++index) {
if (!sameValue(left.srtReads[index].value, right.srtReads[index].value) || left.srtReads[index].flatOffset != right.srtReads[index].flatOffset) return false;
}
for (std::size_t index = 0; index < left.uniformFill.values.size(); ++index) {
if (!sameValue(left.uniformFill.values[index], right.uniformFill.values[index])) return false;
}
return left.materializationSources == right.materializationSources && left.guardedSrtSlots == right.guardedSrtSlots && left.srtGuardOffset == right.srtGuardOffset && left.cleanFlatSlots == right.cleanFlatSlots && left.pureFlatSlots == right.pureFlatSlots &&
left.requiresSpecializationMemory == right.requiresSpecializationMemory && left.srtPlanComplete == right.srtPlanComplete && left.resourceTrackingComplete == right.resourceTrackingComplete && left.info == right.info && left.uniformFill.fill == right.uniformFill.fill;
}
struct MaterializedPlan {
bool ok = false;
std::string error;
ResourceSnapshot snapshot;
SrtReadTrace trace;
std::size_t leaf = 0;
};
MaterializedPlan materializePlan(const IrResourcePlan& plan, SrtRuntime runtime) {
MaterializedPlan outcome;
runtime.readTrace = &outcome.trace;
try {
ResourceMaterializer{}.Materialize(plan, runtime, outcome.snapshot);
outcome.ok = true;
} catch (const std::exception& error) {
outcome.error = error.what();
}
outcome.leaf = static_cast<std::size_t>(std::find_if(plan.valueStorage.begin(), plan.valueStorage.end(), [&](const auto& value) { return value.get() == outcome.trace.leaf; }) - plan.valueStorage.begin());
return outcome;
}
bool sameMaterialization(const MaterializedPlan& left, const MaterializedPlan& right) {
const auto& a = left.snapshot;
const auto& b = right.snapshot;
return left.ok == right.ok && left.error == right.error && a.buffers == b.buffers && a.images == b.images && a.samplers == b.samplers && a.flattenedSrt == b.flattenedSrt && a.userData == b.userData && a.uniformFill == b.uniformFill && a.srtPoison == b.srtPoison && a.nullRootReads == b.nullRootReads &&
left.leaf == right.leaf && left.trace.leafSlot == right.trace.leafSlot && left.trace.leaves == right.trace.leaves && left.trace.otherReads == right.trace.otherReads;
}
void mutateWords(std::vector<std::uint32_t>& words, std::mt19937& random) {
static constexpr std::array<std::uint64_t, 8> addresses{PlanSrtAddress, PlanTableAddress, PlanMaterialAddress, PlanPayloadAddress, PlanPointerAddress, PlanOutputAddress, PlanTextureAddress, PlanUnmappedAddress};
for (auto& word : words) {
switch (random() % 16u) {
case 0: word = static_cast<std::uint32_t>(random()); break;
case 1: word = 0u; break;
case 2: {
const auto address = addresses[random() % addresses.size()] + (random() % 8u) * 4u;
word = random() % 2u == 0u ? static_cast<std::uint32_t>(address) : static_cast<std::uint32_t>(address >> 32u);
break;
}
case 3: word ^= 1u << (random() % 32u); break;
default: break;
}
}
}
std::size_t verifyRoundTripRuntimes(const IrResourcePlan& plan, const IrResourcePlan& decoded, const PlanFixture& fixture, std::uint32_t seed, const std::string& what) {
std::mt19937 random(seed);
std::size_t succeeded = 0;
for (std::uint32_t run = 0; run < 1000u; ++run) {
auto userData = fixture.userData;
auto regions = fixture.regions;
if (userData.size() < plan.userDataCount) userData.resize(plan.userDataCount, 0u);
if (run != 0u) {
mutateWords(userData, random);
for (auto& region : regions) mutateWords(region.second, random);
}
std::vector<MemoryRegion> memory;
for (const auto& [address, words] : regions) memory.push_back({address, std::as_bytes(std::span(words))});
RequestMemoryView view(memory);
const auto runtime = view.MakeRuntime(userData, fixture.codeAddress);
const auto original = materializePlan(plan, runtime);
const auto loaded = materializePlan(decoded, runtime);
require(sameMaterialization(original, loaded), what + ": runtime " + std::to_string(run) + " materialized differently from the decoded plan (" + original.error + " / " + loaded.error + ")");
if (original.ok) ++succeeded;
}
return succeeded;
}
void verifyPlanEntryDamage(const std::vector<std::byte>& key, const IrResourcePlan& plan, const std::string& what) {
const auto file = ShaderDiskCache::EncodePlanEntry(key, plan);
require(!file.empty(), what + ": the plan entry was not encoded");
IrResourcePlan decoded;
require(ShaderDiskCache::DecodePlanEntry(file, key, decoded) == ShaderDiskCache::LoadStatus::Loaded && encodedPlan(decoded) == encodedPlan(plan), what + ": the plan entry does not decode to the plan");
for (std::size_t size = 0; size < file.size(); size += size < 512 ? 1 : 37) {
IrResourcePlan partial;
require(ShaderDiskCache::DecodePlanEntry(std::span(file).first(size), key, partial) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry truncated to " + std::to_string(size) + " bytes is not rejected");
}
for (std::size_t offset = 0; offset < file.size(); offset += offset < 512 ? 1 : 29) {
for (const auto bit : {std::byte{0x01}, std::byte{0x80}}) {
auto damaged = file;
damaged[offset] ^= bit;
IrResourcePlan partial;
require(ShaderDiskCache::DecodePlanEntry(damaged, key, partial) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry with byte " + std::to_string(offset) + " flipped is not rejected");
}
}
auto longer = file;
longer.push_back(std::byte{0});
require(ShaderDiskCache::DecodePlanEntry(longer, key, decoded) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry with a trailing byte is not rejected");
auto otherKey = key;
otherKey.back() ^= std::byte{1};
require(ShaderDiskCache::DecodePlanEntry(file, otherKey, decoded) == ShaderDiskCache::LoadStatus::KeyMismatch, what + ": a plan entry for another key loads");
auto previousFormat = file;
previousFormat[4] = static_cast<std::byte>(ShaderDiskCache::FormatVersion - 1u);
require(ShaderDiskCache::DecodePlanEntry(previousFormat, key, decoded) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry of the previous format loads");
auto otherSource = file;
otherSource[8] ^= std::byte{1};
require(ShaderDiskCache::DecodePlanEntry(otherSource, key, decoded) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry of another source version loads");
CompiledVariant variant;
require(ShaderDiskCache::DecodeEntry(file, key, variant) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry decodes as a compiled variant");
}
void verifyPlanPayloadDamage(const IrResourcePlan& plan, const std::string& what) {
const auto bytes = encodedPlan(plan);
for (std::size_t size = 0; size < bytes.size(); ++size) {
IrResourcePlan partial;
require(!ShaderDiskCache::DecodePlan(std::span(bytes).first(size), partial), what + ": a plan truncated to " + std::to_string(size) + " bytes decodes");
}
std::size_t rejected = 0;
for (std::size_t offset = 0; offset < bytes.size(); ++offset) {
for (const auto bit : {std::byte{0x01}, std::byte{0x40}}) {
auto damaged = bytes;
damaged[offset] ^= bit;
IrResourcePlan decoded;
if (!ShaderDiskCache::DecodePlan(damaged, decoded)) {
++rejected;
continue;
}
std::vector<std::byte> again;
require(ShaderDiskCache::EncodePlan(decoded, again), what + ": a plan accepted with byte " + std::to_string(offset) + " flipped is not self-contained");
}
}
require(rejected != 0u, what + ": no damaged plan was rejected");
}
void verifyMalformedPlans() {
const auto rejects = [](const std::string& what, const std::function<void(IrResourcePlan&)>& damage) {
auto plan = handBuiltPlan();
damage(plan);
std::vector<std::byte> bytes;
require(ShaderDiskCache::EncodePlan(plan, bytes), "malformed plan: " + what + " did not encode");
IrResourcePlan decoded;
require(!ShaderDiskCache::DecodePlan(bytes, decoded), "malformed plan: " + what + " decodes");
};
rejects("a branch with one successor", [](IrResourcePlan& plan) { plan.controlFlow[0].successors.pop_back(); });
rejects("a successor past the blocks", [](IrResourcePlan& plan) { plan.controlFlow[1].successors[0] = 3u; });
rejects("a block source past the sources", [](IrResourcePlan& plan) { plan.controlFlow[0].sources[0] = 4u; });
rejects("a block image past the images", [](IrResourcePlan& plan) { plan.controlFlow[1].images[0] = 1u; });
rejects("a materialized source past the sources", [](IrResourcePlan& plan) { plan.materializationSources.push_back(4u); });
rejects("a buffer past the sources", [](IrResourcePlan& plan) { plan.info.buffers[0].source = 4u; });
rejects("an image past the sources", [](IrResourcePlan& plan) { plan.info.images[0].source = 5u; });
rejects("a descriptor of nine dwords", [](IrResourcePlan& plan) { plan.descriptorSources[1].dwordCount = 9u; });
rejects("a uniform fill of five words", [](IrResourcePlan& plan) { plan.uniformFill.fill.words = 5u; });
rejects("an image table heap past the sources", [](IrResourcePlan& plan) { plan.descriptorSources[1].indirectImage->heapSource = 4u; });
rejects("an image table material past the sources", [](IrResourcePlan& plan) { plan.descriptorSources[1].indirectImage->materialSource = 4u; });
auto foreign = handBuiltPlan();
IrValue stray(IrOpcode::Void, IrType::U32, 99u);
foreign.srtReads[0].value = &stray;
std::vector<std::byte> bytes;
require(!ShaderDiskCache::EncodePlan(foreign, bytes), "a plan reading a value it does not own was encoded");
auto unblocked = handBuiltPlan();
auto& phi = *unblocked.valueStorage.emplace_back(std::make_unique<IrValue>(IrOpcode::Phi, IrType::U32, 100u));
phi.AddArgument(unblocked.valueStorage[0].get());
unblocked.srtReads[0].value = &phi;
bytes.clear();
require(!ShaderDiskCache::EncodePlan(unblocked, bytes), "a phi without predecessor blocks was encoded");
auto emptied = handBuiltPlan();
emptied.valueStorage.push_back(nullptr);
bytes.clear();
require(!ShaderDiskCache::EncodePlan(emptied, bytes), "a plan with an empty value slot was encoded");
}
void verifyPlanRoundTrip() {
const auto handBuilt = handBuiltPlan();
const auto handBytes = encodedPlan(handBuilt);
IrResourcePlan handDecoded;
require(ShaderDiskCache::DecodePlan(handBytes, handDecoded), "the hand-built plan does not decode");
require(encodedPlan(handDecoded) == handBytes && handDecoded.valueStorage.size() == handBuilt.valueStorage.size() && handDecoded.blockStorage.size() == handBuilt.blockStorage.size(), "the hand-built plan changed in a round trip");
const auto& phi = *handDecoded.descriptorSources[0].dwords[2];
require(phi.IsPhi() && phi.PhiBlockCount() == 2u && phi.PhiBlock(0)->Id() == 3u && phi.PhiBlock(1)->Id() == 7u && phi.Argument(1)->ImmediateU32() == 0u, "the decoded phi lost its blocks or operands");
require(handDecoded.uniformFill.values[0] == handDecoded.uniformFill.values[1] && handDecoded.uniformFill.values[0] != nullptr && handDecoded.uniformFill.values[2] == nullptr, "the decoded uniform fill lost its shared or null values");
require(samePlan(handBuilt, handDecoded), "the decoded hand-built plan differs from the plan in a field or a value");
PlanFixture handFixture;
handFixture.userData = {static_cast<std::uint32_t>(PlanSrtAddress), static_cast<std::uint32_t>(PlanSrtAddress >> 32u), 0u, 0u};
std::vector<std::uint32_t> handSrt(16, 0u);
place(handSrt, 0, bufferWords(PlanOutputAddress, 0u, 16u));
handFixture.regions = {{PlanSrtAddress, handSrt}, {PlanPayloadAddress, {0x3f800000u}}};
static_cast<void>(verifyRoundTripRuntimes(handBuilt, handDecoded, handFixture, 7u, "hand-built plan"));
verifyPlanPayloadDamage(handBuilt, "hand-built plan");
verifyMalformedPlans();
auto fixtures = planFixtures();
std::vector<std::shared_ptr<const IrResourcePlan>> plans;
std::size_t index = 0;
for (auto& fixture : fixtures) {
auto request = fixture.Request(false);
const auto plan = GetResourcePlan(request);
plans.push_back(plan);
const auto bytes = encodedPlan(*plan);
IrResourcePlan decoded;
require(ShaderDiskCache::DecodePlan(bytes, decoded), fixture.name + ": the plan does not decode");
require(encodedPlan(decoded) == bytes, fixture.name + ": the plan changed in a round trip");
require(samePlan(*plan, decoded), fixture.name + ": the decoded plan differs from the plan in a field or a value");
require(decoded.valueStorage.size() == plan->valueStorage.size() && decoded.blockStorage.size() == plan->blockStorage.size(), fixture.name + ": the decoded plan holds another graph");
const auto succeeded = verifyRoundTripRuntimes(*plan, decoded, fixture, 0x5eedu + static_cast<std::uint32_t>(index), fixture.name);
require(succeeded != 0u, fixture.name + ": no runtime materialized");
std::vector<std::byte> key;
ShaderDiskCache::BuildPlanKey(request, 32u, key);
if (index % 4u == 0u) verifyPlanEntryDamage(key, *plan, fixture.name);
std::cout << "plan round trip: " << fixture.name << ", " << plan->valueStorage.size() << " values, " << ShaderDiskCache::EncodePlanEntry(key, *plan).size() << " bytes per entry, " << succeeded << " of 1000 runtimes materialized\n";
++index;
}
const auto covers = [&](const std::function<bool(const IrResourcePlan&)>& predicate) {
return std::any_of(plans.begin(), plans.end(), [&](const std::shared_ptr<const IrResourcePlan>& plan) { return predicate(*plan); });
};
require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.descriptorSources.begin(), plan.descriptorSources.end(), [](const DescriptorSource& source) { return source.indirectImage.has_value(); }); }), "no fixture plans an indirect image table");
require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.controlFlow.begin(), plan.controlFlow.end(), [](const ResourceBlock& block) { return block.condition != nullptr; }); }), "no fixture plans a control-flow condition");
require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.valueStorage.begin(), plan.valueStorage.end(), [](const auto& value) { return value->IsPhi(); }); }), "no fixture plans a phi");
require(covers([](const IrResourcePlan& plan) { return !plan.guardedSrtSlots.empty(); }), "no fixture plans a guarded SRT read");
require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.pureFlatSlots.begin(), plan.pureFlatSlots.end(), [](std::uint8_t pure) { return pure != 0u; }); }), "no fixture plans a pure flat slot");
}
void verifyPlanKey() {
auto fixtures = planFixtures();
auto& fixture = fixtures[1];
const auto planKey = [&](const std::function<void(RecompileRequest&)>& change, std::uint32_t hostSubgroupSize = 32u) {
auto request = fixture.Request(true);
change(request);
std::vector<std::byte> key;
ShaderDiskCache::BuildPlanKey(request, hostSubgroupSize, key);
return key;
};
const auto key = planKey([](RecompileRequest&) {});
require(planKey([](RecompileRequest&) {}) == key, "the plan key is not deterministic");
std::vector<std::byte> variantKey;
ShaderDiskCache::BuildKey(fixture.Request(true), 32u, variantKey);
require(variantKey != key && ShaderDiskCache::EntryName(variantKey) != ShaderDiskCache::EntryName(key), "the plan and the compiled variant share an entry");
require(planKey([](RecompileRequest& request) { request.layout = {1u, 4u, 16u, 64u}; }) == key, "the binding layout changed the plan key");
require(planKey([](RecompileRequest& request) { request.shader.codeAddress += 0x1000u; request.context.memory = {}; }) == key, "the code address or the memory changed the plan key");
auto userData = fixture.userData;
userData[0] ^= 0x100u;
require(planKey([&](RecompileRequest& request) { request.context.userData = userData; }) == key, "user data values changed the plan key");
const auto original = fixture.code;
for (std::size_t word = 0; word < original.size(); ++word) {
for (std::uint32_t bit = 0; bit < 32u; ++bit) {
fixture.code = original;
fixture.code[word] ^= 1u << bit;
require(planKey([](RecompileRequest&) {}) != key, "the plan key ignores code word " + std::to_string(word) + " bit " + std::to_string(bit));
}
}
fixture.code = original;
fixture.code.push_back(0xbf810000u);
require(planKey([](RecompileRequest&) {}) != key, "the plan key ignores an appended code word");
fixture.code = original;
require(planKey([](RecompileRequest&) {}, 64u) != key, "the plan key ignores the host subgroup size");
require(planKey([](RecompileRequest& request) { request.target.subgroupSize = 64u; }) != key, "the plan key ignores the target");
require(planKey([](RecompileRequest& request) { request.target.srgbDecodeFormats = 2u; }) != key, "the plan key ignores the sRGB formats decoded in the shader");
require(planKey([](RecompileRequest& request) { request.context.waveSize = 64u; request.context.compute->numThreads[0] = 64u; }) != key, "the plan key ignores the wave size");
require(planKey([](RecompileRequest& request) { request.context.floatMode = ShaderFloatMode{0xc0u, true, false, false}; }) != key, "the plan key ignores the float mode");
}
void appendWords(std::vector<std::byte>& out, std::span<const std::uint32_t> words) {
const auto count = static_cast<std::uint64_t>(words.size());
const auto countBytes = std::as_bytes(std::span(&count, 1));
out.insert(out.end(), countBytes.begin(), countBytes.end());
const auto bytes = std::as_bytes(words);
out.insert(out.end(), bytes.begin(), bytes.end());
}
void appendSnapshot(std::vector<std::byte>& out, const ResourceCapture& capture) {
const auto& snapshot = capture.snapshot;
for (const auto* values : {&snapshot.buffers, &snapshot.images, &snapshot.samplers}) {
std::vector<std::uint32_t> words;
for (const auto& value : *values) {
words.push_back(value.dwordCount);
words.insert(words.end(), value.dwords.begin(), value.dwords.end());
}
appendWords(out, words);
}
appendWords(out, snapshot.flattenedSrt);
appendWords(out, snapshot.userData);
const auto& fill = snapshot.uniformFill;
appendWords(out, std::vector<std::uint32_t>{static_cast<std::uint32_t>(fill.kind), fill.resource, fill.groupStride[0], fill.groupStride[1], fill.groupStride[2], fill.words, fill.value, snapshot.nullRootReads});
std::vector<std::uint32_t> poison;
for (const auto& entry : snapshot.srtPoison) poison.insert(poison.end(), {entry.slot, entry.pc, static_cast<std::uint32_t>(entry.address), static_cast<std::uint32_t>(entry.address >> 32u)});
appendWords(out, poison);
std::vector<std::uint32_t> leaves{capture.readTrace.leafSlot};
for (const auto& [slot, address] : capture.readTrace.leaves) leaves.insert(leaves.end(), {slot, static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u)});
appendWords(out, leaves);
std::vector<std::uint32_t> reads;
for (const auto address : capture.readTrace.otherReads) reads.insert(reads.end(), {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u)});
appendWords(out, reads);
}
std::vector<std::vector<std::byte>> preparedOutputs(std::vector<PlanFixture>& fixtures, std::vector<std::shared_ptr<const IrResourcePlan>>* plans = nullptr) {
std::vector<std::vector<std::byte>> outputs;
for (auto& fixture : fixtures) {
const auto request = fixture.Request(true);
const auto handle = PrepareShader(request);
RequestMemoryView view(request.context.memory);
const auto capture = CaptureResources(request, view.MakeRuntime(request.context.userData, request.shader.codeAddress), *handle);
const auto result = MaterializeShader(request, *capture, *handle);
std::vector<std::byte> bytes;
appendWords(bytes, GetPreparedArtifact(*handle).spirv.Words());
ShaderDiskCache::EncodeResult(*result, bytes);
appendSnapshot(bytes, *capture);
require(ShaderDiskCache::EncodePlan(*capture->plan, bytes), fixture.name + ": the captured plan does not encode");
if (plans != nullptr) plans->push_back(capture->plan);
outputs.push_back(std::move(bytes));
}
return outputs;
}
std::filesystem::path planOutputPath(std::size_t index) {
return ShaderCacheDirectory() / "plan-outputs" / ("fixture-" + std::to_string(index) + ".bin");
}
std::filesystem::path planEntryPath(PlanFixture& fixture) {
std::vector<std::byte> key;
ShaderDiskCache::BuildPlanKey(fixture.Request(true), 32u, key);
return ShaderDiskCache::EntryDirectory() / ShaderDiskCache::EntryName(key);
}
int runPlanLoadingProcess(std::string_view mode) {
if (mode == "--load-plan-switch") setEnvironment("APS5_TRACE_BINDLESS", "1");
if (mode == "--load-plan-off") setEnvironment("APS5_NO_PERF_DISK_PLANS", "1");
auto fixtures = planFixtures();
const auto count = fixtures.size();
std::vector<std::shared_ptr<const IrResourcePlan>> plans;
const auto outputs = preparedOutputs(fixtures, &plans);
ShaderDiskCache::Flush();
const auto totals = ShaderDiskCache::Totals();
const auto builds = FrontEndBuilds();
const auto counts = " (front end builds " + std::to_string(builds) + ", plan hits " + std::to_string(totals.planHits) + ", plan misses " + std::to_string(totals.planMisses) + ", plan load failures " + std::to_string(totals.planLoadFailures) + ", variant hits " + std::to_string(totals.hits) + ", variant misses " + std::to_string(totals.misses) + ")";
if (mode == "--load-plan-switch") {
require(builds == count && totals.planHits == 0u && totals.planMisses == count && totals.misses == count, "another switch value reused stored plans or variants" + counts);
std::cout << "a process with another switch value rebuilt all " << count << " plans\n";
return 0;
}
if (mode == "--load-plan") {
require(builds == 0u && totals.planHits == count && totals.planMisses == 0u && totals.planLoadFailures == 0u && totals.hits == count && totals.misses == 0u, "the second process ran the front end" + counts);
} else if (mode == "--load-plan-off") {
require(builds == count && totals.planHits == 0u && totals.planMisses == 0u && totals.planWrites == 0u && totals.hits == count && totals.misses == 0u, "APS5_NO_PERF_DISK_PLANS=1 used the stored plans or missed the stored variants" + counts);
} else {
require(builds == 1u && totals.planHits == count - 1u && totals.planLoadFailures == 1u && totals.planWrites == 1u && totals.hits == count && totals.misses == 0u, "the damaged plan was not rejected and rebuilt alone" + counts);
}
for (std::size_t index = 0; index < count; ++index) {
std::vector<std::byte> expected;
require(ReadWholeFile(planOutputPath(index), expected), "cannot read the first process's output for " + fixtures[index].name);
require(outputs[index] == expected, fixtures[index].name + ": the SPIR-V, bindings, push constants, snapshot or plan differ from the first process");
}
if (mode == "--load-plan") {
auto changed = planFixtures();
auto& fixture = changed.front();
fixture.code.insert(fixture.code.end() - 1, 0xbf800000u);
const auto request = fixture.Request(true);
static_cast<void>(PrepareShader(request));
const auto after = ShaderDiskCache::Totals();
require(FrontEndBuilds() == 1u && after.planHits == count && after.planMisses == 1u, "a changed code word reused a stored plan");
std::cout << "second process prepared " << count << " shaders from stored plans without running the front end\n";
} else if (mode == "--load-plan-off") {
std::cout << "APS5_NO_PERF_DISK_PLANS=1 ran the front end for all " << count << " shaders with the same output\n";
} else {
std::cout << "a damaged plan entry was rejected, rebuilt and stored again with the same output\n";
}
for (std::size_t index = 0; index < count; ++index) {
const auto fresh = GetResourcePlan(fixtures[index].Request(false));
require(samePlan(*plans[index], *fresh), fixtures[index].name + ": the plan this process used differs from the plan its front end builds");
}
ShaderDiskCache::Flush();
return 0;
}
void verifyPlanAcrossProcesses(const char* self) {
auto fixtures = planFixtures();
const auto builds = FrontEndBuilds();
const auto before = ShaderDiskCache::Totals();
const auto outputs = preparedOutputs(fixtures);
ShaderDiskCache::Flush();
const auto after = ShaderDiskCache::Totals();
require(FrontEndBuilds() - builds == fixtures.size() && after.planMisses - before.planMisses == fixtures.size() && after.planWrites - before.planWrites == fixtures.size(), "the first process did not build and store every plan");
std::filesystem::create_directories(planOutputPath(0).parent_path());
for (std::size_t index = 0; index < outputs.size(); ++index) require(WriteFileAtomically(planOutputPath(index), outputs[index]), "cannot write the first process's output");
auto refused = fixtures[1];
refused.codeAddress = 0x200000u;
refused.code = {0xf40c0200u, 0xfa000000u, 0xf4000400u, 0xfa000050u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf4080500u, 0xfa000020u, 0xbf820002u, 0xf4080500u, 0x20000000u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u};
bool threw = false;
try {
static_cast<void>(PrepareShader(refused.Request(true)));
} catch (const std::runtime_error&) {
threw = true;
}
ShaderDiskCache::Flush();
require(threw && ShaderDiskCache::Totals().planWrites == after.planWrites && !std::filesystem::exists(planEntryPath(refused)), "a plan the front end refused was stored");
std::uintmax_t planBytes = 0;
for (auto& fixture : fixtures) planBytes += std::filesystem::file_size(planEntryPath(fixture));
std::cout << fixtures.size() << " plan entries, " << planBytes / fixtures.size() << " bytes per entry on average\n";
const auto run = [&](const char* mode) {
const std::string command = "\"" + std::string(self) + "\" " + mode;
require(std::system(command.c_str()) == 0, std::string("the process ") + mode + " failed");
};
run("--load-plan");
run("--load-plan-off");
std::vector<std::byte> entry;
const auto damaged = planEntryPath(fixtures.front());
require(ReadWholeFile(damaged, entry), "cannot read a stored plan entry");
entry[entry.size() / 2] ^= std::byte{0x20};
require(WriteFileAtomically(damaged, entry), "cannot damage a stored plan entry");
run("--load-plan-corrupt");
std::vector<std::byte> rewritten;
std::vector<std::byte> key;
ShaderDiskCache::BuildPlanKey(fixtures.front().Request(true), 32u, key);
IrResourcePlan plan;
require(ReadWholeFile(damaged, rewritten) && ShaderDiskCache::DecodePlanEntry(rewritten, key, plan) == ShaderDiskCache::LoadStatus::Loaded, "the rebuilt plan was not stored again");
run("--load-plan-switch");
}
void verifyDefaultDirectory(const char* self) {
setEnvironment("ANYPS5_SHADER_CACHE_DIR", "");
setEnvironment("ANYPS5_NO_SHADER_CACHE", "1");
@ -1843,7 +1098,6 @@ int main(int argc, char** argv) {
try {
if (argc == 2 && std::string_view(argv[1]) == "--load") return runLoadingProcess();
if (argc == 2 && std::string_view(argv[1]) == "--no-failure-memo") return runWithoutFailureMemo();
if (argc == 2 && std::string_view(argv[1]).starts_with("--load-plan")) return runPlanLoadingProcess(argv[1]);
const auto directory = std::filesystem::temp_directory_path() / ("aps5-shader-disk-cache-test-" + std::to_string(std::random_device{}()));
std::filesystem::remove_all(directory);
verifyDefaultDirectory(argv[0]);
@ -1855,11 +1109,8 @@ int main(int argc, char** argv) {
verifyArtifactStorageIsolation();
verifyKeySensitivity();
verifyFloatModeSerialization();
verifyPlanRoundTrip();
verifyPlanKey();
verifyStore();
verifyAcrossProcesses(argv[0]);
verifyPlanAcrossProcesses(argv[0]);
verifyEmissionFailureMemo();
verifyFailureMemoSwitch(argv[0]);
verifyInvocationIsolation();

View File

@ -1686,23 +1686,6 @@ void verifyMeshConfiguration() {
other.graphics = GraphicsCompileContext{0u, {}, otherMesh, std::nullopt, {}};
RecompileCacheKey::Build(other, key);
require(key != first && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(other), "the cache keys ignore the mesh configuration");
RecompileRequest hull{};
hull.shader = {ShaderStage::TessellationControl, 0x10000u, code, 0, {}};
hull.context.waveSize = 64;
hull.graphics = GraphicsCompileContext{0u, {}, std::nullopt, TessellationConfiguration{4u, 4u, 2u, 2u, 3u, 63u}, {}};
const auto hullReplay = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(hull));
const auto* replayed = hullReplay.request.graphics && hullReplay.request.graphics->tessellation ? &*hullReplay.request.graphics->tessellation : nullptr;
require(replayed != nullptr && replayed->domain == 2u && replayed->outputTopology == 3u && replayed->patchesPerGroup == 63u, "the quad tessellation configuration or its patches per group were lost in serialization");
auto grouped = hull;
grouped.graphics->tessellation->patchesPerGroup = 62u;
for (const auto stage : {ShaderStage::Local, ShaderStage::TessellationControl, ShaderStage::TessellationEvaluation}) {
hull.shader.stage = stage;
grouped.shader.stage = stage;
RecompileCacheKey::Build(hull, key);
const auto patchKey = key;
RecompileCacheKey::Build(grouped, key);
require((key != patchKey) == (stage != ShaderStage::Local), "only the hull and domain cache keys may depend on the patches per group, which bound their relative patch index");
}
const std::array<std::uint32_t, 8> users{};
request.context.waveSize = 32u;
request.context.userData = users;
@ -1842,16 +1825,14 @@ void verifyPixelRequestSerialization() {
minimal.context.waveSize = 64;
minimal.context.pixel = ShaderPixelStageInfo{};
minimal.context.pixel->rasterPrimitive = RasterPrimitive::Rectangle;
minimal.context.pixel->layerExported = true;
const auto encoded = serializer.Serialize(minimal);
require(requestPrefix(encoded, 8u) == "NVNQQRcAAAA=", "new requests did not use serialization version 23");
require(requestPrefix(encoded, 8u) == "NVNQQRQAAAA=", "new requests did not use serialization version 20");
require(serializer.Deserialize(encoded).request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle, "the pixel primitive class did not round-trip");
require(serializer.Deserialize(encoded).request.context.pixel->layerExported, "the pixel layer export did not round-trip");
constexpr std::size_t mappingOffset = 8u + 37u + 18u + 163u;
for (std::size_t bytes = 0; bytes < 22u; ++bytes) {
expectFailure([&] { static_cast<void>(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class or layer export flag was accepted");
for (std::size_t bytes = 0; bytes < 20u; ++bytes) {
expectFailure([&] { static_cast<void>(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag or primitive class was accepted");
}
for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRgAAAA="}) {
for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRUAAAA="}) {
expectFailure([&] { static_cast<void>(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted");
}
static constexpr std::string_view version18Request{
@ -1940,14 +1921,6 @@ void verifyVertexInfoVersion19() {
while (offset < plain.size() && plain[offset] == flagged[offset]) ++offset;
require(offset + 4u <= plain.size() && flagged[offset] == 0xefu && flagged[offset + 1u] == 0xcdu && flagged[offset + 2u] == 0xabu && flagged[offset + 3u] == 0x89u, "PA_CL_VS_OUT_CNTL is not the four bytes the version 20 vertex info adds");
require(std::equal(plain.begin() + offset + 4u, plain.end(), flagged.begin() + offset + 4u), "PA_CL_VS_OUT_CNTL moved the fields after it");
auto version20 = flagged;
version20[4] = 20u;
const auto replay20 = serializer.Deserialize(encodeBase64(version20));
require(replay20.request.context.vertex.has_value() && replay20.request.context.vertex->paClVsOutCntl == 0x89abcdefu, "a version 20 vertex info lost its position export layout");
require(serializer.Serialize(replay20.request) == marked, "a version 20 vertex info misread the fields after its position export layout");
vertex.context.vertex->paClVsOutCntl = (1u << 18u) | (1u << 21u) | 0xfu;
const auto layered = serializer.Deserialize(serializer.Serialize(vertex));
require(layered.request.context.vertex.has_value() && layered.request.context.vertex->paClVsOutCntl == vertex.context.vertex->paClVsOutCntl, "a layered vertex info lost its clip distances or render target index");
auto legacy = plain;
legacy.erase(legacy.begin() + offset, legacy.begin() + offset + 4u);
for (const auto version : {16u, 17u, 18u, 19u}) {
@ -1962,20 +1935,11 @@ void verifyVertexInfoVersion19() {
pixel.context.pixel = ShaderPixelStageInfo{};
pixel.context.pixel->rasterPrimitive = RasterPrimitive::Rectangle;
const auto encodedPixel = serializer.Serialize(pixel);
auto exportedPixel = pixel;
exportedPixel.context.pixel->layerExported = true;
const auto exportedBytes = decodeBase64(serializer.Serialize(exportedPixel));
auto legacyPixel = decodeBase64(encodedPixel);
std::size_t flagOffset = 0;
while (flagOffset < legacyPixel.size() && legacyPixel[flagOffset] == exportedBytes[flagOffset]) ++flagOffset;
require(flagOffset < legacyPixel.size(), "the pixel layer export flag is not serialized");
legacyPixel.erase(legacyPixel.begin() + static_cast<std::ptrdiff_t>(flagOffset));
for (const auto version : {19u, 20u, 21u, 22u}) {
legacyPixel[4] = static_cast<std::uint8_t>(version);
const auto replayPixel = serializer.Deserialize(encodeBase64(legacyPixel));
require(replayPixel.request.context.pixel.has_value() && replayPixel.request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle && !replayPixel.request.context.pixel->layerExported, "a main version 19 to 22 request lost its pixel primitive class or read a layer export flag");
require(serializer.Serialize(replayPixel.request) == encodedPixel, "a main version 19 to 22 request misread the fields after its pixel primitive class");
}
legacyPixel[4] = 19u;
const auto replayPixel = serializer.Deserialize(encodeBase64(legacyPixel));
require(replayPixel.request.context.pixel.has_value() && replayPixel.request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle, "a main version 19 request lost its pixel primitive class");
require(serializer.Serialize(replayPixel.request) == encodedPixel, "a main version 19 request misread the fields after its pixel primitive class");
}
void verifyLegacyPixelRequests() {
@ -2070,7 +2034,7 @@ void verifyLegacyPixelRequests() {
require(!pixel.orderedPixelShader, "a legacy request became a primitive-ordered pixel shader");
require(request.shader.stage == ShaderStage::Fragment && request.shader.code.size() == 1u && request.shader.code[0] == 0xbf810000u && !request.context.vertex.has_value() && request.context.memory.empty(), "legacy guest context was misaligned");
require(request.target.vulkanVersion == 0x00401000u && request.target.spirvVersion == 0x00010300u && request.target.subgroupSize == 64u && request.layout.firstBinding == 11u && request.layout.pushConstantSizeBytes == 128u, "legacy target or binding layout was misaligned");
require(request.useCache == (version == 1u) && request.target.nonConstantImageOffsets == (version >= 6u) && request.target.srgbDecodeFormats == 0u && !request.target.narrowSubgroupClock && !request.target.float64DenormPreserve && !request.target.registrationModules && !request.target.float16DenormPreserve, "legacy request trailer was misread");
require(request.useCache == (version == 1u) && request.target.nonConstantImageOffsets == (version >= 6u) && request.target.srgbDecodeFormats == 0u && !request.target.narrowSubgroupClock && !request.target.float64DenormPreserve && !request.target.registrationModules, "legacy request trailer was misread");
const auto upgraded = serializer.Deserialize(serializer.Serialize(request));
require(upgraded.request.context.pixel->targetExportMapping == pixel.targetExportMapping && RecompileCacheKey::ContextHash(upgraded.request) == RecompileCacheKey::ContextHash(request), "upgrading a legacy capture changed its pixel mapping");
}
@ -2491,40 +2455,6 @@ void verifySpecializedBufferAtomicBindings() {
expectFailure([&] { static_cast<void>(compile(shaderOnly)); }, "buffer atomic fault addresses require shaderInt64", "specialized buffer atomic: the program compiled without shaderInt64");
}
void verifyCoherentGds() {
using namespace ShaderRecompiler;
const std::array<std::string_view, 2> extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"};
const std::array<std::uint32_t, 3> capabilities{spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess};
const std::vector<std::uint32_t> code{0x34000082u, 0xbefc03ffu, 0x0000ffffu, 0xd8da0000u, 0x01000000u, 0xbf8cc07fu, 0xd8360080u, 0x00000100u, 0xbf810000u};
RecompileRequest request{};
request.shader = {ShaderStage::Compute, 0x5c000u, code, 0, {}};
request.context.waveSize = 32;
request.context.userDataBaseRegister = 0;
request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u};
request.target.vulkanVersion = 0x00401000u;
request.target.spirvVersion = 0x00010300u;
request.target.subgroupSize = 32;
request.target.bdaAbiVersion = BdaAbi::Version;
request.target.supportedCapabilities = capabilities;
request.target.supportedExtensions = extensions;
request.target.fragmentShaderBarycentricEnabled = false;
request.layout.pushConstantSizeBytes = 128;
request.useCache = false;
const auto words = Recompile(request).spirv;
std::uint32_t gds = 0u;
std::map<std::uint32_t, std::vector<std::uint32_t>> decorations;
for (std::size_t cursor = 5; cursor < words.size();) {
const auto count = words[cursor] >> 16u;
const auto opcode = words[cursor] & 0xffffu;
require(count != 0 && count <= words.size() - cursor, "coherent GDS: truncated SPIR-V instruction");
if (opcode == spv::OpName && std::string_view(reinterpret_cast<const char*>(&words[cursor + 2])) == "gds") gds = words[cursor + 1];
if (opcode == spv::OpDecorate) decorations[words[cursor + 1]].push_back(words[cursor + 2]);
cursor += count;
}
require(gds != 0u, "coherent GDS: a ds_read_b32 and ds_write_b32 on GDS declare no gds buffer");
require(std::ranges::find(decorations[gds], static_cast<std::uint32_t>(spv::DecorationCoherent)) != decorations[gds].end(), "coherent GDS: the gds buffer is not Coherent");
}
void verifyCoherentBufferAccesses() {
using namespace ShaderRecompiler;
alignas(256) static std::array<std::uint32_t, 64> input{};
@ -3599,7 +3529,6 @@ int main(int argc, char** argv) {
verifyGuardedNullPointers();
verifyCubeDepthCompareMode();
verifySrgbDepthCompareModes();
verifyCoherentGds();
#if ANYPS5_ENABLE_SPIRV_TOOLS
const std::vector<std::uint32_t> minimalSpirv{
0x07230203u, 0x00010000u, 0u, 5u, 0u,
@ -3713,9 +3642,6 @@ int main(int argc, char** argv) {
RecompileCacheKey::Build(uncached, plainKey);
RecompileCacheKey::Build(registrationModules, registrationKey);
require(plainKey != registrationKey, "registration modules did not change the cache key");
auto float16Denormals = uncached;
float16Denormals.target.float16DenormPreserve = true;
require(RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(float16Denormals)).request.target.float16DenormPreserve, "f16 denormal preservation was lost in serialization");
auto changedLayout = request;
changedLayout.layout.pushConstantSizeBytes = 64;
require(!Recompile(changedLayout).cacheHit, "binding layout change reused an incompatible variant");

View File

@ -2,7 +2,6 @@
#include "SpirvBackend/SpirvSpecialization.hpp"
#include <array>
#include <cstdio>
#include <exception>
#include <map>
#include <set>
#include <string>
@ -286,21 +285,8 @@ void testFoldingLeavesNoDeadBlocksOrConstants() {
checkFoldedSwitch(foldedSwitchModule(32u, {15u, 16u, 1u}), 32u, "the OpTypeInt width and the LocalSize literals equal the dead ids");
}
void testIdsBeyondTheBoundAreRejected() {
auto words = foldedSwitchModule(13u, {1u, 1u, 1u});
words[3] = 12u;
bool rejected = false;
try {
static_cast<void>(ShaderRecompiler::SpecializeSpirv(words));
} catch (const std::exception&) {
rejected = true;
}
check(rejected, "a module whose ids reach its bound was specialized");
}
int main() {
testFoldingLeavesNoDeadBlocksOrConstants();
testIdsBeyondTheBoundAreRejected();
testStructExtractsAreNotShuffled();
testRepeatedTypesShareOneId();
testOperandShapesDoNotCollide();

View File

@ -10,7 +10,6 @@
#include "prx/libSceAgcDriver/Submit/include/Acb.hpp"
#include "prx/libSceAgcDriver/Eq/include/Query.hpp"
#include "prx/libSceAgcDriver/Eq/include/Event.hpp"
#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp"
#include "prx/libkernel/Equeue/Equeue.hpp"
#include <array>
#include <algorithm>
@ -212,22 +211,6 @@ void testEndOfPipeInterrupts() {
check(sceAgcDriverSubmitDcb(&packet) == 0, "plain release submit failed");
AgcDriverWaitIdle_nid_postfix();
check(owner->GetTriggeredEvents(events.data(), 2) == 0, "release without INT_SEL raised an interrupt");
struct Output final : AgcDriver::IVideoOutput, AgcDriver::IFlipRequest, std::enable_shared_from_this<Output> {
std::atomic<int> ready{0};
std::shared_ptr<AgcDriver::IFlipRequest> Reserve(const AgcDriver::FlipInfo&) override { return shared_from_this(); }
void GpuReady(const std::shared_ptr<AgcDriver::FrameTiming>&) override { ready.fetch_add(1); }
void Fail(std::exception_ptr error) noexcept override { if (!error) std::terminate(); }
};
const auto output = std::make_shared<Output>();
AgcDriverRegisterVideoOutput_nid_postfix(7, output);
std::array<std::uint32_t, 6> flip{AgcDriver::FlipPacketHeader, 7, 0xfffffffeu, 1, 0, 0};
Packet flipPacket{flip.data(), static_cast<std::uint32_t>(flip.size()), 0, {}};
check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "flip submit failed");
AgcDriverWaitIdle_nid_postfix();
check(output->ready.load() == 1, "flip was not handed to its output");
check(owner->GetTriggeredEvents(events.data(), 2) == 1 && events[0].udata == &graphicsTag && events[0].data == 1 && sceAgcDriverGetEqEventType(events.data()) == 0, "flip did not raise the graphics end-of-pipe event");
check(owner->GetTriggeredEvents(events.data(), 2) == 0, "flip event was not cleared");
AgcDriverUnregisterVideoOutput_nid_postfix(7, output);
alignas(8) static volatile std::uint64_t label = 0;
const auto labelAddress = reinterpret_cast<std::uintptr_t>(&label);
words = {0xc0064900, 0x528, (3u << 29u) | (3u << 24u) | (1u << 16u), static_cast<std::uint32_t>(labelAddress), static_cast<std::uint32_t>(static_cast<std::uint64_t>(labelAddress) >> 32u), 0x89abcdefu, 0x01234567u, 0};

View File

@ -1,7 +1,6 @@
#include "GraphicsTests.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp"
#include "RdnaDecoder/RdnaDescriptorFormat.hpp"
#include <array>
@ -49,20 +48,6 @@ void srgbDecodeTests() {
Require(SampledTextureFormat(context, 130) == VK_FORMAT_R8G8B8A8_SRGB && SampledTextureFormat(context, 14) == VK_FORMAT_R8G8_UNORM && SampledTextureFormat(context, 170) == VK_FORMAT_BC1_RGBA_SRGB_BLOCK, "formats without a shader decode must keep their views");
}
void srgbTwinTests() {
constexpr std::uint32_t srgb8 = 1u;
constexpr std::uint32_t srgb8_8 = 2u;
Context context{};
Require(SrgbTwinFormat(context, VK_FORMAT_R8_UNORM) == VK_FORMAT_R8_SRGB && SrgbTwinFormat(context, VK_FORMAT_R8G8_UNORM) == VK_FORMAT_R8G8_SRGB, "a device that samples R8_SRGB and R8G8_SRGB must give R8 and R8G8 their sRGB twins");
context.srgbDecodeFormats = srgb8_8;
Require(SrgbTwinFormat(context, VK_FORMAT_R8G8_UNORM) == VK_FORMAT_UNDEFINED, "a device that decodes 8_8_SRGB in the shader must not give R8G8 an sRGB twin");
Require(SrgbTwinFormat(context, VK_FORMAT_R8_UNORM) == VK_FORMAT_R8_SRGB, "R8 must keep its sRGB twin when only 8_8_SRGB is decoded in the shader");
context.srgbDecodeFormats = srgb8 | srgb8_8;
Require(SrgbTwinFormat(context, VK_FORMAT_R8_UNORM) == VK_FORMAT_UNDEFINED, "a device that decodes 8_SRGB in the shader must not give R8 an sRGB twin");
Require(SrgbTwinFormat(context, VK_FORMAT_R8G8B8A8_UNORM) == VK_FORMAT_R8G8B8A8_SRGB && SrgbTwinFormat(context, VK_FORMAT_BC7_UNORM_BLOCK) == VK_FORMAT_BC7_SRGB_BLOCK, "formats without a shader decode must keep their sRGB twins");
Require(SrgbTwinFormat(context, VK_FORMAT_R32_SFLOAT) == VK_FORMAT_UNDEFINED, "formats without an sRGB counterpart have no twin");
}
template<typename TAction>
void reject(TAction action, std::string_view reason) {
try {
@ -140,28 +125,10 @@ void singleDccKeysTests() {
Require(read(56) == DccKeys::Uncompressed, "comp-to-single keys mixed with uncompressed ones must read as stored");
}
void packedFloatDccClearTests() {
for (const bool alphaOnMsb : {false, true}) {
for (const auto keys : {DccKeys::Clear0000, DccKeys::Clear0001, DccKeys::Clear1110, DccKeys::Clear1111}) {
std::array<std::uint32_t, 4> texels{};
texels.fill(0xdeadbeefu);
Require(FillDccClear(VK_FORMAT_B10G11R11_UFLOAT_PACK32, keys, alphaOnMsb, std::as_writable_bytes(std::span(texels))), "packed float fast clear was not encoded");
const std::uint32_t expected = keys == DccKeys::Clear1110 || keys == DccKeys::Clear1111 ? 0x781e03c0u : 0u;
for (const auto texel : texels) Require(texel == expected, "packed float DCC clear did not encode RGB zero or one");
}
}
alignas(64) std::array<std::uint8_t, 4> keys{0xc0, 0xc0, 0xc0, 0xc0};
GuestTextureResource resource{};
resource.format = 36;
resource.dccAddress = reinterpret_cast<std::uint64_t>(keys.data());
Require(TextureClearKeys(resource, 1024) == DccKeys::Clear1111, "packed float texture discarded its white fast-clear keys");
}
}
void RunTextureFormatTests() {
srgbDecodeTests();
srgbTwinTests();
packedChannelTests();
Require(ResolveTextureFormat(1) == VK_FORMAT_R8_UNORM, "format 1 must resolve to R8_UNORM");
Require(ResolveTextureFormat(2) == VK_FORMAT_R8_SNORM, "format 2 must resolve to R8_SNORM");
@ -169,15 +136,6 @@ void RunTextureFormatTests() {
Require(!IsBlockCompressed(1), "format 1 must not be block compressed");
Require(BlockWidth(1) == 1u && BlockHeight(1) == 1u, "format 1 must have a one-texel block");
Require(ResolveTextureFormat(50) == VK_FORMAT_A2B10G10R10_UNORM_PACK32, "format 50 must resolve to A2B10G10R10_UNORM");
Require(ResolveTextureFormat(51) == VK_FORMAT_A2B10G10R10_SNORM_PACK32, "format 51 must resolve to A2B10G10R10_SNORM");
Require(ResolveTextureFormat(54) == VK_FORMAT_A2B10G10R10_UINT_PACK32, "format 54 must resolve to A2B10G10R10_UINT");
Require(ResolveTextureFormat(55) == VK_FORMAT_A2B10G10R10_SINT_PACK32, "format 55 must resolve to A2B10G10R10_SINT");
for (const std::uint32_t format : {50u, 51u, 54u, 55u}) Require(BytesPerElement(format) == 4u && !IsBlockCompressed(format), "packed 10-bit format " + std::to_string(format) + " must be four bytes and not block compressed");
Require(ShaderRecompiler::SampledTextureNumericClass(ShaderRecompiler::IrBufferFormat::Format10_10_10_2SNorm) == ShaderRecompiler::IrTextureNumericClass::Float, "format 51 must sample as float");
Require(ShaderRecompiler::SampledTextureNumericClass(ShaderRecompiler::IrBufferFormat::Format10_10_10_2SInt) == ShaderRecompiler::IrTextureNumericClass::Sint, "format 55 must sample as signed integer");
Require(ShaderRecompiler::SampledTextureNumericClass(ShaderRecompiler::IrBufferFormat::Format10_10_10_2UInt) == ShaderRecompiler::IrTextureNumericClass::Uint, "format 54 must sample as unsigned integer");
Require(ResolveTextureFormat(56) == VK_FORMAT_R8G8B8A8_UNORM, "format 56 must resolve to R8G8B8A8_UNORM");
Require(BytesPerElement(56) == 4u, "format 56 must be four bytes wide");
@ -217,5 +175,4 @@ void RunTextureFormatTests() {
for (const std::uint32_t format : {1u, 14u, 56u, 71u, 169u, 171u, 173u, 181u}) Require(!IsSrgbTextureFormat(format), "guest format " + std::to_string(format) + " must not be an sRGB texture format");
convertedDccClearTests();
singleDccKeysTests();
packedFloatDccClearTests();
}

View File

@ -1,95 +0,0 @@
#include "Recompiler.hpp"
#include <array>
#include <cstdint>
#include <cstdio>
#include <exception>
#include <string>
#include <string_view>
#include <vector>
namespace {
struct Module {
std::vector<std::uint32_t> capabilities;
std::vector<std::string> extensions;
bool layerOutput = false;
std::uint32_t version = 0;
};
Module inspect(const std::vector<std::uint32_t>& words) {
Module module;
if (words.size() > 1) module.version = words[1];
std::vector<std::uint32_t> layerIds;
for (std::size_t at = 5; at < words.size();) {
const auto length = words[at] >> 16u;
const auto opcode = words[at] & 0xffffu;
if (length == 0 || at + length > words.size()) break;
if (opcode == 17u && length == 2) module.capabilities.push_back(words[at + 1]);
if (opcode == 10u && length >= 2) module.extensions.emplace_back(reinterpret_cast<const char*>(&words[at + 1]));
if (opcode == 71u && length == 4 && words[at + 2] == 11u && words[at + 3] == 9u) module.layerOutput = true;
at += length;
}
return module;
}
bool has(const std::vector<std::uint32_t>& values, std::uint32_t value) {
for (const auto entry : values) {
if (entry == value) return true;
}
return false;
}
bool named(const std::vector<std::string>& values, std::string_view value) {
for (const auto entry : values) {
if (entry == value) return true;
}
return false;
}
Module recompile(std::uint32_t control, bool layerExtension) {
using namespace ShaderRecompiler;
static const std::array<std::uint32_t, 5> code{0xf80000cfu, 0x00000000u, 0xf80008dfu, 0x01010101u, 0xbf810000u};
static const std::array<std::uint32_t, 3> withExtension{1u, 32u, 5254u};
static const std::array<std::uint32_t, 2> without{1u, 32u};
ShaderVertexStageInfo vertex{};
vertex.paClVsOutCntl = control;
RecompileRequest request{};
request.shader = {ShaderStage::Vertex, 0x30000u, code, 0, {}};
request.context.waveSize = 64;
request.context.vertex = vertex;
request.target.vulkanVersion = layerExtension ? 0x00401000u : 0x00402000u;
request.target.spirvVersion = layerExtension ? 0x00010300u : 0x00010500u;
request.target.subgroupSize = 64;
if (layerExtension) request.target.supportedCapabilities = withExtension;
else request.target.supportedCapabilities = without;
request.layout.pushConstantSizeBytes = 128;
request.useCache = false;
return inspect(Recompile(request).spirv);
}
}
int main() {
try {
constexpr std::uint32_t layer = (1u << 18u) | (1u << 21u);
const auto extension = recompile(layer, true);
if (!extension.layerOutput || !has(extension.capabilities, 5254u) || has(extension.capabilities, 69u) || !named(extension.extensions, "SPV_EXT_shader_viewport_index_layer") || extension.version != 0x00010300u) {
std::fprintf(stderr, "a vertex layer export with VK_EXT_shader_viewport_index_layer: layer %d, version 0x%x\n", extension.layerOutput ? 1 : 0, extension.version);
return 1;
}
const auto core = recompile(layer, false);
if (!core.layerOutput || !has(core.capabilities, 69u) || has(core.capabilities, 5254u) || core.version != 0x00010500u) {
std::fprintf(stderr, "a vertex layer export without the extension: layer %d, version 0x%x\n", core.layerOutput ? 1 : 0, core.version);
return 1;
}
const auto ignored = recompile(0u, true);
if (ignored.layerOutput || has(ignored.capabilities, 5254u)) {
std::fprintf(stderr, "a misc export without USE_VTX_RENDER_TARGET_INDX wrote gl_Layer\n");
return 1;
}
} catch (const std::exception& error) {
std::fprintf(stderr, "unexpected error: %s\n", error.what());
return 1;
}
return 0;
}

View File

@ -562,142 +562,6 @@ void CheckUnwatchedImport() {
#endif
}
class PartlyCommitted {
public:
static constexpr std::size_t Bytes = 4 * Block;
PartlyCommitted() {
#ifdef _WIN32
memory = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocateAtOrAbove_nid_postfix(GuestArena::GuestArenaHighestFree_nid_no_patch(Bytes, Block), Bytes, Block));
#else
void* raw = mmap(nullptr, Bytes + Block, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
Require(raw != MAP_FAILED, "cannot reserve the partly committed range");
const auto begin = reinterpret_cast<std::uintptr_t>(raw);
const auto aligned = (begin + Block - 1) & ~static_cast<std::uintptr_t>(Block - 1);
if (aligned != begin) munmap(raw, aligned - begin);
if (aligned + Bytes != begin + Bytes + Block) munmap(reinterpret_cast<void*>(aligned + Bytes), begin + Block - aligned);
memory = reinterpret_cast<std::uint8_t*>(aligned);
#endif
Require(memory != nullptr, "cannot reserve the partly committed range");
}
~PartlyCommitted() {
#ifdef _WIN32
GuestArena::GuestArenaReset_nid_postfix(memory, Bytes);
GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(), Bytes);
GuestArena::GuestArenaRelease_nid_postfix(memory, Bytes);
#else
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory, Bytes);
munmap(memory, Bytes);
GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(), Bytes);
#endif
}
PartlyCommitted(const PartlyCommitted&) = delete;
PartlyCommitted& operator=(const PartlyCommitted&) = delete;
void Commit(std::size_t block) {
#ifdef _WIN32
GuestArena::GuestArenaCommit_nid_postfix(memory + block * Block, Block, PAGE_READWRITE, Block);
#else
Require(mmap(memory + block * Block, Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) != MAP_FAILED, "cannot commit a block of the partly committed range");
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(memory + block * Block, Block);
#endif
GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(block), Block);
}
void Decommit(std::size_t block) {
#ifdef _WIN32
GuestArena::GuestArenaReset_nid_postfix(memory + block * Block, Block);
#else
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory + block * Block, Block);
Require(mmap(memory + block * Block, Block, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED, -1, 0) != MAP_FAILED, "cannot decommit a block of the partly committed range");
#endif
GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(block), Block);
}
std::uint8_t* Data() const { return memory; }
std::uint64_t Address(std::size_t block = 0) const { return reinterpret_cast<std::uint64_t>(memory) + block * Block; }
private:
std::uint8_t* memory = nullptr;
};
using Runs = std::vector<std::pair<std::uint64_t, std::uint64_t>>;
std::uint64_t CollectCommitted(const PartlyCommitted& range, Runs& runs) {
BumpCollectEpoch();
return CollectCommittedWrites(range.Address(), PartlyCommitted::Bytes, runs);
}
void CheckCommittedRanges() {
PartlyCommitted range;
const auto base = range.Address();
range.Commit(0);
std::memset(range.Data(), 0x11, Block);
Require(CollectWrites(base, PartlyCommitted::Bytes) == 0, "a partly committed range was collected as a whole");
Runs runs;
const Runs first{{base, base + Block}};
const auto synced = CollectCommitted(range, runs);
Require(synced != 0 && runs == first, "the committed part of a partly committed range was not collected");
Require(UnchangedCommittedSince(runs, synced), "a collected committed part reads as changed");
range.Data()[8] = 0x22;
const auto written = CollectCommitted(range, runs);
Require(written > synced && runs == first && !UnchangedCommittedSince(runs, synced) && UnchangedCommittedSince(runs, written), "a CPU write to the committed part was missed");
Require(MarkWritten(base + 16, 4) != 0 && !UnchangedCommittedSince(first, written), "a driver store into the committed part was missed");
Require(UnchangedCommittedSince(first, CollectCommitted(range, runs)), "a collect after a driver store reads the committed part as changed");
const auto beforeCommit = CollectCommitted(range, runs);
range.Commit(2);
const Runs both{{base, base + Block}, {range.Address(2), range.Address(3)}};
const auto grown = CollectCommitted(range, runs);
Require(grown != 0 && runs == both, "a block committed inside the range is not described");
Require(!UnchangedCommittedSince(runs, beforeCommit), "a block committed inside the range reads as unchanged since before the commit");
Require(UnchangedCommittedSince(runs, grown), "the committed parts read as changed after their collect");
range.Decommit(2);
range.Commit(2);
const auto recommitted = CollectCommitted(range, runs);
Require(recommitted != 0 && runs == both && !UnchangedCommittedSince(runs, grown), "a block decommitted and committed again reads as unchanged");
range.Decommit(2);
Require(CollectCommitted(range, runs) != 0 && runs == first, "a decommitted block is still described as committed");
#ifdef _WIN32
HANDLE firstSection = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, Block, nullptr);
HANDLE secondSection = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, Block, nullptr);
struct Sections {
HANDLE first;
HANDLE second;
~Sections() {
if (first != nullptr) CloseHandle(first);
if (second != nullptr) CloseHandle(second);
}
} sections{firstSection, secondSection};
Require(firstSection != nullptr && secondSection != nullptr, "cannot create the shared backings of the partly committed range");
const auto beforeMap = CollectCommitted(range, runs);
GuestArena::GuestArenaMap_nid_postfix(range.Data() + 3 * Block, Block, firstSection, 0, PAGE_READWRITE);
GuestAllocations::GuestAllocationsInvalidate_nid_postfix(range.Address(3), Block);
const Runs viewed{{base, base + Block}, {range.Address(3), range.Address(4)}};
const auto mapped = CollectCommitted(range, runs);
Require(mapped != 0 && runs == viewed, "a view mapped inside the range is not described");
Require(!UnchangedCommittedSince(runs, beforeMap), "a view mapped inside the range reads as unchanged since before the mapping");
Require(UnchangedCommittedSince(runs, mapped), "a collected view reads as changed");
GuestArena::GuestArenaReset_nid_postfix(range.Data() + 3 * Block, Block);
GuestArena::GuestArenaMap_nid_postfix(range.Data() + 3 * Block, Block, secondSection, 0, PAGE_READWRITE);
GuestAllocations::GuestAllocationsInvalidate_nid_postfix(range.Address(3), Block);
Require(CollectCommitted(range, runs) != 0 && runs == viewed && !UnchangedCommittedSince(runs, mapped), "a view replaced inside the range reads as unchanged");
GuestArena::GuestArenaReset_nid_postfix(range.Data() + 3 * Block, Block);
GuestAllocations::GuestAllocationsInvalidate_nid_postfix(range.Address(3), Block);
Require(CollectCommitted(range, runs) != 0 && runs == first, "an unmapped view is still described as committed");
#endif
range.Decommit(0);
Require(CollectCommitted(range, runs) != 0 && runs.empty() && UnchangedCommittedSince(runs, TrackerGeneration()), "a range with no committed page did not collect as empty");
Require(!UnchangedCommittedSince(first, 0), "an unknown generation was accepted for the committed parts");
}
int main() {
try {
if (!WriteWatched()) {
@ -723,7 +587,6 @@ int main() {
CheckCollectBoundaries();
CheckImportWindow();
CheckUnwatchedImport();
CheckCommittedRanges();
} catch (const std::exception& error) {
std::cerr << "write tracking test failed: " << error.what() << "\n";
return 1;

View File

@ -165,23 +165,22 @@ void RequireOutput(std::uint32_t inside, std::uint32_t outside, const std::strin
}
}
AgcDriver::Graphics::ColorTarget DecodeTarget(std::uint64_t address, std::uint32_t slice, std::uint32_t swizzleMode, bool collapsed) {
AgcDriver::Graphics::ColorTarget DecodeTarget(std::uint64_t address, std::uint32_t slice, std::uint32_t swizzleMode) {
AgcDriver::Registers cx;
cx[0x318] = static_cast<std::uint32_t>(address >> 8u);
cx[0x31b] = slice | (slice << 13u);
cx[0x31c] = Color32FloatInfo;
cx[0x31d] = 0;
cx[0x3b0] = ((Side - 1u) << 14u) | (Side - 1u);
cx[0x3b8] = (TiledAttrib3 & ~(0x1fu << 14u)) | (swizzleMode << 14u) | (collapsed ? 0u : Layers - 1u);
cx[0x3b8] = (TiledAttrib3 & ~(0x1fu << 14u)) | (swizzleMode << 14u) | (Layers - 1u);
cx[0x390] = static_cast<std::uint32_t>(address >> 40u);
const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0);
Require(color.format == VK_FORMAT_R32_SFLOAT && color.bytes == LayerBytes && color.address == address + slice * LayerBytes, "the COLOR_32 float view did not decode as a 128x128 R32 slice of the array");
if (collapsed) Require(color.arrayAddress == 0 && color.arrayLastSlice == 0, "a collapsed slice view unexpectedly describes a whole array");
else Require(color.arrayAddress == address && color.arrayLastSlice == Layers - 1u && color.arraySlice == slice, "the color view did not keep its array and slice");
Require(color.arrayAddress == address && color.arrayLastSlice == Layers - 1u && color.arraySlice == slice, "the color view did not keep its array and slice");
return color;
}
void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t slice, std::uint32_t swizzleMode, bool collapsed) {
void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t slice, std::uint32_t swizzleMode) {
const auto target = device.Target();
constexpr std::uint32_t waveSize = 64;
std::size_t at = 0;
@ -208,7 +207,7 @@ void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint
const auto vertexResult = ShaderRecompiler::Recompile(vertex);
const auto vertexPush = static_cast<std::uint32_t>(vertexResult.pushConstants.size());
const auto color = DecodeTarget(surface, slice, swizzleMode, collapsed);
const auto color = DecodeTarget(surface, slice, swizzleMode);
VkPipelineColorBlendAttachmentState blend{};
blend.colorWriteMask = 0xf;
AgcDriver::Graphics::State state{};
@ -247,31 +246,29 @@ void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint
device.WaitIdle();
}
void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode, bool collapsed = false, std::uint32_t slice = 1) {
void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode) {
const auto surface = AddressOf(texels);
std::fill_n(reinterpret_cast<std::uint32_t*>(texels), SurfaceBytes / 4u, Initial);
AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes);
AgcDriver::GuestMemory::BumpCollectEpoch();
std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex());
const auto drawn = std::bit_cast<std::uint32_t>(Drawn);
DrawSlice(device, surface, slice, swizzleMode, collapsed);
const auto pending = StorageTexture::FindPending(collapsed ? surface + slice * LayerBytes : surface, collapsed ? LayerBytes : SurfaceBytes);
if (collapsed) Require(pending != nullptr && pending->Descriptor().dimension == TextureDimension::k2D && pending->Descriptor().baseAddress == surface + slice * LayerBytes, "a collapsed slice did not render into its shifted surface");
else Require(pending != nullptr && pending->Descriptor().dimension == TextureDimension::k2DArray && pending->Descriptor().baseAddress == surface && pending->Descriptor().depthOrLastArray == Layers - 1u, "the draw into slice 1 did not render into the image of its array");
LoadLayer(device, surface, slice, swizzleMode);
RequireOutput(drawn, Initial, "slice " + std::to_string(slice) + " read through the array after the draw");
for (std::uint32_t other = 0; other < Layers; ++other) {
if (other == slice) continue;
DrawSlice(device, surface, 1, swizzleMode);
const auto pending = StorageTexture::FindPending(surface, SurfaceBytes);
Require(pending != nullptr && pending->Descriptor().dimension == TextureDimension::k2DArray && pending->Descriptor().baseAddress == surface && pending->Descriptor().depthOrLastArray == Layers - 1u, "the draw into slice 1 did not render into the image of its array");
LoadLayer(device, surface, 1, swizzleMode);
RequireOutput(drawn, Initial, "slice 1 read through the array after the draw");
for (const std::uint32_t other : {0u, 2u, 3u}) {
LoadLayer(device, surface, other, swizzleMode);
RequireOutput(Initial, Initial, "slice " + std::to_string(other) + " after the draw into slice " + std::to_string(slice));
RequireOutput(Initial, Initial, "slice " + std::to_string(other) + " after the draw into slice 1");
}
StorageTexture::FlushPending(surface, SurfaceBytes, nullptr, "test");
device.WaitIdle();
AgcDriver::Graphics::ClearCachedTextures(device.Device());
AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes);
AgcDriver::GuestMemory::BumpCollectEpoch();
LoadLayer(device, surface, slice, swizzleMode);
RequireOutput(drawn, Initial, "slice " + std::to_string(slice) + " uploaded again from guest memory");
LoadLayer(device, surface, 1, swizzleMode);
RequireOutput(drawn, Initial, "slice 1 uploaded again from guest memory");
AgcDriver::Graphics::ClearCachedTextures(device.Device());
}
@ -284,7 +281,6 @@ int main() {
if (!device) return VulkanTestSkipped;
Run(*device, block.Data(), TileR64KBX);
Run(*device, block.Data(), static_cast<std::uint32_t>(AgcDriver::Graphics::ColorTileMode::D64KBX));
for (std::uint32_t slice = 1; slice < Layers; ++slice) Run(*device, block.Data(), TileR64KBX, true, slice);
std::puts("array layer target tests passed");
return 0;
} catch (const std::exception& error) {

Some files were not shown because too many files have changed in this diff Show More