Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a5319e0
OpenFlow: Farneback/DIS hybrid flow, full-speed camera turns, no fram…
qwertypower Oct 10, 2026
4ae9bc9
WFG: generate straight into the swapchain, overlay occlusion, planner…
qwertypower Oct 10, 2026
2bf2828
Rename the frame generation module to WFG
qwertypower Oct 10, 2026
97aced0
WFG: hysteresis on the display-rate cap of the generation count
qwertypower Oct 10, 2026
1de7e6c
WFG: step the generation count up only after half a second
qwertypower Oct 10, 2026
7a0569b
WFG: forward-splat the field to each generated frame where layers com…
qwertypower Oct 12, 2026
e2d1eda
WFG: splat once per source pair for all generated frames
qwertypower Oct 12, 2026
ba12d1b
WFG: drop the Quality resolution scale
qwertypower Oct 12, 2026
ea46089
WFG: drop the unused hardware motion hint and luma shader; full name …
qwertypower Oct 12, 2026
ccf0094
README: WFG - WinNative/Winlator Frame Generator
qwertypower Oct 12, 2026
6bacdfa
WFG: splat only the vectors that land on a different motion
qwertypower Oct 14, 2026
5ebdce4
WFG: replace the vectors the next frame does not confirm for the inte…
qwertypower Oct 14, 2026
d059d4b
WFG: interpolate along the pair's acceleration
qwertypower Oct 14, 2026
28c04f4
WFG: no frame generation from a game below 20 fps
qwertypower Oct 14, 2026
981d187
WFG: relicense the module under MIT
qwertypower Oct 18, 2026
8a54578
CREDITS: WFG authorship in the DIS and Farneback entries, current sha…
qwertypower Oct 21, 2026
0f09ba5
Credits: WFG and its MIT license in the license table and README
qwertypower Oct 21, 2026
6ba1d7f
Merge branch 'WinNative-Emu:main' into wfg
qwertypower Oct 30, 2026
514ab5d
Merge branch 'WinNative-Emu:main' into main
qwertypower Oct 30, 2026
a05a7e6
Framegen: keep the present queue short, no generation on draws withou…
qwertypower Oct 30, 2026
3fb9e22
Merge branch 'main' into wfg
qwertypower Oct 30, 2026
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
43 changes: 41 additions & 2 deletions app/src/main/cpp/winlator/vk/framegen/fg_present.c
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ struct FgPresenter {
uint64_t log_real;
uint64_t log_generated;
uint64_t acquire_misses;
// Present-queue model (see fg_record_and_present): when the last presented image would be
// shown, and the generated frames left out to drain the queue.
uint64_t queue_end_ns;
uint64_t trimmed;
};

static void fg_reset_swapchain(FgPresenter* fg);
Expand Down Expand Up @@ -804,6 +808,12 @@ static void fg_renew_semaphore(FgPresenter* fg, VkSemaphore* handle) {
*handle = fresh;
}

static uint64_t fg_now_ns(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
}

static void fg_record_and_present(FgPresenter* fg, FgImport* source, AImage* image) {
FgFrame* f = &fg->frames[fg->frame_index];

Expand All @@ -822,6 +832,7 @@ static void fg_record_and_present(FgPresenter* fg, FgImport* source, AImage* ima
}

uint32_t image_index = 0;
const uint64_t acquire_begin_ns = fg_now_ns();
VkResult acq = vkAcquireNextImageKHR(fg->device, fg->swapchain, UINT64_MAX, f->acquire,
VK_NULL_HANDLE, &image_index);
if (acq != VK_SUCCESS && acq != VK_SUBOPTIMAL_KHR) {
Expand All @@ -830,6 +841,24 @@ static void fg_record_and_present(FgPresenter* fg, FgImport* source, AImage* ima
return;
}

// Keep the present queue short (see vk_renderer.c): FIFO shows one image per refresh and
// only the swapchain size bounds the queue, so output even slightly faster than the display
// fills it and every frame reaches the screen up to ~80 ms late. While more than two images
// wait beyond the next refresh, one generated frame per pair is left out until it drains.
const uint64_t period_ns = fg->refresh_rate > 1.0f ? (uint64_t)(1.0e9f / fg->refresh_rate) : 0;
if (period_ns > 0) {
const uint64_t now_ns = fg_now_ns();
if (now_ns - acquire_begin_ns > period_ns / 2 && fg->queue_end_ns < now_ns + 2 * period_ns) {
fg->queue_end_ns = now_ns + 2 * period_ns;
}
const uint64_t ahead = fg->queue_end_ns > now_ns + period_ns
? (fg->queue_end_ns - now_ns - period_ns) / period_ns : 0;
if (ahead > 2 && planned > 0) {
planned--;
fg->trimmed++;
}
}

uint64_t gen_timeout = FG_ACQUIRE_TIMEOUT_MAX_NS;
if (fg->refresh_rate > 1.0f) {
gen_timeout = (uint64_t)(2000000000.0f / fg->refresh_rate);
Expand Down Expand Up @@ -983,19 +1012,29 @@ static void fg_record_and_present(FgPresenter* fg, FgImport* source, AImage* ima
VkResult pr = vkQueuePresentKHR(fg->queue, &pi);
if (pr == VK_ERROR_OUT_OF_DATE_KHR) out_of_date = true;

// The queue model: each image presented leaves the queue one refresh after the one before.
if (period_ns > 0) {
const uint64_t now_ns = fg_now_ns();
for (uint32_t i = 0; i < gen_count + 1; i++) {
const uint64_t next = fg->queue_end_ns + period_ns;
fg->queue_end_ns = next > now_ns + period_ns ? next : now_ns + period_ns;
}
}

fg->real_frames++;
fg->generated_frames += gen_count;
if ((fg->real_frames % FG_TELEMETRY_FRAMES) == 0) {
const uint64_t d_real = fg->real_frames - fg->log_real;
const uint64_t d_gen = fg->generated_frames - fg->log_generated;
fg->log_real = fg->real_frames;
fg->log_generated = fg->generated_frames;
FG_LOGI("framegen[%s] real=%llu made=%llu ratio=%.2f planned=%u got=%u images=%u div=%u misses=%llu",
FG_LOGI("framegen[%s] real=%llu made=%llu ratio=%.2f planned=%u got=%u images=%u div=%u "
"misses=%llu trimmed=%llu",
fg_engine_name(fg->active_engine),
(unsigned long long)fg->real_frames, (unsigned long long)fg->generated_frames,
d_real ? (double)(d_real + d_gen) / (double)d_real : 0.0, planned, gen_count,
fg->swapchain_image_count, fg->source_divisor,
(unsigned long long)fg->acquire_misses);
(unsigned long long)fg->acquire_misses, (unsigned long long)fg->trimmed);
}

fg->frame_index = (fg->frame_index + 1) % FG_FRAMES_IN_FLIGHT;
Expand Down
67 changes: 65 additions & 2 deletions app/src/main/cpp/winlator/vk/vk_renderer.c
Original file line number Diff line number Diff line change
Expand Up @@ -1901,6 +1901,9 @@ static void destroy_lsfg(VkRenderer* r) {
r->framegen_real_frames = 0;
r->framegen_made_frames = 0;
r->framegen_draw_ns = 0;
r->framegen_queue_end_ns = 0;
r->framegen_trimmed = 0;
r->framegen_same_source = 0;
r->framegen_gap_ns = 0;
r->framegen_last_end_ns = 0;
r->framegen_timed_frames = 0;
Expand All @@ -1927,6 +1930,9 @@ static void destroy_dis(VkRenderer* r) {
r->framegen_real_frames = 0;
r->framegen_made_frames = 0;
r->framegen_draw_ns = 0;
r->framegen_queue_end_ns = 0;
r->framegen_trimmed = 0;
r->framegen_same_source = 0;
r->framegen_gap_ns = 0;
r->framegen_last_end_ns = 0;
r->framegen_timed_frames = 0;
Expand Down Expand Up @@ -2709,8 +2715,10 @@ static bool record_and_submit_frame(VkRenderer* r) {
VkCompositeTarget* composite = via_composite ? &r->composite[r->frame_index] : NULL;

uint32_t image_index = 0;
const uint64_t acquire_begin_ns = vkr_monotonic_ns();
VkResult acq = vkAcquireNextImageKHR(r->device, r->swapchain, UINT64_MAX,
f->image_available, VK_NULL_HANDLE, &image_index);
const uint64_t real_acquire_wait_ns = vkr_monotonic_ns() - acquire_begin_ns;
bool recreate_after_present = false;
if (acq == VK_ERROR_OUT_OF_DATE_KHR) {
r->surface_ready = false;
Expand Down Expand Up @@ -2741,6 +2749,42 @@ static bool record_and_submit_frame(VkRenderer* r) {
}
}

// Keep the present queue short. FIFO shows one image per refresh and nothing else bounds
// the queue but the swapchain's dozen images, so a game even slightly faster than the
// output cadence (60.5 fps doubled on 120 Hz), a draw without a new game frame, or a burst
// after a stall fills it, and from then on every frame - the game's own included - reaches
// the display up to ~80 ms late: input lag that a game held exactly at 30 fps never shows.
// A draw without a new game frame generates nothing (there is no new motion to show), and
// while more than two images wait beyond the next refresh one generated frame per pair is
// left out until the queue drains - one at a time, so the motion cadence stays even.
const uint64_t source_frames = __atomic_load_n(&r->framegen_source_frames, __ATOMIC_RELAXED);
const bool new_source_frame = source_frames != r->framegen_seen_source;
r->framegen_seen_source = source_frames;
const uint64_t refresh_period_ns =
r->framegen_refresh_rate > 1.0f ? (uint64_t)(1.0e9f / r->framegen_refresh_rate) : 0;
if (framegen_planned > 0 && !new_source_frame) {
framegen_planned = 0;
r->framegen_same_source++;
}
if (refresh_period_ns > 0) {
const uint64_t now_ns = vkr_monotonic_ns();
// A real acquire that blocked means the queue is at least that full, whatever the model.
if (real_acquire_wait_ns > refresh_period_ns / 2 &&
r->framegen_queue_end_ns < now_ns + 2 * refresh_period_ns) {
r->framegen_queue_end_ns = now_ns + 2 * refresh_period_ns;
}
const uint64_t ahead = r->framegen_queue_end_ns > now_ns + refresh_period_ns
? (r->framegen_queue_end_ns - now_ns - refresh_period_ns) / refresh_period_ns : 0;
if (framegen_planned > 0) {
r->framegen_ahead_sum += ahead;
r->framegen_ahead_n++;
if (ahead > 2) {
framegen_planned--;
r->framegen_trimmed++;
}
}
}

uint32_t gen_count = 0;
uint32_t gen_image_index[VK_FRAMEGEN_MAX_GENERATIONS] = {0};
for (uint32_t g = 0; g < framegen_planned; g++) {
Expand Down Expand Up @@ -2951,15 +2995,22 @@ static bool record_and_submit_frame(VkRenderer* r) {
r->framegen_gap_ns = 0;
r->framegen_timed_frames = 0;
VK_LOGI("framegen delivered real=%llu made=%llu ratio=%.2f planned=%u got=%u "
"misses=%llu timeout=%.1fms images=%u capacity=%u draw=%.2fms gap=%.2fms",
"misses=%llu timeout=%.1fms images=%u capacity=%u draw=%.2fms gap=%.2fms "
"queue=%.2f trimmed=%llu same=%llu",
(unsigned long long)r->framegen_real_frames,
(unsigned long long)r->framegen_made_frames,
d_real ? (double)d_made / (double)d_real : 0.0,
framegen_planned, gen_count,
(unsigned long long)r->framegen_acquire_misses,
(double)gen_acquire_timeout / 1000000.0,
r->swapchain_image_count, framegen_capacity,
draw_ms, gap_ms);
draw_ms, gap_ms,
r->framegen_ahead_n ? (double)r->framegen_ahead_sum /
(double)r->framegen_ahead_n : 0.0,
(unsigned long long)r->framegen_trimmed,
(unsigned long long)r->framegen_same_source);
r->framegen_ahead_sum = 0;
r->framegen_ahead_n = 0;
}
}

Expand Down Expand Up @@ -3126,6 +3177,7 @@ static bool record_and_submit_frame(VkRenderer* r) {
pi.pImageIndices = &image_index;

bool gen_present_out_of_date = false;
uint32_t presented_now = 0;
pthread_mutex_lock(&r->queue_mutex);
for (uint32_t g = 0; g < gen_count; g++) {
VkPresentInfoKHR gpi = {VK_STRUCTURE_TYPE_PRESENT_INFO_KHR};
Expand All @@ -3143,11 +3195,22 @@ static bool record_and_submit_frame(VkRenderer* r) {
}
} else {
__atomic_fetch_add(&r->presented_frames, 1, __ATOMIC_RELAXED);
presented_now++;
}
}
VkResult pr = vkQueuePresentKHR(r->graphics_queue, &pi);
if (pr == VK_SUCCESS || pr == VK_SUBOPTIMAL_KHR) {
__atomic_fetch_add(&r->presented_frames, 1, __ATOMIC_RELAXED);
presented_now++;
}
// The queue model: each image presented leaves the queue one refresh after the one before.
if (refresh_period_ns > 0) {
const uint64_t now_ns = vkr_monotonic_ns();
for (uint32_t i = 0; i < presented_now; i++) {
const uint64_t next = r->framegen_queue_end_ns + refresh_period_ns;
r->framegen_queue_end_ns = next > now_ns + refresh_period_ns ? next
: now_ns + refresh_period_ns;
}
}
// Present the mirror separately so its result doesn't disturb the display recreate logic below.
if (rec_this_frame) {
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/cpp/winlator/vk/vk_state.h
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,14 @@ typedef struct VkRenderer {
uint64_t framegen_log_made;
uint64_t framegen_made_frames;
uint64_t framegen_acquire_misses;
// Present-queue model for frame generation: FIFO shows one image per refresh, so the queue
// drains at the display rate while each draw adds its real frame and its generated ones.
uint64_t framegen_queue_end_ns; // when the last presented image would be shown
uint64_t framegen_seen_source; // game frame count at the last draw
uint64_t framegen_trimmed; // generated frames left out to drain the queue
uint64_t framegen_same_source; // draws without a new game frame
uint64_t framegen_ahead_sum;
uint64_t framegen_ahead_n;
uint64_t framegen_draw_ns;
uint64_t framegen_gap_ns;
uint64_t framegen_last_end_ns;
Expand Down
Loading