perf: allocate only solved DD-FG flow levels (#1556)

The flow solver starts at flowFinest_, but its images, uniform buffers and descriptor sets were allocated for every pyramid level. Keep the seven-level indices and allocate flow resources only where the solver reads and writes them.

The default preset avoids three unused RGBA16F images, saving 38,707,200 bytes of pixel payload at 2560x1440 without changing the shaders or recorded GPU work. Add native allocation/dispatch regressions and document the resource budgets.
This commit is contained in:
Thomas Lee authored and GitHub committed 2026-10-25 19:21:40 -04:00
1 parent 71fee6b3cc
commit 4922d309c8
5 files changed
+504 -5

No files matched your search

+2 -1
View File
@@ -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;
@@ -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);
}
+38
View File
@@ -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.
+359
View File
@@ -0,0 +1,359 @@
#include "framegen.hpp"
#include <algorithm>
#include <cassert>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <map>
#include <memory>
#include <string>
#include <vector>
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<uint8_t[]> data; };
struct Binding { VkDescriptorType type; VkImageView view; VkBuffer buffer; VkDeviceSize range; };
struct Dispatch { unsigned pipeline; uint32_t x, y, z; };
std::map<VkImage, Image> images;
std::map<VkImageView, VkImage> views;
std::map<VkBuffer, VkBufferCreateInfo> buffers;
std::map<VkDeviceMemory, Allocation> allocations;
std::map<VkBuffer, VkDeviceMemory> buffer_memory;
std::map<VkDescriptorSet, std::map<uint32_t, Binding>> descriptors;
std::map<VkPipeline, unsigned> pipelines;
std::map<VkImage, unsigned> image_ids;
std::map<VkBuffer, unsigned> buffer_ids;
std::vector<Dispatch> dispatches;
uintptr_t serial = 100;
unsigned buffer_creates, set_creates;
VkPipeline bound_pipeline;
VkDescriptorSet bound_set;
uint64_t command_hash = 14695981039346656037ull;
template<class T> T handle() { return reinterpret_cast<T>(++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<unsigned>(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<class Info, class Out>
VkResult VKAPI_CALL create(VkDevice, const Info *, const VkAllocationCallbacks *, Out *out) {
*out = handle<Out>();
return VK_SUCCESS;
}
template<class T> void VKAPI_CALL destroy(VkDevice, T, const VkAllocationCallbacks *) {}
VkResult VKAPI_CALL create_image(VkDevice, const VkImageCreateInfo *info,
const VkAllocationCallbacks *, VkImage *out) {
*out = handle<VkImage>();
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<VkImageView>();
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<VkBuffer>();
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<VkDeviceMemory>();
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<uint8_t[]>(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<VkDescriptorSet>();
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<unsigned>(pipelines.size());
out[i] = handle<VkPipeline>();
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<unsigned>(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<size_t>(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<VkImage>();
VkImageView view = handle<VkImageView>();
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<VkCommandBuffer>(1), previous, current, external_image(width, height),
float(i + 1) / float(generations + 1), 0, i, i != 0);
std::vector<Dispatch> 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<uint32_t>(std::atoi(argv[4]));
uint32_t height = static_cast<uint32_t>(std::atoi(argv[5]));
const unsigned generations = static_cast<unsigned>(std::atoi(argv[6]));
const unsigned repeats = static_cast<unsigned>(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<VkSamplerCreateInfo, VkSampler>; dd.DestroySampler = destroy<VkSampler>;
dd.CreateShaderModule = create<VkShaderModuleCreateInfo, VkShaderModule>;
dd.DestroyShaderModule = destroy<VkShaderModule>;
dd.CreateDescriptorSetLayout = create<VkDescriptorSetLayoutCreateInfo, VkDescriptorSetLayout>;
dd.DestroyDescriptorSetLayout = destroy<VkDescriptorSetLayout>;
dd.CreatePipelineLayout = create<VkPipelineLayoutCreateInfo, VkPipelineLayout>;
dd.DestroyPipelineLayout = destroy<VkPipelineLayout>;
dd.CreateDescriptorPool = create<VkDescriptorPoolCreateInfo, VkDescriptorPool>;
dd.DestroyDescriptorPool = destroy<VkDescriptorPool>; dd.ResetDescriptorPool = reset_pool;
dd.AllocateDescriptorSets = allocate_sets; dd.UpdateDescriptorSets = update_sets;
dd.CreateComputePipelines = create_pipelines; dd.DestroyPipeline = destroy<VkPipeline>;
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<VkPhysicalDevice>(1), reinterpret_cast<VkDevice>(2),
0, reinterpret_cast<VkQueue>(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<unsigned>(15 - fg.flowFinest()));
assert(set_creates - old_sets == static_cast<unsigned>(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<unsigned long long>(bytes), buffer_creates, set_creates,
dispatches.size(), static_cast<unsigned long long>(command_hash));
fg.destroy();
assert(images.empty() && views.empty());
}
+102
View File
@@ -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()