feat: pipeline disk cache for Vulkan and DX12 (#331) - #339
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
kolkov
left a comment
There was a problem hiding this comment.
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)
DX12AdapterKeyincludes 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.KeepAliveforinitialDataafterCreatePipelineCacheandcachedBlobafter DX12 PSO creation — GC could theoretically collect during FFI call. writeInputElementwrites only 3 of 4 bytes ofInstanceDataStepRate— theoretical collision for values >= 16M.- Test comments claim
t.Runsubtests run concurrently — they don't withoutt.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!
Correct Vulkan cache save pointer, DX12 PSO key hashing, and make pipeline cache init non-fatal when the driver rejects disk data.
b328ba3 to
e2091a5
Compare
|
Addressed all items from review #5061846529 in Blockers
Important
Minor
Also
Ready for re-review. |
Integrate v0.34.0 CHANGELOG (ADR-072 struct params) with pipeline cache [Unreleased] section for #339.
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.
306f68b to
bde5910
Compare
kolkov
left a comment
There was a problem hiding this comment.
All 8 issues from initial review verified as fixed. Enterprise-quality implementation.
Blockers resolved:
- Vulkan GetPipelineCacheData — correct pointer indirection + KeepAlive
- DX12 cache key — math.Float32bits() for DepthBiasClamp/SlopeScaledDepthBias
- 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!
Summary
VkPipelineCacheand DX12CachedPSO/GetCachedBlob(perf: pipeline cache — Vulkan VkPipelineCache + DX12 PSO disk cache #331)internal/pipelinecachehelpers for atomic blob I/O and adapter-scoped cache paths underUserCacheDir()/gogpu/...Test plan
go test ./internal/pipelinecache/... ./hal/vulkan/...UserCacheDir()/gogpu/vulkan/<key>/pipeline.cache.psoblobs underUserCacheDir()/gogpu/dx12/<key>/Closes #331