Files
pdf-odin-llm/device_list_test.odin
T

1275 lines
50 KiB
Odin
Raw Normal View History

2026-09-25 23:32:29 +02:00
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)
}