@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,1803 @@
|
|
|
1
|
+
import type { ProgramType } from "../agal.js";
|
|
2
|
+
import {
|
|
3
|
+
type BlendFactor,
|
|
4
|
+
type Buffer3D,
|
|
5
|
+
type CompareMode,
|
|
6
|
+
type Context3D,
|
|
7
|
+
type CullFace,
|
|
8
|
+
type Program3D,
|
|
9
|
+
SAMPLER_FILTERS,
|
|
10
|
+
SAMPLER_MIPS,
|
|
11
|
+
SAMPLER_WRAPS,
|
|
12
|
+
type SamplerFilter,
|
|
13
|
+
type SamplerMip,
|
|
14
|
+
type SamplerWrap,
|
|
15
|
+
ScissorBoundsError,
|
|
16
|
+
type StencilAction,
|
|
17
|
+
type Texture3D,
|
|
18
|
+
TextureBindingError,
|
|
19
|
+
type TextureFormat,
|
|
20
|
+
VERTEX_FORMATS,
|
|
21
|
+
type VertexFormat,
|
|
22
|
+
VertexOffsetError,
|
|
23
|
+
} from "../backend.js";
|
|
24
|
+
import { blockBytes, type DxtEncoder } from "../dxt.js";
|
|
25
|
+
import { Context3DError } from "../errors.js";
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
type BufferState,
|
|
29
|
+
uploadBuffer,
|
|
30
|
+
uploadBufferBytes,
|
|
31
|
+
type VertexBinding,
|
|
32
|
+
} from "./buffers.js";
|
|
33
|
+
import { resourceLimits, type WebGLContext3DLimits } from "./limits.js";
|
|
34
|
+
import {
|
|
35
|
+
type CopyProgram,
|
|
36
|
+
createCopyProgram,
|
|
37
|
+
type ProgramState,
|
|
38
|
+
uploadProgram,
|
|
39
|
+
} from "./programs.js";
|
|
40
|
+
import {
|
|
41
|
+
BLEND_FUNCTIONS,
|
|
42
|
+
DEPTH_FUNCTIONS,
|
|
43
|
+
range,
|
|
44
|
+
STENCIL_FUNCTIONS,
|
|
45
|
+
StencilState,
|
|
46
|
+
} from "./state.js";
|
|
47
|
+
import {
|
|
48
|
+
allocateMultisample,
|
|
49
|
+
deleteMultisample,
|
|
50
|
+
type MultisampleTarget,
|
|
51
|
+
resolveTarget,
|
|
52
|
+
selectSamples,
|
|
53
|
+
targetFramebuffer,
|
|
54
|
+
} from "./targets.js";
|
|
55
|
+
import {
|
|
56
|
+
type TextureState,
|
|
57
|
+
uploadCompressedLevel,
|
|
58
|
+
uploadTexture,
|
|
59
|
+
uploadTextureBytes,
|
|
60
|
+
} from "./textures.js";
|
|
61
|
+
import { validateIndices, validateVertices } from "./validation.js";
|
|
62
|
+
|
|
63
|
+
/** What AGAL's sampler must name to read each texture format: RGBA, DXT1 or DXT5. */
|
|
64
|
+
const SAMPLER_FORMATS: Record<TextureFormat, number> = {
|
|
65
|
+
bgra: 0,
|
|
66
|
+
compressed: 1,
|
|
67
|
+
compressedAlpha: 2,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** An owned WebGL2 context, deliberately independent of the 2D renderer. */
|
|
71
|
+
export class WebGLContext3D implements Context3D {
|
|
72
|
+
readonly gl: WebGL2RenderingContext;
|
|
73
|
+
width = 0;
|
|
74
|
+
height = 0;
|
|
75
|
+
disposed = false;
|
|
76
|
+
samplerFormats = true;
|
|
77
|
+
private readonly buffers = new Map<Buffer3D, BufferState>();
|
|
78
|
+
private vertexBuffers = 0;
|
|
79
|
+
private bufferBytes = 0;
|
|
80
|
+
private vertexBufferBytes = 0;
|
|
81
|
+
private readonly programs = new Map<Program3D, ProgramState>();
|
|
82
|
+
private readonly textures = new Map<Texture3D, TextureState>();
|
|
83
|
+
private readonly textureCounts = new Float64Array(3);
|
|
84
|
+
private readonly textureQuotaBytes = new Float64Array(3);
|
|
85
|
+
private readonly textureOwners = new WeakSet<Texture3D>();
|
|
86
|
+
private textureBytes = 0;
|
|
87
|
+
private targetDepthBytes = 0;
|
|
88
|
+
private targetColorBytes = 0;
|
|
89
|
+
private copy2D: CopyProgram | null = null;
|
|
90
|
+
private copyCube: CopyProgram | null = null;
|
|
91
|
+
private readonly targetSamples = new Int8Array(10).fill(-1);
|
|
92
|
+
private renderTexture: Texture3D | null = null;
|
|
93
|
+
private renderDepth = false;
|
|
94
|
+
private renderFace = 0;
|
|
95
|
+
private readonly textureSlots: (Texture3D | null)[] = new Array(8).fill(null);
|
|
96
|
+
private textureMask = 0;
|
|
97
|
+
private readonly samplers: (WebGLSampler | null)[] = new Array(96).fill(null);
|
|
98
|
+
// Two wrap bits, three filter bits and two mip bits fit each slot in one byte.
|
|
99
|
+
private readonly samplerStates = new Uint8Array(16);
|
|
100
|
+
private anisotropy: EXT_texture_filter_anisotropic | null | undefined;
|
|
101
|
+
private s3tc: WEBGL_compressed_texture_s3tc | null | undefined;
|
|
102
|
+
private maxAnisotropy = 1;
|
|
103
|
+
private readonly vertices: (VertexBinding | null)[] = new Array(8).fill(null);
|
|
104
|
+
private selected: Program3D | null = null;
|
|
105
|
+
private framebuffer: WebGLFramebuffer | null = null;
|
|
106
|
+
private texture: WebGLTexture | null = null;
|
|
107
|
+
private resolveFramebuffer: WebGLFramebuffer | null = null;
|
|
108
|
+
private colorBuffer: WebGLRenderbuffer | null = null;
|
|
109
|
+
private resolveDirty = false;
|
|
110
|
+
private depthBuffer: WebGLRenderbuffer | null = null;
|
|
111
|
+
private antiAlias = 0;
|
|
112
|
+
private depthWrite = true;
|
|
113
|
+
private blendEnabled = false;
|
|
114
|
+
private cullEnabled = false;
|
|
115
|
+
private colorWrite = 15;
|
|
116
|
+
private readonly stencilFront = new StencilState();
|
|
117
|
+
private readonly stencilBack = new StencilState();
|
|
118
|
+
private stencilEnabled = false;
|
|
119
|
+
private stencilReference = 0;
|
|
120
|
+
private stencilRead = 255;
|
|
121
|
+
private stencilWrite = 255;
|
|
122
|
+
private scissor = false;
|
|
123
|
+
private scissorLeft = 0;
|
|
124
|
+
private scissorTop = 0;
|
|
125
|
+
private scissorRight = 0;
|
|
126
|
+
private scissorBottom = 0;
|
|
127
|
+
private scissorOutside = false;
|
|
128
|
+
private readonly vertexConstants = new Float32Array(128 * 4);
|
|
129
|
+
private readonly fragmentConstants = new Float32Array(28 * 4);
|
|
130
|
+
private pixels = new Uint8Array(0);
|
|
131
|
+
private topDown = new Uint8Array(0);
|
|
132
|
+
private readonly vao: WebGLVertexArrayObject;
|
|
133
|
+
private readonly limits: Readonly<WebGLContext3DLimits>;
|
|
134
|
+
private dxtEncoder: DxtEncoder | null = null;
|
|
135
|
+
|
|
136
|
+
/** The host's whole-level encoder; a refusal retains the TypeScript path. */
|
|
137
|
+
setDxtEncoder(encoder: DxtEncoder): void {
|
|
138
|
+
this.dxtEncoder = encoder;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
constructor(gl: WebGL2RenderingContext, limits: Partial<WebGLContext3DLimits> = {}) {
|
|
142
|
+
this.limits = resourceLimits(limits);
|
|
143
|
+
this.gl = gl;
|
|
144
|
+
if (gl.getContextAttributes()?.antialias) {
|
|
145
|
+
throw new Context3DError(
|
|
146
|
+
"unsupported",
|
|
147
|
+
"Context3D requires a WebGL2 context with antialias: false",
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const vao = gl.createVertexArray();
|
|
152
|
+
if (!vao) {
|
|
153
|
+
throw new Context3DError("operation", "Context3D experiment: cannot create vertex array");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
this.vao = vao;
|
|
157
|
+
gl.bindVertexArray(vao);
|
|
158
|
+
gl.enable(gl.DEPTH_TEST);
|
|
159
|
+
gl.depthFunc(gl.LESS);
|
|
160
|
+
gl.depthMask(true);
|
|
161
|
+
gl.colorMask(true, true, true, true);
|
|
162
|
+
gl.disable(gl.STENCIL_TEST);
|
|
163
|
+
gl.stencilFunc(gl.ALWAYS, 0, 255);
|
|
164
|
+
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
|
|
165
|
+
gl.stencilMask(255);
|
|
166
|
+
gl.disable(gl.BLEND);
|
|
167
|
+
gl.disable(gl.CULL_FACE);
|
|
168
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
169
|
+
// Stage3D front faces are clockwise in clip-space XY.
|
|
170
|
+
gl.frontFace(gl.CW);
|
|
171
|
+
gl.disable(gl.DITHER);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private alive(): void {
|
|
175
|
+
if (this.disposed || this.gl.isContextLost()) {
|
|
176
|
+
throw new Context3DError("disposed", "Context3D experiment: context disposed or lost");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private target(back = false, discardColor = false): void {
|
|
181
|
+
this.alive();
|
|
182
|
+
if (!this.framebuffer) {
|
|
183
|
+
throw new Context3DError(
|
|
184
|
+
"backBuffer",
|
|
185
|
+
"Context3D experiment: configure the back buffer first",
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const texture = back ? null : this.renderTexture;
|
|
190
|
+
const state = texture && this.textureState(texture);
|
|
191
|
+
if (texture && state?.samples && state.multisample && !state.multisample.valid) {
|
|
192
|
+
if (discardColor) {
|
|
193
|
+
state.multisample.valid = true;
|
|
194
|
+
} else {
|
|
195
|
+
this.seedTarget(texture, state);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
this.gl.bindFramebuffer(
|
|
199
|
+
this.gl.FRAMEBUFFER,
|
|
200
|
+
state ? targetFramebuffer(state) : this.framebuffer,
|
|
201
|
+
);
|
|
202
|
+
this.gl.viewport(0, 0, texture?.width ?? this.width, texture?.height ?? this.height);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
configure(width: number, height: number, depth = false, antiAlias = 0): void {
|
|
206
|
+
this.alive();
|
|
207
|
+
range(32, width - 32, 2048);
|
|
208
|
+
range(32, height - 32, 2048);
|
|
209
|
+
if (this.renderTexture) {
|
|
210
|
+
throw new Context3DError(
|
|
211
|
+
"operation",
|
|
212
|
+
"Context3D experiment: resize while rendering to texture",
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// AIR preserves color, depth and bindings when the request is unchanged.
|
|
217
|
+
// Shadow passes can issue this every frame when returning to the back buffer.
|
|
218
|
+
if (
|
|
219
|
+
this.framebuffer &&
|
|
220
|
+
width === this.width &&
|
|
221
|
+
height === this.height &&
|
|
222
|
+
depth === (this.depthBuffer !== null) &&
|
|
223
|
+
antiAlias === this.antiAlias
|
|
224
|
+
) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const samples = selectSamples(this.gl, antiAlias, depth);
|
|
229
|
+
const gl = this.gl;
|
|
230
|
+
const texture = gl.createTexture();
|
|
231
|
+
const framebuffer = gl.createFramebuffer();
|
|
232
|
+
const depthBuffer = depth ? gl.createRenderbuffer() : null;
|
|
233
|
+
const resolveFramebuffer = samples ? gl.createFramebuffer() : null;
|
|
234
|
+
const colorBuffer = samples ? gl.createRenderbuffer() : null;
|
|
235
|
+
try {
|
|
236
|
+
if (
|
|
237
|
+
!texture ||
|
|
238
|
+
!framebuffer ||
|
|
239
|
+
(depth && !depthBuffer) ||
|
|
240
|
+
(samples && (!resolveFramebuffer || !colorBuffer))
|
|
241
|
+
) {
|
|
242
|
+
throw new Context3DError("operation", "Context3D experiment: cannot allocate back buffer");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
246
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
247
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
248
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
|
249
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, resolveFramebuffer ?? framebuffer);
|
|
250
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
|
|
251
|
+
if (resolveFramebuffer) {
|
|
252
|
+
if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) {
|
|
253
|
+
throw new Context3DError("operation", "Context3D experiment: incomplete resolve buffer");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
|
257
|
+
gl.bindRenderbuffer(gl.RENDERBUFFER, colorBuffer);
|
|
258
|
+
gl.renderbufferStorageMultisample(gl.RENDERBUFFER, samples, gl.RGBA8, width, height);
|
|
259
|
+
gl.framebufferRenderbuffer(
|
|
260
|
+
gl.FRAMEBUFFER,
|
|
261
|
+
gl.COLOR_ATTACHMENT0,
|
|
262
|
+
gl.RENDERBUFFER,
|
|
263
|
+
colorBuffer,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
if (depthBuffer) {
|
|
267
|
+
gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer);
|
|
268
|
+
if (samples) {
|
|
269
|
+
gl.renderbufferStorageMultisample(
|
|
270
|
+
gl.RENDERBUFFER,
|
|
271
|
+
samples,
|
|
272
|
+
gl.DEPTH24_STENCIL8,
|
|
273
|
+
width,
|
|
274
|
+
height,
|
|
275
|
+
);
|
|
276
|
+
} else {
|
|
277
|
+
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH24_STENCIL8, width, height);
|
|
278
|
+
}
|
|
279
|
+
gl.framebufferRenderbuffer(
|
|
280
|
+
gl.FRAMEBUFFER,
|
|
281
|
+
gl.DEPTH_STENCIL_ATTACHMENT,
|
|
282
|
+
gl.RENDERBUFFER,
|
|
283
|
+
depthBuffer,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
|
|
287
|
+
if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) {
|
|
288
|
+
throw new Context3DError("operation", "Context3D experiment: incomplete back buffer");
|
|
289
|
+
}
|
|
290
|
+
} catch (error) {
|
|
291
|
+
gl.deleteTexture(texture);
|
|
292
|
+
gl.deleteFramebuffer(framebuffer);
|
|
293
|
+
gl.deleteFramebuffer(resolveFramebuffer);
|
|
294
|
+
gl.deleteRenderbuffer(depthBuffer);
|
|
295
|
+
gl.deleteRenderbuffer(colorBuffer);
|
|
296
|
+
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
|
|
297
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
gl.deleteTexture(this.texture);
|
|
302
|
+
gl.deleteFramebuffer(this.framebuffer);
|
|
303
|
+
gl.deleteFramebuffer(this.resolveFramebuffer);
|
|
304
|
+
gl.deleteRenderbuffer(this.depthBuffer);
|
|
305
|
+
gl.deleteRenderbuffer(this.colorBuffer);
|
|
306
|
+
this.texture = texture;
|
|
307
|
+
this.framebuffer = framebuffer;
|
|
308
|
+
this.resolveFramebuffer = resolveFramebuffer;
|
|
309
|
+
this.colorBuffer = colorBuffer;
|
|
310
|
+
this.depthBuffer = depthBuffer;
|
|
311
|
+
this.antiAlias = antiAlias;
|
|
312
|
+
this.width = width;
|
|
313
|
+
this.height = height;
|
|
314
|
+
gl.canvas.width = width;
|
|
315
|
+
gl.canvas.height = height;
|
|
316
|
+
this.pixels = new Uint8Array(width * height * 4);
|
|
317
|
+
this.topDown = new Uint8Array(this.pixels.length);
|
|
318
|
+
this.applyScissor();
|
|
319
|
+
this.clear(0, 0, 0, 1);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
clear(
|
|
323
|
+
red: number,
|
|
324
|
+
green: number,
|
|
325
|
+
blue: number,
|
|
326
|
+
alpha: number,
|
|
327
|
+
depth = 1,
|
|
328
|
+
mask = 0xffffffff,
|
|
329
|
+
stencil = 0,
|
|
330
|
+
): void {
|
|
331
|
+
this.target(false, (mask & 1) !== 0);
|
|
332
|
+
const gl = this.gl;
|
|
333
|
+
let bits = 0;
|
|
334
|
+
if (mask & 1) {
|
|
335
|
+
gl.clearColor(red, green, blue, alpha);
|
|
336
|
+
bits |= gl.COLOR_BUFFER_BIT;
|
|
337
|
+
this.changedTarget();
|
|
338
|
+
}
|
|
339
|
+
if (mask & 2) {
|
|
340
|
+
gl.clearDepth(depth);
|
|
341
|
+
bits |= gl.DEPTH_BUFFER_BIT;
|
|
342
|
+
}
|
|
343
|
+
if (mask & 4) {
|
|
344
|
+
gl.clearStencil(Math.min(stencil >>> 0, 255));
|
|
345
|
+
bits |= gl.STENCIL_BUFFER_BIT;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Flash clear writes depth even while draw-time depth writes are masked.
|
|
349
|
+
const hasDepth = this.renderTexture ? this.renderDepth : this.depthBuffer !== null;
|
|
350
|
+
const overrideDepth = hasDepth && (mask & 2) !== 0 && !this.depthWrite;
|
|
351
|
+
if (overrideDepth) {
|
|
352
|
+
gl.depthMask(true);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const overrideStencil = hasDepth && (mask & 4) !== 0 && this.stencilWrite !== 255;
|
|
356
|
+
if (overrideStencil) {
|
|
357
|
+
gl.stencilMask(255);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// AIR clears every color channel regardless of the draw-time mask.
|
|
361
|
+
const overrideColor = (mask & 1) !== 0 && this.colorWrite !== 15;
|
|
362
|
+
if (overrideColor) {
|
|
363
|
+
gl.colorMask(true, true, true, true);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (this.scissor) {
|
|
367
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
368
|
+
}
|
|
369
|
+
gl.clear(bits);
|
|
370
|
+
if (this.scissor) {
|
|
371
|
+
gl.enable(gl.SCISSOR_TEST);
|
|
372
|
+
}
|
|
373
|
+
if (overrideDepth) {
|
|
374
|
+
gl.depthMask(false);
|
|
375
|
+
}
|
|
376
|
+
if (overrideColor) {
|
|
377
|
+
this.applyColorMask();
|
|
378
|
+
}
|
|
379
|
+
if (overrideStencil) {
|
|
380
|
+
gl.stencilMask(this.stencilWrite);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
setStencilActions(
|
|
385
|
+
face: CullFace,
|
|
386
|
+
compare: CompareMode,
|
|
387
|
+
pass: StencilAction,
|
|
388
|
+
depthFail: StencilAction,
|
|
389
|
+
stencilFail: StencilAction,
|
|
390
|
+
): void {
|
|
391
|
+
this.alive();
|
|
392
|
+
const gl = this.gl;
|
|
393
|
+
if (face === "front" || face === "frontAndBack") {
|
|
394
|
+
this.stencilFace(gl.FRONT, this.stencilFront, compare, pass, depthFail, stencilFail);
|
|
395
|
+
}
|
|
396
|
+
if (face === "back" || face === "frontAndBack") {
|
|
397
|
+
this.stencilFace(gl.BACK, this.stencilBack, compare, pass, depthFail, stencilFail);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const enabled = this.stencilFront.active || this.stencilBack.active;
|
|
401
|
+
if (enabled !== this.stencilEnabled) {
|
|
402
|
+
this.stencilEnabled = enabled;
|
|
403
|
+
if (enabled) {
|
|
404
|
+
gl.enable(gl.STENCIL_TEST);
|
|
405
|
+
} else {
|
|
406
|
+
gl.disable(gl.STENCIL_TEST);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
private stencilFace(
|
|
412
|
+
face: number,
|
|
413
|
+
state: StencilState,
|
|
414
|
+
compare: CompareMode,
|
|
415
|
+
pass: StencilAction,
|
|
416
|
+
depthFail: StencilAction,
|
|
417
|
+
stencilFail: StencilAction,
|
|
418
|
+
): void {
|
|
419
|
+
const gl = this.gl;
|
|
420
|
+
if (state.compare !== compare) {
|
|
421
|
+
state.compare = compare;
|
|
422
|
+
gl.stencilFuncSeparate(
|
|
423
|
+
face,
|
|
424
|
+
gl[DEPTH_FUNCTIONS[compare]],
|
|
425
|
+
this.stencilReference,
|
|
426
|
+
this.stencilRead,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
if (state.pass !== pass || state.depthFail !== depthFail || state.stencilFail !== stencilFail) {
|
|
430
|
+
state.pass = pass;
|
|
431
|
+
state.depthFail = depthFail;
|
|
432
|
+
state.stencilFail = stencilFail;
|
|
433
|
+
gl.stencilOpSeparate(
|
|
434
|
+
face,
|
|
435
|
+
gl[STENCIL_FUNCTIONS[stencilFail]],
|
|
436
|
+
gl[STENCIL_FUNCTIONS[depthFail]],
|
|
437
|
+
gl[STENCIL_FUNCTIONS[pass]],
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
setStencilReference(reference: number, readMask: number, writeMask: number): void {
|
|
443
|
+
this.alive();
|
|
444
|
+
reference &= 255;
|
|
445
|
+
readMask &= 255;
|
|
446
|
+
writeMask &= 255;
|
|
447
|
+
const gl = this.gl;
|
|
448
|
+
if (reference !== this.stencilReference || readMask !== this.stencilRead) {
|
|
449
|
+
this.stencilReference = reference;
|
|
450
|
+
this.stencilRead = readMask;
|
|
451
|
+
gl.stencilFuncSeparate(
|
|
452
|
+
gl.FRONT,
|
|
453
|
+
gl[DEPTH_FUNCTIONS[this.stencilFront.compare]],
|
|
454
|
+
reference,
|
|
455
|
+
readMask,
|
|
456
|
+
);
|
|
457
|
+
gl.stencilFuncSeparate(
|
|
458
|
+
gl.BACK,
|
|
459
|
+
gl[DEPTH_FUNCTIONS[this.stencilBack.compare]],
|
|
460
|
+
reference,
|
|
461
|
+
readMask,
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
if (writeMask !== this.stencilWrite) {
|
|
465
|
+
this.stencilWrite = writeMask;
|
|
466
|
+
gl.stencilMask(writeMask);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
setColorMask(red: boolean, green: boolean, blue: boolean, alpha: boolean): void {
|
|
471
|
+
this.alive();
|
|
472
|
+
const mask = (red ? 1 : 0) | (green ? 2 : 0) | (blue ? 4 : 0) | (alpha ? 8 : 0);
|
|
473
|
+
if (mask === this.colorWrite) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
this.colorWrite = mask;
|
|
478
|
+
this.applyColorMask();
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private applyColorMask(): void {
|
|
482
|
+
const mask = this.colorWrite;
|
|
483
|
+
this.gl.colorMask((mask & 1) !== 0, (mask & 2) !== 0, (mask & 4) !== 0, (mask & 8) !== 0);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
setDepth(write: boolean, compare: CompareMode): void {
|
|
487
|
+
this.alive();
|
|
488
|
+
this.depthWrite = write;
|
|
489
|
+
this.gl.depthMask(write);
|
|
490
|
+
this.gl.depthFunc(this.gl[DEPTH_FUNCTIONS[compare]]);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
setScissor(x: number | null, y = 0, width = 0, height = 0): void {
|
|
494
|
+
this.alive();
|
|
495
|
+
if (
|
|
496
|
+
x !== null &&
|
|
497
|
+
(!Number.isFinite(x) ||
|
|
498
|
+
!Number.isFinite(y) ||
|
|
499
|
+
!Number.isFinite(width) ||
|
|
500
|
+
!Number.isFinite(height))
|
|
501
|
+
) {
|
|
502
|
+
throw new Context3DError("operation", "Context3D experiment: non-finite scissor rectangle");
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
this.scissor = x !== null;
|
|
506
|
+
if (x !== null) {
|
|
507
|
+
// AIR rounds the edges, not the width/height, and copies the rectangle.
|
|
508
|
+
this.scissorLeft = Math.floor(x + 0.5);
|
|
509
|
+
this.scissorTop = Math.floor(y + 0.5);
|
|
510
|
+
this.scissorRight = Math.floor(x + width + 0.5);
|
|
511
|
+
this.scissorBottom = Math.floor(y + height + 0.5);
|
|
512
|
+
this.scissor = this.scissorRight > this.scissorLeft && this.scissorBottom > this.scissorTop;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
this.applyScissor();
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
private applyScissor(): void {
|
|
519
|
+
const gl = this.gl;
|
|
520
|
+
if (!this.scissor) {
|
|
521
|
+
this.scissorOutside = false;
|
|
522
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const width = this.renderTexture?.width ?? this.width;
|
|
527
|
+
const height = this.renderTexture?.height ?? this.height;
|
|
528
|
+
const left = Math.max(0, Math.min(width, this.scissorLeft));
|
|
529
|
+
const top = Math.max(0, Math.min(height, this.scissorTop));
|
|
530
|
+
const right = Math.max(0, Math.min(width, this.scissorRight));
|
|
531
|
+
const bottom = Math.max(0, Math.min(height, this.scissorBottom));
|
|
532
|
+
this.scissorOutside = right <= left || bottom <= top;
|
|
533
|
+
gl.enable(gl.SCISSOR_TEST);
|
|
534
|
+
gl.scissor(
|
|
535
|
+
left,
|
|
536
|
+
this.renderTexture ? top : height - bottom,
|
|
537
|
+
Math.max(0, right - left),
|
|
538
|
+
Math.max(0, bottom - top),
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
setBlend(source: BlendFactor, destination: BlendFactor): void {
|
|
543
|
+
this.alive();
|
|
544
|
+
const gl = this.gl;
|
|
545
|
+
this.blendEnabled = source !== "one" || destination !== "zero";
|
|
546
|
+
if (source === "one" && destination === "zero") {
|
|
547
|
+
gl.disable(gl.BLEND);
|
|
548
|
+
} else {
|
|
549
|
+
gl.blendFunc(gl[BLEND_FUNCTIONS[source]], gl[BLEND_FUNCTIONS[destination]]);
|
|
550
|
+
gl.enable(gl.BLEND);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
setCulling(face: CullFace): void {
|
|
555
|
+
this.alive();
|
|
556
|
+
const gl = this.gl;
|
|
557
|
+
this.cullEnabled = face !== "none";
|
|
558
|
+
if (face === "none") {
|
|
559
|
+
gl.disable(gl.CULL_FACE);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
gl.enable(gl.CULL_FACE);
|
|
564
|
+
switch (face) {
|
|
565
|
+
case "back":
|
|
566
|
+
gl.cullFace(gl.BACK);
|
|
567
|
+
break;
|
|
568
|
+
case "front":
|
|
569
|
+
gl.cullFace(gl.FRONT);
|
|
570
|
+
break;
|
|
571
|
+
case "frontAndBack":
|
|
572
|
+
gl.cullFace(gl.FRONT_AND_BACK);
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
createBuffer(count: number, stride: number): Buffer3D {
|
|
578
|
+
this.alive();
|
|
579
|
+
range(1, count - 1, stride === 0 ? 524287 : 65535);
|
|
580
|
+
range(0, stride, 64);
|
|
581
|
+
const countLimit = stride === 0 ? this.limits.maxIndexBuffers : this.limits.maxVertexBuffers;
|
|
582
|
+
const used = stride === 0 ? this.buffers.size - this.vertexBuffers : this.vertexBuffers;
|
|
583
|
+
if (this.buffers.size >= this.limits.maxBuffers || used >= countLimit) {
|
|
584
|
+
throw new Context3DError("limit", "Context3D experiment: buffer limit");
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const bytes = count * (stride === 0 ? 2 : stride * 4);
|
|
588
|
+
const usedBytes =
|
|
589
|
+
stride === 0 ? this.bufferBytes - this.vertexBufferBytes : this.vertexBufferBytes;
|
|
590
|
+
const byteLimit =
|
|
591
|
+
stride === 0 ? this.limits.maxIndexBufferBytes : this.limits.maxVertexBufferBytes;
|
|
592
|
+
if (this.bufferBytes + bytes > this.limits.maxBufferBytes || usedBytes + bytes > byteLimit) {
|
|
593
|
+
throw new Context3DError("limit", "Context3D experiment: buffer memory limit");
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const gl = this.gl;
|
|
597
|
+
const handle = gl.createBuffer();
|
|
598
|
+
if (!handle) {
|
|
599
|
+
throw new Context3DError("bufferAllocation", "Context3D experiment: cannot allocate buffer");
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const target = stride === 0 ? gl.ELEMENT_ARRAY_BUFFER : gl.ARRAY_BUFFER;
|
|
603
|
+
gl.bindVertexArray(this.vao);
|
|
604
|
+
gl.bindBuffer(target, handle);
|
|
605
|
+
gl.bufferData(target, bytes, gl.STATIC_DRAW);
|
|
606
|
+
const state: BufferState = {
|
|
607
|
+
handle,
|
|
608
|
+
indices: stride === 0 ? new Uint16Array(count) : null,
|
|
609
|
+
uploaded: new Uint8Array(count),
|
|
610
|
+
prefix: 0,
|
|
611
|
+
version: 0,
|
|
612
|
+
checkedVersion: -1,
|
|
613
|
+
checkedFirst: 0,
|
|
614
|
+
checkedCount: 0,
|
|
615
|
+
maximum: -1,
|
|
616
|
+
};
|
|
617
|
+
const buffer: Buffer3D = {
|
|
618
|
+
count,
|
|
619
|
+
stride,
|
|
620
|
+
upload: (values, start, amount) => {
|
|
621
|
+
this.alive();
|
|
622
|
+
this.buffer(buffer);
|
|
623
|
+
uploadBuffer(gl, this.vao, buffer, state, values, start, amount);
|
|
624
|
+
},
|
|
625
|
+
uploadBytes: (data, offset, start, amount) => {
|
|
626
|
+
this.alive();
|
|
627
|
+
this.buffer(buffer);
|
|
628
|
+
uploadBufferBytes(gl, this.vao, buffer, state, data, offset, start, amount);
|
|
629
|
+
},
|
|
630
|
+
dispose: () => {
|
|
631
|
+
if (!this.buffers.delete(buffer)) {
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
gl.deleteBuffer(handle);
|
|
636
|
+
this.bufferBytes -= bytes;
|
|
637
|
+
if (stride !== 0) {
|
|
638
|
+
this.vertexBuffers--;
|
|
639
|
+
this.vertexBufferBytes -= bytes;
|
|
640
|
+
}
|
|
641
|
+
state.indices = null;
|
|
642
|
+
state.uploaded = new Uint8Array(0);
|
|
643
|
+
for (let i = 0; i < this.vertices.length; i++) {
|
|
644
|
+
const binding = this.vertices[i];
|
|
645
|
+
if (binding?.buffer === buffer) {
|
|
646
|
+
this.vertices[i] = null;
|
|
647
|
+
} else if (binding?.checkedIndex === state) {
|
|
648
|
+
binding.checkedIndex = null;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
},
|
|
652
|
+
};
|
|
653
|
+
this.buffers.set(buffer, state);
|
|
654
|
+
if (stride !== 0) {
|
|
655
|
+
this.vertexBuffers++;
|
|
656
|
+
this.vertexBufferBytes += bytes;
|
|
657
|
+
}
|
|
658
|
+
this.bufferBytes += bytes;
|
|
659
|
+
return buffer;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
private buffer(buffer: Buffer3D): BufferState {
|
|
663
|
+
const state = this.buffers.get(buffer);
|
|
664
|
+
if (!state) {
|
|
665
|
+
throw new Context3DError(
|
|
666
|
+
"disposed",
|
|
667
|
+
"Context3D experiment: buffer disposed or from another context",
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return state;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
createProgram(): Program3D {
|
|
675
|
+
this.alive();
|
|
676
|
+
if (this.programs.size >= this.limits.maxPrograms) {
|
|
677
|
+
throw new Context3DError("limit", "Context3D experiment: program limit");
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const state: ProgramState = {
|
|
681
|
+
handle: null,
|
|
682
|
+
targetY: null,
|
|
683
|
+
attributes: [],
|
|
684
|
+
vertex: [],
|
|
685
|
+
vertexBank: null,
|
|
686
|
+
fragment: [],
|
|
687
|
+
samplers: [],
|
|
688
|
+
samplerMask: 0,
|
|
689
|
+
};
|
|
690
|
+
const program: Program3D = {
|
|
691
|
+
upload: (vertex, fragment) => {
|
|
692
|
+
this.alive();
|
|
693
|
+
this.program(program);
|
|
694
|
+
uploadProgram(this.gl, state, vertex, fragment);
|
|
695
|
+
|
|
696
|
+
if (this.selected === program) {
|
|
697
|
+
this.setProgram(program);
|
|
698
|
+
}
|
|
699
|
+
},
|
|
700
|
+
dispose: () => {
|
|
701
|
+
if (!this.programs.delete(program)) {
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// WebGL keeps a deleted program alive while it is still current.
|
|
706
|
+
if (this.gl.getParameter(this.gl.CURRENT_PROGRAM) === state.handle) {
|
|
707
|
+
this.gl.useProgram(null);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
this.gl.deleteProgram(state.handle);
|
|
711
|
+
state.handle = null;
|
|
712
|
+
state.targetY = null;
|
|
713
|
+
state.attributes.length = 0;
|
|
714
|
+
state.vertex.length = 0;
|
|
715
|
+
state.vertexBank = null;
|
|
716
|
+
state.fragment.length = 0;
|
|
717
|
+
state.samplers.length = 0;
|
|
718
|
+
state.samplerMask = 0;
|
|
719
|
+
if (this.selected === program) {
|
|
720
|
+
this.selected = null;
|
|
721
|
+
}
|
|
722
|
+
},
|
|
723
|
+
};
|
|
724
|
+
this.programs.set(program, state);
|
|
725
|
+
return program;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
private program(program: Program3D): ProgramState {
|
|
729
|
+
const state = this.programs.get(program);
|
|
730
|
+
if (!state) {
|
|
731
|
+
throw new Context3DError(
|
|
732
|
+
"disposed",
|
|
733
|
+
"Context3D experiment: program disposed or from another context",
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
return state;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
createTexture(
|
|
741
|
+
width: number,
|
|
742
|
+
height: number,
|
|
743
|
+
cube = false,
|
|
744
|
+
format: TextureFormat = "bgra",
|
|
745
|
+
): Texture3D {
|
|
746
|
+
return this.allocateTexture(width, height, cube, false, format);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
createRectangleTexture(width: number, height: number): Texture3D {
|
|
750
|
+
return this.allocateTexture(width, height, false, true, "bgra");
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/** GL's internal format for a compressed texture's S3TC, or 0 where the context takes none. */
|
|
754
|
+
private s3tcFormat(format: TextureFormat): number {
|
|
755
|
+
if (this.s3tc === undefined) {
|
|
756
|
+
this.s3tc = this.gl.getExtension("WEBGL_compressed_texture_s3tc");
|
|
757
|
+
}
|
|
758
|
+
if (!this.s3tc || format === "bgra") {
|
|
759
|
+
return 0;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// AIR's DXT1 has DXT1's one bit of alpha: a block of three colours has transparent black.
|
|
763
|
+
return format === "compressed"
|
|
764
|
+
? this.s3tc.COMPRESSED_RGBA_S3TC_DXT1_EXT
|
|
765
|
+
: this.s3tc.COMPRESSED_RGBA_S3TC_DXT5_EXT;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Bytes a mip chain of `format` takes, its faces' every level: S3TC's
|
|
770
|
+
* blocks for a compressed texture whatever its storage, as AIR counts
|
|
771
|
+
* them, so that a GPU without S3TC allocates no less than one with it.
|
|
772
|
+
*/
|
|
773
|
+
private chainBytes(
|
|
774
|
+
width: number,
|
|
775
|
+
height: number,
|
|
776
|
+
levels: number,
|
|
777
|
+
faces: number,
|
|
778
|
+
format: TextureFormat,
|
|
779
|
+
): number {
|
|
780
|
+
let size = 0;
|
|
781
|
+
for (let level = 0; level < levels; level++) {
|
|
782
|
+
const w = Math.max(1, width >> level);
|
|
783
|
+
const h = Math.max(1, height >> level);
|
|
784
|
+
size += format === "bgra" ? w * h * 4 : blockBytes(w, h, format === "compressedAlpha");
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
return size * faces;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
private allocateTexture(
|
|
791
|
+
width: number,
|
|
792
|
+
height: number,
|
|
793
|
+
cube: boolean,
|
|
794
|
+
rectangle: boolean,
|
|
795
|
+
format: TextureFormat,
|
|
796
|
+
): Texture3D {
|
|
797
|
+
this.alive();
|
|
798
|
+
range(1, width - 1, 2048);
|
|
799
|
+
range(1, height - 1, 2048);
|
|
800
|
+
if (cube && width !== height) {
|
|
801
|
+
throw new Context3DError("textureShape", "Context3D cube faces must be square");
|
|
802
|
+
}
|
|
803
|
+
if (cube && width > 1024) {
|
|
804
|
+
throw new Context3DError("textureSize", "Context3D cube faces must be at most 1024");
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const faces = cube ? 6 : 1;
|
|
808
|
+
const levels = rectangle ? 1 : 1 + Math.floor(Math.log2(Math.max(width, height)));
|
|
809
|
+
if (!rectangle && ((width & (width - 1)) !== 0 || (height & (height - 1)) !== 0)) {
|
|
810
|
+
throw new Context3DError(
|
|
811
|
+
"texturePowerOfTwo",
|
|
812
|
+
"Context3D texture size must be a power of two",
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
if (format !== "bgra" && rectangle) {
|
|
816
|
+
throw new Context3DError("unsupported", "Context3D rectangle textures are not compressed");
|
|
817
|
+
}
|
|
818
|
+
if (format !== "bgra" && (width < 4 || height < 4)) {
|
|
819
|
+
throw new Context3DError("compressedSize", "Context3D compressed textures are at least 4x4");
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const gl = this.gl;
|
|
823
|
+
const storage = this.s3tcFormat(format) || gl.RGBA8;
|
|
824
|
+
const size = this.chainBytes(width, height, levels, faces, format);
|
|
825
|
+
const kind = cube ? 2 : rectangle ? 1 : 0;
|
|
826
|
+
const countLimit = cube
|
|
827
|
+
? this.limits.maxCubeTextures
|
|
828
|
+
: rectangle
|
|
829
|
+
? this.limits.maxRectangleTextures
|
|
830
|
+
: this.limits.max2DTextures;
|
|
831
|
+
const byteLimit = cube
|
|
832
|
+
? this.limits.maxCubeTextureBytes
|
|
833
|
+
: rectangle
|
|
834
|
+
? this.limits.maxRectangleTextureBytes
|
|
835
|
+
: this.limits.max2DTextureBytes;
|
|
836
|
+
// AIR reserves half the base size for mips, independently of the exact
|
|
837
|
+
// chain storage, and charges DXT1 an eighth of BGRA and DXT5 a quarter.
|
|
838
|
+
const quotaBytes =
|
|
839
|
+
(width * height * faces * (rectangle ? 4 : 6)) /
|
|
840
|
+
(format === "compressed" ? 8 : format === "compressedAlpha" ? 4 : 1);
|
|
841
|
+
if (
|
|
842
|
+
this.textures.size >= this.limits.maxTextures ||
|
|
843
|
+
this.textureCounts[kind] >= countLimit ||
|
|
844
|
+
this.textureQuotaBytes[kind] + quotaBytes > byteLimit ||
|
|
845
|
+
this.textureBytes + size > this.limits.maxTextureBytes
|
|
846
|
+
) {
|
|
847
|
+
throw new Context3DError("limit", "Context3D experiment: texture count or memory limit");
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const handle = gl.createTexture();
|
|
851
|
+
if (!handle) {
|
|
852
|
+
throw new Context3DError("textureAllocation", "Context3D experiment: cannot create texture");
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
856
|
+
const target = cube ? gl.TEXTURE_CUBE_MAP : gl.TEXTURE_2D;
|
|
857
|
+
gl.bindTexture(target, handle);
|
|
858
|
+
gl.texStorage2D(target, levels, storage, width, height);
|
|
859
|
+
const state: TextureState = {
|
|
860
|
+
handle,
|
|
861
|
+
storage,
|
|
862
|
+
bytes: new Uint8Array(0),
|
|
863
|
+
uploaded: new Uint16Array(faces),
|
|
864
|
+
framebuffer: null,
|
|
865
|
+
depthBuffer: null,
|
|
866
|
+
dirty: false,
|
|
867
|
+
face: 0,
|
|
868
|
+
mipFramebuffer: null,
|
|
869
|
+
depthAttached: false,
|
|
870
|
+
multisample: null,
|
|
871
|
+
samples: 0,
|
|
872
|
+
antiAlias: 0,
|
|
873
|
+
};
|
|
874
|
+
const texture: Texture3D = {
|
|
875
|
+
width,
|
|
876
|
+
height,
|
|
877
|
+
levels,
|
|
878
|
+
cube,
|
|
879
|
+
rectangle,
|
|
880
|
+
format,
|
|
881
|
+
upload: (pixels, level = 0, face = 0, sourceWidth, sourceHeight) => {
|
|
882
|
+
this.alive();
|
|
883
|
+
this.textureState(texture);
|
|
884
|
+
uploadTexture(
|
|
885
|
+
gl,
|
|
886
|
+
texture,
|
|
887
|
+
state,
|
|
888
|
+
pixels,
|
|
889
|
+
level,
|
|
890
|
+
face,
|
|
891
|
+
sourceWidth,
|
|
892
|
+
sourceHeight,
|
|
893
|
+
this.dxtEncoder,
|
|
894
|
+
);
|
|
895
|
+
},
|
|
896
|
+
uploadBytes: (data, offset, level = 0, face = 0) => {
|
|
897
|
+
this.alive();
|
|
898
|
+
this.textureState(texture);
|
|
899
|
+
uploadTextureBytes(gl, texture, state, data, offset, level, face, this.dxtEncoder);
|
|
900
|
+
},
|
|
901
|
+
uploadCompressed: (blocks, level = 0, face = 0) => {
|
|
902
|
+
this.alive();
|
|
903
|
+
this.textureState(texture);
|
|
904
|
+
if (format === "bgra") {
|
|
905
|
+
throw new Context3DError(
|
|
906
|
+
"textureUpload",
|
|
907
|
+
"Context3D experiment: BGRA texture given blocks",
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
uploadCompressedLevel(gl, texture, state, blocks, level, face);
|
|
912
|
+
},
|
|
913
|
+
dispose: () => {
|
|
914
|
+
if (!this.textures.delete(texture)) {
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
this.textureCounts[kind]--;
|
|
919
|
+
this.textureQuotaBytes[kind] -= quotaBytes;
|
|
920
|
+
|
|
921
|
+
if (this.renderTexture === texture) {
|
|
922
|
+
this.renderTexture = null;
|
|
923
|
+
this.renderDepth = false;
|
|
924
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
|
|
925
|
+
gl.frontFace(gl.CW);
|
|
926
|
+
this.applyScissor();
|
|
927
|
+
}
|
|
928
|
+
gl.deleteFramebuffer(state.framebuffer);
|
|
929
|
+
gl.deleteFramebuffer(state.mipFramebuffer);
|
|
930
|
+
gl.deleteRenderbuffer(state.depthBuffer);
|
|
931
|
+
if (state.multisample) {
|
|
932
|
+
this.deleteMultisample(texture, state.multisample);
|
|
933
|
+
state.multisample = null;
|
|
934
|
+
}
|
|
935
|
+
if (state.depthBuffer) {
|
|
936
|
+
this.targetDepthBytes -= width * height * 4;
|
|
937
|
+
}
|
|
938
|
+
state.framebuffer = null;
|
|
939
|
+
state.mipFramebuffer = null;
|
|
940
|
+
state.depthBuffer = null;
|
|
941
|
+
|
|
942
|
+
for (let i = 0; i < this.textureSlots.length; i++) {
|
|
943
|
+
if (this.textureSlots[i] === texture) {
|
|
944
|
+
this.textureSlots[i] = null;
|
|
945
|
+
this.textureMask &= ~(1 << i);
|
|
946
|
+
gl.activeTexture(gl.TEXTURE0 + i);
|
|
947
|
+
gl.bindTexture(target, null);
|
|
948
|
+
gl.bindSampler(i, null);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
gl.deleteTexture(handle);
|
|
953
|
+
state.bytes = new Uint8Array(0);
|
|
954
|
+
this.textureBytes -= size;
|
|
955
|
+
},
|
|
956
|
+
};
|
|
957
|
+
this.textures.set(texture, state);
|
|
958
|
+
this.textureCounts[kind]++;
|
|
959
|
+
this.textureQuotaBytes[kind] += quotaBytes;
|
|
960
|
+
this.textureOwners.add(texture);
|
|
961
|
+
this.textureBytes += size;
|
|
962
|
+
|
|
963
|
+
return texture;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
private textureState(texture: Texture3D): TextureState {
|
|
967
|
+
const state = this.textures.get(texture);
|
|
968
|
+
if (!state) {
|
|
969
|
+
throw new Context3DError(
|
|
970
|
+
"disposed",
|
|
971
|
+
"Context3D experiment: texture disposed or from another context",
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
return state;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
private copyProgram(cube: boolean): CopyProgram {
|
|
979
|
+
const cached = cube ? this.copyCube : this.copy2D;
|
|
980
|
+
if (cached) {
|
|
981
|
+
return cached;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const program = createCopyProgram(this.gl, cube);
|
|
985
|
+
if (cube) {
|
|
986
|
+
this.copyCube = program;
|
|
987
|
+
} else {
|
|
988
|
+
this.copy2D = program;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
return program;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
private seedTarget(texture: Texture3D, state: TextureState): void {
|
|
995
|
+
const target = state.multisample;
|
|
996
|
+
if (!target) {
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
const gl = this.gl;
|
|
1001
|
+
const program = this.copyProgram(texture.cube);
|
|
1002
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, target.framebuffer);
|
|
1003
|
+
gl.viewport(0, 0, texture.width, texture.height);
|
|
1004
|
+
gl.disable(gl.DEPTH_TEST);
|
|
1005
|
+
gl.disable(gl.STENCIL_TEST);
|
|
1006
|
+
gl.disable(gl.BLEND);
|
|
1007
|
+
gl.disable(gl.CULL_FACE);
|
|
1008
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
1009
|
+
gl.colorMask(true, true, true, true);
|
|
1010
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
1011
|
+
gl.bindTexture(texture.cube ? gl.TEXTURE_CUBE_MAP : gl.TEXTURE_2D, state.handle);
|
|
1012
|
+
gl.bindSampler(0, this.sampler(0));
|
|
1013
|
+
gl.bindVertexArray(this.vao);
|
|
1014
|
+
gl.useProgram(program.handle);
|
|
1015
|
+
if (texture.cube) {
|
|
1016
|
+
gl.uniform2f(program.size, texture.width, texture.height);
|
|
1017
|
+
gl.uniform1i(program.face, this.renderFace);
|
|
1018
|
+
}
|
|
1019
|
+
// WebGL cannot blit single-sample color into a multisample framebuffer.
|
|
1020
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
1021
|
+
gl.enable(gl.DEPTH_TEST);
|
|
1022
|
+
if (this.stencilEnabled) {
|
|
1023
|
+
gl.enable(gl.STENCIL_TEST);
|
|
1024
|
+
}
|
|
1025
|
+
if (this.blendEnabled) {
|
|
1026
|
+
gl.enable(gl.BLEND);
|
|
1027
|
+
}
|
|
1028
|
+
if (this.cullEnabled) {
|
|
1029
|
+
gl.enable(gl.CULL_FACE);
|
|
1030
|
+
}
|
|
1031
|
+
if (this.scissor) {
|
|
1032
|
+
gl.enable(gl.SCISSOR_TEST);
|
|
1033
|
+
}
|
|
1034
|
+
this.applyColorMask();
|
|
1035
|
+
target.valid = true;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
private multisamples(quality: number, depth: boolean): number {
|
|
1039
|
+
if (!Number.isInteger(quality)) {
|
|
1040
|
+
throw new Context3DError("operation", "Context3D experiment: invalid antialiasing quality");
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
const key = Math.max(0, Math.min(quality, 4)) * 2 + Number(depth);
|
|
1044
|
+
if (this.targetSamples[key] < 0) {
|
|
1045
|
+
this.targetSamples[key] = selectSamples(this.gl, quality, depth);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
return this.targetSamples[key];
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
private deleteMultisample(texture: Texture3D, target: MultisampleTarget): void {
|
|
1052
|
+
deleteMultisample(this.gl, target);
|
|
1053
|
+
|
|
1054
|
+
const bytes = texture.width * texture.height * 4 * target.samples;
|
|
1055
|
+
this.targetColorBytes -= bytes + (target.texture ? texture.width * texture.height * 4 : 0);
|
|
1056
|
+
if (target.depth) {
|
|
1057
|
+
this.targetDepthBytes -= bytes;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
private prepareMultisample(
|
|
1062
|
+
texture: Texture3D,
|
|
1063
|
+
state: TextureState,
|
|
1064
|
+
samples: number,
|
|
1065
|
+
depth: boolean,
|
|
1066
|
+
): void {
|
|
1067
|
+
const old = state.multisample;
|
|
1068
|
+
const same = old?.samples === samples;
|
|
1069
|
+
const bytes = texture.width * texture.height * 4 * samples;
|
|
1070
|
+
const previous = old ? texture.width * texture.height * 4 * old.samples : 0;
|
|
1071
|
+
const resolveBytes = texture.cube ? texture.width * texture.height * 4 : 0;
|
|
1072
|
+
const newDepth = depth && (!same || !old.depth);
|
|
1073
|
+
if (
|
|
1074
|
+
this.targetColorBytes + (same ? 0 : bytes - previous + (old ? 0 : resolveBytes)) >
|
|
1075
|
+
this.limits.maxTargetColorBytes ||
|
|
1076
|
+
this.targetDepthBytes + (newDepth ? bytes : 0) - (!same && old?.depth ? previous : 0) >
|
|
1077
|
+
this.limits.maxTargetDepthBytes
|
|
1078
|
+
) {
|
|
1079
|
+
throw new Context3DError("limit", "Context3D experiment: multisample target memory limit");
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
this.copyProgram(texture.cube);
|
|
1083
|
+
this.sampler(0);
|
|
1084
|
+
const target = allocateMultisample(
|
|
1085
|
+
this.gl,
|
|
1086
|
+
texture,
|
|
1087
|
+
old,
|
|
1088
|
+
samples,
|
|
1089
|
+
depth,
|
|
1090
|
+
this.depthWrite,
|
|
1091
|
+
this.stencilWrite,
|
|
1092
|
+
);
|
|
1093
|
+
if (same) {
|
|
1094
|
+
if (newDepth) {
|
|
1095
|
+
this.targetDepthBytes += bytes;
|
|
1096
|
+
}
|
|
1097
|
+
} else {
|
|
1098
|
+
if (old) {
|
|
1099
|
+
this.deleteMultisample(texture, old);
|
|
1100
|
+
}
|
|
1101
|
+
state.multisample = target;
|
|
1102
|
+
this.targetColorBytes += bytes + resolveBytes;
|
|
1103
|
+
if (target.depth) {
|
|
1104
|
+
this.targetDepthBytes += bytes;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
setRenderTarget(texture: Texture3D | null, depth = false, face = 0, antiAlias = 0): void {
|
|
1110
|
+
this.alive();
|
|
1111
|
+
if (texture) {
|
|
1112
|
+
range(face, 1, texture.cube ? 6 : 1);
|
|
1113
|
+
}
|
|
1114
|
+
if (
|
|
1115
|
+
texture === this.renderTexture &&
|
|
1116
|
+
(!texture ||
|
|
1117
|
+
(depth === this.renderDepth &&
|
|
1118
|
+
face === this.renderFace &&
|
|
1119
|
+
antiAlias === this.textureState(texture).antiAlias))
|
|
1120
|
+
) {
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
const gl = this.gl;
|
|
1125
|
+
if (texture) {
|
|
1126
|
+
this.textureState(texture);
|
|
1127
|
+
if (texture.format !== "bgra") {
|
|
1128
|
+
throw new Context3DError(
|
|
1129
|
+
"operation",
|
|
1130
|
+
"Context3D experiment: compressed textures are no render targets",
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
const samples = texture ? this.multisamples(antiAlias, depth) : 0;
|
|
1135
|
+
this.finishTarget();
|
|
1136
|
+
if (texture) {
|
|
1137
|
+
const state = this.textureState(texture);
|
|
1138
|
+
const singleDepth = depth && samples === 0;
|
|
1139
|
+
|
|
1140
|
+
let framebuffer = state.framebuffer;
|
|
1141
|
+
let mipFramebuffer = state.mipFramebuffer;
|
|
1142
|
+
let depthBuffer = state.depthBuffer;
|
|
1143
|
+
try {
|
|
1144
|
+
framebuffer ??= gl.createFramebuffer();
|
|
1145
|
+
if (!framebuffer) {
|
|
1146
|
+
throw new Context3DError(
|
|
1147
|
+
"operation",
|
|
1148
|
+
"Context3D experiment: cannot create texture framebuffer",
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
if (texture.cube && !mipFramebuffer) {
|
|
1152
|
+
mipFramebuffer = gl.createFramebuffer();
|
|
1153
|
+
if (!mipFramebuffer) {
|
|
1154
|
+
throw new Context3DError(
|
|
1155
|
+
"operation",
|
|
1156
|
+
"Context3D experiment: cannot create cube mip framebuffer",
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
|
1161
|
+
gl.framebufferTexture2D(
|
|
1162
|
+
gl.FRAMEBUFFER,
|
|
1163
|
+
gl.COLOR_ATTACHMENT0,
|
|
1164
|
+
texture.cube ? gl.TEXTURE_CUBE_MAP_POSITIVE_X + face : gl.TEXTURE_2D,
|
|
1165
|
+
state.handle,
|
|
1166
|
+
0,
|
|
1167
|
+
);
|
|
1168
|
+
if (singleDepth && !depthBuffer) {
|
|
1169
|
+
if (
|
|
1170
|
+
this.targetDepthBytes + texture.width * texture.height * 4 >
|
|
1171
|
+
this.limits.maxTargetDepthBytes
|
|
1172
|
+
) {
|
|
1173
|
+
throw new Context3DError("limit", "Context3D experiment: target depth memory limit");
|
|
1174
|
+
}
|
|
1175
|
+
depthBuffer = gl.createRenderbuffer();
|
|
1176
|
+
if (!depthBuffer) {
|
|
1177
|
+
throw new Context3DError(
|
|
1178
|
+
"operation",
|
|
1179
|
+
"Context3D experiment: cannot create texture depth buffer",
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer);
|
|
1183
|
+
gl.renderbufferStorage(
|
|
1184
|
+
gl.RENDERBUFFER,
|
|
1185
|
+
gl.DEPTH24_STENCIL8,
|
|
1186
|
+
texture.width,
|
|
1187
|
+
texture.height,
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
gl.framebufferRenderbuffer(
|
|
1191
|
+
gl.FRAMEBUFFER,
|
|
1192
|
+
gl.DEPTH_STENCIL_ATTACHMENT,
|
|
1193
|
+
gl.RENDERBUFFER,
|
|
1194
|
+
singleDepth ? depthBuffer : null,
|
|
1195
|
+
);
|
|
1196
|
+
if (
|
|
1197
|
+
(!state.framebuffer || (singleDepth && !state.depthBuffer)) &&
|
|
1198
|
+
gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE
|
|
1199
|
+
) {
|
|
1200
|
+
throw new Context3DError(
|
|
1201
|
+
"operation",
|
|
1202
|
+
"Context3D experiment: incomplete texture framebuffer",
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
if (samples) {
|
|
1206
|
+
this.prepareMultisample(texture, state, samples, depth);
|
|
1207
|
+
}
|
|
1208
|
+
} catch (error) {
|
|
1209
|
+
if (framebuffer !== state.framebuffer) {
|
|
1210
|
+
gl.deleteFramebuffer(framebuffer);
|
|
1211
|
+
} else if (framebuffer) {
|
|
1212
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
|
1213
|
+
gl.framebufferTexture2D(
|
|
1214
|
+
gl.FRAMEBUFFER,
|
|
1215
|
+
gl.COLOR_ATTACHMENT0,
|
|
1216
|
+
texture.cube ? gl.TEXTURE_CUBE_MAP_POSITIVE_X + state.face : gl.TEXTURE_2D,
|
|
1217
|
+
state.handle,
|
|
1218
|
+
0,
|
|
1219
|
+
);
|
|
1220
|
+
// Restore the attachment if this was the currently selected target.
|
|
1221
|
+
gl.framebufferRenderbuffer(
|
|
1222
|
+
gl.FRAMEBUFFER,
|
|
1223
|
+
gl.DEPTH_STENCIL_ATTACHMENT,
|
|
1224
|
+
gl.RENDERBUFFER,
|
|
1225
|
+
state.depthAttached ? state.depthBuffer : null,
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
if (mipFramebuffer !== state.mipFramebuffer) {
|
|
1229
|
+
gl.deleteFramebuffer(mipFramebuffer);
|
|
1230
|
+
}
|
|
1231
|
+
if (depthBuffer !== state.depthBuffer) {
|
|
1232
|
+
gl.deleteRenderbuffer(depthBuffer);
|
|
1233
|
+
}
|
|
1234
|
+
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
|
|
1235
|
+
gl.bindFramebuffer(
|
|
1236
|
+
gl.FRAMEBUFFER,
|
|
1237
|
+
this.renderTexture
|
|
1238
|
+
? targetFramebuffer(this.textureState(this.renderTexture))
|
|
1239
|
+
: this.framebuffer,
|
|
1240
|
+
);
|
|
1241
|
+
this.applyScissor();
|
|
1242
|
+
throw error;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
if (depthBuffer && !state.depthBuffer) {
|
|
1246
|
+
this.targetDepthBytes += texture.width * texture.height * 4;
|
|
1247
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
1248
|
+
gl.depthMask(true);
|
|
1249
|
+
gl.clearDepth(1);
|
|
1250
|
+
gl.clearStencil(0);
|
|
1251
|
+
gl.stencilMask(255);
|
|
1252
|
+
gl.clear(gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
|
|
1253
|
+
gl.depthMask(this.depthWrite);
|
|
1254
|
+
gl.stencilMask(this.stencilWrite);
|
|
1255
|
+
}
|
|
1256
|
+
state.framebuffer = framebuffer;
|
|
1257
|
+
state.mipFramebuffer = mipFramebuffer;
|
|
1258
|
+
state.face = face;
|
|
1259
|
+
state.depthAttached = singleDepth;
|
|
1260
|
+
state.depthBuffer = depthBuffer;
|
|
1261
|
+
state.samples = samples;
|
|
1262
|
+
state.antiAlias = antiAlias;
|
|
1263
|
+
if (samples && state.multisample && state.multisample.face !== face) {
|
|
1264
|
+
state.multisample.face = face;
|
|
1265
|
+
state.multisample.valid = false;
|
|
1266
|
+
}
|
|
1267
|
+
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
this.renderTexture = texture;
|
|
1271
|
+
this.renderFace = texture ? face : 0;
|
|
1272
|
+
this.renderDepth = texture !== null && depth;
|
|
1273
|
+
gl.frontFace(texture ? gl.CCW : gl.CW);
|
|
1274
|
+
this.applyScissor();
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
private changedTarget(): void {
|
|
1278
|
+
if (this.renderTexture) {
|
|
1279
|
+
const state = this.textureState(this.renderTexture);
|
|
1280
|
+
state.dirty = true;
|
|
1281
|
+
if (state.multisample) {
|
|
1282
|
+
if (state.samples) {
|
|
1283
|
+
state.multisample.dirty = true;
|
|
1284
|
+
} else {
|
|
1285
|
+
state.multisample.valid = false;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
} else {
|
|
1289
|
+
this.resolveDirty = true;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
private finishTarget(): void {
|
|
1294
|
+
if (!this.renderTexture) {
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
const texture = this.renderTexture;
|
|
1298
|
+
const state = this.textureState(texture);
|
|
1299
|
+
if (state.dirty) {
|
|
1300
|
+
const gl = this.gl;
|
|
1301
|
+
resolveTarget(this.gl, texture, state, this.renderFace, this.scissor);
|
|
1302
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
1303
|
+
if (texture.cube) {
|
|
1304
|
+
// generateMipmap(CUBE_MAP) would overwrite manually uploaded mips on
|
|
1305
|
+
// untouched faces. AIR regenerates only the face just rendered.
|
|
1306
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
1307
|
+
const face = gl.TEXTURE_CUBE_MAP_POSITIVE_X + this.renderFace;
|
|
1308
|
+
// The base-size depth attachment cannot accompany smaller mip images.
|
|
1309
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, state.framebuffer);
|
|
1310
|
+
if (state.depthAttached) {
|
|
1311
|
+
gl.framebufferRenderbuffer(
|
|
1312
|
+
gl.FRAMEBUFFER,
|
|
1313
|
+
gl.DEPTH_STENCIL_ATTACHMENT,
|
|
1314
|
+
gl.RENDERBUFFER,
|
|
1315
|
+
null,
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
for (let level = 1; level < texture.levels; level++) {
|
|
1319
|
+
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, state.framebuffer);
|
|
1320
|
+
gl.framebufferTexture2D(
|
|
1321
|
+
gl.READ_FRAMEBUFFER,
|
|
1322
|
+
gl.COLOR_ATTACHMENT0,
|
|
1323
|
+
face,
|
|
1324
|
+
state.handle,
|
|
1325
|
+
level - 1,
|
|
1326
|
+
);
|
|
1327
|
+
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, state.mipFramebuffer);
|
|
1328
|
+
gl.framebufferTexture2D(
|
|
1329
|
+
gl.DRAW_FRAMEBUFFER,
|
|
1330
|
+
gl.COLOR_ATTACHMENT0,
|
|
1331
|
+
face,
|
|
1332
|
+
state.handle,
|
|
1333
|
+
level,
|
|
1334
|
+
);
|
|
1335
|
+
const size = texture.width >> level;
|
|
1336
|
+
gl.blitFramebuffer(
|
|
1337
|
+
0,
|
|
1338
|
+
0,
|
|
1339
|
+
size * 2,
|
|
1340
|
+
size * 2,
|
|
1341
|
+
0,
|
|
1342
|
+
0,
|
|
1343
|
+
size,
|
|
1344
|
+
size,
|
|
1345
|
+
gl.COLOR_BUFFER_BIT,
|
|
1346
|
+
gl.LINEAR,
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, state.framebuffer);
|
|
1350
|
+
gl.framebufferTexture2D(
|
|
1351
|
+
gl.FRAMEBUFFER,
|
|
1352
|
+
gl.COLOR_ATTACHMENT0,
|
|
1353
|
+
gl.TEXTURE_CUBE_MAP_POSITIVE_X + state.face,
|
|
1354
|
+
state.handle,
|
|
1355
|
+
0,
|
|
1356
|
+
);
|
|
1357
|
+
if (state.depthAttached) {
|
|
1358
|
+
gl.framebufferRenderbuffer(
|
|
1359
|
+
gl.FRAMEBUFFER,
|
|
1360
|
+
gl.DEPTH_STENCIL_ATTACHMENT,
|
|
1361
|
+
gl.RENDERBUFFER,
|
|
1362
|
+
state.depthBuffer,
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
} else if (!texture.rectangle) {
|
|
1366
|
+
gl.bindTexture(gl.TEXTURE_2D, state.handle);
|
|
1367
|
+
gl.generateMipmap(gl.TEXTURE_2D);
|
|
1368
|
+
}
|
|
1369
|
+
state.uploaded[this.renderFace] = (1 << texture.levels) - 1;
|
|
1370
|
+
state.dirty = false;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
setTexture(index: number, texture: Texture3D | null): void {
|
|
1375
|
+
this.alive();
|
|
1376
|
+
range(index, 1, 8);
|
|
1377
|
+
if (texture && !this.textureOwners.has(texture)) {
|
|
1378
|
+
throw new Context3DError("disposed", "Context3D experiment: texture from another context");
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
// AIR accepts a disposed texture binding and reports 3663 when drawing.
|
|
1382
|
+
this.textureSlots[index] = texture;
|
|
1383
|
+
this.textureMask = (this.textureMask & ~(1 << index)) | (texture ? 1 << index : 0);
|
|
1384
|
+
if (!texture) {
|
|
1385
|
+
this.gl.activeTexture(this.gl.TEXTURE0 + index);
|
|
1386
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, null);
|
|
1387
|
+
this.gl.bindTexture(this.gl.TEXTURE_CUBE_MAP, null);
|
|
1388
|
+
this.gl.bindSampler(index, null);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
setSampler(index: number, wrap: SamplerWrap, filter: SamplerFilter, mip: SamplerMip): void {
|
|
1393
|
+
this.alive();
|
|
1394
|
+
range(index, 1, this.samplerStates.length);
|
|
1395
|
+
const w = SAMPLER_WRAPS.indexOf(wrap);
|
|
1396
|
+
const f = SAMPLER_FILTERS.indexOf(filter);
|
|
1397
|
+
const m = SAMPLER_MIPS.indexOf(mip);
|
|
1398
|
+
if (w < 0 || f < 0 || m < 0) {
|
|
1399
|
+
throw new Context3DError("operation", "Context3D experiment: invalid sampler state");
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
this.samplerStates[index] = w | (f << 2) | (m << 5);
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
private sampler(index: number): WebGLSampler {
|
|
1406
|
+
let sampler = this.samplers[index];
|
|
1407
|
+
if (!sampler) {
|
|
1408
|
+
const gl = this.gl;
|
|
1409
|
+
sampler = gl.createSampler();
|
|
1410
|
+
if (!sampler) {
|
|
1411
|
+
throw new Context3DError("operation", "Context3D experiment: cannot create sampler");
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
const mode = (index >>> 2) & 7;
|
|
1415
|
+
const linear = mode !== 0;
|
|
1416
|
+
const filter = linear ? gl.LINEAR : gl.NEAREST;
|
|
1417
|
+
let minFilter: number = filter;
|
|
1418
|
+
switch (index >>> 5) {
|
|
1419
|
+
case 1:
|
|
1420
|
+
minFilter = linear ? gl.LINEAR_MIPMAP_NEAREST : gl.NEAREST_MIPMAP_NEAREST;
|
|
1421
|
+
break;
|
|
1422
|
+
case 2:
|
|
1423
|
+
minFilter = linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_LINEAR;
|
|
1424
|
+
break;
|
|
1425
|
+
}
|
|
1426
|
+
gl.samplerParameteri(sampler, gl.TEXTURE_MIN_FILTER, minFilter);
|
|
1427
|
+
gl.samplerParameteri(sampler, gl.TEXTURE_MAG_FILTER, filter);
|
|
1428
|
+
if (mode > 1) {
|
|
1429
|
+
if (this.anisotropy === undefined) {
|
|
1430
|
+
this.anisotropy = gl.getExtension("EXT_texture_filter_anisotropic");
|
|
1431
|
+
if (this.anisotropy) {
|
|
1432
|
+
this.maxAnisotropy = gl.getParameter(this.anisotropy.MAX_TEXTURE_MAX_ANISOTROPY_EXT);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
if (this.anisotropy) {
|
|
1437
|
+
gl.samplerParameterf(
|
|
1438
|
+
sampler,
|
|
1439
|
+
this.anisotropy.TEXTURE_MAX_ANISOTROPY_EXT,
|
|
1440
|
+
Math.min(1 << (mode - 1), this.maxAnisotropy),
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
gl.samplerParameteri(sampler, gl.TEXTURE_WRAP_S, index & 1 ? gl.REPEAT : gl.CLAMP_TO_EDGE);
|
|
1445
|
+
gl.samplerParameteri(sampler, gl.TEXTURE_WRAP_T, index & 2 ? gl.REPEAT : gl.CLAMP_TO_EDGE);
|
|
1446
|
+
gl.samplerParameteri(
|
|
1447
|
+
sampler,
|
|
1448
|
+
gl.TEXTURE_WRAP_R,
|
|
1449
|
+
(index & 3) === 3 ? gl.REPEAT : gl.CLAMP_TO_EDGE,
|
|
1450
|
+
);
|
|
1451
|
+
this.samplers[index] = sampler;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
return sampler;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
setProgram(program: Program3D): void {
|
|
1458
|
+
this.alive();
|
|
1459
|
+
const state = this.program(program);
|
|
1460
|
+
if (!state.handle) {
|
|
1461
|
+
throw new Context3DError("program", "Context3D experiment: program not uploaded");
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
this.selected = program;
|
|
1465
|
+
for (let i = 0; i < state.samplers.length; i++) {
|
|
1466
|
+
const source = state.samplers[i].source;
|
|
1467
|
+
if (!source.ignore) {
|
|
1468
|
+
this.samplerStates[source.index] =
|
|
1469
|
+
(source.repeat ? 3 : 0) | (source.filter << 2) | (source.mip << 5);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
setVertex(index: number, buffer: Buffer3D | null, offset: number, format: VertexFormat): void {
|
|
1475
|
+
this.alive();
|
|
1476
|
+
range(index, 1, 8);
|
|
1477
|
+
if (buffer === null) {
|
|
1478
|
+
this.vertices[index] = null;
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
this.buffer(buffer);
|
|
1483
|
+
const packed = format === "bytes4";
|
|
1484
|
+
const size = packed ? 4 : VERTEX_FORMATS.indexOf(format) + 1;
|
|
1485
|
+
range(1, size - 1, 4);
|
|
1486
|
+
if (!Number.isInteger(offset)) {
|
|
1487
|
+
throw new Context3DError(
|
|
1488
|
+
"operation",
|
|
1489
|
+
"Context3D experiment: vertex offset must be an integer",
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// AIR reports stride overruns at draw time, including on an existing binding.
|
|
1494
|
+
const valid = offset >= 0 && offset <= buffer.stride - (packed ? 1 : size);
|
|
1495
|
+
const previous = this.vertices[index];
|
|
1496
|
+
if (previous) {
|
|
1497
|
+
if (previous.buffer !== buffer) {
|
|
1498
|
+
previous.checkedIndex = null;
|
|
1499
|
+
}
|
|
1500
|
+
previous.buffer = buffer;
|
|
1501
|
+
previous.offset = offset;
|
|
1502
|
+
previous.size = size;
|
|
1503
|
+
previous.packed = packed;
|
|
1504
|
+
previous.valid = valid;
|
|
1505
|
+
} else {
|
|
1506
|
+
this.vertices[index] = {
|
|
1507
|
+
buffer,
|
|
1508
|
+
offset,
|
|
1509
|
+
size,
|
|
1510
|
+
packed,
|
|
1511
|
+
valid,
|
|
1512
|
+
checkedIndex: null,
|
|
1513
|
+
checkedIndexVersion: -1,
|
|
1514
|
+
checkedVertexVersion: -1,
|
|
1515
|
+
checkedFirst: 0,
|
|
1516
|
+
checkedCount: 0,
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
setConstants(stage: ProgramType, first: number, values: ArrayLike<number>, count: number): void {
|
|
1522
|
+
this.alive();
|
|
1523
|
+
const dest = stage === "vertex" ? this.vertexConstants : this.fragmentConstants;
|
|
1524
|
+
range(first, count, dest.length / 4);
|
|
1525
|
+
range(0, count * 4, values.length);
|
|
1526
|
+
for (let i = 0; i < count * 4; i++) {
|
|
1527
|
+
dest[first * 4 + i] = values[i];
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
draw(indices: Buffer3D, first: number, triangles: number): void {
|
|
1532
|
+
this.target();
|
|
1533
|
+
if (this.scissorOutside) {
|
|
1534
|
+
throw new ScissorBoundsError("Scissor rectangle does not intersect the target");
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
const index = this.buffer(indices);
|
|
1538
|
+
if (!index.indices) {
|
|
1539
|
+
throw new Context3DError("indexBuffer", "Context3D draw needs an index buffer");
|
|
1540
|
+
}
|
|
1541
|
+
if (!this.selected) {
|
|
1542
|
+
throw new Context3DError("program", "Context3D draw needs a program");
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
const state = this.program(this.selected);
|
|
1546
|
+
if (this.textureMask & ~state.samplerMask) {
|
|
1547
|
+
throw new TextureBindingError("unused");
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
for (let i = 0; i < state.samplers.length; i++) {
|
|
1551
|
+
const source = state.samplers[i].source;
|
|
1552
|
+
const texture = this.textureSlots[source.index];
|
|
1553
|
+
for (let j = i + 1; texture && j < state.samplers.length; j++) {
|
|
1554
|
+
const other = state.samplers[j].source;
|
|
1555
|
+
if (
|
|
1556
|
+
this.textureSlots[other.index] === texture &&
|
|
1557
|
+
this.samplerStates[source.index] !== this.samplerStates[other.index]
|
|
1558
|
+
) {
|
|
1559
|
+
throw new TextureBindingError("conflicting");
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
const count = triangles === -1 ? Math.floor((indices.count - first) / 3) * 3 : triangles * 3;
|
|
1565
|
+
if (!Number.isInteger(first) || !Number.isInteger(count) || first < 0 || count < 0) {
|
|
1566
|
+
throw new Context3DError("triangleCount", "Context3D draw has invalid triangle parameters");
|
|
1567
|
+
}
|
|
1568
|
+
if (first > indices.count - count) {
|
|
1569
|
+
throw new Context3DError("drawRange", "Context3D draw exceeds its index buffer");
|
|
1570
|
+
}
|
|
1571
|
+
if (count % 3 !== 0) {
|
|
1572
|
+
throw new Context3DError("triangleCount", "Context3D experiment: incomplete triangle");
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
const maximum = validateIndices(index, first, count);
|
|
1576
|
+
|
|
1577
|
+
const gl = this.gl;
|
|
1578
|
+
gl.bindVertexArray(this.vao);
|
|
1579
|
+
for (let i = 0; i < 8; i++) {
|
|
1580
|
+
gl.disableVertexAttribArray(i);
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
for (let i = 0; i < state.attributes.length; i++) {
|
|
1584
|
+
const slot = state.attributes[i];
|
|
1585
|
+
const binding = this.vertices[slot];
|
|
1586
|
+
if (!binding) {
|
|
1587
|
+
throw new Context3DError("vertexBinding", "Context3D experiment: missing vertex binding");
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
const buffer = this.buffer(binding.buffer);
|
|
1591
|
+
if (!binding.valid) {
|
|
1592
|
+
throw new VertexOffsetError("Vertex attribute exceeds its stride");
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
validateVertices(buffer, index, binding, first, count, maximum);
|
|
1596
|
+
|
|
1597
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer.handle);
|
|
1598
|
+
gl.enableVertexAttribArray(slot);
|
|
1599
|
+
gl.vertexAttribPointer(
|
|
1600
|
+
slot,
|
|
1601
|
+
binding.size,
|
|
1602
|
+
binding.packed ? gl.UNSIGNED_BYTE : gl.FLOAT,
|
|
1603
|
+
binding.packed,
|
|
1604
|
+
binding.buffer.stride * 4,
|
|
1605
|
+
binding.offset * 4,
|
|
1606
|
+
);
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
gl.useProgram(state.handle);
|
|
1610
|
+
gl.uniform1f(state.targetY, this.renderTexture ? -1 : 1);
|
|
1611
|
+
for (let i = 0; i < state.samplers.length; i++) {
|
|
1612
|
+
const binding = state.samplers[i];
|
|
1613
|
+
const slot = binding.source.index;
|
|
1614
|
+
const texture = this.textureSlots[slot];
|
|
1615
|
+
if (texture && texture === this.renderTexture) {
|
|
1616
|
+
throw new TextureBindingError("feedback");
|
|
1617
|
+
}
|
|
1618
|
+
const resource = texture && this.textures.get(texture);
|
|
1619
|
+
if (!resource || !texture) {
|
|
1620
|
+
throw new TextureBindingError("missing");
|
|
1621
|
+
}
|
|
1622
|
+
if (texture.cube !== binding.source.cube) {
|
|
1623
|
+
throw new TextureBindingError("dimension");
|
|
1624
|
+
}
|
|
1625
|
+
if ((texture.cube || texture.rectangle) && (this.samplerStates[slot] & 3) !== 0) {
|
|
1626
|
+
throw new TextureBindingError("wrap");
|
|
1627
|
+
}
|
|
1628
|
+
if (texture.rectangle && this.samplerStates[slot] >> 5 !== 0) {
|
|
1629
|
+
throw new TextureBindingError("rectangleMip");
|
|
1630
|
+
}
|
|
1631
|
+
if (texture.cube) {
|
|
1632
|
+
let uploaded = 0;
|
|
1633
|
+
for (let face = 0; face < 6; face++) {
|
|
1634
|
+
uploaded |= resource.uploaded[face];
|
|
1635
|
+
}
|
|
1636
|
+
if (!uploaded) {
|
|
1637
|
+
throw new TextureBindingError("unuploaded");
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
for (let face = 0; face < resource.uploaded.length; face++) {
|
|
1641
|
+
if (!texture.cube && !(resource.uploaded[face] & 1)) {
|
|
1642
|
+
throw new TextureBindingError("unuploaded");
|
|
1643
|
+
}
|
|
1644
|
+
// AIR requires every cube face and mip even with mip filtering disabled.
|
|
1645
|
+
if (
|
|
1646
|
+
(texture.cube || this.samplerStates[slot] >>> 5 !== 0) &&
|
|
1647
|
+
resource.uploaded[face] !== (1 << texture.levels) - 1
|
|
1648
|
+
) {
|
|
1649
|
+
throw new TextureBindingError("mipmaps");
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
// AGAL must say a compressed texture is one, and which: AIR reads nothing otherwise.
|
|
1653
|
+
if (this.samplerFormats && binding.source.format !== SAMPLER_FORMATS[texture.format]) {
|
|
1654
|
+
throw new TextureBindingError("format", slot);
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
gl.activeTexture(gl.TEXTURE0 + slot);
|
|
1658
|
+
gl.bindTexture(texture.cube ? gl.TEXTURE_CUBE_MAP : gl.TEXTURE_2D, resource.handle);
|
|
1659
|
+
gl.bindSampler(slot, this.sampler(this.samplerStates[slot]));
|
|
1660
|
+
gl.uniform1i(binding.location, slot);
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
if (state.vertexBank) {
|
|
1664
|
+
gl.uniform4fv(state.vertexBank, this.vertexConstants);
|
|
1665
|
+
} else {
|
|
1666
|
+
this.uniforms(state.vertex, this.vertexConstants);
|
|
1667
|
+
}
|
|
1668
|
+
this.uniforms(state.fragment, this.fragmentConstants);
|
|
1669
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, index.handle);
|
|
1670
|
+
gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_SHORT, first * 2);
|
|
1671
|
+
this.changedTarget();
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
private uniforms(locations: (WebGLUniformLocation | null)[], data: Float32Array): void {
|
|
1675
|
+
for (let i = 0; i < locations.length; i++) {
|
|
1676
|
+
if (locations[i]) {
|
|
1677
|
+
this.gl.uniform4fv(locations[i], data, i * 4, 4);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
private resolve(): void {
|
|
1683
|
+
if (!this.resolveFramebuffer) {
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
const gl = this.gl;
|
|
1688
|
+
if (this.resolveDirty) {
|
|
1689
|
+
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.framebuffer);
|
|
1690
|
+
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.resolveFramebuffer);
|
|
1691
|
+
if (this.scissor) {
|
|
1692
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
1693
|
+
}
|
|
1694
|
+
gl.blitFramebuffer(
|
|
1695
|
+
0,
|
|
1696
|
+
0,
|
|
1697
|
+
this.width,
|
|
1698
|
+
this.height,
|
|
1699
|
+
0,
|
|
1700
|
+
0,
|
|
1701
|
+
this.width,
|
|
1702
|
+
this.height,
|
|
1703
|
+
gl.COLOR_BUFFER_BIT,
|
|
1704
|
+
gl.NEAREST,
|
|
1705
|
+
);
|
|
1706
|
+
if (this.scissor) {
|
|
1707
|
+
gl.enable(gl.SCISSOR_TEST);
|
|
1708
|
+
}
|
|
1709
|
+
this.resolveDirty = false;
|
|
1710
|
+
}
|
|
1711
|
+
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.resolveFramebuffer);
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
read(width = this.width, height = this.height): Uint8Array {
|
|
1715
|
+
width = Math.max(0, Math.min(this.width, Math.floor(width) || 0));
|
|
1716
|
+
height = Math.max(0, Math.min(this.height, Math.floor(height) || 0));
|
|
1717
|
+
this.target(true);
|
|
1718
|
+
this.resolve();
|
|
1719
|
+
const gl = this.gl;
|
|
1720
|
+
// GL's rows run bottom-up, so the top-left region starts height rows below the top.
|
|
1721
|
+
gl.readPixels(0, this.height - height, width, height, gl.RGBA, gl.UNSIGNED_BYTE, this.pixels);
|
|
1722
|
+
const row = width * 4;
|
|
1723
|
+
for (let y = 0; y < height; y++) {
|
|
1724
|
+
const from = (height - y - 1) * row;
|
|
1725
|
+
this.topDown.set(this.pixels.subarray(from, from + row), y * row);
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
return this.topDown;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
present(): void {
|
|
1732
|
+
this.setRenderTarget(null);
|
|
1733
|
+
this.target();
|
|
1734
|
+
this.resolve();
|
|
1735
|
+
const gl = this.gl;
|
|
1736
|
+
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null);
|
|
1737
|
+
if (this.scissor) {
|
|
1738
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
1739
|
+
}
|
|
1740
|
+
gl.blitFramebuffer(
|
|
1741
|
+
0,
|
|
1742
|
+
0,
|
|
1743
|
+
this.width,
|
|
1744
|
+
this.height,
|
|
1745
|
+
0,
|
|
1746
|
+
0,
|
|
1747
|
+
this.width,
|
|
1748
|
+
this.height,
|
|
1749
|
+
gl.COLOR_BUFFER_BIT,
|
|
1750
|
+
gl.NEAREST,
|
|
1751
|
+
);
|
|
1752
|
+
if (this.scissor) {
|
|
1753
|
+
gl.enable(gl.SCISSOR_TEST);
|
|
1754
|
+
}
|
|
1755
|
+
gl.flush();
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
dispose(): void {
|
|
1759
|
+
if (this.disposed) {
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
this.disposed = true;
|
|
1764
|
+
for (const buffer of this.buffers.keys()) {
|
|
1765
|
+
buffer.dispose();
|
|
1766
|
+
}
|
|
1767
|
+
for (const program of this.programs.keys()) {
|
|
1768
|
+
program.dispose();
|
|
1769
|
+
}
|
|
1770
|
+
for (const texture of this.textures.keys()) {
|
|
1771
|
+
texture.dispose();
|
|
1772
|
+
}
|
|
1773
|
+
this.textureSlots.fill(null);
|
|
1774
|
+
this.textureMask = 0;
|
|
1775
|
+
for (let i = 0; i < this.samplers.length; i++) {
|
|
1776
|
+
this.gl.deleteSampler(this.samplers[i]);
|
|
1777
|
+
this.samplers[i] = null;
|
|
1778
|
+
}
|
|
1779
|
+
this.gl.bindVertexArray(null);
|
|
1780
|
+
this.gl.useProgram(null);
|
|
1781
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null);
|
|
1782
|
+
this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, null);
|
|
1783
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null);
|
|
1784
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, null);
|
|
1785
|
+
this.gl.deleteVertexArray(this.vao);
|
|
1786
|
+
this.gl.deleteFramebuffer(this.framebuffer);
|
|
1787
|
+
this.gl.deleteFramebuffer(this.resolveFramebuffer);
|
|
1788
|
+
this.gl.deleteRenderbuffer(this.colorBuffer);
|
|
1789
|
+
this.gl.deleteProgram(this.copy2D?.handle ?? null);
|
|
1790
|
+
this.gl.deleteProgram(this.copyCube?.handle ?? null);
|
|
1791
|
+
this.copy2D = null;
|
|
1792
|
+
this.copyCube = null;
|
|
1793
|
+
this.gl.deleteTexture(this.texture);
|
|
1794
|
+
this.gl.deleteRenderbuffer(this.depthBuffer);
|
|
1795
|
+
this.framebuffer = null;
|
|
1796
|
+
this.resolveFramebuffer = null;
|
|
1797
|
+
this.colorBuffer = null;
|
|
1798
|
+
this.texture = null;
|
|
1799
|
+
this.depthBuffer = null;
|
|
1800
|
+
this.pixels = new Uint8Array(0);
|
|
1801
|
+
this.topDown = new Uint8Array(0);
|
|
1802
|
+
}
|
|
1803
|
+
}
|