Merge remote-tracking branch 'upstream/main' into feat/screenshot-state-overlay

# Conflicts:
#	core/libs/prx/libSceScreenShot/Export.cpp
#	docs/dev/debt/libSceScreenShot.md
This commit is contained in:
hypermist committed 2026-10-22 15:24:22 +13:00
commit dbd82809cc
788 files changed
+43177 -6850

No files matched your search

+3 -4
View File
@@ -18,15 +18,14 @@ jobs:
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.base.sha }}
ref: ${{ github.event.pull_request.base.ref }}
fetch-depth: 0
persist-credentials: false
- name: Fetch pull request objects without checking out its code
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: git fetch --no-tags origin "$HEAD_SHA"
- name: Check conventions with the trusted base script
- name: Check conventions with the current base branch script
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: python3 -I tools/check_conventions.py --base "$BASE_SHA" --head "$HEAD_SHA"
run: python3 -I tools/check_conventions.py --base HEAD --head "$HEAD_SHA"
+2 -1
View File
@@ -5,6 +5,7 @@
- Follow the [coding conventions](docs/dev/CONVENTIONS.md): naming, no comments except [technical debt](docs/dev/TechnicalDebt.md), Conventional Commits.
- Every function either does exactly what it is supposed to or throws. Unimplemented exports call `NotImplemented_nid_no_patch(__func__)` (see [libSceAudioIn](core/libs/prx/libSceAudioIn/Export.cpp)).
- Pull requests that add or change shader instruction semantics say where they come from: measured on hardware (which GPU and what was checked, e.g. with the [hardware oracle](docs/dev/HW_ORACLE.md)) or the exact source (ISA section, LLVM, ACO or Mesa file). Reviewers check the semantics on hardware. Cases the source doesn't settle throw, and behaviour not verified on hardware is recorded in [technical debt](docs/dev/TechnicalDebt.md).
- An export's signature or behaviour taken from a title's call sites or from a library's disassembly follows [reading titles and libraries](docs/dev/REVERSE_ENGINEERING.md): a call site settles the arguments, not what the library does.
- A silent stub is allowed only when it unblocks a title and only affects the UI; add it under Silent stubs in its library's [technical debt](docs/dev/debt/) file.
- Missing imports fail at startup with a clear error; don't replace them with fallbacks that keep running.
- Implement the general behaviour of a function, not what one title happens to need.
@@ -41,7 +42,7 @@ This manual run also builds and tests the relinker on Linux, Windows and macOS w
With `--field full=false`, the manual run skips the full builds and runs only the relinker matrix on Linux, Windows and macOS, if the pull request changes a file the relinker-only build uses, such as the Python tools and their tests in `tools/`.
The Conventions check runs automatically for every contributor, including first-time contributors. It uses the base branch checker and reads pull request Git objects without executing pull request code. The Conventions check runs on every pull request and fails when a rule on this page is broken. It accepts code comments only when the pull request also changes [technical debt](docs/dev/TechnicalDebt.md), the index or a file in `docs/dev/debt/`, and only UTF-8 text files. Run it locally before pushing:
The Conventions check runs automatically for every contributor, including first-time contributors. It uses the checker from the current tip of the base branch, so rules added after a pull request was opened apply to its next run, and reads pull request Git objects without executing pull request code. The Conventions check runs on every pull request and fails when a rule on this page is broken. It accepts code comments only when the pull request also changes [technical debt](docs/dev/TechnicalDebt.md), the index or a file in `docs/dev/debt/`, and only UTF-8 text files. Run it locally before pushing:
```
python3 tools/check_conventions.py --base origin/main
+2
View File
@@ -38,6 +38,8 @@ SDL-mapped game controllers are supported, including analog sticks and triggers.
This project is intended for interoperability, research, preservation, and compatibility purposes. It does not include, distribute, or require copyrighted software, firmware, cryptographic keys, or proprietary libraries. Users are responsible for ensuring that any binaries used with this project are obtained and used in accordance with applicable laws and their respective license terms.
AnyPS5 is an independent project and is not affiliated with, authorized or endorsed by Sony Interactive Entertainment. "PlayStation" and "PS5" are trademarks of Sony Interactive Entertainment Inc.
## License
This project is licensed under the GNU General Public License version 2 only.
@@ -19,6 +19,10 @@ struct Header {
std::uint32_t width;
std::uint32_t height;
std::uint32_t channels;
std::uint8_t componentIds[4]{};
std::uint8_t samplingFactors[4]{};
bool progressive = false;
bool app0BeforeFrame = false;
};
enum class Sampling {
+27 -1
View File
@@ -126,7 +126,33 @@ std::optional<Header> ParseHeader(std::span<const std::uint8_t> jpeg) {
int channels = 0;
if (!stbi_info_from_memory(jpeg.data(), static_cast<int>(jpeg.size()), &width, &height, &channels)) return std::nullopt;
if (width <= 0 || height <= 0 || channels <= 0) return std::nullopt;
return Header{static_cast<std::uint32_t>(width), static_cast<std::uint32_t>(height), static_cast<std::uint32_t>(channels)};
Header header{static_cast<std::uint32_t>(width), static_cast<std::uint32_t>(height), static_cast<std::uint32_t>(channels)};
std::size_t offset = 2;
while (offset < jpeg.size()) {
if (jpeg[offset++] != 0xFF) return std::nullopt;
while (offset < jpeg.size() && jpeg[offset] == 0xFF) ++offset;
if (offset == jpeg.size()) return std::nullopt;
const std::uint8_t marker = jpeg[offset++];
if (marker == 0xD9 || marker == 0xDA) return std::nullopt;
if (marker == 0x01 || (marker >= 0xD0 && marker <= 0xD8)) continue;
if (jpeg.size() - offset < 2) return std::nullopt;
const std::size_t length = (static_cast<std::size_t>(jpeg[offset]) << 8) | jpeg[offset + 1];
if (length < 2 || length > jpeg.size() - offset) return std::nullopt;
if (marker == 0xE0) header.app0BeforeFrame = true;
if (marker == 0xC0 || marker == 0xC1 || marker == 0xC2) {
if (length < 8) return std::nullopt;
header.channels = jpeg[offset + 7];
if (header.channels == 0 || header.channels > 4 || length != 8 + 3 * header.channels) return std::nullopt;
header.progressive = marker == 0xC2;
for (std::uint32_t i = 0; i < header.channels; ++i) {
header.componentIds[i] = jpeg[offset + 8 + 3 * i];
header.samplingFactors[i] = jpeg[offset + 9 + 3 * i];
}
return header;
}
offset += length;
}
return std::nullopt;
}
std::optional<Image> Decode(std::span<const std::uint8_t> jpeg) {
+14 -2
View File
@@ -186,6 +186,8 @@ foreach(LIB_DIR ${LIBS_DIRS})
endif()
endforeach()
target_link_libraries(libSceUsbStorageDialog PRIVATE libSceUserService)
if(APS5_ENABLE_TRACY)
set_target_properties(TracyClient PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${UNPATCHED_DIR}
@@ -382,7 +384,7 @@ if(BUILD_TESTING)
add_test_executable(guest_offline_imports_tests tests/GuestOfflineImports.cpp)
target_include_directories(guest_offline_imports_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_offline_imports_tests PRIVATE libSceHttp libSceNpEntitlementAccess libSceRudp libc)
target_link_libraries(guest_offline_imports_tests PRIVATE libSceHttp libSceNpEntitlementAccess libSceRudp libSceVideoRecordingP libc)
configure_windows_unwind(guest_offline_imports_tests)
add_test(NAME guest_offline_imports COMMAND guest_offline_imports_tests)
if(Python3_Interpreter_FOUND)
@@ -406,7 +408,7 @@ if(BUILD_TESTING)
add_test_executable(guest_usb_storage_dialog_tests tests/GuestUsbStorageDialog.cpp)
target_include_directories(guest_usb_storage_dialog_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_usb_storage_dialog_tests PRIVATE libSceUsbStorageDialog libc)
target_link_libraries(guest_usb_storage_dialog_tests PRIVATE libSceUsbStorageDialog libSceUserService libc)
configure_windows_unwind(guest_usb_storage_dialog_tests)
add_test(NAME guest_usb_storage_dialog COMMAND guest_usb_storage_dialog_tests)
if(WIN32)
@@ -682,4 +684,14 @@ if(BUILD_TESTING)
set_tests_properties(guest_np_game_intent PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpGameIntent>")
endif()
add_test_executable(guest_vorbis_dec_tests tests/GuestVorbisDec.cpp)
target_include_directories(guest_vorbis_dec_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_vorbis_dec_tests PRIVATE libSceVorbisDec libc)
configure_windows_unwind(guest_vorbis_dec_tests)
add_test(NAME guest_vorbis_dec COMMAND guest_vorbis_dec_tests)
if(WIN32)
set_tests_properties(guest_vorbis_dec PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceVorbisDec>")
endif()
endif()
+192 -21
View File
@@ -333,7 +333,7 @@ struct VaList {
};
#pragma pack(pop)
using FiberEntry = void* (*)(std::uint64_t, void*);
using FiberEntry = void (APS5_VABI*)(std::uint64_t argOnInitialize, std::uint64_t argOnRun);
struct FiberOptParam {
std::uint32_t magic;
@@ -432,6 +432,48 @@ struct AcmBatchError {
std::uint32_t reserved[8];
};
struct AcmReverbBuffer {
void* samples;
const void* blockMetadata;
};
struct AcmReverbInput {
std::uint32_t blockSize;
std::uint32_t blockCount;
std::uint32_t channelCount;
std::uint32_t sampleFormat;
std::uint32_t currentBlock;
std::uint32_t availableBlocks;
const AcmReverbBuffer* const* history;
const void* const* inputChannels;
std::uint32_t pendingBlocks;
};
struct AcmReverbFilter {
std::uint32_t blockSize;
std::uint32_t blockCount;
std::uint32_t channelCount;
std::uint32_t sampleFormat;
const void* blockMetadata;
const AcmReverbBuffer* const* channels;
};
struct AcmReverbOutput {
std::uint32_t blockSize;
std::uint32_t channelCount;
void* const* samples;
void* const* workspace;
float gain;
float previousGain;
void* const* previousSamples;
};
static_assert(sizeof(AcmReverbBuffer) == 16);
static_assert(offsetof(AcmReverbInput, history) == 24);
static_assert(offsetof(AcmReverbInput, pendingBlocks) == 40);
static_assert(sizeof(AcmReverbFilter) == 32);
static_assert(offsetof(AcmReverbOutput, previousSamples) == 32);
struct AcmBatchInfo {
void* buffer;
std::size_t offset;
@@ -933,13 +975,60 @@ struct AudiodecM4aacInfo {
std::int32_t i_result;
};
struct VoiceEvent {
std::int32_t type;
std::uint32_t reserved;
void* userData;
union {
struct {
std::uint16_t count;
std::uint16_t ports[7];
} dataReady;
struct {
std::uint16_t port;
std::uint8_t state;
std::uint8_t reserved[13];
} deviceState;
};
};
using VoiceEventHandler = void (APS5_VABI*)(const VoiceEvent* event);
struct VoiceThreadsParams {
std::uint32_t mode;
std::int32_t firstPriority;
std::uint64_t firstAffinity;
std::int32_t secondPriority;
std::uint32_t reserved20;
std::uint64_t secondAffinity;
std::int32_t thirdPriority;
std::uint32_t reserved36;
std::uint64_t thirdAffinity;
};
static_assert(sizeof(VoiceEvent) == 32);
static_assert(offsetof(VoiceEvent, userData) == 8);
static_assert(offsetof(VoiceEvent, dataReady) == 16);
static_assert(offsetof(VoiceEvent, deviceState) == 16);
static_assert(sizeof(VoiceThreadsParams) == 48);
static_assert(offsetof(VoiceThreadsParams, firstAffinity) == 8);
static_assert(offsetof(VoiceThreadsParams, secondPriority) == 16);
static_assert(offsetof(VoiceThreadsParams, secondAffinity) == 24);
static_assert(offsetof(VoiceThreadsParams, thirdPriority) == 32);
static_assert(offsetof(VoiceThreadsParams, thirdAffinity) == 40);
struct VoiceInitParam {
std::int32_t app_type;
std::uint64_t on_event;
VoiceEventHandler on_event;
void* user_data;
std::uint8_t reserved[32 - sizeof(std::int32_t) - sizeof(std::uint64_t) - sizeof(void*)];
std::uint8_t reserved[16];
};
static_assert(sizeof(VoiceInitParam) == 40);
static_assert(offsetof(VoiceInitParam, on_event) == 8);
static_assert(offsetof(VoiceInitParam, user_data) == 16);
static_assert(offsetof(VoiceInitParam, reserved) == 24);
struct VoicePortParam {
std::int32_t port_type;
std::uint16_t threshold;
@@ -962,14 +1051,30 @@ struct VoicePortInfo {
std::uint16_t reserved;
};
struct VoiceResourceInfo {
std::uint16_t max_voice_input_ports;
std::uint16_t max_voice_output_ports;
std::uint16_t max_device_input_ports;
std::uint16_t max_device_output_ports;
std::uint16_t max_ports;
struct NpAuthClientId;
struct NpAuthAuthorizedAppCodeParameter {
std::uint64_t size;
std::int32_t userId;
const NpAuthClientId* clientId;
const char* scope;
std::uint32_t option;
};
static_assert(sizeof(NpAuthAuthorizedAppCodeParameter) == 40);
static_assert(offsetof(NpAuthAuthorizedAppCodeParameter, clientId) == 16);
static_assert(offsetof(NpAuthAuthorizedAppCodeParameter, scope) == 24);
static_assert(offsetof(NpAuthAuthorizedAppCodeParameter, option) == 32);
struct VoiceResourceInfo {
std::uint16_t maxVoiceInputPorts;
std::uint16_t maxVoiceOutputPorts;
std::uint16_t maxDeviceInputPorts;
std::uint16_t maxDeviceOutputPorts;
};
static_assert(sizeof(VoiceResourceInfo) == 8);
struct VoiceStartParam {
void* container;
std::uint32_t mem_size;
@@ -1155,8 +1260,8 @@ struct ImeKeycode {
std::uint64_t timestamp;
};
using ImeTextFilter = std::int32_t (APS5_VABI *)(char16_t* out_text, std::uint32_t* out_text_length, const char16_t* source_text, std::uint32_t source_text_length);
using ImeExtKeyboardFilter = int (APS5_VABI *)(const ImeKeycode* source_keycode, std::uint16_t* out_keycode, std::uint32_t* out_status, void* reserved);
using ImeTextFilter = std::int32_t (APS5_VABI*)(char16_t* outText, std::uint32_t* outTextLength, const char16_t* sourceText, std::uint32_t sourceTextLength);
using ImeExtKeyboardFilter = int (APS5_VABI*)(const ImeKeycode* sourceKeycode, std::uint16_t* outKeycode, std::uint32_t* outStatus, void* reserved);
struct ImeExtendedParam {
std::uint32_t option;
@@ -1318,7 +1423,12 @@ struct NetEpollEvent {
NetEpollData data;
};
struct NetCtlNatInfo { std::uint8_t opaque[128]; };
struct NetCtlNatInfo {
std::uint32_t size;
std::uint8_t opaque[12];
};
static_assert(sizeof(NetCtlNatInfo) == 16);
union NetCtlInfo { std::uint8_t opaque[256]; };
@@ -1333,10 +1443,10 @@ using NetCtlCallback = void (APS5_VABI *)(int, void*);
struct HttpEpoll;
using HttpEpollHandle = HttpEpoll*;
using HttpsCallback = int (*)(int, unsigned int, void* const*, int, void*);
using HttpRedirectCallback = int (*)(int, std::int32_t, std::int32_t*, const char*, void*);
using HttpCookieRecvCallback = int (*)(int, const char*, const char*, std::uint64_t, void*);
using HttpAuthInfoCallback = int (*)(int, int, const char*, char*, char*, int, std::uint8_t**, std::uint64_t*, int*, void*);
using HttpsCallback = int (APS5_VABI*)(int, unsigned int, void* const*, int, void*);
using HttpRedirectCallback = int (APS5_VABI*)(int, std::int32_t, std::int32_t*, const char*, void*);
using HttpCookieRecvCallback = int (APS5_VABI*)(int, const char*, const char*, std::uint64_t, void*);
using HttpAuthInfoCallback = int (APS5_VABI*)(int, int, const char*, char*, char*, int, std::uint8_t**, std::uint64_t*, int*, void*);
struct HttpNBEvent { std::uint8_t opaque[64]; };
@@ -1395,6 +1505,24 @@ struct NpUnifiedEntitlementLabel {
char padding[3];
};
struct NpServiceEntitlementLabel {
char data[7];
std::uint8_t reserved[13];
};
struct NpEntitlementAccessTransactionId {
char data[65];
std::uint8_t reserved[7];
};
struct NpEntitlementAccessListParameter;
struct NpEntitlementAccessUnifiedInfo;
struct NpEntitlementAccessServiceInfo;
static_assert(sizeof(NpUnifiedEntitlementLabel) == 20);
static_assert(sizeof(NpServiceEntitlementLabel) == 20);
static_assert(sizeof(NpEntitlementAccessTransactionId) == 72);
struct NpEntitlementAccessInitParam { char reserved[32]; };
struct NpEntitlementAccessBootParam { char reserved[32]; };
@@ -1690,6 +1818,34 @@ struct AppContentBootParam {
struct AppContentMountPoint { char data[16]; };
using ContentExportMallocCallback = void* (APS5_VABI *)(std::size_t size, void* userData);
using ContentExportFreeCallback = void (APS5_VABI *)(void* memory, void* userData);
using ContentExportDataCallback = std::int32_t (APS5_VABI *)(const void** data, std::size_t* size, void* userData);
struct ContentExportInitParam {
ContentExportMallocCallback mallocFunc;
ContentExportFreeCallback freeFunc;
void* userData;
};
static_assert(sizeof(ContentExportInitParam) == 24);
static_assert(offsetof(ContentExportInitParam, freeFunc) == 8);
static_assert(offsetof(ContentExportInitParam, userData) == 16);
struct ContentExportSfParam {
std::size_t size;
const char* metadata;
const std::int64_t* contentIds;
std::uint32_t contentCount;
std::uint32_t option;
};
static_assert(sizeof(ContentExportSfParam) == 32);
static_assert(offsetof(ContentExportSfParam, metadata) == 8);
static_assert(offsetof(ContentExportSfParam, contentIds) == 16);
static_assert(offsetof(ContentExportSfParam, contentCount) == 24);
static_assert(offsetof(ContentExportSfParam, option) == 28);
struct ContentExportInitParam2 {
void* malloc_func;
void* free_func;
@@ -1897,11 +2053,22 @@ union PlayGoOptionalChunk {
};
struct PlayGoDialogResult {
std::int32_t errorCode;
std::int32_t result;
std::int32_t reserved[9];
std::uint8_t reserved[32];
};
using RudpEventHandler = void (*)(int ctx_id, int event_id, int error_code, void* arg);
struct RudpPollEvent {
std::int32_t contextId;
std::uint16_t events;
std::uint16_t returnedEvents;
};
static_assert(sizeof(RudpPollEvent) == 8);
static_assert(offsetof(RudpPollEvent, events) == 4);
static_assert(offsetof(RudpPollEvent, returnedEvents) == 6);
using RudpEventHandler = void (APS5_VABI*)(int ctxId, int eventId, int errorCode, void* arg);
struct SystemServiceStatus {
std::int32_t event_num = 0;
@@ -1998,12 +2165,16 @@ struct UserServiceGamePresets {
std::uint32_t audio_language;
};
struct UltMutexOptParam {
std::uint32_t reserved_header[2];
struct alignas(8) UltMutexOptParam {
std::uint32_t header[3];
std::uint32_t buildVersion;
std::uint32_t attribute;
std::uint32_t reserved0;
std::uint32_t reserved[27];
};
static_assert(sizeof(UltMutexOptParam) == 128);
static_assert(offsetof(UltMutexOptParam, attribute) == 16);
struct UltUlthreadRuntimeOptParam { std::uint8_t bytes[128]; };
using UltUlthreadEntry = std::int32_t (APS5_VABI *)(std::uint64_t);
+34 -1
View File
@@ -5,6 +5,39 @@
extern "C" {
APS5_DUMMY_FUN
int APS5_VABI sceAc3EncCreateEncoder() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAc3EncDeleteEncoder() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAc3EncEncode() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAc3EncEncodeAsync() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAc3EncFlush() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAc3EncIsEncoding() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAc3EncWaitEncode() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
}
+121 -4
View File
@@ -79,19 +79,130 @@ int APS5_VABI sceAcmContextDestroy(AcmContextId context) {
return 0;
}
int APS5_VABI sceAcm_ConvReverb_SharedInput(void) {
int APS5_VABI sceAcm_ConvReverb_SharedInput(AcmBatchInfo* batch, std::uint32_t mode, AcmReverbInput* input, std::uint32_t outputCount, const AcmReverbFilter* const* filters, const float* gains, const AcmReverbOutput* const* outputs) {
return 0;
}
int APS5_VABI sceAcm_FFT(void) {
int APS5_VABI sceAcm_FFT(AcmBatchInfo* batch, std::int32_t transformSize, std::uint32_t transformCount, std::int32_t inputFormat, const void* const* inputs, std::int32_t outputFormat, void* const* outputs, std::int32_t flags) {
if (!batch || transformCount == 0 || transformCount > 0x7FFFFFFF || !inputs || !outputs || (flags & 3) == 3 || inputFormat < 0 || inputFormat > 1 || outputFormat < 0 || outputFormat > 1) return SCE_ACM_ERROR_BAD_POINTER;
if (transformSize != 512 && transformSize != 1024 && transformSize != 2048) return SCE_ACM_ERROR_BAD_POINTER;
const bool realInput = (flags & 1) != 0;
if (transformSize == 2048 && !realInput) return SCE_ACM_ERROR_BAD_POINTER;
const std::uint32_t inputBytes = transformSize * (inputFormat == 1 ? 2 : 4);
const std::uint32_t outputBytes = transformSize * (outputFormat == 1 ? 2 : 4);
const std::uint32_t inputBlocks = (inputBytes >> 9) >> (realInput ? 1 : 0);
const std::uint32_t outputBlocks = outputBytes >> 9;
std::uint32_t transformCommand;
std::uint32_t inputCommand;
std::uint32_t outputCommand;
std::uint32_t setupCommand;
std::uint32_t sizeCode;
if (transformSize == 512) {
sizeCode = 4;
transformCommand = realInput ? 7 : 11;
inputCommand = realInput ? 2 * inputFormat : 9 + inputFormat;
outputCommand = 4 | outputFormat;
setupCommand = realInput ? 6 : 12;
} else if (transformSize == 1024) {
sizeCode = 5;
transformCommand = realInput ? 8 : 13;
inputCommand = realInput ? 2 * inputFormat : 14 | inputFormat;
outputCommand = 5 | (2 * outputFormat);
setupCommand = realInput ? 10 : 12;
} else {
sizeCode = 6;
transformCommand = 6;
inputCommand = 2 * inputFormat;
outputCommand = 4 | outputFormat;
setupCommand = 8;
}
auto* buffer = static_cast<std::byte*>(batch->buffer);
auto offset = batch->offset;
const auto capacity = batch->buffer_size;
const auto appendWord = [&](std::uint64_t value) {
if (offset > capacity || capacity - offset < sizeof(value)) return false;
std::memcpy(buffer + offset, &value, sizeof(value));
offset += sizeof(value);
return true;
};
std::uint64_t header = static_cast<std::uint64_t>(sizeCode) << 8;
if (!(flags & 8)) {
const auto encodedCount = transformCount < 64000 ? transformCount : 64000;
header |= (std::uint64_t{1} << 16) | (static_cast<std::uint64_t>(transformSize) << 8) | (static_cast<std::uint64_t>(flags & 6) << 19) | (static_cast<std::uint64_t>(flags & 1) << 23) | (std::uint64_t{1} << (inputFormat == 1 ? 24 : 26)) | (std::uint64_t{1} << (outputFormat == 1 ? 25 : 27)) | (static_cast<std::uint64_t>(encodedCount) << 28);
}
constexpr int noSpace = static_cast<int>(0x81940009);
constexpr int badAlignment = static_cast<int>(0x8194000A);
if (!appendWord(header) || !appendWord(10 | (setupCommand << 8))) return noSpace;
for (std::uint32_t index = 0; index < transformCount; ++index) {
const auto inputIndex = static_cast<std::size_t>(index) * ((flags & 2) ? 2 : 1);
const auto input = reinterpret_cast<std::uintptr_t>(inputs[inputIndex]);
const auto secondInput = (flags & 2) ? reinterpret_cast<std::uintptr_t>(inputs[inputIndex + 1]) : 0;
if (!(flags & 2) && !input) return SCE_ACM_ERROR_BAD_POINTER;
if ((input | secondInput) & 127) return badAlignment;
const auto inputRecordSize = (flags & 2) ? 24u : 16u;
if (offset > capacity || capacity - offset < inputRecordSize) return noSpace;
appendWord(((flags & 2) ? 7 : 6) | (static_cast<std::uint64_t>(inputBlocks) << 32) | ((flags & 2) ? static_cast<std::uint64_t>(inputBytes / 2) << 48 : 0));
appendWord(input);
if (flags & 2) appendWord(secondInput);
if (!appendWord(10 | (inputCommand << 8)) || !appendWord(10 | (transformCommand << 8)) || !appendWord(10 | (outputCommand << 8))) return noSpace;
const auto outputIndex = static_cast<std::size_t>(index) * ((flags & 4) ? 2 : 1);
const auto output = reinterpret_cast<std::uintptr_t>(outputs[outputIndex]);
const auto secondOutput = (flags & 4) ? reinterpret_cast<std::uintptr_t>(outputs[outputIndex + 1]) : 0;
if ((output | secondOutput) & 127) return badAlignment;
const auto outputRecordSize = (flags & 4) ? 24u : 16u;
if (offset > capacity || capacity - offset < outputRecordSize) return noSpace;
appendWord(((flags & 4) ? 9 : 8) | (static_cast<std::uint64_t>(outputBlocks) << 32) | ((flags & 4) ? static_cast<std::uint64_t>(outputBytes / 2) << 48 : 0));
appendWord(output);
if (flags & 4) appendWord(secondOutput);
}
batch->offset = offset;
return 0;
}
int APS5_VABI sceAcm_Panner(void) {
int APS5_VABI sceAcm_Panner(AcmBatchInfo* batch, std::uint32_t sourceCount, const void* const* sources, std::uint32_t sourceMode, std::uint32_t outputMode, std::uint32_t channelCount, const void* const* sourceParameters, const void* const* sourceMatrices, const void* const* inputChannels, void* const* outputChannels) {
if (!batch || sourceCount == 0 || sourceCount > 65535 || sourceMode > 2 || outputMode > 1 || channelCount == 0 || channelCount > 36 || !sources || !sourceParameters || !sourceMatrices || !outputChannels) return SCE_ACM_ERROR_BAD_POINTER;
std::uintptr_t alignment = reinterpret_cast<std::uintptr_t>(sources) | reinterpret_cast<std::uintptr_t>(sourceParameters) | reinterpret_cast<std::uintptr_t>(sourceMatrices) | reinterpret_cast<std::uintptr_t>(inputChannels) | reinterpret_cast<std::uintptr_t>(outputChannels);
for (std::uint32_t index = 0; index < sourceCount; ++index) {
if (!sources[index] || !sourceParameters[index] || !sourceMatrices[index]) return SCE_ACM_ERROR_BAD_POINTER;
std::uint32_t parameterMode;
std::memcpy(&parameterMode, sourceParameters[index], sizeof(parameterMode));
if (parameterMode > 1) return SCE_ACM_ERROR_BAD_POINTER;
alignment |= reinterpret_cast<std::uintptr_t>(sources[index]) | reinterpret_cast<std::uintptr_t>(sourceParameters[index]) | reinterpret_cast<std::uintptr_t>(sourceMatrices[index]);
}
for (std::uint32_t index = 0; index < channelCount; ++index) {
if (!outputChannels[index]) return SCE_ACM_ERROR_BAD_POINTER;
alignment |= reinterpret_cast<std::uintptr_t>(outputChannels[index]);
if (inputChannels) alignment |= reinterpret_cast<std::uintptr_t>(inputChannels[index]);
}
if (alignment & 127) return static_cast<int>(0x8194000A);
const auto offset = batch->offset;
const auto capacity = batch->buffer_size;
if (offset > capacity || capacity - offset < 8) return static_cast<int>(0x81940009);
auto* buffer = static_cast<std::byte*>(batch->buffer);
const auto writeWord = [buffer](std::size_t position, std::uint64_t value) { std::memcpy(buffer + position, &value, sizeof(value)); };
const auto encodedCount = sourceCount < 255 ? sourceCount : 255;
writeWord(offset, (static_cast<std::uint64_t>(channelCount) << 24) | (static_cast<std::uint64_t>(encodedCount) << 16) | (std::uint64_t{1} << (sourceMode + 32)) | (std::uint64_t{1} << (outputMode + 35)) | 0x300);
if (capacity < 128 || capacity - offset < 152) return static_cast<int>(0x81940009);
const auto descriptorOffset = capacity - 128;
std::memset(buffer + descriptorOffset, 0, 128);
writeWord(descriptorOffset, sourceCount);
writeWord(descriptorOffset + 8, reinterpret_cast<std::uintptr_t>(inputChannels));
writeWord(descriptorOffset + 16, reinterpret_cast<std::uintptr_t>(outputChannels));
writeWord(descriptorOffset + 24, reinterpret_cast<std::uintptr_t>(sources));
writeWord(descriptorOffset + 32, sourceMode | (static_cast<std::uint64_t>(outputMode) << 32));
writeWord(descriptorOffset + 40, channelCount);
writeWord(descriptorOffset + 48, reinterpret_cast<std::uintptr_t>(sourceParameters));
writeWord(descriptorOffset + 56, reinterpret_cast<std::uintptr_t>(sourceMatrices));
writeWord(offset + 8, 0x8000000000000D);
writeWord(offset + 16, reinterpret_cast<std::uintptr_t>(buffer + descriptorOffset));
if (capacity - offset < 160) return static_cast<int>(0x81940009);
writeWord(offset + 24, 10);
batch->offset = offset + 32;
batch->buffer_size = descriptorOffset;
return 0;
}
int APS5_VABI sceAcmBatchJobPriority(void) {
int APS5_VABI sceAcmBatchJobPriority(AcmBatchInfo* batch, std::uint32_t priority) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
@@ -106,4 +217,10 @@ int APS5_VABI sceAcm_IFFT() {
return 0;
}
int APS5_VABI sceAcmBatchJobNotification() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
}
+14
View File
@@ -35,6 +35,7 @@ add_library(libSceAgc SHARED EXCLUDE_FROM_ALL
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/RegisterDefaults.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/Semaphore.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/ShaderFusion.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/StaticBuffer.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/Suspend.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Patch/src/CondExec.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Patch/src/Dma.cpp
@@ -96,6 +97,19 @@ if(BUILD_TESTING)
endif()
configure_windows_unwind(libSceAgc)
add_test_executable(agc_static_buffer_tests EXCLUDE_FROM_ALL tests/StaticBuffer.cpp)
target_include_directories(agc_static_buffer_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_static_buffer_tests PRIVATE libSceAgc libSceAgcDriver libc)
configure_windows_unwind(agc_static_buffer_tests)
if(BUILD_TESTING)
set_target_properties(agc_static_buffer_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_static_buffer COMMAND agc_static_buffer_tests)
if(WIN32)
set_tests_properties(agc_static_buffer PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_test_executable(agc_platform_tests EXCLUDE_FROM_ALL tests/Platform.cpp)
target_include_directories(agc_platform_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_platform_tests PRIVATE libSceAgc libc)
@@ -42,16 +42,14 @@ std::uint32_t* APS5_VABI sceAgcDcbSetIndexSize_0600(CommandBuffer* buf, std::uin
return packet;
}
std::uint32_t* APS5_VABI sceAgcDcbSetIndexIndirectArgs(CommandBuffer* buf, std::uint32_t dataOffsetInBytes) {
(void)buf;
(void)dataOffsetInBytes;
NotImplemented_nid_no_patch(__func__);
return nullptr;
std::uint32_t* APS5_VABI sceAgcDcbSetIndexIndirectArgs(CommandBuffer* buf, std::uint64_t attributeAddress, std::uint32_t attributeIndex) {
if (attributeAddress != 0) Agc::Command::CheckAddress(attributeAddress, 16, __func__);
Agc::Command::CheckBits(attributeIndex, 0xffffu, __func__);
return Agc::Command::Emit(buf, 0x91u, {static_cast<std::uint32_t>(attributeAddress), static_cast<std::uint32_t>(attributeAddress >> 32u), attributeIndex}, __func__);
}
std::uint32_t APS5_VABI sceAgcDcbSetIndexIndirectArgsGetSize() {
NotImplemented_nid_no_patch(__func__);
return 0;
return 16;
}
}
@@ -21,7 +21,7 @@ std::uint32_t* APS5_VABI sceAgcDcbGetLodStats(CommandBuffer* buf, std::uint8_t c
return Agc::Command::Emit(buf, 0x10u, {bufferSizeInBytes, static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u), control}, __func__);
}
std::uint32_t APS5_VABI sceAgcDcbBeginOcclusionQueryGetSize() {
std::uint32_t APS5_VABI sceAgcDcbBeginOcclusionQueryGetSize(std::int32_t queryType) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
+13
View File
@@ -0,0 +1,13 @@
#include <cstdint>
#include <cstddef>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
extern "C" {
int APS5_VABI sceAgcSuspendPointAndCheckStatus(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
}
@@ -0,0 +1,14 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_STATICBUFFER_HPP
#define CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_STATICBUFFER_HPP
#include <cstdint>
#include "SceTypes.hpp"
extern "C" {
int APS5_VABI sceAgcSetStaticBuffer(std::uint32_t index, const void* buffer);
int APS5_VABI sceAgcGetStaticBuffer(std::uint32_t index, void* buffer);
int APS5_VABI sceAgcSetStaticQword(std::uint32_t index, std::uint64_t value);
int APS5_VABI sceAgcGetStaticQword(std::uint32_t index, std::uint64_t* value);
}
#endif
+7 -18
View File
@@ -13,20 +13,12 @@ namespace {
constexpr std::uint64_t semaphoreMemoryAlignment = 0x4000u;
constexpr std::uint64_t semaphoreLabelBytes = 32u;
constexpr std::uint64_t semaphoreCounterBytes = 8u;
std::mutex semaphoreMutex;
std::uint8_t* semaphoreMemory = nullptr;
std::uint64_t semaphoreBytes = 0;
bool hasOnlyZeroLabelTails(const std::uint8_t* memory, std::uint64_t sizeInBytes) {
for (std::uint64_t label = 0; label < sizeInBytes; label += semaphoreLabelBytes) {
for (std::uint64_t byte = semaphoreCounterBytes; byte < semaphoreLabelBytes; ++byte) {
if (memory[label + byte] != 0) return false;
}
}
return true;
}
}
@@ -35,15 +27,12 @@ extern "C" {
int APS5_VABI sceAgcSetAmmSemaphoreMemory(void* memory, std::uint64_t sizeInBytes) {
const auto address = reinterpret_cast<std::uintptr_t>(memory);
std::lock_guard lock(semaphoreMutex);
Agc::Command::Require(semaphoreMemory == nullptr, __func__, "the AMM semaphore memory is already registered; the console's error is unknown");
if (semaphoreBytes != 0) return static_cast<int>(0x8a6c0048u);
if (((address | sizeInBytes) & (semaphoreMemoryAlignment - 1u)) != 0) return static_cast<int>(0x8a6c0002u);
Agc::Command::Require(memory != nullptr && sizeInBytes != 0, __func__, "null or empty AMM semaphore memory; the console's error is unknown");
Agc::Command::Require(((address | sizeInBytes) & (semaphoreMemoryAlignment - 1u)) == 0, __func__, "AMM semaphore memory or size is not 16 KiB aligned; the console's error is unknown");
Agc::Command::Require(sizeInBytes - 1u <= std::numeric_limits<std::uintptr_t>::max() - address, __func__, "AMM semaphore memory wraps the address space");
auto* labels = static_cast<std::uint8_t*>(memory);
Agc::Command::Require(hasOnlyZeroLabelTails(labels, sizeInBytes), __func__, "AMM semaphore labels hold data past their first 8 bytes; whether registration clears it is unknown");
for (std::uint64_t label = 0; label < sizeInBytes; label += semaphoreLabelBytes) {
std::memset(labels + label, 0, semaphoreCounterBytes);
}
std::memset(labels, 0, sizeInBytes);
semaphoreMemory = labels;
semaphoreBytes = sizeInBytes;
return 0;
@@ -51,9 +40,9 @@ int APS5_VABI sceAgcSetAmmSemaphoreMemory(void* memory, std::uint64_t sizeInByte
int APS5_VABI sceAgcGetSemaphoreLabel(std::uint32_t index, void** label) {
std::lock_guard lock(semaphoreMutex);
Agc::Command::Require(semaphoreMemory != nullptr, __func__, "no AMM semaphore memory is registered; the console's error is unknown");
Agc::Command::Require(index < semaphoreBytes / semaphoreLabelBytes, __func__, "the semaphore label lies outside the AMM semaphore memory; the console's error is unknown");
Agc::Command::Require(label != nullptr, __func__, "null semaphore label output; the console's error is unknown");
if (semaphoreBytes == 0) return static_cast<int>(0x8a6c0049u);
if (index >= semaphoreBytes / semaphoreLabelBytes) return static_cast<int>(0x8a6c000bu);
Agc::Command::Require(label != nullptr, __func__, "null semaphore label output");
*label = semaphoreMemory + static_cast<std::uint64_t>(index) * semaphoreLabelBytes;
return 0;
}
@@ -35,7 +35,6 @@ constexpr std::size_t FusedCodeAlignment = 256;
constexpr char TrailerMagic[8] = {'b', 'a', 'r', 'e', 'f', 'o', 'o', 't'};
constexpr std::size_t TrailerCodeBytesOffset = sizeof(TrailerMagic) + 3 * sizeof(std::uint32_t);
constexpr std::uint32_t Rsrc1VgprMask = 0x3fu;
constexpr std::uint32_t Rsrc1SgprMask = 0xfu << 6u;
constexpr std::uint32_t Rsrc2OffchipLdsEnable = 1u << 18u;
constexpr std::uint32_t Rsrc2UserSgprMask = (0x1fu << 1u) | (1u << 27u);
@@ -121,15 +120,19 @@ std::uint32_t FrontProgramBytes(const char* function, const Shader* front) {
return static_cast<std::uint32_t>((end - 1) * 4);
}
std::uint32_t MergeRsrc1(std::uint32_t back, std::uint32_t front) {
const auto vgprs = std::max(back & Rsrc1VgprMask, front & Rsrc1VgprMask);
const auto sgprs = std::max(back & Rsrc1SgprMask, front & Rsrc1SgprMask);
return (back & ~(Rsrc1VgprMask | Rsrc1SgprMask)) | vgprs | sgprs;
}
std::uint32_t MergeRsrc2(const char* function, std::uint32_t back, std::uint32_t front) {
if ((front & ~(Rsrc2UserSgprMask | Rsrc2OffchipLdsEnable) & ~back) != 0) Fail(function, "front half requires hardware state the back half does not enable");
return (back & ~Rsrc2UserSgprMask) | (front & (Rsrc2UserSgprMask | Rsrc2OffchipLdsEnable));
std::array<std::uint32_t, 2> mergeGeometryResources(std::uint32_t frontRsrc1, std::uint32_t frontRsrc2, std::uint32_t backRsrc1, std::uint32_t backRsrc2) {
const auto frontVgprs = ((frontRsrc1 & Rsrc1VgprMask) + 1u) * 4u;
const auto backVgprs = ((backRsrc1 & Rsrc1VgprMask) + 1u) * 4u;
const auto frontTotal = frontVgprs + (frontRsrc2 >> 28u) * 8u;
const auto backTotal = backVgprs + (backRsrc2 >> 28u) * 8u;
const auto maxTotal = std::max(frontTotal, backTotal);
const auto shared = std::max(frontVgprs, backVgprs) >= maxTotal ? 0u : (maxTotal - std::min(frontTotal, backTotal) + 7u) / 64u;
const auto vgprs = std::max(frontRsrc1 & Rsrc1VgprMask, backRsrc1 & Rsrc1VgprMask);
const auto rsrc1Field = std::max((frontRsrc1 >> 29u) & 3u, (backRsrc1 >> 29u) & 3u);
const auto rsrc2Field = std::max((frontRsrc2 >> 16u) & 3u, (backRsrc2 >> 16u) & 3u);
const auto rsrc1 = (backRsrc1 & ~(Rsrc1VgprMask | (3u << 29u))) | vgprs | (rsrc1Field << 29u);
const auto rsrc2 = (backRsrc2 & ~(0xf0000000u | (3u << 16u) | Rsrc2UserSgprMask | Rsrc2OffchipLdsEnable)) | ((shared & 15u) << 28u) | (rsrc2Field << 16u) | (frontRsrc2 & (Rsrc2UserSgprMask | Rsrc2OffchipLdsEnable));
return {rsrc1, rsrc2};
}
const ShaderRegister* FindHalfRegister(const Shader* shader, std::uint32_t offset) {
@@ -149,10 +152,14 @@ GeometryLayout ComputeGeometryLayout(const char* function, const Shader* front,
const auto* frontRsrc1 = FindHalfRegister(front, SPI_SHADER_PGM_RSRC1_GS);
const auto* frontRsrc2 = FindHalfRegister(front, SPI_SHADER_PGM_RSRC2_GS);
if (frontRsrc1 == nullptr || frontRsrc2 == nullptr) Fail(function, "front half has no GS resource registers");
const auto* backRsrc1 = FindHalfRegister(back, SPI_SHADER_PGM_RSRC1_GS);
const auto* backRsrc2 = FindHalfRegister(back, SPI_SHADER_PGM_RSRC2_GS);
if (backRsrc1 == nullptr || backRsrc2 == nullptr) Fail(function, "back half has no GS resource registers");
const auto resources = mergeGeometryResources(frontRsrc1->value, frontRsrc2->value, backRsrc1->value, backRsrc2->value);
layout.sh.assign(back->sh_registers, back->sh_registers + back->num_sh_registers);
for (auto& reg : layout.sh) {
if (reg.offset == SPI_SHADER_PGM_RSRC1_GS) reg.value = MergeRsrc1(reg.value, frontRsrc1->value);
else if (reg.offset == SPI_SHADER_PGM_RSRC2_GS) reg.value = MergeRsrc2(function, reg.value, frontRsrc2->value);
if (reg.offset == SPI_SHADER_PGM_RSRC1_GS) reg.value = resources[0];
else if (reg.offset == SPI_SHADER_PGM_RSRC2_GS) reg.value = resources[1];
}
for (std::uint32_t i = 0; i < front->num_sh_registers; ++i) {
const auto& reg = front->sh_registers[i];
@@ -0,0 +1,45 @@
#include "prx/libSceAgc/Misc/include/StaticBuffer.hpp"
#include <cstdint>
#include <cstring>
#include "SceTypes.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
namespace {
constexpr int GRAPHICS5_ERROR_INVALID_STATIC_BUFFER = static_cast<int>(0x8a6c000bu);
constexpr std::uint32_t StaticBufferCount = 6;
constexpr std::size_t StaticBufferBytes = 16;
constexpr std::size_t StaticQwordOffset = 16;
std::uint8_t* StaticTable() {
return static_cast<std::uint8_t*>(AgcDriverStaticTable_nid_postfix());
}
}
extern "C" {
int APS5_VABI sceAgcSetStaticBuffer(std::uint32_t index, const void* buffer) {
if (index >= StaticBufferCount) return GRAPHICS5_ERROR_INVALID_STATIC_BUFFER;
std::memcpy(StaticTable() + std::size_t{index} * StaticBufferBytes, buffer, StaticBufferBytes);
return 0;
}
int APS5_VABI sceAgcGetStaticBuffer(std::uint32_t index, void* buffer) {
if (index >= StaticBufferCount) return GRAPHICS5_ERROR_INVALID_STATIC_BUFFER;
std::memcpy(buffer, StaticTable() + std::size_t{index} * StaticBufferBytes, StaticBufferBytes);
return 0;
}
int APS5_VABI sceAgcSetStaticQword(std::uint32_t index, std::uint64_t value) {
std::memcpy(StaticTable() + StaticQwordOffset + std::size_t{index} * sizeof(value), &value, sizeof(value));
return 0;
}
int APS5_VABI sceAgcGetStaticQword(std::uint32_t index, std::uint64_t* value) {
std::memcpy(value, StaticTable() + StaticQwordOffset + std::size_t{index} * sizeof(*value), sizeof(*value));
return 0;
}
}
@@ -45,34 +45,29 @@ extern "C" {
int APS5_VABI sceAgcWaitRegMemPatchAddress(std::uint32_t* cmd, const volatile void* address) {
auto* wait = Agc::Command::ValidateWait(cmd, __func__);
const auto guestAddress = reinterpret_cast<std::uintptr_t>(address);
const auto alignment = ((wait[0] >> 8u) & 0xffu) == 0x3cu ? 4u : 8u;
Agc::Command::CheckAddress(guestAddress, alignment, __func__);
Agc::Command::CheckBits(guestAddress, 0xffffffffffffull, __func__);
cmd[2] = (cmd[2] & 0xffff0000u) | static_cast<std::uint32_t>(guestAddress >> 32u);
const auto lowMask = ((wait[0] >> 8u) & 0xffu) == 0x3cu ? 3u : 7u;
cmd[2] = (cmd[2] & 0xffff0000u) | (static_cast<std::uint32_t>(guestAddress >> 32u) & 0xffffu);
cmd[3] = static_cast<std::uint32_t>(guestAddress);
wait[2] = static_cast<std::uint32_t>(guestAddress);
wait[3] = static_cast<std::uint32_t>(guestAddress >> 32u);
wait[2] = (static_cast<std::uint32_t>(guestAddress) & ~lowMask) | (wait[2] & lowMask);
wait[3] = (static_cast<std::uint32_t>(guestAddress >> 32u) & 0x3ffffu) | (wait[3] & 0xfffc0000u);
return 0;
}
int APS5_VABI sceAgcWaitRegMemPatchReference(std::uint32_t* cmd, std::uint64_t reference) {
int APS5_VABI sceAgcWaitRegMemPatchReference(std::uint32_t* cmd, std::uint32_t reference) {
auto* wait = Agc::Command::ValidateWait(cmd, __func__);
Agc::Command::CheckBits(reference, 0xffffffffu, __func__);
wait[4] = static_cast<std::uint32_t>(reference);
return 0;
}
int APS5_VABI sceAgcWaitRegMemPatchMask(std::uint32_t* cmd, std::uint64_t mask) {
int APS5_VABI sceAgcWaitRegMemPatchMask(std::uint32_t* cmd, std::uint32_t mask) {
auto* wait = Agc::Command::ValidateWait(cmd, __func__);
Agc::Command::CheckBits(mask, 0xffffffffu, __func__);
wait[((wait[0] >> 8u) & 0xffu) == 0x3cu ? 5 : 6] = static_cast<std::uint32_t>(mask);
return 0;
}
int APS5_VABI sceAgcWaitRegMemPatchCompareFunction(std::uint32_t* cmd, std::uint8_t compareFunction) {
auto* wait = Agc::Command::ValidateWait(cmd, __func__);
Agc::Command::Require(compareFunction <= 6, __func__, "invalid wait comparison");
wait[1] = (wait[1] & ~7u) | compareFunction;
wait[1] = (wait[1] & ~7u) | (compareFunction & 7u);
return 0;
}
+8 -8
View File
@@ -5,42 +5,42 @@
extern "C" {
int APS5_VABI sceAgcAcbPushMarkerSpan() {
std::uint32_t* APS5_VABI sceAgcAcbPushMarkerSpan(CommandBuffer* buffer, const char* text, std::uint32_t length, std::uint32_t color) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcAcbSetMarkerSpan() {
std::uint32_t* APS5_VABI sceAgcAcbSetMarkerSpan(CommandBuffer* buffer, const char* text, std::uint32_t length, std::uint32_t color) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcAcquireMemSetEngine() {
int APS5_VABI sceAgcAcquireMemSetEngine(std::uint32_t* packet, std::uint8_t engine) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDcbPushMarkerSpan() {
std::uint32_t* APS5_VABI sceAgcDcbPushMarkerSpan(CommandBuffer* buffer, const char* text, std::uint32_t length, std::uint32_t color) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDcbSetMarkerSpan() {
std::uint32_t* APS5_VABI sceAgcDcbSetMarkerSpan(CommandBuffer* buffer, const char* text, std::uint32_t length, std::uint32_t color) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcSetSemaphoreMemory() {
int APS5_VABI sceAgcSetSemaphoreMemory(void* memory, std::uint64_t size) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcCbMemsetExclusive() {
std::uint32_t* APS5_VABI sceAgcCbMemsetExclusive(CommandBuffer* buffer, void* destination, const void* pattern, std::uint64_t size) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcSetShaderInstrumentation() {
int APS5_VABI sceAgcSetShaderInstrumentation(std::uint32_t flags) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
+36 -7
View File
@@ -28,8 +28,10 @@ extern "C" int APS5_VABI sceAgcInit_0090(std::uint32_t* state, std::uint32_t ver
extern "C" std::uint32_t* APS5_VABI sceAgcDcbDrawIndexAuto(CommandBuffer* buf, std::uint32_t indexCount, std::uint64_t modifier);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbDrawIndexIndirect(CommandBuffer* buf, std::uint32_t dataOffsetInBytes, std::uint64_t modifier);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbDrawIndexIndirectMulti(CommandBuffer* buf, std::uint32_t dataOffsetInBytes, std::uint32_t countIndirect, std::uint32_t maxCountOrCount, const volatile void* countAddress, std::uint32_t strideInBytes, std::uint64_t modifier);
extern "C" int APS5_VABI sceAgcWaitRegMemPatchReference(std::uint32_t* cmd, std::uint64_t reference);
extern "C" int APS5_VABI sceAgcWaitRegMemPatchMask(std::uint32_t* cmd, std::uint64_t mask);
extern "C" int APS5_VABI sceAgcWaitRegMemPatchAddress(std::uint32_t* cmd, const volatile void* address);
extern "C" int APS5_VABI sceAgcWaitRegMemPatchCompareFunction(std::uint32_t* cmd, std::uint8_t compareFunction);
extern "C" int APS5_VABI sceAgcWaitRegMemPatchReference(std::uint32_t* cmd, std::uint32_t reference);
extern "C" int APS5_VABI sceAgcWaitRegMemPatchMask(std::uint32_t* cmd, std::uint32_t mask);
extern "C" int APS5_VABI sceAgcGetDataPacketPayloadAddress_0090(std::uint32_t** addr, std::uint32_t* cmd, int type);
extern "C" std::uint32_t* APS5_VABI sceAgcCbSetShRegisterRangeDirect(CommandBuffer* buf, std::uint32_t offset, const std::uint32_t* values, std::uint32_t numValues);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbContextStateOp_0100(CommandBuffer* buf, std::uint32_t operation);
@@ -48,6 +50,8 @@ extern "C" std::uint32_t* APS5_VABI sceAgcAcbPushMarker(CommandBuffer* buf, cons
extern "C" std::uint32_t* APS5_VABI sceAgcAcbPopMarker(CommandBuffer* buf);
extern "C" std::uint32_t* APS5_VABI sceAgcAcbSetMarker(CommandBuffer* buf, const char* str, std::uint32_t color);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetIndexBuffer(CommandBuffer* buf, std::uint64_t indexAddress);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetIndexIndirectArgs(CommandBuffer* buf, std::uint64_t attributeAddress, std::uint32_t attributeIndex);
extern "C" std::uint32_t APS5_VABI sceAgcDcbSetIndexIndirectArgsGetSize();
extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetBaseIndirectArgs(CommandBuffer* buf, std::uint32_t shaderType, const volatile void* indirectBaseAddr);
extern "C" std::uint32_t* APS5_VABI sceAgcSetNop(CommandBuffer* buf, std::uint32_t sizeDw);
extern "C" int APS5_VABI sceAgcDebugRaiseException(std::uint32_t exceptionId);
@@ -325,6 +329,20 @@ void testIndexBuffer() {
throw std::runtime_error("misaligned index buffer was accepted");
}
void testIndexIndirectArgs() {
Storage storage;
const auto* packet = sceAgcDcbSetIndexIndirectArgs(&storage.buffer, 0x1111111122222220u, 0x4444u);
const std::array<std::uint32_t, 4> expected{0xc0029100u, 0x22222220u, 0x11111111u, 0x4444u};
check(packet == storage.words.data() && std::equal(expected.begin(), expected.end(), packet), "index indirect args packet differs from the console's");
check(sceAgcDcbSetIndexIndirectArgsGetSize() == expected.size() * sizeof(std::uint32_t), "index indirect args size differs from its packet");
const auto* cleared = sceAgcDcbSetIndexIndirectArgs(&storage.buffer, 0, 0);
check(cleared == packet + expected.size() && cleared[0] == expected[0] && cleared[1] == 0 && cleared[2] == 0 && cleared[3] == 0, "null index attribute base was not written as 0");
const auto before = storage.words;
expectFailure([&] { sceAgcDcbSetIndexIndirectArgs(&storage.buffer, 0x1111111122222228u, 0); });
expectFailure([&] { sceAgcDcbSetIndexIndirectArgs(&storage.buffer, 0x1111111122222220u, 0x10000u); });
check(storage.words == before && storage.buffer.cursor_up == cleared + expected.size(), "rejected index indirect args modified the buffer");
}
void testIndirectBase() {
Storage storage;
alignas(8) std::uint32_t arguments[4]{};
@@ -515,18 +533,28 @@ void testMemory() {
check(packet[8] == 0x55667788u && packet[9] == 0x11223344u && packet[12] == 2u, "wait reference or poll interval mismatch");
sceAgcWaitRegMemPatchReference(packet, 7);
check(packet[8] == 7 && packet[9] == 0x11223344u, "reference patch changed the high word");
const auto before = storage.words;
expectFailure([&] { sceAgcWaitRegMemPatchReference(packet, 0x100000000ull); });
check(storage.words == before, "invalid memory operation modified packet memory");
auto* truncated = Agc::Command::WriteWait(&storage.buffer, 0, 3, 0, 0, &value, 0x100000000ull, 0xffffffff00000001ull, 32, __func__);
check(truncated[8] == 0u && truncated[9] == 1u, "32-bit wait did not keep the low halves of the reference and mask");
sceAgcWaitRegMemPatchMask(packet, 0x0f0f0f0fu);
check(packet[10] == 0x0f0f0f0fu && packet[11] == 0xffffffffu && packet[8] == 7, "64-bit mask patch changed the wrong word");
sceAgcWaitRegMemPatchMask(truncated, 0xff00u);
check(truncated[9] == 0xff00u && truncated[8] == 0u && truncated[10] == 2u, "32-bit mask patch changed the wrong word");
for (auto* waitPacket : {packet, truncated}) {
const auto lowMask = waitPacket == packet ? 7u : 3u;
waitPacket[6] |= lowMask;
waitPacket[7] = 0xa5fc0000u;
const auto tag = waitPacket[2] & 0xffff0000u;
const std::uintptr_t address = 0x1236abcd12345679ull;
check(sceAgcWaitRegMemPatchAddress(waitPacket, reinterpret_cast<const volatile void*>(address)) == 0, "wait address patch failed");
check(waitPacket[2] == (tag | 0xabcdu) && waitPacket[3] == 0x12345679u, "wait wrapper address patch mismatch");
check(waitPacket[6] == ((0x12345679u & ~lowMask) | lowMask) && waitPacket[7] == 0xa5feabcdu, "wait address patch did not preserve control bits");
for (std::uint32_t comparison = 0; comparison <= 255; ++comparison) {
const auto control = waitPacket[5] & ~7u;
check(sceAgcWaitRegMemPatchCompareFunction(waitPacket, static_cast<std::uint8_t>(comparison)) == 0, "wait comparison patch failed");
check(waitPacket[5] == (control | (comparison & 7u)), "wait comparison patch changed control bits");
}
}
const auto beforeMask = storage.words;
expectFailure([&] { sceAgcWaitRegMemPatchMask(packet, 0x100000000ull); });
expectFailure([&] { sceAgcWaitRegMemPatchMask(truncated, 0x100000000ull); });
expectFailure([&] { sceAgcWaitRegMemPatchMask(packet + 4, 1); });
check(storage.words == beforeMask, "invalid mask patch modified packet memory");
}
@@ -790,6 +818,7 @@ int main(int argc, char** argv) {
testWorkloads();
testWorkloadStreams();
testIndexBuffer();
testIndexIndirectArgs();
testIndirectBase();
testContextState();
testFlip();
+11 -22
View File
@@ -70,8 +70,8 @@ void fillStorage() {
void testUnregistered() {
std::array<void*, 3> labels{storage.get(), storage.get(), storage.get()};
expectFailure([&] { sceAgcGetSemaphoreLabel(0, &labels[1]); });
expectFailure([] { sceAgcGetSemaphoreLabel(0, nullptr); });
check(sceAgcGetSemaphoreLabel(0, &labels[1]) == static_cast<int>(0x8a6c0049u), "unregistered label error mismatch");
check(sceAgcGetSemaphoreLabel(0, nullptr) == static_cast<int>(0x8a6c0049u), "unregistered null label error mismatch");
check(labels[0] == storage.get() && labels[1] == storage.get() && labels[2] == storage.get(), "a rejected semaphore label changed the output");
}
@@ -80,10 +80,10 @@ void testRegistrationRejections() {
expectFailure([] { sceAgcSetAmmSemaphoreMemory(nullptr, memoryBytes); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(nullptr, 0); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), 0); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory() + 1, memoryBytes); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory() + labelBytes, blockBytes); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), blockBytes - 1u); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), blockBytes + labelBytes); });
check(sceAgcSetAmmSemaphoreMemory(memory() + 1, memoryBytes) == static_cast<int>(0x8a6c0002u), "unaligned semaphore memory error mismatch");
check(sceAgcSetAmmSemaphoreMemory(memory() + labelBytes, blockBytes) == static_cast<int>(0x8a6c0002u), "unaligned semaphore memory error mismatch");
check(sceAgcSetAmmSemaphoreMemory(memory(), blockBytes - 1u) == static_cast<int>(0x8a6c0002u), "unaligned semaphore memory error mismatch");
check(sceAgcSetAmmSemaphoreMemory(memory(), blockBytes + labelBytes) == static_cast<int>(0x8a6c0002u), "unaligned semaphore memory error mismatch");
expectFailure([] {
const auto top = std::numeric_limits<std::uintptr_t>::max() & ~static_cast<std::uintptr_t>(blockBytes - 1u);
sceAgcSetAmmSemaphoreMemory(reinterpret_cast<void*>(top), memoryBytes);
@@ -92,18 +92,8 @@ void testRegistrationRejections() {
testUnregistered();
}
void testUnsettledClearing() {
for (const std::uint64_t offset : {5u * labelBytes + counterBytes, memoryBytes - 1u}) {
memory()[offset] = 0x77u;
const auto before = snapshot();
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes); });
checkUnchanged(before, "a registration with unsettled clearing changed memory");
testUnregistered();
memory()[offset] = 0;
}
}
void testRegistration() {
std::fill(memory(), memory() + memoryBytes, std::uint8_t{0x77});
const auto before = snapshot();
check(sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes) == 0, "registering the AMM semaphore memory failed");
check(std::all_of(memory(), memory() + memoryBytes, [](std::uint8_t byte) { return byte == 0; }), "registration did not clear the semaphore counters");
@@ -123,7 +113,7 @@ void testLabels() {
void testLabelRejections() {
for (const std::uint32_t index : {labelCount, labelCount + 1u, std::numeric_limits<std::uint32_t>::max()}) {
void* label = storage.get();
expectFailure([&] { sceAgcGetSemaphoreLabel(index, &label); });
check(sceAgcGetSemaphoreLabel(index, &label) == static_cast<int>(0x8a6c000bu), "out-of-range label error mismatch");
check(label == storage.get(), "a rejected semaphore label changed the output");
}
expectFailure([] { sceAgcGetSemaphoreLabel(0, nullptr); });
@@ -132,9 +122,9 @@ void testLabelRejections() {
void testRepeatedRegistration() {
std::fill(memory(), memory() + memoryBytes, std::uint8_t{0x3c});
const auto before = snapshot();
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(storage.get(), blockBytes); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(nullptr, 0); });
check(sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes) == static_cast<int>(0x8a6c0048u), "repeated registration error mismatch");
check(sceAgcSetAmmSemaphoreMemory(storage.get(), blockBytes) == static_cast<int>(0x8a6c0048u), "repeated registration error mismatch");
check(sceAgcSetAmmSemaphoreMemory(nullptr, 0) == static_cast<int>(0x8a6c0048u), "repeated registration error mismatch");
checkUnchanged(before, "a repeated registration changed memory");
void* label = nullptr;
check(sceAgcGetSemaphoreLabel(labelCount - 1u, &label) == 0 && label == memory() + memoryBytes - labelBytes, "a repeated registration replaced the semaphore memory");
@@ -198,7 +188,6 @@ int main() {
fillStorage();
testUnregistered();
testRegistrationRejections();
testUnsettledClearing();
testRegistration();
testLabels();
testMemSemaphore();
+35 -7
View File
@@ -147,7 +147,7 @@ void testFusion(std::size_t misalignment, std::uint32_t terminator = SSetpcS6) {
const auto* frontLo = find(fused, 0x0C8u);
check(backLo != nullptr && backLo->value == static_cast<std::uint32_t>((base + 256) >> 8u), "back program address was not patched");
check(frontLo != nullptr && frontLo->value == static_cast<std::uint32_t>(base >> 8u), "fused program address was not patched");
check(find(fused, 0x08Au)->value == ((1u << 24u) | (2u << 6u) | 5u), "RSRC1 register counts were not merged");
check(find(fused, 0x08Au)->value == ((1u << 24u) | (1u << 6u) | 5u), "RSRC1 register counts were not merged");
check(find(fused, 0x08Bu)->value == ((6u << 1u) | (1u << 7u) | (1u << 15u)), "RSRC2 user SGPRs were not taken from the front half");
check(find(fused, 0x0B2u) != nullptr && find(fused, 0x0B2u)->value == 0x55u, "front-only SH register was lost");
check(find(fused, 0x080u) == nullptr, "front program checksum was kept");
@@ -208,7 +208,7 @@ void testOffchipLdsEnable() {
Shader fused{};
check(sceAgcFuseShaderHalves_0200(&fused, &halves.front, &halves.back, aligned(storage, 256)) == 0, "offchip LDS enable rejected by fusion");
const auto* resource = find(fused, 0x08bu);
check(resource != nullptr && resource->value == ((6u << 1u) | (1u << 7u) | (1u << 15u) | frontEnable | backEnable), "fusion lost offchip LDS enable or changed the back-half state or front user SGPRs");
check(resource != nullptr && resource->value == ((6u << 1u) | (1u << 7u) | (1u << 15u) | frontEnable), "fusion did not take offchip LDS enable and user SGPRs from the front half");
}
}
Halves reported;
@@ -222,6 +222,38 @@ void testOffchipLdsEnable() {
check(find(fused, 0x08bu)->value == 0x00af0004u, "reported shader resources did not retain the front offchip LDS enable");
}
void testResourceMerging() {
struct Case {
std::uint32_t front1;
std::uint32_t front2;
std::uint32_t back1;
std::uint32_t back2;
std::uint32_t expected1;
std::uint32_t expected2;
};
const Case cases[] = {
{0u, 0x80000000u, 0u, 0u, 0u, 0x10000000u},
{1u, 0xf0000000u, 3u, 0u, 3u, 0x10000000u},
{0u, 0xf0000000u, 0u, 0xf0000000u, 0u, 0u},
{0x400003c1u, 0x0006000cu, 0x61000043u, 0x00018001u, 0x61000043u, 0x0006800du},
{0x60000007u, 0x00010000u, 0x21000041u, 0x00020000u, 0x61000047u, 0x00020000u}
};
for (const auto& entry : cases) {
Halves halves;
halves.frontSh[1].value = entry.front1;
halves.frontSh[2].value = entry.front2;
halves.backSh[2].value = entry.back1;
halves.backSh[3].value = entry.back2;
SizeAlign size{};
check(sceAgcGetFusedShaderSize_0080(&size, &halves.front, &halves.back) == 0, "resource merge size query failed");
std::vector<std::uint8_t> scratch(size.m_size);
Shader fused{};
check(sceAgcFuseShaderHalves_0200(&fused, &halves.front, &halves.back, scratch.data()) == 0, "resource merge failed");
check(find(fused, 0x08au)->value == entry.expected1, "resource register 1 merge mismatch");
check(find(fused, 0x08bu)->value == entry.expected2, "resource register 2 merge mismatch");
}
}
void testRejections() {
Halves halves;
SizeAlign size{};
@@ -249,11 +281,6 @@ void testRejections() {
noTrailer.frontCode[5] = 0;
expectFailure([&] { sceAgcGetFusedShaderSize_0080(&size, &noTrailer.front, &noTrailer.back); });
}
{
Halves hardware;
hardware.frontSh[2].value |= 1u << 16u;
expectFailure([&] { sceAgcGetFusedShaderSize_0080(&size, &hardware.front, &hardware.back); });
}
{
Halves constants;
constants.back.embedded_constant_buffer_size_dqw = 1;
@@ -305,6 +332,7 @@ int main() {
testFusion(0, SSwappcNullS6);
testScratchReuse();
testOffchipLdsEnable();
testResourceMerging();
testRejections();
testHullHalves();
LibcRunShutdown_nid_postfix();
@@ -0,0 +1,64 @@
#include "prx/libSceAgc/Misc/include/StaticBuffer.hpp"
#include "prx/libc/include/GuestArena.hpp"
#include <array>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <stdexcept>
namespace {
constexpr std::uintptr_t StaticTable = 0xfe0040000ull + GuestArena::GuestAddressOffset;
constexpr int InvalidStaticBuffer = static_cast<int>(0x8a6c000bu);
void check(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
std::array<std::uint64_t, 2> slot(std::uint32_t index) {
std::array<std::uint64_t, 2> words{};
std::memcpy(words.data(), reinterpret_cast<const void*>(StaticTable + index * 16u), sizeof(words));
return words;
}
void testBuffers() {
std::array<std::uint64_t, 2> fetchShaderTable{};
check(sceAgcGetStaticBuffer(0, fetchShaderTable.data()) == 0, "reading static buffer 0 failed");
check(fetchShaderTable == slot(0) && fetchShaderTable[0] >> 48u == 16u, "static buffer 0 is not the driver's fetch shader table");
const std::array<std::uint64_t, 2> written{0x1122334455667788ull, 0x99aabbccddeeff00ull};
check(sceAgcSetStaticBuffer(5, written.data()) == 0, "writing static buffer 5 failed");
check(slot(5) == written, "static buffer 5 was not stored at the sixth 16-byte slot");
std::array<std::uint64_t, 2> read{};
check(sceAgcGetStaticBuffer(5, read.data()) == 0 && read == written, "static buffer 5 did not read back");
check(sceAgcSetStaticBuffer(6, written.data()) == InvalidStaticBuffer, "static buffer index 6 was accepted");
check(sceAgcGetStaticBuffer(6, read.data()) == InvalidStaticBuffer, "static buffer index 6 was read");
check(slot(0) == fetchShaderTable, "static buffer writes changed slot 0");
}
void testQwords() {
check(sceAgcSetStaticQword(0, 0x0123456789abcdefull) == 0, "writing static qword 0 failed");
check(sceAgcSetStaticQword(3, 0xfedcba9876543210ull) == 0, "writing static qword 3 failed");
check(slot(1)[0] == 0x0123456789abcdefull && slot(2)[1] == 0xfedcba9876543210ull, "static qwords are not stored after the first 16-byte slot");
std::uint64_t value = 0;
check(sceAgcGetStaticQword(3, &value) == 0 && value == 0xfedcba9876543210ull, "static qword 3 did not read back");
std::array<std::uint64_t, 2> buffer{};
check(sceAgcGetStaticBuffer(1, buffer.data()) == 0 && buffer[0] == 0x0123456789abcdefull, "static qword 0 is not the first half of static buffer 1");
}
}
int main() {
#ifdef ANYPS5_ENABLE_ASAN
std::puts("skipped: the static table lives at the PS5's fixed 0xfe0040000, which an ANYPS5_ENABLE_ASAN build cannot map");
return 0;
#endif
try {
testBuffers();
testQwords();
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
return 1;
}
return 0;
}
+184 -14
View File
@@ -79,6 +79,18 @@ foreach(agcReadbackKind IN ITEMS Uint Depth)
list(APPEND agcReadbackHeaders ${agcReadbackHeader})
endforeach()
set(agcMultisampleUploadSpv ${agcTextureDetileGeneratedDir}/MultisampleUpload.spv)
set(agcMultisampleUploadHeader ${agcTextureDetileGeneratedDir}/MultisampleUpload_spv.h)
add_custom_command(
OUTPUT "${agcMultisampleUploadHeader}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}"
COMMAND "$<TARGET_FILE:glslang-standalone>" -V --target-env vulkan1.1 -o "${agcMultisampleUploadSpv}" "${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/MultisampleUpload.comp"
COMMAND ${CMAKE_COMMAND} -DINPUT=${agcMultisampleUploadSpv} -DOUTPUT=${agcMultisampleUploadHeader}
-DSYMBOL=MULTISAMPLE_UPLOAD_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake"
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/MultisampleUpload.comp" glslang-standalone
VERBATIM
)
set(agcMeshArgumentsShaderSpv ${agcTextureDetileGeneratedDir}/MeshArguments.spv)
set(agcMeshArgumentsShaderHeader ${agcTextureDetileGeneratedDir}/MeshArguments_spv.h)
add_custom_command(
@@ -130,6 +142,28 @@ add_custom_command(
VERBATIM
)
set(agcDepthSampleShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/DepthSample.comp)
foreach(agcDepthSampleKind IN ITEMS Single Multisample)
string(TOUPPER ${agcDepthSampleKind} agcDepthSampleSymbol)
if(agcDepthSampleKind STREQUAL "Multisample")
set(agcDepthSampleMultisampled 1)
else()
set(agcDepthSampleMultisampled 0)
endif()
set(agcDepthSampleShaderSpv ${agcTextureDetileGeneratedDir}/DepthSample${agcDepthSampleKind}.spv)
set(agcDepthSampleShaderHeader ${agcTextureDetileGeneratedDir}/DepthSample${agcDepthSampleKind}_spv.h)
add_custom_command(
OUTPUT "${agcDepthSampleShaderHeader}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}"
COMMAND "$<TARGET_FILE:glslang-standalone>" -V --target-env vulkan1.1 -DMULTISAMPLED=${agcDepthSampleMultisampled} -o "${agcDepthSampleShaderSpv}" "${agcDepthSampleShaderSource}"
COMMAND ${CMAKE_COMMAND} -DINPUT=${agcDepthSampleShaderSpv} -DOUTPUT=${agcDepthSampleShaderHeader}
-DSYMBOL=DEPTH_SAMPLE_${agcDepthSampleSymbol}_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake"
DEPENDS "${agcDepthSampleShaderSource}" glslang-standalone
VERBATIM
)
list(APPEND agcDepthSampleShaderHeaders ${agcDepthSampleShaderHeader})
endforeach()
set(agcSampleCounterShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/SampleCounter.comp)
set(agcSampleCounterShaderSpv ${agcTextureDetileGeneratedDir}/SampleCounter.spv)
set(agcSampleCounterShaderHeader ${agcTextureDetileGeneratedDir}/SampleCounter_spv.h)
@@ -172,6 +206,25 @@ foreach(agcPresentationStage IN ITEMS Vert Frag)
)
list(APPEND agcPresentationShaderHeaders ${agcPresentationShaderHeader})
endforeach()
foreach(agcSampleOrderVariant IN ITEMS "R8;comp;-DTEXEL_FORMAT=r8ui" "R16;comp;-DTEXEL_FORMAT=r16ui" "R32;comp;-DTEXEL_FORMAT=r32ui" "RG32;comp;-DTEXEL_FORMAT=rg32ui" "RGBA32;comp;-DTEXEL_FORMAT=rgba32ui" "Vertex;vert;-DPLAIN=1" "Depth;frag;-DPLAIN=1" "StencilExport;frag;-DSTENCIL_EXPORT=1" "StencilBits;frag;-DSTENCIL_BITS=1")
list(GET agcSampleOrderVariant 0 agcSampleOrderName)
list(GET agcSampleOrderVariant 1 agcSampleOrderExtension)
list(GET agcSampleOrderVariant 2 agcSampleOrderDefine)
string(TOUPPER "${agcSampleOrderName}" agcSampleOrderSymbol)
set(agcSampleOrderShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/SampleOrder.${agcSampleOrderExtension})
set(agcSampleOrderShaderSpv ${agcTextureDetileGeneratedDir}/SampleOrder${agcSampleOrderName}.spv)
set(agcSampleOrderShaderHeader ${agcTextureDetileGeneratedDir}/SampleOrder${agcSampleOrderName}_spv.h)
add_custom_command(
OUTPUT "${agcSampleOrderShaderHeader}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}"
COMMAND "$<TARGET_FILE:glslang-standalone>" -V --target-env vulkan1.1 ${agcSampleOrderDefine} -o "${agcSampleOrderShaderSpv}" "${agcSampleOrderShaderSource}"
COMMAND ${CMAKE_COMMAND} -DINPUT=${agcSampleOrderShaderSpv} -DOUTPUT=${agcSampleOrderShaderHeader}
-DSYMBOL=SAMPLE_ORDER_${agcSampleOrderSymbol}_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake"
DEPENDS "${agcSampleOrderShaderSource}" glslang-standalone
VERBATIM
)
list(APPEND agcSampleOrderShaderHeaders ${agcSampleOrderShaderHeader})
endforeach()
foreach(agcOverlayStage IN ITEMS Vert Frag)
string(TOLOWER "${agcOverlayStage}" agcOverlayExtension)
string(TOUPPER "${agcOverlayStage}" agcOverlaySymbol)
@@ -293,16 +346,22 @@ set(agcDriverSources
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/TextureFormat.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/DccMetadata.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/TextureTiling.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/MultisampleTextureLayout.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/TextureDetiler.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/MultisampleReadback.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/MultisampleUpload.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Texture.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PassHazards.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/SampleOrder.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/BarrierBatch.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/UnitShadow.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Sampler.cpp
${agcSampleOrderShaderHeaders}
${agcTextureDetileShaderHeader}
${agcReadbackHeaders}
${agcMultisampleUploadHeader}
${agcCmaskClearShaderHeader}
${agcDepthSampleShaderHeaders}
${agcMeshArgumentsShaderHeader}
${agcTextureDetileImageShaderHeader}
${agcTextureRepackShaderHeader}
@@ -712,15 +771,6 @@ if(BUILD_TESTING)
add_test(NAME agc_pipeline_link_optimization COMMAND agc_pipeline_link_optimization_tests)
endif()
add_test_executable(agc_robust_buffer_access_tests EXCLUDE_FROM_ALL tests/RobustBufferAccess.cpp)
target_include_directories(agc_robust_buffer_access_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR})
set_target_properties(agc_robust_buffer_access_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_robust_buffer_access_tests)
if(BUILD_TESTING)
set_target_properties(agc_robust_buffer_access_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_robust_buffer_access COMMAND agc_robust_buffer_access_tests)
endif()
add_test_executable(agc_draw_inputs_in_place_driver_tests EXCLUDE_FROM_ALL tests/DrawInputsInPlace.cpp)
target_include_directories(agc_draw_inputs_in_place_driver_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR})
set_target_properties(agc_draw_inputs_in_place_driver_tests PROPERTIES CXX_EXTENSIONS OFF)
@@ -1406,9 +1456,9 @@ if(BUILD_TESTING)
endif()
if(WIN32)
add_test(NAME agc_reimport_bench COMMAND agc_driver_recorder_tests --reimport-bench)
add_test(NAME agc_trimmed_import COMMAND agc_driver_recorder_tests --trimmed-import-only)
set_tests_properties(agc_reimport_bench agc_trimmed_import PROPERTIES SKIP_RETURN_CODE 77)
set_tests_properties(agc_reimport_bench agc_trimmed_import PROPERTIES ENVIRONMENT_MODIFICATION
add_test(NAME agc_host_import_cut_lifetime COMMAND agc_driver_recorder_tests --host-import-cut-only)
set_tests_properties(agc_reimport_bench agc_host_import_cut_lifetime PROPERTIES SKIP_RETURN_CODE 77)
set_tests_properties(agc_reimport_bench agc_host_import_cut_lifetime PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_unchanged_cpu_stamps COMMAND agc_driver_recorder_tests --unchanged-cpu-stamps-only)
@@ -1417,6 +1467,25 @@ if(BUILD_TESTING)
set_tests_properties(agc_unchanged_cpu_stamps PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
foreach(mode IN ITEMS map ring shared uncached)
add_test(NAME agc_collect_mapping_history_${mode} COMMAND agc_driver_recorder_tests --collect-mapping-history-only)
set_tests_properties(agc_collect_mapping_history_${mode} PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_collect_mapping_history_${mode} PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endforeach()
set_tests_properties(agc_collect_mapping_history_ring PROPERTIES ENVIRONMENT "APS5_NO_EPOCH_COLLECT_MAP=1")
set_tests_properties(agc_collect_mapping_history_shared PROPERTIES ENVIRONMENT "APS5_SHARED_COLLECT_MEMO=1")
set_tests_properties(agc_collect_mapping_history_uncached PROPERTIES ENVIRONMENT "APS5_NO_COLLECT_MEMO=1")
if(WIN32)
foreach(mode IN ITEMS cached uncached)
add_test(NAME agc_hint_mapping_history_${mode} COMMAND agc_driver_recorder_tests --hint-mapping-history-only)
set_tests_properties(agc_hint_mapping_history_${mode} PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30
ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endforeach()
set_tests_properties(agc_hint_mapping_history_uncached PROPERTIES ENVIRONMENT "APS5_NO_COLLECT_MEMO=1")
endif()
if(WIN32)
add_test(NAME agc_partial_watch_restoration COMMAND agc_driver_recorder_tests --partial-watch-restoration-only)
set_tests_properties(agc_partial_watch_restoration PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT_MODIFICATION
@@ -1521,6 +1590,8 @@ if(BUILD_TESTING)
set_tests_properties(agc_sparse_run_import_copied PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
add_test(NAME agc_unit_shadow COMMAND agc_driver_recorder_tests --unit-shadow-only)
set_tests_properties(agc_unit_shadow PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000")
add_test(NAME agc_idle_publish_wait COMMAND agc_driver_recorder_tests --idle-publish-wait-only)
set_tests_properties(agc_idle_publish_wait PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000;APS5_WRITE_WATCH_IMPORTS=watch")
if(WIN32)
add_test(NAME agc_storage_cpu_snapshot COMMAND agc_driver_recorder_tests --storage-cpu-snapshot-only)
set_tests_properties(agc_storage_cpu_snapshot PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 SKIP_RETURN_CODE 77
@@ -1554,7 +1625,7 @@ if(BUILD_TESTING)
add_test(NAME agc_metadata_pass COMMAND agc_driver_recorder_tests --metadata-pass-only)
set_tests_properties(agc_metadata_pass PROPERTIES SKIP_RETURN_CODE 77)
if(WIN32)
set_tests_properties(agc_recorder_read_list agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_sparse_run_import agc_sparse_run_import_copied agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_draw_snapshot_epoch agc_multisample_guest_access agc_draw_inputs_in_place agc_draw_inputs_copied agc_resident_present agc_slice_dirty agc_queued_write_groups agc_metadata_pass agc_tiled_resolve PROPERTIES ENVIRONMENT_MODIFICATION
set_tests_properties(agc_recorder_read_list agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_sparse_run_import agc_sparse_run_import_copied agc_unit_shadow agc_idle_publish_wait agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_draw_snapshot_epoch agc_multisample_guest_access agc_draw_inputs_in_place agc_draw_inputs_copied agc_resident_present agc_slice_dirty agc_queued_write_groups agc_metadata_pass agc_tiled_resolve PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
add_test(NAME agc_host_import_second_mapping COMMAND agc_driver_recorder_tests --host-import-second-mapping-only)
set_tests_properties(agc_host_import_second_mapping PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT_MODIFICATION
@@ -1588,6 +1659,13 @@ if(BUILD_TESTING)
endif()
set_property(TEST agc_driver_host_import_lifetime APPEND PROPERTY ENVIRONMENT_MODIFICATION
"APS5_RELEASE_UNDER_LOCK=unset:" "APS5_RELEASE_ON_UNLOCK=unset:")
if(WIN32)
add_test(NAME agc_driver_host_import_lifetime_other_family COMMAND agc_driver_recorder_tests --host-import-lifetime-other-family-only)
set_tests_properties(agc_driver_host_import_lifetime_other_family PROPERTIES
ENVIRONMENT "APS5_PIN_WAIT_MS=2000" SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
set_property(TEST agc_driver_host_import_lifetime_other_family APPEND PROPERTY ENVIRONMENT_MODIFICATION
"APS5_RELEASE_UNDER_LOCK=unset:" "APS5_RELEASE_ON_UNLOCK=unset:")
endif()
endif()
add_test_executable(agc_recorder_kept_bytes_tests EXCLUDE_FROM_ALL tests/RecorderKeptBytes.cpp)
@@ -1802,6 +1880,28 @@ endif()
set_target_properties(agc_driver_graphics_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_driver_graphics_tests)
add_test_executable(agc_driver_msaa_layout_tests EXCLUDE_FROM_ALL
tests/MultisampleTextureLayout.cpp
Graphics/src/MultisampleTextureLayout.cpp
)
target_include_directories(agc_driver_msaa_layout_tests PRIVATE
${LIBS_INCLUDE_DIR}
${CMAKE_SOURCE_DIR}/core/shader/recompiler
${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}
)
target_link_libraries(agc_driver_msaa_layout_tests PRIVATE libc)
set_target_properties(agc_driver_msaa_layout_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_driver_msaa_layout_tests)
if(BUILD_TESTING)
set_target_properties(agc_driver_msaa_layout_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_driver_msaa_layout COMMAND agc_driver_msaa_layout_tests)
set_tests_properties(agc_driver_msaa_layout PROPERTIES TIMEOUT 20)
if(WIN32)
set_tests_properties(agc_driver_msaa_layout PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
foreach(agcVertexRangeShader IN ITEMS VertexRange.vert VertexRangeProducer.frag)
string(REPLACE "." "_" agcVertexRangeName "${agcVertexRangeShader}")
string(TOUPPER "${agcVertexRangeName}_SPV" agcVertexRangeSymbol)
@@ -2051,6 +2151,27 @@ foreach(stage IN ITEMS vert frag)
endforeach()
target_include_directories(agc_driver_provoking_vertex_tests PRIVATE ${agcTestShaderGeneratedRoot})
foreach(agcSampleOrderTestVariant IN ITEMS "R8;SampleOrderTexels.comp;-DTEXEL_FORMAT=r8ui" "R16;SampleOrderTexels.comp;-DTEXEL_FORMAT=r16ui" "R32;SampleOrderTexels.comp;-DTEXEL_FORMAT=r32ui" "RG32;SampleOrderTexels.comp;-DTEXEL_FORMAT=rg32ui" "RGBA32;SampleOrderTexels.comp;-DTEXEL_FORMAT=rgba32ui" "FillVertex;SampleOrderFill.vert;-DPLAIN=1" "FillDepth;SampleOrderFill.frag;-DPLAIN=1" "FillStencil;SampleOrderFill.frag;-DSTENCIL=1" "ReadDepth;SampleOrderRead.comp;-DPLAIN=1" "ReadStencil;SampleOrderRead.comp;-DSTENCIL=1" "ReadColor;SampleOrderRead.comp;-DCOLOR=1")
list(GET agcSampleOrderTestVariant 0 agcSampleOrderTestName)
list(GET agcSampleOrderTestVariant 1 agcSampleOrderTestFile)
list(GET agcSampleOrderTestVariant 2 agcSampleOrderTestDefine)
string(TOUPPER "${agcSampleOrderTestName}" agcSampleOrderTestSymbol)
set(agcSampleOrderTestSource ${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/${agcSampleOrderTestFile})
set(agcSampleOrderTestSpv ${agcTestShaderGeneratedRoot}/SampleOrderTest${agcSampleOrderTestName}.spv)
set(agcSampleOrderTestHeader ${agcTestShaderGeneratedRoot}/SampleOrderTest${agcSampleOrderTestName}_spv.h)
add_custom_command(
OUTPUT "${agcSampleOrderTestHeader}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTestShaderGeneratedRoot}"
COMMAND "$<TARGET_FILE:glslang-standalone>" -V --target-env vulkan1.1 ${agcSampleOrderTestDefine} -o "${agcSampleOrderTestSpv}" "${agcSampleOrderTestSource}"
COMMAND ${CMAKE_COMMAND} -DINPUT=${agcSampleOrderTestSpv} -DOUTPUT=${agcSampleOrderTestHeader}
-DSYMBOL=SAMPLE_ORDER_TEST_${agcSampleOrderTestSymbol}_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake"
DEPENDS "${agcSampleOrderTestSource}" glslang-standalone
VERBATIM
)
target_sources(agc_driver_sample_order_tests PRIVATE ${agcSampleOrderTestHeader})
endforeach()
target_include_directories(agc_driver_sample_order_tests PRIVATE ${agcTestShaderGeneratedRoot})
target_include_directories(agc_driver_prepared_exports_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
target_include_directories(agc_driver_pipeline_journal_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
@@ -2162,6 +2283,13 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endforeach()
add_test(NAME agc_driver_guarded_direct_root COMMAND agc_driver_guarded_null_pointer_tests --direct-root-only)
set_tests_properties(agc_driver_guarded_direct_root PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(agc_driver_guarded_direct_root PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_bda_device_selection_cpu COMMAND agc_driver_bda_device_tests --device-selection-cpu)
set_tests_properties(agc_driver_bda_device_selection_cpu PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
@@ -2169,6 +2297,13 @@ if(BUILD_TESTING)
set_tests_properties(agc_driver_bda_device_selection_cpu PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_draw_unsupported_buffer_type_skip_refused COMMAND agc_driver_draw_unsupported_buffer_type_tests --skip-refused)
set_tests_properties(agc_driver_draw_unsupported_buffer_type_skip_refused PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_SKIP_REFUSED_DRAWS=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(agc_driver_draw_unsupported_buffer_type_skip_refused PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_sparse_indirect_indices_unreadable COMMAND agc_driver_sparse_indirect_indices_tests --unreadable)
set_tests_properties(agc_driver_sparse_indirect_indices agc_driver_sparse_indirect_indices_unreadable PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
@@ -2193,7 +2328,7 @@ if(BUILD_TESTING)
set_tests_properties(agc_driver_queued_raw_shader PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=8" TIMEOUT 30)
add_test(NAME agc_driver_partially_resident_pending_stores COMMAND agc_driver_partially_resident_snapshot_tests --pending-stores)
set_tests_properties(agc_driver_multisample_readback PROPERTIES TIMEOUT 20)
foreach(agcReadbackCase IN ITEMS uint8 uint16 uint64 uint128 depth)
foreach(agcReadbackCase IN ITEMS uint8 uint16 uint64 uint128 depth upload retile depth_retile)
add_test(NAME agc_driver_multisample_readback_${agcReadbackCase} COMMAND agc_driver_multisample_readback_tests ${agcReadbackCase})
set_tests_properties(agc_driver_multisample_readback_${agcReadbackCase} PROPERTIES TIMEOUT 20 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
@@ -2206,6 +2341,24 @@ if(BUILD_TESTING)
set_tests_properties(agc_driver_partially_resident_pending_stores PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_shader_disk_cache_large_file COMMAND agc_shader_disk_cache_tests --large-file)
if(Python3_Interpreter_FOUND)
set(housekeepingModes plain)
if(WIN32)
list(APPEND housekeepingModes extended extended-long device)
else()
list(APPEND housekeepingModes plain-long)
endif()
foreach(housekeepingMode IN LISTS housekeepingModes)
set(housekeepingTest agc_shader_cache_housekeeping_${housekeepingMode})
add_test(NAME ${housekeepingTest} COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_shader_cache_housekeeping.py
$<TARGET_FILE:agc_shader_disk_cache_tests> ${housekeepingMode})
set_tests_properties(${housekeepingTest} PROPERTIES TIMEOUT 20)
if(WIN32)
set_tests_properties(${housekeepingTest} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endforeach()
endif()
set_tests_properties(agc_shader_disk_cache_large_file PROPERTIES SKIP_RETURN_CODE 77 RUN_SERIAL TRUE WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 240)
if(WIN32)
set_tests_properties(agc_shader_disk_cache_large_file PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
@@ -2267,6 +2420,13 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
set_tests_properties(agc_driver_native_multisample_depth PROPERTIES TIMEOUT 30)
set_tests_properties(agc_driver_sample_order PROPERTIES TIMEOUT 30)
add_test(NAME agc_driver_sample_order_grid_reference COMMAND agc_driver_sample_order_tests --grid-reference)
add_test(NAME agc_driver_sample_order_recipe_depth_locations COMMAND agc_driver_sample_order_tests --recipe-depth-locations)
set_tests_properties(agc_driver_sample_order_grid_reference agc_driver_sample_order_recipe_depth_locations PROPERTIES TIMEOUT 30 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(agc_driver_sample_order_grid_reference agc_driver_sample_order_recipe_depth_locations PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_native_depth_retirement COMMAND agc_driver_native_multisample_depth_tests --retirement)
set_tests_properties(agc_driver_native_depth_retirement PROPERTIES TIMEOUT 30 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
add_test(NAME agc_driver_native_depth_clears COMMAND agc_driver_native_multisample_depth_tests --clears)
@@ -2619,6 +2779,13 @@ if(BUILD_TESTING)
endif()
endforeach()
endforeach()
add_test(NAME agc_driver_array_layer_target_cpu COMMAND agc_driver_array_layer_target_tests)
set_tests_properties(agc_driver_array_layer_target_cpu PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_HOST_IMPORT=1" TIMEOUT 30 SKIP_RETURN_CODE 77)
if(WIN32)
set_tests_properties(agc_driver_array_layer_target_cpu PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
foreach(preparedMode IN ITEMS indirect uncached debug debug_failure deferred deferred_undecodable registered_workgroup registered_workgroup_sync abi_deferred_compute abi_deferred_pixel abi_deferred_stages abi_deferred_rectangle abi_deferred_undecodable_state abi_deferred_registration_failure abi_deferred_vertex_inputs abi_deferred_rectangle_pairs lazy lazy_sync)
set(preparedTest "agc_driver_prepared_shaders_${preparedMode}")
if(preparedMode MATCHES "^abi_deferred_")
@@ -2875,10 +3042,13 @@ if(BUILD_TESTING)
set_tests_properties(agc_driver_covered_centroid_state PROPERTIES TIMEOUT 30)
add_test(NAME agc_driver_sample_location_grid_fallback COMMAND agc_driver_graphics_tests --sample-location-grid-fallback)
set_tests_properties(agc_driver_sample_location_grid_fallback PROPERTIES TIMEOUT 30)
add_test(NAME agc_driver_sample_order_planning COMMAND agc_driver_graphics_tests --sample-order)
set_tests_properties(agc_driver_sample_order_planning PROPERTIES TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_compute_descriptor_layout_flags PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
set_tests_properties(agc_driver_covered_centroid_state PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
set_tests_properties(agc_driver_sample_location_grid_fallback PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
set_tests_properties(agc_driver_sample_order_planning PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_compute_pipeline_capture COMMAND agc_compute_pipeline_capture_tests)
set_tests_properties(agc_compute_pipeline_capture PROPERTIES TIMEOUT 30)
@@ -24,5 +24,6 @@ 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);
extern "C" void* AgcDriverStaticTable_nid_postfix();
#endif
@@ -3,6 +3,8 @@
#include "prx/libSceAgcDriver/Execution/include/QueueState.hpp"
#include <cstdint>
#include <stdexcept>
#include <string>
namespace AgcDriver::DriverDetail {
@@ -10,6 +12,16 @@ double TraceMs();
std::uint64_t SkippedShader();
class RefusedDraw final : public std::runtime_error {
public:
RefusedDraw(std::uint64_t address, const std::string& what) : std::runtime_error(what), address(address) {}
std::uint64_t address;
};
bool SkipRefusedDraws();
void ReportRefusedDraw(const RefusedDraw& refused);
void require(bool condition, const char* reason);
std::uint32_t readRegister(const Registers& registers, std::uint32_t offset);
@@ -1,14 +0,0 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_ROBUSTBUFFERACCESS_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_ROBUSTBUFFERACCESS_HPP
#include <vulkan/vulkan.h>
namespace AgcDriver {
inline bool RobustBufferAccess2Usable(VkDriverId driver) {
return driver != VK_DRIVER_ID_AMD_PROPRIETARY;
}
}
#endif
@@ -54,8 +54,10 @@ public:
VulkanDevice& operator=(const VulkanDevice&) = delete;
std::string DeviceName() const;
std::string DriverInfo() const;
bool SupportsImageFormat(VkFormat format, VkImageUsageFlags usage) const;
bool CalibrateClock(double& gpuNs, std::chrono::steady_clock::time_point& cpu) const;
ShaderRecompiler::SpirvTarget Target() const;
Graphics::SampleOrder HostSampleOrder(const Graphics::State& graphics) const;
std::optional<ShaderRecompiler::GeometryStageLimits> GeometryLimits() const;
std::uint32_t MaxFragmentInputComponents() const;
const std::uint32_t* WorkGroupCountLimit() const;
@@ -321,6 +323,7 @@ public:
bool DrawInputsInPlace() const;
std::optional<std::string> KnownDrawRejection(const Graphics::State& graphics, std::span<const Graphics::CompiledShader> shaders) const;
void ColorMetadataPass(const Graphics::ColorMetadataPass& pass);
void DepthCopyPass(const Graphics::DepthCopyPass& pass);
// A draw-cache hit recorded from its recipe (Graphics::DrawWithRecipe), under the mutex after
// the packet's labels; Rebuild when the recipe's device is not this one or DrawWithRecipe
// missed (nothing recorded: the caller runs Draw with the hit's stages and re-attaches).
@@ -1,7 +1,11 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include <cstdio>
#include <cstdlib>
#include <mutex>
#include <set>
#include <stdexcept>
#include <string_view>
namespace AgcDriver::DriverDetail {
@@ -15,6 +19,23 @@ std::uint64_t SkippedShader() {
return skipped;
}
bool SkipRefusedDraws() {
static const bool skip = std::getenv("APS5_SKIP_REFUSED_DRAWS") != nullptr;
return skip;
}
void ReportRefusedDraw(const RefusedDraw& refused) {
static std::mutex reportedMutex;
static std::set<std::uint64_t> reported;
{
std::lock_guard lock(reportedMutex);
if (!reported.insert(refused.address).second) return;
}
const std::string_view what = refused.what();
const auto line = what.substr(0, what.find('\n'));
std::fprintf(stderr, "[gpu] APS5_SKIP_REFUSED_DRAWS: skipping the draws that use shader 0x%llx: %.*s\n", static_cast<unsigned long long>(refused.address), static_cast<int>(line.size()), line.data());
}
void require(bool condition, const char* reason) {
if (!condition) {
throw std::runtime_error(std::string("AGC driver: ") + reason);
@@ -11,6 +11,7 @@
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
#include "Optimization/ResourceProgram.hpp"
#include <algorithm>
@@ -119,9 +120,14 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
const bool pipelineCandidate = !resolving && !fromAhead && DispatchItemsEnabled() && DrawPipeline::Active() && indirectArguments == 0 && deferredLabels().labels.empty() && !matchesCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute) && !MatchesStreamCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute);
const bool partialLastGroup = partialGroups != std::array<std::uint32_t, 3>{};
if (resolving) {
if (indirectArguments == 0 && Graphics::MatchMultisampledSingleClear(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads)) return;
if (!partialLastGroup && matchesFillKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute)) return;
if (!partialLastGroup && indirectArguments == 0 && (matchesCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute) || MatchesStreamCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute))) return;
} else {
if (indirectArguments == 0 && Graphics::TakeMultisampledSingleClear(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads)) {
pendingDispatchPhases().outcome = DispatchOutcome::FillHle;
return;
}
if (indirectArguments == 0 && (packet[4] & 0x20u) == 0) {
if (const auto fill = Graphics::MatchMemoryValueFill(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads, {packet[1], packet[2], packet[3]})) Graphics::NoteMemoryValueFill(*fill);
}
@@ -488,7 +488,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
stages.insert(stages.begin() + 1, Graphics::CompiledShader{Stage::Geometry, &results.back(), 0});
} else {
require(graphics.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST && !graphics.primitiveRestart, "a pixel shader that reads barycentrics without VK_KHR_fragment_shader_barycentric or geometry shaders needs a triangle list without primitive restart");
auto patches = ShaderRecompiler::BuildBarycentricTessellationShaders(*programResults[0], *programResults[1], localDevice->Target(), localDevice->MaxFragmentInputComponents());
auto patches = ShaderRecompiler::BuildBarycentricTessellationShaders(*programResults[0], *programResults[1], localDevice->Target(), localDevice->MaxFragmentInputComponents(), graphics.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT ? 2u : 0u);
const auto first = results.size();
results.push_back(std::move(patches.control));
results.push_back(std::move(patches.evaluation));
@@ -14,7 +14,7 @@
namespace AgcDriver::DriverDetail {
std::shared_ptr<const 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, const std::optional<ShaderRecompiler::MeshBarycentricLayout>& meshBarycentrics) {
std::shared_ptr<const 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, const std::optional<ShaderRecompiler::MeshBarycentricLayout>& meshBarycentrics) try {
PerformanceTimer timing("Shader.DrawStage");
using Stage = ShaderRecompiler::ShaderStage;
phaseTiming.Phase(DrawRowVectors);
@@ -116,6 +116,9 @@ std::shared_ptr<const ShaderRecompiler::RecompileResult> Driver::materializeDraw
stageCapture.userDataMask = mask;
phaseTiming.Phase(DrawRowRecompile);
return stageCapture.compiled;
} catch (const std::exception& error) {
if (!SkipRefusedDraws()) throw;
throw RefusedDraw(programs[i].binary.codeAddress, error.what());
}
void Driver::recordStageDataPositions(DispatchVariant& variant, const ShaderRecompiler::ResourceCapture& capture, const std::vector<Graphics::DecodeRead>& decodeReads) {
@@ -167,7 +167,11 @@ std::shared_ptr<DrawDecode> Driver::decodeDraw(const QueueState& queue, const Su
product->pixel.reversedBlend = product->state.reversedBlend;
product->pixel.alphaToCoverage = product->state.alphaToCoverage;
product->pixel.quadPixelMask = Graphics::QuadPixelMask(product->state);
product->pixel.quadSampleMask = Graphics::QuadSampleMasks(product->state);
if (product->state.sampleLocationGrid) {
const auto localDevice = device.Load();
if (localDevice != nullptr) product->pixel.sampleOrder = localDevice->HostSampleOrder(product->state);
}
product->pixel.quadSampleMask = Graphics::QuadSampleMasks(product->state, product->pixel.sampleOrder);
if (product->pixel.stencilRefExportEnable && Graphics::StencilReferenceUnused(product->state)) {
const auto localDevice = device.Load();
const auto capabilities = localDevice == nullptr ? std::vector<std::uint32_t>{} : localDevice->Target().supportedCapabilities;
@@ -82,6 +82,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr<Vulk
mix(pixel.iteratesSamples);
mix(static_cast<std::uint64_t>(pixel.positionLocation));
for (const auto location : pixel.sampleLocations) mix(location);
for (const auto order : pixel.sampleOrder) mix(order);
std::lock_guard cacheLock(drawCacheMutex);
++drawEntryCounters.lookups;
const auto found = drawCache.find(drawKey);
@@ -25,6 +25,17 @@ std::optional<DrawVerdict> Driver::precheckDraw(const QueueState& queue, const S
localDevice->ColorMetadataPass(*pass);
return DrawVerdict::Drawn;
}
if (const auto copy = Graphics::DecodeDepthCopyPass(queue)) {
require(!drawParameters.indirect, "indirect DB depth copies are unsupported");
if (DrawPipeline::Active()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, (packet[0] >> 8u) & 0xffu);
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Draw);
std::lock_guard gpuLock(GuestMemory::GpuMutex());
if (device == nullptr) device = std::make_shared<VulkanDevice>();
const std::shared_ptr<VulkanDevice> localDevice = device;
recordLabelsForPacket(localDevice.get(), submission.queue);
localDevice->DepthCopyPass(*copy);
return DrawVerdict::Drawn;
}
if (Graphics::DepthResummarizeDrawsNothing(queue)) return DrawVerdict::Nothing;
rejected = Graphics::DepthMaintenanceRejection(queue);
if (!rejected.empty()) return DrawVerdict::Rejected;
@@ -251,7 +251,7 @@ 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.conservativeRasterization != t.conservativeRasterization || s.perSampleShading != t.perSampleShading || 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.perSampleShading != t.perSampleShading || s.cullMode != t.cullMode || s.polygonMode != t.polygonMode || 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;
@@ -262,7 +262,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) {
const auto& y = b.programs[i];
if (x.binary.stage != y.binary.stage || x.binary.codeAddress != y.binary.codeAddress || x.userDataBase != y.userDataBase || x.firstUserSgpr != y.firstUserSgpr || x.userData != y.userData || x.snapshot != y.snapshot || x.codeOffset != y.codeOffset || x.tailContinuation != y.tailContinuation) return false;
}
if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.reversedBlend != q.reversedBlend || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive || p.ditheredAlphaToCoverage != q.ditheredAlphaToCoverage || p.quadPixelMask != q.quadPixelMask || p.quadSampleMask != q.quadSampleMask || p.iteratesSamples != q.iteratesSamples || p.positionLocation != q.positionLocation || p.sampleLocations != q.sampleLocations) return false;
if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.reversedBlend != q.reversedBlend || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive || p.ditheredAlphaToCoverage != q.ditheredAlphaToCoverage || p.quadPixelMask != q.quadPixelMask || p.quadSampleMask != q.quadSampleMask || p.iteratesSamples != q.iteratesSamples || p.positionLocation != q.positionLocation || p.sampleLocations != q.sampleLocations || p.sampleOrder != q.sampleOrder) return false;
if (s.samples != t.samples || s.mergedCoverage != t.mergedCoverage || s.quadSampleMask != t.quadSampleMask || s.pixelEarlyZ != t.pixelEarlyZ) return false;
return true;
}
@@ -291,3 +291,8 @@ std::unique_lock<std::mutex> HoldAsyncLog() {
}
}
extern "C" void* AgcDriverStaticTable_nid_postfix() {
static_cast<void>(AgcDriver::DriverDetail::Driver::Get());
return reinterpret_cast<void*>(AgcDriver::DriverDetail::FsTableAddress);
}
@@ -181,10 +181,11 @@ std::vector<std::pair<std::uint64_t, std::uint64_t>> QueuedDrawReadRanges(const
}
if (stages.empty() || stages.front().program == nullptr) return ranges;
for (const auto& attribute : stages.front().program->vertexAttributes) {
if (Graphics::NullVertexDescriptor(attribute) || Graphics::VertexFetchOutOfRange(attribute)) continue;
if (Graphics::NullVertexDescriptor(attribute)) continue;
const auto address = attribute.resource.fields[0] | (static_cast<std::uint64_t>(attribute.resource.fields[1] & 0xffffu) << 32u);
std::uint64_t bytes = 0;
try {
if (Graphics::VertexFetchOutOfRange(attribute)) continue;
bytes = Graphics::VertexBufferExtent(attribute);
} catch (const std::runtime_error&) {
continue;
@@ -446,6 +446,10 @@ void Driver::execute(const Submission& submission) {
} else if (traceDraws) {
std::fprintf(stderr, "[draw] target 0x%llx mask 0x%x ok\n",static_cast<unsigned long long>(color), readRegister(queue.context, 0x8e));
}
} catch (const RefusedDraw& refused) {
CaptureTrace::Log("draw-refused submission=%llu offset=%zu reason=%.256s", static_cast<unsigned long long>(submission.serial), cursor, refused.what());
countSkip(Graphics::DrawSkip::Thrown);
ReportRefusedDraw(refused);
} 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());
countSkip(Graphics::DrawSkip::Thrown);
@@ -813,18 +813,33 @@ constexpr std::size_t WritePagesPerBlock = WriteBlockBytes / WritePageBytes;
enum class StampKind : std::uint8_t { Cpu, Driver, ImportWindow, Fresh };
constexpr std::array<std::byte, WritePageBytes> ZeroPage{};
std::uint64_t HashPage(const std::byte* page) {
constexpr std::uint64_t prime1 = 0x9E3779B185EBCA87ull;
constexpr std::uint64_t prime2 = 0xC2B2AE3D27D4EB4Full;
constexpr std::uint64_t prime3 = 0x165667B19E3779F9ull;
constexpr std::uint64_t prime4 = 0x85EBCA77C2B2AE63ull;
const auto round = [](std::uint64_t accumulator, std::uint64_t word) { return std::rotl(accumulator + word * prime2, 31) * prime1; };
std::array<std::uint64_t, 4> lanes{prime1 + prime2, prime2, 0, 0 - prime1};
for (std::size_t offset = 0; offset < WritePageBytes; offset += 4 * sizeof(std::uint64_t)) {
std::array<std::uint64_t, 4> words;
std::memcpy(words.data(), page + offset, sizeof(words));
for (std::size_t lane = 0; lane < 4; ++lane) lanes[lane] = round(lanes[lane], words[lane]);
}
std::uint64_t hash = std::rotl(lanes[0], 1) + std::rotl(lanes[1], 7) + std::rotl(lanes[2], 12) + std::rotl(lanes[3], 18);
for (const auto lane : lanes) hash = (hash ^ round(0, lane)) * prime1 + prime4;
hash ^= hash >> 33;
hash *= prime2;
hash ^= hash >> 29;
hash *= prime3;
return hash ^ (hash >> 32);
}
struct WindowCopy {
static constexpr std::uint32_t Zero = std::numeric_limits<std::uint32_t>::max();
std::uint64_t first;
std::span<const std::byte> bytes;
std::span<const std::uint32_t> slots;
std::span<const std::uint64_t> hashes;
bool Holds(std::uintptr_t page) const {
const auto slot = slots[(page - first) / WritePageBytes];
const auto* expected = slot == Zero ? ZeroPage.data() : bytes.data() + static_cast<std::size_t>(slot) * WritePageBytes;
return std::memcmp(reinterpret_cast<const void*>(page), expected, WritePageBytes) == 0;
return HashPage(reinterpret_cast<const std::byte*>(page)) == hashes[(page - first) / WritePageBytes];
}
};
@@ -873,6 +888,7 @@ struct WriteTracker {
std::uintptr_t end = 0;
std::uint64_t epoch = 0;
std::uint64_t unwatched = 0;
std::uint64_t mapping = 0;
};
std::array<Memo, 256> memo{};
std::size_t nextMemo = 0;
@@ -1348,19 +1364,20 @@ std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoi
// must not differ between them.
const auto epoch = currentCollectEpoch();
const auto unwatched = unwatchSerial.load(std::memory_order_acquire);
const auto mapping = GuestAllocations::GuestAllocationsGeneration_nid_postfix();
const bool useMemo = memoized && collectMemoEnabled() && bytes != 0;
if (useMemo && !sharedCollectMemo()) {
// An entry exists only for a completed walk of an in-arena range, so the tracker is
// initialized and watched; nothing below the lock needs asking.
if (epochCollectMapEnabled()) {
const auto& map = epochCollectMap(epoch);
if (const auto found = map.ranges.find(first); found != map.ranges.end() && found->second.unwatched == unwatched && stop <= found->second.end) {
if (const auto found = map.ranges.find(first); found != map.ranges.end() && found->second.unwatched == unwatched && found->second.mapping == mapping && stop <= found->second.end) {
collectMemoHits.fetch_add(1, std::memory_order_relaxed);
return tracker.generation.load(std::memory_order_relaxed);
}
}
for (const auto& entry : threadCollectMemo.entries) {
if (entry.epoch == epoch && entry.unwatched == unwatched && entry.begin <= first && stop <= entry.end) {
if (entry.epoch == epoch && entry.unwatched == unwatched && entry.mapping == mapping && entry.begin <= first && stop <= entry.end) {
// The current generation, not the memoized one: blocks stamped since (MarkWritten, an
// overlapping collect) were written before this caller reads, and an older value would
// make every later UnchangedSince fail until the epoch ends.
@@ -1375,7 +1392,7 @@ std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoi
auto cursor = first;
if (useMemo && sharedCollectMemo()) {
for (const auto& entry : tracker.memo) {
if (entry.epoch == epoch && entry.unwatched == unwatchSerial.load(std::memory_order_relaxed) && entry.begin <= cursor && stop <= entry.end) {
if (entry.epoch == epoch && entry.unwatched == unwatchSerial.load(std::memory_order_relaxed) && entry.mapping == mapping && entry.begin <= cursor && stop <= entry.end) {
collectMemoHits.fetch_add(1, std::memory_order_relaxed);
return tracker.generation;
}
@@ -1396,12 +1413,12 @@ std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoi
// Only a completed walk is remembered; a failed one (uncommitted pages) returned 0 above.
if (collectMemoEnabled()) {
const auto serial = unwatchSerial.load(std::memory_order_relaxed);
if (sharedCollectMemo()) tracker.memo[tracker.nextMemo++ % tracker.memo.size()] = {first, stop, epoch, serial};
if (sharedCollectMemo()) tracker.memo[tracker.nextMemo++ % tracker.memo.size()] = {first, stop, epoch, serial, mapping};
else {
threadCollectMemo.entries[threadCollectMemo.next++ % threadCollectMemo.entries.size()] = {first, stop, epoch, serial};
threadCollectMemo.entries[threadCollectMemo.next++ % threadCollectMemo.entries.size()] = {first, stop, epoch, serial, mapping};
if (epochCollectMapEnabled()) {
auto& entry = epochCollectMap(epoch).ranges[first];
if (entry.unwatched != serial || entry.end < stop) entry = {first, stop, epoch, serial};
if (entry.unwatched != serial || entry.mapping != mapping || entry.end < stop) entry = {first, stop, epoch, serial, mapping};
}
}
}
@@ -1567,23 +1584,16 @@ bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function
begin = end;
}
}
std::vector<std::uint32_t> slots(static_cast<std::size_t>((stop - first) / page), WindowCopy::Zero);
std::size_t kept = 0;
std::vector<std::uint64_t> hashes(static_cast<std::size_t>((stop - first) / page), 0);
for (const auto& [begin, end] : spans) {
CheckRange(reinterpret_cast<const void*>(begin), static_cast<std::size_t>(end - begin), 1, false);
for (auto cursor = begin; cursor < end; cursor += page) {
if (std::memcmp(reinterpret_cast<const void*>(cursor), ZeroPage.data(), page) != 0) slots[static_cast<std::size_t>((cursor - first) / page)] = static_cast<std::uint32_t>(kept++);
hashes[static_cast<std::size_t>((cursor - first) / page)] = HashPage(reinterpret_cast<const std::byte*>(cursor));
}
}
std::vector<std::byte> saved;
saved.reserve(kept * page);
const auto* source = reinterpret_cast<const std::byte*>(first);
for (std::size_t index = 0; index < slots.size(); ++index) {
if (slots[index] != WindowCopy::Zero) saved.insert(saved.end(), source + index * page, source + (index + 1) * page);
}
std::erase_if(spans, [&](const auto& span) { return !walkWrites(tracker, span.first, span.second, StampKind::Cpu, true); });
if (!runImport()) return false;
const WindowCopy window{first, saved, slots};
const WindowCopy window{first, hashes};
auto unwatchedFrom = first;
for (const auto& [begin, end] : spans) {
if (!walkWrites(tracker, begin, end, StampKind::Cpu, true, &window)) continue;
@@ -1827,15 +1837,16 @@ std::uint64_t UntrackedHint(std::uint64_t address, std::size_t bytes) {
tracker.initialize();
if (!tracker.watched || !tracker.hinted(first, stop)) return 0;
const auto unwatched = unwatchSerial.load(std::memory_order_relaxed);
const auto mapping = GuestAllocations::GuestAllocationsGeneration_nid_postfix();
const bool memoized = collectMemoEnabled() && std::any_of(threadHintMemo.entries.begin(), threadHintMemo.entries.end(), [&](const WriteTracker::Memo& entry) {
return entry.epoch == epoch && entry.unwatched == unwatched && entry.begin <= first && stop <= entry.end;
return entry.epoch == epoch && entry.unwatched == unwatched && entry.mapping == mapping && entry.begin <= first && stop <= entry.end;
});
if (memoized) {
collectMemoHits.fetch_add(1, std::memory_order_relaxed);
} else {
const TimedAccess timed(CounterCollect, bytes);
if (!walkHints(tracker, first, stop)) return 0;
if (collectMemoEnabled()) threadHintMemo.entries[threadHintMemo.next++ % threadHintMemo.entries.size()] = {first, stop, epoch, unwatched};
if (collectMemoEnabled()) threadHintMemo.entries[threadHintMemo.next++ % threadHintMemo.entries.size()] = {first, stop, epoch, unwatched, mapping};
}
return tracker.newestHint(first, stop);
#else
@@ -470,6 +470,8 @@ void Validate(std::span<const std::uint32_t> packet, std::uint32_t queue) {
case 0x49: {
size(8);
const auto dataSelect = packet[2] >> 29u;
require(dataSelect != 4, "RELEASE_MEM of the 64-bit system counter (DATA_SEL 4) is not implemented");
require(dataSelect <= 5, "reserved RELEASE_MEM DATA_SEL");
require(dataSelect <= 3, "GDS RELEASE_MEM data is not implemented");
require(dataSelect == 0 || address(packet[3], packet[4]) == 0 || (packet[3] & (dataSelect == 1 ? 3u : 7u)) == 0, "misaligned RELEASE_MEM destination");
break;
@@ -18,7 +18,6 @@
#include "prx/libSceAgcDriver/Execution/include/Float32FlushControl.hpp"
#include "prx/libSceAgcDriver/Execution/include/PipelineLinkOptimization.hpp"
#include "prx/libSceAgcDriver/Execution/include/DrawInputsInPlace.hpp"
#include "prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp"
#include "prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp"
#include "prx/libSceAgcDriver/Execution/include/SubgroupClock.hpp"
#include "prx/libSceAgcDriver/Execution/include/ShaderDeviceProfile.hpp"
@@ -34,6 +33,7 @@
#include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp"
#include "prx/libSceAgcDriver/Execution/include/HostHdr.hpp"
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp"
#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp"
#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp"
@@ -66,6 +66,7 @@
#include <array>
#include <optional>
#include <algorithm>
#include <bit>
#include <cstring>
#include <deque>
#include <limits>
@@ -314,6 +315,7 @@ struct VulkanDevice::State {
bool geometryShader = false;
bool sampleRateShading = false;
bool storageImageMultisample = false;
bool storageImageWriteWithoutFormat = false;
bool shaderClipDistance = false;
bool shaderCullDistance = false;
bool shaderClock = false;
@@ -347,11 +349,15 @@ struct VulkanDevice::State {
bool imageViewMinLod = false;
bool pipelineExecutableInfo = false;
bool maintenance8 = false;
bool maintenance5 = false;
bool fillModeNonSolid = false;
std::uint32_t srgbDecodeFormats = 0;
std::uint32_t srgbCopyFormats = 0;
bool storageImageWithoutFormat = false;
VkSampleCountFlags sampleLocationSampleCounts = 0;
VkSampleCountFlags sampleLocationGridSampleCounts = 0;
std::array<VkExtent2D, 4> sampleLocationGridSizes{};
bool shaderStencilExport = false;
bool depthClamp = false;
bool depthBounds = false;
bool depthBiasClamp = false;
@@ -777,6 +783,7 @@ struct VulkanDevice::State {
Graphics::ClearCachedPipelines(device);
Graphics::ClearDepthSurfaces(device);
Graphics::ClearMultisampleTargets(device);
Graphics::ClearSampleOrders(device);
{
std::lock_guard pipelines(computePipelinesMutex);
computePipelines.clear();
@@ -1268,6 +1275,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
deviceExtensions.push_back(VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME);
state->capabilities.push_back(spv::CapabilityStencilExportEXT);
state->spirvExtensions.push_back("SPV_EXT_shader_stencil_export");
state->shaderStencilExport = true;
}
if (state->shaderClock) {
deviceExtensions.push_back(VK_KHR_SHADER_CLOCK_EXTENSION_NAME);
@@ -1350,13 +1358,18 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
state->InstanceFunction<PFN_vkGetPhysicalDeviceProperties2>("vkGetPhysicalDeviceProperties2")(selected, &properties);
if (locations.sampleLocationSubPixelBits >= 4 && locations.sampleLocationCoordinateRange[0] <= 0.0f && locations.sampleLocationCoordinateRange[1] >= 15.0f / 16.0f) {
const auto multisampleProperties = state->InstanceFunction<PFN_vkGetPhysicalDeviceMultisamplePropertiesEXT>("vkGetPhysicalDeviceMultisamplePropertiesEXT");
std::string grids;
for (const auto samples : {VK_SAMPLE_COUNT_1_BIT, VK_SAMPLE_COUNT_2_BIT, VK_SAMPLE_COUNT_4_BIT, VK_SAMPLE_COUNT_8_BIT}) {
if ((locations.sampleLocationSampleCounts & samples) == 0) continue;
VkMultisamplePropertiesEXT multisample{VK_STRUCTURE_TYPE_MULTISAMPLE_PROPERTIES_EXT};
multisampleProperties(selected, samples, &multisample);
if (multisample.maxSampleLocationGridSize.width != 0 && multisample.maxSampleLocationGridSize.height != 0) state->sampleLocationSampleCounts |= samples;
if (multisample.maxSampleLocationGridSize.width % 2u == 0 && multisample.maxSampleLocationGridSize.height % 2u == 0) state->sampleLocationGridSampleCounts |= samples;
const auto grid = multisample.maxSampleLocationGridSize;
state->sampleLocationGridSizes[static_cast<std::size_t>(std::countr_zero(static_cast<std::uint32_t>(samples)))] = grid;
if (grid.width != 0 && grid.height != 0) state->sampleLocationSampleCounts |= samples;
if (AgcDriver::Graphics::HostsSampleLocationGrid(grid)) state->sampleLocationGridSampleCounts |= samples;
grids += (grids.empty() ? "" : ", ") + std::to_string(static_cast<std::uint32_t>(samples)) + " samples " + std::to_string(grid.width) + "x" + std::to_string(grid.height);
}
if (!grids.empty()) std::fprintf(stderr, "[gpu] largest sample location grids (vkGetPhysicalDeviceMultisamplePropertiesEXT): %s; maxSampleLocationGridSize for every count %ux%u\n", grids.c_str(), locations.maxSampleLocationGridSize.width, locations.maxSampleLocationGridSize.height);
}
if (state->sampleLocationSampleCounts != 0) deviceExtensions.push_back(VK_EXT_SAMPLE_LOCATIONS_EXTENSION_NAME);
}
@@ -1526,6 +1539,15 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
state->pipelineLinkTimeOptimization = PipelineLinkTimeOptimizationUsable(driver.driverID);
}
}
VkPhysicalDeviceMaintenance5FeaturesKHR maintenance5Features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_5_FEATURES_KHR};
if (state->graphicsPipelineLibrary && hasExtension(VK_KHR_MAINTENANCE_5_EXTENSION_NAME)) {
VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &maintenance5Features};
state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2")(selected, &features);
state->maintenance5 = maintenance5Features.maintenance5 == VK_TRUE;
if (state->maintenance5) deviceExtensions.push_back(VK_KHR_MAINTENANCE_5_EXTENSION_NAME);
}
maintenance5Features = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_5_FEATURES_KHR};
maintenance5Features.maintenance5 = VK_TRUE;
renderingFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES_KHR, nullptr, VK_TRUE};
libraryFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_GRAPHICS_PIPELINE_LIBRARY_FEATURES_EXT, &dynamicStateFeatures, VK_TRUE};
dynamicStateFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_FEATURES_EXT, &renderingFeatures, VK_TRUE};
@@ -1655,6 +1677,8 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
// PA_CL_CLIP_CNTL near/far clip disable maps to depth clamping.
enabled.depthClamp = available.depthClamp;
state->depthClamp = enabled.depthClamp == VK_TRUE;
enabled.fillModeNonSolid = available.fillModeNonSolid;
state->fillModeNonSolid = enabled.fillModeNonSolid == VK_TRUE;
enabled.depthBounds = available.depthBounds;
state->depthBounds = enabled.depthBounds == VK_TRUE;
enabled.depthBiasClamp = available.depthBiasClamp;
@@ -1669,6 +1693,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
if (enabled.shaderStorageImageMultisample) state->capabilities.push_back(spv::CapabilityStorageImageMultisample);
state->capabilities.push_back(spv::CapabilityImageMSArray);
enabled.shaderStorageImageWriteWithoutFormat = available.shaderStorageImageWriteWithoutFormat;
state->storageImageWriteWithoutFormat = enabled.shaderStorageImageWriteWithoutFormat == VK_TRUE;
enabled.shaderStorageImageReadWithoutFormat = available.shaderStorageImageReadWithoutFormat;
// Gathers with non-constant offsets (ImageGatherExtended).
enabled.shaderImageGatherExtended = available.shaderImageGatherExtended;
@@ -1766,6 +1791,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
fifoLatestReadyFeatures.pNext = const_cast<void*>(deviceInfo.pNext);
deviceInfo.pNext = &fifoLatestReadyFeatures;
}
if (state->maintenance5) {
maintenance5Features.pNext = const_cast<void*>(deviceInfo.pNext);
deviceInfo.pNext = &maintenance5Features;
}
if (state->maintenance8) {
maintenance8Features.pNext = const_cast<void*>(deviceInfo.pNext);
deviceInfo.pNext = &maintenance8Features;
@@ -1915,14 +1944,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
VkPhysicalDeviceFeatures2 queried{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &robustness2};
state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2")(selected, &queried);
}
VkDriverId robustnessDriver = VK_DRIVER_ID_MAX_ENUM;
if (hasExtension(VK_KHR_DRIVER_PROPERTIES_EXTENSION_NAME)) {
VkPhysicalDeviceDriverProperties driver{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES};
VkPhysicalDeviceProperties2 driverProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &driver};
state->InstanceFunction<PFN_vkGetPhysicalDeviceProperties2>("vkGetPhysicalDeviceProperties2")(selected, &driverProperties);
robustnessDriver = driver.driverID;
}
state->robustBufferAccess2 = robustness2.robustBufferAccess2 == VK_TRUE && enabled.robustBufferAccess == VK_TRUE && RobustBufferAccess2Usable(robustnessDriver);
state->robustBufferAccess2 = robustness2.robustBufferAccess2 == VK_TRUE && enabled.robustBufferAccess == VK_TRUE;
if (robustness2.nullDescriptor == VK_TRUE || state->robustBufferAccess2) {
const bool nullDescriptor = robustness2.nullDescriptor == VK_TRUE;
robustness2 = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT};
@@ -3192,8 +3214,9 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) {
static const bool noAdopt = std::getenv("APS5_NO_PRESENT_EPOCH") != nullptr;
if (!noAdopt) GuestMemory::AdoptLatestCollectEpoch();
GuestMemory::CollectWritesUncached(buffer.address, bytes);
if (!GuestMemory::UnchangedSince(buffer.address, bytes, resident->Generation()) || (buffer.dccAddress != 0 && ResidentKeysMoved(*resident))) {
resident->Refresh();
if (!GuestMemory::UnchangedSince(buffer.address, bytes, resident->GenerationFor(buffer.address, bytes)) || (buffer.dccAddress != 0 && ResidentKeysMoved(*resident))) {
if (resident->ImageLayers() > 1) resident->RefreshLayers(layer, 1);
else resident->Refresh();
++refreshedPresents;
}
++residentPresents;
@@ -3643,6 +3666,12 @@ std::uint32_t VulkanDevice::VendorId() const {
return state->properties.vendorID;
}
bool VulkanDevice::SupportsImageFormat(VkFormat format, VkImageUsageFlags usage) const {
VkImageFormatProperties properties{};
const auto query = state->InstanceFunction<PFN_vkGetPhysicalDeviceImageFormatProperties>("vkGetPhysicalDeviceImageFormatProperties");
return query(state->physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, 0, &properties) == VK_SUCCESS;
}
ShaderRecompiler::SpirvTarget VulkanDevice::Target() const {
require(state->shaderProfile != nullptr, "shader device profile is unavailable");
auto target = state->shaderProfile->Target();
@@ -3651,6 +3680,11 @@ ShaderRecompiler::SpirvTarget VulkanDevice::Target() const {
return target;
}
Graphics::SampleOrder VulkanDevice::HostSampleOrder(const Graphics::State& graphics) const {
if (!graphics.sampleLocationGrid) return {};
return Graphics::HostSampleLocationsFor(graphicsContext(), graphics).order;
}
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, {}, {}};
@@ -3797,7 +3831,10 @@ Graphics::Context VulkanDevice::buildContext() const {
context.samplerTableUpdateAfterBind = SamplerTableUpdateAfterBind(context);
context.geometryShader = state->geometryShader;
context.sampleRateShading = state->sampleRateShading;
context.fillModeNonSolid = state->fillModeNonSolid;
context.maintenance5 = state->maintenance5;
context.storageImageMultisample = state->storageImageMultisample;
context.storageImageWriteWithoutFormat = state->storageImageWriteWithoutFormat;
context.shaderClipDistance = state->shaderClipDistance;
context.shaderCullDistance = state->shaderCullDistance;
context.shaderTerminateInvocation = state->shaderTerminateInvocation;
@@ -3814,7 +3851,9 @@ Graphics::Context VulkanDevice::buildContext() const {
context.srgbCopyFormats = state->srgbCopyFormats;
context.sampleLocationSampleCounts = state->sampleLocationSampleCounts;
context.sampleLocationGridSampleCounts = state->sampleLocationGridSampleCounts;
context.memoryProperties2 = state->memoryBudget ? state->InstanceFunction<PFN_vkGetPhysicalDeviceMemoryProperties2>("vkGetPhysicalDeviceMemoryProperties2") : nullptr;
context.sampleLocationGridSizes = state->sampleLocationGridSizes;
context.shaderStencilExport = state->shaderStencilExport;
context.memoryProperties2 =state->memoryBudget ? state->InstanceFunction<PFN_vkGetPhysicalDeviceMemoryProperties2>("vkGetPhysicalDeviceMemoryProperties2") : nullptr;
context.singlePassStorage = state->storageImageWithoutFormat;
context.cacheReclaim = Graphics::ReleaseTextureCaches;
return context;
@@ -3828,6 +3867,10 @@ std::optional<std::string> VulkanDevice::KnownDrawRejection(const Graphics::Stat
return Graphics::KnownValidationFailure(graphicsContext(), shaders, graphics);
}
void VulkanDevice::DepthCopyPass(const Graphics::DepthCopyPass& pass) {
Graphics::RunDepthCopyPass(graphicsContext(), pass);
}
void VulkanDevice::ColorMetadataPass(const Graphics::ColorMetadataPass& pass) {
Graphics::RunColorMetadataPass(graphicsContext(), pass);
}
@@ -4704,6 +4747,14 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) {
auto& resources = *record.resources;
const auto arguments = record.arguments;
const auto* argumentImport = record.argumentImport;
const auto* limit = state->properties.limits.maxComputeWorkGroupCount;
if (argumentImport == nullptr && record.shader->program->workgroupMemoryDwords != 0 && (record.x > limit[0] || record.y > limit[1] || record.z > limit[2])) {
char counted[128] = "";
if (record.cpuArguments != 0) std::snprintf(counted, sizeof(counted), " (read on the CPU from the indirect arguments at 0x%llx, cpuReason %d)", static_cast<unsigned long long>(record.cpuArguments), record.cpuReason);
char text[384];
std::snprintf(text, sizeof(text), "Vulkan dispatch: workgroup count %ux%ux%u%s exceeds device limits %ux%ux%u and a split aliases the device-memory LDS slots, whose base is computed from gl_NumWorkGroups of a single chunk", record.x, record.y, record.z, counted, limit[0], limit[1], limit[2]);
throw std::runtime_error(text);
}
// The record phase split (APS5_PROFILE_DRAW, rows "record: ..." of the [dispatch] totals):
// opening the batch, the keeps, the data refresh, the barriers with the bind and the dispatch
// itself, the pending-write notes and marks (MarkGpuWrites), and the completion registration.
@@ -4838,17 +4889,11 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) {
if (argumentImport != nullptr) {
context.Resolved(&Graphics::DeviceFunctions::cmdDispatchIndirect, "vkCmdDispatchIndirect")(commands, indirectBuffer, indirectOffset);
} else {
const auto* limit = state->properties.limits.maxComputeWorkGroupCount;
if (record.x <= limit[0] && record.y <= limit[1] && record.z <= limit[2]) {
context.Resolved(&Graphics::DeviceFunctions::cmdDispatch, "vkCmdDispatch")(commands, record.x, record.y, record.z);
} else {
char counted[128] = "";
if (record.cpuArguments != 0) std::snprintf(counted, sizeof(counted), " (read on the CPU from the indirect arguments at 0x%llx, cpuReason %d)", static_cast<unsigned long long>(record.cpuArguments), record.cpuReason);
if (record.shader->program->workgroupMemoryDwords != 0) {
char text[384];
std::snprintf(text, sizeof(text), "Vulkan dispatch: workgroup count %ux%ux%u%s exceeds device limits %ux%ux%u and a split aliases the device-memory LDS slots, whose base is computed from gl_NumWorkGroups of a single chunk", record.x, record.y, record.z, counted, limit[0], limit[1], limit[2]);
throw std::runtime_error(text);
}
const auto offsets = std::ranges::count_if(record.shader->program->specialization, [](const auto& constant) { return ShaderRecompiler::PipelineSpecialization::IsWorkgroupOffset(constant.id); });
if (offsets != ShaderRecompiler::PipelineSpecialization::WorkgroupOffsetWords) {
char text[320];
@@ -6,6 +6,7 @@
#endif
#include <mutex>
#include <vulkan/vulkan.h>
#include <array>
#include <atomic>
#include <cstdint>
#include <cstdio>
@@ -237,7 +238,10 @@ struct Context {
bool samplerTableUpdateAfterBind = false;
bool geometryShader = false;
bool sampleRateShading = false;
bool fillModeNonSolid = false;
bool maintenance5 = false;
bool storageImageMultisample = false;
bool storageImageWriteWithoutFormat = false;
bool shaderClipDistance = false;
bool shaderCullDistance = false;
bool nullDescriptors = false;
@@ -258,6 +262,8 @@ struct Context {
bool singlePassStorage = false;
VkSampleCountFlags sampleLocationSampleCounts = 0;
VkSampleCountFlags sampleLocationGridSampleCounts = 0;
std::array<VkExtent2D, 4> sampleLocationGridSizes{};
bool shaderStencilExport = false;
PipelineCacheSynchronization* pipelineCacheSynchronization = nullptr;
bool shaderTerminateInvocation = false;
@@ -2,6 +2,7 @@
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_DEPTHSURFACE_HPP
#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp"
#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp"
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
#include <array>
#include <cstdint>
@@ -17,8 +18,17 @@ class Texture;
class StorageTexture;
VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target, bool depthWritten = true);
std::vector<std::pair<std::uint64_t, std::uint64_t>> FlushDepthPlanes(std::uint64_t address, std::size_t bytes);
SampleOrderTarget DepthSurfaceOrderTarget(const Context& context, const DepthTarget& target);
DepthSampleLocations DepthSurfaceSampleLocations(const Context& context, const DepthTarget& target);
enum class DepthPlaneScope { Overlapped, Whole };
std::vector<std::pair<std::uint64_t, std::uint64_t>> FlushDepthPlanes(std::uint64_t address, std::size_t bytes, DepthPlaneScope scope = DepthPlaneScope::Overlapped);
bool DepthPlanesStale();
struct DepthPlaneStoreStatistics {
std::uint64_t stores = 0;
std::uint64_t units = 0;
std::uint64_t bytes = 0;
};
DepthPlaneStoreStatistics DepthPlaneStoreCounts();
bool SampleLocationsCompatibleDepth(const Context& context, VkFormat format, std::uint32_t samples);
std::uint64_t DepthSurfacesSerial();
std::uint64_t DepthSurfacesReleased();
@@ -62,6 +72,7 @@ VkFormat HostDepthFormat(const Context& context, VkFormat format);
float HostDepthBiasConstant(const Context& context, VkFormat format, float constant);
std::shared_ptr<StorageTexture> DepthSurfaceStorage(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, std::uint32_t mip);
bool DepthSurfaceStoreBack(const Context& context, const StorageTexture& storage);
void CopyDepthSample(const Context& context, std::uint64_t depthAddress, std::uint32_t samples, std::uint32_t sample, StorageTexture& destination, VkImageView destinationView, VkExtent2D extent);
struct StorageImageClear {
std::array<std::uint32_t, 8> image{};
@@ -127,6 +127,7 @@ void CountDrawSkip(DrawSkip kind, double us);
std::shared_ptr<std::vector<std::shared_ptr<ShaderResources>>> DrawCopiedWriters();
void RunColorMetadataPass(const Context& context, const ColorMetadataPass& pass);
void RunDepthCopyPass(const Context& context, const DepthCopyPass& pass);
}
@@ -114,6 +114,7 @@ void SetHostImportDeferredSettles(bool enabled);
// that heap: the other half stays for the buffers, mirrors and textures that share it.
std::uint64_t HostImportBudget(const VkPhysicalDeviceMemoryProperties& memory, std::uint64_t physicalBytes);
std::uint64_t HostImportLiveBytes();
std::uint64_t HostImportBudgetRemaining(const Context& context);
struct HostImportSettleCounts {
std::uint64_t submitted = 0;
std::uint64_t waited = 0;
@@ -0,0 +1,53 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_MULTISAMPLESWIZZLEEQUATIONS_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_MULTISAMPLESWIZZLEEQUATIONS_HPP
#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp"
#include <array>
#include <cstdint>
namespace AgcDriver::Graphics {
struct MultisampleSwizzleEquation {
TextureTileMode tileMode;
std::uint32_t elementBytes;
std::uint32_t fragments;
std::array<std::uint64_t, 16> bits;
};
inline constexpr MultisampleSwizzleEquation kMultisampleSwizzleEquations[] = {
{TextureTileMode::kZ64KBX, 1u, 2u, {0x0001000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000000000004ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000100000ull, 0x0000000000400000ull, 0x0000000000000040ull, 0x0000000000800000ull}},
{TextureTileMode::kZ64KBX, 2u, 2u, {0x0000000000000000ull, 0x0001000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000000000004ull, 0x0000000000040000ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0000000000400000ull, 0x0000000000000040ull}},
{TextureTileMode::kZ64KBX, 4u, 2u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0001000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000000000004ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0000000000400000ull, 0x0000000000840000ull}},
{TextureTileMode::kZ64KBX, 1u, 4u, {0x0001000000000000ull, 0x0002000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000000000004ull, 0x0000000000040000ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0000000000400000ull, 0x0000000000000040ull}},
{TextureTileMode::kZ64KBX, 2u, 4u, {0x0000000000000000ull, 0x0001000000000000ull, 0x0002000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000000000004ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000000008ull, 0x0000000000100000ull, 0x0000000000000040ull, 0x0000000000440000ull}},
{TextureTileMode::kZ64KBX, 4u, 4u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0001000000000000ull, 0x0002000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0000000000440000ull, 0x0000000000800004ull}},
{TextureTileMode::kZ64KBX, 1u, 8u, {0x0001000000000000ull, 0x0002000000000000ull, 0x0004000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000000000004ull, 0x0000000400080008ull, 0x0000000200100010ull, 0x0000000100200040ull, 0x0000000800400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0000000000400000ull, 0x0000000000840000ull}},
{TextureTileMode::kZ64KBX, 2u, 8u, {0x0000000000000000ull, 0x0001000000000000ull, 0x0002000000000000ull, 0x0004000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000400080008ull, 0x0000000200100010ull, 0x0000000100200040ull, 0x0000000800400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0000000000440000ull, 0x0000000000800004ull}},
{TextureTileMode::kZ64KBX, 4u, 8u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0001000000000000ull, 0x0002000000000000ull, 0x0004000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000400080008ull, 0x0000000200100010ull, 0x0000000100200040ull, 0x0000000000440020ull, 0x0000000000000008ull, 0x0000000000100000ull, 0x0000000000400004ull, 0x0000000000820000ull}},
{TextureTileMode::kR64KBX, 1u, 2u, {0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000020000ull, 0x0000000000010000ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000000100000ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000400000ull, 0x0000000000000040ull, 0x0000000000800000ull, 0x0001000001000000ull}},
{TextureTileMode::kR64KBX, 2u, 2u, {0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000010000ull, 0x0000000000020000ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000100000ull, 0x0000000000000040ull, 0x0000000000400000ull, 0x0001000000800000ull}},
{TextureTileMode::kR64KBX, 4u, 2u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000010000ull, 0x0000000000020000ull, 0x0000000000040000ull, 0x0000000000000004ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0000000000400000ull, 0x0001000000800000ull}},
{TextureTileMode::kR64KBX, 8u, 2u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000020000ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000000100000ull, 0x0001000000400000ull}},
{TextureTileMode::kR64KBX, 16u, 2u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000400080008ull, 0x0000000200100010ull, 0x0000000100200040ull, 0x0001000000400020ull, 0x0000000000040000ull, 0x0000000000000004ull, 0x0000000000080000ull, 0x0000000000000010ull}},
{TextureTileMode::kR64KBX, 1u, 4u, {0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000020000ull, 0x0000000000010000ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000000100000ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000400000ull, 0x0000000000000040ull, 0x0001000000800000ull, 0x0002000001000000ull}},
{TextureTileMode::kR64KBX, 2u, 4u, {0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000010000ull, 0x0000000000020000ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000100000ull, 0x0000000000000040ull, 0x0001000000400000ull, 0x0002000000800000ull}},
{TextureTileMode::kR64KBX, 4u, 4u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000010000ull, 0x0000000000020000ull, 0x0000000000040000ull, 0x0000000000000004ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0001000000400000ull, 0x0002000000800000ull}},
{TextureTileMode::kR64KBX, 8u, 4u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000020000ull, 0x0000000400080008ull, 0x0000000200100010ull, 0x0002000000200040ull, 0x0000000100400020ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000000100000ull, 0x0001000000400000ull}},
{TextureTileMode::kR64KBX, 16u, 4u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000200080008ull, 0x0000000100100010ull, 0x0002000000200040ull, 0x0001000000400020ull, 0x0000000000040000ull, 0x0000000000000004ull, 0x0000000000080000ull, 0x0000000000000010ull}},
{TextureTileMode::kR64KBX, 1u, 8u, {0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000020000ull, 0x0000000000010000ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000000100000ull, 0x0000000400080008ull, 0x0000000200100010ull, 0x0000000100200040ull, 0x0000000800400020ull, 0x0000000000400000ull, 0x0001000000800000ull, 0x0002000001000000ull, 0x0004000002000000ull}},
{TextureTileMode::kR64KBX, 2u, 8u, {0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000010000ull, 0x0000000000020000ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000400080008ull, 0x0000000200100010ull, 0x0000000100200040ull, 0x0000000800400020ull, 0x0000000000100000ull, 0x0001000000400000ull, 0x0002000000800000ull, 0x0004000001000000ull}},
{TextureTileMode::kR64KBX, 4u, 8u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000010000ull, 0x0000000000020000ull, 0x0000000000040000ull, 0x0000000000000004ull, 0x0000000400080008ull, 0x0000000200100010ull, 0x0000000100200040ull, 0x0004000000400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0001000000400000ull, 0x0002000000800000ull}},
{TextureTileMode::kR64KBX, 8u, 8u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000020000ull, 0x0000000200080008ull, 0x0000000100100010ull, 0x0004000000200040ull, 0x0002000000400020ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000000100000ull, 0x0001000000400000ull}},
{TextureTileMode::kR64KBX, 16u, 8u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000200080008ull, 0x0000000100100010ull, 0x0004000000200040ull, 0x0002000000400020ull, 0x0000000000000004ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0001000000400000ull}},
};
constexpr const MultisampleSwizzleEquation* FindMultisampleSwizzleEquation(TextureTileMode tileMode, std::uint32_t elementBytes, std::uint32_t fragments) {
for (const auto& equation : kMultisampleSwizzleEquations) {
if (equation.tileMode == tileMode && equation.elementBytes == elementBytes && equation.fragments == fragments) return &equation;
}
return nullptr;
}
}
#endif
@@ -3,7 +3,9 @@
#include "prx/libSceAgcDriver/Graphics/include/Context.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp"
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
#include <array>
#include <cstddef>
#include <cstdint>
#include <memory>
@@ -55,7 +57,15 @@ std::uint64_t MultisampledColorBytes(const ColorTarget& target);
std::size_t MultisampledDccBytes(const ColorTarget& target);
VkClearColorValue MultisampledDccClear(const ColorTarget& target, std::span<const std::byte> dcc, std::span<const std::byte> cmask);
bool MultisampledFillClearColor(const ColorTarget& target, std::uint32_t fill, VkClearColorValue& clear);
struct MultisampledSingleClear {
std::uint64_t descriptor = 0;
std::array<std::uint32_t, 4> color{};
};
std::optional<MultisampledSingleClear> MatchMultisampledSingleClear(std::span<const std::uint32_t> code, std::span<const std::uint32_t> userData, const std::array<std::uint32_t, 3>& numThreads);
void NoteMultisampledSingleClear(const GuestTextureResource& resource, const std::array<std::uint32_t, 4>& color);
bool TakeMultisampledSingleClear(std::span<const std::uint32_t> code, std::span<const std::uint32_t> userData, const std::array<std::uint32_t, 3>& numThreads);
bool TakeMultisampledClear(MultisampledCmask& cmask, MultisampledColorFill& colorFill, const ColorTarget& use, VkClearColorValue& clear);
SampleOrderTarget MultisampleTargetOrderTarget(const Context& context, const ColorTarget& target);
std::shared_ptr<Texture> MultisampleTargetTexture(const Context& context, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D);
VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target, VkImage* image = nullptr);
VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target, std::shared_ptr<void>& owner, VkImage* image = nullptr);
@@ -0,0 +1,54 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_MULTISAMPLETEXTURELAYOUT_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_MULTISAMPLETEXTURELAYOUT_HPP
#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp"
#include <array>
#include <cstddef>
#include <cstdint>
#include <span>
#include <vector>
namespace AgcDriver::Graphics {
class MultisampleTextureLayout {
public:
static constexpr std::uint32_t Alignment = 65536u;
MultisampleTextureLayout(TextureTileMode tileMode, std::uint32_t elementBytes, std::uint32_t fragments, std::uint32_t width, std::uint32_t height, std::uint32_t layers, std::uint32_t pipeBankXor = 0);
std::uint64_t Bytes() const { return sliceBytes * layers; }
std::uint64_t SliceBytes() const { return sliceBytes; }
std::uint64_t LinearBytes() const { return static_cast<std::uint64_t>(width) * height * layers * fragments * elementBytes; }
std::uint32_t Pitch() const { return pitch; }
std::uint32_t PaddedHeight() const { return paddedHeight; }
std::uint32_t BlockWidth() const { return blockWidth; }
std::uint32_t BlockHeight() const { return blockHeight; }
const std::array<std::uint32_t, 16>& InverseBlockEquation() const { return inverseBlockEquation; }
std::uint64_t Offset(std::uint32_t x, std::uint32_t y, std::uint32_t layer, std::uint32_t fragment) const;
void Detile(std::span<const std::byte> source, std::span<std::byte> destination) const;
void Tile(std::span<const std::byte> source, std::span<std::byte> destination) const;
private:
std::uint64_t offset(std::uint32_t x, std::uint32_t y, std::uint32_t layer, std::uint32_t fragment) const;
void transfer(std::span<const std::byte> source, std::span<std::byte> destination, bool retile) const;
std::uint32_t elementBytes;
std::uint32_t fragments;
std::uint32_t width;
std::uint32_t height;
std::uint32_t layers;
std::uint32_t pipeBankXor;
std::uint32_t blockWidth;
std::uint32_t blockHeight;
std::uint32_t pitch;
std::uint32_t paddedHeight;
std::uint64_t sliceBytes;
std::array<std::uint32_t, 16> inverseBlockEquation;
std::vector<std::uint32_t> xOffsets;
std::vector<std::uint32_t> yOffsets;
std::vector<std::uint32_t> layerOffsets;
std::vector<std::uint32_t> fragmentOffsets;
};
}
#endif
@@ -58,6 +58,10 @@ public:
void ContinueBackFaces(VkCommandBuffer commands, const State& state) const;
bool SplitsSecondChannel() const { return secondChannel != VK_NULL_HANDLE; }
void ContinueSecondChannel(VkCommandBuffer commands) const;
const SampleOrder& HostSampleOrder() const { return sampleOrder; }
bool ReordersSamples() const { return Graphics::ReordersSamples(sampleOrder); }
bool SampleLocationsEnabled() const { return sampleLocationsEnabled; }
VkExtent2D HostSampleLocationGrid() const { return sampleLocationGrid; }
void PushConstants(VkCommandBuffer commands, VkShaderStageFlags stages, std::span<const std::byte, PipelinePushConstantBytes> bytes) const;
// Forgets the Vulkan objects without destroying them: for entries of a device that is already gone.
void Abandon() noexcept;
@@ -93,6 +97,7 @@ private:
std::vector<std::uint32_t> colorSlots;
bool sampleLocationsEnabled = false;
VkExtent2D sampleLocationGrid{1, 1};
SampleOrder sampleOrder{};
std::vector<CachedFramebuffer> framebuffers;
};
@@ -0,0 +1,74 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_SAMPLEORDER_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_SAMPLEORDER_HPP
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
#include <array>
#include <cstdint>
#include <memory>
#include <span>
#include <string>
#include <vector>
namespace AgcDriver::Graphics {
struct HostSampleLocations {
VkExtent2D grid{1, 1};
SampleOrder order{};
};
enum class SampleOrderMethod : std::uint8_t {
Storage,
Depth,
DepthStencilExport,
DepthStencilBits
};
struct SampleOrderStep {
std::uint32_t attachment = 0;
SampleOrderMethod method = SampleOrderMethod::Storage;
bool toHost = false;
bool toGuest = false;
};
struct SampleOrderPass {
SampleOrder order{};
VkExtent2D extent{};
std::array<VkSampleLocationEXT, 32> locations{};
bool located = false;
};
struct SampleOrderTarget {
VkImage image = VK_NULL_HANDLE;
VkImageView view = VK_NULL_HANDLE;
VkFormat format = VK_FORMAT_UNDEFINED;
VkExtent2D extent{};
std::uint32_t samples = 1;
SampleOrderMethod method = SampleOrderMethod::Storage;
std::shared_ptr<void> keep;
};
struct DepthSampleLocations {
VkImage image = VK_NULL_HANDLE;
VkFormat format = VK_FORMAT_UNDEFINED;
std::uint32_t samples = 1;
VkExtent2D grid{1, 1};
std::array<VkSampleLocationEXT, 32> locations{};
std::shared_ptr<void> keep;
};
HostSampleLocations HostSampleLocationsFor(const Context& context, const State& state);
bool HostsSampleLocationGrid(VkExtent2D largestGrid);
bool MayReorderSamples(const Context& context, std::uint32_t samples);
SampleOrderMethod DepthSampleOrderMethod(const Context& context, const DepthTarget& depth);
VkFormat SampleOrderStorageFormat(const Context& context, VkFormat format, std::uint32_t elementBytes, std::uint32_t samples);
std::string SampleOrderRefusal(const Context& context, const State& state);
std::vector<SampleOrderStep> PlanSampleOrder(const Context& context, const State& state, const SampleOrder& order, VkAttachmentLoadOp load = VK_ATTACHMENT_LOAD_OP_LOAD, VkAttachmentStoreOp store = VK_ATTACHMENT_STORE_OP_STORE);
SampleOrderPass SampleOrderPassFor(const Context& context, const State& state, const SampleOrder& order);
void RecordSampleOrder(const Context& context, VkCommandBuffer commands, const SampleOrderPass& pass, std::span<const SampleOrderTarget> targets, bool toHost, std::vector<std::shared_ptr<void>>& kept);
void RecordDepthSampleLocations(const Context& context, VkCommandBuffer commands, const DepthSampleLocations& depth);
std::array<std::span<const std::uint32_t>, 9> SampleOrderShaderModules();
void ClearSampleOrders(VkDevice device);
}
#endif
@@ -52,6 +52,7 @@ bool SpirvStoresThroughAddresses(std::span<const std::uint32_t> words);
// The cached storage image of a surface (render targets use it as their resident image); brought up
// to date with guest memory before it is returned.
std::shared_ptr<StorageTexture> CachedStorageSurface(const Context& context, const GuestTextureResource& resource);
std::shared_ptr<StorageTexture> CachedStorageLayers(const Context& context, const GuestTextureResource& resource, std::uint32_t firstLayer, std::uint32_t layerCount);
// Whether `image` is still the storage cache's image of its surface (what CachedStorageSurface
// would return); a true answer counts as a use for the cache's eviction order, as a lookup would.
bool StorageImageCached(const Context& context, const StorageTexture* image);
@@ -143,6 +143,7 @@ struct State {
VkConservativeRasterizationModeEXT conservativeRasterization = VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT;
VkRect2D scissor;
VkCullModeFlags cullMode;
VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL;
VkFrontFace frontFace;
VkProvokingVertexModeEXT provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT;
VkPipelineColorBlendAttachmentState blend;
@@ -167,6 +168,14 @@ void NormalizeUnusedViewportDepth(State& state, const ShaderRecompiler::ShaderPi
std::array<VkSampleLocationEXT, 32> SampleLocations(const State& state);
std::array<std::uint8_t, 32> PixelSampleLocations(const State& state, const ShaderRecompiler::ShaderPixelStageInfo& pixel);
VkExtent2D SampleLocationGrid(const State& state);
using SampleOrder = std::array<std::uint32_t, 4>;
inline constexpr std::uint32_t KeptSampleOrder = 0x76543210u;
std::optional<SampleOrder> SameSetSampleOrder(const State& state);
bool ReordersSamples(const SampleOrder& order);
std::uint32_t InvertSampleOrder(std::uint32_t order);
std::uint32_t HostSampleBits(std::uint32_t guestBits, std::uint32_t order);
bool SampleMaskFollowsOrder(const State& state, const SampleOrder& order);
std::array<std::uint16_t, 4> QuadSampleMasks(const State& state, const SampleOrder& order);
std::array<std::uint8_t, 8> ExportMappings(const State& state);
std::array<ShaderRecompiler::ColorExportPacking, 8> ExportPackings(const State& state);
ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state);
@@ -192,6 +201,14 @@ struct ColorMetadataPass {
std::optional<ColorTarget> source;
};
std::optional<ColorMetadataPass> DecodeColorMetadataPass(const QueueState& queue);
struct DepthCopyPass {
std::uint64_t depthAddress;
VkExtent2D extent;
std::uint32_t samples;
std::uint32_t sample;
ColorTarget color;
};
std::optional<DepthCopyPass> DecodeDepthCopyPass(const QueueState& queue);
std::string DepthMaintenanceRejection(const QueueState& queue);
bool DepthResummarizeDrawsNothing(const QueueState& queue);
// The message DecodeState (or the pixel stage decode after it) would throw for the register rules
@@ -63,7 +63,7 @@ public:
// compute pass writing it and the next pass sampling it share one image and copy nothing.
// CanCopyFrom says whether the two descriptors address the same surface compatibly.
Texture(const Context& context, const std::shared_ptr<StorageTexture>& source, const GuestTextureResource& descriptor, VkComponentMapping components);
Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D, std::uint32_t baseLayer = 0, std::shared_ptr<OwnedImage> owner = {});
Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D, std::uint32_t baseLayer = 0, std::shared_ptr<OwnedImage> owner = {}, VkImageUsageFlags usage = 0);
struct DepthCopyTag {};
Texture(const Context& context, const std::shared_ptr<StorageTexture>& source, const GuestTextureResource& descriptor, VkComponentMapping components, DepthCopyTag);
struct SrgbExpandTag {};
@@ -198,6 +198,10 @@ public:
// Deferred write-back (APS5_EAGER_WRITEBACK=1 stores at once instead).
void MarkDirty();
void MarkWholeWritten();
void MarkUnitsWritten(const std::vector<bool>& units);
std::vector<VkRect2D> UnitRects(const std::vector<bool>& units) const;
bool BlockUnits() const { return blockUnits; }
std::uint32_t TrackedUnits() const { return trackedLayers; }
void MarkSliceDirty(std::uint32_t slice, std::uint32_t count = 1);
void Flush();
bool Dirty() const;
@@ -350,6 +354,7 @@ public:
// validates textures copied from this image.
std::uint64_t Version() const { return version; }
std::uint64_t Generation() const { return generation; }
std::uint64_t GenerationFor(std::uint64_t address, std::uint64_t bytes) const;
// The DCC keys the content was uploaded under and the last scan of the surface's keys
// (ProvedClearKeys): Refresh's own key rule for the fast revalidation. Under
// GuestMemory::GpuMutex only, as Refresh is; never from a build's stage A.
@@ -364,6 +369,7 @@ public:
// was still current (nothing uploaded).
// Keeps the image current with guest memory (see GuestMemory::CollectWrites).
bool Refresh();
bool RefreshLayers(std::uint32_t first, std::uint32_t count);
static std::uint64_t SinglePassMoves();
static std::uint64_t SlabRetiles();
static std::uint64_t RefreshesProved();
@@ -380,9 +386,8 @@ private:
StorageTexture(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, std::uint32_t mipLevel);
// The regions of every array layer, or of the tracked layers `layers` selects.
std::vector<VkBufferImageCopy> CopyRegions(const std::vector<bool>* layers = nullptr) const;
// Uploads the surface, or only the tracked layers `layers` selects (the direct path; the others
// upload everything).
void upload(const std::vector<bool>* layers = nullptr);
std::vector<bool> cpuUploadLayers(const std::vector<bool>& units) const;
void captureGuestBytes(const std::vector<bool>* layers);
bool originalHolds(std::uint64_t address, std::size_t bytes) const;
bool compareUntracked(std::uint64_t address, std::size_t bytes, std::span<std::uint8_t> changed, bool memoize = false) const;
@@ -477,7 +482,10 @@ private:
// layer generation (the whole-surface fast check), `dirty` whether any layer is pending.
std::uint64_t layerBegin(std::uint32_t layer) const { return descriptor.baseAddress + static_cast<std::uint64_t>(layer) * trackedLayerBytes; }
bool anyLayerPending() const;
bool refresh(std::uint32_t firstLayer, std::uint32_t lastLayer);
void noteLayersBehind(std::uint32_t firstLayer, std::uint32_t lastLayer, const std::vector<bool>& outside);
void refreshGeneration();
std::uint64_t collectedLayerGeneration(std::uint32_t layer, std::uint64_t whole, bool uncached = false) const;
bool refreshProved();
void takeRefreshProof(bool aliased);
bool otherPendingOverlaps() const;
@@ -555,7 +563,13 @@ private:
std::uint64_t sliceLinearBytes = 0;
SurfaceGeometry geometry;
std::vector<std::byte> original;
mutable std::array<std::uint64_t, 5> comparedGuestBytes{};
struct ComparedRange {
std::uint64_t address = 0;
std::size_t bytes = 0;
std::array<std::uint64_t, 5> stamp{};
};
mutable std::array<ComparedRange, 4> comparedRanges{};
mutable std::uint32_t nextComparedRange = 0;
std::vector<std::byte> generationBaseline;
// DCC keys the image content was uploaded under: a fast-cleared surface starts as its clear value.
DccKeys uploadedKeys = DccKeys::Uncompressed;
@@ -581,6 +595,7 @@ private:
std::uint64_t trackedLayerBytes = 0;
std::vector<std::uint64_t> layerGeneration;
std::vector<bool> layerPending;
std::vector<bool> layerBehind;
bool blockUnits = false;
// Write-back hysteresis (block units, see writeBack): partial stores in the current window of
// presents, its first present, and the present until which every pending unit is stored.
@@ -56,9 +56,12 @@ namespace AgcDriver::Graphics {
void DispatchCmaskClear(VkCommandBuffer commands, VkBuffer cmask, std::uint64_t cmaskOffset, std::size_t cmaskBytes, VkImageView view, std::uint32_t width, std::uint32_t height, std::uint32_t elementBytes, const std::array<std::uint32_t, 2>& clearWords, bool write);
std::uint32_t CmaskErrors();
void DispatchMultisampleReadback(VkCommandBuffer commands, VkFormat format, VkBuffer destination, std::uint64_t offset, std::uint64_t bytes, VkImageView arrayView, std::uint32_t width, std::uint32_t height, std::uint32_t layers, std::uint32_t fragments, std::uint32_t firstElement, std::uint32_t elements);
void DispatchDepthSample(VkCommandBuffer commands, VkImageView depth, bool multisampled, VkImageView target, std::uint32_t width, std::uint32_t height, std::uint32_t sample);
void DispatchMultisampleImage(VkCommandBuffer commands, std::uint32_t elementBytes, VkBuffer source, std::uint64_t offset, std::uint64_t bytes, VkImageView arrayView, std::uint32_t width, std::uint32_t height, std::uint32_t layers, std::uint32_t fragments, std::uint32_t firstElement, std::uint32_t elements);
private:
VkPipeline multisampleReadbackPipeline(std::uint32_t elementBytes, bool depth);
VkPipeline multisampleUploadPipeline(std::uint32_t elementBytes);
VkPipeline pipeline(TextureTileMode tileMode, std::uint32_t elementBytes, bool retile, bool thick, bool image = false);
void release() noexcept;
struct DescriptorRing {
@@ -82,6 +85,11 @@ namespace AgcDriver::Graphics {
VkPipelineLayout cmaskPipelineLayout = VK_NULL_HANDLE;
VkPipeline cmaskPipeline = VK_NULL_HANDLE;
DescriptorRing cmaskDescriptorSets;
VkDescriptorSetLayout depthSampleLayout = VK_NULL_HANDLE;
VkPipelineLayout depthSamplePipelineLayout = VK_NULL_HANDLE;
std::array<VkPipeline, 2> depthSamplePipelines{};
VkSampler depthSampler = VK_NULL_HANDLE;
DescriptorRing depthSampleSets;
std::unique_ptr<Buffer> cmaskErrors;
VkDescriptorSetLayout imageDescriptorLayout = VK_NULL_HANDLE;
VkPipelineLayout imagePipelineLayout = VK_NULL_HANDLE;
@@ -94,6 +102,7 @@ namespace AgcDriver::Graphics {
std::array<VkShaderModule, 2> readbackModules{};
std::vector<std::pair<std::uint32_t, VkPipeline>> readbackPipelines;
DescriptorRing readbackDescriptorSets;
VkShaderModule multisampleUploadModule = VK_NULL_HANDLE;
};
}
@@ -230,14 +230,16 @@ inline std::uint32_t VertexBufferOutOfBoundsSelect(const ShaderRecompiler::Verte
inline bool VertexFetchOutOfRange(const ShaderRecompiler::VertexAttribute& attribute) {
const auto stride = (attribute.resource.fields[1] >> 16u) & 0x3fffu;
if (stride != 0 || NullVertexDescriptor(attribute)) return false;
const auto select = VertexBufferOutOfBoundsSelect(attribute);
return stride == 0 && (select == 2u || select == 3u) && attribute.resource.fields[2] == 0;
const auto records = attribute.resource.fields[2];
if (select == 0u) return true;
if (select == 3u) return records < DecodeVertexFormat(attribute).bytes;
return records == 0;
}
inline std::uint64_t ZeroStrideAvailableBytes(const ShaderRecompiler::VertexAttribute& attribute) {
const auto records = attribute.resource.fields[2];
if (VertexBufferOutOfBoundsSelect(attribute) == 2u && records != 0) return std::max<std::uint64_t>(records, DecodeVertexFormat(attribute).bytes);
return records;
return VertexFetchOutOfRange(attribute) ? 0u : DecodeVertexFormat(attribute).bytes;
}
inline std::size_t VertexBufferExtent(const ShaderRecompiler::VertexAttribute& attribute) {
@@ -261,7 +263,9 @@ inline std::size_t VertexBufferReadSize(const ShaderRecompiler::VertexAttribute&
Require(attribute.fetchIndex <= 1, "unsupported vertex fetch index");
const auto index = attribute.fetchIndex == 0 ? maxIndex : firstInstance + instances - 1u;
const auto bytes = DecodeVertexFormat(attribute).bytes;
Require(stride == 0 || VertexBufferOutOfBoundsSelect(attribute) <= 1u || index < records, "vertex fetch exceeds descriptor record count");
const auto select = VertexBufferOutOfBoundsSelect(attribute);
const auto recordsUnchecked = stride == 0 ? select != 1u : select <= 1u;
Require(recordsUnchecked || index < records, "vertex fetch exceeds descriptor record count");
const auto required = static_cast<std::uint64_t>(stride) * index + bytes;
Require(stride != 0 || required <= ZeroStrideAvailableBytes(attribute), "vertex fetch exceeds descriptor byte range");
Require(required <= std::numeric_limits<std::size_t>::max(), "vertex fetch exceeds addressable byte range");
@@ -0,0 +1,27 @@
#version 450
layout(local_size_x = 8, local_size_y = 8) in;
#if MULTISAMPLED
layout(set = 0, binding = 0) uniform sampler2DMS depth;
#else
layout(set = 0, binding = 0) uniform sampler2D depth;
#endif
layout(set = 0, binding = 1, r32f) writeonly uniform image2D target;
layout(push_constant) uniform Push {
uint width;
uint height;
int sampleIndex;
} push;
void main() {
uvec2 pixel = gl_GlobalInvocationID.xy;
if (pixel.x >= push.width || pixel.y >= push.height) return;
#if MULTISAMPLED
float value = texelFetch(depth, ivec2(pixel), push.sampleIndex).r;
#else
float value = texelFetch(depth, ivec2(pixel), 0).r;
#endif
imageStore(target, ivec2(pixel), vec4(value, 0.0, 0.0, 0.0));
}
@@ -0,0 +1,32 @@
#version 450
#extension GL_EXT_shader_image_load_formatted : require
layout(constant_id = 0) const uint ELEMENT_BYTES = 4u;
layout(local_size_x = 64) in;
layout(set = 0, binding = 0, std430) readonly buffer Input { uint data[]; } sourceBuffer;
layout(set = 0, binding = 1) uniform uimage2DMSArray destinationImage;
layout(push_constant) uniform Push {
uint width;
uint height;
uint layers;
uint fragments;
uint firstElement;
uint elements;
uint rowThreads;
} params;
void main() {
uint index = gl_GlobalInvocationID.y * params.rowThreads + gl_GlobalInvocationID.x;
if (index >= params.elements) return;
uint element = params.firstElement + index;
uint pixel = element / params.fragments;
ivec3 coordinate = ivec3(pixel % params.width, (pixel / params.width) % params.height, pixel / (params.width * params.height));
uint address = index * ELEMENT_BYTES;
uvec4 value = uvec4(0u);
if (ELEMENT_BYTES < 4u) {
value.x = (sourceBuffer.data[address >> 2u] >> ((address & 3u) * 8u)) & (ELEMENT_BYTES == 1u ? 255u : 65535u);
} else {
for (uint component = 0u; component < ELEMENT_BYTES / 4u; ++component) value[component] = sourceBuffer.data[(address >> 2u) + component];
}
imageStore(destinationImage, coordinate, int(element % params.fragments), value);
}
@@ -0,0 +1,60 @@
#version 450
layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
layout(binding = 0, TEXEL_FORMAT) uniform restrict uimage2DMS samples;
layout(push_constant) uniform Order {
uvec4 map;
uvec2 extent;
uint count;
} order;
uvec4 pick(uint source, uvec4 s0, uvec4 s1, uvec4 s2, uvec4 s3, uvec4 s4, uvec4 s5, uvec4 s6, uvec4 s7) {
const bool odd = (source & 1u) != 0u;
const uvec4 low = (source & 2u) == 0u ? (odd ? s1 : s0) : (odd ? s3 : s2);
const uvec4 high = (source & 2u) == 0u ? (odd ? s5 : s4) : (odd ? s7 : s6);
return (source & 4u) == 0u ? low : high;
}
void reorder(ivec2 at, uint map) {
if (map == 0x76543210u || uint(at.x) >= order.extent.x || uint(at.y) >= order.extent.y) return;
const uvec4 s0 = imageLoad(samples, at, 0);
const uvec4 s1 = imageLoad(samples, at, 1);
uvec4 s2 = uvec4(0u);
uvec4 s3 = uvec4(0u);
uvec4 s4 = uvec4(0u);
uvec4 s5 = uvec4(0u);
uvec4 s6 = uvec4(0u);
uvec4 s7 = uvec4(0u);
if (order.count > 2u) {
s2 = imageLoad(samples, at, 2);
s3 = imageLoad(samples, at, 3);
}
if (order.count > 4u) {
s4 = imageLoad(samples, at, 4);
s5 = imageLoad(samples, at, 5);
s6 = imageLoad(samples, at, 6);
s7 = imageLoad(samples, at, 7);
}
imageStore(samples, at, 0, pick(map & 7u, s0, s1, s2, s3, s4, s5, s6, s7));
imageStore(samples, at, 1, pick((map >> 4u) & 7u, s0, s1, s2, s3, s4, s5, s6, s7));
if (order.count > 2u) {
imageStore(samples, at, 2, pick((map >> 8u) & 7u, s0, s1, s2, s3, s4, s5, s6, s7));
imageStore(samples, at, 3, pick((map >> 12u) & 7u, s0, s1, s2, s3, s4, s5, s6, s7));
}
if (order.count > 4u) {
imageStore(samples, at, 4, pick((map >> 16u) & 7u, s0, s1, s2, s3, s4, s5, s6, s7));
imageStore(samples, at, 5, pick((map >> 20u) & 7u, s0, s1, s2, s3, s4, s5, s6, s7));
imageStore(samples, at, 6, pick((map >> 24u) & 7u, s0, s1, s2, s3, s4, s5, s6, s7));
imageStore(samples, at, 7, pick((map >> 28u) & 7u, s0, s1, s2, s3, s4, s5, s6, s7));
}
}
void main() {
const ivec2 quad = ivec2(gl_GlobalInvocationID.xy) * 2;
reorder(quad, order.map.x);
reorder(quad + ivec2(1, 0), order.map.y);
reorder(quad + ivec2(0, 1), order.map.z);
reorder(quad + ivec2(1, 1), order.map.w);
}
@@ -0,0 +1,31 @@
#version 450
#extension GL_EXT_samplerless_texture_functions : require
#ifdef STENCIL_EXPORT
#extension GL_ARB_shader_stencil_export : require
#endif
layout(binding = 0) uniform texture2DMS depthSource;
layout(binding = 1) uniform utexture2DMS stencilSource;
layout(push_constant) uniform Order {
uvec4 map;
uint bit;
} order;
void main() {
const ivec2 at = ivec2(gl_FragCoord.xy);
const bool right = (at.x & 1) != 0;
const uint top = right ? order.map.y : order.map.x;
const uint bottom = right ? order.map.w : order.map.z;
const uint map = (at.y & 1) != 0 ? bottom : top;
if (map == 0x76543210u) discard;
const int source = int((map >> (uint(gl_SampleID) * 4u)) & 7u);
#ifdef STENCIL_BITS
if (((texelFetch(stencilSource, at, source).r >> order.bit) & 1u) == 0u) discard;
#else
gl_FragDepth = texelFetch(depthSource, at, source).r;
#ifdef STENCIL_EXPORT
gl_FragStencilRefARB = int(texelFetch(stencilSource, at, source).r);
#endif
#endif
}
@@ -0,0 +1,6 @@
#version 450
void main() {
const vec2 corner = vec2(float((gl_VertexIndex << 1) & 2), float(gl_VertexIndex & 2));
gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0);
}
@@ -19,6 +19,7 @@
#include <limits>
#include <shared_mutex>
#include <cstdio>
#include <cstdlib>
#include <map>
#include <memory>
#include <mutex>
@@ -150,6 +151,21 @@ DepthClearCounters& depthClearCounters() {
return counters;
}
struct DepthPlaneStoreCounters {
std::atomic<std::uint64_t> stores{0};
std::atomic<std::uint64_t> units{0};
std::atomic<std::uint64_t> bytes{0};
};
DepthPlaneStoreCounters& depthPlaneStoreCounters() {
static DepthPlaneStoreCounters counters;
return counters;
}
bool anyUnit(const std::vector<bool>& units) {
return std::find(units.begin(), units.end(), true) != units.end();
}
thread_local DepthPlaneOverwrite* currentOverwrite = nullptr;
std::optional<DepthPlaneOverwrite> decodeOverwrite(const StorageImageClear& clear) {
@@ -491,6 +507,30 @@ public:
else Recorder::CountBarriers(Recorder::CommandClass::Draw, 2);
}
void CopySample(std::uint32_t sample, StorageTexture& destination, VkImageView destinationView) {
auto* recorder = Recorder::Active();
Require(recorder != nullptr, "depth sample copies need an active recorder");
Require(sampleable, "DB depth copy from a depth surface the device cannot sample at its sample count");
ApplyFastClear();
if (useSampleShadow) {
if (sampleShadow == nullptr) createSampleShadow();
refreshSampleShadow(false);
recorder->Keep(sampleShadow);
}
if (depthView == VK_NULL_HANDLE) {
VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
viewInfo.image = useSampleShadow ? sampleShadow->image : image;
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = hostFormat;
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1};
Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &depthView), "vkCreateImageView depth sample copy");
}
Require(context.detiler != nullptr, "depth sample copies need the texture detiler");
context.detiler->DispatchDepthSample(recorder->Commands(), depthView, target.samples > 1, destinationView, target.extent.width, target.extent.height, sample);
Recorder::CountBarriers(Recorder::CommandClass::Copy, 2);
destination.MarkDirty();
}
bool Holds(const Texture* texture) const {
const auto held = [&](const auto& entry) { return entry.second.get() == texture; };
return std::any_of(textures.begin(), textures.end(), held) || std::any_of(stencilCopy.textures.begin(), stencilCopy.textures.end(), held);
@@ -698,48 +738,173 @@ public:
else Recorder::CountBarriers(Recorder::CommandClass::Copy, 3);
}
std::shared_ptr<StorageTexture> StoreDepthPlane(const GuestTextureResource& resource) {
ApplyFastClear();
std::shared_ptr<StorageTexture> Mirror(const GuestTextureResource& resource) {
if (mirrorPlane.storage == nullptr) {
mirrorPlane.storage = StorageTexture::Create(context, *context.detiler, resource, 0);
++mirrorEpoch;
}
copyPlane(mirrorPlane, mirrorPlane.storage, false, true, true);
return mirrorPlane.storage;
}
bool SyncFromMemory(const GuestTextureResource& resource) {
bool StoresUnits(const StorageTexture& mirror) const {
return mirror.BlockUnits() && mirror.TrackedUnits() == staleUnits.size();
}
std::shared_ptr<StorageTexture> StoreDepthPlane(const GuestTextureResource& resource, std::vector<bool>& units) {
ApplyFastClear();
auto mirror = Mirror(resource);
auto& counters = depthPlaneStoreCounters();
++counters.stores;
const auto planeBytes = mirror->GuestBytes();
if (!StoresUnits(*mirror)) {
units.assign(units.size(), true);
copyPlane(mirrorPlane, mirror, false, true, true);
mirror->MarkWholeWritten();
counters.units += units.size();
counters.bytes += planeBytes;
return mirror;
}
const auto rects = mirror->UnitRects(units);
copyPlane(mirrorPlane, mirror, false, true, true, &rects);
mirror->MarkUnitsWritten(units);
for (std::size_t unit = 0; unit < units.size(); ++unit) {
if (!units[unit]) continue;
++counters.units;
counters.bytes += std::min<std::uint64_t>(unitBytes, planeBytes - unit * unitBytes);
}
return mirror;
}
void SizeUnits(std::uint64_t planeBytes) {
static const bool whole = std::getenv("APS5_DEPTH_PLANE_WHOLE_STORE") != nullptr;
const std::uint64_t bytes = !whole && target.address % 65536u == 0 ? std::uint64_t{65536} : std::max<std::uint64_t>(planeBytes, 1u);
const auto count = static_cast<std::size_t>(std::max<std::uint64_t>((planeBytes + bytes - 1u) / bytes, 1u));
if (unitBytes == bytes && staleUnits.size() == count) return;
unitBytes = bytes;
staleUnits.assign(count, false);
staleCount = 0;
unitGeneration.assign(count, 0);
}
bool AnyStale() const {
return staleCount != 0;
}
void MarkAllStale(std::uint64_t planeBytes) {
SizeUnits(planeBytes);
staleUnits.assign(staleUnits.size(), true);
staleCount = staleUnits.size();
}
void ClearStale() {
staleUnits.assign(staleUnits.size(), false);
staleCount = 0;
}
void ClearStale(const std::vector<bool>& units) {
for (std::size_t unit = 0; unit < units.size() && unit < staleUnits.size(); ++unit) {
if (!units[unit] || !staleUnits[unit]) continue;
staleUnits[unit] = false;
--staleCount;
}
}
std::vector<bool> StaleUnitsIn(std::uint64_t planeBytes, std::uint64_t begin, std::uint64_t end) const {
std::vector<bool> units(staleUnits.size(), false);
const auto plane = target.address;
if (units.empty() || unitBytes == 0 || begin >= end || begin >= plane + planeBytes || plane >= end) return units;
const auto from = std::max(begin, plane) - plane;
const auto to = std::min(end, plane + planeBytes) - plane;
const auto last = std::min<std::uint64_t>((to - 1u) / unitBytes, units.size() - 1u);
for (auto unit = from / unitBytes; unit <= last; ++unit) units[static_cast<std::size_t>(unit)] = staleUnits[static_cast<std::size_t>(unit)];
return units;
}
std::pair<std::uint64_t, std::uint64_t> UnitHull(const std::vector<bool>& units, std::uint64_t planeBytes) const {
std::uint64_t first = planeBytes;
std::uint64_t last = 0;
for (std::size_t unit = 0; unit < units.size(); ++unit) {
if (!units[unit]) continue;
first = std::min<std::uint64_t>(first, unit * unitBytes);
last = std::max<std::uint64_t>(last, std::min<std::uint64_t>(planeBytes, (unit + 1u) * unitBytes));
}
return {target.address + first, target.address + std::max(first, last)};
}
void NoteStored(const std::vector<bool>& units, std::uint64_t generation) {
for (std::size_t unit = 0; unit < units.size() && unit < unitGeneration.size(); ++unit) {
if (units[unit]) unitGeneration[unit] = generation;
}
}
std::vector<bool> WrittenUnits(const GuestTextureResource& resource, std::uint64_t& generation) {
const auto bytes = static_cast<std::size_t>(DescribeSurface(resource).guestBytes);
SizeUnits(bytes);
GuestMemory::CollectWrites(resource.baseAddress, bytes);
if (syncedGeneration == 0 || (pendingClear & VK_IMAGE_ASPECT_DEPTH_BIT) != 0 || !GuestMemory::WrittenSince(resource.baseAddress, bytes, syncedGeneration)) {
syncedGeneration = GuestMemory::TrackerGeneration();
return false;
generation = GuestMemory::TrackerGeneration();
std::vector<bool> changed(unitGeneration.size(), false);
if (unitGeneration.size() == 1u) {
changed[0] = GuestMemory::WrittenSince(resource.baseAddress, bytes, unitGeneration[0]);
return changed;
}
memoryStale = false;
if (mirrorPlane.storage == nullptr) {
mirrorPlane.storage = StorageTexture::Create(context, *context.detiler, resource, 0);
++mirrorEpoch;
const auto oldest = *std::min_element(unitGeneration.begin(), unitGeneration.end());
if (oldest != 0u && !GuestMemory::WrittenSince(resource.baseAddress, bytes, oldest)) return changed;
std::vector<std::uint8_t> stamped(unitGeneration.size(), GuestMemory::BlockUnchanged);
if (!GuestMemory::ChangedBlocks(resource.baseAddress, bytes, unitGeneration, stamped)) return changed;
for (std::size_t unit = 0; unit < changed.size(); ++unit) changed[unit] = unitGeneration[unit] != 0u && stamped[unit] == GuestMemory::BlockWritten;
return changed;
}
void SyncFromMemory(const GuestTextureResource& resource) {
std::uint64_t generation = 0;
const auto changed = WrittenUnits(resource, generation);
if ((pendingClear & VK_IMAGE_ASPECT_DEPTH_BIT) != 0 || !anyUnit(changed)) {
unitGeneration.assign(unitGeneration.size(), generation);
return;
}
ClearStale(changed);
{
struct Syncing {
bool& flag;
~Syncing() { flag = false; }
} guard{syncing};
syncing = true;
if (mirrorPlane.storage == nullptr) {
mirrorPlane.storage = StorageTexture::Create(context, *context.detiler, resource, 0);
++mirrorEpoch;
} else {
static_cast<void>(mirrorPlane.storage->Refresh());
}
}
unitGeneration.assign(unitGeneration.size(), generation);
const auto mirror = mirrorPlane.storage;
if (StoresUnits(*mirror)) {
const auto rects = mirror->UnitRects(changed);
copyPlane(mirrorPlane, mirror, false, false, true, &rects);
} else {
ClearStale();
copyPlane(mirrorPlane, mirror, false, false, true);
}
else static_cast<void>(mirrorPlane.storage->Refresh());
syncedGeneration = GuestMemory::TrackerGeneration();
copyPlane(mirrorPlane, mirrorPlane.storage, false, false, true);
written = ++depthWrites;
return true;
}
void DropMirror() {
mirrorPlane = {};
}
bool memoryStale = false;
std::vector<bool> staleUnits;
std::size_t staleCount = 0;
std::vector<std::uint64_t> unitGeneration;
std::uint64_t unitBytes = 0;
bool syncing = false;
std::uint64_t scannedEpoch = 0;
std::uint64_t syncedGeneration = 0;
const Context context;
const DepthTarget target;
const VkFormat hostFormat;
VkImage image = VK_NULL_HANDLE;
VkImageView view = VK_NULL_HANDLE;
VkImageView depthView = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
std::shared_ptr<std::atomic<bool>> active = std::make_shared<std::atomic<bool>>(true);
bool sampleable = true;
@@ -943,33 +1108,52 @@ private:
return ResolveTextureFormat(resource.format) == (stencil ? VK_FORMAT_R8_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT);
}
void copyPlane(PlaneStorage& plane, const std::shared_ptr<StorageTexture>& storage, bool stencil, bool toStorage, bool ownBatch = false) {
void copyPlane(PlaneStorage& plane, const std::shared_ptr<StorageTexture>& storage, bool stencil, bool toStorage, bool ownBatch = false, const std::vector<VkRect2D>* rects = nullptr) {
if (rects != nullptr && rects->empty()) return;
auto* recorder = Recorder::Active();
Require(recorder != nullptr || ownBatch, "depth surface storage copies need an active recorder");
std::unique_ptr<CommandBatch> batch;
if (recorder == nullptr) batch = std::make_unique<CommandBatch>(context);
const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT;
const auto bytes = static_cast<std::size_t>(target.extent.width) * target.extent.height * (stencil ? 1u : d16 ? 2u : 4u);
const std::uint32_t texelBytes = stencil ? 1u : d16 ? 2u : 4u;
const auto bytes = static_cast<std::size_t>(target.extent.width) * target.extent.height * texelBytes;
if (plane.staging == nullptr) plane.staging = std::make_unique<DeviceBuffer>(context, bytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT);
const auto commands = recorder != nullptr ? recorder->Commands() : batch->Handle();
if (recorder != nullptr) recorder->Keep(storage);
const std::uint32_t timing = recorder != nullptr ? recorder->BeginGpuTiming(Recorder::CommandClass::Copy) : 0u;
VkBufferImageCopy planeRegion{0, 0, 0, {stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT, 0, 0, 1}, {0, 0, 0}, {target.extent.width, target.extent.height, 1}};
VkBufferImageCopy colorRegion = planeRegion;
colorRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
const VkBufferImageCopy planeRegion{0, 0, 0, {stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT, 0, 0, 1}, {0, 0, 0}, {target.extent.width, target.extent.height, 1}};
std::vector<VkBufferImageCopy> planeRegions;
std::size_t moved = bytes;
if (rects == nullptr) {
planeRegions.push_back(planeRegion);
} else {
moved = 0;
for (const auto& rect : *rects) {
auto region = planeRegion;
region.bufferOffset = (static_cast<VkDeviceSize>(rect.offset.y) * target.extent.width + static_cast<VkDeviceSize>(rect.offset.x)) * texelBytes;
region.bufferRowLength = target.extent.width;
region.imageOffset = {rect.offset.x, rect.offset.y, 0};
region.imageExtent = {rect.extent.width, rect.extent.height, 1};
planeRegions.push_back(region);
moved += static_cast<std::size_t>(rect.extent.width) * rect.extent.height * texelBytes;
}
}
auto colorRegions = planeRegions;
for (auto& region : colorRegions) region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
const auto count = static_cast<std::uint32_t>(planeRegions.size());
const auto sourceImage = toStorage ? image : storage->Image();
const auto destinationImage = toStorage ? storage->Image() : image;
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT);
context.Function<PFN_vkCmdCopyImageToBuffer>("vkCmdCopyImageToBuffer")(commands, sourceImage, VK_IMAGE_LAYOUT_GENERAL, plane.staging->Handle(), 1, toStorage ? &planeRegion : &colorRegion);
context.Function<PFN_vkCmdCopyImageToBuffer>("vkCmdCopyImageToBuffer")(commands, sourceImage, VK_IMAGE_LAYOUT_GENERAL, plane.staging->Handle(), count, toStorage ? planeRegions.data() : colorRegions.data());
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT);
context.Function<PFN_vkCmdCopyBufferToImage>("vkCmdCopyBufferToImage")(commands, plane.staging->Handle(), destinationImage, VK_IMAGE_LAYOUT_GENERAL, 1, toStorage ? &colorRegion : &planeRegion);
context.Function<PFN_vkCmdCopyBufferToImage>("vkCmdCopyBufferToImage")(commands, plane.staging->Handle(), destinationImage, VK_IMAGE_LAYOUT_GENERAL, count, toStorage ? colorRegions.data() : planeRegions.data());
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT);
if (batch) {
batch->SubmitAndWait();
return;
}
Recorder::CountBarriers(Recorder::CommandClass::Copy, 3);
recorder->EndGpuTiming(timing, bytes);
recorder->EndGpuTiming(timing, moved);
}
void release() noexcept {
@@ -978,6 +1162,7 @@ private:
releaseStencilCopy();
mirrorPlane = {};
textures.clear();
if (depthView) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, depthView, nullptr);
if (view) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, view, nullptr);
if (owned != nullptr) owned.reset();
else {
@@ -1578,15 +1763,36 @@ void retire(DepthSurface& surface) {
for (auto* owners : {&depthOwners(), &stencilOwners()}) {
std::erase_if(*owners, [&](const auto& entry) { return entry.second.surface == &surface; });
}
if (!surface.memoryStale) return;
surface.memoryStale = false;
if (!surface.AnyStale()) return;
surface.ClearStale();
const auto plane = stalePlaneRange(surface);
if (std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& other) { return other->memoryStale && stalePlaneRange(*other) == plane; })) return;
if (std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& other) { return other->AnyStale() && stalePlaneRange(*other) == plane; })) return;
auto& planes = stalePlanes();
std::lock_guard lock(planes.mutex);
std::erase(planes.ranges, plane);
publishStaleHull(planes);
}
void retireStoringStale(DepthSurface& surface) {
if (!surface.AnyStale() || !mirrorable(surface.target)) {
retire(surface);
return;
}
const auto resource = mirrorResource(surface.target);
const auto planeBytes = DescribeSurface(resource).guestBytes;
std::uint64_t generation = 0;
const auto written = surface.WrittenUnits(resource, generation);
auto units = surface.staleUnits;
for (std::size_t unit = 0; unit < units.size() && unit < written.size(); ++unit) {
if (written[unit]) units[unit] = false;
}
std::shared_ptr<StorageTexture> mirror;
if (anyUnit(units) && (!anyUnit(written) || surface.StoresUnits(*surface.Mirror(resource)))) mirror = surface.StoreDepthPlane(resource, units);
retire(surface);
if (mirror == nullptr) return;
const auto [begin, end] = surface.UnitHull(units, planeBytes);
if (StorageTexture::FlushPending(begin, static_cast<std::size_t>(end - begin), nullptr, "depth plane store")) surface.NoteStored(units, GuestMemory::TrackerGeneration());
}
}
std::uint64_t DepthSurfacesSerial() {
@@ -1749,7 +1955,7 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target,
if (!created) surface->NoteHtileClear(VK_IMAGE_ASPECT_DEPTH_BIT);
}
if (!created) surface->NoteHtileClear(htileCleared);
if (mirrorable(target)) static_cast<void>(surface->SyncFromMemory(mirrorResource(target)));
if (mirrorable(target)) surface->SyncFromMemory(mirrorResource(target));
surface->ApplyFastClear();
if (drawnDepth != nullptr && drawnDepth != surface) drawnDepth->written = ++depthWrites;
surface->written = ++depthWrites;
@@ -1761,70 +1967,123 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target,
if (other.get() == surface || other->context.device != context.device || other->written >= surface->written || !mirrorable(other->target)) continue;
const auto otherPlane = stalePlaneRange(*other);
if (otherPlane == plane || otherPlane.first >= plane.second || plane.first >= otherPlane.second) continue;
other->memoryStale = false;
other->ClearStale();
other->DropMirror();
}
}
if ((depthWritten || cleared) && mirrorable(target)) {
surface->memoryStale = true;
const auto plane = target.address;
noteStalePlane(plane, plane + DescribeSurface(mirrorResource(target)).guestBytes);
const auto planeBytes = DescribeSurface(mirrorResource(target)).guestBytes;
surface->MarkAllStale(planeBytes);
noteStalePlane(plane, plane + planeBytes);
}
drawnDepth = surface;
return surface->view;
}
SampleOrderTarget DepthSurfaceOrderTarget(const Context& context, const DepthTarget& target) {
std::lock_guard gpu(GuestMemory::GpuMutex());
std::unique_lock lock(surfacesMutex());
for (const auto& candidate : surfaces()) {
if (!candidate->active->load(std::memory_order_acquire) || candidate->context.device != context.device || !sameSurface(candidate->target, target)) continue;
Require(candidate->sampleable, "the multisampled depth surface whose samples are put in the guest's order cannot be sampled");
return {candidate->image, candidate->view, candidate->hostFormat, target.extent, target.samples, DepthSampleOrderMethod(context, target), candidate->owned};
}
throw std::runtime_error("AGC graphics: the multisampled depth surface whose samples are put in the guest's order is not resident");
}
DepthSampleLocations DepthSurfaceSampleLocations(const Context& context, const DepthTarget& target) {
std::lock_guard gpu(GuestMemory::GpuMutex());
std::unique_lock lock(surfacesMutex());
for (const auto& candidate : surfaces()) {
if (!candidate->active->load(std::memory_order_acquire) || candidate->context.device != context.device || !sameSurface(candidate->target, target)) continue;
DepthSampleLocations depth;
depth.image = candidate->image;
depth.format = candidate->hostFormat;
depth.samples = target.samples;
depth.keep = candidate->owned;
return depth;
}
throw std::runtime_error("AGC graphics: the depth surface drawn at custom sample locations is not resident");
}
bool DepthPlanesStale() {
auto& planes = stalePlanes();
return planes.begin.load(std::memory_order_acquire) < planes.end.load(std::memory_order_acquire);
}
std::vector<std::pair<std::uint64_t, std::uint64_t>> FlushDepthPlanes(std::uint64_t address, std::size_t bytes) {
std::vector<std::pair<std::uint64_t, std::uint64_t>> FlushDepthPlanes(std::uint64_t address, std::size_t bytes, DepthPlaneScope scope) {
static thread_local bool flushing = false;
const auto end = bytes > std::numeric_limits<std::uint64_t>::max() - address ? std::numeric_limits<std::uint64_t>::max() : address + bytes;
if (flushing || !overlapsStalePlane(address, end)) return {};
if (scope == DepthPlaneScope::Overlapped) {
std::lock_guard lock(surfacesMutex());
const auto staleInRange = [&](const auto& surface) { return surface->AnyStale() && !surface->syncing && ((surface->pendingClear & VK_IMAGE_ASPECT_DEPTH_BIT) != 0 || anyUnit(surface->StaleUnitsIn(DescribeSurface(mirrorResource(surface->target)).guestBytes, address, end))); };
if (std::none_of(surfaces().begin(), surfaces().end(), staleInRange)) return {};
}
flushing = true;
struct Reset {
~Reset() { flushing = false; }
} reset;
std::vector<std::pair<DepthSurface*, std::shared_ptr<StorageTexture>>> stored;
struct Stored {
DepthSurface* surface;
std::shared_ptr<StorageTexture> mirror;
std::vector<bool> units;
std::uint64_t begin;
std::uint64_t end;
bool flushed;
};
std::vector<Stored> stored;
std::vector<std::pair<std::uint64_t, std::uint64_t>> ranges;
{
std::lock_guard gpu(GuestMemory::GpuMutex());
std::lock_guard lock(surfacesMutex());
std::vector<std::pair<std::uint64_t, std::uint64_t>> remaining;
for (const auto& surface : surfaces()) {
if (!surface->memoryStale) continue;
if (!surface->AnyStale()) continue;
const auto resource = mirrorResource(surface->target);
const auto plane = surface->target.address;
const auto planeBytes = DescribeSurface(resource).guestBytes;
if (plane >= end || address >= plane + planeBytes) {
remaining.emplace_back(plane, plane + planeBytes);
continue;
const bool overlapped = !surface->syncing && plane < end && address < plane + planeBytes;
if (overlapped && (surface->pendingClear & VK_IMAGE_ASPECT_DEPTH_BIT) != 0) surface->MarkAllStale(planeBytes);
if (overlapped && (scope == DepthPlaneScope::Whole || anyUnit(surface->StaleUnitsIn(planeBytes, address, end)))) {
surface->SyncFromMemory(resource);
auto units = scope == DepthPlaneScope::Whole ? surface->staleUnits : surface->StaleUnitsIn(planeBytes, address, end);
if (anyUnit(units)) {
auto mirror = surface->StoreDepthPlane(resource, units);
surface->ClearStale(units);
const auto hull = surface->UnitHull(units, planeBytes);
ranges.emplace_back(hull.first, hull.second - hull.first);
stored.push_back({surface.get(), std::move(mirror), std::move(units), hull.first, hull.second, false});
}
}
surface->memoryStale = false;
if (surface->SyncFromMemory(resource)) continue;
stored.emplace_back(surface.get(), surface->StoreDepthPlane(resource));
ranges.emplace_back(plane, planeBytes);
if (surface->AnyStale()) remaining.emplace_back(plane, plane + planeBytes);
}
auto& planes = stalePlanes();
std::lock_guard staleLock(planes.mutex);
planes.ranges = std::move(remaining);
publishStaleHull(planes);
}
for (const auto& entry : stored) entry.second->MarkWholeWritten();
for (const auto& [plane, planeBytes] : ranges) StorageTexture::FlushPending(plane, static_cast<std::size_t>(planeBytes), nullptr, "depth plane store");
for (auto& entry : stored) entry.flushed = StorageTexture::FlushPending(entry.begin, static_cast<std::size_t>(entry.end - entry.begin), nullptr, "depth plane store");
const auto generation = GuestMemory::TrackerGeneration();
std::lock_guard lock(surfacesMutex());
for (const auto& entry : stored) {
if (std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& candidate) { return candidate.get() == entry.first; })) {
entry.first->syncedGeneration = generation;
entry.first->NoteMemory();
}
if (!entry.flushed || std::none_of(surfaces().begin(), surfaces().end(), [&](const auto& candidate) { return candidate.get() == entry.surface; })) continue;
entry.surface->NoteStored(entry.units, generation);
entry.surface->NoteMemory();
}
return ranges;
}
DepthPlaneStoreStatistics DepthPlaneStoreCounts() {
const auto& counters = depthPlaneStoreCounters();
DepthPlaneStoreStatistics statistics;
statistics.stores = counters.stores.load();
statistics.units = counters.units.load();
statistics.bytes = counters.bytes.load();
return statistics;
}
std::uint64_t HtileDepthClearAddress(std::span<const std::uint32_t> code, std::span<const std::uint32_t> userData, const std::array<std::uint32_t, 3>& numThreads) {
static constexpr std::array<std::uint32_t, 17> htileMaskKernel{0xd7460000u, 0x04010c06u, 0x34000084u, 0xdc388000u, 0x04020000u, 0xbf8c3f70u, 0xd7710008u, 0x00120805u, 0xd7710009u, 0x00120a05u, 0xd771000au, 0x00120c05u, 0xd771000bu, 0x00120e05u, 0xdc788000u, 0x00020800u, 0xbf810000u};
if (userData.size() < 6 || numThreads[0] != 64 || numThreads[1] != 1 || numThreads[2] != 1) return 0;
@@ -2001,7 +2260,7 @@ std::shared_ptr<Texture> DepthSurfaceTexture(const Context& context, std::span<c
const auto copyFormat = integer ? VK_FORMAT_R16_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT;
const bool planes = (format == copyFormat || depthBits) && resource.width == base.extent.width && resource.height == base.extent.height && resource.baseLevel == 0 && resource.lastLevel == 0 && !(resource.baseArray != 0 && (cube || !layered || resource.baseArray > resource.depthOrLastArray)) && !(cube && resource.depthOrLastArray != 0 && resource.depthOrLastArray != 5) && (resource.dimension == TextureDimension::k2D || resource.dimension == TextureDimension::k2DArray || cube);
if (!(sampled ? found->SampledAccepts(words, resource) : planes) && found->OverwrittenInMemory()) {
retire(*found);
retireStoringStale(*found);
return nullptr;
}
found->ApplyFastClear();
@@ -2178,6 +2437,17 @@ std::shared_ptr<StorageTexture> DepthSurfaceStorage(const Context& context, std:
return layeredDepthStorage(context, surface, resource, mip);
}
void CopyDepthSample(const Context& context, std::uint64_t depthAddress, std::uint32_t samples, std::uint32_t sample, StorageTexture& destination, VkImageView destinationView, VkExtent2D extent) {
std::lock_guard lock(surfacesMutex());
auto* found = planeSurface(context.device, depthAddress);
Require(found != nullptr && found->target.address == depthAddress, "DB depth copy from a depth plane no draw rendered");
Require(found->target.format == VK_FORMAT_D32_SFLOAT || found->target.format == VK_FORMAT_D32_SFLOAT_S8_UINT, "DB depth copy from a depth surface that is not 32-bit float");
Require(found->target.samples == samples, "DB depth copy from a depth surface of another sample count");
Require(found->target.extent.width == extent.width && found->target.extent.height == extent.height, "DB depth copy from a depth surface of another size");
Require(sample < found->target.samples, "DB depth copy of a sample the depth surface does not have");
found->CopySample(sample, destination, destinationView);
}
bool DepthSurfaceStoreBack(const Context& context, const StorageTexture& storage) {
std::lock_guard lock(surfacesMutex());
for (auto& [key, entry] : layeredStorages()) {
@@ -8,6 +8,7 @@
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp"
#include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
@@ -654,6 +655,7 @@ bool ValidationKey(const Context& context, std::span<const CompiledShader> shade
add(state.rectList);
add(state.topology);
add(state.cullMode);
add(state.polygonMode);
add(state.blends.size());
add(context.subgroup.subgroupSize);
add(context.subgroup.supportedStages);
@@ -803,10 +805,11 @@ void CheckBufferAliases(std::span<const CompiledShader> shaders, const ColorTarg
for (std::size_t offset = 0; offset + 4 <= words.size(); offset += 4) {
const ShaderRecompiler::ShaderBufferResource descriptor{{words[offset], words[offset + 1], words[offset + 2], words[offset + 3]}};
const auto address = descriptor.Base48();
const auto size = descriptor.GetSize();
auto size = descriptor.GetSize();
if (size == 0 || address == 0) continue;
const auto element = offset / 4;
const bool written = element >= binding.bufferWritten.size() || binding.bufferWritten[element];
if (const std::uint64_t stride = descriptor.Stride(); !written && stride != 0 && address < target.address && address + size > target.address && address + size - target.address < stride) size = target.address - address;
Require(!overlap(address, size, target.address, target.bytes), "shader buffer aliases the render target");
Require(!written || !overlap(address, size, indexAddress, indexBytes), "writable shader buffer aliases the index buffer");
}
@@ -1721,6 +1724,9 @@ struct RecordedDraw {
const std::array<std::byte, PipelinePushConstantBytes>* pushBytes = nullptr;
VkShaderStageFlags pushStages = 0;
std::vector<std::pair<std::uint32_t, std::shared_ptr<Pipeline>>> stencilReplay;
std::vector<SampleOrderTarget> reordered;
SampleOrderPass reorderPass;
std::optional<DepthSampleLocations> depthLocations;
};
// A remapped viewport depth range reads its scale and offset from the draw's push block (the vertex shader's
@@ -2021,6 +2027,11 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
}
// Earlier recorded work (dispatches, the previous draw) wrote the images in the general
// layout, which a lean draw renders in: no transitions.
if (!record.reordered.empty()) {
std::vector<std::shared_ptr<void>> kept;
RecordSampleOrder(context, commands, record.reorderPass, record.reordered, true, kept);
for (auto& object : kept) recorder->Keep(std::move(object));
}
APS5_LOG_OUT_DEBUG("Beginning pipeline renderExtent=%ux%u", state.renderExtent.width, state.renderExtent.height);
record.pipeline->Begin(commands, *record.framebuffer, state.renderExtent, state);
}
@@ -2054,12 +2065,22 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
auto checkRecords = indirectRecordCheck(record.indirect);
APS5_LOG_CHARS_OUT_DEBUG("Draw recorded");
std::function<void(VkCommandBuffer)> storeProxies;
if (!continued && !record.proxies.empty()) {
storeProxies = [proxies = record.proxies](VkCommandBuffer passCommands) {
if (!continued && (!record.proxies.empty() || !record.reordered.empty() || record.depthLocations)) {
storeProxies = [proxies = record.proxies, reordered = record.reordered, reorderPass = record.reorderPass, depthLocations = record.depthLocations, context, recorder](VkCommandBuffer passCommands) {
for (const auto& proxy : proxies) {
proxy->RecordAttachmentProxyStore(passCommands, VK_IMAGE_LAYOUT_GENERAL);
Recorder::CountBarriers(Recorder::CommandClass::Draw, 2);
}
if (!reordered.empty()) {
std::vector<std::shared_ptr<void>> kept;
RecordSampleOrder(context, passCommands, reorderPass, reordered, false, kept);
for (auto& object : kept) recorder->Keep(std::move(object));
}
if (depthLocations) {
RecordDepthSampleLocations(context, passCommands, *depthLocations);
Recorder::CountBarriers(Recorder::CommandClass::Draw, 2);
recorder->Keep(depthLocations->keep);
}
};
}
recorder->LeaveRenderPassOpen(passKey, drawTiming, fenced, passAccess, std::move(storeProxies));
@@ -2091,6 +2112,14 @@ std::optional<State> maskedState(const State& state, std::uint32_t fragmentOutpu
return masked;
}
std::optional<DepthSampleLocations> passDepthLocations(const Context& context, const State& state, const Pipeline& pipeline) {
if (!state.depth || state.depth->samples <= 1u || !state.customSampleLocations || !pipeline.SampleLocationsEnabled()) return std::nullopt;
auto locations = DepthSurfaceSampleLocations(context, *state.depth);
locations.grid = pipeline.HostSampleLocationGrid();
locations.locations = SampleLocations(state);
return locations;
}
// Debug aid: APS5_DUMP_TARGETS=<n> saves the first n renders of every color target as a raw file
// (u32 width, u32 height, u32 VkFormat, then tightly packed rows).
int DumpTargetLimit() {
@@ -2211,7 +2240,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
}
APS5_LOG_OUT_DEBUG("Creating color target %zu address=0x%llx bytes=%llu extent=%ux%u", index, static_cast<unsigned long long>(color.address), static_cast<unsigned long long>(color.bytes), color.extent.width, color.extent.height);
const ColorTargetLayout colorLayout(color.extent.width, color.extent.height, color.tileMode, color.elementBytes);
static_cast<void>(FlushDepthPlanes(color.address, colorLayout.Bytes()));
static_cast<void>(FlushDepthPlanes(color.address, colorLayout.Bytes(), DepthPlaneScope::Whole));
RetireDepthSurfaces(context.device, color.address, colorLayout.Bytes());
constexpr VkBufferUsageFlags copies = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
timer.phase(PhaseSetup);
@@ -2235,7 +2264,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
const auto geometry = DescribeSurface(array);
for (std::uint32_t slice = 0; slice <= color.arrayLastSlice; ++slice) Require(geometry.GuestLayerOffset(slice) == slice * sliceBytes, "the array image's layer pitch differs from the color view's slice offset");
Require(color.surfaceAddress == color.arrayAddress + color.arraySlice * sliceBytes, "the array image's layer pitch differs from the color view's slice offset");
auto resident = CachedStorageSurface(context, array);
auto resident = CachedStorageLayers(context, array, color.arraySlice, color.layers);
Require(resident->Attachable(), "storage format cannot be a color attachment");
return resident;
}
@@ -2478,6 +2507,17 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
framebufferOwners.push_back(binding.multisampled ? binding.multisampleOwner : binding.resident);
}
auto framebuffer = pipeline->AcquireFramebuffer(targetViews, framebufferOwners, state.renderExtent, state.renderLayers);
std::vector<SampleOrderTarget> reordered;
SampleOrderPass reorderPass;
if (pipeline->ReordersSamples()) {
reorderPass = SampleOrderPassFor(context, pipelineState, pipeline->HostSampleOrder());
for (const auto& step : PlanSampleOrder(context, state, pipeline->HostSampleOrder())) {
if (step.attachment < targets.size()) reordered.push_back(MultisampleTargetOrderTarget(context, targets[step.attachment].color));
else reordered.push_back(DepthSurfaceOrderTarget(context, *state.depth));
Require(reordered.back().method == step.method, "a reordered attachment's surface disagrees with the planned way of putting its samples in order");
}
}
const auto depthLocations = passDepthLocations(context, pipelineState, *pipeline);
timer.phase(PhasePipeline);
APS5_LOG_CHARS_OUT_DEBUG("Pipeline created");
if (lean) {
@@ -2502,12 +2542,15 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
record.completion = completion;
record.waited = outcome.waited;
record.stencilReplay = stencilReplay;
record.reordered = reordered;
record.reorderPass = reorderPass;
record.depthLocations = depthLocations;
recordDraw(context, state, draw, shaders, inputs, record, outcome, timer, ownWaitedMs);
// The recipe for the caller's draw-cache entry (design_cpu_final M8, rule R3 for draws):
// only a template the resource cache serves under this content key (reusable: no lease,
// no copied writes, every direct region import- or mirror-served) of a direct draw, so a
// hit's proof is the template's ProveCurrent and nothing needs completion work.
if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && resolved.moved.empty() && !multisampled) {
if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && resolved.moved.empty() && !multisampled && !pipeline->ReordersSamples()) {
auto recipe = std::make_shared<DrawRecipe>();
recipe->device = context.device;
recipe->templateRef = resources;
@@ -2626,6 +2669,8 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
context.Resolved(&DeviceFunctions::cmdCopyBufferToImage, "vkCmdCopyBufferToImage")(commands, linear, binding.target->Image(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &copy);
imageBarrier(context, commands, binding.target->Image(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT);
}
std::vector<std::shared_ptr<void>> reorderKept;
RecordSampleOrder(context, commands, reorderPass, reordered, true, reorderKept);
APS5_LOG_OUT_DEBUG("Beginning pipeline renderExtent=%ux%u", state.renderExtent.width, state.renderExtent.height);
pipeline->Begin(commands, *framebuffer, state.renderExtent, state);
APS5_LOG_CHARS_OUT_DEBUG("Pipeline Begin OK");
@@ -2652,6 +2697,15 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
APS5_LOG_CHARS_OUT_DEBUG("Draw recorded");
if (recorded) recorder->EndPassSamples();
EndRenderPass(context, commands);
RecordSampleOrder(context, commands, reorderPass, reordered, false, reorderKept);
if (depthLocations) {
RecordDepthSampleLocations(context, commands, *depthLocations);
countBarrier(2);
reorderKept.push_back(depthLocations->keep);
}
if (recorded) {
for (auto& object : reorderKept) recorder->Keep(std::move(object));
}
APS5_LOG_CHARS_OUT_DEBUG("Render pass ended");
for (auto& binding : targets) {
if (binding.proxied) {
@@ -2834,7 +2888,8 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con
auto stored = recipe.targets[index].lock();
if (stored == nullptr || !StorageImageCached(context, stored.get()) || !StorageImageServesKeys(*stored, state.colors[index].dccAddress)) return miss(DrawRecipeMiss::TargetGone);
auto resident = refreshResidentTarget(context, state, state.colors[index], outcome, profile, [&] {
stored->Refresh();
if (state.colors[index].arrayLastSlice != 0) stored->RefreshLayers(state.colors[index].arraySlice, 1);
else stored->Refresh();
return stored;
});
timer.phase(PhaseReadTarget);
@@ -2895,6 +2950,7 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con
for (const auto& color : state.colors) record.targetSlices.emplace_back(color.arraySlice, color.arrayLastSlice != 0 ? color.layers : 1u);
record.pushBytes = &recipe.pushBytes;
record.pushStages = recipe.pushStages;
record.depthLocations = passDepthLocations(context, state, *record.pipeline);
recordDraw(context, state, draw, shaders, inputs, record, outcome, timer, ownWaitedMs);
if (profile) {
result.recordUs = std::chrono::duration<double, std::micro>(std::chrono::steady_clock::now() - recordStart).count();
@@ -2918,6 +2974,12 @@ std::shared_ptr<StorageTexture> metadataPassResident(const Context& context, con
}
void RunDepthCopyPass(const Context& context, const DepthCopyPass& pass) {
Require(pass.color.tileMode != ColorTileMode::Linear && context.detiler != nullptr, "DB depth copy into a target without a resident image is not implemented");
const auto resident = CachedStorageSurface(context, SurfaceForTarget(pass.color));
CopyDepthSample(context, pass.depthAddress, pass.samples, pass.sample, *resident, resident->StorageView(0, false), pass.extent);
}
void RunColorMetadataPass(const Context& context, const ColorMetadataPass& pass) {
if (pass.mode == ColorMetadataPass::Mode::Resolve) {
Require(pass.source.has_value() && pass.targets.size() == 1, "CB resolve without its source and destination");
@@ -232,7 +232,6 @@ struct HostImports {
PFN_vkDestroyBuffer destroyBuffer = nullptr;
PFN_vkFreeMemory freeMemory = nullptr;
std::map<std::uint64_t, HostImport> imports;
std::map<std::uint64_t, HostImport> trimmed;
std::map<VkDevice, std::vector<std::shared_ptr<RetiredImport>>> parked;
std::uint64_t liveBytes = 0;
std::map<std::uint64_t, ImportFailure> failed;
@@ -300,7 +299,7 @@ SettleBatch* forgetSettle(HostImports& state, std::uint64_t base) {
SettleBatch* running = nullptr;
if (const auto settle = state.settles.find(found->second.device); found->second.phase == SettlePhase::Submitted && settle != state.settles.end()) {
auto& batch = settle->second.batches[found->second.batch];
if (settleBatchRunning(settle->second, settle->first, batch)) running = &batch;
if (batch.busy) running = &batch;
}
state.pendingSettles.erase(found);
return running;
@@ -427,6 +426,8 @@ bool releaseSettleRetired(HostImports& state, std::uint64_t begin, std::uint64_t
++state.settleUnmapWaits;
waitSettleBatch(settle, device, batch);
}
for (const auto alias : batch.aliases) settle.settleDestroyBuffer(device, alias, nullptr);
batch.aliases.clear();
for (auto& holder : batch.retired) released.push_back(std::move(holder));
batch.retired.clear();
}
@@ -458,64 +459,6 @@ void shareDmaBufImport(const Context& context, VkBufferCreateInfo& info, ImportF
}
#endif
void releaseTrimmed(HostImports& state, ImportNode node) {
state.liveBytes -= node.mapped().pinnedBytes;
auto holder = std::make_shared<RetiredImport>(state.device, state.destroyBuffer, state.freeMemory, std::move(node));
auto* recorder = Recorder::Active();
if (recorder != nullptr) recorder->KeepThroughHold(std::move(holder));
}
void dropTrimmed(HostImports& state, std::uint64_t begin, std::uint64_t end) {
for (auto it = state.trimmed.begin(); it != state.trimmed.end();) {
const auto next = std::next(it);
if (it->first < end && begin < it->first + it->second.bytes) releaseTrimmed(state, state.trimmed.extract(it));
it = next;
}
}
void retireOrTrim(VkDevice device, HostImports& state, std::map<std::uint64_t, HostImport>::iterator it, const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) {
const auto& entry = it->second;
const auto entryEnd = entry.base + entry.bytes;
std::uint64_t first = 0;
std::uint64_t last = 0;
if (begin <= entry.base && end > entry.base && end < entryEnd) {
first = end;
last = entryEnd;
} else if (begin > entry.base && begin < entryEnd && end >= entryEnd) {
first = entry.base;
last = begin;
} else if (begin > entry.base && begin < end && end < entryEnd) {
const bool tail = entryEnd - end >= begin - entry.base;
first = tail ? end : entry.base;
last = tail ? entryEnd : begin;
}
const bool plain = entry.alias == nullptr && !entry.dmaBuf && entry.chunk == nullptr && !entry.joined && entry.ranges.size() == 1 && entry.backingSerial != 0 && entry.memoryType != std::numeric_limits<std::uint32_t>::max() && entry.base == entry.range->address && entry.bytes == entry.range->bytes;
#ifdef _WIN32
const bool settling = state.pendingSettles.contains(it->first);
#else
const bool settling = false;
#endif
if (first == last || !plain || settling || state.trimmed.contains(first)) {
retireImport(device, state, it, lease, false);
return;
}
RetireShadow(device, entry, [&lease](std::uint64_t from, std::uint64_t to) { return runCovers(lease, from, to); });
++state.epoch;
auto node = state.imports.extract(it);
auto& kept = node.mapped();
std::shared_ptr<void> holder(nullptr, [device = state.device, destroy = state.destroyBuffer, buffer = std::exchange(kept.buffer, VK_NULL_HANDLE)](void*) { destroy(device, buffer, nullptr); });
kept.range.reset();
kept.ranges.clear();
kept.base = first;
kept.bytes = last - first;
kept.address = 0;
kept.serial = 0;
node.key() = first;
state.trimmed.insert(std::move(node));
auto* recorder = Recorder::Active();
if (recorder != nullptr && !recorder->Idle()) recorder->Keep(std::move(holder));
}
VkResult createImportBuffer(const Context& context, VkExternalMemoryHandleTypeFlagBits handleType, VkDeviceSize size, VkBuffer* buffer) {
const VkExternalMemoryBufferCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO, nullptr, static_cast<VkExternalMemoryHandleTypeFlags>(handleType)};
VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, &external};
@@ -1407,9 +1350,6 @@ bool sameRun(const Entry& entry, const ImportRun& run) {
bool wouldRetireImport(const HostImports& state, const ImportRun& run) {
if (run.ranges.empty()) return false;
for (const auto& [base, entry] : state.trimmed) {
if (base < run.base + run.bytes && run.base < base + entry.bytes) return true;
}
const auto next = state.imports.lower_bound(run.base);
if (next != state.imports.end()) {
if (next->first == run.base && sameRun(next->second, run)) return false;
@@ -1686,7 +1626,6 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import
if (sameRun(failed->second, run)) return nullptr;
failures.erase(failed);
}
dropTrimmed(state, base, base + bytes);
const auto budget = effectiveBudget(context, state);
const auto containing = std::find_if(run.ranges.begin(), run.ranges.end(), [address](const auto& range) { return address >= range->address && address < range->address + range->bytes; });
if (const auto remembered = state.evictingRefusals.find({base, bytes}); evict && remembered != state.evictingRefusals.end()) {
@@ -1920,46 +1859,6 @@ const GuestAllocations::Range* containingRange(const GuestAllocations::Lease& le
return range->readable && begin >= range->address && end <= range->address + range->bytes ? range.get() : nullptr;
}
bool reattachTrimmed(const Context& context, HostImports& state, HostImport& entry, const GuestAllocations::Lease& lease) {
const auto first = entry.base;
const auto last = entry.base + entry.bytes;
std::shared_ptr<const GuestAllocations::Range> kept;
for (auto it = std::lower_bound(lease.begin(), lease.end(), first, [](const auto& range, std::uint64_t value) { return range->address < value; }); it != lease.end() && (*it)->address < last; ++it) {
if ((*it)->backingSerial == entry.backingSerial && (*it)->address + (*it)->bytes <= last && (kept == nullptr || (*it)->bytes > kept->bytes)) kept = *it;
}
if (kept == nullptr) return false;
const auto next = state.imports.lower_bound(kept->address);
if (next != state.imports.end() && next->first < kept->address + kept->bytes) return false;
if (next != state.imports.begin() && std::prev(next)->second.base + std::prev(next)->second.bytes > kept->address) return false;
HostImport candidate{kept->address, kept->bytes, VK_NULL_HANDLE, VK_NULL_HANDLE, 0};
candidate.range = kept;
candidate.ranges = {kept};
if (!sameRange(candidate, lease)) return false;
const auto offset = kept->address - entry.memoryBase;
VkBuffer buffer = VK_NULL_HANDLE;
if (createImportBuffer(context, VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT, kept->bytes, &buffer) != VK_SUCCESS) return false;
const auto refuse = [&] {
context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer")(context.device, buffer, nullptr);
return false;
};
VkMemoryRequirements requirements{};
context.Function<PFN_vkGetBufferMemoryRequirements>("vkGetBufferMemoryRequirements")(context.device, buffer, &requirements);
if (((requirements.memoryTypeBits >> entry.memoryType) & 1u) == 0 || requirements.alignment == 0 || offset % requirements.alignment != 0 || requirements.size > entry.pinnedBytes - offset) return refuse();
if (context.Function<PFN_vkBindBufferMemory>("vkBindBufferMemory")(context.device, buffer, entry.memory, offset) != VK_SUCCESS) return refuse();
const VkBufferDeviceAddressInfo addressInfo{VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO, nullptr, buffer};
const auto address = context.Function<PFN_vkGetBufferDeviceAddressKHR>("vkGetBufferDeviceAddressKHR")(context.device, &addressInfo);
if (address == 0) return refuse();
entry.base = kept->address;
entry.bytes = kept->bytes;
entry.buffer = buffer;
entry.address = address;
entry.range = kept;
entry.ranges = {kept};
entry.serial = 0;
entry.lastUse = ++state.useCounter;
return true;
}
// Drops imports whose registered range changed or disappeared: their pages may no longer back the
// guest addresses. Walks the imports only when the registry changed since the last walk.
void refreshImports(const Context& context, HostImports& state, const GuestAllocations::Lease& lease) {
@@ -1967,17 +1866,15 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc
#ifdef _WIN32
drainSettles(state);
#endif
for (auto* held : {&state.imports, &state.trimmed}) {
while (!held->empty()) {
auto node = held->extract(held->begin());
if (state.device != VK_NULL_HANDLE && state.destroyBuffer != nullptr && state.freeMemory != nullptr) {
state.parked[state.device].push_back(std::make_shared<RetiredImport>(state.device, state.destroyBuffer, state.freeMemory, std::move(node)));
continue;
}
#ifdef _WIN32
GuestArena::GuestArenaUnmapAlias_nid_postfix(node.mapped().alias);
#endif
while (!state.imports.empty()) {
auto node = state.imports.extract(state.imports.begin());
if (state.device != VK_NULL_HANDLE && state.destroyBuffer != nullptr && state.freeMemory != nullptr) {
state.parked[state.device].push_back(std::make_shared<RetiredImport>(state.device, state.destroyBuffer, state.freeMemory, std::move(node)));
continue;
}
#ifdef _WIN32
GuestArena::GuestArenaUnmapAlias_nid_postfix(node.mapped().alias);
#endif
}
state.liveBytes = 0;
state.failed.clear();
@@ -2008,16 +1905,6 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc
};
std::erase_if(state.sparseRunEnds, [&](const auto& entry) { return !starts(entry.first); });
std::erase_if(state.sparseRunJoins, [&](const auto& entry) { return !starts(entry.first); });
while (!state.trimmed.empty()) {
auto node = state.trimmed.extract(state.trimmed.begin());
if (!reattachTrimmed(context, state, node.mapped(), lease)) {
releaseTrimmed(state, std::move(node));
continue;
}
node.key() = node.mapped().base;
++state.epoch;
state.imports.insert(std::move(node));
}
const auto budget = effectiveBudget(context, state);
for (auto it = state.imports.begin(); it != state.imports.end();) {
if (sameRange(it->second, lease, budget)) {
@@ -2857,6 +2744,7 @@ bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) {
bool synced = false;
bool drained = false;
bool retiredImport = false;
bool droppedAddressSpace = false;
// A range pinned only by the cached address space is released by dropping the cache's
// reference: no GPU wait, no device lock (the mutating thread may be a driver thread holding
// it). The registry rescans; a range still pinned by a build or a batch holding the space comes
@@ -2865,12 +2753,24 @@ bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) {
Spaces().droppedByWaiter.store(true, std::memory_order_relaxed);
Spaces().waiterDrops.fetch_add(1, std::memory_order_relaxed);
dropped.reset();
auto& state = Leases();
std::lock_guard lock(state.mutex);
++state.stats.contentionWaits;
++state.stats.cacheDrops;
state.stats.contentionMs += std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
return true;
droppedAddressSpace = true;
auto& imports = Imports();
const auto end = static_cast<std::uint64_t>(address) + bytes;
bool overlapsImport = false;
{
std::lock_guard importsLock(imports.mutex);
const auto next = imports.imports.lower_bound(address);
const auto previous = next == imports.imports.begin() ? imports.imports.end() : std::prev(next);
overlapsImport = (next != imports.imports.end() && next->first < end) || (previous != imports.imports.end() && previous->first + previous->second.bytes > address);
}
if (!overlapsImport) {
auto& state = Leases();
std::lock_guard lock(state.mutex);
++state.stats.contentionWaits;
++state.stats.cacheDrops;
state.stats.contentionMs += std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
return true;
}
}
if (Recorder::InCompletion()) throw std::runtime_error("guest allocation mutation cannot wait for an active GPU completion");
if (GuestMemory::GpuMutex().HeldByThisThread()) {
@@ -2895,7 +2795,7 @@ bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) {
continue;
}
const auto next = std::next(it);
retireOrTrim(imports.device, imports, it, lease, address, end);
retireImport(imports.device, imports, it, lease, false);
it = next;
retiredImport = true;
}
@@ -2961,7 +2861,7 @@ bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) {
continue;
}
const auto next = std::next(it);
retireOrTrim(imports.device, imports, it, lease, address, end);
retireImport(imports.device, imports, it, lease, false);
it = next;
retiredImport = true;
}
@@ -2980,6 +2880,7 @@ bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) {
auto& state = Leases();
std::lock_guard lock(state.mutex);
++state.stats.contentionWaits;
if (droppedAddressSpace) ++state.stats.cacheDrops;
if (synced) ++state.stats.contentionSyncs;
if (drained) ++state.stats.contentionDrains;
state.stats.contentionMs += std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
@@ -3141,9 +3042,7 @@ void ClearHostImports(VkDevice device) {
GuestArena::GuestArenaUnmapAlias_nid_postfix(entry.alias);
#endif
}
for (const auto& [address, entry] : state.trimmed) state.freeMemory(state.device, entry.memory, nullptr);
state.imports.clear();
state.trimmed.clear();
state.liveBytes = 0;
state.failed.clear();
state.failedJoins.clear();
@@ -3436,6 +3335,13 @@ std::uint64_t HostImportLiveBytes() {
return state.liveBytes;
}
std::uint64_t HostImportBudgetRemaining(const Context& context) {
auto& state = Imports();
std::lock_guard lock(state.mutex);
const auto budget = effectiveBudget(context, state);
return budget > state.liveBytes ? budget - state.liveBytes : 0;
}
const char* ImportRequesterName(ImportRequester requester) {
switch (requester) {
case ImportRequester::Map: return "map";
@@ -4636,12 +4542,6 @@ void GuestBufferMemory::UploadFinish(bool addressable) {
if (profile) refreshUs += microsecondsSince(refreshStart);
continue;
}
if (sparseHeapCandidate(region)) {
const auto refreshStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
const bool bound = bindSparseMirror(region, importRanges());
if (profile) refreshUs += microsecondsSince(refreshStart);
if (bound) continue;
}
bool copyOnGpu = false;
if (context.hostImportAlignment != 0) {
auto& state = Imports();
@@ -4728,6 +4628,12 @@ void GuestBufferMemory::UploadFinish(bool addressable) {
gpuCopies.push_back(&region);
continue;
}
if (sparseHeapCandidate(region)) {
const auto refreshStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
const bool bound = bindSparseMirror(region, importRanges());
if (profile) refreshUs += microsecondsSince(refreshStart);
if (bound) continue;
}
if (region.deviceLocal) {
// The staging shadow UploadPrepare made cannot take the CPU copy (no host mapping): the
// region falls back to a host buffer of its own.
@@ -6,6 +6,7 @@
#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
@@ -85,10 +86,13 @@ public:
context.formatProperties(context.physical, target.format, &properties);
Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0, "multisampled render-target format cannot be a color attachment");
usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
orderFormat = MayReorderSamples(context, target.samples) ? SampleOrderStorageFormat(context, target.format, target.elementBytes, target.samples) : VK_FORMAT_UNDEFINED;
const VkImageCreateFlags orderFlags = orderFormat != VK_FORMAT_UNDEFINED ? VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT : 0u;
if (orderFormat != VK_FORMAT_UNDEFINED) usage |= VK_IMAGE_USAGE_STORAGE_BIT;
VkImageFormatProperties supported{};
sampleable = (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0 && context.imageFormatProperties(context.physical, target.format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage | VK_IMAGE_USAGE_SAMPLED_BIT, 0, &supported) == VK_SUCCESS && (supported.sampleCounts & samples) != 0;
sampleable = (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0 && context.imageFormatProperties(context.physical, target.format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage | VK_IMAGE_USAGE_SAMPLED_BIT, orderFlags, &supported) == VK_SUCCESS && (supported.sampleCounts & samples) != 0;
if (sampleable) usage |= VK_IMAGE_USAGE_SAMPLED_BIT;
Check(context.imageFormatProperties(context.physical, target.format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, 0, &supported), "vkGetPhysicalDeviceImageFormatProperties multisampled target");
Check(context.imageFormatProperties(context.physical, target.format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, orderFlags, &supported), "vkGetPhysicalDeviceImageFormatProperties multisampled target");
Require((supported.sampleCounts & samples) != 0 && target.extent.width <= supported.maxExtent.width && target.extent.height <= supported.maxExtent.height, "multisampled render target exceeds device image limits");
storageFormat = HostStorageFormat(context, target.format);
storageFlags = storageFormat != target.format ? VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT : 0u;
@@ -98,7 +102,7 @@ public:
};
if (storageFormat != VK_FORMAT_UNDEFINED && (!context.storageImageMultisample || !storageSupports(storageFormat, VK_IMAGE_USAGE_STORAGE_BIT, 0u) || !storageSupports(target.format, usage | VK_IMAGE_USAGE_STORAGE_BIT, storageFlags))) storageFormat = VK_FORMAT_UNDEFINED;
try {
const auto created = allocate(usage, 0u);
const auto created = allocate(usage, orderFlags);
image = created.image;
memory = created.memory;
view = created.view;
@@ -155,6 +159,9 @@ public:
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = target.format;
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
VkImageViewUsageCreateInfo viewUsage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO};
viewUsage.usage = imageUsage & ~VK_IMAGE_USAGE_STORAGE_BIT;
if (orderFormat != VK_FORMAT_UNDEFINED) viewInfo.pNext = &viewUsage;
Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &created.view), "vkCreateImageView multisampled target");
} catch (...) {
destroy(created);
@@ -285,6 +292,21 @@ public:
return (usage & VK_IMAGE_USAGE_STORAGE_BIT) != 0;
}
VkImageView OrderView() {
Require(orderFormat != VK_FORMAT_UNDEFINED, "the multisampled color target has no storage view to put its samples in the guest's order");
if (orderView != VK_NULL_HANDLE) return orderView;
VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
viewInfo.image = image;
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = orderFormat;
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
VkImageViewUsageCreateInfo viewUsage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO};
viewUsage.usage = VK_IMAGE_USAGE_STORAGE_BIT;
viewInfo.pNext = &viewUsage;
Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &orderView), "vkCreateImageView multisampled target storage");
return orderView;
}
void NoteMetadataFill(std::uint64_t fillAddress, std::size_t fillBytes, std::uint32_t pattern) {
const auto note = [&](std::uint64_t address, std::uint64_t bytes, std::optional<std::uint32_t>& filled) {
if (address == 0 || fillAddress >= address + bytes || address >= fillAddress + fillBytes) return;
@@ -310,7 +332,7 @@ public:
});
const std::array<std::uint32_t, 5> key{static_cast<std::uint32_t>(components.r), static_cast<std::uint32_t>(components.g), static_cast<std::uint32_t>(components.b), static_cast<std::uint32_t>(components.a), static_cast<std::uint32_t>(viewType)};
auto& texture = textures[key];
if (texture == nullptr) texture = std::make_shared<Texture>(context, image, target.format, VK_IMAGE_ASPECT_COLOR_BIT, components, viewType);
if (texture == nullptr) texture = std::make_shared<Texture>(context, image, target.format, VK_IMAGE_ASPECT_COLOR_BIT, components, viewType, 0, std::shared_ptr<OwnedImage>{}, orderFormat != VK_FORMAT_UNDEFINED ? VK_IMAGE_USAGE_SAMPLED_BIT : VkImageUsageFlags{0});
return {shared_from_this(), texture.get()};
}
@@ -358,6 +380,8 @@ public:
MultisampledCmask cmask;
MultisampledColorFill colorFill;
bool fromTexture = false;
VkFormat orderFormat = VK_FORMAT_UNDEFINED;
VkImageView orderView = VK_NULL_HANDLE;
std::uint64_t htileAddress = 0;
std::uint64_t htileBytes = 0;
std::string htileRefusal;
@@ -464,6 +488,7 @@ private:
textures.clear();
retiredTextures.clear();
if (resolvePass) context.Function<PFN_vkDestroyRenderPass>("vkDestroyRenderPass")(context.device, resolvePass, nullptr);
if (orderView) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, orderView, nullptr);
if (storageView) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, storageView, nullptr);
destroy({image, memory, view});
for (const auto& allocation : retired) destroy(allocation);
@@ -506,7 +531,38 @@ void guardReads(std::uint64_t address, std::size_t bytes) {
RequireNoMultisampleTarget(address, bytes, "a CPU read");
}
struct RecordedSingleClear {
std::uint64_t surface = 0;
std::uint32_t pipeBankXor = 0;
std::uint32_t width = 0;
std::uint32_t height = 0;
std::uint32_t samples = 0;
std::array<std::uint32_t, 4> color{};
};
std::mutex& singleClearsMutex() {
static std::mutex mutex;
return mutex;
}
std::vector<RecordedSingleClear>& singleClears() {
static std::vector<RecordedSingleClear> list;
return list;
}
std::atomic<bool>& anySingleClears() {
static std::atomic<bool> any{false};
return any;
}
void forgetSingleClears(std::uintptr_t address, std::size_t bytes) {
std::lock_guard lock(singleClearsMutex());
std::erase_if(singleClears(), [&](const RecordedSingleClear& clear) { return clear.surface >= address && clear.surface - address < bytes; });
anySingleClears().store(!singleClears().empty(), std::memory_order_release);
}
void retireUnmapped(std::uintptr_t address, std::size_t bytes) {
forgetSingleClears(address, bytes);
RetireUnmappedDepth_nid_no_patch(address, bytes);
std::unique_lock lock(guardedMutex());
std::erase_if(guarded(), [&](const auto& range) {
@@ -677,7 +733,7 @@ void InstallResidentImageReadGuard_nid_no_patch() {
void UpdateResidentImageUnmapObserver_nid_no_patch() {
static std::mutex mutex;
std::lock_guard lock(mutex);
GuestAllocations::GuestAllocationsSetUnmapObserver_nid_postfix(anyGuarded().load(std::memory_order_acquire) || HasResidentDepthGuards_nid_no_patch() ? &retireUnmapped : nullptr);
GuestAllocations::GuestAllocationsSetUnmapObserver_nid_postfix(anyGuarded().load(std::memory_order_acquire) || anySingleClears().load(std::memory_order_acquire) || HasResidentDepthGuards_nid_no_patch() ? &retireUnmapped : nullptr);
}
std::optional<DccKeys> MultisampledCmaskKeys(std::uint8_t code) {
@@ -755,6 +811,61 @@ std::size_t MultisampledDccBytes(const ColorTarget& target) {
return static_cast<std::size_t>(((target.extent.width + width - 1) / width) * ((target.extent.height + height - 1) / height) * 4096u);
}
namespace {
std::optional<std::array<std::uint32_t, 4>> recordedSingleClear(const ColorTarget& target) {
std::lock_guard lock(singleClearsMutex());
for (const auto& clear : singleClears()) {
if (clear.surface == target.surfaceAddress && clear.pipeBankXor == target.pipeBankXor && clear.width == target.extent.width && clear.height == target.extent.height && clear.samples == target.samples) return clear.color;
}
return std::nullopt;
}
VkClearColorValue singleClearColor(const ColorTarget& target) {
const auto color = recordedSingleClear(target);
Require(color.has_value(), "multisampled DCC comp-to-single keys without a recorded clear color are not implemented");
const auto elementBytes = static_cast<std::size_t>(target.elementBytes);
Require(elementBytes == 1 || elementBytes == 2 || elementBytes == 4 || elementBytes == 8 || elementBytes == 16, "multisampled DCC comp-to-single clears of this texel size are not implemented");
std::array<std::uint32_t, 4> pattern{};
for (std::size_t offset = 0; offset < sizeof(pattern); offset += elementBytes) std::memcpy(reinterpret_cast<std::byte*>(pattern.data()) + offset, color->data(), elementBytes);
VkClearColorValue clear{};
Require(ClearColorForTexel(target.format, target.elementBytes, pattern, clear), "multisampled DCC comp-to-single clear has no exact host value");
return clear;
}
}
std::optional<MultisampledSingleClear> MatchMultisampledSingleClear(std::span<const std::uint32_t> code, std::span<const std::uint32_t> userData, const std::array<std::uint32_t, 3>& numThreads) {
static constexpr std::array<std::uint32_t, 22> kernel{0xbe800303u, 0xbe830382u, 0xf40c0401u, 0xfa000000u, 0xd7460000u, 0x04010609u, 0xd7460001u, 0x0405060au, 0x7e060208u, 0x7e0c020bu, 0x7e0e0280u, 0xd5690004u, 0x02000100u, 0x7e000205u, 0xd5690005u, 0x02000901u, 0xd7000001u, 0x02000c80u, 0xbf8cc07fu, 0xf0201f38u, 0x00040004u, 0xbf810000u};
if (userData.size() < 9 || numThreads != std::array<std::uint32_t, 3>{8u, 8u, 1u}) return std::nullopt;
if (code.size() < kernel.size() || !std::equal(kernel.begin(), kernel.end(), code.begin())) return std::nullopt;
if (userData[3] == 0 || userData[4] == 0) return std::nullopt;
return MultisampledSingleClear{userData[2] | (std::uint64_t{2} << 32u), {userData[5], userData[6], userData[7], userData[8]}};
}
void NoteMultisampledSingleClear(const GuestTextureResource& resource, const std::array<std::uint32_t, 4>& color) {
{
std::lock_guard lock(singleClearsMutex());
auto& list = singleClears();
std::erase_if(list, [&](const RecordedSingleClear& clear) { return clear.surface == resource.baseAddress; });
list.push_back({resource.baseAddress, resource.pipeBankXor, resource.width, resource.height, resource.samples, color});
anySingleClears().store(true, std::memory_order_release);
}
UpdateResidentImageUnmapObserver_nid_no_patch();
}
bool TakeMultisampledSingleClear(std::span<const std::uint32_t> code, std::span<const std::uint32_t> userData, const std::array<std::uint32_t, 3>& numThreads) {
const auto clear = MatchMultisampledSingleClear(code, userData, numThreads);
if (!clear) return false;
std::array<std::uint32_t, 8> words{};
if (!GuestMemory::Accessible(reinterpret_cast<const void*>(clear->descriptor), sizeof(words))) return false;
GuestMemory::Read(clear->descriptor, std::as_writable_bytes(std::span(words)));
const auto resource = DecodeTextureResource(words);
if (resource.samples < 2) return false;
NoteMultisampledSingleClear(resource, clear->color);
return true;
}
VkClearColorValue MultisampledDccClear(const ColorTarget& target, std::span<const std::byte> dcc, std::span<const std::byte> cmask) {
Require(target.dccAddress != 0 && (target.dccControl & 0x3ff80u) == 0 && (target.pipeBankXor & ~0xcf00u) == 0, "multisampled DCC address, color transform, lossy mode or XOR is not implemented");
Require(dcc.size() == MultisampledDccBytes(target), "multisampled DCC metadata snapshot size mismatch");
@@ -765,6 +876,7 @@ VkClearColorValue MultisampledDccClear(const ColorTarget& target, std::span<cons
const auto expanded = target.samples == 2 ? 0xddu : target.samples == 4 ? 0xeeu : 0xffu;
Require(std::ranges::all_of(cmask, [&](std::byte key) { return key == cmask.front(); }) && (code == 0xccu || code == expanded), "multisampled DCC CMASK compression or register clear is not implemented");
}
if (dcc.front() == std::byte{0x10}) return singleClearColor(target);
DccKeys keys;
switch (std::to_integer<std::uint32_t>(dcc.front())) {
case 0x00u: keys = DccKeys::Clear0000; break;
@@ -836,6 +948,12 @@ bool TakeMultisampledClear(MultisampledCmask& cmask, MultisampledColorFill& colo
return true;
}
SampleOrderTarget MultisampleTargetOrderTarget(const Context& context, const ColorTarget& target) {
std::lock_guard lock(targetsMutex());
auto& found = acquire(context, target);
return {found.image, found.OrderView(), found.orderFormat, target.extent, target.samples, SampleOrderMethod::Storage, found.shared_from_this()};
}
VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target, VkImage* image) {
std::lock_guard lock(targetsMutex());
const auto& acquired = acquire(context, target);
@@ -0,0 +1,120 @@
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTextureLayout.hpp"
#include "prx/libSceAgcDriver/Graphics/include/MultisampleSwizzleEquations.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Context.hpp"
#include <bit>
#include <cstring>
#include <limits>
#include <utility>
namespace AgcDriver::Graphics {
namespace {
std::array<std::uint32_t, 16> inverseBlock(const MultisampleSwizzleEquation& equation, std::uint32_t byteBits, std::uint32_t fragmentBits, std::uint32_t widthBits) {
std::array<std::uint32_t, 16> rows{}, inverse{};
for (std::uint32_t input = 0; input < 16u; ++input) {
std::uint32_t address = 0u;
if (input < byteBits) address = 1u << input;
else {
const auto coordinateBit = input < byteBits + fragmentBits ? 48u + input - byteBits : input < byteBits + fragmentBits + widthBits ? input - byteBits - fragmentBits : 16u + input - byteBits - fragmentBits - widthBits;
const auto coordinate = std::uint64_t{1} << coordinateBit;
for (std::uint32_t output = 0; output < 16u; ++output) address |= static_cast<std::uint32_t>(std::popcount(coordinate & equation.bits[output]) & 1u) << output;
}
for (std::uint32_t output = 0; output < 16u; ++output) rows[output] |= ((address >> output) & 1u) << input;
inverse[input] = 1u << input;
}
for (std::uint32_t input = 0; input < 16u; ++input) {
auto pivot = input;
while (pivot < 16u && (rows[pivot] & (1u << input)) == 0u) ++pivot;
Require(pivot < 16u, "multisample block address equation is not invertible");
std::swap(rows[input], rows[pivot]);
std::swap(inverse[input], inverse[pivot]);
for (std::uint32_t row = 0; row < 16u; ++row) {
if (row == input || (rows[row] & (1u << input)) == 0u) continue;
rows[row] ^= rows[input];
inverse[row] ^= inverse[input];
}
}
return inverse;
}
std::vector<std::uint32_t> axisOffsets(const MultisampleSwizzleEquation& equation, std::uint32_t count, std::uint32_t shift) {
std::vector<std::uint32_t> offsets(count);
for (std::uint32_t value = 0; value < count; ++value) {
const auto coordinate = static_cast<std::uint64_t>(value) << shift;
for (std::uint32_t bit = 0; bit < equation.bits.size(); ++bit) {
offsets[value] |= static_cast<std::uint32_t>(std::popcount(coordinate & equation.bits[bit]) & 1u) << bit;
}
}
return offsets;
}
}
MultisampleTextureLayout::MultisampleTextureLayout(TextureTileMode tileMode, std::uint32_t elementBytes, std::uint32_t fragments, std::uint32_t width, std::uint32_t height, std::uint32_t layers, std::uint32_t pipeBankXor) : elementBytes(elementBytes), fragments(fragments), width(width), height(height), layers(layers), pipeBankXor(pipeBankXor) {
Require(width != 0 && height != 0 && width <= 16384u && height <= 16384u && layers != 0 && layers <= 8192u, "invalid multisampled surface extent or layer count");
Require(std::has_single_bit(elementBytes) && elementBytes <= 16u, "unsupported multisampled surface element size");
Require(fragments == 2u || fragments == 4u || fragments == 8u, "unsupported multisampled surface fragment count");
Require((pipeBankXor & ~0xcf00u) == 0, "multisampled surface pipe/bank XOR uses bits outside its 16-pipe swizzle");
Require(tileMode != TextureTileMode::kZ64KBX || elementBytes <= 4u, "multisampled Z surface element size exceeds 32 bits");
const auto* equation = FindMultisampleSwizzleEquation(tileMode, elementBytes, fragments);
Require(equation != nullptr, "unsupported multisampled surface tile mode");
const auto fragmentBits = static_cast<std::uint32_t>(std::countr_zero(fragments));
const auto elementBits = 16u - static_cast<std::uint32_t>(std::countr_zero(elementBytes)) - fragmentBits;
const auto widthBits = (elementBits + ((fragmentBits & 1u) == 0 ? 1u : 0u)) / 2u;
blockWidth = 1u << widthBits;
blockHeight = 1u << (elementBits - widthBits);
inverseBlockEquation = inverseBlock(*equation, static_cast<std::uint32_t>(std::countr_zero(elementBytes)), fragmentBits, widthBits);
pitch = (width + blockWidth - 1u) / blockWidth * blockWidth;
paddedHeight = (height + blockHeight - 1u) / blockHeight * blockHeight;
sliceBytes = static_cast<std::uint64_t>(pitch) * paddedHeight * fragments * elementBytes;
Require(sliceBytes <= std::numeric_limits<std::uint64_t>::max() / layers, "multisampled surface size overflows");
xOffsets = axisOffsets(*equation, width, 0u);
yOffsets = axisOffsets(*equation, height, 16u);
layerOffsets = axisOffsets(*equation, layers, 32u);
fragmentOffsets = axisOffsets(*equation, fragments, 48u);
}
std::uint64_t MultisampleTextureLayout::offset(std::uint32_t x, std::uint32_t y, std::uint32_t layer, std::uint32_t fragment) const {
const auto block = static_cast<std::uint64_t>(y / blockHeight) * (pitch / blockWidth) + x / blockWidth;
const auto withinBlock = xOffsets[x] ^ yOffsets[y] ^ layerOffsets[layer] ^ fragmentOffsets[fragment] ^ pipeBankXor;
return static_cast<std::uint64_t>(layer) * sliceBytes + block * Alignment + withinBlock;
}
std::uint64_t MultisampleTextureLayout::Offset(std::uint32_t x, std::uint32_t y, std::uint32_t layer, std::uint32_t fragment) const {
Require(x < width && y < height && layer < layers && fragment < fragments, "multisampled surface coordinate is outside its extent");
return offset(x, y, layer, fragment);
}
void MultisampleTextureLayout::transfer(std::span<const std::byte> source, std::span<std::byte> destination, bool retile) const {
const auto sourceBytes = retile ? LinearBytes() : Bytes();
const auto destinationBytes = retile ? Bytes() : LinearBytes();
Require(source.size() >= sourceBytes && destination.size() >= destinationBytes, "multisampled surface transfer buffer is too small");
const auto sourceAddress = reinterpret_cast<std::uintptr_t>(source.data());
const auto destinationAddress = reinterpret_cast<std::uintptr_t>(destination.data());
Require(sourceAddress <= std::numeric_limits<std::uintptr_t>::max() - sourceBytes && destinationAddress <= std::numeric_limits<std::uintptr_t>::max() - destinationBytes, "multisampled surface transfer range overflows");
Require(sourceAddress + sourceBytes <= destinationAddress || destinationAddress + destinationBytes <= sourceAddress, "multisampled surface transfer buffers overlap");
std::uint64_t linear = 0;
for (std::uint32_t layer = 0; layer < layers; ++layer) {
for (std::uint32_t y = 0; y < height; ++y) {
for (std::uint32_t x = 0; x < width; ++x) {
for (std::uint32_t fragment = 0; fragment < fragments; ++fragment) {
const auto tiled = offset(x, y, layer, fragment);
std::memcpy(destination.data() + (retile ? tiled : linear), source.data() + (retile ? linear : tiled), elementBytes);
linear += elementBytes;
}
}
}
}
}
void MultisampleTextureLayout::Detile(std::span<const std::byte> source, std::span<std::byte> destination) const {
transfer(source, destination, false);
}
void MultisampleTextureLayout::Tile(std::span<const std::byte> source, std::span<std::byte> destination) const {
transfer(source, destination, true);
}
}
@@ -0,0 +1,68 @@
#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp"
#include "prx/libSceAgcDriver/Graphics/shaders/MultisampleUpload_spv.h"
#include <algorithm>
#include <array>
#include <bit>
#include <limits>
namespace AgcDriver::Graphics {
VkPipeline TextureDetiler::multisampleUploadPipeline(std::uint32_t elementBytes) {
const auto key = (1u << 19u) | elementBytes;
for (const auto& entry : pipelines) if (entry.first == key) return entry.second;
if (multisampleUploadModule == VK_NULL_HANDLE) {
VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
info.codeSize = sizeof(MULTISAMPLE_UPLOAD_SPV);
info.pCode = MULTISAMPLE_UPLOAD_SPV;
Check(context.Function<PFN_vkCreateShaderModule>("vkCreateShaderModule")(context.device, &info, nullptr, &multisampleUploadModule), "vkCreateShaderModule multisample upload");
}
const VkSpecializationMapEntry entry{0u, 0u, sizeof(elementBytes)};
const VkSpecializationInfo specialization{1u, &entry, sizeof(elementBytes), &elementBytes};
VkComputePipelineCreateInfo info{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO};
info.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO};
info.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT;
info.stage.module = multisampleUploadModule;
info.stage.pName = "main";
info.stage.pSpecializationInfo = &specialization;
info.layout = imagePipelineLayout;
pipelines.reserve(pipelines.size() + 1u);
VkPipeline result = VK_NULL_HANDLE;
{
auto pipelineCacheLock = context.LockPipelineCacheForUse();
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1u, &info, nullptr, &result), "vkCreateComputePipelines multisample upload");
}
pipelines.emplace_back(key, result);
return result;
}
void TextureDetiler::DispatchMultisampleImage(VkCommandBuffer commands, std::uint32_t elementBytes, VkBuffer source, std::uint64_t offset, std::uint64_t bytes, VkImageView arrayView, std::uint32_t width, std::uint32_t height, std::uint32_t layers, std::uint32_t fragments, std::uint32_t firstElement, std::uint32_t elements) {
Require(context.storageImageMultisample && context.storageImageWriteWithoutFormat, "device does not enable multisampled storage image writes without format");
Require(commands != VK_NULL_HANDLE && source != VK_NULL_HANDLE && arrayView != VK_NULL_HANDLE, "multisample upload requires commands, buffer and array view");
Require(std::has_single_bit(elementBytes) && elementBytes <= 16u, "multisample upload element size is unsupported");
Require(width != 0u && width <= 16384u && height != 0u && height <= 16384u && layers != 0u && layers <= 8192u, "multisample upload extent is unsupported");
Require(fragments == 2u || fragments == 4u || fragments == 8u, "multisample upload fragment count is unsupported");
const auto total = static_cast<std::uint64_t>(width) * height * layers * fragments;
Require(total <= std::numeric_limits<std::uint32_t>::max() && elements != 0u && static_cast<std::uint64_t>(firstElement) + elements <= total, "multisample upload element range is invalid");
const auto required = (static_cast<std::uint64_t>(elements) * elementBytes + 3u) & ~3ull;
const auto alignment = std::max<std::uint64_t>(4u, context.limits.minStorageBufferOffsetAlignment);
Require(offset % alignment == 0u && bytes >= required && bytes <= context.limits.maxStorageBufferRange && bytes % 4u == 0u && offset <= std::numeric_limits<std::uint64_t>::max() - bytes, "multisample upload buffer range is invalid");
Require(context.limits.maxComputeWorkGroupInvocations >= 64u && context.limits.maxComputeWorkGroupSize[0] >= 64u && context.limits.maxComputeWorkGroupSize[1] >= 1u && context.limits.maxComputeWorkGroupSize[2] >= 1u && context.limits.maxComputeWorkGroupCount[2] >= 1u, "device does not support the multisample upload workgroup");
const auto groups = (static_cast<std::uint64_t>(elements) + 63u) / 64u;
const auto groupsX = std::min<std::uint64_t>(groups, context.limits.maxComputeWorkGroupCount[0]);
Require(groupsX != 0u, "device has no multisample upload workgroups");
const auto groupsY = (groups + groupsX - 1u) / groupsX;
Require(groupsY <= context.limits.maxComputeWorkGroupCount[1] && groupsX * groupsY * 64u <= std::numeric_limits<std::uint32_t>::max(), "multisample upload dispatch exceeds the device limits");
const auto target = multisampleUploadPipeline(elementBytes);
const auto set = allocateSet(true);
const VkDescriptorBufferInfo buffer{source, offset, bytes};
const VkDescriptorImageInfo image{VK_NULL_HANDLE, arrayView, VK_IMAGE_LAYOUT_GENERAL};
const std::array<VkWriteDescriptorSet, 2> writes{{{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, set, 0u, 0u, 1u, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, nullptr, &buffer, nullptr}, {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, set, 1u, 0u, 1u, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, &image, nullptr, nullptr}}};
context.Function<PFN_vkUpdateDescriptorSets>("vkUpdateDescriptorSets")(context.device, 2u, writes.data(), 0u, nullptr);
context.Function<PFN_vkCmdBindPipeline>("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, target);
context.Function<PFN_vkCmdBindDescriptorSets>("vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, imagePipelineLayout, 0u, 1u, &set, 0u, nullptr);
const std::array<std::uint32_t, 7> push{width, height, layers, fragments, firstElement, elements, static_cast<std::uint32_t>(groupsX * 64u)};
context.Function<PFN_vkCmdPushConstants>("vkCmdPushConstants")(commands, imagePipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0u, sizeof(push), push.data());
context.Function<PFN_vkCmdDispatch>("vkCmdDispatch")(commands, static_cast<std::uint32_t>(groupsX), static_cast<std::uint32_t>(groupsY), 1u);
}
}
@@ -6,6 +6,7 @@
#include "prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include "prx/libSceAgcDriver/Graphics/include/PipelineSpecialization.hpp"
#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp"
#include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp"
#include "prx/libc/include/HostThreadLocal.hpp"
#include <algorithm>
@@ -111,10 +112,17 @@ void ValidateProvokingVertex(const Context& context, const State& state, std::sp
}
return;
}
if (BarycentricPatches(state, shaders)) {
for (const auto& shader : shaders) {
if (shader.stage != ShaderRecompiler::ShaderStage::Fragment || shader.program == nullptr) continue;
Require(std::none_of(shader.program->fragmentParameters.begin(), shader.program->fragmentParameters.end(), [](const ShaderRecompiler::FragmentParameter& parameter) { return parameter.flat && parameter.perVertex; }), "last provoking vertex with explicit per-vertex flat interpolation is unsupported");
}
return;
}
Require(context.provokingVertexLast, "last provoking vertex requires VK_EXT_provoking_vertex with provokingVertexLast enabled");
const bool meshPrimitives = state.stages.path == ShaderPath::Geometry && state.stages.mesh;
Require(!BarycentricPatches(state, shaders) && !state.stages.tessellation && (meshPrimitives || (state.stages.path == ShaderPath::Vertex && !state.stages.mesh)), "last provoking vertex is unsupported for generated primitive pipelines");
Require(!meshPrimitives || (state.topology != VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP && state.topology != VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN), "last provoking vertex is unsupported for triangle strips or fans assembled for a geometry program");
Require(!state.stages.tessellation && (meshPrimitives || (state.stages.path == ShaderPath::Vertex && !state.stages.mesh)), "last provoking vertex is unsupported for generated primitive pipelines");
Require(!meshPrimitives || state.topology != VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, "last provoking vertex is unsupported for triangle fans assembled for a geometry program");
Require(state.rectList || state.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, "last provoking vertex is unsupported for this primitive topology");
for (const auto& shader : shaders) {
if (shader.stage != ShaderRecompiler::ShaderStage::Fragment) continue;
@@ -169,10 +177,11 @@ bool ShadersWriteMemory(std::span<const CompiledShader> shaders) {
void ValidateSampleMask(const Context& context, const State& state, std::span<const CompiledShader> shaders) {
if (!PartialSampleMask(state)) return;
const bool uniform = UniformSampleMask(state) && !SampleMaskFollowsOrder(state, HostSampleLocationsFor(context, state).order);
for (const auto& shader : shaders) {
if (shader.stage != ShaderRecompiler::ShaderStage::Fragment) continue;
Require(shader.program != nullptr, "missing compiled fragment shader");
if (UniformSampleMask(state)) {
if (uniform) {
Require(context.earlyFragmentSampleMaskTestBeforeSampleCounting || !DeclaresEarlyFragmentTests(*shader.program), "a sample mask with early fragment tests needs VkPhysicalDeviceMaintenance5Properties::earlyFragmentSampleMaskTestBeforeSampleCounting, which the device lacks");
continue;
}
@@ -190,13 +199,6 @@ VkShaderStageFlags pushStagesOf(const Context& context) {
return PreRasterizationPushStages(context.meshShader, context.tessellationShader, context.geometryShader);
}
VkExtent2D hostSampleLocationGrid(const Context& context, const State& state) {
if (!state.sampleLocationGrid || (context.sampleLocationGridSampleCounts & state.samples) != 0) return SampleLocationGrid(state);
static std::atomic<std::uint32_t> reported{0};
if ((reported.fetch_or(state.samples, std::memory_order_relaxed) & state.samples) == 0) std::fprintf(stderr, "[gpu] no 2x2 sample location grid at sample count %u on this device: sample locations varying between pixels take pixel (0,0)'s in every pixel\n", state.samples);
return {1, 1};
}
template<typename TValue>
void appendKey(std::vector<std::byte>& key, const TValue& value) {
static_assert(std::is_trivially_copyable_v<TValue>);
@@ -244,6 +246,7 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State&
appendKey(keys.vertexInput, state.topology);
appendKey(keys.vertexInput, state.primitiveRestart);
appendKey(keys.preRasterization, state.provokingVertexMode);
appendKey(keys.preRasterization, state.polygonMode);
appendKey(keys.preRasterization, state.negativeOneToOne);
appendKey(keys.preRasterization, state.depthClamp && context.depthClamp);
appendKey(keys.preRasterization, state.conservativeRasterization);
@@ -283,6 +286,12 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State&
if (state.depth) appendKey(keys.renderPass, state.depth->format);
appendKey(keys.fragmentShader, HostSampleMask(state));
appendKey(keys.fragmentOutput, HostSampleMask(state));
for (auto* key : {&keys.fragmentShader, &keys.fragmentOutput}) {
appendKey(*key, context.sampleLocationSampleCounts);
appendKey(*key, context.sampleLocationGridSampleCounts);
appendKey(*key, context.storageImageMultisample);
appendKey(*key, context.shaderStencilExport);
}
if (!resources.split) {
appendKey(keys.layout, pushStagesOf(context));
for (const auto word : resources.mainKey) appendKey(keys.layout, word);
@@ -315,7 +324,9 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& 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), dynamicRendering(context.graphicsPipelineLibrary) {
PerformanceTimer timing("Vulkan.GraphicsPipeline");
sampleLocationsEnabled = ValidateSampleLocations(context, state);
sampleLocationGrid = hostSampleLocationGrid(context, state);
const auto host = HostSampleLocationsFor(context, state);
sampleLocationGrid = host.grid;
sampleOrder = host.order;
// 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();
@@ -474,7 +485,9 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
dynamic.dynamicStateCount = static_cast<std::uint32_t>(dynamicStates.size());
dynamic.pDynamicStates = dynamicStates.data();
VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
raster.polygonMode = VK_POLYGON_MODE_FILL;
Require(state.polygonMode == VK_POLYGON_MODE_FILL || context.fillModeNonSolid, "a point or line polygon mode needs the fillModeNonSolid feature");
Require(state.polygonMode != VK_POLYGON_MODE_POINT || context.maintenance5, "a point polygon mode needs VK_KHR_maintenance5 for its one-pixel point size");
raster.polygonMode = state.polygonMode;
raster.depthClampEnable = state.depthClamp && context.depthClamp ? VK_TRUE : VK_FALSE;
Require(!state.depthBiasPerFace || (depthBias && state.cullMode == VK_CULL_MODE_NONE), "per-face depth bias needs both faces rasterized");
raster.cullMode = state.depthBiasPerFace ? VK_CULL_MODE_BACK_BIT : state.cullMode;
@@ -515,7 +528,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
depthStencil.front = state.stencilFront;
depthStencil.back = state.stencilBack;
const VkSampleMask sampleMask = HostSampleMask(state);
if (PartialSampleMask(state) && UniformSampleMask(state)) samples.pSampleMask = &sampleMask;
if (PartialSampleMask(state) && UniformSampleMask(state) && !SampleMaskFollowsOrder(state, sampleOrder)) samples.pSampleMask = &sampleMask;
VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
const bool alternateAlpha = SplitsAlternateAlpha(state);
std::vector<VkPipelineColorBlendAttachmentState> firstChannels(state.blends.begin(), state.blends.end());
@@ -868,6 +881,7 @@ void pipelineKey(std::vector<std::byte>& key, const Context& context, const Stat
append(key, state.topology);
append(key, state.primitiveRestart);
append(key, state.cullMode);
append(key, state.polygonMode);
append(key, state.frontFace);
append(key, state.provokingVertexMode);
append(key, state.negativeOneToOne);
@@ -937,6 +951,10 @@ void pipelineKey(std::vector<std::byte>& key, const Context& context, const Stat
append(key, tessellation.breakAtInstance);
}
append(key, HostSampleMask(state));
append(key, context.sampleLocationSampleCounts);
append(key, context.sampleLocationGridSampleCounts);
append(key, context.storageImageMultisample);
append(key, context.shaderStencilExport);
}
std::uint64_t hashKey(const std::vector<std::byte>& key) {
@@ -44,7 +44,7 @@ static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "Tessel
constexpr std::uint32_t FileMagic = 0x4c4a5041u;
// Bumped when the recipe codec, CanonicalPipelineState or the store key change in a way the
// layout guards above do not catch (a field renumbered or reinterpreted at the same size).
constexpr std::uint32_t FileFormat = 5u;
constexpr std::uint32_t FileFormat = 6u;
constexpr std::uint32_t ModuleRecord = 1u;
constexpr std::uint32_t GraphicsRecord = 2u;
constexpr std::uint32_t ComputeRecord = 3u;
@@ -208,6 +208,7 @@ void encodeState(Writer& writer, const State& state) {
writer.Bool(state.depthClamp);
writer.U32(static_cast<std::uint32_t>(state.conservativeRasterization));
writer.U32(state.cullMode);
writer.U32(static_cast<std::uint32_t>(state.polygonMode));
writer.U32(static_cast<std::uint32_t>(state.frontFace));
writer.U32(static_cast<std::uint32_t>(state.provokingVertexMode));
writer.U32(state.samples);
@@ -268,6 +269,7 @@ void decodeState(Reader& reader, State& state) {
state.depthClamp = reader.Bool();
state.conservativeRasterization = static_cast<VkConservativeRasterizationModeEXT>(reader.U32());
state.cullMode = reader.U32();
state.polygonMode = static_cast<VkPolygonMode>(reader.U32());
state.frontFace = static_cast<VkFrontFace>(reader.U32());
state.provokingVertexMode = static_cast<VkProvokingVertexModeEXT>(reader.U32());
state.samples = reader.U32();
@@ -1040,6 +1042,7 @@ State CanonicalPipelineState(const State& state) {
canonical.depthClamp = state.depthClamp;
canonical.conservativeRasterization = state.conservativeRasterization;
canonical.cullMode = state.cullMode;
canonical.polygonMode = state.polygonMode;
canonical.frontFace = state.frontFace;
canonical.provokingVertexMode = state.provokingVertexMode;
canonical.samples = state.samples;
@@ -0,0 +1,672 @@
#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp"
#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderDepth_spv.h"
#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderR16_spv.h"
#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderR32_spv.h"
#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderR8_spv.h"
#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderRG32_spv.h"
#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderRGBA32_spv.h"
#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderStencilBits_spv.h"
#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderStencilExport_spv.h"
#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderVertex_spv.h"
#include <algorithm>
#include <array>
#include <atomic>
#include <bit>
#include <cstdio>
#include <limits>
#include <map>
#include <mutex>
#include <tuple>
#include <utility>
namespace AgcDriver::Graphics {
namespace {
constexpr VkAccessFlags anyWrite = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
constexpr VkAccessFlags anyAccess = anyWrite | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_INPUT_ATTACHMENT_READ_BIT | VK_ACCESS_UNIFORM_READ_BIT | VK_ACCESS_INDEX_READ_BIT | VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT | VK_ACCESS_INDIRECT_COMMAND_READ_BIT;
constexpr VkPipelineStageFlags reorderStages = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
constexpr std::uint32_t setsPerPool = 64;
struct StoragePush {
std::array<std::uint32_t, 4> map;
std::array<std::uint32_t, 2> extent;
std::uint32_t count;
};
struct DepthPush {
std::array<std::uint32_t, 4> map;
std::uint32_t bit;
};
bool exactDepth(VkFormat format) {
return format == VK_FORMAT_D16_UNORM || format == VK_FORMAT_D32_SFLOAT || format == VK_FORMAT_D16_UNORM_S8_UINT || format == VK_FORMAT_D32_SFLOAT_S8_UINT;
}
bool stencilOf(const Context& context, const DepthTarget& depth) {
return depth.stencilAddress != 0 && HasStencil(HostDepthFormat(context, depth.format));
}
bool depthSampleable(const Context& context, VkFormat format, std::uint32_t samples) {
if (context.imageFormatProperties == nullptr || context.formatProperties == nullptr) return false;
VkFormatProperties properties{};
context.formatProperties(context.physical, format, &properties);
if ((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) == 0) return false;
VkImageFormatProperties supported{};
const VkImageCreateFlags flags = SampleLocationsCompatibleDepth(context, format, samples) ? VK_IMAGE_CREATE_SAMPLE_LOCATIONS_COMPATIBLE_DEPTH_BIT_EXT : 0u;
constexpr VkImageUsageFlags usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
if (context.imageFormatProperties(context.physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, flags, &supported) != VK_SUCCESS) return false;
return (supported.sampleCounts & samples) != 0;
}
std::array<std::uint32_t, 4> mapFor(const SampleOrder& order, bool toHost) {
std::array<std::uint32_t, 4> map{};
for (std::size_t pixel = 0; pixel < map.size(); ++pixel) {
const auto word = order[pixel] == 0u ? KeptSampleOrder : order[pixel];
map[pixel] = toHost ? word : InvertSampleOrder(word);
}
return map;
}
VkExtent2D regionOf(const SampleOrderPass& pass, VkExtent2D extent) {
return {std::min(pass.extent.width, extent.width), std::min(pass.extent.height, extent.height)};
}
void transitionLocated(const Context& context, VkCommandBuffer commands, VkImage image, VkFormat format, const VkSampleLocationsInfoEXT& locations, VkImageLayout from, VkImageLayout to, VkPipelineStageFlags sourceStages, VkAccessFlags sourceAccess, VkPipelineStageFlags destinationStages, VkAccessFlags destinationAccess) {
VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, &locations};
barrier.srcAccessMask = sourceAccess;
barrier.dstAccessMask = destinationAccess;
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_DEPTH_BIT | (HasStencil(format) ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u), 0, 1, 0, 1};
context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, sourceStages, destinationStages, 0, 0, nullptr, 0, nullptr, 1, &barrier);
}
class CopyImage {
public:
CopyImage(const Context& context, VkFormat format, std::uint32_t samples, VkExtent2D extent, bool stencil) : context(context), format(format), samples(samples), extent(extent) {
try {
VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
info.imageType = VK_IMAGE_TYPE_2D;
info.format = format;
info.extent = {extent.width, extent.height, 1};
info.mipLevels = 1;
info.arrayLayers = 1;
info.samples = static_cast<VkSampleCountFlagBits>(samples);
info.tiling = VK_IMAGE_TILING_OPTIMAL;
info.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
Check(context.Function<PFN_vkCreateImage>("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage sample order copy");
VkMemoryRequirements requirements{};
context.Function<PFN_vkGetImageMemoryRequirements>("vkGetImageMemoryRequirements")(context.device, image, &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, &memory), "vkAllocateMemory sample order copy");
Check(context.Function<PFN_vkBindImageMemory>("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory sample order copy");
depthView = createView(VK_IMAGE_ASPECT_DEPTH_BIT);
if (stencil) stencilView = createView(VK_IMAGE_ASPECT_STENCIL_BIT);
} catch (...) {
release();
throw;
}
}
~CopyImage() { release(); }
CopyImage(const CopyImage&) = delete;
CopyImage& operator=(const CopyImage&) = delete;
Context context;
VkFormat format;
std::uint32_t samples;
VkExtent2D extent;
VkImage image = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
VkImageView depthView = VK_NULL_HANDLE;
VkImageView stencilView = VK_NULL_HANDLE;
bool general = false;
private:
VkImageView createView(VkImageAspectFlags aspect) {
VkImageViewCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
info.image = image;
info.viewType = VK_IMAGE_VIEW_TYPE_2D;
info.format = format;
info.subresourceRange = {aspect, 0, 1, 0, 1};
VkImageView view = VK_NULL_HANDLE;
Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &info, nullptr, &view), "vkCreateImageView sample order copy");
return view;
}
void release() noexcept {
if (stencilView) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, stencilView, nullptr);
if (depthView) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, depthView, nullptr);
if (image) context.Function<PFN_vkDestroyImage>("vkDestroyImage")(context.device, image, nullptr);
if (memory) context.Function<PFN_vkFreeMemory>("vkFreeMemory")(context.device, memory, nullptr);
}
};
class SampleOrders {
public:
explicit SampleOrders(const Context& context) : context(context) {
this->context.bufferPool.reset();
}
~SampleOrders() { release(); }
SampleOrders(const SampleOrders&) = delete;
SampleOrders& operator=(const SampleOrders&) = delete;
void Storage(VkCommandBuffer commands, const SampleOrderTarget& target, const std::array<std::uint32_t, 4>& map, VkExtent2D region) {
const auto pipeline = storagePipeline(target.format);
const auto set = allocate(storageSetLayout, {{VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, setsPerPool}});
const VkDescriptorImageInfo imageInfo{VK_NULL_HANDLE, target.view, VK_IMAGE_LAYOUT_GENERAL};
VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
write.dstSet = set;
write.dstBinding = 0;
write.descriptorCount = 1;
write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
write.pImageInfo = &imageInfo;
context.Function<PFN_vkUpdateDescriptorSets>("vkUpdateDescriptorSets")(context.device, 1, &write, 0, nullptr);
const StoragePush push{map, {region.width, region.height}, target.samples};
context.Resolved(&DeviceFunctions::cmdBindPipeline, "vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, storagePipelineLayout, 0, 1, &set, 0, nullptr);
context.Resolved(&DeviceFunctions::cmdPushConstants, "vkCmdPushConstants")(commands, storagePipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(push), &push);
context.Resolved(&DeviceFunctions::cmdDispatch, "vkCmdDispatch")(commands, ((region.width + 1u) / 2u + 7u) / 8u, ((region.height + 1u) / 2u + 7u) / 8u, 1);
}
void Depth(VkCommandBuffer commands, const SampleOrderTarget& target, const std::array<std::uint32_t, 4>& map, VkExtent2D region, const SampleOrderPass& order, std::vector<std::shared_ptr<void>>& kept) {
const bool stencil = target.method != SampleOrderMethod::Depth;
auto copy = copyFor(target.format, target.samples, target.extent, stencil);
kept.push_back(copy);
const VkImageAspectFlags aspects = VK_IMAGE_ASPECT_DEPTH_BIT | (stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u);
if (!copy->general) {
VkImageMemoryBarrier toGeneral{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
toGeneral.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
toGeneral.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL;
toGeneral.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toGeneral.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toGeneral.image = copy->image;
toGeneral.subresourceRange = {aspects, 0, 1, 0, 1};
context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral);
copy->general = true;
}
VkImageCopy region3d{};
region3d.srcSubresource = {aspects, 0, 0, 1};
region3d.dstSubresource = {aspects, 0, 0, 1};
region3d.extent = {region.width, region.height, 1};
const auto& locations = order.locations;
const bool located = order.located;
const VkSampleLocationsInfoEXT info{VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT, nullptr, static_cast<VkSampleCountFlagBits>(target.samples), {1, 1}, target.samples, locations.data()};
constexpr VkPipelineStageFlags fragmentStages = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
constexpr VkAccessFlags depthAccess = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
if (located) transitionLocated(context, commands, target.image, target.format, info, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, anyWrite, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT);
context.Function<PFN_vkCmdCopyImage>("vkCmdCopyImage")(commands, target.image, located ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : VK_IMAGE_LAYOUT_GENERAL, copy->image, VK_IMAGE_LAYOUT_GENERAL, 1, &region3d);
if (located) transitionLocated(context, commands, target.image, target.format, info, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, fragmentStages, depthAccess);
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, fragmentStages, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | depthAccess);
const auto& pass = depthPass(target.format, target.samples, target.method, located, locations);
const auto set = allocate(sampledSetLayout, {{VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, setsPerPool * 2u}});
std::array<VkDescriptorImageInfo, 2> images{{{VK_NULL_HANDLE, copy->depthView, VK_IMAGE_LAYOUT_GENERAL}, {VK_NULL_HANDLE, stencil ? copy->stencilView : copy->depthView, VK_IMAGE_LAYOUT_GENERAL}}};
std::array<VkWriteDescriptorSet, 2> writes{};
for (std::uint32_t binding = 0; binding < writes.size(); ++binding) {
writes[binding].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
writes[binding].dstSet = set;
writes[binding].dstBinding = binding;
writes[binding].descriptorCount = 1;
writes[binding].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE;
writes[binding].pImageInfo = &images[binding];
}
context.Function<PFN_vkUpdateDescriptorSets>("vkUpdateDescriptorSets")(context.device, stencil ? 2u : 1u, writes.data(), 0, nullptr);
const std::array<VkImageView, 1> views{target.view};
auto framebuffer = std::make_shared<Framebuffer>(context, pass.renderPass, views, region);
kept.push_back(framebuffer);
VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
begin.renderPass = pass.renderPass;
begin.framebuffer = framebuffer->Handle();
begin.renderArea = {{0, 0}, region};
const VkAttachmentSampleLocationsEXT initial{0, info};
const VkSubpassSampleLocationsEXT post{0, info};
VkRenderPassSampleLocationsBeginInfoEXT sampleLocations{VK_STRUCTURE_TYPE_RENDER_PASS_SAMPLE_LOCATIONS_BEGIN_INFO_EXT};
sampleLocations.attachmentInitialSampleLocationsCount = 1;
sampleLocations.pAttachmentInitialSampleLocations = &initial;
sampleLocations.postSubpassSampleLocationsCount = 1;
sampleLocations.pPostSubpassSampleLocations = &post;
if (located) begin.pNext = &sampleLocations;
context.Resolved(&DeviceFunctions::cmdBeginRenderPass, "vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE);
const VkViewport viewport{0.0f, 0.0f, static_cast<float>(region.width), static_cast<float>(region.height), 0.0f, 1.0f};
const VkRect2D scissor{{0, 0}, region};
context.Resolved(&DeviceFunctions::cmdSetViewport, "vkCmdSetViewport")(commands, 0, 1, &viewport);
context.Resolved(&DeviceFunctions::cmdSetScissor, "vkCmdSetScissor")(commands, 0, 1, &scissor);
context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, sampledPipelineLayout, 0, 1, &set, 0, nullptr);
const auto draw = [&](VkPipeline pipeline, std::uint32_t bit, std::uint32_t writeMask, std::uint32_t reference) {
context.Resolved(&DeviceFunctions::cmdBindPipeline, "vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
context.Resolved(&DeviceFunctions::cmdSetStencilWriteMask, "vkCmdSetStencilWriteMask")(commands, VK_STENCIL_FACE_FRONT_AND_BACK, writeMask);
context.Resolved(&DeviceFunctions::cmdSetStencilReference, "vkCmdSetStencilReference")(commands, VK_STENCIL_FACE_FRONT_AND_BACK, reference);
const DepthPush push{map, bit};
context.Resolved(&DeviceFunctions::cmdPushConstants, "vkCmdPushConstants")(commands, sampledPipelineLayout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(push), &push);
context.Resolved(&DeviceFunctions::cmdDraw, "vkCmdDraw")(commands, 3, 1, 0, 0);
};
draw(pass.depth, 0u, 0xffu, 0u);
if (target.method == SampleOrderMethod::DepthStencilBits) {
for (std::uint32_t bit = 0; bit < 8u; ++bit) draw(pass.stencilBits, bit, 1u << bit, 0xffu);
}
context.Resolved(&DeviceFunctions::cmdEndRenderPass, "vkCmdEndRenderPass")(commands);
}
Context context;
private:
struct Ring {
std::vector<VkDescriptorPool> pools;
std::vector<std::uint64_t> serials;
std::size_t current = 0;
std::uint32_t used = 0;
};
struct DepthPass {
VkRenderPass renderPass = VK_NULL_HANDLE;
VkPipeline depth = VK_NULL_HANDLE;
VkPipeline stencilBits = VK_NULL_HANDLE;
};
using DepthKey = std::tuple<VkFormat, std::uint32_t, SampleOrderMethod, bool, std::array<std::uint32_t, 16>>;
using CopyKey = std::tuple<VkFormat, std::uint32_t, std::uint32_t, std::uint32_t, bool>;
VkShaderModule module(const std::uint32_t* code, std::size_t bytes) {
VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
info.codeSize = bytes;
info.pCode = code;
VkShaderModule created = VK_NULL_HANDLE;
Check(context.Function<PFN_vkCreateShaderModule>("vkCreateShaderModule")(context.device, &info, nullptr, &created), "vkCreateShaderModule sample order");
return created;
}
void destroyModule(VkShaderModule shader) {
context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, shader, nullptr);
}
VkDescriptorSetLayout setLayout(VkDescriptorType type, std::uint32_t count, VkShaderStageFlags stage) {
std::array<VkDescriptorSetLayoutBinding, 2> bindings{};
for (std::uint32_t binding = 0; binding < count; ++binding) bindings[binding] = {binding, type, 1, stage, nullptr};
VkDescriptorSetLayoutCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
info.bindingCount = count;
info.pBindings = bindings.data();
VkDescriptorSetLayout layout = VK_NULL_HANDLE;
Check(context.Function<PFN_vkCreateDescriptorSetLayout>("vkCreateDescriptorSetLayout")(context.device, &info, nullptr, &layout), "vkCreateDescriptorSetLayout sample order");
return layout;
}
VkPipelineLayout pipelineLayout(VkDescriptorSetLayout set, VkShaderStageFlags stage, std::uint32_t pushBytes) {
const VkPushConstantRange range{stage, 0, pushBytes};
VkPipelineLayoutCreateInfo info{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
info.setLayoutCount = 1;
info.pSetLayouts = &set;
info.pushConstantRangeCount = 1;
info.pPushConstantRanges = &range;
VkPipelineLayout layout = VK_NULL_HANDLE;
Check(context.Function<PFN_vkCreatePipelineLayout>("vkCreatePipelineLayout")(context.device, &info, nullptr, &layout), "vkCreatePipelineLayout sample order");
return layout;
}
VkPipeline storagePipeline(VkFormat format) {
const std::array<VkFormat, 5> formats{VK_FORMAT_R8_UINT, VK_FORMAT_R16_UINT, VK_FORMAT_R32_UINT, VK_FORMAT_R32G32_UINT, VK_FORMAT_R32G32B32A32_UINT};
const auto found = std::ranges::find(formats, format);
Require(found != formats.end(), "reordering the samples of a color target through a storage view of this format is not implemented");
const auto index = static_cast<std::size_t>(found - formats.begin());
if (storagePipelines[index] != VK_NULL_HANDLE) return storagePipelines[index];
if (storageSetLayout == VK_NULL_HANDLE) storageSetLayout = setLayout(VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT);
if (storagePipelineLayout == VK_NULL_HANDLE) storagePipelineLayout = pipelineLayout(storageSetLayout, VK_SHADER_STAGE_COMPUTE_BIT, sizeof(StoragePush));
const std::array<std::pair<const std::uint32_t*, std::size_t>, 5> code{{{SAMPLE_ORDER_R8_SPV, sizeof(SAMPLE_ORDER_R8_SPV)}, {SAMPLE_ORDER_R16_SPV, sizeof(SAMPLE_ORDER_R16_SPV)}, {SAMPLE_ORDER_R32_SPV, sizeof(SAMPLE_ORDER_R32_SPV)}, {SAMPLE_ORDER_RG32_SPV, sizeof(SAMPLE_ORDER_RG32_SPV)}, {SAMPLE_ORDER_RGBA32_SPV, sizeof(SAMPLE_ORDER_RGBA32_SPV)}}};
const auto shader = module(code[index].first, code[index].second);
VkComputePipelineCreateInfo info{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO};
info.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO};
info.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT;
info.stage.module = shader;
info.stage.pName = "main";
info.layout = storagePipelineLayout;
VkResult created;
{
auto pipelineCacheLock = context.LockPipelineCacheForUse();
created = context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &info, nullptr, &storagePipelines[index]);
}
destroyModule(shader);
Check(created, "vkCreateComputePipelines sample order");
return storagePipelines[index];
}
const DepthPass& depthPass(VkFormat format, std::uint32_t samples, SampleOrderMethod method, bool located, const std::array<VkSampleLocationEXT, 32>& locations) {
std::array<std::uint32_t, 16> standIn{};
if (located) {
for (std::uint32_t sample = 0; sample < samples && sample < 8u; ++sample) {
standIn[sample * 2u] = std::bit_cast<std::uint32_t>(locations[sample].x);
standIn[sample * 2u + 1u] = std::bit_cast<std::uint32_t>(locations[sample].y);
}
}
const DepthKey key{format, samples, method, located, standIn};
if (const auto found = depthPasses.find(key); found != depthPasses.end()) return found->second;
if (sampledSetLayout == VK_NULL_HANDLE) sampledSetLayout = setLayout(VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 2, VK_SHADER_STAGE_FRAGMENT_BIT);
if (sampledPipelineLayout == VK_NULL_HANDLE) sampledPipelineLayout = pipelineLayout(sampledSetLayout, VK_SHADER_STAGE_FRAGMENT_BIT, sizeof(DepthPush));
const bool stencil = method != SampleOrderMethod::Depth;
DepthPass pass;
try {
VkAttachmentDescription attachment{};
attachment.format = format;
attachment.samples = static_cast<VkSampleCountFlagBits>(samples);
attachment.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
attachment.stencilLoadOp = stencil ? VK_ATTACHMENT_LOAD_OP_LOAD : VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachment.stencilStoreOp = stencil ? VK_ATTACHMENT_STORE_OP_STORE : VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachment.initialLayout = VK_IMAGE_LAYOUT_GENERAL;
attachment.finalLayout = VK_IMAGE_LAYOUT_GENERAL;
if (HasStencil(format) && !stencil) {
attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE;
}
const VkAttachmentReference reference{0, VK_IMAGE_LAYOUT_GENERAL};
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.pDepthStencilAttachment = &reference;
VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
passInfo.attachmentCount = 1;
passInfo.pAttachments = &attachment;
passInfo.subpassCount = 1;
passInfo.pSubpasses = &subpass;
Check(context.Function<PFN_vkCreateRenderPass>("vkCreateRenderPass")(context.device, &passInfo, nullptr, &pass.renderPass), "vkCreateRenderPass sample order");
const auto create = [&](const std::uint32_t* code, std::size_t bytes, bool writesDepth) {
const auto vertex = module(SAMPLE_ORDER_VERTEX_SPV, sizeof(SAMPLE_ORDER_VERTEX_SPV));
const auto fragment = module(code, bytes);
std::array<VkPipelineShaderStageCreateInfo, 2> stages{};
stages[0] = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0, VK_SHADER_STAGE_VERTEX_BIT, vertex, "main", nullptr};
stages[1] = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0, VK_SHADER_STAGE_FRAGMENT_BIT, fragment, "main", nullptr};
VkPipelineVertexInputStateCreateInfo input{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO};
VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};
assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
VkPipelineViewportStateCreateInfo viewports{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
viewports.viewportCount = 1;
viewports.scissorCount = 1;
VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
raster.polygonMode = VK_POLYGON_MODE_FILL;
raster.cullMode = VK_CULL_MODE_NONE;
raster.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
raster.lineWidth = 1.0f;
VkPipelineMultisampleStateCreateInfo multisample{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
multisample.rasterizationSamples = static_cast<VkSampleCountFlagBits>(samples);
multisample.sampleShadingEnable = VK_TRUE;
multisample.minSampleShading = 1.0f;
VkPipelineSampleLocationsStateCreateInfoEXT sampleLocations{VK_STRUCTURE_TYPE_PIPELINE_SAMPLE_LOCATIONS_STATE_CREATE_INFO_EXT};
sampleLocations.sampleLocationsEnable = VK_TRUE;
sampleLocations.sampleLocationsInfo = {VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT, nullptr, static_cast<VkSampleCountFlagBits>(samples), {1, 1}, samples, locations.data()};
if (located) multisample.pNext = &sampleLocations;
VkStencilOpState replace{};
replace.failOp = VK_STENCIL_OP_REPLACE;
replace.passOp = VK_STENCIL_OP_REPLACE;
replace.depthFailOp = VK_STENCIL_OP_REPLACE;
replace.compareOp = VK_COMPARE_OP_ALWAYS;
replace.compareMask = 0xffu;
replace.writeMask = 0xffu;
VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
depthStencil.depthTestEnable = writesDepth ? VK_TRUE : VK_FALSE;
depthStencil.depthWriteEnable = writesDepth ? VK_TRUE : VK_FALSE;
depthStencil.depthCompareOp = VK_COMPARE_OP_ALWAYS;
depthStencil.stencilTestEnable = stencil ? VK_TRUE : VK_FALSE;
depthStencil.front = replace;
depthStencil.back = replace;
const std::array<VkDynamicState, 4> dynamicStates{VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR, VK_DYNAMIC_STATE_STENCIL_WRITE_MASK, VK_DYNAMIC_STATE_STENCIL_REFERENCE};
VkPipelineDynamicStateCreateInfo dynamic{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO};
dynamic.dynamicStateCount = static_cast<std::uint32_t>(dynamicStates.size());
dynamic.pDynamicStates = dynamicStates.data();
VkGraphicsPipelineCreateInfo info{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
info.stageCount = static_cast<std::uint32_t>(stages.size());
info.pStages = stages.data();
info.pVertexInputState = &input;
info.pInputAssemblyState = &assembly;
info.pViewportState = &viewports;
info.pRasterizationState = &raster;
info.pMultisampleState = &multisample;
info.pDepthStencilState = &depthStencil;
info.pDynamicState = &dynamic;
info.layout = sampledPipelineLayout;
info.renderPass = pass.renderPass;
VkPipeline pipeline = VK_NULL_HANDLE;
VkResult created;
{
auto pipelineCacheLock = context.LockPipelineCacheForUse();
created = context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &info, nullptr, &pipeline);
}
destroyModule(fragment);
destroyModule(vertex);
Check(created, "vkCreateGraphicsPipelines sample order");
return pipeline;
};
if (method == SampleOrderMethod::DepthStencilExport) pass.depth = create(SAMPLE_ORDER_STENCILEXPORT_SPV, sizeof(SAMPLE_ORDER_STENCILEXPORT_SPV), true);
else pass.depth = create(SAMPLE_ORDER_DEPTH_SPV, sizeof(SAMPLE_ORDER_DEPTH_SPV), true);
if (method == SampleOrderMethod::DepthStencilBits) pass.stencilBits = create(SAMPLE_ORDER_STENCILBITS_SPV, sizeof(SAMPLE_ORDER_STENCILBITS_SPV), false);
} catch (...) {
destroyPass(pass);
throw;
}
return depthPasses.emplace(key, pass).first->second;
}
std::shared_ptr<CopyImage> copyFor(VkFormat format, std::uint32_t samples, VkExtent2D extent, bool stencil) {
const CopyKey key{format, samples, extent.width, extent.height, stencil};
if (const auto found = copies.find(key); found != copies.end()) {
found->second.second = ++uses;
return found->second.first;
}
constexpr std::size_t bound = 4;
while (copies.size() >= bound) {
const auto oldest = std::ranges::min_element(copies, [](const auto& a, const auto& b) { return a.second.second < b.second.second; });
copies.erase(oldest);
}
auto copy = std::make_shared<CopyImage>(context, format, samples, extent, stencil);
copies.emplace(key, std::pair{copy, ++uses});
return copy;
}
VkDescriptorSet allocate(VkDescriptorSetLayout layout, const std::vector<VkDescriptorPoolSize>& sizes) {
auto& ring = layout == storageSetLayout ? storageRing : sampledRing;
const auto* recorder = Recorder::Active();
if (ring.pools.empty() || ring.used == setsPerPool) {
const auto completed = recorder != nullptr ? recorder->CompletedThrough() : 0;
auto next = ring.pools.size();
for (std::size_t step = 1; step <= ring.pools.size(); ++step) {
const auto index = (ring.current + step) % ring.pools.size();
if (ring.serials[index] <= completed) {
next = index;
break;
}
}
if (next < ring.pools.size()) {
Check(context.Function<PFN_vkResetDescriptorPool>("vkResetDescriptorPool")(context.device, ring.pools[next], 0), "vkResetDescriptorPool sample order");
} else {
VkDescriptorPoolCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
info.maxSets = setsPerPool;
info.poolSizeCount = static_cast<std::uint32_t>(sizes.size());
info.pPoolSizes = sizes.data();
VkDescriptorPool pool = VK_NULL_HANDLE;
Check(context.Function<PFN_vkCreateDescriptorPool>("vkCreateDescriptorPool")(context.device, &info, nullptr, &pool), "vkCreateDescriptorPool sample order");
ring.pools.push_back(pool);
ring.serials.push_back(0);
}
ring.current = next;
ring.used = 0;
}
VkDescriptorSetAllocateInfo allocation{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
allocation.descriptorPool = ring.pools[ring.current];
allocation.descriptorSetCount = 1;
allocation.pSetLayouts = &layout;
VkDescriptorSet set = VK_NULL_HANDLE;
Check(context.Function<PFN_vkAllocateDescriptorSets>("vkAllocateDescriptorSets")(context.device, &allocation, &set), "vkAllocateDescriptorSets sample order");
++ring.used;
ring.serials[ring.current] = recorder != nullptr && recorder->Device() == context.device ? recorder->OpenSerial() : std::numeric_limits<std::uint64_t>::max();
return set;
}
void destroyPass(const DepthPass& pass) noexcept {
if (pass.stencilBits) context.Function<PFN_vkDestroyPipeline>("vkDestroyPipeline")(context.device, pass.stencilBits, nullptr);
if (pass.depth) context.Function<PFN_vkDestroyPipeline>("vkDestroyPipeline")(context.device, pass.depth, nullptr);
if (pass.renderPass) context.Function<PFN_vkDestroyRenderPass>("vkDestroyRenderPass")(context.device, pass.renderPass, nullptr);
}
void release() noexcept {
copies.clear();
for (const auto& [key, pass] : depthPasses) destroyPass(pass);
depthPasses.clear();
for (const auto pipeline : storagePipelines) {
if (pipeline) context.Function<PFN_vkDestroyPipeline>("vkDestroyPipeline")(context.device, pipeline, nullptr);
}
for (auto* ring : {&storageRing, &sampledRing}) {
for (const auto pool : ring->pools) context.Function<PFN_vkDestroyDescriptorPool>("vkDestroyDescriptorPool")(context.device, pool, nullptr);
ring->pools.clear();
}
for (const auto layout : {storagePipelineLayout, sampledPipelineLayout}) {
if (layout) context.Function<PFN_vkDestroyPipelineLayout>("vkDestroyPipelineLayout")(context.device, layout, nullptr);
}
for (const auto layout : {storageSetLayout, sampledSetLayout}) {
if (layout) context.Function<PFN_vkDestroyDescriptorSetLayout>("vkDestroyDescriptorSetLayout")(context.device, layout, nullptr);
}
}
VkDescriptorSetLayout storageSetLayout = VK_NULL_HANDLE;
VkDescriptorSetLayout sampledSetLayout = VK_NULL_HANDLE;
VkPipelineLayout storagePipelineLayout = VK_NULL_HANDLE;
VkPipelineLayout sampledPipelineLayout = VK_NULL_HANDLE;
std::array<VkPipeline, 5> storagePipelines{};
std::map<DepthKey, DepthPass> depthPasses;
std::map<CopyKey, std::pair<std::shared_ptr<CopyImage>, std::uint64_t>> copies;
std::uint64_t uses = 0;
Ring storageRing;
Ring sampledRing;
};
std::mutex& ordersMutex() {
static std::mutex mutex;
return mutex;
}
std::vector<std::unique_ptr<SampleOrders>>& orders() {
static std::vector<std::unique_ptr<SampleOrders>> list;
return list;
}
SampleOrders& ordersFor(const Context& context) {
std::lock_guard lock(ordersMutex());
for (const auto& entry : orders()) {
if (entry->context.device == context.device) return *entry;
}
orders().push_back(std::make_unique<SampleOrders>(context));
return *orders().back();
}
}
SampleOrderMethod DepthSampleOrderMethod(const Context& context, const DepthTarget& depth) {
if (!stencilOf(context, depth)) return SampleOrderMethod::Depth;
return context.shaderStencilExport ? SampleOrderMethod::DepthStencilExport : SampleOrderMethod::DepthStencilBits;
}
bool HostsSampleLocationGrid(VkExtent2D largestGrid) {
return largestGrid.width != 0u && largestGrid.height != 0u && largestGrid.width % 2u == 0u && largestGrid.height % 2u == 0u;
}
bool MayReorderSamples(const Context& context, std::uint32_t samples) {
return samples > 1u && (context.sampleLocationSampleCounts & samples) != 0 && (context.sampleLocationGridSampleCounts & samples) == 0;
}
VkFormat SampleOrderStorageFormat(const Context& context, VkFormat format, std::uint32_t elementBytes, std::uint32_t samples) {
if (!context.storageImageMultisample || context.imageFormatProperties == nullptr || samples <= 1u) return VK_FORMAT_UNDEFINED;
const auto alias = TextureDetiler::ImageElementFormat(elementBytes);
if (alias == VK_FORMAT_UNDEFINED) return VK_FORMAT_UNDEFINED;
VkImageFormatProperties supported{};
if (context.imageFormatProperties(context.physical, alias, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_STORAGE_BIT, 0, &supported) != VK_SUCCESS || (supported.sampleCounts & samples) == 0) return VK_FORMAT_UNDEFINED;
constexpr VkImageUsageFlags usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_STORAGE_BIT;
constexpr VkImageCreateFlags flags = VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT;
if (context.imageFormatProperties(context.physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, flags, &supported) != VK_SUCCESS || (supported.sampleCounts & samples) == 0) return VK_FORMAT_UNDEFINED;
return alias;
}
std::string SampleOrderRefusal(const Context& context, const State& state) {
if (!context.sampleRateShading) return "the device lacks sampleRateShading";
if (!context.storageImageMultisample) return "the device lacks shaderStorageImageMultisample";
for (const auto& color : state.colors) {
if (color.samples > 1u && SampleOrderStorageFormat(context, color.format, color.elementBytes, color.samples) == VK_FORMAT_UNDEFINED) return "color format " + std::to_string(color.format) + " has no storage view at " + std::to_string(color.samples) + " samples";
}
if (state.depth && state.depth->samples > 1u) {
const auto host = HostDepthFormat(context, state.depth->format);
if (!exactDepth(host)) return "depth format " + std::to_string(host) + " is not written back exactly through FragDepth";
if (!depthSampleable(context, host, state.depth->samples)) return "depth format " + std::to_string(host) + " cannot be sampled at " + std::to_string(state.depth->samples) + " samples";
}
return {};
}
HostSampleLocations HostSampleLocationsFor(const Context& context, const State& state) {
if (!state.sampleLocationGrid || (context.sampleLocationGridSampleCounts & state.samples) != 0) return {SampleLocationGrid(state), {}};
const auto order = SameSetSampleOrder(state);
const auto refusal = order ? SampleOrderRefusal(context, state) : std::string("its pixels hold different locations");
static std::atomic<std::uint32_t> reported{0};
const auto bit = refusal.empty() ? state.samples : state.samples << 8u;
if ((reported.fetch_or(bit, std::memory_order_relaxed) & bit) == 0) {
if (refusal.empty()) std::fprintf(stderr, "[gpu] no 2x2 sample location grid at sample count %u on this device: pixel (0,0)'s sample locations stand in for every pixel, and each pixel's samples are put in the guest's order at the bounds of its render passes\n", state.samples);
else std::fprintf(stderr, "[gpu] no 2x2 sample location grid at sample count %u on this device: sample locations varying between pixels take pixel (0,0)'s in every pixel (%s)\n", state.samples, refusal.c_str());
}
return {{1, 1}, refusal.empty() ? *order : SampleOrder{}};
}
std::vector<SampleOrderStep> PlanSampleOrder(const Context& context, const State& state, const SampleOrder& order, VkAttachmentLoadOp load, VkAttachmentStoreOp store) {
std::vector<SampleOrderStep> steps;
if (!ReordersSamples(order)) return steps;
const bool toHost = load == VK_ATTACHMENT_LOAD_OP_LOAD;
const bool toGuest = store == VK_ATTACHMENT_STORE_OP_STORE;
if (!toHost && !toGuest) return steps;
for (std::uint32_t index = 0; index < state.colors.size(); ++index) {
if (state.colors[index].samples > 1u) steps.push_back({index, SampleOrderMethod::Storage, toHost, toGuest});
}
if (state.depth && state.depth->samples > 1u) steps.push_back({static_cast<std::uint32_t>(state.colors.size()), DepthSampleOrderMethod(context, *state.depth), toHost, toGuest});
return steps;
}
SampleOrderPass SampleOrderPassFor(const Context& context, const State& state, const SampleOrder& order) {
return {order, state.renderExtent, SampleLocations(state), ReordersSamples(order) && ValidateSampleLocations(context, state)};
}
void RecordSampleOrder(const Context& context, VkCommandBuffer commands, const SampleOrderPass& pass, std::span<const SampleOrderTarget> targets, bool toHost, std::vector<std::shared_ptr<void>>& kept) {
if (targets.empty() || !ReordersSamples(pass.order)) return;
auto& reorder = ordersFor(context);
const auto map = mapFor(pass.order, toHost);
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, reorderStages, anyWrite, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT);
for (const auto& target : targets) {
Require(target.image != VK_NULL_HANDLE && target.view != VK_NULL_HANDLE, "a reordered attachment has no image");
const auto region = regionOf(pass, target.extent);
if (region.width == 0 || region.height == 0) continue;
if (target.method == SampleOrderMethod::Storage) reorder.Storage(commands, target, map, region);
else reorder.Depth(commands, target, map, region, pass, kept);
if (target.keep != nullptr) kept.push_back(target.keep);
}
RecordMemoryBarrier(context, commands, reorderStages, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, anyAccess);
if (Recorder::Active() != nullptr) Recorder::CountBarriers(Recorder::CommandClass::Draw, 2);
}
void RecordDepthSampleLocations(const Context& context, VkCommandBuffer commands, const DepthSampleLocations& depth) {
Require(depth.image != VK_NULL_HANDLE && depth.samples * depth.grid.width * depth.grid.height <= depth.locations.size(), "a depth target drawn at custom sample locations has no image or too many locations");
const VkSampleLocationsInfoEXT info{VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT, nullptr, static_cast<VkSampleCountFlagBits>(depth.samples), depth.grid, depth.samples * depth.grid.width * depth.grid.height, depth.locations.data()};
transitionLocated(context, commands, depth.image, depth.format, info, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, anyWrite, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT);
transitionLocated(context, commands, depth.image, depth.format, info, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, anyAccess);
}
std::array<std::span<const std::uint32_t>, 9> SampleOrderShaderModules() {
return {SAMPLE_ORDER_R8_SPV, SAMPLE_ORDER_R16_SPV, SAMPLE_ORDER_R32_SPV, SAMPLE_ORDER_RG32_SPV, SAMPLE_ORDER_RGBA32_SPV, SAMPLE_ORDER_VERTEX_SPV, SAMPLE_ORDER_DEPTH_SPV, SAMPLE_ORDER_STENCILEXPORT_SPV, SAMPLE_ORDER_STENCILBITS_SPV};
}
void ClearSampleOrders(VkDevice device) {
std::lock_guard lock(ordersMutex());
std::erase_if(orders(), [&](const auto& entry) { return entry->context.device == device; });
}
}
@@ -461,7 +461,12 @@ private:
bool outermost;
};
std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, std::uint32_t mip, std::uint64_t guestBytes = 0, bool written = false);
void refreshForView(StorageTexture& source, const GuestTextureResource& resource) {
if (resource.dimension == TextureDimension::k2D && source.ImageLayers() > 1) source.RefreshLayers(resource.baseArray, 1);
else source.Refresh();
}
std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, std::uint32_t mip, std::uint64_t guestBytes = 0, bool written = false, std::optional<std::pair<std::uint32_t, std::uint32_t>> layers = std::nullopt);
std::uint64_t heldBytes(const StorageTexture& image) {
return image.BudgetBytes();
@@ -787,9 +792,9 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
// so a write the game made since the last walk is invisible until a collect stamps its page. The
// walk is memoized per packet, so a texture viewed from a storage image repeats it for free.
auto generation = GuestMemory::CollectWrites(address, bytes);
if (source != nullptr && !GuestMemory::UnchangedSince(address, bytes, source->Generation())) {
if (source != nullptr && !GuestMemory::UnchangedSince(address, bytes, source->GenerationFor(address, bytes))) {
// The CPU wrote the memory while results were pending: Refresh merges them the usual way.
source->Refresh();
refreshForView(*source, resource);
}
// Results stored to guest memory just now are stamped newer than `generation`; a snapshot read
// after them is current at the generation of a second (memoized) collect. The store marks the
@@ -857,7 +862,7 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
// written meanwhile is taken in by refreshing the image. A fast clear the image cannot
// see (keys, and no pending results to prefer) ends the view: a snapshot holds the clear.
if (!stagedFlushed && (source == nullptr || source == it->source) && (source != nullptr || *keys == DccKeys::Uncompressed) && StorageImageServesKeys(*it->source, resource.dccAddress)) {
if (!GuestMemory::UnchangedSince(address, bytes, it->source->Generation())) it->source->Refresh();
if (!GuestMemory::UnchangedSince(address, bytes, it->source->GenerationFor(address, bytes))) refreshForView(*it->source, resource);
if (it->texture->IsDepthCopy()) it->texture->RefreshDepthCopy();
it->keys = *keys;
touchTexture(cache, it);
@@ -1108,7 +1113,7 @@ static WrittenStorage WrittenStorageBytes(const GuestTextureResource& viewed, st
}
// `guestBytes` is the surface size when the caller described the surface already (0: described here).
std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& requested, std::uint32_t mip, std::uint64_t guestBytes, bool written) {
std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& requested, std::uint32_t mip, std::uint64_t guestBytes, bool written, std::optional<std::pair<std::uint32_t, std::uint32_t>> layers) {
const auto viewed = ArrayLayerWindow(requested, context.limits.maxImageArrayLayers);
if (viewed.baseAddress != requested.baseAddress) guestBytes = 0;
Require(viewed.dimension != TextureDimension::k2DMsaa, "storage image access to a multisampled texture is not implemented");
@@ -1123,7 +1128,7 @@ std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std
reportGuestAllocation(viewed, viewedBytes);
}
}
if (DepthPlanesStale() || written) static_cast<void>(FlushDepthPlanes(viewed.baseAddress, static_cast<std::size_t>(viewedBytes)));
if (DepthPlanesStale() || written) static_cast<void>(FlushDepthPlanes(viewed.baseAddress, static_cast<std::size_t>(viewedBytes), written ? DepthPlaneScope::Whole : DepthPlaneScope::Overlapped));
if (written && DepthSurfaceOverlaps(context.device, viewed.baseAddress, viewedBytes)) {
const auto storage = WrittenStorageBytes(viewed, mip);
RetireCoveredDepthSurfaces(context.device, storage.bytes, storage.extents);
@@ -1154,7 +1159,8 @@ std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std
it = cache.entries.end();
}
if (it != cache.entries.end()) {
it->texture->Refresh();
if (layers) it->texture->RefreshLayers(layers->first, layers->second);
else it->texture->Refresh();
cache.entries.splice(cache.entries.begin(), cache.entries, it);
texture = it->texture;
counters.storageHits.fetch_add(1, std::memory_order_relaxed);
@@ -1436,6 +1442,10 @@ std::shared_ptr<StorageTexture> CachedStorageSurface(const Context& context, con
return cachedStorageTexture(context, {}, resource, 0);
}
std::shared_ptr<StorageTexture> CachedStorageLayers(const Context& context, const GuestTextureResource& resource, std::uint32_t firstLayer, std::uint32_t layerCount) {
return cachedStorageTexture(context, {}, resource, 0, 0, false, std::pair{firstLayer, layerCount});
}
bool StorageImageServesKeys(const StorageTexture& image, std::uint64_t dccAddress) {
GuestTextureResource resource{};
resource.dccAddress = dccAddress;
@@ -3005,7 +3015,7 @@ bool ShaderResources::fastRevalidate(std::uint64_t serialBefore, std::span<const
}
if (source != nullptr) {
if (!source->Cached()) return fail(FastFail::Evicted);
queries.push_back({address, bytes, source->Generation()});
queries.push_back({address, bytes, source->GenerationFor(address, bytes)});
images.push_back(source);
} else {
queries.push_back({address, bytes, surface.generation});
@@ -3033,7 +3043,7 @@ bool ShaderResources::fastRevalidate(std::uint64_t serialBefore, std::span<const
}
if (!unchanged) query(address, address + bytes, image, nullptr, {i, true, false});
if (!image->Cached()) return fail(FastFail::Evicted);
queries.push_back({address, bytes, image->Generation()});
queries.push_back({address, bytes, image->GenerationFor(address, bytes)});
images.push_back(image);
}
if (!pending.empty()) {
@@ -3101,7 +3111,7 @@ bool ShaderResources::fastRevalidateEach() {
if (surface.source != nullptr) {
// The view follows the image: it needs the image current with guest memory, as the
// lookup's Refresh would make it.
if (!StorageImageCached(context, surface.source) || !GuestMemory::UnchangedSince(address, bytes, surface.source->Generation())) return false;
if (!StorageImageCached(context, surface.source) || !GuestMemory::UnchangedSince(address, bytes, surface.source->GenerationFor(address, bytes))) return false;
} else if (!GuestMemory::UnchangedSince(address, bytes, surface.generation)) {
return false;
}
@@ -3135,7 +3145,7 @@ bool ShaderResources::fastRevalidateEach() {
if (GuestMemory::CollectWrites(address, bytes) == 0) return false;
if (PendingStorageOverlaps(address, bytes, image)) return false;
if (!StorageImageCached(context, image)) return false;
if (!GuestMemory::UnchangedSince(address, bytes, image->Generation())) return false;
if (!GuestMemory::UnchangedSince(address, bytes, image->GenerationFor(address, bytes))) return false;
}
// Every check passed: snapshot textures are current at the collects issued before them. Storage
// images keep their own generation: nothing was stamped above it, so the blocks a later write
@@ -3169,9 +3179,9 @@ ShaderResources::OwnRefreshFallback ShaderResources::refreshOwnObjects(std::span
if (found != nullptr && !Texture::CanCopyFrom(*found, surface.resource)) found.reset();
if (found != nullptr) {
if (found != source) return OwnRefreshFallback::ForeignView;
if (!GuestMemory::UnchangedSince(address, bytes, source->Generation())) {
if (!GuestMemory::UnchangedSince(address, bytes, source->GenerationFor(address, bytes))) {
countOwnRefresh(false);
source->Refresh();
refreshForView(*source, surface.resource);
}
return OwnRefreshFallback::Count;
}
@@ -4178,7 +4188,8 @@ std::size_t ShaderResources::addGuestBuffer(std::span<const std::uint32_t> words
}
Require(byteSize <= context.limits.maxStorageBufferRange, "shader buffer exceeds descriptor range limit");
Require(byteSize <= std::numeric_limits<std::size_t>::max(), "shader buffer size exceeds host address space");
const auto size = static_cast<std::size_t>(byteSize);
auto size = static_cast<std::size_t>(byteSize);
if (const std::uint64_t stride = descriptor.Stride(); !written && target != nullptr && stride != 0 && address < target->address && address + size > target->address && address + size - target->address < stride) size = static_cast<std::size_t>(target->address - address);
Require(target == nullptr || !overlap(address, size, target->address, target->bytes), "shader buffer aliases the render target");
// APS5_ALL_BUFFERS_WRITTEN=1: every element is noted as written, as before bufferWritten existed.
static const bool allWritten = std::getenv("APS5_ALL_BUFFERS_WRITTEN") != nullptr;
@@ -4578,7 +4589,7 @@ std::shared_ptr<Texture> ShaderResources::fastTexture(const ImageRecord& record)
// keys and stays valid while they are unchanged); a change is the full lookup's to judge.
if (keys != record.entryKeys) return nullptr;
if (record.source != nullptr) {
if (!StorageImageCached(context, record.source.get()) || !GuestMemory::UnchangedSince(address, bytes, record.source->Generation())) return nullptr;
if (!StorageImageCached(context, record.source.get()) || !GuestMemory::UnchangedSince(address, bytes, record.source->GenerationFor(address, bytes))) return nullptr;
if (record.resource.dccAddress != 0 && IsDccClear(record.source->FilledKeys())) return nullptr;
// Under fast-clear keys the view holds only while its image's results are still pending over
// the surface; flushed, the clear the image cannot see makes the lookup take a snapshot.
@@ -138,9 +138,10 @@ struct Module {
Require(input && (clipInput || (value == spv::BuiltInPosition && signature == "f32x4") || (value == spv::BuiltInTessCoord && stage == Stage::TessellationEvaluation && signature == "f32x3") || ((value == spv::BuiltInInvocationId || value == spv::BuiltInPrimitiveId || value == spv::BuiltInPatchVertices) && (signature == "i32" || signature == "u32"))), "unsupported tessellation built-in");
return;
}
if (!input && value == spv::BuiltInPrimitivePointIndicesEXT) primitiveIndices = 1u;
if (!input && value == spv::BuiltInPrimitiveLineIndicesEXT) primitiveIndices = 2u;
if (!input && value == spv::BuiltInPrimitiveTriangleIndicesEXT) primitiveIndices = 3u;
Require((input && ((value == spv::BuiltInWorkgroupId || value == spv::BuiltInLocalInvocationId || value == spv::BuiltInGlobalInvocationId || value == spv::BuiltInNumWorkgroups) && signature == "u32x3")) || (input && value == spv::BuiltInLocalInvocationIndex && signature == "u32") || (!input && value == spv::BuiltInPrimitiveLineIndicesEXT && signature == "u32x2") || (!input && value == spv::BuiltInPrimitiveTriangleIndicesEXT && signature == "u32x3") || (!input && value == spv::BuiltInCullPrimitiveEXT && signature == "bool") || (!input && value == spv::BuiltInLayer && signature == "u32"), "unsupported mesh built-in");
Require((input && ((value == spv::BuiltInWorkgroupId || value == spv::BuiltInLocalInvocationId || value == spv::BuiltInGlobalInvocationId || value == spv::BuiltInNumWorkgroups) && signature == "u32x3")) || (input && value == spv::BuiltInLocalInvocationIndex && signature == "u32") || (!input && value == spv::BuiltInPrimitivePointIndicesEXT && signature == "u32") || (!input && value == spv::BuiltInPrimitiveLineIndicesEXT && signature == "u32x2") || (!input && value == spv::BuiltInPrimitiveTriangleIndicesEXT && signature == "u32x3") || (!input && value == spv::BuiltInCullPrimitiveEXT && signature == "bool") || (!input && value == spv::BuiltInLayer && signature == "u32"), "unsupported mesh built-in");
return;
}
const auto signature = Signature(type);
@@ -458,7 +459,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
mode(spv::ExecutionModeLocalSize, {paired ? config.threadsPerGroup / 2u : config.threadsPerGroup, 1, 1});
mode(spv::ExecutionModeOutputVertices, {config.maxVertices});
mode(spv::ExecutionModeOutputPrimitivesEXT, {config.maxPrimitives});
mode(config.outputPrimitiveSize == 2u ? spv::ExecutionModeOutputLinesEXT : spv::ExecutionModeOutputTrianglesEXT, {});
mode(config.outputPrimitiveSize == 1u ? spv::ExecutionModeOutputPoints : config.outputPrimitiveSize == 2u ? spv::ExecutionModeOutputLinesEXT : spv::ExecutionModeOutputTrianglesEXT, {});
Require(module.modes.size() == 4, "unsupported mesh execution mode");
} else if (barycentricPatches) {
if (control) mode(spv::ExecutionModeOutputVertices, {3u});
@@ -513,7 +514,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
const auto length = module.constants.find(outer[3]);
Require(length != module.constants.end() && length->second != 0, "invalid per-vertex interface array length");
if (mesh) {
const bool primitive = decoration.perPrimitive || module.perPrimitiveMembers.contains({outer[2], 0u}) || (decoration.builtin && (*decoration.builtin == spv::BuiltInPrimitiveLineIndicesEXT || *decoration.builtin == spv::BuiltInPrimitiveTriangleIndicesEXT || *decoration.builtin == spv::BuiltInCullPrimitiveEXT));
const bool primitive = decoration.perPrimitive || module.perPrimitiveMembers.contains({outer[2], 0u}) || (decoration.builtin && (*decoration.builtin == spv::BuiltInPrimitivePointIndicesEXT || *decoration.builtin == spv::BuiltInPrimitiveLineIndicesEXT || *decoration.builtin == spv::BuiltInPrimitiveTriangleIndicesEXT || *decoration.builtin == spv::BuiltInCullPrimitiveEXT));
Require(length->second == (primitive ? state.stages.mesh->maxPrimitives : state.stages.mesh->maxVertices), "mesh interface array disagrees with output limits");
} else if (geometry) {
Require(length->second == 3u, "geometry interface array must hold the three vertices of a triangle");
@@ -65,6 +65,36 @@ std::string zeroMessage(std::uint32_t offset, std::uint32_t value, const char* n
return std::string(name) + " is unsupported" + detail;
}
VkPolygonMode decodePolygonMode(const Registers& cx, std::uint32_t raster, const State& state, bool pointsOrLines) {
const auto mode = (raster >> 3u) & 3u;
if (mode == 0) return VK_POLYGON_MODE_FILL;
if (mode != 1) throw std::runtime_error("AGC graphics: " + zeroMessage(0x205, raster, "a reserved POLY_MODE"));
const auto front = (raster >> 5u) & 7u;
const auto back = (raster >> 8u) & 7u;
if (front > 2 || back > 2) throw std::runtime_error("AGC graphics: " + zeroMessage(0x205, raster, "a reserved polygon type"));
if (pointsOrLines && (state.stages.path == ShaderPath::Vertex || state.stages.mesh.has_value())) return VK_POLYGON_MODE_FILL;
const bool frontDrawn = (state.cullMode & VK_CULL_MODE_FRONT_BIT) == 0;
const bool backDrawn = (state.cullMode & VK_CULL_MODE_BACK_BIT) == 0;
if (!frontDrawn && !backDrawn) return VK_POLYGON_MODE_FILL;
if (frontDrawn && backDrawn && front != back) throw std::runtime_error("AGC graphics: " + zeroMessage(0x205, raster, "different front and back polygon modes"));
const auto type = frontDrawn ? front : back;
if (type == 2) return VK_POLYGON_MODE_FILL;
if (state.rectList) throw std::runtime_error("AGC graphics: " + zeroMessage(0x205, raster, "a point or line polygon mode on a rect list"));
if ((raster & 0x3800u) != 0) throw std::runtime_error("AGC graphics: " + zeroMessage(0x205, raster, "depth bias with a point or line polygon mode"));
if (type == 1) {
const auto line = find(cx, 0x282);
if (line == cx.end()) throw std::runtime_error("AGC graphics: a line polygon mode needs PA_SU_LINE_CNTL");
if ((line->second & 0xffffu) != 8u) throw std::runtime_error("AGC graphics: " + zeroMessage(0x282, line->second, "a line polygon mode wider or narrower than one pixel"));
return VK_POLYGON_MODE_LINE;
}
const auto size = find(cx, 0x280);
const auto limits = find(cx, 0x281);
if (size == cx.end() || limits == cx.end()) throw std::runtime_error("AGC graphics: a point polygon mode needs PA_SU_POINT_SIZE and PA_SU_POINT_MINMAX");
if (size->second != 0x80008u) throw std::runtime_error("AGC graphics: " + zeroMessage(0x280, size->second, "a point polygon mode with a point size other than one pixel"));
if ((limits->second & 0xffffu) > 8u || (limits->second >> 16u) < 8u) throw std::runtime_error("AGC graphics: " + zeroMessage(0x281, limits->second, "a point polygon mode whose size limits exclude one pixel"));
return VK_POLYGON_MODE_POINT;
}
std::string tessellationMessage(std::uint32_t config, std::uint32_t parameters, const ShaderRecompiler::TessellationConfiguration& tess) {
static constexpr std::array<std::string_view, 3> Types{"TESS_ISOLINE", "TESS_TRIANGLE", "TESS_QUAD"};
static constexpr std::array<std::string_view, 4> Partitionings{"PART_INTEGER", "PART_POW2", "PART_FRAC_ODD", "PART_FRAC_EVEN"};
@@ -251,7 +281,9 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint
const auto opValue = refMask >> 24u;
auto reference = test;
auto fixed = compare == 0 || compare == 7 ? 0u : face.compareMask;
const auto convert = [&](std::uint32_t op) {
std::uint32_t invertMask = 0;
std::uint32_t inverted = 0;
const auto convert = [&](std::uint32_t op, bool testFailed) {
if (face.writeMask == 0) return VK_STENCIL_OP_KEEP;
char detail[112];
if (opValExport && ((op == 4 && !opValReference) || op == 5 || op == 6 || op == 8 || op == 9)) {
@@ -274,6 +306,16 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint
}
const auto value = op == 2 ? 0xffu : op == 3 ? test : opValue;
if (((reference ^ value) & fixed & face.writeMask) != 0) {
const auto compared = test & face.compareMask;
const bool passKnown = compare == 2 || (compare == 3 && compared == face.compareMask) || (compare == 6 && compared == 0u);
const bool failKnown = compare == 5 || (compare == 1 && compared == 0u) || (compare == 4 && compared == face.compareMask);
const bool testValueKnown = testFailed ? failKnown : passKnown;
const auto flipped = (test ^ value) & face.writeMask;
if (testValueKnown && (face.writeMask & ~face.compareMask) == 0 && (invertMask == 0 || invertMask == flipped)) {
invertMask = flipped;
++inverted;
return VK_STENCIL_OP_INVERT;
}
std::snprintf(detail, sizeof(detail), "AGC graphics: stencil replacement 0x%02x against test value 0x%02x (masks 0x%02x/0x%02x) is unsupported", value, test, face.compareMask, face.writeMask);
throw std::runtime_error(detail);
}
@@ -296,9 +338,14 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint
throw std::runtime_error(detail);
}
};
face.failOp = convert(ops & 0xfu);
face.passOp = convert((ops >> 4u) & 0xfu);
face.depthFailOp = convert((ops >> 8u) & 0xfu);
face.failOp = convert(ops & 0xfu, true);
face.passOp = convert((ops >> 4u) & 0xfu, false);
face.depthFailOp = convert((ops >> 8u) & 0xfu, false);
if (invertMask != 0) {
const auto active = ((ops & 0xfu) != 0u) + (((ops >> 4u) & 0xfu) != 0u) + (((ops >> 8u) & 0xfu) != 0u);
if (inverted != active) throw std::runtime_error("AGC graphics: a stencil replacement drawn as an inversion beside another stencil operation is unsupported");
face.writeMask = invertMask;
}
face.reference = reference;
Require(!opValReference || compare == 0 || compare == 7 || face.compareMask == 0, "a stencil compare against the test value is unsupported with a pixel shader that exports the stencil operation value");
return face;
@@ -926,6 +973,53 @@ VkExtent2D SampleLocationGrid(const State& state) {
return state.sampleLocationGrid ? VkExtent2D{2, 2} : VkExtent2D{1, 1};
}
std::optional<SampleOrder> SameSetSampleOrder(const State& state) {
if (!state.customSampleLocations || !state.sampleLocationGrid || state.samples < 2u || state.samples > 8u) return std::nullopt;
SampleOrder order{};
for (std::uint32_t pixel = 0; pixel < order.size(); ++pixel) {
std::uint32_t word = KeptSampleOrder;
std::uint32_t taken = 0;
for (std::uint32_t sample = 0; sample < state.samples; ++sample) {
std::uint32_t guest = 0;
while (guest < state.samples && (((taken >> guest) & 1u) != 0u || state.sampleLocations[pixel * 8u + guest] != state.sampleLocations[sample])) ++guest;
if (guest == state.samples) return std::nullopt;
taken |= 1u << guest;
word = (word & ~(0xfu << (sample * 4u))) | (guest << (sample * 4u));
}
order[pixel] = word;
}
return order;
}
bool ReordersSamples(const SampleOrder& order) {
return std::ranges::any_of(order, [](std::uint32_t word) { return word != 0u && word != KeptSampleOrder; });
}
std::uint32_t InvertSampleOrder(std::uint32_t order) {
std::uint32_t inverse = 0;
for (std::uint32_t sample = 0; sample < 8u; ++sample) inverse |= sample << (((order >> (sample * 4u)) & 0x7u) * 4u);
return inverse;
}
std::uint32_t HostSampleBits(std::uint32_t guestBits, std::uint32_t order) {
std::uint32_t host = 0;
for (std::uint32_t sample = 0; sample < 8u; ++sample) host |= ((guestBits >> ((order >> (sample * 4u)) & 0x7u)) & 1u) << sample;
return host | (guestBits & ~0xffu);
}
bool SampleMaskFollowsOrder(const State& state, const SampleOrder& order) {
if (!ReordersSamples(order) || !PartialSampleMask(state) || !UniformSampleMask(state)) return false;
const auto full = (1u << state.samples) - 1u;
const auto mask = state.quadSampleMask[0] & full;
return std::ranges::any_of(order, [&](std::uint32_t word) { return (HostSampleBits(mask, word) & full) != mask; });
}
std::array<std::uint16_t, 4> QuadSampleMasks(const State& state, const SampleOrder& order) {
if (!SampleMaskFollowsOrder(state, order)) return QuadSampleMasks(state);
const auto mask = static_cast<std::uint16_t>(state.quadSampleMask[0] & ((1u << state.samples) - 1u));
return {mask, mask, mask, mask};
}
std::array<VkSampleLocationEXT, 32> SampleLocations(const State& state) {
Require(state.samples == 1 || state.samples == 2 || state.samples == 4 || state.samples == 8, "invalid sample count");
std::array<VkSampleLocationEXT, 32> locations{};
@@ -1014,8 +1108,8 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) {
const auto maxVertices = read(queue.context, 0x1ff);
const auto verticesPerPrimitive = vertexWorkgroup ? 3u : read(queue.context, 0x2ce);
const auto outputType = read(queue.context, 0x29b);
validate(outputType == 2 || (outputType == 1 && !vertexWorkgroup), "VGT_GS_OUT_PRIM_TYPE other than a triangle strip, or a line strip from a geometry program, is unsupported");
const auto outputSize = outputType == 1 ? 2u : 3u;
validate(outputType == 2 || ((outputType == 0 || outputType == 1) && !vertexWorkgroup), "VGT_GS_OUT_PRIM_TYPE other than a triangle strip, or a point list or line strip from a geometry program, is unsupported");
const auto outputSize = outputType == 0 ? 1u : outputType == 1 ? 2u : 3u;
validate((primitive == 1 || primitive == 2 || primitive == 4 || primitive == 5 || primitive == 6) && verticesPerPrimitive >= outputSize, "unsupported geometry input or output assembly");
const auto inputSize = primitive == 1 ? 1u : primitive == 2 ? 2u : 3u;
validate(vertices >= inputSize && maxVertices != 0 && (!vertexWorkgroup || maxVertices >= inputSize) && maxVertices <= 256 && verticesPerPrimitive <= 256, "invalid geometry subgroup output");
@@ -1028,7 +1122,7 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) {
validate(esgsItemSize != 0 && esgsItemSize * vertices <= 0xffffu, "invalid VGT_ESGS_RING_ITEMSIZE");
const auto amplification = fastLaunch ? read(queue.context, 0x2d3) & 0x1ffu : verticesPerPrimitive - outputSize + 1u;
validate(amplification != 0, "fast launch subgroup amplifies to no primitives");
const auto maxPrimitives = outputSize == 2u && !fastLaunch ? std::max(primitives * amplification, maxVertices) : primitives * amplification;
const auto maxPrimitives = outputSize <= 2u && !fastLaunch ? std::max(primitives * amplification, maxVertices) : primitives * amplification;
const auto threads = std::max({(groupPrimitives - 1u) * inputStep + inputSize, primitives, maxVertices, maxPrimitives});
const auto raster = find(queue.context, 0x205);
const auto provokingVertex = raster != queue.context.end() && (raster->second & (1u << 19u)) != 0 ? outputSize - 1u : 0u;
@@ -1212,7 +1306,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
if (depthKeepsSampleZero && (read(cx, 0x292) & 1u) == 0) {
const bool filled = result.topology != VK_PRIMITIVE_TOPOLOGY_POINT_LIST && result.topology != VK_PRIMITIVE_TOPOLOGY_LINE_LIST && result.topology != VK_PRIMITIVE_TOPOLOGY_LINE_STRIP;
const auto sampleInputs = read(cx, 0x1b3) & read(cx, 0x1b4) & (ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::SampleCoverage));
Require(filled && !result.alphaToCoverage && (PixelProgramSkipped(queue) || ((read(cx, 0x293) & PixelShaderIteratesSamples) == 0 && sampleInputs == 0)) && (read(cx, 0x203) & 0x100u) == 0 && (read(cx, 0x201) & 0x0f0c0000u) == 0, "a one-sample depth target under a multisampled rasterizer without MSAA_ENABLE is unsupported for points or lines and with alpha-to-coverage, PS_ITER_SAMPLE, a sample index or coverage input, a sample-mask export, or DB_EQAA Z interpolation or overrasterization");
Require(filled && !result.alphaToCoverage && (PixelProgramSkipped(queue) || sampleInputs == 0) && (read(cx, 0x203) & 0x100u) == 0 && (read(cx, 0x201) & 0x0f0c0000u) == 0, "a one-sample depth target under a multisampled rasterizer without MSAA_ENABLE is unsupported for points or lines and with alpha-to-coverage, a sample index or coverage input, a sample-mask export, or DB_EQAA Z interpolation or overrasterization");
result.samples = 1;
result.customSampleLocations = false;
result.sampleLocations.fill(0);
@@ -1260,12 +1354,13 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
const auto raster = read(cx, 0x205);
// Bits 5-10 give the front/back polygon type (2 = filled triangles), which POLY_MODE (bit 3) turns on
// explicitly; KEEP_TOGETHER_ENABLE (bit 24) only affects primitive distribution across the chip.
const bool pointsOrLines = result.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || result.topology == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || result.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP || (result.stages.mesh.has_value() && result.stages.mesh->outputPrimitiveSize == 2u);
const bool pointsOrLines = result.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || result.topology == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || result.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP || (result.stages.mesh.has_value() && result.stages.mesh->outputPrimitiveSize <= 2u);
const auto lineOffset = (raster & 0x1800u) != 0 && !pointsOrLines ? 0x2000u : 0u;
const auto rasterMode = raster & ~0x7u & ~(1u << 24u) & ~0x1800u & ~lineOffset & ~(1u << 19u) & ~(1u << 16u);
if (rasterMode != 0 && rasterMode != 0x240u && rasterMode != 0x248u) throw std::runtime_error("AGC graphics: " + zeroMessage(0x205, raster, "polygon mode, depth bias or nonstandard rasterization"));
const auto rasterMode = raster & ~0x7u & ~0x7f8u & ~(1u << 24u) & ~0x1800u & ~lineOffset & ~(1u << 19u) & ~(1u << 16u);
if (rasterMode != 0) throw std::runtime_error("AGC graphics: " + zeroMessage(0x205, raster, "depth bias or nonstandard rasterization"));
result.cullMode = ((raster & 1u) != 0 ? VK_CULL_MODE_FRONT_BIT : 0u) | ((raster & 2u) != 0 ? VK_CULL_MODE_BACK_BIT : 0u);
if (result.rectList) result.cullMode = VK_CULL_MODE_NONE;
result.polygonMode = decodePolygonMode(cx, raster, result, pointsOrLines);
if ((raster & 0x1800u) != 0 && result.depth) decodeDepthBias(cx, raster, result);
result.frontFace = (raster & 4u) != 0 ? VK_FRONT_FACE_CLOCKWISE : VK_FRONT_FACE_COUNTER_CLOCKWISE;
result.provokingVertexMode = (raster & (1u << 19u)) != 0 ? VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT : VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT;
@@ -1545,6 +1640,7 @@ std::array<std::uint16_t, 4> QuadSampleMasks(const State& state) {
ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state) {
if (state.rectList) return ShaderRecompiler::RasterPrimitive::Rectangle;
if (state.stages.mesh.has_value() && state.stages.mesh->outputPrimitiveSize == 1u) return ShaderRecompiler::RasterPrimitive::Point;
if (state.stages.mesh.has_value() && state.stages.mesh->outputPrimitiveSize == 2u) return ShaderRecompiler::RasterPrimitive::Line;
if (state.stages.mesh.has_value() || state.stages.tessellation.has_value()) return ShaderRecompiler::RasterPrimitive::Triangle;
switch (state.topology) {
@@ -1787,6 +1883,61 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo
return color;
}
namespace {
bool drawCovers(const Registers& cx, VkExtent2D extent) {
const auto viewportControl = read(cx, 0x206);
if (viewportControl != 0x43fu) throw std::runtime_error(vteMessage(viewportControl));
const auto xs = std::fabs(readFloat(cx, 0x10f));
const auto xo = readFloat(cx, 0x110);
const auto ys = std::fabs(readFloat(cx, 0x111));
const auto yo = readFloat(cx, 0x112);
VkRect2D covered{{0, 0}, {0x7fffu, 0x7fffu}};
intersect(covered, cx, 0xc, true);
intersect(covered, cx, 0x81, false);
intersect(covered, cx, 0x90, false);
if ((read(cx, 0x292) & 2u) != 0) intersect(covered, cx, 0x94, false);
const auto width = static_cast<float>(extent.width);
const auto height = static_cast<float>(extent.height);
const bool viewportCovers = xo - xs <= 0.0f && xo + xs >= width && yo - ys <= 0.0f && yo + ys >= height;
const bool scissorCovers = covered.offset.x == 0 && covered.offset.y == 0 && covered.extent.width >= extent.width && covered.extent.height >= extent.height;
return viewportCovers && scissorCovers;
}
}
std::optional<DepthCopyPass> DecodeDepthCopyPass(const QueueState& queue) {
const auto& cx = queue.context;
const auto control = find(cx, 0x000);
if (control == cx.end() || (control->second & 4u) == 0) return std::nullopt;
Require((control->second & ~0xf84u) == 0, "DB depth copy combined with a stencil copy, clear, resummarize or decompress");
Require((read(cx, 0x200) & 0xfu) == 0, "DB depth copy with depth or stencil tests");
Require((read(cx, 0x010) & 3u) == 3u, "DB depth copy from a depth plane that is not Z_32_FLOAT");
Require(ColorWriteMask(cx) == 1u, "DB depth copy into anything but the red channel of color target 0");
const auto viewRegister = find(cx, 0x002);
const auto view = viewRegister == cx.end() ? 0u : viewRegister->second;
Require((read(cx, 0x010) & 0x000f1000u) == 0 && (view & 0x3c000000u) == 0, "DB depth copy from a mipmapped or partially resident depth plane");
DepthCopyPass pass{};
const auto high = find(cx, 0x01a);
pass.depthAddress = (high == cx.end() ? 0ull : static_cast<std::uint64_t>(high->second & 0xffu) << 40u) | (static_cast<std::uint64_t>(read(cx, 0x012)) << 8u);
const auto size = read(cx, 0x007);
pass.extent = {(size & 0x3fffu) + 1u, ((size >> 16u) & 0x3fffu) + 1u};
pass.samples = 1u << ((read(cx, 0x010) >> 2u) & 3u);
if (pass.samples != 1u) pass.depthAddress &= ~std::uint64_t{65535u};
if (const auto slice = view & 0x1fffu; slice != 0) pass.depthAddress += static_cast<std::uint64_t>(slice) * DepthSliceBytes(pass.extent, 4u);
const auto maskLow = find(cx, 0x30e);
const auto maskHigh = find(cx, 0x30f);
Require((maskLow == cx.end() || maskLow->second == 0xffffffffu) && (maskHigh == cx.end() || maskHigh->second == 0xffffffffu), "DB depth copy with a sample mask is unsupported");
pass.sample = (control->second & 0x80u) != 0 ? 0u : (control->second >> 8u) & 0xfu;
Require(pass.sample < pass.samples, "DB depth copy of a sample the depth plane does not have");
pass.color = DecodeColorBuffer(cx, 0);
Require(pass.color.format == VK_FORMAT_R32_SFLOAT && pass.color.samples == 1 && pass.color.mip == 0 && pass.color.layers == 1, "DB depth copy into a color target that is not a single-sample R32 float");
Require(pass.color.extent.width == pass.extent.width && pass.color.extent.height == pass.extent.height, "DB depth copy into a color target of another size");
Require(pass.color.cmaskAddress == 0, "DB depth copy into a color target with CMASK fast clears is unsupported");
Require(drawCovers(cx, pass.extent), "DB depth copy over part of the depth surface");
return pass;
}
std::optional<ColorMetadataPass> DecodeColorMetadataPass(const QueueState& queue) {
const auto& cx = queue.context;
const auto control = find(cx, 0x202);
@@ -1818,24 +1969,7 @@ std::optional<ColorMetadataPass> DecodeColorMetadataPass(const QueueState& queue
const auto samples = resolve && rasterizerSamples && multisampleRaster ? decodeSamples(cx) : 1u;
if (!resolve) zero(cx, 0x2f8, ~0u, "multisampling or coverage conversion");
zero(cx, 0x80, ~0u, "window offset");
const auto viewportControl = read(cx, 0x206);
if (viewportControl != 0x43fu) throw std::runtime_error(vteMessage(viewportControl));
const auto xs = std::fabs(readFloat(cx, 0x10f));
const auto xo = readFloat(cx, 0x110);
const auto ys = std::fabs(readFloat(cx, 0x111));
const auto yo = readFloat(cx, 0x112);
VkRect2D covered{{0, 0}, {0x7fffu, 0x7fffu}};
intersect(covered, cx, 0xc, true);
intersect(covered, cx, 0x81, false);
intersect(covered, cx, 0x90, false);
if ((read(cx, 0x292) & 2u) != 0) intersect(covered, cx, 0x94, false);
const auto requireCovered = [&](const ColorTarget& target) {
const auto width = static_cast<float>(target.extent.width);
const auto height = static_cast<float>(target.extent.height);
const bool viewportCovers = xo - xs <= 0.0f && xo + xs >= width && yo - ys <= 0.0f && yo + ys >= height;
const bool scissorCovers = covered.offset.x == 0 && covered.offset.y == 0 && covered.extent.width >= target.extent.width && covered.extent.height >= target.extent.height;
Require(viewportCovers && scissorCovers, "CB metadata pass over part of a color target");
};
const auto requireCovered = [&](const ColorTarget& target) { Require(drawCovers(cx, target.extent), "CB metadata pass over part of a color target"); };
if (resolve) {
const auto source = DecodeColorBuffer(cx, 0);
const auto destination = DecodeColorBuffer(cx, 1);
@@ -1059,10 +1059,13 @@ Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>&
}
}
Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType, std::uint32_t baseLayer, std::shared_ptr<OwnedImage> owner) : context(context), owned(std::move(owner)) {
Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType, std::uint32_t baseLayer, std::shared_ptr<OwnedImage> owner, VkImageUsageFlags usage) : context(context), owned(std::move(owner)) {
Require(owned == nullptr || (owned->image == depthImage && owned->context.device == context.device), "a depth sampled view must retain its own device and image");
layout = VK_IMAGE_LAYOUT_GENERAL;
VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
VkImageViewUsageCreateInfo viewUsage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO};
viewUsage.usage = usage;
if (usage != 0) viewInfo.pNext = &viewUsage;
viewInfo.image = depthImage;
viewInfo.viewType = viewType;
viewInfo.format = depthFormat;
@@ -2033,11 +2036,31 @@ LookupOutcomes& ThreadLookupOutcomes() {
}
bool StorageTexture::Refresh() {
return refresh(0, trackedLayers);
}
bool StorageTexture::RefreshLayers(std::uint32_t first, std::uint32_t count) {
if (count == 0 || geometry.thick || geometry.imageLayers <= 1 || first >= geometry.imageLayers || count > geometry.imageLayers - first || geometry.layerBytes == 0 || trackedLayerBytes == 0 || descriptor.dccAddress != 0) return Refresh();
const auto begin = geometry.GuestLayerOffset(first);
const auto end = geometry.GuestLayerOffset(first + count - 1) + geometry.layerBytes;
const auto firstLayer = static_cast<std::uint32_t>(begin / trackedLayerBytes);
const auto lastLayer = static_cast<std::uint32_t>(std::min<std::uint64_t>((end + trackedLayerBytes - 1) / trackedLayerBytes, trackedLayers));
if (firstLayer >= lastLayer || (firstLayer == 0 && lastLayer == trackedLayers)) return Refresh();
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
if (layerPending[layer] && (layer < firstLayer || layer >= lastLayer)) return Refresh();
}
if (pendingAlias() != nullptr) return Refresh();
return refresh(firstLayer, lastLayer);
}
bool StorageTexture::refresh(std::uint32_t firstLayer, std::uint32_t lastLayer) {
const bool profile = LookupOutcomes::Profiled();
auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
const bool scoped = firstLayer != 0 || lastLayer != trackedLayers;
CompletePendingCpuWrites(context, descriptor.baseAddress, static_cast<std::size_t>(guestBytes));
if (descriptor.dccAddress != 0) CompletePendingCpuWrites(context, descriptor.dccAddress, DccKeyBytes(guestBytes));
if (refreshProved()) {
layerBehind.clear();
NoteProved();
refreshesProved.fetch_add(1, std::memory_order_relaxed);
if (profile) LookupOutcomes::Add(LookupOutcomes::RefreshProved, start);
@@ -2059,7 +2082,7 @@ bool StorageTexture::Refresh() {
// generation, no results of this image's own there) or dead to its own write-back (tracked and
// stamped); an untracked unit is stored whole by that write-back, and a unit pending in both
// images takes the old order (store, then this image re-uploads it).
auto alias = pendingAlias();
auto alias = scoped ? std::shared_ptr<StorageTexture>{} : pendingAlias();
if (alias != nullptr && !(ProvedClearKeys(descriptor, guestBytes, keyProof) == DccKeys::Uncompressed && HostImportFor(context, descriptor.baseAddress, static_cast<std::size_t>(guestBytes)) != nullptr)) alias = nullptr;
if (alias != nullptr) {
std::vector<std::uint8_t> aliasStamped(trackedLayers);
@@ -2072,22 +2095,35 @@ bool StorageTexture::Refresh() {
}
}
}
const auto scopeBegin = scoped ? layerBegin(firstLayer) : descriptor.baseAddress;
const auto scopeBytes = scoped ? static_cast<std::size_t>(layerBegin(lastLayer - 1) + layerBytes(lastLayer - 1) - scopeBegin) : static_cast<std::size_t>(guestBytes);
// No publish: the upload below reads the stored units from the unit shadow itself.
handedOver.clear();
handedUnits.clear();
const auto handOverKeys = alias == nullptr ? clearHandOver() : std::nullopt;
const bool flushed = !handOverKeys.has_value() && FlushPending(descriptor.baseAddress, static_cast<std::size_t>(guestBytes), alias.get(), "storage refresh", PublishScope::None);
const auto handOverKeys = alias == nullptr && !scoped ? clearHandOver() : std::nullopt;
const bool flushed = !handOverKeys.has_value() && FlushPending(scopeBegin, scopeBytes, alias.get(), "storage refresh", PublishScope::None);
if (profile && flushed) start = LookupOutcomes::Add(LookupOutcomes::PendingFlush, start);
// `original` holds the guest bytes the image was last uploaded from or written back as; while the
// guest memory and the DCC keys still match, the image content is current. Pages nobody wrote
// since `generation` need no comparison.
const auto current = GuestMemory::CollectWrites(descriptor.baseAddress, static_cast<std::size_t>(guestBytes));
const auto scopedCurrent = current != 0 || !scoped ? current : GuestMemory::CollectWrites(scopeBegin, scopeBytes);
std::vector<std::uint64_t> collected;
if (scopedCurrent == 0) {
collected.resize(trackedLayers);
for (auto layer = firstLayer; layer < lastLayer; ++layer) collected[layer] = collectedLayerGeneration(layer, 0);
}
const auto currentFor = [&](std::uint32_t layer) {
if (scoped && (layer < firstLayer || layer >= lastLayer)) return current;
return scopedCurrent != 0 ? scopedCurrent : collected[layer];
};
// Per tracked layer: changed since its generation (a keys change makes every layer stale).
std::vector<bool> changed(trackedLayers, false);
// Per 64 KiB block of the surface (blockGenerations): stamped since its layer's generation, and
// by a CPU store; scanned once when anything changed.
std::vector<std::uint64_t> generations;
std::vector<std::uint8_t> stampedBlocks, cpuBlocks;
std::vector<bool> outside;
// Units taken from the alias's image instead of guest memory.
std::vector<bool> borrowed;
bool anyBorrowed = false;
@@ -2103,6 +2139,7 @@ bool StorageTexture::Refresh() {
const auto equalsOriginal = [&] {
// Named for the [hooksync] attribution: the compare goes through the flush hook.
const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::TextureCompare);
if (scoped) return GuestMemory::EqualsCommitted(scopeBegin, std::span<const std::byte>(original).subspan(static_cast<std::size_t>(scopeBegin - descriptor.baseAddress), scopeBytes));
return GuestMemory::EqualsCommitted(descriptor.baseAddress, original);
};
const auto scanBlocks = [&] {
@@ -2111,13 +2148,19 @@ bool StorageTexture::Refresh() {
cpuBlocks.assign(generations.size(), 0);
tracked = GuestMemory::ChangedBlocks(descriptor.baseAddress, static_cast<std::size_t>(guestBytes), generations, stampedBlocks, cpuBlocks);
if (!tracked) awaitStoredSave(nullptr);
compared = !tracked && compareUntracked(descriptor.baseAddress, static_cast<std::size_t>(guestBytes), stampedBlocks, true);
const auto firstBlock = static_cast<std::size_t>(scopeBegin / 65536 - descriptor.baseAddress / 65536);
const auto blocks = static_cast<std::size_t>((scopeBegin + scopeBytes - 1) / 65536 - scopeBegin / 65536 + 1);
compared = !tracked && (scoped ? compareUntracked(scopeBegin, scopeBytes, std::span(stampedBlocks).subspan(firstBlock, blocks), true) : compareUntracked(descriptor.baseAddress, static_cast<std::size_t>(guestBytes), stampedBlocks, true));
if (compared) {
for (const auto& [from, unitGeneration] : keptAtUnwatch) {
const auto unit = static_cast<std::uint32_t>((from - descriptor.baseAddress) / trackedLayerBytes);
if (keptAtUnwatchHolds(from, unit)) stampedBlocks[static_cast<std::size_t>(from / 65536 - descriptor.baseAddress / 65536)] = GuestMemory::BlockWritten;
}
cpuBlocks = stampedBlocks;
if (scoped) {
std::fill(stampedBlocks.begin(), stampedBlocks.begin() + static_cast<std::ptrdiff_t>(firstBlock), GuestMemory::BlockWritten);
std::fill(stampedBlocks.begin() + static_cast<std::ptrdiff_t>(firstBlock + blocks), stampedBlocks.end(), GuestMemory::BlockWritten);
}
}
};
const auto keysStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
@@ -2136,7 +2179,7 @@ bool StorageTexture::Refresh() {
scanBlocks();
for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) {
if (stampedBlocks[unit] == 0) {
layerGeneration[unit] = current;
layerGeneration[unit] = currentFor(unit);
continue;
}
changed[unit] = true;
@@ -2162,6 +2205,15 @@ bool StorageTexture::Refresh() {
}
unchanged = std::none_of(changed.begin(), changed.end(), [](bool value) { return value; });
}
if (scoped && !keysChanged) {
outside.assign(trackedLayers, false);
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
if ((layer >= firstLayer && layer < lastLayer) || (!changed[layer] && !compared)) continue;
outside[layer] = true;
changed[layer] = false;
}
unchanged = std::none_of(changed.begin(), changed.end(), [](bool value) { return value; });
}
for (std::size_t k = 0; k < cpuBlocks.size(); ++k) {
if (cpuBlocks[k] != 0 && (compared || (tracked && generations[k] != 0))) cpuWrote = true;
}
@@ -2172,12 +2224,11 @@ bool StorageTexture::Refresh() {
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
if (changed[layer] && layerPending[layer]) pendingChanged = true;
}
if (!unchanged && !keysChanged && !pendingChanged && originalHolds(descriptor.baseAddress, static_cast<std::size_t>(guestBytes)) && equalsOriginal()) {
if (!unchanged && !keysChanged && !pendingChanged && (scoped ? originalHolds(scopeBegin, scopeBytes) : originalHolds(descriptor.baseAddress, static_cast<std::size_t>(guestBytes))) && equalsOriginal()) {
unchanged = true;
stamped = false;
if (!scoped) outside.clear();
}
// Only the direct path uploads the selected layers alone (see upload); the others replace
// the whole image, so every pending layer's results are stored first.
direct = keys == DccKeys::Uncompressed && HostImportFor(context, descriptor.baseAddress, static_cast<std::size_t>(guestBytes)) != nullptr;
if (alias != nullptr && direct) {
// The alias's pending units that nothing stamped since its generation (a CPU store or a
@@ -2202,11 +2253,16 @@ bool StorageTexture::Refresh() {
}
}
}
const bool outsideChanged = std::any_of(outside.begin(), outside.end(), [](bool value) { return value; });
if (scoped) noteLayersBehind(firstLayer, lastLayer, outside);
else layerBehind.clear();
if (unchanged) {
++Profile().storageReused;
layerGeneration.assign(trackedLayers, current);
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
if (!outsideChanged || !outside[layer]) layerGeneration[layer] = currentFor(layer);
}
refreshGeneration();
takeRefreshProof(alias != nullptr);
if (layerBehind.empty()) takeRefreshProof(alias != nullptr);
if (profile) LookupOutcomes::Add(stamped ? LookupOutcomes::RefreshUnchanged : LookupOutcomes::RefreshCompared, start);
return true;
}
@@ -2256,6 +2312,7 @@ bool StorageTexture::Refresh() {
}
std::fprintf(stderr, "[dcc-keys] refresh 0x%llx+0x%llx keys %s -> %s (dirty %d, %zu pending units, %zu cpu-stamped units%s)\n", static_cast<unsigned long long>(descriptor.baseAddress), static_cast<unsigned long long>(guestBytes), DccKeysName(uploadedKeys), DccKeysName(keys), dirty ? 1 : 0, pendingUnits, stampedUnits, keyFlip ? ", flip keeps unstamped results" : "");
}
const bool selectedCpu = !direct && keys == DccKeys::Uncompressed && !cpuUploadLayers(changed).empty();
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
if (!layerPending[layer]) continue;
if (keyFlip && layerGeneration[layer] != 0 && stampedBlocks[layer] != GuestMemory::BlockWritten) {
@@ -2274,7 +2331,7 @@ bool StorageTexture::Refresh() {
dropped = true;
continue;
}
} else if (direct) {
} else if (direct || selectedCpu) {
continue;
}
stored[layer] = true;
@@ -2296,7 +2353,7 @@ bool StorageTexture::Refresh() {
uploadReason = "alias";
borrowUnits(*alias, borrowed);
for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) {
if (borrowed[unit]) layerGeneration[unit] = current;
if (borrowed[unit]) layerGeneration[unit] = currentFor(unit);
}
refreshGeneration();
}
@@ -2409,7 +2466,7 @@ void traceKeyStore(const char* path, const GuestTextureResource& descriptor, std
void StorageTexture::captureGuestBytes(const std::vector<bool>* layers) {
original.resize(static_cast<std::size_t>(guestBytes));
comparedGuestBytes = {};
comparedRanges = {};
std::vector<bool> selected(trackedLayers, true);
if (layers != nullptr) selected = *layers;
for (const auto& [begin, end] : unitRuns(selected)) GuestMemory::FlushGpuWrites(descriptor.baseAddress + begin, static_cast<std::size_t>(end - begin));
@@ -2446,11 +2503,10 @@ bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes,
Require(changed.size() == (end - 1) / blockBytes - first + 1, "texture comparison block count differs");
const auto* recorder = Recorder::Active();
const auto epoch = GuestMemory::CollectEpoch();
const bool whole = address == descriptor.baseAddress && bytes == guestBytes;
const auto hint = memoize && epoch != 0 && whole ? GuestMemory::UntrackedHint(address, bytes) : 0;
const auto hint = memoize && epoch != 0 ? GuestMemory::UntrackedHint(address, bytes) : 0;
const std::array<std::uint64_t, 5> stamp{hint != 0, hint != 0 ? hint : epoch, GuestMemory::NewestStamp(address, bytes), GuestMemory::ForgetSerial(), recorder != nullptr ? recorder->NewestWriteNote(address, bytes) : 0};
const bool cacheable = memoize && epoch != 0 && (stamp[3] & 1u) == 0 && whole;
if (cacheable && comparedGuestBytes == stamp) {
const bool cacheable = memoize && epoch != 0 && (stamp[3] & 1u) == 0;
if (cacheable && std::any_of(comparedRanges.begin(), comparedRanges.end(), [&](const ComparedRange& entry) { return entry.address == address && entry.bytes == bytes && entry.stamp == stamp; })) {
std::fill(changed.begin(), changed.end(), GuestMemory::BlockUnchanged);
return true;
}
@@ -2458,10 +2514,14 @@ bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes,
const auto saved = std::span(original).subspan(static_cast<std::size_t>(address - descriptor.baseAddress), bytes);
if (GuestMemory::EqualsCommittedUnsynced(address, saved)) {
std::fill(changed.begin(), changed.end(), GuestMemory::BlockUnchanged);
if (cacheable) comparedGuestBytes = stamp;
if (cacheable) {
auto found = std::find_if(comparedRanges.begin(), comparedRanges.end(), [&](const ComparedRange& entry) { return entry.address == address && entry.bytes == bytes; });
if (found == comparedRanges.end()) found = comparedRanges.begin() + nextComparedRange++ % comparedRanges.size();
*found = {address, bytes, stamp};
}
return true;
}
comparedGuestBytes = {};
comparedRanges = {};
for (std::size_t index = 0; index < changed.size(); ++index) {
const auto begin = std::max(address, (first + index) * blockBytes);
const auto stop = std::min(end, (first + index + 1) * blockBytes);
@@ -2473,6 +2533,25 @@ bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes,
constexpr std::uint64_t GenerationBaselineBytes = 1ull << 20;
std::vector<bool> StorageTexture::cpuUploadLayers(const std::vector<bool>& units) const {
if (descriptor.dimension != TextureDimension::k2DArray || descriptor.mipCount != 1 || descriptor.dccAddress != 0 || IsBlockCompressed(descriptor.format) || geometry.thick || arrayLayers <= 1 || geometry.imageLayers != arrayLayers || trackedLayerBytes == 0 || units.size() != trackedLayers) return {};
std::vector<bool> selected(arrayLayers, false);
for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) {
const auto begin = geometry.GuestLayerOffset(layer);
const auto end = begin + geometry.layerBytes;
if (end <= begin || end > guestBytes || begin % trackedLayerBytes != 0 || (end % trackedLayerBytes != 0 && end != guestBytes)) return {};
const auto first = static_cast<std::uint32_t>(begin / trackedLayerBytes);
const auto last = static_cast<std::uint32_t>((end + trackedLayerBytes - 1) / trackedLayerBytes);
if (first >= last || last > trackedLayers) return {};
selected[layer] = units[first];
for (auto unit = first + 1; unit < last; ++unit) {
if (units[unit] != selected[layer]) return {};
}
}
if (std::none_of(selected.begin(), selected.end(), [](bool value) { return value; }) || std::all_of(selected.begin(), selected.end(), [](bool value) { return value; })) return {};
return selected;
}
void StorageTexture::upload(const std::vector<bool>* layers) {
const bool profile = LookupOutcomes::Profiled();
const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
@@ -2481,8 +2560,6 @@ void StorageTexture::upload(const std::vector<bool>* layers) {
CaptureTrace::Log("upload image=%llx bytes=%llu generation=%llu reason=%s partial=%d", static_cast<unsigned long long>(descriptor.baseAddress), static_cast<unsigned long long>(guestBytes), static_cast<unsigned long long>(generation), uploadReason, layers != nullptr);
const auto elementBytes = BytesPerElement(descriptor.format);
const auto linearBytes = sliceLinearBytes * arrayLayers;
// A layer selection applies to the direct path alone (the others upload everything); it names
// array layers, which the tracked layers are when there are several.
if (layers != nullptr && ((!blockUnits && trackedLayers != arrayLayers) || std::all_of(layers->begin(), layers->end(), [](bool selected) { return selected; }))) layers = nullptr;
const auto now = GuestMemory::CollectWrites(descriptor.baseAddress, static_cast<std::size_t>(guestBytes));
const auto stampLayers = [&](bool selectedOnly) {
@@ -2494,7 +2571,7 @@ void StorageTexture::upload(const std::vector<bool>* layers) {
}
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
if (!whole && !(*layers)[layer]) continue;
layerGeneration[layer] = now;
layerGeneration[layer] = collectedLayerGeneration(layer, now);
if (whole || generationBaseline.size() != guestBytes) continue;
const auto offset = static_cast<std::size_t>(layerBegin(layer) - descriptor.baseAddress);
GuestMemory::ReadCommitted(layerBegin(layer), std::span<std::byte>(generationBaseline).subspan(offset, static_cast<std::size_t>(layerBytes(layer))));
@@ -2689,15 +2766,36 @@ void StorageTexture::upload(const std::vector<bool>* layers) {
if (profile) LookupOutcomes::Add(LookupOutcomes::UploadDirect, start);
return;
}
layerOriginal.assign(trackedLayers, true);
cancelStoredSaves();
forgetBorrowed(0, trackedLayers);
stampLayers(false);
original.resize(static_cast<std::size_t>(guestBytes));
GuestMemory::ReadCommitted(descriptor.baseAddress, original);
const auto cpuLayers = uploadedKeys == DccKeys::Uncompressed && layers != nullptr ? cpuUploadLayers(*layers) : std::vector<bool>{};
const bool selectedCpu = !cpuLayers.empty();
std::uint64_t uploadedBytes = guestBytes;
if (selectedCpu) {
captureGuestBytes(layers);
for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) {
if ((*layers)[unit]) forgetBorrowed(unit, 1);
}
stampLayers(true);
uploadedBytes = 0;
for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) {
if (cpuLayers[layer]) uploadedBytes += geometry.layerBytes;
}
} else {
layerOriginal.assign(trackedLayers, true);
cancelStoredSaves();
forgetBorrowed(0, trackedLayers);
stampLayers(false);
original.resize(static_cast<std::size_t>(guestBytes));
GuestMemory::ReadCommitted(descriptor.baseAddress, original);
}
{
Buffer staging(context, original.size(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
if (uploadedKeys == DccKeys::Uncompressed) std::memcpy(staging.Bytes().data(), original.data(), original.size());
if (selectedCpu) {
for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) {
if (!cpuLayers[layer]) continue;
const auto offset = geometry.GuestLayerOffset(layer);
std::memcpy(staging.Bytes().data() + offset, original.data() + offset, static_cast<std::size_t>(geometry.layerBytes));
}
} else if (uploadedKeys == DccKeys::Uncompressed) std::memcpy(staging.Bytes().data(), original.data(), original.size());
else ReadTextureSurface(descriptor, uploadedKeys, staging.Bytes().first(original.size()));
DeviceBuffer tiled(context, original.size(), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT);
DeviceBuffer linear(context, static_cast<std::size_t>(linearBytes), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
@@ -2705,9 +2803,16 @@ void StorageTexture::upload(const std::vector<bool>* layers) {
CommandBatch batch(context);
const auto commands = batch.Handle();
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT);
CopyBuffer(context, commands, staging.Handle(), 0, tiled.Handle(), 0, original.size());
if (selectedCpu) {
for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) {
if (!cpuLayers[layer]) continue;
const auto offset = geometry.GuestLayerOffset(layer);
CopyBuffer(context, commands, staging.Handle(), offset, tiled.Handle(), offset, geometry.layerBytes);
}
} else CopyBuffer(context, commands, staging.Handle(), 0, tiled.Handle(), 0, original.size());
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT);
for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) {
if (selectedCpu && !cpuLayers[layer]) continue;
for (std::uint32_t level = 0; level < mips.size(); ++level) {
if (!geometry.HasLayer(level, layer)) continue;
const auto& mip = mips[level];
@@ -2716,15 +2821,16 @@ void StorageTexture::upload(const std::vector<bool>* layers) {
}
const auto linearRead = WholeBufferBarrier(linear.Handle(), VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT);
VkImageMemoryBarrier toTransfer{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
toTransfer.srcAccessMask = selectedCpu ? VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT : 0u;
toTransfer.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
toTransfer.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
toTransfer.oldLayout = selectedCpu ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_UNDEFINED;
toTransfer.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
toTransfer.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toTransfer.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toTransfer.image = image;
toTransfer.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, descriptor.mipCount, 0, geometry.imageLayers};
context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &linearRead, 1, &toTransfer);
const auto regions = CopyRegions();
context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | (selectedCpu ? VK_PIPELINE_STAGE_ALL_COMMANDS_BIT : 0u), VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &linearRead, 1, &toTransfer);
const auto regions = CopyRegions(selectedCpu ? &cpuLayers : nullptr);
context.Function<PFN_vkCmdCopyBufferToImage>("vkCmdCopyBufferToImage")(commands, linear.Handle(), image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast<std::uint32_t>(regions.size()), regions.data());
VkImageMemoryBarrier toGeneral = toTransfer;
toGeneral.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
@@ -2736,7 +2842,7 @@ void StorageTexture::upload(const std::vector<bool>* layers) {
batch.SubmitAndWait();
APS5_LOG_CHARS_OUT("StorageTexture upload done");
}
countStorageUpload(2, guestBytes);
countStorageUpload(2, uploadedBytes);
++version;
if (profile) LookupOutcomes::Add(LookupOutcomes::UploadCpu, start);
}
@@ -2834,6 +2940,16 @@ std::vector<StorageTexture::SliceWindow> StorageTexture::sliceWindows(std::span<
return windows;
}
std::vector<VkRect2D> StorageTexture::UnitRects(const std::vector<bool>& units) const {
std::vector<VkRect2D> rects;
if (units.size() != trackedLayers) return rects;
const auto runs = unitRuns(units);
for (const auto& window : sliceWindows(runs)) {
for (const auto& region : window.regions) rects.push_back({{region.imageOffset.x, region.imageOffset.y}, {region.imageExtent.width, region.imageExtent.height}});
}
return rects;
}
std::vector<std::pair<std::uint64_t, std::uint64_t>> StorageTexture::uncoveredBytes(std::span<const std::pair<std::uint64_t, std::uint64_t>> runs) const {
std::vector<std::pair<std::uint64_t, std::uint64_t>> covered;
if (!geometry.thick) {
@@ -3565,6 +3681,51 @@ void StorageTexture::refreshGeneration() {
generation = *std::min_element(layerGeneration.begin(), layerGeneration.end());
}
std::uint64_t StorageTexture::GenerationFor(std::uint64_t address, std::uint64_t bytes) const {
if (layerBehind.empty() && generation != 0) return generation;
std::optional<std::uint64_t> oldest;
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
const auto begin = layerBegin(layer);
if (address >= begin + layerBytes(layer) || begin >= address + bytes) continue;
if (!layerBehind.empty() && layerBehind[layer]) return 0;
oldest = std::min(oldest.value_or(layerGeneration[layer]), layerGeneration[layer]);
}
return oldest.value_or(generation);
}
std::uint64_t StorageTexture::collectedLayerGeneration(std::uint32_t layer, std::uint64_t whole, bool uncached) const {
if (whole != 0 || trackedLayers <= 1) return whole;
const auto begin = layerBegin(layer);
const auto bytes = static_cast<std::size_t>(layerBytes(layer));
return uncached ? GuestMemory::CollectWritesUncached(begin, bytes) : GuestMemory::CollectWrites(begin, bytes);
}
void StorageTexture::noteLayersBehind(std::uint32_t firstLayer, std::uint32_t lastLayer, const std::vector<bool>& outside) {
if (layerBehind.size() != trackedLayers) layerBehind.assign(trackedLayers, false);
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
if (layer >= firstLayer && layer < lastLayer) layerBehind[layer] = false;
else if (layer < outside.size() && outside[layer]) layerBehind[layer] = true;
}
const auto layersEnd = layerBegin(lastLayer - 1) + layerBytes(lastLayer - 1);
const std::array<std::pair<std::uint64_t, std::uint64_t>, 2> ranges{{{descriptor.baseAddress, layerBegin(firstLayer)}, {layersEnd, descriptor.baseAddress + guestBytes}}};
for (const auto& [from, to] : ranges) {
if (to <= from) continue;
for (const auto& other : overlappingPending(from, static_cast<std::size_t>(to - from))) {
const bool whole = other->descriptor.dccAddress != 0 && other->uploadedKeys != DccKeys::Uncompressed;
for (std::uint32_t unit = 0; unit < other->trackedLayers; ++unit) {
if (!whole && !other->layerPending[unit]) continue;
const auto begin = std::max(from, other->layerBegin(unit));
const auto end = std::min(to, other->layerBegin(unit) + other->layerBytes(unit));
if (end <= begin) continue;
const auto first = static_cast<std::uint32_t>((begin - descriptor.baseAddress) / trackedLayerBytes);
const auto last = static_cast<std::uint32_t>(std::min<std::uint64_t>((end - descriptor.baseAddress + trackedLayerBytes - 1) / trackedLayerBytes, trackedLayers));
for (auto layer = first; layer < last; ++layer) layerBehind[layer] = true;
}
}
}
if (std::none_of(layerBehind.begin(), layerBehind.end(), [](bool behind) { return behind; })) layerBehind.clear();
}
void StorageTexture::markLayersPending(std::uint32_t first, std::uint32_t count) {
CaptureTrace::Log("image-write image=%llx first=%u count=%u generation=%llu", static_cast<unsigned long long>(descriptor.baseAddress), first, count, static_cast<unsigned long long>(generation));
++writes;
@@ -3671,6 +3832,18 @@ bool StorageTexture::pendingUnitInside(std::uint64_t address, std::size_t bytes)
return false;
}
namespace {
void waitForIdlePublish(const char* reason) {
if (reason == nullptr || std::strcmp(reason, "idle") != 0 || Recorder::InCompletion()) return;
if (auto* recorder = Recorder::Active()) {
Recorder::CountSync(0);
recorder->Sync();
}
}
}
bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, const StorageTexture* except, const char* reason, PublishScope scope, bool* published) {
static_cast<void>(FlushDepthPlanes(address, bytes));
if (published != nullptr) *published = false;
@@ -3764,6 +3937,7 @@ bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, cons
// import, so they are published under the same hold, after every store.
if (scope != PublishScope::None) {
const auto units = PublishShadow(address, bytes, scope, PublishReasonFor(reason));
if (units != 0) waitForIdlePublish(reason);
if (published != nullptr) *published = units != 0;
}
return true;
@@ -3828,7 +4002,7 @@ void StorageTexture::saveBeforeUnwatch() {
const auto runs = unitRuns(units);
if (runs.empty()) return;
original.resize(static_cast<std::size_t>(guestBytes));
comparedGuestBytes = {};
comparedRanges = {};
for (const auto& [begin, end] : runs) GuestMemory::ReadCommittedUnsynced(descriptor.baseAddress + begin, std::span(original).subspan(static_cast<std::size_t>(begin), static_cast<std::size_t>(end - begin)));
for (const auto& [begin, end] : runs) GuestMemory::CollectWritesUncached(descriptor.baseAddress + begin, static_cast<std::size_t>(end - begin));
constexpr std::uint64_t block = 65536;
@@ -3889,7 +4063,7 @@ void StorageTexture::applyStoredSave(std::uint64_t ticket, const std::vector<std
for (const auto unit : units) {
if (savedTicket[unit] != ticket) continue;
if (original.size() != guestBytes) original.resize(static_cast<std::size_t>(guestBytes));
comparedGuestBytes = {};
comparedRanges = {};
const auto offset = static_cast<std::size_t>(layerBegin(unit) - descriptor.baseAddress);
std::memcpy(original.data() + offset, stored.data() + offset, static_cast<std::size_t>(layerBytes(unit)));
layerOriginal[unit] = true;
@@ -3929,7 +4103,9 @@ void StorageTexture::awaitStoredSave(const std::vector<bool>* layers) {
bool StorageTexture::PublishShadowsOnly(std::uint64_t address, std::size_t bytes, PublishScope scope, const char* reason) {
if (scope == PublishScope::None || !AnyShadowedOverlaps(address, bytes)) return false;
std::lock_guard gpu(GuestMemory::GpuMutex());
return PublishShadow(address, bytes, scope, PublishReasonFor(reason)) != 0;
if (PublishShadow(address, bytes, scope, PublishReasonFor(reason)) == 0) return false;
waitForIdlePublish(reason);
return true;
}
namespace {
@@ -4526,7 +4702,8 @@ bool StorageTexture::clearByKeysFill(DccKeys keys, std::uint8_t key) {
filledKeys = DccKeys::Uncompressed;
keyProof = {};
forgetBorrowed(0, trackedLayers);
layerGeneration.assign(trackedLayers, GuestMemory::CollectWrites(descriptor.baseAddress, static_cast<std::size_t>(guestBytes)));
const auto now = GuestMemory::CollectWrites(descriptor.baseAddress, static_cast<std::size_t>(guestBytes));
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) layerGeneration[layer] = collectedLayerGeneration(layer, now);
refreshGeneration();
++version;
if (traceKeys) std::fprintf(stderr, "[dcc-keys] key fill %s clears 0x%llx+0x%llx at once (keys 0x%llx, %zu pending units dropped, dirty %d)\n", DccKeysName(keys), static_cast<unsigned long long>(descriptor.baseAddress), static_cast<unsigned long long>(guestBytes), static_cast<unsigned long long>(descriptor.dccAddress), droppedUnits, wasDirty ? 1 : 0);
@@ -4687,7 +4864,8 @@ bool StorageTexture::FillClear(std::span<const std::uint32_t, 4> pattern, std::u
return true;
}
if (!watched) captureGuestBytes(nullptr);
layerGeneration.assign(trackedLayers, GuestMemory::CollectWritesUncached(descriptor.baseAddress, static_cast<std::size_t>(guestBytes)));
const auto now = GuestMemory::CollectWritesUncached(descriptor.baseAddress, static_cast<std::size_t>(guestBytes));
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) layerGeneration[layer] = collectedLayerGeneration(layer, now, true);
refreshGeneration();
forgetBorrowed(0, trackedLayers);
MarkDirty();
@@ -4805,12 +4983,45 @@ void StorageTexture::MarkWholeWritten() {
captureGuestBytes(nullptr);
}
uploadedKeys = DccKeys::Uncompressed;
layerGeneration.assign(trackedLayers, GuestMemory::CollectWritesUncached(descriptor.baseAddress, static_cast<std::size_t>(guestBytes)));
const auto now = GuestMemory::CollectWritesUncached(descriptor.baseAddress, static_cast<std::size_t>(guestBytes));
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) layerGeneration[layer] = collectedLayerGeneration(layer, now, true);
refreshGeneration();
forgetBorrowed(0, trackedLayers);
MarkDirty();
}
void StorageTexture::MarkUnitsWritten(const std::vector<bool>& units) {
if (!blockUnits || descriptor.dccAddress != 0 || units.size() != trackedLayers || std::all_of(units.begin(), units.end(), [](bool unit) { return unit; })) {
MarkWholeWritten();
return;
}
const auto runs = unitRuns(units);
if (runs.empty()) return;
if (GuestMemory::Watched(descriptor.baseAddress, static_cast<std::size_t>(guestBytes))) {
for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) {
if (!units[unit]) continue;
layerOriginal[unit] = false;
cancelStoredSave(unit);
}
} else {
captureGuestBytes(&units);
}
uploadedKeys = DccKeys::Uncompressed;
for (const auto& [begin, end] : runs) {
const auto first = static_cast<std::uint32_t>(begin / trackedLayerBytes);
const auto count = static_cast<std::uint32_t>((end - begin + trackedLayerBytes - 1) / trackedLayerBytes);
const auto collected = GuestMemory::CollectWritesUncached(descriptor.baseAddress + begin, static_cast<std::size_t>(end - begin));
for (auto unit = first; unit < first + count; ++unit) layerGeneration[unit] = collected;
}
refreshGeneration();
for (const auto& [begin, end] : runs) {
const auto first = static_cast<std::uint32_t>(begin / trackedLayerBytes);
const auto count = static_cast<std::uint32_t>((end - begin + trackedLayerBytes - 1) / trackedLayerBytes);
forgetBorrowed(first, count);
markLayersPending(first, count);
}
}
// See AdjacentGenerationEnabled above.
std::vector<StorageTexture::Adjacent> StorageTexture::adjacentPendingUnchanged(std::uint64_t firstBlock, std::uint64_t lastBlock) const {
std::vector<Adjacent> adjacent;
@@ -5267,7 +5478,7 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
if (!advances && adjacent.empty() && shadowed.empty()) return;
const auto now = memoizedCollect ? GuestMemory::CollectWrites(firstStored, static_cast<std::size_t>(lastStored - firstStored)) : GuestMemory::CollectWritesUncached(firstStored, static_cast<std::size_t>(lastStored - firstStored));
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
if (layers[layer] && !skippedLayer[layer]) layerGeneration[layer] = now;
if (layers[layer] && !skippedLayer[layer]) layerGeneration[layer] = collectedLayerGeneration(layer, now, !memoizedCollect);
}
refreshGeneration();
advanceAdjacent(adjacent, now, firstBlock, lastBlock);
@@ -4,6 +4,8 @@
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
#include "prx/libSceAgcDriver/Graphics/shaders/TextureDetile_spv.h"
#include "prx/libSceAgcDriver/Graphics/shaders/CmaskClear_spv.h"
#include "prx/libSceAgcDriver/Graphics/shaders/DepthSampleMultisample_spv.h"
#include "prx/libSceAgcDriver/Graphics/shaders/DepthSampleSingle_spv.h"
#include "prx/libSceAgcDriver/Graphics/shaders/TextureDetileImage_spv.h"
#include "prx/libSceAgcDriver/Graphics/shaders/TextureRepack_spv.h"
#include <algorithm>
@@ -108,6 +110,7 @@ void TextureDetiler::release() noexcept {
for (const auto pool : readbackDescriptorSets.pools) context.Function<PFN_vkDestroyDescriptorPool>("vkDestroyDescriptorPool")(context.device, pool, nullptr);
for (const auto& entry : readbackPipelines) context.Function<PFN_vkDestroyPipeline>("vkDestroyPipeline")(context.device, entry.second, nullptr);
for (const auto shader : readbackModules) if (shader) context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, shader, nullptr);
if (multisampleUploadModule) context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, multisampleUploadModule, nullptr);
if (readbackPipelineLayout) context.Function<PFN_vkDestroyPipelineLayout>("vkDestroyPipelineLayout")(context.device, readbackPipelineLayout, nullptr);
if (readbackDescriptorLayout) context.Function<PFN_vkDestroyDescriptorSetLayout>("vkDestroyDescriptorSetLayout")(context.device, readbackDescriptorLayout, nullptr);
for (const auto pool : cmaskDescriptorSets.pools) context.Function<PFN_vkDestroyDescriptorPool>("vkDestroyDescriptorPool")(context.device, pool, nullptr);
@@ -117,6 +120,11 @@ void TextureDetiler::release() noexcept {
cmaskErrors.reset();
for (const auto pool : descriptorSets.pools) context.Function<PFN_vkDestroyDescriptorPool>("vkDestroyDescriptorPool")(context.device, pool, nullptr);
for (const auto pool : imageDescriptorSets.pools) context.Function<PFN_vkDestroyDescriptorPool>("vkDestroyDescriptorPool")(context.device, pool, nullptr);
for (const auto pool : depthSampleSets.pools) context.Function<PFN_vkDestroyDescriptorPool>("vkDestroyDescriptorPool")(context.device, pool, nullptr);
for (const auto pipeline : depthSamplePipelines) if (pipeline) context.Function<PFN_vkDestroyPipeline>("vkDestroyPipeline")(context.device, pipeline, nullptr);
if (depthSamplePipelineLayout) context.Function<PFN_vkDestroyPipelineLayout>("vkDestroyPipelineLayout")(context.device, depthSamplePipelineLayout, nullptr);
if (depthSampleLayout) context.Function<PFN_vkDestroyDescriptorSetLayout>("vkDestroyDescriptorSetLayout")(context.device, depthSampleLayout, nullptr);
if (depthSampler) context.Function<PFN_vkDestroySampler>("vkDestroySampler")(context.device, depthSampler, nullptr);
if (imageModule) context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, imageModule, nullptr);
if (repackPipeline) context.Function<PFN_vkDestroyPipeline>("vkDestroyPipeline")(context.device, repackPipeline, nullptr);
if (repackModule) context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, repackModule, nullptr);
@@ -394,6 +402,79 @@ void TextureDetiler::DispatchCmaskClear(VkCommandBuffer commands, VkBuffer cmask
barrier(VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, anyAccess);
}
void TextureDetiler::DispatchDepthSample(VkCommandBuffer commands, VkImageView depth, bool multisampled, VkImageView target, std::uint32_t width, std::uint32_t height, std::uint32_t sample) {
Require(commands != VK_NULL_HANDLE && depth != VK_NULL_HANDLE && target != VK_NULL_HANDLE, "a depth sample copy needs a command buffer, the depth view and the target view");
if (depthSamplePipelineLayout == VK_NULL_HANDLE) {
VkSamplerCreateInfo samplerInfo{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
samplerInfo.magFilter = VK_FILTER_NEAREST;
samplerInfo.minFilter = VK_FILTER_NEAREST;
samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
Check(context.Function<PFN_vkCreateSampler>("vkCreateSampler")(context.device, &samplerInfo, nullptr, &depthSampler), "vkCreateSampler depth sample copy");
std::array<VkDescriptorSetLayoutBinding, 2> bindings{};
bindings[0] = {0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, &depthSampler};
bindings[1] = {1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr};
VkDescriptorSetLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
layoutInfo.bindingCount = static_cast<std::uint32_t>(bindings.size());
layoutInfo.pBindings = bindings.data();
Check(context.Function<PFN_vkCreateDescriptorSetLayout>("vkCreateDescriptorSetLayout")(context.device, &layoutInfo, nullptr, &depthSampleLayout), "vkCreateDescriptorSetLayout depth sample copy");
const VkPushConstantRange pushRange{VK_SHADER_STAGE_COMPUTE_BIT, 0, 3 * sizeof(std::uint32_t)};
VkPipelineLayoutCreateInfo pipelineLayoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
pipelineLayoutInfo.setLayoutCount = 1;
pipelineLayoutInfo.pSetLayouts = &depthSampleLayout;
pipelineLayoutInfo.pushConstantRangeCount = 1;
pipelineLayoutInfo.pPushConstantRanges = &pushRange;
Check(context.Function<PFN_vkCreatePipelineLayout>("vkCreatePipelineLayout")(context.device, &pipelineLayoutInfo, nullptr, &depthSamplePipelineLayout), "vkCreatePipelineLayout depth sample copy");
}
auto& depthSamplePipeline = depthSamplePipelines[multisampled ? 1u : 0u];
if (depthSamplePipeline == VK_NULL_HANDLE) {
VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
moduleInfo.codeSize = multisampled ? sizeof(DEPTH_SAMPLE_MULTISAMPLE_SPV) : sizeof(DEPTH_SAMPLE_SINGLE_SPV);
moduleInfo.pCode = multisampled ? DEPTH_SAMPLE_MULTISAMPLE_SPV : DEPTH_SAMPLE_SINGLE_SPV;
VkShaderModule depthModule = VK_NULL_HANDLE;
Check(context.Function<PFN_vkCreateShaderModule>("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &depthModule), "vkCreateShaderModule depth sample copy");
VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO};
pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO};
pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT;
pipelineInfo.stage.module = depthModule;
pipelineInfo.stage.pName = "main";
pipelineInfo.layout = depthSamplePipelineLayout;
const auto created = context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &depthSamplePipeline);
context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, depthModule, nullptr);
Check(created, "vkCreateComputePipelines depth sample copy");
}
const auto set = allocateFrom(depthSampleSets, depthSampleLayout, {{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 64}, {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 64}}, "vkCreateDescriptorPool depth sample copy", "vkAllocateDescriptorSets depth sample copy", "vkResetDescriptorPool depth sample copy");
const VkDescriptorImageInfo depthInfo{VK_NULL_HANDLE, depth, VK_IMAGE_LAYOUT_GENERAL};
const VkDescriptorImageInfo targetInfo{VK_NULL_HANDLE, target, VK_IMAGE_LAYOUT_GENERAL};
std::array<VkWriteDescriptorSet, 2> writes{};
for (std::uint32_t index = 0; index < writes.size(); ++index) {
writes[index].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
writes[index].dstSet = set;
writes[index].dstBinding = index;
writes[index].descriptorCount = 1;
writes[index].descriptorType = index == 0 ? VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER : VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
}
writes[0].pImageInfo = &depthInfo;
writes[1].pImageInfo = &targetInfo;
context.Function<PFN_vkUpdateDescriptorSets>("vkUpdateDescriptorSets")(context.device, static_cast<std::uint32_t>(writes.size()), writes.data(), 0, nullptr);
const auto barrier = [&](VkPipelineStageFlags source, VkPipelineStageFlags destination, VkAccessFlags sourceAccess, VkAccessFlags destinationAccess) {
VkMemoryBarrier memory{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
memory.srcAccessMask = sourceAccess;
memory.dstAccessMask = destinationAccess;
context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, source, destination, 0, 1, &memory, 0, nullptr, 0, nullptr);
};
constexpr VkAccessFlags anyWrite = 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_HOST_WRITE_BIT;
constexpr VkAccessFlags anyAccess = anyWrite | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_HOST_READ_BIT;
barrier(VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, anyWrite, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT);
context.Function<PFN_vkCmdBindPipeline>("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, depthSamplePipeline);
context.Function<PFN_vkCmdBindDescriptorSets>("vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, depthSamplePipelineLayout, 0, 1, &set, 0, nullptr);
const std::array<std::uint32_t, 3> push{width, height, sample};
context.Function<PFN_vkCmdPushConstants>("vkCmdPushConstants")(commands, depthSamplePipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(push), push.data());
context.Function<PFN_vkCmdDispatch>("vkCmdDispatch")(commands, (width + 7u) / 8u, (height + 7u) / 8u, 1);
barrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT, anyAccess);
}
void TextureDetiler::DispatchImage(VkCommandBuffer commands, TextureTileMode tileMode, std::uint32_t elementBytes, VkBuffer tiled, std::uint64_t tiledOffset, VkImageView view, const TileMipLayout& layout, bool retile, std::uint32_t slice, const DetileWindow& window) {
Require(commands != VK_NULL_HANDLE, "texture detiling requires an active command buffer");
Require(window.rangeBegin < window.rangeEnd && window.tiledBase <= window.rangeBegin, "texture detiling window is empty");
@@ -147,3 +147,71 @@ int APS5_VABI sceAgcDriverGetAllocatedToolMemoryForGpuReset(uint64_t, void* rang
}
}
extern "C" {
int APS5_VABI sceAgcDriverAcquireHDRScopesACQ() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverGetCoredumpAddress() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverHp3dLockEnd() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverHp3dLockStart() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverIsEsvmProcess() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverNopDataWritePacket() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverReleaseHDRScopesACQ() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverSetTargetRingForDiag() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverSetValueToSharedGds() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverSetWorkloadStreamInactive() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverSubmitReprojectionAcb() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverSuspendPointSubmitCdbg() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverSysGetGfxAppGpuResetStatus() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
}
@@ -2227,6 +2227,95 @@ void importWatchTests() {
#endif
}
void sparseImportedHeapTests(const Context& context, const BdaTestAccess& access) {
#ifndef _WIN32
if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) {
std::cout << "write watch unavailable: sparse imported heaps not tested\n";
return;
}
constexpr std::size_t bytes = 65536;
void* raw = mmap(nullptr, 4 * bytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
Require(raw != MAP_FAILED, "cannot reserve the sparse imported heap fixture");
const auto base = (reinterpret_cast<std::uintptr_t>(raw) + bytes - 1) & ~(bytes - 1);
auto* guest = reinterpret_cast<std::uint8_t*>(base);
Require(mprotect(guest, bytes, PROT_READ | PROT_WRITE) == 0, "cannot commit the sparse imported heap prefix");
std::memset(guest, 0x57, bytes);
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(guest, bytes);
auto importing = context;
importing.hostImportAlignment = bytes;
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, guest, bytes, true, true, true);
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
const auto* imported = HostImportFor(importing, base, bytes);
Require(imported != nullptr, "the sparse heap prefix was not imported");
const auto before = MirrorCounters();
for (unsigned index = 0; index < 3; ++index) {
{
GuestBufferMemory described(importing);
described.AddReadable(base, 2 * bytes);
described.Upload(false);
std::uint32_t adjustment = 0;
const auto view = described.Descriptor(base, 2 * bytes, adjustment);
Require(view.buffer == imported->buffer && view.range == bytes && adjustment == 0, "the sparse descriptor did not bind its imported prefix");
Require(described.CopiedBytes() == 0 && MirrorCounters().rebuilds == before.rebuilds, "the sparse imported descriptor rebuilt or copied its heap mirror");
reject([&] { static_cast<void>(described.Descriptor(base + bytes + 16, 4, adjustment)); }, "starts past its copy");
described.WriteBack();
}
{
GuestBufferMemory addressed(importing);
addressed.AcquireRegistered();
addressed.Upload(true);
Require(MirrorCounters().rebuilds == before.rebuilds && MirrorCounters().heapMirrors == before.heapMirrors, "alternating descriptor and address builds rebuilt the imported heap mirror");
addressed.WriteBack();
}
}
{
auto misaligned = importing;
misaligned.limits.minStorageBufferOffsetAlignment = 256;
std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex());
Recorder recorder(misaligned);
recorder.Activate();
GuestBufferMemory memory(misaligned);
memory.AddReadable(base + 16, 2 * bytes - 16);
memory.Upload(false);
std::uint32_t adjustment = 0;
const auto view = memory.Descriptor(base + 16, 2 * bytes - 16, adjustment);
Require(view.buffer != imported->buffer, "a misaligned sparse descriptor bound its import in place");
Require(view.range != 0 && view.range <= bytes && memory.CopiedBytes() <= bytes && access.bytes(view.buffer)[view.offset + adjustment] == std::byte{0x57}, "a misaligned sparse descriptor does not serve its committed prefix alone");
memory.WriteBack();
}
{
GuestBufferMemory written(importing);
written.AddWritable(base, 2 * bytes);
written.Upload(false);
std::uint32_t adjustment = 0;
const auto view = written.Descriptor(base, 2 * bytes, adjustment);
Require(view.buffer != imported->buffer && view.range == bytes && MirrorCounters().rebuilds == before.rebuilds + 1, "a sparse writable descriptor bound a shortened import");
access.bytes(view.buffer)[view.offset] = std::byte{0x68};
guest[8] = 0x79;
written.WriteBack();
Require(guest[0] == 0x68 && guest[8] == 0x79, "the sparse writable fallback lost a GPU store or rolled back a CPU store");
}
mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, guest);
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
Require(HostImportFor(importing, base, bytes) == nullptr, "the sparse heap import outlived its range");
{
GuestBufferMemory sweep(importing);
sweep.AcquireRegistered();
sweep.Upload(true);
sweep.WriteBack();
}
Require(MirrorCounters().heapMirrors == before.heapMirrors, "the sparse imported heap fixture leaked its mirror");
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(guest, bytes);
munmap(raw, 4 * bytes);
std::cout << "Sparse imported heap tests passed\n";
#else
static_cast<void>(context);
static_cast<void>(access);
#endif
}
void heapMirrorTests(const Context& context, const BdaTestAccess& access) {
if (!GuestArena::GuestArenaAvailable_nid_postfix() || !GuestArena::GuestArenaWriteWatched_nid_postfix()) {
std::cout << "guest arena unavailable or not write-watched: heap mirrors not tested\n";
@@ -3439,6 +3528,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) {
importReportFloorTests(context, access, true);
importReportFloorTests(context, access, false);
importReportRefusalMemoryTests(context, access);
sparseImportedHeapTests(context, access);
heapMirrorTests(context, access);
importedHeapMirrorTests(context, access);
importWatchTests();
+743 -32
View File
@@ -830,7 +830,7 @@ void msaaEnableClearOneSampleTargetTests() {
queue.context[0x1b3] = queue.context[0x1b4] = 2u | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::SampleCoverage);
Require(AgcDriver::Graphics::PixelProgramSkipped(queue), "the depth-only draw did not skip its pixel program");
requireOneSampleDepth(queue, 0, "a one-sample depth target whose skipped pixel program has PS_ITER_SAMPLE and sample index and coverage inputs");
constexpr std::string_view depthRefused = "a one-sample depth target under a multisampled rasterizer without MSAA_ENABLE is unsupported for points or lines and with alpha-to-coverage, PS_ITER_SAMPLE, a sample index or coverage input, a sample-mask export, or DB_EQAA Z interpolation or overrasterization";
constexpr std::string_view depthRefused = "a one-sample depth target under a multisampled rasterizer without MSAA_ENABLE is unsupported for points or lines and with alpha-to-coverage, a sample index or coverage input, a sample-mask export, or DB_EQAA Z interpolation or overrasterization";
for (const bool color : {false, true}) {
for (const auto input : {ShaderRecompiler::PixelInput::Ancillary, ShaderRecompiler::PixelInput::SampleCoverage}) {
queue = depthTarget(color);
@@ -841,6 +841,10 @@ void msaaEnableClearOneSampleTargetTests() {
queue = depthTarget(color);
queue.context[0x203] |= 0x40u;
queue.context[0x293] = 0x10000u;
Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "the PS_ITER_SAMPLE draw skipped its pixel program");
requireOneSampleDepth(queue, color ? 1u : 0u, "a one-sample depth target whose running pixel program has PS_ITER_SAMPLE and no sample index or coverage input");
Require(!DecodeState(queue).perSampleShading, "a PS_ITER_SAMPLE program drawn as one sample still shades per sample");
queue.context[0x1b3] = queue.context[0x1b4] = 2u | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary);
expectFailure([&] { DecodeState(queue); }, depthRefused);
queue = depthTarget(color);
queue.context[0x1c4] = 9;
@@ -1070,21 +1074,26 @@ void ShaderStageTests() {
Require(AgcDriver::Graphics::DecodeState(lines).stages.mesh->provokingVertex == 2 && AgcDriver::Graphics::DecodeShaderStages(lines).mesh->provokingVertex == 2, "a last-vertex geometry draw does not take its flat inputs and layer from the third vertex of each output triangle");
lines.context[0x29b] = 1;
Require(AgcDriver::Graphics::DecodeState(lines).stages.mesh->provokingVertex == 1, "a last-vertex geometry draw does not take its flat inputs and layer from the second vertex of each output line");
lines.context[0x29b] = 0;
state = AgcDriver::Graphics::DecodeState(lines);
mesh = *state.stages.mesh;
Require(mesh.outputPrimitiveSize == 1 && mesh.maxPrimitives == 255 && AgcDriver::Graphics::RasterPrimitiveOf(state) == ShaderRecompiler::RasterPrimitive::Point, "a point-list subgroup did not decode as points");
Require(mesh.provokingVertex == 0, "a point draw has provoking vertex 0");
lines.context[0x205] = raster;
lines.context[0x29b] = 1;
lines.context[0x2ce] = 1;
expectFailure([&] { AgcDriver::Graphics::DecodeState(lines); }, "unsupported geometry input or output assembly");
lines.context[0x2ce] = 5;
for (const auto output : {0u, 3u, 0x80000001u, 0x101u}) {
for (const auto output : {3u, 0x80000001u, 0x101u}) {
lines.context[0x29b] = output;
expectFailure([&] { AgcDriver::Graphics::DecodeState(lines); }, "VGT_GS_OUT_PRIM_TYPE other than a triangle strip, or a line strip from a geometry program, is unsupported");
expectFailure([&] { AgcDriver::Graphics::DecodeState(lines); }, "VGT_GS_OUT_PRIM_TYPE other than a triangle strip, or a point list or line strip from a geometry program, is unsupported");
}
lines.userConfig[0x242] = 4;
lines.context[0x205] = 0x2a40;
lines.context[0x29b] = 2;
Require(AgcDriver::Graphics::DecodeState(lines).stages.mesh->primitivesPerGroup == 17, "triangle input with a triangle-strip output and a triangle polygon offset did not decode");
lines.context[0x29b] = 1;
expectFailure([&] { AgcDriver::Graphics::DecodeState(lines); }, "polygon mode, depth bias or nonstandard rasterization");
expectFailure([&] { AgcDriver::Graphics::DecodeState(lines); }, "depth bias or nonstandard rasterization");
}
queue.context[0x2ab] = 0;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "invalid VGT_ESGS_RING_ITEMSIZE");
@@ -1231,7 +1240,9 @@ void NggVertexWorkgroupTests() {
}
queue.context[0x2d5] = 0x12010;
queue.context[0x29b] = 1;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue, true); }, "VGT_GS_OUT_PRIM_TYPE other than a triangle strip, or a line strip from a geometry program, is unsupported");
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue, true); }, "VGT_GS_OUT_PRIM_TYPE other than a triangle strip, or a point list or line strip from a geometry program, is unsupported");
queue.context[0x29b] = 0;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue, true); }, "VGT_GS_OUT_PRIM_TYPE other than a triangle strip, or a point list or line strip from a geometry program, is unsupported");
}
constexpr std::array<std::uint8_t, 8> IdentityExports{0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u};
@@ -2251,7 +2262,41 @@ void DepthStencilTests() {
queue.context[0x10b] = 0x40;
queue.context[0x10c] = 0x05ffff02;
queue.context[0x200] = 0x00200211;
state = AgcDriver::Graphics::DecodeState(queue);
Require(state.stencilFront.passOp == VK_STENCIL_OP_INVERT && state.stencilFront.failOp == VK_STENCIL_OP_KEEP && state.stencilFront.depthFailOp == VK_STENCIL_OP_KEEP && state.stencilFront.compareOp == VK_COMPARE_OP_EQUAL, "an equal test replaced by another value is not drawn as an inversion");
Require(state.stencilFront.reference == 2 && state.stencilFront.compareMask == 0xff && state.stencilFront.writeMask == 7, "an inverted replacement does not keep the test value and write only the bits that differ from it");
queue.context[0x10b] = 0x440;
state = AgcDriver::Graphics::DecodeState(queue);
Require(state.stencilFront.passOp == VK_STENCIL_OP_INVERT && state.stencilFront.depthFailOp == VK_STENCIL_OP_INVERT && state.stencilFront.writeMask == 7, "a replacement on pass and depth fail is not drawn as one inversion");
queue.context[0x10b] = 0x4;
queue.context[0x200] = 0x00200511;
state = AgcDriver::Graphics::DecodeState(queue);
Require(state.stencilFront.failOp == VK_STENCIL_OP_INVERT && state.stencilFront.passOp == VK_STENCIL_OP_KEEP && state.stencilFront.writeMask == 7, "a not-equal test that fails is not replaced as an inversion");
queue.context[0x10c] = 0x40ffffff;
queue.context[0x10b] = 0x40;
queue.context[0x200] = 0x00200311;
state = AgcDriver::Graphics::DecodeState(queue);
Require(state.stencilFront.passOp == VK_STENCIL_OP_INVERT && state.stencilFront.writeMask == 0xbf && state.stencilFront.reference == 0xff, "a less-or-equal test against 0xff, which passes only on 0xff, is not replaced as an inversion");
queue.context[0x10b] = 0x4;
queue.context[0x200] = 0x00200411;
state = AgcDriver::Graphics::DecodeState(queue);
Require(state.stencilFront.failOp == VK_STENCIL_OP_INVERT && state.stencilFront.writeMask == 0xbf, "a greater test against 0xff, which fails only on 0xff, is not replaced as an inversion");
queue.context[0x10c] = 0x40ffff7f;
queue.context[0x10b] = 0x40;
queue.context[0x200] = 0x00200311;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "stencil replacement");
queue.context[0x10c] = 0x05ffff02;
queue.context[0x200] = 0x00200511;
queue.context[0x10b] = 0x40;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "stencil replacement");
queue.context[0x200] = 0x00200111;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "stencil replacement");
queue.context[0x200] = 0x00200211;
queue.context[0x10c] = 0x05ff0f02;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "stencil replacement");
queue.context[0x10c] = 0x05ffff02;
queue.context[0x10b] = 0x41;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "beside another stencil operation");
queue.context[0x10b] = 0x50;
queue.context[0x200] = 0x00700711;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "stencil add/subtract");
@@ -2427,6 +2472,26 @@ void OneDimensionalColorTests() {
Require(line.color.extent.width == 64u && line.color.extent.height == 1u && line.color.address == reinterpret_cast<std::uintptr_t>(colorMemory.data()) && line.color.depth == 1u, "a 1D color target did not decode as one row");
}
void multisampledSingleClearTests() {
static constexpr std::array<std::uint32_t, 22> kernel{0xbe800303u, 0xbe830382u, 0xf40c0401u, 0xfa000000u, 0xd7460000u, 0x04010609u, 0xd7460001u, 0x0405060au, 0x7e060208u, 0x7e0c020bu, 0x7e0e0280u, 0xd5690004u, 0x02000100u, 0x7e000205u, 0xd5690005u, 0x02000901u, 0xd7000001u, 0x02000c80u, 0xbf8cc07fu, 0xf0201f38u, 0x00040004u, 0xbf810000u};
const std::array<std::uint32_t, 9> userData{0u, 0u, 0x0f7f0008u, 8u, 8u, 0xff00ffccu, 0u, 0u, 0u};
const std::array<std::uint32_t, 3> group{8, 8, 1};
const auto match = [&](std::span<const std::uint32_t> code, std::span<const std::uint32_t> words, const std::array<std::uint32_t, 3>& threads) {
return AgcDriver::Graphics::MatchMultisampledSingleClear(code, words, threads);
};
const auto clear = match(kernel, userData, group);
Require(clear.has_value() && clear->descriptor == 0x20f7f0008ull && clear->color == std::array<std::uint32_t, 4>{0xff00ffccu, 0u, 0u, 0u}, "RADV's multisampled comp-to-single clear was not recognized");
Require(!match(kernel, userData, {64, 1, 1}), "the comp-to-single clear matched with another group size");
Require(!match(kernel, std::span(userData).first(8), group), "the comp-to-single clear matched without its clear color");
auto unsized = userData;
unsized[3] = 0;
Require(!match(kernel, unsized, group), "a comp-to-single clear with no block size was recognized");
auto other = kernel;
other[17] ^= 1u;
Require(!match(other, userData, group), "a different kernel was taken for RADV's comp-to-single clear");
Require(!match(std::span(kernel).first(21), userData, group), "a truncated kernel was taken for RADV's comp-to-single clear");
}
void htileDepthClearTests() {
static constexpr std::array<std::uint32_t, 17> kernel{0xd7460000u, 0x04010c06u, 0x34000084u, 0xdc388000u, 0x04020000u, 0xbf8c3f70u, 0xd7710008u, 0x00120805u, 0xd7710009u, 0x00120a05u, 0xd771000au, 0x00120c05u, 0xd771000bu, 0x00120e05u, 0xdc788000u, 0x00020800u, 0xbf810000u};
const std::array<std::uint32_t, 3> wave{64, 1, 1};
@@ -2669,9 +2734,38 @@ void DepthBoundsBiasTests() {
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "window offset");
queue.context[0x80] = 0;
queue.userConfig[0x242] = 2;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "polygon mode, depth bias or nonstandard rasterization");
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "depth bias or nonstandard rasterization");
queue.userConfig[0x242] = 3;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "polygon mode, depth bias or nonstandard rasterization");
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "depth bias or nonstandard rasterization");
queue.userConfig[0x242] = 4;
queue.context[0x280] = 0x80008u;
queue.context[0x281] = 0xffff0000u;
queue.context[0x282] = 8u;
const auto polygonMode = [&](std::uint32_t raster) {
queue.context[0x205] = raster;
return AgcDriver::Graphics::DecodeState(queue).polygonMode;
};
Require(polygonMode(0x8u) == VK_POLYGON_MODE_POINT, "POLY_MODE with point fronts and backs did not decode to the point polygon mode");
Require(polygonMode(0x128u) == VK_POLYGON_MODE_LINE, "POLY_MODE with line fronts and backs did not decode to the line polygon mode");
Require(polygonMode(0x248u) == VK_POLYGON_MODE_FILL, "POLY_MODE with filled fronts and backs did not decode to the fill polygon mode");
Require(polygonMode(0x20u) == VK_POLYGON_MODE_FILL, "polygon types without POLY_MODE did not keep the triangles filled");
Require(polygonMode(0x4au) == VK_POLYGON_MODE_FILL, "the point type of culled back faces changed the filled front faces");
Require(polygonMode(0x109u) == VK_POLYGON_MODE_LINE, "culled front faces did not leave the back faces' line type");
expectFailure([&] { polygonMode(0x48u); }, "different front and back polygon modes");
expectFailure([&] { polygonMode(0x18u); }, "a reserved POLY_MODE");
expectFailure([&] { polygonMode(0x68u); }, "a reserved polygon type");
expectFailure([&] { polygonMode(0x808u); }, "depth bias with a point or line polygon mode");
queue.context[0x280] = 0x100010u;
expectFailure([&] { polygonMode(0x8u); }, "a point polygon mode with a point size other than one pixel");
queue.context[0x280] = 0x80008u;
queue.context[0x281] = 0xffff0010u;
expectFailure([&] { polygonMode(0x8u); }, "a point polygon mode whose size limits exclude one pixel");
queue.context[0x281] = 0xffff0000u;
queue.context[0x282] = 0x10u;
expectFailure([&] { polygonMode(0x128u); }, "a line polygon mode wider or narrower than one pixel");
queue.context[0x282] = 8u;
queue.userConfig[0x242] = 2;
Require(polygonMode(0x8u) == VK_POLYGON_MODE_FILL, "a line list took the point polygon mode of its triangles");
queue.userConfig[0x242] = 4;
queue.context[0x205] = 0x00001a4au;
queue.context[0x2de] = 0x1f0u;
@@ -2688,6 +2782,65 @@ void DepthBoundsBiasTests() {
alignas(256) std::array<std::uint8_t, 4> dccKeys{};
void depthCopyPassTests() {
using AgcDriver::Graphics::DecodeDepthCopyPass;
auto queue = makeState();
queue.context[0x0] = 0;
Require(!DecodeDepthCopyPass(queue).has_value(), "a draw without DEPTH_COPY decoded as a DB depth copy");
queue.context[0x000] = 0x84;
queue.context[0x010] = 0x80000187;
queue.context[0x011] = 0x20000180;
queue.context[0x012] = 0x00001000;
queue.context[0x007] = 0x0003003f;
queue.context[0x31c] = 0x40710;
queue.context[0x8e] = 1;
auto pass = DecodeDepthCopyPass(queue);
Require(pass.has_value() && pass->depthAddress == 0x100000 && pass->samples == 2 && pass->sample == 0 && pass->extent.width == 64 && pass->extent.height == 4, "a centroid DB depth copy of a two-sample depth plane decoded differently");
Require(pass->color.format == VK_FORMAT_R32_SFLOAT && pass->color.samples == 1 && pass->color.address == reinterpret_cast<std::uintptr_t>(colorMemory.data()), "the DB depth copy target decoded differently");
queue.context[0x000] = 0x104;
Require(DecodeDepthCopyPass(queue)->sample == 1, "COPY_SAMPLE without COPY_CENTROID did not select the sample");
queue.context[0x000] = 0x204;
expectFailure([&] { (void)DecodeDepthCopyPass(queue); }, "a sample the depth plane does not have");
queue.context[0x000] = 0x8c;
expectFailure([&] { (void)DecodeDepthCopyPass(queue); }, "stencil copy");
queue.context[0x000] = 0x84;
queue.context[0x30e] = 0xfffffffe;
expectFailure([&] { (void)DecodeDepthCopyPass(queue); }, "sample mask");
queue.context[0x30e] = 0xffffffff;
queue.context[0x200] = 2;
expectFailure([&] { (void)DecodeDepthCopyPass(queue); }, "depth or stencil tests");
queue.context[0x200] = 0;
queue.context[0x010] = 0x80000185;
expectFailure([&] { (void)DecodeDepthCopyPass(queue); }, "Z_32_FLOAT");
queue.context[0x010] = 0x80000187;
queue.context[0x8e] = 0xf;
expectFailure([&] { (void)DecodeDepthCopyPass(queue); }, "red channel");
queue.context[0x8e] = 1;
queue.context[0x31c] = 0x28028;
expectFailure([&] { (void)DecodeDepthCopyPass(queue); }, "single-sample R32 float");
queue.context[0x31c] = 0x40710;
queue.context[0x10f] = std::bit_cast<std::uint32_t>(16.0f);
expectFailure([&] { (void)DecodeDepthCopyPass(queue); }, "over part of the depth surface");
queue.context[0x10f] = std::bit_cast<std::uint32_t>(32.0f);
queue.context[0x010] = 0x80001187;
expectFailure([&] { (void)DecodeDepthCopyPass(queue); }, "mipmapped or partially resident");
queue.context[0x010] = 0x80000187;
queue.context[0x012] = 0x00001003;
pass = DecodeDepthCopyPass(queue);
Require(pass.has_value() && pass->depthAddress == 0x100000, "a DB depth copy kept the pipe-bank XOR of a two-sample depth plane in its address");
queue.context[0x012] = 0x00001000;
queue.context[0x002] = 1u | (1u << 13u);
pass = DecodeDepthCopyPass(queue);
Require(pass.has_value() && pass->depthAddress == 0x100000 + AgcDriver::Graphics::DepthSliceBytes({64, 4}, 4u), "a DB depth copy of slice 1 did not address the slice the depth surface registers");
queue.context[0x002] = 0;
Require(DecodeDepthCopyPass(queue).has_value(), "the restored DB depth copy was refused");
queue.context[0x010] = 0x80000183;
pass = DecodeDepthCopyPass(queue);
Require(pass.has_value() && pass->samples == 1 && pass->sample == 0, "a centroid DB depth copy of a one-sample depth plane decoded differently");
queue.context[0x000] = 0x104;
expectFailure([&] { (void)DecodeDepthCopyPass(queue); }, "a sample the depth plane does not have");
}
void metadataPassTests() {
using AgcDriver::Graphics::ColorMetadataPass;
using AgcDriver::Graphics::DecodeColorMetadataPass;
@@ -3727,6 +3880,83 @@ void multisampleTests() {
Require(MultisampledFillClearColor(srgb, 0x80ffffffu, clear) && clear.float32[2] == 1.0f && clear.float32[3] == 128.0f / 255.0f, "an sRGB fill of white with linear alpha 0x80 did not clear to it");
Require(!MultisampledFillClearColor(srgb, 0x00000080u, clear), "an sRGB fill whose color decodes inexactly cleared");
using AgcDriver::Graphics::MultisampledDccClear;
auto single = srgb;
single.format = VK_FORMAT_R8G8B8A8_UNORM;
single.tileMode = ColorTileMode::RenderTarget;
single.extent = {581, 729};
single.address = single.surfaceAddress = 0x40010000;
single.dccAddress = 0x50010000;
single.dccPipeAligned = true;
const std::vector<std::byte> singleKeys(AgcDriver::Graphics::MultisampledDccBytes(single), std::byte{0x10});
expectFailure([&] { MultisampledDccClear(single, singleKeys, {}); }, "comp-to-single keys without a recorded clear color");
AgcDriver::Graphics::GuestTextureResource cleared{};
cleared.baseAddress = single.surfaceAddress;
cleared.width = 581;
cleared.height = 729;
cleared.samples = 4;
AgcDriver::Graphics::NoteMultisampledSingleClear(cleared, {0xff00ffccu, 0u, 0u, 0u});
clear = MultisampledDccClear(single, singleKeys, {});
Require(clear.float32[0] == 204.0f / 255.0f && clear.float32[1] == 1.0f && clear.float32[2] == 0.0f && clear.float32[3] == 1.0f, "comp-to-single keys did not clear to the recorded RGBA8 color");
auto resized = single;
resized.extent = {580, 729};
const std::vector<std::byte> resizedKeys(AgcDriver::Graphics::MultisampledDccBytes(resized), std::byte{0x10});
expectFailure([&] { MultisampledDccClear(resized, resizedKeys, {}); }, "comp-to-single keys without a recorded clear color");
auto wide = halfFloat;
wide.surfaceAddress = 0x40000000;
wide.dccAddress = 0x50000000;
wide.dccPipeAligned = true;
AgcDriver::Graphics::GuestTextureResource wideCleared{};
wideCleared.baseAddress = wide.surfaceAddress;
wideCleared.width = wide.extent.width;
wideCleared.height = wide.extent.height;
wideCleared.samples = wide.samples;
AgcDriver::Graphics::NoteMultisampledSingleClear(wideCleared, {0x3c003c00u, 0x3c003c00u, 0u, 0u});
const std::vector<std::byte> wideKeys(AgcDriver::Graphics::MultisampledDccBytes(wide), std::byte{0x10});
clear = MultisampledDccClear(wide, wideKeys, {});
Require(clear.float32[0] == 1.0f && clear.float32[1] == 1.0f && clear.float32[2] == 1.0f && clear.float32[3] == 1.0f, "comp-to-single keys did not clear to the recorded RGBA16F color");
auto widest = wide;
widest.format = VK_FORMAT_R32G32B32A32_SFLOAT;
widest.elementBytes = 16;
widest.address = widest.surfaceAddress = 0x48000000;
widest.dccAddress = 0x58000000;
auto widestCleared = wideCleared;
widestCleared.baseAddress = widest.surfaceAddress;
AgcDriver::Graphics::NoteMultisampledSingleClear(widestCleared, {0x3f800000u, 0x3f000000u, 0u, 0x3f800000u});
const std::vector<std::byte> widestKeys(AgcDriver::Graphics::MultisampledDccBytes(widest), std::byte{0x10});
clear = MultisampledDccClear(widest, widestKeys, {});
Require(clear.float32[0] == 1.0f && clear.float32[1] == 0.5f && clear.float32[2] == 0.0f && clear.float32[3] == 1.0f, "comp-to-single keys did not clear to the recorded RGBA32F color");
auto swizzled = single;
swizzled.pipeBankXor = 0x4000;
expectFailure([&] { MultisampledDccClear(swizzled, singleKeys, {}); }, "comp-to-single keys without a recorded clear color");
constexpr std::size_t reusedBytes = 0x10000;
auto* reusedBlock = static_cast<std::byte*>(::operator new(reusedBytes, std::align_val_t{reusedBytes}));
const auto mapReused = [&] {
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, reusedBlock, reusedBytes, true, true, true);
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
};
const auto unmapReused = [&] {
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, reusedBlock);
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
};
mapReused();
auto reused = single;
reused.address = reused.surfaceAddress = reinterpret_cast<std::uintptr_t>(reusedBlock);
auto reusedCleared = cleared;
reusedCleared.baseAddress = reused.surfaceAddress;
AgcDriver::Graphics::NoteMultisampledSingleClear(reusedCleared, {0xff00ffccu, 0u, 0u, 0u});
clear = MultisampledDccClear(reused, singleKeys, {});
Require(clear.float32[0] == 204.0f / 255.0f && clear.float32[3] == 1.0f, "comp-to-single keys on mapped guest memory did not clear to the recorded color");
unmapReused();
mapReused();
expectFailure([&] { MultisampledDccClear(reused, singleKeys, {}); }, "comp-to-single keys without a recorded clear color");
clear = MultisampledDccClear(single, singleKeys, {});
Require(clear.float32[0] == 204.0f / 255.0f, "unmapping one surface dropped another surface's comp-to-single clear");
unmapReused();
::operator delete(reusedBlock, std::align_val_t{reusedBytes});
using AgcDriver::Graphics::TakeMultisampledClear;
halfFloat.clearWords = {0x3c003c00u, 0x3c003c00u};
MultisampledCmask clearedCmask(0x50000000, 0x1000);
@@ -5785,6 +6015,16 @@ void resourceTests() {
expectSingleAccepted(changed([](auto& binding) { binding.guestDescriptor[2] = 32768; }), "an unregistered V# past the range limit");
expectSingleAccepted(changed([](auto& binding) { binding.guestDescriptor = vsharp(reinterpret_cast<const void*>(0x1000), 8); }), "an unmapped V#");
expectSingleFailure(changed([&](auto& binding) { binding.guestDescriptor = vsharp(reinterpret_cast<const void*>(state.color.address), 64); }), "aliases the render target");
const auto stridedBefore = [&](std::uint32_t past) {
constexpr std::uint32_t stride = 60;
constexpr std::uint32_t records = 4;
auto descriptor = vsharp(reinterpret_cast<const void*>(state.color.address - (stride * records - past)), records);
descriptor[1] |= stride << 16u;
return descriptor;
};
expectSingleAccepted(changed([&](auto& binding) { binding.guestDescriptor = stridedBefore(8); binding.bufferWritten = {false}; }), "a read-only strided V# whose last record runs 8 bytes into the render target");
expectSingleFailure(changed([&](auto& binding) { binding.guestDescriptor = stridedBefore(8); binding.bufferWritten = {true}; }), "aliases the render target");
expectSingleFailure(changed([&](auto& binding) { binding.guestDescriptor = stridedBefore(60); }), "aliases the render target");
expectSingleAccepted(changed([](auto& binding) { binding.count = 3; binding.guestDescriptor = join(join(vsharp(guestFirst.data(), 16), vsharp(guestSecond.data(), 32)), vsharp(reinterpret_cast<const void*>(0x1000), 8)); }), "an unmapped V# element");
expectSingleFailure(changed([&](auto& binding) { binding.count = 2; binding.guestDescriptor = join(vsharp(guestFirst.data(), 16), vsharp(reinterpret_cast<const void*>(state.color.address), 64)); }), "aliases the render target");
expectSingleFailure(changed([](auto& binding) { binding.count = 17; binding.guestDescriptor.assign(68, 0); }), "per-stage limits");
@@ -6961,6 +7201,412 @@ void sampleLocationGridFallbackTests() {
mock = MockVulkan{};
}
std::string varyingIndex(std::span<const std::uint32_t> code) {
Require(code.size() > 5 && code[0] == spv::MagicNumber, "a sample order shader is not a SPIR-V module");
std::set<std::uint32_t> constants;
for (std::size_t at = 5; at < code.size();) {
const auto count = code[at] >> 16u;
const auto opcode = static_cast<spv::Op>(code[at] & 0xffffu);
Require(count != 0 && at + count <= code.size(), "a sample order shader holds a malformed instruction");
if (opcode == spv::OpConstant) constants.insert(code[at + 2]);
if (opcode == spv::OpVectorExtractDynamic || opcode == spv::OpVectorInsertDynamic) return "a vector component chosen per invocation (opcode " + std::to_string(opcode) + ")";
if (opcode == spv::OpAccessChain || opcode == spv::OpInBoundsAccessChain || opcode == spv::OpPtrAccessChain || opcode == spv::OpInBoundsPtrAccessChain) {
for (std::size_t operand = 4; operand < count; ++operand) {
if (!constants.contains(code[at + operand])) return "an access chain into %" + std::to_string(code[at + 3]) + " through the non-constant index %" + std::to_string(code[at + operand]);
}
}
at += count;
}
return {};
}
VkSampleCountFlags orderStorageCounts = VK_SAMPLE_COUNT_2_BIT | VK_SAMPLE_COUNT_4_BIT;
bool orderSixteenBitStencil = true;
std::vector<VkSampleMask> pipelineSampleMasks;
VKAPI_ATTR VkResult VKAPI_CALL sampleOrderImageProperties(VkPhysicalDevice, VkFormat, VkImageType, VkImageTiling, VkImageUsageFlags usage, VkImageCreateFlags, VkImageFormatProperties* properties) {
*properties = {};
properties->maxExtent = {16384, 16384, 1};
properties->maxMipLevels = 1;
properties->maxArrayLayers = 1;
properties->sampleCounts = (usage & VK_IMAGE_USAGE_STORAGE_BIT) != 0 ? orderStorageCounts : VK_SAMPLE_COUNT_1_BIT | VK_SAMPLE_COUNT_2_BIT | VK_SAMPLE_COUNT_4_BIT;
return VK_SUCCESS;
}
VKAPI_ATTR void VKAPI_CALL sampleOrderFormatProperties(VkPhysicalDevice, VkFormat format, VkFormatProperties* properties) {
*properties = {};
if (format == VK_FORMAT_D16_UNORM_S8_UINT && !orderSixteenBitStencil) return;
properties->optimalTilingFeatures = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT | VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT | VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT;
}
VKAPI_ATTR VkResult VKAPI_CALL sampleOrderCreateGraphicsPipelines(VkDevice device, VkPipelineCache cache, std::uint32_t count, const VkGraphicsPipelineCreateInfo* infos, const VkAllocationCallbacks* allocator, VkPipeline* pipelines) {
const auto* samples = infos[0].pMultisampleState;
pipelineSampleMasks.push_back(samples != nullptr && samples->pSampleMask != nullptr ? *samples->pSampleMask : ~VkSampleMask{0});
return sampleLocationCreateGraphicsPipelines(device, cache, count, infos, allocator, pipelines);
}
PFN_vkVoidFunction VKAPI_CALL sampleOrderProc(VkDevice device, const char* name) {
if (std::string_view(name) == "vkCreateGraphicsPipelines") return reinterpret_cast<PFN_vkVoidFunction>(sampleOrderCreateGraphicsPipelines);
return sampleLocationProc(device, name);
}
struct DepthOrderEvent {
bool copy = false;
VkImage image = VK_NULL_HANDLE;
VkImageLayout from = VK_IMAGE_LAYOUT_UNDEFINED;
VkImageLayout to = VK_IMAGE_LAYOUT_UNDEFINED;
VkImageAspectFlags aspects = 0;
VkExtent2D grid{};
std::vector<VkSampleLocationEXT> locations;
};
std::vector<DepthOrderEvent> depthOrderEvents;
VKAPI_ATTR VkResult VKAPI_CALL depthOrderCreateImage(VkDevice, const VkImageCreateInfo*, const VkAllocationCallbacks*, VkImage* image) {
*image = makeHandle<VkImage>();
++mock.live;
return VK_SUCCESS;
}
VKAPI_ATTR void VKAPI_CALL depthOrderDestroyImage(VkDevice, VkImage, const VkAllocationCallbacks*) { --mock.live; }
VKAPI_ATTR void VKAPI_CALL depthOrderImageRequirements(VkDevice, VkImage, VkMemoryRequirements* requirements) { *requirements = {256, 1, 1}; }
VKAPI_ATTR VkResult VKAPI_CALL depthOrderBindImage(VkDevice, VkImage, VkDeviceMemory, VkDeviceSize) { return VK_SUCCESS; }
VKAPI_ATTR VkResult VKAPI_CALL depthOrderCreateView(VkDevice, const VkImageViewCreateInfo*, const VkAllocationCallbacks*, VkImageView* view) {
*view = makeHandle<VkImageView>();
++mock.live;
return VK_SUCCESS;
}
VKAPI_ATTR void VKAPI_CALL depthOrderDestroyView(VkDevice, VkImageView, const VkAllocationCallbacks*) { --mock.live; }
VKAPI_ATTR void VKAPI_CALL depthOrderBarrier(VkCommandBuffer, VkPipelineStageFlags, VkPipelineStageFlags, VkDependencyFlags, std::uint32_t, const VkMemoryBarrier*, std::uint32_t, const VkBufferMemoryBarrier*, std::uint32_t count, const VkImageMemoryBarrier* barriers) {
for (std::uint32_t index = 0; index < count; ++index) {
const auto& barrier = barriers[index];
DepthOrderEvent event{false, barrier.image, barrier.oldLayout, barrier.newLayout, barrier.subresourceRange.aspectMask, {}, {}};
const auto* locations = static_cast<const VkSampleLocationsInfoEXT*>(barrier.pNext);
if (locations != nullptr && locations->sType == VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT) {
event.grid = locations->sampleLocationGridSize;
event.locations.assign(locations->pSampleLocations, locations->pSampleLocations + locations->sampleLocationsCount);
}
depthOrderEvents.push_back(event);
}
}
VKAPI_ATTR void VKAPI_CALL depthOrderCopy(VkCommandBuffer, VkImage source, VkImageLayout layout, VkImage, VkImageLayout, std::uint32_t, const VkImageCopy*) {
depthOrderEvents.push_back({true, source, layout, layout, 0, {}, {}});
}
VKAPI_ATTR void VKAPI_CALL depthOrderEndPass(VkCommandBuffer) {}
VKAPI_ATTR void VKAPI_CALL depthOrderDraw(VkCommandBuffer, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t) {}
VKAPI_ATTR void VKAPI_CALL depthOrderStencil(VkCommandBuffer, VkStencilFaceFlags, std::uint32_t) {}
PFN_vkVoidFunction VKAPI_CALL depthOrderProc(VkDevice device, const char* name) {
static const std::map<std::string_view, PFN_vkVoidFunction> table{
{"vkCreateImage", reinterpret_cast<PFN_vkVoidFunction>(depthOrderCreateImage)},
{"vkDestroyImage", reinterpret_cast<PFN_vkVoidFunction>(depthOrderDestroyImage)},
{"vkGetImageMemoryRequirements", reinterpret_cast<PFN_vkVoidFunction>(depthOrderImageRequirements)},
{"vkBindImageMemory", reinterpret_cast<PFN_vkVoidFunction>(depthOrderBindImage)},
{"vkCreateImageView", reinterpret_cast<PFN_vkVoidFunction>(depthOrderCreateView)},
{"vkDestroyImageView", reinterpret_cast<PFN_vkVoidFunction>(depthOrderDestroyView)},
{"vkCmdPipelineBarrier", reinterpret_cast<PFN_vkVoidFunction>(depthOrderBarrier)},
{"vkCmdCopyImage", reinterpret_cast<PFN_vkVoidFunction>(depthOrderCopy)},
{"vkCmdEndRenderPass", reinterpret_cast<PFN_vkVoidFunction>(depthOrderEndPass)},
{"vkCmdDraw", reinterpret_cast<PFN_vkVoidFunction>(depthOrderDraw)},
{"vkCmdSetStencilWriteMask", reinterpret_cast<PFN_vkVoidFunction>(depthOrderStencil)},
{"vkCmdSetStencilReference", reinterpret_cast<PFN_vkVoidFunction>(depthOrderStencil)}
};
const auto it = table.find(name);
return it != table.end() ? it->second : sampleLocationProc(device, name);
}
void sampleOrderTests() {
using namespace AgcDriver::Graphics;
auto queue = makeState();
queue.context[0x2f8] = 0x00100001;
queue.context[0x292] = 3;
queue.context[0x3b8] = 0x0dc00000u;
queue.context[0x31d] = 0x9000;
queue.context[0x1b3] = 2;
queue.context[0x1b4] = 2;
const auto decode = [&](std::array<std::uint32_t, 4> words) {
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = words[pixel];
return DecodeState(queue);
};
const auto village = decode({0x0c04u, 0x0c04u, 0x040cu, 0x040cu});
const auto residentEvil4 = decode({0x040cu, 0x040cu, 0x0c04u, 0x0c04u});
const auto uniform = decode({0x0c04u, 0x0c04u, 0x0c04u, 0x0c04u});
const auto moved = decode({0x0c04u, 0x0c04u, 0x040bu, 0x040bu});
const SampleOrder rowSwap{KeptSampleOrder, KeptSampleOrder, 0x76543201u, 0x76543201u};
Require(SameSetSampleOrder(village) == rowSwap, "the 2x checkerboard (PPSA01556) did not reorder only its odd rows' two samples");
Require(SameSetSampleOrder(residentEvil4) == rowSwap, "the 2x checkerboard with its rows swapped (PPSA07411) did not reorder only its odd rows' two samples");
Require(!SameSetSampleOrder(uniform) && !SameSetSampleOrder(moved), "a uniform pattern or one whose odd rows hold other positions was taken as a reordering of one set");
State four{};
four.samples = 4;
four.customSampleLocations = true;
four.sampleLocationGrid = true;
const std::array<std::uint8_t, 4> positions{0x2eu, 0xe6u, 0x2au, 0x62u};
const std::array<std::array<std::uint32_t, 4>, 4> placed{{{0, 1, 2, 3}, {1, 0, 3, 2}, {2, 3, 1, 0}, {3, 2, 0, 1}}};
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) {
for (std::uint32_t sample = 0; sample < 4; ++sample) four.sampleLocations[pixel * 8u + placed[pixel][sample]] = positions[sample];
}
const SampleOrder fourOrder{KeptSampleOrder, 0x76542301u, 0x76540132u, 0x76541023u};
Require(SameSetSampleOrder(four) == fourOrder, "a 4x pattern whose pixels hold one set of positions in four orders did not give each pixel its order");
Require(InvertSampleOrder(0x76543201u) == 0x76543201u && InvertSampleOrder(0x76540132u) == 0x76541023u && InvertSampleOrder(KeptSampleOrder) == KeptSampleOrder, "inverting a sample order did not undo it");
Require(HostSampleBits(0x1u, 0x76543201u) == 0x2u && HostSampleBits(0x1u, 0x76540132u) == 0x8u && HostSampleBits(0xffffffffu, 0x76540132u) == 0xffffffffu, "a guest sample mask did not move to the host samples at its guest samples' locations");
Require(ReordersSamples(rowSwap) && !ReordersSamples(SampleOrder{}) && !ReordersSamples(SampleOrder{KeptSampleOrder, KeptSampleOrder, KeptSampleOrder, KeptSampleOrder}), "a sample order that keeps every index counted as a reordering");
Require(!HostsSampleLocationGrid({0, 0}) && !HostsSampleLocationGrid({1, 1}) && !HostsSampleLocationGrid({2, 1}) && !HostsSampleLocationGrid({3, 2}) && HostsSampleLocationGrid({2, 2}) && HostsSampleLocationGrid({4, 2}), "a device's largest sample location grid was taken for one that holds a 2x2 grid, or not, wrongly");
std::size_t module = 0;
for (const auto code : SampleOrderShaderModules()) {
const auto varying = varyingIndex(code);
Require(varying.empty(), "sample order shader " + std::to_string(module) + " picks a pixel's order word or a held sample through " + varying);
++module;
}
const auto capable = [] {
auto context = mockContext();
context.physical = makeHandle<VkPhysicalDevice>();
context.device = makeHandle<VkDevice>();
context.deviceProc = sampleOrderProc;
context.imageFormatProperties = sampleOrderImageProperties;
context.formatProperties = sampleOrderFormatProperties;
context.sampleLocationSampleCounts = VK_SAMPLE_COUNT_1_BIT | VK_SAMPLE_COUNT_2_BIT | VK_SAMPLE_COUNT_4_BIT;
context.sampleLocationGridSampleCounts = 0;
context.storageImageMultisample = true;
context.sampleRateShading = true;
context.shaderStencilExport = true;
context.limits.framebufferNoAttachmentsSampleCounts = VK_SAMPLE_COUNT_1_BIT | VK_SAMPLE_COUNT_2_BIT | VK_SAMPLE_COUNT_4_BIT;
context.limits.framebufferDepthSampleCounts = VK_SAMPLE_COUNT_2_BIT | VK_SAMPLE_COUNT_4_BIT;
context.limits.framebufferColorSampleCounts = VK_SAMPLE_COUNT_2_BIT | VK_SAMPLE_COUNT_4_BIT;
context.limits.maxColorAttachments = 8;
context.limits.maxFramebufferWidth = 64;
context.limits.maxFramebufferHeight = 64;
context.limits.maxFramebufferLayers = 1;
return context;
};
const auto attached = [](const State& decoded, VkFormat depthFormat, bool stencil) {
State state = decoded;
ColorTarget color{};
color.address = 0x100000u;
color.extent = {64, 64};
color.format = VK_FORMAT_R16G16B16A16_SFLOAT;
color.elementBytes = 8;
color.samples = state.samples;
state.colors = {color};
state.color = color;
state.hasColorTarget = true;
VkPipelineColorBlendAttachmentState blend{};
blend.colorWriteMask = 0xf;
state.blends = {blend};
state.renderExtent = {64, 64};
if (depthFormat != VK_FORMAT_UNDEFINED) {
state.depth = DepthTarget{0x200000u, stencil ? 0x300000u : 0u, {64, 64}, depthFormat, 1.0f, 0};
state.depth->samples = state.samples;
}
return state;
};
const auto withStencil = attached(village, VK_FORMAT_D32_SFLOAT_S8_UINT, true);
{
const auto context = capable();
const auto host = HostSampleLocationsFor(context, withStencil);
Require(host.grid.width == 1 && host.grid.height == 1 && host.order == rowSwap, "a 2x checkerboard on a device without a 2x2 grid that can reorder samples did not take the stand-in with its odd rows reordered");
auto gridded = context;
gridded.sampleLocationGridSampleCounts = VK_SAMPLE_COUNT_2_BIT;
const auto native = HostSampleLocationsFor(gridded, withStencil);
Require(native.grid.width == 2 && native.grid.height == 2 && !ReordersSamples(native.order), "a 2x checkerboard on a device with a 2x2 grid was reordered");
Require(!ReordersSamples(HostSampleLocationsFor(context, attached(uniform, VK_FORMAT_D32_SFLOAT, false)).order) && !ReordersSamples(HostSampleLocationsFor(context, attached(moved, VK_FORMAT_D32_SFLOAT, false)).order), "a uniform pattern, or one whose pixels hold other positions, was reordered");
const auto refused = [&](Context device, const State& state, std::string_view reason, const std::string& what) {
Require(!ReordersSamples(HostSampleLocationsFor(device, state).order), what + " was reordered");
Require(SampleOrderRefusal(device, state).find(reason) != std::string::npos, what + " was not refused for " + std::string(reason) + ": " + SampleOrderRefusal(device, state));
};
auto noStorage = context;
noStorage.storageImageMultisample = false;
auto depthOnly = withStencil;
depthOnly.colors.clear();
depthOnly.blends.clear();
depthOnly.hasColorTarget = false;
Require(ReordersSamples(HostSampleLocationsFor(context, depthOnly).order), "a depth-only pass on a device with multisampled storage images was not reordered");
refused(noStorage, depthOnly, "lacks shaderStorageImageMultisample", "a depth-only pass on a device without multisampled storage images");
Require(HostSampleLocationsFor(noStorage, depthOnly).order == HostSampleLocationsFor(noStorage, withStencil).order, "a depth-only pass and a color pass on one depth target took different sample orders on a device without multisampled storage images");
refused(noStorage, withStencil, "lacks shaderStorageImageMultisample", "a color target on a device without multisampled storage images");
auto noSampleShading = context;
noSampleShading.sampleRateShading = false;
refused(noSampleShading, withStencil, "sampleRateShading", "a device without sample-rate shading");
orderStorageCounts = VK_SAMPLE_COUNT_4_BIT;
refused(context, withStencil, "has no storage view at 2 samples", "a color format without 2x storage");
orderStorageCounts = VK_SAMPLE_COUNT_2_BIT | VK_SAMPLE_COUNT_4_BIT;
auto twentyFour = context;
twentyFour.physical = makeHandle<VkPhysicalDevice>();
orderSixteenBitStencil = false;
refused(twentyFour, attached(village, VK_FORMAT_D16_UNORM_S8_UINT, true), "is not written back exactly", "16-bit depth with stencil kept as 24-bit depth");
orderSixteenBitStencil = true;
auto sixteen = context;
sixteen.physical = makeHandle<VkPhysicalDevice>();
Require(ReordersSamples(HostSampleLocationsFor(sixteen, attached(village, VK_FORMAT_D16_UNORM_S8_UINT, true)).order), "16-bit depth with stencil kept as 16-bit depth was not reordered");
using Method = SampleOrderMethod;
const auto steps = [](const std::vector<SampleOrderStep>& plan, std::initializer_list<SampleOrderStep> expected, const std::string& what) {
Require(plan.size() == expected.size(), what + " planned " + std::to_string(plan.size()) + " attachments");
std::size_t index = 0;
for (const auto& step : expected) {
const auto& planned = plan[index++];
Require(planned.attachment == step.attachment && planned.method == step.method && planned.toHost == step.toHost && planned.toGuest == step.toGuest, what + " planned attachment " + std::to_string(planned.attachment) + " differently");
}
};
steps(PlanSampleOrder(context, withStencil, rowSwap), {{0, Method::Storage, true, true}, {1, Method::DepthStencilExport, true, true}}, "a color and depth-stencil pass with stencil export");
auto noExport = context;
noExport.shaderStencilExport = false;
steps(PlanSampleOrder(noExport, withStencil, rowSwap), {{0, Method::Storage, true, true}, {1, Method::DepthStencilBits, true, true}}, "a color and depth-stencil pass without stencil export");
steps(PlanSampleOrder(context, attached(village, VK_FORMAT_D32_SFLOAT, false), rowSwap), {{0, Method::Storage, true, true}, {1, Method::Depth, true, true}}, "a color and depth pass");
steps(PlanSampleOrder(context, attached(village, VK_FORMAT_D32_SFLOAT_S8_UINT, false), rowSwap), {{0, Method::Storage, true, true}, {1, Method::Depth, true, true}}, "a depth target whose host format has stencil but the guest's has none");
steps(PlanSampleOrder(context, withStencil, rowSwap, VK_ATTACHMENT_LOAD_OP_CLEAR, VK_ATTACHMENT_STORE_OP_STORE), {{0, Method::Storage, false, true}, {1, Method::DepthStencilExport, false, true}}, "a pass that clears its attachments");
steps(PlanSampleOrder(context, withStencil, rowSwap, VK_ATTACHMENT_LOAD_OP_LOAD, VK_ATTACHMENT_STORE_OP_DONT_CARE), {{0, Method::Storage, true, false}, {1, Method::DepthStencilExport, true, false}}, "a pass that discards its attachments");
steps(PlanSampleOrder(context, withStencil, rowSwap, VK_ATTACHMENT_LOAD_OP_DONT_CARE, VK_ATTACHMENT_STORE_OP_DONT_CARE), {}, "a pass that neither loads nor stores");
steps(PlanSampleOrder(context, withStencil, SampleOrder{}), {}, "a pass that keeps the guest's order");
auto single = withStencil;
single.samples = 1;
single.colors[0].samples = 1;
single.depth->samples = 1;
steps(PlanSampleOrder(context, single, rowSwap), {}, "a single-sampled pass");
}
auto masked = attached(village, VK_FORMAT_UNDEFINED, false);
masked.quadSampleMask = {1, 1, 1, 1};
Require(SampleMaskFollowsOrder(masked, rowSwap) && QuadSampleMasks(masked, rowSwap) == std::array<std::uint16_t, 4>{1, 1, 1, 1} && QuadSampleMasks(masked) == std::array<std::uint16_t, 4>{0xffffu, 0xffffu, 0xffffu, 0xffffu}, "a uniform mask of sample 0 did not move to the pixel shader under the checkerboard's order");
auto empty = masked;
empty.quadSampleMask = {0, 0, 0, 0};
Require(!SampleMaskFollowsOrder(empty, rowSwap) && !SampleMaskFollowsOrder(masked, SampleOrder{}), "an empty mask, or a mask without a reordering, moved to the pixel shader");
{
auto earlyTested = attached(village, VK_FORMAT_D32_SFLOAT, false);
earlyTested.quadSampleMask = {1, 1, 1, 1};
earlyTested.pixelEarlyZ = true;
earlyTested.depthTest = true;
ShaderRecompiler::RecompileResult memoryWriter, memoryKeeper;
memoryWriter.spirv = makeModule({.fragment = true});
memoryKeeper.spirv = memoryWriter.spirv;
ShaderRecompiler::DescriptorBinding written{};
written.kind = ShaderRecompiler::DescriptorKind::StorageBuffer;
written.role = ShaderRecompiler::DescriptorRole::GuestBuffers;
written.count = 1;
written.bufferWritten = {true};
memoryWriter.bindings.push_back(written);
const std::array<CompiledShader, 1> writing{{{ShaderRecompiler::ShaderStage::Fragment, &memoryWriter, 0}}};
const std::array<CompiledShader, 1> notWriting{{{ShaderRecompiler::ShaderStage::Fragment, &memoryKeeper, 0}}};
const auto context = capable();
Require(SampleMaskFollowsOrder(earlyTested, HostSampleLocationsFor(context, earlyTested).order), "a uniform mask of sample 0 with a depth target did not move to the pixel shader under the checkerboard's order");
expectFailure([&] { ValidateSampleMask(context, earlyTested, writing); }, "from a pixel program with early depth or stencil tests that writes memory, is unsupported");
ValidateSampleMask(context, earlyTested, notWriting);
auto gridded = context;
gridded.sampleLocationGridSampleCounts = VK_SAMPLE_COUNT_2_BIT;
ValidateSampleMask(gridded, earlyTested, writing);
}
ShaderRecompiler::RecompileResult vertex, fragment;
vertex.spirv = makeModule({});
fragment.spirv = makeModule({.fragment = true});
vertex.variantId = 1;
fragment.variantId = 2;
const std::array<CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, PipelinePushSlotBytes}}};
const std::array<std::uint32_t, 1> mainKey{41};
const std::array<VkSampleLocationEXT, 2> pixelZero{{{0.75f, 0.5f}, {0.25f, 0.5f}}};
for (const bool libraries : {false, true}) {
for (const bool maskedPass : {false, true}) {
mock = MockVulkan{};
pipelineSampleLocations.clear();
passSampleLocations.clear();
pipelineSampleMasks.clear();
auto context = capable();
context.graphicsPipelineLibrary = libraries;
context.pipelineLinkTimeOptimization = false;
auto state = maskedPass ? masked : attached(village, VK_FORMAT_UNDEFINED, false);
const PipelineSetLayouts resources{makeHandle<VkDescriptorSetLayout>(), VK_NULL_HANDLE, false, mainKey, {}};
std::size_t reorderedMasks = 0;
{
Pipeline pipeline(context, state, VertexInputLayout{}, resources, shaders, VK_IMAGE_LAYOUT_GENERAL);
Require(pipeline.ReordersSamples() && pipeline.HostSampleOrder() == rowSwap, "a 2x checkerboard pipeline on a device without a 2x2 grid did not reorder its odd rows");
reorderedMasks = pipelineSampleMasks.size();
auto gridded = context;
gridded.sampleLocationGridSampleCounts = VK_SAMPLE_COUNT_2_BIT;
Pipeline native(gridded, state, VertexInputLayout{}, resources, shaders, VK_IMAGE_LAYOUT_GENERAL);
Require(!native.ReordersSamples(), "a 2x checkerboard pipeline on a device with a 2x2 grid reordered its samples");
}
ClearPipelineLibraries(context.device);
Require(mock.live == 0, "sample order pipeline tests leaked Vulkan objects");
Require(!pipelineSampleLocations.empty(), "the reordered pipeline took no sample locations");
const auto& standIn = pipelineSampleLocations.front();
Require(standIn.grid.width == 1 && standIn.grid.height == 1 && standIn.locations.size() == 2 && standIn.locations[0].x == pixelZero[0].x && standIn.locations[1].x == pixelZero[1].x, "the reordered pipeline did not draw with pixel (0,0)'s locations");
const auto keepsMask = [&](std::size_t begin, std::size_t end) { return std::any_of(pipelineSampleMasks.begin() + static_cast<std::ptrdiff_t>(begin), pipelineSampleMasks.begin() + static_cast<std::ptrdiff_t>(end), [](VkSampleMask mask) { return mask == 1u; }); };
Require(reorderedMasks != 0 && !keepsMask(0, reorderedMasks), "the reordered pipeline kept a static sample mask its odd rows would apply to the other sample");
Require(keepsMask(reorderedMasks, pipelineSampleMasks.size()) == maskedPass, "the pipeline on a device with a 2x2 grid did not keep the static sample mask exactly when the pass has one");
}
}
mock = MockVulkan{};
{
auto context = capable();
context.deviceProc = depthOrderProc;
context.memory.memoryTypes[0].propertyFlags |= VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
const auto commands = makeHandle<VkCommandBuffer>();
const auto depthImage = makeHandle<VkImage>();
const SampleOrderTarget target{depthImage, makeHandle<VkImageView>(), VK_FORMAT_D32_SFLOAT_S8_UINT, {16, 8}, 2, SampleOrderMethod::Depth, nullptr};
const auto onTarget = [&] {
std::vector<DepthOrderEvent> events;
for (const auto& event : depthOrderEvents) {
if (event.image == depthImage) events.push_back(event);
}
return events;
};
const auto located = [](const DepthOrderEvent& event, VkImageLayout from, VkImageLayout to, VkImageAspectFlags aspects, VkExtent2D grid, std::span<const VkSampleLocationEXT> locations) {
if (event.copy || event.from != from || event.to != to || event.aspects != aspects || event.grid.width != grid.width || event.grid.height != grid.height || event.locations.size() != locations.size()) return false;
for (std::size_t index = 0; index < locations.size(); ++index) {
if (event.locations[index].x != locations[index].x || event.locations[index].y != locations[index].y) return false;
}
return true;
};
constexpr VkImageAspectFlags depthStencil = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT;
std::vector<std::shared_ptr<void>> kept;
for (const bool standIn : {true, false}) {
depthOrderEvents.clear();
SampleOrderPass pass{rowSwap, {16, 8}, {}, standIn};
std::copy(pixelZero.begin(), pixelZero.end(), pass.locations.begin());
RecordSampleOrder(context, commands, pass, std::span(&target, 1u), false, kept);
const auto events = onTarget();
if (standIn) {
Require(events.size() == 3 && located(events[0], VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, depthStencil, {1, 1}, pixelZero) && events[1].copy && events[1].from == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL && located(events[2], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, depthStencil, {1, 1}, pixelZero), "the depth swap did not copy its target between transitions that name the stand-in sample locations it was rendered with");
} else {
Require(events.size() == 1 && events[0].copy && events[0].from == VK_IMAGE_LAYOUT_GENERAL, "the depth swap of a pass without sample locations transitioned its target");
}
}
depthOrderEvents.clear();
DepthSampleLocations depth;
depth.image = depthImage;
depth.format = VK_FORMAT_D32_SFLOAT;
depth.samples = 2;
depth.grid = {2, 2};
const std::array<VkSampleLocationEXT, 8> grid{{{0.75f, 0.5f}, {0.25f, 0.5f}, {0.75f, 0.5f}, {0.25f, 0.5f}, {0.25f, 0.5f}, {0.75f, 0.5f}, {0.25f, 0.5f}, {0.75f, 0.5f}}};
std::copy(grid.begin(), grid.end(), depth.locations.begin());
RecordDepthSampleLocations(context, commands, depth);
const auto events = onTarget();
Require(events.size() == 2 && located(events[0], VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_ASPECT_DEPTH_BIT, {2, 2}, grid) && located(events[1], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_ASPECT_DEPTH_BIT, {2, 2}, grid), "a depth target drawn on a 2x2 sample location grid was not left in the general layout through transitions that name that grid's locations");
kept.clear();
ClearSampleOrders(context.device);
Require(mock.live == 0, "the depth swap tests leaked Vulkan objects");
depthOrderEvents.clear();
}
mock = MockVulkan{};
ShaderRecompiler::RecompileRequest request{};
request.shader = {ShaderRecompiler::ShaderStage::Fragment, 0x20000u, {}, 0, {}};
request.context.waveSize = 64;
request.context.pixel = ShaderRecompiler::ShaderPixelStageInfo{};
const auto keptHash = ShaderRecompiler::RecompileCacheKey::ContextHash(request);
request.context.pixel->sampleOrder = rowSwap;
Require(ShaderRecompiler::RecompileCacheKey::ContextHash(request) != keptHash, "the pixel shader's sample order did not key its variant");
const ShaderRecompiler::RequestSerializer serializer;
const auto back = serializer.Deserialize(serializer.Serialize(request));
Require(back.request.context.pixel.has_value() && back.request.context.pixel->sampleOrder == rowSwap, "the recompile request lost the pixel shader's sample order");
}
void pipelineLibrarySetTests() {
using namespace AgcDriver::Graphics;
for (const auto mask : {0u, 1u, 2u, 3u}) {
@@ -7241,7 +7887,7 @@ void barycentricEmulationTests() {
}
return values;
};
auto patches = BuildBarycentricTessellationShaders(vertex, fragment, tessellationTarget, 128u);
auto patches = BuildBarycentricTessellationShaders(vertex, fragment, tessellationTarget, 128u, 0u);
Require(modes(patches.control) == std::vector<std::uint32_t>{spv::ExecutionModeOutputVertices, spv::ExecutionModeTriangles, spv::ExecutionModeSpacingEqual, spv::ExecutionModeVertexOrderCw}, "the generated control stage does not declare three output vertices and the triangle domain");
Require(modes(patches.evaluation) == std::vector<std::uint32_t>{spv::ExecutionModeTriangles, spv::ExecutionModeSpacingEqual, spv::ExecutionModeVertexOrderCw}, "the generated evaluation stage does not declare the triangle domain");
const std::array<CompiledShader, 4> patchShaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::TessellationControl, &patches.control, 0}, {ShaderStage::TessellationEvaluation, &patches.evaluation, 0}, {ShaderStage::Fragment, &fragment, 0}}};
@@ -7255,27 +7901,41 @@ void barycentricEmulationTests() {
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
const std::array<CompiledShader, 4> plainPatchShaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::TessellationControl, &patches.control, 0}, {ShaderStage::TessellationEvaluation, &patches.evaluation, 0}, {ShaderStage::Fragment, &plain, 0}}};
expectFailure([&] { ValidateShaders(plainPatchShaders, state, subgroup, false); }, "stage count");
expectFailure([&] { static_cast<void>(BuildBarycentricTessellationShaders(vertex, plain, tessellationTarget, 128u)); }, "does not emulate barycentrics");
expectFailure([&] { static_cast<void>(BuildBarycentricTessellationShaders(vertex, plain, tessellationTarget, 128u, 0u)); }, "does not emulate barycentrics");
auto noTessellation = tessellationTarget;
noTessellation.tessellation.reset();
expectFailure([&] { static_cast<void>(BuildBarycentricTessellationShaders(vertex, fragment, noTessellation, 128u)); }, "tessellation shaders are unavailable");
expectFailure([&] { static_cast<void>(BuildBarycentricTessellationShaders(vertex, fragment, target, 128u)); }, "tessellation shaders are unavailable");
expectFailure([&] { static_cast<void>(BuildBarycentricTessellationShaders(vertex, fragment, noTessellation, 128u, 0u)); }, "tessellation shaders are unavailable");
expectFailure([&] { static_cast<void>(BuildBarycentricTessellationShaders(vertex, fragment, target, 128u, 0u)); }, "tessellation shaders are unavailable");
auto noCapability = tessellationTarget;
noCapability.supportedCapabilities = std::span<const std::uint32_t>(capabilities).first(1);
expectFailure([&] { static_cast<void>(BuildBarycentricTessellationShaders(vertex, fragment, noCapability, 128u)); }, "tessellation capability is unavailable");
expectFailure([&] { static_cast<void>(BuildBarycentricTessellationShaders(vertex, fragment, noCapability, 128u, 0u)); }, "tessellation capability is unavailable");
auto smallPatches = tessellationTarget;
smallPatches.tessellation->maxEvaluationOutputComponents = 4u;
expectFailure([&] { static_cast<void>(BuildBarycentricTessellationShaders(vertex, fragment, smallPatches, 128u)); }, "exceeds device limits");
expectFailure([&] { static_cast<void>(BuildBarycentricTessellationShaders(vertex, fragment, tessellationTarget, 0u)); }, "exceeds device limits");
expectFailure([&] { static_cast<void>(BuildBarycentricTessellationShaders(vertex, fragment, smallPatches, 128u, 0u)); }, "exceeds device limits");
expectFailure([&] { static_cast<void>(BuildBarycentricTessellationShaders(vertex, fragment, tessellationTarget, 0u, 0u)); }, "exceeds device limits");
expectFailure([&] { static_cast<void>(BuildBarycentricTessellationShaders(vertex, fragment, tessellationTarget, 128u, 1u)); }, "first or the last");
static_cast<void>(BuildBarycentricTessellationShaders(vertex, fragment, tessellationTarget, 128u, 2u));
{
auto context = mockContext();
context.provokingVertexLast = false;
auto lastState = state;
lastState.provokingVertexMode = VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT;
ValidateProvokingVertex(context, lastState, patchShaders);
expectFailure([&] { ValidateProvokingVertex(context, lastState, shaders); }, "last provoking vertex requires VK_EXT_provoking_vertex");
RecompileResult flatVertices = fragment;
for (auto& parameter : flatVertices.fragmentParameters) parameter.flat = true;
const std::array<CompiledShader, 4> flatPatchShaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::TessellationControl, &patches.control, 0}, {ShaderStage::TessellationEvaluation, &patches.evaluation, 0}, {ShaderStage::Fragment, &flatVertices, 0}}};
expectFailure([&] { ValidateProvokingVertex(context, lastState, flatPatchShaders); }, "explicit per-vertex flat interpolation");
}
RecompileResult clipped;
clipped.spirv = makeModule({.parameterOutput = true, .clipDistanceLength = 2});
clipped.parameterExports = {0u};
expectFailure([&] { static_cast<void>(BuildBarycentricTessellationShaders(clipped, fragment, tessellationTarget, 128u)); }, "need shaderClipDistance");
expectFailure([&] { static_cast<void>(BuildBarycentricTessellationShaders(clipped, fragment, tessellationTarget, 128u, 0u)); }, "need shaderClipDistance");
const std::array<std::uint32_t, 3> clipCapabilities{spv::CapabilityShader, spv::CapabilityTessellation, spv::CapabilityClipDistance};
auto clipTarget = tessellationTarget;
clipTarget.supportedCapabilities = clipCapabilities;
auto clippedPatches = BuildBarycentricTessellationShaders(clipped, fragment, clipTarget, 128u);
auto clippedPatches = BuildBarycentricTessellationShaders(clipped, fragment, clipTarget, 128u, 0u);
const auto memberBuiltIn = [](const RecompileResult& result, spv::BuiltIn builtin) {
const auto& words = result.spirv.Words();
for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) {
@@ -7300,7 +7960,7 @@ void barycentricEmulationTests() {
const std::array<CompiledShader, 4> clippedShaders{{{ShaderStage::Vertex, &clipped, 0}, {ShaderStage::TessellationControl, &clippedPatches.control, 0}, {ShaderStage::TessellationEvaluation, &clippedPatches.evaluation, 0}, {ShaderStage::Fragment, &fragment, 0}}};
ValidateShaders(clippedShaders, state, subgroup, false, false, false, false, false, false, true);
vertex.parameterExports.clear();
expectFailure([&] { static_cast<void>(BuildBarycentricTessellationShaders(vertex, fragment, tessellationTarget, 128u)); }, "not exported by the vertex shader");
expectFailure([&] { static_cast<void>(BuildBarycentricTessellationShaders(vertex, fragment, tessellationTarget, 128u, 0u)); }, "not exported by the vertex shader");
expectFailure([&] { static_cast<void>(BuildBarycentricGeometryShader(vertex, fragment, target, limits)); }, "not exported by the vertex shader");
}
@@ -8535,10 +9195,16 @@ void ProvokingVertexTests() {
meshDraw.topology = topology;
for (const auto& drawn : {std::span<const CompiledShader>{}, std::span<const CompiledShader>(flatShaders), std::span<const CompiledShader>(flatP10Shaders), std::span<const CompiledShader>(smoothShaders), std::span<const CompiledShader>(slotShaders)}) ValidateProvokingVertex(supported, meshDraw, drawn);
}
meshDraw.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP;
for (const auto& drawn : {std::span<const CompiledShader>{}, std::span<const CompiledShader>(flatShaders), std::span<const CompiledShader>(flatP10Shaders), std::span<const CompiledShader>(smoothShaders)}) ValidateProvokingVertex(supported, meshDraw, drawn);
fragment.fragmentParameters = {{0, 0, false, true, false, false, false}, {1, 1, false, true, false, false, false}, {2, 2, false, true, false, false, false}, {3, 3, false, true, false, false, false}};
ValidateProvokingVertex(supported, meshDraw, shaders);
expectFailure([&] { ValidateProvokingVertex(supported, meshDraw, slotShaders); }, "last provoking vertex with explicit per-vertex triangle-strip interpolation is unsupported");
meshDraw.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN;
expectFailure([&] { ValidateProvokingVertex(supported, meshDraw, flatShaders); }, "last provoking vertex is unsupported for triangle fans assembled for a geometry program");
for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN}) {
meshDraw.topology = topology;
expectFailure([&] { ValidateProvokingVertex(supported, meshDraw, flatShaders); }, "last provoking vertex is unsupported for triangle strips or fans assembled for a geometry program");
auto firstDraw = meshDraw;
firstDraw.topology = topology;
firstDraw.provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT;
ValidateProvokingVertex(supported, firstDraw, flatShaders);
}
@@ -9224,26 +9890,63 @@ void validationTests() {
attribute.resource.fields[3] = 77u << 12u;
attribute.fetchIndex = 1;
attribute.resource.fields[1] = 0;
attribute.resource.fields[2] = 16;
const auto zeroStride = [&](std::uint32_t select, std::uint32_t records) {
attribute.resource.fields[3] = (77u << 12u) | (select << 28u);
attribute.resource.fields[2] = records;
};
for (const auto records : {0u, 1u, 16u, 0xffffffffu}) {
zeroStride(0, records);
Require(AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 0 must read every zero-stride fetch as zero");
Require(AgcDriver::Graphics::ZeroStrideAvailableBytes(attribute) == 0, "OOB_SELECT 0 zero-stride fetch has bytes to read");
expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1); }, "byte range");
expectFailure([&] { AgcDriver::Graphics::VertexBufferExtent(attribute); }, "empty or oversized");
}
zeroStride(1, 16);
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2) == 16, "zero stride must repeat one value");
Require(AgcDriver::Graphics::VertexBufferReadOffset(attribute, 100, 7) == 0, "zero-stride input skipped its repeated value");
attribute.resource.fields[2] = 8;
expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1); }, "byte range");
Require(!AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "a structured zero-stride fetch was treated as out of range");
attribute.resource.fields[3] = (77u << 12u) | (2u << 28u);
attribute.resource.fields[2] = 1;
Require(AgcDriver::Graphics::VertexBufferExtent(attribute) == 16, "OOB_SELECT 1 extent must cover the whole fetch");
zeroStride(1, 0);
Require(AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 1 without records must read zero");
zeroStride(1, 8);
Require(!AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 1 counted NUM_RECORDS in bytes");
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 8) == 16, "OOB_SELECT 1 must read the whole element for every index below NUM_RECORDS");
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1, 7) == 16, "OOB_SELECT 1 must read the whole element for the last index below NUM_RECORDS");
expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 9); }, "record count");
expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1, 8); }, "record count");
zeroStride(1, 1);
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1) == 16, "OOB_SELECT 1 with one record must read the element at index 0");
expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 2); }, "record count");
attribute.fetchIndex = 0;
zeroStride(1, 3);
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 2, 1) == 16, "OOB_SELECT 1 must read the element for index 2 of 3 records");
expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 3, 1); }, "record count");
attribute.fetchIndex = 1;
zeroStride(2, 1);
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2) == 16, "OOB_SELECT 2 must read a zero-stride fetch whole while NUM_RECORDS is nonzero");
Require(AgcDriver::Graphics::VertexBufferExtent(attribute) == 16, "OOB_SELECT 2 extent must cover the whole fetch");
Require(!AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 2 with records was treated as out of range");
attribute.resource.fields[2] = 0;
zeroStride(2, 0);
Require(AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 2 without records must read zero");
attribute.resource.fields[3] = (77u << 12u) | (3u << 28u);
zeroStride(3, 0);
Require(AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 3 without records must read zero");
attribute.resource.fields[2] = 8;
Require(!AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 3 with records was treated as out of range");
expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1); }, "byte range");
attribute.resource.fields[2] = 16;
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1) == 16, "OOB_SELECT 3 must read a fetch inside the range");
for (const auto records : {1u, 8u, 15u}) {
zeroStride(3, records);
Require(AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 3 must read a fetch that only partly fits as zero");
expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1); }, "byte range");
}
for (const auto records : {16u, 17u, 0xffffffffu}) {
zeroStride(3, records);
Require(!AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 3 with the whole fetch in range was treated as out of range");
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2) == 16, "OOB_SELECT 3 must read a fetch inside the range, whatever the index");
Require(AgcDriver::Graphics::VertexBufferExtent(attribute) == 16, "OOB_SELECT 3 extent must cover only the fetch");
}
attribute.resource.fields[3] = (22u << 12u) | (3u << 28u);
attribute.resource.fields[2] = 4;
Require(!AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 3 compared NUM_RECORDS with the wrong fetch size");
attribute.resource.fields[2] = 3;
Require(AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 3 read a 4-byte fetch past 3 bytes");
attribute.resource.fields[3] = (113u << 12u) | (3u << 28u);
expectFailure([&] { AgcDriver::Graphics::VertexFetchOutOfRange(attribute); }, "unsupported vertex format");
attribute.resource.fields[3] = 113u << 12u;
expectFailure([&] { AgcDriver::Graphics::DecodeVertexFormat(attribute); }, "unsupported vertex format");
attribute.resource.fields[3] = 50u << 12u;
@@ -10311,6 +11014,11 @@ int main(int argc, char** argv) {
std::cout << "Sample location grid fallback checks passed\n";
return 0;
}
if (argc == 2 && std::string_view(argv[1]) == "--sample-order") {
sampleOrderTests();
std::cout << "Sample order checks passed\n";
return 0;
}
{
AgcDriver::Graphics::PipelineCacheSynchronization synchronization;
auto pipelineUse = synchronization.LockForPipelineUse();
@@ -10424,6 +11132,7 @@ int main(int argc, char** argv) {
DepthStencilTests();
OneDimensionalColorTests();
htileDepthClearTests();
multisampledSingleClearTests();
memoryValueFillTests();
ZExportTests();
unorm10_11_11TargetTests();
@@ -10454,6 +11163,7 @@ int main(int argc, char** argv) {
DualSourceBlendTests();
ReversedBlendShaderTests();
metadataPassTests();
depthCopyPassTests();
cmaskTests();
uint16ExportTests();
uint8x2TargetTests();
@@ -10467,6 +11177,7 @@ int main(int argc, char** argv) {
msaaEnableClearOneSampleTargetTests();
UnwrittenSampleLocationTests();
sampleLocationGridFallbackTests();
sampleOrderTests();
depthCopyFormatTests();
quadPixelMaskTests();
ShaderStageTests();
@@ -517,7 +517,7 @@ void partialUnmap(Fixture& fixture) {
{
GuestAllocations::Mutation mutation;
mutation.Unmap(middle, part, [&](const void* piece, std::size_t bytes, const void* original, bool last) {
Require(!imported->freed.load(), "an unmap inside a host import freed the memory of the side it keeps");
requireFreed(imported);
Require(!other->freed.load(), "partial unmap retired a disjoint host import");
Require(piece == middle && bytes == part && original == allocation.data && !last, "partial unmap changed the wrong guest extent");
});
@@ -525,9 +525,8 @@ void partialUnmap(Fixture& fixture) {
Require(!RegisteredReadableCovers(reinterpret_cast<std::uint64_t>(middle), part), "the middle of a partial unmap remains registered");
const auto low = fixture.Import(allocation.Address(), part);
const auto high = fixture.Import(allocation.Address() + 2 * part, part);
Require(high->memory == imported->memory && !imported->freed.load(), "the side kept from an unmap inside the host import was imported again");
Require(imported->freed.load(), "the side kept from an unmap inside the host import reused memory imported across the unmapped range");
Require(high->address == allocation.Address() + 2 * part && high->bytes == part, "the kept side of the host import does not cover its remaining range");
Require(low->memory != imported->memory, "the smaller side of an unmap inside the host import was not imported again");
Require(!low->freed.load() && !high->freed.load() && !other->freed.load(), "remaining partial mappings could not be imported");
{
GuestAllocations::Mutation mutation;
@@ -0,0 +1,182 @@
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTextureLayout.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Context.hpp"
#include <algorithm>
#include <array>
#include <bit>
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <span>
#include <vector>
using namespace AgcDriver::Graphics;
namespace {
struct ReferenceCase {
TextureTileMode tileMode;
std::uint32_t elementBytes;
std::uint32_t fragments;
std::uint32_t blockWidth;
std::uint32_t blockHeight;
std::uint32_t pitch;
std::uint32_t paddedHeight;
std::uint64_t sliceBytes;
std::array<std::uint64_t, 7> addresses;
};
constexpr std::array<std::array<std::uint32_t, 3>, 7> kCoordinates{{
{0u, 0u, 0u}, {128u, 66u, 16u}, {64u, 32u, 1u}, {32u, 64u, 2u},
{63u, 31u, 7u}, {127u, 65u, 15u}, {1u, 1u, 3u}
}};
constexpr ReferenceCase kReferences[] = {
{TextureTileMode::kR64KBX, 1u, 2u, 128u, 256u, 256u, 256u, 131072ull, {{0ull, 2201608ull, 141312ull, 300032ull, 919295ull, 2013271ull, 396305ull}}},
{TextureTileMode::kR64KBX, 1u, 4u, 128u, 128u, 256u, 128u, 131072ull, {{0ull, 2185224ull, 174080ull, 316416ull, 919295ull, 1996887ull, 429073ull}}},
{TextureTileMode::kR64KBX, 1u, 8u, 64u, 128u, 192u, 128u, 196608ull, {{0ull, 3291144ull, 279552ull, 422400ull, 1413119ull, 3061847ull, 640529ull}}},
{TextureTileMode::kR64KBX, 2u, 2u, 128u, 128u, 256u, 128u, 131072ull, {{0ull, 2213920ull, 141312ull, 312320ull, 923390ull, 2025630ull, 396306ull}}},
{TextureTileMode::kR64KBX, 2u, 4u, 128u, 64u, 256u, 128u, 262144ull, {{0ull, 4392992ull, 305152ull, 689152ull, 1840894ull, 4073630ull, 822290ull}}},
{TextureTileMode::kR64KBX, 2u, 8u, 64u, 64u, 192u, 128u, 393216ull, {{0ull, 6621216ull, 476160ull, 999936ull, 2793470ull, 6195358ull, 1230354ull}}},
{TextureTileMode::kR64KBX, 4u, 2u, 64u, 128u, 192u, 128u, 196608ull, {{0ull, 3328032ull, 264192ull, 443392ull, 1390332ull, 3074204ull, 592916ull}}},
{TextureTileMode::kR64KBX, 4u, 4u, 64u, 64u, 192u, 128u, 393216ull, {{0ull, 6621216ull, 493568ull, 1016832ull, 2766588ull, 6170780ull, 1215508ull}}},
{TextureTileMode::kR64KBX, 4u, 8u, 32u, 64u, 160u, 128u, 655360ull, {{0ull, 11077664ull, 820224ull, 1737216ull, 4667388ull, 10365084ull, 2002452ull}}},
{TextureTileMode::kR64KBX, 8u, 2u, 64u, 64u, 192u, 128u, 393216ull, {{0ull, 6621312ull, 460800ull, 984064ull, 2782968ull, 6170744ull, 1182744ull}}},
{TextureTileMode::kR64KBX, 8u, 4u, 64u, 32u, 192u, 96u, 589824ull, {{0ull, 9963648ull, 855040ull, 1574400ull, 4158456ull, 9317496ull, 1773080ull}}},
{TextureTileMode::kR64KBX, 8u, 8u, 32u, 32u, 160u, 96u, 983040ull, {{0ull, 16648320ull, 1444352ull, 2689280ull, 6979576ull, 15605880ull, 2952984ull}}},
{TextureTileMode::kR64KBX, 16u, 2u, 32u, 64u, 160u, 128u, 655360ull, {{0ull, 11075712ull, 787456ull, 1706496ull, 4718576ull, 10397808ull, 1967664ull}}},
{TextureTileMode::kR64KBX, 16u, 4u, 32u, 32u, 160u, 96u, 983040ull, {{0ull, 16646272ull, 1443328ull, 2690304ull, 7011312ull, 15641712ull, 2950960ull}}},
{TextureTileMode::kR64KBX, 16u, 8u, 16u, 32u, 144u, 96u, 1769472ull, {{0ull, 30017664ull, 2624000ull, 4851968ull, 12615664ull, 28201072ull, 5312304ull}}},
{TextureTileMode::kZ64KBX, 1u, 2u, 128u, 256u, 256u, 256u, 131072ull, {{0ull, 2172945ull, 149504ull, 271361ull, 923390ull, 1992879ull, 396294ull}}},
{TextureTileMode::kZ64KBX, 1u, 4u, 128u, 128u, 256u, 128u, 131072ull, {{0ull, 2181153ull, 165890ull, 279555ull, 931580ull, 2025565ull, 396302ull}}},
{TextureTileMode::kZ64KBX, 1u, 8u, 64u, 128u, 192u, 128u, 196608ull, {{0ull, 3295297ull, 263170ull, 410115ull, 1425404ull, 3041469ull, 591390ull}}},
{TextureTileMode::kZ64KBX, 2u, 2u, 128u, 128u, 256u, 128u, 131072ull, {{0ull, 2181154ull, 165888ull, 279554ull, 931580ull, 2025566ull, 396300ull}}},
{TextureTileMode::kZ64KBX, 2u, 4u, 128u, 64u, 256u, 128u, 262144ull, {{0ull, 4425794ull, 280580ull, 689158ull, 1881848ull, 4118714ull, 789532ull}}},
{TextureTileMode::kZ64KBX, 2u, 8u, 64u, 64u, 192u, 128u, 393216ull, {{0ull, 6637698ull, 459780ull, 999942ull, 2818040ull, 6219898ull, 1181244ull}}},
{TextureTileMode::kZ64KBX, 4u, 2u, 64u, 128u, 192u, 128u, 196608ull, {{0ull, 3295300ull, 264192ull, 410628ull, 1423096ull, 3041468ull, 592920ull}}},
{TextureTileMode::kZ64KBX, 4u, 4u, 64u, 64u, 192u, 128u, 393216ull, {{0ull, 6637700ull, 460808ull, 1000460ull, 2815728ull, 6219892ull, 1182776ull}}},
{TextureTileMode::kZ64KBX, 4u, 8u, 32u, 64u, 160u, 128u, 655360ull, {{0ull, 11126788ull, 787464ull, 1720844ull, 4716528ull, 10359028ull, 1967736ull}}},
};
template<typename TFunction>
void requireRefusal(TFunction action) {
bool refused = false;
try { action(); } catch (const std::exception&) { refused = true; }
Require(refused, "multisampled layout accepted an unsupported operation");
}
void checkReference(const ReferenceCase& reference) {
MultisampleTextureLayout layout(reference.tileMode, reference.elementBytes, reference.fragments, 129u, 67u, 17u);
Require(layout.BlockWidth() == reference.blockWidth && layout.BlockHeight() == reference.blockHeight, "multisampled block extent differs from AMD reference");
Require(layout.Pitch() == reference.pitch && layout.PaddedHeight() == reference.paddedHeight, "multisampled padded extent differs from AMD reference");
Require(layout.SliceBytes() == reference.sliceBytes && layout.Bytes() == reference.sliceBytes * 17u, "multisampled allocation differs from AMD reference");
Require(layout.LinearBytes() == 129ull * 67u * 17u * reference.fragments * reference.elementBytes, "multisampled linear region omits fragments or layers");
for (std::uint32_t n = 0; n < kCoordinates.size(); ++n) {
const auto& coordinate = kCoordinates[n];
const auto address = layout.Offset(coordinate[0], coordinate[1], coordinate[2], n % reference.fragments);
Require(address == reference.addresses[n], "multisampled fragment address differs from AMD reference");
for (auto xorBytes : {0x100u, 0x800u, 0x4000u, 0x8000u, 0xcf00u}) {
MultisampleTextureLayout swizzled(reference.tileMode, reference.elementBytes, reference.fragments, 129u, 67u, 17u, xorBytes);
const auto expected = (address & ~65535ull) | ((address & 65535ull) ^ xorBytes);
Require(swizzled.Offset(coordinate[0], coordinate[1], coordinate[2], n % reference.fragments) == expected, "multisampled pipe/bank XOR moves a fragment outside its tile");
}
}
}
void checkTransfer(const ReferenceCase& reference) {
MultisampleTextureLayout layout(reference.tileMode, reference.elementBytes, reference.fragments, 129u, 67u, 3u, 0x8100u);
std::vector<std::byte> source(layout.LinearBytes() + 32u, std::byte{0xa5});
auto linear = std::span(source).subspan(16u, layout.LinearBytes());
for (std::size_t n = 0; n < linear.size(); ++n) linear[n] = static_cast<std::byte>((n * 13u + n / 251u) & 255u);
const auto savedSource = source;
std::vector<std::byte> tiled(layout.Bytes() + 32u, std::byte{0xa5});
auto guest = std::span(tiled).subspan(16u, layout.Bytes());
layout.Tile(linear, guest);
Require(source == savedSource, "multisampled retile changed its source");
std::vector<bool> covered(guest.size());
std::size_t position = 0;
for (std::uint32_t layer = 0; layer < 3u; ++layer) {
for (std::uint32_t y = 0; y < 67u; ++y) {
for (std::uint32_t x = 0; x < 129u; ++x) {
for (std::uint32_t fragment = 0; fragment < reference.fragments; ++fragment) {
const auto address = layout.Offset(x, y, layer, fragment);
for (std::uint32_t channel = 0; channel < reference.elementBytes; ++channel) {
Require(address + channel < guest.size() && !covered[address + channel], "multisampled element addresses overlap or exceed the allocation");
covered[address + channel] = true;
Require(guest[address + channel] == linear[position++], "multisampled retile changed element bytes");
}
}
}
}
}
for (std::size_t n = 0; n < guest.size(); ++n) Require(covered[n] || guest[n] == std::byte{0xa5}, "multisampled retile changed tile padding");
Require(std::all_of(tiled.begin(), tiled.begin() + 16u, [](auto value) { return value == std::byte{0xa5}; }) && std::all_of(tiled.end() - 16u, tiled.end(), [](auto value) { return value == std::byte{0xa5}; }), "multisampled retile changed guard bytes");
const auto savedTiled = tiled;
std::vector<std::byte> restored(source.size(), std::byte{0xa5});
layout.Detile(guest, std::span(restored).subspan(16u, layout.LinearBytes()));
Require(restored == source && tiled == savedTiled, "multisampled detile changed element, source or guard bytes");
}
void checkInverse(const ReferenceCase& reference) {
const auto byteBits = static_cast<std::uint32_t>(std::countr_zero(reference.elementBytes));
const auto fragmentBits = static_cast<std::uint32_t>(std::countr_zero(reference.fragments));
const auto widthBits = static_cast<std::uint32_t>(std::countr_zero(reference.blockWidth));
for (const auto xorBytes : {0u, 0x4000u, 0x8000u, 0xcf00u}) {
const MultisampleTextureLayout layout(reference.tileMode, reference.elementBytes, reference.fragments, reference.blockWidth * 4u, reference.blockHeight * 4u, 8u, xorBytes);
for (const auto origin : {std::array{0u, 0u, 0u}, std::array{reference.blockWidth * 2u, reference.blockHeight * 3u, 7u}}) {
const auto base = static_cast<std::uint32_t>(layout.Offset(origin[0], origin[1], origin[2], 0u) & 65535u);
const auto step = xorBytes == 0u && origin[2] == 0u ? 1u : 257u;
for (std::uint32_t packed = 0u; packed < 65536u; packed += step) {
const auto byte = packed & (reference.elementBytes - 1u);
const auto fragment = (packed >> byteBits) & (reference.fragments - 1u);
const auto x = (packed >> (byteBits + fragmentBits)) & (reference.blockWidth - 1u);
const auto y = packed >> (byteBits + fragmentBits + widthBits);
const auto physical = static_cast<std::uint32_t>((layout.Offset(origin[0] + x, origin[1] + y, origin[2], fragment) + byte) & 65535u) ^ base;
std::uint32_t restored = 0u;
for (std::uint32_t bit = 0; bit < 16u; ++bit) restored |= static_cast<std::uint32_t>(std::popcount(physical & layout.InverseBlockEquation()[bit]) & 1u) << bit;
Require(restored == packed, "multisampled inverse equation changed a byte, fragment or pixel coordinate");
}
}
}
}
void checkRefusals() {
for (const auto& extent : {std::array{0u, 3u, 1u}, std::array{5u, 0u, 1u}, std::array{16385u, 3u, 1u}, std::array{5u, 16385u, 1u}, std::array{5u, 3u, 0u}, std::array{5u, 3u, 8193u}}) {
requireRefusal([&] { MultisampleTextureLayout invalid(TextureTileMode::kR64KBX, 8u, 8u, extent[0], extent[1], extent[2]); });
}
for (auto elementBytes : {0u, 3u, 32u}) requireRefusal([&] { MultisampleTextureLayout invalid(TextureTileMode::kR64KBX, elementBytes, 8u, 5u, 3u, 1u); });
for (auto fragments : {0u, 1u, 3u, 16u}) requireRefusal([&] { MultisampleTextureLayout invalid(TextureTileMode::kR64KBX, 8u, fragments, 5u, 3u, 1u); });
for (auto tileMode : {TextureTileMode::kLinear, TextureTileMode::kStandard4KB, TextureTileMode::kStandard64KB, TextureTileMode::kS64KBX, TextureTileMode::kD64KBX}) requireRefusal([&] { MultisampleTextureLayout invalid(tileMode, 8u, 8u, 5u, 3u, 1u); });
for (auto elementBytes : {8u, 16u}) requireRefusal([&] { MultisampleTextureLayout invalid(TextureTileMode::kZ64KBX, elementBytes, 8u, 5u, 3u, 1u); });
for (auto xorBytes : {1u, 255u, 0x1000u, 0x2000u, 0x10000u}) requireRefusal([&] { MultisampleTextureLayout invalid(TextureTileMode::kR64KBX, 8u, 8u, 5u, 3u, 1u, xorBytes); });
MultisampleTextureLayout layout(TextureTileMode::kR64KBX, 8u, 8u, 5u, 3u, 2u);
for (const auto& coordinate : {std::array{5u, 0u, 0u, 0u}, std::array{0u, 3u, 0u, 0u}, std::array{0u, 0u, 2u, 0u}, std::array{0u, 0u, 0u, 8u}}) requireRefusal([&] { layout.Offset(coordinate[0], coordinate[1], coordinate[2], coordinate[3]); });
std::vector<std::byte> source(layout.Bytes(), std::byte{0xa5}), destination(layout.Bytes(), std::byte{0x5a});
const auto savedSource = source, savedDestination = destination;
requireRefusal([&] { layout.Detile(std::span(source).first(layout.Bytes() - 1u), destination); });
requireRefusal([&] { layout.Detile(source, std::span(destination).first(layout.LinearBytes() - 1u)); });
requireRefusal([&] { layout.Tile(std::span(source).first(layout.LinearBytes() - 1u), destination); });
requireRefusal([&] { layout.Tile(source, std::span(destination).first(layout.Bytes() - 1u)); });
requireRefusal([&] { layout.Detile(source, source); });
requireRefusal([&] { layout.Tile(source, source); });
requireRefusal([&] { layout.Detile(source, std::span(source).subspan(8u)); });
Require(source == savedSource && destination == savedDestination, "refused multisampled transfer changed a buffer");
}
}
int main() {
try {
for (const auto& reference : kReferences) {
checkReference(reference);
checkTransfer(reference);
checkInverse(reference);
}
checkRefusals();
std::cout << "Multisampled texture layout reference, fragment, layer, padding and refusal tests passed\n";
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
@@ -1079,6 +1079,15 @@ void testIndexAttributesReadAtExecution() {
check(std::atomic_ref<std::uint32_t>(done).load() == 1, "INDEX_ATTRIBUTES_INDIRECT did not read its record when the packet ran");
}
void testReleaseMemDataSelects() {
alignas(8) static std::uint64_t target = 0;
const auto release = [](std::uint32_t select) { return makePacket(0x49, {0x0030c514, select << 29u, low(&target), high(&target), 1, 0, 0}); };
for (std::uint32_t select = 0; select <= 3; ++select) AgcDriver::Pm4::Validate(release(select), 0);
expectFailure([&] { AgcDriver::Pm4::Validate(release(4), 0); }, "64-bit system counter (DATA_SEL 4)");
expectFailure([&] { AgcDriver::Pm4::Validate(release(5), 0); }, "GDS RELEASE_MEM data");
for (const auto select : {6u, 7u}) expectFailure([&] { AgcDriver::Pm4::Validate(release(select), 0); }, "reserved RELEASE_MEM DATA_SEL");
}
void testSuspendPointWritesQueuedLabels() {
alignas(8) static std::uint32_t gate = 0, marker = 0;
gate = 0;
@@ -1944,6 +1953,7 @@ int main(int argc, char** argv) {
testConditionalValidation();
testConditionReadSynchronization();
testEventWrite();
testReleaseMemDataSelects();
testAcquireMem();
testConditionalWrite();
testConditionalWriteSubmission();
+199 -35
View File
@@ -117,6 +117,32 @@ void ReleaseWatched(void* block, std::size_t bytes) {
#endif
}
bool collectMappingHistoryTests() {
constexpr std::size_t bytes = 131072;
auto* memory = static_cast<std::uint8_t*>(AllocateWatched(bytes, 65536));
if (memory == nullptr) return false;
GuestAllocations::Mutation().Add(memory, bytes, true, true, false);
struct Cleanup {
std::uint8_t* memory;
~Cleanup() {
GuestAllocations::Mutation().Remove(memory);
ReleaseWatched(memory, bytes);
}
} cleanup{memory};
const auto address = reinterpret_cast<std::uint64_t>(memory);
std::memset(memory, 0x11, bytes);
AgcDriver::GuestMemory::BumpCollectEpoch();
const auto before = AgcDriver::GuestMemory::CollectWrites(address, bytes);
Require(before != 0, "the mapping fixture has no complete write history");
memory[4096] = 0x22;
GuestAllocations::Mutation().Remove(memory);
GuestAllocations::Mutation().Add(memory, bytes, true, true, false);
Require(AgcDriver::GuestMemory::CollectWrites(address, bytes) != 0, "the replacement mapping lost write tracking");
Require(!AgcDriver::GuestMemory::UnchangedSince(address, bytes, before), "a cached collect hid a CPU store across allocation replacement");
Require(memory[4096] == 0x22, "collecting the replacement mapping changed its CPU bytes");
return true;
}
bool partialWatchRestorationTests() {
#ifdef _WIN32
constexpr std::size_t bytes = 131072;
@@ -191,6 +217,41 @@ std::optional<std::size_t> selectDevice(std::span<const VkPhysicalDeviceProperti
return selected;
}
bool hintMappingHistoryTests() {
#ifdef _WIN32
constexpr std::size_t bytes = 131072;
auto* memory = static_cast<std::uint8_t*>(AllocateWatched(bytes, 65536));
if (memory == nullptr) return false;
GuestAllocations::Mutation().Add(memory, bytes, true, true, false);
struct Cleanup {
std::uint8_t* memory;
~Cleanup() {
GuestAllocations::Mutation().Remove(memory);
ReleaseWatched(memory, bytes);
}
} cleanup{memory};
const auto address = reinterpret_cast<std::uint64_t>(memory);
std::memset(memory, 0x11, bytes);
AgcDriver::GuestMemory::BumpCollectEpoch();
AgcDriver::GuestMemory::Unwatch(address, bytes);
const auto before = AgcDriver::GuestMemory::UntrackedHint(address, bytes);
Require(before != 0, "the hint mapping fixture has no write hint");
Require(AgcDriver::GuestMemory::UntrackedHint(address, bytes) == before, "an unchanged hint range advanced its history");
memory[4096] = 0x22;
GuestAllocations::Mutation().Remove(memory);
GuestAllocations::Mutation().Add(memory, bytes, true, true, false);
const auto after = AgcDriver::GuestMemory::UntrackedHint(address, bytes);
Require(after > before, "a cached hint walk hid a CPU store across allocation replacement");
Require(memory[4096] == 0x22 && memory[bytes - 4096] == 0x11, "collecting replacement hints changed guest bytes");
GuestAllocations::Mutation().Remove(memory);
GuestAllocations::Mutation().Add(memory, bytes, true, true, false);
Require(AgcDriver::GuestMemory::UntrackedHint(address, bytes) == after, "allocation replacement invented a CPU write hint");
return true;
#else
return false;
#endif
}
void deviceSelectionTests() {
const auto properties = [](const char* name, VkPhysicalDeviceType type) {
VkPhysicalDeviceProperties result{};
@@ -2505,6 +2566,12 @@ int reimportBench(const Device& device) {
constexpr std::size_t moves = 5;
constexpr std::size_t page = 4096;
constexpr std::size_t alignment = 2ull << 20u;
constexpr std::size_t requiredImportBytes = bytes + inside;
const auto availableImportBytes = HostImportBudgetRemaining(context);
if (availableImportBytes < requiredImportBytes) {
std::cout << "skipped, the re-import bench needs " << (requiredImportBytes >> 20u) << " MiB of simultaneous host imports; the current budget has " << (availableImportBytes >> 20u) << " MiB available\n";
return 77;
}
struct Release {
void* mapping = nullptr;
std::int64_t physical = -1;
@@ -2619,15 +2686,15 @@ int reimportBench(const Device& device) {
throw;
}
gpu.lock();
Require(importedAgain.empty(), "the kept side was imported again instead of trimmed after the " + importedAgain + " cuts");
std::printf("re-import bench: every cut at the head, at the tail and inside the range kept the larger side by trimming the first import\n");
Require(importedAgain == "head, tail, inside", "a cut kept memory imported across the changed host range instead of importing the remaining side again: " + importedAgain);
std::printf("re-import bench: every cut at the head, at the tail and inside the range retired the original import before importing the remaining side\n");
return 0;
}
int trimmedImportTests(const Device& device) {
int hostImportCutTests(const Device& device) {
const auto& context = device.GetContext();
if (context.hostImportAlignment == 0 || context.settleQueue == VK_NULL_HANDLE || !AgcDriver::GuestMemory::WriteWatched()) {
std::cout << "skipped, the trimmed import tests require host imports, a settle queue and write watching\n";
std::cout << "skipped, the host import cut tests require host imports, a settle queue and write watching\n";
return 77;
}
constexpr std::size_t bytes = 32ull << 20u;
@@ -2644,14 +2711,13 @@ int trimmedImportTests(const Device& device) {
std::size_t keptEnd;
std::size_t smaller;
std::size_t smallerEnd;
bool trimmed;
};
constexpr std::array<Case, 5> cases{{
{"a page made inaccessible inside the import, nearer its start", inside + guard, guard, false, inside + 2 * guard, bytes, 0, inside + guard, true},
{"a page made inaccessible inside the import, nearer its end", bytes - inside - 2 * guard, guard, false, 0, bytes - inside - 2 * guard, bytes - inside - guard, bytes, true},
{"an unmap of a 2 MiB run inside the import", inside, run, true, inside + run, bytes, 0, inside, true},
{"an unmap of the import's first 2 MiB run", 0, run, true, run, bytes, 0, 0, true},
{"an unmap of 1440 KiB at the start of the import's first 2 MiB run", 0, 1440ull << 10u, true, 1440ull << 10u, bytes, 0, 0, false},
{"a page made inaccessible inside the import, nearer its start", inside + guard, guard, false, inside + 2 * guard, bytes, 0, inside + guard},
{"a page made inaccessible inside the import, nearer its end", bytes - inside - 2 * guard, guard, false, 0, bytes - inside - 2 * guard, bytes - inside - guard, bytes},
{"an unmap of a 2 MiB run inside the import", inside, run, true, inside + run, bytes, 0, inside},
{"an unmap of the import's first 2 MiB run", 0, run, true, run, bytes, 0, 0},
{"an unmap of 1440 KiB at the start of the import's first 2 MiB run", 0, 1440ull << 10u, true, 1440ull << 10u, bytes, 0, 0},
}};
struct Backing {
void* mapping = nullptr;
@@ -2679,15 +2745,14 @@ int trimmedImportTests(const Device& device) {
const auto check = [&](const Case& test) {
const std::string name = test.name;
Backing backing;
Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, run, 0, &backing.physical) == 0, "allocate the trimmed import test backing");
Require(sceKernelMapDirectMemory(&backing.mapping, bytes, 0x33, 0, backing.physical, run) == 0, "map the trimmed import test backing");
Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, run, 0, &backing.physical) == 0, "allocate the host import cut test backing");
Require(sceKernelMapDirectMemory(&backing.mapping, bytes, 0x33, 0, backing.physical, run) == 0, "map the host import cut test backing");
const auto address = reinterpret_cast<std::uint64_t>(backing.mapping);
auto* memory = static_cast<std::byte*>(backing.mapping);
for (std::size_t offset = 0; offset < bytes; offset += page) std::memset(memory + offset, static_cast<int>(0x40u + (offset / page) % 64u), 64);
gpu.lock();
const auto* whole = HostImportFor(context, address, bytes);
Require(whole != nullptr && whole->base == address && whole->bytes == bytes && whole->alias == nullptr && !whole->unwatched, name + ": the range was not imported whole, settled and watched");
const auto original = whole->memory;
auto* cut = memory + test.offset;
gpu.unlock();
if (test.unmap) {
@@ -2702,15 +2767,13 @@ int trimmedImportTests(const Device& device) {
const auto end = address + test.keptEnd;
const auto* kept = HostImportFor(context, begin, static_cast<std::size_t>(end - begin));
Require(kept != nullptr && kept->base == begin && kept->base + kept->bytes == end, name + ": the larger side is not imported whole");
const auto keptMemory = kept->memory;
if (test.trimmed) Require(kept->memory == original, "the larger side of " + name + " was imported again instead of kept");
else Require(kept->memory != original, "the side of " + name + " whose 2 MiB run the unmap replaced is served from the import that pinned the run's old pages");
Require(kept->memoryBase == begin && kept->pinnedBytes == end - begin, "the side of " + name + " is served from the import that pinned the changed host range");
requireImportServesGuest(context, recorder, *kept, begin, end, ++salt, "the larger side of " + name);
if (test.smallerEnd != test.smaller) {
const auto* smaller = HostImportFor(context, address + test.smaller, test.smallerEnd - test.smaller);
Require(smaller != nullptr && smaller->base == address + test.smaller && smaller->bytes == test.smallerEnd - test.smaller && smaller->memory != original, "the smaller side of " + name + " is not imported apart from the larger one");
Require(smaller != nullptr && smaller->base == address + test.smaller && smaller->bytes == test.smallerEnd - test.smaller && smaller->memoryBase == address + test.smaller && smaller->pinnedBytes == test.smallerEnd - test.smaller, "the smaller side of " + name + " is not imported apart from the larger one");
requireImportServesGuest(context, recorder, *smaller, address + test.smaller, address + test.smallerEnd, ++salt, "the smaller side of " + name);
Require(HostImportFor(context, begin, static_cast<std::size_t>(end - begin)) == kept && kept->memory == original, "importing the smaller side of " + name + " dropped the larger one");
Require(HostImportFor(context, begin, static_cast<std::size_t>(end - begin)) == kept && kept->memoryBase == begin && kept->pinnedBytes == end - begin, "importing the smaller side of " + name + " replaced the safe import of the larger one");
}
if (test.unmap) {
gpu.unlock();
@@ -2722,26 +2785,25 @@ int trimmedImportTests(const Device& device) {
const auto from = reinterpret_cast<std::uint64_t>(cut);
const auto* remapped = HostImportFor(context, address, bytes);
Require(remapped != nullptr && remapped->base == address && remapped->bytes == bytes, name + ": the range mapped again is not imported whole");
Require(remapped->memory != keptMemory, "after " + name + " the pages mapped again are served from the import made before they were mapped again");
Require(remapped->memoryBase == address && remapped->pinnedBytes == bytes, "after " + name + " the pages mapped again are served from the import that pinned the old ones");
requireImportServesGuest(context, recorder, *remapped, from, from + test.length, ++salt, "the pages mapped again after " + name);
requireImportServesGuest(context, recorder, *remapped, begin, end, ++salt, "the larger side after " + name + " and a new mapping");
}
cleanup();
gpu.unlock();
std::cout << "trimmed import: " << name << ": the larger side was " << (test.trimmed ? "kept" : "imported again") << " and the GPU and the write watch see the guest's bytes" << (test.unmap ? ", also after the pages were mapped again" : "") << '\n';
std::cout << "host import after cut: " << name << ": the changed import was retired before the remaining range was imported, and the GPU and write watch see the guest's bytes" << (test.unmap ? ", also after the pages were mapped again" : "") << '\n';
};
const auto remapWhileTrimmed = [&] {
const std::string name = "an unmap and a new mapping of the kept side while the import waits trimmed";
const std::string name = "an unmap and a new mapping of the retained side while the old import is retired";
Backing backing;
Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, run, 0, &backing.physical) == 0, "allocate the trimmed import test backing");
Require(sceKernelMapDirectMemory(&backing.mapping, bytes, 0x33, 0, backing.physical, run) == 0, "map the trimmed import test backing");
Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, run, 0, &backing.physical) == 0, "allocate the host import cut test backing");
Require(sceKernelMapDirectMemory(&backing.mapping, bytes, 0x33, 0, backing.physical, run) == 0, "map the host import cut test backing");
const auto address = reinterpret_cast<std::uint64_t>(backing.mapping);
auto* memory = static_cast<std::byte*>(backing.mapping);
for (std::size_t offset = 0; offset < bytes; offset += page) std::memset(memory + offset, static_cast<int>(0x40u + (offset / page) % 64u), 64);
gpu.lock();
const auto* whole = HostImportFor(context, address, bytes);
Require(whole != nullptr && whole->base == address && whole->bytes == bytes && whole->alias == nullptr && !whole->unwatched, name + ": the range was not imported whole, settled and watched");
const auto original = whole->memory;
gpu.unlock();
Require(sceKernelMprotect(memory, run, 0x03) == 0, name + ": the protection of the head failed");
Require(sceKernelMunmap(memory + run, bytes - run) == 0, name + ": the unmap of the kept side failed");
@@ -2757,10 +2819,10 @@ int trimmedImportTests(const Device& device) {
const auto* remapped = HostImportFor(context, begin, static_cast<std::size_t>(end - begin));
Require(remapped != nullptr && remapped->base == begin && remapped->base + remapped->bytes == end, name + ": the pages mapped again are not imported whole");
requireImportServesGuest(context, recorder, *remapped, begin, end, ++salt, "the pages mapped again after " + name);
Require(remapped->memory != original, "after " + name + " the pages mapped again are served from the import that pinned the old ones");
Require(remapped->memoryBase == begin && remapped->pinnedBytes == end - begin, "after " + name + " the pages mapped again are served from the import that pinned the old ones");
cleanup();
gpu.unlock();
std::cout << "trimmed import: " << name << ": the pages mapped again were imported again and the GPU and the write watch see the guest's bytes\n";
std::cout << "host import cut: " << name << ": the pages mapped again were imported again and the GPU and the write watch see the guest's bytes\n";
};
gpu.unlock();
try {
@@ -2771,7 +2833,7 @@ int trimmedImportTests(const Device& device) {
throw;
}
gpu.lock();
std::cout << "Trimmed host import tests passed\n";
std::cout << "Host import cut lifetime tests passed\n";
return 0;
}
#endif
@@ -4677,6 +4739,79 @@ bool unitShadowTests(const Device& device, Recorder& recorder) {
return true;
}
void idlePublishWaitTests(const Device& device, Recorder& recorder) {
using namespace AgcDriver::GuestMemory;
const auto& context = device.GetContext();
constexpr std::uint64_t unit = 65536;
constexpr std::size_t bytes = 8 * unit;
void* block = AllocateWatched(bytes, 65536);
Require(block != nullptr, "unit shadows are on without write watching");
struct Release {
void* block;
~Release() { ReleaseWatched(block, bytes); }
} release{block};
std::memset(block, 0x11, bytes);
const auto address = reinterpret_cast<std::uint64_t>(block);
{
GuestAllocations::Mutation mutation;
mutation.Add(block, bytes, true, true, true);
}
struct Unregister {
const Context& context;
void* block;
std::uint64_t address;
~Unregister() {
{
GuestAllocations::Mutation mutation;
mutation.Remove(block);
}
HostImportFor(context, address, bytes);
}
} unregister{context, block, address};
const auto* import = HostImportFor(context, address, bytes);
if (import == nullptr || !Watched(address, bytes)) {
std::cout << "host import of the idle publish test block refused or not watched: idle publish wait not tested\n";
return;
}
recorder.Sync();
CollectWritesUncached(address, bytes);
auto* words = static_cast<std::uint8_t*>(block);
const auto copyBuffer = context.Function<PFN_vkCmdCopyBuffer>("vkCmdCopyBuffer");
const auto retile = [&](std::uint64_t begin, std::uint8_t value) {
auto destination = ShadowDestinationFor(context, *import, begin, begin + unit);
Require(destination.has_value() && destination->seedUnits.empty(), "a whole-unit retile piece has no destination");
auto pattern = std::make_shared<Buffer>(context, static_cast<std::size_t>(unit), VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
std::memset(pattern->Bytes().data(), value, pattern->Bytes().size());
const auto commands = recorder.Commands();
recorder.Keep(pattern);
recorder.Keep(destination->slab);
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT);
const VkBufferCopy piece{0, destination->offset, unit};
copyBuffer(commands, pattern->Handle(), destination->buffer, 1, &piece);
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT);
recorder.MarkCovered(VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT);
const ShadowedRange range{begin, begin + unit, destination->slab};
MarkShadowed(*import, std::span(&range, 1), TrackerGeneration());
Require(AnyShadowedOverlaps(begin, unit), "the retiled unit is not shadowed");
};
retile(address + 2 * unit, 0x22);
retile(address + 3 * unit, 0x33);
StorageTexture::FlushAllPending("idle");
Require(!AnyShadowedOverlaps(address + 2 * unit, static_cast<std::size_t>(2 * unit)), "the idle flush did not publish the shadowed units");
Require(!recorder.PendingWriteOverlaps(address + 2 * unit, static_cast<std::size_t>(2 * unit)), "the idle flush returned before its publish's copies completed");
words[2 * unit] = 0x44;
words[4 * unit - 1] = 0x45;
recorder.Sync();
Require(words[2 * unit] == 0x44 && words[4 * unit - 1] == 0x45, "a CPU store made after the idle flush returned was overwritten by its publish");
Require(words[2 * unit + 1] == 0x22 && words[3 * unit - 1] == 0x22 && words[3 * unit] == 0x33 && words[1 * unit] == 0x11 && words[4 * unit] == 0x11, "the idle publish's bytes are wrong");
retile(address + 5 * unit, 0x55);
StorageTexture::FlushAllPending("label");
Require(recorder.PendingWriteOverlaps(address + 5 * unit, unit), "a publish of a flush other than the idle one was waited");
recorder.Sync();
Require(words[5 * unit] == 0x55 && words[6 * unit - 1] == 0x55, "the label flush's publish did not land");
std::cout << "idle publish wait verified\n";
}
void storageRefreshTests(const Device& device, Recorder& recorder, bool watched) {
const auto& base = device.GetContext();
if (base.hostImportAlignment == 0) {
@@ -7734,10 +7869,11 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) {
target.format = VK_FORMAT_R8G8B8A8_UNORM;
target.bytes = targetBytes;
target.samples = 4;
const bool ordered = MayReorderSamples(context, target.samples) && SampleOrderStorageFormat(context, target.format, target.elementBytes, target.samples) != VK_FORMAT_UNDEFINED;
{
std::lock_guard gpu(GpuMutex());
Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the multisampled target has no view");
Require(!MultisampleTargetStorable(context, target), "a multisampled target nothing stored to was created with storage usage");
Require(MultisampleTargetStorable(context, target) == ordered, ordered ? "a multisampled target whose samples may be reordered was created without storage usage" : "a multisampled target nothing stored to was created with storage usage");
recorder.Submit();
device.WaitQueue();
recorder.Sync();
@@ -7869,9 +8005,9 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) {
const CompiledShader storedCompute{ShaderRecompiler::ShaderStage::Compute, &stored, 0};
std::lock_guard gpu(GpuMutex());
const auto rendered = MultisampleTargetView(context, target);
Require(!MultisampleTargetStorable(context, target), "sampling the multisampled target gave it storage usage");
Require(MultisampleTargetStorable(context, target) == ordered, "sampling the multisampled target changed its storage usage");
ShaderResources resources(context, storedCompute);
Require(MultisampleTargetStorable(context, target) && MultisampleTargetView(context, target) != rendered, "a storage binding did not move the multisampled target to an image with storage usage");
Require(MultisampleTargetStorable(context, target) && (MultisampleTargetView(context, target) != rendered) != ordered, "a storage binding did not move the multisampled target to an image with storage usage, or moved one that had it");
recorder.Submit();
device.WaitQueue();
recorder.Sync();
@@ -8865,8 +9001,8 @@ bool vertexTargetRangeTests(const Device& device, Recorder& recorder, bool impor
const std::array range{std::pair{address + unit, address + 2 * unit}};
Require(StorageTexture::AnyPendingOverlaps(range), "the color producer did not leave GPU results pending");
};
const auto attributeFor = [](std::uint64_t base, std::uint32_t stride, std::uint32_t fetchIndex) {
return ShaderRecompiler::VertexAttribute{0, 4, {{static_cast<std::uint32_t>(base), static_cast<std::uint32_t>(base >> 32u) | (stride << 16u), static_cast<std::uint32_t>(bytes / 16), 77u << 12u}}, fetchIndex};
const auto attributeFor = [](std::uint64_t base, std::uint32_t stride, std::uint32_t fetchIndex, std::uint32_t outOfBoundsSelect = 0) {
return ShaderRecompiler::VertexAttribute{0, 4, {{static_cast<std::uint32_t>(base), static_cast<std::uint32_t>(base >> 32u) | (stride << 16u), static_cast<std::uint32_t>(bytes / 16), (77u << 12u) | (outOfBoundsSelect << 28u)}}, fetchIndex};
};
const auto check = [&](std::uint32_t edge) {
std::vector<std::byte> texels(64 * 64 * 4);
@@ -8937,9 +9073,12 @@ bool vertexTargetRangeTests(const Device& device, Recorder& recorder, bool impor
reject([&] { Draw(context, consumer, indexed, shaders); }, "index buffer aliases the render target");
}
reject([&] { Draw(context, consumer, triangle, shaders); }, "vertex buffer aliases the render target");
vertex.vertexAttributes = {attributeFor(address, 0, fetchIndex)};
vertex.vertexAttributes = {attributeFor(address, 0, fetchIndex, 3)};
reject([&] { Draw(context, consumer, draw, shaders); }, "vertex buffer aliases the render target");
}
vertex.vertexAttributes = {attributeFor(address, 0, 0)};
Draw(context, consumer, triangle, shaders);
WaitForOptimizedPipelines(context.device);
std::cout << "Vertex range and pending color producer tests passed (" << (imported ? "imported" : "copied") << ")\n";
return true;
}
@@ -9944,6 +10083,16 @@ void Run(Context context) {
int main(int argc, char** argv) {
try {
if (argc == 2 && std::string_view(argv[1]) == "--collect-mapping-history-only") {
if (!collectMappingHistoryTests()) return 77;
std::cout << "collect mapping history tests passed\n";
return 0;
}
if (argc == 2 && std::string_view(argv[1]) == "--hint-mapping-history-only") {
if (!hintMappingHistoryTests()) return 77;
std::cout << "hint mapping history tests passed\n";
return 0;
}
if (argc == 2 && std::string_view(argv[1]) == "--partial-watch-restoration-only") {
if (!partialWatchRestorationTests()) return 77;
std::cout << "partial private write-watch restoration tests passed\n";
@@ -9961,7 +10110,7 @@ int main(int argc, char** argv) {
}
std::unique_ptr<Device> created;
try {
created = std::make_unique<Device>(argc == 2 && (std::string_view(argv[1]) == "--import-watch-other-family-only" || std::string_view(argv[1]) == "--deferred-settle-other-family-only"));
created = std::make_unique<Device>(argc == 2 && (std::string_view(argv[1]) == "--import-watch-other-family-only" || std::string_view(argv[1]) == "--deferred-settle-other-family-only" || std::string_view(argv[1]) == "--host-import-lifetime-other-family-only"));
} catch (const std::exception& error) {
if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw;
std::cout << "skipped, no usable Vulkan device: " << error.what() << '\n';
@@ -9986,6 +10135,13 @@ int main(int argc, char** argv) {
return 0;
}
#ifdef _WIN32
if (argc == 2 && std::string_view(argv[1]) == "--host-import-lifetime-other-family-only") {
if (device.GetContext().settleFamily == device.GetContext().queueFamily) {
std::cout << "skipped, no other queue family to settle imports on\n";
return 77;
}
return RunHostImportLifetimeTests(device.GetContext());
}
if (argc == 2 && (std::string_view(argv[1]) == "--deferred-settle-only" || std::string_view(argv[1]) == "--deferred-settle-other-family-only")) {
if (std::string_view(argv[1]) == "--deferred-settle-other-family-only" && device.GetContext().settleFamily == device.GetContext().queueFamily) {
std::cout << "skipped, no other queue family to settle imports on\n";
@@ -9998,7 +10154,7 @@ int main(int argc, char** argv) {
if (argc == 2 && std::string_view(argv[1]) == "--settle-retire-only") return settleRetireTests(device) ? 0 : 77;
if (argc == 2 && std::string_view(argv[1]) == "--settle-storage-image-only") return settleStorageImageTests(device) ? 0 : 77;
if (argc == 2 && std::string_view(argv[1]) == "--reimport-bench") return reimportBench(device);
if (argc == 2 && std::string_view(argv[1]) == "--trimmed-import-only") return trimmedImportTests(device);
if (argc == 2 && std::string_view(argv[1]) == "--host-import-cut-only") return hostImportCutTests(device);
#endif
if (argc == 2 && std::string_view(argv[1]) == "--host-import-retirement-only") {
const auto alignment = device.GetContext().hostImportAlignment;
@@ -10163,6 +10319,14 @@ int main(int argc, char** argv) {
parkedImportUnmapTests(device);
return 0;
}
if (argc == 2 && std::string_view(argv[1]) == "--idle-publish-wait-only") {
if (!AgcDriver::Graphics::UnitShadowEnabled() || device.GetContext().hostImportAlignment == 0) {
std::cout << "skipped, unit shadows or host imports are unavailable\n";
return 77;
}
idlePublishWaitTests(device, recorder);
return 0;
}
if (argc == 2 && std::string_view(argv[1]) == "--unit-shadow-only") {
if (!AgcDriver::Graphics::UnitShadowEnabled() || device.GetContext().hostImportAlignment == 0) {
std::cout << "skipped, unit shadows or host imports are unavailable\n";
@@ -1,33 +0,0 @@
#include "prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp"
#include <cstdio>
namespace {
struct Case {
VkDriverId driver;
bool usable;
};
constexpr Case Cases[]{
{VK_DRIVER_ID_AMD_PROPRIETARY, false},
{VK_DRIVER_ID_AMD_OPEN_SOURCE, true},
{VK_DRIVER_ID_MESA_RADV, true},
{VK_DRIVER_ID_NVIDIA_PROPRIETARY, true},
{VK_DRIVER_ID_INTEL_PROPRIETARY_WINDOWS, true},
{VK_DRIVER_ID_INTEL_OPEN_SOURCE_MESA, true},
{VK_DRIVER_ID_MESA_LLVMPIPE, true},
{VK_DRIVER_ID_MOLTENVK, true},
};
}
int main() {
int failures = 0;
for (const auto& item : Cases) {
if (AgcDriver::RobustBufferAccess2Usable(item.driver) != item.usable) {
std::fprintf(stderr, "driver %d: expected robustBufferAccess2 to be %s\n", static_cast<int>(item.driver), item.usable ? "usable" : "unusable");
++failures;
}
}
return failures == 0 ? 0 : 1;
}
@@ -1865,7 +1865,7 @@ IrResourcePlan handBuiltPlan() {
wide.SetImmediateU64(0x8000000000000001ull);
material.dwords[3] = &wide;
plan.descriptorSources = {buffer, table, heap, material};
plan.controlFlow = {{&condition, {1u, 2u}, {0u}, {}}, {nullptr, {2u}, {3u}, {0u}}, {nullptr, {}, {}, {}}};
plan.controlFlow = {{&condition, {1u, 2u}, {0u}, {}, {1u}}, {nullptr, {2u}, {3u}, {0u}, {0u, 1u}}, {nullptr, {}, {}, {}, {}}};
plan.materializationSources = {0u, 2u};
plan.srtReads = {{&readConst(4), 2u}, {&pointer, 1u}};
plan.guardedSrtSlots = {1u};
@@ -1947,7 +1947,7 @@ bool samePlan(const IrResourcePlan& left, const IrResourcePlan& right) {
for (std::size_t index = 0; index < left.controlFlow.size(); ++index) {
const auto& a = left.controlFlow[index];
const auto& b = right.controlFlow[index];
if (!sameValue(a.condition, b.condition) || a.successors != b.successors || a.sources != b.sources || a.images != b.images) return false;
if (!sameValue(a.condition, b.condition) || a.successors != b.successors || a.sources != b.sources || a.images != b.images || a.flatSlots != b.flatSlots) return false;
}
for (std::size_t index = 0; index < left.srtReads.size(); ++index) {
if (!sameValue(left.srtReads[index].value, right.srtReads[index].value) || left.srtReads[index].flatOffset != right.srtReads[index].flatOffset) return false;
@@ -2452,6 +2452,14 @@ void verifyDefaultDirectory(const char* self) {
int main(int argc, char** argv) {
try {
if (argc == 2 && std::string_view(argv[1]) == "--housekeeping-safety") {
const auto directory = ShaderDiskCache::EntryDirectory();
require(!directory.empty(), "the housekeeping fixture cache is disabled");
ShaderDiskCache::Shutdown();
require(ShaderDiskCache::Writers() == 0, "the housekeeping writer did not stop");
std::cout << "housekeeping version " << directory.filename().string() << '\n';
return 0;
}
if (argc == 2 && std::string_view(argv[1]) == "--resource-plan-codec") {
verifyResultRoundTrip();
verifyEntryRoundTrip();
@@ -15,6 +15,7 @@
#include "Optimization/SrtWalker/SrtInstructionPredicates.hpp"
#include "Optimization/SrtWalker/SrtFlatSlotClasses.hpp"
#include "Optimization/SrtWalker/SrtTape.hpp"
#include "RdnaDecoder/RdnaInstructionDecoder.hpp"
#include "Optimization/SrtWalker.hpp"
#include "IntermediateRepresentation/IrBuilder.hpp"
#include "IntermediateRepresentation/IrProgram.hpp"
@@ -3550,6 +3551,40 @@ void verifyWaterfallImageTable() {
require(!SrtWalker().ValidateRuntimeValue(planA, &firstOther), "waterfall image table: non-implied condition was accepted by validator");
}
void verifySplitWaterfallImageTable() {
using namespace ShaderRecompiler;
namespace Abi = ImageTableAbi;
static TableTexture texture;
alignas(256) static std::array<std::uint32_t, 0x3000> root{};
static std::array<std::uint32_t, 4> output{};
const auto outputV = TableV(output.data(), 0u, static_cast<std::uint32_t>(sizeof(output)));
std::copy(outputV.begin(), outputV.end(), root.begin() + 4);
const auto descriptor = TableT(texture.bytes.data(), TableFormat8888UNorm, TableType2D);
std::copy(descriptor.begin(), descriptor.end(), root.begin() + 0x40u / 4u);
const auto base = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(root.data()));
const std::array<std::uint32_t, 2> userData{static_cast<std::uint32_t>(base), static_cast<std::uint32_t>(base >> 32u)};
const auto code = [](std::uint32_t addend) {
return std::vector<std::uint32_t>{0xf4080700u, 0xfa000010u, 0xf4080800u, 0xfa000020u, 0x7da80086u, 0x7e220500u, 0xd7460001u, 0x00010b00u | (addend << 18u), 0x7e260501u, 0x8f128511u,
0xf4080900u, 0x24000040u, 0xf4080a00u, 0x26000040u, 0xf09c0f08u, 0x01090000u, 0xe0700000u, 0x80070000u, 0xbf810000u};
};
const auto split = code(0x90u);
auto request = TableRequest(split, userData);
request.shader.codeAddress = 0x3a000u;
const auto plan = GetResourcePlan(request);
require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u, "split waterfall image table: the T# was not planned as a table");
const auto& column = *plan->descriptorSources.at(plan->info.images[0].source).tableColumn;
require(column.address && column.stride == 32u && column.offset == 0x40u && column.dwordCount == 8u && column.foldedCarry == 16u, "split waterfall image table: the column is wrong");
const auto compiled = CompileTable(request);
const auto& snapshot = compiled.capture->snapshot.tables.tables.at(0);
require((snapshot.codes[0] & Abi::PoisonFlag) == 0u && snapshot.words.at(snapshot.codes[0]).dwords == descriptor, "split waterfall image table: record 0 was not read whole");
for (const std::uint32_t addend : {0xa0u, 0x88u}) {
const auto otherCode = code(addend);
auto other = TableRequest(otherCode, userData);
other.shader.codeAddress = 0x3b000u + addend * 0x100u;
expectFailure([&] { static_cast<void>(GetResourcePlan(other)); }, "GetImageResource dword 0 is not a valid runtime value", "split waterfall image table: a second half outside the record was accepted");
}
}
void verifyDescriptorPhis() {
using namespace ShaderRecompiler;
constexpr std::uint32_t Format8888UNorm = 56;
@@ -4848,7 +4883,7 @@ void verifyPixelRequestSerialization() {
minimal.context.pixel->layerExported = true;
minimal.context.pixel->ditheredAlphaToCoverage = true;
const auto encoded = serializer.Serialize(minimal);
require(requestPrefix(encoded, 8u) == "NVNQQSkAAAA=", "new requests did not use serialization version 41");
require(requestPrefix(encoded, 8u) == "NVNQQSoAAAA=", "new requests did not use serialization version 42");
const std::array<std::uint32_t, 2> fallbacks{0x40u, 0x1ac4u};
auto withFallbacks = minimal;
withFallbacks.context.linkedCallFallbacks = fallbacks;
@@ -4966,7 +5001,7 @@ void verifyPixelRequestSerialization() {
for (std::size_t bytes = 0; bytes < 45u; ++bytes) {
expectFailure([&] { static_cast<void>(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class, layer export, dither or front-face flag, quad pixel mask or quad sample mask was accepted");
}
for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQSoAAAA="}) {
for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQSsAAAA="}) {
expectFailure([&] { static_cast<void>(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted");
}
static constexpr std::string_view version18Request{
@@ -5108,6 +5143,39 @@ void verifyVertexInfoVersion19() {
require(blendOffset == allBitsOffset + 18u, "the reversed-order blend code is not serialized after the front-face all-bits flag, quad pixel mask and quad sample masks");
constexpr std::size_t sampleMaskBytes = 16u;
constexpr std::size_t iterationBytes = 35u;
constexpr std::size_t sampleOrderBytes = 16u;
const auto orderOffset = blendOffset + 4u + iterationBytes;
auto ordered = pixel;
ordered.context.pixel->sampleOrder = {0x76543210u, 0x76543210u, 0x76543201u, 0x76543201u};
const auto orderedBytes = decodeBase64(serializer.Serialize(ordered));
require(orderedBytes.size() == plainPixel.size() && std::equal(plainPixel.begin(), plainPixel.begin() + static_cast<std::ptrdiff_t>(orderOffset), orderedBytes.begin()) && orderedBytes[orderOffset] == 0x10u && orderedBytes[orderOffset + 11u] == 0x76u && orderedBytes[orderOffset + 12u] == 0x01u && std::equal(plainPixel.begin() + static_cast<std::ptrdiff_t>(orderOffset + sampleOrderBytes), plainPixel.end(), orderedBytes.begin() + static_cast<std::ptrdiff_t>(orderOffset + sampleOrderBytes)), "the sample order is not the four words the version 42 pixel info adds after the stencil operation value export flag");
auto orderedVersion34 = orderedBytes;
orderedVersion34.pop_back();
orderedVersion34.pop_back();
orderedVersion34.pop_back();
orderedVersion34.erase(orderedVersion34.begin() + static_cast<std::ptrdiff_t>(orderOffset - 1u), orderedVersion34.begin() + static_cast<std::ptrdiff_t>(orderOffset + sampleOrderBytes));
orderedVersion34[4] = 34u;
const auto orderedReplay34 = serializer.Deserialize(encodeBase64(orderedVersion34));
require(orderedReplay34.request.context.pixel.has_value() && orderedReplay34.request.context.pixel->sampleOrder == std::array<std::uint32_t, 4>{} && serializer.Serialize(orderedReplay34.request) == encodedPixel, "a version 34 request read a sample order, or misread the fields after its pixel iteration fields");
auto orderedVersion30 = orderedBytes;
orderedVersion30.resize(orderedVersion30.size() - 4u);
orderedVersion30.erase(orderedVersion30.begin() + static_cast<std::ptrdiff_t>(blendOffset + 4u), orderedVersion30.begin() + static_cast<std::ptrdiff_t>(orderOffset + sampleOrderBytes));
orderedVersion30[4] = 30u;
const auto orderedReplay30 = serializer.Deserialize(encodeBase64(orderedVersion30));
require(orderedReplay30.request.context.pixel.has_value() && orderedReplay30.request.context.pixel->sampleOrder == std::array<std::uint32_t, 4>{} && serializer.Serialize(orderedReplay30.request) == encodedPixel, "a version 30 request read a sample order, or misread the fields after its reversed-order blend code");
auto orderedSpanned = ordered;
orderedSpanned.target.spannedWaves = true;
auto orderedVersion32 = decodeBase64(serializer.Serialize(orderedSpanned));
orderedVersion32.pop_back();
require(orderedVersion32.size() > 2u && orderedVersion32.back() == 0u && orderedVersion32[orderedVersion32.size() - 2u] == 0u && orderedVersion32[orderedVersion32.size() - 3u] == 1u, "the spanned-waves flag is not the byte before the depth remap flag");
orderedVersion32.pop_back();
orderedVersion32.pop_back();
orderedVersion32.erase(orderedVersion32.begin() + static_cast<std::ptrdiff_t>(blendOffset + 4u), orderedVersion32.begin() + static_cast<std::ptrdiff_t>(orderOffset + sampleOrderBytes));
orderedVersion32[4] = 32u;
const auto orderedReplay32 = serializer.Deserialize(encodeBase64(orderedVersion32));
auto unordered = pixel;
unordered.target.spannedWaves = true;
require(orderedReplay32.request.context.pixel.has_value() && orderedReplay32.request.context.pixel->sampleOrder == std::array<std::uint32_t, 4>{} && orderedReplay32.request.target.spannedWaves && serializer.Serialize(orderedReplay32.request) == serializer.Serialize(unordered), "a version 32 request read a sample order or lost its spanned-waves flag");
auto maskedPixel = pixel;
maskedPixel.context.pixel->quadPixelMask = 0x2u;
const auto maskedBytes = decodeBase64(serializer.Serialize(maskedPixel));
@@ -5123,7 +5191,7 @@ void verifyVertexInfoVersion19() {
version30.pop_back();
version30.pop_back();
version30.pop_back();
version30.erase(version30.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u), version30.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes));
version30.erase(version30.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u), version30.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes));
version30[4] = 30u;
const auto replay30 = serializer.Deserialize(encodeBase64(version30));
require(replay30.request.context.pixel.has_value() && !replay30.request.context.pixel->iteratesSamples && replay30.request.context.pixel->positionLocation == PixelPositionLocation::Center && replay30.request.context.pixel->sampleLocations == std::array<std::uint8_t, 32>{} && decodeBase64(serializer.Serialize(replay30.request)) == sampledBytes, "a version 30 request read pixel iteration, a position location or sample locations, or misread the fields after its reversed-order blend code");
@@ -5134,7 +5202,7 @@ void verifyVertexInfoVersion19() {
require(version32.size() > 2u && version32.back() == 0u && version32[version32.size() - 2u] == 0u && version32[version32.size() - 3u] == 1u, "the spanned-waves flag is not the byte before the depth remap flag");
version32.pop_back();
version32.pop_back();
version32.erase(version32.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u), version32.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes));
version32.erase(version32.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u), version32.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes));
version32[4] = 32u;
const auto replay32 = serializer.Deserialize(encodeBase64(version32));
require(replay32.request.context.pixel.has_value() && !replay32.request.context.pixel->iteratesSamples && replay32.request.context.pixel->sampleLocations == std::array<std::uint8_t, 32>{} && replay32.request.target.spannedWaves && serializer.Serialize(replay32.request) == serializer.Serialize(spanned), "a version 32 request read pixel iteration or sample locations, or lost its spanned-waves flag");
@@ -5143,10 +5211,10 @@ void verifyVertexInfoVersion19() {
version29.pop_back();
version29.pop_back();
version29.pop_back();
version29.erase(version29.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes), version29.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes));
version29.erase(version29.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes), version29.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes));
auto version36 = sampledBytes;
version36.pop_back();
version36.erase(version36.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes - 1u));
version36.erase(version36.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes - 1u), version36.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes));
version36[4] = 36u;
const auto replay36 = serializer.Deserialize(encodeBase64(version36));
require(replay36.request.context.pixel.has_value() && !replay36.request.context.pixel->stencilOperationValueExport && decodeBase64(serializer.Serialize(replay36.request)) == sampledBytes, "a version 36 request read a stencil operation value export or misread the fields after the sample locations");
@@ -5158,7 +5226,7 @@ void verifyVertexInfoVersion19() {
version28.pop_back();
version28.pop_back();
version28.pop_back();
version28.erase(version28.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u), version28.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes));
version28.erase(version28.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u), version28.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes));
version28[4] = 28u;
const auto replay28 = serializer.Deserialize(encodeBase64(version28));
require(replay28.request.context.pixel.has_value() && replay28.request.context.pixel->quadPixelMask == 0x2u && replay28.request.context.pixel->quadSampleMask == std::array<std::uint16_t, 4>{0xffffu, 0xffffu, 0xffffu, 0xffffu} && replay28.request.context.pixel->reversedBlend == 0u && decodeBase64(serializer.Serialize(replay28.request)) == maskedBytes, "a version 28 request read quad sample masks or a reversed-order blend code, or misread the fields after them");
@@ -5167,7 +5235,7 @@ void verifyVertexInfoVersion19() {
version27.pop_back();
version27.pop_back();
version27.pop_back();
version27.erase(version27.begin() + static_cast<std::ptrdiff_t>(maskOffset), version27.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes));
version27.erase(version27.begin() + static_cast<std::ptrdiff_t>(maskOffset), version27.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes));
version27[4] = 27u;
const auto replay27 = serializer.Deserialize(encodeBase64(version27));
require(replay27.request.context.pixel.has_value() && replay27.request.context.pixel->quadPixelMask == 0xfu && replay27.request.context.pixel->reversedBlend == 0u && serializer.Serialize(replay27.request) == encodedPixel, "a version 27 request read a quad pixel mask or reversed-order blend code, or misread the fields after it");
@@ -5176,7 +5244,7 @@ void verifyVertexInfoVersion19() {
legacyPixel.pop_back();
legacyPixel.pop_back();
legacyPixel.pop_back();
legacyPixel.erase(legacyPixel.begin() + static_cast<std::ptrdiff_t>(allBitsOffset), legacyPixel.begin() + static_cast<std::ptrdiff_t>(allBitsOffset + 2u + sampleMaskBytes + 4u + iterationBytes));
legacyPixel.erase(legacyPixel.begin() + static_cast<std::ptrdiff_t>(allBitsOffset), legacyPixel.begin() + static_cast<std::ptrdiff_t>(allBitsOffset + 2u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes));
legacyPixel[4] = 26u;
const auto replay26 = serializer.Deserialize(encodeBase64(legacyPixel));
require(replay26.request.context.pixel.has_value() && replay26.request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle && !replay26.request.context.pixel->frontFaceAllBits && replay26.request.context.pixel->reversedBlend == 0u, "a version 26 request lost its pixel primitive class or read a front-face all-bits flag");
@@ -6960,6 +7028,161 @@ bool GuardRecords(const ShaderRecompiler::ResourceCapture& capture) {
return true;
}
void verifyGuardedDirectRoot() {
using namespace ShaderRecompiler;
constexpr std::size_t Reserved = 0x10000;
void* reserved = nullptr;
#ifdef _WIN32
reserved = VirtualAlloc(nullptr, Reserved, MEM_RESERVE, PAGE_NOACCESS);
#else
reserved = mmap(nullptr, Reserved, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (reserved == MAP_FAILED) reserved = nullptr;
#endif
require(reserved != nullptr, "guarded direct root: cannot reserve an inaccessible range");
const auto unmapped = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(reserved));
alignas(256) static std::array<std::uint32_t, 64> output{};
alignas(256) static std::array<std::uint32_t, 16> root{};
static std::uint32_t payload = 0x3f800000u;
const auto outputAddress = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(output.data()));
const auto rootAddress = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(root.data()));
const auto payloadAddress = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&payload));
root = {static_cast<std::uint32_t>(outputAddress), static_cast<std::uint32_t>((outputAddress >> 32u) & 0xffffu), static_cast<std::uint32_t>(sizeof(output)), 0x30027facu, 1u};
const std::vector<std::uint32_t> branchCode{0xf4080100u, 0xfa000000u, 0xf4000200u, 0xfa000010u, 0xbe8a0302u, 0xbe8b0303u, 0xbe8c03ffu, 0x11111111u,
0xbf8cc07fu, 0xbf068008u, 0xbf850003u, 0xf4000305u, 0xfa000000u, 0xbf8cc07fu, 0x7e02020cu, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u};
for (const auto index : {4u, 5u}) {
const auto move = DecodeRdnaInstruction(index * 4u, branchCode, index);
require(move.op == RdnaOpcode::SMovB32 && move.destination.kind == RdnaOperandKind::ScalarRegister && move.destination.reg == index + 6u &&
move.source0.kind == RdnaOperandKind::ScalarRegister && move.source0.reg == index - 2u, "guarded direct root: the fixture does not move the user-data pointer to s10:s11");
}
const auto compare = DecodeRdnaInstruction(0x24u, branchCode, 9u);
require(compare.op == RdnaOpcode::SCmpEqU32 && compare.source0.kind == RdnaOperandKind::ScalarRegister && compare.source0.reg == 8u &&
compare.source1.kind == RdnaOperandKind::IntegerInlineConstant && compare.source1.value == 0u, "guarded direct root: the fixture does not compare the branch word with zero");
const auto condition = DecodeRdnaInstruction(0x28u, branchCode, 10u);
require(condition.op == RdnaOpcode::SCbranchScc1 && condition.branchTarget == 0x38u, "guarded direct root: the fixture does not skip the scalar load when the branch word is zero");
NestedRequest branch(branchCode, root.data());
std::array<std::uint32_t, 4> userData{static_cast<std::uint32_t>(rootAddress), static_cast<std::uint32_t>(rootAddress >> 32u),
static_cast<std::uint32_t>(payloadAddress), static_cast<std::uint32_t>(payloadAddress >> 32u)};
branch.request.context.userData = userData;
AgcDriver::ShaderMemory validMemory({});
const auto valid = validMemory.Capture(branch.request);
require(valid->snapshot.srtPoison.empty(), "guarded direct root: a mapped direct pointer was poisoned");
const auto read = std::find_if(valid->plan->srtReads.begin(), valid->plan->srtReads.end(), [](const auto& candidate) { return candidate.value->template Flags<MemoryFlags>().pc == 0x2cu; });
require(read != valid->plan->srtReads.end(), "guarded direct root: the scalar read was not planned");
require(!Detail::LoadedFromMemory(*read->value->Argument(0)), "guarded direct root: the pointer was classified as loaded from memory");
require(valid->snapshot.flattenedSrt.at(read->flatOffset) == payload, "guarded direct root: the mapped scalar read has the wrong word");
const auto validRegions = validMemory.Regions();
branch.request.context.memory = validRegions;
const auto validResult = Recompile(branch.request, *valid);
require(validResult->poisonedSrtReads == 0u, "guarded direct root: the mapped program reports poison");
#if ANYPS5_ENABLE_SPIRV_TOOLS
static_cast<void>(ValidateAndOptimizeSpirv(validResult->spirv, branch.request.target.vulkanVersion, branch.request.target.spirvVersion));
#endif
std::cout << "guarded direct root: mapped taken load passed\n";
root[4] = 0u;
branch.request.context.memory = {};
AgcDriver::ShaderMemory mappedSkippedMemory({});
const auto mappedSkipped = mappedSkippedMemory.Capture(branch.request);
const auto mappedSkippedRegions = mappedSkippedMemory.Regions();
branch.request.context.memory = mappedSkippedRegions;
const auto mappedSkippedResult = Recompile(branch.request, *mappedSkipped);
require(mappedSkippedResult->poisonedSrtReads == 0u, "guarded direct root: the mapped skipped program reports poison");
#if ANYPS5_ENABLE_SPIRV_TOOLS
static_cast<void>(ValidateAndOptimizeSpirv(mappedSkippedResult->spirv, branch.request.target.vulkanVersion, branch.request.target.spirvVersion));
#endif
std::cout << "guarded direct root: mapped skipped load passed\n";
root[4] = 1u;
userData[2] = static_cast<std::uint32_t>(unmapped);
userData[3] = static_cast<std::uint32_t>(unmapped >> 32u);
branch.request.context.memory = {};
expectFailure([&] {
AgcDriver::ShaderMemory memory({});
static_cast<void>(memory.Capture(branch.request));
}, "guest memory is not readable", "guarded direct root: a taken inaccessible load was accepted");
std::cout << "guarded direct root: inaccessible taken load rejected\n";
root[4] = 0u;
std::vector<std::byte> encodedPlan;
require(ShaderDiskCache::EncodePlan(*valid->plan, encodedPlan), "guarded direct root: cannot encode the resource plan");
IrResourcePlan decodedPlan;
require(ShaderDiskCache::DecodePlan(encodedPlan, decodedPlan), "guarded direct root: cannot decode the resource plan");
require(!valid->plan->controlFlow.empty() && decodedPlan.controlFlow.size() == valid->plan->controlFlow.size(), "guarded direct root: the scalar branch was not retained in the plan");
for (std::size_t block = 0; block < decodedPlan.controlFlow.size(); ++block) {
require(decodedPlan.controlFlow[block].flatSlots == valid->plan->controlFlow[block].flatSlots, "guarded direct root: cached control flow lost its flat slots");
}
IrResourcePlan decodedIrPlan;
require(ShaderDiskCache::DecodePlan(encodedPlan, decodedIrPlan), "guarded direct root: cannot decode the IR control plan");
decodedIrPlan.compact = {};
struct ReadContext {
std::uint64_t root;
std::uint64_t payload;
bool guardReadable;
std::uint32_t invalidReads = 0;
SrtMemoryReader reader = nullptr;
};
const auto walk = [&](const IrResourcePlan& plan, bool tape, bool guardReadable) {
ReadContext context{rootAddress, payloadAddress, guardReadable};
context.reader = [](void* opaque, std::uint64_t address, std::uint32_t* value) {
auto& context = *static_cast<ReadContext*>(opaque);
if (!((address >= context.root && address - context.root <= sizeof(root) - sizeof(*value)) || address == context.payload)) {
++context.invalidReads;
return false;
}
std::memcpy(value, reinterpret_cast<const void*>(address), sizeof(*value));
return true;
};
SrtRuntime runtime{userData};
runtime.userContext = &context;
runtime.readMemory = context.reader;
runtime.readSpecializationMemory = [](void* opaque, std::uint64_t address, std::uint32_t* value) {
auto& context = *static_cast<ReadContext*>(opaque);
if (!context.guardReadable && address == context.root + 16u) return false;
return context.reader(opaque, address, value);
};
const std::vector<std::uint32_t> sourceCopy(plan.materializationSources.begin(), plan.materializationSources.end());
const std::vector<std::uint8_t> cleanCopy(plan.cleanFlatSlots.begin(), plan.cleanFlatSlots.end());
const auto sources = tape ? std::span<const std::uint32_t>(plan.materializationSources) : std::span<const std::uint32_t>(sourceCopy);
const auto clean = tape ? std::span<const std::uint8_t>(plan.cleanFlatSlots) : std::span<const std::uint8_t>(cleanCopy);
std::vector<DescriptorValue> descriptors;
std::vector<std::uint32_t> flat;
std::vector<std::uint8_t> active;
std::vector<SrtReadPoison> poison;
const auto* compiled = tape ? Detail::SrtTape::For(plan) : nullptr;
if (tape) require(compiled != nullptr, "guarded direct root: the plan has no SRT tape");
const bool ok = tape ? compiled->Run(plan, runtime, descriptors, flat, active) : Detail::EvaluateRuntimeSourcesImpl(plan, sources, runtime, descriptors, flat, true, clean, active, &poison, nullptr);
require(ok == guardReadable, "guarded direct root: a known skipped load failed, or an unresolved guard silently succeeded");
if (guardReadable) require(context.invalidReads == 0u && poison.empty(), "guarded direct root: the walker touched a known skipped load");
};
for (const IrResourcePlan* plan : {valid->plan.get(), static_cast<const IrResourcePlan*>(&decodedPlan), static_cast<const IrResourcePlan*>(&decodedIrPlan)}) {
for (const bool tape : {false, true}) {
walk(*plan, tape, true);
walk(*plan, tape, false);
}
}
auto repeatedCode = branchCode;
repeatedCode.insert(repeatedCode.begin() + 14, {0xf4000305u, 0xfa000000u, 0xbf8cc07fu});
NestedRequest repeated(std::move(repeatedCode), root.data());
repeated.request.context.userData = userData;
expectFailure([&] {
AgcDriver::ShaderMemory memory({});
static_cast<void>(memory.Capture(repeated.request));
}, "guest memory is not readable", "guarded direct root: an unconditional repeated load was skipped with its guarded use");
AgcDriver::ShaderMemory skippedMemory({});
const auto skipped = skippedMemory.Capture(branch.request);
const auto skippedRegions = skippedMemory.Regions();
require(skipped->snapshot.srtPoison.empty() && !RegionsTouch(skippedRegions, unmapped, 4u), "guarded direct root: a skipped scalar load was captured or poisoned");
branch.request.context.memory = skippedRegions;
const auto skippedResult = Recompile(branch.request, *skipped);
require(skippedResult->poisonedSrtReads == 0u, "guarded direct root: the skipped program reports poison");
#if ANYPS5_ENABLE_SPIRV_TOOLS
static_cast<void>(ValidateAndOptimizeSpirv(skippedResult->spirv, branch.request.target.vulkanVersion, branch.request.target.spirvVersion));
#endif
std::cout << "guarded direct root: inaccessible skipped load passed\n";
#ifdef _WIN32
VirtualFree(reserved, 0, MEM_RELEASE);
#else
munmap(reserved, Reserved);
#endif
}
void verifyGuardedNullPointers() {
using namespace ShaderRecompiler;
constexpr std::size_t Reserved = 0x10000;
@@ -7041,16 +7264,16 @@ void verifyGuardedNullPointers() {
AgcDriver::ShaderMemory memory({});
const auto capture = memory.Capture(branch.request);
const auto& poison = capture->snapshot.srtPoison;
require(poison.size() == 1u && poison[0].pc == 0x2cu && poison[0].address == pointer && slotPc(*capture->plan, poison[0].slot) == 0x2cu, "guarded pointer: the nested read was not poisoned at its pc and address");
require(condition == 0u ? poison.empty() : poison.size() == 1u && poison[0].pc == 0x2cu && poison[0].address == pointer && slotPc(*capture->plan, poison[0].slot) == 0x2cu, "guarded pointer: a skipped nested read was poisoned, or an executed read has the wrong poison record");
require(GuardRecords(*capture), "guarded pointer: the poison record is not where the guard reads it");
const auto regions = memory.Regions();
require(!RegionsTouch(regions, pointer, 4u) && RegionsCover(regions, root.data() + 6, 8u), "guarded pointer: the captured regions are wrong");
branch.request.context.memory = regions;
const auto result = Recompile(branch.request, *capture);
require(result->poisonedSrtReads == 1u && faultBinding(*result) && result->bdaAbiVersion == BdaAbi::Version, "guarded pointer: the poisoned capture does not report through the fault buffer");
require(result->poisonedSrtReads == condition && faultBinding(*result) && result->bdaAbiVersion == BdaAbi::Version, "guarded pointer: the capture has the wrong poison count or no fault buffer");
require(samePipeline(*result, *validResult), "guarded pointer: poison changed the variant, the specialization or the module");
const auto replay = Recompile(branch.request);
require(replay.poisonedSrtReads == 1u && samePipeline(replay, *result), "guarded pointer: a replay of the captured regions did not reproduce the poison");
require(replay.poisonedSrtReads == condition && samePipeline(replay, *result), "guarded pointer: a replay of the captured regions did not reproduce the poison");
verifyResult(*result, replay);
#if ANYPS5_ENABLE_SPIRV_TOOLS
static_cast<void>(ValidateAndOptimizeSpirv(result->spirv, branch.request.target.vulkanVersion, branch.request.target.spirvVersion));
@@ -7155,11 +7378,11 @@ void verifyGuardedNullPointers() {
vertex.request.context.memory = {};
AgcDriver::ShaderMemory memory({});
const auto capture = memory.Capture(vertex.request);
require(poisonedWords(*capture, 0x20u, 0x10u), "guarded pointer: a vertex V# behind a branch and a null pointer did not poison its four words at its pc");
require(guard == 0u ? capture->snapshot.srtPoison.empty() && GuardRecords(*capture) : poisonedWords(*capture, 0x20u, 0x10u), "guarded pointer: a skipped vertex V# was poisoned, or an executed read has the wrong poison record");
const auto regions = memory.Regions();
vertex.request.context.memory = regions;
const auto result = Recompile(vertex.request, *capture);
require(result->poisonedSrtReads == 4u && faultBinding(*result) && !result->spirv.empty(), "guarded pointer: a vertex capture with a poisoned V# has no fault buffer");
require(result->poisonedSrtReads == guard * 4u && faultBinding(*result) && !result->spirv.empty(), "guarded pointer: a vertex capture has the wrong poison count or no fault buffer");
#if ANYPS5_ENABLE_SPIRV_TOOLS
static_cast<void>(ValidateAndOptimizeSpirv(result->spirv, vertex.request.target.vulkanVersion, vertex.request.target.spirvVersion));
#endif
@@ -7433,6 +7656,7 @@ int main(int argc, char** argv) {
verifyChainedTableOffset();
verifyPointerChainImageTable();
verifyWaterfallImageTable();
verifySplitWaterfallImageTable();
std::cout << "Image table keys, maps, records and registration passed\n";
return 0;
}
@@ -7496,6 +7720,7 @@ int main(int argc, char** argv) {
verifyChainedTableOffset();
verifyPointerChainImageTable();
verifyWaterfallImageTable();
verifySplitWaterfallImageTable();
verifyDescriptorPhis();
verifyUniformTableImage();
verifyLaneSelectedDescriptors();
@@ -7531,6 +7756,7 @@ int main(int argc, char** argv) {
verifyFunctionLdsBound();
verifyFunctionLdsUpdates();
verifySrtReadPatching();
verifyGuardedDirectRoot();
verifyGuardedNullPointers();
verifyFmaskImageMode();
verifyCubeDepthCompareMode();
@@ -386,6 +386,29 @@ void RunTextureTilingTests() {
Require(dense.pitchBytes == 1024 && CoveredMipBytes(TextureTileMode::kLinear, 4, dense) == std::vector<std::pair<std::uint64_t, std::uint64_t>>{{0, 3072}}, "linear rows without padding are not one range");
}
{
const auto* equation = FindTextureSwizzleEquation(XorSwizzleMode(TextureTileMode::kZ64KBX), 4u);
Require(equation != nullptr, "missing the 64KB Z_X swizzle equation of 4-byte elements");
for (const auto mask : equation->bits) Require((mask & 0x00f80f80u) == 0u, "a 64KB Z_X address bit of a 4-byte element reads x or y bits past one 128x128 tile");
GuestTextureResource plane{};
plane.width = 3840;
plane.height = 2160;
plane.mipCount = 1;
plane.tileMode = TextureTileMode::kZ64KBX;
plane.dimension = TextureDimension::k2D;
plane.format = 22;
const auto geometry = DescribeSurface(plane);
const auto& mip = geometry.mips.front();
const auto block = ThinBlockLayout(TextureTileMode::kZ64KBX, 4u);
Require(block[0] == 65536u && block[1] == 128u && block[2] == 128u, "a 64KB Z_X tile of 4-byte elements is not 128x128");
Require(!mip.tail && mip.tiledOffset == 0u && mip.blocksPerRow == 30u && geometry.guestBytes == 510ull * 65536ull, "a 3840x2160 D32 Z_X plane is not 30x17 tiles");
const std::array<std::pair<std::uint32_t, std::uint32_t>, 8> samples{{{0, 0}, {127, 127}, {128, 0}, {3839, 0}, {0, 128}, {1000, 700}, {2047, 1151}, {3839, 2159}}};
for (const auto& [x, y] : samples) {
const auto offset = mip.tiledOffset + (static_cast<std::uint64_t>(y / block[2]) * mip.blocksPerRow + x / block[1]) * block[0] + equationOffset(*equation, x, y, 0);
Require(offset / 65536u == static_cast<std::uint64_t>(y >> 7u) * 30u + (x >> 7u), "element (" + std::to_string(x) + ", " + std::to_string(y) + ") of a 3840x2160 D32 Z_X plane lies outside the 64 KiB unit of its 128x128 tile");
}
}
reject([] { ComputeSurfaceSize({}, 1); }, "empty mip chain");
reject([] { ComputeSurfaceSize(ComputeMipLayout(TextureTileMode::kLinear, 1, 4, 4, 1), 0); }, "zero array layers");
@@ -151,10 +151,25 @@ void run() {
const auto zeroStride = live.TryGet(101, input);
Require(VertexFetchOutOfRange(input[0]), "zero records did not select zero fill");
input[0].resource.fields[2] = 64;
Require(live.TryGet(101, input) == zeroStride && !VertexFetchOutOfRange(input[0]) && VertexBufferExtent(input[0]) == 64, "cached zero-stride layout froze the live zero-fill decision");
Require(live.TryGet(101, input) == zeroStride && !VertexFetchOutOfRange(input[0]) && VertexBufferExtent(input[0]) == 16, "cached zero-stride layout froze the live zero-fill decision");
input[0].resource.fields[2] = 0;
Require(live.TryGet(101, input) == zeroStride && VertexFetchOutOfRange(input[0]), "record count zero reused a live fetch");
input[0] = attribute();
input[0].resource.fields[1] = 0;
input[0].resource.fields[3] |= 3u << 28u;
input[0].resource.fields[2] = 16;
const auto wholeFetch = live.TryGet(105, input);
for (const auto records : {0u, 8u, 15u}) {
input[0].resource.fields[2] = records;
Require(live.TryGet(105, input) == wholeFetch && VertexFetchOutOfRange(input[0]), "a fetch that only partly fits was not read as zero");
}
for (const auto records : {16u, 17u, 0xffffffffu}) {
input[0].resource.fields[2] = records;
Require(live.TryGet(105, input) == wholeFetch && !VertexFetchOutOfRange(input[0]) && VertexBufferExtent(input[0]) == 16, "a fetch inside the byte range was read as zero or sized by NUM_RECORDS");
}
input[0].resource.fields[3] &= ~(3u << 28u);
Require(VertexFetchOutOfRange(input[0]), "OOB_SELECT 0 read a zero-stride fetch");
input[0] = attribute();
input[0].formatComponents = 1;
const auto narrow = live.TryGet(102, input);
input[0].formatComponents = 2;
@@ -71,7 +71,7 @@ int main() {
std::uint32_t provokingVertex;
bool accepted;
};
constexpr std::array meshExpectations{MeshExpectation{0u, 0u, false}, MeshExpectation{1u, 0u, false}, MeshExpectation{2u, 0u, true}, MeshExpectation{2u, 1u, true}, MeshExpectation{2u, 2u, false}, MeshExpectation{3u, 2u, true}, MeshExpectation{3u, 3u, false}, MeshExpectation{4u, 0u, false}, MeshExpectation{4u, 3u, false}};
constexpr std::array meshExpectations{MeshExpectation{0u, 0u, false}, MeshExpectation{1u, 0u, true}, MeshExpectation{1u, 1u, false}, MeshExpectation{2u, 0u, true}, MeshExpectation{2u, 1u, true}, MeshExpectation{2u, 2u, false}, MeshExpectation{3u, 2u, true}, MeshExpectation{3u, 3u, false}, MeshExpectation{4u, 0u, false}, MeshExpectation{4u, 3u, false}};
const auto meshWith = [&](const MeshExpectation& expectation) {
auto varied = mesh;
varied.outputPrimitiveSize = expectation.outputPrimitiveSize;
@@ -12,6 +12,9 @@
#include <sys/mman.h>
#include <unistd.h>
#endif
#ifdef __GLIBC__
#include <malloc.h>
#endif
#include <array>
#include <cstdint>
#include <cstring>
@@ -1153,6 +1156,38 @@ void CheckImportWindowOverUnwatchedBlock() {
Require(after != 0 && !UnchangedSince(base, Block, after), "a store after an import window over an unwatched block was missed");
}
void CheckImportWindowFootprint() {
#ifdef __GLIBC__
constexpr std::size_t bytes = std::size_t{64} << 20;
void* memory = AllocateWatched(bytes);
const auto base = reinterpret_cast<std::uint64_t>(memory);
std::memset(memory, 0x5a, bytes);
Require(CollectWritesUncached(base, bytes) != 0, "the footprint import window block is not collected");
const auto heapBytes = [] {
const auto info = mallinfo2();
return info.uordblks + info.hblkhd;
};
const auto before = heapBytes();
std::size_t during = 0;
Require(ImportWatched(base, bytes, [&] {
during = heapBytes();
return true;
}), "the footprint import window import failed");
Require(Watched(base, bytes), "the footprint import window left its range unwatched");
const auto held = during > before ? during - before : 0;
Require(held <= bytes / 64, "an import window over " + std::to_string(bytes >> 20) + " MiB held " + std::to_string(held >> 20) + " MiB of host memory during the import: it copies the range instead of hashing its pages");
auto* plain = static_cast<volatile std::uint8_t*>(memory);
const auto synced = CollectWritesUncached(base, bytes);
Require(ImportWatched(base, bytes, [&] {
plain[0x100] = 0x5a;
plain[Block + 0x100] = 0x5b;
return true;
}), "the footprint import window re-import failed");
Require(UnchangedSince(base, Block, synced), "a page a hashed import window resolved without changing its bytes reads as written");
Require(WrittenSince(base + Block, Block, synced), "a store of new bytes within a hashed import window was missed");
#endif
}
void ProtectPage(std::uint64_t address, bool readable) {
#ifdef _WIN32
DWORD previous = 0;
@@ -1442,6 +1477,7 @@ int main() {
CheckImportWindowOtherThread();
CheckImportWindowOverUnwatchedBlock();
CheckStoreAcrossImports();
CheckImportWindowFootprint();
CheckUnwatchedImport();
CheckCommittedRanges();
CheckAdoptLatestEpoch();
@@ -68,6 +68,8 @@ alignas(256) std::array<std::uint32_t, 16> PixelCode{};
alignas(256) std::array<std::uint32_t, Threads> Output{};
void SampleSliceZero(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode);
void OverlappingArrays(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode, bool cpuWrite = false);
void OverlappingArrayLastSlice(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode);
std::uint64_t AddressOf(const void* data) {
return static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(data));
@@ -81,22 +83,24 @@ std::string Hex(std::uint32_t value) {
class GuestBlock {
public:
explicit GuestBlock(std::size_t registeredBytes = SurfaceBytes, std::size_t registeredOffset = 0) : registeredOffset(registeredOffset) {
explicit GuestBlock(std::size_t registeredBytes = SurfaceBytes, std::size_t registeredOffset = 0, bool commitAll = true) : registeredOffset(registeredOffset) {
#ifdef _WIN32
block = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, LayerBytes));
if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, SurfaceBytes, PAGE_READWRITE, SurfaceBytes);
if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block + (commitAll ? 0 : registeredOffset), commitAll ? SurfaceBytes : registeredBytes, PAGE_READWRITE, commitAll ? SurfaceBytes : registeredBytes);
#else
void* mapped = mmap(nullptr, SurfaceBytes + LayerBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
void* mapped = mmap(nullptr, SurfaceBytes + LayerBytes, commitAll ? PROT_READ | PROT_WRITE : PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (mapped != MAP_FAILED) {
const auto begin = reinterpret_cast<std::uintptr_t>(mapped);
const auto aligned = (begin + LayerBytes - 1) & ~(static_cast<std::uintptr_t>(LayerBytes) - 1);
if (aligned != begin) munmap(mapped, aligned - begin);
if (aligned + SurfaceBytes != begin + SurfaceBytes + LayerBytes) munmap(reinterpret_cast<void*>(aligned + SurfaceBytes), begin + LayerBytes - aligned);
block = reinterpret_cast<std::uint8_t*>(aligned);
if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, SurfaceBytes);
if (!commitAll) Require(mprotect(block + registeredOffset, registeredBytes, PROT_READ | PROT_WRITE) == 0, "cannot commit the active array slice");
if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block + (commitAll ? 0 : registeredOffset), commitAll ? SurfaceBytes : registeredBytes);
}
#endif
Require(block != nullptr, "array layer target: cannot allocate the guest block");
if (!commitAll) GuestAllocations::GuestAllocationsInvalidate_nid_postfix(reinterpret_cast<std::uintptr_t>(block), SurfaceBytes);
GuestAllocations::Mutation().Add(block + registeredOffset, registeredBytes, true, true, true);
}
@@ -170,10 +174,10 @@ void Load(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code,
Dispatch(device, code, userData);
}
void LoadLayer(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t layer, std::uint32_t swizzleMode) {
void LoadLayer(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t layer, std::uint32_t swizzleMode, std::uint32_t lastLayer = Layers - 1u) {
auto& code = LoadCodes[layer];
code = {0x7e020280, 0x7e040280u | layer, 0xf0001128, 0x00010300, 0x34080082, 0xbf8c3f70, 0xe0701000, 0x80000304, 0xbf810000};
Load(device, code, SurfaceDescriptor(surface, swizzleMode, Type2DArray, Layers - 1u));
Load(device, code, SurfaceDescriptor(surface, swizzleMode, Type2DArray, lastLayer));
}
void Sample2D(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t swizzleMode) {
@@ -187,23 +191,23 @@ void RequireOutput(std::uint32_t inside, std::uint32_t outside, const std::strin
}
}
AgcDriver::Graphics::ColorTarget DecodeTarget(std::uint64_t address, std::uint32_t slice, std::uint32_t swizzleMode, bool collapsed) {
AgcDriver::Graphics::ColorTarget DecodeTarget(std::uint64_t address, std::uint32_t slice, std::uint32_t swizzleMode, bool collapsed, std::uint32_t lastSlice) {
AgcDriver::Registers cx;
cx[0x318] = static_cast<std::uint32_t>(address >> 8u);
cx[0x31b] = slice | (slice << 13u);
cx[0x31c] = Color32FloatInfo;
cx[0x31d] = 0;
cx[0x3b0] = ((Side - 1u) << 14u) | (Side - 1u);
cx[0x3b8] = (TiledAttrib3 & ~(0x1fu << 14u)) | (swizzleMode << 14u) | (collapsed ? 0u : Layers - 1u);
cx[0x3b8] = (TiledAttrib3 & ~(0x1fu << 14u)) | (swizzleMode << 14u) | (collapsed ? 0u : lastSlice);
cx[0x390] = static_cast<std::uint32_t>(address >> 40u);
const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0);
Require(color.format == VK_FORMAT_R32_SFLOAT && color.bytes == LayerBytes && color.address == address + slice * LayerBytes, "the COLOR_32 float view did not decode as a 128x128 R32 slice of the array");
if (collapsed) Require(color.arrayAddress == 0 && color.arrayLastSlice == 0, "a collapsed slice view unexpectedly describes a whole array");
else Require(color.arrayAddress == address && color.arrayLastSlice == Layers - 1u && color.arraySlice == slice, "the color view did not keep its array and slice");
else Require(color.arrayAddress == address && color.arrayLastSlice == lastSlice && color.arraySlice == slice, "the color view did not keep its array and slice");
return color;
}
void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t slice, std::uint32_t swizzleMode, bool collapsed) {
void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t slice, std::uint32_t swizzleMode, bool collapsed, std::uint32_t lastSlice = Layers - 1u, std::uint32_t width = ScissorWidth) {
const auto target = device.Target();
constexpr std::uint32_t waveSize = 64;
std::size_t at = 0;
@@ -230,7 +234,7 @@ void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint
const auto vertexResult = ShaderRecompiler::Recompile(vertex);
const auto vertexPush = static_cast<std::uint32_t>(vertexResult.pushConstants.size());
const auto color = DecodeTarget(surface, slice, swizzleMode, collapsed);
const auto color = DecodeTarget(surface, slice, swizzleMode, collapsed, lastSlice);
VkPipelineColorBlendAttachmentState blend{};
blend.colorWriteMask = 0xf;
AgcDriver::Graphics::State state{};
@@ -242,7 +246,7 @@ void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
state.viewport = {0, static_cast<float>(Side), static_cast<float>(Side), -static_cast<float>(Side), 0, 1};
state.negativeOneToOne = false;
state.scissor = {{0, 0}, {ScissorWidth, Side}};
state.scissor = {{0, 0}, {width, Side}};
state.cullMode = VK_CULL_MODE_NONE;
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
state.blends = {blend};
@@ -296,6 +300,9 @@ void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t sw
RequireOutput(drawn, Initial, "slice " + std::to_string(slice) + " uploaded again from guest memory");
AgcDriver::Graphics::ClearCachedTextures(device.Device());
SampleSliceZero(device, texels, swizzleMode);
OverlappingArrays(device, texels, swizzleMode);
OverlappingArrays(device, texels, swizzleMode, true);
OverlappingArrayLastSlice(device, texels, swizzleMode);
}
void SampleSliceZero(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode) {
@@ -315,6 +322,32 @@ void SampleSliceZero(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std:
AgcDriver::Graphics::ClearCachedTextures(device.Device());
}
void OverlappingArrayLastSlice(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode) {
const auto neighbourAddress = AddressOf(texels);
const auto surface = neighbourAddress + LayerBytes;
const auto cpuValue = std::bit_cast<std::uint32_t>(0.125f);
const auto drawn = std::bit_cast<std::uint32_t>(Drawn);
std::fill_n(reinterpret_cast<std::uint32_t*>(texels), SurfaceBytes / sizeof(std::uint32_t), Initial);
AgcDriver::GuestMemory::CollectWrites(neighbourAddress, SurfaceBytes);
AgcDriver::GuestMemory::BumpCollectEpoch();
DrawSlice(device, surface, 1u, swizzleMode, false, 1u);
DrawSlice(device, neighbourAddress, 1u, swizzleMode, false, 1u, Side);
const auto neighbour = StorageTexture::FindPending(neighbourAddress, 2u * LayerBytes);
Require(neighbour != nullptr && neighbour->Descriptor().baseAddress == neighbourAddress, "the preceding target has no pending results");
std::fill_n(reinterpret_cast<std::uint32_t*>(texels + 2u * LayerBytes), LayerBytes / sizeof(std::uint32_t), cpuValue);
AgcDriver::GuestMemory::CollectWrites(surface + LayerBytes, LayerBytes);
AgcDriver::GuestMemory::BumpCollectEpoch();
DrawSlice(device, surface, 1u, swizzleMode, false, 1u);
Require(StorageTexture::FindPending(neighbourAddress, 2u * LayerBytes) == neighbour, "CPU refresh of the last slice stored the preceding target's results");
LoadLayer(device, surface, 1u, swizzleMode, 1u);
RequireOutput(drawn, cpuValue, "the refreshed last slice keeps the CPU's untouched pixels");
LoadLayer(device, surface, 0u, swizzleMode, 1u);
RequireOutput(drawn, drawn, "the first slice sees the preceding target's pending results");
StorageTexture::FlushPending(neighbourAddress, SurfaceBytes, nullptr, "test");
device.WaitIdle();
AgcDriver::Graphics::ClearCachedTextures(device.Device());
}
void UnregisteredUnusedLayer(AgcDriver::VulkanDevice& device, std::uint32_t slice) {
GuestBlock block(LayerBytes, slice * LayerBytes);
auto* texels = block.Data();
@@ -354,6 +387,75 @@ void RetiredArrayLayer(AgcDriver::VulkanDevice& device) {
Require(untouched, "CPU write-back stored results over a retired allocation");
}
void UncommittedUnusedLayers(AgcDriver::VulkanDevice& device, std::uint32_t slice) {
if (!AgcDriver::GuestMemory::WriteWatched()) return;
GuestBlock block(LayerBytes, slice * LayerBytes, false);
const auto surface = AddressOf(block.Data());
const auto active = surface + slice * LayerBytes;
auto* texels = reinterpret_cast<std::uint32_t*>(active);
std::fill_n(texels, LayerBytes / sizeof(std::uint32_t), Initial);
Require(AgcDriver::GuestMemory::CollectWrites(active, LayerBytes) != 0, "the committed array slice has no write history");
Require(AgcDriver::GuestMemory::CollectWritesUncached(surface, SurfaceBytes) == 0, "the uncommitted array fixture unexpectedly has complete backend write history");
Require(AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes) == 0, "the unused array slices unexpectedly have complete write history");
AgcDriver::GuestMemory::BumpCollectEpoch();
DrawSlice(device, surface, slice, TileR64KBX, false);
const auto pending = StorageTexture::FindPending(surface, SurfaceBytes);
Require(pending != nullptr, "the committed array slice has no pending draw results");
Require(pending->GenerationFor(active, LayerBytes) != 0, "an uncommitted unused slice erased the active slice's write history");
const auto version = pending->Version();
for (unsigned repeat = 0; repeat < 3; ++repeat) {
AgcDriver::GuestMemory::BumpCollectEpoch();
Require(pending->RefreshLayers(slice, 1), "an unchanged committed slice was uploaded again");
Require(pending->GenerationFor(active, LayerBytes) != 0 && pending->Version() == version, "repeated refresh erased the committed slice's write history");
Require(StorageTexture::FindPending(surface, SurfaceBytes) == pending, "refresh settled unchanged GPU results for an uncommitted unused slice");
}
const auto cpu = std::bit_cast<std::uint32_t>(0.125f);
std::fill_n(texels, LayerBytes / sizeof(std::uint32_t), cpu);
AgcDriver::GuestMemory::BumpCollectEpoch();
Require(!pending->RefreshLayers(slice, 1), "the active array slice ignored a genuine CPU write");
LoadLayer(device, surface, slice, TileR64KBX);
RequireOutput(cpu, cpu, "a committed array slice after a genuine CPU write");
DrawSlice(device, surface, slice, TileR64KBX, false);
StorageTexture::FlushPending(active, LayerBytes, nullptr, "test");
device.WaitIdle();
Require(std::count(texels, texels + LayerBytes / sizeof(std::uint32_t), std::bit_cast<std::uint32_t>(Drawn)) == ScissorWidth * Side, "write-back lost results in the committed array slice");
AgcDriver::Graphics::ClearCachedTextures(device.Device());
}
void OverlappingArrays(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode, bool cpuWrite) {
const auto surface = AddressOf(texels);
const auto next = surface + LayerBytes;
const auto drawn = std::bit_cast<std::uint32_t>(Drawn);
std::fill_n(reinterpret_cast<std::uint32_t*>(texels), SurfaceBytes / 4u, Initial);
AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes);
AgcDriver::GuestMemory::BumpCollectEpoch();
DrawSlice(device, surface, 0, swizzleMode, false, 1u);
DrawSlice(device, next, 0, swizzleMode, false, 1u, Side);
const auto neighbour = StorageTexture::FindPending(next, 2u * LayerBytes);
Require(neighbour != nullptr && neighbour->Descriptor().baseAddress == next && neighbour->Descriptor().dimension == TextureDimension::k2DArray, "the next target did not render into the image of its own array");
if (cpuWrite) {
std::fill_n(reinterpret_cast<std::uint32_t*>(texels), LayerBytes / sizeof(std::uint32_t), std::bit_cast<std::uint32_t>(0.125f));
AgcDriver::GuestMemory::CollectWrites(surface, LayerBytes);
AgcDriver::GuestMemory::BumpCollectEpoch();
}
DrawSlice(device, surface, 0, swizzleMode, false, 1u);
Require(StorageTexture::FindPending(next, 2u * LayerBytes) == neighbour, "a draw into slice 0 of the first array stored the next target's results, which only its unused second slice overlaps");
Sample2D(device, surface, swizzleMode);
RequireOutput(drawn, cpuWrite ? std::bit_cast<std::uint32_t>(0.125f) : Initial, "slice 0 of the first array sampled as a 2D texture after both draws");
Require(StorageTexture::FindPending(next, 2u * LayerBytes) == neighbour, "a 2D texture over slice 0 of the first array stored the next target's results, which only its unused second slice overlaps");
LoadLayer(device, next, 0, swizzleMode, 1u);
RequireOutput(drawn, drawn, "slice 0 of the next array after the draw into the first");
LoadLayer(device, surface, 1, swizzleMode, 1u);
RequireOutput(drawn, drawn, "slice 1 of the first array (the next target's slice 0) read while the next target's results were pending");
LoadLayer(device, surface, 0, swizzleMode, 1u);
RequireOutput(drawn, cpuWrite ? std::bit_cast<std::uint32_t>(0.125f) : Initial, "slice 0 of the first array after both draws");
LoadLayer(device, next, 1, swizzleMode, 1u);
RequireOutput(Initial, Initial, "slice 1 of the next array, which nothing drew");
StorageTexture::FlushPending(surface, SurfaceBytes, nullptr, "test");
device.WaitIdle();
AgcDriver::Graphics::ClearCachedTextures(device.Device());
}
}
int main() {
@@ -367,6 +469,8 @@ int main() {
UnregisteredUnusedLayer(*device, 0u);
UnregisteredUnusedLayer(*device, Layers - 1u);
RetiredArrayLayer(*device);
UncommittedUnusedLayers(*device, 0u);
UncommittedUnusedLayers(*device, Layers - 1u);
std::puts("array layer target tests passed");
return 0;
} catch (const std::exception& error) {
@@ -0,0 +1,234 @@
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp"
#include "prx/libc/include/GuestAllocations.hpp"
#include "prx/libc/include/GuestArena.hpp"
#include "prx/libc/include/GuestWriteWatch.hpp"
#include "DepthFastClearHarness.hpp"
#include "VulkanTestDevice.hpp"
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#else
#include <sys/mman.h>
#endif
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <iostream>
#include <span>
#include <string>
#include <utility>
#include <vector>
namespace AgcDriver {
struct VulkanDeviceTestAccess {
static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); }
};
}
namespace {
using AgcDriver::Graphics::DepthPlaneStoreStatistics;
using DepthFastClearHarness::Compile;
using DepthFastClearHarness::Draw;
using DepthFastClearHarness::DrawOptions;
using DepthFastClearHarness::Require;
using DepthFastClearHarness::Surface;
constexpr std::uint32_t PlaneWidth = 384;
constexpr std::uint32_t PlaneHeight = 200;
constexpr VkExtent2D PlaneExtent{PlaneWidth, PlaneHeight};
constexpr std::size_t Block = 65536;
constexpr std::size_t Units = 6;
constexpr std::size_t PlaneBytes = Units * Block;
constexpr std::size_t UnitTexels = Block / sizeof(float);
constexpr std::size_t EdgeTexels = 128u * (PlaneHeight - 128u);
constexpr std::size_t PlaneTexels = static_cast<std::size_t>(PlaneWidth) * PlaneHeight;
constexpr VkExtent2D SmallExtent{64, 32};
constexpr std::size_t SmallColorBytes = static_cast<std::size_t>(SmallExtent.width) * SmallExtent.height * 4u;
constexpr std::uint64_t ReadBefore = 4096;
constexpr std::size_t ReadBytes = 7u * 1024u;
alignas(256) std::array<std::byte, PlaneTexels * 4u> Colors{};
void* AllocateBlocks(std::size_t bytes, bool watched) {
#ifdef _WIN32
void* block = nullptr;
if (watched) {
block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block);
if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes);
} else {
block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
}
Require(block != nullptr, "cannot allocate the depth plane blocks");
#else
void* raw = mmap(nullptr, bytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
Require(raw != MAP_FAILED, "cannot map the depth plane blocks");
const auto begin = reinterpret_cast<std::uintptr_t>(raw);
const auto aligned = (begin + Block - 1) & ~static_cast<std::uintptr_t>(Block - 1);
if (aligned != begin) munmap(raw, aligned - begin);
if (aligned + bytes != begin + bytes + Block) munmap(reinterpret_cast<void*>(aligned + bytes), begin + Block - aligned);
void* block = reinterpret_cast<void*>(aligned);
if (watched) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes);
#endif
GuestAllocations::Mutation().Add(block, bytes, true, true, true);
return block;
}
void Fill(float* plane, float value) {
std::fill(plane, plane + PlaneBytes / sizeof(float), value);
AgcDriver::GuestMemory::BumpCollectEpoch();
}
void FillUnit(float* plane, std::size_t unit, float value) {
std::fill(plane + unit * UnitTexels, plane + (unit + 1u) * UnitTexels, value);
AgcDriver::GuestMemory::BumpCollectEpoch();
}
std::size_t Texels(std::size_t unit) {
return unit < 3u ? UnitTexels : EdgeTexels;
}
void RequireUnit(const float* plane, std::size_t unit, float depth, std::size_t count, const std::string& what) {
const auto* first = plane + unit * UnitTexels;
const auto held = static_cast<std::size_t>(std::count(first, first + UnitTexels, depth));
Require(held == count, what + ": unit " + std::to_string(unit) + " holds " + std::to_string(held) + " texels of " + std::to_string(depth) + " instead of " + std::to_string(count));
}
DepthPlaneStoreStatistics Since(const DepthPlaneStoreStatistics& before) {
const auto now = AgcDriver::Graphics::DepthPlaneStoreCounts();
DepthPlaneStoreStatistics difference;
difference.stores = now.stores - before.stores;
difference.units = now.units - before.units;
difference.bytes = now.bytes - before.bytes;
return difference;
}
std::vector<std::pair<std::uint64_t, std::uint64_t>> ReadStart(AgcDriver::VulkanDevice& device, std::uint64_t base) {
auto stored = AgcDriver::Graphics::FlushDepthPlanes(base - ReadBefore, ReadBytes);
device.WaitIdle();
return stored;
}
}
int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
Require(AgcDriver::Graphics::DepthSliceBytes(PlaneExtent, 4u) == PlaneBytes, "the 384x200 depth plane is not six 64 KiB tiles");
auto* blocks = static_cast<std::byte*>(AllocateBlocks(Block + PlaneBytes, true));
auto* plane = reinterpret_cast<float*>(blocks + Block);
const auto base = reinterpret_cast<std::uintptr_t>(plane);
const auto shaders = Compile(*device);
const auto swizzle = AgcDriver::Graphics::XorSwizzleMode(AgcDriver::Graphics::TextureTileMode::kZ64KBX);
const Surface depth{base, 0, 0, false, VK_FORMAT_D32_SFLOAT, 0, 0, swizzle};
const auto draw = [&](const Surface& surface, const DrawOptions& options) { return Draw(*device, shaders, surface, options, PlaneExtent, Colors); };
const DrawOptions writeHalf{.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS};
Fill(plane, 1.0f);
Require(draw(depth, writeHalf) == PlaneTexels, "the draw writing depth 0.5 did not cover the plane");
Require(AgcDriver::Graphics::DepthPlanesStale(), "the draw writing depth left the plane unregistered");
auto before = AgcDriver::Graphics::DepthPlaneStoreCounts();
const auto first = ReadStart(*device, base);
auto stored = Since(before);
Require(first.size() == 1 && first[0].first == base && first[0].second == Block, "a read ending 3 KiB into the stale plane did not store exactly its first 64 KiB unit");
Require(stored.stores == 1 && stored.units == 1 && stored.bytes == Block, "a read ending 3 KiB into the stale plane did not count one store of one unit");
RequireUnit(plane, 0, 0.5f, UnitTexels, "after a read of the first unit");
for (std::size_t unit = 1; unit < Units; ++unit) RequireUnit(plane, unit, 1.0f, UnitTexels, "after a read of the first unit, a unit it does not overlap");
Require(AgcDriver::Graphics::DepthPlanesStale(), "a store of one unit left no stale plane");
before = AgcDriver::Graphics::DepthPlaneStoreCounts();
Require(ReadStart(*device, base).empty(), "the same read again stored the first unit again");
stored = Since(before);
Require(stored.stores == 0 && stored.units == 0 && stored.bytes == 0, "the same read again counted a store");
before = AgcDriver::Graphics::DepthPlaneStoreCounts();
const auto rest = AgcDriver::Graphics::FlushDepthPlanes(base, PlaneBytes);
device->WaitIdle();
stored = Since(before);
Require(rest.size() == 1 && rest[0].first == base + Block && rest[0].second == PlaneBytes - Block, "a read of the whole plane did not store the five units still stale");
Require(stored.stores == 1 && stored.units == Units - 1u && stored.bytes == PlaneBytes - Block, "a read of the whole plane did not count one store of five units");
for (std::size_t unit = 0; unit < Units; ++unit) RequireUnit(plane, unit, 0.5f, Texels(unit), "after a read of the whole plane");
for (std::size_t unit = 3; unit < Units; ++unit) RequireUnit(plane, unit, 1.0f, UnitTexels - EdgeTexels, "after a read of the whole plane, the padding below the last row");
Require(!AgcDriver::Graphics::DepthPlanesStale(), "a read of the whole plane left it stale");
if (!AgcDriver::GuestMemory::WriteWatched() || !AgcDriver::GuestMemory::Watched(base, PlaneBytes)) {
std::puts("guest memory is not write-watched here: guest writes into a partly stored plane are not exercised");
} else {
Fill(plane, 1.0f);
Require(draw(depth, writeHalf) == PlaneTexels, "the draw writing depth 0.5 over the reloaded plane did not cover it");
Require(!ReadStart(*device, base).empty(), "a read of the first unit of the drawn plane stored nothing");
FillUnit(plane, 4, 0.75f);
draw(depth, {.depthTest = false});
before = AgcDriver::Graphics::DepthPlaneStoreCounts();
Require(!AgcDriver::Graphics::FlushDepthPlanes(base, PlaneBytes).empty(), "a read of the whole plane after a guest write into unit 4 stored nothing");
device->WaitIdle();
stored = Since(before);
Require(stored.units == 4 && stored.bytes == 4u * Block, "a read of the whole plane after a guest write into unit 4 did not store the four units still stale");
for (const std::size_t unit : {0u, 1u, 2u, 3u, 5u}) RequireUnit(plane, unit, 0.5f, Texels(unit), "a guest write into unit 4 after unit 0 was stored, then a read of the whole plane");
RequireUnit(plane, 4, 0.75f, UnitTexels, "a guest write into unit 4 after unit 0 was stored, then a read of the whole plane, the written unit");
Require(draw(depth, {.minDepth = 0.6f, .maxDepth = 0.6f}) == EdgeTexels, "a draw at depth 0.6 did not pass LESS exactly over the unit the guest wrote 0.75 into");
Fill(plane, 1.0f);
Require(draw(depth, writeHalf) == PlaneTexels, "the draw writing depth 0.5 before a guest write and an unmatched view did not cover the plane");
Require(!ReadStart(*device, base).empty(), "a read of the first unit before a guest write and an unmatched view stored nothing");
FillUnit(plane, 2, 0.75f);
const std::array<std::uint32_t, 8> words{0, 0, 0, 0, PlaneWidth, PlaneHeight, 1, 0};
AgcDriver::Graphics::GuestTextureResource levels{};
levels.baseAddress = base;
levels.width = PlaneWidth;
levels.height = PlaneHeight;
levels.mipCount = 2;
levels.lastLevel = 1;
levels.dimension = AgcDriver::Graphics::TextureDimension::k2D;
levels.tileMode = AgcDriver::Graphics::TextureTileMode::kZ64KBX;
levels.format = 22;
Require(AgcDriver::Graphics::DepthSurfaceTexture(AgcDriver::VulkanDeviceTestAccess::ContextOf(*device), words, levels, {}) == nullptr, "a view with two levels of a plane the guest wrote was served by the depth surface");
device->WaitIdle();
Require(!AgcDriver::Graphics::DepthSurfaceAt(base), "a view with two levels of a plane the guest wrote left the depth surface live");
for (const std::size_t unit : {0u, 1u, 3u, 4u, 5u}) RequireUnit(plane, unit, 0.5f, Texels(unit), "the retire of a partly stored plane the guest wrote into");
RequireUnit(plane, 2, 0.75f, UnitTexels, "the retire of a partly stored plane the guest wrote into, the written unit");
}
Fill(plane, 1.0f);
Require(draw(depth, writeHalf) == PlaneTexels, "the draw writing depth 0.5 under a later color target did not cover the plane");
Require(!ReadStart(*device, base).empty(), "a read of the first unit before a color target stored nothing");
auto* smallPlane = AllocateBlocks(Block, true);
const Surface smallDepth{reinterpret_cast<std::uintptr_t>(smallPlane), 0, 0, false, VK_FORMAT_D32_SFLOAT, 0, 0, swizzle};
const std::span<std::byte> overLastUnit(reinterpret_cast<std::byte*>(plane + 5u * UnitTexels), SmallColorBytes);
Draw(*device, shaders, smallDepth, {.depthTest = false}, SmallExtent, overLastUnit);
Require(!AgcDriver::Graphics::DepthSurfaceAt(base), "a color target over the last unit left the depth surface live");
for (std::size_t unit = 0; unit < 5u; ++unit) RequireUnit(plane, unit, 0.5f, Texels(unit), "a color target over the last unit of a partly stored plane");
auto* looseBlocks = static_cast<std::byte*>(AllocateBlocks(Block + PlaneBytes, false));
auto* loose = reinterpret_cast<float*>(looseBlocks + Block);
const auto looseBase = reinterpret_cast<std::uintptr_t>(loose);
if (AgcDriver::GuestMemory::Watched(looseBase, PlaneBytes)) {
std::puts("memory outside the guest arena is write-watched here: a partial store over unwatched memory is not exercised");
} else {
const Surface unwatched{looseBase, 0, 0, false, VK_FORMAT_D32_SFLOAT, 0, 0, swizzle};
Fill(loose, 1.0f);
Require(draw(unwatched, writeHalf) == PlaneTexels, "the draw writing depth 0.5 into an unwatched plane did not cover it");
before = AgcDriver::Graphics::DepthPlaneStoreCounts();
Require(ReadStart(*device, looseBase).size() == 1, "a read of the first unit of an unwatched plane did not store it");
stored = Since(before);
Require(stored.units == 1 && stored.bytes == Block, "a read of the first unit of an unwatched plane did not count one unit");
RequireUnit(loose, 0, 0.5f, UnitTexels, "unwatched plane after a read of the first unit");
for (std::size_t unit = 1; unit < Units; ++unit) RequireUnit(loose, unit, 1.0f, UnitTexels, "unwatched plane after a read of the first unit, a unit it does not overlap");
Require(draw(unwatched, {.minDepth = 0.6f, .maxDepth = 0.6f}) == 0u, "a draw at depth 0.6 passed LESS over unwatched units still holding depth 0.5 on the GPU");
}
std::puts("depth plane partial store tests passed");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
@@ -1,6 +1,9 @@
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp"
@@ -29,6 +32,7 @@ using namespace AgcDriver::Graphics;
constexpr std::uint32_t Side = 64;
constexpr std::size_t BlockBytes = 65536;
alignas(4096) std::array<std::uint8_t, BlockBytes * 2> Storage{};
alignas(4096) std::array<std::uint8_t, BlockBytes * 2> CopyStorage{};
constexpr std::uint32_t Format32Float = 22;
constexpr std::uint32_t Format8Uint = 5;
@@ -301,6 +305,69 @@ void RunPairings(Context context, std::uint8_t* block) {
ExpectFailure([&] { static_cast<void>(DepthSurfaceStoreBack(context, *depth)); }, "through a view that another view of the plane", "a store back through a replaced view was taken by an older pairing that covers the plane");
}
void DepthCopyCase(Context context, std::uint8_t* block) {
const auto colorAddress = reinterpret_cast<std::uint64_t>(block);
const auto depthAddress = colorAddress + BlockBytes / 4;
Recorder recorder(context);
recorder.Activate();
const auto submit = [&] {
recorder.Submit();
Check(context.Function<PFN_vkQueueWaitIdle>("vkQueueWaitIdle")(context.queue), "vkQueueWaitIdle");
};
ColorTarget color{};
color.address = colorAddress;
color.surfaceAddress = colorAddress;
color.extent = {Side, Side};
color.surfaceExtent = color.extent;
color.format = VK_FORMAT_R32_SFLOAT;
color.elementBytes = 4;
color.tileMode = ColorTileMode::RenderTarget;
color.bytes = ColorTargetLayout(Side, Side, ColorTileMode::RenderTarget, 4).Bytes();
const auto copy = [&](std::uint32_t samples, std::uint32_t sample, VkExtent2D extent) {
RunDepthCopyPass(context, DepthCopyPass{depthAddress, extent, samples, sample, color});
submit();
const auto resident = CachedStorageSurface(context, SurfaceForTarget(color));
resident->MarkDirty();
resident->WriteBack();
submit();
};
const auto countBlock = [&](float value) {
std::size_t count = 0;
for (std::size_t offset = 0; offset < BlockBytes; offset += sizeof(float)) {
float texel = 0;
std::memcpy(&texel, block + offset, sizeof(texel));
if (texel == value) ++count;
}
return count;
};
std::fill(block, block + BlockBytes, std::uint8_t{0});
ExpectFailure([&] { copy(1, 0, {Side, Side}); }, "no draw rendered", "a DB depth copy from memory without a depth surface was accepted");
const auto sampledCounts = context.limits.sampledImageDepthSampleCounts & context.limits.framebufferDepthSampleCounts;
const std::uint32_t samples = (sampledCounts & VK_SAMPLE_COUNT_2_BIT) != 0 ? 2u : (sampledCounts & VK_SAMPLE_COUNT_4_BIT) != 0 ? 4u : 0u;
if (samples != 0) {
Require(DepthSurfaceView(context, DepthTarget{depthAddress, 0, {Side, Side}, VK_FORMAT_D32_SFLOAT, 0.375f, 0, 0, false, samples}) != VK_NULL_HANDLE, "the multisample depth surface was not created");
for (std::uint32_t sample = 0; sample < samples; ++sample) {
std::fill(block, block + BlockBytes, std::uint8_t{0});
copy(samples, sample, {Side, Side});
Require(countBlock(0.375f) == Side * Side, "the DB depth copy of sample " + std::to_string(sample) + " of " + std::to_string(samples) + " did not store the cleared depth in every texel of the R32 float target");
}
ExpectFailure([&] { copy(samples, samples, {Side, Side}); }, "does not have", "a DB depth copy of a sample past the last of a multisample surface was accepted");
ExpectFailure([&] { copy(samples, 0, {Side / 2, Side / 2}); }, "of another size", "a DB depth copy of another extent than the depth surface was accepted");
ClearDepthSurfaces(context.device);
}
if ((context.limits.sampledImageDepthSampleCounts & VK_SAMPLE_COUNT_2_BIT) == 0) {
Require(DepthSurfaceView(context, DepthTarget{depthAddress, 0, {Side, Side}, VK_FORMAT_D32_SFLOAT, 0.375f, 0, 0, false, 2}) != VK_NULL_HANDLE, "the unsampleable two-sample depth surface was not created");
ExpectFailure([&] { copy(2, 0, {Side, Side}); }, "cannot sample at its sample count", "a DB depth copy from an unsampleable two-sample depth surface was accepted");
ClearDepthSurfaces(context.device);
std::puts("DB depth copy: unsupported two-sample source refused cleanly");
}
Require(DepthSurfaceView(context, DepthTarget{depthAddress, 0, {Side, Side}, VK_FORMAT_D32_SFLOAT, 0.625f, 0, 0, false, 1}) != VK_NULL_HANDLE, "the one-sample depth surface was not created");
std::fill(block, block + BlockBytes, std::uint8_t{0});
copy(1, 0, {Side, Side});
Require(countBlock(0.625f) == Side * Side, "the DB depth copy of a one-sample depth surface did not store the cleared depth in every texel of the R32 float target");
ExpectFailure([&] { copy(1, 1, {Side, Side}); }, "does not have", "a DB depth copy of a second sample of a one-sample surface was accepted");
}
}
int main() {
@@ -321,6 +388,11 @@ int main() {
{
std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex());
Run(context, block);
auto* copyBlock = CopyStorage.data() + (BlockBytes - reinterpret_cast<std::uintptr_t>(CopyStorage.data()) % BlockBytes) % BlockBytes;
GuestAllocations::Mutation().Add(copyBlock, BlockBytes, true, true, true);
DepthCopyCase(context, copyBlock);
ClearCachedTextures(context.device);
GuestAllocations::Mutation().Remove(copyBlock);
ClearDepthSurfaces(context.device);
RunPairings(context, block);
ClearDepthSurfaces(context.device);
@@ -10,6 +10,7 @@
#include <initializer_list>
#include <iostream>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
@@ -31,7 +32,7 @@ void Append(std::vector<std::uint32_t>& commands, std::uint32_t opcode, std::ini
commands.insert(commands.end(), payload);
}
void Draw(const std::array<std::uint32_t, 4>& descriptor) {
void Draw(const std::array<std::uint32_t, 4>& descriptor, std::uint32_t* label = nullptr) {
const auto vertex = reinterpret_cast<std::uintptr_t>(VertexCode.data());
const auto pixel = reinterpret_cast<std::uintptr_t>(PixelCode.data());
const std::vector<std::pair<std::uint32_t, std::uint32_t>> context{
@@ -68,6 +69,10 @@ void Draw(const std::array<std::uint32_t, 4>& descriptor) {
Append(commands, 0x69, {0x318, static_cast<std::uint32_t>(color >> 8u)});
Append(commands, 0x69, {0x390, static_cast<std::uint32_t>(color >> 40u)});
Append(commands, 0x2d, {3, 2});
if (label != nullptr) {
const auto address = reinterpret_cast<std::uintptr_t>(label);
Append(commands, 0x37, {0x00100200u, static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u), 7u});
}
Packet packet{commands.data(), static_cast<std::uint32_t>(commands.size()), 0, {}};
Require(sceAgcDriverSubmitDcb(&packet) == 0, "unsupported buffer type draw submission failed");
AgcDriverWaitIdle_nid_postfix();
@@ -92,15 +97,29 @@ void NamesShaderAndDescriptor() {
throw std::runtime_error("a pixel shader read a buffer through a descriptor of type 2");
}
void SkipsRefusedDraw() {
const auto address = reinterpret_cast<std::uintptr_t>(Source.data());
const std::array<std::uint32_t, 4> descriptor{static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), sizeof(Source), 0xa1b00facu};
alignas(8) static std::uint32_t label = 0;
for (int submission = 0; submission < 2; ++submission) {
label = 0;
Draw(descriptor, &label);
Require(label == 7u, "the packet after a skipped refused draw was not executed");
}
for (const auto byte : Pixels) Require(byte == std::byte{0}, "a skipped refused draw wrote its color target");
}
int main() {
}
int main(int argc, char** argv) {
try {
Require(argc == 1 || (argc == 2 && std::string_view(argv[1]) == "--skip-refused"), "invalid test arguments");
{
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
}
NamesShaderAndDescriptor();
if (argc == 2) SkipsRefusedDraw();
else NamesShaderAndDescriptor();
try { AgcDriverShutdown_nid_postfix(); } catch (const std::exception&) {}
std::puts("draw unsupported buffer type tests passed");
return 0;
@@ -27,6 +27,7 @@ constexpr std::uint32_t Side = 2;
constexpr std::uint32_t Format8888UNorm = 56;
constexpr std::uint32_t Format8UNorm = 1;
constexpr std::uint32_t Format8_8UNorm = 14;
constexpr std::uint32_t Format16UNorm = 7;
constexpr std::uint32_t Format1616UNorm = 23;
constexpr std::uint32_t Format9995Float = 132;
constexpr std::uint32_t Format8Srgb = 128;
@@ -178,45 +179,6 @@ void FillInput() {
Extra[0] = 0x00001f20u;
}
std::int32_t OffsetComponent(std::uint32_t tid, std::uint32_t component) {
const auto field = (Extra[tid * 3u] >> (component * 8u)) & 0x3fu;
return static_cast<std::int32_t>(field ^ 0x20u) - 0x20;
}
float ReferenceTexel(int x, int y, float reference, const Sampler& sampler) {
const std::uint32_t clamp = sampler.clamp;
const int size = static_cast<int>(Side);
if (clamp == ClampBorder && (x < 0 || x >= size || y < 0 || y >= size)) {
const float border = sampler.border == BorderWhite ? 1.0f : 0.0f;
return reference <= border ? 1.0f : 0.0f;
}
const auto address = [&](int value) {
if (clamp == ClampEdge) return std::clamp(value, 0, static_cast<int>(Side) - 1);
const int size = static_cast<int>(Side);
return ((value % size) + size) % size;
};
const float red = static_cast<float>(Red[address(y) * Side + address(x)]) / 255.0f;
return reference <= red ? 1.0f : 0.0f;
}
float Expected(std::uint32_t tid, const Sampler& sampler, bool offsets) {
const float u = Input[tid * 3u + 1u] * static_cast<float>(Side);
const float v = Input[tid * 3u + 2u] * static_cast<float>(Side);
const float reference = std::clamp(Input[tid * 3u + 0u], 0.0f, 1.0f);
const int offsetX = offsets ? OffsetComponent(tid, 0u) : 0;
const int offsetY = offsets ? OffsetComponent(tid, 1u) : 0;
if (sampler.filter == FilterPoint) return ReferenceTexel(static_cast<int>(std::floor(u)) + offsetX, static_cast<int>(std::floor(v)) + offsetY, reference, sampler);
const float cu = u - 0.5f;
const float cv = v - 0.5f;
const int x = static_cast<int>(std::floor(cu)) + offsetX;
const int y = static_cast<int>(std::floor(cv)) + offsetY;
const float a = cu - std::floor(cu);
const float b = cv - std::floor(cv);
const float top = ReferenceTexel(x, y, reference, sampler) * (1.0f - a) + ReferenceTexel(x + 1, y, reference, sampler) * a;
const float bottom = ReferenceTexel(x, y + 1, reference, sampler) * (1.0f - a) + ReferenceTexel(x + 1, y + 1, reference, sampler) * a;
return top * (1.0f - b) + bottom * b;
}
ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, std::span<const std::uint32_t> code = Code, bool useCache = false, bool nativeSampleOffsets = true, bool cube = false) {
std::vector<std::uint32_t> userData(24, 0u);
const auto input = cube ? BufferDescriptor(CubeInput.data(), static_cast<std::uint32_t>(sizeof(CubeInput))) : BufferDescriptor(Input.data(), static_cast<std::uint32_t>(sizeof(Input)));
@@ -254,9 +216,7 @@ std::uint64_t Run(AgcDriver::VulkanDevice& device, const Sampler& sampler, const
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
device.WaitIdle();
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
const float expected = Expected(tid, sampler, offsets);
const float tolerance = sampler.filter == FilterPoint ? 0.0f : 1e-4f;
Require(std::fabs(Output[tid] - expected) <= tolerance, std::string(name) + ": thread " + std::to_string(tid) + " compared to " + std::to_string(Output[tid]) + ", expected " + std::to_string(expected));
Require(Output[tid] == 0.0f, std::string(name) + ": thread " + std::to_string(tid) + " compared to " + std::to_string(Output[tid]) + ", expected 0");
}
return result.variantId;
}
@@ -274,12 +234,11 @@ void RunChannels(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t>
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
device.WaitIdle();
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
const float compared = Expected(tid, sampler, false);
for (std::uint32_t reg = 0; reg < 4u; ++reg) {
float expected = -1.0f;
if (reg < registers) {
const auto select = channels[firstChannel + reg];
expected = select == 0u ? 0.0f : select == 1u ? 1.0f : compared;
expected = select == 0u ? 0.0f : select == 1u ? 1.0f : 0.0f;
}
const float actual = Output[tid * 4u + reg];
Require(std::fabs(actual - expected) <= 1e-4f, std::string(name) + ": thread " + std::to_string(tid) + " register " + std::to_string(reg) + " returned " + std::to_string(actual) + ", expected " + std::to_string(expected));
@@ -304,7 +263,7 @@ void RunCube(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(CubeCode.data()));
device.WaitIdle();
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
Require(Output[tid] == 1.0f, std::string(name) + ": +X cube face comparison returned " + std::to_string(Output[tid]) + ", expected 1");
Require(Output[tid] == 0.0f, std::string(name) + ": +X cube face comparison returned " + std::to_string(Output[tid]) + ", expected 0");
}
}
@@ -314,7 +273,7 @@ bool BindsDepthCompare(const ShaderRecompiler::RecompileResult& result) {
});
}
void RunSrgbCubeZero(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, const char* name) {
void RunZeroCube(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, const char* name) {
FillCubeTexels();
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
CubeInput[tid * 4u + 0u] = 0.5f;
@@ -332,7 +291,7 @@ void RunSrgbCubeZero(AgcDriver::VulkanDevice& device, std::uint32_t format, cons
}
}
void RunSrgbZero(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, const char* name, bool useCache, std::uint64_t expectedVariant) {
void RunZero(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, const char* name, bool useCache, std::uint64_t expectedVariant) {
Output.fill(-1.0f);
const auto result = Compile(device, format, sampler, Code, useCache);
if (useCache) Require(result.cacheHit && result.variantId == expectedVariant, std::string(name) + " did not reuse the compiled comparison artifact");
@@ -366,25 +325,38 @@ int main() {
FillTexels();
FillInput();
RunCube(*device, {ClampEdge, FilterPoint}, "point cube, clamp to edge");
Reject(*device, Format1616UNorm, {ClampEdge, FilterBilinear}, "bilinear cube comparison requires DISABLE_CUBE_WRAP", CubeCode, true, true);
RunCube(*device, {ClampEdge, FilterBilinear}, "bilinear cube without DISABLE_CUBE_WRAP");
for (std::uint32_t function = 0; function < 8u; ++function) {
const auto name = "bilinear 8_8_8_8 srgb cube function " + std::to_string(function);
RunSrgbCubeZero(*device, Format8888Srgb, {ClampEdge, FilterBilinear, BorderBlack, 0u, 0u, false, function}, name.c_str());
RunZeroCube(*device, Format8888Srgb, {ClampEdge, FilterBilinear, BorderBlack, 0u, 0u, false, function}, name.c_str());
}
RunSrgbCubeZero(*device, Format8Srgb, {ClampEdge, FilterBilinear}, "bilinear 8 srgb cube without DISABLE_CUBE_WRAP");
RunSrgbCubeZero(*device, Format8_8Srgb, {ClampEdge, FilterBilinear}, "bilinear 8_8 srgb cube without DISABLE_CUBE_WRAP");
RunZeroCube(*device, Format8Srgb, {ClampEdge, FilterBilinear}, "bilinear 8 srgb cube without DISABLE_CUBE_WRAP");
RunZeroCube(*device, Format8_8Srgb, {ClampEdge, FilterBilinear}, "bilinear 8_8 srgb cube without DISABLE_CUBE_WRAP");
RunCube(*device, {ClampEdge, FilterBilinear, BorderBlack, 0u, 0u, true}, "bilinear cube with DISABLE_CUBE_WRAP");
Reject(*device, Format9995Float, {ClampEdge, FilterPoint}, "implemented only for float, unorm and snorm formats");
Reject(*device, Format9995Float, {ClampEdge, FilterPoint}, "implemented only for floating-point, UNORM and SNORM formats");
for (std::uint32_t function = 0; function < 8u; ++function) {
const auto name = "bilinear 8_8_8_8 unorm cube function " + std::to_string(function);
RunSrgbCubeZero(*device, Format8888UNorm, {ClampEdge, FilterBilinear, BorderBlack, 0u, 0u, false, function}, name.c_str());
RunZeroCube(*device, Format8888UNorm, {ClampEdge, FilterBilinear, BorderBlack, 0u, 0u, false, function}, name.c_str());
}
RunSrgbCubeZero(*device, Format8888UNorm, {ClampEdge, FilterAnisoBilinear, BorderBlack, 0u, 0u, false, GreaterEqual}, "anisotropic bilinear 8_8_8_8 unorm cube without DISABLE_CUBE_WRAP");
Reject(*device, Format1616UNorm, {ClampEdge, FilterAnisoBilinear}, "point or bilinear");
Reject(*device, Format1616UNorm, {ClampEdge, FilterAnisoPoint}, "point or bilinear");
RunZeroCube(*device, Format8888UNorm, {ClampEdge, FilterAnisoBilinear, BorderBlack, 0u, 0u, false, GreaterEqual}, "anisotropic bilinear 8_8_8_8 unorm cube without DISABLE_CUBE_WRAP");
RunZero(*device, Format1616UNorm, {ClampEdge, FilterAnisoBilinear}, "anisotropic bilinear 16_16 unorm", false, 0u);
RunZero(*device, Format1616UNorm, {ClampEdge, FilterAnisoPoint}, "anisotropic point 16_16 unorm", false, 0u);
FillTexels();
Require(BindsDepthCompare(Compile(*device, Format16UNorm, {ClampEdge, FilterPoint})), "an R16 unorm texture left the native comparison path");
Require(BindsDepthCompare(Compile(*device, Format32Float, {ClampEdge, FilterBilinear})), "an R32 float texture left the native comparison path");
Require(!BindsDepthCompare(Compile(*device, Format8888UNorm, {ClampEdge, FilterBilinear})), "a color texture kept a depth-compare binding");
for (const auto format : {1u, 2u, 8u, 13u, 14u, 15u, 23u, 24u, 29u, 36u, 50u, 51u, 56u, 57u, 65u, 66u, 71u, 77u}) {
if (format == 51u && !device->SupportsImageFormat(VK_FORMAT_A2B10G10R10_SNORM_PACK32, VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT)) {
std::puts("measured GFX10 color format 51 returns zero: skipped, Vulkan device does not support the image format");
continue;
}
const auto name = "measured GFX10 color format " + std::to_string(format) + " returns zero";
RunZero(*device, format, {ClampEdge, FilterPoint}, name.c_str(), false, 0u);
}
for (std::uint32_t function = 0; function < 8u; ++function) {
const auto name = "16_16 unorm compare function " + std::to_string(function) + " returns zero";
RunZero(*device, Format1616UNorm, {ClampEdge, FilterPoint, BorderBlack, 0u, 0u, false, function}, name.c_str(), false, 0u);
}
for (const bool useCache : {false, true}) {
const auto variant = useCache ? Run(*device, {ClampEdge, FilterPoint}, "cached UNORM before sRGB", false, true) : 0u;
if (useCache) Require(variant != 0, "the cached comparison has no compiled artifact identity");
@@ -392,7 +364,7 @@ int main() {
for (std::uint32_t function = 0; function < 8u; ++function) {
for (const auto filter : {FilterPoint, FilterBilinear}) {
const auto name = std::string(useCache ? "cached" : "uncached") + " sRGB format " + std::to_string(format) + " function " + std::to_string(function) + (filter == FilterPoint ? " point" : " bilinear");
RunSrgbZero(*device, format, {ClampEdge, filter, BorderBlack, 0u, 0u, false, function}, name.c_str(), useCache, variant);
RunZero(*device, format, {ClampEdge, filter, BorderBlack, 0u, 0u, false, function}, name.c_str(), useCache, variant);
}
}
}
@@ -403,10 +375,11 @@ int main() {
for (const auto filter : {FilterPoint, FilterBilinear, FilterAnisoBilinear}) {
if (format != Format8888UNorm && function != LessEqual) continue;
const auto name = "8-bit unorm format " + std::to_string(format) + " function " + std::to_string(function) + " filter " + std::to_string(filter);
RunSrgbZero(*device, format, {ClampEdge, filter, BorderBlack, 0u, 0u, false, function}, name.c_str(), false, 0u);
RunZero(*device, format, {ClampEdge, filter, BorderBlack, 0u, 0u, false, function}, name.c_str(), false, 0u);
}
}
}
RunZero(*device, Format1616UNorm, {ClampEdge, FilterPoint}, "16_16 unorm comparison returns zero", false, 0u);
Run(*device, {ClampEdge, FilterPoint}, "point, clamp to edge");
Run(*device, {ClampWrap, FilterPoint}, "point, wrap");
if (lodClamp) Run(*device, {ClampWrap, FilterPoint}, "point, wrap with negative offsets", true);
@@ -9,6 +9,7 @@
#include <cstdio>
#include <iostream>
#include <span>
#include <stdexcept>
#include <string>
#include <vector>
#ifdef _WIN32
@@ -52,6 +53,12 @@ alignas(256) constexpr std::array<std::uint32_t, 21> ExecCode{
0x7e02020cu, 0xbefe04c1u, 0xe0701000u, 0x80010102u, 0xbf810000u,
};
alignas(256) constexpr std::array<std::uint32_t, 19> DirectRootCode{
0xf4080100u, 0xfa000000u, 0xf4000200u, 0xfa000010u, 0xbe8a0302u, 0xbe8b0303u, 0xbe8c03ffu, 0x11111111u,
0xbf8cc07fu, 0xbf068008u, 0xbf850003u, 0xf4000305u, 0xfa000000u, 0xbf8cc07fu, 0x7e02020cu, 0x34040082u,
0xe0701000u, 0x80010102u, 0xbf810000u,
};
alignas(256) constexpr std::array<std::uint32_t, 21> EmptyExecCode{
0xf4080100u, 0xfa000000u, 0xf4000200u, 0xfa000010u, 0xf4040280u, 0xfa000018u, 0xbe8c03ffu, 0x11111111u,
0xbf8cc07fu, 0x7e02020cu, 0x34040082u, 0x7da80008u, 0xbf800000u, 0xf4000305u, 0xfa000000u, 0xbf8cc07fu,
@@ -123,7 +130,7 @@ struct Outcome {
};
template <std::size_t CodeWords>
Outcome Run(AgcDriver::VulkanDevice& device, const std::array<std::uint32_t, CodeWords>& code, std::uint64_t pointer, std::uint32_t guard, std::uint32_t groups = 1u, std::uint64_t second = 0u) {
Outcome Run(AgcDriver::VulkanDevice& device, const std::array<std::uint32_t, CodeWords>& code, std::uint64_t pointer, std::uint32_t guard, std::uint32_t groups = 1u, std::uint64_t second = 0u, bool direct = false) {
const auto output = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(Output.data()));
Root = {};
Root[0] = static_cast<std::uint32_t>(output);
@@ -137,7 +144,8 @@ Outcome Run(AgcDriver::VulkanDevice& device, const std::array<std::uint32_t, Cod
Root[9] = static_cast<std::uint32_t>(second >> 32u);
std::fill(Output.begin(), Output.end(), 0xdeadbeefu);
const auto root = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(Root.data()));
const std::array<std::uint32_t, 2> userData{static_cast<std::uint32_t>(root), static_cast<std::uint32_t>(root >> 32u)};
const std::array<std::uint32_t, 4> words{static_cast<std::uint32_t>(root), static_cast<std::uint32_t>(root >> 32u), static_cast<std::uint32_t>(pointer), static_cast<std::uint32_t>(pointer >> 32u)};
const std::span<const std::uint32_t> userData(words.data(), direct ? 4u : 2u);
const std::span<const std::uint32_t> span(code);
const ShaderRecompiler::ShaderComputeStageInfo compute{{Lanes, 1, 1}, 0u, {groups > 1u, false, false}, false, 1};
ShaderRecompiler::RecompileRequest request{
@@ -186,6 +194,47 @@ void RequireWords(const Outcome& outcome, std::uint32_t lanes, std::uint32_t ins
}
}
void RunDirectRootTests(AgcDriver::VulkanDevice& device) {
const auto valid = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(Data.data()));
void* reserved = nullptr;
#ifdef _WIN32
reserved = VirtualAlloc(nullptr, 65536, MEM_RESERVE, PAGE_NOACCESS);
#else
reserved = mmap(nullptr, 65536, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (reserved == MAP_FAILED) reserved = nullptr;
#endif
Require(reserved != nullptr, "direct root: cannot reserve an inaccessible range");
const auto unmapped = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(reserved));
const auto taken = Run(device, DirectRootCode, valid, 1u, 1u, 0u, true);
Require(!Faulted(taken) && taken.poisoned == 0u && taken.cacheable, "direct root: a mapped taken load faulted, was poisoned or left the caches:\n" + taken.log);
RequireWords(taken, Lanes, Payload, "direct root: a mapped taken load");
std::puts("direct root: mapped taken load wrote the payload");
const auto skippedMapped = Run(device, DirectRootCode, valid, 0u, 1u, 0u, true);
Require(!Faulted(skippedMapped) && skippedMapped.poisoned == 0u && skippedMapped.cacheable, "direct root: a mapped skipped load faulted, was poisoned or left the caches:\n" + skippedMapped.log);
RequireWords(skippedMapped, 0u, 0u, "direct root: a mapped skipped load");
std::puts("direct root: mapped skipped load wrote the default");
bool refused = false;
try {
static_cast<void>(Run(device, DirectRootCode, unmapped, 1u, 1u, 0u, true));
} catch (const std::runtime_error& error) {
char expected[96];
std::snprintf(expected, sizeof(expected), "guest memory is not readable at 0x%llx", static_cast<unsigned long long>(unmapped));
Require(std::string(error.what()).find(expected) != std::string::npos, "direct root: the invalid taken load failed for another reason: " + std::string(error.what()));
refused = true;
}
Require(refused, "direct root: an invalid taken load was accepted");
std::puts("direct root: inaccessible taken load was rejected");
const auto skipped = Run(device, DirectRootCode, unmapped, 0u, 1u, 0u, true);
Require(!Faulted(skipped) && skipped.poisoned == 0u && skipped.cacheable, "direct root: an inaccessible skipped load faulted, was poisoned or left the caches:\n" + skipped.log);
RequireWords(skipped, 0u, 0u, "direct root: an inaccessible skipped load");
std::puts("direct root: inaccessible skipped load wrote the default");
#ifdef _WIN32
VirtualFree(reserved, 0, MEM_RELEASE);
#else
munmap(reserved, 65536);
#endif
}
void RunTests(AgcDriver::VulkanDevice& device) {
const auto valid = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(Data.data()));
void* reserved = nullptr;
@@ -202,7 +251,7 @@ void RunTests(AgcDriver::VulkanDevice& device) {
Require(!Faulted(taken) && taken.poisoned == 0u && taken.cacheable, "guarded pointer: a mapped pointer faulted, was poisoned or left the caches:\n" + taken.log);
RequireWords(taken, Lanes, Payload, "guarded pointer: a mapped pointer under a taken branch");
const auto skippedNull = Run(device, BranchCode, 0u, 0u);
Require(!Faulted(skippedNull) && skippedNull.poisoned == 1u && !skippedNull.cacheable, "guarded pointer: a null pointer behind a branch that skips its read faulted or stayed cacheable:\n" + skippedNull.log);
Require(!Faulted(skippedNull) && skippedNull.poisoned == 0u && skippedNull.cacheable, "guarded pointer: a null pointer behind a branch that skips its read faulted, was poisoned or left the caches:\n" + skippedNull.log);
RequireWords(skippedNull, 0u, 0u, "guarded pointer: a skipped read through a null pointer");
const auto takenNull = Run(device, BranchCode, 0u, 1u);
Require(takenNull.pipelineVariantId == skippedNull.pipelineVariantId && takenNull.pipelineVariantId == taken.pipelineVariantId && takenNull.variantId == taken.variantId, "guarded pointer: the poison or the branch condition changed the pipeline variant");
@@ -233,7 +282,7 @@ void RunTests(AgcDriver::VulkanDevice& device) {
Require(noLanes.pipelineVariantId == someLanes.pipelineVariantId && oneLane.pipelineVariantId == someLanes.pipelineVariantId && emptyNull.pipelineVariantId == emptyMapped.pipelineVariantId, "guarded pointer: the poison changed an EXEC program's pipeline variant");
const auto chainSkipped = Run(device, ChainCode, 0u, 0u);
Require(chainSkipped.poisoned == 3u && Faulted(chainSkipped) && chainSkipped.log.find(Fault(0u, 0x24u)) != std::string::npos, "guarded pointer: a pointer read through a null pointer, used only by a skipped read, did not fault at its own pc:\n" + chainSkipped.log);
Require(chainSkipped.poisoned == 2u && Faulted(chainSkipped) && chainSkipped.log.find(Fault(0u, 0x24u)) != std::string::npos, "guarded pointer: a pointer read through a null pointer, used only by a skipped read, did not fault at its own pc:\n" + chainSkipped.log);
const auto chainTaken = Run(device, ChainCode, 0u, 1u);
Require(Faulted(chainTaken) && chainTaken.log.find(Fault(0u, 0x24u)) != std::string::npos, "guarded pointer: a read through a pointer read through a null pointer did not fault at the first read:\n" + chainTaken.log);
Indirect[0] = valid;
@@ -247,7 +296,7 @@ void RunTests(AgcDriver::VulkanDevice& device) {
Require(!Faulted(descriptorTaken) && descriptorTaken.poisoned == 0u && descriptorTaken.cacheable, "guarded pointer: a V# behind a mapped pointer faulted, was poisoned or left the caches:\n" + descriptorTaken.log);
RequireWords(descriptorTaken, Lanes, Payload, "guarded pointer: a buffer read through a V# behind a mapped pointer");
const auto descriptorSkipped = Run(device, DescriptorCode, 0u, 0u);
Require(!Faulted(descriptorSkipped) && descriptorSkipped.poisoned == 4u && !descriptorSkipped.cacheable, "guarded pointer: a V# behind a branch that skips its null pointer faulted or stayed cacheable:\n" + descriptorSkipped.log);
Require(!Faulted(descriptorSkipped) && descriptorSkipped.poisoned == 0u && descriptorSkipped.cacheable, "guarded pointer: a V# behind a branch that skips its null pointer faulted, was poisoned or left the caches:\n" + descriptorSkipped.log);
RequireWords(descriptorSkipped, 0u, 0u, "guarded pointer: a skipped V# load through a null pointer");
const auto descriptorNull = Run(device, DescriptorCode, 0u, 1u);
Require(descriptorNull.pipelineVariantId == descriptorSkipped.pipelineVariantId && Faulted(descriptorNull) && descriptorNull.log.find(Fault(0x10u, 0x2cu)) != std::string::npos, "guarded pointer: a V# load through a null pointer that runs did not fault at its pc and address:\n" + descriptorNull.log);
@@ -267,12 +316,18 @@ void RunTests(AgcDriver::VulkanDevice& device) {
}
int main() {
int main(int argc, char** argv) {
try {
Require(argc == 1 || (argc == 2 && std::string(argv[1]) == "--direct-root-only"), "unknown guarded-pointer test arguments");
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
RunTests(*device);
std::puts("guarded null pointer tests passed");
if (argc == 2) {
RunDirectRootTests(*device);
std::puts("guarded direct root tests passed");
} else {
RunTests(*device);
std::puts("guarded null pointer tests passed");
}
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
@@ -63,16 +63,16 @@ void* Map(std::size_t bytes = BlockBytes) {
return result;
}
void CheckPattern(const void* pointer) {
void CheckPattern(const void* pointer, std::size_t bytes = FillBytes) {
const auto* data = static_cast<const std::byte*>(pointer);
for (std::size_t offset = 0; offset < FillBytes; offset += sizeof(Pattern))
for (std::size_t offset = 0; offset < bytes; offset += sizeof(Pattern))
Require(std::memcmp(data + offset, Pattern.data(), sizeof(Pattern)) == 0, "guest mutation lost the pending GPU fill");
}
void Fill(AgcDriver::VulkanDevice& device, void* pointer) {
void Fill(AgcDriver::VulkanDevice& device, void* pointer, std::size_t bytes = FillBytes) {
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
Require(device.FillBuffer(Address(pointer), FillBytes, Pattern), "Vulkan device did not import the guest allocation");
Require(Imported(device, pointer), "GPU fill did not create a host import");
Require(device.FillBuffer(Address(pointer), bytes, Pattern), "Vulkan device did not import the guest allocation");
Require(Imported(device, pointer, bytes), "GPU fill did not create a host import");
}
void CheckMappings(AgcDriver::VulkanDevice& device) {
@@ -105,14 +105,25 @@ void CheckMappings(AgcDriver::VulkanDevice& device) {
Require(!Imported(device, mapping), "host import survived guest munmap");
mapping = Map(3 * BlockBytes);
Fill(device, mapping);
Fill(device, mapping, 3 * BlockBytes);
auto* middle = static_cast<std::uint8_t*>(mapping) + BlockBytes;
Require(mprotect_nid_postfix(middle, BlockBytes, 1) == 0, "guest partial protection failed");
Require(!Imported(device, mapping), "whole host import survived partial protection change");
CheckPattern(mapping);
Fill(device, mapping);
Require(!Imported(device, mapping, 3 * BlockBytes), "whole host import survived partial protection change");
CheckPattern(mapping, 3 * BlockBytes);
Require(munmap_nid_postfix(middle, BlockBytes) == 0, "guest middle unmap after protection change failed");
Require(!Imported(device, mapping, 3 * BlockBytes), "host import still covered the protected and unmapped middle range");
Require(munmap_nid_postfix(mapping, BlockBytes) == 0, "guest prefix unmap after protection change failed");
Require(munmap_nid_postfix(middle + BlockBytes, BlockBytes) == 0, "guest suffix unmap after protection change failed");
mapping = Map(3 * BlockBytes);
Fill(device, mapping, 3 * BlockBytes);
Require(Imported(device, mapping, 3 * BlockBytes), "whole host import did not cover the three-block mapping before middle unmap");
middle = static_cast<std::uint8_t*>(mapping) + BlockBytes;
Require(munmap_nid_postfix(middle, BlockBytes) == 0, "guest middle unmap failed");
Require(Imported(device, mapping), "unrelated prefix import was retired by middle unmap");
Require(!Imported(device, mapping, 3 * BlockBytes), "middle unmap left a host import spanning the unmapped range");
Require(!Imported(device, mapping), "middle unmap retained the prefix of the retired host import");
Fill(device, mapping);
Require(Imported(device, mapping), "the retained prefix was not imported again after middle unmap");
Require(munmap_nid_postfix(mapping, BlockBytes) == 0, "guest prefix unmap failed");
Require(!Imported(device, mapping), "prefix host import survived guest munmap");
Require(munmap_nid_postfix(middle + BlockBytes, BlockBytes) == 0, "guest suffix unmap failed");
@@ -11,6 +11,7 @@
#include <cstdio>
#include <exception>
#include <iostream>
#include <span>
#include <string>
#include <utility>
#include <vector>
@@ -23,9 +24,9 @@ constexpr std::uint32_t Width = 64;
constexpr std::uint32_t Height = 64;
using Image = std::array<std::byte, Width * Height * 4>;
alignas(256) Image Pixels{};
alignas(256) constexpr std::array<std::uint16_t, 4> Indices{0, 1, 2, 0};
alignas(256) constexpr std::array<std::array<float, 8>, 3> Vertices{{
{-0.9f, -0.8f, 0, 1, 1, 0, 0, 1}, {-0.1f, -0.8f, 0, 1, 0, 1, 0, 1}, {-0.5f, 0.8f, 0, 1, 0, 0, 1, 1}
alignas(256) constexpr std::array<std::uint16_t, 4> Indices{0, 1, 2, 3};
alignas(256) constexpr std::array<std::array<float, 8>, 4> Vertices{{
{-0.9f, -0.8f, 0, 1, 1, 0, 0, 1}, {-0.1f, -0.8f, 0, 1, 0, 1, 0, 1}, {-0.5f, 0.8f, 0, 1, 0, 0, 1, 1}, {0.3f, 0.8f, 0, 1, 1, 1, 0, 1}
}};
alignas(256) constexpr std::array<std::uint32_t, 7> FlatPixelCode{0xc8120002u, 0xc8160102u, 0xc81a0202u, 0xc81e0302u, 0xf800180fu, 0x07060504u, 0xbf810000u};
alignas(256) constexpr std::array<std::uint32_t, 11> SmoothPixelCode{0xc8100000u, 0xc8110001u, 0xc8140100u, 0xc8150101u, 0xc8180200u, 0xc8190201u, 0xc81c0300u, 0xc81d0301u, 0xf800180fu, 0x07060504u, 0xbf810000u};
@@ -64,7 +65,7 @@ void Write(std::vector<std::uint32_t>& commands, std::uint32_t opcode, std::uint
commands.insert(commands.end(), {0xc0010000u | (opcode << 8u), offset, value});
}
Image Draw(const std::uint32_t* pixelCode, bool flat, bool last) {
Image Draw(const std::uint32_t* pixelCode, bool flat, bool last, bool strip = false) {
Pixels.fill(std::byte{0});
const auto color = reinterpret_cast<std::uintptr_t>(Pixels.data());
const auto geometry = reinterpret_cast<std::uintptr_t>(GeometryCode.data());
@@ -98,13 +99,14 @@ Image Draw(const std::uint32_t* pixelCode, bool flat, bool last) {
{0x8, static_cast<std::uint32_t>(pixel >> 8u)}, {0x9, static_cast<std::uint32_t>(pixel >> 40u)}, {0xb, 0}
};
std::vector<std::uint32_t> commands;
Write(commands, 0x79, 0x242, 4);
Write(commands, 0x79, 0x242, strip ? 6u : 4u);
Write(commands, 0x79, 0x25b, (12u << 9u) | 4u);
for (const auto& [offset, value] : context) Write(commands, 0x69, offset, value);
for (const auto& [offset, value] : shaders) Write(commands, 0x76, offset, value);
commands.insert(commands.end(), {0xc0002a00u, 0u});
const auto address = reinterpret_cast<std::uintptr_t>(Indices.data());
commands.insert(commands.end(), {0xc0042700u, 3u, static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u), 3u, 0u});
const std::uint32_t count = strip ? 4u : 3u;
commands.insert(commands.end(), {0xc0042700u, count, static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u), count, 0u});
Packet packet{commands.data(), static_cast<std::uint32_t>(commands.size()), 0, {}};
Require(sceAgcDriverSubmitDcb(&packet) == 0, "mesh submission failed");
AgcDriverWaitIdle_nid_postfix();
@@ -132,6 +134,25 @@ void FlatInputs() {
}
}
constexpr std::array<std::pair<std::uint32_t, std::uint32_t>, 4> SecondInside{{{26, 48}, {32, 48}, {29, 44}, {29, 52}}};
void CheckFlat(const Image& image, std::span<const std::pair<std::uint32_t, std::uint32_t>> pixels, const std::array<unsigned, 4>& expected, const std::string& what) {
for (const auto& [x, y] : pixels) {
for (std::uint32_t channel = 0; channel < 4; ++channel) {
Require(Channel(image, x, y, channel) == expected[channel], what + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(Channel(image, x, y, channel)) + ", expected " + std::to_string(expected[channel]));
}
}
}
void StripFlatInputs() {
const auto last = Draw(FlatPixelCode.data(), true, true, true);
CheckFlat(last, Inside, {0u, 0u, 255u, 255u}, "triangle strip, last provoking vertex, triangle 0 (the color of vertex 2)");
CheckFlat(last, SecondInside, {255u, 255u, 0u, 255u}, "triangle strip, last provoking vertex, triangle 1 (the color of vertex 3)");
Require(Channel(last, 48, 48, 3) == 0, "triangle strip, last provoking vertex: the draw wrote outside its triangles");
const auto first = Draw(FlatPixelCode.data(), true, false, true);
CheckFlat(first, Inside, {255u, 0u, 0u, 255u}, "triangle strip, first provoking vertex, triangle 0 (the color of vertex 0)");
}
void SmoothInputs() {
const auto first = Draw(SmoothPixelCode.data(), false, false);
for (const auto& [x, y] : Inside) {
@@ -169,9 +190,10 @@ int main() {
Register(FlatPixelHeader, FlatPixelCode.data(), sizeof(FlatPixelCode), 1);
Register(SmoothPixelHeader, SmoothPixelCode.data(), sizeof(SmoothPixelCode), 1);
FlatInputs();
StripFlatInputs();
if (smooth) SmoothInputs();
AgcDriverShutdown_nid_postfix();
std::cout << "geometry-program triangles take flat inputs from vertex 0 with the first provoking vertex and from vertex 2 with the last" << (smooth ? ", and interpolate smooth inputs the same in both modes\n" : "; smooth inputs skipped without fragment shader barycentrics\n");
std::cout << "geometry-program triangles take flat inputs from vertex 0 with the first provoking vertex and from vertex 2 with the last, and the second triangle of a strip takes them from vertex 3 with the last" << (smooth ? ", and interpolate smooth inputs the same in both modes\n" : "; smooth inputs skipped without fragment shader barycentrics\n");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
@@ -1,6 +1,7 @@
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp"
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTextureLayout.hpp"
#include "VulkanTestDevice.hpp"
#include "MsaaReadbackVertex_spv.h"
#include "MsaaReadbackUint_spv.h"
@@ -69,8 +70,8 @@ void CheckSupportSelection() {
static_assert(!SurfaceFits(VkImageFormatProperties{}, 4u));
}
bool SurfaceSupported(const Context& context, VkFormat format, std::uint32_t samples, std::uint32_t width = 17u, std::uint32_t height = 5u, std::uint32_t layers = 2u) {
const VkImageUsageFlags usage = VK_IMAGE_USAGE_SAMPLED_BIT | (format == VK_FORMAT_D32_SFLOAT ? VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT : VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT);
bool SurfaceSupported(const Context& context, VkFormat format, std::uint32_t samples, std::uint32_t width = 17u, std::uint32_t height = 5u, std::uint32_t layers = 2u, bool writable = false) {
const VkImageUsageFlags usage = VK_IMAGE_USAGE_SAMPLED_BIT | (format == VK_FORMAT_D32_SFLOAT ? VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT : VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) | (writable ? VK_IMAGE_USAGE_STORAGE_BIT : 0u);
VkImageFormatProperties supported{};
const auto result = context.imageFormatProperties(context.physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, 0u, &supported);
if (result != VK_ERROR_FORMAT_NOT_SUPPORTED) Check(result, "readback fixture format query");
@@ -97,11 +98,11 @@ struct Surface {
std::vector<VkImageView> views;
std::vector<VkFramebuffer> framebuffers;
Surface(const Context& context, VkFormat format, std::uint32_t elementBytes, std::uint32_t samples, std::uint32_t width = 17u, std::uint32_t height = 5u, std::uint32_t layers = 2u) : context(context), format(format), elementBytes(elementBytes), samples(samples), width(width), height(height), layers(layers) {
Surface(const Context& context, VkFormat format, std::uint32_t elementBytes, std::uint32_t samples, std::uint32_t width = 17u, std::uint32_t height = 5u, std::uint32_t layers = 2u, bool writable = false) : context(context), format(format), elementBytes(elementBytes), samples(samples), width(width), height(height), layers(layers) {
try {
const bool depth = format == VK_FORMAT_D32_SFLOAT;
const auto aspect = depth ? VK_IMAGE_ASPECT_DEPTH_BIT : VK_IMAGE_ASPECT_COLOR_BIT;
const VkImageUsageFlags usage = VK_IMAGE_USAGE_SAMPLED_BIT | (depth ? VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT : VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT);
const VkImageUsageFlags usage = VK_IMAGE_USAGE_SAMPLED_BIT | (depth ? VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT : VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) | (writable ? VK_IMAGE_USAGE_STORAGE_BIT : 0u);
VkImageFormatProperties supported{};
Check(context.imageFormatProperties(context.physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, 0u, &supported), "readback fixture format query");
Require(SurfaceFits(supported, samples, width, height, layers), "readback fixture format does not support its sample count or extent");
@@ -275,15 +276,15 @@ struct Surface {
const auto layer = pixel / (width * height);
const auto value = x + y * width + layer * width * height + sample * 31u;
std::array<std::uint32_t, 4> words{0x91352700u ^ value, 0x45219b00u ^ (value * 3u), 0xc7391d00u ^ (value * 5u), 0x6b43e100u ^ (value * 7u)};
if (format == VK_FORMAT_D32_SFLOAT) words[0] = std::bit_cast<std::uint32_t>(0.125f + static_cast<float>(element + 1u) / 65536.0f);
if (format == VK_FORMAT_D32_SFLOAT) words[0] = std::bit_cast<std::uint32_t>(0.125f + static_cast<float>(element + 1u) / 1048576.0f);
std::memcpy(result.data() + static_cast<std::size_t>(index) * elementBytes, words.data(), elementBytes);
}
return result;
}
};
void CheckReadback(TextureDetiler& reader, const Surface& surface, std::uint32_t first, std::uint32_t elements, std::uint32_t repetitions, std::size_t extraBytes = 0u) {
const auto expected = surface.Expected(first, elements);
std::vector<std::byte> CheckReadback(TextureDetiler& reader, const Surface& surface, std::uint32_t first, std::uint32_t elements, std::uint32_t repetitions, std::size_t extraBytes = 0u, std::span<const std::byte> supplied = {}) {
const auto expected = supplied.empty() ? surface.Expected(first, elements) : std::vector<std::byte>(supplied.begin(), supplied.end());
const auto bytes = ((expected.size() + 3u) & ~std::size_t{3u}) + extraBytes;
const auto alignment = std::max<std::size_t>(4u, surface.context.limits.minStorageBufferOffsetAlignment);
const auto offset = (16u + alignment - 1u) / alignment * alignment;
@@ -291,7 +292,7 @@ void CheckReadback(TextureDetiler& reader, const Surface& surface, std::uint32_t
std::fill(output.Bytes().begin(), output.Bytes().end(), std::byte{0xa5});
reader.BeginBatch();
CommandBatch batch(surface.context);
RecordMemoryBarrier(surface.context, batch.Handle(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT);
RecordMemoryBarrier(surface.context, batch.Handle(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT);
for (std::uint32_t repeat = 0u; repeat < repetitions; ++repeat) {
reader.DispatchMultisampleReadback(batch.Handle(), surface.format, output.Handle(), offset, bytes, surface.arrayView, surface.width, surface.height, surface.layers, surface.samples, first, elements);
if (repeat + 1u < repetitions) RecordMemoryBarrier(surface.context, batch.Handle(), VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT);
@@ -304,6 +305,61 @@ void CheckReadback(TextureDetiler& reader, const Surface& surface, std::uint32_t
if (mismatch.first != expected.end()) std::printf("readback mismatch: format=%u first=%u byte=%zu expected=0x%02x actual=0x%02x\n", static_cast<std::uint32_t>(surface.format), first, static_cast<std::size_t>(mismatch.first - expected.begin()), std::to_integer<unsigned>(*mismatch.first), std::to_integer<unsigned>(*mismatch.second));
Require(std::equal(expected.begin(), expected.end(), data.begin() + offset), "multisample readback changed a rendered sample, component, layer or pixel");
for (std::size_t index = 0u; index < data.size(); ++index) Require((index >= offset && index < offset + expected.size()) || data[index] == std::byte{0xa5}, "multisample readback changed a buffer sentinel or packed tail");
return {data.begin() + offset, data.begin() + offset + expected.size()};
}
void CheckRetile(TextureDetiler& reader, const Surface& surface) {
const auto elements = surface.width * surface.height * surface.layers * surface.samples;
const auto linear = CheckReadback(reader, surface, 0u, elements, 1u);
const auto expected = surface.Expected(0u, elements);
constexpr std::array<std::array<std::uint32_t, 3>, 7> coordinates{{
{0u, 0u, 0u}, {128u, 66u, 16u}, {64u, 32u, 1u}, {32u, 64u, 2u},
{63u, 31u, 7u}, {127u, 65u, 15u}, {1u, 1u, 3u}
}};
constexpr std::array<TextureTileMode, 2> modes{TextureTileMode::kR64KBX, TextureTileMode::kZ64KBX};
constexpr std::array<std::array<std::uint64_t, 7>, 2> references{{
{0ull, 6621216ull, 493568ull, 1016832ull, 2766588ull, 6170780ull, 1215508ull},
{0ull, 6637700ull, 460808ull, 1000460ull, 2815728ull, 6219892ull, 1182776ull}
}};
for (std::size_t mode = 0u; mode < modes.size(); ++mode) {
for (const auto pipeBankXor : {0u, 0x8100u}) {
const MultisampleTextureLayout layout(modes[mode], surface.elementBytes, surface.samples, surface.width, surface.height, surface.layers, pipeBankXor);
std::vector<std::byte> tiled(static_cast<std::size_t>(layout.Bytes()) + 32u, std::byte{0xa5});
auto payload = std::span(tiled).subspan(16u, layout.Bytes());
layout.Tile(linear, payload);
std::vector<bool> covered(payload.size());
std::size_t position = 0u;
for (std::uint32_t layer = 0u; layer < surface.layers; ++layer) {
for (std::uint32_t y = 0u; y < surface.height; ++y) {
for (std::uint32_t x = 0u; x < surface.width; ++x) {
for (std::uint32_t fragment = 0u; fragment < surface.samples; ++fragment) {
const auto address = layout.Offset(x, y, layer, fragment);
Require(address + surface.elementBytes <= payload.size(), "readback retile exceeds its allocation");
for (std::uint32_t byte = 0u; byte < surface.elementBytes; ++byte) {
Require(!covered[address + byte], "readback retile elements overlap");
covered[address + byte] = true;
Require(payload[address + byte] == expected[position++], "readback and retile disagree on layer, pixel or fragment order");
}
}
}
}
}
for (std::size_t index = 0u; index < covered.size(); ++index) Require(covered[index] || payload[index] == std::byte{0xa5}, "readback retile changed tile padding");
for (std::size_t index = 0u; index < coordinates.size(); ++index) {
const auto& coordinate = coordinates[index];
const auto fragment = static_cast<std::uint32_t>(index) % surface.samples;
const auto source = (((coordinate[2] * surface.height + coordinate[1]) * surface.width + coordinate[0]) * surface.samples + fragment) * surface.elementBytes;
const auto reference = references[mode][index];
const auto address = (reference & ~65535ull) | ((reference & 65535ull) ^ pipeBankXor);
Require(address + surface.elementBytes <= payload.size() && std::equal(expected.begin() + source, expected.begin() + source + surface.elementBytes, payload.begin() + address), "rendered readback differs at an AMD reference fragment address");
}
std::vector<std::byte> restored(expected.size());
layout.Detile(payload, restored);
Require(restored == expected && linear == expected, "readback retile roundtrip changed rendered element bits");
for (std::size_t index = 0u; index < tiled.size(); ++index) Require((index >= 16u && index < 16u + payload.size()) || tiled[index] == std::byte{0xa5}, "readback retile changed an allocation sentinel");
std::printf("multisample readback retile: format=%u mode=%s xor=0x%x samples=%u layers=%u elements=%u reference-points=7 padding/sentinels/roundtrip=passed\n", static_cast<std::uint32_t>(surface.format), mode == 0u ? "R_X" : "Z_X", pipeBankXor, surface.samples, surface.layers, elements);
}
}
}
template<typename TAction>
@@ -331,6 +387,106 @@ void CheckRefusals(TextureDetiler& reader, const Surface& surface) {
std::puts("multisample readback invalid-input refusals passed");
}
std::vector<std::byte> Upload(TextureDetiler& transfer, TextureDetiler& reader, const Surface& surface, std::vector<std::byte>& expected, std::uint32_t first, std::uint32_t elements, std::uint32_t repetitions = 1u, std::span<const std::byte> supplied = {}) {
const auto bytes = static_cast<std::size_t>(elements) * surface.elementBytes;
const auto padded = (bytes + 3u) & ~std::size_t{3};
const auto alignment = std::max<std::size_t>({4u, surface.elementBytes, surface.context.limits.minStorageBufferOffsetAlignment});
const auto offset = (16u + alignment - 1u) / alignment * alignment;
Buffer source(surface.context, offset + padded + 16u, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
std::fill(source.Bytes().begin(), source.Bytes().end(), std::byte{0x5a});
Require(supplied.empty() || supplied.size() == bytes, "multisample upload fixture source size mismatch");
for (std::size_t index = 0; index < bytes; ++index) source.Bytes()[offset + index] = supplied.empty() ? static_cast<std::byte>((index * 29u + first * 17u + elements * 7u) & 255u) : supplied[index];
const std::vector<std::byte> retained(source.Bytes().begin(), source.Bytes().end());
std::copy_n(retained.begin() + offset, bytes, expected.begin() + static_cast<std::size_t>(first) * surface.elementBytes);
const auto stride = 256u / alignment * alignment;
Require(stride != 0u, "upload fixture cannot hold an aligned chunk");
transfer.BeginBatch();
CommandBatch batch(surface.context);
RecordMemoryBarrier(surface.context, batch.Handle(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT);
for (std::uint32_t repeat = 0; repeat < repetitions; ++repeat) {
for (std::size_t done = 0; done < bytes; done += stride) {
const auto count = std::min(stride, bytes - done);
transfer.DispatchMultisampleImage(batch.Handle(), surface.elementBytes, source.Handle(), offset + done, (count + 3u) & ~std::size_t{3}, surface.arrayView, surface.width, surface.height, surface.layers, surface.samples, first + static_cast<std::uint32_t>(done / surface.elementBytes), static_cast<std::uint32_t>(count / surface.elementBytes));
}
RecordMemoryBarrier(surface.context, batch.Handle(), VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT);
}
batch.SubmitAndWait();
source.Invalidate();
Require(std::equal(retained.begin(), retained.end(), source.Bytes().begin()), "multisample upload changed source bytes or sentinels");
return CheckReadback(reader, surface, 0u, surface.width * surface.height * surface.layers * surface.samples, 1u, 0u, expected);
}
void CheckUploadRefusals(TextureDetiler& transfer, const Surface& surface) {
Buffer source(surface.context, 256u, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
CommandBatch batch(surface.context);
const auto call = [&](std::uint32_t bytes, std::uint32_t fragments, std::uint32_t first, std::uint32_t elements, std::uint64_t offset = 0u, std::uint64_t range = 4u) {
transfer.DispatchMultisampleImage(batch.Handle(), bytes, source.Handle(), offset, range, surface.arrayView, 1u, 1u, 1u, fragments, first, elements);
};
for (const auto bytes : {0u, 3u, 32u}) Refused([&] { call(bytes, 4u, 0u, 1u); }, "element size is unsupported");
for (const auto fragments : {0u, 1u, 3u, 16u}) Refused([&] { call(4u, fragments, 0u, 1u); }, "fragment count is unsupported");
for (const auto range : {std::array{0u, 0u}, std::array{4u, 1u}, std::array{3u, 2u}, std::array{0xffffffffu, 2u}}) Refused([&] { call(4u, 4u, range[0], range[1]); }, "element range is invalid");
for (const auto range : {std::array<std::uint64_t, 2>{1u, 4u}, std::array<std::uint64_t, 2>{0u, 0u}, std::array<std::uint64_t, 2>{0u, 3u}, std::array<std::uint64_t, 2>{0u, 260u}, std::array<std::uint64_t, 2>{~std::uint64_t{0} - 3u, 8u}}) Refused([&] { call(4u, 4u, 0u, 1u, range[0], range[1]); }, "buffer range is invalid");
Refused([&] { transfer.DispatchMultisampleImage(VK_NULL_HANDLE, 4u, source.Handle(), 0u, 4u, surface.arrayView, 1u, 1u, 1u, 4u, 0u, 1u); }, "requires commands, buffer and array view");
Refused([&] { transfer.DispatchMultisampleImage(batch.Handle(), 4u, VK_NULL_HANDLE, 0u, 4u, surface.arrayView, 1u, 1u, 1u, 4u, 0u, 1u); }, "requires commands, buffer and array view");
Refused([&] { transfer.DispatchMultisampleImage(batch.Handle(), 4u, source.Handle(), 0u, 4u, VK_NULL_HANDLE, 1u, 1u, 1u, 4u, 0u, 1u); }, "requires commands, buffer and array view");
for (const auto extent : {std::array{0u, 1u, 1u}, std::array{1u, 0u, 1u}, std::array{1u, 1u, 0u}, std::array{16385u, 1u, 1u}, std::array{1u, 16385u, 1u}, std::array{1u, 1u, 8193u}}) Refused([&] { transfer.DispatchMultisampleImage(batch.Handle(), 4u, source.Handle(), 0u, 4u, surface.arrayView, extent[0], extent[1], extent[2], 4u, 0u, 1u); }, "extent is unsupported");
Refused([&] { transfer.DispatchMultisampleImage(batch.Handle(), 4u, source.Handle(), 0u, 4u, surface.arrayView, 16384u, 16384u, 8192u, 8u, 0u, 1u); }, "element range is invalid");
for (const bool multisample : {false, true}) {
auto unsupported = surface.context;
if (multisample) unsupported.storageImageMultisample = false;
else unsupported.storageImageWriteWithoutFormat = false;
TextureDetiler unavailable(unsupported);
Refused([&] { unavailable.DispatchMultisampleImage(batch.Handle(), 4u, source.Handle(), 0u, 4u, surface.arrayView, 1u, 1u, 1u, 4u, 0u, 1u); }, "does not enable multisampled storage image writes without format");
}
}
void CheckUpload(const Context& context) {
Require(context.storageImageMultisample && context.storageImageWriteWithoutFormat, "upload fixture requires enabled multisampled formatless storage writes");
auto limited = context;
limited.limits.maxComputeWorkGroupCount[0] = 1u;
limited.limits.maxStorageBufferRange = 256u;
TextureDetiler transfer(limited);
TextureDetiler reader(context);
std::uint32_t exercised = 0u;
for (const auto bytes : {1u, 2u, 4u, 8u, 16u}) {
const auto format = TextureDetiler::ImageElementFormat(bytes);
for (const auto samples : {2u, 4u, 8u}) {
if (!SurfaceSupported(context, format, samples, 17u, 5u, 2u, true)) continue;
Surface surface(context, format, bytes, samples, 17u, 5u, 2u, true);
surface.Render();
const auto elements = surface.width * surface.height * surface.layers * samples;
auto expected = surface.Expected(0u, elements);
Upload(transfer, reader, surface, expected, 3u, 1u);
Upload(transfer, reader, surface, expected, 1u, elements - 3u);
Upload(transfer, reader, surface, expected, 0u, elements);
Upload(transfer, reader, surface, expected, elements - 1u, 1u, samples == 4u ? 65u : 1u);
if (bytes == 4u && samples == 4u) CheckUploadRefusals(transfer, surface);
std::printf("multisample upload: bytes=%u samples=%u layers=2 full/partial/packed/source-sentinels/chunks/reuse=passed\n", bytes, samples);
++exercised;
}
}
Require(exercised != 0u, "no supported multisample upload subcase was exercised");
const MultisampleTextureLayout active(TextureTileMode::kR64KBX, 8u, 4u, 17u, 5u, 2u);
if (SurfaceSupported(context, VK_FORMAT_R32G32_UINT, 4u, active.Pitch(), active.PaddedHeight(), 2u, true)) {
for (const auto xorValue : {0u, 0x8100u}) {
const MultisampleTextureLayout layout(TextureTileMode::kR64KBX, 8u, 4u, active.Pitch(), active.PaddedHeight(), 2u, xorValue);
std::vector<std::byte> tiled(static_cast<std::size_t>(layout.Bytes()));
for (std::size_t byte = 0; byte < tiled.size(); ++byte) tiled[byte] = static_cast<std::byte>((byte * 37u + (byte >> 8u) * 13u + (byte >> 15u) * 71u + (byte >> 16u) * 101u) & 255u);
std::vector<std::byte> linear(static_cast<std::size_t>(layout.LinearBytes()));
layout.Detile(tiled, linear);
Surface surface(context, VK_FORMAT_R32G32_UINT, 8u, 4u, layout.Pitch(), layout.PaddedHeight(), 2u, true);
surface.Render();
const auto elements = surface.width * surface.height * surface.layers * surface.samples;
auto expected = surface.Expected(0u, elements);
const auto actual = Upload(transfer, reader, surface, expected, 0u, elements, 1u, linear);
std::vector<std::byte> retiled(tiled.size(), std::byte{0xa5});
layout.Tile(actual, retiled);
Require(retiled == tiled, "multisample padded initialization changed color bytes, fragments, layers, XOR or padding");
std::printf("multisample padded upload: active=17x5 backing=%ux%u layers=2 fragments=4 bytes=%zu xor=0x%x exact roundtrip passed\n", surface.width, surface.height, retiled.size(), xorValue);
}
}
}
}
int main(int argc, char** argv) {
@@ -343,8 +499,8 @@ int main(int argc, char** argv) {
else if (mode == "uint16") { format = VK_FORMAT_R16_UINT; bytes = 2u; }
else if (mode == "uint64") { format = VK_FORMAT_R32G32_UINT; bytes = 8u; }
else if (mode == "uint128") { format = VK_FORMAT_R32G32B32A32_UINT; bytes = 16u; }
else Require(mode == "uint32" || mode == "depth", "unknown multisample readback test mode");
if (mode == "depth") format = VK_FORMAT_D32_SFLOAT;
else Require(mode == "uint32" || mode == "depth" || mode == "upload" || mode == "retile" || mode == "depth_retile", "unknown multisample readback test mode");
if (mode == "depth" || mode == "depth_retile") format = VK_FORMAT_D32_SFLOAT;
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex());
@@ -357,9 +513,28 @@ int main(int argc, char** argv) {
std::puts("skipped, the device does not support per-sample shading");
return VulkanTestSkipped;
}
if (mode == "upload") {
if (!context.storageImageMultisample || !context.storageImageWriteWithoutFormat) {
std::puts("skipped, the device does not enable multisampled formatless storage writes");
return VulkanTestSkipped;
}
CheckUpload(context);
Require(checkedPipelineCacheUses != 0u, "upload fixture did not check pipeline cache use");
std::printf("multisample upload guarded pipeline creation checks=%u passed\n", checkedPipelineCacheUses);
return 0;
}
auto limited = context;
limited.limits.maxComputeWorkGroupCount[0] = 1u;
TextureDetiler reader(limited);
if (mode == "retile" || mode == "depth_retile") {
if (!SurfaceSupported(context, format, 4u, 129u, 67u, 17u)) return VulkanTestSkipped;
Surface surface(context, format, bytes, 4u, 129u, 67u, 17u);
surface.Render();
CheckRetile(reader, surface);
Require(checkedPipelineCacheUses != 0u, "retile fixture did not check pipeline cache use");
std::printf("multisample retile guarded pipeline creation checks=%u passed\n", checkedPipelineCacheUses);
return 0;
}
std::uint32_t checked = 0u;
for (const auto samples : {2u, 4u, 8u}) {
if (!SurfaceSupported(context, format, samples)) continue;
@@ -407,6 +407,51 @@ public:
AwaitImages(0, "sampled MSAA targets were not fully released");
}
void OrderRemap() {
const auto locations = context.sampleLocationSampleCounts;
const auto grid = context.sampleLocationGridSampleCounts;
context.sampleLocationSampleCounts |= VK_SAMPLE_COUNT_4_BIT;
context.sampleLocationGridSampleCounts &= ~static_cast<VkSampleCountFlags>(VK_SAMPLE_COUNT_4_BIT);
GuestBlock block;
const auto target = Target(block);
if (!MayReorderSamples(context, target.samples) || SampleOrderStorageFormat(context, target.format, target.elementBytes, target.samples) == VK_FORMAT_UNDEFINED) {
context.sampleLocationSampleCounts = locations;
context.sampleLocationGridSampleCounts = grid;
std::puts("the device has no multisampled storage view for the sample-order target; its retirement case is skipped");
return;
}
SampleOrderTarget order;
VkImage oldImage = VK_NULL_HANDLE;
Image old{};
{
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
order = MultisampleTargetOrderTarget(context, target);
old = ImageFor(MultisampleTargetView(context, target), &oldImage);
Require(order.image == oldImage, "the sample-order target does not name its target's image");
}
Sync();
block.Unmap();
block.Map();
{
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
const auto replacement = ImageFor(MultisampleTargetView(context, target));
CheckPendingImages();
Require(replacement.generation != old.generation, "a remapped reordered MSAA target reused its retired owner");
Require(Alive(oldImage, old), "a held sample-order target lost its image when its MSAA target retired");
context.recorder->Keep(order.keep);
order = {};
Require(Alive(oldImage, old), "recorded sample-order work lost its image when its MSAA target retired");
}
Sync();
AwaitImages(1, "a retired sample-order target survives its last batch");
Require(!Alive(oldImage, old), "a retired reordered MSAA parent survives its last batch");
block.Unmap();
Clear();
context.sampleLocationSampleCounts = locations;
context.sampleLocationGridSampleCounts = grid;
AwaitImages(0, "reordered MSAA targets were not fully released");
}
void StorageRemap() {
GuestBlock block;
const auto target = Target(block);
@@ -487,7 +532,8 @@ int main() {
run.RecordedRemap();
run.SampledRemap();
run.StorageRemap();
std::puts("retired MSAA allocations stay bounded, recorded clears and storage promotions retain their images, and sampled aliases release their parents after the last batch");
run.OrderRemap();
std::puts("retired MSAA allocations stay bounded, recorded clears and storage promotions retain their images, sampled aliases release their parents after the last batch, and sample-order targets keep their retired images until their batch completes");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
@@ -18,6 +18,7 @@
#include <exception>
#include <algorithm>
#include <iostream>
#include <limits>
#include <span>
#include <string>
#include <vector>
@@ -141,14 +142,17 @@ void CheckLdsSplitThrows(AgcDriver::VulkanDevice& device) {
request.useCache = false;
const auto result = ShaderRecompiler::Recompile(request);
Require(result.workgroupMemoryDwords != 0u, "oversized dispatch: the LDS shader did not take the device-memory path");
bool threw = false;
try {
device.Dispatch(result, 1, device.WorkGroupCountLimit()[1] + 1u, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
} catch (const std::runtime_error&) {
threw = true;
for (const auto z : {1u, std::numeric_limits<std::uint32_t>::max()}) {
bool threw = false;
try {
device.Dispatch(result, 1, device.WorkGroupCountLimit()[1] + 1u, z, {}, reinterpret_cast<std::uintptr_t>(code.data()));
} catch (const std::runtime_error& error) {
Require(std::string(error.what()).find("a split aliases the device-memory LDS slots") != std::string::npos, "oversized dispatch: wrong LDS split refusal: " + std::string(error.what()));
threw = true;
}
device.WaitIdle();
Require(threw, "oversized dispatch: a Y count over the limit with device-memory LDS did not throw");
}
device.WaitIdle();
Require(threw, "oversized dispatch: a Y count over the limit with device-memory LDS did not throw");
}
alignas(256) constexpr std::array<std::uint32_t, 6> GroupYCode{
Loaded 100 of 788 files, more files were not shown because too many files have changed in this diff. Show more