fix(vulkan): fail closed on swapchain capability and sync errors - #265
Conversation
|
Stacked consumer preview: #268 at exact head The preview remains explicitly draft/non-mergeable and will be rebased to Android-only commits after this prerequisite lands. |
a8ff52e to
e6fd2c1
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
e6fd2c1 to
eb48db0
Compare
kolkov
left a comment
There was a problem hiding this comment.
Thorough review of the full diff and comparison with Rust wgpu-hal (wgpu-hal/src/vulkan/swapchain/native.rs).
The transactional reconfiguration, snapshot validation, and broken-state tracking all align well with Rust wgpu-hal patterns. The generic queryRequiredSwapchainValues with VK_INCOMPLETE retry is a clean abstraction, and the 301-line test file covers the pure-logic paths well.
Must-fix before merge
1. DiscardTexture should not mark the swapchain broken
Currently DiscardTexture calls markBroken(), which prevents any further acquire/present until full reconfigure. Rust wgpu's discard_texture is a no-op (with a TODO) — discarding an acquired image is a legitimate operation during resize/minimize. Marking the swapchain broken on discard forces unnecessary reconfigure cycles.
Suggested fix: clear imageAcquired, detach from queue, but don't call markBroken.
2. Surface.Destroy() leaks VkSurfaceKHR on destroyWithError() failure; Unconfigure() leaves stale references
Destroy() early-returns when destroyWithError() returns an error, skipping cleanup of s.handle (the VkSurfaceKHR). In a device-lost scenario (where vkDeviceWaitIdle fails), this leaks the surface handle. The Vulkan spec allows destroying surfaces even when the device is lost.
Unconfigure() has the same early-return pattern — on destroyWithError() failure it skips s.swapchain = nil and s.device = nil, leaving stale references.
Suggested fix: log the error but continue with cleanup in both methods — don't early-return from destruction paths.
Should-fix
- Restore
VK_ERROR_INITIALIZATION_FAILED → ErrSurfaceLostmapping increateSwapchainto match Rust wgpu (native.rs lines 228-229). Without this, callers may retry on a dead surface instead of giving up.
Nits (non-blocking)
^uint32(0)→math.MaxUint32for readabilityqueryRequiredSwapchainValuestakes two identical operation name parameters in all call sites — could simplify to onedestroyWithError()setssc.broken = trueon clean destroy — harmless but semantically confusing (destroyed takes precedence instateError(), but the intent is unclear to readers)
|
@kolkov Thank you for the detailed review. I addressed every item in
I also added regression coverage for discard reuse, teardown-after-error, surface-loss mapping, and clean destruction. All 10 repository Actions checks are green. Could you please re-review when convenient? |
kolkov
left a comment
There was a problem hiding this comment.
All six items addressed in 4a3d6f3. Verified:
DiscardTexture—markBrokenremoved, now clearsimageAcquired+ detach (Rust parity)Destroy()/Unconfigure()— early-return removed, cleanup continues after errorErrorInitializationFailed → ErrSurfaceLost— restored viaswapchainCreateError()math.MaxUint32— donequeryRequiredSwapchainValues— single operation parameterdestroyWithError()— no longer setsbroken = trueon clean destroy
Regression tests added for discard reuse, teardown-after-error, surface-loss mapping, and clean destruction. CI green.
Summary
Make Vulkan swapchain negotiation, reconfiguration, synchronization, and teardown fail closed:
Destroy()APIThe implementation mirrors the defensive lifecycle used by Rust wgpu-hal while keeping the existing Go HAL surface.
Why
The old path accepted partial/fabricated capability state, assumed requested image counts, and could continue after synchronization or layout failures. A failed reconfigure could also tear down the working swapchain before the replacement was usable.
Validation
Go 1.25.12 and Go 1.26.5:
go test ./hal/vulkan .go test -race ./hal/vulkan .go vet ./hal/vulkan .upstream/mainFailure-injection tests cover unstable counts, unsupported requests, actual image counts, lifecycle guards, broken-state rejection, transactional reconfigure, and idempotent destruction.
This correction is platform-independent and intentionally separate from the Android preview.
Android integration relationship
This Android-independent change is one of five prerequisites replayed in #268. Its canonical candidate head is
eb48db0e7eec4c12eaa87cc998f6ea9e8f348fdd; the corresponding replay in #268 ends at0209353680ae841aee99655f66303456bc84d89a. The #268 description records the current exact integration head and full replay map; it drops this replay after this PR merges. Rust-v29 typed surface parity remains separate in #273. Android runtime and release claims remain in #268.