Skip to content

Offload canvas drawing operations to a worker - #20729

Open
Aditi-1400 wants to merge 14 commits into
mozilla:masterfrom
Aditi-1400:worker-drawing
Open

Offload canvas drawing operations to a worker #20729
Aditi-1400 wants to merge 14 commits into
mozilla:masterfrom
Aditi-1400:worker-drawing

Conversation

@Aditi-1400

@Aditi-1400 Aditi-1400 commented Feb 24, 2026

Copy link
Copy Markdown
Collaborator

Commit descriptions

Note that commit hashes might change in the future due to commit edits later

1. 34ab2b6: Extract ObjectHandler from WorkerTransport …

Extract ObjectHandler from WorkerTransport
Move the commonobj/obj resolution logic from WorkerTransport.setupMessageHandler
into a reusable ObjectHandler class. This enables sharing the object resolution
logic between the main thread (WorkerTransport) and the renderer worker.

Notable changes:

  1. The new argument this.shouldCreatePageObjs is added in object handler, it does not affect the main-thread rendering, it is set to false, but for worker-rendering, the renderer worker keeps only Map<pageIndex, PDFObjects>, not PDFPageProxy instances. So object_handler.js (line 126) has to handle both shapes.
    The shouldCreatePageObjs part exists because renderer-worker obj messages can arrive before InitializeGraphics has called #getPageObjs(pageIndex). In that case the renderer still must cache the image/pattern object, otherwise later CanvasGraphics will hit an unresolved dependency while executing the operator list.

Relevent code:

    let pageOrObjs = this.pageCache.get(pageIndex);
    if (!pageOrObjs) {
      if (!this.shouldCreatePageObjs) {
        return;
      }
      pageOrObjs = new PDFObjects();
      this.pageCache.set(pageIndex, pageOrObjs);
    }

    const objs = pageOrObjs.objs || pageOrObjs;
    if (objs.has(id)) {
      return;
    }

Open questions:

  1. We should probably add unit tests for ObjectHandler class?

2. 741ca8d: Adds RendererWorker class for offloading canvas …

Adds RendererWorker class for offloading canvas

Introduce the RendererWorker class for offloading canvas rendering
to a dedicated Web Worker. Alongside, it adds RendererMessageHandler,
GlobalWorkerOptions.rendererSrc configuration, entrypoints for
pdf.renderer.js bundle and build targets in gulpfile.

No rendering changes are introduced in this commit, this is a setup for
later commits that wire-up graphics execution and object forwarding.

Notable changes

This commit just sets up the renderer worker while following the same pattern as PDFWorker setup.

  1. Introduces a new global flag disableWorkerRendering for disabling the worker-rendering. Note that the flag is only checked once to check whether we should set up the RendererHandler, all the further decisions to use the worker-rendering are deferred to whether RendererHandler is not null.
  2. In this commit the worker rendering is disabled when there is no Worker API, OffScreenCanvas is not supported, a custom ownerDocument is present, since custom ownerDocument can be iframe document etc. which cannot be transferred to a worker and the worker cannot create DOM nodes inside it. Same for styleElement, it is used by FontLoader, and is also a DOM element.
    In the renderer worker, fonts would instead be loaded via the FontFace API (self.fonts.add()). When a custom styleElement is provided (testing scenarios), it forces CSS-based font loading instead of the FontFace API. Since CSS font loading doesn't work in a worker context, the renderer worker can't render fonts correctly if styleElement is in use.

3. c28992d: Add canvas filter detection in core layer …

Add canvas filter detection in core layer

Add hasCanvasFilters method to PartialEvaluator that traverses page
resources (including Pattern and Type3 Font sub-resources) to detect
transfer maps and SMask operations requiring DOM SVG filters. These
filters are unsupported by OffscreenCanvas, so detecting them early
allows the display layer to fall back to main-thread rendering.

Due to bug https://bugzilla.mozilla.org/show_bug.cgi?id=2011237, since there's no DOM access from a worker the filter has to be defined with an external URL which is not currently supported in OffscreenCanvasRenderingContext2D

Notable changes:

  1. The hasCanvasFilters method is very similar to hasBlendModes where both of these methods traverse the resource graph to check the presence of canvas filters. hasCanvasFilters however returns true conservatively. I considererd reusing some of the code from hasBlendModes but that made the patch more complex and hard to read.
  2. It also checks annotations for resources since an annotation can have its own appearance stream, and that appearance stream has its own /Resources dictionary. Annotation rendering later calls annotation.getOperatorList(...), and that uses the appearance stream resources, not the page resources.

4. ffe8e9a: Add object forwarding between main thread and renderer worker …

Add object forwarding between main thread and renderer worker

Add object forwarding so the renderer worker receives
the same commonobj/obj messages as the main thread

WorkerTransport now forwards commonobj/obj to the rendererHandler.
Additionally, allow _startRenderPage propagates
hasCanvasFilters from core layer.

Open questions

At present the way renderer works is we use the main thread for forwarding the objs/commonObjs and font fallback instead of PDFworker directly sending it to the renderer worker. So objs/commonObjs are duplicated and also adds an additional hop.

  1. The commonobjs/objs are still being duplicated for sending to each of main-thread and renderer, which can be fixed in the following two ways:
    a. Use SAB
    b. Move the InternalRenderTask entirely to the renderer worker, I think this is possible and this is the approach I would prefer, to not have main thread deal with objs/commonObjs at all, but this is a much larger refactor which I am not sure should be a part of this patch.

  2. While fixing a. appears to be somewhat easy, I have tried with SAB and the current version of sending to both main and renderer is not the best approach because there is a race-condition that it introduces that causes browser tests to almost always timeout, I've tried fixing several but so far, the tests still timeout, if we want to remove the forwarding, I can spend more time looking into forwarding, however I think if we remove the dependency on main thread entirely, it should fix both issues.

For now there's a TODO comment to remove the forwarding in the future.

5. 9ddbd41: Add graphics initialization and operator list execution in renderer worker …

Add graphics initialization and operator list execution in renderer worker

This decides whether to use worker rendering based on hasCanvasFilters,
pageColors,dependency/image tracking. It transfers the canvas via
transferControlToOffscreen and sends operator list chunks incrementally.
In the renderer worker, it adds the functionality to initialize
graphics and execute the operator list.

Notable changes

  1. api.js
    a. keepRendererCanvas: This flag is added to to still clear normal page state, but ask the renderer worker to keep the transferred canvas alive. This lets PDF.js free operator lists, page objects, fonts/images tied to the page, etc., without making already-rendered visible pages go blank when scrolling etc.
    b. In main-thread rendering, CanvasGraphics gets the actual OptionalContentConfig instance directly. In renderer-worker rendering, that object cannot be sent as-is: it has class methods/private fields. There is a change to pass the plain data we receive from PDFWorker and rebuilding the object on the worker side.
    c. We also need to transfer the annotation canvases, so we find the annotations with hasOwnCanvas, and send it to renderer worker. It also caches the transferred canvases in _transferredAnnotationCanvasIds.
    d. Worker rendering is disabled when we have canvas filters and page colors for reasons described above and it falls back to main-thread rendering. It is also disabled when dependencyTracker and imagesTracker are present, this would require making them transferable, which is not too complex and can be done in future iterations.
    e. Operator list is sent to renderer worker in chunks. Sending the full growing operator list every time would be very expensive. So the main thread only sends the new tail of the operator list: from the last sent index to the current length. After the renderer worker receives it, it appends that partition to its own worker-local operator list.

7. f085c07: Adapt viewer and tests for OffscreenCanvas renderer worker …

Adapt viewer and tests for OffscreenCanvas renderer worker

Update the viewer and tests to handle canvases that have
been transferred to an OffscreenCanvas via the renderer worker.

Notable changes

  1. In viewer_spec.mjs, it first tries the old direct path, and then snapshots the placeholder canvas with createImageBitmap(canvas), draws that bitmap into temporary canvs, calls getImageData on the temporary canvas.
  2. waitForDetailRendered at viewer_spec.mjs waits specifically for pagerendered where isDetailView is true.
  3. waitForCanvasPixels handles another worker-rendering detail: even after the render event, the transferred canvas placeholder may not have committed visible pixels yet. It polls a tiny bitmap snapshot until a non-empty pixel is readable, avoiding flakes where the test sees a canvas element but reads blank/stale pixels.

@codecov-commenter

codecov-commenter commented Feb 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.35593% with 88 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.18%. Comparing base (b4ba666) to head (58eed71).
⚠️ Report is 4 commits behind head on master.

Files with missing lines Patch % Lines
src/display/api.js 86.25% 36 Missing ⚠️
src/display/canvas.js 42.10% 22 Missing ⚠️
src/core/evaluator.js 86.66% 10 Missing ⚠️
src/display/object_handler.js 84.12% 10 Missing ⚠️
src/display/pdf_objects.js 16.66% 5 Missing ⚠️
web/base_pdf_page_view.js 80.00% 2 Missing ⚠️
src/display/canvas_dependency_tracker.js 50.00% 1 Missing ⚠️
src/display/canvas_factory.js 0.00% 1 Missing ⚠️
src/display/worker_options.js 80.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #20729      +/-   ##
==========================================
- Coverage   90.01%   89.18%   -0.84%     
==========================================
  Files         264      265       +1     
  Lines       66893    67263     +370     
==========================================
- Hits        60214    59987     -227     
- Misses       6679     7276     +597     
Flag Coverage Δ
browsertest 65.78% <73.77%> (-0.76%) ⬇️
fonttest 9.03% <ø> (ø)
integrationtest 68.53% <71.39%> (-0.81%) ⬇️
unittest 57.85% <54.58%> (-0.03%) ⬇️
unittestcli 56.49% <41.15%> (-0.11%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@nicolo-ribaudo

Copy link
Copy Markdown
Collaborator

/botio test

@moz-tools-bot

Copy link
Copy Markdown
Collaborator

From: Bot.io (Windows)


Received

Command cmd_test from @nicolo-ribaudo received. Current queue size: 0

Live output at: http://54.193.163.58:8877/e0227fb9789b076/output.txt

@moz-tools-bot

Copy link
Copy Markdown
Collaborator

From: Bot.io (Linux m4)


Received

Command cmd_test from @nicolo-ribaudo received. Current queue size: 0

Live output at: http://54.241.84.105:8877/b6e4acc21cb49a8/output.txt

@moz-tools-bot

Copy link
Copy Markdown
Collaborator

From: Bot.io (Linux m4)


Failed

Full output at http://54.241.84.105:8877/b6e4acc21cb49a8/output.txt

Total script time: 46.91 mins

  • Unit tests: FAILED
  • Integration Tests: FAILED
  • Regression tests: FAILED
  different ref/snapshot: 14

Image differences available at: http://54.241.84.105:8877/b6e4acc21cb49a8/reftest-analyzer.html#web=eq.log

@moz-tools-bot

Copy link
Copy Markdown
Collaborator

From: Bot.io (Windows)


Failed

Full output at http://54.193.163.58:8877/e0227fb9789b076/output.txt

Total script time: 90.47 mins

  • Unit tests: FAILED
  • Integration Tests: FAILED
  • Regression tests: FAILED
  different ref/snapshot: 13

Image differences available at: http://54.193.163.58:8877/e0227fb9789b076/reftest-analyzer.html#web=eq.log

@nicolo-ribaudo

Copy link
Copy Markdown
Collaborator

I was looking through the reftest failures, it seems like the only real ones (the others are minor pixel differences due to the different rendering pipeline, but not visible to humans) are:

  • issue8092
  • issue16127 (for the gradients inside the font)
  • ShowText-ShadingPattern (probably same problem as issue8092)
  • issue1133 (page 4, in the huawei logo)
  • issue19022

@Aditi-1400
Aditi-1400 force-pushed the worker-drawing branch 4 times, most recently from 1d6de6c to 43a73d7 Compare March 9, 2026 17:10
@Aditi-1400

Aditi-1400 commented Mar 9, 2026

Copy link
Copy Markdown
Collaborator Author

I was looking through the reftest failures, it seems like the only real ones (the others are minor pixel differences due to the different rendering pipeline, but not visible to humans) are:

  • issue8092
  • issue16127 (for the gradients inside the font)
  • ShowText-ShadingPattern (probably same problem as issue8092)
  • issue1133 (page 4, in the huawei logo)
  • issue19022

There's also gradient difference in 17069, also there are differences in 19022, same as issue8092

@nicolo-ribaudo

Copy link
Copy Markdown
Collaborator

/botio test

@moz-tools-bot

Copy link
Copy Markdown
Collaborator

From: Bot.io (Windows)


Received

Command cmd_test from @nicolo-ribaudo received. Current queue size: 0

Live output at: http://54.193.163.58:8877/832219967f9b55d/output.txt

@moz-tools-bot

Copy link
Copy Markdown
Collaborator

From: Bot.io (Linux m4)


Received

Command cmd_test from @nicolo-ribaudo received. Current queue size: 0

Live output at: http://54.241.84.105:8877/659eb95d2835e52/output.txt

@moz-tools-bot

Copy link
Copy Markdown
Collaborator

From: Bot.io (Linux m4)


Failed

Full output at http://54.241.84.105:8877/659eb95d2835e52/output.txt

Total script time: 46.69 mins

  • Unit tests: Passed
  • Integration Tests: FAILED
  • Regression tests: FAILED
  different ref/snapshot: 12

Image differences available at: http://54.241.84.105:8877/659eb95d2835e52/reftest-analyzer.html#web=eq.log

@moz-tools-bot

Copy link
Copy Markdown
Collaborator

From: Bot.io (Windows)


Failed

Full output at http://54.193.163.58:8877/832219967f9b55d/output.txt

Total script time: 78.06 mins

  • Unit tests: Passed
  • Integration Tests: Passed
  • Regression tests: FAILED
  different ref/snapshot: 12

Image differences available at: http://54.193.163.58:8877/832219967f9b55d/reftest-analyzer.html#web=eq.log

@Aditi-1400
Aditi-1400 force-pushed the worker-drawing branch 2 times, most recently from 134955c to ba0fa2c Compare March 16, 2026 15:29
@nicolo-ribaudo

Copy link
Copy Markdown
Collaborator

/botio test

@moz-tools-bot

Copy link
Copy Markdown
Collaborator

From: Bot.io (Windows)


Received

Command cmd_test from @nicolo-ribaudo received. Current queue size: 0

Live output at: http://54.193.163.58:8877/528a274cb665c1e/output.txt

@moz-tools-bot

Copy link
Copy Markdown
Collaborator

From: Bot.io (Linux m4)


Received

Command cmd_test from @nicolo-ribaudo received. Current queue size: 0

Live output at: http://54.241.84.105:8877/67316c0b179c2dc/output.txt

@moz-tools-bot

Copy link
Copy Markdown
Collaborator

From: Bot.io (Linux m4)


Failed

Full output at http://54.241.84.105:8877/67316c0b179c2dc/output.txt

Total script time: 46.49 mins

  • Unit tests: Passed
  • Integration Tests: FAILED
  • Regression tests: FAILED
  different ref/snapshot: 1

Image differences available at: http://54.241.84.105:8877/67316c0b179c2dc/reftest-analyzer.html#web=eq.log

@moz-tools-bot

Copy link
Copy Markdown
Collaborator

From: Bot.io (Windows)


Failed

Full output at http://54.193.163.58:8877/528a274cb665c1e/output.txt

Total script time: 75.41 mins

  • Unit tests: Passed
  • Integration Tests: FAILED
  • Regression tests: FAILED
  different ref/snapshot: 1

Image differences available at: http://54.193.163.58:8877/528a274cb665c1e/reftest-analyzer.html#web=eq.log

@Aditi-1400
Aditi-1400 force-pushed the worker-drawing branch 2 times, most recently from 96dda63 to cca0198 Compare March 23, 2026 11:47
Comment thread src/display/api.js
* @param {RendererWorkerParameters} params - The worker initialization
* parameters.
*/
class RendererWorker {

@Snuffleupagus Snuffleupagus Jun 2, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't appear that the verbosity parameter is sent to the worker-thread, compare with the existing PDFWorker implementation, why not?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This now gets sent to renderer worker.

@Snuffleupagus

Copy link
Copy Markdown
Collaborator

The new RendererWorker doesn't appear to report test coverage data, which it probably should?

Comment thread src/display/api.js
? "resource://pdf.js/build/pdf.renderer.mjs"
: "../build/pdf.renderer.mjs";
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this code placed here, since it really ought to be handled in the same way as the workerSrc instead?

pdf.js/web/app_options.js

Lines 553 to 563 in e9a946e

workerSrc: {
/** @type {string} */
value:
// eslint-disable-next-line no-nested-ternary
typeof PDFJSDev === "undefined"
? "../src/pdf.worker.js"
: PDFJSDev.test("MOZCENTRAL")
? "resource://pdf.js/build/pdf.worker.mjs"
: "../build/pdf.worker.mjs",
kind: OptionKind.WORKER,
},

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this block with build-target defaults was removed from api.js; RendererWorker.rendererSrc now throws if GlobalWorkerOptions.rendererSrc is unset, mirroring PDFWorker.workerSrc.
If rendererSrc is unset, the throw is caught, the worker capability rejects, and just disables worker rendering with a warning.

@Snuffleupagus Snuffleupagus left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in #20729 (comment) the RendererWorker must report coverage data; note how #20729 (comment) reports significantly reduced coverage with this patch.

Edit: Also, the commit history should be cleaned-up before this lands by folding any "fixup" commits into their parent commits, in order to keep the commit history clean.

Comment thread src/display/api.js Outdated
renderTaskId: this._renderTaskId,
enableHWA: this._enableHWA,
enableWebGPU: this._enableWebGPU,
optionalContentConfig: optionalContentConfig?.serializable ?? null,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The optionalContentConfig must be available here, otherwise there's a bug somewhere else.

Suggested change
optionalContentConfig: optionalContentConfig?.serializable ?? null,
optionalContentConfig: optionalContentConfig.serializable,

Comment thread src/display/renderer_worker.js Outdated
Comment on lines +236 to +238
const optionalContentConfig = data.optionalContentConfig
? OptionalContentConfig.fromSerializable(data.optionalContentConfig)
: new OptionalContentConfig(null);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, it shouldn't be possible for the optionalContentConfig-data to be undefined here.

Suggested change
const optionalContentConfig = data.optionalContentConfig
? OptionalContentConfig.fromSerializable(data.optionalContentConfig)
: new OptionalContentConfig(null);
const optionalContentConfig =
OptionalContentConfig.fromSerializable(data.optionalContentConfig);

Comment thread src/display/api.js Outdated
}
internalRenderTask.initializeGraphics({
const { transparency, hasCanvasFilters = false } =
typeof renderPageData === "object" && renderPageData !== null

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This can be shortened.

Suggested change
typeof renderPageData === "object" && renderPageData !== null
renderPageData && typeof renderPageData === "object"

@calixteman

Copy link
Copy Markdown
Contributor

As mentioned in #20729 (comment) the RendererWorker must report coverage data; note how #20729 (comment) reports significantly reduced coverage with this patch.

I'll do it in a follow-up.

Comment thread src/display/renderer_worker.js Outdated
static #appendOperatorList(renderTaskState, fnArray, argsArray, lastChunk) {
const { operatorList } = renderTaskState;
if (fnArray) {
operatorList.fnArray.push(...fnArray);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could fails with a large array, so it should be a little more robust.

Comment thread src/display/api.js Outdated
Comment on lines +2093 to +2097
#worker = null;

#rendererHandler = null;

#capability = Promise.withResolvers();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these fields not sorted alphabetically?

Comment thread src/display/api.js Outdated
class RendererWorker {
#worker = null;

#rendererHandler = null;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this not called #messageHandler, since that's what it'll contain?

Comment thread src/display/api.js
Comment on lines +2137 to +2139
try {
const { rendererSrc } = RendererWorker;
const worker = new Worker(rendererSrc, { type: "module" });

@Snuffleupagus Snuffleupagus Jun 24, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that this code looks like it was just copied from the existing PDFWorker implementation, why isn't this fully consistent with the following code?

Suggested change
try {
const { rendererSrc } = RendererWorker;
const worker = new Worker(rendererSrc, { type: "module" });
let { rendererSrc } = RendererWorker;
try {
// Wraps rendererSrc path into blob URL, if the former does not belong
// to the same origin.
if (
typeof PDFJSDev !== "undefined" &&
PDFJSDev.test("GENERIC") &&
!PDFWorker._isSameOrigin(window.location, rendererSrc)
) {
rendererSrc = PDFWorker._createCDNWrapper(
new URL(rendererSrc, window.location).href
);
}
const worker = new Worker(rendererSrc, { type: "module" });

@Aditi-1400

Copy link
Copy Markdown
Collaborator Author

The bug and the patch for updating the number of prefs: https://bugzilla.mozilla.org/show_bug.cgi?id=2056102

Comment thread src/display/api.js Outdated
let initPromise = null;
if (useWorkerRendering) {
try {
const offscreen = this._canvas.transferControlToOffscreen();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once the control is transferred to the offscreen canvas it cannot be done again so I think it could be break the possibility of reusing the same canvas but in such a case we can reuse the associated offscreen one.

Comment thread src/core/evaluator.js Outdated
processed.put(graphicState.objId);
}
try {
if (this._hasTransferMaps(graphicState.get("TR"))) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recently added support for TR2 too

Comment thread src/core/evaluator.js Outdated
}
}

const xObjects = node.get("XObject");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You only visit XObject but a TR/TR2 can be in a tiling pattern too.

@Snuffleupagus Snuffleupagus left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that this PR consists of a number of commits, please make sure that every single one of them works correctly on their own. Hence ensure, by testing locally, that all tests pass when run against each commit.

This is imperative to make sure that it's possible to bisect any future regressions to an exact commit, since the total size of the PR would otherwise make that really difficult.

@Aditi-1400

Copy link
Copy Markdown
Collaborator Author

@Snuffleupagus I have checked by testing locally, that all tests pass when run against each commit.

@Snuffleupagus

Snuffleupagus commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

I have checked by testing locally, that all tests pass when run against each commit.

Considering all of the "fixup" commits here, which must be folded into their appropriate parent commits, it sounds very surprising that every single commits works!
For example, there's no less than two commits called fixup! Adapt viewer and tests for OffscreenCanvas renderer worker which make it seem like commits before those don't actually work correctly.

Move the commonobj/obj resolution logic from WorkerTransport.setupMessageHandler
into a reusable ObjectHandler class. This enables sharing the object resolution
logic between the main thread (WorkerTransport) and the renderer worker.

This commit is a part of the renderer-worker series, the worker rendering
stays disabled until the final commit in this series.
Introduce the RendererWorker class for offloading canvas rendering
to a dedicated Web Worker. Alongside, it adds RendererMessageHandler,
GlobalWorkerOptions.rendererSrc configuration, entrypoints for
pdf.renderer.js bundle and build targets in gulpfile.

No rendering changes are introduced in this commit, this is a setup for
later commits that wire-up graphics execution and object forwarding.
The `disableWorkerRendering` option defaults to disabled and is flipped
in the final commit of this series.

This commit is a part of the renderer-worker series, the worker rendering
stays disabled until the final commit in this series.
Add a hasCanvasFilters method to PartialEvaluator that walks the
page-level ExtGState dictionaries, and those of Form XObject and tiling
pattern resources, to detect transfer functions (TR/TR2) that require DOM
SVG filters. Such filters are unavailable on OffscreenCanvas, so detecting
them up front lets the display layer fall back to main-thread rendering
for affected pages.

The flag rides along on the existing StartRenderPage message down to
initializeGraphics; the rendering decision that consumes it is added
later in this series.

SMask rendering already has a pixel-buffer fallback in canvas.js, and TR
inside Type3 glyph streams or annotation appearance streams is rare enough
in practice that walking those sub-resources (and gating first paint on
annotation parsing) isn't worth it.

This commit is a part of the renderer-worker series, the worker rendering
stays disabled until the final commit in this series.
The renderer worker has no PDFPageProxy objects, only per-page PDFObjects
instances, so accept a page cache holding either and create missing entries
on demand behind `shouldCreatePageObjs`.

This commit is a part of the renderer-worker series, the worker rendering
stays disabled until the final commit in this series.
Forward the commonobj/obj messages that WorkerTransport receives on to
the renderer worker, so that it builds up the same commonObjs and
per-page objs as the main thread.

CopyLocalImage is handled separately, since the core worker sends only an
image reference: the renderer worker is asked to resolve it from its own
objects first, and only when it can't does the main thread send the image
data it just found.

A forwarded object that cannot be delivered is reported back as
`objFailed`, so the renderer worker rejects it rather than waiting
forever on a dependency that will never arrive.

The renderer worker's object stores are released by the cleanupPage and
Cleanup handlers added later in this series.

This commit is a part of the renderer-worker series, the worker rendering
stays disabled until the final commit in this series.
Since operator lists will be posted across threads, Path2D objects can
no longer be materialized into argsArray; they are cached in a pathCache
map on the operator list instead, keeping it structured-cloneable.

This commit is a part of the renderer-worker series, the worker rendering
stays disabled until the final commit in this series.
Look up the annotation canvas in `annotationCanvasMap` before creating a
new one, and resize it in place when it is already there. This lets a
canvas that was created (and possibly transferred) elsewhere be drawn
into, rather than being replaced by a fresh one.

Track the canvas name on the object as well as in the DOM attribute, so
that the same matching works for canvases without a DOM node.

This commit is a part of the renderer-worker series, the worker rendering
stays disabled until the final commit in this series.
Add the handlers that let the renderer worker initialize graphics and
execute an operator list against a transferred OffscreenCanvas:
InitializeGraphics, ExecuteOperatorList, UpdateAnnotationCanvases,
CleanupRenderTask, ReleaseCanvas, cleanupPage, restorePage and Cleanup,
along with the per-render-task state they operate on.

Also adds the OffscreenCanvas and worker-side filter factories, and lets
CanvasGraphics.executeOperatorList report a failed object dependency
through an errorCallback, so a rejected object aborts the render instead
of hanging it.

Nothing sends these messages yet: src/pdf.renderer.js is the only
importer of this file, so no main-thread code path reaches it.

This commit is a part of the renderer-worker series, the worker rendering
stays disabled until the final commit in this series.
Decide whether to use worker rendering based on hasCanvasFilters,
pageColors and the debug-recording path, transfer the canvas via
transferControlToOffscreen and send the operator list to the renderer
worker in chunks, along with any annotation canvases the list refers to.

The recorded bounding boxes and image coordinates now come back from the
worker in the final ExecuteOperatorList response, so the trackers are
built inside initializeGraphics rather than by the caller.

Since a canvas can only be transferred once, the OffscreenCanvas is
tracked per canvas id and reused when the same canvas is re-rendered.

The gate added here is off by default, so nothing takes this path yet;
it is flipped in the final commit of this series.

This commit is a part of the renderer-worker series, the worker rendering
stays disabled until the final commit in this series.
A canvas whose control has been transferred to the renderer worker can
no longer be resized or re-acquired on the main thread, so releasing it
has to go through the worker instead of setting width/height to zero,
and it cannot be reused as the source for a thumbnail.

This is inert while the gate is off, since resetWorkerCanvas is never
set and releaseCanvas behaves exactly as the previous
width = height = 0.

This commit is a part of the renderer-worker series, the worker rendering
stays disabled until the final commit in this series.
Render into a separate canvas in the reftest driver and copy the result
back, since a canvas can only be transferred once, and read pixels
through createImageBitmap in the integration helpers for canvases whose
context can no longer be acquired.

Nothing here enables worker rendering; GlobalWorkerOptions.rendererSrc
is set in the final commit of this series, together with the library and
viewer defaults. Every change in this commit behaves identically on the
main thread.

This commit is a part of the renderer-worker series, the worker rendering
stays disabled until the final commit in this series.
Thread the enableWebGPU flag from getDocument() through WorkerTransport
and InternalRenderTask to the renderer worker's InitializeGraphics
handler, where it triggers GPU device initialization.

The main thread already waits for InitializeGraphics to resolve before
sending any operators, so the GPU device is ready by the time drawing
starts.

This commit is a part of the renderer-worker series, the worker rendering
stays disabled until the final commit in this series.
Flip disableWorkerRendering to opt-out in the API and to false in the
viewer, and point the reftest driver and the unit tests at the renderer
worker bundle, so the whole suite exercises this path from here on.

This changes the default for API consumers: the canvas passed to
render() has its control transferred to the renderer worker, so calling
getContext("2d") on it afterwards will throw. Pass
disableWorkerRendering: true to opt out.

Getting worker rendering also requires GlobalWorkerOptions.rendererSrc to
point at the pdf.renderer.mjs bundle. The viewer and the pdfjs-dist
webpack entry set it automatically, but integrators who bundle the library
themselves must set it as well; when it's unset, or the renderer worker
fails to start, rendering falls back to the main-thread with a warning
rather than failing.

Thumbnails keep rendering on the main thread by passing a canvasContext
rather than a canvas. Note that this also means InternalRenderTask no
longer sees the canvas, so the "same canvas during multiple render()
operations" guard does not cover thumbnails.

This commit is the final commit of the renderer-worker series, it enables
the worker rendering that the previous commits kept disabled.
Mirror the existing nonBlendModesSet with a nonCanvasFiltersSet, so that
resources already proven filter-free aren't walked again on subsequent
pages. A separate set is needed since hasBlendModes descends into Form
XObjects only, while hasCanvasFilters also walks tiling patterns.

Replace _hasTransferMaps with _getTransferFunctions, now shared with
handleTransferFunction, since the two only differed in whether the
256-entry transfer maps get built.

This commit is a part of the renderer-worker series.
@Aditi-1400

Copy link
Copy Markdown
Collaborator Author

@Snuffleupagus
So, when I commented previously about the test passing, they were indeed passing but it was largely a false positive, because worker rendering in the tests only got enabled later in the series. test/driver.js didn't set GlobalWorkerOptions.rendererSrc until Adapt viewer and tests for OffscreenCanvas renderer worker, so before that the renderer worker can't be constructed and every reftest silently falls back to main-thread rendering.
And the fixup commits, were not covered by the current tests, those were largely because of review comments, which involved moving things around rather than any major behavioural changes, the commit fixup! Adapt viewer and tests for OffscreenCanvas renderer only fixed test failures for CI runs while they were always passing locally for me, so that was also not affecting the local results for me either.

Anyway, to address the problem I mentioned earlier that there was a disparity between when feature was enabled vs when we actually test the feature, I have moved around things a little bit, the branch tip stays the same as before, any differences are cosmetic, and the content is close to what you reviewed, with one new commit at the tip
(Reduce the parsing overhead of hasCanvasFilters) which is just an optimisation

Now, the second-to-last commit is the one that actually enables worker-rendering and all commits before that are setting the stage to be able to enable that. Until the 12th commit, the main thread rendering works as expected and on enabling the worker rendering in the 13th commit, we actually test the feature:

The tests that have been run locally on all 14-commits branch (worker rendering is off until commit 13):

  1. Commit validation - lint + all three suites (unit tests and reftests all pass, integration tests show 2-3 flakes).
  2. disableWorkerRendering verification - all three suites on commits 13 and 14 with the feature forced on and off, to make sure the fallback is working fine. Also, about the browsertest being considerably slow, I don't think that is the case on my local machine, I ran the tests several times to note the timings, I didn't notice much disparity.

(Note: They were run only on Firefox locally on my MacOS 26.5.1)

Bisecting is a little complex but the above approach does achieve the following:

A few preparatory commits modify code the existing main-thread renderer runs today:

  • ObjectHandler on a bare PDFObjects page cache
    • Path2D caching on the operator list
    • annotation canvas reuse in beginAnnotation etc.

Those are refactors of current code paths. Their tests passing proves they didn't regress current rendering. Commit 13 is then the only place where behaviour changes at all, which is a clean thing to review.
The split doesn't give real bisectability, since nothing exercises the feature before the enable commit. What it does give is that the preparatory commits refactor code today's main-thread renderer already runs. So, we can cherry-pick it onto any earlier commit and get a working "commit N + enable" build.

Coming to the newer commit division, there are 14 commits, all commits have a "This commit is a part of the renderer-worker series." to make them easier to search, I wasn't sure if to tag them differently.

I will update the PR description with the following details:

Commit Details

  1. Extract ObjectHandler from WorkerTransport - moves the commonobj/obj
    resolution logic out of WorkerTransport.setupMessageHandler into a reusable
    ObjectHandler class, so the main thread and the renderer worker can share it.

  2. [api-minor] Adds RendererWorker class for offloading canvas - adds
    RendererWorker, RendererMessageHandler, GlobalWorkerOptions.rendererSrc,
    the src/pdf.renderer.js entrypoint and the gulp build targets. No rendering
    changes; the disableWorkerRendering gate is added here, defaulting to off.

  3. Detect TR-based canvas filters and report them to the display layer - adds
    PartialEvaluator.hasCanvasFilters, which walks page-level ExtGState
    dictionaries plus Form XObject and tiling-pattern resources looking for
    transfer functions (TR/TR2).

  4. Allow ObjectHandler to operate on a bare PDFObjects page cache - the
    renderer worker has no PDFPageProxy objects, only per-page PDFObjects, so
    the handler accepts either and creates missing entries on demand behind
    shouldCreatePageObjs.

  5. Add object forwarding between main thread and renderer worker - forwards
    the commonobj/obj messages on to the renderer worker so it builds up the same
    object stores. CopyLocalImage is handled separately, since the core worker
    sends only a reference: the renderer worker resolves it from its own objects
    first, and only when it can't does the main thread send the data. Objects that
    can't be delivered come back as objFailed so a render aborts rather than
    hanging on a dependency that will never arrive.

  6. Cache Path2D objects on the operator list - operator lists now cross a
    thread boundary, so Path2D objects can't be materialized into argsArray
    any more; they go into a pathCache map on the operator list instead, keeping
    it structured-cloneable.

  7. Reuse existing annotation canvases in beginAnnotation - looks the canvas up
    in annotationCanvasMap and resizes it in place instead of replacing it, so a
    canvas created (and possibly transferred) elsewhere can be drawn into. The
    canvas name is tracked on the object as well as in the DOM attribute, so the
    same matching works for canvases with no DOM node.

  8. Execute operator lists in the renderer worker - adds the worker-side
    handlers (InitializeGraphics, ExecuteOperatorList, UpdateAnnotationCanvases,
    CleanupRenderTask, ReleaseCanvas, cleanupPage, restorePage, Cleanup)
    and the state they operate on, along with the OffscreenCanvas and worker-side
    filter factories.

  9. Send the operator list to the renderer worker - the main-thread half:
    decides whether to use worker rendering (based on hasCanvasFilters,
    pageColors and the debug-recording path), transfers the canvas, and sends the
    operator list in chunks along with any annotation canvases it refers to.
    Recorded bounding boxes and image coordinates now come back in the final
    ExecuteOperatorList response, so the trackers are built inside
    initializeGraphics. Since a canvas can only be transferred once, the
    OffscreenCanvas is tracked per canvas id and reused on re-render.

  10. Adapt the viewer for transferred canvases - a transferred canvas can't be
    resized or re-acquired on the main thread, so releasing it goes through the
    worker rather than setting width/height to zero, and it can't be reused as a
    thumbnail source.

  11. Adapt the test harness for transferred canvases - the reftest driver
    renders into a separate canvas and copies the result back, and the integration
    helpers read pixels through createImageBitmap for canvases whose context can
    no longer be acquired.

  12. Enable WebGPU in the renderer worker - threads enableWebGPU from
    getDocument() through to the worker's InitializeGraphics handler, where it
    triggers GPU device initialization. The main thread already waits for
    InitializeGraphics to resolve before sending operators, so the device is
    ready by the time drawing starts.

  13. [api-minor] Enable worker rendering - flips disableWorkerRendering to
    opt-out in the API and to false in the viewer, and points the reftest driver
    and unit tests at the renderer worker bundle, so the whole suite exercises this
    path from here on.

  14. Reduce the parsing overhead of hasCanvasFilters - largely an optimisation,
    mirrors the existing nonBlendModesSet with a nonCanvasFiltersSet so
    resources already proven filter-free aren't walked again on later pages, and
    replaces _hasTransferMaps with a _getTransferFunctions shared with
    handleTransferFunction.

I believe all the review comments should be addressed now.
cc @calixteman

Comment thread src/display/api.js
transform,
viewport,
transparency,
background,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the jsdoc, background could be a pattern or a gradient which are neither clonable nor transferable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants