@nodecraft/rendermite 1.0.5 → 1.0.7

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 (112) hide show
  1. package/README.md +5 -0
  2. package/dist/atlas/registry.d.ts +3 -3
  3. package/dist/atlas/sources.d.ts +1 -1
  4. package/dist/{blend-Qvrn6YgK.d.ts → blend-CEHoJ7tM.d.ts} +3 -2
  5. package/dist/{blend-CHP2-j9U.js → blend-D0XWgVQr.js} +20 -8
  6. package/dist/cache-Cr2ON3RQ.js +66 -0
  7. package/dist/cli.js +12 -6
  8. package/dist/compose-lWPrB7wh.js +104 -0
  9. package/dist/descriptor-DWebtkI1.js +26 -0
  10. package/dist/{discovery-D89pVVJA.d.ts → discovery-wiJBGryo.d.ts} +1 -1
  11. package/dist/dynamic/generators.d.ts +1 -1
  12. package/dist/dynamic/generators.js +1 -1
  13. package/dist/{encode-C5kdAm45.d.ts → encode-CaSK8-mi.d.ts} +1 -1
  14. package/dist/{encode-3u49i-eD.js → encode-Df_tWzkl.js} +44 -16
  15. package/dist/{entity-fallback-DfL0_eyf.js → entity-fallback-D9QTGU9X.js} +1 -1
  16. package/dist/{face-CpVdnf-w.d.ts → face-C49L66sQ.d.ts} +10 -4
  17. package/dist/{face-D-N1amV4.js → face-Cl1O4m2i.js} +24 -11
  18. package/dist/{generators-CcVRG30y.js → generators-BlG4KmtP.js} +1 -1
  19. package/dist/{generators-CCB8h0mP.d.ts → generators-DhRov4RB.d.ts} +1 -1
  20. package/dist/{geo-Dq2zve09.js → geo-DHiWBa-C.js} +7 -4
  21. package/dist/index.d.ts +14 -14
  22. package/dist/index.js +12 -12
  23. package/dist/{item-styles-DJ3LWZS2.js → item-styles-upgEO1yP.js} +1 -1
  24. package/dist/items/discovery.d.ts +1 -1
  25. package/dist/{jar-6T3xdgaL.d.ts → jar-CFkA-VgT.d.ts} +2 -1
  26. package/dist/{jar-Bt3i0ls4.js → jar-D9z-X7tA.js} +12 -12
  27. package/dist/{legacy-reshape-CewShfF3.js → legacy-reshape-C05SfAPR.js} +1 -1
  28. package/dist/{loader-A-BZa8J4.js → loader-B8NJO7pR.js} +12 -3
  29. package/dist/{loader-CBzn3Koc.js → loader-CfXMxcEV.js} +37 -7
  30. package/dist/{loader-C-6QN3xv.d.ts → loader-D22zZME7.d.ts} +2 -2
  31. package/dist/{loader-TFFNV4Jl.d.ts → loader-Myy22TDL.d.ts} +8 -2
  32. package/dist/{manager-dfI7b9N4.js → manager-CkletR5K.js} +29 -2
  33. package/dist/{manager-CoZaeQTu.d.ts → manager-Cv1NeFyF.d.ts} +28 -1
  34. package/dist/models/loader.d.ts +1 -1
  35. package/dist/models/loader.js +1 -1
  36. package/dist/mods/prepare.d.ts +1 -1
  37. package/dist/mods/prepare.js +1 -1
  38. package/dist/mods/synthesize.d.ts +2 -2
  39. package/dist/mods/synthesize.js +1 -1
  40. package/dist/mods/tints.d.ts +2 -2
  41. package/dist/mods/tints.js +1 -1
  42. package/dist/output/encode.d.ts +1 -1
  43. package/dist/output/encode.js +1 -1
  44. package/dist/output/item-styles.js +1 -1
  45. package/dist/pipeline-Da2Kavba.js +221 -0
  46. package/dist/pipeline-HiA3DC71.d.ts +58 -0
  47. package/dist/pool-CKGp1qup.js +131 -0
  48. package/dist/{prepare-BeUpXwaP.js → prepare-BESRrh6K.js} +4 -4
  49. package/dist/{prepare-gfR1aIjt.d.ts → prepare-CPxO0kKz.d.ts} +1 -1
  50. package/dist/{prepare-C8wRjAXP.js → prepare-Cc8QZ3j0.js} +4 -4
  51. package/dist/{prepare-wfWtaz_X.d.ts → prepare-Cs6QDqie.d.ts} +1 -1
  52. package/dist/{rasterizer-BHWDrmGz.js → rasterizer-76qO8TDh.js} +42 -4
  53. package/dist/{rasterizer-Dh35qXbn.d.ts → rasterizer-B9ZGJI3-.d.ts} +21 -2
  54. package/dist/render/blend.d.ts +1 -1
  55. package/dist/render/blend.js +1 -1
  56. package/dist/render/compose.d.ts +2 -2
  57. package/dist/render/compose.js +1 -1
  58. package/dist/render/entity-fallback.d.ts +6 -6
  59. package/dist/render/entity-fallback.js +1 -1
  60. package/dist/render/face.d.ts +2 -2
  61. package/dist/render/face.js +1 -1
  62. package/dist/render/fluid-bucket.d.ts +2 -2
  63. package/dist/render/geo.d.ts +4 -4
  64. package/dist/render/geo.js +1 -1
  65. package/dist/render/legacy-tints.d.ts +2 -2
  66. package/dist/render/obj.d.ts +5 -5
  67. package/dist/render/obj.js +1 -1
  68. package/dist/render/pipeline.d.ts +2 -0
  69. package/dist/render/pipeline.js +2 -0
  70. package/dist/render/rasterizer.d.ts +2 -2
  71. package/dist/render/rasterizer.js +2 -2
  72. package/dist/render/renderer.d.ts +2 -2
  73. package/dist/render/renderer.js +1 -1
  74. package/dist/render/tints.d.ts +1 -1
  75. package/dist/{renderer-nbMCw1Pl.d.ts → renderer-B20ssjRm.d.ts} +23 -6
  76. package/dist/{renderer-LDhBNHcl.js → renderer-gQW_I7YG.js} +146 -63
  77. package/dist/resources/cache.d.ts +30 -0
  78. package/dist/resources/cache.js +2 -0
  79. package/dist/resources/descriptor.d.ts +2 -0
  80. package/dist/resources/descriptor.js +2 -0
  81. package/dist/resources/jar.d.ts +1 -1
  82. package/dist/resources/jar.js +1 -1
  83. package/dist/resources/manager.d.ts +1 -1
  84. package/dist/resources/manager.js +1 -1
  85. package/dist/resources/synthetic.d.ts +1 -1
  86. package/dist/resources/synthetic.js +1 -1
  87. package/dist/resources/zip-reader.d.ts +5 -0
  88. package/dist/resources/zip-reader.js +2 -0
  89. package/dist/run-B0qPw3zf.js +209 -0
  90. package/dist/{run-BlSZw9Qk.d.ts → run-CT3VtFqg.d.ts} +12 -2
  91. package/dist/run.d.ts +1 -1
  92. package/dist/run.js +1 -1
  93. package/dist/{sources-DH9dk3KF.d.ts → sources-B9X9w3qP.d.ts} +1 -1
  94. package/dist/{synthesize-CRgSEmEp.js → synthesize-3Q0aaOnK.js} +1 -1
  95. package/dist/{synthetic-CDsj--li.d.ts → synthetic-Qk-BSUXO.d.ts} +5 -1
  96. package/dist/{synthetic-DC2bSnGe.js → synthetic-ZAUjXT7E.js} +0 -0
  97. package/dist/textures/loader.d.ts +1 -1
  98. package/dist/textures/loader.js +1 -1
  99. package/dist/{tints-CuG6GwQJ.js → tints-B8UJmZMu.js} +2 -2
  100. package/dist/{tints-txt-8xI5.d.ts → tints-BH9l0ND0.d.ts} +2 -2
  101. package/dist/versions/legacy-reshape.d.ts +1 -1
  102. package/dist/versions/legacy-reshape.js +1 -1
  103. package/dist/versions/prepare.d.ts +1 -1
  104. package/dist/versions/prepare.js +1 -1
  105. package/dist/workers/pool.d.ts +36 -5
  106. package/dist/workers/pool.js +2 -67
  107. package/dist/workers/render-worker.d.ts +15 -0
  108. package/dist/workers/render-worker.js +40 -0
  109. package/dist/zip-reader-BSbJKUT1.js +156 -0
  110. package/package.json +4 -2
  111. package/dist/compose-qjhyBvnS.js +0 -41
  112. package/dist/run-CfnWIRY4.js +0 -316
package/README.md CHANGED
@@ -75,6 +75,11 @@ Benchmarked against PHP renderchest v5.1.0 on the same machine, rendering the sa
75
75
  | renderchest (docker, 8 workers) | 210s | 1,573 | 33 | ~449/min |
76
76
  | rendermite (one Node process, pool 8) | 22.9s | 2,065 | 0 | ~5,410/min |
77
77
 
78
+ That comparison is a fixed point in time; rendermite has since roughly halved its own wall
79
+ time again (worker threads, a faster compositor, bounded animations). Run `npm run bench` for
80
+ current numbers on your machine — CI runs the same suite and comments the delta on pull
81
+ requests.
82
+
78
83
  ## Testing
79
84
 
80
85
  ```sh
@@ -1,7 +1,7 @@
1
1
  import { n as ResourceLocator } from "../locator-jDbdUeTd.js";
2
- import { a as ResourceManager } from "../manager-CoZaeQTu.js";
3
- import { r as Texture } from "../loader-TFFNV4Jl.js";
4
- import { t as AtlasResolver } from "../sources-DH9dk3KF.js";
2
+ import { a as ResourceManager } from "../manager-Cv1NeFyF.js";
3
+ import { r as Texture } from "../loader-Myy22TDL.js";
4
+ import { t as AtlasResolver } from "../sources-B9X9w3qP.js";
5
5
  //#region src/atlas/registry.d.ts
6
6
  declare function loadAtlasResolver(resources: ResourceManager, loadTexture: (locator: ResourceLocator) => Promise<Texture | null>, onWarning?: (message: string) => void): Promise<AtlasResolver>;
7
7
  //#endregion
@@ -1,2 +1,2 @@
1
- import { a as HasTexture, c as SingleAtlasSource, d as parseAtlasSources, i as DirectoryAtlasSource, l as UnstitchAtlasSource, n as AtlasSource, o as PalettedPermutationsSource, r as AtlasSourceError, s as ResolvedSprite, t as AtlasResolver, u as createAtlasResolver } from "../sources-DH9dk3KF.js";
1
+ import { a as HasTexture, c as SingleAtlasSource, d as parseAtlasSources, i as DirectoryAtlasSource, l as UnstitchAtlasSource, n as AtlasSource, o as PalettedPermutationsSource, r as AtlasSourceError, s as ResolvedSprite, t as AtlasResolver, u as createAtlasResolver } from "../sources-B9X9w3qP.js";
2
2
  export { AtlasResolver, AtlasSource, AtlasSourceError, DirectoryAtlasSource, HasTexture, PalettedPermutationsSource, ResolvedSprite, SingleAtlasSource, UnstitchAtlasSource, createAtlasResolver, parseAtlasSources };
@@ -1,4 +1,4 @@
1
- import { r as Texture } from "./loader-TFFNV4Jl.js";
1
+ import { r as Texture } from "./loader-Myy22TDL.js";
2
2
  //#region src/render/blend.d.ts
3
3
  /** RGBA float pixels for render-time work; converted back to 8-bit at encode. */
4
4
  interface FloatTexture {
@@ -19,7 +19,8 @@ declare function q16(value: number): number;
19
19
  /**
20
20
  * Source-over with straight alpha, in place on `dst` at `offset`:
21
21
  * ra = 1-(1-sa)(1-da); rc = (sc*sa + dc*da*(1-sa)) / ra; rgb = 0 when ra = 0.
22
- * Results are Q16-quantized like every Imagick pixel write.
22
+ * Results are Q16-quantized like every Imagick pixel write. q16 is inlined
23
+ * here because blendPixel is the single hottest function in the pipeline.
23
24
  */
24
25
  declare function blendPixel(dst: Float64Array, offset: number, sr: number, sg: number, sb: number, sa: number): void;
25
26
  /**
@@ -1,4 +1,6 @@
1
1
  //#region src/render/blend.ts
2
+ /** Lookup table for Q16 → 8-bit conversion (ImageMagick's floor(q/257)). */
3
+ const BYTE_LUT = /* @__PURE__ */ new Uint8Array(65536);
2
4
  function toFloatTexture(texture) {
3
5
  const data = new Float64Array(texture.data.length);
4
6
  for (let i = 0; i < texture.data.length; i++) data[i] = texture.data[i] / 255;
@@ -10,9 +12,11 @@ function toFloatTexture(texture) {
10
12
  }
11
13
  function toByteTexture(texture) {
12
14
  const data = new Uint8ClampedArray(texture.data.length);
13
- for (let i = 0; i < texture.data.length; i++) {
14
- const value = texture.data[i];
15
- data[i] = Math.floor(Math.round((value < 0 ? 0 : Math.min(value, 1)) * 65535) / 257);
15
+ if (BYTE_LUT[65535] === 0) for (let q = 0; q < 65536; q++) BYTE_LUT[q] = Math.floor(q / 257);
16
+ const src = texture.data;
17
+ for (let i = 0; i < src.length; i++) {
18
+ const value = src[i];
19
+ data[i] = BYTE_LUT[Math.round((value < 0 ? 0 : value > 1 ? 1 : value) * 65535)];
16
20
  }
17
21
  return {
18
22
  width: texture.width,
@@ -38,7 +42,8 @@ function q16(value) {
38
42
  /**
39
43
  * Source-over with straight alpha, in place on `dst` at `offset`:
40
44
  * ra = 1-(1-sa)(1-da); rc = (sc*sa + dc*da*(1-sa)) / ra; rgb = 0 when ra = 0.
41
- * Results are Q16-quantized like every Imagick pixel write.
45
+ * Results are Q16-quantized like every Imagick pixel write. q16 is inlined
46
+ * here because blendPixel is the single hottest function in the pipeline.
42
47
  */
43
48
  function blendPixel(dst, offset, sr, sg, sb, sa) {
44
49
  const da = dst[offset + 3];
@@ -51,10 +56,17 @@ function blendPixel(dst, offset, sr, sg, sb, sa) {
51
56
  return;
52
57
  }
53
58
  const dstWeight = da * (1 - sa);
54
- dst[offset] = q16((sr * sa + dst[offset] * dstWeight) / ra);
55
- dst[offset + 1] = q16((sg * sa + dst[offset + 1] * dstWeight) / ra);
56
- dst[offset + 2] = q16((sb * sa + dst[offset + 2] * dstWeight) / ra);
57
- dst[offset + 3] = q16(ra);
59
+ let v = (sr * sa + dst[offset] * dstWeight) / ra;
60
+ v = v < 0 ? 0 : v > 1 ? 1 : v;
61
+ dst[offset] = Math.round(v * 65535) / 65535;
62
+ v = (sg * sa + dst[offset + 1] * dstWeight) / ra;
63
+ v = v < 0 ? 0 : v > 1 ? 1 : v;
64
+ dst[offset + 1] = Math.round(v * 65535) / 65535;
65
+ v = (sb * sa + dst[offset + 2] * dstWeight) / ra;
66
+ v = v < 0 ? 0 : v > 1 ? 1 : v;
67
+ dst[offset + 2] = Math.round(v * 65535) / 65535;
68
+ v = ra < 0 ? 0 : ra > 1 ? 1 : ra;
69
+ dst[offset + 3] = Math.round(v * 65535) / 65535;
58
70
  }
59
71
  /**
60
72
  * Upstream tintImage: uses ONLY the red channel as luminance — per pixel,
@@ -0,0 +1,66 @@
1
+ //#region src/resources/cache.ts
2
+ /**
3
+ * Least-recently-used map used by the model and texture loaders. Their caches
4
+ * were unbounded, which is fine for one namespace and not fine for a run that
5
+ * walks thousands of them in a single process — resident memory only ever grew.
6
+ */
7
+ var LruCache = class {
8
+ entries = /* @__PURE__ */ new Map();
9
+ maxEntries;
10
+ weigh;
11
+ maxWeight;
12
+ weights = /* @__PURE__ */ new Map();
13
+ weight = 0;
14
+ constructor(maxEntries, options = {}) {
15
+ this.maxEntries = Math.max(1, maxEntries);
16
+ this.maxWeight = options.maxWeight ?? Infinity;
17
+ this.weigh = options.weigh ?? (() => 0);
18
+ }
19
+ evict() {
20
+ while (this.entries.size > this.maxEntries || this.weight > this.maxWeight && this.entries.size > 1) {
21
+ const oldest = this.entries.keys().next();
22
+ if (oldest.done) return;
23
+ this.weight -= this.weights.get(oldest.value) ?? 0;
24
+ this.weights.delete(oldest.value);
25
+ this.entries.delete(oldest.value);
26
+ }
27
+ }
28
+ get size() {
29
+ return this.entries.size;
30
+ }
31
+ get(key) {
32
+ const value = this.entries.get(key);
33
+ if (value !== void 0) {
34
+ this.entries.delete(key);
35
+ this.entries.set(key, value);
36
+ }
37
+ return value;
38
+ }
39
+ set(key, value) {
40
+ if (this.entries.has(key)) {
41
+ this.weight -= this.weights.get(key) ?? 0;
42
+ this.entries.delete(key);
43
+ }
44
+ this.entries.set(key, value);
45
+ this.weights.set(key, 0);
46
+ this.evict();
47
+ }
48
+ /**
49
+ * Record a value's real cost once it is known. Loaders cache promises, so the
50
+ * weight of a decoded texture only exists after it resolves.
51
+ */
52
+ settle(key, value) {
53
+ if (!this.entries.has(key)) return;
54
+ const size = this.weigh(value);
55
+ this.weight += size - (this.weights.get(key) ?? 0);
56
+ this.weights.set(key, size);
57
+ this.evict();
58
+ }
59
+ clear() {
60
+ this.entries.clear();
61
+ this.weights.clear();
62
+ this.weight = 0;
63
+ }
64
+ };
65
+ //#endregion
66
+ export { LruCache as t };
package/dist/cli.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { n as builtinPackPath, t as bedCompatPath } from "./builtin-caYjESh0.js";
3
- import { n as LayeredResourceManager, t as FolderResourceManager } from "./manager-dfI7b9N4.js";
4
- import { n as harvestTintOverrides, t as curatedTintOverrides } from "./tints-CuG6GwQJ.js";
5
- import { t as JarResourceManager } from "./jar-Bt3i0ls4.js";
6
- import { t as run } from "./run-CfnWIRY4.js";
3
+ import { n as LayeredResourceManager, t as FolderResourceManager } from "./manager-CkletR5K.js";
4
+ import { n as harvestTintOverrides, t as curatedTintOverrides } from "./tints-B8UJmZMu.js";
5
+ import { t as JarResourceManager } from "./jar-D9z-X7tA.js";
6
+ import { t as run } from "./run-B0qPw3zf.js";
7
7
  import { t as downloadClientJar } from "./download-D9aARBFp.js";
8
- import { a as prepareVersionResources, i as eraForVersion, n as GEOMETRY_DONOR_VERSIONS } from "./prepare-C8wRjAXP.js";
8
+ import { a as prepareVersionResources, i as eraForVersion, n as GEOMETRY_DONOR_VERSIONS } from "./prepare-Cc8QZ3j0.js";
9
9
  import path from "node:path";
10
10
  import os from "node:os";
11
11
  import { parseArgs } from "node:util";
@@ -88,6 +88,10 @@ const { values } = parseArgs({ options: {
88
88
  "builtin": { type: "string" },
89
89
  "cache-dir": { type: "string" },
90
90
  "concurrency": { type: "string" },
91
+ "workers": {
92
+ type: "string",
93
+ short: "w"
94
+ },
91
95
  "help": {
92
96
  type: "boolean",
93
97
  short: "h",
@@ -122,7 +126,8 @@ Options:
122
126
  --lighting-profile modern (default) | legacy (matches published corpus)
123
127
  --builtin <dir> override the builtin pack (defaults per era)
124
128
  --cache-dir <dir> jar download cache (default ~/.rendermite)
125
- --concurrency <n> in-flight items (default 8)
129
+ --concurrency <n> in-flight items per thread (default 8)
130
+ -w, --workers <n> render threads (default: one per core, less one)
126
131
  `);
127
132
  process.exit(values.help ? 0 : 1);
128
133
  }
@@ -188,6 +193,7 @@ const result = await run({
188
193
  gif: values.gif,
189
194
  avif: values.avif,
190
195
  concurrency: values.concurrency ? Number(values.concurrency) : void 0,
196
+ workers: values.workers ? Number(values.workers) : void 0,
191
197
  lightingProfile: values["lighting-profile"] === "legacy" ? "legacy" : "modern",
192
198
  itemList: values["item-list"],
193
199
  onProgress: (done, total, id, error) => {
@@ -0,0 +1,104 @@
1
+ import { t as blendPixel } from "./blend-D0XWgVQr.js";
2
+ //#region src/render/compose.ts
3
+ /**
4
+ * Per-pixel painter's compositing, mirroring upstream Model::combineFaceImages:
5
+ * at each pixel, faces sorted ascending by that pixel's depth value (0 where the
6
+ * rasterizer never wrote depth — the overdraw band relies on sorting beneath),
7
+ * then source-over blended in that order when alpha > 0.
8
+ *
9
+ * Upstream re-sorts one persistent array, so each pixel's sort is really a
10
+ * stable partition: faces the pixel misses keep their order, faces with depth
11
+ * move to the end in depth order. That reordering is invariant under taking
12
+ * subsets, so instead of touching every face at every pixel we keep a rank per
13
+ * face (a moved face gets a fresh, higher rank) and visit only the faces whose
14
+ * bounding box contains the pixel. Same order, same pixels, sublinear in faces.
15
+ */
16
+ function combineFaceBuffers(width, height, faces) {
17
+ const out = new Float64Array(width * height * 4);
18
+ const count = faces.length;
19
+ if (count === 0) return {
20
+ width,
21
+ height,
22
+ data: out
23
+ };
24
+ const rowCounts = new Int32Array(height + 1);
25
+ for (const face of faces) {
26
+ const endY = Math.min(face.y + face.height, height);
27
+ for (let y = Math.max(face.y, 0); y < endY; y++) rowCounts[y]++;
28
+ }
29
+ const rowStart = new Int32Array(height + 1);
30
+ for (let y = 0; y < height; y++) rowStart[y + 1] = rowStart[y] + rowCounts[y];
31
+ const cursor = [...rowStart];
32
+ const rowFaces = new Int32Array(rowStart[height]);
33
+ for (let index = 0; index < count; index++) {
34
+ const face = faces[index];
35
+ const endY = Math.min(face.y + face.height, height);
36
+ for (let y = Math.max(face.y, 0); y < endY; y++) rowFaces[cursor[y]++] = index;
37
+ }
38
+ const rank = new Float64Array(count);
39
+ for (let index = 0; index < count; index++) rank[index] = index;
40
+ let nextRank = count;
41
+ const flat = new Int32Array(count);
42
+ const flatRank = new Float64Array(count);
43
+ const deep = new Int32Array(count);
44
+ const deepDepth = new Float64Array(count);
45
+ const deepRank = new Float64Array(count);
46
+ for (let y = 0; y < height; y++) {
47
+ const start = rowStart[y];
48
+ const end = rowStart[y + 1];
49
+ if (start === end) continue;
50
+ for (let x = 0; x < width; x++) {
51
+ let flatCount = 0;
52
+ let deepCount = 0;
53
+ for (let slot = start; slot < end; slot++) {
54
+ const index = rowFaces[slot];
55
+ const face = faces[index];
56
+ const localX = x - face.x;
57
+ if (localX < 0 || localX >= face.width) continue;
58
+ const localPixel = (y - face.y) * face.width + localX;
59
+ const depth = face.depths[localPixel];
60
+ if (depth > 0) {
61
+ let slotIndex = deepCount++;
62
+ while (slotIndex > 0 && (deepDepth[slotIndex - 1] > depth || deepDepth[slotIndex - 1] === depth && deepRank[slotIndex - 1] > rank[index])) {
63
+ deep[slotIndex] = deep[slotIndex - 1];
64
+ deepDepth[slotIndex] = deepDepth[slotIndex - 1];
65
+ deepRank[slotIndex] = deepRank[slotIndex - 1];
66
+ slotIndex--;
67
+ }
68
+ deep[slotIndex] = index;
69
+ deepDepth[slotIndex] = depth;
70
+ deepRank[slotIndex] = rank[index];
71
+ } else {
72
+ let slotIndex = flatCount++;
73
+ while (slotIndex > 0 && flatRank[slotIndex - 1] > rank[index]) {
74
+ flat[slotIndex] = flat[slotIndex - 1];
75
+ flatRank[slotIndex] = flatRank[slotIndex - 1];
76
+ slotIndex--;
77
+ }
78
+ flat[slotIndex] = index;
79
+ flatRank[slotIndex] = rank[index];
80
+ }
81
+ }
82
+ if (flatCount === 0 && deepCount === 0) continue;
83
+ const offset = (y * width + x) * 4;
84
+ for (let i = 0; i < flatCount; i++) blendFace(out, offset, faces[flat[i]], x, y);
85
+ for (let i = 0; i < deepCount; i++) {
86
+ const index = deep[i];
87
+ blendFace(out, offset, faces[index], x, y);
88
+ rank[index] = nextRank++;
89
+ }
90
+ }
91
+ }
92
+ return {
93
+ width,
94
+ height,
95
+ data: out
96
+ };
97
+ }
98
+ function blendFace(out, offset, face, x, y) {
99
+ const faceOffset = ((y - face.y) * face.width + (x - face.x)) * 4;
100
+ const alpha = face.colors[faceOffset + 3];
101
+ if (alpha > 0) blendPixel(out, offset, face.colors[faceOffset], face.colors[faceOffset + 1], face.colors[faceOffset + 2], alpha);
102
+ }
103
+ //#endregion
104
+ export { combineFaceBuffers as t };
@@ -0,0 +1,26 @@
1
+ import { n as LayeredResourceManager, t as FolderResourceManager } from "./manager-CkletR5K.js";
2
+ import { n as MapResourceManager, t as KindFilteredResourceManager } from "./synthetic-ZAUjXT7E.js";
3
+ import { t as JarResourceManager } from "./jar-D9z-X7tA.js";
4
+ //#region src/resources/descriptor.ts
5
+ /**
6
+ * Serializable descriptions of a resource tree, so the same layered tree can be
7
+ * rebuilt inside a worker thread. Managers describe themselves; a tree
8
+ * containing anything that cannot (a consumer's own ResourceManager) describes
9
+ * as null, and the caller renders in-process rather than silently dropping
10
+ * layers.
11
+ */
12
+ function describeResources(manager) {
13
+ const describable = manager;
14
+ return typeof describable.describe === "function" ? describable.describe() : null;
15
+ }
16
+ function restoreResources(descriptor) {
17
+ switch (descriptor.kind) {
18
+ case "folder": return new FolderResourceManager(descriptor.root);
19
+ case "jar": return new JarResourceManager(descriptor.path);
20
+ case "map": return MapResourceManager.fromEntries(descriptor.entries);
21
+ case "layered": return new LayeredResourceManager(descriptor.layers.map((layer) => restoreResources(layer)));
22
+ case "kindFiltered": return new KindFilteredResourceManager(restoreResources(descriptor.inner), descriptor.kinds);
23
+ }
24
+ }
25
+ //#endregion
26
+ export { restoreResources as n, describeResources as t };
@@ -1,4 +1,4 @@
1
- import { a as ResourceManager } from "./manager-CoZaeQTu.js";
1
+ import { a as ResourceManager } from "./manager-Cv1NeFyF.js";
2
2
  import { i as ItemRenderInstruction, r as ItemProperties } from "./definition-BwX6Xxv0.js";
3
3
  //#region src/items/discovery.d.ts
4
4
  declare class ItemResolutionError extends Error {}
@@ -1,2 +1,2 @@
1
- import { a as TRIM_DARKER_MATERIALS, i as POTTERY_SHERDS, n as DynamicItem, o as TRIM_MATERIALS, r as DynamicResources, s as builtinDynamicResources, t as ARMOR_ITEM_TYPES } from "../generators-CCB8h0mP.js";
1
+ import { a as TRIM_DARKER_MATERIALS, i as POTTERY_SHERDS, n as DynamicItem, o as TRIM_MATERIALS, r as DynamicResources, s as builtinDynamicResources, t as ARMOR_ITEM_TYPES } from "../generators-DhRov4RB.js";
2
2
  export { ARMOR_ITEM_TYPES, DynamicItem, DynamicResources, POTTERY_SHERDS, TRIM_DARKER_MATERIALS, TRIM_MATERIALS, builtinDynamicResources };
@@ -1,2 +1,2 @@
1
- import { a as builtinDynamicResources, i as TRIM_MATERIALS, n as POTTERY_SHERDS, r as TRIM_DARKER_MATERIALS, t as ARMOR_ITEM_TYPES } from "../generators-CcVRG30y.js";
1
+ import { a as builtinDynamicResources, i as TRIM_MATERIALS, n as POTTERY_SHERDS, r as TRIM_DARKER_MATERIALS, t as ARMOR_ITEM_TYPES } from "../generators-BlG4KmtP.js";
2
2
  export { ARMOR_ITEM_TYPES, POTTERY_SHERDS, TRIM_DARKER_MATERIALS, TRIM_MATERIALS, builtinDynamicResources };
@@ -1,4 +1,4 @@
1
- import { t as FloatTexture } from "./blend-Qvrn6YgK.js";
1
+ import { t as FloatTexture } from "./blend-CEHoJ7tM.js";
2
2
  //#region src/output/encode.d.ts
3
3
  interface EncodedIcon {
4
4
  png: Buffer;
@@ -1,4 +1,4 @@
1
- import { o as toByteTexture } from "./blend-CHP2-j9U.js";
1
+ import { o as toByteTexture } from "./blend-D0XWgVQr.js";
2
2
  import sharp from "sharp";
3
3
  //#region src/output/encode.ts
4
4
  /**
@@ -6,6 +6,17 @@ import sharp from "sharp";
6
6
  * separate optimization step is needed: lossless everywhere, strictly-smaller
7
7
  * candidates only.
8
8
  */
9
+ /**
10
+ * Lossless WebP effort for single-frame icons. Measured across 1.12.2–1.21.8
11
+ * vanilla, effort 2 is 0.1–1.9% SMALLER than effort 6 for static icons at 2.8×
12
+ * the speed — libvips' higher efforts search predictors that rarely pay off on
13
+ * 64px sprites. Animations are the opposite (up to 21% larger on one item), so
14
+ * they keep effort 6 below.
15
+ */
16
+ const STATIC_WEBP_EFFORT = 2;
17
+ const ANIMATED_WEBP_EFFORT = 6;
18
+ /** Frames encoded concurrently when assembling an animation's pages. */
19
+ const PAGE_ENCODE_BATCH = 8;
9
20
  /** Losslessly transcode an existing PNG resource into the formats referenced by generated CSS. */
10
21
  async function encodeRasterAsset(source, formats) {
11
22
  const image = sharp(source);
@@ -37,26 +48,35 @@ async function encodeRasterAsset(source, formats) {
37
48
  async function encodeAnimatedIcon(frames, delaysMs, options = {}) {
38
49
  if (frames.length === 1) return encodeIcon(frames[0], options);
39
50
  const { width, height } = frames[0];
40
- const pages = await Promise.all(frames.map((frame) => {
51
+ const pages = [];
52
+ for (let start = 0; start < frames.length; start += PAGE_ENCODE_BATCH) pages.push(...await Promise.all(frames.slice(start, start + PAGE_ENCODE_BATCH).map((frame) => {
41
53
  const bytes = toByteTexture(frame);
42
54
  return sharp(Buffer.from(bytes.data.buffer, bytes.data.byteOffset, bytes.data.length), { raw: {
43
55
  width,
44
56
  height,
45
57
  channels: 4
46
58
  } }).png({ compressionLevel: 0 }).toBuffer();
47
- }));
59
+ })));
48
60
  const animated = () => sharp(pages, { join: { animated: true } });
49
- const result = { png: (await encodeIcon(frames[0], { webp: false })).png };
50
- if (options.webp !== false) result.webp = await animated().webp({
61
+ const webpPromise = options.webp !== false ? animated().webp({
51
62
  lossless: true,
52
- effort: 6,
63
+ effort: ANIMATED_WEBP_EFFORT,
53
64
  loop: 0,
54
65
  delay: delaysMs
55
- }).toBuffer();
56
- if (options.gif) result.gif = await animated().gif({
66
+ }).toBuffer() : Promise.resolve(void 0);
67
+ const firstFramePromise = encodeIcon(frames[0], { webp: false });
68
+ const gifPromise = options.gif ? animated().gif({
57
69
  loop: 0,
58
70
  delay: delaysMs
59
- }).toBuffer();
71
+ }).toBuffer() : Promise.resolve(void 0);
72
+ const [firstFrame, webp, gif] = await Promise.all([
73
+ firstFramePromise,
74
+ webpPromise,
75
+ gifPromise
76
+ ]);
77
+ const result = { png: firstFrame.png };
78
+ if (webp) result.webp = webp;
79
+ if (gif) result.gif = gif;
60
80
  if (options.avif) try {
61
81
  result.avif = await animated().avif({
62
82
  lossless: true,
@@ -89,19 +109,27 @@ async function encodeIcon(texture, options = {}) {
89
109
  channels: 4
90
110
  } });
91
111
  const paletteSafe = countColors(bytes.data, 256) <= 256;
92
- const result = { png: await raw.clone().png({
112
+ const pngPromise = raw.clone().png({
93
113
  compressionLevel: 9,
94
114
  effort: 10,
95
115
  palette: paletteSafe
96
- }).toBuffer() };
97
- if (options.webp !== false) result.webp = await raw.clone().webp({
98
- lossless: true,
99
- effort: 6
100
116
  }).toBuffer();
101
- if (options.avif) result.avif = await raw.clone().avif({
117
+ const webpPromise = options.webp !== false ? raw.clone().webp({
118
+ lossless: true,
119
+ effort: STATIC_WEBP_EFFORT
120
+ }).toBuffer() : Promise.resolve(void 0);
121
+ const avifPromise = options.avif ? raw.clone().avif({
102
122
  lossless: true,
103
123
  effort: 6
104
- }).toBuffer();
124
+ }).toBuffer() : Promise.resolve(void 0);
125
+ const [png, webp, avif] = await Promise.all([
126
+ pngPromise,
127
+ webpPromise,
128
+ avifPromise
129
+ ]);
130
+ const result = { png };
131
+ if (webp) result.webp = webp;
132
+ if (avif) result.avif = avif;
105
133
  return result;
106
134
  }
107
135
  //#endregion
@@ -1,5 +1,5 @@
1
1
  import { n as ResourceLocator } from "./locator-ycZzh_6z.js";
2
- import { i as renderModel } from "./renderer-LDhBNHcl.js";
2
+ import { i as renderModel } from "./renderer-gQW_I7YG.js";
3
3
  //#region src/render/entity-fallback.ts
4
4
  /**
5
5
  * Heuristic rendering for mod items that parent `builtin/entity` — chests,
@@ -1,8 +1,8 @@
1
- import { t as FloatTexture } from "./blend-Qvrn6YgK.js";
1
+ import { t as FloatTexture } from "./blend-CEHoJ7tM.js";
2
2
  import { t as Mat4 } from "./vector-DRKRi4--.js";
3
3
  import { n as FaceGeometry } from "./geometry-BSVmKx5b.js";
4
4
  import { r as LightSource } from "./lighting-3FGRC3Af.js";
5
- import { t as FaceBuffer } from "./rasterizer-Dh35qXbn.js";
5
+ import { n as FaceBuffer } from "./rasterizer-B9ZGJI3-.js";
6
6
  //#region src/render/face.d.ts
7
7
  interface TintColor {
8
8
  r: number;
@@ -17,8 +17,14 @@ interface RenderableFace {
17
17
  /** Untransformed model-space max Z — upstream's stable pre-sort key. */
18
18
  maxZ: number;
19
19
  }
20
+ /** Tinted + darkened textures keyed by source texture, then by shading params. */
21
+ type FaceShadeCache = WeakMap<FloatTexture, Map<string, FloatTexture>>;
20
22
  declare function faceMaxZ(geometry: FaceGeometry): number;
21
23
  /** Returns null when the face is culled, degenerate, or has a zero-area UV. */
22
- declare function renderFace(face: RenderableFace, width: number, height: number, transformation: Mat4): FaceBuffer | null;
24
+ declare function renderFace(face: RenderableFace, width: number, height: number, transformation: Mat4,
25
+ /** Reused across a model's faces; cleared here, compacted before return. */
26
+ scratch?: FaceBuffer,
27
+ /** Shared tint/shade results for one model render. */
28
+ shadeCache?: FaceShadeCache): FaceBuffer | null;
23
29
  //#endregion
24
- export { renderFace as i, TintColor as n, faceMaxZ as r, RenderableFace as t };
30
+ export { renderFace as a, faceMaxZ as i, RenderableFace as n, TintColor as r, FaceShadeCache as t };
@@ -1,6 +1,6 @@
1
1
  import { c as normalize, g as transformVector, h as transformDirection, m as sub, r as dot, t as cross } from "./vector-CCxLae_T.js";
2
- import { a as tintTexture, n as cloneFloatTexture, r as darkenTexture } from "./blend-CHP2-j9U.js";
3
- import { a as phpRound, i as drawTexturedTriangle, n as compactFaceBuffer, r as createFaceBuffer, t as RowCursor } from "./rasterizer-BHWDrmGz.js";
2
+ import { a as tintTexture, n as cloneFloatTexture, r as darkenTexture } from "./blend-D0XWgVQr.js";
3
+ import { a as drawTexturedTriangle, i as createFaceBuffer, n as clearFaceBuffer, o as phpRound, r as compactFaceBuffer, t as RowCursor } from "./rasterizer-76qO8TDh.js";
4
4
  //#region src/render/face.ts
5
5
  /**
6
6
  * Face rendering: cull, transform, project, tint, darken, rasterize — a faithful
@@ -18,7 +18,7 @@ function faceNormal(v0, v1, v2) {
18
18
  return normalize(normal);
19
19
  }
20
20
  /** Returns null when the face is culled, degenerate, or has a zero-area UV. */
21
- function renderFace(face, width, height, transformation) {
21
+ function renderFace(face, width, height, transformation, scratch, shadeCache) {
22
22
  const { geometry } = face;
23
23
  const uvWidth = geometry.uv2[0] - geometry.uv1[0];
24
24
  const uvHeight = geometry.uv2[1] - geometry.uv1[1];
@@ -49,13 +49,25 @@ function renderFace(face, width, height, transformation) {
49
49
  y: phpRound(height - ((v[1] - .5) * height + height / 2))
50
50
  };
51
51
  });
52
- const texture = cloneFloatTexture(face.texture);
53
- const absU1 = geometry.uv1[0] * texture.width;
54
- const absV1 = geometry.uv1[1] * texture.height;
55
- const absU2 = geometry.uv2[0] * texture.width;
56
- const absV2 = geometry.uv2[1] * texture.height;
57
- if (geometry.tintIndex !== null && face.tint !== null) tintTexture(texture, face.tint.r, face.tint.g, face.tint.b);
58
- darkenTexture(texture, 1 - face.light.lightLevelForNormal(shadeDirection), absU1, absV1, absU2, absV2);
52
+ const absU1 = geometry.uv1[0] * face.texture.width;
53
+ const absV1 = geometry.uv1[1] * face.texture.height;
54
+ const absU2 = geometry.uv2[0] * face.texture.width;
55
+ const absV2 = geometry.uv2[1] * face.texture.height;
56
+ const tint = geometry.tintIndex !== null ? face.tint : null;
57
+ const darkness = 1 - face.light.lightLevelForNormal(shadeDirection);
58
+ const shadeKey = `${tint ? `${tint.r},${tint.g},${tint.b}` : "-"}|${darkness}|${absU1},${absV1},${absU2},${absV2}`;
59
+ let byKey = shadeCache?.get(face.texture);
60
+ let texture = byKey?.get(shadeKey);
61
+ if (!texture) {
62
+ texture = cloneFloatTexture(face.texture);
63
+ if (tint) tintTexture(texture, tint.r, tint.g, tint.b);
64
+ darkenTexture(texture, darkness, absU1, absV1, absU2, absV2);
65
+ if (shadeCache) {
66
+ byKey ??= /* @__PURE__ */ new Map();
67
+ byKey.set(shadeKey, texture);
68
+ shadeCache.set(face.texture, byKey);
69
+ }
70
+ }
59
71
  const uvs = [
60
72
  [geometry.uv1[0], geometry.uv1[1]],
61
73
  [geometry.uv1[0] + uvWidth, geometry.uv1[1]],
@@ -67,7 +79,8 @@ function renderFace(face, width, height, transformation) {
67
79
  const last = uvs.pop();
68
80
  if (last) uvs.unshift(last);
69
81
  }
70
- const buffer = createFaceBuffer(width, height);
82
+ const buffer = scratch ?? createFaceBuffer(width, height);
83
+ clearFaceBuffer(buffer);
71
84
  const depths = [
72
85
  linearDepth(v0[2]),
73
86
  linearDepth(v1[2]),
@@ -1,4 +1,4 @@
1
- import { n as MapResourceManager } from "./synthetic-DC2bSnGe.js";
1
+ import { n as MapResourceManager } from "./synthetic-ZAUjXT7E.js";
2
2
  //#region src/dynamic/generators.ts
3
3
  /**
4
4
  * Dynamic resources ported from renderchest's DynamicResourceGenerators, built
@@ -1,4 +1,4 @@
1
- import { n as MapResourceManager } from "./synthetic-CDsj--li.js";
1
+ import { n as MapResourceManager } from "./synthetic-Qk-BSUXO.js";
2
2
  //#region src/dynamic/generators.d.ts
3
3
  declare const TRIM_MATERIALS: readonly ["quartz", "iron", "gold", "diamond", "netherite", "redstone", "copper", "emerald", "lapis", "amethyst", "resin"];
4
4
  /** Materials with a darker variant used when the trim matches the armor metal. */
@@ -1,8 +1,9 @@
1
1
  import { n as displayMatrix } from "./vector-CCxLae_T.js";
2
2
  import { n as ResourceLocator } from "./locator-ycZzh_6z.js";
3
- import { s as toFloatTexture } from "./blend-CHP2-j9U.js";
4
- import { t as combineFaceBuffers } from "./compose-qjhyBvnS.js";
5
- import { n as renderFace, t as faceMaxZ } from "./face-D-N1amV4.js";
3
+ import { s as toFloatTexture } from "./blend-D0XWgVQr.js";
4
+ import { t as combineFaceBuffers } from "./compose-lWPrB7wh.js";
5
+ import { i as createFaceBuffer } from "./rasterizer-76qO8TDh.js";
6
+ import { n as renderFace, t as faceMaxZ } from "./face-Cl1O4m2i.js";
6
7
  import { c as lightsForProfile } from "./lighting--5myudVu.js";
7
8
  //#region src/render/geo.ts
8
9
  /**
@@ -322,8 +323,10 @@ async function renderGeo(geo, texture, textures, options) {
322
323
  });
323
324
  renderable.sort((a, b) => a.maxZ - b.maxZ);
324
325
  const buffers = [];
326
+ const scratch = createFaceBuffer(renderSize, renderSize);
327
+ const shadeCache = /* @__PURE__ */ new WeakMap();
325
328
  for (const face of renderable) {
326
- const buffer = renderFace(face, renderSize, renderSize, transformation);
329
+ const buffer = renderFace(face, renderSize, renderSize, transformation, scratch, shadeCache);
327
330
  if (buffer) buffers.push(buffer);
328
331
  }
329
332
  if (buffers.length === 0) return null;