Skip to content

feat(sokol): D3D11 staging-texture readback for Windows (#126) - #127

Merged
apotema merged 1 commit into
mainfrom
feat/sokol-d3d11-preview-126
May 16, 2026
Merged

apotema merged 1 commit into
mainfrom
feat/sokol-d3d11-preview-126

Conversation

@apotema

@apotema apotema commented May 16, 2026

Copy link
Copy Markdown
Contributor

Slice 2 of #122 — extends the sokol PIE viewport producer to Windows D3D11. Slice 1 (#124, just merged as d8fcc71) shipped the GL PBO path for Linux/Android; this slice adds the parallel CPU-side staging-texture path for sokol-on-Windows. Both blocks ship side by side in the generated source, mutually exclusive at comptime via builtin.os.tag.

Sokol back-buffer access

Used the public sokol-zig API rather than walking private internals:

  • sg_d3d11_device() → ID3D11Device*
  • sg_d3d11_device_context() → ID3D11DeviceContext*
  • sapp_d3d11_get_swap_chain() → IDXGISwapChain*
  • IDXGISwapChain::GetBuffer(0, IID_ID3D11Texture2D, &bb) via COM vtable dispatch → resolved back-buffer ID3D11Texture2D*

These three symbols are all part of the public sokol surface and ship on the link line for every sokol-on-Windows build (sokol-zig's build.zig already links d3d11.lib + dxgi.lib when backend == .d3d11). No backends/sokol/build.zig changes needed; verified by inspection.

COM dispatch uses stable D3D11 ABI vtable indices: Map=14, Unmap=15, CopyResource=47 on ID3D11DeviceContext; GetBuffer=9 on IDXGISwapChain; CreateTexture2D=5 on ID3D11Device; Release=2 on IUnknown. These have been stable since D3D11 shipped in 2009.

No offscreen-image fallback was needed — sapp_d3d11_get_swap_chain() is publicly exposed in sokol_app.

Algorithm

Per frame, after rendering and before window.endFrame():

  1. IDXGISwapChain::GetBuffer(0, ...) for the resolved back-buffer
  2. CopyResource(staging[N % 3], backbuffer) — async GPU copy
  3. From frame 2 onwards, Map(staging[(N-2) % 3], MAP_READ) + memcpy with BGRA→RGBA swizzle + Unmap
  4. Preview.publishFrame(pixels)

3-deep staging ring + 2-frame priming gap matches the GL path's pattern — keeps the GPU copy queue from blocking the CPU read.

BGRA→RGBA swizzle

DXGI swapchains created by sokol use DXGI_FORMAT_B8G8R8A8_UNORM by default. The SHM consumer expects RGBA per the existing protocol (matching glReadPixels(GL_RGBA, ...) from the GL path), so we swap channels 0 and 2 per pixel during the memcpy. The swizzle is pinned by the test's assertions so a future protocol variant tagged with the source pixel format can skip it cleanly.

DXGI back buffers are top-down (row 0 = top), so no row flip is needed — unlike the GL path where glReadPixels returns rows bottom-up.

Tests

test/tests.zig gains:

  • sokol desktop emits D3D11 staging-texture readback alongside the GL block — full regression test: asserts the _sokol_preview_d3d11_enabled gate, the _SokolPreviewD3d11 struct namespace, sokol C-symbol externs (sg_d3d11_device, sg_d3d11_device_context, sapp_d3d11_get_swap_chain), COM dispatch helpers (swapChainGetBuffer, deviceCreateTexture2D, contextCopyResource, contextMap, contextUnmap), IID_ID3D11Texture2D constant, staging ring layout, BGRA→RGBA swizzle direction, 2-frame priming gap, source-order constraints (beginFrameStream < publishFrame, contextCopyResource < endFrame(), release < sendBye), AST parses cleanly, and the GL block (feat(sokol): GLCORE/GLES3 PBO readback + publishFrame (#122 slice 1) #124) is still emitted unchanged.
  • Existing non-loop, non-sokol backends do not pull in any preview pixel publish path extended to also assert _sokol_preview_d3d11_enabled / _SokolPreviewD3d11 / sg_d3d11_device don't leak into sdl output.
  • Existing raylib desktop still emits its own PBO readback extended to also assert the new D3D11 names don't leak into raylib.

zig build test --summary all → 300/304 tests pass (4 skipped, unrelated negative-path skips); previously 299.

What I verified

  • zig build test --summary all on macOS host — all 300 tests pass.
  • zig build on macOS host — assembler binary builds.
  • labelle-assembler generate --project-root examples/sokol — regenerates with the new D3D11 block visible in examples/sokol/.labelle/sokol_desktop/main.zig.
  • zig ast-check on the regenerated main.zig — clean parse.
  • Native (macOS) zig build of the regenerated example — succeeds; the comptime gate elides both readback blocks on Darwin as expected.
  • Semantic check for Windows target: extracted the D3D11 block into a standalone file and ran zig build-obj -target x86_64-windows -fno-emit-bin. Compiles cleanly. Also verified the same file compiles for x86_64-linux and aarch64-macos with the block elided.

What I did NOT verify

Full Windows cross-compile of the generated example (zig build -Dtarget=x86_64-windows) hits pre-existing engine-side blockers in labelle-engine/src/preview_mode.zig and preview_shm.zig (they use POSIX read/write and std.c.O.RDWR which are void on Windows). Those are unrelated to this slice — the engine's preview control channel is POSIX-only by design (per its own CLAUDE.md). Slice 1 (#124) ships with the same situation.

Out of scope

Closes #126.

🤖 Generated with Claude Code

Slice 2 of labelle-assembler#122. Adds the Windows sokol-on-D3D11
preview readback path, matching the protocol shape and 3-buffer ring
+ 2-frame priming gap that slice 1 (#124) ships for sokol-on-GL.

Sokol back-buffer access goes through the public sokol-zig API
(`sg_d3d11_device`, `sg_d3d11_device_context`, `sapp_d3d11_get_swap_chain`)
plus COM vtable dispatch on `IDXGISwapChain::GetBuffer`. Swizzles
BGRA→RGBA during the memcpy because the DXGI swapchain defaults to
`DXGI_FORMAT_B8G8R8A8_UNORM` while the SHM consumer expects RGBA.

The new helper/init/frame/cleanup blocks live behind a
`_sokol_preview_d3d11_enabled` comptime gate (`builtin.os.tag ==
.windows`) so the COM-style externs never reach the link line on
Linux/Darwin builds. The GL block from #124 stays unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cursor

cursor Bot commented May 16, 2026 •

Copy link
Copy Markdown

PR Summary

Medium Risk
Medium risk because it introduces new low-level D3D11/COM vtable dispatch and GPU→CPU readback logic in generated code, where incorrect resource management or format handling could cause crashes or leaks on Windows builds.

Overview
Adds a Windows (D3D11) preview-frame readback path for sokol-generated games by generating a comptime-gated staging-texture ring that copies the swapchain back-buffer via CopyResource and Map, swizzles BGRA→RGBA, and publishes into the existing preview SHM stream.

Updates the sokol callback lifecycle codegen to emit/execute both the existing GL PBO block and the new D3D11 block side-by-side (mutually exclusive at comptime), and extends tests to assert the new D3D11 symbols/gates are present for sokol output and do not leak into raylib/SDL outputs.

Reviewed by Cursor Bugbot for commit 309221a. Bugbot is set up for automated code reviews on this repo. Configure here.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request implements D3D11 staging-texture readback for the Sokol backend on Windows. It introduces new code generation templates for D3D11/DXGI COM dispatch, staging-texture management, and pixel swizzling (BGRA to RGBA). These helpers are integrated into the main Zig template generation alongside existing OpenGL readback logic, using comptime gates to ensure platform-specific code is only active on the appropriate OS. Corresponding tests have been added to verify correct code emission and isolation. I have no feedback to provide.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 309221a. Configure here.

Comment thread src/main_zig.zig
\\ pub fn swapChainGetBuffer(sc: *anyopaque, buffer: u32, riid: *const _Guid, out: *?*anyopaque) i32 {
\\ const obj: *Object = @ptrCast(@alignCast(sc));
\\ const fp: *const fn (*anyopaque, u32, *const _Guid, *?*anyopaque) callconv(.c) i32 = @ptrCast(obj.vtbl[9]);
\\ return fp(sc, buffer, riid, out);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

COM vtable calls use wrong calling convention

Low Severity

All COM vtable function pointer casts (in swapChainGetBuffer, deviceCreateTexture2D, contextMap, contextUnmap, contextCopyResource, release) use callconv(.c) instead of callconv(.winapi). COM methods are defined with STDMETHODCALLTYPE (__stdcall). On x86_64 these conventions are identical, but on x86 (32-bit) Windows callconv(.c) is __cdecl while callconv(.winapi) is __stdcall, causing stack corruption. The _sokol_preview_d3d11_enabled gate checks .windows without an architecture guard.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 309221a. Configure here.

@apotema
apotema merged commit 8247ec1 into main May 16, 2026
4 checks passed
@apotema
apotema deleted the feat/sokol-d3d11-preview-126 branch May 16, 2026 13:42
apotema added a commit that referenced this pull request May 16, 2026
Minor release shipping the full sokol PIE viewport producer matrix
plus raylib's macOS zero-copy iosurface mode:

- #121 — raylib: prefer IOSurface mode on macOS. Comptime platform
  switch in `PREVIEW_READBACK_SETUP`/`_LOOP` selects
  `beginFrameStreamIOSurface`/`publishFrameIOSurface`/`endFrameStreamIOSurface`
  on `os.tag == .macos`, falls back to SHM mode elsewhere.

- #122 / #124 — sokol slice 1: GLCORE/GLES3 PBO readback for Linux
  desktop + Android. 3-deep PBO ring with 2-frame priming, gated on
  `_sokol_preview_gl_enabled = (os.tag == .linux)`.

- #122 / #127 — sokol slice 3: D3D11 staging-texture readback for
  Windows. COM-vtable dispatch on `IDXGISwapChain::GetBuffer` + 3-
  buffer staging ring + BGRA→RGBA swizzle. Gated on
  `_sokol_preview_d3d11_enabled = (os.tag == .windows)`.

- #122 / #128 — sokol slice 2: Metal/IOSurface zero-copy producer for
  macOS + iOS. Path B (MTLBlitCommandEncoder copy from swapchain to
  IOSurface-backed MTLTexture). Pinned to the labelle-toolkit/sokol-zig
  fork (`feat/expose-cached-metal-drawable` branch) which restores the
  `sapp_metal_get_current_drawable` accessor — upstream tracking at
  floooh/sokol-zig#154.

- #549 — preview_mode: extract `allocShmName` helper (refactor).

The three sokol slices coexist via mutually-exclusive comptime gates;
the editor (labelle-gui v0.3.0) dispatches consumer mode based on
`frame_offer.format` ("bgra8" → SHM, "iosurface_bgra8" → zero-copy).

Paired downstream: labelle-engine v1.37.3 (Preview frame-stream APIs)
and labelle-gui v0.3.0 (iosurface consumer + FBO blit).

Out of scope (filed as follow-ups):
- engine#551 — Windows port of preview_mode (POSIX TCP + shm).
- engine#552 — `publishFrameIOSurfaceBgra` variant to skip the
  producer↔engine double-swizzle.
- floooh/sokol-zig#154 — restore the upstream cached-drawable accessor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

sokol slice 3: D3D11 staging-texture readback (Windows)

1 participant