test(agc): cover unlocked host import retirement during mapping

This commit is contained in:
jweidenh 2026-10-11 17:19:24 -04:00 committed by blawesom
parent 4bf963d660
commit c4faf55fc4
3 changed files with 141 additions and 6 deletions

View File

@ -814,6 +814,8 @@ if(BUILD_TESTING)
set_tests_properties(agc_host_import_unmap PROPERTIES SKIP_RETURN_CODE 77)
add_test(NAME agc_parked_import_unmap COMMAND agc_driver_recorder_tests --parked-import-unmap-only)
set_tests_properties(agc_parked_import_unmap PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000")
add_test(NAME agc_host_import_retirement COMMAND agc_driver_recorder_tests --host-import-retirement-only)
set_tests_properties(agc_host_import_retirement PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
add_test(NAME agc_unit_shadow COMMAND agc_driver_recorder_tests --unit-shadow-only)
set_tests_properties(agc_unit_shadow PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000")
add_test(NAME agc_descriptor_pool_recycling COMMAND agc_driver_recorder_tests --descriptor-pool-only)
@ -825,7 +827,7 @@ if(BUILD_TESTING)
add_test(NAME agc_draw_inputs_in_place COMMAND agc_driver_recorder_tests --draw-inputs-in-place-only)
set_tests_properties(agc_draw_inputs_in_place PROPERTIES SKIP_RETURN_CODE 77)
if(WIN32)
set_tests_properties(agc_host_import_unmap agc_parked_import_unmap agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_multisample_guest_access agc_draw_inputs_in_place PROPERTIES ENVIRONMENT_MODIFICATION
set_tests_properties(agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_multisample_guest_access agc_draw_inputs_in_place PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()

View File

@ -141,6 +141,9 @@ public:
selectedRank = rank;
}
Require(context.physical != VK_NULL_HANDLE, "no Vulkan 1.1 device");
VkPhysicalDeviceProperties selected{};
physicalProperties(context.physical, &selected);
std::cout << "Vulkan device: " << selected.deviceName << '\n';
const auto extensions = function<PFN_vkEnumerateDeviceExtensionProperties>("vkEnumerateDeviceExtensionProperties");
Check(extensions(context.physical, nullptr, &count, nullptr), "vkEnumerateDeviceExtensionProperties");
std::vector<VkExtensionProperties> available(count);
@ -948,6 +951,129 @@ void remappedImportTests(const Device& device) {
HostImportFor(context, address, bytes);
}
void hostImportRetirementTests(const Device& device) {
const auto& context = device.GetContext();
Require(context.hostImportAlignment != 0 && context.hostImportAlignment <= 65536, "host import retirement requires host imports aligned within 64 KiB");
constexpr std::size_t UnitBytes = 65536;
constexpr std::size_t AllocationBytes = 4 * UnitBytes;
constexpr std::size_t CopyBytes = 256;
#ifdef _WIN32
void* block = VirtualAlloc(nullptr, AllocationBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
#else
void* block = std::aligned_alloc(UnitBytes, AllocationBytes);
#endif
Require(block != nullptr, "cannot allocate the host import retirement block");
const auto release = [](void* pointer) {
#ifdef _WIN32
VirtualFree(pointer, 0, MEM_RELEASE);
#else
std::free(pointer);
#endif
};
std::unique_ptr<void, decltype(release)> allocation(block, release);
auto* memory = static_cast<std::byte*>(block);
const auto address = reinterpret_cast<std::uint64_t>(block);
std::unique_lock gpu(GpuMutex());
PrepareImportWatch(context);
Recorder recorder(context);
recorder.Activate();
Buffer readback(context, CopyBytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT);
std::array<bool, 4> registered{};
const auto cleanup = [&] {
if (!gpu.owns_lock()) gpu.lock();
recorder.Sync();
recorder.FlushDeferredReleases();
ClearHostImports(context.device);
GuestAllocations::Mutation mutation;
for (std::size_t index = 0; index < registered.size(); ++index) {
if (!registered[index]) continue;
mutation.Remove(memory + index * UnitBytes);
registered[index] = false;
}
SetHostImportWindow(0);
};
const auto add = [&](std::size_t index) {
GuestAllocations::Mutation mutation;
mutation.Add(memory + index * UnitBytes, UnitBytes, true, true, true);
registered[index] = true;
};
const auto mapped = [&](std::size_t index) {
GuestAllocations::Mapped result;
for (const auto& range : GuestAllocations::GuestAllocationsAcquire_nid_postfix()) {
if (range->address == address + index * UnitBytes) result.push_back(range);
}
Require(result.size() == 1, "host import retirement lost its registered range");
return result;
};
bool deferredEveryRetirement = true;
bool preservedEveryRecorder = true;
try {
const std::array<const char*, 3> shapes{"same-base growth", "earlier-base overlap", "predecessor split"};
for (std::size_t shape = 0; shape < shapes.size(); ++shape) {
SetHostImportWindow(0);
Require(ImportMappedRanges(context, {}, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), true), "host import retirement could not adopt the device");
const std::size_t first = shape == 1 ? 1 : 0;
add(first);
if (shape == 2) add(1);
for (std::size_t index = 0; index < AllocationBytes; ++index) memory[index] = static_cast<std::byte>((index * 37u + shape * 19u + 11u) & 0xffu);
const auto initial = mapped(first);
const auto initialGeneration = GuestAllocations::GuestAllocationsGeneration_nid_postfix();
gpu.unlock();
const bool fresh = ImportMappedRanges(context, initial, initialGeneration, false);
const bool unchanged = ImportMappedRanges(context, initial, initialGeneration, false);
gpu.lock();
Require(fresh && unchanged, "host import retirement blocked a fresh or unchanged fast import");
const auto sourceAddress = address + (shape == 2 ? UnitBytes : first * UnitBytes);
const auto* imported = HostImportFor(context, sourceAddress, CopyBytes);
Require(imported != nullptr, "host import retirement could not import its source");
const auto source = imported->buffer;
const auto sourceOffset = sourceAddress - imported->base;
std::array<std::byte, CopyBytes> expected{};
std::memcpy(expected.data(), reinterpret_cast<const void*>(sourceAddress), CopyBytes);
recorder.Commands();
recorder.Submit();
device.WaitQueue();
Require(!recorder.Recording() && recorder.InFlightBatches() == 1, "host import retirement requires a submitted batch without an open batch");
std::size_t changed = 0;
if (shape == 0) {
changed = 1;
add(changed);
} else if (shape == 1) {
add(changed);
} else {
SetHostImportWindow(UnitBytes);
add(3);
changed = 1;
}
const auto ranges = mapped(changed);
const auto generation = GuestAllocations::GuestAllocationsGeneration_nid_postfix();
gpu.unlock();
const bool deferred = !ImportMappedRanges(context, ranges, generation, false);
gpu.lock();
const bool preserved = !recorder.Recording() && recorder.InFlightBatches() == 1;
const auto commands = recorder.Commands();
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, source, sourceOffset, readback.Handle(), 0, CopyBytes);
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT);
Require(ImportMappedRanges(context, ranges, generation, true), "host import retirement refused its retry under the GPU lock");
Require(HostImportFor(context, sourceAddress, CopyBytes) != nullptr, "host import retirement lost the replacement import");
recorder.Sync();
readback.Invalidate();
Require(std::equal(expected.begin(), expected.end(), readback.Bytes().begin()), "host import retirement changed bytes copied from the retired import");
std::cout << "host import retirement: " << shapes[shape] << " GPU readback verified; deferred=" << deferred << "; recorder preserved=" << preserved << '\n';
deferredEveryRetirement = deferredEveryRetirement && deferred;
preservedEveryRecorder = preservedEveryRecorder && preserved;
cleanup();
}
} catch (...) {
cleanup();
throw;
}
Require(deferredEveryRetirement, "unlocked mapping retired an import instead of requesting the GPU lock");
Require(preservedEveryRecorder, "unlocked mapping changed recorder batches");
std::cout << "Host import retirement locking and GPU readback tests passed\n";
}
void movedMetadataTests(const Device& device, Recorder& recorder) {
const auto& base = device.GetContext();
if (base.hostImportAlignment == 0) {
@ -5447,6 +5573,10 @@ int main(int argc, char** argv) {
return 77;
}
Device& device = *created;
if (argc == 2 && std::string_view(argv[1]) == "--host-import-retirement-only") {
hostImportRetirementTests(device);
return 0;
}
if (argc == 2 && std::string_view(argv[1]) == "--pipeline-library-only") {
pipelineLibraryTests(device);
return 0;

View File

@ -25,16 +25,18 @@ using AgcDriver::Graphics::Require;
constexpr std::size_t BlockBytes = 65536;
constexpr std::size_t AllocationBytes = BlockBytes * 4;
constexpr std::size_t ReservationBytes = AllocationBytes + BlockBytes * 2;
class GuestAllocation {
public:
GuestAllocation() {
#ifdef _WIN32
memory = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, AllocationBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
allocation = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, ReservationBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
#else
memory = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, AllocationBytes));
allocation = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, ReservationBytes));
#endif
Require(memory != nullptr, "host import at map: cannot allocate the guest memory");
Require(allocation != nullptr, "host import at map: cannot allocate the guest memory");
memory = allocation + BlockBytes;
std::memset(memory, 0, AllocationBytes);
Register();
}
@ -42,9 +44,9 @@ public:
~GuestAllocation() {
if (registered) Unregister();
#ifdef _WIN32
VirtualFree(memory, 0, MEM_RELEASE);
VirtualFree(allocation, 0, MEM_RELEASE);
#else
std::free(memory);
std::free(allocation);
#endif
}
@ -73,6 +75,7 @@ public:
}
private:
std::uint8_t* allocation = nullptr;
std::uint8_t* memory = nullptr;
bool registered = false;
};