Skip to content

Commit 9fe3f8c

Browse files
committed
feat: add WebGPU XR view instancing
Add native WebGPU XR view-instanced rendering when projection-layer sub-images share a texture array. The WebGPU XR bridge now detects compatible layered sub-images, records the view-count state on the device, and drives FramePassMultiView through a single native pass when supported. Request and expose the WebGPU view-instancing and multisampled-array-textures capabilities, including maxViewInstanceCount handling. Backbuffer render-target setup now allocates array-layered color/depth attachments for native view-instanced XR, including multisampled texture-array attachments when needed. Teach WGSL processing and bind group layout generation about multisampled 2D array textures, native view-index handling, and view-uniform arrays. Materials and mesh shader caching now include the view-instancing shader variant so XR array uniforms select the correct per-view matrices and camera data.
1 parent a8773f9 commit 9fe3f8c

17 files changed

Lines changed: 564 additions & 120 deletions

src/platform/graphics/bind-group-format.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,10 @@ class BindTextureFormat extends BindBaseFormat {
132132
* sampler is used, it will take up an additional slot, directly following the texture slot.
133133
* Defaults to true.
134134
* @param {string|null} [samplerName] - Optional name of the sampler. Defaults to null.
135+
* @param {boolean} [multisampled] - True if the texture binding is multisampled. Defaults to
136+
* false.
135137
*/
136-
constructor(name, visibility, textureDimension = TEXTUREDIMENSION_2D, sampleType = SAMPLETYPE_FLOAT, hasSampler = true, samplerName = null) {
138+
constructor(name, visibility, textureDimension = TEXTUREDIMENSION_2D, sampleType = SAMPLETYPE_FLOAT, hasSampler = true, samplerName = null, multisampled = false) {
137139
super(name, visibility);
138140

139141
// TEXTUREDIMENSION_***
@@ -147,6 +149,9 @@ class BindTextureFormat extends BindBaseFormat {
147149

148150
// optional name of the sampler (its automatically generated if not provided)
149151
this.samplerName = samplerName ?? `${name}_sampler`;
152+
153+
// whether the texture is multisampled
154+
this.multisampled = multisampled;
150155
}
151156
}
152157

src/platform/graphics/shader-definition-utils.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,9 @@ class ShaderDefinitionUtils {
221221
if (shaderType === 'fragment' && device.supportsPrimitiveIndex) {
222222
code += 'enable primitive_index;\n';
223223
}
224+
if (device.supportsMultisampledArrayTextures) {
225+
code += 'enable multisampled_array_textures;\n';
226+
}
224227
if (device.supportsSubgroups) {
225228
code += 'enable subgroups;\n';
226229
}

src/platform/graphics/shader-processor-options.js

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,26 @@ class ShaderProcessorOptions {
2222
/** @type {VertexFormat[]} */
2323
vertexFormat;
2424

25+
/** @type {boolean} */
26+
viewInstancing = false;
27+
2528
/**
2629
* Constructs shader processing options, used to process the shader for uniform buffer support.
2730
*
2831
* @param {UniformBufferFormat} [viewUniformFormat] - Format of the uniform buffer.
2932
* @param {BindGroupFormat} [viewBindGroupFormat] - Format of the bind group.
3033
* @param {VertexFormat} [vertexFormat] - Format of the vertex buffer.
34+
* @param {boolean} [viewInstancing] - True to process the shader for WebGPU native view
35+
* instancing. Defaults to false.
3136
*/
32-
constructor(viewUniformFormat, viewBindGroupFormat, vertexFormat) {
37+
constructor(viewUniformFormat, viewBindGroupFormat, vertexFormat, viewInstancing = false) {
3338

3439
// construct a sparse array
3540
this.uniformFormats[BINDGROUP_VIEW] = viewUniformFormat;
3641
this.bindGroupFormats[BINDGROUP_VIEW] = viewBindGroupFormat;
3742

3843
this.vertexFormat = vertexFormat;
44+
this.viewInstancing = viewInstancing;
3945
}
4046

4147
/**
@@ -45,15 +51,25 @@ class ShaderProcessorOptions {
4551
* @returns {boolean} - Returns true if the uniform exists, false otherwise.
4652
*/
4753
hasUniform(name) {
54+
return !!this.getUniform(name);
55+
}
4856

57+
/**
58+
* Get the uniform format for the uniform name.
59+
*
60+
* @param {string} name - The name of the uniform.
61+
* @returns {UniformFormat|undefined} - Returns the uniform format if it exists.
62+
*/
63+
getUniform(name) {
4964
for (let i = 0; i < this.uniformFormats.length; i++) {
5065
const uniformFormat = this.uniformFormats[i];
51-
if (uniformFormat?.get(name)) {
52-
return true;
66+
const format = uniformFormat?.get(name) ?? uniformFormat?.get(`${name}[0]`);
67+
if (format) {
68+
return format;
5369
}
5470
}
5571

56-
return false;
72+
return undefined;
5773
}
5874

5975
/**
@@ -92,6 +108,7 @@ class ShaderProcessorOptions {
92108
// WebGPU shaders are processed per vertex format
93109
if (device.isWebGPU) {
94110
key += this.vertexFormat?.shaderProcessingHashString;
111+
key += `#viewInstancing:${this.viewInstancing ? 1 : 0}`;
95112
}
96113

97114
return key;

src/platform/graphics/webgpu/webgpu-bind-group-format.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ class WebgpuBindGroupFormat {
123123
// texture
124124
const sampleType = textureFormat.sampleType;
125125
const viewDimension = textureFormat.textureDimension;
126-
const multisampled = false;
126+
const multisampled = textureFormat.multisampled;
127127

128128
const gpuSampleType = sampleTypes[sampleType];
129129
Debug.assert(gpuSampleType);

src/platform/graphics/webgpu/webgpu-graphics-device.js

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,30 @@ class WebgpuGraphicsDevice extends GraphicsDevice {
181181
*/
182182
xrColorTextureViewDescriptor = null;
183183

184+
/**
185+
* True when the active XR frame is rendered using WebGPU view instancing into a texture array.
186+
*
187+
* @type {boolean}
188+
* @ignore
189+
*/
190+
xrNativeViewInstancing = false;
191+
192+
/**
193+
* Number of XR views rendered by a native view-instanced render pass.
194+
*
195+
* @type {number}
196+
* @ignore
197+
*/
198+
xrViewCount = 1;
199+
200+
/**
201+
* View count used to allocate the current WebGPU backbuffer attachments.
202+
*
203+
* @type {number}
204+
* @private
205+
*/
206+
_backBufferViewCount = 1;
207+
184208
/**
185209
* Per-view XR sub-image entries populated each frame by the WebGPU XR bridge. Each entry
186210
* describes one XR view: the underlying GPU color texture, the view descriptor that selects the
@@ -200,6 +224,38 @@ class WebgpuGraphicsDevice extends GraphicsDevice {
200224
*/
201225
xrCurrentViewIndex = -1;
202226

227+
/**
228+
* True when the WebGPU `view-instancing` feature is available on the device.
229+
*
230+
* @type {boolean}
231+
* @ignore
232+
*/
233+
supportsViewInstancing = false;
234+
235+
/**
236+
* True when the WebGPU `multisampled-array-textures` feature is available on the device.
237+
*
238+
* @type {boolean}
239+
* @ignore
240+
*/
241+
supportsMultisampledArrayTextures = false;
242+
243+
/**
244+
* Maximum view count requested for WebGPU view instancing.
245+
*
246+
* @type {number}
247+
* @ignore
248+
*/
249+
maxViewInstanceCount = 1;
250+
251+
/**
252+
* Fixed shader uniform array size for XR stereo views.
253+
*
254+
* @type {number}
255+
* @ignore
256+
*/
257+
maxXrViews = 2;
258+
203259
/**
204260
* When set, used as the main color attachment in {@link WebgpuGraphicsDevice#frameStart} if there is
205261
* no XR color texture and no canvas {@link GPUCanvasContext#getCurrentTexture} (for example headless
@@ -285,6 +341,8 @@ class WebgpuGraphicsDevice extends GraphicsDevice {
285341
this.xrColorTexture = null;
286342
this.xrColorTextureViewFormat = null;
287343
this.xrColorTextureViewDescriptor = null;
344+
this.xrNativeViewInstancing = false;
345+
this.xrViewCount = 1;
288346
this.xrSubImages.length = 0;
289347
this.xrCurrentViewIndex = -1;
290348
}
@@ -405,6 +463,20 @@ class WebgpuGraphicsDevice extends GraphicsDevice {
405463
this.supportsTextureFormatTier1 ||= this.supportsTextureFormatTier2;
406464
this.supportsPrimitiveIndex = requireFeature('primitive-index');
407465
this.supportsSubgroups = requireFeature('subgroups');
466+
this.supportsMultisampledArrayTextures = requireFeature('multisampled-array-textures');
467+
468+
const adapterLimits = this.gpuAdapter?.limits;
469+
const maxViewInstanceCount = adapterLimits?.maxViewInstanceCount ?? 1;
470+
this.supportsViewInstancing = !bare &&
471+
this.gpuAdapter.features.has('view-instancing') &&
472+
maxViewInstanceCount >= this.maxXrViews;
473+
if (this.supportsViewInstancing) {
474+
requiredFeatures.push('view-instancing');
475+
this.maxViewInstanceCount = maxViewInstanceCount;
476+
} else {
477+
this.maxViewInstanceCount = 1;
478+
}
479+
408480
this.maxSubgroupSize = this.supportsSubgroups ? (this.gpuAdapter?.info?.subgroupMaxSize ?? 0) : 0;
409481
this.minSubgroupSize = this.supportsSubgroups ? (this.gpuAdapter?.info?.subgroupMinSize ?? 0) : 0;
410482
Debug.log(
@@ -428,6 +500,9 @@ class WebgpuGraphicsDevice extends GraphicsDevice {
428500
}
429501
}
430502
}
503+
if (this.supportsViewInstancing && requiredLimits.maxViewInstanceCount === undefined) {
504+
requiredLimits.maxViewInstanceCount = this.maxXrViews;
505+
}
431506

432507
/** @type {GPUDeviceDescriptor} */
433508
const deviceDescr = {
@@ -591,9 +666,15 @@ class WebgpuGraphicsDevice extends GraphicsDevice {
591666
// Reallocate framebuffer if dimensions change, to match the output texture. For WebXR
592667
// WebGPU projection color targets that are 2d-array textures, width/height are the per-layer
593668
// extent (same for every view), which matches what the render pass and internal depth need.
594-
if (this.backBufferSize.x !== outColorBuffer.width || this.backBufferSize.y !== outColorBuffer.height) {
669+
const viewCount = this.xrNativeViewInstancing ? this.xrViewCount : 1;
670+
if (
671+
this.backBufferSize.x !== outColorBuffer.width ||
672+
this.backBufferSize.y !== outColorBuffer.height ||
673+
this._backBufferViewCount !== viewCount
674+
) {
595675

596676
this.backBufferSize.set(outColorBuffer.width, outColorBuffer.height);
677+
this._backBufferViewCount = viewCount;
597678

598679
this.backBuffer.destroy();
599680
this.backBuffer = null;

src/platform/graphics/webgpu/webgpu-render-target.js

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,15 @@ class WebgpuRenderTarget {
235235
}
236236
}
237237

238+
/**
239+
* @param {WebgpuGraphicsDevice} device - The graphics device.
240+
* @returns {number} Number of array layers rendered by a native view-instanced backbuffer pass.
241+
* @private
242+
*/
243+
getViewCount(device) {
244+
return this.isBackbuffer && device.xrNativeViewInstancing ? device.xrViewCount : 1;
245+
}
246+
238247
/**
239248
* Initialize render target for rendering one time.
240249
*
@@ -277,6 +286,13 @@ class WebgpuRenderTarget {
277286

278287
this.updateKey();
279288

289+
const viewCount = this.getViewCount(device);
290+
if (viewCount > 1) {
291+
this.renderPassDescriptor.viewCount = viewCount;
292+
} else {
293+
delete this.renderPassDescriptor.viewCount;
294+
}
295+
280296
this.initialized = true;
281297

282298
WebgpuDebug.end(device, 'RenderTarget initialization', { renderTarget });
@@ -286,6 +302,12 @@ class WebgpuRenderTarget {
286302
initDepthStencil(device, wgpu, renderTarget) {
287303

288304
const { samples, width, height, depth, depthBuffer } = renderTarget;
305+
const viewCount = this.getViewCount(device);
306+
const viewDesc = viewCount > 1 ? {
307+
dimension: '2d-array',
308+
baseArrayLayer: 0,
309+
arrayLayerCount: viewCount
310+
} : undefined;
289311

290312
// depth buffer that we render to (single or multi-sampled). We don't create resolve
291313
// depth buffer as we don't currently resolve it. This might need to change in the future.
@@ -302,7 +324,7 @@ class WebgpuRenderTarget {
302324

303325
/** @type {GPUTextureDescriptor} */
304326
const depthTextureDesc = {
305-
size: [width, height, 1],
327+
size: [width, height, viewCount],
306328
dimension: '2d',
307329
sampleCount: samples,
308330
format: this.depthAttachment.format,
@@ -325,7 +347,7 @@ class WebgpuRenderTarget {
325347
this.depthAttachment.depthTexture = depthTexture;
326348
this.depthAttachment.depthTextureInternal = true;
327349

328-
renderingView = depthTexture.createView();
350+
renderingView = depthTexture.createView(viewDesc);
329351
DebugHelper.setLabel(renderingView, `${renderTarget.name}.autoDepthView`);
330352

331353
} else { // use provided depth buffer
@@ -341,7 +363,7 @@ class WebgpuRenderTarget {
341363
this.depthAttachment.hasStencil = depthFormat === 'depth24plus-stencil8';
342364

343365
// key for matching multi-sampled depth buffer
344-
const key = `${depthBuffer.id}:${width}:${height}:${samples}:${depthFormat}`;
366+
const key = `${depthBuffer.id}:${width}:${height}:${viewCount}:${samples}:${depthFormat}`;
345367

346368
// check if we have already allocated a multi-sampled depth buffer for the depth buffer
347369
const msTextures = getMultisampledTextureCache(device);
@@ -350,7 +372,7 @@ class WebgpuRenderTarget {
350372

351373
/** @type {GPUTextureDescriptor} */
352374
const multisampledDepthDesc = {
353-
size: [width, height, 1],
375+
size: [width, height, viewCount],
354376
dimension: '2d',
355377
sampleCount: samples,
356378
format: depthFormat,
@@ -370,7 +392,7 @@ class WebgpuRenderTarget {
370392
this.depthAttachment.multisampledDepthBuffer = msDepthTexture;
371393
this.depthAttachment.multisampledDepthBufferKey = key;
372394

373-
renderingView = msDepthTexture.createView();
395+
renderingView = msDepthTexture.createView(viewDesc);
374396
DebugHelper.setLabel(renderingView, `${renderTarget.name}.multisampledDepthView`);
375397

376398
} else {
@@ -379,7 +401,7 @@ class WebgpuRenderTarget {
379401
const depthTexture = depthBuffer.impl.gpuTexture;
380402
this.depthAttachment.depthTexture = depthTexture;
381403

382-
renderingView = depthTexture.createView();
404+
renderingView = depthTexture.createView(viewDesc);
383405
DebugHelper.setLabel(renderingView, `${renderTarget.name}.depthView`);
384406
}
385407
}
@@ -409,6 +431,12 @@ class WebgpuRenderTarget {
409431

410432
const { samples, width, height, mipLevel } = renderTarget;
411433
const colorBuffer = renderTarget.getColorBuffer(index);
434+
const viewCount = this.getViewCount(device);
435+
const viewDesc = viewCount > 1 ? {
436+
dimension: '2d-array',
437+
baseArrayLayer: 0,
438+
arrayLayerCount: viewCount
439+
} : undefined;
412440

413441
// view used to write to the color buffer (either by rendering to it, or resolving to it)
414442
let colorView = null;
@@ -446,7 +474,7 @@ class WebgpuRenderTarget {
446474

447475
/** @type {GPUTextureDescriptor} */
448476
const multisampledTextureDesc = {
449-
size: [width, height, 1],
477+
size: [width, height, viewCount],
450478
dimension: '2d',
451479
sampleCount: samples,
452480
format: format,
@@ -458,7 +486,7 @@ class WebgpuRenderTarget {
458486
DebugHelper.setLabel(multisampledColorBuffer, `${renderTarget.name}.multisampledColor`);
459487
this.setColorAttachment(index, multisampledColorBuffer, multisampledTextureDesc.format);
460488

461-
colorAttachment.view = multisampledColorBuffer.createView();
489+
colorAttachment.view = multisampledColorBuffer.createView(viewDesc);
462490
DebugHelper.setLabel(colorAttachment.view, `${renderTarget.name}.multisampledColorView`);
463491

464492
colorAttachment.resolveTarget = colorView;

0 commit comments

Comments
 (0)