detached 7dfb0ed

feat(render): draw client SHM contents in window rects

commit
7dfb0ed4617be2237d525c9f8f17399008b9c993
author
brice · · 2026-09-23 15:26 UTC
parent
f1e2d50
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +141 -28
M internal/adapters/vulkan/renderer.go +97 -27
@@ -29,6 +29,7 @@ type Renderer struct {
2929 staging vk.Buffer
3030 stagingMemory vk.DeviceMemory
3131 stagingMapped unsafe.Pointer
32+ stagingSize int
3233 mapped unsafe.Pointer
3334 pool vk.CommandPool
3435 command vk.CommandBuffer
@@ -174,8 +175,9 @@ func New(width, height int) (r *Renderer, err error) {
174175 if err = checked("vkMapMemory", r.dd.MapMemory(r.device, r.bufferMemory, 0, size, 0, &r.mapped)); err != nil {
175176 return
176177 }
177- // Two frame-sized regions accommodate non-overlapping window bodies and their overlapping borders.
178+ // Start with two frame-sized regions; later frames grow staging on demand.
178179 stagingSize := size * 2
180+ r.stagingSize = int(stagingSize)
179181 stagingInfo := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: stagingSize, Usage: vk.BufferUsageTransferSrcBit, SharingMode: vk.SharingModeExclusive}
180182 if err = checked("vkCreateBuffer(staging)", r.dd.CreateBuffer(r.device, &stagingInfo, nil, &r.staging)); err != nil {
181183 return
@@ -258,54 +260,122 @@ func windowColor(id ports.WindowID) [3]uint8 {
258260 return [3]uint8{uint8(a * 255), uint8(b * 255), uint8(c * 255)}
259261 }
260262
263+// ensureStaging grows the mapped transfer buffer before recording any commands.
264+func (r *Renderer) ensureStaging(size int) error {
265+ if size <= r.stagingSize {
266+ return nil
267+ }
268+ d := r.dd
269+ capacity := size
270+ info := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: vk.DeviceSize(capacity), Usage: vk.BufferUsageTransferSrcBit, SharingMode: vk.SharingModeExclusive}
271+ var buffer vk.Buffer
272+ if err := checked("vkCreateBuffer(staging)", d.CreateBuffer(r.device, &info, nil, &buffer)); err != nil {
273+ return err
274+ }
275+ var memory vk.DeviceMemory
276+ defer func() {
277+ if buffer != 0 {
278+ d.DestroyBuffer(r.device, buffer, nil)
279+ }
280+ if memory != 0 {
281+ d.FreeMemory(r.device, memory, nil)
282+ }
283+ }()
284+ var req vk.MemoryRequirements
285+ d.GetBufferMemoryRequirements(r.device, buffer, &req)
286+ kind, err := r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit)
287+ if err != nil {
288+ return err
289+ }
290+ alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind}
291+ if err := checked("vkAllocateMemory(staging)", d.AllocateMemory(r.device, &alloc, nil, &memory)); err != nil {
292+ return err
293+ }
294+ if err := checked("vkBindBufferMemory(staging)", d.BindBufferMemory(r.device, buffer, memory, 0)); err != nil {
295+ return err
296+ }
297+ var mapped unsafe.Pointer
298+ if err := checked("vkMapMemory(staging)", d.MapMemory(r.device, memory, 0, vk.DeviceSize(capacity), 0, &mapped)); err != nil {
299+ return err
300+ }
301+ d.UnmapMemory(r.device, r.stagingMemory)
302+ d.DestroyBuffer(r.device, r.staging, nil)
303+ d.FreeMemory(r.device, r.stagingMemory, nil)
304+ r.staging, r.stagingMemory, r.stagingMapped, r.stagingSize = buffer, memory, mapped, capacity
305+ buffer, memory = 0, 0
306+ return nil
307+}
308+
261309 func (r *Renderer) Clear(rgb [3]uint8) error {
262- return r.Render(ports.Scene{Background: fmt.Sprintf("#%02x%02x%02x", rgb[0], rgb[1], rgb[2])})
310+ return r.Render(ports.Scene{Background: fmt.Sprintf("#%02x%02x%02x", rgb[0], rgb[1], rgb[2])}, nil)
263311 }
264312
265-func (r *Renderer) Render(s ports.Scene) error {
266- // Stage before recording: a full buffer is an error, not a partial submission.
267- type fill struct {
268- rect image.Rectangle
269- color [3]uint8
270- offset int
271- }
272- var fills []fill
313+func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) error {
314+ type upload struct {
315+ rect image.Rectangle
316+ offset int
317+ pixels []byte
318+ stride, sourceX, sourceY int
319+ color [3]uint8
320+ }
321+ var uploads []upload
273322 used := 0
274- add := func(rect image.Rectangle, c [3]uint8) error {
275- rect = rect.Intersect(image.Rect(0, 0, r.width, r.height))
323+ bounds := image.Rect(0, 0, r.width, r.height)
324+ add := func(rect image.Rectangle, c [3]uint8, content *ports.SurfaceContent, origin image.Point) {
325+ rect = rect.Intersect(bounds)
276326 if rect.Empty() {
277- return nil
327+ return
278328 }
279329 n := rect.Dx() * rect.Dy() * 4
280- if n > r.width*r.height*8-used {
281- return fmt.Errorf("staging buffer full")
330+ u := upload{rect: rect, offset: used, color: c}
331+ if content != nil {
332+ u.pixels, u.stride = content.Pixels, content.Stride
333+ u.sourceX, u.sourceY = rect.Min.X-origin.X, rect.Min.Y-origin.Y
282334 }
283- data := unsafe.Slice((*byte)(r.stagingMapped), r.width*r.height*8)[used : used+n]
284- for i := 0; i < n; i += 4 {
285- data[i], data[i+1], data[i+2], data[i+3] = c[2], c[1], c[0], 255
286- }
287- fills = append(fills, fill{rect, c, used})
335+ uploads = append(uploads, u)
288336 used += n
289- return nil
290337 }
291338 for _, w := range s.Windows {
292339 if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 {
293340 continue
294341 }
295342 x, y := w.Rect.X, w.Rect.Y
296- // Clip before converting coordinates to Vulkan's signed 32-bit offsets.
297343 body := image.Rect(x, y, x+w.Rect.W, y+w.Rect.H)
298- if err := add(body, windowColor(w.ID)); err != nil {
299- return err
344+ content := contents[w.ID]
345+ if content.Pixels == nil {
346+ add(body, windowColor(w.ID), nil, image.Point{})
347+ } else {
348+ add(body, parseColor(s.Background), nil, image.Point{})
349+ width, height := min(content.Width, w.Rect.W), min(content.Height, w.Rect.H)
350+ if width > 0 && height > 0 && content.Stride >= width*4 && len(content.Pixels) >= (height-1)*content.Stride+width*4 {
351+ add(image.Rect(x, y, x+width, y+height), [3]uint8{}, &content, image.Pt(x, y))
352+ }
300353 }
301354 if w.Focused {
302355 for _, strip := range []image.Rectangle{image.Rect(x, y, x+w.Rect.W, y+4), image.Rect(x, y+w.Rect.H-4, x+w.Rect.W, y+w.Rect.H), image.Rect(x, y+4, x+4, y+w.Rect.H-4), image.Rect(x+w.Rect.W-4, y+4, x+w.Rect.W, y+w.Rect.H-4)} {
303- if err := add(strip, [3]uint8{255, 255, 255}); err != nil {
304- return err
356+ add(strip, [3]uint8{255, 255, 255}, nil, image.Point{})
357+ }
358+ }
359+ }
360+ if err := r.ensureStaging(used); err != nil {
361+ return err
362+ }
363+ data := unsafe.Slice((*byte)(r.stagingMapped), r.stagingSize)
364+ for _, u := range uploads {
365+ dst := data[u.offset : u.offset+u.rect.Dx()*u.rect.Dy()*4]
366+ for row := 0; row < u.rect.Dy(); row++ {
367+ line := dst[row*u.rect.Dx()*4 : (row+1)*u.rect.Dx()*4]
368+ if u.pixels != nil {
369+ start := (u.sourceY+row)*u.stride + u.sourceX*4
370+ copy(line, u.pixels[start:start+len(line)])
371+ } else {
372+ for i := 0; i < len(line); i += 4 {
373+ line[i], line[i+1], line[i+2], line[i+3] = u.color[2], u.color[1], u.color[0], 255
305374 }
306375 }
307376 }
308377 }
378+ // B8G8R8A8 pixels are copied unchanged; blending belongs to the compositing pipeline.
309379 rgb := parseColor(s.Background)
310380 d := r.dd
311381 if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(r.command, 0)); err != nil {
@@ -333,7 +403,7 @@ func (r *Renderer) Render(s ports.Scene) error {
333403 barrier.OldLayout = vk.ImageLayoutTransferDstOptimal
334404 barrier.NewLayout = vk.ImageLayoutTransferDstOptimal
335405 d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)
336- for _, f := range fills {
406+ for _, f := range uploads {
337407 rect := f.rect
338408 region := vk.BufferImageCopy{BufferOffset: vk.DeviceSize(f.offset), ImageSubresource: vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}, ImageOffset: vk.Offset3D{X: int32(rect.Min.X), Y: int32(rect.Min.Y)}, ImageExtent: vk.Extent3D{Width: uint32(rect.Dx()), Height: uint32(rect.Dy()), Depth: 1}}
339409 d.CmdCopyBufferToImage(r.command, r.staging, r.image, vk.ImageLayoutTransferDstOptimal, 1, &region)
@@ -29,6 +29,7 @@ type Renderer struct {
29 staging vk.Buffer29 staging vk.Buffer
30 stagingMemory vk.DeviceMemory30 stagingMemory vk.DeviceMemory
31 stagingMapped unsafe.Pointer31 stagingMapped unsafe.Pointer
32+ stagingSize int
32 mapped unsafe.Pointer33 mapped unsafe.Pointer
33 pool vk.CommandPool34 pool vk.CommandPool
34 command vk.CommandBuffer35 command vk.CommandBuffer
@@ -174,8 +175,9 @@ func New(width, height int) (r *Renderer, err error) {
174 if err = checked("vkMapMemory", r.dd.MapMemory(r.device, r.bufferMemory, 0, size, 0, &r.mapped)); err != nil {175 if err = checked("vkMapMemory", r.dd.MapMemory(r.device, r.bufferMemory, 0, size, 0, &r.mapped)); err != nil {
175 return176 return
176 }177 }
177- // Two frame-sized regions accommodate non-overlapping window bodies and their overlapping borders.178+ // Start with two frame-sized regions; later frames grow staging on demand.
178 stagingSize := size * 2179 stagingSize := size * 2
180+ r.stagingSize = int(stagingSize)
179 stagingInfo := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: stagingSize, Usage: vk.BufferUsageTransferSrcBit, SharingMode: vk.SharingModeExclusive}181 stagingInfo := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: stagingSize, Usage: vk.BufferUsageTransferSrcBit, SharingMode: vk.SharingModeExclusive}
180 if err = checked("vkCreateBuffer(staging)", r.dd.CreateBuffer(r.device, &stagingInfo, nil, &r.staging)); err != nil {182 if err = checked("vkCreateBuffer(staging)", r.dd.CreateBuffer(r.device, &stagingInfo, nil, &r.staging)); err != nil {
181 return183 return
@@ -258,54 +260,122 @@ func windowColor(id ports.WindowID) [3]uint8 {
258 return [3]uint8{uint8(a * 255), uint8(b * 255), uint8(c * 255)}260 return [3]uint8{uint8(a * 255), uint8(b * 255), uint8(c * 255)}
259 }261 }
260 262
263+// ensureStaging grows the mapped transfer buffer before recording any commands.
264+func (r *Renderer) ensureStaging(size int) error {
265+ if size <= r.stagingSize {
266+ return nil
267+ }
268+ d := r.dd
269+ capacity := size
270+ info := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: vk.DeviceSize(capacity), Usage: vk.BufferUsageTransferSrcBit, SharingMode: vk.SharingModeExclusive}
271+ var buffer vk.Buffer
272+ if err := checked("vkCreateBuffer(staging)", d.CreateBuffer(r.device, &info, nil, &buffer)); err != nil {
273+ return err
274+ }
275+ var memory vk.DeviceMemory
276+ defer func() {
277+ if buffer != 0 {
278+ d.DestroyBuffer(r.device, buffer, nil)
279+ }
280+ if memory != 0 {
281+ d.FreeMemory(r.device, memory, nil)
282+ }
283+ }()
284+ var req vk.MemoryRequirements
285+ d.GetBufferMemoryRequirements(r.device, buffer, &req)
286+ kind, err := r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit)
287+ if err != nil {
288+ return err
289+ }
290+ alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind}
291+ if err := checked("vkAllocateMemory(staging)", d.AllocateMemory(r.device, &alloc, nil, &memory)); err != nil {
292+ return err
293+ }
294+ if err := checked("vkBindBufferMemory(staging)", d.BindBufferMemory(r.device, buffer, memory, 0)); err != nil {
295+ return err
296+ }
297+ var mapped unsafe.Pointer
298+ if err := checked("vkMapMemory(staging)", d.MapMemory(r.device, memory, 0, vk.DeviceSize(capacity), 0, &mapped)); err != nil {
299+ return err
300+ }
301+ d.UnmapMemory(r.device, r.stagingMemory)
302+ d.DestroyBuffer(r.device, r.staging, nil)
303+ d.FreeMemory(r.device, r.stagingMemory, nil)
304+ r.staging, r.stagingMemory, r.stagingMapped, r.stagingSize = buffer, memory, mapped, capacity
305+ buffer, memory = 0, 0
306+ return nil
307+}
308+
261 func (r *Renderer) Clear(rgb [3]uint8) error {309 func (r *Renderer) Clear(rgb [3]uint8) error {
262- return r.Render(ports.Scene{Background: fmt.Sprintf("#%02x%02x%02x", rgb[0], rgb[1], rgb[2])})310+ return r.Render(ports.Scene{Background: fmt.Sprintf("#%02x%02x%02x", rgb[0], rgb[1], rgb[2])}, nil)
263 }311 }
264 312
265-func (r *Renderer) Render(s ports.Scene) error {313+func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) error {
266- // Stage before recording: a full buffer is an error, not a partial submission.314+ type upload struct {
267- type fill struct {315+ rect image.Rectangle
268- rect image.Rectangle316+ offset int
269- color [3]uint8317+ pixels []byte
270- offset int318+ stride, sourceX, sourceY int
271- }319+ color [3]uint8
272- var fills []fill320+ }
321+ var uploads []upload
273 used := 0322 used := 0
274- add := func(rect image.Rectangle, c [3]uint8) error {323+ bounds := image.Rect(0, 0, r.width, r.height)
275- rect = rect.Intersect(image.Rect(0, 0, r.width, r.height))324+ add := func(rect image.Rectangle, c [3]uint8, content *ports.SurfaceContent, origin image.Point) {
325+ rect = rect.Intersect(bounds)
276 if rect.Empty() {326 if rect.Empty() {
277- return nil327+ return
278 }328 }
279 n := rect.Dx() * rect.Dy() * 4329 n := rect.Dx() * rect.Dy() * 4
280- if n > r.width*r.height*8-used {330+ u := upload{rect: rect, offset: used, color: c}
281- return fmt.Errorf("staging buffer full")331+ if content != nil {
332+ u.pixels, u.stride = content.Pixels, content.Stride
333+ u.sourceX, u.sourceY = rect.Min.X-origin.X, rect.Min.Y-origin.Y
282 }334 }
283- data := unsafe.Slice((*byte)(r.stagingMapped), r.width*r.height*8)[used : used+n]335+ uploads = append(uploads, u)
284- for i := 0; i < n; i += 4 {
285- data[i], data[i+1], data[i+2], data[i+3] = c[2], c[1], c[0], 255
286- }
287- fills = append(fills, fill{rect, c, used})
288 used += n336 used += n
289- return nil
290 }337 }
291 for _, w := range s.Windows {338 for _, w := range s.Windows {
292 if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 {339 if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 {
293 continue340 continue
294 }341 }
295 x, y := w.Rect.X, w.Rect.Y342 x, y := w.Rect.X, w.Rect.Y
296- // Clip before converting coordinates to Vulkan's signed 32-bit offsets.
297 body := image.Rect(x, y, x+w.Rect.W, y+w.Rect.H)343 body := image.Rect(x, y, x+w.Rect.W, y+w.Rect.H)
298- if err := add(body, windowColor(w.ID)); err != nil {344+ content := contents[w.ID]
299- return err345+ if content.Pixels == nil {
346+ add(body, windowColor(w.ID), nil, image.Point{})
347+ } else {
348+ add(body, parseColor(s.Background), nil, image.Point{})
349+ width, height := min(content.Width, w.Rect.W), min(content.Height, w.Rect.H)
350+ if width > 0 && height > 0 && content.Stride >= width*4 && len(content.Pixels) >= (height-1)*content.Stride+width*4 {
351+ add(image.Rect(x, y, x+width, y+height), [3]uint8{}, &content, image.Pt(x, y))
352+ }
300 }353 }
301 if w.Focused {354 if w.Focused {
302 for _, strip := range []image.Rectangle{image.Rect(x, y, x+w.Rect.W, y+4), image.Rect(x, y+w.Rect.H-4, x+w.Rect.W, y+w.Rect.H), image.Rect(x, y+4, x+4, y+w.Rect.H-4), image.Rect(x+w.Rect.W-4, y+4, x+w.Rect.W, y+w.Rect.H-4)} {355 for _, strip := range []image.Rectangle{image.Rect(x, y, x+w.Rect.W, y+4), image.Rect(x, y+w.Rect.H-4, x+w.Rect.W, y+w.Rect.H), image.Rect(x, y+4, x+4, y+w.Rect.H-4), image.Rect(x+w.Rect.W-4, y+4, x+w.Rect.W, y+w.Rect.H-4)} {
303- if err := add(strip, [3]uint8{255, 255, 255}); err != nil {356+ add(strip, [3]uint8{255, 255, 255}, nil, image.Point{})
304- return err357+ }
358+ }
359+ }
360+ if err := r.ensureStaging(used); err != nil {
361+ return err
362+ }
363+ data := unsafe.Slice((*byte)(r.stagingMapped), r.stagingSize)
364+ for _, u := range uploads {
365+ dst := data[u.offset : u.offset+u.rect.Dx()*u.rect.Dy()*4]
366+ for row := 0; row < u.rect.Dy(); row++ {
367+ line := dst[row*u.rect.Dx()*4 : (row+1)*u.rect.Dx()*4]
368+ if u.pixels != nil {
369+ start := (u.sourceY+row)*u.stride + u.sourceX*4
370+ copy(line, u.pixels[start:start+len(line)])
371+ } else {
372+ for i := 0; i < len(line); i += 4 {
373+ line[i], line[i+1], line[i+2], line[i+3] = u.color[2], u.color[1], u.color[0], 255
305 }374 }
306 }375 }
307 }376 }
308 }377 }
378+ // B8G8R8A8 pixels are copied unchanged; blending belongs to the compositing pipeline.
309 rgb := parseColor(s.Background)379 rgb := parseColor(s.Background)
310 d := r.dd380 d := r.dd
311 if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(r.command, 0)); err != nil {381 if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(r.command, 0)); err != nil {
@@ -333,7 +403,7 @@ func (r *Renderer) Render(s ports.Scene) error {
333 barrier.OldLayout = vk.ImageLayoutTransferDstOptimal403 barrier.OldLayout = vk.ImageLayoutTransferDstOptimal
334 barrier.NewLayout = vk.ImageLayoutTransferDstOptimal404 barrier.NewLayout = vk.ImageLayoutTransferDstOptimal
335 d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)405 d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)
336- for _, f := range fills {406+ for _, f := range uploads {
337 rect := f.rect407 rect := f.rect
338 region := vk.BufferImageCopy{BufferOffset: vk.DeviceSize(f.offset), ImageSubresource: vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}, ImageOffset: vk.Offset3D{X: int32(rect.Min.X), Y: int32(rect.Min.Y)}, ImageExtent: vk.Extent3D{Width: uint32(rect.Dx()), Height: uint32(rect.Dy()), Depth: 1}}408 region := vk.BufferImageCopy{BufferOffset: vk.DeviceSize(f.offset), ImageSubresource: vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}, ImageOffset: vk.Offset3D{X: int32(rect.Min.X), Y: int32(rect.Min.Y)}, ImageExtent: vk.Extent3D{Width: uint32(rect.Dx()), Height: uint32(rect.Dy()), Depth: 1}}
339 d.CmdCopyBufferToImage(r.command, r.staging, r.image, vk.ImageLayoutTransferDstOptimal, 1, &region)409 d.CmdCopyBufferToImage(r.command, r.staging, r.image, vk.ImageLayoutTransferDstOptimal, 1, &region)
M internal/adapters/vulkan/renderer_test.go +44 -1
@@ -45,7 +45,7 @@ func TestRendererRender(t *testing.T) {
4545 bg := color.RGBA{16, 32, 48, 255}
4646 check := func(s ports.Scene, expected map[image.Point]color.RGBA) {
4747 t.Helper()
48- if err := r.Render(s); err != nil {
48+ if err := r.Render(s, nil); err != nil {
4949 t.Fatal(err)
5050 }
5151 for p, want := range expected {
@@ -67,3 +67,46 @@ func TestRendererRender(t *testing.T) {
6767 s.Windows[0].Hidden = true
6868 check(s, map[image.Point]color.RGBA{{5, 5}: bg})
6969 }
70+
71+func TestRendererContents(t *testing.T) {
72+ r, err := New(64, 48)
73+ if err != nil {
74+ t.Skipf("Vulkan unavailable: %v", err)
75+ }
76+ defer r.Close()
77+ bg := color.RGBA{16, 32, 48, 255}
78+ scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 8, Y: 8, W: 20, H: 10}}}}
79+ pixels := make([]byte, 4*2*4)
80+ for i := 0; i < len(pixels); i += 4 {
81+ copy(pixels[i:i+4], []byte{0, 0, 255, 255})
82+ }
83+ render := func(c *ports.SurfaceContent) {
84+ t.Helper()
85+ var contents map[ports.WindowID]ports.SurfaceContent
86+ if c != nil {
87+ contents = map[ports.WindowID]ports.SurfaceContent{1: *c}
88+ }
89+ if err := r.Render(scene, contents); err != nil {
90+ t.Fatal(err)
91+ }
92+ }
93+ check := func(x, y int, want color.RGBA) {
94+ t.Helper()
95+ if got := r.Pixels().At(x, y); got != want {
96+ t.Errorf("At(%d,%d)=%v want %v", x, y, got, want)
97+ }
98+ }
99+ render(&ports.SurfaceContent{Width: 4, Height: 2, Stride: 16, Pixels: pixels})
100+ check(8, 8, color.RGBA{255, 0, 0, 255})
101+ check(11, 9, color.RGBA{255, 0, 0, 255})
102+ check(12, 8, bg)
103+ check(0, 0, bg)
104+ scene.Windows[0].Rect = ports.Rect{X: -2, Y: 0, W: 10, H: 10}
105+ padded := make([]byte, 4*20)
106+ copy(padded[8:12], []byte{7, 11, 23, 255})
107+ render(&ports.SurfaceContent{Width: 4, Height: 4, Stride: 20, Pixels: padded})
108+ check(0, 0, color.RGBA{23, 11, 7, 255})
109+ render(nil)
110+ c := windowColor(1)
111+ check(0, 0, color.RGBA{c[0], c[1], c[2], 255})
112+}
@@ -45,7 +45,7 @@ func TestRendererRender(t *testing.T) {
45 bg := color.RGBA{16, 32, 48, 255}45 bg := color.RGBA{16, 32, 48, 255}
46 check := func(s ports.Scene, expected map[image.Point]color.RGBA) {46 check := func(s ports.Scene, expected map[image.Point]color.RGBA) {
47 t.Helper()47 t.Helper()
48- if err := r.Render(s); err != nil {48+ if err := r.Render(s, nil); err != nil {
49 t.Fatal(err)49 t.Fatal(err)
50 }50 }
51 for p, want := range expected {51 for p, want := range expected {
@@ -67,3 +67,46 @@ func TestRendererRender(t *testing.T) {
67 s.Windows[0].Hidden = true67 s.Windows[0].Hidden = true
68 check(s, map[image.Point]color.RGBA{{5, 5}: bg})68 check(s, map[image.Point]color.RGBA{{5, 5}: bg})
69 }69 }
70+
71+func TestRendererContents(t *testing.T) {
72+ r, err := New(64, 48)
73+ if err != nil {
74+ t.Skipf("Vulkan unavailable: %v", err)
75+ }
76+ defer r.Close()
77+ bg := color.RGBA{16, 32, 48, 255}
78+ scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 8, Y: 8, W: 20, H: 10}}}}
79+ pixels := make([]byte, 4*2*4)
80+ for i := 0; i < len(pixels); i += 4 {
81+ copy(pixels[i:i+4], []byte{0, 0, 255, 255})
82+ }
83+ render := func(c *ports.SurfaceContent) {
84+ t.Helper()
85+ var contents map[ports.WindowID]ports.SurfaceContent
86+ if c != nil {
87+ contents = map[ports.WindowID]ports.SurfaceContent{1: *c}
88+ }
89+ if err := r.Render(scene, contents); err != nil {
90+ t.Fatal(err)
91+ }
92+ }
93+ check := func(x, y int, want color.RGBA) {
94+ t.Helper()
95+ if got := r.Pixels().At(x, y); got != want {
96+ t.Errorf("At(%d,%d)=%v want %v", x, y, got, want)
97+ }
98+ }
99+ render(&ports.SurfaceContent{Width: 4, Height: 2, Stride: 16, Pixels: pixels})
100+ check(8, 8, color.RGBA{255, 0, 0, 255})
101+ check(11, 9, color.RGBA{255, 0, 0, 255})
102+ check(12, 8, bg)
103+ check(0, 0, bg)
104+ scene.Windows[0].Rect = ports.Rect{X: -2, Y: 0, W: 10, H: 10}
105+ padded := make([]byte, 4*20)
106+ copy(padded[8:12], []byte{7, 11, 23, 255})
107+ render(&ports.SurfaceContent{Width: 4, Height: 4, Stride: 20, Pixels: padded})
108+ check(0, 0, color.RGBA{23, 11, 7, 255})
109+ render(nil)
110+ c := windowColor(1)
111+ check(0, 0, color.RGBA{c[0], c[1], c[2], 255})
112+}

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 23

Tags 10

↑↓ moveenter openesc close