v0.1.0 47aaa0f

atlas.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 12

↑↓ moveenter openesc close