@ludicon/spark.js 0.0.2 → 0.0.4

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # spark.js⚡️
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/@ludicon/spark.js.svg)](https://www.npmjs.com/package/@ludicon/spark.js) [![WebGPU](https://img.shields.io/badge/WebGPU-supported-green.svg)](https://developer.mozilla.org/en-US/docs/Web/API/WebGPU_API)
3
+ [![npm version](https://img.shields.io/npm/v/@ludicon/spark.js.svg)](https://www.npmjs.com/package/@ludicon/spark.js) [![install size](https://packagephobia.com/badge?p=@ludicon/spark.js)](https://packagephobia.com/result?p=@ludicon/spark.js) [![WebGPU](https://img.shields.io/badge/WebGPU-supported-green.svg)](https://developer.mozilla.org/en-US/docs/Web/API/WebGPU_API)
4
4
 
5
5
  Real-time texture compression library for the Web.
6
6
 
@@ -21,14 +21,14 @@ npm install @ludicon/spark.js
21
21
  ## Usage Example
22
22
 
23
23
  ```js
24
- import { Spark } from "spark.js"
24
+ import { Spark } from "@ludicon/spark.js"
25
25
 
26
26
  // Initialize a WebGPU device with required features
27
27
  const adapter = await navigator.gpu.requestAdapter()
28
28
  const requiredFeatures = Spark.getRequiredFeatures(adapter)
29
29
  const device = await adapter.requestDevice({ requiredFeatures })
30
30
 
31
- // Create Spark instance for the WebGPU device
31
+ // Create spark instance for the WebGPU device
32
32
  const spark = await Spark.create(device)
33
33
 
34
34
  // Load and encode an image into a GPU texture
@@ -37,7 +37,7 @@ const texture = await spark.encodeTexture("image.avif")
37
37
 
38
38
  The main entry point is `spark.encodeTexture()`, which loads an image and transcodes it into a compressed `GPUTexture` using the selected format and options. The example above uses default settings, but `encodeTexture` supports additional parameters for mipmap generation, sRGB encoding, normal map processing, and more.
39
39
 
40
- If the input image dimensions are not multiples of the block size, it will be resized to meet GPU format requirements. For best results, use textures with dimensions that are multiples of 4.
40
+ If the input image dimensions are not multiples of the block size, it will be resized to meet GPU format requirements. For best results, use images with dimensions that are multiples of 4.
41
41
 
42
42
 
43
43
  ## Development
package/dist/index.esm.js CHANGED
@@ -1,11 +1,10 @@
1
- const modules = /* @__PURE__ */ Object.assign({ "./spark_astc_rgb.wgsl": () => import("./spark_astc_rgb-ylbf30mQ.js"), "./spark_astc_rgba.wgsl": () => import("./spark_astc_rgba-C4NuyfHw.js"), "./spark_bc1_rgb.wgsl": () => import("./spark_bc1_rgb-CRQwJRCp.js"), "./spark_bc3_rgba.wgsl": () => import("./spark_bc3_rgba-CyRcvC8t.js"), "./spark_bc4_r.wgsl": () => import("./spark_bc4_r-BSB9VB_w.js"), "./spark_bc5_rg.wgsl": () => import("./spark_bc5_rg-NX_OBH9I.js"), "./spark_bc7_rgb.wgsl": () => import("./spark_bc7_rgb-CYdL55pE.js"), "./spark_bc7_rgba.wgsl": () => import("./spark_bc7_rgba-BFgOyqos.js"), "./spark_eac_r.wgsl": () => import("./spark_eac_r-BFwH430b.js"), "./spark_eac_rg.wgsl": () => import("./spark_eac_rg--Gm5Gzmk.js"), "./spark_etc2_rgb.wgsl": () => import("./spark_etc2_rgb-CWjBHhHQ.js"), "./spark_etc2_rgba.wgsl": () => import("./spark_etc2_rgba-BRX5DwNI.js"), "./utils.wgsl": () => import("./utils-BybjJ-PV.js") });
1
+ const modules = /* @__PURE__ */ Object.assign({ "./spark_astc_rgb.wgsl": () => import("./spark_astc_rgb-ylbf30mQ.js"), "./spark_astc_rgba.wgsl": () => import("./spark_astc_rgba-C4NuyfHw.js"), "./spark_bc1_rgb.wgsl": () => import("./spark_bc1_rgb-CRQwJRCp.js"), "./spark_bc3_rgba.wgsl": () => import("./spark_bc3_rgba-CyRcvC8t.js"), "./spark_bc4_r.wgsl": () => import("./spark_bc4_r-BSB9VB_w.js"), "./spark_bc5_rg.wgsl": () => import("./spark_bc5_rg-NX_OBH9I.js"), "./spark_bc7_rgb.wgsl": () => import("./spark_bc7_rgb-CYdL55pE.js"), "./spark_bc7_rgba.wgsl": () => import("./spark_bc7_rgba-BFgOyqos.js"), "./spark_eac_r.wgsl": () => import("./spark_eac_r-BFwH430b.js"), "./spark_eac_rg.wgsl": () => import("./spark_eac_rg--Gm5Gzmk.js"), "./spark_etc2_rgb.wgsl": () => import("./spark_etc2_rgb-CWjBHhHQ.js"), "./spark_etc2_rgba.wgsl": () => import("./spark_etc2_rgba-BRX5DwNI.js"), "./utils.wgsl": () => import("./utils-Dpsm7Knh.js") });
2
2
  const shaders = Object.fromEntries(
3
- await Promise.all(
4
- Object.entries(modules).map(async function([path, module]) {
5
- const { default: shader } = await module(), name = path.replace("./", "");
6
- return [name, shader];
7
- })
8
- )
3
+ Object.entries(modules).map(([path, module]) => {
4
+ const name = path.replace("./", "");
5
+ const fn = async () => (await module()).default;
6
+ return [name, fn];
7
+ })
9
8
  );
10
9
  const SparkFormat = {
11
10
  ASTC_4x4_RGB: 0,
@@ -461,11 +460,13 @@ class Spark {
461
460
  /**
462
461
  * Initialize the encoder by detecting available compression formats.
463
462
  * @param {GPUDevice} device - WebGPU device.
463
+ * @param {Object} options - Encoder options.
464
+ * @param {boolean} options.preload - Whether to preload all encoder pipelines (false by default).
464
465
  * @returns {Promise<void>} Resolves when initialization is complete.
465
466
  */
466
- static async create(device) {
467
+ static async create(device, options = {}) {
467
468
  const instance = new Spark();
468
- await instance.#init(device);
469
+ await instance.#init(device, options.preload ?? false);
469
470
  return instance;
470
471
  }
471
472
  /**
@@ -757,7 +758,7 @@ class Spark {
757
758
  }
758
759
  const commandEncoder = this.#device.createCommandEncoder();
759
760
  commandEncoder.resolveQuerySet(this.#querySet, 0, 2, this.#queryBuffer, 0);
760
- commandEncoder.copyBufferToBuffer(this.#queryBuffer, this.#queryReadbackBuffer, 16);
761
+ commandEncoder.copyBufferToBuffer(this.#queryBuffer, 0, this.#queryReadbackBuffer, 0, 16);
761
762
  this.#device.queue.submit([commandEncoder.finish()]);
762
763
  await this.#device.queue.onSubmittedWorkDone();
763
764
  await this.#queryReadbackBuffer.mapAsync(GPUMapMode.READ);
@@ -770,7 +771,7 @@ class Spark {
770
771
  const elapsedMilliseconds = elapsedNanoseconds / 1e6;
771
772
  return elapsedMilliseconds;
772
773
  }
773
- async #init(device) {
774
+ async #init(device, preload) {
774
775
  assert(device, "device is required");
775
776
  assert(isWebGPU(device), "device is not a WebGPU device");
776
777
  this.#device = device;
@@ -793,7 +794,10 @@ class Spark {
793
794
  const webkitVersion = getSafariVersion();
794
795
  const firefoxVersion = getFirefoxVersion();
795
796
  if ((!webkitVersion || webkitVersion >= 26) && !firefoxVersion) {
796
- this.#querySet = this.#device.createQuerySet({ type: "timestamp", count: 2 });
797
+ this.#querySet = this.#device.createQuerySet({
798
+ type: "timestamp",
799
+ count: 2
800
+ });
797
801
  this.#queryBuffer = this.#device.createBuffer({
798
802
  size: 16,
799
803
  // 2 timestamps × 8 bytes each
@@ -808,17 +812,19 @@ class Spark {
808
812
  }
809
813
  this.#supportsFloat16 = this.#device.features.has("shader-f16");
810
814
  await this.#loadUtilPipelines();
811
- for (const format of this.#supportedFormats) {
812
- if (!this.#pipelines[format]) {
813
- this.#loadPipeline(format).catch((err) => {
814
- console.error(`Failed to preload pipeline for format ${format}:`, err);
815
- });
815
+ if (preload) {
816
+ for (const format of this.#supportedFormats) {
817
+ if (!this.#pipelines[format]) {
818
+ this.#loadPipeline(format).catch((err) => {
819
+ console.error(`Failed to preload pipeline for format ${format}:`, err);
820
+ });
821
+ }
816
822
  }
817
823
  }
818
824
  }
819
825
  async #loadUtilPipelines() {
820
826
  const shaderModule = this.#device.createShaderModule({
821
- code: shaders["utils.wgsl"],
827
+ code: await shaders["utils.wgsl"](),
822
828
  label: "utils"
823
829
  });
824
830
  if (typeof shaderModule.compilationInfo == "function") {
@@ -867,7 +873,7 @@ class Spark {
867
873
  const pipelinePromise = (async () => {
868
874
  const shaderFile = SparkShaderFiles[format];
869
875
  assert(shaderFile, `No shader available for format ${SparkFormatName[format]}`);
870
- let shaderCode = shaders[shaderFile];
876
+ let shaderCode = await shaders[shaderFile]();
871
877
  if (!this.#supportsFloat16) {
872
878
  shaderCode = shaderCode.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");
873
879
  }
@@ -0,0 +1,4 @@
1
+ const utils = "struct Params {\n to_srgb: u32,\n};\n\n@group(0) @binding(0) var src : texture_2d<f32>;\n@group(0) @binding(1) var dst : texture_storage_2d<rgba8unorm, write>;\n@group(0) @binding(2) var smp: sampler;\n@group(0) @binding(3) var<uniform> params: Params;\n\nfn linear_to_srgb_vec3(c: vec3<f32>) -> vec3<f32> {\n return select(\n 1.055 * pow(c, vec3<f32>(1.0 / 2.4)) - 0.055,\n c * 12.92,\n c <= vec3<f32>(0.0031308)\n );\n}\n\nfn linear_to_srgb_vec4(c: vec4<f32>) -> vec4<f32> {\n return vec4<f32>(linear_to_srgb_vec3(c.xyz), 1.0);\n}\n\n@compute @workgroup_size(8, 8)\nfn mipmap(@builtin(global_invocation_id) id : vec3<u32>) {\n let dstSize = textureDimensions(dst).xy;\n if (id.x >= dstSize.x || id.y >= dstSize.y) {\n return;\n }\n\n let size_rcp = vec2f(1.0) / vec2f(dstSize);\n let uv0 = vec2f(id.xy) * size_rcp;\n let uv1 = uv0 + size_rcp;\n\n var color = vec4f(0.0);\n color += textureSampleLevel(src, smp, vec2f(uv0.x, uv0.y), 0);\n color += textureSampleLevel(src, smp, vec2f(uv1.x, uv0.y), 0);\n color += textureSampleLevel(src, smp, vec2f(uv0.x, uv1.y), 0);\n color += textureSampleLevel(src, smp, vec2f(uv1.x, uv1.y), 0);\n color *= 0.25; \n\n if (params.to_srgb != 0) {\n color = linear_to_srgb_vec4(color);\n }\n\n textureStore(dst, id.xy, color);\n}\n\n@compute @workgroup_size(8, 8)\nfn resize(@builtin(global_invocation_id) id : vec3<u32>) {\n let dstSize = textureDimensions(dst).xy;\n if (id.x >= dstSize.x || id.y >= dstSize.y) {\n return;\n }\n\n let uv = (vec2f(id.xy) + vec2f(0.5)) / vec2f(dstSize);\n var color = textureSampleLevel(src, smp, uv, 0);\n\n if (params.to_srgb != 0) {\n color = linear_to_srgb_vec4(color);\n }\n\n textureStore(dst, id.xy, color);\n}\n\n@compute @workgroup_size(8, 8)\nfn flipy(@builtin(global_invocation_id) id : vec3<u32>) {\n let dstSize = textureDimensions(dst).xy;\n if (id.x >= dstSize.x || id.y >= dstSize.y) {\n return;\n }\n\n let uv = (vec2f(f32(id.x), f32(dstSize.y - 1u - id.y)) + vec2f(0.5)) / vec2f(dstSize);\n var color = textureSampleLevel(src, smp, uv, 0);\n\n if (params.to_srgb != 0) {\n color = linear_to_srgb_vec4(color);\n }\n\n textureStore(dst, id.xy, color);\n}\n\n\n@group(0) @binding(1) var<storage, read_write> global_counters: array<atomic<u32>, 3>;\n\nvar<workgroup> local_opaque: atomic<u32>;\nvar<workgroup> local_grayscale: atomic<u32>;\nvar<workgroup> local_invalid_normals: atomic<u32>;\n\n@compute @workgroup_size(8, 8)\nfn detect_channel_count(@builtin(global_invocation_id) global_id: vec3<u32>,\n @builtin(local_invocation_index) local_id: u32) {\n \n if (local_id == 0u) {\n atomicStore(&local_opaque, 1u);\n atomicStore(&local_grayscale, 1u);\n atomicStore(&local_invalid_normals, 0u);\n }\n workgroupBarrier();\n\n let tex_size = textureDimensions(src);\n if (global_id.x < tex_size.x && global_id.y < tex_size.y) {\n\n let color = textureLoad(src, vec2<i32>(global_id.xy), 0);\n\n // Alpha check\n if (color.a < 1.0) {\n atomicStore(&local_opaque, 0u);\n }\n\n // Grayscale check\n if (color.r != color.g || color.g != color.b) {\n atomicStore(&local_grayscale, 0u);\n }\n\n // Normal check\n let n = color.rgb * 2.0 - vec3(1.0);\n let len = length(n);\n\n if (abs(len - 1.0) > 0.2 || n.z < -0.1) {\n atomicAdd(&local_invalid_normals, 1u);\n }\n }\n\n workgroupBarrier();\n\n if (local_id == 0u) {\n // If not opaque, write not-opaque flag.\n if (atomicLoad(&local_opaque) == 0u) {\n atomicStore(&global_counters[0], 1u);\n }\n\n // If not greyscale, write not greyscale flag.\n if (atomicLoad(&local_grayscale) == 0u) {\n atomicStore(&global_counters[1], 1u);\n }\n\n // Add number of texels that are not normal.\n atomicAdd(&global_counters[2], atomicLoad(&local_invalid_normals));\n }\n}\n\n\n// @@ Compute RMSE?\n\n\n";
2
+ export {
3
+ utils as default
4
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ludicon/spark.js",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Real-Time GPU Texture Codecs for the Web",
5
5
  "main": "dist/index.esm.js",
6
6
  "module": "dist/index.esm.js",
@@ -1,4 +0,0 @@
1
- const utils = "struct Params {\n to_srgb: u32,\n};\n\n@group(0) @binding(0) var src : texture_2d<f32>;\n@group(0) @binding(1) var dst : texture_storage_2d<rgba8unorm, write>;\n@group(0) @binding(2) var smp: sampler;\n@group(0) @binding(3) var<uniform> params: Params;\n\nfn linear_to_srgb_vec3(c: vec3<f32>) -> vec3<f32> {\n return select(\n 1.055 * pow(c, vec3<f32>(1.0 / 2.4)) - 0.055,\n c * 12.92,\n c <= vec3<f32>(0.0031308)\n );\n}\n\nfn linear_to_srgb_vec4(c: vec4<f32>) -> vec4<f32> {\n return vec4<f32>(linear_to_srgb_vec3(c.xyz), 1.0);\n}\n\n@compute @workgroup_size(8, 8)\nfn mipmap(@builtin(global_invocation_id) id : vec3<u32>) {\n let dstSize = textureDimensions(dst).xy;\n if (id.x >= dstSize.x || id.y >= dstSize.y) {\n return;\n }\n\n let size_rcp = vec2f(1.0) / vec2f(dstSize);\n let uv0 = vec2f(id.xy) * size_rcp;\n let uv1 = uv0 + size_rcp;\n\n var color = vec4f(0.0);\n color += textureSampleLevel(src, smp, vec2f(uv0.x, uv0.y), 0);\n color += textureSampleLevel(src, smp, vec2f(uv1.x, uv0.y), 0);\n color += textureSampleLevel(src, smp, vec2f(uv0.x, uv1.y), 0);\n color += textureSampleLevel(src, smp, vec2f(uv1.x, uv1.y), 0);\n color *= 0.25; \n\n if (params.to_srgb != 0) {\n color = linear_to_srgb_vec4(color);\n }\n\n textureStore(dst, id.xy, color);\n}\n\n@compute @workgroup_size(8, 8)\nfn resize(@builtin(global_invocation_id) id : vec3<u32>) {\n let dstSize = textureDimensions(dst).xy;\n if (id.x >= dstSize.x || id.y >= dstSize.y) {\n return;\n }\n\n let uv = (vec2f(id.xy) + vec2f(0.5)) / vec2f(dstSize);\n var color = textureSampleLevel(src, smp, uv, 0);\n\n if (params.to_srgb != 0) {\n color = linear_to_srgb_vec4(color);\n }\n\n textureStore(dst, id.xy, color);\n}\n\n@compute @workgroup_size(8, 8)\nfn flipy(@builtin(global_invocation_id) id : vec3<u32>) {\n let dstSize = textureDimensions(dst).xy;\n if (id.x >= dstSize.x || id.y >= dstSize.y) {\n return;\n }\n\n let uv = vec2f(f32(id.x), f32(dstSize.y - 1u - id.y)) / vec2f(dstSize);\n var color = textureSampleLevel(src, smp, uv, 0);\n\n if (params.to_srgb != 0) {\n color = linear_to_srgb_vec4(color);\n }\n\n textureStore(dst, id.xy, color);\n}\n\n\n@group(0) @binding(1) var<storage, read_write> global_counters: array<atomic<u32>, 3>;\n\nvar<workgroup> local_opaque: atomic<u32>;\nvar<workgroup> local_grayscale: atomic<u32>;\nvar<workgroup> local_invalid_normals: atomic<u32>;\n\n@compute @workgroup_size(8, 8)\nfn detect_channel_count(@builtin(global_invocation_id) global_id: vec3<u32>,\n @builtin(local_invocation_index) local_id: u32) {\n \n if (local_id == 0u) {\n atomicStore(&local_opaque, 1u);\n atomicStore(&local_grayscale, 1u);\n atomicStore(&local_invalid_normals, 0u);\n }\n workgroupBarrier();\n\n let tex_size = textureDimensions(src);\n if (global_id.x < tex_size.x && global_id.y < tex_size.y) {\n\n let color = textureLoad(src, vec2<i32>(global_id.xy), 0);\n\n // Alpha check\n if (color.a < 1.0) {\n atomicStore(&local_opaque, 0u);\n }\n\n // Grayscale check\n if (color.r != color.g || color.g != color.b) {\n atomicStore(&local_grayscale, 0u);\n }\n\n // Normal check\n let n = color.rgb * 2.0 - vec3(1.0);\n let len = length(n);\n\n if (abs(len - 1.0) > 0.2 || n.z < -0.1) {\n atomicAdd(&local_invalid_normals, 1u);\n }\n }\n\n workgroupBarrier();\n\n if (local_id == 0u) {\n // If not opaque, write not-opaque flag.\n if (atomicLoad(&local_opaque) == 0u) {\n atomicStore(&global_counters[0], 1u);\n }\n\n // If not greyscale, write not greyscale flag.\n if (atomicLoad(&local_grayscale) == 0u) {\n atomicStore(&global_counters[1], 1u);\n }\n\n // Add number of texels that are not normal.\n atomicAdd(&global_counters[2], atomicLoad(&local_invalid_normals));\n }\n}\n\n\n// @@ Compute RMSE?\n\n\n";
2
- export {
3
- utils as default
4
- };