PageSourceSearch

https://binomial.biz/ktx2_studio/

html binomial.biz collected 2026-09-25 22:08:59 UTC 235,906 bytes, 5,757 lines download raw bytes

1<!--
2   Simple .KTX2/.DDS encode/transcode testbed.
3   Supports numerous texture formats: ETC1, PVRTC1, BC1-BC7, BC6H, ASTC LDR 4x4-12x12, ASTC HDR 4x4 and 6x6, and various uncompressed fallbacks (16-bit 565, 32bpp, 64bpp).
4   Supports loading .PNG, .JPG, .QOI, .EXR and .HDR files using the basisu library compiled to WASM with emscripten.
5   Also supports .WebP, .AVIF, .GIF, .BMP, .TIFF, .HEIC, .HEIF, and .JXL files via browser-assisted decoding (browser support varies).
6   Thanks to Evan Parker for providing webgl-texture-utils and the original test bed.
7   Script wasm-feature-detect.js was from: https://unpkg.com/wasm-feature-detect/dist/umd/index.js, see https://github.com/GoogleChromeLabs/wasm-feature-detect (Apache 2.0).
8-->
9
10<!doctype html>
11<html>
12<head>
13<meta charset="utf-8">
14<script src="renderer.js"></script>
vendor: 1 bytes, line 14
14
15<script src="wasm-feature-detect.js"></script>
vendor: 1 bytes, line 15
15
16<script src="encode_dto_apply.js"></script>
vendor: 1 bytes, line 16
16
17<script type="text/javascript">
18
19   // The max limit is controlled by the basisu encoder module's linker options in CMakeLists.txt (see "set(LINK_THREADS "-s USE_PTHREADS=1 -s PTHREAD_POOL_SIZE=32 -s ENVIRONMENT=web,worker")")
20   const MAX_WORKER_THREADS = 32;
21   
22   const VERY_LARGE_BLOCK_SIZE_IN_PIXELS = 80; // blocks with >= 80 texels (10x8 or larger) will be automatically deblocked
23
24   var loggingDisabled = true;
25   function setLoggingDisabled(v)
26   {
27      loggingDisabled = !!v;
28   }
29   function log(s)
30   {
31      if (loggingDisabled)
32         return;
33      var panel = document.getElementById('log-panel');
34
35      if (panel.childElementCount >= 750)
36         panel.innerHTML = '';
37
38      var div = document.createElement('div');
39      div.innerHTML = s;
40      panel.appendChild(div);
41   }
42
43   function logClear()
44   {
45      elem('log-panel').innerHTML = '';
46   }
47
48   function logTime(desc, t)
49   {
50      log(t + 'ms ' + desc);
51   }
52
53   function isDef(v)
54   {
55      return typeof v != 'undefined';
56   }
57
58   function elem(id)
59   {
60      return document.getElementById(id);
61   }
62
63   formatTable = function (rows)
64   {
65      var colLengths = [];
66
67      for (var i = 0; i < rows.length; i++)
68      {
69         var row = rows[i];
70         for (var j = 0; j < row.length; j++)
71         {
72            if (colLengths.length <= j) colLengths.push(0);
73            if (colLengths[j] < row[j].length) colLengths[j] = row[j].length;
74         }
75      }
76
77      function formatRow(row)
78      {
79         var parts = [];
80         for (var i = 0; i < colLengths.length; i++)
81         {
82            var s = row.length > i ? row[i] : '';
83            var padding = (new Array(1 + colLengths[i] - s.length)).join(' ');
84            if (s && s[0] >= '0' && s[0] <= '9')
85            {
86               // Right-align numbers.
87               parts.push(padding + s);
88            }
89            else
90            {
91               parts.push(s + padding);
92            }
93         }
94         return parts.join(' | ');
95      }
96
97      var width = 0;
98      for (var i = 0; i < colLengths.length; i++)
99      {
100         width += colLengths[i];
101         // Add another 3 for the separator.
102         if (i != 0) width += 3;
103      }
104
105      var lines = [];
106      lines.push(formatRow(rows[0]));
107      lines.push((new Array(width + 1)).join('-'));
108      for (var i = 1; i < rows.length; i++)
109      {
110         lines.push(formatRow(rows[i]));
111      }
112
113      return lines.join('\n');
114   };
115
116   function loadArrayBufferFromURI(uri, callback, errorCallback)
117   {
118      log('Loading ' + uri + '...');
119
120      var xhr = new XMLHttpRequest();
121      xhr.responseType = "arraybuffer";
122      xhr.open('GET', uri, true);
123
124      xhr.onreadystatechange = function (e)
125      {
126         if (xhr.readyState == 4)  // Request is done
127         {
128            if (xhr.status == 200)
129            {
130               // Success, call the callback with the response
131               callback(xhr.response, uri);
132            }
133            else
134            {
135               // Error, call the errorCallback with the status
136               errorCallback('Failed to load file. Status: ' + xhr.status + ' - ' + xhr.statusText);
137            }
138         }
139      };
140
141      xhr.onerror = function (e)
142      {
143         // Network error or request couldn't be made
144         errorCallback('Network error or request failed.');
145      };
146
147      xhr.send(null);
148   }
149
150   // ASTC format, from:
151   // https://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_astc/
152   COMPRESSED_RGBA_ASTC_4x4_KHR = 0x93B0;
153   COMPRESSED_RGBA_ASTC_5x4_KHR = 0x93B1;
154   COMPRESSED_RGBA_ASTC_5x5_KHR = 0x93B2;
155   COMPRESSED_RGBA_ASTC_6x5_KHR = 0x93B3;
156   COMPRESSED_RGBA_ASTC_6x6_KHR = 0x93B4;
157   COMPRESSED_RGBA_ASTC_8x5_KHR = 0x93B5;
158   COMPRESSED_RGBA_ASTC_8x6_KHR = 0x93B6;
159   COMPRESSED_RGBA_ASTC_10x5_KHR = 0x93B8;
160   COMPRESSED_RGBA_ASTC_10x6_KHR = 0x93B9;
161   COMPRESSED_RGBA_ASTC_8x8_KHR = 0x93B7;  // note the WebGL block order is not the standard ASTC block order
162   COMPRESSED_RGBA_ASTC_10x8_KHR = 0x93BA;
163   COMPRESSED_RGBA_ASTC_10x10_KHR = 0x93BB;
164   COMPRESSED_RGBA_ASTC_12x10_KHR = 0x93BC;
165   COMPRESSED_RGBA_ASTC_12x12_KHR = 0x93BD;
166
167   // ASTC sRGB variants - 0x20 higher
168   COMPRESSED_SRGBA_ASTC_4x4_KHR = 0x93D0;
169   COMPRESSED_SRGBA_ASTC_5x4_KHR = 0x93D1;
170   COMPRESSED_SRGBA_ASTC_5x5_KHR = 0x93D2;
171   COMPRESSED_SRGBA_ASTC_6x5_KHR = 0x93D3;
172   COMPRESSED_SRGBA_ASTC_6x6_KHR = 0x93D4;
173   COMPRESSED_SRGBA_ASTC_8x5_KHR = 0x93D5;
174   COMPRESSED_SRGBA_ASTC_8x6_KHR = 0x93D6;
175   COMPRESSED_SRGBA_ASTC_10x5_KHR = 0x93D8;
176   COMPRESSED_SRGBA_ASTC_10x6_KHR = 0x93D9;
177   COMPRESSED_SRGBA_ASTC_8x8_KHR = 0x93D7;  // note the WebGL block order is not the standard ASTC block order
178   COMPRESSED_SRGBA_ASTC_10x8_KHR = 0x93DA;
179   COMPRESSED_SRGBA_ASTC_10x10_KHR = 0x93DB;
180   COMPRESSED_SRGBA_ASTC_12x10_KHR = 0x93DC;
181   COMPRESSED_SRGBA_ASTC_12x12_KHR = 0x93DD;
182
183   // DXT formats, from:
184   // http://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_s3tc/
185   COMPRESSED_RGB_S3TC_DXT1_EXT = 0x83F0;
186   COMPRESSED_RGBA_S3TC_DXT1_EXT = 0x83F1;
187   COMPRESSED_RGBA_S3TC_DXT3_EXT = 0x83F2;
188   COMPRESSED_RGBA_S3TC_DXT5_EXT = 0x83F3;
189
190   // BC6H/BC7 formats, from:
191   // https://www.khronos.org/registry/webgl/extensions/EXT_texture_compression_bptc/
192   COMPRESSED_RGBA_BPTC_UNORM = 0x8E8C;
193   COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_EXT = 0x8E8F;
194
195   // BC4/BC5 (RGTC) formats, from:
196   // https://www.khronos.org/registry/webgl/extensions/EXT_texture_compression_rgtc/
197   // The transcoder emits unsigned BC4 (1 channel -> red) and unsigned BC5 (2 channels -> red/green).
198   COMPRESSED_RED_RGTC1_EXT = 0x8DBB;        // BC4 unsigned
199   COMPRESSED_RED_GREEN_RGTC2_EXT = 0x8DBD;  // BC5 unsigned
200
201   // ETC format, from:
202   // https://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_etc1/
203   COMPRESSED_RGB_ETC1_WEBGL = 0x8D64;
204
205   // ETC2 / EAC formats, from:
206   // https://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_etc/
207   COMPRESSED_RGBA8_ETC2_EAC = 0x9278;        // ETC2 RGBA (LDR color + alpha), 16 bytes/block
208   COMPRESSED_SRGB8_ALPHA8_ETC2_EAC = 0x9279; // sRGB variant of ETC2 RGBA (kept for completeness; not used)
209   COMPRESSED_R11_EAC = 0x9270;               // EAC R11 (unsigned, 1 channel), 8 bytes/block (no sRGB variant)
210   COMPRESSED_RG11_EAC = 0x9272;              // EAC RG11 (unsigned, 2 channels), 16 bytes/block (no sRGB variant)
211
212   // PVRTC format, from:
213   // https://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_pvrtc/
214   COMPRESSED_RGB_PVRTC_4BPPV1_IMG = 0x8C00;
215   COMPRESSED_RGBA_PVRTC_4BPPV1_IMG = 0x8C02;
216
217   // Half float RGBA, from:
218   // https://registry.khronos.org/webgl/extensions/OES_texture_half_float/
219   HALF_FLOAT_OES = 0x8D61;
220
221   // Same as the Module.transcoder_texture_format enum
222   BASIS_FORMAT =
223   {
224      cTFETC1: 0,
225      cTFETC2: 1,
226      cTFBC1: 2,
227      cTFBC3: 3,
228      cTFBC4: 4,
229      cTFBC5: 5,
230      cTFBC7: 6,
231      cTFPVRTC1_4_RGB: 8,
232      cTFPVRTC1_4_RGBA: 9,
233      cTFASTC_4x4: 10,
234      cTFATC_RGB: 11,
235      cTFATC_RGBA_INTERPOLATED_ALPHA: 12,
236      cTFRGBA32: 13,
237      cTFRGB565: 14,
238      cTFBGR565: 15,
239      cTFRGBA4444: 16,
240      cTFFXT1_RGB: 17,
241      cTFPVRTC2_4_RGB: 18,
242      cTFPVRTC2_4_RGBA: 19,
243      cTFETC2_EAC_R11: 20,
244      cTFETC2_EAC_RG11: 21,
245      cTFBC6H: 22,
246      cTFASTC_HDR_4x4_RGBA: 23,
247      cTFRGB_HALF: 24,
248      cTFRGBA_HALF: 25,
249      cTFRGB_9E5: 26,
250      cTFASTC_HDR_6x6_RGBA: 27,
251      cTFASTC_LDR_5x4_RGBA: 28,
252      cTFASTC_LDR_5x5_RGBA: 29,
253      cTFASTC_LDR_6x5_RGBA: 30,
254      cTFASTC_LDR_6x6_RGBA: 31,
255      cTFASTC_LDR_8x5_RGBA: 32,
256      cTFASTC_LDR_8x6_RGBA: 33,
257      cTFASTC_LDR_10x5_RGBA: 34,
258      cTFASTC_LDR_10x6_RGBA: 35,
259      cTFASTC_LDR_8x8_RGBA: 36,
260      cTFASTC_LDR_10x8_RGBA: 37,
261      cTFASTC_LDR_10x10_RGBA: 38,
262      cTFASTC_LDR_12x10_RGBA: 39,
263      cTFASTC_LDR_12x12_RGBA: 40
264   };
265
266   BASIS_FORMAT_NAMES = {};
267   for (var name in BASIS_FORMAT)
268   {
269      BASIS_FORMAT_NAMES[BASIS_FORMAT[name]] = name;
270   }
271
272   DXT_FORMAT_MAP = {};
273   DXT_FORMAT_MAP[BASIS_FORMAT.cTFBC1] = COMPRESSED_RGB_S3TC_DXT1_EXT;
274   DXT_FORMAT_MAP[BASIS_FORMAT.cTFBC3] = COMPRESSED_RGBA_S3TC_DXT5_EXT;
275   DXT_FORMAT_MAP[BASIS_FORMAT.cTFBC7] = COMPRESSED_RGBA_BPTC_UNORM;
276   DXT_FORMAT_MAP[BASIS_FORMAT.cTFBC4] = COMPRESSED_RED_RGTC1_EXT;
277   DXT_FORMAT_MAP[BASIS_FORMAT.cTFBC5] = COMPRESSED_RED_GREEN_RGTC2_EXT;
278
279   var astcSupported = false, etcSupported = false, dxtSupported = false, bc7Supported = false, bc6hSupported = false, astcHDRSupported = false,
280      rgbaHalfSupported = false, halfFloatWebGLFormat = 0, pvrtcSupported = false;
281
282   // BC4/BC5 (RGTC) support. Currently consumed only by the DDS viewer, but kept as
283   // general shared infrastructure (DXT_FORMAT_MAP entries, capability detection, the
284   // disable toggle) so a future KTX2 BC4/BC5 transcode path can reuse it directly.
285   // A single combined flag/toggle keeps the new UI minimal.
286   var rgtcSupported = false, rgtcDisabled = false;
287
288   // ETC2 (RGBA) + EAC (R11/RG11) support, from the WEBGL_compressed_texture_etc extension (distinct
289   // from the ETC1-only WEBGL_compressed_texture_etc1 flag above). The *Disabled flags drive the
290   // per-format enable/disable testing toggles (same pattern as the other formats).
291   var etc2Supported = false, eacSupported = false, etc2Disabled = false, eacDisabled = false;
292
293   var astcDisabled = false, etcDisabled = false, dxtDisabled = false, bc7Disabled = false, bc6hDisabled = false, astcHDRDisabled = false, rgbaHalfDisabled = false, pvrtcDisabled = false;
294
295   var drawMode = 0;
296   var drawScale = 1.0;
297   var ldrHDRUpconversionScale = 1.0;
298   var linearToSRGBFlag = false;
299   var drawUseBilinearFiltering = false;
300   var displayZoom = 1.0;
301
302   var tex, width, height, is_hdr, layers, levels, faces, tex_has_alpha, tex_is_srgb;
303   var alignedWidth, alignedHeight, format, displayWidth, displayHeight;
304   var tex_block_width = 0, tex_block_height = 0, tex_deblocking_filter_id = 0;
305   var used_gpu_deblocking_flag = false, used_cpu_deblocking_flag = false;
306
307   var curLoadedImageData = null, curLoadedImageURI = null;
308   var curLoadedBrowserRGBA = null;  // non-null when curLoadedImageData came from a browser-decoded format
309   var curLoadedKTX2Data = null, curLoadedKTX2URI = null;
310
311   // DDS source support (new, additive). A .DDS is a pre-compressed source we only
312   // transcode+view (no encode, no export yet). The viewer is tri-state: nothing
313   // loaded / a KTX2 loaded / a DDS loaded. We keep a SEPARATE pair of globals for
314   // the DDS bytes, and on a DDS load we null curLoadedKTX2Data so every existing
315   // KTX2-guarded code path (re-transcodes, exports) naturally short-circuits and
316   // the KTX2 logic is never handed DDS data. The KTX2 code paths are left as-is.
317   var curLoadedDDSData = null, curLoadedDDSURI = null;
318
319   // Returns which kind of compressed source is currently loaded for viewing:
320   // 'ktx2', 'dds', or 'none'. Use this (rather than poking curLoadedKTX2Data
321   // directly) anywhere the UI needs to branch on, or fetch metadata about, the
322   // currently loaded file.
323   function getLoadedSourceKind()
324   {
325      if (curLoadedDDSData != null) return 'dds';
326      if (curLoadedKTX2Data != null) return 'ktx2';
327      return 'none';
328   }
329
330   // Update the small "Loaded: ..." labels under the source-image / .ktx2
331   // inputs. Called from every load site after the corresponding URI global
332   // is assigned. Safe to call before the DOM elements exist (no-op in that
333   // case) — the labels also start out reading "(none)" from the markup.
334   function updateLoadedSourceDisplay()
335   {
336      const span = document.getElementById('loaded-source-uri-display');
337      const btn  = document.getElementById('copy-loaded-source-uri-btn');
338      if (!span) return;
339      const uri = curLoadedImageURI;
340      if (uri)
341      {
342         span.textContent = uri;
343         span.title = uri;
344         if (btn) btn.disabled = false;
345      }
346      else
347      {
348         span.textContent = '(none)';
349         span.title = '';
350         if (btn) btn.disabled = true;
351      }
352   }
353   function updateLoadedFormatDisplay()
354   {
355      const span = document.getElementById('loaded-ktx2-uri-display');
356      const btn  = document.getElementById('copy-loaded-ktx2-uri-btn');
357      if (!span) return;
358      // This label shows the currently-loaded compressed texture. It's source-aware:
359      // a loaded .DDS is surfaced here too (with a suffix), since DDS has no separate label.
360      var uri = curLoadedKTX2URI;
361      if (getLoadedSourceKind() === 'dds' && curLoadedDDSURI)
362         uri = curLoadedDDSURI + ' (.DDS)';
363      if (uri)
364      {
365         span.textContent = uri;
366         span.title = uri;
367         if (btn) btn.disabled = false;
368      }
369      else
370      {
371         span.textContent = '(none)';
372         span.title = '';
373         if (btn) btn.disabled = true;
374      }
375   }
376
377   var g_transcodingTime = 0;
378   // DEV: when true, the main-thread "Encode (on Main Thread)" path ALSO runs the DTO
379   // encode and byte-compares it (so it encodes twice). Off by default. Long-term that
380   // path will switch to the DTO technique outright; this is just the verification.
381   var g_enableDTOValidation = false;
382   var g_lastEncodeTime = 0;
383   var g_lastEncodeMip0RGBAPSNR = 0;
384
385   function redraw()
386   {
387      if (!width)
388         return;
389
390      // Keep the WebGL buffer at native resolution; use CSS to zoom the display.
391      var canvas = elem('canvas');
392      if (canvas.width !== displayWidth || canvas.height !== displayHeight)
393      {
394         canvas.width = displayWidth;
395         canvas.height = displayHeight;
396      }
397
398      var cssW = Math.round(displayWidth * displayZoom);
399      var cssH = Math.round(displayHeight * displayZoom);
400      canvas.style.width  = cssW + 'px';
401      canvas.style.height = cssH + 'px';
402
403      // Match CSS upscaling to the bilinear filtering setting.
404      canvas.style.imageRendering = drawUseBilinearFiltering ? 'auto' : 'pixelated';
405      
406      // GPU shader deblocking control
407      const deblocking_filter_select = document.getElementById("deblocking-filter-select");
408      const deblocking_mode = Number(deblocking_filter_select.value);
409
410      var should_gpu_deblock_flag = false;
411      
412      if (deblocking_mode == 1) // auto (GPU)
413      {
414         // By default only apply the filter if the KTX2 file enabled it
415         if (tex_deblocking_filter_id != 0)
416            should_gpu_deblock_flag = true;
417      }
418      else if (deblocking_mode == 2) // Forced - Use GPU
419      {
420         // Decide if we should apply the filter or not
421         if (elem('xuastc_ldr_force_deblocking_on_all_block_sizes').checked)
422            should_gpu_deblock_flag = true;
423         else if ((tex_block_width * tex_block_height) >= VERY_LARGE_BLOCK_SIZE_IN_PIXELS) // check for very large block sizes
424            should_gpu_deblock_flag = true;
425      }
426      
427      if ((should_gpu_deblock_flag) &&
428          (tex_block_width >= 4) && (tex_block_height >= 4))
429      {
430          var show_edge_weights = elem('xuastc_ldr_gpu_deblocking_show_edge_weights').checked;
431          
432          // Draw with deblocking filter
433          renderer.drawTextureDeblocked(tex, displayWidth, displayHeight, drawMode, drawScale * ldrHDRUpconversionScale, linearToSRGBFlag, false, tex_block_width, tex_block_height, show_edge_weights);      
434          
435          used_gpu_deblocking_flag = true;
436      }
437      else
438      {
439          // Draw normally, no filtering
440          renderer.drawTexture(tex, displayWidth, displayHeight, drawMode, drawScale * ldrHDRUpconversionScale, linearToSRGBFlag, false);
441          
442          used_gpu_deblocking_flag = false;
443      }
444      
445      const deblockingIndicator = document.getElementById("deblocking-indicator");
446
447      // Build the string first, then only touch the DOM if it actually changed.
448      // Writing innerText forces layout work even when the value is identical,
449      // and this runs on every draw, so guarding the write matters here.
450      let deblockingText = "Deblocking Filter:";
451
452      if (used_gpu_deblocking_flag)
453        deblockingText += " GPU Shader ";
454
455      if (used_cpu_deblocking_flag)
456        deblockingText += " CPU (during transcode if available)";
457
458      if (!used_gpu_deblocking_flag && !used_cpu_deblocking_flag)
459        deblockingText += " Disabled";
460
461      if (deblockingIndicator.innerText !== deblockingText)
462        deblockingIndicator.innerText = deblockingText;
463      
464   }
465
466   function dumpKTX2FileDesc(ktx2File)
467   {
468      log('------');
469
470      log('Width: ' + ktx2File.getWidth());
471      log('Height: ' + ktx2File.getHeight());
472
473      log('BlockWidth: ' + ktx2File.getBlockWidth());
474      log('BlockHeight: ' + ktx2File.getBlockHeight());
475      log('BasisTexFormat: ' + ktx2File.getBasisTexFormat());
476
477      log('isSRGB: ' + ktx2File.isSRGB());
478      log('Has alpha: ' + ktx2File.getHasAlpha());
479
480      log('Faces: ' + ktx2File.getFaces());
481      log('Layers: ' + ktx2File.getLayers());
482      log('Levels: ' + ktx2File.getLevels());
483
484      log('IsLDR: ' + ktx2File.isLDR());
485      log('IsHDR: ' + ktx2File.isHDR());
486      log('isETC1S: ' + ktx2File.isETC1S());
487      log('isUASTC: ' + ktx2File.isUASTC());
488      log('IsHDR4x4: ' + ktx2File.isHDR4x4());
489      log('IsHDR6x6: ' + ktx2File.isHDR6x6());
490      log('isASTC_LDR: ' + ktx2File.isASTC_LDR());
491      log('isXUASTC_LDR: ' + ktx2File.isXUASTC_LDR());
492
493      log('Total Keys: ' + ktx2File.getTotalKeys());
494
495      log('DFD Size: ' + ktx2File.getDFDSize());
496      log('DFD Color Model: ' + ktx2File.getDFDColorModel());
497      log('DFD Color Primaries: ' + ktx2File.getDFDColorPrimaries());
498      log('DFD Transfer Function: ' + ktx2File.getDFDTransferFunc());
499      log('DFD Flags: ' + ktx2File.getDFDFlags());
500      log('DFD Total Samples: ' + ktx2File.getDFDTotalSamples());
501      log('DFD Channel0: ' + ktx2File.getDFDChannelID0());
502      log('DFD Channel1: ' + ktx2File.getDFDChannelID1());
503
504      log('Is Video: ' + ktx2File.isVideo());
505
506      var dfdSize = ktx2File.getDFDSize();
507
508      var dfdData = new Uint8Array(dfdSize);
509      ktx2File.getDFD(dfdData);
510
511      log('DFD bytes:' + dfdData.toString());
512      log('--');
513
514      log('--');
515      log('Key values:');
516      var key_index;
517
518      for (key_index = 0; key_index < ktx2File.getTotalKeys(); key_index++)
519      {
520         var key_name = ktx2File.getKey(key_index);
521         log('Key ' + key_index + ': "' + key_name + '"');
522
523         var valSize = ktx2File.getKeyValueSize(key_name);
524
525         if (valSize != 0)
526         {
527            var val_data = new Uint8Array(valSize);
528            var status = ktx2File.getKeyValue(key_name, val_data);
529
530            if (!status)
531               log('getKeyValue() failed');
532            else
533            {
534               log('value size: ' + val_data.length);
535               var i, str = "";
536
537               for (i = 0; i < val_data.length; i++)
538               {
539                  var c = val_data[i];
540                  str = str + String.fromCharCode(c);
541               }
542
543               log(str);
544            }
545
546         }
547         else
548            log('<empty value>');
549      }
550
551      log('--');
552      log('Image level information:');
553      var level_index;
554
555      var total_texels = 0;
556
557      for (level_index = 0; level_index < ktx2File.getLevels(); level_index++)
558      {
559         var layer_index;
560         for (layer_index = 0; layer_index < Math.max(1, ktx2File.getLayers()); layer_index++)
561         {
562            var face_index;
563            for (face_index = 0; face_index < ktx2File.getFaces(); face_index++)
564            {
565               var imageLevelInfo = ktx2File.getImageLevelInfo(level_index, layer_index, face_index);
566
567               log('level: ' + level_index + ' layer: ' + layer_index + ' face: ' + face_index);
568
569               log('orig_width: ' + imageLevelInfo.origWidth);
570               log('orig_height: ' + imageLevelInfo.origHeight);
571               log('width: ' + imageLevelInfo.width);
572               log('height: ' + imageLevelInfo.height);
573               log('numBlocksX: ' + imageLevelInfo.numBlocksX);
574               log('numBlocksY: ' + imageLevelInfo.numBlocksY);
575               log('blockWidth: ' + imageLevelInfo.blockWidth);
576               log('blockHeight: ' + imageLevelInfo.blockHeight);
577               log('totalBlocks: ' + imageLevelInfo.totalBlocks);
578               log('alphaFlag: ' + imageLevelInfo.alphaFlag);
579               log('iframeFlag: ' + imageLevelInfo.iframeFlag);
580
581               if (ktx2File.isETC1S())
582                  log('ETC1S image desc image flags: ' + ktx2File.getETC1SImageDescImageFlags(level_index, layer_index, face_index));
583
584               log('--');
585
586               total_texels += imageLevelInfo.origWidth * imageLevelInfo.origHeight;
587            }
588         }
589      }
590      log('--');
591      log('KTX2 header:');
592
593      var hdr = ktx2File.getHeader();
594
595      log('vkFormat: ' + hdr.vkFormat);
596      log('typeSize: ' + hdr.typeSize);
597      log('pixelWidth: ' + hdr.pixelWidth);
598      log('pixelHeight: ' + hdr.pixelHeight);
599      log('pixelDepth: ' + hdr.pixelDepth);
600      log('layerCount: ' + hdr.layerCount);
601      log('faceCount: ' + hdr.faceCount);
602      log('levelCount: ' + hdr.levelCount);
603      log('superCompressionScheme: ' + hdr.supercompressionScheme);
604      log('dfdByteOffset: ' + hdr.dfdByteOffset);
605      log('dfdByteLength: ' + hdr.dfdByteLength);
606      log('kvdByteOffset: ' + hdr.kvdByteOffset);
607      log('kvdByteLength: ' + hdr.kvdByteLength);
608      log('sgdByteOffset: ' + hdr.sgdByteOffset);
609      log('sgdByteLength: ' + hdr.sgdByteLength);
610
611      log('------');
612
613      return total_texels;
614   }
615
616   function setCanvasSize(width, height)
617   {
618      var canvas = elem('canvas');
619
620      canvas.width = width;
621      canvas.height = height;
622
623      canvas.style.width  = width + 'px';
624      canvas.style.height = height + 'px';
625   }
626
627   function transcodeTexture(data, uri)
628   {
629      updateErrorLine("");
630
631      log('------');
632      log('transcodeTexture(): Done loading .KTX2 file, decoded header:');
633
634      const { KTX2File, initializeBasis, encodeBasisTexture } = Module;
635
636      resetDrawSettings();
637
638      initializeBasis();
639      
640      used_cpu_deblocking_flag = false;
641
642      const ktx2File = new KTX2File(new Uint8Array(data));
643
644      if (!ktx2File.isValid())
645      {
646         updateErrorLine('Invalid or unsupported .ktx2 file');
647         console.warn('Invalid or unsupported .ktx2 file');
648         ktx2File.close();
649         ktx2File.delete();
650         return;
651      }
652
653      var baseWidth = ktx2File.getWidth();
654      var baseHeight = ktx2File.getHeight();
655      var srcBlockWidth = ktx2File.getBlockWidth();
656      var srcBlockHeight = ktx2File.getBlockHeight();
657
658      var basisTexFormat = ktx2File.getBasisTexFormat();
659      var is_uastc = ktx2File.isUASTC();
660      var is_astc_ldr = ktx2File.isASTC_LDR();
661      var is_xuastc_ldr = ktx2File.isXUASTC_LDR();
662      var is_xubc7 = ktx2File.isXUBC7();
663
664      is_hdr = ktx2File.isHDR();
665
666      layers = ktx2File.getLayers();
667      levels = ktx2File.getLevels();
668      faces = ktx2File.getFaces();
669      tex_has_alpha = ktx2File.getHasAlpha();
670      tex_is_srgb = ktx2File.isSRGB();
671
672      updateMipmapSliderRange(levels);
673
674      // Read the desired mipmap level from the slider, validate, and clamp if needed.
675      var selectedLevel = parseInt(document.getElementById('mipmap-level-slider').value, 10);
676      if (isNaN(selectedLevel) || selectedLevel < 0 || selectedLevel >= levels)
677      {
678         selectedLevel = 0;
679         document.getElementById('mipmap-level-slider').value = 0;
680         document.getElementById('mipmap-level-value').textContent = '0';
681      }
682
683      updateCubemapFaceRange(faces);
684
685      // Read the desired cubemap face from the slider, validate, and clamp if needed.
686      var selectedFace = parseInt(document.getElementById('cubemap-face-slider').value, 10);
687      if (isNaN(selectedFace) || selectedFace < 0 || selectedFace >= faces)
688      {
689         selectedFace = 0;
690         document.getElementById('cubemap-face-slider').value = 0;
691         document.getElementById('cubemap-face-value').textContent = '0';
692      }
693
694      // For array textures, layers==0 means non-arrayed (treat as 1 layer at index 0).
695      var effectiveLayers = Math.max(1, layers);
696      updateArrayLayerRange(effectiveLayers);
697
698      // Read the desired array layer from the slider, validate, and clamp if needed.
699      var selectedLayer = parseInt(document.getElementById('array-layer-slider').value, 10);
700      if (isNaN(selectedLayer) || selectedLayer < 0 || selectedLayer >= effectiveLayers)
701      {
702         selectedLayer = 0;
703         document.getElementById('array-layer-slider').value = 0;
704         document.getElementById('array-layer-value').textContent = '0';
705         document.getElementById('array-layer-input').value = 0;
706      }
707
708      // Set width/height to the selected mip level's original (unpadded) dimensions.
709      // This way all downstream code (alignment, transcoding, texture creation) works unchanged.
710      var imageLevelInfo = ktx2File.getImageLevelInfo(selectedLevel, selectedLayer, selectedFace);
711      width = imageLevelInfo.origWidth;
712      height = imageLevelInfo.origHeight;
713    
714      // Retrieve the file's block dimensions (used for deblocking)  
715      tex_block_width = imageLevelInfo.blockWidth;
716      tex_block_height = imageLevelInfo.blockHeight;
717            
718      tex_deblocking_filter_id = ktx2File.getDeblockingFilterIndex();
719      
720      // If a HDR KTX2 file was upconverted from LDR/SDR content by us, it'll have a key indicating the Nit scale that was appplied.
721      // We can use that to make viewing the file work out of the box.
722      var ldrHDRUpconversionNitMultiplier = ktx2File.getLDRHDRUpconversionNitMultiplier();
723
724      if (ldrHDRUpconversionNitMultiplier > 0.0)
725         ldrHDRUpconversionScale = 1.0 / ldrHDRUpconversionNitMultiplier;
726      else
727         ldrHDRUpconversionScale = 1.0;
728
729      var dstBlockWidth = 4, dstBlockHeight = 4;
730      
731      // PVRTC1 requires both width/height be a power of 2 (but not square is okay)      
732      var is_pow2 = ((width & (width - 1)) == 0) && ((height & (height - 1)) == 0);
733
734      if ((!is_hdr) && (!is_pow2))
735      {
736         if ((pvrtcSupported) && (!pvrtcDisabled))
737         {
738            updateErrorLine("Note: PVRTC is available but the texture's dimensions are not both a power of 2.");
739            log("Note: PVRTC is available but the texture's dimensions are not both a power of 2.");
740         }
741      }
742
743      if (!width || !height || !levels)
744      {
745         updateErrorLine('Invalid .ktx2 file');
746         console.warn('Invalid .ktx2 file');
747         ktx2File.close();
748         ktx2File.delete();
749         return;
750      }
751          
752      // Decide which texture format to transcode to.
753      // Note: If the file is UASTC LDR, the preferred formats are ASTC/BC7. For UASTC HDR, ASTC HDR/BC6H.
754      // If the file is ETC1S and doesn't have alpha, the preferred formats are ETC1 and BC1. For alpha, the preferred formats are ETC2, BC3 or BC7.
755
756      var formatString = 'UNKNOWN';
757      if (is_hdr)
758      {
759         if ((bc6hSupported) && (!bc6hDisabled))
760         {
761            formatString = 'BC6H';
762            format = BASIS_FORMAT.cTFBC6H;
763         }
764         else if ((astcHDRSupported) && (!astcHDRDisabled))
765         {
766            if (basisTexFormat == Module.basis_tex_format.cUASTC_HDR_4x4.value)
767            {
768               formatString = 'ASTC HDR 4x4';
769
770               format = BASIS_FORMAT.cTFASTC_HDR_4x4_RGBA;
771            }
772            else
773            {
774               formatString = 'ASTC HDR 6x6';
775
776               format = BASIS_FORMAT.cTFASTC_HDR_6x6_RGBA;
777
778               dstBlockWidth = 6;
779               dstBlockHeight = 6;
780            }
781         }
782         else if ((rgbaHalfSupported) && (!rgbaHalfDisabled))
783         {
784            formatString = 'RGBA_HALF';
785            format = BASIS_FORMAT.cTFRGBA_HALF;
786         }
787         else
788         {
789            formatString = '32-bit RGBA';
790            format = BASIS_FORMAT.cTFRGBA_HALF;
791
792           log('Warning: Falling back to decoding texture to uncompressed 32-bit RGBA');
793         }
794      }
795      else if ((astcSupported) && (!astcDisabled))
796      {
797         switch (basisTexFormat)
798         {
799            case Module.basis_tex_format.cASTC_LDR_4x4.value:    format = BASIS_FORMAT.cTFASTC_4x4; formatString = 'ASTC LDR 4x4'; dstBlockWidth = 4; dstBlockHeight = 4; break;
800            case Module.basis_tex_format.cASTC_LDR_5x4.value:    format = BASIS_FORMAT.cTFASTC_LDR_5x4_RGBA; formatString = 'ASTC LDR 5x4'; dstBlockWidth = 5; dstBlockHeight = 4; break;
801            case Module.basis_tex_format.cASTC_LDR_5x5.value:    format = BASIS_FORMAT.cTFASTC_LDR_5x5_RGBA; formatString = 'ASTC LDR 5x5'; dstBlockWidth = 5; dstBlockHeight = 5; break;
802            case Module.basis_tex_format.cASTC_LDR_6x5.value:    format = BASIS_FORMAT.cTFASTC_LDR_6x5_RGBA; formatString = 'ASTC LDR 6x5'; dstBlockWidth = 6; dstBlockHeight = 5; break;
803            case Module.basis_tex_format.cASTC_LDR_6x6.value:    format = BASIS_FORMAT.cTFASTC_LDR_6x6_RGBA; formatString = 'ASTC LDR 6x6'; dstBlockWidth = 6; dstBlockHeight = 6; break;
804            case Module.basis_tex_format.cASTC_LDR_8x5.value:    format = BASIS_FORMAT.cTFASTC_LDR_8x5_RGBA; formatString = 'ASTC LDR 8x5'; dstBlockWidth = 8; dstBlockHeight = 5; break;
805            case Module.basis_tex_format.cASTC_LDR_8x6.value:    format = BASIS_FORMAT.cTFASTC_LDR_8x6_RGBA; formatString = 'ASTC LDR 8x6'; dstBlockWidth = 8; dstBlockHeight = 6; break;
806            case Module.basis_tex_format.cASTC_LDR_10x5.value:   format = BASIS_FORMAT.cTFASTC_LDR_10x5_RGBA; formatString = 'ASTC LDR 10x5'; dstBlockWidth = 10; dstBlockHeight = 5; break;
807            case Module.basis_tex_format.cASTC_LDR_10x6.value:   format = BASIS_FORMAT.cTFASTC_LDR_10x6_RGBA; formatString = 'ASTC LDR 10x6'; dstBlockWidth = 10; dstBlockHeight = 6; break;
808            case Module.basis_tex_format.cASTC_LDR_10x8.value:   format = BASIS_FORMAT.cTFASTC_LDR_10x8_RGBA; formatString = 'ASTC LDR 10x8'; dstBlockWidth = 10; dstBlockHeight = 8; break;
809            case Module.basis_tex_format.cASTC_LDR_8x8.value:    format = BASIS_FORMAT.cTFASTC_LDR_8x8_RGBA; formatString = 'ASTC LDR 8x8'; dstBlockWidth = 8; dstBlockHeight = 8; break;
810            case Module.basis_tex_format.cASTC_LDR_10x10.value:  format = BASIS_FORMAT.cTFASTC_LDR_10x10_RGBA; formatString = 'ASTC LDR 10x10'; dstBlockWidth = 10; dstBlockHeight = 10; break;
811            case Module.basis_tex_format.cASTC_LDR_12x10.value:  format = BASIS_FORMAT.cTFASTC_LDR_12x10_RGBA; formatString = 'ASTC LDR 12x10'; dstBlockWidth = 12; dstBlockHeight = 10; break;
812            case Module.basis_tex_format.cASTC_LDR_12x12.value:  format = BASIS_FORMAT.cTFASTC_LDR_12x12_RGBA; formatString = 'ASTC LDR 12x12'; dstBlockWidth = 12; dstBlockHeight = 12; break;
813
814            case Module.basis_tex_format.cXUASTC_LDR_4x4.value:  format = BASIS_FORMAT.cTFASTC_4x4; formatString = 'ASTC LDR 4x4'; dstBlockWidth = 4; dstBlockHeight = 4; break;
815            case Module.basis_tex_format.cXUASTC_LDR_5x4.value:  format = BASIS_FORMAT.cTFASTC_LDR_5x4_RGBA; formatString = 'ASTC LDR 5x4'; dstBlockWidth = 5; dstBlockHeight = 4; break;
816            case Module.basis_tex_format.cXUASTC_LDR_5x5.value:  format = BASIS_FORMAT.cTFASTC_LDR_5x5_RGBA; formatString = 'ASTC LDR 5x5'; dstBlockWidth = 5; dstBlockHeight = 5; break;
817            case Module.basis_tex_format.cXUASTC_LDR_6x5.value:  format = BASIS_FORMAT.cTFASTC_LDR_6x5_RGBA; formatString = 'ASTC LDR 6x5'; dstBlockWidth = 6; dstBlockHeight = 5; break;
818            case Module.basis_tex_format.cXUASTC_LDR_6x6.value:  format = BASIS_FORMAT.cTFASTC_LDR_6x6_RGBA; formatString = 'ASTC LDR 6x6'; dstBlockWidth = 6; dstBlockHeight = 6; break;
819            case Module.basis_tex_format.cXUASTC_LDR_8x5.value:  format = BASIS_FORMAT.cTFASTC_LDR_8x5_RGBA; formatString = 'ASTC LDR 8x5'; dstBlockWidth = 8; dstBlockHeight = 5; break;
820            case Module.basis_tex_format.cXUASTC_LDR_8x6.value:  format = BASIS_FORMAT.cTFASTC_LDR_8x6_RGBA; formatString = 'ASTC LDR 8x6'; dstBlockWidth = 8; dstBlockHeight = 6; break;
821            case Module.basis_tex_format.cXUASTC_LDR_10x5.value: format = BASIS_FORMAT.cTFASTC_LDR_10x5_RGBA; formatString = 'ASTC LDR 10x5'; dstBlockWidth = 10; dstBlockHeight = 5; break;
822            case Module.basis_tex_format.cXUASTC_LDR_10x6.value: format = BASIS_FORMAT.cTFASTC_LDR_10x6_RGBA; formatString = 'ASTC LDR 10x6'; dstBlockWidth = 10; dstBlockHeight = 6; break;
823            case Module.basis_tex_format.cXUASTC_LDR_10x8.value: format = BASIS_FORMAT.cTFASTC_LDR_10x8_RGBA; formatString = 'ASTC LDR 10x8'; dstBlockWidth = 10; dstBlockHeight = 8; break;
824            case Module.basis_tex_format.cXUASTC_LDR_8x8.value:  format = BASIS_FORMAT.cTFASTC_LDR_8x8_RGBA; formatString = 'ASTC LDR 8x8'; dstBlockWidth = 8; dstBlockHeight = 8; break;
825            case Module.basis_tex_format.cXUASTC_LDR_10x10.value: format = BASIS_FORMAT.cTFASTC_LDR_10x10_RGBA; formatString = 'ASTC LDR 10x10'; dstBlockWidth = 10; dstBlockHeight = 10; break;
826            case Module.basis_tex_format.cXUASTC_LDR_12x10.value: format = BASIS_FORMAT.cTFASTC_LDR_12x10_RGBA; formatString = 'ASTC LDR 12x10'; dstBlockWidth = 12; dstBlockHeight = 10; break;
827            case Module.basis_tex_format.cXUASTC_LDR_12x12.value: format = BASIS_FORMAT.cTFASTC_LDR_12x12_RGBA; formatString = 'ASTC LDR 12x12'; dstBlockWidth = 12; dstBlockHeight = 12; break;
828
829            default:
830               format = BASIS_FORMAT.cTFASTC_4x4;
831               formatString = 'ASTC LDR 4x4';
832               break;
833         }
834      }
835      else if ((bc7Supported) && (!bc7Disabled))
836      {
837         formatString = 'BC7';
838         format = BASIS_FORMAT.cTFBC7;
839      }
840      else if ((dxtSupported) && (!dxtDisabled))
841      {
842         if (tex_has_alpha)
843         {
844            formatString = 'BC3';
845            format = BASIS_FORMAT.cTFBC3;
846         }
847         else
848         {
849            formatString = 'BC1';
850            format = BASIS_FORMAT.cTFBC1;
851         }
852      }
853      // ETC2 RGBA: alpha-capable color codec, higher quality than PVRTC1 (and ETC1), so it ranks above
854      // both. Targets ETC-tier GPUs lacking ASTC/BC; ETC1 below is RGB-only and would drop alpha.
855      else if ((etc2Supported) && (!etc2Disabled))
856      {
857         formatString = 'ETC2 RGBA';
858         format = BASIS_FORMAT.cTFETC2;
859      }
860      else if ((pvrtcSupported) && (!pvrtcDisabled) && (is_pow2))
861      {
862         if (tex_has_alpha)
863         {
864            formatString = 'PVRTC1_RGBA';
865            format = BASIS_FORMAT.cTFPVRTC1_4_RGBA;
866         }
867         else
868         {
869            formatString = 'PVRTC1_RGB';
870            format = BASIS_FORMAT.cTFPVRTC1_4_RGB;
871         }
872      }
873      else if ((etcSupported) && (!etcDisabled))
874      {
875         formatString = 'ETC1';
876         format = BASIS_FORMAT.cTFETC1;
877      }
878      else
879      {
880         formatString = 'RGBA32';
881         format = BASIS_FORMAT.cTFRGBA32;
882         log('Warning: Falling back to decoding texture data to uncompressed 32-bit RGBA');
883      }
884
885      var descString = formatString;
886
887      if (is_hdr)
888      {
889         if (basisTexFormat == Module.basis_tex_format.cUASTC_HDR_4x4.value)
890            descString += ' from UASTC HDR 4x4';
891         else if (basisTexFormat == Module.basis_tex_format.cASTC_HDR_6x6.value)
892            descString += ' from ASTC HDR 6x6';
893         else
894            descString += ' from UASTC HDR 6x6 Intermediate';
895      }
896      else if (is_astc_ldr)
897      {
898         descString += ' from ASTC LDR ';
899
900         switch (basisTexFormat)
901         {
902            case Module.basis_tex_format.cASTC_LDR_4x4.value:  descString += '4x4'; break;
903            case Module.basis_tex_format.cASTC_LDR_5x4.value:  descString += '5x4'; break;
904            case Module.basis_tex_format.cASTC_LDR_5x5.value:  descString += '5x5'; break;
905            case Module.basis_tex_format.cASTC_LDR_6x5.value:  descString += '6x5'; break;
906            case Module.basis_tex_format.cASTC_LDR_6x6.value:  descString += '6x6'; break;
907            case Module.basis_tex_format.cASTC_LDR_8x5.value:  descString += '8x5'; break;
908            case Module.basis_tex_format.cASTC_LDR_8x6.value:  descString += '8x6'; break;
909            case Module.basis_tex_format.cASTC_LDR_10x5.value: descString += '10x5'; break;
910            case Module.basis_tex_format.cASTC_LDR_10x6.value: descString += '10x6'; break;
911            case Module.basis_tex_format.cASTC_LDR_10x8.value: descString += '10x8'; break;
912            case Module.basis_tex_format.cASTC_LDR_8x8.value:  descString += '8x8'; break;
913            case Module.basis_tex_format.cASTC_LDR_10x10.value: descString += '10x10'; break;
914            case Module.basis_tex_format.cASTC_LDR_12x10.value: descString += '12x10'; break;
915            case Module.basis_tex_format.cASTC_LDR_12x12.value: descString += '12x12'; break;
916            default:
917             break;
918        }
919      }
920      else if (is_xubc7)
921      {
922         descString += ' from XUBC7';
923      }
924      else if (is_xuastc_ldr)
925      {
926         descString += ' from XUASTC LDR ';
927
928         switch (basisTexFormat)
929         {
930            case Module.basis_tex_format.cXUASTC_LDR_4x4.value:  descString += '4x4'; break;
931            case Module.basis_tex_format.cXUASTC_LDR_5x4.value:  descString += '5x4'; break;
932            case Module.basis_tex_format.cXUASTC_LDR_5x5.value:  descString += '5x5'; break;
933            case Module.basis_tex_format.cXUASTC_LDR_6x5.value:  descString += '6x5'; break;
934            case Module.basis_tex_format.cXUASTC_LDR_6x6.value:  descString += '6x6'; break;
935            case Module.basis_tex_format.cXUASTC_LDR_8x5.value:  descString += '8x5'; break;
936            case Module.basis_tex_format.cXUASTC_LDR_8x6.value:  descString += '8x6'; break;
937            case Module.basis_tex_format.cXUASTC_LDR_10x5.value: descString += '10x5'; break;
938            case Module.basis_tex_format.cXUASTC_LDR_10x6.value: descString += '10x6'; break;
939            case Module.basis_tex_format.cXUASTC_LDR_10x8.value: descString += '10x8'; break;
940            case Module.basis_tex_format.cXUASTC_LDR_8x8.value:  descString += '8x8'; break;
941            case Module.ba
941sis_tex_format.cXUASTC_LDR_10x10.value: descString += '10x10'; break;
942            case Module.basis_tex_format.cXUASTC_LDR_12x10.value: descString += '12x10'; break;
943            case Module.basis_tex_format.cXUASTC_LDR_12x12.value: descString += '12x12'; break;
944            default:
945             break;
946        }
947      }
948      else if (is_uastc)
949         descString += ' from UASTC LDR 4x4';
950      else
951         descString += ' from ETC1S';
952         
953      // Start transcoding first before calling dumpKTX2FileDesc() so isVideo()) is always set correctly. 
954      // (That flag depends on KTX2 global supercompression fields that are only unpacked when startTranscoding() is called.)
955      if (!ktx2File.startTranscoding())
956      {
957         updateErrorLine('startTranscoding failed');
958
959         log('startTranscoding failed');
960         console.warn('startTranscoding failed');
961
962         ktx2File.close();
963         ktx2File.delete();
964         return;
965      }
966
967      var total_texels = dumpKTX2FileDesc(ktx2File);
968      var bpp = (data.byteLength * 8.0) / total_texels;
969      var fileSizeKB = data.byteLength / 1024.0;
970      descString += ` (${bpp.toFixed(3)} bits/pixel, ${fileSizeKB.toFixed(2)} KB).`;
971
972      elem('format').innerText = descString;
973            
974      const dstSize = ktx2File.getImageTranscodedSizeInBytes(selectedLevel, selectedLayer, selectedFace, format);
975      const dst = new Uint8Array(dstSize);
976
977      const levelIndex = selectedLevel;
978      const layerIndex = selectedLayer;
979      const faceIndex = selectedFace;
980
981      var flags = elem('highquality_transcoding').checked ? Module.basisu_decode_flags.cDecodeFlagsHighQuality.value : 0;
982
983      if (elem('etc1s_bc7_transcoder_no_chroma_filtering').checked)
984         flags = flags | Module.basisu_decode_flags.cDecodeFlagsNoETC1SChromaFiltering.value;
985
986      if (elem('xuastc_ldr_disable_fast_bc7_transcoding').checked)
987         flags = flags | Module.basisu_decode_flags.cDecodeFlagXUASTCLDRDisableFastBC7Transcoding.value;
988
989      if (elem('transcode_alpha_to_opaque_formats').checked)
990         flags = flags | Module.basisu_decode_flags.cDecodeFlagsTranscodeAlphaDataToOpaqueFormats.value;
991
992      // Set deblocking related transcode options
993      const deblocking_filter_select = document.getElementById("deblocking-filter-select");
994      const deblocking_mode = Number(deblocking_filter_select.value);
995              
996      // if CPU deblocking is forced on
997      if (deblocking_mode == 3)
998      {
999         // They're forcing CPU deblocking, which is also the transcoder default. By default the transcoder only deblocks block sizes >= VERY_LARGE_BLOCK_SIZE_IN_PIXELS (80) texels, but we can force it on for all block sizes here.
1000         // We either enable it or disable it here.
1001         if (elem('xuastc_ldr_force_deblocking_on_all_block_sizes').checked)
1002         {
1003            flags = flags | Module.basisu_decode_flags.cDecodeFlagsForceDeblockFiltering.value;
1004            used_cpu_deblocking_flag = true;
1005         }
1006         else
1007         {
1008            // By default CPU deblocking during transcoding will only happen on block sizes >= 50 texels
1009            if (tex_block_width * tex_block_height >= VERY_LARGE_BLOCK_SIZE_IN_PIXELS)
1010            {
1011               flags = flags | Module.basisu_decode_flags.cDecodeFlagsForceDeblockFiltering.value;
1012               used_cpu_deblocking_flag = true;
1013            }
1014         }
1015         
1016         if (!used_cpu_deblocking_flag)
1017         {
1018            flags = flags | Module.basisu_decode_flags.cDecodeFlagsNoDeblockFiltering.value;
1019         }
1020      }
1021      else
1022      {
1023        // By default, disable all CPU deblocking during transcoding (we'll use GPU or none during display).
1024        // This overrides the transcoder's default behavior, which is to deblock while transcoding to non-ASTC formats, block sizes >=10x8 or larger. 
1025        flags = flags | Module.basisu_decode_flags.cDecodeFlagsNoDeblockFiltering.value;
1026      }
1027            
1028      // For simplicity here this only includes the transcode time, not initial file opening/global codebook decomp time.
1029      // For all formats except ETC1S the file opening time will be tiny. ETC1S codebook decomp is also quite fast, but not free with large codebooks.
1030      const startTime = performance.now();
1031
1032      if (!ktx2File.transcodeImageWithFlags(dst, levelIndex, layerIndex, faceIndex, format, flags, -1, -1))
1033      {
1034         updateErrorLine('ktx2File.transcodeImage failed');
1035
1036         log('ktx2File.transcodeImage failed');
1037         console.warn('transcodeImage failed');
1038
1039         ktx2File.close();
1040         ktx2File.delete();
1041
1042         return;
1043      }
1044
1045      const elapsed = performance.now() - startTime;
1046
1047      g_transcodingTime = elapsed;
1048      
1049      descString += '\nTexture Size: ' + baseWidth + 'x' + baseHeight + ', Levels: ' + levels + ', Layers: ' + layers + ', Faces: ' + faces + ', sRGB: ' + tex_is_srgb + ', alpha: ' + tex_has_alpha;
1050
1051      descString += '\nViewing: Mip ' + selectedLevel + ' (' + width + 'x' + height + ') Face ' + selectedFace + ' Layer ' + selectedLayer;
1052
1053      descString += '\nTranscode time: ' + g_transcodingTime.toFixed(3) + 'ms';
1054
1055      if (g_lastEncodeTime > 0)
1056      {
1057         descString += '\nEncode time: ' + g_lastEncodeTime.toFixed(3) + 'ms';
1058         if (g_lastEncodeMip0RGBAPSNR > 0)
1059            descString += '. Mip0: ' + g_lastEncodeMip0RGBAPSNR.toFixed(3) + ' dB Avg. PSNR';
1060      }
1061
1062      elem('format').innerText = descString;
1063
1064      ktx2File.close();
1065      ktx2File.delete();
1066
1067      log('width: ' + width);
1068      log('height: ' + height);
1069      log('basisTexFormat: ' + basisTexFormat);
1070      log('has_alpha: ' + tex_has_alpha);
1071      log('is_srgb: ' + tex_is_srgb);
1072      log('isUASTC: ' + is_uastc);
1073      log('isHDR: ' + is_hdr);
1074      log('isASTC_LDR: ' + is_astc_ldr);
1075      log('isXUASTC_LDR: ' + is_xuastc_ldr);
1076      log('srcBlockWidth: ' + srcBlockWidth);
1077      log('srcBlockHeight: ' + srcBlockHeight);
1078      log('dstBlockWidth: ' + dstBlockWidth);
1079      log('dstBlockHeight: ' + dstBlockHeight);
1080      log('levels: ' + levels);
1081      log('layers: ' + layers);
1082      log('faces: ' + faces);
1083
1084      log('ldrHDRUpconversionNitMultiplier: ' + ldrHDRUpconversionNitMultiplier);
1085      log('tex_deblocking_filter_id: ' + tex_deblocking_filter_id);
1086
1087      log('selectedLevel: ' + selectedLevel + ' (' + width + 'x' + height + '), face: ' + selectedFace + ', layer: ' + selectedLayer);
1088
1089      logTime('transcoding time', elapsed.toFixed(3));
1090
1091      alignedWidth = Math.floor((width + dstBlockWidth - 1) / dstBlockWidth) * dstBlockWidth;
1092      alignedHeight = Math.floor((height + dstBlockHeight - 1) / dstBlockHeight) * dstBlockHeight;
1093
1094      displayWidth = alignedWidth;
1095      displayHeight = alignedHeight;
1096
1097      setCanvasSize(alignedWidth, alignedHeight);
1098
1099      // Now create the WebGL texture object.
1100      
1101      var force_srgb_sampling = false;
1102
1103      // For ASTC LDR and XUASTC LDR, select the right format depending on the transfer function in the DFD (as reported by the transcoder API).
1104      // It's important for the GPU to decompress the texture using the same decode profile as what the encoder used.
1105      if ((format === BASIS_FORMAT.cTFASTC_4x4) || (format === BASIS_FORMAT.cTFASTC_HDR_4x4_RGBA))
1106      {
1107         tex = renderer.createCompressedTexture(dst, alignedWidth, alignedHeight, COMPRESSED_RGBA_ASTC_4x4_KHR + (tex_is_srgb ? 0x20 : 0));
1108         force_srgb_sampling = tex_is_srgb;
1109      }
1110      else if ((format === BASIS_FORMAT.cTFASTC_LDR_6x6_RGBA) || (format === BASIS_FORMAT.cTFASTC_HDR_6x6_RGBA))
1111      {
1112         tex = renderer.createCompressedTexture(dst, alignedWidth, alignedHeight, COMPRESSED_RGBA_ASTC_6x6_KHR + (tex_is_srgb ? 0x20 : 0));
1113         force_srgb_sampling = tex_is_srgb;
1114      }
1115      else if (format === BASIS_FORMAT.cTFASTC_LDR_5x4_RGBA)
1116      {
1117         tex = renderer.createCompressedTexture(dst, alignedWidth, alignedHeight, COMPRESSED_RGBA_ASTC_5x4_KHR + (tex_is_srgb ? 0x20 : 0));
1118         force_srgb_sampling = tex_is_srgb; 
1119      }
1120      else if (format === BASIS_FORMAT.cTFASTC_LDR_5x5_RGBA)
1121      {
1122         tex = renderer.createCompressedTexture(dst, alignedWidth, alignedHeight, COMPRESSED_RGBA_ASTC_5x5_KHR + (tex_is_srgb ? 0x20 : 0));
1123         force_srgb_sampling = tex_is_srgb;
1124      }
1125      else if (format === BASIS_FORMAT.cTFASTC_LDR_6x5_RGBA)
1126      {
1127         tex = renderer.createCompressedTexture(dst, alignedWidth, alignedHeight, COMPRESSED_RGBA_ASTC_6x5_KHR + (tex_is_srgb ? 0x20 : 0));
1128         force_srgb_sampling = tex_is_srgb;
1129      }
1130      else if (format === BASIS_FORMAT.cTFASTC_LDR_8x5_RGBA)
1131      {
1132         tex = renderer.createCompressedTexture(dst, alignedWidth, alignedHeight, COMPRESSED_RGBA_ASTC_8x5_KHR + (tex_is_srgb ? 0x20 : 0));
1133         force_srgb_sampling = tex_is_srgb;
1134      }
1135      else if (format === BASIS_FORMAT.cTFASTC_LDR_8x6_RGBA)
1136      {
1137         tex = renderer.createCompressedTexture(dst, alignedWidth, alignedHeight, COMPRESSED_RGBA_ASTC_8x6_KHR + (tex_is_srgb ? 0x20 : 0));
1138         force_srgb_sampling = tex_is_srgb;
1139      }
1140      else if (format === BASIS_FORMAT.cTFASTC_LDR_10x5_RGBA)
1141      {
1142         tex = renderer.createCompressedTexture(dst, alignedWidth, alignedHeight, COMPRESSED_RGBA_ASTC_10x5_KHR + (tex_is_srgb ? 0x20 : 0));
1143         force_srgb_sampling = tex_is_srgb;
1144      }
1145      else if (format === BASIS_FORMAT.cTFASTC_LDR_10x6_RGBA)
1146      {
1147         tex = renderer.createCompressedTexture(dst, alignedWidth, alignedHeight, COMPRESSED_RGBA_ASTC_10x6_KHR + (tex_is_srgb ? 0x20 : 0));
1148         force_srgb_sampling = tex_is_srgb;
1149      }
1150      else if (format === BASIS_FORMAT.cTFASTC_LDR_8x8_RGBA)
1151      {
1152         tex = renderer.createCompressedTexture(dst, alignedWidth, alignedHeight, COMPRESSED_RGBA_ASTC_8x8_KHR + (tex_is_srgb ? 0x20 : 0));
1153         force_srgb_sampling = tex_is_srgb;
1154      }
1155      else if (format === BASIS_FORMAT.cTFASTC_LDR_10x8_RGBA)
1156      {
1157         tex = renderer.createCompressedTexture(dst, alignedWidth, alignedHeight, COMPRESSED_RGBA_ASTC_10x8_KHR + (tex_is_srgb ? 0x20 : 0));
1158         force_srgb_sampling = tex_is_srgb;
1159      }
1160      else if (format === BASIS_FORMAT.cTFASTC_LDR_10x10_RGBA)
1161      {
1162         tex = renderer.createCompressedTexture(dst, alignedWidth, alignedHeight, COMPRESSED_RGBA_ASTC_10x10_KHR + (tex_is_srgb ? 0x20 : 0));
1163         force_srgb_sampling = tex_is_srgb;
1164      }
1165      else if (format === BASIS_FORMAT.cTFASTC_LDR_12x10_RGBA)
1166      {
1167         tex = renderer.createCompressedTexture(dst, alignedWidth, alignedHeight, COMPRESSED_RGBA_ASTC_12x10_KHR + (tex_is_srgb ? 0x20 : 0));
1168         force_srgb_sampling = tex_is_srgb;
1169      }
1170      else if (format === BASIS_FORMAT.cTFASTC_LDR_12x12_RGBA)
1171      {
1172         tex = renderer.createCompressedTexture(dst, alignedWidth, alignedHeight, COMPRESSED_RGBA_ASTC_12x12_KHR + (tex_is_srgb ? 0x20 : 0));
1173         force_srgb_sampling = tex_is_srgb;
1174      }
1175      else if ((format === BASIS_FORMAT.cTFBC3) || (format === BASIS_FORMAT.cTFBC1) || (format == BASIS_FORMAT.cTFBC7))
1176      {
1177         tex = renderer.createCompressedTexture(dst, alignedWidth, alignedHeight, DXT_FORMAT_MAP[format]);
1178      }
1179      else if (format === BASIS_FORMAT.cTFETC1)
1180      {
1181         tex = renderer.createCompressedTexture(dst, alignedWidth, alignedHeight, COMPRESSED_RGB_ETC1_WEBGL);
1182      }
1183      else if (format === BASIS_FORMAT.cTFETC2)
1184      {
1185         // ETC2 RGBA (non-sRGB, matching the BC/UNORM path); 16 bytes/block.
1186         tex = renderer.createCompressedTexture(dst, alignedWidth, alignedHeight, COMPRESSED_RGBA8_ETC2_EAC);
1187      }
1188      else if (format === BASIS_FORMAT.cTFPVRTC1_4_RGB)
1189      {
1190         tex = renderer.createCompressedTexture(dst, alignedWidth, alignedHeight, COMPRESSED_RGB_PVRTC_4BPPV1_IMG);
1191      }
1192      else if (format === BASIS_FORMAT.cTFPVRTC1_4_RGBA)
1193      {
1194         tex = renderer.createCompressedTexture(dst, alignedWidth, alignedHeight, COMPRESSED_RGBA_PVRTC_4BPPV1_IMG);
1195      }
1196      else if (format === BASIS_FORMAT.cTFBC6H)
1197      {
1198         tex = renderer.createCompressedTexture(dst, alignedWidth, alignedHeight, COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_EXT);
1199      }
1200      else if (format == BASIS_FORMAT.cTFRGBA_HALF)
1201      {
1202         // Uncompressed RGBA HALF or 32bpp (with no block alignment)
1203         displayWidth = width;
1204         displayHeight = height;
1205         setCanvasSize(width, height);
1206
1207         if ((rgbaHalfSupported) && (!rgbaHalfDisabled))
1208         {
1209            var numHalfs = dstSize / 2;
1210
1211            // Create uint16 data from the uint8 data.
1212            var dstHalfs = new Uint16Array(numHalfs);
1213
1214            // Convert the array of bytes to an array of uint16's.
1215            for (var i = 0; i < numHalfs; i++)
1216               dstHalfs[i] = dst[2 * i + 0] | (dst[2 * i + 1] << 8);
1217
1218            tex = renderer.createHalfRGBATexture(dstHalfs, width, height, halfFloatWebGLFormat);
1219         }
1220         else
1221         {
1222            // No HDR texture formats are supported (TODO: 9e5?) Fall back to plain 32bpp RGBA, just to do *something*. (Could also convert to RGBM.)
1223            const dstRGBA = new Uint8Array(width * height * 4);
1224
1225            // Convert the array of half floats to uint8_t's, clamping as needed.
1226            var srcOfs = 0, dstOfs = 0;
1227            for (var y = 0; y < height; y++)
1228            {
1229               for (var x = 0; x < width; x++)
1230               {
1231                  for (var c = 0; c < 4; c++)
1232                  {
1233                     var h = dst[srcOfs] | (dst[srcOfs + 1] << 8);
1234                     var f = Module.convertHalfToFloat(h) * ((c == 3) ? 1 : ldrHDRUpconversionScale);
1235
1236                     dstRGBA[dstOfs] = Math.min(255, Math.max(0, Math.round(f * 255.0)));
1237
1238                     srcOfs += 2;
1239                     dstOfs++;
1240                  }
1241               }
1242            }
1243
1244            tex = renderer.createRgbaTexture(dstRGBA, width, height);
1245
1246            // We've applied the LDR->
1246HDR upconversion scale when converting to 32bpp, so don't have the shader do it.
1247            ldrHDRUpconversionScale = 1.0;
1248         }
1249      }
1250      else
1251      {
1252         // Uncompressed 32-bit RGBA (with no block alignment)
1253
1254         displayWidth = width;
1255         displayHeight = height;
1256         setCanvasSize(width, height);
1257
1258         tex = renderer.createRgbaTexture(dst, width, height);
1259      }
1260
1261      linearToSRGBFlag = (is_hdr != 0) || (force_srgb_sampling != 0);
1262
1263      elem('linear_to_srgb').innerText = linearToSRGBFlag ? 'Enabled' : 'Disabled';
1264
1265      redraw();
1266
1267      // Tri-state: mark KTX2 as the active source and clear any DDS source. Done on the
1268      // success path only, so a failed load leaves the previously-loaded source intact.
1269      curLoadedKTX2Data = data;
1270      curLoadedKTX2URI = uri;
1271      curLoadedDDSData = null;
1272      curLoadedDDSURI = null;
1273      updateLoadedFormatDisplay();
1274   }
1275
1276   // Logs a .DDS source's metadata (using ONLY the DDSFile API) and returns the total
1277   // texel count across all images (for the bits/pixel readout). The DDS analog of
1278   // dumpKTX2FileDesc(); DDS has no DFD/key-value/header machinery so this is simpler.
1279   function dumpDDSFileDesc(ddsFile)
1280   {
1281      log('------');
1282      log('DDS Width: ' + ddsFile.getWidth());
1283      log('DDS Height: ' + ddsFile.getHeight());
1284      log('Levels: ' + ddsFile.getLevels());
1285      log('Layers: ' + ddsFile.getLayers());
1286      log('Faces: ' + ddsFile.getFaces());
1287      log('IsCubemap: ' + ddsFile.getIsCubemap());
1288      log('Has alpha: ' + ddsFile.getHasAlpha());
1289      log('isSRGB: ' + ddsFile.isSRGB());
1290      log('Contained transcoder_texture_format: ' + ddsFile.getFormat());
1291      log('DDS format (dds_format enum): ' + ddsFile.getDDSFormat() + ' (' + ddsFormatName(ddsFile) + ')');
1292      log('Source kind (dds_source_kind enum): ' + ddsFile.getSourceKind());
1293
1294      log('--');
1295      log('Image level information:');
1296
1297      var total_texels = 0;
1298      var level_index;
1299      for (level_index = 0; level_index < ddsFile.getLevels(); level_index++)
1300      {
1301         var layer_index;
1302         // layers==0 means non-arrayed (treat as a single layer at index 0).
1303         for (layer_index = 0; layer_index < Math.max(1, ddsFile.getLayers()); layer_index++)
1304         {
1305            var face_index;
1306            for (face_index = 0; face_index < ddsFile.getFaces(); face_index++)
1307            {
1308               var info = ddsFile.getImageLevelInfo(level_index, layer_index, face_index);
1309
1310               log('level: ' + level_index + ' layer: ' + layer_index + ' face: ' + face_index);
1311               log('orig_width: ' + info.origWidth);
1312               log('orig_height: ' + info.origHeight);
1313               log('width: ' + info.width);
1314               log('height: ' + info.height);
1315               log('numBlocksX: ' + info.numBlocksX);
1316               log('numBlocksY: ' + info.numBlocksY);
1317               log('blockWidth: ' + info.blockWidth);
1318               log('blockHeight: ' + info.blockHeight);
1319               log('totalBlocks: ' + info.totalBlocks);
1320               log('--');
1321
1322               total_texels += info.origWidth * info.origHeight;
1323            }
1324         }
1325      }
1326      log('------');
1327
1328      return total_texels;
1329   }
1330
1331   // Human-readable name for a .DDS's exact physical format. Prefers the encoder
1332   // build's authoritative DDSFile.getDDSFormatString() (present in basis_encoder*.js);
1333   // falls back to a reverse lookup of the Module.dds_format enum if that's absent
1334   // (e.g. a transcoder-only build).
1335   function ddsFormatName(ddsFile)
1336   {
1337      var ddsFmt = ddsFile.getDDSFormat();
1338      if (typeof ddsFile.getDDSFormatString === 'function')
1339         return ddsFile.getDDSFormatString(ddsFmt);
1340      if (Module.dds_format)
1341      {
1342         for (var name in Module.dds_format)
1343         {
1344            if (Module.dds_format[name] && Module.dds_format[name].value === ddsFmt)
1345               return name;
1346         }
1347      }
1348      return String(ddsFmt);
1349   }
1350
1351   // Transcode + display a loaded .DDS source. NEW functionality, deliberately kept
1352   // separate from transcodeTexture() (the KTX2 path) so that path is untouched.
1353   // DDS is LDR/BCn-or-uncompressed only here (no HDR), view-only (no encode/export).
1354   // It picks a GPU display format using the SAME capability + disable-toggle priority
1355   // as the KTX2 viewer (ASTC 4x4 -> BC7 -> BC1/BC3 -> PVRTC1 -> ETC1 -> RGBA32), plus
1356   // a BC4/BC5 passthrough when the RGTC extension is available & enabled. Every
1357   // candidate is additionally gated by DDSFile.isTranscodeFormatSupported(); anything
1358   // unsupported falls through to the next, ending at always-supported RGBA32.
1359   function transcodeDDSTexture(data, uri)
1360   {
1361      updateErrorLine("");
1362
1363      log('------');
1364      log('transcodeDDSTexture(): Loading .DDS file:');
1365
1366      const { DDSFile, initializeBasis } = Module;
1367
1368      resetDrawSettings();
1369
1370      initializeBasis();
1371
1372      used_cpu_deblocking_flag = false;
1373
1374      const ddsFile = new DDSFile(new Uint8Array(data));
1375
1376      if (!ddsFile.isValid())
1377      {
1378         updateErrorLine('Invalid or unsupported .dds file');
1379         console.warn('Invalid or unsupported .dds file');
1380         ddsFile.close();
1381         ddsFile.delete();
1382         return;
1383      }
1384
1385      var baseWidth = ddsFile.getWidth();
1386      var baseHeight = ddsFile.getHeight();
1387
1388      // DDS here is always LDR (the dds_format enum has no HDR entries).
1389      is_hdr = false;
1390
1391      layers = ddsFile.getLayers();
1392      levels = ddsFile.getLevels();
1393      faces = ddsFile.getFaces();
1394      tex_has_alpha = ddsFile.getHasAlpha();
1395      tex_is_srgb = ddsFile.isSRGB();
1396
1397      // DDS has no deblocking concept; clear so redraw()'s GPU-deblock branch never
1398      // engages on a stale KTX2 value.
1399      tex_deblocking_filter_id = 0;
1400      ldrHDRUpconversionScale = 1.0;
1401
1402      updateMipmapSliderRange(levels);
1403
1404      var selectedLevel = parseInt(document.getElementById('mipmap-level-slider').value, 10);
1405      if (isNaN(selectedLevel) || selectedLevel < 0 || selectedLevel >= levels)
1406      {
1407         selectedLevel = 0;
1408         document.getElementById('mipmap-level-slider').value = 0;
1409         document.getElementById('mipmap-level-value').textContent = '0';
1410      }
1411
1412      updateCubemapFaceRange(faces);
1413
1414      var selectedFace = parseInt(document.getElementById('cubemap-face-slider').value, 10);
1415      if (isNaN(selectedFace) || selectedFace < 0 || selectedFace >= faces)
1416      {
1417         selectedFace = 0;
1418         document.getElementById('cubemap-face-slider').value = 0;
1419         document.getElementById('cubemap-face-value').textContent = '0';
1420      }
1421
1422      // layers==0 means non-arrayed (treat as 1 layer at index 0).
1423      var effectiveLayers = Math.max(1, layers);
1424      updateArrayLayerRange(effectiveLayers);
1425
1426      var selectedLayer = parseInt(document.getElementById('array-layer-slider').value, 10);
1427      if (isNaN(selectedLayer) || selectedLayer < 0 || selectedLayer >= effectiveLayers)
1428      {
1429         selectedLayer = 0;
1430         document.getElementById('array-layer-slider').value = 0;
1431         document.getElementById('array-layer-value').textContent = '0';
1432         document.getElementById('array-layer-input').value = 0;
1433      }
1434
1435      var imageLevelInfo = ddsFile.getImageLevelInfo(selectedLevel, selectedLayer, selectedFace);
1436      width = imageLevelInfo.origWidth;
1437      height = imageLevelInfo.origHeight;
1438      tex_block_width = imageLevelInfo.blockWidth;
1439      tex_block_height = imageLevelInfo.blockHeight;
1440
1441      if (!width || !height || !levels)
1442      {
1443         updateErrorLine('Invalid .dds file');
1444         console.warn('Invalid .dds file');
1445         ddsFile.close();
1446         ddsFile.delete();
1447         return;
1448      }
1449
1450      var dstBlockWidth = 4, dstBlockHeight = 4;
1451      var is_pow2 = ((width & (width - 1)) == 0) && ((height & (height - 1)) == 0);
1452
1453      var srcKind = ddsFile.getSourceKind();
1454      var DSK = Module.dds_source_kind;
1455
1456      // Returns true only if the transcoder can actually produce 'fmt' for this DDS.
1457      function ddsCanProduce(fmt) { return ddsFile.isTranscodeFormatSupported(fmt); }
1458
1459      // Decide the GPU display format.
1460      //  1) NATIVE PASSTHROUGH (preferred): if the DDS is a block format the GPU supports natively,
1461      //     transcode to that EXACT format -- a lossless passthrough (no re-encode, exact channels,
1462      //     fastest). BC1->BC1, BC3->BC3, BC4->BC4, BC5->BC5, BC7->BC7.
1463      //  2) FALLBACK (native unavailable, or an uncompressed source): pick the highest-quality
1464      //     supported format that PRESERVES the source's channels -- an alpha source only ever falls
1465      //     back to an alpha-capable target (never an RGB-only format), so no channels/data are lost.
1466      //     RGBA32 is the universal last resort (always available).
1467      // Every step is gated by both the GPU capability/disable toggles AND the transcoder's own
1468      // format support (ddsCanProduce).
1469      var formatString = 'UNKNOWN';
1470
1471      // When "Transcode alpha to opaque (ETC1/PVRTC1_RGB)" is checked, the transcoder carries alpha
1472      // separately, so those RGB-only targets become usable for alpha sources too (a testing feature).
1473      var allowAlphaToOpaque = elem('transcode_alpha_to_opaque_formats').checked;
1474
1475      // Detect single-channel (R) and dual-channel (RG) sources -- compressed (BC4/BC5) OR uncompressed
1476      // (R8/R8G8) -- via the source's actual DDS format. These route to the channel-matched
1477      // BC4/BC5/EAC R11/EAC RG11 targets below instead of expanding to a color codec. (Color sources
1478      // leave both flags false.)
1479      var ddsFmt = ddsFile.getDDSFormat();
1480      var DF = Module.dds_format;
1481      var isSingleChannelSrc = !!DF && ((ddsFmt === DF.cBC4.value) || (ddsFmt === DF.cR8.value));
1482      var isDualChannelSrc   = !!DF && ((ddsFmt === DF.cBC5.value) || (ddsFmt === DF.cR8G8.value));
1483
1484      // 1) native passthrough -- exact format match. (A BC2 source has no JS source_kind enum member and
1485      //    is never byte-passthrough-able, so it intentionally falls through to the fallback below.)
1486      if ((srcKind === DSK.cBC1.value) && dxtSupported && !dxtDisabled && ddsCanProduce(BASIS_FORMAT.cTFBC1))
1487      {
1488         formatString = 'BC1'; format = BASIS_FORMAT.cTFBC1;
1489      }
1490      else if ((srcKind === DSK.cBC3.value) && dxtSupported && !dxtDisabled && ddsCanProduce(BASIS_FORMAT.cTFBC3))
1491      {
1492         formatString = 'BC3'; format = BASIS_FORMAT.cTFBC3;
1493      }
1494      else if ((srcKind === DSK.cBC4.value) && rgtcSupported && !rgtcDisabled && ddsCanProduce(BASIS_FORMAT.cTFBC4))
1495      {
1496         formatString = 'BC4'; format = BASIS_FORMAT.cTFBC4;
1497      }
1498      else if ((srcKind === DSK.cBC5.value) && rgtcSupported && !rgtcDisabled && ddsCanProduce(BASIS_FORMAT.cTFBC5))
1499      {
1500         formatString = 'BC5'; format = BASIS_FORMAT.cTFBC5;
1501      }
1502      else if ((srcKind === DSK.cBC7.value) && bc7Supported && !bc7Disabled && ddsCanProduce(BASIS_FORMAT.cTFBC7))
1503      {
1504         formatString = 'BC7'; format = BASIS_FORMAT.cTFBC7;
1505      }
1506      // Single/dual-channel sources (BC4/BC5 OR uncompressed R8/R8G8): channel-matched targets so we don't
1507      // expand them to a color codec -- BC4/BC5 (RGTC) preferred, else EAC R11/RG11. BC4/BC5 SOURCES are
1508      // byte-passthrough'd above; this handles R8/R8G8 re-encode and the no-RGTC case.
1509      else if (isSingleChannelSrc && rgtcSupported && !rgtcDisabled && ddsCanProduce(BASIS_FORMAT.cTFBC4))
1510      {
1511         formatString = 'BC4'; format = BASIS_FORMAT.cTFBC4;
1512      }
1513      else if (isSingleChannelSrc && eacSupported && !eacDisabled && ddsCanProduce(BASIS_FORMAT.cTFETC2_EAC_R11))
1514      {
1515         formatString = 'EAC R11'; format = BASIS_FORMAT.cTFETC2_EAC_R11;
1516      }
1517      else if (isDualChannelSrc && rgtcSupported && !rgtcDisabled && ddsCanProduce(BASIS_FORMAT.cTFBC5))
1518      {
1519         formatString = 'BC5'; format = BASIS_FORMAT.cTFBC5;
1520      }
1521      else if (isDualChannelSrc && eacSupported && !eacDisabled && ddsCanProduce(BASIS_FORMAT.cTFETC2_EAC_RG11))
1522      {
1523         formatString = 'EAC RG11'; format = BASIS_FORMAT.cTFETC2_EAC_RG11;
1524      }
1525      // 2) channel-preserving fallback. Highest quality first; alpha sources stay alpha-capable.
1526      else if (astcSupported && !astcDisabled && ddsCanProduce(BASIS_FORMAT.cTFASTC_4x4))
1527      {
1528         formatString = 'ASTC LDR 4x4'; format = BASIS_FORMAT.cTFASTC_4x4;
1529      }
1530      else if (bc7Supported && !bc7Disabled && ddsCanProduce(BASIS_FORMAT.cTFBC7))
1531      {
1532         formatString = 'BC7'; format = BASIS_FORMAT.cTFBC7;
1533      }
1534      else if (dxtSupported && !dxtDisabled && ddsCanProduce(BASIS_FORMAT.cTFBC3))
1535      {
1536         formatString = 'BC3'; format = BASIS_FORMAT.cTFBC3;
1537      }
1538      else if (etc2Supported && !etc2Disabled && ddsCanProduce(BASIS_FORMAT.cTFETC2))
1539      {
1540         formatString = 'ETC2 RGBA'; format = BASIS_FORMAT.cTFETC2;
1541      }
1542      else if (pvrtcSupported && !pvrtcDisabled && is_pow2 && tex_has_alpha && ddsCanProduce(BASIS_FORMAT.cTFPVRTC1_4_RGBA))
1543      {
1544         formatString = 'PVRTC1_RGBA'; format = BASIS_FORMAT.cTFPVRTC1_4_RGBA;
1545      }
1546      else if (dxtSupported && !dxtDisabled && ddsCanProduce(BASIS_FORMAT.cTFBC1))
1547      {
1548         formatString = 'BC1'; format = BASIS_FORMAT.cTFBC1;
1549      }
1550      else if (etcSupported && !etcDisabled && ddsCanProduce(BASIS_FORMAT.cTFETC1))
1551      {
1552         // ETC1 is RGB-only and preferred over PVRTC1 (higher quality). Usable for an alpha source only
1553         // when "Transcode alpha to opaque" is checked (else alpha would be silently dropped).
1554         formatString = 'ETC1'; format = BASIS_FORMAT.cTFETC1;
1555      }
1556      else if (pvrtcSupported && !pvrtcDisabled && is_pow2 && (!tex_has_alpha || allowAlphaToOpaque) && ddsCanProduce(BASIS_FORMAT.cTFPVRTC1_4_RGB))
1557      {
1558         // PVRTC1 RGB is RGB-only; usable for an alpha source only when "Transcode alpha to opaque" is checked.
1559         formatString = 'PVRTC1_RGB'; format = BASIS_FORMAT.cTFPVRTC1_4_RGB;
1560      }
1561      else
1562      {
1563         formatString = 'RGBA32'; format = BASIS_FORMAT.cTFRGBA32;
1564         log('Note: decoding .DDS to uncompressed 32-bit RGBA (no enabled/supported GPU format matched).');
1565      }
1566
1567      var descString = formatString + ' from DDS ' + ddsFormatName(ddsFile);
1568
1569      if (!ddsFile.startTranscoding())
1570      {
1571         updateErrorLine('startTranscoding failed');
1572         log('startTranscoding failed');
1573         console.warn('startTranscoding failed');
1574         ddsFile.close();
1575         ddsFile.delete();
1576         return;
1577      }
1578
1579      var total_texels = dumpDDSFileDesc(ddsFile);
1580      var bpp = total_texels ? (data.byteLength * 8.0) / total_texels : 0;
1581      var fileSizeKB = data.byteLength / 1024.0;
1582      descString += ` (${bpp.toFixed(3)} bits/pixel, ${fileSizeKB.toFixed(2)} KB).`;
1583
1584      elem('format').innerText = descString;
1585
1586      const dstSize = ddsFile.getImageTranscodedSizeInBytes(selectedLevel, selectedLayer, selectedFace, format);
1587      const dst = new Uint8Array(dstSize);
1588
1589      // Reuse the shared decode-flag composer (DDS ignores the ETC1S/XUASTC-specific
1590      // bits; deblocking defaults to off for these formats, which is correct).
1591      var flags = composeTranscodeDecodeFlags(tex_block_width, tex_block_height);
1592
1593      const startTime = performance.now();
1594
1595      if (!ddsFile.transcodeImageWithFlags(dst, selectedLevel, selectedLayer, selectedFace, format, flags, -1, -1))
1596      {
1597         updateErrorLine('ddsFile.transcodeImageWithFlags failed');
1598         log('ddsFile.transcodeImageWithFlags failed');
1599         console.warn('transcodeImageWithFlags failed');
1600         ddsFile.close();
1601         ddsFile.delete();
1602         return;
1603      }
1604
1605      const elapsed = performance.now() - startTime;
1606      g_transcodingTime = elapsed;
1607
1608      descString += '\nTexture Size: ' + baseWidth + 'x' + baseHeight + ', Levels: ' + levels + ', Layers: ' + layers + ', Faces: ' + faces + ', sRGB: ' + tex_is_srgb + ', alpha: ' + tex_has_alpha;
1609      descString += '\nViewing: Mip ' + selectedLevel + ' (' + width + 'x' + height + ') Face ' + selectedFace + ' Layer ' + selectedLayer;
1610      descString += '\nTranscode time: ' + g_transcodingTime.toFixed(3) + 'ms';
1611      elem('format').innerText = descString;
1612
1613      ddsFile.close();
1614      ddsFile.delete();
1615
1616      log('width: ' + width);
1617      log('height: ' + height);
1618      log('has_alpha: ' + tex_has_alpha);
1619      log('is_srgb: ' + tex_is_srgb);
1620      log('dstBlockWidth: ' + dstBlockWidth);
1621      log('dstBlockHeight: ' + dstBlockHeight);
1622      log('levels: ' + levels);
1623      log('layers: ' + layers);
1624      log('faces: ' + faces);
1625      logTime('transcoding time', elapsed.toFixed(3));
1626
1627      alignedWidth = Math.floor((width + dstBlockWidth - 1) / dstBlockWidth) * dstBlockWidth;
1628      alignedHeight = Math.floor((height + dstBlockHeight - 1) / dstBlockHeight) * dstBlockHeight;
1629
1630      displayWidth = alignedWidth;
1631      displayHeight = alignedHeight;
1632
1633      setCanvasSize(alignedWidth, alignedHeight);
1634
1635      var force_srgb_sampling = false;
1636
1637      if (format === BASIS_FORMAT.cTFASTC_4x4)
1638      {
1639         tex = renderer.createCompressedTexture(dst, alignedWidth, alignedHeight, COMPRESSED_RGBA_ASTC_4x4_KHR + (tex_is_srgb ? 0x20 : 0));
1640         force_srgb_sampling = tex_is_srgb;
1641      }
1642      else if ((format === BASIS_FORMAT.cTFBC1) || (format === BASIS_FORMAT.cTFBC3) || (format === BASIS_FORMAT.cTFBC7) ||
1643               (format === BASIS_FORMAT.cTFBC4) || (format === BASIS_FORMAT.cTFBC5))
1644      {
1645         tex = renderer.createCompressedTexture(dst, alignedWidth, alignedHeight, DXT_FORMAT_MAP[format]);
1646      }
1647      else if (format === BASIS_FORMAT.cTFETC1)
1648      {
1649         tex = renderer.createCompressedTexture(dst, alignedWidth, alignedHeight, COMPRESSED_RGB_ETC1_WEBGL);
1650      }
1651      else if (format === BASIS_FORMAT.cTFETC2)
1652      {
1653         // ETC2 RGBA (non-sRGB, matching the BC/UNORM path); 16 bytes/block.
1654         tex = renderer.createCompressedTexture(dst, alignedWidth, alignedHeight, COMPRESSED_RGBA8_ETC2_EAC);
1655      }
1656      else if (format === BASIS_FORMAT.cTFETC2_EAC_R11)
1657      {
1658         tex = renderer.createCompressedTexture(dst, alignedWidth, alignedHeight, COMPRESSED_R11_EAC);
1659      }
1660      else if (format === BASIS_FORMAT.cTFETC2_EAC_RG11)
1661      {
1662         tex = renderer.createCompressedTexture(dst, alignedWidth, alignedHeight, COMPRESSED_RG11_EAC);
1663      }
1664      else if (format === BASIS_FORMAT.cTFPVRTC1_4_RGB)
1665      {
1666         tex = renderer.createCompressedTexture(dst, alignedWidth, alignedHeight, COMPRESSED_RGB_PVRTC_4BPPV1_IMG);
1667      }
1668      else if (format === BASIS_FORMAT.cTFPVRTC1_4_RGBA)
1669      {
1670         tex = renderer.createCompressedTexture(dst, alignedWidth, alignedHeight, COMPRESSED_RGBA_PVRTC_4BPPV1_IMG);
1671      }
1672      else
1673      {
1674         // Uncompressed 32-bit RGBA (no block alignment)
1675         displayWidth = width;
1676         displayHeight = height;
1677         setCanvasSize(width, height);
1678         tex = renderer.createRgbaTexture(dst, width, height);
1679      }
1680
1681      linearToSRGBFlag = (force_srgb_sampling != 0);
1682      elem('linear_to_srgb').innerText = linearToSRGBFlag ? 'Enabled' : 'Disabled';
1683
1684      redraw();
1685
1686      // Tri-state: mark DDS as the active source and clear any KTX2 source. Done on the
1687      // success path only, so a failed load leaves the previously-loaded source intact.
1688      curLoadedDDSData = data;
1689      curLoadedDDSURI = uri;
1690      curLoadedKTX2Data = null;
1691      curLoadedKTX2URI = null;
1692      updateLoadedFormatDisplay();
1693   }
1694
1695   function download_file(filename, body)
1696   {
1697      var element = document.createElement('a');
1698
1699      //element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
1700
1701      const blob = new Blob([body]);
1702      const url = URL.createObjectURL(blob);
1703      element.setAttribute('href', url);
1704
1705      element.setAttribute('download', filename);
1706
1707      element.style.display = 'none';
1708      document.body.appendChild(element);
1709
1710      element.click();
1711
1712      URL.revokeObjectURL(url);
1713
1714      document.body.removeChild(element);
1715   }
1716
1717   var encodedKTX2File;
1718
1719   function resetDrawSettings()
1720   {
1721      drawMode = 0;
1722      updateViewModeButtons();
1723
1724      linearToSRGBFlag = false;
1725      elem('linear_to_srgb').innerText = linearToSRGBFlag ? 'Enabled' : 'Disabled';
1726
1727//         drawScale = 1.0;
1728//         elem('scale-slider').value = .5;
1729//         elem('scale-value').textContent = 1;
1730
1731      ldrHDRUpconversionScale = 1.0;
1732   }
1733
1734   function getFileExtension(url)
1735   {
1736      const lastDotIndex = url.lastIndexOf('.');
1737      if (lastDotIndex === -1)
1738         return null; // No extension found
1739      const extension = url.substring(lastDotIndex + 1);
1740
1741      // Remove any query parameters or fragments from the extension and convert to lowercase
1742      const cleanExtension = extension.split(/[\?#]/)[0].toLowerCase();
1743
1744      return cleanExtension;
1745   }
1746
1747   // -----------------------------------------------------------------------
1748   // Browser-assisted image loading for formats the Basis library can't
1749   // decode natively (WebP, AVIF, GIF, BMP, ...).
1750   // The browser decodes the image and we extract raw 32-bit RGBA pixels,
1751   // which the encoder accepts via its cRGBA32 image type.
1752   // Only used for LDR/SDR content. HDR files (.EXR, .HDR) must be loaded
1753   // by the library directly.
1754   // -----------------------------------------------------------------------
1755
1756   // Extensions the Basis Universal library decodes internally.
1757   const BASISU_NATIVE_EXTENSIONS = new Set([
1758      'png', 'jpg', 'jpeg', 'jfif',   // LDR
1759      'qoi',                           // LDR (decoded by the library)
1760      'exr', 'hdr',                    // HDR
1761      'ktx2'                           // handled separately
1762   ]);
1763
1764   // Map extensions the *browser* can typically decode to their MIME types.
1765   const BROWSER_IMAGE_MIME_TYPES = {
1766      'webp': 'image/webp',
1767      'avif': 'image/avif',
1768      'bmp':  'image/bmp',
1769      'gif':  'image/gif',
1770      'tif':  'image/tiff',
1771      'tiff': 'image/tiff',
1772      'jxl':  'image/jxl',
1773      'heic': 'image/heic',
1774      'heif': 'image/heif',
1775   };
1776
1777   // Returns true when the file extension is NOT handled by the Basis
1778   // library but IS a recognized image format the browser might decode.
1779   function needsBrowserDecode(ext)
1780   {
1781      if (!ext) return false;
1782      var lower = ext.toLowerCase();
1783      return !BASISU_NATIVE_EXTENSIONS.has(lower) && (lower in BROWSER_IMAGE_MIME_TYPES);
1784   }
1785
1786   // Decode an image file's ArrayBuffer through the browser's built-in
1787   // codecs and return raw 32-bit RGBA pixel data suitable for the
1788   // encoder's cRGBA32 mode.
1789   //
1790   // Returns a Promise resolving to { rgbaU8: Uint8Array, width, height }.
1791   //
1792   // We use a throw-away WebGL context for pixel read-back instead of
1793   // Canvas 2D because Canvas 2D always premultiplies alpha internally,
1794   // which destroys RGB values on semi-transparent pixels.  The WebGL
1795   // path keeps straight alpha throughout so the output is bit-exact.
1796   //
1797   // Per the WebGL spec, UNPACK_PREMULTIPLY_ALPHA_WEBGL and
1798   // UNPACK_FLIP_Y_WEBGL are IGNORED for ImageBitmap uploads --
1799   // alpha and orientation must be set via createImageBitmap() options.
1800   function browserDecodeToRGBA(arrayBuffer, fileName)
1801   {
1802      var ext = getFileExtension(fileName);
1803      var mime = BROWSER_IMAGE_MIME_TYPES[ext] || ('image/' + ext);
1804      var blob = new Blob([arrayBuffer], { type: mime });
1805
1806      // premultiplyAlpha:'none' -> straight alpha, lossless.
1807      return createImageBitmap(blob, {
1808         premultiplyAlpha: 'none'
1809      }).then(function (bitmap)
1810      {
1811         var w = bitmap.width;
1812         var h = bitmap.height;
1813
1814         if (!w || !h)
1815         {
1816            bitmap.close();
1817            throw 'Browser decoded a 0\u00d70 image from ' + fileName;
1818         }
1819
1820         var canvas = document.createElement('canvas');
1821         canvas.width = w;
1822         canvas.height = h;
1823         var gl = canvas.getContext('webgl', {
1824            alpha: true,
1825            premultipliedAlpha: false
1826         });
1827         if (!gl)
1828         {
1829            bitmap.close();
1830            throw 'Could not create WebGL context for pixel read-back';
1831         }
1832
1833         var tex = gl.createTexture();
1834         gl.bindTexture(gl.TEXTURE_2D, tex);
1835         gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, bitmap);
1836         bitmap.close();
1837
1838         var fb = gl.createFramebuffer();
1839         gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
1840         gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
1841
1842         var pixels = new Uint8Array(w * h * 4);
1843         gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
1844
1845         gl.deleteFramebuffer(fb);
1846         gl.deleteTexture(tex);
1847         var lose = gl.getExtension('WEBGL_lose_context');
1848         if (lose) lose.loseContext();
1849
1850         return { rgbaU8: pixels, width: w, height: h };
1851      });
1852   }
1853
1854   function getETC1SCompLevel()
1855   {
1856      var slider = document.getElementById('etc1s-comp-level-slider'); // Get the slider element
1857      var compLevel = parseInt(slider.value, 10);  // Convert the slider value to an integer
1858      return compLevel;
1859   }
1860
1861   // UASTC HDR effort (it's misnamed "quality" in the API for backwards compat)
1862   function getUASTCHDRQuality()
1863   {
1864      var slider = document.getElementById('uastc-hdr-quality-slider'); // Get the slider element
1865      var qualityLevel = parseInt(slider.value, 10);  // Convert the slider value to an integer
1866      return qualityLevel;
1867   }
1868
1869   function getASTCHDR6x6CompLevel()
1870   {
1871      var slider = document.getElementById('astc-hdr6x6-comp-level-slider');
1872      var compLevel = parseInt(slider.value, 10);  // Convert the slider value to an integer
1873      return compLevel;
1874   }
1875
1876   function getUASTCLDRQuality()
1877   {
1878      var slider = document.getElementById('uastc-ldr-quality-slider'); // Get the slider element
1879      var qualityLevel = parseInt(slider.value, 10);  // Convert the slider value to an integer
1880      return qualityLevel;
1881   }
1882
1883   function getUASTCLDRRDOQuality()
1884   {
1885      var rdoSlider = document.getElementById('rdo-quality-slider'); // Get the slider element
1886      var rdoQuality = parseFloat(rdoSlider.value);  // Convert the slider value to a floating-point number
1887      return rdoQuality;  // Return the current value
1888   }
1889
1890   function getXUASTCLDREffortLevel()
1891   {
1892      var slider = document.getElementById('xuastc_ldr_effort_level_slider');
1893      var effortLevel = parseInt(slider.value, 10);  // Convert the slider value to an integer
1894
1895      return effortLevel;
1896   }
1897
1898   function getXUBC7RDOLevel()
1899   {
1900      var slider = document.getElementById('xubc7_rdo_level_slider');
1901      var rdoLevel = parseInt(slider.value, 10);  // [0,100], 0 = no RDO
1902      return rdoLevel;
1903   }
1904
1905   function getXUBC7NumStripes()
1906   {
1907      var slider = document.getElementById('xubc7_num_stripes_slider');
1908      var numStripes = parseInt(slider.value, 10);  // [1,16]
1909      return numStripes;
1910   }
1911
1912   function getXUBC7Encoder()
1913   {
1914      var sel = document.getElementById('xubc7_encoder_select');
1915      return parseInt(sel.value, 10);  // 0 = bc7f, 1 = bc7e_scalar
1916   }
1917
1918   function getXUBC7BC7EScalarLevel()
1919   {
1920      var slider = document.getElementById('xubc7_bc7e_scalar_level_slider');
1921      return parseInt(slider.value, 10);  // [0,6]
1922   }
1923
1924   // Snapshot ALL encode inputs into a plain, structured-clone-able DTO for the
1925   // background encode worker (see the encode-worker migration). Every enum is
1926   // resolved to an int here on the main thread -- the worker's separate module
1927   // instance uses the SAME build, so the int values are identical and the worker
1928   // never needs an enum object. The caller passes the already-resolved formatMode
1929   // (the HDR-source override + dropdown mutation stays on the main thread) plus
1930   // the source context. The source bytes are referenced as-is: posting the DTO
1931   // WITHOUT a transfer list structure-clones (copies) them, so the retained
1932   // curLoadedImageData / curLoadedBrowserRGBA stay valid for re-encodes.
1933   //
1934   // This mirrors the inline encode path's reads exactly (same elem()/getX()
1935   // helpers), so the two stay value-for-value identical. Currently UNWIRED --
1936   // it's the payload builder for the worker path; the legacy path is untouched.
1937   // Reads the 4 channel-swizzle inputs (the source channel index for each output channel R,G,B,A),
1938   // each clamped to an integer in [0,3]. Default is identity [0,1,2,3] (no swizzle).
1939   function getSwizzle()
1940   {
1941      function s(id) { return Math.max(0, Math.min(3, parseInt(elem(id).value, 10) || 0)); }
1942      return [s('swizzle_r'), s('swizzle_g'), s('swizzle_b'), s('swizzle_a')];
1943   }
1944
1945   function buildEncodeDTO(formatMode, extension, fileData, browserDecodedRGBA, isHDRSourceFile)
1946   {
1947      const isHDRTarget = Module.isBasisTexFormatHDR(formatMode);
1948
1949      // ---- source image: pre-decoded RGBA, or raw file bytes the encoder
1950      //      decodes itself (img_type tells it the format; dims 0,0) ----
1951      let source;
1952      if (browserDecodedRGBA)
1953      {
1954         // browser-decoded (WebP/AVIF/etc.): always LDR 8-bit RGBA
1955         source = {
1956            isPreDecodedRGBA: true,
1957            isHDRTarget: isHDRTarget,
1958            bytes: browserDecodedRGBA.rgbaU8,
1959            width: browserDecodedRGBA.width,
1960            height: browserDecodedRGBA.height,
1961            imgType: isHDRTarget ? Module.hdr_image_type.cHITRGBA8Image.value
1962                                 : Module.ldr_image_type.cRGBA32.value,
1963            convertLDRToLinear: elem('ConvertLDRToLinear').checked,
1964            nitMultiplier: getNitMultiplier()
1965         };
1966      }
1967      else if (isHDRTarget)
1968      {
1969         let imgType = Module.hdr_image_type.cHITPNGImage.value;
1970         if (extension === "exr") imgType = Module.hdr_image_type.cHITEXRImage.value;
1971         else if (extension === "hdr") imgType = Module.hdr_image_type.cHITHDRImage.value;
1972         else if (extension === "jpg" || extension === "jpeg" || extension === "jfif") imgType = Module.hdr_image_type.cHITJPGImage.value;
1973         else if (extension === "qoi") imgType = Module.hdr_image_type.cHITQOIImage.value;
1974         source = {
1975            isPreDecodedRGBA: false, isHDRTarget: true,
1976            bytes: new Uint8Array(fileData), width: 0, height: 0, imgType: imgType, // matches legacy's new Uint8Array(data)
1977            convertLDRToLinear: elem('ConvertLDRToLinear').checked,
1978            nitMultiplier: getNitMultiplier()
1979         };
1980      }
1981      else
1982      {
1983         let imgType = Module.ldr_image_type.cPNGImage.value;
1984         if (extension === "jpg" || extension === "jpeg" || extension === "jfif") imgType = Module.ldr_image_type.cJPGImage.value;
1985         else if (extension === "qoi") imgType = Module.ldr_image_type.cQOIImage.value;
1986         source = {
1987            isPreDecodedRGBA: false, isHDRTarget: false,
1988            bytes: new Uint8Array(fileData), width: 0, height: 0, imgType: imgType, // matches legacy's new Uint8Array(data)
1989            convertLDRToLinear: false, nitMultiplier: 1.0
1990         };
1991      }
1992
1993      const sRGB_flag = elem('SRGB').checked;
1994
1995      return {
1996         source: source,
1997
1998         // container / threading
1999         multithreaded: elem('multithreaded_encoding').checked,
2000         numWorkerThreads: getNumWorkerThreads(),
2001         createKTX2: true,
2002         ktx2UASTCSupercompression: true,
2003
2004         // color / transfer
2005         sRGB: sRGB_flag,
2006         isHDRSourceFile: isHDRSourceFile, // gates setKTX2AndBasisSRGBTransferFunc
2007         rec2020: elem('Rec2020').checked,
2008
2009         // format + quality/effort
2010         formatMode: formatMode,
2011         isXUASTCLDRTarget: Module.isBasisTexFormatXUASTCLDR(formatMode), // resolved here so applyEncodeDTO stays Module-free
2012         isXUBC7Target: (formatMode === Module.basis_tex_format.cXUBC7.value),
2013         // Two INDEPENDENT signals, mirroring the legacy path exactly:
2014         //  lowLevelOptsEnabled = run the low-level codec block (legacy: unified checkbox OFF)
2015         //  applyUnified        = run setFormatModeAndQualityEffort (legacy: checkbox ON *or* XUBC7 forces it)
2016         // Both can be true at once (XUBC7 + checkbox-off: low-level block runs, then unified overrides).
2017         lowLevelOptsEnabled: !elem('use_new_style_quality_effort').checked,
2018         applyUnified: elem('use_new_style_quality_effort').checked || (formatMode === Module.basis_tex_format.cXUBC7.value),
2019         unifiedQuality: parseInt(document.getElementById('unified-quality-slider').value, 10),
2020         unifiedEffort: parseInt(document.getElementById('unified-effort-slider').value, 10),
2021
2022         // low-level codec opts (applied only when lowLevelOptsEnabled, i.e. unified checkbox off)
2023         uastcHDRQuality: getUASTCHDRQuality(),
2024         astcHDR6x6Level: getASTCHDR6x6CompLevel(),
2025         astc6x6Lambda: getASTC6x6RDOLambda(),
2026         xuastcDCTQuality: getXUASTCLDRDCTQuality(), // worker: setQualityLevel+UseDCT when <100
2027         etc1sQuality: parseInt(elem('EncodeQuality').value, 10),
2028         xuastcLossySupercompression: elem('XUASTCLossySupercompression').checked,
2029         astcXuastcEffortLevel: getXUASTCLDREffortLevel(),
2030         rdoUASTC: elem('UASTC_LDR_RDO').checked,
2031         rdoUASTCQualityScalar: getUASTCLDRRDOQuality(),
2032         packUASTCFlags: getUASTCLDRQuality(),
2033         etc1sCompLevel: getETC1SCompLevel(),
2034
2035         // always-applied opts (regardless of the unified/low-level toggle)
2036         xuastcBoundedRDO: [
2037            getSafeFloat("xuastc_ldr_min_psnr"), getSafeFloat("xuastc_ldr_min_alpha_psnr"),
2038            getSafeFloat("xuastc_ldr_thresh_psnr"), getSafeFloat("xuastc_ldr_thresh_alpha_psnr"),
2039            getSafeFloat("xuastc_ldr_thresh_edge_psnr"), getSafeFloat("xuastc_ldr_thresh_edge_alpha_psnr")
2040         ],
2041         xuastcDisableRGBDualPlane: elem('XUASTCDisableRGBDualPlane').checked,
2042         xuastcDisableSubsets: elem('XUASTCDisableSubsets').checked,
2043         xuastcUseBlur: elem('XUASTCUseBlur').checked,
2044         xuastcSelectCompressor: Module.xuastc_ldr_astc_comp_selection[elem('XUASTCLDRCompressor').value].value,
2045         xuastcHeavySubsetUsage: elem('XUASTCHeavySubsetUsage').checked,
2046         xuastcSharpenMode: Number(document.getElementById("XUASTCSharpenMode").value),
2047         xuastcSharpenAmount: parseFloat(elem('XUASTCSharpenAmount').value) || 0.0,
2048         xuastcDeblockingMode: Number(document.getElementById("encoding_deblocking_filter_select").value),
2049         xuastcNumDeblockingPasses: getDeblockingNumPasses(),
2050         weights: [
2051            Math.max(1, Math.min(32, parseInt(elem('xuastc_ldr_weight_r').value, 10) || 1)),
2052            Math.max(1, Math.min(32, parseInt(elem('xuastc_ldr_weight_g').value, 10) || 1)),
2053            Math.max(1, Math.min(32, parseInt(elem('xuastc_ldr_weight_b').value, 10) || 1)),
2054            Math.max(1, Math.min(32, parseInt(elem('xuastc_ldr_weight_a').value, 10) || 1))
2055         ],
2056         swizzle: getSwizzle(),
2057         xuastcSyntax: getXUASTCLDRSyntax(),
2058         xubc7RDOLevel: getXUBC7RDOLevel(),
2059         xubc7NumStripes: getXUBC7NumStripes(),
2060         xubc7Encoder: getXUBC7Encoder(),
2061         xubc7BC7EScalarLevel: getXUBC7BC7EScalarLevel(),
2062
2063         // mipmaps + debug
2064         mipGen: elem('Mipmaps').checked,
2065         mipFilter: parseInt(elem('mip-filter-select').value, 10),
2066         mipScale: parseFloat(elem('mip-scale-input').value) || 1.0,
2067         mipSmallestDim: parseInt(elem('mip-smallest-dim-input').value, 10) || 1,
2068         mipRenormalize: elem('MipRenormalize').checked,
2069         mipWrapping: elem('MipWrapping').checked,
2070         yFlip: elem('YFlip').checked,
2071         debug: elem('Debug').checked,
2072         computeStats: elem('ComputeStats').checked,
2073         printStats: elem('PrintStats').checked
2074      };
2075   }
2076
2077   // applyEncodeDTO(encoder, dto) now lives in the shared file encode_dto_apply.js
2078   // (loaded via 
2078<script src> on the page and importScripts() in the encode worker,
2079   // so there is a single definition for both the non-worker and worker paths).
2080
2081   // ENCODE WORKER (persistent singleton). The threaded encoder module is created
2082   // and initialized ONCE (lazily, on first use) and reused for every encode -- the
2083   // module load is costly, so we never recreate it (and a future cancel will be a
2084   // synchronous in-encoder callback, not a respawn). encodeViaWorker() builds a DTO
2085   // from the currently loaded source + settings, sends it to the worker, and when
2086   // the worker returns the encoded KTX2 bytes the page processes them through the
2087   // SAME path as a normal encode (store + transcodeTexture + UI update).
2088   let g_encodeWorker = null;         // persistent worker (null until first use)
2089   let g_encodeWorkerLoaded = false;  // module loaded + initialized in the worker
2090   let g_encodeWorkerOnReady = null;  // fired once when the worker becomes ready
2091   let g_encodeWorkerWatchdog = null; // load-time hang detector only
2092
2093   // The worker can flood us with print lines (esp. with debug on). Buffer them and
2094   // flush to the DOM at most every ~100ms instead of writing on every message.
2095   let g_encodeBusyLogBuffer = '';
2096   let g_encodeBusyFlushTimer = null;
2097
2098   // ---- worker-encode busy modal (truly modal via <dialog>.showModal()) ----
2099   function showEncodeBusy()
2100   {
2101      const dlg = document.getElementById('encode-busy-dialog');
2102      const prog = document.getElementById('encode-busy-progress');
2103      const logEl = document.getElementById('encode-busy-log');
2104      const closeBtn = document.getElementById('encode-busy-close');
2105      const autoCloseChk = document.getElementById('encode-busy-autoclose');
2106      const autoCloseDefault = document.getElementById('encode-modal-autoclose');
2107      if (prog) prog.removeAttribute('value'); // indeterminate "busy" bar (set .value later for real %)
2108      if (logEl) logEl.textContent = '';        // clear the previous run's output
2109      g_encodeBusyLogBuffer = '';               // drop any leftover buffered text
2110      if (g_encodeBusyFlushTimer !== null) { clearTimeout(g_encodeBusyFlushTimer); g_encodeBusyFlushTimer = null; }
2111      if (closeBtn) closeBtn.disabled = true;   // grayed out (and ESC blocked) until the encode finishes
2112      if (autoCloseChk && autoCloseDefault) autoCloseChk.checked = autoCloseDefault.checked; // seed from the page default
2113      if (dlg && !dlg.open) dlg.showModal();
2114   }
2115   // Write the buffered text to the log in one go (called on a ~100ms timer and on finish).
2116   function flushEncodeBusyLog()
2117   {
2118      if (g_encodeBusyFlushTimer !== null) { clearTimeout(g_encodeBusyFlushTimer); g_encodeBusyFlushTimer = null; }
2119      if (!g_encodeBusyLogBuffer) return;
2120      const logEl = document.getElementById('encode-busy-log');
2121      if (logEl)
2122      {
2123         // Only auto-scroll if the user is already at (or near) the bottom; if they've scrolled
2124         // up to read earlier text, leave their position alone so new output doesn't yank it.
2125         const atBottom = (logEl.scrollHeight - logEl.scrollTop - logEl.clientHeight) <= 4;
2126         logEl.textContent += g_encodeBusyLogBuffer;
2127         if (atBottom)
2128            logEl.scrollTop = logEl.scrollHeight; // follow the latest line
2129      }
2130      g_encodeBusyLogBuffer = '';
2131   }
2132   function appendEncodeBusyLog(text)
2133   {
2134      // Buffer the line; schedule a single flush ~100ms out (coalesces a flood into
2135      // ~10 DOM updates per second).
2136      g_encodeBusyLogBuffer += text + '\n';
2137      if (g_encodeBusyFlushTimer === null)
2138         g_encodeBusyFlushTimer = setTimeout(flushEncodeBusyLog, 100);
2139   }
2140   // Called when a worker encode finishes (success or error). Default: auto-close.
2141   // If the user UNchecked "Automatically close when done", leave the dialog up with
2142   // an OK button so they can study the log (useful for long/debug encodes).
2143   function finishEncodeBusy()
2144   {
2145      document.body.style.cursor = ''; // clear the DDS busy cursor (no-op if it wasn't set)
2146
2147      flushEncodeBusyLog(); // drain any buffered tail so the full log is visible
2148
2149      // Encode finished: enable the Close button (+ allow ESC) and stop the busy bar.
2150      const closeBtn = document.getElementById('encode-busy-close');
2151      const prog = document.getElementById('encode-busy-progress');
2152      if (closeBtn) closeBtn.disabled = false;
2153      if (prog) { prog.max = 1; prog.value = 1; } // show "done" (full, stops the busy animation)
2154
2155      const autoCloseChk = document.getElementById('encode-busy-autoclose');
2156      if (!autoCloseChk || autoCloseChk.checked)
2157         hideEncodeBusy(); // default behaviour: close automatically
2158      // else: leave the dialog up; the now-enabled Close button (or ESC) dismisses it
2159   }
2160   function hideEncodeBusy()
2161   {
2162      document.body.style.cursor = ''; // ensure the busy cursor is cleared on every close path (watchdog/onerror/manual)
2163      const dlg = document.getElementById('encode-busy-dialog');
2164      if (dlg && dlg.open) dlg.close();
2165   }
2166
2167   // Process KTX2 bytes from an encode exactly like the normal encode button:
2168   // store them (download button reads encodedKTX2File) + transcode + update the UI.
2169   function handleEncodedKTX2(ktx2Bytes)
2170   {
2171      const n = ktx2Bytes ? ktx2Bytes.length : 0;
2172      if (n === 0)
2173      {
2174         updateErrorLine('Image is invalid or unsupported, or it may be too large to compress in WASM without risking running out of memory.');
2175         log('encodeBasisTexture() failed!');
2176         return;
2177      }
2178      log('encodeBasisTexture() succeeded, output size ' + n);
2179      encodedKTX2File = ktx2Bytes;   // same global the legacy path sets
2180      transcodeTexture(ktx2Bytes);   // same transcode + display + UI as a normal encode
2181   }
2182
2183   // Process .DDS bytes from a worker DDS-encode: display them through the SAME path a
2184   // loaded-from-disk .dds takes (transcodeDDSTexture), so the result appears exactly as if
2185   // the user had just opened this .dds file. Optionally also downloads it.
2186   function handleEncodedDDS(ddsBytes)
2187   {
2188      const n = ddsBytes ? ddsBytes.length : 0;
2189      if (n === 0)
2190      {
2191         updateErrorLine('DDS encode produced no data — the source or chosen format may be unsupported.');
2192         log('encodeToDDS() failed!');
2193         return;
2194      }
2195      log('encodeToDDS() succeeded, output size ' + n);
2196
2197      const fmtName = ddsEncodeFormatName();
2198      const filename = 'encoded_' + fmtName + '.dds';
2199
2200      // Display it through the SAME well-tested path a loaded-from-disk .dds takes. The result also
2201      // becomes the current .dds source, so the "Download Encoded .KTX2/.DDS File" button saves it.
2202      transcodeDDSTexture(ddsBytes, filename);
2203
2204      // Optionally auto-download the encoded .dds (the "Download encoded .DDS file when done" checkbox).
2205      var dl = document.getElementById('dds-encode-download');
2206      if (dl && dl.checked)
2207         download_file(filename, ddsBytes);
2208   }
2209
2210   // Create the worker + load the module ONCE; call onReady() when ready (immediately
2211   // if already loaded). The worker is never terminated.
2212   function ensureEncodeWorker(onReady)
2213   {
2214      if (g_encodeWorker && g_encodeWorkerLoaded) { onReady(); return; }
2215
2216      g_encodeWorkerOnReady = onReady; // latest caller wins if clicked again mid-load
2217
2218      if (g_encodeWorker) return; // already created and still loading; onReady fires on 'loaded'
2219
2220      const scriptSrc = (window.wasm64 && window.isThreaded) ? "../encoder/build/basis_encoder_threads_wasm64.js" :
2221                        (window.isThreaded ? "../encoder/build/basis_encoder_threads.js" : "../encoder/build/basis_encoder.js");
2222
2223      console.log("encodeWorker: creating worker (one-time), scriptSrc=" + scriptSrc);
2224
2225      const t0 = performance.now();
2226      g_encodeWorker = new Worker('encode-worker.js');
2227
2228      g_encodeWorkerWatchdog = setTimeout(function ()
2229      {
2230         hideEncodeBusy();
2231         console.warn('encodeWorker: no loaded/error after 20s -- likely a pthread-spawn hang (check mainScriptUrlOrBlob / COOP+COEP)');
2232      }, 20000);
2233
2234      const clearWatchdog = function () { if (g_encodeWorkerWatchdog) { clearTimeout(g_encodeWorkerWatchdog); g_encodeWorkerWatchdog = null; } };
2235
2236      g_encodeWorker.onmessage = function (e)
2237      {
2238         const msg = e.data || {};
2239         if (msg.type === 'loaded')
2240         {
2241            clearWatchdog();
2242            g_encodeWorkerLoaded = true;
2243            console.log('%cencodeWorker: ' + msg.text + '  (' + (performance.now() - t0).toFixed(0) + ' ms)', 'color:#0a0;font-weight:bold');
2244            if (g_encodeWorkerOnReady) { const cb = g_encodeWorkerOnReady; g_encodeWorkerOnReady = null; cb(); }
2245         }
2246         else if (msg.type === 'print')
2247         {
2248            // worker stdout/stderr -> live modal log + console (streams during encode)
2249            appendEncodeBusyLog(msg.text);
2250            console.log(msg.text);
2251         }
2252         else if (msg.type === 'encoded')
2253         {
2254            console.log('%cencodeWorker: ' + msg.text, 'color:#0a0;font-weight:bold');
2255
2256            // mirror the main-thread path's UI updates so the display is identical:
2257            // encode time + mip0 PSNR feed the description string built in transcodeTexture.
2258            if (typeof msg.encodeMs === 'number') { g_lastEncodeTime = msg.encodeMs; logTime('encoding time', msg.encodeMs.toFixed(2)); }
2259            if (typeof msg.psnr === 'number') g_lastEncodeMip0RGBAPSNR = msg.psnr;
2260
2261            const bytes = new Uint8Array(msg.bytes); // KTX2 file, transferred back from the worker
2262            handleEncodedKTX2(bytes);                // process like a normal encode (sets encodedKTX2File + transcodeTexture)
2263
2264            finishEncodeBusy();                      // auto-close, or keep open if the user asked to
2265         }
2266         else if (msg.type === 'encodedDDS')
2267         {
2268            console.log('%cencodeWorker: ' + msg.text, 'color:#0a0;font-weight:bold');
2269
2270            if (typeof msg.encodeMs === 'number') { g_lastEncodeTime = msg.encodeMs; logTime('DDS encoding time', msg.encodeMs.toFixed(2)); }
2271
2272            const bytes = new Uint8Array(msg.bytes); // .DDS file, transferred back from the worker
2273            // finishEncodeBusy() MUST run even if the display (transcodeDDSTexture) throws, so the busy
2274            // cursor + modal are always cleared no matter how the encode/display turns out.
2275            try { handleEncodedDDS(bytes); }         // display it like a loaded .dds (+ optional download)
2276            finally { finishEncodeBusy(); }          // auto-close, or keep open if the user asked to
2277         }
2278         else if (msg.type === 'error')
2279         {
2280            clearWatchdog();
2281            appendEncodeBusyLog('ERROR: ' + msg.text);
2282            console.error('encodeWorker: ' + msg.text);
2283            // Surface the same on-page failure message the main-thread path shows, so a worker-side
2284            // encode failure (e.g. a bogus/unsupported image) isn't silent in the UI.
2285            updateErrorLine('Image is invalid or unsupported, or it may be too large to compress in WASM without risking running out of memory.');
2286            log('encodeBasisTexture() failed!');
2287            finishEncodeBusy();
2288         }
2289         else
2290            console.log('encodeWorker: ' + msg.text);
2291      };
2292
2293      g_encodeWorker.onerror = function (err)
2294      {
2295         clearWatchdog();
2296         hideEncodeBusy();
2297         console.error('encodeWorker: worker.onerror: ' + err.message + ' @ ' + err.filename + ':' + err.lineno);
2298      };
2299
2300      g_encodeWorker.postMessage({ type: 'init', scriptSrc: scriptSrc });
2301   }
2302
2303   // Worker-encode the CURRENTLY loaded source (curLoaded* must be set): build the
2304   // DTO, show the modal, and post to the persistent worker. Shared by the "Encode!"
2305   // button (already-loaded case) and the auto-load path (compressImage routes here
2306   // for worker mode).
2307   function encodeViaWorkerCore()
2308   {
2309      if (!curLoadedImageData && !curLoadedBrowserRGBA)
2310      {
2311         console.warn('encodeViaWorkerCore: no source loaded');
2312         return;
2313      }
2314
2315      // Match the main encode path: optionally clear the devtools console first.
2316      const clearConsoleEl = document.getElementById('clear-console-on-encode');
2317      if (clearConsoleEl && clearConsoleEl.checked && typeof console.clear === 'function')
2318         console.clear();
2319
2320      const ext = curLoadedImageURI ? getFileExtension(curLoadedImageURI) : null;
2321      const isHDRSrc = (ext != null) && (ext === "exr" || ext === "hdr");
2322
2323      // Same HDR-source override as the main path: an HDR source file forces a HDR
2324      // format (and updates the dropdown).
2325      let fmt = getSelectedBasisTexFormat();
2326      if (Module.isBasisTexFormatLDR(fmt) && isHDRSrc)
2327      {
2328         fmt = Module.basis_tex_format.cUASTC_HDR_4x4.value;
2329         setDropdownValue('basis-tex-format', fmt);
2330      }
2331
2332      const dto = buildEncodeDTO(fmt, ext, curLoadedImageData, curLoadedBrowserRGBA, isHDRSrc);
2333
2334      showEncodeBusy(); // modal up for the whole load(if first)+encode; hidden on encoded/error
2335
2336      ensureEncodeWorker(function ()
2337      {
2338         console.log('encodeViaWorker: sending DTO to worker (formatMode=' + fmt + ')...');
2339         g_encodeWorker.postMessage({ type: 'encode', dto: dto });
2340      });
2341   }
2342
2343   // "Encode!" button. Mirrors runEncodeImageFile: if a source is already loaded,
2344   // worker-encode it; otherwise load the file named in the imagefile box (e.g. the
2345   // default assets/kodim06.png on first run) and then worker-encode it.
2346   function encodeViaWorker()
2347   {
2348      if ((elem('imagefile').value === '<externally loaded>') && (curLoadedImageData != null))
2349      {
2350         encodeViaWorkerCore();
2351      }
2352      else
2353      {
2354         loadArrayBufferFromURI(elem('imagefile').value, function (d, u) { loadAndCompressImage(d, u, 'worker'); }, dataLoadError);
2355      }
2356   }
2357
2358   // --- DDS export (direct source -> .DDS, on the worker; mirrors the KTX2 worker path) ---
2359
2360   // Resolve the selected DDS output format dropdown (member name -> numeric dds_output_format).
2361   function getDDSEncodeFormatValue()
2362   {
2363      const name = document.getElementById('dds-encode-format').value; // e.g. "cDDSFmtBC7"
2364      if (Module.dds_output_format && Module.dds_output_format[name])
2365         return Module.dds_output_format[name].value;
2366      return -1; // cDDSFmtInvalid
2367   }
2368
2369   // Canonical token string for the selected format (for the output filename / log).
2370   function ddsEncodeFormatName()
2371   {
2372      const v = getDDSEncodeFormatValue();
2373      if ((v >= 0) && Module.getDDSOutputFormatString)
2374         return Module.getDDSOutputFormatString(v);
2375      return 'dds';
2376   }
2377
2378   // Worst-case .DDS output buffer size: the page only ever encodes a single 2D slice, and the
2379   // encoder caps total source texels (BASISU_ENCODER_MAX_SOURCE_IMAGE_PIXELS_HIGHER_LIMIT). The
2380   // largest possible output is 32bpp uncompressed + a full mip chain (~4/3x); 1.5x + 64KB gives
2381   // comfortable margin. This is a plain JS ArrayBuffer (not WASM memory), so it's cheap to allocate.
2382   function computeDDSBufferSize()
2383   {
2384      const maxTexels = window.wasm64 ? (16 * 1024 * 1024) : (12 * 1024 * 1024);
2385      return Math.ceil(maxTexels * 4 * 1.5) + 65536;
2386   }
2387
2388   // Worker-encode the CURRENTLY loaded source to .DDS (curLoaded* must be set). Builds the DTO
2389   // (reusing buildEncodeDTO), augments it with the DDS options, shows the modal, posts to the worker.
2390   function encodeDDSViaWorkerCore()
2391   {
2392      if (!curLoadedImageData && !curLoadedBrowserRGBA)
2393      {
2394         console.warn('encodeDDSViaWorkerCore: no source loaded');
2395         return;
2396      }
2397
2398      const clearConsoleEl = document.getElementById('clear-console-on-encode');
2399      if (clearConsoleEl && clearConsoleEl.checked && typeof console.clear === 'function')
2400         console.clear();
2401
2402      const ext = curLoadedImageURI ? getFileExtension(curLoadedImageURI) : null;
2403      const isHDRSrc = (ext != null) && (ext === "exr" || ext === "hdr");
2404
2405      // DDS export is LDR-only for now (HDR DDS formats / build_dds HDR path are a future addition).
2406      if (isHDRSrc)
2407      {
2408         updateErrorLine('DDS export is LDR-only for now; load an LDR image (PNG/JPG/WebP/etc).');
2409         return;
2410      }
2411
2412      const ddsFormat = getDDSEncodeFormatValue();
2413      if (ddsFormat < 0)
2414      {
2415         updateErrorLine('Please choose a valid DDS output format.');
2416         return;
2417      }
2418
2419      // Pass a plain LDR format mode to buildEncodeDTO so the source is treated as LDR. encodeToDDS()
2420      // chooses the real prep format internally (and restores it after), so this only affects how the
2421      // DTO selects the source image type (LDR vs HDR) -- not the actual DDS output.
2422      const ldrFmt = Module.basis_tex_format.cUASTC_LDR_4x4.value;
2423
2424      const dto = buildEncodeDTO(ldrFmt, ext, curLoadedImageData, curLoadedBrowserRGBA, false);
2425
2426      // DDS-specific fields consumed by applyDDSEncodeDTO() / the worker.
2427      dto.ddsFormat          = ddsFormat;
2428      dto.ddsBC7Encoder      = parseInt(document.getElementById('dds-encode-bc7-encoder').value, 10);
2429      dto.ddsBC7FLevel       = parseInt(document.getElementById('dds-encode-bc7f-level').value, 10);
2430      dto.ddsBC7EScalarLevel = parseInt(document.getElementById('dds-encode-bc7e-level').value, 10);
2431      dto.ddsBufferSize      = computeDDSBufferSize();
2432
2433      // Busy cursor for the whole DDS create (worker encode + the brief main-thread display that
2434      // follows). Cleared in finishEncodeBusy()/hideEncodeBusy() so it can never get stuck.
2435      document.body.style.cursor = 'wait';
2436
2437      showEncodeBusy(); // same modal/log window as the KTX2 encode
2438
2439      ensureEncodeWorker(function ()
2440      {
2441         console.log('encodeDDSViaWorker: sending DDS DTO to worker (ddsFormat=' + dto.ddsFormat + ')...');
2442         g_encodeWorker.postMessage({ type: 'encodeDDS', dto: dto });
2443      });
2444   }
2445
2446   // "Encode DDS!" button. Mirrors encodeViaWorker(): if a source image is already loaded, DDS-encode
2447   // it; otherwise load the file named in the imagefile box and then DDS-encode it.
2448   function encodeDDSViaWorker()
2449   {
2450      if ((elem('imagefile').value === '<externally loaded>') && (curLoadedImageData != null))
2451      {
2452         encodeDDSViaWorkerCore();
2453      }
2454      else
2455      {
2456         loadArrayBufferFromURI(elem('imagefile').value, function (d, u) { loadAndCompressImage(d, u, 'ddsworker'); }, dataLoadError);
2457      }
2458   }
2459
2460   function getXUASTCLDRDCTQuality()
2461   {
2462      var slider = document.getElementById('xuastc_ldr_dct_quality_slider');
2463      var effortLevel = parseInt(slider.value, 10);  // Convert the slider value to an integer
2464      return effortLevel;
2465   }
2466
2467   function getDeblockingNumPasses()
2468   {
2469      var slider = document.getElementById('deblocking-num-passes-slider');
2470      var n = parseInt(slider.value, 10);
2471      if (!isFinite(n)) n = 16;
2472      // Clamp to declared range [2, 256].
2473      if (n < 2) n = 2;
2474      if (n > 256) n = 256;
2475      return n;
2476   }
2477   // True for Apple Safari (any version, desktop or iOS). Excludes other engines
2478   // whose UA string also contains "Safari" (Chrome, Edge, Firefox-on-iOS, etc.).
2479   function isSafari()
2480   {
2481      var ua = navigator.userAgent || "";
2482      if (/chrome|chromium|crios|android|fxios|edg\/|opr\//i.test(ua)) return false;
2483      return /safari/i.test(ua);
2484   }
2485
2486   // Detects Safari 26.0-26.2, which have a WebKit bug where growing a shared
2487   // (multithreaded) WASM heap corrupts memory -> "Out of bounds memory access"
2488   // crashes during encoding. Fixed in Safari 26.3 (see emscripten issue #25905).
2489   // There's no clean feature-test (the bug is silent until a heap grow), so we
2490   // sniff the Safari version from the user-agent string.
2491   function isBuggySafari()
2492   {
2493      if (!isSafari()) return false;
2494      var m = (navigator.userAgent || "").match(/version\/(\d+)\.(\d+)/i);
2495      if (!m) return false;
2496      var major = parseInt(m[1], 10), minor = parseInt(m[2], 10);
2497      return (major === 26) && (minor < 3);   // 26.0, 26.1, 26.2
2498   }
2499
2500   // Returns the number of extra worker threads the encoder should ACTUALLY use
2501   // (0 = single-threaded). BOTH encode paths -- the main-thread path and the
2502   // background worker (via the DTO) -- call this, so any override here applies to
2503   // both. On the buggy Safari versions we force single-threaded regardless of the
2504   // UI controls, to avoid the multithreaded heap-grow crash.
2505   function getNumWorkerThreads()
2506   {
2507      if (isBuggySafari())
2508         return 0;
2509
2510      var slider = document.getElementById('num_worker_threads');
2511      var num = parseInt(slider.value, 10);  // Convert the slider value to an integer
2512
2513      num = Math.floor(num);
2514
2515      if (num < 0)
2516         num = 0;
2517      else if (num > MAX_WORKER_THREADS)
2518         num = MAX_WORKER_THREADS;
2519
2520      return num;
2521   }
2522
2523   function getSafeFloat(id, defaultValue = 0)
2524   {
2525      const v = parseFloat(document.getElementById(id).value);
2526      return Number.isFinite(v) ? v : defaultValue;
2527   }
2528
2529   // Wrapper suitable as a loadArrayBufferFromURI callback.
2530   // Checks whether the loaded file needs browser-assisted decoding
2531   // and, if so, decodes first before calling compressImage.
2532   // mode (optional): 'main' / 'worker' / undefined(=auto) -- forwarded to
2533   // compressImage to pick the encode path. Omitted on normal loads (auto: respects
2534   // the "Encode on worker thread by default" checkbox).
2535   function loadAndCompressImage(data, uri, mode)
2536   {
2537      var ext = getFileExtension(uri);
2538      if (needsBrowserDecode(ext))
2539      {
2540         log('Format ".' + ext + '" is not natively supported by Basis Universal; decoding via browser...');
2541
2542         browserDecodeToRGBA(data, uri).then(function (decoded)
2543         {
2544            log('Browser decode OK: ' + decoded.width + 'x' + decoded.height);
2545            compressImage(data, uri, decoded, mode);
2546         }).catch(function (err)
2547         {
2548            var msg = 'Failed to decode .' + ext + ' file. Your browser may not support this image format.';
2549            updateErrorLine(msg);
2550            log(msg);
2551            log('(' + err + ')');
2552         });
2553      }
2554      else
2555      {
2556         compressImage(data, uri, undefined, mode);
2557      }
2558   }
2559
2560   // mode: 'main' = force main-thread; 'worker' = force worker; undefined = auto
2561   // (respect the "Encode on worker thread by default" checkbox).
2562   function compressImage(data, uri, browserDecodedRGBA, mode)
2563   {
2564      // Dev option: clear the browser's devtools console on each new encode.
2565      // The null guard on the element and the function-type check on console.clear
2566      // are defensive — both should always be true in practice.
2567      const clearConsoleEl = document.getElementById('clear-console-on-encode');
2568      if (clearConsoleEl && clearConsoleEl.checked && typeof console.clear === 'function') {
2569         console.clear();
2570      }
2571      updateErrorLine("");
2572    
2573      logClear();
2574
2575      log('------');
2576      log('compressImage(): URI: ' + uri);
2577
2578      const { BasisFile, BasisEncoder, initializeBasis, encodeBasisTexture } = Module;
2579
2580      var extension = getFileExtension(uri);
2581
2582      resetDrawSettings();
2583
2584      initializeBasis();
2585
2586      curLoadedImageData = data;
2587      curLoadedImageURI = uri;
2588      curLoadedBrowserRGBA = browserDecodedRGBA || null;
2589      // An image is now the active source for encoding: drop any prior DDS source so a
2590      // failed encode (which never reaches transcodeTexture) can't leave the viewer stuck
2591      // in the DDS state and block re-encoding.
2592      curLoadedDDSData = null;
2593      curLoadedDDSURI = null;
2594      updateLoadedSourceDisplay();
2595
2596      // Route to the background worker for mode 'worker', or for auto (undefined)
2597      // when "Encode on worker thread by default" is checked. mode 'main' stays on
2598      // this (main-thread) path. curLoaded* are set above, so encodeViaWorkerCore()
2599      // has what it needs (it does its own HDR-source override + console clear).
2600      // DDS export goes through its own worker path (same machinery, encodeToDDS instead of encode).
2601      if (mode === 'ddsworker')
2602      {
2603         encodeDDSViaWorkerCore();
2604         return;
2605      }
2606
2607      const workerDefault = elem('encode-on-worker-default') && elem('encode-on-worker-default').checked;
2608      if ((mode === 'worker') || (mode !== 'main' && workerDefault))
2609      {
2610         encodeViaWorkerCore();
2611         return;
2612      }
2613
2614      // Create a destination buffer to hold the compressed .basis file data. If this buffer isn't large enough compression will fail.
2615      var ktx2FileData = new Uint8Array(1024 * 1024 * 24);
2616
2617      // Compress using the BasisEncoder class.
2618      log('BasisEncoder::encode() started:');
2619
2620      const basisEncoder = new BasisEncoder();
2621
2622      basisEncoder.controlThreading(elem('multithreaded_encoding').checked, getNumWorkerThreads());
2623
2624      var desiredBasisTexFormat = getSelectedBasisTexFormat();
2625
2626      const isHDRSourceFile = (extension != null) && (extension === "exr" || extension === "hdr");
2627
2628      if (Module.isBasisTexFormatLDR(desiredBasisTexFormat))
2629      {
2630         if (isHDRSourceFile)
2631         {
2632            log('Image is HDR - must encode to a HDR format. Defaulting to UASTC HDR 4x4.');
2633            desiredBasisTexFormat = Module.basis_tex_format.cUASTC_HDR_4x4.value;
2634
2635            setDropdownValue('basis-tex-format', Module.basis_tex_format.cUASTC_HDR_4x4.value);
2636         }
2637      }
2638
2639      basisEncoder.setCreateKTX2File(true);
2640      basisEncoder.setKTX2UASTCSupercompression(true);
2641
2642      // Consistently set sRGB related options
2643      var sRGB_flag = elem('SRGB').checked;
2644
2645      // We could also set setSRGBOptions() here. (The KTX2 sRGB transfer func setting will be ignored when writing HDR files, as it's always linear.)
2646      // sRGB colorspace metrics (used by some codecs, like ETC1S/UASTC)
2647      basisEncoder.setPerceptual(sRGB_flag);
2648
2649      if (!isHDRSourceFile)
2650      {
2651         // KTX2 DFD transfer function.
2652         // This also controls the ASTC decode mode profile used during XUASTC/ASTC LDR 4x4-12x12 compression.
2653         basisEncoder.setKTX2AndBasisSRGBTransferFunc(sRGB_flag);
2654      }
2655
2656      // Mipmap generator sRGB->linear conversion during filtering
2657      basisEncoder.setMipSRGB(sRGB_flag);
2658
2659      if (browserDecodedRGBA)
2660      {
2661         // Browser-decoded image (WebP, AVIF, etc.): always LDR 8-bit RGBA.
2662         
2663         if (Module.isBasisTexFormatHDR(desiredBasisTexFormat))
2664         {
2665            var ldr_to_hdr_nit_multiplier = getNitMultiplier();
2666            
2667            basisEncoder.setSliceSourceImageHDR(0, browserDecodedRGBA.rgbaU8, 
2668                browserDecodedRGBA.width, browserDecodedRGBA.height, 
2669                Module.hdr_image_type.cHITRGBA8Image.value, elem('ConvertLDRToLinear').checked, ldr_to_hdr_nit_multiplier);
2670         }
2671         else
2672         {
2673            basisEncoder.setSliceSourceImage(0, browserDecodedRGBA.rgbaU8,
2674                browserDecodedRGBA.width, browserDecodedRGBA.height,
2675                Module.ldr_image_type.cRGBA32.value);
2676         }
2677      }
2678      else if (Module.isBasisTexFormatHDR(desiredBasisTexFormat))
2679      {
2680         var img_type = Module.hdr_image_type.cHITPNGImage.value;
2681
2682         if (extension != null)
2683         {
2684            if (extension === "exr")
2685               img_type = Module.hdr_image_type.cHITEXRImage.value;
2686            else if (extension === "hdr")
2687               img_type = Module.hdr_image_type.cHITHDRImage.value;
2688            else if ((extension === "jpg") || (extension === "jpeg") || (extension === "jfif"))
2689               img_type = Module.hdr_image_type.cHITJPGImage.value;
2690            else if (extension === "qoi")
2691               img_type = Module.hdr_image_type.cHITQOIImage.value;
2692         }
2693
2694         var ldr_to_hdr_nit_multiplier = getNitMultiplier();
2695
2696         basisEncoder.setSliceSourceImageHDR(0, new Uint8Array(data), 0, 0, img_type, elem('ConvertLDRToLinear').checked, ldr_to_hdr_nit_multiplier);
2697
2698         /*
2699                 // Float image data test
2700                 const checkerboard = new Float32Array(64);
2701
2702                 // Fill the checkerboard array as before
2703                 for (let y = 0; y < 4; y++)
2704                 {
2705                     for (let x = 0; x < 4; x++)
2706                     {
2707                         const index = (y * 4 + x) * 4;
2708                         const isWhite = (x + y) % 2 === 0;
2709
2710                         if (isWhite)
2711                         {
2712                             checkerboard[index] = 1.0;
2713                             checkerboard[index + 1] = 0.0;
2714                             checkerboard[index + 2] = 1.0;
2715                             checkerboard[index + 3] = 1.0;
2716                         }
2717                         else
2718                         {
2719                             checkerboard[index] = 0.0;
2720                             checkerboard[index + 1] = 0.0;
2721                             checkerboard[index + 2] = 0.0;
2722                             checkerboard[index + 3] = 1.0;
2723                         }
2724                     }
2725                 }
2726
2727                 // Convert Float32Array to Uint8Array by sharing the same buffer
2728                 const byteArray = new Uint8Array(checkerboard.buffer);
2729
2730                 basisEncoder.setSliceSourceImageHDR(0, byteArray, 4, 4, Module.hdr_image_type.cHITRGBAFloat.value, elem('ConvertLDRToLinear').checked, 1.0);
2731         */
2732
2733         /*
2734                 // Half float image data test
2735                 var W = 16;
2736                 var H = 16;
2737                 const checkerboard = new Uint16Array(W*H*4);
2738
2739                 // Values to represent 1.0 and 0 in 16-bit integers
2740                 const VALUE_ONE = 0x3C00; // FP16 representation of 1.0
2741                 const VALUE_ZERO = 0x0000; // FP16 representation of 0.0
2742
2743                 // Fill the checkerboard array
2744                 for (let y = 0; y < H; y++)
2745                 {
2746                     for (let x = 0; x < W; x++)
2747                     {
2748                         const index = (y * W + x) * 4;
2749                         const isWhite = (x + y) % 2 === 0;
2750
2751                         if (isWhite)
2752                         {
2753                             checkerboard[index] = VALUE_ONE;       // R
2754                             checkerboard[index + 1] = VALUE_ONE;   // G
2755                             checkerboard[index + 2] = VALUE_ONE;   // B
2756                             checkerboard[index + 3] = VALUE_ONE;   // A
2757                         }
2758                         else
2759                         {
2760                             checkerboard[index] = VALUE_ZERO;      // R
2761                             checkerboard[index + 1] = VALUE_ZERO;  // G
2762                             checkerboard[index + 2] = VALUE_ZERO;  // B
2763                             checkerboard[index + 3] = VALUE_ONE;   // A (1.0)
2764                         }
2765                     }
2766                 }
2767
2768                 // Convert Uint16Array to Uint8Array by sharing the same buffer
2769                 const byteArray = new Uint8Array(checkerboard.buffer);
2770
2771                 basisEncoder.setSliceSourceImageHDR(0, byteArray, W, H, Module.hdr_image_type.cHITRGBAHalfFloat.value, elem('ConvertLDRToLinear').checked, 1.0);
2772         */
2773      }
2774      else
2775      {
2776         // basis_tex_format is LDR, source is a natively-supported file format
2777         var img_type = Module.ldr_image_type.cPNGImage.value;
2778
2779         if (extension != null)
2780         {
2781            if ((extension === "jpg") || (extension === "jpeg") || (extension === "jfif"))
2782               img_type = Module.ldr_image_type.cJPGImage.value;
2783            else if (extension === "qoi")
2784               img_type = Module.ldr_image_type.cQOIImage.value;
2785         }
2786
2787         basisEncoder.setSliceSourceImage(0, new Uint8Array(data), 0, 0, img_type);
2788      }
2789
2790      basisEncoder.setFormatMode(desiredBasisTexFormat);
2791            
2792      basisEncoder.setRec2020(elem('Rec2020').checked);
2793
2794      basisEncoder.setDebug(elem('Debug').checked);
2795      basisEncoder.setStatusOutput(true);
2796      basisEncoder.setComputeStats(elem('ComputeStats').checked);
2797      basisEncoder.setPrintStats(elem('PrintStats').checked);
2798                  
2799      // Some newer codecs (e.g. XUBC7) ALWAYS use the unified quality/effort
2800      // controls even when the page is in low-level mode. The low-level branch
2801      // below sets this flag for them; the unified block further down then runs
2802      // regardless of the "use unified" checkbox.
2803      var force_unified_effort_quality = false;
2804
2805      // If not using unified effort/quality options, set various low-level effort and quality related parameters.
2806      // These low-level settings get set automatically if unified effort/quality settings are used.
2807      if (!elem('use_new_style_quality_effort').checked)
2808      {
2809        // Set UASTC HDR 4x4 effort (0=fastest) (somewhat misnamed "quality" because it impacts both performance and quality)
2810        basisEncoder.setUASTCHDRQualityLevel(getUASTCHDRQuality());
2811
2812        // Set ASTC HDR 6x6/UASTC HDR 6x6 effort
2813        basisEncoder.setASTC_HDR_6x6_Level(getASTCHDR6x6CompLevel());
2814
2815        // Set ASTC HDR 6x6/UASTC HDR 6x6 lambda (quality)
2816        basisEncoder.setLambda(getASTC6x6RDOLambda());
2817      
2818        // Retrieve the correct quality slider depending on XUASTC vs. ETC1S.
2819        // New codecs (e.g. XUBC7) always use the unified quality/effort controls
2820        // even when the page is in low-level mode -- the low-level options are
2821        // effectively ignored for them (we're moving new codecs to the unified
2822        // options exclusively).
2823        if (desiredBasisTexFormat === Module.basis_tex_format.cXUBC7.value)
2824        {
2825            // XUBC7 uses the unified quality/effort controls -- defer to the
2826            // unified block below (the low-level Q/effort options are ignored).
2827            force_unified_effort_quality = true;
2828        }
2829        else if (Module.isBasisTexFormatXUASTCLDR(desiredBasisTexFormat))
2830        {
2831            var weightGridDCTQuality = getXUASTCLDRDCTQuality();
2832            
2833            if (weightGridDCTQuality < 100)
2834            {
2835                basisEncoder.setQualityLevel(weightGridDCTQuality);
2836                basisEncoder.setXUASTCLDRUseDCT(true);
2837            }
2838        }
2839        else
2840        {
2841            const etc1SQualityLevel = parseInt(elem('EncodeQuality').value, 10);
2842            basisEncoder.setQualityLevel(etc1SQualityLevel);
2843        }
2844
2845        // Low level ASTC and XUASTC LDR bounded/windowed RDO settings
2846        basisEncoder.setXUASTCLDRUseLossySupercompression(elem('XUASTCLossySupercompression').checked);
2847        
2848        basisEncoder.setASTCOrXUASTCLDREffortLevel(getXUASTCLDREffortLevel());
2849        
2850        // UASTC LDR 4x4
2851        basisEncoder.setRDOUASTC(elem('UASTC_LDR_RDO').checked);
2852        basisEncoder.setRDOUASTCQualityScalar(getUASTCLDRRDOQuality());
2853        basisEncoder.setPackUASTCFlags(getUASTCLDRQuality());
2854        
2855        // ETC1S
2856        basisEncoder.setETC1SCompressionLevel(getETC1SCompLevel());
2857      }
2858
2859      // Low-level ASTC LDR/XUASTC LDR 4x4-12x12 options (that are available even if unified effort/quality settings are used)
2860      basisEncoder.setXUASTCLDRBoundedRDOParam(0, getSafeFloat("xuastc_ldr_min_psnr"));
2861      basisEncoder.setXUASTCLDRBoundedRDOParam(1, getSafeFloat("xuastc_ldr_min_alpha_psnr"));
2862      basisEncoder.setXUASTCLDRBoundedRDOParam(2, getSafeFloat("xuastc_ldr_thresh_psnr"));
2863      basisEncoder.setXUASTCLDRBoundedRDOParam(3, getSafeFloat("xuastc_ldr_thresh_alpha_psnr"));
2864      basisEncoder.setXUASTCLDRBoundedRDOParam(4, getSafeFloat("xuastc_ldr_thresh_edge_psnr"));
2865      basisEncoder.setXUASTCLDRBoundedRDOParam(5, getSafeFloat("xuastc_ldr_thresh_edge_alpha_psnr"));
2866
2867      basisEncoder.setXUASTCLDRForceDisableRGBDualPlane(elem('XUASTCDisableRGBDualPlane').checked);
2868      basisEncoder.setXUASTCLDRForceDisableSubsets(elem('XUASTCDisableSubsets').checked);
2869      basisEncoder.setXUASTCLDRUseBlurring(elem('XUASTCUseBlur').checked);
2870      
2871      // Dropdown value is the enum member name (e.g. "cBasisU"); look up the int
2872      // via the Embind-exported enum so we don't hardcode integer values here.
2873      basisEncoder.setXUASTCLDRSelectCompressor(
2874         Module.xuastc_ldr_astc_comp_selection[elem('XUASTCLDRCompressor').value].value);
2875          
2876      basisEncoder.setXUASTCLDRHeavySubsetUsage(elem('XUASTCHeavySubsetUsage').checked);
2877                  
2878      const sharpenModeSelect = document.getElementById("XUASTCSharpenMode");
2879      basisEncoder.setXUASTCLDRSharpenMode(Number(sharpenModeSelect.value));
2880      basisEncoder.setXUASTCLDRSharpenAmount(parseFloat(elem('XUASTCSharpenAmount').value) || 0.0);
2881      
2882      // Set deblocking related encode time parameters
2883      const encoding_deblocking_filter_select = document.getElementById("encoding_deblocking_filter_select");
2884      const encoding_deblocking_filter_mode = Number(encoding_deblocking_filter_select.value);
2885
2886      // By default: No deblocking
2887      basisEncoder.setXUASTCLDRDeblockingMode(encoding_deblocking_filter_mode);
2888            
2889      basisEncoder.setXUASTCLDRNumDeblockingPasses(getDeblockingNumPasses());
2890      
2891      // Set channel weights. Note the codec's default ASTC/XUASTC LDR channel weights were selected for sRGB content and are 3,11,1,11 (RGBA)
2892      basisEncoder.setASTCOrXUASTCLDRWeights(
2893         Math.max(1, Math.min(32, parseInt(elem('xuastc_ldr_weight_r').value, 10) || 1)),
2894         Math.max(1, Math.min(32, parseInt(elem('xuastc_ldr_weight_g').value, 10) || 1)),
2895         Math.max(1, Math.min(32, parseInt(elem('xuastc_ldr_weight_b').value, 10) || 1)),
2896         Math.max(1, Math.min(32, parseInt(elem('xuastc_ldr_weight_a').value, 10) || 1))
2897      );
2898
2899      // Set source image channel swizzle (default 0,1,2,3 = no swizzle).
2900      {
2901         var sw = getSwizzle();
2902         basisEncoder.setSwizzle(sw[0], sw[1], sw[2], sw[3]);
2903      }
2904
2905      // Set XUASTC LDR entropy coding profile/syntax
2906      basisEncoder.setXUASTCLDRSyntax(getXUASTCLDRSyntax());
2907
2908      // XUBC7 RDO level [0,100] (0=off). A low-level XUBC7 knob applied regardless
2909      // of the unified controls (which only handle XUBC7's quality+effort). Setting
2910      // it is harmless for non-XUBC7 formats (ignored unless the format is XUBC7).
2911      basisEncoder.setXUBC7RDOLevel(getXUBC7RDOLevel());
2912
2913      // XUBC7 number of encode stripes [1,16] (default 8). Independent of the
2914      // unified controls;
2914 harmless for non-XUBC7 formats. More stripes = more
2915      // encode/decode parallelism for a slightly larger file.
2916      basisEncoder.setXUBC7NumStripes(getXUBC7NumStripes());
2917
2918      // XUBC7 BC7 base encoder: 0 = bc7f (fast, default), 1 = bc7e_scalar (slower,
2919      // higher quality), plus its quality level [0,6]. Independent of the unified
2920      // controls; harmless for non-XUBC7 formats (ignored unless format is XUBC7).
2921      basisEncoder.setXUBC7Encoder(getXUBC7Encoder());
2922      basisEncoder.setXUBC7BC7EScalarLevel(getXUBC7BC7EScalarLevel());
2923
2924      // Mipmap options (setMipSRGB() set is above, and matches the sRGB checkbox)
2925      basisEncoder.setMipGen(elem('Mipmaps').checked);
2926      basisEncoder.setMipFilter(parseInt(elem('mip-filter-select').value, 10));
2927      basisEncoder.setMipScale(parseFloat(elem('mip-scale-input').value) || 1.0);
2928      basisEncoder.setMipSmallestDimension(parseInt(elem('mip-smallest-dim-input').value, 10) || 1);
2929      basisEncoder.setMipRenormalize(elem('MipRenormalize').checked);
2930      basisEncoder.setMipWrapping(elem('MipWrapping').checked);
2931
2932      // Image Y flip
2933      basisEncoder.setYFlip(elem('YFlip').checked);
2934               
2935      // See if they've enabled the new-style unified effort and quality levels.
2936      // If so, set them now, which will override some of the codec-specific lower level options set previously.
2937      // force_unified_effort_quality is set for codecs (e.g. XUBC7) that always use the unified controls.
2938      if (elem('use_new_style_quality_effort').checked || force_unified_effort_quality)
2939      {
2940         const unified_effort_level = parseInt(document.getElementById("unified-effort-slider").value, 10);
2941         const unified_quality_level = parseInt(document.getElementById("unified-quality-slider").value, 10);
2942                         
2943         basisEncoder.setFormatModeAndQualityEffort(desiredBasisTexFormat, unified_quality_level, unified_effort_level, true);
2944         
2945         log('Encoding to unified effort level ' + unified_effort_level + ', unified quality level ' + unified_quality_level);
2946      }
2947      
2948      if (desiredBasisTexFormat === Module.basis_tex_format.cUASTC_HDR_4x4.value)
2949         log('Encoding to UASTC HDR 4x4');
2950      else if (desiredBasisTexFormat === Module.basis_tex_format.cASTC_HDR_6x6.value)
2951         log('Encoding to ASTC HDR 6x6');
2952      else if (desiredBasisTexFormat === Module.basis_tex_format.cUASTC_HDR_6x6_INTERMEDIATE.value)
2953         log('Encoding to UASTC HDR 6x6i');
2954      else if (desiredBasisTexFormat === Module.basis_tex_format.cUASTC_LDR_4x4.value)
2955         log('Encoding to UASTC LDR 4x4');
2956      else if (Module.isBasisTexFormatASTCLDR(desiredBasisTexFormat))
2957         log('Encoding to ASTC LDR 4x4-12x12');
2958      else if (Module.isBasisTexFormatXUASTCLDR(desiredBasisTexFormat))
2959         log('Encoding to XUASTC LDR 4x4-12x12');
2960      else
2961         log('Encoding to ETC1S');
2962
2963      showBusyModal();
2964
2965      requestAnimationFrame(() =>
2966      {
2967         requestAnimationFrame(() =>
2968         {
2969            const startTime = performance.now();
2970
2971            var num_output_bytes = basisEncoder.encode(ktx2FileData);
2972
2973            const elapsed = performance.now() - startTime;
2974
2975            g_lastEncodeTime = elapsed;
2976            g_lastEncodeMip0RGBAPSNR = basisEncoder.getLastEncodeMip0RGBAPSNR();
2977
2978            hideBusyModal();
2979
2980            logTime('encoding time', elapsed.toFixed(2));
2981
2982            var actualKTX2FileData = new Uint8Array(ktx2FileData.buffer, 0, num_output_bytes);
2983
2984            basisEncoder.delete();
2985
2986            // DEV VALIDATION: run the DTO encode path on the SAME main thread (no
2987            // worker yet) and verify it produces byte-identical output to the legacy
2988            // path above. Logs to the JS console (Chrome F12). Wrapped so it can
2989            // never break the real encode. Temporary -- remove once the DTO path is
2990            // trusted and used for both non-worker and worker encoding.
2991            // Skipped when the legacy encode failed (nothing meaningful to compare).
2992            if (g_enableDTOValidation && num_output_bytes > 0) try
2993            {
2994               const _dtoT0 = performance.now();
2995               const _dto = buildEncodeDTO(desiredBasisTexFormat, extension, data, browserDecodedRGBA, isHDRSourceFile);
2996               const _dtoEnc = new BasisEncoder();
2997               applyEncodeDTO(_dtoEnc, _dto);
2998               const _dtoOut = new Uint8Array(1024 * 1024 * 24);
2999               const _dtoLen = _dtoEnc.encode(_dtoOut);
3000               _dtoEnc.delete();
3001               const _dtoMs = (performance.now() - _dtoT0).toFixed(2);
3002
3003               let _same = (_dtoLen === num_output_bytes);
3004               let _firstDiff = -1;
3005               if (_same)
3006               {
3007                  for (let i = 0; i < _dtoLen; i++)
3008                  {
3009                     if (_dtoOut[i] !== actualKTX2FileData[i]) { _same = false; _firstDiff = i; break; }
3010                  }
3011               }
3012
3013               if (_same)
3014                  console.log(`%cDTO validation: IDENTICAL ✓  (legacy=${num_output_bytes} B, DTO=${_dtoLen} B, DTO encode ${_dtoMs} ms)`, 'color:#0a0;font-weight:bold');
3015               else
3016                  console.error(`DTO validation: MISMATCH ✗  legacy=${num_output_bytes} B, DTO=${_dtoLen} B` +
3017                     (_firstDiff >= 0 ? `, first differing byte @ ${_firstDiff} (legacy=${actualKTX2FileData[_firstDiff]}, DTO=${_dtoOut[_firstDiff]})` : ' (length differs)') +
3018                     (_dto.multithreaded ? ' [multithreaded: a mismatch may be encoder non-determinism, not a DTO bug -- retry single-threaded to confirm]' : ''));
3019            }
3020            catch (_e)
3021            {
3022               console.error('DTO validation: ERROR', _e);
3023            }
3024
3025            if (num_output_bytes == 0)
3026            {
3027               updateErrorLine('Image is invalid or unsupported, or it may be too large to compress in WASM without risking running out of memory.');
3028
3029               log('encodeBasisTexture() failed!');
3030            }
3031            else
3032            {
3033               log('encodeBasisTexture() succeeded, output size ' + num_output_bytes);
3034
3035               encodedKTX2File = actualKTX2FileData;
3036            }
3037
3038            if (num_output_bytes != 0)
3039            {
3040               //log('HERE');
3041               
3042               transcodeTexture(actualKTX2FileData);
3043            }
3044
3045       });
3046    });
3047
3048   }
3049
3050   function dataLoadError(msg)
3051   {
3052      updateErrorLine(msg);
3053      log(msg);
3054   }
3055
3056   function runLoadFile()
3057   {
3058      //logClear();
3059      resetMipmapSliderToZero();
3060      resetCubemapFaceToZero();
3061      resetArrayLayerToZero();
3062      resetDisplayZoom();
3063      resetExposure();
3064      loadArrayBufferFromURI(elem('file').value, transcodeTexture, dataLoadError);
3065   }
3066
3067   function runEncodeImageFile()
3068   {
3069      //logClear();
3070
3071      //resetMipmapSliderToZero();
3072      //resetCubemapFaceToZero();
3073      //resetArrayLayerToZero();
3074
3075      if ((elem('imagefile').value === '<externally loaded>') && (curLoadedImageData != null))
3076      {
3077         //console.log("calling compressImage 1");
3078
3079         compressImage(curLoadedImageData, curLoadedImageURI, curLoadedBrowserRGBA, 'main'); // this button forces main-thread
3080      }
3081      else
3082      {
3083         //console.log("calling compressImage 4");
3084
3085         loadArrayBufferFromURI(elem('imagefile').value, function (d, u) { loadAndCompressImage(d, u, 'main'); }, dataLoadError); // force main-thread
3086      }
3087   }
3088
3089   function onFilenameSelect()
3090   {
3091      // Get the selected value from the drop-down
3092      var dropdown = document.getElementById('filename-dropdown');
3093      var selectedFilename = dropdown.value;
3094
3095      if (selectedFilename)
3096      {
3097         resetMipmapSliderToZero();
3098         resetCubemapFaceToZero();
3099         resetArrayLayerToZero();
3100         resetDisplayZoom();
3101         resetExposure();
3102
3103         elem('imagefile').value = selectedFilename;
3104
3105         //logClear();
3106
3107         //console.log("calling compressImage 3");
3108
3109         loadArrayBufferFromURI(selectedFilename, loadAndCompressImage, dataLoadError);
3110      }
3111   }
3112
3113   function updateViewModeButtons()
3114   {
3115      var ids = ['btn-alpha-blend', 'btn-view-rgb', 'btn-view-alpha', 'btn-view-r', 'btn-view-g', 'btn-view-b'];
3116      for (var i = 0; i < ids.length; i++)
3117         elem(ids[i]).classList.toggle('active-btn', i === drawMode);
3118   }
3119   function alphaBlend() { drawMode = 0; updateViewModeButtons(); redraw(); }
3120   function viewRGB() { drawMode = 1; updateViewModeButtons(); redraw(); }
3121   function viewAlpha() { drawMode = 2; updateViewModeButtons(); redraw(); }
3122   function viewR() { drawMode = 3; updateViewModeButtons(); redraw(); }
3123   function viewG() { drawMode = 4; updateViewModeButtons(); redraw(); }
3124   function viewB() { drawMode = 5; updateViewModeButtons(); redraw(); }
3125
3126   // Re-transcode + redraw the currently-viewed compressed texture (KTX2 or DDS) after a transcoder
3127   // option changes. No-op if nothing is loaded. Shared by the transcoder-option change handlers.
3128   function retranscodeCurrentTexture()
3129   {
3130      if (curLoadedKTX2Data != null)
3131      {
3132         logClear();
3133         transcodeTexture(curLoadedKTX2Data, curLoadedKTX2URI);
3134      }
3135      else if (curLoadedDDSData != null)
3136      {
3137         logClear();
3138         transcodeDDSTexture(curLoadedDDSData, curLoadedDDSURI);
3139      }
3140   }
3141
3142   function BC7ChromaFilterClicked() { retranscodeCurrentTexture(); }
3143   function XUASTCDeblockingFilterClicked() { retranscodeCurrentTexture(); }
3144   function DeblockingFilterModeChanged() { retranscodeCurrentTexture(); }
3145   
3146   function XUASTCGPUDeblockingFilterClicked()
3147   {
3148      redraw();
3149   }
3150
3151   function highQualityTranscodingClicked() { retranscodeCurrentTexture(); }
3152
3153   // Toggles cDecodeFlagsTranscodeAlphaDataToOpaqueFormats. Only meaningful for opaque target
3154   // formats (PVRTC1 RGB, ETC1): it transcodes the source's alpha channel into the opaque output
3155   // instead of RGB, so the alpha can be viewed. No effect on formats that already carry alpha.
3156   function alphaToOpaqueFormatsClicked() { retranscodeCurrentTexture(); }
3157
3158   function linearToSRGB()
3159   {
3160      linearToSRGBFlag = !linearToSRGBFlag;
3161      elem('linear_to_srgb').innerText = linearToSRGBFlag ? 'Enabled' : 'Disabled';
3162      redraw();
3163   }
3164
3165   function lerp(a, b, t)
3166   {
3167      return a + t * (b - a);
3168   }
3169
3170   function updateScale(value)
3171   {
3172      var v = value;
3173      if (v < .5)
3174      {
3175         v = v / .5;
3176         v = v ** 3.0;
3177      }
3178      else
3179      {
3180         v = (v - .5) / .5;
3181         v = lerp(1.0, 32.0, v);
3182      }
3183
3184      document.getElementById('scale-value').textContent = v.toFixed(4);
3185      drawScale = v;
3186      redraw();
3187   }
3188
3189   function updateMipmapLevel(value)
3190   {
3191      var v = parseInt(value, 10);
3192      document.getElementById('mipmap-level-value').textContent = v;
3193
3194      // Re-transcode the current texture at the selected mip level.
3195      if (curLoadedKTX2Data != null)
3196      {
3197         transcodeTexture(curLoadedKTX2Data, curLoadedKTX2URI);
3198      }
3199      else if (curLoadedDDSData != null)
3200      {
3201         transcodeDDSTexture(curLoadedDDSData, curLoadedDDSURI);
3202      }
3203   }
3204
3205   function updateDisplayZoom(value)
3206   {
3207      // Slider steps: 0=0.5x, 1=1x, 2=2x, ... 8=8x
3208      var v = parseInt(value, 10);
3209      displayZoom = (v === 0) ? 0.5 : v;
3210      document.getElementById('display-zoom-value').textContent = displayZoom + 'x';
3211      redraw();
3212   }
3213
3214   function updateCanvasColorSpace(value)
3215   {
3216      if (!renderer) return;
3217      var target;
3218      if (value === 'default')
3219         target = originalCanvasColorSpace;
3220      else
3221         target = value;
3222      // Unsupported browser (snapshot is null): nothing to do.
3223      if (target === null) return;
3224      renderer.setCanvasColorSpace(target);
3225      redraw();
3226   }
3227   // Encoding presets: shortcuts that flip a small set of encoder controls to
3228   // sensible values for a given content type. Only invoked when the user picks
3229   // a value from the preset dropdown -- never on page load. Each branch sets
3230   // every preset-controlled field explicitly, so switching back and forth is
3231   // fully predictable regardless of intermediate manual edits.
3232   function applyEncodingPreset(value)
3233   {
3234      if (value === 'srgb_photo')
3235      {
3236         elem('SRGB').checked = true;
3237         elem('xuastc_ldr_weight_r').value = 9;
3238         elem('xuastc_ldr_weight_g').value = 11;
3239         elem('xuastc_ldr_weight_b').value = 1;
3240         elem('xuastc_ldr_weight_a').value = 11;
3241         //elem('XUASTCSharpenMode').value = '0'; // Disabled
3242         //elem('encoding_deblocking_filter_select').value = '1'; // Auto
3243      }
3244      else if (value === 'linear_texture')
3245      {
3246         elem('SRGB').checked = false;
3247         elem('xuastc_ldr_weight_r').value = 1;
3248         elem('xuastc_ldr_weight_g').value = 1;
3249         elem('xuastc_ldr_weight_b').value = 1;
3250         elem('xuastc_ldr_weight_a').value = 1;
3251         //elem('XUASTCSharpenMode').value = '0'; // Disabled
3252         //elem('encoding_deblocking_filter_select').value = '0'; // Disabled
3253      }
3254   }
3255   
3256   function toggleBilinearFiltering()
3257   {
3258      drawUseBilinearFiltering = document.getElementById('bilinear-filtering').checked;
3259      redraw();
3260   }
3261
3262   function resetDisplayZoom()
3263   {
3264      displayZoom = 1.0;
3265      document.getElementById('display-zoom-slider').value = 1;
3266      document.getElementById('display-zoom-value').textContent = '1x';
3267   }
3268
3269   function resetExposure()
3270   {
3271      drawScale = 1.0;
3272      document.getElementById('scale-slider').value = 0.5;
3273      document.getElementById('scale-value').textContent = '1.0000';
3274   }
3275
3276   function resetDisplayOptions()
3277   {
3278      // View mode -> Alpha blend
3279      drawMode = 0;
3280      updateViewModeButtons();
3281
3282      // LinearToSRGB -> Disabled
3283      linearToSRGBFlag = false;
3284      elem('linear_to_srgb').innerText = 'Disabled';
3285
3286      // Exposure -> 1.0
3287      resetExposure();
3288
3289      // Mipmap level -> 0
3290      resetMipmapSliderToZero();
3291
3292      // Cubemap face -> 0
3293      resetCubemapFaceToZero();
3294
3295      // Array layer -> 0
3296      resetArrayLayerToZero();
3297
3298      // Display zoom -> 1x
3299      resetDisplayZoom();
3300
3301      // Bilinear filtering -> off
3302      drawUseBilinearFiltering = false;
3303      document.getElementById('bilinear-filtering').checked = false;
3304
3305      document.getElementById('xuastc_ldr_gpu_deblocking_show_edge_weights').checked = false;     
3306      
3307      // Re-transcode at mip 0 / face 0 / layer 0 if a texture is loaded,
3308      // then redraw with the reset settings.
3309      if (curLoadedKTX2Data != null)
3310         transcodeTexture(curLoadedKTX2Data, curLoadedKTX2URI);
3311      else if (curLoadedDDSData != null)
3312         transcodeDDSTexture(curLoadedDDSData, curLoadedDDSURI);
3313      else
3314         redraw();
3315   }
3316
3317   // Updates the slider range to match the current texture's mip count.
3318   // Preserves the current slider value if it's still in range; otherwise clamps to 0.
3319   function updateMipmapSliderRange(numLevels)
3320   {
3321      var slider = document.getElementById('mipmap-level-slider');
3322      var label = document.getElementById('mipmap-level-value');
3323
3324      var maxLevel = Math.max(0, numLevels - 1);
3325
3326      slider.min = 0;
3327      slider.max = maxLevel;
3328      slider.disabled = (maxLevel === 0);
3329
3330      // Clamp current value to the new valid range.
3331      var curVal = parseInt(slider.value, 10);
3332      if (isNaN(curVal) || curVal < 0 || curVal > maxLevel)
3333      {
3334         slider.value = 0;
3335         label.textContent = '0';
3336      }
3337   }
3338
3339   // Resets the slider to level 0 (for when a new file is loaded).
3340   function resetMipmapSliderToZero()
3341   {
3342      var slider = document.getElementById('mipmap-level-slider');
3343      var label = document.getElementById('mipmap-level-value');
3344
3345      slider.value = 0;
3346      label.textContent = '0';
3347   }
3348
3349   function updateCubemapFace(value)
3350   {
3351      var v = parseInt(value, 10);
3352      document.getElementById('cubemap-face-value').textContent = v;
3353
3354      if (curLoadedKTX2Data != null)
3355      {
3356         transcodeTexture(curLoadedKTX2Data, curLoadedKTX2URI);
3357      }
3358      else if (curLoadedDDSData != null)
3359      {
3360         transcodeDDSTexture(curLoadedDDSData, curLoadedDDSURI);
3361      }
3362   }
3363
3364   function updateCubemapFaceRange(numFaces)
3365   {
3366      var slider = document.getElementById('cubemap-face-slider');
3367      var label = document.getElementById('cubemap-face-value');
3368
3369      var maxFace = Math.max(0, numFaces - 1);
3370
3371      slider.min = 0;
3372      slider.max = maxFace;
3373      slider.disabled = (maxFace === 0);
3374
3375      var curVal = parseInt(slider.value, 10);
3376      if (isNaN(curVal) || curVal < 0 || curVal > maxFace)
3377      {
3378         slider.value = 0;
3379         label.textContent = '0';
3380      }
3381   }
3382
3383   function resetCubemapFaceToZero()
3384   {
3385      var slider = document.getElementById('cubemap-face-slider');
3386      var label = document.getElementById('cubemap-face-value');
3387
3388      slider.value = 0;
3389      label.textContent = '0';
3390   }
3391
3392   function updateArrayLayer(value)
3393   {
3394      var v = parseInt(value, 10);
3395      document.getElementById('array-layer-value').textContent = v;
3396      document.getElementById('array-layer-input').value = v;
3397
3398      if (curLoadedKTX2Data != null)
3399      {
3400         transcodeTexture(curLoadedKTX2Data, curLoadedKTX2URI);
3401      }
3402      else if (curLoadedDDSData != null)
3403      {
3404         transcodeDDSTexture(curLoadedDDSData, curLoadedDDSURI);
3405      }
3406   }
3407
3408   function updateArrayLayerFromInput(value)
3409   {
3410      var v = parseInt(value, 10);
3411      var slider = document.getElementById('array-layer-slider');
3412      var maxLayer = parseInt(slider.max, 10);
3413
3414      if (isNaN(v) || v < 0) v = 0;
3415      if (v > maxLayer) v = maxLayer;
3416
3417      slider.value = v;
3418      document.getElementById('array-layer-value').textContent = v;
3419      document.getElementById('array-layer-input').value = v;
3420
3421      if (curLoadedKTX2Data != null)
3422      {
3423         transcodeTexture(curLoadedKTX2Data, curLoadedKTX2URI);
3424      }
3425      else if (curLoadedDDSData != null)
3426      {
3427         transcodeDDSTexture(curLoadedDDSData, curLoadedDDSURI);
3428      }
3429   }
3430
3431   function updateArrayLayerRange(numLayers)
3432   {
3433      var slider = document.getElementById('array-layer-slider');
3434      var label = document.getElementById('array-layer-value');
3435      var input = document.getElementById('array-layer-input');
3436
3437      var maxLayer = Math.max(0, numLayers - 1);
3438
3439      slider.min = 0;
3440      slider.max = maxLayer;
3441      slider.disabled = (maxLayer === 0);
3442
3443      input.min = 0;
3444      input.max = maxLayer;
3445      input.disabled = (maxLayer === 0);
3446
3447      var curVal = parseInt(slider.value, 10);
3448      if (isNaN(curVal) || curVal < 0 || curVal > maxLayer)
3449      {
3450         slider.value = 0;
3451         input.value = 0;
3452         label.textContent = '0';
3453      }
3454   }
3455
3456   function resetArrayLayerToZero()
3457   {
3458      var slider = document.getElementById('array-layer-slider');
3459      var label = document.getElementById('array-layer-value');
3460      var input = document.getElementById('array-layer-input');
3461
3462      slider.value = 0;
3463      input.value = 0;
3464      label.textContent = '0';
3465   }
3466
3467
3468
3469   function downloadEncodedFile()
3470   {
3471      // Source-aware: whatever compressed texture is currently active gets downloaded. A .DDS view
3472      // (an "Encode DDS!" result, or a loaded .dds) downloads the .dds bytes; otherwise the encoded
3473      // .KTX2 is downloaded. The current-source globals tell us which.
3474      if (getLoadedSourceKind() === 'dds')
3475      {
3476         if (curLoadedDDSData && curLoadedDDSData.length)
3477         {
3478            var ddsName = 'encoded_file.dds';
3479            if (curLoadedDDSURI)
3480            {
3481               var base = curLoadedDDSURI.split(/[\\/]/).pop();
3482               if (base && /\.dds$/i.test(base)) ddsName = base;
3483            }
3484            download_file(ddsName, curLoadedDDSData);
3485         }
3486         else
3487            updateErrorLine('No .DDS data to download.');
3488         return;
3489      }
3490      if (encodedKTX2File)
3491      {
3492         if (encodedKTX2File.length)
3493            download_file("encoded_file.ktx2", encodedKTX2File);
3494      }
3495   }
3496
3497   // Composes the transcoder decode flags from the "Transcoder Options (Decode Flags)"
3498   // UI elements, matching how the normal transcode path (transcodeTexture()) builds
3499   // them, so DDS/KTX exports decode the same way. blockWidth/blockHeight are the
3500   // source texture's block dimensions (for the force-deblock-on-large-blocks
3501   // heuristic). Returns the flags integer. (Keep in sync with transcodeTexture().)
3502   function composeTranscodeDecodeFlags(blockWidth, blockHeight)
3503   {
3504      var F = Module.basisu_decode_flags;
3505
3506      var flags = elem('highquality_transcoding').checked ? F.cDecodeFlagsHighQuality.value : 0;
3507
3508      if (elem('etc1s_bc7_transcoder_no_chroma_filtering').checked)
3509         flags = flags | F.cDecodeFlagsNoETC1SChromaFiltering.value;
3510
3511      if (elem('xuastc_ldr_disable_fast_bc7_transcoding').checked)
3512         flags = flags | F.cDecodeFlagXUASTCLDRDisableFastBC7Transcoding.value;
3513
3514      if (elem('transcode_alpha_to_opaque_formats').checked)
3515         flags = flags | F.cDecodeFlagsTranscodeAlphaDataToOpaqueFormats.value;
3516
3517      var deblocking_mode = Number(document.getElementById("deblocking-filter-select").value);
3518
3519      if (deblocking_mode == 3)
3520      {
3521         // Force CPU deblocking: on all block sizes if requested, else only on large
3522         // blocks (the transcoder's default region); otherwise explicitly disable it.
3523         if (elem('xuastc_ldr_force_deblocking_on_all_block_sizes').checked)
3524            flags = flags | F.cDecodeFlagsForceDeblockFiltering.value;
3525         else if ((blockWidth * blockHeight) >= VERY_LARGE_BLOCK_SIZE_IN_PIXELS)
3526            flags = flags | F.cDecodeFlagsForceDeblockFiltering.value;
3527         else
3528            flags = flags | F.cDecodeFlagsNoDeblockFiltering.value;
3529      }
3530      else
3531      {
3532         // Disable CPU deblocking during transcoding (display uses GPU or none).
3533         flags = flags | F.cDecodeFlagsNoDeblockFiltering.value;
3534      }
3535
3536      return flags;
3537   }
3538
3539   // Exports the currently loaded/displayed .KTX2 to a .DDS file (transcoding every
3540   // mip level, array layer, and cubemap face to the format chosen in the
3541   // #dds-export-format dropdown) and downloads it. Uses the encoder build's
3542   // KTX2File.exportToDDS() / getDDSData().
3543   // Runs a synchronous, main-thread function with a busy ('wait') cursor shown during it. The work is
3544   // deferred one frame (double rAF) so the browser actually paints the cursor before the blocking call,
3545   // and the cursor is ALWAYS restored in finally -- so it can never get stuck on any return/throw path.
3546   function withBusyCursor(fn)
3547   {
3548      document.body.style.cursor = 'wait';
3549      requestAnimationFrame(function () { requestAnimationFrame(function ()
3550      {
3551         try { fn(); }
3552         finally { document.body.style.cursor = ''; }
3553      }); });
3554   }
3555
3556   function downloadExportedDDS() { withBusyCursor(downloadExportedDDSImpl); }
3557
3558   function downloadExportedDDSImpl()
3559   {
3560      // A loaded .DDS source is view-only for now (no DDS->
3560DDS re-export path yet).
3561      if (getLoadedSourceKind() === 'dds')
3562      {
3563         updateErrorLine('Export is not supported for .DDS sources yet.');
3564         return;
3565      }
3566      // Nothing loaded/encoded yet -> nothing to export, so bail with an error.
3567      if (!curLoadedKTX2Data)
3568      {
3569         updateErrorLine('No .KTX2 currently loaded to export. Load or encode a texture first.');
3570         return;
3571      }
3572
3573      var ktx2Bytes = new Uint8Array(curLoadedKTX2Data);
3574      if (!ktx2Bytes.length)
3575      {
3576         updateErrorLine('No .KTX2 currently loaded to export. Load or encode a texture first.');
3577         return;
3578      }
3579
3580      var sel = document.getElementById('dds-export-format');
3581      var format = parseInt(sel.value, 10);
3582      var fmtLabel = sel.options[sel.selectedIndex].text.split(' ')[0];
3583
3584      var ktx2File = new Module.KTX2File(ktx2Bytes);
3585      if (!ktx2File.isValid())
3586      {
3587         updateErrorLine('Export failed: the encoded .KTX2 is invalid.');
3588         ktx2File.close();
3589         ktx2File.delete();
3590         return;
3591      }
3592
3593      // Match the decode flags the normal transcode path uses (from the Decode Flags UI).
3594      var decodeFlags = composeTranscodeDecodeFlags(ktx2File.getBlockWidth(), ktx2File.getBlockHeight());
3595      // exportToDDS() calls start_transcoding() internally and returns the .DDS
3596      // size in bytes (0 on failure, e.g. requesting an LDR format from an HDR texture).
3597      // srgb_mode -1 = auto: follow the KTX2's transfer function (HDR -> linear formats).
3598      var numBytes = ktx2File.exportToDDS(format, -1, decodeFlags);
3599      if (!numBytes)
3600      {
3601         updateErrorLine('DDS export failed. The chosen format may be unsupported for this texture (e.g. a BC1-7/RGBA32 LDR format from an HDR texture; HDR supports only BC6H).');
3602         ktx2File.close();
3603         ktx2File.delete();
3604         return;
3605      }
3606
3607      var ddsData = new Uint8Array(numBytes);
3608      var ok = ktx2File.getDDSData(ddsData);
3609
3610      ktx2File.close();
3611      ktx2File.delete();
3612
3613      if (!ok)
3614      {
3615         updateErrorLine('Failed retrieving the exported .DDS data.');
3616         return;
3617      }
3618
3619      download_file('exported_' + fmtLabel + '.dds', ddsData);
3620      log('Exported .DDS (' + fmtLabel + ', ' + numBytes + ' bytes).');
3621   }
3622
3623   // Exports the currently loaded/displayed .KTX2 to a compressed KTX1 (.ktx) file
3624   // (transcoding every mip level, array layer, and cubemap face to the format chosen
3625   // in the #ktx-export-format dropdown) and downloads it. Uses the encoder build's
3626   // KTX2File.exportToKTX() / getKTXData().
3627   function downloadExportedKTX() { withBusyCursor(downloadExportedKTXImpl); }
3628
3629   function downloadExportedKTXImpl()
3630   {
3631      // A loaded .DDS source is view-only for now (no DDS->KTX re-export path yet).
3632      if (getLoadedSourceKind() === 'dds')
3633      {
3634         updateErrorLine('Export is not supported for .DDS sources yet.');
3635         return;
3636      }
3637      // Nothing loaded/encoded yet -> nothing to export, so bail with an error.
3638      if (!curLoadedKTX2Data)
3639      {
3640         updateErrorLine('No .KTX2 currently loaded to export. Load or encode a texture first.');
3641         return;
3642      }
3643
3644      var ktx2Bytes = new Uint8Array(curLoadedKTX2Data);
3645      if (!ktx2Bytes.length)
3646      {
3647         updateErrorLine('No .KTX2 currently loaded to export. Load or encode a texture first.');
3648         return;
3649      }
3650
3651      var sel = document.getElementById('ktx-export-format');
3652      var format = parseInt(sel.value, 10);
3653      // Sanitize the option label into a filename token (e.g. "ASTC LDR 6x6" -> "ASTC_LDR_6x6").
3654      var fmtLabel = sel.options[sel.selectedIndex].text.replace(/[^A-Za-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
3655
3656      var ktx2File = new Module.KTX2File(ktx2Bytes);
3657      if (!ktx2File.isValid())
3658      {
3659         updateErrorLine('Export failed: the encoded .KTX2 is invalid.');
3660         ktx2File.close();
3661         ktx2File.delete();
3662         return;
3663      }
3664
3665      // Match the decode flags the normal transcode path uses (from the Decode Flags UI).
3666      var decodeFlags = composeTranscodeDecodeFlags(ktx2File.getBlockWidth(), ktx2File.getBlockHeight());
3667      // exportToKTX() calls start_transcoding() internally and returns the .KTX size
3668      // in bytes (0 on failure). srgb_mode -1 = auto (only affects ASTC LDR output).
3669      var numBytes = ktx2File.exportToKTX(format, -1, decodeFlags);
3670      if (!numBytes)
3671      {
3672         updateErrorLine('KTX export failed. The chosen format may be unsupported for this texture (e.g. an LDR format from an HDR texture, or a non-power-of-2 texture for PVRTC1).');
3673         ktx2File.close();
3674         ktx2File.delete();
3675         return;
3676      }
3677
3678      var ktxData = new Uint8Array(numBytes);
3679      var ok = ktx2File.getKTXData(ktxData);
3680
3681      ktx2File.close();
3682      ktx2File.delete();
3683
3684      if (!ok)
3685      {
3686         updateErrorLine('Failed retrieving the exported .KTX data.');
3687         return;
3688      }
3689
3690      download_file('exported_' + fmtLabel + '.ktx', ktxData);
3691      log('Exported .KTX (' + fmtLabel + ', ' + numBytes + ' bytes).');
3692   }
3693
3694   // Renders the current framebuffer to a PNG Blob and passes it to onBlob(blob).
3695   // Calls onBlob(null) if the framebuffer isn't ready or PNG encoding fails.
3696   function getFramebufferPNGBlob(onBlob)
3697   {
3698      if (!width || !displayWidth || !displayHeight)
3699      {
3700         onBlob(null);
3701         return;
3702      }
3703
3704      // Ensure the canvas contents are fresh.
3705      redraw();
3706
3707      var canvas = elem('canvas');
3708      var gl = canvas.getContext('webgl');
3709
3710      // Read the framebuffer. WebGL readPixels returns rows bottom-to-top.
3711      var pixels = new Uint8Array(displayWidth * displayHeight * 4);
3712      gl.readPixels(0, 0, displayWidth, displayHeight, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
3713
3714      // Flip vertically into a temporary canvas via putImageData.
3715      var tmpCanvas = document.createElement('canvas');
3716      tmpCanvas.width = displayWidth;
3717      tmpCanvas.height = displayHeight;
3718      var ctx = tmpCanvas.getContext('2d');
3719      var imageData = ctx.createImageData(displayWidth, displayHeight);
3720
3721      for (var y = 0; y < displayHeight; y++)
3722      {
3723         var srcRow = (displayHeight - 1 - y) * displayWidth * 4;
3724         var dstRow = y * displayWidth * 4;
3725         imageData.data.set(pixels.subarray(srcRow, srcRow + displayWidth * 4), dstRow);
3726      }
3727
3728      ctx.putImageData(imageData, 0, 0);
3729
3730      tmpCanvas.toBlob(function(blob) {
3731         onBlob(blob || null);
3732      }, 'image/png');
3733   }
3734   function saveFramebufferAsPNG()
3735   {
3736      getFramebufferPNGBlob(function(blob) {
3737         if (!blob)
3738            return;
3739         var url = URL.createObjectURL(blob);
3740         var a = document.createElement('a');
3741         a.href = url;
3742         a.download = 'framebuffer.png';
3743         document.body.appendChild(a);
3744         a.click();
3745         document.body.removeChild(a);
3746         URL.revokeObjectURL(url);
3747      });
3748   }
3749   function copyFramebufferToClipboard()
3750   {
3751      if (!navigator.clipboard || typeof navigator.clipboard.write !== 'function')
3752      {
3753         updateErrorLine('Clipboard image writing is not available. Serve this page over https:// or localhost, and use a recent browser.');
3754         return;
3755      }
3756      // Pre-check framebuffer readiness here (in addition to the same check inside
3757      // getFramebufferPNGBlob) so we can show a specific "no framebuffer" message
3758      // rather than the generic clipboard-write failure that would otherwise surface.
3759      if (!width || !displayWidth || !displayHeight)
3760      {
3761         updateErrorLine('No framebuffer to copy.');
3762         return;
3763      }
3764      // Construct the ClipboardItem synchronously inside the user-gesture click handler,
3765      // passing a Promise<Blob> rather than awaiting the blob first. Safari (and stricter
3766      // gesture-tracking in other browsers) requires this so the gesture isn't considered
3767      // expired by the time toBlob resolves.
3768      var blobSize = 0;
3769      var blobPromise = new Promise(function(resolve, reject) {
3770         getFramebufferPNGBlob(function(blob) {
3771            if (!blob) { reject(new Error('PNG encoding failed')); return; }
3772            blobSize = blob.size;
3773            resolve(blob);
3774         });
3775      });
3776      // Suppress unhandled-rejection warnings on blobPromise itself; the rejection
3777      // (if any) is also surfaced through clipboard.write's promise below.
3778      blobPromise.catch(function() {});
3779      navigator.clipboard.write([
3780         new ClipboardItem({ 'image/png': blobPromise })
3781      ]).then(function() {
3782         updateStatusLine('Copied framebuffer to clipboard.');
3783         log('------');
3784         log('Copied framebuffer to clipboard (' + blobSize + ' bytes, PNG).');
3785      }).catch(function(err) {
3786         updateErrorLine('Failed to copy framebuffer to clipboard.');
3787         log('Clipboard write failed: ' + err);
3788      });
3789   }
3790   function disabledFormatsChanged()
3791   {
3792      updateSupportedFormats();
3793      updateDisableButtons();
3794
3795      retranscodeCurrentTexture();
3796   }
3797
3798   function disableASTC()
3799   {
3800      astcDisabled = !astcDisabled;
3801      disabledFormatsChanged();
3802   }
3803
3804   function disableETC1()
3805   {
3806      etcDisabled = !etcDisabled;
3807      disabledFormatsChanged();
3808   }
3809
3810   function disableDXT()
3811   {
3812      dxtDisabled = !dxtDisabled;
3813      disabledFormatsChanged();
3814   }
3815
3816   function disablePVRTC()
3817   {
3818      pvrtcDisabled = !pvrtcDisabled;
3819      disabledFormatsChanged();
3820   }
3821
3822   function disableBC7()
3823   {
3824      bc7Disabled = !bc7Disabled;
3825      disabledFormatsChanged();
3826   }
3827
3828   function disableASTCHDR()
3829   {
3830      astcHDRDisabled = !astcHDRDisabled;
3831      disabledFormatsChanged();
3832   }
3833
3834   function disableBC6H()
3835   {
3836      bc6hDisabled = !bc6hDisabled;
3837      disabledFormatsChanged();
3838   }
3839
3840   function disableRGBA_HALF()
3841   {
3842      rgbaHalfDisabled = !rgbaHalfDisabled;
3843      disabledFormatsChanged();
3844   }
3845
3846   // BC4/BC5 (RGTC) toggle. Only affects the DDS viewer.
3847   function disableRGTC()
3848   {
3849      rgtcDisabled = !rgtcDisabled;
3850      disabledFormatsChanged();
3851   }
3852
3853   function disableETC2()
3854   {
3855      etc2Disabled = !etc2Disabled;
3856      disabledFormatsChanged();
3857   }
3858
3859   function disableEAC()
3860   {
3861      eacDisabled = !eacDisabled;
3862      disabledFormatsChanged();
3863   }
3864
3865   function disableAllFormats()
3866   {
3867      astcDisabled = true;
3868      etcDisabled = true;
3869      dxtDisabled = true;
3870      pvrtcDisabled = true;
3871      bc7Disabled = true;
3872      astcHDRDisabled = true;
3873      bc6hDisabled = true;
3874      rgbaHalfDisabled = true;
3875      rgtcDisabled = true;
3876      etc2Disabled = true;
3877      eacDisabled = true;
3878      disabledFormatsChanged();
3879   }
3880
3881   function enableAllFormats()
3882   {
3883      astcDisabled = false;
3884      etcDisabled = false;
3885      dxtDisabled = false;
3886      pvrtcDisabled = false;
3887      bc7Disabled = false;
3888      astcHDRDisabled = false;
3889      bc6hDisabled = false;
3890      rgbaHalfDisabled = false;
3891      rgtcDisabled = false;
3892      etc2Disabled = false;
3893      eacDisabled = false;
3894      disabledFormatsChanged();
3895   }
3896
3897   function updateDisableButtons()
3898   {
3899      const buttonsDiv = elem('disable-buttons');
3900      buttonsDiv.innerHTML = '';  // Clear any previous buttons
3901
3902      let buttonCount = 0; // Keep track of how many buttons are added
3903
3904      function addButton(buttonHTML)
3905      {
3906         if (buttonCount > 0 && buttonCount % 3 === 0) {
3907            // Insert a line break after every 3rd button
3908            buttonsDiv.innerHTML += '<br>';
3909         }
3910         buttonsDiv.innerHTML += buttonHTML;
3911         buttonCount++;
3912      }
3913
3914      if (astcSupported)
3915      {
3916         if (astcDisabled)
3917            addButton('<button onclick="disableASTC()">Enable ASTC LDR</button>');
3918         else
3919            addButton('<button onclick="disableASTC()">Disable ASTC LDR</button>');
3920      }
3921
3922      if (etcSupported)
3923      {
3924         if (etcDisabled)
3925            addButton('<button onclick="disableETC1()">Enable ETC1</button>');
3926         else
3927            addButton('<button onclick="disableETC1()">Disable ETC1</button>');
3928      }
3929
3930      if (dxtSupported)
3931      {
3932         if (dxtDisabled)
3933            addButton('<button onclick="disableDXT()">Enable BC1/BC3</button>');
3934         else
3935            addButton('<button onclick="disableDXT()">Disable BC1/BC3</button>');
3936      }
3937
3938      if (pvrtcSupported)
3939      {
3940         if (pvrtcDisabled)
3941            addButton('<button onclick="disablePVRTC()">Enable PVRTC</button>');
3942         else
3943            addButton('<button onclick="disablePVRTC()">Disable PVRTC</button>');
3944      }
3945
3946      if (bc7Supported)
3947      {
3948         if (bc7Disabled)
3949            addButton('<button onclick="disableBC7()">Enable BC7</button>');
3950         else
3951            addButton('<button onclick="disableBC7()">Disable BC7</button>');
3952      }
3953
3954      if (astcHDRSupported)
3955      {
3956         if (astcHDRDisabled)
3957            addButton('<button onclick="disableASTCHDR()">Enable ASTC HDR</button>');
3958         else
3959            addButton('<button onclick="disableASTCHDR()">Disable ASTC HDR</button>');
3960      }
3961
3962      if (bc6hSupported)
3963      {
3964         if (bc6hDisabled)
3965            addButton('<button onclick="disableBC6H()">Enable BC6H</button>');
3966         else
3967            addButton('<button onclick="disableBC6H()">Disable BC6H</button>');
3968      }
3969
3970      if (rgbaHalfSupported)
3971      {
3972         if (rgbaHalfDisabled)
3973            addButton('<button onclick="disableRGBA_HALF()">Enable RGBA_HALF</button>');
3974         else
3975            addButton('<button onclick="disableRGBA_HALF()">Disable RGBA_HALF</button>');
3976      }
3977
3978      // BC4/BC5 (RGTC): only meaningful for DDS sources, but the toggle is always shown when supported.
3979      if (rgtcSupported)
3980      {
3981         if (rgtcDisabled)
3982            addButton('<button onclick="disableRGTC()">Enable BC4/BC5</button>');
3983         else
3984            addButton('<button onclick="disableRGTC()">Disable BC4/BC5</button>');
3985      }
3986
3987      // ETC2 RGBA + EAC R11/RG11 (from WEBGL_compressed_texture_etc): shown when supported (typically mobile).
3988      if (etc2Supported)
3989      {
3990         if (etc2Disabled)
3991            addButton('<button onclick="disableETC2()">Enable ETC2 RGBA</button>');
3992         else
3993            addButton('<button onclick="disableETC2()">Disable ETC2 RGBA</button>');
3994      }
3995
3996      if (eacSupported)
3997      {
3998         if (eacDisabled)
3999            addButton('<button onclick="disableEAC()">Enable EAC R11/RG11</button>');
4000         else
4001            addButton('<button onclick="disableEAC()">Disable EAC R11/RG11</button>');
4002      }
4003
4004      addButton('<button onclick="disableAllFormats()">Disable All</button>');
4005      addButton('<button onclick="enableAllFormats()">Enable All</button>');
4006   }
4007
4008   function updateSupportedFormats()
4009   {
4010      let supportedFormats = [];
4011
4012      // Collect all supported formats
4013      if (astcSupported && !astcDisabled)
4014         supportedFormats.push('ASTC LDR');
4015
4016      if (etcSupported && !etcDisabled)
4017         supportedFormats.push('ETC1');
4018
4019      if (dxtSupported && !dxtDisabled)
4020         supportedFormats.push('BC1-3');
4021
4022      if (pvrtcSupported && !pvrtcDisabled)
4023         supportedFormats.push('PVRTC');
4024
4025      if (bc7Supported && !bc7Disabled)
4026         supportedFormats.push('BC7');
4027
4028      if (astcHDRSupported && !astcHDRDisabled)
4029         supportedFormats.push('ASTC HDR');
4030
4031      if (bc6hSupported && !bc6hDisabled)
4032         supportedFormats.push('BC6H');
4033
4034      if (rgbaHalfSupported && !rgbaHalfDisabled)
4035         supportedFormats.push('RGBA_HALF');
4036
4037      if (rgtcSupported && !rgtcDisabled)
4038         supportedFormats.push('BC4/BC5');
4039
4040      if (etc2Supported && !etc2Disabled)
4041         supportedFormats.push('ETC2 RGBA');
4042
4043      if (eacSupported && !eacDisabled)
4044         supportedFormats.push('EAC R11/RG11');
4045
4046      // Prepare the display string
4047      let supportedString = 'Supported WebGL formats: ';
4048
4049      if (supportedFormats.length > 0)
4050      {
4051         for (let i = 0; i < supportedFormats.length; i++)
4052         {
4053            supportedString += supportedFormats[i];
4054            // Add a comma after each format except the last
4055            if (i < supportedFormats.length - 1)
4056            {
4057               supportedString += ', ';
4058            }
4059
4060            // Start a new line after every 4 formats
4061            if ((i + 1) % 4 === 0)
4062            {
4063               supportedString += '<br>';
4064            }
4065         }
4066      }
4067      else
4068      {
4069         supportedString = 'No WebGL formats detected/enabled; using uncompressed fallback formats.';
4070      }
4071
4072      // Update the HTML element
4073      elem('supported-formats').innerHTML = supportedString;
4074   }
4075
4076   function checkForGPUFormatSupport(gl)
4077   {
4078      astcSupported = !!gl.getExtension('WEBGL_compressed_texture_astc');
4079      etcSupported = !!gl.getExtension('WEBGL_compressed_texture_etc1');
4080      dxtSupported = !!gl.getExtension('WEBGL_compressed_texture_s3tc');
4081      pvrtcSupported = !!(gl.getExtension('WEBGL_compressed_texture_pvrtc')) || !!(gl.getExtension('WEBKIT_WEBGL_compressed_texture_pvrtc'));
4082      bc7Supported = !!gl.getExtension('EXT_texture_compression_bptc');
4083      // BC4/BC5 come from the same RGTC extension (used only by the DDS viewer).
4084      rgtcSupported = !!gl.getExtension('EXT_texture_compression_rgtc');
4085
4086      // ETC2 (RGB/RGBA) + EAC (R11/RG11) come from WEBGL_compressed_texture_etc (NOT the ETC1-only
4087      // WEBGL_compressed_texture_etc1 above). ETC2 RGBA is used by both the KTX2 and DDS transcode paths;
4088      // EAC R11/RG11 are used by the DDS path only (channel-matched fallback for R8/R8G8 / BC4/BC5).
4089      {
4090         var etc2Ext = gl.getExtension('WEBGL_compressed_texture_etc');
4091         if (etc2Ext)
4092         {
4093            etc2Supported = !!etc2Ext.COMPRESSED_RGBA8_ETC2_EAC;  // ETC2 RGB / RGBA
4094            eacSupported  = !!etc2Ext.COMPRESSED_RG11_EAC;        // EAC R11 / RG11
4095         }
4096      }
4097
4098      // Check for BC6H support
4099      {
4100         var ext = gl.getExtension('EXT_texture_compression_bptc');
4101         if (ext)
4102         {
4103            bc6hSupported = !!ext.COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_EXT;
4104         }
4105      }
4106
4107      // Check for ASTC HDR support
4108      {
4109         var ext = gl.getExtension('WEBGL_compressed_texture_astc');
4110
4111         if (ext)
4112         {
4113            var supportedProfiles = ext.getSupportedProfiles();
4114
4115            var hdrProfiles = supportedProfiles.filter(profile => profile.includes('hdr'));
4116
4117            if (hdrProfiles.length > 0)
4118            {
4119               astcHDRSupported = true;
4120            }
4121         }
4122      }
4123
4124      // Check for half-float texture support.
4125      {
4126         var ext = gl.getExtension('OES_texture_half_float');
4127         if (ext)
4128         {
4129            rgbaHalfSupported = true;
4130            halfFloatWebGLFormat = ext.HALF_FLOAT_OES;
4131         }
4132      }
4133
4134      // HACK HACK - for testing uncompressed fallbacks.
4135      //astcSupported = false;
4136      //etcSupported = false;
4137      //dxtSupported = false;
4138      //bc7Supported = false;
4139      //pvrtcSupported = false;
4140      //bc6hSupported = false;
4141      //astcHDRSupported = false;
4142      //rgbaHalfSupported = false;
4143
4144      console.log('astcSupported: ' + astcSupported);
4145      console.log('etcSupported: ' + etcSupported);
4146      console.log('etc2Supported (ETC2 RGB/RGBA): ' + etc2Supported);
4147      console.log('eacSupported (EAC R11/RG11): ' + eacSupported);
4148      console.log('dxtSupported: ' + dxtSupported);
4149      console.log('bc7Supported: ' + bc7Supported);
4150      console.log('pvrtcSupported: ' + pvrtcSupported);
4151      console.log('bc6hSupported: ' + bc6hSupported);
4152      console.log('astcHDRSupported: ' + astcHDRSupported);
4153      console.log('rgbaHalfSupported: ' + rgbaHalfSupported);
4154      console.log('rgtcSupported (BC4/BC5): ' + rgtcSupported);
4155
4156      updateSupportedFormats();
4157      updateDisableButtons();
4158   }
4159
4160   function validateNitMultiplier(input)
4161   {
4162      const warning = document.getElementById('ldr-to-hdr-warning');
4163      const value = parseFloat(input.value);
4164
4165      if (isNaN(value) || value < 0.1 || value > 1000)
4166      {
4167        // Show warning if value is invalid
4168        warning.style.display = 'inline';
4169      }
4170      else
4171      {
4172           // Hide warning if value is valid
4173         warning.style.display = 'none';
4174      }
4175   }
4176
4177   function getNitMultiplier()
4178   {
4179      const input = document.getElementById('ldr-to-hdr-multiplier');
4180      const value = parseFloat(input.value);
4181
4182      if (!isNaN(value) && value >= 0.1 && value <= 1000)
4183      {
4184         return value;
4185      }
4186      else
4187      {
4188         log('Invalid LDR to HDR Nit Multiplier value. Defaulting to 100.0');
4189         return 100.0; // Default value if invalid
4190      }
4191   }
4192
4193   function getSelectedBasisTexFormat()
4194   {
4195      const selectElem = document.getElementById('basis-tex-format');
4196      const selectedValue = parseInt(selectElem.value, 10);
4197
4198      return selectedValue;
4199   }
4200
4201   function getXUASTCLDRSyntax()
4202   {
4203      const selectElem = document.getElementById('xuastc_ldr_syntax');
4204      const selectedValue = parseInt(selectElem.value, 10);
4205
4206      return selectedValue;
4207   }
4208
4209   function getASTC6x6RDOLambda()
4210   {
4211      const input = document.getElementById('astc6x6-rdo-lambda');
4212      const value = parseFloat(input.value);
4213
4214      if (!isNaN(value) && value >= 0.0 && value <= 1000000)
4215      {
4216         return value;
4217      }
4218      else
4219      {
4220         log('Invalid lambda value. Defaulting to 0.0');
4221         return 0.0; // Default value if invalid
4222      }
4223   }
4224
4225   function showBusyModal()
4226   {
4227      //console.log("Showing modal");
4228      document.getElementById('busy-modal').style.display = 'block';
4229   }
4230
4231   function hideBusyModal()
4232   {
4233      //console.log("Hiding modal");
4234      document.getElementById('busy-modal').style.display = 'none';
4235   }
4236
4237   function setDropdownValue(selectId, value)
4238   {
4239      const selectElement = document.getElementById(selectId);
4240      if (!selectElement)
4241         return;
4242
4243      selectElement.value = value;
4244      selectElement.dispatchEvent(new Event('change'));
4245   }
4246
4247   function globalInit()
4248   {
4249      elem('SRGB').checked = true;
4250      updateViewModeButtons();
4251
4252      var gl = elem('canvas').getContext('webgl');
4253      checkForGPUFormatSupport(gl);
4254
4255      // Snapshot the browser's chosen default color space so the UI can revert to it.
4256      // null on browsers that don't expose the property (treated as "no control available").
4257      window.originalCanvasColorSpace =
4258         (typeof gl.drawingBufferColorSpace !== 'undefined') ? gl.drawingBufferColorSpace : null;
4259      window.renderer = new Renderer(gl);
4260
4261      elem('file').addEventListener('keydown', function (e)
4262      {
4263          if (e.keyCode == 13) {
4264              runLoadFile();
4265          }
4266      }, false);
4267
4268      elem('imagefile').addEventListener('keydown', function (e)
4269      {
4270          if (e.keyCode == 13) {
4271              runEncodeImageFile();
4272          }
4273      }, false);
4274
4275      {
4276          let etc1SLevelSlider = document.getElementById('etc1s-comp-level-slider');
4277          let etc1sLevelSliderValueDisplay = document.getElementById('etc1s-comp-level-slider-value');
4278          etc1SLevelSlider.oninput = function()
4279          {
4280              etc1sLevelSliderValueDisplay.textContent = this.value;
4281          }
4282      }
4283
4284      {
4285          let uastcHDRSlider = document.getElementById('uastc-hdr-quality-slider');
4286          let qualityHDRValueDisplay = document.getElementById('uastc-hdr-quality-value');
4287          uastcHDRSlider.oninput = function()
4288          {
4289              qualityHDRValueDisplay.textContent = this.value;
4290          }
4291      }
4292
4293      {
4294          let astcHDR6x6Slider = document.getElementById('astc-hdr6x6-comp-level-slider');
4295          let compLevelValueDisplay = document.getElementById('astc-hdr6x6-comp-level-value');
4296          astcHDR6x6Slider.oninput = function()
4297          {
4298              compLevelValueDisplay.textContent = this.value;
4299          }
4300      }
4301
4302      {
4303          let uastcLDRSlider = document.getElementById('uastc-ldr-quality-slider');
4304          let qualityLDRValueDisplay = document.getElementById('uastc-ldr-quality-value');
4305          uastcLDRSlider.oninput = function()
4306          {
4307              qualityLDRValueDisplay.textContent = this.value;
4308          }
4309      }
4310
4311      {
4312          let rdoSlider = document.getElementById('rdo-quality-slider');
4313          let rdoValueDisplay = document.getElementById('rdo-quality-value');
4314          rdoSlider.oninput = function()
4315          {
4316              rdoValueDisplay.textContent = parseFloat(this.value).toFixed(1);
4317          }
4318      }
4319
4320      {
4321          let etc1SQualitySlider = document.getElementById('EncodeQuality');
4322          let etc1SQualitySliderValue = document.getElementById('encode-quality-value');
4323          etc1SQualitySlider.oninput = function()
4324          {
4325              etc1SQualitySliderValue.textContent = parseFloat(this.value).toFixed(0);
4326          }
4327      }
4328
4329      {
4330          let xuastcLDREffortSlider = document.getElementById('xuastc_ldr_effort_level_slider');
4331          let xuastcLDREffortValue = document.getElementById('xuastc_ldr_effort_level_value');
4332          xuastcLDREffortSlider.oninput = function()
4333          {
4334              xuastcLDREffortValue.textContent = this.value;
4335          }
4336      }
4337
4338      {
4339          let xuastcLDRDCTSlider = document.getElementById('xuastc_ldr_dct_quality_slider');
4340          let xuastcLDRDCTValue = document.getElementById('xuastc_ldr_dct_quality_value');
4341          xuastcLDRDCTSlider.oninput = function()
4342          {
4343              xuastcLDRDCTValue.textContent = this.value;
4344          }
4345      }
4346      {
4347          let deblockingPassesSlider = document.getElementById('deblocking-num-passes-slider');
4348          let deblockingPassesValue  = document.getElementById('deblocking-num-passes-value');
4349          deblockingPassesSlider.oninput = function()
4350          {
4351              deblockingPassesValue.textContent = this.value;
4352          }
4353      }
4354      {
4355          let xubc7RDOSlider = document.getElementById('xubc7_rdo_level_slider');
4356          let xubc7RDOValue  = document.getElementById('xubc7_rdo_level_value');
4357          xubc7RDOSlider.oninput = function()
4358          {
4359              xubc7RDOValue.textContent = this.value;
4360          }
4361      }
4362      {
4363          let xubc7StripesSlider = document.getElementById('xubc7_num_stripes_slider');
4364          let xubc7StripesValue  = document.getElementById('xubc7_num_stripes_value');
4365          xubc7StripesSlider.oninput = function()
4366          {
4367              xubc7StripesValue.textContent = this.value;
4368          }
4369      }
4370      {
4371          let xubc7Bc7eLevelSlider = document.getElementById('xubc7_bc7e_scalar_level_slider');
4372          let xubc7Bc7eLevelValue  = document.getElementById('xubc7_bc7e_scalar_level_value');
4373          xubc7Bc7eLevelSlider.oninput = function()
4374          {
4375              xubc7Bc7eLevelValue.textContent = this.value;
4376          }
4377      }
4378
4379      // Wire the two "Loaded: ..." displays for source image and .ktx2 file.
4380      // The display text is updated by updateLoadedSourceDisplay() /
4381      // updateLoadedFormatDisplay(), which are called from every load site that
4382      // assigns curLoadedImageURI or curLoadedKTX2URI.
4383      {
4384          function wireCopyButton(btnId, getUri)
4385          {
4386              const btn = document.getElementById(btnId);
4387              if (!btn) return;
4388              btn.addEventListener('click', async function()
4389              {
4390                  const uri = getUri();
4391                  if (!uri) return;
4392                  const originalText = btn.textContent;
4393                  try
4394                  {
4395                      await navigator.clipboard.writeText(uri);
4396                      btn.textContent = 'Copied!';
4397                  }
4398                  catch (err)
4399                  {
4400                      // Fallback for environments where the async clipboard API
4401                      // is unavailable (e.g. some older browsers or non-HTTPS).
4402                      try
4403                      {
4404                          const ta = document.createElement('textarea');
4405                          ta.value = uri;
4406                          ta.style.position = 'fixed';
4407                          ta.style.opacity = '0';
4408                          document.body.appendChild(ta);
4409                          ta.select();
4410                          document.execCommand('copy');
4411                          document.body.removeChild(ta);
4412                          btn.textContent = 'Copied!';
4413                      }
4414                      catch (err2)
4415                      {
4416                          btn.textContent = 'Copy failed';
4417                          log('Copy to clipboard failed: ' + err2);
4418                      }
4419                  }
4420                  setTimeout(function() { btn.textContent = originalText; }, 1200);
4421              });
4422          }
4423          wireCopyButton('copy-loaded-source-uri-btn', function() { return curLoadedImageURI; });
4424          wireCopyButton('copy-loaded-ktx2-uri-btn',   function() { return getLoadedSourceKind() === 'dds' ? curLoadedDDSURI : curLoadedKTX2URI; });
4425      }
4426     
4427      {
4428        document.getElementById("unified-effort-slider").oninput = function() 
4429        {
4430            document.getElementById("unified-effort-value").textContent = this.value;
4431        }
4432
4433        document.getElementById("unified-quality-slider").oninput = function() 
4434        {
4435            document.getElementById("unified-quality-value").textContent = this.value;
4436        }
4437      }
4438      
4439      runLoadFile();
4440   }
4441
4442   // Single source of truth for the message line. Pass '' (or whitespace) to clear.
4443   // type is 'error' (red) or 'status' (gray).
4444   function updateMessageLine(message, type)
4445   {
4446     const line = document.getElementById('error-line');
4447     line.textContent = message;
4448     if (!message.trim())
4449     {
4450        line.style.color = '';
4451        return;
4452     }
4453     line.style.color = (type === 'status') ? 'green' : 'red';
4454   }
4455   function updateErrorLine(message)  { updateMessageLine(message, 'error'); }
4456   function updateStatusLine(message) { updateMessageLine(message, 'status'); }
4457
4458   async function pasteImageFromClipboard()
4459   {
4460      // navigator.clipboard is undefined in non-secure contexts (file://, http://).
4461      // The function check guards against browsers (notably some Firefox versions)
4462      // where navigator.clipboard exists for text-only methods but .read is missing.
4463      if (!navigator.clipboard || typeof navigator.clipboard.read !== 'function')
4464      {
4465         updateErrorLine('Clipboard image reading is not available. Serve this page over https:// or localhost, and use a recent browser.');
4466         return;
4467      }
4468      let clipboardItems;
4469      try
4470      {
4471         clipboardItems = await navigator.clipboard.read();
4472      }
4473      catch (err)
4474      {
4475         updateErrorLine('Could not read clipboard. Browser permission was denied or unavailable.');
4476         log('Clipboard read failed: ' + err);
4477         return;
4478      }
4479      // MIME types we support pasting. createImageBitmap (used by browserDecodeToRGBA
4480      // via loadAndCompressImage -> compressImage) handles all of these in modern browsers.
4481      const SUPPORTED = {
4482         'image/png':  'png',
4483         'image/jpeg': 'jpg',
4484         'image/webp': 'webp',
4485         'image/bmp':  'bmp',
4486         'image/gif':  'gif'
4487      };
4488      for (const clipboardItem of clipboardItems)
4489      {
4490         for (const type of clipboardItem.types)
4491         {
4492            const ext = SUPPORTED[type];
4493            if (!ext) continue;
4494            let blob;
4495            try
4496            {
4497               blob = await clipboardItem.getType(type);
4498            }
4499            catch (err)
4500            {
4501               log('Clipboard image getType failed for ' + type + ': ' + err);
4502               continue;
4503            }
4504            // Wrap the blob as a File so it has .name and .size like the drag/drop path,
4505            // then reuse processFile() — same code path as drag-drop and file-picker.
4506            const fileName = 'clipboard-image.' + ext;
4507            const file = new File([blob], fileName, { type: type });
4508            updateErrorLine('');
4509            window.processFile(file);
4510            return;
4511         }
4512      }
4513      updateErrorLine('Clipboard does not contain a supported LDR/SDR bitmap image.');
4514   }
4515</script>
4515
4516
4517<style>
4518  /* NEW: make the page use the full viewport height */
4519  html, body { margin: 0; height: 100%; }
4520
4521  .wrap {
4522    display:flex;
4523    gap:16px;
4524    align-items:flex-start;
4525    padding:12px;
4526
4527    /* NEW: make the flex row fill the viewport and respect padding */
4528    height: 100vh;
4529    box-sizing: border-box;
4530    overflow: hidden;
4531  }
4532
4533  /* Fixed-width controls column (never shrinks), scrolls independently */
4534  .controls { flex: 0 0 600px; width: 600px; overflow-y: auto; max-height: calc(100vh - 24px); }
4535
4536  /* Viewer takes the remaining space and scrolls when texture is huge */
4537  /* EDIT: cap height so vertical scrolling appears inside the viewer */
4538  .viewer {
4539    flex: 1 1 0;
4540    min-width: 0;
4541    overflow: auto;
4542    max-height: calc(100vh - 24px); /* 24px = 12px top + 12px bottom padding in .wrap */
4543    cursor: grab;
4544  }
4545  .viewer.dragging {
4546    cursor: grabbing;
4547    user-select: none;
4548  }
4549  .active-btn {
4550    background-color: #4a90d9;
4551    color: white;
4552    border-style: inset;
4553  }
4554
4555  /* IMPORTANT: do NOT scale the canvas with CSS; let JS size it 1:1 */
4556  #canvas { display:block; image-rendering: pixelated; }
4557
4558  /* Small screens: stack vertically so it's still usable */
4559  @media (max-width: 1100px){
4560    .wrap{ flex-direction:column; height:auto; overflow:auto; }
4561    .controls{ flex: 0 0 auto; width:auto; max-height:none; overflow-y:visible; }
4562    .viewer{ flex: 1 1 auto; max-height:none; }
4563  }
4564  
4565  .log-panel 
4566  {
4567    margin-top: 12px;
4568    background: #111;
4569    color: #0f0;
4570    padding: 10px;
4571    font-family: monospace;
4572    font-size: 12px;
4573    line-height: 1.4em;
4574
4575    border: 1px solid #333;
4576    border-radius: 4px;
4577
4578    height: 250px;     /* adjust as desired */
4579    overflow-y: scroll;
4580    white-space: pre-wrap;
4581  }
4582  
4583</style>
4584
4585</head>
4586
4587<body>
4588<div id="safari-bug-banner" style="display:none; background:#c0392b; color:#ffffff; font-weight:bold; padding:10px 14px; text-align:center; border-bottom:2px solid #7b241c; font-family:sans-serif;">
4589&#9888; Your Safari version (26.0-26.2) has a WebAssembly memory bug that crashes multithreaded encoding. Encoding is running single-threaded (slower) as a workaround. For full speed, update to Safari 26.3+, or use Chrome or Firefox.
4590</div>
4591
4592<div class="wrap">
4593  <div class="controls">
4594
4595    <br>
4596    <div style="display: flex; align-items: center; gap: 12px; flex-wrap: wrap;">
4597    <div style="font-size: 24pt; font-weight: bold">
4598    Basis Universal .KTX2/.DDS Studio v2.50
4599      </div>
4600      <button type="button" onclick="document.getElementById('help-dialog').showModal()"
4601              style="font-size: 11pt; padding: 4px 10px;">Help / About</button>
4602    </div>
4603
4604    <br>This tool uses the <a href="https://github.com/BinomialLLC/basis_universal/">Basis Universal</a> C++ transcoder (compiled to WebAssembly using Emscripten) to transcode a .ktx2 file to:
4605    <br><b id='format'>FORMAT</b>
4606    <br>
4607    <dialog id="help-dialog" style="max-width: 640px; padding: 20px; border: 1px solid #888; border-radius: 6px;"
4608            onclick="if (event.target === this) this.close();">
4609      <h2 style="margin-top: 0;">About Basis Universal .KTX2/.DDS Studio</h2>
4610      <p>The viewer is implemented in WebGL and renders a single textured quad. It also supports encoding .PNG, .JPG, .QOI, .EXR or .HDR files to LDR or HDR .KTX2 files.</p>
4611      <p>Additional image formats (.WebP, .AVIF, .GIF, .BMP, .TIFF, .HEIC, .HEIF, .JXL) are supported via browser-assisted decoding (browser support varies by format).</p>
4612      <p>Thanks to Evan Parker for providing <a href="https://github.com/toji/webgl-texture-utils">webgl-texture-utils</a> and this test bed. <a href="../index.html">Go back.</a></p>
4613      <p><b>Notes:</b> Enable your browser debug console (F12 on Chrome/Firefox) to see debug output.
4614      The largest image resolution that can be compressed in the browser with this library is limited to either 16 megapixels or 4 megapixels (depending on format and WASM64/WASM32) to avoid running out of WASM memory.</p>
4615      <div style="text-align: right; margin-top: 16px;">
4616        <button type="button" onclick="document.getElementById('help-dialog').close()"
4617                style="padding: 4px 14px;">Close</button>
4618      </div>
4619    </dialog>
4620
4621    <br>
4622    <div id="supported-formats">
4623    Supported WebGL formats:
4624    </div>
4625
4626    <br>
4627    <div id="error-line">
4628    </div>
4629
4630    <div id="disable-buttons" style="margin-top: 10px;">
4631    <!-- Buttons will be dynamically added here -->
4632    </div>
4633
4634    <br>
4635    <div id="threading-indicator"> </div>
4636    
4637    <br>
4638    <input type="checkbox" id="multithreaded_encoding" checked>Use Multithreading (if available)
4639
4640    <br>Additional Worker Threads (Max 32):
4641    <input type="number" id="num_worker_threads" value="12" min="0" max="32">
4642    <br><br>
4643    
4644    <div id="deblocking-indicator"> </div>
4645    
4646    <hr style="width: 40%; margin: 10px 0; background-color: #ccc; border: none; height: 1px;">
4647    <b>Source Image / File Input:</b>
4648    <br>
4649    <div id="drop-area" style="width: 40%; height: 75px; border: 2px dashed #ccc; display: flex; justify-content: center; align-items: center; margin-top: 20px;">
4650    <p>Drag and drop a .KTX2, .DDS, or image file here, or click to select a file.</p>
4651    <input type="file" id="file-input" style="display: none;">
4652    </div>
4653
4654    <br>
4655    .ktx2 file:
4656    <input id="file" type="text" size=30 value="assets/kodim23.ktx2"></input>
4657    <input type="button" value="Transcode!" onclick="runLoadFile()"></input>
4658    <br>
4659    <div style="margin: 2px 0 0 60px; font-size: 0.85em; color: var(--ink-dim, #666); word-break: break-all;">
4660        Loaded:
4661        <span id="loaded-ktx2-uri-display" title="URI of the currently loaded .ktx2 file">(none)</span>
4662        <button id="copy-loaded-ktx2-uri-btn" type="button"
4663                style="margin-left: 6px; padding: 0 6px; font-size: 0.85em;"
4664                title="Copy this URI to the clipboard"
4665                disabled>Copy</button>
4666    </div>
4667
4668    <br>
4669    .png/.jpg/.webp/.exr/.hdr file:
4670    <input id="imagefile" type="text" size=30 value="assets/kodim06.png"></input>
4671    <input type="button" value="Encode!" onclick="encodeViaWorker()" title="Encode on a background worker thread: the UI stays responsive and a modal progress dialog is shown."></input>
4672    <input type="button" value="Encode (on Main Thread)" onclick="runEncodeImageFile()" title="Legacy/testing path: encodes on the main thread (freezes the UI). Also runs the on-main DTO validation check."></input>
4673    <br>
4674    <div style="margin: 2px 0 0 60px; font-size: 0.85em; color: var(--ink-dim, #666); word-break: break-all;">
4675        Loaded:
4676        <span id="loaded-source-uri-display" title="URI/filename of the currently loaded source image">(none)</span>
4677        <button id="copy-loaded-source-uri-btn" type="button"
4678                style="margin-left: 6px; padding: 0 6px; font-size: 0.85em;"
4679                title="Copy this URI to the clipboard"
4680                disabled>Copy</button>
4681    </div>
4682    <br>
4683    <select id="filename-dropdown" onchange="onFilenameSelect()">
4684    <option value="">--Select Example Image--</option>
4685    <option value="assets/desk.exr">desk.exr</option>
4686    <option value="assets/MtTamWest.exr">MtTamWest.exr</option>
4687    <option value="assets/sponza.exr">sponza.exr</option>
4688    <option value="assets/CandleGlass.exr">CandleGlass.exr</option>
4689
4690    <option value="assets/src_atrium.exr">src_atrium.exr</option>
4691    <option value="assets/src_backyard.exr">src_backyard.exr</option>
4692
4693    <option value="assets/src_memorial.exr">src_memorial.exr</option>
4694    <option value="assets/src_yucca.exr">src_yucca.exr</option>
4695    <option value="assets/Tree.exr">Tree.exr</option>
4696
4697    <option value="assets/kodim18_512x512.png">kodim18_512x512.png</option>
4698    <option value="assets/kodim18_64x64.png">kodim18_64x64.png</option>
4699
4700    <option value="assets/kodim01.png">kodim01.png</option>
4701    <option value="assets/kodim02.png">kodim02.png</option>
4702    <option value="assets/kodim03.png">kodim03.png</option>
4703    <option value="assets/kodim04.png">kodim04.png</option>
4704    <option value="assets/kodim05.png">kodim05.png</option>
4705    <option value="assets/kodim06.png">kodim06.png</option>
4706    <option value="assets/kodim07.png">kodim07.png</option>
4707    <option value="assets/kodim08.png">kodim08.png</option>
4708    <option value="assets/kodim09.png">kodim09.png</option>
4709    <option value="assets/kodim10.png">kodim10.png</option>
4710    <option value="assets/kodim11.png">kodim11.png</option>
4711    <option value="assets/kodim12.png">kodim12.png</option>
4712    <option value="assets/kodim13.png">kodim13.png</option>
4713    <option value="assets/kodim14.png">kodim14.png</option>
4714    <option value="assets/kodim15.png">kodim15.png</option>
4715    <option value="assets/kodim16.png">kodim16.png</option>
4716    <option value="assets/kodim17.png">kodim17.png</option>
4717    <option value="assets/kodim18.png">kodim18.png</option>
4718    <option value="assets/kodim19.png">kodim19.png</option>
4719    <option value="assets/kodim20.png">kodim20.png</option>
4720    <option value="assets/kodim21.png">kodim21.png</option>
4721    <option value="assets/kodim22.png">kodim22.png</option>
4722    <option value="assets/kodim23.png">kodim23.png</option>
4723
4724    <option value="assets/kodim24.png">kodim24.png</option>
4725    <option value="assets/kodim25.png">kodim25.png</option>
4726    <option value="assets/kodim26.png">kodim26.png</option>
4727
4728    <option value="assets/xmen.png">xmen.png</option>
4729    <option value="assets/tng.png">tng.png</option>
4730    <option value="assets/base.png">base.png</option>
4731
4732    </select>
4733        
4734    <br><br>
4735
4736    <input type="button" value="Download Encoded .KTX2/.DDS File" onclick="downloadEncodedFile()">
4737    <input type="button" value="Paste and Encode Image from Clipboard" onclick="pasteImageFromClipboard()">
4738
4739    <hr style="width: 40%; margin: 10px 0; background-color: #ccc; border: none; height: 1px;">
4740    <b>Encoding Settings Preset:</b>
4741    <select id="encoding-preset"
4742            onchange="applyEncodingPreset(this.value)"
4743            title="Encode-time presets. Sets sRGB perceptual flag and XUASTC LDR channel weights. Display-side options unchanged. Individual fields stay editable.">
4744    <option value="srgb_photo" selected
4745            title="sRGB perceptual: ON. Channel weights R=3 G=11 B=1 A=11 (RGB Rec. 709 luminance). Sharpening: Disabled. Encode deblocking: Auto. Deblocking-aware encoding: ON.">sRGB Photo (default)</option>
4746    <option value="linear_texture"
4747            title="sRGB perceptual: OFF. Channel weights R=1 G=1 B=1 A=1 (equal). Sharpening: Disabled. Encode deblocking: Disabled. Deblocking-aware encoding: OFF. Disables perceptual ops that distort linear data.">Linear Texture / Normal Map</option>
4748    </select>
4749    <hr style="width: 40%; margin: 10px 0; background-color: #ccc; border: none; height: 1px;">
4750    
4751    <b>KTX2 Texture Format to Encode:</b>
4752    <select id="basis-tex-format">
4753    <option value="0">ETC1S</option>
4754    <option value="1">UASTC LDR 4x4</option>
4755    <option value="2">UASTC HDR 4x4</option>
4756    <option value="3">RDO ASTC HDR 6x6</option>
4757    <option value="4">UASTC HDR 6x6 Intermediate</option>
4758
4759    <option value="5" selected>XUASTC LDR 4x4</option>
4760    <option value="6">XUASTC LDR 5x4</option>
4761    <option value="7">XUASTC LDR 5x5</option>
4762    <option value="8">XUASTC LDR 6x5</option>
4763    <option value="9">XUASTC LDR 6x6</option>
4764    <option value="10">XUASTC LDR 8x5</option>
4765    <option value="11">XUASTC LDR 8x6</option>
4766    <option value="12">XUASTC LDR 10x5</option>
4767    <option value="13">XUASTC LDR 10x6</option>
4768    <option value="14">XUASTC LDR 8x8</option>
4769    <option value="15">XUASTC LDR 10x8</option>
4770    <option value="16">XUASTC LDR 10x10</option>
4771    <option value="17">XUASTC LDR 12x10</option>
4772    <option value="18">XUASTC LDR 12x12</option>
4773
4774    <option value="19">ASTC LDR 4x4</option>
4775    <option value="20">ASTC LDR 5x4</option>
4776    <option value="21">ASTC LDR 5x5</option>
4777    <option value="22">ASTC LDR 6x5</option>
4778    <option value="23">ASTC LDR 6x6</option>
4779    <option value="24">ASTC LDR 8x5</option>
4780    <option value="25">ASTC LDR 8x6</option>
4781    <option value="26">ASTC LDR 10x5</option>
4782    <option value="27">ASTC LDR 10x6</option>
4783    <option value="28">ASTC LDR 8x8</option>
4784    <option value="29">ASTC LDR 10x8</option>
4785    <option value="30">ASTC LDR 10x10</option>
4786    <option value="31">ASTC LDR 12x10</option>
4787    <option value="32">ASTC LDR 12x12</option>
4788    <option value="33">XUBC7</option>
4789
4790    </select>
4791    
4792    <br><br>
4793
4794    <b>XUASTC LDR Syntax:</b>
4795    <select id="xuastc_ldr_syntax">
4796    <option value="0" selected>Full arithmetic (slowest transcoding, highest compression)</option>
4797    <option value="1">
4797Hybrid arithmetic+Zstd</option>
4798    <option value="2">Full ZStd (fastest transcoding, lowest compression)</option>
4799    </select>
4800
4801    <br><br>
4802
4803    <b>XUBC7 RDO Level (higher=smaller):</b>
4804    <input type="range" id="xubc7_rdo_level_slider" min="0" max="100" step="1" value="0">
4805    <span id="xubc7_rdo_level_value">0</span>
4806            
4807    <br>
4808    <hr style="width: 40%; margin: 10px 0; background-color: #ccc; border: none; height: 1px;">
4809    
4810    <b>Primary compression quality/effort options:</b>
4811
4812    <br>
4813    Use unified quality/effort options (overrides many below low-level options):
4814    <input type="checkbox" id="use_new_style_quality_effort" checked></input>   
4815
4816    <br>
4817    <label for="unified-effort-slider"><b>Effort:</b></label>
4818    <input type="range"
4819       id="unified-effort-slider"
4820       min="0"
4821       max="10"
4822       step="1"
4823       value="3"
4824       style="width: 300px;">
4825    <span id="unified-effort-value">3</span>
4826
4827    <br><br>
4828
4829    <label for="unified-quality-slider"><b>Quality:</b></label>
4830    <input type="range"
4831       id="unified-quality-slider"
4832       min="1"
4833       max="100"
4834       step="1"
4835       value="80"
4836       style="width: 300px;">
4837    <span id="unified-quality-value">80</span>
4838        
4839    <br>
4840    <hr style="width: 40%; margin: 10px 0; background-color: #ccc; border: none; height: 1px;">    
4841            
4842    <b>Display/Visualization Options:</b>
4843    <input type="button" value="Reset" onclick="resetDisplayOptions()" style="margin-left: 10px;">
4844    <br>
4845    
4846    <input type="button" id="btn-alpha-blend" value="Alpha blend" onclick="alphaBlend()"></input>
4847    <input type="button" id="btn-view-rgb" value="View RGB" onclick="viewRGB()"></input>
4848    <input type="button" id="btn-view-alpha" value="View Alpha" onclick="viewAlpha()"></input>
4849    <input type="button" id="btn-view-r" value="View R" onclick="viewR()"></input>
4850    <input type="button" id="btn-view-g" value="View G" onclick="viewG()"></input>
4851    <input type="button" id="btn-view-b" value="View B" onclick="viewB()"></input>
4852
4853    <br>
4854
4855    <br>
4856    <input type="button" value="LinearToSRGB" onclick="linearToSRGB()"></input> <b id='linear_to_srgb'>Disabled</b>
4857
4858    <br>
4859
4860    <label for="scale-slider">Exposure:</label>
4861    <input type="range" id="scale-slider" min="0" max="1" step=".01" value=".5" style="width: 300px;" oninput="updateScale(this.value)">
4862    <span id="scale-value">1.0000</span>
4863    
4864    <br>
4865
4866    <label for="display-zoom-slider">Display Zoom:</label>
4867    <input type="range" id="display-zoom-slider" min="0" max="8" step="1" value="1" style="width: 300px;" oninput="updateDisplayZoom(this.value)">
4868    <span id="display-zoom-value">1x</span>
4869        
4870    <br>
4871
4872    <label for="mipmap-level-slider">Mipmap Level:</label>
4873    <input type="range" id="mipmap-level-slider" min="0" max="0" step="1" value="0" style="width: 300px;" oninput="updateMipmapLevel(this.value)" disabled>
4874    <span id="mipmap-level-value">0</span>
4875
4876    <br>
4877
4878    <label for="cubemap-face-slider">Cubemap Face:</label>
4879    <input type="range" id="cubemap-face-slider" min="0" max="0" step="1" value="0" style="width: 300px;" oninput="updateCubemapFace(this.value)" disabled>
4880    <span id="cubemap-face-value">0</span>
4881
4882    <br>
4883
4884    <label for="array-layer-slider">Array Layer:</label>
4885    <input type="range" id="array-layer-slider" min="0" max="0" step="1" value="0" style="width: 300px;" oninput="updateArrayLayer(this.value)" disabled>
4886    <span id="array-layer-value">0</span>
4887    <input type="number" id="array-layer-input" min="0" max="0" value="0" style="width: 60px; margin-left: 8px;" onchange="updateArrayLayerFromInput(this.value)" disabled>
4888        
4889    <br>
4890
4891    Canvas Bilinear Filtering:
4892    <input type="checkbox" id="bilinear-filtering" onclick="toggleBilinearFiltering()">
4893    <br>
4894    <label for="canvas-colorspace-select">Canvas Color Space:</label>
4895    <select id="canvas-colorspace-select" onchange="updateCanvasColorSpace(this.value)">
4896      <option value="default" selected>Default (browser)</option>
4897      <option value="srgb">sRGB</option>
4898      <option value="display-p3">Display P3</option>
4899    </select>
4900    
4901    <br>
4902    XUASTC/ASTC LDR: Visualize GPU deblocking edge weights:
4903    <input type="checkbox" id="xuastc_ldr_gpu_deblocking_show_edge_weights" onclick="XUASTCGPUDeblockingFilterClicked()"></input>
4904
4905    <br>
4906
4907    <hr style="width: 40%; margin: 10px 0; background-color: #ccc; border: none; height: 1px;">
4908    
4909    <b>Transcoder Options (Decode Flags):</b>
4910    
4911    <br>
4912    
4913    <label for="deblocking-filter-select">Deblocking Filter Mode:</label>
4914    <select id="deblocking-filter-select" onchange="DeblockingFilterModeChanged()">
4915    <option value="0">Disabled</option> 
4916    <option value="1" selected>Auto: Use file's FilterID - GPU if enabled by texture file, otherwise disabled</option>    
4917    <option value="2">Force GPU deblocking on >=10x8 blocks, ignore file FilterID</option>  
4918    <option value="3">Force CPU deblocking on >=10x8 blocks, ignore file FilterID (if available for tex fmt)</option>  
4919    </select>
4920    
4921    <br>
4922    Apply deblocking filter to all block sizes when forced, not just to large block sizes:
4923    <input type="checkbox" id="xuastc_ldr_force_deblocking_on_all_block_sizes" onclick="XUASTCDeblockingFilterClicked()"></input>
4924    
4925    <br>
4926    ETC1S: No BC7 Chroma Artifact Filtering (faster transcoding):
4927    <input type="checkbox" id="etc1s_bc7_transcoder_no_chroma_filtering" onclick="BC7ChromaFilterClicked()"></input>
4928
4929    <br>
4930    XUASTC LDR 4x4/6x6/8x6: No direct BC7 transcoding (slower/higher quality):
4931    <input type="checkbox" id="xuastc_ldr_disable_fast_bc7_transcoding" onclick="XUASTCDeblockingFilterClicked()"></input>
4932
4933    <br>
4934
4935    Prefer higher quality transcoding when supported (slower):
4936    <input type="checkbox" id="highquality_transcoding" onclick="highQualityTranscodingClicked()">
4937
4938    <br>
4939
4940    <span title="Only specific opaque-only formats like ETC1 and PVRTC1_RGB support this flag, so textures with alpha can be transcoded to two textures (one for RGB, another for alpha).">Transcode alpha to opaque (ETC1/PVRTC1_RGB):</span>
4941    <input type="checkbox" id="transcode_alpha_to_opaque_formats" onclick="alphaToOpaqueFormatsClicked()"
4942           title="Only specific opaque-only formats like ETC1 and PVRTC1_RGB support this flag, so textures with alpha can be transcoded to two textures (one for RGB, another for alpha).">
4943
4944    <br>
4945
4946    <hr style="width: 40%; margin: 10px 0; background-color: #ccc; border: none; height: 1px;">
4947
4948    <details>
4949    <summary style="cursor: pointer; font-weight: bold;">Mipmap Generation Options</summary>
4950
4951    Generate mipmap levels:
4952    <input type="checkbox" id="Mipmaps">
4953
4954    <br>
4955
4956    <label for="mip-filter-select">Mip Filter:</label>
4957    <select id="mip-filter-select">
4958      <option value="0">box</option>
4959      <option value="1">tent</option>
4960      <option value="2">bell</option>
4961      <option value="3">b-spline</option>
4962      <option value="4">mitchell</option>
4963      <option value="5">blackman</option>
4964      <option value="6">lanczos3</option>
4965      <option value="7">lanczos4</option>
4966      <option value="8">lanczos6</option>
4967      <option value="9">lanczos12</option>
4968      <option value="10" selected>kaiser</option>
4969      <option value="11">gaussian</option>
4970      <option value="12">catmullrom</option>
4971      <option value="13">quadratic_interp</option>
4972      <option value="14">quadratic_approx</option>
4973      <option value="15">quadratic_mix</option>
4974    </select>
4975
4976    <br>
4977
4978    <label for="mip-scale-input">Mip Scale:</label>
4979    <input type="number" id="mip-scale-input" value="1.0" min="0.000125" max="4.0" step="0.1" style="width: 80px;">
4980
4981    <br>
4982
4983    <label for="mip-smallest-dim-input">Smallest Mip Dimension:</label>
4984    <input type="number" id="mip-smallest-dim-input" value="1" min="1" max="16384" step="1" style="width: 80px;">
4985
4986    <br>
4987
4988    Mip Wrapping:
4989    <input type="checkbox" id="MipWrapping">
4990
4991    Mip Renormalize:
4992    <input type="checkbox" id="MipRenormalize">
4993
4994    </details>
4995
4996    <hr style="width: 40%; margin: 10px 0; background-color: #ccc; border: none; height: 1px;">
4997    <details>
4998    <summary style="cursor: pointer; font-weight: bold;">.DDS/.KTX Developer Tools</summary>
4999
5000    <input type="button" value="Save Framebuffer as .PNG" onclick="saveFramebufferAsPNG()">
5001    <input type="button" value="Copy Framebuffer to Clipboard" onclick="copyFramebufferToClipboard()">
5002
5003    <hr style="width: 40%; margin: 10px 0; background-color: #ccc; border: none; height: 1px;">
5004    <b>Export encoded .KTX2 to .DDS:</b>
5005    <select id="dds-export-format" title="GPU format to transcode the encoded .KTX2 into when exp
5005orting a .DDS file. Note: HDR textures support only BC6H.">
5006    <option value="2">BC1 (RGB)</option>
5007    <option value="3">BC3 (RGBA)</option>
5008    <option value="4">BC4 (R)</option>
5009    <option value="5">BC5 (RG)</option>
5010    <option value="22">BC6H (HDR RGB)</option>
5011    <option value="6" selected>BC7 (RGBA)</option>
5012    <option value="13">RGBA32 (uncompressed)</option>
5013    </select>
5014    <input type="button" value="Download Exported .DDS File" onclick="downloadExportedDDS()">
5015
5016    <hr style="width: 40%; margin: 10px 0; background-color: #ccc; border: none; height: 1px;">
5017    <b>Export encoded .KTX2 to .KTX (v1):</b>
5018    <select id="ktx-export-format" title="GPU format to transcode the encoded .KTX2 into when exporting a compressed KTX1 (.ktx) file. Compressed formats only; HDR textures support only BC6H / ASTC HDR; PVRTC1 needs power-of-2 dimensions. For ASTC, the chosen block size must match the .KTX2 texture's block size.">
5019    <option value="2">BC1 (RGB)</option>
5020    <option value="3">BC3 (RGBA)</option>
5021    <option value="4">BC4 (R)</option>
5022    <option value="5">BC5 (RG)</option>
5023    <option value="22">BC6H (HDR RGB)</option>
5024    <option value="6" selected>BC7 (RGBA)</option>
5025    <option value="0">ETC1 (RGB)</option>
5026    <option value="1">ETC2 (RGBA)</option>
5027    <option value="20">EAC R11</option>
5028    <option value="21">EAC RG11</option>
5029    <option value="8">PVRTC1 4bpp RGB</option>
5030    <option value="9">PVRTC1 4bpp RGBA</option>
5031    <option value="18">PVRTC2 4bpp RGB</option>
5032    <option value="19">PVRTC2 4bpp RGBA</option>
5033    <option value="10">ASTC LDR 4x4</option>
5034    <option value="28">ASTC LDR 5x4</option>
5035    <option value="29">ASTC LDR 5x5</option>
5036    <option value="30">ASTC LDR 6x5</option>
5037    <option value="31">ASTC LDR 6x6</option>
5038    <option value="32">ASTC LDR 8x5</option>
5039    <option value="33">ASTC LDR 8x6</option>
5040    <option value="34">ASTC LDR 10x5</option>
5041    <option value="35">ASTC LDR 10x6</option>
5042    <option value="36">ASTC LDR 8x8</option>
5043    <option value="37">ASTC LDR 10x8</option>
5044    <option value="38">ASTC LDR 10x10</option>
5045    <option value="39">ASTC LDR 12x10</option>
5046    <option value="40">ASTC LDR 12x12</option>
5047    <option value="23">ASTC HDR 4x4</option>
5048    <option value="27">ASTC HDR 6x6</option>
5049    </select>
5050    <input type="button" value="Download Exported .KTX File" onclick="downloadExportedKTX()">
5051
5052    <hr style="width: 40%; margin: 10px 0; background-color: #ccc; border: none; height: 1px;">
5053    <b>Encode loaded source image directly to .DDS:</b>
5054    <br>
5055    <label><input type="checkbox" id="dds-encode-download"> Download encoded .DDS file when done</label>
5056    <br>
5057    DDS Texture Format to Encode: <select id="dds-encode-format" title="Physical DDS format to encode the loaded SOURCE IMAGE into (direct, high-quality; not via KTX2).">
5058      <option value="cDDSFmtBC1">BC1</option>
5059      <option value="cDDSFmtBC2">BC2</option>
5060      <option value="cDDSFmtBC3">BC3</option>
5061      <option value="cDDSFmtBC4">BC4 (R)</option>
5062      <option value="cDDSFmtBC5">BC5 (RG)</option>
5063      <option value="cDDSFmtBC7" selected>BC7</option>
5064      <option value="cDDSFmtA8R8G8B8">A8R8G8B8 (BGRA)</option>
5065      <option value="cDDSFmtA8B8G8R8">A8B8G8R8 (RGBA)</option>
5066      <option value="cDDSFmtR8">R8</option>
5067      <option value="cDDSFmtR8G8">R8G8</option>
5068      <option value="cDDSFmtR5G6B5">R5G6B5</option>
5069      <option value="cDDSFmtA1R5G5B5">A1R5G5B5</option>
5070      <option value="cDDSFmtA4R4G4B4">A4R4G4B4</option>
5071    </select>
5072    <input type="button" value="Encode DDS!" onclick="encodeDDSViaWorker()" title="Encode the loaded source image directly to a .DDS on a background worker, then display it like an opened .dds file.">
5073    <br>
5074    <br>
5075    <span title="BC7-only options (ignored for non-BC7 formats).">
5076      <label for="dds-encode-bc7-encoder">BC7 Encoder:</label>
5077      <select id="dds-encode-bc7-encoder">
5078        <option value="0" selected>bc7f (fast, default)</option>
5079        <option value="1">bc7e_scalar (slower, higher quality)</option>
5080      </select>
5081      <br>
5082      <br>
5083      <label for="dds-encode-bc7f-level">bc7f options:</label>
5084      <select id="dds-encode-bc7f-level" title="bc7f analytical level (
5084only used when the bc7f encoder is selected).">
5085        <option value="0">analytical</option>
5086        <option value="1" selected>partial</option>
5087        <option value="2">non-analytical</option>
5088      </select>
5089      <br>
5090      <br>
5091      <label for="dds-encode-bc7e-level">bc7e_scalar level:</label>
5092      <select id="dds-encode-bc7e-level" title="bc7e_scalar quality level 0-6, higher=slower/better (only used when the bc7e_scalar encoder is selected).">
5093        <option value="0">0</option>
5094        <option value="1">1</option>
5095        <option value="2" selected>2</option>
5096        <option value="3">3</option>
5097        <option value="4">4</option>
5098        <option value="5">5</option>
5099        <option value="6">6</option>
5100      </select>
5101    </span>
5102
5103    </details>
5104    
5105    <hr style="width: 40%; margin: 10px 0; background-color: #ccc; border: none; height: 1px;">
5106    <details>
5107    <summary style="cursor: pointer; font-weight: bold;">Low-level XUASTC/ASTC LDR 4x4-12x12 Options</summary>
5108    
5109    <br>
5110
5111    <label for="xuastc_ldr_effort_level_slider">Effort Level (higher=slower):</label>
5112    <input type="range" id="xuastc_ldr_effort_level_slider" min="0" max="10" step="1" value="2">
5113    <span id="xuastc_ldr_effort_level_value">2</span> <!-- Displays the current value -->
5114    <br>
5115
5116    <label for="xuastc_ldr_dct_quality_slider">XUASTC Weight Grid DCT Quality (100=no DCT):</label>
5117    <input type="range" id="xuastc_ldr_dct_quality_slider" min="1" max="100" step="1" value="80">
5118    <span id="xuastc_ldr_dct_quality_value">80</span> <!-- Displays the current value -->
5119
5120    <br><br>
5121        
5122    <label for="encoding_deblocking_filter_select">Global Optimization (SCD)/Deblocking (FilterID stored in file):</label>
5123    <select id="encoding_deblocking_filter_select">
5124    <option value="0">Disabled (no SCD, no filtering)</option> 
5125    <option value="1" selected>Auto: Use SCD+filtering only on largest (>=10x8) block sizes</option>  
5126    <option value="2">Enable SCD+filtering on all block sizes</option>   
5127    <option value="3">Enable SCD, disable filtering</option>   
5128    <option value="4">Disable SCD, enable filtering on largest block sizes</option>   
5129    <option value="5">Disable SCD, enable filtering on all block sizes</option>   
5130    </select>
5131    
5132    <br>
5133    <label for="deblocking-num-passes-slider">Deblocking Passes (2-255, 256=auto depending on effort):</label>
5134    <input type="range"
5135       id="deblocking-num-passes-slider"
5136       min="2"
5137       max="256"
5138       step="1"
5139       value="256"
5140       style="width: 300px;">
5141    <span id="deblocking-num-passes-value">256</span>
5142    
5143    <br><br>
5144    Bounded/windowed RDO lossy supercompression:
5145    <input type="checkbox" id="XUASTCLossySupercompression" checked>
5146
5147    <br>
5148    No RGB dual plane (lower quality, faster encoding/BC7 transcoding):
5149    <input type="checkbox" id="XUASTCDisableRGBDualPlane">
5150
5151    <br>
5152    No 2-3 subset usage (lower quality, faster encoding/BC7 transcoding):
5153    <input type="checkbox" id="XUASTCDisableSubsets">
5154    
5155    <br>
5156    Enable prefiltering/blurred candidates (higher quality, slower):
5157    <input type="checkbox" id="XUASTCUseBlur">
5158    
5159    <br>
5160    Heavy subset usage (higher quality at lowest bitrates, slower):
5161    <input type="checkbox" id="XUASTCHeavySubsetUsage">
5162    
5163    <br><br>
5164    <label for="XUASTCLDRCompressor">LDR Compressor:</label>
5165    <select id="XUASTCLDRCompressor"
5166            title="Selects the encoder backend for XUASTC LDR. The ASTCENC and merged modes require astcenc to have been enabled at compile time.">
5167      <option value="cAuto" selected>Auto (tied to effort): ASTCF or merge basisu+ASTCF</option>
5168      <option value="cBasisU">Use basisu's original ASTC encoder</option>
5169      <option value="cASTCENC">Use ASTCENC</option>
5170      <option value="cASTCF">Use ASTCF</option>
5171      <option value="cBasisU_and_ASTCENC">Merge basisu+ASTCENC</option>
5172      <option value="cBasisU_and_ASTCF">Merge basisu+ASTCF</option>
5173      <option value="cUseAll">Merge All</option>
5174    </select>
5175    
5176    <br>
5177    
5178    <br>
5179    <label for="XUASTCSharpenMode">Sharpening Mode:</label>
5180    <select id="XUASTCSharpenMode">
5181    <option value="0" selected>Disabled</option> 
5182    <option value="1">Auto: On 10x8 or larger block sizes</option>    
5183    <option value="2">All block sizes</option>  
5184    </select>
5185    
5186    <br>
5187    <label>Sharpening Strength: </label>
5188    <input type="number" id="XUASTCSharpenAmount" value="1.1" min="0.000125" max="10.0" step="0.1" style="width: 80px;">
5189
5190    <br><br>
5191    <b>ASTC/XUASTC LDR Bounded/Windowed RDO Params:</b>
5192
5193    <br>
5194    Opaque:
5195    <br>
5196    <label>Min Acceptable PSNR:                 <input type="number" id="xuastc_ldr_min_psnr"               value="35.0" min="0" max="100" step="0.25"></label>
5197    <br>
5198    <label>Window Size (Simple) PSNR:           <input type="number" id="xuastc_ldr_thresh_psnr"            value="1.5" min="0" max="100" step="0.25"></label>
5199    <br>
5200    <label>Window Size (Edge) PSNR:             <input type="number" id="xuastc_ldr_thresh_edge_psnr"       value="1.0" min="0" max="100" step="0.25"></label>
5201
5202    <br>
5203
5204    <br>Alpha:<br>
5205    <label>Min Acceptable Alpha PSNR:           <input type="number" id="xuastc_ldr_min_alpha_psnr"         value="38.0" min="0" max="100" step="0.25"></label>
5206    <br>
5207    <label>Window Size (Simple) Alpha PSNR:     <input type="number" id="xuastc_ldr_thresh_alpha_psnr"      value="
52070.75" min="0" max="100" step="0.25"></label>
5208    <br>
5209    <label>Window Size (Edge) Alpha Edge PSNR:  <input type="number" id="xuastc_ldr_thresh_edge_alpha_psnr" value=".5" min="0" max="100" step="0.25"></label>
5210    <br>
5211    </details>
5212
5213    <hr style="width: 40%; margin: 10px 0; background-color: #ccc; border: none; height: 1px;">
5214
5215    <details>
5216    <summary style="cursor: pointer; font-weight: bold;">Low-level XUBC7 Options</summary>
5217
5218    <br>
5219
5220    <label for="xubc7_num_stripes_slider">Number of Stripes (1-16, more=faster encode/decode, slightly larger):</label>
5221    <input type="range" id="xubc7_num_stripes_slider" min="1" max="16" step="1" value="8">
5222    <span id="xubc7_num_stripes_value">8</span> <!-- Displays the current value -->
5223    <br>
5224    <br>
5225
5226    <label for="xubc7_encoder_select">BC7 Base Encoder:</label>
5227    <select id="xubc7_encoder_select">
5228       <option value="0" selected>bc7f (fast, default)</option>
5229       <option value="1">bc7e_scalar (slower, higher quality)</option>
5230    </select>
5231    <br>
5232    <br>
5233
5234    <label for="xubc7_bc7e_scalar_level_slider">bc7e_scalar Level (0-6, higher=slower/better; only used when bc7e_scalar selected):</label>
5235    <input type="range" id="xubc7_bc7e_scalar_level_slider" min="0" max="6" step="1" value="2">
5236    <span id="xubc7_bc7e_scalar_level_value">2</span> <!-- Displays the current value -->
5237    <br>
5238    </details>
5239
5240    <hr style="width: 40%; margin: 10px 0; background-color: #ccc; border: none; height: 1px;">
5241
5242    <details>
5243    <summary style="cursor: pointer; font-weight: bold;">Low-level ETC1S LDR Options</summary>
5244    <br>
5245    ETC1S Quality:
5246    <input type="range" min="1" max="255" value="255" class="slider" id="EncodeQuality" style="width: 300px;">
5247    <span id="encode-quality-value">255</span>
5248
5249    <br>
5250    <label for="etc1s-comp-level-slider">Effort Level:</label>
5251    <input type="range" id="etc1s-comp-level-slider" min="0" max="6" step="1" value="1">
5252    <span id="etc1s-comp-level-slider-value">1</span> <!-- Displays the current value -->
5253    </details>
5254
5255    <hr style="width: 40%; margin: 10px 0; background-color: #ccc; border: none; height: 1px;">
5256
5257    <details>
5258    <summary style="cursor: pointer; font-weight: bold;">Low-level UASTC LDR 4x4 Options</summary>
5259
5260    <br>
5261    UASTC LDR RDO:
5262    <input type="checkbox" id="UASTC_LDR_RDO">
5263
5264    <label for="rdo-quality-slider">RDO Quality:</label>
5265    <input type="range" id="rdo-quality-slider" min="0.1" max="10" step="0.1" value="1.0">
5266    <span id="rdo-quality-value">1.0</span> <!-- Displays the current value -->
5267
5268    <br>
5269    <label for="uastc-ldr-quality-slider">UASTC LDR Effort:</label>
5270    <input type="range" id="uastc-ldr-quality-slider" min="0" max="4" step="1" value="1">
5271    <span id="uastc-ldr-quality-value">1</span> <!-- Displays the current value -->
5272
5273    <br>
5274    </details>
5275
5276    <hr style="width: 40%; margin: 10px 0; background-color: #ccc; border: none; height: 1px;">
5277
5278    <details>
5279    <summary style="cursor: pointer; font-weight: bold;">Low-level UASTC HDR 4x4 Options</summary>
5280
5281    <br>
5282    <label for="uastc-hdr-quality-slider">UASTC HDR Effort:</label>
5283    <input type="range" id="uastc-hdr-quality-slider" min="0" max="4" step="1" value="0">
5284    <span id="uastc-hdr-quality-value">0</span> <!-- Displays the current value -->
5285    <br>
5286    </details>
5287
5288    <hr style="width: 40%; margin: 10px 0; background-color: #ccc; border: none; height: 1px;">
5289
5290    <details>
5291    <summary style="cursor: pointer; font-weight: bold;">Low-level ASTC/UASTC HDR 6x6 Options</summary>
5292    <br>
5293
5294    <label for="astc-hdr6x6-comp-level-slider">Effort Level:</label>
5295    <input type="range" id="astc-hdr6x6-comp-level-slider" min="0" max="12" step="1" value="0">
5296    <span id="astc-hdr6x6-comp-level-value">0</span> <!-- Displays the current value -->
5297    <br>
5298
5299    RDO Quality (Lambda, 0-50k, try 0-5k, higher=smaller):
5300    <input type="text" id="astc6x6-rdo-lambda" value="0.0" size="10">
5301    <br>
5302        
5303    REC 2020 Colorspace (impacts error metrics, KTX2 DFD Gamut):
5304    <input type="checkbox" id="Rec2020">
5305        
5306    </details>
5307
5308    <hr style="width: 40%; margin: 10px 0; background-color: #ccc; border: none; height: 1px;">
5309
5310    <details>
5311    <summary style="cursor: pointer; font-weight: bold;">LDR->HDR Upconversion Options</summary>
5312    <br>
5313    Convert LDR images to linear light:
5314    <input type="checkbox" id="ConvertLDRToLinear" checked>
5315
5316    <br>
5317    LDR to HDR Upconversion Nit Multiplier:
5318    <input type="text" id="ldr-to-hdr-multiplier" value="100.0" size="10"
5319        oninput="validateNitMultiplier(this)"
5320        placeholder="Enter a value between 0.1 and 1000">
5321    <span id="ldr-to-hdr-warning" style="color: red; display: none;">Invalid value!</span>
5322    <br>
5323    </details>
5324
5325    <hr style="width: 40%; margin: 10px 0; background-color: #ccc; border: none; height: 1px;">
5326    <b>Other Options:</b>
5327
5328    <br>
5329    Image is sRGB/use sRGB perceptual metrics:
5330    <input type="checkbox" id="SRGB">
5331        
5332    <br>
5333    Y flip source image:
5334    <input type="checkbox" id="YFlip">
5335
5336    <br>
5337    <br>
5338    <b>Channel Weights (XUASTC/ASTC/XUBC7/DDS bc7e_scalar):</b>
5339    <br>
5340    <label>R: <input type="number" id="xuastc_ldr_weight_r" value="9"  min="1" max="32" step="1" style="width: 60px;" onchange="this.value=Math.max(1,Math.min(32,parseInt(this.value)||1))"></label>
5341    <label>G: <input type="number" id="xuastc_ldr_weight_g" value="11" min="1" max="32" step="1" style="width: 60px;" onchange="this.value=Math.max(1,Math.min(32,parseInt(this.value)||1))"></label>
5342    <label>B: <input type="number" id="xuastc_ldr_weight_b" value="1"  min="1" max="32" step="1" style="width: 60px;" onchange="this.value=Math.max(1,Math.min(32,parseInt(this.value)||1))"></label>
5343    <label>A: <input type="number" id="xuastc_ldr_weight_a" value="11" min="1" max="32" step="1" style="width: 60px;" onchange="this.value=Math.max(1,Math.min(32,parseInt(this.value)||1))"></label>
5344
5345    <br>
5346    <br>
5347    <b>Channel Swizzle (each output channel's source index 0-3; default 0,1,2,3 = no swizzle):</b>
5348    <br>
5349    <label>R: <input type="number" id="swizzle_r" value="0" min="0" max="3" step="1" style="width: 60px;" onchange="this.value=Math.max(0,Math.min(3,parseInt(this.value)||0))"></label>
5350    <label>G: <input type="number" id="swizzle_g" value="1" min="0" max="3" step="1" style="width: 60px;" onchange="this.value=Math.max(0,Math.min(3,parseInt(this.value)||0))"></label>
5351    <label>B: <input type="number" id="swizzle_b" value="2" min="0" max="3" step="1" style="width: 60px;" onchange="this.value=Math.max(0,Math.min(3,parseInt(this.value)||0))"></label>
5352    <label>A: <input type="number" id="swizzle_a" value="3" min="0" max="3" step="1" style="width: 60px;" onchange="this.value=Math.max(0,Math.min(3,parseInt(this.value)||0))"></label>
5353
5354    <br>
5355    <br>
5356    Debug Output (See Dev Console):
5357    <input type="checkbox" id="Debug">
5358
5359    <br>
5360    Clear Dev Console on each encode:
5361    <input type="checkbox" id="clear-console-on-encode">
5362    <br>
5363    Encode on worker thread by default (on image load):
5364    <input type="checkbox" id="encode-on-worker-default" checked>
5365    <br>
5366    Auto-close encode progress dialog when done (uncheck to study debug output):
5367    <input type="checkbox" id="encode-modal-autoclose" checked>
5368    <br>
5369    Compute Stats (slower encoding):
5370    <input type="checkbox" id="ComputeStats">
5371
5372    <br>
5373    Print Stats:
5374    <input type="checkbox" id="PrintStats">
5375
5376    <hr style="width: 40%; margin: 10px 0; background-color: #ccc; border: none; height: 1px;">
5377
5378    <details>
5379    <summary style="cursor: pointer; font-weight: bold;">
5380      Development Log Output
5381      <button type="button"
5382              onclick="event.preventDefault(); event.stopPropagation(); logClear();"
5383              style="margin-left: 10px; font-weight: normal;">
5384        Clear
5385      </button>
5386    </summary>
5387    <div id="log-panel" class="log-panel">
5388    </div>
5389    </details>
5390
5391    <div style="margin-top: 6px;">
5392      <label><input type="checkbox" id="disable-logging" onchange="setLoggingDisabled(this.checked)" checked> Disable logging</label>
5393    </div>
5394    <div id="busy-modal" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: rgba(0,0,0,0.85); z-index: 9999; font-size: 18px; color: white; display: none; padding: 20px 40px; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.5);">
5395    Encoding... Please wait.
5396    </div>
5397
5398    <!-- Modal busy dialog for WORKER encodes. showModal() makes it truly modal
5399         (top layer + backdrop + blocks all interaction with the page behind it).
5400         oncancel prevents ESC from dismissing it (no cancel during encode). The
5401         <progress> is indeterminate ("busy") for now; set .value/.max later for a
5402         real progress bar. The log <pre> streams the worker's stdout/stderr live. -->
5403    <style>
5404      #encode-busy-dialog::backdrop { background: rgba(0,0,0,0.55); }
5405      /* Fixed size so the dialog does NOT grow/shrink as the live log streams in. */
5406      #encode-busy-dialog { box-sizing: border-box; width: 70vw; border: none; border-radius: 10px;
5406 padding: 22px 26px; box-shadow: 0 8px 40px rgba(0,0,0,0.5); }
5407      #encode-busy-log { box-sizing: border-box; width: 100%; height: 42vh; margin: 12px 0 0 0; overflow: auto; background: #111; color: #b8f5b8; font: 12px/1.35 ui-monospace, monospace; padding: 8px 10px; border-radius: 6px; white-space: pre-wrap; text-align: left; }
5408    </style>
5409    <!-- ESC is blocked while the Close button is disabled (i.e. during encoding); allowed once it's enabled. -->
5410    <dialog id="encode-busy-dialog" oncancel="if (document.getElementById('encode-busy-close').disabled) event.preventDefault();">
5411      <div style="font-size: 18px; font-weight: bold;">Encoding&hellip;</div>
5412      <progress id="encode-busy-progress" style="width: 100%; height: 16px; margin-top: 12px;"></progress>
5413      <pre id="encode-busy-log"></pre>
5414      <div style="margin-top: 12px; display: flex; align-items: center; gap: 16px;">
5415        <label style="font-size: 13px;"><input type="checkbox" id="encode-busy-autoclose" checked onchange="var m=document.getElementById('encode-modal-autoclose'); if (m) m.checked = this.checked;"> Automatically close when done</label>
5416        <button type="button" id="encode-busy-close" onclick="hideEncodeBusy()" disabled style="margin-left: auto; padding: 8px 28px; font-size: 15px; font-weight: bold; cursor: pointer;">Close</button>
5417      </div>
5418    </dialog>
5419
5420  </div>
5421    
5422  <div class="viewer">
5423     <canvas id="canvas"></canvas>
5424  </div>
5425
5426</div>
5427
5428<script>
5429
5430// Function to check for WebAssembly threading support
5431function isWasmThreadingSupported()
5432{
5433    try
5434    {
5435        if (typeof WebAssembly === "object" && typeof WebAssembly.Memory === "function")
5436        {
5437            const testMemory = new WebAssembly.Memory({
5438                initial: 1,
5439                maximum: 1,
5440                shared: true,
5441            });
5442            return testMemory instanceof WebAssembly.Memory;
5443        }
5444        return false;
5445    }
5446    catch (e)
5447    {
5448        return false;
5449    }
5450}
5451
5452async function loadBasisModule()
5453{
5454    // Safari 26.0-26.2 has a WebKit heap-grow bug that crashes multithreaded encoding.
5455    // Show a persistent warning; getNumWorkerThreads() forces single-threaded for both
5456    // encode paths when this is detected.
5457    if (isBuggySafari())
5458    {
5459        const banner = document.getElementById('safari-bug-banner');
5460        if (banner) banner.style.display = 'block';
5461    }
5462
5463    // Determine if WASM threading and WASM64 are available
5464    const threadingSupported = window.wasmFeatureDetect ? await wasmFeatureDetect.threads() : isWasmThreadingSupported();
5465    const wasm64Supported = window.wasmFeatureDetect ? await wasmFeatureDetect.memory64() : false;
5466
5467    window.isThreaded = threadingSupported; // Set global variable to indicate threading support
5468    window.wasm64 = wasm64Supported;
5469
5470    // Select WASM module
5471    const scriptSrc = (wasm64Supported && threadingSupported) ? "../encoder/build/basis_encoder_threads_wasm64.js" :
5472        (threadingSupported ? "../encoder/build/basis_encoder_threads.js" : "../encoder/build/basis_encoder.js");
5473
5474    const script = document.createElement("script");
5475    script.src = scriptSrc;
5476    script.onload = () =>
5477    {
5478        BASIS({
5479            onRuntimeInitialized: () => {
5480
5481                console.warn('isThreaded: ' + window.isThreaded);
5482                console.warn('isWASM64: ' + window.wasm64);
5483
5484                globalInit();
5485
5486                const threadingIndicator = document.getElementById("threading-indicator");
5487
5488                if (!threadingSupported)
5489                {
5490                  threadingIndicator.style.color = "red";
5491                  threadingIndicator.innerText = "Multithreading not supported! Running in non-threaded mode.";
5492
5493                  const threadingCheckbox = document.getElementById("multithreaded_encoding");
5494                  threadingCheckbox.checked = false;
5495                }
5496                else
5497                {
5498                  threadingIndicator.innerText = "Multithreading is supported.";
5499                }
5500
5501                if (!wasm64Supported)
5502                {
5503                    threadingIndicator.innerText += " WASM64 is NOT supported. Without WASM64 the maximum image size is limited (to avoid running out of memory).";
5504                    // Safari has no WASM64 support yet -- point users to a browser that does.
5505                    if (isSafari())
5506                        threadingIndicator.innerText += " For WASM64 support, use Chrome or Firefox.";
5507                }
5508                else
5509                    threadingIndicator.innerText += " Using WASM64.";
5510
5511                if (!threadingSupported)
5512                {
5513                    const slider = document.getElementById("xuastc_ldr_effort_level_slider");
5514                    const label  = document.getElementById("xuastc_ldr_effort_level_value");
5515
5516                    if (slider && label)
5517                    {
5518                        slider.value = 0;
5519                        label.textContent = "0";
5520                    }
5521                }
5522            }
5523
5524        }).then(module => { window.Module = module;
5525
5526          if (module.initializeBasis)
5527          {
5528             module.initializeBasis();
5529             console.log("module.initializeBasis() called successfully.");
5530          }
5531          else {
5532             console.error("module.initializeBasis() is not available on the Module object.");
5533          }
5534
5535          }
5536
5537        );
5538    };
5539
5540    script.onerror = () => {
5541        console.error("Failed to load the Basis module.");
5542        updateErrorLine("Failed to load Basis module. Check console for details.");
5543    };
5544
5545    document.head.appendChild(script);
5546}
5547
5548document.addEventListener('DOMContentLoaded', loadBasisModule);
5549</script>
5549
5550
5551<script>
5552document.addEventListener('DOMContentLoaded', () => {
5553  const dropArea = document.getElementById('drop-area');
5554  const fileInput = document.getElementById('file-input');
5555
5556  // Prevent default drag behaviors
5557  ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
5558      dropArea.addEventListener(eventName, e => e.preventDefault());
5559      dropArea.addEventListener(eventName, e => e.stopPropagation());
5560  });
5561
5562  // Highlight the drop area on dragover
5563  dropArea.addEventListener('dragover', () => {
5564      dropArea.style.backgroundColor = '#f1f1f1';
5565  });
5566
5567  // Remove highlight on dragleave
5568  dropArea.addEventListener('dragleave', () => {
5569      dropArea.style.backgroundColor = '';
5570  });
5571
5572  // Handle dropped files
5573  dropArea.addEventListener('drop', e => {
5574      dropArea.style.backgroundColor = '';
5575      const files = e.dataTransfer.files;
5576      if (files.length > 0) {
5577          processFile(files[0]);
5578      }
5579  });
5580
5581  // Open file picker on click
5582  dropArea.addEventListener('click', () => {
5583      fileInput.value = '';  // Clear so re-selecting the same file triggers 'change'
5584      fileInput.click();
5585  });
5586
5587  // Handle file input change
5588  fileInput.addEventListener('change', () => {
5589      if (fileInput.files.length > 0) {
5590          processFile(fileInput.files[0]);
5591      }
5592  });
5593
5594  // Function to process the file
5595  window.processFile = processFile;
5596  function processFile(file)
5597  {
5598      if (!file)
5599         return;
5600
5601      resetMipmapSliderToZero();
5602      resetCubemapFaceToZero();
5603      resetArrayLayerToZero();
5604      resetDisplayZoom();
5605      resetExposure();
5606
5607      const reader = new FileReader();
5608
5609      reader.onload = function (e)
5610      {
5611          const arrayBuffer = e.target.result;
5612
5613          // Assume you have a function to process the file
5614          log('------');
5615          log(`Loaded file: ${file.name}`);
5616          log(`File size: ${file.size} bytes`);
5617          
5618          const lowerName = file.name.toLowerCase();
5619
5620          if (lowerName.endsWith(".ktx2"))
5621          {
5622              // transcodeTexture() owns the source globals (set on success only), so a
5623              // failed load leaves the previously-loaded source intact.
5624              transcodeTexture(arrayBuffer, file.name);
5625              return;
5626          }
5627          else if (lowerName.endsWith(".dds"))
5628          {
5629              // New: view/transcode a .DDS source. transcodeDDSTexture() owns the source
5630              // globals (set on success only) and clears the KTX2 source, keeping the
5631              // viewer correctly tri-state even on a failed load.
5632              transcodeDDSTexture(arrayBuffer, file.name);
5633              return;
5634          }
5635          else
5636          {
5637              loadAndCompressImage(arrayBuffer, file.name);
5638          }
5639
5640          elem('imagefile').value = '<externally loaded>';
5641      };
5642
5643      reader.onerror = function (e) {
5644          log('Error reading file.');
5645      };
5646
5647      reader.readAsArrayBuffer(file);
5648  }
5649});
5650
5651</script>
5651
5652
5653<script>
5654// Drag-to-scroll: hold left mouse button and drag to pan the texture view.
5655document.addEventListener('DOMContentLoaded', () => {
5656  const viewer = document.querySelector('.viewer');
5657  let isDragging = false;
5658  let startX, startY, scrollLeftStart, scrollTopStart;
5659  viewer.addEventListener('mousedown', e => {
5660    // Only left button
5661    if (e.button !== 0) return;
5662    isDragging = true;
5663    viewer.classList.add('dragging');
5664    startX = e.clientX;
5665    startY = e.clientY;
5666    scrollLeftStart = viewer.scrollLeft;
5667    scrollTopStart = viewer.scrollTop;
5668    e.preventDefault();
5669  });
5670  window.addEventListener('mousemove', e => {
5671    if (!isDragging) return;
5672    viewer.scrollLeft = scrollLeftStart - (e.clientX - startX);
5673    viewer.scrollTop  = scrollTopStart  - (e.clientY - startY);
5674  });
5675  window.addEventListener('mouseup', e => {
5676    if (!isDragging) return;
5677    isDragging = false;
5678    viewer.classList.remove('dragging');
5679  });
5680  // Wheel-to-zoom: scroll wheel zooms in/out, preserving the point under the cursor.
5681  viewer.addEventListener('wheel', e => {
5682    e.preventDefault();
5683    const slider = document.getElementById('display-zoom-slider');
5684    if (!slider) return;
5685
5686    const oldSliderVal = parseInt(slider.value, 10);
5687    const newSliderVal = e.deltaY < 0
5688      ? Math.min(oldSliderVal + 1, parseInt(slider.max, 10))
5689      : Math.max(oldSliderVal - 1, parseInt(slider.min, 10));
5690
5691    if (newSliderVal === oldSliderVal) return;
5692
5693    const oldZoom = (oldSliderVal === 0) ? 0.5 : oldSliderVal;
5694    const newZoom = (newSliderVal === 0) ? 0.5 : newSliderVal;
5695
5696    // Mouse position relative to the viewer's top-left corner.
5697    const rect = viewer.getBoundingClientRect();
5698    const mouseX = e.clientX - rect.left;
5699    const mouseY = e.clientY - rect.top;
5700
5701    // The texture-space coordinate currently under the cursor.
5702    const texX = (viewer.scrollLeft + mouseX) / oldZoom;
5703    const texY = (viewer.scrollTop  + mouseY) / oldZoom;
5704
5705    // Apply the zoom through the existing slider/function so all state stays in sync.
5706    slider.value = newSliderVal;
5707    updateDisplayZoom(newSliderVal);
5708
5709    // Adjust scroll so the same texture-space point remains under the cursor.
5710    viewer.scrollLeft = texX * newZoom - mouseX;
5711    viewer.scrollTop  = texY * newZoom - mouseY;
5712
5713  }, { passive: false });
5714});
5715// Keyboard shortcuts: Ctrl/Cmd+V to paste an image, Ctrl/Cmd+C to copy the framebuffer.
5716document.addEventListener('DOMContentLoaded', () => {
5717  function isEditableTarget(t)
5718  {
5719    return t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable);
5720  }
5721  // Paste: handled via the native 'paste' event for synchronous access to clipboard
5722  // data (no permission prompt, works in file:// contexts). Returns a File directly.
5723  document.addEventListener('paste', e => {
5724    if (isEditableTarget(e.target)) return;
5725    const items = e.clipboardData && e.clipboardData.items;
5726    if (!items) return;
5727    for (const item of items) {
5728      if (item.kind === 'file' && item.type.startsWith('image/')) {
5729        const file = item.getAsFile();
5730        if (!file) continue;
5731          e.preventDefault();
5732          updateErrorLine('');
5733          window.processFile(file);
5734        return;
5735      }
5736    }
5737    // No image item found — could be plain text, files of other types, or empty.
5738    e.preventDefault();
5739    updateErrorLine('Clipboard does not contain a supported bitmap image.');
5740  });
5741
5742  // Copy: routed through the existing async-clipboard path. Skip when text is
5743  // selected (user is trying to copy that text, not the framebuffer).
5744  document.addEventListener('keydown', e => {
5745    const ctrl = e.ctrlKey || e.metaKey;
5746    if (!ctrl || e.shiftKey || e.altKey) return;
5747    if (isEditableTarget(e.target)) return;
5748    if (e.key !== 'c' && e.key !== 'C') return;
5749    if (window.getSelection().toString().length > 0) return;
5750    e.preventDefault();
5751    copyFramebufferToClipboard();
5752  });
5753});
5754</script>
5754
5755</body>
5756
5757</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.