Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- **Pipeline disk cache** (#331) — driver-compiled GPU ISA persistence for faster cold starts on repeat launches
- **Vulkan**: `VkPipelineCache` created at device init, passed to all `vkCreateGraphicsPipelines` / `vkCreateComputePipelines`, saved via `vkGetPipelineCacheData` on device destroy. Disk path: `UserCacheDir()/gogpu/vulkan/<adapterKey>/pipeline.cache`
- **DX12**: `GetCachedBlob` after PSO creation, `D3D12_CACHED_PIPELINE_STATE` on restore. Per-PSO blobs keyed by root signature + shader bytecode + fixed-function state hash. Disk path: `UserCacheDir()/gogpu/dx12/<adapterKey>/`
- **`internal/pipelinecache`**: shared atomic blob I/O and adapter key helpers (DRY across backends)

## [0.34.1] - 2026-08-31

### Changed
Expand Down
16 changes: 10 additions & 6 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,18 @@ coverage:
default:
target: 70%
threshold: 5%
patch:
default:
target: 85%

# Ignore paths from coverage calculation
# HAL backend implementations require real GPU hardware and cannot be unit tested
# Ignore paths from coverage calculation.
# HAL backends need real GPU hardware and cannot be unit-tested meaningfully.
# Use ** globs so nested packages (hal/vulkan/..., etc.) are excluded from patch.
ignore:
- "hal/"
- "examples/"
- "cmd/"
- "tmp/"
- "hal/**"
- "examples/**"
- "cmd/**"
- "tmp/**"

# Go-specific parser settings
parsers:
Expand Down
14 changes: 9 additions & 5 deletions core/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ var (
// providers stores registered backend providers by type.
providers = make(map[gputypes.Backend]BackendProvider)

registerHALBackendsOnce sync.Once

// providerPriority defines the order in which backends are tried.
// Higher priority backends are tried first.
providerPriority = []gputypes.Backend{
Expand Down Expand Up @@ -148,12 +150,14 @@ func SelectBestBackendProvider() BackendProvider {
// This function queries the HAL registry for all registered backends and creates
// wrapper providers for them.
func RegisterHALBackends() {
for _, variant := range hal.AvailableBackends() {
backend, ok := hal.GetBackend(variant)
if ok {
RegisterBackendProvider(&halBackendProvider{backend: backend})
registerHALBackendsOnce.Do(func() {
for _, variant := range hal.AvailableBackends() {
backend, ok := hal.GetBackend(variant)
if ok {
RegisterBackendProvider(&halBackendProvider{backend: backend})
}
}
}
})
}

// FilterBackendsByMask filters backend providers by the enabled backends mask.
Expand Down
8 changes: 8 additions & 0 deletions core/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import (
"github.com/gogpu/wgpu/hal"
)

// instanceEnumerateMu serializes HAL backend probing during NewInstance.
// Concurrent CreateInstance calls on Windows CI (kolkov/racedetector) can
// otherwise race inside driver init while enumerating adapters.
var instanceEnumerateMu sync.Mutex

// Instance represents a WebGPU instance for GPU discovery and initialization.
// The instance is responsible for enumerating available GPU adapters and
// creating adapters based on application requirements.
Expand Down Expand Up @@ -134,6 +139,9 @@ func NewInstanceWithMock(desc *gputypes.InstanceDescriptor) *Instance {
// enumerateRealAdapters attempts to enumerate real GPU adapters via HAL
// backends. If none are available, the instance remains empty.
func (i *Instance) enumerateRealAdapters(desc *gputypes.InstanceDescriptor) {
instanceEnumerateMu.Lock()
defer instanceEnumerateMu.Unlock()

// First, ensure HAL backends are registered
RegisterHALBackends()

Expand Down
112 changes: 50 additions & 62 deletions descriptor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,71 +245,59 @@ func TestComputePipelineDescriptorToHAL(t *testing.T) {
}
})

t.Run("zero init workgroup memory defaults to true", func(t *testing.T) {
// When ZeroInitializeWorkgroupMemory is nil (not set), the default
// should be true per WebGPU spec.
desc := ComputePipelineDescriptor{
Label: "compute-default-zero-init",
EntryPoint: "main",
}
halDesc := desc.toHAL()
// Module is nil so ComputeState won't be filled; verify by checking
// the conversion logic directly.
if desc.ZeroInitializeWorkgroupMemory != nil {
t.Error("ZeroInitializeWorkgroupMemory should be nil by default")
}

// Verify the default logic: nil -> true
zeroInit := true
if desc.ZeroInitializeWorkgroupMemory != nil {
zeroInit = *desc.ZeroInitializeWorkgroupMemory
}
if !zeroInit {
t.Error("default zero_initialize_workgroup_memory should be true")
}

_ = halDesc // used above
})

t.Run("zero init workgroup memory explicit false", func(t *testing.T) {
explicitFalse := false
desc := ComputePipelineDescriptor{
Label: "compute-no-zero-init",
EntryPoint: "main",
ZeroInitializeWorkgroupMemory: &explicitFalse,
}

// Verify the conversion logic: explicit false -> false
zeroInit := true
if desc.ZeroInitializeWorkgroupMemory != nil {
zeroInit = *desc.ZeroInitializeWorkgroupMemory
}
if zeroInit {
t.Error("explicit false should yield zero_initialize_workgroup_memory=false")
}

halDesc := desc.toHAL()
_ = halDesc
})

t.Run("zero init workgroup memory explicit true", func(t *testing.T) {
explicitTrue := true
desc := ComputePipelineDescriptor{
Label: "compute-explicit-zero-init",
EntryPoint: "main",
ZeroInitializeWorkgroupMemory: &explicitTrue,
// Zero-init cases run sequentially in one block; separate t.Run subtests
// previously reused stack-scoped *bool fields during toHAL().
t.Run("zero init workgroup memory", func(t *testing.T) {
{
desc := ComputePipelineDescriptor{
Label: "compute-default-zero-init",
EntryPoint: "main",
}
halDesc := desc.toHAL()
if desc.ZeroInitializeWorkgroupMemory != nil {
t.Error("ZeroInitializeWorkgroupMemory should be nil by default")
}
zeroInit := true
if desc.ZeroInitializeWorkgroupMemory != nil {
zeroInit = *desc.ZeroInitializeWorkgroupMemory
}
if !zeroInit {
t.Error("default zero_initialize_workgroup_memory should be true")
}
_ = halDesc
}

zeroInit := true
if desc.ZeroInitializeWorkgroupMemory != nil {
zeroInit = *desc.ZeroInitializeWorkgroupMemory
{
explicitFalse := false
desc := ComputePipelineDescriptor{
Label: "compute-no-zero-init",
EntryPoint: "main",
ZeroInitializeWorkgroupMemory: &explicitFalse,
}
zeroInit := true
if desc.ZeroInitializeWorkgroupMemory != nil {
zeroInit = *desc.ZeroInitializeWorkgroupMemory
}
if zeroInit {
t.Error("explicit false should yield zero_initialize_workgroup_memory=false")
}
_ = desc.toHAL()
}
if !zeroInit {
t.Error("explicit true should yield zero_initialize_workgroup_memory=true")
{
explicitTrue := true
desc := ComputePipelineDescriptor{
Label: "compute-explicit-zero-init",
EntryPoint: "main",
ZeroInitializeWorkgroupMemory: &explicitTrue,
}
zeroInit := true
if desc.ZeroInitializeWorkgroupMemory != nil {
zeroInit = *desc.ZeroInitializeWorkgroupMemory
}
if !zeroInit {
t.Error("explicit true should yield zero_initialize_workgroup_memory=true")
}
_ = desc.toHAL()
}

halDesc := desc.toHAL()
_ = halDesc
})
}

Expand Down
4 changes: 2 additions & 2 deletions hal/dx12/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ func (a *Adapter) Open(features gputypes.Features, limits gputypes.Limits) (hal.
}

// Create device using the adapter
device, err := newDevice(a.instance, unsafe.Pointer(a.raw), a.capabilities.FeatureLevel)
device, err := newDevice(a.instance, unsafe.Pointer(a.raw), a.desc, a.capabilities.FeatureLevel)
if err != nil {
return hal.OpenDevice{}, err
}
Expand Down Expand Up @@ -642,7 +642,7 @@ func (a *AdapterLegacy) Open(features gputypes.Features, limits gputypes.Limits)
}

// Create device using the legacy adapter
device, err := newDevice(a.instance, unsafe.Pointer(a.raw), a.capabilities.FeatureLevel)
device, err := newDevice(a.instance, unsafe.Pointer(a.raw), a.desc, a.capabilities.FeatureLevel)
if err != nil {
return hal.OpenDevice{}, err
}
Expand Down
40 changes: 40 additions & 0 deletions hal/dx12/d3d12/pipeline_state.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright 2025 The GoGPU Authors
// SPDX-License-Identifier: MIT

//go:build windows && !(js && wasm)

package d3d12

import (
"syscall"
"unsafe"
)

// GetCachedBlob returns the driver-compiled PSO blob for disk caching.
// The blob can be passed back via D3D12_CACHED_PIPELINE_STATE on next launch.
func (p *ID3D12PipelineState) GetCachedBlob() ([]byte, error) {
var blob *ID3DBlob
ret, _, _ := syscall.Syscall(
p.vtbl.GetCachedBlob,
2,
uintptr(unsafe.Pointer(p)),
uintptr(unsafe.Pointer(&blob)),
0,
)
if ret != 0 {
return nil, HRESULTError(ret)
}
if blob == nil {
return nil, nil
}
defer blob.Release()

ptr := blob.GetBufferPointer()
size := blob.GetBufferSize()
if ptr == nil || size == 0 {
return nil, nil
}
data := make([]byte, size)
copy(data, unsafe.Slice((*byte)(ptr), size))
return data, nil
}
Loading
Loading