From a728482731a5b419014ee3d41f132335e79b7892 Mon Sep 17 00:00:00 2001 From: catbot Date: Tue, 16 Jun 2026 18:23:25 +0000 Subject: [PATCH] test(pipeline): cover pipeline-cache header validation (#69) PipelineCacheValidation drives Device::PipelineCacheDataCompatible directly (no GPU): synthetic device identities + 32-byte cache headers assert that a matching header is accepted while foreign vendor/device, a bumped UUID (driver update), unknown version, undersized headerSize, and short/empty blobs are all rejected. Mirrors the GPU-free style of MemoryTypeFallback / UploadStrategy. Also gitignore the serialized pipeline_cache.bin a real-device test run drops in the working dir. Co-Authored-By: Claude Opus 4.8 --- .gitignore | 3 +- project.cpp | 29 +++++ tests/PipelineCacheValidation/main.cpp | 157 +++++++++++++++++++++++++ 3 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 tests/PipelineCacheValidation/main.cpp diff --git a/.gitignore b/.gitignore index 4e9c87a..b85ab43 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ build/ -bin/ \ No newline at end of file +bin/ +pipeline_cache.bin diff --git a/project.cpp b/project.cpp index 5cab540..d56ad86 100644 --- a/project.cpp +++ b/project.cpp @@ -587,6 +587,35 @@ extern "C" Configuration CrafterBuildProject(std::span a hitImpls.emplace_back("tests/InputFieldHitTest/main"); hc.GetInterfacesAndImplementations(ifaces, hitImpls); cfg.tests.push_back(std::move(hitTest)); + + // Issue #69: the engine feeds one shared Device::pipelineCache to every + // vkCreate*Pipelines call and persists it across runs, discarding an + // on-disk blob whose header doesn't match the current GPU. That gate — + // Device::PipelineCacheDataCompatible — is pure logic over the standard + // VkPipelineCache header and Device::deviceProperties, so this test + // stamps synthetic device identities + headers and drives it directly, + // no GPU device needed at runtime. + Test cacheTest; + Configuration& cc = cacheTest.config; + cc.path = cfg.path; + cc.name = "PipelineCacheValidation"; + cc.outputName = "PipelineCacheValidation"; + cc.type = ConfigurationType::Executable; + cc.target = cfg.target; + cc.march = cfg.march; + cc.mtune = cfg.mtune; + cc.debug = cfg.debug; + cc.sysroot = cfg.sysroot; + cc.dependencies = cfg.dependencies; + cc.externalDependencies = cfg.externalDependencies; + cc.compileFlags = cfg.compileFlags; + cc.linkFlags = cfg.linkFlags; + cc.defines = cfg.defines; + cc.cFiles = cfg.cFiles; + std::vector cacheImpls(impls.begin(), impls.end()); + cacheImpls.emplace_back("tests/PipelineCacheValidation/main"); + cc.GetInterfacesAndImplementations(ifaces, cacheImpls); + cfg.tests.push_back(std::move(cacheTest)); } return cfg; diff --git a/tests/PipelineCacheValidation/main.cpp b/tests/PipelineCacheValidation/main.cpp new file mode 100644 index 0000000..a55b693 --- /dev/null +++ b/tests/PipelineCacheValidation/main.cpp @@ -0,0 +1,157 @@ +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +// 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; +}