Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: WebGPU Occlusion queries #26335

Merged
merged 22 commits into from
Aug 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@
"webgpu_materials",
"webgpu_materials_video",
"webgpu_morphtargets",
"webgpu_occlusion",
"webgpu_particles",
"webgpu_rtt",
"webgpu_sandbox",
Expand Down
6 changes: 6 additions & 0 deletions examples/jsm/renderers/common/RenderList.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ class RenderList {
this.lightsNode = new LightsNode( [] );
this.lightsArray = [];

this.occlusionQueryCount = 0;

}

begin() {
Expand All @@ -71,6 +73,8 @@ class RenderList {
this.transparent.length = 0;
this.lightsArray.length = 0;

this.occlusionQueryCount = 0;

return this;

}
Expand Down Expand Up @@ -117,6 +121,8 @@ class RenderList {

const renderItem = this.getNextRenderItem( object, geometry, material, groupOrder, z, group );

if ( object.occlusionTest === true ) this.occlusionQueryCount ++;

( material.transparent === true ? this.transparent : this.opaque ).push( renderItem );

}
Expand Down
9 changes: 9 additions & 0 deletions examples/jsm/renderers/common/Renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ class Renderer {
}

renderContext.activeCubeFace = activeCubeFace;
renderContext.occlusionQueryCount = renderList.occlusionQueryCount;

//

Expand Down Expand Up @@ -550,6 +551,14 @@ class Renderer {

}

isOccluded( object ) {

const renderContext = this._currentRenderContext || this._lastRenderContext;

return renderContext ? this.backend.isOccluded( renderContext, object ) : false;

}

clear( color = true, depth = true, stencil = true ) {

const renderContext = this._currentRenderContext || this._lastRenderContext;
Expand Down
153 changes: 152 additions & 1 deletion examples/jsm/renderers/webgpu/WebGPUBackend.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ class WebGPUBackend extends Backend {
this.bindingUtils = new WebGPUBindingUtils( this );
this.pipelineUtils = new WebGPUPipelineUtils( this );
this.textureUtils = new WebGPUTextureUtils( this );
this.occludedResolveCache = new Map();

}

Expand Down Expand Up @@ -135,14 +136,41 @@ class WebGPUBackend extends Backend {
const renderContextData = this.get( renderContext );

const device = this.device;
const occlusionQueryCount = renderContext.occlusionQueryCount;

let occlusionQuerySet;

if ( occlusionQueryCount > 0 ) {

if ( renderContextData.currentOcclusionQuerySet ) renderContextData.currentOcclusionQuerySet.destroy();
if ( renderContextData.currentOcclusionQueryBuffer ) renderContextData.currentOcclusionQueryBuffer.destroy();

// Get a reference to the array of objects with queries. The renderContextData property
// can be changed by another render pass before the buffer.mapAsyc() completes.
renderContextData.currentOcclusionQuerySet = renderContextData.occlusionQuerySet;
renderContextData.currentOcclusionQueryBuffer = renderContextData.occlusionQueryBuffer;
renderContextData.currentOcclusionQueryObjects = renderContextData.occlusionQueryObjects;

//

occlusionQuerySet = device.createQuerySet( { type: 'occlusion', count: occlusionQueryCount } );

renderContextData.occlusionQuerySet = occlusionQuerySet;
renderContextData.occlusionQueryIndex = 0;
renderContextData.occlusionQueryObjects = new Array( occlusionQueryCount );

renderContextData.lastOcclusionObject = null;

}

const descriptor = {
colorAttachments: [ {
view: null
} ],
depthStencilAttachment: {
view: null
}
},
occlusionQuerySet
};

const colorAttachment = descriptor.colorAttachments[ 0 ];
Expand Down Expand Up @@ -282,9 +310,58 @@ class WebGPUBackend extends Backend {
finishRender( renderContext ) {

const renderContextData = this.get( renderContext );
const occlusionQueryCount = renderContext.occlusionQueryCount;

if ( occlusionQueryCount > renderContextData.occlusionQueryIndex ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the correct condition?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because I think endOcclusionQuery is also called in the draw method?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should catch the case when the last queried object being rendered is the last draw call in the pass. In the draw call. The end is inserted when a new object is seen, to allow handling of multiple draw calls per object (array cameras etc). Otherwise the endOcclusionQuery call could be at the end of the draw method.

According to the docs you can't use a querySet index in multiple beginOcclusionQuery( index ) calls in the same pass, otherwise you could just have repeated begin/end pairs around each draw() call.


renderContextData.currentPass.endOcclusionQuery();

}

renderContextData.currentPass.end();

if ( occlusionQueryCount > 0 ) {

const bufferSize = occlusionQueryCount * 8; // 8 byte entries for query results

//

let queryResolveBuffer = this.occludedResolveCache.get( bufferSize );

if ( queryResolveBuffer === undefined ) {

queryResolveBuffer = this.device.createBuffer(
{
size: bufferSize,
usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC
}
);

this.occludedResolveCache.set( bufferSize, queryResolveBuffer );

}

//

const readBuffer = this.device.createBuffer(
{
size: bufferSize,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
}
);

// two buffers required here - WebGPU doesn't allow usage of QUERY_RESOLVE & MAP_READ to be combined
renderContextData.encoder.resolveQuerySet( renderContextData.occlusionQuerySet, 0, occlusionQueryCount, queryResolveBuffer, 0 );
renderContextData.encoder.copyBufferToBuffer( queryResolveBuffer, 0, readBuffer, 0, bufferSize );

renderContextData.occlusionQueryBuffer = readBuffer;

//

this.resolveOccludedAsync( renderContext );

}

this.device.queue.submit( [ renderContextData.encoder.finish() ] );

//
Expand All @@ -297,6 +374,52 @@ class WebGPUBackend extends Backend {

}

isOccluded( renderContext, object ) {

const renderContextData = this.get( renderContext );

return renderContextData.occluded && renderContextData.occluded.has( object );

}

async resolveOccludedAsync( renderContext ) {

const renderContextData = this.get( renderContext );

// handle occlusion query results

const { currentOcclusionQueryBuffer, currentOcclusionQueryObjects } = renderContextData;

if ( currentOcclusionQueryBuffer && currentOcclusionQueryObjects ) {

const occluded = new WeakSet();

renderContextData.currentOcclusionQueryObjects = null;
renderContextData.currentOcclusionQueryBuffer = null;

await currentOcclusionQueryBuffer.mapAsync( GPUMapMode.READ );

const buffer = currentOcclusionQueryBuffer.getMappedRange();
const results = new BigUint64Array( buffer );

for ( let i = 0; i < currentOcclusionQueryObjects.length; i++ ) {

if ( results[ i ] !== 0n ) {

occluded.add( currentOcclusionQueryObjects[ i ], true );

}

}

currentOcclusionQueryBuffer.destroy();

renderContextData.occluded = occluded;

}

}

updateViewport( renderContext ) {

const { currentPass } = this.get( renderContext );
Expand Down Expand Up @@ -486,6 +609,34 @@ class WebGPUBackend extends Backend {

}

// occlusion queries - handle multiple consecutive draw calls for an object

if ( contextData.occlusionQuerySet !== undefined ) {

const lastObject = contextData.lastOcclusionObject;

if ( lastObject !== object ) {

if ( lastObject !== null && lastObject.occlusionTest === true ) {

passEncoderGPU.endOcclusionQuery();
contextData.occlusionQueryIndex ++;

}

if ( object.occlusionTest === true ) {

passEncoderGPU.beginOcclusionQuery( contextData.occlusionQueryIndex );
contextData.occlusionQueryObjects[ contextData.occlusionQueryIndex ] = object;

}

contextData.lastOcclusionObject = object;

}

}

// draw

const drawRange = geometry.drawRange;
Expand Down
Binary file added examples/screenshots/webgpu_occlusion.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.