Merge pull request #21049 from calixteman/load_gpu

Unconditionally create a gpu device
This commit is contained in:
calixteman 2026-04-06 14:14:22 +02:00 committed by GitHub
commit 6ac79d05d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 97 additions and 89 deletions

View File

@ -105,7 +105,7 @@ const DefaultPartialEvaluatorOptions = Object.freeze({
iccUrl: null, iccUrl: null,
standardFontDataUrl: null, standardFontDataUrl: null,
wasmUrl: null, wasmUrl: null,
prepareWebGPU: null, hasGPU: false,
}); });
const PatternType = { const PatternType = {
@ -1520,8 +1520,7 @@ 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) {

View File

@ -46,18 +46,24 @@ const ShadingType = {
}; };
class Pattern { class Pattern {
// eslint-disable-next-line no-unused-private-class-members
static #hasGPU = false;
constructor() { constructor() {
unreachable("Cannot initialize Pattern."); unreachable("Cannot initialize Pattern.");
} }
static setOptions({ hasGPU }) {
this.#hasGPU = hasGPU;
}
static parseShading( static parseShading(
shading, shading,
xref, xref,
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");
@ -65,7 +71,6 @@ class Pattern {
try { try {
switch (type) { switch (type) {
case ShadingType.FUNCTION_BASED: case ShadingType.FUNCTION_BASED:
prepareWebGPU?.();
return new FunctionBasedShading( return new FunctionBasedShading(
dict, dict,
xref, xref,
@ -88,7 +93,6 @@ 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,

View File

@ -27,6 +27,7 @@ import { JpegStream } from "./jpeg_stream.js";
import { JpxImage } from "./jpx.js"; import { JpxImage } from "./jpx.js";
import { MissingDataException } from "./core_utils.js"; import { MissingDataException } from "./core_utils.js";
import { OperatorList } from "./operator_list.js"; import { OperatorList } from "./operator_list.js";
import { Pattern } from "./pattern.js";
import { PDFDocument } from "./document.js"; import { PDFDocument } from "./document.js";
import { PDFFunctionFactory } from "./function.js"; import { PDFFunctionFactory } from "./function.js";
import { Stream } from "./stream.js"; import { Stream } from "./stream.js";
@ -73,19 +74,6 @@ class BasePdfManager {
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.
@ -99,6 +87,7 @@ class BasePdfManager {
CmykICCBasedCS.setOptions(options); CmykICCBasedCS.setOptions(options);
JBig2CCITTFaxWasmImage.setOptions(options); JBig2CCITTFaxWasmImage.setOptions(options);
PDFFunctionFactory.setOptions(options); PDFFunctionFactory.setOptions(options);
Pattern.setOptions(options);
} }
get docId() { get docId() {

View File

@ -75,7 +75,7 @@ import { DOMCanvasFactory } from "./canvas_factory.js";
import { DOMFilterFactory } from "./filter_factory.js"; import { DOMFilterFactory } from "./filter_factory.js";
import { getNetworkStream } from "display-network_stream"; import { getNetworkStream } from "display-network_stream";
import { GlobalWorkerOptions } from "./worker_options.js"; 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 { MathClamp } from "../shared/math_clamp.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";
@ -323,6 +323,9 @@ function getDocument(src = {}) {
: DOMBinaryDataFactory); : DOMBinaryDataFactory);
const enableHWA = src.enableHWA === true; const enableHWA = src.enableHWA === true;
const enableWebGPU = src.enableWebGPU === 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 useWasm = src.useWasm !== false;
const pagesMapper = src.pagesMapper || new PagesMapper(); const pagesMapper = src.pagesMapper || new PagesMapper();
@ -405,7 +408,7 @@ function getDocument(src = {}) {
iccUrl, iccUrl,
standardFontDataUrl, standardFontDataUrl,
wasmUrl, wasmUrl,
enableWebGPU, hasGPU: false, // Set below.
}, },
}; };
const transportParams = { const transportParams = {
@ -419,8 +422,8 @@ function getDocument(src = {}) {
}, },
}; };
worker.promise Promise.all([worker.promise, gpuPromise])
.then(function () { .then(function ([, hasGPU]) {
if (task.destroyed) { if (task.destroyed) {
throw new Error("Loading aborted"); throw new Error("Loading aborted");
} }
@ -428,6 +431,8 @@ function getDocument(src = {}) {
throw new Error("Worker was destroyed"); throw new Error("Worker was destroyed");
} }
docParams.evaluatorOptions.hasGPU = hasGPU;
const workerIdPromise = worker.messageHandler.sendWithPromise( const workerIdPromise = worker.messageHandler.sendWithPromise(
"GetDocRequest", "GetDocRequest",
docParams, docParams,
@ -2843,13 +2848,6 @@ 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) {

View File

@ -13,7 +13,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { drawMeshWithGPU, isWebGPUMeshReady } from "./webgpu_mesh.js"; import { drawMeshWithGPU, isGPUReady, loadMeshShader } from "./webgpu.js";
import { import {
FormatError, FormatError,
info, info,
@ -441,6 +441,9 @@ class MeshShadingPattern extends BaseShadingPattern {
this._bbox = IR[6]; this._bbox = IR[6];
this._background = IR[7]; this._background = IR[7];
this.matrix = null; 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) { _createMeshCanvas(combinedScale, backgroundColor, canvasFactory) {
@ -486,7 +489,7 @@ class MeshShadingPattern extends BaseShadingPattern {
const paddedHeight = height + BORDER_SIZE * 2; const paddedHeight = height + BORDER_SIZE * 2;
const tmpCanvas = canvasFactory.create(paddedWidth, paddedHeight); const tmpCanvas = canvasFactory.create(paddedWidth, paddedHeight);
if (isWebGPUMeshReady()) { if (isGPUReady()) {
tmpCanvas.context.drawImage( tmpCanvas.context.drawImage(
drawMeshWithGPU( drawMeshWithGPU(
this._figures, this._figures,

View File

@ -20,7 +20,7 @@ import { MeshFigureType } from "../shared/util.js";
// applies the affine transform supplied via a uniform buffer to map them // applies the affine transform supplied via a uniform buffer to map them
// to NDC (X: -1..1 left→right, Y: -1..1 bottom→top). // 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. // Colors are delivered as unorm8x4 (r,g,b,_) and passed through as-is.
const WGSL = /* wgsl */ ` const MESH_WGSL = /* wgsl */ `
struct Uniforms { struct Uniforms {
offsetX : f32, offsetX : f32,
offsetY : f32, offsetY : f32,
@ -65,12 +65,12 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
} }
`; `;
class WebGPUMesh { class WebGPU {
#initPromise = null; #initPromise = null;
#device = null; #device = null;
#pipeline = null; #meshPipeline = null;
// Format chosen to match the OffscreenCanvas swapchain on this device. // Format chosen to match the OffscreenCanvas swapchain on this device.
#preferredFormat = null; #preferredFormat = null;
@ -85,57 +85,62 @@ class WebGPUMesh {
return false; return false;
} }
this.#preferredFormat = navigator.gpu.getPreferredCanvasFormat(); this.#preferredFormat = navigator.gpu.getPreferredCanvasFormat();
const device = (this.#device = await adapter.requestDevice()); 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; return true;
} catch { } catch {
return false; return false;
} }
} }
/**
* Start GPU initialization.
* @returns {Promise<boolean>} true when a GPU device is available.
*/
init() { init() {
if (this.#initPromise === null) { return (this.#initPromise ||= this.#initGPU());
this.#initPromise = this.#initGPU();
}
} }
get isReady() { get isReady() {
return this.#device !== null; 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 * Build flat Float32Array (positions) and Uint8Array (colors) vertex
* streams for non-indexed triangle-list rendering. * streams for non-indexed triangle-list rendering.
@ -248,6 +253,9 @@ class WebGPUMesh {
paddedHeight, paddedHeight,
borderSize borderSize
) { ) {
// Lazily compile the mesh pipeline the first time we need to draw.
this.loadMeshShader();
const device = this.#device; const device = this.#device;
const { offsetX, offsetY, scaleX, scaleY } = context; const { offsetX, offsetY, scaleX, scaleY } = context;
const { posData, colData, vertexCount } = this.#buildVertexStreams( const { posData, colData, vertexCount } = this.#buildVertexStreams(
@ -294,7 +302,7 @@ class WebGPUMesh {
); );
const bindGroup = device.createBindGroup({ const bindGroup = device.createBindGroup({
layout: this.#pipeline.getBindGroupLayout(0), layout: this.#meshPipeline.getBindGroupLayout(0),
entries: [{ binding: 0, resource: { buffer: uniformBuffer } }], entries: [{ binding: 0, resource: { buffer: uniformBuffer } }],
}); });
@ -330,7 +338,7 @@ class WebGPUMesh {
], ],
}); });
if (vertexCount > 0) { if (vertexCount > 0) {
renderPass.setPipeline(this.#pipeline); renderPass.setPipeline(this.#meshPipeline);
renderPass.setBindGroup(0, bindGroup); renderPass.setBindGroup(0, bindGroup);
renderPass.setVertexBuffer(0, posBuffer); renderPass.setVertexBuffer(0, posBuffer);
renderPass.setVertexBuffer(1, colBuffer); 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<boolean>} true if a GPU device was acquired.
*/
function initGPU() {
return _webGPU.init();
} }
function isWebGPUMeshReady() { function isGPUReady() {
return _webGPUMesh.isReady; return _webGPU.isReady;
}
/**
* Pre-compile the Gouraud-mesh WGSL pipeline.
*/
function loadMeshShader() {
_webGPU.loadMeshShader();
} }
function drawMeshWithGPU( function drawMeshWithGPU(
@ -369,7 +388,7 @@ function drawMeshWithGPU(
paddedHeight, paddedHeight,
borderSize borderSize
) { ) {
return _webGPUMesh.draw( return _webGPU.draw(
figures, figures,
context, context,
backgroundColor, backgroundColor,
@ -379,4 +398,4 @@ function drawMeshWithGPU(
); );
} }
export { drawMeshWithGPU, initWebGPUMesh, isWebGPUMeshReady }; export { drawMeshWithGPU, initGPU, isGPUReady, loadMeshShader };

View File

@ -197,12 +197,8 @@ describe("api", function () {
expect(loadingTask).toBeInstanceOf(PDFDocumentLoadingTask); expect(loadingTask).toBeInstanceOf(PDFDocumentLoadingTask);
// This can be somewhat random -- we cannot guarantee perfect // This can be somewhat random -- we cannot guarantee perfect
// 'Terminate' message to the worker before/after setting up pdfManager. // 'Terminate' message to the worker before/after setting up pdfManager.
const destroyed = loadingTask._worker.promise.then(() => await loadingTask._worker.promise.then(() => loadingTask.destroy());
loadingTask.destroy() await loadingTask.promise.catch(() => {});
);
await destroyed;
expect(true).toEqual(true);
}); });
it("creates pdf doc from TypedArray", async function () { it("creates pdf doc from TypedArray", async function () {