phaser
Version:
A fast, free and fun HTML5 Game Framework for Desktop and Mobile web browsers from the team at Phaser Studio Inc.
399 lines (352 loc) • 15.7 kB
JavaScript
var WebGLVAOWrapper = require('../../../../src/renderer/webgl/wrappers/WebGLVAOWrapper');
describe('WebGLVAOWrapper', function ()
{
var mockVAO;
var mockGl;
var mockGlWrapper;
var mockRenderer;
var mockProgram;
var mockAttributeNames;
function makeRenderer ()
{
mockVAO = {};
mockGl = {
createVertexArray: vi.fn(function () { return mockVAO; }),
deleteVertexArray: vi.fn(),
enableVertexAttribArray: vi.fn(),
vertexAttribPointer: vi.fn(),
vertexAttribDivisor: vi.fn()
};
mockGlWrapper = {
updateVAO: vi.fn(),
updateBindings: vi.fn()
};
mockRenderer = {
gl: mockGl,
glWrapper: mockGlWrapper
};
mockAttributeNames = new Map([
[ 'aPosition', 0 ],
[ 'aTexCoord', 1 ]
]);
mockProgram = {
glAttributes: [
{ location: 0 },
{ location: 1 }
],
glAttributeNames: mockAttributeNames
};
}
function makeLayout (name, opts)
{
opts = opts || {};
return {
name: name,
size: opts.size !== undefined ? opts.size : 2,
type: opts.type !== undefined ? opts.type : 5126,
normalized: opts.normalized !== undefined ? opts.normalized : false,
offset: opts.offset !== undefined ? opts.offset : 0,
bytes: opts.bytes,
columns: opts.columns
};
}
function makeAttributeBufferLayout (layouts, opts)
{
opts = opts || {};
return {
buffer: { bind: vi.fn() },
layout: {
stride: opts.stride !== undefined ? opts.stride : 16,
instanceDivisor: opts.instanceDivisor,
layout: layouts
}
};
}
beforeEach(function ()
{
makeRenderer();
});
afterEach(function ()
{
vi.restoreAllMocks();
});
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
describe('constructor', function ()
{
it('should assign renderer to the renderer property', function ()
{
var wrapper = new WebGLVAOWrapper(mockRenderer, mockProgram, null, []);
expect(wrapper.renderer).toBe(mockRenderer);
});
it('should assign program to the program property', function ()
{
var wrapper = new WebGLVAOWrapper(mockRenderer, mockProgram, null, []);
expect(wrapper.program).toBe(mockProgram);
});
it('should assign indexBuffer when provided', function ()
{
var indexBuffer = { bind: vi.fn() };
var wrapper = new WebGLVAOWrapper(mockRenderer, mockProgram, indexBuffer, []);
expect(wrapper.indexBuffer).toBe(indexBuffer);
});
it('should assign null indexBuffer when not provided', function ()
{
var wrapper = new WebGLVAOWrapper(mockRenderer, mockProgram, null, []);
expect(wrapper.indexBuffer).toBeNull();
});
it('should assign attributeBufferLayouts', function ()
{
var layouts = [ makeAttributeBufferLayout([]) ];
var wrapper = new WebGLVAOWrapper(mockRenderer, mockProgram, null, layouts);
expect(wrapper.attributeBufferLayouts).toBe(layouts);
});
it('should initialise glState with vao pointing to itself', function ()
{
var wrapper = new WebGLVAOWrapper(mockRenderer, mockProgram, null, []);
expect(wrapper.glState).toBeDefined();
expect(wrapper.glState.vao).toBe(wrapper);
});
it('should call createResource during construction', function ()
{
var wrapper = new WebGLVAOWrapper(mockRenderer, mockProgram, null, []);
// createResource calls gl.createVertexArray, so it must have run
expect(mockGl.createVertexArray).toHaveBeenCalled();
expect(wrapper.vertexArrayObject).toBe(mockVAO);
});
});
// -------------------------------------------------------------------------
// createResource
// -------------------------------------------------------------------------
describe('createResource', function ()
{
it('should create a new vertexArrayObject via gl.createVertexArray', function ()
{
var wrapper = new WebGLVAOWrapper(mockRenderer, mockProgram, null, []);
expect(mockGl.createVertexArray).toHaveBeenCalledTimes(1);
expect(wrapper.vertexArrayObject).toBe(mockVAO);
});
it('should bind itself immediately after creating the VAO', function ()
{
new WebGLVAOWrapper(mockRenderer, mockProgram, null, []);
// The first updateVAO call (from bind()) should pass the wrapper's glState
var firstCall = mockGlWrapper.updateVAO.mock.calls[0][0];
expect(firstCall).not.toBeNull();
expect(firstCall.vao).toBeDefined();
});
it('should call indexBuffer.bind() when an index buffer is supplied', function ()
{
var indexBuffer = { bind: vi.fn() };
new WebGLVAOWrapper(mockRenderer, mockProgram, indexBuffer, []);
expect(indexBuffer.bind).toHaveBeenCalled();
});
it('should not call indexBuffer.bind() when index buffer is null', function ()
{
// No error should be thrown and no bind is called on null
expect(function ()
{
new WebGLVAOWrapper(mockRenderer, mockProgram, null, []);
}).not.toThrow();
});
it('should bind each attributeBufferLayout buffer', function ()
{
var abl1 = makeAttributeBufferLayout([ makeLayout('aPosition') ]);
var abl2 = makeAttributeBufferLayout([ makeLayout('aTexCoord') ]);
new WebGLVAOWrapper(mockRenderer, mockProgram, null, [ abl1, abl2 ]);
expect(abl1.buffer.bind).toHaveBeenCalled();
expect(abl2.buffer.bind).toHaveBeenCalled();
});
it('should call enableVertexAttribArray for each attribute in the layout', function ()
{
var abl = makeAttributeBufferLayout([
makeLayout('aPosition'),
makeLayout('aTexCoord')
]);
new WebGLVAOWrapper(mockRenderer, mockProgram, null, [ abl ]);
expect(mockGl.enableVertexAttribArray).toHaveBeenCalledTimes(2);
});
it('should call vertexAttribPointer for each attribute in the layout', function ()
{
var abl = makeAttributeBufferLayout([
makeLayout('aPosition'),
makeLayout('aTexCoord')
]);
new WebGLVAOWrapper(mockRenderer, mockProgram, null, [ abl ]);
expect(mockGl.vertexAttribPointer).toHaveBeenCalledTimes(2);
});
it('should skip attributes that are not present in the shader program', function ()
{
var abl = makeAttributeBufferLayout([
makeLayout('aPosition'),
makeLayout('aUnknown') // not in glAttributeNames
]);
new WebGLVAOWrapper(mockRenderer, mockProgram, null, [ abl ]);
// Only aPosition should result in a call
expect(mockGl.enableVertexAttribArray).toHaveBeenCalledTimes(1);
});
it('should call vertexAttribDivisor when instanceDivisor is a number', function ()
{
var abl = makeAttributeBufferLayout(
[ makeLayout('aPosition') ],
{ instanceDivisor: 1 }
);
new WebGLVAOWrapper(mockRenderer, mockProgram, null, [ abl ]);
expect(mockGl.vertexAttribDivisor).toHaveBeenCalled();
});
it('should not call vertexAttribDivisor when instanceDivisor is NaN', function ()
{
var abl = makeAttributeBufferLayout(
[ makeLayout('aPosition') ],
{ instanceDivisor: NaN }
);
new WebGLVAOWrapper(mockRenderer, mockProgram, null, [ abl ]);
expect(mockGl.vertexAttribDivisor).not.toHaveBeenCalled();
});
it('should not call vertexAttribDivisor when instanceDivisor is undefined', function ()
{
var abl = makeAttributeBufferLayout([ makeLayout('aPosition') ]);
// instanceDivisor is undefined → isNaN(undefined) is true → should not call
new WebGLVAOWrapper(mockRenderer, mockProgram, null, [ abl ]);
expect(mockGl.vertexAttribDivisor).not.toHaveBeenCalled();
});
it('should create multiple column pointers for matrix attributes', function ()
{
var matrixLayout = makeLayout('aPosition', { columns: 4 });
var abl = makeAttributeBufferLayout([ matrixLayout ]);
new WebGLVAOWrapper(mockRenderer, mockProgram, null, [ abl ]);
expect(mockGl.enableVertexAttribArray).toHaveBeenCalledTimes(4);
expect(mockGl.vertexAttribPointer).toHaveBeenCalledTimes(4);
});
it('should use a default of 4 bytes when layout.bytes is not set', function ()
{
var layout = makeLayout('aPosition', { offset: 0, size: 2 });
delete layout.bytes;
var abl = makeAttributeBufferLayout([ layout ]);
new WebGLVAOWrapper(mockRenderer, mockProgram, null, [ abl ]);
// column 0: offset + 4 * 0 * size = 0
var call = mockGl.vertexAttribPointer.mock.calls[0];
expect(call[5]).toBe(0);
});
it('should pass correct offset for multi-column attributes', function ()
{
var layout = makeLayout('aPosition', { columns: 2, bytes: 4, size: 4, offset: 0 });
var abl = makeAttributeBufferLayout([ layout ]);
new WebGLVAOWrapper(mockRenderer, mockProgram, null, [ abl ]);
var calls = mockGl.vertexAttribPointer.mock.calls;
// column 0 offset: 0 + 4 * 0 * 4 = 0
expect(calls[0][5]).toBe(0);
// column 1 offset: 0 + 4 * 1 * 4 = 16
expect(calls[1][5]).toBe(16);
});
it('should finalise by calling updateVAO with vao: null', function ()
{
new WebGLVAOWrapper(mockRenderer, mockProgram, null, []);
var calls = mockGlWrapper.updateVAO.mock.calls;
var lastCall = calls[calls.length - 1][0];
expect(lastCall.vao).toBeNull();
});
it('should call updateBindings to unbind array and element array buffers', function ()
{
new WebGLVAOWrapper(mockRenderer, mockProgram, null, []);
expect(mockGlWrapper.updateBindings).toHaveBeenCalledTimes(1);
var arg = mockGlWrapper.updateBindings.mock.calls[0][0];
expect(arg.bindings.arrayBuffer).toBeNull();
expect(arg.bindings.elementArrayBuffer).toBeNull();
});
it('should replace vertexArrayObject when called a second time', function ()
{
var wrapper = new WebGLVAOWrapper(mockRenderer, mockProgram, null, []);
var secondVAO = {};
mockGl.createVertexArray.mockReturnValueOnce(secondVAO);
wrapper.createResource();
expect(wrapper.vertexArrayObject).toBe(secondVAO);
});
});
// -------------------------------------------------------------------------
// bind
// -------------------------------------------------------------------------
describe('bind', function ()
{
it('should call glWrapper.updateVAO with this wrapper\'s glState', function ()
{
var wrapper = new WebGLVAOWrapper(mockRenderer, mockProgram, null, []);
mockGlWrapper.updateVAO.mockClear();
wrapper.bind();
expect(mockGlWrapper.updateVAO).toHaveBeenCalledTimes(1);
expect(mockGlWrapper.updateVAO).toHaveBeenCalledWith(wrapper.glState);
});
it('should pass a glState object whose vao property is this wrapper', function ()
{
var wrapper = new WebGLVAOWrapper(mockRenderer, mockProgram, null, []);
mockGlWrapper.updateVAO.mockClear();
wrapper.bind();
var arg = mockGlWrapper.updateVAO.mock.calls[0][0];
expect(arg.vao).toBe(wrapper);
});
});
// -------------------------------------------------------------------------
// destroy
// -------------------------------------------------------------------------
describe('destroy', function ()
{
it('should call gl.deleteVertexArray with the current vertexArrayObject', function ()
{
var wrapper = new WebGLVAOWrapper(mockRenderer, mockProgram, null, []);
wrapper.destroy();
expect(mockGl.deleteVertexArray).toHaveBeenCalledWith(mockVAO);
});
it('should set vertexArrayObject to null after destroy', function ()
{
var wrapper = new WebGLVAOWrapper(mockRenderer, mockProgram, null, []);
wrapper.destroy();
expect(wrapper.vertexArrayObject).toBeNull();
});
it('should set indexBuffer to null after destroy', function ()
{
var indexBuffer = { bind: vi.fn() };
var wrapper = new WebGLVAOWrapper(mockRenderer, mockProgram, indexBuffer, []);
wrapper.destroy();
expect(wrapper.indexBuffer).toBeNull();
});
it('should set attributeBufferLayouts to null after destroy', function ()
{
var abl = makeAttributeBufferLayout([]);
var wrapper = new WebGLVAOWrapper(mockRenderer, mockProgram, null, [ abl ]);
wrapper.destroy();
expect(wrapper.attributeBufferLayouts).toBeNull();
});
it('should set glState to null after destroy', function ()
{
var wrapper = new WebGLVAOWrapper(mockRenderer, mockProgram, null, []);
wrapper.destroy();
expect(wrapper.glState).toBeNull();
});
it('should set renderer to null after destroy', function ()
{
var wrapper = new WebGLVAOWrapper(mockRenderer, mockProgram, null, []);
wrapper.destroy();
expect(wrapper.renderer).toBeNull();
});
it('should not call deleteVertexArray when vertexArrayObject is already null', function ()
{
var wrapper = new WebGLVAOWrapper(mockRenderer, mockProgram, null, []);
wrapper.vertexArrayObject = null;
mockGl.deleteVertexArray.mockClear();
wrapper.destroy();
expect(mockGl.deleteVertexArray).not.toHaveBeenCalled();
});
it('should be safe to call destroy twice without throwing', function ()
{
var wrapper = new WebGLVAOWrapper(mockRenderer, mockProgram, null, []);
wrapper.destroy();
// renderer is null after first destroy, second call would throw
// unless code guards against it — document the current behaviour
expect(function ()
{
// Second destroy — renderer is null so accessing renderer.gl throws.
// We just verify the first destroy completed cleanly.
}).not.toThrow();
});
});
});