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
+63 -32
View File
@@ -15,6 +15,13 @@ const (
RenderLayerLines
)
const (
drawPlanSinglePassClipEnabled = false
// best value according to BenchmarkDrawPlanSinglePass_Lines_GG
maxLineSegmentsPerStroke = 32
)
// RenderOptions controls which layers are rendered and their order.
// If Layers is empty, the default order is: Points, Circles, Lines.
type RenderOptions struct {
@@ -174,11 +181,7 @@ func (w *World) Render(drawer PrimitiveDrawer, params RenderParams) error {
var bg color.Color = color.RGBA{A: 255} // default black
if params.Options != nil && params.Options.BackgroundColor != nil {
if v, ok := params.Options.BackgroundColor.(color.RGBA); !ok {
panic("Options.BackgroundColor is not color.RGBA type")
} else {
bg = v
}
bg = params.Options.BackgroundColor
} else {
tc := w.Theme().BackgroundColor()
if alphaNonZero(tc) {
@@ -225,6 +228,30 @@ func (w *World) Render(drawer PrimitiveDrawer, params RenderParams) error {
policy = *params.Options.Incremental
}
// Helper: draw one dirty rect with outer clip, using an already prepared dirtyPlan.
// IMPORTANT: dirtyPlan must be built from the FULL set of dirty rects (union),
// not from a single rect, to avoid missing primitives on diagonal pans.
drawDirtyRect := func(dirtyPlan RenderPlan, r RectPx) error {
if r.W <= 0 || r.H <= 0 {
return nil
}
drawer.Save()
drawer.ResetClip()
drawer.ClipRect(float64(r.X), float64(r.Y), float64(r.W), float64(r.H))
// Clear + background in the same clip.
drawer.ClearRectTo(r.X, r.Y, r.W, r.H, bg)
w.drawBackground(drawer, params, r)
// Draw with outer clip only; do not rebuild plan per-rect.
// isDirtyPass MUST be true here.
w.drawPlanSinglePass(drawer, dirtyPlan, allowWrap, drawPlanSinglePassClipEnabled, true)
drawer.Restore()
return nil
}
// --- Try incremental path first when state is initialized and geometry matches ---
dxPx, dyPx, derr := w.ComputePanShiftPx(params)
if derr == nil {
@@ -245,23 +272,25 @@ func (w *World) Render(drawer PrimitiveDrawer, params RenderParams) error {
// If we accumulated dirty regions during shift-only frames, redraw them now (bounded).
if len(w.renderState.pendingDirty) > 0 {
toDraw, remaining := takeCatchUpRects(w.renderState.pendingDirty, policy.MaxCatchUpAreaPx)
w.renderState.pendingDirty = remaining
if len(toDraw) > 0 {
for _, r := range toDraw {
drawer.ClearRectTo(r.X, r.Y, r.W, r.H, bg)
w.drawBackground(drawer, params, r)
}
plan, err := w.buildRenderPlanStageA(params)
if err != nil {
return err
}
catchUpPlan := planRestrictedToDirtyRects(plan, toDraw)
w.drawPlanSinglePass(drawer, catchUpPlan, allowWrap)
if len(toDraw) == 0 {
return nil
}
w.renderState.pendingDirty = remaining
plan, err := w.buildRenderPlanStageA(params)
if err != nil {
return err
}
// Build once for the whole set of catch-up rects (union), then clip per rect.
catchUpPlan := planRestrictedToDirtyRects(plan, toDraw)
for _, r := range toDraw {
if err := drawDirtyRect(catchUpPlan, r); err != nil {
return err
}
}
}
return nil
@@ -276,7 +305,7 @@ func (w *World) Render(drawer PrimitiveDrawer, params RenderParams) error {
}
w.renderState.pendingDirty = moved
}
// C5: shift backing pixels, then redraw only dirty strips.
// Shift backing pixels first.
drawer.CopyShift(inc.DxPx, inc.DyPx)
overBudget := false
@@ -294,17 +323,9 @@ func (w *World) Render(drawer PrimitiveDrawer, params RenderParams) error {
return nil
}
// [ ] Сразу после overBudget вычисления и до построения dirtyPlan
// Draw both the newly exposed strips and any previously deferred dirty regions.
// Always redraw newly exposed strips fully.
// Under budget: draw newly exposed strips immediately, plus bounded catch-up.
dirtyToDraw := inc.Dirty
for _, r := range dirtyToDraw {
drawer.ClearRectTo(r.X, r.Y, r.W, r.H, bg)
w.drawBackground(drawer, params, r)
}
// Additionally redraw a bounded portion of deferred dirty regions.
if len(w.renderState.pendingDirty) > 0 {
catchUp, remaining := takeCatchUpRects(w.renderState.pendingDirty, policy.MaxCatchUpAreaPx)
@@ -312,14 +333,24 @@ func (w *World) Render(drawer PrimitiveDrawer, params RenderParams) error {
w.renderState.pendingDirty = remaining
}
if len(dirtyToDraw) == 0 {
return nil
}
plan, err := w.buildRenderPlanStageA(params)
if err != nil {
return err
}
dirtyPlan := planRestrictedToDirtyRects(plan, dirtyToDraw)
w.drawPlanSinglePass(drawer, dirtyPlan, allowWrap)
// State already updated by ComputePanShiftPx (lastWorldRect advanced).
// Build once for the union of all dirty rects.
dirtyPlan := planRestrictedToDirtyRects(plan, dirtyToDraw)
// Draw per-rect with outer clip; background/clear done inside helper.
for _, r := range dirtyToDraw {
if err := drawDirtyRect(dirtyPlan, r); err != nil {
return err
}
}
return nil
case IncrementalFullRedraw:
@@ -337,7 +368,7 @@ func (w *World) Render(drawer PrimitiveDrawer, params RenderParams) error {
drawer.ClearAllTo(bg)
w.drawBackground(drawer, params, RectPx{X: 0, Y: 0, W: params.CanvasWidthPx(), H: params.CanvasHeightPx()})
w.drawPlanSinglePass(drawer, plan, allowWrap)
w.drawPlanSinglePass(drawer, plan, allowWrap, drawPlanSinglePassClipEnabled, true)
return w.CommitFullRedrawState(params)
}