@glissade/cli 0.27.0 → 0.27.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.
@@ -0,0 +1,119 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { deflateSync, inflateSync } from "node:zlib";
5
+ //#region src/layerCache.ts
6
+ /**
7
+ * §3.5 disk layer-cache (0.27.1): the fs-backed `LayerStore` the CLI injects into
8
+ * the Skia backend. It persists a `cache:true` group's DEVICE-space raster across
9
+ * renders, so an expensive static subtree (a blurred mesh backdrop) rasterizes
10
+ * ONCE and re-blits on later runs — surviving a re-narration that defeats the
11
+ * whole-frame cache (the backdrop's sub-DisplayList is unchanged; only captions /
12
+ * timing move). Sibling of `frameCache.ts`; same on-disk discipline (deflate,
13
+ * atomic write, content-addressed filenames).
14
+ *
15
+ * The compositor supplies the raw key `<sub-DisplayList fnv1a>@<deviceTransformKey>`;
16
+ * this store SALTS it with the toolchain version ⊕ backend caps ⊕ frame size, so a
17
+ * version / capability / resolution change can never serve a stale raster. A
18
+ * restored RGBA composites byte-identically to a fresh raster (the compositor
19
+ * round-trips it through putImageData) — the same contract the frame cache holds.
20
+ */
21
+ const MAGIC = 1196641329;
22
+ const HEADER_BYTES = 20;
23
+ const BOUNDS_BYTES = 32;
24
+ const FLAG_HAS_BOUNDS = 1;
25
+ const FLAG_UNBOUNDED = 2;
26
+ function encodeEntry(e) {
27
+ const header = Buffer.alloc(HEADER_BYTES);
28
+ header.writeUInt32BE(MAGIC, 0);
29
+ header.writeUInt32BE(1, 4);
30
+ header.writeUInt32BE(e.w, 8);
31
+ header.writeUInt32BE(e.h, 12);
32
+ header.writeUInt32BE((e.bounds ? FLAG_HAS_BOUNDS : 0) | (e.unbounded ? FLAG_UNBOUNDED : 0), 16);
33
+ const parts = [header];
34
+ if (e.bounds) {
35
+ const b = Buffer.alloc(BOUNDS_BYTES);
36
+ b.writeDoubleBE(e.bounds.minX, 0);
37
+ b.writeDoubleBE(e.bounds.minY, 8);
38
+ b.writeDoubleBE(e.bounds.maxX, 16);
39
+ b.writeDoubleBE(e.bounds.maxY, 24);
40
+ parts.push(b);
41
+ }
42
+ parts.push(deflateSync(Buffer.from(e.rgba.buffer, e.rgba.byteOffset, e.rgba.byteLength)));
43
+ return Buffer.concat(parts);
44
+ }
45
+ function decodeEntry(buf) {
46
+ if (buf.length < HEADER_BYTES || buf.readUInt32BE(0) !== MAGIC) throw new Error("corrupt .gsl layer entry (bad magic)");
47
+ const w = buf.readUInt32BE(8);
48
+ const h = buf.readUInt32BE(12);
49
+ const flags = buf.readUInt32BE(16);
50
+ let off = HEADER_BYTES;
51
+ let bounds = null;
52
+ if (flags & FLAG_HAS_BOUNDS) {
53
+ bounds = {
54
+ minX: buf.readDoubleBE(off),
55
+ minY: buf.readDoubleBE(off + 8),
56
+ maxX: buf.readDoubleBE(off + 16),
57
+ maxY: buf.readDoubleBE(off + 24)
58
+ };
59
+ off += BOUNDS_BYTES;
60
+ }
61
+ const raw = inflateSync(buf.subarray(off));
62
+ return {
63
+ rgba: new Uint8ClampedArray(raw.buffer, raw.byteOffset, raw.byteLength),
64
+ w,
65
+ h,
66
+ bounds,
67
+ unbounded: (flags & FLAG_UNBOUNDED) !== 0
68
+ };
69
+ }
70
+ var LayerCache = class {
71
+ dir;
72
+ mode;
73
+ salt;
74
+ stats = {
75
+ hits: 0,
76
+ misses: 0,
77
+ stored: 0
78
+ };
79
+ constructor(opts) {
80
+ this.dir = opts.dir;
81
+ this.mode = opts.mode;
82
+ this.salt = opts.salt;
83
+ if (this.mode !== "off") mkdirSync(this.dir, { recursive: true });
84
+ }
85
+ fileFor(key) {
86
+ const h = createHash("sha256").update(this.salt).update("\0").update(key).digest("hex");
87
+ return join(this.dir, `${h}.gsl`);
88
+ }
89
+ get(key) {
90
+ if (this.mode === "off") return void 0;
91
+ const f = this.fileFor(key);
92
+ if (!existsSync(f)) {
93
+ this.stats.misses++;
94
+ return;
95
+ }
96
+ try {
97
+ const e = decodeEntry(readFileSync(f));
98
+ this.stats.hits++;
99
+ return e;
100
+ } catch {
101
+ this.stats.misses++;
102
+ return;
103
+ }
104
+ }
105
+ put(key, entry) {
106
+ if (this.mode !== "read-write") return;
107
+ const f = this.fileFor(key);
108
+ if (existsSync(f)) return;
109
+ const tmp = `${f}.tmp${globalThis.process?.pid ?? 0}`;
110
+ writeFileSync(tmp, encodeEntry(entry));
111
+ renameSync(tmp, f);
112
+ this.stats.stored++;
113
+ }
114
+ getStats() {
115
+ return { ...this.stats };
116
+ }
117
+ };
118
+ //#endregion
119
+ export { LayerCache };
package/dist/render.js CHANGED
@@ -420,11 +420,19 @@ async function render(opts) {
420
420
  mode: opts.cache.mode,
421
421
  ...opts.cache.maxSize !== void 0 ? { maxSize: opts.cache.maxSize } : {}
422
422
  });
423
+ const version = glissadeVersion();
424
+ const caps = capsId(backend.caps);
423
425
  keyCtx = {
424
- version: glissadeVersion(),
425
- capsId: capsId(backend.caps),
426
+ version,
427
+ capsId: caps,
426
428
  assetsDigest: combineAssetDigests(assetDigests)
427
429
  };
430
+ const { LayerCache } = await import("./layerCache.js");
431
+ backend.setLayerStore(new LayerCache({
432
+ dir: join(opts.cache.dir, "layers"),
433
+ mode: opts.cache.mode,
434
+ salt: `${version}|${caps}|${scene.size.w}x${scene.size.h}`
435
+ }));
428
436
  }
429
437
  let videoOut;
430
438
  let remuxDigest;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/cli",
3
- "version": "0.27.0",
3
+ "version": "0.27.1",
4
4
  "description": "glissade CLI: headless rendering via backend-skia (+ FFmpeg mux).",
5
5
  "license": "Apache-2.0",
6
6
  "engines": {
@@ -23,15 +23,15 @@
23
23
  "@napi-rs/canvas": "^0.1.65",
24
24
  "esbuild": "0.28.0",
25
25
  "jiti": "^2.4.2",
26
- "@glissade/backend-skia": "0.27.0",
27
- "@glissade/core": "0.27.0",
28
- "@glissade/interact": "0.27.0",
29
- "@glissade/lottie": "0.27.0",
30
- "@glissade/svg": "0.27.0",
31
- "@glissade/narrate": "0.27.0",
32
- "@glissade/player": "0.27.0",
33
- "@glissade/sfx": "0.27.0",
34
- "@glissade/scene": "0.27.0"
26
+ "@glissade/backend-skia": "0.27.1",
27
+ "@glissade/core": "0.27.1",
28
+ "@glissade/interact": "0.27.1",
29
+ "@glissade/lottie": "0.27.1",
30
+ "@glissade/svg": "0.27.1",
31
+ "@glissade/narrate": "0.27.1",
32
+ "@glissade/player": "0.27.1",
33
+ "@glissade/scene": "0.27.1",
34
+ "@glissade/sfx": "0.27.1"
35
35
  },
36
36
  "repository": {
37
37
  "type": "git",