Skip to content

feat: pipeline disk cache for Vulkan and DX12 (#331) - #339

Merged
kolkov merged 6 commits into
mainfrom
feature/pipeline-cache-331
Aug 31, 2026
Merged

feat: pipeline disk cache for Vulkan and DX12 (#331)#339
kolkov merged 6 commits into
mainfrom
feature/pipeline-cache-331

Conversation

@lkmavi

@lkmavi lkmavi commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Persist driver-compiled GPU ISA across launches via Vulkan VkPipelineCache and DX12 CachedPSO / GetCachedBlob (perf: pipeline cache — Vulkan VkPipelineCache + DX12 PSO disk cache #331)
  • Shared internal/pipelinecache helpers for atomic blob I/O and adapter-scoped cache paths under UserCacheDir()/gogpu/...
  • Stale/invalid cache blobs fall back to empty cache / recreate without crashing

Test plan

  • go test ./internal/pipelinecache/... ./hal/vulkan/...
  • Vulkan: create pipelines twice across process restarts; confirm cache file under UserCacheDir()/gogpu/vulkan/<key>/pipeline.cache
  • DX12 (Windows): create render/compute PSOs twice; confirm .pso blobs under UserCacheDir()/gogpu/dx12/<key>/
  • Corrupt/stale cache file → graceful fallback (empty Vulkan cache / DX12 retry without CachedPSO)
  • CI green on PR

Closes #331

@lkmavi
lkmavi requested a review from kolkov as a code owner August 30, 2026 18:03
@codecov

codecov Bot commented Aug 30, 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.

Excellent work, @lkmavi! This is a substantial contribution — pipeline disk cache is a real-world performance win. Architecture is solid: internal/pipelinecache/ follows ADR-069 correctly, Vulkan monolithic VkPipelineCache matches both Rust wgpu and Dawn, DX12 GetCachedBlob/CachedPSO is correct pattern (ahead of Rust wgpu which has an empty stub). Race fixes are welcome improvements.

5 issues found during enterprise-level review (3 blockers, 2 important):

Blockers

1. Vulkan savePipelineCache — wrong pointer indirection (stack corruption)

dataPtr := uintptr(unsafe.Pointer(&data[0]))
result = d.cmds.GetPipelineCacheData(d.handle, d.pipelineCache, &size, &dataPtr)

&dataPtr passes the address of the stack variable dataPtr, not the buffer address. Vulkan writes cache data to the stack, corrupting memory for any cache > 8 bytes. Fix:

bufPtr := (*uintptr)(unsafe.Pointer(&data[0]))
result = d.cmds.GetPipelineCacheData(d.handle, d.pipelineCache, &size, bufPtr)
runtime.KeepAlive(data)

2. DX12 writeRasterizer — float32 truncation in cache key

binary.LittleEndian.PutUint32(buf[12:16], uint32(rs.DepthBiasClamp))
binary.LittleEndian.PutUint32(buf[16:20], uint32(rs.SlopeScaledDepthBias))

uint32(float32) truncates — 0.001 and 0.002 both become 0. Two pipelines differing only in depth bias would share a cache key → wrong cached PSO used. Fix:

binary.LittleEndian.PutUint32(buf[12:16], math.Float32bits(rs.DepthBiasClamp))
binary.LittleEndian.PutUint32(buf[16:20], math.Float32bits(rs.SlopeScaledDepthBias))

3. DX12 writeDepthStencil — incomplete hash (stencil state missing)

func writeDepthStencil(h hash.Hash, ds *d3d12.D3D12_DEPTH_STENCIL_DESC) {
    var buf [24]byte       // allocates 24 but writes only 4
    buf[0] = boolByte(ds.DepthEnable)
    buf[1] = byte(ds.DepthWriteMask)
    buf[2] = byte(ds.DepthFunc)
    buf[3] = boolByte(ds.StencilEnable)
    _, _ = h.Write(buf[:4])
}

Missing: StencilReadMask, StencilWriteMask, FrontFace (StencilFailOp, StencilDepthFailOp, StencilPassOp, StencilFunc), BackFace (same 4 fields). Two PSOs with identical depth but different stencil operations would collide → incorrect stencil rendering.

Important

4. Vulkan initPipelineCache failure is fatal

If initPipelineCache fails (e.g., os.UserCacheDir() unavailable on some Linux without $HOME), device creation fails entirely. Pipeline cache is a performance optimization — device should work without it. Consider logging a warning and continuing with pipelineCache = 0 (VK_NULL_HANDLE).

5. HexKey double-hashing in DX12 PSO cache key

func graphicsPSOCacheKey(...) string {
    h := sha256New()
    // ... write all PSO state to h ...
    return pipelinecache.HexKey(digestBytes(h))  // SHA-256 of SHA-256!
}

digestBytes(h) already returns SHA-256 digest. HexKey() computes another SHA-256 of that digest. Correct but wastes CPU. Use hex.EncodeToString(digestBytes(h)) directly.


Minor (not blocking)

  • DX12AdapterKey includes LUID which is session-scoped on some systems — cache directory may change on reboot. The E_INVALIDARG fallback handles this, so not a correctness issue.
  • Missing runtime.KeepAlive for initialData after CreatePipelineCache and cachedBlob after DX12 PSO creation — GC could theoretically collect during FFI call.
  • writeInputElement writes only 3 of 4 bytes of InstanceDataStepRate — theoretical collision for values >= 16M.
  • Test comments claim t.Run subtests run concurrently — they don't without t.Parallel(). Comments should be corrected.

Great foundation. The three blockers are straightforward fixes (pointer indirection, math.Float32bits, complete stencil hash). Happy to re-review after fixes!

lkmavi added a commit that referenced this pull request Aug 31, 2026
Correct Vulkan cache save pointer, DX12 PSO key hashing, and make
pipeline cache init non-fatal when the driver rejects disk data.
@lkmavi
lkmavi force-pushed the feature/pipeline-cache-331 branch from b328ba3 to e2091a5 Compare August 31, 2026 07:43
@lkmavi

lkmavi commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Addressed all items from review #5061846529 in e2091a5 (rebased onto main, CHANGELOG conflict resolved).

Blockers

  1. Vulkan savePipelineCache buffer pointerGetPipelineCacheData now receives (*uintptr)(unsafe.Pointer(&data[0])) instead of &dataPtr; added runtime.KeepAlive(data).
  2. DX12 PSO cache key float truncationDepthBiasClamp and SlopeScaledDepthBias are hashed via math.Float32bits() instead of uint32() cast.
  3. Incomplete depth/stencil hash — cache key now includes StencilReadMask, StencilWriteMask, and full FrontFace/BackFace stencil ops via writeStencilOp.

Important

  1. Non-fatal pipeline cache initinitPipelineCache no longer fails device creation on driver/cache errors; logs a warning and continues with pipelineCache = 0 (VK_NULL_HANDLE).
  2. Single SHA-256 for DX12 blob filenames — replaced HexKey(digest) (double hash) with hex.EncodeToString(digestBytes(h)).

Minor

  1. runtime.KeepAlive — added for initialData (Vulkan create) and cachedBlob (DX12 restore).
  2. InstanceDataStepRate — full uint32 written in a dedicated 5-byte block (slot class + step rate).
  3. Test comments — corrected misleading “concurrent” notes in descriptor_test.go and headless_surface_native_test.go (t.Run is sequential unless t.Parallel()).

Also

Ready for re-review.

lkmavi added a commit that referenced this pull request Aug 31, 2026
Integrate v0.34.0 CHANGELOG (ADR-072 struct params) with pipeline cache
[Unreleased] section for #339.
lkmavi added 6 commits August 31, 2026 15:17
Persist driver-compiled GPU ISA across launches via VkPipelineCache and
DX12 CachedPSO blobs to cut cold-start pipeline creation time.
RenderTargetWriteMask is uint8; ColorTargetState flags need a bool helper.
Cover disk I/O error paths and fix codecov ignore globs so nested
hal/** packages stay excluded from patch coverage.
Register HAL backends once and guard enumerateRealAdapters with a mutex
so concurrent CreateInstance calls do not race on Windows racedetector.
Collapse descriptor/headless subtests that shared stack-scoped state.
Windows resolves os.UserCacheDir without HOME/USERPROFILE, so the error
path is only exercised on Unix-like CI hosts.
Correct Vulkan cache save pointer, DX12 PSO key hashing, and make
pipeline cache init non-fatal when the driver rejects disk data.
@lkmavi
lkmavi force-pushed the feature/pipeline-cache-331 branch from 306f68b to bde5910 Compare August 31, 2026 11:18

@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 8 issues from initial review verified as fixed. Enterprise-quality implementation.

Blockers resolved:

  1. Vulkan GetPipelineCacheData — correct pointer indirection + KeepAlive
  2. DX12 cache key — math.Float32bits() for DepthBiasClamp/SlopeScaledDepthBias
  3. Stencil hash — all 14 fields including writeStencilOp helper

Important fixes:
4. Non-fatal pipeline cache init (warning + continue)
5. Single SHA-256 for DX12 blob filenames

CI 15/15 green. Rebased on v0.34.1. Excellent work, @lkmavi!

@kolkov
kolkov merged commit 13118ee into main Aug 31, 2026
15 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.

perf: pipeline cache — Vulkan VkPipelineCache + DX12 PSO disk cache

2 participants