#include "framegen_context.hpp" #include "logging.hpp" #include #include #include #include #include #include #include #include #include #include namespace bionic_fg { // ─── Pass ──────────────────────────────────────────────────────────────────── Pass::Pass(const vk::Device& dev, VkDescriptorPool descPool, int shaderIdx, const std::vector& bindings) { if (shaderIdx < 0 || static_cast(shaderIdx) >= embedded::kShaderRegistry.size()) throw std::runtime_error("Pass: shader index out of range"); const auto& blob = embedded::kShaderRegistry[static_cast(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& sampled, const vk::Sampler& sampler, const std::vector& storage) { std::vector 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::create( const vk::Device& device, uint32_t provisionedOutputs, VkExtent2D extent, VkFormat format, const Config& cfg) { return create(device, nullptr, nullptr, std::vector(provisionedOutputs, nullptr), extent, format, cfg); } std::unique_ptr FramegenContext::create( const vk::Device& device, AHardwareBuffer* prevAhb, AHardwareBuffer* currAhb, const std::vector& 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(); 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((uint64_t(W) * r.num + r.den - 1u) / r.den)); const uint32_t eh = std::max(1u, static_cast((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 out; out.reserve(static_cast(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 out; out.reserve(static_cast(count)); for (int i = 0; i < count; ++i) out.push_back(extLabel(firstExt + i)); return out; }; auto append = [](std::vector& dst, std::vector 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 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& sampledLabels, const std::vector& storageLabels, const std::vector& storageRatios) { const VkFormat fmt = storageFormatForShader(shaderIdx); std::vector 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 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 reads; reads.reserve(sampled.size()); for (const auto* s : sampled) reads.push_back(s->handle()); std::vector 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 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 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 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 sampled = { &ctx->prevFrame_, &ctx->currFrame_, &resourceForD(dLabel(89, 48)), &resourceForD(dLabel(94, 48)), &resourceForD(dLabel(99, 48)), }; std::vector 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 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(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( outputImages_.size(), cfg_.multiplier > 1 ? static_cast(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 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