@swf2es/context3d 0.1.19 → 0.1.21

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.
Files changed (56) hide show
  1. package/README.md +120 -27
  2. package/dist/agal.d.ts +11 -2
  3. package/dist/agal.d.ts.map +1 -1
  4. package/dist/agal.js +318 -47
  5. package/dist/agal.js.map +1 -1
  6. package/dist/backend.d.ts +50 -3
  7. package/dist/backend.d.ts.map +1 -1
  8. package/dist/backend.js +74 -2
  9. package/dist/backend.js.map +1 -1
  10. package/dist/errors.d.ts +1 -1
  11. package/dist/errors.d.ts.map +1 -1
  12. package/dist/errors.js.map +1 -1
  13. package/dist/index.d.ts +2 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +2 -2
  16. package/dist/index.js.map +1 -1
  17. package/dist/webgl/back-buffer.d.ts +6 -0
  18. package/dist/webgl/back-buffer.d.ts.map +1 -0
  19. package/dist/webgl/back-buffer.js +197 -0
  20. package/dist/webgl/back-buffer.js.map +1 -0
  21. package/dist/webgl/buffers.d.ts +2 -2
  22. package/dist/webgl/buffers.d.ts.map +1 -1
  23. package/dist/webgl/buffers.js +105 -2
  24. package/dist/webgl/buffers.js.map +1 -1
  25. package/dist/webgl/context.d.ts +29 -48
  26. package/dist/webgl/context.d.ts.map +1 -1
  27. package/dist/webgl/context.js +227 -364
  28. package/dist/webgl/context.js.map +1 -1
  29. package/dist/webgl/programs.d.ts +4 -1
  30. package/dist/webgl/programs.d.ts.map +1 -1
  31. package/dist/webgl/programs.js +7 -6
  32. package/dist/webgl/programs.js.map +1 -1
  33. package/dist/webgl/readback.d.ts +3 -0
  34. package/dist/webgl/readback.d.ts.map +1 -0
  35. package/dist/webgl/readback.js +40 -0
  36. package/dist/webgl/readback.js.map +1 -0
  37. package/dist/webgl/targets.d.ts +3 -1
  38. package/dist/webgl/targets.d.ts.map +1 -1
  39. package/dist/webgl/targets.js +5 -3
  40. package/dist/webgl/targets.js.map +1 -1
  41. package/dist/webgl/textures.d.ts +9 -1
  42. package/dist/webgl/textures.d.ts.map +1 -1
  43. package/dist/webgl/textures.js +161 -7
  44. package/dist/webgl/textures.js.map +1 -1
  45. package/package.json +1 -1
  46. package/src/agal.ts +375 -51
  47. package/src/backend.ts +113 -3
  48. package/src/errors.ts +27 -1
  49. package/src/index.ts +12 -1
  50. package/src/webgl/back-buffer.ts +287 -0
  51. package/src/webgl/buffers.ts +114 -2
  52. package/src/webgl/context.ts +291 -489
  53. package/src/webgl/programs.ts +10 -5
  54. package/src/webgl/readback.ts +54 -0
  55. package/src/webgl/targets.ts +4 -2
  56. package/src/webgl/textures.ts +183 -9
@@ -1,4 +1,5 @@
1
1
  import { type SamplerSource, translate } from "../agal.js";
2
+ import type { Profile } from "../backend.js";
2
3
  import { linkProgram as link, ShaderError } from "../device/programs.js";
3
4
  import { Context3DError } from "../errors.js";
4
5
 
@@ -10,6 +11,8 @@ export interface ProgramState {
10
11
  attributeMask: number;
11
12
  vertex: (WebGLUniformLocation | null)[];
12
13
  vertexBank: WebGLUniformLocation | null;
14
+ /** The colour outputs its fragment program writes. */
15
+ outputs: number;
13
16
  fragment: (WebGLUniformLocation | null)[];
14
17
  samplers: { source: SamplerSource; location: WebGLUniformLocation | null }[];
15
18
  samplerMask: number;
@@ -32,19 +35,20 @@ export function uploadProgram(
32
35
  state: ProgramState,
33
36
  vertex: Uint8Array,
34
37
  fragment: Uint8Array,
38
+ profile: Profile,
35
39
  ): void {
36
40
  // Translate both before allocating anything on the GPU.
37
- const vs = translate(vertex, "vertex");
38
- const fs = translate(fragment, "fragment");
39
- for (let i = 0; i < 8; i++) {
40
- if (fs.varyings[i] & ~vs.varyings[i]) {
41
+ const vs = translate(vertex, "vertex", profile);
42
+ const fs = translate(fragment, "fragment", profile);
43
+ for (let i = 0; i < fs.varyings.length; i++) {
44
+ if (fs.varyings[i] & ~(vs.varyings[i] ?? 0)) {
41
45
  throw new Context3DError(
42
46
  "agalVaryingMissing",
43
47
  "Fragment program reads an unwritten varying component",
44
48
  );
45
49
  }
46
50
  }
47
- for (let i = 0; i < 8; i++) {
51
+ for (let i = 0; i < vs.varyings.length; i++) {
48
52
  if (vs.varyings[i] && vs.varyings[i] !== 15) {
49
53
  throw new Context3DError(
50
54
  "agalVaryingPartial",
@@ -70,6 +74,7 @@ export function uploadProgram(
70
74
 
71
75
  state.vertex = [];
72
76
  state.vertexBank = vs.indirect ? gl.getUniformLocation(handle, "vc[0]") : null;
77
+ state.outputs = fs.outputs;
73
78
  state.fragment = [];
74
79
  state.samplers = [];
75
80
  state.samplerMask = 0;
@@ -0,0 +1,54 @@
1
+ // The back buffer read back for drawToBitmapData: its rows top-down, at the size asked for.
2
+
3
+ import { resolveBackBuffer } from "./back-buffer.js";
4
+ import type { WebGLContext3D } from "./context.js";
5
+
6
+ export function readBackBuffer(ctx: WebGLContext3D, width: number, height: number): Uint8Array {
7
+ width = Math.max(0, Math.min(ctx.width, Math.floor(width) || 0));
8
+ height = Math.max(0, Math.min(ctx.height, Math.floor(height) || 0));
9
+ ctx.target(true);
10
+ resolveBackBuffer(ctx);
11
+ const gl = ctx.gl;
12
+ const scaleX = ctx.drawnWidth / ctx.width;
13
+ const scaleY = ctx.drawnHeight / ctx.height;
14
+ const drawnWidth = Math.min(ctx.drawnWidth, Math.ceil(width * scaleX));
15
+ const drawnHeight = Math.min(ctx.drawnHeight, Math.ceil(height * scaleY));
16
+ if (ctx.pixels.length < drawnWidth * drawnHeight * 4) {
17
+ ctx.pixels = new Uint8Array(drawnWidth * drawnHeight * 4);
18
+ }
19
+ if (ctx.topDown.length < width * height * 4) {
20
+ ctx.topDown = new Uint8Array(width * height * 4);
21
+ }
22
+
23
+ // GL's rows run bottom-up, so the top-left region starts height rows below the top.
24
+ gl.readPixels(
25
+ 0,
26
+ ctx.drawnHeight - drawnHeight,
27
+ drawnWidth,
28
+ drawnHeight,
29
+ gl.RGBA,
30
+ gl.UNSIGNED_BYTE,
31
+ ctx.pixels,
32
+ );
33
+ const row = width * 4;
34
+ if (drawnWidth === width && drawnHeight === height) {
35
+ for (let y = 0; y < height; y++) {
36
+ const from = (height - y - 1) * row;
37
+ ctx.topDown.set(ctx.pixels.subarray(from, from + row), y * row);
38
+ }
39
+
40
+ return ctx.topDown;
41
+ }
42
+
43
+ // Storage smaller than the back buffer: each pixel is the stored one it falls on.
44
+ const source = new Uint32Array(ctx.pixels.buffer, 0, drawnWidth * drawnHeight);
45
+ const target = new Uint32Array(ctx.topDown.buffer, 0, width * height);
46
+ for (let y = 0; y < height; y++) {
47
+ const from = (drawnHeight - 1 - Math.min(drawnHeight - 1, Math.floor(y * scaleY))) * drawnWidth;
48
+ for (let x = 0; x < width; x++) {
49
+ target[y * width + x] = source[from + Math.min(drawnWidth - 1, Math.floor(x * scaleX))];
50
+ }
51
+ }
52
+
53
+ return ctx.topDown;
54
+ }
@@ -107,6 +107,8 @@ export function allocateMultisample(
107
107
  depth: boolean,
108
108
  depthWrite: boolean,
109
109
  stencilWrite: number,
110
+ /** The texture's internal format, which its samples resolve into. */
111
+ storage: number = gl.RGBA8,
110
112
  ): MultisampleTarget {
111
113
  const same = old?.samples === samples;
112
114
  const newDepth = depth && (!same || !old.depth);
@@ -125,7 +127,7 @@ export function allocateMultisample(
125
127
  if (texture.cube && !same) {
126
128
  gl.activeTexture(gl.TEXTURE0);
127
129
  gl.bindTexture(gl.TEXTURE_2D, resolved);
128
- gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8, texture.width, texture.height);
130
+ gl.texStorage2D(gl.TEXTURE_2D, 1, storage, texture.width, texture.height);
129
131
  gl.bindFramebuffer(gl.FRAMEBUFFER, resolve);
130
132
  gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, resolved, 0);
131
133
  if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) {
@@ -148,7 +150,7 @@ export function allocateMultisample(
148
150
  gl.renderbufferStorageMultisample(
149
151
  gl.RENDERBUFFER,
150
152
  samples,
151
- gl.RGBA8,
153
+ storage,
152
154
  texture.width,
153
155
  texture.height,
154
156
  );
@@ -1,4 +1,4 @@
1
- import type { Texture3D } from "../backend.js";
1
+ import { compressedFormat, TEXEL_BYTES, type Texture3D, type TextureFormat } from "../backend.js";
2
2
  import { blockBytes, type DxtEncoder, decodeDxt, encodeDxt } from "../dxt.js";
3
3
  import { Context3DError } from "../errors.js";
4
4
  import { range } from "./state.js";
@@ -20,20 +20,169 @@ export interface TextureState {
20
20
  multisample: MultisampleTarget | null;
21
21
  samples: number;
22
22
  antiAlias: number;
23
+ /** The extra colour outputs its framebuffer has attached, a bit each from output 1. */
24
+ outputs: number;
23
25
  /** TEXTURE_BASE_LEVEL: a streaming texture's finest level whole to the smallest. */
24
26
  base: number;
25
27
  }
26
28
 
27
- /** The staging array, made base-sized at its first use: a texture given only blocks on S3TC has none. */
29
+ /**
30
+ * GL's internal format for an uncompressed texture's storage. The packed formats are kept
31
+ * at 8 bits a channel, as AIR draws into them, their texels widened on upload as Direct3D
32
+ * samples them; 5:6:5 has no alpha to read but 1.
33
+ */
34
+ export function storageFormat(gl: WebGL2RenderingContext, format: TextureFormat): number {
35
+ switch (format) {
36
+ case "bgrPacked565":
37
+ return gl.RGB8;
38
+ case "rgbaHalfFloat":
39
+ return gl.RGBA16F;
40
+ default:
41
+ return gl.RGBA8;
42
+ }
43
+ }
44
+
45
+ /** 8-bit channel values as half floats, for bitmaps uploaded to rgbaHalfFloat. */
46
+ let halves: Uint16Array | null = null;
47
+
48
+ function halfOf(byte: number): number {
49
+ if (!halves) {
50
+ halves = new Uint16Array(256);
51
+ const float = new Float32Array(1);
52
+ const bits = new Uint32Array(float.buffer);
53
+ for (let i = 1; i < 256; i++) {
54
+ float[0] = i / 255;
55
+ const word = bits[0];
56
+ const exponent = ((word >>> 23) & 255) - 127 + 15;
57
+ // Every value here is a normal half: round its 23-bit mantissa to 10 bits, to nearest even.
58
+ const mantissa = word & 0x7fffff;
59
+ let half = (exponent << 10) | (mantissa >>> 13);
60
+ const rest = mantissa & 0x1fff;
61
+ if (rest > 0x1000 || (rest === 0x1000 && half & 1)) {
62
+ half++;
63
+ }
64
+ halves[i] = half;
65
+ }
66
+ }
67
+
68
+ return halves[byte];
69
+ }
70
+
71
+ /** A channel cut to its high `bits` and widened back to 8 by repeating them, as Direct3D reads it. */
72
+ function widen(value: number, bits: number): number {
73
+ const kept = value >> (8 - bits);
74
+ return (kept << (8 - bits)) | (kept >> (2 * bits - 8));
75
+ }
76
+
77
+ /**
78
+ * The staging array, made base-sized at its first use: a texture given only blocks on S3TC
79
+ * has none. It holds a level as RGBA bytes, or a byte upload's texels as they came, with
80
+ * room for half floats' eight bytes a texel.
81
+ */
28
82
  function staging(texture: Texture3D, state: TextureState): Uint8Array {
29
83
  if (state.bytes.length === 0) {
30
- state.bytes = new Uint8Array(texture.width * texture.height * 4);
84
+ state.bytes = new Uint8Array(
85
+ texture.width * texture.height * Math.max(4, TEXEL_BYTES[texture.format] ?? 4),
86
+ );
31
87
  }
32
88
 
33
89
  return state.bytes;
34
90
  }
35
91
 
36
- /** The staged level uploaded: as it is, or a compressed texture's encoded to its blocks, as AIR compresses pixels. */
92
+ /**
93
+ * The staged level turned, in place, into what the texture's storage takes: from RGBA
94
+ * bytes, or from its format's texels where `raw`, little-endian words. 5:6:5 and 4:4:4:4
95
+ * keep each channel's high bits, as AIR packs a bitmap; half floats take each byte over 255.
96
+ */
97
+ function convertStaged(
98
+ format: TextureFormat,
99
+ bytes: Uint8Array,
100
+ count: number,
101
+ raw: boolean,
102
+ ): void {
103
+ const words = new Uint16Array(bytes.buffer, bytes.byteOffset, bytes.length >> 1);
104
+ switch (format) {
105
+ case "bgrPacked565":
106
+ if (raw) {
107
+ // Backwards: texel i's three bytes land at 3i, ahead of its word at 2i.
108
+ for (let i = count - 1; i >= 0; i--) {
109
+ const word = words[i];
110
+ const at = i * 3;
111
+ bytes[at] = ((word >> 11) << 3) | (word >> 13);
112
+ bytes[at + 1] = (((word >> 5) & 63) << 2) | ((word >> 9) & 3);
113
+ bytes[at + 2] = ((word & 31) << 3) | ((word >> 2) & 7);
114
+ }
115
+ } else {
116
+ // Forwards: texel i's three bytes land at 3i, behind its own four at 4i.
117
+ for (let i = 0; i < count; i++) {
118
+ const at = i * 4;
119
+ const to = i * 3;
120
+ const r = widen(bytes[at], 5);
121
+ const g = widen(bytes[at + 1], 6);
122
+ const b = widen(bytes[at + 2], 5);
123
+ bytes[to] = r;
124
+ bytes[to + 1] = g;
125
+ bytes[to + 2] = b;
126
+ }
127
+ }
128
+ break;
129
+ case "bgraPacked4444":
130
+ if (raw) {
131
+ // A word is alpha, red, green and blue from its high nibble down.
132
+ for (let i = count - 1; i >= 0; i--) {
133
+ const word = words[i];
134
+ const at = i * 4;
135
+ bytes[at] = ((word >> 8) & 15) * 17;
136
+ bytes[at + 1] = ((word >> 4) & 15) * 17;
137
+ bytes[at + 2] = (word & 15) * 17;
138
+ bytes[at + 3] = (word >> 12) * 17;
139
+ }
140
+ } else {
141
+ for (let i = 0; i < count * 4; i++) {
142
+ bytes[i] = (bytes[i] >> 4) * 17;
143
+ }
144
+ }
145
+ break;
146
+ case "rgbaHalfFloat":
147
+ if (!raw) {
148
+ // Backwards: texel i's four words land at 8i, ahead of its own bytes at 4i.
149
+ for (let i = count - 1; i >= 0; i--) {
150
+ const at = i * 4;
151
+ const r = halfOf(bytes[at]);
152
+ const g = halfOf(bytes[at + 1]);
153
+ const b = halfOf(bytes[at + 2]);
154
+ const a = halfOf(bytes[at + 3]);
155
+ words[i * 4] = r;
156
+ words[i * 4 + 1] = g;
157
+ words[i * 4 + 2] = b;
158
+ words[i * 4 + 3] = a;
159
+ }
160
+ }
161
+ break;
162
+ }
163
+ }
164
+
165
+ /** The staged level's texels, as WebGL takes them for the texture's storage. */
166
+ function stagedPixels(
167
+ gl: WebGL2RenderingContext,
168
+ format: TextureFormat,
169
+ bytes: Uint8Array,
170
+ count: number,
171
+ ): [number, number, ArrayBufferView] {
172
+ switch (format) {
173
+ case "bgrPacked565":
174
+ return [gl.RGB, gl.UNSIGNED_BYTE, bytes];
175
+ case "rgbaHalfFloat":
176
+ return [gl.RGBA, gl.HALF_FLOAT, new Uint16Array(bytes.buffer, bytes.byteOffset, count * 4)];
177
+ default:
178
+ return [gl.RGBA, gl.UNSIGNED_BYTE, bytes];
179
+ }
180
+ }
181
+
182
+ /**
183
+ * The staged level uploaded: as it is, or a compressed texture's encoded to its blocks, as
184
+ * AIR compresses pixels; `raw` where it is staged in the texture's own format, not as RGBA.
185
+ */
37
186
  function uploadLevel(
38
187
  gl: WebGL2RenderingContext,
39
188
  texture: Texture3D,
@@ -43,8 +192,9 @@ function uploadLevel(
43
192
  w: number,
44
193
  h: number,
45
194
  encoder: DxtEncoder | null,
195
+ raw = false,
46
196
  ): void {
47
- if (texture.format !== "bgra") {
197
+ if (compressedFormat(texture.format)) {
48
198
  const rgba = state.bytes.subarray(0, w * h * 4);
49
199
  const dxt5 = texture.format === "compressedAlpha";
50
200
  const blocks = encoder?.(rgba, dxt5, w, h) ?? encodeDxt(rgba, dxt5, w, h);
@@ -52,6 +202,18 @@ function uploadLevel(
52
202
  return;
53
203
  }
54
204
 
205
+ // BGRA's staging is RGBA bytes already: no conversion and no views.
206
+ const bgra = texture.format === "bgra";
207
+ if (!bgra) {
208
+ convertStaged(texture.format, state.bytes, w * h, raw);
209
+ }
210
+ const [layout, type, pixels] = bgra
211
+ ? [gl.RGBA, gl.UNSIGNED_BYTE, state.bytes]
212
+ : stagedPixels(gl, texture.format, state.bytes, w * h);
213
+ // 5:6:5's rows of three bytes need not be 4-byte aligned.
214
+ if (texture.format === "bgrPacked565") {
215
+ gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
216
+ }
55
217
  const cube = texture.cube;
56
218
  const target = cube ? gl.TEXTURE_CUBE_MAP : gl.TEXTURE_2D;
57
219
  gl.activeTexture(gl.TEXTURE0);
@@ -63,10 +225,13 @@ function uploadLevel(
63
225
  0,
64
226
  w,
65
227
  h,
66
- gl.RGBA,
67
- gl.UNSIGNED_BYTE,
68
- state.bytes,
228
+ layout,
229
+ type,
230
+ pixels,
69
231
  );
232
+ if (texture.format === "bgrPacked565") {
233
+ gl.pixelStorei(gl.UNPACK_ALIGNMENT, 4);
234
+ }
70
235
  state.uploaded[face] |= 1 << level;
71
236
  }
72
237
 
@@ -149,10 +314,19 @@ export function uploadTextureBytes(
149
314
  range(face, 1, faces);
150
315
  const w = Math.max(1, width >> level);
151
316
  const h = Math.max(1, height >> level);
152
- const length = w * h * 4;
317
+ const texel = TEXEL_BYTES[texture.format] ?? 4;
318
+ const length = w * h * texel;
153
319
  range(offset, length, data.length);
154
320
  staging(texture, state);
155
321
 
322
+ // BGRA, and the compressed formats' pixels, which are BGRA too, go up as RGBA; the
323
+ // others' texels as they are, little-endian words.
324
+ if (texel !== 4) {
325
+ state.bytes.set(data.subarray(offset, offset + length));
326
+ uploadLevel(gl, texture, state, level, face, w, h, encoder, true);
327
+ return;
328
+ }
329
+
156
330
  for (let i = 0; i < length; i += 4) {
157
331
  state.bytes[i] = data[offset + i + 2];
158
332
  state.bytes[i + 1] = data[offset + i + 1];