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⚠ 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…</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.