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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CREDITS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
- **DIS optical flow frame generation** by **qwertypower**
- **OpenFlow 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.
Expand Down Expand Up @@ -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.*`).

## DIS frame generation — the fully open-source engine
## OpenFlow 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.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 DIS |
| 🎞️ **Frame generation** | [docs/FRAME-GENERATION.md](docs/FRAME-GENERATION.md) — LSFG and OpenFlow |
| 🖥️ **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) |
Expand Down Expand Up @@ -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 DIS engine), **OpenCV** and
**qwertypower** of **DEVAR Entertainment LLC** (the open-source OpenFlow engine), **OpenCV** and
Till Kroeger (the DIS algorithm), **DXVK** (the `dxbc` translator), and **The412Banner**
(DirectAudio and Steam Controller support), and the **SDL** contributors.

Expand Down
6 changes: 3 additions & 3 deletions app/src/main/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,8 @@ endforeach()

add_custom_target(winlator_shaders DEPENDS ${SHADER_HEADERS})

# DIS frame generator (optical flow + interpolation), linked by libwinlator and libwnwayland.
add_subdirectory(dis)
# OpenFlow frame generator (DIS optical flow + interpolation), linked by libwinlator and libwnwayland.
add_subdirectory(openflow)

# ----------------------------------------------------------------------------
# Winlator native library (X-server, AHB, Vulkan compositor, helpers)
Expand Down Expand Up @@ -187,7 +187,7 @@ target_include_directories(winlator PRIVATE
target_compile_features(winlator PRIVATE cxx_std_17)

target_link_libraries(winlator
wndis
openflow
log
android
mediandk
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# DIS frame generator: Dense Inverse Search 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.
# OpenFlow 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.
#
# Layout:
# include/vkr_dis.h public API
Expand Down Expand Up @@ -50,24 +50,24 @@ foreach(base ${DIS_SHADERS})
-DVAR_NAME=${var}
-P "${BIN2C_SCRIPT}"
DEPENDS "${input}" "${BIN2C_SCRIPT}"
COMMENT "Compiling DIS shader ${base}.comp -> ${var}.spv.h"
COMMENT "Compiling OpenFlow shader ${base}.comp -> ${var}.spv.h"
VERBATIM
)
list(APPEND DIS_SHADER_HEADERS "${hdr}")
endforeach()

add_custom_target(wndis_shaders DEPENDS ${DIS_SHADER_HEADERS})
add_custom_target(openflow_shaders DEPENDS ${DIS_SHADER_HEADERS})

add_library(wndis STATIC
add_library(openflow STATIC
src/vkr_dis.c
src/dis_qcom_me.c
)
add_dependencies(wndis wndis_shaders)
add_dependencies(openflow openflow_shaders)

set_target_properties(wndis PROPERTIES POSITION_INDEPENDENT_CODE ON)
target_compile_options(wndis PRIVATE -Wall -Wextra -fvisibility=hidden)
target_compile_definitions(wndis PUBLIC VK_USE_PLATFORM_ANDROID_KHR)
target_include_directories(wndis
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
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
# vk_dispatch.h: the public header takes its Vulkan types from there.
Expand All @@ -76,4 +76,4 @@ target_include_directories(wndis
${CMAKE_CURRENT_BINARY_DIR}
)
# EGL/GLES are opened with dlopen by dis_qcom_me.c, so nothing links them here.
target_link_libraries(wndis PRIVATE log dl)
target_link_libraries(openflow PRIVATE log dl)
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC)
// SPDX-License-Identifier: GPL-3.0-or-later
//
// DIS frame generation: a Vulkan compute realisation of Dense Inverse Search
// OpenFlow 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.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC)
// SPDX-License-Identifier: GPL-3.0-or-later
//
// DIS frame generation: a Vulkan compute realisation of Dense Inverse Search
// OpenFlow 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.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC)
// SPDX-License-Identifier: GPL-3.0-or-later
//
// DIS frame generation: a Vulkan compute realisation of Dense Inverse Search
// OpenFlow 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.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC)
// SPDX-License-Identifier: GPL-3.0-or-later
//
// DIS frame generation: a Vulkan compute realisation of Dense Inverse Search
// OpenFlow 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.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC)
// SPDX-License-Identifier: GPL-3.0-or-later
//
// DIS frame generation: a Vulkan compute realisation of Dense Inverse Search
// OpenFlow 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.
Expand Down Expand Up @@ -132,7 +132,13 @@ void main() {
vec2 texel = 1.0 / vec2(size);

const float GUARD_PX = 20.0;
vec2 guard = GUARD_PX * texel;
// Where content leaves the frame between the pair, its patches have nothing to match, so
// the field is unreliable in a band as wide as the motion itself - on a fast camera turn
// far wider than the fixed guard. The band grows by the motion measured well inside the
// frame, and across it the field is taken from its inner edge.
vec2 fDeep = sampleFlow(clamp(uv, vec2(0.15), vec2(0.85)));
if (any(isnan(fDeep)) || any(isinf(fDeep))) fDeep = vec2(0.0);
vec2 guard = GUARD_PX * texel + min(abs(fDeep), vec2(0.25));
vec2 dEdge = min(uv, 1.0 - uv);
vec2 ramp = clamp((dEdge - guard) / max(guard, texel), 0.0, 1.0);
vec2 eased = ramp * ramp * (3.0 - 2.0 * ramp);
Expand Down Expand Up @@ -193,12 +199,15 @@ void main() {
vec3 c1 = warpSample(nextColor, uv1, vec2(size));


// How far each sample is from being inside its frame. The feather lies inside the frame: a
// sample past the edge has nothing (the fetch is clamped to the edge column), so it gets no
// weight at all, and one just inside it gives way gradually.
const float FEATHER_PX = 8.0;
vec2 feather = FEATHER_PX / vec2(size);
vec2 e0 = max(max(-uv0, uv0 - vec2(1.0)), vec2(0.0)) / feather;
vec2 e1 = max(max(-uv1, uv1 - vec2(1.0)), vec2(0.0)) / feather;
float out0 = clamp(max(e0.x, e0.y), 0.0, 1.0);
float out1 = clamp(max(e1.x, e1.y), 0.0, 1.0);
vec2 d0 = min(uv0, vec2(1.0) - uv0) / feather;
vec2 d1 = min(uv1, vec2(1.0) - uv1) / feather;
float out0 = 1.0 - clamp(min(d0.x, d0.y), 0.0, 1.0);
float out1 = 1.0 - clamp(min(d1.x, d1.y), 0.0, 1.0);

float w0 = (1.0 - pc.t) * (1.0 - out0);
float w1 = pc.t * (1.0 - out1);
Expand All @@ -209,9 +218,13 @@ void main() {
: (out0 <= out1 ? c0 : c1);

// Where the two warped samples agree, occl is exactly zero and so is its effect on the result;
// the side map is only fetched where it can matter.
// the side map is only fetched where it can matter. A sample from outside its frame is not
// evidence of an occlusion: comparing against it would call the frame edge one and hand the
// pixel to exactly the sample that is off-frame. There the in-frame sample above already
// stands alone.
float bothIn = (1.0 - out0) * (1.0 - out1);
float rMotion = dot(abs(c0 - c1), vec3(1.0));
float occl = smoothstep(OCCL_LO, OCCL_HI, rMotion);
float occl = smoothstep(OCCL_LO, OCCL_HI, rMotion * bothIn);
if (occl > 0.0) {
vec3 single = pc.t < 0.5 ? c0 : c1;
#if DIS_OCCL_SIDED
Expand All @@ -238,8 +251,12 @@ void main() {
if (stillWins > 0.0) {
float keep = (1.0 - smoothstep(0.03, 0.12, rStatic)) * stillWins;
#if DIS_USE_HIST
// Not where a sample is off-frame: there rMotion measures the frame edge, not the
// pixel, so stillWins says nothing, and the history would pin moving scenery along the
// edge as a blend of the pair - the doubled band on a camera turn. An overlay there is
// still kept by the per-pixel test above.
float histStatic = textureLod(histTex, uv, 0.0).r;
keep = max(keep, histStatic * stillWins);
keep = max(keep, histStatic * stillWins * bothIn);
#endif
result = mix(result, mix(s0, s1, pc.t), keep);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC)
// SPDX-License-Identifier: GPL-3.0-or-later
//
// DIS frame generation: a Vulkan compute realisation of Dense Inverse Search
// OpenFlow 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.
Expand Down Expand Up @@ -55,6 +55,21 @@ precision highp int;
// there is nothing for the test to change; skip its 64 samples.
#define DIS_ZERO_TEST_MIN_PX 0.5

// Gauss-Newton only converges within a few texels of where it starts, and a move of more than a
// patch is thrown out, so starting the coarsest level from zero caps the motion the whole
// pyramid can follow - at 180p flow, somewhere between 100 and 190 px per frame at 1280x720,
// which a fast camera turn exceeds. The coarsest level is tiny (40x22 texels at 180p), so there
// the displacements within the radius are block-matched first, every second one and on every
// second texel of the patch, and Gauss-Newton refines the best from there. The bias, per sample
// and squared texel of displacement, makes a flat block that matches anywhere keep the smallest
// move.
#define DIS_COARSE_SEARCH_RADIUS 12
#define DIS_COARSE_SEARCH_BIAS 1.0

// Below this distance, in texels, from the vector a level would start from, the previous pair's
// vector is the same start and is not scored again.
#define DIS_PRIOR_MIN_PX 0.5

layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;

layout(set = 0, binding = 0) uniform sampler2D lastLumaMap;
Expand All @@ -66,11 +81,15 @@ layout(set = 0, binding = 3) uniform sampler2D flowMap;
// 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.
layout(set = 0, binding = 6) uniform sampler2D priorFlowMap;

layout(push_constant) uniform PC {
int level;
int coarseLevel;
int hintLevel;
int priorLevels;
} pc;

#define HINT_INVALID 1.0e6
Expand Down Expand Up @@ -102,6 +121,23 @@ shared vec2 sGrad[TILE * TILE];
#define REF(i, j) sLuma[(lp.y + (j)) * TILE + lp.x + (i)]
#define GRAD(i, j) sGrad[(lp.y + (j)) * TILE + lp.x + (i)]

// Mean-normalised SSD of the patch at pix displaced by f (texels), the metric the search minimises.
float patchSSD(vec2 f, ivec2 pix, ivec2 lp, ivec2 denseSize) {
vec2 invImageSize = 1.0 / vec2(denseSize);
vec2 o = clamp(vec2(pix) + f, vec2(0.0), vec2(denseSize) - 8.0);
float s = 0.0;
float s2 = 0.0;
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
vec2 tc = (o + vec2(i, j) + 0.5) * invImageSize;
float diff = textureLod(nextLumaMap, tc, 0.0).x - REF(i, j);
s += diff;
s2 += diff * diff;
}
}
return s2 - s * s / 64.0;
}

void main() {
const float patchSize = 8.0;

Expand Down Expand Up @@ -146,6 +182,28 @@ void main() {
vec2 flow;
if (pc.level == pc.coarseLevel) {
flow = vec2(0.0);
float bestCost = 1e30;
ivec2 maxOrigin = denseSize - ivec2(8);
for (int dy = -DIS_COARSE_SEARCH_RADIUS; dy <= DIS_COARSE_SEARCH_RADIUS; dy += 2) {
for (int dx = -DIS_COARSE_SEARCH_RADIUS; dx <= DIS_COARSE_SEARCH_RADIUS; dx += 2) {
ivec2 o = pix + ivec2(dx, dy);
if (any(lessThan(o, ivec2(0))) || any(greaterThan(o, maxOrigin))) continue;
float s = 0.0;
float s2 = 0.0;
for (int i = 0; i < 8; i += 2) {
for (int j = 0; j < 8; j += 2) {
float diff = texelFetch(nextLumaMap, o + ivec2(i, j), 0).x - REF(i, j);
s += diff;
s2 += diff * diff;
}
}
float cost = s2 - s * s / 16.0 + DIS_COARSE_SEARCH_BIAS * 16.0 * float(dx * dx + dy * dy);
if (cost < bestCost) {
bestCost = cost;
flow = vec2(dx, dy);
}
}
}
} else {
ivec2 fmMax = textureSize(flowMap, 0) - 1;
vec4 cf = texelFetch(flowMap, clamp(ivec2(pix / 2) + 2, ivec2(0), fmMax), 0);
Expand All @@ -155,36 +213,43 @@ void main() {
vec2 invImageSize = 1.0 / vec2(denseSize);
const float N = 64.0;

// A second starting point from the hardware estimator. Both candidates are scored on the
// patch with the same mean-normalised SSD the search minimises, and the search starts from
// the better one. The coarse level still covers what the estimator cannot: its search range
// is a few dozen pixels, and past that its vector is garbage that simply loses here.
// Further starting points, each scored on the patch with the same mean-normalised SSD the
// 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);
vec2 cand[2] = vec2[2](flow, hint);
float best = 1e30;
for (int c = 0; c < 2; c++) {
vec2 o = clamp(vec2(pix) + cand[c], vec2(0.0), vec2(denseSize) - patchSize);
float s = 0.0;
float s2 = 0.0;
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
vec2 tc = (o + vec2(i, j) + 0.5) * invImageSize;
float diff = textureLod(nextLumaMap, tc, 0.0).x - REF(i, j);
s += diff;
s2 += diff * diff;
}
}
float ssd = s2 - s * s / N;
if (ssd < best) {
best = ssd;
flow = cand[c];
}
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
// is indexed by position in the previous pair's first frame, where this patch's content was
// one vector ago, so it is looked up there (one fixed-point step from the patch centre).
if ((pc.priorLevels & (1 << pc.level)) != 0) {
vec2 c = (vec2(pix) + 4.0) * invImageSize;
vec2 p = textureLod(priorFlowMap, c, 0.0).xy;
p = textureLod(priorFlowMap, c - p, 0.0).xy * vec2(denseSize);
if (!any(isnan(p)) && !any(isinf(p)) && distance(p, flow) > DIS_PRIOR_MIN_PX) {
if (startSSD < 0.0) startSSD = patchSSD(flow, pix, lp, denseSize);
float priorSSD = patchSSD(p, pix, lp, denseSize);
if (priorSSD < startSSD) {
startSSD = priorSSD;
flow = p;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: Copyright 2026 qwertypower (DEVAR Entertainment LLC)
// SPDX-License-Identifier: GPL-3.0-or-later
//
// DIS frame generation: a Vulkan compute realisation of Dense Inverse Search
// OpenFlow 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.
Expand Down
Loading
Loading