Skip to content

Let a container serve both D8VK and Zink on a pre-1.2 ICD - #13

Merged
utkarshdalal merged 2 commits into
GameNative:wrapper-25from
abesmon:wrapper-25
Sep 8, 2026
Merged

Let a container serve both D8VK and Zink on a pre-1.2 ICD#13
utkarshdalal merged 2 commits into
GameNative:wrapper-25from
abesmon:wrapper-25

Conversation

@abesmon

@abesmon abesmon commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Two control points and one emulation. All off by default — with no
environment set, this changes nothing for any driver.

Motivation

Newer D8VK calls Vulkan 1.3 core entry points. On a Vulkan 1.1 ICD those function
pointers do not exist, and calling one surfaced in 32-bit Wine as an assertion in
loader_thunks.c rather than a useful Vulkan error. A container can report a
higher core version, but then the wrapper must actually lower the calls that
version implies.

The catch: raising the reported version raises it for every client, and Zink
changes ABI at Vulkan 1.2
. Above it, it requests features through the
VkPhysicalDeviceVulkan12Features / Vulkan13Features aggregates instead of the
extension feature structs an older ICD understands — and process_pnext_chain
drops an aggregate the base driver is too old to have. So the moment D3D8 works,
OpenGL stops.

That is why these ship together: raising the version for D8VK is what creates the
need for a per-engine version control point, and reporting 1.1 to Zink is what
makes it ask for VK_KHR_imageless_framebuffer as an extension the driver lacks.

Technical basis

Vulkan 1.3 promoted VK_KHR_synchronization2 (vkQueueSubmit2),
VK_EXT_extended_dynamic_state and …state2, and VK_KHR_dynamic_rendering.
Vulkan 1.2 promoted VK_KHR_imageless_framebuffer.
See the promotion table.

A promoted entry point is only guaranteed where the core version is. The 1.1-era
ICD here still exposes the pre-promotion EXT entry points, so the lowering
enables the EXT names by name and routes core calls to them; where no EXT
equivalent exists (vkQueueSubmit2), it translates to the legacy operation.

Before / after

before after
D3D8 on a 1.1 ICD blank window, 3 FPS, never renders renders, 55 avg fps
OpenGL when the container raises the version for D3D8 black screen, 0 FPS renders, 60 fps
a driver lacking vkQueueSubmit2 called through a NULL pointer VK_ERROR_FEATURE_NOT_PRESENT
everything else unchanged, measured

Why this shape

The wrapper decides nothing; the container operator does. This ends a chain of
narrowing guesses — an earlier revision inspected driverID and picked a version
itself, a later one hardcoded a zink-named variable, this one removes the
guessing:

const char *override_engine = getenv("WRAPPER_ENGINE_NAME");
if (engine && override_engine && !strcmp(engine, override_engine))

Both variables are required, so an unset knob is identical to the code being
absent. The match is an exact strcmp, not a substring, so no unrelated engine
is silently caught and no engine is named in the code — the Zink case is one
instantiation, not the mechanism.

That is also the whole platform-safety argument: there is no default to regress.
The diff was audited hunk by hunk for this, and three places that ran regardless
of the knobs were brought behind them — the pNext unlink of the imageless
feature (keyed on the driver's extension list, it would have stripped a
legitimate request on a 1.2 driver supporting the feature as core), the
command-buffer walk in ResetCommandPool, and the per-view hash insert in
CreateImageView.

Flags, including for non-NVIDIA

Nothing is driver-gated. Any driver hitting the same gaps can opt in.

variable when to set it
WRAPPER_EMULATE_VULKAN_1_3=1 base driver below 1.3 and a client calls 1.3 core entry points (D8VK). Inert on a 1.3+ driver.
WRAPPER_ENGINE_NAME + WRAPPER_ENGINE_VK_VERSION one engine needs a different reported version than the rest. Both required.
WRAPPER_EMULATE_IMAGELESS_FRAMEBUFFER=1 a client asks for VK_KHR_imageless_framebuffer and the driver lacks it.

The Tegra configuration, as a worked example:

WRAPPER_VK_VERSION=1.3.178          # what D8VK needs
WRAPPER_ENGINE_NAME=mesa\ zink      # put Zink back on the older ABI
WRAPPER_ENGINE_VK_VERSION=1.1.178
WRAPPER_EMULATE_VULKAN_1_3=1
WRAPPER_EMULATE_IMAGELESS_FRAMEBUFFER=1

Note the escaped space. WRAPPER_ENGINE_NAME is an exact match and Zink's
engine name is mesa zink; in a launcher whose environment field is
space-separated, writing it unescaped silently yields
WRAPPER_ENGINE_NAME=mesa and the override never fires. Each control point logs
when it engages, which is how to confirm it took:

Lowering Vulkan 1.3 core calls for a 1.1 base driver (WRAPPER_EMULATE_VULKAN_1_3)
Reporting Vulkan 1.1 to engine 'mesa zink' (WRAPPER_ENGINE_VK_VERSION)
Emulating VK_KHR_imageless_framebuffer

Evidence

Switch v1, Tegra X1, NVIDIA proprietary 111.0.0 (Vulkan 1.1). Necessity was
measured by toggling one binary — an unset knob equals the commit being absent,
so this removes build variance. Removing each in turn: D3D8 drops to a blank
window at 3 FPS; Balatro goes black at 0 FPS; Balatro exits at
CreateGraphicsPipelines. For the first, diffing enabled device extensions
between arms shows exactly VK_EXT_extended_dynamic_state and …state2.

No regression elsewhere: 21-point render matrix, three passes, against the
preceding upstream build — 21/21, and D3D9/D3D10/D3D11 unmoved in both bitnesses.

Limitations

  • Not general Vulkan 1.3. QueueSubmit2 lowering forwards pNext into a
    legacy VkSubmitInfo without proof that sync2-only chains are filtered or
    absent; dynamic-rendering lowering has no full depth/stencil resolve; colour
    attachments are capped at eight. It is a shim for the calls D8VK makes.
  • One driver was available. Everything above is Tegra X1 — which is exactly
    why nothing is on by default.

A global WRAPPER_VK_VERSION can be necessary for one compatibility layer while exposing that same raised version changes another engine's feature-query ABI beyond what the base driver accepts.

Add a generic override selected by exact VkApplicationInfo engine name through WRAPPER_ENGINE_NAME and WRAPPER_ENGINE_VK_VERSION. Both variables must be present, so existing behavior is unchanged by default. Exact matching avoids silently catching unrelated engines.
Lower the Vulkan 1.3 commands needed by legacy consumers to supported extension or Vulkan 1.2 paths, and add an opt-in imageless-framebuffer compatibility path.

Keep both compatibility layers disabled unless explicitly requested, and only expose them when the base driver lacks the native capability.
@utkarshdalal
utkarshdalal merged commit cd0c2eb into GameNative:wrapper-25 Sep 8, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants