diff --git a/src/scene/skin-instance.js b/src/scene/skin-instance.js index 803326f5d77..b9c8435842d 100644 --- a/src/scene/skin-instance.js +++ b/src/scene/skin-instance.js @@ -33,6 +33,7 @@ class SkinInstance { * matrices to generate the final matrix palette. */ constructor(skin) { + this._dirty = true; // optional root bone - used for cache lookup, not used for skinning @@ -44,6 +45,16 @@ class SkinInstance { // true if bones need to be updated before the frustum culling (bones are needed to update bounds of the MeshInstance) this._updateBeforeCull = true; + // ring buffer size. Multiple slots are used instead of just one to + // avoid GPU stalls during asynchronous read/write operations. + this._numTexturesInRing = 1; + + // current ring buffer texture index + this._currentRingIndex = 0; + + // ring buffer + this._boneTextureRingBuffer = []; + if (skin) { this.initSkin(skin); } @@ -57,33 +68,92 @@ class SkinInstance { return this._rootBone; } - init(device, numBones) { + set numTexturesInRing(value) { + this._numTexturesInRing = value; + this._resizeBoneTextureRingBuffer(value); + } - // texture size - roughly square that fits all bones, width is multiply of 3 to simplify shader math - const numPixels = numBones * 3; - let width = Math.ceil(Math.sqrt(numPixels)); - width = math.roundUp(width, 3); - const height = Math.ceil(numPixels / width); + get numTexturesInRing() { + return this._numTexturesInRing; + } + + get boneTexture() { + return this._boneTextureRingBuffer[this._currentRingIndex]; + } + + _nextTexture() { + this._currentRingIndex = (this._currentRingIndex + 1) % this._boneTextureRingBuffer.length; + return this._boneTextureRingBuffer[this._currentRingIndex]; + } + + _createBoneTexture(device, width, height, data) { - this.boneTexture = new Texture(device, { + return new Texture(device, { width: width, height: height, format: PIXELFORMAT_RGBA32F, mipmaps: false, minFilter: FILTER_NEAREST, magFilter: FILTER_NEAREST, - name: 'skin' + name: 'skin', + numLevels: data ? 1 : undefined, + levels: data ? [data] : undefined }); + } + + _resizeBoneTextureRingBuffer(size) { + + Debug.assert(size > 0, 'Bone texture ring buffer size must be more than 0'); + Debug.assert(this._boneTextureRingBuffer.length > 0, 'Bone texture ring buffer is empty'); + + const currentLength = this._boneTextureRingBuffer.length; + + if (currentLength > size) { - this.matrixPalette = this.boneTexture.lock({ mode: TEXTURELOCK_READ }); - this.boneTexture.unlock(); + for (let i = size; i < currentLength; i++) { + this._boneTextureRingBuffer[i]?.destroy(); + } + + this._boneTextureRingBuffer.length = size; + + } else if (currentLength < size) { + + const mainTexture = this._boneTextureRingBuffer[0]; + const device = mainTexture.device; + const width = mainTexture.width; + const height = mainTexture.height; + const data = this.matrixPalette; + + for (let i = currentLength; i < size; i++) { + const newTexture = this._createBoneTexture(device, width, height, data); + this._boneTextureRingBuffer.push(newTexture); + } + } } - destroy() { + init(device, numBones) { + + // texture size - roughly square that fits all bones, width is multiply of 3 to simplify shader math + const numPixels = numBones * 3; + let width = Math.ceil(Math.sqrt(numPixels)); + width = math.roundUp(width, 3); + const height = Math.ceil(numPixels / width); + + const mainBoneTexture = this._createBoneTexture(device, width, height); + + this.matrixPalette = mainBoneTexture.lock({ mode: TEXTURELOCK_READ }); - if (this.boneTexture) { - this.boneTexture.destroy(); - this.boneTexture = null; + mainBoneTexture.unlock(); + + this._boneTextureRingBuffer.push(mainBoneTexture); + + this._resizeBoneTextureRingBuffer(this._numTexturesInRing); + } + + destroy() { + if (this._boneTextureRingBuffer.length) { + this._boneTextureRingBuffer.map(x => x?.destroy()); + this._boneTextureRingBuffer.length = 0; } } @@ -137,7 +207,7 @@ class SkinInstance { } uploadBones(device) { - this.boneTexture.upload(); + this._nextTexture().upload(); } _updateMatrices(rootNode, skinUpdateIndex) {