UNPKG

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
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(); }); }); });