diff --git a/src/platform/graphics/bind-group-format.js b/src/platform/graphics/bind-group-format.js index d9efee34479..b283e757eef 100644 --- a/src/platform/graphics/bind-group-format.js +++ b/src/platform/graphics/bind-group-format.js @@ -132,8 +132,10 @@ class BindTextureFormat extends BindBaseFormat { * sampler is used, it will take up an additional slot, directly following the texture slot. * Defaults to true. * @param {string|null} [samplerName] - Optional name of the sampler. Defaults to null. + * @param {boolean} [multisampled] - True if the texture binding is multisampled. Defaults to + * false. */ - constructor(name, visibility, textureDimension = TEXTUREDIMENSION_2D, sampleType = SAMPLETYPE_FLOAT, hasSampler = true, samplerName = null) { + constructor(name, visibility, textureDimension = TEXTUREDIMENSION_2D, sampleType = SAMPLETYPE_FLOAT, hasSampler = true, samplerName = null, multisampled = false) { super(name, visibility); // TEXTUREDIMENSION_*** @@ -147,6 +149,9 @@ class BindTextureFormat extends BindBaseFormat { // optional name of the sampler (its automatically generated if not provided) this.samplerName = samplerName ?? `${name}_sampler`; + + // whether the texture is multisampled + this.multisampled = multisampled; } } diff --git a/src/platform/graphics/shader-definition-utils.js b/src/platform/graphics/shader-definition-utils.js index 5fb1ecc4dc9..fe8bef4a1bd 100644 --- a/src/platform/graphics/shader-definition-utils.js +++ b/src/platform/graphics/shader-definition-utils.js @@ -221,6 +221,9 @@ class ShaderDefinitionUtils { if (shaderType === 'fragment' && device.supportsPrimitiveIndex) { code += 'enable primitive_index;\n'; } + if (device.supportsMultisampledArrayTextures) { + code += 'enable multisampled_array_textures;\n'; + } if (device.supportsSubgroups) { code += 'enable subgroups;\n'; } diff --git a/src/platform/graphics/shader-processor-options.js b/src/platform/graphics/shader-processor-options.js index e281f2932f0..29cab5e32c3 100644 --- a/src/platform/graphics/shader-processor-options.js +++ b/src/platform/graphics/shader-processor-options.js @@ -22,20 +22,26 @@ class ShaderProcessorOptions { /** @type {VertexFormat[]} */ vertexFormat; + /** @type {boolean} */ + viewInstancing = false; + /** * Constructs shader processing options, used to process the shader for uniform buffer support. * * @param {UniformBufferFormat} [viewUniformFormat] - Format of the uniform buffer. * @param {BindGroupFormat} [viewBindGroupFormat] - Format of the bind group. * @param {VertexFormat} [vertexFormat] - Format of the vertex buffer. + * @param {boolean} [viewInstancing] - True to process the shader for WebGPU native view + * instancing. Defaults to false. */ - constructor(viewUniformFormat, viewBindGroupFormat, vertexFormat) { + constructor(viewUniformFormat, viewBindGroupFormat, vertexFormat, viewInstancing = false) { // construct a sparse array this.uniformFormats[BINDGROUP_VIEW] = viewUniformFormat; this.bindGroupFormats[BINDGROUP_VIEW] = viewBindGroupFormat; this.vertexFormat = vertexFormat; + this.viewInstancing = viewInstancing; } /** @@ -45,15 +51,25 @@ class ShaderProcessorOptions { * @returns {boolean} - Returns true if the uniform exists, false otherwise. */ hasUniform(name) { + return !!this.getUniform(name); + } + /** + * Get the uniform format for the uniform name. + * + * @param {string} name - The name of the uniform. + * @returns {UniformFormat|undefined} - Returns the uniform format if it exists. + */ + getUniform(name) { for (let i = 0; i < this.uniformFormats.length; i++) { const uniformFormat = this.uniformFormats[i]; - if (uniformFormat?.get(name)) { - return true; + const format = uniformFormat?.get(name) ?? uniformFormat?.get(`${name}[0]`); + if (format) { + return format; } } - return false; + return undefined; } /** @@ -92,6 +108,7 @@ class ShaderProcessorOptions { // WebGPU shaders are processed per vertex format if (device.isWebGPU) { key += this.vertexFormat?.shaderProcessingHashString; + key += `#viewInstancing:${this.viewInstancing ? 1 : 0}`; } return key; diff --git a/src/platform/graphics/webgpu/webgpu-bind-group-format.js b/src/platform/graphics/webgpu/webgpu-bind-group-format.js index 00d55bec8e3..9183d1522f2 100644 --- a/src/platform/graphics/webgpu/webgpu-bind-group-format.js +++ b/src/platform/graphics/webgpu/webgpu-bind-group-format.js @@ -123,7 +123,7 @@ class WebgpuBindGroupFormat { // texture const sampleType = textureFormat.sampleType; const viewDimension = textureFormat.textureDimension; - const multisampled = false; + const multisampled = textureFormat.multisampled; const gpuSampleType = sampleTypes[sampleType]; Debug.assert(gpuSampleType); diff --git a/src/platform/graphics/webgpu/webgpu-graphics-device.js b/src/platform/graphics/webgpu/webgpu-graphics-device.js index 1c8eb938a54..d6934d8150c 100644 --- a/src/platform/graphics/webgpu/webgpu-graphics-device.js +++ b/src/platform/graphics/webgpu/webgpu-graphics-device.js @@ -181,6 +181,30 @@ class WebgpuGraphicsDevice extends GraphicsDevice { */ xrColorTextureViewDescriptor = null; + /** + * True when the active XR frame is rendered using WebGPU view instancing into a texture array. + * + * @type {boolean} + * @ignore + */ + xrNativeViewInstancing = false; + + /** + * Number of XR views rendered by a native view-instanced render pass. + * + * @type {number} + * @ignore + */ + xrViewCount = 1; + + /** + * View count used to allocate the current WebGPU backbuffer attachments. + * + * @type {number} + * @private + */ + _backBufferViewCount = 1; + /** * Per-view XR sub-image entries populated each frame by the WebGPU XR bridge. Each entry * describes one XR view: the underlying GPU color texture, the view descriptor that selects the @@ -200,6 +224,38 @@ class WebgpuGraphicsDevice extends GraphicsDevice { */ xrCurrentViewIndex = -1; + /** + * True when the WebGPU `view-instancing` feature is available on the device. + * + * @type {boolean} + * @ignore + */ + supportsViewInstancing = false; + + /** + * True when the WebGPU `multisampled-array-textures` feature is available on the device. + * + * @type {boolean} + * @ignore + */ + supportsMultisampledArrayTextures = false; + + /** + * Maximum view count requested for WebGPU view instancing. + * + * @type {number} + * @ignore + */ + maxViewInstanceCount = 1; + + /** + * Fixed shader uniform array size for XR stereo views. + * + * @type {number} + * @ignore + */ + maxXrViews = 2; + /** * When set, used as the main color attachment in {@link WebgpuGraphicsDevice#frameStart} if there is * no XR color texture and no canvas {@link GPUCanvasContext#getCurrentTexture} (for example headless @@ -285,6 +341,8 @@ class WebgpuGraphicsDevice extends GraphicsDevice { this.xrColorTexture = null; this.xrColorTextureViewFormat = null; this.xrColorTextureViewDescriptor = null; + this.xrNativeViewInstancing = false; + this.xrViewCount = 1; this.xrSubImages.length = 0; this.xrCurrentViewIndex = -1; } @@ -405,6 +463,20 @@ class WebgpuGraphicsDevice extends GraphicsDevice { this.supportsTextureFormatTier1 ||= this.supportsTextureFormatTier2; this.supportsPrimitiveIndex = requireFeature('primitive-index'); this.supportsSubgroups = requireFeature('subgroups'); + this.supportsMultisampledArrayTextures = requireFeature('multisampled-array-textures'); + + const adapterLimits = this.gpuAdapter?.limits; + const maxViewInstanceCount = adapterLimits?.maxViewInstanceCount ?? 1; + this.supportsViewInstancing = !bare && + this.gpuAdapter.features.has('view-instancing') && + maxViewInstanceCount >= this.maxXrViews; + if (this.supportsViewInstancing) { + requiredFeatures.push('view-instancing'); + this.maxViewInstanceCount = maxViewInstanceCount; + } else { + this.maxViewInstanceCount = 1; + } + this.maxSubgroupSize = this.supportsSubgroups ? (this.gpuAdapter?.info?.subgroupMaxSize ?? 0) : 0; this.minSubgroupSize = this.supportsSubgroups ? (this.gpuAdapter?.info?.subgroupMinSize ?? 0) : 0; Debug.log( @@ -428,6 +500,9 @@ class WebgpuGraphicsDevice extends GraphicsDevice { } } } + if (this.supportsViewInstancing && requiredLimits.maxViewInstanceCount === undefined) { + requiredLimits.maxViewInstanceCount = this.maxXrViews; + } /** @type {GPUDeviceDescriptor} */ const deviceDescr = { @@ -591,9 +666,15 @@ class WebgpuGraphicsDevice extends GraphicsDevice { // Reallocate framebuffer if dimensions change, to match the output texture. For WebXR // WebGPU projection color targets that are 2d-array textures, width/height are the per-layer // extent (same for every view), which matches what the render pass and internal depth need. - if (this.backBufferSize.x !== outColorBuffer.width || this.backBufferSize.y !== outColorBuffer.height) { + const viewCount = this.xrNativeViewInstancing ? this.xrViewCount : 1; + if ( + this.backBufferSize.x !== outColorBuffer.width || + this.backBufferSize.y !== outColorBuffer.height || + this._backBufferViewCount !== viewCount + ) { this.backBufferSize.set(outColorBuffer.width, outColorBuffer.height); + this._backBufferViewCount = viewCount; this.backBuffer.destroy(); this.backBuffer = null; diff --git a/src/platform/graphics/webgpu/webgpu-render-target.js b/src/platform/graphics/webgpu/webgpu-render-target.js index ff95e47344c..4813acb63aa 100644 --- a/src/platform/graphics/webgpu/webgpu-render-target.js +++ b/src/platform/graphics/webgpu/webgpu-render-target.js @@ -235,6 +235,15 @@ class WebgpuRenderTarget { } } + /** + * @param {WebgpuGraphicsDevice} device - The graphics device. + * @returns {number} Number of array layers rendered by a native view-instanced backbuffer pass. + * @private + */ + getViewCount(device) { + return this.isBackbuffer && device.xrNativeViewInstancing ? device.xrViewCount : 1; + } + /** * Initialize render target for rendering one time. * @@ -277,6 +286,13 @@ class WebgpuRenderTarget { this.updateKey(); + const viewCount = this.getViewCount(device); + if (viewCount > 1) { + this.renderPassDescriptor.viewCount = viewCount; + } else { + delete this.renderPassDescriptor.viewCount; + } + this.initialized = true; WebgpuDebug.end(device, 'RenderTarget initialization', { renderTarget }); @@ -286,6 +302,12 @@ class WebgpuRenderTarget { initDepthStencil(device, wgpu, renderTarget) { const { samples, width, height, depth, depthBuffer } = renderTarget; + const viewCount = this.getViewCount(device); + const viewDesc = viewCount > 1 ? { + dimension: '2d-array', + baseArrayLayer: 0, + arrayLayerCount: viewCount + } : undefined; // depth buffer that we render to (single or multi-sampled). We don't create resolve // depth buffer as we don't currently resolve it. This might need to change in the future. @@ -302,7 +324,7 @@ class WebgpuRenderTarget { /** @type {GPUTextureDescriptor} */ const depthTextureDesc = { - size: [width, height, 1], + size: [width, height, viewCount], dimension: '2d', sampleCount: samples, format: this.depthAttachment.format, @@ -325,7 +347,7 @@ class WebgpuRenderTarget { this.depthAttachment.depthTexture = depthTexture; this.depthAttachment.depthTextureInternal = true; - renderingView = depthTexture.createView(); + renderingView = depthTexture.createView(viewDesc); DebugHelper.setLabel(renderingView, `${renderTarget.name}.autoDepthView`); } else { // use provided depth buffer @@ -341,7 +363,7 @@ class WebgpuRenderTarget { this.depthAttachment.hasStencil = depthFormat === 'depth24plus-stencil8'; // key for matching multi-sampled depth buffer - const key = `${depthBuffer.id}:${width}:${height}:${samples}:${depthFormat}`; + const key = `${depthBuffer.id}:${width}:${height}:${viewCount}:${samples}:${depthFormat}`; // check if we have already allocated a multi-sampled depth buffer for the depth buffer const msTextures = getMultisampledTextureCache(device); @@ -350,7 +372,7 @@ class WebgpuRenderTarget { /** @type {GPUTextureDescriptor} */ const multisampledDepthDesc = { - size: [width, height, 1], + size: [width, height, viewCount], dimension: '2d', sampleCount: samples, format: depthFormat, @@ -370,7 +392,7 @@ class WebgpuRenderTarget { this.depthAttachment.multisampledDepthBuffer = msDepthTexture; this.depthAttachment.multisampledDepthBufferKey = key; - renderingView = msDepthTexture.createView(); + renderingView = msDepthTexture.createView(viewDesc); DebugHelper.setLabel(renderingView, `${renderTarget.name}.multisampledDepthView`); } else { @@ -379,7 +401,7 @@ class WebgpuRenderTarget { const depthTexture = depthBuffer.impl.gpuTexture; this.depthAttachment.depthTexture = depthTexture; - renderingView = depthTexture.createView(); + renderingView = depthTexture.createView(viewDesc); DebugHelper.setLabel(renderingView, `${renderTarget.name}.depthView`); } } @@ -409,6 +431,12 @@ class WebgpuRenderTarget { const { samples, width, height, mipLevel } = renderTarget; const colorBuffer = renderTarget.getColorBuffer(index); + const viewCount = this.getViewCount(device); + const viewDesc = viewCount > 1 ? { + dimension: '2d-array', + baseArrayLayer: 0, + arrayLayerCount: viewCount + } : undefined; // view used to write to the color buffer (either by rendering to it, or resolving to it) let colorView = null; @@ -446,7 +474,7 @@ class WebgpuRenderTarget { /** @type {GPUTextureDescriptor} */ const multisampledTextureDesc = { - size: [width, height, 1], + size: [width, height, viewCount], dimension: '2d', sampleCount: samples, format: format, @@ -458,7 +486,7 @@ class WebgpuRenderTarget { DebugHelper.setLabel(multisampledColorBuffer, `${renderTarget.name}.multisampledColor`); this.setColorAttachment(index, multisampledColorBuffer, multisampledTextureDesc.format); - colorAttachment.view = multisampledColorBuffer.createView(); + colorAttachment.view = multisampledColorBuffer.createView(viewDesc); DebugHelper.setLabel(colorAttachment.view, `${renderTarget.name}.multisampledColorView`); colorAttachment.resolveTarget = colorView; diff --git a/src/platform/graphics/webgpu/webgpu-shader-processor-wgsl.js b/src/platform/graphics/webgpu/webgpu-shader-processor-wgsl.js index a89cee5d68d..bb82c6320ac 100644 --- a/src/platform/graphics/webgpu/webgpu-shader-processor-wgsl.js +++ b/src/platform/graphics/webgpu/webgpu-shader-processor-wgsl.js @@ -54,6 +54,7 @@ const textureBaseInfo = { 'texture_cube': { viewDimension: TEXTUREDIMENSION_CUBE, baseSampleType: SAMPLETYPE_FLOAT }, 'texture_cube_array': { viewDimension: TEXTUREDIMENSION_CUBE_ARRAY, baseSampleType: SAMPLETYPE_FLOAT }, 'texture_multisampled_2d': { viewDimension: TEXTUREDIMENSION_2D, baseSampleType: SAMPLETYPE_FLOAT }, + 'texture_multisampled_2d_array': { viewDimension: TEXTUREDIMENSION_2D_ARRAY, baseSampleType: SAMPLETYPE_FLOAT }, 'texture_depth_2d': { viewDimension: TEXTUREDIMENSION_2D, baseSampleType: SAMPLETYPE_DEPTH }, 'texture_depth_2d_array': { viewDimension: TEXTUREDIMENSION_2D_ARRAY, baseSampleType: SAMPLETYPE_DEPTH }, 'texture_depth_cube': { viewDimension: TEXTUREDIMENSION_CUBE, baseSampleType: SAMPLETYPE_DEPTH }, @@ -68,7 +69,7 @@ const getTextureInfo = (baseType, componentType) => { Debug.assert(baseInfo); let finalSampleType = baseInfo.baseSampleType; - if (baseInfo.baseSampleType === SAMPLETYPE_FLOAT && baseType !== 'texture_multisampled_2d') { + if (baseInfo.baseSampleType === SAMPLETYPE_FLOAT) { switch (componentType) { case 'u32': finalSampleType = SAMPLETYPE_UINT; break; case 'i32': finalSampleType = SAMPLETYPE_INT; break; @@ -87,7 +88,28 @@ const getTextureInfo = (baseType, componentType) => { // reverse to getTextureInfo, convert view dimension and sample type to texture declaration // example: 2d_array & float -> texture_2d_array -const getTextureDeclarationType = (viewDimension, sampleType) => { +const getTextureDeclarationType = (viewDimension, sampleType, multisampled = false) => { + + // multisampled texture declarations + if (multisampled) { + let baseTypeString; + switch (viewDimension) { + case TEXTUREDIMENSION_2D: baseTypeString = 'texture_multisampled_2d'; break; + case TEXTUREDIMENSION_2D_ARRAY: baseTypeString = 'texture_multisampled_2d_array'; break; + default: Debug.assert(false); + } + + let coreFormatString; + switch (sampleType) { + case SAMPLETYPE_FLOAT: + case SAMPLETYPE_UNFILTERABLE_FLOAT: coreFormatString = 'f32'; break; + case SAMPLETYPE_UINT: coreFormatString = 'u32'; break; + case SAMPLETYPE_INT: coreFormatString = 'i32'; break; + default: Debug.assert(false); + } + + return `${baseTypeString}<${coreFormatString}>`; + } // types without template specifiers if (sampleType === SAMPLETYPE_DEPTH) { @@ -157,6 +179,8 @@ class UniformLine { arraySize = 0; + arrayIndexExpression = ''; + constructor(line, shader) { // Save the raw line this.line = line; @@ -227,6 +251,7 @@ class ResourceLine { this.isExternalTexture = false; this.type = ''; this.matchedElements = []; + this.multisampled = false; // handle texture type const textureMatch = this.line.match(TEXTURE_REGEX); @@ -235,6 +260,7 @@ class ResourceLine { this.type = textureMatch[2]; // texture type (e.g., texture_2d or texture_cube_array) this.textureFormat = textureMatch[3]; // texture format (e.g., f32) this.isTexture = true; + this.multisampled = this.type.startsWith('texture_multisampled_'); this.matchedElements.push(...textureMatch); // get dimension and sample type @@ -304,6 +330,7 @@ class ResourceLine { if (this.access !== other.access) return false; if (this.accessMode !== other.accessMode) return false; if (this.samplerType !== other.samplerType) return false; + if (this.multisampled !== other.multisampled) return false; return true; } } @@ -332,13 +359,14 @@ class WebgpuShaderProcessorWGSL { // VS - convert a list of attributes to a shader block with fixed locations const attributesMap = new Map(); - const attributesBlock = WebgpuShaderProcessorWGSL.processAttributes(vertexExtracted.attributes, shaderDefinition.attributes, attributesMap, shaderDefinition.processingOptions, shader); + const processingOptions = shaderDefinition.processingOptions; + const attributesBlock = WebgpuShaderProcessorWGSL.processAttributes(vertexExtracted.attributes, shaderDefinition.attributes, attributesMap, processingOptions, shader); // VS - convert a list of varyings to a shader block - const vertexVaryingsBlock = WebgpuShaderProcessorWGSL.processVaryings(vertexExtracted.varyings, varyingMap, true, device); + const vertexVaryingsBlock = WebgpuShaderProcessorWGSL.processVaryings(vertexExtracted.varyings, varyingMap, true, device, processingOptions); // FS - convert a list of varyings to a shader block - const fragmentVaryingsBlock = WebgpuShaderProcessorWGSL.processVaryings(fragmentExtracted.varyings, varyingMap, false, device); + const fragmentVaryingsBlock = WebgpuShaderProcessorWGSL.processVaryings(fragmentExtracted.varyings, varyingMap, false, device, processingOptions); // uniforms - merge vertex and fragment uniforms, and create shared uniform buffers // Note that as both vertex and fragment can declare the same uniform, we need to remove duplicates @@ -357,7 +385,7 @@ class WebgpuShaderProcessorWGSL { map.set(uni.name, uni.line); }); }); - const uniformsData = WebgpuShaderProcessorWGSL.processUniforms(device, parsedUniforms, shaderDefinition.processingOptions, shader); + const uniformsData = WebgpuShaderProcessorWGSL.processUniforms(device, parsedUniforms, processingOptions, shader); // rename references to uniforms to match the uniform buffer vertexExtracted.src = WebgpuShaderProcessorWGSL.renameUniformAccess(vertexExtracted.src, parsedUniforms); @@ -365,7 +393,7 @@ class WebgpuShaderProcessorWGSL { // parse resource lines const parsedResources = WebgpuShaderProcessorWGSL.mergeResources(vertexExtracted.resources, fragmentExtracted.resources, shader); - const resourcesData = WebgpuShaderProcessorWGSL.processResources(device, parsedResources, shaderDefinition.processingOptions, shader); + const resourcesData = WebgpuShaderProcessorWGSL.processResources(device, parsedResources, processingOptions, shader); // generate fragment output struct const fOutput = WebgpuShaderProcessorWGSL.generateFragmentOutputStruct(fragmentExtracted.src, device.maxColorAttachments); @@ -376,11 +404,17 @@ class WebgpuShaderProcessorWGSL { // VS - insert the blocks to the source const vBlock = `${attributesBlock}\n${vertexVaryingsBlock}\n${uniformsData.code}\n${resourcesData.code}\n`; - const vshader = vertexExtracted.src.replace(MARKER, vBlock); + const vertexViewInstancingEnable = processingOptions.viewInstancing && !vertexExtracted.src.includes('enable view_instancing;') ? + 'enable view_instancing;\n' : + ''; + const vshader = vertexViewInstancingEnable + vertexExtracted.src.replace(MARKER, vBlock); // FS - insert the blocks to the source const fBlock = `${fragmentVaryingsBlock}\n${fOutput}\n${uniformsData.code}\n${resourcesData.code}\n`; - const fshader = fragmentExtracted.src.replace(MARKER, fBlock); + const fragmentViewInstancingEnable = processingOptions.viewInstancing && !fragmentExtracted.src.includes('enable view_instancing;') ? + 'enable view_instancing;\n' : + ''; + const fshader = fragmentViewInstancingEnable + fragmentExtracted.src.replace(MARKER, fBlock); return { vshader: vshader, @@ -464,7 +498,8 @@ class WebgpuShaderProcessorWGSL { const meshUniforms = []; uniforms.forEach((uniform) => { // uniforms not already in supplied uniform buffers go to the mesh buffer - if (!processingOptions.hasUniform(uniform.name)) { + const existingUniform = processingOptions.getUniform(uniform.name); + if (!existingUniform) { uniform.ubName = 'ub_mesh_ub'; @@ -478,6 +513,9 @@ class WebgpuShaderProcessorWGSL { // TODO: when we add material ub, this name will need to be updated uniform.ubName = 'ub_view'; + if (existingUniform.isArrayType && processingOptions.viewInstancing) { + uniform.arrayIndexExpression = 'select(ub_view.view_index, pcViewIndex, ub_view.view_instancing != 0u)'; + } // Validate types here if needed Debug.assert(true, `Uniform ${uniform.name} already processed, skipping additional validation.`); @@ -522,7 +560,9 @@ class WebgpuShaderProcessorWGSL { static renameUniformAccess(source, uniforms) { uniforms.forEach((uniform) => { const srcName = `uniform.${uniform.name}`; - const dstName = `${uniform.ubName}.${uniform.name}`; + const dstName = uniform.arrayIndexExpression ? + `${uniform.ubName}.${uniform.name}[${uniform.arrayIndexExpression}]` : + `${uniform.ubName}.${uniform.name}`; // Use a regular expression to match `uniform.name` as a whole word. const regex = new RegExp(`\\b${srcName}\\b`, 'g'); source = source.replace(regex, dstName); @@ -570,14 +610,14 @@ class WebgpuShaderProcessorWGSL { // followed by optional sampler uniform const sampler = resources[i + 1]; - const hasSampler = sampler?.isSampler; + const hasSampler = !resource.multisampled && sampler?.isSampler; // TODO: handle external, and storage types const sampleType = resource.sampleType; const dimension = resource.textureDimension; // TODO: we could optimize visibility to only stages that use any of the data - textureFormats.push(new BindTextureFormat(resource.name, SHADERSTAGE_VERTEX | SHADERSTAGE_FRAGMENT, dimension, sampleType, hasSampler, hasSampler ? sampler.name : null)); + textureFormats.push(new BindTextureFormat(resource.name, SHADERSTAGE_VERTEX | SHADERSTAGE_FRAGMENT, dimension, sampleType, hasSampler, hasSampler ? sampler.name : null, resource.multisampled)); // following sampler was already handled if (hasSampler) i++; @@ -678,7 +718,7 @@ class WebgpuShaderProcessorWGSL { format.textureFormats.forEach((format) => { - const textureTypeName = getTextureDeclarationType(format.textureDimension, format.sampleType); + const textureTypeName = getTextureDeclarationType(format.textureDimension, format.sampleType, format.multisampled); code += `@group(${bindGroup}) @binding(${format.slot}) var ${format.name}: ${textureTypeName};\n`; if (format.hasSampler) { @@ -701,7 +741,7 @@ class WebgpuShaderProcessorWGSL { return code; } - static processVaryings(varyingLines, varyingMap, isVertex, device) { + static processVaryings(varyingLines, varyingMap, isVertex, device, processingOptions) { let block = ''; let blockPrivates = ''; let blockCopy = ''; @@ -740,6 +780,9 @@ class WebgpuShaderProcessorWGSL { if (isVertex) { block += ' @builtin(position) position : vec4f,\n'; // output position } else { + if (processingOptions.viewInstancing) { + block += ' @builtin(view_index) viewIndex : u32,\n'; + } block += ' @builtin(position) position : vec4f,\n'; // interpolated fragment position block += ' @builtin(front_facing) frontFacing : bool,\n'; // front-facing block += ' @builtin(sample_index) sampleIndex : u32,\n'; // sample index for MSAA @@ -755,12 +798,19 @@ class WebgpuShaderProcessorWGSL { const primitiveIndexCopy = device.supportsPrimitiveIndex ? ` pcPrimitiveIndex = input.primitiveIndex; ` : ''; + const viewIndexGlobals = processingOptions.viewInstancing ? ` + var pcViewIndex: u32; + ` : ''; + const viewIndexCopy = processingOptions.viewInstancing ? ` + pcViewIndex = input.viewIndex; + ` : ''; // global variables for build-in input into fragment shader const fragmentGlobals = isVertex ? '' : ` var pcPosition: vec4f; var pcFrontFacing: bool; var pcSampleIndex: u32; + ${viewIndexGlobals} ${primitiveIndexGlobals} ${blockPrivates} @@ -770,6 +820,7 @@ class WebgpuShaderProcessorWGSL { pcPosition = input.position; pcFrontFacing = input.frontFacing; pcSampleIndex = input.sampleIndex; + ${viewIndexCopy} ${primitiveIndexCopy} } `; @@ -879,21 +930,34 @@ class WebgpuShaderProcessorWGSL { } }); + const viewIndexInput = processingOptions.viewInstancing ? + '@builtin(view_index) viewIndex : u32,' : + ''; + const viewIndexGlobal = processingOptions.viewInstancing ? + 'var pcViewIndex: u32;' : + ''; + const viewIndexCopy = processingOptions.viewInstancing ? + 'pcViewIndex = input.viewIndex;' : + ''; + return ` struct VertexInput { ${blockAttributes} @builtin(vertex_index) vertexIndex : u32, // built-in vertex index - @builtin(instance_index) instanceIndex : u32 // built-in instance index + @builtin(instance_index) instanceIndex : u32, // built-in instance index + ${viewIndexInput} }; ${blockPrivates} var pcVertexIndex: u32; var pcInstanceIndex: u32; + ${viewIndexGlobal} fn _pcCopyInputs(input: VertexInput) { ${blockCopy} pcVertexIndex = input.vertexIndex; pcInstanceIndex = input.instanceIndex; + ${viewIndexCopy} } `; } diff --git a/src/platform/graphics/webgpu/webgpu-xr-bridge.js b/src/platform/graphics/webgpu/webgpu-xr-bridge.js index ace5dae17b4..86a2b7eac94 100644 --- a/src/platform/graphics/webgpu/webgpu-xr-bridge.js +++ b/src/platform/graphics/webgpu/webgpu-xr-bridge.js @@ -109,8 +109,13 @@ class WebgpuXrBridge { // caches texture size for getFramebufferSize when the projection layer omits dimensions. const first = subImages[0]; if (first) { + const nativeViewDescriptor = this._getNativeViewDescriptor(subImages); + device.xrColorTexture = first.colorTexture; device.xrColorTextureViewFormat = first.viewFormat; + device.xrColorTextureViewDescriptor = nativeViewDescriptor; + device.xrNativeViewInstancing = !!nativeViewDescriptor; + device.xrViewCount = nativeViewDescriptor ? subImages.length : 1; this._cachedFramebufferSize.set(first.colorTexture.width, first.colorTexture.height); } } @@ -173,6 +178,70 @@ class WebgpuXrBridge { return /** @type {XRViewport} */ ({ x: 0, y: 0, width: 0, height: 0 }); } + /** + * @param {{ colorTexture: any, viewDescriptor: any, viewport: any, viewFormat: any }[]} subImages - XR sub-images for the frame. + * @returns {any} A GPUTextureViewDescriptor covering all XR view layers, or null for fallback. + * @private + */ + _getNativeViewDescriptor(subImages) { + const device = this.xrBridge.device; + const viewCount = subImages.length; + if ( + viewCount < 2 || + viewCount > device.maxXrViews || + viewCount > device.maxViewInstanceCount || + !device.supportsViewInstancing || + (device.samples > 1 && !device.supportsMultisampledArrayTextures) + ) { + return null; + } + + const first = subImages[0]; + const firstViewport = first.viewport; + const textureArrayLength = this._layer?.textureArrayLength ?? first.colorTexture.depthOrArrayLayers ?? 1; + if (!first.viewDescriptor && textureArrayLength < viewCount) { + return null; + } + + const firstBaseArrayLayer = first.viewDescriptor?.baseArrayLayer; + if (first.viewDescriptor && firstBaseArrayLayer === undefined) { + return null; + } + + const baseArrayLayer = firstBaseArrayLayer ?? 0; + for (let i = 0; i < viewCount; i++) { + const sub = subImages[i]; + if ( + sub.colorTexture !== first.colorTexture || + sub.viewFormat !== first.viewFormat || + sub.viewport.x !== firstViewport.x || + sub.viewport.y !== firstViewport.y || + sub.viewport.width !== firstViewport.width || + sub.viewport.height !== firstViewport.height + ) { + return null; + } + + const desc = sub.viewDescriptor; + if (desc) { + const layer = desc.baseArrayLayer; + if (layer === undefined) { + return null; + } + if (layer !== baseArrayLayer + i) { + return null; + } + } + } + + return { + ...first.viewDescriptor, + dimension: '2d-array', + baseArrayLayer, + arrayLayerCount: viewCount + }; + } + /** * @param {XRSession} session - XR session. * @param {object} options - Presentation options. diff --git a/src/scene/materials/lit-material.js b/src/scene/materials/lit-material.js index d67b65af07b..0ac873c058a 100644 --- a/src/scene/materials/lit-material.js +++ b/src/scene/materials/lit-material.js @@ -95,7 +95,7 @@ class LitMaterial extends Material { options.defines = ShaderUtils.getCoreDefines(this, params); LitMaterialOptionsBuilder.update(options.litOptions, this, params.scene, params.cameraShaderParams, params.objDefs, params.pass, params.sortedLights); - const processingOptions = new ShaderProcessorOptions(params.viewUniformFormat, params.viewBindGroupFormat, params.vertexFormat); + const processingOptions = new ShaderProcessorOptions(params.viewUniformFormat, params.viewBindGroupFormat, params.vertexFormat, params.viewInstancing); const library = getProgramLibrary(params.device); library.register('lit', lit); const shader = library.getProgram('lit', options, processingOptions, this.userId); diff --git a/src/scene/materials/material.js b/src/scene/materials/material.js index ad4bde1735a..e6e40e1010d 100644 --- a/src/scene/materials/material.js +++ b/src/scene/materials/material.js @@ -63,6 +63,7 @@ let id = 0; * @property {UniformBufferFormat|undefined} viewUniformFormat - The view uniform format. * @property {BindGroupFormat|undefined} viewBindGroupFormat - The view bind group format. * @property {VertexFormat} vertexFormat - The vertex format. + * @property {boolean} [viewInstancing] - True if the shader is used by a native view-instanced pass. * @ignore */ diff --git a/src/scene/materials/shader-material.js b/src/scene/materials/shader-material.js index f2e59a35860..5e0a5398ed5 100644 --- a/src/scene/materials/shader-material.js +++ b/src/scene/materials/shader-material.js @@ -147,7 +147,7 @@ class ShaderMaterial extends Material { shaderChunks: this.shaderChunks // override chunks from the material }; - const processingOptions = new ShaderProcessorOptions(params.viewUniformFormat, params.viewBindGroupFormat, params.vertexFormat); + const processingOptions = new ShaderProcessorOptions(params.viewUniformFormat, params.viewBindGroupFormat, params.vertexFormat, params.viewInstancing); const library = getProgramLibrary(params.device); library.register('shader-material', shaderGeneratorShader); diff --git a/src/scene/materials/standard-material.js b/src/scene/materials/standard-material.js index f174ab6e3d1..c8579fc9832 100644 --- a/src/scene/materials/standard-material.js +++ b/src/scene/materials/standard-material.js @@ -864,7 +864,7 @@ class StandardMaterial extends Material { options = this.onUpdateShader(options); } - const processingOptions = new ShaderProcessorOptions(params.viewUniformFormat, params.viewBindGroupFormat, params.vertexFormat); + const processingOptions = new ShaderProcessorOptions(params.viewUniformFormat, params.viewBindGroupFormat, params.vertexFormat, params.viewInstancing); const library = getProgramLibrary(device); library.register('standard', standard); diff --git a/src/scene/mesh-instance.js b/src/scene/mesh-instance.js index e3fce64b98b..a0e19269401 100644 --- a/src/scene/mesh-instance.js +++ b/src/scene/mesh-instance.js @@ -51,7 +51,7 @@ const _tempSphere = new BoundingSphere(); const _meshSet = new Set(); // internal array used to evaluate the hash for the shader instance -const lookupHashes = new Uint32Array(4); +const lookupHashes = new Uint32Array(5); /** * Internal data structure used to store data used by hardware instancing. @@ -724,10 +724,11 @@ class MeshInstance { * @param {UniformBufferFormat} [viewUniformFormat] - The format of the view uniform buffer. * @param {BindGroupFormat} [viewBindGroupFormat] - The format of the view bind group. * @param {any} [sortedLights] - Array of arrays of lights. + * @param {boolean} [viewInstancing] - True if the shader needs native WebGPU view instancing. * @returns {ShaderInstance} - the shader instance. * @ignore */ - getShaderInstance(shaderPass, lightHash, scene, cameraShaderParams, viewUniformFormat, viewBindGroupFormat, sortedLights) { + getShaderInstance(shaderPass, lightHash, scene, cameraShaderParams, viewUniformFormat, viewBindGroupFormat, sortedLights, viewInstancing = false) { const shaderDefs = this._shaderDefs; @@ -736,6 +737,7 @@ class MeshInstance { lookupHashes[1] = lightHash; lookupHashes[2] = shaderDefs; lookupHashes[3] = cameraShaderParams.hash; + lookupHashes[4] = viewInstancing ? 1 : 0; const hash = hash32Fnv1a(lookupHashes); // look up the cache @@ -766,7 +768,8 @@ class MeshInstance { sortedLights: sortedLights, viewUniformFormat: viewUniformFormat, viewBindGroupFormat: viewBindGroupFormat, - vertexFormat: this.mesh.vertexBuffer?.format + vertexFormat: this.mesh.vertexBuffer?.format, + viewInstancing: viewInstancing }); DebugGraphics.popGpuMarker(this.mesh.device); diff --git a/src/scene/particle-system/particle-material.js b/src/scene/particle-system/particle-material.js index 0fdf0b5ee68..32c6c9412ef 100644 --- a/src/scene/particle-system/particle-material.js +++ b/src/scene/particle-system/particle-material.js @@ -68,7 +68,7 @@ class ParticleMaterial extends Material { customFace: this.emitter.orientation !== PARTICLEORIENTATION_SCREEN }; - const processingOptions = new ShaderProcessorOptions(params.viewUniformFormat, params.viewBindGroupFormat, params.vertexFormat); + const processingOptions = new ShaderProcessorOptions(params.viewUniformFormat, params.viewBindGroupFormat, params.vertexFormat, params.viewInstancing); const library = getProgramLibrary(device); library.register('particle', particle); diff --git a/src/scene/renderer/forward-renderer.js b/src/scene/renderer/forward-renderer.js index bc99b6e216c..ced061c90f2 100644 --- a/src/scene/renderer/forward-renderer.js +++ b/src/scene/renderer/forward-renderer.js @@ -487,7 +487,7 @@ class ForwardRenderer extends Renderer { } // execute first pass over draw calls, in order to update materials / shaders - renderForwardPrepareMaterials(camera, renderTarget, drawCalls, sortedLights, layer, pass) { + renderForwardPrepareMaterials(camera, renderTarget, drawCalls, sortedLights, layer, pass, viewInstancing = false) { // fog params from the scene, or overridden by the camera const fogParams = camera.fogParams ?? this.scene.fog; @@ -562,7 +562,9 @@ class ForwardRenderer extends Renderer { } } - const shaderInstance = drawCall.getShaderInstance(pass, lightHash, scene, shaderParams, this.viewUniformFormat, this.viewBindGroupFormat, sortedLights); + const viewUniformFormat = viewInstancing ? this.viewUniformFormatViewInstancing : this.viewUniformFormat; + const viewBindGroupFormat = viewInstancing ? this.viewBindGroupFormatViewInstancing : this.viewBindGroupFormat; + const shaderInstance = drawCall.getShaderInstance(pass, lightHash, scene, shaderParams, viewUniformFormat, viewBindGroupFormat, sortedLights, viewInstancing); addCall(drawCall, shaderInstance, material !== prevMaterial, !prevMaterial || lightMask !== prevLightMask); @@ -574,7 +576,7 @@ class ForwardRenderer extends Renderer { return _drawCallList; } - renderForwardInternal(camera, preparedCalls, sortedLights, pass, drawCallback, flipFaces, viewBindGroups) { + renderForwardInternal(camera, preparedCalls, sortedLights, pass, drawCallback, flipFaces, viewBindGroups, viewInstancing = false) { const device = this.device; const scene = this.scene; const passFlag = 1 << pass; @@ -588,6 +590,7 @@ class ForwardRenderer extends Renderer { // (xrCurrentViewIndex === -1 means "no wrapper active": fall back to the default behaviour // of rendering all views or the single non-XR view) const activeView = device.xrCurrentViewIndex ?? -1; + const nativeViewInstancing = !!(viewList && viewInstancing && activeView < 0); const viewListStart = (viewList && activeView >= 0) ? activeView : 0; const viewListEnd = (viewList && activeView >= 0) ? activeView + 1 : (viewList ? viewList.length : 0); @@ -666,7 +669,24 @@ class ForwardRenderer extends Renderer { const indirectData = drawCall.getDrawCommands(camera); - if (viewList) { + if (nativeViewInstancing) { + const viewport = device.xrSubImages[0]?.viewport ?? viewList[0].viewport; + device.setViewport(viewport.x, viewport.y, viewport.width ?? viewport.z, viewport.height ?? viewport.w); + + if (device.supportsUniformBuffers) { + device.setBindGroup(BINDGROUP_VIEW, viewBindGroups[0]); + } else { + this.setupViewUniforms(viewList[0], 0); + } + + device.draw(mesh.primitive[style], indexBuffer, instancingData?.count, indirectData); + + this._forwardDrawCalls++; + if (drawCall.instancingData) { + this._instancedDrawCalls++; + } + + } else if (viewList) { for (let v = viewListStart; v < viewListEnd; v++) { const view = viewList[v]; @@ -709,17 +729,17 @@ class ForwardRenderer extends Renderer { } } - renderForward(camera, renderTarget, allDrawCalls, sortedLights, pass, drawCallback, layer, flipFaces, viewBindGroups) { + renderForward(camera, renderTarget, allDrawCalls, sortedLights, pass, drawCallback, layer, flipFaces, viewBindGroups, viewInstancing = false) { // #if _PROFILER const forwardStartTime = now(); // #endif // run first pass over draw calls and handle material / shader updates - const preparedCalls = this.renderForwardPrepareMaterials(camera, renderTarget, allDrawCalls, sortedLights, layer, pass); + const preparedCalls = this.renderForwardPrepareMaterials(camera, renderTarget, allDrawCalls, sortedLights, layer, pass, viewInstancing); // render mesh instances - this.renderForwardInternal(camera, preparedCalls, sortedLights, pass, drawCallback, flipFaces, viewBindGroups); + this.renderForwardInternal(camera, preparedCalls, sortedLights, pass, drawCallback, flipFaces, viewBindGroups, viewInstancing); _drawCallList.clear(); @@ -811,8 +831,11 @@ class ForwardRenderer extends Renderer { this.setFogConstants(fogParams); const viewList = this.setCameraUniforms(camera, renderTarget); + const nativeViewInstancing = !!(viewList && device.xrNativeViewInstancing); if (device.supportsUniformBuffers) { - this.setupViewUniformBuffers(viewBindGroups, this.viewUniformFormat, this.viewBindGroupFormat, viewList); + const viewUniformFormat = nativeViewInstancing ? this.viewUniformFormatViewInstancing : this.viewUniformFormat; + const viewBindGroupFormat = nativeViewInstancing ? this.viewBindGroupFormatViewInstancing : this.viewBindGroupFormat; + this.setupViewUniformBuffers(viewBindGroups, viewUniformFormat, viewBindGroupFormat, viewList, nativeViewInstancing); } // clearing - do it after the view bind groups are set up, to avoid overriding those @@ -835,7 +858,8 @@ class ForwardRenderer extends Renderer { null, layer, flipFaces, - viewBindGroups); + viewBindGroups, + nativeViewInstancing); if (layer) { layer._forwardDrawCalls += this._forwardDrawCalls - forwardDrawCalls; diff --git a/src/scene/renderer/frame-pass-multi-view.js b/src/scene/renderer/frame-pass-multi-view.js index 284b3f9ad6a..02fbacbb5c0 100644 --- a/src/scene/renderer/frame-pass-multi-view.js +++ b/src/scene/renderer/frame-pass-multi-view.js @@ -5,27 +5,18 @@ import { FramePass } from '../../platform/graphics/frame-pass.js'; */ /** - * A frame pass that wraps an ordered list of child frame passes and runs them once per XR view. - * Currently used by the WebGPU XR path: per eye, the wrapper sets the active view index on the - * graphics device, swaps the backbuffer color view to the matching XR sub-image view descriptor, - * and invokes each child's `render()`. + * A frame pass that wraps an ordered list of child frame passes and runs them for XR views. + * The WebGPU XR path uses a native single-pass route when the projection layer is backed by a + * texture array and the device supports view instancing. Otherwise, per eye, the wrapper sets the + * active view index on the graphics device, swaps the backbuffer color view to the matching XR + * sub-image view descriptor, and invokes each child's `render()`. * * The children are not added to {@link FrameGraph#renderPasses} - they are owned by the wrapper * and invoked from {@link FramePassMultiView#render}. This guarantees the frame graph's * pass-merging cannot accidentally merge eye-N's last pass with eye-(N+1)'s first pass. * - * ## Future extension paths + * ## WebGL stereo * - * ### GPU-native multiview (single-pass stereo) - * Both WebGL (`OVR_multiview2`) and a future WebGPU multiview extension allow the GPU to render - * all views in **one draw call**, writing to each array layer simultaneously via - * `gl_ViewID_OVR` (WebGL) or `@builtin(view_index)` (WGSL). When those APIs become available - * this class is the right place to switch strategy: instead of looping N times, `render()` would - * configure a single multiview render pass targeting an array render target, upload all N view - * matrices as an array UBO, and issue children once. The serial-iteration path would remain as a - * fallback when the extension is absent. - * - * ### WebGL stereo * WebGL XR currently uses a single framebuffer with per-eye viewports (no wrapper needed). * If `OVR_multiview2` support is added, `ForwardRenderer._isMultiview` could be extended to * return `true` for WebGL when the extension is present, allowing this wrapper to orchestrate @@ -90,9 +81,36 @@ class FramePassMultiView extends FramePass { // - backbuffer assignedColorTexture: lets passes after the wrapper (composite camera, // HUD, …) keep targeting the original backbuffer instead of the last eye's sub-image. const savedXrColorTexture = device.xrColorTexture; + const savedXrColorTextureViewDescriptor = device.xrColorTextureViewDescriptor; + const savedXrCurrentViewIndex = device.xrCurrentViewIndex; const savedColorTexture = backBufferImpl?.assignedColorTexture ?? null; const savedViewFormat = backBufferImpl?.colorAttachments?.[0]?.format ?? null; + if (device.xrNativeViewInstancing) { + const sub = subs[0]; + device.xrCurrentViewIndex = -1; + device.xrColorTexture = sub.colorTexture; + device.xrColorTextureViewDescriptor = savedXrColorTextureViewDescriptor; + + backBufferImpl?.assignColorTexture?.(sub.colorTexture, sub.viewFormat); + + for (let c = 0; c < childCount; c++) { + children[c].render(); + } + + device.xrCurrentViewIndex = savedXrCurrentViewIndex; + device.xrColorTextureViewDescriptor = savedXrColorTextureViewDescriptor; + device.xrColorTexture = savedXrColorTexture ?? null; + + if ( + backBufferImpl && savedColorTexture && savedViewFormat && + backBufferImpl.assignedColorTexture !== savedColorTexture + ) { + backBufferImpl.assignColorTexture(savedColorTexture, savedViewFormat); + } + return; + } + for (let v = 0; v < numViews; v++) { const sub = subs[v]; @@ -112,8 +130,8 @@ class FramePassMultiView extends FramePass { // clears xrSubImages / xrColorTextureViewFormat for the frame, which would break a second // FramePassMultiView in the same frame (numViews would read as 0). The XR bridge clears // full state at endFrame. - device.xrCurrentViewIndex = -1; - device.xrColorTextureViewDescriptor = null; + device.xrCurrentViewIndex = savedXrCurrentViewIndex; + device.xrColorTextureViewDescriptor = savedXrColorTextureViewDescriptor; device.xrColorTexture = savedXrColorTexture ?? null; // restore the backbuffer to whatever it was bound to before the per-eye loop, but only if diff --git a/src/scene/renderer/renderer.js b/src/scene/renderer/renderer.js index 8ed97594f2e..22f89c42a04 100644 --- a/src/scene/renderer/renderer.js +++ b/src/scene/renderer/renderer.js @@ -12,7 +12,7 @@ import { CLEARFLAG_COLOR, CLEARFLAG_DEPTH, CLEARFLAG_STENCIL, BINDGROUP_MESH, BINDGROUP_VIEW, UNIFORM_BUFFER_DEFAULT_SLOT_NAME, UNIFORMTYPE_MAT4, UNIFORMTYPE_MAT3, UNIFORMTYPE_VEC4, UNIFORMTYPE_VEC3, UNIFORMTYPE_IVEC3, UNIFORMTYPE_VEC2, UNIFORMTYPE_FLOAT, UNIFORMTYPE_INT, - SHADERSTAGE_VERTEX, SHADERSTAGE_FRAGMENT, + UNIFORMTYPE_UINT, SHADERSTAGE_VERTEX, SHADERSTAGE_FRAGMENT, CULLFACE_NONE, BINDGROUP_MESH_UB, FRONTFACE_CCW, @@ -87,6 +87,7 @@ const _tempProjMat1 = new Mat4(); const _tempProjMat4 = new Mat4(); const _tempProjMat5 = new Mat4(); const _tempSet = new Set(); +const VIEW_UNIFORM_ARRAY_COUNT = 2; const _tempMeshInstances = []; const _tempMeshInstancesSkinned = []; @@ -189,6 +190,8 @@ class Renderer { // view bind group format with its uniform buffer format this.viewUniformFormat = null; this.viewBindGroupFormat = null; + this.viewUniformFormatViewInstancing = null; + this.viewBindGroupFormatViewInstancing = null; // timing this._skinTime = 0; @@ -230,6 +233,11 @@ class Renderer { this.viewportSizeId = scope.resolve('viewport_size'); this.viewIndexId = scope.resolve('view_index'); this.viewIndexId.setValue(0); + this.viewInstancingId = scope.resolve('view_instancing'); + this.viewInstancingId.setValue(0); + + this.viewArrayIds = null; + this.viewArrayData = null; this.blueNoiseJitterVersion = 0; this.blueNoiseJitterVec = new Vec4(); @@ -685,64 +693,103 @@ class Renderer { if (this.device.supportsUniformBuffers && !this.viewUniformFormat) { - // format of the view uniform buffer - const uniforms = [ - new UniformFormat('matrix_view', UNIFORMTYPE_MAT4), - new UniformFormat('matrix_viewInverse', UNIFORMTYPE_MAT4), - new UniformFormat('matrix_projection', UNIFORMTYPE_MAT4), - new UniformFormat('matrix_projectionSkybox', UNIFORMTYPE_MAT4), - new UniformFormat('matrix_viewProjection', UNIFORMTYPE_MAT4), - new UniformFormat('matrix_view3', UNIFORMTYPE_MAT3), - new UniformFormat('cubeMapRotationMatrix', UNIFORMTYPE_MAT3), - new UniformFormat('view_position', UNIFORMTYPE_VEC3), - new UniformFormat('viewport_size', UNIFORMTYPE_VEC4), - new UniformFormat('skyboxIntensity', UNIFORMTYPE_FLOAT), - new UniformFormat('exposure', UNIFORMTYPE_FLOAT), - new UniformFormat('textureBias', UNIFORMTYPE_FLOAT), - new UniformFormat('view_index', UNIFORMTYPE_FLOAT) - ]; - - if (isClustered) { - uniforms.push(...[ - new UniformFormat('clusterCellsCountByBoundsSize', UNIFORMTYPE_VEC3), - new UniformFormat('clusterBoundsMin', UNIFORMTYPE_VEC3), - new UniformFormat('clusterBoundsDelta', UNIFORMTYPE_VEC3), - new UniformFormat('clusterCellsDot', UNIFORMTYPE_IVEC3), - new UniformFormat('clusterCellsMax', UNIFORMTYPE_IVEC3), - new UniformFormat('shadowAtlasParams', UNIFORMTYPE_VEC2), - new UniformFormat('clusterMaxCells', UNIFORMTYPE_INT), - new UniformFormat('numClusteredLights', UNIFORMTYPE_INT), - new UniformFormat('clusterTextureWidth', UNIFORMTYPE_INT) - ]); - } + const createViewFormats = (viewArrayCount) => { + const viewUniform = (name, type) => new UniformFormat(name, type, viewArrayCount); + + // format of the view uniform buffer + const uniforms = [ + viewUniform('matrix_view', UNIFORMTYPE_MAT4), + viewUniform('matrix_viewInverse', UNIFORMTYPE_MAT4), + viewUniform('matrix_projection', UNIFORMTYPE_MAT4), + viewUniform('matrix_projectionSkybox', UNIFORMTYPE_MAT4), + viewUniform('matrix_viewProjection', UNIFORMTYPE_MAT4), + new UniformFormat('matrix_view3', UNIFORMTYPE_MAT3), + new UniformFormat('cubeMapRotationMatrix', UNIFORMTYPE_MAT3), + viewUniform('view_position', UNIFORMTYPE_VEC3), + viewUniform('viewport_size', UNIFORMTYPE_VEC4), + new UniformFormat('skyboxIntensity', UNIFORMTYPE_FLOAT), + new UniformFormat('exposure', UNIFORMTYPE_FLOAT), + new UniformFormat('textureBias', UNIFORMTYPE_FLOAT), + new UniformFormat('view_index', UNIFORMTYPE_UINT), + new UniformFormat('view_instancing', UNIFORMTYPE_UINT) + ]; + + if (isClustered) { + uniforms.push(...[ + new UniformFormat('clusterCellsCountByBoundsSize', UNIFORMTYPE_VEC3), + new UniformFormat('clusterBoundsMin', UNIFORMTYPE_VEC3), + new UniformFormat('clusterBoundsDelta', UNIFORMTYPE_VEC3), + new UniformFormat('clusterCellsDot', UNIFORMTYPE_IVEC3), + new UniformFormat('clusterCellsMax', UNIFORMTYPE_IVEC3), + new UniformFormat('shadowAtlasParams', UNIFORMTYPE_VEC2), + new UniformFormat('clusterMaxCells', UNIFORMTYPE_INT), + new UniformFormat('numClusteredLights', UNIFORMTYPE_INT), + new UniformFormat('clusterTextureWidth', UNIFORMTYPE_INT) + ]); + } + + const viewUniformFormat = new UniformBufferFormat(this.device, uniforms); + if (viewArrayCount) { + const scope = this.device.scope; + this.viewArrayIds = { + view: scope.resolve('matrix_view[0]'), + viewInv: scope.resolve('matrix_viewInverse[0]'), + proj: scope.resolve('matrix_projection[0]'), + projSkybox: scope.resolve('matrix_projectionSkybox[0]'), + viewProj: scope.resolve('matrix_viewProjection[0]'), + viewPos: scope.resolve('view_position[0]'), + viewportSize: scope.resolve('viewport_size[0]') + }; + this.viewArrayData = { + view: new Float32Array(viewArrayCount * 16), + viewInv: new Float32Array(viewArrayCount * 16), + proj: new Float32Array(viewArrayCount * 16), + projSkybox: new Float32Array(viewArrayCount * 16), + viewProj: new Float32Array(viewArrayCount * 16), + viewPos: new Float32Array(viewArrayCount * 3), + viewportSize: new Float32Array(viewArrayCount * 4) + }; + } + + // format of the view bind group - contains single uniform buffer, and some textures + const formats = [ - this.viewUniformFormat = new UniformBufferFormat(this.device, uniforms); + // uniform buffer needs to be first, as the shader processor assumes slot 0 for it + new BindUniformBufferFormat(UNIFORM_BUFFER_DEFAULT_SLOT_NAME, SHADERSTAGE_VERTEX | SHADERSTAGE_FRAGMENT) - // format of the view bind group - contains single uniform buffer, and some textures - const formats = [ + // disable view level textures, as they consume texture slots. They get automatically added to mesh bind group + // for the meshes that uses them + // new BindTextureFormat('lightsTexture', SHADERSTAGE_FRAGMENT, TEXTUREDIMENSION_2D, SAMPLETYPE_UNFILTERABLE_FLOAT), + // new BindTextureFormat('shadowAtlasTexture', SHADERSTAGE_FRAGMENT, TEXTUREDIMENSION_2D, SAMPLETYPE_DEPTH), + // new BindTextureFormat('cookieAtlasTexture', SHADERSTAGE_FRAGMENT, TEXTUREDIMENSION_2D, SAMPLETYPE_FLOAT), - // uniform buffer needs to be first, as the shader processor assumes slot 0 for it - new BindUniformBufferFormat(UNIFORM_BUFFER_DEFAULT_SLOT_NAME, SHADERSTAGE_VERTEX | SHADERSTAGE_FRAGMENT) + // new BindTextureFormat('areaLightsLutTex1', SHADERSTAGE_FRAGMENT, TEXTUREDIMENSION_2D, SAMPLETYPE_FLOAT), + // new BindTextureFormat('areaLightsLutTex2', SHADERSTAGE_FRAGMENT, TEXTUREDIMENSION_2D, SAMPLETYPE_FLOAT) + ]; // disable view level textures, as they consume texture slots. They get automatically added to mesh bind group // for the meshes that uses them - // new BindTextureFormat('lightsTexture', SHADERSTAGE_FRAGMENT, TEXTUREDIMENSION_2D, SAMPLETYPE_UNFILTERABLE_FLOAT), - // new BindTextureFormat('shadowAtlasTexture', SHADERSTAGE_FRAGMENT, TEXTUREDIMENSION_2D, SAMPLETYPE_DEPTH), - // new BindTextureFormat('cookieAtlasTexture', SHADERSTAGE_FRAGMENT, TEXTUREDIMENSION_2D, SAMPLETYPE_FLOAT), - - // new BindTextureFormat('areaLightsLutTex1', SHADERSTAGE_FRAGMENT, TEXTUREDIMENSION_2D, SAMPLETYPE_FLOAT), - // new BindTextureFormat('areaLightsLutTex2', SHADERSTAGE_FRAGMENT, TEXTUREDIMENSION_2D, SAMPLETYPE_FLOAT) - ]; - - // disable view level textures, as they consume texture slots. They get automatically added to mesh bind group - // for the meshes that uses them - // if (isClustered) { - // formats.push(...[ - // new BindTextureFormat('clusterWorldTexture', SHADERSTAGE_FRAGMENT, TEXTUREDIMENSION_2D, SAMPLETYPE_UNFILTERABLE_FLOAT) - // ]); - // } - - this.viewBindGroupFormat = new BindGroupFormat(this.device, formats); + // if (isClustered) { + // formats.push(...[ + // new BindTextureFormat('clusterWorldTexture', SHADERSTAGE_FRAGMENT, TEXTUREDIMENSION_2D, SAMPLETYPE_UNFILTERABLE_FLOAT) + // ]); + // } + + return { + viewUniformFormat, + viewBindGroupFormat: new BindGroupFormat(this.device, formats) + }; + }; + + const viewFormats = createViewFormats(0); + this.viewUniformFormat = viewFormats.viewUniformFormat; + this.viewBindGroupFormat = viewFormats.viewBindGroupFormat; + + if (this.device.supportsViewInstancing) { + const instancingViewFormats = createViewFormats(VIEW_UNIFORM_ARRAY_COUNT); + this.viewUniformFormatViewInstancing = instancingViewFormats.viewUniformFormat; + this.viewBindGroupFormatViewInstancing = instancingViewFormats.viewBindGroupFormat; + } } } @@ -762,21 +809,105 @@ class Renderer { this.viewIndexId.setValue(index); } - setupViewUniformBuffers(viewBindGroups, viewUniformFormat, viewBindGroupFormat, viewList) { + /** + * Sets view uniform arrays used by WebGPU native XR view instancing. + * + * @param {object[]|null} viewList - XR views, or null for a single camera view. + * @private + */ + setupViewUniformArrays(viewList) { + const ids = this.viewArrayIds; + const data = this.viewArrayData; + if (!ids || !data) { + return; + } + + const count = viewList ? Math.min(viewList.length, VIEW_UNIFORM_ARRAY_COUNT) : 1; + + for (let i = 0; i < VIEW_UNIFORM_ARRAY_COUNT; i++) { + const view = viewList?.[Math.min(i, count - 1)]; + const matOffset = i * 16; + const vec3Offset = i * 3; + const vec4Offset = i * 4; + + if (view) { + data.view.set(view.viewOffMat.data, matOffset); + data.viewInv.set(view.viewInvOffMat.data, matOffset); + data.proj.set(view.projMat.data, matOffset); + data.projSkybox.set(view.projMat.data, matOffset); + data.viewProj.set(view.projViewOffMat.data, matOffset); + data.viewPos.set(view.positionData, vec3Offset); + + const viewport = view.viewport; + data.viewportSize[vec4Offset] = viewport.z; + data.viewportSize[vec4Offset + 1] = viewport.w; + data.viewportSize[vec4Offset + 2] = 1 / viewport.z; + data.viewportSize[vec4Offset + 3] = 1 / viewport.w; + } else { + data.view.set(this.viewId.value, matOffset); + data.viewInv.set(this.viewInvId.value, matOffset); + data.proj.set(this.projId.value, matOffset); + data.projSkybox.set(this.projSkyboxId.value, matOffset); + data.viewProj.set(this.viewProjId.value, matOffset); + data.viewPos.set(this.viewPosId.value, vec3Offset); + data.viewportSize.set(this.viewportSizeId.value, vec4Offset); + } + } + + ids.view.setValue(data.view); + ids.viewInv.setValue(data.viewInv); + ids.proj.setValue(data.proj); + ids.projSkybox.setValue(data.projSkybox); + ids.viewProj.setValue(data.viewProj); + ids.viewPos.setValue(data.viewPos); + ids.viewportSize.setValue(data.viewportSize); + + if (viewList?.length) { + const view = viewList[0]; + this.projId.setValue(view.projMat.data); + this.projSkyboxId.setValue(view.projMat.data); + this.viewId.setValue(view.viewOffMat.data); + this.viewInvId.setValue(view.viewInvOffMat.data); + this.viewId3.setValue(view.viewMat3.data); + this.viewProjId.setValue(view.projViewOffMat.data); + this.viewPosId.setValue(view.positionData); + } + } + + setupViewUniformBuffers(viewBindGroups, viewUniformFormat, viewBindGroupFormat, viewList, viewInstancing = false) { Debug.assert(Array.isArray(viewBindGroups), 'viewBindGroups must be an array'); const { device } = this; + // The same render action can render with scalar view uniforms in 2D and array view uniforms + // in native XR. Rebuild its cached view bind groups when the format changes. + if (viewBindGroups.length > 0 && viewBindGroups[0].format !== viewBindGroupFormat) { + viewBindGroups.forEach((bg) => { + bg.destroy(); + }); + viewBindGroups.length = 0; + } + // make sure we have bind group for each view const viewCount = viewList?.length ?? 1; - while (viewBindGroups.length < viewCount) { + const bindGroupCount = viewInstancing ? 1 : viewCount; + while (viewBindGroups.length < bindGroupCount) { const ub = new UniformBuffer(device, viewUniformFormat, false); const bg = new BindGroup(device, viewBindGroupFormat, ub); DebugHelper.setName(bg, `ViewBindGroup_${bg.id}`); viewBindGroups.push(bg); } - if (viewList) { + this.viewInstancingId.setValue(viewInstancing ? 1 : 0); + + if (viewInstancing && this.viewArrayIds) { + this.setupViewUniformArrays(viewList); + this.viewIndexId.setValue(0); + const viewBindGroup = viewBindGroups[0]; + viewBindGroup.defaultUniformBuffer.update(); + viewBindGroup.update(); + + } else if (viewList) { for (let i = 0; i < viewCount; i++) {