diff --git a/src/assets/assetLoaderTypes/AssetLoaderTypeMesh.js b/src/assets/assetLoaderTypes/AssetLoaderTypeMesh.js index a45ebf91..215a38aa 100644 --- a/src/assets/assetLoaderTypes/AssetLoaderTypeMesh.js +++ b/src/assets/assetLoaderTypes/AssetLoaderTypeMesh.js @@ -41,7 +41,7 @@ export class AssetLoaderTypeMesh extends AssetLoaderType { assertInstanceType: VertexState, }, }); - mesh.setVertexState(vertexState); + mesh.setVertexState(vertexState, { deleteUnusedBuffers: true }); } const indexFormat = decomposer.getUint8(); diff --git a/src/core/Mesh.js b/src/core/Mesh.js index 1fb90b21..4c0c3033 100644 --- a/src/core/Mesh.js +++ b/src/core/Mesh.js @@ -344,7 +344,7 @@ export class Mesh { /** * @param {InternalMeshAttributeBuffer} attributeBuffer */ - copyAttributeBufferData(attributeBuffer) { + copyAttributeBufferData(attributeBuffer, allowUnusedBufferCreation = true) { // todo: there's probably still some performance that can be gained here // currently it's decomposing the buffer into vectors and turning // it back into a buffer, if the buffer doesn't need to be changed it @@ -355,12 +355,24 @@ export class Mesh { // TODO: handle converting attribute data when the attribute type is not specified continue; } - const array = Array.from(attributeBuffer.getVertexData(attribute.attributeType)); - const castArray = /** @type {number[] | import("../math/Vec2.js").Vec2[] | import("../math/Vec3.js").Vec3[]} */ (array); - this.setVertexData(attribute.attributeType, castArray, { + + /** @type {UnusedAttributeBufferOptions} */ + const opts = { unusedFormat: attribute.format, unusedComponentCount: attribute.componentCount, - }); + }; + + if (!allowUnusedBufferCreation) { + const existingBuffer = this.#getInternalAttributeBuffer(attribute.attributeType, { + ...opts, + createUnused: false, + }); + if (!existingBuffer) continue; + } + + const array = Array.from(attributeBuffer.getVertexData(attribute.attributeType)); + const castArray = /** @type {number[] | import("../math/Vec2.js").Vec2[] | import("../math/Vec3.js").Vec3[]} */ (array); + this.setVertexData(attribute.attributeType, castArray, opts); } } @@ -453,8 +465,13 @@ export class Mesh { * This means that old references obtained via {@linkcode getAttributeBufferForType} * will no longer be part of this mesh. Only when the format stays the same, will old references be reused. * @param {import("../rendering/VertexState.js").VertexState?} vertexState + * @param {object} [options] + * @param {boolean} [options.deleteUnusedBuffers] Defaults to true, deletes all old buffers that are not present in + * the provided vertexState. */ - setVertexState(vertexState) { + setVertexState(vertexState, { + deleteUnusedBuffers = true, + } = {}) { if (vertexState == this.#vertexState) return; this.#vertexState = vertexState; @@ -504,7 +521,7 @@ export class Mesh { } for (const buffer of buffersNeedingCopy) { - this.copyAttributeBufferData(buffer); + this.copyAttributeBufferData(buffer, !deleteUnusedBuffers); } } diff --git a/test/unit/src/core/Mesh/Mesh.test.js b/test/unit/src/core/Mesh/Mesh.test.js index 7eba6a1c..37ef7e0f 100644 --- a/test/unit/src/core/Mesh/Mesh.test.js +++ b/test/unit/src/core/Mesh/Mesh.test.js @@ -1,6 +1,6 @@ import { assertEquals, assertExists, assertNotStrictEquals, assertStrictEquals, assertThrows } from "std/testing/asserts.ts"; import { Mesh, Vec2, Vec3, Vec4 } from "../../../../../src/mod.js"; -import { FakeVertexState, mockVertexStateColor, mockVertexStateSingleAttribute, mockVertexStateSingleAttributeSameFormat, mockVertexStateTwoAttributes, mockVertexStateUv } from "./shared.js"; +import { FakeVertexState, mockVertexStateColor, mockVertexStateSingleAttribute, mockVertexStateSingleAttributeSameFormat, mockVertexStateTwoAttributes, mockVertexStateTwoBuffers, mockVertexStateUv } from "./shared.js"; import { assertVecAlmostEquals } from "../../../../../src/util/asserts.js"; Deno.test({ @@ -543,6 +543,79 @@ Deno.test({ assertEquals(result2.isUnused, true); assertEquals(Array.from(mesh.getAttributeBuffers()).length, 2); + mesh.setVertexState(mockVertexStateTwoAttributes, { deleteUnusedBuffers: false }); + + assertEquals(Array.from(mesh.getAttributeBuffers()).length, 1); + + const positions = Array.from(mesh.getVertexData(Mesh.AttributeType.POSITION)); + assertEquals(positions.length, 2); + assertVecAlmostEquals(positions[0], [1, 2, 3]); + assertVecAlmostEquals(positions[1], [4, 5, 6]); + + const normals = Array.from(mesh.getVertexData(Mesh.AttributeType.NORMAL)); + assertEquals(normals.length, 2); + assertVecAlmostEquals(normals[0], [1, 2, 3]); + assertVecAlmostEquals(normals[1], [4, 5, 6]); + }, +}); + +Deno.test({ + name: "setVertexState() deletes old unused buffers by default.", + fn() { + const mesh = new Mesh(); + mesh.setVertexState(mockVertexStateTwoBuffers); + mesh.setVertexCount(2); + mesh.setVertexData(Mesh.AttributeType.POSITION, [ + new Vec3(1, 2, 3), + new Vec3(4, 5, 6), + ]); + mesh.setVertexData(Mesh.AttributeType.NORMAL, [ + new Vec3(1, 2, 3), + new Vec3(4, 5, 6), + ]); + + const result1 = mesh.getAttributeBufferForType(Mesh.AttributeType.POSITION); + assertExists(result1); + assertEquals(result1.isUnused, false); + const result2 = mesh.getAttributeBufferForType(Mesh.AttributeType.NORMAL); + assertExists(result2); + assertEquals(result2.isUnused, false); + assertEquals(Array.from(mesh.getAttributeBuffers()).length, 2); + + mesh.setVertexState(mockVertexStateSingleAttribute); + + assertEquals(Array.from(mesh.getAttributeBuffers()).length, 1); + + const positions = Array.from(mesh.getVertexData(Mesh.AttributeType.POSITION)); + assertEquals(positions.length, 2); + assertVecAlmostEquals(positions[0], [1, 2, 3]); + assertVecAlmostEquals(positions[1], [4, 5, 6]); + }, +}); + +Deno.test({ + name: "setVertexState() converts two separate buffers into a single interleaved one.", + fn() { + const mesh = new Mesh(); + mesh.setVertexState(mockVertexStateTwoBuffers); + mesh.setVertexCount(2); + mesh.setVertexData(Mesh.AttributeType.POSITION, [ + new Vec3(1, 2, 3), + new Vec3(4, 5, 6), + ]); + mesh.setVertexData(Mesh.AttributeType.NORMAL, [ + new Vec3(1, 2, 3), + new Vec3(4, 5, 6), + ]); + + const result1 = mesh.getAttributeBufferForType(Mesh.AttributeType.POSITION); + assertExists(result1); + assertEquals(result1.isUnused, false); + const result2 = mesh.getAttributeBufferForType(Mesh.AttributeType.NORMAL); + assertExists(result2); + assertEquals(result2.isUnused, false); + assertEquals(Array.from(mesh.getAttributeBuffers()).length, 2); + mesh.setVertexState(mockVertexStateTwoAttributes); assertEquals(Array.from(mesh.getAttributeBuffers()).length, 1); diff --git a/test/unit/src/core/Mesh/shared.js b/test/unit/src/core/Mesh/shared.js index 4d6b0b07..fb846293 100644 --- a/test/unit/src/core/Mesh/shared.js +++ b/test/unit/src/core/Mesh/shared.js @@ -101,3 +101,32 @@ export const mockVertexStateColor = /** @type {import("../../../../../src/mod.js ]), }, ])); + +export const mockVertexStateTwoBuffers = /** @type {import("../../../../../src/mod.js").VertexState} */ (new FakeVertexState([ + { + attributes: new Map([ + [ + Mesh.AttributeType.POSITION, + { + attributeType: Mesh.AttributeType.POSITION, + offset: 0, + format: Mesh.AttributeFormat.FLOAT32, + componentCount: 3, + }, + ], + ]), + }, + { + attributes: new Map([ + [ + Mesh.AttributeType.NORMAL, + { + attributeType: Mesh.AttributeType.NORMAL, + offset: 12, + format: Mesh.AttributeFormat.FLOAT32, + componentCount: 3, + }, + ], + ]), + }, +]));