package pdf import "core:mem" import "core:testing" // --------------------------------------------------------------------------- // Helpers // // Comparisons are structural: a recorded command holds its OWN copy of the // path and the glyph run, so the pointers deliberately differ and only the // contents may be compared. // --------------------------------------------------------------------------- @(private = "file") dl_path_eq :: proc(a, b: ^Path) -> bool { if a == nil || b == nil { return a == b } if len(a.verbs) != len(b.verbs) || len(a.points) != len(b.points) { return false } for v, i in a.verbs { if v != b.verbs[i] { return false } } for p, i in a.points { if p != b.points[i] { return false } } return true } @(private = "file") dl_text_eq :: proc(a, b: ^Text) -> bool { if a == nil || b == nil { return a == b } if a.font != b.font || a.wmode != b.wmode || a.render_mode != b.render_mode { return false } if len(a.items) != len(b.items) { return false } for it, i in a.items { if it != b.items[i] { return false } } return true } // dl_paint_eq compares two paints, taking the soft mask by value: the list // copies the mask, so the two pointers must NOT be equal and the two masks // must be. @(private = "file") dl_paint_eq :: proc(a, b: Paint) -> bool { x, y := a, b x.soft_mask, y.soft_mask = nil, nil if x != y { return false } if (a.soft_mask == nil) != (b.soft_mask == nil) { return false } if a.soft_mask != nil && a.soft_mask^ != b.soft_mask^ { return false } return true } // dl_mask_eq compares two recorded masks the same way, for the hooks that // carry a `^Soft_Mask` of their own rather than one inside a Paint: // `begin_group`, whose mask is the one the group's RESULT composites through, // and `end_mask`, whose mask names the plane just drawn. @(private = "file") dl_mask_eq :: proc(a, b: ^Soft_Mask) -> bool { if (a == nil) != (b == nil) { return false } return a == nil || a^ == b^ } // dl_find_group returns the first recorded `begin_group`, by pointer so that // the mask it carries can be compared against the other list's copy. @(private = "file") dl_find_group :: proc(dl: ^Display_List) -> ^Display_Begin_Group { for &cmd in dl.cmds { if g, ok := &cmd.(Display_Begin_Group); ok { return g } } return nil } @(private = "file") dl_cmd_eq :: proc(a, b: Display_Cmd) -> bool { switch av in a { case Display_Fill_Path: bv := b.(Display_Fill_Path) or_return return dl_path_eq(av.path, bv.path) && av.even_odd == bv.even_odd && av.ctm == bv.ctm && dl_paint_eq(av.paint, bv.paint) case Display_Stroke_Path: bv := b.(Display_Stroke_Path) or_return return dl_path_eq(av.path, bv.path) && av.stroke == bv.stroke && av.ctm == bv.ctm && dl_paint_eq(av.paint, bv.paint) case Display_Clip_Path: bv := b.(Display_Clip_Path) or_return return dl_path_eq(av.path, bv.path) && av.even_odd == bv.even_odd && av.ctm == bv.ctm case Display_Clip_Stroke_Path: bv := b.(Display_Clip_Stroke_Path) or_return return dl_path_eq(av.path, bv.path) && av.stroke == bv.stroke && av.ctm == bv.ctm case Display_Clip_Text: bv := b.(Display_Clip_Text) or_return return dl_text_eq(av.text, bv.text) && av.ctm == bv.ctm case Display_Clip_Image_Mask: bv := b.(Display_Clip_Image_Mask) or_return return av.img == bv.img && av.ctm == bv.ctm case Display_Pop_Clip: _ = b.(Display_Pop_Clip) or_return return true case Display_Fill_Text: bv := b.(Display_Fill_Text) or_return return dl_text_eq(av.text, bv.text) && av.ctm == bv.ctm && dl_paint_eq(av.paint, bv.paint) case Display_Stroke_Text: bv := b.(Display_Stroke_Text) or_return return dl_text_eq(av.text, bv.text) && av.stroke == bv.stroke && av.ctm == bv.ctm && dl_paint_eq(av.paint, bv.paint) case Display_Ignore_Text: bv := b.(Display_Ignore_Text) or_return return dl_text_eq(av.text, bv.text) && av.ctm == bv.ctm case Display_Fill_Image: bv := b.(Display_Fill_Image) or_return return av.img == bv.img && av.ctm == bv.ctm && dl_paint_eq(av.paint, bv.paint) case Display_Fill_Image_Mask: bv := b.(Display_Fill_Image_Mask) or_return return av.img == bv.img && av.ctm == bv.ctm && dl_paint_eq(av.paint, bv.paint) case Display_Fill_Shade: bv := b.(Display_Fill_Shade) or_return return av.shade == bv.shade && av.ctm == bv.ctm && dl_paint_eq(av.paint, bv.paint) case Display_Begin_Mask: bv := b.(Display_Begin_Mask) or_return return av.area == bv.area && av.luminosity == bv.luminosity && dl_paint_eq(av.backdrop, bv.backdrop) case Display_End_Mask: bv := b.(Display_End_Mask) or_return return av.transfer == bv.transfer && dl_mask_eq(av.mask, bv.mask) case Display_Begin_Group: bv := b.(Display_Begin_Group) or_return return av.area == bv.area && av.isolated == bv.isolated && av.knockout == bv.knockout && av.blend == bv.blend && av.alpha == bv.alpha && dl_mask_eq(av.soft_mask, bv.soft_mask) case Display_End_Group: _ = b.(Display_End_Group) or_return return true case Display_Begin_Tile: bv := b.(Display_Begin_Tile) or_return return av == bv case Display_End_Tile: _ = b.(Display_End_Tile) or_return return true case Display_Begin_Layer: bv := b.(Display_Begin_Layer) or_return return av.name == bv.name case Display_End_Layer: _ = b.(Display_End_Layer) or_return return true } return false } // dl_kinds spells the recorded sequence as a list of tags, which is how the // ordering tests read. @(private = "file") dl_kind :: proc(cmd: Display_Cmd) -> string { switch _ in cmd { case Display_Fill_Path: return "fill" case Display_Stroke_Path: return "stroke" case Display_Clip_Path: return "clip" case Display_Clip_Stroke_Path: return "clip_stroke" case Display_Clip_Text: return "clip_text" case Display_Clip_Image_Mask: return "clip_image_mask" case Display_Pop_Clip: return "pop" case Display_Fill_Text: return "text" case Display_Stroke_Text: return "stroke_text" case Display_Ignore_Text: return "ignore_text" case Display_Fill_Image: return "image" case Display_Fill_Image_Mask: return "image_mask" case Display_Fill_Shade: return "shade" case Display_Begin_Mask: return "begin_mask" case Display_End_Mask: return "end_mask" case Display_Begin_Group: return "begin_group" case Display_End_Group: return "end_group" case Display_Begin_Tile: return "begin_tile" case Display_End_Tile: return "end_tile" case Display_Begin_Layer: return "begin_layer" case Display_End_Layer: return "end_layer" } return "?" } @(private = "file") dl_expect_kinds :: proc(t: ^testing.T, dl: ^Display_List, want: []string, loc := #caller_location) { if !testing.expect_value(t, len(dl.cmds), len(want), loc = loc) { for cmd, i in dl.cmds { testing.expectf(t, false, " recorded [%d] %s", i, dl_kind(cmd), loc = loc) } return } for cmd, i in dl.cmds { got := dl_kind(cmd) testing.expectf(t, got == want[i], "command %d is %s, want %s", i, got, want[i], loc = loc) } } @(private = "file") dl_test_paint :: proc(cs: ^Colorspace) -> Paint { return Paint{ cs = cs, color = color_make(0.25, 0.5, 0.75), rgb = {0.25, 0.5, 0.75}, alpha = 0.5, blend = .Multiply, } } @(private = "file") dl_rect_near :: proc(t: ^testing.T, got, want: Rect, tol: f64, loc := #caller_location) { ok := abs(got.x0 - want.x0) <= tol && abs(got.y0 - want.y0) <= tol && abs(got.x1 - want.x1) <= tol && abs(got.y1 - want.y1) <= tol testing.expectf(t, ok, "bounds %v, want %v (tolerance %v)", got, want, tol, loc = loc) } // dl_contains reports whether `outer` covers `inner` -- what a surface sized // from the bounds has to do for every command in the list. @(private = "file") dl_contains :: proc(outer, inner: Rect) -> bool { return outer.x0 <= inner.x0 + 1e-9 && outer.y0 <= inner.y0 + 1e-9 && outer.x1 >= inner.x1 - 1e-9 && outer.y1 >= inner.y1 - 1e-9 } // --------------------------------------------------------------------------- // A probe device: a foreign backend, to prove a replay reaches one with the // values it was recorded with rather than only agreeing with another list. // --------------------------------------------------------------------------- @(private = "file") Dl_Event :: struct { kind: string, path: ^Path, text: ^Text, even_odd: bool, ctm: Matrix, paint: Paint, stroke: Stroke_State, } @(private = "file") Dl_Probe :: struct { dev: Device, events: [dynamic]Dl_Event, } @(private = "file") dl_probe_init :: proc(p: ^Dl_Probe) { p.events = make([dynamic]Dl_Event, 0, 8) p.dev = Device{ user = p, fill_path = proc(dev: ^Device, path: ^Path, even_odd: bool, ctm: Matrix, paint: Paint) { p := cast(^Dl_Probe)dev.user append(&p.events, Dl_Event{ kind = "fill", path = path, even_odd = even_odd, ctm = ctm, paint = paint, }) }, stroke_path = proc(dev: ^Device, path: ^Path, stroke: ^Stroke_State, ctm: Matrix, paint: Paint) { p := cast(^Dl_Probe)dev.user append(&p.events, Dl_Event{ kind = "stroke", path = path, ctm = ctm, paint = paint, stroke = stroke != nil ? stroke^ : {}, }) }, fill_text = proc(dev: ^Device, text: ^Text, ctm: Matrix, paint: Paint) { p := cast(^Dl_Probe)dev.user append(&p.events, Dl_Event{kind = "text", text = text, ctm = ctm, paint = paint}) }, pop_clip = proc(dev: ^Device) { p := cast(^Dl_Probe)dev.user append(&p.events, Dl_Event{kind = "pop"}) }, begin_group = proc(dev: ^Device, area: Rect, isolated, knockout: bool, blend: Blend_Mode, alpha: f64, soft_mask: ^Soft_Mask) { p := cast(^Dl_Probe)dev.user append(&p.events, Dl_Event{kind = "begin_group"}) }, end_group = proc(dev: ^Device) { p := cast(^Dl_Probe)dev.user append(&p.events, Dl_Event{kind = "end_group"}) }, fill_image = proc(dev: ^Device, img: ^Image, ctm: Matrix, paint: Paint) { p := cast(^Dl_Probe)dev.user append(&p.events, Dl_Event{kind = "image", ctm = ctm, paint = paint}) }, } } @(private = "file") dl_probe_destroy :: proc(p: ^Dl_Probe) { delete(p.events) } // --------------------------------------------------------------------------- // Recording // --------------------------------------------------------------------------- @(test) test_display_list_records_a_fill_verbatim :: proc(t: ^testing.T) { ld, err := list_device_make() testing.expect_value(t, err, Error.None) defer list_device_destroy(ld) dev := list_device_as_device(ld) p := path_make() defer path_destroy(&p) path_rect(&p, 10, 20, 30, 40) cs := Colorspace{kind = .Device_RGB, n = 3} paint := dl_test_paint(&cs) ctm := Matrix{2, 0, 0, 2, 5, 7} dev_fill_path(dev, &p, true, ctm, paint) dl := list_device_result(ld) testing.expect_value(t, len(dl.cmds), 1) cmd, ok := dl.cmds[0].(Display_Fill_Path) testing.expect(t, ok, "a fill must record a Display_Fill_Path") if !ok { return } testing.expect(t, cmd.path != &p, "the list must not alias the caller's path") testing.expect(t, dl_path_eq(cmd.path, &p), "the recorded path must equal the one given") testing.expect_value(t, cmd.even_odd, true) testing.expect_value(t, cmd.ctm, ctm) testing.expect(t, dl_paint_eq(cmd.paint, paint), "the paint must be recorded unchanged") testing.expect_value(t, cmd.paint.cs, &cs) testing.expect_value(t, cmd.paint.alpha, 0.5) testing.expect_value(t, cmd.paint.blend, Blend_Mode.Multiply) testing.expect_value(t, cmd.paint.color.v[1], 0.5) testing.expect(t, !dl.overflow, "one fill is not an overflow") testing.expect(t, !dl.closed, "the list is not closed until close is called") } // THE headline: the interpreter reuses its path for the next operator, so a // list that kept the pointer records whatever the path became later. Mutating // the original after recording must not be visible in the list. @(test) test_display_list_copies_the_path :: proc(t: ^testing.T) { ld, _ := list_device_make() defer list_device_destroy(ld) dev := list_device_as_device(ld) p := path_make() defer path_destroy(&p) path_rect(&p, 0, 0, 10, 10) dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{}) want := path_clone(&p) defer path_destroy(&want) // Everything the interpreter does to a path between two operators. path_transform(&p, Matrix{3, 0, 0, 3, 100, 100}) path_line_to(&p, 999, 999) dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{}) path_clear(&p) path_move_to(&p, -5, -5) dl := list_device_result(ld) testing.expect_value(t, len(dl.cmds), 2) first, ok := dl.cmds[0].(Display_Fill_Path) testing.expect(t, ok, "") if !ok { return } testing.expect(t, dl_path_eq(first.path, &want), "the first recording changed when the caller's path was reused") testing.expect_value(t, len(first.path.points), 4) testing.expect_value(t, first.path.points[2], Point{10, 10}) // And the second recording is its own copy of the mutated path, not a // second view of the same one. second, ok2 := dl.cmds[1].(Display_Fill_Path) testing.expect(t, ok2, "") if !ok2 { return } testing.expect(t, second.path != first.path, "each command owns its own copy") testing.expect_value(t, len(second.path.points), 5) testing.expect_value(t, second.path.points[0], Point{100, 100}) testing.expect_value(t, second.path.points[4], Point{999, 999}) // The list outlives the path it was recorded from: destroy it entirely and // the recording is still readable. path_destroy(&p) testing.expect_value(t, len(first.path.points), 4) testing.expect(t, !rect_is_empty(display_list_bounds(dl)), "bounds after the source path is gone") } @(test) test_display_list_copies_the_text_run :: proc(t: ^testing.T) { ld, _ := list_device_make() defer list_device_destroy(ld) dev := list_device_as_device(ld) font := Font{mtx = Matrix{0.001, 0, 0, 0.001, 0, 0}} txt := text_make() defer text_destroy(&txt) txt.font = &font // A vertical CMap: the writing mode is part of the run and has to survive // the copy, and leaving it at its default would hide a copy that dropped it. txt.wmode = 1 txt.render_mode = .Fill_Stroke append(&txt.items, Text_Item{gid = 36, code = 'A', cid = 36, trm = Matrix{0.01, 0, 0, 0.01, 100, 200}, advance = 0.6, unicode = 'A'}) append(&txt.items, Text_Item{gid = 37, code = 'B', cid = 37, trm = Matrix{0.01, 0, 0, 0.01, 106, 200}, advance = 0.6, unicode = 'B'}) append(&txt.items, Text_Item{gid = 3, code = 32, cid = 3, trm = Matrix{0.01, 0, 0, 0.01, 112, 200}, advance = 0.25, unicode = ' ', is_space = true}) dev_fill_text(dev, &txt, MATRIX_IDENTITY, Paint{alpha = 1}) // The interpreter reuses one Text for every show operator on the page. clear(&txt.items) append(&txt.items, Text_Item{gid = 99, unicode = 'Z'}) txt.render_mode = .Invisible dl := list_device_result(ld) testing.expect_value(t, len(dl.cmds), 1) cmd, ok := dl.cmds[0].(Display_Fill_Text) testing.expect(t, ok, "") if !ok { return } testing.expect(t, cmd.text != &txt, "the list must not alias the interpreter's Text") testing.expect_value(t, cmd.text.font, &font) testing.expect_value(t, cmd.text.render_mode, Text_Render_Mode.Fill_Stroke) testing.expect_value(t, cmd.text.wmode, 1) testing.expect_value(t, len(cmd.text.items), 3) testing.expect_value(t, cmd.text.items[0].gid, 36) testing.expect_value(t, cmd.text.items[1].unicode, 'B') testing.expect_value(t, cmd.text.items[1].trm, Matrix{0.01, 0, 0, 0.01, 106, 200}) testing.expect_value(t, cmd.text.items[2].is_space, true) testing.expect_value(t, cmd.text.items[2].advance, 0.25) } @(test) test_display_list_copies_the_soft_mask :: proc(t: ^testing.T) { ld, _ := list_device_make() defer list_device_destroy(ld) dev := list_device_as_device(ld) p := path_make() defer path_destroy(&p) path_rect(&p, 0, 0, 1, 1) // Interpreter scratch: one Soft_Mask value, reused for a second mask later // in the page, which is why the recorder compares contents and not the // address. mask := Soft_Mask{luminosity = true, backdrop = color_make(0.5), ctm = Matrix{1, 0, 0, 1, 3, 4}} paint := Paint{alpha = 1, soft_mask = &mask} dev_fill_path(dev, &p, false, MATRIX_IDENTITY, paint) dev_fill_path(dev, &p, false, MATRIX_IDENTITY, paint) mask.luminosity = false mask.backdrop = color_make(0.25) dev_fill_path(dev, &p, false, MATRIX_IDENTITY, paint) dl := list_device_result(ld) testing.expect_value(t, len(dl.cmds), 3) a := dl.cmds[0].(Display_Fill_Path).paint.soft_mask b := dl.cmds[1].(Display_Fill_Path).paint.soft_mask c := dl.cmds[2].(Display_Fill_Path).paint.soft_mask testing.expect(t, a != nil && b != nil && c != nil, "") if a == nil || b == nil || c == nil { return } testing.expect(t, a != &mask, "the mask must be copied out of interpreter scratch") testing.expect(t, a == b, "two fills under one unchanged mask share one copy") testing.expect(t, c != a, "a changed mask at the same address is a different mask") testing.expect_value(t, a.luminosity, true) testing.expect_value(t, a.backdrop.v[0], 0.5) testing.expect_value(t, a.ctm, Matrix{1, 0, 0, 1, 3, 4}) testing.expect_value(t, c.luminosity, false) testing.expect_value(t, c.backdrop.v[0], 0.25) } @(test) test_display_list_copies_the_layer_name :: proc(t: ^testing.T) { ld, _ := list_device_make() defer list_device_destroy(ld) dev := list_device_as_device(ld) scratch := make([]byte, 5) defer delete(scratch) copy(scratch, "Layer") dev_begin_layer(dev, string(scratch)) dev_end_layer(dev) // The interpreter's buffer is reused for the next /OC name. copy(scratch, "XXXXX") dl := list_device_result(ld) cmd, ok := dl.cmds[0].(Display_Begin_Layer) testing.expect(t, ok, "") if !ok { return } testing.expect_value(t, cmd.name, "Layer") testing.expect(t, raw_data(cmd.name) != raw_data(scratch), "the name must be copied") // An /OC with no usable name still nests, and copying "" must not upset // the teardown that gives every copied name back. dev_begin_layer(dev, "") dev_end_layer(dev) empty, ok2 := dl.cmds[2].(Display_Begin_Layer) testing.expect(t, ok2, "") if ok2 { testing.expect_value(t, empty.name, "") } dl_expect_kinds(t, dl, {"begin_layer", "end_layer", "begin_layer", "end_layer"}) } // --------------------------------------------------------------------------- // Ordering and nesting // --------------------------------------------------------------------------- @(test) test_display_list_clip_pushes_and_pops_in_order :: proc(t: ^testing.T) { ld, _ := list_device_make() defer list_device_destroy(ld) dev := list_device_as_device(ld) p := path_make() defer path_destroy(&p) path_rect(&p, 0, 0, 10, 10) stroke := STROKE_STATE_DEFAULT dev_clip_path(dev, &p, false, MATRIX_IDENTITY) dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{}) dev_clip_stroke_path(dev, &p, &stroke, MATRIX_IDENTITY) dev_fill_path(dev, &p, true, MATRIX_IDENTITY, Paint{}) dev_pop_clip(dev) dev_pop_clip(dev) dl := list_device_result(ld) dl_expect_kinds(t, dl, {"clip", "fill", "clip_stroke", "fill", "pop", "pop"}) // The even-odd flag belongs to the fill it was given with, not to the // clip that preceded it. testing.expect_value(t, dl.cmds[1].(Display_Fill_Path).even_odd, false) testing.expect_value(t, dl.cmds[3].(Display_Fill_Path).even_odd, true) testing.expect_value(t, dl.cmds[0].(Display_Clip_Path).even_odd, false) } @(test) test_display_list_nested_groups :: proc(t: ^testing.T) { ld, _ := list_device_make() defer list_device_destroy(ld) dev := list_device_as_device(ld) p := path_make() defer path_destroy(&p) path_rect(&p, 0, 0, 10, 10) dev_begin_group(dev, Rect{0, 0, 100, 100}, true, false, .Multiply, 0.5, nil) dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{}) dev_begin_layer(dev, "inner") dev_begin_group(dev, Rect{10, 10, 20, 20}, false, true, .Screen, 0.25, nil) dev_begin_mask(dev, Rect{0, 0, 5, 5}, true, Paint{}) dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{}) dev_end_mask(dev, nil, nil) dev_begin_tile(dev, Rect{0, 0, 8, 8}, Rect{0, 0, 8, 8}, 8, 8, MATRIX_IDENTITY) dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{}) dev_end_tile(dev) dev_end_group(dev) dev_end_layer(dev) dev_end_group(dev) dl := list_device_result(ld) dl_expect_kinds(t, dl, { "begin_group", "fill", "begin_layer", "begin_group", "begin_mask", "fill", "end_mask", "begin_tile", "fill", "end_tile", "end_group", "end_layer", "end_group", }) outer := dl.cmds[0].(Display_Begin_Group) inner := dl.cmds[3].(Display_Begin_Group) testing.expect_value(t, outer.area, Rect{0, 0, 100, 100}) testing.expect_value(t, outer.isolated, true) testing.expect_value(t, outer.knockout, false) testing.expect_value(t, outer.blend, Blend_Mode.Multiply) testing.expect_value(t, outer.alpha, 0.5) testing.expect_value(t, inner.area, Rect{10, 10, 20, 20}) testing.expect_value(t, inner.isolated, false) testing.expect_value(t, inner.knockout, true) testing.expect_value(t, inner.blend, Blend_Mode.Screen) testing.expect_value(t, inner.alpha, 0.25) tile := dl.cmds[7].(Display_Begin_Tile) testing.expect_value(t, tile.xstep, 8) testing.expect_value(t, tile.ystep, 8) testing.expect_value(t, dl.cmds[4].(Display_Begin_Mask).luminosity, true) } // A recorder that wants the tile's content once, into the tile, must say 0 -- // a positive count would have the interpreter replay the cell for every // placement and the list would hold a thousand copies of one cell. @(test) test_display_list_begin_tile_asks_for_one_pass :: proc(t: ^testing.T) { ld, _ := list_device_make() defer list_device_destroy(ld) dev := list_device_as_device(ld) n := dev_begin_tile(dev, Rect{0, 0, 4, 4}, Rect{0, 0, 4, 4}, 4, 4, MATRIX_IDENTITY) testing.expect_value(t, n, 0) dev_end_tile(dev) testing.expect_value(t, len(list_device_result(ld).cmds), 2) } // --------------------------------------------------------------------------- // Replay // --------------------------------------------------------------------------- @(test) test_display_list_replay_is_faithful :: proc(t: ^testing.T) { ld, _ := list_device_make() defer list_device_destroy(ld) dev := list_device_as_device(ld) p := path_make() defer path_destroy(&p) path_rect(&p, 3, 4, 20, 30) path_move_to(&p, 1, 2) path_curve_to(&p, 3, 4, 5, 6, 7, 8) font := Font{mtx = Matrix{0.001, 0, 0, 0.001, 0, 0}} txt := text_make() defer text_destroy(&txt) txt.font = &font append(&txt.items, Text_Item{gid = 7, trm = Matrix{0.012, 0, 0, 0.012, 50, 60}, advance = 0.5, unicode = 'x'}) cs := Colorspace{kind = .Device_RGB, n = 3} paint := dl_test_paint(&cs) mask := Soft_Mask{luminosity = true, ctm = MATRIX_IDENTITY} paint.soft_mask = &mask img := Image{width = 4, height = 4, bpc = 8} shade := Shading{kind = .Axial, has_bbox = true, bbox = Rect{0, 0, 1, 1}} stroke := Stroke_State{line_width = 2.5, line_cap = .Round, line_join = .Bevel, miter_limit = 4} stroke.dash[0] = 3 stroke.dash[1] = 1 stroke.dash_count = 2 dev_clip_path(dev, &p, true, Matrix{1, 0, 0, 1, 9, 9}) dev_fill_path(dev, &p, true, MATRIX_IDENTITY, paint) dev_stroke_path(dev, &p, &stroke, Matrix{2, 0, 0, 2, 0, 0}, paint) dev_clip_text(dev, &txt, MATRIX_IDENTITY) dev_fill_text(dev, &txt, MATRIX_IDENTITY, paint) dev_stroke_text(dev, &txt, &stroke, MATRIX_IDENTITY, paint) dev_ignore_text(dev, &txt, MATRIX_IDENTITY) dev_fill_image(dev, &img, Matrix{10, 0, 0, 10, 1, 1}, paint) dev_fill_image_mask(dev, &img, MATRIX_IDENTITY, paint) dev_clip_image_mask(dev, &img, MATRIX_IDENTITY) dev_fill_shade(dev, &shade, MATRIX_IDENTITY, paint) // The group's own mask is a different mask from the paints', so a replay // that confused the two would be visible: 11.6.6 hands `begin_group` the // mask the group's RESULT composites through, and it has to survive being // recorded and played back like every other operand. gmask := Soft_Mask{luminosity = true, ctm = Matrix{1, 0, 0, 1, 5, 6}} dev_begin_group(dev, Rect{0, 0, 9, 9}, true, true, .Darken, 0.75, &gmask) dev_begin_layer(dev, "L1") dev_fill_path(dev, &p, false, MATRIX_IDENTITY, paint) dev_end_layer(dev) dev_end_group(dev) dev_begin_mask(dev, Rect{1, 1, 2, 2}, false, paint) dev_end_mask(dev, &gmask, nil) dev_pop_clip(dev) dev_pop_clip(dev) dev_pop_clip(dev) first := list_device_result(ld) n_first := len(first.cmds) testing.expect(t, n_first > 0, "") // Replay into a second recorder: the second device must see exactly what // the first was given, command for command. ld2, _ := list_device_make() defer list_device_destroy(ld2) testing.expect_value(t, display_list_run(first, list_device_as_device(ld2)), Error.None) second := list_device_result(ld2) testing.expect_value(t, len(second.cmds), n_first) if len(second.cmds) != n_first { return } for cmd, i in first.cmds { testing.expectf(t, dl_cmd_eq(cmd, second.cmds[i]), "command %d (%s) did not survive the replay", i, dl_kind(cmd)) } // The second list is its own recording, not a view onto the first. a := first.cmds[1].(Display_Fill_Path) b := second.cmds[1].(Display_Fill_Path) testing.expect(t, a.path != b.path, "a replay into a recorder copies again") testing.expect(t, a.paint.soft_mask != b.paint.soft_mask, "and copies the mask again") ga := dl_find_group(first) gb := dl_find_group(second) testing.expect(t, ga != nil && gb != nil, "both lists recorded the group") if ga == nil || gb == nil { return } testing.expect(t, ga.soft_mask != nil, "begin_group recorded its soft mask") testing.expect(t, ga.soft_mask != &gmask, "the group's mask must be copied out of interpreter scratch") testing.expect(t, ga.soft_mask != gb.soft_mask, "and copied again on replay") if ga.soft_mask != nil && gb.soft_mask != nil { testing.expect_value(t, gb.soft_mask^, gmask) } // And a replay into a foreign device gets the same values. probe: Dl_Probe dl_probe_init(&probe) defer dl_probe_destroy(&probe) testing.expect_value(t, display_list_run(first, &probe.dev), Error.None) // The probe implements seven hooks; the commands for the ones it leaves // nil (clip_text, stroke_text, ignore_text, image masks, shade, mask, // layer) pass through the dev_* wrappers and are simply not seen. testing.expect_value(t, len(probe.events), 10) if len(probe.events) != 10 { return } testing.expect_value(t, probe.events[0].kind, "fill") testing.expect_value(t, probe.events[0].even_odd, true) testing.expect(t, dl_path_eq(probe.events[0].path, a.path), "the probe saw the recorded path") testing.expect(t, dl_paint_eq(probe.events[0].paint, paint), "the probe saw the recorded paint") testing.expect_value(t, probe.events[1].kind, "stroke") testing.expect_value(t, probe.events[1].stroke, stroke) testing.expect_value(t, probe.events[1].ctm, Matrix{2, 0, 0, 2, 0, 0}) testing.expect_value(t, probe.events[2].kind, "text") testing.expect(t, dl_text_eq(probe.events[2].text, first.cmds[4].(Display_Fill_Text).text), "") testing.expect_value(t, probe.events[3].kind, "image") testing.expect_value(t, probe.events[3].ctm, Matrix{10, 0, 0, 10, 1, 1}) testing.expect_value(t, probe.events[4].kind, "begin_group") testing.expect_value(t, probe.events[5].kind, "fill") testing.expect_value(t, probe.events[6].kind, "end_group") testing.expect_value(t, probe.events[7].kind, "pop") testing.expect_value(t, probe.events[8].kind, "pop") testing.expect_value(t, probe.events[9].kind, "pop") } // A replay honours the target's hints, exactly as the interpreter does: a text // device has no more use for a decoded image the second time round. @(test) test_display_list_replay_honours_hints :: proc(t: ^testing.T) { ld, _ := list_device_make() defer list_device_destroy(ld) dev := list_device_as_device(ld) p := path_make() defer path_destroy(&p) path_rect(&p, 0, 0, 1, 1) img := Image{width = 2, height = 2} dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{}) dev_fill_image(dev, &img, MATRIX_IDENTITY, Paint{}) probe: Dl_Probe dl_probe_init(&probe) defer dl_probe_destroy(&probe) probe.dev.hints = {.Ignore_Image} testing.expect_value(t, display_list_run(list_device_result(ld), &probe.dev), Error.None) testing.expect_value(t, len(probe.events), 1) testing.expect_value(t, probe.events[0].kind, "fill") clear(&probe.events) probe.dev.hints = {} _ = display_list_run(list_device_result(ld), &probe.dev) testing.expect_value(t, len(probe.events), 2) } @(test) test_display_list_replay_tolerates_nil :: proc(t: ^testing.T) { ld, _ := list_device_make() defer list_device_destroy(ld) p := path_make() defer path_destroy(&p) path_rect(&p, 0, 0, 1, 1) dev_fill_path(list_device_as_device(ld), &p, false, MATRIX_IDENTITY, Paint{}) testing.expect_value(t, display_list_run(nil, nil), Error.Invalid_Argument) testing.expect_value(t, display_list_run(list_device_result(ld), nil), Error.None) // A device with every hook nil is the interpreter's own tolerance case. empty: Device testing.expect_value(t, display_list_run(list_device_result(ld), &empty), Error.None) // Destroying the list before the device that owns it is a misuse, and one // that must not take the allocator with it: the list is zeroed, so the // device has to remember where it came from. ld2, _ := list_device_make() display_list_destroy(list_device_result(ld2)) display_list_destroy(list_device_result(ld2)) list_device_destroy(ld2) testing.expect_value(t, list_device_as_device(nil), (^Device)(nil)) testing.expect_value(t, list_device_result(nil), (^Display_List)(nil)) list_device_destroy(nil) display_list_destroy(nil) } // --------------------------------------------------------------------------- // Bounds // --------------------------------------------------------------------------- @(test) test_display_list_bounds_covers_every_command :: proc(t: ^testing.T) { testing.expect_value(t, display_list_bounds(nil), RECT_EMPTY) rect := path_make() defer path_destroy(&rect) path_rect(&rect, 10, 20, 20, 20) // 10,20 .. 30,40 line := path_make() defer path_destroy(&line) path_move_to(&line, 200, 100) path_line_to(&line, 260, 100) font := Font{mtx = Matrix{0.001, 0, 0, 0.001, 0, 0}} txt := text_make() defer text_destroy(&txt) txt.font = &font // Glyph space is 1000 units to the em, and the trm scales by 0.01, so the // em is 10 device units and the 0.5-em advance is 5 of them. append(&txt.items, Text_Item{gid = 1, trm = Matrix{0.01, 0, 0, 0.01, 400, 500}, advance = 0.5}) img := Image{width = 2, height = 2} shade := Shading{kind = .Axial, has_bbox = true, bbox = Rect{0, 0, 2, 2}} stroke := Stroke_State{line_width = 4, line_join = .Bevel, miter_limit = 10} // Each command alone, so that a bounds case that stopped contributing // cannot hide inside the union of the others. { ld, _ := list_device_make() defer list_device_destroy(ld) dev_fill_path(list_device_as_device(ld), &rect, false, MATRIX_IDENTITY, Paint{}) dl_rect_near(t, display_list_bounds(list_device_result(ld)), Rect{10, 20, 30, 40}, 1e-9) } { ld, _ := list_device_make() defer list_device_destroy(ld) dev_fill_path(list_device_as_device(ld), &rect, false, Matrix{2, 0, 0, 2, 5, 5}, Paint{}) dl_rect_near(t, display_list_bounds(list_device_result(ld)), Rect{25, 45, 65, 85}, 1e-9) } { // A clip is unioned in too: a consumer still has to address it. ld, _ := list_device_make() defer list_device_destroy(ld) dev_clip_path(list_device_as_device(ld), &rect, false, MATRIX_IDENTITY) dl_rect_near(t, display_list_bounds(list_device_result(ld)), Rect{10, 20, 30, 40}, 1e-9) } { ld, _ := list_device_make() defer list_device_destroy(ld) dev_stroke_path(list_device_as_device(ld), &line, &stroke, MATRIX_IDENTITY, Paint{}) b := display_list_bounds(list_device_result(ld)) testing.expect(t, dl_contains(b, Rect{200, 98, 260, 102}), "the stroked line's ink") testing.expect(t, dl_contains(Rect{170, 70, 290, 130}, b), "and not much more than that") } { ld, _ := list_device_make() defer list_device_destroy(ld) dev_fill_text(list_device_as_device(ld), &txt, MATRIX_IDENTITY, Paint{}) b := display_list_bounds(list_device_result(ld)) testing.expect(t, dl_contains(b, Rect{400, 500, 405, 500}), "the glyph origin and its advance") testing.expect(t, dl_contains(Rect{399, 485, 420, 515}, b), "roughly one em around the origin") } { // Invisible text is the exception: it paints nothing, so it cannot // enlarge the surface. ld, _ := list_device_make() defer list_device_destroy(ld) dev_ignore_text(list_device_as_device(ld), &txt, MATRIX_IDENTITY) testing.expect_value(t, len(list_device_result(ld).cmds), 1) testing.expect_value(t, display_list_bounds(list_device_result(ld)), RECT_EMPTY) } { ld, _ := list_device_make() defer list_device_destroy(ld) dev_fill_image(list_device_as_device(ld), &img, Matrix{50, 0, 0, 50, 600, 700}, Paint{}) dl_rect_near(t, display_list_bounds(list_device_result(ld)), Rect{600, 700, 650, 750}, 1e-9) } { ld, _ := list_device_make() defer list_device_destroy(ld) dev_fill_image_mask(list_device_as_device(ld), &img, Matrix{4, 0, 0, 4, 0, 0}, Paint{}) dl_rect_near(t, display_list_bounds(list_device_result(ld)), Rect{0, 0, 4, 4}, 1e-9) } { ld, _ := list_device_make() defer list_device_destroy(ld) dev_clip_image_mask(list_device_as_device(ld), &img, Matrix{0, 3, -3, 0, 10, 10}) dl_rect_near(t, display_list_bounds(list_device_result(ld)), Rect{7, 10, 10, 13}, 1e-9) } { ld, _ := list_device_make() defer list_device_destroy(ld) dev_fill_shade(list_device_as_device(ld), &shade, Matrix{10, 0, 0, 10, 800, 900}, Paint{}) dl_rect_near(t, display_list_bounds(list_device_result(ld)), Rect{800, 900, 820, 920}, 1e-9) } { ld, _ := list_device_make() defer list_device_destroy(ld) dev_begin_group(list_device_as_device(ld), Rect{1, 2, 3, 4}, false, false, .Normal, 1, nil) dev_end_group(list_device_as_device(ld)) dl_rect_near(t, display_list_bounds(list_device_result(ld)), Rect{1, 2, 3, 4}, 1e-9) } { ld, _ := list_device_make() defer list_device_destroy(ld) dev_begin_mask(list_device_as_device(ld), Rect{5, 6, 7, 8}, true, Paint{}) dev_end_mask(list_device_as_device(ld), nil, nil) dl_rect_near(t, display_list_bounds(list_device_result(ld)), Rect{5, 6, 7, 8}, 1e-9) } { ld, _ := list_device_make() defer list_device_destroy(ld) _ = dev_begin_tile(list_device_as_device(ld), Rect{2, 2, 6, 6}, Rect{2, 2, 6, 6}, 4, 4, MATRIX_IDENTITY) dev_end_tile(list_device_as_device(ld)) dl_rect_near(t, display_list_bounds(list_device_result(ld)), Rect{2, 2, 6, 6}, 1e-9) } // And all of them together: the union, from the fill's top-left corner to // the shading's bottom-right. ld, _ := list_device_make() defer list_device_destroy(ld) dev := list_device_as_device(ld) testing.expect_value(t, display_list_bounds(list_device_result(ld)), RECT_EMPTY) dev_fill_path(dev, &rect, false, MATRIX_IDENTITY, Paint{}) dev_stroke_path(dev, &line, &stroke, MATRIX_IDENTITY, Paint{}) dev_fill_text(dev, &txt, MATRIX_IDENTITY, Paint{}) dev_fill_image(dev, &img, Matrix{50, 0, 0, 50, 600, 700}, Paint{}) dev_fill_shade(dev, &shade, Matrix{10, 0, 0, 10, 800, 900}, Paint{}) dl := list_device_result(ld) b := display_list_bounds(dl) testing.expect(t, !dl.unbounded, "every command here has a bound") testing.expect(t, dl_contains(b, Rect{10, 20, 30, 40}), "the fill") testing.expect(t, dl_contains(b, Rect{200, 98, 260, 102}), "the stroke") testing.expect(t, dl_contains(b, Rect{600, 700, 650, 750}), "the image") testing.expect(t, dl_contains(b, Rect{800, 900, 820, 920}), "the shading") dl_rect_near(t, b, Rect{10, 20, 820, 920}, 1) } @(test) test_display_list_bounds_flags_the_unbounded :: proc(t: ^testing.T) { ld, _ := list_device_make() defer list_device_destroy(ld) dev := list_device_as_device(ld) p := path_make() defer path_destroy(&p) path_rect(&p, 0, 0, 10, 10) dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{}) dl := list_device_result(ld) testing.expect(t, !dl.unbounded, "") // A shading with no /BBox paints the whole clip: there is no box for it. shade := Shading{kind = .Axial} dev_fill_shade(dev, &shade, MATRIX_IDENTITY, Paint{}) testing.expect(t, dl.unbounded, "an unbounded shading must be flagged") dl_rect_near(t, display_list_bounds(dl), Rect{0, 0, 10, 10}, 1e-9) // So does an infinite group area, which is what an XObject with no /BBox // reaches the device as. ld2, _ := list_device_make() defer list_device_destroy(ld2) dev_begin_group(list_device_as_device(ld2), RECT_INFINITE, false, false, .Normal, 1, nil) dev_end_group(list_device_as_device(ld2)) dl2 := list_device_result(ld2) testing.expect(t, dl2.unbounded, "an infinite area must be flagged") testing.expect_value(t, display_list_bounds(dl2), RECT_EMPTY) } // --------------------------------------------------------------------------- // Robustness // --------------------------------------------------------------------------- // Past its cap the recorder must stop growing without unbalancing: a clip // whose push was refused must not have its pop recorded, or every replay pops // a clip the target never pushed. @(test) test_display_list_overflow_stays_balanced :: proc(t: ^testing.T) { ld, _ := list_device_make() defer list_device_destroy(ld) dev := list_device_as_device(ld) dl := list_device_result(ld) dl.max_cmds = 2 p := path_make() defer path_destroy(&p) path_rect(&p, 0, 0, 10, 10) dev_clip_path(dev, &p, false, MATRIX_IDENTITY) // recorded dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{}) // recorded, now full dev_clip_path(dev, &p, false, MATRIX_IDENTITY) // refused dev_begin_group(dev, RECT_EMPTY, false, false, .Normal, 1, nil) // refused dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{}) // refused dev_end_group(dev) // must not be recorded dev_pop_clip(dev) // matches the refused clip dev_pop_clip(dev) // matches the recorded clip testing.expect(t, dl.overflow, "the cap must be reported") dl_expect_kinds(t, dl, {"clip", "fill", "pop"}) depth := 0 for cmd in dl.cmds { #partial switch _ in cmd { case Display_Clip_Path: depth += 1 case Display_Pop_Clip: depth -= 1 } testing.expect(t, depth >= 0, "a replay must never pop more than it pushed") } testing.expect_value(t, depth, 0) } @(test) test_display_list_byte_cap :: proc(t: ^testing.T) { ld, _ := list_device_make() defer list_device_destroy(ld) dev := list_device_as_device(ld) dl := list_device_result(ld) dl.max_bytes = 1 p := path_make() defer path_destroy(&p) path_rect(&p, 0, 0, 10, 10) dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{}) testing.expect_value(t, len(dl.cmds), 1) testing.expect(t, dl.bytes > 1, "the copy must be counted") dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{}) testing.expect_value(t, len(dl.cmds), 1) testing.expect(t, dl.overflow, "") } // close is the last thing the interpreter does, including on the error path, // so it must leave a list that replays into a balanced device even if the page // died inside three groups and a clip. @(test) test_display_list_close_unwinds_what_was_left_open :: proc(t: ^testing.T) { ld, _ := list_device_make() defer list_device_destroy(ld) dev := list_device_as_device(ld) p := path_make() defer path_destroy(&p) path_rect(&p, 0, 0, 10, 10) dev_clip_path(dev, &p, false, MATRIX_IDENTITY) dev_begin_group(dev, RECT_EMPTY, false, false, .Normal, 1, nil) dev_begin_layer(dev, "oc") dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{}) dev_close(dev) dl := list_device_result(ld) testing.expect(t, dl.closed, "") dl_expect_kinds(t, dl, {"clip", "begin_group", "begin_layer", "fill", "end_layer", "end_group", "pop"}) // Replaying it leaves a counting device back at depth zero. depth := 0 for cmd in dl.cmds { #partial switch _ in cmd { case Display_Clip_Path: depth += 1 case Display_Begin_Group: depth += 1 case Display_Begin_Layer: depth += 1 case Display_Pop_Clip: depth -= 1 case Display_End_Group: depth -= 1 case Display_End_Layer: depth -= 1 } } testing.expect_value(t, depth, 0) } // A content stream with more Q than q reaches the device as a stray pop. The // recorder must swallow it rather than record a pop with no push. @(test) test_display_list_stray_pops_are_dropped :: proc(t: ^testing.T) { ld, _ := list_device_make() defer list_device_destroy(ld) dev := list_device_as_device(ld) dev_pop_clip(dev) dev_pop_clip(dev) dev_end_group(dev) dev_end_mask(dev, nil, nil) dev_end_tile(dev) dev_end_layer(dev) testing.expect_value(t, len(list_device_result(ld).cmds), 0) p := path_make() defer path_destroy(&p) path_rect(&p, 0, 0, 1, 1) dev_clip_path(dev, &p, false, MATRIX_IDENTITY) dev_pop_clip(dev) dev_pop_clip(dev) dl_expect_kinds(t, list_device_result(ld), {"clip", "pop"}) } // Nothing the interpreter can hand a hook may crash it, including the nil an // image that failed to load leaves behind. @(test) test_display_list_tolerates_nil_arguments :: proc(t: ^testing.T) { ld, _ := list_device_make() defer list_device_destroy(ld) dev := list_device_as_device(ld) dev_fill_path(dev, nil, false, MATRIX_IDENTITY, Paint{}) dev_stroke_path(dev, nil, nil, MATRIX_IDENTITY, Paint{}) dev_clip_text(dev, nil, MATRIX_IDENTITY) dev_clip_image_mask(dev, nil, MATRIX_IDENTITY) dev_fill_text(dev, nil, MATRIX_IDENTITY, Paint{}) dev_fill_image(dev, nil, MATRIX_IDENTITY, Paint{}) dev_fill_image_mask(dev, nil, MATRIX_IDENTITY, Paint{}) dev_fill_shade(dev, nil, MATRIX_IDENTITY, Paint{}) dev_ignore_text(dev, nil, MATRIX_IDENTITY) testing.expect_value(t, len(list_device_result(ld).cmds), 0) // A clip that could not be recorded still has to swallow its pop. dev_pop_clip(dev) testing.expect_value(t, len(list_device_result(ld).cmds), 0) // A stroke with a nil Stroke_State records the default, not zeros: a zero // line width means a hairline, but a zero miter limit means nothing. p := path_make() defer path_destroy(&p) path_rect(&p, 0, 0, 1, 1) dev_stroke_path(dev, &p, nil, MATRIX_IDENTITY, Paint{}) cmd, ok := list_device_result(ld).cmds[0].(Display_Stroke_Path) testing.expect(t, ok, "") if ok { testing.expect_value(t, cmd.stroke, STROKE_STATE_DEFAULT) } } // A page can nest deeper than the recorder tracks. Past the cap it stops // recording, and the pops still have to line up. @(test) test_display_list_deep_nesting_is_capped :: proc(t: ^testing.T) { ld, _ := list_device_make() defer list_device_destroy(ld) dev := list_device_as_device(ld) n := MAX_DISPLAY_SCOPES + 10 for _ in 0 ..< n { dev_begin_group(dev, RECT_EMPTY, false, false, .Normal, 1, nil) } for _ in 0 ..< n { dev_end_group(dev) } dev_close(dev) dl := list_device_result(ld) testing.expect(t, dl.overflow, "the depth cap must be reported") begins, ends := 0, 0 for cmd in dl.cmds { #partial switch _ in cmd { case Display_Begin_Group: begins += 1 case Display_End_Group: ends += 1 } } testing.expect_value(t, begins, MAX_DISPLAY_SCOPES) testing.expect_value(t, ends, MAX_DISPLAY_SCOPES) testing.expect_value(t, len(dl.cmds), 2 * MAX_DISPLAY_SCOPES) } // Non-finite geometry must never reach a consumer through the list. @(test) test_display_list_keeps_nan_out :: proc(t: ^testing.T) { ld, _ := list_device_make() defer list_device_destroy(ld) dev := list_device_as_device(ld) inf := max(f64) * 10 dev_begin_group(dev, Rect{0, 0, inf, inf}, false, false, .Normal, inf, nil) dev_begin_tile(dev, RECT_EMPTY, RECT_EMPTY, inf, inf, MATRIX_IDENTITY) dl := list_device_result(ld) g := dl.cmds[0].(Display_Begin_Group) testing.expect(t, is_finite(g.alpha), "a non-finite alpha must be clamped") testing.expect_value(t, g.alpha, 1) testing.expect(t, dl.unbounded, "an area that is not finite has no bound") testing.expect_value(t, g.area, RECT_INFINITE) testing.expect(t, rect_is_finite(g.area) || g.area == RECT_INFINITE, "a NaN corner must never reach a consumer") tile := dl.cmds[1].(Display_Begin_Tile) testing.expect(t, is_finite(tile.xstep) && is_finite(tile.ystep), "steps must be finite") // path.odin filters NaN on construction, so the bounds of whatever does // get recorded are finite too. testing.expect(t, rect_is_finite(display_list_bounds(dl)), "bounds must be finite") } // Replaying a list into the device that is recording it is something a caller // can do by accident. It has to terminate: the walk is over a snapshot of the // command count, not over an array growing under it. @(test) test_display_list_self_replay_terminates :: proc(t: ^testing.T) { ld, _ := list_device_make() defer list_device_destroy(ld) dev := list_device_as_device(ld) p := path_make() defer path_destroy(&p) path_rect(&p, 0, 0, 4, 4) dev_fill_path(dev, &p, false, MATRIX_IDENTITY, Paint{}) dev_clip_path(dev, &p, false, MATRIX_IDENTITY) dev_pop_clip(dev) dl := list_device_result(ld) testing.expect_value(t, len(dl.cmds), 3) testing.expect_value(t, display_list_run(dl, dev), Error.None) testing.expect_value(t, len(dl.cmds), 6) dl_expect_kinds(t, dl, {"fill", "clip", "pop", "fill", "clip", "pop"}) } // Everything the list copied has to come back. The runner's own leak check // reports a leak as a warning and still calls the suite successful, so the // teardown gets an allocator it can be held to: one recording of every command // that owns memory, destroyed, and nothing left behind. @(test) test_display_list_destroy_gives_everything_back :: proc(t: ^testing.T) { track: mem.Tracking_Allocator mem.tracking_allocator_init(&track, context.allocator) defer mem.tracking_allocator_destroy(&track) alloc := mem.tracking_allocator(&track) p := path_make() defer path_destroy(&p) path_rect(&p, 0, 0, 10, 10) font := Font{mtx = Matrix{0.001, 0, 0, 0.001, 0, 0}} txt := text_make() defer text_destroy(&txt) txt.font = &font append(&txt.items, Text_Item{gid = 1, trm = MATRIX_IDENTITY, advance = 0.5}) img := Image{width = 2, height = 2} shade := Shading{kind = .Axial, has_bbox = true, bbox = Rect{0, 0, 1, 1}} stroke := STROKE_STATE_DEFAULT mask := Soft_Mask{luminosity = true} paint := Paint{alpha = 1, soft_mask = &mask} { ld, err := list_device_make(alloc) testing.expect_value(t, err, Error.None) dev := list_device_as_device(ld) dev_clip_path(dev, &p, false, MATRIX_IDENTITY) dev_clip_stroke_path(dev, &p, &stroke, MATRIX_IDENTITY) dev_clip_text(dev, &txt, MATRIX_IDENTITY) dev_clip_image_mask(dev, &img, MATRIX_IDENTITY) dev_fill_path(dev, &p, false, MATRIX_IDENTITY, paint) dev_stroke_path(dev, &p, &stroke, MATRIX_IDENTITY, paint) dev_fill_text(dev, &txt, MATRIX_IDENTITY, paint) dev_stroke_text(dev, &txt, &stroke, MATRIX_IDENTITY, paint) dev_ignore_text(dev, &txt, MATRIX_IDENTITY) dev_fill_image(dev, &img, MATRIX_IDENTITY, paint) dev_fill_image_mask(dev, &img, MATRIX_IDENTITY, paint) dev_fill_shade(dev, &shade, MATRIX_IDENTITY, paint) dev_begin_group(dev, RECT_EMPTY, false, false, .Normal, 1, nil) dev_begin_layer(dev, "a layer name long enough to be its own allocation") mask.luminosity = false dev_fill_path(dev, &p, false, MATRIX_IDENTITY, paint) dev_end_layer(dev) dev_end_group(dev) dev_close(dev) testing.expect(t, len(list_device_result(ld).cmds) > 15, "") testing.expect(t, len(track.allocation_map) > 0, "the recording allocated") list_device_destroy(ld) } testing.expectf(t, len(track.allocation_map) == 0, "%d allocations were not given back", len(track.allocation_map)) testing.expect_value(t, len(track.bad_free_array), 0) // And the same for a list destroyed before the device that owns it. { ld, _ := list_device_make(alloc) dev_fill_path(list_device_as_device(ld), &p, false, MATRIX_IDENTITY, paint) display_list_destroy(list_device_result(ld)) list_device_destroy(ld) } testing.expectf(t, len(track.allocation_map) == 0, "%d allocations survived a list destroyed out of order", len(track.allocation_map)) testing.expect_value(t, len(track.bad_free_array), 0) }