Skip to content

Commit 13a6f99

Browse files
Kaappibgreen
authored andcommitted
feat: Create explicit pipeline layouts from shaderLayout (#2355)
1 parent 8ac4e59 commit 13a6f99

File tree

6 files changed

+176
-7
lines changed

6 files changed

+176
-7
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import {Device} from '../device';
2+
import {ShaderLayout} from '../types/shader-layout';
3+
import {Resource, ResourceProps} from './resource';
4+
5+
export type PipelineLayoutProps = ResourceProps & {
6+
shaderLayout: ShaderLayout;
7+
};
8+
9+
/** Immutable PipelineLayout object */
10+
export abstract class PipelineLayout extends Resource<PipelineLayoutProps> {
11+
get [Symbol.toStringTag](): string {
12+
return 'PipelineLayout';
13+
}
14+
15+
constructor(device: Device, props: PipelineLayoutProps) {
16+
super(device, props, PipelineLayout.defaultProps);
17+
}
18+
19+
static override defaultProps: Required<PipelineLayoutProps> = {
20+
...Resource.defaultProps,
21+
shaderLayout: {
22+
attributes: [],
23+
bindings: []
24+
}
25+
};
26+
}

modules/core/src/adapter/types/shader-layout.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ export type StorageBufferBindingLayout = {
110110
minBindingSize?: number;
111111
};
112112

113-
type TextureBindingLayout = {
113+
export type TextureBindingLayout = {
114114
type: 'texture';
115115
/** Name of the binding. Used by luma to map bindings by name */
116116
name: string;
@@ -125,7 +125,7 @@ type TextureBindingLayout = {
125125
multisampled?: boolean;
126126
};
127127

128-
type SamplerBindingLayout = {
128+
export type SamplerBindingLayout = {
129129
type: 'sampler';
130130
/** Name of the binding. Used by luma to map bindings by name */
131131
name: string;
@@ -138,7 +138,7 @@ type SamplerBindingLayout = {
138138
samplerType?: 'filtering' | 'non-filtering' | 'comparison'; // default: filtering
139139
};
140140

141-
type StorageTextureBindingLayout = {
141+
export type StorageTextureBindingLayout = {
142142
type: 'storage';
143143
/** Name of the binding. Used by luma to map bindings by name */
144144
name: string;

modules/core/src/index.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ export {TransformFeedback} from './adapter/resources/transform-feedback';
7272
export type {QuerySetProps} from './adapter/resources/query-set';
7373
export {QuerySet} from './adapter/resources/query-set';
7474

75+
export type {PipelineLayoutProps} from './adapter/resources/pipeline-layout';
76+
export {PipelineLayout} from './adapter/resources/pipeline-layout';
77+
7578
// PORTABLE API - UNIFORM BUFFERS
7679
export {UniformBufferLayout} from './portable/uniform-buffer-layout';
7780
export {UniformBlock} from './portable/uniform-block';
@@ -122,7 +125,12 @@ export type {
122125
ComputeShaderLayout,
123126
AttributeDeclaration,
124127
BindingDeclaration,
125-
Binding
128+
Binding,
129+
UniformBufferBindingLayout,
130+
StorageBufferBindingLayout,
131+
TextureBindingLayout,
132+
SamplerBindingLayout,
133+
StorageTextureBindingLayout
126134
} from './adapter/types/shader-layout';
127135
export type {BufferLayout, BufferAttributeLayout} from './adapter/types/buffer-layout';
128136
export type {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import {
2+
PipelineLayout,
3+
PipelineLayoutProps,
4+
StorageBufferBindingLayout,
5+
StorageTextureBindingLayout
6+
} from '@luma.gl/core';
7+
import {WebGPUDevice} from '../webgpu-device';
8+
9+
const VISIBILITY_ALL = GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT | GPUShaderStage.COMPUTE;
10+
11+
export class WebGPUPipelineLayout extends PipelineLayout {
12+
device: WebGPUDevice;
13+
handle: GPUPipelineLayout;
14+
15+
constructor(device: WebGPUDevice, props: PipelineLayoutProps) {
16+
super(device, props);
17+
18+
this.device = device;
19+
20+
const bindGroupEntries = this.mapShaderLayoutToBindGroupEntries();
21+
22+
this.handle = this.device.handle.createPipelineLayout({
23+
label: props?.id ?? 'unnamed-pipeline-layout',
24+
bindGroupLayouts: [
25+
// TODO (kaapp): We can cache these to re-use them across
26+
// layers, particularly if using a separate group for injected
27+
// bindings (e.g. project/lighting)
28+
this.device.handle.createBindGroupLayout({
29+
label: 'bind-group-layout',
30+
entries: bindGroupEntries
31+
})
32+
]
33+
});
34+
}
35+
36+
override destroy(): void {
37+
// WebGPUPipelineLayout has no destroy method.
38+
// @ts-expect-error
39+
this.handle = null;
40+
}
41+
42+
protected mapShaderLayoutToBindGroupEntries(): GPUBindGroupLayoutEntry[] {
43+
// Set up the pipeline layout
44+
// TODO (kaapp): This only supports the first group, but so does the rest of the code
45+
const bindGroupEntries: GPUBindGroupLayoutEntry[] = [];
46+
47+
for (let i = 0; i < this.props.shaderLayout.bindings.length; i++) {
48+
const binding = this.props.shaderLayout.bindings[i];
49+
const bindingTypeInfo: Omit<GPUBindGroupLayoutEntry, 'binding' | 'visibility'> = {};
50+
51+
switch (binding.type) {
52+
case 'uniform': {
53+
bindingTypeInfo.buffer = {
54+
type: 'uniform',
55+
hasDynamicOffset: binding.hasDynamicOffset,
56+
minBindingSize: binding.minBindingSize
57+
};
58+
break;
59+
}
60+
61+
case 'read-only-storage': {
62+
bindingTypeInfo.buffer = {
63+
type: 'read-only-storage',
64+
hasDynamicOffset: binding.hasDynamicOffset,
65+
minBindingSize: binding.minBindingSize
66+
};
67+
break;
68+
}
69+
70+
case 'sampler': {
71+
bindingTypeInfo.sampler = {
72+
type: binding.samplerType
73+
};
74+
break;
75+
}
76+
77+
case 'storage': {
78+
if (isStorageTextureBindingLayout(binding)) {
79+
bindingTypeInfo.storageTexture = {
80+
// TODO (kaapp): Not all formats in the binding layout are supported
81+
// by WebGPU, but at least it will provide a clear error for now.
82+
format: binding.format as GPUTextureFormat,
83+
access: binding.access,
84+
viewDimension: binding.viewDimension
85+
};
86+
} else {
87+
bindingTypeInfo.buffer = {
88+
type: 'storage',
89+
hasDynamicOffset: binding.hasDynamicOffset,
90+
minBindingSize: binding.minBindingSize
91+
};
92+
}
93+
break;
94+
}
95+
96+
case 'texture': {
97+
bindingTypeInfo.texture = {
98+
multisampled: binding.multisampled,
99+
sampleType: binding.sampleType,
100+
viewDimension: binding.viewDimension
101+
};
102+
break;
103+
}
104+
105+
default: {
106+
console.warn('unhandled binding type when creating pipeline descriptor');
107+
}
108+
}
109+
110+
bindGroupEntries.push({
111+
binding: binding.location,
112+
visibility: binding.visibility || VISIBILITY_ALL,
113+
...bindingTypeInfo
114+
});
115+
}
116+
117+
return bindGroupEntries;
118+
}
119+
}
120+
121+
const isStorageTextureBindingLayout = (
122+
maybe: StorageBufferBindingLayout | StorageTextureBindingLayout
123+
): maybe is StorageTextureBindingLayout => {
124+
return (maybe as StorageTextureBindingLayout).format !== undefined;
125+
};

modules/webgpu/src/adapter/resources/webgpu-render-pipeline.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -168,14 +168,18 @@ export class WebGPURenderPipeline extends RenderPipeline {
168168
]
169169
};
170170

171+
const layout = this.device.createPipelineLayout({
172+
shaderLayout: this.shaderLayout
173+
});
174+
171175
// Create a partially populated descriptor
172176
const descriptor: GPURenderPipelineDescriptor = {
173177
vertex,
174178
fragment,
175179
primitive: {
176180
topology: this.props.topology
177181
},
178-
layout: 'auto'
182+
layout: layout.handle
179183
};
180184

181185
if (this.props.parameters.depthWriteEnabled && this.props.parameters.depthCompare) {

modules/webgpu/src/adapter/webgpu-device.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ import type {
2828
TransformFeedbackProps,
2929
QuerySet,
3030
QuerySetProps,
31-
DeviceProps
31+
DeviceProps,
32+
CommandEncoderProps,
33+
PipelineLayoutProps,
3234
} from '@luma.gl/core';
3335
import {Device, DeviceFeatures} from '@luma.gl/core';
3436
import {WebGPUBuffer} from './resources/webgpu-buffer';
@@ -46,6 +48,7 @@ import {WebGPUVertexArray} from './resources/webgpu-vertex-array';
4648

4749
import {WebGPUCanvasContext} from './webgpu-canvas-context';
4850
import {WebGPUQuerySet} from './resources/webgpu-query-set';
51+
import {WebGPUPipelineLayout} from './resources/webgpu-pipeline-layout';
4952

5053
/** WebGPU Device implementation */
5154
export class WebGPUDevice extends Device {
@@ -194,6 +197,10 @@ export class WebGPUDevice extends Device {
194197
return new WebGPUCanvasContext(this, this.adapter, props);
195198
}
196199

200+
createPipelineLayout(props: PipelineLayoutProps): WebGPUPipelineLayout {
201+
return new WebGPUPipelineLayout(this, props);
202+
}
203+
197204
submit(): void {
198205
const commandBuffer = this.commandEncoder?.finish();
199206
if (commandBuffer) {
@@ -205,7 +212,6 @@ export class WebGPUDevice extends Device {
205212
}
206213
});
207214
}
208-
this.commandEncoder = null;
209215
}
210216

211217
// PRIVATE METHODS

0 commit comments

Comments
 (0)