gputex 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -605,4 +605,141 @@ interface RasterizeSvgOptions {
605
605
  */
606
606
  declare function rasterizeSvg(source: string | Blob, options?: RasterizeSvgOptions): Promise<ImageBitmap>;
607
607
 
608
- export { ASTC4x4Encoder, ASTC4x4WebGLEncoder, BC1Encoder, BC1WebGLEncoder, BC5Encoder, BC5WebGLEncoder, BC7Encoder, type BC7EncoderOptions, BC7WebGLEncoder, type Capabilities, ETC2Encoder, type EncodeBytesResult, type EncodeCallOptions, type EncodeMipChainResult, type EncodedLevelBytes, Encoder, type EncoderConstructor, type EncoderImageSource, type EncoderOptions, type ExtensionProvider, type FeatureProvider, type FormatQuality, type FormatSelection, type FormatVariant, type MipLevel, type PreferredFormat, type RasterizeSvgOptions, type RawPixelSource, type SelectFormatOptions, type SvgRasterSize, TextureFormat, type TextureHint, WebGLBlockEncoder, type WebGLCapabilities, type WebGLEncodeBytesResult, type WebGLEncoderConstructor, type WebGLEncoderImageSource, type WebGLEncoderOptions, type WebGLFormatSelection, WebGPUFeature, createWebGLContext, detectCapabilities, detectWebGLCapabilities, generateGpuMipChain, generateMipChain, getSharedWebGLContext, gpuMipLevelCount, isWebGLAvailable, padToBlockMultiple, rasterizeSvg, selectFormat, selectWebGLFormat };
608
+ /**
609
+ * Everything `compressTexture()` can take as an image source. A superset
610
+ * of `EncoderImageSource` (see Encoder.ts) that also accepts URL strings
611
+ * and Blob / File objects — the common cases in a web app.
612
+ *
613
+ * SVG works through all of these: a URL to an `.svg` file, a string of
614
+ * inline SVG markup (detected by a leading `<`), an SVG Blob/File, or an
615
+ * HTMLImageElement whose src is SVG. Vector sources are rasterised to RGBA
616
+ * before encoding — see the `svgSize` option.
617
+ */
618
+ type CompressTextureSource = string | Blob | File | ImageBitmap | HTMLImageElement | HTMLCanvasElement | OffscreenCanvas | ImageData;
619
+ interface CompressOptions {
620
+ /** How the texture will be used. Drives format selection. Default 'color'. */
621
+ hint?: TextureHint;
622
+ /**
623
+ * Prefer a specific format over the default choice when the device
624
+ * supports it; falls back to the normal selection (BC7 → ASTC → ETC2 →
625
+ * RGBA8) when it doesn't. Currently only 'bc1': half the memory of BC7
626
+ * for opaque colour textures, at lower quality. Only honoured with
627
+ * `hint: 'color'` — BC1 can't carry real alpha or normal maps.
628
+ */
629
+ preferredFormat?: PreferredFormat;
630
+ /**
631
+ * Memory/fidelity trade-off for opaque colour textures. Default 'high'
632
+ * (BC7 / ASTC 4×4, 1 byte/pixel). 'low' picks the 4-bpp formats when the
633
+ * device has one — BC1 on desktop-class GPUs, ETC2 RGB8 on mobile-class
634
+ * ones — halving GPU memory at visibly lower quality on smooth content.
635
+ * Ignored for `hint: 'colorWithAlpha'` and `hint: 'normal'` (the 4-bpp
636
+ * formats can't carry them). On the WebGL fallback tier only BC1 is
637
+ * available at 'low'.
638
+ */
639
+ quality?: FormatQuality;
640
+ /** Pick the sRGB or linear variant of the chosen format. Default 'srgb'. */
641
+ colorSpace?: 'srgb' | 'linear';
642
+ /**
643
+ * Rasterisation size for SVG sources. A number scales the SVG so its
644
+ * longest side matches (aspect ratio preserved); `{ width, height }`
645
+ * rasterises at exactly that size. Default: the SVG's intrinsic size
646
+ * (absolute width/height attributes, else the viewBox dimensions).
647
+ * Ignored for non-SVG sources.
648
+ */
649
+ svgSize?: SvgRasterSize;
650
+ /** Flip the image vertically before encoding. Default true (matches Three.js convention). */
651
+ flipY?: boolean;
652
+ /** Generate a full mip chain down to 1×1 on the CPU, encode every level. */
653
+ mipmaps?: boolean;
654
+ /** Reuse an existing device (e.g. Three.js's renderer device) instead
655
+ * of creating a new one. WebGPU path only. When provided, the encoder
656
+ * never destroys it. */
657
+ device?: GPUDevice;
658
+ adapter?: GPUAdapter;
659
+ /**
660
+ * Keep the compressed bytes in a session-scoped in-memory LRU and reuse
661
+ * them on repeat calls, skipping BOTH the image decode and the encode —
662
+ * the dominant costs. Re-loading a texture later in the session (e.g.
663
+ * two worlds sharing an atlas) becomes a few ms. Keyed by source
664
+ * identity + selected format + encode options; capped at 256 MiB of
665
+ * compressed bytes by default (`setTranscodeCacheLimit()` to tune) and
666
+ * never touches persistent storage. Default false.
667
+ *
668
+ * URL and Blob/File sources get an identity automatically (URL string or
669
+ * content hash). Pixel sources (ImageBitmap, canvas, ImageData) are only
670
+ * cached when `cacheKey` is provided.
671
+ */
672
+ cache?: boolean;
673
+ /**
674
+ * Explicit cache identity for the source, overriding the derived one.
675
+ * Use when you already know a stable name (e.g. an asset path) and want
676
+ * to skip content hashing, or to make pixel sources cacheable.
677
+ */
678
+ cacheKey?: string;
679
+ }
680
+ interface CompressResult {
681
+ /**
682
+ * Encoded compressed mip levels (`levels[0]` is the base level), ready to
683
+ * upload to a compressed texture. Null on the RGBA8 fallback path — use
684
+ * `fallbackBitmap` instead.
685
+ */
686
+ levels: EncodedLevelBytes[] | null;
687
+ /**
688
+ * Decoded RGBA8 bitmap, set only when `fallbackUncompressed` (no compressed
689
+ * format was available on either backend). Upload it as a plain RGBA8
690
+ * texture; the caller applies colour space / flipY at the texture level.
691
+ */
692
+ fallbackBitmap: ImageBitmap | null;
693
+ /** The compressed format selected, or null when we fell back to RGBA8. */
694
+ format: TextureFormat | null;
695
+ /** True iff we fell back to an uncompressed RGBA8 bitmap because no encoder fit. */
696
+ fallbackUncompressed: boolean;
697
+ /**
698
+ * Which backend produced the result. 'webgpu' = compute path, 'webgl' =
699
+ * fragment-shader fallback, 'none' = uncompressed RGBA8.
700
+ */
701
+ backend: 'webgpu' | 'webgl' | 'none';
702
+ /**
703
+ * True iff the chosen format is ASTC and the hint was 'normal'. The
704
+ * caller must apply the (R, W) → (x, y) swizzle in the material — ASTC
705
+ * has no 2-channel mode, so normal maps ride the RGBA path.
706
+ */
707
+ astcNormalRemap: boolean;
708
+ width: number;
709
+ height: number;
710
+ mipLevels: number;
711
+ /** Wall-clock time of GPU encoding, summed across mip levels. */
712
+ encodeMs: number;
713
+ /**
714
+ * Wall-clock time to turn the source into decoded RGBA pixels: fetch /
715
+ * base64 decode, image decode, SVG rasterisation. Usually the dominant
716
+ * cost for large images — when a load feels slower than `encodeMs`
717
+ * suggests, this is where the time went.
718
+ */
719
+ decodeMs: number;
720
+ /** Wall-clock time of the whole `compressTexture()` call: decode + CPU
721
+ * mip generation + encode + texture assembly. */
722
+ totalMs: number;
723
+ /** True when the result came from the in-memory transcode cache (the
724
+ * `cache` option) — no decode or encode ran; decodeMs/encodeMs are 0. */
725
+ cacheHit: boolean;
726
+ }
727
+ /**
728
+ * Destroy the WebGPU device and encoders that `compressTexture()` shares
729
+ * across calls (created lazily when neither the `device` nor the `adapter`
730
+ * option is passed). Safe to call at any time — in-flight encodes on the
731
+ * shared device will fail, and the next `compressTexture()` call recreates
732
+ * everything. No-op when nothing is cached.
733
+ */
734
+ declare function releaseSharedGpuResources(): void;
735
+ declare function compressTextureToBytes(source: CompressTextureSource, options?: CompressOptions): Promise<CompressResult>;
736
+
737
+ /**
738
+ * Cap the cache's total compressed payload in bytes (default 256 MiB).
739
+ * Lower it to evict immediately; 0 disables caching entirely.
740
+ */
741
+ declare function setTranscodeCacheLimit(bytes: number): void;
742
+ /** Drop every cached transcode. Textures already built from entries are unaffected. */
743
+ declare function clearTranscodeCache(): void;
744
+
745
+ export { ASTC4x4Encoder, ASTC4x4WebGLEncoder, BC1Encoder, BC1WebGLEncoder, BC5Encoder, BC5WebGLEncoder, BC7Encoder, type BC7EncoderOptions, BC7WebGLEncoder, type Capabilities, type CompressOptions, type CompressResult, type CompressTextureSource, ETC2Encoder, type EncodeBytesResult, type EncodeCallOptions, type EncodeMipChainResult, type EncodedLevelBytes, Encoder, type EncoderConstructor, type EncoderImageSource, type EncoderOptions, type ExtensionProvider, type FeatureProvider, type FormatQuality, type FormatSelection, type FormatVariant, type MipLevel, type PreferredFormat, type RasterizeSvgOptions, type RawPixelSource, type SelectFormatOptions, type SvgRasterSize, TextureFormat, type TextureHint, WebGLBlockEncoder, type WebGLCapabilities, type WebGLEncodeBytesResult, type WebGLEncoderConstructor, type WebGLEncoderImageSource, type WebGLEncoderOptions, type WebGLFormatSelection, WebGPUFeature, clearTranscodeCache, compressTextureToBytes, createWebGLContext, detectCapabilities, detectWebGLCapabilities, generateGpuMipChain, generateMipChain, getSharedWebGLContext, gpuMipLevelCount, isWebGLAvailable, padToBlockMultiple, rasterizeSvg, releaseSharedGpuResources, selectFormat, selectWebGLFormat, setTranscodeCacheLimit };
package/dist/index.js CHANGED
@@ -44,6 +44,10 @@ function detectCapabilities(adapter) {
44
44
  }
45
45
 
46
46
  // src/workarounds.ts
47
+ function needsWriteTextureWorkaround(adapter) {
48
+ const { vendor, architecture } = adapter.info ?? {};
49
+ return vendor === "img-tec" && architecture === "d-series";
50
+ }
47
51
  function uploadSourceTexture(device, srcTex, source, width, height, flipY) {
48
52
  if (source instanceof ImageData && !flipY) {
49
53
  if (srcTex.format === "rg8unorm") {
@@ -460,25 +464,25 @@ var Encoder = class {
460
464
  if (useCache && (srcTexIsNew || dstIsNew || prepPlanesNew)) this._cachedBindGroup = null;
461
465
  let bindGroup = useCache ? this._cachedBindGroup : null;
462
466
  if (!bindGroup) {
463
- const entries = [
467
+ const entries2 = [
464
468
  { binding: 0, resource: prepPlanes ? prepPlanes[0].createView() : srcTex.createView() },
465
469
  { binding: 1, resource: { buffer: dstBuffer } },
466
470
  { binding: 2, resource: { buffer: paramsBuffer } }
467
471
  ];
468
472
  if (prepPlanes) {
469
- entries.push({ binding: 3, resource: prepPlanes[1].createView() });
473
+ entries2.push({ binding: 3, resource: prepPlanes[1].createView() });
470
474
  } else if (this._usesSampler) {
471
475
  this._sampler ??= device.createSampler({
472
476
  label: `${this.label}-clamp-sampler`,
473
477
  addressModeU: "clamp-to-edge",
474
478
  addressModeV: "clamp-to-edge"
475
479
  });
476
- entries.push({ binding: 3, resource: this._sampler });
480
+ entries2.push({ binding: 3, resource: this._sampler });
477
481
  }
478
482
  bindGroup = device.createBindGroup({
479
483
  label: `${this.label}-bg`,
480
484
  layout: pipeline.getBindGroupLayout(0),
481
- entries
485
+ entries: entries2
482
486
  });
483
487
  if (useCache) this._cachedBindGroup = bindGroup;
484
488
  }
@@ -646,7 +650,7 @@ var Encoder = class {
646
650
  (g, i) => this._createPrepBindGroup(prepPipeline, texs[i].createView(), planeSets[i], paramsBuf, i * CHAIN_ALIGN)
647
651
  ) : [];
648
652
  bindGroups = geoms.map((g, i) => {
649
- const entries = [
653
+ const entries2 = [
650
654
  {
651
655
  binding: 0,
652
656
  resource: prepPipeline ? planeSets[i][0].createView() : texs[i].createView()
@@ -655,14 +659,14 @@ var Encoder = class {
655
659
  { binding: 2, resource: { buffer: paramsBuf, offset: i * CHAIN_ALIGN, size: 16 } }
656
660
  ];
657
661
  if (prepPipeline) {
658
- entries.push({ binding: 3, resource: planeSets[i][1].createView() });
662
+ entries2.push({ binding: 3, resource: planeSets[i][1].createView() });
659
663
  } else if (this._usesSampler) {
660
- entries.push({ binding: 3, resource: this._sampler });
664
+ entries2.push({ binding: 3, resource: this._sampler });
661
665
  }
662
666
  return device.createBindGroup({
663
667
  label: `${this.label}-chain-bg-${i}`,
664
668
  layout: pipeline.getBindGroupLayout(0),
665
- entries
669
+ entries: entries2
666
670
  });
667
671
  });
668
672
  if (useCache) {
@@ -813,7 +817,7 @@ var Encoder = class {
813
817
  )
814
818
  ) : null;
815
819
  const bindGroups = geoms.map((g, i) => {
816
- const entries = [
820
+ const entries2 = [
817
821
  {
818
822
  binding: 0,
819
823
  resource: planes ? planes[i][0].createView() : srcTex.createView({ baseMipLevel: i, mipLevelCount: 1 })
@@ -822,14 +826,14 @@ var Encoder = class {
822
826
  { binding: 2, resource: { buffer: paramsBuf, offset: i * CHAIN_ALIGN, size: 16 } }
823
827
  ];
824
828
  if (planes) {
825
- entries.push({ binding: 3, resource: planes[i][1].createView() });
829
+ entries2.push({ binding: 3, resource: planes[i][1].createView() });
826
830
  } else if (this._usesSampler) {
827
- entries.push({ binding: 3, resource: this._sampler });
831
+ entries2.push({ binding: 3, resource: this._sampler });
828
832
  }
829
833
  return device.createBindGroup({
830
834
  label: `${this.label}-chain-bg-${i}`,
831
835
  layout: pipeline.getBindGroupLayout(0),
832
- entries
836
+ entries: entries2
833
837
  });
834
838
  });
835
839
  return await this._submitChainAndRead(
@@ -2492,6 +2496,18 @@ async function generateGpuMipChain(device, source, { flipY = false } = {}) {
2492
2496
  }
2493
2497
 
2494
2498
  // src/svg.ts
2499
+ function isSvgMarkup(source) {
2500
+ return source.trimStart().startsWith("<");
2501
+ }
2502
+ function hasSvgExtension(url) {
2503
+ return /\.svg$/i.test(url.split(/[?#]/, 1)[0]);
2504
+ }
2505
+ function isSvgBlob(blob) {
2506
+ if (blob.type) {
2507
+ return blob.type.split(";", 1)[0].trim().toLowerCase() === "image/svg+xml";
2508
+ }
2509
+ return typeof File !== "undefined" && blob instanceof File && hasSvgExtension(blob.name);
2510
+ }
2495
2511
  var ROOT_TAG_RE = /<svg(?=[\s/>])[^>]*>/;
2496
2512
  function getAttr(tag, name) {
2497
2513
  const m = new RegExp(`\\s${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`).exec(tag);
@@ -2612,6 +2628,482 @@ async function rasterizeSvg(source, options = {}) {
2612
2628
  URL.revokeObjectURL(url);
2613
2629
  }
2614
2630
  }
2631
+
2632
+ // src/transcodeCache.ts
2633
+ var DEFAULT_LIMIT = 256 * 1024 * 1024;
2634
+ var maxBytes = DEFAULT_LIMIT;
2635
+ var totalBytes = 0;
2636
+ var entries = /* @__PURE__ */ new Map();
2637
+ async function sha256Hex(data) {
2638
+ const digest = await crypto.subtle.digest("SHA-256", data);
2639
+ let hex = "";
2640
+ for (const b of new Uint8Array(digest)) hex += b.toString(16).padStart(2, "0");
2641
+ return hex;
2642
+ }
2643
+ async function sourceIdentity(source, cacheKey) {
2644
+ if (cacheKey) return `k:${cacheKey}`;
2645
+ const canHash = typeof crypto !== "undefined" && !!crypto.subtle;
2646
+ if (typeof source === "string") {
2647
+ if (source.length > 1024 || /^data:/i.test(source) || /^\s*</.test(source)) {
2648
+ return canHash ? `s:${await sha256Hex(new TextEncoder().encode(source))}` : null;
2649
+ }
2650
+ return `u:${source}`;
2651
+ }
2652
+ if (typeof Blob !== "undefined" && source instanceof Blob) {
2653
+ return canHash ? `b:${await sha256Hex(await source.arrayBuffer())}` : null;
2654
+ }
2655
+ return null;
2656
+ }
2657
+ async function buildTranscodeKey(source, cacheKey, fp) {
2658
+ if (maxBytes <= 0) return null;
2659
+ const id = await sourceIdentity(source, cacheKey);
2660
+ if (!id) return null;
2661
+ const fingerprint = [
2662
+ fp.format,
2663
+ fp.colorSpace,
2664
+ fp.flipY ? "flip" : "noflip",
2665
+ fp.mipmaps ? "mips" : "nomips",
2666
+ fp.svgSize === void 0 ? "" : JSON.stringify(fp.svgSize)
2667
+ ].join("|");
2668
+ return `${fingerprint}\0${id}`;
2669
+ }
2670
+ function readTranscodeCache(key) {
2671
+ const hit = entries.get(key);
2672
+ if (!hit) return null;
2673
+ entries.delete(key);
2674
+ entries.set(key, hit);
2675
+ return hit.entry;
2676
+ }
2677
+ function writeTranscodeCache(key, entry) {
2678
+ const bytes = entry.levels.reduce((sum, l) => sum + l.data.byteLength, 0);
2679
+ if (bytes > maxBytes) return;
2680
+ const prev = entries.get(key);
2681
+ if (prev) {
2682
+ totalBytes -= prev.bytes;
2683
+ entries.delete(key);
2684
+ }
2685
+ entries.set(key, { entry, bytes });
2686
+ totalBytes += bytes;
2687
+ evictToLimit();
2688
+ }
2689
+ function evictToLimit() {
2690
+ for (const [key, value] of entries) {
2691
+ if (totalBytes <= maxBytes) break;
2692
+ entries.delete(key);
2693
+ totalBytes -= value.bytes;
2694
+ }
2695
+ }
2696
+ function setTranscodeCacheLimit(bytes) {
2697
+ maxBytes = Math.max(0, bytes);
2698
+ evictToLimit();
2699
+ }
2700
+ function clearTranscodeCache() {
2701
+ entries.clear();
2702
+ totalBytes = 0;
2703
+ }
2704
+
2705
+ // src/compressTexture.ts
2706
+ var sharedGpuPromise = null;
2707
+ var SHARED_DEVICE_FEATURES = [
2708
+ "texture-compression-bc",
2709
+ "texture-compression-astc",
2710
+ "texture-compression-etc2",
2711
+ "shader-f16",
2712
+ "timestamp-query"
2713
+ ];
2714
+ async function createSharedGpu() {
2715
+ const adapter = await navigator.gpu.requestAdapter();
2716
+ if (!adapter) return null;
2717
+ const requiredFeatures = SHARED_DEVICE_FEATURES.filter((f) => adapter.features.has(f));
2718
+ const device = await adapter.requestDevice({ requiredFeatures });
2719
+ return { adapter, device, encoders: /* @__PURE__ */ new Map() };
2720
+ }
2721
+ function getSharedGpu() {
2722
+ if (!sharedGpuPromise) {
2723
+ const p = createSharedGpu();
2724
+ sharedGpuPromise = p;
2725
+ p.then((shared) => {
2726
+ if (!shared) return;
2727
+ void shared.device.lost.then(() => {
2728
+ shared.encoders.forEach((encoder) => encoder.destroy());
2729
+ shared.encoders.clear();
2730
+ if (sharedGpuPromise === p) sharedGpuPromise = null;
2731
+ });
2732
+ }).catch(() => {
2733
+ if (sharedGpuPromise === p) sharedGpuPromise = null;
2734
+ });
2735
+ }
2736
+ return sharedGpuPromise;
2737
+ }
2738
+ function releaseSharedGpuResources() {
2739
+ const p = sharedGpuPromise;
2740
+ sharedGpuPromise = null;
2741
+ void p?.then((shared) => {
2742
+ if (!shared) return;
2743
+ shared.encoders.forEach((encoder) => encoder.destroy());
2744
+ shared.encoders.clear();
2745
+ shared.device.destroy();
2746
+ }).catch(() => {
2747
+ });
2748
+ }
2749
+ async function sourceToBitmap(source, svgSize) {
2750
+ const opts = {
2751
+ colorSpaceConversion: "none",
2752
+ premultiplyAlpha: "none"
2753
+ };
2754
+ if (typeof source === "string") {
2755
+ if (isSvgMarkup(source)) {
2756
+ return rasterizeSvg(source, { size: svgSize });
2757
+ }
2758
+ if (/^data:/i.test(source)) {
2759
+ const blob2 = dataUrlToBlob(source);
2760
+ if (isSvgBlob(blob2)) {
2761
+ return rasterizeSvg(blob2, { size: svgSize });
2762
+ }
2763
+ return createImageBitmap(blob2, opts);
2764
+ }
2765
+ const resp = await fetch(source);
2766
+ if (!resp.ok) {
2767
+ throw new Error(`compressTexture: fetch ${source} failed (${resp.status})`);
2768
+ }
2769
+ const blob = await resp.blob();
2770
+ if (isSvgBlob(blob) || !isImageMimeType(blob.type) && hasSvgExtension(source)) {
2771
+ return rasterizeSvg(blob, { size: svgSize });
2772
+ }
2773
+ return createImageBitmap(blob, opts);
2774
+ }
2775
+ if (source instanceof Blob) {
2776
+ if (isSvgBlob(source)) {
2777
+ return rasterizeSvg(source, { size: svgSize });
2778
+ }
2779
+ return createImageBitmap(source, opts);
2780
+ }
2781
+ if (source instanceof ImageBitmap) {
2782
+ return source;
2783
+ }
2784
+ if (typeof HTMLImageElement !== "undefined" && source instanceof HTMLImageElement) {
2785
+ const src = source.currentSrc || source.src;
2786
+ if (src && (hasSvgExtension(src) || /^data:image\/svg\+xml/i.test(src))) {
2787
+ const resp = await fetch(src);
2788
+ if (!resp.ok) {
2789
+ throw new Error(`compressTexture: fetch ${src} failed (${resp.status})`);
2790
+ }
2791
+ return rasterizeSvg(await resp.blob(), { size: svgSize });
2792
+ }
2793
+ }
2794
+ return createImageBitmap(source, opts);
2795
+ }
2796
+ function isImageMimeType(type) {
2797
+ return /^image\//i.test(type) && !/svg/i.test(type);
2798
+ }
2799
+ function dataUrlToBlob(url) {
2800
+ const comma = url.indexOf(",");
2801
+ if (comma < 0) {
2802
+ throw new Error("compressTexture: malformed data: URL (no comma)");
2803
+ }
2804
+ const header = url.slice(5, comma);
2805
+ const isBase64 = /;base64$/i.test(header);
2806
+ const type = header.replace(/;base64$/i, "");
2807
+ if (!isBase64) {
2808
+ return new Blob([decodeURIComponent(url.slice(comma + 1))], { type });
2809
+ }
2810
+ const payload = url.slice(comma + 1);
2811
+ const fromBase64 = Uint8Array.fromBase64;
2812
+ if (fromBase64) {
2813
+ return new Blob([fromBase64(payload)], { type });
2814
+ }
2815
+ const bin = atob(payload);
2816
+ const bytes = new Uint8Array(bin.length);
2817
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
2818
+ return new Blob([bytes], { type });
2819
+ }
2820
+ function bitmapToMipLevel(bitmap, flipY) {
2821
+ const w = bitmap.width, h = bitmap.height;
2822
+ const canvas = typeof OffscreenCanvas !== "undefined" ? new OffscreenCanvas(w, h) : Object.assign(document.createElement("canvas"), { width: w, height: h });
2823
+ const ctx = canvas.getContext("2d", { willReadFrequently: true });
2824
+ if (!ctx) {
2825
+ throw new Error("compressTexture: no 2D context available for mip generation");
2826
+ }
2827
+ if (flipY) {
2828
+ ctx.translate(0, h);
2829
+ ctx.scale(1, -1);
2830
+ }
2831
+ ctx.drawImage(bitmap, 0, 0);
2832
+ const imageData = ctx.getImageData(0, 0, w, h);
2833
+ return { data: imageData.data, width: w, height: h };
2834
+ }
2835
+ function mipLevelToImageData(level) {
2836
+ return new ImageData(level.data, level.width, level.height);
2837
+ }
2838
+ async function compressTextureToBytes(source, options = {}) {
2839
+ const {
2840
+ hint = "color",
2841
+ preferredFormat,
2842
+ quality = "high",
2843
+ colorSpace = "srgb",
2844
+ svgSize,
2845
+ flipY = true,
2846
+ mipmaps = false,
2847
+ cache = false,
2848
+ cacheKey,
2849
+ device: providedDevice,
2850
+ adapter: providedAdapter
2851
+ } = options;
2852
+ const t0 = performance.now();
2853
+ const gpu = await resolveWebGPU();
2854
+ const gl = gpu ? null : resolveWebGL();
2855
+ const activeFormat = gpu?.selection.format ?? gl?.selection.format ?? null;
2856
+ let transcodeKey = null;
2857
+ if (cache && activeFormat) {
2858
+ transcodeKey = await buildTranscodeKey(source, cacheKey, {
2859
+ format: activeFormat,
2860
+ colorSpace,
2861
+ flipY,
2862
+ mipmaps,
2863
+ svgSize
2864
+ });
2865
+ if (transcodeKey) {
2866
+ const hit = readTranscodeCache(transcodeKey);
2867
+ if (hit) {
2868
+ return {
2869
+ levels: hit.levels,
2870
+ fallbackBitmap: null,
2871
+ format: hit.format,
2872
+ fallbackUncompressed: false,
2873
+ backend: gpu ? "webgpu" : "webgl",
2874
+ astcNormalRemap: (gpu ?? gl).selection.astcNormalRemap,
2875
+ width: hit.width,
2876
+ height: hit.height,
2877
+ mipLevels: hit.levels.length,
2878
+ encodeMs: 0,
2879
+ decodeMs: 0,
2880
+ totalMs: performance.now() - t0,
2881
+ cacheHit: true
2882
+ };
2883
+ }
2884
+ }
2885
+ }
2886
+ const tDecode = performance.now();
2887
+ const bitmap = await sourceToBitmap(source, svgSize);
2888
+ const decodeMs = performance.now() - tDecode;
2889
+ if (gpu) return encodeViaWebGPU(gpu);
2890
+ const viaWebGL = gl ? encodeViaWebGL(gl) : null;
2891
+ if (viaWebGL) return viaWebGL;
2892
+ console.warn(
2893
+ "[compressTextureToBytes] No compressed path available (WebGPU and WebGL2 both lack a usable compressed-texture format); returning uncompressed RGBA8."
2894
+ );
2895
+ return {
2896
+ levels: null,
2897
+ fallbackBitmap: bitmap,
2898
+ format: null,
2899
+ fallbackUncompressed: true,
2900
+ backend: "none",
2901
+ astcNormalRemap: false,
2902
+ width: bitmap.width,
2903
+ height: bitmap.height,
2904
+ mipLevels: 1,
2905
+ encodeMs: 0,
2906
+ decodeMs,
2907
+ totalMs: performance.now() - t0,
2908
+ cacheHit: false
2909
+ };
2910
+ async function resolveWebGPU() {
2911
+ if (!("gpu" in navigator)) return null;
2912
+ let shared = null;
2913
+ let adapter;
2914
+ if (providedAdapter) {
2915
+ adapter = providedAdapter;
2916
+ } else if (providedDevice) {
2917
+ adapter = await navigator.gpu.requestAdapter();
2918
+ } else {
2919
+ shared = await getSharedGpu();
2920
+ adapter = shared?.adapter ?? null;
2921
+ }
2922
+ if (!adapter) return null;
2923
+ const selection = selectFormat(adapter, hint, { colorSpace, preferredFormat, quality });
2924
+ if (!selection.format || !selection.encoderClass) return null;
2925
+ return {
2926
+ adapter,
2927
+ shared,
2928
+ selection: { ...selection, format: selection.format, encoderClass: selection.encoderClass }
2929
+ };
2930
+ }
2931
+ async function encodeViaWebGPU({ adapter, shared, selection }) {
2932
+ const EncoderCtor = selection.encoderClass;
2933
+ let encoder;
2934
+ let sharedEncoder = false;
2935
+ if (providedDevice) {
2936
+ encoder = new EncoderCtor({ device: providedDevice, adapter, ownsDevice: false });
2937
+ } else if (shared) {
2938
+ sharedEncoder = true;
2939
+ let cached = shared.encoders.get(EncoderCtor);
2940
+ if (!cached) {
2941
+ cached = new EncoderCtor({ device: shared.device, adapter: shared.adapter, ownsDevice: false });
2942
+ shared.encoders.set(EncoderCtor, cached);
2943
+ }
2944
+ encoder = cached;
2945
+ } else {
2946
+ encoder = await EncoderCtor.create();
2947
+ }
2948
+ const destroyEncoder = sharedEncoder ? () => {
2949
+ } : () => encoder.destroy();
2950
+ try {
2951
+ const needsWriteTexture = needsWriteTextureWorkaround(adapter);
2952
+ if (!mipmaps) {
2953
+ let bytes;
2954
+ if (needsWriteTexture) {
2955
+ const level0 = bitmapToMipLevel(bitmap, flipY);
2956
+ const imageData = mipLevelToImageData(level0);
2957
+ bytes = await encoder.encodeToBytes(imageData);
2958
+ } else {
2959
+ bytes = await encoder.encodeToBytes(bitmap, { flipY });
2960
+ }
2961
+ if (transcodeKey) {
2962
+ writeTranscodeCache(transcodeKey, {
2963
+ format: selection.format,
2964
+ width: bytes.width,
2965
+ height: bytes.height,
2966
+ levels: [bytes]
2967
+ });
2968
+ }
2969
+ destroyEncoder();
2970
+ return {
2971
+ levels: [bytes],
2972
+ fallbackBitmap: null,
2973
+ format: selection.format,
2974
+ fallbackUncompressed: false,
2975
+ backend: "webgpu",
2976
+ astcNormalRemap: selection.astcNormalRemap,
2977
+ width: bytes.width,
2978
+ height: bytes.height,
2979
+ mipLevels: 1,
2980
+ encodeMs: bytes.encodeMs,
2981
+ decodeMs,
2982
+ totalMs: performance.now() - t0,
2983
+ cacheHit: false
2984
+ };
2985
+ }
2986
+ let chainResult;
2987
+ if (needsWriteTexture) {
2988
+ const level0 = bitmapToMipLevel(bitmap, flipY);
2989
+ chainResult = await encoder.encodeMipChainToBytes(generateMipChain(level0).map(padToBlockMultiple));
2990
+ } else {
2991
+ const chainTex = await generateGpuMipChain(encoder.device, bitmap, { flipY });
2992
+ try {
2993
+ chainResult = await encoder.encodeMipChainFromTexture(chainTex);
2994
+ } finally {
2995
+ chainTex.destroy();
2996
+ }
2997
+ }
2998
+ const { levels, encodeMs } = chainResult;
2999
+ if (transcodeKey) {
3000
+ writeTranscodeCache(transcodeKey, {
3001
+ format: selection.format,
3002
+ width: bitmap.width,
3003
+ height: bitmap.height,
3004
+ levels
3005
+ });
3006
+ }
3007
+ destroyEncoder();
3008
+ return {
3009
+ levels,
3010
+ fallbackBitmap: null,
3011
+ format: selection.format,
3012
+ fallbackUncompressed: false,
3013
+ backend: "webgpu",
3014
+ astcNormalRemap: selection.astcNormalRemap,
3015
+ width: bitmap.width,
3016
+ height: bitmap.height,
3017
+ mipLevels: levels.length,
3018
+ encodeMs,
3019
+ decodeMs,
3020
+ totalMs: performance.now() - t0,
3021
+ cacheHit: false
3022
+ };
3023
+ } catch (e) {
3024
+ destroyEncoder();
3025
+ throw e;
3026
+ }
3027
+ }
3028
+ function resolveWebGL() {
3029
+ const gl2 = getSharedWebGLContext();
3030
+ if (!gl2) return null;
3031
+ const caps = detectWebGLCapabilities(gl2);
3032
+ const selection = selectWebGLFormat(caps, hint, { colorSpace, preferredFormat, quality });
3033
+ if (!selection.format || !selection.encoderClass) return null;
3034
+ return { gl: gl2, selection: { ...selection, format: selection.format, encoderClass: selection.encoderClass } };
3035
+ }
3036
+ function encodeViaWebGL({ gl: gl2, selection }) {
3037
+ const encoder = selection.encoderClass.create(gl2);
3038
+ try {
3039
+ if (!mipmaps) {
3040
+ const bytes = encoder.encodeToBytes(bitmap, { flipY });
3041
+ if (transcodeKey) {
3042
+ writeTranscodeCache(transcodeKey, {
3043
+ format: selection.format,
3044
+ width: bytes.width,
3045
+ height: bytes.height,
3046
+ levels: [bytes]
3047
+ });
3048
+ }
3049
+ encoder.destroy();
3050
+ return {
3051
+ levels: [bytes],
3052
+ fallbackBitmap: null,
3053
+ format: selection.format,
3054
+ fallbackUncompressed: false,
3055
+ backend: "webgl",
3056
+ astcNormalRemap: selection.astcNormalRemap,
3057
+ width: bytes.width,
3058
+ height: bytes.height,
3059
+ mipLevels: 1,
3060
+ encodeMs: bytes.encodeMs,
3061
+ decodeMs,
3062
+ totalMs: performance.now() - t0,
3063
+ cacheHit: false
3064
+ };
3065
+ }
3066
+ const level0 = bitmapToMipLevel(bitmap, flipY);
3067
+ const chain = generateMipChain(level0);
3068
+ const encodedLevels = [];
3069
+ let totalEncodeMs = 0;
3070
+ for (const level of chain) {
3071
+ const padded = padToBlockMultiple(level);
3072
+ const bytes = encoder.encodeToBytes(padded);
3073
+ encodedLevels.push(bytes);
3074
+ totalEncodeMs += bytes.encodeMs;
3075
+ }
3076
+ if (transcodeKey) {
3077
+ writeTranscodeCache(transcodeKey, {
3078
+ format: selection.format,
3079
+ width: level0.width,
3080
+ height: level0.height,
3081
+ levels: encodedLevels
3082
+ });
3083
+ }
3084
+ encoder.destroy();
3085
+ return {
3086
+ levels: encodedLevels,
3087
+ fallbackBitmap: null,
3088
+ format: selection.format,
3089
+ fallbackUncompressed: false,
3090
+ backend: "webgl",
3091
+ astcNormalRemap: selection.astcNormalRemap,
3092
+ width: level0.width,
3093
+ height: level0.height,
3094
+ mipLevels: encodedLevels.length,
3095
+ encodeMs: totalEncodeMs,
3096
+ decodeMs,
3097
+ totalMs: performance.now() - t0,
3098
+ cacheHit: false
3099
+ };
3100
+ } catch (e) {
3101
+ encoder.destroy();
3102
+ console.warn("[compressTextureToBytes] WebGL fallback encode failed; returning uncompressed RGBA8.", e);
3103
+ return null;
3104
+ }
3105
+ }
3106
+ }
2615
3107
  export {
2616
3108
  ASTC4x4Encoder,
2617
3109
  ASTC4x4WebGLEncoder,
@@ -2626,6 +3118,8 @@ export {
2626
3118
  TextureFormat,
2627
3119
  WebGLBlockEncoder,
2628
3120
  WebGPUFeature,
3121
+ clearTranscodeCache,
3122
+ compressTextureToBytes,
2629
3123
  createWebGLContext,
2630
3124
  detectCapabilities,
2631
3125
  detectWebGLCapabilities,
@@ -2636,6 +3130,8 @@ export {
2636
3130
  isWebGLAvailable,
2637
3131
  padToBlockMultiple,
2638
3132
  rasterizeSvg,
3133
+ releaseSharedGpuResources,
2639
3134
  selectFormat,
2640
- selectWebGLFormat
3135
+ selectWebGLFormat,
3136
+ setTranscodeCacheLimit
2641
3137
  };
package/dist/three.d.ts CHANGED
@@ -1,139 +1,18 @@
1
- import { TextureHint, PreferredFormat, FormatQuality, SvgRasterSize, TextureFormat, Encoder, EncoderImageSource } from './index.js';
2
- export { ASTC4x4Encoder, ASTC4x4WebGLEncoder, BC1Encoder, BC1WebGLEncoder, BC5Encoder, BC5WebGLEncoder, BC7Encoder, BC7EncoderOptions, BC7WebGLEncoder, Capabilities, ETC2Encoder, EncodeBytesResult, EncodeCallOptions, EncodeMipChainResult, EncodedLevelBytes, EncoderConstructor, EncoderOptions, ExtensionProvider, FeatureProvider, FormatSelection, FormatVariant, MipLevel, RasterizeSvgOptions, RawPixelSource, SelectFormatOptions, WebGLBlockEncoder, WebGLCapabilities, WebGLEncodeBytesResult, WebGLEncoderConstructor, WebGLEncoderImageSource, WebGLEncoderOptions, WebGLFormatSelection, WebGPUFeature, createWebGLContext, detectCapabilities, detectWebGLCapabilities, generateGpuMipChain, generateMipChain, getSharedWebGLContext, gpuMipLevelCount, isWebGLAvailable, padToBlockMultiple, rasterizeSvg, selectFormat, selectWebGLFormat } from './index.js';
1
+ import { CompressResult as CompressResult$1, CompressTextureSource, CompressOptions, TextureHint, PreferredFormat, FormatQuality, SvgRasterSize, TextureFormat, Encoder, EncoderImageSource } from './index.js';
2
+ export { ASTC4x4Encoder, ASTC4x4WebGLEncoder, BC1Encoder, BC1WebGLEncoder, BC5Encoder, BC5WebGLEncoder, BC7Encoder, BC7EncoderOptions, BC7WebGLEncoder, Capabilities, ETC2Encoder, EncodeBytesResult, EncodeCallOptions, EncodeMipChainResult, EncodedLevelBytes, EncoderConstructor, EncoderOptions, ExtensionProvider, FeatureProvider, FormatSelection, FormatVariant, MipLevel, RasterizeSvgOptions, RawPixelSource, SelectFormatOptions, WebGLBlockEncoder, WebGLCapabilities, WebGLEncodeBytesResult, WebGLEncoderConstructor, WebGLEncoderImageSource, WebGLEncoderOptions, WebGLFormatSelection, WebGPUFeature, clearTranscodeCache, compressTextureToBytes, createWebGLContext, detectCapabilities, detectWebGLCapabilities, generateGpuMipChain, generateMipChain, getSharedWebGLContext, gpuMipLevelCount, isWebGLAvailable, padToBlockMultiple, rasterizeSvg, releaseSharedGpuResources, selectFormat, selectWebGLFormat, setTranscodeCacheLimit } from './index.js';
3
3
  import { Texture, CompressedTexture, Loader, CompressedPixelFormat } from 'three';
4
4
 
5
- /**
6
- * Everything `compressTexture()` can take as an image source. A superset
7
- * of `EncoderImageSource` (see Encoder.ts) that also accepts URL strings
8
- * and Blob / File objects — the common cases in a web app.
9
- *
10
- * SVG works through all of these: a URL to an `.svg` file, a string of
11
- * inline SVG markup (detected by a leading `<`), an SVG Blob/File, or an
12
- * HTMLImageElement whose src is SVG. Vector sources are rasterised to RGBA
13
- * before encoding — see the `svgSize` option.
14
- */
15
- type CompressTextureSource = string | Blob | File | ImageBitmap | HTMLImageElement | HTMLCanvasElement | OffscreenCanvas | ImageData;
16
- interface CompressOptions {
17
- /** How the texture will be used. Drives format selection. Default 'color'. */
18
- hint?: TextureHint;
19
- /**
20
- * Prefer a specific format over the default choice when the device
21
- * supports it; falls back to the normal selection (BC7 → ASTC → ETC2 →
22
- * RGBA8) when it doesn't. Currently only 'bc1': half the memory of BC7
23
- * for opaque colour textures, at lower quality. Only honoured with
24
- * `hint: 'color'` — BC1 can't carry real alpha or normal maps.
25
- */
26
- preferredFormat?: PreferredFormat;
27
- /**
28
- * Memory/fidelity trade-off for opaque colour textures. Default 'high'
29
- * (BC7 / ASTC 4×4, 1 byte/pixel). 'low' picks the 4-bpp formats when the
30
- * device has one — BC1 on desktop-class GPUs, ETC2 RGB8 on mobile-class
31
- * ones — halving GPU memory at visibly lower quality on smooth content.
32
- * Ignored for `hint: 'colorWithAlpha'` and `hint: 'normal'` (the 4-bpp
33
- * formats can't carry them). On the WebGL fallback tier only BC1 is
34
- * available at 'low'.
35
- */
36
- quality?: FormatQuality;
37
- /** Pick the sRGB or linear variant of the chosen format. Default 'srgb'. */
38
- colorSpace?: 'srgb' | 'linear';
39
- /**
40
- * Rasterisation size for SVG sources. A number scales the SVG so its
41
- * longest side matches (aspect ratio preserved); `{ width, height }`
42
- * rasterises at exactly that size. Default: the SVG's intrinsic size
43
- * (absolute width/height attributes, else the viewBox dimensions).
44
- * Ignored for non-SVG sources.
45
- */
46
- svgSize?: SvgRasterSize;
47
- /** Flip the image vertically before encoding. Default true (matches Three.js convention). */
48
- flipY?: boolean;
49
- /** Generate a full mip chain down to 1×1 on the CPU, encode every level. */
50
- mipmaps?: boolean;
51
- /** Reuse an existing device (e.g. Three.js's renderer device) instead
52
- * of creating a new one. WebGPU path only. When provided, the encoder
53
- * never destroys it. */
54
- device?: GPUDevice;
55
- adapter?: GPUAdapter;
56
- /**
57
- * Keep the compressed bytes in a session-scoped in-memory LRU and reuse
58
- * them on repeat calls, skipping BOTH the image decode and the encode —
59
- * the dominant costs. Re-loading a texture later in the session (e.g.
60
- * two worlds sharing an atlas) becomes a few ms. Keyed by source
61
- * identity + selected format + encode options; capped at 256 MiB of
62
- * compressed bytes by default (`setTranscodeCacheLimit()` to tune) and
63
- * never touches persistent storage. Default false.
64
- *
65
- * URL and Blob/File sources get an identity automatically (URL string or
66
- * content hash). Pixel sources (ImageBitmap, canvas, ImageData) are only
67
- * cached when `cacheKey` is provided.
68
- */
69
- cache?: boolean;
70
- /**
71
- * Explicit cache identity for the source, overriding the derived one.
72
- * Use when you already know a stable name (e.g. an asset path) and want
73
- * to skip content hashing, or to make pixel sources cacheable.
74
- */
75
- cacheKey?: string;
76
- }
77
- interface CompressResult {
78
- /** CompressedTexture on a compressed path; Texture on RGBA8 fallback. */
5
+ /** A `compressTexture()` result: a ready-to-use texture plus the same encode
6
+ * metadata `compressTextureToBytes()` returns (minus the raw `levels`). */
7
+ interface CompressResult extends Omit<CompressResult$1, 'levels' | 'fallbackBitmap'> {
8
+ /** `CompressedTexture` on a compressed path; a plain `Texture` on the RGBA8 fallback. */
79
9
  texture: Texture | CompressedTexture;
80
- /** The compressed format selected, or null when we fell back to RGBA8. */
81
- format: TextureFormat | null;
82
- /** True iff we returned an uncompressed Texture because no encoder fit. */
83
- fallbackUncompressed: boolean;
84
- /**
85
- * Which backend produced the result. 'webgpu' = compute path, 'webgl' =
86
- * fragment-shader fallback, 'none' = uncompressed RGBA8.
87
- */
88
- backend: 'webgpu' | 'webgl' | 'none';
89
- /**
90
- * True iff the chosen format is ASTC and the hint was 'normal'. The
91
- * caller must apply the (R, W) → (x, y) swizzle in the material — ASTC
92
- * has no 2-channel mode, so normal maps ride the RGBA path.
93
- */
94
- astcNormalRemap: boolean;
95
- width: number;
96
- height: number;
97
- mipLevels: number;
98
- /** Wall-clock time of GPU encoding, summed across mip levels. */
99
- encodeMs: number;
100
- /**
101
- * Wall-clock time to turn the source into decoded RGBA pixels: fetch /
102
- * base64 decode, image decode, SVG rasterisation. Usually the dominant
103
- * cost for large images — when a load feels slower than `encodeMs`
104
- * suggests, this is where the time went.
105
- */
106
- decodeMs: number;
107
- /** Wall-clock time of the whole `compressTexture()` call: decode + CPU
108
- * mip generation + encode + texture assembly. */
109
- totalMs: number;
110
- /** True when the result came from the in-memory transcode cache (the
111
- * `cache` option) — no decode or encode ran; decodeMs/encodeMs are 0. */
112
- cacheHit: boolean;
113
- /** Dispose the texture and release GPU resources owned by this call.
114
- * On the default path (no `device`/`adapter` option) the encoder and
115
- * device are shared across `compressTexture()` calls and survive this —
116
- * release those with `releaseSharedGpuResources()`. */
10
+ /** Dispose the texture. The shared encoder/device survive — release those
11
+ * with `releaseSharedGpuResources()`. */
117
12
  destroy(): void;
118
13
  }
119
- /**
120
- * Destroy the WebGPU device and encoders that `compressTexture()` shares
121
- * across calls (created lazily when neither the `device` nor the `adapter`
122
- * option is passed). Safe to call at any time — in-flight encodes on the
123
- * shared device will fail, and the next `compressTexture()` call recreates
124
- * everything. No-op when nothing is cached.
125
- */
126
- declare function releaseSharedGpuResources(): void;
127
14
  declare function compressTexture(source: CompressTextureSource, options?: CompressOptions): Promise<CompressResult>;
128
15
 
129
- /**
130
- * Cap the cache's total compressed payload in bytes (default 256 MiB).
131
- * Lower it to evict immediately; 0 disables caching entirely.
132
- */
133
- declare function setTranscodeCacheLimit(bytes: number): void;
134
- /** Drop every cached transcode. Textures already built from entries are unaffected. */
135
- declare function clearTranscodeCache(): void;
136
-
137
16
  declare class GputexLoader extends Loader<Texture> {
138
17
  /** Format-selection hint. Default 'color'. */
139
18
  hint: TextureHint;
@@ -230,4 +109,4 @@ interface EncodeToTextureOptions {
230
109
  */
231
110
  declare function encodeToTexture(encoder: Encoder, source: EncoderImageSource, { colorSpace, flipY }?: EncodeToTextureOptions): Promise<EncodeResult>;
232
111
 
233
- export { type CompressOptions, type CompressResult, type CompressTextureSource, type EncodeResult, type EncodeToTextureOptions, Encoder, EncoderImageSource, FormatQuality, GputexLoader, PreferredFormat, SvgRasterSize, TextureFormat, TextureHint, buildCompressedTexture, clearTranscodeCache, compressTexture, encodeToTexture, releaseSharedGpuResources, setTranscodeCacheLimit, threeFormatFor };
112
+ export { CompressOptions, type CompressResult, CompressTextureSource, type EncodeResult, type EncodeToTextureOptions, Encoder, EncoderImageSource, FormatQuality, GputexLoader, PreferredFormat, SvgRasterSize, TextureFormat, TextureHint, buildCompressedTexture, compressTexture, encodeToTexture, threeFormatFor };
package/dist/three.js CHANGED
@@ -2629,87 +2629,7 @@ async function rasterizeSvg(source, options = {}) {
2629
2629
  }
2630
2630
  }
2631
2631
 
2632
- // src/three/compressTexture.ts
2633
- import { ClampToEdgeWrapping as ClampToEdgeWrapping2, LinearFilter as LinearFilter2, LinearSRGBColorSpace as LinearSRGBColorSpace2, SRGBColorSpace as SRGBColorSpace2, Texture } from "three";
2634
-
2635
- // src/three/buildTexture.ts
2636
- import {
2637
- RED_GREEN_RGTC2_Format,
2638
- RGB_ETC2_Format,
2639
- RGBA_ASTC_4x4_Format,
2640
- RGBA_BPTC_Format,
2641
- RGBA_S3TC_DXT1_Format
2642
- } from "three";
2643
-
2644
- // src/three/textureAssembly.ts
2645
- import {
2646
- ClampToEdgeWrapping,
2647
- CompressedTexture,
2648
- LinearFilter,
2649
- LinearMipmapLinearFilter,
2650
- LinearSRGBColorSpace,
2651
- SRGBColorSpace
2652
- } from "three";
2653
- function assembleCompressedTexture(levels, threeFormat, effectiveSrgb) {
2654
- if (levels.length === 0) {
2655
- throw new Error("assembleCompressedTexture: no levels provided");
2656
- }
2657
- const mipmaps = levels.map((l) => ({
2658
- data: l.data,
2659
- width: l.paddedWidth,
2660
- height: l.paddedHeight
2661
- }));
2662
- const base = levels[0];
2663
- const texture = new CompressedTexture(mipmaps, base.paddedWidth, base.paddedHeight, threeFormat);
2664
- texture.colorSpace = effectiveSrgb ? SRGBColorSpace : LinearSRGBColorSpace;
2665
- texture.magFilter = LinearFilter;
2666
- texture.minFilter = levels.length > 1 ? LinearMipmapLinearFilter : LinearFilter;
2667
- texture.generateMipmaps = false;
2668
- texture.wrapS = texture.wrapT = ClampToEdgeWrapping;
2669
- texture.needsUpdate = true;
2670
- texture.userData.logicalWidth = base.width;
2671
- texture.userData.logicalHeight = base.height;
2672
- texture.userData.mipLevels = levels.length;
2673
- return texture;
2674
- }
2675
-
2676
- // src/three/buildTexture.ts
2677
- var THREE_FORMAT = {
2678
- [TextureFormat.BC1]: RGBA_S3TC_DXT1_Format,
2679
- [TextureFormat.BC1_SRGB]: RGBA_S3TC_DXT1_Format,
2680
- [TextureFormat.BC5]: RED_GREEN_RGTC2_Format,
2681
- [TextureFormat.BC7]: RGBA_BPTC_Format,
2682
- [TextureFormat.BC7_SRGB]: RGBA_BPTC_Format,
2683
- [TextureFormat.ASTC_4x4]: RGBA_ASTC_4x4_Format,
2684
- [TextureFormat.ASTC_4x4_SRGB]: RGBA_ASTC_4x4_Format,
2685
- [TextureFormat.ETC2_RGB8]: RGB_ETC2_Format,
2686
- [TextureFormat.ETC2_RGB8_SRGB]: RGB_ETC2_Format
2687
- };
2688
- var SRGB_FORMATS = /* @__PURE__ */ new Set([
2689
- TextureFormat.BC1_SRGB,
2690
- TextureFormat.BC7_SRGB,
2691
- TextureFormat.ASTC_4x4_SRGB,
2692
- TextureFormat.ETC2_RGB8_SRGB
2693
- ]);
2694
- function isSrgbFormat(format) {
2695
- return SRGB_FORMATS.has(format);
2696
- }
2697
- function threeFormatFor(format) {
2698
- return THREE_FORMAT[format];
2699
- }
2700
- function buildCompressedTexture(levels, format) {
2701
- return assembleCompressedTexture(levels, threeFormatFor(format), isSrgbFormat(format));
2702
- }
2703
- async function encodeToTexture(encoder, source, { colorSpace = "srgb", flipY = false } = {}) {
2704
- const formats = encoder.constructor.textureFormats;
2705
- const wantSrgb = colorSpace === "srgb" && encoder.supportsSrgb;
2706
- const format = formats.find((f) => isSrgbFormat(f) === wantSrgb) ?? formats[0];
2707
- const bytes = await encoder.encodeToBytes(source, { flipY });
2708
- const texture = buildCompressedTexture([bytes], format);
2709
- return { ...bytes, texture };
2710
- }
2711
-
2712
- // src/three/transcodeCache.ts
2632
+ // src/transcodeCache.ts
2713
2633
  var DEFAULT_LIMIT = 256 * 1024 * 1024;
2714
2634
  var maxBytes = DEFAULT_LIMIT;
2715
2635
  var totalBytes = 0;
@@ -2782,7 +2702,7 @@ function clearTranscodeCache() {
2782
2702
  totalBytes = 0;
2783
2703
  }
2784
2704
 
2785
- // src/three/compressTexture.ts
2705
+ // src/compressTexture.ts
2786
2706
  var sharedGpuPromise = null;
2787
2707
  var SHARED_DEVICE_FEATURES = [
2788
2708
  "texture-compression-bc",
@@ -2915,18 +2835,7 @@ function bitmapToMipLevel(bitmap, flipY) {
2915
2835
  function mipLevelToImageData(level) {
2916
2836
  return new ImageData(level.data, level.width, level.height);
2917
2837
  }
2918
- function wrapUncompressed(bitmap, srgb, flipY) {
2919
- const tex = new Texture(bitmap);
2920
- tex.colorSpace = srgb ? SRGBColorSpace2 : LinearSRGBColorSpace2;
2921
- tex.magFilter = LinearFilter2;
2922
- tex.minFilter = LinearFilter2;
2923
- tex.wrapS = tex.wrapT = ClampToEdgeWrapping2;
2924
- tex.generateMipmaps = false;
2925
- tex.flipY = flipY;
2926
- tex.needsUpdate = true;
2927
- return tex;
2928
- }
2929
- async function compressTexture(source, options = {}) {
2838
+ async function compressTextureToBytes(source, options = {}) {
2930
2839
  const {
2931
2840
  hint = "color",
2932
2841
  preferredFormat,
@@ -2940,7 +2849,6 @@ async function compressTexture(source, options = {}) {
2940
2849
  device: providedDevice,
2941
2850
  adapter: providedAdapter
2942
2851
  } = options;
2943
- const srgb = colorSpace === "srgb";
2944
2852
  const t0 = performance.now();
2945
2853
  const gpu = await resolveWebGPU();
2946
2854
  const gl = gpu ? null : resolveWebGL();
@@ -2957,9 +2865,9 @@ async function compressTexture(source, options = {}) {
2957
2865
  if (transcodeKey) {
2958
2866
  const hit = readTranscodeCache(transcodeKey);
2959
2867
  if (hit) {
2960
- const tex2 = buildCompressedTexture(hit.levels, hit.format);
2961
2868
  return {
2962
- texture: tex2,
2869
+ levels: hit.levels,
2870
+ fallbackBitmap: null,
2963
2871
  format: hit.format,
2964
2872
  fallbackUncompressed: false,
2965
2873
  backend: gpu ? "webgpu" : "webgl",
@@ -2970,10 +2878,7 @@ async function compressTexture(source, options = {}) {
2970
2878
  encodeMs: 0,
2971
2879
  decodeMs: 0,
2972
2880
  totalMs: performance.now() - t0,
2973
- cacheHit: true,
2974
- destroy: () => {
2975
- tex2.dispose();
2976
- }
2881
+ cacheHit: true
2977
2882
  };
2978
2883
  }
2979
2884
  }
@@ -2985,11 +2890,11 @@ async function compressTexture(source, options = {}) {
2985
2890
  const viaWebGL = gl ? encodeViaWebGL(gl) : null;
2986
2891
  if (viaWebGL) return viaWebGL;
2987
2892
  console.warn(
2988
- "[compressTexture] No compressed path available (WebGPU and WebGL2 both lack a usable compressed-texture format); returning uncompressed RGBA8."
2893
+ "[compressTextureToBytes] No compressed path available (WebGPU and WebGL2 both lack a usable compressed-texture format); returning uncompressed RGBA8."
2989
2894
  );
2990
- const tex = wrapUncompressed(bitmap, srgb, flipY);
2991
2895
  return {
2992
- texture: tex,
2896
+ levels: null,
2897
+ fallbackBitmap: bitmap,
2993
2898
  format: null,
2994
2899
  fallbackUncompressed: true,
2995
2900
  backend: "none",
@@ -3000,10 +2905,7 @@ async function compressTexture(source, options = {}) {
3000
2905
  encodeMs: 0,
3001
2906
  decodeMs,
3002
2907
  totalMs: performance.now() - t0,
3003
- cacheHit: false,
3004
- destroy: () => {
3005
- tex.dispose();
3006
- }
2908
+ cacheHit: false
3007
2909
  };
3008
2910
  async function resolveWebGPU() {
3009
2911
  if (!("gpu" in navigator)) return null;
@@ -3056,7 +2958,6 @@ async function compressTexture(source, options = {}) {
3056
2958
  } else {
3057
2959
  bytes = await encoder.encodeToBytes(bitmap, { flipY });
3058
2960
  }
3059
- const tex3 = buildCompressedTexture([bytes], selection.format);
3060
2961
  if (transcodeKey) {
3061
2962
  writeTranscodeCache(transcodeKey, {
3062
2963
  format: selection.format,
@@ -3065,8 +2966,10 @@ async function compressTexture(source, options = {}) {
3065
2966
  levels: [bytes]
3066
2967
  });
3067
2968
  }
2969
+ destroyEncoder();
3068
2970
  return {
3069
- texture: tex3,
2971
+ levels: [bytes],
2972
+ fallbackBitmap: null,
3070
2973
  format: selection.format,
3071
2974
  fallbackUncompressed: false,
3072
2975
  backend: "webgpu",
@@ -3077,11 +2980,7 @@ async function compressTexture(source, options = {}) {
3077
2980
  encodeMs: bytes.encodeMs,
3078
2981
  decodeMs,
3079
2982
  totalMs: performance.now() - t0,
3080
- cacheHit: false,
3081
- destroy: () => {
3082
- tex3.dispose();
3083
- destroyEncoder();
3084
- }
2983
+ cacheHit: false
3085
2984
  };
3086
2985
  }
3087
2986
  let chainResult;
@@ -3097,7 +2996,6 @@ async function compressTexture(source, options = {}) {
3097
2996
  }
3098
2997
  }
3099
2998
  const { levels, encodeMs } = chainResult;
3100
- const tex2 = buildCompressedTexture(levels, selection.format);
3101
2999
  if (transcodeKey) {
3102
3000
  writeTranscodeCache(transcodeKey, {
3103
3001
  format: selection.format,
@@ -3106,8 +3004,10 @@ async function compressTexture(source, options = {}) {
3106
3004
  levels
3107
3005
  });
3108
3006
  }
3007
+ destroyEncoder();
3109
3008
  return {
3110
- texture: tex2,
3009
+ levels,
3010
+ fallbackBitmap: null,
3111
3011
  format: selection.format,
3112
3012
  fallbackUncompressed: false,
3113
3013
  backend: "webgpu",
@@ -3118,11 +3018,7 @@ async function compressTexture(source, options = {}) {
3118
3018
  encodeMs,
3119
3019
  decodeMs,
3120
3020
  totalMs: performance.now() - t0,
3121
- cacheHit: false,
3122
- destroy: () => {
3123
- tex2.dispose();
3124
- destroyEncoder();
3125
- }
3021
+ cacheHit: false
3126
3022
  };
3127
3023
  } catch (e) {
3128
3024
  destroyEncoder();
@@ -3142,7 +3038,6 @@ async function compressTexture(source, options = {}) {
3142
3038
  try {
3143
3039
  if (!mipmaps) {
3144
3040
  const bytes = encoder.encodeToBytes(bitmap, { flipY });
3145
- const tex3 = buildCompressedTexture([bytes], selection.format);
3146
3041
  if (transcodeKey) {
3147
3042
  writeTranscodeCache(transcodeKey, {
3148
3043
  format: selection.format,
@@ -3151,8 +3046,10 @@ async function compressTexture(source, options = {}) {
3151
3046
  levels: [bytes]
3152
3047
  });
3153
3048
  }
3049
+ encoder.destroy();
3154
3050
  return {
3155
- texture: tex3,
3051
+ levels: [bytes],
3052
+ fallbackBitmap: null,
3156
3053
  format: selection.format,
3157
3054
  fallbackUncompressed: false,
3158
3055
  backend: "webgl",
@@ -3163,11 +3060,7 @@ async function compressTexture(source, options = {}) {
3163
3060
  encodeMs: bytes.encodeMs,
3164
3061
  decodeMs,
3165
3062
  totalMs: performance.now() - t0,
3166
- cacheHit: false,
3167
- destroy: () => {
3168
- tex3.dispose();
3169
- encoder.destroy();
3170
- }
3063
+ cacheHit: false
3171
3064
  };
3172
3065
  }
3173
3066
  const level0 = bitmapToMipLevel(bitmap, flipY);
@@ -3180,7 +3073,6 @@ async function compressTexture(source, options = {}) {
3180
3073
  encodedLevels.push(bytes);
3181
3074
  totalEncodeMs += bytes.encodeMs;
3182
3075
  }
3183
- const tex2 = buildCompressedTexture(encodedLevels, selection.format);
3184
3076
  if (transcodeKey) {
3185
3077
  writeTranscodeCache(transcodeKey, {
3186
3078
  format: selection.format,
@@ -3189,8 +3081,10 @@ async function compressTexture(source, options = {}) {
3189
3081
  levels: encodedLevels
3190
3082
  });
3191
3083
  }
3084
+ encoder.destroy();
3192
3085
  return {
3193
- texture: tex2,
3086
+ levels: encodedLevels,
3087
+ fallbackBitmap: null,
3194
3088
  format: selection.format,
3195
3089
  fallbackUncompressed: false,
3196
3090
  backend: "webgl",
@@ -3201,20 +3095,114 @@ async function compressTexture(source, options = {}) {
3201
3095
  encodeMs: totalEncodeMs,
3202
3096
  decodeMs,
3203
3097
  totalMs: performance.now() - t0,
3204
- cacheHit: false,
3205
- destroy: () => {
3206
- tex2.dispose();
3207
- encoder.destroy();
3208
- }
3098
+ cacheHit: false
3209
3099
  };
3210
3100
  } catch (e) {
3211
3101
  encoder.destroy();
3212
- console.warn("[compressTexture] WebGL fallback encode failed; returning uncompressed RGBA8.", e);
3102
+ console.warn("[compressTextureToBytes] WebGL fallback encode failed; returning uncompressed RGBA8.", e);
3213
3103
  return null;
3214
3104
  }
3215
3105
  }
3216
3106
  }
3217
3107
 
3108
+ // src/three/compressTexture.ts
3109
+ import { ClampToEdgeWrapping as ClampToEdgeWrapping2, LinearFilter as LinearFilter2, LinearSRGBColorSpace as LinearSRGBColorSpace2, SRGBColorSpace as SRGBColorSpace2, Texture } from "three";
3110
+
3111
+ // src/three/buildTexture.ts
3112
+ import {
3113
+ RED_GREEN_RGTC2_Format,
3114
+ RGB_ETC2_Format,
3115
+ RGBA_ASTC_4x4_Format,
3116
+ RGBA_BPTC_Format,
3117
+ RGBA_S3TC_DXT1_Format
3118
+ } from "three";
3119
+
3120
+ // src/three/textureAssembly.ts
3121
+ import {
3122
+ ClampToEdgeWrapping,
3123
+ CompressedTexture,
3124
+ LinearFilter,
3125
+ LinearMipmapLinearFilter,
3126
+ LinearSRGBColorSpace,
3127
+ SRGBColorSpace
3128
+ } from "three";
3129
+ function assembleCompressedTexture(levels, threeFormat, effectiveSrgb) {
3130
+ if (levels.length === 0) {
3131
+ throw new Error("assembleCompressedTexture: no levels provided");
3132
+ }
3133
+ const mipmaps = levels.map((l) => ({
3134
+ data: l.data,
3135
+ width: l.paddedWidth,
3136
+ height: l.paddedHeight
3137
+ }));
3138
+ const base = levels[0];
3139
+ const texture = new CompressedTexture(mipmaps, base.paddedWidth, base.paddedHeight, threeFormat);
3140
+ texture.colorSpace = effectiveSrgb ? SRGBColorSpace : LinearSRGBColorSpace;
3141
+ texture.magFilter = LinearFilter;
3142
+ texture.minFilter = levels.length > 1 ? LinearMipmapLinearFilter : LinearFilter;
3143
+ texture.generateMipmaps = false;
3144
+ texture.wrapS = texture.wrapT = ClampToEdgeWrapping;
3145
+ texture.needsUpdate = true;
3146
+ texture.userData.logicalWidth = base.width;
3147
+ texture.userData.logicalHeight = base.height;
3148
+ texture.userData.mipLevels = levels.length;
3149
+ return texture;
3150
+ }
3151
+
3152
+ // src/three/buildTexture.ts
3153
+ var THREE_FORMAT = {
3154
+ [TextureFormat.BC1]: RGBA_S3TC_DXT1_Format,
3155
+ [TextureFormat.BC1_SRGB]: RGBA_S3TC_DXT1_Format,
3156
+ [TextureFormat.BC5]: RED_GREEN_RGTC2_Format,
3157
+ [TextureFormat.BC7]: RGBA_BPTC_Format,
3158
+ [TextureFormat.BC7_SRGB]: RGBA_BPTC_Format,
3159
+ [TextureFormat.ASTC_4x4]: RGBA_ASTC_4x4_Format,
3160
+ [TextureFormat.ASTC_4x4_SRGB]: RGBA_ASTC_4x4_Format,
3161
+ [TextureFormat.ETC2_RGB8]: RGB_ETC2_Format,
3162
+ [TextureFormat.ETC2_RGB8_SRGB]: RGB_ETC2_Format
3163
+ };
3164
+ var SRGB_FORMATS = /* @__PURE__ */ new Set([
3165
+ TextureFormat.BC1_SRGB,
3166
+ TextureFormat.BC7_SRGB,
3167
+ TextureFormat.ASTC_4x4_SRGB,
3168
+ TextureFormat.ETC2_RGB8_SRGB
3169
+ ]);
3170
+ function isSrgbFormat(format) {
3171
+ return SRGB_FORMATS.has(format);
3172
+ }
3173
+ function threeFormatFor(format) {
3174
+ return THREE_FORMAT[format];
3175
+ }
3176
+ function buildCompressedTexture(levels, format) {
3177
+ return assembleCompressedTexture(levels, threeFormatFor(format), isSrgbFormat(format));
3178
+ }
3179
+ async function encodeToTexture(encoder, source, { colorSpace = "srgb", flipY = false } = {}) {
3180
+ const formats = encoder.constructor.textureFormats;
3181
+ const wantSrgb = colorSpace === "srgb" && encoder.supportsSrgb;
3182
+ const format = formats.find((f) => isSrgbFormat(f) === wantSrgb) ?? formats[0];
3183
+ const bytes = await encoder.encodeToBytes(source, { flipY });
3184
+ const texture = buildCompressedTexture([bytes], format);
3185
+ return { ...bytes, texture };
3186
+ }
3187
+
3188
+ // src/three/compressTexture.ts
3189
+ function wrapUncompressed(bitmap, srgb, flipY) {
3190
+ const tex = new Texture(bitmap);
3191
+ tex.colorSpace = srgb ? SRGBColorSpace2 : LinearSRGBColorSpace2;
3192
+ tex.magFilter = LinearFilter2;
3193
+ tex.minFilter = LinearFilter2;
3194
+ tex.wrapS = tex.wrapT = ClampToEdgeWrapping2;
3195
+ tex.generateMipmaps = false;
3196
+ tex.flipY = flipY;
3197
+ tex.needsUpdate = true;
3198
+ return tex;
3199
+ }
3200
+ async function compressTexture(source, options = {}) {
3201
+ const { levels, fallbackBitmap, ...rest } = await compressTextureToBytes(source, options);
3202
+ const texture = fallbackBitmap !== null ? wrapUncompressed(fallbackBitmap, options.colorSpace !== "linear", options.flipY ?? true) : buildCompressedTexture(levels, rest.format);
3203
+ return { ...rest, texture, destroy: () => texture.dispose() };
3204
+ }
3205
+
3218
3206
  // src/three/GputexLoader.ts
3219
3207
  import { Loader } from "three";
3220
3208
  var GputexLoader = class extends Loader {
@@ -3327,6 +3315,7 @@ export {
3327
3315
  buildCompressedTexture,
3328
3316
  clearTranscodeCache,
3329
3317
  compressTexture,
3318
+ compressTextureToBytes,
3330
3319
  createWebGLContext,
3331
3320
  detectCapabilities,
3332
3321
  detectWebGLCapabilities,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gputex",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "dist"