@spearwolf/twopoint5d
Version:
Create 2.5D realtime graphics and pixelart with WebGL and three.js
374 lines • 17.3 kB
JavaScript
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { VOBufferPool } from './VOBufferPool.js';
import { VOUtils } from './VOUtils.js';
import { VertexObjectPool } from './VertexObjectPool.js';
import { voBuffer, voIndex } from './constants.js';
describe('VertexObjectPool', () => {
let descriptor;
beforeEach(() => {
descriptor = {
vertexCount: 4,
indices: [0, 1, 2, 0, 2, 3],
attributes: {
foo: {
components: ['x', 'y'],
type: 'float32',
usage: 'dynamic',
},
bar: {
size: 1,
type: 'float32',
},
plah: {
components: ['a', 'b', 'c'],
type: 'float32',
},
zack: {
components: ['zack'],
type: 'float32',
usage: 'dynamic',
},
},
};
});
test('construct', () => {
const pool = new VertexObjectPool(descriptor, 100);
expect(pool).toBeDefined();
expect(pool.capacity).toBe(100);
expect(pool.usedCount).toBe(0);
expect(pool.availableCount).toBe(100);
});
describe('createVO()', () => {
test('vertexCount > 1', () => {
const pool = new VertexObjectPool(descriptor, 100);
const vo = pool.createVO();
vo.setFoo(3, 2, 1, 0, 4, 5, 6, 7);
vo.y1 = -1;
vo.x2 = -4;
vo.setBar([100, 101, 102, 103]);
vo.zack0 = 10;
vo.zack1 = 20;
vo.zack2 = 30;
vo.zack3 = 40;
expect(vo).toBeDefined();
expect(vo[voBuffer]).toBe(pool.buffer);
expect(Array.from(vo.getFoo())).toEqual([3, 2, 1, -1, -4, 5, 6, 7]);
expect(vo.x0).toBe(3);
expect(vo.y0).toBe(2);
expect(vo.x1).toBe(1);
expect(vo.y1).toBe(-1);
expect(vo.x2).toBe(-4);
expect(vo.y2).toBe(5);
expect(vo.x3).toBe(6);
expect(vo.y3).toBe(7);
expect(Array.from(vo.getBar())).toEqual([100, 101, 102, 103]);
expect(Array.from(vo.getZack())).toEqual([10, 20, 30, 40]);
expect(vo.zack0).toBe(10);
expect(vo.zack1).toBe(20);
expect(vo.zack2).toBe(30);
expect(vo.zack3).toBe(40);
});
test('vertexCount = 1', () => {
const pool = new VertexObjectPool({ meshCount: 1, attributes: descriptor.attributes }, 100);
const vo = pool.createVO();
vo.setFoo(3, 2);
vo.y = -2;
expect(vo).toBeDefined();
expect(vo[voBuffer]).toBe(pool.buffer);
expect(Array.from(vo.getFoo())).toEqual([3, -2]);
expect(vo.x).toBe(3);
expect(vo.y).toBe(-2);
expect(vo.bar).toBe(0);
expect(vo.zack).toBe(0);
vo.zack = 10;
expect(Array.from(vo[voBuffer].buffers.get('dynamic_float32').typedArray).slice(0, 3)).toEqual([3, -2, 10]);
vo.bar = 77;
vo.a = 99;
vo.b = 88;
vo.c = 66;
expect(Array.from(vo[voBuffer].buffers.get('static_float32').typedArray).slice(0, 4)).toEqual([77, 99, 88, 66]);
});
test('basePrototype', () => {
class BaseVO {
}
const pool = new VertexObjectPool({ ...descriptor, basePrototype: BaseVO.prototype }, 1);
const vo = pool.createVO();
expect(vo).toBeInstanceOf(BaseVO);
});
});
describe('freeVO()', () => {
test('clears the internal buffer reference', () => {
const pool = new VertexObjectPool(descriptor, 100);
const vo = pool.createVO();
expect(pool.usedCount).toBe(1);
expect(vo[voBuffer]).toBe(pool.buffer);
expect(vo[voIndex]).toBe(0);
pool.freeVO(vo);
expect(pool.usedCount).toBe(0);
expect(vo[voBuffer]).toBeUndefined();
});
test('copies and re-link the underlying internal buffers', () => {
const pool = new VertexObjectPool(descriptor, 100);
const vo0 = pool.createVO();
const vo1 = pool.createVO();
const vo2 = pool.createVO();
vo1.setFoo(30, 20, 10, 0, 40, 50, 60, 70);
vo2.setFoo(3, 2, 1, 0, 4, 5, 6, 7);
expect(pool.usedCount).toBe(3);
expect(vo0[voIndex]).toBe(0);
expect(vo1[voIndex]).toBe(1);
expect(vo2[voIndex]).toBe(2);
expect(Array.from(vo0.getFoo())).toEqual([0, 0, 0, 0, 0, 0, 0, 0]);
expect(Array.from(vo1.getFoo())).toEqual([30, 20, 10, 0, 40, 50, 60, 70]);
expect(Array.from(vo2.getFoo())).toEqual([3, 2, 1, 0, 4, 5, 6, 7]);
pool.freeVO(vo0);
expect(pool.usedCount).toBe(2);
expect(vo1[voIndex]).toBe(1);
expect(vo2[voIndex]).toBe(0);
expect(Array.from(vo1.getFoo())).toEqual([30, 20, 10, 0, 40, 50, 60, 70]);
expect(Array.from(vo2.getFoo())).toEqual([3, 2, 1, 0, 4, 5, 6, 7]);
const vo3 = pool.createVO();
vo3.setFoo(33, 22, 11, 0, 44, 55, 66, 77);
expect(pool.usedCount).toBe(3);
expect(vo1[voIndex]).toBe(1);
expect(vo2[voIndex]).toBe(0);
expect(vo3[voIndex]).toBe(2);
expect(Array.from(vo1.getFoo())).toEqual([30, 20, 10, 0, 40, 50, 60, 70]);
expect(Array.from(vo2.getFoo())).toEqual([3, 2, 1, 0, 4, 5, 6, 7]);
expect(Array.from(vo3.getFoo())).toEqual([33, 22, 11, 0, 44, 55, 66, 77]);
});
test('create vertex objects from attributes data', () => {
const pool = new VertexObjectPool(descriptor, 100);
const [objectCount, firstObjectIdx] = pool.createFromAttributes({
bar: [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3],
});
expect(objectCount).toEqual(3);
expect(firstObjectIdx).toEqual(0);
expect(pool.createFromAttributes({
bar: [1, 1, 1, 1, 2, 2, 2, 2, 3, 33, 333, 3333],
zack: [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 55, 555, 5555],
})).toEqual([5, objectCount]);
expect(pool.usedCount).toEqual(8);
expect(Array.from(pool.getVO(2).getBar())).toEqual([3, 3, 3, 3]);
expect(Array.from(pool.getVO(3).getBar())).toEqual([1, 1, 1, 1]);
expect(Array.from(pool.getVO(5).getBar())).toEqual([3, 33, 333, 3333]);
expect(Array.from(pool.getVO(7).getZack())).toEqual([5, 55, 555, 5555]);
});
test('use VertexObjectPool.setIndex() to use a single VO as proxy', () => {
const pool = new VertexObjectPool(descriptor, 100);
const vo = pool.createVO();
vo.setBar([1, 2, 3, 4]);
pool.usedCount = 2;
VOUtils.setIndex(vo, 1);
vo.setBar([5, 6, 7, 8]);
VOUtils.setIndex(vo, 0);
expect(Array.from(pool.getVO(0).getBar())).toEqual([1, 2, 3, 4]);
expect(Array.from(pool.getVO(1).getBar())).toEqual([5, 6, 7, 8]);
expect(vo).toBe(pool.getVO(0));
expect(vo).not.toBe(pool.getVO(1));
});
test('use buffersData structure to directly create a pool from typed arrays data without copying values', () => {
const source = new VertexObjectPool(descriptor, 100);
source.createVO().setBar([1, 2, 3, 4]);
source.createVO().setFoo([11, 22, 33, 44, 55, 66, 77, 88]);
const buffersData = source.toBuffersData();
const pool = new VertexObjectPool(descriptor, buffersData);
expect(pool.capacity).toBe(100);
expect(pool.usedCount).toBe(2);
expect(Array.from(pool.getVO(0).getBar())).toEqual([1, 2, 3, 4]);
expect(Array.from(pool.getVO(1).getFoo())).toEqual([11, 22, 33, 44, 55, 66, 77, 88]);
});
});
describe('resize()', () => {
test('resize to larger capacity preserves existing data', () => {
const pool = new VertexObjectPool(descriptor, 10);
const vo0 = pool.createVO();
vo0.setBar([1, 2, 3, 4]);
const vo1 = pool.createVO();
vo1.setFoo([11, 22, 33, 44, 55, 66, 77, 88]);
expect(pool.capacity).toBe(10);
expect(pool.usedCount).toBe(2);
pool.resize(50);
expect(pool.capacity).toBe(50);
expect(pool.usedCount).toBe(2);
expect(pool.availableCount).toBe(48);
expect(Array.from(pool.getVO(0).getBar())).toEqual([1, 2, 3, 4]);
expect(Array.from(pool.getVO(1).getFoo())).toEqual([11, 22, 33, 44, 55, 66, 77, 88]);
const vo2 = pool.createVO();
vo2.setBar([5, 6, 7, 8]);
expect(pool.usedCount).toBe(3);
expect(Array.from(pool.getVO(2).getBar())).toEqual([5, 6, 7, 8]);
});
test('resize to smaller capacity preserves data within new capacity', () => {
const pool = new VertexObjectPool(descriptor, 100);
const vo0 = pool.createVO();
vo0.setBar([1, 2, 3, 4]);
const vo1 = pool.createVO();
vo1.setFoo([11, 22, 33, 44, 55, 66, 77, 88]);
const vo2 = pool.createVO();
vo2.setBar([5, 6, 7, 8]);
expect(pool.capacity).toBe(100);
expect(pool.usedCount).toBe(3);
pool.resize(2);
expect(pool.capacity).toBe(2);
expect(pool.usedCount).toBe(2);
expect(pool.availableCount).toBe(0);
expect(Array.from(pool.getVO(0).getBar())).toEqual([1, 2, 3, 4]);
expect(Array.from(pool.getVO(1).getFoo())).toEqual([11, 22, 33, 44, 55, 66, 77, 88]);
expect(pool.getVO(2)).toBeUndefined();
const vo3 = pool.createVO();
expect(vo3).toBeUndefined();
});
test('resize to same capacity is a no-op', () => {
const pool = new VertexObjectPool(descriptor, 10);
const vo0 = pool.createVO();
vo0.setBar([1, 2, 3, 4]);
const oldBuffer = pool.buffer;
pool.resize(10);
expect(pool.capacity).toBe(10);
expect(pool.usedCount).toBe(1);
expect(pool.buffer).toBe(oldBuffer);
expect(Array.from(pool.getVO(0).getBar())).toEqual([1, 2, 3, 4]);
});
test('resize to 0 capacity', () => {
const pool = new VertexObjectPool(descriptor, 10);
pool.createVO().setBar([1, 2, 3, 4]);
pool.createVO().setFoo([11, 22, 33, 44, 55, 66, 77, 88]);
pool.resize(0);
expect(pool.capacity).toBe(0);
expect(pool.usedCount).toBe(0);
expect(pool.availableCount).toBe(0);
const vo = pool.createVO();
expect(vo).toBeUndefined();
});
test('resize preserves VO references in voIndex', () => {
const pool = new VertexObjectPool(descriptor, 10);
const vo0 = pool.createVO();
vo0.setBar([1, 2, 3, 4]);
const vo1 = pool.createVO();
vo1.setFoo([11, 22, 33, 44, 55, 66, 77, 88]);
pool.resize(50);
expect(pool.getVO(0)).toBe(vo0);
expect(pool.getVO(1)).toBe(vo1);
expect(Array.from(vo0.getBar())).toEqual([1, 2, 3, 4]);
expect(Array.from(vo1.getFoo())).toEqual([11, 22, 33, 44, 55, 66, 77, 88]);
});
test('resize updates buffer reference for existing VOs', () => {
const pool = new VertexObjectPool(descriptor, 10);
const vo0 = pool.createVO();
vo0.setBar([1, 2, 3, 4]);
const oldBuffer = pool.buffer;
pool.resize(50);
expect(pool.buffer).not.toBe(oldBuffer);
expect(Array.from(pool.getVO(0).getBar())).toEqual([1, 2, 3, 4]);
vo0.setBar([10, 20, 30, 40]);
expect(Array.from(pool.getVO(0).getBar())).toEqual([10, 20, 30, 40]);
});
test('resize throws error for negative capacity', () => {
const pool = new VertexObjectPool(descriptor, 10);
expect(() => pool.resize(-1)).toThrow('Capacity must be a non-negative integer');
});
test('resize throws error for non-integer capacity', () => {
const pool = new VertexObjectPool(descriptor, 10);
expect(() => pool.resize(10.5)).toThrow('Capacity must be a non-negative integer');
});
});
describe('dispose()', () => {
test('VOBufferPool: starts as not disposed and toggles isDisposed on dispose()', () => {
const pool = new VOBufferPool(descriptor, 10);
expect(pool.isDisposed).toBe(false);
pool.dispose();
expect(pool.isDisposed).toBe(true);
});
test('VOBufferPool: resets usedCount and releases typed-array references', () => {
const pool = new VOBufferPool(descriptor, 10);
pool.createFromAttributes({ bar: [1, 1, 1, 1, 2, 2, 2, 2] });
expect(pool.usedCount).toBe(2);
const buffersBefore = Array.from(pool.buffer.buffers.values());
expect(buffersBefore.length).toBeGreaterThan(0);
for (const buf of buffersBefore) {
expect(buf.typedArray).toBeDefined();
}
pool.dispose();
expect(pool.usedCount).toBe(0);
expect(pool.buffer.buffers.size).toBe(0);
for (const buf of buffersBefore) {
expect(buf.typedArray).toBeUndefined();
}
});
test('VOBufferPool: dispose() is idempotent', () => {
const pool = new VOBufferPool(descriptor, 4);
expect(() => {
pool.dispose();
pool.dispose();
pool.dispose();
}).not.toThrow();
expect(pool.isDisposed).toBe(true);
});
test('VertexObjectPool: clears buffer reference on every tracked VO', () => {
const pool = new VertexObjectPool(descriptor, 10);
const vo0 = pool.createVO();
const vo1 = pool.createVO();
const vo2 = pool.createVO();
expect(vo0[voBuffer]).toBe(pool.buffer);
expect(vo1[voBuffer]).toBe(pool.buffer);
expect(vo2[voBuffer]).toBe(pool.buffer);
pool.dispose();
expect(vo0[voBuffer]).toBeUndefined();
expect(vo1[voBuffer]).toBeUndefined();
expect(vo2[voBuffer]).toBeUndefined();
});
test('VertexObjectPool: invokes onDestroyVO for every still-alive VO', () => {
const pool = new VertexObjectPool(descriptor, 5);
const onDestroyVO = vi.fn();
pool.onDestroyVO = onDestroyVO;
const vo0 = pool.createVO();
const vo1 = pool.createVO();
const vo2 = pool.createVO();
pool.freeVO(vo1);
pool.dispose();
const destroyed = onDestroyVO.mock.calls.map(([vo]) => vo);
expect(destroyed).toContain(vo0);
expect(destroyed).toContain(vo2);
expect(destroyed).not.toContain(vo1);
});
test('VertexObjectPool: does not invoke onDestroyVO when no VOs are alive', () => {
const pool = new VertexObjectPool(descriptor, 5);
const onDestroyVO = vi.fn();
pool.onDestroyVO = onDestroyVO;
pool.dispose();
expect(onDestroyVO).not.toHaveBeenCalled();
});
test('VertexObjectPool: getVO() / createVO() are no-ops after dispose()', () => {
const pool = new VertexObjectPool(descriptor, 5);
pool.createVO();
pool.createVO();
pool.dispose();
expect(pool.getVO(0)).toBeUndefined();
expect(pool.getVO(1)).toBeUndefined();
expect(pool.isDisposed).toBe(true);
});
test('VertexObjectPool: dispose() is idempotent and a second call does not re-fire onDestroyVO', () => {
const pool = new VertexObjectPool(descriptor, 5);
const onDestroyVO = vi.fn();
pool.onDestroyVO = onDestroyVO;
pool.createVO();
pool.dispose();
pool.dispose();
expect(onDestroyVO).toHaveBeenCalledTimes(1);
});
test('VertexObjectPool: dispose() while VOs were freed-via-swap still tears the swapped slot down cleanly', () => {
const pool = new VertexObjectPool(descriptor, 4);
const vo0 = pool.createVO();
const vo1 = pool.createVO();
const vo2 = pool.createVO();
pool.freeVO(vo0);
expect(vo0[voBuffer]).toBeUndefined();
expect(VOUtils.getIndex(vo2)).toBe(0);
pool.dispose();
expect(vo1[voBuffer]).toBeUndefined();
expect(vo2[voBuffer]).toBeUndefined();
});
});
});
//# sourceMappingURL=VertexObjectPool.spec.js.map