Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions framegen/src/core/device.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<Core::Image>(*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");
}
2 changes: 2 additions & 0 deletions include/context.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
36 changes: 34 additions & 2 deletions src/context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -518,15 +518,31 @@ 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) {
const int64_t stepNs = std::max<int64_t>(1,
(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;
Expand Down Expand Up @@ -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<double>(this->statsPresents) / windowSec
<< " work_ms=" << static_cast<double>(this->statsWorkNs) / 1e6 / frames
<< " gen_late_ms=" << static_cast<double>(this->statsGenLateNs) / 1e6 / frames
<< " gen_sleep_ms=" << static_cast<double>(this->statsGenSleepNs) / 1e6 / frames
<< " gen_present_ms=" << static_cast<double>(this->statsGenPresentNs) / 1e6 / frames
<< " real_present_ms=" << static_cast<double>(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;
Expand Down
32 changes: 32 additions & 0 deletions src/layer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include <exception>
#include <iostream>
#include <cstdint>
#include <cstring>
#include <string>

namespace {
Expand Down Expand Up @@ -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<typename T>
bool initInstanceFunc(VkInstance instance, const char* name, T* func) {
*func = reinterpret_cast<T>(next_vkGetInstanceProcAddr(instance, name));
Expand Down Expand Up @@ -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<PFN_vkVoidFunction>(ovkGetDeviceProcAddr);
return next_vkGetInstanceProcAddr(instance, pName);
}
PFN_vkVoidFunction ovkGetDeviceProcAddr(
VkDevice device,
const char* pName) {
if (std::strcmp(pName, "vkAllocateCommandBuffers") == 0)
return reinterpret_cast<PFN_vkVoidFunction>(framegenAllocateCommandBuffers);
return next_vkGetDeviceProcAddr(device, pName);
}

Expand Down
Loading