Skip to content

Commit ecbe55d

Browse files
authored
feat: Add support for setting stencil reference for pipeline and add a simple stencil example (#1979)
1 parent 118e8b1 commit ecbe55d

9 files changed

Lines changed: 307 additions & 11 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<canvas></canvas>
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import tgpu from 'typegpu';
2+
import * as d from 'typegpu/data';
3+
4+
const root = await tgpu.init();
5+
const canvas = document.querySelector('canvas') as HTMLCanvasElement;
6+
const context = canvas.getContext('webgpu') as GPUCanvasContext;
7+
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
8+
9+
context.configure({
10+
device: root.device,
11+
format: presentationFormat,
12+
});
13+
14+
let stencilTexture = root['~unstable'].createTexture({
15+
size: [canvas.width, canvas.height],
16+
format: 'stencil8',
17+
}).$usage('render');
18+
19+
const triangleData = {
20+
vertices: tgpu.const(d.arrayOf(d.vec2f, 3), [
21+
d.vec2f(0, 0.5),
22+
d.vec2f(-0.5, -0.5),
23+
d.vec2f(0.5, -0.5),
24+
]),
25+
uvs: tgpu.const(d.arrayOf(d.vec2f, 3), [
26+
d.vec2f(0.5, 1),
27+
d.vec2f(0, 0),
28+
d.vec2f(1, 0),
29+
]),
30+
};
31+
32+
const rotationUniform = root.createUniform(d.mat2x2f, d.mat2x2f.identity());
33+
34+
const vertexFn = tgpu['~unstable'].vertexFn({
35+
in: {
36+
vid: d.builtin.vertexIndex,
37+
},
38+
out: {
39+
position: d.builtin.position,
40+
uv: d.vec2f,
41+
},
42+
})(({ vid }) => {
43+
const pos = triangleData.vertices.$[vid];
44+
const uv = triangleData.uvs.$[vid];
45+
46+
const rotatedPos = rotationUniform.$.mul(pos);
47+
48+
return {
49+
position: d.vec4f(rotatedPos, 0, 1),
50+
uv,
51+
};
52+
});
53+
54+
const fragmentFn = tgpu['~unstable'].fragmentFn({
55+
in: {
56+
uv: d.vec2f,
57+
},
58+
out: d.vec4f,
59+
})(({ uv }) => d.vec4f(uv, 0, 1));
60+
61+
const basePipeline = root['~unstable']
62+
.withVertex(vertexFn);
63+
64+
const writeStencilPipeline = basePipeline
65+
.withDepthStencil({
66+
format: 'stencil8',
67+
stencilFront: { passOp: 'replace' },
68+
})
69+
.createPipeline()
70+
.withStencilReference(1);
71+
72+
const testStencilPipeline = basePipeline
73+
.withFragment(fragmentFn, { format: presentationFormat })
74+
.withDepthStencil({
75+
format: 'stencil8',
76+
stencilFront: {
77+
compare: 'equal',
78+
passOp: 'keep',
79+
},
80+
})
81+
.createPipeline()
82+
.withStencilReference(1);
83+
84+
writeStencilPipeline
85+
.withDepthStencilAttachment({
86+
view: stencilTexture,
87+
stencilClearValue: 0,
88+
stencilLoadOp: 'clear',
89+
stencilStoreOp: 'store',
90+
}).draw(3);
91+
92+
let frameId: number;
93+
function frame(timestamp: number) {
94+
const rotationAngle = (timestamp / 1000) * Math.PI * 0.5;
95+
const cosA = Math.cos(rotationAngle);
96+
const sinA = Math.sin(rotationAngle);
97+
rotationUniform.write(d.mat2x2f(cosA, -sinA, sinA, cosA));
98+
99+
testStencilPipeline
100+
.withDepthStencilAttachment({
101+
view: stencilTexture,
102+
stencilLoadOp: 'load',
103+
stencilStoreOp: 'store',
104+
})
105+
.withColorAttachment({
106+
view: context.getCurrentTexture().createView(),
107+
loadOp: 'clear',
108+
storeOp: 'store',
109+
})
110+
.draw(3);
111+
112+
frameId = requestAnimationFrame(frame);
113+
}
114+
frameId = requestAnimationFrame(frame);
115+
116+
const resizeObserver = new ResizeObserver(() => {
117+
stencilTexture = root['~unstable'].createTexture({
118+
size: [canvas.width, canvas.height],
119+
format: 'stencil8',
120+
}).$usage('render');
121+
122+
rotationUniform.write(d.mat2x2f.identity());
123+
124+
writeStencilPipeline.withDepthStencilAttachment({
125+
view: stencilTexture,
126+
stencilClearValue: 0,
127+
stencilLoadOp: 'clear',
128+
stencilStoreOp: 'store',
129+
}).draw(3);
130+
});
131+
resizeObserver.observe(canvas);
132+
133+
export function onCleanup() {
134+
if (frameId) {
135+
cancelAnimationFrame(frameId);
136+
}
137+
root.destroy();
138+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"title": "Stencil",
3+
"category": "simple",
4+
"tags": ["rendering", "experimental"]
5+
}
190 KB
Loading

packages/typegpu/src/core/pipeline/renderPipeline.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ export interface TgpuRenderPipeline<Output extends IOLayout = IOLayout>
127127
attachment: DepthStencilAttachment,
128128
): this;
129129

130+
withStencilReference(
131+
reference: GPUStencilValue,
132+
): this;
133+
130134
withIndexBuffer(
131135
buffer: TgpuBuffer<AnyWgslData> & IndexFlag,
132136
offsetElements?: number,
@@ -332,6 +336,7 @@ type TgpuRenderPipelinePriors = {
332336
| undefined;
333337
readonly colorAttachment?: AnyFragmentColorAttachment | undefined;
334338
readonly depthStencilAttachment?: DepthStencilAttachment | undefined;
339+
readonly stencilReference?: GPUStencilValue | undefined;
335340
readonly indexBuffer?:
336341
| {
337342
buffer: TgpuBuffer<AnyWgslData> & IndexFlag | GPUBuffer;
@@ -479,6 +484,17 @@ class TgpuRenderPipelineImpl implements TgpuRenderPipeline {
479484
}) as this;
480485
}
481486

487+
withStencilReference(
488+
reference: GPUStencilValue,
489+
): this {
490+
const internals = this[$internal];
491+
492+
return new TgpuRenderPipelineImpl(internals.core, {
493+
...internals.priors,
494+
stencilReference: reference,
495+
}) as this;
496+
}
497+
482498
withIndexBuffer(
483499
buffer: TgpuBuffer<AnyWgslData> & IndexFlag,
484500
offsetElements?: number,
@@ -593,6 +609,10 @@ class TgpuRenderPipelineImpl implements TgpuRenderPipeline {
593609

594610
pass.setPipeline(memo.pipeline);
595611

612+
if (internals.priors.stencilReference !== undefined) {
613+
pass.setStencilReference(internals.priors.stencilReference);
614+
}
615+
596616
const missingBindGroups = new Set(memo.usedBindGroupLayouts);
597617

598618
memo.usedBindGroupLayouts.forEach((layout, idx) => {

packages/typegpu/src/tgsl/accessIndex.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
import { stitch } from '../core/resolve/stitch.ts';
2-
import { AnyData, isDisarray, MatrixColumnsAccess } from '../data/dataTypes.ts';
2+
import {
3+
type AnyData,
4+
isDisarray,
5+
MatrixColumnsAccess,
6+
} from '../data/dataTypes.ts';
37
import { derefSnippet } from '../data/ref.ts';
4-
import { isEphemeralSnippet, Origin, snip, Snippet } from '../data/snippet.ts';
8+
import {
9+
isEphemeralSnippet,
10+
type Origin,
11+
snip,
12+
type Snippet,
13+
} from '../data/snippet.ts';
514
import { vec2f, vec3f, vec4f } from '../data/vector.ts';
615
import {
716
isNaturallyEphemeral,
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
5+
import { describe, expect } from 'vitest';
6+
import { it } from '../../utils/extendedIt.ts';
7+
import { mockResizeObserver } from '../utils/commonMocks.ts';
8+
import { runExampleTest, setupCommonMocks } from '../utils/baseTest.ts';
9+
10+
describe('stencil example', () => {
11+
setupCommonMocks();
12+
13+
it('should produce valid code', async ({ device }) => {
14+
const shaderCodes = await runExampleTest({
15+
category: 'simple',
16+
name: 'stencil',
17+
expectedCalls: 2,
18+
setupMocks: mockResizeObserver,
19+
}, device);
20+
21+
expect(shaderCodes).toMatchInlineSnapshot(`
22+
"const vertices_1: array<vec2f, 3> = array<vec2f, 3>(vec2f(0, 0.5), vec2f(-0.5), vec2f(0.5, -0.5));
23+
24+
const uvs_2: array<vec2f, 3> = array<vec2f, 3>(vec2f(0.5, 1), vec2f(), vec2f(1, 0));
25+
26+
@group(0) @binding(0) var<uniform> rotationUniform_3: mat2x2f;
27+
28+
struct vertexFn_Output_4 {
29+
@builtin(position) position: vec4f,
30+
@location(0) uv: vec2f,
31+
}
32+
33+
struct vertexFn_Input_5 {
34+
@builtin(vertex_index) vid: u32,
35+
}
36+
37+
@vertex fn vertexFn_0(_arg_0: vertexFn_Input_5) -> vertexFn_Output_4 {
38+
let pos = vertices_1[_arg_0.vid];
39+
let uv = uvs_2[_arg_0.vid];
40+
var rotatedPos = (rotationUniform_3 * pos);
41+
return vertexFn_Output_4(vec4f(rotatedPos, 0f, 1f), uv);
42+
}
43+
44+
const vertices_1: array<vec2f, 3> = array<vec2f, 3>(vec2f(0, 0.5), vec2f(-0.5), vec2f(0.5, -0.5));
45+
46+
const uvs_2: array<vec2f, 3> = array<vec2f, 3>(vec2f(0.5, 1), vec2f(), vec2f(1, 0));
47+
48+
@group(0) @binding(0) var<uniform> rotationUniform_3: mat2x2f;
49+
50+
struct vertexFn_Output_4 {
51+
@builtin(position) position: vec4f,
52+
@location(0) uv: vec2f,
53+
}
54+
55+
struct vertexFn_Input_5 {
56+
@builtin(vertex_index) vid: u32,
57+
}
58+
59+
@vertex fn vertexFn_0(_arg_0: vertexFn_Input_5) -> vertexFn_Output_4 {
60+
let pos = vertices_1[_arg_0.vid];
61+
let uv = uvs_2[_arg_0.vid];
62+
var rotatedPos = (rotationUniform_3 * pos);
63+
return vertexFn_Output_4(vec4f(rotatedPos, 0f, 1f), uv);
64+
}
65+
66+
struct fragmentFn_Input_7 {
67+
@location(0) uv: vec2f,
68+
}
69+
70+
@fragment fn fragmentFn_6(_arg_0: fragmentFn_Input_7) -> @location(0) vec4f {
71+
return vec4f(_arg_0.uv, 0f, 1f);
72+
}"
73+
`);
74+
});
75+
});

packages/typegpu/tests/renderPipeline.test.ts

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,32 +32,32 @@ describe('TgpuRenderPipeline', () => {
3232

3333
// Using none
3434
const pipeline = root
35-
.withVertex(vert, {})
36-
.withFragment(emptyFragment, {})
35+
.withVertex(vert)
36+
.withFragment(emptyFragment)
3737
.createPipeline();
3838

3939
// Using none (builtins are erased from the vertex output)
4040
const pipeline2 = root
41-
.withVertex(vertWithBuiltin, {})
42-
.withFragment(emptyFragment, {})
41+
.withVertex(vertWithBuiltin)
42+
.withFragment(emptyFragment)
4343
.createPipeline();
4444

4545
// Using none (builtins are ignored in the fragment input)
4646
const pipeline3 = root
47-
.withVertex(vert, {})
48-
.withFragment(emptyFragmentWithBuiltin, {})
47+
.withVertex(vert)
48+
.withFragment(emptyFragmentWithBuiltin)
4949
.createPipeline();
5050

5151
// Using none (builtins are ignored in both input and output,
5252
// so their conflict of the `pos` key is fine)
5353
const pipeline4 = root
54-
.withVertex(vertWithBuiltin, {})
55-
.withFragment(emptyFragmentWithBuiltin, {})
54+
.withVertex(vertWithBuiltin)
55+
.withFragment(emptyFragmentWithBuiltin)
5656
.createPipeline();
5757

5858
// Using all
5959
const pipeline5 = root
60-
.withVertex(vert, {})
60+
.withVertex(vert)
6161
.withFragment(fullFragment, { format: 'rgba8unorm' })
6262
.createPipeline();
6363

@@ -795,6 +795,53 @@ describe('TgpuRenderPipeline', () => {
795795
);
796796
});
797797

798+
it('should handle stencil reference value correctly', ({ root, commandEncoder }) => {
799+
const vertexFn = tgpu['~unstable']
800+
.vertexFn({
801+
out: { pos: d.builtin.position },
802+
})('')
803+
.$name('vertex');
804+
805+
const fragmentFn = tgpu['~unstable']
806+
.fragmentFn({
807+
out: { color: d.vec4f },
808+
})('')
809+
.$name('fragment');
810+
811+
const pipeline = root
812+
.withVertex(vertexFn, {})
813+
.withFragment(fragmentFn, { color: { format: 'rgba8unorm' } })
814+
.withDepthStencil({
815+
format: 'stencil8',
816+
stencilFront: { passOp: 'replace' },
817+
})
818+
.createPipeline()
819+
.withColorAttachment({
820+
color: {
821+
view: {} as unknown as GPUTextureView,
822+
loadOp: 'clear',
823+
storeOp: 'store',
824+
},
825+
})
826+
.withDepthStencilAttachment({
827+
view: {} as unknown as GPUTextureView,
828+
stencilLoadOp: 'clear',
829+
stencilStoreOp: 'store',
830+
stencilClearValue: 5,
831+
})
832+
.withStencilReference(3);
833+
834+
pipeline.draw(3);
835+
836+
const renderPassEncoder = commandEncoder.mock.beginRenderPass();
837+
expect(renderPassEncoder.setStencilReference)
838+
.toHaveBeenCalledExactlyOnceWith(3);
839+
840+
pipeline.withStencilReference(7).draw(3);
841+
842+
expect(renderPassEncoder.setStencilReference).toHaveBeenNthCalledWith(2, 7);
843+
});
844+
798845
it('should onlly allow for drawIndexed with assigned index buffer', ({ root }) => {
799846
const vertexFn = tgpu['~unstable']
800847
.vertexFn({

packages/typegpu/tests/utils/extendedIt.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ const mockRenderPassEncoder = {
6161
setPipeline: vi.fn(),
6262
setVertexBuffer: vi.fn(),
6363
setIndexBuffer: vi.fn(),
64+
setStencilReference: vi.fn(),
6465
};
6566

6667
const mockQuerySet = {

0 commit comments

Comments
 (0)