summaryrefslogtreecommitdiff
path: root/src/framegen_context.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/framegen_context.cpp')
-rw-r--r--src/framegen_context.cpp809
1 files changed, 809 insertions, 0 deletions
diff --git a/src/framegen_context.cpp b/src/framegen_context.cpp
new file mode 100644
index 0000000..47ef48e
--- /dev/null
+++ b/src/framegen_context.cpp
@@ -0,0 +1,809 @@
+#include "framegen_context.hpp"
+#include "logging.hpp"
+
+#include <cmath>
+#include <cstring>
+#include <sstream>
+#include <stdexcept>
+#include <algorithm>
+#include <array>
+#include <cstdio>
+#include <string>
+#include <unordered_map>
+#include <unordered_set>
+
+namespace bionic_fg {
+
+// ─── Pass ────────────────────────────────────────────────────────────────────
+
+Pass::Pass(const vk::Device& dev,
+ VkDescriptorPool descPool,
+ int shaderIdx,
+ const std::vector<vk::DescriptorBinding>& bindings) {
+ if (shaderIdx < 0 || static_cast<size_t>(shaderIdx) >= embedded::kShaderRegistry.size())
+ throw std::runtime_error("Pass: shader index out of range");
+ const auto& blob = embedded::kShaderRegistry[static_cast<size_t>(shaderIdx)];
+ if (!embedded::IsValidSpirv(blob))
+ throw std::runtime_error(std::string("Pass: invalid SPIR-V for ") + blob.name);
+ layout_ = vk::DescriptorSetLayout(dev, bindings);
+ descSets_[0] = vk::DescriptorSet(dev, descPool, layout_.handle());
+ descSets_[1] = vk::DescriptorSet(dev, descPool, layout_.handle());
+ pipeline_ = vk::ComputePipeline(dev, blob.data, blob.size, layout_.handle());
+ BFG_LOGI("Pass: loaded %s", blob.name);
+}
+void Pass::destroy(const vk::Device& dev) {
+ pipeline_.destroy(dev);
+ layout_.destroy(dev);
+}
+void Pass::bindUBO(const vk::Device& dev, uint32_t b, const vk::Buffer& buf) {
+ for (auto& s : descSets_) s.bindUBO(dev, b, buf);
+}
+void Pass::bindSampled(const vk::Device& dev, uint32_t b, const vk::Image& img,
+ const vk::Sampler& sampler) {
+ for (auto& s : descSets_)
+ s.bindCombinedImageSampler(dev, b, img, sampler,
+ img.external() ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
+}
+void Pass::bindSampledGeneral(const vk::Device& dev, uint32_t b, const vk::Image& img,
+ const vk::Sampler& sampler) {
+ for (auto& s : descSets_)
+ s.bindCombinedImageSampler(dev, b, img, sampler, VK_IMAGE_LAYOUT_GENERAL);
+}
+void Pass::bindStorage(const vk::Device& dev, uint32_t b, const vk::Image& img) {
+ for (auto& s : descSets_) s.bindStorageImage(dev, b, img);
+}
+void Pass::bindSampledAt(const vk::Device& dev, uint32_t parity, uint32_t b,
+ const vk::Image& img, const vk::Sampler& sampler) {
+ descSets_[parity & 1].bindCombinedImageSampler(dev, b, img, sampler,
+ img.external() ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
+}
+void Pass::bindSampledGeneralAt(const vk::Device& dev, uint32_t parity, uint32_t b,
+ const vk::Image& img, const vk::Sampler& sampler) {
+ descSets_[parity & 1].bindCombinedImageSampler(dev, b, img, sampler, VK_IMAGE_LAYOUT_GENERAL);
+}
+void Pass::dispatch(VkCommandBuffer cmd, uint32_t gx, uint32_t gy, uint32_t parity) const {
+ pipeline_.bind(cmd);
+ pipeline_.bindDescriptorSet(cmd, descSets_[parity & 1].handle());
+ pipeline_.dispatch(cmd, gx, gy, 1);
+}
+
+// ─── Helpers ─────────────────────────────────────────────────────────────────
+
+static void computeBarrier(VkCommandBuffer cmd) {
+ VkMemoryBarrier mb{};
+ mb.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER;
+ mb.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT;
+ mb.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT;
+ vkCmdPipelineBarrier(cmd,
+ VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
+ VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
+ 0, 1, &mb, 0, nullptr, 0, nullptr);
+}
+
+static void toStorage(VkCommandBuffer cmd, vk::Image& img) {
+ if (img.external() || img.layout == VK_IMAGE_LAYOUT_GENERAL) return;
+ vk::imageBarrier(cmd, img.handle(),
+ img.layout == VK_IMAGE_LAYOUT_UNDEFINED
+ ? VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT : VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
+ 0,
+ VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT,
+ img.layout, VK_IMAGE_LAYOUT_GENERAL);
+ img.layout = VK_IMAGE_LAYOUT_GENERAL;
+}
+
+static VkDescriptorPool makeDescPool(const vk::Device& dev, uint32_t maxSets) {
+ VkDescriptorPoolSize sizes[3]{};
+ sizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; sizes[0].descriptorCount = maxSets * 4;
+ sizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; sizes[1].descriptorCount = maxSets * 16;
+ sizes[2].type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; sizes[2].descriptorCount = maxSets * 16;
+ VkDescriptorPoolCreateInfo pci{};
+ pci.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
+ pci.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
+ pci.maxSets = maxSets;
+ pci.poolSizeCount = 3;
+ pci.pPoolSizes = sizes;
+ VkDescriptorPool pool = VK_NULL_HANDLE;
+ vkCreateDescriptorPool(dev.handle(), &pci, nullptr, &pool);
+ return pool;
+}
+
+static constexpr uint32_t kDbgProbeTexels = 64;
+static constexpr uint32_t kDbgProbeSpots = 4; // sample locations per field
+static constexpr uint32_t kDbgProbeFields = 3; // d089/d094/d099.b48
+
+static float halfToFloat(uint16_t h) {
+ const uint32_t sign = (uint32_t(h) & 0x8000u) << 16;
+ uint32_t exp = (h >> 10) & 0x1f;
+ uint32_t mant = h & 0x3ffu;
+ uint32_t bits;
+ if (exp == 0) {
+ if (mant == 0) { bits = sign; }
+ else {
+ exp = 127 - 15 + 1;
+ while (!(mant & 0x400u)) { mant <<= 1; --exp; }
+ mant &= 0x3ffu;
+ bits = sign | (exp << 23) | (mant << 13);
+ }
+ } else if (exp == 31) {
+ bits = sign | 0x7f800000u | (mant << 13);
+ } else {
+ bits = sign | ((exp - 15 + 127) << 23) | (mant << 13);
+ }
+ float f;
+ std::memcpy(&f, &bits, sizeof(f));
+ return f;
+}
+
+static Pass makeModel1Pass(const vk::Device& dev,
+ VkDescriptorPool pool,
+ int shaderIdx,
+ const vk::Buffer* ubo,
+ const std::vector<const vk::Image*>& sampled,
+ const vk::Sampler& sampler,
+ const std::vector<vk::Image*>& storage) {
+ std::vector<vk::DescriptorBinding> binds;
+ binds.reserve((ubo ? 1u : 0u) + sampled.size() + storage.size());
+ if (ubo) binds.push_back({0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1});
+ for (uint32_t i = 0; i < sampled.size(); ++i)
+ binds.push_back({32u + i, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1});
+ for (uint32_t i = 0; i < storage.size(); ++i)
+ binds.push_back({48u + i, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1});
+
+ Pass p(dev, pool, shaderIdx, binds);
+ if (ubo) p.bindUBO(dev, 0, *ubo);
+ for (uint32_t i = 0; i < sampled.size(); ++i)
+ p.bindSampledGeneral(dev, 32u + i, *sampled[i], sampler);
+ for (uint32_t i = 0; i < storage.size(); ++i)
+ p.bindStorage(dev, 48u + i, *storage[i]);
+ return p;
+}
+
+// ─── FramegenContext::create ─────────────────────────────────────────────────
+
+#ifdef __ANDROID__
+std::unique_ptr<FramegenContext> FramegenContext::create(
+ const vk::Device& device,
+ uint32_t provisionedOutputs,
+ VkExtent2D extent, VkFormat format, const Config& cfg) {
+ return create(device, nullptr, nullptr,
+ std::vector<AHardwareBuffer*>(provisionedOutputs, nullptr),
+ extent, format, cfg);
+}
+
+std::unique_ptr<FramegenContext> FramegenContext::create(
+ const vk::Device& device,
+ AHardwareBuffer* prevAhb, AHardwareBuffer* currAhb,
+ const std::vector<AHardwareBuffer*>& outputAhbs,
+ VkExtent2D extent, VkFormat format, const Config& cfg) {
+ if ((prevAhb != nullptr) != (currAhb != nullptr) || outputAhbs.empty()) {
+ BFG_LOGE("FramegenContext::create: mismatched input AHBs or empty outputs");
+ return nullptr;
+ }
+ auto ctx = std::make_unique<FramegenContext>();
+ ctx->cfg_ = cfg; ctx->cfg_.sanitize();
+ ctx->extent_ = extent; ctx->format_ = format;
+ ctx->prevAhbPtr_ = prevAhb;
+ ctx->currAhbPtr_ = currAhb;
+ const uint32_t W = extent.width, H = extent.height;
+ const uint32_t outputs = ctx->cfg_.multiplier - 1;
+
+ try {
+ const bool useModel1 = ctx->cfg_.model == 1;
+ const int N = useModel1 ? 4 : 2;
+
+ // Single-device mode: adopt the application's device (not owned) instead
+ // of spinning up a standalone instance/device. All vk:: helpers call
+ // global vkXxx routed by handle, and the layer hooks none of the
+ // functions they use, so they operate on the app device with no
+ // recursion — and the AHB producer/consumer are now one device, so the
+ // sync that previously deadlocked across two devices resolves.
+ ctx->device_ = device;
+ ctx->cmdPool_ = vk::CommandPool(ctx->device_);
+ ctx->linearSampler_ = vk::Sampler(ctx->device_, VK_FILTER_LINEAR,
+ VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER);
+ ctx->nearestSampler_= vk::Sampler(ctx->device_, VK_FILTER_NEAREST,
+ VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE);
+ ctx->descPool_ = makeDescPool(ctx->device_, 300);
+
+ // ── Frame input/output images (AHB-backed or device-local) ─────────
+ vk::ImageInfo ahbInfo;
+ ahbInfo.extent = extent; ahbInfo.format = format;
+ ahbInfo.usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT
+ | VK_IMAGE_USAGE_TRANSFER_SRC_BIT
+ | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
+ ctx->prevFrame_ = prevAhb ? vk::Image(ctx->device_, ahbInfo, prevAhb)
+ : vk::Image(ctx->device_, ahbInfo);
+ ctx->currFrame_ = currAhb ? vk::Image(ctx->device_, ahbInfo, currAhb)
+ : vk::Image(ctx->device_, ahbInfo);
+ ctx->outputImages_.reserve(outputAhbs.size());
+ for (auto* ahb : outputAhbs)
+ ctx->outputImages_.emplace_back(
+ ahb ? vk::Image(ctx->device_, ahbInfo, ahb)
+ : vk::Image(ctx->device_, ahbInfo));
+
+ if (!useModel1)
+ ctx->dbgFlowBuf_ = vk::Buffer(ctx->device_, kDbgProbeTexels * 8u * kDbgProbeFields,
+ VK_BUFFER_USAGE_TRANSFER_DST_BIT);
+
+ // ── UBOs ──────────────────────────────────────────────────────────
+ PyramidUBO pyubo; pyubo.scale=2; pyubo.aspect=W/std::max(1u,H); pyubo.pad0=0; pyubo.pad1=0;
+ ctx->uboPyramid_ = vk::Buffer(ctx->device_, sizeof(PyramidUBO),
+ VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, &pyubo);
+ FlowUBO fubo; fubo.flowScale=cfg.flowScale; fubo.pad0=fubo.pad1=fubo.pad2=0;
+ ctx->uboFlow_ = vk::Buffer(ctx->device_, sizeof(FlowUBO),
+ VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, &fubo);
+ ctx->uboSynth_.reserve(outputs);
+ for (uint32_t k = 0; k < outputs; ++k) {
+ SynthUBO s;
+ s.flowScale = cfg.flowScale;
+ s.alpha = float(k+1) / float(cfg.multiplier);
+ s.epsilon = 1e-5f;
+ ctx->uboSynth_.emplace_back(ctx->device_, sizeof(SynthUBO),
+ VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, &s);
+ }
+
+ // ── Shared graph ───────────────────────────────────────────────────
+ // The reference implementation owns more persistent/history resources
+ // than an implicit layer can see directly. The compute side still needs
+ // to match the observed dispatch/resource graph: 99 internal dispatches
+ // (shader_03 then the per-slot table) plus final shader_04 per
+ // generated output. Descriptor labels below follow the traced
+ // descriptor-edge map. The graph is written in model-1 shader indices;
+ // shaderForModel swaps in the model-0 shader per slot, and N is the
+ // per-model feature width.
+ {
+ struct Ratio { uint32_t num; uint32_t den; };
+ auto ratio = [](uint32_t num, uint32_t den) { return Ratio{num, den}; };
+ auto extentFor = [&](Ratio r) -> VkExtent2D {
+ const uint32_t ew = std::max(1u, static_cast<uint32_t>((uint64_t(W) * r.num + r.den - 1u) / r.den));
+ const uint32_t eh = std::max(1u, static_cast<uint32_t>((uint64_t(H) * r.num + r.den - 1u) / r.den));
+ return {ew, eh};
+ };
+ auto groupsFor = [&](Ratio r) -> VkExtent2D {
+ VkExtent2D e = extentFor(r);
+ return {std::max(1u, (e.width + 15u) / 16u),
+ std::max(1u, (e.height + 15u) / 16u)};
+ };
+ auto dLabel = [](int dispatch, int binding) -> std::string {
+ char buf[16];
+ std::snprintf(buf, sizeof(buf), "d%03d.b%d", dispatch, binding);
+ return std::string(buf);
+ };
+ auto extLabel = [](int ext) -> std::string {
+ char buf[16];
+ std::snprintf(buf, sizeof(buf), "ext%d", ext);
+ return std::string(buf);
+ };
+ auto dLabels = [&](int dispatch, int firstBinding, int count) {
+ std::vector<std::string> out;
+ out.reserve(static_cast<size_t>(count));
+ for (int i = 0; i < count; ++i) out.push_back(dLabel(dispatch, firstBinding + i));
+ return out;
+ };
+ auto extLabels = [&](int firstExt, int count) {
+ std::vector<std::string> out;
+ out.reserve(static_cast<size_t>(count));
+ for (int i = 0; i < count; ++i) out.push_back(extLabel(firstExt + i));
+ return out;
+ };
+ auto append = [](std::vector<std::string>& dst, std::vector<std::string> src) {
+ dst.insert(dst.end(), src.begin(), src.end());
+ };
+
+ // Slots 13..24 map -24, not -25: shader_17 sits out of sequence
+ // in the model-0-only slot 25.
+ auto shaderForModel = [&](int s) {
+ if (useModel1) return s;
+ if (s >= 30 && s <= 41) return s - 25; // slots 1..12
+ if (s >= 42 && s <= 53) return s - 24; // slots 13..24
+ return s; // slots 0 and 26
+ };
+
+ auto storageFormatForShader = [](int shaderIdx) -> VkFormat {
+ switch (shaderIdx) {
+ case 3:
+ case 38:
+ return VK_FORMAT_R8_UNORM;
+ case 43:
+ case 48:
+ case 53:
+ return VK_FORMAT_R16G16B16A16_SFLOAT;
+ default:
+ return VK_FORMAT_R8G8B8A8_UNORM;
+ }
+ };
+ auto uboForShader = [&](int shaderIdx) -> const vk::Buffer* {
+ switch (shaderIdx) {
+ case 3:
+ return &ctx->uboPyramid_;
+ case 38:
+ case 39:
+ case 43:
+ case 44:
+ case 48:
+ case 49:
+ return &ctx->uboFlow_;
+ default:
+ return nullptr;
+ }
+ };
+
+ ctx->model1Resources_.reserve(320);
+ std::unordered_map<std::string, size_t> resourceIndex;
+ auto allocateResource = [&](const std::string& label, VkFormat fmt, VkExtent2D e) -> vk::Image& {
+ auto found = resourceIndex.find(label);
+ if (found != resourceIndex.end()) return ctx->model1Resources_[found->second];
+ vk::ImageInfo info;
+ info.extent = e;
+ info.format = fmt;
+ info.usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
+ if (!useModel1 && (label == dLabel(89, 48) || label == dLabel(94, 48) ||
+ label == dLabel(99, 48)))
+ info.usage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
+ const size_t idx = ctx->model1Resources_.size();
+ ctx->model1Resources_.emplace_back(ctx->device_, info);
+ resourceIndex.emplace(label, idx);
+ return ctx->model1Resources_.back();
+ };
+ auto resourceForD = [&](const std::string& label) -> vk::Image& {
+ return ctx->model1Resources_[resourceIndex.at(label)];
+ };
+
+ // Most extN labels are persistent/history resources from the traced
+ // native graph. In the implicit-layer adaptation we feed matching
+ // current-frame graph products instead; ext1/ext35 remain the real
+ // curr/prev frame pair used by shader_03 and final shader_04.
+ auto mappedExternalLabel = [&](int ext) -> std::string {
+ if (ext >= 2 && ext <= 5) return dLabel(23, 48 + (ext - 2));
+ if (ext >= 6 && ext <= 9) return dLabel(23, 48 + (ext - 6));
+ if (ext >= 10 && ext <= 13) return dLabel(29, 48 + (ext - 10));
+ if (ext == 14) return dLabel(34, 53);
+ if (ext >= 15 && ext <= 18) return dLabel(28, 48 + (ext - 15));
+ if (ext >= 19 && ext <= 22) return dLabel(27, 48 + (ext - 19));
+ if (ext >= 23 && ext <= 26) return dLabel(26, 48 + (ext - 23));
+ if (ext >= 27 && ext <= 30) return dLabel(25, 48 + (ext - 27));
+ if (ext >= 31 && ext <= 34) return dLabel(24, 48 + (ext - 31));
+ return dLabel(23, 48);
+ };
+ auto resolveSample = [&](const std::string& label) -> const vk::Image* {
+ if (label.rfind("ext", 0) == 0) {
+ const int ext = std::stoi(label.substr(3));
+ if (ext == 1) return &ctx->currFrame_;
+ if (ext == 35) return &ctx->prevFrame_;
+ return &resourceForD(mappedExternalLabel(ext));
+ }
+ return &resourceForD(label);
+ };
+
+ auto addPass = [&](int shaderIdx,
+ Ratio passRatio,
+ const std::vector<std::string>& sampledLabels,
+ const std::vector<std::string>& storageLabels,
+ const std::vector<Ratio>& storageRatios) {
+ const VkFormat fmt = storageFormatForShader(shaderIdx);
+ std::vector<vk::Image*> storage;
+ storage.reserve(storageLabels.size());
+ for (size_t i = 0; i < storageLabels.size(); ++i) {
+ const Ratio r = storageRatios.empty() ? passRatio : storageRatios[i];
+ storage.push_back(&allocateResource(storageLabels[i], fmt, extentFor(r)));
+ }
+
+ std::vector<const vk::Image*> sampled;
+ sampled.reserve(sampledLabels.size());
+ for (const auto& label : sampledLabels) sampled.push_back(resolveSample(label));
+
+ ctx->model1GraphPasses_.push_back(
+ makeModel1Pass(ctx->device_, ctx->descPool_, shaderForModel(shaderIdx),
+ uboForShader(shaderIdx),
+ sampled, ctx->linearSampler_, storage));
+ ctx->model1GraphDispatch_.push_back(groupsFor(passRatio));
+ std::vector<VkImage> reads; reads.reserve(sampled.size());
+ for (const auto* s : sampled) reads.push_back(s->handle());
+ std::vector<VkImage> writes; writes.reserve(storage.size());
+ for (const auto* s : storage) writes.push_back(s->handle());
+ ctx->model1Reads_.push_back(std::move(reads));
+ ctx->model1Writes_.push_back(std::move(writes));
+ };
+
+ const std::vector<Ratio> pyramidOut = {
+ ratio(1,5), ratio(1,10), ratio(1,20), ratio(1,40),
+ ratio(1,80), ratio(1,160), ratio(1,320),
+ };
+ const std::array<Ratio, 7> expandRatios = {{
+ ratio(2,5), ratio(1,5), ratio(1,10), ratio(1,20),
+ ratio(1,40), ratio(1,80), ratio(1,160),
+ }};
+ const std::array<Ratio, 7> smallRatios = {{
+ ratio(1,5), ratio(1,10), ratio(1,20), ratio(1,40),
+ ratio(1,80), ratio(1,160), ratio(1,320),
+ }};
+
+ // 0x1a9ed4: shader_03, shader_30 x7, shader_31 x7,
+ // shader_32 x7, shader_33 x7.
+ addPass(3, ratio(1,5), {extLabel(1)}, dLabels(1, 48, 7), pyramidOut);
+ for (int i = 0; i < 7; ++i)
+ addPass(30, expandRatios[size_t(i)], {dLabel(1, 48 + i)}, dLabels(2 + i, 48, 2), {});
+ for (int i = 0; i < 7; ++i)
+ addPass(31, expandRatios[size_t(i)], dLabels(2 + i, 48, 2), dLabels(9 + i, 48, 2), {});
+ for (int i = 0; i < 7; ++i)
+ addPass(32, smallRatios[size_t(i)], dLabels(9 + i, 48, 2), dLabels(16 + i, 48, N), {});
+ for (int i = 0; i < 7; ++i)
+ addPass(33, smallRatios[size_t(i)], dLabels(16 + i, 48, N), dLabels(23 + i, 48, N), {});
+
+ // 0x1b004c: shader_34..38.
+ {
+ auto sampled = extLabels(2, N);
+ append(sampled, extLabels(6, N));
+ append(sampled, dLabels(23, 48, N));
+ addPass(34, ratio(1,5), sampled, dLabels(30, 48, 2), {});
+ }
+ addPass(35, ratio(1,5), dLabels(30, 48, 2), dLabels(31, 48, 2), {});
+ addPass(36, ratio(1,5), dLabels(31, 48, 2), dLabels(32, 48, 2), {});
+ addPass(37, ratio(1,5), dLabels(32, 48, 2), dLabels(33, 48, 2), {});
+ addPass(38, ratio(1,10), dLabels(33, 48, 2), dLabels(34, 48, 6), {
+ ratio(1,10), ratio(1,20), ratio(1,40), ratio(1,80), ratio(1,160), ratio(1,320)
+ });
+
+ auto addFivePassRound = [&](Ratio r, int extStart, int srcD,
+ const std::string& carry, const std::string& aux,
+ int firstDispatch) {
+ auto sampled39 = extLabels(extStart, N);
+ append(sampled39, dLabels(srcD, 48, N));
+ sampled39.push_back(carry);
+ addPass(39, r, sampled39, dLabels(firstDispatch, 48, 3), {});
+ addPass(40, r, dLabels(firstDispatch, 48, 3), dLabels(firstDispatch + 1, 48, 4), {});
+ addPass(41, r, dLabels(firstDispatch + 1, 48, 4), dLabels(firstDispatch + 2, 48, 4), {});
+ addPass(42, r, dLabels(firstDispatch + 2, 48, 4), dLabels(firstDispatch + 3, 48, 4), {});
+ auto sampled43 = dLabels(firstDispatch + 3, 48, 4);
+ sampled43.push_back(carry);
+ sampled43.push_back(aux);
+ addPass(43, r, sampled43, dLabels(firstDispatch + 4, 48, 1), {});
+ };
+
+ // 0x1b0708: three 1x1 rounds plus one 2x2 round using shader_39..43.
+ addFivePassRound(ratio(1,320), 10, 29, extLabel(14), dLabel(34, 53), 35);
+ addFivePassRound(ratio(1,160), 15, 28, dLabel(39, 48), dLabel(34, 53), 40);
+ addFivePassRound(ratio(1,80), 19, 27, dLabel(44, 48), dLabel(34, 52), 45);
+ addFivePassRound(ratio(1,40), 23, 26, dLabel(49, 48), dLabel(34, 51), 50);
+
+ auto addFifteenPassRound = [&](Ratio r, int extStart, int srcD,
+ const std::string& carry,
+ const std::string& aux,
+ const std::string& shader49Extra,
+ const std::string& shader53Extra,
+ int firstDispatch) {
+ addFivePassRound(r, extStart, srcD, carry, aux, firstDispatch);
+
+ auto sampled44 = extLabels(extStart, N);
+ append(sampled44, dLabels(srcD, 48, N));
+ sampled44.push_back(carry);
+ addPass(44, r, sampled44, dLabels(firstDispatch + 5, 48, 3), {});
+ addPass(45, r, dLabels(firstDispatch + 5, 48, 3), dLabels(firstDispatch + 6, 48, 4), {});
+ addPass(46, r, dLabels(firstDispatch + 6, 48, 4), dLabels(firstDispatch + 7, 48, 4), {});
+ addPass(47, r, dLabels(firstDispatch + 7, 48, 4), dLabels(firstDispatch + 8, 48, 4), {});
+ auto sampled48 = dLabels(firstDispatch + 8, 48, 4);
+ sampled48.push_back(carry);
+ sampled48.push_back(aux);
+ addPass(48, r, sampled48, dLabels(firstDispatch + 9, 48, 1), {});
+
+ auto sampled49 = extLabels(extStart, N);
+ append(sampled49, dLabels(srcD, 48, N));
+ sampled49.push_back(carry);
+ sampled49.push_back(shader49Extra);
+ addPass(49, r, sampled49, dLabels(firstDispatch + 10, 48, 2), {});
+ addPass(50, r, dLabels(firstDispatch + 10, 48, 2), dLabels(firstDispatch + 11, 48, 2), {});
+ addPass(51, r, dLabels(firstDispatch + 11, 48, 2), dLabels(firstDispatch + 12, 48, 2), {});
+ addPass(52, r, dLabels(firstDispatch + 12, 48, 2), dLabels(firstDispatch + 13, 48, 2), {});
+ auto sampled53 = dLabels(firstDispatch + 13, 48, 2);
+ sampled53.push_back(shader53Extra);
+ addPass(53, r, sampled53, dLabels(firstDispatch + 14, 48, 1), {});
+ };
+
+ addFifteenPassRound(ratio(1,20), 27, 25, dLabel(54, 48), dLabel(34, 50),
+ extLabel(14), extLabel(14), 55);
+ addFifteenPassRound(ratio(1,10), 31, 24, dLabel(59, 48), dLabel(34, 49),
+ dLabel(64, 48), dLabel(69, 48), 70);
+ addFifteenPassRound(ratio(1,5), 6, 23, dLabel(74, 48), dLabel(34, 48),
+ dLabel(79, 48), dLabel(84, 48), 85);
+
+ // Final full-resolution shader_04. The traced native path then
+ // performs a barrier/copy/barrier in 0x1b1b10; as a layer we bind
+ // the storage output directly to the generated AHB image and let
+ // layer.cpp blit it into swapchain images.
+ ctx->model1FinalPassStart_ = ctx->model1GraphPasses_.size();
+ for (uint32_t k = 0; k < outputs; ++k) {
+ std::vector<const vk::Image*> sampled = {
+ &ctx->prevFrame_,
+ &ctx->currFrame_,
+ &resourceForD(dLabel(89, 48)),
+ &resourceForD(dLabel(94, 48)),
+ &resourceForD(dLabel(99, 48)),
+ };
+ std::vector<vk::Image*> storage = {&ctx->outputImages_[k]};
+ ctx->model1GraphPasses_.push_back(
+ makeModel1Pass(ctx->device_, ctx->descPool_, 4, &ctx->uboSynth_[k],
+ sampled, ctx->linearSampler_, storage));
+ ctx->model1GraphDispatch_.push_back(groupsFor(ratio(1,1)));
+ std::vector<VkImage> reads; reads.reserve(sampled.size());
+ for (const auto* s : sampled) reads.push_back(s->handle());
+ ctx->model1Reads_.push_back(std::move(reads));
+ ctx->model1Writes_.push_back({ctx->outputImages_[k].handle()});
+ }
+
+ ctx->dbgFlowIdx_[0] = resourceIndex.at(dLabel(89, 48));
+ ctx->dbgFlowIdx_[1] = resourceIndex.at(dLabel(94, 48));
+ ctx->dbgFlowIdx_[2] = resourceIndex.at(dLabel(99, 48));
+
+ BFG_LOGI("model=%u graph: %zu passes, %zu resources, finalStart=%zu",
+ ctx->cfg_.model, ctx->model1GraphPasses_.size(),
+ ctx->model1Resources_.size(), ctx->model1FinalPassStart_);
+ }
+
+
+ // ── Frame ring ────────────────────────────────────────────────────────
+ for (auto& f : ctx->frames_) {
+ f.cmd = vk::CommandBuffer(ctx->device_, ctx->cmdPool_);
+ f.fence = vk::Fence(ctx->device_, true);
+ }
+
+ BFG_LOGI("FramegenContext ready: %ux%u mult=%u model=%u graph=shared-table N=%d",
+ W, H, cfg.multiplier, cfg.model, N);
+ return ctx;
+ } catch (const vk::VkError& e) {
+ BFG_LOGE("FramegenContext::create VkError %d: %s", e.code, e.msg.c_str());
+ ctx->destroy();
+ return nullptr;
+ } catch (const std::exception& e) {
+ BFG_LOGE("FramegenContext::create exception: %s", e.what());
+ ctx->destroy();
+ return nullptr;
+ }
+}
+
+void FramegenContext::rebindFrameInputs() {
+ // Only the parity set of the NEXT run is written; the other set may be in
+ // use by the in-flight graph. That parity's last user was the run two
+ // frames back — its fence gates the update (signalled long ago in steady
+ // state).
+ const uint32_t p = frameIdx_ & 1u;
+ frames_[p].fence.wait(device_);
+ if (!model1GraphPasses_.empty())
+ model1GraphPasses_[0].bindSampledGeneralAt(device_, p, 32, currFrame_, linearSampler_); // shader_03 ext1
+ for (size_t i = model1FinalPassStart_; i < model1GraphPasses_.size(); ++i) {
+ model1GraphPasses_[i].bindSampledGeneralAt(device_, p, 32, prevFrame_, linearSampler_); // shader_04 ext35
+ model1GraphPasses_[i].bindSampledGeneralAt(device_, p, 33, currFrame_, linearSampler_); // shader_04 ext1
+ }
+}
+
+void FramegenContext::swapFrameInputs() {
+ std::swap(prevFrame_, currFrame_);
+ std::swap(prevAhbPtr_, currAhbPtr_);
+ rebindFrameInputs();
+}
+
+void FramegenContext::present(AHardwareBuffer* newPrev, AHardwareBuffer* newCurr) {
+ if (newPrev && newCurr && (newPrev != prevAhbPtr_ || newCurr != currAhbPtr_)) {
+ if (newPrev == currAhbPtr_ && newCurr == prevAhbPtr_) {
+ swapFrameInputs();
+ } else {
+ BFG_LOGW("FramegenContext::present: unexpected AHB input order; using existing descriptors");
+ }
+ }
+
+ const uint32_t W = extent_.width, H = extent_.height;
+ const uint32_t fi = frameIdx_ & 1u;
+ auto& fr = frames_[fi];
+ fr.fence.wait(device_); fr.fence.reset(device_);
+
+ // Decode the flow probe written by the run this fence just retired.
+ if (cfg_.model != 1 && dbgFlowBuf_.valid() && frameIdx_ >= 2 &&
+ ++dbgLogCounter_ >= 120) {
+ dbgLogCounter_ = 0;
+ const auto* halves = static_cast<const uint16_t*>(dbgFlowBuf_.mapped());
+ if (halves) {
+ static const char* kName[kDbgProbeFields] = {"d089", "d094", "d099"};
+ for (uint32_t f = 0; f < kDbgProbeFields; ++f) {
+ const uint16_t* t = halves + size_t(f) * kDbgProbeTexels * 4u;
+ float avg = 0.f, mx = 0.f, sgn[4] = {0.f, 0.f, 0.f, 0.f};
+ for (uint32_t i = 0; i < kDbgProbeTexels; ++i) {
+ float c[4];
+ for (int j = 0; j < 4; ++j) {
+ c[j] = halfToFloat(t[i*4+uint32_t(j)]);
+ sgn[j] += c[j];
+ }
+ const float x = std::fabs(c[0]), y = std::fabs(c[1]);
+ avg += (x + y) * 0.5f;
+ mx = std::max(mx, std::max(x, y));
+ }
+ avg /= float(kDbgProbeTexels);
+ for (int j = 0; j < 4; ++j) sgn[j] /= float(kDbgProbeTexels);
+ BFG_LOGI("model0 flow %s: avg|xy|=%.3f max=%.2f xy=(%+.2f,%+.2f) zw=(%+.2f,%+.2f)",
+ kName[f], double(avg), double(mx),
+ double(sgn[0]), double(sgn[1]), double(sgn[2]), double(sgn[3]));
+ }
+ }
+ }
+
+ vkResetCommandBuffer(fr.cmd.handle(), 0);
+ fr.cmd.begin();
+ VkCommandBuffer cmd = fr.cmd.handle();
+
+ // Order this run against the previous run's writes to the shared
+ // intermediates AND the layer's transfer-stage blits of the outputs: runs
+ // overlap on the queue now that nothing CPU-waits between them, and the
+ // layout-elision below can otherwise start dispatching with no barrier
+ // against the prior run's tail or the output blit's reads.
+ {
+ VkMemoryBarrier mb{};
+ mb.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER;
+ mb.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
+ mb.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT;
+ vkCmdPipelineBarrier(cmd,
+ VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT,
+ VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
+ 0, 1, &mb, 0, nullptr, 0, nullptr);
+ }
+
+ // Acquire AHB inputs from external; device-local inputs are moved to
+ // GENERAL, the layout the sampled-image descriptors were recorded with
+ if (prevFrame_.external()) vk::acquireFromExternal(cmd, prevFrame_, device_.computeFamily(), VK_ACCESS_SHADER_READ_BIT);
+ else toStorage(cmd, prevFrame_);
+ if (currFrame_.external()) vk::acquireFromExternal(cmd, currFrame_, device_.computeFamily(), VK_ACCESS_SHADER_READ_BIT);
+ else toStorage(cmd, currFrame_);
+
+ // Runtime-confirmed steady-state order (shared by both models):
+ // 0x1a9ed4 -> 0x1afe28(no compute) -> 0x1b004c -> 0x1b0708
+ // -> 0x1b1b10(copy/barrier). We emit the internal dispatch graph plus
+ // the active final shader_04 passes directly into the provisioned AHB
+ // outputs required by the current multiplier.
+ const size_t activeOutputs = std::min<size_t>(
+ outputImages_.size(),
+ cfg_.multiplier > 1 ? static_cast<size_t>(cfg_.multiplier - 1) : size_t{0});
+
+ for (auto& img : model1Resources_) toStorage(cmd, img);
+ for (size_t i = 0; i < activeOutputs; ++i) {
+ auto& out = outputImages_[i];
+ if (out.external())
+ vk::acquireFromExternal(cmd, out, device_.computeFamily(), VK_ACCESS_SHADER_WRITE_BIT);
+ toStorage(cmd, out);
+ }
+
+ // Dispatch with barriers only at true dependencies. A barrier is
+ // required when a pass reads or writes an image written since the last
+ // barrier (RAW/WAW), or writes an image read since it (WAR); anything
+ // else may overlap. The unconditional barrier-per-dispatch replay cost
+ // ~100 pipeline drains per frame and dominated the graph's GPU time.
+ const size_t passCount = std::min(model1GraphPasses_.size(), model1GraphDispatch_.size());
+ std::unordered_set<VkImage> dirty, readSince;
+ for (size_t i = 0; i < passCount; ++i) {
+ bool needBarrier = false;
+ if (i < model1Reads_.size() && i < model1Writes_.size()) {
+ for (VkImage r : model1Reads_[i])
+ if (dirty.count(r)) { needBarrier = true; break; }
+ if (!needBarrier)
+ for (VkImage w : model1Writes_[i])
+ if (dirty.count(w) || readSince.count(w)) { needBarrier = true; break; }
+ } else {
+ needBarrier = true; // no dependency info; stay conservative
+ }
+ if (needBarrier) {
+ computeBarrier(cmd);
+ dirty.clear();
+ readSince.clear();
+ }
+ const VkExtent2D g = model1GraphDispatch_[i];
+ model1GraphPasses_[i].dispatch(cmd, g.width, g.height, fi);
+ if (i < model1Reads_.size() && i < model1Writes_.size()) {
+ for (VkImage r : model1Reads_[i]) readSince.insert(r);
+ for (VkImage w : model1Writes_[i]) dirty.insert(w);
+ }
+ }
+ // Make the final synthesis writes visible to the layer's blit
+ computeBarrier(cmd);
+
+ // Flow probe (model 0): the three flow fields shader_04 consumes
+ // (rgba16f at 1/5), four spread sample spots each. Next run's opening
+ // barrier includes the TRANSFER stage, ordering these reads against its
+ // rewrites.
+ if (cfg_.model != 1 && dbgFlowBuf_.valid() &&
+ dbgFlowIdx_[0] < model1Resources_.size() &&
+ dbgFlowIdx_[1] < model1Resources_.size() &&
+ dbgFlowIdx_[2] < model1Resources_.size()) {
+ const uint32_t fw = std::max(1u, (W + 4u) / 5u);
+ const uint32_t fh = std::max(1u, (H + 4u) / 5u);
+ const uint32_t run = kDbgProbeTexels / kDbgProbeSpots;
+ for (int f = 0; f < 3; ++f) {
+ vk::Image& img = model1Resources_[dbgFlowIdx_[f]];
+ vk::imageBarrier(cmd, img.handle(),
+ VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT,
+ VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT,
+ VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL);
+ VkBufferImageCopy regions[kDbgProbeSpots]{};
+ uint32_t count = 0;
+ for (uint32_t s = 0; s < kDbgProbeSpots; ++s) {
+ const uint32_t px = fw * (s + 1u) / (kDbgProbeSpots + 1u);
+ const uint32_t py = fh * (s + 1u) / (kDbgProbeSpots + 1u);
+ if (px + run > fw || py >= fh) continue;
+ auto& r = regions[count++];
+ r.bufferOffset = VkDeviceSize(f) * kDbgProbeTexels * 8u
+ + VkDeviceSize(s) * run * 8u;
+ r.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
+ r.imageOffset = {int32_t(px), int32_t(py), 0};
+ r.imageExtent = {run, 1, 1};
+ }
+ if (count)
+ vkCmdCopyImageToBuffer(cmd, img.handle(), VK_IMAGE_LAYOUT_GENERAL,
+ dbgFlowBuf_.handle(), count, regions);
+ }
+ }
+
+ for (size_t i = 0; i < activeOutputs; ++i) {
+ auto& out = outputImages_[i];
+ if (out.external())
+ vk::releaseToExternal(cmd, out, device_.computeFamily(), VK_ACCESS_SHADER_WRITE_BIT);
+ }
+ if (prevFrame_.external()) vk::releaseToExternal(cmd, prevFrame_, device_.computeFamily(), VK_ACCESS_SHADER_READ_BIT);
+ if (currFrame_.external()) vk::releaseToExternal(cmd, currFrame_, device_.computeFamily(), VK_ACCESS_SHADER_READ_BIT);
+
+ fr.cmd.end();
+ fr.cmd.submit(device_, fr.fence.handle());
+ frameIdx_++;
+}
+
+#endif // __ANDROID__
+
+void FramegenContext::updateConfig(const Config& cfg) {
+ Config next = cfg; next.sanitize();
+ // The UBOs are read by up to two in-flight runs; only touch them when a
+ // value actually changed, and drain the queue first when it did.
+ if (next.flowScale == cfg_.flowScale && next.multiplier == cfg_.multiplier &&
+ next.model == cfg_.model) {
+ cfg_ = next;
+ return;
+ }
+ waitIdle();
+ cfg_ = next;
+ for (size_t k=0;k<uboSynth_.size();++k) {
+ SynthUBO s;
+ s.flowScale = cfg_.flowScale;
+ s.alpha=float(k+1)/float(cfg_.multiplier); s.epsilon=1e-5f;
+ uboSynth_[k].write(device_,&s,sizeof(s));
+ }
+ FlowUBO f; f.flowScale=cfg_.flowScale; f.pad0=f.pad1=f.pad2=0;
+ uboFlow_.write(device_,&f,sizeof(f));
+}
+
+void FramegenContext::waitIdle() {
+ if (device_.valid()) vkQueueWaitIdle(device_.computeQueue());
+}
+
+void FramegenContext::destroy() {
+ waitIdle();
+ for (auto& f:frames_) { f.cmd.destroy(device_,cmdPool_); f.fence.destroy(device_); }
+ for (auto& p:model1GraphPasses_) p.destroy(device_); model1GraphPasses_.clear();
+ model1GraphDispatch_.clear();
+ model1Reads_.clear();
+ model1Writes_.clear();
+ model1FinalPassStart_ = 0;
+ uboPyramid_.destroy(device_); uboFlow_.destroy(device_);
+ dbgFlowBuf_.destroy(device_);
+ for (auto& b:uboSynth_) b.destroy(device_); uboSynth_.clear();
+ // Images
+ prevFrame_.destroy(device_); currFrame_.destroy(device_);
+ for (auto& i:outputImages_) i.destroy(device_); outputImages_.clear();
+ for (auto& i:model1Resources_) i.destroy(device_); model1Resources_.clear();
+ if (descPool_) vkDestroyDescriptorPool(device_.handle(), descPool_, nullptr);
+ descPool_ = VK_NULL_HANDLE;
+ linearSampler_.destroy(device_); nearestSampler_.destroy(device_);
+ cmdPool_.destroy(device_); device_.destroy();
+}
+
+std::string FramegenContext::describe() const {
+ std::ostringstream o;
+ o << "FramegenContext{" << extent_.width << "x" << extent_.height
+ << " mult=" << cfg_.multiplier << " flowScale=" << cfg_.flowScale
+ << " model=" << cfg_.model << " valid=" << (valid()?"true":"false") << "}";
+ return o.str();
+}
+
+} // namespace bionic_fg