@nodecraft/rendermite 1.0.4 → 1.0.6

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 (121) hide show
  1. package/README.md +5 -0
  2. package/assets/README.md +3 -0
  3. package/assets/mod-tints/README.md +38 -0
  4. package/assets/mod-tints/advanced_ae/items/quantum_boots.json +16 -0
  5. package/assets/mod-tints/advanced_ae/items/quantum_chestplate.json +16 -0
  6. package/assets/mod-tints/advanced_ae/items/quantum_helmet.json +16 -0
  7. package/assets/mod-tints/advanced_ae/items/quantum_leggings.json +16 -0
  8. package/dist/atlas/registry.d.ts +3 -3
  9. package/dist/atlas/sources.d.ts +1 -1
  10. package/dist/{blend-Qvrn6YgK.d.ts → blend-CyDIxCne.d.ts} +1 -1
  11. package/dist/{builtin-C8GZEN40.js → builtin-caYjESh0.js} +5 -1
  12. package/dist/builtin.d.ts +3 -1
  13. package/dist/builtin.js +2 -2
  14. package/dist/cache-Cr2ON3RQ.js +66 -0
  15. package/dist/cli.js +40 -7
  16. package/dist/compose-CQCb2LVv.js +104 -0
  17. package/dist/descriptor-DWebtkI1.js +26 -0
  18. package/dist/{discovery-D89pVVJA.d.ts → discovery-wiJBGryo.d.ts} +1 -1
  19. package/dist/dynamic/generators.d.ts +1 -1
  20. package/dist/dynamic/generators.js +1 -1
  21. package/dist/{encode-3u49i-eD.js → encode-D6dQu2dJ.js} +16 -4
  22. package/dist/{encode-C5kdAm45.d.ts → encode-DaNQF28M.d.ts} +1 -1
  23. package/dist/{entity-fallback-CdCVEG6X.js → entity-fallback-BRCeGKBT.js} +1 -1
  24. package/dist/{face-CpVdnf-w.d.ts → face-BIYg14TE.d.ts} +10 -4
  25. package/dist/{face-D-N1amV4.js → face-DmCiHUd5.js} +23 -10
  26. package/dist/{generators-CcVRG30y.js → generators-BlG4KmtP.js} +1 -1
  27. package/dist/{generators-CCB8h0mP.d.ts → generators-DhRov4RB.d.ts} +1 -1
  28. package/dist/{geo-u8rndjLA.js → geo-Diky41Tw.js} +7 -4
  29. package/dist/index.d.ts +14 -14
  30. package/dist/index.js +13 -13
  31. package/dist/{item-styles-DJ3LWZS2.js → item-styles-upgEO1yP.js} +1 -1
  32. package/dist/items/discovery.d.ts +1 -1
  33. package/dist/items/discovery.js +1 -1
  34. package/dist/{jar-6T3xdgaL.d.ts → jar-CFkA-VgT.d.ts} +2 -1
  35. package/dist/{jar-Bt3i0ls4.js → jar-D9z-X7tA.js} +12 -12
  36. package/dist/{legacy-reshape-CewShfF3.js → legacy-reshape-C05SfAPR.js} +1 -1
  37. package/dist/{loader-A-BZa8J4.js → loader-B8NJO7pR.js} +12 -3
  38. package/dist/{loader-CBzn3Koc.js → loader-CfXMxcEV.js} +37 -7
  39. package/dist/{loader-C-6QN3xv.d.ts → loader-D22zZME7.d.ts} +2 -2
  40. package/dist/{loader-TFFNV4Jl.d.ts → loader-Myy22TDL.d.ts} +8 -2
  41. package/dist/{manager-dfI7b9N4.js → manager-CkletR5K.js} +29 -2
  42. package/dist/{manager-CoZaeQTu.d.ts → manager-Cv1NeFyF.d.ts} +28 -1
  43. package/dist/models/loader.d.ts +1 -1
  44. package/dist/models/loader.js +1 -1
  45. package/dist/mods/prepare.d.ts +1 -1
  46. package/dist/mods/prepare.js +1 -1
  47. package/dist/mods/synthesize.d.ts +2 -2
  48. package/dist/mods/synthesize.js +1 -1
  49. package/dist/mods/tints.d.ts +28 -0
  50. package/dist/mods/tints.js +2 -0
  51. package/dist/output/encode.d.ts +1 -1
  52. package/dist/output/encode.js +1 -1
  53. package/dist/output/item-styles.js +1 -1
  54. package/dist/pipeline-BIk1RRIp.js +221 -0
  55. package/dist/pipeline-zq-yU3Te.d.ts +58 -0
  56. package/dist/pool-CKGp1qup.js +131 -0
  57. package/dist/{prepare-Bvc5ve2m.js → prepare-BESRrh6K.js} +33 -5
  58. package/dist/{prepare-DnnGGv9k.d.ts → prepare-CPxO0kKz.d.ts} +8 -1
  59. package/dist/{prepare-DROxBZp1.js → prepare-Cc8QZ3j0.js} +4 -4
  60. package/dist/{prepare-wfWtaz_X.d.ts → prepare-Cs6QDqie.d.ts} +1 -1
  61. package/dist/{rasterizer-BHWDrmGz.js → rasterizer-76qO8TDh.js} +42 -4
  62. package/dist/{rasterizer-Dh35qXbn.d.ts → rasterizer-yqJkicm1.d.ts} +21 -2
  63. package/dist/render/blend.d.ts +1 -1
  64. package/dist/render/compose.d.ts +2 -2
  65. package/dist/render/compose.js +1 -1
  66. package/dist/render/entity-fallback.d.ts +6 -6
  67. package/dist/render/entity-fallback.js +1 -1
  68. package/dist/render/face.d.ts +2 -2
  69. package/dist/render/face.js +1 -1
  70. package/dist/render/fluid-bucket.d.ts +2 -2
  71. package/dist/render/geo.d.ts +4 -4
  72. package/dist/render/geo.js +1 -1
  73. package/dist/render/legacy-tints.d.ts +2 -2
  74. package/dist/render/obj.d.ts +5 -5
  75. package/dist/render/obj.js +1 -1
  76. package/dist/render/pipeline.d.ts +2 -0
  77. package/dist/render/pipeline.js +2 -0
  78. package/dist/render/rasterizer.d.ts +2 -2
  79. package/dist/render/rasterizer.js +2 -2
  80. package/dist/render/renderer.d.ts +2 -2
  81. package/dist/render/renderer.js +1 -1
  82. package/dist/render/tints.d.ts +1 -1
  83. package/dist/{renderer-BeOinVC8.js → renderer-CjT19tHd.js} +131 -54
  84. package/dist/{renderer-nbMCw1Pl.d.ts → renderer-DVsZHAvf.d.ts} +23 -6
  85. package/dist/resources/cache.d.ts +30 -0
  86. package/dist/resources/cache.js +2 -0
  87. package/dist/resources/descriptor.d.ts +2 -0
  88. package/dist/resources/descriptor.js +2 -0
  89. package/dist/resources/jar.d.ts +1 -1
  90. package/dist/resources/jar.js +1 -1
  91. package/dist/resources/manager.d.ts +1 -1
  92. package/dist/resources/manager.js +1 -1
  93. package/dist/resources/synthetic.d.ts +1 -1
  94. package/dist/resources/synthetic.js +1 -1
  95. package/dist/resources/zip-reader.d.ts +5 -0
  96. package/dist/resources/zip-reader.js +2 -0
  97. package/dist/run-BnICOF4I.js +209 -0
  98. package/dist/{run-BlSZw9Qk.d.ts → run-CKUtynTy.d.ts} +12 -2
  99. package/dist/run.d.ts +1 -1
  100. package/dist/run.js +1 -1
  101. package/dist/{sources-DH9dk3KF.d.ts → sources-B9X9w3qP.d.ts} +1 -1
  102. package/dist/{synthesize-CRgSEmEp.js → synthesize-3Q0aaOnK.js} +1 -1
  103. package/dist/{synthetic-CDsj--li.d.ts → synthetic-Qk-BSUXO.d.ts} +5 -1
  104. package/dist/{synthetic-DC2bSnGe.js → synthetic-ZAUjXT7E.js} +0 -0
  105. package/dist/textures/loader.d.ts +1 -1
  106. package/dist/textures/loader.js +1 -1
  107. package/dist/tints-B8UJmZMu.js +83 -0
  108. package/dist/{tints-txt-8xI5.d.ts → tints-DLBExVWN.d.ts} +2 -2
  109. package/dist/versions/legacy-reshape.d.ts +1 -1
  110. package/dist/versions/legacy-reshape.js +1 -1
  111. package/dist/versions/prepare.d.ts +1 -1
  112. package/dist/versions/prepare.js +1 -1
  113. package/dist/workers/pool.d.ts +36 -5
  114. package/dist/workers/pool.js +2 -67
  115. package/dist/workers/render-worker.d.ts +15 -0
  116. package/dist/workers/render-worker.js +40 -0
  117. package/dist/zip-reader-BSbJKUT1.js +156 -0
  118. package/package.json +4 -2
  119. package/dist/compose-qjhyBvnS.js +0 -41
  120. package/dist/run-DI-cB38y.js +0 -316
  121. package/dist/{discovery-iph0P2F2.js → discovery-0SM0cTLw.js} +1 -1
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
package/assets/README.md CHANGED
@@ -15,3 +15,6 @@ generators).
15
15
 
16
16
  Layout is `<namespace>/<models|textures|items|atlases>/…` (no `assets/` nesting) — mount with
17
17
  `FolderResourceManager` above the version's own assets.
18
+
19
+ `mod-tints/` is ours rather than vendored: curated item definitions for mod items that
20
+ register their tint colors in client code. See `mod-tints/README.md`.
@@ -0,0 +1,38 @@
1
+ # Curated mod tint overrides
2
+
3
+ Item definitions for mod items whose tint colors are registered in client Java code and
4
+ therefore appear in no release's asset JSON. Layout is `<namespace>/items/<id>.json`, the
5
+ vanilla 1.21.4+ item-definition format, mounted with `FolderResourceManager` (see
6
+ `src/mods/tints.ts`). Nothing in the renderer special-cases these files.
7
+
8
+ ## Precedence
9
+
10
+ This pack layers **above** the mod jars, unlike harvested overrides (which layer below so a
11
+ mod's own data always wins). Curated entries exist because the mod ships no usable data in
12
+ any release, and in the modern era the mod's own definition is typically an unrenderable
13
+ code-driven model type that would shadow the override. Delete an entry once the mod ships a
14
+ release that states the tint as data: the harvester will then source it authoritatively.
15
+
16
+ ## Adding an entry
17
+
18
+ Every entry needs its provenance recorded below, because these values are the only ones in
19
+ the project that cannot be traced to a jar. Hold a slot open with
20
+ `{"type": "minecraft:constant", "value": -1}` when a layer takes no tint. Values are signed
21
+ ARGB ints, matching vanilla's own definition files.
22
+
23
+ ## Entries
24
+
25
+ ### `advanced_ae` quantum armor (helmet, chestplate, leggings, boots)
26
+
27
+ `#7110a5` on layer 1, sourced by eye and **not** verifiable against any jar. Checked:
28
+
29
+ - 1.21.1 models are `layer0: <piece>_base` + `layer1: <piece>_tint`, and both textures are
30
+ fully grayscale, so nothing renders colored without an external value.
31
+ - The 26.1.x definitions use `"type": "advanced_ae:quantum_armor"` with no `tints` at all.
32
+ - `0x7110A5` appears in no class constant pool in AdvancedAE 1.21.1, AdvancedAE 26.1.7, or
33
+ AE2 1.21.1.
34
+
35
+ The mod's own `quantum_crafter_terminal_part` tints with `ae2:color` (`color: fluix`), so
36
+ these pieces are very likely AE2's fluix purple. Teaching `resolveTint` the `ae2:color`
37
+ palette would replace these four files with a sourced value and cover ~116 other AE2-family
38
+ items that currently resolve no tint at all.
@@ -0,0 +1,16 @@
1
+ {
2
+ "model": {
3
+ "type": "minecraft:model",
4
+ "model": "advanced_ae:item/quantum_boots",
5
+ "tints": [
6
+ {
7
+ "type": "minecraft:constant",
8
+ "value": -1
9
+ },
10
+ {
11
+ "type": "minecraft:constant",
12
+ "value": -9367387
13
+ }
14
+ ]
15
+ }
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "model": {
3
+ "type": "minecraft:model",
4
+ "model": "advanced_ae:item/quantum_chestplate",
5
+ "tints": [
6
+ {
7
+ "type": "minecraft:constant",
8
+ "value": -1
9
+ },
10
+ {
11
+ "type": "minecraft:constant",
12
+ "value": -9367387
13
+ }
14
+ ]
15
+ }
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "model": {
3
+ "type": "minecraft:model",
4
+ "model": "advanced_ae:item/quantum_helmet",
5
+ "tints": [
6
+ {
7
+ "type": "minecraft:constant",
8
+ "value": -1
9
+ },
10
+ {
11
+ "type": "minecraft:constant",
12
+ "value": -9367387
13
+ }
14
+ ]
15
+ }
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "model": {
3
+ "type": "minecraft:model",
4
+ "model": "advanced_ae:item/quantum_leggings",
5
+ "tints": [
6
+ {
7
+ "type": "minecraft:constant",
8
+ "value": -1
9
+ },
10
+ {
11
+ "type": "minecraft:constant",
12
+ "value": -9367387
13
+ }
14
+ ]
15
+ }
16
+ }
@@ -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 {
@@ -12,5 +12,9 @@ function builtinPackPath(era) {
12
12
  function bedCompatPath() {
13
13
  return path.join(assetsRoot, "builtin-bed-compat");
14
14
  }
15
+ /** Curated tint overrides for mod items that register their colors in code. */
16
+ function modTintPackPath() {
17
+ return path.join(assetsRoot, "mod-tints");
18
+ }
15
19
  //#endregion
16
- export { builtinPackPath as n, bedCompatPath as t };
20
+ export { builtinPackPath as n, modTintPackPath as r, bedCompatPath as t };
package/dist/builtin.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  //#region src/builtin.d.ts
2
2
  declare function builtinPackPath(era: 'modern' | 'legacy'): string;
3
3
  declare function bedCompatPath(): string;
4
+ /** Curated tint overrides for mod items that register their colors in code. */
5
+ declare function modTintPackPath(): string;
4
6
  //#endregion
5
- export { bedCompatPath, builtinPackPath };
7
+ export { bedCompatPath, builtinPackPath, modTintPackPath };
package/dist/builtin.js CHANGED
@@ -1,2 +1,2 @@
1
- import { n as builtinPackPath, t as bedCompatPath } from "./builtin-C8GZEN40.js";
2
- export { bedCompatPath, builtinPackPath };
1
+ import { n as builtinPackPath, r as modTintPackPath, t as bedCompatPath } from "./builtin-caYjESh0.js";
2
+ export { bedCompatPath, builtinPackPath, modTintPackPath };
@@ -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,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { n as builtinPackPath, t as bedCompatPath } from "./builtin-C8GZEN40.js";
3
- import { n as LayeredResourceManager, t as FolderResourceManager } from "./manager-dfI7b9N4.js";
4
- import { t as JarResourceManager } from "./jar-Bt3i0ls4.js";
5
- import { t as run } from "./run-DI-cB38y.js";
2
+ import { n as builtinPackPath, t as bedCompatPath } from "./builtin-caYjESh0.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-BnICOF4I.js";
6
7
  import { t as downloadClientJar } from "./download-D9aARBFp.js";
7
- import { a as prepareVersionResources, i as eraForVersion, n as GEOMETRY_DONOR_VERSIONS } from "./prepare-DROxBZp1.js";
8
+ import { a as prepareVersionResources, i as eraForVersion, n as GEOMETRY_DONOR_VERSIONS } from "./prepare-Cc8QZ3j0.js";
8
9
  import path from "node:path";
9
10
  import os from "node:os";
10
11
  import { parseArgs } from "node:util";
@@ -30,6 +31,10 @@ const { values } = parseArgs({ options: {
30
31
  short: "m",
31
32
  multiple: true
32
33
  },
34
+ "tint-donor": {
35
+ type: "string",
36
+ multiple: true
37
+ },
33
38
  "namespace": {
34
39
  type: "string",
35
40
  short: "n",
@@ -83,6 +88,10 @@ const { values } = parseArgs({ options: {
83
88
  "builtin": { type: "string" },
84
89
  "cache-dir": { type: "string" },
85
90
  "concurrency": { type: "string" },
91
+ "workers": {
92
+ type: "string",
93
+ short: "w"
94
+ },
86
95
  "help": {
87
96
  type: "boolean",
88
97
  short: "h",
@@ -101,6 +110,7 @@ Input:
101
110
  -v, --version <ver> download + prepare a vanilla version (1.2.5 … latest)
102
111
  -a, --assets <dir> asset root (repeatable, first wins), <namespace>/ children
103
112
  -m, --mod <jar> mod jar layered under vanilla (repeatable)
113
+ --tint-donor <jar> 1.21.4+ jar of a loaded mod, read for its tints (repeatable)
104
114
 
105
115
  Options:
106
116
  -n, --namespace <ns> namespace to render (repeatable, default: minecraft)
@@ -116,7 +126,8 @@ Options:
116
126
  --lighting-profile modern (default) | legacy (matches published corpus)
117
127
  --builtin <dir> override the builtin pack (defaults per era)
118
128
  --cache-dir <dir> jar download cache (default ~/.rendermite)
119
- --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)
120
131
  `);
121
132
  process.exit(values.help ? 0 : 1);
122
133
  }
@@ -147,7 +158,28 @@ if (values.version) {
147
158
  resources = managers.length === 1 ? managers[0] : new LayeredResourceManager(managers);
148
159
  builtin ??= builtinPackPath("modern");
149
160
  }
150
- if (values.mod?.length) resources = new LayeredResourceManager([resources, ...values.mod.map((jar) => new JarResourceManager(jar))]);
161
+ if (values["tint-donor"]?.length && !values.mod?.length) {
162
+ console.error("--tint-donor needs --mod: donors are read for the tints of loaded mods.");
163
+ process.exit(1);
164
+ }
165
+ if (values.mod?.length) {
166
+ const base = new LayeredResourceManager([
167
+ resources,
168
+ curatedTintOverrides(),
169
+ ...values.mod.map((jar) => new JarResourceManager(jar))
170
+ ]);
171
+ resources = base;
172
+ if (values["tint-donor"]?.length) {
173
+ const donorJars = values["tint-donor"].map((jar) => new JarResourceManager(jar));
174
+ const harvest = await harvestTintOverrides({
175
+ donors: new LayeredResourceManager(donorJars),
176
+ target: base
177
+ });
178
+ await Promise.all(donorJars.map((jar) => jar.close().catch(() => {})));
179
+ console.log(`Harvested tints for ${harvest.harvested} items from ${donorJars.length} donor jars`);
180
+ if (harvest.harvested > 0) resources = new LayeredResourceManager([base, harvest.overrides]);
181
+ }
182
+ }
151
183
  const result = await run({
152
184
  resources,
153
185
  builtin,
@@ -161,6 +193,7 @@ const result = await run({
161
193
  gif: values.gif,
162
194
  avif: values.avif,
163
195
  concurrency: values.concurrency ? Number(values.concurrency) : void 0,
196
+ workers: values.workers ? Number(values.workers) : void 0,
164
197
  lightingProfile: values["lighting-profile"] === "legacy" ? "legacy" : "modern",
165
198
  itemList: values["item-list"],
166
199
  onProgress: (done, total, id, error) => {
@@ -0,0 +1,104 @@
1
+ import { t as blendPixel } from "./blend-CHP2-j9U.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 };
@@ -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,19 +48,20 @@ 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
61
  const result = { png: (await encodeIcon(frames[0], { webp: false })).png };
50
62
  if (options.webp !== false) result.webp = await animated().webp({
51
63
  lossless: true,
52
- effort: 6,
64
+ effort: ANIMATED_WEBP_EFFORT,
53
65
  loop: 0,
54
66
  delay: delaysMs
55
67
  }).toBuffer();
@@ -96,7 +108,7 @@ async function encodeIcon(texture, options = {}) {
96
108
  }).toBuffer() };
97
109
  if (options.webp !== false) result.webp = await raw.clone().webp({
98
110
  lossless: true,
99
- effort: 6
111
+ effort: STATIC_WEBP_EFFORT
100
112
  }).toBuffer();
101
113
  if (options.avif) result.avif = await raw.clone().avif({
102
114
  lossless: true,
@@ -1,4 +1,4 @@
1
- import { t as FloatTexture } from "./blend-Qvrn6YgK.js";
1
+ import { t as FloatTexture } from "./blend-CyDIxCne.js";
2
2
  //#region src/output/encode.d.ts
3
3
  interface EncodedIcon {
4
4
  png: Buffer;
@@ -1,5 +1,5 @@
1
1
  import { n as ResourceLocator } from "./locator-ycZzh_6z.js";
2
- import { i as renderModel } from "./renderer-BeOinVC8.js";
2
+ import { i as renderModel } from "./renderer-CjT19tHd.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-CyDIxCne.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-yqJkicm1.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
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";
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