Home Reference Source

viewer/buffermanager.js

import {BufferSet} from "./bufferset.js";

/**
 * BufferManager keeps track of (CPU side) buffers, these buffers are eventually flushed to the GPU.
 * 
 * This class should be considered abstract. It contains all code that is shared by the implementations:
 * 	- BufferManagerPerColor (keeps a buffer alive per unique color)
 *  - BufferManagerTransparencyOnly (keeps two buffers alive, one for opaque objects and one for transparent)
 *  
 *  Because of allocation costs, buffers are reused. Flushed buffers will reset their indices (resetBuffer), subsequently overwriting old data.
 */
export class BufferManager {
	constructor(viewer, settings, renderer, bufferSetPool) {
		this.viewer = viewer;
		this.settings = settings;
		this.renderer = renderer;
		this.bufferSetPool = bufferSetPool;
		
		/*
		 * Speed/size tradeoff. Allocating huge buffers here means less calls to the GPU to flush, but especially when using the BufferManagerPerColor, 
		 * models with a lot of unique colors could potentially create a lot of buffers, each of MAX_BUFFER_SIZE. Smaller buffers also results in more updates 
		 * to the screen (good for progress indication)
		 */
		this.MAX_BUFFER_SIZE = 900000; // In number of vertex numbers, must be a multiple of 9

		// An average factor, amount of index numbers per vertex number
		this.indicesVerticesFactor = 0.5;
		
		// An average factor, amount of color numbers per vertex number
		this.colorBufferFactor = 1.33;
		
		// Map of buffers
		this.bufferSets = new Map();
		
		this.defaultSizes = {
			vertices: this.MAX_BUFFER_SIZE,
			normals: this.MAX_BUFFER_SIZE,
			indices: this.MAX_BUFFER_SIZE * this.indicesVerticesFactor,
			lineIndices: this.MAX_BUFFER_SIZE * this.indicesVerticesFactor,
			colors: this.MAX_BUFFER_SIZE * this.colorBufferFactor,
			pickColors: this.MAX_BUFFER_SIZE * this.colorBufferFactor
		};
	}

	/*
	 * Determines whether flushing is necessary, there are two reasons for flushing:
	 * - The given sizes are not going to fit in the buffer
	 * - The given sizes do not fit in a default buffer, in this case an exclusive buffer is given
	 */
	shouldFlush(sizes, buffer) {
		var result = 
			(sizes.vertices + (buffer != null ? buffer.positionsIndex : 0) > this.MAX_BUFFER_SIZE) || 
			(sizes.indices + (buffer != null ? buffer.indicesIndex : 0) > this.MAX_BUFFER_SIZE * this.indicesVerticesFactor) ||
			(sizes.lineIndices + (buffer != null ? buffer.lineIndicesIndex : 0) > this.MAX_BUFFER_SIZE * this.indicesVerticesFactor) ||
			(sizes.pickColors + (buffer != null ? buffer.pickColorsIndex : 0) > this.MAX_BUFFER_SIZE * this.colorBufferFactor);
		
		// Not storing the results in a variable resulted in this always returning something that evaluates to false...
		
		return result;
	}
	
	getDefaultByteSize() {
		var result = 
			this.defaultSizes.vertices * (this.settings.quantizeVertices ? 2 : 4) +
			this.defaultSizes.normals * (this.settings.quantizeNormals ? 1 : 4) +
			this.defaultSizes.indices * 4 +
			this.defaultSizes.lineIndices * 4;

		return result;
	}
	
	/*
	 * Get a buffer based on the given arguments, different implementations might not use all arguments
	 */
	getBufferSet(transparency, color, sizes) {
		if (this.shouldFlush(sizes, null)) {
			// The amount of geometry is more than the default buffer size, so this geometry gets its own buffer which also gets flushed right away
			var bufferSet = this.createBufferSet(transparency, color, sizes);
			// The renderer is responsable for flushing this buffer after it's populated, this flag tells it to do so
			bufferSet.needsToFlush = true;
			// We return immediately, and do _not_ store this buffer in the map, no need to
			return bufferSet;
		}
		var key = this.getKey(transparency, color, sizes);

		var bufferSet = this.bufferSets.get(key);
		if (bufferSet == null) {
			// Create a new buffer according to the defaults and store it in the buffers Map
			bufferSet = this.createBufferSet(transparency, color, this.defaultSizes);
			this.bufferSets.set(key, bufferSet);
		} else {
			if (this.shouldFlush(sizes, bufferSet)) {
				// In this case we flush the buffer right away (it's already populated with data). We then reset it and return immediately after this.
				this.renderer.flushBuffer(bufferSet);
				this.resetBuffer(bufferSet);
			}
		}
		return bufferSet;
	}
	
	/*
	 * Default implementation to create a buffer, subclasses can add other buffers
	 */
	createBufferSet(hasTransparency, color, sizes) {
		return new BufferSet(this.viewer, this.settings, hasTransparency, color, sizes);
	}
	
	createBufferSetPooled(hasTransparency, color, sizes) {
		var bufferSet = this.bufferSetPool.lease(this, hasTransparency, color, sizes);
		bufferSet.hasTransparency = hasTransparency;
		bufferSet.color = color;
		return bufferSet;
	}
	
	clear() {
		for (var bufferSet of this.bufferSets.values()) {
			this.bufferSetPool.release(bufferSet);
		}
		this.bufferSets = null;
	}
	
	resetBuffer(bufferSet) {
		if (bufferSet.reset) {
			bufferSet.reset(this.viewer);
		}
	}
	
	getAllBuffers() {
		return this.bufferSets.values();
	}
}