@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 +4 -4
- package/dist/index.esm.js +25 -19
- package/dist/utils-Dpsm7Knh.js +4 -0
- package/package.json +1 -1
- package/dist/utils-BybjJ-PV.js +0 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# spark.js⚡️
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@ludicon/spark.js) [](https://developer.mozilla.org/en-US/docs/Web/API/WebGPU_API)
|
|
3
|
+
[](https://www.npmjs.com/package/@ludicon/spark.js) [](https://packagephobia.com/result?p=@ludicon/spark.js) [](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
|
|
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
|
|
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-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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({
|
|
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
|
-
|
|
812
|
-
|
|
813
|
-
this.#
|
|
814
|
-
|
|
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
package/dist/utils-BybjJ-PV.js
DELETED
|
@@ -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
|
-
};
|