1package text
2
3import (
4 "errors"
5 "image"
6)
7
8// Upload is an immutable tightly packed grayscale rectangle, for the future renderer.
9// It contains the latest pixels for Rect, including zeroed areas after eviction.
10type Upload struct {
11 Page int
12 Rect image.Rectangle
13 Bytes []byte
14}
15type Placement struct {
16 Page int
17 Rect image.Rectangle
18 Origin image.Point
19}
20type atlasEntry struct {
21 placement Placement
22 generation uint64
23}
24type atlasPage struct {
25 pixels []byte
26 x, y, row int
27 generation uint64
28}
29
30// Atlas owns fixed-size monochrome pages. One pixel padding isolates bilinear samples.
31// It is not concurrent-safe; call BeginFrame before inserting a new frame and drain Uploads
32// before displaying that frame. An active page is never evicted within the same frame.
33// Placements expire when their page is evicted; the caller must only retain them
34// for its current frame. Drain Uploads once per frame; undrained changes are
35// coalesced, never discarded. At most one pending upload per page (bounding
36// union of all dirty rects) uses at most MaxPages*Size*Size pending bytes.
37type Atlas struct {
38 Size, MaxPages int
39 pages []atlasPage
40 entries map[Key]atlasEntry
41 frame uint64
42 dirty map[int]image.Rectangle
43 empty int // cached empty-glyph entries, bounded by maxEmptyGlyphs
44}
45
46// maxEmptyGlyphs bounds cached empty glyphs (spaces), which own no page and so
47// are never dropped by page eviction.
48const maxEmptyGlyphs = 1024
49
50func NewAtlas(size, maxPages int) (*Atlas, error) {
51 if size < 4 || size > 4096 || maxPages < 1 || maxPages > 64 {
52 return nil, errors.New("invalid atlas bounds")
53 }
54 return &Atlas{Size: size, MaxPages: maxPages, entries: map[Key]atlasEntry{}, dirty: map[int]image.Rectangle{}, frame: 1}, nil
55}
56func (a *Atlas) BeginFrame() { a.frame++ }
57
58// PagesUsed reports the number of allocated atlas pages.
59func (a *Atlas) PagesUsed() int { return len(a.pages) }
60
61// MarkAllDirty restores the GPU mirror after a failed upload or atlas recreation.
62// Existing pending changes are subsumed by the full-page rectangles.
63func (a *Atlas) MarkAllDirty() {
64 for page := range a.pages {
65 a.dirty[page] = image.Rect(0, 0, a.Size, a.Size)
66 }
67}
68
69// Uploads returns a snapshot of all pending page changes, then clears the pending
70// set. Call once per frame before submitting the renderer uploads.
71func (a *Atlas) Uploads() []Upload {
72 out := make([]Upload, 0, len(a.dirty))
73 for page := range a.pages {
74 rect, ok := a.dirty[page]
75 if !ok {
76 continue
77 }
78 p := &a.pages[page]
79 w, h := rect.Dx(), rect.Dy()
80 buf := make([]byte, w*h)
81 for y := 0; y < h; y++ {
82 copy(buf[y*w:(y+1)*w], p.pixels[(rect.Min.Y+y)*a.Size+rect.Min.X:])
83 }
84 out = append(out, Upload{Page: page, Rect: rect, Bytes: buf})
85 }
86 clear(a.dirty)
87 return out
88}
89func (a *Atlas) markDirty(page int, rect image.Rectangle) {
90 if old, ok := a.dirty[page]; ok {
91 rect = old.Union(rect)
92 }
93 a.dirty[page] = rect
94}
95
96// Lookup only updates the page generation; a hit neither rasterizes nor uploads.
97func (a *Atlas) Lookup(k Key) (Placement, bool) {
98 e, ok := a.entries[k]
99 if !ok {
100 return Placement{}, false
101 }
102 if e.placement.Page < 0 {
103 return e.placement, true // empty glyph: no page to keep alive
104 }
105 e.generation = a.frame
106 a.entries[k] = e
107 a.pages[e.placement.Page].generation = a.frame
108 return e.placement, true
109}
110func (a *Atlas) Insert(k Key, m Mask) (Placement, error) {
111 if m.Alpha == nil || m.Alpha.Rect.Min != image.Pt(0, 0) || m.Alpha.Stride < m.Alpha.Rect.Dx() || len(m.Alpha.Pix) < m.Alpha.Stride*m.Alpha.Rect.Dy() {
112 return Placement{}, errors.New("invalid mask")
113 }
114 if p, ok := a.Lookup(k); ok {
115 return p, nil
116 }
117 w, h := m.Alpha.Rect.Dx(), m.Alpha.Rect.Dy()
118 if w < 0 || h < 0 || w+2 > a.Size || h+2 > a.Size {
119 return Placement{}, errors.New("glyph too large")
120 }
121 if w == 0 || h == 0 {
122 // Cache empty glyphs (spaces) too, so they are not rasterized every
123 // frame. They own no page, so a full set is dropped instead of evicted.
124 if a.empty >= maxEmptyGlyphs {
125 for key, e := range a.entries {
126 if e.placement.Page < 0 {
127 delete(a.entries, key)
128 }
129 }
130 a.empty = 0
131 }
132 place := Placement{Page: -1, Origin: m.Origin}
133 a.entries[k] = atlasEntry{placement: place}
134 a.empty++
135 return place, nil
136 }
137 chosen := -1
138 var x, y int
139 for i := range a.pages {
140 if xx, yy, ok := a.position(&a.pages[i], w+2, h+2); ok {
141 chosen = i
142 x = xx
143 y = yy
144 break
145 }
146 }
147 if chosen < 0 {
148 if len(a.pages) < a.MaxPages {
149 chosen = len(a.pages)
150 a.pages = append(a.pages, atlasPage{pixels: make([]byte, a.Size*a.Size)})
151 } else {
152 oldest := uint64(^uint64(0))
153 for i, p := range a.pages {
154 if p.generation < a.frame && p.generation < oldest {
155 oldest = p.generation
156 chosen = i
157 }
158 }
159 if chosen < 0 {
160 return Placement{}, errors.New("atlas full: all pages used this frame")
161 }
162 for key, e := range a.entries {
163 if e.placement.Page == chosen {
164 delete(a.entries, key)
165 }
166 }
167 a.pages[chosen] = atlasPage{pixels: make([]byte, a.Size*a.Size)}
168 a.markDirty(chosen, image.Rect(0, 0, a.Size, a.Size))
169 }
170 x, y = 0, 0
171 }
172 p := &a.pages[chosen]
173 if x == 0 && p.x != 0 {
174 p.row = 0
175 }
176 p.x = x + w + 2
177 p.y = y
178 p.row = max(p.row, h+2)
179 p.generation = a.frame
180 rect := image.Rect(x+1, y+1, x+1+w, y+1+h)
181 buf := make([]byte, w*h)
182 for j := 0; j < h; j++ {
183 copy(buf[j*w:(j+1)*w], m.Alpha.Pix[j*m.Alpha.Stride:j*m.Alpha.Stride+w])
184 copy(p.pixels[(y+1+j)*a.Size+x+1:], buf[j*w:(j+1)*w])
185 }
186 place := Placement{Page: chosen, Rect: rect, Origin: m.Origin}
187 a.entries[k] = atlasEntry{place, a.frame}
188 a.markDirty(chosen, rect)
189 return place, nil
190}
191func (a *Atlas) position(p *atlasPage, w, h int) (int, int, bool) {
192 x, y := p.x, p.y
193 if x+w > a.Size {
194 x = 0
195 y += p.row
196 }
197 return x, y, y+h <= a.Size
198}