Files
SpeedScope/src/gl/graphics.ts
T
Jamie Wong 375040e892 Bump dependency versions to unbreak build (#253)
I ended up in a horrible peer dependency hell and apparently needed to bump the versions of quicktype, typescript, ts-jest, *and* jest to get out of it. But I think I got out of it!

Local builds and deployment builds both seem to work after these changes.
2020-01-15 23:32:14 -08:00

1380 lines
40 KiB
TypeScript

// This is a port of the GPU APIs from https://github.com/evanw/sky from Skew to
// TypeScript.
//
// The MIT License (MIT)
// Original work Copyright (c) 2016 Evan Wallace
// Modified work Copyright (c) 2018 Jamie Wong
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
// NOTE: This file intentionally has no dependencies.
// Dependencies & polyfills for import from skew
const RELEASE =
typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'production'
function assert(condition: boolean) {
if (!RELEASE) {
if (!condition) throw new Error('Assertion failed.')
}
}
function appendOne<T>(ts: T[], t: T): void {
if (ts.indexOf(t) === -1) ts.push(t)
}
function removeOne<T>(ts: T[], t: T): void {
const index = ts.indexOf(t)
if (index !== -1) ts.splice(index, 1)
}
function TEXTURE_N(gl: WebGLRenderingContext, index: number) {
assert(index >= 0 && index <= 31)
return (gl.TEXTURE0 + index) as GLenum
}
export namespace Graphics {
export class Rect {
constructor(
public x: number = 0,
public y: number = 0,
public width: number = 0,
public height: number = 0,
) {}
set(x: number, y: number, width: number, height: number) {
this.x = x
this.y = y
this.width = width
this.height = height
}
equals(other: Rect) {
return (
this.x === other.x &&
this.y === other.y &&
this.width === other.width &&
this.height === other.height
)
}
}
export class Color {
constructor(
public redF: number,
public greenF: number,
public blueF: number,
public alphaF: number,
) {}
static TRANSPARENT = new Color(0, 0, 0, 0)
}
// Converted from https://github.com/evanw/sky/blob/c72de77/src/graphics/context.sk
export enum BlendOperation {
ZERO = 0,
ONE = 1,
SOURCE_COLOR = 2,
TARGET_COLOR = 3,
INVERSE_SOURCE_COLOR = 4,
INVERSE_TARGET_COLOR = 5,
SOURCE_ALPHA = 6,
TARGET_ALPHA = 7,
INVERSE_SOURCE_ALPHA = 8,
INVERSE_TARGET_ALPHA = 9,
CONSTANT = 10,
INVERSE_CONSTANT = 11,
}
export enum Primitive {
TRIANGLES,
TRIANGLE_STRIP,
}
export abstract class Context {
abstract addContextResetHandler(callback: () => void): void
abstract beginFrame(): void
abstract clear(color: Color): void
abstract createMaterial(
format: VertexFormat,
vertexSource: string,
fragmentSource: string,
): Material
abstract createTexture(
format: TextureFormat,
width: number,
height: number,
pixels?: Uint8Array,
): Texture
abstract createRenderTarget(texture: Texture): RenderTarget
abstract createVertexBuffer(byteCount: number): VertexBuffer
abstract currentRenderTarget: RenderTarget | null
abstract draw(primitive: Primitive, material: Material, vertices: VertexBuffer): void
abstract endFrame(): void
abstract removeContextResetHandler(callback: () => void): void
abstract resize(
widthInPixels: number,
heightInPixels: number,
widthInAppUnits: number,
heightInAppUnits: number,
): void
abstract setRenderTarget(renderTarget: RenderTarget | null): void
abstract setViewport(x: number, y: number, width: number, height: number): void
abstract viewport: Rect
abstract widthInPixels: number
abstract heightInPixels: number
abstract renderTargetHeightInPixels: number
abstract renderTargetWidthInPixels: number
abstract setBlendState(source: BlendOperation, target: BlendOperation): void
setCopyBlendState() {
this.setBlendState(BlendOperation.ONE, BlendOperation.ZERO)
}
setAddBlendState() {
this.setBlendState(BlendOperation.ONE, BlendOperation.ONE)
}
setPremultipliedBlendState() {
this.setBlendState(BlendOperation.ONE, BlendOperation.INVERSE_SOURCE_ALPHA)
}
setUnpremultipliedBlendState() {
this.setBlendState(BlendOperation.SOURCE_ALPHA, BlendOperation.INVERSE_SOURCE_ALPHA)
}
}
export interface Material {
context: Context
format: VertexFormat
fragmentSource: string
vertexSource: string
setUniformFloat(name: string, x: number): void
setUniformInt(name: string, x: number): void
setUniformVec2(name: string, x: number, y: number): void
setUniformVec3(name: string, x: number, y: number, z: number): void
setUniformVec4(name: string, x: number, y: number, z: number, w: number): void
setUniformMat3(
name: string,
m00: number,
m01: number,
m02: number,
m10: number,
m11: number,
m12: number,
m20: number,
m21: number,
m22: number,
): void
setUniformSampler(name: string, texture: Texture, index: number): void
}
export enum AttributeType {
FLOAT,
BYTE,
}
export function attributeByteLength(type: AttributeType) {
return type == AttributeType.FLOAT ? 4 : 1
}
export class Attribute {
constructor(
readonly name: string,
readonly type: AttributeType,
readonly count: number,
readonly byteOffset: number,
) {}
}
export class VertexFormat {
private _attributes: Attribute[] = []
private _stride = 0
get attributes() {
return this._attributes
}
get stride() {
return this._stride
}
add(name: string, type: AttributeType, count: number): VertexFormat {
this.attributes.push(new Attribute(name, type, count, this.stride))
this._stride += count * attributeByteLength(type)
return this
}
}
export abstract class VertexBuffer {
abstract byteCount: number
abstract context: Context
abstract move(sourceByteOffset: number, targetByteOffset: number, byteCount: number): void
abstract upload(bytes: Uint8Array, byteOffset?: number): void
uploadFloat32Array(floats: Float32Array) {
this.upload(new Uint8Array(floats.buffer), 0)
}
uploadFloats(floats: number[]) {
this.uploadFloat32Array(new Float32Array(floats))
}
abstract free(): void
}
export enum PixelFilter {
NEAREST,
LINEAR,
}
export enum PixelWrap {
REPEAT,
CLAMP,
}
export class TextureFormat {
constructor(
readonly minFilter: PixelFilter,
readonly magFilter: PixelFilter,
readonly wrap: PixelWrap,
) {}
static LINEAR_CLAMP = new TextureFormat(PixelFilter.LINEAR, PixelFilter.LINEAR, PixelWrap.CLAMP)
static LINEAR_MIN_NEAREST_MAG_CLAMP = new TextureFormat(
PixelFilter.LINEAR,
PixelFilter.NEAREST,
PixelWrap.CLAMP,
)
static NEAREST_CLAMP = new TextureFormat(
PixelFilter.NEAREST,
PixelFilter.NEAREST,
PixelWrap.CLAMP,
)
}
export interface Texture {
context: Context
format: TextureFormat
width: number
height: number
resize(width: number, height: number, pixels?: Uint8Array): void
setFormat(format: TextureFormat): void
free(): void
}
export interface RenderTarget {
context: Context
texture: Texture
setColor(texture: Texture): void
free(): void
}
}
// Converted from https://github.com/evanw/sky/blob/c72de77/src/browser/context.sk
export namespace WebGL {
export class Context extends Graphics.Context {
private _attributeCount = 0
private _blendOperationMap: {[key: number]: GLenum}
private _blendOperations = 0
private _contextResetHandlers: (() => void)[] = []
private _currentClearColor = Graphics.Color.TRANSPARENT
private _currentRenderTarget: RenderTarget | null = null
private _defaultViewport = new Graphics.Rect()
private _forceStateUpdate = true
private _generation = 1
private _gl: WebGLRenderingContext
private _height = 0
private _oldBlendOperations = 0
private _oldRenderTarget: RenderTarget | null = null
private _oldViewport = new Graphics.Rect()
private _width = 0
get widthInPixels() {
return this._width
}
get heightInPixels() {
return this._height
}
constructor(canvas: HTMLCanvasElement = document.createElement('canvas')) {
super()
let gl = canvas.getContext('webgl', {
alpha: false,
antialias: false,
depth: false,
preserveDrawingBuffer: false,
stencil: false,
})
if (gl == null) {
throw new Error('Setup failure')
}
this._gl = gl
let style = canvas.style
canvas.width = 0
canvas.height = 0
style.width = style.height = '0'
canvas.addEventListener('webglcontextlost', (e: Event) => {
e.preventDefault()
})
canvas.addEventListener('webglcontextrestored', this.handleWebglContextRestored)
// Using maps makes these compact in release
this._blendOperationMap = {
[Graphics.BlendOperation.ZERO]: this._gl.ZERO,
[Graphics.BlendOperation.ONE]: this._gl.ONE,
[Graphics.BlendOperation.SOURCE_COLOR]: this._gl.SRC_COLOR,
[Graphics.BlendOperation.TARGET_COLOR]: this._gl.DST_COLOR,
[Graphics.BlendOperation.INVERSE_SOURCE_COLOR]: this._gl.ONE_MINUS_SRC_COLOR,
[Graphics.BlendOperation.INVERSE_TARGET_COLOR]: this._gl.ONE_MINUS_DST_COLOR,
[Graphics.BlendOperation.SOURCE_ALPHA]: this._gl.SRC_ALPHA,
[Graphics.BlendOperation.TARGET_ALPHA]: this._gl.DST_ALPHA,
[Graphics.BlendOperation.INVERSE_SOURCE_ALPHA]: this._gl.ONE_MINUS_SRC_ALPHA,
[Graphics.BlendOperation.INVERSE_TARGET_ALPHA]: this._gl.ONE_MINUS_DST_ALPHA,
[Graphics.BlendOperation.CONSTANT]: this._gl.CONSTANT_COLOR,
[Graphics.BlendOperation.INVERSE_CONSTANT]: this._gl.ONE_MINUS_CONSTANT_COLOR,
}
}
private handleWebglContextRestored = () => {
this._attributeCount = 0
this._currentClearColor = Graphics.Color.TRANSPARENT
this._forceStateUpdate = true
this._generation++
for (let handler of this._contextResetHandlers) {
handler()
}
}
public testContextLoss() {
this.handleWebglContextRestored()
}
get gl() {
return this._gl
}
get generation() {
return this._generation
}
addContextResetHandler(callback: () => void) {
appendOne(this._contextResetHandlers, callback)
}
removeContextResetHandler(callback: () => void) {
removeOne(this._contextResetHandlers, callback)
}
get currentRenderTarget() {
return this._currentRenderTarget
}
beginFrame() {
this.setRenderTarget(null)
}
endFrame() {}
setBlendState(source: Graphics.BlendOperation, target: Graphics.BlendOperation) {
this._blendOperations = Context._packBlendModes(source, target)
}
setViewport(x: number, y: number, width: number, height: number) {
;(this._currentRenderTarget != null
? this._currentRenderTarget.viewport
: this._defaultViewport
).set(x, y, width, height)
}
get viewport() {
return this._currentRenderTarget != null
? this._currentRenderTarget.viewport
: this._defaultViewport
}
get renderTargetWidthInPixels() {
return this._currentRenderTarget != null
? this._currentRenderTarget.viewport.width
: this._width
}
get renderTargetHeightInPixels() {
return this._currentRenderTarget != null
? this._currentRenderTarget.viewport.height
: this._height
}
draw(
primitive: Graphics.Primitive,
material: Graphics.Material,
vertices: Graphics.VertexBuffer,
) {
// Update the texture set before preparing the material so uniform samplers can check for that they use different textures
this._updateRenderTargetAndViewport()
Material.from(material).prepare()
// Update the vertex buffer before updating the format so attributes can bind correctly
VertexBuffer.from(vertices).prepare()
this._updateFormat(material.format)
// Draw now that everything is ready
this._updateBlendState()
this._gl.drawArrays(
primitive == Graphics.Primitive.TRIANGLES ? this._gl.TRIANGLES : this._gl.TRIANGLE_STRIP,
0,
Math.floor(vertices.byteCount / material.format.stride),
)
// Forced state updates are done once after a context loss
this._forceStateUpdate = false
}
resize(
widthInPixels: number,
heightInPixels: number,
widthInAppUnits: number,
heightInAppUnits: number,
) {
let canvas = this._gl.canvas as HTMLCanvasElement
const bounds = canvas.getBoundingClientRect()
if (
this._width === widthInAppUnits &&
this._height === heightInPixels &&
bounds.width === widthInAppUnits &&
bounds.height === heightInAppUnits
) {
// Nothing to do here!
return
}
let style = canvas.style
canvas.width = widthInPixels
canvas.height = heightInPixels
style.width = `${widthInAppUnits}px`
style.height = `${heightInAppUnits}px`
this.setViewport(0, 0, widthInPixels, heightInPixels)
this._width = widthInPixels
this._height = heightInPixels
}
clear(color: Graphics.Color) {
this._updateRenderTargetAndViewport()
this._updateBlendState()
if (color != this._currentClearColor) {
this._gl.clearColor(color.redF, color.greenF, color.blueF, color.alphaF)
this._currentClearColor = color
}
this._gl.clear(this._gl.COLOR_BUFFER_BIT)
}
setRenderTarget(renderTarget: Graphics.RenderTarget | null) {
this._currentRenderTarget = RenderTarget.from(renderTarget)
}
createMaterial(
format: Graphics.VertexFormat,
vertexSource: string,
fragmentSource: string,
): Graphics.Material {
let material = new Material(this, format, vertexSource, fragmentSource)
// Compiling shaders is really expensive so we want to get that started
// as early as possible. In Chrome and possibly other browsers, shader
// compilation can happen asynchronously as long as you don't call
// useProgram().
//
// https://plus.google.com/+BrandonJonesToji/posts/4ERHkicC5Ny
//
material.program
return material
}
createVertexBuffer(byteCount: number): Graphics.VertexBuffer {
assert(byteCount > 0 && byteCount % 4 == 0)
return new VertexBuffer(this, byteCount)
}
createTexture(
format: Graphics.TextureFormat,
width: number,
height: number,
pixels?: Uint8Array,
): Graphics.Texture {
return new Texture(this, format, width, height, pixels)
}
createRenderTarget(texture: Graphics.Texture): Graphics.RenderTarget {
return new RenderTarget(this, Texture.from(texture))
}
private ANGLE_instanced_arrays: ANGLE_instanced_arrays | null = null
private ANGLE_instanced_arrays_generation: number = -1
getANGLE_instanced_arrays(): ANGLE_instanced_arrays {
if (this.ANGLE_instanced_arrays_generation !== this._generation) {
this.ANGLE_instanced_arrays = null
}
if (!this.ANGLE_instanced_arrays) {
this.ANGLE_instanced_arrays = this.gl.getExtension('ANGLE_instanced_arrays')
if (!this.ANGLE_instanced_arrays) {
throw new Error('Failed to get extension ANGLE_instanced_arrays')
}
}
return this.ANGLE_instanced_arrays!
}
_updateRenderTargetAndViewport() {
let renderTarget = this._currentRenderTarget
let viewport = renderTarget != null ? renderTarget.viewport : this._defaultViewport
let gl = this._gl
if (this._forceStateUpdate || this._oldRenderTarget != renderTarget) {
gl.bindFramebuffer(gl.FRAMEBUFFER, renderTarget ? renderTarget.framebuffer : null)
this._oldRenderTarget = renderTarget
}
if (this._forceStateUpdate || !this._oldViewport.equals(viewport)) {
gl.viewport(
viewport.x,
this.renderTargetHeightInPixels - viewport.y - viewport.height,
viewport.width,
viewport.height,
)
this._oldViewport.set(viewport.x, viewport.y, viewport.width, viewport.height)
}
}
_updateBlendState() {
if (this._forceStateUpdate || this._oldBlendOperations != this._blendOperations) {
let gl = this._gl
let operations = this._blendOperations
let oldOperations = this._oldBlendOperations
let source = (operations & 0xf) as Graphics.BlendOperation
let target = (operations >> 4) as Graphics.BlendOperation
assert(source in this._blendOperationMap)
assert(target in this._blendOperationMap)
// Special-case the blend mode that just writes over the target buffer
if (operations == Context.COPY_BLEND_OPERATIONS) {
gl.disable(gl.BLEND)
} else {
if (this._forceStateUpdate || oldOperations == Context.COPY_BLEND_OPERATIONS) {
gl.enable(gl.BLEND)
}
// Otherwise, use actual blending
gl.blendFunc(this._blendOperationMap[source], this._blendOperationMap[target])
}
this._oldBlendOperations = operations
}
}
_updateFormat(format: Graphics.VertexFormat) {
// Update the attributes
let gl = this._gl
let attributes = format.attributes
let count = attributes.length
for (let i = 0; i < count; i++) {
let attribute = attributes[i]
let isByte = attribute.type == Graphics.AttributeType.BYTE
gl.vertexAttribPointer(
i,
attribute.count,
isByte ? gl.UNSIGNED_BYTE : gl.FLOAT,
isByte,
format.stride,
attribute.byteOffset,
)
}
// Update the attribute count
while (this._attributeCount < count) {
gl.enableVertexAttribArray(this._attributeCount)
this._attributeCount++
}
while (this._attributeCount > count) {
this._attributeCount--
gl.disableVertexAttribArray(this._attributeCount)
}
this._attributeCount = count
}
getWebGLInfo(): {renderer: string | null; vendor: string | null; version: string | null} {
const ext = this.gl.getExtension('WEBGL_debug_renderer_info')
const renderer = ext ? this.gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) : null
const vendor = ext ? this.gl.getParameter(ext.UNMASKED_VENDOR_WEBGL) : null
const version = this.gl.getParameter(this.gl.VERSION)
return {renderer, vendor, version}
}
static from(context: Graphics.Context): Context {
assert(context == null || context instanceof Context)
return context as Context
}
static _packBlendModes(
source: Graphics.BlendOperation,
target: Graphics.BlendOperation,
): number {
return source | (target << 4)
}
static COPY_BLEND_OPERATIONS = Context._packBlendModes(
Graphics.BlendOperation.ONE,
Graphics.BlendOperation.ZERO,
)
}
abstract class Uniform {
constructor(
protected readonly _material: Material,
protected readonly _name: string,
protected _generation = 0,
protected _location: WebGLUniformLocation | null = null,
protected _isDirty = true,
) {}
// Upload this uniform if it's dirty
abstract prepare(): void
get location(): WebGLUniformLocation {
let context = Context.from(this._material.context)
if (this._generation != context.generation) {
this._location = context.gl.getUniformLocation(this._material.program, this._name)
this._generation = context.generation
// Validate the shader against this uniform
if (!RELEASE) {
let program = this._material.program
let gl = context.gl
for (let i = 0, ii = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); i < ii; i++) {
let info = gl.getActiveUniform(program, i)
if (info && info.name == this._name) {
assert(info.size == 1)
switch (info.type) {
case gl.FLOAT: {
assert(this instanceof UniformFloat)
break
}
case gl.FLOAT_MAT3: {
assert(this instanceof UniformMat3)
break
}
case gl.FLOAT_VEC2: {
assert(this instanceof UniformVec2)
break
}
case gl.FLOAT_VEC3: {
assert(this instanceof UniformVec3)
break
}
case gl.FLOAT_VEC4: {
assert(this instanceof UniformVec4)
break
}
case gl.INT: {
assert(this instanceof UniformInt)
break
}
case gl.SAMPLER_2D: {
assert(this instanceof UniformSampler)
break
}
default:
assert(false)
}
}
}
}
}
if (!this._location) {
throw new Error('Failed to get uniform location')
}
return this._location
}
}
class UniformFloat extends Uniform {
private _x = 0.0
set(x: number) {
if (x != this._x) {
this._x = x
this._isDirty = true
}
}
prepare() {
let context = Context.from(this._material.context)
if (this._generation != context.generation || this._isDirty) {
context.gl.uniform1f(this.location, this._x)
this._isDirty = false
}
}
}
class UniformInt extends Uniform {
private _x = 0
set(x: number) {
if (x != this._x) {
this._x = x
this._isDirty = true
}
}
prepare() {
let context = Context.from(this._material.context)
if (this._generation != context.generation || this._isDirty) {
context.gl.uniform1i(this.location, this._x)
this._isDirty = false
}
}
}
class UniformVec2 extends Uniform {
private _x = 0.0
private _y = 0.0
set(x: number, y: number) {
if (x != this._x || y != this._y) {
this._x = x
this._y = y
this._isDirty = true
}
}
prepare() {
let context = Context.from(this._material.context)
if (this._generation != context.generation || this._isDirty) {
context.gl.uniform2f(this.location, this._x, this._y)
this._isDirty = false
}
}
}
class UniformVec3 extends Uniform {
private _x = 0.0
private _y = 0.0
private _z = 0.0
set(x: number, y: number, z: number) {
if (x != this._x || y != this._y || z != this._z) {
this._x = x
this._y = y
this._z = z
this._isDirty = true
}
}
prepare() {
let context = Context.from(this._material.context)
if (this._generation != context.generation || this._isDirty) {
context.gl.uniform3f(this.location, this._x, this._y, this._z)
this._isDirty = false
}
}
}
class UniformVec4 extends Uniform {
private _x = 0.0
private _y = 0.0
private _z = 0.0
private _w = 0.0
set(x: number, y: number, z: number, w: number) {
if (x != this._x || y != this._y || z != this._z || w != this._w) {
this._x = x
this._y = y
this._z = z
this._w = w
this._isDirty = true
}
}
prepare() {
let context = Context.from(this._material.context)
if (this._generation != context.generation || this._isDirty) {
context.gl.uniform4f(this.location, this._x, this._y, this._z, this._w)
this._isDirty = false
}
}
}
class UniformMat3 extends Uniform {
private _values = [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]
set(
m00: number,
m01: number,
m02: number,
m10: number,
m11: number,
m12: number,
m20: number,
m21: number,
m22: number,
) {
// These values are deliberately transposed because WebGL requires the "transpose" argument of uniformMatrix() to be false
UniformMat3._cachedValues[0] = m00
UniformMat3._cachedValues[1] = m10
UniformMat3._cachedValues[2] = m20
UniformMat3._cachedValues[3] = m01
UniformMat3._cachedValues[4] = m11
UniformMat3._cachedValues[5] = m21
UniformMat3._cachedValues[6] = m02
UniformMat3._cachedValues[7] = m12
UniformMat3._cachedValues[8] = m22
for (let i = 0; i < 9; i++) {
if (UniformMat3._cachedValues[i] != this._values[i]) {
let swap = this._values
this._values = UniformMat3._cachedValues
UniformMat3._cachedValues = swap
this._isDirty = true
break
}
}
}
prepare() {
let context = Context.from(this._material.context)
if (this._generation != context.generation || this._isDirty) {
context.gl.uniformMatrix3fv(this.location, false, this._values)
this._isDirty = false
}
}
// Statically allocate this to avoid allocations
static _cachedValues = [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]
}
class UniformSampler extends Uniform {
private _texture: Texture | null = null
private _index = -1
set(texture: Graphics.Texture, index: number) {
if (this._texture != texture || this._index != index) {
this._texture = Texture.from(texture)
this._index = index
this._isDirty = true
}
}
prepare() {
let context = Context.from(this._material.context)
let gl = context.gl
assert(
this._texture == null ||
context.currentRenderTarget == null ||
this._texture != context.currentRenderTarget.texture,
)
if (this._generation != context.generation || this._isDirty) {
gl.uniform1i(this.location, this._index)
this._isDirty = false
}
gl.activeTexture(TEXTURE_N(gl, this._index))
gl.bindTexture(
gl.TEXTURE_2D,
this._texture != null && this._texture.width > 0 && this._texture.height > 0
? this._texture.texture
: null,
)
}
}
class Material implements Graphics.Material {
constructor(
private readonly _context: Context,
private readonly _format: Graphics.VertexFormat,
private readonly _vertexSource: string,
private readonly _fragmentSource: string,
private readonly _uniformsMap: {[key: string]: Uniform} = {},
private readonly _uniformsList: Uniform[] = [],
private _generation = 0,
private _program: WebGLProgram | null = null,
) {}
get context() {
return this._context
}
get format() {
return this._format
}
get vertexSource() {
return this._vertexSource
}
get fragmentSource() {
return this._fragmentSource
}
setUniformFloat(name: string, x: number) {
let uniform = this._uniformsMap[name] || null
if (uniform == null) {
uniform = new UniformFloat(this, name)
this._uniformsMap[name] = uniform
this._uniformsList.push(uniform)
}
assert(uniform instanceof UniformFloat)
;(uniform as UniformFloat).set(x)
}
setUniformInt(name: string, x: number) {
let uniform = this._uniformsMap[name] || null
if (uniform == null) {
uniform = new UniformInt(this, name)
this._uniformsMap[name] = uniform
this._uniformsList.push(uniform)
}
assert(uniform instanceof UniformInt)
;(uniform as UniformInt).set(x)
}
setUniformVec2(name: string, x: number, y: number) {
let uniform = this._uniformsMap[name] || null
if (uniform == null) {
uniform = new UniformVec2(this, name)
this._uniformsMap[name] = uniform
this._uniformsList.push(uniform)
}
assert(uniform instanceof UniformVec2)
;(uniform as UniformVec2).set(x, y)
}
setUniformVec3(name: string, x: number, y: number, z: number) {
let uniform = this._uniformsMap[name] || null
if (uniform == null) {
uniform = new UniformVec3(this, name)
this._uniformsMap[name] = uniform
this._uniformsList.push(uniform)
}
assert(uniform instanceof UniformVec3)
;(uniform as UniformVec3).set(x, y, z)
}
setUniformVec4(name: string, x: number, y: number, z: number, w: number) {
let uniform = this._uniformsMap[name] || null
if (uniform == null) {
uniform = new UniformVec4(this, name)
this._uniformsMap[name] = uniform
this._uniformsList.push(uniform)
}
assert(uniform instanceof UniformVec4)
;(uniform as UniformVec4).set(x, y, z, w)
}
setUniformMat3(
name: string,
m00: number,
m01: number,
m02: number,
m10: number,
m11: number,
m12: number,
m20: number,
m21: number,
m22: number,
) {
let uniform = this._uniformsMap[name] || null
if (uniform == null) {
uniform = new UniformMat3(this, name)
this._uniformsMap[name] = uniform
this._uniformsList.push(uniform)
}
assert(uniform instanceof UniformMat3)
;(uniform as UniformMat3).set(m00, m01, m02, m10, m11, m12, m20, m21, m22)
}
setUniformSampler(name: string, texture: Graphics.Texture, index: number) {
let uniform = this._uniformsMap[name] || null
if (uniform == null) {
uniform = new UniformSampler(this, name)
this._uniformsMap[name] = uniform
this._uniformsList.push(uniform)
}
assert(uniform instanceof UniformSampler)
;(uniform as UniformSampler).set(texture, index)
}
get program(): WebGLProgram {
let gl = this._context.gl
if (this._generation != this._context.generation) {
this._program = gl.createProgram()!
this._compileShader(gl, gl.VERTEX_SHADER, this.vertexSource)
this._compileShader(gl, gl.FRAGMENT_SHADER, this.fragmentSource)
let attributes = this.format.attributes
for (let i = 0; i < attributes.length; i++) {
gl.bindAttribLocation(this._program, i, attributes[i].name)
}
gl.linkProgram(this._program)
if (!gl.getProgramParameter(this._program, gl.LINK_STATUS)) {
throw new Error(`${gl.getProgramInfoLog(this._program)}`)
}
this._generation = this._context.generation
// Validate this shader against the format
if (!RELEASE) {
for (let attribute of attributes) {
for (
let i = 0, ii = gl.getProgramParameter(this.program, gl.ACTIVE_ATTRIBUTES);
i < ii;
i++
) {
let info = gl.getActiveAttrib(this.program, i)
if (info && info.name == attribute.name) {
assert(info.size == 1)
switch (attribute.count) {
case 1: {
assert(info.type == gl.FLOAT)
break
}
case 2: {
assert(info.type == gl.FLOAT_VEC2)
break
}
case 3: {
assert(info.type == gl.FLOAT_VEC3)
break
}
case 4: {
assert(info.type == gl.FLOAT_VEC4)
break
}
default: {
assert(false)
}
}
}
}
}
}
}
return this._program!
}
prepare(): void {
this._context.gl.useProgram(this.program)
for (let uniform of this._uniformsList) {
uniform.prepare()
}
}
_compileShader(gl: WebGLRenderingContext, type: GLenum, source: string) {
let shader = gl.createShader(type)
if (!shader) {
throw new Error('Failed to create shader')
}
gl.shaderSource(shader, source)
gl.compileShader(shader)
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
throw new Error(`${gl.getShaderInfoLog(shader)}`)
}
if (!this._program) {
throw new Error('Tried to attach shader before program was created')
}
gl.attachShader(this._program, shader)
}
static from(material: Graphics.Material): Material {
assert(material == null || material instanceof Material)
return material as Material
}
}
class VertexBuffer extends Graphics.VertexBuffer {
private readonly _context: Context
private _generation = 0
private _buffer: WebGLBuffer | null = null
private _bytes: Uint8Array | null = null
private _isDirty = true
private _dirtyMin = VertexBuffer.INT_MAX
private _dirtyMax = 0
private _totalMin = VertexBuffer.INT_MAX
private _totalMax = 0
private _byteCount = 0
constructor(context: Context, byteCount: number) {
super()
this._context = context
this._byteCount = byteCount
this._bytes = new Uint8Array(byteCount)
}
get context() {
return this._context
}
get byteCount() {
return this._byteCount
}
move(sourceByteOffset: number, targetByteOffset: number, byteCount: number) {
assert(byteCount >= 0)
assert(0 <= sourceByteOffset && sourceByteOffset + byteCount <= this._byteCount)
assert(0 <= targetByteOffset && targetByteOffset + byteCount <= this._byteCount)
if (this._bytes && sourceByteOffset != targetByteOffset && byteCount != 0) {
this._bytes.set(this._bytes.subarray(sourceByteOffset, this._byteCount), targetByteOffset)
this._growDirtyRegion(
Math.min(sourceByteOffset, targetByteOffset),
Math.max(sourceByteOffset, targetByteOffset) + byteCount,
)
}
}
upload(bytes: Uint8Array, byteOffset: number = 0) {
assert(0 <= byteOffset && byteOffset + bytes.length <= this._byteCount)
assert(this._bytes != null)
this._bytes!.set(bytes, byteOffset)
this._growDirtyRegion(byteOffset, byteOffset + bytes.length)
}
free() {
if (this._buffer) {
this._context.gl.deleteBuffer(this._buffer)
}
// Reset the generation to force this to be re-uploaded if it's used again
// in the future.
this._generation = 0
}
prepare(): void {
let gl = this._context.gl
if (this._generation !== this._context.generation) {
this._buffer = gl.createBuffer()
this._generation = this._context.generation
this._isDirty = true
}
gl.bindBuffer(gl.ARRAY_BUFFER, this._buffer)
if (this._isDirty) {
gl.bufferData(gl.ARRAY_BUFFER, this._byteCount, gl.DYNAMIC_DRAW)
this._dirtyMin = this._totalMin
this._dirtyMax = this._totalMax
this._isDirty = false
}
if (this._dirtyMin < this._dirtyMax) {
gl.bufferSubData(
gl.ARRAY_BUFFER,
this._dirtyMin,
this._bytes!.subarray(this._dirtyMin, this._dirtyMax),
)
this._dirtyMin = VertexBuffer.INT_MAX
this._dirtyMax = 0
}
}
_growDirtyRegion(min: number, max: number) {
this._dirtyMin = Math.min(this._dirtyMin, min)
this._dirtyMax = Math.max(this._dirtyMax, max)
this._totalMin = Math.min(this._totalMin, min)
this._totalMax = Math.max(this._totalMax, max)
}
static INT_MAX = 0x7fffffff
static from(buffer: Graphics.VertexBuffer): VertexBuffer {
assert(buffer == null || buffer instanceof VertexBuffer)
return buffer as VertexBuffer
}
}
class Texture implements Graphics.Texture {
constructor(
private readonly _context: Context,
private _format: Graphics.TextureFormat,
private _width: number,
private _height: number,
private _pixels: Uint8Array | null = null,
private _texture: WebGLTexture | null = null,
private _generation = 0,
private _isFormatDirty = true,
private _isContentDirty = true,
) {}
get context() {
return this._context
}
get format() {
return this._format
}
get width() {
return this._width
}
get height() {
return this._height
}
resize(width: number, height: number, pixels: Uint8Array | null = null) {
this._width = width
this._height = height
this._pixels = pixels
this._isContentDirty = true
}
setFormat(format: Graphics.TextureFormat) {
if (this._format != format) {
this._format = format
this._isFormatDirty = true
}
}
get texture(): WebGLTexture {
let gl = this._context.gl
// Create
if (this._generation != this._context.generation) {
this._texture = gl.createTexture()
this._generation = this._context.generation
this._isFormatDirty = true
this._isContentDirty = true
}
// Format
if (this._isFormatDirty) {
gl.bindTexture(gl.TEXTURE_2D, this._texture)
gl.texParameteri(
gl.TEXTURE_2D,
gl.TEXTURE_MAG_FILTER,
this.format.magFilter == Graphics.PixelFilter.NEAREST ? gl.NEAREST : gl.LINEAR,
)
gl.texParameteri(
gl.TEXTURE_2D,
gl.TEXTURE_MIN_FILTER,
this.format.minFilter == Graphics.PixelFilter.NEAREST ? gl.NEAREST : gl.LINEAR,
)
gl.texParameteri(
gl.TEXTURE_2D,
gl.TEXTURE_WRAP_S,
this.format.wrap == Graphics.PixelWrap.REPEAT ? gl.REPEAT : gl.CLAMP_TO_EDGE,
)
gl.texParameteri(
gl.TEXTURE_2D,
gl.TEXTURE_WRAP_T,
this.format.wrap == Graphics.PixelWrap.REPEAT ? gl.REPEAT : gl.CLAMP_TO_EDGE,
)
this._isFormatDirty = false
}
if (this._isContentDirty) {
gl.bindTexture(gl.TEXTURE_2D, this._texture)
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
this._width,
this._height,
0,
gl.RGBA,
gl.UNSIGNED_BYTE,
this._pixels,
)
this._isContentDirty = false
}
return this._texture!
}
free() {
if (this.texture) {
this._context.gl.deleteTexture(this.texture)
this._generation = 0
}
}
static from(texture: Graphics.Texture): Texture {
assert(texture == null || texture instanceof Texture)
return texture as Texture
}
}
class RenderTarget implements Graphics.RenderTarget {
constructor(
private _context: Context,
private _texture: Texture,
private _framebuffer: WebGLFramebuffer | null = null,
private _generation = 0,
private _isDirty = true,
private _viewport = new Graphics.Rect(),
) {}
get context() {
return this._context
}
get texture() {
return this._texture
}
get viewport() {
return this._viewport
}
setColor(texture: Graphics.Texture) {
if (this._texture != texture) {
this._texture = Texture.from(texture)
this._isDirty = true
}
}
get framebuffer(): WebGLFramebuffer {
let gl = this._context.gl
let texture = this._texture.texture
// Create
if (this._generation != this._context.generation) {
this._framebuffer = gl.createFramebuffer()
this._generation = this._context.generation
this._isDirty = true
}
// Update
if (this._isDirty) {
gl.bindFramebuffer(gl.FRAMEBUFFER, this._framebuffer)
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0)
assert(gl.checkFramebufferStatus(gl.FRAMEBUFFER) == gl.FRAMEBUFFER_COMPLETE)
this._isDirty = false
}
return this._framebuffer!
}
free() {
if (this._framebuffer) {
this._context.gl.deleteFramebuffer(this._framebuffer)
this._generation = 0
}
}
static from(renderTarget: Graphics.RenderTarget | null): RenderTarget | null {
assert(renderTarget == null || renderTarget instanceof RenderTarget)
return renderTarget as RenderTarget | null
}
}
}