From 0c4361a693624cb97cfba3d984650f0fdbefa064 Mon Sep 17 00:00:00 2001 From: butilly Date: Sat, 10 Oct 2026 18:59:00 +0000 Subject: [PATCH 1/2] LSFG: initialize loader data for adopted-device framegen command buffers --- framegen/src/core/device.cpp | 3 +++ src/layer.cpp | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/framegen/src/core/device.cpp b/framegen/src/core/device.cpp index 6a178873..885790b4 100644 --- a/framegen/src/core/device.cpp +++ b/framegen/src/core/device.cpp @@ -586,9 +586,12 @@ Device::Device(const Instance& instance, VkPhysicalDevice physical, new VkDevice(external), [](VkDevice* device) { delete device; } ); + LSFG_FRAMEGEN_LOGI("Adopted-device init: creating fallback descriptor image"); this->fallbackDescriptorImage = std::make_shared(*this, VkExtent2D{1, 1}, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT); + LSFG_FRAMEGEN_LOGI("Adopted-device init: clearing fallback descriptor image"); Utils::clearImage(*this, *this->fallbackDescriptorImage); + LSFG_FRAMEGEN_LOGI("Adopted-device init: fallback descriptor ready"); } diff --git a/src/layer.cpp b/src/layer.cpp index fa9c173c..04e2c978 100644 --- a/src/layer.cpp +++ b/src/layer.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include namespace { @@ -76,6 +77,31 @@ namespace { PFN_vkInvalidateMappedMemoryRanges next_vkInvalidateMappedMemoryRanges{}; PFN_vkCmdCopyImageToBuffer next_vkCmdCopyImageToBuffer{}; + // Framegen adopts the game's device but allocates below the loader via + // Volk. Dispatchable objects created by a layer must receive loader data, + // just like the command buffers allocated by Mini::CommandBuffer. + VkResult framegenAllocateCommandBuffers( + VkDevice device, + const VkCommandBufferAllocateInfo* info, + VkCommandBuffer* buffers) { + auto res = next_vkAllocateCommandBuffers(device, info, buffers); + if (res != VK_SUCCESS) + return res; + for (uint32_t i = 0; i < info->commandBufferCount; ++i) { + res = next_vSetDeviceLoaderData(device, buffers[i]); + if (res != VK_SUCCESS) { + std::cerr << "lsfg-vk: framegen command buffer loader setup failed: " + << res << '\n'; + next_vkFreeCommandBuffers(device, info->commandPool, + info->commandBufferCount, buffers); + for (uint32_t j = 0; j < info->commandBufferCount; ++j) + buffers[j] = VK_NULL_HANDLE; + return res; + } + } + return VK_SUCCESS; + } + template bool initInstanceFunc(VkInstance instance, const char* name, T* func) { *func = reinterpret_cast(next_vkGetInstanceProcAddr(instance, name)); @@ -361,11 +387,17 @@ namespace Layer { PFN_vkVoidFunction ovkGetInstanceProcAddr( VkInstance instance, const char* pName) { + // Keep Volk's adopted-device dispatch below our hooks, while routing + // its allocations through the loader-data adapter above. + if (std::strcmp(pName, "vkGetDeviceProcAddr") == 0) + return reinterpret_cast(ovkGetDeviceProcAddr); return next_vkGetInstanceProcAddr(instance, pName); } PFN_vkVoidFunction ovkGetDeviceProcAddr( VkDevice device, const char* pName) { + if (std::strcmp(pName, "vkAllocateCommandBuffers") == 0) + return reinterpret_cast(framegenAllocateCommandBuffers); return next_vkGetDeviceProcAddr(device, pName); } From effec9cf301a8ca742e6ec3631603b871321d897 Mon Sep 17 00:00:00 2001 From: butilly Date: Tue, 13 Oct 2026 17:28:55 +0000 Subject: [PATCH 2/2] LSFG: refresh active pacing deadline from display phase --- include/context.hpp | 2 ++ src/context.cpp | 36 ++++++++++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/include/context.hpp b/include/context.hpp index ae382de9..a7a3e3ba 100644 --- a/include/context.hpp +++ b/include/context.hpp @@ -101,6 +101,7 @@ class LsContext { int64_t pacerAnchorNs{0}; // schedule anchor of the current real frame int64_t pacerNextDueNs{0}; // next vsync-grid slot for the real frame + int64_t pacerCorrectionNs{0}; // latest display-grid phase correction int64_t lastRealPresentNs{0}; // previous real-frame entry, for the EWMA uint64_t baseIntervalEwmaNs{0}; // measured real-frame interval @@ -113,6 +114,7 @@ class LsContext { uint64_t statsPresents{0}; // presents (real + generated) in the window uint64_t statsRealPresents{0}; // real presents in the window int64_t statsWindowStartNs{0}; + int64_t statsLogNs{0}; // rate-limit pacing diagnostics // per-stage timing accumulators for the stats window uint64_t statsWorkNs{0}; // copy fence wait + framegen + waitIdle diff --git a/src/context.cpp b/src/context.cpp index 23854544..68fd6807 100644 --- a/src/context.cpp +++ b/src/context.cpp @@ -518,6 +518,20 @@ void LsContext::paceBaseFrame(int fpsLimit) { if (!readVsyncFile(conf.config_file, &this->vsyncPhaseNs, &this->vsyncPeriodNs)) { this->vsyncPhaseNs = 0; this->vsyncPeriodNs = 0; + } else if (this->pacerNextDueNs > 0) { + // Refresh the active deadline too, not only the cached grid. + // Otherwise the deadline free-runs from the first phase forever + // and beats against the display despite receiving new timestamps. + // Choose the nearest vblank, preserving the base-frame divisor's + // slot instead of selecting a new every-N-vblank phase each read. + int64_t correction = (this->vsyncPhaseNs - this->pacerNextDueNs) + % this->vsyncPeriodNs; + if (correction > this->vsyncPeriodNs / 2) + correction -= this->vsyncPeriodNs; + else if (correction < -this->vsyncPeriodNs / 2) + correction += this->vsyncPeriodNs; + this->pacerNextDueNs += correction; + this->pacerCorrectionNs = correction; } } if (targetNs > 0 && this->vsyncPeriodNs > 0) { @@ -525,8 +539,10 @@ void LsContext::paceBaseFrame(int fpsLimit) { (targetNs + this->vsyncPeriodNs / 2) / this->vsyncPeriodNs) * this->vsyncPeriodNs; int64_t due = this->pacerNextDueNs; if (due <= 0 || due < entryNs - stepNs || due > entryNs + 2 * stepNs) { - due = this->vsyncPhaseNs - + ((entryNs - this->vsyncPhaseNs) / stepNs + 1) * stepNs; + // Normalized modulo also handles a phase just ahead of entry. + const int64_t offset = ((entryNs - this->vsyncPhaseNs) % stepNs + + stepNs) % stepNs; + due = entryNs + stepNs - offset; } sleepUntilNs(due); this->pacerAnchorNs = due; @@ -1059,6 +1075,22 @@ VkResult LsContext::present(const Hooks::DeviceInfo& info, const void* pNext, Vk .genSkips = this->statsGenSkips, .seq = this->statsSeq, }); + // Low-rate diagnostics distinguish base-frame timing from generated + // output; the app's frame-time histogram only measures base frames. + if (statsNow - this->statsLogNs >= 2'000'000'000LL) { + this->statsLogNs = statsNow; + std::cerr << "lsfg-vk: pacing-grid-refresh" + << " base_fps=" << frames / windowSec + << " output_fps=" << static_cast(this->statsPresents) / windowSec + << " work_ms=" << static_cast(this->statsWorkNs) / 1e6 / frames + << " gen_late_ms=" << static_cast(this->statsGenLateNs) / 1e6 / frames + << " gen_sleep_ms=" << static_cast(this->statsGenSleepNs) / 1e6 / frames + << " gen_present_ms=" << static_cast(this->statsGenPresentNs) / 1e6 / frames + << " real_present_ms=" << static_cast(this->statsRealPresentNs) / 1e6 / frames + << " gen_skips=" << this->statsGenSkips + << " vsync_period_ns=" << this->vsyncPeriodNs + << " phase_correction_ns=" << this->pacerCorrectionNs << '\n'; + } this->statsSeq.clear(); this->statsPresents = 0; this->statsRealPresents = 0;