diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 772bd730d..7126a604b 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -105,7 +105,7 @@ const DefaultPartialEvaluatorOptions = Object.freeze({ iccUrl: null, standardFontDataUrl: null, wasmUrl: null, - prepareWebGPU: null, + hasGPU: false, }); const PatternType = { @@ -1520,8 +1520,7 @@ class PartialEvaluator { resources, this._pdfFunctionFactory, this.globalColorSpaceCache, - localColorSpaceCache, - this.options.prepareWebGPU + localColorSpaceCache ); patternIR = shadingFill.getIR(); } catch (reason) { diff --git a/src/core/pattern.js b/src/core/pattern.js index cb7fff730..9c9a9a4ef 100644 --- a/src/core/pattern.js +++ b/src/core/pattern.js @@ -46,18 +46,24 @@ const ShadingType = { }; class Pattern { + // eslint-disable-next-line no-unused-private-class-members + static #hasGPU = false; + constructor() { unreachable("Cannot initialize Pattern."); } + static setOptions({ hasGPU }) { + this.#hasGPU = hasGPU; + } + static parseShading( shading, xref, res, pdfFunctionFactory, globalColorSpaceCache, - localColorSpaceCache, - prepareWebGPU = null + localColorSpaceCache ) { const dict = shading instanceof BaseStream ? shading.dict : shading; const type = dict.get("ShadingType"); @@ -65,7 +71,6 @@ class Pattern { try { switch (type) { case ShadingType.FUNCTION_BASED: - prepareWebGPU?.(); return new FunctionBasedShading( dict, xref, @@ -88,7 +93,6 @@ class Pattern { case ShadingType.LATTICE_FORM_MESH: case ShadingType.COONS_PATCH_MESH: case ShadingType.TENSOR_PATCH_MESH: - prepareWebGPU?.(); return new MeshShading( shading, xref, diff --git a/src/core/pdf_manager.js b/src/core/pdf_manager.js index dfb8c4432..9c47fcec2 100644 --- a/src/core/pdf_manager.js +++ b/src/core/pdf_manager.js @@ -27,6 +27,7 @@ import { JpegStream } from "./jpeg_stream.js"; import { JpxImage } from "./jpx.js"; import { MissingDataException } from "./core_utils.js"; import { OperatorList } from "./operator_list.js"; +import { Pattern } from "./pattern.js"; import { PDFDocument } from "./document.js"; import { PDFFunctionFactory } from "./function.js"; import { Stream } from "./stream.js"; @@ -73,19 +74,6 @@ class BasePdfManager { evaluatorOptions.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); // Initialize image-options once per document. @@ -99,6 +87,7 @@ class BasePdfManager { CmykICCBasedCS.setOptions(options); JBig2CCITTFaxWasmImage.setOptions(options); PDFFunctionFactory.setOptions(options); + Pattern.setOptions(options); } get docId() { diff --git a/src/display/api.js b/src/display/api.js index 2a1790f3d..b62b502e6 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -75,7 +75,7 @@ import { DOMCanvasFactory } from "./canvas_factory.js"; import { DOMFilterFactory } from "./filter_factory.js"; import { getNetworkStream } from "display-network_stream"; import { GlobalWorkerOptions } from "./worker_options.js"; -import { initWebGPUMesh } from "./webgpu_mesh.js"; +import { initGPU } from "./webgpu.js"; import { MathClamp } from "../shared/math_clamp.js"; import { Metadata } from "./metadata.js"; import { OptionalContentConfig } from "./optional_content_config.js"; @@ -323,6 +323,9 @@ function getDocument(src = {}) { : DOMBinaryDataFactory); const enableHWA = src.enableHWA === true; const enableWebGPU = src.enableWebGPU === true; + // Start GPU initialisation immediately so it runs in parallel with the + // worker bootstrap; the resolved boolean is forwarded to the worker. + const gpuPromise = enableWebGPU ? initGPU() : Promise.resolve(false); const useWasm = src.useWasm !== false; const pagesMapper = src.pagesMapper || new PagesMapper(); @@ -405,7 +408,7 @@ function getDocument(src = {}) { iccUrl, standardFontDataUrl, wasmUrl, - enableWebGPU, + hasGPU: false, // Set below. }, }; const transportParams = { @@ -419,8 +422,8 @@ function getDocument(src = {}) { }, }; - worker.promise - .then(function () { + Promise.all([worker.promise, gpuPromise]) + .then(function ([, hasGPU]) { if (task.destroyed) { throw new Error("Loading aborted"); } @@ -428,6 +431,8 @@ function getDocument(src = {}) { throw new Error("Worker was destroyed"); } + docParams.evaluatorOptions.hasGPU = hasGPU; + const workerIdPromise = worker.messageHandler.sendWithPromise( "GetDocRequest", docParams, @@ -2843,13 +2848,6 @@ class WorkerTransport { this.#onProgress(data); }); - messageHandler.on("PrepareWebGPU", () => { - if (this.destroyed) { - return; - } - initWebGPUMesh(); - }); - if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) { messageHandler.on("FetchBinaryData", async data => { if (this.destroyed) { diff --git a/src/display/pattern_helper.js b/src/display/pattern_helper.js index df4f76869..9f4e5ca04 100644 --- a/src/display/pattern_helper.js +++ b/src/display/pattern_helper.js @@ -13,7 +13,7 @@ * limitations under the License. */ -import { drawMeshWithGPU, isWebGPUMeshReady } from "./webgpu_mesh.js"; +import { drawMeshWithGPU, isGPUReady, loadMeshShader } from "./webgpu.js"; import { FormatError, info, @@ -441,6 +441,9 @@ class MeshShadingPattern extends BaseShadingPattern { this._bbox = IR[6]; this._background = IR[7]; this.matrix = null; + // Pre-compile the mesh pipeline now that we know GPU-renderable content + // is present; no-op if the GPU is not available or already compiled. + loadMeshShader(); } _createMeshCanvas(combinedScale, backgroundColor, canvasFactory) { @@ -486,7 +489,7 @@ class MeshShadingPattern extends BaseShadingPattern { const paddedHeight = height + BORDER_SIZE * 2; const tmpCanvas = canvasFactory.create(paddedWidth, paddedHeight); - if (isWebGPUMeshReady()) { + if (isGPUReady()) { tmpCanvas.context.drawImage( drawMeshWithGPU( this._figures, diff --git a/src/display/webgpu_mesh.js b/src/display/webgpu.js similarity index 82% rename from src/display/webgpu_mesh.js rename to src/display/webgpu.js index 17a1ef8f6..7ecb2820d 100644 --- a/src/display/webgpu_mesh.js +++ b/src/display/webgpu.js @@ -20,7 +20,7 @@ import { MeshFigureType } from "../shared/util.js"; // 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 */ ` +const MESH_WGSL = /* wgsl */ ` struct Uniforms { offsetX : f32, offsetY : f32, @@ -65,12 +65,12 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4 { } `; -class WebGPUMesh { +class WebGPU { #initPromise = null; #device = null; - #pipeline = null; + #meshPipeline = null; // Format chosen to match the OffscreenCanvas swapchain on this device. #preferredFormat = null; @@ -85,57 +85,62 @@ class WebGPUMesh { 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" }, - }); - + this.#device = await adapter.requestDevice(); return true; } catch { return false; } } + /** + * Start GPU initialization. + * @returns {Promise} true when a GPU device is available. + */ init() { - if (this.#initPromise === null) { - this.#initPromise = this.#initGPU(); - } + return (this.#initPromise ||= this.#initGPU()); } get isReady() { return this.#device !== null; } + /** + * Compile (and cache) the Gouraud-mesh pipeline. + */ + loadMeshShader() { + if (!this.#device || this.#meshPipeline) { + return; + } + const shaderModule = this.#device.createShaderModule({ code: MESH_WGSL }); + this.#meshPipeline = this.#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" }, + }); + } + /** * Build flat Float32Array (positions) and Uint8Array (colors) vertex * streams for non-indexed triangle-list rendering. @@ -248,6 +253,9 @@ class WebGPUMesh { paddedHeight, borderSize ) { + // Lazily compile the mesh pipeline the first time we need to draw. + this.loadMeshShader(); + const device = this.#device; const { offsetX, offsetY, scaleX, scaleY } = context; const { posData, colData, vertexCount } = this.#buildVertexStreams( @@ -294,7 +302,7 @@ class WebGPUMesh { ); const bindGroup = device.createBindGroup({ - layout: this.#pipeline.getBindGroupLayout(0), + layout: this.#meshPipeline.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: uniformBuffer } }], }); @@ -330,7 +338,7 @@ class WebGPUMesh { ], }); if (vertexCount > 0) { - renderPass.setPipeline(this.#pipeline); + renderPass.setPipeline(this.#meshPipeline); renderPass.setBindGroup(0, bindGroup); renderPass.setVertexBuffer(0, posBuffer); renderPass.setVertexBuffer(1, colBuffer); @@ -351,14 +359,25 @@ class WebGPUMesh { } } -const _webGPUMesh = new WebGPUMesh(); +const _webGPU = new WebGPU(); -function initWebGPUMesh() { - _webGPUMesh.init(); +/** + * Start GPU initialization as early as possible. + * @returns {Promise} true if a GPU device was acquired. + */ +function initGPU() { + return _webGPU.init(); } -function isWebGPUMeshReady() { - return _webGPUMesh.isReady; +function isGPUReady() { + return _webGPU.isReady; +} + +/** + * Pre-compile the Gouraud-mesh WGSL pipeline. + */ +function loadMeshShader() { + _webGPU.loadMeshShader(); } function drawMeshWithGPU( @@ -369,7 +388,7 @@ function drawMeshWithGPU( paddedHeight, borderSize ) { - return _webGPUMesh.draw( + return _webGPU.draw( figures, context, backgroundColor, @@ -379,4 +398,4 @@ function drawMeshWithGPU( ); } -export { drawMeshWithGPU, initWebGPUMesh, isWebGPUMeshReady }; +export { drawMeshWithGPU, initGPU, isGPUReady, loadMeshShader }; diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index 27ca2ed5c..0795f9f34 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -197,12 +197,8 @@ describe("api", function () { expect(loadingTask).toBeInstanceOf(PDFDocumentLoadingTask); // This can be somewhat random -- we cannot guarantee perfect // 'Terminate' message to the worker before/after setting up pdfManager. - const destroyed = loadingTask._worker.promise.then(() => - loadingTask.destroy() - ); - - await destroyed; - expect(true).toEqual(true); + await loadingTask._worker.promise.then(() => loadingTask.destroy()); + await loadingTask.promise.catch(() => {}); }); it("creates pdf doc from TypedArray", async function () {