@ludicon/spark.js
Version:
Real-Time GPU Texture Codecs for the Web
1,133 lines (1,132 loc) • 32.8 kB
JavaScript
const V = /* @__PURE__ */ Object.assign({ "./spark_astc_rgb.wgsl": () => import("./spark_astc_rgb-DaSIMKXW.js"), "./spark_astc_rgba.wgsl": () => import("./spark_astc_rgba-BToA2Rcq.js"), "./spark_bc1_rgb.wgsl": () => import("./spark_bc1_rgb-DZwuM1tO.js"), "./spark_bc4_r.wgsl": () => import("./spark_bc4_r-DjThizCH.js"), "./spark_bc5_rg.wgsl": () => import("./spark_bc5_rg-6bO0Gvo9.js"), "./spark_bc7_rgb.wgsl": () => import("./spark_bc7_rgb-FXpBw9fE.js"), "./spark_bc7_rgba.wgsl": () => import("./spark_bc7_rgba-C8Hi2pUY.js"), "./spark_eac_r.wgsl": () => import("./spark_eac_r-D8HGiglc.js"), "./spark_eac_rg.wgsl": () => import("./spark_eac_rg-DvsrLP9h.js"), "./spark_etc2_rgb.wgsl": () => import("./spark_etc2_rgb-C-j5FZpn.js"), "./utils.wgsl": () => import("./utils-BigAnOxg.js") }), I = Object.fromEntries(
Object.entries(V).map(([c, e]) => [c.replace("./", ""), async () => (await e()).default])
), i = {
ASTC_4x4_RGB: 0,
ASTC_4x4_RGBA: 1,
// ASTC_4x4_RGBM: 2,
// ASTC_6x6_RGB: 3,
EAC_R: 4,
EAC_RG: 5,
ETC2_RGB: 6,
// ETC2_RGBA: 7,
// ETC2_RGBM: 8,
BC1_RGB: 9,
// BC3_RGBA: 10,
// BC3_YCoCg: 11,
// BC3_RGBM: 12,
BC4_R: 13,
BC5_RG: 14,
// BC6H_RGB: 15,
BC7_RGB: 16,
BC7_RGBA: 17
}, G = [
/* 0 */
"astc-4x4-rgb",
// ASTC_4x4_RGB
/* 1 */
"astc-4x4-rgba",
// ASTC_4x4_RGBA
/* 2 */
null,
/* 3 */
null,
/* 4 */
"eac-r",
// EAC_R
/* 5 */
"eac-rg",
// EAC_RG
/* 6 */
"etc2-rgb",
// ETC2_RGB
/* 7 */
null,
/* 8 */
null,
/* 9 */
"bc1-rgb",
// BC1_RGB
/* 10 */
null,
/* 11 */
null,
/* 12 */
null,
/* 13 */
"bc4-r",
// BC4_R
/* 14 */
"bc5-rg",
// BC5_RG
/* 15 */
null,
/* 16 */
"bc7-rgb",
// BC7_RGB
/* 17 */
"bc7-rgba"
// BC7_RGBA
], q = [
/* 0 */
"spark_astc_rgb.wgsl",
// ASTC_4x4_RGB
/* 1 */
"spark_astc_rgba.wgsl",
// ASTC_4x4_RGBA
/* 2 */
null,
/* 3 */
null,
/* 4 */
"spark_eac_r.wgsl",
// EAC_R
/* 5 */
"spark_eac_rg.wgsl",
// EAC_RG
/* 6 */
"spark_etc2_rgb.wgsl",
// ETC2_RGB
/* 7 */
null,
/* 8 */
null,
/* 9 */
"spark_bc1_rgb.wgsl",
// BC1_RGB
/* 10 */
null,
/* 11 */
null,
/* 12 */
null,
/* 13 */
"spark_bc4_r.wgsl",
// BC4_R
/* 14 */
"spark_bc5_rg.wgsl",
// BC5_RG
/* 15 */
null,
/* 16 */
"spark_bc7_rgb.wgsl",
// BC7_RGB
/* 17 */
"spark_bc7_rgba.wgsl"
// BC7_RGBA
], W = [
/* 0 */
16,
// ASTC_4x4_RGB
/* 1 */
16,
// ASTC_4x4_RGBA
/* 2 */
0,
/* 3 */
0,
/* 4 */
8,
// EAC_R
/* 5 */
16,
// EAC_RG
/* 6 */
8,
// ETC2_RGB
/* 7 */
0,
/* 8 */
0,
/* 9 */
8,
// BC1_RGB
/* 10 */
0,
/* 11 */
0,
/* 12 */
0,
/* 13 */
8,
// BC4_R
/* 14 */
16,
// BC5_RG
/* 15 */
0,
/* 16 */
16,
// BC7_RGB
/* 17 */
16
// BC7_RGB
], Y = [
/* 0 */
4,
// ASTC_4x4_RGB
/* 1 */
4,
// ASTC_4x4_RGBA
/* 2 */
0,
/* 3 */
0,
/* 4 */
2,
// EAC_R
/* 5 */
2,
// EAC_RG
/* 6 */
8,
// ETC2_RGB
/* 7 */
0,
/* 8 */
0,
/* 9 */
8,
// BC1_RGB
/* 10 */
0,
/* 11 */
0,
/* 12 */
0,
/* 13 */
2,
// BC4_R
/* 14 */
2,
// BC5_RG
/* 15 */
0,
/* 16 */
4,
// BC7_RGB
/* 17 */
4
// BC7_RGB
], P = Object.freeze({
"astc-4x4-rgb": i.ASTC_4x4_RGB,
"astc-4x4-rgba": i.ASTC_4x4_RGBA,
"eac-r": i.EAC_R,
"eac-rg": i.EAC_RG,
"etc2-rgb": i.ETC2_RGB,
"bc1-rgb": i.BC1_RGB,
"bc4-r": i.BC4_R,
"bc5-rg": i.BC5_RG,
"bc7-rgb": i.BC7_RGB,
"bc7-rgba": i.BC7_RGBA,
// aliases:
"astc-rgb": i.ASTC_4x4_RGB,
"astc-rgba": i.ASTC_4x4_RGBA,
// webgpu aliases:
"bc1-rgba-unorm": i.BC1_RGB,
"bc1-rgba-unorm-srgb": i.BC1_RGB,
"bc4-r-unorm": i.BC4_R,
"bc5-rg-unorm": i.BC5_RG,
"bc7-rgba-unorm": i.BC7_RGBA,
"bc7-rgba-unorm-srgb": i.BC7_RGBA,
"etc2-rgb8unorm": i.ETC2_RGB,
"etc2-rgb8unorm-srgb": i.ETC2_RGB,
"eac-r11unorm": i.EAC_R,
"eac-rg11unorm": i.EAC_RG,
"astc-4x4-unorm": i.ASTC_4x4_RGBA,
"astc-4x4-unorm-srgb": i.ASTC_4x4_RGBA
}), $ = [
/* 0 */
"astc-4x4-unorm",
// ASTC_4x4_RGB
/* 1 */
"astc-4x4-unorm",
// ASTC_4x4_RGBA
/* 2 */
null,
/* 3 */
null,
/* 4 */
"eac-r11unorm",
// EAC_R
/* 5 */
"eac-rg11unorm",
// EAC_RG
/* 6 */
"etc2-rgb8unorm",
// ETC2_RGB
/* 7 */
null,
/* 8 */
null,
/* 9 */
"bc1-rgba-unorm",
// BC1_RGB
/* 10 */
null,
/* 11 */
null,
/* 12 */
null,
/* 13 */
"bc4-r-unorm",
// BC4_R
/* 14 */
"bc5-rg-unorm",
// BC5_RG
/* 15 */
null,
/* 16 */
"bc7-rgba-unorm",
// BC7_RGB
/* 17 */
"bc7-rgba-unorm"
// BC7_RGB
], X = [
/* 0 */
!0,
// ASTC_4x4_RGB
/* 1 */
!0,
// ASTC_4x4_RGBA
/* 2 */
null,
/* 3 */
null,
/* 4 */
!1,
// EAC_R
/* 5 */
!1,
// EAC_RG
/* 6 */
!0,
// ETC2_RGB
/* 7 */
null,
/* 8 */
null,
/* 9 */
!0,
// BC1_RGB
/* 10 */
null,
/* 11 */
null,
/* 12 */
null,
/* 13 */
!1,
// BC4_R
/* 14 */
!1,
// BC5_RG
/* 15 */
null,
/* 16 */
!0,
// BC7_RGB
/* 17 */
!0
// BC7_RGB
];
function w(c, e) {
if (!c)
throw new Error(e);
}
function H(c) {
return typeof GPUDevice < "u" && c instanceof GPUDevice;
}
function j() {
return ["iPad Simulator", "iPhone Simulator", "iPod Simulator", "iPad", "iPhone", "iPod"].includes(navigator.platform) || // iPad on iOS 13 detection
navigator.userAgent.includes("Mac") && "ontouchend" in document;
}
function O() {
const c = navigator.userAgent.match(/Safari\/(\d+(\.\d+)?)/);
return c && parseFloat(c[1]);
}
function k() {
const c = navigator.userAgent.match(/Firefox\/(\d+(\.\d+)?)/);
return c && parseFloat(c[1]);
}
function Q(c) {
const e = /* @__PURE__ */ new Set(), r = {
"texture-compression-bc": [
i.BC1_RGB,
i.BC4_R,
i.BC5_RG,
i.BC7_RGB,
i.BC7_RGBA
],
"texture-compression-etc2": [i.ETC2_RGB, i.EAC_R, i.EAC_RG],
"texture-compression-astc": [i.ASTC_4x4_RGB, i.ASTC_4x4_RGBA]
};
for (const [t, s] of Object.entries(r))
if (c.features.has(t))
for (const n of s)
e.add(n);
return e;
}
function Z(c) {
const e = document.createElement("canvas");
e.width = c.width, e.height = c.height;
const r = e.getContext("2d");
r.drawImage(c, 0, 0);
const t = r.getImageData(0, 0, c.width, c.height);
return new Uint8Array(t.data.buffer);
}
function J(c) {
return /\.svg(?:$|\?)/i.test(c) || /^data:image\/svg\+xml[,;]/i.test(c);
}
function K(c) {
return new Promise((e, r) => {
const t = new Image();
t.crossOrigin = "anonymous", t.decoding = "async", t.onload = () => e(t), t.onerror = r, t.src = c;
});
}
async function ee(c, e = {}) {
const r = await fetch(c, { mode: "cors" });
if (!r.ok) throw new Error(`HTTP ${r.status} for ${c}`);
const t = await r.blob();
return createImageBitmap(t, {
imageOrientation: e.flipY ? "flipY" : "none",
colorSpaceConversion: e.colorSpaceConversion ?? "none",
premultiplyAlpha: "none"
});
}
const te = O();
function N(c) {
return J(c) || te ? K(c) : ee(c);
}
const L = 256, F = 4;
function re(c, e, r, t) {
let s = 0, n = 0;
const a = [];
do {
const o = Math.ceil(c / 4), u = Math.ceil(e / 4), l = Math.ceil(o * r / L) * L, f = u * l;
s++, a.push({ offset: n, alignedSize: f, w: c, h: e, bw: o, bh: u, bytesPerRow: l }), n += f, c = Math.max(1, Math.floor(c / 2)), e = Math.max(1, Math.floor(e / 2));
} while (t && (c >= F || e >= F));
return { mipmapCount: s, outputSize: n, bufferRanges: a };
}
class D {
#e;
#f;
#l = [];
#g;
#a = !1;
#s;
#o;
#u;
#m;
#i;
#n = new Array(3);
#r;
#p;
#c;
#G = 0;
/**
* Initialize the encoder by detecting available compression formats.
* @param {GPUDevice} device - WebGPU device.
* @param {Object} options - Encoder options.
* @param {boolean} options.preload - Whether to preload all encoder pipelines (false by default).
* @returns {Promise<void>} Resolves when initialization is complete.
*/
static async create(e, r = {}) {
const t = new D();
return await t.#B(e, r.preload ?? !1, r.useTimestampQueries ?? !1), t;
}
/**
* Returns a list of supported texture compression format names.
*
* This function checks a predefined list of common GPU compression formats
* (ASTC, ETC2, EAC, BCn) and filters it based on the formats actually supported
* by the current device as determined by `Spark.supportedFormats`.
*
* @returns {string[]} An array of format names (e.g., "bc1-rgb", "astc-4x4-rgba") that are supported on the current platform.
*
* @example
* const spark = await Spark.create(device);
* const formats = spark.enumerateSupportedFormats();
* console.log("Supported formats:", formats);
*/
enumerateSupportedFormats() {
const e = [
"astc-4x4-rgb",
"astc-4x4-rgba",
"eac-r",
"eac-rg",
"etc2-rgb",
"bc1-rgb",
"bc4-r",
"bc5-rg",
"bc7-rgb",
"bc7-rgba"
], r = [];
for (const t of e) {
const s = P[t];
if (this.#t(s)) {
const n = Y[s];
r.push({ format: t, ratio: n });
}
}
return r;
}
/**
* Determines the set of WebGPU features to request when initializing the device.
*
* This function inspects the given `adapter` to see which texture compression and shader
* features are available, and returns a list of those that are both supported and safe to enable.
*
* @param {GPUAdapter} adapter - The WebGPU adapter returned from `navigator.gpu.requestAdapter()`.
* @returns {string[]} An array of WebGPU feature names to request during `adapter.requestDevice()`.
*
* @example
* // Create device using the features required by spark.js
* const adapter = await navigator.gpu.requestAdapter()
* const requiredFeatures = Spark.getRequiredFeatures(adapter)
* const device = await adapter.requestDevice({ requiredFeatures })
*
* // Create spark object for the given device.
* const spark = Spark.create(device)
*/
static getRequiredFeatures(e) {
const r = [];
return !j() && e.features.has("texture-compression-bc") && r.push("texture-compression-bc"), e.features.has("texture-compression-etc2") && r.push("texture-compression-etc2"), e.features.has("texture-compression-astc") && r.push("texture-compression-astc"), e.features.has("shader-f16") && r.push("shader-f16"), e.features.has("timestamp-query") && r.push("timestamp-query"), r;
}
/**
* Try to determine the best compression options automatically. Do not use this in production, this is
* for the convenience of the spark.js image viewer only.
*
* @param {string | HTMLImageElement | ImageBitmap | GPUTexture} source - Image input.
* @param {Object} options - Encoding options.
* @returns {Object} - Recommended encoding options with an explicit encoding format.
*/
async selectPreferredOptions(e, r = {}) {
if (r.format == null || r.format == "auto") {
const t = e instanceof Image || e instanceof ImageBitmap || e instanceof GPUTexture ? e : await N(e);
r.format = "auto";
const s = await this.#d(r, t);
r.format = G[s], t instanceof GPUTexture && t.format.endsWith("-srgb") && (r.srgb = !0), (s == i.EAC_RG || s == i.BC5_RG) && (r.normal = !0);
}
return r;
}
/**
* Load an image and encode it to a compressed GPU texture.
*
* @param {string | HTMLImageElement | ImageBitmap | GPUTexture} source
* The image to encode. Can be a GPUTexture, URL, DOM image or ImageBitmap.
*
* @param {Object} [options] - Optional configuration for encoding.
*
* @param {string} [options.format="rgb"]
* Desired block compression format. Can be specified in several ways:
* - A channel mask indicating the number of channels in your input:
* "rgba", "rgb", "rg", or "r". The actual GPU format is selected
* based on device capabilities.
* - An explicit WebGPU BC, ETC, or ASTC format name, or an abbreviated
* form such as "bc7" or "astc". Note: only 4x4 LDR formats are supported.
* - "auto" to analyze the input texture and detect the required channels.
* This has some overhead, so specifying a format explicitly is preferred.
*
* @param {boolean} [options.alpha]
* Hint for the automatic format selector. When no explicit format is provided,
* the format is assumed to be "rgb". Supplying `alpha: true` will favor RGBA formats.
*
* @param {boolean} [options.mips=false] | [options.generateMipmaps=false]
* Whether to generate mipmaps. Mipmaps are generated with a basic box filter
* in linear space.
*
* @param {boolean} [options.srgb=false]
* Whether to encode the image in an sRGB format. Also affects mipmap generation.
* The `srgb` mode can also be inferred from the `format`.
*
* @param {boolean} [options.normal=false]
* Interpret the image as a normal map. Affects automatic format selection,
* favoring "bc5" and "eac-rg" formats.
*
* @param {boolean} [options.flipY=false]
* Whether to vertically flip the image before encoding.
*
* @returns {Promise<GPUTexture>} A promise resolving to the encoded GPU texture.
*/
async encodeTexture(e, r = {}) {
w(this.#e, "Spark is not initialized");
const t = e instanceof Image || e instanceof ImageBitmap || e instanceof GPUTexture ? e : await N(e);
console.log("Loaded image", t);
const s = await this.#d(r, t), n = this.#h(s), a = Math.ceil(t.width / 4) * 4, o = Math.ceil(t.height / 4) * 4, u = W[s], l = r.generateMipmaps || r.mips, { mipmapCount: f, outputSize: h, bufferRanges: m } = re(a, o, u, l), b = (r.srgb || r.format?.endsWith("srgb")) && X[s], _ = b ? 1 : r.normal ? 2 : 0, B = $[s] + (b ? "-srgb" : ""), A = b ? ["rgba8unorm", "rgba8unorm-srgb"] : ["rgba8unorm"], x = this.#G++;
console.time("create input texture #" + x);
let C = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST;
this.#a ? C |= GPUTextureUsage.RENDER_ATTACHMENT : C |= GPUTextureUsage.STORAGE_BINDING;
const y = r.flipY || a != t.width || o != t.height;
!y && !(t instanceof GPUTexture) && (C |= GPUTextureUsage.RENDER_ATTACHMENT);
const p = this.#e.createCommandEncoder();
p.pushDebugGroup?.("spark process texture"), this.#r && typeof p.writeTimestamp == "function" && p.writeTimestamp(this.#r, 0);
let d;
(y || !(t instanceof GPUTexture && !l)) && (d = this.#e.createTexture({
size: [a, o, 1],
mipLevelCount: f,
format: "rgba8unorm",
usage: C,
viewFormats: A
}));
let R;
y ? t instanceof GPUTexture ? this.#_(p, t, d, a, o, b, r.flipY) : (R = this.#e.createTexture({
size: [t.width, t.height, 1],
mipLevelCount: 1,
format: "rgba8unorm",
// RENDER_ATTACHMENT usage is necessary for copyExternalImageToTexture
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
viewFormats: A
}), this.#e.queue.copyExternalImageToTexture(
{ source: t },
{ texture: R },
{ width: t.width, height: t.height }
), this.#_(p, R, d, a, o, _, r.flipY)) : t instanceof GPUTexture ? l ? p.copyTextureToTexture({ texture: t }, { texture: d }, { width: a, height: o }) : d = t : this.#e.queue.copyExternalImageToTexture({ source: t }, { texture: d }, { width: a, height: o }), l && this.#w(p, d, f, a, o, _), p.popDebugGroup?.(), console.timeEnd("create input texture #" + x);
const E = this.#e.createTexture({
size: [a, o, 1],
mipLevelCount: f,
format: B,
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST
}), U = this.#e.createBuffer({
size: h,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
}), S = `dispatch compute shader '${G[s]}' #${x}`;
console.time(S), p.pushDebugGroup?.("spark encode texture");
let M = {};
this.#r && typeof p.writeTimestamp != "function" && (M = {
writeTimestamps: {
querySet: this.#r,
beginningOfPassWriteIndex: 0,
endOfPassWriteIndex: 1
}
});
const v = await n, T = p.beginComputePass(M);
T.setPipeline(v);
for (let g = 0; g < f; g++) {
const z = this.#e.createBindGroup({
layout: v.getBindGroupLayout(0),
entries: [
{
binding: 0,
resource: d.createView({
baseMipLevel: g,
mipLevelCount: 1
})
},
{
binding: 1,
resource: this.#i
},
{
binding: 2,
resource: {
buffer: U,
offset: m[g].offset,
size: m[g].size
}
}
]
});
T.setBindGroup(0, z), T.dispatchWorkgroups(Math.ceil(m[g].bw / 16), Math.ceil(m[g].bh / 16));
}
T.end();
for (let g = 0; g < f; g++)
p.copyBufferToTexture(
{
buffer: U,
offset: m[g].offset,
bytesPerRow: m[g].bytesPerRow,
rowsPerImage: m[g].bh
},
{
texture: E,
mipLevel: g
},
{
width: m[g].bw * 4,
height: m[g].bh * 4,
depthOrArrayLayers: 1
}
);
return this.#r && typeof p.writeTimestamp == "function" && p.writeTimestamp(this.#r, 1), p.popDebugGroup?.(), this.#e.queue.submit([p.finish()]), console.timeEnd(S), R?.destroy(), d != t && d?.destroy(), U?.destroy(), E;
}
/**
* Returns the time (in milliseconds) it took to perform the most recent `encodeTexture()` call.
*
* This function resolves GPU timestamp queries that were recorded before and after the
* compression dispatch in `encodeTexture()`. It waits for the GPU to finish processing,
* reads back the timestamps, and computes the elapsed GPU time.
*
* Must be called *after* `encodeTexture()` has been invoked and submitted.
*
* @async
* @returns {Promise<number>} Elapsed GPU time in milliseconds.
*
* @example
* await spark.encodeTexture(...);
* const elapsed = await spark.getTimeElapsed();
* console.log(`Encode took ${elapsed.toFixed(2)} ms`);
*
* @throws {Error} If the GPU work has not been submitted, or if timestamp queries fail.
*/
async getTimeElapsed() {
if (!this.#r)
return 0;
const e = this.#e.createCommandEncoder();
e.resolveQuerySet(this.#r, 0, 2, this.#p, 0), e.copyBufferToBuffer(this.#p, 0, this.#c, 0, 16), this.#e.queue.submit([e.finish()]), await this.#e.queue.onSubmittedWorkDone(), await this.#c.mapAsync(GPUMapMode.READ);
const r = this.#c.getMappedRange(), t = new BigUint64Array(r), s = t[0], n = t[1];
return this.#c.unmap(), Number(n - s) / 1e6;
}
async #B(e, r, t) {
w(e, "device is required"), w(H(e), "device is not a WebGPU device"), this.#e = e, this.#f = Q(this.#e), this.#i = this.#e.createSampler({
magFilter: "linear",
minFilter: "linear"
});
for (let s = 0; s < 3; s++)
this.#n[s] = this.#e.createBuffer({
size: 4,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
}), this.#e.queue.writeBuffer(this.#n[s], 0, new Uint32Array([s]));
if (t && this.#e.features.has("timestamp-query")) {
const s = O(), n = k();
(!s || s >= 26) && !n && (this.#r = this.#e.createQuerySet({
type: "timestamp",
count: 2
}), this.#p = this.#e.createBuffer({
size: 16,
// 2 timestamps × 8 bytes each
usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.QUERY_RESOLVE
}), this.#c = this.#e.createBuffer({
size: 16,
// 2 timestamps × 8 bytes each
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
}));
}
if (this.#g = this.#e.features.has("shader-f16"), await this.#C(), r) {
let s;
Array.isArray(r) ? s = r.map((n) => this.#b(n, !1)) : s = this.#f;
for (const n of s)
n !== void 0 && !this.#l[n] && this.#h(n).catch((a) => {
console.error(`Failed to preload pipeline for format ${n}:`, a);
});
}
}
async #C() {
const e = this.#e.createShaderModule({
code: await I["utils.wgsl"](),
label: "utils"
});
if (typeof e.compilationInfo == "function") {
const t = await e.compilationInfo();
if (t.messages.some((s) => s.type == "error")) {
console.error("WGSL compilation errors:");
for (const s of t.messages)
console.error(s);
throw new Error("Shader compilation failed");
}
}
if (k() && (this.#a = !0), this.#a) {
this.#s = {}, this.#o = {}, this.#u = {};
const t = ["rgba8unorm-srgb", "rgba8unorm"];
for (const s of t)
this.#s[s] = this.#e.createRenderPipeline({
label: `mipmap-pipeline-${s}`,
layout: "auto",
vertex: {
module: e,
entryPoint: "fullscreen_vs"
},
fragment: {
module: e,
entryPoint: "mipmap_fs",
targets: [{ format: s }]
},
primitive: {
topology: "triangle-strip",
stripIndexFormat: "uint32"
}
}), this.#o[s] = this.#e.createRenderPipeline({
label: `resize-pipeline-${s}`,
layout: "auto",
vertex: {
module: e,
entryPoint: "fullscreen_vs"
},
fragment: {
module: e,
entryPoint: "resize_fs",
targets: [{ format: s }]
},
primitive: {
topology: "triangle-strip",
stripIndexFormat: "uint32"
}
}), this.#u[s] = this.#e.createRenderPipeline({
label: `flip-y-pipeline-${s}`,
layout: "auto",
vertex: {
module: e,
entryPoint: "fullscreen_vs"
},
fragment: {
module: e,
entryPoint: "flipy_fs",
targets: [{ format: s }]
},
primitive: {
topology: "triangle-strip",
stripIndexFormat: "uint32"
}
});
} else
this.#s = this.#e.createComputePipeline({
layout: "auto",
compute: {
module: e,
entryPoint: "mipmap"
}
}), this.#o = this.#e.createComputePipeline({
layout: "auto",
compute: {
module: e,
entryPoint: "resize"
}
}), this.#u = this.#e.createComputePipeline({
layout: "auto",
compute: {
module: e,
entryPoint: "flipy"
}
});
this.#m = await this.#e.createComputePipelineAsync({
layout: "auto",
compute: {
module: e,
entryPoint: "detect_channel_count"
}
});
}
async #h(e) {
if (this.#l[e])
return this.#l[e];
const r = (async () => {
console.time("loadPipeline " + G[e]);
const t = q[e];
w(t, `No shader available for format ${G[e]}`);
let s = await I[t]();
this.#g || (s = s.replace(/^enable f16;\s*/m, "").replace(/\bf16\b/g, "f32").replace(/\bvec([234])h\b/g, "vec$1f").replace(/\bmat([234]x[234])h/g, "mat$1f").replace(/\b(\d*\.\d+|\d+\.)h\b/g, "$1"));
const n = this.#e.createShaderModule({
code: s,
label: G[e]
});
if (typeof n.getCompilationInfo == "function") {
const o = await n.getCompilationInfo();
if (o.messages.some((u) => u.type == "error")) {
console.error("WGSL compilation errors:");
for (const u of o.messages)
console.error(u);
throw new Error("Shader compilation failed");
}
}
const a = await this.#e.createComputePipelineAsync({
layout: "auto",
compute: {
module: n,
entryPoint: "main"
}
});
return console.timeEnd("loadPipeline " + G[e]), a;
})();
return this.#l[e] = r, r;
}
#t(e) {
return this.#f.has(e);
}
#b(e, r) {
const t = P[e];
if (t != null && this.#t(t))
return t;
const s = r ? [
"bc4-r",
"eac-r",
"bc5-rg",
"eac-rg",
"bc1-rgb",
"etc2-rgb",
"bc7-rgb",
"astc-rgb",
"astc-4x4-rgb",
"bc7-rgba",
"astc-rgba",
"astc-4x4-rgba"
] : [
"bc4-r",
"eac-r",
"bc5-rg",
"eac-rg",
"bc7-rgb",
"bc1-rgb",
"astc-rgb",
"astc-4x4-rgb",
"etc2-rgb",
"bc7-rgba",
"astc-rgba",
"astc-4x4-rgba"
];
for (const n of s)
if (n.includes(e) && this.#t(P[n]))
return P[n];
}
async #d(e, r) {
if (e.format == null)
e.format = "rgb";
else if (e.format == "auto") {
if (e.alpha) {
if (this.#t(i.BC7_RGBA)) return i.BC7_RGBA;
if (this.#t(i.ASTC_4x4_RGBA)) return i.ASTC_4x4_RGBA;
} else if (e.srgb) {
if (this.#t(i.BC7_RGB)) return i.BC7_RGB;
if (this.#t(i.ASTC_4x4_RGB)) return i.ASTC_4x4_RGB;
if (this.#t(i.BC1_RGB)) return i.BC1_RGB;
if (this.#t(i.ETC2_RGB)) return i.ETC2_RGB;
} else if (e.normal) {
if (this.#t(i.BC5_RG)) return i.BC5_RG;
if (this.#t(i.EAC_RG)) return i.EAC_RG;
} else {
let s;
if (r instanceof GPUTexture)
r.format == "r8unorm" || r.format == "r16unorm" ? s = 1 : r.format == "rg8unorm" || r.format == "rg16unorm" ? s = 2 : s = await this.#T(r);
else {
const n = Z(r);
s = this.#R(n);
}
if (s == 4) {
if (this.#t(i.BC7_RGBA)) return i.BC7_RGBA;
if (this.#t(i.ASTC_4x4_RGBA)) return i.ASTC_4x4_RGBA;
} else if (s == 3) {
if (this.#t(i.BC7_RGB)) return i.BC7_RGB;
if (this.#t(i.ASTC_4x4_RGB)) return i.ASTC_4x4_RGB;
if (this.#t(i.BC1_RGB)) return i.BC1_RGB;
if (this.#t(i.ETC2_RGB)) return i.ETC2_RGB;
} else if (s == 2) {
if (this.#t(i.BC5_RG)) return i.BC5_RG;
if (this.#t(i.EAC_RG)) return i.EAC_RG;
} else if (s == 1) {
if (this.#t(i.BC4_R)) return i.BC4_R;
if (this.#t(i.EAC_R)) return i.EAC_R;
}
}
throw new Error("No supported format found.");
}
const t = this.#b(e.format, e.preferLowQuality);
if (t === void 0)
throw new Error(`Unsupported format: ${e.format}`);
return t;
}
#R(e) {
let r = !0, t = !0, s = 0;
const n = Math.min(1024 * 128, e.length);
for (let a = 0; a < n; a += 4) {
const o = e[a] / 255, u = e[a + 1] / 255, l = e[a + 2] / 255;
e[a + 3] < 255 && (r = !1), (o != u || u != l) && (t = !1);
const h = 2 * o - 1, m = 2 * u - 1, b = 2 * l - 1, _ = h * h + m * m + b * b, B = Math.sqrt(_);
(Math.abs(B - 1) > 0.2 || b < -0.1) && (s += 1);
}
return r ? t ? 1 : 16 * s < n ? 2 : 3 : 4;
}
async #T(e) {
const t = this.#e.createBuffer({
size: 12,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
}), s = this.#e.createBuffer({
size: 12,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
}), n = this.#e.createBindGroup({
layout: this.#m.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: e.createView() },
{ binding: 1, resource: { buffer: t } }
]
}), a = this.#e.createCommandEncoder(), o = a.beginComputePass();
o.setPipeline(this.#m), o.setBindGroup(0, n);
const { width: u, height: l } = e, f = Math.ceil(u / 8), h = Math.ceil(l / 8);
o.dispatchWorkgroups(f, h), o.end(), a.copyBufferToBuffer(t, 0, s, 0, 12), this.#e.queue.submit([a.finish()]), await this.#e.queue.onSubmittedWorkDone(), await s.mapAsync(GPUMapMode.READ);
const m = new Uint32Array(s.getMappedRange()), b = m[0] == 0, _ = m[1] == 0, B = m[2];
return s.unmap(), s.destroy(), t.destroy(), b ? _ ? 1 : 4 * B < u * l ? 2 : 3 : 4;
}
// Apply scaling and flipY transform.
#_(e, r, t, s, n, a, o) {
if (this.#a) {
this.#P(e, r, t, s, n, a, o);
return;
}
const u = e.beginComputePass(), l = o ? this.#u : this.#o;
u.setPipeline(l);
const f = this.#e.createBindGroup({
layout: l.getBindGroupLayout(0),
entries: [
{
binding: 0,
resource: r.createView({
baseMipLevel: 0,
mipLevelCount: 1,
format: a == 1 ? "rgba8unorm-srgb" : "rgba8unorm",
usage: GPUTextureUsage.TEXTURE_BINDING
})
},
{
binding: 1,
resource: t.createView({
baseMipLevel: 0,
mipLevelCount: 1,
dimension: "2d",
format: "rgba8unorm",
usage: GPUTextureUsage.STORAGE_BINDING
})
},
{
binding: 2,
resource: this.#i
},
{
binding: 3,
resource: { buffer: this.#n[a] }
}
]
});
u.setBindGroup(0, f), u.dispatchWorkgroups(Math.ceil(s / 8), Math.ceil(n / 8)), u.end();
}
// Apply scaling and flipY transform.
#P(e, r, t, s, n, a, o) {
const u = a == 1 ? "rgba8unorm-srgb" : "rgba8unorm", l = t.createView({
baseMipLevel: 0,
mipLevelCount: 1,
dimension: "2d",
format: u,
usage: GPUTextureUsage.RENDER_ATTACHMENT
}), f = e.beginRenderPass({
colorAttachments: [
{
view: l,
loadOp: "clear",
storeOp: "store",
clearValue: [0, 0, 0, 0]
}
]
}), h = o ? this.#u[u] : this.#o[u];
f.setPipeline(h);
const m = this.#e.createBindGroup({
layout: h.getBindGroupLayout(0),
entries: [
{
binding: 0,
resource: r.createView({
baseMipLevel: 0,
mipLevelCount: 1,
format: a == 1 ? "rgba8unorm-srgb" : "rgba8unorm",
usage: GPUTextureUsage.TEXTURE_BINDING
})
},
{
binding: 2,
resource: this.#i
},
{
binding: 3,
resource: { buffer: this.#n[a] }
}
]
});
f.setBindGroup(0, m), f.draw(4, 1, 0, 0), f.end();
}
async #w(e, r, t, s, n, a) {
let o = s, u = n;
if (this.#a)
for (let l = 0; l < t - 1; l++)
o = Math.max(1, Math.floor(o / 2)), u = Math.max(1, Math.floor(u / 2)), this.#y(e, r, l, l + 1, o, u, a);
else {
const l = e.beginComputePass();
l.setPipeline(this.#s);
for (let f = 0; f < t - 1; f++)
o = Math.max(1, Math.floor(o / 2)), u = Math.max(1, Math.floor(u / 2)), this.#x(l, r, f, f + 1, o, u, a);
l.end();
}
}
#x(e, r, t, s, n, a, o) {
const u = this.#e.createBindGroup({
layout: this.#s.getBindGroupLayout(0),
entries: [
{
binding: 0,
resource: r.createView({
baseMipLevel: t,
mipLevelCount: 1,
format: o == 1 ? "rgba8unorm-srgb" : "rgba8unorm",
usage: GPUTextureUsage.TEXTURE_BINDING
})
},
{
binding: 1,
resource: r.createView({
baseMipLevel: s,
mipLevelCount: 1,
dimension: "2d",
format: "rgba8unorm",
usage: GPUTextureUsage.STORAGE_BINDING
})
},
{
binding: 2,
resource: this.#i
},
{
binding: 3,
resource: { buffer: this.#n[o] }
}
]
});
e.setBindGroup(0, u), e.dispatchWorkgroups(Math.ceil(n / 8), Math.ceil(a / 8));
}
#y(e, r, t, s, n, a, o) {
const u = o == 1 ? "rgba8unorm-srgb" : "rgba8unorm", l = r.createView({
baseMipLevel: s,
mipLevelCount: 1,
dimension: "2d",
format: u,
usage: GPUTextureUsage.RENDER_ATTACHMENT
}), f = e.beginRenderPass({
colorAttachments: [
{
view: l,
loadOp: "clear",
storeOp: "store",
clearValue: [0, 0, 0, 0]
}
]
}), h = this.#e.createBindGroup({
layout: this.#s[u].getBindGroupLayout(0),
entries: [
{
binding: 0,
resource: r.createView({
baseMipLevel: t,
mipLevelCount: 1,
format: u,
usage: GPUTextureUsage.TEXTURE_BINDING
})
},
{
binding: 2,
resource: this.#i
},
{
binding: 3,
resource: { buffer: this.#n[o] }
}
]
});
f.setPipeline(this.#s[u]), f.setBindGroup(0, h), f.draw(4, 1, 0, 0), f.end();
}
}
export {
D as Spark
};