@swf2es/native 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.
Files changed (105) hide show
  1. package/LICENSE +202 -0
  2. package/LICENSE.third-party +313 -0
  3. package/README.md +489 -2
  4. package/dist/atf/binary.d.ts +2 -0
  5. package/dist/atf/binary.d.ts.map +1 -0
  6. package/dist/atf/binary.js +4 -0
  7. package/dist/atf/binary.js.map +1 -0
  8. package/dist/atf/index.d.ts +27 -0
  9. package/dist/atf/index.d.ts.map +1 -0
  10. package/dist/atf/index.js +158 -0
  11. package/dist/atf/index.js.map +1 -0
  12. package/dist/atf/jxr.d.ts +22 -0
  13. package/dist/atf/jxr.d.ts.map +1 -0
  14. package/dist/atf/jxr.js +60 -0
  15. package/dist/atf/jxr.js.map +1 -0
  16. package/dist/atf/parse.d.ts +65 -0
  17. package/dist/atf/parse.d.ts.map +1 -0
  18. package/dist/atf/parse.js +204 -0
  19. package/dist/atf/parse.js.map +1 -0
  20. package/dist/audio/binary.d.ts +2 -0
  21. package/dist/audio/binary.d.ts.map +1 -0
  22. package/dist/audio/binary.js +4 -0
  23. package/dist/audio/binary.js.map +1 -0
  24. package/dist/audio/index.d.ts +21 -0
  25. package/dist/audio/index.d.ts.map +1 -0
  26. package/dist/audio/index.js +73 -0
  27. package/dist/audio/index.js.map +1 -0
  28. package/dist/base64.d.ts +3 -0
  29. package/dist/base64.d.ts.map +1 -0
  30. package/dist/base64.js +10 -0
  31. package/dist/base64.js.map +1 -0
  32. package/dist/compress/binary.d.ts +2 -0
  33. package/dist/compress/binary.d.ts.map +1 -0
  34. package/dist/compress/binary.js +4 -0
  35. package/dist/compress/binary.js.map +1 -0
  36. package/dist/compress/index.d.ts +59 -0
  37. package/dist/compress/index.d.ts.map +1 -0
  38. package/dist/compress/index.js +146 -0
  39. package/dist/compress/index.js.map +1 -0
  40. package/dist/instance.d.ts +25 -0
  41. package/dist/instance.d.ts.map +1 -0
  42. package/dist/instance.js +50 -0
  43. package/dist/instance.js.map +1 -0
  44. package/dist/mp3/binary.d.ts +2 -0
  45. package/dist/mp3/binary.d.ts.map +1 -0
  46. package/dist/mp3/binary.js +4 -0
  47. package/dist/mp3/binary.js.map +1 -0
  48. package/dist/mp3/index.d.ts +20 -0
  49. package/dist/mp3/index.d.ts.map +1 -0
  50. package/dist/mp3/index.js +49 -0
  51. package/dist/mp3/index.js.map +1 -0
  52. package/dist/render/binary.d.ts +2 -0
  53. package/dist/render/binary.d.ts.map +1 -0
  54. package/dist/render/binary.js +4 -0
  55. package/dist/render/binary.js.map +1 -0
  56. package/dist/render/bitmap-kernel.d.ts +118 -0
  57. package/dist/render/bitmap-kernel.d.ts.map +1 -0
  58. package/dist/render/bitmap-kernel.js +592 -0
  59. package/dist/render/bitmap-kernel.js.map +1 -0
  60. package/dist/render/draw-kernel.d.ts +79 -0
  61. package/dist/render/draw-kernel.d.ts.map +1 -0
  62. package/dist/render/draw-kernel.js +205 -0
  63. package/dist/render/draw-kernel.js.map +1 -0
  64. package/dist/render/dxt-encoder.d.ts +19 -0
  65. package/dist/render/dxt-encoder.d.ts.map +1 -0
  66. package/dist/render/dxt-encoder.js +95 -0
  67. package/dist/render/dxt-encoder.js.map +1 -0
  68. package/dist/render/index.d.ts +6 -0
  69. package/dist/render/index.d.ts.map +1 -0
  70. package/dist/render/index.js +11 -0
  71. package/dist/render/index.js.map +1 -0
  72. package/dist/render/lines.d.ts +55 -0
  73. package/dist/render/lines.d.ts.map +1 -0
  74. package/dist/render/lines.js +123 -0
  75. package/dist/render/lines.js.map +1 -0
  76. package/dist/render/module.d.ts +11 -0
  77. package/dist/render/module.d.ts.map +1 -0
  78. package/dist/render/module.js +27 -0
  79. package/dist/render/module.js.map +1 -0
  80. package/dist/render/pixel-ops.d.ts +77 -0
  81. package/dist/render/pixel-ops.d.ts.map +1 -0
  82. package/dist/render/pixel-ops.js +236 -0
  83. package/dist/render/pixel-ops.js.map +1 -0
  84. package/dist/render/tessellator.d.ts +90 -0
  85. package/dist/render/tessellator.d.ts.map +1 -0
  86. package/dist/render/tessellator.js +251 -0
  87. package/dist/render/tessellator.js.map +1 -0
  88. package/package.json +53 -3
  89. package/src/atf/index.ts +212 -0
  90. package/src/atf/jxr.ts +83 -0
  91. package/src/atf/parse.ts +263 -0
  92. package/src/audio/index.ts +93 -0
  93. package/src/base64.ts +10 -0
  94. package/src/compress/index.ts +225 -0
  95. package/src/instance.ts +53 -0
  96. package/src/mp3/index.ts +71 -0
  97. package/src/render/bitmap-kernel.ts +856 -0
  98. package/src/render/draw-kernel.ts +268 -0
  99. package/src/render/dxt-encoder.ts +121 -0
  100. package/src/render/index.ts +37 -0
  101. package/src/render/lines.ts +161 -0
  102. package/src/render/module.ts +30 -0
  103. package/src/render/pixel-ops.ts +356 -0
  104. package/src/render/tessellator.ts +367 -0
  105. package/src/webassembly.d.ts +22 -0
package/README.md CHANGED
@@ -1,3 +1,490 @@
1
- # Temporary Holding Version
1
+ # @swf2es/native
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ swf2es's WebAssembly modules, built with the repository's pinned Zig:
4
+
5
+ - `@swf2es/native/render`: what the renderer computes natively, each a
6
+ wrapper over an instance of its own: double-precision libtess2 fill
7
+ tessellation (`Tessellator`), a shape layer's lines built in one call
8
+ (`LineBuilder`), BitmapData's filters and pixel operations (`BitmapKernel`),
9
+ and whole-level DXT1/DXT5 texture encoding (`DxtEncoder`).
10
+ - `@swf2es/native/compress`: synchronous zlib and LZMA.
11
+ - `@swf2es/native/mp3`: synchronous MP3 frame decoding with minimp3.
12
+ - `@swf2es/native/audio`: synchronous ADPCM and Nellymoser decoding, in
13
+ Zig alone.
14
+ - `@swf2es/native/atf`: Stage3D's ATF textures, read and decoded to what a
15
+ desktop GPU takes, their JPEG XR images by jxrlib.
16
+
17
+ The package depends on no other package and uses no DOM or node APIs.
18
+ `compress`, `mp3`, `audio` and `atf` each compile their own module. What the renderer
19
+ computes is one module, `render` (`src/render/`), compiled once; each user
20
+ instantiates it apart, so each has its own memory and lifetime, and a large
21
+ bitmap job never grows the tessellator's memory, nor does disposing one
22
+ take the other down. Within an instance, every part allocates from one
23
+ arena (`src/render/arena.zig`), which each call resets.
24
+
25
+ `render` requires wasm SIMD, which every current browser ships (Chrome 91,
26
+ Firefox 89, Safari 16.4 on): it is built with SIMD throughout and has no
27
+ scalar build. An engine without it refuses the module, as one without Wasm
28
+ or under a CSP without `'wasm-unsafe-eval'` does: the player then plays
29
+ nothing, as its fills, lines and bitmap convolution need the module.
30
+
31
+ ## Building
32
+
33
+ The package is one Zig project inside the npm package: `build.zig` at its
34
+ root builds every binary, and each binary's Zig source, in `src/<binary>/`,
35
+ sits beside its generated `binary.ts` and its TypeScript wrapper
36
+ (`index.ts`). tsc compiles only the `.ts` files, and only `dist/` and
37
+ `LICENSE.third-party` are published, so the Zig and C sources are in the
38
+ repository but not in the npm tarball. `include/` holds the libc headers
39
+ the vendored C includes, which wasm32-freestanding lacks: `string.h` maps
40
+ `memcpy`, `memmove` and `memset` to clang's builtins, which become bulk
41
+ memory instructions, and the rest declare only what the C calls.
42
+
43
+ The binaries are checked in, so ordinary builds need no compiler. `pnpm
44
+ build` verifies each one's input digest and the notices. To rebuild after
45
+ changing the sources or an upstream pin:
46
+
47
+ ```sh
48
+ pnpm --filter @swf2es/native build:wasm
49
+ ```
50
+
51
+ This runs the bitmap kernel's tests (`zig build test`) and then `zig
52
+ build` here with Zig 0.17.0, from the PATH or else downloaded once into
53
+ `~/.cache/swf2es` by `tools/zig.ts` and checked against its SHA-256, with
54
+ fresh caches, and writes each module into its `src/<binary>/binary.ts`
55
+ and the vendored C's notices, verbatim, into `LICENSE.third-party`. CI
56
+ rebuilds and compares the generated bytes. `zig build` alone, in this
57
+ folder, builds `zig-out/bin/*.wasm` like any Zig project (`.zig-cache/` and
58
+ `zig-out/` are ignored).
59
+
60
+ ## Licences
61
+
62
+ The wrappers, the Zig and the build are Apache-2.0. `LICENSE.third-party`
63
+ carries each vendored library's notice: zlib is under the zlib license,
64
+ the LZMA SDK files are in the public domain, minimp3 is CC0-1.0,
65
+ libtess2 is under SGI Free Software License B, and jxrlib is under the BSD
66
+ 2-Clause License; and those of the code derived from FFmpeg's Nellymoser
67
+ decoder (`src/audio/nellymoser.zig`), under the MIT License, and of the code
68
+ derived from SVG 1.1's feTurbulence (`src/render/pixels.zig`'s
69
+ perlinNoise), under the W3C Software and Document License.
70
+
71
+ ## bitmap
72
+
73
+ `BitmapKernel` is BitmapData's CPU filters: `convolve()` a convolution
74
+ (`src/render/convolve.zig`), `blur()` a blur, and `layer()` a glow, a
75
+ drop shadow, a bevel or a gradient glow or bevel (`src/render/blur.zig`).
76
+ The player filters through it alone: AS3's and AVM1's
77
+ `BitmapData.applyFilter` call the one `BitmapKernel` of their realm
78
+ (`packages/player/src/bitmap/kernel.ts`), made at the first filter or
79
+ operation that needs it, which compiles the module; where it traps, the
80
+ call is made again in a new instance, and where that fails too, nothing
81
+ is written. A displacement map and GPU filters are unchanged. These three
82
+ return owned premultiplied ARGB `Uint32Array` pixels, or `null` where they
83
+ would not fit or the instance trapped. No source pixels are changed.
84
+ Results survive later calls, memory growth, rejection and disposal.
85
+
86
+ It also runs the loops of BitmapData's pixel operations
87
+ (`src/render/pixels.zig`, driven by `pixel-ops.ts`): `copy()` for
88
+ copyChannel, merge, paletteMap and a colour matrix filter, `transform()`
89
+ for colorTransform, `noise()` and `perlin()`. These work in place: they
90
+ write into the caller's arrays, the source too where it is the
91
+ destination, and return whether they did. They go a band of rows of about
92
+ 1 MiB at a time, each copied in and back before the next, which reads
93
+ what the bands before it wrote; where the source is the destination and a
94
+ band's rows of the two overlap, both go in as one block, so that a pixel
95
+ reads what the pixels before it wrote, as in Flash. A band that traps is
96
+ made again in a new instance, the bands before it not twice; where it
97
+ fails again, the call returns false with the bands before it written.
98
+
99
+ `blur()` takes the rect to blur, `layer()` the rect to place a layer on
100
+ and the offsets it reads the source's alpha from (one, or a bevel's two, on
101
+ and back); the wrapper copies in the blocks of the source each reads, 0
102
+ past the bitmap, a layer's alpha a byte a pixel. A large rect is filtered
103
+ in tiles, each read as far round as the blur reaches, so a call's scratch
104
+ stays within 4 MiB, and an instance kept between frames, wherever the
105
+ reach lets it: a tile is four times the reach a side at least, and its
106
+ blocks are cut to the bitmap and the reach round it, past which every pass
107
+ holds nothing. Each takes values within the ranges the player's filter
108
+ objects keep, blurs 0 to 255 or NaN, quality 0 to 15, strength 0 to 255,
109
+ alphas 0 to 1, colours of 24 bits, and refuses others (null), in the
110
+ wrapper and again in `blur.zig`; the player brings a filter from a SWF's
111
+ tags, which may hold more, within them first.
112
+ `blur.zig` keeps the JS's arithmetic, in the same order in f64, converting
113
+ to an int as JS converts, so its pixels are the JS's to the bit: a blur's
114
+ box runs truncated to 8 bits each, a glow's alpha from the blur so
115
+ truncated times strength, a bevel's from the difference of two reads of
116
+ it, placed as adl places them. Planes are bytes, which every value fits,
117
+ so a call's scratch is a few bytes a pixel; runs along y go four columns a
118
+ lane, and reads by whole pixels are row copies. Against the JS in node, on
119
+ a 512-pixel square, a blur is 1.7 to 1.9 times as fast, a glow 1.4, an
120
+ inner glow 2.1, a drop shadow 2.0 and a bevel 1.5.
121
+
122
+ The wrapper convolves a rect a band of rows at a time, about 1 MiB of
123
+ input and output a call: it copies each band's rows plus the kernel's halo
124
+ and the coefficients in, and the band's output out before scratch is
125
+ reused. The kernel keeps straight-colour planes for only the kernel's rows,
126
+ so a call's scratch stays near 2 MiB even 4096 pixels wide, and a player
127
+ filtering large bitmaps every frame keeps its instance. The player handles
128
+ clipping, destination writing and notifications. Strict double arithmetic
129
+ and Flash's fixed-point 3×3 quirks are adl's, as the JS the player had
130
+ computed them. The kernel uses explicit f64x2 wasm SIMD, as all of
131
+ `render` may. Traps discard the instance, and normal rejection can reuse
132
+ its arena.
133
+
134
+ Wasm memory has a 128 MiB maximum. Instances above 8 MiB are dropped after a
135
+ call; `new BitmapKernel({ retainMemory: bytes })` adjusts that threshold.
136
+ Arena reset reuses capacity; only dropping the instance permits its memory
137
+ to be collected.
138
+
139
+ The package's build first runs the kernels' Zig tests on the host (`zig
140
+ build test` in this folder): exact pixels for small cases, and every
141
+ allocation failing in turn. The kernels, `src/render/convolve.zig`,
142
+ `src/render/blur.zig` and `src/render/pixels.zig`, take any `std.mem.Allocator` or slices;
143
+ `src/render/bitmap.zig` wraps them in the Wasm module's bump arena. No external dependency is needed.
144
+
145
+ `node --test tests/unit/native-bitmap.test.ts` holds the convolution to
146
+ the JS the player had (`tests/unit/player/bitmap/convolution-reference.ts`),
147
+ bit for bit on thousands of random filters, special numbers among them,
148
+ and checks ownership, rejection and traps; `native-blur.test.ts` holds
149
+ `applyFilter`'s blurs, glows, shadows and bevels to theirs
150
+ (`blur-reference.ts`), and takes `SWF2ES_FUZZ_STEPS` and
151
+ `SWF2ES_FUZZ_SEED` for longer runs; `native-pixels.test.ts` holds the
152
+ pixel operations to theirs (`pixels-reference.ts`), bit for bit, onto
153
+ their own bitmaps too and across bands, and a band that traps. The player's references
154
+ (`apply-filter`, `convolution`) hold it to Flash's own pixels.
155
+
156
+ ## DXT encoding
157
+
158
+ `DxtEncoder.encode(rgba, dxt5, width, height)` compresses a whole texture
159
+ level in one call, into DXT1 or DXT5 blocks identical to
160
+ `@swf2es/context3d`'s TypeScript encoder. It takes tightly packed top-down
161
+ RGBA, repeats edge pixels for partial 4×4 blocks, and keeps the reference's
162
+ double arithmetic and tie breaking. Four colour distances are compared
163
+ together with integer SIMD. It returns `null` for invalid dimensions or
164
+ input sizes, failed allocation, a refused module or a trap, so the caller
165
+ can use the TypeScript path.
166
+
167
+ Each encoder owns an instance. Input and output share one reserved block,
168
+ grown only when a larger level needs it: one encoding crossing per level,
169
+ plus an arena reset on allocation or growth. The result is a view of that
170
+ memory, valid until the next call; upload or copy it before encoding again.
171
+ `release()` drops the instance, and a level that grew it beyond 32 MiB
172
+ causes it to be replaced before the next call. That budget keeps a 2048²
173
+ DXT5 upload's workspace between calls. A trap also drops the instance.
174
+
175
+ The host Zig tests cover flat blocks and edge repetition;
176
+ `tests/unit/native-dxt.test.ts` compares blocks against the TypeScript
177
+ reference on thousands of levels, checks that each was encoded in wasm,
178
+ and exercises rejection, traps and release.
179
+
180
+ ## compress
181
+
182
+ Synchronous zlib and LZMA for SWF bodies, bitmaps and `ByteArray`, in one
183
+ WebAssembly module that imports nothing. Its upstream sources are pinned
184
+ submodules of swf2es's own repositories: zlib 1.3.2 in
185
+ [`vendor/zlib`](../../vendor/zlib) (https://github.com/swf2es/zlib, a fork
186
+ of madler/zlib, at tag `v1.3.2`), and the LZMA SDK 26.05 in
187
+ [`vendor/lzma-sdk`](../../vendor/lzma-sdk)
188
+ (https://github.com/swf2es/lzma-sdk at tag `v26.05`: the release as
189
+ published, line endings made LF, whose README names the archive and its
190
+ SHA-256). Of the SDK only the encoder and the decoder are compiled, all
191
+ public domain:
192
+
193
+ | File | SHA-256 |
194
+ |---|---|
195
+ | `C/7zTypes.h` | `a5d03b8cb65cd3a2a56bde46dd68fb356d2ebacd9d70283e7d4d20b4e0a64713` |
196
+ | `C/Compiler.h` | `d5e42be77d26beaa8c81d3e62ef99fc5685736cfc079779fdb9154d05fa8bc4a` |
197
+ | `C/CpuArch.h` | `259145c6f4b085f44284014574f39f3f562fc78466746bfb388efe06b4dd59b4` |
198
+ | `C/LzFind.c` | `f73da68845094a85b006a0d2da80b3ecdec92a1ca0f85df637e565c642e8d5ba` |
199
+ | `C/LzFind.h` | `42732b38df9bb18f82866d7deaa8663f6c6f9cb10e2a2cdfa9e6f420c2e2f239` |
200
+ | `C/LzHash.h` | `42d146d2130dfe49d0b21d15cc6f0fe9d79ac0ebb629985fe3aedde7b2bee80e` |
201
+ | `C/LzmaEnc.c` | `67f23656339b5e19015acac41acbcf36ea7787a8419907dce91c1dd336a2fb0f` |
202
+ | `C/LzmaEnc.h` | `c5e78398309363dd181840b7ba0bcb856f66619f755d5e2f5e04f7437186ba5f` |
203
+ | `C/Precomp.h` | `c8903f2e36a771a272d8981e1e9ffed5ba16ca3d2f3dca6d1414d2340a6829d0` |
204
+ | `C/LzmaDec.c` | `4e6ec665a6df01f1722e2b2fc36dc5e3aa7c3890e0f8cb11c6a07c9a01f3309b` |
205
+ | `C/LzmaDec.h` | `3aaf07b4ae4173a2d103179455dc7089b5ddbc7fc3db3c0e40964a7499c69266` |
206
+
207
+ `src/compress/root.zig` exports the module's calls, gives zlib and the SDK a bump
208
+ arena through their allocator callbacks, and calls them through bindings
209
+ that `build.zig` translates from `zlib.h`, `LzmaEnc.h` and `LzmaDec.h`
210
+ (Zig 0.17 has no `@cImport`), with the same defines as it compiles the C
211
+ with, so a define cannot change a struct's layout on one side only.
212
+ wasm32-freestanding has no libc headers, so `include/` declares what the
213
+ C calls. Zig compiles C there with `-ffreestanding`, which would leave
214
+ `memcpy`, `memset` and `memmove` calls to compiler-rt's loops, so
215
+ `include/string.h` maps them to clang's builtins, which become bulk memory
216
+ instructions; `memcmp` and `strlen` come from Zig's compiler-rt.
217
+ Upstream sources stay unchanged.
218
+
219
+ - `inflate(data, raw, limit)` inflates a zlib or raw deflate stream to at
220
+ most `limit` bytes, stopping at the stream's end.
221
+ - `deflate(data, level, raw)` deflates as zlib does at that level.
222
+ - `lzmaDecode(stream, size)` decodes an LZMA stream, its 5 property bytes
223
+ first, to `size` bytes or to its end marker.
224
+ - `lzmaEncode(data)` writes `ByteArray`'s LZMA: properties, an 8-byte
225
+ length and the stream, at level 9 with a 1 MB dictionary as avmplus, or
226
+ a smaller one for less data.
227
+
228
+ Decoders return a status, the bytes decoded and how much input they read,
229
+ and can leave room before their output so that a SWF's header needs no
230
+ second copy. A failure gives the bytes decoded before it only when the
231
+ caller asks (`partial`), so a bomb that fails is never copied out of the
232
+ module; an output the page cannot allocate is `NO_MEMORY`. An instance
233
+ whose call threw is dropped too.
234
+
235
+ The module is compiled synchronously on first use from the base64 in
236
+ `src/compress/binary.ts`. Each call resets a bump arena in the module's memory,
237
+ copies its input in and its output out; outputs of unknown size grow in
238
+ place at the arena's top, so they are never copied as they grow. Memory
239
+ never shrinks, so an instance whose memory grew past 8 MiB, which LZMA
240
+ encoding of more than a few hundred KB does, is dropped after its call and the next call
241
+ instantiates a new one.
242
+
243
+ ## mp3
244
+
245
+ Synchronous MP3 frame decoding with minimp3, in one WebAssembly module that
246
+ imports nothing. Its upstream source is the pinned submodule
247
+ [`vendor/minimp3`](../../vendor/minimp3) (https://github.com/swf2es/minimp3,
248
+ a fork of lieff/minimp3, at commit `ea99364`), compiled unchanged.
249
+
250
+ Call `await prepareMp3()` before running scripts. It compiles the embedded
251
+ WASM binary once, without instantiating a decoder or decoding any audio.
252
+ An `Mp3Decoder` then decodes a sequence of whole frames synchronously.
253
+ Its `pcm` view is overwritten by the next `read()`; copy samples out before
254
+ reading again. Each decoder owns a fixed 128 KiB workspace and imports
255
+ nothing from the host. The module is shared, but decoder state is not.
256
+
257
+ The player uses this only for `Sound.extract()`. It keeps the samples weakly
258
+ by sound, decodes on the first extraction, and preserves Flash's untrimmed
259
+ MP3 frame counts. Playback still uses the audio host's decoder.
260
+
261
+ minimp3 is a single header. `build.zig` translates its declarations for
262
+ `src/mp3/root.zig` (Zig 0.17 has no `@cImport`) and compiles its implementation
263
+ from a generated one-line C file that defines `MINIMP3_IMPLEMENTATION` and
264
+ includes it, both with the same defines (`MINIMP3_ONLY_MP3`,
265
+ `MINIMP3_NO_SIMD`, `MINIMP3_FLOAT_OUTPUT`), so the sample type cannot
266
+ differ between the two. The implementation stays C, compiled by Zig's
267
+ clang with `-ffp-contract=off`, rather than translated to Zig, so the
268
+ samples are the same arithmetic as before.
269
+
270
+ wasm SIMD was measured and left off: with the `simd128` feature, LLVM
271
+ vectorises enough of minimp3 to decode about 12% faster (some 405 ms
272
+ against 460 for 22,854 frames) with bit-identical samples, but the module
273
+ grows by 12.7 KB and would need SIMD of every engine. minimp3's own SIMD
274
+ is SSE and NEON only, which `MINIMP3_NO_SIMD` keeps off.
275
+
276
+ Where the module does not compile, `prepareMp3()` still resolves,
277
+ `mp3Available()` is false, and the player plays on: `Sound.extract()`
278
+ writes no samples for an MP3, as Flash does for a sound it cannot decode.
279
+
280
+ ## audio
281
+
282
+ SWF's other compressed sound, decoded synchronously in Zig alone
283
+ (`src/audio/`), in one module that imports nothing, compiled the first
284
+ time it is called and kept as `compress`'s is:
285
+
286
+ - `decodeAdpcm(data, channels)`: ADPCM (`adpcm.zig`) as 16-bit samples,
287
+ channels interleaved, each packet's header sample first, as adl gives it.
288
+ - `NellymoserDecoder.decode(data)`: Nellymoser (`nellymoser.zig`, FFmpeg's
289
+ decoder translated) as samples from -1 to 1, 256 for each whole 64-byte
290
+ block; a decoder carries its stream's overlap and noise from one call to
291
+ the next, its state copied in and out of the module with each call.
292
+
293
+ Each call copies its input into the module's memory, decodes there in an
294
+ arena reset first, and copies the samples out. Both give the samples the
295
+ TypeScript decoders they replaced gave, bit for bit: Nellymoser's
296
+ coefficients and samples are rounded to single precision, and its
297
+ transform done in double, where that decoder stored them. Decoding is
298
+ about 1.7 times as fast as that decoder for Nellymoser and 2.6 times for
299
+ ADPCM, where copying the samples out is most of the time. `zig build test`
300
+ runs each decoder's own test on the host.
301
+
302
+ ## tessellate
303
+
304
+ This compiles the small double-precision patch in `vendor/libtess2`.
305
+ `build.zig` compiles the vendored C and the arena/API adapter in
306
+ `src/render/tessellate.zig` together, into the `render` module, translating the C header with
307
+ `b.addTranslateC`, as for MP3 and compression. libtess2 remains under SGI
308
+ Free Software License B.
309
+
310
+ The player draws every fill and line through it. A `RenderView` makes a
311
+ `Tessellator` of its own, or takes one as its `fillTessellator` option
312
+ (its `tessellate`), which several views may share:
313
+
314
+ ```ts
315
+ import { Tessellator } from "@swf2es/native/render";
316
+ import { RenderView } from "@swf2es/player/scene";
317
+
318
+ const tessellator = new Tessellator();
319
+ const view = new RenderView({ width, height, fillTessellator: tessellator.tessellate });
320
+ ```
321
+
322
+ A `Tessellator` compiles the module as it is made, and throws where the
323
+ host refuses it. The player keeps its paint and draw order, and passes
324
+ every contour of a fill together, using even-odd or non-zero winding, with
325
+ non-zero for masks; a fill the tessellator refuses is left out, resolved
326
+ once with the rest of the layer (`scene/fills.ts`, `resolveFills`), so
327
+ nothing retries it. Dynamic drawings and morph ratios construct new
328
+ results when their contents change. A fill alone, `tessellate(contours,
329
+ nonZero)`, takes flattened polygons, for other hosts.
330
+
331
+ The player tessellates a shape layer's fills in one call,
332
+ `tessellator.tessellate.layer(fills, nonZero)`, which takes each contour as
333
+ the shape's own path commands (`PATH_MOVE`, `PATH_LINE`, `PATH_CURVE`,
334
+ `PATH_CUBIC`, each followed by its coordinates), not as points: the wrapper
335
+ copies every fill's commands, the contours' ends, each fill's last contour
336
+ and its rule into one allocation, and `src/render/flatten.zig` flattens
337
+ each contour as the player's JS did, and the fuzzer's still does
338
+ (`tests/fuzz/tessellate.ts`):
339
+ chords within 0.02 px of the curve, at most 256 a curve, at the same
340
+ parametric points, repeated points dropped, a closing point that repeats the
341
+ first dropped, and contours of fewer than three points left out, all in
342
+ strict f64 arithmetic. So the player builds no point arrays for a layer.
343
+ The chords' length takes a plain square root where JS used `Math.hypot`,
344
+ which may differ in the last bit, so a curve whose bend falls within that
345
+ bit of a whole number of chords takes one chord more or fewer. Either is
346
+ within the flatness. Then `layer` runs libtess2 on each fill as
347
+ `run` would alone on those polygons, keeps each fill's output past its
348
+ scratch, which then goes, and gathers every fill's positions into one buffer
349
+ and its indices into another, counted from the layer's first vertex. The
350
+ wrapper copies those two out once into JS-owned typed arrays and gives each
351
+ fill views of its part, with `indexOffset`, its first vertex in the layer.
352
+ Positions remain doubles; indices use 16 bits through 65,536 vertices in the
353
+ layer and 32 bits above that. The module
354
+ checks every input coordinate and every output position and index, so the
355
+ player draws a layer's output unchecked; a fill alone (`tessellate(contours,
356
+ nonZero)`) is checked by its caller. Malformed commands and a fill whose
357
+ curves would flatten to more than a million points are refused; a fill
358
+ that ran out of memory, which may be the layer's, is tried as a layer of
359
+ its own, and a layer that unwinds or does not fit is tried a fill at a
360
+ time, so a layer gives every fill what a layer of its own would. No cached geometry aliases Wasm scratch. The
361
+ renderer copies each fill's triangles into a geometry of its own with its
362
+ paint's colours and texels (`scene/shapes.ts`), which its atlas holds
363
+ through context loss.
364
+
365
+ The absolute memory ceiling is 128 MiB, including static data, the C stack
366
+ and the arena. Instances above 8 MiB are discarded after the call. Options
367
+ can lower the limit or retained capacity. Coordinates that are not
368
+ finite, malformed contours, more than one million input vertices and
369
+ running out of memory return `null`. A fill of one convex contour, a
370
+ triangle above all, is a fan from its first point, its points as given,
371
+ with no sweep: convex as every turn goes one way and its edges go back
372
+ along each axis at most twice, which a contour winding round more than
373
+ once, as a pentagram, does not pass. What libtess2 fails at, gives output
374
+ unfit to draw for, or cannot take (coordinates outside ±2^23), the module
375
+ fills by bands (`src/render/bands.zig`): the fill cut at every vertex's
376
+ height and every height two edges cross at, each span the rule fills a
377
+ trapezoid between two edges, the bands meeting along horizontal lines,
378
+ which the rasterizer splits without a crack. A fill that would make it
379
+ test more than 2^26 pairs of edges for a crossing is left out, as too slow
380
+ for a frame. A throwing failure import replaces libtess2's longjmp; that
381
+ instance is always discarded before another call, and the call is tried
382
+ again in a new one: a layer a fill at a time, and a fill that unwinds alone
383
+ by the bands, which never unwind. `new Tessellator({ bands: true })` fills
384
+ by the bands alone, for tests. Arena reset alone
385
+ does not shrink Wasm memory. `dispose()` drops retained scratch without
386
+ affecting previous output.
387
+
388
+ The player references and context-loss checks draw with the native
389
+ tessellator, as everything does.
390
+ `node tests/fuzz/tessellate.ts [cases] [seed]`
391
+ checks both fill rules against independent ray crossings, by libtess2 and
392
+ again by the bands alone; the default test suite includes 2,000 fills and
393
+ 24,000 coverage samples each way. It then flattens
394
+ curved layers, quadratics, cubics, curves of no length and NaN coordinates,
395
+ and checks each fill against a JS flattening tessellated alone: drawn or
396
+ refused alike, the same geometry bit for bit as JS with plain square roots
397
+ gives, and within the flatness of what `Math.hypot` gives. `zig build
398
+ test` in this folder runs `flatten.zig`'s and `bands.zig`'s own tests. Synthetic fixtures
399
+ also check coincident edges, holes and detail at large coordinates.
400
+
401
+ ## lines
402
+
403
+ `tessellator.tessellate.lines`, a `LineBuilder`, builds a shape layer's
404
+ lines in one call: the player packs the layer into `input(n)`
405
+ (`scene/strokes.ts`) and `build(length)` returns its positions, colours
406
+ and indices, copied out in one block. `src/render/strokes.zig` is the
407
+ player's only line builder. It began as a port of the player's TS, with
408
+ the same arithmetic in the same order, and keeps JS's semantics where they
409
+ show: `Math.hypot` is V8's algorithm (normalised to the larger value,
410
+ summed with Kahan's compensation), `Math.sin` and `Math.cos` are the host's
411
+ own, imported, since engines differ in their last bit, and `atan2` and
412
+ `acos` are Zig's, which are fdlibm's, as V8's are. A line drawn opaque is
413
+ triangles along its path; any other is outlined and its outline filled
414
+ non-zero by the module's own libtess2, or its bands, as the `Tessellator`
415
+ fills it, with no call back to JS.
416
+
417
+ Where it cannot build a layer, `build()` returns `undefined`: an outline it
418
+ could not fill in its memory, a position that is NaN, or an instance that
419
+ unwound, after which the next fills the layer's outlines by the bands
420
+ alone; the player asks once more and otherwise draws the layer without
421
+ lines. The builder has an instance of its own, which no `Tessellator` call
422
+ shares, held to its `Tessellator`'s `memoryLimit` and `retainMemory`, and
423
+ counted in its `capacity`; an instance grown past what is retained is let
424
+ go after its call. `tests/unit/player/scene/strokes.test.ts` checks the
425
+ lines it builds: their widths, caps, joins and miters by the area they
426
+ cover, that a wide line on a short curve leaves no hole, that an opaque
427
+ line's triangles cover what its outline does and meet corner to corner.
428
+
429
+ ## atf
430
+
431
+ Stage3D's ATF textures, which `uploadCompressedTextureFromByteArray`
432
+ takes: `readAtfHeader(data, offset)` reads the header, `decodeAtf(data,
433
+ header)` each face's levels, decoded to what Flash Player uploads on the
434
+ desktop. RGB888 and RGBA8888 hold a JPEG XR image a level and decode to
435
+ BGRA, alpha straight. The block compressed formats hold every GPU's
436
+ encoding, and decode to S3TC: Compressed (2) and its lossy variant (12) to
437
+ DXT1, CompressedAlpha (4, and 13) to DXT5, rebuilt from their LZMA'd
438
+ indices and the JPEG XR images of their endpoints; the raw formats (3, 5)
439
+ hold the blocks as they are. PVRTC, ETC1 and ETC2's blocks are skipped.
440
+
441
+ The layout is Adobe's "ATF file format" (version 3, 2015), and the old
442
+ header, which has no version, 24-bit lengths and no ETC2, as Adobe's MIT
443
+ [dds2atf](https://github.com/adobe/dds2atf) writes it; the endpoints'
444
+ images are a pixel a 4x4 block, the first endpoints above the second, the
445
+ indices 4 bytes a block (DXT5's alpha indices 6), as dds2atf encodes them.
446
+ Nothing is taken from dds2atf. A level rebuilt this way is the very bytes
447
+ png2atf's raw output holds for the same image (`tests/unit/atf.test.ts`).
448
+
449
+ Refusals are AIR's, as AIR 51 gives them for the same bytes: an
450
+ `AtfError` whose `reason` is `size` (#3669: less data than the header or
451
+ its length), `decode` (#3677: no signature, a format past 13, a size past
452
+ 4096, a level count the size has not, a block the desktop reads cut short
453
+ by the data's end, a block that does not decode) or `format` (#3675:
454
+ the formats 6 to 11, which no texture takes). `readAtfHeader` checks only
455
+ what AIR checks before an asynchronous upload returns, the signature and
456
+ the length, and an offset that is a whole number within the data;
457
+ `decodeAtf` the rest. Past the length check, AIR reads the blocks as far
458
+ as the data goes, the length aside, and streams what it finds: data that
459
+ ends before a level's first block length, or within a block the desktop
460
+ skips (PVRTC's, ETC's), leaves that level and those after it absent, as
461
+ does a level whose blocks for the desktop are empty (png2atf's `-n`).
462
+ Nothing of the reserved bytes is read.
463
+
464
+ jxrlib's decoder and glue (`vendor/jxrlib`, https://github.com/swf2es/jxrlib,
465
+ a fork of 4creators/jxrlib, Microsoft's release, at tag `v2019.10.9`) are
466
+ compiled unchanged, without its encoder, at `-Os`: the module is about
467
+ 100 KB, against 150 KB at `-O2` for 3% less time. `src/atf/jxr.c` decodes
468
+ an image in the pixel format its block must have, setting jxrlib's output
469
+ to the container's pixel format as jxrlib's own decoder does (the
470
+ codestream may name another, which jxrlib would write by into a buffer
471
+ sized for this one), and taking 24-bit RGB as png2atf writes it, named
472
+ 24bppBGR. `src/atf/include/` holds what jxrlib needs of libc beyond what
473
+ `include/` declares, each header taking the package's with
474
+ `#include_next`, its file streams and diagnostics inert, and `root.zig`
475
+ its allocator over a bump arena that each call resets. The instance is
476
+ kept between images as compress's is (`src/instance.ts`, the same
477
+ `MEMORY_KEPT`).
478
+
479
+ jxrlib is not memory safe on corrupt input: AddressSanitizer finds it
480
+ reading past its tables and writing through null pointers. In the module
481
+ these stay within its memory, whose stack comes first: a null pointer's
482
+ writes land at the bottom of the unused stack. An image refused or a
483
+ trap (#3677 either way) lets the instance go, and the next image starts
484
+ in a new one, so nothing a corrupt image did reaches another; one that
485
+ decodes keeps it.
486
+ `node tests/fuzz/atf.ts [cases] [seed]` mutates the fixtures, in both
487
+ headers, and checks that nothing traps or throws but `AtfError` and that
488
+ every level decoded is the size it should be; 450,000 cases traps none.
489
+ The 768 ATFs of a set of Alternativa3D demos, old-header Compressed up to
490
+ 2048 pixels, decode in 3.6 s in node, 68 ms the largest.