Merge upstream/main into fix/recompiler-descriptor-phi-web

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chad 2026-10-08 14:17:07 -05:00
commit cd43c270d6
624 changed files with 26549 additions and 4073 deletions

View File

@ -20,7 +20,73 @@ env:
-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
jobs:
changes:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
relinker: ${{ steps.filter.outputs.relinker }}
code: ${{ steps.filter.outputs.code }}
steps:
- uses: dorny/paths-filter@v4
id: filter
with:
predicate-quantifier: some-with-excludes
filters: |
relinker:
- 'core/relinker/**'
- 'core/libs/prx/libc/src/specifics/windows/**'
- 'CMakeLists.txt'
- '.github/workflows/build.yml'
code:
- '**'
- '!**/*.md'
- '!docs/**'
relinker:
needs: changes
if: ${{ !cancelled() && (needs.changes.result != 'success' || needs.changes.outputs.relinker == 'true') }}
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
compiler: g++
- os: windows-latest
compiler: g++
- os: macos-latest
compiler: clang++
env:
WINLIBS_RELEASE: 15.2.0posix-14.0.0-ucrt-r7
WINLIBS_ZIP: winlibs-x86_64-posix-seh-gcc-15.2.0-mingw-w64ucrt-14.0.0-r7.zip
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v7
- if: runner.os == 'Windows'
id: toolchain
uses: actions/cache@v6
with:
path: C:\winlibs
key: ${{ env.WINLIBS_ZIP }}
- if: runner.os == 'Windows' && steps.toolchain.outputs.cache-hit != 'true'
run: |
curl -sSfL -o "$RUNNER_TEMP/winlibs.zip" "https://github.com/brechtsanders/winlibs_mingw/releases/download/$WINLIBS_RELEASE/$WINLIBS_ZIP"
7z x "$RUNNER_TEMP/winlibs.zip" -oC:/winlibs > /dev/null
- if: runner.os == 'Windows'
run: echo 'C:\winlibs\mingw64\bin' >> "$GITHUB_PATH"
- run: cmake -S . -B build-relinker -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=${{ matrix.compiler }} -DANYPS5_RELINKER_ONLY=ON -DBUILD_TESTING=ON
- run: cmake --build build-relinker --parallel 3
- run: ctest --test-dir build-relinker --output-on-failure --timeout 120
linux:
needs: changes
if: ${{ !cancelled() && (needs.changes.result != 'success' || needs.changes.outputs.code == 'true') }}
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
@ -34,15 +100,26 @@ jobs:
evict-old-files: job
- name: Install lavapipe for the GPU execution tests
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libvulkan1 mesa-vulkan-drivers
- run: cmake -S . -B build $CONFIGURE
- 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
- run: cmake --build build
- run: cmake --build build --target libs
- run: ctest --test-dir build --output-on-failure --timeout 120 --repeat until-fail:3
- run: ctest --test-dir build --output-on-failure --timeout 120 --repeat until-fail:3 --parallel 4 --output-log ctest.log
env:
ANYPS5_REQUIRE_VULKAN: 1
GALLIUM_OVERRIDE_CPU_CAPS: avx
- name: Report tests slower than 30 s
if: ${{ !cancelled() }}
shell: bash
run: |
[ -f ctest.log ] || exit 0
awk '/Test +#[0-9]+: / && $NF == "sec" { for (i = 1; i <= NF; i++) if ($i ~ /^#[0-9]+:$/) name = $(i + 1); t = $(NF - 1) + 0; if (t > max[name]) max[name] = t }
END { for (name in max) if (max[name] > 30) printf "::warning title=Slow test::%s took %.1f s on %s; tests must finish in 30 s (CONTRIBUTING.md)\n", name, max[name], ENVIRON["RUNNER_OS"] }' ctest.log
windows:
needs: changes
if: ${{ !cancelled() && (needs.changes.result != 'success' || needs.changes.outputs.code == 'true') }}
runs-on: windows-latest
timeout-minutes: 45
env:
@ -73,9 +150,19 @@ jobs:
evict-old-files: job
- shell: cmd
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
- shell: cmd
run: cmake --build build
- shell: cmd
run: cmake --build build --target libs
- shell: cmd
run: ctest --test-dir build --output-on-failure --timeout 120 --repeat until-fail:3
run: ctest --test-dir build --output-on-failure --timeout 120 --repeat until-fail:3 --parallel 4 --output-log ctest.log
- name: Report tests slower than 30 s
if: ${{ !cancelled() }}
shell: bash
run: |
[ -f ctest.log ] || exit 0
awk '/Test +#[0-9]+: / && $NF == "sec" { for (i = 1; i <= NF; i++) if ($i ~ /^#[0-9]+:$/) name = $(i + 1); t = $(NF - 1) + 0; if (t > max[name]) max[name] = t }
END { for (name in max) if (max[name] > 30) printf "::warning title=Slow test::%s took %.1f s on %s; tests must finish in 30 s (CONTRIBUTING.md)\n", name, max[name], ENVIRON["RUNNER_OS"] }' ctest.log

View File

@ -2,6 +2,10 @@ name: Progress report
on:
pull_request:
paths:
- 'core/libs/prx/**'
- 'tools/progress.py'
- '.github/workflows/progress-report.yml'
permissions:
contents: read

View File

@ -9,6 +9,7 @@ function(add_test_executable target)
endfunction()
option(BUILD_TESTING "Build and register relinker tests" OFF)
option(ANYPS5_RELINKER_ONLY "Build the relinker without system libraries or third-party dependencies" OFF)
if(BUILD_TESTING)
enable_testing()
if(MINGW)
@ -23,13 +24,30 @@ if(BUILD_TESTING)
endif()
option(ANYPS5_ENABLE_SPIRV_TOOLS "Enable SPIRV-Tools validation and optimization" OFF)
option(APS5_ENABLE_TIMING_LOG "Enable frame timing logging" OFF)
option(APS5_ENABLE_TIMING_LOG "Write frame timings asynchronously to frame-timing.log" OFF)
add_compile_definitions(APS5_ENABLE_TIMING_LOG=$<BOOL:${APS5_ENABLE_TIMING_LOG}>)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static -static-libgcc -static-libstdc++")
if(NOT APPLE)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static -static-libgcc -static-libstdc++")
endif()
if(BUILD_TESTING)
find_package(Python3 QUIET COMPONENTS Interpreter)
if(Python3_Interpreter_FOUND)
add_test(NAME progress_report COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress.py)
add_test(NAME import_audit COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_import_audit.py)
endif()
endif()
if(ANYPS5_RELINKER_ONLY)
add_subdirectory(core/relinker)
return()
endif()
set(SDL_SHARED OFF CACHE BOOL "" FORCE)
set(SDL_STATIC ON CACHE BOOL "" FORCE)

View File

@ -13,10 +13,13 @@
- The relinker uses only the C++20 standard library.
- Third-party code is added as a submodule under `3rdparty/` and built from source, not found on the system.
- Don't add tests that only check that a symbol is exported: a missing export already fails at startup.
- Each test finishes in 30 s on the CI runners, Vulkan tests included (Linux runs them on lavapipe), counting the first run with no shader cache. ctest runs in parallel there, so a slow test competes for the cores and times out on some runs and not others. Split or shrink a test that gets close instead of raising its timeout. CI reports every test over 30 s as a warning on the pull request.
## Build and test
Toolchains are listed in the [README](README.md#build).
Toolchains are listed in the [build instructions](docs/dev/BUILD.md).
For relinker changes, use the [relinker-only build](docs/dev/BUILD.md#relinker-only) to build and test without third-party submodules, system libraries or a GPU. It also works on macOS. Changes to system libraries or shaders still need the full build and relevant runtime tests.
```
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON

View File

@ -131,6 +131,15 @@ endforeach()
add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS})
if(BUILD_TESTING)
add_test_executable(guest_offline_imports_tests tests/GuestOfflineImports.cpp)
target_include_directories(guest_offline_imports_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_offline_imports_tests PRIVATE libSceHttp libSceNpEntitlementAccess libSceRudp libc)
configure_windows_unwind(guest_offline_imports_tests)
add_test(NAME guest_offline_imports COMMAND guest_offline_imports_tests)
if(WIN32)
set_tests_properties(guest_offline_imports PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceHttp>")
endif()
add_test_executable(savedata_memory_metadata_tests tests/SaveDataMemoryMetadata.cpp)
target_include_directories(savedata_memory_metadata_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(savedata_memory_metadata_tests PRIVATE libSceSaveData.native)

View File

@ -1219,6 +1219,8 @@ using NetCtlCallback = void (*)(int, void*);
struct HttpEpoll {};
using HttpEpollHandle = HttpEpoll*;
using HttpsCallback = int (*)(int, unsigned int, void* const*, int, void*);
using HttpRedirectCallback = int (*)(int, std::int32_t, std::int32_t*, const char*, void*);
using HttpCookieRecvCallback = int (*)(int, const char*, const char*, std::uint64_t, void*);
struct HttpNBEvent { std::uint8_t opaque[64]; };
@ -1243,6 +1245,14 @@ struct Http2AsyncResult {
void* reserved;
};
struct Http2AsyncOption {
KernelEqueue equeue;
int user_event_id;
std::uint8_t padding[4];
void* user_data;
void* reserved;
};
struct NpTitleId { char data[13]; char pad[3]; };
struct NpTitleSecret { std::uint8_t data[128]; };
struct NpContentRestriction { std::uint8_t opaque[128]; };
@ -1266,6 +1276,10 @@ struct NpEntitlementAccessAddcontEntitlementInfo {
std::uint32_t download_status;
};
struct NpEntitlementAccessEntitlementKey {
std::uint8_t data[16];
};
struct NpUniversalDataSystemInitParam {
@ -1375,7 +1389,7 @@ struct SaveDataMountInfo {
};
struct SceSaveDataTitleId { char data[10]; char pad[2]; };
struct SceSaveDataDirName { char data[33]; char pad[3]; };
struct SceSaveDataDirName { char data[32]; };
struct SaveDataSearchInfo { std::uint8_t opaque[128]; };
struct SaveDataMemoryData { void* buf; std::size_t buf_size; std::size_t offset; };
@ -1838,12 +1852,6 @@ struct LibcHeapInfo {
using Info = LibcHeapInfo;
#define VA_ARGS \
std::uint64_t rdi, std::uint64_t rsi, std::uint64_t rdx, std::uint64_t rcx, \
std::uint64_t r8, std::uint64_t r9, std::uint64_t overflow_arg_area, \
__m128 xmm0, __m128 xmm1, __m128 xmm2, __m128 xmm3, \
__m128 xmm4, __m128 xmm5, __m128 xmm6, __m128 xmm7, ...
struct Packet {
std::uint32_t* addr;
std::uint32_t dw_num;

View File

@ -91,6 +91,7 @@ inline bool IsDynamicNameTag(std::int64_t tag) {
return tag == kDtNeeded || tag == kDtSoname || tag == kDtRpath || tag == kDtRunpath;
}
constexpr std::uint32_t kShtDynamic = 6u;
constexpr std::uint32_t kShtGnuVerdef = 0x6ffffffdu;
constexpr std::uint32_t kShtGnuVerneed = 0x6ffffffeu;
constexpr std::uint32_t kShtDynsym = 11u;
constexpr std::uint32_t kShtGnuHash = 0x6ffffff6u;

View File

@ -337,6 +337,8 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
}
if (shdr.sh_type == kShtGnuVersym)
versymOffset = static_cast<std::size_t>(shdr.sh_offset);
if (shdr.sh_type == kShtGnuVerdef)
throw std::runtime_error("unsupported .gnu.version_d section: symbol version definitions cannot be preserved");
}
if (dynSymOffset == 0u) throw std::runtime_error("no .dynsym section");

View File

@ -0,0 +1,209 @@
import re
import struct
import subprocess
import sys
import tempfile
from pathlib import Path
SHT_NULL = 0
SHT_STRTAB = 3
SHT_DYNSYM = 11
SHT_GNU_HASH = 0x6FFFFFF6
SHT_GNU_VERDEF = 0x6FFFFFFD
SHT_GNU_VERSYM = 0x6FFFFFFF
SHN_UNDEF = 0
SHN_ABS = 0xFFF1
STB_GLOBAL = 1
STT_FUNC = 2
LIBRARY_NAME = "libsymbols"
DYNSYM_OFFSET = 0x200
DYNSTR_OFFSET = 0x300
HASH_OFFSET = 0x500
VERSYM_OFFSET = 0x580
VERDEF_OFFSET = 0x600
SHOFF = 0x800
BUCKETS = 4
HASHED_FROM = 3
NID_NAME = re.compile(r"^[A-Za-z0-9+-]{11}$")
SONAME = "libsymbols.so"
VERSIONS = [(1, [SONAME]), (2, ["VERS_ONE"]), (3, ["VERS_TWO", "VERS_ONE"])]
MARKERS = [("VERS_ONE", 2, 0x1100), ("VERS_TWO", 3, 0x1101)]
DEFINITIONS = [
("sceAlphaComputeLength", 2, 0x1200),
("sceAlphaCompareStrings", 2, 0x1220),
("sceAlphaFindCharacter", 2, 0x1240),
("sceBetaCopyString", 3, 0x1260),
("sceBetaConcatenateStrings", 3, 0x1280),
("sceBetaSearchCharacter", 3, 0x12A0),
]
IMPORTS = ["jeJEvW13vxk", "e0m0E5Sq2N8"]
def gnu_hash(name):
value = 5381
for byte in name.encode("utf-8"):
value = (value * 33 + byte) & 0xFFFFFFFF
return value
def symbols(versioned):
entries = [(None, 1, SHN_UNDEF, 0, 0)]
entries += [(name, 1, SHN_UNDEF, 0, 0) for name in IMPORTS]
if versioned:
entries += [(name, version, SHN_ABS, 0, value) for name, version, value in MARKERS]
entries += [(name, version, 9, STT_FUNC, value) for name, version, value in DEFINITIONS]
else:
entries += [(name, 1, 9, STT_FUNC, value) for name, _, value in DEFINITIONS]
return entries
def string_table(names):
offsets = {}
table = bytearray(b"\x00")
for name in names:
offsets.setdefault(name, len(table))
table += name.encode("utf-8") + b"\x00"
return table, offsets
def build_image(entries, versioned):
image = bytearray(0x1000)
image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9)
names = [entry[0] for entry in entries if entry[0]]
if versioned:
names += [name for _, version_names in VERSIONS for name in version_names]
strings, offsets = string_table(names)
for index, (name, version, shndx, symbol_type, value) in enumerate(entries):
info = (STB_GLOBAL << 4) | symbol_type
struct.pack_into("<IBBHQQ", image, DYNSYM_OFFSET + index * 24,
offsets[name] if name else 0, info, 0, shndx, value, 0x10 if symbol_type else 0)
image[DYNSTR_OFFSET:DYNSTR_OFFSET + len(strings)] = strings
chain_count = len(entries) - HASHED_FROM
bucket_offset = HASH_OFFSET + 16 + 8
chain_offset = bucket_offset + BUCKETS * 4
struct.pack_into("<IIII", image, HASH_OFFSET, BUCKETS, HASHED_FROM, 1, 6)
for index in range(HASHED_FROM, len(entries)):
name = entries[index][0]
bucket = gnu_hash(name) % BUCKETS
if struct.unpack_from("<I", image, bucket_offset + bucket * 4)[0] == 0:
struct.pack_into("<I", image, bucket_offset + bucket * 4, index)
chain = gnu_hash(name)
if index + 1 == len(entries) or gnu_hash(entries[index + 1][0]) % BUCKETS != bucket:
chain |= 1
struct.pack_into("<I", image, chain_offset + (index - HASHED_FROM) * 4, chain)
verdef_end = 0
if versioned:
for index, (_, version, _, _, _) in enumerate(entries):
struct.pack_into("<H", image, VERSYM_OFFSET + index * 2, version)
position = VERDEF_OFFSET
for version_index, version_names in VERSIONS:
entry_size = 20 + 8 * len(version_names)
struct.pack_into("<HHHHIII", image, position, 1, 1 if version_index == 1 else 0, version_index,
len(version_names), gnu_hash(version_names[0]), 20,
0 if version_index == len(VERSIONS) else entry_size)
aux = position + 20
for name_index, name in enumerate(version_names):
struct.pack_into("<II", image, aux, offsets[name], 0 if name_index + 1 == len(version_names) else 8)
aux += 8
position += entry_size
verdef_end = position
sections = [
(0, SHT_NULL, 0, 0, 0, 0, 0, 0, 0, 0),
(0, SHT_DYNSYM, 2, 0, DYNSYM_OFFSET, len(entries) * 24, 2, 1, 8, 24),
(0, SHT_STRTAB, 2, 0, DYNSTR_OFFSET, len(strings), 0, 0, 1, 0),
(0, SHT_GNU_HASH, 2, 0, HASH_OFFSET, chain_offset - HASH_OFFSET + chain_count * 4, 1, 0, 8, 0),
]
if versioned:
sections.append((0, SHT_GNU_VERSYM, 2, 0, VERSYM_OFFSET, len(entries) * 2, 1, 0, 2, 2))
sections.append((0, SHT_GNU_VERDEF, 2, 0, VERDEF_OFFSET, verdef_end - VERDEF_OFFSET, 2, len(VERSIONS), 4, 0))
struct.pack_into("<HHIQQQIHHHHHH", image, 16, 3, 62, 1, 0, 0, SHOFF, 0, 64, 0, 0, 64, len(sections), 0)
for index, section in enumerate(sections):
struct.pack_into("<IIQQQQIIQQ", image, SHOFF + index * 64, *section)
return image
def sections(image):
shoff, = struct.unpack_from("<Q", image, 0x28)
shentsize, shnum = struct.unpack_from("<HH", image, 0x3A)
return [struct.unpack_from("<IIQQQQIIQQ", image, shoff + index * shentsize) for index in range(shnum)]
def read_dynsym(image):
headers = sections(image)
dynsym = next(section for section in headers if section[1] == SHT_DYNSYM)
strings = headers[dynsym[6]]
entries = []
for index in range(dynsym[5] // 24):
name_offset, _, _, shndx, value, _ = struct.unpack_from("<IBBHQQ", image, dynsym[4] + index * 24)
name = None
if name_offset:
end = image.index(0, strings[4] + name_offset)
name = image[strings[4] + name_offset:end].decode("utf-8")
entries.append((name, shndx, value))
return entries
def patch(patcher, image, filename):
with tempfile.TemporaryDirectory(prefix="anyps5-symbol-versions-") as directory:
path = Path(directory) / filename
path.write_bytes(image)
result = subprocess.run([patcher, LIBRARY_NAME, str(path)], capture_output=True)
return result, path.read_bytes()
def test_versioned_library_is_rejected(patcher):
image = bytes(build_image(symbols(True), True))
assert any(section[1] == SHT_GNU_VERDEF for section in sections(image))
result, on_disk = patch(patcher, image, SONAME)
assert result.returncode == 2, (result.returncode, result.stderr)
message = result.stderr.decode("utf-8", errors="replace")
assert message.startswith("FAIL: "), message
assert ".gnu.version_d" in message, message
assert result.stdout == b"", result.stdout
assert on_disk == image, "the rejected library was written back modified"
def test_unversioned_library_is_still_patched(patcher):
image = bytes(build_image(symbols(False), False))
assert not any(section[1] == SHT_GNU_VERDEF for section in sections(image))
before = read_dynsym(image)
result, patched = patch(patcher, image, SONAME)
assert result.returncode == 0, result.stderr.decode("utf-8", errors="replace")
assert result.stdout.decode("utf-8", errors="replace").startswith("OK: "), result.stdout
after = read_dynsym(patched)
assert len(after) == len(before), (before, after)
assert [entry[0] for entry in after] != [entry[0] for entry in before], "no export was renamed"
for name, _, value in DEFINITIONS:
found = [entry for entry in after if entry[2] == value]
assert len(found) == 1, (name, found)
renamed, shndx, _ = found[0]
assert renamed != name, (name, found)
assert NID_NAME.match(renamed), (name, found)
assert shndx == 9, (name, found)
for name in IMPORTS:
found = [entry for entry in after if entry[0] == name]
assert found == [(name, SHN_UNDEF, 0)], (name, found)
assert len({entry[0] for entry in after if entry[0]}) == len([entry for entry in after if entry[0]])
def main():
patcher = Path(sys.argv[1]).resolve()
test_versioned_library_is_rejected(patcher)
test_unversioned_library_is_still_patched(patcher)
print("NID patcher symbol version tests passed")
if __name__ == "__main__":
main()

View File

@ -12,8 +12,7 @@
extern "C" {
// unknown signature
APS5_EXPORT("gQkqkLttcpw", sceAgcAcb_gQkqkLttcpw);
void* APS5_VABI sceAgcAcb_gQkqkLttcpw (void) {
void* APS5_VABI sceAgcAcbAtomicGds_0900(void) {
NotImplemented_nid_no_patch(__func__);
return nullptr;
}

View File

@ -209,14 +209,15 @@ if(BUILD_TESTING)
endif()
add_test_executable(agc_shader_fusion_tests EXCLUDE_FROM_ALL tests/ShaderFusion.cpp)
target_include_directories(agc_shader_fusion_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_shader_fusion_tests PRIVATE libSceAgc libc)
target_include_directories(agc_shader_fusion_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include)
target_link_libraries(agc_shader_fusion_tests PRIVATE libSceAgc libSceAgcDriver libc)
set_target_properties(agc_shader_fusion_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_shader_fusion_tests)
if(BUILD_TESTING)
set_target_properties(agc_shader_fusion_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_shader_fusion COMMAND agc_shader_fusion_tests)
set_tests_properties(agc_shader_fusion PROPERTIES SKIP_RETURN_CODE 77)
if(WIN32)
set_tests_properties(agc_shader_fusion PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
@ -236,9 +237,13 @@ if(BUILD_TESTING)
endif()
endif()
add_test_executable(agc_interpolant_mapping_tests EXCLUDE_FROM_ALL tests/InterpolantMapping.cpp)
add_test_executable(agc_interpolant_mapping_tests EXCLUDE_FROM_ALL
tests/InterpolantMapping.cpp
Shader/src/InterpolantMapping.cpp
Shader/src/ShaderUtils.cpp
)
target_include_directories(agc_interpolant_mapping_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_interpolant_mapping_tests PRIVATE libSceAgc libc)
target_link_libraries(agc_interpolant_mapping_tests PRIVATE libSceAgcDriver libc)
set_target_properties(agc_interpolant_mapping_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_interpolant_mapping_tests)
@ -252,9 +257,15 @@ if(BUILD_TESTING)
endif()
if(BUILD_TESTING)
add_test_executable(guest_shader_link_tests ${LIBS_INCLUDE_DIR}/tests/GuestShaderLink.cpp)
add_test_executable(guest_shader_link_tests
${LIBS_INCLUDE_DIR}/tests/GuestShaderLink.cpp
Shader/src/LinkShaders.cpp
Shader/src/PrimState.cpp
Shader/src/InterpolantMapping.cpp
Shader/src/ShaderUtils.cpp
)
target_include_directories(guest_shader_link_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_shader_link_tests PRIVATE libSceAgc libc)
target_link_libraries(guest_shader_link_tests PRIVATE libSceAgc libSceAgcDriver libc)
configure_windows_unwind(guest_shader_link_tests)
add_test(NAME guest_shader_link COMMAND guest_shader_link_tests)
if(WIN32)
@ -358,3 +369,22 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_test_executable(agc_shader_helpers_tests EXCLUDE_FROM_ALL
tests/ShaderHelpers.cpp
Shader/src/LinkShaders.cpp
Shader/src/PrimState.cpp
Shader/src/InterpolantMapping.cpp
Shader/src/ShaderUtils.cpp
)
target_include_directories(agc_shader_helpers_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_shader_helpers_tests PRIVATE libc libSceAgcDriver)
configure_windows_unwind(agc_shader_helpers_tests)
if(BUILD_TESTING)
set_target_properties(agc_shader_helpers_tests PROPERTIES EXCLUDE_FROM_ALL FALSE)
add_test(NAME agc_shader_helpers COMMAND agc_shader_helpers_tests)
if(WIN32)
set_tests_properties(agc_shader_helpers PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()

View File

@ -40,7 +40,7 @@ std::uint32_t available(const CommandBuffer& buffer, const char* function) {
const auto bottom = reinterpret_cast<std::uintptr_t>(buffer.bottom);
const auto top = reinterpret_cast<std::uintptr_t>(buffer.top);
const auto up = reinterpret_cast<std::uintptr_t>(buffer.cursor_up);
const auto down = reinterpret_cast<std::uintptr_t>(buffer.cursor_down);
const auto down = buffer.cursor_down != nullptr ? reinterpret_cast<std::uintptr_t>(buffer.cursor_down) : top;
Require(((bottom | top | up | down) & 3u) == 0, function, "misaligned command buffer");
Require(bottom <= up && up <= down && down <= top, function, "invalid command buffer cursors");
Require(bottom != 0 || top == 0, function, "null command buffer storage");

View File

@ -35,8 +35,7 @@ std::uint32_t APS5_VABI sceAgcDcbSetIndexSizeGetSize() {
return 12;
}
APS5_EXPORT("-KRzWekV120", sceAgcUnknown__MKRzWekV120);
std::uint32_t* APS5_VABI sceAgcUnknown__MKRzWekV120(CommandBuffer* buf, std::uint8_t indexSize, std::uint8_t cachePolicy, std::uint8_t perInstanceObjectId) {
std::uint32_t* APS5_VABI sceAgcDcbSetIndexSize_0600(CommandBuffer* buf, std::uint8_t indexSize, std::uint8_t cachePolicy, std::uint8_t perInstanceObjectId) {
Agc::Command::CheckBits(perInstanceObjectId, 1, __func__);
auto* packet = sceAgcDcbSetIndexSize(buf, indexSize, cachePolicy);
packet[2] |= static_cast<std::uint32_t>(perInstanceObjectId) << 14u;

View File

@ -26,8 +26,7 @@ uint64_t APS5_VABI sceAgcDcbContextStateOpGetSize(uint32_t operation) {
return 27 * sizeof(std::uint32_t);
}
APS5_EXPORT("qj7QZpgr9Uw", sceAgcDcbContextStateAnotherOp);
uint32_t* APS5_VABI sceAgcDcbContextStateAnotherOp(CommandBuffer* buf, uint32_t operation) {
uint32_t* APS5_VABI sceAgcDcbContextStateOp_0100(CommandBuffer* buf, uint32_t operation) {
Agc::Command::Require(buf != nullptr, __func__, "null command buffer");
Agc::Command::Require(operation <= 3, __func__, "invalid context state operation");
const auto* function = __func__;

View File

@ -9,8 +9,7 @@
extern "C" {
APS5_EXPORT("23LRUSvYu1M", sceAgcUnknownInitState);
int APS5_VABI sceAgcUnknownInitState(std::uint32_t* state, std::uint32_t version) {
int APS5_VABI sceAgcInit_0090(std::uint32_t* state, std::uint32_t version) {
Agc::Command::CheckAddress(reinterpret_cast<std::uintptr_t>(state), alignof(std::uint32_t), __func__);
Agc::Command::Require(version < 14, __func__, "unsupported AGC initialization version");
return 0;

View File

@ -10,7 +10,7 @@ struct SizeAlign {
std::size_t m_align;
};
extern "C" int APS5_VABI sceAgcUnknownFuseShaderHalves(Shader* fused_result, const Shader* front, const Shader* back, void* scratch_mem);
extern "C" int APS5_VABI sceAgcUnknownGetFusedShaderSize(SizeAlign* dst, const Shader* front, const Shader* back);
extern "C" int APS5_VABI sceAgcFuseShaderHalves_0200(Shader* fused_result, const Shader* front, const Shader* back, void* scratch_mem);
extern "C" int APS5_VABI sceAgcGetFusedShaderSize_0080(SizeAlign* dst, const Shader* front, const Shader* back);
#endif

View File

@ -17,8 +17,7 @@ uint32_t APS5_VABI sceAgcGetPacketSize(uint32_t* packet) {
return ((packet[0] >> 16u) & 0x3fffu) + 2u;
}
APS5_EXPORT("V++UgBtQhn0", sceAgcGetDataPacketPayloadAddressUnk);
int APS5_VABI sceAgcGetDataPacketPayloadAddressUnk(uint32_t** addr, uint32_t* cmd, int type) {
int APS5_VABI sceAgcGetDataPacketPayloadAddress_0090(uint32_t** addr, uint32_t* cmd, int type) {
Agc::Command::CheckAddress(reinterpret_cast<std::uintptr_t>(addr), alignof(uint32_t*), __func__);
Agc::Command::CheckAddress(reinterpret_cast<std::uintptr_t>(cmd), alignof(uint32_t), __func__);
if (type != 0) {

View File

@ -28,6 +28,7 @@ constexpr std::uint32_t SPI_SHADER_PGM_RSRC2_HS = 0x10Bu;
constexpr std::uint32_t SCodeEnd = 0xbf9f0000u;
constexpr std::uint32_t SNop = 0xbf800000u;
constexpr std::uint32_t SSetpcS6 = 0xbe802006u;
constexpr std::uint32_t SSwappcNullS6 = 0xbefd2106u;
constexpr std::uint8_t FusedCodeAlignmentLog2 = 8;
constexpr std::size_t FusedCodeAlignment = std::size_t{1} << FusedCodeAlignmentLog2;
constexpr char TrailerMagic[8] = {'b', 'a', 'r', 'e', 'f', 'o', 'o', 't'};
@ -51,7 +52,7 @@ ShaderRegister& FindRegister(ShaderRegister* regs, std::uint32_t count, std::uin
for (std::uint32_t i = 0; regs != nullptr && i < count; ++i) {
if (regs[i].offset == offset && occurrence-- == 0) return regs[i];
}
throw std::runtime_error("sceAgcUnknownFuseShaderHalves: shader half lacks register " + std::to_string(offset));
throw std::runtime_error("sceAgcFuseShaderHalves_0200: shader half lacks register " + std::to_string(offset));
}
void MergeMax(ShaderRegister& dst, const ShaderRegister& src, std::uint32_t shift, std::uint32_t mask) {
@ -114,7 +115,7 @@ std::uint32_t FrontProgramBytes(const char* function, const Shader* front) {
return value;
};
while (end > 0 && word(end - 1) == SCodeEnd) --end;
if (end == 0 || word(end - 1) != SSetpcS6) Fail(function, "front half does not end with s_setpc_b64 s[6:7]");
if (end == 0 || (word(end - 1) != SSetpcS6 && word(end - 1) != SSwappcNullS6)) Fail(function, "front half does not end with s_setpc_b64 s[6:7] or s_swappc_b64 null, s[6:7]");
return static_cast<std::uint32_t>((end - 1) * 4);
}
@ -187,11 +188,11 @@ void SetProgramAddress(std::vector<ShaderRegister>& regs, std::uint32_t loOffset
regs[i + 1].value = (regs[i + 1].value & 0xFFFFFF00u) | static_cast<std::uint32_t>((address >> 40u) & 0xFFu);
return;
}
throw std::runtime_error("sceAgcUnknownFuseShaderHalves: back half has no program address register pair");
throw std::runtime_error("sceAgcFuseShaderHalves_0200: back half has no program address register pair");
}
int FuseGeometryHalves(Shader* fused_result, const Shader* front, const Shader* back, void* scratch_mem) {
constexpr auto fn = "sceAgcUnknownFuseShaderHalves";
constexpr auto fn = "sceAgcFuseShaderHalves_0200";
if (scratch_mem == nullptr) Fail(fn, "geometry halves need fused shader memory");
auto layout = ComputeGeometryLayout(fn, front, back);
const auto scratch = reinterpret_cast<std::uintptr_t>(scratch_mem);
@ -251,8 +252,7 @@ int FuseGeometryHalves(Shader* fused_result, const Shader* front, const Shader*
extern "C" {
APS5_EXPORT("fd5Bp5tGTgo", sceAgcUnknownFuseShaderHalves);
int APS5_VABI sceAgcUnknownFuseShaderHalves(Shader* fused_result, const Shader* front, const Shader* back, void* scratch_mem) {
int APS5_VABI sceAgcFuseShaderHalves_0200(Shader* fused_result, const Shader* front, const Shader* back, void* scratch_mem) {
if (fused_result == nullptr || front == nullptr || back == nullptr) APS5_INVALID_ARG_EX;
if (!ValidHalves(front, back)) return GRAPHICS5_ERROR_INVALID_SHADER_HALVES;
const bool isGs = GeometryHalves(front);
@ -298,15 +298,14 @@ int APS5_VABI sceAgcUnknownFuseShaderHalves(Shader* fused_result, const Shader*
}
int APS5_VABI sceAgcFuseShaderHalves_nid_postfix(Shader* fused_result, const Shader* front, const Shader* back, void* scratch_mem) {
return sceAgcUnknownFuseShaderHalves(fused_result, front, back, scratch_mem);
return sceAgcFuseShaderHalves_0200(fused_result, front, back, scratch_mem);
}
APS5_EXPORT("dolOmWH+huQ", sceAgcUnknownGetFusedShaderSize);
int APS5_VABI sceAgcUnknownGetFusedShaderSize(SizeAlign* dst, const Shader* front, const Shader* back) {
int APS5_VABI sceAgcGetFusedShaderSize_0080(SizeAlign* dst, const Shader* front, const Shader* back) {
if (dst == nullptr || front == nullptr || back == nullptr) APS5_INVALID_ARG_EX;
if (!ValidHalves(front, back)) return GRAPHICS5_ERROR_INVALID_SHADER_HALVES;
if (GeometryHalves(front)) {
const auto layout = ComputeGeometryLayout("sceAgcUnknownGetFusedShaderSize", front, back);
const auto layout = ComputeGeometryLayout("sceAgcGetFusedShaderSize_0080", front, back);
dst->m_size = layout.totalBytes + FusedCodeAlignment - 1;
dst->m_align = FusedCodeAlignmentLog2;
return 0;
@ -316,8 +315,7 @@ int APS5_VABI sceAgcUnknownGetFusedShaderSize(SizeAlign* dst, const Shader* fron
return 0;
}
APS5_EXPORT("k0E7vkgqAuE", sceAgcCreateInterpolantMappingVsPs);
int APS5_VABI sceAgcCreateInterpolantMappingVsPs(ShaderRegister* regs, const Shader* vs, const Shader* ps) {
int APS5_VABI sceAgcUpdateInterpolantMapping_0100(ShaderRegister* regs, const Shader* vs, const Shader* ps) {
(void)regs;
(void)vs;
(void)ps;

View File

@ -28,8 +28,7 @@ int APS5_VABI sceAgcJumpPatchSetTarget(uint32_t* cmd, const volatile uint32_t* t
return 0;
}
APS5_EXPORT("Ikfdt-rIqCE", sceAgcUnknown_Ikfdt_MrIqCE);
int APS5_VABI sceAgcUnknown_Ikfdt_MrIqCE(uint32_t* cmd, uint64_t cache_policy, const volatile uint32_t* target, uint32_t size_in_dwords) {
int APS5_VABI sceAgcJumpPatchSetTarget_0300(uint32_t* cmd, uint64_t cache_policy, const volatile uint32_t* target, uint32_t size_in_dwords) {
Agc::Command::CheckBits(cache_policy, 3u, __func__);
PatchJump(cmd, target, size_in_dwords, __func__);
cmd[3] = (cmd[3] & ~(3u << 28u)) | (static_cast<std::uint32_t>(cache_policy) << 28u);

View File

@ -1,10 +1,14 @@
#include "prx/libSceAgc/Shader/include/InterpolantMapping.hpp"
#include <cstdio>
#include <array>
#include <algorithm>
#include <stdexcept>
#include <prx/libc/include/General.hpp>
#include "SceShaders.hpp"
#include "prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libSceAgc/Shader/include/ShaderUtils.hpp"
#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp"
@ -53,9 +57,22 @@ int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader*
if (regs == nullptr) {
throw std::runtime_error(std::string(fn) + ": regs is null");
}
if (ps != nullptr && ps->num_input_semantics > 32) {
throw std::runtime_error(std::string(fn) + ": input semantic count exceeds 32");
}
AgcDriver::ShaderPreparationScope transaction;
std::array<ShaderRegister, 32> values{};
auto* output = regs;
regs = values.data();
if (ps == nullptr || ps->num_input_semantics == 0) {
FillIdentityInterpolants(regs, 0);
if (ps != nullptr) {
AgcDriverResolveShaderAbi_nid_postfix(ps, {regs, 32}, {});
if (gs != nullptr) AgcDriverResolveGraphicsAbi_nid_postfix(gs, ps, 0);
}
transaction.Commit();
std::copy(values.begin(), values.end(), output);
return 0;
}
@ -82,6 +99,10 @@ int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader*
}
FillIdentityInterpolants(regs, ps->num_input_semantics);
AgcDriverResolveShaderAbi_nid_postfix(ps, {regs, 32}, {});
AgcDriverResolveGraphicsAbi_nid_postfix(gs, ps, 0);
transaction.Commit();
std::copy(values.begin(), values.end(), output);
return 0;
}

View File

@ -1,4 +1,6 @@
#include "SceShaders.hpp"
#include "prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp"
#include <array>
@ -20,10 +22,13 @@ extern "C" int APS5_VABI sceAgcLinkShaders(ShaderRegister* context, ShaderRegist
// The simple vertex/pixel pipeline combines two primitive context registers
// and 32 interpolant registers. Publish only after both helpers succeed.
AgcDriver::ShaderPreparationScope transaction;
std::array<ShaderRegister, 34> contextValues{};
std::array<ShaderRegister, 3> primitiveValues{};
sceAgcCreatePrimState(contextValues.data(), primitiveValues.data(), nullptr, vertex, primitiveType);
sceAgcCreateInterpolantMapping(contextValues.data() + 2, vertex, pixel);
AgcDriverResolveGraphicsAbi_nid_postfix(vertex, pixel, primitiveType);
transaction.Commit();
std::copy(contextValues.begin(), contextValues.end(), context);
std::copy(primitiveValues.begin(), primitiveValues.end(), primitive);
return 0;

View File

@ -1,10 +1,13 @@
#include "prx/libSceAgc/Shader/include/PrimState.hpp"
#include <cstdio>
#include <array>
#include <algorithm>
#include <stdexcept>
#include <prx/libc/include/General.hpp>
#include "SceShaders.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libSceAgc/Shader/include/ShaderUtils.hpp"
#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp"
@ -22,44 +25,26 @@ int APS5_VABI sceAgcCreatePrimState(ShaderRegister* cx_regs, ShaderRegister* uc_
throw std::runtime_error(std::string(__func__) + ": invalid shader type or missing special registers");
}
(void)GraphicsPrimTypeToGsOut(prim_type);
if (cx_regs != nullptr) {
const auto valid = [](const Shader* shader) { return shader->specials->vgt_shader_stages_en.offset == ShaderRegs::VGT_SHADER_STAGES_EN && shader->specials->vgt_gs_out_prim_type.offset == ShaderRegs::VGT_GS_OUT_PRIM_TYPE; };
if (!valid(gs) || (hs != nullptr && !valid(hs))) {
throw std::runtime_error(std::string(__func__) + ": invalid context register offsets");
}
const auto valid = [](const Shader* shader) { return shader->specials->vgt_shader_stages_en.offset == ShaderRegs::VGT_SHADER_STAGES_EN && shader->specials->vgt_gs_out_prim_type.offset == ShaderRegs::VGT_GS_OUT_PRIM_TYPE; };
if (!valid(gs) || (hs != nullptr && !valid(hs))) {
throw std::runtime_error(std::string(__func__) + ": invalid context register offsets");
}
if (uc_regs != nullptr && (gs->specials->ge_cntl.offset != ShaderRegs::GE_CNTL || gs->specials->ge_user_vgpr_en.offset != ShaderRegs::GE_USER_VGPR_EN || (hs != nullptr && hs->specials->ge_user_vgpr_en.offset != ShaderRegs::GE_USER_VGPR_EN))) {
if (gs->specials->ge_cntl.offset != ShaderRegs::GE_CNTL || gs->specials->ge_user_vgpr_en.offset != ShaderRegs::GE_USER_VGPR_EN || (hs != nullptr && hs->specials->ge_user_vgpr_en.offset != ShaderRegs::GE_USER_VGPR_EN)) {
throw std::runtime_error(std::string(__func__) + ": invalid user configuration register offsets");
}
if (cx_regs != nullptr) {
cx_regs[0] = gs->specials->vgt_shader_stages_en;
if ((cx_regs[0].value & ShaderRegs::VGT_SHADER_STAGES_GS_BIT) != 0) {
cx_regs[1] = gs->specials->vgt_gs_out_prim_type;
} else {
cx_regs[1].offset = ShaderRegs::VGT_GS_OUT_PRIM_TYPE;
cx_regs[1].value = GraphicsPrimTypeToGsOut(prim_type);
}
if (hs != nullptr) {
cx_regs[0].value |= hs->specials->vgt_shader_stages_en.value;
if ((cx_regs[0].value & ShaderRegs::VGT_SHADER_STAGES_GS_BIT) == 0) {
cx_regs[1] = hs->specials->vgt_gs_out_prim_type;
}
}
std::array<ShaderRegister, 2> contextValues{gs->specials->vgt_shader_stages_en, gs->specials->vgt_gs_out_prim_type};
if ((contextValues[0].value & ShaderRegs::VGT_SHADER_STAGES_GS_BIT) == 0) {
contextValues[1] = {ShaderRegs::VGT_GS_OUT_PRIM_TYPE, GraphicsPrimTypeToGsOut(prim_type)};
}
if (uc_regs != nullptr) {
uc_regs[0] = gs->specials->ge_cntl;
uc_regs[1] = gs->specials->ge_user_vgpr_en;
uc_regs[2].offset = ShaderRegs::VGT_PRIMITIVE_TYPE;
uc_regs[2].value = prim_type;
if (hs != nullptr) {
uc_regs[1] = hs->specials->ge_user_vgpr_en;
}
if (hs != nullptr) {
contextValues[0].value |= hs->specials->vgt_shader_stages_en.value;
if ((contextValues[0].value & ShaderRegs::VGT_SHADER_STAGES_GS_BIT) == 0) contextValues[1] = hs->specials->vgt_gs_out_prim_type;
}
std::array<ShaderRegister, 3> primitiveValues{gs->specials->ge_cntl, hs != nullptr ? hs->specials->ge_user_vgpr_en : gs->specials->ge_user_vgpr_en, ShaderRegister{ShaderRegs::VGT_PRIMITIVE_TYPE, prim_type}};
const std::array<const Shader*, 2> stages{hs, gs};
AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span(stages).subspan(hs == nullptr ? 1u : 0u), contextValues, primitiveValues);
if (cx_regs != nullptr) std::copy(contextValues.begin(), contextValues.end(), cx_regs);
if (uc_regs != nullptr) std::copy(primitiveValues.begin(), primitiveValues.end(), uc_regs);
return 0;
}

View File

@ -80,8 +80,7 @@ int APS5_VABI sceAgcCbMemsetExclusive() {
return 0;
}
APS5_EXPORT("7Wa3aeJgeVU", sceAgcUnknown_7Wa3aeJgeVU);
int APS5_VABI sceAgcUnknown_7Wa3aeJgeVU() {
int APS5_VABI sceAgcBranchPatchSetThenTarget_0300() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
@ -101,8 +100,7 @@ int APS5_VABI sceAgcGetShaderInstrumentation() {
return 0;
}
APS5_EXPORT("rP5xLdOf26k", sceAgcUnknown_rP5xLdOf26k);
int APS5_VABI sceAgcUnknown_rP5xLdOf26k() {
int APS5_VABI sceAgcBranchPatchSetElseTarget_0300() {
NotImplemented_nid_no_patch(__func__);
return 0;
}

View File

@ -18,15 +18,15 @@ extern "C" int APS5_VABI sceAgcInit(std::uint32_t version);
extern "C" void* APS5_VABI sceAgcGetRegisterDefaults();
extern "C" void* APS5_VABI sceAgcGetRegisterDefaultsInternal();
extern "C" void* APS5_VABI sceAgcGetRegisterDefaults2Internal(std::uint32_t version);
extern "C" int APS5_VABI sceAgcUnknownInitState(std::uint32_t* state, std::uint32_t version);
extern "C" int APS5_VABI sceAgcInit_0090(std::uint32_t* state, std::uint32_t version);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbDrawIndexAuto(CommandBuffer* buf, std::uint32_t indexCount, std::uint64_t modifier);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbDrawIndexIndirect(CommandBuffer* buf, std::uint32_t dataOffsetInBytes, std::uint64_t modifier);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbDrawIndexIndirectMulti(CommandBuffer* buf, std::uint32_t dataOffsetInBytes, std::uint32_t countIndirect, std::uint32_t maxCountOrCount, const volatile void* countAddress, std::uint32_t strideInBytes, std::uint64_t modifier);
extern "C" int APS5_VABI sceAgcWaitRegMemPatchReference(std::uint32_t* cmd, std::uint64_t reference);
extern "C" int APS5_VABI sceAgcWaitRegMemPatchMask(std::uint32_t* cmd, std::uint64_t mask);
extern "C" int APS5_VABI sceAgcGetDataPacketPayloadAddressUnk(std::uint32_t** addr, std::uint32_t* cmd, int type);
extern "C" int APS5_VABI sceAgcGetDataPacketPayloadAddress_0090(std::uint32_t** addr, std::uint32_t* cmd, int type);
extern "C" std::uint32_t* APS5_VABI sceAgcCbSetShRegisterRangeDirect(CommandBuffer* buf, std::uint32_t offset, const std::uint32_t* values, std::uint32_t numValues);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbContextStateAnotherOp(CommandBuffer* buf, std::uint32_t operation);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbContextStateOp_0100(CommandBuffer* buf, std::uint32_t operation);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbPushMarker(CommandBuffer* buf, const char* str, std::uint32_t color);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbPopMarker(CommandBuffer* buf);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetMarker(CommandBuffer* buf, const char* str, std::uint32_t color);
@ -101,6 +101,12 @@ void testPackets() {
exhausted.buffer.cursor_down = exhausted.words.data() + 2;
expectFailure([&] { Agc::Command::WriteNop(&exhausted.buffer, 3, __func__); });
check(exhausted.buffer.cursor_up == exhausted.words.data(), "failed allocation advanced cursor");
std::array<std::uint32_t, 8> scratch{};
CommandBuffer noDown{scratch.data(), scratch.data() + scratch.size(), scratch.data(), nullptr, nullptr, nullptr, 0};
Agc::Command::WriteNop(&noDown, 6, __func__);
check(noDown.cursor_up == scratch.data() + 6, "buffer without a down cursor did not use its top as the limit");
expectFailure([&] { Agc::Command::WriteNop(&noDown, 3, __func__); });
check(noDown.cursor_up == scratch.data() + 6, "allocation past the top of a buffer without a down cursor advanced its cursor");
}
void testClearState() {
@ -222,7 +228,7 @@ void testContextState() {
source.buffer.reserved_dw = 2;
source.buffer.callback = growContext;
source.buffer.user_data = &growth;
auto* first = sceAgcDcbContextStateAnotherOp(&source.buffer, operation);
auto* first = sceAgcDcbContextStateOp_0100(&source.buffer, operation);
check(first == (split == 0 ? growth.destination.words.data() : source.words.data()), "incorrect first context packet address");
check(growth.calls == (requested == 0 ? 0u : 1u), "incorrect context callback count");
auto* end = requested == 0 ? source.words.data() + totals[operation] : growth.destination.words.data() + totals[operation] - split;
@ -243,24 +249,24 @@ void testContextState() {
}
}
}
expectFailure([] { sceAgcDcbContextStateAnotherOp(nullptr, 0); });
expectFailure([] { sceAgcDcbContextStateOp_0100(nullptr, 0); });
Storage invalid;
expectFailure([&] { sceAgcDcbContextStateAnotherOp(&invalid.buffer, 4); });
expectFailure([&] { sceAgcDcbContextStateOp_0100(&invalid.buffer, 4); });
check(invalid.buffer.cursor_up == invalid.words.data(), "invalid context operation advanced cursor");
invalid.buffer.cursor_up = invalid.words.data() + 1;
invalid.buffer.cursor_down = invalid.words.data();
expectFailure([&] { sceAgcDcbContextStateAnotherOp(&invalid.buffer, 0); });
expectFailure([&] { sceAgcDcbContextStateOp_0100(&invalid.buffer, 0); });
invalid.buffer.cursor_up = invalid.words.data();
expectFailure([&] { sceAgcDcbContextStateAnotherOp(&invalid.buffer, 1); });
expectFailure([&] { sceAgcDcbContextStateOp_0100(&invalid.buffer, 1); });
ContextGrowth growth{{}, invalid.words.data(), 22};
invalid.buffer.callback = growContext;
invalid.buffer.user_data = &growth;
growth.success = false;
expectFailure([&] { sceAgcDcbContextStateAnotherOp(&invalid.buffer, 1); });
expectFailure([&] { sceAgcDcbContextStateOp_0100(&invalid.buffer, 1); });
growth.calls = 0;
growth.success = true;
growth.destination.buffer.cursor_down = growth.destination.words.data() + 21;
expectFailure([&] { sceAgcDcbContextStateAnotherOp(&invalid.buffer, 1); });
expectFailure([&] { sceAgcDcbContextStateOp_0100(&invalid.buffer, 1); });
check(invalid.buffer.cursor_up == growth.destination.words.data(), "failed reservation advanced cursor");
}
@ -322,19 +328,19 @@ void testPacketPayloadAddress() {
Storage storage;
auto* packet = sceAgcCbSetShRegisterRangeDirect(&storage.buffer, 0x8c, nullptr, 4);
std::uint32_t* payload = nullptr;
check(sceAgcGetDataPacketPayloadAddressUnk(&payload, packet, 1) == 0 && payload == packet + 2, "incorrect register packet payload address");
check(sceAgcGetDataPacketPayloadAddress_0090(&payload, packet, 1) == 0 && payload == packet + 2, "incorrect register packet payload address");
const std::array<std::uint32_t, 4> values{11, 22, 33, 44};
std::copy(values.begin(), values.end(), payload);
const std::array<std::uint32_t, 6> expected{0xc0047600u, 0x8c, 11, 22, 33, 44};
check(std::equal(expected.begin(), expected.end(), packet), "payload write corrupted register packet");
check(sceAgcGetDataPacketPayloadAddressUnk(&payload, packet, 0) == 0 && payload == packet + 1, "incorrect generic packet payload address");
check(sceAgcGetDataPacketPayloadAddress_0090(&payload, packet, 0) == 0 && payload == packet + 1, "incorrect generic packet payload address");
packet[0] = 0xffff1000u;
check(sceAgcGetDataPacketPayloadAddressUnk(&payload, packet, 0) == 0 && payload == nullptr, "empty payload marker was not recognized");
check(sceAgcGetDataPacketPayloadAddressUnk(&payload, packet, -1) == 0 && payload == packet + 2, "nonzero payload type did not skip two words");
expectFailure([&] { sceAgcGetDataPacketPayloadAddressUnk(nullptr, packet, 1); });
expectFailure([&] { sceAgcGetDataPacketPayloadAddressUnk(&payload, nullptr, 1); });
check(sceAgcGetDataPacketPayloadAddress_0090(&payload, packet, 0) == 0 && payload == nullptr, "empty payload marker was not recognized");
check(sceAgcGetDataPacketPayloadAddress_0090(&payload, packet, -1) == 0 && payload == packet + 2, "nonzero payload type did not skip two words");
expectFailure([&] { sceAgcGetDataPacketPayloadAddress_0090(nullptr, packet, 1); });
expectFailure([&] { sceAgcGetDataPacketPayloadAddress_0090(&payload, nullptr, 1); });
auto* misaligned = reinterpret_cast<std::uint32_t*>(reinterpret_cast<unsigned char*>(packet) + 1);
expectFailure([&] { sceAgcGetDataPacketPayloadAddressUnk(&payload, misaligned, 0); });
expectFailure([&] { sceAgcGetDataPacketPayloadAddress_0090(&payload, misaligned, 0); });
check(payload == packet + 2, "invalid packet changed output address");
}
@ -367,10 +373,10 @@ void testMemory() {
void testDefaults() {
std::uint32_t state = 0x12345678;
check(sceAgcUnknownInitState(&state, 8) == 0 && state == 0x12345678, "AGC initialization failed or modified caller state");
check(sceAgcUnknownInitState(&state, 13) == 0 && state == 0x12345678, "AGC version 13 initialization changed caller state");
expectFailure([] { sceAgcUnknownInitState(nullptr, 8); });
expectFailure([&] { sceAgcUnknownInitState(&state, 14); });
check(sceAgcInit_0090(&state, 8) == 0 && state == 0x12345678, "AGC initialization failed or modified caller state");
check(sceAgcInit_0090(&state, 13) == 0 && state == 0x12345678, "AGC version 13 initialization changed caller state");
expectFailure([] { sceAgcInit_0090(nullptr, 8); });
expectFailure([&] { sceAgcInit_0090(&state, 14); });
check(sceAgcInit(8) == 0, "AGC version initialization failed");
expectFailure([] { sceAgcInit(14); });
expectFailure([] { sceAgcInit(0xffffffffu); });

View File

@ -8,7 +8,7 @@
#include <stdexcept>
extern "C" std::uint64_t APS5_VABI sceAgcDcbContextStateOpGetSize(std::uint32_t operation);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbContextStateAnotherOp(CommandBuffer* buf, std::uint32_t operation);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbContextStateOp_0100(CommandBuffer* buf, std::uint32_t operation);
namespace {
@ -34,7 +34,7 @@ void testSizes() {
std::array<std::uint32_t, 64> words{};
CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(),
nullptr, nullptr, 0};
check(sceAgcDcbContextStateAnotherOp(&buffer, operation) == words.data(), "context state op did not start at the cursor");
check(sceAgcDcbContextStateOp_0100(&buffer, operation) == words.data(), "context state op did not start at the cursor");
const auto written = static_cast<std::uint64_t>(buffer.cursor_up - words.data()) * sizeof(std::uint32_t);
check(written == sizes[operation], "context state size differs from the written packets");
}

View File

@ -4,10 +4,12 @@
#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp"
#include <array>
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <stdexcept>
#include <span>
extern "C" int APS5_VABI sceAgcCreateInterpolantMapping(ShaderRegister* regs, const Shader* gs, const Shader* ps);
extern "C" int APS5_VABI sceAgcUnknownCreateInterpolantMapping(ShaderRegister* regs, const Shader* gs, const Shader* ps);
@ -15,6 +17,10 @@ extern "C" int APS5_VABI sceAgcUnknownCreateInterpolantMapping(ShaderRegister* r
namespace {
using Registers = std::array<ShaderRegister, 32>;
const Shader* mappedPixel = nullptr;
Registers preparedMapping{};
unsigned mappings = 0;
unsigned links = 0;
void check(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
@ -82,6 +88,7 @@ void testMapping() {
auto split = filled();
check(sceAgcUnknownCreateInterpolantMapping(split.data(), &gs, &ps) == 0, "split f16 mapping failed");
check(std::memcmp(split.data(), preparedMapping.data(), sizeof(split)) == 0, "prepared split mapping differs from published registers");
for (std::uint32_t i = 0; i < inputs.size(); ++i) {
check(split[i].offset == ShaderRegs::SPI_PS_INPUT_CNTL_0 + i, "interpolant register offset changed");
}
@ -94,6 +101,7 @@ void testMapping() {
auto regular = filled();
check(sceAgcCreateInterpolantMapping(regular.data(), &gs, &ps) == 0, "mapping failed");
check(std::memcmp(regular.data(), preparedMapping.data(), sizeof(regular)) == 0, "prepared mapping differs from published registers");
check(regular[3].value == split[3].value && regular[4].value == split[4].value, "mappings differ outside the high f16 half mode");
checkIdentity(regular, static_cast<std::uint32_t>(inputs.size()), "unused interpolants are not the identity");
}
@ -117,11 +125,24 @@ void testRejections() {
}
extern "C" void AgcDriverResolveShaderAbi_nid_postfix(const Shader* shader, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive) {
check(shader != nullptr && context.size() == preparedMapping.size() && primitive.empty(), "invalid mapping preparation call");
mappedPixel = shader;
std::copy(context.begin(), context.end(), preparedMapping.begin());
++mappings;
}
extern "C" void AgcDriverResolveGraphicsAbi_nid_postfix(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType) {
check(vertex != nullptr && pixel == mappedPixel && primitiveType == 0u, "invalid mapping link call");
++links;
}
int main() {
try {
testIdentity();
testMapping();
testRejections();
check(mappings == 3u && links == 2u, "mapping preparation call count changed");
LibcRunShutdown_nid_postfix();
std::puts("AGC interpolant mapping tests passed");
return 0;

View File

@ -9,7 +9,7 @@
#include <tuple>
extern "C" int APS5_VABI sceAgcGetDataPacketPayloadRange(SceAgcMemoryRange* range, std::uint32_t* cmd, int type);
extern "C" int APS5_VABI sceAgcGetDataPacketPayloadAddressUnk(std::uint32_t** addr, std::uint32_t* cmd, int type);
extern "C" int APS5_VABI sceAgcGetDataPacketPayloadAddress_0090(std::uint32_t** addr, std::uint32_t* cmd, int type);
namespace {
@ -42,7 +42,7 @@ void testRanges() {
check(range.size == size, "payload range size mismatch");
check(words[0] == header && words[1] == 0x12345678u, "payload range query modified the packet");
std::uint32_t* address = nullptr;
check(sceAgcGetDataPacketPayloadAddressUnk(&address, words.data(), type) == 0 && address == range.base,
check(sceAgcGetDataPacketPayloadAddress_0090(&address, words.data(), type) == 0 && address == range.base,
"payload range base differs from the payload address");
}
}

View File

@ -1,4 +1,5 @@
#include "prx/libSceAgc/Misc/include/ShaderFusion.hpp"
#include "prx/libSceAgcDriver/tests/execution/VulkanTestDevice.hpp"
#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp"
#include "prx/libc/include/Shutdown.hpp"
@ -15,6 +16,7 @@ namespace {
constexpr std::uint32_t SCodeEnd = 0xbf9f0000u;
constexpr std::uint32_t SNop = 0xbf800000u;
constexpr std::uint32_t SSetpcS6 = 0xbe802006u;
constexpr std::uint32_t SSwappcNullS6 = 0xbefd2106u;
constexpr std::uint32_t SMovS0 = 0xbe800380u;
constexpr std::uint32_t VMovV0 = 0x7e000280u;
constexpr std::uint32_t SEndpgm = 0xbf810000u;
@ -67,8 +69,8 @@ struct Halves {
Shader front{};
Shader back{};
Halves() {
frontCode = {SMovS0, VMovV0, SSetpcS6, SCodeEnd, SCodeEnd};
explicit Halves(std::uint32_t terminator = SSetpcS6) {
frontCode = {SMovS0, VMovV0, terminator, SCodeEnd, SCodeEnd};
const auto codeBytes = static_cast<std::uint32_t>(frontCode.size() * 4);
std::array<std::uint32_t, 2> magic{};
std::memcpy(magic.data(), "barefoot", 8);
@ -123,16 +125,16 @@ std::uint8_t* aligned(std::vector<std::uint8_t>& storage, std::size_t alignment)
return storage.data() + ((alignment - address % alignment) % alignment);
}
void testFusion(std::size_t misalignment) {
Halves halves;
void testFusion(std::size_t misalignment, std::uint32_t terminator = SSetpcS6) {
Halves halves(terminator);
SizeAlign size{};
check(sceAgcUnknownGetFusedShaderSize(&size, &halves.front, &halves.back) == 0, "fused size query failed");
check(sceAgcGetFusedShaderSize_0080(&size, &halves.front, &halves.back) == 0, "fused size query failed");
check(size.m_align == 8, "fused shaders are not 256-byte aligned");
check(size.m_size > 256 + 255 + halves.back.shader_size + sizeof(Shader), "fused size does not cover the alignment padding, code and header");
std::vector<std::uint8_t> storage(size.m_size + 512, 0xcd);
auto* scratch = aligned(storage, 256) + misalignment;
Shader fused{};
check(sceAgcUnknownFuseShaderHalves(&fused, &halves.front, &halves.back, scratch) == 0, "fusion failed");
check(sceAgcFuseShaderHalves_0200(&fused, &halves.front, &halves.back, scratch) == 0, "fusion failed");
for (std::size_t i = 0; i < 16; ++i) check(scratch[size.m_size + i] == 0xcd, "fusion wrote past the reported size");
auto* memory = scratch + (256 - misalignment) % 256;
const auto base = reinterpret_cast<std::uintptr_t>(memory);
@ -173,38 +175,38 @@ void testRejections() {
Halves halves;
SizeAlign size{};
Shader fused{};
expectFailure([&] { sceAgcUnknownGetFusedShaderSize(nullptr, &halves.front, &halves.back); });
expectFailure([&] { sceAgcUnknownFuseShaderHalves(&fused, &halves.front, &halves.back, nullptr); });
expectFailure([&] { sceAgcGetFusedShaderSize_0080(nullptr, &halves.front, &halves.back); });
expectFailure([&] { sceAgcFuseShaderHalves_0200(&fused, &halves.front, &halves.back, nullptr); });
{
Halves swapped;
check(sceAgcUnknownGetFusedShaderSize(&size, &swapped.back, &swapped.front) == InvalidShaderHalves, "swapped halves were accepted by the size query");
check(sceAgcUnknownFuseShaderHalves(&fused, &swapped.back, &swapped.front, nullptr) == InvalidShaderHalves, "swapped halves were fused");
check(sceAgcGetFusedShaderSize_0080(&size, &swapped.back, &swapped.front) == InvalidShaderHalves, "swapped halves were accepted by the size query");
check(sceAgcFuseShaderHalves_0200(&fused, &swapped.back, &swapped.front, nullptr) == InvalidShaderHalves, "swapped halves were fused");
}
{
Halves stages;
stages.backSpecials.vgt_shader_stages_en.value = 0;
std::vector<std::uint8_t> storage(4096);
check(sceAgcUnknownFuseShaderHalves(&fused, &stages.front, &stages.back, storage.data()) == InvalidShaderHalves, "halves with different GS stages were fused");
check(sceAgcFuseShaderHalves_0200(&fused, &stages.front, &stages.back, storage.data()) == InvalidShaderHalves, "halves with different GS stages were fused");
}
{
Halves noJump;
noJump.frontCode[2] = SNop;
expectFailure([&] { sceAgcUnknownGetFusedShaderSize(&size, &noJump.front, &noJump.back); });
expectFailure([&] { sceAgcGetFusedShaderSize_0080(&size, &noJump.front, &noJump.back); });
}
{
Halves noTrailer;
noTrailer.frontCode[5] = 0;
expectFailure([&] { sceAgcUnknownGetFusedShaderSize(&size, &noTrailer.front, &noTrailer.back); });
expectFailure([&] { sceAgcGetFusedShaderSize_0080(&size, &noTrailer.front, &noTrailer.back); });
}
{
Halves hardware;
hardware.frontSh[2].value |= 1u << 16u;
expectFailure([&] { sceAgcUnknownGetFusedShaderSize(&size, &hardware.front, &hardware.back); });
expectFailure([&] { sceAgcGetFusedShaderSize_0080(&size, &hardware.front, &hardware.back); });
}
{
Halves constants;
constants.back.embedded_constant_buffer_size_dqw = 1;
expectFailure([&] { sceAgcUnknownGetFusedShaderSize(&size, &constants.front, &constants.back); });
expectFailure([&] { sceAgcGetFusedShaderSize_0080(&size, &constants.front, &constants.back); });
}
}
@ -228,10 +230,10 @@ void testHullHalves() {
back.specials = &specials;
back.type = static_cast<std::uint8_t>(ShaderRegs::ShaderBinaryType::HsBack);
SizeAlign size{};
check(sceAgcUnknownGetFusedShaderSize(&size, &front, &back) == 0 && size.m_size == backSh.size() * sizeof(ShaderRegister) && size.m_align == 4, "hull size query changed");
check(sceAgcGetFusedShaderSize_0080(&size, &front, &back) == 0 && size.m_size == backSh.size() * sizeof(ShaderRegister) && size.m_align == 4, "hull size query changed");
std::vector<ShaderRegister> scratch(backSh.size());
Shader fused{};
check(sceAgcUnknownFuseShaderHalves(&fused, &front, &back, scratch.data()) == 0, "hull fusion failed");
check(sceAgcFuseShaderHalves_0200(&fused, &front, &back, scratch.data()) == 0, "hull fusion failed");
check(fused.type == static_cast<std::uint8_t>(ShaderRegs::ShaderBinaryType::Hs) && fused.code == back.code && fused.sh_registers == scratch.data() && fused.user_data == nullptr, "hull halves were not fused at the register level");
const auto address = reinterpret_cast<std::uint64_t>(front.code);
check(scratch[0].value == 0x11u && scratch[1].value == 0x22u, "hull checksums were not taken from the front half");
@ -244,9 +246,12 @@ void testHullHalves() {
int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
testFusion(0);
testFusion(0xa0);
testFusion(4);
testFusion(0, SSwappcNullS6);
testRejections();
testHullHalves();
LibcRunShutdown_nid_postfix();

View File

@ -0,0 +1,129 @@
#include "SceShaders.hpp"
#include "prx/libSceAgc/Shader/include/PrimState.hpp"
#include "prx/libSceAgc/Shader/include/InterpolantMapping.hpp"
#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include <algorithm>
#include <array>
#include <span>
#include <stdexcept>
#include <iostream>
extern "C" int APS5_VABI sceAgcLinkShaders(ShaderRegister*, ShaderRegister*, const void*, const Shader*, const Shader*, std::uint32_t);
extern "C" int APS5_VABI sceAgcCreatePrimState(ShaderRegister*, ShaderRegister*, const Shader*, const Shader*, std::uint32_t);
extern "C" int APS5_VABI sceAgcCreateInterpolantMapping(ShaderRegister*, const Shader*, const Shader*);
namespace {
std::array<ShaderRegister, 2> preparedContext;
std::array<ShaderRegister, 3> preparedPrimitive;
std::size_t stageCount = 0;
unsigned preparations = 0;
unsigned mappings = 0;
unsigned links = 0;
bool rejectLink = false;
const Shader* mappedPixel = nullptr;
void Require(bool condition) {
if (!condition) throw std::runtime_error("shader helper preparation mismatch");
}
}
extern "C" void AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span<const Shader* const> stages, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive) {
Require(context.size() == 2 && primitive.size() == 3);
std::copy(context.begin(), context.end(), preparedContext.begin());
std::copy(primitive.begin(), primitive.end(), preparedPrimitive.begin());
stageCount = stages.size();
++preparations;
}
extern "C" void AgcDriverResolveShaderAbi_nid_postfix(const Shader* shader, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive) {
Require(context.size() == 32 && primitive.empty());
mappedPixel = shader;
++mappings;
}
extern "C" void AgcDriverResolveGraphicsAbi_nid_postfix(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType) {
if (rejectLink) throw std::runtime_error("injected link failure");
Require(vertex != nullptr && pixel == mappedPixel && (primitiveType == 0 || primitiveType == 7));
++links;
}
int main() {
try {
using namespace ShaderRegs;
ShaderSpecialRegs special{};
special.vgt_shader_stages_en = {VGT_SHADER_STAGES_EN, 0};
special.vgt_gs_out_prim_type = {VGT_GS_OUT_PRIM_TYPE, 2};
special.ge_cntl = {GE_CNTL, 0x123};
special.ge_user_vgpr_en = {GE_USER_VGPR_EN, 7};
Shader vertex{};
vertex.type = static_cast<std::uint8_t>(ShaderBinaryType::Gs);
vertex.specials = &special;
Shader hull{};
hull.type = static_cast<std::uint8_t>(ShaderBinaryType::Hs);
hull.specials = &special;
Shader pixel{};
pixel.type = static_cast<std::uint8_t>(ShaderBinaryType::Ps);
ShaderSemantic input{};
input.semantic = 9;
pixel.input_semantics = &input;
std::array<ShaderRegister, 32> interpolants{};
for (const auto count : {0u, 1u}) {
pixel.num_input_semantics = count;
for (const bool primitiveFirst : {false, true}) {
for (unsigned outputs = 1; outputs <= 3; ++outputs) {
for (const auto* hs : {static_cast<const Shader*>(nullptr), static_cast<const Shader*>(&hull)}) {
preparations = mappings = links = 0;
std::array<ShaderRegister, 3> context{};
std::array<ShaderRegister, 4> primitive{};
context.back().value = primitive.back().value = 0xdeadbeef;
const auto prepare = [&] { Require(sceAgcCreatePrimState(outputs & 1 ? context.data() : nullptr, outputs & 2 ? primitive.data() : nullptr, hs, &vertex, 7) == 0); };
if (primitiveFirst) prepare();
Require(sceAgcCreateInterpolantMapping(interpolants.data(), &vertex, &pixel) == 0);
if (!primitiveFirst) prepare();
Require(preparations == 1 && mappings == 1 && links == 1 && stageCount == (hs ? 2u : 1u));
Require(preparedContext[0].offset == VGT_SHADER_STAGES_EN && preparedContext[1].offset == VGT_GS_OUT_PRIM_TYPE);
Require(preparedPrimitive[0].value == 0x123 && preparedPrimitive[1].value == 7 && preparedPrimitive[2].value == 7);
Require(context.back().value == 0xdeadbeef && primitive.back().value == 0xdeadbeef);
for (unsigned i = 0; i < 2; ++i) Require(context[i].value == (outputs & 1 ? preparedContext[i].value : 0));
for (unsigned i = 0; i < 3; ++i) Require(primitive[i].value == (outputs & 2 ? preparedPrimitive[i].value : 0));
}
}
}
}
std::array<ShaderRegister, 34> linkedContext{};
std::array<ShaderRegister, 3> linkedPrimitive{};
for (auto& value : linkedContext) value.value = 0xdeadbeef;
for (auto& value : linkedPrimitive) value.value = 0xdeadbeef;
for (auto& value : interpolants) value.value = 0xdeadbeef;
rejectLink = true;
bool rejected = false;
try { sceAgcLinkShaders(linkedContext.data(), linkedPrimitive.data(), nullptr, &vertex, &pixel, 7); }
catch (const std::runtime_error&) { rejected = true; }
Require(rejected);
for (const auto value : linkedContext) Require(value.value == 0xdeadbeef);
for (const auto value : linkedPrimitive) Require(value.value == 0xdeadbeef);
rejected = false;
try { sceAgcCreateInterpolantMapping(interpolants.data(), &vertex, &pixel); }
catch (const std::runtime_error&) { rejected = true; }
Require(rejected);
for (const auto value : interpolants) Require(value.value == 0xdeadbeef);
rejectLink = false;
pixel.num_input_semantics = 33;
rejected = false;
try { sceAgcCreateInterpolantMapping(interpolants.data(), &vertex, &pixel); }
catch (const std::runtime_error&) { rejected = true; }
Require(rejected);
for (const auto value : interpolants) Require(value.value == 0xdeadbeef);
pixel.num_input_semantics = 1;
Require(sceAgcLinkShaders(linkedContext.data(), linkedPrimitive.data(), nullptr, &vertex, &pixel, 7) == 0);
preparations = mappings = links = 0;
Require(sceAgcCreatePrimState(nullptr, nullptr, nullptr, nullptr, 7) == 0);
Require(sceAgcCreateInterpolantMapping(interpolants.data(), &vertex, nullptr) == 0);
Require(preparations == 0 && mappings == 0 && links == 0);
std::cout << "shader helper preparation tests passed\n";
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}

View File

@ -126,6 +126,8 @@ set(agcDriverSources
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/GuestMemory.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/ShaderMemory.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/BdaFeatures.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/SubgroupClock.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/ShaderDeviceProfile.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Pm4.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/VulkanDevice.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/AspectFit.cpp
@ -180,12 +182,12 @@ target_include_directories(libSceAgcDriver PRIVATE
${CMAKE_SOURCE_DIR}/3rdparty/SPIRV-Headers/include
)
file(GLOB_RECURSE agcShaderCacheGuardSources CONFIGURE_DEPENDS
file(GLOB_RECURSE agcShaderCacheGuardSources CONFIGURE_DEPENDS RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}"
"${CMAKE_CURRENT_SOURCE_DIR}/*.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/*.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/*.h"
)
list(FILTER agcShaderCacheGuardSources EXCLUDE REGEX "^${CMAKE_CURRENT_SOURCE_DIR}/(tests|tools)/")
list(FILTER agcShaderCacheGuardSources EXCLUDE REGEX "^(tests|tools)/")
add_shader_recompiler(shader_recompiler_agc_driver libc
GUARD_SOURCES
${agcShaderCacheGuardSources}
@ -202,6 +204,16 @@ set_target_properties(libSceAgcDriver PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(libSceAgcDriver)
if(BUILD_TESTING)
add_test_executable(agc_write_watch_coverage_tests tests/WriteWatchCoverage.cpp)
target_include_directories(agc_write_watch_coverage_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_write_watch_coverage_tests PRIVATE libc)
configure_windows_unwind(agc_write_watch_coverage_tests)
add_test(NAME agc_write_watch_coverage COMMAND agc_write_watch_coverage_tests)
if(WIN32)
set_tests_properties(agc_write_watch_coverage PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test_executable(agc_profile_output_tests tests/ProfileOutput.cpp)
target_include_directories(agc_profile_output_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_profile_output_tests PRIVATE libc)
@ -229,6 +241,7 @@ add_test_executable(agc_driver_tests EXCLUDE_FROM_ALL
target_include_directories(agc_driver_tests PRIVATE
${LIBS_INCLUDE_DIR}
${CMAKE_SOURCE_DIR}/core/shader/recompiler
${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include
)
target_link_libraries(agc_driver_tests PRIVATE
@ -273,11 +286,35 @@ target_link_libraries(agc_structurizer_tests PRIVATE shader_recompiler_agc_drive
set_target_properties(agc_structurizer_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_structurizer_tests)
add_test_executable(agc_subgroup_clock_tests EXCLUDE_FROM_ALL
tests/SubgroupClock.cpp
Execution/src/SubgroupClock.cpp
)
target_include_directories(agc_subgroup_clock_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include)
set_target_properties(agc_subgroup_clock_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_subgroup_clock_tests)
if(BUILD_TESTING)
set_target_properties(agc_subgroup_clock_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_subgroup_clock COMMAND agc_subgroup_clock_tests)
endif()
add_test_executable(agc_spirv_module_tests EXCLUDE_FROM_ALL tests/SpirvModule.cpp)
target_link_libraries(agc_spirv_module_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_spirv_module_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_spirv_module_tests)
add_test_executable(agc_spirv_narrow_constant_store_tests EXCLUDE_FROM_ALL tests/SpirvNarrowConstantStore.cpp)
target_link_libraries(agc_spirv_narrow_constant_store_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_spirv_narrow_constant_store_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_spirv_narrow_constant_store_tests)
if(ANYPS5_ENABLE_SPIRV_TOOLS)
target_link_libraries(agc_spirv_narrow_constant_store_tests PRIVATE SPIRV-Tools-static)
add_test_executable(agc_spirv_optimizer_tests EXCLUDE_FROM_ALL tests/SpirvOptimizer.cpp)
target_link_libraries(agc_spirv_optimizer_tests PRIVATE shader_recompiler_agc_driver libc SPIRV-Tools-static)
set_target_properties(agc_spirv_optimizer_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_spirv_optimizer_tests)
endif()
add_test_executable(agc_dpp_write_tests EXCLUDE_FROM_ALL tests/DppWrite.cpp)
target_link_libraries(agc_dpp_write_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_dpp_write_tests PROPERTIES CXX_EXTENSIONS OFF)
@ -288,6 +325,20 @@ 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_wave_control_decode_tests EXCLUDE_FROM_ALL tests/WaveControlDecode.cpp)
target_include_directories(agc_wave_control_decode_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_wave_control_decode_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_wave_control_decode_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_wave_control_decode_tests)
if(BUILD_TESTING)
set_target_properties(agc_wave_control_decode_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_wave_control_decode COMMAND agc_wave_control_decode_tests)
if(WIN32)
set_tests_properties(agc_wave_control_decode PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_test_executable(agc_vop1_control_decode_tests EXCLUDE_FROM_ALL tests/Vop1ControlDecode.cpp)
target_link_libraries(agc_vop1_control_decode_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_vop1_control_decode_tests PROPERTIES CXX_EXTENSIONS OFF)
@ -332,6 +383,12 @@ target_link_libraries(agc_shader_disk_cache_tests PRIVATE shader_recompiler_agc_
set_target_properties(agc_shader_disk_cache_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_shader_disk_cache_tests)
add_test_executable(agc_shader_device_profile_tests EXCLUDE_FROM_ALL tests/ShaderDeviceProfile.cpp Execution/src/ShaderDeviceProfile.cpp)
target_include_directories(agc_shader_device_profile_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include)
target_link_libraries(agc_shader_device_profile_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_shader_device_profile_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_shader_device_profile_tests)
add_test_executable(agc_shader_control_flow_tests EXCLUDE_FROM_ALL
tests/ControlFlow.cpp
)
@ -370,10 +427,24 @@ add_custom_command(
VERBATIM
)
set(agcSampleArrayShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/SampleArray.comp)
set(agcSampleArrayShaderSpv ${agcTestShaderGeneratedRoot}/SampleArray.spv)
set(agcSampleArrayShaderHeader ${agcTestShaderGeneratedRoot}/SampleArray_spv.h)
add_custom_command(
OUTPUT "${agcSampleArrayShaderHeader}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTestShaderGeneratedRoot}"
COMMAND "$<TARGET_FILE:glslang-standalone>" -V --target-env vulkan1.1 -o "${agcSampleArrayShaderSpv}" "${agcSampleArrayShaderSource}"
COMMAND ${CMAKE_COMMAND} -DINPUT=${agcSampleArrayShaderSpv} -DOUTPUT=${agcSampleArrayShaderHeader}
-DSYMBOL=SAMPLE_ARRAY_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake"
DEPENDS "${agcSampleArrayShaderSource}" glslang-standalone
VERBATIM
)
add_test_executable(agc_driver_recorder_tests EXCLUDE_FROM_ALL
tests/Recorder.cpp
tests/ResidentPresent.cpp
${agcSampleLodShaderHeader}
${agcSampleArrayShaderHeader}
)
target_include_directories(agc_driver_recorder_tests PRIVATE
@ -392,6 +463,20 @@ target_link_libraries(agc_driver_recorder_tests PRIVATE
set_target_properties(agc_driver_recorder_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_driver_recorder_tests)
add_test_executable(agc_recorder_kept_bytes_tests EXCLUDE_FROM_ALL tests/RecorderKeptBytes.cpp)
target_include_directories(agc_recorder_kept_bytes_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include)
target_link_libraries(agc_recorder_kept_bytes_tests PRIVATE libSceAgcDriver libc)
set_target_properties(agc_recorder_kept_bytes_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_recorder_kept_bytes_tests)
if(BUILD_TESTING)
set_target_properties(agc_recorder_kept_bytes_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_recorder_kept_bytes COMMAND agc_recorder_kept_bytes_tests)
if(WIN32)
set_tests_properties(agc_recorder_kept_bytes PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_test_executable(agc_driver_pm4_tests EXCLUDE_FROM_ALL
tests/Pm4.cpp
)
@ -403,6 +488,7 @@ target_include_directories(agc_driver_pm4_tests PRIVATE
target_link_libraries(agc_driver_pm4_tests PRIVATE
libSceAgcDriver
libc
libkernel
)
set_target_properties(agc_driver_pm4_tests PROPERTIES CXX_EXTENSIONS OFF)
@ -464,6 +550,7 @@ target_include_directories(agc_driver_graphics_tests PRIVATE
target_link_libraries(agc_driver_graphics_tests PRIVATE
shader_recompiler_agc_driver
libc
libkernel
)
set_target_properties(agc_driver_graphics_tests PROPERTIES CXX_EXTENSIONS OFF)
@ -520,17 +607,90 @@ foreach(agcExecutionTestSource IN LISTS agcExecutionTestSources)
list(APPEND agcExecutionTests ${agcExecutionTest})
endforeach()
target_include_directories(agc_driver_prepared_exports_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
target_include_directories(agc_driver_prepared_publication_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
target_include_directories(agc_driver_prepared_graphics_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
target_include_directories(agc_driver_prepared_shaders_tests PRIVATE
$<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>
)
if(BUILD_TESTING)
foreach(agcTest IN ITEMS agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_shader_disk_cache_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests ${agcExecutionTests})
add_test(NAME agc_shader_bindless COMMAND agc_shader_memory_tests --bindless)
set_tests_properties(agc_shader_bindless PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(agc_shader_bindless PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
foreach(agcTest IN ITEMS agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_shader_disk_cache_tests agc_shader_device_profile_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests ${agcExecutionTests})
if(agcTest STREQUAL "agc_driver_constant_store_tests")
continue()
endif()
string(REGEX REPLACE "_tests$" "" agcTestName "${agcTest}")
set_target_properties(${agcTest} PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME ${agcTestName} COMMAND ${agcTest})
set_tests_properties(${agcTestName} PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77)
set_tests_properties(${agcTestName} PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(${agcTestName} PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endforeach()
set_tests_properties(agc_driver_flat_store PROPERTIES RUN_SERIAL TRUE)
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)
if(WIN32)
set_tests_properties(agc_driver_pm4_failure PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
foreach(agcTest IN ITEMS agc_spirv_narrow_constant_store_tests agc_spirv_optimizer_tests agc_driver_constant_store_tests)
if(NOT TARGET ${agcTest})
continue()
endif()
set_target_properties(${agcTest} PROPERTIES EXCLUDE_FROM_ALL OFF)
string(REGEX REPLACE "_tests$" "" agcTestBaseName "${agcTest}")
foreach(agcSpirvMode IN ITEMS default full none)
if(agcSpirvMode STREQUAL "default")
set(agcTestName "${agcTestBaseName}")
set(agcSpirvEnvironment "--unset=APS5_SPIRV_OPT")
else()
set(agcTestName "${agcTestBaseName}_${agcSpirvMode}")
set(agcSpirvEnvironment "APS5_SPIRV_OPT=${agcSpirvMode}")
endif()
add_test(NAME ${agcTestName} COMMAND ${CMAKE_COMMAND} -E env
${agcSpirvEnvironment} "$<TARGET_FILE:${agcTest}>")
set_tests_properties(${agcTestName} PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 60 SKIP_RETURN_CODE 77)
if(agcTest STREQUAL "agc_driver_constant_store_tests")
set_tests_properties(${agcTestName} PROPERTIES RUN_SERIAL TRUE)
endif()
if(WIN32)
set_tests_properties(${agcTestName} PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endforeach()
endforeach()
foreach(preparedMode IN ITEMS indirect uncached debug debug_failure)
set(preparedTest "agc_driver_prepared_shaders_${preparedMode}")
if(preparedMode STREQUAL "indirect")
add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --indirect)
elseif(preparedMode STREQUAL "debug_failure")
add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --fail-before-registration)
set_tests_properties(${preparedTest} PROPERTIES WILL_FAIL TRUE TIMEOUT 30)
else()
add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests)
endif()
set_tests_properties(${preparedTest} PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(preparedMode STREQUAL "uncached")
set_property(TEST ${preparedTest} APPEND PROPERTY ENVIRONMENT "APS5_NO_DISPATCH_CACHE=1")
elseif(preparedMode STREQUAL "debug" OR preparedMode STREQUAL "debug_failure")
set_property(TEST ${preparedTest} APPEND PROPERTY ENVIRONMENT "APS5_PROFILE_DRAW=1" "APS5_NO_UNLOCKED_DEVICE=1")
endif()
if(WIN32)
set_tests_properties(${preparedTest} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endforeach()
endif()
option(AGC_BUILD_VISUAL_TEST "Enable the standalone AGC SPIR-V visual test" OFF)
@ -568,6 +728,8 @@ if(AGC_BUILD_VISUAL_TEST)
Execution/src/PresentationScaler.cpp
Execution/src/GuestMemory.cpp
Execution/src/BdaFeatures.cpp
Execution/src/SubgroupClock.cpp
Execution/src/ShaderDeviceProfile.cpp
Graphics/src/Draw.cpp
Graphics/src/DepthSurface.cpp
Graphics/src/ColorTargetLayout.cpp
@ -769,6 +931,14 @@ if(BUILD_TESTING)
set_tests_properties(scalar_saveexec PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test_executable(scalar_swappc_tests ${LIBS_INCLUDE_DIR}/tests/ScalarSwappc.cpp)
target_link_libraries(scalar_swappc_tests PRIVATE shader_recompiler_agc_driver libc)
configure_windows_unwind(scalar_swappc_tests)
add_test(NAME scalar_swappc COMMAND scalar_swappc_tests)
if(WIN32)
set_tests_properties(scalar_swappc PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test_executable(scalar_bitcount64_tests ${LIBS_INCLUDE_DIR}/tests/ScalarBitCount64.cpp)
target_link_libraries(scalar_bitcount64_tests PRIVATE shader_recompiler_agc_driver libc)
configure_windows_unwind(scalar_bitcount64_tests)
@ -785,6 +955,14 @@ if(BUILD_TESTING)
set_tests_properties(scalar_scratch_load PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test_executable(scalar_call_tests ${LIBS_INCLUDE_DIR}/tests/ScalarCall.cpp)
target_link_libraries(scalar_call_tests PRIVATE shader_recompiler_agc_driver libc)
configure_windows_unwind(scalar_call_tests)
add_test(NAME scalar_call COMMAND scalar_call_tests)
if(WIN32)
set_tests_properties(scalar_call PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test_executable(image_sample_gather_tests ${LIBS_INCLUDE_DIR}/tests/ImageSampleGather.cpp)
target_link_libraries(image_sample_gather_tests PRIVATE shader_recompiler_agc_driver libc)
configure_windows_unwind(image_sample_gather_tests)

View File

@ -28,6 +28,8 @@ std::size_t DisplayBufferSize(const DisplayBuffer& buffer);
std::vector<std::byte> DecodeDisplayBuffer(const DisplayBuffer& buffer, std::span<const std::byte> source);
std::vector<std::byte> ReadDisplayBuffer(const DisplayBuffer& buffer);
std::array<std::byte, 4> DisplayBufferClearPixel(const DisplayBuffer& buffer, Graphics::DccKeys keys);
std::size_t DisplayBufferKeyBytes(const DisplayBuffer& buffer);
Graphics::DccKeys DisplayBufferKeys(const DisplayBuffer& buffer);
}

View File

@ -1,6 +1,7 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_HPP
#include <span>
#include "SceTypes.hpp"
#include "SceShaders.hpp"
@ -16,5 +17,8 @@ extern "C" void AgcDriverUnlockVulkanLoader_nid_postfix();
extern "C" void AgcDriverShutdown_nid_postfix();
extern "C" void AgcDriverSuspendPoint_nid_postfix();
extern "C" void AgcDriverRegisterShader_nid_postfix(const Shader* shader);
extern "C" void AgcDriverResolveShaderAbi_nid_postfix(const Shader* shader, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive);
extern "C" void AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span<const Shader* const> stages, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive);
extern "C" void AgcDriverResolveGraphicsAbi_nid_postfix(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType);
#endif

View File

@ -32,6 +32,15 @@ struct DrawDecode {
std::vector<ShaderRecompiler::ProgramRole> roles;
};
void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const ShaderRegistry& registry, bool staticAbi, bool includeFragment);
struct PreparedGraphicsStage {
std::shared_ptr<const ShaderSnapshot> snapshot;
PreparedShaders::Entry entry;
};
std::vector<PreparedGraphicsStage> PrepareGraphicsStages(const DrawDecode& decoded, const ShaderRecompiler::SpirvTarget& target);
struct DrawRecipeRecord {
std::vector<std::weak_ptr<const DispatchVariant>> stages;
std::shared_ptr<const DrawRecipe> recipe;

View File

@ -56,6 +56,9 @@ public:
void Present(const PresentationWindow& window, const DisplayBuffer* buffer, bool opaque, void (*gpuReady)(void*), void* context);
void ReleaseWindow(void* window);
void RegisterShader(const Shader* shader);
void ResolveShaderAbi(const Shader* shader, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive);
void ResolveGraphicsStagesAbi(std::span<const Shader* const> stages, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive);
void ResolveGraphicsAbi(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType);
private:
friend class SampledReadScope;
@ -68,6 +71,7 @@ private:
static bool& onWorkerThread();
static void copyCommands(Submission& submission, const std::uint32_t* guest, std::size_t words);
static bool copySegment(Submission& submission, const std::uint32_t* guest, std::size_t words, std::size_t& budget);
static void readRegisterLists(Submission& submission);
void waitForFlipRoom(const Submission& submission);
void reserveOutputs(Submission& submission);
void executeRewindTail(const Submission& stalled);
@ -93,7 +97,7 @@ private:
void lookupDispatch(std::uint64_t address, const Submission& submission, std::uint64_t key, bool noDispatchCache, bool traceCache, bool profile, std::span<const ShaderRecompiler::MemoryRegion> memory, DispatchPhaseTiming& phaseTiming, std::array<double, DriverPhaseCount>& phaseMs, std::shared_ptr<const ShaderRecompiler::RecompileResult>& compiledResult, std::shared_ptr<DispatchVariant>& keepVariant, std::vector<ShaderRecompiler::MemoryRegion>& captured, std::vector<std::uint32_t>& liveWords, bool& dataHit, bool& cached, bool& validated, std::shared_ptr<DispatchEntry>& missedEntry, bool& missedDiffering);
void insertDispatch(std::uint64_t address, std::uint64_t key, bool noDispatchCache, bool profile, const std::shared_ptr<const ShaderSnapshot>& registeredShader, std::uint64_t forgetAtCapture, std::span<const ShaderRecompiler::MemoryRegion> memory, const std::shared_ptr<ShaderMemory>& shaderMemory, const std::vector<ShaderRecompiler::MemoryRegion>& captured, const std::shared_ptr<const ShaderRecompiler::ResourceCapture>& capture, const std::shared_ptr<const ShaderRecompiler::RecompileResult>& compiledResult, const std::shared_ptr<DispatchEntry>& missedEntry, bool missedDiffering, std::shared_ptr<DispatchVariant>& attachVariant, DispatchPhaseTiming& phaseTiming);
void dispatch(QueueState& queue, std::span<const std::uint32_t> packet, const Submission& submission, std::uint64_t indirectArguments = 0);
void verifyDataHit(const ShaderSnapshot& snapshot, std::size_t codeOffset, std::uint64_t deviceSerial, ShaderRecompiler::RecompileRequest request, std::span<const ShaderRecompiler::MemoryRegion> memory, std::uint64_t address, const DispatchVariant& variant, std::span<const std::uint32_t> liveWords, const ShaderRecompiler::RecompileResult& patched);
void verifyDataHit(const ShaderSnapshot& snapshot, std::size_t codeOffset, ShaderRecompiler::RecompileRequest request, std::span<const ShaderRecompiler::MemoryRegion> memory, std::uint64_t address, const DispatchVariant& variant, std::span<const std::uint32_t> liveWords, const ShaderRecompiler::RecompileResult& patched);
static bool drawEntries();
static bool verifyDrawEntries();
static bool registerKeyEnabled();
@ -110,7 +114,7 @@ private:
std::shared_ptr<DrawDecode> decodeDraw(const QueueState& queue, const Submission& submission);
void resolveDrawDecode(const QueueState& queue, const Submission& submission, std::shared_ptr<const DrawDecode>& decode, bool registerKey, std::uint64_t drawKey, bool profile);
void lookupDraw(const Submission& submission, const std::shared_ptr<VulkanDevice>& localDevice, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector<DrawProgram>& programs, const std::vector<ShaderRecompiler::ProgramRole>& roles, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, bool useDrawEntries, bool registerKey, bool profile, std::uint64_t& drawKey, std::shared_ptr<DrawEntry>& entry, std::vector<std::shared_ptr<DispatchVariant>>& matched, std::vector<std::vector<ShaderRecompiler::MemoryRegion>>& matchedRegions, bool& drawHit, bool& verifyHit, DrawPhaseTiming& phaseTiming, std::array<double, DrawDriverPhaseCount>& phaseMs);
ShaderRecompiler::RecompileResult compileDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector<DrawProgram>& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, std::vector<ShaderRecompiler::MemoryRegion>& memory, const std::vector<ShaderRecompiler::LinkedProgram>& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr<VulkanDevice>& localDevice, ShaderMemory& shaderMemory, std::vector<StageCapture>& stageCaptures, std::vector<bool>& recompiled, bool drawHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, const std::vector<std::vector<ShaderRecompiler::MemoryRegion>>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array<double, DrawDriverPhaseCount>& phaseMs, std::string& rejected);
ShaderRecompiler::RecompileResult materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector<DrawProgram>& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, std::vector<ShaderRecompiler::MemoryRegion>& memory, const std::vector<ShaderRecompiler::LinkedProgram>& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr<VulkanDevice>& localDevice, ShaderMemory& shaderMemory, std::vector<StageCapture>& stageCaptures, std::vector<bool>& recompiled, bool drawHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, const std::vector<std::vector<ShaderRecompiler::MemoryRegion>>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array<double, DrawDriverPhaseCount>& phaseMs, std::string& rejected);
void 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);
static bool drawPrecheck();
std::optional<DrawVerdict> precheckDraw(const QueueState& queue, const Submission& submission, std::span<const std::uint32_t> packet, const Pm4::DrawParameters& drawParameters, std::string& rejected, bool& traceIndirect);
@ -215,8 +219,8 @@ private:
void dumpSampleCounters(std::uint64_t address);
template <typename TWork>
static void timed(double WorkerProfile::*bucket, TWork&& work);
template <typename TWork>
void tolerate(const char* kind, TWork&& work);
FrameTiming* frameTiming();
FrameTiming* includeTimingSubmission(const Submission& submission, bool firstSegment);
void execute(const Submission& submission);
void markCompleted(std::uint64_t serial);
static const std::atomic<std::uint64_t>*& workerQueued();
@ -229,6 +233,7 @@ private:
std::map<std::uint32_t, QueueWorker> workers;
std::uint64_t frameSerial = 0;
std::shared_ptr<FrameTiming> pendingFrameTiming;
std::atomic<std::uint64_t> flipsCounted{0};
std::atomic<std::uint64_t> flipSerial{0};

View File

@ -26,13 +26,20 @@ struct Submission {
std::map<std::size_t, std::shared_ptr<IFlipRequest>> flips;
std::map<std::size_t, std::shared_ptr<IRenderingWait>> renderingWaits;
std::map<std::size_t, std::size_t> conditionalEnds;
std::map<std::size_t, std::vector<std::uint32_t>> registerLists;
bool suspend = false;
bool waitFree = false;
std::uint64_t received = 0;
std::vector<std::uint64_t> labelWrites;
std::set<std::size_t> heldAtSubmit;
std::chrono::steady_clock::time_point receivedAt{};
std::chrono::steady_clock::time_point copiedAt{};
std::chrono::steady_clock::time_point validatedAt{};
std::chrono::steady_clock::time_point roomReadyAt{};
std::chrono::steady_clock::time_point enqueuedAt{};
std::chrono::steady_clock::time_point dequeuedAt{};
std::chrono::steady_clock::time_point orderedAt{};
const std::uint32_t* rewindTail = nullptr;
std::size_t rewindWords = 0;
};

View File

@ -2,6 +2,7 @@
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_SHADERREGISTRY_HPP
#include "prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/QueueState.hpp"
#include <array>
#include <atomic>
#include <cstddef>
@ -14,16 +15,45 @@
namespace AgcDriver::DriverDetail {
struct HandleMemos {
struct ShaderSnapshot;
using ShaderRegistry = std::map<std::uint64_t, std::shared_ptr<const ShaderSnapshot>>;
struct PreparedShaderState {
struct Entry {
std::uint64_t key = 0;
std::size_t codeOffset;
std::shared_ptr<const ShaderRecompiler::SourceHandle> handle;
std::shared_ptr<const std::string> failure;
};
struct Rectangle {
std::uint64_t vertexId;
std::uint64_t fragmentId;
std::shared_ptr<const ShaderRecompiler::RectListShaders> shaders;
};
struct RectangleProgress {
std::weak_ptr<const ShaderSnapshot> fragment;
std::size_t vertexCount;
std::size_t fragmentCount;
};
struct GraphicsAbi {
std::vector<std::uint64_t> key;
std::weak_ptr<const ShaderRegistry> registry;
std::vector<std::weak_ptr<const ShaderSnapshot>> stages;
};
std::vector<Entry> entries;
std::vector<std::vector<std::uint64_t>> registeredAbis;
std::vector<GraphicsAbi> graphicsAbis;
std::vector<Rectangle> rectangles;
std::vector<RectangleProgress> rectangleProgress;
std::vector<std::weak_ptr<const ShaderSnapshot>> fragments;
bool rectangleRequested = false;
};
struct PreparedShaders : PreparedShaderState {
std::mutex mutex;
std::array<Entry, 8> entries;
std::size_t next = 0;
std::atomic<std::uint32_t> poisoned{0};
};
struct RegisteredShaderState {
Registers shader;
Registers context;
Registers userConfig;
};
struct ShaderSnapshot {
@ -32,17 +62,22 @@ struct ShaderSnapshot {
std::uint8_t type;
std::vector<std::uint32_t> code;
std::vector<std::byte> header;
std::unique_ptr<HandleMemos> handles = std::make_unique<HandleMemos>();
std::shared_ptr<PreparedShaders> prepared = std::make_shared<PreparedShaders>();
std::shared_ptr<const RegisteredShaderState> registeredState;
};
using ShaderRegistry = std::map<std::uint64_t, std::shared_ptr<const ShaderSnapshot>>;
bool FailureMemo();
std::shared_ptr<const ShaderSnapshot> ReadRawComputeShader(std::uint64_t address);
std::uint64_t NullPixelProgramAddress();
void PublishRegisteredShader(std::shared_ptr<ShaderRegistry>& registry, const std::shared_ptr<const ShaderSnapshot>& snapshot);
std::shared_ptr<const ShaderRecompiler::SourceHandle> SourceHandleFor(const ShaderSnapshot& snapshot, std::size_t codeOffset, std::uint64_t deviceSerial, const ShaderRecompiler::RecompileRequest& request, bool bypass, const std::string** poisoned = nullptr);
void ResolvePreparedGraphics(const ShaderSnapshot& front, const std::shared_ptr<const ShaderSnapshot>& fragment, std::uint32_t primitiveType, const ShaderRecompiler::SpirvTarget& target);
ShaderRecompiler::RectListShaders PreparedRectangle(const ShaderSnapshot& snapshot, std::uint64_t vertexId, std::uint64_t fragmentId);
std::shared_ptr<const ShaderRecompiler::SourceHandle> SourceHandleFor(const ShaderSnapshot& snapshot, std::size_t codeOffset, const ShaderRecompiler::RecompileRequest& request);
ShaderRecompiler::PreparedShaderInvocation InvocationFor(const ShaderSnapshot& snapshot, std::size_t codeOffset, const ShaderRecompiler::RecompileRequest& request);
}

View File

@ -5,17 +5,15 @@
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <functional>
#include <iomanip>
#include <locale>
#include <map>
#include <memory>
#include <mutex>
#include <sstream>
#include <ostream>
#include <stdexcept>
#include <string_view>
#include <utility>
#include "prx/libc/include/general/LogMacros.hpp"
namespace AgcDriver {
@ -30,23 +28,68 @@ public:
std::uint64_t bytes = 0;
};
explicit FrameTiming(std::uint64_t id) : id(id) {}
explicit FrameTiming(std::uint64_t id, bool localMetrics = false) : localMetrics(localMetrics), id(id), workerStart(Clock::now()) {}
static FrameTiming* Preparation() {
if (!APS5_ENABLE_TIMING_LOG) return nullptr;
static FrameTiming timing(0);
return &timing;
}
static FrameTiming* Async() {
if (!APS5_ENABLE_TIMING_LOG) return nullptr;
static FrameTiming timing(0);
return &timing;
}
void CollectBackground() {
const auto collect = [&](FrameTiming* source, auto& destination) {
if (source == nullptr) return;
std::scoped_lock lock(mutex, source->mutex);
for (auto& [key, metric] : source->metrics) {
if (metric.count == 0) continue;
auto& target = destination[key];
target.total += metric.total;
target.maximum = std::max(target.maximum, metric.maximum);
target.count += metric.count;
target.bytes += metric.bytes;
metric = {};
}
};
collect(Preparation(), metrics);
collect(Async(), asyncMetrics);
}
Metric* Get(const char* scope, const char* stage) {
std::lock_guard lock(mutex);
std::unique_lock lock(mutex, std::defer_lock);
if (!localMetrics) lock.lock();
return &metrics[{scope, stage}];
}
void Add(Metric* metric, Clock::duration elapsed, std::uint64_t bytes = 0) {
std::lock_guard lock(mutex);
std::unique_lock lock(mutex, std::defer_lock);
if (!localMetrics) lock.lock();
metric->total += elapsed;
metric->maximum = std::max(metric->maximum, elapsed);
++metric->count;
metric->bytes += bytes;
}
void NotePacket(std::uint64_t serial, std::size_t offset, std::uint32_t opcode, std::size_t words, Clock::duration elapsed) {
std::unique_lock lock(mutex, std::defer_lock);
if (!localMetrics) lock.lock();
++packetCount;
packetWords += words;
if (elapsed > slowPacketTime) {
slowPacketTime = elapsed;
slowPacketSerial = serial;
slowPacketOffset = offset;
slowPacketOpcode = opcode;
}
}
void IncludeSubmission(std::uint64_t serial, Clock::time_point received, Clock::time_point enqueued, Clock::time_point dequeued, bool firstSegment) {
if (serial == 0 || received > enqueued || enqueued > dequeued) throw std::runtime_error("Frame timing: invalid submission timestamps");
if (serial == 0 || received == Clock::time_point{} || received > enqueued || enqueued > dequeued) throw std::runtime_error("Frame timing: invalid submission timestamps");
if (firstSerial == 0) {
firstSerial = serial;
start = received;
@ -66,6 +109,7 @@ public:
flipOffset = offset;
flipReceived = received;
flipReached = reached;
localMetrics = false;
}
Clock::time_point FlipReached() {
@ -107,39 +151,52 @@ public:
gpuGapMs += gapMs;
}
void Print(std::uint32_t outputHandle, std::int32_t buffer, std::int64_t argument, Clock::time_point finished, Clock::duration interval) {
std::function<void(std::ostream&)> Capture(std::uint32_t outputHandle, std::int32_t buffer, std::int64_t argument, Clock::time_point finished, Clock::duration interval) {
std::lock_guard lock(mutex);
if (firstSerial == 0 || flipSerial == 0) throw std::runtime_error("Frame timing: incomplete submission lineage");
std::ostringstream output;
output.imbue(std::locale::classic());
output << std::fixed << std::setprecision(3);
output << "[FrameTiming] frame=" << id << " submissions=" << firstSerial << ':' << lastSerial;
output << " output=" << outputHandle;
output << " flip=" << flipSerial << ':' << flipOffset << " buffer=" << buffer << " argument=" << argument;
output << " endpoint=flip_complete display_confirmed=0";
output << " first_submit_to_flip_ms=" << milliseconds(finished - start);
output << " flip_submit_to_complete_ms=" << milliseconds(finished - flipReceived);
output << " first_submit_to_execute_ms=" << milliseconds(executionStart - start);
output << " execute_to_flip_packet_ms=" << milliseconds(flipReached - executionStart);
output << " flip_packet_to_complete_ms=" << milliseconds(finished - flipReached);
auto accounted = Clock::duration::zero();
for (const auto scope : {"Driver.Packet", "Driver.Suspend", "Driver.Worker", "Driver.Completion"}) {
const auto it = metrics.find({scope, "total"});
if (it != metrics.end()) accounted += it->second.total;
}
output << " worker_unattributed_ms=" << milliseconds(flipReached - executionStart - accounted);
if (interval != Clock::duration::zero()) output << " flip_interval_ms=" << milliseconds(interval);
output << " batches_at_flip=" << batchesAtFlip << " unsignaled_at_flip=" << unsignaledAtFlip << " batches_at_blit=" << batchesAtBlit << " batches_after_flip=" << batchesAfterFlip;
output << " gpu_busy_ms=" << gpuBusyMs << " gpu_gap_ms=" << gpuGapMs;
output << " last_submit_after_flip_ms=" << milliseconds(lastSubmitAfterFlip) << " blit_submit_after_flip_ms=" << milliseconds(blitSubmitAfterFlip);
output << " metrics=inclusive(count,sum_ms,max_ms[,bytes])";
for (const auto& [key, metric] : metrics) {
if (metric.count == 0) continue;
output << ' ' << key.first << '.' << key.second << "=(" << metric.count << ',' << milliseconds(metric.total) << ',' << milliseconds(metric.maximum);
if (metric.bytes != 0) output << ',' << metric.bytes;
output << ')';
}
APS5_LOG_TIMING("%s", output.str().c_str());
return [asyncMetrics = asyncMetrics, metrics = metrics, id = id, firstSerial = firstSerial, lastSerial = lastSerial, flipSerial = flipSerial, flipOffset = flipOffset, workerStart = workerStart, start = start, executionStart = executionStart, flipReceived = flipReceived, flipReached = flipReached, batchesAtFlip = batchesAtFlip, unsignaledAtFlip = unsignaledAtFlip, batchesAtBlit = batchesAtBlit, batchesAfterFlip = batchesAfterFlip, gpuBusyMs = gpuBusyMs, gpuGapMs = gpuGapMs, lastSubmitAfterFlip = lastSubmitAfterFlip, blitSubmitAfterFlip = blitSubmitAfterFlip, packetCount = packetCount, packetWords = packetWords, slowPacketTime = slowPacketTime, slowPacketSerial = slowPacketSerial, slowPacketOffset = slowPacketOffset, slowPacketOpcode = slowPacketOpcode, outputHandle, buffer, argument, finished, interval](std::ostream& output) {
output << std::fixed << std::setprecision(3);
output << "[FrameTiming] frame=" << id << " submissions=" << firstSerial << ':' << lastSerial;
output << " output=" << outputHandle;
output << " flip=" << flipSerial << ':' << flipOffset << " buffer=" << buffer << " argument=" << argument;
output << " endpoint=flip_complete display_confirmed=0 worker_queue=0 background_metrics=completed_since_previous_flip";
output << " first_submit_at_ms=" << milliseconds(start.time_since_epoch());
output << " first_execute_at_ms=" << milliseconds(executionStart.time_since_epoch());
output << " flip_packet_at_ms=" << milliseconds(flipReached.time_since_epoch());
output << " flip_complete_at_ms=" << milliseconds(finished.time_since_epoch());
output << " first_submit_to_flip_ms=" << milliseconds(finished - start);
output << " flip_submit_to_complete_ms=" << milliseconds(finished - flipReceived);
output << " first_submit_to_execute_ms=" << milliseconds(executionStart - start);
output << " execute_to_flip_packet_ms=" << milliseconds(flipReached - executionStart);
output << " flip_packet_to_complete_ms=" << milliseconds(finished - flipReached);
auto accounted = Clock::duration::zero();
for (const auto scope : {"Driver.Packet", "Driver.Suspend", "Driver.WorkerWait", "Driver.Completion", "Driver.EndSubmission", "Driver.Rewind"}) {
const auto it = metrics.find({scope, "total"});
if (it != metrics.end()) accounted += it->second.total;
}
output << " worker_begin_at_ms=" << milliseconds(workerStart.time_since_epoch());
output << " worker_span_ms=" << milliseconds(flipReached - workerStart);
output << " worker_accounted_ms=" << milliseconds(accounted);
output << " worker_unattributed_ms=" << milliseconds(flipReached - workerStart - accounted);
if (interval != Clock::duration::zero()) output << " flip_interval_ms=" << milliseconds(interval);
output << " batches_at_flip=" << batchesAtFlip << " unsignaled_at_flip=" << unsignaledAtFlip << " batches_at_blit=" << batchesAtBlit << " batches_after_flip=" << batchesAfterFlip;
output << " gpu_busy_ms=" << gpuBusyMs << " gpu_gap_ms=" << gpuGapMs;
output << " last_submit_after_flip_ms=" << milliseconds(lastSubmitAfterFlip) << " blit_submit_after_flip_ms=" << milliseconds(blitSubmitAfterFlip);
output << " packet_count=" << packetCount << " packet_dwords=" << packetWords;
output << " slow_packet_submission=" << slowPacketSerial << " slow_packet_offset=" << slowPacketOffset << " slow_packet_opcode=" << slowPacketOpcode << " slow_packet_ms=" << milliseconds(slowPacketTime);
output << " metrics=inclusive(count,sum_ms,max_ms[,bytes])";
const auto writeMetrics = [&](const auto& entries, const char* prefix) {
for (const auto& [key, metric] : entries) {
if (metric.count == 0) continue;
output << ' ' << prefix << key.first << '.' << key.second << "=(" << metric.count << ',' << milliseconds(metric.total) << ',' << milliseconds(metric.maximum);
if (metric.bytes != 0) output << ',' << metric.bytes;
output << ')';
}
};
writeMetrics(metrics, "");
writeMetrics(asyncMetrics, "Async.");
output << '\n';
};
}
private:
@ -148,12 +205,21 @@ private:
}
std::mutex mutex;
bool localMetrics = false;
std::map<std::pair<std::string_view, std::string_view>, Metric> metrics;
std::map<std::pair<std::string_view, std::string_view>, Metric> asyncMetrics;
std::uint64_t packetCount = 0;
std::uint64_t packetWords = 0;
Clock::duration slowPacketTime{};
std::uint64_t slowPacketSerial = 0;
std::size_t slowPacketOffset = 0;
std::uint32_t slowPacketOpcode = 0;
std::uint64_t id;
std::uint64_t firstSerial = 0;
std::uint64_t lastSerial = 0;
std::uint64_t flipSerial = 0;
std::size_t flipOffset = 0;
Clock::time_point workerStart{};
Clock::time_point start{};
Clock::time_point executionStart{};
Clock::time_point flipReceived{};
@ -196,7 +262,7 @@ public:
if (frame != nullptr) {
total = frame->Get(scope, "total");
}
start = Clock::now();
if (frame != nullptr) start = Clock::now();
previous = start;
}
@ -204,10 +270,20 @@ public:
PerformanceTimer& operator=(const PerformanceTimer&) = delete;
~PerformanceTimer() {
Finish();
}
void Finish() {
if (frame == nullptr) return;
const auto end = Clock::now();
if (tail != nullptr) frame->Add(tail, end - previous);
frame->Add(total, end - start);
frame = nullptr;
}
void FinishPacket(std::uint64_t serial, std::size_t offset, std::uint32_t opcode, std::size_t words) {
if (frame != nullptr) frame->NotePacket(serial, offset, opcode, words, Clock::now() - start);
Finish();
}
void Mark(const char* stage, std::uint64_t bytes = 0) {
@ -225,8 +301,8 @@ private:
const char* scope;
FrameTiming::Metric* total = nullptr;
FrameTiming::Metric* tail = nullptr;
Clock::time_point start;
Clock::time_point previous;
Clock::time_point start{};
Clock::time_point previous{};
};
}

View File

@ -12,6 +12,9 @@
namespace AgcDriver::Pm4 {
constexpr std::size_t GdsBytes = 0x10000;
std::uint64_t GdsAddress();
struct DrawParameters {
std::uint64_t indexAddress;
std::uint32_t indexCount;
@ -83,6 +86,9 @@ inline std::size_t PacketWords(std::uint32_t header) { return FillerPacket(heade
std::string_view UnsupportedReason(std::uint32_t header);
void Validate(std::span<const std::uint32_t> packet, std::uint32_t queue);
void Execute(std::span<const std::uint32_t> packet, QueueState& queue);
inline bool IndirectRegisterOpcode(std::uint32_t opcode) { return opcode == 0x63 || opcode == 0x64 || opcode == 0x9f; }
std::vector<std::uint32_t> ReadIndirectRegisters(std::span<const std::uint32_t> packet);
void ExecuteIndirectRegisters(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> pairs, QueueState& queue);
bool AccessesMemory(std::uint32_t header);
// Whether an ACQUIRE_MEM packet asks only for GPU cache actions (no CPU-visible memory
// synchronization): such a packet needs a pipeline barrier, not a device drain.

View File

@ -24,12 +24,17 @@ public:
}
~ProfileOutput() {
Stop();
}
void Stop() {
std::lock_guard stopLock(stopMutex);
{
std::lock_guard lock(mutex);
stopping = true;
}
changed.notify_one();
worker.join();
if (worker.joinable()) worker.join();
}
bool Write(std::string text) {
@ -117,6 +122,7 @@ private:
std::function<void(std::string_view)> writer;
const std::size_t capacity;
std::mutex stopMutex;
std::mutex mutex;
std::condition_variable changed;
std::condition_variable idle;

View File

@ -148,15 +148,18 @@ inline Registers InitialContextRegisters() {
{0xc, 0}, {0xd, 0x40004000}, {0x81, 0x80000000}, {0x82, 0x40004000},
{0x90, 0x80000000}, {0x91, 0x40004000},
{0x105, 0}, {0x106, 0}, {0x107, 0}, {0x108, 0},
{0x1b1, 0}, {0x1b6, 0}, {0x1c3, 0}, {0x1c4, 0}, {0x1c5, 0},
{0x1b1, 0}, {0x1b3, 0}, {0x1b4, 0}, {0x1b6, 0}, {0x1c3, 0}, {0x1c4, 0}, {0x1c5, 0},
{0x1ff, 0}, {0x292, 2}, {0x293, 0}, {0x29b, 0},
{0x2ce, 0}, {0x2d3, 0}, {0x2d5, 0}, {0x2d6, 0}, {0x2db, 0},
{0x2dc, 0xaa00}, {0x2e4, 0}, {0x2f8, 0}, {0x2f9, 0x2d},
{0x2dc, 0xaa00}, {0x2df, 0}, {0x2e4, 0}, {0x2f8, 0}, {0x2f9, 0x2d},
{0x30e, 0xffffffff}, {0x30f, 0xffffffff}, {0x313, 0x6000},
{0x318, 0}, {0x31b, 0}, {0x31c, 0}, {0x31d, 0},
{0x390, 0}, {0x3b0, 0}, {0x3b8, 0}
};
for (std::uint32_t i = 0; i < 8; ++i) result.emplace(0x1e0 + i, 0x20010001);
for (std::uint32_t i = 0; i < 8; ++i) {
result.emplace(0x1e0 + i, 0x20010001);
result.emplace(0x31c + 0xfu * i, 0);
}
for (std::uint32_t i = 0; i < 16; ++i) {
result.emplace(0x94 + 2 * i, 0x80000000);
result.emplace(0x95 + 2 * i, 0x40004000);

View File

@ -0,0 +1,31 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SHADERDEVICEPROFILE_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SHADERDEVICEPROFILE_HPP
#include "Recompiler.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Context.hpp"
#include <string>
#include <vector>
namespace AgcDriver {
class ShaderDeviceProfile {
public:
ShaderDeviceProfile(const ShaderRecompiler::SpirvTarget& target, const VkDeviceCreateInfo& deviceInfo, const VkPhysicalDeviceLimits& limits);
ShaderDeviceProfile(const ShaderDeviceProfile&) = delete;
ShaderDeviceProfile& operator=(const ShaderDeviceProfile&) = delete;
ShaderRecompiler::SpirvTarget Target() const { return target; }
const VkPhysicalDeviceLimits& Limits() const { return limits; }
bool NullDescriptors() const { return nullDescriptors; }
private:
ShaderRecompiler::SpirvTarget target;
const VkPhysicalDeviceLimits limits;
std::vector<std::uint32_t> capabilities;
std::vector<std::string> extensions;
std::vector<std::string_view> extensionViews;
bool nullDescriptors = false;
};
}
#endif

View File

@ -14,6 +14,7 @@
namespace ShaderRecompiler {
struct SourceHandle;
class PreparedShaderInvocation;
}
namespace AgcDriver {
@ -61,6 +62,7 @@ public:
// (the driver's memoized ShaderRecompiler::ResolveSource result) the capture skips the source
// resolution.
std::shared_ptr<const ShaderRecompiler::ResourceCapture> Capture(const ShaderRecompiler::RecompileRequest& request, const ShaderRecompiler::SourceHandle* handle = nullptr);
std::shared_ptr<const ShaderRecompiler::ResourceCapture> Capture(const ShaderRecompiler::PreparedShaderInvocation& invocation);
[[nodiscard]] std::vector<ShaderRecompiler::MemoryRegion> Regions() const;
// The page regions read since the previous call (or construction), a word read again
// included, the initial regions excluded: one stage's own reads on the draw path's shared
@ -70,6 +72,7 @@ public:
static void CountHandleMemo(bool hit);
private:
std::shared_ptr<const ShaderRecompiler::ResourceCapture> capture(const ShaderRecompiler::RecompileRequest& request, const ShaderRecompiler::SourceHandle* handle, const ShaderRecompiler::PreparedShaderInvocation* invocation);
static constexpr std::size_t PageBytes = 4096;
static constexpr std::size_t PageWords = PageBytes / sizeof(std::uint32_t);

View File

@ -0,0 +1,30 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SHADERPREPARATION_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SHADERPREPARATION_HPP
#include <memory>
namespace AgcDriver::DriverDetail {
struct ShaderSnapshot;
struct PreparedShaderState;
class ShaderPreparationTransaction {
public:
ShaderPreparationTransaction();
~ShaderPreparationTransaction();
ShaderPreparationTransaction(const ShaderPreparationTransaction&) = delete;
ShaderPreparationTransaction& operator=(const ShaderPreparationTransaction&) = delete;
PreparedShaderState& Edit(const ShaderSnapshot& snapshot);
const PreparedShaderState& Read(const ShaderSnapshot& snapshot) const;
void Commit();
private:
struct State;
std::unique_ptr<State> state;
State* root = nullptr;
bool committed = false;
};
}
#endif

View File

@ -0,0 +1,28 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SHADERPREPARATIONSCOPE_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SHADERPREPARATIONSCOPE_HPP
namespace AgcDriver::DriverDetail {
class ShaderPreparationTransaction;
}
extern "C" AgcDriver::DriverDetail::ShaderPreparationTransaction* AgcDriverBeginShaderPreparation_nid_postfix();
extern "C" void AgcDriverCommitShaderPreparation_nid_postfix(AgcDriver::DriverDetail::ShaderPreparationTransaction* transaction);
extern "C" void AgcDriverEndShaderPreparation_nid_postfix(AgcDriver::DriverDetail::ShaderPreparationTransaction* transaction) noexcept;
namespace AgcDriver {
class ShaderPreparationScope {
public:
ShaderPreparationScope() : transaction(AgcDriverBeginShaderPreparation_nid_postfix()) {}
~ShaderPreparationScope() { AgcDriverEndShaderPreparation_nid_postfix(transaction); }
ShaderPreparationScope(const ShaderPreparationScope&) = delete;
ShaderPreparationScope& operator=(const ShaderPreparationScope&) = delete;
void Commit() { AgcDriverCommitShaderPreparation_nid_postfix(transaction); }
private:
DriverDetail::ShaderPreparationTransaction* transaction;
};
}
#endif

View File

@ -0,0 +1,13 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SUBGROUPCLOCK_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SUBGROUPCLOCK_HPP
#include <vulkan/vulkan.h>
#include <string_view>
namespace AgcDriver {
bool NarrowSubgroupClock(VkDriverId driver, std::string_view deviceName);
}
#endif

View File

@ -161,6 +161,7 @@ public:
bool Presentable() const;
bool PrimitiveListRestart() const;
bool SamplerFilterMinmax() const;
bool ConservativeRasterization() const;
// A presentation is a few steps so the presenter holds GuestMemory::GpuMutex only while it
// touches the queue. Presentations are slots (FlipInFlight() + 1, each with its own command
// buffer, fence, kept resident image and dump buffer): RetirePresents(keep) (no mutex) retires
@ -299,6 +300,7 @@ private:
// The device's Graphics::Context: a copy of the one built at setup (its instance functions
// resolved then, its function table filled then), or with APS5_NO_CONTEXT_CACHE=1 built anew.
Graphics::Context graphicsContext() const;
ShaderRecompiler::SpirvTarget buildTarget() const;
Graphics::Context buildContext() const;
// Body of Dispatch and DispatchIndirect: `arguments` 0 dispatches x, y, z groups.
IndirectOutcome dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::uint64_t programAddress, std::shared_ptr<PreparedDispatch> prepared, std::shared_ptr<const Recipe>* recipe);

View File

@ -0,0 +1,85 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_WRITEWATCHCOVERAGE_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_WRITEWATCHCOVERAGE_HPP
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <vector>
#include <limits>
#include <unordered_map>
namespace AgcDriver::GuestMemory {
class WriteWatchCoverage {
public:
void Initialize(std::uint64_t address, std::size_t bytes) {
base = address;
size = bytes;
excluded.assign(bytes / BlockBytes + (bytes % BlockBytes != 0), 0);
excludedGenerations.clear();
}
bool Covers(std::uint64_t address, std::size_t bytes) const {
if (bytes == 0 || bytes > size || address < base || address - base > size - bytes) return false;
const auto first = (address - base) / BlockBytes;
const auto last = (address - base + bytes - 1) / BlockBytes;
return std::none_of(excluded.begin() + first, excluded.begin() + last + 1, [](auto generation) { return generation != 0; });
}
bool Exclude(std::uint64_t address, std::size_t bytes, std::uint64_t generation = 1) {
if (bytes == 0 || size == 0) return false;
if (address < base) {
if (base - address >= bytes) return false;
bytes -= base - address;
address = base;
}
const auto offset = address - base;
if (offset >= size) return false;
bytes = static_cast<std::size_t>(std::min<std::uint64_t>(bytes, size - offset));
const auto first = offset / BlockBytes;
const auto last = (offset + bytes - 1) / BlockBytes;
bool changed = false;
for (auto block = first; block <= last; ++block) {
changed = changed || excluded[block] == 0;
auto& stamp = excludedGenerations[block];
stamp = std::max(stamp, std::max<std::uint64_t>(generation, 1));
excluded[block] = 1;
}
return changed;
}
bool Restore(std::uint64_t address, std::size_t bytes, std::uint64_t generation = std::numeric_limits<std::uint64_t>::max()) {
if (bytes == 0 || size == 0) return false;
if (address < base) {
if (base - address >= bytes) return false;
bytes -= base - address;
address = base;
}
const auto offset = address - base;
if (offset >= size) return false;
bytes = static_cast<std::size_t>(std::min<std::uint64_t>(bytes, size - offset));
const auto first = offset / BlockBytes + (offset % BlockBytes != 0);
const auto stop = offset + bytes;
const auto end = stop / BlockBytes + (stop == size && stop % BlockBytes != 0);
bool changed = false;
for (auto block = first; block < end; ++block) {
if (excluded[block] != 0 && excludedGenerations.at(block) < generation) {
changed = true;
excluded[block] = 0;
excludedGenerations.erase(block);
}
}
return changed;
}
private:
static constexpr std::size_t BlockBytes = 65536;
std::uint64_t base = 0;
std::size_t size = 0;
std::vector<std::uint8_t> excluded;
std::unordered_map<std::uint64_t, std::uint64_t> excludedGenerations;
};
}
#endif

View File

@ -3,6 +3,7 @@
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
#include <algorithm>
#include <array>
#include <chrono>
#include <cstdio>
@ -10,6 +11,7 @@
#include <cstring>
#include <limits>
#include <stdexcept>
#include <string>
namespace AgcDriver {
namespace {
@ -50,6 +52,40 @@ void decodeTexel(const std::byte* texel, std::byte* pixel, bool rgba, bool tenBi
pixel[3] = texel[3];
}
constexpr std::uint32_t MetaBlockEdge = 512;
constexpr std::size_t MetaBlockKeys = 4096;
constexpr std::uint32_t KeyEdge = 8;
std::array<std::uint32_t, 2> displayableKeyPixel(const DisplayBuffer& buffer, std::size_t key) {
const auto pitch = (buffer.width + MetaBlockEdge - 1u) / MetaBlockEdge;
const auto block = static_cast<std::uint32_t>(key / MetaBlockKeys);
const auto offset = static_cast<std::uint32_t>(key % MetaBlockKeys);
std::uint32_t x = 0;
std::uint32_t y = 0;
for (std::uint32_t bit = 0; bit < 6; ++bit) {
x |= ((offset >> (2u * bit)) & 1u) << bit;
y |= ((offset >> (2u * bit + 1u)) & 1u) << bit;
}
return {block % pitch * MetaBlockEdge + x * KeyEdge, block / pitch * MetaBlockEdge + y * KeyEdge};
}
void traceDisplayKeys(const DisplayBuffer& buffer, const std::uint8_t* keys, std::size_t count) {
static const bool trace = std::getenv("APS5_TRACE_DCC_KEYS") != nullptr;
if (!trace) return;
constexpr std::size_t MaxRuns = 256;
std::string runs;
std::size_t begin = 0;
for (std::size_t run = 0; begin < count && run < MaxRuns; ++run) {
const auto end = static_cast<std::size_t>(std::find_if(keys + begin, keys + count, [&](std::uint8_t key) { return key != keys[begin]; }) - keys);
char text[32];
std::snprintf(text, sizeof(text), "%s%02x x%zu", run == 0 ? "" : ", ", keys[begin], end - begin);
runs += text;
begin = end;
}
if (begin < count) runs += ", ...";
std::fprintf(stderr, "[dcc-keys] display keys 0x%llx+0x%zx: %s\n", static_cast<unsigned long long>(buffer.dccAddress), count, runs.c_str());
}
}
std::size_t DisplayBufferSize(const DisplayBuffer& buffer) {
@ -117,6 +153,35 @@ std::array<std::byte, 4> DisplayBufferClearPixel(const DisplayBuffer& buffer, Gr
return pixel;
}
std::size_t DisplayBufferKeyBytes(const DisplayBuffer& buffer) {
const auto bytes = DisplayBufferSize(buffer);
require(buffer.tilingMode == 0, "VideoOut: a linear display buffer has no DCC keys");
return Graphics::DccKeyCount(Graphics::TextureTileMode::kR64KBX, 4, buffer.width, buffer.height, bytes);
}
Graphics::DccKeys DisplayBufferKeys(const DisplayBuffer& buffer) {
const auto count = DisplayBufferKeyBytes(buffer);
require(buffer.dccAddress != 0, "VideoOut: a display buffer without DCC metadata has no keys");
const auto keys = Graphics::CurrentDccKeys(buffer.dccAddress, DisplayBufferSize(buffer), count);
if (keys == Graphics::DccKeys::Uncompressed || Graphics::IsDccClear(keys)) return keys;
char differing[128] = "";
if (keys == Graphics::DccKeys::Mixed && GuestMemory::Accessible(reinterpret_cast<const void*>(buffer.dccAddress), count)) {
const auto* bytes = reinterpret_cast<const std::uint8_t*>(buffer.dccAddress);
traceDisplayKeys(buffer, bytes, count);
const auto* other = std::find_if(bytes + 1, bytes + count, [&](std::uint8_t key) { return key != bytes[0]; });
if (other == bytes + count) {
std::snprintf(differing, sizeof(differing), ": every key is 0x%02x", bytes[0]);
} else {
const auto key = static_cast<std::size_t>(other - bytes);
const auto [x, y] = displayableKeyPixel(buffer, key);
std::snprintf(differing, sizeof(differing), ": key %zu (pixels %u..%u x %u..%u in the displayable order) is 0x%02x, key 0 is 0x%02x", key, x, x + KeyEdge - 1u, y, y + KeyEdge - 1u, *other, bytes[0]);
}
}
char message[384];
std::snprintf(message, sizeof(message), "VideoOut: display buffer 0x%llx reads %s DCC keys at 0x%llx over its 0x%zx key bytes%s: presenting DCC metadata that is not uniformly uncompressed or fast-cleared is not implemented", static_cast<unsigned long long>(buffer.address), Graphics::DccKeysName(keys), static_cast<unsigned long long>(buffer.dccAddress), count, differing);
throw std::runtime_error(message);
}
std::vector<std::byte> ReadDisplayBuffer(const DisplayBuffer& buffer) {
const auto size = DisplayBufferSize(buffer);
static const bool traceGpu = std::getenv("APS5_TRACE_GPU") != nullptr;

View File

@ -1,22 +1,23 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp"
#include "Optimization/ResourceProgram.hpp"
#include <cstdlib>
#include <cstring>
namespace AgcDriver::DriverDetail {
void Driver::verifyDataHit(const ShaderSnapshot& snapshot, std::size_t codeOffset, std::uint64_t deviceSerial, ShaderRecompiler::RecompileRequest request, std::span<const ShaderRecompiler::MemoryRegion> memory, std::uint64_t address, const DispatchVariant& variant, std::span<const std::uint32_t> liveWords, const ShaderRecompiler::RecompileResult& patched) {
void Driver::verifyDataHit(const ShaderSnapshot& snapshot, std::size_t codeOffset, ShaderRecompiler::RecompileRequest request, std::span<const ShaderRecompiler::MemoryRegion> memory, std::uint64_t address, const DispatchVariant& variant, std::span<const std::uint32_t> liveWords, const ShaderRecompiler::RecompileResult& patched) {
const auto fail = [&](const char* what, std::size_t position, std::size_t slot) {
std::fprintf(stderr, "[dispatch-cache] APS5_VERIFY_DATA_HITS: %s disagrees (program 0x%llx, position %zu, slot %zu)\n", what, static_cast<unsigned long long>(address), position, slot);
std::fflush(stderr);
std::abort();
};
auto verifyMemory = std::make_shared<ShaderMemory>(memory, &queryPendingWrite, &observePendingWrite, hookWaitCounter());
const auto handle = SourceHandleFor(snapshot, codeOffset, deviceSerial, request, false);
const auto capture = verifyMemory->Capture(request, handle.get());
const auto invocation = InvocationFor(snapshot, codeOffset, request);
const auto capture = verifyMemory->Capture(invocation);
const auto regions = verifyMemory->Regions();
request.context.memory = regions;
const auto result = ShaderRecompiler::Recompile(request, *capture);
const auto result = invocation.Materialize(*capture);
std::size_t position = 0, run = 0;
for (const auto& region : regions) {
const bool registered = std::any_of(memory.begin(), memory.end(), [&](const auto& known) { return region.guestAddress >= known.guestAddress && region.guestAddress < known.guestAddress + known.bytes.size(); });

View File

@ -1,4 +1,5 @@
#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp"
@ -11,6 +12,7 @@
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);
auto it = submission.shaders->upper_bound(address);
std::shared_ptr<const ShaderSnapshot> registeredShader;
@ -26,7 +28,7 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
for (std::uint32_t i = 0; i < userCount; ++i) {
userData.push_back(readUserData(queue.shader, 0x240 + i));
}
auto compute = Graphics::DecodeComputeStageInfo(queue.shader);
auto compute = Graphics::DecodeComputeStageInfo(queue.shader, snapshot.header);
std::vector<ShaderRecompiler::MemoryRegion> memory{{snapshot.codeAddress, std::as_bytes(std::span(snapshot.code))}};
if (!snapshot.header.empty()) memory.push_back({snapshot.headerAddress, snapshot.header});
@ -98,13 +100,11 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
if (probeThis) std::fprintf(stderr, "[gpu] probing dispatch %llu of 0x%llx\n", static_cast<unsigned long long>(probeDispatch.second), static_cast<unsigned long long>(address));
}
if (FailureMemo() && snapshot.handles->poisoned.load(std::memory_order_relaxed) != 0) {
const std::string* poisoned = nullptr;
if (SourceHandleFor(snapshot, codeOffset, localDevice->Serial(), request, probeThis, &poisoned) == nullptr && poisoned != nullptr) {
pendingDispatchPhases().outcome = DispatchOutcome::SkippedMemo;
return;
}
}
struct ProbeScope {
bool active;
explicit ProbeScope(bool active) : active(active) { if (active) ShaderRecompiler::SetDebugProbeActive(true); }
~ProbeScope() { if (active) ShaderRecompiler::SetDebugProbeActive(false); }
} probeScope{probeThis};
const bool noDispatchCache = noDispatchCacheEnv || probeThis;
std::uint64_t key = 0xcbf29ce484222325ull;
const auto mix = [&](std::uint64_t value) {
@ -179,10 +179,13 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
previous.key = key;
}
if (!stampValidate()) mix(reinterpret_cast<std::uintptr_t>(registeredShader.get()));
mix(reinterpret_cast<std::uintptr_t>(registeredShader.get()));
phaseTiming.Phase(PhaseKey);
timing.Mark("decode_key_device_setup");
lookupDispatch(address, submission, key, noDispatchCache, traceCache, profile, memory, phaseTiming, phaseMs, compiledResult, keepVariant, captured, liveWords, dataHit, cached, validated, missedEntry, missedDiffering);
timing.Mark("lookup_cache");
if (cached) {
require(keepVariant != nullptr && keepVariant->shader == registeredShader, "dispatch cache belongs to another registered shader");
captureMs += phaseTiming.Elapsed();
} else {
shaderMemory = std::make_shared<ShaderMemory>(memory, &queryPendingWrite, &observePendingWrite, hookWaitCounter());
@ -190,18 +193,14 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
static const bool dumpShaders = std::getenv("APS5_DUMP_SHADERS") != nullptr;
try {
const auto invocation = InvocationFor(snapshot, codeOffset, request);
timing.Mark("prepared_invocation");
struct ProbeScope {
bool active;
explicit ProbeScope(bool active) : active(active) { if (active) ShaderRecompiler::SetDebugProbeActive(true); }
~ProbeScope() { if (active) ShaderRecompiler::SetDebugProbeActive(false); }
} probeScope{probeThis};
const auto waitedBefore = traceCapSync() ? Graphics::Recorder::ThreadWaitedMs() : 0.0;
forgetAtCapture = GuestMemory::ForgetSerial();
const auto handle = SourceHandleFor(snapshot, codeOffset, localDevice->Serial(), request, probeThis);
capture = [&] {
const SampledReadScope sampling(evidenceReads);
return shaderMemory->Capture(request, handle.get());
return shaderMemory->Capture(invocation);
}();
captured = shaderMemory->Regions();
request.context.memory = captured;
@ -211,10 +210,11 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
if (dumpShaders) static_cast<void>(dumpRequest(address, request));
const auto started = std::chrono::steady_clock::now();
static const bool reuseCapture = std::getenv("APS5_NO_CAPTURE_REUSE") == nullptr;
bool memoHit = false;
compiledResult = reuseCapture ? ShaderRecompiler::Recompile(request, *capture, &memoHit) : std::make_shared<const ShaderRecompiler::RecompileResult>(ShaderRecompiler::Recompile(request));
if (compiledResult->cacheHit || memoHit) ++cacheHits;
timing.Mark("capture_resources");
compiledResult = invocation.Materialize(*capture);
timing.Mark("materialize");
if (compiledResult->cacheHit) ++cacheHits;
const auto elapsed = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - started).count();
static double totalMs = 0;
totalMs += elapsed;
@ -233,7 +233,7 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
phaseTiming.Phase(PhaseRecompile);
insertDispatch(address, key, noDispatchCache, profile, registeredShader, forgetAtCapture, memory, shaderMemory, captured, capture, compiledResult, missedEntry, missedDiffering, attachVariant, phaseTiming);
}
if (verifyDataHits() && dataHit) verifyDataHit(snapshot, codeOffset, localDevice->Serial(), request, memory, address, *keepVariant, liveWords, *compiledResult);
if (verifyDataHits() && dataHit) verifyDataHit(snapshot, codeOffset, request, memory, address, *keepVariant, liveWords, *compiledResult);
if (recordQueuedLabelsAfterCapture(submission.queue, captured)) {
dispatch(queue, packet, submission, indirectArguments);

View File

@ -91,7 +91,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
if (!graphics.stages.mesh) return;
auto& words = programs.front().userData;
require(programs.front().firstUserSgpr == 0 && words.size() >= ShaderRecompiler::MeshIndexBufferUserWord + 4, "mesh program lacks the hidden user words");
const auto descriptor = Graphics::MeshIndexBufferDescriptor(parameters, programs.front().binary.codeAddress);
const auto descriptor = Graphics::MeshIndexBufferDescriptor(parameters);
std::copy(descriptor.begin(), descriptor.end(), words.begin() + ShaderRecompiler::MeshIndexBufferUserWord);
};
if (!drawParameters.indirect) setMeshIndexBuffer(drawParameters);
@ -204,7 +204,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
memory.insert(memory.end(), matchedRegions[i].begin(), matchedRegions[i].end());
} else {
resultIndex[i] = results.size();
results.push_back(compileDrawStage(i, pushCursorBytes, queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected));
results.push_back(materializeDrawStage(i, pushCursorBytes, queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected));
if (!rejected.empty()) return DrawVerdict::Rejected;
programResults[i] = &results.back();
}
@ -234,7 +234,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
const auto buildRectList = [&] {
phaseTiming.Phase(DrawRowVectors);
require(programs.size() == 2 && programResults[0] != nullptr && programResults[1] != nullptr, "rect-list requires vertex and fragment programs");
auto rectangle = ShaderRecompiler::BuildRectListShaders(*programResults[0], *programResults[1], localDevice->Target());
auto rectangle = PreparedRectangle(*programs[0].snapshot, programResults[0]->variantId, programResults[1]->variantId);
if (rectListBuilt) {
results[rectIndex] = std::move(rectangle.control);
results[rectIndex + 1] = std::move(rectangle.evaluation);
@ -329,7 +329,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
auto& result = results[resultIndex[programIndex]];
const auto pushBytes = result.pushConstants.size();
decodeVertexInfo(programIndex);
result = compileDrawStage(programIndex, pushOffsets[programIndex], queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected);
result = materializeDrawStage(programIndex, pushOffsets[programIndex], queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected);
if (!rejected.empty()) return DrawVerdict::Rejected;
require(result.pushConstants.size() == pushBytes, "patched program changed its push constant layout");
}

View File

@ -1,14 +1,17 @@
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
#include "Optimization/ResourceProgram.hpp"
#include <cstdlib>
#include <cstring>
namespace AgcDriver::DriverDetail {
ShaderRecompiler::RecompileResult Driver::compileDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector<DrawProgram>& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, std::vector<ShaderRecompiler::MemoryRegion>& memory, const std::vector<ShaderRecompiler::LinkedProgram>& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr<VulkanDevice>& localDevice, ShaderMemory& shaderMemory, std::vector<StageCapture>& stageCaptures, std::vector<bool>& recompiled, bool drawHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, const std::vector<std::vector<ShaderRecompiler::MemoryRegion>>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array<double, DrawDriverPhaseCount>& phaseMs, std::string& rejected) {
ShaderRecompiler::RecompileResult Driver::materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector<DrawProgram>& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, std::vector<ShaderRecompiler::MemoryRegion>& memory, const std::vector<ShaderRecompiler::LinkedProgram>& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr<VulkanDevice>& localDevice, ShaderMemory& shaderMemory, std::vector<StageCapture>& stageCaptures, std::vector<bool>& recompiled, bool drawHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, const std::vector<std::vector<ShaderRecompiler::MemoryRegion>>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array<double, DrawDriverPhaseCount>& phaseMs, std::string& rejected) {
PerformanceTimer timing("Shader.DrawStage");
using Stage = ShaderRecompiler::ShaderStage;
phaseTiming.Phase(DrawRowVectors);
const auto& program = programs[i];
@ -21,18 +24,14 @@ ShaderRecompiler::RecompileResult Driver::compileDrawStage(std::size_t i, std::u
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;
const std::string* poisoned = nullptr;
const auto handle = SourceHandleFor(*program.snapshot, program.codeOffset, localDevice->Serial(), request, false, FailureMemo() ? &poisoned : nullptr);
if (handle == nullptr && poisoned != nullptr) {
rejected = *poisoned;
return {};
}
const auto invocation = InvocationFor(*program.snapshot, program.codeOffset, request);
timing.Mark("prepared_invocation");
auto& stageCapture = stageCaptures[i];
stageCapture.forgetSerial = GuestMemory::ForgetSerial();
stageCapture.pushOffset = pushOffset;
const auto capture = [&] {
const SampledReadScope sampling(evidenceReads);
return shaderMemory.Capture(request, handle.get());
return shaderMemory.Capture(invocation);
}();
stageCapture.regions = shaderMemory.TakeRecentRegions();
@ -73,10 +72,11 @@ ShaderRecompiler::RecompileResult Driver::compileDrawStage(std::size_t i, std::u
}
}
static const bool reuseCapture = std::getenv("APS5_NO_CAPTURE_REUSE") == nullptr;
phaseTiming.Phase(DrawRowCapture);
stageCapture.compiled = reuseCapture ? ShaderRecompiler::Recompile(request, *capture) : std::make_shared<const ShaderRecompiler::RecompileResult>(ShaderRecompiler::Recompile(request));
timing.Mark("capture_resources");
stageCapture.compiled = invocation.Materialize(*capture);
timing.Mark("materialize");
ShaderRecompiler::RecompileResult result = *stageCapture.compiled;
phaseTiming.Phase(DrawRowRecompile);
return result;

View File

@ -5,27 +5,38 @@
namespace AgcDriver::DriverDetail {
std::shared_ptr<DrawDecode> Driver::decodeDraw(const QueueState& queue, const Submission& submission) {
namespace {
std::uint32_t ReadGraphicsRegister(const Registers& registers, std::uint32_t offset) {
const auto found = registers.find(offset);
return found == registers.end() ? 0u : found->second;
}
}
void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const ShaderRegistry& registry, bool staticAbi, bool includeFragment) {
using Stage = ShaderRecompiler::ShaderStage;
using Role = ShaderRecompiler::ProgramRole;
const auto programAddress = [&](std::uint32_t base) {
Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, base);
Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, base + 1);
const auto high = readRegister(queue.shader, base + 1);
const auto high = ReadGraphicsRegister(queue.shader, base + 1);
require((high & ~0xffu) == 0, "reserved graphics program address bits are set");
return (static_cast<std::uint64_t>(readRegister(queue.shader, base)) << 8u) | (static_cast<std::uint64_t>(high) << 40u);
return (static_cast<std::uint64_t>(ReadGraphicsRegister(queue.shader, base)) << 8u) | (static_cast<std::uint64_t>(high) << 40u);
};
const bool pixelSkipped = Graphics::PixelProgramSkipped(queue);
const auto prepare = [&](std::uint64_t address, std::uint8_t type, Stage stage, std::uint32_t rsrc2, std::uint32_t userDataBase) {
const bool nullPixel = address == 0 && stage == Stage::Fragment;
const bool nullPixel = stage == Stage::Fragment && (address == 0 || pixelSkipped);
if (nullPixel) address = NullPixelProgramAddress();
auto it = submission.shaders->upper_bound(address);
require(it != submission.shaders->begin(), "graphics program does not belong to a registered shader");
auto it = registry.upper_bound(address);
require(it != registry.begin(), "graphics program does not belong to a registered shader");
--it;
const auto& snapshot = *it->second;
require(address - snapshot.codeAddress < snapshot.code.size() * sizeof(std::uint32_t), "graphics program is outside registered shader code");
require((address - snapshot.codeAddress) % sizeof(std::uint32_t) == 0, "graphics entry point is not dword aligned");
require(snapshot.type == type, "graphics program refers to an incompatible shader binary type");
Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, rsrc2);
const auto resources = nullPixel && !queue.shader.contains(rsrc2) ? 0u : readRegister(queue.shader, rsrc2);
const auto resources = nullPixel && !queue.shader.contains(rsrc2) ? 0u : ReadGraphicsRegister(queue.shader, rsrc2);
const auto userCount = ((resources >> 1u) & 0x1fu) | (((resources >> 27u) & 1u) << 5u);
require(userCount <= 32, "graphics user SGPR count exceeds the register bank");
const auto codeOffset = static_cast<std::size_t>((address - snapshot.codeAddress) / sizeof(std::uint32_t));
@ -40,13 +51,12 @@ std::shared_ptr<DrawDecode> Driver::decodeDraw(const QueueState& queue, const Su
};
for (std::uint32_t i = 0; i < userCount; ++i) {
Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, userDataBase + i);
result.userData.push_back(readUserData(queue.shader, userDataBase + i));
result.userData.push_back((staticAbi ? ReadGraphicsRegister(queue.shader, userDataBase + i) : readUserData(queue.shader, userDataBase + i)));
}
return result;
};
{
auto product = std::make_shared<DrawDecode>();
product->state = Graphics::DecodeState(queue);
auto* product = &decoded;
auto& programs = product->programs;
auto& roles = product->roles;
programs.reserve(5);
@ -60,9 +70,10 @@ std::shared_ptr<DrawDecode> Driver::decodeDraw(const QueueState& queue, const Su
program.userData.insert(program.userData.begin(), 8, 0);
Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, pointerBase);
Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, pointerBase + 1);
if (staticAbi) return;
if (!pointerRequired && !queue.shader.contains(pointerBase) && !queue.shader.contains(pointerBase + 1)) return;
const auto low = readRegister(queue.shader, pointerBase);
const auto high = readRegister(queue.shader, pointerBase + 1);
const auto low = ReadGraphicsRegister(queue.shader, pointerBase);
const auto high = ReadGraphicsRegister(queue.shader, pointerBase + 1);
const auto address = static_cast<std::uint64_t>(low) | (static_cast<std::uint64_t>(high) << 32u);
require(address != 0 || !pointerRequired, "merged shader user-data address is null");
if (address == 0) return;
@ -78,8 +89,8 @@ std::shared_ptr<DrawDecode> Driver::decodeDraw(const QueueState& queue, const Su
append(0x0c8, 2, Stage::TessellationEvaluation, 0x08b, 0x08c, Role::Domain);
} else if (graphics.stages.path == Graphics::ShaderPath::Geometry) {
const auto frontAddress = programAddress(0xc8);
auto snapshot = submission.shaders->upper_bound(frontAddress);
require(snapshot != submission.shaders->begin(), "geometry front program is not registered");
auto snapshot = registry.upper_bound(frontAddress);
require(snapshot != registry.begin(), "geometry front program is not registered");
--snapshot;
const auto type = snapshot->second->type;
require(type == 2 || type == 4, "invalid geometry front binary type");
@ -89,18 +100,25 @@ std::shared_ptr<DrawDecode> Driver::decodeDraw(const QueueState& queue, const Su
} else {
append(0xc8, 2, Stage::Vertex, 0x8b, 0x8c, Role::Main);
}
const bool nullPixel = Graphics::PixelProgramUnset(queue);
if (nullPixel) {
const auto rejection = Graphics::NullPixelProgramRejection(queue);
require(rejection.empty(), rejection.c_str());
if (includeFragment) {
if (pixelSkipped) {
const auto rejection = Graphics::NullPixelProgramRejection(queue);
require(rejection.empty(), rejection.c_str());
}
append(0x008, 1, Stage::Fragment, 0x00b, 0x00c, Role::Fragment);
programs.back().firstUserSgpr = 0;
}
append(0x008, 1, Stage::Fragment, 0x00b, 0x00c, Role::Fragment);
programs.back().firstUserSgpr = 0;
product->pixel = Graphics::DecodePixelStageInfo(queue.context, Graphics::ExportMappings(graphics), nullPixel);
return product;
}
}
std::shared_ptr<DrawDecode> Driver::decodeDraw(const QueueState& queue, const Submission& submission) {
auto product = std::make_shared<DrawDecode>();
product->state = Graphics::DecodeState(queue);
DecodeGraphicsPrograms(*product, queue, *submission.shaders, false, true);
product->pixel = Graphics::DecodePixelStageInfo(queue.context, Graphics::ExportMappings(product->state), Graphics::PixelProgramSkipped(queue));
return product;
}
void Driver::resolveDrawDecode(const QueueState& queue, const Submission& submission, std::shared_ptr<const DrawDecode>& decode, bool registerKey, std::uint64_t drawKey, bool profile) {
if (decode == nullptr || verifyDrawRecipe()) {
std::vector<Graphics::RegisterRead> readLog;

View File

@ -63,6 +63,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr<Vulk
mix(pixel.inputAddr);
for (const bool flag : {pixel.wave32, pixel.hasPerspectiveCenterVgpr, pixel.perspectiveCentroid, pixel.posX, pixel.posY, pixel.posZ, pixel.posW, pixel.frontFace, pixel.ancillary, pixel.sampleShading, pixel.noPerspective, pixel.linearCentroid, pixel.pixelKillEnable, pixel.depthExportEnable, pixel.sampleMaskExportEnable, pixel.earlyZ, pixel.executeOnNoop}) mix(flag);
mix(static_cast<std::uint64_t>(pixel.conservativeZExport));
mix(pixel.orderedPixelShader);
for (const auto value : pixel.targetOutputMode) mix(value);
for (const auto value : pixel.targetExportMapping) mix(value);
std::lock_guard cacheLock(drawCacheMutex);

View File

@ -23,13 +23,15 @@ std::optional<DrawVerdict> Driver::precheckDraw(const QueueState& queue, const S
localDevice->ColorMetadataPass(*pass);
return DrawVerdict::Drawn;
}
rejected = Graphics::DepthMaintenanceRejection(queue);
if (!rejected.empty()) return DrawVerdict::Rejected;
static const bool traceIndirectEnabled = std::getenv("APS5_TRACE_INDIRECT_DRAWS") != nullptr;
traceIndirect = traceIndirectEnabled;
if (traceIndirect && drawParameters.indirect) std::fprintf(stderr, "[draw] indirect packet %s args 0x%llx count %u reached\n", Pm4::Name(packet[0]).c_str(), static_cast<unsigned long long>(drawParameters.indirect->arguments), drawParameters.indirect->count);
{
const auto targetMask = queue.context.find(0x8e);
const auto shaderMask = queue.context.find(0x8f);
const bool colorWrites = targetMask != queue.context.end() && shaderMask != queue.context.end() && (targetMask->second & shaderMask->second) != 0;
const bool colorWrites = targetMask != queue.context.end() && shaderMask != queue.context.end() && Graphics::ColorWriteMask(queue.context) != 0;
if (!colorWrites && !queue.shader.contains(0x8)) {
const auto word = [&](std::uint32_t offset) { const auto it = queue.context.find(offset); return it == queue.context.end() ? 0u : it->second; };
if ((word(0x200) & 3u) == 0 || ((word(0x010) & 3u) == 0 && (word(0x011) & 1u) == 0)) return DrawVerdict::Nothing;

View File

@ -1,20 +1,37 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include <algorithm>
#include <array>
#include <cstdlib>
#include <cstring>
namespace AgcDriver::DriverDetail {
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;
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);
}
@ -77,11 +94,11 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) {
for (std::size_t i = 0; i < s.blends.size(); ++i) {
if (!sameBlend(s.blends[i], t.blends[i])) return false;
}
if (s.hasColorTarget != t.hasColorTarget || s.rectList != t.rectList || s.renderExtent.width != t.renderExtent.width || s.renderExtent.height != t.renderExtent.height || s.topology != t.topology || s.negativeOneToOne != t.negativeOneToOne || s.depthClamp != t.depthClamp || s.cullMode != t.cullMode || s.frontFace != t.frontFace || !sameBlend(s.blend, t.blend) || s.blendConstants != t.blendConstants) return false;
if (s.hasColorTarget != t.hasColorTarget || s.rectList != t.rectList || s.renderExtent.width != t.renderExtent.width || s.renderExtent.height != t.renderExtent.height || s.topology != t.topology || s.negativeOneToOne != t.negativeOneToOne || s.depthClamp != t.depthClamp || s.conservativeRasterization != t.conservativeRasterization || s.cullMode != t.cullMode || s.frontFace != t.frontFace || !sameBlend(s.blend, t.blend) || s.blendConstants != t.blendConstants) return false;
if (std::memcmp(&s.viewport, &t.viewport, sizeof(VkViewport)) != 0 || std::memcmp(&s.scissor, &t.scissor, sizeof(VkRect2D)) != 0) return false;
const auto& p = a.pixel;
const auto& q = b.pixel;
if (p.interpolatorCount != q.interpolatorCount || p.interpolatorSettings != q.interpolatorSettings || p.wave32 != q.wave32 || p.inputAddr != q.inputAddr || p.hasPerspectiveCenterVgpr != q.hasPerspectiveCenterVgpr || p.perspectiveCentroid != q.perspectiveCentroid || p.posX != q.posX || p.posY != q.posY || p.posZ != q.posZ || p.posW != q.posW || p.frontFace != q.frontFace || p.ancillary != q.ancillary || p.sampleShading != q.sampleShading || p.noPerspective != q.noPerspective || p.linearCentroid != q.linearCentroid || p.pixelKillEnable != q.pixelKillEnable || p.depthExportEnable != q.depthExportEnable || p.sampleMaskExportEnable != q.sampleMaskExportEnable || p.earlyZ != q.earlyZ || p.executeOnNoop != q.executeOnNoop || p.conservativeZExport != q.conservativeZExport || p.targetOutputMode != q.targetOutputMode || p.targetExportMapping != q.targetExportMapping) return false;
if (p.interpolatorCount != q.interpolatorCount || p.interpolatorSettings != q.interpolatorSettings || p.wave32 != q.wave32 || p.inputAddr != q.inputAddr || p.hasPerspectiveCenterVgpr != q.hasPerspectiveCenterVgpr || p.perspectiveCentroid != q.perspectiveCentroid || p.posX != q.posX || p.posY != q.posY || p.posZ != q.posZ || p.posW != q.posW || p.frontFace != q.frontFace || p.ancillary != q.ancillary || p.sampleShading != q.sampleShading || p.noPerspective != q.noPerspective || p.linearCentroid != q.linearCentroid || p.pixelKillEnable != q.pixelKillEnable || p.depthExportEnable != q.depthExportEnable || p.sampleMaskExportEnable != q.sampleMaskExportEnable || p.earlyZ != q.earlyZ || p.executeOnNoop != q.executeOnNoop || p.conservativeZExport != q.conservativeZExport || p.orderedPixelShader != q.orderedPixelShader || p.targetOutputMode != q.targetOutputMode || p.targetExportMapping != q.targetExportMapping) return false;
if (a.roles != b.roles || a.programs.size() != b.programs.size()) return false;
for (std::size_t i = 0; i < a.programs.size(); ++i) {
const auto& x = a.programs[i];

View File

@ -109,3 +109,14 @@ extern "C" void AgcDriverReleaseWindow_nid_postfix(void* window) {
extern "C" void AgcDriverReportFailure_nid_postfix(std::exception_ptr error) {
AgcDriver::ReportFailure(error);
}
extern "C" void AgcDriverResolveShaderAbi_nid_postfix(const Shader* shader, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive) {
AgcDriver::DriverDetail::Driver::Get().ResolveShaderAbi(shader, context, primitive);
}
extern "C" void AgcDriverResolveGraphicsAbi_nid_postfix(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType) {
AgcDriver::DriverDetail::Driver::Get().ResolveGraphicsAbi(vertex, pixel, primitiveType);
}
extern "C" void AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span<const Shader* const> stages, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive) {
AgcDriver::DriverDetail::Driver::Get().ResolveGraphicsStagesAbi(stages, context, primitive);
}

View File

@ -2,6 +2,7 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp"
#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
namespace AgcDriver::DriverDetail {
@ -44,6 +45,7 @@ void Driver::stop() {
device.Reset();
replacedDevices.clear();
Graphics::ShutdownGuestBufferWorkers();
ProfileOutput_nid_no_patch().Stop();
stopped = true;
}

View File

@ -29,17 +29,11 @@ void Driver::timed(double WorkerProfile::*bucket, TWork&& work) {
}
}
template <typename TWork>
void Driver::tolerate(const char* kind, TWork&& work) {
try {
work();
} catch (const std::exception& error) {
reportSkip(kind, error.what());
}
}
void Driver::execute(const Submission& submission) {
auto* submissionTiming = includeTimingSubmission(submission, true);
if (submission.suspend) {
PerformanceContext timingContext(submissionTiming);
PerformanceTimer timing("Driver.Suspend");
static const bool suspendDrain = std::getenv("APS5_SUSPEND_DRAIN") != nullptr;
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
@ -113,9 +107,13 @@ void Driver::execute(const Submission& submission) {
const auto packet = std::span(submission.commands).subspan(cursor, count);
const auto opcode = (header >> 8u) & 0xffu;
auto nextCursor = cursor + count;
if (APS5_ENABLE_TIMING_LOG && submission.queue == 0 && pendingFrameTiming == nullptr) includeTimingSubmission(submission, false);
PerformanceContext timingContext(submission.queue == 0 ? pendingFrameTiming.get() : FrameTiming::Async());
PerformanceTimer timing("Driver.Packet");
std::shared_lock deviceUse(deviceReplacement, std::defer_lock);
if (opcode != 0x3c && opcode != 0x93 && header != RenderingWaitPacketHeader && header != FlipPacketHeader) deviceUse.lock();
timing.Mark("device_lock");
GuestMemory::SetCurrentPacket(header == FlipPacketHeader ? 0xffffu : opcode, submission.queue);
CaptureTrace::Log("packet submission=%llu queue=%x offset=%zu header=%08x words=%zu", static_cast<unsigned long long>(submission.serial), submission.queue, cursor, header, packet.size());
if (Pm4::Predicated(header) && queue.predication.operation != 0) {
@ -132,6 +130,7 @@ void Driver::execute(const Submission& submission) {
const auto flushStart = profilePackets ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
flushBetweenPackets(submission.queue, header, opcode == 0x49 || opcode == 0x37);
timing.Mark("flush_between_packets");
PacketTimer packetTimer{profilePackets, header == FlipPacketHeader ? 0xffffu : opcode, submission.queue, packetProfile, std::chrono::steady_clock::now()};
if (profilePackets) {
@ -170,6 +169,7 @@ void Driver::execute(const Submission& submission) {
bool wroteOnGpu = false, endOfPipeInterrupt = false, interruptDeferred = false, drawPacket = false, sampleDump = false;
const bool drains = preparePacketMemory(submission, queue, packet, header, opcode, wroteOnGpu, endOfPipeInterrupt, interruptDeferred, drawPacket, sampleDump);
traceLabel(packet, submission.queue);
timing.Mark("prepare_memory");
const bool waitPacket = opcode == 0x3c || opcode == 0x93 || header == RenderingWaitPacketHeader;
struct Progress {
@ -201,10 +201,14 @@ void Driver::execute(const Submission& submission) {
if (batchesAtFlip != 0) flipSerial = batchesAtFlip;
flipBatchesUnsignaled += unsignaledAtFlip;
auto frame = std::make_shared<FrameTiming>(++frameSerial);
timing.Mark("flip_flush");
timing.FinishPacket(submission.serial, cursor, 0xffffu, count);
auto frame = APS5_ENABLE_TIMING_LOG ? std::move(pendingFrameTiming) : std::make_shared<FrameTiming>(++frameSerial);
require(frame != nullptr, "flip has no frame timing");
const auto now = FrameTiming::Clock::now();
frame->IncludeSubmission(submission.serial, now, now, now, true);
frame->SetFlip(submission.serial, cursor, now, now);
if (!APS5_ENABLE_TIMING_LOG) frame->IncludeSubmission(submission.serial, now, now, now, true);
frame->SetFlip(submission.serial, cursor, APS5_ENABLE_TIMING_LOG ? submission.receivedAt : now, now);
if (APS5_ENABLE_TIMING_LOG) frame->CollectBackground();
frame->NoteFlipBatches(batchesAtFlip, unsignaledAtFlip);
CaptureTrace::Log("flip frame=%llu submission=%llu offset=%zu batch=%llu unsignaled=%llu", static_cast<unsigned long long>(frameSerial), static_cast<unsigned long long>(submission.serial), cursor, static_cast<unsigned long long>(batchesAtFlip), static_cast<unsigned long long>(unsignaledAtFlip));
submission.flips.at(cursor)->GpuReady(frame);
@ -234,7 +238,7 @@ void Driver::execute(const Submission& submission) {
}
} else if (drawPacket) {
bool drawn = false;
timed(&WorkerProfile::drawMs, [&] { tolerate("draw", [&] {
timed(&WorkerProfile::drawMs, [&] {
static const bool traceDraws = std::getenv("APS5_TRACE_DRAWS") != nullptr;
static const bool profileDraws = std::getenv("APS5_PROFILE_DRAW") != nullptr;
const auto color = (static_cast<std::uint64_t>(readRegister(queue.context, 0x390)) << 40u) | (static_cast<std::uint64_t>(readRegister(queue.context, 0x318)) << 8u);
@ -243,36 +247,14 @@ void Driver::execute(const Submission& submission) {
if (profileDraws) Graphics::CountDrawSkip(kind, std::chrono::duration<double, std::micro>(std::chrono::steady_clock::now() - started).count());
};
const auto skipped = [&](const std::string& what) {
if (traceDraws) std::fprintf(stderr, "[draw] target 0x%llx mask 0x%x failed: %.160s\n",static_cast<unsigned long long>(color), readRegister(queue.context, 0x8e), what.c_str());
char suffix[80];
std::snprintf(suffix, sizeof(suffix), " [%s, color target 0x%llx]", Pm4::Name(header).c_str(), static_cast<unsigned long long>(color));
static std::set<std::pair<std::uint64_t, std::string>> dumpedTargets;
const std::string reason = what.substr(0, 48);
if (dumpedTargets.insert({color, reason}).second) {
char name[64];
std::snprintf(name, sizeof(name), "draw_%llx_%08x.regs", static_cast<unsigned long long>(color), static_cast<std::uint32_t>(std::hash<std::string>{}(reason)));
if (std::FILE* file = std::fopen(name, "w")) {
std::fprintf(file, "# %s\n", what.c_str());
recent.Print(file, "# packet %s\n");
for (const auto& [offset, value] : queue.context) std::fprintf(file, "context %x %08x\n", offset, value);
for (const auto& [offset, value] : queue.userConfig) std::fprintf(file, "uconfig %x %08x\n", offset, value);
for (const auto& [offset, value] : queue.shader) std::fprintf(file, "shader %x %08x\n", offset, value);
std::fclose(file);
}
}
reportSkip("draw", what + suffix);
};
try {
std::string rejected;
const auto verdict = draw(queue, packet, submission, rejected);
drawn = verdict == DrawVerdict::Drawn;
CaptureTrace::Log("draw submission=%llu queue=%x offset=%zu target=%llx mask=%x verdict=%d reason=%.256s", static_cast<unsigned long long>(submission.serial), submission.queue, cursor, static_cast<unsigned long long>(color), readRegister(queue.context, 0x8e), static_cast<int>(verdict), rejected.c_str());
if (verdict == DrawVerdict::Rejected) {
skipped(rejected);
countSkip(Graphics::DrawSkip::Prechecked);
throw std::runtime_error(rejected);
} else if (verdict == DrawVerdict::Nothing) {
countSkip(Graphics::DrawSkip::Nothing);
} else if (traceDraws) {
@ -280,10 +262,10 @@ void Driver::execute(const Submission& submission) {
}
} catch (const std::exception& error) {
CaptureTrace::Log("draw-error submission=%llu offset=%zu reason=%.256s", static_cast<unsigned long long>(submission.serial), cursor, error.what());
skipped(error.what());
countSkip(Graphics::DrawSkip::Thrown);
throw;
}
}); });
});
finishDrawPacket(drawn);
} else if (opcode == 0x22) {
recordQueuedLabelsBeforeRead(submission.queue);
@ -294,7 +276,8 @@ void Driver::execute(const Submission& submission) {
dumpSampleCounters(packet[2] | (static_cast<std::uint64_t>(packet[3]) << 32u));
} else if (opcode != 0x42 && opcode != 0x46 && opcode != 0x58) {
if (!wroteOnGpu) {
Pm4::Execute(packet, queue);
if (Pm4::IndirectRegisterOpcode(opcode)) Pm4::ExecuteIndirectRegisters(packet, submission.registerLists.at(cursor), queue);
else Pm4::Execute(packet, queue);
if (opcode == 0x49 || opcode == 0x37) {
if (const auto label = Pm4::DecodeLabelWrite(packet)) noteLabelStore(label->address, label->Bytes(), ++eventSerial);
}
@ -302,9 +285,14 @@ void Driver::execute(const Submission& submission) {
if (endOfPipeInterrupt && !interruptDeferred) AgcDriverDeliverEopInterrupt(submission.queue);
}
if (drawPacket || (sampleDump && wroteOnGpu)) Graphics::Recorder::CountRecordedWork();
timing.Mark(header == RenderingWaitPacketHeader ? "rendering_wait" : opcode == 0x3c || opcode == 0x93 ? "memory_wait" : opcode == 0x15 || opcode == 0x16 ? "dispatch" : drawPacket ? "draw" : "execute");
if (submission.queue == 0) timing.FinishPacket(submission.serial, cursor, opcode, count);
else timing.Finish();
cursor = nextCursor;
}
PerformanceContext endContext(submission.queue == 0 ? frameTiming() : FrameTiming::Async());
PerformanceTimer endTiming("Driver.EndSubmission");
static const bool submitAtEnd = std::getenv("APS5_SUBMIT_AT_END") != nullptr;
if (!deferredLabels().labels.empty() || Graphics::Recorder::PendingLabelSince().has_value() || Graphics::Recorder::RecordedWorkSinceSubmit() != 0) {
auto& costs = submissionCosts(submission.queue);
@ -320,6 +308,7 @@ void Driver::execute(const Submission& submission) {
recordDeferredLabels(localDevice.get(), submission.queue);
if (localDevice != nullptr) localDevice->SubmitRecorded(submission.queue == 0);
}
endTiming.Finish();
if (submission.rewindTail != nullptr) executeRewindTail(submission);
}

View File

@ -18,7 +18,8 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue
bool orderedAlready = false;
endOfPipeInterrupt = opcode == 0x49 && ((packet[2] >> 24u) & 7u) != 0;
const auto interruptSelect = (packet[2] >> 24u) & 7u;
endOfPipeInterrupt = opcode == 0x49 && interruptSelect != 0 && interruptSelect != 3;
interruptDeferred = false;
if (!drainAll && endOfPipeInterrupt) {
const auto label = Pm4::DecodeLabelWrite(packet);

View File

@ -2,12 +2,42 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Queues/WorkerAffinity.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/WaitMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include <cstdlib>
namespace AgcDriver::DriverDetail {
FrameTiming* Driver::frameTiming() {
if (!APS5_ENABLE_TIMING_LOG) return nullptr;
if (pendingFrameTiming == nullptr) pendingFrameTiming = std::make_shared<FrameTiming>(++frameSerial, true);
return pendingFrameTiming.get();
}
FrameTiming* Driver::includeTimingSubmission(const Submission& submission, bool firstSegment) {
if (!APS5_ENABLE_TIMING_LOG) return nullptr;
auto* frame = submission.queue == 0 ? frameTiming() : FrameTiming::Async();
require(submission.receivedAt != FrameTiming::Clock::time_point{} && submission.receivedAt <= submission.enqueuedAt && submission.enqueuedAt <= submission.dequeuedAt && submission.dequeuedAt <= submission.orderedAt, "invalid submission timing order");
if (submission.queue == 0) frame->IncludeSubmission(submission.serial, submission.receivedAt, submission.enqueuedAt, submission.orderedAt, firstSegment);
else if (firstSegment) {
frame->Add(frame->Get("Submission", "accept"), submission.enqueuedAt - submission.receivedAt);
frame->Add(frame->Get("Submission", "queue"), submission.orderedAt - submission.enqueuedAt);
}
if (firstSegment) {
frame->Add(frame->Get("Submission", "dequeue"), submission.dequeuedAt - submission.enqueuedAt);
frame->Add(frame->Get("Submission", "order_wait"), submission.orderedAt - submission.dequeuedAt);
if (!submission.suspend) {
frame->Add(frame->Get("Submission", "copy"), submission.copiedAt - submission.receivedAt, submission.commands.size() * sizeof(std::uint32_t));
frame->Add(frame->Get("Submission", "validate"), submission.validatedAt - submission.copiedAt);
frame->Add(frame->Get("Submission", "flip_room_wait"), submission.roomReadyAt - submission.validatedAt);
frame->Add(frame->Get("Submission", "reserve_enqueue"), submission.enqueuedAt - submission.roomReadyAt);
}
}
return frame;
}
void Driver::markCompleted(std::uint64_t serial) {
completedOutOfOrder.insert(serial);
while (!completedOutOfOrder.empty() && *completedOutOfOrder.begin() == completed + 1) {
@ -52,12 +82,25 @@ void Driver::run(std::uint32_t id) noexcept {
submission = Submission{};
static const bool traceGpu = std::getenv("APS5_TRACE_GPU") != nullptr;
{
PerformanceContext timingContext(id == 0 ? frameTiming() : FrameTiming::Async());
PerformanceTimer timing("Driver.WorkerWait");
std::unique_lock lock(mutex);
timing.Mark("mutex_wait");
auto& worker = workers.at(id);
auto& pending = worker.pending;
workerQueued() = &worker.queued;
if (traceGpu && pending.empty()) std::fprintf(stderr, "[gpu] %.1f idle queue=0x%x\n", TraceMs(), id);
const auto ready = [&] { return failure || stopping || !pending.empty(); };
const auto poll = [&](const auto& done) {
#ifdef _WIN32
lock.unlock();
PollSleep();
lock.lock();
return done();
#else
return changed.wait_for(lock, std::chrono::milliseconds(1), done);
#endif
};
while (!ready()) {
if (!completionsPending() || (id != 0 && Graphics::Recorder::PendingCompletionLabels() == 0)) {
@ -66,24 +109,29 @@ void Driver::run(std::uint32_t id) noexcept {
if (id == 0) queue0Dormant.store(false, std::memory_order_relaxed);
break;
}
if (changed.wait_for(lock, std::chrono::milliseconds(1), ready)) break;
if (poll(ready)) break;
lock.unlock();
reapCompletionLabels();
lock.lock();
}
timing.Mark("idle_and_completion_poll");
rethrowFailure();
if (stopping || shutdownToken.stop_requested() || pending.empty()) {
break;
}
submission = std::move(pending.front());
pending.pop_front();
if (APS5_ENABLE_TIMING_LOG) submission.dequeuedAt = std::chrono::steady_clock::now();
if (id == 0) queue0Executing = submission.suspend ? 0 : submission.received;
if (submission.waitFree) {
orderHolders.fetch_add(1, std::memory_order_acq_rel);
while (!failure && !stopping && !orderReleased(id, submission.received)) changed.wait_for(lock, std::chrono::milliseconds(1));
const auto released = [&] { return failure || stopping || orderReleased(id, submission.received); };
while (!released()) poll(released);
orderHolders.fetch_sub(1, std::memory_order_acq_rel);
rethrowFailure();
}
if (APS5_ENABLE_TIMING_LOG) submission.orderedAt = std::chrono::steady_clock::now();
timing.Mark("dequeue_order");
worker.queued.fetch_sub(1, std::memory_order_acq_rel);
if (profile && submission.enqueuedAt != std::chrono::steady_clock::time_point{}) costs.dequeueNs += static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - submission.enqueuedAt).count());
}
@ -97,6 +145,8 @@ void Driver::run(std::uint32_t id) noexcept {
}
if (traceGpu) std::fprintf(stderr, "[gpu] %.1f done serial=%llu queue=0x%x\n", TraceMs(), static_cast<unsigned long long>(submission.serial), id);
const auto completeStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
PerformanceContext completionContext(id == 0 ? frameTiming() : FrameTiming::Async());
PerformanceTimer completionTiming("Driver.Completion");
bool notify = true;
{
std::lock_guard lock(mutex);

View File

@ -1,3 +1,4 @@
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Queues/Submission.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
@ -103,6 +104,19 @@ bool Driver::copySegment(Submission& submission, const std::uint32_t* guest, std
return false;
}
void Driver::readRegisterLists(Submission& submission) {
submission.registerLists.clear();
for (std::size_t cursor = 0; cursor < submission.commands.size(); cursor += Pm4::PacketWords(submission.commands[cursor])) {
const auto header = submission.commands[cursor];
if ((header >> 30u) != 3u || !Pm4::IndirectRegisterOpcode((header >> 8u) & 0xffu)) continue;
try {
submission.registerLists.emplace(cursor, Pm4::ReadIndirectRegisters(std::span<const std::uint32_t>(submission.commands).subspan(cursor, Pm4::PacketWords(header))));
} catch (const std::exception& error) {
throw std::runtime_error("AGC driver: " + Pm4::Name(header) + " at DWORD " + std::to_string(cursor) + ": " + error.what());
}
}
}
void Driver::waitForFlipRoom(const Submission& submission) {
for (std::size_t cursor = 0; cursor < submission.commands.size(); cursor += Pm4::PacketWords(submission.commands[cursor])) {
if (submission.commands[cursor] != FlipPacketHeader) continue;
@ -141,6 +155,7 @@ void Driver::reserveOutputs(Submission& submission) {
}
void Driver::executeRewindTail(const Submission& stalled) {
PerformanceTimer timing("Driver.Rewind");
std::atomic_ref<std::uint32_t> control(*const_cast<std::uint32_t*>(stalled.rewindTail - 1));
if ((control.load(std::memory_order_acquire) & 0x80000000u) == 0) {
noteWaitBlocked(stalled.queue, reinterpret_cast<std::uint64_t>(stalled.rewindTail - 1), true);
@ -157,9 +172,14 @@ void Driver::executeRewindTail(const Submission& stalled) {
}
Submission tail{};
tail.queue = stalled.queue;
if (APS5_ENABLE_TIMING_LOG) tail.receivedAt = std::chrono::steady_clock::now();
copyCommands(tail, stalled.rewindTail, stalled.rewindWords);
if (APS5_ENABLE_TIMING_LOG) tail.copiedAt = std::chrono::steady_clock::now();
validate(tail, stalled.rewindTail);
readRegisterLists(tail);
if (APS5_ENABLE_TIMING_LOG) tail.validatedAt = std::chrono::steady_clock::now();
waitForFlipRoom(tail);
if (APS5_ENABLE_TIMING_LOG) tail.roomReadyAt = std::chrono::steady_clock::now();
{
std::lock_guard lock(mutex);
rethrowFailure();
@ -169,10 +189,13 @@ void Driver::executeRewindTail(const Submission& stalled) {
tail.serial = stalled.serial;
tail.received = ++eventSerial;
}
if (APS5_ENABLE_TIMING_LOG) tail.enqueuedAt = tail.dequeuedAt = tail.orderedAt = std::chrono::steady_clock::now();
timing.Finish();
execute(tail);
}
void Driver::Submit(const Packet* packet, std::uint32_t queue) {
const auto receivedAt = APS5_ENABLE_TIMING_LOG ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
CheckFailure();
require(queue == 0 || (queue >= 0x20 && queue < 0x58), "unsupported compute queue");
GuestMemory::CheckRange(packet, sizeof(Packet), alignof(Packet));
@ -180,6 +203,7 @@ void Driver::Submit(const Packet* packet, std::uint32_t queue) {
require(descriptor.flags == 0, "nonzero submission flags are not implemented");
Submission submission{};
submission.queue = queue;
submission.receivedAt = receivedAt;
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
auto& costs = submissionCosts(queue);
const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
@ -189,8 +213,12 @@ void Driver::Submit(const Packet* packet, std::uint32_t queue) {
copyCommands(submission, descriptor.addr, descriptor.dw_num);
}
const auto copied = profile ? std::chrono::steady_clock::now() : start;
if (APS5_ENABLE_TIMING_LOG) submission.copiedAt = std::chrono::steady_clock::now();
validate(submission, descriptor.addr);
readRegisterLists(submission);
if (APS5_ENABLE_TIMING_LOG) submission.validatedAt = std::chrono::steady_clock::now();
waitForFlipRoom(submission);
if (APS5_ENABLE_TIMING_LOG) submission.roomReadyAt = std::chrono::steady_clock::now();
static const bool trace = std::getenv("APS5_TRACE_GPU") != nullptr;
if (trace) std::fprintf(stderr, "[gpu] %.1f submit queue=0x%x dwords=%zu at %p\n", TraceMs(), queue, submission.commands.size(), static_cast<const void*>(descriptor.addr));
const auto validated = profile ? std::chrono::steady_clock::now() : start;
@ -211,6 +239,7 @@ void Driver::Submit(const Packet* packet, std::uint32_t queue) {
costs.validateNs += static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(validated - copied).count());
costs.copyNs += static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>((copied - start) + (now - validated)).count());
}
if (APS5_ENABLE_TIMING_LOG) submission.enqueuedAt = std::chrono::steady_clock::now();
enqueue(std::move(submission));
++accepted;
}
@ -218,6 +247,7 @@ void Driver::Submit(const Packet* packet, std::uint32_t queue) {
}
void Driver::SuspendPoint() {
const auto receivedAt = APS5_ENABLE_TIMING_LOG ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
require(!onWorkerThread(), "worker cannot suspend itself");
std::unique_lock lock(mutex);
rethrowFailure();
@ -228,6 +258,7 @@ void Driver::SuspendPoint() {
boundary.suspend = true;
boundary.queue = 0;
boundary.receivedAt = receivedAt;
boundary.enqueuedAt = std::chrono::steady_clock::now();
enqueue(std::move(boundary));
++accepted;

View File

@ -10,7 +10,9 @@ std::uint64_t WorkerAffinityMask() {
if (std::getenv("APS5_NO_WORKER_AFFINITY") != nullptr) return std::uint64_t{0};
const auto requested = CpuTopology::MaskFromEnvironment("APS5_WORKER_AFFINITY_MASK");
if (requested != 0) return requested;
#ifndef __linux__
if (std::getenv("APS5_WORKER_AFFINITY") == nullptr) return std::uint64_t{0};
#endif
const auto& layout = CpuTopology::Get();
return layout.hybrid ? layout.performant : std::uint64_t{0};
}();

View File

@ -1,8 +1,15 @@
#include "prx/libc/include/general/LogMacros.hpp"
#include "ControlFlow/GraphBuilder.hpp"
#include "ControlFlow/Structurizer.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "CacheKey.hpp"
#include "prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp"
#include "prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp"
#include "CompiledVariant.hpp"
#include "Optimization/ResourceProgram.hpp"
#include <cstdlib>
#include <cstring>
@ -78,71 +85,458 @@ std::shared_ptr<const ShaderSnapshot> ReadRawComputeShader(std::uint64_t address
throw std::runtime_error("AGC driver: raw compute program has no reachable end within mapped code or the size limit");
}
bool FailureMemo() {
static const bool memo = std::getenv("APS5_NO_FAILURE_MEMO") == nullptr;
return memo;
struct ShaderPreparationTransaction::State {
struct Change {
std::shared_ptr<PreparedShaders> destination;
PreparedShaderState prepared;
};
static std::mutex writers;
static thread_local State* active;
std::unique_lock<std::mutex> lock{writers};
std::map<PreparedShaders*, Change> changes;
bool failed = false;
};
std::mutex ShaderPreparationTransaction::State::writers;
thread_local ShaderPreparationTransaction::State* ShaderPreparationTransaction::State::active = nullptr;
ShaderPreparationTransaction::ShaderPreparationTransaction() {
if (State::active == nullptr) {
state = std::make_unique<State>();
State::active = state.get();
}
root = State::active;
}
std::shared_ptr<const ShaderRecompiler::SourceHandle> SourceHandleFor(const ShaderSnapshot& snapshot, std::size_t codeOffset, std::uint64_t deviceSerial, const ShaderRecompiler::RecompileRequest& request, bool bypass, const std::string** poisoned) {
static const bool enabled = std::getenv("APS5_NO_SOURCE_HANDLE_CACHE") == nullptr && std::getenv("APS5_NO_CAPTURE_REUSE") == nullptr;
static const bool verify = std::getenv("APS5_VERIFY_SOURCE_HANDLE") != nullptr;
if (!enabled || bypass || ShaderRecompiler::DebugProbeActive() || !request.useCache) return nullptr;
std::uint64_t key = 0xcbf29ce484222325ull;
for (const auto value : {static_cast<std::uint64_t>(codeOffset), deviceSerial, ShaderRecompiler::RecompileCacheKey::ContextHash(request)}) {
key ^= value;
key *= 0x100000001b3ull;
ShaderPreparationTransaction::~ShaderPreparationTransaction() {
if (!committed) root->failed = true;
if (state != nullptr) State::active = nullptr;
}
PreparedShaderState& ShaderPreparationTransaction::Edit(const ShaderSnapshot& snapshot) {
require(!committed, "shader preparation transaction is already committed");
auto& changes = root->changes;
const auto found = changes.find(snapshot.prepared.get());
if (found != changes.end()) return found->second.prepared;
std::lock_guard lock(snapshot.prepared->mutex);
return changes.emplace(snapshot.prepared.get(), State::Change{snapshot.prepared, *snapshot.prepared}).first->second.prepared;
}
const PreparedShaderState& ShaderPreparationTransaction::Read(const ShaderSnapshot& snapshot) const {
require(!committed, "shader preparation transaction is already committed");
const auto found = root->changes.find(snapshot.prepared.get());
return found != root->changes.end() ? found->second.prepared : *snapshot.prepared;
}
void ShaderPreparationTransaction::Commit() {
require(!committed && !root->failed, "shader preparation transaction was aborted");
if (state != nullptr) {
std::vector<std::unique_lock<std::mutex>> locks;
locks.reserve(state->changes.size());
for (const auto& [key, change] : state->changes) locks.emplace_back(change.destination->mutex);
for (auto& [key, change] : state->changes) {
auto& destination = *change.destination;
destination.entries.swap(change.prepared.entries);
destination.registeredAbis.swap(change.prepared.registeredAbis);
destination.graphicsAbis.swap(change.prepared.graphicsAbis);
destination.rectangles.swap(change.prepared.rectangles);
destination.rectangleProgress.swap(change.prepared.rectangleProgress);
destination.fragments.swap(change.prepared.fragments);
std::swap(destination.rectangleRequested, change.prepared.rectangleRequested);
}
}
if (request.graphics.has_value()) {
for (const auto& linked : request.graphics->linkedPrograms) {
for (const auto value : {static_cast<std::uint64_t>(linked.role), linked.binary.codeAddress}) {
key ^= value;
key *= 0x100000001b3ull;
committed = true;
}
void PublishRegisteredShader(std::shared_ptr<ShaderRegistry>& registry, const std::shared_ptr<const ShaderSnapshot>& snapshot) {
ShaderPreparationTransaction transaction;
if (registry != nullptr) {
const auto found = registry->find(snapshot->codeAddress);
if (found != registry->end()) {
const auto& current = *found->second;
if (current.headerAddress == snapshot->headerAddress && current.type == snapshot->type && current.code == snapshot->code && current.header == snapshot->header) {
transaction.Commit();
return;
}
}
}
auto& memos = *snapshot.handles;
auto next = registry != nullptr ? std::make_shared<ShaderRegistry>(*registry) : std::make_shared<ShaderRegistry>();
next->insert_or_assign(snapshot->codeAddress, snapshot);
registry = std::move(next);
transaction.Commit();
}
std::shared_ptr<const ShaderRecompiler::SourceHandle> SourceHandleFor(const ShaderSnapshot& snapshot, std::size_t codeOffset, const ShaderRecompiler::RecompileRequest& request) {
struct PreparedKeyStorage {};
auto& key = HostThreadLocal<std::vector<std::uint64_t>, PreparedKeyStorage>();
ShaderRecompiler::BuildPreparedShaderKey(request, key);
std::lock_guard lock(snapshot.prepared->mutex);
for (const auto& entry : snapshot.prepared->entries) {
if (entry.codeOffset == codeOffset && ShaderRecompiler::MatchesPreparedShader(request, *entry.handle, key)) return entry.handle;
}
if (snapshot.header.empty()) {
if (snapshot.type != 0 || request.shader.stage != ShaderRecompiler::ShaderStage::Compute) throw std::runtime_error("AGC driver: unregistered program is not a compute shader");
APS5_LOG_ERR("Compute shader 0x%llx was not registered; preparing its artifact at dispatch", static_cast<unsigned long long>(snapshot.codeAddress));
auto handle = ShaderRecompiler::PrepareShader(request);
snapshot.prepared->entries.push_back({codeOffset, handle});
return handle;
}
std::string layouts;
for (const auto& entry : snapshot.prepared->entries) {
if (entry.codeOffset != codeOffset || entry.handle == nullptr || entry.handle->artifact == nullptr) continue;
const auto& layout = entry.handle->artifact->layout;
layouts += " [" + std::to_string(layout.pushConstantOffsetBytes) + "," + std::to_string(layout.pushConstantSizeBytes) + "]";
}
throw std::runtime_error("AGC driver: prepared shader artifact is missing for the requested static ABI: address=" + std::to_string(request.shader.codeAddress) + " stage=" + std::to_string(static_cast<std::uint32_t>(request.shader.stage)) + " wave=" + std::to_string(request.context.waveSize) + " pushOffset=" + std::to_string(request.layout.pushConstantOffsetBytes) + " pushCapacity=" + std::to_string(request.layout.pushConstantSizeBytes) + " preparedLayouts=" + layouts);
}
ShaderRecompiler::RectListShaders PreparedRectangle(const ShaderSnapshot& snapshot, std::uint64_t vertexId, std::uint64_t fragmentId) {
std::lock_guard lock(snapshot.prepared->mutex);
for (const auto& entry : snapshot.prepared->rectangles) {
if (entry.vertexId == vertexId && entry.fragmentId == fragmentId) {
require(entry.shaders != nullptr, "prepared rectangle artifact is missing");
return *entry.shaders;
}
}
throw std::runtime_error("AGC driver: prepared rectangle artifacts are missing");
}
ShaderRecompiler::PreparedShaderInvocation InvocationFor(const ShaderSnapshot& snapshot, std::size_t codeOffset, const ShaderRecompiler::RecompileRequest& request) {
require(codeOffset < snapshot.code.size(), "prepared shader code offset is outside the snapshot");
const auto code = std::span(snapshot.code).subspan(codeOffset);
require(request.shader.code.data() == code.data() && request.shader.code.size() == code.size(), "prepared invocation does not refer to registered code");
auto invocationRequest = request;
struct PreparedKeyStorage {};
auto& key = HostThreadLocal<std::vector<std::uint64_t>, PreparedKeyStorage>();
ShaderRecompiler::BuildPreparedShaderKey(request, key);
std::lock_guard lock(snapshot.prepared->mutex);
for (const auto& entry : snapshot.prepared->entries) {
if (entry.codeOffset != codeOffset) continue;
invocationRequest.shader.code = ShaderRecompiler::GetPreparedCode(*entry.handle);
if (auto invocation = ShaderRecompiler::PreparedShaderInvocation::TryCreate(invocationRequest, entry.handle, key)) return std::move(*invocation);
}
if (snapshot.header.empty()) {
if (snapshot.type != 0 || request.shader.stage != ShaderRecompiler::ShaderStage::Compute) throw std::runtime_error("AGC driver: unregistered program is not a compute shader");
APS5_LOG_ERR("Compute shader 0x%llx was not registered; preparing its artifact at dispatch", static_cast<unsigned long long>(snapshot.codeAddress));
auto handle = ShaderRecompiler::PrepareShader(request);
invocationRequest = request;
invocationRequest.shader.code = ShaderRecompiler::GetPreparedCode(*handle);
auto invocation = ShaderRecompiler::PreparedShaderInvocation::TryCreate(invocationRequest, handle, key);
if (!invocation.has_value()) throw std::runtime_error("AGC driver: raw compute artifact does not match its invocation");
snapshot.prepared->entries.push_back({codeOffset, std::move(handle)});
return std::move(*invocation);
}
std::string layouts;
for (const auto& entry : snapshot.prepared->entries) {
if (entry.codeOffset != codeOffset || entry.handle == nullptr || entry.handle->artifact == nullptr) continue;
const auto& layout = entry.handle->artifact->layout;
layouts += " [" + std::to_string(layout.pushConstantOffsetBytes) + "," + std::to_string(layout.pushConstantSizeBytes) + "]";
}
throw std::runtime_error("AGC driver: prepared shader artifact is missing for the requested static ABI: address=" + std::to_string(request.shader.codeAddress) + " stage=" + std::to_string(static_cast<std::uint32_t>(request.shader.stage)) + " wave=" + std::to_string(request.context.waveSize) + " pushOffset=" + std::to_string(request.layout.pushConstantOffsetBytes) + " pushCapacity=" + std::to_string(request.layout.pushConstantSizeBytes) + " preparedLayouts=" + layouts);
}
namespace {
template<typename TValue>
std::vector<TValue> ReadHeaderArray(const ShaderSnapshot& snapshot, const TValue* pointer, std::size_t count) {
if (count == 0) return {};
const auto address = reinterpret_cast<std::uintptr_t>(pointer);
if (address < snapshot.headerAddress || address - snapshot.headerAddress > snapshot.header.size()) throw std::runtime_error("AGC driver: shader metadata is outside the registered header");
const auto offset = static_cast<std::size_t>(address - snapshot.headerAddress);
if (count > (snapshot.header.size() - offset) / sizeof(TValue)) throw std::runtime_error("AGC driver: truncated shader metadata");
std::vector<TValue> result(count);
std::memcpy(result.data(), snapshot.header.data() + offset, count * sizeof(TValue));
return result;
}
Shader ReadHeader(const ShaderSnapshot& snapshot) {
Shader header;
std::memcpy(&header, snapshot.header.data(), sizeof(header));
return header;
}
RegisteredShaderState DecodeRegisteredState(const ShaderSnapshot& snapshot) {
const auto header = ReadHeader(snapshot);
RegisteredShaderState state{{}, InitialContextRegisters(), {{0x24a, 0}, {0x24b, 0}}};
for (const auto reg : ReadHeaderArray(snapshot, header.sh_registers, header.num_sh_registers)) {
state.shader.insert_or_assign(reg.offset, reg.value);
}
for (const auto reg : ReadHeaderArray(snapshot, header.cx_registers, header.num_cx_registers)) {
state.context.insert_or_assign(reg.offset, reg.value);
}
if (header.specials != nullptr) {
const auto special = ReadHeaderArray(snapshot, header.specials, 1).front();
state.context[special.vgt_shader_stages_en.offset] = special.vgt_shader_stages_en.value;
state.context[special.vgt_gs_out_prim_type.offset] = special.vgt_gs_out_prim_type.value;
state.userConfig[special.ge_cntl.offset] = special.ge_cntl.value;
state.userConfig[special.ge_user_vgpr_en.offset] = special.ge_user_vgpr_en.value;
}
return state;
}
std::uint32_t RegisterValue(const Registers& registers, std::uint32_t offset) {
const auto found = registers.find(offset);
if (found == registers.end()) throw std::runtime_error("AGC driver: missing static shader ABI register " + std::to_string(offset));
return found->second;
}
void BuildRegisteredAbiKey(const QueueState& state, const VulkanDevice& device, std::vector<std::uint64_t>& key) {
key.clear();
key.reserve(7u + state.shader.size() + state.context.size() + state.userConfig.size());
key.insert(key.end(), {device.Serial(), ShaderRecompiler::DebugProbeActive(), ShaderRecompiler::RayTracingStrict(), ShaderRecompiler::RayTracingMiss()});
for (const auto* registers : {&state.shader, &state.context, &state.userConfig}) {
key.push_back(registers->size());
for (const auto& [offset, value] : *registers) key.push_back((static_cast<std::uint64_t>(offset) << 32u) | value);
}
}
std::vector<PreparedShaders::Entry> PrepareRegistered(const ShaderSnapshot& snapshot, const VulkanDevice& device, const QueueState& state, bool registration) {
using Stage = ShaderRecompiler::ShaderStage;
const auto header = ReadHeader(snapshot);
std::uint32_t programRegister;
std::uint32_t resourceRegister;
std::uint32_t firstUser = 0;
Stage stage;
switch (snapshot.type) {
case 0: stage = Stage::Compute; programRegister = 0x20c; resourceRegister = 0x213; break;
case 1: stage = Stage::Fragment; programRegister = 0x008; resourceRegister = 0x00b; break;
case 2: stage = Stage::Vertex; programRegister = 0x0c8; resourceRegister = 0x08b; firstUser = 8; break;
case 4: stage = Stage::Mesh; programRegister = 0x0c8; resourceRegister = 0x08b; break;
case 5: stage = Stage::Local; programRegister = 0x148; resourceRegister = 0x10b; firstUser = 8; break;
case 6: stage = Stage::Mesh; programRegister = 0x088; resourceRegister = 0x08b; break;
case 7: stage = Stage::TessellationControl; programRegister = 0x108; resourceRegister = 0x10b; break;
default: throw std::runtime_error("AGC driver: unsupported registered shader type");
}
const auto high = RegisterValue(state.shader, programRegister + 1);
if ((high & ~0xffu) != 0) throw std::runtime_error("AGC driver: invalid registered program address");
const auto address = (static_cast<std::uint64_t>(RegisterValue(state.shader, programRegister)) << 8u) | (static_cast<std::uint64_t>(high) << 40u);
if (address < snapshot.codeAddress || address - snapshot.codeAddress >= snapshot.code.size() * 4u) throw std::runtime_error("AGC driver: registered entry point is outside shader code");
const auto codeOffset = static_cast<std::size_t>((address - snapshot.codeAddress) / 4u);
const auto code = std::span(snapshot.code).subspan(codeOffset);
const auto decoded = ShaderRecompiler::RdnaInstructionDecoder{}.Decode(code);
std::optional<ShaderRecompiler::ShaderComputeStageInfo> compute;
std::optional<ShaderRecompiler::ShaderPixelStageInfo> pixel;
std::optional<ShaderRecompiler::ShaderVertexStageInfo> vertex;
std::optional<ShaderRecompiler::GraphicsCompileContext> graphics;
std::uint32_t wave;
const auto resources = RegisterValue(state.shader, resourceRegister);
auto userCount = (resources >> 1u) & 0x1fu;
if (stage != Stage::Compute) userCount |= ((resources >> 27u) & 1u) << 5u;
if (userCount > 32) throw std::runtime_error("AGC driver: static user SGPR count exceeds the register bank");
if (stage == Stage::Compute) {
compute = Graphics::DecodeComputeStageInfo(state.shader, snapshot.header);
const auto special = ReadHeaderArray(snapshot, header.specials, 1).front();
wave = (special.dispatch_modifier & 0x8000u) != 0 ? 32u : 64u;
} else if (stage == Stage::Fragment) {
const auto count = RegisterValue(state.context, 0x1b6) & 0x3fu;
if (registration && count != 0) return {};
pixel = Graphics::DecodePixelStageInfo(state.context, {});
wave = pixel->wave32 ? 32u : 64u;
} else {
const auto routing = RegisterValue(state.context, 0x2d5);
wave = (routing & 0x00400000u) != 0 ? 32u : 64u;
if ((routing & 0x20u) != 0 || stage == Stage::Mesh || (routing & 4u) != 0) {
if (registration) return {};
auto stageState = state;
stageState.context[0x1b6] = 0;
const auto stages = Graphics::DecodeShaderStages(stageState);
graphics = ShaderRecompiler::GraphicsCompileContext{0, {}, stages.mesh, stages.tessellation, {}};
if (stages.mesh) { stage = Stage::Mesh; firstUser = 0; }
if (stages.tessellation && snapshot.type == 2) stage = Stage::TessellationEvaluation;
}
if (firstUser == 0) userCount += 8;
}
std::vector<std::uint32_t> userData(userCount);
if (stage != Stage::Compute && stage != Stage::Fragment && snapshot.type != 6) vertex = Graphics::DecodeVertexStageInfo(snapshot.header, snapshot.headerAddress, userData, nullptr, true);
const ShaderRecompiler::SwappcInfo swappc{vertex.has_value(), firstUser, userCount};
auto graph = ShaderRecompiler::GraphBuilder{}.Build(decoded, &swappc);
ShaderRecompiler::Structurizer{}.Structurize(graph);
const std::array<ShaderRecompiler::MemoryRegion, 2> memory{{{snapshot.codeAddress, std::as_bytes(std::span(snapshot.code))}, {snapshot.headerAddress, snapshot.header}}};
ShaderRecompiler::RecompileRequest request{{stage, address, code, snapshot.headerAddress, snapshot.header}, {wave, firstUser, userData, compute, pixel, vertex, memory}, stage == Stage::Compute ? device.ComputeTarget(wave) : device.Target(), {0, 0, 0, 128}, graphics};
if (graphics && graphics->mesh) request.layout.pushConstantSizeBytes = ShaderRecompiler::MeshDrawPushOffsetBytes;
std::vector<PreparedShaders::Entry> entries;
const auto append = [&] {
PerformanceTimer timing("Shader.PrepareArtifact");
entries.push_back({codeOffset, ShaderRecompiler::PrepareShader(request)});
};
append();
if (compute) {
request.context.compute->partialThreads = {1, 1, 1};
append();
} else if (pixel) {
request.layout.pushConstantSizeBytes = 0;
append();
}
return entries;
}
}
std::vector<PreparedGraphicsStage> PrepareGraphicsStages(const DrawDecode& decoded, const ShaderRecompiler::SpirvTarget& target) {
ShaderPreparationTransaction transaction;
require(decoded.programs.size() == decoded.roles.size(), "graphics ABI program roles are incomplete");
for (const auto& program : decoded.programs) require(program.snapshot != nullptr, "graphics ABI has no registered shader snapshot");
std::vector<ShaderRecompiler::ProgramRole> expected;
if (decoded.state.stages.path == Graphics::ShaderPath::Tessellation) expected = {ShaderRecompiler::ProgramRole::Local, ShaderRecompiler::ProgramRole::Hull, ShaderRecompiler::ProgramRole::Domain};
else {
expected.push_back(ShaderRecompiler::ProgramRole::Main);
if (decoded.state.stages.path == Graphics::ShaderPath::Geometry && !decoded.programs.empty() && decoded.programs.front().snapshot->type == 4) expected.push_back(ShaderRecompiler::ProgramRole::GeometryBack);
}
if (!decoded.roles.empty() && decoded.roles.back() == ShaderRecompiler::ProgramRole::Fragment) expected.push_back(ShaderRecompiler::ProgramRole::Fragment);
require(expected == decoded.roles, "graphics ABI linked programs do not match the stage routing");
std::vector<ShaderRecompiler::LinkedProgram> linked;
std::vector<ShaderRecompiler::MemoryRegion> memory;
for (std::size_t index = 0; index < decoded.programs.size(); ++index) {
const auto& program = decoded.programs[index];
require(program.snapshot != nullptr && program.codeOffset < program.snapshot->code.size(), "graphics ABI has an invalid registered entry point");
const auto code = std::span(program.snapshot->code).subspan(program.codeOffset);
require(program.binary.code.data() == code.data() && program.binary.code.size() == code.size() && program.binary.codeAddress == program.snapshot->codeAddress + program.codeOffset * sizeof(std::uint32_t), "graphics ABI entry point differs from its snapshot");
linked.push_back({decoded.roles[index], program.binary, program.userDataBase, program.firstUserSgpr, program.userData});
memory.insert(memory.end(), program.memory.begin(), program.memory.end());
}
std::vector<PreparedGraphicsStage> prepared;
std::uint32_t pushOffset = 0;
const auto capacity = decoded.state.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : Graphics::PipelinePushConstantBytes;
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];
const bool fragment = program.binary.stage == ShaderRecompiler::ShaderStage::Fragment;
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);
ShaderRecompiler::RecompileRequest request{program.binary, {wave, program.firstUserSgpr, program.userData, {}, fragment ? std::optional(decoded.pixel) : std::nullopt, vertex, memory}, target, {0, 0, pushOffset, capacity - pushOffset}, ShaderRecompiler::GraphicsCompileContext{program.firstUserSgpr, linked, decoded.state.stages.mesh, decoded.state.stages.tessellation, {}}};
std::vector<std::uint64_t> key;
ShaderRecompiler::BuildPreparedShaderKey(request, key);
std::shared_ptr<const ShaderRecompiler::SourceHandle> handle;
for (const auto& entry : transaction.Read(*program.snapshot).entries) {
if (entry.codeOffset == program.codeOffset && ShaderRecompiler::MatchesPreparedShader(request, *entry.handle, key)) {
handle = entry.handle;
break;
}
}
if (handle == nullptr) {
PerformanceTimer timing("Shader.PrepareArtifact");
handle = ShaderRecompiler::PrepareShader(request);
}
const auto bytes = handle->artifact->bindings.pushConstantSizeBytes;
require(bytes <= capacity - pushOffset, "prepared graphics stages exceed the push constant block");
prepared.push_back({program.snapshot, {program.codeOffset, handle}});
pushOffset += bytes;
}
transaction.Commit();
return prepared;
}
void Driver::ResolveGraphicsStagesAbi(std::span<const Shader* const> stages, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive) {
PerformanceContext timingContext(FrameTiming::Preparation());
PerformanceTimer timing("Shader.ResolveGraphicsStagesAbi");
ShaderPreparationTransaction transaction;
CheckFailure();
require(!stages.empty(), "graphics ABI has no shader headers");
QueueState state{};
std::shared_ptr<const ShaderRegistry> registry;
std::shared_ptr<const ShaderSnapshot> owner;
{
std::lock_guard lock(memos.mutex);
for (const auto& entry : memos.entries) {
if (entry.key != key || (entry.handle == nullptr && entry.failure == nullptr)) continue;
if (entry.handle == nullptr) {
if (verify) {
bool resolved = true;
try {
static_cast<void>(ShaderRecompiler::ResolveSource(request));
} catch (const std::exception&) {
resolved = false;
}
if (resolved) throw std::runtime_error("AGC driver: source handle memo poisoned but the source resolved");
}
ShaderMemory::CountHandleMemo(true);
if (poisoned == nullptr) throw std::runtime_error(*entry.failure);
*poisoned = entry.failure.get();
return nullptr;
}
if (verify && ShaderRecompiler::ResolveSource(request)->source != entry.handle->source) throw std::runtime_error("AGC driver: source handle memo answered a different source");
ShaderMemory::CountHandleMemo(true);
return entry.handle;
std::lock_guard lock(mutex);
registry = shaders;
for (const auto* shader : stages) {
GuestMemory::CheckRange(shader, sizeof(Shader), alignof(Shader));
const auto address = reinterpret_cast<std::uintptr_t>(const_cast<const void*>(shader->code));
require(registry != nullptr && registry->contains(address), "graphics ABI refers to an unregistered shader");
if (owner == nullptr) owner = registry->at(address);
const auto& snapshot = *registry->at(address);
require(snapshot.headerAddress == reinterpret_cast<std::uintptr_t>(shader), "graphics ABI refers to a replaced shader header");
require(snapshot.registeredState != nullptr, "registered shader state is missing");
const auto& registered = *snapshot.registeredState;
for (const auto& [offset, value] : registered.shader) state.shader.insert_or_assign(offset, value);
for (const auto& [offset, value] : registered.context) state.context.insert_or_assign(offset, value);
for (const auto& [offset, value] : registered.userConfig) state.userConfig.insert_or_assign(offset, value);
}
}
std::shared_ptr<const ShaderRecompiler::SourceHandle> handle;
try {
handle = ShaderRecompiler::ResolveSource(request);
} catch (const std::exception& error) {
if (FailureMemo()) {
std::lock_guard lock(memos.mutex);
memos.entries[memos.next] = {key, nullptr, std::make_shared<const std::string>(error.what())};
memos.next = (memos.next + 1) % memos.entries.size();
memos.poisoned.fetch_add(1, std::memory_order_relaxed);
for (const auto reg : context) state.context.insert_or_assign(reg.offset, reg.value);
for (const auto reg : primitive) state.userConfig.insert_or_assign(reg.offset, reg.value);
const auto localDevice = device.Load();
require(localDevice != nullptr, "graphics ABI device is missing");
struct GraphicsAbiKeyStorage {};
auto& key = HostThreadLocal<std::vector<std::uint64_t>, GraphicsAbiKeyStorage>();
BuildRegisteredAbiKey(state, *localDevice, key);
const auto primitiveType = state.userConfig.find(0x242u);
for (const auto& abi : transaction.Read(*owner).graphicsAbis) {
if (abi.key != key || abi.registry.lock() != registry) continue;
for (const auto& stage : abi.stages) {
const auto snapshot = stage.lock();
require(snapshot != nullptr, "prepared graphics ABI lost a registered stage");
if (snapshot->type != 1) ResolvePreparedGraphics(*snapshot, {}, primitiveType == state.userConfig.end() ? 0u : primitiveType->second, localDevice->Target());
}
throw;
transaction.Commit();
return;
}
ShaderMemory::CountHandleMemo(false);
if (handle == nullptr) return nullptr;
std::lock_guard lock(memos.mutex);
memos.entries[memos.next] = {key, handle, nullptr};
memos.next = (memos.next + 1) % memos.entries.size();
return handle;
DrawDecode decoded{};
decoded.state.stages = Graphics::DecodeShaderStages(state);
DecodeGraphicsPrograms(decoded, state, *registry, true, false);
auto prepared = PrepareGraphicsStages(decoded, localDevice->Target());
for (auto& stage : prepared) {
const auto& entries = transaction.Read(*stage.snapshot).entries;
const auto duplicate = std::ranges::any_of(entries, [&](const auto& existing) { return existing.codeOffset == stage.entry.codeOffset && existing.handle->artifact == stage.entry.handle->artifact; });
if (!duplicate) transaction.Edit(*stage.snapshot).entries.push_back(std::move(stage.entry));
}
for (const auto& stage : prepared) {
if (stage.snapshot->type == 1) continue;
ResolvePreparedGraphics(*stage.snapshot, {}, primitiveType == state.userConfig.end() ? 0u : primitiveType->second, localDevice->Target());
}
PreparedShaderState::GraphicsAbi abi{key, registry, {}};
for (const auto& stage : prepared) abi.stages.push_back(stage.snapshot);
auto& graphicsAbis = transaction.Edit(*owner).graphicsAbis;
std::erase_if(graphicsAbis, [](const auto& entry) { return entry.registry.expired(); });
graphicsAbis.push_back(std::move(abi));
transaction.Commit();
}
void Driver::ResolveShaderAbi(const Shader* shader, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive) {
PerformanceContext timingContext(FrameTiming::Preparation());
PerformanceTimer timing("Shader.ResolveAbi");
ShaderPreparationTransaction transaction;
CheckFailure();
GuestMemory::CheckRange(shader, sizeof(Shader), alignof(Shader));
if (shader->type != 0 && shader->type != 1) {
const std::array<const Shader*, 1> stages{shader};
ResolveGraphicsStagesAbi(stages, context, primitive);
transaction.Commit();
return;
}
std::shared_ptr<const ShaderSnapshot> snapshot;
{
std::lock_guard lock(mutex);
const auto address = reinterpret_cast<std::uintptr_t>(const_cast<const void*>(shader->code));
require(shaders != nullptr && shaders->contains(address), "static ABI refers to an unregistered shader");
snapshot = shaders->at(address);
require(snapshot->headerAddress == reinterpret_cast<std::uintptr_t>(shader), "static ABI refers to a replaced shader header");
}
require(snapshot->registeredState != nullptr, "registered shader state is missing");
QueueState state{};
state.shader = snapshot->registeredState->shader;
state.context = snapshot->registeredState->context;
state.userConfig = snapshot->registeredState->userConfig;
for (const auto reg : context) state.context[reg.offset] = reg.value;
for (const auto reg : primitive) state.userConfig[reg.offset] = reg.value;
const auto localDevice = device.Load();
require(localDevice != nullptr, "shader registration device is missing");
struct RegisteredAbiKeyStorage {};
auto& key = HostThreadLocal<std::vector<std::uint64_t>, RegisteredAbiKeyStorage>();
BuildRegisteredAbiKey(state, *localDevice, key);
const auto& registeredAbis = transaction.Read(*snapshot).registeredAbis;
if (std::ranges::find(registeredAbis, key) != registeredAbis.end()) {
transaction.Commit();
return;
}
auto entries = PrepareRegistered(*snapshot, *localDevice, state, false);
auto& prepared = transaction.Edit(*snapshot);
for (auto& entry : entries) {
const auto duplicate = std::ranges::any_of(prepared.entries, [&](const auto& existing) { return existing.handle->artifact == entry.handle->artifact; });
if (!duplicate) prepared.entries.push_back(std::move(entry));
}
prepared.registeredAbis.push_back(key);
transaction.Commit();
}
alignas(256) static const std::uint32_t NullPixelCode[64] = {0xbf810000u};
@ -161,7 +555,97 @@ std::uint64_t NullPixelProgramAddress() {
return reinterpret_cast<std::uintptr_t>(NullPixelCode);
}
void Driver::ResolveGraphicsAbi(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType) {
PerformanceContext timingContext(FrameTiming::Preparation());
PerformanceTimer timing("Shader.ResolveGraphicsAbi");
CheckFailure();
if (primitiveType != 0 && primitiveType != 7 && primitiveType != 17) return;
ShaderPreparationTransaction transaction;
require(vertex != nullptr && pixel != nullptr, "rectangle ABI requires vertex and fragment shaders");
std::shared_ptr<const ShaderSnapshot> front;
std::shared_ptr<const ShaderSnapshot> fragment;
{
std::lock_guard lock(mutex);
GuestMemory::CheckRange(vertex, sizeof(Shader), alignof(Shader));
GuestMemory::CheckRange(pixel, sizeof(Shader), alignof(Shader));
const auto lookup = [&](const Shader* shader) {
const auto address = reinterpret_cast<std::uintptr_t>(const_cast<const void*>(shader->code));
require(shaders != nullptr && shaders->contains(address), "rectangle ABI refers to an unregistered shader");
const auto snapshot = shaders->at(address);
require(snapshot->headerAddress == reinterpret_cast<std::uintptr_t>(shader), "rectangle ABI refers to a replaced shader header");
return snapshot;
};
front = lookup(vertex);
fragment = lookup(pixel);
}
require(front != fragment, "rectangle stages refer to the same shader");
const auto localDevice = device.Load();
require(localDevice != nullptr, "shader registration device is missing");
ResolvePreparedGraphics(*front, fragment, primitiveType, localDevice->Target());
transaction.Commit();
}
void ResolvePreparedGraphics(const ShaderSnapshot& front, const std::shared_ptr<const ShaderSnapshot>& fragment, std::uint32_t primitiveType, const ShaderRecompiler::SpirvTarget& target) {
ShaderPreparationTransaction transaction;
require(fragment.get() != &front, "rectangle stages refer to the same shader");
std::vector<std::shared_ptr<const ShaderSnapshot>> fragments;
{
const auto& current = transaction.Read(front);
const bool newFragment = fragment && !std::ranges::any_of(current.fragments, [&](const auto& entry) { return entry.lock() == fragment; });
const bool newRequest = !current.rectangleRequested && (primitiveType == 7 || primitiveType == 17);
const bool expiredFragment = std::ranges::any_of(current.fragments, [](const auto& entry) { return entry.expired(); });
if (newFragment || newRequest || expiredFragment) {
auto& prepared = transaction.Edit(front);
std::erase_if(prepared.fragments, [](const auto& entry) { return entry.expired(); });
std::erase_if(prepared.rectangleProgress, [](const auto& entry) { return entry.fragment.expired(); });
if (newFragment) prepared.fragments.push_back(fragment);
prepared.rectangleRequested |= newRequest;
}
const auto& prepared = transaction.Read(front);
if (!prepared.rectangleRequested) {
transaction.Commit();
return;
}
for (const auto& entry : prepared.fragments) {
if (auto snapshot = entry.lock()) fragments.push_back(std::move(snapshot));
}
}
for (const auto& pixel : fragments) {
const auto& current = transaction.Read(front);
const auto& pixelPrepared = transaction.Read(*pixel);
require(!current.entries.empty() && !pixelPrepared.entries.empty(), "rectangle ABI has missing stage artifacts");
const auto progress = std::ranges::find_if(current.rectangleProgress, [&](const auto& entry) { return entry.fragment.lock() == pixel; });
const auto vertexCount = progress == current.rectangleProgress.end() ? 0u : progress->vertexCount;
const auto fragmentCount = progress == current.rectangleProgress.end() ? 0u : progress->fragmentCount;
if (vertexCount == current.entries.size() && fragmentCount == pixelPrepared.entries.size()) continue;
auto& prepared = transaction.Edit(front);
std::vector<PreparedShaders::Rectangle> rectangles;
for (std::size_t vertexIndex = 0; vertexIndex < prepared.entries.size(); ++vertexIndex) {
const auto& vertexEntry = prepared.entries[vertexIndex];
for (std::size_t fragmentIndex = vertexIndex < vertexCount ? fragmentCount : 0u; fragmentIndex < pixelPrepared.entries.size(); ++fragmentIndex) {
const auto& fragmentEntry = pixelPrepared.entries[fragmentIndex];
const auto& vertexArtifact = ShaderRecompiler::GetPreparedArtifact(*vertexEntry.handle);
const auto& fragmentArtifact = ShaderRecompiler::GetPreparedArtifact(*fragmentEntry.handle);
const auto exists = [&](const auto& entry) { return entry.vertexId == vertexArtifact.variantId && entry.fragmentId == fragmentArtifact.variantId; };
if (std::ranges::any_of(prepared.rectangles, exists) || std::ranges::any_of(rectangles, exists)) continue;
ShaderRecompiler::RecompileResult vertexResult;
ShaderRecompiler::RecompileResult fragmentResult;
static_cast<ShaderRecompiler::CompiledShaderArtifact&>(vertexResult) = vertexArtifact;
static_cast<ShaderRecompiler::CompiledShaderArtifact&>(fragmentResult) = fragmentArtifact;
rectangles.push_back({vertexArtifact.variantId, fragmentArtifact.variantId, std::make_shared<const ShaderRecompiler::RectListShaders>(ShaderRecompiler::BuildRectListShaders(vertexResult, fragmentResult, target))});
}
}
prepared.rectangles.insert(prepared.rectangles.end(), std::make_move_iterator(rectangles.begin()), std::make_move_iterator(rectangles.end()));
std::erase_if(prepared.rectangleProgress, [&](const auto& entry) { return entry.fragment.expired() || entry.fragment.lock() == pixel; });
prepared.rectangleProgress.push_back({pixel, prepared.entries.size(), pixelPrepared.entries.size()});
}
transaction.Commit();
}
void Driver::RegisterShader(const Shader* shader) {
PerformanceContext timingContext(FrameTiming::Preparation());
PerformanceTimer timing("Shader.Register");
ShaderPreparationTransaction transaction;
CheckFailure();
GuestMemory::CheckRange(shader, sizeof(Shader), 1);
Shader fields;
@ -178,6 +662,38 @@ void Driver::RegisterShader(const Shader* shader) {
snapshot.header.resize(fields.header_size);
std::memcpy(snapshot.header.data(), static_cast<const void*>(shader), fields.header_size);
{
std::lock_guard lock(mutex);
rethrowFailure();
if (shaders != nullptr) {
const auto found = shaders->find(snapshot.codeAddress);
if (found != shaders->end()) {
const auto& current = *found->second;
if (current.headerAddress == snapshot.headerAddress && current.type == snapshot.type && current.code == snapshot.code && current.header == snapshot.header) {
transaction.Commit();
return;
}
}
}
}
std::shared_ptr<VulkanDevice> localDevice = device.Load();
if (localDevice == nullptr) {
std::lock_guard gpuLock(GuestMemory::GpuMutex());
if (device == nullptr) device = std::make_shared<VulkanDevice>();
localDevice = device;
}
snapshot.registeredState = std::make_shared<const RegisteredShaderState>(DecodeRegisteredState(snapshot));
QueueState registered{};
registered.shader = snapshot.registeredState->shader;
registered.context = snapshot.registeredState->context;
registered.userConfig = snapshot.registeredState->userConfig;
snapshot.prepared->entries = PrepareRegistered(snapshot, *localDevice, registered, true);
if ((snapshot.type == 0 || snapshot.type == 1) && !snapshot.prepared->entries.empty()) {
std::vector<std::uint64_t> key;
BuildRegisteredAbiKey(registered, *localDevice, key);
snapshot.prepared->registeredAbis.push_back(std::move(key));
}
static const char* traceRegs = std::getenv("APS5_TRACE_SHADER_REGS");
if (traceRegs != nullptr && (std::string(traceRegs) == "all" || std::strtoull(traceRegs, nullptr, 16) == snapshot.codeAddress)) {
const auto print = [](const ShaderRegister* registers, std::uint32_t count) {
@ -195,18 +711,39 @@ void Driver::RegisterShader(const Shader* shader) {
}
std::lock_guard lock(mutex);
rethrowFailure();
const auto address = snapshot.codeAddress;
if (shaders == nullptr) shaders = std::make_shared<ShaderRegistry>();
else if (shaders.use_count() != 1) shaders = std::make_shared<ShaderRegistry>(*shaders);
shaders->insert_or_assign(address, std::make_shared<const ShaderSnapshot>(std::move(snapshot)));
PublishRegisteredShader(shaders, std::make_shared<const ShaderSnapshot>(std::move(snapshot)));
if (shaders->find(NullPixelProgramAddress()) == shaders->end()) {
ShaderSnapshot null{NullPixelProgramAddress(), reinterpret_cast<std::uintptr_t>(&NullPixelShader), NullPixelShader.type, {}, {}};
null.code.assign(std::begin(NullPixelCode), std::end(NullPixelCode));
null.header.resize(sizeof(Shader));
std::memcpy(null.header.data(), &NullPixelShader, sizeof(Shader));
shaders->insert_or_assign(NullPixelProgramAddress(), std::make_shared<const ShaderSnapshot>(std::move(null)));
auto nullRegisteredState = DecodeRegisteredState(null);
nullRegisteredState.shader.emplace(0x008u, static_cast<std::uint32_t>(null.codeAddress >> 8u));
nullRegisteredState.shader.emplace(0x009u, static_cast<std::uint32_t>(null.codeAddress >> 40u));
nullRegisteredState.shader.emplace(0x00bu, 0u);
nullRegisteredState.context.emplace(0x1b3u, 0x2u);
nullRegisteredState.context.emplace(0x1b4u, 0x2u);
null.registeredState = std::make_shared<const RegisteredShaderState>(std::move(nullRegisteredState));
QueueState nullState{};
nullState.shader = null.registeredState->shader;
nullState.context = null.registeredState->context;
nullState.userConfig = null.registeredState->userConfig;
null.prepared->entries = PrepareRegistered(null, *localDevice, nullState, true);
PublishRegisteredShader(shaders, std::make_shared<const ShaderSnapshot>(std::move(null)));
}
transaction.Commit();
}
}
extern "C" AgcDriver::DriverDetail::ShaderPreparationTransaction* AgcDriverBeginShaderPreparation_nid_postfix() {
return new AgcDriver::DriverDetail::ShaderPreparationTransaction();
}
extern "C" void AgcDriverCommitShaderPreparation_nid_postfix(AgcDriver::DriverDetail::ShaderPreparationTransaction* transaction) {
transaction->Commit();
}
extern "C" void AgcDriverEndShaderPreparation_nid_postfix(AgcDriver::DriverDetail::ShaderPreparationTransaction* transaction) noexcept {
delete transaction;
}

View File

@ -5,6 +5,8 @@
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
#include <cstdlib>
#include <mutex>
#include <set>
#ifdef _WIN32
#include <windows.h>
#endif
@ -287,10 +289,14 @@ void Driver::waitMemory(std::span<const std::uint32_t> packet, std::uint32_t que
if (!warned && std::chrono::steady_clock::now() - start > std::chrono::milliseconds(waitTimeoutMs())) {
warned = true;
++outcomes.timedOut;
static std::set<std::uint64_t> reported;
static std::uint64_t timeouts = 0;
if (++timeouts % 20 == 0) std::fprintf(stderr, "[gpu] %llu GPU waits have timed out\n", static_cast<unsigned long long>(timeouts));
if (!reported.insert(packet[2] | (static_cast<std::uint64_t>(packet[3]) << 32u)).second) return;
{
static std::mutex reportedMutex;
static std::set<std::uint64_t> reported;
static std::uint64_t timeouts = 0;
const std::lock_guard lock(reportedMutex);
if (++timeouts % 20 == 0) std::fprintf(stderr, "[gpu] %llu GPU waits have timed out\n", static_cast<unsigned long long>(timeouts));
if (!reported.insert(packet[2] | (static_cast<std::uint64_t>(packet[3]) << 32u)).second) return;
}
const bool wide = ((packet[0] >> 8u) & 0xffu) == 0x93u;
const std::uint64_t reference = wide ? packet[4] | (static_cast<std::uint64_t>(packet[5]) << 32u) : packet[4];
const std::uint64_t mask = wide ? packet[6] | (static_cast<std::uint64_t>(packet[7]) << 32u) : packet[5];

View File

@ -1,5 +1,6 @@
#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/WriteWatchCoverage.hpp"
#include "ThreadOwned.hpp"
#include "prx/libc/include/GuestAllocations.hpp"
#include "prx/libc/include/GuestArena.hpp"
@ -26,9 +27,43 @@
#endif
#include <windows.h>
#else
#include <cerrno>
#include <cstdint>
#include <fcntl.h>
#include <fstream>
#include <pthread.h>
#include <sstream>
#include <sys/ioctl.h>
#include <unistd.h>
#if __has_include(<linux/fs.h>)
#include <linux/fs.h>
#endif
#if !defined(PROCMAP_QUERY)
struct procmap_query {
std::uint64_t size;
std::uint64_t query_flags;
std::uint64_t query_addr;
std::uint64_t vma_start;
std::uint64_t vma_end;
std::uint64_t vma_flags;
std::uint64_t vma_page_size;
std::uint64_t vma_offset;
std::uint64_t inode;
std::uint32_t dev_major;
std::uint32_t dev_minor;
std::uint32_t vma_name_size;
std::uint32_t build_id_size;
std::uint64_t vma_name_addr;
std::uint64_t build_id_addr;
};
static_assert(sizeof(procmap_query) == 104);
enum : std::uint64_t {
PROCMAP_QUERY_VMA_READABLE = 0x01,
PROCMAP_QUERY_VMA_WRITABLE = 0x02,
PROCMAP_QUERY_COVERING_OR_NEXT_VMA = 0x10,
};
#define PROCMAP_QUERY _IOWR('f', 17, struct procmap_query)
#endif
#endif
namespace AgcDriver::GuestMemory {
@ -478,6 +513,23 @@ struct PageRun {
bool writable;
};
#if !defined(_WIN32)
int ProcMapsQueryFd() {
static const int fd = [] {
if (std::getenv("APS5_NO_PROCMAP_QUERY") != nullptr) return -1;
const int opened = open("/proc/self/maps", O_RDONLY | O_CLOEXEC);
if (opened < 0) return -1;
procmap_query probe{};
probe.size = sizeof(probe);
probe.query_flags = PROCMAP_QUERY_COVERING_OR_NEXT_VMA;
if (ioctl(opened, PROCMAP_QUERY, &probe) == 0 || errno == ENOENT) return opened;
close(opened);
return -1;
}();
return fd;
}
#endif
// Calls `emit` with consecutive runs of uniform accessibility covering [address, address + bytes) in
// order, stopping early when it returns false. Returns false when the address space cannot be queried.
template <class Emit>
@ -535,6 +587,31 @@ bool describePages(std::uintptr_t address, std::size_t bytes, Emit&& emit) {
if (!emit(PageRun{cursor, next, readable, writable})) return true;
cursor = next;
#else
if (const int fd = ProcMapsQueryFd(); fd >= 0) {
procmap_query query{};
query.size = sizeof(query);
query.query_flags = PROCMAP_QUERY_COVERING_OR_NEXT_VMA;
query.query_addr = cursor;
if (ioctl(fd, PROCMAP_QUERY, &query) == 0) {
if (query.vma_end <= cursor || query.vma_start >= query.vma_end) return false;
if (query.vma_start > cursor) {
const auto gapEnd = std::min<std::uintptr_t>(end, query.vma_start);
if (!emit(PageRun{cursor, gapEnd, false, false})) return true;
cursor = gapEnd;
continue;
}
const bool readable = (query.vma_flags & PROCMAP_QUERY_VMA_READABLE) != 0;
const auto next = std::min<std::uintptr_t>(end, query.vma_end);
if (!emit(PageRun{cursor, next, readable, readable && (query.vma_flags & PROCMAP_QUERY_VMA_WRITABLE) != 0})) return true;
cursor = next;
continue;
}
if (errno == ENOENT) {
static_cast<void>(emit(PageRun{cursor, end, false, false}));
return true;
}
return false;
}
std::ifstream maps("/proc/self/maps");
if (!maps.is_open()) return false;
std::string line;
@ -667,9 +744,15 @@ std::vector<std::pair<std::uint64_t, std::uint64_t>> CommittedRanges(std::uint64
namespace {
constexpr std::size_t WriteBlockBytes = 65536;
constexpr std::size_t WritePageBytes = 4096;
constexpr std::size_t WritePagesPerBlock = WriteBlockBytes / WritePageBytes;
enum class StampKind : std::uint8_t { Cpu, Driver, ImportWindow };
#ifdef _WIN32
void watchPrivateMapping(std::uintptr_t address, std::size_t bytes, std::uint64_t generation);
#endif
struct WriteTracker {
std::mutex mutex;
bool initialized = false;
@ -684,6 +767,7 @@ struct WriteTracker {
// records in the same block (the title's per-job slots are 0x20 apart) must not count.
std::vector<std::uint32_t> cpuBlocks;
std::vector<std::uint32_t> writtenBlocks;
WriteWatchCoverage coverage;
#else
static constexpr std::size_t LeafBlocks = std::size_t{1} << 16;
static constexpr std::size_t LeafCount = std::size_t{1} << 15;
@ -727,7 +811,9 @@ struct WriteTracker {
blocks.assign(size / WriteBlockBytes + 1, 0);
cpuBlocks.assign(size / WriteBlockBytes + 1, 0);
writtenBlocks.assign(size / WriteBlockBytes + 1, 0);
coverage.Initialize(base, size);
pages.resize(1u << 16);
GuestArena::GuestArenaSetPrivateMappingObserver_nid_postfix(&watchPrivateMapping);
#else
watched = GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix();
if (watched) leaves.resize(LeafCount);
@ -736,7 +822,7 @@ struct WriteTracker {
bool covers(std::uint64_t address, std::size_t bytes) const {
#ifdef _WIN32
return address >= base && address - base <= size - bytes;
return coverage.Covers(address, bytes);
#else
return address + bytes <= LeafCount * LeafBlocks * WriteBlockBytes && GuestWriteWatch::GuestWriteWatchCovers_nid_postfix(static_cast<std::uintptr_t>(address), bytes);
#endif
@ -771,6 +857,24 @@ struct WriteTracker {
};
std::unordered_map<std::uint64_t, DriverPieces> driverPieces;
struct CpuPages {
std::array<std::uint32_t, WritePagesPerBlock> pages{};
};
std::unordered_map<std::uint64_t, CpuPages> cpuPages;
void noteCpuStore(std::uint64_t begin, std::uint64_t end, std::uint32_t stampGeneration) {
for (auto block = blockOf(begin); block <= blockOf(end - 1); ++block) {
const auto blockStart = blockBegin(block);
const auto from = std::max(begin, blockStart) - blockStart;
const auto to = std::min<std::uint64_t>(end, blockStart + WriteBlockBytes) - blockStart;
auto& entry = cpuPages[block];
for (auto page = from / WritePageBytes; page <= (to - 1) / WritePageBytes; ++page) {
auto& stamp = entry.pages[page];
stamp = std::max(stamp, stampGeneration);
}
}
}
void noteDriverStore(std::uint64_t block, std::uint64_t address, std::uint64_t end, std::uint32_t stampGeneration) {
const auto begin = blockBegin(block);
const auto from = static_cast<std::uint32_t>(std::max(address, begin) - begin);
@ -851,6 +955,21 @@ std::unique_lock<std::mutex> lockTracker(WriteTracker& tracker) {
return lock;
}
#ifdef _WIN32
void watchPrivateMapping(std::uintptr_t address, std::size_t bytes, std::uint64_t generation) {
auto& tracker = Tracker();
const auto lock = lockTracker(tracker);
if (!tracker.watched || bytes == 0 || bytes > tracker.size || address < tracker.base || address - tracker.base > tracker.size - bytes) return;
tracker.coverage.Restore(address, bytes, generation);
++tracker.generation;
for (auto block = tracker.blockOf(address); block <= tracker.blockOf(address + bytes - 1); ++block) {
tracker.driverPieces.erase(block);
tracker.stamp(block, tracker.generation, StampKind::Cpu);
}
unwatchSerial.fetch_add(1, std::memory_order_release);
}
#endif
// Collect epochs are per thread and globally unique: every bump takes a fresh value from one counter,
// so a memo entry (stamped with the epoch of its walk) can only match the thread that made it, and
// only until that thread's next ordering point (the queue workers bump at theirs, see the header). A
@ -894,6 +1013,7 @@ void stampWrittenRun(void* context, std::uintptr_t begin, std::uintptr_t end) {
auto& [tracker, kind] = *static_cast<StampRuns*>(context);
if (end <= begin) return;
const auto generation = tracker.generation.load(std::memory_order_relaxed);
if (kind != StampKind::Driver) tracker.noteCpuStore(begin, end, generation);
for (auto block = tracker.blockOf(begin); block <= tracker.blockOf(end - 1); ++block) tracker.stamp(block, generation, kind);
collectDirtyRuns.fetch_add(1, std::memory_order_relaxed);
}
@ -918,7 +1038,11 @@ bool walkWrites(WriteTracker& tracker, std::uint64_t first, std::uint64_t stop,
return false;
}
if (count != 0) dirty = true;
for (ULONG_PTR i = 0; i < count; ++i) tracker.stamp(tracker.blockOf(reinterpret_cast<std::uintptr_t>(tracker.pages[i])), tracker.generation, kind);
for (ULONG_PTR i = 0; i < count; ++i) {
const auto page = reinterpret_cast<std::uintptr_t>(tracker.pages[i]);
tracker.stamp(tracker.blockOf(page), tracker.generation, kind);
if (kind != StampKind::Driver) tracker.noteCpuStore(page, page + WritePageBytes, tracker.generation);
}
if (count < tracker.pages.size()) break;
cursor = reinterpret_cast<std::uintptr_t>(tracker.pages[count - 1]) + (granularity != 0 ? granularity : page);
}
@ -930,7 +1054,11 @@ bool walkWrites(WriteTracker& tracker, std::uint64_t first, std::uint64_t stop,
std::size_t count = tracker.pages.size();
DWORD granularity = 4096;
if (!GuestArena::GuestArenaCollectWrites_nid_postfix(cursor, static_cast<std::size_t>(stop - cursor), tracker.pages.data(), &count, true)) return false;
for (ULONG_PTR i = 0; i < count; ++i) tracker.stamp(tracker.blockOf(reinterpret_cast<std::uintptr_t>(tracker.pages[i])), tracker.generation, kind);
for (ULONG_PTR i = 0; i < count; ++i) {
const auto page = reinterpret_cast<std::uintptr_t>(tracker.pages[i]);
tracker.stamp(tracker.blockOf(page), tracker.generation, kind);
if (kind != StampKind::Driver) tracker.noteCpuStore(page, page + WritePageBytes, tracker.generation);
}
if (count < tracker.pages.size()) break;
cursor = reinterpret_cast<std::uintptr_t>(tracker.pages[count - 1]) + (granularity != 0 ? granularity : page);
}
@ -1013,11 +1141,10 @@ std::uint64_t CollectEpochBumps() {
namespace {
void unwatchLocked(const WriteTracker& tracker, std::uint64_t address, std::size_t bytes) {
void unwatchLocked(WriteTracker& tracker, std::uint64_t address, std::size_t bytes) {
if (!tracker.watched) return;
#ifdef _WIN32
static_cast<void>(address);
static_cast<void>(bytes);
if (tracker.coverage.Exclude(address, bytes, GuestArena::GuestArenaCommitGeneration_nid_postfix())) unwatchSerial.fetch_add(1, std::memory_order_release);
#else
if (GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(reinterpret_cast<const void*>(address), bytes)) unwatchSerial.fetch_add(1, std::memory_order_release);
#endif
@ -1154,7 +1281,16 @@ bool StoredOver(std::uint64_t address, std::size_t bytes, std::uint64_t generati
const auto last = tracker.blockOf(end - 1);
for (auto block = first; block <= last; ++block) {
if (tracker.writtenStampOf(block) <= generation) continue;
if (tracker.cpuStampOf(block) > generation) return true;
if (tracker.cpuStampOf(block) > generation) {
const auto begin = tracker.blockBegin(block);
const auto from = std::max(address, begin) - begin;
const auto to = std::min<std::uint64_t>(end, begin + WriteBlockBytes) - begin;
const auto found = tracker.cpuPages.find(block);
if (found == tracker.cpuPages.end()) return true;
for (auto page = from / WritePageBytes; page <= (to - 1) / WritePageBytes; ++page) {
if (found->second.pages[page] > generation) return true;
}
}
const auto found = tracker.driverPieces.find(block);
if (found == tracker.driverPieces.end()) return true;
const auto& entry = found->second;

View File

@ -10,7 +10,21 @@
#include <stdexcept>
#include <string>
extern "C" void* APS5_VABI mmap_nid_postfix(void* address, std::size_t length, int protection, int flags, int descriptor, std::int64_t offset) noexcept;
namespace AgcDriver::Pm4 {
std::uint64_t GdsAddress() {
static const std::uint64_t address = [] {
constexpr int ReadWrite = 0x3;
constexpr int PrivateAnonymous = 0x1002;
void* mapped = mmap_nid_postfix(nullptr, GdsBytes, ReadWrite, PrivateAnonymous, -1, 0);
if (mapped == nullptr || mapped == reinterpret_cast<void*>(static_cast<std::uintptr_t>(-1))) throw std::runtime_error("cannot allocate the global data share");
return reinterpret_cast<std::uint64_t>(mapped);
}();
return address;
}
namespace {
std::string ToHex(std::uint32_t value) {
@ -99,17 +113,6 @@ std::vector<std::byte> copySource(std::uint64_t source, std::size_t bytes, bool
}
constexpr std::uint32_t DmaSelectGds = 1;
constexpr std::size_t GdsBytes = 0x10000;
struct GdsStorage {
std::mutex mutex;
std::array<std::byte, GdsBytes> bytes{};
};
GdsStorage& Gds() {
static GdsStorage storage;
return storage;
}
bool gdsRange(std::uint64_t offset, std::size_t bytes) {
return offset <= GdsBytes && bytes <= GdsBytes - offset;
@ -118,9 +121,7 @@ bool gdsRange(std::uint64_t offset, std::size_t bytes) {
std::vector<std::byte> dmaSourceBytes(std::span<const std::uint32_t> packet) {
const std::size_t bytes = packet[6] & 0x3ffffffu;
if (dmaSource(packet) != DmaSelectGds) return copySource(address(packet[2], packet[3]), bytes, dmaSource(packet) == 2);
auto& gds = Gds();
std::lock_guard lock(gds.mutex);
return {gds.bytes.begin() + packet[2], gds.bytes.begin() + packet[2] + bytes};
return copySource(GdsAddress() + packet[2], bytes, false);
}
void copyMemory(std::uint64_t source, std::uint64_t destination, std::size_t bytes, bool immediate) {
@ -129,6 +130,22 @@ void copyMemory(std::uint64_t source, std::uint64_t destination, std::size_t byt
GuestMemory::Write(destination, copySource(source, bytes, immediate));
}
constexpr std::uint32_t CopyDataGpuClockSource = 18;
constexpr std::uint32_t CopyDataCachePolicy = (3u << 13u) | (3u << 25u);
std::uint64_t gpuClockCount() {
return static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch()).count() / 10);
}
std::vector<std::byte> copyDataSource(std::span<const std::uint32_t> packet, std::size_t bytes) {
const auto source = ((packet[1] & 0xfu) << 1u) | ((packet[1] >> 30u) & 1u);
if (source != CopyDataGpuClockSource) return copySource(address(packet[2], packet[3]), bytes, source >= 10);
const auto value = gpuClockCount();
std::vector<std::byte> data(bytes);
for (std::size_t i = 0; i < bytes; ++i) data[i] = static_cast<std::byte>(value >> (i * 8));
return data;
}
}
std::string Name(std::uint32_t header) {
@ -416,12 +433,12 @@ void Validate(std::span<const std::uint32_t> packet, std::uint32_t queue) {
}
case 0x40: {
size(6);
require((packet[1] & ~0x40110f0fu) == 0, "COPY_DATA engine, cache or reserved fields are not implemented");
require((packet[1] & ~(0x40110f0fu | CopyDataCachePolicy)) == 0, "COPY_DATA engine or reserved fields are not implemented");
const auto source = ((packet[1] & 0xfu) << 1u) | ((packet[1] >> 30u) & 1u);
const auto destination = ((packet[1] >> 8u) & 0xfu) << 1u;
require(destination == 2 || destination == 4, "COPY_DATA register or GDS destination is not implemented");
require(source == 2 || source == 4 || source == 5 || source == 10 || source == 11, "COPY_DATA register, GDS or reference-clock source is not implemented");
require(source < 10 || ((packet[1] & 0x10000u) == 0 && packet[3] == 0), "64-bit immediate COPY_DATA is not implemented");
require(source == 2 || source == 4 || source == 5 || source == 10 || source == 11 || source == CopyDataGpuClockSource, "COPY_DATA register, GDS or reference-clock source is not implemented");
require(source < 10 || source == CopyDataGpuClockSource || ((packet[1] & 0x10000u) == 0 && packet[3] == 0), "64-bit immediate COPY_DATA is not implemented");
break;
}
case 0x50:
@ -549,7 +566,7 @@ std::optional<StoreWrite> ResolveStore(std::span<const std::uint32_t> packet, co
const std::size_t bytes = (packet[1] & 0x10000u) != 0 ? 8 : 4;
const auto destination = address(packet[4], packet[5]);
if (!fits(destination, bytes)) return std::nullopt;
return StoreWrite{destination, {}, copySource(address(packet[2], packet[3]), bytes, source >= 10)};
return StoreWrite{destination, {}, copyDataSource(packet, bytes)};
}
case 0x50: {
if (packet.size() < 7 || dmaDestination(packet) == DmaSelectGds) return std::nullopt;
@ -715,6 +732,28 @@ DrawParameters ResolveDraw(std::span<const std::uint32_t> packet, const QueueSta
return {address, count, indexSize, queue.instanceCount, packet.back(), true, indexOffset->second, 0};
}
std::vector<std::uint32_t> ReadIndirectRegisters(std::span<const std::uint32_t> packet) {
require(packet.size() == 5 && IndirectRegisterOpcode((packet[0] >> 8u) & 0xffu), "expected indirect register packet");
std::vector<std::uint32_t> pairs(static_cast<std::size_t>(packet[4]) * 2);
// Named for the [hooksync] attribution (the read goes through the flush hook).
const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::Registers);
GuestMemory::Read(address(packet[1], packet[2]), std::as_writable_bytes(std::span(pairs)), 4);
return pairs;
}
void ExecuteIndirectRegisters(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> pairs, QueueState& queue) {
const auto opcode = (packet[0] >> 8u) & 0xffu;
require(packet.size() == 5 && IndirectRegisterOpcode(opcode), "expected indirect register packet");
require(pairs.size() == static_cast<std::size_t>(packet[4]) * 2, "indirect register list does not match its packet");
for (std::size_t i = 0; i < pairs.size(); i += 2) registerOffset(pairs[i]);
for (std::size_t i = 0; i < pairs.size(); i += 2) writeRegister(queue, opcode, registerOffset(pairs[i]), pairs[i + 1]);
if (queue.savedContext.has_value() && TraceContextState()) {
std::fprintf(stderr, "[context] indirect 0x%x:", opcode);
for (std::size_t i = 0; i < pairs.size(); i += 2) std::fprintf(stderr, " %x", registerOffset(pairs[i]));
std::fprintf(stderr, "\n");
}
}
void Execute(std::span<const std::uint32_t> packet, QueueState& queue) {
const auto opcode = (packet[0] >> 8u) & 0xffu;
switch (opcode) {
@ -763,20 +802,9 @@ void Execute(std::span<const std::uint32_t> packet, QueueState& queue) {
case 0x26: queue.indexBase = address(packet[1], packet[2]); return;
case 0x2a: queue.indexType = packet[1]; return;
case 0x2f: queue.instanceCount = packet[1]; return;
case 0x63: case 0x64: case 0x9f: {
std::vector<std::uint32_t> pairs(static_cast<std::size_t>(packet[4]) * 2);
// Named for the [hooksync] attribution (the read goes through the flush hook).
const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::Registers);
GuestMemory::Read(address(packet[1], packet[2]), std::as_writable_bytes(std::span(pairs)), 4);
for (std::size_t i = 0; i < pairs.size(); i += 2) registerOffset(pairs[i]);
for (std::size_t i = 0; i < pairs.size(); i += 2) writeRegister(queue, opcode, registerOffset(pairs[i]), pairs[i + 1]);
if (queue.savedContext.has_value() && TraceContextState()) {
std::fprintf(stderr, "[context] indirect 0x%x:", opcode);
for (std::size_t i = 0; i < pairs.size(); i += 2) std::fprintf(stderr, " %x", registerOffset(pairs[i]));
std::fprintf(stderr, "\n");
}
case 0x63: case 0x64: case 0x9f:
ExecuteIndirectRegisters(packet, ReadIndirectRegisters(packet), queue);
return;
}
case 0x59: break;
case 0x3c: case 0x93: {
// The waited-on value is written by the CPU or another queue; poll it like the CP would.
@ -834,21 +862,17 @@ void Execute(std::span<const std::uint32_t> packet, QueueState& queue) {
return;
}
case 0x40: {
const auto source = ((packet[1] & 0xfu) << 1u) | ((packet[1] >> 30u) & 1u);
copyMemory(address(packet[2], packet[3]), address(packet[4], packet[5]), (packet[1] & 0x10000u) != 0 ? 8 : 4, source >= 10);
const auto destination = address(packet[4], packet[5]);
const auto data = copyDataSource(packet, (packet[1] & 0x10000u) != 0 ? 8 : 4);
GuestMemory::CheckRange(reinterpret_cast<void*>(destination), data.size(), 1, true);
GuestMemory::Write(destination, data);
return;
}
case 0x50: {
const std::size_t bytes = packet[6] & 0x3ffffffu;
if (bytes == 0) return;
auto data = dmaSourceBytes(packet);
if (dmaDestination(packet) == DmaSelectGds) {
auto& gds = Gds();
std::lock_guard lock(gds.mutex);
std::copy(data.begin(), data.end(), gds.bytes.begin() + packet[4]);
return;
}
const auto destination = address(packet[4], packet[5]);
const auto destination = dmaDestination(packet) == DmaSelectGds ? GdsAddress() + packet[4] : address(packet[4], packet[5]);
GuestMemory::CheckRange(reinterpret_cast<void*>(destination), bytes, 1, true);
GuestMemory::Write(destination, data);
return;

View File

@ -0,0 +1,80 @@
#include "prx/libSceAgcDriver/Execution/include/ShaderDeviceProfile.hpp"
#include "BdaAbi.hpp"
#include <spirv/unified1/spirv.hpp>
#include <algorithm>
namespace AgcDriver {
namespace {
template<typename TFeatures>
const TFeatures* findFeatures(const void* chain, VkStructureType type) {
auto* current = static_cast<const VkBaseInStructure*>(chain);
while (current != nullptr) {
if (current->sType == type) return reinterpret_cast<const TFeatures*>(current);
current = current->pNext;
}
return nullptr;
}
}
ShaderDeviceProfile::ShaderDeviceProfile(const ShaderRecompiler::SpirvTarget& target, const VkDeviceCreateInfo& deviceInfo, const VkPhysicalDeviceLimits& limits) : target(target), limits(limits), capabilities(target.supportedCapabilities.begin(), target.supportedCapabilities.end()) {
using Graphics::Require;
Require(deviceInfo.pEnabledFeatures != nullptr, "shader device profile requires enabled core features");
const auto& core = *deviceInfo.pEnabledFeatures;
const auto* robustness = findFeatures<VkPhysicalDeviceRobustness2FeaturesEXT>(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT);
nullDescriptors = robustness != nullptr && robustness->nullDescriptor == VK_TRUE;
const auto* bda = findFeatures<VkPhysicalDeviceBufferDeviceAddressFeatures>(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_BUFFER_DEVICE_ADDRESS_FEATURES);
const auto* bytes = findFeatures<VkPhysicalDevice8BitStorageFeatures>(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_8BIT_STORAGE_FEATURES);
const auto* indexing = findFeatures<VkPhysicalDeviceDescriptorIndexingFeatures>(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES);
const auto* barycentric = findFeatures<VkPhysicalDeviceFragmentShaderBarycentricFeaturesKHR>(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_BARYCENTRIC_FEATURES_KHR);
const auto* mesh = findFeatures<VkPhysicalDeviceMeshShaderFeaturesEXT>(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_FEATURES_EXT);
const auto* clock = findFeatures<VkPhysicalDeviceShaderClockFeaturesKHR>(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_CLOCK_FEATURES_KHR);
const auto* maintenance8 = findFeatures<VkPhysicalDeviceMaintenance8FeaturesKHR>(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_8_FEATURES_KHR);
Require(target.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version, "shader device profile has an incompatible BDA ABI");
Require(bda != nullptr && bda->bufferDeviceAddress == VK_TRUE && core.shaderInt64 == VK_TRUE, "shader runtime requires enabled bufferDeviceAddress and shaderInt64");
Require(bytes != nullptr && bytes->storageBuffer8BitAccess == VK_TRUE, "shader runtime requires enabled storageBuffer8BitAccess");
Require(nullDescriptors, "shader runtime requires enabled nullDescriptor");
Require(core.vertexPipelineStoresAndAtomics == VK_TRUE && core.fragmentStoresAndAtomics == VK_TRUE, "shader runtime requires enabled graphics stores and atomics");
Require(limits.maxPushConstantsSize >= ShaderRecompiler::RuntimeAbi::PushConstantDwords * sizeof(std::uint32_t) && limits.maxBoundDescriptorSets > ShaderRecompiler::RuntimeAbi::DescriptorSet, "shader runtime ABI exceeds device limits");
Require(limits.maxStorageBufferRange >= sizeof(ShaderRecompiler::RuntimeAbi::ShaderData), "shader runtime ShaderData exceeds device limits");
std::ranges::sort(capabilities);
capabilities.erase(std::unique(capabilities.begin(), capabilities.end()), capabilities.end());
const auto hasCapability = [&](spv::Capability capability) { return std::ranges::binary_search(capabilities, static_cast<std::uint32_t>(capability)); };
Require(hasCapability(spv::CapabilityShader) && hasCapability(spv::CapabilityInt64) && hasCapability(spv::CapabilityPhysicalStorageBufferAddresses) && hasCapability(spv::CapabilityStorageBuffer8BitAccess), "shader device profile is missing runtime capabilities");
const auto checkFeature = [&](spv::Capability capability, bool enabled, const char* name) {
Require(!hasCapability(capability) || enabled, std::string("shader device profile advertises a disabled feature: ") + name);
};
checkFeature(spv::CapabilityFloat64, core.shaderFloat64 == VK_TRUE, "shaderFloat64");
checkFeature(spv::CapabilityTessellation, core.tessellationShader == VK_TRUE, "tessellationShader");
checkFeature(spv::CapabilityImageGatherExtended, core.shaderImageGatherExtended == VK_TRUE, "shaderImageGatherExtended");
checkFeature(spv::CapabilityMinLod, core.shaderResourceMinLod == VK_TRUE, "shaderResourceMinLod");
checkFeature(spv::CapabilityStorageImageMultisample, core.shaderStorageImageMultisample == VK_TRUE, "shaderStorageImageMultisample");
checkFeature(spv::CapabilityStorageImageReadWithoutFormat, core.shaderStorageImageReadWithoutFormat == VK_TRUE, "shaderStorageImageReadWithoutFormat");
checkFeature(spv::CapabilityStorageImageWriteWithoutFormat, core.shaderStorageImageWriteWithoutFormat == VK_TRUE, "shaderStorageImageWriteWithoutFormat");
checkFeature(spv::CapabilitySampledImageArrayDynamicIndexing, core.shaderSampledImageArrayDynamicIndexing == VK_TRUE, "shaderSampledImageArrayDynamicIndexing");
checkFeature(spv::CapabilityStorageImageArrayDynamicIndexing, core.shaderStorageImageArrayDynamicIndexing == VK_TRUE, "shaderStorageImageArrayDynamicIndexing");
const bool sampledNonUniform = indexing != nullptr && indexing->shaderSampledImageArrayNonUniformIndexing == VK_TRUE;
const bool storageNonUniform = indexing != nullptr && indexing->shaderStorageImageArrayNonUniformIndexing == VK_TRUE;
checkFeature(spv::CapabilitySampledImageArrayNonUniformIndexing, sampledNonUniform, "shaderSampledImageArrayNonUniformIndexing");
checkFeature(spv::CapabilityStorageImageArrayNonUniformIndexing, storageNonUniform, "shaderStorageImageArrayNonUniformIndexing");
checkFeature(spv::CapabilityShaderNonUniform, sampledNonUniform || storageNonUniform, "descriptor indexing");
const bool barycentricEnabled = barycentric != nullptr && barycentric->fragmentShaderBarycentric == VK_TRUE;
const bool meshEnabled = mesh != nullptr && mesh->meshShader == VK_TRUE;
checkFeature(spv::CapabilityFragmentBarycentricKHR, barycentricEnabled, "fragmentShaderBarycentric");
checkFeature(spv::CapabilityMeshShadingEXT, meshEnabled, "meshShader");
checkFeature(spv::CapabilityShaderClockKHR, clock != nullptr && clock->shaderSubgroupClock == VK_TRUE && clock->shaderDeviceClock == VK_TRUE, "shader clock");
Require(!target.fragmentShaderBarycentricEnabled || barycentricEnabled, "shader device profile exposes disabled barycentrics");
Require(!target.mesh || meshEnabled, "shader device profile exposes disabled mesh shaders");
Require(!target.nonConstantImageOffsets || (maintenance8 != nullptr && maintenance8->maintenance8 == VK_TRUE), "shader device profile exposes disabled maintenance8");
Require(!target.tessellation || core.tessellationShader == VK_TRUE, "shader device profile exposes disabled tessellation");
for (const auto extension : target.supportedExtensions) extensions.emplace_back(extension);
std::ranges::sort(extensions);
extensions.erase(std::unique(extensions.begin(), extensions.end()), extensions.end());
for (const auto& extension : extensions) extensionViews.emplace_back(extension);
Require(std::ranges::find(extensionViews, "SPV_KHR_physical_storage_buffer") != extensionViews.end() && std::ranges::find(extensionViews, "SPV_KHR_8bit_storage") != extensionViews.end(), "shader device profile is missing runtime extensions");
this->target.supportedCapabilities = capabilities;
this->target.supportedExtensions = extensionViews;
}
}

View File

@ -212,6 +212,14 @@ void ShaderMemory::CountHandleMemo(bool hit) {
}
std::shared_ptr<const ShaderRecompiler::ResourceCapture> ShaderMemory::Capture(const ShaderRecompiler::RecompileRequest& request, const ShaderRecompiler::SourceHandle* handle) {
return capture(request, handle, nullptr);
}
std::shared_ptr<const ShaderRecompiler::ResourceCapture> ShaderMemory::Capture(const ShaderRecompiler::PreparedShaderInvocation& invocation) {
return capture(invocation.Request(), nullptr, &invocation);
}
std::shared_ptr<const ShaderRecompiler::ResourceCapture> ShaderMemory::capture(const ShaderRecompiler::RecompileRequest& request, const ShaderRecompiler::SourceHandle* handle, const ShaderRecompiler::PreparedShaderInvocation* invocation) {
// The capture's word and page reads (through `read`) are attributed to it ([hooksync], [guestmem]).
const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::Capture);
const bool profile = CaptureProfiled();
@ -225,7 +233,7 @@ std::shared_ptr<const ShaderRecompiler::ResourceCapture> ShaderMemory::Capture(c
runtime.userContext = this;
runtime.readMemory = &read;
runtime.readSpecializationMemory = &read;
auto capture = handle != nullptr ? ShaderRecompiler::CaptureResources(request, runtime, *handle) : ShaderRecompiler::CaptureResources(request, runtime);
auto capture = invocation != nullptr ? invocation->Capture(runtime) : handle != nullptr ? ShaderRecompiler::CaptureResources(request, runtime, *handle) : ShaderRecompiler::CaptureResources(request, runtime);
if (profile) {
totals.captureNanoseconds += NanosecondsSince(started);
totals.resolveNanoseconds += capture->sourceNanoseconds;

View File

@ -0,0 +1,27 @@
#include "prx/libSceAgcDriver/Execution/include/SubgroupClock.hpp"
#include <algorithm>
#include <array>
namespace AgcDriver {
namespace {
constexpr std::array<std::string_view, 19> NarrowClockFamilies{
"NAVI21", "NAVI22", "VANGOGH", "NAVI23", "NAVI24", "REMBRANDT", "RAPHAEL_MENDOCINO",
"NAVI31", "NAVI32", "NAVI33", "PHOENIX", "PHOENIX2",
"STRIX1", "STRIX_HALO", "KRACKAN1", "GFX1153", "GFX1156",
"GFX1170", "GFX1171",
};
}
bool NarrowSubgroupClock(VkDriverId driver, std::string_view deviceName) {
if (driver != VK_DRIVER_ID_MESA_RADV) return false;
constexpr std::string_view prefix = "(RADV ";
const auto start = deviceName.rfind(prefix);
if (start == std::string_view::npos || deviceName.empty() || deviceName.back() != ')') return false;
const auto family = deviceName.substr(start + prefix.size(), deviceName.size() - start - prefix.size() - 1u);
return std::find(NarrowClockFamilies.begin(), NarrowClockFamilies.end(), family) != NarrowClockFamilies.end();
}
}

View File

@ -1,10 +1,13 @@
#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp"
#include "prx/libSceAgcDriver/Graphics/include/PipelineSpecialization.hpp"
#include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp"
#include "BdaAbi.hpp"
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp"
#include "prx/libSceAgcDriver/Execution/include/SubgroupClock.hpp"
#include "prx/libSceAgcDriver/Execution/include/ShaderDeviceProfile.hpp"
#include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
@ -187,12 +190,14 @@ struct VulkanDevice::State {
VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES};
std::vector<std::uint32_t> capabilities{1};
std::vector<std::string_view> spirvExtensions;
std::unique_ptr<const ShaderDeviceProfile> shaderProfile;
bool tessellationShader = false;
bool meshShader = false;
bool fragmentShaderBarycentric = false;
bool geometryShader = false;
bool sampleRateShading = false;
bool shaderClock = false;
bool narrowSubgroupClock = 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;
@ -208,13 +213,17 @@ struct VulkanDevice::State {
bool depthBiasClamp = false;
bool occlusionQueryPrecise = false;
VkDeviceSize hostImportAlignment = 0;
bool dmaBufImport = false;
bool depthRangeUnrestricted = false;
bool samplerAnisotropy = false;
bool textureCompressionBC = false;
bool samplerFilterMinmax = false;
bool fragmentShaderPixelInterlock = false;
bool conservativeRasterization = false;
// VK_KHR_timeline_semaphore enabled: the recorder's unlocked waits are available.
bool timelineSemaphores = false;
bool computeWave32 = false;
std::uint32_t maxComputeSubgroupSize = 0;
// Indirect draw features enabled (see Graphics::Context).
bool drawIndirectFirstInstance = false;
bool multiDrawIndirect = false;
@ -523,6 +532,7 @@ struct VulkanDevice::State {
resourceCache.Clear();
Graphics::ClearCachedTextures(device);
Graphics::ClearImageMirrors(device);
Graphics::ClearHostImports(device);
patternBuffers.clear();
descriptorCache.reset();
emptyBuffer.reset();
@ -713,6 +723,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
state->capabilities.push_back(spv::CapabilityGroupNonUniform);
if ((state->subgroup.supportedOperations & VK_SUBGROUP_FEATURE_BALLOT_BIT) != 0) state->capabilities.push_back(spv::CapabilityGroupNonUniformBallot);
if ((state->subgroup.supportedOperations & VK_SUBGROUP_FEATURE_SHUFFLE_BIT) != 0) state->capabilities.push_back(spv::CapabilityGroupNonUniformShuffle);
if ((state->subgroup.supportedOperations & VK_SUBGROUP_FEATURE_ARITHMETIC_BIT) != 0) state->capabilities.push_back(spv::CapabilityGroupNonUniformArithmetic);
}
APS5_LOG_OUT("Selected GPU name=%s vendor=0x%x device=0x%x subgroup=%u", state->properties.deviceName, state->properties.vendorID, state->properties.deviceID, state->subgroup.subgroupSize);
state->InstanceFunction<PFN_vkGetPhysicalDeviceMemoryProperties>("vkGetPhysicalDeviceMemoryProperties")(selected, &state->memoryProperties);
@ -749,12 +760,27 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2")(selected, &features);
state->fragmentShaderBarycentric = barycentricFeatures.fragmentShaderBarycentric == VK_TRUE;
}
VkPhysicalDeviceFragmentShaderInterlockFeaturesEXT interlockFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_INTERLOCK_FEATURES_EXT};
if (hasExtension(VK_EXT_FRAGMENT_SHADER_INTERLOCK_EXTENSION_NAME)) {
VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &interlockFeatures};
state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2")(selected, &features);
state->fragmentShaderPixelInterlock = interlockFeatures.fragmentShaderPixelInterlock == VK_TRUE;
}
interlockFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_INTERLOCK_FEATURES_EXT};
interlockFeatures.fragmentShaderPixelInterlock = VK_TRUE;
VkPhysicalDeviceShaderClockFeaturesKHR clockFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_CLOCK_FEATURES_KHR};
if (hasExtension(VK_KHR_SHADER_CLOCK_EXTENSION_NAME)) {
VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &clockFeatures};
state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2")(selected, &features);
state->shaderClock = clockFeatures.shaderSubgroupClock == VK_TRUE && clockFeatures.shaderDeviceClock == VK_TRUE;
}
if (state->shaderClock && hasExtension(VK_KHR_DRIVER_PROPERTIES_EXTENSION_NAME)) {
VkPhysicalDeviceDriverProperties driver{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES};
VkPhysicalDeviceProperties2 driverProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &driver};
state->InstanceFunction<PFN_vkGetPhysicalDeviceProperties2>("vkGetPhysicalDeviceProperties2")(selected, &driverProperties);
state->narrowSubgroupClock = NarrowSubgroupClock(driver.driverID, state->properties.deviceName);
APS5_LOG_OUT("Shader clock driver=%d narrowSubgroupClock=%d", static_cast<int>(driver.driverID), state->narrowSubgroupClock ? 1 : 0);
}
std::vector<const char*> deviceExtensions;
if (window != nullptr) deviceExtensions.assign(presentationExtensions.begin(), presentationExtensions.end());
if (state->fragmentShaderBarycentric) {
@ -762,6 +788,11 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
state->capabilities.push_back(spv::CapabilityFragmentBarycentricKHR);
state->spirvExtensions.push_back("SPV_KHR_fragment_shader_barycentric");
}
if (state->fragmentShaderPixelInterlock) {
deviceExtensions.push_back(VK_EXT_FRAGMENT_SHADER_INTERLOCK_EXTENSION_NAME);
state->capabilities.push_back(spv::CapabilityFragmentShaderPixelInterlockEXT);
state->spirvExtensions.push_back("SPV_EXT_fragment_shader_interlock");
}
if (state->shaderClock) {
deviceExtensions.push_back(VK_KHR_SHADER_CLOCK_EXTENSION_NAME);
state->capabilities.push_back(spv::CapabilityShaderClockKHR);
@ -773,6 +804,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2")(selected, &features);
}
const bool bufferInt64Atomics = atomicInt64Features.shaderBufferInt64Atomics == VK_TRUE;
if (bufferInt64Atomics) state->capabilities.push_back(spv::CapabilityInt64Atomics);
atomicInt64Features = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_ATOMIC_INT64_FEATURES_KHR};
atomicInt64Features.shaderBufferInt64Atomics = VK_TRUE;
if (bufferInt64Atomics) deviceExtensions.push_back(VK_KHR_SHADER_ATOMIC_INT64_EXTENSION_NAME);
@ -813,6 +845,13 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
state->samplerFilterMinmax = minmaxProperties.filterMinmaxSingleComponentFormats == VK_TRUE && minmaxProperties.filterMinmaxImageComponentMapping == VK_TRUE;
if (state->samplerFilterMinmax) deviceExtensions.push_back(VK_EXT_SAMPLER_FILTER_MINMAX_EXTENSION_NAME);
}
if (hasExtension(VK_EXT_CONSERVATIVE_RASTERIZATION_EXTENSION_NAME)) {
VkPhysicalDeviceConservativeRasterizationPropertiesEXT conservativeProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_CONSERVATIVE_RASTERIZATION_PROPERTIES_EXT};
VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &conservativeProperties};
state->InstanceFunction<PFN_vkGetPhysicalDeviceProperties2>("vkGetPhysicalDeviceProperties2")(selected, &properties);
state->conservativeRasterization = conservativeProperties.primitiveOverestimationSize <= 1.0f / 256.0f && conservativeProperties.degenerateTrianglesRasterized == VK_TRUE;
if (state->conservativeRasterization) deviceExtensions.push_back(VK_EXT_CONSERVATIVE_RASTERIZATION_EXTENSION_NAME);
}
// Indirect draws with a GPU-side count (DRAW_INDIRECT_MULTI with count_indirect); a device
// without it resolves such draws on the CPU.
state->drawIndirectCount = hasExtension(VK_KHR_DRAW_INDIRECT_COUNT_EXTENSION_NAME);
@ -820,7 +859,8 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
// Guest memory is host memory: importing it lets address-based shaders use it in place instead of
// copying every registered allocation per draw.
if (hasExtension(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME) && std::getenv("APS5_NO_HOST_IMPORT") == nullptr) {
VkPhysicalDeviceExternalMemoryHostPropertiesEXT hostProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_MEMORY_HOST_PROPERTIES_EXT};
VkPhysicalDeviceDriverProperties driverProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES};
VkPhysicalDeviceExternalMemoryHostPropertiesEXT hostProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_MEMORY_HOST_PROPERTIES_EXT, &driverProperties};
VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &hostProperties};
state->InstanceFunction<PFN_vkGetPhysicalDeviceProperties2>("vkGetPhysicalDeviceProperties2")(selected, &properties);
state->hostImportAlignment = hostProperties.minImportedHostPointerAlignment;
@ -832,6 +872,13 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
}
#endif
deviceExtensions.push_back(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME);
#ifndef _WIN32
state->dmaBufImport = driverProperties.driverID != VK_DRIVER_ID_NVIDIA_PROPRIETARY && hasExtension(VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME) && hasExtension(VK_EXT_EXTERNAL_MEMORY_DMA_BUF_EXTENSION_NAME);
if (state->dmaBufImport) {
deviceExtensions.push_back(VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME);
deviceExtensions.push_back(VK_EXT_EXTERNAL_MEMORY_DMA_BUF_EXTENSION_NAME);
}
#endif
}
VkPhysicalDevicePrimitiveTopologyListRestartFeaturesEXT listRestartFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRIMITIVE_TOPOLOGY_LIST_RESTART_FEATURES_EXT};
if (hasExtension(VK_EXT_PRIMITIVE_TOPOLOGY_LIST_RESTART_EXTENSION_NAME)) {
@ -914,6 +961,9 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
enabled.occlusionQueryPrecise = available.occlusionQueryPrecise;
state->occlusionQueryPrecise = enabled.occlusionQueryPrecise == VK_TRUE;
// Recompiled storage-image access declares no format (the guest descriptor decides it).
enabled.shaderStorageImageMultisample = available.shaderStorageImageMultisample;
if (enabled.shaderStorageImageMultisample) state->capabilities.push_back(spv::CapabilityStorageImageMultisample);
state->capabilities.push_back(spv::CapabilityImageMSArray);
enabled.shaderStorageImageWriteWithoutFormat = available.shaderStorageImageWriteWithoutFormat;
enabled.shaderStorageImageReadWithoutFormat = available.shaderStorageImageReadWithoutFormat;
// Gathers with non-constant offsets (ImageGatherExtended).
@ -980,6 +1030,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
barycentricFeatures.pNext = byteFeatures.pNext;
byteFeatures.pNext = &barycentricFeatures;
}
if (state->fragmentShaderPixelInterlock) {
interlockFeatures.pNext = byteFeatures.pNext;
byteFeatures.pNext = &interlockFeatures;
}
if (state->shaderClock) {
clockFeatures.pNext = byteFeatures.pNext;
byteFeatures.pNext = &clockFeatures;
@ -1011,7 +1065,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
state->timelineSemaphores = timelineFeatures.timelineSemaphore == VK_TRUE;
}
VkPhysicalDeviceSubgroupSizeControlFeaturesEXT subgroupSizeFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_SIZE_CONTROL_FEATURES_EXT};
if (hasExtension(VK_EXT_SUBGROUP_SIZE_CONTROL_EXTENSION_NAME) && state->subgroup.subgroupSize > 32u) {
if (hasExtension(VK_EXT_SUBGROUP_SIZE_CONTROL_EXTENSION_NAME) && state->subgroup.subgroupSize >= 32u) {
VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &subgroupSizeFeatures};
state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2")(selected, &features);
VkPhysicalDeviceSubgroupSizeControlPropertiesEXT subgroupSize{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_SIZE_CONTROL_PROPERTIES_EXT};
@ -1019,6 +1073,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
state->InstanceFunction<PFN_vkGetPhysicalDeviceProperties2>("vkGetPhysicalDeviceProperties2")(selected, &sizeProperties);
state->computeWave32 = subgroupSizeFeatures.subgroupSizeControl == VK_TRUE && subgroupSize.minSubgroupSize <= 32u && subgroupSize.maxSubgroupSize >= 32u &&
(subgroupSize.requiredSubgroupSizeStages & VK_SHADER_STAGE_COMPUTE_BIT) != 0;
state->maxComputeSubgroupSize = subgroupSize.maxSubgroupSize;
}
if (state->computeWave32) {
subgroupSizeFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_SIZE_CONTROL_FEATURES_EXT};
@ -1056,14 +1111,33 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
byteFeatures.pNext = &pipelineFeatures;
}
bdaFeatures.pNext = &byteFeatures;
VkPhysicalDeviceRobustness2FeaturesEXT robustness2{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT};
Graphics::Require(hasExtension(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME), "shader runtime requires VK_EXT_robustness2");
VkPhysicalDeviceFeatures2 queried{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &robustness2};
state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2")(selected, &queried);
Graphics::Require(robustness2.nullDescriptor == VK_TRUE, "shader runtime requires nullDescriptor");
robustness2 = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT};
robustness2.nullDescriptor = VK_TRUE;
robustness2.pNext = bdaFeatures.pNext;
bdaFeatures.pNext = &robustness2;
deviceExtensions.push_back(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME);
deviceInfo.enabledExtensionCount = static_cast<std::uint32_t>(deviceExtensions.size());
deviceInfo.ppEnabledExtensionNames = deviceExtensions.data();
deviceInfo.pNext = &bdaFeatures;
auto shaderProfile = std::make_unique<const ShaderDeviceProfile>(buildTarget(), deviceInfo, state->properties.limits);
check(state->InstanceFunction<PFN_vkCreateDevice>("vkCreateDevice")(selected, &deviceInfo, nullptr, &state->device), "vkCreateDevice");
state->shaderProfile = std::move(shaderProfile);
state->DeviceFunction<PFN_vkGetDeviceQueue>("vkGetDeviceQueue")(state->device, family, 0, &state->queue);
APS5_LOG_OUT("Vulkan device ready device=%p queue=%p family=%u", reinterpret_cast<void*>(state->device), reinterpret_cast<void*>(state->queue), family);
VkCommandPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
poolInfo.queueFamilyIndex = family;
check(state->DeviceFunction<PFN_vkCreateCommandPool>("vkCreateCommandPool")(state->device, &poolInfo, nullptr, &state->pool), "vkCreateCommandPool");
static const bool procTable = std::getenv("APS5_NO_PROC_TABLE") == nullptr;
if (procTable) {
Graphics::FillDeviceFunctions(graphicsContext(), state->deviceFunctions);
state->functionsReady = true;
}
Graphics::PrepareImportWatch(graphicsContext());
state->bufferPool = std::make_shared<Graphics::BufferPool>(graphicsContext());
state->emptyBuffer = std::make_unique<Graphics::Buffer>(graphicsContext(), Graphics::EmptyBufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
@ -1075,14 +1149,6 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
state->samplerCache = std::make_unique<Graphics::SamplerCache>();
state->recorder = std::make_unique<Graphics::Recorder>(graphicsContext(), state->timelineSemaphores);
state->recorder->Activate();
// The entry points every record site uses, resolved once (APS5_NO_PROC_TABLE=1: per call, as
// before), and the context copy graphicsContext() hands out from here on: built after the
// recorder and the descriptor cache exist, so it carries them.
static const bool procTable = std::getenv("APS5_NO_PROC_TABLE") == nullptr;
if (procTable) {
Graphics::FillDeviceFunctions(graphicsContext(), state->deviceFunctions);
state->functionsReady = true;
}
state->context = buildContext();
state->contextReady = true;
if (window != nullptr) {
@ -1498,7 +1564,8 @@ bool VulkanDevice::FillBuffer(std::uint64_t address, std::size_t bytes, std::spa
recorder.EndGpuTiming(timing, bytes);
recorder.MarkCovered(filledAccess);
phase(recordMs);
recorder.NotePendingWrite(address, bytes);
if (uniform && pattern[0] == (pattern[0] & 0xffu) * 0x01010101u) recorder.NotePendingFill(address, bytes, static_cast<std::uint8_t>(pattern[0]));
else recorder.NotePendingWrite(address, bytes);
GuestMemory::MarkWritten(address, bytes);
phase(notesMs);
return true;
@ -1905,37 +1972,30 @@ std::shared_ptr<Graphics::StorageTexture> PresentableResident(const Graphics::Co
return resident;
}
Graphics::DccKeys DisplayKeys(const DisplayBuffer& buffer, std::size_t bytes) {
const auto keys = Graphics::CurrentDccKeys(buffer.dccAddress, bytes);
if (keys != Graphics::DccKeys::Uncompressed && !Graphics::IsDccClear(keys)) {
char message[256];
std::snprintf(message, sizeof(message), "VideoOut: display buffer 0x%llx reads %s DCC keys at 0x%llx: presenting DCC metadata that is not uniformly uncompressed or fast-cleared is not implemented", static_cast<unsigned long long>(buffer.address), Graphics::DccKeysName(keys), static_cast<unsigned long long>(buffer.dccAddress));
throw std::runtime_error(message);
}
return keys;
Graphics::DccKeys ResidentKeys(const Graphics::StorageTexture& resident) {
const auto& own = resident.Descriptor();
GuestMemory::CollectWritesUncached(own.dccAddress, Graphics::DccKeyBytes(resident.GuestBytes()));
return Graphics::ProvedClearKeys(own, resident.GuestBytes(), resident.KeyProof());
}
bool ResidentServesDisplay(const Graphics::StorageTexture& resident, const DisplayBuffer& buffer, std::size_t bytes) {
const auto keys = DisplayKeys(buffer, bytes);
bool ResidentServesDisplay(const Graphics::StorageTexture& resident, const DisplayBuffer& buffer) {
const auto keys = DisplayBufferKeys(buffer);
if (keys == Graphics::DccKeys::ClearRegister) {
char message[320];
std::snprintf(message, sizeof(message), "VideoOut: display buffer 0x%llx reads register-clear DCC keys at 0x%llx over the pending image 0x%llx (DCC 0x%llx, filled keys %s): whether its results precede the clear is not modeled", static_cast<unsigned long long>(buffer.address), static_cast<unsigned long long>(buffer.dccAddress), static_cast<unsigned long long>(resident.Descriptor().baseAddress), static_cast<unsigned long long>(resident.Descriptor().dccAddress), Graphics::DccKeysName(resident.FilledKeys()));
throw std::runtime_error(message);
}
if (Graphics::IsDccClear(resident.FilledKeys()) && resident.FilledKeys() == keys) return false;
return Graphics::StorageImageServesKeys(resident, buffer.dccAddress);
return Graphics::KeysServeSurface(resident.Descriptor().dccAddress, resident.UploadedKeys(), resident.FilledKeys(), buffer.dccAddress, [&] { return ResidentKeys(resident); }, [&] { return keys; });
}
bool ResidentKeysMoved(const Graphics::StorageTexture& resident) {
const auto& own = resident.Descriptor();
if (own.dccAddress == 0) return false;
GuestMemory::CollectWritesUncached(own.dccAddress, Graphics::DccKeyBytes(resident.GuestBytes()));
return Graphics::ProvedClearKeys(own, resident.GuestBytes(), resident.KeyProof()) != resident.UploadedKeys();
return resident.Descriptor().dccAddress != 0 && ResidentKeys(resident) != resident.UploadedKeys();
}
std::optional<std::array<std::byte, 4>> CompressedClearPixel(const DisplayBuffer& buffer, std::size_t bytes) {
GuestMemory::FlushGpuWrites(buffer.address, bytes);
const auto keys = DisplayKeys(buffer, bytes);
const auto keys = DisplayBufferKeys(buffer);
if (keys == Graphics::DccKeys::Uncompressed) return std::nullopt;
return DisplayBufferClearPixel(buffer, keys);
}
@ -1989,7 +2049,7 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) {
if (!NoResidentPresent()) {
bool pending = false;
resident = PresentableResident(graphicsContext(), buffer, filter, pending, convert);
if (resident != nullptr && buffer.dccAddress != 0 && !ResidentServesDisplay(*resident, buffer, bytes)) {
if (resident != nullptr && buffer.dccAddress != 0 && !ResidentServesDisplay(*resident, buffer)) {
resident.reset();
convert = false;
}
@ -2389,10 +2449,16 @@ std::string VulkanDevice::DeviceName() const {
}
ShaderRecompiler::SpirvTarget VulkanDevice::Target() const {
require(state->shaderProfile != nullptr, "shader device profile is unavailable");
return state->shaderProfile->Target();
}
ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const {
const auto& limits = state->properties.limits;
ShaderRecompiler::SpirvTarget target{VK_API_VERSION_1_1, state->meshShader ? 0x00010400u : 0x00010300u, state->subgroup.subgroupSize, ShaderRecompiler::BdaAbi::Version, state->capabilities, state->spirvExtensions, false, {limits.maxComputeWorkGroupSize[0], limits.maxComputeWorkGroupSize[1], limits.maxComputeWorkGroupSize[2]}, limits.maxComputeWorkGroupInvocations, limits.maxComputeSharedMemorySize, {}, {}};
target.fragmentShaderBarycentricEnabled = state->fragmentShaderBarycentric;
target.nonConstantImageOffsets = state->maintenance8;
target.narrowSubgroupClock = state->narrowSubgroupClock;
target.srgbDecodeFormats = state->srgbDecodeFormats;
if (state->meshShader) {
const auto& mesh = state->meshLimits;
@ -2410,6 +2476,10 @@ bool VulkanDevice::SamplerFilterMinmax() const {
return state->samplerFilterMinmax;
}
bool VulkanDevice::ConservativeRasterization() const {
return state->conservativeRasterization;
}
Graphics::Context VulkanDevice::graphicsContext() const {
static const bool noCache = std::getenv("APS5_NO_CONTEXT_CACHE") != nullptr;
if (state->contextReady && !noCache) return state->context;
@ -2445,6 +2515,7 @@ Graphics::Context VulkanDevice::buildContext() const {
state->depthClamp,
state->hostImportAlignment
};
context.dmaBufImport = state->dmaBufImport;
context.recorder = state->recorder.get();
context.descriptorCache = state->descriptorCache.get();
context.samplerCache = state->samplerCache.get();
@ -2453,6 +2524,7 @@ Graphics::Context VulkanDevice::buildContext() const {
context.depthBounds = state->depthBounds;
context.depthBiasClamp = state->depthBiasClamp;
context.samplerFilterMinmax = state->samplerFilterMinmax;
context.conservativeRasterization = state->conservativeRasterization;
context.drawIndirectCount = state->drawIndirectCount;
context.occlusionQueryPrecise = state->occlusionQueryPrecise;
context.emptyBuffer = state->emptyBuffer ? state->emptyBuffer->Handle() : VK_NULL_HANDLE;
@ -2462,6 +2534,7 @@ Graphics::Context VulkanDevice::buildContext() const {
context.imageInt64Atomics = state->imageInt64Atomics;
context.geometryShader = state->geometryShader;
context.sampleRateShading = state->sampleRateShading;
context.nullDescriptors = state->shaderProfile != nullptr && state->shaderProfile->NullDescriptors();
context.primitiveListRestart = state->primitiveListRestart;
context.imageViewMinLod = state->imageViewMinLod;
context.pipelineExecutableInfo = state->pipelineExecutableInfo;
@ -2482,6 +2555,10 @@ void VulkanDevice::ColorMetadataPass(const Graphics::ColorMetadataPass& pass) {
}
void VulkanDevice::Draw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span<const Graphics::CompiledShader> shaders, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::shared_ptr<const DrawRecipe>* recipe) {
for (const auto& shader : shaders) {
require(shader.program != nullptr, "missing compiled shader");
ShaderRecompiler::RuntimeAbi::RequireVersion(shader.program->runtimeAbiVersion);
}
// Two stdout lines per draw cost ~1.3 ms per frame of the queue-0 worker (part of it under the
// GPU mutex); APS5_TRACE_DRAWS=1 restores them.
static const bool trace = std::getenv("APS5_TRACE_DRAWS") != nullptr;
@ -2888,6 +2965,7 @@ struct RecordedDispatch {
DispatchTimer* timer;
// Whether the data refresh recorded anything.
bool refreshed = false;
bool resourceKeyMatched = false;
};
VkDevice VulkanDevice::Device() const {
@ -2991,6 +3069,7 @@ std::uint64_t VulkanDevice::presync(std::span<const std::pair<std::uint64_t, std
}
std::shared_ptr<PreparedDispatch> VulkanDevice::PrepareDispatch(const ShaderRecompiler::RecompileResult& shader, std::span<const Graphics::GuestMemorySnapshot> snapshots) {
ShaderRecompiler::RuntimeAbi::RequireVersion(shader.runtimeAbiVersion);
// APS5_LOCKED_BUILD=1: the whole build under the mutex, as before the split.
static const bool lockedBuild = std::getenv("APS5_LOCKED_BUILD") != nullptr;
if (lockedBuild || shader.spirv.size() < 5 || shader.spirv[0] != 0x07230203u) return nullptr;
@ -3010,7 +3089,7 @@ std::shared_ptr<PreparedDispatch> VulkanDevice::PrepareDispatch(const ShaderReco
prepared->phaseMs[which] += std::chrono::duration<double, std::milli>(now - phaseStart).count();
phaseStart = now;
};
if (ResourceCacheEnabled() && shader.variantId != 0) {
if (ResourceCacheEnabled() && shader.PipelineVariantId() != 0) {
prepared->key = DispatchContentKey(compute, context.device);
phase(PreparedDispatch::PrepareKey);
cached = state->resourceCache.Find(prepared->key);
@ -3206,7 +3285,7 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) {
// after an earlier dispatch's reads of the buffers by that dispatch's trailing barrier. A
// recipe compares the two 64-bit hashes first: equal hashes mean the buffers hold the words.
const bool differs = record.dataRefresh == RecordedDispatch::DataRefresh::Words || resources.DataWordsHash() != record.dataWordsHash;
if (differs && resources.RefreshData(commands, *record.shader, &recorder)) {
if (differs && (record.resourceKeyMatched ? resources.refreshData(commands, *record.shader, &recorder) : resources.RefreshData(commands, *record.shader, &recorder))) {
covered = 0;
++d.templateRefreshed;
d.templateRevalidateMs += record.revalidateMs;
@ -3283,6 +3362,7 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) {
}
VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::uint64_t programAddress, std::shared_ptr<PreparedDispatch> prepared, std::shared_ptr<const Recipe>* recipeOut) {
ShaderRecompiler::RuntimeAbi::RequireVersion(shader.runtimeAbiVersion);
PerformanceTimer timing("Vulkan.Dispatch");
if (recipeOut != nullptr) *recipeOut = nullptr;
// Group counts for the trace lines; an indirect dispatch does not know them.
@ -3306,7 +3386,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
}
// Pipelines are shared by dispatches of one compiled variant; descriptor set layouts built from the
// same bindings are compatible, so the pipeline layout of the first dispatch serves them all.
const std::uint64_t pipelineKey = shader.variantId != 0 ? (shader.variantId << 1u) | (pushStages != 0 ? 1u : 0u) : 0u;
const std::uint64_t pipelineKey = shader.PipelineVariantId() != 0 ? (shader.PipelineVariantId() << 1u) | (pushStages != 0 ? 1u : 0u) : 0u;
std::shared_ptr<ComputePipelineObjects> objects;
if (pipelineKey != 0) {
std::lock_guard pipelines(state->computePipelinesMutex);
@ -3322,7 +3402,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
const auto lookupsBefore = Graphics::DeviceProcLookups();
std::shared_ptr<Graphics::ShaderResources> resources;
ResourceCache::Key contentKey;
const bool cacheable = ResourceCacheEnabled() && shader.variantId != 0;
const bool cacheable = ResourceCacheEnabled() && shader.PipelineVariantId() != 0;
// The object came from the resource cache: a compute template whose data buffers take this
// dispatch's words at the record (ShaderResources::RefreshData).
bool fromCache = false;
@ -3475,9 +3555,11 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT;
pipelineInfo.stage.module = objects->module;
pipelineInfo.stage.pName = "main";
const Graphics::PipelineSpecialization specialization(shader);
pipelineInfo.stage.pSpecializationInfo = specialization.Info();
VkPipelineShaderStageRequiredSubgroupSizeCreateInfoEXT requiredSubgroup{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_REQUIRED_SUBGROUP_SIZE_CREATE_INFO_EXT};
requiredSubgroup.requiredSubgroupSize = shader.hostSubgroupSize;
if (state->computeWave32 && shader.hostSubgroupSize == 32u) pipelineInfo.stage.pNext = &requiredSubgroup;
requiredSubgroup.requiredSubgroupSize = std::min(shader.hostSubgroupSize, state->maxComputeSubgroupSize);
if (state->computeWave32 && (shader.hostSubgroupSize == 32u || shader.hostSubgroupSize > state->maxComputeSubgroupSize)) pipelineInfo.stage.pNext = &requiredSubgroup;
pipelineInfo.layout = objects->layout;
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));
check(state->DeviceFunction<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(state->device, context.pipelineCache, 1, &pipelineInfo, nullptr, &objects->pipeline), "vkCreateComputePipelines");
@ -3496,6 +3578,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
auto& recorder = *state->recorder;
IndirectOutcome outcome{0, 0};
RecordedDispatch record{&context, &shaders[0], resources, objects, pushStages, &pushBytes, x, y, z, arguments, nullptr, programAddress, fromCache && TemplateDataRefresh() ? RecordedDispatch::DataRefresh::Words : RecordedDispatch::DataRefresh::None, 0, revalidateMs, &timer};
record.resourceKeyMatched = fromCache;
if (arguments != 0) decideIndirect(record, outcome, groupsText);
// The indirect hold total below still covers the whole call of a CPU-resolved one.
const bool indirect = record.arguments != 0 || outcome.cpuReason != 0;
@ -3523,7 +3606,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
static auto lastReport = std::chrono::steady_clock::now();
const auto now = std::chrono::steady_clock::now();
const auto ms = std::chrono::duration<double, std::milli>(now - syncStart).count();
auto& waits = byProgram[programAddress != 0 ? programAddress : shader.variantId];
auto& waits = byProgram[programAddress != 0 ? programAddress : shader.PipelineVariantId()];
++waits.count;
waits.ms += ms;
++syncs;
@ -3571,6 +3654,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
}
RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::uint64_t programAddress, const std::shared_ptr<RecipeHit>& hit, IndirectOutcome& outcome, const std::shared_ptr<PreparedDispatch>& verify, bool refreshByWords) {
ShaderRecompiler::RuntimeAbi::RequireVersion(shader.runtimeAbiVersion);
PerformanceTimer timing("Vulkan.DispatchRecipe");
outcome = {0, 0};
char groupsText[40];
@ -3620,7 +3704,7 @@ RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResu
return RecipeOutcome::Rebuild;
}
std::shared_ptr<ComputePipelineObjects> mapped;
const auto pipelineKey = (shader.variantId << 1u) | (recipe.pushes ? 1u : 0u);
const auto pipelineKey = (shader.PipelineVariantId() << 1u) | (recipe.pushes ? 1u : 0u);
{
std::lock_guard pipelines(state->computePipelinesMutex);
if (const auto found = state->computePipelines.find(pipelineKey); found != state->computePipelines.end()) mapped = found->second;
@ -3665,6 +3749,7 @@ RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResu
const auto dataRefresh = !TemplateDataRefresh() ? RecordedDispatch::DataRefresh::None : refreshByWords ? RecordedDispatch::DataRefresh::Words : RecordedDispatch::DataRefresh::Hash;
if (dataRefresh == RecordedDispatch::DataRefresh::Words) counters.refreshByWords.fetch_add(1, std::memory_order_relaxed);
RecordedDispatch record{&context, &shaders[0], hit->resources, hit->objects, recipe.pushes ? VkShaderStageFlags{VK_SHADER_STAGE_COMPUTE_BIT} : VkShaderStageFlags{0}, &recipe.pushBytes, x, y, z, arguments, nullptr, programAddress, dataRefresh, recipe.dataWordsHash, 0, &timer};
record.resourceKeyMatched = verify != nullptr;
if (arguments != 0) decideIndirect(record, outcome, groupsText);
const bool indirect = record.arguments != 0 || outcome.cpuReason != 0;
const auto recordStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};

View File

@ -12,11 +12,12 @@ namespace AgcDriver::Graphics {
enum class ColorTileMode : std::uint32_t {
Linear = 0,
Standard4KB = 5,
Standard64KB = 9,
RenderTarget = 0x1b
};
constexpr TextureTileMode ColorTextureTileMode(ColorTileMode mode) {
return mode == ColorTileMode::Linear ? TextureTileMode::kLinear : mode == ColorTileMode::Standard4KB ? TextureTileMode::kStandard4KB : TextureTileMode::kR64KBX;
return mode == ColorTileMode::Linear ? TextureTileMode::kLinear : mode == ColorTileMode::Standard4KB ? TextureTileMode::kStandard4KB : mode == ColorTileMode::Standard64KB ? TextureTileMode::kStandard64KB : TextureTileMode::kR64KBX;
}
ColorTileMode DecodeColorTileMode(std::uint32_t attrib3);

View File

@ -106,6 +106,7 @@ struct Context {
bool depthClamp = false;
// Nonzero when VK_EXT_external_memory_host is enabled: the required host pointer alignment.
VkDeviceSize hostImportAlignment = 0;
bool dmaBufImport = false;
RenderCache* renderCache = nullptr;
DrawQueue* drawQueue = nullptr;
GraphicsPipelineCache* graphicsPipelines = nullptr;
@ -124,6 +125,7 @@ struct Context {
bool depthBounds = false;
bool depthBiasClamp = false;
bool samplerFilterMinmax = false;
bool conservativeRasterization = false;
VkBuffer emptyBuffer = VK_NULL_HANDLE;
// The device's list of recorded dispatches whose copied written buffers await a CPU write-back
// (VulkanDevice's State::copiedWriters; the draw counterpart is DrawCopiedWriters): an indirect
@ -137,6 +139,7 @@ struct Context {
bool imageInt64Atomics = false;
bool geometryShader = false;
bool sampleRateShading = false;
bool nullDescriptors = false;
bool primitiveListRestart = false;
bool imageViewMinLod = false;
bool pipelineExecutableInfo = false;

View File

@ -8,6 +8,7 @@
#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp"
#include <cstddef>
#include <cstdint>
#include <optional>
#include <span>
namespace AgcDriver::Graphics {
@ -22,6 +23,8 @@ enum class DccKeys { Uncompressed, Clear0000, Clear0001, Clear1110, Clear1111, C
const char* DccKeysName(DccKeys keys);
std::size_t DccKeyBytes(std::uint64_t surfaceBytes);
std::size_t DccKeyCount(TextureTileMode tileMode, std::uint32_t elementBytes, std::uint32_t width, std::uint32_t height, std::uint64_t surfaceBytes);
std::size_t DccKeyCount(const GuestTextureResource& surface, std::uint64_t surfaceBytes);
template<typename ReadFollowed, typename ReadNamed>
bool KeysServeSurface(std::uint64_t followedDcc, DccKeys uploaded, DccKeys filled, std::uint64_t namedDcc, ReadFollowed&& readFollowed, ReadNamed&& readNamed) {
@ -34,15 +37,18 @@ bool KeysServeSurface(std::uint64_t followedDcc, DccKeys uploaded, DccKeys fille
DccKeys ReadDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes);
bool IsDccClear(DccKeys keys);
DccKeys CurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes);
DccKeys CurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes, std::size_t keyCount);
// Stores "uncompressed" keys over the surface's metadata on the CPU (a guest memory write: it waits
// for recorded GPU work that writes the keys first).
void MarkDccUncompressed(std::uint64_t metaAddress, std::uint64_t surfaceBytes);
void MarkDccUncompressed(std::uint64_t metaAddress, std::uint64_t surfaceBytes, std::size_t keyCount);
// The same after a write-back recorded under GuestMemory::GpuMutex: when the keys are in host-imported
// memory and a recorder is active, the store is a fill recorded into the open batch (ordered behind
// the title's key-writing kernels like the write-back itself, nothing waits on the CPU) and the range
// reads as uncompressed from ReadDccKeys while that batch is pending; otherwise the CPU store above.
// APS5_CPU_DCC_KEYS=1 always stores on the CPU.
void MarkDccUncompressed(const Context& context, std::uint64_t metaAddress, std::uint64_t surfaceBytes);
void MarkDccUncompressed(const Context& context, std::uint64_t metaAddress, std::uint64_t surfaceBytes, std::size_t keyCount);
// Fills `bytes` with the texel a 0000/0001/1110/1111 clear code stands for ("1" is 1.0 or the integer
// maximum; the alpha channel is the last one in memory when alphaOnMsb, else the first, and 3-channel
// formats have none). False when the format has no encoding here.
@ -82,6 +88,7 @@ DccKeyProofCounts KeyProofCounts();
// A surface's texels as a read sees them: the guest bytes, or the clear value of fast-cleared keys.
void ReadTextureSurface(const GuestTextureResource& resource, DccKeys keys, std::span<std::byte> bytes);
void NoteKeysFillOnGpu(std::uint64_t begin, std::size_t count, DccKeys keys);
std::optional<DccKeys> WaitForKeyWriters(const GuestTextureResource& resource, std::uint64_t guestBytes);
}

View File

@ -28,7 +28,7 @@ struct DrawInputCopy {
DrawInputCopy CopyDrawInput(const Context& context, Recorder* recorder, std::uint64_t address, std::size_t bytes, std::size_t alignment, Recorder::SnapshotUse use);
void KeepDrawInput(Recorder* recorder, std::uint64_t address, const DrawInputCopy& copy, Recorder::SnapshotUse use, std::uint32_t derived);
std::array<std::uint32_t, 4> MeshIndexBufferDescriptor(const Pm4::DrawParameters& draw, std::uint64_t unreadAddress);
std::array<std::uint32_t, 4> MeshIndexBufferDescriptor(const Pm4::DrawParameters& draw);
struct MeshArguments {
std::uint32_t groups;

View File

@ -27,9 +27,11 @@ struct HostImport {
VkDeviceMemory memory;
VkDeviceAddress address;
void* alias = nullptr;
std::weak_ptr<const GuestAllocations::Range> range {};
// Identity for the life of this import (see HostImportSerial); 0 until first asked for.
std::uint64_t serial = 0;
bool unwatched = false;
bool dmaBuf = false;
};
enum class ImportWatch : std::uint8_t { Watch, Unwatch };
@ -40,9 +42,11 @@ struct ImportProbe {
std::uint32_t pages = 0;
std::uint32_t writtenAtImport = 0;
std::uint32_t writtenAfterSubmit = 0;
std::uint32_t writtenByCpu = 0;
};
ImportProbe ProbeImportWriteProtection(const Context& context);
ImportProbe ProbeDmaBufImportWriteProtection(const Context& context);
ImportWatch PrepareImportWatch(const Context& context);
void SetImportWatch(const Context& context, ImportWatch watch);
@ -149,6 +153,7 @@ struct MirrorStats {
};
MirrorStats MirrorCounters();
void ClearImageMirrors(VkDevice device);
void ClearHostImports(VkDevice device);
struct AddressCopy {
std::uint64_t begin;
@ -199,6 +204,7 @@ public:
void UploadPrepare(bool addressable);
void UploadFinish(bool addressable);
VkDescriptorBufferInfo Descriptor(std::uint64_t address, std::size_t bytes, std::uint32_t& adjustment) const;
static std::uint64_t ViewBytes(std::uint64_t bytes, std::uint32_t adjustment);
std::vector<ShaderRecompiler::BdaAbi::Range> AddressRanges() const;
// The BDA table of the cached address space when it serves this upload alone (an address-based
// build with no region outside it): its ranges, immutable while the space lives, and the
@ -340,6 +346,8 @@ private:
// The cached address space this build maps through (its lease pins the ranges); `regions` then
// holds only the regions outside it (V#s, snapshots, ranges copied per build).
std::shared_ptr<const AddressSpace> space;
mutable std::uint64_t writeTableSerial = 0;
mutable std::shared_ptr<const std::vector<ShaderRecompiler::BdaAbi::Range>> writeTableRanges;
// 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;

View File

@ -17,6 +17,13 @@ enum class TextureTileMode {
kS64KBX,
kD64KBX,
kR64KBX,
kD256B,
kD4KB,
kD64KB,
kS64KBT,
kD64KBT,
kS4KBX,
kD4KBX,
// Upstream's name for SW_64KB_R_X (tile mode 0x1b).
RenderTarget64KB = kR64KBX
};
@ -28,10 +35,23 @@ constexpr std::uint32_t XorSwizzleMode(TextureTileMode mode) {
case TextureTileMode::kS64KBX: return 25u;
case TextureTileMode::kD64KBX: return 26u;
case TextureTileMode::kR64KBX: return 27u;
case TextureTileMode::kS64KBT: return 17u;
case TextureTileMode::kD64KBT: return 18u;
case TextureTileMode::kS4KBX: return 21u;
case TextureTileMode::kD4KBX: return 22u;
default: return 0u;
}
}
constexpr std::uint32_t EquationSwizzleMode(TextureTileMode mode) {
switch (mode) {
case TextureTileMode::kD256B: return 2u;
case TextureTileMode::kD4KB: return 6u;
case TextureTileMode::kD64KB: return 10u;
default: return XorSwizzleMode(mode);
}
}
enum class TextureDimension {
k1D,
k2D,
@ -61,6 +81,7 @@ struct GuestTextureResource {
// DCC metadata of a compressed surface, or 0 (see DccMetadata.hpp).
std::uint64_t dccAddress = 0;
bool dccAlphaOnMsb = false;
bool dccPipeAligned = false;
std::uint32_t minLod = 0;
std::uint32_t allocatedMipCount = 0;
};

View File

@ -0,0 +1,76 @@
#ifndef CORE_LIBS_AGCDRIVER_GRAPHICS_PIPELINESPECIALIZATION_HPP
#define CORE_LIBS_AGCDRIVER_GRAPHICS_PIPELINESPECIALIZATION_HPP
#include "prx/libSceAgcDriver/Graphics/include/Shaders.hpp"
#include <limits>
#include <map>
#include <set>
#include <spirv/unified1/spirv.hpp>
#include <vector>
namespace AgcDriver::Graphics {
class PipelineSpecialization {
public:
static void Validate(const ShaderRecompiler::RecompileResult& shader) {
const auto& words = shader.spirv;
Require(words.size() >= 5 && words[0] == spv::MagicNumber && words[3] != 0, "invalid specialization module header");
std::set<std::uint32_t> unsignedTypes;
std::map<std::uint32_t, std::uint32_t> constants;
std::map<std::uint32_t, std::uint32_t> decorations;
std::set<std::uint32_t> constantIds;
std::set<std::uint32_t> supplied;
for (const auto& constant : shader.specialization) Require(supplied.insert(constant.id).second, "duplicate pipeline specialization ID");
for (std::size_t cursor = 5; cursor < words.size();) {
const auto count = words[cursor] >> 16u;
const auto op = static_cast<spv::Op>(words[cursor] & 0xffffu);
Require(count != 0 && count <= words.size() - cursor, "truncated specialization module instruction");
const auto validId = [&](std::uint32_t id) { Require(id != 0 && id < words[3], "specialization ID exceeds module bound"); };
if (op == spv::OpTypeInt) {
Require(count == 4, "malformed integer type");
if (words[cursor + 2] == 32u && words[cursor + 3] == 0u) unsignedTypes.insert(words[cursor + 1]);
} else if (op == spv::OpSpecConstant) {
Require(count == 4, "specialization constant must have a 32-bit literal");
validId(words[cursor + 2]);
Require(constants.emplace(words[cursor + 2], words[cursor + 1]).second, "duplicate specialization result ID");
} else if (op == spv::OpDecorate && count >= 3 && words[cursor + 2] == spv::DecorationSpecId) {
Require(count == 4, "malformed SpecId decoration");
validId(words[cursor + 1]);
Require(decorations.emplace(words[cursor + 1], words[cursor + 3]).second && constantIds.insert(words[cursor + 3]).second, "duplicate SpecId decoration");
} else if (op == spv::OpSpecConstantTrue || op == spv::OpSpecConstantFalse || op == spv::OpSpecConstantComposite || op == spv::OpSpecConstantOp) {
throw std::runtime_error("AGC graphics: unsupported specialization constant representation");
}
cursor += count;
}
Require(constants.size() == decorations.size(), "specialization declarations and SpecId decorations disagree");
for (const auto& [id, type] : constants) {
Require(unsignedTypes.contains(type), "specialization constant must be uint32");
const auto decoration = decorations.find(id);
Require(decoration != decorations.end() && supplied.contains(decoration->second), "required pipeline specialization value is missing");
}
}
explicit PipelineSpecialization(const ShaderRecompiler::RecompileResult& shader) {
Validate(shader);
entries.reserve(shader.specialization.size());
for (std::size_t index = 0; index < shader.specialization.size(); ++index) {
const auto offset = index * sizeof(ShaderRecompiler::PipelineSpecializationConstant) + offsetof(ShaderRecompiler::PipelineSpecializationConstant, value);
Require(offset <= std::numeric_limits<std::uint32_t>::max(), "pipeline specialization offset exceeds Vulkan limits");
entries.push_back({shader.specialization[index].id, static_cast<std::uint32_t>(offset), sizeof(std::uint32_t)});
}
info.mapEntryCount = static_cast<std::uint32_t>(entries.size());
info.pMapEntries = entries.data();
info.dataSize = shader.specialization.size() * sizeof(ShaderRecompiler::PipelineSpecializationConstant);
info.pData = shader.specialization.data();
}
[[nodiscard]] const VkSpecializationInfo* Info() const { return entries.empty() ? nullptr : &info; }
private:
std::vector<VkSpecializationMapEntry> entries;
VkSpecializationInfo info{};
};
}
#endif

View File

@ -67,7 +67,7 @@ public:
// continuing draw the command buffer without ending it.
bool ContinuesRenderPass(std::uint64_t key) const;
VkCommandBuffer CommandsInRenderPass();
void LeaveRenderPassOpen(std::uint64_t key, std::uint32_t timing, bool continuable);
void LeaveRenderPassOpen(std::uint64_t key, std::uint32_t timing, bool continuable, std::function<void(VkCommandBuffer)> afterPass = {});
// DCC "uncompressed" key stores (DccMetadata.cpp StoreUncompressedOnGpu): queued on the open
// batch and recorded as one run (one barrier pair for every queued fill) at Submit, before a
// label store (RecordStore), or before a command that writes or reads a queued range (the
@ -86,14 +86,10 @@ public:
bool Idle() const { return open == nullptr && inFlight.empty(); }
// Whether recorded work still has completion actions (write-backs the CPU must see) to run.
bool HasCompletions() const;
// The object lives at least until the batch open now completed; it is then destroyed AFTER the
// finishing thread released GuestMemory::GpuMutex, on a release thread of its own (never under
// a hold, see Recorder.cpp ReleaseDeferredKeeps; the finishing thread destroys it itself when
// the thread's queue is full, APS5_RELEASE_QUEUE_MAX batches, or with APS5_RELEASE_ON_UNLOCK=1;
// APS5_RELEASE_UNDER_LOCK=1 destroys it in the reap as before), so its destructor must need
// neither the mutex nor the recorder nor a particular thread. ~Recorder joins the release
// thread and waits for every release in progress before the device goes.
void Keep(std::shared_ptr<void> object);
void Keep(std::shared_ptr<void> object, std::size_t bytes = 0);
static constexpr std::size_t KeptBytesBudget = std::size_t{512} << 20u;
void BoundKeptBytes();
std::size_t InFlightKeptBytes() const { return inFlightKeptBytes; }
enum class SnapshotUse : std::uint8_t { Storage, Vertex, Index16, Index32 };
static constexpr std::size_t DrawSnapshotBudget = std::size_t{256} << 20u;
static constexpr std::size_t DrawSnapshotEntries = 1024;
@ -103,6 +99,7 @@ public:
void KeepDrawSnapshot(std::uint64_t address, std::size_t bytes, std::uint64_t generation, std::uint64_t registryGeneration, std::shared_ptr<Buffer> buffer, SnapshotUse use = SnapshotUse::Storage, std::uint32_t derived = 0);
void OnComplete(std::function<void()> action);
void NotePendingWrite(std::uint64_t address, std::size_t bytes);
void NotePendingFill(std::uint64_t address, std::size_t bytes, std::uint8_t value);
// Notes several [begin, end) ranges and publishes the snapshot once (a dispatch writes many buffers).
void NotePendingWrites(std::span<const std::pair<std::uint64_t, std::uint64_t>> ranges);
bool PendingWriteOverlaps(std::uint64_t address, std::size_t bytes) const;
@ -112,6 +109,13 @@ public:
bool PendingWriteSettled(std::uint64_t address, std::size_t bytes) const;
std::uint64_t LastWriteNote(std::uint64_t address, std::size_t bytes) const;
std::uint64_t NewestWriteNote(std::uint64_t address, std::size_t bytes) const;
struct PendingWrite {
std::uint64_t begin;
std::uint64_t end;
std::uint64_t note;
int value;
};
std::vector<PendingWrite> PendingWritesOver(std::uint64_t address, std::size_t bytes) const;
// Batch read tracking: the guest ranges the recorded work reads IN PLACE through a host import
// and the GPU has not executed yet (a V# element bound in place, a region the GPU copies out of
// an import, an address-based build's leased heaps, indirect arguments, a GPU-direct storage
@ -491,9 +495,11 @@ private:
VkCommandBuffer commands = VK_NULL_HANDLE;
VkFence fence = VK_NULL_HANDLE;
std::vector<std::shared_ptr<void>> kept;
std::size_t keptBytes = 0;
std::vector<std::function<void()>> completions;
std::vector<std::pair<std::uint64_t, std::uint64_t>> writes;
std::vector<std::uint64_t> writeNotes;
std::vector<std::int16_t> writeValues;
// In-place reads (see NotePendingRead), dying with the batch: a finished batch's reads are done.
struct Read {
std::uint64_t begin;
@ -581,6 +587,7 @@ private:
bool continuable = false;
std::uint64_t key = 0;
std::uint32_t timing = NoTiming;
std::function<void(VkCommandBuffer)> afterPass;
} renderPass;
// A pass ended in this batch: Submit records the host-read barrier its draws left out.
bool hostReadOwed = false;
@ -676,7 +683,7 @@ private:
// Returns false when the locked path must run instead (no timeline, nested acquisition, off).
bool syncThroughUnlocked(std::uint64_t address, std::uint64_t end, int source, const void* site);
// `source` is the CountSync source the wait is attributed to.
void finish(std::unique_ptr<Batch> batch, bool wait, int source);
void finish(std::unique_ptr<Batch> batch, bool wait, int source, bool releaseNow = false);
void release(Batch& batch) noexcept;
static bool overlaps(const Batch& batch, std::uint64_t address, std::uint64_t end);
// The first noted read of `batch` overlapping [address, end), or null.
@ -687,7 +694,7 @@ private:
// Appends one range to the open batch; returns whether the snapshot must be rebuilt for it.
// `ownLabel`: the range is a label's own store (NoteLabel, AfterCompletions), which does not
// overwrite the table entries it covers; any other range flags them (table mutex, briefly).
bool noteWrite(std::uint64_t address, std::size_t bytes, bool ownLabel = false);
bool noteWrite(std::uint64_t address, std::size_t bytes, bool ownLabel = false, int value = -1);
// Appends one range to `batch` (open or in flight) and publishes the snapshot if needed.
void noteWriteOn(Batch& batch, std::uint64_t address, std::size_t bytes, bool ownLabel = false);
void noteLabelOn(Batch& batch, std::uint64_t address, std::span<const std::byte> bytes, std::uint64_t stamp, std::uint32_t queue, bool behindCompletion = false);
@ -731,6 +738,7 @@ private:
// snapshot (a rebuild from inside a completion must not drop them) until finish returns.
std::vector<const Batch*> finishing;
std::uint64_t submissions = 0;
std::size_t inFlightKeptBytes = 0;
std::uint64_t writeNoteCount = 0;
// Command buffers and fences of completed batches, reused by later ones (hundreds of batches per
// frame would otherwise allocate and free their objects each time).

View File

@ -11,14 +11,14 @@
namespace AgcDriver::Graphics {
ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& context, const std::array<std::uint8_t, 8>& exportMappings, bool nullProgram = false);
ShaderRecompiler::ShaderComputeStageInfo DecodeComputeStageInfo(const Registers& shader);
ShaderRecompiler::ShaderComputeStageInfo DecodeComputeStageInfo(const Registers& shader, std::span<const std::byte> header);
// A guest range DecodeVertexStageInfo read (an attribute word, a vertex V#) with the bytes as read:
// the draw cache validates them by value with the stage's capture (design_cpu_final rule RD).
struct DecodeRead {
std::uint64_t address;
std::vector<std::byte> bytes;
};
ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span<const std::byte> header, std::uint64_t headerAddress, std::span<const std::uint32_t> userData, std::vector<DecodeRead>* reads = nullptr);
ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span<const std::byte> header, std::uint64_t headerAddress, std::span<const std::uint32_t> userData, std::vector<DecodeRead>* reads = nullptr, bool staticAbi = false);
}

View File

@ -18,6 +18,8 @@
#include <unordered_map>
#include <vector>
namespace AgcDriver { class VulkanDevice; }
namespace AgcDriver::Graphics {
class Recorder;
@ -60,9 +62,6 @@ public:
VkDescriptorSet set = VK_NULL_HANDLE;
VkDescriptorPool pool = VK_NULL_HANDLE;
};
// A set of `layout` needing `sizes` descriptors from the pool chain, opening a pool when no pool
// has room; a null set when the needs exceed what one chain pool holds (the caller then makes a
// dedicated pool, as before).
SetAllocation Allocate(VkDescriptorSetLayout layout, std::span<const VkDescriptorPoolSize> sizes);
void Free(const SetAllocation& allocation) noexcept;
// APS5_PROFILE_DRAW counters: layouts served from the map / created, sets allocated, pools opened.
@ -71,6 +70,7 @@ public:
std::uint64_t layoutMisses = 0;
std::uint64_t sets = 0;
std::uint64_t pools = 0;
std::uint64_t dedicatedPools = 0;
};
Stats Counters() const;
@ -82,6 +82,7 @@ private:
mutable std::mutex mutex;
std::map<std::vector<std::uint32_t>, VkDescriptorSetLayout> layouts;
std::vector<VkDescriptorPool> pools;
std::vector<VkDescriptorPool> dedicated;
Stats stats;
};
@ -228,6 +229,8 @@ public:
std::vector<std::pair<std::uint64_t, std::uint64_t>> PresyncSurfaces() const;
private:
friend class AgcDriver::VulkanDevice;
bool refreshData(VkCommandBuffer commands, const CompiledShader& shader, Recorder* recorder);
struct DescribedRange {
const char* kind;
std::uint64_t address;
@ -396,7 +399,6 @@ private:
// Whether the layout is this object's own (no cache) and destroyed with it.
bool ownsLayout = false;
VkDescriptorSet _set = VK_NULL_HANDLE;
// Dedicated pool of this object's set (no cache, or a set too large for a cache pool).
VkDescriptorPool pool = VK_NULL_HANDLE;
// The cache pool the set was allocated from, freed back to it on release.
VkDescriptorPool cachePool = VK_NULL_HANDLE;
@ -414,6 +416,7 @@ private:
std::vector<bool> storageAtomic;
std::vector<bool> storageAtomic64;
std::vector<std::shared_ptr<Sampler>> samplers;
std::shared_ptr<Sampler> paddingSampler;
bool reusable = false;
std::vector<DirectRegion> directRegions;
std::vector<ValidatedSurface> validatedTextures;
@ -434,6 +437,7 @@ private:
// still to look up (index into `bindings`; the DescriptorBinding lives in the compiled shader),
// the descriptor counts the set was sized for, and the compute stage of a deferred build.
std::vector<Binding> bindings;
std::vector<std::uint32_t> refreshResourceKey;
struct DeferredImages {
const ShaderRecompiler::DescriptorBinding* binding;
std::size_t index;

View File

@ -41,6 +41,9 @@ struct ColorTarget {
// DCC metadata of a compressed target (CB_COLOR_INFO DCC_ENABLE), or 0 (see DccMetadata.hpp).
std::uint64_t dccAddress = 0;
bool dccAlphaOnMsb = false;
bool dccPipeAligned = false;
std::uint64_t cmaskAddress = 0;
std::size_t cmaskBytes = 0;
std::uint64_t surfaceAddress = 0;
VkExtent2D surfaceExtent{};
std::uint32_t mipCount = 1;
@ -89,6 +92,7 @@ struct State {
VkViewport viewport;
bool negativeOneToOne;
bool depthClamp = false;
VkConservativeRasterizationModeEXT conservativeRasterization = VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT;
VkRect2D scissor;
VkCullModeFlags cullMode;
VkFrontFace frontFace;
@ -100,6 +104,8 @@ ShaderStages DecodeShaderStages(const QueueState& queue);
State DecodeState(const QueueState& queue);
std::array<std::uint8_t, 8> ExportMappings(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);
struct ColorMetadataPass {
enum class Mode { EliminateFastClear, DccDecompress };
@ -107,11 +113,12 @@ struct ColorMetadataPass {
std::vector<ColorTarget> targets;
};
std::optional<ColorMetadataPass> DecodeColorMetadataPass(const QueueState& queue);
std::string DepthMaintenanceRejection(const QueueState& queue);
// The message DecodeState (or the pixel stage decode after it) would throw for the register rules
// this precheck covers, evaluated without exceptions before the draw is decoded; empty when they
// pass (DecodeState still checks everything). A register a rule needs that is absent is no verdict.
std::string DrawRejection(const QueueState& queue, bool indexed);
bool PixelProgramUnset(const QueueState& queue);
bool PixelProgramSkipped(const QueueState& queue);
std::string NullPixelProgramRejection(const QueueState& queue);
// The recording facade of the draw decoders (design_cpu_final M8, step 8a): every register read

View File

@ -107,6 +107,8 @@ private:
std::unique_ptr<CommandBatch> upload;
};
VkFormat AttachmentProxyFormat(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),
// either at once (WriteBack) or deferred: MarkDirty keeps them on the GPU until something reads that
@ -131,6 +133,9 @@ public:
// 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);
VkImageView AttachmentProxyView();
void RecordAttachmentProxyLoad(VkCommandBuffer commands, VkImageLayout attachmentLayout) const;
void RecordAttachmentProxyStore(VkCommandBuffer commands, VkImageLayout attachmentLayout) const;
void WriteBack();
// Deferred write-back (APS5_EAGER_WRITEBACK=1 stores at once instead).
void MarkDirty();
@ -278,6 +283,7 @@ public:
DccKeys UploadedKeys() const { return uploadedKeys; }
DccKeys FilledKeys() const { return filledKeys; }
DccKeyProof& KeyProof() const { return keyProof; }
DccKeys ProvedKeys() const;
bool ServesKeysAt(std::uint64_t dccAddress) const;
// Brings the image up to date with guest memory before another use; returns whether its content
// was still current (nothing uploaded).
@ -421,7 +427,7 @@ private:
std::vector<std::byte> original;
// DCC keys the image content was uploaded under: a fast-cleared surface starts as its clear value.
DccKeys uploadedKeys = DccKeys::Uncompressed;
DccKeys filledKeys = DccKeys::Uncompressed;
mutable DccKeys filledKeys = DccKeys::Uncompressed;
mutable DccKeyProof keyProof;
struct ForeignKeyProof {
std::uint64_t dccAddress = 0;
@ -459,6 +465,9 @@ private:
std::map<std::pair<std::uint32_t, bool>, VkImageView> uintViews;
bool attachable = false;
std::map<std::tuple<VkFormat, std::uint32_t, std::uint32_t>, VkImageView> attachmentViews;
VkImage proxyImage = VK_NULL_HANDLE;
VkDeviceMemory proxyMemory = VK_NULL_HANDLE;
VkImageView proxyView = VK_NULL_HANDLE;
VkFormat storageFormat = VK_FORMAT_UNDEFINED;
// Results are on the GPU only (guarded by the pending-write registry lock).
bool dirty = false;

View File

@ -38,7 +38,42 @@ inline constexpr TextureSwizzleEquation kTextureSwizzleEquations[] = {
{27u, 4u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000002u, 0x00001000u, 0x00002000u, 0x00004000u, 0x00000004u, 0x08008008u, 0x04010010u, 0x02020040u, 0x01040020u, 0x00008000u, 0x00000010u, 0x00040000u, 0x00000040u}},
{27u, 8u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x00000002u, 0x00000004u, 0x00002000u, 0x08008008u, 0x04010010u, 0x02020040u, 0x01040020u, 0x00004000u, 0x00000008u, 0x00010000u, 0x00000040u}},
{27u, 16u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x00000002u, 0x00002000u, 0x08008008u, 0x04010010u, 0x02020040u, 0x01040020u, 0x00004000u, 0x00000004u, 0x00008000u, 0x00000010u}},
// Thick (3D) standard swizzles SW_4KB_S and SW_64KB_S use the S3 patterns, keyed 0x100 | SW_MODE.
{2u, 1u, {0x00000001u, 0x00000002u, 0x00000004u, 0x00002000u, 0x00001000u, 0x00004000u, 0x00000008u, 0x00008000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
{2u, 2u, {0x00000000u, 0x00000001u, 0x00000002u, 0x00000004u, 0x00001000u, 0x00002000u, 0x00004000u, 0x00000008u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
{2u, 4u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000002u, 0x00001000u, 0x00002000u, 0x00004000u, 0x00000004u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
{2u, 8u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x00000002u, 0x00000004u, 0x00002000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
{2u, 16u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x00000002u, 0x00002000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
{6u, 1u, {0x00000001u, 0x00000002u, 0x00000004u, 0x00002000u, 0x00001000u, 0x00004000u, 0x00000008u, 0x00008000u, 0x00010000u, 0x00000010u, 0x00020000u, 0x00000020u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
{6u, 2u, {0x00000000u, 0x00000001u, 0x00000002u, 0x00000004u, 0x00001000u, 0x00002000u, 0x00004000u, 0x00000008u, 0x00008000u, 0x00000010u, 0x00010000u, 0x00000020u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
{6u, 4u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000002u, 0x00001000u, 0x00002000u, 0x00004000u, 0x00000004u, 0x00008000u, 0x00000008u, 0x00010000u, 0x00000010u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
{6u, 8u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x00000002u, 0x00000004u, 0x00002000u, 0x00004000u, 0x00000008u, 0x00008000u, 0x00000010u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
{6u, 16u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x00000002u, 0x00002000u, 0x00004000u, 0x00000004u, 0x00008000u, 0x00000008u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
{10u, 1u, {0x00000001u, 0x00000002u, 0x00000004u, 0x00002000u, 0x00001000u, 0x00004000u, 0x00000008u, 0x00008000u, 0x00010000u, 0x00000010u, 0x00020000u, 0x00000020u, 0x00040000u, 0x00000040u, 0x00080000u, 0x00000080u}},
{10u, 2u, {0x00000000u, 0x00000001u, 0x00000002u, 0x00000004u, 0x00001000u, 0x00002000u, 0x00004000u, 0x00000008u, 0x00008000u, 0x00000010u, 0x00010000u, 0x00000020u, 0x00020000u, 0x00000040u, 0x00040000u, 0x00000080u}},
{10u, 4u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000002u, 0x00001000u, 0x00002000u, 0x00004000u, 0x00000004u, 0x00008000u, 0x00000008u, 0x00010000u, 0x00000010u, 0x00020000u, 0x00000020u, 0x00040000u, 0x00000040u}},
{10u, 8u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x00000002u, 0x00000004u, 0x00002000u, 0x00004000u, 0x00000008u, 0x00008000u, 0x00000010u, 0x00010000u, 0x00000020u, 0x00020000u, 0x00000040u}},
{10u, 16u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x00000002u, 0x00002000u, 0x00004000u, 0x00000004u, 0x00008000u, 0x00000008u, 0x00010000u, 0x00000010u, 0x00020000u, 0x00000020u}},
{17u, 1u, {0x00000001u, 0x00000002u, 0x00000004u, 0x00000008u, 0x00001000u, 0x00002000u, 0x00004000u, 0x00008000u, 0x00010080u, 0x00080010u, 0x00020040u, 0x00040020u, 0x00040000u, 0x00000040u, 0x00080000u, 0x00000080u}},
{17u, 2u, {0x00000000u, 0x00000001u, 0x00000002u, 0x00000004u, 0x00001000u, 0x00002000u, 0x00004000u, 0x00000008u, 0x00008080u, 0x00040010u, 0x00010040u, 0x00020020u, 0x00020000u, 0x00000040u, 0x00040000u, 0x00000080u}},
{17u, 4u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000002u, 0x00001000u, 0x00002000u, 0x00004000u, 0x00000004u, 0x00008040u, 0x00040008u, 0x00010020u, 0x00020010u, 0x00020000u, 0x00000020u, 0x00040000u, 0x00000040u}},
{17u, 8u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x00002000u, 0x00000002u, 0x00000004u, 0x00004040u, 0x00020008u, 0x00008020u, 0x00010010u, 0x00010000u, 0x00000020u, 0x00020000u, 0x00000040u}},
{17u, 16u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00001000u, 0x00002000u, 0x00000001u, 0x00000002u, 0x00004020u, 0x00020004u, 0x00008010u, 0x00010008u, 0x00010000u, 0x00000010u, 0x00020000u, 0x00000020u}},
{18u, 1u, {0x00000001u, 0x00000002u, 0x00000004u, 0x00002000u, 0x00001000u, 0x00004000u, 0x00000008u, 0x00008000u, 0x00010080u, 0x00080010u, 0x00020040u, 0x00040020u, 0x00040000u, 0x00000040u, 0x00080000u, 0x00000080u}},
{18u, 2u, {0x00000000u, 0x00000001u, 0x00000002u, 0x00000004u, 0x00001000u, 0x00002000u, 0x00004000u, 0x00000008u, 0x00008080u, 0x00040010u, 0x00010040u, 0x00020020u, 0x00020000u, 0x00000040u, 0x00040000u, 0x00000080u}},
{18u, 4u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000002u, 0x00001000u, 0x00002000u, 0x00004000u, 0x00000004u, 0x00008040u, 0x00040008u, 0x00010020u, 0x00020010u, 0x00020000u, 0x00000020u, 0x00040000u, 0x00000040u}},
{18u, 8u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x00000002u, 0x00000004u, 0x00002000u, 0x00004040u, 0x00020008u, 0x00008020u, 0x00010010u, 0x00010000u, 0x00000020u, 0x00020000u, 0x00000040u}},
{18u, 16u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x00000002u, 0x00002000u, 0x00004020u, 0x00020004u, 0x00008010u, 0x00010008u, 0x00010000u, 0x00000010u, 0x00020000u, 0x00000020u}},
{21u, 1u, {0x00000001u, 0x00000002u, 0x00000004u, 0x00000008u, 0x00001000u, 0x00002000u, 0x00004000u, 0x00008000u, 0x08010080u, 0x04080010u, 0x02020040u, 0x01040020u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
{21u, 2u, {0x00000000u, 0x00000001u, 0x00000002u, 0x00000004u, 0x00001000u, 0x00002000u, 0x00004000u, 0x00000008u, 0x08008080u, 0x04040010u, 0x02010040u, 0x01020020u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
{21u, 4u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000002u, 0x00001000u, 0x00002000u, 0x00004000u, 0x00000004u, 0x08008040u, 0x04040008u, 0x02010020u, 0x01020010u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
{21u, 8u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x00002000u, 0x00000002u, 0x00000004u, 0x08004040u, 0x04020008u, 0x02008020u, 0x01010010u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
{21u, 16u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00001000u, 0x00002000u, 0x00000001u, 0x00000002u, 0x08004020u, 0x04020004u, 0x02008010u, 0x01010008u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
{22u, 1u, {0x00000001u, 0x00000002u, 0x00000004u, 0x00002000u, 0x00001000u, 0x00004000u, 0x00000008u, 0x00008000u, 0x08010080u, 0x04080010u, 0x02020040u, 0x01040020u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
{22u, 2u, {0x00000000u, 0x00000001u, 0x00000002u, 0x00000004u, 0x00001000u, 0x00002000u, 0x00004000u, 0x00000008u, 0x08008080u, 0x04040010u, 0x02010040u, 0x01020020u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
{22u, 4u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000002u, 0x00001000u, 0x00002000u, 0x00004000u, 0x00000004u, 0x08008040u, 0x04040008u, 0x02010020u, 0x01020010u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
{22u, 8u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x00000002u, 0x00000004u, 0x00002000u, 0x08004040u, 0x04020008u, 0x02008020u, 0x01010010u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
{22u, 16u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x00000002u, 0x00002000u, 0x08004020u, 0x04020004u, 0x02008010u, 0x01010008u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
// Thick (3D) standard swizzles SW_4KB_S, SW_64KB_S and SW_64KB_S_X use the S3 patterns, keyed 0x100 | SW_MODE.
{0x105u, 1u, {0x00000001u, 0x00000002u, 0x01000000u, 0x00001000u, 0x02000000u, 0x00002000u, 0x00000004u, 0x04000000u, 0x00004000u, 0x00000008u, 0x08000000u, 0x00008000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
{0x105u, 2u, {0x00000000u, 0x00000001u, 0x01000000u, 0x00001000u, 0x02000000u, 0x00002000u, 0x00000002u, 0x04000000u, 0x00004000u, 0x00000004u, 0x08000000u, 0x00008000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
{0x105u, 4u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x01000000u, 0x00002000u, 0x00000002u, 0x02000000u, 0x00004000u, 0x00000004u, 0x04000000u, 0x00008000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
@ -49,6 +84,11 @@ inline constexpr TextureSwizzleEquation kTextureSwizzleEquations[] = {
{0x109u, 4u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x01000000u, 0x00002000u, 0x00000002u, 0x02000000u, 0x00004000u, 0x00000004u, 0x04000000u, 0x00008000u, 0x00000008u, 0x08000000u, 0x00010000u, 0x00000010u}},
{0x109u, 8u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x01000000u, 0x00001000u, 0x00000002u, 0x02000000u, 0x00002000u, 0x00000004u, 0x04000000u, 0x00004000u, 0x00000008u, 0x08000000u, 0x00008000u, 0x00000010u}},
{0x109u, 16u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x01000000u, 0x00001000u, 0x00000001u, 0x02000000u, 0x00002000u, 0x00000002u, 0x04000000u, 0x00004000u, 0x00000004u, 0x08000000u, 0x00008000u, 0x00000008u}},
{0x119u, 1u, {0x00000001u, 0x00000002u, 0x01000000u, 0x00001000u, 0x02000000u, 0x00002000u, 0x00000004u, 0x04000000u, 0x40004040u, 0x20020008u, 0x08010020u, 0x10008010u, 0x00000010u, 0x10000000u, 0x00010000u, 0x00000020u}},
{0x119u, 2u, {0x00000000u, 0x00000001u, 0x01000000u, 0x00001000u, 0x02000000u, 0x00002000u, 0x00000002u, 0x04000000u, 0x40004020u, 0x20020004u, 0x08010010u, 0x10008008u, 0x00000008u, 0x10000000u, 0x00010000u, 0x00000010u}},
{0x119u, 4u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x01000000u, 0x00002000u, 0x00000002u, 0x02000000u, 0x20004020u, 0x10020004u, 0x04010010u, 0x08008008u, 0x00000008u, 0x08000000u, 0x00010000u, 0x00000010u}},
{0x119u, 8u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x01000000u, 0x00001000u, 0x00000002u, 0x02000000u, 0x20002020u, 0x10010004u, 0x04008010u, 0x08004008u, 0x00000008u, 0x08000000u, 0x00008000u, 0x00000010u}},
{0x119u, 16u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x01000000u, 0x00001000u, 0x00000001u, 0x02000000u, 0x20002010u, 0x10010002u, 0x04008008u, 0x08004004u, 0x00000004u, 0x08000000u, 0x00008000u, 0x00000008u}},
};
inline const TextureSwizzleEquation* FindTextureSwizzleEquation(std::uint32_t swizzleMode, std::uint32_t elementBytes) {

View File

@ -20,37 +20,39 @@ struct VertexFormat {
inline VertexFormat DecodeVertexFormat(const ShaderRecompiler::VertexAttribute& attribute) {
Require(attribute.components >= 1 && attribute.components <= 4, "invalid vertex attribute component count");
const auto components = attribute.formatComponents == 0u ? attribute.components : attribute.formatComponents;
Require(components >= 1u && components <= 4u, "invalid vertex format component count");
const auto format = (attribute.resource.fields[3] >> 12u) & 0x7fu;
switch (format) {
case 1: { const std::array formats{VK_FORMAT_R8_UNORM}; const auto count = std::min(attribute.components, 1u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 2: { const std::array formats{VK_FORMAT_R8_SNORM}; const auto count = std::min(attribute.components, 1u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 3: { const std::array formats{VK_FORMAT_R8_USCALED}; const auto count = std::min(attribute.components, 1u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 4: { const std::array formats{VK_FORMAT_R8_SSCALED}; const auto count = std::min(attribute.components, 1u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 5: { const std::array formats{VK_FORMAT_R8_UINT}; const auto count = std::min(attribute.components, 1u); return {formats[count - 1], count * 1u, 1u, "u32"}; }
case 6: { const std::array formats{VK_FORMAT_R8_SINT}; const auto count = std::min(attribute.components, 1u); return {formats[count - 1], count * 1u, 1u, "i32"}; }
case 7: { const std::array formats{VK_FORMAT_R16_UNORM}; const auto count = std::min(attribute.components, 1u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 8: { const std::array formats{VK_FORMAT_R16_SNORM}; const auto count = std::min(attribute.components, 1u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 9: { const std::array formats{VK_FORMAT_R16_USCALED}; const auto count = std::min(attribute.components, 1u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 10: { const std::array formats{VK_FORMAT_R16_SSCALED}; const auto count = std::min(attribute.components, 1u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 11: { const std::array formats{VK_FORMAT_R16_UINT}; const auto count = std::min(attribute.components, 1u); return {formats[count - 1], count * 2u, 2u, "u32"}; }
case 12: { const std::array formats{VK_FORMAT_R16_SINT}; const auto count = std::min(attribute.components, 1u); return {formats[count - 1], count * 2u, 2u, "i32"}; }
case 13: { const std::array formats{VK_FORMAT_R16_SFLOAT}; const auto count = std::min(attribute.components, 1u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 14: { const std::array formats{VK_FORMAT_R8_UNORM, VK_FORMAT_R8G8_UNORM}; const auto count = std::min(attribute.components, 2u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 15: { const std::array formats{VK_FORMAT_R8_SNORM, VK_FORMAT_R8G8_SNORM}; const auto count = std::min(attribute.components, 2u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 16: { const std::array formats{VK_FORMAT_R8_USCALED, VK_FORMAT_R8G8_USCALED}; const auto count = std::min(attribute.components, 2u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 17: { const std::array formats{VK_FORMAT_R8_SSCALED, VK_FORMAT_R8G8_SSCALED}; const auto count = std::min(attribute.components, 2u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 18: { const std::array formats{VK_FORMAT_R8_UINT, VK_FORMAT_R8G8_UINT}; const auto count = std::min(attribute.components, 2u); return {formats[count - 1], count * 1u, 1u, "u32"}; }
case 19: { const std::array formats{VK_FORMAT_R8_SINT, VK_FORMAT_R8G8_SINT}; const auto count = std::min(attribute.components, 2u); return {formats[count - 1], count * 1u, 1u, "i32"}; }
case 20: { const std::array formats{VK_FORMAT_R32_UINT}; const auto count = std::min(attribute.components, 1u); return {formats[count - 1], count * 4u, 4u, "u32"}; }
case 21: { const std::array formats{VK_FORMAT_R32_SINT}; const auto count = std::min(attribute.components, 1u); return {formats[count - 1], count * 4u, 4u, "i32"}; }
case 22: { const std::array formats{VK_FORMAT_R32_SFLOAT}; const auto count = std::min(attribute.components, 1u); return {formats[count - 1], count * 4u, 4u, "f32"}; }
case 23: { const std::array formats{VK_FORMAT_R16_UNORM, VK_FORMAT_R16G16_UNORM}; const auto count = std::min(attribute.components, 2u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 24: { const std::array formats{VK_FORMAT_R16_SNORM, VK_FORMAT_R16G16_SNORM}; const auto count = std::min(attribute.components, 2u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 25: { const std::array formats{VK_FORMAT_R16_USCALED, VK_FORMAT_R16G16_USCALED}; const auto count = std::min(attribute.components, 2u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 26: { const std::array formats{VK_FORMAT_R16_SSCALED, VK_FORMAT_R16G16_SSCALED}; const auto count = std::min(attribute.components, 2u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 27: { const std::array formats{VK_FORMAT_R16_UINT, VK_FORMAT_R16G16_UINT}; const auto count = std::min(attribute.components, 2u); return {formats[count - 1], count * 2u, 2u, "u32"}; }
case 28: { const std::array formats{VK_FORMAT_R16_SINT, VK_FORMAT_R16G16_SINT}; const auto count = std::min(attribute.components, 2u); return {formats[count - 1], count * 2u, 2u, "i32"}; }
case 29: { const std::array formats{VK_FORMAT_R16_SFLOAT, VK_FORMAT_R16G16_SFLOAT}; const auto count = std::min(attribute.components, 2u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 1: { const std::array formats{VK_FORMAT_R8_UNORM}; const auto count = std::min(components, 1u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 2: { const std::array formats{VK_FORMAT_R8_SNORM}; const auto count = std::min(components, 1u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 3: { const std::array formats{VK_FORMAT_R8_USCALED}; const auto count = std::min(components, 1u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 4: { const std::array formats{VK_FORMAT_R8_SSCALED}; const auto count = std::min(components, 1u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 5: { const std::array formats{VK_FORMAT_R8_UINT}; const auto count = std::min(components, 1u); return {formats[count - 1], count * 1u, 1u, "u32"}; }
case 6: { const std::array formats{VK_FORMAT_R8_SINT}; const auto count = std::min(components, 1u); return {formats[count - 1], count * 1u, 1u, "i32"}; }
case 7: { const std::array formats{VK_FORMAT_R16_UNORM}; const auto count = std::min(components, 1u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 8: { const std::array formats{VK_FORMAT_R16_SNORM}; const auto count = std::min(components, 1u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 9: { const std::array formats{VK_FORMAT_R16_USCALED}; const auto count = std::min(components, 1u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 10: { const std::array formats{VK_FORMAT_R16_SSCALED}; const auto count = std::min(components, 1u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 11: { const std::array formats{VK_FORMAT_R16_UINT}; const auto count = std::min(components, 1u); return {formats[count - 1], count * 2u, 2u, "u32"}; }
case 12: { const std::array formats{VK_FORMAT_R16_SINT}; const auto count = std::min(components, 1u); return {formats[count - 1], count * 2u, 2u, "i32"}; }
case 13: { const std::array formats{VK_FORMAT_R16_SFLOAT}; const auto count = std::min(components, 1u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 14: { const std::array formats{VK_FORMAT_R8_UNORM, VK_FORMAT_R8G8_UNORM}; const auto count = std::min(components, 2u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 15: { const std::array formats{VK_FORMAT_R8_SNORM, VK_FORMAT_R8G8_SNORM}; const auto count = std::min(components, 2u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 16: { const std::array formats{VK_FORMAT_R8_USCALED, VK_FORMAT_R8G8_USCALED}; const auto count = std::min(components, 2u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 17: { const std::array formats{VK_FORMAT_R8_SSCALED, VK_FORMAT_R8G8_SSCALED}; const auto count = std::min(components, 2u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 18: { const std::array formats{VK_FORMAT_R8_UINT, VK_FORMAT_R8G8_UINT}; const auto count = std::min(components, 2u); return {formats[count - 1], count * 1u, 1u, "u32"}; }
case 19: { const std::array formats{VK_FORMAT_R8_SINT, VK_FORMAT_R8G8_SINT}; const auto count = std::min(components, 2u); return {formats[count - 1], count * 1u, 1u, "i32"}; }
case 20: { const std::array formats{VK_FORMAT_R32_UINT}; const auto count = std::min(components, 1u); return {formats[count - 1], count * 4u, 4u, "u32"}; }
case 21: { const std::array formats{VK_FORMAT_R32_SINT}; const auto count = std::min(components, 1u); return {formats[count - 1], count * 4u, 4u, "i32"}; }
case 22: { const std::array formats{VK_FORMAT_R32_SFLOAT}; const auto count = std::min(components, 1u); return {formats[count - 1], count * 4u, 4u, "f32"}; }
case 23: { const std::array formats{VK_FORMAT_R16_UNORM, VK_FORMAT_R16G16_UNORM}; const auto count = std::min(components, 2u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 24: { const std::array formats{VK_FORMAT_R16_SNORM, VK_FORMAT_R16G16_SNORM}; const auto count = std::min(components, 2u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 25: { const std::array formats{VK_FORMAT_R16_USCALED, VK_FORMAT_R16G16_USCALED}; const auto count = std::min(components, 2u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 26: { const std::array formats{VK_FORMAT_R16_SSCALED, VK_FORMAT_R16G16_SSCALED}; const auto count = std::min(components, 2u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 27: { const std::array formats{VK_FORMAT_R16_UINT, VK_FORMAT_R16G16_UINT}; const auto count = std::min(components, 2u); return {formats[count - 1], count * 2u, 2u, "u32"}; }
case 28: { const std::array formats{VK_FORMAT_R16_SINT, VK_FORMAT_R16G16_SINT}; const auto count = std::min(components, 2u); return {formats[count - 1], count * 2u, 2u, "i32"}; }
case 29: { const std::array formats{VK_FORMAT_R16_SFLOAT, VK_FORMAT_R16G16_SFLOAT}; const auto count = std::min(components, 2u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 36: return {VK_FORMAT_B10G11R11_UFLOAT_PACK32, 4u, 4u, "f32"};
case 50: return {VK_FORMAT_A2B10G10R10_UNORM_PACK32, 4u, 4u, "f32"};
case 51: return {VK_FORMAT_A2B10G10R10_SNORM_PACK32, 4u, 4u, "f32"};
@ -58,28 +60,28 @@ inline VertexFormat DecodeVertexFormat(const ShaderRecompiler::VertexAttribute&
case 53: return {VK_FORMAT_A2B10G10R10_SSCALED_PACK32, 4u, 4u, "f32"};
case 54: return {VK_FORMAT_A2B10G10R10_UINT_PACK32, 4u, 4u, "u32"};
case 55: return {VK_FORMAT_A2B10G10R10_SINT_PACK32, 4u, 4u, "i32"};
case 56: { const std::array formats{VK_FORMAT_R8_UNORM, VK_FORMAT_R8G8_UNORM, VK_FORMAT_R8G8B8_UNORM, VK_FORMAT_R8G8B8A8_UNORM}; const auto count = std::min(attribute.components, 4u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 57: { const std::array formats{VK_FORMAT_R8_SNORM, VK_FORMAT_R8G8_SNORM, VK_FORMAT_R8G8B8_SNORM, VK_FORMAT_R8G8B8A8_SNORM}; const auto count = std::min(attribute.components, 4u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 58: { const std::array formats{VK_FORMAT_R8_USCALED, VK_FORMAT_R8G8_USCALED, VK_FORMAT_R8G8B8_USCALED, VK_FORMAT_R8G8B8A8_USCALED}; const auto count = std::min(attribute.components, 4u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 59: { const std::array formats{VK_FORMAT_R8_SSCALED, VK_FORMAT_R8G8_SSCALED, VK_FORMAT_R8G8B8_SSCALED, VK_FORMAT_R8G8B8A8_SSCALED}; const auto count = std::min(attribute.components, 4u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 60: { const std::array formats{VK_FORMAT_R8_UINT, VK_FORMAT_R8G8_UINT, VK_FORMAT_R8G8B8_UINT, VK_FORMAT_R8G8B8A8_UINT}; const auto count = std::min(attribute.components, 4u); return {formats[count - 1], count * 1u, 1u, "u32"}; }
case 61: { const std::array formats{VK_FORMAT_R8_SINT, VK_FORMAT_R8G8_SINT, VK_FORMAT_R8G8B8_SINT, VK_FORMAT_R8G8B8A8_SINT}; const auto count = std::min(attribute.components, 4u); return {formats[count - 1], count * 1u, 1u, "i32"}; }
case 62: { const std::array formats{VK_FORMAT_R32_UINT, VK_FORMAT_R32G32_UINT}; const auto count = std::min(attribute.components, 2u); return {formats[count - 1], count * 4u, 4u, "u32"}; }
case 63: { const std::array formats{VK_FORMAT_R32_SINT, VK_FORMAT_R32G32_SINT}; const auto count = std::min(attribute.components, 2u); return {formats[count - 1], count * 4u, 4u, "i32"}; }
case 64: { const std::array formats{VK_FORMAT_R32_SFLOAT, VK_FORMAT_R32G32_SFLOAT}; const auto count = std::min(attribute.components, 2u); return {formats[count - 1], count * 4u, 4u, "f32"}; }
case 65: { const std::array formats{VK_FORMAT_R16_UNORM, VK_FORMAT_R16G16_UNORM, VK_FORMAT_R16G16B16_UNORM, VK_FORMAT_R16G16B16A16_UNORM}; const auto count = std::min(attribute.components, 4u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 66: { const std::array formats{VK_FORMAT_R16_SNORM, VK_FORMAT_R16G16_SNORM, VK_FORMAT_R16G16B16_SNORM, VK_FORMAT_R16G16B16A16_SNORM}; const auto count = std::min(attribute.components, 4u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 67: { const std::array formats{VK_FORMAT_R16_USCALED, VK_FORMAT_R16G16_USCALED, VK_FORMAT_R16G16B16_USCALED, VK_FORMAT_R16G16B16A16_USCALED}; const auto count = std::min(attribute.components, 4u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 68: { const std::array formats{VK_FORMAT_R16_SSCALED, VK_FORMAT_R16G16_SSCALED, VK_FORMAT_R16G16B16_SSCALED, VK_FORMAT_R16G16B16A16_SSCALED}; const auto count = std::min(attribute.components, 4u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 69: { const std::array formats{VK_FORMAT_R16_UINT, VK_FORMAT_R16G16_UINT, VK_FORMAT_R16G16B16_UINT, VK_FORMAT_R16G16B16A16_UINT}; const auto count = std::min(attribute.components, 4u); return {formats[count - 1], count * 2u, 2u, "u32"}; }
case 70: { const std::array formats{VK_FORMAT_R16_SINT, VK_FORMAT_R16G16_SINT, VK_FORMAT_R16G16B16_SINT, VK_FORMAT_R16G16B16A16_SINT}; const auto count = std::min(attribute.components, 4u); return {formats[count - 1], count * 2u, 2u, "i32"}; }
case 71: { const std::array formats{VK_FORMAT_R16_SFLOAT, VK_FORMAT_R16G16_SFLOAT, VK_FORMAT_R16G16B16_SFLOAT, VK_FORMAT_R16G16B16A16_SFLOAT}; const auto count = std::min(attribute.components, 4u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 72: { const std::array formats{VK_FORMAT_R32_UINT, VK_FORMAT_R32G32_UINT, VK_FORMAT_R32G32B32_UINT}; const auto count = std::min(attribute.components, 3u); return {formats[count - 1], count * 4u, 4u, "u32"}; }
case 73: { const std::array formats{VK_FORMAT_R32_SINT, VK_FORMAT_R32G32_SINT, VK_FORMAT_R32G32B32_SINT}; const auto count = std::min(attribute.components, 3u); return {formats[count - 1], count * 4u, 4u, "i32"}; }
case 74: { const std::array formats{VK_FORMAT_R32_SFLOAT, VK_FORMAT_R32G32_SFLOAT, VK_FORMAT_R32G32B32_SFLOAT}; const auto count = std::min(attribute.components, 3u); return {formats[count - 1], count * 4u, 4u, "f32"}; }
case 75: { const std::array formats{VK_FORMAT_R32_UINT, VK_FORMAT_R32G32_UINT, VK_FORMAT_R32G32B32_UINT, VK_FORMAT_R32G32B32A32_UINT}; const auto count = std::min(attribute.components, 4u); return {formats[count - 1], count * 4u, 4u, "u32"}; }
case 76: { const std::array formats{VK_FORMAT_R32_SINT, VK_FORMAT_R32G32_SINT, VK_FORMAT_R32G32B32_SINT, VK_FORMAT_R32G32B32A32_SINT}; const auto count = std::min(attribute.components, 4u); return {formats[count - 1], count * 4u, 4u, "i32"}; }
case 77: { const std::array formats{VK_FORMAT_R32_SFLOAT, VK_FORMAT_R32G32_SFLOAT, VK_FORMAT_R32G32B32_SFLOAT, VK_FORMAT_R32G32B32A32_SFLOAT}; const auto count = std::min(attribute.components, 4u); return {formats[count - 1], count * 4u, 4u, "f32"}; }
case 56: { const std::array formats{VK_FORMAT_R8_UNORM, VK_FORMAT_R8G8_UNORM, VK_FORMAT_R8G8B8_UNORM, VK_FORMAT_R8G8B8A8_UNORM}; const auto count = std::min(components, 4u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 57: { const std::array formats{VK_FORMAT_R8_SNORM, VK_FORMAT_R8G8_SNORM, VK_FORMAT_R8G8B8_SNORM, VK_FORMAT_R8G8B8A8_SNORM}; const auto count = std::min(components, 4u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 58: { const std::array formats{VK_FORMAT_R8_USCALED, VK_FORMAT_R8G8_USCALED, VK_FORMAT_R8G8B8_USCALED, VK_FORMAT_R8G8B8A8_USCALED}; const auto count = std::min(components, 4u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 59: { const std::array formats{VK_FORMAT_R8_SSCALED, VK_FORMAT_R8G8_SSCALED, VK_FORMAT_R8G8B8_SSCALED, VK_FORMAT_R8G8B8A8_SSCALED}; const auto count = std::min(components, 4u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 60: { const std::array formats{VK_FORMAT_R8_UINT, VK_FORMAT_R8G8_UINT, VK_FORMAT_R8G8B8_UINT, VK_FORMAT_R8G8B8A8_UINT}; const auto count = std::min(components, 4u); return {formats[count - 1], count * 1u, 1u, "u32"}; }
case 61: { const std::array formats{VK_FORMAT_R8_SINT, VK_FORMAT_R8G8_SINT, VK_FORMAT_R8G8B8_SINT, VK_FORMAT_R8G8B8A8_SINT}; const auto count = std::min(components, 4u); return {formats[count - 1], count * 1u, 1u, "i32"}; }
case 62: { const std::array formats{VK_FORMAT_R32_UINT, VK_FORMAT_R32G32_UINT}; const auto count = std::min(components, 2u); return {formats[count - 1], count * 4u, 4u, "u32"}; }
case 63: { const std::array formats{VK_FORMAT_R32_SINT, VK_FORMAT_R32G32_SINT}; const auto count = std::min(components, 2u); return {formats[count - 1], count * 4u, 4u, "i32"}; }
case 64: { const std::array formats{VK_FORMAT_R32_SFLOAT, VK_FORMAT_R32G32_SFLOAT}; const auto count = std::min(components, 2u); return {formats[count - 1], count * 4u, 4u, "f32"}; }
case 65: { const std::array formats{VK_FORMAT_R16_UNORM, VK_FORMAT_R16G16_UNORM, VK_FORMAT_R16G16B16_UNORM, VK_FORMAT_R16G16B16A16_UNORM}; const auto count = std::min(components, 4u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 66: { const std::array formats{VK_FORMAT_R16_SNORM, VK_FORMAT_R16G16_SNORM, VK_FORMAT_R16G16B16_SNORM, VK_FORMAT_R16G16B16A16_SNORM}; const auto count = std::min(components, 4u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 67: { const std::array formats{VK_FORMAT_R16_USCALED, VK_FORMAT_R16G16_USCALED, VK_FORMAT_R16G16B16_USCALED, VK_FORMAT_R16G16B16A16_USCALED}; const auto count = std::min(components, 4u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 68: { const std::array formats{VK_FORMAT_R16_SSCALED, VK_FORMAT_R16G16_SSCALED, VK_FORMAT_R16G16B16_SSCALED, VK_FORMAT_R16G16B16A16_SSCALED}; const auto count = std::min(components, 4u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 69: { const std::array formats{VK_FORMAT_R16_UINT, VK_FORMAT_R16G16_UINT, VK_FORMAT_R16G16B16_UINT, VK_FORMAT_R16G16B16A16_UINT}; const auto count = std::min(components, 4u); return {formats[count - 1], count * 2u, 2u, "u32"}; }
case 70: { const std::array formats{VK_FORMAT_R16_SINT, VK_FORMAT_R16G16_SINT, VK_FORMAT_R16G16B16_SINT, VK_FORMAT_R16G16B16A16_SINT}; const auto count = std::min(components, 4u); return {formats[count - 1], count * 2u, 2u, "i32"}; }
case 71: { const std::array formats{VK_FORMAT_R16_SFLOAT, VK_FORMAT_R16G16_SFLOAT, VK_FORMAT_R16G16B16_SFLOAT, VK_FORMAT_R16G16B16A16_SFLOAT}; const auto count = std::min(components, 4u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 72: { const std::array formats{VK_FORMAT_R32_UINT, VK_FORMAT_R32G32_UINT, VK_FORMAT_R32G32B32_UINT}; const auto count = std::min(components, 3u); return {formats[count - 1], count * 4u, 4u, "u32"}; }
case 73: { const std::array formats{VK_FORMAT_R32_SINT, VK_FORMAT_R32G32_SINT, VK_FORMAT_R32G32B32_SINT}; const auto count = std::min(components, 3u); return {formats[count - 1], count * 4u, 4u, "i32"}; }
case 74: { const std::array formats{VK_FORMAT_R32_SFLOAT, VK_FORMAT_R32G32_SFLOAT, VK_FORMAT_R32G32B32_SFLOAT}; const auto count = std::min(components, 3u); return {formats[count - 1], count * 4u, 4u, "f32"}; }
case 75: { const std::array formats{VK_FORMAT_R32_UINT, VK_FORMAT_R32G32_UINT, VK_FORMAT_R32G32B32_UINT, VK_FORMAT_R32G32B32A32_UINT}; const auto count = std::min(components, 4u); return {formats[count - 1], count * 4u, 4u, "u32"}; }
case 76: { const std::array formats{VK_FORMAT_R32_SINT, VK_FORMAT_R32G32_SINT, VK_FORMAT_R32G32B32_SINT, VK_FORMAT_R32G32B32A32_SINT}; const auto count = std::min(components, 4u); return {formats[count - 1], count * 4u, 4u, "i32"}; }
case 77: { const std::array formats{VK_FORMAT_R32_SFLOAT, VK_FORMAT_R32G32_SFLOAT, VK_FORMAT_R32G32B32_SFLOAT, VK_FORMAT_R32G32B32A32_SFLOAT}; const auto count = std::min(components, 4u); return {formats[count - 1], count * 4u, 4u, "f32"}; }
default: throw std::runtime_error("AGC graphics: unsupported vertex format " + std::to_string(format));
}
}

View File

@ -27,6 +27,16 @@ std::uint32_t standardOffset(std::uint32_t x, std::uint32_t y, std::uint32_t ele
}
}
std::uint32_t standard64Extra(std::uint32_t x, std::uint32_t y, std::uint32_t elementBytes) {
switch (elementBytes) {
case 1u: return ((x << 7) & 0x2000u) ^ ((x << 8) & 0x8000u) ^ ((y << 6) & 0x1000u) ^ ((y << 7) & 0x4000u);
case 2u: return ((x << 7) & 0x2000u) ^ ((x << 8) & 0x8000u) ^ ((y << 7) & 0x1000u) ^ ((y << 8) & 0x4000u);
case 4u: return ((x << 8) & 0x2000u) ^ ((x << 9) & 0x8000u) ^ ((y << 7) & 0x1000u) ^ ((y << 8) & 0x4000u);
case 8u: return ((x << 8) & 0x2000u) ^ ((x << 9) & 0x8000u) ^ ((y << 8) & 0x1000u) ^ ((y << 9) & 0x4000u);
default: return ((x << 9) & 0x2000u) ^ ((x << 10) & 0x8000u) ^ ((y << 8) & 0x1000u) ^ ((y << 9) & 0x4000u);
}
}
}
ColorTileMode DecodeColorTileMode(std::uint32_t attrib3) {
@ -35,7 +45,7 @@ ColorTileMode DecodeColorTileMode(std::uint32_t attrib3) {
const auto mode = (attrib3 >> 14u) & 0x1fu;
const auto fmaskMode = (attrib3 >> 19u) & 0x1fu;
require(fmaskMode == 0 || fmaskMode == 0x18, "AGC graphics: unsupported color FMASK swizzle mode");
require(mode == 0 || mode == 5 || mode == 0x1b, "AGC graphics: unsupported color tile mode");
require(mode == 0 || mode == 5 || mode == 9 || mode == 0x1b, "AGC graphics: unsupported color tile mode");
return static_cast<ColorTileMode>(mode);
}
@ -70,18 +80,18 @@ const SwizzleTables& renderTargetTables(std::uint32_t bytesPerElement, std::uint
return tables[index];
}
const SwizzleTables& standardTables(std::uint32_t bytesPerElement, std::uint32_t blockWidth, std::uint32_t blockHeight) {
static std::once_flag once[5];
static SwizzleTables tables[5];
const SwizzleTables& standardTables(std::uint32_t bytesPerElement, std::uint32_t blockWidth, std::uint32_t blockHeight, bool block64KB) {
static std::once_flag once[2][5];
static SwizzleTables tables[2][5];
const auto index = static_cast<std::size_t>(std::countr_zero(bytesPerElement));
std::call_once(once[index], [&] {
auto& table = tables[index];
std::call_once(once[block64KB][index], [&] {
auto& table = tables[block64KB][index];
table.x.resize(blockWidth);
table.y.resize(blockHeight);
for (std::uint32_t x = 0; x < blockWidth; ++x) table.x[x] = standardOffset(x, 0, bytesPerElement);
for (std::uint32_t y = 0; y < blockHeight; ++y) table.y[y] = standardOffset(0, y, bytesPerElement);
for (std::uint32_t x = 0; x < blockWidth; ++x) table.x[x] = standardOffset(x, 0, bytesPerElement) ^ (block64KB ? standard64Extra(x, 0, bytesPerElement) : 0u);
for (std::uint32_t y = 0; y < blockHeight; ++y) table.y[y] = standardOffset(0, y, bytesPerElement) ^ (block64KB ? standard64Extra(0, y, bytesPerElement) : 0u);
});
return tables[index];
return tables[block64KB][index];
}
}
@ -108,13 +118,16 @@ ColorTargetLayout::ColorTargetLayout(std::uint32_t width, std::uint32_t height,
yOffsets = tables.y.data();
break;
}
case ColorTileMode::Standard4KB: {
case ColorTileMode::Standard4KB:
case ColorTileMode::Standard64KB: {
const auto log2Bytes = static_cast<std::uint32_t>(std::countr_zero(bytesPerElement));
blockWidth = 1u << (6u - (log2Bytes + 1u) / 2u);
blockHeight = 1u << (6u - log2Bytes / 2u);
const bool block64KB = mode == ColorTileMode::Standard64KB;
const auto log2Side = block64KB ? 8u : 6u;
blockWidth = 1u << (log2Side - log2Bytes / 2u);
blockHeight = 1u << (log2Side - (log2Bytes + 1u) / 2u);
pitch = (width + blockWidth - 1u) / blockWidth * blockWidth;
paddedHeight = (height + blockHeight - 1u) / blockHeight * blockHeight;
const auto& tables = standardTables(bytesPerElement, blockWidth, blockHeight);
const auto& tables = standardTables(bytesPerElement, blockWidth, blockHeight, block64KB);
xOffsets = tables.x.data();
yOffsets = tables.y.data();
break;

View File

@ -44,6 +44,18 @@ bool AllKeysEqual(const std::uint8_t* keys, std::size_t count, std::uint8_t firs
return true;
}
DccKeys ByteKeys(std::uint8_t key) {
switch (key) {
case 0x00: return DccKeys::Clear0000;
case 0x40: return DccKeys::Clear0001;
case 0x80: return DccKeys::Clear1110;
case 0xc0: return DccKeys::Clear1111;
case 0x20: return DccKeys::ClearRegister;
case 0xff: return DccKeys::Uncompressed;
default: return DccKeys::Mixed;
}
}
// APS5_PROFILE_DRAW: key scans, their bytes and time, printed as [dcc] every 10 s, with the hits of
// the GPU-store memo below and the "uncompressed" key stores by where they were made. Flush syncs
// stay 0 (no key read syncs the recorder); the field is kept so the line's shape does not change.
@ -226,7 +238,7 @@ bool StoreUncompressedOnGpu(const Context& context, Recorder& recorder, std::uin
// Queued on the open batch and recorded with the batch's other key stores as one run (or before
// a later command writing the keys; see Recorder::QueueKeyStore).
recorder.QueueKeyStore(import->buffer, first, last, std::move(seed), begin, begin + count);
recorder.NotePendingWrite(begin, count);
recorder.NotePendingFill(begin, count, 0xff);
GuestMemory::MarkWritten(begin, count);
// After the note: an unlocked memo lookup drops an entry the snapshot does not cover yet. The
// entry is kept only for the active recorder (the one the flush hook and the lookups consult).
@ -380,13 +392,27 @@ std::size_t DccKeyBytes(std::uint64_t surfaceBytes) {
return static_cast<std::size_t>(surfaceBytes / KeyBytes);
}
std::size_t DccKeyCount(TextureTileMode tileMode, std::uint32_t elementBytes, std::uint32_t width, std::uint32_t height, std::uint64_t surfaceBytes) {
const auto keys = DccKeyBytes(surfaceBytes);
if (tileMode != TextureTileMode::kR64KBX || elementBytes != 4) return keys;
constexpr std::uint64_t MetaBlockEdge = 512;
constexpr std::uint64_t MetaBlockBytes = 4096;
const auto console = (width + MetaBlockEdge - 1) / MetaBlockEdge * ((height + MetaBlockEdge - 1) / MetaBlockEdge) * MetaBlockBytes;
return std::max(keys, static_cast<std::size_t>(console));
}
std::size_t DccKeyCount(const GuestTextureResource& surface, std::uint64_t surfaceBytes) {
const bool single = surface.mipCount == 1 && surface.baseArray == 0 && surface.depthOrLastArray == 0 && (surface.dimension == TextureDimension::k2D || surface.dimension == TextureDimension::k2DArray);
if (!single || !surface.dccPipeAligned || surface.tileMode != TextureTileMode::kR64KBX) return DccKeyBytes(surfaceBytes);
return DccKeyCount(surface.tileMode, BytesPerElement(surface.format), surface.width, surface.height, surfaceBytes);
}
namespace {
// ReadDccKeys, saying in `memoized` whether the answer came from the pending-store memo rather
// than the bytes.
DccKeys readDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes, bool& memoized) {
DccKeys readDccKeys(std::uint64_t metaAddress, std::size_t count, bool& memoized) {
memoized = false;
const auto count = static_cast<std::size_t>(surfaceBytes / KeyBytes);
if (metaAddress == 0 || count == 0 || !GuestMemory::Accessible(reinterpret_cast<const void*>(metaAddress), count)) return DccKeys::Unreadable;
// A driver store still pending on the GPU is not in the bytes yet.
if (const auto memo = MemoizedKeys(metaAddress, metaAddress + count)) {
@ -412,22 +438,13 @@ DccKeys readDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes, bool&
const auto start = ScanProfileEnabled() ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
const bool uniform = AllKeysEqual(keys + 1, count - 1, first);
if (ScanProfileEnabled()) CountScan(count, start);
if (!uniform) return DccKeys::Mixed;
switch (first) {
case 0x00: return DccKeys::Clear0000;
case 0x40: return DccKeys::Clear0001;
case 0x80: return DccKeys::Clear1110;
case 0xc0: return DccKeys::Clear1111;
case 0x20: return DccKeys::ClearRegister;
case 0xff: return DccKeys::Uncompressed;
default: return DccKeys::Mixed;
}
return uniform ? ByteKeys(first) : DccKeys::Mixed;
}
DccKeys textureClearKeys(const GuestTextureResource& resource, std::uint64_t guestBytes, bool& memoized) {
memoized = false;
if (resource.dccAddress == 0) return DccKeys::Uncompressed;
const auto keys = readDccKeys(resource.dccAddress, guestBytes, memoized);
const auto keys = readDccKeys(resource.dccAddress, DccKeyBytes(guestBytes), memoized);
if (keys == DccKeys::Uncompressed) 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]{};
@ -441,15 +458,7 @@ DccKeys textureClearKeys(const GuestTextureResource& resource, std::uint64_t gue
return keys;
}
}
DccKeys ReadDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes) {
bool memoized = false;
return readDccKeys(metaAddress, surfaceBytes, memoized);
}
DccKeys CurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes) {
const auto count = static_cast<std::size_t>(surfaceBytes / KeyBytes);
DccKeys currentDccKeys(std::uint64_t metaAddress, std::size_t count) {
if (metaAddress != 0 && count != 0 && GuestMemory::GpuMutex().HeldByThisThread()) {
if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->PendingWriteOverlaps(metaAddress, count)) {
if (const auto keys = PendingStoreKeys(*recorder, metaAddress, count)) return *keys;
@ -457,7 +466,35 @@ DccKeys CurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes) {
recorder->SyncThrough(metaAddress, count);
}
}
return ReadDccKeys(metaAddress, surfaceBytes);
bool memoized = false;
return readDccKeys(metaAddress, count, memoized);
}
std::size_t StoredKeyCount(std::uint64_t metaAddress, std::uint64_t surfaceBytes, std::size_t keyCount) {
const auto keys = DccKeyBytes(surfaceBytes);
Require(keyCount >= keys, "a DCC key range covers fewer keys than one per 256 surface bytes");
if (metaAddress == 0 || keyCount == 0) return 0;
if (GuestMemory::Accessible(reinterpret_cast<const void*>(metaAddress), keyCount, true)) return keyCount;
if (keyCount == keys) return 0;
char message[256];
std::snprintf(message, sizeof(message), "AGC graphics: DCC metadata 0x%llx is not writable over the 0x%zx key bytes of its surface (0x%zx at one per 256 bytes): metadata smaller than the console's DCC extent is not modeled", static_cast<unsigned long long>(metaAddress), keyCount, keys);
throw std::runtime_error(message);
}
}
DccKeys ReadDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes) {
bool memoized = false;
return readDccKeys(metaAddress, DccKeyBytes(surfaceBytes), memoized);
}
DccKeys CurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes) {
return currentDccKeys(metaAddress, DccKeyBytes(surfaceBytes));
}
DccKeys CurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes, std::size_t keyCount) {
Require(keyCount >= DccKeyBytes(surfaceBytes), "a DCC key range covers fewer keys than one per 256 surface bytes");
return currentDccKeys(metaAddress, keyCount);
}
bool IsDccClear(DccKeys keys) {
@ -465,8 +502,12 @@ bool IsDccClear(DccKeys keys) {
}
void MarkDccUncompressed(std::uint64_t metaAddress, std::uint64_t surfaceBytes) {
const auto count = static_cast<std::size_t>(surfaceBytes / KeyBytes);
if (metaAddress == 0 || count == 0 || !GuestMemory::Accessible(reinterpret_cast<const void*>(metaAddress), count, true)) return;
MarkDccUncompressed(metaAddress, surfaceBytes, DccKeyBytes(surfaceBytes));
}
void MarkDccUncompressed(std::uint64_t metaAddress, std::uint64_t surfaceBytes, std::size_t keyCount) {
const auto count = StoredKeyCount(metaAddress, surfaceBytes, keyCount);
if (count == 0) return;
// The bytes are trusted only when no recorded work writes them (lock-free, with or without
// GuestMemory::GpuMutex); otherwise the store runs and waits through the flush hook. A pending
// GPU store of the driver's own is no reason to skip: a title kernel recorded into the same
@ -476,8 +517,12 @@ void MarkDccUncompressed(std::uint64_t metaAddress, std::uint64_t surfaceBytes)
}
void MarkDccUncompressed(const Context& context, std::uint64_t metaAddress, std::uint64_t surfaceBytes) {
const auto count = static_cast<std::size_t>(surfaceBytes / KeyBytes);
if (metaAddress == 0 || count == 0 || !GuestMemory::Accessible(reinterpret_cast<const void*>(metaAddress), count, true)) return;
MarkDccUncompressed(context, metaAddress, surfaceBytes, DccKeyBytes(surfaceBytes));
}
void MarkDccUncompressed(const Context& context, std::uint64_t metaAddress, std::uint64_t surfaceBytes, std::size_t keyCount) {
const auto count = StoredKeyCount(metaAddress, surfaceBytes, keyCount);
if (count == 0) return;
auto* recorder = CpuKeysOnly() || !GuestMemory::GpuMutex().HeldByThisThread() ? nullptr : Recorder::Active();
// The bytes are scanned only when no recorded work writes them: a pending clear kernel's keys are
// not in memory yet, so the bytes could read as uncompressed while the kernel will store a clear
@ -583,4 +628,63 @@ void ReadTextureSurface(const GuestTextureResource& resource, DccKeys keys, std:
else FillDccClear(ResolveTextureFormat(resource.format), keys, resource.dccAlphaOnMsb, bytes);
}
namespace {
DccKeys EncodedKeys(const GuestTextureResource& resource, DccKeys keys) {
std::byte probe[16]{};
if (!IsDccClear(keys) || !FillDccClear(ResolveTextureFormat(resource.format), keys, resource.dccAlphaOnMsb, std::span(probe, std::min<std::size_t>(sizeof(probe), BytesPerElement(resource.format))))) return DccKeys::Uncompressed;
return keys;
}
bool PendingFillsLeaveUncompressed(const Recorder& recorder, const GuestTextureResource& resource, std::size_t count) {
const auto begin = resource.dccAddress;
if (!GuestMemory::Accessible(reinterpret_cast<const void*>(begin), count) || AnyShadowedOverlaps(begin, count)) return false;
auto writes = recorder.PendingWritesOver(begin, count);
std::sort(writes.begin(), writes.end(), [](const Recorder::PendingWrite& a, const Recorder::PendingWrite& b) { return a.note > b.note; });
std::vector<std::pair<std::uint64_t, std::uint64_t>> unwritten{{begin, begin + count}};
int value = -1;
bool mixed = false;
const auto see = [&](int key) {
if (value < 0) value = key;
else if (value != key) mixed = true;
};
for (const auto& write : writes) {
if (mixed || unwritten.empty()) break;
std::vector<std::pair<std::uint64_t, std::uint64_t>> rest;
bool covered = false;
for (const auto& [from, to] : unwritten) {
if (write.end <= from || to <= write.begin) {
rest.emplace_back(from, to);
continue;
}
covered = true;
if (from < write.begin) rest.emplace_back(from, write.begin);
if (write.end < to) rest.emplace_back(write.end, to);
}
unwritten = std::move(rest);
if (covered && write.value >= 0) see(write.value);
}
for (const auto& [from, to] : unwritten) {
if (mixed) break;
const auto* keys = reinterpret_cast<const std::uint8_t*>(from);
if (AllKeysEqual(keys + 1, static_cast<std::size_t>(to - from) - 1, keys[0])) see(keys[0]);
else mixed = true;
}
return mixed || (value >= 0 && EncodedKeys(resource, ByteKeys(static_cast<std::uint8_t>(value))) == DccKeys::Uncompressed);
}
}
std::optional<DccKeys> WaitForKeyWriters(const GuestTextureResource& resource, std::uint64_t guestBytes) {
const auto count = static_cast<std::size_t>(guestBytes / KeyBytes);
const auto metaAddress = resource.dccAddress;
if (metaAddress == 0 || count == 0 || !GuestMemory::GpuMutex().HeldByThisThread()) return std::nullopt;
auto* recorder = Recorder::Active();
if (recorder == nullptr || !recorder->PendingWriteOverlaps(metaAddress, count) || PendingStoreKeys(*recorder, metaAddress, count).has_value()) return std::nullopt;
if (PendingFillsLeaveUncompressed(*recorder, resource, count)) return DccKeys::Uncompressed;
Recorder::CountSync(2);
recorder->SyncThrough(metaAddress, count);
return std::nullopt;
}
}

View File

@ -42,7 +42,7 @@ std::uint32_t GuestFormatFor(VkFormat format, std::uint32_t elementBytes) {
// 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::RenderTarget || color.tileMode == ColorTileMode::Standard4KB, "only 4 KiB standard and 64 KiB tiled color targets are resident");
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;
@ -62,6 +62,7 @@ GuestTextureResource SurfaceForTarget(const ColorTarget& color) {
surface.dstSelW = 7;
surface.dccAddress = color.dccAddress;
surface.dccAlphaOnMsb = color.dccAlphaOnMsb;
surface.dccPipeAligned = color.dccPipeAligned;
return surface;
}
@ -69,6 +70,11 @@ GuestTextureResource SurfaceForTarget(const ColorTarget& color) {
namespace {
std::size_t colorKeyCount(const ColorTarget& color, std::size_t bytes) {
if (color.mipCount != 1 || color.depth != 1 || !color.dccPipeAligned) return DccKeyBytes(bytes);
return DccKeyCount(ColorTextureTileMode(color.tileMode), color.elementBytes, color.extent.width, color.extent.height, bytes);
}
std::array<std::byte, 16> clearTexel(const ColorTarget& color, DccKeys keys) {
std::array<std::byte, 16> texel{};
const auto elementBytes = static_cast<std::size_t>(color.elementBytes);
@ -90,16 +96,19 @@ bool clearToTexel(StorageTexture& image, const std::array<std::byte, 16>& texel,
return image.FillClear(std::span<const std::uint32_t, 4>(pattern), StorageTexture::WholeImage, refusal);
}
void writeTexels(const ColorTarget& color, const std::array<std::byte, 16>& texel) {
const auto elementBytes = static_cast<std::size_t>(color.elementBytes);
std::vector<std::byte> texels(color.bytes);
for (std::size_t offset = 0; offset + elementBytes <= texels.size(); offset += elementBytes) std::memcpy(texels.data() + offset, texel.data(), elementBytes);
GuestMemory::Write(color.address, texels);
}
void storeClearTexels(const Context& context, const ColorTarget& color, const std::array<std::byte, 16>& texel) {
StorageTexture::FlushPending(color.address, color.bytes, nullptr, "fast-clear materialization");
const auto keys = ReadDccKeys(color.dccAddress, color.bytes);
if (!IsDccClear(keys)) return;
const auto current = keys == DccKeys::ClearRegister ? texel : clearTexel(color, keys);
const auto elementBytes = static_cast<std::size_t>(color.elementBytes);
std::vector<std::byte> texels(color.bytes);
for (std::size_t offset = 0; offset + elementBytes <= texels.size(); offset += elementBytes) std::memcpy(texels.data() + offset, current.data(), elementBytes);
GuestMemory::Write(color.address, texels);
MarkDccUncompressed(context, color.dccAddress, color.bytes);
writeTexels(color, keys == DccKeys::ClearRegister ? texel : clearTexel(color, keys));
MarkDccUncompressed(context, color.dccAddress, color.bytes, colorKeyCount(color, color.bytes));
}
void materializeRegisterClear(const Context& context, const ColorTarget& color, StorageTexture& resident) {
@ -118,13 +127,31 @@ void materializeRegisterClear(const Context& context, const ColorTarget& color,
}
}
if (cleared) {
MarkDccUncompressed(context, color.dccAddress, color.bytes);
MarkDccUncompressed(context, color.dccAddress, color.bytes, colorKeyCount(color, color.bytes));
return;
}
storeClearTexels(context, color, texel);
resident.Refresh();
}
bool materializeCmaskClear(const Context& context, const ColorTarget& color, StorageTexture* resident) {
if (color.cmaskAddress == 0) return false;
const auto metadataBytes = static_cast<std::uint64_t>(color.cmaskBytes) * 256u;
const auto state = CurrentDccKeys(color.cmaskAddress, metadataBytes);
if (state == DccKeys::Uncompressed) return false;
Require(color.dccAddress == 0, std::string("CMASK of a DCC color target that is not all expanded is not modeled (") + DccKeysName(state) + ")");
Require(state == DccKeys::Clear0000, std::string("CMASK whose tiles are not all fast-cleared or all expanded is not modeled (") + DccKeysName(state) + ")");
const auto texel = clearTexel(color, DccKeys::ClearRegister);
const char* refusal = nullptr;
if (resident == nullptr || !clearToTexel(*resident, texel, color.elementBytes, refusal)) {
StorageTexture::FlushPending(color.address, color.bytes, nullptr, "fast-clear materialization");
writeTexels(color, texel);
if (resident != nullptr) resident->Refresh();
}
MarkDccUncompressed(context, color.cmaskAddress, metadataBytes);
return true;
}
void imageBarrier(const Context& context, VkCommandBuffer commands, VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout, VkPipelineStageFlags sourceStage, VkPipelineStageFlags destinationStage, VkAccessFlags sourceAccess, VkAccessFlags destinationAccess) {
VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
barrier.srcAccessMask = sourceAccess;
@ -438,12 +465,12 @@ bool ValidationKey(const Context& context, std::span<const CompiledShader> shade
Require(shader.program != nullptr, "missing compiled shader");
const auto& program = *shader.program;
const bool generated = state.rectList && (shader.stage == Stage::TessellationControl || shader.stage == Stage::TessellationEvaluation);
if (!generated && program.variantId == 0) {
if (!generated && program.PipelineVariantId() == 0) {
keyed = false;
break;
}
add(shader.stage);
add(generated ? std::uint64_t{0} : program.variantId);
add(generated ? std::uint64_t{0} : program.PipelineVariantId());
add(shader.pushConstantOffset);
add(program.pushConstants.size());
add(program.bdaAbiVersion);
@ -452,6 +479,7 @@ bool ValidationKey(const Context& context, std::span<const CompiledShader> shade
add(attribute.location);
add(attribute.components);
add(attribute.fetchIndex);
add(attribute.formatComponents);
// The data format bits of the V# decide the attribute's signature.
add((attribute.resource.fields[3] >> 12u) & 0x7fu);
}
@ -601,11 +629,12 @@ void CheckBufferAliases(std::span<const CompiledShader> shaders, const ColorTarg
}
std::array<std::uint32_t, 4> MeshIndexBufferDescriptor(const Pm4::DrawParameters& draw, std::uint64_t unreadAddress) {
const auto address = draw.indexed ? draw.indexAddress : unreadAddress;
const auto bytes = draw.indexed ? (static_cast<std::uint64_t>(draw.indexCount) * draw.indexSize + 3u) & ~std::uint64_t{3} : 4u;
Require(address != 0 && bytes != 0 && bytes <= 0xffffffffu && (address >> 48u) == 0, "invalid mesh index buffer range");
std::array<std::uint32_t, 4> MeshIndexBufferDescriptor(const Pm4::DrawParameters& draw) {
constexpr std::uint32_t RawWord3 = 0x31016facu;
if (!draw.indexed) return {0u, 0u, 0u, RawWord3};
const auto address = draw.indexAddress;
const auto bytes = (static_cast<std::uint64_t>(draw.indexCount) * draw.indexSize + 3u) & ~std::uint64_t{3};
Require(address != 0 && bytes != 0 && bytes <= 0xffffffffu && (address >> 48u) == 0, "invalid mesh index buffer range");
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u) & 0xffffu, static_cast<std::uint32_t>(bytes), RawWord3};
}
@ -895,6 +924,7 @@ std::shared_ptr<StorageTexture> refreshResidentTarget(const Context& context, co
std::lock_guard lock(reportMutex);
if (reported.insert(color.address).second) std::fprintf(stderr, "[gpu] color target 0x%llx stays non-resident: %s\n", static_cast<unsigned long long>(color.address), error.what());
}
if (resident != nullptr) materializeCmaskClear(context, color, resident.get());
if (profile) {
const auto lookupUs = std::chrono::duration<double, std::micro>(std::chrono::steady_clock::now() - lookupStart).count();
++outcome.targetLookups;
@ -934,7 +964,7 @@ ResolvedResources resolveDrawResources(const Context& context, const State& stat
// The render target and index ranges stay out of the key (see DrawResourceKey); a hit repeats
// the alias checks instead. Debug aid: APS5_NO_DRAW_KEY_TRIM=1 keys them as before.
static const bool trimKey = std::getenv("APS5_NO_DRAW_KEY_TRIM") == nullptr;
resolved.cacheable = recordable && !noDrawResourceCache && !noTextureCache && std::all_of(shaders.begin(), shaders.end(), [](const CompiledShader& shader) { return shader.program != nullptr && shader.program->variantId != 0; });
resolved.cacheable = recordable && !noDrawResourceCache && !noTextureCache && std::all_of(shaders.begin(), shaders.end(), [](const CompiledShader& shader) { return shader.program != nullptr && shader.program->PipelineVariantId() != 0; });
if (resolved.cacheable) {
resolved.contentKey = DrawResourceKey(context, shaders, state.color, draw.indexAddress, indexBytes, !trimKey);
if (auto cached = SharedResourceCache().Find(resolved.contentKey)) {
@ -1182,6 +1212,7 @@ struct RecordedDraw {
std::shared_ptr<Framebuffer> framebuffer;
std::span<const VkImageView> targetViews;
std::vector<std::shared_ptr<StorageTexture>> targets;
std::vector<std::shared_ptr<StorageTexture>> proxies;
const IndirectRecord* indirect = nullptr;
std::span<const ShaderResources::MovedBuffer> moved;
bool listed = false;
@ -1358,6 +1389,10 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
if (continued) {
record.pipeline->Continue(commands, state);
} else {
for (const auto& proxy : record.proxies) {
proxy->RecordAttachmentProxyLoad(commands, VK_IMAGE_LAYOUT_GENERAL);
countBarrier(2);
}
const VkMemoryBarrier before{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_INDEX_READ_BIT | VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT | VK_ACCESS_UNIFORM_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT};
context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | inputs.shaderStages, 0, 1, &before, 0, nullptr, 0, nullptr);
countBarrier(1);
@ -1391,7 +1426,16 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
// The pass stays open for the next draw of these attachments; the recorder ends it (and the
// draw class range) before anything else is recorded. A draw that wrote memory owes the next
// one a barrier, so its pass cannot be continued.
recorder->LeaveRenderPassOpen(passKey, drawTiming, !resources.WritesMemory());
std::function<void(VkCommandBuffer)> storeProxies;
if (!continued && !record.proxies.empty()) {
storeProxies = [proxies = record.proxies](VkCommandBuffer passCommands) {
for (const auto& proxy : proxies) {
proxy->RecordAttachmentProxyStore(passCommands, VK_IMAGE_LAYOUT_GENERAL);
Recorder::CountBarriers(Recorder::CommandClass::Draw, 2);
}
};
}
recorder->LeaveRenderPassOpen(passKey, drawTiming, !resources.WritesMemory(), std::move(storeProxies));
timer.phase(PhaseRecord);
keepRecordedDraw(*recorder, record.resources, std::move(record.pipeline), std::move(record.framebuffer), inputs, std::move(record.targets), std::move(scratch), std::move(checkRecords), record.listed, record.completion, outcome);
timer.phase(PhaseKeep);
@ -1480,6 +1524,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
// Resident target: the surface's cached storage image is attached directly and marked dirty
// afterwards, so nothing is copied in or out per draw.
std::shared_ptr<StorageTexture> resident;
bool proxied = false;
// Linear pixels converted on the CPU, for targets the GPU detiler does not handle.
std::unique_ptr<Buffer> transfer;
// Guest bytes in host memory, and their device-local copies for the detiler.
@ -1508,7 +1553,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
timer.phase(PhaseSetup);
// Debug aid: APS5_NO_RESIDENT_TARGETS=1 copies every target in and out again.
static const bool residentTargets = std::getenv("APS5_NO_RESIDENT_TARGETS") == nullptr;
if ((binding.gpuTiling || (color.tileMode == ColorTileMode::Standard4KB && context.detiler != nullptr)) && residentTargets) {
if ((binding.gpuTiling || (color.tileMode != ColorTileMode::Linear && context.detiler != nullptr)) && residentTargets) {
// The lookup refreshes the image on every draw (StorageTexture::Refresh: FlushPending,
// CollectWrites over the target's pages, the DCC key scan of TextureClearKeys, then
// UnchangedSince). The page walk is skipped while the worker's collect epoch lasts
@ -1524,9 +1569,11 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
}
if (binding.resident != nullptr) {
timer.phase(PhaseReadTarget);
targetViews.push_back(binding.resident->AttachmentView(color.format, color.mip, color.depthSlice));
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));
continue;
}
materializeCmaskClear(context, color, nullptr);
Require(!color.mipTail, "rendering into a packed mip tail needs the resident image of its surface");
Require(color.depth == 1, "rendering into a 3D color target needs the resident image of its surface");
if (binding.gpuTiling) {
@ -1683,6 +1730,13 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
APS5_LOG_CHARS_OUT_DEBUG("Creating Pipeline");
// The state is copied only when a mask must change.
auto masked = maskedState(state, inputs.fragmentOutputs);
if (std::any_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.proxied; })) recipeOut = nullptr;
for (std::size_t index = 0; index < targets.size(); ++index) {
if (!targets[index].proxied) continue;
if (!masked.has_value()) masked = state;
masked->colors[index].format = AttachmentProxyFormat(context, state.colors[index].format);
masked->blends.at(state.colors[index].exportIndex).colorWriteMask &= VK_COLOR_COMPONENT_R_BIT;
}
const State& pipelineState = masked.has_value() ? *masked : state;
auto pipeline = CachedPipeline(context, pipelineState, inputs.vertexInput, *resources, shaders, lean ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
// Resident targets' views are stable while their storage image lives, so the framebuffer is
@ -1702,6 +1756,9 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
record.framebuffer = framebuffer;
record.targetViews = targetViews;
record.targets = owners;
for (const auto& binding : targets) {
if (binding.proxied) record.proxies.push_back(binding.resident);
}
record.indirect = args != nullptr ? &indirect : nullptr;
record.listed = listed;
record.completion = completion;
@ -1800,6 +1857,11 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
rewritten = recordIndirectArguments(context, commands, recorder, recorded, indirect, scratch, argumentBuffer, argumentOffset, countBarrier);
}
for (auto& binding : targets) {
if (binding.proxied) {
binding.resident->RecordAttachmentProxyLoad(commands, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
countBarrier(2);
continue;
}
if (binding.resident != nullptr) {
imageBarrier(context, commands, binding.resident->Image(), VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT);
countBarrier();
@ -1835,6 +1897,11 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
context.Resolved(&DeviceFunctions::cmdEndRenderPass, "vkCmdEndRenderPass")(commands);
APS5_LOG_CHARS_OUT_DEBUG("Render pass ended");
for (auto& binding : targets) {
if (binding.proxied) {
binding.resident->RecordAttachmentProxyStore(commands, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
countBarrier(2);
continue;
}
if (binding.resident != nullptr) {
imageBarrier(context, commands, binding.resident->Image(), VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT);
countBarrier();
@ -1930,7 +1997,8 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
if (binding.gpuTiling) GuestMemory::WriteChanged(binding.color.address, binding.tiled->Bytes(), binding.original);
else WriteColorTarget(binding.color, binding.transfer->Bytes());
// The stored texels are the whole target now, so later reads must see them rather than a fast clear.
MarkDccUncompressed(binding.color.dccAddress, ColorTargetLayout(binding.color.extent.width, binding.color.extent.height, binding.color.tileMode, binding.color.elementBytes).Bytes());
const auto targetBytes = ColorTargetLayout(binding.color.extent.width, binding.color.extent.height, binding.color.tileMode, binding.color.elementBytes).Bytes();
MarkDccUncompressed(binding.color.dccAddress, targetBytes, colorKeyCount(binding.color, targetBytes));
}
timer.phase(PhaseWriteBack);
if (profile && traceDraws) {
@ -2067,28 +2135,39 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con
return result;
}
namespace {
std::shared_ptr<StorageTexture> metadataPassResident(const Context& context, const ColorTarget& color) {
if (color.tileMode == ColorTileMode::Linear || context.detiler == nullptr) return nullptr;
std::shared_ptr<StorageTexture> resident;
try {
resident = CachedStorageSurface(context, SurfaceForTarget(color));
} catch (const std::exception&) {
return nullptr;
}
return resident != nullptr && resident->GuestBytes() == color.bytes ? resident : nullptr;
}
}
void RunColorMetadataPass(const Context& context, const ColorMetadataPass& pass) {
for (const auto& color : pass.targets) {
if (color.cmaskAddress != 0) {
const auto resident = metadataPassResident(context, color);
if (materializeCmaskClear(context, color, resident.get()) && resident != nullptr) resident->MarkDirty();
}
if (color.dccAddress == 0) continue;
auto keys = CurrentDccKeys(color.dccAddress, color.bytes);
if (keys == DccKeys::Uncompressed) continue;
Require(IsDccClear(keys), std::string("CB metadata pass over DCC keys that are ") + DccKeysName(keys) + " (per-block metadata is not modeled)");
const auto texel = clearTexel(color, keys);
std::shared_ptr<StorageTexture> resident;
if ((color.tileMode == ColorTileMode::RenderTarget || color.tileMode == ColorTileMode::Standard4KB) && context.detiler != nullptr) {
try {
resident = CachedStorageSurface(context, SurfaceForTarget(color));
} catch (const std::exception&) {
resident = nullptr;
}
if (resident != nullptr && resident->GuestBytes() != color.bytes) resident = nullptr;
}
const auto resident = metadataPassResident(context, color);
if (resident != nullptr) {
const char* refusal = nullptr;
const bool current = keys == DccKeys::ClearRegister ? clearToTexel(*resident, texel, color.elementBytes, refusal) : StorageTexture::FindPending(color.address, color.bytes) == resident || resident->UploadedKeys() == keys;
if (current) {
resident->MarkDirty();
MarkDccUncompressed(context, color.dccAddress, color.bytes);
MarkDccUncompressed(context, color.dccAddress, color.bytes, colorKeyCount(color, color.bytes));
continue;
}
}

View File

@ -13,9 +13,14 @@
#ifdef _WIN32
#include <windows.h>
#else
#include <fcntl.h>
#include <linux/udmabuf.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
#endif
#include <atomic>
#include <functional>
#include <bit>
#include <condition_variable>
#include <iterator>
@ -81,6 +86,8 @@ struct GuestBufferMemory::AddressSpace {
std::vector<CopiedRange> copied;
// The BDA table entries of `base`, in its order.
std::vector<ShaderRecompiler::BdaAbi::Range> ranges;
mutable std::mutex tableMutex;
mutable std::map<std::vector<std::pair<std::uint64_t, std::uint64_t>>, std::pair<std::uint64_t, std::shared_ptr<const std::vector<ShaderRecompiler::BdaAbi::Range>>>> writeTables;
};
namespace {
@ -128,6 +135,7 @@ struct HostImports {
std::uint64_t epoch = 1;
VkDevice watchDevice = VK_NULL_HANDLE;
bool unwatchImports = false;
bool unwatchDmaBufImports = false;
};
HostImports& Imports() {
@ -170,9 +178,7 @@ void retireImport(const Context& context, HostImports& state, std::map<std::uint
state.imports.erase(it);
}
const char* createImport(const Context& context, HostImport& entry, VkResult& failure) {
const auto bytes = entry.bytes;
void* const host = entry.alias != nullptr ? entry.alias : reinterpret_cast<void*>(entry.base);
const char* bindImport(const Context& context, HostImport& entry, VkExternalMemoryHandleTypeFlagBits handleType, const void* import, std::uint32_t importTypes, bool& allocated, VkResult& failure) {
const auto failed = [&](const char* step, VkResult result) -> const char* {
if (entry.buffer) context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer")(context.device, entry.buffer, nullptr);
if (entry.memory) context.Function<PFN_vkFreeMemory>("vkFreeMemory")(context.device, entry.memory, nullptr);
@ -181,25 +187,24 @@ const char* createImport(const Context& context, HostImport& entry, VkResult& fa
failure = result;
return step;
};
const VkExternalMemoryBufferCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO, nullptr, VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT};
allocated = false;
const VkExternalMemoryBufferCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO, nullptr, static_cast<VkExternalMemoryHandleTypeFlags>(handleType)};
VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, &external};
info.size = bytes;
info.size = entry.bytes;
// INDIRECT_BUFFER: DISPATCH_INDIRECT group counts are read in place (VulkanDevice::DispatchIndirect).
info.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT;
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
if (const auto result = context.Function<PFN_vkCreateBuffer>("vkCreateBuffer")(context.device, &info, nullptr, &entry.buffer); result != VK_SUCCESS) return failed("vkCreateBuffer", result);
VkMemoryHostPointerPropertiesEXT pointer{VK_STRUCTURE_TYPE_MEMORY_HOST_POINTER_PROPERTIES_EXT};
if (const auto result = context.Function<PFN_vkGetMemoryHostPointerPropertiesEXT>("vkGetMemoryHostPointerPropertiesEXT")(context.device, VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT, host, &pointer); result != VK_SUCCESS) return failed("vkGetMemoryHostPointerPropertiesEXT", result);
VkMemoryRequirements requirements{};
context.Function<PFN_vkGetBufferMemoryRequirements>("vkGetBufferMemoryRequirements")(context.device, entry.buffer, &requirements);
const auto types = requirements.memoryTypeBits & pointer.memoryTypeBits;
const auto types = requirements.memoryTypeBits & importTypes;
if (types == 0) return failed("memory type selection", VK_ERROR_FORMAT_NOT_SUPPORTED);
const VkImportMemoryHostPointerInfoEXT import{VK_STRUCTURE_TYPE_IMPORT_MEMORY_HOST_POINTER_INFO_EXT, nullptr, VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT, host};
const VkMemoryAllocateFlagsInfo flags{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_FLAGS_INFO, &import, VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT, 0};
const VkMemoryAllocateFlagsInfo flags{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_FLAGS_INFO, import, VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT, 0};
VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, &flags};
allocation.allocationSize = bytes;
allocation.allocationSize = entry.bytes;
allocation.memoryTypeIndex = static_cast<std::uint32_t>(std::countr_zero(types));
if (const auto result = context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &entry.memory); result != VK_SUCCESS) return failed("vkAllocateMemory", result);
allocated = true;
if (const auto result = context.Function<PFN_vkBindBufferMemory>("vkBindBufferMemory")(context.device, entry.buffer, entry.memory, 0); result != VK_SUCCESS) return failed("vkBindBufferMemory", result);
const VkBufferDeviceAddressInfo addressInfo{VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO, nullptr, entry.buffer};
entry.address = context.Function<PFN_vkGetBufferDeviceAddressKHR>("vkGetBufferDeviceAddressKHR")(context.device, &addressInfo);
@ -207,6 +212,62 @@ const char* createImport(const Context& context, HostImport& entry, VkResult& fa
return nullptr;
}
const char* createHostPointerImport(const Context& context, HostImport& entry, VkResult& failure) {
void* const host = entry.alias != nullptr ? entry.alias : reinterpret_cast<void*>(entry.base);
VkMemoryHostPointerPropertiesEXT pointer{VK_STRUCTURE_TYPE_MEMORY_HOST_POINTER_PROPERTIES_EXT};
if (const auto result = context.Function<PFN_vkGetMemoryHostPointerPropertiesEXT>("vkGetMemoryHostPointerPropertiesEXT")(context.device, VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT, host, &pointer); result != VK_SUCCESS) {
failure = result;
return "vkGetMemoryHostPointerPropertiesEXT";
}
const VkImportMemoryHostPointerInfoEXT import{VK_STRUCTURE_TYPE_IMPORT_MEMORY_HOST_POINTER_INFO_EXT, nullptr, VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT, host};
bool allocated = false;
return bindImport(context, entry, VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT, &import, pointer.memoryTypeBits, allocated, failure);
}
#ifndef _WIN32
int udmabufDevice() {
static const int device = open("/dev/udmabuf", O_RDWR | O_CLOEXEC);
return device;
}
const char* createDmaBufImport(const Context& context, HostImport& entry, int file, std::uint64_t offset, VkResult& failure) {
udmabuf_create request{};
request.memfd = static_cast<std::uint32_t>(file);
request.flags = UDMABUF_FLAGS_CLOEXEC;
request.offset = offset;
request.size = entry.bytes;
const int device = udmabufDevice();
const int buffer = device < 0 ? -1 : ioctl(device, UDMABUF_CREATE, &request);
close(file);
if (buffer < 0) {
failure = VK_ERROR_INVALID_EXTERNAL_HANDLE;
return device < 0 ? "open /dev/udmabuf" : "UDMABUF_CREATE";
}
VkMemoryFdPropertiesKHR properties{VK_STRUCTURE_TYPE_MEMORY_FD_PROPERTIES_KHR};
if (const auto result = context.Function<PFN_vkGetMemoryFdPropertiesKHR>("vkGetMemoryFdPropertiesKHR")(context.device, VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, buffer, &properties); result != VK_SUCCESS) {
close(buffer);
failure = result;
return "vkGetMemoryFdPropertiesKHR";
}
const VkImportMemoryFdInfoKHR import{VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR, nullptr, VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, buffer};
bool allocated = false;
const char* step = bindImport(context, entry, VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, &import, properties.memoryTypeBits, allocated, failure);
if (!allocated) close(buffer);
entry.dmaBuf = step == nullptr;
return step;
}
#endif
const char* createImport(const Context& context, HostImport& entry, VkResult& failure) {
const char* step = createHostPointerImport(context, entry, failure);
#ifndef _WIN32
int file = -1;
std::uint64_t offset = 0;
if (step != nullptr && context.dmaBufImport && GuestArena::GuestArenaSharedBacking_nid_postfix(static_cast<std::uintptr_t>(entry.base), static_cast<std::size_t>(entry.bytes), &file, &offset)) step = createDmaBufImport(context, entry, file, offset, failure);
#endif
return step;
}
#ifndef _WIN32
enum class ImportWatchRequest : std::uint8_t { Probe, Watch, Unwatch };
@ -226,11 +287,14 @@ void decideImportWatch(const Context& context, HostImports& state) {
if (state.watchDevice == context.device) return;
#ifdef _WIN32
state.watchDevice = context.device;
state.unwatchImports = false;
state.unwatchImports = true;
state.unwatchDmaBufImports = false;
if (context.hostImportAlignment != 0 && GuestMemory::WriteWatched()) std::fprintf(stderr, "[write-watch] host imports are compared on Windows because driver writes can arrive after the import window\n");
#else
const auto request = importWatchRequest();
state.watchDevice = context.device;
state.unwatchImports = false;
state.unwatchDmaBufImports = false;
if (context.hostImportAlignment == 0 || !GuestMemory::WriteWatched()) return;
if (request == ImportWatchRequest::Watch) {
std::fprintf(stderr, "[write-watch] host imports stay watched (APS5_WRITE_WATCH_IMPORTS=watch)\n");
@ -238,6 +302,7 @@ void decideImportWatch(const Context& context, HostImports& state) {
}
if (request == ImportWatchRequest::Unwatch) {
state.unwatchImports = true;
state.unwatchDmaBufImports = true;
std::fprintf(stderr, "[write-watch] host imports are compared, not watched (APS5_WRITE_WATCH_IMPORTS=unwatch)\n");
return;
}
@ -245,16 +310,36 @@ void decideImportWatch(const Context& context, HostImports& state) {
if (probe.failure != nullptr) {
state.unwatchImports = true;
std::fprintf(stderr, "[write-watch] host imports resolve write protection: unknown (probe failed at %s, %d); imported ranges are compared\n", probe.failure, static_cast<int>(probe.result));
} else {
state.unwatchImports = probe.writtenAfterSubmit != 0;
std::fprintf(stderr, "[write-watch] host imports resolve write protection: %s (%u of %u scratch pages written after a GPU read, %u after the import); imported ranges %s\n", state.unwatchImports ? "yes" : "no", probe.writtenAfterSubmit, probe.pages, probe.writtenAtImport, state.unwatchImports ? "are compared" : "stay watched");
}
state.unwatchDmaBufImports = state.unwatchImports;
if (!context.dmaBufImport) return;
const auto dmaBuf = ProbeDmaBufImportWriteProtection(context);
if (dmaBuf.failure != nullptr) {
state.unwatchDmaBufImports = true;
std::fprintf(stderr, "[write-watch] dma-buf imports keep write protection: unknown (probe failed at %s, %d); imported ranges are compared\n", dmaBuf.failure, static_cast<int>(dmaBuf.result));
return;
}
state.unwatchImports = probe.writtenAfterSubmit != 0;
std::fprintf(stderr, "[write-watch] host imports resolve write protection: %s (%u of %u scratch pages written after a GPU read, %u after the import); imported ranges %s\n", state.unwatchImports ? "yes" : "no", probe.writtenAfterSubmit, probe.pages, probe.writtenAtImport, state.unwatchImports ? "are compared" : "stay watched");
state.unwatchDmaBufImports = dmaBuf.writtenAfterSubmit != 0 || dmaBuf.writtenByCpu == 0;
std::fprintf(stderr, "[write-watch] dma-buf imports keep write protection: %s (%u of %u scratch pages written after a GPU read, %u after the import, %u seen after a CPU store); imported ranges %s\n", state.unwatchDmaBufImports ? "no" : "yes", dmaBuf.writtenAfterSubmit, dmaBuf.pages, dmaBuf.writtenAtImport, dmaBuf.writtenByCpu, state.unwatchDmaBufImports ? "are compared" : "stay watched");
#endif
}
std::shared_ptr<const GuestAllocations::Range> leasedRangeOwner(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 : nullptr;
}
bool sameRange(const HostImport& entry, const GuestAllocations::Lease& lease) {
const auto current = leasedRangeOwner(lease, entry.base);
return current != nullptr && !entry.range.owner_before(current) && !current.owner_before(entry.range);
}
const HostImport* importAllocation(const Context& context, HostImports& state, std::uint64_t base, std::uint64_t bytes, const GuestAllocations::Lease& lease) {
if (const auto found = state.imports.find(base); found != state.imports.end()) {
if (found->second.bytes == bytes) return &found->second;
if (found->second.bytes == bytes && sameRange(found->second, lease)) return &found->second;
retireImport(context, state, found, lease);
}
const auto alignment = context.hostImportAlignment;
@ -281,6 +366,7 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s
return nullptr;
}
HostImport entry{base, bytes, VK_NULL_HANDLE, VK_NULL_HANDLE, 0};
entry.range = leasedRangeOwner(lease, base);
#ifdef _WIN32
// Drivers pin imported pages, so every page must be committed and accessible.
bool writable = true;
@ -312,16 +398,21 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s
}
entry.alias = GuestArena::GuestArenaMapAlias_nid_postfix(static_cast<std::uintptr_t>(base), static_cast<std::size_t>(bytes));
}
#else
if (!GuestMemory::Accessible(reinterpret_cast<const void*>(base), static_cast<std::size_t>(bytes), true)) {
state.failed.insert(base);
return nullptr;
}
#endif
decideImportWatch(context, state);
if (state.unwatchImports) GuestMemory::Unwatch(base, bytes);
entry.unwatched = state.unwatchImports;
VkResult result = VK_SUCCESS;
const char* step = nullptr;
GuestMemory::ImportWatched(base, bytes, [&] {
step = createImport(context, entry, result);
return step == nullptr;
});
entry.unwatched = step == nullptr && (entry.dmaBuf ? state.unwatchDmaBufImports : state.unwatchImports);
if (entry.unwatched) GuestMemory::Unwatch(base, bytes);
if (step != nullptr) {
#ifdef _WIN32
GuestArena::GuestArenaUnmapAlias_nid_postfix(entry.alias);
@ -337,6 +428,18 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s
std::fprintf(stderr, "[gpu] 0x%llx+0x%llx state 0x%lx protect 0x%lx type 0x%lx allocation 0x%llx\n", static_cast<unsigned long long>(cursor), static_cast<unsigned long long>(std::min(regionEnd, base + bytes) - cursor), info.State, info.Protect, info.Type, reinterpret_cast<unsigned long long>(info.AllocationBase));
cursor = regionEnd;
}
#else
static const bool trace = std::getenv("APS5_TRACE_HOST_IMPORT") != nullptr;
if (std::FILE* maps = trace ? std::fopen("/proc/self/maps", "r") : nullptr) {
char line[512];
while (std::fgets(line, sizeof(line), maps) != nullptr) {
unsigned long long first = 0;
unsigned long long last = 0;
if (std::sscanf(line, "%llx-%llx", &first, &last) != 2 || last <= base || first >= base + bytes) continue;
std::fprintf(stderr, "[gpu] %s", line);
}
std::fclose(maps);
}
#endif
return nullptr;
}
@ -389,11 +492,13 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc
state.refreshedGeneration = generation;
for (auto it = state.imports.begin(); it != state.imports.end();) {
const auto* range = leasedRangeAt(lease, it->first);
if (range != nullptr && range->bytes == it->second.bytes) {
if (range != nullptr && range->bytes == it->second.bytes && 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 && 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, state, it, lease);
it = next;
@ -1013,35 +1118,31 @@ void ClearImageMirrors(VkDevice device) {
state.heapBytes = 0;
state.device = VK_NULL_HANDLE;
}
Spaces().current.store(nullptr);
}
ImportProbe ProbeImportWriteProtection(const Context& context) {
ImportProbe probe;
void ClearHostImports(VkDevice device) {
auto& state = Imports();
std::lock_guard lock(state.mutex);
if (state.device != device) return;
for (const auto& [address, entry] : state.imports) {
state.destroyBuffer(state.device, entry.buffer, nullptr);
state.freeMemory(state.device, entry.memory, nullptr);
#ifdef _WIN32
static_cast<void>(context);
probe.failure = "the Linux write watch";
return probe;
#else
if (context.hostImportAlignment == 0) {
probe.failure = "host import support";
return probe;
GuestArena::GuestArenaUnmapAlias_nid_postfix(entry.alias);
#endif
}
if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) {
probe.failure = "the write watch";
return probe;
}
constexpr std::uint64_t page = 4096;
const std::uint64_t alignment = std::max<std::uint64_t>(context.hostImportAlignment, page);
const std::uint64_t bytes = (4 * page + alignment - 1) / alignment * alignment;
probe.pages = static_cast<std::uint32_t>(bytes / page);
void* raw = mmap(nullptr, bytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (raw == MAP_FAILED) {
probe.failure = "mmap";
return probe;
}
const auto base = (reinterpret_cast<std::uint64_t>(raw) + alignment - 1) / alignment * alignment;
auto* scratch = reinterpret_cast<volatile std::uint8_t*>(base);
for (std::uint64_t offset = 0; offset < bytes; offset += page) scratch[offset] = 1;
state.imports.clear();
state.failed.clear();
state.device = VK_NULL_HANDLE;
state.refreshedGeneration = 0;
++state.epoch;
}
#ifndef _WIN32
namespace {
void runImportProbe(const Context& context, std::uint64_t base, std::uint64_t bytes, const std::function<const char*(HostImport&, VkResult&)>& importStep, ImportProbe& probe) {
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(reinterpret_cast<const void*>(base), bytes);
HostImport import{base, bytes, VK_NULL_HANDLE, VK_NULL_HANDLE, 0};
VkBuffer destination = VK_NULL_HANDLE;
@ -1058,7 +1159,7 @@ ImportProbe ProbeImportWriteProtection(const Context& context) {
if (!collect(quiet)) return "the first collect";
if (!collect(quiet)) return "the second collect";
if (quiet != 0) return "an unwritten scratch range";
if (const char* step = createImport(context, import, probe.result)) return step;
if (const char* step = importStep(import, probe.result)) return step;
if (!collect(probe.writtenAtImport)) return "the collect after the import";
VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
info.size = bytes;
@ -1094,6 +1195,8 @@ ImportProbe ProbeImportWriteProtection(const Context& context) {
if ((probe.result = context.Function<PFN_vkWaitForFences>("vkWaitForFences")(context.device, 1, &fence, VK_TRUE, 10'000'000'000ull)) != VK_SUCCESS) return "vkWaitForFences";
submitted = false;
if (!collect(probe.writtenAfterSubmit)) return "the collect after the submission";
*reinterpret_cast<volatile std::uint8_t*>(base) = 2;
if (!collect(probe.writtenByCpu)) return "the collect after a CPU store";
return nullptr;
};
probe.failure = run();
@ -1104,11 +1207,92 @@ ImportProbe ProbeImportWriteProtection(const Context& context) {
if (destinationMemory != VK_NULL_HANDLE) context.Function<PFN_vkFreeMemory>("vkFreeMemory")(context.device, destinationMemory, nullptr);
if (import.buffer != VK_NULL_HANDLE) destroyImport(context, import);
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(reinterpret_cast<const void*>(base), bytes);
}
}
#endif
ImportProbe ProbeImportWriteProtection(const Context& context) {
ImportProbe probe;
#ifdef _WIN32
static_cast<void>(context);
probe.failure = "the Linux write watch";
return probe;
#else
if (context.hostImportAlignment == 0) {
probe.failure = "host import support";
return probe;
}
if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) {
probe.failure = "the write watch";
return probe;
}
constexpr std::uint64_t page = 4096;
const std::uint64_t alignment = std::max<std::uint64_t>(context.hostImportAlignment, page);
const std::uint64_t bytes = (4 * page + alignment - 1) / alignment * alignment;
probe.pages = static_cast<std::uint32_t>(bytes / page);
void* raw = mmap(nullptr, bytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (raw == MAP_FAILED) {
probe.failure = "mmap";
return probe;
}
const auto base = (reinterpret_cast<std::uint64_t>(raw) + alignment - 1) / alignment * alignment;
auto* scratch = reinterpret_cast<volatile std::uint8_t*>(base);
for (std::uint64_t offset = 0; offset < bytes; offset += page) scratch[offset] = 1;
runImportProbe(context, base, bytes, [&](HostImport& import, VkResult& result) { return createImport(context, import, result); }, probe);
munmap(raw, bytes + alignment);
return probe;
#endif
}
ImportProbe ProbeDmaBufImportWriteProtection(const Context& context) {
ImportProbe probe;
#ifdef _WIN32
static_cast<void>(context);
probe.failure = "the Linux write watch";
return probe;
#else
if (!context.dmaBufImport) {
probe.failure = "dma-buf import support";
return probe;
}
if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) {
probe.failure = "the write watch";
return probe;
}
constexpr std::uint64_t page = 4096;
const std::uint64_t alignment = std::max<std::uint64_t>(context.hostImportAlignment, page);
const std::uint64_t bytes = (4 * page + alignment - 1) / alignment * alignment;
probe.pages = static_cast<std::uint32_t>(bytes / page);
const int file = memfd_create("aps5-dma-buf-import-probe", MFD_CLOEXEC | MFD_ALLOW_SEALING);
if (file < 0) {
probe.failure = "memfd_create";
return probe;
}
void* mapped = MAP_FAILED;
if (ftruncate(file, static_cast<off_t>(bytes)) == 0 && fcntl(file, F_ADD_SEALS, F_SEAL_SHRINK) == 0) mapped = mmap(nullptr, bytes, PROT_READ | PROT_WRITE, MAP_SHARED, file, 0);
if (mapped == MAP_FAILED) {
close(file);
probe.failure = "the shared scratch mapping";
return probe;
}
const auto base = reinterpret_cast<std::uint64_t>(mapped);
auto* scratch = reinterpret_cast<volatile std::uint8_t*>(base);
for (std::uint64_t offset = 0; offset < bytes; offset += page) scratch[offset] = 1;
runImportProbe(context, base, bytes, [&](HostImport& import, VkResult& result) -> const char* {
const int copy = dup(file);
if (copy < 0) {
result = VK_ERROR_INITIALIZATION_FAILED;
return "dup";
}
return createDmaBufImport(context, import, copy, 0, result);
}, probe);
munmap(mapped, bytes);
close(file);
return probe;
#endif
}
ImportWatch PrepareImportWatch(const Context& context) {
auto& state = Imports();
std::lock_guard lock(state.mutex);
@ -1121,6 +1305,7 @@ void SetImportWatch(const Context& context, ImportWatch watch) {
std::lock_guard lock(state.mutex);
state.watchDevice = context.device;
state.unwatchImports = watch == ImportWatch::Unwatch;
state.unwatchDmaBufImports = state.unwatchImports;
}
const HostImport* HostImportFor(const Context& context, std::uint64_t address, std::size_t bytes) {
@ -1446,7 +1631,9 @@ const GuestBufferMemory::Region* GuestBufferMemory::owner(std::uint64_t address)
void GuestBufferMemory::addDescriptorRegion(std::uint64_t address, std::size_t bytes, bool atomic) {
validate(address, bytes);
switch (baseOverlap(address, address + bytes, nullptr)) {
const auto begin = address & ~std::uint64_t{3};
const auto end = begin == address ? address + bytes : (address + bytes + 3) & ~std::uint64_t{3};
switch (baseOverlap(begin, end, nullptr)) {
case BaseOverlap::Inside:
return;
case BaseOverlap::Partial:
@ -1458,9 +1645,9 @@ void GuestBufferMemory::addDescriptorRegion(std::uint64_t address, std::size_t b
}
// `writable` here means the bytes come from live guest memory (not a snapshot), whether or not
// the shader stores to them; what is written back is decided by Writes() alone.
Region region{address, address + bytes, true, {}, nullptr};
Region region{begin, end, true, {}, nullptr};
region.atomic = atomic;
auto committed = GuestMemory::DescribeCommitted(address, bytes);
auto committed = GuestMemory::DescribeCommitted(begin, static_cast<std::size_t>(end - begin));
if (!committed.whole) {
// A GPU heap bound whole while the guest commits its pages on demand, or a descriptor left
region.sparse = true;
@ -2302,11 +2489,18 @@ VkDescriptorBufferInfo GuestBufferMemory::Descriptor(std::uint64_t address, std:
const auto base = region.direct != nullptr ? region.direct->base : region.mirror != nullptr ? region.mirror->base : region.begin;
const auto offset = address - base;
Require(context.limits.minStorageBufferOffsetAlignment != 0, "no storage buffer offset alignment");
adjustment = static_cast<std::uint32_t>(offset % context.limits.minStorageBufferOffsetAlignment);
Require(adjustment % 4 == 0, "guest buffer view off the storage buffer offset alignment is not DWORD aligned");
Require(bytes + adjustment <= context.limits.maxStorageBufferRange, "guest buffer view exceeds descriptor range limit");
Require(base % 4 == 0, "a guest buffer view in a GPU owner that does not start at a DWORD boundary is not implemented");
adjustment = static_cast<std::uint32_t>(offset % std::max<VkDeviceSize>(context.limits.minStorageBufferOffsetAlignment, 4));
const auto range = ViewBytes(bytes, adjustment);
const auto end = region.direct != nullptr ? region.direct->base + region.direct->bytes : region.end;
Require(address - adjustment + range <= end, "guest buffer view exceeds its GPU owner");
Require(range <= context.limits.maxStorageBufferRange, "guest buffer view exceeds descriptor range limit");
const auto handle = region.direct != nullptr ? region.direct->buffer : region.mirror != nullptr ? region.mirror->buffer->Handle() : region.buffer->Handle();
return {handle, offset - adjustment, bytes + adjustment};
return {handle, offset - adjustment, range};
}
std::uint64_t GuestBufferMemory::ViewBytes(std::uint64_t bytes, std::uint32_t adjustment) {
return adjustment % 4 == 0 ? bytes + adjustment : 4 * (adjustment / 4 + bytes / 4 + 1);
}
ShaderRecompiler::BdaAbi::Range GuestBufferMemory::addressRange(const Region& region) {
@ -2322,19 +2516,70 @@ std::vector<ShaderRecompiler::BdaAbi::Range> GuestBufferMemory::AddressRanges()
std::vector<ShaderRecompiler::BdaAbi::Range> result;
result.reserve(regions.size() + (space != nullptr ? space->ranges.size() : 0));
for (const auto& region : regions) result.push_back(addressRange(region));
if (space == nullptr) return result;
// The table is searched by address (the recompiler's lookup bisects it): both lists are sorted
// and disjoint, so a merge keeps it so.
if (space != nullptr) {
std::vector<ShaderRecompiler::BdaAbi::Range> combined;
combined.reserve(result.size() + space->ranges.size());
std::merge(space->ranges.begin(), space->ranges.end(), result.begin(), result.end(), std::back_inserter(combined), [](const auto& left, const auto& right) { return left.begin < right.begin; });
result = std::move(combined);
}
if (writes.empty()) return result;
auto sortedWrites = writes;
std::sort(sortedWrites.begin(), sortedWrites.end());
std::vector<std::pair<std::uint64_t, std::uint64_t>> intervals;
for (const auto& interval : sortedWrites) {
if (!intervals.empty() && interval.first <= intervals.back().second) intervals.back().second = std::max(intervals.back().second, interval.second);
else intervals.push_back(interval);
}
std::vector<ShaderRecompiler::BdaAbi::Range> merged;
merged.reserve(result.size() + space->ranges.size());
std::merge(space->ranges.begin(), space->ranges.end(), result.begin(), result.end(), std::back_inserter(merged), [](const auto& left, const auto& right) { return left.begin < right.begin; });
merged.reserve(result.size() + intervals.size() * 2u);
std::size_t firstWrite = 0;
for (const auto& range : result) {
if ((range.permissions & ShaderRecompiler::BdaAbi::Write) != 0u) {
merged.push_back(range);
continue;
}
while (firstWrite < intervals.size() && intervals[firstWrite].second <= range.begin) ++firstWrite;
auto cursor = range.begin;
const auto append = [&](std::uint64_t end, bool written) {
if (cursor == end) return;
auto part = range;
part.begin = cursor;
part.end = end;
part.deviceAddress += cursor - range.begin;
if (written) {
const auto* region = owner(cursor);
Require(region != nullptr && region->writable && (region->mirror == nullptr || region->mirror->writable), "runtime buffer write has no writable GPU owner");
part.permissions |= ShaderRecompiler::BdaAbi::Write;
}
merged.push_back(part);
cursor = end;
};
for (auto index = firstWrite; index < intervals.size() && intervals[index].first < range.end; ++index) {
append(std::max(cursor, intervals[index].first), false);
append(std::min(range.end, intervals[index].second), true);
}
append(range.end, false);
}
return merged;
}
std::optional<GuestBufferMemory::CachedTable> GuestBufferMemory::CachedAddressTable() const {
if (space == nullptr || !regions.empty()) return std::nullopt;
Require(uploaded && !committed, "guest GPU address ranges are not available");
return CachedTable{space->serial, &space->ranges};
if (writes.empty()) return CachedTable{space->serial, &space->ranges};
if (writeTableRanges != nullptr) return CachedTable{writeTableSerial, writeTableRanges.get()};
std::lock_guard lock(space->tableMutex);
const auto found = space->writeTables.find(writes);
if (found != space->writeTables.end()) {
writeTableSerial = found->second.first;
writeTableRanges = found->second.second;
} else {
writeTableRanges = std::make_shared<const std::vector<ShaderRecompiler::BdaAbi::Range>>(AddressRanges());
writeTableSerial = Spaces().serials.fetch_add(1, std::memory_order_relaxed) + 1;
if (space->writeTables.size() >= 64u) space->writeTables.erase(space->writeTables.begin());
space->writeTables.emplace(writes, std::pair{writeTableSerial, writeTableRanges});
}
return CachedTable{writeTableSerial, writeTableRanges.get()};
}
bool GuestBufferMemory::HasCopiedWrites() const {

View File

@ -18,8 +18,15 @@ TextureTileMode resolveTileMode(std::uint32_t raw) {
switch (raw) {
case 0x00: return TextureTileMode::kLinear;
case 0x01: return TextureTileMode::kStandard256B;
case 0x02: return TextureTileMode::kD256B;
case 0x05: return TextureTileMode::kStandard4KB;
case 0x06: return TextureTileMode::kD4KB;
case 0x09: return TextureTileMode::kStandard64KB;
case 0x0a: return TextureTileMode::kD64KB;
case 0x11: return TextureTileMode::kS64KBT;
case 0x12: return TextureTileMode::kD64KBT;
case 0x15: return TextureTileMode::kS4KBX;
case 0x16: return TextureTileMode::kD4KBX;
case 0x18: return TextureTileMode::kZ64KBX;
case 0x19: return TextureTileMode::kS64KBX;
case 0x1a: return TextureTileMode::kD64KBX;
@ -64,7 +71,7 @@ GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words)
const auto bcSwizzle = (words[3] >> 25u) & 0x7u;
const auto typeRaw = (words[3] >> 28u) & 0xfu;
const auto depth = (words[4] >> 0u) & 0x1fffu;
auto depth = (words[4] >> 0u) & 0x1fffu;
const auto baseArray = (words[4] >> 16u) & 0x1fffu;
const auto arrayPitch = (words[5] >> 0u) & 0xfu;
@ -108,7 +115,6 @@ GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words)
static_cast<void>(maxCompBlkSize);
// Surfaces are always written uncompressed here (render targets and storage images bypass DCC), so
// only the fast-clear keys in DCC metadata change what a read returns (see DccMetadata.hpp).
static_cast<void>(metaPipeAligned);
static_cast<void>(writeCompress);
if (dccColorTransf) {
static bool reported = false;
@ -135,7 +141,7 @@ GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words)
// hardware never addresses them, so the view ends at the surface's last level.
if (baseLevel <= maxMip) lastLevel = std::min(lastLevel, maxMip);
// XOR swizzles fold a pipe/bank XOR into the low address bits; only unmodified 64 KiB bases are modeled.
Require(XorSwizzleMode(tileMode) == 0 || (baseAddress & 0xffffu) == 0, "guest texture descriptor combines an XOR swizzle with a pipe/bank XOR base which is not implemented");
Require(XorSwizzleMode(tileMode) == 0 || (baseAddress & (tileMode == TextureTileMode::kS4KBX || tileMode == TextureTileMode::kD4KBX ? 0xfffu : 0xffffu)) == 0, "guest texture descriptor combines an XOR swizzle with a pipe/bank XOR base which is not implemented");
switch (dimension) {
case TextureDimension::k1D:
@ -159,7 +165,11 @@ GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words)
case TextureDimension::kCube:
Require(width == height, "guest cube texture descriptor is not square");
Require(baseArray <= depth, "guest cube texture descriptor has a base array past its last array slice");
Require((depth - baseArray + 1u) % 6u == 0, "guest cube texture descriptor does not contain a multiple of 6 array slices");
if (baseArray == depth) depth += 5u;
if ((depth - baseArray + 1u) % 6u != 0) {
Require(false, "guest cube texture descriptor does not contain a multiple of 6 array slices" +
describe() + " (base array " + std::to_string(baseArray) + ")");
}
break;
}
@ -182,6 +192,7 @@ GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words)
result.dstSelW = static_cast<std::uint8_t>(dstSelW);
result.dccAddress = metaCompress ? metaAddr << 8u : 0u;
result.dccAlphaOnMsb = dccAlphaPos;
result.dccPipeAligned = metaPipeAligned;
result.minLod = minLod;
if (baseLevel > maxMip) {
Require(LevelsFitAllocation(result, lastLevel + 1u), "guest texture descriptor starts past the surface's last mip level at levels that would move the surface's own" + describe());

View File

@ -1,5 +1,7 @@
#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp"
#include "prx/libSceAgcDriver/Graphics/include/PipelineSpecialization.hpp"
#include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp"
#include <algorithm>
#include <array>
@ -92,6 +94,7 @@ void ValidateViewport(const Context& context, const VkViewport& viewport) {
}
Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout) : context(context), _modules(shaders.size()), attachments(state.colors.size() + (state.depth ? 1u : 0u)), colorAttachments(state.colors.size()), depthBounds(state.depth.has_value() && state.depthBoundsTest), depthBias(state.depth.has_value() && state.depthBias) {
PerformanceTimer timing("Vulkan.GraphicsPipeline");
// A cached pipeline may outlive its device's teardown (see ClearCachedPipelines); it must not keep
// the buffer pool, which is reset with the device, alive past it.
this->context.bufferPool.reset();
@ -101,6 +104,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
Require(!depthBounds || context.depthBounds, "device does not support the depth bounds test");
Require(!depthBias || state.depthBiasClamp == 0.0f || context.depthBiasClamp, "device does not support depth bias clamping");
Require(!state.negativeOneToOne || context.depthClipControl, "negative-one-to-one depth clipping requires VK_EXT_depth_clip_control with depthClipControl enabled");
Require(state.conservativeRasterization == VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT || context.conservativeRasterization, "conservative rasterization requires VK_EXT_conservative_rasterization with at most 1/256 pixel of overestimation and degenerate triangles rasterized");
if (state.rectList) Require(context.tessellationShader && context.limits.maxTessellationPatchSize >= 4, "rect-list requires tessellation with four output control points");
if (state.stages.tessellation) {
Require(context.tessellationShader, "device does not support tessellation shaders");
@ -117,6 +121,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
Require(pushStages == 0 || context.limits.maxPushConstantsSize >= PipelinePushConstantBytes, "graphics push constant range exceeds device limit");
try {
std::vector<VkPipelineShaderStageCreateInfo> stages(shaders.size());
std::vector<PipelineSpecialization> specializations;
specializations.reserve(shaders.size());
for (std::uint32_t i = 0; i < shaders.size(); ++i) {
const auto& shader = *shaders[i].program;
VkShaderModuleCreateInfo module{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
@ -128,6 +134,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
stages[i].stage = stage;
stages[i].module = _modules[i];
stages[i].pName = "main";
specializations.emplace_back(shader);
stages[i].pSpecializationInfo = specializations.back().Info();
}
// A descriptor set layout with the same bindings as this one is compatible with the pipeline
// layout, so later draws bind their own ShaderResources' set under it.
@ -208,6 +216,9 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
raster.frontFace = state.frontFace;
raster.depthBiasEnable = depthBias;
raster.lineWidth = 1;
VkPipelineRasterizationConservativeStateCreateInfoEXT conservative{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_CONSERVATIVE_STATE_CREATE_INFO_EXT};
conservative.conservativeRasterizationMode = state.conservativeRasterization;
if (state.conservativeRasterization != VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT) raster.pNext = &conservative;
VkPipelineMultisampleStateCreateInfo samples{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
samples.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
@ -241,7 +252,9 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
pipelineInfo.pDynamicState = &dynamic;
pipelineInfo.layout = layout;
pipelineInfo.renderPass = renderPass;
timing.Mark("modules_and_state");
Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines");
timing.Mark("create");
LogPipelineStatistics_nid_no_patch(context, pipeline);
} catch (...) {
release();
@ -367,9 +380,9 @@ std::vector<std::byte> pipelineKey(const Context& context, const State& state, c
for (const auto& shader : shaders) {
Require(shader.program != nullptr, "missing compiled shader");
const bool generated = state.rectList && (shader.stage == Stage::TessellationControl || shader.stage == Stage::TessellationEvaluation);
if (!generated && shader.program->variantId == 0) return {};
if (!generated && shader.program->PipelineVariantId() == 0) return {};
append(key, shader.stage);
append(key, generated ? std::uint64_t{0} : shader.program->variantId);
append(key, generated ? std::uint64_t{0} : shader.program->PipelineVariantId());
// Where the stage's push constants sit in the block (AssemblePushConstants).
append(key, shader.pushConstantOffset);
}
@ -397,6 +410,7 @@ std::vector<std::byte> pipelineKey(const Context& context, const State& state, c
append(key, state.frontFace);
append(key, state.negativeOneToOne);
append(key, state.depthClamp && context.depthClamp);
append(key, state.conservativeRasterization);
append(key, state.blends.size());
for (const auto& blend : state.blends) append(key, blend);
for (const auto value : state.blendConstants) append(key, value);

View File

@ -328,11 +328,15 @@ struct DeferredBatch {
};
struct DeferredBatchesTag {};
auto& DeferredBatches() { return HostThreadLocal<std::vector<DeferredBatch>, DeferredBatchesTag>(); }
struct DeferredBytesTag {};
auto& DeferredBytes() { return HostThreadLocal<std::size_t, DeferredBytesTag>(); }
std::vector<DeferredBatch> TakeDeferred() {
auto taken = std::move(DeferredBatches());
DeferredBatches().clear();
DeferredBytes() = 0;
return taken;
}
std::atomic<std::uint64_t> deferredPending{0};
// APS5_PROFILE_DRAW, for the [recorder] line: batches and objects released after an unlock and the
// time that took, on the release thread and inline (on the unlocking thread: the kill switch, a
// full queue, a stopping thread or a failed hand-off), the batches that went inline because the
// queue was full, and the longest queue seen (in batches).
std::atomic<std::uint64_t> threadReleases{0}, threadObjects{0}, threadReleaseUs{0};
std::atomic<std::uint64_t> inlineReleases{0}, inlineObjects{0}, inlineReleaseUs{0}, inlineOverBound{0};
std::atomic<std::uint64_t> releaseQueueMax{0};
@ -446,8 +450,7 @@ void ReleaseDeferredKeeps() {
if (DeferredBatches().empty()) return;
// Taken off the thread's list first: a destructor that took and released the mutex (none is
// known to) would re-enter here and must find nothing.
auto releasing = std::move(DeferredBatches());
DeferredBatches().clear();
auto releasing = TakeDeferred();
if (ReleaseOnUnlock()) {
DestroyDeferred(std::move(releasing), false);
return;
@ -1134,11 +1137,7 @@ Recorder::~Recorder() {
// other threads (an unlock hook that found the queue stopping or full) are waited for before
// the caller destroys the device. The count is global: a newer recorder's batches (device
// replacement) are waited for too, which only prolongs the spin.
if (!DeferredBatches().empty()) {
auto own = std::move(DeferredBatches());
DeferredBatches().clear();
DestroyDeferred(std::move(own), false);
}
if (!DeferredBatches().empty()) DestroyDeferred(TakeDeferred(), false);
JoinReleaseThread();
while (deferredPending.load(std::memory_order_acquire) != 0) std::this_thread::yield();
{
@ -1370,10 +1369,13 @@ VkCommandBuffer Recorder::CommandsInRenderPass() {
return open->commands;
}
void Recorder::LeaveRenderPassOpen(std::uint64_t key, std::uint32_t timing, bool continuable) {
void Recorder::LeaveRenderPassOpen(std::uint64_t key, std::uint32_t timing, bool continuable, std::function<void(VkCommandBuffer)> afterPass) {
Require(open != nullptr, "no batch is open for the render pass");
auto& pass = open->renderPass;
if (!pass.open) pass.timing = timing;
if (!pass.open) {
pass.timing = timing;
pass.afterPass = std::move(afterPass);
}
pass.open = true;
pass.key = key;
pass.continuable = continuable;
@ -1386,6 +1388,7 @@ void Recorder::endOpenRenderPass() {
// host sees them at the batch's fence).
recordBarrier(open->commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT);
CountBarriers(CommandClass::Draw);
if (pass.afterPass) pass.afterPass(open->commands);
EndGpuTiming(pass.timing);
open->coveredAccess = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
open->hostReadOwed = true;
@ -2186,9 +2189,21 @@ std::size_t Recorder::UnsignaledBatches() const {
return static_cast<std::size_t>(std::count_if(inFlight.begin(), inFlight.end(), [&](const auto& batch) { return !signaled(*batch); }));
}
void Recorder::Keep(std::shared_ptr<void> object) {
void Recorder::Keep(std::shared_ptr<void> object, std::size_t bytes) {
ensureOpen();
open->kept.push_back(std::move(object));
open->keptBytes += bytes;
}
void Recorder::BoundKeptBytes() {
if (!GuestMemory::GpuMutex().HeldByThisThread()) return;
if (open != nullptr && open->keptBytes >= KeptBytesBudget) Submit();
if (DeferredBytes() != 0 && inFlightKeptBytes + DeferredBytes() > 2 * KeptBytesBudget) DestroyDeferred(TakeDeferred(), false);
while (!inFlight.empty() && inFlightKeptBytes > 2 * KeptBytesBudget) {
auto batch = std::move(inFlight.front());
inFlight.pop_front();
finish(std::move(batch), true, 4, true);
}
}
namespace {
@ -2247,13 +2262,14 @@ void Recorder::OnComplete(std::function<void()> action) {
writeBackCompletions.fetch_add(1, std::memory_order_acq_rel);
}
bool Recorder::noteWrite(std::uint64_t address, std::size_t bytes, bool ownLabel) {
bool Recorder::noteWrite(std::uint64_t address, std::size_t bytes, bool ownLabel, int value) {
CaptureTrace::Log("buffer-write batch=%llu address=%llx bytes=%zu label=%d", static_cast<unsigned long long>(submissions + 1), static_cast<unsigned long long>(address), bytes, ownLabel);
if (bytes == 0) return false;
ensureOpen();
const auto end = address + bytes;
open->writes.emplace_back(address, end);
open->writeNotes.push_back(++writeNoteCount);
open->writeValues.push_back(static_cast<std::int16_t>(value));
if (!ownLabel) markOverwritten(address, end);
// A poller waiting on this range learns that the open batch may now hold its producer.
if (activeRecorder == this) writeGeneration.fetch_add(1, std::memory_order_release);
@ -2279,6 +2295,7 @@ void Recorder::noteWriteOn(Batch& batch, std::uint64_t address, std::size_t byte
// moves as well, so a poller re-consults the label table for a completion label.
batch.writes.emplace_back(address, address + bytes);
batch.writeNotes.push_back(++writeNoteCount);
batch.writeValues.push_back(-1);
if (!ownLabel) markOverwritten(address, address + bytes);
if (activeRecorder == this) writeGeneration.fetch_add(1, std::memory_order_release);
if (!SnapshotCovers(address, address + bytes)) publishPendingWrites();
@ -2300,6 +2317,12 @@ void Recorder::NotePendingWrite(std::uint64_t address, std::size_t bytes) {
std::atomic_thread_fence(std::memory_order_seq_cst);
}
void Recorder::NotePendingFill(std::uint64_t address, std::size_t bytes, std::uint8_t value) {
if (!noteWrite(address, bytes, false, value)) return;
publishPendingWrites();
std::atomic_thread_fence(std::memory_order_seq_cst);
}
void Recorder::NotePendingWrites(std::span<const std::pair<std::uint64_t, std::uint64_t>> ranges) {
bool publish = false;
for (const auto& [begin, end] : ranges) {
@ -2387,6 +2410,20 @@ std::uint64_t Recorder::NewestWriteNote(std::uint64_t address, std::size_t bytes
return newest;
}
std::vector<Recorder::PendingWrite> Recorder::PendingWritesOver(std::uint64_t address, std::size_t bytes) const {
std::vector<PendingWrite> found;
if (bytes == 0) return found;
const auto end = address + bytes;
const auto scan = [&](const Batch& batch) {
for (std::size_t i = 0; i < batch.writes.size(); ++i) {
if (address < batch.writes[i].second && batch.writes[i].first < end) found.push_back({batch.writes[i].first, batch.writes[i].second, batch.writeNotes[i], batch.writeValues[i]});
}
};
if (open != nullptr) scan(*open);
for (const auto& batch : inFlight) scan(*batch);
return found;
}
bool Recorder::ReadTracking() {
return ReadTrackingEnabled();
}
@ -2725,6 +2762,7 @@ bool Recorder::writtenBackSince(std::uint64_t sequence, std::uint64_t begin, std
}
void Recorder::Submit() {
PerformanceTimer frameTiming("Recorder.Submit");
// The work count is cleared even when nothing is open: the driver counts a dispatch after its
// call returns (outside the mutex), so a submit by another thread in between leaves a stale
// count behind, and the callers that act on it would otherwise take the mutex for nothing at
@ -2785,6 +2823,7 @@ void Recorder::Submit() {
newestSubmittedAt = batch->submittedAt;
}
inFlight.push_back(std::move(batch));
inFlightKeptBytes += inFlight.back()->keptBytes;
if (activeRecorder == this) pendingLabelSince.store(NoPendingLabel, std::memory_order_release);
}
@ -2833,6 +2872,7 @@ VkResult WaitTimeline(VkDevice device, VkSemaphore timeline, PFN_vkWaitSemaphore
}
void Recorder::WaitSerial(std::uint64_t serial) {
PerformanceTimer frameTiming("Recorder.WaitSerial");
if (timeline == VK_NULL_HANDLE || serial == 0) return;
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{};
@ -2862,6 +2902,7 @@ void Recorder::FinishUpTo(std::uint64_t serial) {
}
void Recorder::Sync() {
PerformanceTimer frameTiming("Recorder.Sync");
const auto source = std::exchange(announcedSource, 4);
const void* site = std::exchange(announcedSite, nullptr);
Submit();
@ -3134,6 +3175,7 @@ bool Recorder::syncThroughUnlocked(std::uint64_t address, std::uint64_t end, int
}
bool Recorder::Reap() {
PerformanceTimer frameTiming("Recorder.Reap");
const bool profile = DrawProfiled();
const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
std::uint64_t retired = 0;
@ -3153,7 +3195,8 @@ bool Recorder::Reap() {
return inFlight.empty();
}
void Recorder::finish(std::unique_ptr<Batch> batch, bool wait, int source) {
void Recorder::finish(std::unique_ptr<Batch> batch, bool wait, int source, bool releaseNow) {
inFlightKeptBytes -= batch->keptBytes;
// The batch's writes stay published until its completions ran, on every exit path.
struct Finishing {
Recorder& recorder;
@ -3265,7 +3308,7 @@ void Recorder::finish(std::unique_ptr<Batch> batch, bool wait, int source) {
const auto keptStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
// Deferred only from inside a hold: the unlock that releases the list is this thread's own, and
// a caller finishing batches without the mutex (a test) might never make one.
if (ReleaseUnderLock() || !GuestMemory::GpuMutex().HeldByThisThread()) {
if (releaseNow || ReleaseUnderLock() || !GuestMemory::GpuMutex().HeldByThisThread()) {
batch->completions.clear();
batch->kept.clear();
} else {
@ -3276,6 +3319,7 @@ void Recorder::finish(std::unique_ptr<Batch> batch, bool wait, int source) {
try {
DeferredBatches().push_back({std::move(batch->kept), std::move(batch->completions)});
deferredPending.fetch_add(1, std::memory_order_acq_rel);
DeferredBytes() += batch->keptBytes;
} catch (...) {
}
batch->completions.clear();

View File

@ -38,12 +38,6 @@ std::uint32_t read(const Registers& registers, std::uint32_t offset, RegisterBan
return it->second;
}
std::uint32_t readOr(const Registers& registers, std::uint32_t offset, RegisterBank bank, std::uint32_t fallback) {
NoteRegisterRead(bank, offset);
const auto it = registers.find(offset);
return it == registers.end() ? fallback : it->second;
}
template <typename T> T _readHeaderPod(std::span<const std::byte> header, std::uint64_t headerAddress, const void* pointer) {
if (pointer == nullptr) throw std::runtime_error("AGC graphics: null AGC header pointer");
const auto address = reinterpret_cast<std::uint64_t>(pointer);
@ -68,7 +62,7 @@ template <typename T> void _readHeaderArray(std::span<const std::byte> header, s
}
ShaderRecompiler::ShaderComputeStageInfo DecodeComputeStageInfo(const Registers& shader) {
ShaderRecompiler::ShaderComputeStageInfo DecodeComputeStageInfo(const Registers& shader, std::span<const std::byte> header) {
const auto numThreadX = read(shader, computeNumThreadX, RegisterBank::Shader);
const auto numThreadY = read(shader, computeNumThreadY, RegisterBank::Shader);
const auto numThreadZ = read(shader, computeNumThreadZ, RegisterBank::Shader);
@ -76,8 +70,13 @@ ShaderRecompiler::ShaderComputeStageInfo DecodeComputeStageInfo(const Registers&
throw std::runtime_error("AGC graphics: COMPUTE_NUM_THREAD_X/Y/Z must be nonzero");
}
const auto rsrc2 = read(shader, computePgmRsrc2, RegisterBank::Shader);
std::uint32_t scratchDwords = 0;
if ((rsrc2 & 0x1u) != 0) {
throw std::runtime_error("AGC graphics: COMPUTE_PGM_RSRC2.SCRATCH_EN is unsupported");
if (header.size() < sizeof(Shader)) throw std::runtime_error("AGC graphics: COMPUTE_PGM_RSRC2.SCRATCH_EN without an AGC shader header");
Shader agcShader;
std::memcpy(&agcShader, header.data(), sizeof(Shader));
scratchDwords = agcShader.scratch_size_dw_per_thread;
if (scratchDwords == 0) throw std::runtime_error("AGC graphics: COMPUTE_PGM_RSRC2.SCRATCH_EN with a zero scratch size");
}
// Debug aid: APS5_LDS_SLACK=<dwords> grows every dispatch's LDS allocation by that much, to tell
// whether a program depends on addresses past its declared allocation.
@ -87,18 +86,20 @@ ShaderRecompiler::ShaderComputeStageInfo DecodeComputeStageInfo(const Registers&
std::min(((rsrc2 >> 15u) & 0x1FFu) * 128u + ldsSlack, 16384u),
{((rsrc2 >> 7u) & 0x1u) != 0, ((rsrc2 >> 8u) & 0x1u) != 0, ((rsrc2 >> 9u) & 0x1u) != 0},
((rsrc2 >> 10u) & 0x1u) != 0,
((rsrc2 >> 11u) & 0x3u) + 1u
((rsrc2 >> 11u) & 0x3u) + 1u,
{},
scratchDwords
};
}
ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& context, const std::array<std::uint8_t, 8>& exportMappings, bool nullProgram) {
const auto inControl = read(context, spiPsInControl, RegisterBank::Context);
const auto inControl = nullProgram ? 0u : read(context, spiPsInControl, RegisterBank::Context);
const auto inputNum = inControl & 0x3Fu;
if (inputNum > 32u) {
throw std::runtime_error("AGC graphics: SPI_PS_IN_CONTROL input count exceeds 32");
}
const auto ena = nullProgram ? readOr(context, spiPsInputEna, RegisterBank::Context, defaultPixelInputs) : read(context, spiPsInputEna, RegisterBank::Context);
const auto addr = nullProgram ? readOr(context, spiPsInputAddr, RegisterBank::Context, defaultPixelInputs) : read(context, spiPsInputAddr, RegisterBank::Context);
const auto ena = nullProgram ? defaultPixelInputs : read(context, spiPsInputEna, RegisterBank::Context);
const auto addr = nullProgram ? defaultPixelInputs : read(context, spiPsInputAddr, RegisterBank::Context);
const auto activeInputs = ena & addr;
using ShaderRecompiler::PixelInput;
using ShaderRecompiler::PixelInputBit;
@ -115,14 +116,14 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con
for (std::uint32_t i = 0; i < inputNum; ++i) {
interpolatorSettings[i] = read(context, spiPsInputCntl0 + i, RegisterBank::Context);
}
const auto shaderControl = read(context, dbShaderControl, RegisterBank::Context);
const auto shaderControl = nullProgram ? 0u : read(context, dbShaderControl, RegisterBank::Context);
// Bits 9 and 11 are EXEC_ON_HIER_FAIL and ALPHA_TO_MASK_DISABLE on GFX10; neither changes what the
// recompiled pixel shader computes.
const auto conservativeZExport = (shaderControl >> 13u) & 0x3u;
if (conservativeZExport == 3u) {
throw std::runtime_error("AGC graphics: DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT uses the reserved value 3");
}
const auto colFormat = nullProgram ? readOr(context, spiShaderColFormat, RegisterBank::Context, 0u) : read(context, spiShaderColFormat, RegisterBank::Context);
const auto colFormat = nullProgram ? 0u : read(context, spiShaderColFormat, RegisterBank::Context);
std::array<std::uint8_t, 8> targetOutputMode{};
for (std::uint32_t i = 0; i < 8u; ++i) {
targetOutputMode[i] = static_cast<std::uint8_t>((colFormat >> (4u * i)) & 0xFu);
@ -154,12 +155,13 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con
.earlyZ = zOrder == 1u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable,
.executeOnNoop = ((shaderControl >> 10u) & 0x1u) != 0,
.conservativeZExport = static_cast<ShaderRecompiler::ConservativeZExport>(conservativeZExport),
.orderedPixelShader = ((shaderControl >> 16u) & 0x1u) != 0,
.targetOutputMode = targetOutputMode,
.targetExportMapping = exportMappings
};
}
ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span<const std::byte> header, std::uint64_t headerAddress, std::span<const std::uint32_t> userData, std::vector<DecodeRead>* reads) {
ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span<const std::byte> header, std::uint64_t headerAddress, std::span<const std::uint32_t> userData, std::vector<DecodeRead>* reads, bool staticAbi) {
if (header.size() < sizeof(Shader)) throw std::runtime_error("AGC graphics: shader header is smaller than the fixed AGC header");
Shader shader;
std::memcpy(&shader, header.data(), sizeof(Shader));
@ -188,14 +190,21 @@ ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span<const st
_readHeaderArray(header, headerAddress, shader.input_semantics, shader.num_input_semantics, semantics.data());
const auto attribTableAddr = static_cast<std::uint64_t>(userData[static_cast<std::uint32_t>(vertexAttribReg)]) | (static_cast<std::uint64_t>(userData[static_cast<std::uint32_t>(vertexAttribReg) + 1u]) << 32u);
const auto bufferTableAddr = static_cast<std::uint64_t>(userData[static_cast<std::uint32_t>(vertexBufferReg)]) | (static_cast<std::uint64_t>(userData[static_cast<std::uint32_t>(vertexBufferReg) + 1u]) << 32u);
if (attribTableAddr == 0) throw std::runtime_error("AGC graphics: null vertex attribute table address");
if (bufferTableAddr == 0) throw std::runtime_error("AGC graphics: null vertex buffer table address");
if (!staticAbi && attribTableAddr == 0) throw std::runtime_error("AGC graphics: null vertex attribute table address");
if (!staticAbi && bufferTableAddr == 0) throw std::runtime_error("AGC graphics: null vertex buffer table address");
info.fetchEmbedded = true;
info.fetchAttribReg = static_cast<std::uint32_t>(vertexAttribReg);
info.fetchBufferReg = static_cast<std::uint32_t>(vertexBufferReg);
for (std::uint32_t i = 0; i < shader.num_input_semantics; ++i) {
const auto& semantic = semantics[i];
if (semantic.static_vb_index == 1 || semantic.static_attribute == 1) throw std::runtime_error("AGC graphics: statically bound vertex attributes are not implemented");
if (staticAbi) {
auto& destination = info.resourcesDst.at(info.resourcesNum++);
destination.registerStart = static_cast<std::int32_t>(semantic.hardware_mapping);
destination.registersNum = static_cast<std::int32_t>(semantic.size_in_elements);
destination.attrId = static_cast<std::int32_t>(semantic.semantic);
continue;
}
std::array<std::byte, 4> attribWordBytes{};
const auto attribWordAddress = attribTableAddr + static_cast<std::uint64_t>(semantic.semantic) * 4u;
AgcDriver::GuestMemory::Read(attribWordAddress, attribWordBytes, 4);

View File

@ -1,4 +1,5 @@
#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp"
#include "ThreadOwned.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
@ -13,6 +14,7 @@
#include <cstdio>
#include <cstdlib>
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp"
@ -282,6 +284,7 @@ std::shared_ptr<StorageTexture> sampledStorageSource(const Context& context, con
// `guestBytes` is the surface size when the caller described the surface already (0: described here).
std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, VkComponentMapping components, std::uint64_t guestBytes = 0, bool depthCompare = false) {
CaptureTrace::Log("sampled-lookup address=%llx width=%u height=%u dcc=%llx", static_cast<unsigned long long>(resource.baseAddress), resource.width, resource.height, static_cast<unsigned long long>(resource.dccAddress));
if (auto* recorder = Recorder::Active(); recorder != nullptr) recorder->BoundKeptBytes();
if (auto depth = DepthSurfaceTexture(context, words, resource, components)) return depth;
const auto depthBitsWidth = words.size() >= 4 ? ShaderRecompiler::DepthBitsTextureWidth(words[1], words[3]) : 0u;
if (depthBitsWidth == 32u) {
@ -569,6 +572,7 @@ std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std
static const bool disabled = std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr;
if (disabled) return std::make_shared<StorageTexture>(context, *context.detiler, viewed, mip);
static_cast<void>(words);
if (auto* recorder = Recorder::Active(); recorder != nullptr) recorder->BoundKeptBytes();
const bool profile = LookupOutcomes::Profiled();
auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
auto& counters = TextureCounts();
@ -883,7 +887,61 @@ double ShaderResources::phase(BuildPhase which) {
return ms;
}
namespace {
void ValidateDescriptorLayout(const Context& context, std::span<const VkDescriptorSetLayoutBinding> bindings) {
std::map<VkDescriptorType, std::uint64_t> heapTotals;
std::map<std::pair<VkShaderStageFlags, VkDescriptorType>, std::uint64_t> stageTotals;
std::map<VkShaderStageFlags, std::uint64_t> stageResources;
for (const auto& layout : bindings) {
heapTotals[layout.descriptorType] += layout.descriptorCount;
for (std::uint32_t bit = 0u; bit < 32u; ++bit) {
const auto stage = VkShaderStageFlags{1u} << bit;
if ((layout.stageFlags & stage) == 0u) continue;
const auto count = stageTotals[{stage, layout.descriptorType}] += layout.descriptorCount;
const auto all = stageResources[stage] += layout.descriptorType == VK_DESCRIPTOR_TYPE_SAMPLER ? 0u : layout.descriptorCount;
Require(all <= context.limits.maxPerStageResources, "typed heaps exceed per-stage resource capacity");
if (layout.descriptorType == VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE) Require(count <= context.limits.maxPerStageDescriptorSampledImages, "typed heaps exceed per-stage sampled image capacity");
if (layout.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE) Require(count <= context.limits.maxPerStageDescriptorStorageImages, "typed heaps exceed per-stage storage image capacity");
if (layout.descriptorType == VK_DESCRIPTOR_TYPE_SAMPLER) Require(count <= context.limits.maxPerStageDescriptorSamplers, "typed heaps exceed per-stage sampler capacity");
}
}
Require(heapTotals[VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE] <= context.limits.maxDescriptorSetSampledImages && heapTotals[VK_DESCRIPTOR_TYPE_STORAGE_IMAGE] <= context.limits.maxDescriptorSetStorageImages && heapTotals[VK_DESCRIPTOR_TYPE_SAMPLER] <= context.limits.maxDescriptorSetSamplers, "typed heaps exceed descriptor set capacity");
}
void ValidateRuntimeResources(const CompiledShader& shader, std::span<const std::uint32_t> words) {
Require(shader.program != nullptr, "missing compiled shader");
const auto& program = *shader.program;
const auto& descriptors = program.bindings;
constexpr auto bindingCount = static_cast<std::uint32_t>(ShaderRecompiler::RuntimeAbi::Binding::Count);
std::array<const ShaderRecompiler::DescriptorBinding*, bindingCount> heaps{};
if (program.runtimeImageCount != 0u) {
for (const auto& binding : descriptors) {
if (binding.role != ShaderRecompiler::DescriptorRole::GuestImages) continue;
auto& heap = heaps[binding.binding % bindingCount];
Require(heap == nullptr, "duplicate runtime image binding");
heap = &binding;
}
}
Require(words.size() == program.shaderDataDwords, "invalid compact shader data size");
constexpr auto metadataWords = sizeof(ShaderRecompiler::RuntimeAbi::ResourceMetadata) / sizeof(std::uint32_t);
Require(program.imageMetadataDword <= words.size() && program.runtimeImageCount <= (words.size() - program.imageMetadataDword) / metadataWords, "runtime image metadata exceeds shader data");
for (const auto index : program.runtimeImageResources) {
Require(index < program.runtimeImageCount, "runtime image resource exceeds its compact layout");
const auto offset = program.imageMetadataDword + index * metadataWords;
const auto kind = words[offset];
const auto first = words[offset + 1u];
const auto count = words[offset + 2u];
Require(count != 0u, "runtime image metadata has no descriptor elements");
Require(kind < heaps.size() && heaps[kind] != nullptr, "runtime metadata references an unbound image heap");
Require(first < heaps[kind]->count && count <= heaps[kind]->count - first, "runtime metadata exceeds its bound heap");
}
}
}
void ShaderResources::buildPrepare(std::span<const CompiledShader> shaders, const ColorTarget* target, std::uint64_t indexAddress, std::size_t indexBytes) {
PerformanceTimer frameTiming("Resources.Prepare");
const auto stageStart = std::chrono::steady_clock::now();
phaseStart = stageStart;
if (BuildProfiled()) {
@ -892,9 +950,11 @@ void ShaderResources::buildPrepare(std::span<const CompiledShader> shaders, cons
}
try {
Require(!shaders.empty() && context.limits.maxBoundDescriptorSets >= 1, "shader descriptor set exceeds device limits");
if (shaders.size() == 1u) refreshResourceKey = ContentKey(shaders.front(), false, true);
std::set<std::uint32_t> occupied;
for (const auto& shader : shaders) {
Require(shader.program != nullptr, "missing compiled shader");
ShaderRecompiler::RuntimeAbi::RequireVersion(shader.program->runtimeAbiVersion);
const VkShaderStageFlags flags = VulkanStage(shader.stage);
std::uint64_t stageStorageBuffers = 0;
std::uint64_t stageResources = 0;
@ -906,7 +966,7 @@ void ShaderResources::buildPrepare(std::span<const CompiledShader> shaders, cons
Require(binding.descriptorSet == 0, "unexpected descriptor set: every shader resource must use descriptor set zero");
Require(occupied.insert(binding.binding).second, "duplicate shader binding");
const bool addressRole = binding.role == ShaderRecompiler::DescriptorRole::BdaPagetable || binding.role == ShaderRecompiler::DescriptorRole::FaultBuffer;
const bool bufferRole = addressRole || binding.role == ShaderRecompiler::DescriptorRole::GuestBuffers || binding.role == ShaderRecompiler::DescriptorRole::ShaderData || binding.role == ShaderRecompiler::DescriptorRole::FlattenedSrt;
const bool bufferRole = addressRole || binding.role == ShaderRecompiler::DescriptorRole::GuestBuffers || binding.role == ShaderRecompiler::DescriptorRole::ShaderData || binding.role == ShaderRecompiler::DescriptorRole::FlattenedSrt || binding.role == ShaderRecompiler::DescriptorRole::Gds;
const bool imageRole = binding.role == ShaderRecompiler::DescriptorRole::GuestImages || binding.role == ShaderRecompiler::DescriptorRole::GuestSamplers;
if (imageRole) {
if (binding.kind == ShaderRecompiler::DescriptorKind::SampledImage || binding.kind == ShaderRecompiler::DescriptorKind::StorageImage) stageResources += binding.count;
@ -945,9 +1005,18 @@ void ShaderResources::buildPrepare(std::span<const CompiledShader> shaders, cons
} else if (addressRole) {
item.allocations.push_back(allocations.size());
allocations.push_back({0, 0, false, nullptr, binding.role});
} else if (binding.role == ShaderRecompiler::DescriptorRole::Gds) {
Require(binding.count == 1 && binding.guestDescriptor.empty(), "invalid GDS descriptor contract");
const auto gds = Pm4::GdsAddress();
guestMemory.AddWritable(gds, Pm4::GdsBytes, true);
item.allocations.push_back(allocations.size());
allocations.push_back({gds, Pm4::GdsBytes, true, nullptr, ShaderRecompiler::DescriptorRole::ShaderData, true});
} else {
Require(binding.count == 1, "shader data and flattened SRT descriptors must not be arrays");
Require(!binding.guestDescriptor.empty(), "empty shader data descriptor");
if (binding.role == ShaderRecompiler::DescriptorRole::ShaderData) {
ValidateRuntimeResources(shader, binding.guestDescriptor);
}
item.allocations.push_back(addDataBuffer(binding.guestDescriptor));
if (binding.role == ShaderRecompiler::DescriptorRole::ShaderData) shaderData = static_cast<std::int64_t>(item.allocations.back());
}
@ -976,6 +1045,7 @@ void ShaderResources::buildPrepare(std::span<const CompiledShader> shaders, cons
if (context.descriptorCache != nullptr && !noLayoutCache) {
_layout = context.descriptorCache->Layout(layoutKey, description);
} else {
ValidateDescriptorLayout(context, description);
VkDescriptorSetLayoutCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
info.bindingCount = static_cast<std::uint32_t>(description.size());
info.pBindings = description.data();
@ -1020,6 +1090,8 @@ void ShaderResources::buildPrepare(std::span<const CompiledShader> shaders, cons
}
void ShaderResources::buildComplete() {
PerformanceTimer frameTiming("Resources.Complete");
Require(!completed, "cannot update a published descriptor heap");
const auto stageStart = std::chrono::steady_clock::now();
phaseStart = stageStart;
try {
@ -1047,7 +1119,7 @@ void ShaderResources::buildComplete() {
std::size_t imageCount = 0;
for (const auto& binding : bindings) {
bufferCount += binding.allocations.size();
imageCount += binding.imageAllocations.size();
if (binding.layout.descriptorType != VK_DESCRIPTOR_TYPE_STORAGE_BUFFER) imageCount += binding.layout.descriptorCount;
}
std::vector<VkDescriptorBufferInfo> buffers;
std::vector<VkDescriptorImageInfo> images;
@ -1068,15 +1140,24 @@ void ShaderResources::buildComplete() {
break;
case VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE:
write.pImageInfo = images.data() + images.size();
for (const auto index : binding.imageAllocations) images.push_back({VK_NULL_HANDLE, textureFirstLayer[index] ? textures[index]->FirstLayerView() : textures[index]->View(), textures[index]->Layout()});
for (const auto index : binding.imageAllocations) {
if (index == std::numeric_limits<std::size_t>::max()) images.push_back({VK_NULL_HANDLE, VK_NULL_HANDLE, VK_IMAGE_LAYOUT_GENERAL});
else images.push_back({VK_NULL_HANDLE, 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;
case VK_DESCRIPTOR_TYPE_STORAGE_IMAGE:
write.pImageInfo = images.data() + images.size();
for (const auto index : binding.imageAllocations) images.push_back({VK_NULL_HANDLE, storageAtomic64[index] ? storageTextures[index]->Atomic64View(storageMips[index], storageFirstLayer[index]) : storageAtomic[index] ? storageTextures[index]->AtomicView(storageMips[index], storageFirstLayer[index]) : storageTextures[index]->StorageView(storageMips[index], storageFirstLayer[index]), VK_IMAGE_LAYOUT_GENERAL});
for (const auto index : binding.imageAllocations) {
if (index == std::numeric_limits<std::size_t>::max()) images.push_back({VK_NULL_HANDLE, VK_NULL_HANDLE, VK_IMAGE_LAYOUT_GENERAL});
else images.push_back({VK_NULL_HANDLE, storageAtomic64[index] ? storageTextures[index]->Atomic64View(storageMips[index], storageFirstLayer[index]) : storageAtomic[index] ? storageTextures[index]->AtomicView(storageMips[index], storageFirstLayer[index]) : storageTextures[index]->StorageView(storageMips[index], storageFirstLayer[index]), VK_IMAGE_LAYOUT_GENERAL});
}
Require(binding.imageAllocations.size() == binding.layout.descriptorCount, "descriptor allocations disagree with compact binding");
break;
case VK_DESCRIPTOR_TYPE_SAMPLER:
write.pImageInfo = images.data() + images.size();
for (const auto index : binding.imageAllocations) images.push_back({samplers[index]->Handle(), VK_NULL_HANDLE, VK_IMAGE_LAYOUT_UNDEFINED});
Require(binding.imageAllocations.size() == binding.layout.descriptorCount, "descriptor allocations disagree with compact binding");
break;
default: throw std::runtime_error("AGC graphics: ShaderResources encountered an unknown descriptor type while writing the descriptor set");
}
@ -1177,42 +1258,41 @@ void ShaderResources::reportDescriptorCaches() const {
const auto descriptors = context.descriptorCache != nullptr ? context.descriptorCache->Counters() : DescriptorCache::Stats{};
const auto samplerHits = context.samplerCache != nullptr ? context.samplerCache->Hits() : 0;
const auto samplerMisses = context.samplerCache != nullptr ? context.samplerCache->Misses() : 0;
AgcDriver::ProfilePrint_nid_no_patch("[descriptors] layouts %llu hits / %llu created, sets %llu from %llu pools, samplers %llu hits / %llu created\n", static_cast<unsigned long long>(descriptors.layoutHits), static_cast<unsigned long long>(descriptors.layoutMisses), static_cast<unsigned long long>(descriptors.sets), static_cast<unsigned long long>(descriptors.pools), static_cast<unsigned long long>(samplerHits), static_cast<unsigned long long>(samplerMisses));
AgcDriver::ProfilePrint_nid_no_patch("[descriptors] layouts %llu hits / %llu created, sets %llu from %llu pools and %llu dedicated pools, samplers %llu hits / %llu created\n", static_cast<unsigned long long>(descriptors.layoutHits), static_cast<unsigned long long>(descriptors.layoutMisses), static_cast<unsigned long long>(descriptors.sets), static_cast<unsigned long long>(descriptors.pools), static_cast<unsigned long long>(descriptors.dedicatedPools), static_cast<unsigned long long>(samplerHits), static_cast<unsigned long long>(samplerMisses));
}
std::vector<std::uint32_t> ShaderResources::ContentKey(const CompiledShader& shader, bool dataWords, bool movableBuffers) {
template<typename TVisitor>
void VisitContentKey(const CompiledShader& shader, bool dataWords, bool movableBuffers, const TVisitor& visit) {
Require(shader.program != nullptr, "missing compiled shader");
const auto& program = *shader.program;
std::vector<std::uint32_t> key;
key.reserve(8 + program.bindings.size() * 12);
key.push_back(dataWords ? 1u : 0u);
key.push_back(static_cast<std::uint32_t>(shader.stage));
key.push_back(static_cast<std::uint32_t>(program.variantId));
key.push_back(static_cast<std::uint32_t>(program.variantId >> 32u));
key.push_back(static_cast<std::uint32_t>(program.bindings.size()));
visit(dataWords ? 1u : 0u);
visit(static_cast<std::uint32_t>(shader.stage));
visit(static_cast<std::uint32_t>(program.PipelineVariantId()));
visit(static_cast<std::uint32_t>(program.PipelineVariantId() >> 32u));
visit(static_cast<std::uint32_t>(program.bindings.size()));
const auto packBits = [&](const std::vector<bool>& bits) {
key.push_back(static_cast<std::uint32_t>(bits.size()));
visit(static_cast<std::uint32_t>(bits.size()));
std::uint32_t word = 0;
for (std::size_t i = 0; i < bits.size(); ++i) {
if (bits[i]) word |= 1u << (i % 32u);
if (i % 32u == 31u || i + 1 == bits.size()) {
key.push_back(word);
visit(word);
word = 0;
}
}
};
for (const auto& binding : program.bindings) {
key.insert(key.end(), {static_cast<std::uint32_t>(binding.kind), static_cast<std::uint32_t>(binding.role), binding.descriptorSet, binding.binding, binding.count, binding.readOnly ? 1u : 0u, binding.imageShape.has_value() ? static_cast<std::uint32_t>(*binding.imageShape) + 1u : 0u, static_cast<std::uint32_t>(binding.guestDescriptor.size())});
for (const auto word : {static_cast<std::uint32_t>(binding.kind), static_cast<std::uint32_t>(binding.role), binding.descriptorSet, binding.binding, binding.count, binding.readOnly ? 1u : 0u, binding.imageShape.has_value() ? static_cast<std::uint32_t>(*binding.imageShape) + 1u : 0u, static_cast<std::uint32_t>(binding.guestDescriptor.size())}) visit(word);
if (movableBuffers && binding.role == ShaderRecompiler::DescriptorRole::GuestBuffers && binding.guestDescriptor.size() == static_cast<std::size_t>(binding.count) * 4u) {
for (std::uint32_t element = 0; element < binding.count; ++element) {
const auto* words = binding.guestDescriptor.data() + static_cast<std::size_t>(element) * 4u;
const bool written = element >= binding.bufferWritten.size() || binding.bufferWritten[element];
const bool empty = words[2] == 0 || (words[0] == 0 && (words[1] & 0xffffu) == 0);
if (written || empty) key.insert(key.end(), words, words + 4);
else key.insert(key.end(), {0u, words[1] & 0xffff0000u, 0u, words[3]});
if (written || empty) { for (std::uint32_t index = 0; index < 4u; ++index) visit(words[index]); }
else { for (const auto word : {0u, words[1] & 0xffff0000u, 0u, words[3]}) visit(word); }
}
} else if ((dataWords && !movableBuffers) || !DataRole(binding.role)) {
key.insert(key.end(), binding.guestDescriptor.begin(), binding.guestDescriptor.end());
for (const auto word : binding.guestDescriptor) visit(word);
}
packBits(binding.imageWritten);
packBits(binding.samplerDepthCompare);
@ -1223,12 +1303,29 @@ std::vector<std::uint32_t> ShaderResources::ContentKey(const CompiledShader& sha
// Read-only elements are bound without a write set: an object built for one written set
// must not serve a build with another (the variant implies it, this makes it explicit).
packBits(binding.bufferWritten);
key.push_back(static_cast<std::uint32_t>(binding.imageSamplers.size()));
key.insert(key.end(), binding.imageSamplers.begin(), binding.imageSamplers.end());
visit(static_cast<std::uint32_t>(binding.imageSamplers.size()));
for (const auto sampler : binding.imageSamplers) visit(sampler);
}
}
std::vector<std::uint32_t> ShaderResources::ContentKey(const CompiledShader& shader, bool dataWords, bool movableBuffers) {
Require(shader.program != nullptr, "missing compiled shader");
std::vector<std::uint32_t> key;
key.reserve(8u + shader.program->bindings.size() * 12u);
VisitContentKey(shader, dataWords, movableBuffers, [&](std::uint32_t word) { key.push_back(word); });
return key;
}
bool MatchesContentKey(const CompiledShader& shader, std::span<const std::uint32_t> key) {
std::size_t cursor = 0;
bool equal = true;
VisitContentKey(shader, false, true, [&](std::uint32_t word) {
equal &= cursor < key.size() && key[cursor] == word;
++cursor;
});
return equal && cursor == key.size();
}
namespace {
// APS5_PROFILE_DRAW: Revalidate outcomes and time, printed as [rescache] every 10 s next to the
@ -1462,7 +1559,7 @@ bool ShaderResources::fastRevalidate(std::uint64_t serialBefore, std::span<const
// since its generation (the query below). The surface's own keys come from the same
// proof when they are the image's, else from the texture's.
const auto& own = source->Descriptor();
const auto sourceKeys = own.dccAddress != 0 ? ProvedClearKeys(own, source->GuestBytes(), source->KeyProof()) : DccKeys::Uncompressed;
const auto sourceKeys = source->ProvedKeys();
if (sourceKeys != source->UploadedKeys()) return fail(FastFail::Keys);
const auto keys = surface.resource.dccAddress == 0 ? DccKeys::Uncompressed : SameKeySurface(source, surface.resource, surface.bytes) ? sourceKeys : ProvedClearKeys(surface.resource, surface.bytes, textures[i]->KeyProof());
if (surface.resource.dccAddress != 0 && surface.resource.dccAddress != own.dccAddress && (keys != DccKeys::Uncompressed || !StorageImageServesKeys(*source, surface.resource.dccAddress))) return fail(FastFail::Keys);
@ -1504,7 +1601,7 @@ bool ShaderResources::fastRevalidate(std::uint64_t serialBefore, std::span<const
// Refresh's unchanged branch (its key compare against the keys the content was
// uploaded under, through the image's proof; the memory query below), minus its
// byte-compare fallback; the image's generation is left where it is.
if (!keyProofs || ProvedClearKeys(own, bytes, image->KeyProof()) != image->UploadedKeys()) return fail(FastFail::StorageKeys);
if (!keyProofs || image->ProvedKeys() != image->UploadedKeys()) return fail(FastFail::StorageKeys);
countKeysProven(true);
}
if (!unchanged) query(address, address + bytes, image, nullptr, {i, true, false});
@ -1666,17 +1763,23 @@ ShaderResources::OwnRefreshFallback ShaderResources::refreshOwnObjects(std::span
for (const auto& binding : shader.program->bindings) {
if (binding.role != ShaderRecompiler::DescriptorRole::GuestImages) continue;
if (binding.kind == ShaderRecompiler::DescriptorKind::SampledImage) {
for (std::uint32_t element = 0; element < binding.count; ++element, ++textureIndex) {
for (std::uint32_t element = 0; element < binding.count; ++element) {
const auto words = std::span<const std::uint32_t>(binding.guestDescriptor).subspan(static_cast<std::size_t>(element) * 8u, 8u);
if (words[0] == 0u && (words[1] & 0xffu) == 0u) continue;
if (textureIndex >= textures.size()) return OwnRefreshFallback::Uncached;
const auto overlap = listed(textureIndex, false);
const auto index = textureIndex++;
const auto overlap = listed(index, false);
if (overlap == overlapping.end()) continue;
if (const auto fallback = refreshView(textureIndex, *overlap); fallback != OwnRefreshFallback::Count) return fallback;
if (const auto fallback = refreshView(index, *overlap); fallback != OwnRefreshFallback::Count) return fallback;
}
} else if (binding.kind == ShaderRecompiler::DescriptorKind::StorageImage) {
for (std::uint32_t element = 0; element < binding.count; ++element, ++storageIndex) {
for (std::uint32_t element = 0; element < binding.count; ++element) {
const auto words = std::span<const std::uint32_t>(binding.guestDescriptor).subspan(static_cast<std::size_t>(element) * 8u, 8u);
if (words[0] == 0u && (words[1] & 0xffu) == 0u) continue;
if (storageIndex >= storageTextures.size()) return OwnRefreshFallback::Uncached;
if (listed(storageIndex, true) == overlapping.end()) continue;
if (const auto fallback = refreshStorage(storageIndex); fallback != OwnRefreshFallback::Count) return fallback;
const auto index = storageIndex++;
if (listed(index, true) == overlapping.end()) continue;
if (const auto fallback = refreshStorage(index); fallback != OwnRefreshFallback::Count) return fallback;
}
}
}
@ -1715,6 +1818,7 @@ bool ShaderResources::Revalidate(std::span<const CompiledShader> shaders, ProofR
const auto elementWords = binding.count != 0 ? binding.guestDescriptor.size() / binding.count : 0;
for (std::uint32_t element = 0; element < binding.count; ++element) {
const auto words = std::span<const std::uint32_t>(binding.guestDescriptor).subspan(static_cast<std::size_t>(element) * elementWords, elementWords);
if (words[0] == 0u && (words[1] & 0xffu) == 0u) continue;
const auto resource = DecodeTextureResource(words);
const VkComponentMapping components = ViewComponents(resource);
if (textureIndex >= textures.size() || cachedTexture(context, words, resource, components, 0, !binding.imageDepthCompare.empty() && binding.imageDepthCompare.at(element)) != textures[textureIndex]) return false;
@ -1723,6 +1827,7 @@ bool ShaderResources::Revalidate(std::span<const CompiledShader> shaders, ProofR
} else if (binding.kind == ShaderRecompiler::DescriptorKind::StorageImage) {
for (std::uint32_t element = 0; element < binding.count; ++element) {
const auto words = std::span<const std::uint32_t>(binding.guestDescriptor).subspan(static_cast<std::size_t>(element) * 8u, 8u);
if (words[0] == 0u && (words[1] & 0xffu) == 0u) continue;
if (storageIndex >= storageTextures.size()) return false;
std::shared_ptr<StorageTexture> expected;
if (SameAsPreviousStorageElement(binding, element) && StorageDedupeEnabled()) expected = storageTextures[storageIndex - 1];
@ -2094,6 +2199,7 @@ DescriptorCache::DescriptorCache(const Context& context) : context(context), des
DescriptorCache::~DescriptorCache() {
for (const auto pool : pools) destroyPool(context.device, pool, nullptr);
for (const auto pool : dedicated) destroyPool(context.device, pool, nullptr);
for (const auto& [key, layout] : layouts) destroyLayout(context.device, layout, nullptr);
}
@ -2104,6 +2210,7 @@ VkDescriptorSetLayout DescriptorCache::Layout(std::span<const std::uint32_t> key
++stats.layoutHits;
return found->second;
}
ValidateDescriptorLayout(context, bindings);
++stats.layoutMisses;
VkDescriptorSetLayoutCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
info.bindingCount = static_cast<std::uint32_t>(bindings.size());
@ -2115,21 +2222,54 @@ VkDescriptorSetLayout DescriptorCache::Layout(std::span<const std::uint32_t> key
}
namespace {
std::uint32_t SetDescriptorLimit(const VkPhysicalDeviceLimits& limits, VkDescriptorType type) {
switch (type) {
case VK_DESCRIPTOR_TYPE_STORAGE_BUFFER: return limits.maxDescriptorSetStorageBuffers;
case VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE: return limits.maxDescriptorSetSampledImages;
case VK_DESCRIPTOR_TYPE_STORAGE_IMAGE: return limits.maxDescriptorSetStorageImages;
case VK_DESCRIPTOR_TYPE_SAMPLER: return limits.maxDescriptorSetSamplers;
default: throw std::runtime_error("AGC graphics: descriptor set uses an unsupported descriptor type " + std::to_string(type));
}
}
// What one chain pool holds; a set needing more of any type gets a dedicated pool.
constexpr std::uint32_t ChainPoolSets = 1024;
constexpr std::array<VkDescriptorPoolSize, 4> ChainPoolSizes{{{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4096}, {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1024}, {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1024}, {VK_DESCRIPTOR_TYPE_SAMPLER, 512}}};
constexpr std::array<VkDescriptorPoolSize, 4> ChainPoolCreateSizes{{{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4096 + 2}, {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1024}, {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1024}, {VK_DESCRIPTOR_TYPE_SAMPLER, 512}}};
}
DescriptorCache::SetAllocation DescriptorCache::Allocate(VkDescriptorSetLayout layout, std::span<const VkDescriptorPoolSize> sizes) {
bool fits = true;
for (const auto& size : sizes) {
Require(size.descriptorCount <= SetDescriptorLimit(context.limits, size.type), "descriptor set exceeds the device's per-set descriptor limit");
const auto capacity = std::find_if(ChainPoolSizes.begin(), ChainPoolSizes.end(), [&](const auto& item) { return item.type == size.type; });
if (capacity == ChainPoolSizes.end() || size.descriptorCount > capacity->descriptorCount) return {};
if (capacity == ChainPoolSizes.end() || size.descriptorCount > capacity->descriptorCount) fits = false;
}
std::lock_guard lock(mutex);
const auto allocate = context.Resolved(&DeviceFunctions::allocateDescriptorSets, "vkAllocateDescriptorSets");
VkDescriptorSetAllocateInfo allocation{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
allocation.descriptorSetCount = 1;
allocation.pSetLayouts = &layout;
if (!fits) {
dedicated.reserve(dedicated.size() + 1);
VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
poolInfo.maxSets = 1;
poolInfo.poolSizeCount = static_cast<std::uint32_t>(sizes.size());
poolInfo.pPoolSizes = sizes.data();
VkDescriptorPool pool = VK_NULL_HANDLE;
Check(context.Function<PFN_vkCreateDescriptorPool>("vkCreateDescriptorPool")(context.device, &poolInfo, nullptr, &pool), "vkCreateDescriptorPool");
allocation.descriptorPool = pool;
VkDescriptorSet set = VK_NULL_HANDLE;
const auto result = allocate(context.device, &allocation, &set);
if (result != VK_SUCCESS) {
destroyPool(context.device, pool, nullptr);
Check(result, "vkAllocateDescriptorSets");
}
dedicated.push_back(pool);
++stats.dedicatedPools;
++stats.sets;
return {set, pool};
}
// Newest pool first: it has the most room; a full or fragmented pool is left for its sets to
// drain and tried again later.
for (auto it = pools.rbegin(); it != pools.rend(); ++it) {
@ -2145,8 +2285,8 @@ DescriptorCache::SetAllocation DescriptorCache::Allocate(VkDescriptorSetLayout l
VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
poolInfo.maxSets = ChainPoolSets;
poolInfo.poolSizeCount = static_cast<std::uint32_t>(ChainPoolSizes.size());
poolInfo.pPoolSizes = ChainPoolSizes.data();
poolInfo.poolSizeCount = static_cast<std::uint32_t>(ChainPoolCreateSizes.size());
poolInfo.pPoolSizes = ChainPoolCreateSizes.data();
VkDescriptorPool pool = VK_NULL_HANDLE;
Check(context.Function<PFN_vkCreateDescriptorPool>("vkCreateDescriptorPool")(context.device, &poolInfo, nullptr, &pool), "vkCreateDescriptorPool");
pools.push_back(pool);
@ -2161,6 +2301,11 @@ DescriptorCache::SetAllocation DescriptorCache::Allocate(VkDescriptorSetLayout l
void DescriptorCache::Free(const SetAllocation& allocation) noexcept {
if (allocation.set == VK_NULL_HANDLE || allocation.pool == VK_NULL_HANDLE) return;
std::lock_guard lock(mutex);
if (const auto found = std::find(dedicated.begin(), dedicated.end(), allocation.pool); found != dedicated.end()) {
dedicated.erase(found);
destroyPool(context.device, allocation.pool, nullptr);
return;
}
static_cast<void>(freeSets(context.device, allocation.pool, 1, &allocation.set));
}
@ -2307,6 +2452,12 @@ bool ShaderResources::DataWordsDiffer(const CompiledShader& shader) const {
}
bool ShaderResources::RefreshData(VkCommandBuffer commands, const CompiledShader& shader, Recorder* recorder) {
Require(shader.program != nullptr, "missing compiled shader");
Require(!refreshResourceKey.empty() && MatchesContentKey(shader, refreshResourceKey), "runtime data refresh cannot replace bound resources");
return refreshData(commands, shader, recorder);
}
bool ShaderResources::refreshData(VkCommandBuffer commands, const CompiledShader& shader, Recorder* recorder) {
Require(shader.program != nullptr, "missing compiled shader");
const auto& program = *shader.program;
Require(program.bindings.size() == bindings.size(), "template bindings disagree with the shader");
@ -2323,6 +2474,7 @@ bool ShaderResources::RefreshData(VkCommandBuffer commands, const CompiledShader
const auto size = binding.guestDescriptor.size() * sizeof(std::uint32_t);
Require(allocation.buffer != nullptr && !allocation.guest && allocation.size == size && size <= MaxRefreshBytes, "template data buffer cannot take the dispatch's words");
if (allocation.dataWords.size() == binding.guestDescriptor.size() && std::equal(allocation.dataWords.begin(), allocation.dataWords.end(), binding.guestDescriptor.begin())) continue;
if (binding.role == ShaderRecompiler::DescriptorRole::ShaderData) ValidateRuntimeResources(shader, binding.guestDescriptor);
if (recorder != nullptr && timing == Recorder::NoTiming) timing = recorder->BeginGpuTiming(Recorder::CommandClass::TemplateDataRefresh);
writeDataWords(commands, bindings[index].allocations.front(), binding.guestDescriptor);
allocation.dataWords.assign(binding.guestDescriptor.begin(), binding.guestDescriptor.end());
@ -2355,13 +2507,21 @@ void ShaderResources::PrecollectSurfaces() const {
void ShaderResources::addImageBinding(const ShaderRecompiler::DescriptorBinding& binding, VkShaderStageFlags flags) {
Require(binding.count != 0, "empty descriptor binding");
const auto heap = static_cast<ShaderRecompiler::RuntimeAbi::Binding>(binding.binding % static_cast<std::uint32_t>(ShaderRecompiler::RuntimeAbi::Binding::Count));
const auto capacity = binding.count;
Require(binding.count <= ShaderRecompiler::RuntimeAbi::HeapCapacity(heap), "typed descriptor heap capacity exceeded");
const auto localBinding = static_cast<std::uint32_t>(heap);
const bool sampledHeap = localBinding >= ShaderRecompiler::RuntimeAbi::FirstImageBinding && localBinding < ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding;
const bool storageHeap = localBinding >= ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding && localBinding < static_cast<std::uint32_t>(ShaderRecompiler::RuntimeAbi::Binding::Samplers);
Require((binding.kind == ShaderRecompiler::DescriptorKind::SampledImage && sampledHeap) || (binding.kind == ShaderRecompiler::DescriptorKind::StorageImage && storageHeap) || (binding.kind == ShaderRecompiler::DescriptorKind::Sampler && heap == ShaderRecompiler::RuntimeAbi::Binding::Samplers), "resource class disagrees with typed descriptor heap");
Require(binding.kind == ShaderRecompiler::DescriptorKind::Sampler || context.nullDescriptors, "typed image heaps require enabled nullDescriptor");
if (binding.kind == ShaderRecompiler::DescriptorKind::StorageImage) {
// Storage images are guest textures the shader writes (looked up in stage B).
Require(binding.role == ShaderRecompiler::DescriptorRole::GuestImages, "storage image binding has a non-image role");
Require(binding.guestDescriptor.size() == static_cast<std::size_t>(binding.count) * 8u, "guest storage image descriptors must contain 8 dwords each");
Require(context.detiler != nullptr, "device texture detiler is unavailable");
Require(binding.count <= context.limits.maxPerStageDescriptorStorageImages, "shader storage-image descriptors exceed per-stage limits");
bindings.push_back({{binding.binding, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, binding.count, flags, nullptr}, {}, {}});
Require(capacity <= context.limits.maxPerStageDescriptorStorageImages, "shader storage-image descriptors exceed per-stage limits");
bindings.push_back({{binding.binding, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, capacity, flags, nullptr}, {}, {}});
plannedStorageImages += binding.count;
deferredImages.push_back({&binding, bindings.size() - 1});
return;
@ -2373,19 +2533,19 @@ void ShaderResources::addImageBinding(const ShaderRecompiler::DescriptorBinding&
Require(binding.guestDescriptor.size() % binding.count == 0, "guest image descriptor size is not a multiple of the binding count");
const auto elementWords = binding.guestDescriptor.size() / binding.count;
Binding item{{binding.binding, sampledImage ? VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE : VK_DESCRIPTOR_TYPE_SAMPLER, binding.count, flags, nullptr}, {}, {}};
Binding item{{binding.binding, sampledImage ? VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE : VK_DESCRIPTOR_TYPE_SAMPLER, capacity, flags, nullptr}, {}, {}};
if (sampledImage) {
Require(elementWords == 8, "guest texture descriptor must contain 8 dwords");
Require(binding.imageShape.has_value(), "guest image binding is missing an image shape");
Require(context.detiler != nullptr, "device texture detiler is unavailable");
Require(context.textureCache != nullptr, "device texture cache is unavailable");
Require(binding.count <= context.limits.maxPerStageDescriptorSampledImages, "shader sampled-image descriptors exceed per-stage limits");
Require(capacity <= context.limits.maxPerStageDescriptorSampledImages, "shader sampled-image descriptors exceed per-stage limits");
plannedSampledImages += binding.count;
Require(plannedSampledImages <= context.limits.maxDescriptorSetSampledImages, "pipeline sampled-image descriptors exceed device limits");
} else {
Require(elementWords == 4, "guest sampler descriptor must contain 4 dwords");
Require(binding.count <= context.limits.maxPerStageDescriptorSamplers, "shader sampler descriptors exceed per-stage limits");
Require(capacity <= context.limits.maxPerStageDescriptorSamplers, "shader sampler descriptors exceed per-stage limits");
Require(binding.samplerDepthCompare.size() == binding.count, "guest sampler binding is missing depth comparison metadata");
Require(binding.samplerUnnormalized.empty() || binding.samplerUnnormalized.size() == binding.count, "guest sampler binding has unnormalized coordinate metadata of another size");
for (std::uint32_t element = 0; element < binding.count; ++element) {
@ -2452,7 +2612,7 @@ bool ShaderResources::precollectImages() {
const auto words = std::span<const std::uint32_t>(binding.guestDescriptor).subspan(static_cast<std::size_t>(element) * elementWords, elementWords);
ImageRecord record;
record.sampled = binding.kind == ShaderRecompiler::DescriptorKind::SampledImage;
try {
if (words[0] != 0u || (words[1] & 0xffu) != 0u) {
record.resource = DecodeTextureResource(words);
record.guestBytes = DescribeSurface(record.resource).guestBytes;
record.generation = GuestMemory::CollectWrites(record.resource.baseAddress, static_cast<std::size_t>(record.guestBytes));
@ -2471,8 +2631,6 @@ bool ShaderResources::precollectImages() {
counters.records.fetch_add(1, std::memory_order_relaxed);
}
}
} catch (const std::exception&) {
record.decoded = false;
}
imageRecords.push_back(std::move(record));
}
@ -2545,6 +2703,11 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind
for (std::uint32_t element = 0; element < binding.count; ++element) {
const auto words = std::span<const std::uint32_t>(binding.guestDescriptor).subspan(static_cast<std::size_t>(element) * elementWords, elementWords);
const auto* record = nextRecord();
if (words[0] == 0u && (words[1] & 0xffu) == 0u) {
item.imageAllocations.push_back(std::numeric_limits<std::size_t>::max());
--plannedSampledImages;
continue;
}
const auto resource = record != nullptr && record->decoded ? record->resource : DecodeTextureResource(words);
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)) + ")");
@ -2580,6 +2743,11 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind
if (sameAsPrevious) ++mipOffset;
else mipOffset = 0;
const auto* record = nextRecord();
if (words[0] == 0u && (words[1] & 0xffu) == 0u) {
item.imageAllocations.push_back(std::numeric_limits<std::size_t>::max());
--plannedStorageImages;
continue;
}
const auto resource = record != nullptr && record->decoded ? record->resource : DecodeTextureResource(words);
const bool firstLayer = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k2DArray;
if (binding.imageShape.has_value() && !firstLayer && !MatchesGuestDimension(*binding.imageShape, resource.dimension)) throw std::runtime_error("AGC graphics: guest storage 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)) + ")");
@ -2643,12 +2811,9 @@ std::vector<std::pair<std::uint64_t, std::uint64_t>> ShaderResources::PresyncSur
// Stage A has not run (an address-based build, or the pass is off): decoded from the shader.
if (deferredCompute.program == nullptr) return surfaces;
forEachImageElement(*deferredCompute.program, [&](const ShaderRecompiler::DescriptorBinding& binding, std::uint32_t, std::span<const std::uint32_t> words) {
try {
const auto resource = DecodeTextureResource(words);
consider(resource.format, resource.baseAddress, DescribeSurface(resource).guestBytes, binding.kind == ShaderRecompiler::DescriptorKind::SampledImage);
} catch (const std::exception&) {
// Stage B reports the bad descriptor.
}
if (words[0] == 0u && (words[1] & 0xffu) == 0u) return;
const auto resource = DecodeTextureResource(words);
consider(resource.format, resource.baseAddress, DescribeSurface(resource).guestBytes, binding.kind == ShaderRecompiler::DescriptorKind::SampledImage);
});
return surfaces;
}
@ -2714,7 +2879,7 @@ std::optional<std::vector<ShaderResources::MovedBuffer>> ShaderResources::MovedR
const bool written = element >= binding.bufferWritten.size() || binding.bufferWritten[element];
if (written || empty || item.written || size > context.limits.maxStorageBufferRange) return std::nullopt;
const auto begin = address - item.adjustment;
const auto bytes = static_cast<std::size_t>(size) + item.adjustment;
const auto bytes = static_cast<std::size_t>(GuestBufferMemory::ViewBytes(size, item.adjustment));
if (!GuestMemory::Accessible(reinterpret_cast<const void*>(begin), bytes)) return std::nullopt;
if (guestMemory.WritesOverlap(begin, bytes) || recorder.PendingWriteOverlaps(begin, bytes) || PendingStorageOverlaps(begin, bytes, nullptr) || AnyShadowedOverlaps(begin, bytes)) return std::nullopt;
moved.push_back({index, address, static_cast<std::size_t>(size)});
@ -2753,7 +2918,7 @@ std::shared_ptr<ShaderResources::DrawBindings> ShaderResources::PrepareDrawBindi
if (!direct || recorder.PendingWriteOverlaps(item.address, item.size)) continue;
}
const auto begin = address - item.adjustment;
const auto bytes = size + item.adjustment;
const auto bytes = static_cast<std::size_t>(GuestBufferMemory::ViewBytes(size, item.adjustment));
const auto registryGeneration = GuestAllocations::GuestAllocationsGeneration_nid_postfix();
const auto generation = GuestMemory::CollectWrites(begin, bytes);
auto buffer = recorder.ReusableDrawSnapshot(begin, bytes);

View File

@ -1,5 +1,6 @@
#include "BdaAbi.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp"
#include "prx/libSceAgcDriver/Graphics/include/PipelineSpecialization.hpp"
#include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp"
#include <spirv/unified1/spirv.hpp>
#include <algorithm>
@ -132,6 +133,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
using Stage = ShaderRecompiler::ShaderStage;
Require(compiled.program != nullptr, "missing compiled shader");
const auto& shader = *compiled.program;
PipelineSpecialization::Validate(shader);
const auto stage = compiled.stage;
const bool vertex = stage == Stage::Vertex || stage == Stage::Local;
const bool fragment = stage == Stage::Fragment;
@ -181,6 +183,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
case spv::CapabilityGroupNonUniform: subgroupOperations = VK_SUBGROUP_FEATURE_BASIC_BIT; break;
case spv::CapabilityGroupNonUniformBallot: subgroupOperations = VK_SUBGROUP_FEATURE_BASIC_BIT | VK_SUBGROUP_FEATURE_BALLOT_BIT; break;
case spv::CapabilityGroupNonUniformShuffle: subgroupOperations = VK_SUBGROUP_FEATURE_BASIC_BIT | VK_SUBGROUP_FEATURE_SHUFFLE_BIT; break;
case spv::CapabilityGroupNonUniformArithmetic: subgroupOperations = VK_SUBGROUP_FEATURE_BASIC_BIT | VK_SUBGROUP_FEATURE_ARITHMETIC_BIT; break;
default: break;
}
const bool isSubgroupCapability = subgroupOperations != 0;
@ -207,6 +210,10 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
mesh &&
capability == spv::CapabilityMeshShadingEXT;
const bool isInterlockCapability =
fragment &&
capability == spv::CapabilityFragmentShaderPixelInterlockEXT;
// Enabled unconditionally or by the device setup in VulkanDevice.
const bool isFeatureCapability =
capability == spv::CapabilitySampled1D ||
@ -215,6 +222,8 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
capability == spv::CapabilityImageQuery ||
(fragment && geometryShader && capability == spv::CapabilityGeometry) ||
(fragment && sampleRateShading && capability == spv::CapabilitySampleRateShading) ||
capability == spv::CapabilityImageMSArray ||
capability == spv::CapabilityStorageImageMultisample ||
capability == spv::CapabilityStorageImageWriteWithoutFormat ||
capability == spv::CapabilityStorageImageReadWithoutFormat ||
capability == spv::CapabilityInt64 ||
@ -241,6 +250,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
isBdaCapability ||
isTessellationCapability ||
isMeshCapability ||
isInterlockCapability ||
isFeatureCapability ||
isDescriptorIndexingCapability ||
(imageInt64Atomics && (capability == spv::CapabilityInt64Atomics || capability == spv::CapabilityInt64ImageEXT)),
@ -256,6 +266,10 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
const auto end = std::find(text, text + bytes.size(), '\0');
Require(end != text + bytes.size(), "unterminated SPIR-V extension");
const std::string_view extension(text, static_cast<std::size_t>(end - text));
if (extension == "SPV_EXT_fragment_shader_interlock") {
Require(fragment, "SPV_EXT_fragment_shader_interlock requires a fragment shader");
break;
}
if (extension == "SPV_KHR_fragment_shader_barycentric") {
Require(fragment && fragmentShaderBarycentric, "SPV_KHR_fragment_shader_barycentric requires enabled fragmentShaderBarycentric in a fragment shader");
break;
@ -267,7 +281,6 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
case spv::OpDecorationGroup:
case spv::OpGroupDecorate:
case spv::OpGroupMemberDecorate:
case spv::OpSpecConstant:
case spv::OpSpecConstantTrue:
case spv::OpSpecConstantFalse:
case spv::OpSpecConstantComposite:
@ -345,6 +358,8 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
case spv::OpConstant:
if (count == 4) module.constants.emplace(instruction[2], instruction[3]);
break;
case spv::OpSpecConstant:
break;
default: break;
}
cursor += count;
@ -376,7 +391,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
mode(spv::ExecutionModeVertexOrderCw, {});
Require(module.modes.size() == 3, "unsupported tessellation-evaluation execution mode");
} else if (fragment) {
for (const auto& [name, operands] : module.modes) Require(operands.empty() && (name == spv::ExecutionModeOriginUpperLeft || name == spv::ExecutionModeEarlyFragmentTests || name == spv::ExecutionModeDepthReplacing || name == spv::ExecutionModeDepthLess || name == spv::ExecutionModeDepthGreater), "unsupported fragment execution mode");
for (const auto& [name, operands] : module.modes) Require(operands.empty() && (name == spv::ExecutionModeOriginUpperLeft || name == spv::ExecutionModeEarlyFragmentTests || name == spv::ExecutionModeDepthReplacing || name == spv::ExecutionModeDepthLess || name == spv::ExecutionModeDepthGreater || name == spv::ExecutionModePixelInterlockOrderedEXT), "unsupported fragment execution mode");
} else Require(module.modes.empty(), "unsupported vertex execution mode");
std::set<std::pair<std::uint32_t, std::uint32_t>> descriptors;
bool push = false;
@ -451,6 +466,11 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
const auto binding = std::find_if(shader.bindings.begin(), shader.bindings.end(), [&](const auto& item) { return item.descriptorSet == key.first && item.binding == key.second; });
Require(binding != shader.bindings.end(), "SPIR-V resource is absent from recompiler binding metadata");
Require(binding->kind == ShaderRecompiler::DescriptorKind::Sampler || binding->kind == ShaderRecompiler::DescriptorKind::SampledImage || binding->kind == ShaderRecompiler::DescriptorKind::StorageImage, "SPIR-V descriptor type disagrees with recompiler binding metadata");
Require(type.size() == 4 && (type[0] & 0xffffu) == spv::OpTypeArray, "typed heap must be declared as a descriptor array");
const auto heap = static_cast<ShaderRecompiler::RuntimeAbi::Binding>(binding->binding % static_cast<std::uint32_t>(ShaderRecompiler::RuntimeAbi::Binding::Count));
const auto capacity = ShaderRecompiler::RuntimeAbi::HeapCapacity(heap);
const auto count = module.constants.find(type[3]);
Require(binding->count != 0u && binding->count <= capacity && count != module.constants.end() && count->second == binding->count, "typed heap array length disagrees with compact binding");
} else {
Require(variable.storage == spv::StorageClassStorageBuffer && decoration.set && decoration.binding, "unsupported or unbound shader resource");
const auto key = std::make_pair(*decoration.set, *decoration.binding);
@ -460,7 +480,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
Require(binding->kind == ShaderRecompiler::DescriptorKind::StorageBuffer, "SPIR-V descriptor type disagrees with recompiler binding metadata");
Require(!binding->readOnly, "read-only descriptor metadata is unsupported because the recompiler emits no NonWritable decoration");
const bool array = binding->role == ShaderRecompiler::DescriptorRole::GuestBuffers;
Require(array || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (binding->role == ShaderRecompiler::DescriptorRole::BdaPagetable || binding->role == ShaderRecompiler::DescriptorRole::FaultBuffer)) || binding->role == ShaderRecompiler::DescriptorRole::ShaderData || binding->role == ShaderRecompiler::DescriptorRole::FlattenedSrt, "SPIR-V descriptor role is unsupported");
Require(array || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (binding->role == ShaderRecompiler::DescriptorRole::BdaPagetable || binding->role == ShaderRecompiler::DescriptorRole::FaultBuffer)) || binding->role == ShaderRecompiler::DescriptorRole::ShaderData || binding->role == ShaderRecompiler::DescriptorRole::FlattenedSrt || binding->role == ShaderRecompiler::DescriptorRole::Gds, "SPIR-V descriptor role is unsupported");
auto blockId = typeId;
if (array) {
Require(type.size() == 4 && (type[0] & 0xffffu) == spv::OpTypeArray, "guest buffer descriptors must be declared as a descriptor array");
@ -519,7 +539,12 @@ std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders,
if (i != 0) {
for (const auto& [location, signature] : current.inputs) {
const auto output = previous.outputs.find(location);
Require(output != previous.outputs.end() && output->second == signature, "graphics interfaces disagree at location " + std::to_string(location));
if (output == previous.outputs.end() || output->second != signature) {
Require(false, "graphics interfaces disagree at location " + std::to_string(location) +
" (stage " + std::to_string(i - 1) + " output " +
(output == previous.outputs.end() ? "missing" : output->second) +
", stage " + std::to_string(i) + " input " + signature + ")");
}
}
}
previous = current;

View File

@ -56,6 +56,43 @@ std::string vteMessage(std::uint32_t viewportControl) {
return message.str();
}
std::string conservativeMessage(std::uint32_t control) {
std::ostringstream message;
message << "AGC graphics: PA_SC_CONSERVATIVE_RASTERIZATION_CNTL=0x" << std::hex << control << ": only 0x6000 (off) and 0x6001 (overestimation) are supported";
return message.str();
}
void requireConservativeTriangles(bool triangles, const char* primitive, const char* source, std::uint32_t value) {
if (triangles) return;
std::ostringstream message;
message << "AGC graphics: conservative rasterization of " << primitive << " is unsupported (" << source << "=0x" << std::hex << value << "): only triangles are overestimated";
throw std::runtime_error(message.str());
}
VkConservativeRasterizationModeEXT decodeConservativeRasterization(const QueueState& queue) {
const auto& cx = queue.context;
const auto control = read(cx, 0x313);
if (control == 0x6000u) return VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT;
if (control != 0x6001u) throw std::runtime_error(conservativeMessage(control));
const auto stages = read(cx, 0x2d5);
if ((stages & 0x20u) != 0) {
const auto output = read(cx, 0x29b);
const auto type = output & 0x3fu;
const bool perStream = (output & 0x80000000u) != 0;
requireConservativeTriangles(!perStream && type == 2u, perStream ? "per-stream primitive types" : type == 0u ? "points" : type == 1u ? "lines" : "other primitive types", "VGT_GS_OUT_PRIM_TYPE", output);
} else if ((stages & 4u) != 0) {
const auto parameters = read(cx, 0x2db);
const auto domain = parameters & 3u;
const auto topology = (parameters >> 5u) & 7u;
requireConservativeTriangles((domain == 1u || domain == 2u) && (topology == 2u || topology == 3u), topology == 0u ? "points" : topology == 1u || domain == 0u ? "lines" : "other primitive types", "VGT_TF_PARAM", parameters);
} else {
const auto primitive = read(queue.userConfig, 0x242, RegisterBank::UserConfig);
requireConservativeTriangles(primitive == 4u || primitive == 5u || primitive == 6u, primitive == 1u ? "points" : primitive == 2u || primitive == 3u ? "lines" : primitive == 7u || primitive == 17u ? "rectangles" : "other primitive types", "VGT_PRIMITIVE_TYPE", primitive);
}
Require((read(cx, 0x1b3) & 0x44u) == 0, "conservative rasterization with centroid interpolation is unsupported");
return VK_CONSERVATIVE_RASTERIZATION_MODE_OVERESTIMATE_EXT;
}
// 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
@ -64,9 +101,10 @@ constexpr std::uint32_t LayerExports = (1u << 18u) | (1u << 19u) | (1u << 21u) |
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.
constexpr std::uint32_t ShaderControlMask = ~(0x0000f870u | 0x00020600u);
constexpr std::uint32_t ShaderControlMask = ~(0x0000f870u | 0x00020600u | 0x00010000u);
constexpr std::uint32_t PixelStageRunsMask = 0x00020747u;
constexpr std::uint32_t AlphaToCoverageMask = ~0x0001ff00u;
constexpr std::uint32_t ScanModeMask = ~2u;
constexpr std::uint32_t ScanModeMask = ~0x22u;
constexpr std::uint32_t ScanControlMask = ~0x06023fffu;
constexpr std::uint32_t ScreenOffsetMask = ~0x01ff01ffu;
// Bits 26/27 (ZCLIP_NEAR/FAR_DISABLE) become depth clamping; bit 19 selects the [0, 1] clip space.
@ -234,7 +272,8 @@ std::uint32_t effectiveDepthControl(std::uint32_t depthControl) {
}
bool colorControlSupported(std::uint32_t colorControl, bool hasColorTarget) {
return colorControl == 0xcc0010u || (!hasColorTarget && (colorControl & ~0x70u) == 0xcc0000u);
colorControl &= ~1u;
return colorControl == 0xcc0010u || ((colorControl >> 4u) & 7u) == 0u || (!hasColorTarget && (colorControl & ~0x70u) == 0xcc0000u);
}
std::string colorControlMessage(std::uint32_t colorControl) {
@ -292,12 +331,13 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number,
};
const bool alternate = swap == 1;
const auto single = [&](VkFormat vkFormat, std::uint32_t bytes) { return DecodedColorFormat{vkFormat, bytes, static_cast<std::uint8_t>((0xe4u & ~3u) | swap)}; };
if (swap > 1 && format != 1 && format != 2 && format != 4) return fail();
if (swap > 1 && format != 1 && format != 2 && format != 4 && format != 10 && format != 12) return fail();
switch (format) {
case 1:
if (number == unorm) return single(VK_FORMAT_R8_UNORM, 1);
if (number == snorm) return single(VK_FORMAT_R8_SNORM, 1);
if (number == uint) return single(VK_FORMAT_R8_UINT, 1);
if (number == srgb && swap == 0) return single(VK_FORMAT_R8_SRGB, 1);
return fail();
case 2:
if (number == unorm) return single(VK_FORMAT_R16_UNORM, 2);
@ -330,23 +370,27 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number,
// COLOR_2_10_10_10 keeps red in the low bits, the Vulkan A2B10G10R10 packing.
if (number != unorm) return fail();
return {alternate ? VK_FORMAT_A2R10G10B10_UNORM_PACK32 : VK_FORMAT_A2B10G10R10_UNORM_PACK32, 4};
case 10:
if (number == unorm) return {alternate ? VK_FORMAT_B8G8R8A8_UNORM : VK_FORMAT_R8G8B8A8_UNORM, 4};
if (number == snorm) return {alternate ? VK_FORMAT_B8G8R8A8_SNORM : VK_FORMAT_R8G8B8A8_SNORM, 4};
if (number == srgb) return {alternate ? VK_FORMAT_B8G8R8A8_SRGB : VK_FORMAT_R8G8B8A8_SRGB, 4};
case 10: {
const auto reversed = static_cast<std::uint8_t>(swap == 2 ? 0x1bu : swap == 3 ? 0x93u : 0xe4u);
if (number == unorm) return {alternate ? VK_FORMAT_B8G8R8A8_UNORM : VK_FORMAT_R8G8B8A8_UNORM, 4, reversed};
if (number == snorm) return {alternate ? VK_FORMAT_B8G8R8A8_SNORM : VK_FORMAT_R8G8B8A8_SNORM, 4, reversed};
if (number == srgb) return {alternate ? VK_FORMAT_B8G8R8A8_SRGB : VK_FORMAT_R8G8B8A8_SRGB, 4, reversed};
return fail();
}
case 11:
if (swap != 0) return fail();
if (number == floating) return {VK_FORMAT_R32G32_SFLOAT, 8};
if (number == uint) return {VK_FORMAT_R32G32_UINT, 8};
return fail();
case 12:
if (swap != 0) return fail();
if (number == floating) return {VK_FORMAT_R16G16B16A16_SFLOAT, 8};
if (number == unorm) return {VK_FORMAT_R16G16B16A16_UNORM, 8};
if (number == snorm) return {VK_FORMAT_R16G16B16A16_SNORM, 8};
if (number == uint) return {VK_FORMAT_R16G16B16A16_UINT, 8};
case 12: {
if (swap == 1) return fail();
const auto reversed = static_cast<std::uint8_t>(swap == 2 ? 0x1bu : swap == 3 ? 0x93u : 0xe4u);
if (number == floating) return {VK_FORMAT_R16G16B16A16_SFLOAT, 8, reversed};
if (number == unorm) return {VK_FORMAT_R16G16B16A16_UNORM, 8, reversed};
if (number == snorm) return {VK_FORMAT_R16G16B16A16_SNORM, 8, reversed};
if (number == uint) return {VK_FORMAT_R16G16B16A16_UINT, 8, reversed};
return fail();
}
case 14:
if (swap != 0) return fail();
if (number == floating) return {VK_FORMAT_R32G32B32A32_SFLOAT, 16};
@ -396,7 +440,7 @@ ShaderStages DecodeShaderStages(const QueueState& queue) {
APS5_LOG_OUT_DEBUG("DecodeShaderStages value=0x%x primitive=%u path=%u vertexWave=%u", value, primitive, static_cast<unsigned>(result.path), result.vertexWaveSize);
if (path == ShaderPath::Vertex) {
validate((value & 0x2000u) != 0, "legacy vertex routing without PRIMGEN_EN is unsupported");
validate((value & ~0x02402010u) == 0, "unsupported vertex routing, scheduling or wave-ID state");
validate((value & ~0x0247a010u) == 0, "unsupported vertex routing, scheduling or wave-ID state");
} else if (path == ShaderPath::Tessellation) {
validate((value & 0x00600020u) == 0, "wave32 tessellation or geometry amplification is unsupported");
validate((value & ~0x0007ed0du) == 0 && (value & 3u) == 1u && ((value >> 3u) & 3u) == 1u, "unsupported tessellation routing");
@ -429,9 +473,17 @@ ShaderStages DecodeShaderStages(const QueueState& queue) {
return result;
}
std::string DepthMaintenanceRejection(const QueueState& queue) {
const auto control = find(queue.context, 0x000);
if (control == queue.context.end() || (control->second & ~0x2063u) == 0) return {};
return zeroMessage(0x000, control->second, "DB_RENDER_CONTROL depth copy, resummarize or decompress");
}
State DecodeState(const QueueState& queue) {
if (auto reason = DepthMaintenanceRejection(queue); !reason.empty()) throw std::runtime_error(reason);
const auto& cx = queue.context;
State result{};
result.conservativeRasterization = decodeConservativeRasterization(queue);
result.stages = DecodeShaderStages(queue);
const auto primitive = read(queue.userConfig, 0x242, RegisterBank::UserConfig);
APS5_LOG_OUT_DEBUG("DecodeState primitive=%u path=%u vertexWave=%u", primitive, static_cast<unsigned>(result.stages.path), result.stages.vertexWaveSize);
@ -492,7 +544,6 @@ State DecodeState(const QueueState& queue) {
Require(read(cx, 0x83) == 0xffffu, "clip rectangles are unsupported");
Require((read(cx, 0x8c) & 0xfu) == 0xau, "nonstandard triangle edge rules are unsupported");
Require(read(cx, 0x2f9) == 0x2du, "nonstandard pixel center or vertex quantization is unsupported");
Require(read(cx, 0x313) == 0x6000u, "conservative rasterization is unsupported");
Require(read(cx, 0x30e) == 0xffffffffu && read(cx, 0x30f) == 0xffffffffu, "sample masks are unsupported");
const auto viewportControl = read(cx, 0x206);
if (viewportControl != 0x43fu) throw std::runtime_error(vteMessage(viewportControl));
@ -512,7 +563,7 @@ State DecodeState(const QueueState& queue) {
const auto shaderMask = read(cx, 0x8f);
// Channels of targets the pixel shader does not export are never written, so the target mask only
// matters where the shader exports.
const auto targetMask = read(cx, 0x8e) & shaderMask;
const auto targetMask = ColorWriteMask(cx);
APS5_LOG_OUT_DEBUG("CB_TARGET_MASK=0x%x CB_SHADER_MASK=0x%x", targetMask, shaderMask);
std::vector<std::uint32_t> exportSlots;
for (std::uint32_t slot = 0; slot < 8; ++slot) {
@ -523,6 +574,7 @@ State DecodeState(const QueueState& queue) {
for (std::uint32_t index = 0; index < exportSlots.size(); ++index) {
if (written(exportSlots[index])) exportCount = index + 1;
}
if (((read(cx, 0x202) >> 4u) & 7u) == 0u) exportCount = 0;
result.hasColorTarget = exportCount != 0;
APS5_LOG_OUT_DEBUG("hasColorTarget=%u exports=%u", result.hasColorTarget ? 1u : 0u, exportCount);
@ -598,6 +650,7 @@ State DecodeState(const QueueState& queue) {
if (((exportedMask >> ((mapping >> (2u * component)) & 3u)) & 1u) != 0) state.colorWriteMask |= 1u << component;
}
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");
if (state.blendEnable) {
Require((read(cx, 0x31c + slot * 0xfu) & 0x10000u) == 0, "blend bypass conflicts with enabled blending");
state.srcColorBlendFactor = blendFactor(blend & 0x1fu);
@ -630,6 +683,24 @@ std::array<std::uint8_t, 8> ExportMappings(const State& state) {
return mappings;
}
std::size_t CmaskBytes(std::uint32_t width, std::uint32_t height) {
constexpr std::size_t metablockWidth = 1024;
constexpr std::size_t metablockHeight = 512;
constexpr std::size_t metablockBytes = 4096;
return ((width + metablockWidth - 1) / metablockWidth) * ((height + metablockHeight - 1) / metablockHeight) * metablockBytes;
}
std::uint32_t ColorWriteMask(const Registers& context) {
auto mask = read(context, 0x8e) & read(context, 0x8f);
for (std::uint32_t slot = 0; slot < 8; ++slot) {
const auto channels = 0xfu << (slot * 4u);
if ((mask & channels) == 0) continue;
const auto info = find(context, 0x31c + slot * 0xfu);
if (info != context.end() && ((info->second >> 2u) & 0x1fu) == 0) mask &= ~channels;
}
return mask;
}
ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) {
const auto stride = slot * 0xfu;
ColorTarget color{};
@ -641,7 +712,7 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) {
const auto format = (info >> 2u) & 0x1fu;
const auto decoded = DecodeColorFormat(format, number, swap);
// ROUND_MODE (bit 18) only affects unorm rounding. With DCC_ENABLE (bit 28) the target is written
if ((info & ~(0x00039f7cu | 0x00040000u | 0x10000000u)) != 0) throw std::runtime_error("AGC graphics: color compression, DCC, endian conversion, nonstandard rounding or color optimization is unsupported (CB_COLOR_INFO 0x" + [&] { char text[16]; std::snprintf(text, sizeof(text), "%08x", info); return std::string(text); }() + ")");
if ((info & ~(0x00039f7cu | 0x00040000u | 0x10000000u | 0x00002000u)) != 0) throw std::runtime_error("AGC graphics: color compression, DCC, endian conversion, nonstandard rounding or color optimization is unsupported (CB_COLOR_INFO 0x" + [&] { char text[16]; std::snprintf(text, sizeof(text), "%08x", info); return std::string(text); }() + ")");
Require((info & 0x8000u) != 0 || number == 7 || number == 4 || number == 5, "unclamped normalized color is unsupported");
const auto view = read(cx, 0x31b + stride);
Require((view & ~0x3fffffffu) == 0, "reserved CB_COLOR_VIEW bits are set");
@ -689,11 +760,19 @@ 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) {
Require(maxMip == 0 && !volume && slice == 0, "CMASK fast clears of a mipmapped, 3D or array color target are unsupported");
const auto cmaskHigh = find(cx, 0x398 + slot);
color.cmaskAddress = ((cmaskHigh == cx.end() ? 0ull : static_cast<std::uint64_t>(cmaskHigh->second & 0xffu)) << 40u) | (static_cast<std::uint64_t>(read(cx, 0x31f + stride)) << 8u);
Require(color.cmaskAddress != 0, "CMASK fast clears without a CMASK address are unsupported");
color.cmaskBytes = CmaskBytes(color.extent.width, color.extent.height);
}
if ((info & 0x10000000u) != 0) {
if (maxMip == 0) {
const auto dccHigh = find(cx, 0x3a8 + slot);
color.dccAddress = ((dccHigh == cx.end() ? 0ull : static_cast<std::uint64_t>(dccHigh->second & 0xffu)) << 40u) | (static_cast<std::uint64_t>(read(cx, 0x325 + stride)) << 8u);
color.dccAlphaOnMsb = DccAlphaOnMsb(color.format, swap);
color.dccPipeAligned = ((attrib3 >> 30u) & 1u) != 0;
} else {
static bool reported = false;
if (!reported) {
@ -743,6 +822,7 @@ std::optional<ColorMetadataPass> DecodeColorMetadataPass(const QueueState& queue
}
std::string DrawRejection(const QueueState& queue, bool indexed) {
if (auto reason = DepthMaintenanceRejection(queue); !reason.empty()) return reason;
const auto& cx = queue.context;
// A register a rule needs that is absent gives no verdict here: DecodeState reports it.
const auto value = [&](const Registers& registers, std::uint32_t offset, std::uint32_t& out) {
@ -783,15 +863,15 @@ std::string DrawRejection(const QueueState& queue, bool indexed) {
if (value(cx, 0x83, word) && word != 0xffffu) return require(false, "clip rectangles are unsupported");
if (value(cx, 0x8c, word) && (word & 0xfu) != 0xau) return require(false, "nonstandard triangle edge rules are unsupported");
if (value(cx, 0x2f9, word) && word != 0x2du) return require(false, "nonstandard pixel center or vertex quantization is unsupported");
if (value(cx, 0x313, word) && word != 0x6000u) return require(false, "conservative rasterization is unsupported");
if (value(cx, 0x313, word) && word != 0x6000u && word != 0x6001u) return conservativeMessage(word);
std::uint32_t other = 0;
if (value(cx, 0x30e, word) && value(cx, 0x30f, other) && (word != 0xffffffffu || other != 0xffffffffu)) return require(false, "sample masks are unsupported");
if (value(cx, 0x206, word) && word != 0x43fu) return vteMessage(word);
if (auto reason = nonzero(cx, 0x204, ClipControlMask, "unsupported PA_CL_CLIP_CNTL flags"); !reason.empty()) return reason;
std::uint32_t targetMask = 0, shaderMask = 0;
if (value(cx, 0x8e, targetMask) && value(cx, 0x8f, shaderMask) && value(cx, 0x202, word) && !colorControlSupported(word, (targetMask & shaderMask) != 0)) return colorControlMessage(word);
if (value(cx, 0x8e, targetMask) && value(cx, 0x8f, shaderMask) && value(cx, 0x202, word) && !colorControlSupported(word, ColorWriteMask(cx) != 0)) return colorControlMessage(word);
if (auto reason = nonzero(cx, 0x1c4, zFormatSupported(zFormat) ? 0u : ~0u, "depth or sample-mask export"); !reason.empty()) return reason;
if (PixelProgramUnset(queue)) return NullPixelProgramRejection(queue);
if (PixelProgramSkipped(queue)) return NullPixelProgramRejection(queue);
for (const auto offset : {0x1b3u, 0x1b4u, 0x1c5u}) {
if (find(cx, offset) != cx.end()) continue;
char text[64];
@ -801,17 +881,24 @@ std::string DrawRejection(const QueueState& queue, bool indexed) {
return {};
}
bool PixelProgramUnset(const QueueState& queue) {
bool PixelProgramSkipped(const QueueState& queue) {
const auto low = find(queue.shader, 0x008, RegisterBank::Shader);
const auto high = find(queue.shader, 0x009, RegisterBank::Shader);
return low != queue.shader.end() && high != queue.shader.end() && low->second == 0 && high->second == 0;
if (low != queue.shader.end() && high != queue.shader.end() && low->second == 0 && high->second == 0) return true;
const auto& cx = queue.context;
const auto targetMask = find(cx, 0x8e);
const auto shaderMask = find(cx, 0x8f);
const auto zFormat = find(cx, 0x1c4);
const auto shaderControl = find(cx, 0x203);
if (targetMask == cx.end() || shaderMask == cx.end() || zFormat == cx.end() || shaderControl == cx.end()) return false;
return (targetMask->second & shaderMask->second) == 0 && zFormat->second == 0 && (shaderControl->second & PixelStageRunsMask) == 0;
}
std::string NullPixelProgramRejection(const QueueState& queue) {
const auto targetMask = find(queue.context, 0x8e);
const auto shaderMask = find(queue.context, 0x8f);
if (targetMask == queue.context.end() || shaderMask == queue.context.end()) return "AGC graphics: a draw without a pixel program needs CB_TARGET_MASK and CB_SHADER_MASK";
if ((targetMask->second & shaderMask->second) != 0) return "AGC graphics: a draw without a pixel program writes color";
if (ColorWriteMask(queue.context) != 0) return "AGC graphics: a draw without a pixel program writes color";
return {};
}

View File

@ -312,9 +312,9 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex
VkCommandBuffer commands = VK_NULL_HANDLE;
if (recorder != nullptr) {
commands = recorder->Commands();
recorder->Keep(staging);
recorder->Keep(tiled);
recorder->Keep(linear);
recorder->Keep(staging, static_cast<std::size_t>(guestBytes));
recorder->Keep(tiled, static_cast<std::size_t>(guestBytes));
recorder->Keep(linear, static_cast<std::size_t>(linearBytes));
recorder->Keep(owned);
} else {
batch = std::make_unique<CommandBatch>(context);
@ -655,6 +655,28 @@ bool StorageClearAvailable(const Context& context, std::uint32_t guestFormat, Dc
return IsDccClear(keys) && StorageFormatAvailable(context, guestFormat) && ClearColorFor(StorageFormatForGuest(context, guestFormat), keys, clear);
}
VkFormat AttachmentProxyFormat(const Context& context, VkFormat format) {
if (format != VK_FORMAT_R8_SRGB) return VK_FORMAT_UNDEFINED;
struct Table {
std::mutex mutex;
std::unordered_map<VkPhysicalDevice, VkFormat> formats;
};
static Table table;
{
std::lock_guard lock(table.mutex);
if (const auto found = table.formats.find(context.physical); found != table.formats.end()) return found->second;
}
const char* forced = std::getenv("APS5_SRGB_ATTACHMENT_PROXY");
VkFormatProperties properties{};
context.formatProperties(context.physical, format, &properties);
constexpr VkFormatFeatureFlags attachment = VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT | VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT;
const bool proxied = (forced != nullptr && std::strcmp(forced, "1") == 0) || (properties.optimalTilingFeatures & attachment) != attachment;
const auto proxy = proxied ? VK_FORMAT_R8G8B8A8_SRGB : VK_FORMAT_UNDEFINED;
std::lock_guard lock(table.mutex);
table.formats.emplace(context.physical, proxy);
return proxy;
}
StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, std::uint32_t mipLevel) : context(context), detiler(detiler), descriptor(descriptor) {
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
PhaseTimer timer;
@ -1006,6 +1028,95 @@ VkImageView StorageTexture::AttachmentView(VkFormat format, std::uint32_t mip, s
return created;
}
VkImageView StorageTexture::AttachmentProxyView() {
if (proxyView != VK_NULL_HANDLE) return proxyView;
Require(storageFormat == VK_FORMAT_R8_UNORM && descriptor.dimension == TextureDimension::k2D && descriptor.mipCount == 1 && geometry.imageLayers == 1, "an RGBA8_SRGB attachment proxy is only modeled for a single-mip 2D R8 surface");
try {
VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
imageInfo.flags = VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
imageInfo.imageType = VK_IMAGE_TYPE_2D;
imageInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
imageInfo.extent = {descriptor.width, descriptor.height, 1};
imageInfo.mipLevels = 1;
imageInfo.arrayLayers = 1;
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imageInfo.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
Check(context.Function<PFN_vkCreateImage>("vkCreateImage")(context.device, &imageInfo, nullptr, &proxyImage), "vkCreateImage attachment proxy");
VkMemoryRequirements requirements{};
context.Function<PFN_vkGetImageMemoryRequirements>("vkGetImageMemoryRequirements")(context.device, proxyImage, &requirements);
VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
allocation.allocationSize = requirements.size;
allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
Check(context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &proxyMemory), "vkAllocateMemory attachment proxy");
Check(context.Function<PFN_vkBindImageMemory>("vkBindImageMemory")(context.device, proxyImage, proxyMemory, 0), "vkBindImageMemory attachment proxy");
VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
viewInfo.image = proxyImage;
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = VK_FORMAT_R8G8B8A8_SRGB;
viewInfo.components = {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY};
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &proxyView), "vkCreateImageView attachment proxy");
} catch (...) {
if (proxyImage) context.Function<PFN_vkDestroyImage>("vkDestroyImage")(context.device, proxyImage, nullptr);
if (proxyMemory) context.Function<PFN_vkFreeMemory>("vkFreeMemory")(context.device, proxyMemory, nullptr);
proxyImage = VK_NULL_HANDLE;
proxyMemory = VK_NULL_HANDLE;
proxyView = VK_NULL_HANDLE;
throw;
}
return proxyView;
}
namespace {
void blitWhole(const Context& context, VkCommandBuffer commands, VkImage source, VkImage destination, std::uint32_t width, std::uint32_t height) {
VkImageBlit region{};
region.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
region.srcOffsets[1] = {static_cast<std::int32_t>(width), static_cast<std::int32_t>(height), 1};
region.dstSubresource = region.srcSubresource;
region.dstOffsets[1] = region.srcOffsets[1];
context.Function<PFN_vkCmdBlitImage>("vkCmdBlitImage")(commands, source, VK_IMAGE_LAYOUT_GENERAL, destination, VK_IMAGE_LAYOUT_GENERAL, 1, &region, VK_FILTER_NEAREST);
}
VkImageMemoryBarrier proxyBarrier(VkImage image, VkAccessFlags source, VkAccessFlags destination, VkImageLayout from, VkImageLayout to) {
VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
barrier.srcAccessMask = source;
barrier.dstAccessMask = destination;
barrier.oldLayout = from;
barrier.newLayout = to;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = image;
barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
return barrier;
}
}
void StorageTexture::RecordAttachmentProxyLoad(VkCommandBuffer commands, VkImageLayout attachmentLayout) const {
Require(proxyView != VK_NULL_HANDLE, "the storage image has no attachment proxy");
const auto barrier = context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier");
const VkMemoryBarrier written{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT};
const auto discard = proxyBarrier(proxyImage, 0, VK_ACCESS_TRANSFER_WRITE_BIT, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL);
barrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &written, 0, nullptr, 1, &discard);
blitWhole(context, commands, image, proxyImage, descriptor.width, descriptor.height);
const auto attach = proxyBarrier(proxyImage, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_IMAGE_LAYOUT_GENERAL, attachmentLayout);
barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0, 0, nullptr, 0, nullptr, 1, &attach);
}
void StorageTexture::RecordAttachmentProxyStore(VkCommandBuffer commands, VkImageLayout attachmentLayout) const {
Require(proxyView != VK_NULL_HANDLE, "the storage image has no attachment proxy");
const auto barrier = context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier");
const auto detach = proxyBarrier(proxyImage, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, attachmentLayout, VK_IMAGE_LAYOUT_GENERAL);
barrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &detach);
blitWhole(context, commands, proxyImage, image, descriptor.width, descriptor.height);
const VkMemoryBarrier stored{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT};
barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &stored, 0, nullptr, 0, nullptr);
}
VkImageView StorageTexture::View(std::uint32_t mip) {
if (mip == defaultMip) return view;
const auto found = extraViews.find(mip);
@ -1145,7 +1256,7 @@ bool StorageTexture::Refresh() {
tracked = GuestMemory::ChangedBlocks(descriptor.baseAddress, static_cast<std::size_t>(guestBytes), generations, stampedBlocks, cpuBlocks);
};
const auto keysStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
keys = ProvedClearKeys(descriptor, guestBytes, keyProof);
keys = ProvedKeys();
if (profile && descriptor.dccAddress != 0) LookupOutcomes::Add(LookupOutcomes::DccScan, keysStart);
if (keys != uploadedKeys) {
changed.assign(trackedLayers, true);
@ -1323,6 +1434,15 @@ bool StorageTexture::Refresh() {
return false;
}
DccKeys StorageTexture::ProvedKeys() const {
std::optional<DccKeys> keys;
if (descriptor.dccAddress != 0 && uploadedKeys == DccKeys::Uncompressed) keys = WaitForKeyWriters(descriptor, guestBytes);
if (keys.has_value()) keyProof = {};
else keys = ProvedClearKeys(descriptor, guestBytes, keyProof);
if (IsDccClear(filledKeys) && *keys != filledKeys && !anyLayerPending()) filledKeys = DccKeys::Uncompressed;
return *keys;
}
bool StorageTexture::ServesKeysAt(std::uint64_t dccAddress) const {
const bool locked = GuestMemory::GpuMutex().HeldByThisThread();
const auto readFollowed = [&] {
@ -1352,7 +1472,7 @@ void traceKeyStore(const char* path, const GuestTextureResource& descriptor, std
static const bool trace = std::getenv("APS5_TRACE_DCC_KEYS") != nullptr;
if (!trace || descriptor.dccAddress == 0) return;
const auto packet = GuestMemory::CurrentPacket();
std::fprintf(stderr, "[dcc-keys] uncompressed store by %s (%s) for surface 0x%llx+0x%llx: keys 0x%llx+0x%llx (packet 0x%x queue 0x%x)\n", path, flushReason != nullptr ? flushReason : "?", static_cast<unsigned long long>(descriptor.baseAddress), static_cast<unsigned long long>(guestBytes), static_cast<unsigned long long>(descriptor.dccAddress), static_cast<unsigned long long>(guestBytes / 256), packet.opcode, packet.queue);
std::fprintf(stderr, "[dcc-keys] uncompressed store by %s (%s) for surface 0x%llx+0x%llx: keys 0x%llx+0x%llx (packet 0x%x queue 0x%x)\n", path, flushReason != nullptr ? flushReason : "?", static_cast<unsigned long long>(descriptor.baseAddress), static_cast<unsigned long long>(guestBytes), static_cast<unsigned long long>(descriptor.dccAddress), static_cast<unsigned long long>(DccKeyCount(descriptor, guestBytes)), packet.opcode, packet.queue);
}
}
@ -1479,7 +1599,7 @@ void StorageTexture::upload(const std::vector<bool>* layers) {
recorder->FlushStoresOverlapping(descriptor.baseAddress, static_cast<std::size_t>(guestBytes));
commands = recorder->Commands();
timing = recorder->BeginGpuTiming(Recorder::CommandClass::StorageUpload);
recorder->Keep(linear);
recorder->Keep(linear, linear->Size());
// The image itself must outlive the recorded copy: the cache may evict it right after.
if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self));
// The detile reads the tiled bytes from the import when the batch runs.
@ -1808,7 +1928,7 @@ std::uint64_t StorageTexture::uploadWindows(const HostImport& import, std::span<
for (const auto& [begin, end] : reads) recorder->FlushStoresOverlapping(begin, static_cast<std::size_t>(end - begin));
commands = recorder->Commands();
timing = recorder->BeginGpuTiming(Recorder::CommandClass::StorageUpload);
recorder->Keep(linear);
recorder->Keep(linear, linear->Size());
for (const auto& source : sources) {
if (source.slab != nullptr) recorder->Keep(source.slab);
}
@ -1974,8 +2094,8 @@ std::uint64_t StorageTexture::writeBackWindows(const HostImport& import, std::sp
recorder->FlushStoresOverlapping(flushBegin, static_cast<std::size_t>(flushEnd - flushBegin));
commands = recorder->Commands(&covered);
timing = recorder->BeginGpuTiming(Recorder::CommandClass::StorageWriteBack);
recorder->Keep(linear);
recorder->Keep(tiledScratch);
recorder->Keep(linear, linear->Size());
recorder->Keep(tiledScratch, tiledScratch->Size());
for (const auto& pieces : slabPieces) recorder->Keep(pieces.slab);
for (const auto& slab : padding.slabs) recorder->Keep(slab);
if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self));
@ -2093,6 +2213,11 @@ bool StorageTexture::overlaps(std::uint64_t address, std::size_t bytes) const {
}
void StorageTexture::MarkDirty() {
if (descriptor.dccAddress != 0 && IsDccClear(uploadedKeys) && !IsDccClear(filledKeys)) {
traceKeyStore("first write", descriptor, guestBytes);
MarkDccUncompressed(context, descriptor.dccAddress, guestBytes, DccKeyCount(descriptor, guestBytes));
uploadedKeys = DccKeys::Uncompressed;
}
markLayersPending(0, trackedLayers);
}
@ -3360,7 +3485,7 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
if (profile) Profile().storageGpu += timer.lap();
originalValid = false;
traceKeyStore("block write-back", descriptor, guestBytes);
if (!IsDccClear(filledKeys)) MarkDccUncompressed(context, descriptor.dccAddress, guestBytes);
if (!IsDccClear(filledKeys)) MarkDccUncompressed(context, descriptor.dccAddress, guestBytes, DccKeyCount(descriptor, guestBytes));
uploadedKeys = DccKeys::Uncompressed;
for (const auto& [from, to] : keep) GuestMemory::MarkWritten(from, static_cast<std::size_t>(to - from));
settle(true);
@ -3391,8 +3516,8 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
recorder->FlushStoresOverlapping(firstStored, static_cast<std::size_t>(lastStored - firstStored));
commands = recorder->Commands();
timing = recorder->BeginGpuTiming(Recorder::CommandClass::StorageWriteBack);
recorder->Keep(linear);
recorder->Keep(tiledScratch);
recorder->Keep(linear, linear->Size());
recorder->Keep(tiledScratch, tiledScratch->Size());
for (const auto& slab : padding.slabs) recorder->Keep(slab);
// The image itself must outlive the recorded retile: the cache may evict it right after.
if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self));
@ -3468,7 +3593,7 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
// 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).
traceKeyStore("layer write-back", descriptor, guestBytes);
if (!IsDccClear(filledKeys)) MarkDccUncompressed(context, descriptor.dccAddress, guestBytes);
if (!IsDccClear(filledKeys)) MarkDccUncompressed(context, descriptor.dccAddress, guestBytes, DccKeyCount(descriptor, guestBytes));
uploadedKeys = DccKeys::Uncompressed;
for (const auto& [from, to] : keep) GuestMemory::MarkWritten(from, static_cast<std::size_t>(to - from));
// The walk covers this surface's pages only: an adjacent image's later CPU write is stamped
@ -3578,7 +3703,7 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
// The texels now hold the whole image, so later reads must see them rather than a fast clear
// (the keys may be host-imported although the texels were not: then a recorded fill, else a CPU store).
traceKeyStore("cpu write-back", descriptor, guestBytes);
if (!IsDccClear(filledKeys)) MarkDccUncompressed(context, descriptor.dccAddress, guestBytes);
if (!IsDccClear(filledKeys)) MarkDccUncompressed(context, descriptor.dccAddress, guestBytes, DccKeyCount(descriptor, guestBytes));
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.
@ -3623,6 +3748,9 @@ void StorageTexture::release() noexcept {
uintViews.clear();
for (const auto& [format, attachment] : attachmentViews) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, attachment, nullptr);
attachmentViews.clear();
if (proxyView) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, proxyView, nullptr);
if (proxyImage) context.Function<PFN_vkDestroyImage>("vkDestroyImage")(context.device, proxyImage, nullptr);
if (proxyMemory) context.Function<PFN_vkFreeMemory>("vkFreeMemory")(context.device, proxyMemory, nullptr);
if (view) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, view, nullptr);
if (image) context.Function<PFN_vkDestroyImage>("vkDestroyImage")(context.device, image, nullptr);
if (memory) context.Function<PFN_vkFreeMemory>("vkFreeMemory")(context.device, memory, nullptr);

View File

@ -36,9 +36,16 @@ struct Push {
std::uint32_t BlockBytesFor(TextureTileMode tileMode) {
switch (tileMode) {
case TextureTileMode::kLinear: return 0u;
case TextureTileMode::kStandard256B: return 256u;
case TextureTileMode::kStandard4KB: return 4096u;
case TextureTileMode::kStandard256B:
case TextureTileMode::kD256B: return 256u;
case TextureTileMode::kStandard4KB:
case TextureTileMode::kD4KB:
case TextureTileMode::kS4KBX:
case TextureTileMode::kD4KBX: return 4096u;
case TextureTileMode::kStandard64KB:
case TextureTileMode::kD64KB:
case TextureTileMode::kS64KBT:
case TextureTileMode::kD64KBT:
case TextureTileMode::kZ64KBX:
case TextureTileMode::kS64KBX:
case TextureTileMode::kD64KBX:
@ -108,8 +115,8 @@ VkPipeline TextureDetiler::pipeline(TextureTileMode tileMode, std::uint32_t elem
// per-bit XOR equation for the equation family (2); 20-21 override the block extent in elements.
std::array<std::uint32_t, 22> values{elementBytes, BlockBytesFor(tileMode), tileMode == TextureTileMode::kLinear ? 0u : 1u, retile ? 1u : 0u};
if (thick) {
const auto thickMode = tileMode == TextureTileMode::kStandard4KB ? 0x105u : tileMode == TextureTileMode::kStandard64KB ? 0x109u : 0u;
Require(thickMode != 0, "3D textures are only detiled from SW_4KB_S or SW_64KB_S");
const auto thickMode = tileMode == TextureTileMode::kStandard4KB ? 0x105u : tileMode == TextureTileMode::kStandard64KB ? 0x109u : tileMode == TextureTileMode::kS64KBX ? 0x119u : 0u;
Require(thickMode != 0, "3D textures are only detiled thick from SW_4KB_S, SW_64KB_S or SW_64KB_S_X");
const auto* equation = FindTextureSwizzleEquation(thickMode, elementBytes);
Require(equation != nullptr, "no thick swizzle equation for the element size");
values[2] = 2u;
@ -117,7 +124,7 @@ VkPipeline TextureDetiler::pipeline(TextureTileMode tileMode, std::uint32_t elem
const auto extent = ThickBlockExtent(tileMode, elementBytes);
values[20] = extent[0];
values[21] = extent[1];
} else if (const auto mode = XorSwizzleMode(tileMode); mode != 0) {
} else if (const auto mode = EquationSwizzleMode(tileMode); mode != 0) {
const auto* equation = FindTextureSwizzleEquation(mode, elementBytes);
if (equation == nullptr) Require(false, "no swizzle equation for tile mode " + std::to_string(mode) + " at " + std::to_string(elementBytes) + " bytes per element");
values[2] = 2u;

View File

@ -96,7 +96,7 @@ constexpr auto MakeFormatLookupTable() {
constexpr auto kFormatLookupTable = MakeFormatLookupTable();
std::uint32_t remapGuestFormat(std::uint32_t guestFormat) {
return guestFormat == 30 || guestFormat == 34 ? 20 : guestFormat;
return guestFormat == 30 || guestFormat == 34 || guestFormat == 43 ? 20 : guestFormat;
}
const FormatEntry& findFormatEntry(std::uint32_t guestFormat) {

View File

@ -44,9 +44,16 @@ BlockLayout GetBlockLayout(TextureTileMode tileMode, std::uint32_t bytesPerEleme
const auto index = static_cast<std::size_t>(std::countr_zero(bytesPerElement));
switch (tileMode) {
case TextureTileMode::kLinear: throw std::runtime_error("AGC graphics: GetBlockLayout does not apply to linear tiling");
case TextureTileMode::kStandard256B: return {256u, 1u << kLog2BlockThin256B[index].width, 1u << kLog2BlockThin256B[index].height};
case TextureTileMode::kStandard4KB: return {4096u, 1u << kLog2BlockThin4KB[index].width, 1u << kLog2BlockThin4KB[index].height};
case TextureTileMode::kStandard256B:
case TextureTileMode::kD256B: return {256u, 1u << kLog2BlockThin256B[index].width, 1u << kLog2BlockThin256B[index].height};
case TextureTileMode::kStandard4KB:
case TextureTileMode::kD4KB:
case TextureTileMode::kS4KBX:
case TextureTileMode::kD4KBX: return {4096u, 1u << kLog2BlockThin4KB[index].width, 1u << kLog2BlockThin4KB[index].height};
case TextureTileMode::kStandard64KB:
case TextureTileMode::kD64KB:
case TextureTileMode::kS64KBT:
case TextureTileMode::kD64KBT:
case TextureTileMode::kZ64KBX:
case TextureTileMode::kS64KBX:
case TextureTileMode::kD64KBX:
@ -92,11 +99,18 @@ bool GetMipTailLayout(TextureTileMode tileMode, const BlockLayout& block, std::u
const auto index = static_cast<std::size_t>(std::countr_zero(bytesPerElement));
switch (tileMode) {
case TextureTileMode::kLinear:
case TextureTileMode::kStandard256B: return false;
case TextureTileMode::kStandard256B:
case TextureTileMode::kD256B: return false;
case TextureTileMode::kStandard4KB:
case TextureTileMode::kD4KB:
case TextureTileMode::kS4KBX:
case TextureTileMode::kD4KBX:
out = MakeMipTailLayout(kMipTailThin4KB[index], block.blockWidth >> 1u, block.blockHeight);
return true;
case TextureTileMode::kStandard64KB:
case TextureTileMode::kD64KB:
case TextureTileMode::kS64KBT:
case TextureTileMode::kD64KBT:
case TextureTileMode::kZ64KBX:
case TextureTileMode::kS64KBX:
case TextureTileMode::kD64KBX:
@ -245,8 +259,9 @@ std::array<std::uint32_t, 3> ThickBlockExtent(TextureTileMode tileMode, std::uin
const auto index = static_cast<std::size_t>(std::countr_zero(bytesPerElement));
switch (tileMode) {
case TextureTileMode::kStandard4KB: return {1u << thick4KB[index][0], 1u << thick4KB[index][1], 1u << thick4KB[index][2]};
case TextureTileMode::kStandard64KB: return {1u << thick64KB[index][0], 1u << thick64KB[index][1], 1u << thick64KB[index][2]};
default: throw std::runtime_error("AGC graphics: 3D textures are only supported linear or in SW_4KB_S / SW_64KB_S, not in XOR swizzle " + std::to_string(XorSwizzleMode(tileMode)) + " at " + std::to_string(bytesPerElement) + " bytes per element");
case TextureTileMode::kStandard64KB:
case TextureTileMode::kS64KBX: return {1u << thick64KB[index][0], 1u << thick64KB[index][1], 1u << thick64KB[index][2]};
default: throw std::runtime_error("AGC graphics: thick 3D textures are only supported in SW_4KB_S, SW_64KB_S and SW_64KB_S_X, not in swizzle mode " + std::to_string(EquationSwizzleMode(tileMode)) + " at " + std::to_string(bytesPerElement) + " bytes per element");
}
}
@ -283,7 +298,10 @@ ThickLayout ComputeThickLayout(TextureTileMode tileMode, std::uint32_t format, s
Require(width != 0 && height != 0 && depth != 0, "cannot compute layout for a zero-sized 3D texture");
Require(mipCount != 0 && mipCount <= 16u, "3D texture mip count is out of range");
const auto bytesPerElement = BytesPerElement(format);
Require(BlockWidth(format) == 1u && BlockHeight(format) == 1u, "block-compressed 3D textures are not implemented");
const auto texelWidth = BlockWidth(format);
const auto texelHeight = BlockHeight(format);
const auto elementsWidth0 = (width + texelWidth - 1u) / texelWidth;
const auto elementsHeight0 = (height + texelHeight - 1u) / texelHeight;
ThickLayout result{};
result.depth = depth;
result.mips.resize(mipCount);
@ -300,7 +318,7 @@ ThickLayout ComputeThickLayout(TextureTileMode tileMode, std::uint32_t format, s
const auto effectiveLog2 = log2BlockBytes - (log2BlockBytes - 8u) / 3u;
maxTailLevels = effectiveLog2 <= 11u ? 1u + (1u << (effectiveLog2 - 9u)) : effectiveLog2 - 4u;
for (std::uint32_t level = 0; mipCount > 1 && level < mipCount; ++level) {
if (ShiftCeil(width, level) <= tailWidth && ShiftCeil(height, level) <= tailHeight && mipCount - level <= maxTailLevels) {
if (ShiftCeil(elementsWidth0, level) <= tailWidth && ShiftCeil(elementsHeight0, level) <= tailHeight && mipCount - level <= maxTailLevels) {
firstTailLevel = level;
break;
}
@ -311,14 +329,14 @@ ThickLayout ComputeThickLayout(TextureTileMode tileMode, std::uint32_t format, s
std::uint64_t linearOffset = 0;
for (auto level = mipCount; level-- > 0;) {
auto& mip = result.mips[level];
mip.width = std::max(width >> level, 1u);
mip.height = std::max(height >> level, 1u);
mip.width = TexelLevelDimension(width, level, texelWidth);
mip.height = TexelLevelDimension(height, level, texelHeight);
mip.tail = level >= firstTailLevel;
mip.tailX = 0;
mip.tailY = 0;
const bool tiled = tileMode != TextureTileMode::kLinear;
const auto levelWidth = tiled ? std::max((width + (1u << level) - 1u) >> level, 1u) : mip.width;
const auto levelHeight = tiled ? std::max((height + (1u << level) - 1u) >> level, 1u) : mip.height;
const auto levelWidth = tiled ? std::max(ShiftCeil(elementsWidth0, level), 1u) : mip.width;
const auto levelHeight = tiled ? std::max(ShiftCeil(elementsHeight0, level), 1u) : mip.height;
const auto paddedWidth = AlignUp(levelWidth, block[0]);
mip.pitchBytes = paddedWidth * bytesPerElement;
if (mip.tail) {
@ -359,7 +377,7 @@ ThickLayout ComputeThickLayout(TextureTileMode tileMode, std::uint32_t format, s
SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor) {
SurfaceGeometry geometry;
if (descriptor.dimension == TextureDimension::k3D && (descriptor.tileMode == TextureTileMode::kD64KBX || descriptor.tileMode == TextureTileMode::kR64KBX)) {
if (descriptor.dimension == TextureDimension::k3D && (descriptor.tileMode == TextureTileMode::kZ64KBX || descriptor.tileMode == TextureTileMode::kD64KBX || descriptor.tileMode == TextureTileMode::kR64KBX)) {
const auto depth = descriptor.depthOrLastArray + 1u;
geometry.mips = ComputeMipLayout(descriptor.tileMode, descriptor.format, descriptor.width, descriptor.height, descriptor.mipCount);
geometry.layers = depth;

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