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}
44
45func NewAtlas(size, maxPages int) (*Atlas, error) {
46 if size < 4 || size > 4096 || maxPages < 1 || maxPages > 64 {
47 return nil, errors.New("invalid atlas bounds")
48 }
49 return &Atlas{Size: size, MaxPages: maxPages, entries: map[Key]atlasEntry{}, dirty: map[int]image.Rectangle{}, frame: 1}, nil
50}
51func (a *Atlas) BeginFrame() { a.frame++ }
52
53// PagesUsed reports the number of allocated atlas pages.
54func (a *Atlas) PagesUsed() int { return len(a.pages) }
55
56// MarkAllDirty restores the GPU mirror after a failed upload or atlas recreation.
57// Existing pending changes are subsumed by the full-page rectangles.
58func (a *Atlas) MarkAllDirty() {
59 for page := range a.pages {
60 a.dirty[page] = image.Rect(0, 0, a.Size, a.Size)
61 }
62}
63
64// Uploads returns a snapshot of all pending page changes, then clears the pending
65// set. Call once per frame before submitting the renderer uploads.
66func (a *Atlas) Uploads() []Upload {
67 out := make([]Upload, 0, len(a.dirty))
68 for page := range a.pages {
69 rect, ok := a.dirty[page]
70 if !ok {
71 continue
72 }
73 p := &a.pages[page]
74 w, h := rect.Dx(), rect.Dy()
75 buf := make([]byte, w*h)
76 for y := 0; y < h; y++ {
77 copy(buf[y*w:(y+1)*w], p.pixels[(rect.Min.Y+y)*a.Size+rect.Min.X:])
78 }
79 out = append(out, Upload{Page: page, Rect: rect, Bytes: buf})
80 }
81 clear(a.dirty)
82 return out
83}
84func (a *Atlas) markDirty(page int, rect image.Rectangle) {
85 if old, ok := a.dirty[page]; ok {
86 rect = old.Union(rect)
87 }
88 a.dirty[page] = rect
89}
90
91// Lookup only updates the page generation; a hit neither rasterizes nor uploads.
92func (a *Atlas) Lookup(k Key) (Placement, bool) {
93 e, ok := a.entries[k]
94 if !ok {
95 return Placement{}, false
96 }
97 e.generation = a.frame
98 a.entries[k] = e
99 a.pages[e.placement.Page].generation = a.frame
100 return e.placement, true
101}
102func (a *Atlas) Insert(k Key, m Mask) (Placement, error) {
103 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() {
104 return Placement{}, errors.New("invalid mask")
105 }
106 if p, ok := a.Lookup(k); ok {
107 return p, nil
108 }
109 w, h := m.Alpha.Rect.Dx(), m.Alpha.Rect.Dy()
110 if w < 0 || h < 0 || w+2 > a.Size || h+2 > a.Size {
111 return Placement{}, errors.New("glyph too large")
112 }
113 if w == 0 || h == 0 {
114 return Placement{Page: -1, Origin: m.Origin}, nil
115 }
116 chosen := -1
117 var x, y int
118 for i := range a.pages {
119 if xx, yy, ok := a.position(&a.pages[i], w+2, h+2); ok {
120 chosen = i
121 x = xx
122 y = yy
123 break
124 }
125 }
126 if chosen < 0 {
127 if len(a.pages) < a.MaxPages {
128 chosen = len(a.pages)
129 a.pages = append(a.pages, atlasPage{pixels: make([]byte, a.Size*a.Size)})
130 } else {
131 oldest := uint64(^uint64(0))
132 for i, p := range a.pages {
133 if p.generation < a.frame && p.generation < oldest {
134 oldest = p.generation
135 chosen = i
136 }
137 }
138 if chosen < 0 {
139 return Placement{}, errors.New("atlas full: all pages used this frame")
140 }
141 for key, e := range a.entries {
142 if e.placement.Page == chosen {
143 delete(a.entries, key)
144 }
145 }
146 a.pages[chosen] = atlasPage{pixels: make([]byte, a.Size*a.Size)}
147 a.markDirty(chosen, image.Rect(0, 0, a.Size, a.Size))
148 }
149 x, y = 0, 0
150 }
151 p := &a.pages[chosen]
152 if x == 0 && p.x != 0 {
153 p.row = 0
154 }
155 p.x = x + w + 2
156 p.y = y
157 p.row = max(p.row, h+2)
158 p.generation = a.frame
159 rect := image.Rect(x+1, y+1, x+1+w, y+1+h)
160 buf := make([]byte, w*h)
161 for j := 0; j < h; j++ {
162 copy(buf[j*w:(j+1)*w], m.Alpha.Pix[j*m.Alpha.Stride:j*m.Alpha.Stride+w])
163 copy(p.pixels[(y+1+j)*a.Size+x+1:], buf[j*w:(j+1)*w])
164 }
165 place := Placement{Page: chosen, Rect: rect, Origin: m.Origin}
166 a.entries[k] = atlasEntry{place, a.frame}
167 a.markDirty(chosen, rect)
168 return place, nil
169}
170func (a *Atlas) position(p *atlasPage, w, h int) (int, int, bool) {
171 x, y := p.x, p.y
172 if x+w > a.Size {
173 x = 0
174 y += p.row
175 }
176 return x, y, y+h <= a.Size
177}