Source: rendering/renderers/shared/buffer/Buffer.ts

import EventEmitter from 'eventemitter3';
import { uid } from '../../../../utils/data/uid';
import { BufferUsage } from './const';

import type { BindResource } from '../../gpu/shader/BindResource';

/** All the various typed arrays that exist in js */
// eslint-disable-next-line max-len
export type TypedArray = Int8Array | Uint8Array | Int16Array | Uint16Array | Int32Array | Uint32Array | Uint8ClampedArray | Float32Array | Float64Array;

/** Options for creating a buffer */
export interface BufferOptions
{
    /**
     * the data to initialize the buffer with, this can be a typed array,
     * or a regular number array. If it is a number array, it will be converted to a Float32Array
     */
    data?: TypedArray | number[];
    /** the size of the buffer in bytes, if not supplied, it will be inferred from the data */
    size?: number;
    /** the usage of the buffer, see BufferUsage */
    usage: number;
    /** a label for the buffer, this is useful for debugging */
    label?: string;
    /**
     * should the GPU buffer be shrunk when the data becomes smaller?
     * changing this will cause the buffer to be destroyed and a new one created on the GPU
     * this can be expensive, especially if the buffer is already big enough!
     * setting this to false will prevent the buffer from being shrunk. This will yield better performance
     * if you are constantly setting data that is changing size often.
     * @default true
     */
    shrinkToFit?: boolean;
}

export interface BufferDescriptor
{
    label?: string;
    size: GPUSize64;
    usage: BufferUsage;
    mappedAtCreation?: boolean;
}

/**
 * A wrapper for a WebGPU/WebGL Buffer.
 * In PixiJS, the Buffer class is used to manage the data that is sent to the GPU rendering pipeline.
 * It abstracts away the underlying GPU buffer and provides an interface for uploading typed arrays or other data to the GPU,
 * They are used in the following places:
 * <br><br>
 * .1. Geometry as attribute data or index data for geometry
 * <br>
 * .2. UniformGroup as an underlying buffer for uniform data
 * <br>
 * .3. BufferResource as an underlying part of a buffer used directly by the GPU program
 * <br>
 *
 * It is important to note that you must provide a usage type when creating a buffer. This is because
 * the underlying GPU buffer needs to know how it will be used. For example, if you are creating a buffer
 * to hold vertex data, you would use `BufferUsage.VERTEX`. This will tell the GPU that this buffer will be
 * used as a vertex buffer. This is important because it will affect how you can use the buffer.
 *
 * Buffers are updated by calling the Buffer.update method. This immediately updates the buffer on the GPU.
 * Be mindful of calling this more often than you need to. It is recommended to update buffers only when needed.
 *
 * In WebGPU, a GPU buffer cannot resized. This limitation is abstracted away, but know that resizing a buffer means
 * creating a brand new one and destroying the old, so it is best to limit this if possible.
 * @example
 *
 * const buffer = new Buffer({
 *     data: new Float32Array([1, 2, 3, 4]),
 *     usage: BufferUsage.VERTEX,
 * });
 * @memberof rendering
 */
export class Buffer extends EventEmitter<{
    change: BindResource,
    update: Buffer,
    destroy: Buffer,
}> implements BindResource
{
    /**
     * emits when the underlying buffer has changed shape (i.e. resized)
     * letting the renderer know that it needs to discard the old buffer on the GPU and create a new one
     * @event change
     */

    /**
     * emits when the underlying buffer data has been updated. letting the renderer know
     * that it needs to update the buffer on the GPU
     * @event update
     */

    /**
     * emits when the buffer is destroyed. letting the renderer know that it needs to destroy the buffer on the GPU
     * @event destroy
     */

    /**
     * a unique id for this uniform group used through the renderer
     * @internal
     * @ignore
     */
    public readonly uid = uid('buffer');

    /**
     * a resource type, used to identify how to handle it when its in a bind group / shader resource
     * @internal
     * @ignore
     */
    public readonly _resourceType = 'buffer';

    /**
     * the resource id used internally by the renderer to build bind group keys
     * @internal
     * @ignore
     */
    public _resourceId = uid('resource');

    /**
     * used internally to know if a uniform group was used in the last render pass
     * @internal
     * @ignore
     */
    public _touched = 0;

    /**
     * a description of the buffer and how it should be set up on the GPU
     * @internal
     * @ignore
     */
    public readonly descriptor: BufferDescriptor;

    /**
     * @internal
     * @ignore
     */
    public _updateID = 1;

    /**
     * @internal
     * @ignore
     */
    public _updateSize: number;

    private _data: TypedArray;

    /**
     * should the GPU buffer be shrunk when the data becomes smaller?
     * changing this will cause the buffer to be destroyed and a new one created on the GPU
     * this can be expensive, especially if the buffer is already big enough!
     * setting this to false will prevent the buffer from being shrunk. This will yield better performance
     * if you are constantly setting data that is changing size often.
     * @default true
     */
    public shrinkToFit = true;

    /**
     * Has the buffer been destroyed?
     * @readonly
     */
    public destroyed = false;

    /**
     * Creates a new Buffer with the given options
     * @param options - the options for the buffer
     */
    constructor(options: BufferOptions)
    {
        let { data, size } = options;
        const { usage, label, shrinkToFit } = options;

        super();

        if (data instanceof Array)
        {
            data = new Float32Array(data as number[]);
        }

        this._data = data as TypedArray;

        size = size ?? (data as TypedArray)?.byteLength;

        const mappedAtCreation = !!data;

        this.descriptor = {
            size,
            usage,
            mappedAtCreation,
            label,
        };

        this.shrinkToFit = shrinkToFit ?? true;
    }

    /** the data in the buffer */
    get data()
    {
        return this._data;
    }

    set data(value: TypedArray)
    {
        this.setDataWithSize(value, value.length, true);
    }

    /** whether the buffer is static or not */
    get static()
    {
        return !!(this.descriptor.usage & BufferUsage.STATIC);
    }

    set static(value: boolean)
    {
        if (value)
        {
            this.descriptor.usage |= BufferUsage.STATIC;
        }
        else
        {
            this.descriptor.usage &= ~BufferUsage.STATIC;
        }
    }

    /**
     * Sets the data in the buffer to the given value. This will immediately update the buffer on the GPU.
     * If you only want to update a subset of the buffer, you can pass in the size of the data.
     * @param value - the data to set
     * @param size - the size of the data in bytes
     * @param syncGPU - should the buffer be updated on the GPU immediately?
     */
    public setDataWithSize(value: TypedArray, size: number, syncGPU: boolean)
    {
        // Increment update ID
        this._updateID++;

        this._updateSize = (size * value.BYTES_PER_ELEMENT);

        // If the data hasn't changed, early return after emitting 'update'
        if (this._data === value)
        {
            if (syncGPU) this.emit('update', this);

            return;
        }

        // Cache old data and update to new value
        const oldData = this._data;

        this._data = value;

        // Event handling
        if (oldData.length !== value.length)
        {
            if (!this.shrinkToFit && value.byteLength < oldData.byteLength)
            {
                if (syncGPU) this.emit('update', this);
            }
            else
            {
                this.descriptor.size = value.byteLength;
                this._resourceId = uid('resource');
                this.emit('change', this);
            }

            return;
        }

        if (syncGPU) this.emit('update', this);
    }

    /**
     * updates the buffer on the GPU to reflect the data in the buffer.
     * By default it will update the entire buffer. If you only want to update a subset of the buffer,
     * you can pass in the size of the buffer to update.
     * @param sizeInBytes - the new size of the buffer in bytes
     */
    public update(sizeInBytes?: number): void
    {
        this._updateSize = sizeInBytes ?? this._updateSize;

        this._updateID++;

        this.emit('update', this);
    }

    /** Destroys the buffer */
    public destroy()
    {
        this.destroyed = true;

        this.emit('destroy', this);
        this.emit('change', this);

        this._data = null;
        (this.descriptor as null) = null;

        this.removeAllListeners();
    }
}