
const TYPES = {
	U8:        1,
	I8:        2,
	U16:       3,
	I16:       4,
	U32:       5,
	I32:       6,
	U64:       7,
	I64:       8,
	SizeT:     9,
	F32:       10,
	F64:       11,
	Bool:      12,
	String:    13,
	U8Array:   14,
	I8Array:   15,
	U16Array:  16,
	I16Array:  17,
	U32Array:  18,
	I32Array:  19,
	U64Array:  20,
	I64Array:  21,
	F32Array:  22,
	F64Array:  23,
	Char:      24,
	CharArray: 25,
};

let LoadSize  = null;
let StoreSize = null;

function assert(condition, message)
{
	if(!condition)
	{
		console.assert(condition, message);
		throw "Assert failure";
	}
}

let MEMORY        = null;
let DATA_VIEW     = null;
let GL            = null;
let STDOUT_STRING = "";
let SPRINF_VALUES = [];
let PTR_SIZE      = 4;
let GL_ELEMENT    = null;
let GL_VERSION    = 1.0;

let GL_LAST_ERROR      = 0;
let GL_CURRENT_ID      = 1;
let BUFFERS            = new Map([0, null]);
let PROGRAMS           = new Map([0, null]);
let FRAMEBUFFERS       = new Map([0, null]);
let RENDERBUFFERS      = new Map([0, null]);
let TEXTURES           = new Map([0, null]);
let UNIFORMS           = new Map([0, null]);
let SHADERS            = new Map([0, null]);
let VAOS               = new Map([0, null]);
let QUERIES            = new Map([0, null]);
let SAMPLERS           = new Map([0, null]);
let TRANSFORM_FEEDBACK = new Map([0, null]);
let SYNCS              = new Map([0, null]);
let PROGRAM_INFO       = new Map([0, null]);

function InitWasmManager(memory, ptr_size = 4)
{
	PTR_SIZE = ptr_size;

	SPRINTF_VALUES = [];

	STDOUT_STRING = "";

	switch(PTR_SIZE)
	{
		case 8:  LoadSize = LoadU64; StoreSize = StoreU64; break;
		case 4:
		default: LoadSize = LoadU32; StoreSize = StoreU32; break;
	}
}

function SetMemory(memory)
{
	MEMORY    = memory;
	DATA_VIEW = new DataView(MEMORY.buffer);
}

function Memory()
{
	return new DataView(MEMORY.buffer);
}

function GLRemoveItem(map, id)
{
	map.delete(id);
}

function GLGetItem(map, id)
{
	return map.has(id) ? map.get(id) : null;
}

function GLAddItem(map, item)
{
	map.set(GL_CURRENT_ID, item);
	return GL_CURRENT_ID++;
}

function SetGLError(error)
{
	if(!GL_LAST_ERROR) GL_LAST_ERROR = error;
}

function SetGLContext(element, settings)
{
	if(!element)                           return false;
	if(GL_ELEMENT == element) return true;

	settings      ??= {};
	GL = element.getContext("webgl2", settings) || element.getContext("webgl", settings);
	if(GL)
	{
		GL_ELEMENT = element;
		GL_VERSION = GL.getParameter(0x1F02).indexOf("WebGL 2.0") !== -1 ? 2.0 : 1.0;
	}

	return !!GL;
}

function AssertWebGL2()
{
	if(GL_VERSION < 2.0)
	{
		throw new Error("WebGL2 function called without a WebGL2 enabled context");
	}
}

function PopulateUniformTable(program) 
{
	let p = GLGetItem(PROGRAMS, program);
	PROGRAM_INFO[program] = {
		uniforms:                  {},
		maxUniformLength:          0,
		maxAttributeLength:        -1,
		maxUniformBlockNameLength: -1,
	};
	
	let ptable      = PROGRAM_INFO[program];
	let utable      = ptable.uniforms;
	let numUniforms = GL.getProgramParameter(p, GL.ACTIVE_UNIFORMS);

	for(i = 0; i < numUniforms; ++i) 
	{
		let u    = GL.getActiveUniform(p, i);
		let name = u.name;
		if(ptable.maxUniformLength = Math.max(ptable.maxUniformLength, name.length + 1), name.indexOf("]", name.length - 1) !== -1) 
		{
			name = name.slice(0, name.lastIndexOf("["));
		}

		let loc = GL.getUniformLocation(p, name);
		if(loc !== null) 
		{
			let id = GLAddItem(UNIFORMS, loc);
			utable[name] = [u.size, id];
			for(let j = 1; j < u.size; ++j) 
			{
				let n   = name + "[" + j + "]";
				let loc = GL.getUniformLocation(p, n);
				GLAddItem(UNIFORMS, loc);
			}
		}
	}
}

function LoadBool(addr) { return Boolean(Memory().getUint8(addr, true)); }
function LoadF32(addr)  { return Memory().getFloat32(addr, true); }
function LoadF64(addr)  { return Memory().getFloat64(addr, true); }
function LoadU8 (addr)  { return Memory().getUint8  (addr, true); }
function LoadI8 (addr)  { return Memory().getInt8   (addr, true); }
function LoadU16(addr)  { return Memory().getUint16 (addr, true); }
function LoadI16(addr)  { return Memory().getInt16  (addr, true); }
function LoadU32(addr)  { return Memory().getUint32 (addr, true); }
function LoadI32(addr)  { return Memory().getInt32  (addr, true); }

function LoadU64(addr)
{
	const lo = Memory().getUint32(addr+0, true);
	const hi = Memory().getUint32(addr+4, true);
	return lo + hi*4294967296;
}

function LoadI64(addr)
{
	const lo = Memory().getUint32(addr+0, true);
	const hi = Memory().getInt32 (addr+4, true);
	return lo + hi*4294967296;
}

function LoadArray(array_class, length, addr) 
{
	return new array_class(MEMORY.buffer, addr, length);
}

function LoadString(length, addr)
{
	return new TextDecoder().decode(LoadArray(Uint8Array, length, addr));
}

function LoadCString(ptr)
{
	if(!ptr) return null;

	let length = 0;
	for(; LoadU8(ptr+length); length += 1) {}

	return LoadString(ptr, length);
}

function StoreString(ptr, value)
{
	const src = new TextEncoder().encode(value);
	const dst = new Uint8Array(MEMORY.buffer, ptr, src.length);

	dst.set(src);

	return src.length;
}

function StoreU8(addr, value) { Memory().setUint8(addr, value, true); }
function StoreI8(addr, value) { Memory().setInt8 (addr, value, true); }
function StoreU16(addr, value) { Memory().setUint16(addr, value, true); }
function StoreI16(addr, value) { Memory().setInt16(addr, value, true); }
function StoreU32(addr, value) { Memory().setUint32(addr, value, true); }
function StoreI32(addr, value) { Memory().setInt32(addr, value, true); }

function StoreU64(addr, value)
{
	let div = 4294967296;
	if(typeof value === 'bigint')
	{
		div = BigInt(div);
	}

	StoreU32(addr+0, Number(value),                 true);
	StoreU32(addr+4, Math.floor(Number(value/div)), true);
}

function StoreI64(addr, value)
{
	let div = 4294967296;
	if(typeof value === 'bigint')
	{
		div = BigInt(div);
	}

	StoreI32(addr+0, Number(value),                 true);
	StoreI32(addr+4, Math.floor(Number(value/div)), true);
}

function StoreF32(addr, value) { Memory().setFloat32(addr, value, true); }
function StoreF64(addr, value) { Memory().setFloat64(addr, value, true); }

const imports = {
	env: {
		Abort(string_length, string_ptr) 
		{
			console.error(LoadString(string_length, string_ptr));
		},
		SprintfLoadValue(ptr, type)
		{
			let value = null;
			switch(type)
			{
				case TYPES.U8:    value = LoadU8(ptr);        break;
				case TYPES.I8:    value = LoadI8(ptr);        break;
				case TYPES.U16:   value = LoadU16(ptr);       break;
				case TYPES.I16:   value = LoadI16(ptr);       break;
				case TYPES.U32:   value = LoadU32(ptr);       break;
				case TYPES.I32:   value = LoadI32(ptr);       break;
				case TYPES.U64:   value = LoadU64(ptr);       break;
				case TYPES.I64:   value = LoadI64(ptr);       break;
				case TYPES.SizeT: value = LoadSize(ptr);      break;
				case TYPES.F32:   value = LoadF32(ptr);       break;
				case TYPES.F64:   value = LoadF64(ptr);       break;
				case TYPES.Bool:  value = LoadBool(ptr);      break;
				case TYPES.Char:  value = LoadString(1, ptr); break;
				default: break;
			}

			if(value != null)
			{
				if(type === TYPES.Bool)
				{
					SPRINTF_VALUES.push(value ? 'true' : 'false');
				}
				else
				{
					SPRINTF_VALUES.push(''+value);
				}
			}
		},
		SprintfLoadArray(length, ptr, type)
		{
			let value = null;
			switch(type)
			{
				case TYPES.CharArray:
				case TYPES.String:    value = LoadString(length, ptr);                break;
				case TYPES.U8Array:   value = LoadArray(Uint8Array,     length, ptr); break;
				case TYPES.I8Array:   value = LoadArray(Int8Array,      length, ptr); break;
				case TYPES.U16Array:  value = LoadArray(Uint16Array,    length, ptr); break;
				case TYPES.I16Array:  value = LoadArray(Int16Array,     length, ptr); break;
				case TYPES.U32Array:  value = LoadArray(Uint32Array,    length, ptr); break;
				case TYPES.I32Array:  value = LoadArray(Int32Array,     length, ptr); break;
				case TYPES.U64Array:  value = LoadArray(BigUint64Array, length, ptr); break;
				case TYPES.I64Array:  value = LoadArray(BigInt64Array,  length, ptr); break;
				case TYPES.F32Array:  value = LoadArray(Float32Array,   length, ptr); break;
				case TYPES.F64Array:  value = LoadArray(Float64Array,   length, ptr); break;
			}

			if(value)
			{
				if(type !== TYPES.String && type !== TYPES.CharArray)
				{
					value = `[${value.join(", ")}]`;
				}

				SPRINTF_VALUES.push(value);
			}
		},
		SprintfEnd(buffer_length, buffer_ptr, string_length, string_ptr)
		{
			let format_string = LoadString(string_length, string_ptr);

			let index  = format_string.indexOf("%");

			let result = format_string.substring(0, index);
			let format = format_string.substring(index);

			function Next()
			{
				index   = format.indexOf("%");
				result += format.substring(0, index);
				format  = format.substring(index);
			}

			let value_index = 0;
			for(; value_index < SPRINTF_VALUES.length;)
			{
				assert(format.length);

				const char = format.charAt(1);
				assert(char !== " " && char.length, "Invalid format string " + format_string);

				if(char === "%")
				{
					result += "%";
					format = format.substring(2);
					Next();
					continue;
				}
				else if(char >= '0' && char <= '9')
				{
					let digit_index = 2;
					for(;;)
					{
						let next_char = format.charAt(digit_index++);

						assert(next_char !== " " && next_char.length, "Invalid format string " + format_string);

						if(next_char < '0' || next_char > '9')
						{
							continue;
						}

						result += SPRINTF_VALUES[value_index++];
						format  = format.substring(digit_index);
						Next();

						break;
					}
				}
				else if((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z'))
				{
					result += SPRINTF_VALUES[value_index++];
					format  = format.substring(2);
					Next();
				}
				else assert(false, "Invalid format string " + format_string);
			}

			result += format;

			const bytes_written = StoreString(buffer_ptr, result);
			assert(bytes_written <= buffer_length, "Format string is longer than buffer length");

			SPRINTF_VALUES = [];

			return bytes_written;
		},
		Console(string_length, string_ptr, write_line)
		{
			STDOUT_STRING += LoadString(string_length, string_ptr);
			const arr = LoadArray(Uint8Array, string_length, string_ptr);

			if(write_line || STDOUT_STRING.includes("\n"))
			{
				console.log(STDOUT_STRING);
				STDOUT_STRING = "";
			}
		},
		Console2(string_length, string_ptr, write_line)
		{
			STDOUT_STRING += LoadString(string_length, string_ptr);
			const arr = LoadArray(Uint8Array, string_length, string_ptr);

			if(write_line || STDOUT_STRING.includes("\n"))
			{
				console.log(STDOUT_STRING);
				STDOUT_STRING = "";
			}
		},
		pow:  Math.pow,
		cos:  Math.cos,
		acos: Math.acos,
	},
	gl: {
		SetCurrentContextById: (name_length, name_ptr) => 
		{
			let name    = LoadString(name_length, name_ptr);
			let element = getElement(name);
			return SetGLContext(element, {alpha: true, antialias: true, depth: true, premultipliedAlpha: true});
		},
		CreateCurrentContextById: (name_length, name_ptr, attributes) => 
		{
			let name    = LoadString(name_length, name_ptr);
			let element = getElement(name);

			let contextSettings = {
				alpha:                        ! (attributes & (1<<0)),
				antialias:                    ! (attributes & (1<<1)),
				depth:                        ! (attributes & (1<<2)),
				failIfMajorPerformanceCaveat: !!(attributes & (1<<3)),
				premultipliedAlpha:           ! (attributes & (1<<4)),
				preserveDrawingBuffer:        !!(attributes & (1<<5)),
				stencil:                      !!(attributes & (1<<6)),
				desynchronized:               !!(attributes & (1<<7)),
			};

			return SetGLContext(element, contextSettings);
		},
		Clear: (x) => {
			GL.clear(x);
		},
		ClearColor: (r, g, b, a) => {
			GL.clearColor(r, g, b, a);
		},
		ClearDepth: (x) => {
			GL.clearDepth(x);
		},
		ClearStencil: (x) => {
			GL.clearStencil(x);
		},
		ColorMask: (r, g, b, a) => {
			GL.colorMask(!!r, !!g, !!b, !!a);
		},
		CompileShader: (shader) => {
			if(shader)
			{
				GL.compileShader(GLGetItem(SHADERS, shader));
			}
		},
		UseProgram: (program) => {
			if(program)
			{
				GL.useProgram(GLGetItem(PROGRAMS, program));
			}
		},
		ValidateProgram: (program) => {
			if(program)
			{
				GL.validateProgram(GLGetItem(PROGRAMS, program));
			}
		},
		BindBufferBase: (target, index, buffer) => {
			AssertWebGL2();
			GL.bindBufferBase(target, index, GLGetItem(BUFFERS, buffer));
		},
		GetUniformBlockIndex: (program, uniformBlockName_ptr) => {
			AssertWebGL2();
			return GL.getUniformBlockIndex(GLGetItem(PROGRAMS, program), LoadCString(uniformBlockName_ptr));
		},
		GenTextures: (count, texture_ptr) => 
		{
			for(let i = 0; i < count; i += 1)
			{
				const id = GLAddItem(TEXTURES, GL.createTexture());
				StoreU32(texture_ptr+4*i, id);
			}
		},
		CullFace: (mode) => 
		{
			GL.cullFace(mode);
		},
		GenBuffers: (count, buffer_ptr) => 
		{
			for(let i = 0; i < count; i += 1)
			{
				const id = GLAddItem(BUFFERS, GL.createBuffer());
				StoreU32(buffer_ptr+4*i, id);
			}
		},
		Enable: (cap) => 
		{
			GL.enable(cap);
		},
		EnableVertexAttribArray: (index) => 
		{
			GL.enableVertexAttribArray(index);
		},
		Disable: (cap) => 
		{
			GL.disable(cap);
		},
		DisableVertexAttribArray: (index) => 
		{
			GL.disableVertexAttribArray(index);
		},
		FrontFace: (mode) => 
		{
			GL.frontFace(mode);
		},
		BlendFuncSeparate: (srcRGB, dstRGB, srcAlpha, dstAlpha) => 
		{
			GL.blendFuncSeparate(srcRGB, dstRGB, srcAlpha, dstAlpha);
		},
		BindTexture: (target, texture) => 
		{
			GL.bindTexture(target, GLGetItem(TEXTURES, texture));
		},
		TexParameterf: (target, pname, param) => 
		{
			GL.texParameterf(target, pname, param);
		},
		TexParameteri: (target, pname, param) => 
		{
			GL.texParameteri(target, pname, param);
		},
		TexImage2D: (target, level, internalformat, width, height, border, format, type, size, data) => 
		{
			let buffer = null;

			if(data)
			{
				switch(internalformat)
				{
					case GL.RGBA32F:  
					case GL.RGB32F:
					case GL.RG32F:
					case GL.R32F:     buffer = LoadArray(Float32Array, size, data); break;
					case GL.RGBA32UI: 
					case GL.RGB32UI: 
					case GL.RG32UI: 
					case GL.R32UI:    buffer = LoadArray(Uint32Array, size, data);  break;
					case GL.RGBA32I:  
					case GL.RGB32I:  
					case GL.RG32I:  
					case GL.R32I:     buffer = LoadArray(Int32Array, size, data);   break;
					default:          buffer = LoadArray(Uint8Array, size, data);   break;
				}
			}

			GL.texImage2D(target, level, internalformat, width, height, border, format, type, buffer);
		},
		GenVertexArrays: (count, vao_ptr) => 
		{
			AssertWebGL2();
			for(let i = 0; i < count; i += 1)
			{
				const id = GLAddItem(VAOS, GL.createVertexArray());
				StoreU32(vao_ptr+4*i, id);
			}
		},
		DeleteVertexArrays: (count, vao_ptr) => 
		{
			AssertWebGL2();

			const vaos = LoadArray(Uint32Array, count, vao_ptr);
			for(let i = 0; i < vaos.length; i += 1)
			{
				const id = vaos[i];
				let obj  = GLGetItem(VAOS, id);
				if(obj) 
				{
					GL.deleteVertexArray(obj);
					GLRemoveItem(VAOS, id);
				}
			}
		},
		BindVertexArray: (vertexArray) => 
		{
			AssertWebGL2();
			GL.bindVertexArray(GLGetItem(VAOS, vertexArray));
		},
		BlendFunc: (sfactor, dfactor) => 
		{
			GL.blendFunc(sfactor, dfactor);
		},
		BindBuffer: (target, buffer) => 
		{
			let bufferObj = GLGetItem(BUFFERS, buffer);
			if(target == 35051) 
			{
				GL.currentPixelPackBufferBinding = buffer;
			} 
			else 
			{
				if(target == 35052) 
				{
					GL.currentPixelUnpackBufferBinding = buffer;
				}
				GL.bindBuffer(target, bufferObj)
			}
		},
		BufferData: (target, size, data, usage) => 
		{
			if(data) 
			{
				GL.bufferData(target, LoadArray(Uint8Array, size, data), usage);
			} 
			else 
			{
				GL.bufferData(target, size, usage);
			}
		},
		BufferSubData: (target, offset, size, data) => 
		{
			if(data) 
			{
				GL.bufferSubData(target, offset, LoadArray(Uint8Array, size, data));
			}
			else 
			{
				GL.bufferSubData(target, offset, null);
			}
		},
		VertexAttribDivisor: (index, divisor) => 
		{
			AssertWebGL2();
			GL.vertexAttribDivisor(index, divisor);
		},
		VertexAttribPointer: (index, size, type, normalized, stride, ptr) => 
		{
			GL.vertexAttribPointer(index, size, type, !!normalized, stride, ptr);
		},
		Viewport: (x, y, w, h) => 
		{
			if(GLELEMENT)
			{
				GLELEMENT.width  = w;
				GLELEMENT.height = h;
			}

			GL.viewport(x, y, w, h);
		},
		DrawArraysInstanced: (mode, first, count, instanceCount) => 
		{
			AssertWebGL2();
			GL.drawArraysInstanced(mode, first, count, instanceCount);
		},
		DrawElementsInstanced: (mode, count, type, offset, instanceCount) => 
		{
			AssertWebGL2();
			GL.drawElementsInstanced(mode, count, type, offset, instanceCount);
		},
		ActiveTexture: (x) => 
		{
			GL.activeTexture(x);
		},
		GetUniformLocation: (program, name_ptr) => 
		{
			let name        = LoadCString(name_ptr);
			let arrayOffset = 0;
			if(name.indexOf("]", name.length - 1) !== -1) 
			{
				let ls         = name.lastIndexOf("[");
				let	arrayIndex = name.slice(ls + 1, -1);
				if(arrayIndex.length > 0 && (arrayOffset = parseInt(arrayIndex)) < 0) 
				{
					return -1;
				}

				name = name.slice(0, ls)
			}

			let ptable = PROGRAM_INFO[program];
			if(!ptable) 
			{
				return -1;
			}
			var uniformInfo = ptable.uniforms[name];
			return (uniformInfo && arrayOffset < uniformInfo[0]) ? uniformInfo[1] + arrayOffset : -1
		},
		Uniform1f: (location, v0)             => { GL.uniform1f(GLGetItem(UNIFORMS, location), v0);             },
		Uniform2f: (location, v0, v1)         => { GL.uniform2f(GLGetItem(UNIFORMS, location), v0, v1);         },
		Uniform3f: (location, v0, v1, v2)     => { GL.uniform3f(GLGetItem(UNIFORMS, location), v0, v1, v2);     },
		Uniform4f: (location, v0, v1, v2, v3) => { GL.uniform4f(GLGetItem(UNIFORMS, location), v0, v1, v2, v3); },
		Uniform1i: (location, v0)             => { GL.uniform1i(GLGetItem(UNIFORMS, location), v0);             },
		Uniform2i: (location, v0, v1)         => { GL.uniform2i(GLGetItem(UNIFORMS, location), v0, v1);         },
		Uniform3i: (location, v0, v1, v2)     => { GL.uniform3i(GLGetItem(UNIFORMS, location), v0, v1, v2);     },
		Uniform4i: (location, v0, v1, v2, v3) => { GL.uniform4i(GLGetItem(UNIFORMS, location), v0, v1, v2, v3); },
		UniformMatrix2fv: (location, addr) => 
		{
			let array = LoadArray(Float32Array, 2*2, addr);
			GL.uniformMatrix2fv(GLGetItem(UNIFORMS, location), false, array);
		},
		UniformMatrix3fv: (location, addr) => 
		{
			let array = LoadArray(Float32Array, 3*3, addr);
			GL.uniformMatrix3fv(GLGetItem(UNIFORMS, location), false, array);
		},
		UniformMatrix4fv: (location, addr) => 
		{
			let array = LoadArray(Float32Array, 4*4, addr);
			GL.uniformMatrix4fv(GLGetItem(UNIFORMS, location), false, array);
		},
		CreateProgram: () => 
		{
			return GLAddItem(PROGRAMS, GL.createProgram());
		},
		GenRenderbuffer: (count, renderbuffer_ptr) => 
		{
			for(let i = 0; i < count; i += 1)
			{
				const id = GLAddItem(RENDERBUFFERS, GL.createRenderbuffer());
				StoreU32(renderbuffer_ptr+4*i, id);
			}
		},
		CreateShader: (shaderType) => 
		{
			return GLAddItem(SHADERS, GL.createShader(shaderType));
		},
		DeleteBuffers: (count, buffer_ptr) => 
		{
			const buffers = LoadArray(Uint32Array, count, buffer_ptr);
			for(let i = 0; i < buffers.length; i += 1)
			{
				const id = buffers[i];
				let obj  = GLGetItem(BUFFERS, id);
				if(obj && id != 0) 
				{
					GL.deleteBuffer(obj);
					GLRemoveItem(BUFFERS, id);
				}
			}
		},
		DeleteFramebuffers: (count, framebuffer_ptr) => 
		{
			const framebuffers = LoadArray(Uint32Array, count, framebuffer_ptr);
			for(let i = 0; i < framebuffers.length; i += 1)
			{
				const id = framebuffers[i];
				let obj  = GLGetItem(FRAMEBUFFERS, id);
				if(obj && id != 0) 
				{
					GL.deleteFramebuffer(obj);
					GLRemoveItem(FRAMEBUFFERS, id);
				}
			}
		},
		DeleteProgram: (id) => 
		{
			let obj = GLGetItem(PROGRAMS, id);
			if(obj && id != 0) 
			{
				GL.deleteProgram(obj);
				GLRemoveItem(PROGRAMS, id);
			}
		},
		DeleteRenderbuffers: (count, renderbuffer_ptr) => 
		{
			const renderbuffers = LoadArray(Uint32Array, count, renderbuffer_ptr);
			for(let i = 0; i < renderbuffers.length; i += 1)
			{
				const id = renderbuffers[i];
				let obj  = GLGetItem(RENDERBUFFERS, id);
				if(obj && id != 0) 
				{
					GL.deleteRenderbuffer(obj);
					GLRemoveItem(RENDERBUFFERS, id);
				}
			}
		},
		DeleteShader: (id) => 
		{
			let obj = GLGetItem(SHADERS, id);
			if(obj && id != 0) 
			{
				GL.deleteShader(obj);
				GLRemoveItem(SHADERS, id);
			}
		},
		DeleteTextures: (count, texture_ptr) => 
		{
			const textures = LoadArray(Uint32Array, count, texture_ptr);
			for(let i = 0; i < textures.length; i += 1)
			{
				const id = textures[i];
				let obj  = GLGetItem(TEXTURES, id);
				if(obj && id != 0) 
				{
					GL.deleteTexture(obj);
					GLRemoveItem(TEXTURES, id);
				}
			}
		},
		Scissor: (x, y, width, height) => 
		{
			GL.scissor(x, y, width, height);
		},
		ShaderSource: (shader, count, strings_ptr, lengths_ptr) => 
		{
			let lengths = null;
			if(lengths_ptr)
			{
				lengths = LoadArray(Uint32Array, count, lengths_ptr);
			}

			let source = "";
			for(let i = 0; i < count; i += 1)
			{
				const addr      = LoadSize(strings_ptr + i*PTR_SIZE);
				let source_part = lengths && (lengths?.[i] ?? 0) > 0 ? LoadString(lengths[i], addr) : LoadCString(addr);

				source += source_part;
			}

			GL.shaderSource(GLGetItem(SHADERS, shader), source);
		},
		GetError: () => 
		{
			let err = GL_LAST_ERROR;
			SetGLError(0);
			if(err) 
			{
				return err;
			}

			return GL.getError();
		},
		GetProgramiv: (program, pname, ptr) => 
		{
			const param = GL.getProgramParameter(GLGetItem(PROGRAMS, program), pname);
			StoreI32(ptr, param);
		},
		GetProgramInfoLog: (program, buf_length, buf_ptr, length_ptr) => 
		{
			let log = GL.getProgramInfoLog(GLGetItem(PROGRAMS, program));
			if(log === null) 
			{
				log = "(unknown error)";
			}

			if(buf_len > 0 && buf_ptr) 
			{
				let n = Math.min(buf_len, log.length);
				log   = log.substring(0, n);
				LoadArray(Uint8Array, buf_length, buf_ptr).set(new TextEncoder().encode(log))

				StoreSize(length_ptr, n);
			}
		},
		GetShaderInfoLog: (shader, buf_ptr, buf_len, length_ptr) => 
		{
			let log = GL.getShaderInfoLog(GLGetItem(SHADERS, shader));
			if(log === null) 
			{
				log = "(unknown error)";
			}

			if(buf_len > 0 && buf_ptr) 
			{
				let n = Math.min(buf_len, log.length);
				log   = log.substring(0, n);
				LoadArray(Uint8Array, buf_len, buf_ptr).set(new TextEncoder().encode(log))

				StoreSize(length_ptr, n);
			}
		},
		GetShaderiv: (shader_id, pname, p) => {
			const shader = GLGetItem(SHADERS, shader);
			if(p && shader) 
			{
				if(pname == 35716) 
				{
					let log = GL.getShaderInfoLog(shader);
					if(log === null) 
					{
						log = "(unknown error)";
					}

					StoreSize(p, log.length+1);
				} 
				else if(pname == 35720) 
				{
					let source       = GL.getShaderSource(shader);
					let sourceLength = (source === null || source.length == 0) ? 0 : source.length+1;
					this.mem.storeInt(p, sourceLength);
				} 
				else 
				{
					let param = GL.getShaderParameter(shader, pname);
					StoreI32(p, param);
				}
			} 
			else 
			{
				SetGLError(1281);
			}
		},
		AttachShader: (program, shader) => 
		{
			GL.attachShader(GLGetItem(PROGRAMS, program), GLGetItem(SHADERS, shader));
		},
		LinkProgram: (program) =>
		{
			GL.linkProgram(GLGetItem(PROGRAMS, program));
			PROGRAM_INFO[program] = null;
			PopulateUniformTable(program);
		},
	},
};

const other_gl_funcs = {
	webgl: {
		GetCurrentContextAttributes: () => {
			if(!GL) {
				return 0;
			}
			let attrs = GL.getContextAttributes();
			let res = 0;
			if(!attrs.alpha)                        res |= 1<<0;
			if(!attrs.antialias)                    res |= 1<<1;
			if(!attrs.depth)                        res |= 1<<2;
			if(attrs.failIfMajorPerformanceCaveat)  res |= 1<<3;
			if(!attrs.premultipliedAlpha)           res |= 1<<4;
			if(attrs.preserveDrawingBuffer)         res |= 1<<5;
			if(attrs.stencil)                       res |= 1<<6;
			if(attrs.desynchronized)                res |= 1<<7;
			return res;
		},

		DrawingBufferWidth:  () => GL.drawingBufferWidth,
		DrawingBufferHeight: () => GL.drawingBufferHeight,

		IsExtensionSupported: (name_length, name_ptr) => {
			let name       = LoadString(name_length, name_ptr);
			let extensions = GL.getSupportedExtensions();
			return extensions.indexOf(name) !== -1
		},


		GetWebGLVersion: (major_ptr, minor_ptr) => {
			let version = GL.getParameter(0x1F02);
			if(version.indexOf("WebGL 2.0") !== -1) {
				this.mem.storeI32(major_ptr, 2);
				this.mem.storeI32(minor_ptr, 0);
				return;
			}

			this.mem.storeI32(major_ptr, 1);
			this.mem.storeI32(minor_ptr, 0);
		},
		GetESVersion: (major_ptr, minor_ptr) => {
			let version = GL.getParameter(0x1F02);
			if(version.indexOf("OpenGL ES 3.0") !== -1) {
				this.mem.storeI32(major_ptr, 3);
				this.mem.storeI32(minor_ptr, 0);
				return;
			}

			this.mem.storeI32(major_ptr, 2);
			this.mem.storeI32(minor_ptr, 0);
		},
		BindAttribLocation: (program, index, name_length, name_ptr) => {
			let name = LoadString(name_length, name_ptr);
			GL.bindAttribLocation(PROGRAMS[program], index, name)
		},
		BindFramebuffer: (target, framebuffer) => {
			GL.bindFramebuffer(target, framebuffer ? FRAMEBUFFERS[framebuffer] : null)
		},
		BlendColor: (red, green, blue, alpha) => {
			GL.blendColor(red, green, blue, alpha);
		},
		BlendEquation: (mode) => {
			GL.blendEquation(mode);
		},
		BlendEquationSeparate: (modeRGB, modeAlpha) => {
			GL.blendEquationSeparate(modeRGB, modeAlpha);
		},
		CompressedTexImage2D: (target, level, internalformat, width, height, border, imageSize, data) => {
			if(data) {
				GL.compressedTexImage2D(target, level, internalformat, width, height, border, this.mem.loadBytes(data, imageSize));
			} else {
				GL.compressedTexImage2D(target, level, internalformat, width, height, border, null);
			}
		},
		CompressedTexSubImage2D: (target, level, xoffset, yoffset, width, height, format, imageSize, data) => {
			if(data) {
				GL.compressedTexSubImage2D(target, level, xoffset, yoffset, width, height, format, this.mem.loadBytes(data, imageSize));
			} else {
				GL.compressedTexSubImage2D(target, level, xoffset, yoffset, width, height, format, null);
			}
		},
		CopyTexImage2D: (target, level, internalformat, x, y, width, height, border) => {
			GL.copyTexImage2D(target, level, internalformat, x, y, width, height, border);
		},
		CopyTexSubImage2D: (target, level, xoffset, yoffset, x, y, width, height) => {
			GL.copyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height);
		},
		CreateFramebuffer: () => {
			let buffer = GL.createFramebuffer();
			let id = this.getNewId(FRAMEBUFFERS);
			buffer.name = id
			FRAMEBUFFERS[id] = buffer;
			return id;
		},
		Clear: (x) => {
			GL.clear(x);
		},
		ClearColor: (r, g, b, a) => {
			GL.clearColor(r, g, b, a);
		},
		ClearDepth: (x) => {
			GL.clearDepth(x);
		},
		ClearStencil: (x) => {
			GL.clearStencil(x);
		},
		ColorMask: (r, g, b, a) => {
			GL.colorMask(!!r, !!g, !!b, !!a);
		},
		CompileShader: (shader) => {
			GL.compileShader(SHADERS[shader]);
		},
		UseProgram: (program) => {
			if(program) GL.useProgram(PROGRAMS[program]);
		},
		ValidateProgram: (program) => {
			if(program) GL.validateProgram(PROGRAMS[program]);
		},
		BindBufferBase: (target, index, buffer) => {
			AssertWebGL2();
			GL.bindBufferBase(target, index, BUFFERS[buffer]);
		},
		GetUniformBlockIndex: (program, uniformBlockName_ptr, uniformBlockName_len) => {
			AssertWebGL2();
			return GL.getUniformBlockIndex(PROGRAMS[program], this.mem.loadString(uniformBlockName_ptr, uniformBlockName_len));
		},
		CullFace: (mode) => {
			GL.cullFace(mode);
		},
		CreateBuffer: () => {
			let buffer = GL.createBuffer();
			if(!buffer) {
				SetGLError(1282);
				return 0;
			}
			let id = this.getNewId(BUFFERS);
			buffer.name = id
			BUFFERS[id] = buffer;
			return id;
		},
		Enable: (cap) => {
			GL.enable(cap);
		},
		EnableVertexAttribArray: (index) => {
			GL.enableVertexAttribArray(index);
		},
		Disable: (cap) => {
			GL.disable(cap);
		},
		DisableVertexAttribArray: (index) => {
			GL.disableVertexAttribArray(index);
		},
		FrontFace: (mode) => {
			GL.frontFace(mode);
		},
		BlendFuncSeparate: (srcRGB, dstRGB, srcAlpha, dstAlpha) => {
			GL.blendFuncSeparate(srcRGB, dstRGB, srcAlpha, dstAlpha);
		},
		BindTexture: (target, texture) => {
			GL.bindTexture(target, texture ? TEXTURES[texture] : null)
		},
		TexParameterf: (target, pname, param) => {
			GL.texParameterf(target, pname, param);
		},
		TexParameteri: (target, pname, param) => {
			GL.texParameteri(target, pname, param);
		},
		TexImage2D: (target, level, internalformat, width, height, border, format, type, size, data) => {
			let buffer = null;

			if(data)
			{
				switch(internalformat)
				{
					case GL.RGBA32F:  
					case GL.RGB32F:
					case GL.RG32F:
					case GL.R32F:     buffer = this.mem.loadF32Array(data, size); break;
					case GL.RGBA32UI: 
					case GL.RGB32UI: 
					case GL.RG32UI: 
					case GL.R32UI:    buffer = this.mem.loadU32Array(data, size); break;
					case GL.RGBA32I:  
					case GL.RGB32I:  
					case GL.RG32I:  
					case GL.R32I:     buffer = this.mem.loadI32Array(data, size); break;
					default:                buffer = this.mem.loadBytes(data, size);    break;
				}
			}

			GL.texImage2D(target, level, internalformat, width, height, border, format, type, buffer);
		},
		DeleteVertexArray: (id) => {
			AssertWebGL2();
			let obj = VAOS[id];
			if(obj && id != 0) {
				GL.deleteVertexArray(obj);
				VAOS[id] = null;
			}
		},
		BindVertexArray: (vertexArray) => {
			AssertWebGL2();
			GL.bindVertexArray(VAOS[vertexArray]);
		},
		BlendFunc: (sfactor, dfactor) => {
			GL.blendFunc(sfactor, dfactor);
		},
		BindBuffer: (target, buffer) => {
			let bufferObj = buffer ? BUFFERS[buffer] : null;
			if(target == 35051) {
				GL.currentPixelPackBufferBinding = buffer;
			} else {
				if(target == 35052) {
					GL.currentPixelUnpackBufferBinding = buffer;
				}
				GL.bindBuffer(target, bufferObj)
			}
		},
		BufferData: (target, size, data, usage) => {
			if(data) {
				GL.bufferData(target, this.mem.loadBytes(data, size), usage);
			} else {
				GL.bufferData(target, size, usage);
			}
		},
		BufferSubData: (target, offset, size, data) => {
			if(data) {
				GL.bufferSubData(target, offset, this.mem.loadBytes(data, size));
			} else {
				GL.bufferSubData(target, offset, null);
			}
		},
		VertexAttribDivisor: (index, divisor) => {
			AssertWebGL2();
			GL.vertexAttribDivisor(index, divisor);
		},
		VertexAttribPointer: (index, size, type, normalized, stride, ptr) => {
			GL.vertexAttribPointer(index, size, type, !!normalized, stride, ptr);
		},
		Viewport: (x, y, w, h) => {
			if(GLELEMENT)
			{
				GLELEMENT.width  = w;
				GLELEMENT.height = h;
			}

			GL.viewport(x, y, w, h);
		},
		DrawArraysInstanced: (mode, first, count, instanceCount) => {
			AssertWebGL2();
			GL.drawArraysInstanced(mode, first, count, instanceCount);
		},
		DrawElementsInstanced: (mode, count, type, offset, instanceCount) => {
			AssertWebGL2();
			GL.drawElementsInstanced(mode, count, type, offset, instanceCount);
		},
		ActiveTexture: (x) => {
			GL.activeTexture(x);
		},
		GetUniformLocation: (program, name_ptr, name_len) => {
			let name = this.mem.loadString(name_ptr, name_len);
			let arrayOffset = 0;
			if(name.indexOf("]", name.length - 1) !== -1) {
				let ls = name.lastIndexOf("["),
					arrayIndex = name.slice(ls + 1, -1);
				if(arrayIndex.length > 0 && (arrayOffset = parseInt(arrayIndex)) < 0) {
					return -1;
				}
				name = name.slice(0, ls)
			}
			var ptable = PROGRAM_INFO[program];
			if(!ptable) {
				return -1;
			}
			var uniformInfo = ptable.uniforms[name];
			return (uniformInfo && arrayOffset < uniformInfo[0]) ? uniformInfo[1] + arrayOffset : -1
		},
		Uniform1f: (location, v0)             => { GL.uniform1f(GLGetItem(UNIFORMS, location), v0);             },
		Uniform2f: (location, v0, v1)         => { GL.uniform2f(GLGetItem(UNIFORMS, location), v0, v1);         },
		Uniform3f: (location, v0, v1, v2)     => { GL.uniform3f(GLGetItem(UNIFORMS, location), v0, v1, v2);     },
		Uniform4f: (location, v0, v1, v2, v3) => { GL.uniform4f(GLGetItem(UNIFORMS, location), v0, v1, v2, v3); },
		Uniform1i: (location, v0)             => { GL.uniform1i(GLGetItem(UNIFORMS, location), v0);             },
		Uniform2i: (location, v0, v1)         => { GL.uniform2i(GLGetItem(UNIFORMS, location), v0, v1);         },
		Uniform3i: (location, v0, v1, v2)     => { GL.uniform3i(GLGetItem(UNIFORMS, location), v0, v1, v2);     },
		Uniform4i: (location, v0, v1, v2, v3) => { GL.uniform4i(GLGetItem(UNIFORMS, location), v0, v1, v2, v3); },
		UniformMatrix2fv: (location, addr) => {
			let array = this.mem.loadF32Array(addr, 2*2);
			GL.uniformMatrix2fv(GLGetItem(UNIFORMS, location), false, array);
		},
		UniformMatrix3fv: (location, addr) => {
			let array = this.mem.loadF32Array(addr, 3*3);
			GL.uniformMatrix3fv(GLGetItem(UNIFORMS, location), false, array);
		},
		UniformMatrix4fv: (location, addr) => {
			let array = this.mem.loadF32Array(addr, 4*4);
			GL.uniformMatrix4fv(GLGetItem(UNIFORMS, location), false, array);
		},
		CreateProgram: () => {
			let program = GL.createProgram();
			let id = this.getNewId(PROGRAMS);
			program.name = id;
			PROGRAMS[id] = program;
			return id;
		},
		CreateRenderbuffer: () => {
			let buffer = GL.createRenderbuffer();
			let id = this.getNewId(RENDERBUFFERS);
			buffer.name = id;
			RENDERBUFFERS[id] = buffer;
			return id;
		},
		CreateShader: (shaderType) => {
			let shader = GL.createShader(shaderType);
			let id = this.getNewId(SHADERS);
			shader.name = id;
			SHADERS[id] = shader;
			return id;
		},
		DeleteBuffer: (id) => {
			let obj = BUFFERS[id];
			if(obj && id != 0) {
				GL.deleteBuffer(obj);
				BUFFERS[id] = null;
			}
		},
		DeleteProgram: (id) => {
			let obj = PROGRAMS[id];
			if(obj && id != 0) {
				GL.deleteProgram(obj);
				PROGRAMS[id] = null;
			}
		},
		DeleteRenderbuffer: (id) => {
			let obj = RENDERBUFFERS[id];
			if(obj && id != 0) {
				GL.deleteRenderbuffer(obj);
				RENDERBUFFERS[id] = null;
			}
		},
		DeleteShader: (id) => {
			let obj = SHADERS[id];
			if(obj && id != 0) {
				GL.deleteShader(obj);
				SHADERS[id] = null;
			}
		},
		DepthFunc: (func) => {
			GL.depthFunc(func);
		},
		DepthMask: (flag) => {
			GL.depthMask(!!flag);
		},
		DepthRange: (zNear, zFar) => {
			GL.depthRange(zNear, zFar);
		},
		DetachShader: (program, shader) => {
			GL.detachShader(PROGRAMS[program], SHADERS[shader]);
		},
		DrawArrays: (mode, first, count) => {
			GL.drawArrays(mode, first, count);
		},
		DrawElements: (mode, count, type, indices) => {
			GL.drawElements(mode, count, type, indices);
		},
		Finish: () => {
			GL.finish();
		},
		Flush: () => {
			GL.flush();
		},
		FramebufferRenderbuffer: (target, attachment, renderbuffertarget, renderbuffer) => {
			GL.framebufferRenderbuffer(target, attachment, renderbuffertarget, RENDERBUFFERS[renderbuffer]);
		},
		FramebufferTexture2D: (target, attachment, textarget, texture, level) => {
			GL.framebufferTexture2D(target, attachment, textarget, TEXTURES[texture], level);
		},
		GenerateMipmap: (target) => {
			GL.generateMipmap(target);
		},
		GetAttribLocation: (program, name_length, name_ptr) => {
			let name = LoadString(name_length, name_ptr);
			return GL.getAttribLocation(PROGRAMS[program], name);
		},
		GetParameter: (pname) => {
			return GL.getParameter(pname);
		},
		GetParameter4i: (pname, v0, v1, v2, v3) => {
			const i4 = GL.getParameter(pname);
			this.mem.storeI32(v0, i4[0]);
			this.mem.storeI32(v1, i4[1]);
			this.mem.storeI32(v2, i4[2]);
			this.mem.storeI32(v3, i4[3]);
		},
		GetVertexAttribOffset: (index, pname) => {
			return GL.getVertexAttribOffset(index, pname);
		},
		Hint: (target, mode) => {
			GL.hint(target, mode);
		},
		IsBuffer:       (buffer)       => GL.isBuffer(BUFFERS[buffer]),
		IsEnabled:      (cap)          => GL.isEnabled(cap),
		IsFramebuffer:  (framebuffer)  => GL.isFramebuffer(FRAMEBUFFERS[framebuffer]),
		IsProgram:      (program)      => GL.isProgram(PROGRAMS[program]),
		IsRenderbuffer: (renderbuffer) => GL.isRenderbuffer(RENDERBUFFERS[renderbuffer]),
		IsShader:       (shader)       => GL.isShader(SHADERS[shader]),
		IsTexture:      (texture)      => GL.isTexture(TEXTURES[texture]),
		LineWidth: (width) => {
			GL.lineWidth(width);
		},
		PixelStorei: (pname, param) => {
			GL.pixelStorei(pname, param);
		},
		PolygonOffset: (factor, units) => {
			GL.polygonOffset(factor, units);
		},
		ReadnPixels: (x, y, width, height, format, type, bufSize, data) => {
			GL.readPixels(x, y, width, height, format, type, this.mem.loadBytes(data, bufSize));
		},
		RenderbufferStorage: (target, internalformat, width, height) => {
			GL.renderbufferStorage(target, internalformat, width, height);
		},
		SampleCoverage: (value, invert) => {
			GL.sampleCoverage(value, !!invert);
		},
		StencilFunc: (func, ref, mask) => {
			GL.stencilFunc(func, ref, mask);
		},
		StencilFuncSeparate: (face, func, ref, mask) => {
			GL.stencilFuncSeparate(face, func, ref, mask);
		},
		StencilMask: (mask) => {
			GL.stencilMask(mask);
		},
		StencilMaskSeparate: (face, mask) => {
			GL.stencilMaskSeparate(face, mask);
		},
		StencilOp: (fail, zfail, zpass) => {
			GL.stencilOp(fail, zfail, zpass);
		},
		StencilOpSeparate: (face, fail, zfail, zpass) => {
			GL.stencilOpSeparate(face, fail, zfail, zpass);
		},
		TexSubImage2D: (target, level, xoffset, yoffset, width, height, format, type, size, data) => {
			GL.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, this.mem.loadBytes(data, size));
		},
		VertexAttrib1f: (index, x) => {
			GL.vertexAttrib1f(index, x);
		},
		VertexAttrib2f: (index, x, y) => {
			GL.vertexAttrib2f(index, x, y);
		},
		VertexAttrib3f: (index, x, y, z) => {
			GL.vertexAttrib3f(index, x, y, z);
		},
		VertexAttrib4f: (index, x, y, z, w) => {
			GL.vertexAttrib4f(index, x, y, z, w);
		},
	},
	webgl2: {
		CopyBufferSubData: (readTarget, writeTarget, readOffset, writeOffset, size) => {
			AssertWebGL2();
			GL.copyBufferSubData(readTarget, writeTarget, readOffset, writeOffset, size);
		},
		GetBufferSubData: (target, srcByteOffset, dst_buffer_ptr, dst_buffer_len, dstOffset, length) => {
			AssertWebGL2();
			GL.getBufferSubData(target, srcByteOffset, this.mem.loadBytes(dst_buffer_ptr, dst_buffer_len), dstOffset, length);
		},
		/* Framebuffer objects */
		BlitFramebuffer: (srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter) => {
			AssertWebGL2();
			GL.blitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
		},
		FramebufferTextureLayer: (target, attachment, texture, level, layer) => {
			AssertWebGL2();
			GL.framebufferTextureLayer(target, attachment, TEXTURES[texture], level, layer);
		},
		InvalidateFramebuffer: (target, attachments_ptr, attachments_len) => {
			AssertWebGL2();
			let attachments = this.mem.loadU32Array(attachments_ptr, attachments_len);
			GL.invalidateFramebuffer(target, attachments);
		},
		InvalidateSubFramebuffer: (target, attachments_ptr, attachments_len, x, y, width, height) => {
			AssertWebGL2();
			let attachments = this.mem.loadU32Array(attachments_ptr, attachments_len);
			GL.invalidateSubFramebuffer(target, attachments, x, y, width, height);
		},
		ReadBuffer: (src) => {
			AssertWebGL2();
			GL.readBuffer(src);
		},

		/* Renderbuffer objects */
		RenderbufferStorageMultisample: (target, samples, internalformat, width, height) => {
			AssertWebGL2();
			GL.renderbufferStorageMultisample(target, samples, internalformat, width, height);
		},

		/* Texture objects */

		TexStorage3D: (target, levels, internalformat, width, height, depth) => {
			AssertWebGL2();
			GL.texStorage3D(target, levels, internalformat, width, height, depth);
		},
		TexImage3D: (target, level, internalformat, width, height, depth, border, format, type, size, data) => {
			AssertWebGL2();
			if(data) {
				GL.texImage3D(target, level, internalformat, width, height, depth, border, format, type, this.mem.loadBytes(data, size));
			} else {
				GL.texImage3D(target, level, internalformat, width, height, depth, border, format, type, null);
			}
		},
		TexSubImage3D: (target, level, xoffset, yoffset, zoffset, width, height, depth, format, type, size, data) => {
			AssertWebGL2();
			GL.texSubImage3D(target, level, xoffset, yoffset, zoffset, width, height, depth, format, type, this.mem.loadBytes(data, size));
		},
		CompressedTexImage3D: (target, level, internalformat, width, height, depth, border, imageSize, data) => {
			AssertWebGL2();
			if(data) {
				GL.compressedTexImage3D(target, level, internalformat, width, height, depth, border, this.mem.loadBytes(data, imageSize));
			} else {
				GL.compressedTexImage3D(target, level, internalformat, width, height, depth, border, null);
			}
		},
		CompressedTexSubImage3D: (target, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data) => {
			AssertWebGL2();
			if(data) {
				GL.compressedTexSubImage3D(target, level, xoffset, yoffset, zoffset, width, height, depth, format, this.mem.loadBytes(data, imageSize));
			} else {
				GL.compressedTexSubImage3D(target, level, xoffset, yoffset, zoffset, width, height, depth, format, null);
			}
		},

		CopyTexSubImage3D: (target, level, xoffset, yoffset, zoffset, x, y, width, height) => {
			AssertWebGL2();
			GL.copyTexSubImage3D(target, level, xoffset, yoffset, zoffset, x, y, width, height);
		},

		/* Programs and shaders */
		GetFragDataLocation: (program, name_ptr, name_len) => {
			AssertWebGL2();
			return GL.getFragDataLocation(PROGRAMS[program], this.mem.loadString(name_ptr, name_len));
		},

		/* Uniforms */
		Uniform1ui: (location, v0) => {
			AssertWebGL2();
			GL.uniform1ui(GLGetItem(UNIFORMS, location), v0);
		},
		Uniform2ui: (location, v0, v1) => {
			AssertWebGL2();
			GL.uniform2ui(GLGetItem(UNIFORMS, location), v0, v1);
		},
		Uniform3ui: (location, v0, v1, v2) => {
			AssertWebGL2();
			GL.uniform3ui(GLGetItem(UNIFORMS, location), v0, v1, v2);
		},
		Uniform4ui: (location, v0, v1, v2, v3) => {
			AssertWebGL2();
			GL.uniform4ui(GLGetItem(UNIFORMS, location), v0, v1, v2, v3);
		},

		UniformMatrix3x2fv: (location, addr) => {
			AssertWebGL2();
			let array = this.mem.loadF32Array(addr, 3*2);
			GL.uniformMatrix3x2fv(GLGetItem(UNIFORMS, location), false, array);
		},
		UniformMatrix4x2fv: (location, addr) => {
			AssertWebGL2();
			let array = this.mem.loadF32Array(addr, 4*2);
			GL.uniformMatrix4x2fv(GLGetItem(UNIFORMS, location), false, array);
		},
		UniformMatrix2x3fv: (location, addr) => {
			AssertWebGL2();
			let array = this.mem.loadF32Array(addr, 2*3);
			GL.uniformMatrix2x3fv(GLGetItem(UNIFORMS, location), false, array);
		},
		UniformMatrix4x3fv: (location, addr) => {
			AssertWebGL2();
			let array = this.mem.loadF32Array(addr, 4*3);
			GL.uniformMatrix4x3fv(GLGetItem(UNIFORMS, location), false, array);
		},
		UniformMatrix2x4fv: (location, addr) => {
			AssertWebGL2();
			let array = this.mem.loadF32Array(addr, 2*4);
			GL.uniformMatrix2x4fv(GLGetItem(UNIFORMS, location), false, array);
		},
		UniformMatrix3x4fv: (location, addr) => {
			AssertWebGL2();
			let array = this.mem.loadF32Array(addr, 3*4);
			GL.uniformMatrix3x4fv(GLGetItem(UNIFORMS, location), false, array);
		},

		/* Vertex attribs */
		VertexAttribI4i: (index, x, y, z, w) => {
			AssertWebGL2();
			GL.vertexAttribI4i(index, x, y, z, w);
		},
		VertexAttribI4ui: (index, x, y, z, w) => {
			AssertWebGL2();
			GL.vertexAttribI4ui(index, x, y, z, w);
		},
		VertexAttribIPointer: (index, size, type, stride, offset) => {
			AssertWebGL2();
			GL.vertexAttribIPointer(index, size, type, stride, offset);
		},

		/* Writing to the drawing buffer */
		DrawRangeElements: (mode, start, end, count, type, offset) => {
			AssertWebGL2();
			GL.drawRangeElements(mode, start, end, count, type, offset);
		},

		/* Multiple Render Targets */
		DrawBuffers: (buffers_ptr, buffers_len) => {
			AssertWebGL2();
			let array = this.mem.loadU32Array(buffers_ptr, buffers_len);
			GL.drawBuffers(array);
		},
		ClearBufferfv: (buffer, drawbuffer, values_ptr, values_len) => {
			AssertWebGL2();
			let array = this.mem.loadF32Array(values_ptr, values_len);
			GL.clearBufferfv(buffer, drawbuffer, array);
		},
		ClearBufferiv: (buffer, drawbuffer, values_ptr, values_len) => {
			AssertWebGL2();
			let array = this.mem.loadI32Array(values_ptr, values_len);
			GL.clearBufferiv(buffer, drawbuffer, array);
		},
		ClearBufferuiv: (buffer, drawbuffer, values_ptr, values_len) => {
			AssertWebGL2();
			let array = this.mem.loadU32Array(values_ptr, values_len);
			GL.clearBufferuiv(buffer, drawbuffer, array);
		},
		ClearBufferfi: (buffer, drawbuffer, depth, stencil) => {
			AssertWebGL2();
			GL.clearBufferfi(buffer, drawbuffer, depth, stencil);
		},

		/* Query Objects */
		CreateQuery: () => {
			AssertWebGL2();
			let query = GL.createQuery();
			let id = this.getNewId(QUERIES);
			query.name = id;
			QUERIES[id] = query;
			return id;
		},
		DeleteQuery: (id) => {
			AssertWebGL2();
			let obj = QUERIES[id];
			if(obj && id != 0) {
				GL.deleteQuery(obj);
				QUERIES[id] = null;
			}
		},
		IsQuery: (query) => {
			AssertWebGL2();
			return GL.isQuery(QUERIES[query]);
		},
		BeginQuery: (target, query) => {
			AssertWebGL2();
			GL.beginQuery(target, QUERIES[query])
		},
		EndQuery: (target) => {
			AssertWebGL2();
			GL.endQuery(target);
		},
		GetQuery: (target, pname) => {
			AssertWebGL2();
			let query = GL.getQuery(target, pname);
			if(!query) {
				return 0;
			}
			if(QUERIES.indexOf(query) !== -1) {
				return query.name;
			}
			let id = this.getNewId(QUERIES);
			query.name = id;
			QUERIES[id] = query;
			return id;
		},

		/* Sampler Objects */
		CreateSampler: () => {
			AssertWebGL2();
			let sampler = GL.createSampler();
			let id = this.getNewId(this.samplers);
			sampler.name = id;
			this.samplers[id] = sampler;
			return id;
		},
		DeleteSampler: (id) => {
			AssertWebGL2();
			let obj = this.samplers[id];
			if(obj && id != 0) {
				GL.deleteSampler(obj);
				this.samplers[id] = null;
			}
		},
		IsSampler: (sampler) => {
			AssertWebGL2();
			return GL.isSampler(this.samplers[sampler]);
		},
		BindSampler: (unit, sampler) => {
			AssertWebGL2();
			GL.bindSampler(unit, this.samplers[sampler]);
		},
		SamplerParameteri: (sampler, pname, param) => {
			AssertWebGL2();
			GL.samplerParameteri(this.samplers[sampler], pname, param);
		},
		SamplerParameterf: (sampler, pname, param) => {
			AssertWebGL2();
			GL.samplerParameterf(this.samplers[sampler], pname, param);
		},

		/* Sync objects */
		FenceSync: (condition, flags) => {
			AssertWebGL2();
			let sync = GL.fenceSync(condition, flags);
			let id = this.getNewId(SYNCS);
			sync.name = id;
			SYNCS[id] = sync;
			return id;
		},
		IsSync: (sync) => {
			AssertWebGL2();
			return GL.isSync(SYNCS[sync]);
		},
		DeleteSync: (id) => {
			AssertWebGL2();
			let obj = SYNCS[id];
			if(obj && id != 0) {
				GL.deleteSampler(obj);
				SYNCS[id] = null;
			}
		},
		ClientWaitSync: (sync, flags, timeout) => {
			AssertWebGL2();
			return GL.clientWaitSync(SYNCS[sync], flags, timeout);
		},
		WaitSync: (sync, flags, timeout) => {
			AssertWebGL2();
			GL.waitSync(SYNCS[sync], flags, timeout)	;
		},


		/* Transform Feedback */
		CreateTransformFeedback: () => {
			AssertWebGL2();
			let transformFeedback = GL.createTransformFeedback();
			let id = this.getNewId(TRANSFORM_FEEDBACK);
			transformFeedback.name = id;
			TRANSFORM_FEEDBACK[id] = transformFeedback;
			return id;
		},
		DeleteTransformFeedback: (id)  => {
			AssertWebGL2();
			let obj = TRANSFORM_FEEDBACK[id];
			if(obj && id != 0) {
				GL.deleteTransformFeedback(obj);
				TRANSFORM_FEEDBACK[id] = null;
			}
		},
		IsTransformFeedback: (tf) => {
			AssertWebGL2();
			return GL.isTransformFeedback(TRANSFORM_FEEDBACK[tf]);
		},
		BindTransformFeedback: (target, tf) => {
			AssertWebGL2();
			GL.bindTransformFeedback(target, TRANSFORM_FEEDBACK[tf]);
		},
		BeginTransformFeedback: (primitiveMode) => {
			AssertWebGL2();
			GL.beginTransformFeedback(primitiveMode);
		},
		EndTransformFeedback: () => {
			AssertWebGL2();
			GL.endTransformFeedback();
		},
		TransformFeedbackVaryings: (program, varyings_ptr, varyings_len, bufferMode) => {
			AssertWebGL2();
			const stringSize = this.mem.intSize*2;
			let varyings = [];
			for(let i = 0; i < varyings_len; i++) {
				let ptr = this.mem.loadPtr(varyings_ptr + i*stringSize + 0*4);
				let len = this.mem.loadPtr(varyings_ptr + i*stringSize + 1*4);
				varyings.push(this.mem.loadString(ptr, len));
			}
			GL.transformFeedbackVaryings(PROGRAMS[program], varyings, bufferMode);
		},
		PauseTransformFeedback: () => {
			AssertWebGL2();
			GL.pauseTransformFeedback();
		},
		ResumeTransformFeedback: () => {
			AssertWebGL2();
			GL.resumeTransformFeedback();
		},


		/* Uniform Buffer Objects and Transform Feedback Buffers */
		BindBufferRange: (target, index, buffer, offset, size) => {
			AssertWebGL2();
			GL.bindBufferRange(target, index, BUFFERS[buffer], offset, size);
		},
		// any getActiveUniformBlockParameter(WebGLProgram program, GLuint uniformBlockIndex, GLenum pname);
		GetActiveUniformBlockName: (program, uniformBlockIndex, buf_ptr, buf_len, length_ptr) => {
			AssertWebGL2();
			let name = GL.getActiveUniformBlockName(PROGRAMS[program], uniformBlockIndex);

			let n = Math.min(buf_len, name.length);
			name = name.substring(0, n);
			this.mem.loadBytes(buf_ptr, buf_len).set(new TextEncoder().encode(name))
			this.mem.storeInt(length_ptr, n);
		},
		UniformBlockBinding: (program, uniformBlockIndex, uniformBlockBinding) => {
			AssertWebGL2();
			GL.uniformBlockBinding(PROGRAMS[program], uniformBlockIndex, uniformBlockBinding);
		},

		/* Vertex Array Objects */
		IsVertexArray: (vertexArray) => {
			AssertWebGL2();
			return GL.isVertexArray(VAOS[vertexArray]);
		},
	},
};

async function StartWasm(path)
{
	const response = await fetch(path);
	const data     = await response.arrayBuffer();
	const wasm     = await WebAssembly.instantiate(data, imports);

	const exports = wasm.instance.exports;

	if(exports.memory)
	{
		SetMemory(exports.memory);
	}

	exports?._start();

	exports?.RunTests();

	return;
}
