@needle-tools/gltf-build-pipeline 2.15.3 → 2.15.5

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.
@@ -8,7 +8,7 @@ import xxhash from "xxhash-wasm";
8
8
  let xxHashModule = null;
9
9
  xxhash().then(module => {
10
10
  xxHashModule = module;
11
- });
11
+ }).catch(() => { });
12
12
  /**
13
13
  * Get the available space on the cache directory in bytes.
14
14
  */
@@ -10,7 +10,12 @@ export declare class NeedlePmremContext implements INeedleTextureTransformContex
10
10
  private available;
11
11
  private logger;
12
12
  private readonly convertedTextures;
13
+ /** In-memory dedup: keyed by EXR content hash, resolves to KTX2 bytes */
14
+ private readonly processingPromises;
13
15
  prepare(document: Document): Promise<boolean>;
14
16
  process(index: number, texture: Texture, _settings: NEEDLE_compression_texture_schema | null, context: NeedleTransformContext): Promise<boolean>;
17
+ /** Get or process EXR bytes into KTX2, with in-memory dedup and disk caching */
18
+ private getOrProcessExr;
19
+ private processExrToKtx2;
15
20
  finalize(document: Document): Promise<void>;
16
21
  }
@@ -4,7 +4,9 @@ import { join, dirname } from 'path';
4
4
  import { fileURLToPath, pathToFileURL } from 'url';
5
5
  import { execFile } from 'child_process';
6
6
  import { v4 as uuid } from 'uuid';
7
+ import { createHash } from 'crypto';
7
8
  import tmp from 'tmp';
9
+ import { addToCache, tryGetFromCache } from '../cache/index.js';
8
10
  /** Resolve the tools/pmrem/ directory relative to this file.
9
11
  * At runtime this file is at dist/transforms/needle_pmrem.js,
10
12
  * so tools/pmrem/ is at ../../tools/pmrem/ */
@@ -46,6 +48,8 @@ export class NeedlePmremContext {
46
48
  available = false;
47
49
  logger;
48
50
  convertedTextures = [];
51
+ /** In-memory dedup: keyed by EXR content hash, resolves to KTX2 bytes */
52
+ processingPromises = new Map();
49
53
  async prepare(document) {
50
54
  this.logger = document.getLogger();
51
55
  this.processed = 0;
@@ -97,25 +101,13 @@ export class NeedlePmremContext {
97
101
  if (!image)
98
102
  return false;
99
103
  const name = texture.getName() || `texture_${index}`;
100
- const id = uuid();
101
104
  const wasExternal = !!texture.getURI();
102
- // 1. Run PMREM on the EXR bytes
103
- this.logger.info(`pmrem:texture[${index}] ${name}: Running PMREM...`);
104
105
  const exrBytes = new Uint8Array(image.buffer, image.byteOffset, image.byteLength);
105
- const pmremExrBytes = this.wasmModule.pmrem_exr(exrBytes);
106
- // 2. Write PMREM EXR to temp file
107
- const tempExr = join(this.tmpDir, `${id}.pmrem.exr`);
108
- const tempKtx2 = join(this.tmpDir, `${id}.ktx2`);
109
- writeFileSync(tempExr, Buffer.from(pmremExrBytes));
110
- // 3. Encode to KTX2 HDR via basisu
111
- this.logger.debug(`pmrem:texture[${index}] ${name}: Encoding KTX2 HDR...`);
112
- const variant = 'hdr4x4'; // 6x6 needs an update in Needle
113
- await encodeKTX2WithBasisu(this.basisuExe, tempExr, tempKtx2, variant, true);
114
- if (!existsSync(tempKtx2)) {
115
- throw new Error(`pmrem:texture[${index}] ${name}: basisu did not produce output`);
116
- }
117
- // 4. Read KTX2 and update texture
118
- const ktx2Bytes = readFileSync(tempKtx2);
106
+ // Hash the EXR content for dedup and caching
107
+ const contentHash = createHash('md5').update(exrBytes).digest('hex');
108
+ const cacheKey = `pmrem-${contentHash}`;
109
+ // Get KTX2 bytes — deduplicated and optionally cached
110
+ const ktx2Bytes = await this.getOrProcessExr(cacheKey, exrBytes, index, name, context.useCache);
119
111
  // Remove EXT_texture_exr property since the texture is now KTX2
120
112
  const exrProp = texture.getExtension('EXT_texture_exr');
121
113
  if (exrProp) {
@@ -157,6 +149,50 @@ export class NeedlePmremContext {
157
149
  this.processed++;
158
150
  return true;
159
151
  }
152
+ /** Get or process EXR bytes into KTX2, with in-memory dedup and disk caching */
153
+ getOrProcessExr(cacheKey, exrBytes, index, name, useCache) {
154
+ // In-memory dedup: if we're already processing identical EXR content, reuse the promise
155
+ const existing = this.processingPromises.get(cacheKey);
156
+ if (existing) {
157
+ this.logger.info(`pmrem:texture[${index}] ${name}: reusing in-flight result (identical content)`);
158
+ return existing;
159
+ }
160
+ const promise = this.processExrToKtx2(cacheKey, exrBytes, index, name, useCache);
161
+ this.processingPromises.set(cacheKey, promise);
162
+ return promise;
163
+ }
164
+ async processExrToKtx2(cacheKey, exrBytes, index, name, useCache) {
165
+ // Disk cache check
166
+ if (useCache) {
167
+ const cached = tryGetFromCache(cacheKey);
168
+ if (cached && cached.length > 0) {
169
+ this.logger.info(`pmrem:texture[${index}] ${name}: loaded from cache`);
170
+ return cached;
171
+ }
172
+ }
173
+ // 1. Run PMREM on the EXR bytes
174
+ this.logger.info(`pmrem:texture[${index}] ${name}: Running PMREM...`);
175
+ const pmremExrBytes = this.wasmModule.pmrem_exr(exrBytes);
176
+ // 2. Write PMREM EXR to temp file
177
+ const id = uuid();
178
+ const tempExr = join(this.tmpDir, `${id}.pmrem.exr`);
179
+ const tempKtx2 = join(this.tmpDir, `${id}.ktx2`);
180
+ writeFileSync(tempExr, Buffer.from(pmremExrBytes));
181
+ // 3. Encode to KTX2 HDR via basisu
182
+ this.logger.debug(`pmrem:texture[${index}] ${name}: Encoding KTX2 HDR...`);
183
+ const variant = 'hdr4x4'; // 6x6 needs an update in Needle
184
+ await encodeKTX2WithBasisu(this.basisuExe, tempExr, tempKtx2, variant, true);
185
+ if (!existsSync(tempKtx2)) {
186
+ throw new Error(`pmrem:texture[${index}] ${name}: basisu did not produce output`);
187
+ }
188
+ // 4. Read KTX2 result
189
+ const ktx2Bytes = readFileSync(tempKtx2);
190
+ // Store in disk cache
191
+ if (useCache) {
192
+ addToCache(cacheKey, ktx2Bytes);
193
+ }
194
+ return new Uint8Array(ktx2Bytes);
195
+ }
160
196
  async finalize(document) {
161
197
  if (this.processed > 0) {
162
198
  // Register NEEDLE_pmrem extension and mark converted textures
@@ -50,6 +50,8 @@ function sendEvent(eventName, properties, type = 'custom_event') {
50
50
  });
51
51
  req.on('error', () => { });
52
52
  req.on('timeout', () => { req.destroy(); });
53
+ // Allow the process to exit even if the request is still in-flight
54
+ req.on('socket', (socket) => { socket.unref(); });
53
55
  req.end(body);
54
56
  }
55
57
  catch {
@@ -1 +1 @@
1
- export declare const version = "2.15.3";
1
+ export declare const version = "2.15.5";
@@ -1 +1 @@
1
- export const version = "2.15.3";
1
+ export const version = "2.15.5";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needle-tools/gltf-build-pipeline",
3
- "version": "2.15.3",
3
+ "version": "2.15.5",
4
4
  "description": "Pipeline and tools for optimizing gltf files using gltf-transform and compression settings within glTF extensions",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",