@swf2es/native 0.0.0-stage → 0.1.0
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/LICENSE.third-party +313 -0
- package/README.md +489 -2
- package/dist/atf/binary.d.ts +2 -0
- package/dist/atf/binary.d.ts.map +1 -0
- package/dist/atf/binary.js +4 -0
- package/dist/atf/binary.js.map +1 -0
- package/dist/atf/index.d.ts +27 -0
- package/dist/atf/index.d.ts.map +1 -0
- package/dist/atf/index.js +158 -0
- package/dist/atf/index.js.map +1 -0
- package/dist/atf/jxr.d.ts +22 -0
- package/dist/atf/jxr.d.ts.map +1 -0
- package/dist/atf/jxr.js +60 -0
- package/dist/atf/jxr.js.map +1 -0
- package/dist/atf/parse.d.ts +65 -0
- package/dist/atf/parse.d.ts.map +1 -0
- package/dist/atf/parse.js +204 -0
- package/dist/atf/parse.js.map +1 -0
- package/dist/audio/binary.d.ts +2 -0
- package/dist/audio/binary.d.ts.map +1 -0
- package/dist/audio/binary.js +4 -0
- package/dist/audio/binary.js.map +1 -0
- package/dist/audio/index.d.ts +21 -0
- package/dist/audio/index.d.ts.map +1 -0
- package/dist/audio/index.js +73 -0
- package/dist/audio/index.js.map +1 -0
- package/dist/base64.d.ts +3 -0
- package/dist/base64.d.ts.map +1 -0
- package/dist/base64.js +10 -0
- package/dist/base64.js.map +1 -0
- package/dist/compress/binary.d.ts +2 -0
- package/dist/compress/binary.d.ts.map +1 -0
- package/dist/compress/binary.js +4 -0
- package/dist/compress/binary.js.map +1 -0
- package/dist/compress/index.d.ts +59 -0
- package/dist/compress/index.d.ts.map +1 -0
- package/dist/compress/index.js +146 -0
- package/dist/compress/index.js.map +1 -0
- package/dist/instance.d.ts +25 -0
- package/dist/instance.d.ts.map +1 -0
- package/dist/instance.js +50 -0
- package/dist/instance.js.map +1 -0
- package/dist/mp3/binary.d.ts +2 -0
- package/dist/mp3/binary.d.ts.map +1 -0
- package/dist/mp3/binary.js +4 -0
- package/dist/mp3/binary.js.map +1 -0
- package/dist/mp3/index.d.ts +20 -0
- package/dist/mp3/index.d.ts.map +1 -0
- package/dist/mp3/index.js +49 -0
- package/dist/mp3/index.js.map +1 -0
- package/dist/render/binary.d.ts +2 -0
- package/dist/render/binary.d.ts.map +1 -0
- package/dist/render/binary.js +4 -0
- package/dist/render/binary.js.map +1 -0
- package/dist/render/bitmap-kernel.d.ts +118 -0
- package/dist/render/bitmap-kernel.d.ts.map +1 -0
- package/dist/render/bitmap-kernel.js +592 -0
- package/dist/render/bitmap-kernel.js.map +1 -0
- package/dist/render/draw-kernel.d.ts +79 -0
- package/dist/render/draw-kernel.d.ts.map +1 -0
- package/dist/render/draw-kernel.js +205 -0
- package/dist/render/draw-kernel.js.map +1 -0
- package/dist/render/dxt-encoder.d.ts +19 -0
- package/dist/render/dxt-encoder.d.ts.map +1 -0
- package/dist/render/dxt-encoder.js +95 -0
- package/dist/render/dxt-encoder.js.map +1 -0
- package/dist/render/index.d.ts +6 -0
- package/dist/render/index.d.ts.map +1 -0
- package/dist/render/index.js +11 -0
- package/dist/render/index.js.map +1 -0
- package/dist/render/lines.d.ts +55 -0
- package/dist/render/lines.d.ts.map +1 -0
- package/dist/render/lines.js +123 -0
- package/dist/render/lines.js.map +1 -0
- package/dist/render/module.d.ts +11 -0
- package/dist/render/module.d.ts.map +1 -0
- package/dist/render/module.js +27 -0
- package/dist/render/module.js.map +1 -0
- package/dist/render/pixel-ops.d.ts +77 -0
- package/dist/render/pixel-ops.d.ts.map +1 -0
- package/dist/render/pixel-ops.js +236 -0
- package/dist/render/pixel-ops.js.map +1 -0
- package/dist/render/tessellator.d.ts +90 -0
- package/dist/render/tessellator.d.ts.map +1 -0
- package/dist/render/tessellator.js +251 -0
- package/dist/render/tessellator.js.map +1 -0
- package/package.json +53 -3
- package/src/atf/index.ts +212 -0
- package/src/atf/jxr.ts +83 -0
- package/src/atf/parse.ts +263 -0
- package/src/audio/index.ts +93 -0
- package/src/base64.ts +10 -0
- package/src/compress/index.ts +225 -0
- package/src/instance.ts +53 -0
- package/src/mp3/index.ts +71 -0
- package/src/render/bitmap-kernel.ts +856 -0
- package/src/render/draw-kernel.ts +268 -0
- package/src/render/dxt-encoder.ts +121 -0
- package/src/render/index.ts +37 -0
- package/src/render/lines.ts +161 -0
- package/src/render/module.ts +30 -0
- package/src/render/pixel-ops.ts +356 -0
- package/src/render/tessellator.ts +367 -0
- package/src/webassembly.d.ts +22 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// BitmapData.draw of a bitmap in the normal blend mode, in the render
|
|
2
|
+
// module (draw.zig), one call a draw. Each draw copies into the module's
|
|
3
|
+
// input the rows of the source it reads and the destination's area, and
|
|
4
|
+
// the area back out after: no source is kept between draws, which costs no
|
|
5
|
+
// more than keeping them did for a game drawing hundreds of small bitmaps
|
|
6
|
+
// a frame, and holds nothing for them. Its own instance, so that the
|
|
7
|
+
// filters' calls (BitmapKernel) do not reset its input, let go of when its
|
|
8
|
+
// owner says it went idle, and after a draw that grew it past KEEP_BYTES.
|
|
9
|
+
import { renderInstance } from "./module.js";
|
|
10
|
+
|
|
11
|
+
interface DrawExports {
|
|
12
|
+
memory: WebAssembly.Memory;
|
|
13
|
+
resetArena(bytes: number): number;
|
|
14
|
+
drawPixels(params: number): number;
|
|
15
|
+
compositePixels(params: number): number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A draw, which its caller keeps and fills for each: the source's pixels
|
|
20
|
+
* and size and the rows it reads, the destination's pixels (read only) and
|
|
21
|
+
* width and the area drawn, and the numbers draw.zig takes.
|
|
22
|
+
*/
|
|
23
|
+
export interface DrawJob {
|
|
24
|
+
source: Uint32Array;
|
|
25
|
+
sourceWidth: number;
|
|
26
|
+
sourceHeight: number;
|
|
27
|
+
/** The first row the draw reads, and how many. */
|
|
28
|
+
sourceTop: number;
|
|
29
|
+
sourceRows: number;
|
|
30
|
+
into: Uint32Array;
|
|
31
|
+
intoWidth: number;
|
|
32
|
+
x: number;
|
|
33
|
+
y: number;
|
|
34
|
+
width: number;
|
|
35
|
+
height: number;
|
|
36
|
+
/** composite's, unscaled at whole pixels (tx, ty), rather than drawBitmap's through the matrix. */
|
|
37
|
+
moved: boolean;
|
|
38
|
+
/** The matrix's inverse: a, b, c, d, tx, ty. */
|
|
39
|
+
inverse: Float64Array;
|
|
40
|
+
/** The prepared colour transform, multipliers in 256ths then offsets, red, green, blue, alpha. */
|
|
41
|
+
transform: Float64Array;
|
|
42
|
+
tx: number;
|
|
43
|
+
ty: number;
|
|
44
|
+
flags: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** draw.zig's flags. */
|
|
48
|
+
export const DRAW_SMOOTH = 1;
|
|
49
|
+
export const DRAW_TRANSFORM = 2;
|
|
50
|
+
export const DRAW_OPAQUE = 4;
|
|
51
|
+
export const DRAW_SKIP_ZERO_ALPHA = 8;
|
|
52
|
+
|
|
53
|
+
// draw.zig's results.
|
|
54
|
+
const INVALID = 0;
|
|
55
|
+
const WRITTEN = 2;
|
|
56
|
+
/** draw.zig's DrawParams: 14 doubles, then 14 words. */
|
|
57
|
+
const PARAMS = 168;
|
|
58
|
+
/** The most a draw copies in, its source's rows and its area; a larger one is drawn by the caller. */
|
|
59
|
+
const DRAWN_BYTES = 32 << 20;
|
|
60
|
+
/** The input a new instance takes first. */
|
|
61
|
+
const FIRST_BYTES = 1 << 20;
|
|
62
|
+
/** The memory an instance keeps after a draw; past it, the instance is let go of. */
|
|
63
|
+
const KEEP_BYTES = 16 << 20;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* What a draw did: refused, where the module could not take it and the
|
|
67
|
+
* caller draws it itself; drew nothing; or wrote the area, which `writeBack`
|
|
68
|
+
* then copies into the store.
|
|
69
|
+
*/
|
|
70
|
+
export type DrawResult = "refused" | "untouched" | "written";
|
|
71
|
+
|
|
72
|
+
export class DrawKernel {
|
|
73
|
+
private wasm: DrawExports | null = null;
|
|
74
|
+
/** Whether making an instance failed once, after which the module is never tried again. */
|
|
75
|
+
private broken = false;
|
|
76
|
+
/** The input resetArena gave, and its bytes. */
|
|
77
|
+
private input = 0;
|
|
78
|
+
private inputBytes = 0;
|
|
79
|
+
private buffer: ArrayBuffer | null = null;
|
|
80
|
+
private words = new Uint32Array(0);
|
|
81
|
+
private doubles = new Float64Array(0);
|
|
82
|
+
private ints = new Int32Array(0);
|
|
83
|
+
/** Where the last draw's area lies in the input. */
|
|
84
|
+
private area = 0;
|
|
85
|
+
/** Whether the module drew since `idle` last asked. */
|
|
86
|
+
private busy = false;
|
|
87
|
+
/** How many draws the module took. */
|
|
88
|
+
drawn = 0;
|
|
89
|
+
|
|
90
|
+
/** A job to fill for each draw. */
|
|
91
|
+
static job(): DrawJob {
|
|
92
|
+
return {
|
|
93
|
+
source: new Uint32Array(0),
|
|
94
|
+
sourceWidth: 0,
|
|
95
|
+
sourceHeight: 0,
|
|
96
|
+
sourceTop: 0,
|
|
97
|
+
sourceRows: 0,
|
|
98
|
+
into: new Uint32Array(0),
|
|
99
|
+
intoWidth: 0,
|
|
100
|
+
x: 0,
|
|
101
|
+
y: 0,
|
|
102
|
+
width: 0,
|
|
103
|
+
height: 0,
|
|
104
|
+
moved: false,
|
|
105
|
+
inverse: new Float64Array(6),
|
|
106
|
+
transform: new Float64Array(8),
|
|
107
|
+
tx: 0,
|
|
108
|
+
ty: 0,
|
|
109
|
+
flags: 0,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Whether an instance is held, with its memory. */
|
|
114
|
+
get held(): boolean {
|
|
115
|
+
return this.wasm !== null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The job drawn in the module; never a write to the destination, which `writeBack` makes. */
|
|
119
|
+
draw(job: DrawJob): DrawResult {
|
|
120
|
+
try {
|
|
121
|
+
return this.run(job);
|
|
122
|
+
} catch {
|
|
123
|
+
// A trap: never resume that instance.
|
|
124
|
+
this.release();
|
|
125
|
+
return "refused";
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The area the last draw wrote, copied into `into`. */
|
|
130
|
+
writeBack(job: DrawJob, into: Uint32Array): void {
|
|
131
|
+
const { x, y, width: w, height: h, intoWidth } = job;
|
|
132
|
+
const base = this.area >> 2;
|
|
133
|
+
this.refresh();
|
|
134
|
+
const words = this.words;
|
|
135
|
+
for (let row = 0; row < h; row++) {
|
|
136
|
+
const at = base + row * w;
|
|
137
|
+
into.set(words.subarray(at, at + w), (y + row) * intoWidth + x);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Whether the module drew nothing since this was last asked. */
|
|
142
|
+
idle(): boolean {
|
|
143
|
+
const idle = !this.busy;
|
|
144
|
+
this.busy = false;
|
|
145
|
+
return idle;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The instance let go of, and its memory with it; the next draw makes another. */
|
|
149
|
+
release(): void {
|
|
150
|
+
this.wasm = null;
|
|
151
|
+
this.buffer = null;
|
|
152
|
+
this.words = new Uint32Array(0);
|
|
153
|
+
this.doubles = new Float64Array(0);
|
|
154
|
+
this.ints = new Int32Array(0);
|
|
155
|
+
this.input = 0;
|
|
156
|
+
this.inputBytes = 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private run(job: DrawJob): DrawResult {
|
|
160
|
+
const { sourceWidth: sw, sourceRows: rows, width: w, height: h } = job;
|
|
161
|
+
const sourceBytes = sw * rows * 4;
|
|
162
|
+
const areaBytes = w * h * 4;
|
|
163
|
+
if (
|
|
164
|
+
job.source.length !== sw * job.sourceHeight ||
|
|
165
|
+
sourceBytes + areaBytes > DRAWN_BYTES ||
|
|
166
|
+
!this.room(PARAMS + sourceBytes + areaBytes)
|
|
167
|
+
) {
|
|
168
|
+
return "refused";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const wasm = this.wasm as DrawExports;
|
|
172
|
+
this.refresh();
|
|
173
|
+
const { words, doubles, ints } = this;
|
|
174
|
+
const p = this.input;
|
|
175
|
+
const src = p + PARAMS;
|
|
176
|
+
this.area = src + sourceBytes;
|
|
177
|
+
const top = job.sourceTop * sw;
|
|
178
|
+
words.set(job.source.subarray(top, top + rows * sw), src >> 2);
|
|
179
|
+
|
|
180
|
+
const base = this.area >> 2;
|
|
181
|
+
const { into, intoWidth, x, y } = job;
|
|
182
|
+
for (let row = 0; row < h; row++) {
|
|
183
|
+
const at = (y + row) * intoWidth + x;
|
|
184
|
+
words.set(into.subarray(at, at + w), base + row * w);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const d = p >> 3;
|
|
188
|
+
doubles.set(job.inverse, d);
|
|
189
|
+
doubles.set(job.transform, d + 6);
|
|
190
|
+
const i = (p + 112) >> 2;
|
|
191
|
+
words[i] = src;
|
|
192
|
+
words[i + 1] = sw;
|
|
193
|
+
words[i + 2] = job.sourceHeight;
|
|
194
|
+
words[i + 3] = this.area;
|
|
195
|
+
ints[i + 4] = x;
|
|
196
|
+
ints[i + 5] = y;
|
|
197
|
+
words[i + 6] = w;
|
|
198
|
+
words[i + 7] = h;
|
|
199
|
+
words[i + 8] = job.flags;
|
|
200
|
+
ints[i + 9] = job.tx;
|
|
201
|
+
ints[i + 10] = job.ty;
|
|
202
|
+
words[i + 11] = job.sourceTop;
|
|
203
|
+
words[i + 12] = rows;
|
|
204
|
+
words[i + 13] = 0;
|
|
205
|
+
const result = job.moved ? wasm.compositePixels(p) : wasm.drawPixels(p);
|
|
206
|
+
if (result === INVALID) {
|
|
207
|
+
return "refused";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
this.drawn++;
|
|
211
|
+
this.busy = true;
|
|
212
|
+
return result === WRITTEN ? "written" : "untouched";
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Whether the input holds `bytes`: an instance made where there is none,
|
|
217
|
+
* one grown past KEEP_BYTES made anew, and the input taken anew where it
|
|
218
|
+
* is smaller.
|
|
219
|
+
*/
|
|
220
|
+
private room(bytes: number): boolean {
|
|
221
|
+
if (this.broken) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Grown past what it keeps by a large draw before, it goes, and its memory with it.
|
|
226
|
+
if (this.wasm && this.wasm.memory.buffer.byteLength > KEEP_BYTES) {
|
|
227
|
+
this.release();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (this.wasm && this.inputBytes >= bytes) {
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!this.wasm) {
|
|
235
|
+
try {
|
|
236
|
+
this.wasm = renderInstance(() => {
|
|
237
|
+
throw new Error("the draw kernel takes no tessellator");
|
|
238
|
+
}) as unknown as DrawExports;
|
|
239
|
+
} catch {
|
|
240
|
+
this.broken = true;
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Half again what is asked, so that a few larger draws do not each take it anew.
|
|
246
|
+
const want = Math.max(FIRST_BYTES, bytes + (bytes >> 1));
|
|
247
|
+
this.input = this.wasm.resetArena(want);
|
|
248
|
+
if (!this.input) {
|
|
249
|
+
// Out of memory: the instance goes, and the next draw that fits makes another.
|
|
250
|
+
this.release();
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
this.inputBytes = want;
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** The views of the memory made again where it grew, which detached them. */
|
|
259
|
+
private refresh(): void {
|
|
260
|
+
const buffer = (this.wasm as DrawExports).memory.buffer;
|
|
261
|
+
if (buffer !== this.buffer) {
|
|
262
|
+
this.buffer = buffer;
|
|
263
|
+
this.words = new Uint32Array(buffer);
|
|
264
|
+
this.doubles = new Float64Array(buffer);
|
|
265
|
+
this.ints = new Int32Array(buffer);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Compressed texture uploads, one whole level at a time in dxt.zig. The
|
|
2
|
+
// input is kept between levels and grown only as needed; the result is a
|
|
3
|
+
// view to consume before the next call, as context3d's TS encoder gives.
|
|
4
|
+
import { renderInstance } from "./module.js";
|
|
5
|
+
|
|
6
|
+
interface DxtExports {
|
|
7
|
+
memory: WebAssembly.Memory;
|
|
8
|
+
resetArena(bytes: number): number;
|
|
9
|
+
encodeDxt(
|
|
10
|
+
source: number,
|
|
11
|
+
destination: number,
|
|
12
|
+
width: number,
|
|
13
|
+
height: number,
|
|
14
|
+
alpha: number,
|
|
15
|
+
): number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Enough to keep a 2048² DXT5 upload's input and output, including growth slack. */
|
|
19
|
+
const KEEP_BYTES = 32 << 20;
|
|
20
|
+
const FIRST_BYTES = 64 << 10;
|
|
21
|
+
|
|
22
|
+
export class DxtEncoder {
|
|
23
|
+
private wasm: DxtExports | null = null;
|
|
24
|
+
private broken = false;
|
|
25
|
+
private input = 0;
|
|
26
|
+
private inputBytes = 0;
|
|
27
|
+
/** Successful native encodes, so a reference comparison can rule out falling back. */
|
|
28
|
+
encoded = 0;
|
|
29
|
+
|
|
30
|
+
/** The memory kept now: a larger level's instance goes before the next call. */
|
|
31
|
+
get capacity(): number {
|
|
32
|
+
return this.wasm?.memory.buffer.byteLength ?? 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Release the instance and its memory; the next level makes another. */
|
|
36
|
+
release(): void {
|
|
37
|
+
this.wasm = null;
|
|
38
|
+
this.input = 0;
|
|
39
|
+
this.inputBytes = 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Top-down RGBA to DXT1 or DXT5, matching the TS encoder's blocks. The
|
|
44
|
+
* result stays valid until the next call; null leaves the level to TS.
|
|
45
|
+
*/
|
|
46
|
+
encode = (rgba: Uint8Array, dxt5: boolean, width: number, height: number): Uint8Array | null => {
|
|
47
|
+
if (
|
|
48
|
+
!Number.isInteger(width) ||
|
|
49
|
+
!Number.isInteger(height) ||
|
|
50
|
+
width <= 0 ||
|
|
51
|
+
height <= 0 ||
|
|
52
|
+
width > 4096 ||
|
|
53
|
+
height > 4096 ||
|
|
54
|
+
rgba.length !== width * height * 4
|
|
55
|
+
) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
// A caller may use an earlier result as a smaller level's input. Preserve it if growth
|
|
61
|
+
// detaches that view, or a large earlier level causes its instance to be replaced.
|
|
62
|
+
if (this.wasm && rgba.buffer === this.wasm.memory.buffer) {
|
|
63
|
+
rgba = rgba.slice();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const length = Math.ceil(width / 4) * Math.ceil(height / 4) * (dxt5 ? 16 : 8);
|
|
67
|
+
if (!this.room(rgba.length + length)) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const wasm = this.wasm as DxtExports;
|
|
72
|
+
const destination = this.input + rgba.length;
|
|
73
|
+
new Uint8Array(wasm.memory.buffer, this.input, rgba.length).set(rgba);
|
|
74
|
+
if (!wasm.encodeDxt(this.input, destination, width, height, dxt5 ? 1 : 0)) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this.encoded++;
|
|
79
|
+
return new Uint8Array(wasm.memory.buffer, destination, length);
|
|
80
|
+
} catch {
|
|
81
|
+
// A trapped instance is never resumed, and a refused module leaves the TS path available.
|
|
82
|
+
this.release();
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
private room(bytes: number): boolean {
|
|
88
|
+
if (this.broken) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (this.wasm && this.wasm.memory.buffer.byteLength > KEEP_BYTES) {
|
|
93
|
+
this.release();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (this.wasm && this.inputBytes >= bytes) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!this.wasm) {
|
|
101
|
+
try {
|
|
102
|
+
this.wasm = renderInstance(() => {
|
|
103
|
+
throw new Error("the DXT encoder takes no tessellator");
|
|
104
|
+
}) as unknown as DxtExports;
|
|
105
|
+
} catch {
|
|
106
|
+
this.broken = true;
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const want = Math.max(FIRST_BYTES, bytes + Math.floor(bytes / 2));
|
|
112
|
+
this.input = this.wasm.resetArena(want);
|
|
113
|
+
if (!this.input) {
|
|
114
|
+
this.release();
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this.inputBytes = want;
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// The render module's wrappers, each over an instance of its own: fills
|
|
2
|
+
// (Tessellator), a layer's lines (LineBuilder, also a Tessellator's
|
|
3
|
+
// `tessellate.lines`) and BitmapData's CPU filters and pixel operations
|
|
4
|
+
// (BitmapKernel), BitmapData.draw of a bitmap (DrawKernel), and DXT1/DXT5
|
|
5
|
+
// texture encoding (DxtEncoder).
|
|
6
|
+
export {
|
|
7
|
+
BitmapKernel,
|
|
8
|
+
type BitmapKernelOptions,
|
|
9
|
+
type BitmapRect,
|
|
10
|
+
type BitmapSource,
|
|
11
|
+
type ConvolutionFilter,
|
|
12
|
+
type CopyOp,
|
|
13
|
+
type LayerFilter,
|
|
14
|
+
type PerlinSetup,
|
|
15
|
+
type PixelRegion,
|
|
16
|
+
} from "./bitmap-kernel.js";
|
|
17
|
+
export {
|
|
18
|
+
DRAW_OPAQUE,
|
|
19
|
+
DRAW_SKIP_ZERO_ALPHA,
|
|
20
|
+
DRAW_SMOOTH,
|
|
21
|
+
DRAW_TRANSFORM,
|
|
22
|
+
type DrawJob,
|
|
23
|
+
DrawKernel,
|
|
24
|
+
type DrawResult,
|
|
25
|
+
} from "./draw-kernel.js";
|
|
26
|
+
export { DxtEncoder } from "./dxt-encoder.js";
|
|
27
|
+
export { LineBuilder, type LineBuilderOptions, type LineGeometry } from "./lines.js";
|
|
28
|
+
export {
|
|
29
|
+
type FillGeometry,
|
|
30
|
+
type FillTessellate,
|
|
31
|
+
PATH_CUBIC,
|
|
32
|
+
PATH_CURVE,
|
|
33
|
+
PATH_LINE,
|
|
34
|
+
PATH_MOVE,
|
|
35
|
+
Tessellator,
|
|
36
|
+
type TessellatorOptions,
|
|
37
|
+
} from "./tessellator.js";
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { renderInstance, renderModule } from "./module.js";
|
|
2
|
+
|
|
3
|
+
/** A layer's lines as triangles: two floats a vertex, its colour, and three indices a triangle. */
|
|
4
|
+
export interface LineGeometry {
|
|
5
|
+
positions: Float32Array;
|
|
6
|
+
colors: Uint32Array;
|
|
7
|
+
indices: Uint32Array;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface NativeExports {
|
|
11
|
+
memory: WebAssembly.Memory;
|
|
12
|
+
lineInput(n: number): number;
|
|
13
|
+
lineHeader(): number;
|
|
14
|
+
lineBuild(length: number): number;
|
|
15
|
+
setLimit(n: number): void;
|
|
16
|
+
bandsOnly(on: number): void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface LineBuilderOptions {
|
|
20
|
+
/** Maximum arena address, including the C stack; at most 128 MiB. */
|
|
21
|
+
memoryLimit?: number;
|
|
22
|
+
/** Drop instances larger than this after each call. Defaults to 8 MiB. */
|
|
23
|
+
retainMemory?: number;
|
|
24
|
+
/** Fill outlines by the module's bands alone, never by libtess2: for tests of the bands. */
|
|
25
|
+
bands?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A shape layer's lines built in one call by the render module's
|
|
30
|
+
* strokes.zig, its outlines filled by the module's libtess2, or its bands
|
|
31
|
+
* where libtess2 fails: the caller writes the layer into input() as
|
|
32
|
+
* strokes.zig reads it (the player's scene/strokes.ts) and calls build()
|
|
33
|
+
* with its length. Its instance is its own, apart from any Tessellator's,
|
|
34
|
+
* and held to the same limits as a Tessellator's (TessellatorOptions),
|
|
35
|
+
* which pass them on: where a fill would have run out of memory, so do the
|
|
36
|
+
* lines, and none are drawn.
|
|
37
|
+
*/
|
|
38
|
+
export class LineBuilder {
|
|
39
|
+
private instance: NativeExports | null = null;
|
|
40
|
+
private view = new Float64Array(0);
|
|
41
|
+
private layout = new Uint32Array(0);
|
|
42
|
+
/** libtess2 trapped the last instance: the next fills its outlines by the bands alone. */
|
|
43
|
+
private trapped = false;
|
|
44
|
+
private lastUnwound = false;
|
|
45
|
+
private readonly limit: number;
|
|
46
|
+
private readonly keep: number;
|
|
47
|
+
private readonly bands: boolean;
|
|
48
|
+
|
|
49
|
+
constructor(options: LineBuilderOptions = {}) {
|
|
50
|
+
this.limit = options.memoryLimit ?? 128 << 20;
|
|
51
|
+
this.keep = options.retainMemory ?? 8 << 20;
|
|
52
|
+
this.bands = options.bands ?? false;
|
|
53
|
+
renderModule();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Whether the last build() unwound, which the next, in a new instance by the bands, may not. */
|
|
57
|
+
get unwound(): boolean {
|
|
58
|
+
return this.lastUnwound;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Current retained Wasm capacity. */
|
|
62
|
+
get capacity(): number {
|
|
63
|
+
return this.instance?.memory.buffer.byteLength ?? 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A view of at least `n` f64s of input, valid until build(); null where no instance could be made. */
|
|
67
|
+
input(n: number): Float64Array | null {
|
|
68
|
+
if (!this.instance) {
|
|
69
|
+
try {
|
|
70
|
+
this.instance = renderInstance(() => {
|
|
71
|
+
throw new Error("line tessellation allocation failed");
|
|
72
|
+
}) as unknown as NativeExports;
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.instance.setLimit(this.limit);
|
|
78
|
+
this.instance.bandsOnly(Number(this.bands || this.trapped));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const x = this.instance;
|
|
82
|
+
if (n > this.view.length || this.view.buffer !== x.memory.buffer) {
|
|
83
|
+
const size = Math.max(n, this.view.length * 2, 1024);
|
|
84
|
+
const at = x.lineInput(size);
|
|
85
|
+
if (!at) {
|
|
86
|
+
this.dispose();
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this.view = new Float64Array(x.memory.buffer, at, size);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return this.view;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The layer in the input's first `length` f64s, copied out in one
|
|
98
|
+
* block: null for none, undefined where the module refused it (a
|
|
99
|
+
* position not a number) or failed. Where libtess2 trapped, the next
|
|
100
|
+
* instance fills the layer's outlines by the bands alone, for the
|
|
101
|
+
* caller's next input() and build().
|
|
102
|
+
*/
|
|
103
|
+
build(length: number): LineGeometry | null | undefined {
|
|
104
|
+
this.lastUnwound = false;
|
|
105
|
+
const x = this.instance;
|
|
106
|
+
if (!x) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const status = x.lineBuild(length);
|
|
112
|
+
// Built without unwinding: libtess2 again for the next.
|
|
113
|
+
if (this.trapped) {
|
|
114
|
+
this.trapped = false;
|
|
115
|
+
x.bandsOnly(Number(this.bands));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (status !== 0) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const [at, vertices, count] = this.header(x);
|
|
123
|
+
if (count === 0) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const copy = x.memory.buffer.slice(at, at + (vertices * 3 + count) * 4);
|
|
128
|
+
return {
|
|
129
|
+
positions: new Float32Array(copy, 0, vertices * 2),
|
|
130
|
+
colors: new Uint32Array(copy, vertices * 8, vertices),
|
|
131
|
+
indices: new Uint32Array(copy, vertices * 12, count),
|
|
132
|
+
};
|
|
133
|
+
} catch {
|
|
134
|
+
// libtess2 unwound through the failure import: never resume that instance.
|
|
135
|
+
this.dispose();
|
|
136
|
+
this.trapped = true;
|
|
137
|
+
this.lastUnwound = true;
|
|
138
|
+
return undefined;
|
|
139
|
+
} finally {
|
|
140
|
+
if (this.capacity > this.keep) {
|
|
141
|
+
this.dispose();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** strokes.zig's layout, read again where memory grew. */
|
|
147
|
+
private header(x: NativeExports): Uint32Array {
|
|
148
|
+
if (this.layout.buffer !== x.memory.buffer) {
|
|
149
|
+
this.layout = new Uint32Array(x.memory.buffer, x.lineHeader(), 3);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return this.layout;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The instance let go, its memory with it; the next input() makes another. */
|
|
156
|
+
dispose(): void {
|
|
157
|
+
this.instance = null;
|
|
158
|
+
this.view = new Float64Array(0);
|
|
159
|
+
this.layout = new Uint32Array(0);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { decodeBase64 } from "../base64.js";
|
|
2
|
+
import { binary } from "./binary.js";
|
|
3
|
+
|
|
4
|
+
let module: WebAssembly.Module | null = null;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The render module (root.zig), compiled once and shared: the bitmap
|
|
8
|
+
* kernels, DXT encoder, tessellator and lines each make an instance of their
|
|
9
|
+
* own, so that each has its memory and lifecycle apart. It needs wasm
|
|
10
|
+
* SIMD throughout; this throws where the host refuses to compile it, as
|
|
11
|
+
* without SIMD or Wasm or under a CSP without 'wasm-unsafe-eval'.
|
|
12
|
+
*/
|
|
13
|
+
export function renderModule(): WebAssembly.Module {
|
|
14
|
+
module ??= new WebAssembly.Module(decodeBase64(binary));
|
|
15
|
+
return module;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* What an instance imports: libtess2's longjmp, which `failure` must
|
|
20
|
+
* throw from, and the host's own sine and cosine, which the lines take so
|
|
21
|
+
* that theirs are the player's JS's to the last bit.
|
|
22
|
+
*/
|
|
23
|
+
function renderImports(failure: () => never): object {
|
|
24
|
+
return { env: { failure, sine: Math.sin, cosine: Math.cos } };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** An instance of the module, its memory its own. */
|
|
28
|
+
export function renderInstance(failure: () => never): Record<string, unknown> {
|
|
29
|
+
return new WebAssembly.Instance(renderModule(), renderImports(failure)).exports;
|
|
30
|
+
}
|