Skip to content

DirectX 11 support for windows - #6457

Draft
roffe wants to merge 7 commits into
fyne-io:developfrom
roffe:directx
Draft

DirectX 11 support for windows#6457
roffe wants to merge 7 commits into
fyne-io:developfrom
roffe:directx

Conversation

@roffe

@roffe roffe commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Description:

Adds Windows DirectX 11 support. build with -tags=directx

Implements #911

Checklist:

  • Tests included.
  • Lint and formatter run with no errors.
  • Tests all pass.

Where applicable:

  • Public APIs match existing style and have Since: line.
  • Any breaking changes have a deprecation path or have been discussed.
  • Check for binary size increases when importing new modules.

@roffe roffe changed the title Directx DirectX 11 support for windows Aug 3, 2026
@coveralls

coveralls commented Aug 3, 2026

Copy link
Copy Markdown

Coverage Status

coverage: 59.808% (-0.02%) from 59.828% — roffe:directx into fyne-io:develop

@andydotxyz andydotxyz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's completely amazing to see something as substantial as this being contributed.
However I have a few reservations or change requests and these probably need discussion.

  • There is no support for the Shader GL rendering and instead adds a new platform specific shader langauge - which is not gated or built for specific platforms, so most apps here will simply show nothing when built for directx
  • The whole directx driver and painter seem to be compiled for all windows builds rather than those specifically requesting directx? (only the app entry point is actually gating on the build tag - as far as I can see)
  • There is a massive amount of code generated here, particularly the d3d11.go with no testing. I have not looked at the code coverage in generally but it feels problematic.

I worry also that we have either a lack in our testing setup or in this particular addition to be able to compare what is rendered in one driver vs another (i.e. definitive either GL or software). For example does every line rendering support stroke width or is every circle vs ellipse contract the same...

@andydotxyz

Copy link
Copy Markdown
Member

One other thing I forgot to note - a lot of the docs generated in the DirectX seem to compare it to the GL or other renderers, whereas docs should stand on their own as describing behavior not differences.

@roffe

roffe commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for taking the time to go through something this large. All fair points!. I've pushed fixes for two of them and would like to align on the other two before writing more code.

Build gating — fixed. Only the app entry point was gated; the packages themselves built for plain GOOS=windows, and the embedded HLSL sources actually built on every platform.

Every file in internal/driver/directx and internal/painter/dx is now //go:build windows && directx.
Without the tag, go list reports zero Go files in both packages, so a normal Windows build compiles none of this; with the tag it builds and vets clean

Shader support Every built-in canvas primitive is implemented in the D3D pipeline: rectangles with per-corner radii, strokes and shadows, circle/ellipse, arc, regular and arbitrary polygons, bezier, line, text, image, raster, both gradients, and blur.

The one thing that can't render from GLSL is a user-supplied canvas.Shader, because D3D11's compiler only accepts HLSL, a platform-specific source is unavoidable at some layer.

The PR follows the pattern Source/SourceES already establishes (one GLSL source doesn't serve every target either) by adding SourceHLSL; a Shader without it is skipped and logged once, and everything else draws normally.

If a third per-target source isn't acceptable, the alternatives I see are:
(a) automatic GLSL→HLSL translation, which is a SPIRV-Cross-sized dependency I didn't want to smuggle into a PR this big already, or
(b) shipping the driver documented as not supporting custom shaders initially.

Happy to go whichever way you prefer.

d3d11.go / testing — it's hand-written rather than generated: minimal COM vtable bindings for exactly the slots the painter calls (~850 lines covering device, context, swap-chain and the shader compiler).

The existing tests pin the CPU-side invariants that fail silently: the Go constant-buffer structs against the HLSL declarations, glyph-atlas packing overlap, uniform offset parsing, blend-state configuration, blur kernel weights, text windowing maths.

The binding layer itself only gets meaningfully exercised with a device, so my proposal is a Windows CI job running go test -tags directx that creates the device through WARP — the driver already falls back to WARP when no GPU exists, and WARP runs headless on stock GitHub runners. I can add that workflow to this PR if you'd like it here.

Cross-driver comparison — agreed, and I'd say the gap exists between GL and software today as well.

Concrete proposal: a conformance suite rendering one fixed scene per primitive family (lines at several stroke widths, circle vs ellipse in non-square bounds, per-corner radii, gradients, text) through Painter.Capture on each backend, diffed against the software renderer's output with a tolerance — the same contract enforced on all three backends, with WARP making the D3D side CI-able.

To your specific examples: line stroke width is supported, and circle shares the ellipse shader with aspect correction, so that contract is identical — but I agree that should be proven by a test, not asserted in a comment.

I'd prefer building the suite as a follow-up PR since it touches and benefits every driver, but can fold it into this one if you consider it a blocker.

Docs — fair, and fixed. I've swept the driver, the painter and the HLSL sources; comments now describe behaviour on their own terms without referencing the GL or software renderers.

@andydotxyz

Copy link
Copy Markdown
Member

Let's get others thoughts on the Shader situation

@dweymouth

dweymouth commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Let's get others thoughts on the Shader situation

I do think it makes sense to add an HLSL shader property to the canvas.Shader object if we merge this PR and make DirectX a supported driver. And if the developer doesn't supply a shader for the platform it's running on, it makes sense that it just doesn't render. I guess an alternative could be to somehow still use GL to render the GL shader in the DirectX driver if there is no HLSL shader but I don't know if this is possible. And it's almost certainly not possible if also building with CGO_ENABLED=0. I also don't think the existence of canvas.Shader alone is a reason to hold back this PR (but I am concerned about the vastly increased surface for maintaining, though I think in the long term supporting DirectX and Metal directly will probably be the right choice)

@roffe

roffe commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Mixing OpenGL and Direct3D

Same window, both APIs presenting

Painful. On Windows an HWND's pixel format is set once and is permanent, and DXGI wants to own the swapchain for that HWND. You can hack around it (child windows, D3D rendering to an offscreen target then blitting), but there's no clean way to have both APIs alternately presenting to one surface.

Shared GPU resources

one API renders into a texture, the other consumes it, no round-trip through system memory.

  • WGL_NV_DX_interop2 — despite the NV prefix, it's supported by AMD and Intel too. You open a D3D device with wglDXOpenDeviceNV, register a D3D9/10/11 texture or renderbuffer with wglDXRegisterObjectNV, then bracket your GL access with wglDXLockObjectsNV/wglDXUnlockObjectsNV. The lock/unlock does the synchronization for you. This is the well-trodden path. Caveat: D3D11 and older only — no D3D12.

  • GL_EXT_memory_object_win32 + GL_EXT_semaphore_win32 — the modern route, same machinery Vulkan external memory uses. You create a shared handle on the D3D side (ID3D12Device::CreateSharedHandle, or IDXGIResource1::CreateSharedHandle for D3D11 with MISC_SHARED_NTHANDLE), import it into GL as a memory object, and bind a texture to it with glTextureStorageMem2DEXT. Synchronization is explicit: import a D3D fence as a GL semaphore and use glWaitSemaphoreEXT/glSignalSemaphoreEXT. More code, but it's the only option if the D3D side is D3D12.

  • Readback fallback — glReadPixels into a staging buffer, upload to a D3D texture. Universal, works everywhere, and costs you a full GPU→CPU→GPU round trip per frame. Only for compatibility fallback.

The downside of this is of course you'd have dual graphics stacks running inside your fyne app and it would not be possible to build without CGO due to the requirement of GLFW

Regarding the Shader type

We could reuse the existing Source field on Shader rather than introducing a new mechanism. Cross-compiling users would then select the appropriate shader source through the standard Go build constraints — either filename suffixes, explicit //go:build tags, or a combination of the two.

A typical layout:

shader.go            // shared Shader definition
shader_windows.go    // embeds the HLSL source
shader_darwin.go     // embeds the MSL source
myshader.hlsl
myshader.frag
//go:build windows

package myapp

import _ "embed"

//go:embed myshader.hlsl
var myShaderSource []byte
//go:build linux

package myapp

import _ "embed"

//go:embed myshader.frag
var myShaderSource []byte
package myapp

var myShader = Shader{
	Name:   "myShader",
	Source: myShaderSource,
}
package myapp

var myShader = Shader{
	Name:   "myShader",
	Source: myShaderSource,
}

Advantages of this approach:

  • No new API surface. Source already carries the payload, so the toolchain handles platform selection rather than the library.
  • Compile-time selection means unused shader variants are excluded from the binary entirely.
  • Users who prefer a single file can group all variants into one embed.FS and index it at runtime — the same Source field still works.

@dweymouth

Copy link
Copy Markdown
Contributor

We could reuse the existing Source field on Shader rather than introducing a new mechanism. Cross-compiling users would then select the appropriate shader source through the standard Go build constraints

Nah, we already have the pattern of Source and SourceES, so adding SourceHLSL would be following that existing pattern.

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.

4 participants