MCP 3D Printer Server
by DMontgomery40
Verified
import { warnOnce } from '../../../utils.js';
import TimestampQueryPool from '../../common/TimestampQueryPool.js';
/**
* Manages a pool of WebGPU timestamp queries for performance measurement.
* Extends the base TimestampQueryPool to provide WebGPU-specific implementation.
* @extends TimestampQueryPool
*/
class WebGPUTimestampQueryPool extends TimestampQueryPool {
/**
* Creates a new WebGPU timestamp query pool.
* @param {GPUDevice} device - The WebGPU device to create queries on.
* @param {string} type - The type identifier for this query pool.
* @param {number} [maxQueries=2048] - Maximum number of queries this pool can hold.
*/
constructor( device, type, maxQueries = 2048 ) {
super( maxQueries );
this.device = device;
this.type = type;
this.querySet = this.device.createQuerySet( {
type: 'timestamp',
count: this.maxQueries,
label: `queryset_global_timestamp_${type}`
} );
const bufferSize = this.maxQueries * 8;
this.resolveBuffer = this.device.createBuffer( {
label: `buffer_timestamp_resolve_${type}`,
size: bufferSize,
usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC
} );
this.resultBuffer = this.device.createBuffer( {
label: `buffer_timestamp_result_${type}`,
size: bufferSize,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
} );
}
/**
* Allocates a pair of queries for a given render context.
* @param {Object} renderContext - The render context to allocate queries for.
* @returns {?number} The base offset for the allocated queries, or null if allocation failed.
*/
allocateQueriesForContext( renderContext ) {
if ( ! this.trackTimestamp || this.isDisposed ) return null;
if ( this.currentQueryIndex + 2 > this.maxQueries ) {
warnOnce( `WebGPUTimestampQueryPool [${ this.type }]: Maximum number of queries exceeded, when using trackTimestamp it is necessary to resolves the queries via renderer.resolveTimestampsAsync( THREE.TimestampQuery.${ this.type.toUpperCase() } ).` );
return null;
}
const baseOffset = this.currentQueryIndex;
this.currentQueryIndex += 2;
this.queryOffsets.set( renderContext.id, baseOffset );
return baseOffset;
}
/**
* Asynchronously resolves all pending queries and returns the total duration.
* If there's already a pending resolve operation, returns that promise instead.
* @returns {Promise<number>} The total duration in milliseconds, or the last valid value if resolution fails.
*/
async resolveQueriesAsync() {
if ( ! this.trackTimestamp || this.currentQueryIndex === 0 || this.isDisposed ) {
return this.lastValue;
}
if ( this.pendingResolve ) {
return this.pendingResolve;
}
this.pendingResolve = this._resolveQueries();
try {
const result = await this.pendingResolve;
return result;
} finally {
this.pendingResolve = null;
}
}
/**
* Internal method to resolve queries and calculate total duration.
* @private
* @returns {Promise<number>} The total duration in milliseconds.
*/
async _resolveQueries() {
if ( this.isDisposed ) {
return this.lastValue;
}
try {
if ( this.resultBuffer.mapState !== 'unmapped' ) {
return this.lastValue;
}
const currentOffsets = new Map( this.queryOffsets );
const queryCount = this.currentQueryIndex;
const bytesUsed = queryCount * 8;
// Reset state before GPU work
this.currentQueryIndex = 0;
this.queryOffsets.clear();
const commandEncoder = this.device.createCommandEncoder();
commandEncoder.resolveQuerySet(
this.querySet,
0,
queryCount,
this.resolveBuffer,
0
);
commandEncoder.copyBufferToBuffer(
this.resolveBuffer,
0,
this.resultBuffer,
0,
bytesUsed
);
const commandBuffer = commandEncoder.finish();
this.device.queue.submit( [ commandBuffer ] );
if ( this.resultBuffer.mapState !== 'unmapped' ) {
return this.lastValue;
}
// Create and track the mapping operation
await this.resultBuffer.mapAsync( GPUMapMode.READ, 0, bytesUsed );
if ( this.isDisposed ) {
if ( this.resultBuffer.mapState === 'mapped' ) {
this.resultBuffer.unmap();
}
return this.lastValue;
}
const times = new BigUint64Array( this.resultBuffer.getMappedRange( 0, bytesUsed ) );
let totalDuration = 0;
for ( const [ , baseOffset ] of currentOffsets ) {
const startTime = times[ baseOffset ];
const endTime = times[ baseOffset + 1 ];
const duration = Number( endTime - startTime ) / 1e6;
totalDuration += duration;
}
this.resultBuffer.unmap();
this.lastValue = totalDuration;
return totalDuration;
} catch ( error ) {
console.error( 'Error resolving queries:', error );
if ( this.resultBuffer.mapState === 'mapped' ) {
this.resultBuffer.unmap();
}
return this.lastValue;
}
}
async dispose() {
if ( this.isDisposed ) {
return;
}
this.isDisposed = true;
// Wait for pending resolve operation
if ( this.pendingResolve ) {
try {
await this.pendingResolve;
} catch ( error ) {
console.error( 'Error waiting for pending resolve:', error );
}
}
// Ensure buffer is unmapped before destroying
if ( this.resultBuffer && this.resultBuffer.mapState === 'mapped' ) {
try {
this.resultBuffer.unmap();
} catch ( error ) {
console.error( 'Error unmapping buffer:', error );
}
}
// Destroy resources
if ( this.querySet ) {
this.querySet.destroy();
this.querySet = null;
}
if ( this.resolveBuffer ) {
this.resolveBuffer.destroy();
this.resolveBuffer = null;
}
if ( this.resultBuffer ) {
this.resultBuffer.destroy();
this.resultBuffer = null;
}
this.queryOffsets.clear();
this.pendingResolve = null;
}
}
export default WebGPUTimestampQueryPool;