Skip to content

feat(software): expose headless surface readback - #276

Merged
kolkov merged 1 commit into
gogpu:mainfrom
besmpl:besmpl/headless-software-surface-readback
Jul 24, 2026
Merged

feat(software): expose headless surface readback#276
kolkov merged 1 commit into
gogpu:mainfrom
besmpl:besmpl/headless-software-surface-readback

Conversation

@besmpl

@besmpl besmpl commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Closes #256.

I’m very excited to get this foundation upstream: it makes deterministic, display-free software rendering available through the normal public surface lifecycle, while keeping backend ownership explicit and the API small.

What this adds

  • wgpu.HeadlessSurfaceTarget, a zero-sized safe target backed by the internal hal.SurfaceTargetHeadless delivered in feat(surface): match Rust v29 typed surface targets #273.
  • Surface.ReadPixels(), returning an owned, tightly packed, top-left RGBA8 snapshot after present/discard.
  • An optional hal.PixelReader capability rather than widening the mandatory hal.Surface interface.
  • Software readback for RGBA/BGRA, including sRGB variants and normalization to RGBA.
  • Public clear and WGSL-triangle lifecycle proofs, ownership/error-state coverage, and a complete runnable documentation example.
  • software.Surface.GetFramebuffer() remains as a compatibility alias.

This is intentionally a Go software extension, not a claim that Rust wgpu exposes a headless SurfaceTarget. Rust-tag and browser builds retain a compatible Go method set but reject the target/readback explicitly. Other native backends remain unchanged and unsupported; ordinary WebGPU GPU readback continues to use texture-to-buffer copies.

Public flow

surface, err := instance.CreateSurfaceFromTarget(wgpu.HeadlessSurfaceTarget{})
adapter, err := instance.RequestAdapter(&wgpu.RequestAdapterOptions{
    CompatibleSurface:    surface,
    ForceFallbackAdapter: true,
})
device, err := adapter.RequestDevice(nil)
err = surface.Configure(device, &wgpu.SurfaceConfiguration{ /* extent + format */ })

// Acquire -> render -> submit -> present through the ordinary surface API.
pixels, err := surface.ReadPixels() // owned RGBA8 snapshot

docs/SURFACE-TARGETS.md contains the complete compilable acquire/render/submit/present/readback example.

Verification

Passed locally with CGO_ENABLED=0 and GOWORK=off:

  • go test -count=1 ./hal/software ./core .
  • every repository package except ./hal/metal
  • golangci-lint run --timeout=5m --new-from-rev=upstream/main (0 issues)
  • Rust-tag API compile
  • GOOS=js GOARCH=wasm API compile
  • Linux amd64 and Windows amd64 full cross-compilation
  • documented public example (64 output bytes)
  • git diff --check

The complete local Apple-silicon go test ./... currently reaches only the existing three hal/metal private-staging fixture expectation failures. The standalone, clean, green portability fix is #275; this PR deliberately does not stack or duplicate that unrelated change. Full coverage reaches the same fixture failure. This branch is based directly on current main.

This PR supplies the wgpu foundation only; it does not claim to implement the downstream golden-file harness from later phases of the parent roadmap.

@besmpl
besmpl requested a review from kolkov as a code owner July 24, 2026 10:03
@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@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.

Clean, well-designed contribution. A few notes from the review:

API design — The optional hal.PixelReader capability pattern is exactly right. It matches the existing PixelWriter/PixelPresenter family and avoids widening the mandatory hal.Surface interface. The zero-sized HeadlessSurfaceTarget{} integrates cleanly with the typed target system from #273.

Correctness — Verified: owned copy semantics (HAL make+copy), BGRA normalization, state machine enforcement (nil/released/unconfigured/acquired), and cross-backend consistency (native/browser/rust all expose same ReadPixels() ([]byte, error) with clear rejection messages).

Test coverage — 462 lines of tests cover format variants, ownership isolation, triangle rendering readback, state error paths, unsupported backend rejection, and GetFramebuffer backward compatibility. Solid.

One minor observation (not blocking): ReadPixels in surface_native.go has a s == nil guard that the sibling WritePixels method lacks. The nil check is strictly better — consider adding it to WritePixels in a follow-up for consistency.

Meets #256 requirements. LGTM.

@kolkov
kolkov merged commit de58947 into gogpu:main Jul 24, 2026
13 checks passed
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.

feat: Headless software surface for golden image testing

2 participants