Repository navigation
A pixel gets the same value in whatever region it is drawn - #37
Merged
Merged
Conversation
A scene drawn in bands or tiles was not, byte for byte, the scene drawn at once: one or two levels in a few antialiased pixels, depending on where the clip cut. Of the first pages of 965 files of the pdf.js corpus drawn by cera at 72 dpi, 85 differed between a render at once and one in bands of 100 rows or tiles of 256 pixels; none does now. Four causes, independent of one another: Edges. AddLine cut a segment at the clip and rounded the ends of what was left to 1/256 pixel; the slope came from the rounded ends, so the same segment had another slope in every clip that cut it elsewhere. An edge is now the line of its segment in device space (c + y·dxdy, evaluated at device y and moved to the clip in integers) with the rows of it that are drawn. Nothing is cut: an edge that leaves the clip at a side is drawn by rasterWide, which sums what lies left of the clip into its first cell and drops what lies right of it, cell by cell as a wider clip would have it. Edges that are not nearly level (|dx/dy| <= 256) are "plain": their x needs no bounding and they cross cells by the arithmetic there was. Nearly level ones are bounded by the ends of their segment and cross cells where their line does, since such an end may have been moved by a caller that shortened the segment outside the clip (the stroker's chords for culled curves). Runs. SolidBlitter.BlitRun with a coverage below 255 scaled the color and composited it, two roundings where BlitCoverage has one. How a row is cut into runs and coverage depends on the clip. An opaque color is now blended by runLerp, the arithmetic of covOpaque (scalar, AVX2 and AVX-512 kernels). Analytic strokes and culled curves. Which rows of a segment are filled analytically is set by the vertices of the whole polyline; a curve outside the clip that takes its chord has other vertices, and rows of its neighbour inside the clip changed sides. Curves are not culled on the analytic path any more (subpaths still are). Analytic rows. They stepped n·c by nx from the first pixel drawn, which is the clip's; every pixel is now measured from its own place, by an exact count of x. Tests: TestFillTiles (paths in rows, columns, tiles and single pixels against the fill at once), TestCanvasTiles (scenes of fills, strokes, clips, colors and shaders), TestRunLikeCoverage. Speed, old against new, best of six alternating runs: the scenes of BenchmarkScenes that are all edges cost 4 % more (uncached glyphs 5 to 9 %, short strokes and hatches 3 to 6 %), BenchmarkStrokeFixedCost 5 %, wide fills nothing. The first pages of the pdf.js corpus drawn by cera at 144 dpi take 1451 ms before and 1426 ms after: no difference that can be measured. Fixes #36
A compiler may fuse a multiplication and an addition into one operation with one rounding (arm64, amd64 at GOAMD64=v3), and need not do so alike wherever the same expression stands. The x of an edge is computed in AddLine, which keeps it at the ends, and again row by row; the cells an edge writes to are in range because its x lies between the ends. The conversions make every one of these the same arithmetic on every machine, and the centres of analytic rows with them.
timzifer
added a commit
to timzifer/cera
that referenced
this pull request
Oct 13, 2026
The release that has the change (timzifer/stilus#37), in place of its commit.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #36. For timzifer/cera#211.
A scene drawn in bands or tiles is now, byte for byte, the scene drawn at once.
The four causes, and what became of them
1. Edges (
raster.go).AddLinecut a segment at the clip and rounded the ends of what was left to 1/256 pixel; the slope came from the rounded ends. The same segment had another slope in every clip that cut it elsewhere.An
edgeis now the line of its segment in device space —c + y·dxdy, evaluated at the device y and moved to the clip in integers, so it is the same position for every clip — and the rows of it that are drawn. Nothing is cut any more:wideand drawn byrasterWide/cellLineWide: what lies left of the clip is summed into its first cell (as the edge on the border was), what lies right of it is dropped, and each cell inside gets what a wider clip gives it.|dx/dy| <= plainSlope, all but a quarter of a degree) areplain: their x needs no bounding (xPlain), their ends are kept, and they cross cells by the arithmetic there was (cellLine).xAt), because half a step of y moves its line far, and it crosses cells where its line does (rowLine,cellLineFlat) and not from where it enters the row: such an end may have been moved by a caller that shortened the segment outside the clip — the stroker's chord for a culled curve — andTestStrokeCullExactholds that against the uncut stroke.2. Runs (
blit.go,simd_*.go).SolidBlitter.BlitRunwith a coverage below 255 scaled the color and composited it, two roundings whereBlitCoveragehas one (lerpx). How the sweep cuts a row into runs and coverage depends on the clip: its first cells are coverage where the whole row has a run. An opaque color is now blended byrunLerp, the arithmetic ofcovOpaque, with AVX2 and AVX-512 kernels.3. Analytic strokes and culled curves (
stroke.go). Which rows of a segment are filled analytically is set by the vertices of the whole polyline. A curve outside the clip that takes its chord has other vertices, and rows of its neighbour inside the clip changed sides; the two sides round differently. Curves are no longer culled on the analytic path. Subpaths still are, and the outline path culls as before.4. Analytic rows (
strip.go,rasterStrip). They steppedn·cbynxfrom the first pixel drawn, which is the clip's. Every pixel is now measured from its own place:nx·x + gwith x counted exactly (rowCtx.rowAt). This was one pixel in the corpus.Tests
TestFillTiles: 300 paths (lines, curves, nearly level and nearly upright segments, points far away) filled in rows, columns, tiles and single pixels against the fill at once. Fails onmainat the first path.TestCanvasTiles: 400 scenes of fills and strokes (hairlines to wide, all caps and joins, dashes) in opaque and translucent colors and shaders, under path and rectangle clips, over a backdrop that is not plain.TestRunLikeCoverage: a run of every coverage against the same coverage pixel by pixel, for colors and shaders.-raceand withGOEXPERIMENT=simd;staticcheck.Speed
Old against new, precompiled, alternating, best of six (the machine is noisy; single runs spread by a quarter):
BenchmarkScenes, the scenes that are all edges (uncached glyphs, short strokes, hatches, mixed drawing; 12)BenchmarkStrokeFixedCostBenchmarkDenseDashBenchmarkRasterizeSparseBandsBenchmarkWideFillWhat it costs is paid per edge, in
AddLine(the ends of the line are evaluated instead of converted) and by the largeredge(48 bytes for 40); per row a plain edge costs what it did. On pages it is not measurable. A first version that bounded every edge and took every crossing from the line cost 25 % per stroke;plainis what brought it back.