diff --git a/app/src/main/cpp/framegen/ddfg/framegen.cpp b/app/src/main/cpp/framegen/ddfg/framegen.cpp index 82d0a764b..5200f6b09 100644 --- a/app/src/main/cpp/framegen/ddfg/framegen.cpp +++ b/app/src/main/cpp/framegen/ddfg/framegen.cpp @@ -184,8 +184,9 @@ bool FrameGen::onResize(VkExtent2D extent, VkFormat , bool force) { VkExtent2D e = levelExtent(extent, l); if (!makeImage(pyrA_[l], e, VK_FORMAT_R32_SFLOAT, lumaUsage)) return false; if (!makeImage(pyrB_[l], e, VK_FORMAT_R32_SFLOAT, lumaUsage)) return false; - if (!makeImage(flowLvl_[l], e, VK_FORMAT_R16G16B16A16_SFLOAT, flowUsage)) return false; } + for (int l = flowFinest_; l < kLevels; ++l) + if (!makeImage(flowLvl_[l], levelExtent(extent, l), VK_FORMAT_R16G16B16A16_SFLOAT, flowUsage)) return false; pyrAs_.resize(kLevels); for (int l = flowFinest_; l < kLevels; ++l) if (!makeImage(pyrAs_[l], levelExtent(extent, l), VK_FORMAT_R32_SFLOAT, lumaUsage)) return false; diff --git a/app/src/main/cpp/framegen/ddfg/record_impl.inc b/app/src/main/cpp/framegen/ddfg/record_impl.inc index 554c05c7f..734d94cdc 100644 --- a/app/src/main/cpp/framegen/ddfg/record_impl.inc +++ b/app/src/main/cpp/framegen/ddfg/record_impl.inc @@ -439,7 +439,7 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev, makeUbo(dd, dev, mp, sizeof(FlowRegUBO), S.flowRegUbo, S.flowRegUboMem, S.flowRegUboPtr); if (S.flowRegUboPtr) { FlowRegUBO fu{cfg.frDt, cfg.frLambda, cfg.frEdge, cfg.frEps}; std::memcpy(S.flowRegUboPtr, &fu, sizeof(fu)); } S.flowUbo.resize(kLevels); S.flowUboMem.resize(kLevels); S.flowUboPtr.resize(kLevels); - for (int l = 0; l < kLevels; ++l) { + for (int l = fg->flowFinest(); l < kLevels; ++l) { makeUbo(dd, dev, mp, sizeof(FlowUBO), S.flowUbo[l], S.flowUboMem[l], S.flowUboPtr[l]); if (S.flowUboPtr[l]) { FlowUBO u{cfg.flowScale, (uint32_t)l, 0.6f, 1.6f}; std::memcpy(S.flowUboPtr[l], &u, sizeof(u)); } } @@ -464,13 +464,12 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev, } S.sFlow.resize(kLevels); - for (int l = 0; l < kLevels; ++l) { + for (int l = fg->flowFinest(); l < kLevels; ++l) { S.sFlow[l] = allocSet(dd, dev, pool, pFlow.setLayout); int pred = (l + 1 < kLevels) ? l + 1 : l; - VkImageView currLvl = (l >= fg->flowFinest()) ? pyrAs[l].view : pyrA[l].view; wUbo(dd, dev, S.sFlow[l], 0, S.flowUbo[l], sizeof(FlowUBO)); wImg(dd, dev, S.sFlow[l], 32, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, pyrB[l].view, samp, GEN); - wImg(dd, dev, S.sFlow[l], 33, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, currLvl, samp, GEN); + wImg(dd, dev, S.sFlow[l], 33, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, pyrAs[l].view, samp, GEN); wImg(dd, dev, S.sFlow[l], 34, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, flowLvl[pred].view, samp, GEN); wImg(dd, dev, S.sFlow[l], 48, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, flowLvl[l].view, VK_NULL_HANDLE, GEN); } diff --git a/docs/development/dd-fg.md b/docs/development/dd-fg.md new file mode 100644 index 000000000..81cb43a9e --- /dev/null +++ b/docs/development/dd-fg.md @@ -0,0 +1,38 @@ +# DD-FG flow resources + +DD-FG keeps seven luminance pyramid levels for both input frames. Its performance +preset chooses the finest level at which optical flow is solved: + +| Preset | Finest solved level | Allocated flow levels | +|---|---:|---| +| 0 | 1 | 1 through 6 | +| 1 | 2 | 2 through 6 | +| 2 (the app default) | 3 | 3 through 6 | + +Only those solved levels need RGBA16F flow images, flow UBOs and flow descriptor +sets. The vectors keep their original level indices; unused entries are null. +The full luminance pyramids, full-resolution expanded flow images, regularization +and synthesis are unchanged. + +At the default preset this avoids three flow images, three uniform buffers and +three descriptor sets. The removed image payload is +`8 * (width * height + (width >> 1) * (height >> 1) + (width >> 2) * (height >> 2))` +bytes for dimensions at least four pixels: + +| Input size | Removed image payload | +|---|---:| +| 1280 x 720 | 9,676,800 bytes | +| 1920 x 1080 | 21,772,800 bytes | +| 2560 x 1440 | 38,707,200 bytes | + +These are pixel payloads, not driver allocation sizes or measured FPS gains. + +## Validation + +Run `python3 -m unittest discover -s tools/tests -p 'test_ddfg_flow_levels.py'`. +The host test needs a C++17 compiler and system Vulkan headers or an Android NDK +selected through the usual SDK/NDK environment variables. It compiles the actual +DD-FG chain against a mock Vulkan dispatch table and checks resource counts, +dispatch order and sizes, valid descriptor bindings, both models, every preset, +2x/3x/4x generation, small/odd extents, resize and live preset changes. +It does not execute shaders on a GPU or replace on-device validation. diff --git a/tools/tests/ddfg_flow_levels_harness.cpp b/tools/tests/ddfg_flow_levels_harness.cpp new file mode 100644 index 000000000..e5cf7bf70 --- /dev/null +++ b/tools/tests/ddfg_flow_levels_harness.cpp @@ -0,0 +1,359 @@ +#include "framegen.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr int levels = 7; +constexpr VkImageUsageFlags flow_usage = + VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; +enum Pipeline { LUMA, DOWNSAMPLE, FLOW3, FLOW4, EXPAND3, EXPAND4, GLOBAL_MOTION, PREWARP, FLOW_REG, SYNTH }; +struct Image { VkExtent3D extent; VkFormat format; VkImageUsageFlags usage; }; +struct Allocation { VkDeviceSize size; std::unique_ptr data; }; +struct Binding { VkDescriptorType type; VkImageView view; VkBuffer buffer; VkDeviceSize range; }; +struct Dispatch { unsigned pipeline; uint32_t x, y, z; }; +std::map images; +std::map views; +std::map buffers; +std::map allocations; +std::map buffer_memory; +std::map> descriptors; +std::map pipelines; +std::map image_ids; +std::map buffer_ids; +std::vector dispatches; +uintptr_t serial = 100; +unsigned buffer_creates, set_creates; +VkPipeline bound_pipeline; +VkDescriptorSet bound_set; +uint64_t command_hash = 14695981039346656037ull; + +template T handle() { return reinterpret_cast(++serial); } + +void hash(uint64_t value) { + for (unsigned i = 0; i < 8; i++) { + command_hash = (command_hash ^ (value & 255)) * 1099511628211ull; + value >>= 8; + } +} + +void hash_image(VkImage image) { + assert(images.count(image)); + if (!image_ids.count(image)) image_ids[image] = static_cast(image_ids.size() + 1); + const auto &info = images.at(image); + hash(image_ids.at(image)); + hash(info.extent.width); hash(info.extent.height); hash(info.format); hash(info.usage); +} + +template +VkResult VKAPI_CALL create(VkDevice, const Info *, const VkAllocationCallbacks *, Out *out) { + *out = handle(); + return VK_SUCCESS; +} +template void VKAPI_CALL destroy(VkDevice, T, const VkAllocationCallbacks *) {} + +VkResult VKAPI_CALL create_image(VkDevice, const VkImageCreateInfo *info, + const VkAllocationCallbacks *, VkImage *out) { + *out = handle(); + images[*out] = {info->extent, info->format, info->usage}; + return VK_SUCCESS; +} +VkResult VKAPI_CALL create_view(VkDevice, const VkImageViewCreateInfo *info, + const VkAllocationCallbacks *, VkImageView *out) { + assert(images.count(info->image)); + *out = handle(); + views[*out] = info->image; + return VK_SUCCESS; +} +void VKAPI_CALL destroy_image(VkDevice, VkImage image, const VkAllocationCallbacks *) { + assert(images.erase(image) == 1); +} +void VKAPI_CALL destroy_view(VkDevice, VkImageView view, const VkAllocationCallbacks *) { + assert(views.erase(view) == 1); +} +VkResult VKAPI_CALL create_buffer(VkDevice, const VkBufferCreateInfo *info, + const VkAllocationCallbacks *, VkBuffer *out) { + *out = handle(); + buffers[*out] = *info; + buffer_creates++; + return VK_SUCCESS; +} +void VKAPI_CALL destroy_buffer(VkDevice, VkBuffer buffer, const VkAllocationCallbacks *) { + assert(buffers.erase(buffer) == 1); + buffer_memory.erase(buffer); +} +void VKAPI_CALL image_requirements(VkDevice, VkImage image, VkMemoryRequirements *out) { + const auto &info = images.at(image); + const uint64_t bytes = info.format == VK_FORMAT_R32_SFLOAT ? 4 : 8; + *out = {uint64_t(info.extent.width) * info.extent.height * bytes, 1, 1}; +} +void VKAPI_CALL buffer_requirements(VkDevice, VkBuffer buffer, VkMemoryRequirements *out) { + *out = {buffers.at(buffer).size, 1, 1}; +} +VkResult VKAPI_CALL allocate(VkDevice, const VkMemoryAllocateInfo *info, + const VkAllocationCallbacks *, VkDeviceMemory *out) { + *out = handle(); + allocations[*out].size = info->allocationSize; + return VK_SUCCESS; +} +void VKAPI_CALL free_memory(VkDevice, VkDeviceMemory memory, const VkAllocationCallbacks *) { + assert(allocations.erase(memory) == 1); +} +VkResult VKAPI_CALL bind_image(VkDevice, VkImage image, VkDeviceMemory memory, VkDeviceSize) { + assert(images.count(image) && allocations.count(memory)); + return VK_SUCCESS; +} +VkResult VKAPI_CALL bind_buffer(VkDevice, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize) { + assert(buffers.count(buffer) && allocations.count(memory)); + buffer_memory[buffer] = memory; + return VK_SUCCESS; +} +VkResult VKAPI_CALL map_memory(VkDevice, VkDeviceMemory memory, VkDeviceSize offset, + VkDeviceSize size, VkMemoryMapFlags, void **out) { + auto &allocation = allocations.at(memory); + assert(offset == 0 && size <= allocation.size); + allocation.data = std::make_unique(allocation.size); + *out = allocation.data.get(); + return VK_SUCCESS; +} +void VKAPI_CALL unmap(VkDevice, VkDeviceMemory memory) { assert(allocations.count(memory)); } +VkResult VKAPI_CALL idle(VkDevice) { return VK_SUCCESS; } +void VKAPI_CALL memory_properties(VkPhysicalDevice, VkPhysicalDeviceMemoryProperties *out) { + *out = {}; + out->memoryTypeCount = 1; + out->memoryTypes[0].propertyFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; +} +VkResult VKAPI_CALL allocate_sets(VkDevice, const VkDescriptorSetAllocateInfo *info, VkDescriptorSet *out) { + for (uint32_t i = 0; i < info->descriptorSetCount; i++) { + out[i] = handle(); + descriptors[out[i]] = {}; + set_creates++; + } + return VK_SUCCESS; +} +VkResult VKAPI_CALL reset_pool(VkDevice, VkDescriptorPool, VkDescriptorPoolResetFlags) { + descriptors.clear(); + return VK_SUCCESS; +} +void VKAPI_CALL update_sets(VkDevice, uint32_t count, const VkWriteDescriptorSet *writes, + uint32_t, const VkCopyDescriptorSet *) { + for (uint32_t i = 0; i < count; i++) { + const auto &write = writes[i]; + assert(descriptors.count(write.dstSet) && write.descriptorCount == 1); + Binding binding{write.descriptorType, VK_NULL_HANDLE, VK_NULL_HANDLE, 0}; + if (write.pImageInfo) { + binding.view = write.pImageInfo->imageView; + assert(views.count(binding.view)); + } else { + binding.buffer = write.pBufferInfo->buffer; + binding.range = write.pBufferInfo->range; + assert(buffers.count(binding.buffer)); + } + descriptors.at(write.dstSet)[write.dstBinding] = binding; + } +} +VkResult VKAPI_CALL create_pipelines(VkDevice, VkPipelineCache, uint32_t count, + const VkComputePipelineCreateInfo *, const VkAllocationCallbacks *, + VkPipeline *out) { + for (uint32_t i = 0; i < count; i++) { + const auto index = static_cast(pipelines.size()); + out[i] = handle(); + pipelines[out[i]] = index; + } + return VK_SUCCESS; +} +void VKAPI_CALL bind_pipeline(VkCommandBuffer, VkPipelineBindPoint, VkPipeline pipeline) { + assert(pipelines.count(pipeline)); + bound_pipeline = pipeline; +} +void VKAPI_CALL bind_sets(VkCommandBuffer, VkPipelineBindPoint, VkPipelineLayout, + uint32_t, uint32_t count, const VkDescriptorSet *sets, + uint32_t, const uint32_t *) { + assert(count == 1 && descriptors.count(sets[0])); + bound_set = sets[0]; +} +void VKAPI_CALL dispatch(VkCommandBuffer, uint32_t x, uint32_t y, uint32_t z) { + const unsigned pipeline = pipelines.at(bound_pipeline); + dispatches.push_back({pipeline, x, y, z}); + hash(1); hash(pipeline); hash(x); hash(y); hash(z); + for (const auto &[slot, binding] : descriptors.at(bound_set)) { + hash(slot); hash(binding.type); + if (binding.view) { + hash_image(views.at(binding.view)); + } else { + if (!buffer_ids.count(binding.buffer)) + buffer_ids[binding.buffer] = static_cast(buffer_ids.size() + 1); + const auto &allocation = allocations.at(buffer_memory.at(binding.buffer)); + const auto size = binding.range == VK_WHOLE_SIZE ? allocation.size : binding.range; + assert(allocation.data && size <= allocation.size); + hash(buffer_ids.at(binding.buffer)); hash(size); + for (VkDeviceSize i = 0; i < size; i++) hash(allocation.data[i]); + } + } +} +void VKAPI_CALL barrier(VkCommandBuffer, VkPipelineStageFlags src, VkPipelineStageFlags dst, + VkDependencyFlags flags, uint32_t nm, const VkMemoryBarrier *memory, + uint32_t nb, const VkBufferMemoryBarrier *, uint32_t ni, + const VkImageMemoryBarrier *image) { + assert(nb == 0); + hash(2); hash(src); hash(dst); hash(flags); hash(nm); hash(ni); + for (uint32_t i = 0; i < nm; i++) { + hash(memory[i].srcAccessMask); hash(memory[i].dstAccessMask); + } + for (uint32_t i = 0; i < ni; i++) { + hash_image(image[i].image); + hash(image[i].oldLayout); hash(image[i].newLayout); + hash(image[i].srcAccessMask); hash(image[i].dstAccessMask); + hash(image[i].srcQueueFamilyIndex); hash(image[i].dstQueueFamilyIndex); + } +} +void VKAPI_CALL clear_image(VkCommandBuffer, VkImage image, VkImageLayout layout, + const VkClearColorValue *color, uint32_t count, const VkImageSubresourceRange *) { + assert(count == 1); + hash(3); hash_image(image); hash(layout); + for (unsigned i = 0; i < 4; i++) hash(color->uint32[i]); +} +void VKAPI_CALL copy_image(VkCommandBuffer, VkImage src, VkImageLayout sl, VkImage dst, + VkImageLayout dl, uint32_t count, const VkImageCopy *region) { + assert(count == 1); + hash(4); hash_image(src); hash(sl); hash_image(dst); hash(dl); + hash(region->extent.width); hash(region->extent.height); hash(region->extent.depth); +} + +uint32_t dimension(uint32_t full, int level) { return std::max(full >> level, 1u); } + +void check_images(uint32_t width, uint32_t height, int finest) { + assert(images.size() == static_cast(32 - 2 * finest)); + for (int level = finest; level < levels; level++) { + const auto w = dimension(width, level), h = dimension(height, level); + assert(std::any_of(images.begin(), images.end(), [=](const auto &entry) { + const auto &image = entry.second; + return image.format == VK_FORMAT_R16G16B16A16_SFLOAT && image.usage == flow_usage && + image.extent.width == w && image.extent.height == h; + })); + } +} + +VkImageView external_image(uint32_t width, uint32_t height) { + VkImage image = handle(); + VkImageView view = handle(); + images[image] = {{width, height, 1}, VK_FORMAT_R8G8B8A8_UNORM, + VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT}; + views[view] = image; + return view; +} + +void record(ddfg::FrameGen &fg, int model, uint32_t width, uint32_t height, unsigned generations) { + const auto previous = external_image(width, height), current = external_image(width, height); + const auto first = dispatches.size(); + for (unsigned i = 0; i < generations; i++) + fg.record(reinterpret_cast(1), previous, current, external_image(width, height), + float(i + 1) / float(generations + 1), 0, i, i != 0); + std::vector expected; + auto add = [&](unsigned pipeline, int level) { + expected.push_back({pipeline, (dimension(width, level) + 15) / 16, + (dimension(height, level) + 15) / 16, 1}); + }; + add(LUMA, 0); add(LUMA, 0); + for (int level = 1; level < levels; level++) { add(DOWNSAMPLE, level); add(DOWNSAMPLE, level); } + expected.push_back({GLOBAL_MOTION, 8, 1, 1}); + for (int level = fg.flowFinest(); level < levels; level++) add(PREWARP, level); + for (int level = levels - 1; level >= fg.flowFinest(); level--) add(model == 4 ? FLOW4 : FLOW3, level); + for (int i = 0; i < 4; i++) add(FLOW_REG, fg.flowFinest()); + add(model == 4 ? EXPAND4 : EXPAND3, 0); + for (unsigned i = 0; i < generations; i++) add(SYNTH, 0); + assert(dispatches.size() - first == expected.size()); + for (size_t i = 0; i < expected.size(); i++) { + const auto &actual = dispatches[first + i], &want = expected[i]; + assert(actual.pipeline == want.pipeline && actual.x == want.x && actual.y == want.y && actual.z == want.z); + } + for (auto it = views.begin(); it != views.end();) { + if (images.at(it->second).format == VK_FORMAT_R8G8B8A8_UNORM) { + images.erase(it->second); + it = views.erase(it); + } else ++it; + } +} + +} + +int main(int argc, char **argv) { + assert(argc == 8); + const std::string mode = argv[1]; + const int model = std::atoi(argv[2]), preset = std::atoi(argv[3]); + uint32_t width = static_cast(std::atoi(argv[4])); + uint32_t height = static_cast(std::atoi(argv[5])); + const unsigned generations = static_cast(std::atoi(argv[6])); + const unsigned repeats = static_cast(std::atoi(argv[7])); + assert((model == 3 || model == 4) && preset >= 0 && preset <= 2); + assert(width && height && generations >= 1 && generations <= 3 && repeats > 0); + ddfg::DeviceDispatch dd; + ddfg::InstanceDispatch id; + id.GetPhysicalDeviceMemoryProperties = memory_properties; + dd.CreateImage = create_image; dd.DestroyImage = destroy_image; + dd.CreateImageView = create_view; dd.DestroyImageView = destroy_view; + dd.CreateBuffer = create_buffer; dd.DestroyBuffer = destroy_buffer; + dd.GetImageMemoryRequirements = image_requirements; dd.GetBufferMemoryRequirements = buffer_requirements; + dd.AllocateMemory = allocate; dd.FreeMemory = free_memory; + dd.BindImageMemory = bind_image; dd.BindBufferMemory = bind_buffer; + dd.MapMemory = map_memory; dd.UnmapMemory = unmap; dd.DeviceWaitIdle = idle; + dd.CreateSampler = create; dd.DestroySampler = destroy; + dd.CreateShaderModule = create; + dd.DestroyShaderModule = destroy; + dd.CreateDescriptorSetLayout = create; + dd.DestroyDescriptorSetLayout = destroy; + dd.CreatePipelineLayout = create; + dd.DestroyPipelineLayout = destroy; + dd.CreateDescriptorPool = create; + dd.DestroyDescriptorPool = destroy; dd.ResetDescriptorPool = reset_pool; + dd.AllocateDescriptorSets = allocate_sets; dd.UpdateDescriptorSets = update_sets; + dd.CreateComputePipelines = create_pipelines; dd.DestroyPipeline = destroy; + dd.CmdBindPipeline = bind_pipeline; dd.CmdBindDescriptorSets = bind_sets; dd.CmdDispatch = dispatch; + dd.CmdPipelineBarrier = barrier; dd.CmdClearColorImage = clear_image; dd.CmdCopyImage = copy_image; + ddfg::FrameGen fg; + assert(fg.init(&dd, &id, reinterpret_cast(1), reinterpret_cast(2), + 0, reinterpret_cast(3))); + ddfg::Config cfg; + cfg.enabled = true; cfg.model = model; cfg.perfPreset = preset; cfg.flowScale = 0.6f; + fg.configure(cfg); + assert(fg.onResize({width, height}, VK_FORMAT_R8G8B8A8_UNORM)); + assert(mode == "resources" || mode == "record" || mode == "resize" || mode == "preset"); + for (unsigned repeat = 0; repeat < repeats; repeat++) { + const auto old_buffers = buffer_creates, old_sets = set_creates; + if (mode != "record") check_images(width, height, fg.flowFinest()); + record(fg, model, width, height, generations); + if (mode != "record") { + assert(buffer_creates - old_buffers == static_cast(15 - fg.flowFinest())); + assert(set_creates - old_sets == static_cast(54 - 4 * fg.flowFinest())); + } + if (repeat + 1 == repeats) break; + if (mode == "resize") { + width += 17; height += 9; + assert(fg.onResize({width, height}, VK_FORMAT_R8G8B8A8_UNORM)); + } else if (mode == "preset") { + cfg.perfPreset = (cfg.perfPreset + 1) % 3; + fg.configure(cfg); + } else { + assert(mode == "record"); + } + } + uint64_t bytes = 0; + for (const auto &[image, info] : images) { + (void)image; + bytes += uint64_t(info.extent.width) * info.extent.height * + (info.format == VK_FORMAT_R32_SFLOAT ? 4 : 8); + } + std::printf("PASS images=%zu bytes=%llu buffers=%u sets=%u dispatches=%zu trace=%016llx\n", + images.size(), static_cast(bytes), buffer_creates, set_creates, + dispatches.size(), static_cast(command_hash)); + fg.destroy(); + assert(images.empty() && views.empty()); +} diff --git a/tools/tests/test_ddfg_flow_levels.py b/tools/tests/test_ddfg_flow_levels.py new file mode 100644 index 000000000..d5efd6355 --- /dev/null +++ b/tools/tests/test_ddfg_flow_levels.py @@ -0,0 +1,102 @@ +import os +from pathlib import Path +import re +import shutil +import subprocess +import tempfile +import unittest + + +ROOT = Path(__file__).resolve().parents[2] +CHAIN = ROOT / "app/src/main/cpp/framegen/ddfg" + + +class DdfgFlowLevelsTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + compiler = shutil.which(os.environ.get("CXX", "c++")) + if compiler is None: + raise unittest.SkipTest("A host C++ compiler is required") + candidates = [Path("/usr/include/vulkan"), Path("/opt/homebrew/include/vulkan")] + for key in ("ANDROID_NDK_HOME", "ANDROID_NDK_ROOT", "NDK_HOME"): + if os.environ.get(key): + candidates.extend(Path(os.environ[key]).glob("toolchains/llvm/prebuilt/*/sysroot/usr/include/vulkan")) + for sdk in {Path(os.environ.get("ANDROID_HOME", "/nonexistent")), + Path(os.environ.get("ANDROID_SDK_ROOT", "/nonexistent")), + Path.home() / "Library/Android/sdk", Path.home() / "Android/Sdk"}: + candidates.extend(sdk.glob("ndk/*/toolchains/llvm/prebuilt/*/sysroot/usr/include/vulkan")) + headers = next((p for p in candidates if (p / "vulkan.h").is_file()), None) + if headers is None: + raise unittest.SkipTest("Vulkan headers from the Android NDK or system SDK are required") + cls.temp = tempfile.TemporaryDirectory(prefix="droiddeck-ddfg-levels-") + cls.addClassCleanup(cls.temp.cleanup) + work = Path(cls.temp.name) + shutil.copytree(headers, work / "include/vulkan", ignore=shutil.ignore_patterns("*.hpp")) + if (headers.parent / "vk_video").is_dir(): + shutil.copytree(headers.parent / "vk_video", work / "include/vk_video") + cls.binary = work / "ddfg-flow-levels" + build = subprocess.run([ + compiler, "-std=c++17", "-O2", "-Wall", "-Wextra", + "-Wno-missing-field-initializers", "-Wno-misleading-indentation", + "-I" + str(work / "include"), "-I" + str(CHAIN), + str(ROOT / "tools/tests/ddfg_flow_levels_harness.cpp"), str(CHAIN / "framegen.cpp"), + "-o", str(cls.binary), + ], capture_output=True, text=True, timeout=120) + if build.returncode: + raise RuntimeError(build.stdout + build.stderr) + + def run_case(self, mode="resources", model=4, preset=2, width=1280, height=720, generations=1, repeats=1): + result = subprocess.run([ + str(self.binary), mode, str(model), str(preset), str(width), str(height), + str(generations), str(repeats), + ], capture_output=True, text=True, timeout=10) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertTrue(result.stdout.startswith("PASS "), result.stdout) + return dict(re.findall(r"(\w+)=(\w+)", result.stdout)) + + def test_each_model_and_preset_allocates_only_solved_flow_levels(self): + for model in (3, 4): + for preset, finest in enumerate((1, 2, 3)): + with self.subTest(model=model, preset=preset): + values = self.run_case(model=model, preset=preset) + self.assertEqual(int(values["images"]), 32 - 2 * finest) + self.assertEqual(int(values["buffers"]), 18 - finest) + self.assertEqual(int(values["sets"]), 54 - 4 * finest) + + def test_default_preset_saves_the_exact_high_resolution_flow_payload(self): + for width, height in ((1280, 720), (1920, 1080), (2560, 1440)): + with self.subTest(width=width, height=height): + values = self.run_case(width=width, height=height) + pixels = lambda level: max(width >> level, 1) * max(height >> level, 1) + luma_bytes = 4 * (2 * sum(pixels(l) for l in range(7)) + + sum(pixels(l) for l in range(3, 7))) + flow_bytes = 8 * (sum(pixels(l) for l in range(3, 7)) + + 2 * width * height + 2 * pixels(3)) + self.assertEqual(int(values["bytes"]), luma_bytes + flow_bytes) + + def test_small_and_odd_extents_keep_valid_bindings_and_dispatch_sizes(self): + for width, height in ((1, 1), (17, 9), (135, 73)): + for preset in range(3): + with self.subTest(width=width, height=height, preset=preset): + self.run_case(width=width, height=height, preset=preset) + + def test_two_three_and_four_times_generation_keep_the_same_dispatch_schedule(self): + for model in (3, 4): + for preset in range(3): + for generations in (1, 2, 3): + with self.subTest(model=model, preset=preset, generations=generations): + self.run_case("record", model=model, preset=preset, generations=generations, repeats=3) + + def test_resize_rebuilds_every_solved_level(self): + for preset in range(3): + with self.subTest(preset=preset): + self.run_case("resize", preset=preset, generations=3, repeats=3) + + def test_live_preset_changes_rebuild_the_level_range(self): + for model in (3, 4): + with self.subTest(model=model): + self.run_case("preset", model=model, generations=3, repeats=4) + + +if __name__ == "__main__": + unittest.main()