draw optimizations

This commit is contained in:
IliaDenisov
2026-03-08 23:30:11 +02:00
parent fdcbb5d6f4
commit ac35360d60
18 changed files with 875 additions and 566 deletions
+118 -44
View File
@@ -20,9 +20,9 @@ type drawItem struct {
styleID StyleID
// Exactly one of these is set.
p *Point
c *Circle
l *Line
p Point
c Circle
l Line
}
// drawPlanSinglePass renders a plan using a single ordered pass per tile.
@@ -31,7 +31,9 @@ type drawItem struct {
// allowWrap controls torus behavior:
// - true: circles/points produce wrap copies, lines use torus-shortest segments
// - false: no copies, lines drawn directly as stored
func (w *World) drawPlanSinglePass(drawer PrimitiveDrawer, plan RenderPlan, allowWrap bool) {
// tileClipEnabled controls whether per-tile ClipRect is applied.
// When an outer clip is already set (e.g. dirty rect), disable tile clips for speed.
func (w *World) drawPlanSinglePass(drawer PrimitiveDrawer, plan RenderPlan, allowWrap bool, tileClipEnabled bool, isDirtyPass bool) {
var lastStyleID StyleID = StyleIDInvalid
var lastStyle Style
@@ -41,11 +43,9 @@ func (w *World) drawPlanSinglePass(drawer PrimitiveDrawer, plan RenderPlan, allo
}
s, ok := w.styles.Get(styleID)
if !ok {
// Unknown style ID is a programming/config error.
panic("render: unknown style ID")
}
// Apply style state. Some fields may be nil intentionally.
if s.FillColor != nil {
drawer.SetFillColor(s.FillColor)
}
@@ -56,7 +56,6 @@ func (w *World) drawPlanSinglePass(drawer PrimitiveDrawer, plan RenderPlan, allo
if len(s.StrokeDashes) > 0 {
drawer.SetDash(s.StrokeDashes...)
} else {
// Ensure solid line when switching from dashed style.
drawer.SetDash()
}
drawer.SetDashOffset(s.StrokeDashOffset)
@@ -70,52 +69,61 @@ func (w *World) drawPlanSinglePass(drawer PrimitiveDrawer, plan RenderPlan, allo
continue
}
// Collect items for this tile.
items := make([]drawItem, 0, len(td.Candidates))
// Per-tile clip is optional. When outer-clip is used (dirty rect),
// tileClipEnabled must be false to avoid resetting the outer clip.
if tileClipEnabled {
drawer.Save()
drawer.ResetClip()
drawer.ClipRect(float64(td.ClipX), float64(td.ClipY), float64(td.ClipW), float64(td.ClipH))
}
items := w.scratchDrawItems[:0]
if cap(items) < len(td.Candidates) {
items = make([]drawItem, 0, len(td.Candidates))
}
for _, it := range td.Candidates {
id := it.ID()
cur, ok := w.objects[id]
if !ok {
// Stale grid entry (object removed). Skip.
continue
}
switch v := cur.(type) {
case Point:
vv := v
items = append(items, drawItem{
kind: drawKindPoint,
priority: vv.Priority,
id: vv.Id,
styleID: vv.StyleID,
p: &vv,
priority: v.Priority,
id: v.Id,
styleID: v.StyleID,
p: v,
})
case Circle:
vv := v
items = append(items, drawItem{
kind: drawKindCircle,
priority: vv.Priority,
id: vv.Id,
styleID: vv.StyleID,
c: &vv,
priority: v.Priority,
id: v.Id,
styleID: v.StyleID,
c: v,
})
case Line:
vv := v
items = append(items, drawItem{
kind: drawKindLine,
priority: vv.Priority,
id: vv.Id,
styleID: vv.StyleID,
l: &vv,
priority: v.Priority,
id: v.Id,
styleID: v.StyleID,
l: v,
})
default:
// Unknown map items should not exist.
panic("render: unknown map item type")
}
}
if len(items) == 0 {
if tileClipEnabled {
drawer.Restore()
}
w.scratchDrawItems = items[:0]
continue
}
@@ -130,27 +138,93 @@ func (w *World) drawPlanSinglePass(drawer PrimitiveDrawer, plan RenderPlan, allo
return a.id < b.id
})
drawer.Save()
drawer.ClipRect(float64(td.ClipX), float64(td.ClipY), float64(td.ClipW), float64(td.ClipH))
// If this is not a dirty pass (full redraw), keep the old behavior for lines:
// stroke per segment. This is usually faster for gg on huge scenes.
if !isDirtyPass {
for i := 0; i < len(items); i++ {
di := items[i]
applyStyle(di.styleID)
for _, di := range items {
applyStyle(di.styleID)
switch di.kind {
case drawKindPoint:
w.drawPointInTile(drawer, plan, td, *di.p, allowWrap, lastStyle)
case drawKindCircle:
w.drawCircleInTile(drawer, plan, td, *di.c, allowWrap, lastStyle)
case drawKindLine:
w.drawLineInTile(drawer, plan, td, *di.l, allowWrap)
default:
panic("render: unknown draw kind")
switch di.kind {
case drawKindPoint:
w.drawPointInTile(drawer, plan, td, di.p, allowWrap, lastStyle)
case drawKindCircle:
w.drawCircleInTile(drawer, plan, td, di.c, allowWrap, lastStyle)
case drawKindLine:
// Old behavior: drawLineInTile includes Stroke() per segment.
w.drawLineInTile(drawer, plan, td, di.l, allowWrap)
default:
panic("render: unknown draw kind")
}
}
} else {
// Dirty pass: batch lines to reduce overhead while panning.
inLineRun := false
var lineRunStyleID StyleID
lineSegCount := 0
flushLineRun := func() {
if !inLineRun {
return
}
drawer.Stroke()
inLineRun = false
lineSegCount = 0
}
for i := 0; i < len(items); i++ {
di := items[i]
if inLineRun {
if di.kind != drawKindLine || di.styleID != lineRunStyleID {
flushLineRun()
}
}
switch di.kind {
case drawKindLine:
if !inLineRun {
lineRunStyleID = di.styleID
applyStyle(lineRunStyleID)
inLineRun = true
} else {
// style matches by construction; keep style state valid if code changes later
applyStyle(di.styleID)
}
added := w.drawLineInTilePath(drawer, plan, td, di.l, allowWrap)
lineSegCount += added
if lineSegCount >= maxLineSegmentsPerStroke {
drawer.Stroke()
lineSegCount = 0
// keep run active
}
case drawKindPoint:
flushLineRun()
applyStyle(di.styleID)
w.drawPointInTile(drawer, plan, td, di.p, allowWrap, lastStyle)
case drawKindCircle:
flushLineRun()
applyStyle(di.styleID)
w.drawCircleInTile(drawer, plan, td, di.c, allowWrap, lastStyle)
default:
flushLineRun()
panic("render: unknown draw kind")
}
}
flushLineRun()
}
drawer.Restore()
if tileClipEnabled {
drawer.Restore()
}
// Reuse buffer for next tile.
w.scratchDrawItems = items[:0]
}
}