mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-07-31 11:27:21 +02:00
Merge pull request #20885 from calixteman/gouraud_gpu
Implement Gouraud-based shading using WebGPU.
This commit is contained in:
commit
918a319de6
@ -105,6 +105,7 @@ const DefaultPartialEvaluatorOptions = Object.freeze({
|
|||||||
iccUrl: null,
|
iccUrl: null,
|
||||||
standardFontDataUrl: null,
|
standardFontDataUrl: null,
|
||||||
wasmUrl: null,
|
wasmUrl: null,
|
||||||
|
prepareWebGPU: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const PatternType = {
|
const PatternType = {
|
||||||
@ -1513,7 +1514,8 @@ class PartialEvaluator {
|
|||||||
resources,
|
resources,
|
||||||
this._pdfFunctionFactory,
|
this._pdfFunctionFactory,
|
||||||
this.globalColorSpaceCache,
|
this.globalColorSpaceCache,
|
||||||
localColorSpaceCache
|
localColorSpaceCache,
|
||||||
|
this.options.prepareWebGPU
|
||||||
);
|
);
|
||||||
patternIR = shadingFill.getIR();
|
patternIR = shadingFill.getIR();
|
||||||
} catch (reason) {
|
} catch (reason) {
|
||||||
|
|||||||
@ -55,7 +55,8 @@ class Pattern {
|
|||||||
res,
|
res,
|
||||||
pdfFunctionFactory,
|
pdfFunctionFactory,
|
||||||
globalColorSpaceCache,
|
globalColorSpaceCache,
|
||||||
localColorSpaceCache
|
localColorSpaceCache,
|
||||||
|
prepareWebGPU = null
|
||||||
) {
|
) {
|
||||||
const dict = shading instanceof BaseStream ? shading.dict : shading;
|
const dict = shading instanceof BaseStream ? shading.dict : shading;
|
||||||
const type = dict.get("ShadingType");
|
const type = dict.get("ShadingType");
|
||||||
@ -76,6 +77,7 @@ class Pattern {
|
|||||||
case ShadingType.LATTICE_FORM_MESH:
|
case ShadingType.LATTICE_FORM_MESH:
|
||||||
case ShadingType.COONS_PATCH_MESH:
|
case ShadingType.COONS_PATCH_MESH:
|
||||||
case ShadingType.TENSOR_PATCH_MESH:
|
case ShadingType.TENSOR_PATCH_MESH:
|
||||||
|
prepareWebGPU?.();
|
||||||
return new MeshShading(
|
return new MeshShading(
|
||||||
shading,
|
shading,
|
||||||
xref,
|
xref,
|
||||||
@ -934,7 +936,7 @@ class MeshShading extends BaseShading {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_packData() {
|
_packData() {
|
||||||
let i, ii, j, jj;
|
let i, ii, j;
|
||||||
|
|
||||||
const coords = this.coords;
|
const coords = this.coords;
|
||||||
const coordsPacked = new Float32Array(coords.length * 2);
|
const coordsPacked = new Float32Array(coords.length * 2);
|
||||||
@ -945,25 +947,27 @@ class MeshShading extends BaseShading {
|
|||||||
}
|
}
|
||||||
this.coords = coordsPacked;
|
this.coords = coordsPacked;
|
||||||
|
|
||||||
|
// Stride 4 (RGBA layout, alpha unused) so the buffer maps directly to
|
||||||
|
// array<u32> in the WebGPU vertex shader without any repacking.
|
||||||
const colors = this.colors;
|
const colors = this.colors;
|
||||||
const colorsPacked = new Uint8Array(colors.length * 3);
|
const colorsPacked = new Uint8Array(colors.length * 4);
|
||||||
for (i = 0, j = 0, ii = colors.length; i < ii; i++) {
|
for (i = 0, j = 0, ii = colors.length; i < ii; i++) {
|
||||||
const c = colors[i];
|
const c = colors[i];
|
||||||
colorsPacked[j++] = c[0];
|
colorsPacked[j++] = c[0];
|
||||||
colorsPacked[j++] = c[1];
|
colorsPacked[j++] = c[1];
|
||||||
colorsPacked[j++] = c[2];
|
colorsPacked[j++] = c[2];
|
||||||
|
j++; // alpha — unused, stays 0
|
||||||
}
|
}
|
||||||
this.colors = colorsPacked;
|
this.colors = colorsPacked;
|
||||||
|
|
||||||
|
// Store raw vertex indices (not byte offsets) so the GPU shader can
|
||||||
|
// address coords / colors without knowing their strides, and so the
|
||||||
|
// arrays are transferable Uint32Arrays.
|
||||||
const figures = this.figures;
|
const figures = this.figures;
|
||||||
for (i = 0, ii = figures.length; i < ii; i++) {
|
for (i = 0, ii = figures.length; i < ii; i++) {
|
||||||
const figure = figures[i],
|
const figure = figures[i];
|
||||||
ps = figure.coords,
|
figure.coords = new Uint32Array(figure.coords);
|
||||||
cs = figure.colors;
|
figure.colors = new Uint32Array(figure.colors);
|
||||||
for (j = 0, jj = ps.length; j < jj; j++) {
|
|
||||||
ps[j] *= 2;
|
|
||||||
cs[j] *= 3;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -71,6 +71,20 @@ class BasePdfManager {
|
|||||||
FeatureTest.isOffscreenCanvasSupported;
|
FeatureTest.isOffscreenCanvasSupported;
|
||||||
evaluatorOptions.isImageDecoderSupported &&=
|
evaluatorOptions.isImageDecoderSupported &&=
|
||||||
FeatureTest.isImageDecoderSupported;
|
FeatureTest.isImageDecoderSupported;
|
||||||
|
|
||||||
|
// Set up a one-shot callback so evaluators can notify the main thread that
|
||||||
|
// WebGPU-acceleratable content was found. The flag ensures the message is
|
||||||
|
// sent at most once per document.
|
||||||
|
if (evaluatorOptions.enableWebGPU) {
|
||||||
|
let prepareWebGPUSent = false;
|
||||||
|
evaluatorOptions.prepareWebGPU = () => {
|
||||||
|
if (!prepareWebGPUSent) {
|
||||||
|
prepareWebGPUSent = true;
|
||||||
|
handler.send("PrepareWebGPU", null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
delete evaluatorOptions.enableWebGPU;
|
||||||
this.evaluatorOptions = Object.freeze(evaluatorOptions);
|
this.evaluatorOptions = Object.freeze(evaluatorOptions);
|
||||||
|
|
||||||
// Initialize image-options once per document.
|
// Initialize image-options once per document.
|
||||||
|
|||||||
@ -79,6 +79,7 @@ import { DOMFilterFactory } from "./filter_factory.js";
|
|||||||
import { DOMStandardFontDataFactory } from "display-standard_fontdata_factory";
|
import { DOMStandardFontDataFactory } from "display-standard_fontdata_factory";
|
||||||
import { DOMWasmFactory } from "display-wasm_factory";
|
import { DOMWasmFactory } from "display-wasm_factory";
|
||||||
import { GlobalWorkerOptions } from "./worker_options.js";
|
import { GlobalWorkerOptions } from "./worker_options.js";
|
||||||
|
import { initWebGPUMesh } from "./webgpu_mesh.js";
|
||||||
import { Metadata } from "./metadata.js";
|
import { Metadata } from "./metadata.js";
|
||||||
import { OptionalContentConfig } from "./optional_content_config.js";
|
import { OptionalContentConfig } from "./optional_content_config.js";
|
||||||
import { PagesMapper } from "./pages_mapper.js";
|
import { PagesMapper } from "./pages_mapper.js";
|
||||||
@ -347,6 +348,7 @@ function getDocument(src = {}) {
|
|||||||
? NodeFilterFactory
|
? NodeFilterFactory
|
||||||
: DOMFilterFactory);
|
: DOMFilterFactory);
|
||||||
const enableHWA = src.enableHWA === true;
|
const enableHWA = src.enableHWA === true;
|
||||||
|
const enableWebGPU = src.enableWebGPU === true;
|
||||||
const useWasm = src.useWasm !== false;
|
const useWasm = src.useWasm !== false;
|
||||||
const pagesMapper = src.pagesMapper || new PagesMapper();
|
const pagesMapper = src.pagesMapper || new PagesMapper();
|
||||||
|
|
||||||
@ -440,6 +442,7 @@ function getDocument(src = {}) {
|
|||||||
iccUrl,
|
iccUrl,
|
||||||
standardFontDataUrl,
|
standardFontDataUrl,
|
||||||
wasmUrl,
|
wasmUrl,
|
||||||
|
enableWebGPU,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const transportParams = {
|
const transportParams = {
|
||||||
@ -2926,6 +2929,13 @@ class WorkerTransport {
|
|||||||
this.#onProgress(data);
|
this.#onProgress(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
messageHandler.on("PrepareWebGPU", () => {
|
||||||
|
if (this.destroyed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
initWebGPUMesh();
|
||||||
|
});
|
||||||
|
|
||||||
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
|
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
|
||||||
messageHandler.on("FetchBinaryData", async data => {
|
messageHandler.on("FetchBinaryData", async data => {
|
||||||
if (this.destroyed) {
|
if (this.destroyed) {
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { drawMeshWithGPU, isWebGPUMeshReady } from "./webgpu_mesh.js";
|
||||||
import {
|
import {
|
||||||
FormatError,
|
FormatError,
|
||||||
info,
|
info,
|
||||||
@ -282,7 +283,7 @@ function drawTriangle(data, context, p1, p2, p3, c1, c2, c3) {
|
|||||||
const bytes = data.data,
|
const bytes = data.data,
|
||||||
rowSize = data.width * 4;
|
rowSize = data.width * 4;
|
||||||
let tmp;
|
let tmp;
|
||||||
if (coords[p1 + 1] > coords[p2 + 1]) {
|
if (coords[p1 * 2 + 1] > coords[p2 * 2 + 1]) {
|
||||||
tmp = p1;
|
tmp = p1;
|
||||||
p1 = p2;
|
p1 = p2;
|
||||||
p2 = tmp;
|
p2 = tmp;
|
||||||
@ -290,7 +291,7 @@ function drawTriangle(data, context, p1, p2, p3, c1, c2, c3) {
|
|||||||
c1 = c2;
|
c1 = c2;
|
||||||
c2 = tmp;
|
c2 = tmp;
|
||||||
}
|
}
|
||||||
if (coords[p2 + 1] > coords[p3 + 1]) {
|
if (coords[p2 * 2 + 1] > coords[p3 * 2 + 1]) {
|
||||||
tmp = p2;
|
tmp = p2;
|
||||||
p2 = p3;
|
p2 = p3;
|
||||||
p3 = tmp;
|
p3 = tmp;
|
||||||
@ -298,7 +299,7 @@ function drawTriangle(data, context, p1, p2, p3, c1, c2, c3) {
|
|||||||
c2 = c3;
|
c2 = c3;
|
||||||
c3 = tmp;
|
c3 = tmp;
|
||||||
}
|
}
|
||||||
if (coords[p1 + 1] > coords[p2 + 1]) {
|
if (coords[p1 * 2 + 1] > coords[p2 * 2 + 1]) {
|
||||||
tmp = p1;
|
tmp = p1;
|
||||||
p1 = p2;
|
p1 = p2;
|
||||||
p2 = tmp;
|
p2 = tmp;
|
||||||
@ -306,24 +307,24 @@ function drawTriangle(data, context, p1, p2, p3, c1, c2, c3) {
|
|||||||
c1 = c2;
|
c1 = c2;
|
||||||
c2 = tmp;
|
c2 = tmp;
|
||||||
}
|
}
|
||||||
const x1 = (coords[p1] + context.offsetX) * context.scaleX;
|
const x1 = (coords[p1 * 2] + context.offsetX) * context.scaleX;
|
||||||
const y1 = (coords[p1 + 1] + context.offsetY) * context.scaleY;
|
const y1 = (coords[p1 * 2 + 1] + context.offsetY) * context.scaleY;
|
||||||
const x2 = (coords[p2] + context.offsetX) * context.scaleX;
|
const x2 = (coords[p2 * 2] + context.offsetX) * context.scaleX;
|
||||||
const y2 = (coords[p2 + 1] + context.offsetY) * context.scaleY;
|
const y2 = (coords[p2 * 2 + 1] + context.offsetY) * context.scaleY;
|
||||||
const x3 = (coords[p3] + context.offsetX) * context.scaleX;
|
const x3 = (coords[p3 * 2] + context.offsetX) * context.scaleX;
|
||||||
const y3 = (coords[p3 + 1] + context.offsetY) * context.scaleY;
|
const y3 = (coords[p3 * 2 + 1] + context.offsetY) * context.scaleY;
|
||||||
if (y1 >= y3) {
|
if (y1 >= y3) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const c1r = colors[c1],
|
const c1r = colors[c1 * 4],
|
||||||
c1g = colors[c1 + 1],
|
c1g = colors[c1 * 4 + 1],
|
||||||
c1b = colors[c1 + 2];
|
c1b = colors[c1 * 4 + 2];
|
||||||
const c2r = colors[c2],
|
const c2r = colors[c2 * 4],
|
||||||
c2g = colors[c2 + 1],
|
c2g = colors[c2 * 4 + 1],
|
||||||
c2b = colors[c2 + 2];
|
c2b = colors[c2 * 4 + 2];
|
||||||
const c3r = colors[c3],
|
const c3r = colors[c3 * 4],
|
||||||
c3g = colors[c3 + 1],
|
c3g = colors[c3 * 4 + 1],
|
||||||
c3b = colors[c3 + 2];
|
c3b = colors[c3 * 4 + 2];
|
||||||
|
|
||||||
const minY = Math.round(y1),
|
const minY = Math.round(y1),
|
||||||
maxY = Math.round(y3);
|
maxY = Math.round(y3);
|
||||||
@ -494,26 +495,39 @@ class MeshShadingPattern extends BaseShadingPattern {
|
|||||||
paddedWidth,
|
paddedWidth,
|
||||||
paddedHeight
|
paddedHeight
|
||||||
);
|
);
|
||||||
const tmpCtx = tmpCanvas.context;
|
|
||||||
|
|
||||||
const data = tmpCtx.createImageData(width, height);
|
if (isWebGPUMeshReady()) {
|
||||||
if (backgroundColor) {
|
tmpCanvas.context.drawImage(
|
||||||
const bytes = data.data;
|
drawMeshWithGPU(
|
||||||
for (let i = 0, ii = bytes.length; i < ii; i += 4) {
|
this._figures,
|
||||||
bytes[i] = backgroundColor[0];
|
context,
|
||||||
bytes[i + 1] = backgroundColor[1];
|
backgroundColor,
|
||||||
bytes[i + 2] = backgroundColor[2];
|
paddedWidth,
|
||||||
bytes[i + 3] = 255;
|
paddedHeight,
|
||||||
|
BORDER_SIZE
|
||||||
|
),
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const data = tmpCanvas.context.createImageData(width, height);
|
||||||
|
if (backgroundColor) {
|
||||||
|
const bytes = data.data;
|
||||||
|
for (let i = 0, ii = bytes.length; i < ii; i += 4) {
|
||||||
|
bytes[i] = backgroundColor[0];
|
||||||
|
bytes[i + 1] = backgroundColor[1];
|
||||||
|
bytes[i + 2] = backgroundColor[2];
|
||||||
|
bytes[i + 3] = 255;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
for (const figure of this._figures) {
|
||||||
|
drawFigure(data, figure, context);
|
||||||
|
}
|
||||||
|
tmpCanvas.context.putImageData(data, BORDER_SIZE, BORDER_SIZE);
|
||||||
}
|
}
|
||||||
for (const figure of this._figures) {
|
|
||||||
drawFigure(data, figure, context);
|
|
||||||
}
|
|
||||||
tmpCtx.putImageData(data, BORDER_SIZE, BORDER_SIZE);
|
|
||||||
const canvas = tmpCanvas.canvas;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
canvas,
|
canvas: tmpCanvas.canvas,
|
||||||
offsetX: offsetX - BORDER_SIZE * scaleX,
|
offsetX: offsetX - BORDER_SIZE * scaleX,
|
||||||
offsetY: offsetY - BORDER_SIZE * scaleY,
|
offsetY: offsetY - BORDER_SIZE * scaleY,
|
||||||
scaleX,
|
scaleX,
|
||||||
|
|||||||
382
src/display/webgpu_mesh.js
Normal file
382
src/display/webgpu_mesh.js
Normal file
@ -0,0 +1,382 @@
|
|||||||
|
/* Copyright 2026 Mozilla Foundation
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MeshFigureType } from "../shared/util.js";
|
||||||
|
|
||||||
|
// WGSL shader for Gouraud-shaded triangle mesh rasterization.
|
||||||
|
// Vertices arrive in PDF content-space coordinates; the vertex shader
|
||||||
|
// applies the affine transform supplied via a uniform buffer to map them
|
||||||
|
// to NDC (X: -1..1 left→right, Y: -1..1 bottom→top).
|
||||||
|
// Colors are delivered as unorm8x4 (r,g,b,_) and passed through as-is.
|
||||||
|
const WGSL = /* wgsl */ `
|
||||||
|
struct Uniforms {
|
||||||
|
offsetX : f32,
|
||||||
|
offsetY : f32,
|
||||||
|
scaleX : f32,
|
||||||
|
scaleY : f32,
|
||||||
|
paddedWidth : f32,
|
||||||
|
paddedHeight : f32,
|
||||||
|
borderSize : f32,
|
||||||
|
_pad : f32,
|
||||||
|
};
|
||||||
|
|
||||||
|
@group(0) @binding(0) var<uniform> u : Uniforms;
|
||||||
|
|
||||||
|
struct VertexInput {
|
||||||
|
@location(0) position : vec2<f32>,
|
||||||
|
@location(1) color : vec4<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct VertexOutput {
|
||||||
|
@builtin(position) position : vec4<f32>,
|
||||||
|
@location(0) color : vec3<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vs_main(in : VertexInput) -> VertexOutput {
|
||||||
|
var out : VertexOutput;
|
||||||
|
let cx = (in.position.x + u.offsetX) * u.scaleX;
|
||||||
|
let cy = (in.position.y + u.offsetY) * u.scaleY;
|
||||||
|
out.position = vec4<f32>(
|
||||||
|
((cx + u.borderSize) / u.paddedWidth) * 2.0 - 1.0,
|
||||||
|
1.0 - ((cy + u.borderSize) / u.paddedHeight) * 2.0,
|
||||||
|
0.0,
|
||||||
|
1.0
|
||||||
|
);
|
||||||
|
out.color = in.color.rgb;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
return vec4<f32>(in.color, 1.0);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
class WebGPUMesh {
|
||||||
|
#initPromise = null;
|
||||||
|
|
||||||
|
#device = null;
|
||||||
|
|
||||||
|
#pipeline = null;
|
||||||
|
|
||||||
|
// Format chosen to match the OffscreenCanvas swapchain on this device.
|
||||||
|
#preferredFormat = null;
|
||||||
|
|
||||||
|
async #initGPU() {
|
||||||
|
if (!globalThis.navigator?.gpu) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const adapter = await navigator.gpu.requestAdapter();
|
||||||
|
if (!adapter) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.#preferredFormat = navigator.gpu.getPreferredCanvasFormat();
|
||||||
|
const device = (this.#device = await adapter.requestDevice());
|
||||||
|
const shaderModule = device.createShaderModule({ code: WGSL });
|
||||||
|
|
||||||
|
this.#pipeline = device.createRenderPipeline({
|
||||||
|
layout: "auto",
|
||||||
|
vertex: {
|
||||||
|
module: shaderModule,
|
||||||
|
entryPoint: "vs_main",
|
||||||
|
buffers: [
|
||||||
|
{
|
||||||
|
// Buffer 0: PDF content-space coords, 2 × float32 per vertex.
|
||||||
|
arrayStride: 2 * 4,
|
||||||
|
attributes: [
|
||||||
|
{ shaderLocation: 0, offset: 0, format: "float32x2" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Buffer 1: vertex colors, 4 × unorm8 per vertex (r, g, b, _).
|
||||||
|
arrayStride: 4,
|
||||||
|
attributes: [
|
||||||
|
{ shaderLocation: 1, offset: 0, format: "unorm8x4" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
fragment: {
|
||||||
|
module: shaderModule,
|
||||||
|
entryPoint: "fs_main",
|
||||||
|
// Use the canvas-preferred format so the OffscreenCanvas swapchain
|
||||||
|
// and the pipeline output format always agree.
|
||||||
|
targets: [{ format: this.#preferredFormat }],
|
||||||
|
},
|
||||||
|
primitive: { topology: "triangle-list" },
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
if (this.#initPromise === null) {
|
||||||
|
this.#initPromise = this.#initGPU();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get isReady() {
|
||||||
|
return this.#device !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build flat Float32Array (positions) and Uint8Array (colors) vertex
|
||||||
|
* streams for non-indexed triangle-list rendering.
|
||||||
|
*
|
||||||
|
* Coords and colors intentionally use separate lookup indices. For patch
|
||||||
|
* mesh figures (types 6/7 converted to LATTICE in the worker), the coord
|
||||||
|
* index-space and color index-space differ because the stream interleaves
|
||||||
|
* them at different densities (12 coords but 4 colors per flag-0 patch).
|
||||||
|
* A single shared index buffer cannot represent both simultaneously, so we
|
||||||
|
* expand each triangle vertex individually into the two flat streams.
|
||||||
|
*
|
||||||
|
* @param {Array} figures
|
||||||
|
* @param {Object} context coords/colors/offsetX/offsetY/scaleX/scaleY
|
||||||
|
* @returns {{ posData: Float32Array, colData: Uint8Array,
|
||||||
|
* vertexCount: number }}
|
||||||
|
*/
|
||||||
|
#buildVertexStreams(figures, context) {
|
||||||
|
const { coords, colors } = context;
|
||||||
|
|
||||||
|
// Count vertices first so we can allocate the typed arrays exactly once.
|
||||||
|
let vertexCount = 0;
|
||||||
|
for (const figure of figures) {
|
||||||
|
const ps = figure.coords;
|
||||||
|
if (figure.type === MeshFigureType.TRIANGLES) {
|
||||||
|
vertexCount += ps.length;
|
||||||
|
} else if (figure.type === MeshFigureType.LATTICE) {
|
||||||
|
const vpr = figure.verticesPerRow;
|
||||||
|
// 2 triangles × 3 vertices per quad cell
|
||||||
|
vertexCount += (Math.floor(ps.length / vpr) - 1) * (vpr - 1) * 6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// posData: 2 × float32 per vertex (raw PDF content-space x, y).
|
||||||
|
// colData: 4 × uint8 per vertex (r, g, b, unused — required by unorm8x4).
|
||||||
|
const posData = new Float32Array(vertexCount * 2);
|
||||||
|
const colData = new Uint8Array(vertexCount * 4);
|
||||||
|
let pOff = 0,
|
||||||
|
cOff = 0;
|
||||||
|
|
||||||
|
// pi and ci are raw vertex indices; coords is stride-2, colors stride-4.
|
||||||
|
const addVertex = (pi, ci) => {
|
||||||
|
posData[pOff++] = coords[pi * 2];
|
||||||
|
posData[pOff++] = coords[pi * 2 + 1];
|
||||||
|
colData[cOff++] = colors[ci * 4];
|
||||||
|
colData[cOff++] = colors[ci * 4 + 1];
|
||||||
|
colData[cOff++] = colors[ci * 4 + 2];
|
||||||
|
cOff++; // alpha channel — unused in the fragment shader
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const figure of figures) {
|
||||||
|
const ps = figure.coords;
|
||||||
|
const cs = figure.colors;
|
||||||
|
if (figure.type === MeshFigureType.TRIANGLES) {
|
||||||
|
for (let i = 0, ii = ps.length; i < ii; i += 3) {
|
||||||
|
addVertex(ps[i], cs[i]);
|
||||||
|
addVertex(ps[i + 1], cs[i + 1]);
|
||||||
|
addVertex(ps[i + 2], cs[i + 2]);
|
||||||
|
}
|
||||||
|
} else if (figure.type === MeshFigureType.LATTICE) {
|
||||||
|
const vpr = figure.verticesPerRow;
|
||||||
|
const rows = Math.floor(ps.length / vpr) - 1;
|
||||||
|
const cols = vpr - 1;
|
||||||
|
for (let i = 0; i < rows; i++) {
|
||||||
|
let q = i * vpr;
|
||||||
|
for (let j = 0; j < cols; j++, q++) {
|
||||||
|
// Upper-left triangle: q, q+1, q+vpr
|
||||||
|
addVertex(ps[q], cs[q]);
|
||||||
|
addVertex(ps[q + 1], cs[q + 1]);
|
||||||
|
addVertex(ps[q + vpr], cs[q + vpr]);
|
||||||
|
// Lower-right triangle: q+vpr+1, q+1, q+vpr
|
||||||
|
addVertex(ps[q + vpr + 1], cs[q + vpr + 1]);
|
||||||
|
addVertex(ps[q + 1], cs[q + 1]);
|
||||||
|
addVertex(ps[q + vpr], cs[q + vpr]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { posData, colData, vertexCount };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a mesh shading to an ImageBitmap using WebGPU.
|
||||||
|
*
|
||||||
|
* Two flat vertex streams (positions and colors) are uploaded from the
|
||||||
|
* packed IR typed arrays. A uniform buffer carries the affine transform
|
||||||
|
* so the vertex shader maps PDF content-space coordinates to NDC without
|
||||||
|
* any CPU arithmetic per vertex.
|
||||||
|
*
|
||||||
|
* After `device.queue.submit()`, `transferToImageBitmap()` presents the
|
||||||
|
* current GPU frame synchronously – the browser ensures all submitted GPU
|
||||||
|
* commands are complete before returning. The resulting ImageBitmap stays
|
||||||
|
* GPU-resident; `ctx2d.drawImage(bitmap)` is a zero-copy GPU-to-GPU blit.
|
||||||
|
*
|
||||||
|
* The GPU device must already be initialized (`this.isReady === true`).
|
||||||
|
*
|
||||||
|
* @param {Array} figures
|
||||||
|
* @param {Object} context coords/colors/offsetX/offsetY/…
|
||||||
|
* @param {Uint8Array|null} backgroundColor [r,g,b] or null for transparent
|
||||||
|
* @param {number} paddedWidth render-target width
|
||||||
|
* @param {number} paddedHeight render-target height
|
||||||
|
* @param {number} borderSize transparent border size in pixels
|
||||||
|
* @returns {ImageBitmap}
|
||||||
|
*/
|
||||||
|
draw(
|
||||||
|
figures,
|
||||||
|
context,
|
||||||
|
backgroundColor,
|
||||||
|
paddedWidth,
|
||||||
|
paddedHeight,
|
||||||
|
borderSize
|
||||||
|
) {
|
||||||
|
const device = this.#device;
|
||||||
|
const { offsetX, offsetY, scaleX, scaleY } = context;
|
||||||
|
const { posData, colData, vertexCount } = this.#buildVertexStreams(
|
||||||
|
figures,
|
||||||
|
context
|
||||||
|
);
|
||||||
|
|
||||||
|
// Upload vertex positions (raw PDF coords) and colors as separate buffers.
|
||||||
|
// GPUBufferUsage requires size > 0.
|
||||||
|
const posBuffer = device.createBuffer({
|
||||||
|
size: Math.max(posData.byteLength, 4),
|
||||||
|
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
||||||
|
});
|
||||||
|
if (posData.byteLength > 0) {
|
||||||
|
device.queue.writeBuffer(posBuffer, 0, posData);
|
||||||
|
}
|
||||||
|
|
||||||
|
const colBuffer = device.createBuffer({
|
||||||
|
size: Math.max(colData.byteLength, 4),
|
||||||
|
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
||||||
|
});
|
||||||
|
if (colData.byteLength > 0) {
|
||||||
|
device.queue.writeBuffer(colBuffer, 0, colData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uniform buffer: affine transform parameters for the vertex shader.
|
||||||
|
const uniformBuffer = device.createBuffer({
|
||||||
|
size: 8 * 4, // 8 × float32 = 32 bytes
|
||||||
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
||||||
|
});
|
||||||
|
device.queue.writeBuffer(
|
||||||
|
uniformBuffer,
|
||||||
|
0,
|
||||||
|
new Float32Array([
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
scaleX,
|
||||||
|
scaleY,
|
||||||
|
paddedWidth,
|
||||||
|
paddedHeight,
|
||||||
|
borderSize,
|
||||||
|
0, // padding to 32 bytes
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
const bindGroup = device.createBindGroup({
|
||||||
|
layout: this.#pipeline.getBindGroupLayout(0),
|
||||||
|
entries: [{ binding: 0, resource: { buffer: uniformBuffer } }],
|
||||||
|
});
|
||||||
|
|
||||||
|
// The canvas covers the full padded area so the border is naturally clear.
|
||||||
|
const offscreen = new OffscreenCanvas(paddedWidth, paddedHeight);
|
||||||
|
const gpuCtx = offscreen.getContext("webgpu");
|
||||||
|
gpuCtx.configure({
|
||||||
|
device,
|
||||||
|
format: this.#preferredFormat,
|
||||||
|
// "premultiplied" allows fully transparent border pixels when there is
|
||||||
|
// no backgroundColor; "opaque" is slightly more efficient otherwise.
|
||||||
|
alphaMode: backgroundColor ? "opaque" : "premultiplied",
|
||||||
|
});
|
||||||
|
|
||||||
|
const clearValue = backgroundColor
|
||||||
|
? {
|
||||||
|
r: backgroundColor[0] / 255,
|
||||||
|
g: backgroundColor[1] / 255,
|
||||||
|
b: backgroundColor[2] / 255,
|
||||||
|
a: 1,
|
||||||
|
}
|
||||||
|
: { r: 0, g: 0, b: 0, a: 0 };
|
||||||
|
|
||||||
|
const commandEncoder = device.createCommandEncoder();
|
||||||
|
const renderPass = commandEncoder.beginRenderPass({
|
||||||
|
colorAttachments: [
|
||||||
|
{
|
||||||
|
view: gpuCtx.getCurrentTexture().createView(),
|
||||||
|
clearValue,
|
||||||
|
loadOp: "clear",
|
||||||
|
storeOp: "store",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
if (vertexCount > 0) {
|
||||||
|
renderPass.setPipeline(this.#pipeline);
|
||||||
|
renderPass.setBindGroup(0, bindGroup);
|
||||||
|
renderPass.setVertexBuffer(0, posBuffer);
|
||||||
|
renderPass.setVertexBuffer(1, colBuffer);
|
||||||
|
renderPass.draw(vertexCount);
|
||||||
|
}
|
||||||
|
renderPass.end();
|
||||||
|
|
||||||
|
device.queue.submit([commandEncoder.finish()]);
|
||||||
|
posBuffer.destroy();
|
||||||
|
colBuffer.destroy();
|
||||||
|
uniformBuffer.destroy();
|
||||||
|
|
||||||
|
// Present the current GPU frame and capture it as an ImageBitmap.
|
||||||
|
// The browser flushes all pending GPU commands before returning, so this
|
||||||
|
// is synchronous from the JavaScript perspective. The ImageBitmap is
|
||||||
|
// GPU-resident; drawing it onto a 2D canvas is a GPU-to-GPU blit.
|
||||||
|
return offscreen.transferToImageBitmap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const _webGPUMesh = new WebGPUMesh();
|
||||||
|
|
||||||
|
function initWebGPUMesh() {
|
||||||
|
_webGPUMesh.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWebGPUMeshReady() {
|
||||||
|
return _webGPUMesh.isReady;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawMeshWithGPU(
|
||||||
|
figures,
|
||||||
|
context,
|
||||||
|
backgroundColor,
|
||||||
|
paddedWidth,
|
||||||
|
paddedHeight,
|
||||||
|
borderSize
|
||||||
|
) {
|
||||||
|
return _webGPUMesh.draw(
|
||||||
|
figures,
|
||||||
|
context,
|
||||||
|
backgroundColor,
|
||||||
|
paddedWidth,
|
||||||
|
paddedHeight,
|
||||||
|
borderSize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { drawMeshWithGPU, initWebGPUMesh, isWebGPUMeshReady };
|
||||||
@ -460,6 +460,11 @@ const defaultOptions = {
|
|||||||
value: typeof PDFJSDev !== "undefined" && !PDFJSDev.test("MOZCENTRAL"),
|
value: typeof PDFJSDev !== "undefined" && !PDFJSDev.test("MOZCENTRAL"),
|
||||||
kind: OptionKind.API + OptionKind.PREFERENCE,
|
kind: OptionKind.API + OptionKind.PREFERENCE,
|
||||||
},
|
},
|
||||||
|
enableWebGPU: {
|
||||||
|
/** @type {boolean} */
|
||||||
|
value: true,
|
||||||
|
kind: OptionKind.API + OptionKind.PREFERENCE,
|
||||||
|
},
|
||||||
enableXfa: {
|
enableXfa: {
|
||||||
/** @type {boolean} */
|
/** @type {boolean} */
|
||||||
value: true,
|
value: true,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user