@swf2es/context3d 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +219 -2
- package/dist/agal.d.ts +23 -0
- package/dist/agal.d.ts.map +1 -0
- package/dist/agal.js +410 -0
- package/dist/agal.js.map +1 -0
- package/dist/backend.d.ts +122 -0
- package/dist/backend.d.ts.map +1 -0
- package/dist/backend.js +63 -0
- package/dist/backend.js.map +1 -0
- package/dist/device/index.d.ts +3 -0
- package/dist/device/index.d.ts.map +1 -0
- package/dist/device/index.js +6 -0
- package/dist/device/index.js.map +1 -0
- package/dist/device/programs.d.ts +13 -0
- package/dist/device/programs.d.ts.map +1 -0
- package/dist/device/programs.js +59 -0
- package/dist/device/programs.js.map +1 -0
- package/dist/device/state.d.ts +53 -0
- package/dist/device/state.d.ts.map +1 -0
- package/dist/device/state.js +196 -0
- package/dist/device/state.js.map +1 -0
- package/dist/dxt.d.ts +22 -0
- package/dist/dxt.d.ts.map +1 -0
- package/dist/dxt.js +304 -0
- package/dist/dxt.js.map +1 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +8 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/webgl/buffers.d.ts +27 -0
- package/dist/webgl/buffers.d.ts.map +1 -0
- package/dist/webgl/buffers.js +53 -0
- package/dist/webgl/buffers.js.map +1 -0
- package/dist/webgl/context.d.ts +123 -0
- package/dist/webgl/context.d.ts.map +1 -0
- package/dist/webgl/context.js +1368 -0
- package/dist/webgl/context.js.map +1 -0
- package/dist/webgl/index.d.ts +3 -0
- package/dist/webgl/index.d.ts.map +1 -0
- package/dist/webgl/index.js +2 -0
- package/dist/webgl/index.js.map +1 -0
- package/dist/webgl/limits.d.ts +23 -0
- package/dist/webgl/limits.d.ts.map +1 -0
- package/dist/webgl/limits.js +35 -0
- package/dist/webgl/limits.js.map +1 -0
- package/dist/webgl/programs.d.ts +22 -0
- package/dist/webgl/programs.d.ts.map +1 -0
- package/dist/webgl/programs.js +110 -0
- package/dist/webgl/programs.js.map +1 -0
- package/dist/webgl/state.d.ts +42 -0
- package/dist/webgl/state.d.ts.map +1 -0
- package/dist/webgl/state.js +55 -0
- package/dist/webgl/state.js.map +1 -0
- package/dist/webgl/targets.d.ts +20 -0
- package/dist/webgl/targets.d.ts.map +1 -0
- package/dist/webgl/targets.js +138 -0
- package/dist/webgl/targets.js.map +1 -0
- package/dist/webgl/textures.d.ts +28 -0
- package/dist/webgl/textures.d.ts.map +1 -0
- package/dist/webgl/textures.js +119 -0
- package/dist/webgl/textures.js.map +1 -0
- package/dist/webgl/validation.d.ts +4 -0
- package/dist/webgl/validation.d.ts.map +1 -0
- package/dist/webgl/validation.js +52 -0
- package/dist/webgl/validation.js.map +1 -0
- package/package.json +37 -3
- package/src/agal.ts +496 -0
- package/src/backend.ts +210 -0
- package/src/device/index.ts +5 -0
- package/src/device/programs.ts +66 -0
- package/src/device/state.ts +214 -0
- package/src/dxt.ts +349 -0
- package/src/errors.ts +68 -0
- package/src/index.ts +35 -0
- package/src/webgl/buffers.ts +102 -0
- package/src/webgl/context.ts +1803 -0
- package/src/webgl/index.ts +2 -0
- package/src/webgl/limits.ts +61 -0
- package/src/webgl/programs.ts +145 -0
- package/src/webgl/state.ts +65 -0
- package/src/webgl/targets.ts +229 -0
- package/src/webgl/textures.ts +200 -0
- package/src/webgl/validation.ts +72 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Per-context resource budgets; byte counts cover buffer storage, texture mip
|
|
2
|
+
* chains and texture-target attachments, not physical driver VRAM. */
|
|
3
|
+
export interface WebGLContext3DLimits {
|
|
4
|
+
maxBuffers: number;
|
|
5
|
+
maxVertexBuffers: number;
|
|
6
|
+
maxIndexBuffers: number;
|
|
7
|
+
maxPrograms: number;
|
|
8
|
+
maxTextures: number;
|
|
9
|
+
max2DTextures: number;
|
|
10
|
+
maxRectangleTextures: number;
|
|
11
|
+
maxCubeTextures: number;
|
|
12
|
+
maxBufferBytes: number;
|
|
13
|
+
maxVertexBufferBytes: number;
|
|
14
|
+
maxIndexBufferBytes: number;
|
|
15
|
+
maxTextureBytes: number;
|
|
16
|
+
max2DTextureBytes: number;
|
|
17
|
+
maxRectangleTextureBytes: number;
|
|
18
|
+
maxCubeTextureBytes: number;
|
|
19
|
+
maxTargetDepthBytes: number;
|
|
20
|
+
maxTargetColorBytes: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const DEFAULT_LIMITS: WebGLContext3DLimits = {
|
|
24
|
+
maxBuffers: 8192,
|
|
25
|
+
maxVertexBuffers: 4096,
|
|
26
|
+
maxIndexBuffers: 4096,
|
|
27
|
+
maxPrograms: 4096,
|
|
28
|
+
maxTextures: 12288,
|
|
29
|
+
max2DTextures: 4096,
|
|
30
|
+
maxRectangleTextures: 4096,
|
|
31
|
+
maxCubeTextures: 4096,
|
|
32
|
+
maxBufferBytes: 384 * 1024 * 1024,
|
|
33
|
+
maxVertexBufferBytes: 256 * 1024 * 1024,
|
|
34
|
+
maxIndexBufferBytes: 128 * 1024 * 1024,
|
|
35
|
+
maxTextureBytes: 1280 * 1024 * 1024,
|
|
36
|
+
max2DTextureBytes: 512 * 1024 * 1024,
|
|
37
|
+
maxRectangleTextureBytes: 512 * 1024 * 1024,
|
|
38
|
+
maxCubeTextureBytes: 256 * 1024 * 1024,
|
|
39
|
+
maxTargetDepthBytes: 64 * 1024 * 1024,
|
|
40
|
+
maxTargetColorBytes: 64 * 1024 * 1024,
|
|
41
|
+
};
|
|
42
|
+
const LIMIT_NAMES = Object.keys(DEFAULT_LIMITS) as (keyof WebGLContext3DLimits)[];
|
|
43
|
+
|
|
44
|
+
export function resourceLimits(
|
|
45
|
+
overrides: Partial<WebGLContext3DLimits>,
|
|
46
|
+
): Readonly<WebGLContext3DLimits> {
|
|
47
|
+
const limits = { ...DEFAULT_LIMITS };
|
|
48
|
+
for (let i = 0; i < LIMIT_NAMES.length; i++) {
|
|
49
|
+
const name = LIMIT_NAMES[i];
|
|
50
|
+
const value = overrides[name];
|
|
51
|
+
if (value !== undefined) {
|
|
52
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
53
|
+
throw new RangeError(`Context3D resource limit ${name} must be a nonnegative safe integer`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
limits[name] = value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return Object.freeze(limits);
|
|
61
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { type SamplerSource, translate } from "../agal.js";
|
|
2
|
+
import { linkProgram as link, ShaderError } from "../device/programs.js";
|
|
3
|
+
import { Context3DError } from "../errors.js";
|
|
4
|
+
|
|
5
|
+
export interface ProgramState {
|
|
6
|
+
handle: WebGLProgram | null;
|
|
7
|
+
targetY: WebGLUniformLocation | null;
|
|
8
|
+
attributes: number[];
|
|
9
|
+
vertex: (WebGLUniformLocation | null)[];
|
|
10
|
+
vertexBank: WebGLUniformLocation | null;
|
|
11
|
+
fragment: (WebGLUniformLocation | null)[];
|
|
12
|
+
samplers: { source: SamplerSource; location: WebGLUniformLocation | null }[];
|
|
13
|
+
samplerMask: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CopyProgram {
|
|
17
|
+
handle: WebGLProgram;
|
|
18
|
+
size: WebGLUniformLocation | null;
|
|
19
|
+
face: WebGLUniformLocation | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function uploadProgram(
|
|
23
|
+
gl: WebGL2RenderingContext,
|
|
24
|
+
state: ProgramState,
|
|
25
|
+
vertex: Uint8Array,
|
|
26
|
+
fragment: Uint8Array,
|
|
27
|
+
): void {
|
|
28
|
+
// Translate both before allocating anything on the GPU.
|
|
29
|
+
const vs = translate(vertex, "vertex");
|
|
30
|
+
const fs = translate(fragment, "fragment");
|
|
31
|
+
for (let i = 0; i < 8; i++) {
|
|
32
|
+
if (fs.varyings[i] & ~vs.varyings[i]) {
|
|
33
|
+
throw new Context3DError(
|
|
34
|
+
"agalVaryingMissing",
|
|
35
|
+
"Fragment program reads an unwritten varying component",
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
for (let i = 0; i < 8; i++) {
|
|
40
|
+
if (vs.varyings[i] && vs.varyings[i] !== 15) {
|
|
41
|
+
throw new Context3DError(
|
|
42
|
+
"agalVaryingPartial",
|
|
43
|
+
"Vertex program only partially writes a varying",
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const handle = linkProgram(gl, vs.source, fs.source);
|
|
49
|
+
if (state.handle && gl.getParameter(gl.CURRENT_PROGRAM) === state.handle) {
|
|
50
|
+
gl.useProgram(null);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
gl.deleteProgram(state.handle);
|
|
54
|
+
state.handle = handle;
|
|
55
|
+
state.targetY = gl.getUniformLocation(handle, "targetY");
|
|
56
|
+
state.attributes = vs.attributes;
|
|
57
|
+
state.vertex = [];
|
|
58
|
+
state.vertexBank = vs.indirect ? gl.getUniformLocation(handle, "vc[0]") : null;
|
|
59
|
+
state.fragment = [];
|
|
60
|
+
state.samplers = [];
|
|
61
|
+
state.samplerMask = 0;
|
|
62
|
+
for (let i = 0; i < vs.constants.length; i++) {
|
|
63
|
+
const index = vs.constants[i];
|
|
64
|
+
state.vertex[index] = gl.getUniformLocation(handle, `vc${index}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (let i = 0; i < fs.constants.length; i++) {
|
|
68
|
+
const index = fs.constants[i];
|
|
69
|
+
state.fragment[index] = gl.getUniformLocation(handle, `fc${index}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (let i = 0; i < fs.samplers.length; i++) {
|
|
73
|
+
const source = fs.samplers[i];
|
|
74
|
+
state.samplerMask |= 1 << source.index;
|
|
75
|
+
state.samplers.push({
|
|
76
|
+
source,
|
|
77
|
+
location: gl.getUniformLocation(handle, `fs${source.index}`),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A program linked as the device links one, its failures Context3D's errors as before. */
|
|
83
|
+
function linkProgram(gl: WebGL2RenderingContext, vertex: string, fragment: string): WebGLProgram {
|
|
84
|
+
try {
|
|
85
|
+
return link(gl, vertex, fragment);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
if (!(e instanceof ShaderError)) {
|
|
88
|
+
throw e;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
switch (e.stage) {
|
|
92
|
+
case "allocate":
|
|
93
|
+
throw new Context3DError("operation", "Context3D experiment: cannot allocate shader");
|
|
94
|
+
case "compile":
|
|
95
|
+
throw new Context3DError("nativeShader", `Context3D shader: ${e.log}`);
|
|
96
|
+
default:
|
|
97
|
+
throw new Context3DError("nativeShader", `Context3D link: ${e.log}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function createCopyProgram(gl: WebGL2RenderingContext, cube: boolean): CopyProgram {
|
|
103
|
+
const vertex = `#version 300 es
|
|
104
|
+
void main() {
|
|
105
|
+
vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
|
106
|
+
gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
|
|
107
|
+
}`;
|
|
108
|
+
const fragment = cube
|
|
109
|
+
? `#version 300 es
|
|
110
|
+
precision highp float;
|
|
111
|
+
uniform samplerCube uSource;
|
|
112
|
+
uniform vec2 uSize;
|
|
113
|
+
uniform int uFace;
|
|
114
|
+
out vec4 color;
|
|
115
|
+
void main() {
|
|
116
|
+
vec2 p = gl_FragCoord.xy * 2.0 / uSize - 1.0;
|
|
117
|
+
vec3 direction;
|
|
118
|
+
switch (uFace) {
|
|
119
|
+
case 0: direction = vec3(1.0, -p.y, -p.x); break;
|
|
120
|
+
case 1: direction = vec3(-1.0, -p.y, p.x); break;
|
|
121
|
+
case 2: direction = vec3(p.x, 1.0, p.y); break;
|
|
122
|
+
case 3: direction = vec3(p.x, -1.0, -p.y); break;
|
|
123
|
+
case 4: direction = vec3(p.x, -p.y, 1.0); break;
|
|
124
|
+
default: direction = vec3(-p.x, -p.y, -1.0); break;
|
|
125
|
+
}
|
|
126
|
+
color = textureLod(uSource, direction, 0.0);
|
|
127
|
+
}`
|
|
128
|
+
: `#version 300 es
|
|
129
|
+
precision highp float;
|
|
130
|
+
uniform sampler2D uSource;
|
|
131
|
+
out vec4 color;
|
|
132
|
+
void main() {
|
|
133
|
+
color = texelFetch(uSource, ivec2(gl_FragCoord.xy), 0);
|
|
134
|
+
}`;
|
|
135
|
+
const handle = linkProgram(gl, vertex, fragment);
|
|
136
|
+
const program = {
|
|
137
|
+
handle,
|
|
138
|
+
size: gl.getUniformLocation(handle, "uSize"),
|
|
139
|
+
face: gl.getUniformLocation(handle, "uFace"),
|
|
140
|
+
};
|
|
141
|
+
gl.useProgram(handle);
|
|
142
|
+
gl.uniform1i(gl.getUniformLocation(handle, "uSource"), 0);
|
|
143
|
+
|
|
144
|
+
return program;
|
|
145
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { CompareMode, StencilAction } from "../backend.js";
|
|
2
|
+
import { Context3DError } from "../errors.js";
|
|
3
|
+
|
|
4
|
+
export const DEPTH_FUNCTIONS = {
|
|
5
|
+
always: "ALWAYS",
|
|
6
|
+
never: "NEVER",
|
|
7
|
+
less: "LESS",
|
|
8
|
+
lessEqual: "LEQUAL",
|
|
9
|
+
equal: "EQUAL",
|
|
10
|
+
notEqual: "NOTEQUAL",
|
|
11
|
+
greaterEqual: "GEQUAL",
|
|
12
|
+
greater: "GREATER",
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
export const BLEND_FUNCTIONS = {
|
|
16
|
+
zero: "ZERO",
|
|
17
|
+
one: "ONE",
|
|
18
|
+
sourceColor: "SRC_COLOR",
|
|
19
|
+
oneMinusSourceColor: "ONE_MINUS_SRC_COLOR",
|
|
20
|
+
sourceAlpha: "SRC_ALPHA",
|
|
21
|
+
oneMinusSourceAlpha: "ONE_MINUS_SRC_ALPHA",
|
|
22
|
+
destinationColor: "DST_COLOR",
|
|
23
|
+
oneMinusDestinationColor: "ONE_MINUS_DST_COLOR",
|
|
24
|
+
destinationAlpha: "DST_ALPHA",
|
|
25
|
+
oneMinusDestinationAlpha: "ONE_MINUS_DST_ALPHA",
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
export const STENCIL_FUNCTIONS = {
|
|
29
|
+
keep: "KEEP",
|
|
30
|
+
zero: "ZERO",
|
|
31
|
+
set: "REPLACE",
|
|
32
|
+
incrementSaturate: "INCR",
|
|
33
|
+
decrementSaturate: "DECR",
|
|
34
|
+
invert: "INVERT",
|
|
35
|
+
incrementWrap: "INCR_WRAP",
|
|
36
|
+
decrementWrap: "DECR_WRAP",
|
|
37
|
+
} as const;
|
|
38
|
+
|
|
39
|
+
export class StencilState {
|
|
40
|
+
compare: CompareMode = "always";
|
|
41
|
+
pass: StencilAction = "keep";
|
|
42
|
+
depthFail: StencilAction = "keep";
|
|
43
|
+
stencilFail: StencilAction = "keep";
|
|
44
|
+
|
|
45
|
+
get active(): boolean {
|
|
46
|
+
return (
|
|
47
|
+
this.compare !== "always" ||
|
|
48
|
+
this.pass !== "keep" ||
|
|
49
|
+
this.depthFail !== "keep" ||
|
|
50
|
+
this.stencilFail !== "keep"
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function range(start: number, count: number, length: number): void {
|
|
56
|
+
if (
|
|
57
|
+
!Number.isInteger(start) ||
|
|
58
|
+
!Number.isInteger(count) ||
|
|
59
|
+
start < 0 ||
|
|
60
|
+
count < 0 ||
|
|
61
|
+
start > length - count
|
|
62
|
+
) {
|
|
63
|
+
throw new Context3DError("range", "Context3D experiment: range outside resource");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import type { Texture3D } from "../backend.js";
|
|
2
|
+
import { Context3DError } from "../errors.js";
|
|
3
|
+
import type { TextureState } from "./textures.js";
|
|
4
|
+
|
|
5
|
+
export interface MultisampleTarget {
|
|
6
|
+
framebuffer: WebGLFramebuffer;
|
|
7
|
+
color: WebGLRenderbuffer;
|
|
8
|
+
depth: WebGLRenderbuffer | null;
|
|
9
|
+
samples: number;
|
|
10
|
+
resolve: WebGLFramebuffer | null;
|
|
11
|
+
texture: WebGLTexture | null;
|
|
12
|
+
valid: boolean;
|
|
13
|
+
dirty: boolean;
|
|
14
|
+
face: number;
|
|
15
|
+
depthAttached: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function selectSamples(gl: WebGL2RenderingContext, quality: number, depth: boolean): number {
|
|
19
|
+
if (!Number.isInteger(quality)) {
|
|
20
|
+
throw new Context3DError("operation", "Context3D experiment: invalid antialiasing quality");
|
|
21
|
+
}
|
|
22
|
+
if (quality < 2) {
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const color: Int32Array = gl.getInternalformatParameter(gl.RENDERBUFFER, gl.RGBA8, gl.SAMPLES);
|
|
27
|
+
const stencil: Int32Array | null = depth
|
|
28
|
+
? gl.getInternalformatParameter(gl.RENDERBUFFER, gl.DEPTH24_STENCIL8, gl.SAMPLES)
|
|
29
|
+
: null;
|
|
30
|
+
let samples = 0;
|
|
31
|
+
// Quality is a hardware-dependent request; bound this experiment to 4x.
|
|
32
|
+
for (let i = 0; i < color.length; i++) {
|
|
33
|
+
const count = color[i];
|
|
34
|
+
if (count > samples && count <= Math.min(quality, 4) && (!stencil || stencil.includes(count))) {
|
|
35
|
+
samples = count;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return samples;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function targetFramebuffer(state: TextureState): WebGLFramebuffer | null {
|
|
43
|
+
return state.samples ? (state.multisample?.framebuffer ?? null) : state.framebuffer;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function resolveTarget(
|
|
47
|
+
gl: WebGL2RenderingContext,
|
|
48
|
+
texture: Texture3D,
|
|
49
|
+
state: TextureState,
|
|
50
|
+
face: number,
|
|
51
|
+
scissor: boolean,
|
|
52
|
+
): void {
|
|
53
|
+
const target = state.multisample;
|
|
54
|
+
if (!state.samples || !target?.dirty) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, target.framebuffer);
|
|
59
|
+
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, target.resolve ?? state.framebuffer);
|
|
60
|
+
if (scissor) {
|
|
61
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
62
|
+
}
|
|
63
|
+
gl.blitFramebuffer(
|
|
64
|
+
0,
|
|
65
|
+
0,
|
|
66
|
+
texture.width,
|
|
67
|
+
texture.height,
|
|
68
|
+
0,
|
|
69
|
+
0,
|
|
70
|
+
texture.width,
|
|
71
|
+
texture.height,
|
|
72
|
+
gl.COLOR_BUFFER_BIT,
|
|
73
|
+
gl.NEAREST,
|
|
74
|
+
);
|
|
75
|
+
if (target.resolve) {
|
|
76
|
+
// SwiftShader corrupts direct multisample blits into cube faces.
|
|
77
|
+
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, target.resolve);
|
|
78
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
79
|
+
gl.bindTexture(gl.TEXTURE_CUBE_MAP, state.handle);
|
|
80
|
+
gl.copyTexSubImage2D(
|
|
81
|
+
gl.TEXTURE_CUBE_MAP_POSITIVE_X + face,
|
|
82
|
+
0,
|
|
83
|
+
0,
|
|
84
|
+
0,
|
|
85
|
+
0,
|
|
86
|
+
0,
|
|
87
|
+
texture.width,
|
|
88
|
+
texture.height,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
target.dirty = false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function deleteMultisample(gl: WebGL2RenderingContext, target: MultisampleTarget): void {
|
|
95
|
+
gl.deleteFramebuffer(target.framebuffer);
|
|
96
|
+
gl.deleteRenderbuffer(target.color);
|
|
97
|
+
gl.deleteRenderbuffer(target.depth);
|
|
98
|
+
gl.deleteFramebuffer(target.resolve);
|
|
99
|
+
gl.deleteTexture(target.texture);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function allocateMultisample(
|
|
103
|
+
gl: WebGL2RenderingContext,
|
|
104
|
+
texture: Texture3D,
|
|
105
|
+
old: MultisampleTarget | null,
|
|
106
|
+
samples: number,
|
|
107
|
+
depth: boolean,
|
|
108
|
+
depthWrite: boolean,
|
|
109
|
+
stencilWrite: number,
|
|
110
|
+
): MultisampleTarget {
|
|
111
|
+
const same = old?.samples === samples;
|
|
112
|
+
const newDepth = depth && (!same || !old.depth);
|
|
113
|
+
const framebuffer = same ? old.framebuffer : gl.createFramebuffer();
|
|
114
|
+
const color = same ? old.color : gl.createRenderbuffer();
|
|
115
|
+
const depthBuffer = newDepth ? gl.createRenderbuffer() : same ? old.depth : null;
|
|
116
|
+
const resolve = same ? old.resolve : texture.cube ? gl.createFramebuffer() : null;
|
|
117
|
+
const resolved = same ? old.texture : texture.cube ? gl.createTexture() : null;
|
|
118
|
+
try {
|
|
119
|
+
if (texture.cube && (!resolve || !resolved)) {
|
|
120
|
+
throw new Context3DError(
|
|
121
|
+
"operation",
|
|
122
|
+
"Context3D experiment: cannot allocate cube resolve target",
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
if (texture.cube && !same) {
|
|
126
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
127
|
+
gl.bindTexture(gl.TEXTURE_2D, resolved);
|
|
128
|
+
gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8, texture.width, texture.height);
|
|
129
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, resolve);
|
|
130
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, resolved, 0);
|
|
131
|
+
if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) {
|
|
132
|
+
throw new Context3DError(
|
|
133
|
+
"operation",
|
|
134
|
+
"Context3D experiment: incomplete cube resolve target",
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (!framebuffer || !color || (newDepth && !depthBuffer)) {
|
|
139
|
+
throw new Context3DError(
|
|
140
|
+
"operation",
|
|
141
|
+
"Context3D experiment: cannot allocate multisample target",
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
|
146
|
+
if (!same) {
|
|
147
|
+
gl.bindRenderbuffer(gl.RENDERBUFFER, color);
|
|
148
|
+
gl.renderbufferStorageMultisample(
|
|
149
|
+
gl.RENDERBUFFER,
|
|
150
|
+
samples,
|
|
151
|
+
gl.RGBA8,
|
|
152
|
+
texture.width,
|
|
153
|
+
texture.height,
|
|
154
|
+
);
|
|
155
|
+
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, color);
|
|
156
|
+
}
|
|
157
|
+
if (newDepth) {
|
|
158
|
+
gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer);
|
|
159
|
+
gl.renderbufferStorageMultisample(
|
|
160
|
+
gl.RENDERBUFFER,
|
|
161
|
+
samples,
|
|
162
|
+
gl.DEPTH24_STENCIL8,
|
|
163
|
+
texture.width,
|
|
164
|
+
texture.height,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
gl.framebufferRenderbuffer(
|
|
168
|
+
gl.FRAMEBUFFER,
|
|
169
|
+
gl.DEPTH_STENCIL_ATTACHMENT,
|
|
170
|
+
gl.RENDERBUFFER,
|
|
171
|
+
depth ? depthBuffer : null,
|
|
172
|
+
);
|
|
173
|
+
if (
|
|
174
|
+
(!same || newDepth) &&
|
|
175
|
+
gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE
|
|
176
|
+
) {
|
|
177
|
+
throw new Context3DError("operation", "Context3D experiment: incomplete multisample target");
|
|
178
|
+
}
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if (!same) {
|
|
181
|
+
gl.deleteFramebuffer(framebuffer);
|
|
182
|
+
gl.deleteRenderbuffer(color);
|
|
183
|
+
gl.deleteFramebuffer(resolve);
|
|
184
|
+
gl.deleteTexture(resolved);
|
|
185
|
+
} else {
|
|
186
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, old.framebuffer);
|
|
187
|
+
gl.framebufferRenderbuffer(
|
|
188
|
+
gl.FRAMEBUFFER,
|
|
189
|
+
gl.DEPTH_STENCIL_ATTACHMENT,
|
|
190
|
+
gl.RENDERBUFFER,
|
|
191
|
+
old.depthAttached ? old.depth : null,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
if (newDepth) {
|
|
195
|
+
gl.deleteRenderbuffer(depthBuffer);
|
|
196
|
+
}
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (newDepth) {
|
|
201
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
202
|
+
gl.depthMask(true);
|
|
203
|
+
gl.stencilMask(255);
|
|
204
|
+
gl.clearDepth(1);
|
|
205
|
+
gl.clearStencil(0);
|
|
206
|
+
gl.clear(gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
|
|
207
|
+
gl.depthMask(depthWrite);
|
|
208
|
+
gl.stencilMask(stencilWrite);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (same) {
|
|
212
|
+
old.depth = depthBuffer;
|
|
213
|
+
old.depthAttached = depth;
|
|
214
|
+
return old;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
framebuffer,
|
|
219
|
+
color,
|
|
220
|
+
depth: depthBuffer,
|
|
221
|
+
samples,
|
|
222
|
+
resolve,
|
|
223
|
+
texture: resolved,
|
|
224
|
+
valid: false,
|
|
225
|
+
dirty: false,
|
|
226
|
+
face: 0,
|
|
227
|
+
depthAttached: depth,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import type { Texture3D } from "../backend.js";
|
|
2
|
+
import { blockBytes, type DxtEncoder, decodeDxt, encodeDxt } from "../dxt.js";
|
|
3
|
+
import { Context3DError } from "../errors.js";
|
|
4
|
+
import { range } from "./state.js";
|
|
5
|
+
import type { MultisampleTarget } from "./targets.js";
|
|
6
|
+
|
|
7
|
+
export interface TextureState {
|
|
8
|
+
handle: WebGLTexture;
|
|
9
|
+
/** The internal format its storage has: RGBA8, or S3TC's for a compressed texture. */
|
|
10
|
+
storage: number;
|
|
11
|
+
/** RGBA staging for a level, made at the first upload that needs it: base-sized. */
|
|
12
|
+
bytes: Uint8Array;
|
|
13
|
+
uploaded: Uint16Array;
|
|
14
|
+
framebuffer: WebGLFramebuffer | null;
|
|
15
|
+
depthBuffer: WebGLRenderbuffer | null;
|
|
16
|
+
dirty: boolean;
|
|
17
|
+
face: number;
|
|
18
|
+
mipFramebuffer: WebGLFramebuffer | null;
|
|
19
|
+
depthAttached: boolean;
|
|
20
|
+
multisample: MultisampleTarget | null;
|
|
21
|
+
samples: number;
|
|
22
|
+
antiAlias: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The staging array, made base-sized at its first use: a texture given only blocks on S3TC has none. */
|
|
26
|
+
function staging(texture: Texture3D, state: TextureState): Uint8Array {
|
|
27
|
+
if (state.bytes.length === 0) {
|
|
28
|
+
state.bytes = new Uint8Array(texture.width * texture.height * 4);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return state.bytes;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The staged level uploaded: as it is, or a compressed texture's encoded to its blocks, as AIR compresses pixels. */
|
|
35
|
+
function uploadLevel(
|
|
36
|
+
gl: WebGL2RenderingContext,
|
|
37
|
+
texture: Texture3D,
|
|
38
|
+
state: TextureState,
|
|
39
|
+
level: number,
|
|
40
|
+
face: number,
|
|
41
|
+
w: number,
|
|
42
|
+
h: number,
|
|
43
|
+
encoder: DxtEncoder | null,
|
|
44
|
+
): void {
|
|
45
|
+
if (texture.format !== "bgra") {
|
|
46
|
+
const rgba = state.bytes.subarray(0, w * h * 4);
|
|
47
|
+
const dxt5 = texture.format === "compressedAlpha";
|
|
48
|
+
const blocks = encoder?.(rgba, dxt5, w, h) ?? encodeDxt(rgba, dxt5, w, h);
|
|
49
|
+
uploadCompressedLevel(gl, texture, state, blocks, level, face);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const cube = texture.cube;
|
|
54
|
+
const target = cube ? gl.TEXTURE_CUBE_MAP : gl.TEXTURE_2D;
|
|
55
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
56
|
+
gl.bindTexture(target, state.handle);
|
|
57
|
+
gl.texSubImage2D(
|
|
58
|
+
cube ? gl.TEXTURE_CUBE_MAP_POSITIVE_X + face : target,
|
|
59
|
+
level,
|
|
60
|
+
0,
|
|
61
|
+
0,
|
|
62
|
+
w,
|
|
63
|
+
h,
|
|
64
|
+
gl.RGBA,
|
|
65
|
+
gl.UNSIGNED_BYTE,
|
|
66
|
+
state.bytes,
|
|
67
|
+
);
|
|
68
|
+
state.uploaded[face] |= 1 << level;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function uploadTexture(
|
|
72
|
+
gl: WebGL2RenderingContext,
|
|
73
|
+
texture: Texture3D,
|
|
74
|
+
state: TextureState,
|
|
75
|
+
pixels: Uint32Array,
|
|
76
|
+
level: number,
|
|
77
|
+
face: number,
|
|
78
|
+
sourceWidth?: number,
|
|
79
|
+
sourceHeight?: number,
|
|
80
|
+
encoder: DxtEncoder | null = null,
|
|
81
|
+
): void {
|
|
82
|
+
const { width, height, levels, cube } = texture;
|
|
83
|
+
const faces = cube ? 6 : 1;
|
|
84
|
+
range(level, 1, levels);
|
|
85
|
+
range(face, 1, faces);
|
|
86
|
+
const w = Math.max(1, width >> level);
|
|
87
|
+
const h = Math.max(1, height >> level);
|
|
88
|
+
const sw = sourceWidth ?? w;
|
|
89
|
+
const sh = sourceHeight ?? h;
|
|
90
|
+
if (
|
|
91
|
+
!Number.isSafeInteger(sw) ||
|
|
92
|
+
sw <= 0 ||
|
|
93
|
+
!Number.isSafeInteger(sh) ||
|
|
94
|
+
sh <= 0 ||
|
|
95
|
+
pixels.length !== sw * sh
|
|
96
|
+
) {
|
|
97
|
+
throw new Context3DError("textureUpload", "Context3D experiment: texture upload size mismatch");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Made once the call is known good: a refused one allocates nothing.
|
|
101
|
+
staging(texture, state);
|
|
102
|
+
|
|
103
|
+
if (sw === w && sh === h) {
|
|
104
|
+
for (let i = 0; i < pixels.length; i++) {
|
|
105
|
+
const pixel = pixels[i];
|
|
106
|
+
const at = i * 4;
|
|
107
|
+
state.bytes[at] = pixel >>> 16;
|
|
108
|
+
state.bytes[at + 1] = pixel >>> 8;
|
|
109
|
+
state.bytes[at + 2] = pixel;
|
|
110
|
+
state.bytes[at + 3] = pixel >>> 24;
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
// AIR clips at the top-left and clears uncovered pixels, without scaling.
|
|
114
|
+
if (sw < w || sh < h) {
|
|
115
|
+
state.bytes.fill(0, 0, w * h * 4);
|
|
116
|
+
}
|
|
117
|
+
const columns = Math.min(w, sw);
|
|
118
|
+
const rows = Math.min(h, sh);
|
|
119
|
+
for (let y = 0; y < rows; y++) {
|
|
120
|
+
for (let x = 0; x < columns; x++) {
|
|
121
|
+
const pixel = pixels[y * sw + x];
|
|
122
|
+
const at = (y * w + x) * 4;
|
|
123
|
+
state.bytes[at] = pixel >>> 16;
|
|
124
|
+
state.bytes[at + 1] = pixel >>> 8;
|
|
125
|
+
state.bytes[at + 2] = pixel;
|
|
126
|
+
state.bytes[at + 3] = pixel >>> 24;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
uploadLevel(gl, texture, state, level, face, w, h, encoder);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function uploadTextureBytes(
|
|
135
|
+
gl: WebGL2RenderingContext,
|
|
136
|
+
texture: Texture3D,
|
|
137
|
+
state: TextureState,
|
|
138
|
+
data: Uint8Array,
|
|
139
|
+
offset: number,
|
|
140
|
+
level: number,
|
|
141
|
+
face: number,
|
|
142
|
+
encoder: DxtEncoder | null = null,
|
|
143
|
+
): void {
|
|
144
|
+
const { width, height, levels, cube } = texture;
|
|
145
|
+
const faces = cube ? 6 : 1;
|
|
146
|
+
range(level, 1, levels);
|
|
147
|
+
range(face, 1, faces);
|
|
148
|
+
const w = Math.max(1, width >> level);
|
|
149
|
+
const h = Math.max(1, height >> level);
|
|
150
|
+
const length = w * h * 4;
|
|
151
|
+
range(offset, length, data.length);
|
|
152
|
+
staging(texture, state);
|
|
153
|
+
|
|
154
|
+
for (let i = 0; i < length; i += 4) {
|
|
155
|
+
state.bytes[i] = data[offset + i + 2];
|
|
156
|
+
state.bytes[i + 1] = data[offset + i + 1];
|
|
157
|
+
state.bytes[i + 2] = data[offset + i];
|
|
158
|
+
state.bytes[i + 3] = data[offset + i + 3];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
uploadLevel(gl, texture, state, level, face, w, h, encoder);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* A compressed texture's level from S3TC blocks: as they are where its
|
|
166
|
+
* storage is S3TC's, or decoded to RGBA where the context takes no S3TC.
|
|
167
|
+
*/
|
|
168
|
+
export function uploadCompressedLevel(
|
|
169
|
+
gl: WebGL2RenderingContext,
|
|
170
|
+
texture: Texture3D,
|
|
171
|
+
state: TextureState,
|
|
172
|
+
blocks: Uint8Array,
|
|
173
|
+
level: number,
|
|
174
|
+
face: number,
|
|
175
|
+
): void {
|
|
176
|
+
const { width, height, levels, cube } = texture;
|
|
177
|
+
range(level, 1, levels);
|
|
178
|
+
range(face, 1, cube ? 6 : 1);
|
|
179
|
+
const w = Math.max(1, width >> level);
|
|
180
|
+
const h = Math.max(1, height >> level);
|
|
181
|
+
const dxt5 = texture.format === "compressedAlpha";
|
|
182
|
+
if (blocks.length !== blockBytes(w, h, dxt5)) {
|
|
183
|
+
throw new Context3DError(
|
|
184
|
+
"textureUpload",
|
|
185
|
+
"Context3D experiment: compressed level size mismatch",
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const target = cube ? gl.TEXTURE_CUBE_MAP_POSITIVE_X + face : gl.TEXTURE_2D;
|
|
190
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
191
|
+
gl.bindTexture(cube ? gl.TEXTURE_CUBE_MAP : gl.TEXTURE_2D, state.handle);
|
|
192
|
+
if (state.storage === gl.RGBA8) {
|
|
193
|
+
decodeDxt(blocks, dxt5, w, h, staging(texture, state));
|
|
194
|
+
gl.texSubImage2D(target, level, 0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, state.bytes);
|
|
195
|
+
} else {
|
|
196
|
+
gl.compressedTexSubImage2D(target, level, 0, 0, w, h, state.storage, blocks);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
state.uploaded[face] |= 1 << level;
|
|
200
|
+
}
|