diff --git a/README.md b/README.md index 29097c7..49e764e 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,13 @@ stroked text painted with a shader. reach it. They are made by the first band that draws the shape, so the work stays in the workers. Byte-identical to `Fill`/`Stroke`; 2 000 hatch lines drawn as 64 bands on one core: 87 → 54 ms (one band: 46 ms). +- **Bands and tiles**: a pixel gets the same value in whatever region it + is drawn (`Canvas.Reset`). An edge is the line of its segment in device + space, not the segment cut at the clip and rounded there; a run blends + as coverage does; an analytic row measures every pixel from its own + place. A scene drawn in tiles is byte-identical to the scene drawn at + once (`TestCanvasTiles`), so neither the number of workers nor the + tiling of a viewer shows in a page. - **Rectangle clips** (91 % of real PDF clips) cost nothing per pixel; other clips are rasterized once into a mask that is never cleared as a whole and keeps fully opaque runs as runs. diff --git a/blit.go b/blit.go index a8ef7a7..79e619f 100644 --- a/blit.go +++ b/blit.go @@ -157,8 +157,14 @@ func (b *SolidBlitter) SetColor(c color.RGBA) { func (b *SolidBlitter) BlitRun(y, x0, x1 int, alpha uint8) { d := b.t.row(y, x0, x1) - if alpha == 255 && b.opaque { - fill32(d, b.c) + if b.opaque { + if alpha == 255 { + fill32(d, b.c) + } else if alpha != 0 { + // As BlitCoverage blends it: how a row is cut into runs and + // coverage depends on the clip, the pixels must not. + runLerp(d, b.c, uint32(alpha)) + } return } s := b.c @@ -193,6 +199,16 @@ func runOverScalar(d []uint32, s uint32) { } } +// runLerpScalar blends the opaque color c into every pixel with coverage +// a: the pixels of covOpaqueScalar for a constant coverage. +func runLerpScalar(d []uint32, c, a uint32) { + ca := expand(c) * uint64(a) + inv := uint64(255 - a) + for i, v := range d { + d[i] = compact(div255x(expand(v)*inv + ca)) + } +} + // covOpaqueScalar blends an opaque color c (cx expanded) with coverage. func covOpaqueScalar(d []uint32, cov []uint8, c uint32, cx uint64) { d = d[:len(cov)] diff --git a/doc.go b/doc.go index 47b6550..804fac4 100644 --- a/doc.go +++ b/doc.go @@ -54,6 +54,11 @@ // concurrent use; use one per worker and give each worker its own band or // tile of the destination (Canvas.Reset takes a region). // +// A pixel gets the same value in whatever region it is drawn: a scene +// drawn in bands or tiles is, byte for byte, the scene drawn at once. How +// many workers draw a page, and how a viewer cuts it into tiles, does not +// show in it. +// // # Robustness // // NaN and infinite coordinates are rejected, huge ones are clipped diff --git a/raster.go b/raster.go index 5753753..db10398 100644 --- a/raster.go +++ b/raster.go @@ -52,23 +52,97 @@ const ( maxSegs = 1024 ) -// edge is a line segment in fixed point 24.8, x relative to the clip's left -// edge and y relative to its top edge, oriented so that y0 < y1. +// edge is a line segment of a path, oriented downward, as the line it +// lies on and the rows of it that are drawn. Coordinates are fixed point +// 24.8, x relative to the clip's left edge and y to its top edge. +// +// The line is that of the segment as the path gives it, not of its end +// points rounded to fixed point, and it is measured in device space, not +// from the clip: where it passes a row depends neither on where the +// segment ends nor on the clip. A segment the clip cuts, or one a caller +// shortened outside the clip, is thereby drawn like the whole one, and a +// path covers a pixel the same whether it is drawn at once, in bands or +// in tiles. type edge struct { - x0, y0, x1, y1 int32 - dir int32 // +1 downward in the original path, -1 upward - dxdy float64 // (x1-x0)/(y1-y0) - dydx float64 // |(y1-y0)/(x1-x0)|; 0 until first needed (vertical edges never need it) + // In device fixed point, x at y is c + y·dxdy - xAbs, rounded down + // (see xAt): c holds the half that rounds and the bias that makes + // the conversion a floor. + c float64 + dxdy float64 + dydx float64 // 1/dxdy; 0 until first needed (vertical edges never need it) + // For a plain edge, lo and hi are its x at y0 and at y1. For any + // other they bound x: the x of the segment's ends, xSlack beyond + // them. They are equal for an edge whose x is the same in all its + // rows, a vertical one above all: it is drawn as that. + lo, hi int32 + y0, y1 int32 // the rows drawn, y0 < y1 + dir int32 // +1 downward in the original path, -1 upward + // wide says that the edge leaves the clip at its left or right within + // its rows: its x is then not a cell of the accumulator everywhere + // (see rasterWide). The x of any other edge is in [0, w·256]. + wide bool + // plain says that the edge is not nearly level (see plainSlope): its + // line stays near the segment in all its rows, whatever the clip, and + // its x needs no bounding (xPlain). + plain bool } -// xAt returns the edge's x at fixed-point y (y0 <= y <= y1). The same formula -// is used on both sides of every band and row boundary, so adjacent pieces of -// an edge always meet exactly. -func (e *edge) xAt(y int32) int32 { - if y >= e.y1 { - return e.x1 - } - return e.x0 + int32(float64(y-e.y0)*e.dxdy+0.5+float64(1<<20)) - (1 << 20) +const ( + // xFar bounds the x of an edge, in fixed point from the clip's left. + xFar = 1 << 29 + // xAbs bounds the device x of a line, in fixed point, where it is + // rounded; what lies beyond is at an end of its edge. + xAbs = 1 << 36 + // xSlack is how far beyond the ends of its segment the x of an edge + // may lie, in fixed point. The rows of an edge start and end on + // 1/256 pixel, up to half of that beside the segment's ends, where + // the line of a nearly level segment is far beyond them. + xSlack = 256 + // plainSlope is the dx/dy up to which an edge is plain: half a step + // of y moves its line by half of xSlack at most, so it never leaves + // the slack. Only edges within a quarter of a degree of level are + // steeper in x. + plainSlope = 256 + // nearCoord bounds the device coordinates up to which a line is + // measured from the segment's own point, alike for every clip. + nearCoord = 1 << 26 +) + +// xAt returns the x at fixed-point y (y0 <= y <= y1) of an edge that is +// not plain, both relative to the clip, whose top is oy in device fixed +// point and whose left, with xAbs added, bo. The line is evaluated in +// device space and the result moved to the clip in integers: it is the +// same pixel position for every clip. The same formula is used on both +// sides of every band and row boundary, so adjacent pieces of an edge +// always meet exactly. +func (e *edge) xAt(y int32, bo, oy int64) int32 { + v := e.c + float64(float64(int64(y)+oy)*e.dxdy) + if !(v > 0) { // also NaN + return e.lo + } + if v > 2*xAbs { + return e.hi + } + x := int64(v) - bo + if x < int64(e.lo) { + return e.lo + } + if x > int64(e.hi) { + return e.hi + } + return int32(x) +} + +// xPlain is xAt for a plain edge: its line as it is. +// +// The product is rounded before it is added, here and wherever a line is +// evaluated (the conversion says so): a compiler may fuse a multiplication +// and an addition into one operation with one rounding, and need not do +// so alike everywhere. The x of an edge must be the same wherever it is +// computed — AddLine keeps what it is at the ends, and the cells an edge +// writes to are in range because it lies between them. +func (e *edge) xPlain(y int32, bo, oy int64) int32 { + return int32(int64(e.c+float64(float64(int64(y)+oy)*e.dxdy)) - bo) } // Rasterizer converts paths into coverage spans. Its cost per path is the @@ -86,6 +160,9 @@ type Rasterizer struct { cy0 float64 cx1 float64 cy1 float64 + ox, oy int64 // the clip's corner in device fixed point + bo, yo int64 // ox and oy with xAbs added + near bool // the clip lies within nearCoord: its rows convert without checks w, h int stride int // cells per accumulation row: w + 2 nw int // dirty words per row @@ -140,6 +217,9 @@ func (r *Rasterizer) SetClip(clip image.Rectangle) { r.cx0, r.cy0 = float64(clip.Min.X), float64(clip.Min.Y) r.cx1, r.cy1 = float64(clip.Max.X), float64(clip.Max.Y) r.ry0, r.ry1 = r.cy0, r.cy1 + r.ox, r.oy = int64(clip.Min.X)<<8, int64(clip.Min.Y)<<8 + r.bo, r.yo = r.ox+xAbs, r.oy+xAbs + r.near = clip.Min.X > -nearCoord && clip.Min.Y > -nearCoord && clip.Max.X < nearCoord && clip.Max.Y < nearCoord r.w, r.h = clip.Dx(), clip.Dy() r.stride = r.w + 2 r.nw = (r.stride>>blkShift)/64 + 1 @@ -437,10 +517,10 @@ func segCount(v float64) int { return int(math.Ceil(math.Sqrt(v))) } -// AddLine adds a device-space line segment. Segments are clipped -// analytically: parts above or below the clip are dropped, parts right of it -// are dropped, and parts left of it become vertical edges on the clip's left -// border, which preserves the winding of everything inside. +// AddLine adds a device-space line segment. Of a segment the rows inside +// the clip are kept; where it lies right of the clip it adds nothing, and +// where it lies left of it it counts as an edge on the clip's left border, +// which preserves the winding of everything inside. func (r *Rasterizer) AddLine(x0, y0, x1, y1 float64) { r.begin() if y0 == y1 || !(math.Abs(x0) <= math.MaxFloat64 && math.Abs(x1) <= math.MaxFloat64 && @@ -456,41 +536,79 @@ func (r *Rasterizer) AddLine(x0, y0, x1, y1 float64) { if y1 <= cy0 || y0 >= cy1 { return } - if y0 < cy0 { - x0 = lineAt(cy0, y0, y1, x0, x1) - y0 = cy0 + fy0 := r.fixedY(max(y0, cy0)) + fy1 := r.fixedY(min(y1, cy1)) + if fy0 >= fy1 { + return } - if y1 > cy1 { - x1 = lineAt(cy1, y0, y1, x0, x1) - y1 = cy1 + right := int32(r.w << 8) + var e edge + var xa, xb int32 + if x0 == x1 { + x := r.fixedFar(x0) + if x >= right { + return + } + x = max(x, 0) + e = edge{lo: x, hi: x, y0: fy0, y1: fy1, dir: dir} + xa, xb = x, x + } else { + dy := y1 - y0 + dxdy := (x1 - x0) / dy + if dy > math.MaxFloat64 { + dxdy = (x1*0.5 - x0*0.5) / (y1*0.5 - y0*0.5) + } + // The line is measured from the segment's upper end: from the + // same point for every clip. A segment that comes from very far + // away is measured from where it enters the clip, which keeps + // the numbers small. + ax, ay := x0, y0 + if y0 < cy0 && !(math.Abs(x0) < nearCoord && math.Abs(y0) < nearCoord && math.Abs(x1) < nearCoord && math.Abs(y1) < nearCoord) { + ax, ay = lineAt(cy0, y0, y1, x0, x1), cy0 + } + e = edge{c: (ax-ay*dxdy)*256 + (0.5 + xAbs), dxdy: dxdy, y0: fy0, y1: fy1, dir: dir} + // Plain by its slope, not by this clip: an edge is drawn by the + // same arithmetic in every clip (but one it is too far from to + // be counted in fixed point: a conversion out of range gives a + // number out of range here, whatever it is). + a := int64(e.c+float64(float64(int64(fy0)+r.oy)*dxdy)) - r.bo + b := int64(e.c+float64(float64(int64(fy1)+r.oy)*dxdy)) - r.bo + if math.Abs(dxdy) <= plainSlope && uint64(a+xFar) < 2*xFar && uint64(b+xFar) < 2*xFar { + xa, xb = int32(a), int32(b) + e.lo, e.hi, e.plain = xa, xb, true + } else { + e.lo = r.fixedFar(min(x0, x1)) - xSlack + e.hi = r.fixedFar(max(x0, x1)) + xSlack + xa, xb = e.xAt(fy0, r.bo, r.oy), e.xAt(fy1, r.bo, r.oy) + } + switch { + case xa >= right && xb >= right: + return + case xa <= 0 && xb <= 0: + e = edge{y0: fy0, y1: fy1, dir: dir} + xa, xb = 0, 0 + case xa < 0 || xb < 0 || xa > right || xb > right: + e.wide = true + xa, xb = min(max(xa, 0), right), min(max(xb, 0), right) + } } - cx0, cx1 := r.cx0, r.cx1 - if x0 >= cx1 && x1 >= cx1 { - return + limit := r.MaxEdges + if limit == 0 { + limit = DefaultMaxEdges } - if x0 <= cx0 && x1 <= cx0 { - r.emit(cx0, y0, cx0, y1, dir) + if len(r.edges) >= limit { + r.truncated = true return } - if x0 < cx0 || x1 < cx0 { - yc := lineAt(cx0, x0, x1, y0, y1) - if x0 < cx0 { - r.emit(cx0, y0, cx0, yc, dir) - x0, y0 = cx0, yc - } else { - r.emit(cx0, yc, cx0, y1, dir) - x1, y1 = cx0, yc - } + r.edges = append(r.edges, e) + r.minX = min(r.minX, xa, xb) + r.maxX = max(r.maxX, xa, xb) + if fy0 < r.minY { + r.minY = fy0 } - if x0 > cx1 || x1 > cx1 { - yc := lineAt(cx1, x0, x1, y0, y1) - if x0 > cx1 { - x0, y0 = cx1, yc - } else { - x1, y1 = cx1, yc - } + if fy1 > r.maxY { + r.maxY = fy1 } - r.emit(x0, y0, x1, y1, dir) } // lineAt returns the value at v of the line through (a, fa) and (b, fb), @@ -518,37 +636,35 @@ func fixed(v, o float64, n int) int32 { return int32(f) } -// emit stores a clipped segment (y0 <= y1) as a fixed-point edge. -func (r *Rasterizer) emit(x0, y0, x1, y1 float64, dir int32) { - fy0 := fixed(y0, r.cy0, r.h) - fy1 := fixed(y1, r.cy0, r.h) - if fy0 >= fy1 { - return - } - limit := r.MaxEdges - if limit == 0 { - limit = DefaultMaxEdges - } - if len(r.edges) >= limit { - r.truncated = true - return - } - fx0 := fixed(x0, r.cx0, r.w) - fx1 := fixed(x1, r.cx0, r.w) - // dydx is computed when a row of the edge first crosses a cell border - // (rasterEdge); most edges of small shapes never do. - r.edges = append(r.edges, edge{ - x0: fx0, y0: fy0, x1: fx1, y1: fy1, dir: dir, - dxdy: float64(fx1-fx0) / float64(fy1-fy0), - }) - r.minX = min(r.minX, fx0, fx1) - r.maxX = max(r.maxX, fx0, fx1) - if fy0 < r.minY { - r.minY = fy0 +// fixedY converts a device y inside the clip's rows into 24.8 fixed point +// relative to the clip's top. It is rounded in device space and moved to +// the clip then: the same y lands on the same place for every clip. +func (r *Rasterizer) fixedY(v float64) int32 { + if r.near { + // v is a y of the clip: the sum is positive, and its floor lies + // in the clip's rows. + return int32(int64(v*256+(0.5+xAbs)) - r.yo) + } + var y int64 + if f := v*256 + (0.5 + xAbs); f > 0 && f < 2*xAbs { + y = int64(f) - xAbs - r.oy + } else if g := math.Floor(v*256+0.5) - float64(r.oy); g > 0 { // not NaN + y = int64(min(g, float64(r.h<<8))) + } + return int32(min(max(y, 0), int64(r.h<<8))) +} + +// fixedFar converts a device x into 24.8 fixed point relative to the +// clip's left, clamped to ±xFar: an x that may lie outside the clip. +func (r *Rasterizer) fixedFar(v float64) int32 { + f := v*256 + (0.5 + xAbs) + if !(f > 0) { // also NaN + return -xFar } - if fy1 > r.maxY { - r.maxY = fy1 + if f > 2*xAbs { + return xFar } + return int32(min(max(int64(f)-r.bo, -xFar), xFar)) } // reverse reverses the orientation of the edges added since there were n. @@ -804,23 +920,27 @@ func (r *Rasterizer) rasterStrip(s *accStrip, bandRow int) uint64 { a, b = ia, ib } } + // Each pixel from its own place (see rowCtx.centre). + x, g := rc.rowAt(i0, fy) var prev int64 for i := 0; i < a; i++ { + d := float64(nx*x) + g + x++ v := int64((tr.area(k1-d) - tr.area(k2-d)) * fullArea) acc[i] += v - prev prev = v - d += nx } if b > a { acc[a] += fullArea - prev prev = fullArea - d += float64(b-a) * nx } + x, _ = rc.rowAt(i0+b, fy) for i := b; i < n; i++ { + d := float64(nx*x) + g + x++ v := int64((tr.area(k1-d) - tr.area(k2-d)) * fullArea) acc[i] += v - prev prev = v - d += nx } acc[n] -= prev if !r.narrow { @@ -847,6 +967,9 @@ func (r *Rasterizer) rasterEdge(e *edge, bandRow int) uint64 { if yA >= yB { return 0 } + if e.wide { + return r.rasterWide(e, bandRow, yA, yB) + } stride := r.stride acc := r.acc dirty := r.dirty @@ -854,10 +977,10 @@ func (r *Rasterizer) rasterEdge(e *edge, bandRow int) uint64 { dir := e.dir var mask uint64 - if e.x0 == e.x1 { - // Vertical edge: one cell per row. - c := int(e.x0 >> 8) - fx2 := (e.x0 & 255) * 2 + if e.lo == e.hi { + // Vertical edge, or one that stays at its x: one cell per row. + c := int(e.lo >> 8) + fx2 := (e.lo & 255) * 2 wA := dir * (512 - fx2) wB := dir * fx2 bw, bb := c>>(blkShift+6), uint64(1)<<(uint(c>>blkShift)&63) @@ -889,7 +1012,19 @@ func (r *Rasterizer) rasterEdge(e *edge, bandRow int) uint64 { return mask } - x := e.xAt(yA) + bo, oy := r.bo, r.oy + // The ends of a plain edge are known; most edges of small shapes + // have no rows between them. + plain := e.plain + var x int32 + switch { + case !plain: + x = e.xAt(yA, bo, oy) + case yA == e.y0: + x = e.lo + default: + x = e.xPlain(yA, bo, oy) + } // Indices below are in range by construction: x ∈ [0, w·256], so a cell // and its right neighbour are < stride, and ri < bandH. The single-cell // case, by far the most common, is written without bounds checks. @@ -900,7 +1035,15 @@ func (r *Rasterizer) rasterEdge(e *edge, bandRow int) uint64 { if rowEnd > yB { rowEnd = yB } - xn := e.xAt(rowEnd) + var xn int32 + switch { + case !plain: + xn = e.xAt(rowEnd, bo, oy) + case rowEnd == e.y1: + xn = e.hi + default: + xn = e.xPlain(rowEnd, bo, oy) + } ri := int(y>>8) - bandRow c0, c1 := x>>8, xn>>8 var lo, hi int32 @@ -925,9 +1068,14 @@ func (r *Rasterizer) rasterEdge(e *edge, bandRow int) uint64 { dr = dirty[ri*nw : (ri+1)*nw] } if e.dydx == 0 { - e.dydx = math.Abs(float64(e.y1-e.y0) / float64(e.x1-e.x0)) + e.dydx = 1 / e.dxdy + } + if plain { + lo, hi = cellLine(acc[ri*stride:(ri+1)*stride], dr, x, y-base, xn, rowEnd-base, dir, math.Abs(e.dydx)) + } else { + l := e.at(x, y-base, base, r.ox, oy) + lo, hi = cellLineFlat(acc[ri*stride:(ri+1)*stride], dr, x, y-base, xn, rowEnd-base, dir, &l) } - lo, hi = cellLine(acc[ri*stride:(ri+1)*stride], dr, x, y-base, xn, rowEnd-base, dir, e.dydx) } if lo < r.rmin[ri] { r.rmin[ri] = lo @@ -941,9 +1089,130 @@ func (r *Rasterizer) rasterEdge(e *edge, bandRow int) uint64 { return mask } -// cellLine accumulates a segment that lies within one pixel row, from -// (x0, fy0) to (x1, fy1), with 0 <= fy0 < fy1 <= 256, and returns the range -// of touched cells [lo, hi]. +// rasterWide is rasterEdge for an edge that leaves the clip at its left +// or right, rows yA to yB of it. It accumulates what the edge would in a +// clip wide enough for it and keeps the part of the clip's cells: what +// lies left of the clip is summed into its first cell, as an edge on the +// border would be, and what lies right of it is dropped. The cells +// inside are thereby the same for every clip. +func (r *Rasterizer) rasterWide(e *edge, bandRow int, yA, yB int32) uint64 { + stride := r.stride + nw := r.nw + dir := e.dir + last := int32(r.w) // the cell after the clip's; it and its neighbour exist + var mask uint64 + if e.dydx == 0 && e.dxdy != 0 { + e.dydx = 1 / e.dxdy + } + // A plain edge has the x of a plain one here too: in a clip that + // holds it, it is drawn by rasterEdge. + bo, oy := r.bo, r.oy + plain := e.plain + at := func(y int32) int32 { + if plain { + return e.xPlain(y, bo, oy) + } + return e.xAt(y, bo, oy) + } + x := at(yA) + for y := yA; y < yB; { + rowEnd := (y | 255) + 1 + if rowEnd > yB { + rowEnd = yB + } + xn := at(rowEnd) + ri := int(y>>8) - bandRow + c0, c1 := x>>8, xn>>8 + if c0 > last && c1 > last { + x, y = xn, rowEnd + continue + } + acc := r.acc[ri*stride : (ri+1)*stride] + var dr []uint64 + if !r.narrow { + dr = r.dirty[ri*nw : (ri+1)*nw] + } + var lo, hi int32 + switch { + case c0 < 0 && c1 < 0: + acc[0] += int64((rowEnd - y) * dir * 512) + markRange(dr, 0, 1) + lo, hi = 0, 1 + case c0 == c1: + d := (rowEnd - y) * dir + sx := (x & 255) + (xn & 255) + acc[c0] += int64(d * (512 - sx)) + acc[c0+1] += int64(d * sx) + markRange(dr, int(c0), int(c0)+1) + lo, hi = c0, c0+1 + default: + base := y &^ 255 + l := e.at(x, y-base, base, r.ox, oy) + lo, hi = cellLineWide(acc, dr, x, y-base, xn, rowEnd-base, dir, &l, last) + } + if lo < r.rmin[ri] { + r.rmin[ri] = lo + } + if hi > r.rmax[ri] { + r.rmax[ri] = hi + } + mask |= 1 << uint(ri) + x, y = xn, rowEnd + } + return mask +} + +// rowLine says where an edge crosses the borders of the cells in a pixel +// row. +// +// A plain edge crosses them as the straight piece from where it enters +// the row, (x0, fy0), with its slope: both are the same for every clip. +// +// Any other edge may enter the row at an end of its bounds, off its line, +// and that end depends on where a caller cut the segment outside the +// clip. Its crossings are taken from the line itself: in the row that +// starts at fixed-point y base of a clip whose corner is (ox, oy), it +// reaches the clip's x at (x + ox - c)·dydx - base, counted from the +// row's top, with c and base in device fixed point. +type rowLine struct { + plain bool + x0, fy0 int32 + dydx float64 // plain: |dy/dx| + c, base, offx float64 // base less the half that rounds +} + +// at returns the crossings of e, whose dydx is set, in the row at base, +// which it enters at (x0, fy0). +func (e *edge) at(x0, fy0, base int32, ox, oy int64) rowLine { + if e.plain { + return rowLine{plain: true, x0: x0, fy0: fy0, dydx: math.Abs(e.dydx)} + } + return rowLine{dydx: e.dydx, c: e.c - (0.5 + xAbs), base: float64(int64(base)+oy) - 0.5, offx: float64(ox)} +} + +// crossing returns the y in the row at which the edge reaches x, no less +// than ya and no more than yb. +func (l *rowLine) crossing(x, ya, yb int32) int32 { + if l.plain { + d := x - l.x0 + if d < 0 { + d = -d + } + return max(min(l.fy0+int32(float64(float64(d)*l.dydx)+0.5), yb), ya) + } + t := float64((float64(x)+l.offx-l.c)*l.dydx) - l.base + if !(t > float64(ya)) { // also NaN + return ya + } + if t >= float64(yb) { + return yb + } + return int32(t) +} + +// cellLine accumulates a segment of a plain edge that lies within one +// pixel row, from (x0, fy0) to (x1, fy1), with 0 <= fy0 < fy1 <= 256, and +// returns the range of touched cells [lo, hi]. dydx is |dy/dx|. func cellLine(acc []int64, dirty []uint64, x0, fy0, x1, fy1, dir int32, dydx float64) (lo, hi int32) { c0 := x0 >> 8 c1 := x1 >> 8 @@ -959,7 +1228,56 @@ func cellLine(acc []int64, dirty []uint64, x0, fy0, x1, fy1, dir int32, dydx flo xa, ya := x0, fy0 for c := c0; c < c1; c++ { bx := (c + 1) << 8 - yb := min(fy0+int32(float64(bx-x0)*dydx+0.5), fy1) + yb := min(fy0+int32(float64(float64(bx-x0)*dydx)+0.5), fy1) + d := (yb - ya) * dir + s := (xa - c<<8) + 256 + acc[c] += int64(d * (512 - s)) + acc[c+1] += int64(d * s) + xa, ya = bx, yb + } + d := (fy1 - ya) * dir + s := x1 - c1<<8 + acc[c1] += int64(d * (512 - s)) + acc[c1+1] += int64(d * s) + markRange(dirty, int(c0), int(c1)+1) + return c0, c1 + 1 + } + xa, ya := x0, fy0 + for c := c0; c > c1; c-- { + bx := c << 8 + yb := min(fy0+int32(float64(float64(x0-bx)*dydx)+0.5), fy1) + d := (yb - ya) * dir + s := xa - c<<8 + acc[c] += int64(d * (512 - s)) + acc[c+1] += int64(d * s) + xa, ya = bx, yb + } + d := (fy1 - ya) * dir + s := 256 + (x1 - c1<<8) + acc[c1] += int64(d * (512 - s)) + acc[c1+1] += int64(d * s) + markRange(dirty, int(c1), int(c0)+1) + return c1, c0 + 1 +} + +// cellLineFlat is cellLine for an edge that is not plain: it crosses the +// borders of the cells where l says. +func cellLineFlat(acc []int64, dirty []uint64, x0, fy0, x1, fy1, dir int32, l *rowLine) (lo, hi int32) { + c0 := x0 >> 8 + c1 := x1 >> 8 + if c0 == c1 { + d := (fy1 - fy0) * dir + s := (x0 & 255) + (x1 & 255) + acc[c0] += int64(d * (512 - s)) + acc[c0+1] += int64(d * s) + markRange(dirty, int(c0), int(c0)+1) + return c0, c0 + 1 + } + if x1 > x0 { + xa, ya := x0, fy0 + for c := c0; c < c1; c++ { + bx := (c + 1) << 8 + yb := l.crossing(bx, ya, fy1) d := (yb - ya) * dir s := (xa - c<<8) + 256 acc[c] += int64(d * (512 - s)) @@ -976,7 +1294,7 @@ func cellLine(acc []int64, dirty []uint64, x0, fy0, x1, fy1, dir int32, dydx flo xa, ya := x0, fy0 for c := c0; c > c1; c-- { bx := c << 8 - yb := min(fy0+int32(float64(x0-bx)*dydx+0.5), fy1) + yb := l.crossing(bx, ya, fy1) d := (yb - ya) * dir s := xa - c<<8 acc[c] += int64(d * (512 - s)) @@ -991,6 +1309,76 @@ func cellLine(acc []int64, dirty []uint64, x0, fy0, x1, fy1, dir int32, dydx flo return c1, c0 + 1 } +// cellLineWide is cellLine for a segment whose cells are not all the +// clip's, of which last is the one after them: cells left of the first +// are summed into it, cells after last are left out. Each cell gets what +// cellLine or cellLineFlat gives it. +func cellLineWide(acc []int64, dirty []uint64, x0, fy0, x1, fy1, dir int32, l *rowLine, last int32) (lo, hi int32) { + c0 := x0 >> 8 + c1 := x1 >> 8 + put := func(c int32, a, b int32) { + switch { + case c < 0: + acc[0] += int64(a) + int64(b) + case c <= last: + acc[c] += int64(a) + acc[c+1] += int64(b) + } + } + if x1 > x0 { + xa, ya := x0, fy0 + c := c0 + if c < -1 { + // The cells before the one left of the clip, at once. + c = -1 + xa, ya = -256, l.crossing(-256, fy0, fy1) + acc[0] += int64((ya - fy0) * dir * 512) + } + for ; c < c1 && c <= last; c++ { + bx := (c + 1) << 8 + yb := l.crossing(bx, ya, fy1) + d := (yb - ya) * dir + s := (xa - c<<8) + 256 + put(c, d*(512-s), d*s) + xa, ya = bx, yb + } + if c == c1 { + d := (fy1 - ya) * dir + s := x1 - c1<<8 + put(c1, d*(512-s), d*s) + } + lo, hi = max(c0, 0), min(c1, last)+1 + markRange(dirty, int(lo), int(hi)) + return lo, hi + } + xa, ya := x0, fy0 + c := c0 + if c > last { + // The cells after last add nothing. + c = last + xa, ya = (last+1)<<8, l.crossing((last+1)<<8, fy0, fy1) + } + for ; c > c1 && c >= 0; c-- { + bx := c << 8 + yb := l.crossing(bx, ya, fy1) + d := (yb - ya) * dir + s := xa - c<<8 + put(c, d*(512-s), d*s) + xa, ya = bx, yb + } + if c == c1 { + d := (fy1 - ya) * dir + s := 256 + (x1 - c1<<8) + put(c1, d*(512-s), d*s) + } else { + // The rest lies left of the clip. + acc[0] += int64((fy1 - ya) * dir * 512) + } + lo, hi = max(c1, 0), min(c0, last)+1 + markRange(dirty, int(lo), int(hi)) + return lo, hi +} + // markRange sets the dirty bits covering cells [lo, hi]. func markRange(dirty []uint64, lo, hi int) { if dirty == nil { diff --git a/simd_amd64.go b/simd_amd64.go index 545c934..f6d1f6e 100644 --- a/simd_amd64.go +++ b/simd_amd64.go @@ -171,6 +171,31 @@ func runOver(d []uint32, s uint32) { runOverScalar(d[i:], s) } +func runLerp(d []uint32, c, a uint32) { + if !useSIMD || len(d) < 4 { + runLerpScalar(d, c, a) + return + } + if use512 && len(d) >= 8 { + runLerp512(d, c, a) + return + } + db := bytesOf(d) + k128 := archsimd.BroadcastUint16x16(128) + k257 := archsimd.BroadcastUint16x16(257) + pk := loadInt8x32(&packIdx) + ca := archsimd.BroadcastUint32x4(c).AsUint8x16().ExtendToUint16().Mul(archsimd.BroadcastUint16x16(uint16(a))) + inv := archsimd.BroadcastUint16x16(uint16(255 - a)) + i := 0 + for ; i+4 <= len(d); i += 4 { + px := db[i*4 : i*4+16] + d16 := loadUint8x16(px).ExtendToUint16() + storeUint8x16(packLanes(div255v(d16.Mul(inv).Add(ca), k128, k257), pk), px) + } + archsimd.ClearAVXUpperBits() + runLerpScalar(d[i:], c, a) +} + // AVX-512 kernels: eight pixels in 16-bit lanes of one ZMM register. func div255w(x, k128, k257 archsimd.Uint16x32) archsimd.Uint16x32 { @@ -248,6 +273,22 @@ func runOver512(d []uint32, s uint32) { runOverScalar(d[i:], s) } +func runLerp512(d []uint32, c, a uint32) { + db := bytesOf(d) + k128 := archsimd.BroadcastUint16x32(128) + k257 := archsimd.BroadcastUint16x32(257) + ca := archsimd.BroadcastUint32x8(c).AsUint8x32().ExtendToUint16().Mul(archsimd.BroadcastUint16x32(uint16(a))) + inv := archsimd.BroadcastUint16x32(uint16(255 - a)) + i := 0 + for ; i+8 <= len(d); i += 8 { + px := db[i*4 : i*4+32] + d16 := loadUint8x32(px).ExtendToUint16() + storeUint8x32(truncToUint8(div255w(d16.Mul(inv).Add(ca), k128, k257)), px) + } + archsimd.ClearAVXUpperBits() + runLerpScalar(d[i:], c, a) +} + // spanOver and spanOverCov composite varying source pixels: a shader's // span, or a layer's or an image's own row. diff --git a/simd_generic.go b/simd_generic.go index 273860e..ae27275 100644 --- a/simd_generic.go +++ b/simd_generic.go @@ -7,6 +7,7 @@ package stilus func SIMD() bool { return false } func runOver(d []uint32, s uint32) { runOverScalar(d, s) } +func runLerp(d []uint32, c, a uint32) { runLerpScalar(d, c, a) } func covOpaque(d []uint32, cov []uint8, c uint32, cx uint64) { covOpaqueScalar(d, cov, c, cx) } func covOver(d []uint32, cov []uint8, cx uint64) { covOverScalar(d, cov, cx) } func spanOver(d, s []uint32, k, a uint32) { spanOverScalar(d, s, k, a) } diff --git a/strip.go b/strip.go index c478a12..e541fc4 100644 --- a/strip.go +++ b/strip.go @@ -491,9 +491,21 @@ func (rc *rowCtx) span(j, cx0, cx1 int) (i0, i1 int, fy float64) { return max(ffloor(rc.xl+fy*rc.sl), cx0), min(ffloor(rc.xr+fy*rc.sl)+1, cx1), fy } -// centre returns n·c of the centre of pixel (i0, fy). +// centre returns n·c of the centre of pixel (i0, fy). Every pixel of a +// row is measured this way, from its own place, and not by steps of nx +// from the first one drawn: where a row starts depends on the clip, what +// a pixel gets must not. Loops over a row count the centres' x instead, +// which is exact (see rowAt). func (rc *rowCtx) centre(i0 int, fy float64) float64 { - return rc.nx*(float64(i0)+0.5) + rc.ny*(fy+0.5) + return float64(rc.nx*(float64(i0)+0.5)) + float64(rc.ny*(fy+0.5)) +} + +// rowAt returns the x of the centre of pixel i0 and the part of n·c that +// row fy adds: the centre of the pixel k further has n·c = nx·(x+k) + g, +// which is centre(i0+k, fy) to the bit (the products rounded before they +// are added, on every machine). +func (rc *rowCtx) rowAt(i0 int, fy float64) (x, g float64) { + return float64(i0) + 0.5, rc.ny * (fy + 0.5) } // borders returns the clip's partially covered border columns (the first @@ -522,7 +534,7 @@ func (f *segFast) solidRows(y0, y1 int, rc *rowCtx) { // last; they are taken out of the loop. if y0 == clip.Min.Y && f.frac[1] != 255 { if i0, i1, fy := rc.span(y0, cx0, cx1); i0 < i1 { - f.row(y0, i0, i1, rc.centre(i0, fy), rc) + f.row(y0, i0, i1, fy, rc) } y0++ } @@ -539,15 +551,16 @@ func (f *segFast) solidRows(y0, y1 int, rc *rowCtx) { if i0 >= i1 { continue } - d := rc.centre(i0, fy) if i0 == rc.col0 || i1 == rc.col1 || (SIMD() && i1-i0 >= 16) { - f.row(j, i0, i1, d, rc) + f.row(j, i0, i1, fy, rc) continue } row := solid.t.row(j, i0, i1) + x, g := rc.rowAt(i0, fy) for i, dst := range row { + d := float64(nx*x) + g + x++ a := quant(tr.area(k1-d) - tr.area(k2-d)) - d += nx switch a { case 0: case 255: @@ -559,7 +572,7 @@ func (f *segFast) solidRows(y0, y1 int, rc *rowCtx) { } if last >= 0 { if i0, i1, fy := rc.span(last, cx0, cx1); i0 < i1 { - f.row(last, i0, i1, rc.centre(i0, fy), rc) + f.row(last, i0, i1, fy, rc) } } } @@ -576,50 +589,50 @@ func (f *segFast) blitRows(y0, y1 int, rc *rowCtx) { if i0 >= i1 { continue } - d0 := rc.centre(i0, fy) if j == f.by0 || j == f.by1 { - f.borderRow(j, i0, i1, d0, rc) + f.borderRow(j, i0, i1, fy, rc) continue } if (f.bx0 >= i0 && f.bx0 < i1) || (f.bx1 >= i0 && f.bx1 < i1) { - f.borderCols(j, i0, i1, d0, rc) + f.borderCols(j, i0, i1, fy, rc) continue } if rc.runs { - f.emitRow(j, i0, i1, d0, rc) + f.emitRow(j, i0, i1, fy, rc) continue } // A thin strip's row: one coverage span (emitRow, inline). cov := f.cov[:i1-i0] - d := d0 + x, g := rc.rowAt(i0, fy) for i := range cov { + d := float64(nx*x) + g + x++ cov[i] = quant(tr.area(k1-d) - tr.area(k2-d)) - d += nx } emitCoverage(f.b, j, i0, cov) } } -// row handles pixels [i0, i1) of row j (d0 = n·centre of pixel i0) that +// row handles pixels [i0, i1) of row j (fy as rowCtx.span gives it) that // solidRows cannot take: rows in a partially covered clip border row or // crossing a border column go to the accumulator, the rest through the // blitter chain. -func (f *segFast) row(j, i0, i1 int, d0 float64, rc *rowCtx) { +func (f *segFast) row(j, i0, i1 int, fy float64, rc *rowCtx) { if j == f.by0 || j == f.by1 { - f.borderRow(j, i0, i1, d0, rc) + f.borderRow(j, i0, i1, fy, rc) return } if (f.bx0 >= i0 && f.bx0 < i1) || (f.bx1 >= i0 && f.bx1 < i1) { - f.borderCols(j, i0, i1, d0, rc) + f.borderCols(j, i0, i1, fy, rc) return } - f.emitRow(j, i0, i1, d0, rc) + f.emitRow(j, i0, i1, fy, rc) } -// emitRow blits pixels [i0, i1) of row j (d0 = n·centre of pixel i0) -// through the blitter chain: edge pixels as coverage, a fully covered -// interior as a run. -func (f *segFast) emitRow(j, i0, i1 int, d0 float64, rc *rowCtx) { +// emitRow blits pixels [i0, i1) of row j through the blitter chain: edge +// pixels as coverage, a fully covered interior as a run. +func (f *segFast) emitRow(j, i0, i1 int, fy float64, rc *rowCtx) { + d0 := rc.centre(i0, fy) n := i1 - i0 a, b := n, n // interior [a, b) relative to i0; none by default if rc.runs { @@ -634,10 +647,10 @@ func (f *segFast) emitRow(j, i0, i1 int, d0 float64, rc *rowCtx) { } } cov := f.cov[:n] - d := d0 + x, g := rc.rowAt(i0, fy) for i := 0; i < a; i++ { - cov[i] = quant(rc.cov(d)) - d += rc.nx + cov[i] = quant(rc.cov(float64(rc.nx*x) + g)) + x++ } if a > 0 { emitCoverage(f.b, j, i0, cov[:a]) @@ -645,10 +658,10 @@ func (f *segFast) emitRow(j, i0, i1 int, d0 float64, rc *rowCtx) { if b > a { f.b.BlitRun(j, i0+a, i0+b, 255) } - d = d0 + float64(b)*rc.nx + x, _ = rc.rowAt(i0+b, fy) for i := b; i < n; i++ { - cov[i] = quant(rc.cov(d)) - d += rc.nx + cov[i] = quant(rc.cov(float64(rc.nx*x) + g)) + x++ } if n > b { emitCoverage(f.b, j, i0+b, cov[b:n]) @@ -658,33 +671,34 @@ func (f *segFast) emitRow(j, i0, i1 int, d0 float64, rc *rowCtx) { // borderCols handles a row crossing a partially covered clip border // column: the border pixels go to the accumulator, the pieces between them // are emitted like any other row. -func (f *segFast) borderCols(j, i0, i1 int, d0 float64, rc *rowCtx) { +func (f *segFast) borderCols(j, i0, i1 int, fy float64, rc *rowCtx) { x := i0 for _, bx := range [2]int{min(f.bx0, f.bx1), max(f.bx0, f.bx1)} { if bx < x || bx >= i1 { continue } if bx > x { - f.emitRow(j, x, bx, d0+float64(x-i0)*rc.nx, rc) + f.emitRow(j, x, bx, fy, rc) } - f.inject(bx, j, rc.cov(d0+float64(bx-i0)*rc.nx)) + f.inject(bx, j, rc.cov(rc.centre(bx, fy))) x = bx + 1 } if x < i1 { - f.emitRow(j, x, i1, d0+float64(x-i0)*rc.nx, rc) + f.emitRow(j, x, i1, fy, rc) } } // borderRow sends a row lying in a partially covered clip border row to // the accumulator: fully covered stretches as one rectangle each, edge // pixels one by one. -func (f *segFast) borderRow(j, i0, i1 int, d float64, rc *rowCtx) { +func (f *segFast) borderRow(j, i0, i1 int, fy float64, rc *rowCtx) { // The current fully covered stretch starts at runStart while inRun; // x can be negative, so no coordinate doubles as the marker. inRun, runStart := false, 0 + xc, g := rc.rowAt(i0, fy) for x := i0; x < i1; x++ { - c := rc.cov(d) - d += rc.nx + c := rc.cov(float64(rc.nx*xc) + g) + xc++ if c >= 1-1e-12 { if !inRun { inRun, runStart = true, x diff --git a/stroke.go b/stroke.go index 23e5bdc..4ce6686 100644 --- a/stroke.go +++ b/stroke.go @@ -254,8 +254,11 @@ func (s *Stroker) run(p *Path, m Matrix, st *StrokeStyle, pr strokePrep) { } // A chord changes the outline only outside the clip, but it shortens - // the path a dash pattern walks. - s.cullCurves = s.cull && !dashed + // the path a dash pattern walks. On the analytic path it also moves + // the rows that are filled analytically, which the vertices of the + // polyline set, those outside the clip too: the pixels inside would + // depend on the clip. + s.cullCurves = s.cull && !dashed && s.seg == nil pts := p.Points pi := 0 closed := false diff --git a/tiles_test.go b/tiles_test.go new file mode 100644 index 0000000..4fc20b2 --- /dev/null +++ b/tiles_test.go @@ -0,0 +1,259 @@ +package stilus + +import ( + "bytes" + "image" + "image/color" + "math/rand/v2" + "testing" +) + +// randomPath returns a path of lines and curves in and around a box of +// the given size, some of them far outside it. +func randomPath(rng *rand.Rand, w, h float64) *Path { + f := func(n float64) float32 { + v := rng.Float64()*1.6 - 0.3 + if rng.IntN(12) == 0 { + v = rng.Float64()*40 - 20 // far away + } + return float32(v * n) + } + var p Path + for range 1 + rng.IntN(3) { + p.MoveTo(f(w), f(h)) + for range 1 + rng.IntN(6) { + switch rng.IntN(5) { + case 0: + p.QuadTo(f(w), f(h), f(w), f(h)) + case 1: + p.CubicTo(f(w), f(h), f(w), f(h), f(w), f(h)) + case 2: + // Nearly level, nearly upright. + x, y := f(w), f(h) + p.LineTo(x, y) + if rng.IntN(2) == 0 { + p.LineTo(f(w), y+float32(rng.Float64()*0.01)) + } else { + p.LineTo(x+float32(rng.Float64()*0.01), f(h)) + } + default: + p.LineTo(f(w), f(h)) + } + } + p.Close() + } + return &p +} + +// tilings are the ways the tests cut a clip: rows, columns, tiles. +func tilings(r image.Rectangle) [][]image.Rectangle { + var out [][]image.Rectangle + for _, size := range []image.Point{{r.Dx(), 7}, {r.Dx(), 32}, {r.Dx(), 33}, {5, r.Dy()}, {64, r.Dy()}, {17, 13}, {1, 1}} { + var tiles []image.Rectangle + for y := r.Min.Y; y < r.Max.Y; y += size.Y { + for x := r.Min.X; x < r.Max.X; x += size.X { + tiles = append(tiles, image.Rect(x, y, x+size.X, y+size.Y).Intersect(r)) + } + } + out = append(out, tiles) + } + return out +} + +// TestFillTiles holds a path filled in tiles against the path filled at +// once: every pixel has the same coverage, whatever the clip. +func TestFillTiles(t *testing.T) { + rng := rand.New(rand.NewPCG(11, 12)) + box := image.Rect(-3, 5, 97, 85) + for it := range 300 { + p := randomPath(rng, 100, 80) + m := []Matrix{Identity, {0.7, 0.4, -0.3, 1.2, 5, -3}, {-1, 0, 0, 1, 100, 0}, {3, 0, 0, 3, -100, -80}}[rng.IntN(4)] + rule := FillRule(rng.IntN(2)) + want := image.NewAlpha(box) + r := NewRasterizer(box) + r.Fill(p, m, rule, &MaskBlitter{want}) + for _, tiles := range tilings(box) { + if it%10 != 0 && tiles[0].Dx() == 1 { + continue // pixel by pixel is slow + } + got := image.NewAlpha(box) + for _, tile := range tiles { + r.SetClip(tile) + r.Fill(p, m, rule, &MaskBlitter{got}) + } + if mean, max := diff(got, want); max > 0 { + t.Fatalf("iteration %d, tiles of %v: mean %.4f max %d", it, tiles[0].Size(), mean, max) + } + } + } +} + +// hashShader paints every pixel a color of its place alone, opaque or +// not. +type hashShader struct{ opaque bool } + +func (h hashShader) ShadeSpan(y, x int, out []uint32) { + for i := range out { + v := uint32(x+i)*0x9e3779b1 ^ uint32(y)*0x85ebca6b + v ^= v >> 15 + v *= 0x2c1b3c6d + v ^= v >> 12 + c := color.RGBA{uint8(v), uint8(v >> 8), uint8(v >> 16), 255} + if !h.opaque { + a := uint32(uint8(v >> 24)) + c = color.RGBA{uint8(uint32(c.R) * a / 255), uint8(uint32(c.G) * a / 255), uint8(uint32(c.B) * a / 255), uint8(a)} + } + out[i] = PackRGBA(c) + } +} + +// randomPaint returns an opaque or a translucent color, or a shader. +func randomPaint(rng *rand.Rand) *Paint { + switch rng.IntN(5) { + case 0: + return &Paint{Shader: hashShader{opaque: true}} + case 1: + return &Paint{Shader: hashShader{}} + case 2: + a := uint32(1 + rng.IntN(254)) + ch := func() uint8 { return uint8(uint32(rng.IntN(256)) * a / 255) } + return &Paint{Color: color.RGBA{ch(), ch(), ch(), uint8(a)}} + } + return &Paint{Color: color.RGBA{uint8(rng.IntN(256)), uint8(rng.IntN(256)), uint8(rng.IntN(256)), 255}} +} + +// TestCanvasTiles holds scenes drawn in tiles against the scenes drawn at +// once: fills and strokes of all kinds, in colors and shaders, under +// clips, over a backdrop that is not plain. A viewer that draws a page in +// tiles, or in bands by several workers, gets the pixels of the page. +func TestCanvasTiles(t *testing.T) { + rng := rand.New(rand.NewPCG(21, 22)) + box := image.Rect(-3, 5, 97, 85) + backdrop := image.NewRGBA(box) + hashShader{opaque: true}.ShadeSpan(0, 0, pixels(backdrop)) + for it := range 400 { + var ops []func(c *Canvas) + m := []Matrix{Identity, {0.7, 0.4, -0.3, 1.2, 5, -3}, {-1, 0, 0, 1, 100, 0}, {3, 0, 0, 3, -100, -80}, {1.5, 0, 0, -1.5, 0, 90}}[rng.IntN(5)] + clips := 0 + for range 1 + rng.IntN(4) { + p := randomPath(rng, 100, 80) + if rng.IntN(3) == 0 { + // A box, as most of what a page draws. + x, y := float32(rng.Float64()*60), float32(rng.Float64()*40) + w, h := float32(rng.Float64()*70), float32(rng.Float64()*50) + p = &Path{} + p.MoveTo(x, y) + p.LineTo(x+w, y) + p.LineTo(x+w, y+h) + p.LineTo(x, y+h) + p.Close() + } + st := &StrokeStyle{ + Width: []float64{0, 0.3, 0.75, 1, 2.83, 9}[rng.IntN(6)], + Cap: Cap(rng.IntN(3)), + Join: Join(rng.IntN(3)), + MiterLimit: []float64{1, 4, 20}[rng.IntN(3)], + } + if rng.IntN(4) == 0 { + st.Dash = [][]float64{{7, 5}, {1, 1}, {0.2, 0.3}, {12, 3, 2, 3}}[rng.IntN(4)] + st.DashPhase = rng.Float64() * 5 + } + rule := FillRule(rng.IntN(2)) + paint := randomPaint(rng) + switch k := rng.IntN(10); { + case k < 4: + ops = append(ops, func(c *Canvas) { c.Fill(p, m, rule, paint) }) + case k < 8: + ops = append(ops, func(c *Canvas) { c.Stroke(p, m, st, paint) }) + case k == 8: + clips++ + ops = append(ops, func(c *Canvas) { c.ClipPath(p, m, rule) }) + default: + clips++ + r := Rect{rng.Float64() * 40, rng.Float64() * 30, 40 + rng.Float64()*60, 30 + rng.Float64()*50} + ops = append(ops, func(c *Canvas) { c.ClipRect(r, m) }) + } + } + draw := func(dst *image.RGBA, region image.Rectangle) { + c := NewCanvas(dst) + c.Reset(dst, region) + for _, op := range ops { + op(c) + } + for range clips { + c.PopClip() + } + if err := c.Err(); err != nil { + t.Fatalf("iteration %d: %v", it, err) + } + } + want := image.NewRGBA(box) + copy(want.Pix, backdrop.Pix) + draw(want, box) + for _, tiles := range tilings(box) { + if it%10 != 0 && tiles[0].Dx() == 1 { + continue + } + got := image.NewRGBA(box) + copy(got.Pix, backdrop.Pix) + for _, tile := range tiles { + draw(got, tile) + } + if !bytes.Equal(got.Pix, want.Pix) { + n, at := 0, image.Point{} + for i := 0; i < len(got.Pix); i += 4 { + if !bytes.Equal(got.Pix[i:i+4], want.Pix[i:i+4]) { + if n == 0 { + at = image.Pt(box.Min.X+(i/4)%box.Dx(), box.Min.Y+(i/4)/box.Dx()) + } + n++ + } + } + t.Errorf("iteration %d, tiles of %v: %d pixels differ, the first at %v: %v, want %v", it, tiles[0].Size(), n, at, got.RGBAAt(at.X, at.Y), want.RGBAAt(at.X, at.Y)) + break + } + } + } +} + +// TestRunLikeCoverage holds a run of constant coverage against the same +// coverage given pixel by pixel: the rasterizer cuts a row into runs and +// coverage as the clip and the shape have it, so the two must blend +// alike. +func TestRunLikeCoverage(t *testing.T) { + rng := rand.New(rand.NewPCG(31, 32)) + const n = 41 + rect := image.Rect(0, 0, n, 1) + for _, tt := range []struct { + name string + new func(dst *image.RGBA) Blitter + }{ + {"opaque color", func(dst *image.RGBA) Blitter { return NewSolidBlitter(dst, color.RGBA{10, 128, 250, 255}) }}, + {"translucent color", func(dst *image.RGBA) Blitter { return NewSolidBlitter(dst, color.RGBA{10, 100, 90, 130}) }}, + {"opaque shader", func(dst *image.RGBA) Blitter { return NewShaderBlitter(dst, hashShader{opaque: true}) }}, + {"translucent shader", func(dst *image.RGBA) Blitter { return NewShaderBlitter(dst, hashShader{}) }}, + } { + for alpha := 0; alpha < 256; alpha++ { + run, cov := image.NewRGBA(rect), image.NewRGBA(rect) + for i := range run.Pix { + run.Pix[i] = uint8(rng.IntN(256)) + } + for i := 3; i < len(run.Pix); i += 4 { + run.Pix[i] = 255 // an opaque backdrop, as a page is + } + copy(cov.Pix, run.Pix) + for _, span := range [][2]int{{0, n}, {3, 9}, {7, 8}} { + tt.new(run).BlitRun(0, span[0], span[1], uint8(alpha)) + c := make([]uint8, span[1]-span[0]) + for i := range c { + c[i] = uint8(alpha) + } + tt.new(cov).BlitCoverage(0, span[0], c) + } + if !bytes.Equal(run.Pix, cov.Pix) { + t.Errorf("%s, coverage %d: a run blends unlike coverage", tt.name, alpha) + break + } + } + } +}