Problem
render_mesh_shading (src/rendering/mesh_shading.rs) resolves a shading's /Function once, but evaluates it through the to_rgba closure — once per domain grid point for a Type 1 function-based shading (render_function_based walks a grid of up to 129×129 = 16,641 points), and once per vertex for the mesh types.
For a Type 0 (sampled) function, eval_type0 calls decode_stream_data() at the top of every evaluation. The sample stream is therefore decoded up to 16,641 times per shading, and render time scales with stream size × grid area instead of paying the decode once per shading.
Measured on a 600×600 page painting one Type 1 shading over a Type 0 function (median of 5 renders, three interleaved rounds, one pinned core on an otherwise-idle 32-core box):
| sample stream |
render time |
| 16 KiB |
62.1 / 62.7 / 62.7 ms |
| 64 KiB |
81.8 / 81.6 / 81.9 ms |
| 256 KiB |
153.6 / 163.7 / 166.5 ms |
Render time tracks the stream size even though the output is identical — which only happens if the stream is decoded per evaluation. Root cause confirmed by that scaling, not by reading.
Proposed direction
Decode the stream where the function is resolved, in render_mesh_shading, and pass the bytes down to eval_type0. Output is byte-identical — the change is purely where the decode happens. With the decode hoisted, the same three cases render in ~56 ms flat (null experiment spread on the same box: 154.5–164.0 ms for base-vs-base at 256 KiB, so the 64 KiB and 256 KiB deltas are far outside the floor; the 16 KiB delta is inside it and not claimed):
| sample stream |
base |
fix |
| 64 KiB |
~82 ms |
~56 ms (~1.5×) |
| 256 KiB |
~154–167 ms |
~57 ms (~2.8×) |
Not covered, deliberately: a top-level array of functions and the children of a Type 3 stitching function are reached from the same per-grid-point path and would still decode per call. Same defect class, unmeasured: eval_type4 also re-decodes and re-parses its PostScript program on every call even though functions::Program is compile-once/evaluate-many — typical Type 4 programs are small, so the impact is likely minor; worth its own measurement before touching.
Acceptance criteria
- Byte-identity across a real-PDF corpus regression sweep, base vs fix: per-page render digests plus text/table/image extraction digests, zero differing documents.
- The targeted benchmark above with its base-vs-base null experiment; claim only deltas outside the floor.
Problem
render_mesh_shading(src/rendering/mesh_shading.rs) resolves a shading's/Functiononce, but evaluates it through theto_rgbaclosure — once per domain grid point for a Type 1 function-based shading (render_function_basedwalks a grid of up to 129×129 = 16,641 points), and once per vertex for the mesh types.For a Type 0 (sampled) function,
eval_type0callsdecode_stream_data()at the top of every evaluation. The sample stream is therefore decoded up to 16,641 times per shading, and render time scales with stream size × grid area instead of paying the decode once per shading.Measured on a 600×600 page painting one Type 1 shading over a Type 0 function (median of 5 renders, three interleaved rounds, one pinned core on an otherwise-idle 32-core box):
Render time tracks the stream size even though the output is identical — which only happens if the stream is decoded per evaluation. Root cause confirmed by that scaling, not by reading.
Proposed direction
Decode the stream where the function is resolved, in
render_mesh_shading, and pass the bytes down toeval_type0. Output is byte-identical — the change is purely where the decode happens. With the decode hoisted, the same three cases render in ~56 ms flat (null experiment spread on the same box: 154.5–164.0 ms for base-vs-base at 256 KiB, so the 64 KiB and 256 KiB deltas are far outside the floor; the 16 KiB delta is inside it and not claimed):Not covered, deliberately: a top-level array of functions and the children of a Type 3 stitching function are reached from the same per-grid-point path and would still decode per call. Same defect class, unmeasured:
eval_type4also re-decodes and re-parses its PostScript program on every call even thoughfunctions::Programis compile-once/evaluate-many — typical Type 4 programs are small, so the impact is likely minor; worth its own measurement before touching.Acceptance criteria