Skip to content

Commit 918a319

Browse files
authored
Merge pull request #20885 from calixteman/gouraud_gpu
Implement Gouraud-based shading using WebGPU.
2 parents 5cb8f22 + 86441e9 commit 918a319

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)