From a5319e06659490b66a91d132c26f6105725feba0 Mon Sep 17 00:00:00 2001 From: qwertypower <1qwertypower1@gmail.com> Date: Sat, 10 Oct 2026 22:56:04 +0300 Subject: [PATCH 01/18] OpenFlow: Farneback/DIS hybrid flow, full-speed camera turns, no frame overshoot - Farneback optical flow (polynomial expansion, windowed least squares with Cauchy reweighting, after OpenCV's calcOpticalFlowFarneback) as a second estimator, and a hybrid as the default: Farneback to level 2, the DIS patch search on levels 1 and 0, Farneback's level 1 as a second start for level 0, a 2x2 refinement on level 0 only. dis/farneback selectable with setprop debug.winnative.openflow.engine. - The flow magnitude clamp is half the frame instead of a quarter: a hard camera turn at 30 fps moves the picture by over a quarter and came out uniformly short. - The frame planner no longer plans more frames than the display shows: rounding the output count up at a refresh-rate target overshot it (28 fps at 120 Hz gave 142 fps), generated frames timed out on acquire and were dropped, a visible jerk. - The engine is called OpenFG in the frame generation menu. Bench (1280x720, 180p, x4 from 30 fps): mean PSNR 22.64 -> 22.98 dB, hard camera turn 14.4 -> 17.6. Adreno 750: flow 5.5 -> 2.36 ms per pair; on device, dropped generated frames 8-18 per 120 real -> 0. --- app/src/main/cpp/openflow/CMakeLists.txt | 3 + .../openflow/include/shaders/dis_fb_hsum.comp | 120 ++++ .../openflow/include/shaders/dis_fb_poly.comp | 99 ++++ .../include/shaders/dis_fb_vsolve.comp | 48 ++ .../include/shaders/dis_inverse_search.comp | 19 + .../openflow/include/shaders/dis_vr_add.comp | 9 +- app/src/main/cpp/openflow/src/vkr_dis.c | 530 +++++++++++++++--- app/src/main/res/values-b+es+419/strings.xml | 2 +- app/src/main/res/values-da/strings.xml | 2 +- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-es/strings.xml | 2 +- app/src/main/res/values-fi/strings.xml | 2 +- app/src/main/res/values-fr/strings.xml | 2 +- app/src/main/res/values-hi/strings.xml | 2 +- app/src/main/res/values-it/strings.xml | 2 +- app/src/main/res/values-ja/strings.xml | 2 +- app/src/main/res/values-ko/strings.xml | 2 +- app/src/main/res/values-no/strings.xml | 2 +- app/src/main/res/values-pl/strings.xml | 2 +- app/src/main/res/values-pt-rBR/strings.xml | 2 +- app/src/main/res/values-pt/strings.xml | 2 +- app/src/main/res/values-ro/strings.xml | 2 +- app/src/main/res/values-ru/strings.xml | 2 +- app/src/main/res/values-sv/strings.xml | 2 +- app/src/main/res/values-th/strings.xml | 2 +- app/src/main/res/values-tr/strings.xml | 2 +- app/src/main/res/values-uk/strings.xml | 2 +- app/src/main/res/values-zh-rCN/strings.xml | 2 +- app/src/main/res/values-zh-rTW/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 30 files changed, 771 insertions(+), 103 deletions(-) create mode 100644 app/src/main/cpp/openflow/include/shaders/dis_fb_hsum.comp create mode 100644 app/src/main/cpp/openflow/include/shaders/dis_fb_poly.comp create mode 100644 app/src/main/cpp/openflow/include/shaders/dis_fb_vsolve.comp diff --git a/app/src/main/cpp/openflow/CMakeLists.txt b/app/src/main/cpp/openflow/CMakeLists.txt index e873a4042..8e5ef2c15 100644 --- a/app/src/main/cpp/openflow/CMakeLists.txt +++ b/app/src/main/cpp/openflow/CMakeLists.txt @@ -33,6 +33,9 @@ set(DIS_SHADERS dis_vr_sor dis_vr_add dis_me_luma + dis_fb_poly + dis_fb_hsum + dis_fb_vsolve ) set(DIS_SHADER_HEADERS "") diff --git a/app/src/main/cpp/openflow/include/shaders/dis_fb_hsum.comp b/app/src/main/cpp/openflow/include/shaders/dis_fb_hsum.comp new file mode 100644 index 000000000..0da2186f0 --- /dev/null +++ b/app/src/main/cpp/openflow/include/shaders/dis_fb_hsum.comp @@ -0,0 +1,120 @@ +// SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) +// SPDX-License-Identifier: GPL-3.0-or-later +// +// OpenFlow, Farneback flow: the per-texel displacement equations of one iteration, summed +// across the window horizontally (dis_fb_vsolve sums them vertically and solves). +// +// With the first frame's expansion (A0, b0) at x and the second's (A1, b1) at x + d, Farneback's +// constraint is A d = db with A = (A0 + A1) / 2 and db = (b0 - b1) / 2 + A d_prev. Its normal +// equations G = A^T A, h = A^T db are summed over the window, and d = G^-1 h. As in OpenCV +// (FarnebackUpdateMatrices), a second-frame sample off the level keeps only the first frame's +// terms, and the equations fade out over the last five texels of the border. + +#version 450 + +precision highp float; +precision highp int; + +layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in; + +layout(set = 0, binding = 0) uniform sampler2D polyB0; +layout(set = 0, binding = 1) uniform sampler2D polyC0; +layout(set = 0, binding = 2) uniform sampler2D polyB1; +layout(set = 0, binding = 3) uniform sampler2D polyC1; +layout(set = 0, binding = 4) uniform sampler2D flowIn; // normalised uv, any resolution +layout(set = 0, binding = 8, rgba32f) uniform writeonly image2D sumG; // (gxx, gxy, gyy, hx) +layout(set = 0, binding = 9, r32f) uniform writeonly image2D sumH; // hy + +layout(push_constant) uniform PC { + int radius; // window half-width, at most MAX_RADIUS + float sigma; // Gaussian window; 0 is a box + int useFlow; // 0: start from zero + float robust; // > 0: down-weight texels the current flow does not fit (see below) +} pc; + +#define MAX_RADIUS 16 +#define W (64 + 2 * MAX_RADIUS) + +shared vec4 sG[W]; +shared float sH[W]; + +float borderScale(int x, int n) { + int e = min(x, n - 1 - x); + return e >= 5 ? 1.0 : (e < 2 ? 0.14 : 0.4472); +} + +void equations(ivec2 p, ivec2 size, out vec4 G, out float H) { + vec2 d = vec2(0.0); + if (pc.useFlow != 0) { + d = textureLod(flowIn, (vec2(p) + 0.5) / vec2(size), 0.0).xy * vec2(size); + if (any(isnan(d)) || any(isinf(d))) d = vec2(0.0); + } + vec4 r0 = texelFetch(polyB0, p, 0); + float c0 = texelFetch(polyC0, p, 0).x; + + vec2 q = vec2(p) + d; + float axx, ayy, axy; + vec2 db; + float weight = 1.0; + if (all(greaterThanEqual(q, vec2(0.0))) && all(lessThanEqual(q, vec2(size - 1)))) { + vec2 quv = (q + 0.5) / vec2(size); + vec4 r1 = textureLod(polyB1, quv, 0.0); + float c1 = textureLod(polyC1, quv, 0.0).x; + axx = (r0.z + r1.z) * 0.5; + ayy = (r0.w + r1.w) * 0.5; + axy = (c0 + c1) * 0.25; + db = (r0.xy - r1.xy) * 0.5; + // Where the current flow is right, the two frames' linear terms agree at x and x + d; + // where they do not, the texel belongs to other motion (an edge, an occlusion) and + // would drag the window's solution across it. A Cauchy weight on the mismatch keeps the + // window to the texels that share its motion - iteratively reweighted least squares, + // one reweighting per iteration. + if (pc.robust > 0.0) weight = 1.0 / (1.0 + dot(db, db) / (pc.robust * pc.robust)); + } else { + axx = r0.z; + ayy = r0.w; + axy = c0 * 0.5; + db = r0.xy * 0.5; + } + db += vec2(axx * d.x + axy * d.y, axy * d.x + ayy * d.y); + + float s = borderScale(p.x, size.x) * borderScale(p.y, size.y); + axx *= s; ayy *= s; axy *= s; db *= s; + + G = vec4(axx * axx + axy * axy, axy * (axx + ayy), ayy * ayy + axy * axy, + axx * db.x + axy * db.y) * weight; + H = (axy * db.x + ayy * db.y) * weight; +} + +void main() { + ivec2 size = textureSize(polyB0, 0); + int R = min(pc.radius, MAX_RADIUS); + int y = int(gl_GlobalInvocationID.y); + int x0 = int(gl_WorkGroupID.x) * 64 - R; + int span = 64 + 2 * R; + for (int k = int(gl_LocalInvocationID.x); k < span; k += 64) { + ivec2 p = ivec2(clamp(x0 + k, 0, size.x - 1), y); + vec4 G; + float H; + equations(p, size, G, H); + sG[k] = G; + sH[k] = H; + } + barrier(); + + int x = int(gl_GlobalInvocationID.x); + if (x >= size.x || y >= size.y) return; + + vec4 accG = vec4(0.0); + float accH = 0.0; + float wsum = 0.0; + int c = int(gl_LocalInvocationID.x) + R; + for (int j = -R; j <= R; j++) { + float w = pc.sigma > 0.0 ? exp(-float(j * j) / (2.0 * pc.sigma * pc.sigma)) : 1.0; + accG += sG[c + j] * w; + accH += sH[c + j] * w; + wsum += w; + } + imageStore(sumG, ivec2(x, y), accG / wsum); + imageStore(sumH, ivec2(x, y), vec4(accH / wsum, 0.0, 0.0, 0.0)); +} diff --git a/app/src/main/cpp/openflow/include/shaders/dis_fb_poly.comp b/app/src/main/cpp/openflow/include/shaders/dis_fb_poly.comp new file mode 100644 index 000000000..5a4d73a1f --- /dev/null +++ b/app/src/main/cpp/openflow/include/shaders/dis_fb_poly.comp @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) +// SPDX-License-Identifier: GPL-3.0-or-later +// +// OpenFlow, Farneback flow: polynomial expansion of one pyramid level of a frame. +// +// Gunnar Farneback, "Two-Frame Motion Estimation Based on Polynomial Expansion" (SCIA 2003); +// the formulation follows OpenCV's calcOpticalFlowFarneback (FarnebackPolyExp). Around every +// texel the luminance is fitted, under a Gaussian applicability, by +// f(x, y) ~ r1 + rx*x + ry*y + rxx*x^2 + ryy*y^2 + rxy*x*y, +// which is two separable passes of three moments each followed by the closed-form inverse of +// the 6x6 normal matrix (its four distinct entries come in as push constants). Computed once +// per frame and level: the frame is the second of this pair and the first of the next one. + +#version 450 + +precision highp float; +precision highp int; + +layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; + +layout(set = 0, binding = 0) uniform sampler2D lumaMap; +layout(set = 0, binding = 8, rgba32f) uniform writeonly image2D polyB; // (rx, ry, rxx, ryy) +layout(set = 0, binding = 9, r32f) uniform writeonly image2D polyC; // rxy + +layout(push_constant) uniform PC { + float sigma; + float ig11; + float ig03; + float ig33; + float ig55; +} pc; + +// Half-width of the applicability. With sigma 1.1 the weights beyond 3 are under 2.5% of the +// centre, so this is the 11-tap OpenCV kernel to within its own float precision. +#define N 3 +#define T (8 + 2 * N) + +shared float sL[T * T]; +shared vec3 sV[8 * T]; + +void main() { + ivec2 size = textureSize(lumaMap, 0); + ivec2 base = ivec2(gl_WorkGroupID.xy) * 8 - N; + for (int k = int(gl_LocalInvocationIndex); k < T * T; k += 64) { + ivec2 q = clamp(base + ivec2(k % T, k / T), ivec2(0), size - 1); + sL[k] = texelFetch(lumaMap, q, 0).x; + } + + float g[N + 1]; + float s = 0.0; + for (int k = 0; k <= N; k++) { + g[k] = exp(-float(k * k) / (2.0 * pc.sigma * pc.sigma)); + s += k == 0 ? g[k] : 2.0 * g[k]; + } + for (int k = 0; k <= N; k++) g[k] /= s; + + barrier(); + + // Vertical moments (I, y*I, y^2*I) for this thread's output row, on every tile column. + ivec2 l = ivec2(gl_LocalInvocationID.xy); + int r = l.y + N; + for (int c = l.x; c < T; c += 8) { + float v0 = g[0] * sL[r * T + c]; + float v1 = 0.0; + float v2 = 0.0; + for (int k = 1; k <= N; k++) { + float a = sL[(r - k) * T + c]; + float b = sL[(r + k) * T + c]; + v0 += g[k] * (a + b); + v1 += float(k) * g[k] * (b - a); + v2 += float(k * k) * g[k] * (a + b); + } + sV[l.y * T + c] = vec3(v0, v1, v2); + } + barrier(); + + ivec2 p = ivec2(gl_GlobalInvocationID.xy); + if (p.x >= size.x || p.y >= size.y) return; + + // Horizontal moments: b1 ~ 1, b2 ~ x, b3 ~ y, b4 ~ x^2, b5 ~ y^2, b6 ~ xy. + int c = l.x + N; + vec3 v = sV[l.y * T + c]; + float b1 = g[0] * v.x, b2 = 0.0, b3 = g[0] * v.y, b4 = 0.0, b5 = g[0] * v.z, b6 = 0.0; + for (int k = 1; k <= N; k++) { + vec3 vp = sV[l.y * T + c + k]; + vec3 vm = sV[l.y * T + c - k]; + float xg = float(k) * g[k]; + b1 += g[k] * (vp.x + vm.x); + b4 += float(k) * xg * (vp.x + vm.x); + b2 += xg * (vp.x - vm.x); + b3 += g[k] * (vp.y + vm.y); + b6 += xg * (vp.y - vm.y); + b5 += g[k] * (vp.z + vm.z); + } + + imageStore(polyB, p, vec4(b2 * pc.ig11, b3 * pc.ig11, b1 * pc.ig03 + b4 * pc.ig33, + b1 * pc.ig03 + b5 * pc.ig33)); + imageStore(polyC, p, vec4(b6 * pc.ig55, 0.0, 0.0, 0.0)); +} diff --git a/app/src/main/cpp/openflow/include/shaders/dis_fb_vsolve.comp b/app/src/main/cpp/openflow/include/shaders/dis_fb_vsolve.comp new file mode 100644 index 000000000..75caf09d6 --- /dev/null +++ b/app/src/main/cpp/openflow/include/shaders/dis_fb_vsolve.comp @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) +// SPDX-License-Identifier: GPL-3.0-or-later +// +// OpenFlow, Farneback flow: sums the horizontally summed equations of dis_fb_hsum vertically +// across the window and solves G d = h for this texel's displacement. + +#version 450 + +precision highp float; +precision highp int; + +layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; + +layout(set = 0, binding = 0) uniform sampler2D sumG; // (gxx, gxy, gyy, hx) +layout(set = 0, binding = 1) uniform sampler2D sumH; // hy +layout(set = 0, binding = 8, rg32f) uniform writeonly image2D flowOut; // normalised uv + +layout(push_constant) uniform PC { + int radius; + float sigma; +} pc; + +void main() { + ivec2 size = textureSize(sumG, 0); + ivec2 p = ivec2(gl_GlobalInvocationID.xy); + if (p.x >= size.x || p.y >= size.y) return; + + int R = pc.radius; + vec4 G = vec4(0.0); + float H = 0.0; + float wsum = 0.0; + for (int j = -R; j <= R; j++) { + ivec2 q = ivec2(p.x, clamp(p.y + j, 0, size.y - 1)); + float w = pc.sigma > 0.0 ? exp(-float(j * j) / (2.0 * pc.sigma * pc.sigma)) : 1.0; + G += texelFetch(sumG, q, 0) * w; + H += texelFetch(sumH, q, 0).x * w; + wsum += w; + } + G /= wsum; + H /= wsum; + + // G = [gxx gxy; gxy gyy], h = (hx, hy). The small term keeps flat windows finite, as in + // OpenCV, where it pulls the solution towards zero. + float idet = 1.0 / (G.x * G.z - G.y * G.y + 1e-3); + vec2 d = vec2(G.z * G.w - G.y * H, G.x * H - G.y * G.w) * idet; + if (any(isnan(d)) || any(isinf(d))) d = vec2(0.0); + imageStore(flowOut, p, vec4(d / vec2(size), 0.0, 0.0)); +} diff --git a/app/src/main/cpp/openflow/include/shaders/dis_inverse_search.comp b/app/src/main/cpp/openflow/include/shaders/dis_inverse_search.comp index c4e364fe3..8211b4e35 100644 --- a/app/src/main/cpp/openflow/include/shaders/dis_inverse_search.comp +++ b/app/src/main/cpp/openflow/include/shaders/dis_inverse_search.comp @@ -84,12 +84,16 @@ layout(set = 0, binding = 5, rgba32f) uniform image2D sparseFlowMap; // The previous pair's final flow, in normalised uv units, indexed by position in its first frame; // read on the levels whose bit is set in priorLevels. layout(set = 0, binding = 6) uniform sampler2D priorFlowMap; +// The hybrid's Farneback field at the level above this one, in normalised uv units; read on the +// levels whose bit is set in fbLevels. +layout(set = 0, binding = 7) uniform sampler2D fbFlowMap; layout(push_constant) uniform PC { int level; int coarseLevel; int hintLevel; int priorLevels; + int fbLevels; } pc; #define HINT_INVALID 1.0e6 @@ -253,6 +257,21 @@ void main() { } } } + + // In the hybrid the level above was searched by DIS, which finds small objects but follows + // smooth and fast motion worse than Farneback's windowed solve; Farneback's field for that + // level is the other start, and the patch decides between them. + if ((pc.fbLevels & (1 << pc.level)) != 0) { + vec2 f = textureLod(fbFlowMap, (vec2(pix) + 4.0) * invImageSize, 0.0).xy * vec2(denseSize); + if (!any(isnan(f)) && !any(isinf(f)) && distance(f, flow) > DIS_PRIOR_MIN_PX) { + if (startSSD < 0.0) startSSD = patchSSD(flow, pix, lp, denseSize); + float fbSSD = patchSSD(f, pix, lp, denseSize); + if (fbSSD < startSSD) { + startSSD = fbSSD; + flow = f; + } + } + } vec2 initialFlow = flow; const float N_INV = 1.0 / N; diff --git a/app/src/main/cpp/openflow/include/shaders/dis_vr_add.comp b/app/src/main/cpp/openflow/include/shaders/dis_vr_add.comp index 7d61aa97a..2a83707b4 100644 --- a/app/src/main/cpp/openflow/include/shaders/dis_vr_add.comp +++ b/app/src/main/cpp/openflow/include/shaders/dis_vr_add.comp @@ -37,10 +37,11 @@ void main() { // The old guard was `abs(f) < 4 * width` per component: four frame widths is no limit // at all, and worse, when it did trip it fell back to W, which by then is itself the // huge value. Measured: input +-400 px came out at 1624 px and the guard never fired - // once. Clamp the magnitude instead. The fastest real motion measured on this content - // is about 0.1 of the frame per source frame, so a quarter of the frame leaves four - // times the headroom anything legitimate needs. - const float FLOW_LIMIT_FRACTION = 0.25; + // once. Clamp the magnitude instead, at half the frame: a hard camera turn at 30 fps moves + // the picture by over a quarter of the frame per source frame (the bench's fastest flick, + // 0.28), which the earlier quarter-frame limit cut short - the whole field came out short + // and the generated frames lagged behind the motion. + const float FLOW_LIMIT_FRACTION = 0.5; float lim = FLOW_LIMIT_FRACTION * max(uSize.x, uSize.y); float mag = length(f); vec2 fr = mag > lim ? f * (lim / mag) : f; diff --git a/app/src/main/cpp/openflow/src/vkr_dis.c b/app/src/main/cpp/openflow/src/vkr_dis.c index b2e3088c0..036bb28bf 100644 --- a/app/src/main/cpp/openflow/src/vkr_dis.c +++ b/app/src/main/cpp/openflow/src/vkr_dis.c @@ -28,6 +28,9 @@ #include "shaders/dis_vr_coef_comp.spv.h" #include "shaders/dis_vr_sor_comp.spv.h" #include "shaders/dis_vr_add_comp.spv.h" +#include "shaders/dis_fb_poly_comp.spv.h" +#include "shaders/dis_fb_hsum_comp.spv.h" +#include "shaders/dis_fb_vsolve_comp.spv.h" #include #include @@ -48,6 +51,34 @@ #define DIS_MIN_EXTENT 16u #define DIS_DEFAULT_FLOW_MIN_SIDE 180u + +// Flow estimators. DIS: sparse inverse search, densification and variational refinement. +// Farneback: polynomial expansion and a windowed least-squares solve per level (dis_fb_*). +#define DIS_ENGINE_DIS 0 +#define DIS_ENGINE_FARNEBACK 1 +// Farneback down to level 2, then the DIS patch search (with its prior and static tests) on +// levels 1 and 0 and a shorter variational refinement. Farneback still solves level 1, into +// fb_l1, as the other start of level 0's search: DIS on level 1 catches small fast objects that +// Farneback's windows average away, Farneback follows smooth and fast motion better, and each +// patch of level 0 starts from whichever of the two explains it. +#define DIS_ENGINE_HYBRID 2 +// Farneback, tuned on the bench (window in flow texels): 3 iterations a level, a Gaussian +// window of radius 12 and sigma 8, a Cauchy reweighting at mismatch 5, and the 7-tap +// polynomial applicability of OpenCV's default sigma 1.1. +#define DIS_FB_ITERATIONS 3 +#define DIS_FB_RADIUS 12 +#define DIS_FB_WINDOW_SIGMA 8.0f +#define DIS_FB_POLY_SIGMA 1.1f +#define DIS_FB_POLY_N 3 +#define DIS_FB_ROBUST 5.0f +// The hybrid's refinement, on level 0 only (fb_hybrid_vr_upper): at most 2 fixed-point passes +// of 2 SOR sweeps (the x4 path of DIS runs 4 of 7), which pays for the level-1 search. Against +// Farneback to level 1 with DIS on level 0 and a 2x4 refinement: 7% less flow time on Adreno 750 +// (2.36 against 2.54 ms a pair) and 22.98 dB against 22.90 on the bench, small fast objects 29.1 +// against 27.3 (DIS alone: 22.87 and 29.9 at 5.5 ms). Refining level 1 too, or a third sweep, +// measured within 0.01 dB of this. +#define DIS_HYB_FIXED_POINT 2u +#define DIS_HYB_SOR 2u #define DIS_FLOW_MIN_SIDE_FLOOR 64u #define DIS_FLOW_MIN_SIDE_CEIL 1080u #define DIS_MAX_LEVELS 8u @@ -97,7 +128,7 @@ #define DIS_VR_LEVELS 8u -#define DIS_SET_SAMPLERS 6u +#define DIS_SET_SAMPLERS 7u #define DIS_SET_STORAGE 1u #define DIS_SHARED_SETS_PER_LEVEL 6u #define DIS_VR_SHARED_SETS 7u @@ -168,6 +199,15 @@ struct VkrDis { DisImage flow_out; DisImage me_field; + // Farneback flow (DIS_ENGINE_FARNEBACK and _HYBRID): each frame's polynomial expansion, + // one per slot with a mip per level, and the horizontally summed equations of a level. + DisImage fb_b[DIS_SLOTS]; + DisImage fb_c[DIS_SLOTS]; + DisImage fb_sg; + DisImage fb_sh; + // The hybrid's Farneback level 1 when DIS searches level 1 too: a start for level 0's search. + DisImage fb_l1; + VkImageView view_color[DIS_SLOTS]; VkImageView view_flow_color[DIS_SLOTS][DIS_MAX_LEVELS]; VkImageView view_flow_luma[DIS_SLOTS][DIS_MAX_LEVELS]; @@ -188,6 +228,11 @@ struct VkrDis { VkImageView view_side; VkImageView view_flow_out; VkImageView view_me_field; + VkImageView view_fb_b[DIS_SLOTS][DIS_MAX_LEVELS]; + VkImageView view_fb_c[DIS_SLOTS][DIS_MAX_LEVELS]; + VkImageView view_fb_sg[DIS_MAX_LEVELS]; + VkImageView view_fb_sh[DIS_MAX_LEVELS]; + VkImageView view_fb_l1; VkSampler sampler; @@ -204,6 +249,15 @@ struct VkrDis { VkDescriptorSet hist_sets[DIS_SLOTS][2]; VkDescriptorSet side_sets[DIS_SLOTS]; VkDescriptorSet pack_set; + // Farneback, all from vr_set_layout: the expansion of slot s at level l; the equations of + // the pair ending in slot s at level l, starting from the level above ([0]) or from this + // level's previous iteration ([1]); and the solve of level l. + VkDescriptorSet fb_poly_sets[DIS_SLOTS][DIS_MAX_LEVELS]; + VkDescriptorSet fb_hsum_sets[DIS_SLOTS][DIS_MAX_LEVELS][2]; + VkDescriptorSet fb_vsolve_sets[DIS_MAX_LEVELS]; + // Farneback's level 1 into fb_l1: later iterations start from it, the solve writes it. + VkDescriptorSet fb_hsum1_sets[DIS_SLOTS]; + VkDescriptorSet fb_vsolve1_set; VkDescriptorSetLayout vr_set_layout; VkPipelineLayout vr_pipeline_layout; @@ -223,6 +277,9 @@ struct VkrDis { DisPass pass_densify; DisPass pass_interp; DisPass pass_hist; + DisPass pass_fb_poly; + DisPass pass_fb_hsum; + DisPass pass_fb_vsolve; DisPass pass_side; DisPass pass_pack; DisPass pass_me_luma; @@ -262,6 +319,22 @@ struct VkrDis { // start from it; false after a reset, a rebuild or a frame whose flow was not computed. bool prior_valid; + // Which estimator produces the flow; see DIS_ENGINE_*. The Farneback parameters, and the + // four distinct entries of its inverted applicability moments. + int flow_engine; + int fb_iterations; + int fb_radius; + float fb_window_sigma; + float fb_poly_sigma; + float fb_robust; + bool fb_hybrid_vr; // DIS_ENGINE_HYBRID: refine its DIS levels variationally + uint32_t fb_hybrid_level; // DIS_ENGINE_HYBRID: the first level left to Farneback + uint32_t fb_hybrid_fp; // DIS_ENGINE_HYBRID: cap on refinement fixed-point passes (0: none) + uint32_t fb_hybrid_sor; // DIS_ENGINE_HYBRID: cap on SOR sweeps per pass (0: none) + bool fb_hybrid_l1_start; // DIS_ENGINE_HYBRID on two DIS levels: Farneback's level 1 as a start + bool fb_hybrid_vr_upper; // DIS_ENGINE_HYBRID: refine the DIS levels above 0 as well + float fb_ig[4]; + // Hardware motion hint (GL_QCOM_motion_estimation). me is NULL whenever the hint is off: // disabled, unsupported, or the pyramid too shallow for the level it seeds. bool hw_motion; @@ -299,8 +372,24 @@ typedef struct { int coarseLevel; int hintLevel; int priorLevels; + int fbLevels; } DisInversePC; +typedef struct { + float sigma; + float ig11; + float ig03; + float ig33; + float ig55; +} DisFbPolyPC; + +typedef struct { + int radius; + float sigma; + int useFlow; + float robust; +} DisFbSumPC; + typedef struct { int dist; } DisPropPC; @@ -489,11 +578,18 @@ static uint32_t dis_collect_images(VkrDis* d, DisImage** out, uint32_t cap) { DIS_PUSH(&d->side); DIS_PUSH(&d->flow_out); DIS_PUSH(&d->me_field); + for (uint32_t s = 0; s < DIS_SLOTS; s++) { + DIS_PUSH(&d->fb_b[s]); + DIS_PUSH(&d->fb_c[s]); + } + DIS_PUSH(&d->fb_sg); + DIS_PUSH(&d->fb_sh); + DIS_PUSH(&d->fb_l1); #undef DIS_PUSH return n; } -#define DIS_MAX_OWNED_IMAGES 48u +#define DIS_MAX_OWNED_IMAGES 64u static void dis_prime_layouts(VkrDis* d, VkCommandBuffer cmd) { if (d->layouts_primed) return; @@ -646,10 +742,11 @@ static VkPipeline dis_create_compute_pipeline(VkrDis* d, const uint32_t* code, s } static bool dis_create_pipelines(VkrDis* d) { - // Samplers at 0-4 and 6 (6 is only the search's previous-pair flow), the storage image at 5. - VkDescriptorSetLayoutBinding bindings[7]; + // Samplers at 0-4, 6 and 7 (6 and 7 are only the search's previous-pair flow and the + // hybrid's Farneback field), the storage image at 5. + VkDescriptorSetLayoutBinding bindings[8]; memset(bindings, 0, sizeof(bindings)); - for (uint32_t i = 0; i < 7; i++) { + for (uint32_t i = 0; i < 8; i++) { bindings[i].binding = i; bindings[i].descriptorType = i == 5 ? VK_DESCRIPTOR_TYPE_STORAGE_IMAGE : VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; @@ -660,7 +757,7 @@ static bool dis_create_pipelines(VkrDis* d) { VkDescriptorSetLayoutCreateInfo li; memset(&li, 0, sizeof(li)); li.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; - li.bindingCount = 7; + li.bindingCount = 8; li.pBindings = bindings; if (vkd.CreateDescriptorSetLayout(d->device, &li, NULL, &d->set_layout) != VK_SUCCESS) { return false; @@ -691,7 +788,8 @@ static bool dis_create_pipelines(VkrDis* d) { const uint32_t vr_sets = (DIS_SLOTS + DIS_VR_SHARED_SETS) * DIS_VR_LEVELS; const uint32_t hist_sets = DIS_SLOTS * 2u; - const uint32_t vr_layout_sets = vr_sets + hist_sets; + const uint32_t fb_sets = DIS_SLOTS * DIS_MAX_LEVELS * 3u + DIS_MAX_LEVELS + DIS_SLOTS + 1u; + const uint32_t vr_layout_sets = vr_sets + hist_sets + fb_sets; const uint32_t total_sets = shared_sets + vr_layout_sets; // The history sets are allocated from vr_set_layout, so they consume the full @@ -787,6 +885,9 @@ static bool dis_create_pipelines(VkrDis* d) { d->pass_vr_sor.pipeline = dis_create_compute_pipeline_with_layout(d, dis_vr_sor_comp, dis_vr_sor_comp_size, d->vr_pipeline_layout, NULL); d->pass_vr_add.pipeline = dis_create_compute_pipeline_with_layout(d, dis_vr_add_comp, dis_vr_add_comp_size, d->vr_pipeline_layout, NULL); d->pass_hist.pipeline = dis_create_compute_pipeline_with_layout(d, dis_hist_comp, dis_hist_comp_size, d->vr_pipeline_layout, NULL); + d->pass_fb_poly.pipeline = dis_create_compute_pipeline_with_layout(d, dis_fb_poly_comp, dis_fb_poly_comp_size, d->vr_pipeline_layout, NULL); + d->pass_fb_hsum.pipeline = dis_create_compute_pipeline_with_layout(d, dis_fb_hsum_comp, dis_fb_hsum_comp_size, d->vr_pipeline_layout, NULL); + d->pass_fb_vsolve.pipeline = dis_create_compute_pipeline_with_layout(d, dis_fb_vsolve_comp, dis_fb_vsolve_comp_size, d->vr_pipeline_layout, NULL); d->pass_side.pipeline = dis_create_compute_pipeline(d, dis_side_comp, dis_side_comp_size); d->pass_pack.pipeline = dis_create_compute_pipeline(d, dis_flow_pack_comp, dis_flow_pack_comp_size); @@ -843,7 +944,8 @@ static bool dis_create_pipelines(VkrDis* d) { !d->pass_vr_prep.pipeline || !d->pass_vr_d1.pipeline || !d->pass_vr_d2.pipeline || !d->pass_vr_w.pipeline || !d->pass_vr_coef.pipeline || !d->pass_vr_sor.pipeline || !d->pass_vr_add.pipeline || !d->pass_hist.pipeline || !d->pass_side.pipeline || - !d->pass_pack.pipeline || !d->pass_me_luma.pipeline) { + !d->pass_pack.pipeline || !d->pass_me_luma.pipeline || + !d->pass_fb_poly.pipeline || !d->pass_fb_hsum.pipeline || !d->pass_fb_vsolve.pipeline) { return false; } return true; @@ -889,6 +991,43 @@ static uint32_t dis_sparse_extent(uint32_t extent) { return extent > 8u ? 1u + (extent - 8u) / DIS_PATCH_STRIDE : 1u; } +// The four distinct entries of the inverted 6x6 moment matrix of Farneback's polynomial fit +// under a normalised Gaussian applicability of half-width n (OpenCV's FarnebackPrepareGaussian). +// The matrix is block-structured: 1 and 2 (x, y) are diagonal, 5 (xy) is diagonal, and +// 0, 3, 4 (1, x^2, y^2) couple as [[g00 g11 g11] [g11 g33 g55] [g11 g55 g33]]. +static void dis_fb_moments(int n, double sigma, float out[4]) { + double g[16]; + double s = 0.0; + for (int x = -n; x <= n; x++) { + g[x + n] = exp(-x * x / (2.0 * sigma * sigma)); + s += g[x + n]; + } + for (int x = -n; x <= n; x++) g[x + n] /= s; + + double g00 = 0.0, g11 = 0.0, g33 = 0.0, g55 = 0.0; + for (int y = -n; y <= n; y++) { + for (int x = -n; x <= n; x++) { + const double w = g[y + n] * g[x + n]; + g00 += w; + g11 += w * x * x; + g33 += w * x * x * x * x; + g55 += w * x * x * y * y; + } + } + // Inverse of the coupled block; by symmetry only its (0,0)-row entry against x^2 and the + // x^2 diagonal are needed. + const double m[3][3] = {{g00, g11, g11}, {g11, g33, g55}, {g11, g55, g33}}; + const double det = m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) + - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0]) + + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]); + const double inv01 = (m[0][2] * m[2][1] - m[0][1] * m[2][2]) / det; + const double inv11 = (m[0][0] * m[2][2] - m[0][2] * m[2][0]) / det; + out[0] = (float)(1.0 / g11); // ig11 + out[1] = (float)inv01; // ig03 + out[2] = (float)inv11; // ig33 + out[3] = (float)(1.0 / g55); // ig55 +} + static uint32_t dis_prop_steps_for(uint32_t level, uint32_t levels, uint32_t floor_steps) { static const uint32_t profile[DIS_PROP_STEPS_MAX] = {4u, 3u, 2u, 1u}; const uint32_t from_coarse = (levels - 1u) - level; @@ -995,6 +1134,7 @@ static void dis_write_all_descriptors(VkrDis* d) { dis_batch_sampled(d, &b, d->inverse_sets[s][l], 4, d->view_me_field, d->sampler); dis_batch_storage(d, &b, d->inverse_sets[s][l], 5, d->view_sparse[l]); dis_batch_sampled(d, &b, d->inverse_sets[s][l], 6, d->view_flow_out, d->sampler); + dis_batch_sampled(d, &b, d->inverse_sets[s][l], 7, d->view_fb_l1, d->sampler); dis_batch_sampled(d, &b, d->prop_ab_sets[s][l], 0, d->view_flow_luma[prev][l], d->sampler); dis_batch_sampled(d, &b, d->prop_ab_sets[s][l], 1, d->view_flow_luma[next][l], d->sampler); @@ -1078,6 +1218,51 @@ static void dis_write_all_descriptors(VkrDis* d) { dis_batch_sampled(d, &b, d->pack_set, 0, d->view_flow_refined[0], d->sampler); dis_batch_storage(d, &b, d->pack_set, 5, d->view_flow_out); + // Farneback. The pair ending in slot s runs from prev to s; a level starts from the level + // above (normalised uv reads the same at any size), the coarsest from the previous pair's + // flow, and later iterations from the level's own field. + for (uint32_t l = 0; l < L; l++) { + for (uint32_t s = 0; s < DIS_SLOTS; s++) { + const uint32_t prev = (s + DIS_SLOTS - 1u) % DIS_SLOTS; + dis_batch_sampled(d, &b, d->fb_poly_sets[s][l], 0, d->view_flow_luma[s][l], d->sampler); + dis_batch_storage(d, &b, d->fb_poly_sets[s][l], DIS_VR_FIRST_STORAGE, d->view_fb_b[s][l]); + dis_batch_storage(d, &b, d->fb_poly_sets[s][l], DIS_VR_FIRST_STORAGE + 1, d->view_fb_c[s][l]); + for (uint32_t k = 0; k < 2; k++) { + VkDescriptorSet set = d->fb_hsum_sets[s][l][k]; + // The coarsest level starts from the previous pair's flow: on a hard camera turn + // that warm start is what keeps up with the motion. + const VkImageView start = k == 1 ? d->view_flow_refined[l] + : (l + 1 < L ? d->view_flow_refined[l + 1] : d->view_flow_out); + dis_batch_sampled(d, &b, set, 0, d->view_fb_b[prev][l], d->sampler); + dis_batch_sampled(d, &b, set, 1, d->view_fb_c[prev][l], d->sampler); + dis_batch_sampled(d, &b, set, 2, d->view_fb_b[s][l], d->sampler); + dis_batch_sampled(d, &b, set, 3, d->view_fb_c[s][l], d->sampler); + dis_batch_sampled(d, &b, set, 4, start, d->sampler); + dis_batch_storage(d, &b, set, DIS_VR_FIRST_STORAGE, d->view_fb_sg[l]); + dis_batch_storage(d, &b, set, DIS_VR_FIRST_STORAGE + 1, d->view_fb_sh[l]); + } + } + dis_batch_sampled(d, &b, d->fb_vsolve_sets[l], 0, d->view_fb_sg[l], d->sampler); + dis_batch_sampled(d, &b, d->fb_vsolve_sets[l], 1, d->view_fb_sh[l], d->sampler); + dis_batch_storage(d, &b, d->fb_vsolve_sets[l], DIS_VR_FIRST_STORAGE, d->view_flow_refined[l]); + } + // Farneback's level 1 into fb_l1 (the hybrid with DIS on level 1 as well). + if (L > 1) { + for (uint32_t s = 0; s < DIS_SLOTS; s++) { + const uint32_t prev = (s + DIS_SLOTS - 1u) % DIS_SLOTS; + VkDescriptorSet set = d->fb_hsum1_sets[s]; + dis_batch_sampled(d, &b, set, 0, d->view_fb_b[prev][1], d->sampler); + dis_batch_sampled(d, &b, set, 1, d->view_fb_c[prev][1], d->sampler); + dis_batch_sampled(d, &b, set, 2, d->view_fb_b[s][1], d->sampler); + dis_batch_sampled(d, &b, set, 3, d->view_fb_c[s][1], d->sampler); + dis_batch_sampled(d, &b, set, 4, d->view_fb_l1, d->sampler); + dis_batch_storage(d, &b, set, DIS_VR_FIRST_STORAGE, d->view_fb_sg[1]); + dis_batch_storage(d, &b, set, DIS_VR_FIRST_STORAGE + 1, d->view_fb_sh[1]); + } + dis_batch_sampled(d, &b, d->fb_vsolve1_set, 0, d->view_fb_sg[1], d->sampler); + dis_batch_sampled(d, &b, d->fb_vsolve1_set, 1, d->view_fb_sh[1], d->sampler); + dis_batch_storage(d, &b, d->fb_vsolve1_set, DIS_VR_FIRST_STORAGE, d->view_fb_l1); + } dis_batch_flush(d, &b); } @@ -1089,6 +1274,7 @@ static void dis_destroy_views(VkrDis* d) { dis_destroy_view(d, &d->view_side); dis_destroy_view(d, &d->view_flow_out); dis_destroy_view(d, &d->view_me_field); + dis_destroy_view(d, &d->view_fb_l1); for (uint32_t l = 0; l < DIS_MAX_LEVELS; l++) { dis_destroy_view(d, &d->view_vr_prep[l]); dis_destroy_view(d, &d->view_vr_d1[l]); @@ -1102,7 +1288,11 @@ static void dis_destroy_views(VkrDis* d) { for (uint32_t s = 0; s < DIS_SLOTS; s++) { dis_destroy_view(d, &d->view_flow_color[s][l]); dis_destroy_view(d, &d->view_flow_luma[s][l]); + dis_destroy_view(d, &d->view_fb_b[s][l]); + dis_destroy_view(d, &d->view_fb_c[s][l]); } + dis_destroy_view(d, &d->view_fb_sg[l]); + dis_destroy_view(d, &d->view_fb_sh[l]); dis_destroy_view(d, &d->view_grad[l]); dis_destroy_view(d, &d->view_sparse[l]); dis_destroy_view(d, &d->view_sparse_b[l]); @@ -1138,6 +1328,13 @@ static void dis_destroy_images(VkrDis* d) { dis_destroy_image(d, &d->side); dis_destroy_image(d, &d->flow_out); dis_destroy_image(d, &d->me_field); + for (uint32_t s = 0; s < DIS_SLOTS; s++) { + dis_destroy_image(d, &d->fb_b[s]); + dis_destroy_image(d, &d->fb_c[s]); + } + dis_destroy_image(d, &d->fb_sg); + dis_destroy_image(d, &d->fb_sh); + dis_destroy_image(d, &d->fb_l1); dis_destroy_me(d); } @@ -1335,6 +1532,19 @@ static bool dis_create_resources(VkrDis* d, uint32_t w, uint32_t h, uint32_t ful if (!dis_create_image(d, &d->me_field, hint_w ? hint_w : 1u, hint_h ? hint_h : 1u, VK_FORMAT_R32G32_SFLOAT, 1, VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT)) return false; + for (uint32_t s = 0; s < DIS_SLOTS; s++) { + if (!dis_create_image(d, &d->fb_b[s], w, h, VK_FORMAT_R32G32B32A32_SFLOAT, L, + VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT)) return false; + if (!dis_create_image(d, &d->fb_c[s], w, h, VK_FORMAT_R32_SFLOAT, L, + VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT)) return false; + } + if (!dis_create_image(d, &d->fb_sg, w, h, VK_FORMAT_R32G32B32A32_SFLOAT, L, + VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT)) return false; + if (!dis_create_image(d, &d->fb_sh, w, h, VK_FORMAT_R32_SFLOAT, L, + VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT)) return false; + if (!dis_create_image(d, &d->fb_l1, w > 1u ? w >> 1 : 1u, h > 1u ? h >> 1 : 1u, + VK_FORMAT_R32G32_SFLOAT, 1, + VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT)) return false; for (uint32_t s = 0; s < DIS_SLOTS; s++) { if (!dis_create_view(d, d->color[s].image, format, 0, 1, &d->view_color[s])) return false; @@ -1359,6 +1569,12 @@ static bool dis_create_resources(VkrDis* d, uint32_t w, uint32_t h, uint32_t ful if (!dis_create_view(d, d->vr_dw[0].image, VK_FORMAT_R32G32_SFLOAT, l, 1, &d->view_vr_dw[0][l])) return false; if (!dis_create_view(d, d->vr_dw[1].image, VK_FORMAT_R32G32_SFLOAT, l, 1, &d->view_vr_dw[1][l])) return false; if (!dis_create_view(d, d->flow_refined.image, VK_FORMAT_R32G32_SFLOAT, l, 1, &d->view_flow_refined[l])) return false; + for (uint32_t s = 0; s < DIS_SLOTS; s++) { + if (!dis_create_view(d, d->fb_b[s].image, VK_FORMAT_R32G32B32A32_SFLOAT, l, 1, &d->view_fb_b[s][l])) return false; + if (!dis_create_view(d, d->fb_c[s].image, VK_FORMAT_R32_SFLOAT, l, 1, &d->view_fb_c[s][l])) return false; + } + if (!dis_create_view(d, d->fb_sg.image, VK_FORMAT_R32G32B32A32_SFLOAT, l, 1, &d->view_fb_sg[l])) return false; + if (!dis_create_view(d, d->fb_sh.image, VK_FORMAT_R32_SFLOAT, l, 1, &d->view_fb_sh[l])) return false; } if (!dis_create_view(d, d->interp_out.image, VK_FORMAT_R8G8B8A8_UNORM, 0, 1, &d->view_interp_out)) return false; if (!dis_create_view(d, d->hist[0].image, VK_FORMAT_R32_SFLOAT, 0, 1, &d->view_hist[0])) return false; @@ -1366,6 +1582,7 @@ static bool dis_create_resources(VkrDis* d, uint32_t w, uint32_t h, uint32_t ful if (!dis_create_view(d, d->side.image, VK_FORMAT_R32_SFLOAT, 0, 1, &d->view_side)) return false; if (!dis_create_view(d, d->flow_out.image, VK_FORMAT_R16G16_SFLOAT, 0, 1, &d->view_flow_out)) return false; if (!dis_create_view(d, d->me_field.image, VK_FORMAT_R32G32_SFLOAT, 0, 1, &d->view_me_field)) return false; + if (!dis_create_view(d, d->fb_l1.image, VK_FORMAT_R32G32_SFLOAT, 0, 1, &d->view_fb_l1)) return false; vkr_dis_reset(d); dis_write_all_descriptors(d); @@ -1437,6 +1654,21 @@ static bool dis_allocate_sets(VkrDis* d) { d->vr_sor_ba_set[l] = vr_sets[5]; d->vr_add_set[l] = vr_sets[6]; } + + for (uint32_t l = 0; l < DIS_MAX_LEVELS; l++) { + for (uint32_t s = 0; s < DIS_SLOTS; s++) { + VkDescriptorSet fb[3]; + if (!dis_alloc(d, d->vr_set_layout, 3, fb)) return false; + d->fb_poly_sets[s][l] = fb[0]; + d->fb_hsum_sets[s][l][0] = fb[1]; + d->fb_hsum_sets[s][l][1] = fb[2]; + } + if (!dis_alloc(d, d->vr_set_layout, 1, &d->fb_vsolve_sets[l])) return false; + } + for (uint32_t s = 0; s < DIS_SLOTS; s++) { + if (!dis_alloc(d, d->vr_set_layout, 1, &d->fb_hsum1_sets[s])) return false; + } + if (!dis_alloc(d, d->vr_set_layout, 1, &d->fb_vsolve1_set)) return false; return true; } @@ -1605,6 +1837,50 @@ VkrDis* vkr_dis_create(VkDevice device, VkPhysicalDevice physical_device) { } #endif d->hint_level = -1; + + // Flow estimator: DIS unless asked for Farneback, by OPENFLOW_ENGINE=farneback in the + // environment (the desktop bench) or, on a device, + // adb shell setprop debug.winnative.openflow.engine farneback + // The hybrid is the default: on the bench it matches DIS overall (better on fast camera + // motion, worse on small fast objects with a periodic texture) at 2.3x less flow time on + // Adreno 750, nearly all of DIS's time there being the level-0 SOR sweeps. + d->flow_engine = DIS_ENGINE_HYBRID; + const char* engine = getenv("OPENFLOW_ENGINE"); +#ifdef __ANDROID__ + char engine_prop[PROP_VALUE_MAX] = {0}; + if (__system_property_get("debug.winnative.openflow.engine", engine_prop) > 0) engine = engine_prop; +#endif + if (engine && strcmp(engine, "dis") == 0) d->flow_engine = DIS_ENGINE_DIS; + if (engine && strcmp(engine, "farneback") == 0) d->flow_engine = DIS_ENGINE_FARNEBACK; + d->fb_iterations = DIS_FB_ITERATIONS; + d->fb_radius = DIS_FB_RADIUS; + d->fb_window_sigma = DIS_FB_WINDOW_SIGMA; + d->fb_poly_sigma = DIS_FB_POLY_SIGMA; + d->fb_robust = DIS_FB_ROBUST; + d->fb_hybrid_vr = true; + d->fb_hybrid_level = 2; + d->fb_hybrid_l1_start = true; + d->fb_hybrid_vr_upper = false; + d->fb_hybrid_fp = DIS_HYB_FIXED_POINT; + d->fb_hybrid_sor = DIS_HYB_SOR; + { + const char* v; + if ((v = getenv("OPENFLOW_FB_ROBUST")) != NULL) d->fb_robust = (float)atof(v); + if ((v = getenv("OPENFLOW_HYB_VR")) != NULL) d->fb_hybrid_vr = atoi(v) != 0; + if ((v = getenv("OPENFLOW_HYB_LEVEL")) != NULL) d->fb_hybrid_level = (uint32_t)atoi(v); + if ((v = getenv("OPENFLOW_HYB_FP")) != NULL) d->fb_hybrid_fp = (uint32_t)atoi(v); + if ((v = getenv("OPENFLOW_HYB_SOR")) != NULL) d->fb_hybrid_sor = (uint32_t)atoi(v); + if ((v = getenv("OPENFLOW_HYB_L1START")) != NULL) d->fb_hybrid_l1_start = atoi(v) != 0; + if ((v = getenv("OPENFLOW_HYB_VR1")) != NULL) d->fb_hybrid_vr_upper = atoi(v) != 0; + if ((v = getenv("OPENFLOW_FB_ITERS")) != NULL) d->fb_iterations = atoi(v); + if ((v = getenv("OPENFLOW_FB_RADIUS")) != NULL) d->fb_radius = atoi(v); + if ((v = getenv("OPENFLOW_FB_WSIGMA")) != NULL) d->fb_window_sigma = (float)atof(v); + if ((v = getenv("OPENFLOW_FB_PSIGMA")) != NULL) d->fb_poly_sigma = (float)atof(v); + } + dis_fb_moments(DIS_FB_POLY_N, d->fb_poly_sigma, d->fb_ig); + DIS_LOGI("OpenFlow flow estimator: %s", + d->flow_engine == DIS_ENGINE_DIS ? "DIS" + : d->flow_engine == DIS_ENGINE_FARNEBACK ? "Farneback" : "Farneback + DIS levels 1-0"); vkd.GetPhysicalDeviceMemoryProperties(physical_device, &d->mem_props); d->luma_format = dis_pick_luma_format(d); if (!dis_audit_formats(d)) { @@ -1647,6 +1923,9 @@ void vkr_dis_destroy(VkrDis* d) { if (d->pass_hist.pipeline) vkd.DestroyPipeline(d->device, d->pass_hist.pipeline, NULL); if (d->pass_side.pipeline) vkd.DestroyPipeline(d->device, d->pass_side.pipeline, NULL); if (d->pass_pack.pipeline) vkd.DestroyPipeline(d->device, d->pass_pack.pipeline, NULL); + if (d->pass_fb_poly.pipeline) vkd.DestroyPipeline(d->device, d->pass_fb_poly.pipeline, NULL); + if (d->pass_fb_hsum.pipeline) vkd.DestroyPipeline(d->device, d->pass_fb_hsum.pipeline, NULL); + if (d->pass_fb_vsolve.pipeline) vkd.DestroyPipeline(d->device, d->pass_fb_vsolve.pipeline, NULL); if (d->pass_me_luma.pipeline) vkd.DestroyPipeline(d->device, d->pass_me_luma.pipeline, NULL); if (d->me_pool) vkd.DestroyDescriptorPool(d->device, d->me_pool, NULL); if (d->me_pipeline_layout) vkd.DestroyPipelineLayout(d->device, d->me_pipeline_layout, NULL); @@ -1872,6 +2151,16 @@ uint32_t vkr_dis_plan(VkrDis* d, uint32_t capacity, uint64_t source_frames) { raw = want_down; } if (raw < 1) raw = 1; + // Never more frames than the display can show. dis_gen_for_ratio rounds the output count + // up so as not to fall short of the target, but when the target is the refresh rate + // itself that overshoots it: 28.4 fps at 120 Hz is a ratio of 4.22, rounded to 5 outputs + // - 142 fps. The swapchain then runs out of free images, generated frames time out on + // acquire and are dropped, and the pair shows one frame fewer than its neighbours: a + // visible jerk. Capped at the display rate (2% for the rate estimate's noise). + if (d->refresh_rate > 0.0f) { + const int max_gen = (int)floorf(d->refresh_rate * 1.02f / source_rate) - 1; + if (max_gen >= 1 && raw > max_gen) raw = max_gen; + } } if (raw > d->planned_gen) { @@ -1923,6 +2212,11 @@ static void dis_vr_level(VkrDis* d, VkCommandBuffer cmd, uint32_t slot, uint32_t uint32_t vr_fixed_point = 0; uint32_t vr_sor = 0; dis_vr_budget(refine, l, &vr_fixed_point, &vr_sor); + // The hybrid's level 0 starts from Farneback's field, which is already close. + if (d->flow_engine == DIS_ENGINE_HYBRID) { + if (d->fb_hybrid_fp > 0 && vr_fixed_point > d->fb_hybrid_fp) vr_fixed_point = d->fb_hybrid_fp; + if (d->fb_hybrid_sor > 0 && vr_sor > d->fb_hybrid_sor) vr_sor = d->fb_hybrid_sor; + } vkd.CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->pass_vr_prep.pipeline); vkd.CmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->vr_pipeline_layout, 0, 1, @@ -2143,6 +2437,69 @@ static VkCommandBuffer dis_hardware_motion(VkrDis* d, VkCommandBuffer cmd, uint3 return cmd; } +// Farneback: the polynomial expansion of every level of the frame just put in slot. It is the +// second frame of this pair and the first of the next, so each frame is expanded once. +static void dis_fb_expand(VkrDis* d, VkCommandBuffer cmd, uint32_t slot, uint32_t L, + uint32_t w, uint32_t h) { + DisFbPolyPC ppc; + ppc.sigma = d->fb_poly_sigma; + ppc.ig11 = d->fb_ig[0]; + ppc.ig03 = d->fb_ig[1]; + ppc.ig33 = d->fb_ig[2]; + ppc.ig55 = d->fb_ig[3]; + vkd.CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->pass_fb_poly.pipeline); + vkd.CmdPushConstants(cmd, d->vr_pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(ppc), &ppc); + for (uint32_t l = 0; l < L; l++) { + const uint32_t lw = w >> l; + const uint32_t lh = h >> l; + vkd.CmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->vr_pipeline_layout, 0, 1, + &d->fb_poly_sets[slot][l], 0, NULL); + vkd.CmdDispatch(cmd, (lw + DIS_LOCAL_SIZE - 1) / DIS_LOCAL_SIZE, + (lh + DIS_LOCAL_SIZE - 1) / DIS_LOCAL_SIZE, 1); + } + dis_compute_barrier(cmd); +} + +// Farneback flow for the pair ending in slot, into flow_refined, from the coarsest level down +// to min_level: each level starts from the one above (the coarsest from the previous pair's +// flow, or zero), then fb_iterations of equations summed over the window and solved. Two +// dispatches an iteration. +static void dis_farneback(VkrDis* d, VkCommandBuffer cmd, uint32_t slot, uint32_t L, + uint32_t w, uint32_t h, uint32_t min_level, bool l1_apart) { + for (uint32_t li = 0; li + min_level < L; li++) { + const uint32_t l = L - 1u - li; + const uint32_t lw = w >> l; + const uint32_t lh = h >> l; + for (int it = 0; it < d->fb_iterations; it++) { + DisFbSumPC spc; + spc.radius = d->fb_radius; + spc.sigma = d->fb_window_sigma; + spc.useFlow = (it > 0 || l + 1u < L || d->prior_valid) ? 1 : 0; + spc.robust = d->fb_robust; + + vkd.CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->pass_fb_hsum.pipeline); + vkd.CmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->vr_pipeline_layout, 0, 1, + (l == 1 && l1_apart && it > 0) ? &d->fb_hsum1_sets[slot] + : &d->fb_hsum_sets[slot][l][it > 0 ? 1 : 0], 0, NULL); + vkd.CmdPushConstants(cmd, d->vr_pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, + sizeof(spc), &spc); + vkd.CmdDispatch(cmd, (lw + 63u) / 64u, lh, 1); + dis_compute_barrier(cmd); + + vkd.CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->pass_fb_vsolve.pipeline); + vkd.CmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->vr_pipeline_layout, 0, 1, + (l == 1 && l1_apart) ? &d->fb_vsolve1_set : &d->fb_vsolve_sets[l], + 0, NULL); + vkd.CmdPushConstants(cmd, d->vr_pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, + sizeof(spc), &spc); + vkd.CmdDispatch(cmd, (lw + DIS_LOCAL_SIZE - 1) / DIS_LOCAL_SIZE, + (lh + DIS_LOCAL_SIZE - 1) / DIS_LOCAL_SIZE, 1); + dis_compute_barrier(cmd); + } + } + DIS_PROF(cmd, "farneback"); +} + VkCommandBuffer vkr_dis_process_ex(VkrDis* d, VkCommandBuffer cmd, VkImage source, uint32_t width, uint32_t height, uint32_t generations, VkrDisFlushFn flush, void* flush_user) { @@ -2220,87 +2577,108 @@ VkCommandBuffer vkr_dis_process_ex(VkrDis* d, VkCommandBuffer cmd, VkImage sourc dis_compute_barrier(cmd); DIS_PROF(cmd, "copy+luma"); + const bool farneback = d->flow_engine == DIS_ENGINE_FARNEBACK; + const bool hybrid = d->flow_engine == DIS_ENGINE_HYBRID; + if (farneback || hybrid) dis_fb_expand(d, cmd, slot, L, w, h); + if (!wants_flow) { d->prior_valid = false; return cmd; } - DisGradientPC gpc; - gpc.lesser = 3.0f; - gpc.upper = 10.0f; - gpc.normVal = 1.0f / (2.0f * 10.0f + 4.0f * 3.0f); - - for (uint32_t l = 0; l < L; l++) { - const uint32_t lw = w >> l; - const uint32_t lh = h >> l; - vkd.CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->pass_gradient.pipeline); - vkd.CmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->pipeline_layout, 0, 1, - &d->grad_sets[slot][l], 0, NULL); - vkd.CmdPushConstants(cmd, d->pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(gpc), &gpc); - vkd.CmdDispatch(cmd, (lw + DIS_LOCAL_SIZE - 1) / DIS_LOCAL_SIZE, - (lh + DIS_LOCAL_SIZE - 1) / DIS_LOCAL_SIZE, 1); - } + if (farneback) { + dis_farneback(d, cmd, slot, L, w, h, 0, false); + } else { + // The hybrid's coarse levels are Farneback's; the search on the levels below starts from + // them (its coarse estimate is flow_refined[l + 1]). + if (hybrid) { + // With DIS on level 1 too, Farneback still solves level 1 - into fb_l1, as the other + // start of level 0's search. + const bool l1_apart = d->fb_hybrid_l1_start && d->fb_hybrid_level == 2u && L > 2; + dis_farneback(d, cmd, slot, L, w, h, l1_apart ? 1u : d->fb_hybrid_level, l1_apart); + } + DisGradientPC gpc; + gpc.lesser = 3.0f; + gpc.upper = 10.0f; + gpc.normVal = 1.0f / (2.0f * 10.0f + 4.0f * 3.0f); - dis_compute_barrier(cmd); - DIS_PROF(cmd, "gradient"); - - static const char* const k_search[DIS_MAX_LEVELS] = {"L0 search", "L1 search", "L2 search", "L3 search", "L4 search", "L5 search", "L6 search", "L7 search"}; - static const char* const k_vr[DIS_MAX_LEVELS] = {"L0 refine", "L1 refine", "L2 refine", "L3 refine", "L4 refine", "L5 refine", "L6 refine", "L7 refine"}; - static const char* const k_inv[DIS_MAX_LEVELS] = {"L0 inverse", "L1 inverse", "L2 inverse", "L3 inverse", "L4 inverse", "L5 inverse", "L6 inverse", "L7 inverse"}; - static const char* const k_prop[DIS_MAX_LEVELS] = {"L0 propagate", "L1 propagate", "L2 propagate", "L3 propagate", "L4 propagate", "L5 propagate", "L6 propagate", "L7 propagate"}; - (void)k_search; (void)k_vr; (void)k_inv; (void)k_prop; - for (uint32_t li = 0; li < L; li++) { - const uint32_t l = coarse - li; - // The hardware field already stands in for this level and everything above it. - if (d->me_primary_frame && l >= d->me_level) continue; - const uint32_t lw = w >> l; - const uint32_t lh = h >> l; - const uint32_t spw = dis_sparse_extent(lw); - const uint32_t sph = dis_sparse_extent(lh); - - DisInversePC ipc; - ipc.level = (int)l; - ipc.coarseLevel = (int)coarse; - ipc.hintLevel = d->hint_level; - ipc.priorLevels = d->prior_valid ? (int)((1u << L) - 1u) : 0; - vkd.CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->pass_inverse.pipeline); - vkd.CmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->pipeline_layout, 0, 1, - &d->inverse_sets[slot][l], 0, NULL); - vkd.CmdPushConstants(cmd, d->pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(ipc), &ipc); - vkd.CmdDispatch(cmd, (spw + DIS_LOCAL_SIZE - 1) / DIS_LOCAL_SIZE, - (sph + DIS_LOCAL_SIZE - 1) / DIS_LOCAL_SIZE, 1); + for (uint32_t l = 0; l < L; l++) { + const uint32_t lw = w >> l; + const uint32_t lh = h >> l; + vkd.CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->pass_gradient.pipeline); + vkd.CmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->pipeline_layout, 0, 1, + &d->grad_sets[slot][l], 0, NULL); + vkd.CmdPushConstants(cmd, d->pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(gpc), &gpc); + vkd.CmdDispatch(cmd, (lw + DIS_LOCAL_SIZE - 1) / DIS_LOCAL_SIZE, + (lh + DIS_LOCAL_SIZE - 1) / DIS_LOCAL_SIZE, 1); + } dis_compute_barrier(cmd); - DIS_PROF(cmd, k_inv[l]); - - uint32_t prop_passes = dis_prop_steps_for(l, L, refine.prop_floor); - const uint32_t prop_doubling = prop_passes; - if (prop_passes & 1u) prop_passes++; - - for (uint32_t k = 0; k < prop_passes; k++) { - VkDescriptorSet prop_set = - (k & 1u) ? d->prop_ba_sets[slot][l] : d->prop_ab_sets[slot][l]; - DisPropPC ppc; - ppc.dist = k < prop_doubling ? (int)(1u << k) : 1; - vkd.CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->pass_propagate.pipeline); - vkd.CmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->pipeline_layout, 0, - 1, &prop_set, 0, NULL); - vkd.CmdPushConstants(cmd, d->pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, - sizeof(ppc), &ppc); + DIS_PROF(cmd, "gradient"); + + static const char* const k_search[DIS_MAX_LEVELS] = {"L0 search", "L1 search", "L2 search", "L3 search", "L4 search", "L5 search", "L6 search", "L7 search"}; + static const char* const k_vr[DIS_MAX_LEVELS] = {"L0 refine", "L1 refine", "L2 refine", "L3 refine", "L4 refine", "L5 refine", "L6 refine", "L7 refine"}; + static const char* const k_inv[DIS_MAX_LEVELS] = {"L0 inverse", "L1 inverse", "L2 inverse", "L3 inverse", "L4 inverse", "L5 inverse", "L6 inverse", "L7 inverse"}; + static const char* const k_prop[DIS_MAX_LEVELS] = {"L0 propagate", "L1 propagate", "L2 propagate", "L3 propagate", "L4 propagate", "L5 propagate", "L6 propagate", "L7 propagate"}; + (void)k_search; (void)k_vr; (void)k_inv; (void)k_prop; + for (uint32_t li = 0; li < L; li++) { + const uint32_t l = coarse - li; + // The hardware field already stands in for this level and everything above it. + if (d->me_primary_frame && l >= d->me_level) continue; + if (hybrid && l >= d->fb_hybrid_level) continue; + const uint32_t lw = w >> l; + const uint32_t lh = h >> l; + const uint32_t spw = dis_sparse_extent(lw); + const uint32_t sph = dis_sparse_extent(lh); + + DisInversePC ipc; + ipc.level = (int)l; + ipc.coarseLevel = (int)coarse; + ipc.hintLevel = d->hint_level; + ipc.priorLevels = d->prior_valid ? (int)((1u << L) - 1u) : 0; + ipc.fbLevels = (hybrid && d->fb_hybrid_l1_start && d->fb_hybrid_level == 2u && L > 2) ? 1 : 0; + vkd.CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->pass_inverse.pipeline); + vkd.CmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->pipeline_layout, 0, 1, + &d->inverse_sets[slot][l], 0, NULL); + vkd.CmdPushConstants(cmd, d->pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(ipc), &ipc); vkd.CmdDispatch(cmd, (spw + DIS_LOCAL_SIZE - 1) / DIS_LOCAL_SIZE, (sph + DIS_LOCAL_SIZE - 1) / DIS_LOCAL_SIZE, 1); dis_compute_barrier(cmd); - } - DIS_PROF(cmd, k_prop[l]); - - dis_dispatch(d, cmd, d->pass_densify.pipeline, d->densify_sets[slot][l], lw, lh); + DIS_PROF(cmd, k_inv[l]); + + uint32_t prop_passes = dis_prop_steps_for(l, L, refine.prop_floor); + const uint32_t prop_doubling = prop_passes; + if (prop_passes & 1u) prop_passes++; + + for (uint32_t k = 0; k < prop_passes; k++) { + VkDescriptorSet prop_set = + (k & 1u) ? d->prop_ba_sets[slot][l] : d->prop_ab_sets[slot][l]; + DisPropPC ppc; + ppc.dist = k < prop_doubling ? (int)(1u << k) : 1; + vkd.CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->pass_propagate.pipeline); + vkd.CmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->pipeline_layout, 0, + 1, &prop_set, 0, NULL); + vkd.CmdPushConstants(cmd, d->pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, + sizeof(ppc), &ppc); + vkd.CmdDispatch(cmd, (spw + DIS_LOCAL_SIZE - 1) / DIS_LOCAL_SIZE, + (sph + DIS_LOCAL_SIZE - 1) / DIS_LOCAL_SIZE, 1); + + dis_compute_barrier(cmd); + } + DIS_PROF(cmd, k_prop[l]); - dis_compute_barrier(cmd); - DIS_PROF(cmd, k_search[l]); + dis_dispatch(d, cmd, d->pass_densify.pipeline, d->densify_sets[slot][l], lw, lh); - if (l < refine.vr_levels) { - dis_vr_level(d, cmd, slot, l, lw, lh, &refine, true); - DIS_PROF(cmd, k_vr[l]); + dis_compute_barrier(cmd); + DIS_PROF(cmd, k_search[l]); + + if (l < refine.vr_levels) { + // Without the full refinement the pass still moves the dense field into + // flow_refined, where the rest of the pipeline reads level 0. + dis_vr_level(d, cmd, slot, l, lw, lh, &refine, + !hybrid || (d->fb_hybrid_vr && (l == 0 || d->fb_hybrid_vr_upper))); + DIS_PROF(cmd, k_vr[l]); + } } } diff --git a/app/src/main/res/values-b+es+419/strings.xml b/app/src/main/res/values-b+es+419/strings.xml index 4f2150523..33ed76e38 100644 --- a/app/src/main/res/values-b+es+419/strings.xml +++ b/app/src/main/res/values-b+es+419/strings.xml @@ -2723,7 +2723,7 @@ Ruta instalada: Generar fotogramas Desactivado LSFG - OpenFlow + OpenFG Multiplicador %1$dx Objetivo adaptativo diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index b5388d825..2b7486dcb 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -2736,7 +2736,7 @@ Installeret sti: Generér billeder Fra LSFG - OpenFlow + OpenFG Multiplikator %1$dx Adaptivt mål diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index bcaec5f29..50ba2ebc8 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -2727,7 +2727,7 @@ Installierter Pfad: Frames generieren Aus LSFG - OpenFlow + OpenFG Multiplikator %1$dx Adaptives Ziel diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index cba9a6413..db5325f42 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -2727,7 +2727,7 @@ Ruta instalada: Generar fotogramas Desactivado LSFG - OpenFlow + OpenFG Multiplicador %1$dx Objetivo adaptativo diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 5fba1869c..3c98a4b2d 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -2723,7 +2723,7 @@ Asennuspolku: Generoi ruutuja Pois LSFG - OpenFlow + OpenFG Kerroin %1$dx Mukautuva tavoite diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index b7af5b378..fa461b0f1 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -2727,7 +2727,7 @@ Chemin installé : Générer des images Désactivé LSFG - OpenFlow + OpenFG Multiplicateur %1$dx Cible adaptative diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index e00624c65..cf71adf13 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -2621,7 +2621,7 @@ फ़्रेम जनरेट करें बंद LSFG - OpenFlow + OpenFG गुणक %1$dx अनुकूली लक्ष्य diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 5add93458..cec2979ed 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -2725,7 +2725,7 @@ Percorso installato: Genera fotogrammi Disattivo LSFG - OpenFlow + OpenFG Moltiplicatore %1$dx Obiettivo adattivo diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 24e3f080c..c6c3bbe40 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -2723,7 +2723,7 @@ フレームを生成 オフ LSFG - OpenFlow + OpenFG 倍率 %1$dx アダプティブ目標 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index d2f9ce964..18b575fd6 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -2726,7 +2726,7 @@ 프레임 생성 사용 끄기 LSFG - OpenFlow + OpenFG 배수 %1$dx 적응형 목표 diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index 0d2bfff74..da82f2006 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -2723,7 +2723,7 @@ Installert bane: Generer bilder Av LSFG - OpenFlow + OpenFG Multiplikator %1$dx Adaptivt mål diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index e77528a5a..2a4a0580f 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -2731,7 +2731,7 @@ Zainstalowana ścieżka: Generuj klatki Wył. LSFG - OpenFlow + OpenFG Mnożnik %1$dx Cel adaptacyjny diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 5c1220a09..0d8c86824 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -2725,7 +2725,7 @@ Caminho instalado: Gerar quadros Desligado LSFG - OpenFlow + OpenFG Multiplicador %1$dx Alvo adaptativo diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index a0e641928..935e0f884 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -2723,7 +2723,7 @@ Caminho instalado: Gerar fotogramas Desligado LSFG - OpenFlow + OpenFG Multiplicador %1$dx Alvo adaptativo diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 24fcae306..33f03ceb5 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -2724,7 +2724,7 @@ Cale instalata: Generează cadre Oprit LSFG - OpenFlow + OpenFG Multiplicator %1$dx Țintă adaptivă diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 6ef965078..871b64092 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -2631,7 +2631,7 @@ Генерировать кадры Выкл. LSFG - OpenFlow + OpenFG Множитель %1$dx Адаптивная цель diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index f7c3dacac..1a77469d6 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -2723,7 +2723,7 @@ Installerad sökväg: Generera bilder Av LSFG - OpenFlow + OpenFG Multiplikator %1$dx Adaptivt mål diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 778a46a86..96723f61c 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -2723,7 +2723,7 @@ สร้างเฟรม ปิด LSFG - OpenFlow + OpenFG ตัวคูณ %1$dx เป้าหมายแบบปรับอัตโนมัติ diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 6bc31e576..20c75ea3e 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -2723,7 +2723,7 @@ Yüklü konum: Kare üret Kapalı LSFG - OpenFlow + OpenFG Çarpan %1$dx Uyarlanabilir hedef diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index f75c6c029..58630590b 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -2731,7 +2731,7 @@ Генерувати кадри Вимк. LSFG - OpenFlow + OpenFG Множник %1$dx Адаптивна ціль diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 6c2270815..18aae6052 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -2725,7 +2725,7 @@ 生成帧 关闭 LSFG - OpenFlow + OpenFG 倍数 %1$dx 自适应目标 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 0a2f21a79..39b9d9940 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -2724,7 +2724,7 @@ 生成影格 關閉 LSFG - OpenFlow + OpenFG 倍數 %1$dx 自適應目標 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 18d1d6564..176d9df74 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -920,7 +920,7 @@ E.g. META for META key, \n Generate Frames Off LSFG - OpenFlow + OpenFG Multiplier %1$dx Adaptive Target From 4ae9bc986f436df74a74ef47a6b7870358c1638b Mon Sep 17 00:00:00 2001 From: qwertypower <1qwertypower1@gmail.com> Date: Sat, 10 Oct 2026 23:39:34 +0300 Subject: [PATCH 02/18] WFG: generate straight into the swapchain, overlay occlusion, planner tolerance, rename in UI --- .../include/shaders/dis_interpolate.comp | 15 ++++++++++ app/src/main/cpp/openflow/src/vkr_dis.c | 5 ++-- .../cpp/winlator/vk/framegen/fg_present.c | 19 ++++++------ app/src/main/cpp/winlator/vk/vk_renderer.c | 30 ++++++++++++------- app/src/main/res/values-b+es+419/strings.xml | 6 ++-- app/src/main/res/values-da/strings.xml | 6 ++-- app/src/main/res/values-de/strings.xml | 6 ++-- app/src/main/res/values-es/strings.xml | 6 ++-- app/src/main/res/values-fi/strings.xml | 6 ++-- app/src/main/res/values-fr/strings.xml | 6 ++-- app/src/main/res/values-hi/strings.xml | 6 ++-- app/src/main/res/values-it/strings.xml | 6 ++-- app/src/main/res/values-ja/strings.xml | 6 ++-- app/src/main/res/values-ko/strings.xml | 6 ++-- app/src/main/res/values-no/strings.xml | 6 ++-- app/src/main/res/values-pl/strings.xml | 6 ++-- app/src/main/res/values-pt-rBR/strings.xml | 6 ++-- app/src/main/res/values-pt/strings.xml | 6 ++-- app/src/main/res/values-ro/strings.xml | 6 ++-- app/src/main/res/values-ru/strings.xml | 6 ++-- app/src/main/res/values-sv/strings.xml | 6 ++-- app/src/main/res/values-th/strings.xml | 6 ++-- app/src/main/res/values-tr/strings.xml | 6 ++-- app/src/main/res/values-uk/strings.xml | 6 ++-- app/src/main/res/values-zh-rCN/strings.xml | 6 ++-- app/src/main/res/values-zh-rTW/strings.xml | 6 ++-- app/src/main/res/values/strings.xml | 6 ++-- 27 files changed, 116 insertions(+), 91 deletions(-) diff --git a/app/src/main/cpp/openflow/include/shaders/dis_interpolate.comp b/app/src/main/cpp/openflow/include/shaders/dis_interpolate.comp index acf180fda..34cc1d289 100644 --- a/app/src/main/cpp/openflow/include/shaders/dis_interpolate.comp +++ b/app/src/main/cpp/openflow/include/shaders/dis_interpolate.comp @@ -55,6 +55,11 @@ layout(constant_id = 0) const int manualFlowFilter = 0; #define DIS_USE_HIST 1 #endif +// Hand a pixel that a warped sample fetches from a static overlay to the other frame. +#ifndef DIS_OVERLAY_OCCL +#define DIS_OVERLAY_OCCL 1 +#endif + #ifndef SIDE_LO #define SIDE_LO 4.0 #endif @@ -235,6 +240,16 @@ void main() { float div = textureLod(sideTex, uv, 0.0).r; float sure = smoothstep(SIDE_LO, SIDE_HI, abs(div)); single = mix(single, div > 0.0 ? c1 : c0, sure); +#endif +#if DIS_OVERLAY_OCCL + // Scenery that moves out from under a HUD panel was hidden in one frame of the pair, and + // the warp fetches the panel there. Where one sample sits on an overlay and the other and + // the pixel do not, the other one is the scenery. + float hp = textureLod(histTex, uv, 0.0).r; + float h0 = textureLod(histTex, uv0, 0.0).r; + float h1 = textureLod(histTex, uv1, 0.0).r; + single = mix(single, c1, smoothstep(0.3, 0.7, (1.0 - hp) * h0 * (1.0 - h1))); + single = mix(single, c0, smoothstep(0.3, 0.7, (1.0 - hp) * h1 * (1.0 - h0))); #endif result = mix(result, single, occl); } diff --git a/app/src/main/cpp/openflow/src/vkr_dis.c b/app/src/main/cpp/openflow/src/vkr_dis.c index 036bb28bf..c0042f29b 100644 --- a/app/src/main/cpp/openflow/src/vkr_dis.c +++ b/app/src/main/cpp/openflow/src/vkr_dis.c @@ -2156,9 +2156,10 @@ uint32_t vkr_dis_plan(VkrDis* d, uint32_t capacity, uint64_t source_frames) { // itself that overshoots it: 28.4 fps at 120 Hz is a ratio of 4.22, rounded to 5 outputs // - 142 fps. The swapchain then runs out of free images, generated frames time out on // acquire and are dropped, and the pair shows one frame fewer than its neighbours: a - // visible jerk. Capped at the display rate (2% for the rate estimate's noise). + // visible jerk. Capped at the display rate, with 5% for the rate estimate's noise: at 2% a + // 30 fps game reading 30.7 for a few frames already dropped to 2 generated frames. if (d->refresh_rate > 0.0f) { - const int max_gen = (int)floorf(d->refresh_rate * 1.02f / source_rate) - 1; + const int max_gen = (int)floorf(d->refresh_rate * 1.05f / source_rate) - 1; if (max_gen >= 1 && raw > max_gen) raw = max_gen; } } diff --git a/app/src/main/cpp/winlator/vk/framegen/fg_present.c b/app/src/main/cpp/winlator/vk/framegen/fg_present.c index 1607c5d67..d9b09a1f3 100644 --- a/app/src/main/cpp/winlator/vk/framegen/fg_present.c +++ b/app/src/main/cpp/winlator/vk/framegen/fg_present.c @@ -917,17 +917,18 @@ static void fg_record_and_present(FgPresenter* fg, FgImport* source, AImage* ima for (uint32_t g = 0; g < gen_count; g++) { FgTarget* generated = &fg->targets[FG_FRAMES_IN_FLIGHT + g]; if (fg->active_engine == FG_ENGINE_DIS) { - vkr_dis_generate_into(fg->dis, f->cmd, g, FG_FRAMES_IN_FLIGHT + g, generated->image, - generated->view, fg->extent.width, fg->extent.height, + // DIS blits its interpolated frame into whatever target it is given, so it goes + // straight into the swapchain image, without a generated target and second blit. + vkr_dis_generate_into(fg->dis, f->cmd, g, gen_index[g], fg->swapchain_images[gen_index[g]], + VK_NULL_HANDLE, fg->extent.width, fg->extent.height, VK_NULL_HANDLE); - fg_barrier(f->cmd, generated->image, VK_IMAGE_LAYOUT_GENERAL, - VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_TRANSFER_BIT, - VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, - VK_ACCESS_TRANSFER_READ_BIT); - } else { - vkr_lsfg_generate_into(fg->lsfg, f->cmd, g, FG_FRAMES_IN_FLIGHT + g, generated->image, - generated->view, fg->extent.width, fg->extent.height); + fg_barrier(f->cmd, fg->swapchain_images[gen_index[g]], VK_IMAGE_LAYOUT_GENERAL, + VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, 0); + continue; } + vkr_lsfg_generate_into(fg->lsfg, f->cmd, g, FG_FRAMES_IN_FLIGHT + g, generated->image, + generated->view, fg->extent.width, fg->extent.height); fg_barrier(f->cmd, fg->swapchain_images[gen_index[g]], VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT); diff --git a/app/src/main/cpp/winlator/vk/vk_renderer.c b/app/src/main/cpp/winlator/vk/vk_renderer.c index d1b79816f..986b52c66 100644 --- a/app/src/main/cpp/winlator/vk/vk_renderer.c +++ b/app/src/main/cpp/winlator/vk/vk_renderer.c @@ -2951,19 +2951,27 @@ static bool record_and_submit_frame(VkRenderer* r) { VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_ACCESS_SHADER_WRITE_BIT, 0); + } else if (use_dis) { + // DIS scales its interpolated content rect into whatever target it is given + // and fills the strips around it from the composite, so it goes straight + // into the swapchain image: no composite target and second full-frame blit. + vkr_dis_generate_into(r->dis, f->cmd, g, idx, + r->swapchain_images[idx], VK_NULL_HANDLE, + r->swapchain_extent.width, + r->swapchain_extent.height, + composite->image); + vkr_image_barrier(f->cmd, r->swapchain_images[idx], + VK_IMAGE_LAYOUT_GENERAL, + VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, + VK_ACCESS_TRANSFER_WRITE_BIT, 0); } else { VkCompositeTarget* gt = &r->composite[VK_FRAMES_IN_FLIGHT + g]; - if (use_dis) { - vkr_dis_generate_into(r->dis, f->cmd, g, VK_FRAMES_IN_FLIGHT + g, - gt->image, gt->view, - gt->width, gt->height, - composite->image); - } else { - vkr_lsfg_generate_into(r->lsfg, f->cmd, g, VK_FRAMES_IN_FLIGHT + g, - gt->image, gt->view, - r->swapchain_extent.width, - r->swapchain_extent.height); - } + vkr_lsfg_generate_into(r->lsfg, f->cmd, g, VK_FRAMES_IN_FLIGHT + g, + gt->image, gt->view, + r->swapchain_extent.width, + r->swapchain_extent.height); blit_composite_to_swapchain(r, f->cmd, gt, r->swapchain_images[idx]); } } diff --git a/app/src/main/res/values-b+es+419/strings.xml b/app/src/main/res/values-b+es+419/strings.xml index 33ed76e38..96e7c29db 100644 --- a/app/src/main/res/values-b+es+419/strings.xml +++ b/app/src/main/res/values-b+es+419/strings.xml @@ -2723,7 +2723,7 @@ Ruta instalada: Generar fotogramas Desactivado LSFG - OpenFG + WFG Multiplicador %1$dx Objetivo adaptativo @@ -2731,7 +2731,7 @@ Ruta instalada: %1$d fps Interpola entre fotogramas renderizados con Lossless Scaling. Movimiento más fluido, pero un fotograma extra de latencia de entrada. Sube hacia el objetivo solo mientras ayude de forma medible, en lugar de mantener un multiplicador fijo. - Instala Lossless Scaling en los ajustes del contenedor para usar el motor LSFG. OpenFlow no necesita nada más. + Instala Lossless Scaling en los ajustes del contenedor para usar el motor LSFG. WFG no necesita nada más. Interpola entre fotogramas renderizados con Dense Inverse Search. Integrado; no requiere shaders externos. Escala de resolución Rápido @@ -2742,7 +2742,7 @@ Ruta instalada: Frecuencia máx. %1$d fps Mostrar flujo - Dibuja los vectores de movimiento que estima OpenFlow en lugar del fotograma generado. Solo para diagnóstico. + Dibuja los vectores de movimiento que estima WFG en lugar del fotograma generado. Solo para diagnóstico. Preajuste de calidad Ultra rendimiento Rendimiento diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 2b7486dcb..26c695871 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -2736,7 +2736,7 @@ Installeret sti: Generér billeder Fra LSFG - OpenFG + WFG Multiplikator %1$dx Adaptivt mål @@ -2744,7 +2744,7 @@ Installeret sti: %1$d fps Interpolerer mellem gengivne billeder med Lossless Scaling. Blødere bevægelse, men ét ekstra billede med inputforsinkelse. Stiger mod målet, kun mens det målbart hjælper, i stedet for at fastholde en fast multiplikator. - Installér Lossless Scaling i containerindstillingerne for at bruge LSFG-motoren. OpenFlow kræver ikke noget ekstra. + Installér Lossless Scaling i containerindstillingerne for at bruge LSFG-motoren. WFG kræver ikke noget ekstra. Interpolerer mellem gengivne billeder med Dense Inverse Search. Indbygget; ingen eksterne shaders kræves. Opløsningsskala Hurtig @@ -2755,7 +2755,7 @@ Installeret sti: Maks. opdateringshastighed %1$d fps Vis flow - Tegner de bevægelsesvektorer, OpenFlow beregner, i stedet for det genererede billede. Kun til diagnostik. + Tegner de bevægelsesvektorer, WFG beregner, i stedet for det genererede billede. Kun til diagnostik. Kvalitetsforudindstilling Ultra ydeevne Ydeevne diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 50ba2ebc8..a751d268c 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -2727,7 +2727,7 @@ Installierter Pfad: Frames generieren Aus LSFG - OpenFG + WFG Multiplikator %1$dx Adaptives Ziel @@ -2735,7 +2735,7 @@ Installierter Pfad: %1$d fps Interpoliert zwischen gerenderten Frames mit Lossless Scaling. Flüssigere Bewegung, aber ein zusätzlicher Frame Eingabeverzögerung. Steigt nur dann in Richtung Ziel, wenn es messbar hilft, statt einen festen Multiplikator zu halten. - Installiere Lossless Scaling in den Container-Einstellungen, um die LSFG-Engine zu nutzen. OpenFlow benötigt nichts Zusätzliches. + Installiere Lossless Scaling in den Container-Einstellungen, um die LSFG-Engine zu nutzen. WFG benötigt nichts Zusätzliches. Interpoliert zwischen gerenderten Frames mit Dense Inverse Search. Eingebaut; keine externen Shader nötig. Auflösungsskalierung Schnell @@ -2746,7 +2746,7 @@ Installierter Pfad: Max. Bildwiederholrate %1$d fps Fluss anzeigen - Zeichnet die von OpenFlow geschätzten Bewegungsvektoren statt des generierten Frames. Nur zur Diagnose. + Zeichnet die von WFG geschätzten Bewegungsvektoren statt des generierten Frames. Nur zur Diagnose. Qualitätsvoreinstellung Ultra-Leistung Leistung diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index db5325f42..037a9a7f3 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -2727,7 +2727,7 @@ Ruta instalada: Generar fotogramas Desactivado LSFG - OpenFG + WFG Multiplicador %1$dx Objetivo adaptativo @@ -2735,7 +2735,7 @@ Ruta instalada: %1$d fps Interpola entre fotogramas renderizados con Lossless Scaling. Movimiento más fluido, pero un fotograma extra de latencia de entrada. Sube hacia el objetivo solo mientras ayude de forma medible, en lugar de mantener un multiplicador fijo. - Instala Lossless Scaling en los ajustes del contenedor para usar el motor LSFG. OpenFlow no necesita nada más. + Instala Lossless Scaling en los ajustes del contenedor para usar el motor LSFG. WFG no necesita nada más. Interpola entre fotogramas renderizados con Dense Inverse Search. Integrado; no requiere shaders externos. Escala de resolución Rápido @@ -2746,7 +2746,7 @@ Ruta instalada: Frecuencia máx. %1$d fps Mostrar flujo - Dibuja los vectores de movimiento que estima OpenFlow en lugar del fotograma generado. Solo para diagnóstico. + Dibuja los vectores de movimiento que estima WFG en lugar del fotograma generado. Solo para diagnóstico. Preajuste de calidad Ultra rendimiento Rendimiento diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 3c98a4b2d..4645576f1 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -2723,7 +2723,7 @@ Asennuspolku: Generoi ruutuja Pois LSFG - OpenFG + WFG Kerroin %1$dx Mukautuva tavoite @@ -2731,7 +2731,7 @@ Asennuspolku: %1$d fps Interpoloi renderöityjen ruutujen välillä Lossless Scalingilla. Sulavampi liike, mutta yhden ruudun lisäviive syötteessä. Nousee kohti tavoitetta vain, kun siitä on mitattavaa hyötyä, sen sijaan että pitäisi kiinteän kertoimen. - Asenna Lossless Scaling säiliön asetuksista käyttääksesi LSFG-moottoria. OpenFlow ei tarvitse mitään lisäksi. + Asenna Lossless Scaling säiliön asetuksista käyttääksesi LSFG-moottoria. WFG ei tarvitse mitään lisäksi. Interpoloi renderöityjen ruutujen välillä Dense Inverse Searchilla. Sisäänrakennettu; ulkoisia varjostimia ei tarvita. Resoluutioskaala Nopea @@ -2742,7 +2742,7 @@ Asennuspolku: Maks. virkistystaajuus %1$d fps Näytä liikevuo - Piirtää OpenFlow:n arvioimat liikevektorit generoidun ruudun sijaan. Vain diagnostiikkaan. + Piirtää WFG:n arvioimat liikevektorit generoidun ruudun sijaan. Vain diagnostiikkaan. Laatuesiasetus Ultra-suorituskyky Suorituskyky diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index fa461b0f1..786855827 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -2727,7 +2727,7 @@ Chemin installé : Générer des images Désactivé LSFG - OpenFG + WFG Multiplicateur %1$dx Cible adaptative @@ -2735,7 +2735,7 @@ Chemin installé : %1$d fps Interpole entre les images rendues avec Lossless Scaling. Mouvement plus fluide, mais une image supplémentaire de latence d\'entrée. Monte vers la cible uniquement tant que cela aide de façon mesurable, au lieu de conserver un multiplicateur fixe. - Installez Lossless Scaling dans les paramètres du conteneur pour utiliser le moteur LSFG. OpenFlow ne nécessite rien de plus. + Installez Lossless Scaling dans les paramètres du conteneur pour utiliser le moteur LSFG. WFG ne nécessite rien de plus. Interpole entre les images rendues avec Dense Inverse Search. Intégré ; aucun shader externe requis. Échelle de résolution Rapide @@ -2746,7 +2746,7 @@ Chemin installé : Fréquence max. %1$d fps Afficher le flux - Dessine les vecteurs de mouvement estimés par OpenFlow au lieu de l\'image générée. Diagnostic uniquement. + Dessine les vecteurs de mouvement estimés par WFG au lieu de l\'image générée. Diagnostic uniquement. Préréglage de qualité Ultra performance Performance diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index cf71adf13..5e82b2266 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -2621,7 +2621,7 @@ फ़्रेम जनरेट करें बंद LSFG - OpenFG + WFG गुणक %1$dx अनुकूली लक्ष्य @@ -2629,7 +2629,7 @@ %1$d fps Lossless Scaling से रेंडर किए गए फ़्रेमों के बीच इंटरपोलेट करता है। गति अधिक सहज, लेकिन इनपुट में एक अतिरिक्त फ़्रेम की देरी। स्थिर गुणक बनाए रखने के बजाय, केवल तभी लक्ष्य की ओर बढ़ता है जब इससे मापने योग्य लाभ हो। - LSFG इंजन उपयोग करने के लिए कंटेनर सेटिंग्स में Lossless Scaling इंस्टॉल करें। OpenFlow को कुछ अतिरिक्त नहीं चाहिए। + LSFG इंजन उपयोग करने के लिए कंटेनर सेटिंग्स में Lossless Scaling इंस्टॉल करें। WFG को कुछ अतिरिक्त नहीं चाहिए। Dense Inverse Search से रेंडर किए गए फ़्रेम के बीच इंटरपोलेट करता है। ऐप में बिल्ट-इन; बाहरी शेडर की ज़रूरत नहीं। रिज़ॉल्यूशन स्केल तेज़ @@ -2640,7 +2640,7 @@ अधिकतम रिफ्रेश दर %1$d fps फ़्लो दिखाएँ - जनरेट किए गए फ़्रेम के बजाय OpenFlow द्वारा अनुमानित मोशन वेक्टर दिखाता है। केवल निदान के लिए। + जनरेट किए गए फ़्रेम के बजाय WFG द्वारा अनुमानित मोशन वेक्टर दिखाता है। केवल निदान के लिए। गुणवत्ता प्रीसेट अल्ट्रा प्रदर्शन प्रदर्शन diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index cec2979ed..ab347dc3f 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -2725,7 +2725,7 @@ Percorso installato: Genera fotogrammi Disattivo LSFG - OpenFG + WFG Moltiplicatore %1$dx Obiettivo adattivo @@ -2733,7 +2733,7 @@ Percorso installato: %1$d fps Interpola tra i fotogrammi renderizzati con Lossless Scaling. Movimento più fluido, ma un fotogramma in più di latenza di input. Sale verso l\'obiettivo solo finché aiuta in modo misurabile, invece di mantenere un moltiplicatore fisso. - Installa Lossless Scaling nelle impostazioni del contenitore per usare il motore LSFG. OpenFlow non richiede nulla di aggiuntivo. + Installa Lossless Scaling nelle impostazioni del contenitore per usare il motore LSFG. WFG non richiede nulla di aggiuntivo. Interpola tra i fotogrammi renderizzati con Dense Inverse Search. Integrato; nessuno shader esterno richiesto. Scala risoluzione Veloce @@ -2744,7 +2744,7 @@ Percorso installato: Freq. max %1$d fps Mostra flusso - Disegna i vettori di movimento stimati da OpenFlow al posto del fotogramma generato. Solo a scopo diagnostico. + Disegna i vettori di movimento stimati da WFG al posto del fotogramma generato. Solo a scopo diagnostico. Preset qualità Ultra prestazioni Prestazioni diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index c6c3bbe40..574919d7a 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -2723,7 +2723,7 @@ フレームを生成 オフ LSFG - OpenFG + WFG 倍率 %1$dx アダプティブ目標 @@ -2731,7 +2731,7 @@ %1$d fps Lossless Scaling でレンダリング済みフレーム間を補間します。動きは滑らかになりますが、入力遅延が 1 フレーム増えます。 固定倍率を保つのではなく、効果が測定できる間だけ目標に向けて上げていきます。 - LSFG エンジンを使用するには、コンテナ設定で Lossless Scaling をインストールしてください。OpenFlow には追加のものは不要です。 + LSFG エンジンを使用するには、コンテナ設定で Lossless Scaling をインストールしてください。WFG には追加のものは不要です。 Dense Inverse Search で描画フレーム間を補間します。アプリ内蔵で外部シェーダーは不要です。 解像度スケール 高速 @@ -2742,7 +2742,7 @@ 最大リフレッシュレート %1$d fps フローを表示 - 生成フレームの代わりに、OpenFlow が推定した動きベクトルを描画します。診断専用です。 + 生成フレームの代わりに、WFG が推定した動きベクトルを描画します。診断専用です。 画質プリセット ウルトラパフォーマンス パフォーマンス diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 18b575fd6..f9e59deb0 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -2726,7 +2726,7 @@ 프레임 생성 사용 끄기 LSFG - OpenFG + WFG 배수 %1$dx 적응형 목표 @@ -2734,7 +2734,7 @@ %1$d fps Lossless Scaling으로 렌더링된 프레임 사이를 보간합니다. 움직임은 부드러워지지만 입력 지연이 한 프레임 늘어납니다. 고정 배수를 유지하는 대신, 측정 가능한 이득이 있을 때만 목표를 향해 올립니다. - LSFG 엔진을 사용하려면 컨테이너 설정에서 Lossless Scaling을 설치하세요. OpenFlow는 추가 설치가 필요 없습니다. + LSFG 엔진을 사용하려면 컨테이너 설정에서 Lossless Scaling을 설치하세요. WFG는 추가 설치가 필요 없습니다. Dense Inverse Search로 렌더링된 프레임 사이를 보간합니다. 내장 기능이며 외부 셰이더가 필요 없습니다. 해상도 배율 빠름 @@ -2745,7 +2745,7 @@ 최대 새로고침 빈도 %1$d fps 흐름 표시 - 생성된 프레임 대신 OpenFlow가 추정한 모션 벡터를 그립니다. 진단용입니다. + 생성된 프레임 대신 WFG가 추정한 모션 벡터를 그립니다. 진단용입니다. 품질 프리셋 울트라 성능 성능 diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index da82f2006..15a36afbf 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -2723,7 +2723,7 @@ Installert bane: Generer bilder Av LSFG - OpenFG + WFG Multiplikator %1$dx Adaptivt mål @@ -2731,7 +2731,7 @@ Installert bane: %1$d fps Interpolerer mellom gjengitte bilder med Lossless Scaling. Jevnere bevegelse, men ett ekstra bilde med inndataforsinkelse. Stiger mot målet bare så lenge det hjelper målbart, i stedet for å holde en fast multiplikator. - Installer Lossless Scaling i containerinnstillingene for å bruke LSFG-motoren. OpenFlow trenger ikke noe ekstra. + Installer Lossless Scaling i containerinnstillingene for å bruke LSFG-motoren. WFG trenger ikke noe ekstra. Interpolerer mellom gjengitte bilder med Dense Inverse Search. Innebygd; ingen eksterne shadere kreves. Oppløsningsskala Rask @@ -2742,7 +2742,7 @@ Installert bane: Maks. oppdateringsfrekvens %1$d fps Vis flyt - Tegner bevegelsesvektorene OpenFlow beregner i stedet for det genererte bildet. Kun for diagnostikk. + Tegner bevegelsesvektorene WFG beregner i stedet for det genererte bildet. Kun for diagnostikk. Kvalitetsforhåndsinnstilling Ultra ytelse Ytelse diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 2a4a0580f..472ade6ff 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -2731,7 +2731,7 @@ Zainstalowana ścieżka: Generuj klatki Wył. LSFG - OpenFG + WFG Mnożnik %1$dx Cel adaptacyjny @@ -2739,7 +2739,7 @@ Zainstalowana ścieżka: %1$d fps Interpoluje między wyrenderowanymi klatkami przy użyciu Lossless Scaling. Płynniejszy ruch, ale jedna dodatkowa klatka opóźnienia sterowania. Zwiększa się w kierunku celu tylko wtedy, gdy przynosi to mierzalną korzyść, zamiast utrzymywać stały mnożnik. - Zainstaluj Lossless Scaling w ustawieniach kontenera, aby korzystać z silnika LSFG. OpenFlow nie wymaga niczego dodatkowego. + Zainstaluj Lossless Scaling w ustawieniach kontenera, aby korzystać z silnika LSFG. WFG nie wymaga niczego dodatkowego. Interpoluje między wyrenderowanymi klatkami przy użyciu Dense Inverse Search. Wbudowane; nie wymaga zewnętrznych shaderów. Skala rozdzielczości Szybko @@ -2750,7 +2750,7 @@ Zainstalowana ścieżka: Maks. częstotliwość odświeżania %1$d fps Pokaż przepływ - Rysuje wektory ruchu szacowane przez OpenFlow zamiast wygenerowanej klatki. Tylko do diagnostyki. + Rysuje wektory ruchu szacowane przez WFG zamiast wygenerowanej klatki. Tylko do diagnostyki. Ustawienie jakości Ultra wydajność Wydajność diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 0d8c86824..69a266dd5 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -2725,7 +2725,7 @@ Caminho instalado: Gerar quadros Desligado LSFG - OpenFG + WFG Multiplicador %1$dx Alvo adaptativo @@ -2733,7 +2733,7 @@ Caminho instalado: %1$d fps Interpola entre quadros renderizados com o Lossless Scaling. Movimento mais suave, mas um quadro extra de latência de entrada. Sobe em direção ao alvo apenas enquanto ajudar de forma mensurável, em vez de manter um multiplicador fixo. - Instale o Lossless Scaling nas configurações do contêiner para usar o motor LSFG. O OpenFlow não precisa de nada extra. + Instale o Lossless Scaling nas configurações do contêiner para usar o motor LSFG. O WFG não precisa de nada extra. Interpola entre quadros renderizados com Dense Inverse Search. Integrado; sem shaders externos. Escala de resolução Rápido @@ -2744,7 +2744,7 @@ Caminho instalado: Frequência máx. %1$d fps Mostrar fluxo - Desenha os vetores de movimento estimados pelo OpenFlow em vez do quadro gerado. Apenas para diagnóstico. + Desenha os vetores de movimento estimados pelo WFG em vez do quadro gerado. Apenas para diagnóstico. Predefinição de qualidade Ultra desempenho Desempenho diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 935e0f884..04d912615 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -2723,7 +2723,7 @@ Caminho instalado: Gerar fotogramas Desligado LSFG - OpenFG + WFG Multiplicador %1$dx Alvo adaptativo @@ -2731,7 +2731,7 @@ Caminho instalado: %1$d fps Interpola entre fotogramas renderizados com o Lossless Scaling. Movimento mais suave, mas um fotograma extra de latência de entrada. Sobe em direção ao alvo apenas enquanto ajudar de forma mensurável, em vez de manter um multiplicador fixo. - Instale o Lossless Scaling nas definições do contentor para usar o motor LSFG. O OpenFlow não precisa de nada extra. + Instale o Lossless Scaling nas definições do contentor para usar o motor LSFG. O WFG não precisa de nada extra. Interpola entre fotogramas renderizados com Dense Inverse Search. Integrado; sem shaders externos. Escala de resolução Rápido @@ -2742,7 +2742,7 @@ Caminho instalado: Frequência máx. %1$d fps Mostrar fluxo - Desenha os vetores de movimento estimados pelo OpenFlow em vez do fotograma gerado. Apenas para diagnóstico. + Desenha os vetores de movimento estimados pelo WFG em vez do fotograma gerado. Apenas para diagnóstico. Predefinição de qualidade Ultra desempenho Desempenho diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 33f03ceb5..577a5aea1 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -2724,7 +2724,7 @@ Cale instalata: Generează cadre Oprit LSFG - OpenFG + WFG Multiplicator %1$dx Țintă adaptivă @@ -2732,7 +2732,7 @@ Cale instalata: %1$d fps Interpolează între cadrele randate cu Lossless Scaling. Mișcare mai fluidă, dar un cadru suplimentar de latență la intrare. Urcă spre țintă doar cât timp ajută în mod măsurabil, în loc să păstreze un multiplicator fix. - Instalează Lossless Scaling din setările containerului pentru a folosi motorul LSFG. OpenFlow nu necesită nimic în plus. + Instalează Lossless Scaling din setările containerului pentru a folosi motorul LSFG. WFG nu necesită nimic în plus. Interpolează între cadrele randate cu Dense Inverse Search. Încorporat; fără shadere externe. Scală rezoluție Rapid @@ -2743,7 +2743,7 @@ Cale instalata: Rată de reîmprospătare max. %1$d fps Arată fluxul - Desenează vectorii de mișcare estimați de OpenFlow în locul cadrului generat. Doar pentru diagnosticare. + Desenează vectorii de mișcare estimați de WFG în locul cadrului generat. Doar pentru diagnosticare. Presetare de calitate Ultra performanță Performanță diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 871b64092..6055706c5 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -2631,7 +2631,7 @@ Генерировать кадры Выкл. LSFG - OpenFG + WFG Множитель %1$dx Адаптивная цель @@ -2639,7 +2639,7 @@ %1$d fps Интерполирует между отрисованными кадрами с помощью Lossless Scaling. Движение плавнее, но задержка ввода увеличивается на один кадр. Повышается к цели только пока это даёт измеримый выигрыш, вместо удержания фиксированного множителя. - Установите Lossless Scaling в настройках контейнера, чтобы использовать движок LSFG. Для OpenFlow ничего дополнительно не нужно. + Установите Lossless Scaling в настройках контейнера, чтобы использовать движок LSFG. Для WFG ничего дополнительно не нужно. Интерполирует между отрендеренными кадрами методом Dense Inverse Search. Встроено; внешние шейдеры не требуются. Масштаб разрешения Быстро @@ -2650,7 +2650,7 @@ Макс. частота обновления %1$d fps Показать поток - Рисует векторы движения, вычисленные OpenFlow, вместо сгенерированного кадра. Только для диагностики. + Рисует векторы движения, вычисленные WFG, вместо сгенерированного кадра. Только для диагностики. Пресет качества Ультрапроизводительность Производительность diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 1a77469d6..1b496aeb0 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -2723,7 +2723,7 @@ Installerad sökväg: Generera bilder Av LSFG - OpenFG + WFG Multiplikator %1$dx Adaptivt mål @@ -2731,7 +2731,7 @@ Installerad sökväg: %1$d fps Interpolerar mellan renderade bildrutor med Lossless Scaling. Mjukare rörelse, men en extra bildruta med inmatningsfördröjning. Stiger mot målet endast så länge det hjälper mätbart, i stället för att hålla en fast multiplikator. - Installera Lossless Scaling i containerinställningarna för att använda LSFG-motorn. OpenFlow kräver inget extra. + Installera Lossless Scaling i containerinställningarna för att använda LSFG-motorn. WFG kräver inget extra. Interpolerar mellan renderade bildrutor med Dense Inverse Search. Inbyggd; inga externa shaders krävs. Upplösningsskala Snabb @@ -2742,7 +2742,7 @@ Installerad sökväg: Max uppdateringsfrekvens %1$d fps Visa flöde - Ritar de rörelsevektorer som OpenFlow beräknar i stället för den genererade bildrutan. Endast för diagnostik. + Ritar de rörelsevektorer som WFG beräknar i stället för den genererade bildrutan. Endast för diagnostik. Kvalitetsförval Ultraprestanda Prestanda diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 96723f61c..587aaeef9 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -2723,7 +2723,7 @@ สร้างเฟรม ปิด LSFG - OpenFG + WFG ตัวคูณ %1$dx เป้าหมายแบบปรับอัตโนมัติ @@ -2731,7 +2731,7 @@ %1$d fps แทรกเฟรมระหว่างเฟรมที่เรนเดอร์ด้วย Lossless Scaling การเคลื่อนไหวลื่นขึ้น แต่มีความหน่วงของอินพุตเพิ่มขึ้นหนึ่งเฟรม จะไต่ขึ้นสู่เป้าหมายเฉพาะเมื่อวัดผลได้ว่าช่วยจริง แทนที่จะคงตัวคูณไว้คงที่ - ติดตั้ง Lossless Scaling ในการตั้งค่าคอนเทนเนอร์เพื่อใช้เอนจิน LSFG ส่วน OpenFlow ไม่ต้องใช้อะไรเพิ่ม + ติดตั้ง Lossless Scaling ในการตั้งค่าคอนเทนเนอร์เพื่อใช้เอนจิน LSFG ส่วน WFG ไม่ต้องใช้อะไรเพิ่ม แทรกเฟรมระหว่างเฟรมที่เรนเดอร์ด้วย Dense Inverse Search มีในตัว ไม่ต้องใช้เชเดอร์ภายนอก สเกลความละเอียด เร็ว @@ -2742,7 +2742,7 @@ อัตรารีเฟรชสูงสุด %1$d fps แสดงโฟลว์ - วาดเวกเตอร์การเคลื่อนไหวที่ OpenFlow ประมาณไว้แทนเฟรมที่สร้างขึ้น ใช้เพื่อวินิจฉัยเท่านั้น + วาดเวกเตอร์การเคลื่อนไหวที่ WFG ประมาณไว้แทนเฟรมที่สร้างขึ้น ใช้เพื่อวินิจฉัยเท่านั้น ค่าที่ตั้งไว้ของคุณภาพ ประสิทธิภาพสูงสุด ประสิทธิภาพ diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 20c75ea3e..ae0f57eca 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -2723,7 +2723,7 @@ Yüklü konum: Kare üret Kapalı LSFG - OpenFG + WFG Çarpan %1$dx Uyarlanabilir hedef @@ -2731,7 +2731,7 @@ Yüklü konum: %1$d fps Lossless Scaling ile işlenmiş kareler arasında ara kare üretir. Daha akıcı hareket, ancak bir kare fazladan giriş gecikmesi. Sabit bir çarpanı korumak yerine, yalnızca ölçülebilir fayda sağladığı sürece hedefe doğru yükselir. - LSFG motorunu kullanmak için kapsayıcı ayarlarından Lossless Scaling\'i yükleyin. OpenFlow için ek bir şey gerekmez. + LSFG motorunu kullanmak için kapsayıcı ayarlarından Lossless Scaling\'i yükleyin. WFG için ek bir şey gerekmez. Dense Inverse Search ile işlenmiş kareler arasında ara kare oluşturur. Uygulamaya gömülü; harici shader gerekmez. Çözünürlük Ölçeği Hızlı @@ -2742,7 +2742,7 @@ Yüklü konum: Maks. yenileme hızı %1$d fps Akışı göster - Üretilen kare yerine OpenFlow\'un tahmin ettiği hareket vektörlerini çizer. Yalnızca tanılama amaçlıdır. + Üretilen kare yerine WFG\'un tahmin ettiği hareket vektörlerini çizer. Yalnızca tanılama amaçlıdır. Kalite ön ayarı Ultra performans Performans diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 58630590b..419aee50b 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -2731,7 +2731,7 @@ Генерувати кадри Вимк. LSFG - OpenFG + WFG Множник %1$dx Адаптивна ціль @@ -2739,7 +2739,7 @@ %1$d fps Інтерполює між відрендереними кадрами за допомогою Lossless Scaling. Рух плавніший, але затримка вводу зростає на один кадр. Підвищується до цілі лише доки це дає вимірний виграш, замість утримання фіксованого множника. - Установіть Lossless Scaling у налаштуваннях контейнера, щоб використовувати рушій LSFG. OpenFlow не потребує нічого додатково. + Установіть Lossless Scaling у налаштуваннях контейнера, щоб використовувати рушій LSFG. WFG не потребує нічого додатково. Інтерполює між відрендереними кадрами методом Dense Inverse Search. Вбудовано; зовнішні шейдери не потрібні. Масштаб роздільної здатності Швидко @@ -2750,7 +2750,7 @@ Макс. частота оновлення %1$d fps Показати потік - Малює вектори руху, які оцінює OpenFlow, замість згенерованого кадру. Лише для діагностики. + Малює вектори руху, які оцінює WFG, замість згенерованого кадру. Лише для діагностики. Пресет якості Ультрапродуктивність Продуктивність diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 18aae6052..a9fc81230 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -2725,7 +2725,7 @@ 生成帧 关闭 LSFG - OpenFG + WFG 倍数 %1$dx 自适应目标 @@ -2733,7 +2733,7 @@ %1$d fps 使用 Lossless Scaling 在已渲染的帧之间插帧。画面更流畅,但输入延迟会增加一帧。 仅在能带来可测量的提升时才向目标提升,而不是保持固定倍数。 - 请在容器设置中安装 Lossless Scaling 以使用 LSFG 引擎。OpenFlow 无需任何额外内容。 + 请在容器设置中安装 Lossless Scaling 以使用 LSFG 引擎。WFG 无需任何额外内容。 使用 Dense Inverse Search 在渲染帧之间插帧。内置功能,无需外部着色器。 分辨率缩放 快速 @@ -2744,7 +2744,7 @@ 最高刷新率 %1$d fps 显示光流 - 绘制 OpenFlow 估算的运动矢量,而不是生成的帧。仅用于诊断。 + 绘制 WFG 估算的运动矢量,而不是生成的帧。仅用于诊断。 画质预设 极致性能 性能 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 39b9d9940..b0e1a5bc7 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -2724,7 +2724,7 @@ 生成影格 關閉 LSFG - OpenFG + WFG 倍數 %1$dx 自適應目標 @@ -2732,7 +2732,7 @@ %1$d fps 使用 Lossless Scaling 在已算繪的影格之間插補。畫面更流暢,但輸入延遲會增加一個影格。 僅在能帶來可測量的提升時才朝目標提升,而不是維持固定倍數。 - 請在容器設定中安裝 Lossless Scaling 以使用 LSFG 引擎。OpenFlow 則不需要額外安裝任何東西。 + 請在容器設定中安裝 Lossless Scaling 以使用 LSFG 引擎。WFG 則不需要額外安裝任何東西。 使用 Dense Inverse Search 在已算繪的影格之間插補。內建功能,無需外部著色器。 解析度縮放 快速 @@ -2743,7 +2743,7 @@ 最高更新率 %1$d fps 顯示光流 - 繪製 OpenFlow 估算的動態向量,而非生成的影格。僅供診斷使用。 + 繪製 WFG 估算的動態向量,而非生成的影格。僅供診斷使用。 畫質預設 極致效能 效能 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 176d9df74..4e53b4273 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -920,7 +920,7 @@ E.g. META for META key, \n Generate Frames Off LSFG - OpenFG + WFG Multiplier %1$dx Adaptive Target @@ -941,7 +941,7 @@ E.g. META for META key, \n Motion estimated at full resolution. Highest cost; use it only when the game has GPU headroom to spare. Interpolates between rendered frames with Lossless Scaling. Smoother motion, but one extra frame of input latency. Climbs toward the target only while it measurably helps, instead of holding a fixed multiplier. - Install Lossless Scaling in container settings to use the LSFG engine. OpenFlow needs nothing extra. + Install Lossless Scaling in container settings to use the LSFG engine. WFG needs nothing extra. Interpolates between rendered frames with Dense Inverse Search. Built into the app; no external shaders required. Resolution Scale Fast @@ -952,7 +952,7 @@ E.g. META for META key, \n Max refresh rate %1$d fps Show flow - Draws the motion vectors OpenFlow estimates instead of the generated frame. Diagnostic only. + Draws the motion vectors WFG estimates instead of the generated frame. Diagnostic only. Frame Generation Looking for Lossless Scaling… Lossless Scaling shaders ready. From 2bf2828025ad0a8d166d62c3a1124a8658fbd78f Mon Sep 17 00:00:00 2001 From: qwertypower <1qwertypower1@gmail.com> Date: Sat, 10 Oct 2026 23:41:47 +0300 Subject: [PATCH 03/18] Rename the frame generation module to WFG --- CREDITS.md | 4 +- README.md | 4 +- app/src/main/cpp/CMakeLists.txt | 6 +- app/src/main/cpp/waylandcomp/CMakeLists.txt | 6 +- .../cpp/waylandcomp/src/framegen_bridge.c | 2 +- .../cpp/waylandcomp/src/framegen_engine.c | 2 +- .../waylandcomp/src/framegen_engine_stub.c | 2 +- app/src/main/cpp/waylandcomp/src/vk_present.c | 2 +- .../main/cpp/{openflow => wfg}/CMakeLists.txt | 20 +++--- .../include/shaders/dis_densify.comp | 2 +- .../include/shaders/dis_fb_hsum.comp | 2 +- .../include/shaders/dis_fb_poly.comp | 2 +- .../include/shaders/dis_fb_vsolve.comp | 2 +- .../include/shaders/dis_flow_pack.comp | 2 +- .../include/shaders/dis_gradient.comp | 2 +- .../include/shaders/dis_hist.comp | 2 +- .../include/shaders/dis_interpolate.comp | 2 +- .../include/shaders/dis_inverse_search.comp | 2 +- .../include/shaders/dis_luma.comp | 2 +- .../include/shaders/dis_luma_r16.comp | 2 +- .../include/shaders/dis_luma_r32.comp | 2 +- .../include/shaders/dis_me_luma.comp | 2 +- .../include/shaders/dis_propagate.comp | 2 +- .../include/shaders/dis_side.comp | 2 +- .../include/shaders/dis_vr_add.comp | 2 +- .../include/shaders/dis_vr_coef.comp | 2 +- .../include/shaders/dis_vr_d1.comp | 2 +- .../include/shaders/dis_vr_d2.comp | 2 +- .../include/shaders/dis_vr_prep.comp | 2 +- .../include/shaders/dis_vr_sor.comp | 2 +- .../include/shaders/dis_vr_w.comp | 2 +- .../cpp/{openflow => wfg}/include/vkr_dis.h | 2 +- .../cpp/{openflow => wfg}/src/dis_qcom_me.c | 4 +- .../cpp/{openflow => wfg}/src/dis_qcom_me.h | 0 .../main/cpp/{openflow => wfg}/src/vkr_dis.c | 70 +++++++++---------- .../cpp/winlator/vk/framegen/fg_present.c | 6 +- app/src/main/cpp/winlator/vk/vk_renderer.c | 6 +- docs/FRAME-GENERATION.md | 16 ++--- 38 files changed, 98 insertions(+), 98 deletions(-) rename app/src/main/cpp/{openflow => wfg}/CMakeLists.txt (77%) rename app/src/main/cpp/{openflow => wfg}/include/shaders/dis_densify.comp (97%) rename app/src/main/cpp/{openflow => wfg}/include/shaders/dis_fb_hsum.comp (98%) rename app/src/main/cpp/{openflow => wfg}/include/shaders/dis_fb_poly.comp (97%) rename app/src/main/cpp/{openflow => wfg}/include/shaders/dis_fb_vsolve.comp (94%) rename app/src/main/cpp/{openflow => wfg}/include/shaders/dis_flow_pack.comp (95%) rename app/src/main/cpp/{openflow => wfg}/include/shaders/dis_gradient.comp (95%) rename app/src/main/cpp/{openflow => wfg}/include/shaders/dis_hist.comp (97%) rename app/src/main/cpp/{openflow => wfg}/include/shaders/dis_interpolate.comp (99%) rename app/src/main/cpp/{openflow => wfg}/include/shaders/dis_inverse_search.comp (99%) rename app/src/main/cpp/{openflow => wfg}/include/shaders/dis_luma.comp (91%) rename app/src/main/cpp/{openflow => wfg}/include/shaders/dis_luma_r16.comp (91%) rename app/src/main/cpp/{openflow => wfg}/include/shaders/dis_luma_r32.comp (91%) rename app/src/main/cpp/{openflow => wfg}/include/shaders/dis_me_luma.comp (95%) rename app/src/main/cpp/{openflow => wfg}/include/shaders/dis_propagate.comp (97%) rename app/src/main/cpp/{openflow => wfg}/include/shaders/dis_side.comp (96%) rename app/src/main/cpp/{openflow => wfg}/include/shaders/dis_vr_add.comp (96%) rename app/src/main/cpp/{openflow => wfg}/include/shaders/dis_vr_coef.comp (97%) rename app/src/main/cpp/{openflow => wfg}/include/shaders/dis_vr_d1.comp (93%) rename app/src/main/cpp/{openflow => wfg}/include/shaders/dis_vr_d2.comp (92%) rename app/src/main/cpp/{openflow => wfg}/include/shaders/dis_vr_prep.comp (93%) rename app/src/main/cpp/{openflow => wfg}/include/shaders/dis_vr_sor.comp (96%) rename app/src/main/cpp/{openflow => wfg}/include/shaders/dis_vr_w.comp (94%) rename app/src/main/cpp/{openflow => wfg}/include/vkr_dis.h (97%) rename app/src/main/cpp/{openflow => wfg}/src/dis_qcom_me.c (98%) rename app/src/main/cpp/{openflow => wfg}/src/dis_qcom_me.h (100%) rename app/src/main/cpp/{openflow => wfg}/src/vkr_dis.c (97%) diff --git a/CREDITS.md b/CREDITS.md index 728818df6..6cd678d6b 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -57,7 +57,7 @@ corresponding source for every copyleft component is available from the projects [Eden Emulator Project](https://git.eden-emu.dev/eden-emu/eden) (GPL-3.0-or-later) — the Vulkan port of that chain that WinNative's frame generation is derived from. See [what came from Camille LaVey's Eden port](#frame-generation--what-came-from-camille-laveys-eden-port) -- **OpenFlow frame generation** (DIS optical flow) by **qwertypower** +- **WFG frame generation** (DIS optical flow) by **qwertypower** ([DEVAR Entertainment LLC](https://devar.ai/)) (GPL-3.0) — a complete open-source implementation of a Dense Inverse Search frame generator, the second frame generation engine in WinNative and the one that needs no shaders from anywhere else. @@ -119,7 +119,7 @@ of a user's own Lossless Scaling install (`lsfg_dll.*`), translating them when o available (`lsfg_dxbc.*`), the JNI surface (`lsfg_jni.*`), driver probing (`lsfg_probe.*`), and wiring the chain into WinNative's compositor and swapchain (`vkr_lsfg.*`). -## OpenFlow frame generation — the fully open-source engine +## WFG frame generation — the fully open-source engine WinNative's second frame generator is a complete open-source implementation of **Dense Inverse Search** optical flow, contributed by **qwertypower** (DEVAR Entertainment LLC) under GPL-3.0. diff --git a/README.md b/README.md index 7a1cf622f..ccb1ae4d7 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ alongside them. | --- | --- | | 📦 **Install** | [Releases](https://github.com/WinNative-Emu/WinNative/releases) | | 🎮 **Retro consoles** | [docs/RETRO-CONSOLES.md](docs/RETRO-CONSOLES.md) — NES through PlayStation 2 | -| 🎞️ **Frame generation** | [docs/FRAME-GENERATION.md](docs/FRAME-GENERATION.md) — LSFG and OpenFlow | +| 🎞️ **Frame generation** | [docs/FRAME-GENERATION.md](docs/FRAME-GENERATION.md) — LSFG and WFG | | 🖥️ **Wayland display server** | [docs/WAYLAND-DISPLAY.md](docs/WAYLAND-DISPLAY.md) — winewayland sessions on the embedded compositor | | 🔨 **Build from source** | [docs/BUILDING.md](docs/BUILDING.md) | | 🙏 **Credits & licenses** | [CREDITS.md](CREDITS.md) · [EMULATOR_CREDITS.md](EMULATOR_CREDITS.md) | @@ -60,7 +60,7 @@ WinNative stands on work by **brunodev85** (Winlator), **Pipetto-crypto** (Winla (Goldberg Steam Emulator), **Filippo Scognamiglio** (LibretroDroid) and the **libretro** core authors, the **ARMSX2**, **PCSX2** and **Dolphin** teams, **PancakeTAS** (lsfg-vk), **Camille LaVey** of the **Eden Emulator Project** (the Vulkan LSFG port this one derives from), -**qwertypower** of **DEVAR Entertainment LLC** (the open-source OpenFlow engine), **OpenCV** and +**qwertypower** of **DEVAR Entertainment LLC** (the open-source WFG engine), **OpenCV** and Till Kroeger (the DIS algorithm), **DXVK** (the `dxbc` translator), and **The412Banner** (DirectAudio and Steam Controller support), and the **SDL** contributors. diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 52c3caef9..9677ad588 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -133,8 +133,8 @@ endforeach() add_custom_target(winlator_shaders DEPENDS ${SHADER_HEADERS}) -# OpenFlow frame generator (DIS optical flow + interpolation), linked by libwinlator and libwnwayland. -add_subdirectory(openflow) +# WFG frame generator (DIS optical flow + interpolation), linked by libwinlator and libwnwayland. +add_subdirectory(wfg) # ---------------------------------------------------------------------------- # Winlator native library (X-server, AHB, Vulkan compositor, helpers) @@ -187,7 +187,7 @@ target_include_directories(winlator PRIVATE target_compile_features(winlator PRIVATE cxx_std_17) target_link_libraries(winlator - openflow + wfg log android mediandk diff --git a/app/src/main/cpp/waylandcomp/CMakeLists.txt b/app/src/main/cpp/waylandcomp/CMakeLists.txt index 8d30d73b0..0e596fb82 100644 --- a/app/src/main/cpp/waylandcomp/CMakeLists.txt +++ b/app/src/main/cpp/waylandcomp/CMakeLists.txt @@ -43,11 +43,11 @@ target_include_directories(wnwayland PRIVATE ) # The X11 renderer's frame-generation engines run here too, on the compositor's own Turnip -# device. LSFG is compiled in and OpenFlow comes from its static library rather than being linked +# device. LSFG is compiled in and WFG comes from its static library rather than being linked # from libwinlator: both libraries keep a private VkDispatch, so the visibility below must stay for # the two copies not to merge. set(FRAMEGEN_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../winlator/vk) -if (TARGET dxbc AND TARGET openflow) +if (TARGET dxbc AND TARGET wfg) target_sources(wnwayland PRIVATE src/framegen_engine.c ${FRAMEGEN_DIR}/vk_dispatch.c @@ -69,7 +69,7 @@ if (TARGET dxbc AND TARGET openflow) target_compile_definitions(wnwayland PRIVATE VK_USE_PLATFORM_ANDROID_KHR) target_compile_options(wnwayland PRIVATE -fvisibility=hidden) target_compile_features(wnwayland PRIVATE cxx_std_17) - target_link_libraries(wnwayland dxbc openflow) + target_link_libraries(wnwayland dxbc wfg) add_dependencies(wnwayland winlator_shaders) else() target_sources(wnwayland PRIVATE src/framegen_engine_stub.c) diff --git a/app/src/main/cpp/waylandcomp/src/framegen_bridge.c b/app/src/main/cpp/waylandcomp/src/framegen_bridge.c index e11df5f53..a7d87bba8 100644 --- a/app/src/main/cpp/waylandcomp/src/framegen_bridge.c +++ b/app/src/main/cpp/waylandcomp/src/framegen_bridge.c @@ -194,7 +194,7 @@ void vkp_framegen_device_ready(VkDevice dev, VkQueue queue, uint32_t qfam, int f g_vk.GetPhysicalDeviceMemoryProperties(g_pd, &g_memprops); fge_device_ready(g_gipa, g_inst, g_pd, dev, queue, qfam, features_enabled); g_dev_ready = vk.CreateImage && vk.CreateImageView && vk.CmdPipelineBarrier; - FGLOG("engines ready on the compositor's device: LSFG Native %s (%s), OpenFlow Native %s", + FGLOG("engines ready on the compositor's device: LSFG Native %s (%s), WFG Native %s", fge_caps_ok(VKP_FG_ENGINE_LSFG) ? "available" : "unavailable", fge_caps_reason(), fge_caps_ok(VKP_FG_ENGINE_DIS) ? "available" : "unavailable"); } diff --git a/app/src/main/cpp/waylandcomp/src/framegen_engine.c b/app/src/main/cpp/waylandcomp/src/framegen_engine.c index 325745953..b5c404ca9 100644 --- a/app/src/main/cpp/waylandcomp/src/framegen_engine.c +++ b/app/src/main/cpp/waylandcomp/src/framegen_engine.c @@ -214,7 +214,7 @@ void fge_telemetry(float *accepted, float *source_rate, float *thermal) { } const char *fge_engine_name(int kind) { - return kind == VKP_FG_ENGINE_DIS ? "OpenFlow Native" : "LSFG Native"; + return kind == VKP_FG_ENGINE_DIS ? "WFG Native" : "LSFG Native"; } const char *fge_build_info(void) { return "lsfg-native/dis-native"; } diff --git a/app/src/main/cpp/waylandcomp/src/framegen_engine_stub.c b/app/src/main/cpp/waylandcomp/src/framegen_engine_stub.c index 8c687a36a..a7b2f00a4 100644 --- a/app/src/main/cpp/waylandcomp/src/framegen_engine_stub.c +++ b/app/src/main/cpp/waylandcomp/src/framegen_engine_stub.c @@ -36,5 +36,5 @@ void fge_forget_targets(void) {} void fge_telemetry(float *accepted, float *source_rate, float *thermal) { *accepted = 0.0f; *source_rate = 0.0f; *thermal = -1.0f; } -const char *fge_engine_name(int kind) { return kind == 1 ? "OpenFlow Native" : "LSFG Native"; } +const char *fge_engine_name(int kind) { return kind == 1 ? "WFG Native" : "LSFG Native"; } const char *fge_build_info(void) { return "stub"; } diff --git a/app/src/main/cpp/waylandcomp/src/vk_present.c b/app/src/main/cpp/waylandcomp/src/vk_present.c index 97ecd41ea..91159bec5 100644 --- a/app/src/main/cpp/waylandcomp/src/vk_present.c +++ b/app/src/main/cpp/waylandcomp/src/vk_present.c @@ -479,7 +479,7 @@ static int dev_init(void) { .queueFamilyIndex = g_qfam, .queueCount = 1, .pQueuePriorities = &prio}; /* Frame generation (framegen_bridge.c): the LSFG chain needs the memory-model and * storage-image features enabled at device creation; the probe hands back the pNext chain - * for them, or NULL on a device that cannot run it (then OpenFlow Native alone is offered). A + * for them, or NULL on a device that cannot run it (then WFG Native alone is offered). A * driver that rejects the chain costs nothing: retried once without it, exactly as before. */ const void *fg_features = vkp_framegen_device_features(g_inst, vk_loader_gipa(), g_pd); VkDeviceCreateInfo dci = {.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, .pNext = fg_features, diff --git a/app/src/main/cpp/openflow/CMakeLists.txt b/app/src/main/cpp/wfg/CMakeLists.txt similarity index 77% rename from app/src/main/cpp/openflow/CMakeLists.txt rename to app/src/main/cpp/wfg/CMakeLists.txt index 8e5ef2c15..db267298d 100644 --- a/app/src/main/cpp/openflow/CMakeLists.txt +++ b/app/src/main/cpp/wfg/CMakeLists.txt @@ -1,4 +1,4 @@ -# OpenFlow frame generator: Dense Inverse Search (DIS) optical flow and the frame interpolator +# WFG frame generator: Dense Inverse Search (DIS) optical flow and the frame interpolator # built on it, as one static library. libwinlator (X11 compositor, standalone presenter) and # libwnwayland (Wayland compositor) both link it; each keeps its own VkDispatch, which the objects # here resolve against at link time. @@ -53,24 +53,24 @@ foreach(base ${DIS_SHADERS}) -DVAR_NAME=${var} -P "${BIN2C_SCRIPT}" DEPENDS "${input}" "${BIN2C_SCRIPT}" - COMMENT "Compiling OpenFlow shader ${base}.comp -> ${var}.spv.h" + COMMENT "Compiling WFG shader ${base}.comp -> ${var}.spv.h" VERBATIM ) list(APPEND DIS_SHADER_HEADERS "${hdr}") endforeach() -add_custom_target(openflow_shaders DEPENDS ${DIS_SHADER_HEADERS}) +add_custom_target(wfg_shaders DEPENDS ${DIS_SHADER_HEADERS}) -add_library(openflow STATIC +add_library(wfg STATIC src/vkr_dis.c src/dis_qcom_me.c ) -add_dependencies(openflow openflow_shaders) +add_dependencies(wfg wfg_shaders) -set_target_properties(openflow PROPERTIES POSITION_INDEPENDENT_CODE ON) -target_compile_options(openflow PRIVATE -Wall -Wextra -fvisibility=hidden) -target_compile_definitions(openflow PUBLIC VK_USE_PLATFORM_ANDROID_KHR) -target_include_directories(openflow +set_target_properties(wfg PROPERTIES POSITION_INDEPENDENT_CODE ON) +target_compile_options(wfg PRIVATE -Wall -Wextra -fvisibility=hidden) +target_compile_definitions(wfg PUBLIC VK_USE_PLATFORM_ANDROID_KHR) +target_include_directories(wfg PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include # vk_dispatch.h: the public header takes its Vulkan types from there. @@ -79,4 +79,4 @@ target_include_directories(openflow ${CMAKE_CURRENT_BINARY_DIR} ) # EGL/GLES are opened with dlopen by dis_qcom_me.c, so nothing links them here. -target_link_libraries(openflow PRIVATE log dl) +target_link_libraries(wfg PRIVATE log dl) diff --git a/app/src/main/cpp/openflow/include/shaders/dis_densify.comp b/app/src/main/cpp/wfg/include/shaders/dis_densify.comp similarity index 97% rename from app/src/main/cpp/openflow/include/shaders/dis_densify.comp rename to app/src/main/cpp/wfg/include/shaders/dis_densify.comp index a817d2f67..110a13800 100644 --- a/app/src/main/cpp/openflow/include/shaders/dis_densify.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_densify.comp @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) // SPDX-License-Identifier: GPL-3.0-or-later // -// OpenFlow frame generation: a Vulkan compute realisation of Dense Inverse Search +// WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from // OpenCV's DISOpticalFlow, which adopted Till Kroeger's original OF_DIS. // See CREDITS.md for the full attribution. diff --git a/app/src/main/cpp/openflow/include/shaders/dis_fb_hsum.comp b/app/src/main/cpp/wfg/include/shaders/dis_fb_hsum.comp similarity index 98% rename from app/src/main/cpp/openflow/include/shaders/dis_fb_hsum.comp rename to app/src/main/cpp/wfg/include/shaders/dis_fb_hsum.comp index 0da2186f0..a81aa52fb 100644 --- a/app/src/main/cpp/openflow/include/shaders/dis_fb_hsum.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_fb_hsum.comp @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) // SPDX-License-Identifier: GPL-3.0-or-later // -// OpenFlow, Farneback flow: the per-texel displacement equations of one iteration, summed +// WFG, Farneback flow: the per-texel displacement equations of one iteration, summed // across the window horizontally (dis_fb_vsolve sums them vertically and solves). // // With the first frame's expansion (A0, b0) at x and the second's (A1, b1) at x + d, Farneback's diff --git a/app/src/main/cpp/openflow/include/shaders/dis_fb_poly.comp b/app/src/main/cpp/wfg/include/shaders/dis_fb_poly.comp similarity index 97% rename from app/src/main/cpp/openflow/include/shaders/dis_fb_poly.comp rename to app/src/main/cpp/wfg/include/shaders/dis_fb_poly.comp index 5a4d73a1f..5b22617e1 100644 --- a/app/src/main/cpp/openflow/include/shaders/dis_fb_poly.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_fb_poly.comp @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) // SPDX-License-Identifier: GPL-3.0-or-later // -// OpenFlow, Farneback flow: polynomial expansion of one pyramid level of a frame. +// WFG, Farneback flow: polynomial expansion of one pyramid level of a frame. // // Gunnar Farneback, "Two-Frame Motion Estimation Based on Polynomial Expansion" (SCIA 2003); // the formulation follows OpenCV's calcOpticalFlowFarneback (FarnebackPolyExp). Around every diff --git a/app/src/main/cpp/openflow/include/shaders/dis_fb_vsolve.comp b/app/src/main/cpp/wfg/include/shaders/dis_fb_vsolve.comp similarity index 94% rename from app/src/main/cpp/openflow/include/shaders/dis_fb_vsolve.comp rename to app/src/main/cpp/wfg/include/shaders/dis_fb_vsolve.comp index 75caf09d6..4eb6beced 100644 --- a/app/src/main/cpp/openflow/include/shaders/dis_fb_vsolve.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_fb_vsolve.comp @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) // SPDX-License-Identifier: GPL-3.0-or-later // -// OpenFlow, Farneback flow: sums the horizontally summed equations of dis_fb_hsum vertically +// WFG, Farneback flow: sums the horizontally summed equations of dis_fb_hsum vertically // across the window and solves G d = h for this texel's displacement. #version 450 diff --git a/app/src/main/cpp/openflow/include/shaders/dis_flow_pack.comp b/app/src/main/cpp/wfg/include/shaders/dis_flow_pack.comp similarity index 95% rename from app/src/main/cpp/openflow/include/shaders/dis_flow_pack.comp rename to app/src/main/cpp/wfg/include/shaders/dis_flow_pack.comp index e898f8084..215866d71 100644 --- a/app/src/main/cpp/openflow/include/shaders/dis_flow_pack.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_flow_pack.comp @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) // SPDX-License-Identifier: GPL-3.0-or-later // -// OpenFlow frame generation: a Vulkan compute realisation of Dense Inverse Search +// WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from // OpenCV's DISOpticalFlow, which adopted Till Kroeger's original OF_DIS. // See CREDITS.md for the full attribution. diff --git a/app/src/main/cpp/openflow/include/shaders/dis_gradient.comp b/app/src/main/cpp/wfg/include/shaders/dis_gradient.comp similarity index 95% rename from app/src/main/cpp/openflow/include/shaders/dis_gradient.comp rename to app/src/main/cpp/wfg/include/shaders/dis_gradient.comp index ccb332152..ed175a5e1 100644 --- a/app/src/main/cpp/openflow/include/shaders/dis_gradient.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_gradient.comp @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) // SPDX-License-Identifier: GPL-3.0-or-later // -// OpenFlow frame generation: a Vulkan compute realisation of Dense Inverse Search +// WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from // OpenCV's DISOpticalFlow, which adopted Till Kroeger's original OF_DIS. // See CREDITS.md for the full attribution. diff --git a/app/src/main/cpp/openflow/include/shaders/dis_hist.comp b/app/src/main/cpp/wfg/include/shaders/dis_hist.comp similarity index 97% rename from app/src/main/cpp/openflow/include/shaders/dis_hist.comp rename to app/src/main/cpp/wfg/include/shaders/dis_hist.comp index b3d9c8611..5c78678b2 100644 --- a/app/src/main/cpp/openflow/include/shaders/dis_hist.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_hist.comp @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) // SPDX-License-Identifier: GPL-3.0-or-later // -// OpenFlow frame generation: a Vulkan compute realisation of Dense Inverse Search +// WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from // OpenCV's DISOpticalFlow, which adopted Till Kroeger's original OF_DIS. // See CREDITS.md for the full attribution. diff --git a/app/src/main/cpp/openflow/include/shaders/dis_interpolate.comp b/app/src/main/cpp/wfg/include/shaders/dis_interpolate.comp similarity index 99% rename from app/src/main/cpp/openflow/include/shaders/dis_interpolate.comp rename to app/src/main/cpp/wfg/include/shaders/dis_interpolate.comp index 34cc1d289..5bc272a3a 100644 --- a/app/src/main/cpp/openflow/include/shaders/dis_interpolate.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_interpolate.comp @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) // SPDX-License-Identifier: GPL-3.0-or-later // -// OpenFlow frame generation: a Vulkan compute realisation of Dense Inverse Search +// WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from // OpenCV's DISOpticalFlow, which adopted Till Kroeger's original OF_DIS. // See CREDITS.md for the full attribution. diff --git a/app/src/main/cpp/openflow/include/shaders/dis_inverse_search.comp b/app/src/main/cpp/wfg/include/shaders/dis_inverse_search.comp similarity index 99% rename from app/src/main/cpp/openflow/include/shaders/dis_inverse_search.comp rename to app/src/main/cpp/wfg/include/shaders/dis_inverse_search.comp index 8211b4e35..fc45cd83a 100644 --- a/app/src/main/cpp/openflow/include/shaders/dis_inverse_search.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_inverse_search.comp @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) // SPDX-License-Identifier: GPL-3.0-or-later // -// OpenFlow frame generation: a Vulkan compute realisation of Dense Inverse Search +// WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from // OpenCV's DISOpticalFlow, which adopted Till Kroeger's original OF_DIS. // See CREDITS.md for the full attribution. diff --git a/app/src/main/cpp/openflow/include/shaders/dis_luma.comp b/app/src/main/cpp/wfg/include/shaders/dis_luma.comp similarity index 91% rename from app/src/main/cpp/openflow/include/shaders/dis_luma.comp rename to app/src/main/cpp/wfg/include/shaders/dis_luma.comp index eaa8a7a82..d88976f32 100644 --- a/app/src/main/cpp/openflow/include/shaders/dis_luma.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_luma.comp @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) // SPDX-License-Identifier: GPL-3.0-or-later // -// OpenFlow frame generation: a Vulkan compute realisation of Dense Inverse Search +// WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from // OpenCV's DISOpticalFlow, which adopted Till Kroeger's original OF_DIS. // See CREDITS.md for the full attribution. diff --git a/app/src/main/cpp/openflow/include/shaders/dis_luma_r16.comp b/app/src/main/cpp/wfg/include/shaders/dis_luma_r16.comp similarity index 91% rename from app/src/main/cpp/openflow/include/shaders/dis_luma_r16.comp rename to app/src/main/cpp/wfg/include/shaders/dis_luma_r16.comp index b7cf1171b..505644b3c 100644 --- a/app/src/main/cpp/openflow/include/shaders/dis_luma_r16.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_luma_r16.comp @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) // SPDX-License-Identifier: GPL-3.0-or-later // -// OpenFlow frame generation: a Vulkan compute realisation of Dense Inverse Search +// WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from // OpenCV's DISOpticalFlow, which adopted Till Kroeger's original OF_DIS. // See CREDITS.md for the full attribution. diff --git a/app/src/main/cpp/openflow/include/shaders/dis_luma_r32.comp b/app/src/main/cpp/wfg/include/shaders/dis_luma_r32.comp similarity index 91% rename from app/src/main/cpp/openflow/include/shaders/dis_luma_r32.comp rename to app/src/main/cpp/wfg/include/shaders/dis_luma_r32.comp index 99a4e99ed..9391612ce 100644 --- a/app/src/main/cpp/openflow/include/shaders/dis_luma_r32.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_luma_r32.comp @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) // SPDX-License-Identifier: GPL-3.0-or-later // -// OpenFlow frame generation: a Vulkan compute realisation of Dense Inverse Search +// WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from // OpenCV's DISOpticalFlow, which adopted Till Kroeger's original OF_DIS. // See CREDITS.md for the full attribution. diff --git a/app/src/main/cpp/openflow/include/shaders/dis_me_luma.comp b/app/src/main/cpp/wfg/include/shaders/dis_me_luma.comp similarity index 95% rename from app/src/main/cpp/openflow/include/shaders/dis_me_luma.comp rename to app/src/main/cpp/wfg/include/shaders/dis_me_luma.comp index bde5d953b..207620765 100644 --- a/app/src/main/cpp/openflow/include/shaders/dis_me_luma.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_me_luma.comp @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) // SPDX-License-Identifier: GPL-3.0-or-later // -// OpenFlow frame generation: a Vulkan compute realisation of Dense Inverse Search +// WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from // OpenCV's DISOpticalFlow, which adopted Till Kroeger's original OF_DIS. // See CREDITS.md for the full attribution. diff --git a/app/src/main/cpp/openflow/include/shaders/dis_propagate.comp b/app/src/main/cpp/wfg/include/shaders/dis_propagate.comp similarity index 97% rename from app/src/main/cpp/openflow/include/shaders/dis_propagate.comp rename to app/src/main/cpp/wfg/include/shaders/dis_propagate.comp index 61afb39e6..eae27ce2c 100644 --- a/app/src/main/cpp/openflow/include/shaders/dis_propagate.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_propagate.comp @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) // SPDX-License-Identifier: GPL-3.0-or-later // -// OpenFlow frame generation: a Vulkan compute realisation of Dense Inverse Search +// WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from // OpenCV's DISOpticalFlow, which adopted Till Kroeger's original OF_DIS. // See CREDITS.md for the full attribution. diff --git a/app/src/main/cpp/openflow/include/shaders/dis_side.comp b/app/src/main/cpp/wfg/include/shaders/dis_side.comp similarity index 96% rename from app/src/main/cpp/openflow/include/shaders/dis_side.comp rename to app/src/main/cpp/wfg/include/shaders/dis_side.comp index 7ceda0613..4e29a5a18 100644 --- a/app/src/main/cpp/openflow/include/shaders/dis_side.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_side.comp @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) // SPDX-License-Identifier: GPL-3.0-or-later // -// OpenFlow frame generation: a Vulkan compute realisation of Dense Inverse Search +// WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from // OpenCV's DISOpticalFlow, which adopted Till Kroeger's original OF_DIS. // See CREDITS.md for the full attribution. diff --git a/app/src/main/cpp/openflow/include/shaders/dis_vr_add.comp b/app/src/main/cpp/wfg/include/shaders/dis_vr_add.comp similarity index 96% rename from app/src/main/cpp/openflow/include/shaders/dis_vr_add.comp rename to app/src/main/cpp/wfg/include/shaders/dis_vr_add.comp index 2a83707b4..9c3e6ba96 100644 --- a/app/src/main/cpp/openflow/include/shaders/dis_vr_add.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_vr_add.comp @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) // SPDX-License-Identifier: GPL-3.0-or-later // -// OpenFlow frame generation: a Vulkan compute realisation of Dense Inverse Search +// WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from // OpenCV's DISOpticalFlow, which adopted Till Kroeger's original OF_DIS. // See CREDITS.md for the full attribution. diff --git a/app/src/main/cpp/openflow/include/shaders/dis_vr_coef.comp b/app/src/main/cpp/wfg/include/shaders/dis_vr_coef.comp similarity index 97% rename from app/src/main/cpp/openflow/include/shaders/dis_vr_coef.comp rename to app/src/main/cpp/wfg/include/shaders/dis_vr_coef.comp index 0d9ff74fe..48dc6de11 100644 --- a/app/src/main/cpp/openflow/include/shaders/dis_vr_coef.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_vr_coef.comp @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) // SPDX-License-Identifier: GPL-3.0-or-later // -// OpenFlow frame generation: a Vulkan compute realisation of Dense Inverse Search +// WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from // OpenCV's DISOpticalFlow, which adopted Till Kroeger's original OF_DIS. // See CREDITS.md for the full attribution. diff --git a/app/src/main/cpp/openflow/include/shaders/dis_vr_d1.comp b/app/src/main/cpp/wfg/include/shaders/dis_vr_d1.comp similarity index 93% rename from app/src/main/cpp/openflow/include/shaders/dis_vr_d1.comp rename to app/src/main/cpp/wfg/include/shaders/dis_vr_d1.comp index 2ee2cce31..2351010d0 100644 --- a/app/src/main/cpp/openflow/include/shaders/dis_vr_d1.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_vr_d1.comp @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) // SPDX-License-Identifier: GPL-3.0-or-later // -// OpenFlow frame generation: a Vulkan compute realisation of Dense Inverse Search +// WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from // OpenCV's DISOpticalFlow, which adopted Till Kroeger's original OF_DIS. // See CREDITS.md for the full attribution. diff --git a/app/src/main/cpp/openflow/include/shaders/dis_vr_d2.comp b/app/src/main/cpp/wfg/include/shaders/dis_vr_d2.comp similarity index 92% rename from app/src/main/cpp/openflow/include/shaders/dis_vr_d2.comp rename to app/src/main/cpp/wfg/include/shaders/dis_vr_d2.comp index 81ea494e8..9966f3c94 100644 --- a/app/src/main/cpp/openflow/include/shaders/dis_vr_d2.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_vr_d2.comp @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) // SPDX-License-Identifier: GPL-3.0-or-later // -// OpenFlow frame generation: a Vulkan compute realisation of Dense Inverse Search +// WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from // OpenCV's DISOpticalFlow, which adopted Till Kroeger's original OF_DIS. // See CREDITS.md for the full attribution. diff --git a/app/src/main/cpp/openflow/include/shaders/dis_vr_prep.comp b/app/src/main/cpp/wfg/include/shaders/dis_vr_prep.comp similarity index 93% rename from app/src/main/cpp/openflow/include/shaders/dis_vr_prep.comp rename to app/src/main/cpp/wfg/include/shaders/dis_vr_prep.comp index b1d424963..357a8b79b 100644 --- a/app/src/main/cpp/openflow/include/shaders/dis_vr_prep.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_vr_prep.comp @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) // SPDX-License-Identifier: GPL-3.0-or-later // -// OpenFlow frame generation: a Vulkan compute realisation of Dense Inverse Search +// WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from // OpenCV's DISOpticalFlow, which adopted Till Kroeger's original OF_DIS. // See CREDITS.md for the full attribution. diff --git a/app/src/main/cpp/openflow/include/shaders/dis_vr_sor.comp b/app/src/main/cpp/wfg/include/shaders/dis_vr_sor.comp similarity index 96% rename from app/src/main/cpp/openflow/include/shaders/dis_vr_sor.comp rename to app/src/main/cpp/wfg/include/shaders/dis_vr_sor.comp index a892c059a..913e565b1 100644 --- a/app/src/main/cpp/openflow/include/shaders/dis_vr_sor.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_vr_sor.comp @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) // SPDX-License-Identifier: GPL-3.0-or-later // -// OpenFlow frame generation: a Vulkan compute realisation of Dense Inverse Search +// WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from // OpenCV's DISOpticalFlow, which adopted Till Kroeger's original OF_DIS. // See CREDITS.md for the full attribution. diff --git a/app/src/main/cpp/openflow/include/shaders/dis_vr_w.comp b/app/src/main/cpp/wfg/include/shaders/dis_vr_w.comp similarity index 94% rename from app/src/main/cpp/openflow/include/shaders/dis_vr_w.comp rename to app/src/main/cpp/wfg/include/shaders/dis_vr_w.comp index a8a9744a3..69ab98580 100644 --- a/app/src/main/cpp/openflow/include/shaders/dis_vr_w.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_vr_w.comp @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) // SPDX-License-Identifier: GPL-3.0-or-later // -// OpenFlow frame generation: a Vulkan compute realisation of Dense Inverse Search +// WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from // OpenCV's DISOpticalFlow, which adopted Till Kroeger's original OF_DIS. // See CREDITS.md for the full attribution. diff --git a/app/src/main/cpp/openflow/include/vkr_dis.h b/app/src/main/cpp/wfg/include/vkr_dis.h similarity index 97% rename from app/src/main/cpp/openflow/include/vkr_dis.h rename to app/src/main/cpp/wfg/include/vkr_dis.h index 074763034..bb8280a6f 100644 --- a/app/src/main/cpp/openflow/include/vkr_dis.h +++ b/app/src/main/cpp/wfg/include/vkr_dis.h @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) // SPDX-License-Identifier: GPL-3.0-or-later // -// OpenFlow frame generation: a Vulkan compute realisation of Dense Inverse Search +// WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from // OpenCV's DISOpticalFlow, which adopted Till Kroeger's original OF_DIS. // See CREDITS.md for the full attribution. diff --git a/app/src/main/cpp/openflow/src/dis_qcom_me.c b/app/src/main/cpp/wfg/src/dis_qcom_me.c similarity index 98% rename from app/src/main/cpp/openflow/src/dis_qcom_me.c rename to app/src/main/cpp/wfg/src/dis_qcom_me.c index e7c0b31bb..c861bf8d1 100644 --- a/app/src/main/cpp/openflow/src/dis_qcom_me.c +++ b/app/src/main/cpp/wfg/src/dis_qcom_me.c @@ -24,8 +24,8 @@ #include #include -#define ME_LOGI(...) __android_log_print(ANDROID_LOG_INFO, "OpenFlow", __VA_ARGS__) -#define ME_LOGW(...) __android_log_print(ANDROID_LOG_WARN, "OpenFlow", __VA_ARGS__) +#define ME_LOGI(...) __android_log_print(ANDROID_LOG_INFO, "WFG", __VA_ARGS__) +#define ME_LOGW(...) __android_log_print(ANDROID_LOG_WARN, "WFG", __VA_ARGS__) #define GL_MOTION_ESTIMATION_SEARCH_BLOCK_X_QCOM 0x8C90 #define GL_MOTION_ESTIMATION_SEARCH_BLOCK_Y_QCOM 0x8C91 diff --git a/app/src/main/cpp/openflow/src/dis_qcom_me.h b/app/src/main/cpp/wfg/src/dis_qcom_me.h similarity index 100% rename from app/src/main/cpp/openflow/src/dis_qcom_me.h rename to app/src/main/cpp/wfg/src/dis_qcom_me.h diff --git a/app/src/main/cpp/openflow/src/vkr_dis.c b/app/src/main/cpp/wfg/src/vkr_dis.c similarity index 97% rename from app/src/main/cpp/openflow/src/vkr_dis.c rename to app/src/main/cpp/wfg/src/vkr_dis.c index c0042f29b..0ae0c4ab8 100644 --- a/app/src/main/cpp/openflow/src/vkr_dis.c +++ b/app/src/main/cpp/wfg/src/vkr_dis.c @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) // SPDX-License-Identifier: GPL-3.0-or-later // -// OpenFlow frame generation: a Vulkan compute realisation of Dense Inverse Search +// WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from // OpenCV's DISOpticalFlow, which adopted Till Kroeger's original OF_DIS. // See CREDITS.md for the full attribution. @@ -43,8 +43,8 @@ #include #endif -#define DIS_LOGI(...) __android_log_print(ANDROID_LOG_INFO, "OpenFlow", __VA_ARGS__) -#define DIS_LOGW(...) __android_log_print(ANDROID_LOG_WARN, "OpenFlow", __VA_ARGS__) +#define DIS_LOGI(...) __android_log_print(ANDROID_LOG_INFO, "WFG", __VA_ARGS__) +#define DIS_LOGW(...) __android_log_print(ANDROID_LOG_WARN, "WFG", __VA_ARGS__) #define DIS_LOCAL_SIZE 8u #define DIS_PATCH_STRIDE 3u @@ -1419,7 +1419,7 @@ static void dis_create_me(VkrDis* d, uint32_t w, uint32_t h) { !dis_create_host_buffer(d, field_bytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, false, &d->me_field_buf, &d->me_field_mem, &d->me_field_map, &d->me_field_coherent)) { - DIS_LOGW("OpenFlow hardware motion: host buffers unavailable; using OpenFlow alone"); + DIS_LOGW("WFG hardware motion: host buffers unavailable; using WFG alone"); dis_destroy_me(d); return; } @@ -1456,7 +1456,7 @@ static void dis_create_me(VkrDis* d, uint32_t w, uint32_t h) { w2[1].pBufferInfo = &bi; vkd.UpdateDescriptorSets(d->device, 2, w2, 0, NULL); } - DIS_LOGI("OpenFlow hardware motion hint: GL_QCOM_motion_estimation on %ux%u seeds level %u (%ux%u)", + DIS_LOGI("WFG hardware motion hint: GL_QCOM_motion_estimation on %ux%u seeds level %u (%ux%u)", d->me_w, d->me_h, d->me_level, d->me_field_w, d->me_field_h); } @@ -1603,7 +1603,7 @@ static bool dis_alloc(VkrDis* d, VkDescriptorSetLayout layout, uint32_t count, ai.pSetLayouts = layouts; const VkResult res = vkd.AllocateDescriptorSets(d->device, &ai, out); if (res != VK_SUCCESS) { - DIS_LOGW("OpenFlow descriptor allocation failed (%d) asking for %u sets", (int)res, count); + DIS_LOGW("WFG descriptor allocation failed (%d) asking for %u sets", (int)res, count); return false; } return true; @@ -1761,7 +1761,7 @@ static bool dis_audit_formats(VkrDis* d) { const VkFormatFeatureFlags FILTER = VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT; if (d->luma_format == VK_FORMAT_UNDEFINED) { - DIS_LOGW("OpenFlow needs a single-channel float plane it can both write and filter; " + DIS_LOGW("WFG needs a single-channel float plane it can both write and filter; " "neither R16_SFLOAT nor R32_SFLOAT qualifies on this device"); return false; } @@ -1798,7 +1798,7 @@ static bool dis_audit_formats(VkrDis* d) { if (missing) { char names[256]; dis_missing_features(missing, names, sizeof(names)); - DIS_LOGW("OpenFlow needs %s on %s for %s, and this device does not report it", + DIS_LOGW("WFG needs %s on %s for %s, and this device does not report it", names, dis_format_name(reqs[i].format), reqs[i].purpose); ok = false; } @@ -1809,7 +1809,7 @@ static bool dis_audit_formats(VkrDis* d) { vkd.GetPhysicalDeviceFormatProperties(d->physical_device, VK_FORMAT_R16G16_SFLOAT, &flow_fp); d->manual_flow_filter = (flow_fp.optimalTilingFeatures & FILTER) == 0; - DIS_LOGI("OpenFlow format support: %s | flow filtering: %s", line, + DIS_LOGI("WFG format support: %s | flow filtering: %s", line, d->manual_flow_filter ? "in shader (driver cannot filter R16G16_SFLOAT)" : "sampler"); return ok; @@ -1833,22 +1833,22 @@ VkrDis* vkr_dis_create(VkDevice device, VkPhysicalDevice physical_device) { char prop[PROP_VALUE_MAX] = {0}; if (__system_property_get("debug.winnative.dis.hwme", prop) > 0 && prop[0] == '1') { d->hw_motion = true; - DIS_LOGI("OpenFlow hardware motion hint enabled by debug.winnative.dis.hwme"); + DIS_LOGI("WFG hardware motion hint enabled by debug.winnative.dis.hwme"); } #endif d->hint_level = -1; - // Flow estimator: DIS unless asked for Farneback, by OPENFLOW_ENGINE=farneback in the + // Flow estimator: DIS unless asked for Farneback, by WFG_ENGINE=farneback in the // environment (the desktop bench) or, on a device, - // adb shell setprop debug.winnative.openflow.engine farneback + // adb shell setprop debug.winnative.wfg.engine farneback // The hybrid is the default: on the bench it matches DIS overall (better on fast camera // motion, worse on small fast objects with a periodic texture) at 2.3x less flow time on // Adreno 750, nearly all of DIS's time there being the level-0 SOR sweeps. d->flow_engine = DIS_ENGINE_HYBRID; - const char* engine = getenv("OPENFLOW_ENGINE"); + const char* engine = getenv("WFG_ENGINE"); #ifdef __ANDROID__ char engine_prop[PROP_VALUE_MAX] = {0}; - if (__system_property_get("debug.winnative.openflow.engine", engine_prop) > 0) engine = engine_prop; + if (__system_property_get("debug.winnative.wfg.engine", engine_prop) > 0) engine = engine_prop; #endif if (engine && strcmp(engine, "dis") == 0) d->flow_engine = DIS_ENGINE_DIS; if (engine && strcmp(engine, "farneback") == 0) d->flow_engine = DIS_ENGINE_FARNEBACK; @@ -1865,41 +1865,41 @@ VkrDis* vkr_dis_create(VkDevice device, VkPhysicalDevice physical_device) { d->fb_hybrid_sor = DIS_HYB_SOR; { const char* v; - if ((v = getenv("OPENFLOW_FB_ROBUST")) != NULL) d->fb_robust = (float)atof(v); - if ((v = getenv("OPENFLOW_HYB_VR")) != NULL) d->fb_hybrid_vr = atoi(v) != 0; - if ((v = getenv("OPENFLOW_HYB_LEVEL")) != NULL) d->fb_hybrid_level = (uint32_t)atoi(v); - if ((v = getenv("OPENFLOW_HYB_FP")) != NULL) d->fb_hybrid_fp = (uint32_t)atoi(v); - if ((v = getenv("OPENFLOW_HYB_SOR")) != NULL) d->fb_hybrid_sor = (uint32_t)atoi(v); - if ((v = getenv("OPENFLOW_HYB_L1START")) != NULL) d->fb_hybrid_l1_start = atoi(v) != 0; - if ((v = getenv("OPENFLOW_HYB_VR1")) != NULL) d->fb_hybrid_vr_upper = atoi(v) != 0; - if ((v = getenv("OPENFLOW_FB_ITERS")) != NULL) d->fb_iterations = atoi(v); - if ((v = getenv("OPENFLOW_FB_RADIUS")) != NULL) d->fb_radius = atoi(v); - if ((v = getenv("OPENFLOW_FB_WSIGMA")) != NULL) d->fb_window_sigma = (float)atof(v); - if ((v = getenv("OPENFLOW_FB_PSIGMA")) != NULL) d->fb_poly_sigma = (float)atof(v); + if ((v = getenv("WFG_FB_ROBUST")) != NULL) d->fb_robust = (float)atof(v); + if ((v = getenv("WFG_HYB_VR")) != NULL) d->fb_hybrid_vr = atoi(v) != 0; + if ((v = getenv("WFG_HYB_LEVEL")) != NULL) d->fb_hybrid_level = (uint32_t)atoi(v); + if ((v = getenv("WFG_HYB_FP")) != NULL) d->fb_hybrid_fp = (uint32_t)atoi(v); + if ((v = getenv("WFG_HYB_SOR")) != NULL) d->fb_hybrid_sor = (uint32_t)atoi(v); + if ((v = getenv("WFG_HYB_L1START")) != NULL) d->fb_hybrid_l1_start = atoi(v) != 0; + if ((v = getenv("WFG_HYB_VR1")) != NULL) d->fb_hybrid_vr_upper = atoi(v) != 0; + if ((v = getenv("WFG_FB_ITERS")) != NULL) d->fb_iterations = atoi(v); + if ((v = getenv("WFG_FB_RADIUS")) != NULL) d->fb_radius = atoi(v); + if ((v = getenv("WFG_FB_WSIGMA")) != NULL) d->fb_window_sigma = (float)atof(v); + if ((v = getenv("WFG_FB_PSIGMA")) != NULL) d->fb_poly_sigma = (float)atof(v); } dis_fb_moments(DIS_FB_POLY_N, d->fb_poly_sigma, d->fb_ig); - DIS_LOGI("OpenFlow flow estimator: %s", + DIS_LOGI("WFG flow estimator: %s", d->flow_engine == DIS_ENGINE_DIS ? "DIS" : d->flow_engine == DIS_ENGINE_FARNEBACK ? "Farneback" : "Farneback + DIS levels 1-0"); vkd.GetPhysicalDeviceMemoryProperties(physical_device, &d->mem_props); d->luma_format = dis_pick_luma_format(d); if (!dis_audit_formats(d)) { - DIS_LOGW("OpenFlow cannot run on this device's format support; frame generation stays off"); + DIS_LOGW("WFG cannot run on this device's format support; frame generation stays off"); vkr_dis_destroy(d); return NULL; } if (!dis_create_sampler(d) || !dis_create_pipelines(d)) { - DIS_LOGW("OpenFlow shaders could not be built; frame generation stays off"); + DIS_LOGW("WFG shaders could not be built; frame generation stays off"); vkr_dis_destroy(d); return NULL; } if (!dis_allocate_sets(d)) { - DIS_LOGW("OpenFlow descriptor sets could not be allocated; frame generation stays off"); + DIS_LOGW("WFG descriptor sets could not be allocated; frame generation stays off"); vkr_dis_destroy(d); return NULL; } - DIS_LOGI("OpenFlow frame generation ready"); + DIS_LOGI("WFG frame generation ready"); return d; } @@ -2007,7 +2007,7 @@ bool vkr_dis_prepare(VkrDis* d, uint32_t width, uint32_t height, VkFormat format if (missing) { char names[256]; dis_missing_features(missing, names, sizeof(names)); - DIS_LOGW("OpenFlow needs %s on the guest frame format (%d) and this device does not " + DIS_LOGW("WFG needs %s on the guest frame format (%d) and this device does not " "report it; frame generation stays off", names, (int)format); d->unavailable = true; return false; @@ -2017,7 +2017,7 @@ bool vkr_dis_prepare(VkrDis* d, uint32_t width, uint32_t height, VkFormat format d->levels = levels; if (!dis_create_resources(d, w, h, content.width, content.height, format)) { - DIS_LOGW("OpenFlow resource build failed at %ux%u; frame generation unavailable", w, h); + DIS_LOGW("WFG resource build failed at %ux%u; frame generation unavailable", w, h); dis_destroy_images(d); d->unavailable = true; return false; @@ -2036,7 +2036,7 @@ bool vkr_dis_prepare(VkrDis* d, uint32_t width, uint32_t height, VkFormat format d->next_idx = 0; d->active_slot = 0; d->last_generations = 0; - DIS_LOGI("OpenFlow resources built at %ux%u (flow min side %u, %u levels); content rect " + DIS_LOGI("WFG resources built at %ux%u (flow min side %u, %u levels); content rect " "%dx%d+%d+%d inside a %ux%u composite", w, h, d->flow_min_side, levels, (int)content.width, (int)content.height, (int)content.x, (int)content.y, width, height); @@ -2093,7 +2093,7 @@ static void dis_log_plan(VkrDis* d, uint64_t now, float source_rate, float desir if (d->planned_gen == d->plan_log_gen && now - d->plan_log_ns < DIS_PLAN_LOG_NS) return; d->plan_log_gen = d->planned_gen; d->plan_log_ns = now; - DIS_LOGI("OpenFlow plan: source %.1f fps, target %.1f fps, ratio %.2f -> %d generated " + DIS_LOGI("WFG plan: source %.1f fps, target %.1f fps, ratio %.2f -> %d generated " "(capacity %u, output %.1f fps)", (double)source_rate, (double)desired, (double)ratio, d->planned_gen, capacity, (double)(source_rate * (float)(d->planned_gen + 1))); @@ -2432,7 +2432,7 @@ static VkCommandBuffer dis_hardware_motion(VkrDis* d, VkCommandBuffer cmd, uint3 } d->me_hinted++; if ((d->me_hinted % 600u) == 1u) { - DIS_LOGI("OpenFlow hardware motion hint: %llu of %llu pairs seeded", + DIS_LOGI("WFG hardware motion hint: %llu of %llu pairs seeded", (unsigned long long)d->me_hinted, (unsigned long long)d->me_pairs); } return cmd; diff --git a/app/src/main/cpp/winlator/vk/framegen/fg_present.c b/app/src/main/cpp/winlator/vk/framegen/fg_present.c index d9b09a1f3..db99c78e2 100644 --- a/app/src/main/cpp/winlator/vk/framegen/fg_present.c +++ b/app/src/main/cpp/winlator/vk/framegen/fg_present.c @@ -737,7 +737,7 @@ static bool fg_engine_acquire(FgPresenter* fg) { if (fg->dis) return true; fg->dis = vkr_dis_create(fg->device, fg->physical_device); if (!fg->dis) { - FG_LOGW("OpenFlow optical flow is unavailable on this device"); + FG_LOGW("WFG optical flow is unavailable on this device"); return false; } return true; @@ -756,7 +756,7 @@ static bool fg_engine_ready(const FgPresenter* fg) { } static const char* fg_engine_name(uint32_t engine) { - return engine == FG_ENGINE_DIS ? "OpenFlow" : "LSFG"; + return engine == FG_ENGINE_DIS ? "WFG" : "LSFG"; } static void fg_apply_config(FgPresenter* fg) { @@ -822,7 +822,7 @@ static VkCommandBuffer fg_dis_flush(void* user, VkCommandBuffer cmd) { if (vkQueueSubmit(fg->queue, 1, &si, fg->flush_fence) == VK_SUCCESS) { vkWaitForFences(fg->device, 1, &fg->flush_fence, VK_TRUE, UINT64_MAX); } else { - FG_LOGW("OpenFlow mid-frame submit failed"); + FG_LOGW("WFG mid-frame submit failed"); vkDeviceWaitIdle(fg->device); } vkResetCommandBuffer(cmd, 0); diff --git a/app/src/main/cpp/winlator/vk/vk_renderer.c b/app/src/main/cpp/winlator/vk/vk_renderer.c index 986b52c66..f2e575b05 100644 --- a/app/src/main/cpp/winlator/vk/vk_renderer.c +++ b/app/src/main/cpp/winlator/vk/vk_renderer.c @@ -1960,7 +1960,7 @@ static VkCommandBuffer dis_flush_frame(void* user, VkCommandBuffer cmd) { if (sr == VK_SUCCESS && r->dis_flush_fence) { vkWaitForFences(r->device, 1, &r->dis_flush_fence, VK_TRUE, UINT64_MAX); } else if (sr != VK_SUCCESS) { - VK_LOGW("OpenFlow mid-frame submit -> %d", (int)sr); + VK_LOGW("WFG mid-frame submit -> %d", (int)sr); } vkResetCommandBuffer(cmd, 0); VkCommandBufferBeginInfo bi = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; @@ -1974,7 +1974,7 @@ static void create_dis(VkRenderer* r) { r->dis = vkr_dis_create(r->device, r->physical_device); if (!r->dis) { - VK_LOGW("OpenFlow shaders unavailable; frame generation stays off"); + VK_LOGW("WFG shaders unavailable; frame generation stays off"); return; } vkr_dis_configure(r->dis, r->dis_scale ? r->dis_scale : 180u, r->dis_target_fps, @@ -3987,7 +3987,7 @@ JNIEXPORT void JNICALL JNI_FN(nativeSetDisFrameGenerationEnabled)(JNIEnv* env, j } framegen_rebuild_swapchain(r); pthread_mutex_unlock(&r->render_mutex); - VK_LOGI("OpenFlow frame generation composite path %s (supported=%d)", + VK_LOGI("WFG frame generation composite path %s (supported=%d)", want ? "enabled" : "disabled", (int)r->framegen_supported); } diff --git a/docs/FRAME-GENERATION.md b/docs/FRAME-GENERATION.md index 94de4ef4f..59341b3f8 100644 --- a/docs/FRAME-GENERATION.md +++ b/docs/FRAME-GENERATION.md @@ -12,7 +12,7 @@ its own settings so switching between them does not disturb the other. | Engine | Needs | Character | | --- | --- | --- | | **Lossless Scaling (LSFG)** | Your own copy of Lossless Scaling on Steam | The 25-shader chain from Lossless Scaling, ported to Vulkan | -| **OpenFlow** | Nothing — ships with the APK | Dense Inverse Search optical flow, fully open source | +| **WFG** | Nothing — ships with the APK | Dense Inverse Search optical flow, fully open source | ## Lossless Scaling (LSFG) @@ -26,9 +26,9 @@ Generation**. WinNative finds the DLL automatically from your Steam library; if **Select Lossless.dll…** to point at it. The LSFG half of the **FG** tab stays disabled until the shaders import successfully. -## OpenFlow +## WFG -**Nothing to buy, nothing to import.** OpenFlow is a complete open-source Dense Inverse Search frame +**Nothing to buy, nothing to import.** WFG is a complete open-source Dense Inverse Search frame generator built into WinNative — sixteen compute shaders that ship with the APK — so it works on a fresh install with no Steam account and no `Lossless.dll`. Turn it on in the **FG** tab and it runs. @@ -41,12 +41,12 @@ runs. | Multiplier | LSFG | 2× / 3× / 4× — generated frames per rendered frame | | Adaptive Target | LSFG | Aim for a specific output rate (60/90/120/144/165) instead of a fixed multiplier | | Flow Scale | LSFG | 25–100%, resolution of the optical-flow pyramid; lower is cheaper and softer | -| Resolution Scale | OpenFlow | Fast / Balance / Quality — 180 / 252 / 360 pixels on the frame's **shorter** side | -| Target FPS | OpenFlow | Max refresh rate, or a specific one (60/90/120/144/165) | -| Show flow | OpenFlow | Debug view: renders the estimated motion field instead of the frame | +| Resolution Scale | WFG | Fast / Balance / Quality — 180 / 252 / 360 pixels on the frame's **shorter** side | +| Target FPS | WFG | Max refresh rate, or a specific one (60/90/120/144/165) | +| Show flow | WFG | Debug view: renders the estimated motion field instead of the frame | | FPS Limiter | Both | Caps the game's own frame rate, from 15 fps upward | -OpenFlow states its Resolution Scale in pixels rather than as a percentage on purpose: a fixed pixel +WFG states its Resolution Scale in pixels rather than as a percentage on purpose: a fixed pixel budget costs the same on a 720p container and a 1440p one, whereas the same percentage would cost four times as much on the larger container without telling the search anything more about the motion. Lower is cheaper; higher tracks small or fast-moving detail better. @@ -64,6 +64,6 @@ gives the most even pacing. ## Credits Both engines are other people's work. The LSFG chain derives from **Camille LaVey**'s Vulkan port -in the Eden Emulator Project, which derives from **PancakeTAS**'s lsfg-vk; the OpenFlow engine was +in the Eden Emulator Project, which derives from **PancakeTAS**'s lsfg-vk; the WFG engine was contributed by **qwertypower** of DEVAR Entertainment LLC. See [CREDITS.md](../CREDITS.md) for the full attribution, including which source files came from where. From 97aced01e98b79ec8e255bcf968d1fd495bfa36b Mon Sep 17 00:00:00 2001 From: qwertypower <1qwertypower1@gmail.com> Date: Sat, 10 Oct 2026 23:49:47 +0300 Subject: [PATCH 04/18] WFG: hysteresis on the display-rate cap of the generation count --- app/src/main/cpp/wfg/src/vkr_dis.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/cpp/wfg/src/vkr_dis.c b/app/src/main/cpp/wfg/src/vkr_dis.c index 0ae0c4ab8..bccd572a2 100644 --- a/app/src/main/cpp/wfg/src/vkr_dis.c +++ b/app/src/main/cpp/wfg/src/vkr_dis.c @@ -2156,10 +2156,13 @@ uint32_t vkr_dis_plan(VkrDis* d, uint32_t capacity, uint64_t source_frames) { // itself that overshoots it: 28.4 fps at 120 Hz is a ratio of 4.22, rounded to 5 outputs // - 142 fps. The swapchain then runs out of free images, generated frames time out on // acquire and are dropped, and the pair shows one frame fewer than its neighbours: a - // visible jerk. Capped at the display rate, with 5% for the rate estimate's noise: at 2% a - // 30 fps game reading 30.7 for a few frames already dropped to 2 generated frames. + // visible jerk. Capped at the display rate, with room for the rate estimate's noise: a + // steady 30 fps game reads anywhere from 27.6 to 31.6 on a Snapdragon 8 Gen 3, and each + // flip of the count is a hitch of its own. 5% to step up to a count, 10% to step down + // from the one in use. if (d->refresh_rate > 0.0f) { - const int max_gen = (int)floorf(d->refresh_rate * 1.05f / source_rate) - 1; + const float slack = raw > d->planned_gen ? 1.05f : 1.10f; + const int max_gen = (int)floorf(d->refresh_rate * slack / source_rate) - 1; if (max_gen >= 1 && raw > max_gen) raw = max_gen; } } From 1de7e6c554eda0005f5aeb755e6f89bcf1bd41ab Mon Sep 17 00:00:00 2001 From: qwertypower <1qwertypower1@gmail.com> Date: Sat, 10 Oct 2026 23:53:12 +0300 Subject: [PATCH 05/18] WFG: step the generation count up only after half a second --- app/src/main/cpp/wfg/src/vkr_dis.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/cpp/wfg/src/vkr_dis.c b/app/src/main/cpp/wfg/src/vkr_dis.c index bccd572a2..067db24c8 100644 --- a/app/src/main/cpp/wfg/src/vkr_dis.c +++ b/app/src/main/cpp/wfg/src/vkr_dis.c @@ -2170,7 +2170,13 @@ uint32_t vkr_dis_plan(VkrDis* d, uint32_t capacity, uint64_t source_frames) { if (raw > d->planned_gen) { d->gen_low_streak = 0; d->gen_high_streak++; - if (d->gen_high_streak >= 2) { + // Once generating, a step up waits for half a second of the slower rate: a game that + // hitches for a few frames reads as 25 fps from 30, and a step to 4 and back is two + // changes of cadence plus a frame the display cannot take. A step down stays quick, + // since too many frames is what overflows the swapchain. + int up_streak = d->planned_gen > 0 ? (int)(source_rate * 0.5f) : 2; + if (up_streak < 2) up_streak = 2; + if (d->gen_high_streak >= up_streak) { d->planned_gen = raw; d->gen_high_streak = 0; } From 7a0569b2709cc4ffbce94acc7a8825fba3af7029 Mon Sep 17 00:00:00 2001 From: qwertypower <1qwertypower1@gmail.com> Date: Mon, 12 Oct 2026 22:41:38 +0300 Subject: [PATCH 06/18] WFG: forward-splat the field to each generated frame where layers compete --- app/src/main/cpp/wfg/CMakeLists.txt | 1 + .../wfg/include/shaders/dis_interpolate.comp | 45 ++++++++- .../cpp/wfg/include/shaders/dis_splat.comp | 98 +++++++++++++++++++ app/src/main/cpp/wfg/src/vkr_dis.c | 61 +++++++++++- 4 files changed, 199 insertions(+), 6 deletions(-) create mode 100644 app/src/main/cpp/wfg/include/shaders/dis_splat.comp diff --git a/app/src/main/cpp/wfg/CMakeLists.txt b/app/src/main/cpp/wfg/CMakeLists.txt index db267298d..d59e59c8c 100644 --- a/app/src/main/cpp/wfg/CMakeLists.txt +++ b/app/src/main/cpp/wfg/CMakeLists.txt @@ -25,6 +25,7 @@ set(DIS_SHADERS dis_hist dis_side dis_flow_pack + dis_splat dis_vr_prep dis_vr_d1 dis_vr_d2 diff --git a/app/src/main/cpp/wfg/include/shaders/dis_interpolate.comp b/app/src/main/cpp/wfg/include/shaders/dis_interpolate.comp index 5bc272a3a..ebe66b951 100644 --- a/app/src/main/cpp/wfg/include/shaders/dis_interpolate.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_interpolate.comp @@ -19,6 +19,9 @@ layout(set = 0, binding = 2) uniform sampler2D flowTex; layout(set = 0, binding = 3) uniform sampler2D sideTex; layout(set = 0, binding = 4) uniform sampler2D histTex; layout(set = 0, binding = 5, rgba8) uniform image2D outImage; +// The field splatted to t (dis_splat.comp): xy the vector times z, z the share of the bilinear +// footprint something landed on. +layout(set = 0, binding = 6) uniform sampler2D splatTex; layout(push_constant) uniform PC { float t; @@ -55,6 +58,17 @@ layout(constant_id = 0) const int manualFlowFilter = 0; #define DIS_USE_HIST 1 #endif +// Take the vector that lands on the pixel from the splatted field where there is one. +#ifndef DIS_SPLAT +#define DIS_SPLAT 1 +#endif +#ifndef SPLAT_APART_LO +#define SPLAT_APART_LO 0.5 +#endif +#ifndef SPLAT_APART_HI +#define SPLAT_APART_HI 1.5 +#endif + // Hand a pixel that a warped sample fetches from a static overlay to the other frame. #ifndef DIS_OVERLAY_OCCL #define DIS_OVERLAY_OCCL 1 @@ -177,15 +191,38 @@ void main() { if (any(isnan(fn)) || any(isinf(fn))) break; fa = fn; } - fa = mix(f, fa, edgeMix); #endif - - // The real pair where it sits: the early-out below and the static test at the end both - // need it. + // The real pair where it sits: the splat guard, the early-out below and the static test at + // the end all need it. vec3 s0 = textureLod(prevColor, uv, 0.0).xyz; vec3 s1 = textureLod(nextColor, uv, 0.0).xyz; float rStatic = dot(abs(s0 - s1), vec3(1.0)); +#if DIS_SPLAT + // The fixed-point steps cannot choose between two layers that both claim the pixel; the + // splat already did, by which of them the next frame confirms. Where nothing landed (content + // coming into view) the steps stand. Only where the two disagree by more than a flow + // texel: inside one layer the steps follow the smooth field, and the splat, a winner per + // texel, would only make it blocky. + vec4 sp = textureLod(splatTex, uv, 0.0); + if (sp.z > 1e-3 && !(any(isnan(sp.xy)) || any(isinf(sp.xy)))) { + vec2 fs = sp.xy / sp.z; + float apart = length((fs - fa) * vec2(textureSize(splatTex, 0))); + float take = smoothstep(0.25, 0.75, sp.z) * smoothstep(SPLAT_APART_LO, SPLAT_APART_HI, apart); + // Not on a static overlay - still in the history and the same in both frames here: there + // the moving scenery behind ties with it on how well the pair confirms it and outranks + // its zero on length. + if (take > 0.0) { + take *= 1.0 - textureLod(histTex, uv, 0.0).r * (1.0 - smoothstep(0.03, 0.12, rStatic)); + } + fa = mix(fa, fs, take); + } +#endif +#if DIS_SOURCE_STEPS > 0 || DIS_SPLAT + fa = mix(f, fa, edgeMix); +#endif + + // Under a quarter of a pixel no warp can change an output pixel visibly, and where the // pair also agrees nothing crossed the pixel either, so the ten warped fetches below // would buy nothing. The agreement test matters: a still background just ahead of a diff --git a/app/src/main/cpp/wfg/include/shaders/dis_splat.comp b/app/src/main/cpp/wfg/include/shaders/dis_splat.comp new file mode 100644 index 000000000..67810c7aa --- /dev/null +++ b/app/src/main/cpp/wfg/include/shaders/dis_splat.comp @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) +// SPDX-License-Identifier: GPL-3.0-or-later +// +// WFG frame generation: a Vulkan compute realisation of Dense Inverse Search +// optical flow. The algorithm and its reference implementation come from +// OpenCV's DISOpticalFlow, which adopted Till Kroeger's original OF_DIS. +// See CREDITS.md for the full attribution. + +#version 450 + +precision highp float; +precision highp int; + +// The field forward-splatted to the generated frame's time, at flow resolution: every level-0 +// texel carries its vector to where it lands at t, and where several land on one texel the +// one both real frames confirm wins. Run three times per generated frame (pc.mode): +// 0 - clear the key and the field; +// 1 - every texel atomically raises the key of the texels it lands on to its priority; +// 2 - the texels whose priority is the key there write their vector. +// +// The interpolate pass reads its vectors from the previous frame's grid and has to find, for +// each output pixel, the point that lands on it. Its fixed-point steps do that inside a layer +// but cannot choose between layers: around an object the camera orbits, the background sliding +// behind its silhouette and the object's own surface both claim the band along the edge, the +// lookup takes whichever the blurred field there is closer to, and the surface smears onto the +// background or the background onto the surface. Splatting asks the opposite question - who +// lands here - and the priority settles it the way FSR 3's frame interpolation does with depth +// or vector length, but on what the pair can tell: a vector whose content is still there at +// its end in the next frame is visible at t; one whose end shows something else is going +// behind an occluder (or is wrong) and gives way. Vector length breaks the remaining ties. +// +// The key is cleared to 0 and every priority is at least 1, so 0 means "nothing landed". + +layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; + +layout(set = 0, binding = 0) uniform sampler2D prevLuma; +layout(set = 0, binding = 1) uniform sampler2D nextLuma; +layout(set = 0, binding = 2) uniform sampler2D flowTex; +layout(set = 0, binding = 8, r32ui) uniform uimage2D splatKey; +layout(set = 0, binding = 9, rgba16f) uniform image2D splatField; + +layout(push_constant) uniform PC { + float t; + int mode; +} pc; + +// Mean absolute luma difference (0..255) per consistency step: 2 luma levels per step, 31 steps. +#define SPLAT_ERR_STEP 8.0 + +uint priority(ivec2 p, vec2 fpx, ivec2 sz) { + vec2 inv = 1.0 / vec2(sz); + vec2 end = vec2(p) + 0.5 + fpx; + // An end off the frame has nothing to confirm it. + if (any(lessThan(end, vec2(0.0))) || any(greaterThan(end, vec2(sz)))) return 1u; + float e = 0.0; + for (int y = -1; y <= 1; y++) { + for (int x = -1; x <= 1; x++) { + vec2 o = vec2(x, y); + float a = textureLod(prevLuma, (vec2(p) + 0.5 + o) * inv, 0.0).r; + float b = textureLod(nextLuma, (end + o) * inv, 0.0).r; + e += abs(a - b); + } + } + e *= 1.0 / 9.0; + uint q = 31u - uint(clamp(e / SPLAT_ERR_STEP, 0.0, 31.0)); + uint m = uint(clamp(length(fpx) * 4.0, 0.0, 1023.0)); + return ((q << 10) | m) + 1u; +} + +void main() { + ivec2 p = ivec2(gl_GlobalInvocationID.xy); + ivec2 sz = imageSize(splatKey); + if (p.x >= sz.x || p.y >= sz.y) return; + + if (pc.mode == 0) { + imageStore(splatKey, p, uvec4(0u)); + imageStore(splatField, p, vec4(0.0)); + return; + } + + vec2 f = texelFetch(flowTex, p, 0).xy; + if (any(isnan(f)) || any(isinf(f))) return; + vec2 fpx = f * vec2(sz); + uint key = priority(p, fpx, sz); + + // The four texels round the landing point, so a stretched field leaves no gaps of a texel. + vec2 target = vec2(p) + 0.5 + pc.t * fpx; + ivec2 base = ivec2(floor(target - 0.5)); + for (int i = 0; i < 4; i++) { + ivec2 q = base + ivec2(i & 1, i >> 1); + if (any(lessThan(q, ivec2(0))) || any(greaterThanEqual(q, sz))) continue; + if (pc.mode == 1) { + imageAtomicMax(splatKey, q, key); + } else if (imageLoad(splatKey, q).r == key) { + imageStore(splatField, q, vec4(f, 1.0, 0.0)); + } + } +} diff --git a/app/src/main/cpp/wfg/src/vkr_dis.c b/app/src/main/cpp/wfg/src/vkr_dis.c index 067db24c8..109912d4b 100644 --- a/app/src/main/cpp/wfg/src/vkr_dis.c +++ b/app/src/main/cpp/wfg/src/vkr_dis.c @@ -20,6 +20,7 @@ #include "shaders/dis_hist_comp.spv.h" #include "shaders/dis_side_comp.spv.h" #include "shaders/dis_flow_pack_comp.spv.h" +#include "shaders/dis_splat_comp.spv.h" #include "shaders/dis_me_luma_comp.spv.h" #include "shaders/dis_vr_prep_comp.spv.h" #include "shaders/dis_vr_d1_comp.spv.h" @@ -197,6 +198,8 @@ struct VkrDis { DisImage hist[2]; DisImage side; DisImage flow_out; + DisImage splat_key; + DisImage splat_field; DisImage me_field; // Farneback flow (DIS_ENGINE_FARNEBACK and _HYBRID): each frame's polynomial expansion, @@ -227,6 +230,8 @@ struct VkrDis { VkImageView view_hist[2]; VkImageView view_side; VkImageView view_flow_out; + VkImageView view_splat_key; + VkImageView view_splat_field; VkImageView view_me_field; VkImageView view_fb_b[DIS_SLOTS][DIS_MAX_LEVELS]; VkImageView view_fb_c[DIS_SLOTS][DIS_MAX_LEVELS]; @@ -249,6 +254,7 @@ struct VkrDis { VkDescriptorSet hist_sets[DIS_SLOTS][2]; VkDescriptorSet side_sets[DIS_SLOTS]; VkDescriptorSet pack_set; + VkDescriptorSet splat_sets[DIS_SLOTS]; // Farneback, all from vr_set_layout: the expansion of slot s at level l; the equations of // the pair ending in slot s at level l, starting from the level above ([0]) or from this // level's previous iteration ([1]); and the solve of level l. @@ -282,6 +288,7 @@ struct VkrDis { DisPass pass_fb_vsolve; DisPass pass_side; DisPass pass_pack; + DisPass pass_splat; DisPass pass_me_luma; DisPass pass_vr_prep; DisPass pass_vr_d1; @@ -577,6 +584,8 @@ static uint32_t dis_collect_images(VkrDis* d, DisImage** out, uint32_t cap) { DIS_PUSH(&d->hist[1]); DIS_PUSH(&d->side); DIS_PUSH(&d->flow_out); + DIS_PUSH(&d->splat_key); + DIS_PUSH(&d->splat_field); DIS_PUSH(&d->me_field); for (uint32_t s = 0; s < DIS_SLOTS; s++) { DIS_PUSH(&d->fb_b[s]); @@ -787,7 +796,7 @@ static bool dis_create_pipelines(VkrDis* d) { // VR sets exist only for the levels the refinement actually runs on. const uint32_t vr_sets = (DIS_SLOTS + DIS_VR_SHARED_SETS) * DIS_VR_LEVELS; - const uint32_t hist_sets = DIS_SLOTS * 2u; + const uint32_t hist_sets = DIS_SLOTS * 2u + DIS_SLOTS; // + the splat sets const uint32_t fb_sets = DIS_SLOTS * DIS_MAX_LEVELS * 3u + DIS_MAX_LEVELS + DIS_SLOTS + 1u; const uint32_t vr_layout_sets = vr_sets + hist_sets + fb_sets; const uint32_t total_sets = shared_sets + vr_layout_sets; @@ -890,6 +899,7 @@ static bool dis_create_pipelines(VkrDis* d) { d->pass_fb_vsolve.pipeline = dis_create_compute_pipeline_with_layout(d, dis_fb_vsolve_comp, dis_fb_vsolve_comp_size, d->vr_pipeline_layout, NULL); d->pass_side.pipeline = dis_create_compute_pipeline(d, dis_side_comp, dis_side_comp_size); d->pass_pack.pipeline = dis_create_compute_pipeline(d, dis_flow_pack_comp, dis_flow_pack_comp_size); + d->pass_splat.pipeline = dis_create_compute_pipeline_with_layout(d, dis_splat_comp, dis_splat_comp_size, d->vr_pipeline_layout, NULL); // The hardware-motion luminance pass writes a host-visible buffer, which neither shared // layout has, so it gets a small layout and pool of its own: the colour frame and the buffer. @@ -944,7 +954,7 @@ static bool dis_create_pipelines(VkrDis* d) { !d->pass_vr_prep.pipeline || !d->pass_vr_d1.pipeline || !d->pass_vr_d2.pipeline || !d->pass_vr_w.pipeline || !d->pass_vr_coef.pipeline || !d->pass_vr_sor.pipeline || !d->pass_vr_add.pipeline || !d->pass_hist.pipeline || !d->pass_side.pipeline || - !d->pass_pack.pipeline || !d->pass_me_luma.pipeline || + !d->pass_pack.pipeline || !d->pass_splat.pipeline || !d->pass_me_luma.pipeline || !d->pass_fb_poly.pipeline || !d->pass_fb_hsum.pipeline || !d->pass_fb_vsolve.pipeline) { return false; } @@ -1158,6 +1168,7 @@ static void dis_write_all_descriptors(VkrDis* d) { dis_batch_sampled(d, &b, d->interp_sets[s][dir], 2, d->view_flow_out, d->sampler); dis_batch_sampled(d, &b, d->interp_sets[s][dir], 3, d->view_side, d->sampler); dis_batch_sampled(d, &b, d->interp_sets[s][dir], 4, d->view_hist[dir], d->sampler); + dis_batch_sampled(d, &b, d->interp_sets[s][dir], 6, d->view_splat_field, d->sampler); dis_batch_storage(d, &b, d->interp_sets[s][dir], 5, d->view_interp_out); dis_batch_sampled(d, &b, d->hist_sets[s][dir], 0, d->view_color[prev], d->sampler); @@ -1166,6 +1177,12 @@ static void dis_write_all_descriptors(VkrDis* d) { dis_batch_storage(d, &b, d->hist_sets[s][dir], DIS_VR_FIRST_STORAGE, d->view_hist[dir]); } + dis_batch_sampled(d, &b, d->splat_sets[s], 0, d->view_flow_luma[prev][0], d->sampler); + dis_batch_sampled(d, &b, d->splat_sets[s], 1, d->view_flow_luma[next][0], d->sampler); + dis_batch_sampled(d, &b, d->splat_sets[s], 2, d->view_flow_out, d->sampler); + dis_batch_storage(d, &b, d->splat_sets[s], DIS_VR_FIRST_STORAGE, d->view_splat_key); + dis_batch_storage(d, &b, d->splat_sets[s], DIS_VR_FIRST_STORAGE + 1, d->view_splat_field); + dis_batch_sampled(d, &b, d->side_sets[s], 0, d->view_flow_refined[0], d->sampler); dis_batch_storage(d, &b, d->side_sets[s], 5, d->view_side); @@ -1273,6 +1290,8 @@ static void dis_destroy_views(VkrDis* d) { dis_destroy_view(d, &d->view_hist[1]); dis_destroy_view(d, &d->view_side); dis_destroy_view(d, &d->view_flow_out); + dis_destroy_view(d, &d->view_splat_key); + dis_destroy_view(d, &d->view_splat_field); dis_destroy_view(d, &d->view_me_field); dis_destroy_view(d, &d->view_fb_l1); for (uint32_t l = 0; l < DIS_MAX_LEVELS; l++) { @@ -1327,6 +1346,8 @@ static void dis_destroy_images(VkrDis* d) { dis_destroy_image(d, &d->hist[1]); dis_destroy_image(d, &d->side); dis_destroy_image(d, &d->flow_out); + dis_destroy_image(d, &d->splat_key); + dis_destroy_image(d, &d->splat_field); dis_destroy_image(d, &d->me_field); for (uint32_t s = 0; s < DIS_SLOTS; s++) { dis_destroy_image(d, &d->fb_b[s]); @@ -1525,6 +1546,12 @@ static bool dis_create_resources(VkrDis* d, uint32_t w, uint32_t h, uint32_t ful // every device, so its three lookups per output pixel stay single bilinear taps. if (!dis_create_image(d, &d->flow_out, w, h, VK_FORMAT_R16G16_SFLOAT, 1, VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT)) return false; + // The field splatted to a generated frame's time (dis_splat.comp): the winning priority per + // texel (R32_UINT, whose storage atomics every device has) and the vector it carries. + if (!dis_create_image(d, &d->splat_key, w, h, VK_FORMAT_R32_UINT, 1, + VK_IMAGE_USAGE_STORAGE_BIT)) return false; + if (!dis_create_image(d, &d->splat_field, w, h, VK_FORMAT_R16G16B16A16_SFLOAT, 1, + VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT)) return false; // Hardware motion hint, one vector per block of the level it seeds. It always exists so the // search's binding stays valid; the search reads it only on frames that uploaded one. const uint32_t hint_w = L >= 3 ? (w >> 2) : 1u; @@ -1581,6 +1608,8 @@ static bool dis_create_resources(VkrDis* d, uint32_t w, uint32_t h, uint32_t ful if (!dis_create_view(d, d->hist[1].image, VK_FORMAT_R32_SFLOAT, 0, 1, &d->view_hist[1])) return false; if (!dis_create_view(d, d->side.image, VK_FORMAT_R32_SFLOAT, 0, 1, &d->view_side)) return false; if (!dis_create_view(d, d->flow_out.image, VK_FORMAT_R16G16_SFLOAT, 0, 1, &d->view_flow_out)) return false; + if (!dis_create_view(d, d->splat_key.image, VK_FORMAT_R32_UINT, 0, 1, &d->view_splat_key)) return false; + if (!dis_create_view(d, d->splat_field.image, VK_FORMAT_R16G16B16A16_SFLOAT, 0, 1, &d->view_splat_field)) return false; if (!dis_create_view(d, d->me_field.image, VK_FORMAT_R32G32_SFLOAT, 0, 1, &d->view_me_field)) return false; if (!dis_create_view(d, d->fb_l1.image, VK_FORMAT_R32G32_SFLOAT, 0, 1, &d->view_fb_l1)) return false; @@ -1626,6 +1655,7 @@ static bool dis_allocate_sets(VkrDis* d) { if (!dis_alloc(d, d->set_layout, 1, &d->side_sets[s])) return false; if (!dis_alloc(d, d->vr_set_layout, 1, &d->hist_sets[s][0])) return false; if (!dis_alloc(d, d->vr_set_layout, 1, &d->hist_sets[s][1])) return false; + if (!dis_alloc(d, d->vr_set_layout, 1, &d->splat_sets[s])) return false; for (uint32_t l = 0; l < DIS_VR_LEVELS; l++) { if (!dis_alloc(d, d->vr_set_layout, 1, &d->vr_prep_sets[s][l])) return false; } @@ -1923,6 +1953,7 @@ void vkr_dis_destroy(VkrDis* d) { if (d->pass_hist.pipeline) vkd.DestroyPipeline(d->device, d->pass_hist.pipeline, NULL); if (d->pass_side.pipeline) vkd.DestroyPipeline(d->device, d->pass_side.pipeline, NULL); if (d->pass_pack.pipeline) vkd.DestroyPipeline(d->device, d->pass_pack.pipeline, NULL); + if (d->pass_splat.pipeline) vkd.DestroyPipeline(d->device, d->pass_splat.pipeline, NULL); if (d->pass_fb_poly.pipeline) vkd.DestroyPipeline(d->device, d->pass_fb_poly.pipeline, NULL); if (d->pass_fb_hsum.pipeline) vkd.DestroyPipeline(d->device, d->pass_fb_hsum.pipeline, NULL); if (d->pass_fb_vsolve.pipeline) vkd.DestroyPipeline(d->device, d->pass_fb_vsolve.pipeline, NULL); @@ -2733,6 +2764,32 @@ static void dis_render_into(VkrDis* d, VkCommandBuffer cmd, float t, int debug_m const uint32_t w = d->content.width; const uint32_t h = d->content.height; + // The field splatted to t: clear, raise the keys, write the winners (dis_splat.comp). + { + const uint32_t fw = d->flow_out.extent.width; + const uint32_t fh = d->flow_out.extent.height; + vkd.CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->pass_splat.pipeline); + vkd.CmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->vr_pipeline_layout, 0, 1, + &d->splat_sets[d->active_slot], 0, NULL); + for (int mode = 0; mode < 3; mode++) { + const struct { float t; int mode; } spc = {t, mode}; + vkd.CmdPushConstants(cmd, d->vr_pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, + sizeof(spc), &spc); + vkd.CmdDispatch(cmd, (fw + DIS_LOCAL_SIZE - 1) / DIS_LOCAL_SIZE, + (fh + DIS_LOCAL_SIZE - 1) / DIS_LOCAL_SIZE, 1); + // The key is read-modify-written by the atomics, so the next pass waits for both. + VkMemoryBarrier mb; + memset(&mb, 0, sizeof(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; + vkd.CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 1, &mb, 0, NULL, 0, + NULL); + } + DIS_PROF(cmd, "splat"); + } + DisInterpPC ipc; ipc.t = t; ipc.debugMode = debug_mode; From e2d1eda05cd83f5fd1d76f1b02d6ae65b0c16a2b Mon Sep 17 00:00:00 2001 From: qwertypower <1qwertypower1@gmail.com> Date: Mon, 12 Oct 2026 23:12:00 +0300 Subject: [PATCH 07/18] WFG: splat once per source pair for all generated frames --- .../wfg/include/shaders/dis_interpolate.comp | 13 +++- .../cpp/wfg/include/shaders/dis_splat.comp | 45 +++++++---- app/src/main/cpp/wfg/src/vkr_dis.c | 75 +++++++++++-------- 3 files changed, 81 insertions(+), 52 deletions(-) diff --git a/app/src/main/cpp/wfg/include/shaders/dis_interpolate.comp b/app/src/main/cpp/wfg/include/shaders/dis_interpolate.comp index ebe66b951..7eda5fb24 100644 --- a/app/src/main/cpp/wfg/include/shaders/dis_interpolate.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_interpolate.comp @@ -20,12 +20,13 @@ layout(set = 0, binding = 3) uniform sampler2D sideTex; layout(set = 0, binding = 4) uniform sampler2D histTex; layout(set = 0, binding = 5, rgba8) uniform image2D outImage; // The field splatted to t (dis_splat.comp): xy the vector times z, z the share of the bilinear -// footprint something landed on. +// footprint something landed on. One strip of SPLAT_STRIPS per generated frame, stacked down. layout(set = 0, binding = 6) uniform sampler2D splatTex; layout(push_constant) uniform PC { float t; int debugMode; + int layer; // which generated frame of the pair: the splat strip } pc; layout(constant_id = 0) const int manualFlowFilter = 0; @@ -62,6 +63,10 @@ layout(constant_id = 0) const int manualFlowFilter = 0; #ifndef DIS_SPLAT #define DIS_SPLAT 1 #endif +// Strips in the splat images: VKR_DIS_MAX_GENERATIONS. +#ifndef SPLAT_STRIPS +#define SPLAT_STRIPS 7 +#endif #ifndef SPLAT_APART_LO #define SPLAT_APART_LO 0.5 #endif @@ -204,10 +209,12 @@ void main() { // coming into view) the steps stand. Only where the two disagree by more than a flow // texel: inside one layer the steps follow the smooth field, and the splat, a winner per // texel, would only make it blocky. - vec4 sp = textureLod(splatTex, uv, 0.0); + vec2 splatSize = vec2(textureSize(splatTex, 0)) / vec2(1.0, float(SPLAT_STRIPS)); + float sv = clamp(uv.y, 0.5 / splatSize.y, 1.0 - 0.5 / splatSize.y); + vec4 sp = textureLod(splatTex, vec2(uv.x, (float(pc.layer) + sv) / float(SPLAT_STRIPS)), 0.0); if (sp.z > 1e-3 && !(any(isnan(sp.xy)) || any(isinf(sp.xy)))) { vec2 fs = sp.xy / sp.z; - float apart = length((fs - fa) * vec2(textureSize(splatTex, 0))); + float apart = length((fs - fa) * splatSize); float take = smoothstep(0.25, 0.75, sp.z) * smoothstep(SPLAT_APART_LO, SPLAT_APART_HI, apart); // Not on a static overlay - still in the history and the same in both frames here: there // the moving scenery behind ties with it on how well the pair confirms it and outranks diff --git a/app/src/main/cpp/wfg/include/shaders/dis_splat.comp b/app/src/main/cpp/wfg/include/shaders/dis_splat.comp index 67810c7aa..01d826c44 100644 --- a/app/src/main/cpp/wfg/include/shaders/dis_splat.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_splat.comp @@ -13,7 +13,8 @@ precision highp int; // The field forward-splatted to the generated frame's time, at flow resolution: every level-0 // texel carries its vector to where it lands at t, and where several land on one texel the -// one both real frames confirm wins. Run three times per generated frame (pc.mode): +// one both real frames confirm wins. Run three times per source pair (pc.mode), for every +// generated frame of the pair at once - one strip of the images per frame: // 0 - clear the key and the field; // 1 - every texel atomically raises the key of the texels it lands on to its priority; // 2 - the texels whose priority is the key there write their vector. @@ -30,6 +31,11 @@ precision highp int; // behind an occluder (or is wrong) and gives way. Vector length breaks the remaining ties. // // The key is cleared to 0 and every priority is at least 1, so 0 means "nothing landed". +// +// Per pair rather than per generated frame because what it costs is barriers, not work: the +// grid is the flow's, a few tens of thousands of texels, but three passes with a barrier +// between each added 0.13 ms per generated frame on an Adreno 750. Here the clear rides the +// luma pass's barrier, the splat the pack pass's, and only the resolve adds one. layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; @@ -40,11 +46,11 @@ layout(set = 0, binding = 8, r32ui) uniform uimage2D splatKey; layout(set = 0, binding = 9, rgba16f) uniform image2D splatField; layout(push_constant) uniform PC { - float t; + int gens; int mode; } pc; -// Mean absolute luma difference (0..255) per consistency step: 2 luma levels per step, 31 steps. +// Mean absolute luma difference (0..255) per consistency step: 8 luma levels per step, 31 steps. #define SPLAT_ERR_STEP 8.0 uint priority(ivec2 p, vec2 fpx, ivec2 sz) { @@ -69,30 +75,37 @@ uint priority(ivec2 p, vec2 fpx, ivec2 sz) { void main() { ivec2 p = ivec2(gl_GlobalInvocationID.xy); - ivec2 sz = imageSize(splatKey); + ivec2 sz = textureSize(flowTex, 0); if (p.x >= sz.x || p.y >= sz.y) return; if (pc.mode == 0) { - imageStore(splatKey, p, uvec4(0u)); - imageStore(splatField, p, vec4(0.0)); + for (int g = 0; g < pc.gens; g++) { + imageStore(splatKey, p + ivec2(0, g * sz.y), uvec4(0u)); + imageStore(splatField, p + ivec2(0, g * sz.y), vec4(0.0)); + } return; } vec2 f = texelFetch(flowTex, p, 0).xy; if (any(isnan(f)) || any(isinf(f))) return; vec2 fpx = f * vec2(sz); + // The same for every generated frame: scored once, splatted to each. uint key = priority(p, fpx, sz); - // The four texels round the landing point, so a stretched field leaves no gaps of a texel. - vec2 target = vec2(p) + 0.5 + pc.t * fpx; - ivec2 base = ivec2(floor(target - 0.5)); - for (int i = 0; i < 4; i++) { - ivec2 q = base + ivec2(i & 1, i >> 1); - if (any(lessThan(q, ivec2(0))) || any(greaterThanEqual(q, sz))) continue; - if (pc.mode == 1) { - imageAtomicMax(splatKey, q, key); - } else if (imageLoad(splatKey, q).r == key) { - imageStore(splatField, q, vec4(f, 1.0, 0.0)); + for (int g = 0; g < pc.gens; g++) { + // The four texels round the landing point, so a stretched field leaves no gaps. + float t = float(g + 1) / float(pc.gens + 1); + vec2 target = vec2(p) + 0.5 + t * fpx; + ivec2 base = ivec2(floor(target - 0.5)); + ivec2 strip = ivec2(0, g * sz.y); + for (int i = 0; i < 4; i++) { + ivec2 q = base + ivec2(i & 1, i >> 1); + if (any(lessThan(q, ivec2(0))) || any(greaterThanEqual(q, sz))) continue; + if (pc.mode == 1) { + imageAtomicMax(splatKey, q + strip, key); + } else if (imageLoad(splatKey, q + strip).r == key) { + imageStore(splatField, q + strip, vec4(f, 1.0, 0.0)); + } } } } diff --git a/app/src/main/cpp/wfg/src/vkr_dis.c b/app/src/main/cpp/wfg/src/vkr_dis.c index 109912d4b..1408e96e4 100644 --- a/app/src/main/cpp/wfg/src/vkr_dis.c +++ b/app/src/main/cpp/wfg/src/vkr_dis.c @@ -404,6 +404,7 @@ typedef struct { typedef struct { float t; int debugMode; + int layer; } DisInterpPC; typedef struct { @@ -1179,7 +1180,7 @@ static void dis_write_all_descriptors(VkrDis* d) { dis_batch_sampled(d, &b, d->splat_sets[s], 0, d->view_flow_luma[prev][0], d->sampler); dis_batch_sampled(d, &b, d->splat_sets[s], 1, d->view_flow_luma[next][0], d->sampler); - dis_batch_sampled(d, &b, d->splat_sets[s], 2, d->view_flow_out, d->sampler); + dis_batch_sampled(d, &b, d->splat_sets[s], 2, d->view_flow_refined[0], d->sampler); dis_batch_storage(d, &b, d->splat_sets[s], DIS_VR_FIRST_STORAGE, d->view_splat_key); dis_batch_storage(d, &b, d->splat_sets[s], DIS_VR_FIRST_STORAGE + 1, d->view_splat_field); @@ -1547,10 +1548,12 @@ static bool dis_create_resources(VkrDis* d, uint32_t w, uint32_t h, uint32_t ful if (!dis_create_image(d, &d->flow_out, w, h, VK_FORMAT_R16G16_SFLOAT, 1, VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT)) return false; // The field splatted to a generated frame's time (dis_splat.comp): the winning priority per - // texel (R32_UINT, whose storage atomics every device has) and the vector it carries. - if (!dis_create_image(d, &d->splat_key, w, h, VK_FORMAT_R32_UINT, 1, + // texel (R32_UINT, whose storage atomics every device has) and the vector it carries, one + // strip of the flow's height per generated frame of the pair. + if (!dis_create_image(d, &d->splat_key, w, h * VKR_DIS_MAX_GENERATIONS, VK_FORMAT_R32_UINT, 1, VK_IMAGE_USAGE_STORAGE_BIT)) return false; - if (!dis_create_image(d, &d->splat_field, w, h, VK_FORMAT_R16G16B16A16_SFLOAT, 1, + if (!dis_create_image(d, &d->splat_field, w, h * VKR_DIS_MAX_GENERATIONS, + VK_FORMAT_R16G16B16A16_SFLOAT, 1, VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT)) return false; // Hardware motion hint, one vector per block of the level it seeds. It always exists so the // search's binding stays valid; the search reads it only on frames that uploaded one. @@ -2541,6 +2544,9 @@ static void dis_farneback(VkrDis* d, VkCommandBuffer cmd, uint32_t slot, uint32_ DIS_PROF(cmd, "farneback"); } +static void dis_splat_pass(VkrDis* d, VkCommandBuffer cmd, uint32_t slot, uint32_t generations, + int mode); + VkCommandBuffer vkr_dis_process_ex(VkrDis* d, VkCommandBuffer cmd, VkImage source, uint32_t width, uint32_t height, uint32_t generations, VkrDisFlushFn flush, void* flush_user) { @@ -2608,6 +2614,11 @@ VkCommandBuffer vkr_dis_process_ex(VkrDis* d, VkCommandBuffer cmd, VkImage sourc } const bool wants_flow = generations > 0 || d->debug_flow; + // The field splatted to every generated frame's time (dis_splat.comp): cleared here, under + // the luma pass's barrier; splatted next to the pack pass and resolved after it. + const uint32_t splat_gens = + generations < VKR_DIS_MAX_GENERATIONS ? generations : VKR_DIS_MAX_GENERATIONS; + if (splat_gens > 0) dis_splat_pass(d, cmd, slot, splat_gens, 0); if (d->me && flush && wants_flow) { cmd = dis_hardware_motion(d, cmd, slot, flush, flush_user); } else if (d->me) { @@ -2733,6 +2744,7 @@ VkCommandBuffer vkr_dis_process_ex(VkrDis* d, VkCommandBuffer cmd, VkImage sourc // other - the packed field and the side map read the refined field, the overlay // history reads the colour pair - so they share one barrier. dis_dispatch(d, cmd, d->pass_pack.pipeline, d->pack_set, w, h); + if (splat_gens > 0) dis_splat_pass(d, cmd, slot, splat_gens, 1); d->prior_valid = true; vkd.CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->pass_hist.pipeline); @@ -2749,6 +2761,11 @@ VkCommandBuffer vkr_dis_process_ex(VkrDis* d, VkCommandBuffer cmd, VkImage sourc dis_dispatch(d, cmd, d->pass_side.pipeline, d->side_sets[slot], w, h); dis_compute_barrier(cmd); DIS_PROF(cmd, "pack+hist+side"); + if (splat_gens > 0) { + dis_splat_pass(d, cmd, slot, splat_gens, 2); + dis_compute_barrier(cmd); + DIS_PROF(cmd, "splat"); + } } return cmd; } @@ -2758,41 +2775,32 @@ void vkr_dis_process(VkrDis* d, VkCommandBuffer cmd, VkImage source, uint32_t wi (void)vkr_dis_process_ex(d, cmd, source, width, height, generations, NULL, NULL); } -static void dis_render_into(VkrDis* d, VkCommandBuffer cmd, float t, int debug_mode, +// One pass of dis_splat.comp over every generated frame of the pair: 0 clears, 1 splats, 2 +// writes the winners. The caller places the barriers. +static void dis_splat_pass(VkrDis* d, VkCommandBuffer cmd, uint32_t slot, uint32_t generations, + int mode) { + const struct { int gens; int mode; } spc = {(int)generations, mode}; + const uint32_t fw = d->flow_out.extent.width; + const uint32_t fh = d->flow_out.extent.height; + vkd.CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->pass_splat.pipeline); + vkd.CmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->vr_pipeline_layout, 0, 1, + &d->splat_sets[slot], 0, NULL); + vkd.CmdPushConstants(cmd, d->vr_pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(spc), + &spc); + vkd.CmdDispatch(cmd, (fw + DIS_LOCAL_SIZE - 1) / DIS_LOCAL_SIZE, + (fh + DIS_LOCAL_SIZE - 1) / DIS_LOCAL_SIZE, 1); +} + +static void dis_render_into(VkrDis* d, VkCommandBuffer cmd, float t, int layer, int debug_mode, VkImage target_image, uint32_t width, uint32_t height, VkImage base_image) { const uint32_t w = d->content.width; const uint32_t h = d->content.height; - // The field splatted to t: clear, raise the keys, write the winners (dis_splat.comp). - { - const uint32_t fw = d->flow_out.extent.width; - const uint32_t fh = d->flow_out.extent.height; - vkd.CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->pass_splat.pipeline); - vkd.CmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->vr_pipeline_layout, 0, 1, - &d->splat_sets[d->active_slot], 0, NULL); - for (int mode = 0; mode < 3; mode++) { - const struct { float t; int mode; } spc = {t, mode}; - vkd.CmdPushConstants(cmd, d->vr_pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, - sizeof(spc), &spc); - vkd.CmdDispatch(cmd, (fw + DIS_LOCAL_SIZE - 1) / DIS_LOCAL_SIZE, - (fh + DIS_LOCAL_SIZE - 1) / DIS_LOCAL_SIZE, 1); - // The key is read-modify-written by the atomics, so the next pass waits for both. - VkMemoryBarrier mb; - memset(&mb, 0, sizeof(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; - vkd.CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, - VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 1, &mb, 0, NULL, 0, - NULL); - } - DIS_PROF(cmd, "splat"); - } - DisInterpPC ipc; ipc.t = t; ipc.debugMode = debug_mode; + ipc.layer = layer; vkd.CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->pass_interp.pipeline); vkd.CmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->pipeline_layout, 0, 1, @@ -2876,14 +2884,15 @@ void vkr_dis_generate_into(VkrDis* d, VkCommandBuffer cmd, uint32_t generation, if (d->last_generations == 0) return; const float t = (float)(generation + 1) / (float)(d->last_generations + 1); - dis_render_into(d, cmd, t, d->debug_flow ? 1 : 0, target_image, width, height, base_image); + dis_render_into(d, cmd, t, (int)generation, d->debug_flow ? 1 : 0, target_image, width, height, + base_image); } void vkr_dis_debug_into(VkrDis* d, VkCommandBuffer cmd, VkImage target_image, uint32_t width, uint32_t height) { if (!d || !d->built || d->unavailable || !d->debug_flow) return; if (d->frame_count < 2) return; - dis_render_into(d, cmd, 0.5f, 1, target_image, width, height, VK_NULL_HANDLE); + dis_render_into(d, cmd, 0.5f, 0, 1, target_image, width, height, VK_NULL_HANDLE); } void vkr_dis_forget_targets(VkrDis* d) { From ba12d1b1046364b6a621b2937a69c096dd74f0e9 Mon Sep 17 00:00:00 2001 From: qwertypower <1qwertypower1@gmail.com> Date: Mon, 12 Oct 2026 23:45:27 +0300 Subject: [PATCH 08/18] WFG: drop the Quality resolution scale --- app/src/main/res/values-b+es+419/strings.xml | 1 - app/src/main/res/values-da/strings.xml | 1 - app/src/main/res/values-de/strings.xml | 1 - app/src/main/res/values-es/strings.xml | 1 - app/src/main/res/values-fi/strings.xml | 1 - app/src/main/res/values-fr/strings.xml | 1 - app/src/main/res/values-hi/strings.xml | 1 - app/src/main/res/values-it/strings.xml | 1 - app/src/main/res/values-ja/strings.xml | 1 - app/src/main/res/values-ko/strings.xml | 1 - app/src/main/res/values-no/strings.xml | 1 - app/src/main/res/values-pl/strings.xml | 1 - app/src/main/res/values-pt-rBR/strings.xml | 1 - app/src/main/res/values-pt/strings.xml | 1 - app/src/main/res/values-ro/strings.xml | 1 - app/src/main/res/values-ru/strings.xml | 1 - app/src/main/res/values-sv/strings.xml | 1 - app/src/main/res/values-th/strings.xml | 1 - app/src/main/res/values-tr/strings.xml | 1 - app/src/main/res/values-uk/strings.xml | 1 - app/src/main/res/values-zh-rCN/strings.xml | 1 - app/src/main/res/values-zh-rTW/strings.xml | 1 - app/src/main/res/values/strings.xml | 1 - app/src/main/runtime/display/XServerDisplayActivity.java | 2 +- app/src/main/shared/framegen/FrameGenPreset.kt | 1 - docs/FRAME-GENERATION.md | 2 +- 26 files changed, 2 insertions(+), 26 deletions(-) diff --git a/app/src/main/res/values-b+es+419/strings.xml b/app/src/main/res/values-b+es+419/strings.xml index 96e7c29db..a069d808c 100644 --- a/app/src/main/res/values-b+es+419/strings.xml +++ b/app/src/main/res/values-b+es+419/strings.xml @@ -2736,7 +2736,6 @@ Ruta instalada: Escala de resolución Rápido Equilibrado - Calidad El movimiento se analiza con %1$d píxeles en el lado corto del fotograma. Menos cuesta menos; más sigue mejor los detalles pequeños o rápidos. FPS objetivo Frecuencia máx. diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 26c695871..e39c801f0 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -2749,7 +2749,6 @@ Installeret sti: Opløsningsskala Hurtig Balanceret - Kvalitet Bevægelse analyseres ved %1$d pixels på billedets korteste side. Lavere er billigere; højere følger små eller hurtige detaljer bedre. Mål-FPS Maks. opdateringshastighed diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index a751d268c..b22572d01 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -2740,7 +2740,6 @@ Installierter Pfad: Auflösungsskalierung Schnell Ausgewogen - Qualität Bewegung wird mit %1$d Pixeln an der kürzeren Bildseite analysiert. Niedriger ist günstiger, höher erfasst kleine oder schnelle Details besser. Ziel-FPS Max. Bildwiederholrate diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 037a9a7f3..d88ee4a5a 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -2740,7 +2740,6 @@ Ruta instalada: Escala de resolución Rápido Equilibrado - Calidad El movimiento se analiza con %1$d píxeles en el lado corto del fotograma. Menos cuesta menos; más sigue mejor los detalles pequeños o rápidos. FPS objetivo Frecuencia máx. diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 4645576f1..d824ed924 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -2736,7 +2736,6 @@ Asennuspolku: Resoluutioskaala Nopea Tasapainoinen - Laatu Liikettä analysoidaan %1$d pikselin tarkkuudella kuvan lyhyemmällä sivulla. Pienempi on kevyempi, suurempi seuraa pieniä tai nopeita yksityiskohtia paremmin. Tavoite-FPS Maks. virkistystaajuus diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 786855827..d127554e4 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -2740,7 +2740,6 @@ Chemin installé : Échelle de résolution Rapide Équilibré - Qualité Le mouvement est analysé à %1$d pixels sur le côté le plus court de l\'image. Plus bas coûte moins cher ; plus haut suit mieux les détails petits ou rapides. FPS cible Fréquence max. diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 5e82b2266..85bd0f4b2 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -2634,7 +2634,6 @@ रिज़ॉल्यूशन स्केल तेज़ संतुलित - गुणवत्ता फ़्रेम की छोटी भुजा पर %1$d पिक्सेल पर गति का विश्लेषण किया जाता है। कम मान हल्का होता है; अधिक मान छोटे या तेज़ विवरण को बेहतर ट्रैक करता है। लक्ष्य FPS अधिकतम रिफ्रेश दर diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index ab347dc3f..f7e2dabc2 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -2738,7 +2738,6 @@ Percorso installato: Scala risoluzione Veloce Bilanciato - Qualità Il movimento viene analizzato a %1$d pixel sul lato corto del fotogramma. Più basso costa meno; più alto segue meglio i dettagli piccoli o rapidi. FPS target Freq. max diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 574919d7a..1c01cf80e 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -2736,7 +2736,6 @@ 解像度スケール 高速 バランス - 高品質 フレームの短辺 %1$d ピクセルで動きを解析します。低いほど負荷が軽く、高いほど小さい動きや速い動きを追跡できます。 目標FPS 最大リフレッシュレート diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index f9e59deb0..cb5566c87 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -2739,7 +2739,6 @@ 해상도 배율 빠름 균형 - 품질 프레임의 짧은 변 %1$d픽셀에서 움직임을 분석합니다. 낮을수록 가볍고, 높을수록 작거나 빠른 디테일을 더 잘 추적합니다. 목표 FPS 최대 새로고침 빈도 diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index 15a36afbf..a8cf47777 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -2736,7 +2736,6 @@ Installert bane: Oppløsningsskala Rask Balansert - Kvalitet Bevegelse analyseres ved %1$d piksler på bildets korteste side. Lavere er billigere; høyere følger små eller raske detaljer bedre. Mål-FPS Maks. oppdateringsfrekvens diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 472ade6ff..9c529ab2c 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -2744,7 +2744,6 @@ Zainstalowana ścieżka: Skala rozdzielczości Szybko Zrównoważony - Jakość Ruch jest analizowany przy %1$d pikselach na krótszym boku klatki. Niższa wartość jest tańsza; wyższa lepiej śledzi drobne lub szybkie szczegóły. Docelowe FPS Maks. częstotliwość odświeżania diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 69a266dd5..ae011336a 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -2738,7 +2738,6 @@ Caminho instalado: Escala de resolução Rápido Equilibrado - Qualidade O movimento é analisado a %1$d pixels no lado menor do quadro. Menor é mais barato; maior acompanha melhor os detalhes pequenos ou rápidos. FPS alvo Frequência máx. diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 04d912615..fe61cdf32 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -2736,7 +2736,6 @@ Caminho instalado: Escala de resolução Rápido Equilibrado - Qualidade O movimento é analisado a %1$d píxeis no lado mais curto do fotograma. Menor é mais barato; maior acompanha melhor os detalhes pequenos ou rápidos. FPS alvo Frequência máx. diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 577a5aea1..b2b4e4bbe 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -2737,7 +2737,6 @@ Cale instalata: Scală rezoluție Rapid Echilibrat - Calitate Mișcarea este analizată la %1$d pixeli pe latura scurtă a cadrului. Mai mic costă mai puțin; mai mare urmărește mai bine detaliile mici sau rapide. FPS țintă Rată de reîmprospătare max. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 6055706c5..51e664c99 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -2644,7 +2644,6 @@ Масштаб разрешения Быстро Баланс - Качество Движение анализируется при %1$d пикселях по короткой стороне кадра. Меньше — дешевле, больше — лучше отслеживает мелкие и быстрые детали. Целевой FPS Макс. частота обновления diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 1b496aeb0..22a2f673d 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -2736,7 +2736,6 @@ Installerad sökväg: Upplösningsskala Snabb Balanserad - Kvalitet Rörelse analyseras vid %1$d pixlar på bildens kortaste sida. Lägre är billigare; högre följer små eller snabba detaljer bättre. Mål-FPS Max uppdateringsfrekvens diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 587aaeef9..032572ca7 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -2736,7 +2736,6 @@ สเกลความละเอียด เร็ว สมดุล - คุณภาพ วิเคราะห์การเคลื่อนไหวที่ %1$d พิกเซลบนด้านสั้นของเฟรม ค่าต่ำใช้ทรัพยากรน้อยกว่า ค่าสูงติดตามรายละเอียดเล็กหรือเร็วได้ดีกว่า FPS เป้าหมาย อัตรารีเฟรชสูงสุด diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index ae0f57eca..f608dbb03 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -2736,7 +2736,6 @@ Yüklü konum: Çözünürlük Ölçeği Hızlı Dengeli - Kalite Hareket, karenin kısa kenarında %1$d piksel çözünürlükte analiz edilir. Düşük değer daha ucuzdur; yüksek değer küçük veya hızlı ayrıntıları daha iyi izler. Hedef FPS Maks. yenileme hızı diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 419aee50b..24532a23f 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -2744,7 +2744,6 @@ Масштаб роздільної здатності Швидко Баланс - Якість Рух аналізується за %1$d пікселями по короткій стороні кадру. Менше — дешевше, більше — краще відстежує дрібні та швидкі деталі. Цільовий FPS Макс. частота оновлення diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index a9fc81230..7c521bee8 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -2738,7 +2738,6 @@ 分辨率缩放 快速 均衡 - 画质 以画面短边 %1$d 像素分析运动。数值越低开销越小,越高越能跟踪细小或快速的细节。 目标 FPS 最高刷新率 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index b0e1a5bc7..0c7ea24a6 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -2737,7 +2737,6 @@ 解析度縮放 快速 均衡 - 畫質 以畫面短邊 %1$d 像素分析動態。數值越低開銷越小,越高越能追蹤細小或快速的細節。 目標 FPS 最高更新率 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4e53b4273..9b5e7fa19 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -946,7 +946,6 @@ E.g. META for META key, \n Resolution Scale Fast Balance - Quality Motion is analysed at %1$d pixels on the shorter side of the frame. Lower is cheaper; higher tracks small or fast detail better. Target FPS Max refresh rate diff --git a/app/src/main/runtime/display/XServerDisplayActivity.java b/app/src/main/runtime/display/XServerDisplayActivity.java index 78c3e74ba..a491acf05 100644 --- a/app/src/main/runtime/display/XServerDisplayActivity.java +++ b/app/src/main/runtime/display/XServerDisplayActivity.java @@ -523,7 +523,7 @@ private boolean isAnyControllerConnected() { private Thread waylandRendererThread; /* The process behind the window the compositor is showing, from the compositor itself. */ private volatile int waylandGamePid; - private static final int[] DIS_FLOW_MIN_SIDES = {180, 252, 360}; + private static final int[] DIS_FLOW_MIN_SIDES = {180, 252}; private static final int DIS_FRAME_GEN_SCALE_DEFAULT = 180; private int disFrameGenScale = DIS_FRAME_GEN_SCALE_DEFAULT; diff --git a/app/src/main/shared/framegen/FrameGenPreset.kt b/app/src/main/shared/framegen/FrameGenPreset.kt index 286b37ee1..b83143212 100644 --- a/app/src/main/shared/framegen/FrameGenPreset.kt +++ b/app/src/main/shared/framegen/FrameGenPreset.kt @@ -6,7 +6,6 @@ import kotlin.math.abs enum class DisFlowPreset(val minSide: Int, val labelRes: Int) { FAST(180, R.string.session_drawer_dis_preset_fast), BALANCE(252, R.string.session_drawer_dis_preset_balance), - QUALITY(360, R.string.session_drawer_dis_preset_quality), ; companion object { diff --git a/docs/FRAME-GENERATION.md b/docs/FRAME-GENERATION.md index 59341b3f8..4abbc5907 100644 --- a/docs/FRAME-GENERATION.md +++ b/docs/FRAME-GENERATION.md @@ -41,7 +41,7 @@ runs. | Multiplier | LSFG | 2× / 3× / 4× — generated frames per rendered frame | | Adaptive Target | LSFG | Aim for a specific output rate (60/90/120/144/165) instead of a fixed multiplier | | Flow Scale | LSFG | 25–100%, resolution of the optical-flow pyramid; lower is cheaper and softer | -| Resolution Scale | WFG | Fast / Balance / Quality — 180 / 252 / 360 pixels on the frame's **shorter** side | +| Resolution Scale | WFG | Fast / Balance — 180 / 252 pixels on the frame's **shorter** side | | Target FPS | WFG | Max refresh rate, or a specific one (60/90/120/144/165) | | Show flow | WFG | Debug view: renders the estimated motion field instead of the frame | | FPS Limiter | Both | Caps the game's own frame rate, from 15 fps upward | From ea460899e5565ff3fe6dc2761e0252471c666e3a Mon Sep 17 00:00:00 2001 From: qwertypower <1qwertypower1@gmail.com> Date: Mon, 12 Oct 2026 23:56:41 +0300 Subject: [PATCH 09/18] WFG: drop the unused hardware motion hint and luma shader; full name in docs --- CREDITS.md | 4 +- app/src/main/cpp/wfg/CMakeLists.txt | 13 +- .../include/shaders/dis_inverse_search.comp | 25 - .../cpp/wfg/include/shaders/dis_luma.comp | 29 -- .../cpp/wfg/include/shaders/dis_me_luma.comp | 53 --- app/src/main/cpp/wfg/include/vkr_dis.h | 19 - app/src/main/cpp/wfg/src/dis_qcom_me.c | 373 --------------- app/src/main/cpp/wfg/src/dis_qcom_me.h | 30 -- app/src/main/cpp/wfg/src/vkr_dis.c | 433 +----------------- .../cpp/winlator/vk/framegen/fg_present.c | 33 +- app/src/main/cpp/winlator/vk/vk_renderer.c | 42 +- app/src/main/cpp/winlator/vk/vk_state.h | 1 - docs/FRAME-GENERATION.md | 10 +- 13 files changed, 22 insertions(+), 1043 deletions(-) delete mode 100644 app/src/main/cpp/wfg/include/shaders/dis_luma.comp delete mode 100644 app/src/main/cpp/wfg/include/shaders/dis_me_luma.comp delete mode 100644 app/src/main/cpp/wfg/src/dis_qcom_me.c delete mode 100644 app/src/main/cpp/wfg/src/dis_qcom_me.h diff --git a/CREDITS.md b/CREDITS.md index 6cd678d6b..93a28b092 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -57,7 +57,7 @@ corresponding source for every copyleft component is available from the projects [Eden Emulator Project](https://git.eden-emu.dev/eden-emu/eden) (GPL-3.0-or-later) — the Vulkan port of that chain that WinNative's frame generation is derived from. See [what came from Camille LaVey's Eden port](#frame-generation--what-came-from-camille-laveys-eden-port) -- **WFG frame generation** (DIS optical flow) by **qwertypower** +- **WFG** (WinNative/Winlator Frame Generator, GPL-3.0; DIS optical flow) by **qwertypower** ([DEVAR Entertainment LLC](https://devar.ai/)) (GPL-3.0) — a complete open-source implementation of a Dense Inverse Search frame generator, the second frame generation engine in WinNative and the one that needs no shaders from anywhere else. @@ -119,7 +119,7 @@ of a user's own Lossless Scaling install (`lsfg_dll.*`), translating them when o available (`lsfg_dxbc.*`), the JNI surface (`lsfg_jni.*`), driver probing (`lsfg_probe.*`), and wiring the chain into WinNative's compositor and swapchain (`vkr_lsfg.*`). -## WFG frame generation — the fully open-source engine +## WFG (WinNative/Winlator Frame Generator) — the fully open-source engine WinNative's second frame generator is a complete open-source implementation of **Dense Inverse Search** optical flow, contributed by **qwertypower** (DEVAR Entertainment LLC) under GPL-3.0. diff --git a/app/src/main/cpp/wfg/CMakeLists.txt b/app/src/main/cpp/wfg/CMakeLists.txt index d59e59c8c..6e22535b0 100644 --- a/app/src/main/cpp/wfg/CMakeLists.txt +++ b/app/src/main/cpp/wfg/CMakeLists.txt @@ -1,7 +1,7 @@ -# WFG frame generator: Dense Inverse Search (DIS) optical flow and the frame interpolator -# built on it, as one static library. libwinlator (X11 compositor, standalone presenter) and -# libwnwayland (Wayland compositor) both link it; each keeps its own VkDispatch, which the objects -# here resolve against at link time. +# WFG (WinNative/Winlator Frame Generator, GPL-3.0): Dense Inverse Search (DIS) optical flow and +# the frame interpolator built on it, as one static library. libwinlator (X11 compositor, +# standalone presenter) and libwnwayland (Wayland compositor) both link it; each keeps its own +# VkDispatch, which the objects here resolve against at link time. # # Layout: # include/vkr_dis.h public API @@ -33,7 +33,6 @@ set(DIS_SHADERS dis_vr_coef dis_vr_sor dis_vr_add - dis_me_luma dis_fb_poly dis_fb_hsum dis_fb_vsolve @@ -64,7 +63,6 @@ add_custom_target(wfg_shaders DEPENDS ${DIS_SHADER_HEADERS}) add_library(wfg STATIC src/vkr_dis.c - src/dis_qcom_me.c ) add_dependencies(wfg wfg_shaders) @@ -79,5 +77,4 @@ target_include_directories(wfg PRIVATE ${CMAKE_CURRENT_BINARY_DIR} ) -# EGL/GLES are opened with dlopen by dis_qcom_me.c, so nothing links them here. -target_link_libraries(wfg PRIVATE log dl) +target_link_libraries(wfg PRIVATE log) diff --git a/app/src/main/cpp/wfg/include/shaders/dis_inverse_search.comp b/app/src/main/cpp/wfg/include/shaders/dis_inverse_search.comp index fc45cd83a..6bad0c56f 100644 --- a/app/src/main/cpp/wfg/include/shaders/dis_inverse_search.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_inverse_search.comp @@ -76,10 +76,6 @@ layout(set = 0, binding = 0) uniform sampler2D lastLumaMap; layout(set = 0, binding = 1) uniform sampler2D nextLumaMap; layout(set = 0, binding = 2) uniform sampler2D lastGradientMap; layout(set = 0, binding = 3) uniform sampler2D flowMap; -// Motion hint from a hardware estimator (GL_QCOM_motion_estimation), one vector per block, in -// normalised uv units; read only on the level named by hintLevel. A component beyond HINT_INVALID -// marks a block the estimator had nothing for. -layout(set = 0, binding = 4) uniform sampler2D hintFlowMap; layout(set = 0, binding = 5, rgba32f) uniform image2D sparseFlowMap; // The previous pair's final flow, in normalised uv units, indexed by position in its first frame; // read on the levels whose bit is set in priorLevels. @@ -91,13 +87,10 @@ layout(set = 0, binding = 7) uniform sampler2D fbFlowMap; layout(push_constant) uniform PC { int level; int coarseLevel; - int hintLevel; int priorLevels; int fbLevels; } pc; -#define HINT_INVALID 1.0e6 - float uluminance(vec3 c) { return (0.299 * c.x + 0.587 * c.y + 0.114 * c.z) * 255.0; } @@ -221,24 +214,6 @@ void main() { // search minimises; the search starts from the best. A wrong candidate simply loses here. float startSSD = -1.0; - // The hardware estimator's vector. The coarse level still covers what the estimator cannot: - // its search range is a few dozen pixels, and past that its vector is garbage. - if (pc.level == pc.hintLevel) { - vec2 hintSize = vec2(textureSize(hintFlowMap, 0)); - ivec2 hp = clamp(ivec2((vec2(pix) + 4.0) * hintSize / vec2(denseSize)), - ivec2(0), ivec2(hintSize) - 1); - vec2 hint = texelFetch(hintFlowMap, hp, 0).xy; - if (all(lessThan(abs(hint), vec2(HINT_INVALID * 0.5)))) { - hint *= vec2(denseSize); - startSSD = patchSSD(flow, pix, lp, denseSize); - float hintSSD = patchSSD(hint, pix, lp, denseSize); - if (hintSSD < startSSD) { - startSSD = hintSSD; - flow = hint; - } - } - } - // The previous pair's vector: motion goes on. It is what follows an object too small or too // fast for the coarse levels to see - there it is lost in the background - and a periodic // texture that the refinement from the coarse vector would lock onto a period off. The field diff --git a/app/src/main/cpp/wfg/include/shaders/dis_luma.comp b/app/src/main/cpp/wfg/include/shaders/dis_luma.comp deleted file mode 100644 index d88976f32..000000000 --- a/app/src/main/cpp/wfg/include/shaders/dis_luma.comp +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) -// SPDX-License-Identifier: GPL-3.0-or-later -// -// WFG frame generation: a Vulkan compute realisation of Dense Inverse Search -// optical flow. The algorithm and its reference implementation come from -// OpenCV's DISOpticalFlow, which adopted Till Kroeger's original OF_DIS. -// See CREDITS.md for the full attribution. - -#version 450 - -precision highp float; -precision highp int; - -layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; - -layout(set = 0, binding = 0) uniform sampler2D colorMap; -layout(set = 0, binding = 5) writeonly uniform image2D lumaMap; - -float uluminance(vec3 c) { - return (0.299 * c.x + 0.587 * c.y + 0.114 * c.z) * 255.0; -} - -void main() { - ivec2 pix = ivec2(gl_GlobalInvocationID.xy); - ivec2 size = textureSize(colorMap, 0); - if (pix.x >= size.x || pix.y >= size.y) return; - - imageStore(lumaMap, pix, vec4(uluminance(texelFetch(colorMap, pix, 0).xyz), 0.0, 0.0, 1.0)); -} diff --git a/app/src/main/cpp/wfg/include/shaders/dis_me_luma.comp b/app/src/main/cpp/wfg/include/shaders/dis_me_luma.comp deleted file mode 100644 index 207620765..000000000 --- a/app/src/main/cpp/wfg/include/shaders/dis_me_luma.comp +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) -// SPDX-License-Identifier: GPL-3.0-or-later -// -// WFG frame generation: a Vulkan compute realisation of Dense Inverse Search -// optical flow. The algorithm and its reference implementation come from -// OpenCV's DISOpticalFlow, which adopted Till Kroeger's original OF_DIS. -// See CREDITS.md for the full attribution. - -#version 450 - -precision highp float; -precision highp int; - -// Luminance of the newest real frame at the hardware motion estimator's input size, packed four -// pixels to a word into a host-visible buffer: GL_QCOM_motion_estimation takes an R8 texture, and -// the frame reaches GL as a plain upload. Each output pixel box-filters its footprint with four -// bilinear taps, which is exact for the usual 2:1 reduction and keeps larger ones from aliasing. - -layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; - -layout(set = 0, binding = 0) uniform sampler2D colorTex; -layout(set = 0, binding = 1, std430) writeonly buffer LumaOut { - uint words[]; -}; - -layout(push_constant) uniform PC { - int width; // multiple of 4 - int height; -} pc; - -float lumaAt(vec2 uv, vec2 q) { - vec3 c = textureLod(colorTex, uv + vec2(-q.x, -q.y), 0.0).rgb - + textureLod(colorTex, uv + vec2( q.x, -q.y), 0.0).rgb - + textureLod(colorTex, uv + vec2(-q.x, q.y), 0.0).rgb - + textureLod(colorTex, uv + vec2( q.x, q.y), 0.0).rgb; - return dot(c * 0.25, vec3(0.299, 0.587, 0.114)); -} - -void main() { - ivec2 g = ivec2(gl_GlobalInvocationID.xy); - int wordsPerRow = pc.width / 4; - if (g.x >= wordsPerRow || g.y >= pc.height) return; - - vec2 texel = 1.0 / vec2(pc.width, pc.height); - vec2 q = 0.25 * texel; - uint packed = 0u; - for (int i = 0; i < 4; i++) { - vec2 uv = (vec2(g.x * 4 + i, g.y) + 0.5) * texel; - uint v = uint(clamp(lumaAt(uv, q), 0.0, 1.0) * 255.0 + 0.5); - packed |= v << (8u * uint(i)); - } - words[g.y * wordsPerRow + g.x] = packed; -} diff --git a/app/src/main/cpp/wfg/include/vkr_dis.h b/app/src/main/cpp/wfg/include/vkr_dis.h index bb8280a6f..fe8ff5991 100644 --- a/app/src/main/cpp/wfg/include/vkr_dis.h +++ b/app/src/main/cpp/wfg/include/vkr_dis.h @@ -47,25 +47,6 @@ uint32_t vkr_dis_plan(VkrDis* dis, uint32_t capacity, uint64_t source_frames); void vkr_dis_process(VkrDis* dis, VkCommandBuffer cmd, VkImage source, uint32_t width, uint32_t height, uint32_t generations); -// Submits `cmd` (ended by the callee, no semaphores), waits for it on the CPU, and returns a -// command buffer in the recording state for the rest of the frame - `cmd` itself reset and begun -// again is fine. VK_NULL_HANDLE on failure. -typedef VkCommandBuffer (*VkrDisFlushFn)(void* user, VkCommandBuffer cmd); - -// As vkr_dis_process, and when the GLES driver offers GL_QCOM_motion_estimation, seeds the flow -// with a hardware estimate. That needs this frame's pixels mid-frame, so DIS calls `flush` once -// and records the rest into the buffer it returns; the caller continues with - and finally -// submits - the returned buffer. With flush NULL, or without the extension, this is exactly -// vkr_dis_process and returns `cmd`. -VkCommandBuffer vkr_dis_process_ex(VkrDis* dis, VkCommandBuffer cmd, VkImage source, - uint32_t width, uint32_t height, uint32_t generations, - VkrDisFlushFn flush, void* flush_user); - -// Hardware motion hint on or off (default off, or debug.winnative.dis.hwme=1); takes effect at -// the next resource build. -void vkr_dis_set_hw_motion(VkrDis* dis, bool enabled); -bool vkr_dis_hw_motion_active(const VkrDis* dis); - void vkr_dis_generate_into(VkrDis* dis, VkCommandBuffer cmd, uint32_t generation, uint32_t target_index, VkImage target_image, VkImageView target_view, uint32_t width, uint32_t height, diff --git a/app/src/main/cpp/wfg/src/dis_qcom_me.c b/app/src/main/cpp/wfg/src/dis_qcom_me.c deleted file mode 100644 index c861bf8d1..000000000 --- a/app/src/main/cpp/wfg/src/dis_qcom_me.c +++ /dev/null @@ -1,373 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) -// SPDX-License-Identifier: GPL-3.0-or-later -// -// Hardware motion estimation through GL_QCOM_motion_estimation. See dis_qcom_me.h. -// -// The estimator lives in the platform GLES driver, while DIS runs on whichever Vulkan driver the -// compositor loaded (often Turnip), so nothing is shared between the two APIs: the luminance goes -// in with a texture upload and the field comes back with a read, both a few hundred kilobytes at -// most. EGL and GLES are opened with dlopen so the libraries linking DIS take no hard dependency -// on them, and each instance owns a surfaceless context that is made current only for the -// duration of a call, restoring whatever the calling thread had current before. - -#include "dis_qcom_me.h" - -#ifdef __ANDROID__ - -#include -#include -#include - -#include -#include -#include -#include -#include - -#define ME_LOGI(...) __android_log_print(ANDROID_LOG_INFO, "WFG", __VA_ARGS__) -#define ME_LOGW(...) __android_log_print(ANDROID_LOG_WARN, "WFG", __VA_ARGS__) - -#define GL_MOTION_ESTIMATION_SEARCH_BLOCK_X_QCOM 0x8C90 -#define GL_MOTION_ESTIMATION_SEARCH_BLOCK_Y_QCOM 0x8C91 - -// The NDK's EGL headers only declare the core entry points as prototypes, so their pointer types -// are spelled out here for the dlsym'd table below. -typedef EGLDisplay (*MeGetDisplay)(EGLNativeDisplayType); -typedef EGLBoolean (*MeInitialize)(EGLDisplay, EGLint*, EGLint*); -typedef EGLBoolean (*MeChooseConfig)(EGLDisplay, const EGLint*, EGLConfig*, EGLint, EGLint*); -typedef EGLBoolean (*MeBindAPI)(EGLenum); -typedef EGLContext (*MeCreateContext)(EGLDisplay, EGLConfig, EGLContext, const EGLint*); -typedef EGLBoolean (*MeDestroyContext)(EGLDisplay, EGLContext); -typedef EGLSurface (*MeCreatePbufferSurface)(EGLDisplay, EGLConfig, const EGLint*); -typedef EGLBoolean (*MeDestroySurface)(EGLDisplay, EGLSurface); -typedef EGLBoolean (*MeMakeCurrent)(EGLDisplay, EGLSurface, EGLSurface, EGLContext); -typedef EGLContext (*MeGetCurrentContext)(void); -typedef EGLDisplay (*MeGetCurrentDisplay)(void); -typedef EGLSurface (*MeGetCurrentSurface)(EGLint); -typedef const char* (*MeQueryString)(EGLDisplay, EGLint); -typedef void (*(*MeGetProcAddress)(const char*))(void); -typedef struct { - bool loaded; - bool ok; - MeGetDisplay GetDisplay; - MeInitialize Initialize; - MeChooseConfig ChooseConfig; - MeBindAPI BindAPI; - MeCreateContext CreateContext; - MeDestroyContext DestroyContext; - MeCreatePbufferSurface CreatePbufferSurface; - MeDestroySurface DestroySurface; - MeMakeCurrent MakeCurrent; - MeGetCurrentContext GetCurrentContext; - MeGetCurrentDisplay GetCurrentDisplay; - MeGetCurrentSurface GetCurrentSurface; - MeQueryString QueryString; - MeGetProcAddress GetProcAddress; - - const GLubyte* (*GetString)(GLenum); - void (*GetIntegerv)(GLenum, GLint*); - GLenum (*GetError)(void); - void (*GenTextures)(GLsizei, GLuint*); - void (*DeleteTextures)(GLsizei, const GLuint*); - void (*BindTexture)(GLenum, GLuint); - void (*TexStorage2D)(GLenum, GLsizei, GLenum, GLsizei, GLsizei); - void (*TexSubImage2D)(GLenum, GLint, GLint, GLint, GLsizei, GLsizei, GLenum, GLenum, const void*); - void (*TexParameteri)(GLenum, GLenum, GLint); - void (*PixelStorei)(GLenum, GLint); - void (*GenFramebuffers)(GLsizei, GLuint*); - void (*DeleteFramebuffers)(GLsizei, const GLuint*); - void (*BindFramebuffer)(GLenum, GLuint); - void (*FramebufferTexture2D)(GLenum, GLenum, GLenum, GLuint, GLint); - GLenum (*CheckFramebufferStatus)(GLenum); - void (*ReadPixels)(GLint, GLint, GLsizei, GLsizei, GLenum, GLenum, void*); - void (*EstimateMotion)(GLuint, GLuint, GLuint); -} MeApi; - -static MeApi g_api; -static pthread_mutex_t g_api_lock = PTHREAD_MUTEX_INITIALIZER; -static int g_probe = -1; // -1 not probed, 0 unsupported, 1 supported -static uint32_t g_block_x, g_block_y; - -struct DisQcomMe { - EGLDisplay display; - EGLContext context; - EGLSurface surface; // EGL_NO_SURFACE when surfaceless - uint32_t width, height; - uint32_t field_w, field_h; - GLuint luma[2]; - GLuint field; - GLuint fbo; - uint32_t newest; // index of the texture holding the newest frame - bool have_prev; -}; - -static bool me_load_api(void) { - if (g_api.loaded) return g_api.ok; - g_api.loaded = true; - void* egl = dlopen("libEGL.so", RTLD_NOW | RTLD_LOCAL); - void* gles = dlopen("libGLESv3.so", RTLD_NOW | RTLD_LOCAL); - if (!egl || !gles) return false; -#define EGLFN(field, name) g_api.field = (void*)dlsym(egl, name); if (!g_api.field) return false -#define GLFN(field, name) g_api.field = (void*)dlsym(gles, name); if (!g_api.field) return false - EGLFN(GetDisplay, "eglGetDisplay"); - EGLFN(Initialize, "eglInitialize"); - EGLFN(ChooseConfig, "eglChooseConfig"); - EGLFN(BindAPI, "eglBindAPI"); - EGLFN(CreateContext, "eglCreateContext"); - EGLFN(DestroyContext, "eglDestroyContext"); - EGLFN(CreatePbufferSurface, "eglCreatePbufferSurface"); - EGLFN(DestroySurface, "eglDestroySurface"); - EGLFN(MakeCurrent, "eglMakeCurrent"); - EGLFN(GetCurrentContext, "eglGetCurrentContext"); - EGLFN(GetCurrentDisplay, "eglGetCurrentDisplay"); - EGLFN(GetCurrentSurface, "eglGetCurrentSurface"); - EGLFN(QueryString, "eglQueryString"); - EGLFN(GetProcAddress, "eglGetProcAddress"); - GLFN(GetString, "glGetString"); - GLFN(GetIntegerv, "glGetIntegerv"); - GLFN(GetError, "glGetError"); - GLFN(GenTextures, "glGenTextures"); - GLFN(DeleteTextures, "glDeleteTextures"); - GLFN(BindTexture, "glBindTexture"); - GLFN(TexStorage2D, "glTexStorage2D"); - GLFN(TexSubImage2D, "glTexSubImage2D"); - GLFN(TexParameteri, "glTexParameteri"); - GLFN(PixelStorei, "glPixelStorei"); - GLFN(GenFramebuffers, "glGenFramebuffers"); - GLFN(DeleteFramebuffers, "glDeleteFramebuffers"); - GLFN(BindFramebuffer, "glBindFramebuffer"); - GLFN(FramebufferTexture2D, "glFramebufferTexture2D"); - GLFN(CheckFramebufferStatus, "glCheckFramebufferStatus"); - GLFN(ReadPixels, "glReadPixels"); -#undef EGLFN -#undef GLFN - g_api.EstimateMotion = (void*)g_api.GetProcAddress("glTexEstimateMotionQCOM"); - g_api.ok = g_api.EstimateMotion != NULL; - return g_api.ok; -} - -typedef struct { - EGLDisplay display; - EGLContext context; - EGLSurface draw, read; -} MeSaved; - -static void me_save(MeSaved* s) { - s->display = g_api.GetCurrentDisplay(); - s->context = g_api.GetCurrentContext(); - s->draw = g_api.GetCurrentSurface(EGL_DRAW); - s->read = g_api.GetCurrentSurface(EGL_READ); -} - -static void me_restore(const MeSaved* s, EGLDisplay own) { - if (s->context != EGL_NO_CONTEXT && s->display != EGL_NO_DISPLAY) { - g_api.MakeCurrent(s->display, s->draw, s->read, s->context); - } else { - g_api.MakeCurrent(own, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); - } -} - -// Display, context and (only without surfaceless support) a 1x1 pbuffer. -static bool me_context(EGLDisplay* out_dpy, EGLContext* out_ctx, EGLSurface* out_surf) { - EGLDisplay dpy = g_api.GetDisplay(EGL_DEFAULT_DISPLAY); - if (dpy == EGL_NO_DISPLAY || !g_api.Initialize(dpy, NULL, NULL)) return false; - const char* ext = g_api.QueryString(dpy, EGL_EXTENSIONS); - const bool surfaceless = ext && strstr(ext, "EGL_KHR_surfaceless_context") != NULL; - const EGLint cfg_attr[] = {EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT_KHR, - EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_NONE}; - EGLConfig cfg; - EGLint n = 0; - if (!g_api.ChooseConfig(dpy, cfg_attr, &cfg, 1, &n) || n < 1) return false; - g_api.BindAPI(EGL_OPENGL_ES_API); - const EGLint ctx_attr[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE}; - EGLContext ctx = g_api.CreateContext(dpy, cfg, EGL_NO_CONTEXT, ctx_attr); - if (ctx == EGL_NO_CONTEXT) return false; - EGLSurface surf = EGL_NO_SURFACE; - if (!surfaceless) { - const EGLint pb_attr[] = {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE}; - surf = g_api.CreatePbufferSurface(dpy, cfg, pb_attr); - if (surf == EGL_NO_SURFACE) { - g_api.DestroyContext(dpy, ctx); - return false; - } - } - *out_dpy = dpy; - *out_ctx = ctx; - *out_surf = surf; - return true; -} - -bool dis_qcom_me_supported(uint32_t* block_x, uint32_t* block_y) { - pthread_mutex_lock(&g_api_lock); - if (g_probe < 0) { - g_probe = 0; - EGLDisplay dpy; - EGLContext ctx; - EGLSurface surf; - if (me_load_api() && me_context(&dpy, &ctx, &surf)) { - MeSaved saved; - me_save(&saved); - if (g_api.MakeCurrent(dpy, surf, surf, ctx)) { - const char* ext = (const char*)g_api.GetString(GL_EXTENSIONS); - GLint bx = 0, by = 0; - if (ext && strstr(ext, "GL_QCOM_motion_estimation")) { - g_api.GetIntegerv(GL_MOTION_ESTIMATION_SEARCH_BLOCK_X_QCOM, &bx); - g_api.GetIntegerv(GL_MOTION_ESTIMATION_SEARCH_BLOCK_Y_QCOM, &by); - } - if (bx > 0 && by > 0) { - g_block_x = (uint32_t)bx; - g_block_y = (uint32_t)by; - g_probe = 1; - } - ME_LOGI("GL_QCOM_motion_estimation: %s (block %dx%d, %s)", - g_probe ? "available" : "not available", bx, by, - (const char*)g_api.GetString(GL_RENDERER)); - } - me_restore(&saved, dpy); - if (surf != EGL_NO_SURFACE) g_api.DestroySurface(dpy, surf); - g_api.DestroyContext(dpy, ctx); - } - } - const bool ok = g_probe == 1; - if (ok) { - if (block_x) *block_x = g_block_x; - if (block_y) *block_y = g_block_y; - } - pthread_mutex_unlock(&g_api_lock); - return ok; -} - -static GLuint me_texture(GLenum format, uint32_t w, uint32_t h) { - GLuint t = 0; - g_api.GenTextures(1, &t); - g_api.BindTexture(GL_TEXTURE_2D, t); - g_api.TexStorage2D(GL_TEXTURE_2D, 1, format, (GLsizei)w, (GLsizei)h); - g_api.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - g_api.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - return t; -} - -DisQcomMe* dis_qcom_me_create(uint32_t width, uint32_t height) { - uint32_t bx = 0, by = 0; - if (!dis_qcom_me_supported(&bx, &by)) return NULL; - if (width == 0 || height == 0 || width % bx || height % by) return NULL; - - DisQcomMe* me = (DisQcomMe*)calloc(1, sizeof(DisQcomMe)); - if (!me) return NULL; - me->width = width; - me->height = height; - me->field_w = width / bx; - me->field_h = height / by; - if (!me_context(&me->display, &me->context, &me->surface)) { - free(me); - return NULL; - } - - MeSaved saved; - me_save(&saved); - bool ok = g_api.MakeCurrent(me->display, me->surface, me->surface, me->context); - if (ok) { - me->luma[0] = me_texture(GL_R8, width, height); - me->luma[1] = me_texture(GL_R8, width, height); - me->field = me_texture(GL_RGBA16F, me->field_w, me->field_h); - g_api.GenFramebuffers(1, &me->fbo); - g_api.BindFramebuffer(GL_FRAMEBUFFER, me->fbo); - g_api.FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, me->field, 0); - ok = g_api.CheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE && - g_api.GetError() == GL_NO_ERROR; - } - me_restore(&saved, me->display); - if (!ok) { - ME_LOGW("GL_QCOM_motion_estimation: could not set up %ux%u; using DIS alone", width, height); - dis_qcom_me_destroy(me); - return NULL; - } - ME_LOGI("GL_QCOM_motion_estimation: %ux%u luminance -> %ux%u field", width, height, - me->field_w, me->field_h); - return me; -} - -void dis_qcom_me_destroy(DisQcomMe* me) { - if (!me) return; - if (me->context != EGL_NO_CONTEXT) { - MeSaved saved; - me_save(&saved); - if (g_api.MakeCurrent(me->display, me->surface, me->surface, me->context)) { - if (me->fbo) g_api.DeleteFramebuffers(1, &me->fbo); - GLuint tex[3] = {me->luma[0], me->luma[1], me->field}; - g_api.DeleteTextures(3, tex); - } - me_restore(&saved, me->display); - if (me->surface != EGL_NO_SURFACE) g_api.DestroySurface(me->display, me->surface); - g_api.DestroyContext(me->display, me->context); - } - free(me); -} - -void dis_qcom_me_invalidate(DisQcomMe* me) { - if (me) me->have_prev = false; -} - -bool dis_qcom_me_push(DisQcomMe* me, const uint8_t* luma, float* out_xy) { - if (!me || !luma) return false; - MeSaved saved; - me_save(&saved); - if (!g_api.MakeCurrent(me->display, me->surface, me->surface, me->context)) { - me_restore(&saved, me->display); - return false; - } - - const uint32_t cur = me->have_prev ? 1u - me->newest : me->newest; - g_api.BindTexture(GL_TEXTURE_2D, me->luma[cur]); - g_api.PixelStorei(GL_UNPACK_ALIGNMENT, 1); - g_api.TexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, (GLsizei)me->width, (GLsizei)me->height, GL_RED, - GL_UNSIGNED_BYTE, luma); - - bool estimated = false; - if (me->have_prev && out_xy) { - g_api.EstimateMotion(me->luma[me->newest], me->luma[cur], me->field); - const uint32_t n = me->field_w * me->field_h; - float* rgba = (float*)malloc((size_t)n * 4 * sizeof(float)); - if (rgba) { - g_api.BindFramebuffer(GL_FRAMEBUFFER, me->fbo); - g_api.ReadPixels(0, 0, (GLsizei)me->field_w, (GLsizei)me->field_h, GL_RGBA, GL_FLOAT, - rgba); - if (g_api.GetError() == GL_NO_ERROR) { - for (uint32_t i = 0; i < n; i++) { - out_xy[i * 2] = rgba[i * 4]; - out_xy[i * 2 + 1] = rgba[i * 4 + 1]; - } - estimated = true; - } - free(rgba); - } - } - me->newest = cur; - me->have_prev = true; - - me_restore(&saved, me->display); - return estimated; -} - -#else // !__ANDROID__: no GLES driver to ask; DIS runs alone. - -bool dis_qcom_me_supported(uint32_t* block_x, uint32_t* block_y) { - (void)block_x; - (void)block_y; - return false; -} -DisQcomMe* dis_qcom_me_create(uint32_t width, uint32_t height) { - (void)width; - (void)height; - return NULL; -} -void dis_qcom_me_destroy(DisQcomMe* me) { (void)me; } -bool dis_qcom_me_push(DisQcomMe* me, const uint8_t* luma, float* out_xy) { - (void)me; - (void)luma; - (void)out_xy; - return false; -} -void dis_qcom_me_invalidate(DisQcomMe* me) { (void)me; } - -#endif diff --git a/app/src/main/cpp/wfg/src/dis_qcom_me.h b/app/src/main/cpp/wfg/src/dis_qcom_me.h deleted file mode 100644 index 3975cf596..000000000 --- a/app/src/main/cpp/wfg/src/dis_qcom_me.h +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) -// SPDX-License-Identifier: GPL-3.0-or-later -// -// Hardware motion estimation through GL_QCOM_motion_estimation (Adreno), used by DIS as a -// second starting point for its block search. Private to the DIS module. - -#pragma once - -#include -#include -#include - -typedef struct DisQcomMe DisQcomMe; - -// Whether the platform's GLES driver exposes the extension, and its search block size. -// Probed once per process; cheap after the first call. -bool dis_qcom_me_supported(uint32_t* block_x, uint32_t* block_y); - -// Input frames are width x height R8 luminance, both multiples of the block size; the field is -// (width / block_x) x (height / block_y) vectors. NULL when unsupported or on failure. -DisQcomMe* dis_qcom_me_create(uint32_t width, uint32_t height); -void dis_qcom_me_destroy(DisQcomMe* me); - -// Hands the newest frame over. When an earlier frame is held, estimates the motion from it to -// this one into out_xy (field_w * field_h pairs, pixels of the input size, previous -> newest) -// and returns true. Either way the newest frame becomes the reference for the next call. -bool dis_qcom_me_push(DisQcomMe* me, const uint8_t* luma, float* out_xy); - -// Drops the held frame, so the next push only primes the history. -void dis_qcom_me_invalidate(DisQcomMe* me); diff --git a/app/src/main/cpp/wfg/src/vkr_dis.c b/app/src/main/cpp/wfg/src/vkr_dis.c index 1408e96e4..3d881dea8 100644 --- a/app/src/main/cpp/wfg/src/vkr_dis.c +++ b/app/src/main/cpp/wfg/src/vkr_dis.c @@ -8,7 +8,6 @@ #include "vkr_dis.h" -#include "dis_qcom_me.h" #include "vk_dispatch.h" #include "shaders/dis_luma_r16_comp.spv.h" #include "shaders/dis_luma_r32_comp.spv.h" @@ -21,7 +20,6 @@ #include "shaders/dis_side_comp.spv.h" #include "shaders/dis_flow_pack_comp.spv.h" #include "shaders/dis_splat_comp.spv.h" -#include "shaders/dis_me_luma_comp.spv.h" #include "shaders/dis_vr_prep_comp.spv.h" #include "shaders/dis_vr_d1_comp.spv.h" #include "shaders/dis_vr_d2_comp.spv.h" @@ -87,12 +85,6 @@ #define DIS_SLOTS 3u -// How the hardware motion field enters the flow: as a second starting candidate for the search on -// its level (0), or as that level's result outright, skipping the search above it (1). -#ifndef DIS_ME_PRIMARY -#define DIS_ME_PRIMARY 0 -#endif - #define DIS_PROP_STEPS_MAX 4u #define DIS_SRC_SMOOTHING 0.08f @@ -200,7 +192,6 @@ struct VkrDis { DisImage flow_out; DisImage splat_key; DisImage splat_field; - DisImage me_field; // Farneback flow (DIS_ENGINE_FARNEBACK and _HYBRID): each frame's polynomial expansion, // one per slot with a mip per level, and the horizontally summed equations of a level. @@ -232,7 +223,6 @@ struct VkrDis { VkImageView view_flow_out; VkImageView view_splat_key; VkImageView view_splat_field; - VkImageView view_me_field; VkImageView view_fb_b[DIS_SLOTS][DIS_MAX_LEVELS]; VkImageView view_fb_c[DIS_SLOTS][DIS_MAX_LEVELS]; VkImageView view_fb_sg[DIS_MAX_LEVELS]; @@ -289,7 +279,6 @@ struct VkrDis { DisPass pass_side; DisPass pass_pack; DisPass pass_splat; - DisPass pass_me_luma; DisPass pass_vr_prep; DisPass pass_vr_d1; DisPass pass_vr_d2; @@ -341,31 +330,6 @@ struct VkrDis { bool fb_hybrid_l1_start; // DIS_ENGINE_HYBRID on two DIS levels: Farneback's level 1 as a start bool fb_hybrid_vr_upper; // DIS_ENGINE_HYBRID: refine the DIS levels above 0 as well float fb_ig[4]; - - // Hardware motion hint (GL_QCOM_motion_estimation). me is NULL whenever the hint is off: - // disabled, unsupported, or the pyramid too shallow for the level it seeds. - bool hw_motion; - DisQcomMe* me; - uint32_t me_level; - uint32_t me_w, me_h; - uint32_t me_field_w, me_field_h; - VkBuffer me_luma_buf; - VkDeviceMemory me_luma_mem; - void* me_luma_map; - bool me_luma_coherent; - VkBuffer me_field_buf; - VkDeviceMemory me_field_mem; - void* me_field_map; - bool me_field_coherent; - float* me_xy; - int hint_level; - bool me_primary_frame; - VkDescriptorSetLayout me_set_layout; - VkPipelineLayout me_pipeline_layout; - VkDescriptorPool me_pool; - VkDescriptorSet me_sets[DIS_SLOTS]; - uint64_t me_hinted; - uint64_t me_pairs; }; typedef struct { @@ -377,7 +341,6 @@ typedef struct { typedef struct { int level; int coarseLevel; - int hintLevel; int priorLevels; int fbLevels; } DisInversePC; @@ -424,7 +387,6 @@ typedef struct { int parity; } DisVrSorPC; -static void dis_destroy_me(VkrDis* d); static uint64_t dis_now_ns(void) { struct timespec ts; @@ -587,7 +549,6 @@ static uint32_t dis_collect_images(VkrDis* d, DisImage** out, uint32_t cap) { DIS_PUSH(&d->flow_out); DIS_PUSH(&d->splat_key); DIS_PUSH(&d->splat_field); - DIS_PUSH(&d->me_field); for (uint32_t s = 0; s < DIS_SLOTS; s++) { DIS_PUSH(&d->fb_b[s]); DIS_PUSH(&d->fb_c[s]); @@ -902,60 +863,12 @@ static bool dis_create_pipelines(VkrDis* d) { d->pass_pack.pipeline = dis_create_compute_pipeline(d, dis_flow_pack_comp, dis_flow_pack_comp_size); d->pass_splat.pipeline = dis_create_compute_pipeline_with_layout(d, dis_splat_comp, dis_splat_comp_size, d->vr_pipeline_layout, NULL); - // The hardware-motion luminance pass writes a host-visible buffer, which neither shared - // layout has, so it gets a small layout and pool of its own: the colour frame and the buffer. - VkDescriptorSetLayoutBinding me_bindings[2]; - memset(me_bindings, 0, sizeof(me_bindings)); - me_bindings[0].binding = 0; - me_bindings[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - me_bindings[0].descriptorCount = 1; - me_bindings[0].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; - me_bindings[1].binding = 1; - me_bindings[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; - me_bindings[1].descriptorCount = 1; - me_bindings[1].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; - VkDescriptorSetLayoutCreateInfo me_li; - memset(&me_li, 0, sizeof(me_li)); - me_li.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; - me_li.bindingCount = 2; - me_li.pBindings = me_bindings; - if (vkd.CreateDescriptorSetLayout(d->device, &me_li, NULL, &d->me_set_layout) != VK_SUCCESS) { - return false; - } - VkPipelineLayoutCreateInfo me_pli; - memset(&me_pli, 0, sizeof(me_pli)); - me_pli.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; - me_pli.setLayoutCount = 1; - me_pli.pSetLayouts = &d->me_set_layout; - me_pli.pushConstantRangeCount = 1; - me_pli.pPushConstantRanges = &pcr; - if (vkd.CreatePipelineLayout(d->device, &me_pli, NULL, &d->me_pipeline_layout) != VK_SUCCESS) { - return false; - } - VkDescriptorPoolSize me_sizes[2]; - memset(me_sizes, 0, sizeof(me_sizes)); - me_sizes[0].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - me_sizes[0].descriptorCount = DIS_SLOTS; - me_sizes[1].type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; - me_sizes[1].descriptorCount = DIS_SLOTS; - VkDescriptorPoolCreateInfo me_pci; - memset(&me_pci, 0, sizeof(me_pci)); - me_pci.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; - me_pci.maxSets = DIS_SLOTS; - me_pci.poolSizeCount = 2; - me_pci.pPoolSizes = me_sizes; - if (vkd.CreateDescriptorPool(d->device, &me_pci, NULL, &d->me_pool) != VK_SUCCESS) { - return false; - } - d->pass_me_luma.pipeline = dis_create_compute_pipeline_with_layout( - d, dis_me_luma_comp, dis_me_luma_comp_size, d->me_pipeline_layout, NULL); - if (!d->pass_gradient.pipeline || !d->pass_inverse.pipeline || !d->pass_propagate.pipeline || !d->pass_densify.pipeline || !d->pass_interp.pipeline || !d->pass_vr_prep.pipeline || !d->pass_vr_d1.pipeline || !d->pass_vr_d2.pipeline || !d->pass_vr_w.pipeline || !d->pass_vr_coef.pipeline || !d->pass_vr_sor.pipeline || !d->pass_vr_add.pipeline || !d->pass_hist.pipeline || !d->pass_side.pipeline || - !d->pass_pack.pipeline || !d->pass_splat.pipeline || !d->pass_me_luma.pipeline || + !d->pass_pack.pipeline || !d->pass_splat.pipeline || !d->pass_fb_poly.pipeline || !d->pass_fb_hsum.pipeline || !d->pass_fb_vsolve.pipeline) { return false; } @@ -1142,7 +1055,6 @@ static void dis_write_all_descriptors(VkrDis* d) { const VkImageView coarse_view = l + 1 < DIS_VR_LEVELS ? d->view_flow_refined[coarse_l] : d->view_dense[coarse_l]; dis_batch_sampled(d, &b, d->inverse_sets[s][l], 3, coarse_view, d->sampler); - dis_batch_sampled(d, &b, d->inverse_sets[s][l], 4, d->view_me_field, d->sampler); dis_batch_storage(d, &b, d->inverse_sets[s][l], 5, d->view_sparse[l]); dis_batch_sampled(d, &b, d->inverse_sets[s][l], 6, d->view_flow_out, d->sampler); dis_batch_sampled(d, &b, d->inverse_sets[s][l], 7, d->view_fb_l1, d->sampler); @@ -1293,7 +1205,6 @@ static void dis_destroy_views(VkrDis* d) { dis_destroy_view(d, &d->view_flow_out); dis_destroy_view(d, &d->view_splat_key); dis_destroy_view(d, &d->view_splat_field); - dis_destroy_view(d, &d->view_me_field); dis_destroy_view(d, &d->view_fb_l1); for (uint32_t l = 0; l < DIS_MAX_LEVELS; l++) { dis_destroy_view(d, &d->view_vr_prep[l]); @@ -1349,7 +1260,6 @@ static void dis_destroy_images(VkrDis* d) { dis_destroy_image(d, &d->flow_out); dis_destroy_image(d, &d->splat_key); dis_destroy_image(d, &d->splat_field); - dis_destroy_image(d, &d->me_field); for (uint32_t s = 0; s < DIS_SLOTS; s++) { dis_destroy_image(d, &d->fb_b[s]); dis_destroy_image(d, &d->fb_c[s]); @@ -1357,129 +1267,6 @@ static void dis_destroy_images(VkrDis* d) { dis_destroy_image(d, &d->fb_sg); dis_destroy_image(d, &d->fb_sh); dis_destroy_image(d, &d->fb_l1); - dis_destroy_me(d); -} - -static void dis_destroy_buffer(VkrDis* d, VkBuffer* buf, VkDeviceMemory* mem, void** map) { - if (*map) vkd.UnmapMemory(d->device, *mem); - if (*buf) vkd.DestroyBuffer(d->device, *buf, NULL); - if (*mem) vkd.FreeMemory(d->device, *mem, NULL); - *buf = VK_NULL_HANDLE; - *mem = VK_NULL_HANDLE; - *map = NULL; -} - -// Host-visible buffer, mapped for its whole life. Cached memory is preferred for a buffer the -// CPU reads back: uncached reads of a few hundred kilobytes cost far more than the invalidate. -static bool dis_create_host_buffer(VkrDis* d, VkDeviceSize size, VkBufferUsageFlags usage, - bool readback, VkBuffer* buf, VkDeviceMemory* mem, void** map, - bool* coherent) { - VkBufferCreateInfo bi; - memset(&bi, 0, sizeof(bi)); - bi.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; - bi.size = size; - bi.usage = usage; - bi.sharingMode = VK_SHARING_MODE_EXCLUSIVE; - if (vkd.CreateBuffer(d->device, &bi, NULL, buf) != VK_SUCCESS) return false; - VkMemoryRequirements mr; - vkd.GetBufferMemoryRequirements(d->device, *buf, &mr); - const VkMemoryPropertyFlags HV = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT; - const VkMemoryPropertyFlags HC = VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; - const VkMemoryPropertyFlags CA = VK_MEMORY_PROPERTY_HOST_CACHED_BIT; - uint32_t type = UINT32_MAX; - if (readback) { - type = dis_find_memory_type(d, mr.memoryTypeBits, HV | CA | HC); - if (type == UINT32_MAX) type = dis_find_memory_type(d, mr.memoryTypeBits, HV | CA); - } - if (type == UINT32_MAX) type = dis_find_memory_type(d, mr.memoryTypeBits, HV | HC); - if (type == UINT32_MAX) type = dis_find_memory_type(d, mr.memoryTypeBits, HV); - if (type == UINT32_MAX) return false; - *coherent = (d->mem_props.memoryTypes[type].propertyFlags & HC) != 0; - VkMemoryAllocateInfo ai; - memset(&ai, 0, sizeof(ai)); - ai.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; - ai.allocationSize = mr.size; - ai.memoryTypeIndex = type; - if (vkd.AllocateMemory(d->device, &ai, NULL, mem) != VK_SUCCESS) return false; - if (vkd.BindBufferMemory(d->device, *buf, *mem, 0) != VK_SUCCESS) return false; - return vkd.MapMemory(d->device, *mem, 0, VK_WHOLE_SIZE, 0, map) == VK_SUCCESS; -} - -static void dis_destroy_me(VkrDis* d) { - dis_qcom_me_destroy(d->me); - d->me = NULL; - dis_destroy_buffer(d, &d->me_luma_buf, &d->me_luma_mem, &d->me_luma_map); - dis_destroy_buffer(d, &d->me_field_buf, &d->me_field_mem, &d->me_field_map); - free(d->me_xy); - d->me_xy = NULL; - d->hint_level = -1; -} - -// The hint seeds the search on level me_level of a w x h pyramid: the estimator's field is that -// level's size, and its input is the field times the block size, which for the usual 2:1 levels -// is twice the flow extent - so the estimator sees finer detail than the level it seeds, and its -// fixed search range covers twice the motion it would at the flow extent. -static void dis_create_me(VkrDis* d, uint32_t w, uint32_t h) { - dis_destroy_me(d); - if (!d->hw_motion || d->levels < 3) return; - uint32_t bx = 0, by = 0; - if (!dis_qcom_me_supported(&bx, &by)) return; - - d->me_level = 2; - d->me_field_w = w >> d->me_level; - d->me_field_h = h >> d->me_level; - d->me_w = d->me_field_w * bx; - d->me_h = d->me_field_h * by; - // Tiny inputs came back as NaN on Adreno 750 (320x176); stay well clear of that. - if (d->me_w < 256 || d->me_h < 144 || (d->me_w & 3u)) return; - - const VkDeviceSize luma_bytes = (VkDeviceSize)d->me_w * d->me_h; - const VkDeviceSize field_bytes = (VkDeviceSize)d->me_field_w * d->me_field_h * 2 * sizeof(float); - if (!dis_create_host_buffer(d, luma_bytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, true, - &d->me_luma_buf, &d->me_luma_mem, &d->me_luma_map, - &d->me_luma_coherent) || - !dis_create_host_buffer(d, field_bytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, false, - &d->me_field_buf, &d->me_field_mem, &d->me_field_map, - &d->me_field_coherent)) { - DIS_LOGW("WFG hardware motion: host buffers unavailable; using WFG alone"); - dis_destroy_me(d); - return; - } - d->me_xy = (float*)malloc((size_t)d->me_field_w * d->me_field_h * 2 * sizeof(float)); - d->me = d->me_xy ? dis_qcom_me_create(d->me_w, d->me_h) : NULL; - if (!d->me) { - dis_destroy_me(d); - return; - } - - for (uint32_t s = 0; s < DIS_SLOTS; s++) { - VkDescriptorImageInfo ii; - memset(&ii, 0, sizeof(ii)); - ii.sampler = d->sampler; - ii.imageView = d->view_color[s]; - ii.imageLayout = VK_IMAGE_LAYOUT_GENERAL; - VkDescriptorBufferInfo bi; - memset(&bi, 0, sizeof(bi)); - bi.buffer = d->me_luma_buf; - bi.range = VK_WHOLE_SIZE; - VkWriteDescriptorSet w2[2]; - memset(w2, 0, sizeof(w2)); - w2[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - w2[0].dstSet = d->me_sets[s]; - w2[0].dstBinding = 0; - w2[0].descriptorCount = 1; - w2[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - w2[0].pImageInfo = ⅈ - w2[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - w2[1].dstSet = d->me_sets[s]; - w2[1].dstBinding = 1; - w2[1].descriptorCount = 1; - w2[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; - w2[1].pBufferInfo = &bi; - vkd.UpdateDescriptorSets(d->device, 2, w2, 0, NULL); - } - DIS_LOGI("WFG hardware motion hint: GL_QCOM_motion_estimation on %ux%u seeds level %u (%ux%u)", - d->me_w, d->me_h, d->me_level, d->me_field_w, d->me_field_h); } static bool dis_create_resources(VkrDis* d, uint32_t w, uint32_t h, uint32_t full_w, @@ -1555,13 +1342,6 @@ static bool dis_create_resources(VkrDis* d, uint32_t w, uint32_t h, uint32_t ful if (!dis_create_image(d, &d->splat_field, w, h * VKR_DIS_MAX_GENERATIONS, VK_FORMAT_R16G16B16A16_SFLOAT, 1, VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT)) return false; - // Hardware motion hint, one vector per block of the level it seeds. It always exists so the - // search's binding stays valid; the search reads it only on frames that uploaded one. - const uint32_t hint_w = L >= 3 ? (w >> 2) : 1u; - const uint32_t hint_h = L >= 3 ? (h >> 2) : 1u; - if (!dis_create_image(d, &d->me_field, hint_w ? hint_w : 1u, hint_h ? hint_h : 1u, - VK_FORMAT_R32G32_SFLOAT, 1, - VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT)) return false; for (uint32_t s = 0; s < DIS_SLOTS; s++) { if (!dis_create_image(d, &d->fb_b[s], w, h, VK_FORMAT_R32G32B32A32_SFLOAT, L, VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT)) return false; @@ -1613,12 +1393,10 @@ static bool dis_create_resources(VkrDis* d, uint32_t w, uint32_t h, uint32_t ful if (!dis_create_view(d, d->flow_out.image, VK_FORMAT_R16G16_SFLOAT, 0, 1, &d->view_flow_out)) return false; if (!dis_create_view(d, d->splat_key.image, VK_FORMAT_R32_UINT, 0, 1, &d->view_splat_key)) return false; if (!dis_create_view(d, d->splat_field.image, VK_FORMAT_R16G16B16A16_SFLOAT, 0, 1, &d->view_splat_field)) return false; - if (!dis_create_view(d, d->me_field.image, VK_FORMAT_R32G32_SFLOAT, 0, 1, &d->view_me_field)) return false; if (!dis_create_view(d, d->fb_l1.image, VK_FORMAT_R32G32_SFLOAT, 0, 1, &d->view_fb_l1)) return false; vkr_dis_reset(d); dis_write_all_descriptors(d); - dis_create_me(d, w, h); return true; } @@ -1666,16 +1444,6 @@ static bool dis_allocate_sets(VkrDis* d) { if (!dis_alloc(d, d->set_layout, 1, &d->pack_set)) return false; - for (uint32_t s = 0; s < DIS_SLOTS; s++) { - VkDescriptorSetAllocateInfo ai; - memset(&ai, 0, sizeof(ai)); - ai.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; - ai.descriptorPool = d->me_pool; - ai.descriptorSetCount = 1; - ai.pSetLayouts = &d->me_set_layout; - if (vkd.AllocateDescriptorSets(d->device, &ai, &d->me_sets[s]) != VK_SUCCESS) return false; - } - for (uint32_t l = 0; l < DIS_VR_LEVELS; l++) { VkDescriptorSet vr_sets[DIS_VR_SHARED_SETS]; if (!dis_alloc(d, d->vr_set_layout, DIS_VR_SHARED_SETS, vr_sets)) return false; @@ -1858,18 +1626,6 @@ VkrDis* vkr_dis_create(VkDevice device, VkPhysicalDevice physical_device) { d->target_fps = 0; d->refresh_rate = 0.0f; d->plan_log_gen = -1; - // Off by default: on Adreno 750 the hint left quality unchanged and cost ~2.5 ms per real - // frame, most of it the mid-frame submit it needs. Opt in on a device without a rebuild: - // adb shell setprop debug.winnative.dis.hwme 1 - d->hw_motion = false; -#ifdef __ANDROID__ - char prop[PROP_VALUE_MAX] = {0}; - if (__system_property_get("debug.winnative.dis.hwme", prop) > 0 && prop[0] == '1') { - d->hw_motion = true; - DIS_LOGI("WFG hardware motion hint enabled by debug.winnative.dis.hwme"); - } -#endif - d->hint_level = -1; // Flow estimator: DIS unless asked for Farneback, by WFG_ENGINE=farneback in the // environment (the desktop bench) or, on a device, @@ -1960,10 +1716,6 @@ void vkr_dis_destroy(VkrDis* d) { if (d->pass_fb_poly.pipeline) vkd.DestroyPipeline(d->device, d->pass_fb_poly.pipeline, NULL); if (d->pass_fb_hsum.pipeline) vkd.DestroyPipeline(d->device, d->pass_fb_hsum.pipeline, NULL); if (d->pass_fb_vsolve.pipeline) vkd.DestroyPipeline(d->device, d->pass_fb_vsolve.pipeline, NULL); - if (d->pass_me_luma.pipeline) vkd.DestroyPipeline(d->device, d->pass_me_luma.pipeline, NULL); - if (d->me_pool) vkd.DestroyDescriptorPool(d->device, d->me_pool, NULL); - if (d->me_pipeline_layout) vkd.DestroyPipelineLayout(d->device, d->me_pipeline_layout, NULL); - if (d->me_set_layout) vkd.DestroyDescriptorSetLayout(d->device, d->me_set_layout, NULL); if (d->pool) vkd.DestroyDescriptorPool(d->device, d->pool, NULL); if (d->pipeline_layout) vkd.DestroyPipelineLayout(d->device, d->pipeline_layout, NULL); if (d->vr_pipeline_layout) vkd.DestroyPipelineLayout(d->device, d->vr_pipeline_layout, NULL); @@ -2210,7 +1962,7 @@ uint32_t vkr_dis_plan(VkrDis* d, uint32_t capacity, uint64_t source_frames) { // since too many frames is what overflows the swapchain. int up_streak = d->planned_gen > 0 ? (int)(source_rate * 0.5f) : 2; if (up_streak < 2) up_streak = 2; - if (d->gen_high_streak >= up_streak) { + if (d->gen_high_streak >= (uint32_t)up_streak) { d->planned_gen = raw; d->gen_high_streak = 0; } @@ -2339,148 +2091,6 @@ static void dis_vr_level(VkrDis* d, VkCommandBuffer cmd, uint32_t slot, uint32_t dis_compute_barrier(cmd); } -// Hardware motion hint for the pair ending in slot: the newest frame's luminance goes to the -// GLES estimator and its field comes back as the starting candidate of the search on me_level. -// -// The estimator needs this frame's pixels, which the caller has only just recorded, so the -// command buffer is handed back through lush to be submitted and waited on first. What was -// recorded so far - the caller's composite and the copies above - then runs ahead of the rest -// of the frame; the caller submits the returned buffer for everything after. -static VkCommandBuffer dis_hardware_motion(VkrDis* d, VkCommandBuffer cmd, uint32_t slot, - VkrDisFlushFn flush, void* flush_user) { - const int32_t me_pc[2] = {(int32_t)d->me_w, (int32_t)d->me_h}; - vkd.CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->pass_me_luma.pipeline); - vkd.CmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->me_pipeline_layout, 0, 1, - &d->me_sets[slot], 0, NULL); - vkd.CmdPushConstants(cmd, d->me_pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, - sizeof(me_pc), me_pc); - vkd.CmdDispatch(cmd, (d->me_w / 4u + DIS_LOCAL_SIZE - 1) / DIS_LOCAL_SIZE, - (d->me_h + DIS_LOCAL_SIZE - 1) / DIS_LOCAL_SIZE, 1); - VkMemoryBarrier hb; - memset(&hb, 0, sizeof(hb)); - hb.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER; - hb.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT; - hb.dstAccessMask = VK_ACCESS_HOST_READ_BIT; - vkd.CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, - 0, 1, &hb, 0, NULL, 0, NULL); - - cmd = flush(flush_user, cmd); - if (cmd == VK_NULL_HANDLE) return cmd; - - if (!d->me_luma_coherent) { - VkMappedMemoryRange r; - memset(&r, 0, sizeof(r)); - r.sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE; - r.memory = d->me_luma_mem; - r.size = VK_WHOLE_SIZE; - vkd.InvalidateMappedMemoryRanges(d->device, 1, &r); - } - d->me_pairs++; - if (!dis_qcom_me_push(d->me, (const uint8_t*)d->me_luma_map, d->me_xy)) return cmd; - - // Pixels of the estimator's input -> normalised uv, which is what every flow image in the - // chain stores. Vectors the estimator could not have found - non-finite, or past half the - // frame - are marked invalid rather than clamped, so the search simply ignores them. - const uint32_t n = d->me_field_w * d->me_field_h; - float* dst = (float*)d->me_field_map; - const float inv_w = 1.0f / (float)d->me_w; - const float inv_h = 1.0f / (float)d->me_h; - for (uint32_t i = 0; i < n; i++) { - const float vx = d->me_xy[i * 2]; - const float vy = d->me_xy[i * 2 + 1]; - const bool ok = isfinite(vx) && isfinite(vy) && - fabsf(vx) < 0.5f * (float)d->me_w && fabsf(vy) < 0.5f * (float)d->me_h; - dst[i * 2] = ok ? vx * inv_w : 1.0e7f; - dst[i * 2 + 1] = ok ? vy * inv_h : 1.0e7f; - } - if (DIS_ME_PRIMARY) { - // As the level's result the field has no search behind it to reject a bad block, so - // outliers are taken out here: a 3x3 component median over the valid neighbours, and - // zero where there are none. - const int fw = (int)d->me_field_w, fh = (int)d->me_field_h; - float* med = d->me_xy; // reused: the raw pixels are no longer needed - for (int y = 0; y < fh; y++) { - for (int x = 0; x < fw; x++) { - for (int c = 0; c < 2; c++) { - float v[9]; - int k = 0; - for (int dy = -1; dy <= 1; dy++) { - for (int dx = -1; dx <= 1; dx++) { - const int xx = x + dx, yy = y + dy; - if (xx < 0 || yy < 0 || xx >= fw || yy >= fh) continue; - const float s = dst[(yy * fw + xx) * 2 + c]; - if (fabsf(s) < 1.0e6f) v[k++] = s; - } - } - for (int a = 1; a < k; a++) { - const float key = v[a]; - int b = a - 1; - while (b >= 0 && v[b] > key) { v[b + 1] = v[b]; b--; } - v[b + 1] = key; - } - med[(y * fw + x) * 2 + c] = k ? v[k / 2] : 0.0f; - } - } - } - memcpy(dst, med, (size_t)n * 2 * sizeof(float)); - } - if (!d->me_field_coherent) { - VkMappedMemoryRange r; - memset(&r, 0, sizeof(r)); - r.sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE; - r.memory = d->me_field_mem; - r.size = VK_WHOLE_SIZE; - vkd.FlushMappedMemoryRanges(d->device, 1, &r); - } - - dis_barrier(cmd, d->me_field.image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, - VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, - VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); - VkBufferImageCopy region; - memset(®ion, 0, sizeof(region)); - region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - region.imageSubresource.layerCount = 1; - region.imageExtent.width = d->me_field_w; - region.imageExtent.height = d->me_field_h; - region.imageExtent.depth = 1; - vkd.CmdCopyBufferToImage(cmd, d->me_field_buf, d->me_field.image, VK_IMAGE_LAYOUT_GENERAL, 1, - ®ion); - dis_barrier(cmd, d->me_field.image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, - VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, - VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); - if (DIS_ME_PRIMARY) { - // The field is exactly the size of level me_level, so it drops into that mip of the - // refined flow, where the next finer level's search picks it up as its coarse estimate. - VkImageCopy ic; - memset(&ic, 0, sizeof(ic)); - ic.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - ic.srcSubresource.layerCount = 1; - ic.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - ic.dstSubresource.mipLevel = d->me_level; - ic.dstSubresource.layerCount = 1; - ic.extent.width = d->me_field_w; - ic.extent.height = d->me_field_h; - ic.extent.depth = 1; - dis_barrier(cmd, d->flow_refined.image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, - VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, - VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); - vkd.CmdCopyImage(cmd, d->me_field.image, VK_IMAGE_LAYOUT_GENERAL, d->flow_refined.image, - VK_IMAGE_LAYOUT_GENERAL, 1, &ic); - dis_barrier(cmd, d->flow_refined.image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, - VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, - VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); - d->me_primary_frame = true; - } else { - d->hint_level = (int)d->me_level; - } - d->me_hinted++; - if ((d->me_hinted % 600u) == 1u) { - DIS_LOGI("WFG hardware motion hint: %llu of %llu pairs seeded", - (unsigned long long)d->me_hinted, (unsigned long long)d->me_pairs); - } - return cmd; -} - // Farneback: the polynomial expansion of every level of the frame just put in slot. It is the // second frame of this pair and the first of the next, so each frame is expanded once. static void dis_fb_expand(VkrDis* d, VkCommandBuffer cmd, uint32_t slot, uint32_t L, @@ -2547,14 +2157,11 @@ static void dis_farneback(VkrDis* d, VkCommandBuffer cmd, uint32_t slot, uint32_ static void dis_splat_pass(VkrDis* d, VkCommandBuffer cmd, uint32_t slot, uint32_t generations, int mode); -VkCommandBuffer vkr_dis_process_ex(VkrDis* d, VkCommandBuffer cmd, VkImage source, - uint32_t width, uint32_t height, uint32_t generations, - VkrDisFlushFn flush, void* flush_user) { - if (!d || !d->built || d->unavailable) return cmd; +void vkr_dis_process(VkrDis* d, VkCommandBuffer cmd, VkImage source, uint32_t width, + uint32_t height, uint32_t generations) { + if (!d || !d->built || d->unavailable) return; d->last_generations = generations; - d->hint_level = -1; - d->me_primary_frame = false; dis_prime_layouts(d, cmd); DIS_PROF_BEGIN(d, cmd); @@ -2619,12 +2226,6 @@ VkCommandBuffer vkr_dis_process_ex(VkrDis* d, VkCommandBuffer cmd, VkImage sourc const uint32_t splat_gens = generations < VKR_DIS_MAX_GENERATIONS ? generations : VKR_DIS_MAX_GENERATIONS; if (splat_gens > 0) dis_splat_pass(d, cmd, slot, splat_gens, 0); - if (d->me && flush && wants_flow) { - cmd = dis_hardware_motion(d, cmd, slot, flush, flush_user); - } else if (d->me) { - // Without this pair's estimate the held frame no longer precedes the next one. - dis_qcom_me_invalidate(d->me); - } dis_compute_barrier(cmd); DIS_PROF(cmd, "copy+luma"); @@ -2635,7 +2236,7 @@ VkCommandBuffer vkr_dis_process_ex(VkrDis* d, VkCommandBuffer cmd, VkImage sourc if (!wants_flow) { d->prior_valid = false; - return cmd; + return; } if (farneback) { dis_farneback(d, cmd, slot, L, w, h, 0, false); @@ -2674,8 +2275,6 @@ VkCommandBuffer vkr_dis_process_ex(VkrDis* d, VkCommandBuffer cmd, VkImage sourc (void)k_search; (void)k_vr; (void)k_inv; (void)k_prop; for (uint32_t li = 0; li < L; li++) { const uint32_t l = coarse - li; - // The hardware field already stands in for this level and everything above it. - if (d->me_primary_frame && l >= d->me_level) continue; if (hybrid && l >= d->fb_hybrid_level) continue; const uint32_t lw = w >> l; const uint32_t lh = h >> l; @@ -2685,7 +2284,6 @@ VkCommandBuffer vkr_dis_process_ex(VkrDis* d, VkCommandBuffer cmd, VkImage sourc DisInversePC ipc; ipc.level = (int)l; ipc.coarseLevel = (int)coarse; - ipc.hintLevel = d->hint_level; ipc.priorLevels = d->prior_valid ? (int)((1u << L) - 1u) : 0; ipc.fbLevels = (hybrid && d->fb_hybrid_l1_start && d->fb_hybrid_level == 2u && L > 2) ? 1 : 0; vkd.CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->pass_inverse.pipeline); @@ -2767,12 +2365,6 @@ VkCommandBuffer vkr_dis_process_ex(VkrDis* d, VkCommandBuffer cmd, VkImage sourc DIS_PROF(cmd, "splat"); } } - return cmd; -} - -void vkr_dis_process(VkrDis* d, VkCommandBuffer cmd, VkImage source, uint32_t width, - uint32_t height, uint32_t generations) { - (void)vkr_dis_process_ex(d, cmd, source, width, height, generations, NULL, NULL); } // One pass of dis_splat.comp over every generated frame of the pair: 0 clears, 1 splats, 2 @@ -2921,17 +2513,4 @@ void vkr_dis_reset(VkrDis* d) { d->hist_parity = 0; d->hist_valid = false; d->prior_valid = false; - d->hint_level = -1; - if (d->me) dis_qcom_me_invalidate(d->me); -} - -void vkr_dis_set_hw_motion(VkrDis* d, bool enabled) { - if (!d || d->hw_motion == enabled) return; - d->hw_motion = enabled; - // Takes effect at the next resource build; force one. - d->built = false; -} - -bool vkr_dis_hw_motion_active(const VkrDis* d) { - return d && d->me != NULL; } diff --git a/app/src/main/cpp/winlator/vk/framegen/fg_present.c b/app/src/main/cpp/winlator/vk/framegen/fg_present.c index db99c78e2..83bab5dc9 100644 --- a/app/src/main/cpp/winlator/vk/framegen/fg_present.c +++ b/app/src/main/cpp/winlator/vk/framegen/fg_present.c @@ -87,7 +87,6 @@ struct FgPresenter { VkCommandPool command_pool; FgFrame frames[FG_FRAMES_IN_FLIGHT]; - VkFence flush_fence; uint32_t frame_index; FgTarget targets[FG_MAX_TARGETS]; @@ -541,9 +540,6 @@ static bool fg_create_frames(FgPresenter* fg) { cpi.queueFamilyIndex = fg->queue_family; if (vkCreateCommandPool(fg->device, &cpi, NULL, &fg->command_pool) != VK_SUCCESS) return false; - VkFenceCreateInfo ffi = {VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; - if (vkCreateFence(fg->device, &ffi, NULL, &fg->flush_fence) != VK_SUCCESS) return false; - for (uint32_t i = 0; i < FG_FRAMES_IN_FLIGHT; i++) { FgFrame* f = &fg->frames[i]; @@ -808,30 +804,6 @@ static void fg_renew_semaphore(FgPresenter* fg, VkSemaphore* handle) { *handle = fresh; } -// VkrDisFlushFn: DIS needs this frame's pixels on the CPU mid-frame for the hardware motion -// estimator. Everything recorded so far - the source blit and DIS's copies - touches no -// swapchain image and waits on no semaphore, so it goes out on its own; the same buffer is then -// begun again for the rest of the frame, which keeps every semaphore on the final submit. -static VkCommandBuffer fg_dis_flush(void* user, VkCommandBuffer cmd) { - FgPresenter* fg = (FgPresenter*)user; - vkEndCommandBuffer(cmd); - VkSubmitInfo si = {VK_STRUCTURE_TYPE_SUBMIT_INFO}; - si.commandBufferCount = 1; - si.pCommandBuffers = &cmd; - vkResetFences(fg->device, 1, &fg->flush_fence); - if (vkQueueSubmit(fg->queue, 1, &si, fg->flush_fence) == VK_SUCCESS) { - vkWaitForFences(fg->device, 1, &fg->flush_fence, VK_TRUE, UINT64_MAX); - } else { - FG_LOGW("WFG mid-frame submit failed"); - vkDeviceWaitIdle(fg->device); - } - vkResetCommandBuffer(cmd, 0); - VkCommandBufferBeginInfo bi = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; - bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(cmd, &bi); - return cmd; -} - static void fg_record_and_present(FgPresenter* fg, FgImport* source, AImage* image) { FgFrame* f = &fg->frames[fg->frame_index]; @@ -907,8 +879,8 @@ static void fg_record_and_present(FgPresenter* fg, FgImport* source, AImage* ima VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT); if (fg->active_engine == FG_ENGINE_DIS) { - vkr_dis_process_ex(fg->dis, f->cmd, composite->image, fg->extent.width, - fg->extent.height, gen_count, fg_dis_flush, fg); + vkr_dis_process(fg->dis, f->cmd, composite->image, fg->extent.width, + fg->extent.height, gen_count); } else if (fg->lsfg) { vkr_lsfg_process(fg->lsfg, f->cmd, composite->image, fg->extent.width, fg->extent.height, gen_count); @@ -1358,7 +1330,6 @@ void fg_destroy(FgPresenter* fg) { vkDestroySemaphore(fg->device, fg->retired[i], NULL); } fg->retired_count = 0; - if (fg->flush_fence) vkDestroyFence(fg->device, fg->flush_fence, NULL); if (fg->command_pool) vkDestroyCommandPool(fg->device, fg->command_pool, NULL); vkDestroyDevice(fg->device, NULL); } diff --git a/app/src/main/cpp/winlator/vk/vk_renderer.c b/app/src/main/cpp/winlator/vk/vk_renderer.c index f2e575b05..16f71411c 100644 --- a/app/src/main/cpp/winlator/vk/vk_renderer.c +++ b/app/src/main/cpp/winlator/vk/vk_renderer.c @@ -1924,10 +1924,6 @@ static void destroy_dis(VkRenderer* r) { if (!r->dis) return; vkr_dis_destroy(r->dis); r->dis = NULL; - if (r->dis_flush_fence) { - vkDestroyFence(r->device, r->dis_flush_fence, NULL); - r->dis_flush_fence = VK_NULL_HANDLE; - } r->framegen_real_frames = 0; r->framegen_made_frames = 0; r->framegen_draw_ns = 0; @@ -1936,39 +1932,6 @@ static void destroy_dis(VkRenderer* r) { r->framegen_timed_frames = 0; } -// VkrDisFlushFn: DIS needs this frame's pixels on the CPU mid-frame for the hardware motion -// estimator. What has been recorded by then is the scene pass into the composite target and -// DIS's own copies - no swapchain image, no semaphore - so it is submitted on its own and the -// same buffer is begun again; acquire waits and present signals all stay on the frame's submit. -static VkCommandBuffer dis_flush_frame(void* user, VkCommandBuffer cmd) { - VkRenderer* r = (VkRenderer*)user; - if (!r->dis_flush_fence) { - VkFenceCreateInfo fci = {VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; - if (vkCreateFence(r->device, &fci, NULL, &r->dis_flush_fence) != VK_SUCCESS) { - r->dis_flush_fence = VK_NULL_HANDLE; - } - } - vkEndCommandBuffer(cmd); - VkSubmitInfo si = {VK_STRUCTURE_TYPE_SUBMIT_INFO}; - si.commandBufferCount = 1; - si.pCommandBuffers = &cmd; - if (r->dis_flush_fence) vkResetFences(r->device, 1, &r->dis_flush_fence); - pthread_mutex_lock(&r->queue_mutex); - VkResult sr = vkQueueSubmit(r->graphics_queue, 1, &si, r->dis_flush_fence); - if (sr != VK_SUCCESS || !r->dis_flush_fence) vkQueueWaitIdle(r->graphics_queue); - pthread_mutex_unlock(&r->queue_mutex); - if (sr == VK_SUCCESS && r->dis_flush_fence) { - vkWaitForFences(r->device, 1, &r->dis_flush_fence, VK_TRUE, UINT64_MAX); - } else if (sr != VK_SUCCESS) { - VK_LOGW("WFG mid-frame submit -> %d", (int)sr); - } - vkResetCommandBuffer(cmd, 0); - VkCommandBufferBeginInfo bi = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; - bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(cmd, &bi); - return cmd; -} - static void create_dis(VkRenderer* r) { if (r->dis || !r->device || !r->physical_device) return; @@ -2922,9 +2885,8 @@ static bool record_and_submit_frame(VkRenderer* r) { if (composite) { if ((use_dis || r->lsfg) && framegen_capacity > 0) { if (use_dis) { - vkr_dis_process_ex(r->dis, f->cmd, composite->image, - composite->width, composite->height, gen_count, - dis_flush_frame, r); + vkr_dis_process(r->dis, f->cmd, composite->image, + composite->width, composite->height, gen_count); } else { vkr_lsfg_process(r->lsfg, f->cmd, composite->image, r->swapchain_extent.width, r->swapchain_extent.height, gen_count); diff --git a/app/src/main/cpp/winlator/vk/vk_state.h b/app/src/main/cpp/winlator/vk/vk_state.h index 398b073bc..d424053ac 100644 --- a/app/src/main/cpp/winlator/vk/vk_state.h +++ b/app/src/main/cpp/winlator/vk/vk_state.h @@ -429,7 +429,6 @@ typedef struct VkRenderer { struct VkrDis* dis; bool dis_requested; uint32_t dis_scale; - VkFence dis_flush_fence; // mid-frame submit for DIS's hardware motion hint uint32_t dis_target_fps; bool dis_debug_flow; uint64_t sgsr1_dbg_sig; diff --git a/docs/FRAME-GENERATION.md b/docs/FRAME-GENERATION.md index 4abbc5907..00ea4a592 100644 --- a/docs/FRAME-GENERATION.md +++ b/docs/FRAME-GENERATION.md @@ -26,12 +26,12 @@ Generation**. WinNative finds the DLL automatically from your Steam library; if **Select Lossless.dll…** to point at it. The LSFG half of the **FG** tab stays disabled until the shaders import successfully. -## WFG +## WFG — WinNative/Winlator Frame Generator -**Nothing to buy, nothing to import.** WFG is a complete open-source Dense Inverse Search frame -generator built into WinNative — sixteen compute shaders that ship with the APK — so it works on -a fresh install with no Steam account and no `Lossless.dll`. Turn it on in the **FG** tab and it -runs. +**Nothing to buy, nothing to import.** WFG (WinNative/Winlator Frame Generator, GPL-3.0) is a +complete open-source Dense Inverse Search frame generator built into WinNative — compute shaders +that ship with the APK — so it works on a fresh install with no Steam account and no +`Lossless.dll`. Turn it on in the **FG** tab and it runs. ## Controls From ccf00940008dfa7a115472551551ced7080e820d Mon Sep 17 00:00:00 2001 From: qwertypower <1qwertypower1@gmail.com> Date: Tue, 13 Oct 2026 00:01:38 +0300 Subject: [PATCH 10/18] README: WFG - WinNative/Winlator Frame Generator --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ccb1ae4d7..857640938 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ alongside them. | --- | --- | | 📦 **Install** | [Releases](https://github.com/WinNative-Emu/WinNative/releases) | | 🎮 **Retro consoles** | [docs/RETRO-CONSOLES.md](docs/RETRO-CONSOLES.md) — NES through PlayStation 2 | -| 🎞️ **Frame generation** | [docs/FRAME-GENERATION.md](docs/FRAME-GENERATION.md) — LSFG and WFG | +| 🎞️ **Frame generation** | [docs/FRAME-GENERATION.md](docs/FRAME-GENERATION.md) — LSFG and WFG (WinNative/Winlator Frame Generator) | | 🖥️ **Wayland display server** | [docs/WAYLAND-DISPLAY.md](docs/WAYLAND-DISPLAY.md) — winewayland sessions on the embedded compositor | | 🔨 **Build from source** | [docs/BUILDING.md](docs/BUILDING.md) | | 🙏 **Credits & licenses** | [CREDITS.md](CREDITS.md) · [EMULATOR_CREDITS.md](EMULATOR_CREDITS.md) | @@ -60,7 +60,8 @@ WinNative stands on work by **brunodev85** (Winlator), **Pipetto-crypto** (Winla (Goldberg Steam Emulator), **Filippo Scognamiglio** (LibretroDroid) and the **libretro** core authors, the **ARMSX2**, **PCSX2** and **Dolphin** teams, **PancakeTAS** (lsfg-vk), **Camille LaVey** of the **Eden Emulator Project** (the Vulkan LSFG port this one derives from), -**qwertypower** of **DEVAR Entertainment LLC** (the open-source WFG engine), **OpenCV** and +**qwertypower** of **DEVAR Entertainment LLC** (WFG — WinNative/Winlator Frame Generator, the +open-source frame generation engine), **OpenCV** and Till Kroeger (the DIS algorithm), **DXVK** (the `dxbc` translator), and **The412Banner** (DirectAudio and Steam Controller support), and the **SDL** contributors. From 6bacdfa9cbd08bbd3dd37319bc33fcf20fb9ac33 Mon Sep 17 00:00:00 2001 From: qwertypower <1qwertypower1@gmail.com> Date: Wed, 14 Oct 2026 18:24:45 +0300 Subject: [PATCH 11/18] WFG: splat only the vectors that land on a different motion --- .../cpp/wfg/include/shaders/dis_splat.comp | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/app/src/main/cpp/wfg/include/shaders/dis_splat.comp b/app/src/main/cpp/wfg/include/shaders/dis_splat.comp index 01d826c44..cd0c1592d 100644 --- a/app/src/main/cpp/wfg/include/shaders/dis_splat.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_splat.comp @@ -50,6 +50,15 @@ layout(push_constant) uniform PC { int mode; } pc; +// Splat only the vectors that land where the field moves differently, by more than this many +// flow texels. +#ifndef SPLAT_CONFLICT_ONLY +#define SPLAT_CONFLICT_ONLY 1 +#endif +#ifndef SPLAT_CONFLICT +#define SPLAT_CONFLICT 1.0 +#endif + // Mean absolute luma difference (0..255) per consistency step: 8 luma levels per step, 31 steps. #define SPLAT_ERR_STEP 8.0 @@ -89,13 +98,34 @@ void main() { vec2 f = texelFetch(flowTex, p, 0).xy; if (any(isnan(f)) || any(isinf(f))) return; vec2 fpx = f * vec2(sz); - // The same for every generated frame: scored once, splatted to each. - uint key = priority(p, fpx, sz); + // The same for every generated frame: scored once, splatted to each - scored lazily, since + // most texels splat to none. + uint key = 0u; for (int g = 0; g < pc.gens; g++) { - // The four texels round the landing point, so a stretched field leaves no gaps. float t = float(g + 1) / float(pc.gens + 1); vec2 target = vec2(p) + 0.5 + t * fpx; +#if SPLAT_CONFLICT_ONLY + // Only where the field at the landing point moves differently: inside a layer the + // interpolate's own lookup finds this same vector, and the splat (the only atomics in + // WFG, the larger part of its cost on an Adreno 750) would change nothing. + ivec2 land = clamp(ivec2(floor(target)), ivec2(0), sz - 1); + vec2 fl = texelFetch(flowTex, land, 0).xy; + if (length((fl - f) * vec2(sz)) < SPLAT_CONFLICT) continue; +#endif + if (key == 0u) key = priority(p, fpx, sz); +#if SPLAT_CONFLICT_ONLY + // The texels that do not splat still compete: the content the lookup would put at the + // landing point comes from where the field there starts, and a vector that would lose + // to it changes nothing either. + if (all(equal(fl, fl))) { + vec2 flpx = fl * vec2(sz); + ivec2 nat = clamp(ivec2(floor(vec2(land) + 0.5 - t * flpx)), ivec2(0), sz - 1); + vec2 fn = texelFetch(flowTex, nat, 0).xy; + if (all(equal(fn, fn)) && priority(nat, fn * vec2(sz), sz) >= key) continue; + } +#endif + // The four texels round the landing point, so a stretched field leaves no gaps. ivec2 base = ivec2(floor(target - 0.5)); ivec2 strip = ivec2(0, g * sz.y); for (int i = 0; i < 4; i++) { From 5ebdce4127b5d4fe015b7d09acfd9a053c2b5c21 Mon Sep 17 00:00:00 2001 From: qwertypower <1qwertypower1@gmail.com> Date: Wed, 14 Oct 2026 18:46:33 +0300 Subject: [PATCH 12/18] WFG: replace the vectors the next frame does not confirm for the interpolation --- app/src/main/cpp/wfg/CMakeLists.txt | 1 + .../cpp/wfg/include/shaders/dis_fill.comp | 140 ++++++++++++++++++ app/src/main/cpp/wfg/src/vkr_dis.c | 73 +++++++-- 3 files changed, 205 insertions(+), 9 deletions(-) create mode 100644 app/src/main/cpp/wfg/include/shaders/dis_fill.comp diff --git a/app/src/main/cpp/wfg/CMakeLists.txt b/app/src/main/cpp/wfg/CMakeLists.txt index 6e22535b0..7637a5a3d 100644 --- a/app/src/main/cpp/wfg/CMakeLists.txt +++ b/app/src/main/cpp/wfg/CMakeLists.txt @@ -26,6 +26,7 @@ set(DIS_SHADERS dis_side dis_flow_pack dis_splat + dis_fill dis_vr_prep dis_vr_d1 dis_vr_d2 diff --git a/app/src/main/cpp/wfg/include/shaders/dis_fill.comp b/app/src/main/cpp/wfg/include/shaders/dis_fill.comp new file mode 100644 index 000000000..1d9ca17de --- /dev/null +++ b/app/src/main/cpp/wfg/include/shaders/dis_fill.comp @@ -0,0 +1,140 @@ +// SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) +// SPDX-License-Identifier: GPL-3.0-or-later +// +// WFG frame generation: a Vulkan compute realisation of Dense Inverse Search +// optical flow. The algorithm and its reference implementation come from +// OpenCV's DISOpticalFlow, which adopted Till Kroeger's original OF_DIS. +// See CREDITS.md for the full attribution. + +#version 450 + +precision highp float; +precision highp int; + +// Two passes over the level-0 field, per source pair (pc.mode): +// 0 - how well the next frame confirms each vector: the mean absolute luma difference +// (0..255) between the previous frame at the texel and the next frame where its vector +// ends, over the centre and its four diagonal neighbours (255 for an end off the frame); +// 1 - the field the interpolation uses, with the vectors the next frame does not confirm +// replaced. +// +// Those are content going out of view - background sliding behind an object, the far side of +// a turning one - which has nothing to match in the next frame, so the search leaves a vector +// borrowed from whatever matched least badly, typically the occluder's: the generated frame +// drags the occluder's texture over the background in a band as wide as the motion. That +// content keeps moving with its own surface until it is hidden, so it takes the vector of the +// nearest confirmed texels of that surface: walking out in eight directions, the cost of a +// walk is its length plus the luma steps it crosses, so a silhouette separates the surfaces and +// the cheapest walk stays on the texel's own. Only the interpolation sees the result; the next +// pair's search still starts from the field as it was found. + +layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; + +layout(set = 0, binding = 0) uniform sampler2D flowIn; // packed level-0 field +layout(set = 0, binding = 1) uniform sampler2D consTex; // mode 0's output +layout(set = 0, binding = 2) uniform sampler2D prevLuma; +layout(set = 0, binding = 3) uniform sampler2D nextLuma; +layout(set = 0, binding = 4) uniform sampler2D flowRaw; // refined level-0 field, for mode 0 +layout(set = 0, binding = 5) uniform sampler2D histTex; // overlay history (dis_hist.comp) +layout(set = 0, binding = 8, r32f) uniform writeonly image2D consOut; +layout(set = 0, binding = 9, rg16f) uniform writeonly image2D flowOut; + +layout(push_constant) uniform PC { + int mode; +} pc; + +// Confirmation error below which a vector is trusted, and above which it is replaced outright. +#ifndef FILL_LO +#define FILL_LO 16.0 +#endif +#ifndef FILL_HI +#define FILL_HI 32.0 +#endif +// Mean luma difference in place below which the pair counts as agreeing there. +#ifndef FILL_STILL +#define FILL_STILL 4.0 +#endif +// Longest walk in texels: the band is as wide as the motion; a fast turn is ~16 texels at 180p. +#ifndef FILL_WALK +#define FILL_WALK 24 +#endif +// Cost of a texel of walk, in luma steps crossed. +#ifndef FILL_STEP_COST +#define FILL_STEP_COST 2.0 +#endif + +bool finite2(vec2 v) { + return !(any(isnan(v)) || any(isinf(v))); +} + +void main() { + ivec2 p = ivec2(gl_GlobalInvocationID.xy); + ivec2 sz = textureSize(flowIn, 0); + if (p.x >= sz.x || p.y >= sz.y) return; + vec2 f = texelFetch(flowIn, p, 0).xy; + + if (pc.mode == 0) { + // From the refined field, so this runs next to the pack pass rather than after it. + f = texelFetch(flowRaw, p, 0).xy; + // A vector that ends off the frame is content leaving it, not going behind something: + // there is nothing to confirm it with, and nothing to replace it with either. + float e = 255.0; + if (finite2(f)) { + vec2 inv = 1.0 / vec2(sz); + vec2 end = vec2(p) + 0.5 + f * vec2(sz); + bool inFrame = all(greaterThanEqual(end, vec2(0.0))) && all(lessThanEqual(end, vec2(sz))); + float moved = 0.0; + float still = 0.0; + for (int i = 0; i < 5; i++) { + vec2 o = i == 0 ? vec2(0.0) : vec2((i & 1) != 0 ? 1.0 : -1.0, i < 3 ? 1.0 : -1.0); + ivec2 s = clamp(p + ivec2(o), ivec2(0), sz - 1); + float a = texelFetch(prevLuma, s, 0).r; + moved += abs(a - textureLod(nextLuma, (end + o) * inv, 0.0).r); + still += abs(a - texelFetch(nextLuma, s, 0).r); + } + // Where the pair agrees in place the content is a static overlay or still scenery, + // or too flat for any vector to matter: either way its vector stays. + e = (!inFrame || still * 0.2 < FILL_STILL) ? 0.0 : moved * 0.2; + } + imageStore(consOut, p, vec4(e, 0.0, 0.0, 0.0)); + return; + } + + // Not on a static overlay, translucent panels included: the scenery moving behind one makes + // its vectors look unconfirmed, and borrowing the scenery's motion would carry the panel. + // The history also remembers still scenery for a few frames after something moves onto it, + // so it only counts for a vector that is still about zero, as an overlay's is. + float replace = smoothstep(FILL_LO, FILL_HI, texelFetch(consTex, p, 0).r) * + (1.0 - textureLod(histTex, (vec2(p) + 0.5) / vec2(sz), 0.0).r * + (1.0 - smoothstep(0.5, 1.5, length(f * vec2(sz))))); + if (replace > 0.0) { + const ivec2 dirs[8] = ivec2[8](ivec2(1, 0), ivec2(-1, 0), ivec2(0, 1), ivec2(0, -1), + ivec2(1, 1), ivec2(-1, 1), ivec2(1, -1), ivec2(-1, -1)); + float bestCost = 1e30; + vec2 best = f; + float l0 = texelFetch(prevLuma, p, 0).r; + for (int i = 0; i < 8; i++) { + float stepLen = i < 4 ? 1.0 : 1.41421356; + float cost = 0.0; + float lp = l0; + for (int k = 1; k <= FILL_WALK; k++) { + ivec2 q = p + dirs[i] * k; + if (any(lessThan(q, ivec2(0))) || any(greaterThanEqual(q, sz))) break; + float lq = texelFetch(prevLuma, q, 0).r; + cost += abs(lq - lp) + FILL_STEP_COST * stepLen; + lp = lq; + if (cost >= bestCost) break; + if (texelFetch(consTex, q, 0).r < FILL_LO) { + vec2 fq = texelFetch(flowIn, q, 0).xy; + if (finite2(fq)) { + bestCost = cost; + best = fq; + } + break; + } + } + } + f = mix(f, best, replace); + } + imageStore(flowOut, p, vec4(f, 0.0, 0.0)); +} diff --git a/app/src/main/cpp/wfg/src/vkr_dis.c b/app/src/main/cpp/wfg/src/vkr_dis.c index 3d881dea8..f5aaa1929 100644 --- a/app/src/main/cpp/wfg/src/vkr_dis.c +++ b/app/src/main/cpp/wfg/src/vkr_dis.c @@ -20,6 +20,7 @@ #include "shaders/dis_side_comp.spv.h" #include "shaders/dis_flow_pack_comp.spv.h" #include "shaders/dis_splat_comp.spv.h" +#include "shaders/dis_fill_comp.spv.h" #include "shaders/dis_vr_prep_comp.spv.h" #include "shaders/dis_vr_d1_comp.spv.h" #include "shaders/dis_vr_d2_comp.spv.h" @@ -191,6 +192,8 @@ struct VkrDis { DisImage side; DisImage flow_out; DisImage splat_key; + DisImage fill_cons; + DisImage flow_fill; DisImage splat_field; // Farneback flow (DIS_ENGINE_FARNEBACK and _HYBRID): each frame's polynomial expansion, @@ -222,6 +225,8 @@ struct VkrDis { VkImageView view_side; VkImageView view_flow_out; VkImageView view_splat_key; + VkImageView view_fill_cons; + VkImageView view_flow_fill; VkImageView view_splat_field; VkImageView view_fb_b[DIS_SLOTS][DIS_MAX_LEVELS]; VkImageView view_fb_c[DIS_SLOTS][DIS_MAX_LEVELS]; @@ -245,6 +250,7 @@ struct VkrDis { VkDescriptorSet side_sets[DIS_SLOTS]; VkDescriptorSet pack_set; VkDescriptorSet splat_sets[DIS_SLOTS]; + VkDescriptorSet fill_sets[DIS_SLOTS][2]; // per overlay history direction // Farneback, all from vr_set_layout: the expansion of slot s at level l; the equations of // the pair ending in slot s at level l, starting from the level above ([0]) or from this // level's previous iteration ([1]); and the solve of level l. @@ -279,6 +285,7 @@ struct VkrDis { DisPass pass_side; DisPass pass_pack; DisPass pass_splat; + DisPass pass_fill; DisPass pass_vr_prep; DisPass pass_vr_d1; DisPass pass_vr_d2; @@ -548,6 +555,8 @@ static uint32_t dis_collect_images(VkrDis* d, DisImage** out, uint32_t cap) { DIS_PUSH(&d->side); DIS_PUSH(&d->flow_out); DIS_PUSH(&d->splat_key); + DIS_PUSH(&d->fill_cons); + DIS_PUSH(&d->flow_fill); DIS_PUSH(&d->splat_field); for (uint32_t s = 0; s < DIS_SLOTS; s++) { DIS_PUSH(&d->fb_b[s]); @@ -758,7 +767,7 @@ static bool dis_create_pipelines(VkrDis* d) { // VR sets exist only for the levels the refinement actually runs on. const uint32_t vr_sets = (DIS_SLOTS + DIS_VR_SHARED_SETS) * DIS_VR_LEVELS; - const uint32_t hist_sets = DIS_SLOTS * 2u + DIS_SLOTS; // + the splat sets + const uint32_t hist_sets = DIS_SLOTS * 2u + DIS_SLOTS + DIS_SLOTS * 2u; // + splat and fill const uint32_t fb_sets = DIS_SLOTS * DIS_MAX_LEVELS * 3u + DIS_MAX_LEVELS + DIS_SLOTS + 1u; const uint32_t vr_layout_sets = vr_sets + hist_sets + fb_sets; const uint32_t total_sets = shared_sets + vr_layout_sets; @@ -862,13 +871,14 @@ static bool dis_create_pipelines(VkrDis* d) { d->pass_side.pipeline = dis_create_compute_pipeline(d, dis_side_comp, dis_side_comp_size); d->pass_pack.pipeline = dis_create_compute_pipeline(d, dis_flow_pack_comp, dis_flow_pack_comp_size); d->pass_splat.pipeline = dis_create_compute_pipeline_with_layout(d, dis_splat_comp, dis_splat_comp_size, d->vr_pipeline_layout, NULL); + d->pass_fill.pipeline = dis_create_compute_pipeline_with_layout(d, dis_fill_comp, dis_fill_comp_size, d->vr_pipeline_layout, NULL); if (!d->pass_gradient.pipeline || !d->pass_inverse.pipeline || !d->pass_propagate.pipeline || !d->pass_densify.pipeline || !d->pass_interp.pipeline || !d->pass_vr_prep.pipeline || !d->pass_vr_d1.pipeline || !d->pass_vr_d2.pipeline || !d->pass_vr_w.pipeline || !d->pass_vr_coef.pipeline || !d->pass_vr_sor.pipeline || !d->pass_vr_add.pipeline || !d->pass_hist.pipeline || !d->pass_side.pipeline || - !d->pass_pack.pipeline || !d->pass_splat.pipeline || + !d->pass_pack.pipeline || !d->pass_splat.pipeline || !d->pass_fill.pipeline || !d->pass_fb_poly.pipeline || !d->pass_fb_hsum.pipeline || !d->pass_fb_vsolve.pipeline) { return false; } @@ -1078,7 +1088,7 @@ static void dis_write_all_descriptors(VkrDis* d) { for (uint32_t dir = 0; dir < 2u; dir++) { dis_batch_sampled(d, &b, d->interp_sets[s][dir], 0, d->view_color[prev], d->sampler); dis_batch_sampled(d, &b, d->interp_sets[s][dir], 1, d->view_color[next], d->sampler); - dis_batch_sampled(d, &b, d->interp_sets[s][dir], 2, d->view_flow_out, d->sampler); + dis_batch_sampled(d, &b, d->interp_sets[s][dir], 2, d->view_flow_fill, d->sampler); dis_batch_sampled(d, &b, d->interp_sets[s][dir], 3, d->view_side, d->sampler); dis_batch_sampled(d, &b, d->interp_sets[s][dir], 4, d->view_hist[dir], d->sampler); dis_batch_sampled(d, &b, d->interp_sets[s][dir], 6, d->view_splat_field, d->sampler); @@ -1092,11 +1102,23 @@ static void dis_write_all_descriptors(VkrDis* d) { dis_batch_sampled(d, &b, d->splat_sets[s], 0, d->view_flow_luma[prev][0], d->sampler); dis_batch_sampled(d, &b, d->splat_sets[s], 1, d->view_flow_luma[next][0], d->sampler); - dis_batch_sampled(d, &b, d->splat_sets[s], 2, d->view_flow_refined[0], d->sampler); + dis_batch_sampled(d, &b, d->splat_sets[s], 2, d->view_flow_fill, d->sampler); dis_batch_storage(d, &b, d->splat_sets[s], DIS_VR_FIRST_STORAGE, d->view_splat_key); dis_batch_storage(d, &b, d->splat_sets[s], DIS_VR_FIRST_STORAGE + 1, d->view_splat_field); - dis_batch_sampled(d, &b, d->side_sets[s], 0, d->view_flow_refined[0], d->sampler); + for (uint32_t dir = 0; dir < 2u; dir++) { + VkDescriptorSet fs = d->fill_sets[s][dir]; + dis_batch_sampled(d, &b, fs, 0, d->view_flow_out, d->sampler); + dis_batch_sampled(d, &b, fs, 1, d->view_fill_cons, d->sampler); + dis_batch_sampled(d, &b, fs, 2, d->view_flow_luma[prev][0], d->sampler); + dis_batch_sampled(d, &b, fs, 3, d->view_flow_luma[next][0], d->sampler); + dis_batch_sampled(d, &b, fs, 4, d->view_flow_refined[0], d->sampler); + dis_batch_sampled(d, &b, fs, 5, d->view_hist[dir], d->sampler); + dis_batch_storage(d, &b, fs, DIS_VR_FIRST_STORAGE, d->view_fill_cons); + dis_batch_storage(d, &b, fs, DIS_VR_FIRST_STORAGE + 1, d->view_flow_fill); + } + + dis_batch_sampled(d, &b, d->side_sets[s], 0, d->view_flow_fill, d->sampler); dis_batch_storage(d, &b, d->side_sets[s], 5, d->view_side); for (uint32_t l = 0; l < vrL; l++) { @@ -1204,6 +1226,8 @@ static void dis_destroy_views(VkrDis* d) { dis_destroy_view(d, &d->view_side); dis_destroy_view(d, &d->view_flow_out); dis_destroy_view(d, &d->view_splat_key); + dis_destroy_view(d, &d->view_fill_cons); + dis_destroy_view(d, &d->view_flow_fill); dis_destroy_view(d, &d->view_splat_field); dis_destroy_view(d, &d->view_fb_l1); for (uint32_t l = 0; l < DIS_MAX_LEVELS; l++) { @@ -1259,6 +1283,8 @@ static void dis_destroy_images(VkrDis* d) { dis_destroy_image(d, &d->side); dis_destroy_image(d, &d->flow_out); dis_destroy_image(d, &d->splat_key); + dis_destroy_image(d, &d->fill_cons); + dis_destroy_image(d, &d->flow_fill); dis_destroy_image(d, &d->splat_field); for (uint32_t s = 0; s < DIS_SLOTS; s++) { dis_destroy_image(d, &d->fb_b[s]); @@ -1334,6 +1360,12 @@ static bool dis_create_resources(VkrDis* d, uint32_t w, uint32_t h, uint32_t ful // every device, so its three lookups per output pixel stay single bilinear taps. if (!dis_create_image(d, &d->flow_out, w, h, VK_FORMAT_R16G16_SFLOAT, 1, VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT)) return false; + // How well the next frame confirms each level-0 vector, and the field with the unconfirmed + // ones replaced, which is what the interpolation uses (dis_fill.comp). + if (!dis_create_image(d, &d->fill_cons, w, h, VK_FORMAT_R32_SFLOAT, 1, + VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT)) return false; + if (!dis_create_image(d, &d->flow_fill, w, h, VK_FORMAT_R16G16_SFLOAT, 1, + VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT)) return false; // The field splatted to a generated frame's time (dis_splat.comp): the winning priority per // texel (R32_UINT, whose storage atomics every device has) and the vector it carries, one // strip of the flow's height per generated frame of the pair. @@ -1392,6 +1424,8 @@ static bool dis_create_resources(VkrDis* d, uint32_t w, uint32_t h, uint32_t ful if (!dis_create_view(d, d->side.image, VK_FORMAT_R32_SFLOAT, 0, 1, &d->view_side)) return false; if (!dis_create_view(d, d->flow_out.image, VK_FORMAT_R16G16_SFLOAT, 0, 1, &d->view_flow_out)) return false; if (!dis_create_view(d, d->splat_key.image, VK_FORMAT_R32_UINT, 0, 1, &d->view_splat_key)) return false; + if (!dis_create_view(d, d->fill_cons.image, VK_FORMAT_R32_SFLOAT, 0, 1, &d->view_fill_cons)) return false; + if (!dis_create_view(d, d->flow_fill.image, VK_FORMAT_R16G16_SFLOAT, 0, 1, &d->view_flow_fill)) return false; if (!dis_create_view(d, d->splat_field.image, VK_FORMAT_R16G16B16A16_SFLOAT, 0, 1, &d->view_splat_field)) return false; if (!dis_create_view(d, d->fb_l1.image, VK_FORMAT_R32G32_SFLOAT, 0, 1, &d->view_fb_l1)) return false; @@ -1437,6 +1471,7 @@ static bool dis_allocate_sets(VkrDis* d) { if (!dis_alloc(d, d->vr_set_layout, 1, &d->hist_sets[s][0])) return false; if (!dis_alloc(d, d->vr_set_layout, 1, &d->hist_sets[s][1])) return false; if (!dis_alloc(d, d->vr_set_layout, 1, &d->splat_sets[s])) return false; + if (!dis_alloc(d, d->vr_set_layout, 2, d->fill_sets[s])) return false; for (uint32_t l = 0; l < DIS_VR_LEVELS; l++) { if (!dis_alloc(d, d->vr_set_layout, 1, &d->vr_prep_sets[s][l])) return false; } @@ -1713,6 +1748,7 @@ void vkr_dis_destroy(VkrDis* d) { if (d->pass_side.pipeline) vkd.DestroyPipeline(d->device, d->pass_side.pipeline, NULL); if (d->pass_pack.pipeline) vkd.DestroyPipeline(d->device, d->pass_pack.pipeline, NULL); if (d->pass_splat.pipeline) vkd.DestroyPipeline(d->device, d->pass_splat.pipeline, NULL); + if (d->pass_fill.pipeline) vkd.DestroyPipeline(d->device, d->pass_fill.pipeline, NULL); if (d->pass_fb_poly.pipeline) vkd.DestroyPipeline(d->device, d->pass_fb_poly.pipeline, NULL); if (d->pass_fb_hsum.pipeline) vkd.DestroyPipeline(d->device, d->pass_fb_hsum.pipeline, NULL); if (d->pass_fb_vsolve.pipeline) vkd.DestroyPipeline(d->device, d->pass_fb_vsolve.pipeline, NULL); @@ -2154,6 +2190,19 @@ static void dis_farneback(VkrDis* d, VkCommandBuffer cmd, uint32_t slot, uint32_ DIS_PROF(cmd, "farneback"); } +// One pass of dis_fill.comp: 0 scores how well the next frame confirms each vector, 1 writes +// the field with the unconfirmed ones replaced. The caller places the barriers. +static void dis_fill_pass(VkrDis* d, VkCommandBuffer cmd, uint32_t slot, int mode, uint32_t w, + uint32_t h) { + vkd.CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->pass_fill.pipeline); + vkd.CmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->vr_pipeline_layout, 0, 1, + &d->fill_sets[slot][d->hist_parity], 0, NULL); + vkd.CmdPushConstants(cmd, d->vr_pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(mode), + &mode); + vkd.CmdDispatch(cmd, (w + DIS_LOCAL_SIZE - 1) / DIS_LOCAL_SIZE, + (h + DIS_LOCAL_SIZE - 1) / DIS_LOCAL_SIZE, 1); +} + static void dis_splat_pass(VkrDis* d, VkCommandBuffer cmd, uint32_t slot, uint32_t generations, int mode); @@ -2338,11 +2387,11 @@ void vkr_dis_process(VkrDis* d, VkCommandBuffer cmd, VkImage source, uint32_t wi const uint32_t hist_dir = 1u - d->hist_parity; const int hist_reset = d->hist_valid ? 0 : 1; - // The three per-pair products the interpolate pass reads are independent of each - // other - the packed field and the side map read the refined field, the overlay - // history reads the colour pair - so they share one barrier. + // The packed field, how well the next frame confirms each vector (both from the refined + // field) and the overlay history (from the colour pair) are independent of each other, + // so they share one barrier. dis_dispatch(d, cmd, d->pass_pack.pipeline, d->pack_set, w, h); - if (splat_gens > 0) dis_splat_pass(d, cmd, slot, splat_gens, 1); + dis_fill_pass(d, cmd, slot, 0, w, h); d->prior_valid = true; vkd.CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->pass_hist.pipeline); @@ -2354,9 +2403,15 @@ void vkr_dis_process(VkrDis* d, VkCommandBuffer cmd, VkImage source, uint32_t wi (hist_h + DIS_LOCAL_SIZE - 1) / DIS_LOCAL_SIZE, 1); d->hist_parity = hist_dir; d->hist_valid = true; + dis_compute_barrier(cmd); + + // The field the interpolation uses, its unconfirmed vectors replaced (dis_fill.comp). + dis_fill_pass(d, cmd, slot, 1, w, h); + dis_compute_barrier(cmd); // Which real frame a true occlusion takes, and how sure, per level-0 texel. dis_dispatch(d, cmd, d->pass_side.pipeline, d->side_sets[slot], w, h); + if (splat_gens > 0) dis_splat_pass(d, cmd, slot, splat_gens, 1); dis_compute_barrier(cmd); DIS_PROF(cmd, "pack+hist+side"); if (splat_gens > 0) { From d059d4b98f54086d79ba6cb986d3e1a8397e273f Mon Sep 17 00:00:00 2001 From: qwertypower <1qwertypower1@gmail.com> Date: Wed, 14 Oct 2026 19:44:21 +0300 Subject: [PATCH 13/18] WFG: interpolate along the pair's acceleration --- .../cpp/wfg/include/shaders/dis_fill.comp | 71 ++++++++++++++++++- .../wfg/include/shaders/dis_flow_pack.comp | 7 +- .../wfg/include/shaders/dis_interpolate.comp | 28 +++++--- app/src/main/cpp/wfg/src/vkr_dis.c | 50 +++++++++---- 4 files changed, 130 insertions(+), 26 deletions(-) diff --git a/app/src/main/cpp/wfg/include/shaders/dis_fill.comp b/app/src/main/cpp/wfg/include/shaders/dis_fill.comp index 1d9ca17de..1e50fa29f 100644 --- a/app/src/main/cpp/wfg/include/shaders/dis_fill.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_fill.comp @@ -37,10 +37,12 @@ layout(set = 0, binding = 3) uniform sampler2D nextLuma; layout(set = 0, binding = 4) uniform sampler2D flowRaw; // refined level-0 field, for mode 0 layout(set = 0, binding = 5) uniform sampler2D histTex; // overlay history (dis_hist.comp) layout(set = 0, binding = 8, r32f) uniform writeonly image2D consOut; -layout(set = 0, binding = 9, rg16f) uniform writeonly image2D flowOut; +layout(set = 0, binding = 6) uniform sampler2D flowPrevTex; // the previous pair's field +layout(set = 0, binding = 9, rgba16f) uniform writeonly image2D flowOut; // xy field, zw acceleration layout(push_constant) uniform PC { int mode; + int accel; // flowPrevTex holds the previous pair's field } pc; // Confirmation error below which a vector is trusted, and above which it is replaced outright. @@ -58,6 +60,13 @@ layout(push_constant) uniform PC { #ifndef FILL_WALK #define FILL_WALK 24 #endif +// Acceleration below / above which (in flow texels) none / all of it is applied. +#ifndef ACCEL_LO +#define ACCEL_LO 0.5 +#endif +#ifndef ACCEL_HI +#define ACCEL_HI 1.5 +#endif // Cost of a texel of walk, in luma steps crossed. #ifndef FILL_STEP_COST #define FILL_STEP_COST 2.0 @@ -67,6 +76,64 @@ bool finite2(vec2 v) { return !(any(isnan(v)) || any(isinf(v))); } +// Acceleration of the content at p over the pair, for a motion that is not uniform within it: +// a camera turn speeding up or slowing down covers less or more than t of the pair's way by +// time t, and on a turn's ramp a linear split puts the generated frame several pixels off - +// as bad as repeating a frame on sharp texture. The content came into this pair's first frame +// moving with the previous pair's vector (looked up where it came from, two fixed-point steps), +// and leaves with this pair's: the difference is the acceleration a in the position +// d(t) = f t + a (t^2 - t) / 2. Estimation noise between two fields of a steady motion stays +// out: below half a flow texel nothing is applied. +// Where the content at uv came from in the previous pair's field: two fixed-point steps of +// y = uv - v(y) from the given start. z is how well the next frame confirmed that vector then. +vec3 previousVector(vec2 uv, vec2 start) { + vec2 v = textureLod(flowPrevTex, clamp(uv - start, vec2(0.0), vec2(1.0)), 0.0).xy; + v = textureLod(flowPrevTex, clamp(uv - v, vec2(0.0), vec2(1.0)), 0.0).xy; + return textureLod(flowPrevTex, clamp(uv - v, vec2(0.0), vec2(1.0)), 0.0).xyz; +} + +vec2 accelerationAt(vec2 uv, ivec2 sz) { + vec2 f = textureLod(flowIn, uv, 0.0).xy; + if (!finite2(f)) return vec2(0.0); + // Two starts: uv itself, and where the content would have been had it kept this pair's + // motion. Starting at uv, a texel just ahead of a moving object reads the background there + // and the lookup never leaves it - a whole object speed of false acceleration. Of the two, + // the smaller acceleration is the explanation to keep. + vec3 p0 = previousVector(uv, vec2(0.0)); + vec3 p1 = previousVector(uv, f); + vec2 a0 = f - p0.xy; + vec2 a1 = f - p1.xy; + if (!finite2(a0)) a0 = vec2(1e9); + if (!finite2(a1)) a1 = vec2(1e9); + bool first = dot(a0, a0) <= dot(a1, a1); + vec2 a = first ? a0 : a1; + if (dot(a, a) >= 1e8) return vec2(0.0); + // Not from a vector the next frame did not confirm then either - the pair across a scene + // cut, content that was going out of view: its difference to this one is no acceleration. + return a * (1.0 - smoothstep(FILL_LO, FILL_HI, first ? p0.z : p1.z)); +} + +vec2 median3(vec2 a, vec2 b, vec2 c) { + return max(min(a, b), min(max(a, b), c)); +} + +vec2 acceleration(ivec2 p, ivec2 sz) { + if (pc.accel == 0) return vec2(0.0); + // A camera's acceleration is the same over large parts of the frame; where the two fields + // meet a different layer - an object's edge, a cut, something that wrapped round - the + // difference is not an acceleration at all. The median of the texel and its neighbours two + // texels away on either axis keeps the first and drops the second. + vec2 uv = (vec2(p) + 0.5) / vec2(sz); + vec2 o = 2.0 / vec2(sz); + vec2 c = accelerationAt(uv, sz); + vec2 h = median3(c, accelerationAt(uv - vec2(o.x, 0.0), sz), accelerationAt(uv + vec2(o.x, 0.0), sz)); + vec2 w = median3(c, accelerationAt(uv - vec2(0.0, o.y), sz), accelerationAt(uv + vec2(0.0, o.y), sz)); + vec2 a = median3(c, h, w); + // Only for a vector the next frame confirms. + float trust = 1.0 - smoothstep(FILL_LO, FILL_HI, texelFetch(consTex, p, 0).r); + return a * trust * smoothstep(ACCEL_LO, ACCEL_HI, length(a * vec2(sz))); +} + void main() { ivec2 p = ivec2(gl_GlobalInvocationID.xy); ivec2 sz = textureSize(flowIn, 0); @@ -136,5 +203,5 @@ void main() { } f = mix(f, best, replace); } - imageStore(flowOut, p, vec4(f, 0.0, 0.0)); + imageStore(flowOut, p, vec4(f, acceleration(p, sz))); } diff --git a/app/src/main/cpp/wfg/include/shaders/dis_flow_pack.comp b/app/src/main/cpp/wfg/include/shaders/dis_flow_pack.comp index 215866d71..87be67080 100644 --- a/app/src/main/cpp/wfg/include/shaders/dis_flow_pack.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_flow_pack.comp @@ -24,7 +24,11 @@ precision highp int; layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; layout(set = 0, binding = 0) uniform sampler2D flowIn; -layout(set = 0, binding = 5, rg16f) uniform image2D flowOut; +layout(set = 0, binding = 1) uniform sampler2D consPrev; // dis_fill.comp's confirmation, last pair +layout(set = 0, binding = 8, rg16f) uniform image2D flowOut; +// The previous pair's field and how well its vectors were confirmed, kept for dis_fill.comp's +// acceleration estimate. +layout(set = 0, binding = 9, rgba16f) uniform writeonly image2D flowPrev; void main() { ivec2 p = ivec2(gl_GlobalInvocationID.xy); @@ -35,5 +39,6 @@ void main() { // A zero field degrades to repeating the real frame, which is the right answer // wherever the flow has nothing finite to say. if (any(isnan(f)) || any(isinf(f))) f = vec2(0.0); + imageStore(flowPrev, p, vec4(imageLoad(flowOut, p).xy, texelFetch(consPrev, p, 0).r, 0.0)); imageStore(flowOut, p, vec4(f, 0.0, 0.0)); } diff --git a/app/src/main/cpp/wfg/include/shaders/dis_interpolate.comp b/app/src/main/cpp/wfg/include/shaders/dis_interpolate.comp index 7eda5fb24..04c780d9e 100644 --- a/app/src/main/cpp/wfg/include/shaders/dis_interpolate.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_interpolate.comp @@ -93,8 +93,9 @@ layout(constant_id = 0) const int manualFlowFilter = 0; #define OCCL_HI 0.60 #endif -vec2 sampleFlow(vec2 uv) { - if (manualFlowFilter == 0) return textureLod(flowTex, uv, 0.0).xy; +// The field in xy and the acceleration (dis_fill.comp) in zw, one fetch. +vec4 sampleFlow4(vec2 uv) { + if (manualFlowFilter == 0) return textureLod(flowTex, uv, 0.0); vec2 sz = vec2(textureSize(flowTex, 0)); vec2 p = uv * sz - 0.5; @@ -102,13 +103,17 @@ vec2 sampleFlow(vec2 uv) { ivec2 i0 = ivec2(floor(p)); ivec2 mx = ivec2(sz) - 1; - vec2 a = texelFetch(flowTex, clamp(i0, ivec2(0), mx), 0).xy; - vec2 b = texelFetch(flowTex, clamp(i0 + ivec2(1, 0), ivec2(0), mx), 0).xy; - vec2 c = texelFetch(flowTex, clamp(i0 + ivec2(0, 1), ivec2(0), mx), 0).xy; - vec2 e = texelFetch(flowTex, clamp(i0 + ivec2(1, 1), ivec2(0), mx), 0).xy; + vec4 a = texelFetch(flowTex, clamp(i0, ivec2(0), mx), 0); + vec4 b = texelFetch(flowTex, clamp(i0 + ivec2(1, 0), ivec2(0), mx), 0); + vec4 c = texelFetch(flowTex, clamp(i0 + ivec2(0, 1), ivec2(0), mx), 0); + vec4 e = texelFetch(flowTex, clamp(i0 + ivec2(1, 1), ivec2(0), mx), 0); return mix(mix(a, b, frac.x), mix(c, e, frac.x), frac.y); } +vec2 sampleFlow(vec2 uv) { + return sampleFlow4(uv).xy; +} + float maxChannel(vec3 v) { return max(max(v.x, v.y), v.z); } @@ -168,7 +173,8 @@ void main() { vec2 eased = ramp * ramp * (3.0 - 2.0 * ramp); float edgeMix = min(eased.x, eased.y); - vec2 f = sampleFlow(uv); + vec4 f4 = sampleFlow4(uv); + vec2 f = f4.xy; if (edgeMix < 1.0) { vec2 fInner = sampleFlow(clamp(uv, guard, 1.0 - guard)); f = mix(fInner, f, edgeMix); @@ -241,8 +247,12 @@ void main() { return; } - vec2 uv0 = uv - pc.t * fa; - vec2 uv1 = uv + (1.0 - pc.t) * fa; + // Where the content is at t, with the pair's acceleration (dis_fill.comp): a turn speeding up + // or slowing down does not cover t of its way by time t. + vec2 acc = f4.zw * edgeMix; + if (any(isnan(acc)) || any(isinf(acc))) acc = vec2(0.0); + vec2 uv0 = uv - (pc.t * fa + 0.5 * (pc.t * pc.t - pc.t) * acc); + vec2 uv1 = uv0 + fa; vec3 c0 = warpSample(prevColor, uv0, vec2(size)); vec3 c1 = warpSample(nextColor, uv1, vec2(size)); diff --git a/app/src/main/cpp/wfg/src/vkr_dis.c b/app/src/main/cpp/wfg/src/vkr_dis.c index f5aaa1929..d41e80953 100644 --- a/app/src/main/cpp/wfg/src/vkr_dis.c +++ b/app/src/main/cpp/wfg/src/vkr_dis.c @@ -194,6 +194,7 @@ struct VkrDis { DisImage splat_key; DisImage fill_cons; DisImage flow_fill; + DisImage flow_prev; DisImage splat_field; // Farneback flow (DIS_ENGINE_FARNEBACK and _HYBRID): each frame's polynomial expansion, @@ -227,6 +228,7 @@ struct VkrDis { VkImageView view_splat_key; VkImageView view_fill_cons; VkImageView view_flow_fill; + VkImageView view_flow_prev; VkImageView view_splat_field; VkImageView view_fb_b[DIS_SLOTS][DIS_MAX_LEVELS]; VkImageView view_fb_c[DIS_SLOTS][DIS_MAX_LEVELS]; @@ -557,6 +559,7 @@ static uint32_t dis_collect_images(VkrDis* d, DisImage** out, uint32_t cap) { DIS_PUSH(&d->splat_key); DIS_PUSH(&d->fill_cons); DIS_PUSH(&d->flow_fill); + DIS_PUSH(&d->flow_prev); DIS_PUSH(&d->splat_field); for (uint32_t s = 0; s < DIS_SLOTS; s++) { DIS_PUSH(&d->fb_b[s]); @@ -767,7 +770,7 @@ static bool dis_create_pipelines(VkrDis* d) { // VR sets exist only for the levels the refinement actually runs on. const uint32_t vr_sets = (DIS_SLOTS + DIS_VR_SHARED_SETS) * DIS_VR_LEVELS; - const uint32_t hist_sets = DIS_SLOTS * 2u + DIS_SLOTS + DIS_SLOTS * 2u; // + splat and fill + const uint32_t hist_sets = DIS_SLOTS * 2u + DIS_SLOTS + DIS_SLOTS * 2u + 1u; // + splat, fill, pack const uint32_t fb_sets = DIS_SLOTS * DIS_MAX_LEVELS * 3u + DIS_MAX_LEVELS + DIS_SLOTS + 1u; const uint32_t vr_layout_sets = vr_sets + hist_sets + fb_sets; const uint32_t total_sets = shared_sets + vr_layout_sets; @@ -869,7 +872,7 @@ static bool dis_create_pipelines(VkrDis* d) { d->pass_fb_hsum.pipeline = dis_create_compute_pipeline_with_layout(d, dis_fb_hsum_comp, dis_fb_hsum_comp_size, d->vr_pipeline_layout, NULL); d->pass_fb_vsolve.pipeline = dis_create_compute_pipeline_with_layout(d, dis_fb_vsolve_comp, dis_fb_vsolve_comp_size, d->vr_pipeline_layout, NULL); d->pass_side.pipeline = dis_create_compute_pipeline(d, dis_side_comp, dis_side_comp_size); - d->pass_pack.pipeline = dis_create_compute_pipeline(d, dis_flow_pack_comp, dis_flow_pack_comp_size); + d->pass_pack.pipeline = dis_create_compute_pipeline_with_layout(d, dis_flow_pack_comp, dis_flow_pack_comp_size, d->vr_pipeline_layout, NULL); d->pass_splat.pipeline = dis_create_compute_pipeline_with_layout(d, dis_splat_comp, dis_splat_comp_size, d->vr_pipeline_layout, NULL); d->pass_fill.pipeline = dis_create_compute_pipeline_with_layout(d, dis_fill_comp, dis_fill_comp_size, d->vr_pipeline_layout, NULL); @@ -1114,6 +1117,7 @@ static void dis_write_all_descriptors(VkrDis* d) { dis_batch_sampled(d, &b, fs, 3, d->view_flow_luma[next][0], d->sampler); dis_batch_sampled(d, &b, fs, 4, d->view_flow_refined[0], d->sampler); dis_batch_sampled(d, &b, fs, 5, d->view_hist[dir], d->sampler); + dis_batch_sampled(d, &b, fs, 6, d->view_flow_prev, d->sampler); dis_batch_storage(d, &b, fs, DIS_VR_FIRST_STORAGE, d->view_fill_cons); dis_batch_storage(d, &b, fs, DIS_VR_FIRST_STORAGE + 1, d->view_flow_fill); } @@ -1168,7 +1172,9 @@ static void dis_write_all_descriptors(VkrDis* d) { // Not per slot: the input and the output are single images, and this set is written // once here and never updated, so sharing it across frames is safe. dis_batch_sampled(d, &b, d->pack_set, 0, d->view_flow_refined[0], d->sampler); - dis_batch_storage(d, &b, d->pack_set, 5, d->view_flow_out); + dis_batch_sampled(d, &b, d->pack_set, 1, d->view_fill_cons, d->sampler); + dis_batch_storage(d, &b, d->pack_set, DIS_VR_FIRST_STORAGE, d->view_flow_out); + dis_batch_storage(d, &b, d->pack_set, DIS_VR_FIRST_STORAGE + 1, d->view_flow_prev); // Farneback. The pair ending in slot s runs from prev to s; a level starts from the level // above (normalised uv reads the same at any size), the coarsest from the previous pair's @@ -1228,6 +1234,7 @@ static void dis_destroy_views(VkrDis* d) { dis_destroy_view(d, &d->view_splat_key); dis_destroy_view(d, &d->view_fill_cons); dis_destroy_view(d, &d->view_flow_fill); + dis_destroy_view(d, &d->view_flow_prev); dis_destroy_view(d, &d->view_splat_field); dis_destroy_view(d, &d->view_fb_l1); for (uint32_t l = 0; l < DIS_MAX_LEVELS; l++) { @@ -1285,6 +1292,7 @@ static void dis_destroy_images(VkrDis* d) { dis_destroy_image(d, &d->splat_key); dis_destroy_image(d, &d->fill_cons); dis_destroy_image(d, &d->flow_fill); + dis_destroy_image(d, &d->flow_prev); dis_destroy_image(d, &d->splat_field); for (uint32_t s = 0; s < DIS_SLOTS; s++) { dis_destroy_image(d, &d->fb_b[s]); @@ -1364,7 +1372,9 @@ static bool dis_create_resources(VkrDis* d, uint32_t w, uint32_t h, uint32_t ful // ones replaced, which is what the interpolation uses (dis_fill.comp). if (!dis_create_image(d, &d->fill_cons, w, h, VK_FORMAT_R32_SFLOAT, 1, VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT)) return false; - if (!dis_create_image(d, &d->flow_fill, w, h, VK_FORMAT_R16G16_SFLOAT, 1, + if (!dis_create_image(d, &d->flow_prev, w, h, VK_FORMAT_R16G16B16A16_SFLOAT, 1, + VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT)) return false; + if (!dis_create_image(d, &d->flow_fill, w, h, VK_FORMAT_R16G16B16A16_SFLOAT, 1, VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT)) return false; // The field splatted to a generated frame's time (dis_splat.comp): the winning priority per // texel (R32_UINT, whose storage atomics every device has) and the vector it carries, one @@ -1425,7 +1435,8 @@ static bool dis_create_resources(VkrDis* d, uint32_t w, uint32_t h, uint32_t ful if (!dis_create_view(d, d->flow_out.image, VK_FORMAT_R16G16_SFLOAT, 0, 1, &d->view_flow_out)) return false; if (!dis_create_view(d, d->splat_key.image, VK_FORMAT_R32_UINT, 0, 1, &d->view_splat_key)) return false; if (!dis_create_view(d, d->fill_cons.image, VK_FORMAT_R32_SFLOAT, 0, 1, &d->view_fill_cons)) return false; - if (!dis_create_view(d, d->flow_fill.image, VK_FORMAT_R16G16_SFLOAT, 0, 1, &d->view_flow_fill)) return false; + if (!dis_create_view(d, d->flow_fill.image, VK_FORMAT_R16G16B16A16_SFLOAT, 0, 1, &d->view_flow_fill)) return false; + if (!dis_create_view(d, d->flow_prev.image, VK_FORMAT_R16G16B16A16_SFLOAT, 0, 1, &d->view_flow_prev)) return false; if (!dis_create_view(d, d->splat_field.image, VK_FORMAT_R16G16B16A16_SFLOAT, 0, 1, &d->view_splat_field)) return false; if (!dis_create_view(d, d->fb_l1.image, VK_FORMAT_R32G32_SFLOAT, 0, 1, &d->view_fb_l1)) return false; @@ -1477,7 +1488,7 @@ static bool dis_allocate_sets(VkrDis* d) { } } - if (!dis_alloc(d, d->set_layout, 1, &d->pack_set)) return false; + if (!dis_alloc(d, d->vr_set_layout, 1, &d->pack_set)) return false; for (uint32_t l = 0; l < DIS_VR_LEVELS; l++) { VkDescriptorSet vr_sets[DIS_VR_SHARED_SETS]; @@ -2192,13 +2203,14 @@ static void dis_farneback(VkrDis* d, VkCommandBuffer cmd, uint32_t slot, uint32_ // One pass of dis_fill.comp: 0 scores how well the next frame confirms each vector, 1 writes // the field with the unconfirmed ones replaced. The caller places the barriers. -static void dis_fill_pass(VkrDis* d, VkCommandBuffer cmd, uint32_t slot, int mode, uint32_t w, - uint32_t h) { +static void dis_fill_pass(VkrDis* d, VkCommandBuffer cmd, uint32_t slot, int mode, int accel, + uint32_t w, uint32_t h) { vkd.CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->pass_fill.pipeline); vkd.CmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->vr_pipeline_layout, 0, 1, &d->fill_sets[slot][d->hist_parity], 0, NULL); - vkd.CmdPushConstants(cmd, d->vr_pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(mode), - &mode); + const struct { int mode; int accel; } fpc = {mode, accel}; + vkd.CmdPushConstants(cmd, d->vr_pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(fpc), + &fpc); vkd.CmdDispatch(cmd, (w + DIS_LOCAL_SIZE - 1) / DIS_LOCAL_SIZE, (h + DIS_LOCAL_SIZE - 1) / DIS_LOCAL_SIZE, 1); } @@ -2390,8 +2402,14 @@ void vkr_dis_process(VkrDis* d, VkCommandBuffer cmd, VkImage source, uint32_t wi // The packed field, how well the next frame confirms each vector (both from the refined // field) and the overlay history (from the colour pair) are independent of each other, // so they share one barrier. - dis_dispatch(d, cmd, d->pass_pack.pipeline, d->pack_set, w, h); - dis_fill_pass(d, cmd, slot, 0, w, h); + // The pack keeps the previous pair's field for the fill's acceleration estimate; it is + // that pair's only when that pair computed one. + const int had_prior = d->prior_valid ? 1 : 0; + vkd.CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->pass_pack.pipeline); + vkd.CmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->vr_pipeline_layout, 0, 1, + &d->pack_set, 0, NULL); + vkd.CmdDispatch(cmd, (w + DIS_LOCAL_SIZE - 1) / DIS_LOCAL_SIZE, + (h + DIS_LOCAL_SIZE - 1) / DIS_LOCAL_SIZE, 1); d->prior_valid = true; vkd.CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, d->pass_hist.pipeline); @@ -2405,8 +2423,12 @@ void vkr_dis_process(VkrDis* d, VkCommandBuffer cmd, VkImage source, uint32_t wi d->hist_valid = true; dis_compute_barrier(cmd); - // The field the interpolation uses, its unconfirmed vectors replaced (dis_fill.comp). - dis_fill_pass(d, cmd, slot, 1, w, h); + // How well the next frame confirms each vector - after the pack has kept the previous + // pair's - then the field the interpolation uses, unconfirmed vectors replaced + // (dis_fill.comp). + dis_fill_pass(d, cmd, slot, 0, had_prior, w, h); + dis_compute_barrier(cmd); + dis_fill_pass(d, cmd, slot, 1, had_prior, w, h); dis_compute_barrier(cmd); // Which real frame a true occlusion takes, and how sure, per level-0 texel. From 28c04f43b648f19eeffb9b5330a6867d8dc3d1bc Mon Sep 17 00:00:00 2001 From: qwertypower <1qwertypower1@gmail.com> Date: Wed, 14 Oct 2026 22:06:48 +0300 Subject: [PATCH 14/18] WFG: no frame generation from a game below 20 fps --- app/src/main/cpp/wfg/src/vkr_dis.c | 15 +++++++++++++-- docs/FRAME-GENERATION.md | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/src/main/cpp/wfg/src/vkr_dis.c b/app/src/main/cpp/wfg/src/vkr_dis.c index d41e80953..0f98e1fd7 100644 --- a/app/src/main/cpp/wfg/src/vkr_dis.c +++ b/app/src/main/cpp/wfg/src/vkr_dis.c @@ -100,6 +100,10 @@ #define DIS_RATIO_HYST 0.10f +// Source rate below which frame generation stops, and above which it starts again. +#define DIS_MIN_SOURCE_FPS_OFF 20.0f +#define DIS_MIN_SOURCE_FPS_ON 24.0f + #define DIS_VR_ALPHA 20.0f #define DIS_VR_DELTA 5.0f #define DIS_VR_GAMMA 10.0f @@ -1970,8 +1974,15 @@ uint32_t vkr_dis_plan(VkrDis* d, uint32_t capacity, uint64_t source_frames) { } const int cur = d->planned_gen > (int)capacity ? (int)capacity : d->planned_gen; - const bool generate = cur > 0 ? (ratio >= DIS_MIN_GEN_RATIO - DIS_RATIO_HYST) - : (ratio >= DIS_MIN_GEN_RATIO); + // Not from a game below ~20 fps. Measured from 15, 20, 24 and 30 fps to 120 Hz: a fast camera + // turn from 24 fps still interpolates (19.96 dB against 12.4 for repeating the frame), from + // 20 fps it collapses to the repeat (13.1 against 12.1) - the motion between two real frames + // outruns the search - and every generated frame also waits for the next real one, 50-66 ms + // there. Off below 20, back on above 24, so a game around the limit does not flicker on and off. + const bool fast_enough = cur > 0 ? source_rate >= DIS_MIN_SOURCE_FPS_OFF + : source_rate >= DIS_MIN_SOURCE_FPS_ON; + const bool generate = fast_enough && (cur > 0 ? (ratio >= DIS_MIN_GEN_RATIO - DIS_RATIO_HYST) + : (ratio >= DIS_MIN_GEN_RATIO)); int raw = 0; if (generate) { diff --git a/docs/FRAME-GENERATION.md b/docs/FRAME-GENERATION.md index 00ea4a592..3fb67347d 100644 --- a/docs/FRAME-GENERATION.md +++ b/docs/FRAME-GENERATION.md @@ -42,7 +42,7 @@ that ship with the APK — so it works on a fresh install with no Steam account | Adaptive Target | LSFG | Aim for a specific output rate (60/90/120/144/165) instead of a fixed multiplier | | Flow Scale | LSFG | 25–100%, resolution of the optical-flow pyramid; lower is cheaper and softer | | Resolution Scale | WFG | Fast / Balance — 180 / 252 pixels on the frame's **shorter** side | -| Target FPS | WFG | Max refresh rate, or a specific one (60/90/120/144/165) | +| Target FPS | WFG | Max refresh rate, or a specific one (60/90/120/144/165). Generation pauses while the game runs below ~20 fps and resumes above 24 fps: below that a fast camera turn outruns the motion search and every frame waits 50+ ms for the next real one | | Show flow | WFG | Debug view: renders the estimated motion field instead of the frame | | FPS Limiter | Both | Caps the game's own frame rate, from 15 fps upward | From 981d18739a4f588cb0e7b0023db38aaac79bb719 Mon Sep 17 00:00:00 2001 From: qwertypower <1qwertypower1@gmail.com> Date: Sun, 18 Oct 2026 20:28:34 +0300 Subject: [PATCH 15/18] WFG: relicense the module under MIT --- CREDITS.md | 8 +++-- app/src/main/cpp/wfg/CMakeLists.txt | 8 ++--- app/src/main/cpp/wfg/LICENSE | 29 +++++++++++++++++++ .../cpp/wfg/include/shaders/dis_densify.comp | 2 +- .../cpp/wfg/include/shaders/dis_fb_hsum.comp | 2 +- .../cpp/wfg/include/shaders/dis_fb_poly.comp | 2 +- .../wfg/include/shaders/dis_fb_vsolve.comp | 2 +- .../cpp/wfg/include/shaders/dis_fill.comp | 2 +- .../wfg/include/shaders/dis_flow_pack.comp | 2 +- .../cpp/wfg/include/shaders/dis_gradient.comp | 2 +- .../cpp/wfg/include/shaders/dis_hist.comp | 2 +- .../wfg/include/shaders/dis_interpolate.comp | 2 +- .../include/shaders/dis_inverse_search.comp | 2 +- .../cpp/wfg/include/shaders/dis_luma_r16.comp | 2 +- .../cpp/wfg/include/shaders/dis_luma_r32.comp | 2 +- .../wfg/include/shaders/dis_propagate.comp | 2 +- .../cpp/wfg/include/shaders/dis_side.comp | 2 +- .../cpp/wfg/include/shaders/dis_splat.comp | 2 +- .../cpp/wfg/include/shaders/dis_vr_add.comp | 2 +- .../cpp/wfg/include/shaders/dis_vr_coef.comp | 2 +- .../cpp/wfg/include/shaders/dis_vr_d1.comp | 2 +- .../cpp/wfg/include/shaders/dis_vr_d2.comp | 2 +- .../cpp/wfg/include/shaders/dis_vr_prep.comp | 2 +- .../cpp/wfg/include/shaders/dis_vr_sor.comp | 2 +- .../cpp/wfg/include/shaders/dis_vr_w.comp | 2 +- app/src/main/cpp/wfg/include/vkr_dis.h | 2 +- app/src/main/cpp/wfg/src/vkr_dis.c | 2 +- docs/FRAME-GENERATION.md | 2 +- 28 files changed, 63 insertions(+), 32 deletions(-) create mode 100644 app/src/main/cpp/wfg/LICENSE diff --git a/CREDITS.md b/CREDITS.md index 93a28b092..767f81562 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -57,8 +57,8 @@ corresponding source for every copyleft component is available from the projects [Eden Emulator Project](https://git.eden-emu.dev/eden-emu/eden) (GPL-3.0-or-later) — the Vulkan port of that chain that WinNative's frame generation is derived from. See [what came from Camille LaVey's Eden port](#frame-generation--what-came-from-camille-laveys-eden-port) -- **WFG** (WinNative/Winlator Frame Generator, GPL-3.0; DIS optical flow) by **qwertypower** - ([DEVAR Entertainment LLC](https://devar.ai/)) (GPL-3.0) — a complete open-source implementation +- **WFG** (WinNative/Winlator Frame Generator; DIS optical flow) by **qwertypower** + ([DEVAR Entertainment LLC](https://devar.ai/)) (MIT) — a complete open-source implementation of a Dense Inverse Search frame generator, the second frame generation engine in WinNative and the one that needs no shaders from anywhere else. See [the fully open-source engine](#dis-frame-generation--the-fully-open-source-engine) @@ -122,7 +122,9 @@ wiring the chain into WinNative's compositor and swapchain (`vkr_lsfg.*`). ## WFG (WinNative/Winlator Frame Generator) — the fully open-source engine WinNative's second frame generator is a complete open-source implementation of **Dense Inverse -Search** optical flow, contributed by **qwertypower** (DEVAR Entertainment LLC) under GPL-3.0. +Search** optical flow, contributed by **qwertypower** (DEVAR Entertainment LLC) under the MIT +license (`app/src/main/cpp/wfg/LICENSE`); the rest of WinNative, including the compositor code that +calls it, stays GPL-3.0. Unlike the Lossless Scaling path it depends on nothing the user has to own or install. The whole chain ships with the APK as fourteen compute shaders and runs in the same Vulkan compositor, so diff --git a/app/src/main/cpp/wfg/CMakeLists.txt b/app/src/main/cpp/wfg/CMakeLists.txt index 7637a5a3d..1a0b30a49 100644 --- a/app/src/main/cpp/wfg/CMakeLists.txt +++ b/app/src/main/cpp/wfg/CMakeLists.txt @@ -1,7 +1,7 @@ -# WFG (WinNative/Winlator Frame Generator, GPL-3.0): Dense Inverse Search (DIS) optical flow and -# the frame interpolator built on it, as one static library. libwinlator (X11 compositor, -# standalone presenter) and libwnwayland (Wayland compositor) both link it; each keeps its own -# VkDispatch, which the objects here resolve against at link time. +# WFG (WinNative/Winlator Frame Generator, MIT - see LICENSE here): Dense Inverse Search (DIS) +# optical flow and the frame interpolator built on it, as one static library. libwinlator (X11 +# compositor, standalone presenter) and libwnwayland (Wayland compositor) both link it; each keeps +# its own VkDispatch, which the objects here resolve against at link time. # # Layout: # include/vkr_dis.h public API diff --git a/app/src/main/cpp/wfg/LICENSE b/app/src/main/cpp/wfg/LICENSE new file mode 100644 index 000000000..84686156c --- /dev/null +++ b/app/src/main/cpp/wfg/LICENSE @@ -0,0 +1,29 @@ +MIT License + +Copyright (c) 2026 qwertypower (DEVAR Entertainment LLC) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + +The algorithms implemented here come from published work and their reference code: +Dense Inverse Search optical flow (Till Kroeger et al., OF_DIS; OpenCV DISOpticalFlow) and +Farneback's polynomial-expansion optical flow (OpenCV optflowgf.cpp). OpenCV is licensed under +the Apache License 2.0: https://github.com/opencv/opencv/blob/4.x/LICENSE +See CREDITS.md at the repository root for the full attribution. diff --git a/app/src/main/cpp/wfg/include/shaders/dis_densify.comp b/app/src/main/cpp/wfg/include/shaders/dis_densify.comp index 110a13800..4c4ba2f45 100644 --- a/app/src/main/cpp/wfg/include/shaders/dis_densify.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_densify.comp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT // // WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from diff --git a/app/src/main/cpp/wfg/include/shaders/dis_fb_hsum.comp b/app/src/main/cpp/wfg/include/shaders/dis_fb_hsum.comp index a81aa52fb..62b72e20d 100644 --- a/app/src/main/cpp/wfg/include/shaders/dis_fb_hsum.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_fb_hsum.comp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT // // WFG, Farneback flow: the per-texel displacement equations of one iteration, summed // across the window horizontally (dis_fb_vsolve sums them vertically and solves). diff --git a/app/src/main/cpp/wfg/include/shaders/dis_fb_poly.comp b/app/src/main/cpp/wfg/include/shaders/dis_fb_poly.comp index 5b22617e1..cc622529c 100644 --- a/app/src/main/cpp/wfg/include/shaders/dis_fb_poly.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_fb_poly.comp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT // // WFG, Farneback flow: polynomial expansion of one pyramid level of a frame. // diff --git a/app/src/main/cpp/wfg/include/shaders/dis_fb_vsolve.comp b/app/src/main/cpp/wfg/include/shaders/dis_fb_vsolve.comp index 4eb6beced..39e2d3741 100644 --- a/app/src/main/cpp/wfg/include/shaders/dis_fb_vsolve.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_fb_vsolve.comp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT // // WFG, Farneback flow: sums the horizontally summed equations of dis_fb_hsum vertically // across the window and solves G d = h for this texel's displacement. diff --git a/app/src/main/cpp/wfg/include/shaders/dis_fill.comp b/app/src/main/cpp/wfg/include/shaders/dis_fill.comp index 1e50fa29f..852147757 100644 --- a/app/src/main/cpp/wfg/include/shaders/dis_fill.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_fill.comp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT // // WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from diff --git a/app/src/main/cpp/wfg/include/shaders/dis_flow_pack.comp b/app/src/main/cpp/wfg/include/shaders/dis_flow_pack.comp index 87be67080..69025f193 100644 --- a/app/src/main/cpp/wfg/include/shaders/dis_flow_pack.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_flow_pack.comp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT // // WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from diff --git a/app/src/main/cpp/wfg/include/shaders/dis_gradient.comp b/app/src/main/cpp/wfg/include/shaders/dis_gradient.comp index ed175a5e1..1b8e22e2d 100644 --- a/app/src/main/cpp/wfg/include/shaders/dis_gradient.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_gradient.comp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT // // WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from diff --git a/app/src/main/cpp/wfg/include/shaders/dis_hist.comp b/app/src/main/cpp/wfg/include/shaders/dis_hist.comp index 5c78678b2..ef03a1072 100644 --- a/app/src/main/cpp/wfg/include/shaders/dis_hist.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_hist.comp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT // // WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from diff --git a/app/src/main/cpp/wfg/include/shaders/dis_interpolate.comp b/app/src/main/cpp/wfg/include/shaders/dis_interpolate.comp index 04c780d9e..bd998f9b1 100644 --- a/app/src/main/cpp/wfg/include/shaders/dis_interpolate.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_interpolate.comp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT // // WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from diff --git a/app/src/main/cpp/wfg/include/shaders/dis_inverse_search.comp b/app/src/main/cpp/wfg/include/shaders/dis_inverse_search.comp index 6bad0c56f..411687f5b 100644 --- a/app/src/main/cpp/wfg/include/shaders/dis_inverse_search.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_inverse_search.comp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT // // WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from diff --git a/app/src/main/cpp/wfg/include/shaders/dis_luma_r16.comp b/app/src/main/cpp/wfg/include/shaders/dis_luma_r16.comp index 505644b3c..b8c20c35f 100644 --- a/app/src/main/cpp/wfg/include/shaders/dis_luma_r16.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_luma_r16.comp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT // // WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from diff --git a/app/src/main/cpp/wfg/include/shaders/dis_luma_r32.comp b/app/src/main/cpp/wfg/include/shaders/dis_luma_r32.comp index 9391612ce..780924be4 100644 --- a/app/src/main/cpp/wfg/include/shaders/dis_luma_r32.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_luma_r32.comp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT // // WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from diff --git a/app/src/main/cpp/wfg/include/shaders/dis_propagate.comp b/app/src/main/cpp/wfg/include/shaders/dis_propagate.comp index eae27ce2c..864412047 100644 --- a/app/src/main/cpp/wfg/include/shaders/dis_propagate.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_propagate.comp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT // // WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from diff --git a/app/src/main/cpp/wfg/include/shaders/dis_side.comp b/app/src/main/cpp/wfg/include/shaders/dis_side.comp index 4e29a5a18..179df4ddd 100644 --- a/app/src/main/cpp/wfg/include/shaders/dis_side.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_side.comp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT // // WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from diff --git a/app/src/main/cpp/wfg/include/shaders/dis_splat.comp b/app/src/main/cpp/wfg/include/shaders/dis_splat.comp index cd0c1592d..ee7a2e786 100644 --- a/app/src/main/cpp/wfg/include/shaders/dis_splat.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_splat.comp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT // // WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from diff --git a/app/src/main/cpp/wfg/include/shaders/dis_vr_add.comp b/app/src/main/cpp/wfg/include/shaders/dis_vr_add.comp index 9c3e6ba96..e5ff966d9 100644 --- a/app/src/main/cpp/wfg/include/shaders/dis_vr_add.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_vr_add.comp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT // // WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from diff --git a/app/src/main/cpp/wfg/include/shaders/dis_vr_coef.comp b/app/src/main/cpp/wfg/include/shaders/dis_vr_coef.comp index 48dc6de11..43d953f66 100644 --- a/app/src/main/cpp/wfg/include/shaders/dis_vr_coef.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_vr_coef.comp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT // // WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from diff --git a/app/src/main/cpp/wfg/include/shaders/dis_vr_d1.comp b/app/src/main/cpp/wfg/include/shaders/dis_vr_d1.comp index 2351010d0..8302cc7b8 100644 --- a/app/src/main/cpp/wfg/include/shaders/dis_vr_d1.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_vr_d1.comp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT // // WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from diff --git a/app/src/main/cpp/wfg/include/shaders/dis_vr_d2.comp b/app/src/main/cpp/wfg/include/shaders/dis_vr_d2.comp index 9966f3c94..645e7757c 100644 --- a/app/src/main/cpp/wfg/include/shaders/dis_vr_d2.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_vr_d2.comp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT // // WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from diff --git a/app/src/main/cpp/wfg/include/shaders/dis_vr_prep.comp b/app/src/main/cpp/wfg/include/shaders/dis_vr_prep.comp index 357a8b79b..77bf24711 100644 --- a/app/src/main/cpp/wfg/include/shaders/dis_vr_prep.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_vr_prep.comp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT // // WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from diff --git a/app/src/main/cpp/wfg/include/shaders/dis_vr_sor.comp b/app/src/main/cpp/wfg/include/shaders/dis_vr_sor.comp index 913e565b1..106e744df 100644 --- a/app/src/main/cpp/wfg/include/shaders/dis_vr_sor.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_vr_sor.comp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT // // WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from diff --git a/app/src/main/cpp/wfg/include/shaders/dis_vr_w.comp b/app/src/main/cpp/wfg/include/shaders/dis_vr_w.comp index 69ab98580..2142928c0 100644 --- a/app/src/main/cpp/wfg/include/shaders/dis_vr_w.comp +++ b/app/src/main/cpp/wfg/include/shaders/dis_vr_w.comp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT // // WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from diff --git a/app/src/main/cpp/wfg/include/vkr_dis.h b/app/src/main/cpp/wfg/include/vkr_dis.h index fe8ff5991..ce4e6657d 100644 --- a/app/src/main/cpp/wfg/include/vkr_dis.h +++ b/app/src/main/cpp/wfg/include/vkr_dis.h @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT // // WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from diff --git a/app/src/main/cpp/wfg/src/vkr_dis.c b/app/src/main/cpp/wfg/src/vkr_dis.c index 0f98e1fd7..be54f3083 100644 --- a/app/src/main/cpp/wfg/src/vkr_dis.c +++ b/app/src/main/cpp/wfg/src/vkr_dis.c @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC) -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT // // WFG frame generation: a Vulkan compute realisation of Dense Inverse Search // optical flow. The algorithm and its reference implementation come from diff --git a/docs/FRAME-GENERATION.md b/docs/FRAME-GENERATION.md index 3fb67347d..dd4289342 100644 --- a/docs/FRAME-GENERATION.md +++ b/docs/FRAME-GENERATION.md @@ -28,7 +28,7 @@ the shaders import successfully. ## WFG — WinNative/Winlator Frame Generator -**Nothing to buy, nothing to import.** WFG (WinNative/Winlator Frame Generator, GPL-3.0) is a +**Nothing to buy, nothing to import.** WFG (WinNative/Winlator Frame Generator, MIT) is a complete open-source Dense Inverse Search frame generator built into WinNative — compute shaders that ship with the APK — so it works on a fresh install with no Steam account and no `Lossless.dll`. Turn it on in the **FG** tab and it runs. From 8a54578ad4486b90be2f93f82921c83aee8df1ae Mon Sep 17 00:00:00 2001 From: qwertypower <1qwertypower1@gmail.com> Date: Wed, 21 Oct 2026 21:00:09 +0300 Subject: [PATCH 16/18] CREDITS: WFG authorship in the DIS and Farneback entries, current shader table --- CREDITS.md | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/CREDITS.md b/CREDITS.md index 767f81562..c676c7380 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -61,11 +61,16 @@ corresponding source for every copyleft component is available from the projects ([DEVAR Entertainment LLC](https://devar.ai/)) (MIT) — a complete open-source implementation of a Dense Inverse Search frame generator, the second frame generation engine in WinNative and the one that needs no shaders from anywhere else. - See [the fully open-source engine](#dis-frame-generation--the-fully-open-source-engine) -- **DIS optical flow** — the algorithm and its reference implementation come from + See [the fully open-source engine](#wfg-winnativewinlator-frame-generator--the-fully-open-source-engine) +- **DIS optical flow** — implemented in WFG by **qwertypower** (DEVAR Entertainment LLC) as Vulkan + compute shaders; the algorithm and its reference implementation come from [OpenCV](https://github.com/opencv/opencv) (`DISOpticalFlow`, [LICENSE](https://github.com/opencv/opencv/blob/5.x/LICENSE)), which adopted Till Kroeger's original [OF_DIS](https://github.com/tikroeger/OF_DIS) +- **Farneback optical flow** — implemented in WFG by **qwertypower** (DEVAR Entertainment LLC) as + the coarse levels of its hybrid estimator; Gunnar Farnebäck's polynomial-expansion method, ported + from OpenCV's `calcOpticalFlowFarneback` (`optflowgf.cpp`, + [LICENSE](https://github.com/opencv/opencv/blob/5.x/LICENSE)) - **DXVK** by [Philip Rebohle and contributors](https://github.com/doitsujin/dxvk) (zlib/libpng) — the `dxbc` shader translator, vendored at `app/src/main/cpp/thirdparty/dxbc` to convert the frame generation shaders to SPIR-V @@ -127,26 +132,33 @@ license (`app/src/main/cpp/wfg/LICENSE`); the rest of WinNative, including the c calls it, stays GPL-3.0. Unlike the Lossless Scaling path it depends on nothing the user has to own or install. The whole -chain ships with the APK as fourteen compute shaders and runs in the same Vulkan compositor, so +chain ships with the APK as twenty-two compute shaders and runs in the same Vulkan compositor, so frame generation is available on a fresh install with no Steam account and no `Lossless.dll`. -The algorithm is DIS, and its reference implementation is OpenCV's `DISOpticalFlow` +The flow estimator is a hybrid of two published methods. The fine levels are DIS, whose reference +implementation is OpenCV's `DISOpticalFlow` ([OpenCV LICENSE](https://github.com/opencv/opencv/blob/5.x/LICENSE)). OpenCV in turn adopted -Till Kroeger's original [OF_DIS](https://github.com/tikroeger/OF_DIS). What is original here is -the Vulkan compute realisation of it — the pyramid, the descriptor and barrier layout, the -sparse-to-dense step and the pacing — built to run inside a mobile compositor at frame rate. +Till Kroeger's original [OF_DIS](https://github.com/tikroeger/OF_DIS). The coarse levels are +Gunnar Farnebäck's polynomial-expansion flow, ported from OpenCV's `calcOpticalFlowFarneback`. +What is original here, by **qwertypower**, is the Vulkan compute realisation of both and the hybrid +itself — the pyramid, the descriptor and barrier layout, the sparse-to-dense step, the +interpolation (occlusion handling, forward splatting, acceleration) and the pacing — built to run +inside a mobile compositor at frame rate. | Stage | Shaders | | --- | --- | -| Pyramid and gradients | `dis_gradient` | -| Patch inverse search, coarse to fine | `dis_inverse_search`, `dis_propagate` | +| Luminance pyramid and gradients | `dis_luma_r16`, `dis_luma_r32`, `dis_gradient` | +| Farneback on the coarse levels | `dis_fb_poly`, `dis_fb_hsum`, `dis_fb_vsolve` | +| Patch inverse search on the fine levels | `dis_inverse_search`, `dis_propagate` | | Sparse grid to dense flow | `dis_densify` | | Variational refinement | `dis_vr_prep`, `dis_vr_d1`, `dis_vr_d2`, `dis_vr_w`, `dis_vr_coef`, `dis_vr_sor`, `dis_vr_add` | +| Packed field, occlusion fill, acceleration | `dis_flow_pack`, `dis_fill` | +| Overlay history, occlusion side, splat | `dis_hist`, `dis_side`, `dis_splat` | | Warp to the in-between frame | `dis_interpolate` | -The engine sits behind its own settings — a Fast / Balance / Quality flow resolution and an -optional target frame rate — kept separate from the Lossless Scaling ones, and the two engines -are mutually exclusive because the compositor drives one interpolator per frame. +The engine sits behind its own settings — a Fast / Balance flow resolution and an optional target +frame rate — kept separate from the Lossless Scaling ones, and the two engines are mutually +exclusive because the compositor drives one interpolator per frame. ## DirectAudio — what came from The412Banner's driver From 0f09ba5602aab656560d30ccb4d16a5555cfeb79 Mon Sep 17 00:00:00 2001 From: qwertypower <1qwertypower1@gmail.com> Date: Wed, 21 Oct 2026 21:02:40 +0300 Subject: [PATCH 17/18] Credits: WFG and its MIT license in the license table and README --- EMULATOR_CREDITS.md | 21 ++++++++++++++++++++- README.md | 2 ++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/EMULATOR_CREDITS.md b/EMULATOR_CREDITS.md index 1d23bf15a..a86d38f06 100644 --- a/EMULATOR_CREDITS.md +++ b/EMULATOR_CREDITS.md @@ -60,7 +60,11 @@ Each core is shipped as an unmodified `arm64-v8a` build and loaded through Libre ## Frame generation -Frame generation is a port of the Lossless Scaling compute chain to Vulkan. WinNative did not +WinNative has two frame generation engines. + +### LSFG + +LSFG is a port of the Lossless Scaling compute chain to Vulkan. WinNative did not port it from scratch: it derives from **Camille LaVey**'s port in the **Eden Emulator Project**, which in turn derives from **lsfg-vk**. Both are GPL-3.0-or-later, and both copyright notices are preserved in the header of every file that carries their work. @@ -81,6 +85,21 @@ probing (`lsfg_probe`) and compositor integration (`vkr_lsfg`). The frame generation shaders themselves are **not** redistributed. They are read at runtime from the user's own Lossless Scaling installation, which they must own separately on Steam. +### WFG + +**WFG** (WinNative/Winlator Frame Generator) is the second engine, written for WinNative by +**qwertypower** (DEVAR Entertainment LLC) and licensed under the **MIT** license — see +[`app/src/main/cpp/wfg/LICENSE`](app/src/main/cpp/wfg/LICENSE). It is a Vulkan compute +implementation of two published optical-flow methods plus the frame interpolation built on them, +and ships with the APK: nothing is read from anywhere else. The compositor code that calls it is +part of WinNative and stays GPL-3.0. + +| Component | Role | License | Source | +| --- | --- | --- | --- | +| WFG (qwertypower, DEVAR Entertainment LLC) | Hybrid Farneback/DIS optical flow, occlusion handling, interpolation and pacing (`app/src/main/cpp/wfg`) | MIT | this repository | +| OpenCV (`DISOpticalFlow`, `calcOpticalFlowFarneback`) | Reference implementations WFG follows for DIS and Farneback | Apache-2.0 | https://github.com/opencv/opencv | +| Dense Inverse Search (Kroeger et al., ECCV 2016) | The DIS algorithm; no code from the authors' OF_DIS (GPL-3.0) is used | — | https://github.com/tikroeger/OF_DIS | + ## Frontend, achievements, and supporting libraries | Component | Role | License | Source | diff --git a/README.md b/README.md index 857640938..0ad50fc0e 100644 --- a/README.md +++ b/README.md @@ -71,3 +71,5 @@ acknowledgments, including exactly which files came from which upstream project, GPL corresponding-source statement. WinNative is released under the **GNU General Public License v3.0** — see [LICENSE](LICENSE). +The WFG frame generator in `app/src/main/cpp/wfg` is by **qwertypower** (DEVAR Entertainment LLC) and +is MIT-licensed — see [its LICENSE](app/src/main/cpp/wfg/LICENSE). From a05a7e66d539db861b8f2f3dd6c4881b6105b03f Mon Sep 17 00:00:00 2001 From: qwertypower <1qwertypower1@gmail.com> Date: Fri, 30 Oct 2026 19:31:24 +0300 Subject: [PATCH 18/18] Framegen: keep the present queue short, no generation on draws without a new game frame --- .../cpp/winlator/vk/framegen/fg_present.c | 43 +++++++++++- app/src/main/cpp/winlator/vk/vk_renderer.c | 67 ++++++++++++++++++- app/src/main/cpp/winlator/vk/vk_state.h | 8 +++ 3 files changed, 114 insertions(+), 4 deletions(-) diff --git a/app/src/main/cpp/winlator/vk/framegen/fg_present.c b/app/src/main/cpp/winlator/vk/framegen/fg_present.c index 83bab5dc9..7e29ca63b 100644 --- a/app/src/main/cpp/winlator/vk/framegen/fg_present.c +++ b/app/src/main/cpp/winlator/vk/framegen/fg_present.c @@ -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); @@ -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]; @@ -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) { @@ -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); @@ -983,6 +1012,15 @@ 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) { @@ -990,12 +1028,13 @@ static void fg_record_and_present(FgPresenter* fg, FgImport* source, AImage* ima 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; diff --git a/app/src/main/cpp/winlator/vk/vk_renderer.c b/app/src/main/cpp/winlator/vk/vk_renderer.c index 16f71411c..f226fe724 100644 --- a/app/src/main/cpp/winlator/vk/vk_renderer.c +++ b/app/src/main/cpp/winlator/vk/vk_renderer.c @@ -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; @@ -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; @@ -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; @@ -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++) { @@ -2951,7 +2995,8 @@ 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, @@ -2959,7 +3004,13 @@ static bool record_and_submit_frame(VkRenderer* r) { (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; } } @@ -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}; @@ -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) { diff --git a/app/src/main/cpp/winlator/vk/vk_state.h b/app/src/main/cpp/winlator/vk/vk_state.h index d424053ac..703cf9370 100644 --- a/app/src/main/cpp/winlator/vk/vk_state.h +++ b/app/src/main/cpp/winlator/vk/vk_state.h @@ -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;