//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // Regression test for issue #69: the engine feeds a shared Device::pipelineCache // to every vkCreate*Pipelines call and persists it across runs. A blob written // by a different GPU (or a corrupt/short file) must be rejected before it is // handed to vkCreatePipelineCache, otherwise the driver ignores or rejects it. // Device::PipelineCacheDataCompatible is that gate: pure logic over the standard // 32-byte VkPipelineCacheHeaderVersionOne header (headerSize, headerVersion, // vendorID, deviceID, pipelineCacheUUID) compared against Device::deviceProperties. // It needs no GPU, so this test stamps synthetic device identities and headers // and drives it directly, mirroring MemoryTypeFallback / UploadStrategy. #include #include #include "vulkan/vulkan.h" import Crafter.Graphics; import std; using namespace Crafter; namespace { int failures = 0; void Check(bool ok, std::string_view what) { std::println("{} {}", ok ? "PASS" : "FAIL", what); if (!ok) ++failures; } // Stamp the device identity the validator compares against. void SetDevice(std::uint32_t vendorID, std::uint32_t deviceID, const std::array& uuid) { Device::deviceProperties = {}; Device::deviceProperties.vendorID = vendorID; Device::deviceProperties.deviceID = deviceID; std::memcpy(Device::deviceProperties.pipelineCacheUUID, uuid.data(), VK_UUID_SIZE); } // Build a 32-byte VkPipelineCacheHeaderVersionOne blob, optionally with extra // trailing payload bytes (the real cache body). Fields are written little-endian // at fixed offsets, matching how the driver lays the header out on disk. std::vector MakeHeader(std::uint32_t headerSize, std::uint32_t headerVersion, std::uint32_t vendorID, std::uint32_t deviceID, const std::array& uuid, std::size_t trailing = 0) { std::vector blob(16 + VK_UUID_SIZE + trailing, std::byte{0xAB}); auto put = [&](std::size_t off, std::uint32_t v) { std::memcpy(blob.data() + off, &v, sizeof(v)); }; put(0, headerSize); put(4, headerVersion); put(8, vendorID); put(12, deviceID); std::memcpy(blob.data() + 16, uuid.data(), VK_UUID_SIZE); return blob; } constexpr std::uint32_t kVendor = 0x10DE; // NVIDIA constexpr std::uint32_t kDevice = 0x2204; // some GPU device id constexpr std::array kUuid = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10 }; constexpr std::uint32_t kV1 = VK_PIPELINE_CACHE_HEADER_VERSION_ONE; constexpr std::uint32_t kHeaderSize = 16 + VK_UUID_SIZE; // 32 } // namespace int main() { SetDevice(kVendor, kDevice, kUuid); // --- the happy path: a header written by this exact device -------------- { auto blob = MakeHeader(kHeaderSize, kV1, kVendor, kDevice, kUuid); Check(Device::PipelineCacheDataCompatible(blob), "matching vendor/device/UUID header is accepted"); auto withBody = MakeHeader(kHeaderSize, kV1, kVendor, kDevice, kUuid, /*trailing*/ 4096); Check(Device::PipelineCacheDataCompatible(withBody), "matching header followed by a cache body is accepted"); } // --- foreign / stale blobs must be rejected ----------------------------- { auto otherVendor = MakeHeader(kHeaderSize, kV1, 0x1002 /*AMD*/, kDevice, kUuid); Check(!Device::PipelineCacheDataCompatible(otherVendor), "different vendorID is rejected"); auto otherDevice = MakeHeader(kHeaderSize, kV1, kVendor, 0x9999, kUuid); Check(!Device::PipelineCacheDataCompatible(otherDevice), "different deviceID is rejected"); std::array otherUuid = kUuid; otherUuid[15] ^= 0xFF; // a driver update bumps the UUID auto staleUuid = MakeHeader(kHeaderSize, kV1, kVendor, kDevice, otherUuid); Check(!Device::PipelineCacheDataCompatible(staleUuid), "different pipelineCacheUUID (e.g. driver update) is rejected"); } // --- malformed headers -------------------------------------------------- { auto badVersion = MakeHeader(kHeaderSize, 0xDEAD, kVendor, kDevice, kUuid); Check(!Device::PipelineCacheDataCompatible(badVersion), "unknown headerVersion is rejected"); auto smallHeaderSize = MakeHeader(8, kV1, kVendor, kDevice, kUuid); Check(!Device::PipelineCacheDataCompatible(smallHeaderSize), "headerSize smaller than the 32-byte header is rejected"); Check(!Device::PipelineCacheDataCompatible({}), "empty blob (no file / cold start) is rejected"); std::vector truncated(20, std::byte{0}); Check(!Device::PipelineCacheDataCompatible(truncated), "blob too short to hold the header is rejected"); } // --- identity follows the active device --------------------------------- { // Re-stamp as a different device; a blob valid for the old one is now // foreign. Guards against the validator caching identity anywhere but // Device::deviceProperties. auto blob = MakeHeader(kHeaderSize, kV1, kVendor, kDevice, kUuid); SetDevice(0x8086 /*Intel*/, 0x1234, kUuid); Check(!Device::PipelineCacheDataCompatible(blob), "a blob from the previous device is rejected after the device changes"); SetDevice(kVendor, kDevice, kUuid); Check(Device::PipelineCacheDataCompatible(blob), "...and accepted again once the matching device is restored"); } if (failures != 0) { std::println("{} check(s) failed", failures); return EXIT_FAILURE; } std::println("all checks passed"); return EXIT_SUCCESS; }