1// Package render prepares ordered physical-pixel primitives for Vulkan submission.
2// It does not own the display list or its borrowed font faces.
3package render
4
5import (
6 "fmt"
7 "image"
8 "math"
9
10 "github.com/bnema/nefergui/internal/css"
11 "github.com/bnema/nefergui/internal/layout"
12 "github.com/bnema/nefergui/internal/text"
13 "github.com/go-text/typesetting/font"
14)
15
16// Quad is a physical-pixel rectangle with a physical clip. Color is straight
17// sRGB; conversion to premultiplied linear color belongs at the GPU boundary.
18// Glyphs refer to an atlas placement valid only for this prepared frame.
19type Quad struct {
20 Op string
21 Rect layout.Rect
22 Clip layout.Rect
23 Color css.Color
24 Radii [4]float64
25 Widths layout.Edges
26 Colors css.ColorSides
27 Glyph text.Placement
28 Image image.Image
29 // Shape is the original box when the draw bounds extend past it.
30 Shape layout.Rect
31 // Shadow holds physical offset, blur and spread. Inset is carried separately.
32 Shadow [4]float64
33 Inset bool
34}
35
36type Frame struct {
37 Quads []Quad
38 Uploads []text.Upload
39}
40
41// Preparer owns the glyph atlas and the obligation to deliver its changes to the
42// GPU mirror. Prepare hands the caller every change not yet known to be
43// submitted; Submitted is the only acknowledgement. A frame that is retried,
44// abandoned, superseded or never reached submission therefore never loses
45// atlas changes: the next Prepare resends whole pages (placements are kept).
46type Preparer struct {
47 Atlas *text.Atlas
48 // owed is true while the latest issued uploads have not been submitted.
49 owed bool
50 issued []text.Upload
51}
52
53// NewPreparer bounds the grayscale glyph atlas. Call Prepare synchronously with
54// layout; the atlas and shaped faces are not safe for concurrent use.
55func NewPreparer(size, pages int) (*Preparer, error) {
56 a, err := text.NewAtlas(size, pages)
57 if err != nil {
58 return nil, err
59 }
60 return &Preparer{Atlas: a}, nil
61}
62
63func intersect(a, b layout.Rect) layout.Rect {
64 x, y := math.Max(a.X, b.X), math.Max(a.Y, b.Y)
65 return layout.Rect{X: x, Y: y, W: math.Max(0, math.Min(a.X+a.W, b.X+b.W)-x), H: math.Max(0, math.Min(a.Y+a.H, b.Y+b.H)-y)}
66}
67
68func physical(r layout.Rect, scale float64) layout.Rect {
69 x, y, w, h := layout.Physical(r, scale)
70 return layout.Rect{X: float64(x), Y: float64(y), W: float64(w), H: float64(h)}
71}
72
73func scaled(e layout.Edges, scale float64) layout.Edges {
74 return layout.Edges{Top: e.Top * scale, Right: e.Right * scale, Bottom: e.Bottom * scale, Left: e.Left * scale}
75}
76
77// maxQuadPrealloc bounds speculative storage for mostly clipped documents.
78// Visible frames larger than this grow normally as their quads are emitted.
79const maxQuadPrealloc = 512
80
81// quadCapacity estimates one quad per glyph and drawable command, capped so
82// offscreen content cannot force a large allocation before culling.
83func quadCapacity(commands []layout.Command) int {
84 n := 0
85 for i := range commands {
86 switch cmd := &commands[i]; cmd.Op {
87 case "clip-push", "clip-pop":
88 case "text", "preedit-text":
89 for j := range cmd.Runs {
90 n += len(cmd.Runs[j].Glyphs)
91 }
92 default:
93 n++
94 }
95 }
96 return min(n, maxQuadPrealloc)
97}
98
99// Prepare converts logical rectangles once, using layout.Physical as the sole
100// logical-to-physical edge-rounding boundary. A clip-push intersects its parent
101// clip; rounded clips intentionally use only their axis-aligned bounds in V1.
102// Clip-pop without a push and unknown operations are rejected, not dropped.
103// Image commands without a source are rejected. Failed frames do not produce
104// partial output.
105func (p *Preparer) Prepare(commands []layout.Command, scale float64, width, height int) (Frame, error) {
106 if p == nil || p.Atlas == nil || scale <= 0 || math.IsNaN(scale) || math.IsInf(scale, 0) || width <= 0 || height <= 0 {
107 return Frame{}, fmt.Errorf("render: invalid frame dimensions, scale, or atlas")
108 }
109 p.Atlas.BeginFrame()
110 frame := Frame{}
111 if n := quadCapacity(commands); n > 0 {
112 // Frames are returned detached and may outlive later Prepare calls, so
113 // the backing array is never reused. Bound the initial reservation;
114 // larger visible frames grow through append after culling.
115 frame.Quads = make([]Quad, 0, n)
116 }
117 clip := layout.Rect{W: float64(width), H: float64(height)}
118 stack := []layout.Rect{}
119 for _, cmd := range commands {
120 switch cmd.Op {
121 case "clip-push":
122 stack = append(stack, clip)
123 clip = intersect(clip, physical(cmd.Rect, scale))
124 continue
125 case "clip-pop":
126 if len(stack) == 0 {
127 return Frame{}, fmt.Errorf("render: unbalanced clip-pop")
128 }
129 clip = stack[len(stack)-1]
130 stack = stack[:len(stack)-1]
131 continue
132 case "text", "preedit-text":
133 for _, run := range cmd.Runs {
134 if run.Face == nil || run.FaceID != run.Face.ID || run.Size <= 0 {
135 return Frame{}, fmt.Errorf("render: missing or mismatched face for %q", cmd.ID)
136 }
137 for _, glyph := range run.Glyphs {
138 x, y := glyph.X*scale, glyph.Y*scale
139 key, err := text.GlyphKey(run.Face, font.GID(glyph.ID), run.Size*scale, x, run.Face.Variations)
140 if err != nil {
141 return Frame{}, err
142 }
143 place, ok := p.Atlas.Lookup(key)
144 if !ok {
145 mask, err := text.Rasterize(run.Face, key)
146 if err != nil {
147 return Frame{}, err
148 }
149 place, err = p.Atlas.Insert(key, mask)
150 if err != nil {
151 return Frame{}, err
152 }
153 }
154 if place.Page < 0 || place.Rect.Empty() {
155 continue
156 }
157 r := layout.Rect{X: float64(int(math.Floor(x)) + place.Origin.X), Y: float64(int(math.Floor(y)) + place.Origin.Y), W: float64(place.Rect.Dx()), H: float64(place.Rect.Dy())}
158 if intersect(clip, r).W > 0 && intersect(clip, r).H > 0 {
159 frame.Quads = append(frame.Quads, Quad{Op: "glyph", Rect: r, Clip: clip, Color: opacity(cmd.Color, cmd.Opacity), Glyph: place})
160 }
161 }
162 }
163 continue
164 case "image":
165 if cmd.Image == nil {
166 return Frame{}, fmt.Errorf("render: image %q has no pixel source", cmd.ID)
167 }
168 case "rect", "border", "outline", "shadow", "selection", "caret", "preedit-underline", "preedit-caret":
169 default:
170 return Frame{}, fmt.Errorf("render: unknown display operation %q", cmd.Op)
171 }
172 r := physical(cmd.Rect, scale)
173 shape := r
174 var shadow [4]float64
175 inset := false
176 if cmd.Op == "outline" {
177 // Layout already applies outline-offset to Rect; width grows outward.
178 w := cmd.Widths.Top * scale
179 r = layout.Rect{X: r.X - w, Y: r.Y - w, W: r.W + 2*w, H: r.H + 2*w}
180 }
181 if cmd.Op == "shadow" {
182 if cmd.Shadow == nil {
183 return Frame{}, fmt.Errorf("render: shadow %q has no parameters", cmd.ID)
184 }
185 sh := cmd.Shadow
186 shadow = [4]float64{sh.X.Value * scale, sh.Y.Value * scale, sh.Blur.Value * scale, sh.Spread.Value * scale}
187 inset = sh.Inset
188 if !inset {
189 // Three standard deviations cover the visible Gaussian tail.
190 pad := shadow[2]*1.5 + math.Max(0, shadow[3])
191 r = layout.Rect{X: r.X + shadow[0] - pad, Y: r.Y + shadow[1] - pad, W: r.W + 2*pad, H: r.H + 2*pad}
192 }
193 }
194 if intersect(clip, r).W == 0 || intersect(clip, r).H == 0 {
195 continue
196 }
197 color := cmd.Color
198 if cmd.Op == "shadow" {
199 color = cmd.Shadow.Color
200 }
201 q := Quad{Op: cmd.Op, Rect: r, Clip: clip, Color: opacity(color, cmd.Opacity), Widths: scaled(cmd.Widths, scale), Colors: cmd.Colors, Shape: shape, Shadow: shadow, Inset: inset, Image: cmd.Image}
202 if cmd.Op == "image" {
203 q.Color = css.Color{R: 1, G: 1, B: 1, A: opacity(css.Color{A: 1}, cmd.Opacity).A}
204 }
205 for i, v := range cmd.Radii {
206 q.Radii[i] = v * scale
207 }
208 frame.Quads = append(frame.Quads, q)
209 }
210 if len(stack) > 0 {
211 return Frame{}, fmt.Errorf("render: unbalanced clip-push")
212 }
213 if p.owed {
214 // The previous frame's uploads may never have reached the GPU. Full pages
215 // subsume them and any changes made since.
216 p.Atlas.MarkAllDirty()
217 }
218 frame.Uploads = p.Atlas.Uploads()
219 p.issued, p.owed = frame.Uploads, len(frame.Uploads) > 0
220 return frame, nil
221}
222
223// Submitted acknowledges that uploads, as returned in the most recent Prepare's
224// Frame.Uploads, were recorded in a successfully submitted GPU submission.
225// Any other slice (an older, superseded frame or foreign data) is ignored and
226// the latest frame's uploads stay owed. Call it on the Prepare owner's loop.
227func (p *Preparer) Submitted(uploads []text.Upload) {
228 if p == nil || len(uploads) == 0 || len(uploads) != len(p.issued) || &uploads[0] != &p.issued[0] {
229 return
230 }
231 p.owed, p.issued = false, nil
232}
233
234func opacity(c css.Color, a float64) css.Color {
235 c.A *= math.Max(0, math.Min(1, a))
236 return c
237}