Skip to content

fix(vulkan): fail closed on swapchain capability and sync errors - #265

Merged
kolkov merged 3 commits into
gogpu:mainfrom
besmpl:besmpl/swapchain-fail-closed
Jul 22, 2026
Merged

fix(vulkan): fail closed on swapchain capability and sync errors#265
kolkov merged 3 commits into
gogpu:mainfrom
besmpl:besmpl/swapchain-fail-closed

Conversation

@besmpl

@besmpl besmpl commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Make Vulkan swapchain negotiation, reconfiguration, synchronization, and teardown fail closed:

  • query complete surface snapshots and validate requested format/color-space, present mode, alpha mode, image usage, transform, extents, and image counts
  • size image views and synchronization from the images actually returned by the driver
  • keep reconfiguration transactional: retain the old swapchain until the replacement and all dependent resources are ready
  • propagate query, wait, fence, reset, layout-transition, present, and teardown failures
  • mark unsafe synchronization state broken so it cannot be reused
  • make destruction idempotent while preserving the existing public Destroy() API

The 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 .
  • current golangci-lint v2: zero issues introduced relative to upstream/main

Failure-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 at 0209353680ae841aee99655f66303456bc84d89a. 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.

@besmpl

besmpl commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Stacked consumer preview: #268 at exact head b11d2671e80081a191705c41c4e77e8b61472017 incorporates this PR's head a8ff52e340f0a06c8e1b6599a03856d7fc74d1a2; overlapping swapchain changes are conflict-resolved against the Android-specific policy layer.

The preview remains explicitly draft/non-mergeable and will be rebased to Android-only commits after this prerequisite lands.

@besmpl
besmpl force-pushed the besmpl/swapchain-fail-closed branch from a8ff52e to e6fd2c1 Compare July 19, 2026 07:14
@codecov

codecov Bot commented Jul 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@besmpl
besmpl force-pushed the besmpl/swapchain-fail-closed branch from e6fd2c1 to eb48db0 Compare July 19, 2026 07:45

@kolkov kolkov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 → ErrSurfaceLost mapping in createSwapchain to 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.MaxUint32 for readability
  • queryRequiredSwapchainValues takes two identical operation name parameters in all call sites — could simplify to one
  • destroyWithError() sets sc.broken = true on clean destroy — harmless but semantically confusing (destroyed takes precedence in stateError(), but the intent is unclear to readers)

@besmpl

besmpl commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

@kolkov Thank you for the detailed review. I addressed every item in 4a3d6f3:

  • DiscardTexture now clears the acquisition and detaches it without poisoning the swapchain;
  • Surface.Unconfigure and Surface.Destroy continue releasing references and the Vulkan surface even when swapchain teardown reports an error;
  • VK_ERROR_INITIALIZATION_FAILED maps to ErrSurfaceLost;
  • the sentinel now uses math.MaxUint32;
  • the checked enumeration helper has one operation name;
  • a clean swapchain destroy no longer marks the swapchain broken.

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 kolkov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All six items addressed in 4a3d6f3. Verified:

  1. DiscardTexturemarkBroken removed, now clears imageAcquired + detach (Rust parity)
  2. Destroy() / Unconfigure() — early-return removed, cleanup continues after error
  3. ErrorInitializationFailed → ErrSurfaceLost — restored via swapchainCreateError()
  4. math.MaxUint32 — done
  5. queryRequiredSwapchainValues — single operation parameter
  6. destroyWithError() — no longer sets broken = true on clean destroy

Regression tests added for discard reuse, teardown-after-error, surface-loss mapping, and clean destruction. CI green.

@kolkov
kolkov merged commit efb8b58 into gogpu:main Jul 22, 2026
11 checks passed
kolkov added a commit that referenced this pull request Jul 22, 2026
* docs: update CONTRIBUTING.md — Smart Coding framework, AI-assisted policy, current project structure

* docs: CHANGELOG for merged prerequisite PRs (#264, #265, #266, #267, #269)
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