Repository navigation
feat(sokol): D3D11 staging-texture readback for Windows (#126) - #127
Conversation
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>
PR SummaryMedium Risk Overview 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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.
| \\ 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); |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit 309221a. Configure here.
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>


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 viabuiltin.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-bufferID3D11Texture2D*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.zigalready linksd3d11.lib+dxgi.libwhenbackend == .d3d11). Nobackends/sokol/build.zigchanges needed; verified by inspection.COM dispatch uses stable D3D11 ABI vtable indices:
Map=14,Unmap=15,CopyResource=47 onID3D11DeviceContext;GetBuffer=9 onIDXGISwapChain;CreateTexture2D=5 onID3D11Device;Release=2 onIUnknown. 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():IDXGISwapChain::GetBuffer(0, ...)for the resolved back-bufferCopyResource(staging[N % 3], backbuffer)— async GPU copyMap(staging[(N-2) % 3], MAP_READ)+ memcpy with BGRA→RGBA swizzle +UnmapPreview.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_UNORMby default. The SHM consumer expects RGBA per the existing protocol (matchingglReadPixels(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
glReadPixelsreturns rows bottom-up.Tests
test/tests.ziggains:sokol desktop emits D3D11 staging-texture readback alongside the GL block— full regression test: asserts the_sokol_preview_d3d11_enabledgate, the_SokolPreviewD3d11struct 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_ID3D11Texture2Dconstant, 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.non-loop, non-sokol backends do not pull in any preview pixel publish pathextended to also assert_sokol_preview_d3d11_enabled/_SokolPreviewD3d11/sg_d3d11_devicedon't leak into sdl output.raylib desktop still emits its own PBO readbackextended 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 allon macOS host — all 300 tests pass.zig buildon macOS host — assembler binary builds.labelle-assembler generate --project-root examples/sokol— regenerates with the new D3D11 block visible inexamples/sokol/.labelle/sokol_desktop/main.zig.zig ast-checkon the regeneratedmain.zig— clean parse.zig buildof the regenerated example — succeeds; thecomptimegate elides both readback blocks on Darwin as expected.zig build-obj -target x86_64-windows -fno-emit-bin. Compiles cleanly. Also verified the same file compiles forx86_64-linuxandaarch64-macoswith 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 inlabelle-engine/src/preview_mode.zigandpreview_shm.zig(they use POSIXread/writeandstd.c.O.RDWRwhich 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
preview_mode.zigPOSIX/Windows divergence.Closes #126.
🤖 Generated with Claude Code