package pdf import "core:math" import "core:mem" import "core:testing" import "core:time" // --------------------------------------------------------------------------- // Span recorder // // Every test drives the rasteriser through this, so the ordering and bounds // contract -- increasing y, increasing x within a row, no overlapping runs, // nothing outside the clip -- is checked by every case rather than by one // dedicated test that the others could quietly violate. // --------------------------------------------------------------------------- @(private) Raster_Recorder :: struct { clip: Raster_Clip, w, h: int, grid: []u8, n_spans: int, total: int, ordered: bool, in_bounds: bool, disjoint: bool, nonempty: bool, nonzero: bool, last_y: int, last_end: int, } @(private) raster_rec_make :: proc(clip: Raster_Clip) -> Raster_Recorder { w := clip.x1 - clip.x0 h := clip.y1 - clip.y0 return Raster_Recorder{ clip = clip, w = w, h = h, grid = make([]u8, max(w * h, 1)), ordered = true, in_bounds = true, disjoint = true, nonempty = true, nonzero = true, last_y = min(int), last_end = min(int), } } @(private) raster_rec_destroy :: proc(rec: ^Raster_Recorder) { delete(rec.grid) rec.grid = nil } @(private) raster_rec_at :: proc(rec: ^Raster_Recorder, x, y: int) -> u8 { ix := x - rec.clip.x0 iy := y - rec.clip.y0 if ix < 0 || iy < 0 || ix >= rec.w || iy >= rec.h { return 0 } return rec.grid[iy * rec.w + ix] } @(private) raster_rec_span :: proc(user: rawptr, y: int, x0: int, coverage: []u8) { rec := (^Raster_Recorder)(user) rec.n_spans += 1 if len(coverage) == 0 { rec.nonempty = false return } if y < rec.last_y || (y == rec.last_y && x0 < rec.last_end) { rec.ordered = false } rec.last_y = y rec.last_end = x0 + len(coverage) for c, i in coverage { x := x0 + i if c == 0 { rec.nonzero = false } rec.total += int(c) ix := x - rec.clip.x0 iy := y - rec.clip.y0 if ix < 0 || iy < 0 || ix >= rec.w || iy >= rec.h { rec.in_bounds = false continue } if rec.grid[iy * rec.w + ix] != 0 { rec.disjoint = false } rec.grid[iy * rec.w + ix] = c } } @(private) raster_rec_check :: proc(t: ^testing.T, rec: ^Raster_Recorder, loc := #caller_location) { testing.expect(t, rec.ordered, "spans must arrive in increasing y then x", loc = loc) testing.expect(t, rec.in_bounds, "spans must lie inside the clip", loc = loc) testing.expect(t, rec.disjoint, "spans must not overlap", loc = loc) testing.expect(t, rec.nonempty, "spans must not be empty", loc = loc) testing.expect(t, rec.nonzero, "spans must hold no zero coverage", loc = loc) } // Raster_Counter is the recorder for cases whose clip is far too large to // hold a grid for. @(private) Raster_Counter :: struct { spans: int, total: int, } @(private) raster_count_span :: proc(user: rawptr, y: int, x0: int, coverage: []u8) { c := (^Raster_Counter)(user) c.spans += 1 for v in coverage { c.total += int(v) } } @(private) raster_test_add :: proc(fp: ^Flat_Path, closed: bool, pts: []Point) { start := len(fp.points) append(&fp.points, ..pts) append(&fp.subpaths, Flat_Subpath{start = start, count = len(pts), closed = closed}) } // raster_test_run fills one polygon and returns the recorder, already checked // for the structural invariants. @(private) raster_test_run :: proc(t: ^testing.T, pts: []Point, even_odd: bool, clip: Raster_Clip, loc := #caller_location) -> Raster_Recorder { fp := flat_path_make() defer flat_path_destroy(&fp) raster_test_add(&fp, true, pts) rec := raster_rec_make(clip) err := raster_fill(&fp, even_odd, clip, raster_rec_span, &rec) testing.expect_value(t, err, Error.None, loc = loc) raster_rec_check(t, &rec, loc = loc) return rec } // --------------------------------------------------------------------------- // Exactness // --------------------------------------------------------------------------- // A pixel-aligned square must come out with no fringe at all: every interior // pixel fully on, every pixel outside it untouched. Anti-aliasing that leaks // half a pixel of grey along a table rule is the single most visible way to // get this wrong. @(test) test_raster_square_is_exact :: proc(t: ^testing.T) { pts := [][2]f64{{2, 3}, {12, 3}, {12, 13}, {2, 13}} rec := raster_test_run(t, pts[:], false, Raster_Clip{0, 0, 16, 16}) defer raster_rec_destroy(&rec) bad := 0 for y in 0 ..< 16 { for x in 0 ..< 16 { want: u8 = (x >= 2 && x < 12 && y >= 3 && y < 13) ? 255 : 0 if raster_rec_at(&rec, x, y) != want { bad += 1 } } } testing.expect_value(t, bad, 0) // Ten full rows, one run each. testing.expect_value(t, rec.n_spans, 10) testing.expect_value(t, rec.total, 100 * 255) } // Half a pixel of coverage is half of 255. The tolerance is two levels, not // because the maths is approximate -- it is exact -- but so the assertion is // about the value and not about a rounding convention. @(test) test_raster_half_covered_pixel :: proc(t: ^testing.T) { pts := [][2]f64{{0, 0}, {0.5, 0}, {0.5, 1}, {0, 1}} rec := raster_test_run(t, pts[:], false, Raster_Clip{0, 0, 4, 4}) defer raster_rec_destroy(&rec) got := int(raster_rec_at(&rec, 0, 0)) testing.expectf(t, abs(got - 128) <= 2, "half-covered pixel gave %d, want 128", got) testing.expect_value(t, raster_rec_at(&rec, 1, 0), u8(0)) testing.expect_value(t, raster_rec_at(&rec, 0, 1), u8(0)) } // A rectangle on fractional boundaries has a closed-form coverage for every // pixel it touches: the product of the overlaps in x and in y. Matching it to // one level proves the coverage really is analytic in BOTH axes -- a // sub-scanline sampler would quantise the y factor and fail this. @(test) test_raster_fractional_rect_is_analytic :: proc(t: ^testing.T) { RX0 :: 1.25 RY0 :: 0.5 RX1 :: 4.75 RY1 :: 3.125 pts := [][2]f64{{RX0, RY0}, {RX1, RY0}, {RX1, RY1}, {RX0, RY1}} rec := raster_test_run(t, pts[:], false, Raster_Clip{0, 0, 8, 8}) defer raster_rec_destroy(&rec) worst := 0 for y in 0 ..< 8 { for x in 0 ..< 8 { ox := max(0.0, min(f64(x + 1), RX1) - max(f64(x), RX0)) oy := max(0.0, min(f64(y + 1), RY1) - max(f64(y), RY0)) want := int(ox * oy * 255 + 0.5) got := int(raster_rec_at(&rec, x, y)) worst = max(worst, abs(got - want)) } } testing.expectf(t, worst <= 1, "worst analytic coverage error was %d levels", worst) } // Total coverage is the polygon's area. Shoelace gives the answer to compare // against, and a fill that is a fraction of a pixel out anywhere -- a // half-open range applied to the wrong side, a dropped last scanline -- misses // it by far more than the quantisation slack. @(test) test_raster_triangle_area :: proc(t: ^testing.T) { pts := [][2]f64{{1.3, 1.7}, {9.1, 2.4}, {4.2, 8.6}} area := 0.5 * abs( pts[0].x * (pts[1].y - pts[2].y) + pts[1].x * (pts[2].y - pts[0].y) + pts[2].x * (pts[0].y - pts[1].y)) for even_odd in ([2]bool{false, true}) { rec := raster_test_run(t, pts[:], even_odd, Raster_Clip{0, 0, 12, 12}) defer raster_rec_destroy(&rec) got := f64(rec.total) / 255 testing.expectf(t, abs(got - area) < 0.15, "triangle covered %f pixels, analytic area is %f (even_odd=%v)", got, area, even_odd) } } // --------------------------------------------------------------------------- // Fill rules // --------------------------------------------------------------------------- // Two concentric squares, wound the same way and wound oppositely, under both // rules. Three of the four cases punch a hole; only same-winding nonzero fills // the middle. A rasteriser that ignores the fill rule, or that takes the // absolute value of the winding before folding it, agrees in all four. @(test) test_raster_fill_rules_concentric :: proc(t: ^testing.T) { outer := [][2]f64{{0, 0}, {10, 0}, {10, 10}, {0, 10}} same := [][2]f64{{3, 3}, {7, 3}, {7, 7}, {3, 7}} opp := [][2]f64{{3, 3}, {3, 7}, {7, 7}, {7, 3}} Case :: struct { inner: [][2]f64, even_odd: bool, middle: u8 } cases := []Case{ {same, false, 255}, // winding 2 -> filled {same, true, 0}, // winding 2 -> folded away {opp, false, 0}, // winding 0 -> hole {opp, true, 0}, // winding 0 -> hole } for c, i in cases { fp := flat_path_make() defer flat_path_destroy(&fp) raster_test_add(&fp, true, outer) raster_test_add(&fp, true, c.inner) rec := raster_rec_make(Raster_Clip{0, 0, 12, 12}) defer raster_rec_destroy(&rec) testing.expect_value(t, raster_fill(&fp, c.even_odd, rec.clip, raster_rec_span, &rec), Error.None) raster_rec_check(t, &rec) testing.expectf(t, raster_rec_at(&rec, 5, 5) == c.middle, "case %d: middle pixel was %d, want %d", i, raster_rec_at(&rec, 5, 5), c.middle) // The ring between the squares is wound once either way, so it is // solid in every case; that is what makes the middle the only thing // the fill rule changes. testing.expectf(t, raster_rec_at(&rec, 1, 1) == 255, "case %d: ring pixel was %d, want 255", i, raster_rec_at(&rec, 1, 1)) } } // The same square traced twice in the same direction winds to 2. Nonzero must // clamp that to solid, not wrap it; even-odd must fold it to nothing. @(test) test_raster_double_wound_square :: proc(t: ^testing.T) { sq := [][2]f64{{1, 1}, {6, 1}, {6, 6}, {1, 6}} for even_odd in ([2]bool{false, true}) { fp := flat_path_make() defer flat_path_destroy(&fp) raster_test_add(&fp, true, sq) raster_test_add(&fp, true, sq) rec := raster_rec_make(Raster_Clip{0, 0, 8, 8}) defer raster_rec_destroy(&rec) testing.expect_value(t, raster_fill(&fp, even_odd, rec.clip, raster_rec_span, &rec), Error.None) raster_rec_check(t, &rec) want := 25 * 255 if !even_odd else 0 testing.expectf(t, rec.total == want, "double-wound square totalled %d, want %d (even_odd=%v)", rec.total, want, even_odd) } } // A self-intersecting figure of eight. The two lobes wind +1 and -1, so both // rules fill both of them and the total is the sum of the two triangles -- // 25 + 25. A sign error in the winding, or a clamp applied before the fold, // loses one lobe entirely. @(test) test_raster_figure_of_eight :: proc(t: ^testing.T) { pts := [][2]f64{{0, 0}, {10, 0}, {0, 10}, {10, 10}} for even_odd in ([2]bool{false, true}) { rec := raster_test_run(t, pts[:], even_odd, Raster_Clip{0, 0, 12, 12}) defer raster_rec_destroy(&rec) got := f64(rec.total) / 255 testing.expectf(t, abs(got - 50) < 0.5, "figure of eight covered %f pixels, want 50 (even_odd=%v)", got, even_odd) // The crossing is at (5,5); both lobes must be present. testing.expectf(t, raster_rec_at(&rec, 5, 1) == 255 && raster_rec_at(&rec, 5, 8) == 255, "both lobes must be solid (even_odd=%v)", even_odd) } } // --------------------------------------------------------------------------- // Degenerate input // --------------------------------------------------------------------------- @(test) test_raster_degenerate_paths :: proc(t: ^testing.T) { clip := Raster_Clip{0, 0, 8, 8} Case :: struct { name: string, pts: [][2]f64 } cases := []Case{ {"empty", {}}, {"single point", {{2, 2}}}, {"two equal points", {{2, 2}, {2, 2}}}, {"horizontal", {{1, 4}, {7, 4}}}, {"three collinear", {{1, 4}, {4, 4}, {7, 4}}}, {"vertical", {{4, 1}, {4, 7}}}, {"zero-area square", {{3, 3}, {3, 3}, {3, 3}, {3, 3}}}, } for c in cases { fp := flat_path_make() defer flat_path_destroy(&fp) if len(c.pts) > 0 { raster_test_add(&fp, false, c.pts) } rec := raster_rec_make(clip) defer raster_rec_destroy(&rec) err := raster_fill(&fp, false, clip, raster_rec_span, &rec) testing.expectf(t, err == .None, "%s returned %v", c.name, err) testing.expectf(t, rec.n_spans == 0, "%s emitted %d spans", c.name, rec.n_spans) } } // A path outside the clip costs nothing and paints nothing, whichever side it // is on. @(test) test_raster_outside_clip :: proc(t: ^testing.T) { clip := Raster_Clip{10, 10, 20, 20} offsets := [][2]f64{{-40, 0}, {40, 0}, {0, -40}, {0, 40}, {40, 40}} for off in offsets { pts := [][2]f64{ {12 + off.x, 12 + off.y}, {18 + off.x, 12 + off.y}, {18 + off.x, 18 + off.y}, {12 + off.x, 18 + off.y}, } rec := raster_test_run(t, pts[:], false, clip) defer raster_rec_destroy(&rec) testing.expectf(t, rec.n_spans == 0, "offset %v emitted %d spans", off, rec.n_spans) } } // A shape larger than the clip is bounded by it exactly: the clip fills solid // and nothing is reported outside. This is the case that decides whether the // left-hand winding of a clipped-away edge survives -- drop it and the whole // region comes out empty. @(test) test_raster_clips_oversized_shape :: proc(t: ^testing.T) { clip := Raster_Clip{10, 20, 40, 45} pts := [][2]f64{{-500, -500}, {500, -500}, {500, 500}, {-500, 500}} rec := raster_test_run(t, pts[:], false, clip) defer raster_rec_destroy(&rec) testing.expect_value(t, rec.n_spans, 25) testing.expect_value(t, rec.total, 30 * 25 * 255) } @(test) test_raster_rejects_nil_span :: proc(t: ^testing.T) { fp := flat_path_make() defer flat_path_destroy(&fp) raster_test_add(&fp, true, [][2]f64{{0, 0}, {4, 0}, {4, 4}}) testing.expect_value(t, raster_fill(&fp, false, Raster_Clip{0, 0, 8, 8}, nil, nil), Error.Invalid_Argument) } @(test) test_raster_empty_clip :: proc(t: ^testing.T) { fp := flat_path_make() defer flat_path_destroy(&fp) raster_test_add(&fp, true, [][2]f64{{0, 0}, {4, 0}, {4, 4}}) for clip in ([]Raster_Clip{{0, 0, 0, 8}, {0, 0, 8, 0}, {5, 0, 2, 8}, {0, 5, 8, 2}}) { rec := raster_rec_make(Raster_Clip{0, 0, 8, 8}) defer raster_rec_destroy(&rec) testing.expect_value(t, raster_fill(&fp, false, clip, raster_rec_span, &rec), Error.None) testing.expect_value(t, rec.n_spans, 0) } } // An unclosed subpath fills as though it were closed: that is what `f` means, // and a triangle drawn with two lines and no `h` is ordinary content. @(test) test_raster_closes_subpaths_implicitly :: proc(t: ^testing.T) { pts := [][2]f64{{1, 1}, {9, 1}, {9, 9}, {1, 9}} fp_open := flat_path_make() defer flat_path_destroy(&fp_open) raster_test_add(&fp_open, false, pts) fp_closed := flat_path_make() defer flat_path_destroy(&fp_closed) raster_test_add(&fp_closed, true, pts) a := raster_rec_make(Raster_Clip{0, 0, 12, 12}) defer raster_rec_destroy(&a) b := raster_rec_make(Raster_Clip{0, 0, 12, 12}) defer raster_rec_destroy(&b) testing.expect_value(t, raster_fill(&fp_open, false, a.clip, raster_rec_span, &a), Error.None) testing.expect_value(t, raster_fill(&fp_closed, false, b.clip, raster_rec_span, &b), Error.None) testing.expect_value(t, a.total, 64 * 255) testing.expect_value(t, a.total, b.total) testing.expect_value(t, a.n_spans, b.n_spans) } // --------------------------------------------------------------------------- // Robustness // --------------------------------------------------------------------------- // Coordinates like 1e9 are ordinary in content streams -- a "clip to // everything" rectangle usually is one. The cost must follow the clip, not the // path, so this is timed as well as checked: a rasteriser that iterates the // path's scanlines instead of the clip's runs for hours here. @(test) test_raster_huge_coordinates_terminate :: proc(t: ^testing.T) { clip := Raster_Clip{0, 0, 100, 100} B :: 1e9 Case :: struct { name: string, pts: [][2]f64, want_total: int } cases := []Case{ {"covers clip", {{-B, -B}, {B, -B}, {B, B}, {-B, B}}, 100 * 100 * 255}, {"tall spike", {{-B, -B}, {B, -B}, {0, B}}, 100 * 100 * 255}, {"far away", {{B, B}, {B + 10, B}, {B + 10, B + 10}}, 0}, {"one huge vertex", {{10, 10}, {B, 20}, {10, 30}}, -1}, } for c in cases { start := time.now() rec := raster_test_run(t, c.pts, false, clip) defer raster_rec_destroy(&rec) ms := time.duration_milliseconds(time.since(start)) testing.expectf(t, ms < 250, "%s took %f ms", c.name, ms) if c.want_total >= 0 { testing.expectf(t, rec.total == c.want_total, "%s totalled %d, want %d", c.name, rec.total, c.want_total) } } } // The mirror image of the case above: a small path inside an enormous clip. // The cost has to follow the path there, so nothing may be allocated or // iterated per row of the clip -- a million-row clip would otherwise mean a // million-entry scanline table before a single pixel was touched. @(test) test_raster_small_path_in_huge_clip :: proc(t: ^testing.T) { fp := flat_path_make() defer flat_path_destroy(&fp) raster_test_add(&fp, true, [][2]f64{{500000, 400000}, {500010, 400000}, {500010, 400010}, {500000, 400010}}) count := Raster_Counter{} start := time.now() err := raster_fill(&fp, false, Raster_Clip{0, 0, 1000000, 1000000}, raster_count_span, &count) ms := time.duration_milliseconds(time.since(start)) testing.expect_value(t, err, Error.None) testing.expectf(t, ms < 250, "small path in a huge clip took %f ms", ms) testing.expect_value(t, count.spans, 10) testing.expect_value(t, count.total, 100 * 255) } // raster_test_measure fills through a tracking allocator and reports what the // fill cost in allocations and in bytes. @(private) raster_test_measure :: proc(t: ^testing.T, fp: ^Flat_Path, clip: Raster_Clip, loc := #caller_location) -> (calls: int, bytes: int) { track: mem.Tracking_Allocator mem.tracking_allocator_init(&track, context.allocator) defer mem.tracking_allocator_destroy(&track) count := Raster_Counter{} err := raster_fill(fp, false, clip, raster_count_span, &count, mem.tracking_allocator(&track)) testing.expect_value(t, err, Error.None, loc = loc) testing.expectf(t, len(track.allocation_map) == 0, "raster_fill leaked %d allocations", len(track.allocation_map), loc = loc) return int(track.total_allocation_count), int(track.total_memory_allocated) } // Two things at once, both of them contract rather than taste. // // Nothing is allocated per scanline: a path 400 rows tall must cost the same // number of allocations as one 4 rows tall. // // And the working set follows the path rather than the clip: the same small // path against a clip a thousand times taller must not allocate a byte more, // which is what stops a 300 dpi page from building a scanline table for rows // no edge will ever reach. @(test) test_raster_allocation_is_bounded :: proc(t: ^testing.T) { short := flat_path_make() defer flat_path_destroy(&short) raster_test_add(&short, true, [][2]f64{{1, 1}, {5, 1}, {5, 5}, {1, 5}}) tall := flat_path_make() defer flat_path_destroy(&tall) raster_test_add(&tall, true, [][2]f64{{1, 1}, {5, 1}, {5, 401}, {1, 401}}) short_calls, short_bytes := raster_test_measure(t, &short, Raster_Clip{0, 0, 8, 8}) tall_calls, _ := raster_test_measure(t, &tall, Raster_Clip{0, 0, 8, 512}) huge_calls, huge_bytes := raster_test_measure(t, &short, Raster_Clip{0, 0, 8, 100000}) testing.expectf(t, short_calls <= 8, "a fill took %d allocations", short_calls) testing.expectf(t, tall_calls == short_calls, "a path 100x taller took %d allocations against %d", tall_calls, short_calls) testing.expectf(t, huge_calls == short_calls && huge_bytes == short_bytes, "a clip 12000x taller cost %d allocations / %d bytes against %d / %d", huge_calls, huge_bytes, short_calls, short_bytes) } // path.odin filters non-finite coordinates on the way in, but a Flat_Path can // also come from a transform that overflowed, so the rasteriser guards them // itself rather than trusting its caller. // // The guard clamps rather than discards, and that distinction is the whole // assertion here: dropping a segment because one end is infinite breaks the // polygon open and loses everything the remaining edges were enclosing, while // pulling that end back to the edge of the world keeps it closed and fills // what the geometry meant. An infinite rectangle fills to the wall. @(test) test_raster_non_finite_coordinates :: proc(t: ^testing.T) { nan := math.nan_f64() inf := math.INF_F64 clip := Raster_Clip{0, 0, 8, 8} open_right := raster_test_run(t, [][2]f64{{2, 2}, {inf, 2}, {inf, 6}, {2, 6}}, false, clip) defer raster_rec_destroy(&open_right) testing.expect_value(t, open_right.total, 6 * 4 * 255) open_left := raster_test_run(t, [][2]f64{{-inf, 2}, {6, 2}, {6, 6}, {-inf, 6}}, false, clip) defer raster_rec_destroy(&open_left) testing.expect_value(t, open_left.total, 6 * 4 * 255) // The rest have no defensible answer; the contract for them is only that // the rasteriser returns, stays inside the clip, and keeps its spans in // order -- all of which raster_test_run checks. paths := [][][2]f64{ {{nan, 0}, {8, 0}, {8, 8}}, {{0, nan}, {8, 0}, {8, 8}}, {{inf, inf}, {-inf, inf}, {0, -inf}}, {{nan, nan}, {nan, nan}, {nan, nan}}, {{1, 1}, {7, nan}, {7, 7}, {nan, 7}}, } for pts in paths { rec := raster_test_run(t, pts, false, clip) defer raster_rec_destroy(&rec) testing.expect(t, rec.total >= 0) } } // raster_test_tangle fills `n` points of a dense self-crossing scribble into // `fp`, all of it inside a 64-pixel box. @(private) raster_test_tangle :: proc(fp: ^Flat_Path, n: int, ry: f64) { start := len(fp.points) for i in 0 ..< n { a := f64(i) * 0.31 append(&fp.points, Point{32 + 30 * math.cos(a), 32 + ry * math.sin(a * 1.7)}) } append(&fp.subpaths, Flat_Subpath{start = start, count = n, closed = true}) } // A dense self-crossing scribble: thousands of edges live on every scanline at // once and the winding runs deep, which is where an active-edge list that // retires an edge one row early or late shows itself. @(test) test_raster_many_segments :: proc(t: ^testing.T) { fp := flat_path_make() defer flat_path_destroy(&fp) raster_test_tangle(&fp, 20000, 30) rec := raster_rec_make(Raster_Clip{0, 0, 64, 64}) defer raster_rec_destroy(&rec) testing.expect_value(t, raster_fill(&fp, true, rec.clip, raster_rec_span, &rec), Error.None) raster_rec_check(t, &rec) testing.expect(t, rec.total > 0) } // Past MAX_RASTER_EDGES the edge table stops growing and the rest of the path // is dropped. The shape that comes out is wrong -- that is the point of a cap // -- but it still comes out, in order, inside the clip, without the table // having grown to whatever a hostile content stream asked for. Uncapped, the // same path allocates several hundred megabytes. @(test) test_raster_edge_cap :: proc(t: ^testing.T) { fp := flat_path_make() defer flat_path_destroy(&fp) // Kept to a couple of scanlines so the cap is what the test measures // rather than the time it takes to sweep a million tall edges. raster_test_tangle(&fp, MAX_RASTER_EDGES + (MAX_RASTER_EDGES / 2), 1) track: mem.Tracking_Allocator mem.tracking_allocator_init(&track, context.allocator) defer mem.tracking_allocator_destroy(&track) rec := raster_rec_make(Raster_Clip{0, 0, 64, 64}) defer raster_rec_destroy(&rec) err := raster_fill(&fp, true, rec.clip, raster_rec_span, &rec, mem.tracking_allocator(&track)) testing.expect_value(t, err, Error.None) raster_rec_check(t, &rec) testing.expect_value(t, len(track.allocation_map), 0) peak := int(track.peak_memory_allocated) testing.expectf(t, peak < 128 * 1024 * 1024, "the edge table peaked at %d bytes", peak) } // --------------------------------------------------------------------------- // Clip windowing // --------------------------------------------------------------------------- // Coverage is a property of the path, not of the clip: narrowing the clip must // reproduce the wider fill exactly over the window they share. This is what // lets a device split a page into bands. @(test) test_raster_clip_window_matches :: proc(t: ^testing.T) { pts := [][2]f64{{1.4, 2.2}, {17.6, 4.1}, {14.2, 18.9}, {3.3, 15.5}} full := raster_test_run(t, pts[:], false, Raster_Clip{0, 0, 24, 24}) defer raster_rec_destroy(&full) win := Raster_Clip{6, 7, 13, 16} part := raster_test_run(t, pts[:], false, win) defer raster_rec_destroy(&part) bad := 0 for y in win.y0 ..< win.y1 { for x in win.x0 ..< win.x1 { if raster_rec_at(&full, x, y) != raster_rec_at(&part, x, y) { bad += 1 } } } testing.expect_value(t, bad, 0) } // --------------------------------------------------------------------------- // Fuzz // --------------------------------------------------------------------------- @(private) raster_test_rand :: proc(state: ^u64) -> u64 { // xorshift64*, so the corpus is the same on every machine and a failure // can be reproduced from the seed alone. x := state^ x ~= x >> 12 x ~= x << 25 x ~= x >> 27 state^ = x return x * 0x2545_f491_4f6c_dd1d } @(private) raster_test_randf :: proc(state: ^u64, lo, hi: f64) -> f64 { u := f64(raster_test_rand(state) >> 11) / f64(u64(1) << 53) return lo + (hi - lo) * u } // raster_test_inside is a deliberately naive reference: cast a ray and count // signed crossings. Slow, obviously correct, and shares no code with the // rasteriser, which is the whole point of comparing against it. @(private) raster_test_inside :: proc(pts: [][2]f64, px, py: f64, even_odd: bool) -> bool { w := 0 for i in 0 ..< len(pts) { a := pts[i] b := pts[(i + 1) % len(pts)] if (a.y <= py) == (b.y <= py) { continue } tx := a.x + (b.x - a.x) * (py - a.y) / (b.y - a.y) if tx <= px { continue } w += b.y > a.y ? 1 : -1 } return even_odd ? (w & 1) != 0 : w != 0 } // Random polygons, checked two ways: the structural contract on every span, // and -- for every pixel the reference says is unambiguously inside or // unambiguously outside at 12x12 samples -- that the coverage agrees. A // half-pixel offset, an inverted fill rule or a lost winding all show up as an // outside pixel that came back painted. @(test) test_raster_fuzz_polygons :: proc(t: ^testing.T) { SAMPLES :: 12 clip := Raster_Clip{0, 0, 24, 24} seed: u64 = 0x9e37_79b9_7f4a_7c15 for iter in 0 ..< 40 { n := 3 + int(raster_test_rand(&seed) % 8) pts := make([][2]f64, n) defer delete(pts) for &p in pts { p = Point{raster_test_randf(&seed, -8, 32), raster_test_randf(&seed, -8, 32)} } even_odd := (raster_test_rand(&seed) & 1) != 0 rec := raster_test_run(t, pts, even_odd, clip) defer raster_rec_destroy(&rec) testing.expectf(t, rec.total <= 24 * 24 * 255, "iter %d: total coverage %d exceeds the clip", iter, rec.total) for y in 0 ..< 24 { for x in 0 ..< 24 { n_in := 0 for sy in 0 ..< SAMPLES { for sx in 0 ..< SAMPLES { px := f64(x) + (f64(sx) + 0.5) / SAMPLES py := f64(y) + (f64(sy) + 0.5) / SAMPLES if raster_test_inside(pts, px, py, even_odd) { n_in += 1 } } } got := int(raster_rec_at(&rec, x, y)) if n_in == SAMPLES * SAMPLES { testing.expectf(t, got >= 210, "iter %d: pixel %d,%d is solidly inside but covered %d", iter, x, y, got) } else if n_in == 0 { testing.expectf(t, got <= 45, "iter %d: pixel %d,%d is solidly outside but covered %d", iter, x, y, got) } } } } } // The same corpus, with the clip offset away from the origin and the polygons // straddling every wall, to exercise the guard cell and the left-hand winding // substitution rather than the interior. @(test) test_raster_fuzz_straddling_clip :: proc(t: ^testing.T) { clip := Raster_Clip{7, 11, 23, 29} seed: u64 = 0xdead_beef_1234_5678 for _ in 0 ..< 120 { n := 3 + int(raster_test_rand(&seed) % 6) pts := make([][2]f64, n) defer delete(pts) for &p in pts { p = Point{raster_test_randf(&seed, -200, 200), raster_test_randf(&seed, -200, 200)} } rec := raster_test_run(t, pts, (raster_test_rand(&seed) & 1) != 0, clip) defer raster_rec_destroy(&rec) } }