raw-webgpu 0.1.0 → 0.1.1
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 +11 -3
- package/dist/develop/gpu.d.ts +3 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +59 -23
- package/dist/types.d.ts +6 -0
- package/docs/conversion.md +4 -19
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# raw-webgpu
|
|
2
2
|
|
|
3
|
-
Load camera RAW, DNG and TIFF files into WebGPU textures. Develop into linear Rec.2020
|
|
3
|
+
Load camera RAW, DNG and TIFF files into WebGPU textures. Develop into linear Rec.2020 or display sRGB, with adjustable white balance and exposure.
|
|
4
4
|
|
|
5
5
|
LibRaw, Adobe DNG SDK and libjxl decode files in a WASM worker. Simple TIFF strips use original file bytes or browser Deflate; complex layouts retain the SDK fallback. WebGPU handles supported demosaic and color processing. Supply your own `GPUDevice`; no rendering framework or runtime dependencies are required.
|
|
6
6
|
|
|
@@ -15,7 +15,7 @@ LibRaw, Adobe DNG SDK and libjxl decode files in a WASM worker. Simple TIFF stri
|
|
|
15
15
|
## Installation
|
|
16
16
|
|
|
17
17
|
```sh
|
|
18
|
-
npm install --save-exact raw-webgpu@0.1.
|
|
18
|
+
npm install --save-exact raw-webgpu@0.1.1
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
WASM, workers and types are included; consumers do not compile C++. Use a WebGPU browser with WASM SIMD/exception support and a bundler that handles worker/WASM asset URLs, such as Vite. TypeScript 5.9 and newer are tested; with TypeScript 5.9, install `@webgpu/types` and include it in `compilerOptions.types`.
|
|
@@ -45,6 +45,10 @@ destination.destroy();
|
|
|
45
45
|
decoder.dispose();
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
For direct display, create a pass with `{ outputColorSpace: "srgb", format }`, where `format` matches your sRGB canvas configuration, usually `navigator.gpu.getPreferredCanvasFormat()`. Render to `context.getCurrentTexture()` with the canvas dimensions set to `source.size`. No app shader is needed.
|
|
49
|
+
|
|
50
|
+
The default is `{ outputColorSpace: "linear-rec2020", format: "rgba16float" }`. Supported formats are `rgba16float`, `rgba8unorm` and `bgra8unorm`. sRGB output converts primaries, clips to [0, 1] and applies the sRGB transfer function on GPU; it does not add a photographic tone curve. Do not apply sRGB encoding again.
|
|
51
|
+
|
|
48
52
|
For TIFF:
|
|
49
53
|
|
|
50
54
|
```ts
|
|
@@ -62,11 +66,15 @@ Use separate passes for independent previews or exports. If passing an external
|
|
|
62
66
|
For file export, see the [Bun PNG/JPEG/BMP conversion example](docs/conversion.md).
|
|
63
67
|
|
|
64
68
|
|
|
69
|
+
## Minimal website
|
|
70
|
+
|
|
71
|
+
`web/` is a fullscreen RAW viewer with no app shader or rendering framework. It currently uses a local link: build the library and run `bun link` at the repository root, then `cd web && bun install && bun run dev`. After publishing the new API, pin that npm version in `web/package.json` for standalone deployment. Build with `bun run build` and serve `web/dist/` over HTTPS. Files stay in the browser. With the dev server running, `bun run test:web` from the repository root checks loading through Vite.
|
|
72
|
+
|
|
65
73
|
## API contract and support
|
|
66
74
|
|
|
67
75
|
The supported browser baseline is Chromium with WebGPU. Verified environments are Chromium 151 on macOS with Apple M4 Pro and Linux CI with software rendering, plus Chrome 153 on macOS. Safari, Firefox, mobile browsers and other physical GPUs have not been validated. Tests simulate lower texture/buffer limits; they do not establish a device memory budget. Bun is an additional tested runtime, not a CPU fallback.
|
|
68
76
|
|
|
69
|
-
-
|
|
77
|
+
- Default RAW and decoded TIFF output is linear Rec.2020/D65 `rgba16float`, with no display tone curve. HDR values can exceed 1. RAW passes can instead output encoded sRGB for display. TIFF alpha is straight. Camera JPEG previews apply their own rendering and are not pixel references for this output.
|
|
70
78
|
- `source.texture` contains sensor or camera-RGB samples, not developed output. Read its format and metadata; do not assume every RAW is a one-channel mosaic. `source.size` is oriented output size; `metadata.size` is the unrotated sample size.
|
|
71
79
|
- Calibration has three RGB gains and a nine-value column-major matrix. Development applies gains before the camera-to-working-space matrix; exposure is in stops. Independent passes can use different calibration without changing the source.
|
|
72
80
|
- A decoder owns its sources, each source owns its passes and worker, and the caller owns destination textures. Disposal is idempotent. Device loss closes the decoder; create another decoder with a new device. Pending loads and calibration requests reject when their owner closes.
|
package/dist/develop/gpu.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { DevelopOptions, RawPixels } from "../types";
|
|
1
|
+
import type { DevelopOptions, DevelopPassOptions, RawPixels } from "../types";
|
|
2
2
|
export declare function createPipeline(device: GPUDevice): Promise<{
|
|
3
3
|
sensor: GPURenderPipeline;
|
|
4
4
|
camera: GPURenderPipeline;
|
|
5
5
|
refine: GPURenderPipeline;
|
|
6
|
+
get(entryPoint: string, options: DevelopPassOptions): GPURenderPipeline;
|
|
6
7
|
}>;
|
|
7
8
|
/** Owns the sensor texture and immutable uniforms; each pass owns its calibration buffer. */
|
|
8
9
|
export declare function createGpuSource(device: GPUDevice, pixels: RawPixels, pipeline: Awaited<ReturnType<typeof createPipeline>>): {
|
|
@@ -20,7 +21,7 @@ export declare function createGpuSource(device: GPUDevice, pixels: RawPixels, pi
|
|
|
20
21
|
whiteBalanceOrigin?: "as-shot" | "daylight";
|
|
21
22
|
};
|
|
22
23
|
size: [number, number];
|
|
23
|
-
createDevelopPass(): {
|
|
24
|
+
createDevelopPass(options?: DevelopPassOptions): {
|
|
24
25
|
render(options: DevelopOptions): void;
|
|
25
26
|
dispose: () => void;
|
|
26
27
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LoadOptions } from "./types";
|
|
2
|
-
export type { Calibration, DevelopOptions, LoadOptions, RawMetadata, WhiteBalance, } from "./types";
|
|
2
|
+
export type { Calibration, DevelopOptions, DevelopPassOptions, LoadOptions, RawMetadata, WhiteBalance, } from "./types";
|
|
3
3
|
/** Reuses a pipeline on the consumer's device. Each load owns an independent worker and source. */
|
|
4
4
|
export declare function createRawDecoder(device: GPUDevice): {
|
|
5
5
|
load(file: Blob, { signal }?: LoadOptions): Promise<{
|
|
@@ -17,7 +17,7 @@ export declare function createRawDecoder(device: GPUDevice): {
|
|
|
17
17
|
whiteBalanceOrigin?: "as-shot" | "daylight";
|
|
18
18
|
};
|
|
19
19
|
size: [number, number];
|
|
20
|
-
createDevelopPass(): {
|
|
20
|
+
createDevelopPass(options?: import("./types").DevelopPassOptions): {
|
|
21
21
|
render(options: import("./types").DevelopOptions): void;
|
|
22
22
|
dispose: () => void;
|
|
23
23
|
};
|
package/dist/index.js
CHANGED
|
@@ -90,9 +90,24 @@ function createSession(compiled2) {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
// src/develop/develop.wgsl
|
|
93
|
-
var develop_default = `// Sensor samples in,
|
|
93
|
+
var develop_default = `// Sensor samples in, linear Rec.2020 or display sRGB out: normalize, white balance,
|
|
94
94
|
// demosaic when needed, correct vignetting, convert color and apply exposure.
|
|
95
95
|
|
|
96
|
+
// Specialization removes display conversion from the default editor pipeline.
|
|
97
|
+
override outputSrgb: bool = false;
|
|
98
|
+
|
|
99
|
+
fn output(color: vec3f) -> vec4f {
|
|
100
|
+
if (!outputSrgb) { return vec4f(color, 1.0); }
|
|
101
|
+
let matrix = mat3x3f(
|
|
102
|
+
1.660491, -0.124550, -0.018151,
|
|
103
|
+
-0.587641, 1.132900, -0.100579,
|
|
104
|
+
-0.072850, -0.008349, 1.118730,
|
|
105
|
+
);
|
|
106
|
+
let rgb = clamp(matrix * color, vec3f(0.0), vec3f(1.0));
|
|
107
|
+
let encoded = select(1.055 * pow(rgb, vec3f(1.0 / 2.4)) - 0.055, 12.92 * rgb, rgb <= vec3f(0.0031308));
|
|
108
|
+
return vec4f(encoded, 1.0);
|
|
109
|
+
}
|
|
110
|
+
|
|
96
111
|
struct Sensor {
|
|
97
112
|
black: vec4f,
|
|
98
113
|
patternSize: vec4u,
|
|
@@ -218,7 +233,7 @@ fn demosaicPattern(p: vec2i, size: vec2i) -> vec3f {
|
|
|
218
233
|
let k = vignette.coefficients;
|
|
219
234
|
let gain = 1.0 + r2 * (k.x + r2 * (k.y + r2 * (k.z + r2 * (k.w + r2 * vignette.last))));
|
|
220
235
|
|
|
221
|
-
return
|
|
236
|
+
return output(calibration.matrix * camera * gain * calibration.exposure);
|
|
222
237
|
}
|
|
223
238
|
|
|
224
239
|
@group(0) @binding(5) var cameraSource: texture_2d<f32>;
|
|
@@ -261,7 +276,7 @@ fn demosaicPattern(p: vec2i, size: vec2i) -> vec3f {
|
|
|
261
276
|
// Cached X-Trans camera RGB: subsequent edits only apply per-pixel linear transforms.
|
|
262
277
|
@fragment fn fs_camera(@builtin(position) position: vec4f) -> @location(0) vec4f {
|
|
263
278
|
let camera = textureLoad(cameraSource, vec2i(position.xy), 0).rgb;
|
|
264
|
-
return
|
|
279
|
+
return output(calibration.matrix * (camera * calibration.gains) * calibration.exposure);
|
|
265
280
|
}
|
|
266
281
|
|
|
267
282
|
// One triangle covering the whole destination.
|
|
@@ -274,20 +289,46 @@ fn demosaicPattern(p: vec2i, size: vec2i) -> vec3f {
|
|
|
274
289
|
// src/develop/gpu.ts
|
|
275
290
|
async function createPipeline(device) {
|
|
276
291
|
const module = device.createShaderModule({ code: develop_default });
|
|
277
|
-
|
|
278
|
-
|
|
292
|
+
const pipelines = new Map;
|
|
293
|
+
function descriptor(entryPoint, options = {}) {
|
|
294
|
+
const { format = "rgba16float", outputColorSpace = "linear-rec2020" } = options;
|
|
295
|
+
if (!["rgba16float", "rgba8unorm", "bgra8unorm"].includes(format)) {
|
|
296
|
+
throw Error("RAW output format must be rgba16float, rgba8unorm or bgra8unorm.");
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
279
299
|
layout: "auto",
|
|
280
300
|
vertex: { module, entryPoint: "vs_main" },
|
|
281
|
-
fragment: {
|
|
301
|
+
fragment: {
|
|
302
|
+
module,
|
|
303
|
+
entryPoint,
|
|
304
|
+
targets: [{ format }],
|
|
305
|
+
constants: { outputSrgb: Number(outputColorSpace === "srgb") }
|
|
306
|
+
},
|
|
282
307
|
primitive: { topology: "triangle-list" }
|
|
283
|
-
}
|
|
308
|
+
};
|
|
284
309
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
310
|
+
function key(entryPoint, options = {}) {
|
|
311
|
+
return `${entryPoint}:${options.format ?? "rgba16float"}:${options.outputColorSpace ?? "linear-rec2020"}`;
|
|
312
|
+
}
|
|
313
|
+
const [sensor, camera, refine] = await Promise.all(["fs_main", "fs_camera", "fs_refine"].map(async (entryPoint) => {
|
|
314
|
+
const pipeline = await device.createRenderPipelineAsync(descriptor(entryPoint));
|
|
315
|
+
pipelines.set(key(entryPoint), pipeline);
|
|
316
|
+
return pipeline;
|
|
317
|
+
}));
|
|
318
|
+
return {
|
|
319
|
+
sensor,
|
|
320
|
+
camera,
|
|
321
|
+
refine,
|
|
322
|
+
get(entryPoint, options) {
|
|
323
|
+
const id = key(entryPoint, options);
|
|
324
|
+
let pipeline = pipelines.get(id);
|
|
325
|
+
if (!pipeline) {
|
|
326
|
+
pipeline = device.createRenderPipeline(descriptor(entryPoint, options));
|
|
327
|
+
pipelines.set(id, pipeline);
|
|
328
|
+
}
|
|
329
|
+
return pipeline;
|
|
330
|
+
}
|
|
331
|
+
};
|
|
291
332
|
}
|
|
292
333
|
function createUniform(device, data) {
|
|
293
334
|
const buffer = device.createBuffer({
|
|
@@ -346,7 +387,7 @@ function createGpuSource(device, pixels, pipeline) {
|
|
|
346
387
|
texture.destroy();
|
|
347
388
|
cameraTexture?.destroy();
|
|
348
389
|
}
|
|
349
|
-
function createPass(renderPipeline, entries) {
|
|
390
|
+
function createPass(renderPipeline, entries, format2 = "rgba16float") {
|
|
350
391
|
if (closed) {
|
|
351
392
|
throw Error("RAW source is closed.");
|
|
352
393
|
}
|
|
@@ -372,8 +413,8 @@ function createGpuSource(device, pixels, pipeline) {
|
|
|
372
413
|
throw Error("RAW development pass is closed.");
|
|
373
414
|
}
|
|
374
415
|
const { destination, calibration, exposure = 0 } = options;
|
|
375
|
-
if (destination.format !==
|
|
376
|
-
throw Error(
|
|
416
|
+
if (destination.format !== format2 || destination.width !== size[0] || destination.height !== size[1]) {
|
|
417
|
+
throw Error(`RAW destination must be a ${format2} texture matching the oriented source size.`);
|
|
377
418
|
}
|
|
378
419
|
calibrationData.set(calibration.gains);
|
|
379
420
|
calibrationData[3] = 2 ** exposure;
|
|
@@ -477,13 +518,8 @@ function createGpuSource(device, pixels, pipeline) {
|
|
|
477
518
|
texture,
|
|
478
519
|
metadata,
|
|
479
520
|
size,
|
|
480
|
-
createDevelopPass() {
|
|
481
|
-
|
|
482
|
-
return createPass(pipeline.camera, [
|
|
483
|
-
{ binding: 5, resource: cameraTexture.createView() }
|
|
484
|
-
]);
|
|
485
|
-
}
|
|
486
|
-
return createPass(pipeline.sensor, sensorEntries);
|
|
521
|
+
createDevelopPass(options = {}) {
|
|
522
|
+
return createPass(pipeline.get(cameraTexture ? "fs_camera" : "fs_main", options), cameraTexture ? [{ binding: 5, resource: cameraTexture.createView() }] : sensorEntries, options.format);
|
|
487
523
|
},
|
|
488
524
|
dispose
|
|
489
525
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -41,6 +41,12 @@ export type LoadOptions = {
|
|
|
41
41
|
/** Aborting rejects a pending load with the signal's reason; a loaded source is unaffected. */
|
|
42
42
|
signal?: AbortSignal;
|
|
43
43
|
};
|
|
44
|
+
export type DevelopPassOptions = {
|
|
45
|
+
/** Default linear-rec2020 preserves HDR. srgb converts primaries, clips to [0, 1] and encodes sRGB. */
|
|
46
|
+
outputColorSpace?: "linear-rec2020" | "srgb";
|
|
47
|
+
/** rgba16float by default; rgba8unorm and bgra8unorm also supported for direct display. */
|
|
48
|
+
format?: GPUTextureFormat;
|
|
49
|
+
};
|
|
44
50
|
export type DevelopOptions = {
|
|
45
51
|
destination: GPUTexture;
|
|
46
52
|
calibration: Calibration;
|
package/docs/conversion.md
CHANGED
|
@@ -7,34 +7,19 @@ import { PNG } from "pngjs";
|
|
|
7
7
|
import { init, target } from "vgpu/node";
|
|
8
8
|
import { createRawDecoder } from "raw-webgpu";
|
|
9
9
|
|
|
10
|
-
function srgb(value: number) {
|
|
11
|
-
const x = Math.max(0, Math.min(1, value));
|
|
12
|
-
return Math.round(255 * (
|
|
13
|
-
x <= 0.0031308 ? 12.92 * x : 1.055 * x ** (1 / 2.4) - 0.055
|
|
14
|
-
));
|
|
15
|
-
}
|
|
16
|
-
|
|
17
10
|
const gpu = await init();
|
|
18
11
|
const decoder = createRawDecoder(gpu.gpu);
|
|
19
12
|
try {
|
|
20
13
|
const source = await decoder.load(Bun.file("photo.dng"));
|
|
21
|
-
const output = target(gpu, { size: source.size, format: "
|
|
14
|
+
const output = target(gpu, { size: source.size, format: "rgba8unorm" });
|
|
22
15
|
try {
|
|
23
|
-
source.createDevelopPass().render({
|
|
16
|
+
source.createDevelopPass({ outputColorSpace: "srgb", format: "rgba8unorm" }).render({
|
|
24
17
|
destination: output.color.gpu,
|
|
25
18
|
calibration: source.calibration,
|
|
26
19
|
});
|
|
27
20
|
|
|
28
|
-
const pixels = await output.readFloats();
|
|
29
21
|
const image = new PNG({ width: source.size[0], height: source.size[1] });
|
|
30
|
-
|
|
31
|
-
const [r, g, b] = pixels.subarray(i, i + 3);
|
|
32
|
-
// Linear Rec.2020 → linear sRGB → encoded sRGB.
|
|
33
|
-
image.data[i] = srgb(1.660491 * r - 0.587641 * g - 0.072850 * b);
|
|
34
|
-
image.data[i + 1] = srgb(-0.124550 * r + 1.132900 * g - 0.008349 * b);
|
|
35
|
-
image.data[i + 2] = srgb(-0.018151 * r - 0.100579 * g + 1.118730 * b);
|
|
36
|
-
image.data[i + 3] = 255;
|
|
37
|
-
}
|
|
22
|
+
image.data.set(await output.read());
|
|
38
23
|
await Bun.write("photo.png", PNG.sync.write(image));
|
|
39
24
|
} finally {
|
|
40
25
|
output.color.dispose();
|
|
@@ -49,4 +34,4 @@ For JPEG, install `jpeg-js`, import its `encode` function and replace the final
|
|
|
49
34
|
|
|
50
35
|
Bun supplies the worker and local file-fetch APIs; Chromium is not required. Plain Node.js is not supported by this snippet. A working GPU backend is required; `vgpu/mock` cannot execute the development shader.
|
|
51
36
|
|
|
52
|
-
The example uses the initial white balance, develops on GPU, then reads back
|
|
37
|
+
The example uses the initial white balance, develops and converts to sRGB on GPU, then reads back the encoded bytes. Output is 8-bit SDR/sRGB with clipping and no photographic tone curve. File encoding belongs to the consuming application; the library does not include image encoders or a conversion helper.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "raw-webgpu",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Camera RAW, DNG and TIFF decoding with WebGPU development and interactive white balance.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"test": "bun test",
|
|
29
29
|
"test:gpu": "GPU=1 bun test",
|
|
30
30
|
"test:browser": "bun tests/browser.ts",
|
|
31
|
+
"test:web": "bun tests/web.ts",
|
|
31
32
|
"build:sdk": "bun build.ts && tsc --emitDeclarationOnly",
|
|
32
33
|
"test:tiff": "bun tests/tiff.browser.ts",
|
|
33
34
|
"benchmark:tiff": "bun tests/tiff.browser.ts --benchmark",
|