Skip to content

Commit bdfb201

Browse files
committed
feat(WebGPU): add MSAA (multi-sample anti-aliasing) support
Add configurable sampleCount to the WebGPU render pipeline: - RenderWindow: expose setSampleCount()/getSampleCount() (default: 1) - Texture: support sampleCount in create(), resize(), resizeToMatch() - RenderEncoder: add resolveTextureViews for MSAA resolve targets - OpaquePass: create multisampled color/depth textures when sampleCount > 1, with a resolved (1-sample) color texture for downstream sampling - RenderWindow.getPixelsAsync: use resolved texture for readback when MSAA active Usage: const gpuRenderWindow = vtkWebGPURenderWindow.newInstance(); gpuRenderWindow.setSampleCount(4); // Enable 4x MSAA
1 parent 948a897 commit bdfb201

File tree

4 files changed

+105
-24
lines changed

4 files changed

+105
-24
lines changed

Sources/Rendering/WebGPU/OpaquePass/index.js

Lines changed: 75 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,50 +22,85 @@ function vtkWebGPUOpaquePass(publicAPI, model) {
2222
model._currentParent = viewNode;
2323

2424
const device = viewNode.getDevice();
25+
const sampleCount = viewNode.getSampleCount
26+
? viewNode.getSampleCount()
27+
: 1;
2528

2629
if (!model.renderEncoder) {
27-
publicAPI.createRenderEncoder();
30+
publicAPI.createRenderEncoder(sampleCount);
31+
32+
const width = viewNode.getCanvas().width;
33+
const height = viewNode.getCanvas().height;
34+
35+
// Color texture — multisampled when sampleCount > 1
2836
model.colorTexture = vtkWebGPUTexture.newInstance({
2937
label: 'opaquePassColor',
3038
});
39+
/* eslint-disable no-undef */
40+
/* eslint-disable no-bitwise */
3141
model.colorTexture.create(device, {
32-
width: viewNode.getCanvas().width,
33-
height: viewNode.getCanvas().height,
42+
width,
43+
height,
3444
format: 'rgba16float',
35-
/* eslint-disable no-undef */
36-
/* eslint-disable no-bitwise */
45+
sampleCount,
3746
usage:
3847
GPUTextureUsage.RENDER_ATTACHMENT |
39-
GPUTextureUsage.TEXTURE_BINDING |
40-
GPUTextureUsage.COPY_SRC,
48+
(sampleCount === 1
49+
? GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC
50+
: 0),
4151
});
4252
const ctView = model.colorTexture.createView('opaquePassColorTexture');
4353
model.renderEncoder.setColorTextureView(0, ctView);
4454

55+
// When MSAA is active, create a resolve target (1-sample) for
56+
// downstream passes that need to sample the color result
57+
if (sampleCount > 1) {
58+
model.resolveColorTexture = vtkWebGPUTexture.newInstance({
59+
label: 'opaquePassResolveColor',
60+
});
61+
model.resolveColorTexture.create(device, {
62+
width,
63+
height,
64+
format: 'rgba16float',
65+
usage:
66+
GPUTextureUsage.RENDER_ATTACHMENT |
67+
GPUTextureUsage.TEXTURE_BINDING |
68+
GPUTextureUsage.COPY_SRC,
69+
});
70+
const resolveView = model.resolveColorTexture.createView(
71+
'opaquePassColorTexture'
72+
);
73+
model.renderEncoder.setResolveTextureView(0, resolveView);
74+
}
75+
76+
// Depth texture — also multisampled
4577
model.depthFormat = 'depth32float';
4678
model.depthTexture = vtkWebGPUTexture.newInstance({
4779
label: 'opaquePassDepth',
4880
});
4981
model.depthTexture.create(device, {
50-
width: viewNode.getCanvas().width,
51-
height: viewNode.getCanvas().height,
82+
width,
83+
height,
5284
format: model.depthFormat,
85+
sampleCount,
5386
usage:
5487
GPUTextureUsage.RENDER_ATTACHMENT |
55-
GPUTextureUsage.TEXTURE_BINDING |
56-
GPUTextureUsage.COPY_SRC,
88+
(sampleCount === 1
89+
? GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC
90+
: 0),
5791
});
92+
/* eslint-enable no-undef */
93+
/* eslint-enable no-bitwise */
5894
const dView = model.depthTexture.createView('opaquePassDepthTexture');
5995
model.renderEncoder.setDepthTextureView(dView);
6096
} else {
61-
model.colorTexture.resize(
62-
viewNode.getCanvas().width,
63-
viewNode.getCanvas().height
64-
);
65-
model.depthTexture.resize(
66-
viewNode.getCanvas().width,
67-
viewNode.getCanvas().height
68-
);
97+
const width = viewNode.getCanvas().width;
98+
const height = viewNode.getCanvas().height;
99+
model.colorTexture.resize(width, height);
100+
model.depthTexture.resize(width, height);
101+
if (model.resolveColorTexture) {
102+
model.resolveColorTexture.resize(width, height);
103+
}
69104
}
70105

71106
model.renderEncoder.attachTextureViews();
@@ -74,18 +109,30 @@ function vtkWebGPUOpaquePass(publicAPI, model) {
74109
renNode.traverse(publicAPI);
75110
};
76111

77-
publicAPI.getColorTextureView = () =>
78-
model.renderEncoder.getColorTextureViews()[0];
112+
// When MSAA is active, downstream passes must sample from the resolved
113+
// (1-sample) texture, not the multisampled one
114+
publicAPI.getColorTextureView = () => {
115+
if (model.resolveColorTexture) {
116+
return model.resolveColorTexture.createView('opaquePassColorTexture');
117+
}
118+
return model.renderEncoder.getColorTextureViews()[0];
119+
};
79120

80121
publicAPI.getDepthTextureView = () =>
81122
model.renderEncoder.getDepthTextureView();
82123

83-
publicAPI.createRenderEncoder = () => {
124+
publicAPI.createRenderEncoder = (sampleCount = 1) => {
84125
model.renderEncoder = vtkWebGPURenderEncoder.newInstance({
85126
label: 'OpaquePass',
86127
});
87128
// default settings are fine for this
88129
model.renderEncoder.setPipelineHash('op');
130+
// Set multisample state in pipeline settings when MSAA is active
131+
if (sampleCount > 1) {
132+
const settings = model.renderEncoder.getPipelineSettings();
133+
settings.multisample = { count: sampleCount };
134+
model.renderEncoder.setPipelineSettings(settings);
135+
}
89136
};
90137
}
91138

@@ -97,6 +144,7 @@ const DEFAULT_VALUES = {
97144
renderEncoder: null,
98145
colorTexture: null,
99146
depthTexture: null,
147+
resolveColorTexture: null,
100148
};
101149

102150
// ----------------------------------------------------------------------------
@@ -107,7 +155,11 @@ export function extend(publicAPI, model, initialValues = {}) {
107155
// Build VTK API
108156
vtkRenderPass.extend(publicAPI, model, initialValues);
109157

110-
macro.get(publicAPI, model, ['colorTexture', 'depthTexture']);
158+
macro.get(publicAPI, model, [
159+
'colorTexture',
160+
'depthTexture',
161+
'resolveColorTexture',
162+
]);
111163

112164
// Object methods
113165
vtkWebGPUOpaquePass(publicAPI, model);

Sources/Rendering/WebGPU/RenderEncoder/index.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,13 @@ function vtkWebGPURenderEncoder(publicAPI, model) {
9696
model.colorTextureViews[idx] = view;
9797
};
9898

99+
publicAPI.setResolveTextureView = (idx, view) => {
100+
if (model.resolveTextureViews[idx] === view) {
101+
return;
102+
}
103+
model.resolveTextureViews[idx] = view;
104+
};
105+
99106
publicAPI.activateBindGroup = (bg) => {
100107
const device = model.boundPipeline.getDevice();
101108
const midx = model.boundPipeline.getBindGroupLayoutCount(bg.getLabel());
@@ -126,6 +133,14 @@ function vtkWebGPURenderEncoder(publicAPI, model) {
126133
model.description.colorAttachments[i].view =
127134
model.colorTextureViews[i].getHandle();
128135
}
136+
// MSAA: set resolveTarget if a resolve texture view is provided
137+
if (model.resolveTextureViews[i]) {
138+
model.description.colorAttachments[i].resolveTarget =
139+
model.resolveTextureViews[i].getHandle();
140+
// When using MSAA, the multisampled texture is transient;
141+
// store only into the resolve target
142+
model.description.colorAttachments[i].storeOp = 'discard';
143+
}
129144
}
130145
if (model.depthTextureView) {
131146
model.description.depthStencilAttachment.view =
@@ -225,6 +240,7 @@ export function extend(publicAPI, model, initialValues = {}) {
225240
};
226241

227242
model.colorTextureViews = [];
243+
model.resolveTextureViews = [];
228244

229245
macro.get(publicAPI, model, ['boundPipeline', 'colorTextureViews']);
230246

Sources/Rendering/WebGPU/RenderWindow/index.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,11 @@ function vtkWebGPURenderWindow(publicAPI, model) {
469469

470470
publicAPI.getPixelsAsync = async () => {
471471
const device = model.device;
472-
const texture = model.renderPasses[0].getOpaquePass().getColorTexture();
472+
const opaquePass = model.renderPasses[0].getOpaquePass();
473+
// When MSAA is active, use the resolved (1-sample) texture for readback
474+
const texture = opaquePass.getResolveColorTexture()
475+
? opaquePass.getResolveColorTexture()
476+
: opaquePass.getColorTexture();
473477

474478
// as this is async we really don't want to store things in
475479
// the class as multiple calls may start before resolving
@@ -578,6 +582,7 @@ const DEFAULT_VALUES = {
578582
nextPropID: 1,
579583
xrSupported: false,
580584
presentationFormat: null,
585+
sampleCount: 1,
581586
};
582587

583588
// ----------------------------------------------------------------------------
@@ -621,6 +626,7 @@ export function extend(publicAPI, model, initialValues = {}) {
621626
'presentationFormat',
622627
'useBackgroundImage',
623628
'xrSupported',
629+
'sampleCount',
624630
]);
625631

626632
macro.setGet(publicAPI, model, [
@@ -632,6 +638,7 @@ export function extend(publicAPI, model, initialValues = {}) {
632638
'notifyStartCaptureImage',
633639
'cursor',
634640
'useOffScreen',
641+
'sampleCount',
635642
]);
636643

637644
macro.setGetArray(publicAPI, model, ['size'], 2);

Sources/Rendering/WebGPU/Texture/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ function vtkWebGPUTexture(publicAPI, model) {
2424
const dimension = model.depth === 1 ? '2d' : '3d';
2525
model.format = options.format ? options.format : 'rgba8unorm';
2626
model.mipLevel = options.mipLevel ? options.mipLevel : 0;
27+
model.sampleCount = options.sampleCount ? options.sampleCount : 1;
2728
/* eslint-disable no-undef */
2829
/* eslint-disable no-bitwise */
2930
model.usage = options.usage
@@ -37,6 +38,7 @@ function vtkWebGPUTexture(publicAPI, model) {
3738
usage: model.usage,
3839
label: model.label,
3940
dimension,
41+
sampleCount: model.sampleCount,
4042
mipLevelCount: model.mipLevel + 1,
4143
});
4244
};
@@ -266,6 +268,7 @@ function vtkWebGPUTexture(publicAPI, model) {
266268
format: model.format,
267269
usage: model.usage,
268270
label: model.label,
271+
sampleCount: model.sampleCount,
269272
});
270273
}
271274
};
@@ -284,6 +287,7 @@ function vtkWebGPUTexture(publicAPI, model) {
284287
format: model.format,
285288
usage: model.usage,
286289
label: model.label,
290+
sampleCount: model.sampleCount,
287291
});
288292
}
289293
};
@@ -309,6 +313,7 @@ const DEFAULT_VALUES = {
309313
buffer: null,
310314
ready: false,
311315
label: null,
316+
sampleCount: 1,
312317
};
313318

314319
// ----------------------------------------------------------------------------
@@ -327,6 +332,7 @@ export function extend(publicAPI, model, initialValues = {}) {
327332
'depth',
328333
'format',
329334
'usage',
335+
'sampleCount',
330336
]);
331337
macro.setGet(publicAPI, model, ['device', 'label']);
332338

0 commit comments

Comments
 (0)