Skip to content

GameNative/vk-layer-surface-format

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

vk-layer-surface-format

A tiny implicit Vulkan layer that controls which swapchain surface format an application ends up using — primarily to fix the red/blue channel swap tint seen with some Android Vulkan drivers/wrappers.

Because it is a Vulkan layer, it sits above the ICD, so it works with every graphics driver/wrapper without rebuilding any of them: wrapper, wrapper-leegao, wrapper-v2, wrapper-legacy, turnip, vortek, freedreno, … one .so, all drivers.

Status: experimental, for device testing.

What it does

The "wrong colors" problem is a channel-order mismatch: the app/DXVK renders in one order (e.g. B8G8R8A8) while the WSI scans out the other (R8G8B8A8), so red and blue come out swapped. This layer fixes that by steering format negotiation — zero-copy, it never touches pixels.

Three modes (env GAMENATIVE_SF_MODE):

Mode What it does Cost
prefer (default) Reorders vkGetPhysicalDeviceSurfaceFormats(2)KHR so the target channel order is first. Most apps pick formats[0]. zero-copy
filter Same, but also hides the non-target orders so the app must use the target. zero-copy
force Rewrites VkSwapchainCreateInfoKHR::imageFormat to the target order at creation. Adds VK_SWAPCHAIN_CREATE_MUTABLE_FORMAT_BIT + an image-format-list so a view the app makes in its originally-requested format stays valid. zero-copy

Target channel order (env GAMENATIVE_SF_TARGET): rgba8 (default) or bgra8.

force requires the driver to support VK_KHR_swapchain_mutable_format + VK_KHR_image_format_list (the GameNative wrapper does). If the mutable path fails, the layer falls back to the unmodified call so you never lose the swapchain.

Build

Requires the Android NDK (Vulkan headers, incl. vulkan/vk_layer.h, ship in its sysroot). Tested with NDK r27.

# auto-detects ~/Library/Android/sdk/ndk or set ANDROID_NDK explicitly
ANDROID_NDK=/path/to/ndk ./build.sh

Output:

dist/arm64-v8a/libVkLayer_surface_format.so
dist/arm64-v8a/VkLayer_surface_format.json

ABI/API are overridable: ABI=arm64-v8a API=26 ./build.sh.

Install (into a GameNative imagefs)

Drop the two files next to the other implicit layers — the same directory that already holds vkBasalt.json / libbcn_layer.json:

<imagefs>/usr/lib/libVkLayer_surface_format.so
<imagefs>/usr/share/vulkan/implicit_layer.d/VkLayer_surface_format.json

The manifest uses a bare library_path (libVkLayer_surface_format.so, no path), exactly like vkBasalt.json. The loader resolves a bare filename via the normal library search path, so the .so must be on it — /usr/lib is. (Do not use ./name — that resolves relative to the manifest's own directory, so if the .so isn't sitting in implicit_layer.d/ the loader silently skips the layer.) If you'd rather be explicit, an absolute library_path like /usr/lib/libVkLayer_surface_format.so also works.

Enable / use

The layer is inert unless explicitly enabled (manifest enable_environment). Set these env vars for the guest process:

ENABLE_SURFACE_FORMAT_LAYER=1     # turn the layer on (required)
GAMENATIVE_SF_TARGET=rgba8        # rgba8 (default) | bgra8
GAMENATIVE_SF_MODE=prefer         # prefer (default) | filter | force
GAMENATIVE_SF_VERBOSE=1           # optional: log decisions to stderr/logcat

To disable without removing the files: DISABLE_SURFACE_FORMAT_LAYER=1.

Wiring into GameNative

In XServerScreen.kt, where the other wrapper env vars are set, drive it from a container config key (mirroring WRAPPER_RESOURCE_TYPE, etc.):

val surfaceFormat = graphicsDriverConfig.get("surfaceFormat", "BGRA8")
if (!surfaceFormat.equals("BGRA8", true)) {
    envVars.put("ENABLE_SURFACE_FORMAT_LAYER", "1")
    envVars.put("GAMENATIVE_SF_TARGET", "rgba8")
    envVars.put("GAMENATIVE_SF_MODE", "prefer")
}

(Ensure the layer files are unpacked into the imagefs for whichever graphics driver the container selects.)

Quick test outside GameNative

With the Vulkan SDK's vulkaninfo, point the loader at the manifest and confirm the surface format list changes:

# bare library_path => the loader searches LD_LIBRARY_PATH, so add dist/ to it
VK_ADD_LAYER_PATH=$PWD/dist/arm64-v8a \
LD_LIBRARY_PATH=$PWD/dist/arm64-v8a:$LD_LIBRARY_PATH \
ENABLE_SURFACE_FORMAT_LAYER=1 GAMENATIVE_SF_MODE=filter GAMENATIVE_SF_TARGET=rgba8 \
GAMENATIVE_SF_VERBOSE=1 \
vulkaninfo | grep -A2 -i "surface formats"

Is it getting picked up? (logging & verification)

Two independent signals:

1. The layer's own logcat output. The layer logs via Android liblog, so it shows in logcat regardless of how the guest's stderr is routed. On every instance creation it emits one always-on line (no flag needed):

adb logcat -s surface_format_layer
# I/surface_format_layer: ACTIVE: mode=prefer target=rgba8 (intercepting surface formats)

If you see ACTIVE: …, the loader found the manifest, the enable_environment gate passed, and the layer is in the chain. Add GAMENATIVE_SF_VERBOSE=1 for per-call detail (each force swap, etc.).

2. The Vulkan loader's discovery log — tells you why a layer is or isn't loaded (manifest path, library_path resolution, enable_environment result, ABI mismatch). Set on the guest process:

VK_LOADER_DEBUG=all        # or: layer,error,warn

Then adb logcat and look for VK_LAYER_GAMENATIVE_surface_format and "Insert instance layer" / skip-reason lines.

Sanity cross-check: vkBasalt (already shipped in GameNative) loads via the exact same implicit_layer.d + enable_environment mechanism. If ENABLE_VKBASALT=1 visibly engages but this layer's ACTIVE line never appears, the issue is this layer's files/manifest, not the loading path.

Common reasons it won't pick up

  • ENABLE_SURFACE_FORMAT_LAYER=1 not set on the guest process (it's gated off by default).
  • Manifest not in <imagefs>/usr/share/vulkan/implicit_layer.d/, or its library_path doesn't resolve to the .so.
  • The layer files weren't unpacked into the imagefs for the graphics driver the container actually selected.
  • Wrong ABI — the .so must be arm64-v8a (it is).

How it works (layer internals)

Standard implicit-layer dispatch: it chains vkCreateInstance / vkCreateDevice to capture the next-level vkGetInstanceProcAddr / vkGetDeviceProcAddr, keeps a small dispatch table keyed by the loader dispatch pointer, and intercepts exactly:

  • vkGetPhysicalDeviceSurfaceFormatsKHR / …2KHR — reorder/filter (prefer, filter)
  • vkCreateSwapchainKHR — mutable-format override (force)

Everything else passes straight through.

Roadmap / extension points

This is structured so a post-processing pass (shaders) can be added at the vkCreateSwapchainKHR / vkQueuePresentKHR boundary — intercept present, run a fullscreen pass into the swapchain image, then present. Note that any real shader effect requires one render pass (it is not zero-copy); the zero-copy property of this layer applies only to the channel-order fix. For a full ReShade-style pipeline today, GameNative already ships vkBasalt.

License

MIT — see LICENSE.

About

Implicit Vulkan layer to control swapchain surface format (zero-copy R/B channel-order fix) across all wrappers/drivers

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Packages

 
 
 

Contributors