Skip to content

Commit 86441e9

Browse files
committed
Implement Gouraud-based shading using WebGPU.
The WebGPU feature hasn't been released yet but it's interesting to see how we can use it in order to speed up the rendering of some objects. This patch allows to render mesh patterns using WebGPU. I didn't see any significant performance improvement on my machine (mac M2) but it may be different on other platforms.
1 parent ab228da commit 86441e9

7 files changed

Lines changed: 475 additions & 44 deletions

File tree

src/core/evaluator.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ const DefaultPartialEvaluatorOptions = Object.freeze({
105105
iccUrl: null,
106106
standardFontDataUrl: null,
107107
wasmUrl: null,
108+
prepareWebGPU: null,
108109
});
109110

110111
const PatternType = {
@@ -1513,7 +1514,8 @@ class PartialEvaluator {
15131514
resources,
15141515
this._pdfFunctionFactory,
15151516
this.globalColorSpaceCache,
1516-
localColorSpaceCache
1517+
localColorSpaceCache,
1518+
this.options.prepareWebGPU
15171519
);
15181520
patternIR = shadingFill.getIR();
15191521
} catch (reason) {

src/core/pattern.js

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ class Pattern {
5555
res,
5656
pdfFunctionFactory,
5757
globalColorSpaceCache,
58-
localColorSpaceCache
58+
localColorSpaceCache,
59+
prepareWebGPU = null
5960
) {
6061
const dict = shading instanceof BaseStream ? shading.dict : shading;
6162
const type = dict.get("ShadingType");
@@ -76,6 +77,7 @@ class Pattern {
7677
case ShadingType.LATTICE_FORM_MESH:
7778
case ShadingType.COONS_PATCH_MESH:
7879
case ShadingType.TENSOR_PATCH_MESH:
80+
prepareWebGPU?.();
7981
return new MeshShading(
8082
shading,
8183
xref,
@@ -934,7 +936,7 @@ class MeshShading extends BaseShading {
934936
}
935937

936938
_packData() {
937-
let i, ii, j, jj;
939+
let i, ii, j;
938940

939941
const coords = this.coords;
940942
const coordsPacked = new Float32Array(coords.length * 2);
@@ -945,25 +947,27 @@ class MeshShading extends BaseShading {
945947
}
946948
this.coords = coordsPacked;
947949

950+
// Stride 4 (RGBA layout, alpha unused) so the buffer maps directly to
951+
// array<u32> in the WebGPU vertex shader without any repacking.
948952
const colors = this.colors;
949-
const colorsPacked = new Uint8Array(colors.length * 3);
953+
const colorsPacked = new Uint8Array(colors.length * 4);
950954
for (i = 0, j = 0, ii = colors.length; i < ii; i++) {
951955
const c = colors[i];
952956
colorsPacked[j++] = c[0];
953957
colorsPacked[j++] = c[1];
954958
colorsPacked[j++] = c[2];
959+
j++; // alpha — unused, stays 0
955960
}
956961
this.colors = colorsPacked;
957962

963+
// Store raw vertex indices (not byte offsets) so the GPU shader can
964+
// address coords / colors without knowing their strides, and so the
965+
// arrays are transferable Uint32Arrays.
958966
const figures = this.figures;
959967
for (i = 0, ii = figures.length; i < ii; i++) {
960-
const figure = figures[i],
961-
ps = figure.coords,
962-
cs = figure.colors;
963-
for (j = 0, jj = ps.length; j < jj; j++) {
964-
ps[j] *= 2;
965-
cs[j] *= 3;
966-
}
968+
const figure = figures[i];
969+
figure.coords = new Uint32Array(figure.coords);
970+
figure.colors = new Uint32Array(figure.colors);
967971
}
968972
}
969973

src/core/pdf_manager.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,20 @@ class BasePdfManager {
7171
FeatureTest.isOffscreenCanvasSupported;
7272
evaluatorOptions.isImageDecoderSupported &&=
7373
FeatureTest.isImageDecoderSupported;
74+
75+
// Set up a one-shot callback so evaluators can notify the main thread that
76+
// WebGPU-acceleratable content was found. The flag ensures the message is
77+
// sent at most once per document.
78+
if (evaluatorOptions.enableWebGPU) {
79+
let prepareWebGPUSent = false;
80+
evaluatorOptions.prepareWebGPU = () => {
81+
if (!prepareWebGPUSent) {
82+
prepareWebGPUSent = true;
83+
handler.send("PrepareWebGPU", null);
84+
}
85+
};
86+
}
87+
delete evaluatorOptions.enableWebGPU;
7488
this.evaluatorOptions = Object.freeze(evaluatorOptions);
7589

7690
// Initialize image-options once per document.

src/display/api.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ import { DOMFilterFactory } from "./filter_factory.js";
7979
import { DOMStandardFontDataFactory } from "display-standard_fontdata_factory";
8080
import { DOMWasmFactory } from "display-wasm_factory";
8181
import { GlobalWorkerOptions } from "./worker_options.js";
82+
import { initWebGPUMesh } from "./webgpu_mesh.js";
8283
import { Metadata } from "./metadata.js";
8384
import { OptionalContentConfig } from "./optional_content_config.js";
8485
import { PagesMapper } from "./pages_mapper.js";
@@ -347,6 +348,7 @@ function getDocument(src = {}) {
347348
? NodeFilterFactory
348349
: DOMFilterFactory);
349350
const enableHWA = src.enableHWA === true;
351+
const enableWebGPU = src.enableWebGPU === true;
350352
const useWasm = src.useWasm !== false;
351353
const pagesMapper = src.pagesMapper || new PagesMapper();
352354

@@ -440,6 +442,7 @@ function getDocument(src = {}) {
440442
iccUrl,
441443
standardFontDataUrl,
442444
wasmUrl,
445+
enableWebGPU,
443446
},
444447
};
445448
const transportParams = {
@@ -2926,6 +2929,13 @@ class WorkerTransport {
29262929
this.#onProgress(data);
29272930
});
29282931

2932+
messageHandler.on("PrepareWebGPU", () => {
2933+
if (this.destroyed) {
2934+
return;
2935+
}
2936+
initWebGPUMesh();
2937+
});
2938+
29292939
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
29302940
messageHandler.on("FetchBinaryData", async data => {
29312941
if (this.destroyed) {

src/display/pattern_helper.js

Lines changed: 47 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* limitations under the License.
1414
*/
1515

16+
import { drawMeshWithGPU, isWebGPUMeshReady } from "./webgpu_mesh.js";
1617
import {
1718
FormatError,
1819
info,
@@ -282,48 +283,48 @@ function drawTriangle(data, context, p1, p2, p3, c1, c2, c3) {
282283
const bytes = data.data,
283284
rowSize = data.width * 4;
284285
let tmp;
285-
if (coords[p1 + 1] > coords[p2 + 1]) {
286+
if (coords[p1 * 2 + 1] > coords[p2 * 2 + 1]) {
286287
tmp = p1;
287288
p1 = p2;
288289
p2 = tmp;
289290
tmp = c1;
290291
c1 = c2;
291292
c2 = tmp;
292293
}
293-
if (coords[p2 + 1] > coords[p3 + 1]) {
294+
if (coords[p2 * 2 + 1] > coords[p3 * 2 + 1]) {
294295
tmp = p2;
295296
p2 = p3;
296297
p3 = tmp;
297298
tmp = c2;
298299
c2 = c3;
299300
c3 = tmp;
300301
}
301-
if (coords[p1 + 1] > coords[p2 + 1]) {
302+
if (coords[p1 * 2 + 1] > coords[p2 * 2 + 1]) {
302303
tmp = p1;
303304
p1 = p2;
304305
p2 = tmp;
305306
tmp = c1;
306307
c1 = c2;
307308
c2 = tmp;
308309
}
309-
const x1 = (coords[p1] + context.offsetX) * context.scaleX;
310-
const y1 = (coords[p1 + 1] + context.offsetY) * context.scaleY;
311-
const x2 = (coords[p2] + context.offsetX) * context.scaleX;
312-
const y2 = (coords[p2 + 1] + context.offsetY) * context.scaleY;
313-
const x3 = (coords[p3] + context.offsetX) * context.scaleX;
314-
const y3 = (coords[p3 + 1] + context.offsetY) * context.scaleY;
310+
const x1 = (coords[p1 * 2] + context.offsetX) * context.scaleX;
311+
const y1 = (coords[p1 * 2 + 1] + context.offsetY) * context.scaleY;
312+
const x2 = (coords[p2 * 2] + context.offsetX) * context.scaleX;
313+
const y2 = (coords[p2 * 2 + 1] + context.offsetY) * context.scaleY;
314+
const x3 = (coords[p3 * 2] + context.offsetX) * context.scaleX;
315+
const y3 = (coords[p3 * 2 + 1] + context.offsetY) * context.scaleY;
315316
if (y1 >= y3) {
316317
return;
317318
}
318-
const c1r = colors[c1],
319-
c1g = colors[c1 + 1],
320-
c1b = colors[c1 + 2];
321-
const c2r = colors[c2],
322-
c2g = colors[c2 + 1],
323-
c2b = colors[c2 + 2];
324-
const c3r = colors[c3],
325-
c3g = colors[c3 + 1],
326-
c3b = colors[c3 + 2];
319+
const c1r = colors[c1 * 4],
320+
c1g = colors[c1 * 4 + 1],
321+
c1b = colors[c1 * 4 + 2];
322+
const c2r = colors[c2 * 4],
323+
c2g = colors[c2 * 4 + 1],
324+
c2b = colors[c2 * 4 + 2];
325+
const c3r = colors[c3 * 4],
326+
c3g = colors[c3 * 4 + 1],
327+
c3b = colors[c3 * 4 + 2];
327328

328329
const minY = Math.round(y1),
329330
maxY = Math.round(y3);
@@ -494,26 +495,39 @@ class MeshShadingPattern extends BaseShadingPattern {
494495
paddedWidth,
495496
paddedHeight
496497
);
497-
const tmpCtx = tmpCanvas.context;
498498

499-
const data = tmpCtx.createImageData(width, height);
500-
if (backgroundColor) {
501-
const bytes = data.data;
502-
for (let i = 0, ii = bytes.length; i < ii; i += 4) {
503-
bytes[i] = backgroundColor[0];
504-
bytes[i + 1] = backgroundColor[1];
505-
bytes[i + 2] = backgroundColor[2];
506-
bytes[i + 3] = 255;
499+
if (isWebGPUMeshReady()) {
500+
tmpCanvas.context.drawImage(
501+
drawMeshWithGPU(
502+
this._figures,
503+
context,
504+
backgroundColor,
505+
paddedWidth,
506+
paddedHeight,
507+
BORDER_SIZE
508+
),
509+
0,
510+
0
511+
);
512+
} else {
513+
const data = tmpCanvas.context.createImageData(width, height);
514+
if (backgroundColor) {
515+
const bytes = data.data;
516+
for (let i = 0, ii = bytes.length; i < ii; i += 4) {
517+
bytes[i] = backgroundColor[0];
518+
bytes[i + 1] = backgroundColor[1];
519+
bytes[i + 2] = backgroundColor[2];
520+
bytes[i + 3] = 255;
521+
}
507522
}
523+
for (const figure of this._figures) {
524+
drawFigure(data, figure, context);
525+
}
526+
tmpCanvas.context.putImageData(data, BORDER_SIZE, BORDER_SIZE);
508527
}
509-
for (const figure of this._figures) {
510-
drawFigure(data, figure, context);
511-
}
512-
tmpCtx.putImageData(data, BORDER_SIZE, BORDER_SIZE);
513-
const canvas = tmpCanvas.canvas;
514528

515529
return {
516-
canvas,
530+
canvas: tmpCanvas.canvas,
517531
offsetX: offsetX - BORDER_SIZE * scaleX,
518532
offsetY: offsetY - BORDER_SIZE * scaleY,
519533
scaleX,

0 commit comments

Comments
 (0)