fix(render): order overlapping uploads, force xrgb alpha, accept unpadded last rows
- commit
- 32f342a8928b65329dd0a71f753be6864c346480
- author
- brice · · 2026-09-23 15:46 UTC
- parent
- 18a2776
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
6 files · +131 -20
M internal/adapters/headless/output.go +3 -15
| @@ -95,7 +95,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 95 | 95 | if err := writePNG(filepath.Join(opts.ScreenshotDir, fmt.Sprintf("frame-%06d.png", frame)), r.Pixels()); err != nil { | |
| 96 | 96 | return fmt.Errorf("screenshot: %w", err) | |
| 97 | 97 | } | |
| 98 | - | if err := writeLatest(opts.ScreenshotDir, r.Pixels()); err != nil { | |
| 98 | + | if err := writePNG(filepath.Join(opts.ScreenshotDir, "latest.png"), r.Pixels()); err != nil { | |
| 99 | 99 | return fmt.Errorf("latest screenshot: %w", err) | |
| 100 | 100 | } | |
| 101 | 101 | } | |
| @@ -104,19 +104,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 104 | 104 | } | |
| 105 | 105 | ||
| 106 | 106 | func writePNG(path string, img *image.RGBA) error { | |
| 107 | - | f, err := os.Create(path) | |
| 108 | - | if err != nil { | |
| 109 | - | return err | |
| 110 | - | } | |
| 111 | - | if err := png.Encode(f, img); err != nil { | |
| 112 | - | _ = f.Close() | |
| 113 | - | return err | |
| 114 | - | } | |
| 115 | - | return f.Close() | |
| 116 | - | } | |
| 117 | - | ||
| 118 | - | func writeLatest(dir string, img *image.RGBA) error { | |
| 119 | - | f, err := os.CreateTemp(dir, ".latest-*.png") | |
| 107 | + | f, err := os.CreateTemp(filepath.Dir(path), ".frame-*.png") | |
| 120 | 108 | if err != nil { | |
| 121 | 109 | return err | |
| 122 | 110 | } | |
| @@ -128,5 +116,5 @@ func writeLatest(dir string, img *image.RGBA) error { | |||
| 128 | 116 | if err := f.Close(); err != nil { | |
| 129 | 117 | return err | |
| 130 | 118 | } | |
| 131 | - | return os.Rename(f.Name(), filepath.Join(dir, "latest.png")) | |
| 119 | + | return os.Rename(f.Name(), path) | |
| 132 | 120 | } | |
| @@ -95,7 +95,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 95 | if err := writePNG(filepath.Join(opts.ScreenshotDir, fmt.Sprintf("frame-%06d.png", frame)), r.Pixels()); err != nil { | 95 | if err := writePNG(filepath.Join(opts.ScreenshotDir, fmt.Sprintf("frame-%06d.png", frame)), r.Pixels()); err != nil { | ||
| 96 | return fmt.Errorf("screenshot: %w", err) | 96 | return fmt.Errorf("screenshot: %w", err) | ||
| 97 | } | 97 | } | ||
| 98 | - | if err := writeLatest(opts.ScreenshotDir, r.Pixels()); err != nil { | 98 | + | if err := writePNG(filepath.Join(opts.ScreenshotDir, "latest.png"), r.Pixels()); err != nil { |
| 99 | return fmt.Errorf("latest screenshot: %w", err) | 99 | return fmt.Errorf("latest screenshot: %w", err) | ||
| 100 | } | 100 | } | ||
| 101 | } | 101 | } | ||
| @@ -104,19 +104,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 104 | } | 104 | } | ||
| 105 | 105 | ||||
| 106 | func writePNG(path string, img *image.RGBA) error { | 106 | func writePNG(path string, img *image.RGBA) error { | ||
| 107 | - | f, err := os.Create(path) | 107 | + | f, err := os.CreateTemp(filepath.Dir(path), ".frame-*.png") |
| 108 | - | if err != nil { | |||
| 109 | - | return err | |||
| 110 | - | } | |||
| 111 | - | if err := png.Encode(f, img); err != nil { | |||
| 112 | - | _ = f.Close() | |||
| 113 | - | return err | |||
| 114 | - | } | |||
| 115 | - | return f.Close() | |||
| 116 | - | } | |||
| 117 | - | ||||
| 118 | - | func writeLatest(dir string, img *image.RGBA) error { | |||
| 119 | - | f, err := os.CreateTemp(dir, ".latest-*.png") | |||
| 120 | if err != nil { | 108 | if err != nil { | ||
| 121 | return err | 109 | return err | ||
| 122 | } | 110 | } | ||
| @@ -128,5 +116,5 @@ func writeLatest(dir string, img *image.RGBA) error { | |||||
| 128 | if err := f.Close(); err != nil { | 116 | if err := f.Close(); err != nil { | ||
| 129 | return err | 117 | return err | ||
| 130 | } | 118 | } | ||
| 131 | - | return os.Rename(f.Name(), filepath.Join(dir, "latest.png")) | 119 | + | return os.Rename(f.Name(), path) |
| 132 | } | 120 | } | ||
M internal/adapters/headless/output_test.go +26 -0
| @@ -115,3 +115,29 @@ func TestErrors(t *testing.T) { | |||
| 115 | 115 | t.Fatal("not closed") | |
| 116 | 116 | } | |
| 117 | 117 | } | |
| 118 | + | ||
| 119 | + | func TestWritePNGAtomic(t *testing.T) { | |
| 120 | + | dir := t.TempDir() | |
| 121 | + | img := image.NewRGBA(image.Rect(0, 0, 1, 1)) | |
| 122 | + | for _, name := range []string{"frame-000001.png", "latest.png"} { | |
| 123 | + | path := filepath.Join(dir, name) | |
| 124 | + | if err := writePNG(path, img); err != nil { | |
| 125 | + | t.Fatal(err) | |
| 126 | + | } | |
| 127 | + | f, err := os.Open(path) | |
| 128 | + | if err != nil { | |
| 129 | + | t.Fatal(err) | |
| 130 | + | } | |
| 131 | + | if _, err := png.Decode(f); err != nil { | |
| 132 | + | t.Fatal(err) | |
| 133 | + | } | |
| 134 | + | _ = f.Close() | |
| 135 | + | } | |
| 136 | + | entries, err := os.ReadDir(dir) | |
| 137 | + | if err != nil { | |
| 138 | + | t.Fatal(err) | |
| 139 | + | } | |
| 140 | + | if len(entries) != 2 { | |
| 141 | + | t.Fatalf("unexpected files: %v", entries) | |
| 142 | + | } | |
| 143 | + | } | |
| @@ -115,3 +115,29 @@ func TestErrors(t *testing.T) { | |||||
| 115 | t.Fatal("not closed") | 115 | t.Fatal("not closed") | ||
| 116 | } | 116 | } | ||
| 117 | } | 117 | } | ||
| 118 | + | ||||
| 119 | + | func TestWritePNGAtomic(t *testing.T) { | |||
| 120 | + | dir := t.TempDir() | |||
| 121 | + | img := image.NewRGBA(image.Rect(0, 0, 1, 1)) | |||
| 122 | + | for _, name := range []string{"frame-000001.png", "latest.png"} { | |||
| 123 | + | path := filepath.Join(dir, name) | |||
| 124 | + | if err := writePNG(path, img); err != nil { | |||
| 125 | + | t.Fatal(err) | |||
| 126 | + | } | |||
| 127 | + | f, err := os.Open(path) | |||
| 128 | + | if err != nil { | |||
| 129 | + | t.Fatal(err) | |||
| 130 | + | } | |||
| 131 | + | if _, err := png.Decode(f); err != nil { | |||
| 132 | + | t.Fatal(err) | |||
| 133 | + | } | |||
| 134 | + | _ = f.Close() | |||
| 135 | + | } | |||
| 136 | + | entries, err := os.ReadDir(dir) | |||
| 137 | + | if err != nil { | |||
| 138 | + | t.Fatal(err) | |||
| 139 | + | } | |||
| 140 | + | if len(entries) != 2 { | |||
| 141 | + | t.Fatalf("unexpected files: %v", entries) | |||
| 142 | + | } | |||
| 143 | + | } | |||
M internal/adapters/vulkan/renderer.go +11 -2
| @@ -317,6 +317,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 317 | 317 | pixels []byte | |
| 318 | 318 | stride, sourceX, sourceY int | |
| 319 | 319 | color [3]uint8 | |
| 320 | + | opaque bool | |
| 320 | 321 | } | |
| 321 | 322 | var uploads []upload | |
| 322 | 323 | used := 0 | |
| @@ -329,7 +330,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 329 | 330 | n := rect.Dx() * rect.Dy() * 4 | |
| 330 | 331 | u := upload{rect: rect, offset: used, color: c} | |
| 331 | 332 | if content != nil { | |
| 332 | - | u.pixels, u.stride = content.Pixels, content.Stride | |
| 333 | + | u.pixels, u.stride, u.opaque = content.Pixels, content.Stride, content.Opaque | |
| 333 | 334 | u.sourceX, u.sourceY = rect.Min.X-origin.X, rect.Min.Y-origin.Y | |
| 334 | 335 | } | |
| 335 | 336 | uploads = append(uploads, u) | |
| @@ -368,6 +369,11 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 368 | 369 | if u.pixels != nil { | |
| 369 | 370 | start := (u.sourceY+row)*u.stride + u.sourceX*4 | |
| 370 | 371 | copy(line, u.pixels[start:start+len(line)]) | |
| 372 | + | if u.opaque { | |
| 373 | + | for i := 3; i < len(line); i += 4 { | |
| 374 | + | line[i] = 255 | |
| 375 | + | } | |
| 376 | + | } | |
| 371 | 377 | } else { | |
| 372 | 378 | for i := 0; i < len(line); i += 4 { | |
| 373 | 379 | line[i], line[i+1], line[i+2], line[i+3] = u.color[2], u.color[1], u.color[0], 255 | |
| @@ -403,7 +409,10 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 403 | 409 | barrier.OldLayout = vk.ImageLayoutTransferDstOptimal | |
| 404 | 410 | barrier.NewLayout = vk.ImageLayoutTransferDstOptimal | |
| 405 | 411 | d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier) | |
| 406 | - | for _, f := range uploads { | |
| 412 | + | for i, f := range uploads { | |
| 413 | + | if i > 0 { | |
| 414 | + | d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier) | |
| 415 | + | } | |
| 407 | 416 | rect := f.rect | |
| 408 | 417 | 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}} | |
| 409 | 418 | d.CmdCopyBufferToImage(r.command, r.staging, r.image, vk.ImageLayoutTransferDstOptimal, 1, ®ion) | |
| @@ -317,6 +317,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 317 | pixels []byte | 317 | pixels []byte | ||
| 318 | stride, sourceX, sourceY int | 318 | stride, sourceX, sourceY int | ||
| 319 | color [3]uint8 | 319 | color [3]uint8 | ||
| 320 | + | opaque bool | |||
| 320 | } | 321 | } | ||
| 321 | var uploads []upload | 322 | var uploads []upload | ||
| 322 | used := 0 | 323 | used := 0 | ||
| @@ -329,7 +330,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 329 | n := rect.Dx() * rect.Dy() * 4 | 330 | n := rect.Dx() * rect.Dy() * 4 | ||
| 330 | u := upload{rect: rect, offset: used, color: c} | 331 | u := upload{rect: rect, offset: used, color: c} | ||
| 331 | if content != nil { | 332 | if content != nil { | ||
| 332 | - | u.pixels, u.stride = content.Pixels, content.Stride | 333 | + | u.pixels, u.stride, u.opaque = content.Pixels, content.Stride, content.Opaque |
| 333 | u.sourceX, u.sourceY = rect.Min.X-origin.X, rect.Min.Y-origin.Y | 334 | u.sourceX, u.sourceY = rect.Min.X-origin.X, rect.Min.Y-origin.Y | ||
| 334 | } | 335 | } | ||
| 335 | uploads = append(uploads, u) | 336 | uploads = append(uploads, u) | ||
| @@ -368,6 +369,11 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 368 | if u.pixels != nil { | 369 | if u.pixels != nil { | ||
| 369 | start := (u.sourceY+row)*u.stride + u.sourceX*4 | 370 | start := (u.sourceY+row)*u.stride + u.sourceX*4 | ||
| 370 | copy(line, u.pixels[start:start+len(line)]) | 371 | copy(line, u.pixels[start:start+len(line)]) | ||
| 372 | + | if u.opaque { | |||
| 373 | + | for i := 3; i < len(line); i += 4 { | |||
| 374 | + | line[i] = 255 | |||
| 375 | + | } | |||
| 376 | + | } | |||
| 371 | } else { | 377 | } else { | ||
| 372 | for i := 0; i < len(line); i += 4 { | 378 | 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 | 379 | line[i], line[i+1], line[i+2], line[i+3] = u.color[2], u.color[1], u.color[0], 255 | ||
| @@ -403,7 +409,10 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 403 | barrier.OldLayout = vk.ImageLayoutTransferDstOptimal | 409 | barrier.OldLayout = vk.ImageLayoutTransferDstOptimal | ||
| 404 | barrier.NewLayout = vk.ImageLayoutTransferDstOptimal | 410 | barrier.NewLayout = vk.ImageLayoutTransferDstOptimal | ||
| 405 | d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier) | 411 | d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier) | ||
| 406 | - | for _, f := range uploads { | 412 | + | for i, f := range uploads { |
| 413 | + | if i > 0 { | |||
| 414 | + | d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier) | |||
| 415 | + | } | |||
| 407 | rect := f.rect | 416 | rect := f.rect | ||
| 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}} | 417 | 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}} | ||
| 409 | d.CmdCopyBufferToImage(r.command, r.staging, r.image, vk.ImageLayoutTransferDstOptimal, 1, ®ion) | 418 | d.CmdCopyBufferToImage(r.command, r.staging, r.image, vk.ImageLayoutTransferDstOptimal, 1, ®ion) | ||
M internal/adapters/vulkan/renderer_test.go +35 -0
| @@ -110,3 +110,38 @@ func TestRendererContents(t *testing.T) { | |||
| 110 | 110 | c := windowColor(1) | |
| 111 | 111 | check(0, 0, color.RGBA{c[0], c[1], c[2], 255}) | |
| 112 | 112 | } | |
| 113 | + | ||
| 114 | + | func TestRendererUploadOrderAndOpaque(t *testing.T) { | |
| 115 | + | r, err := New(32, 32) | |
| 116 | + | if err != nil { | |
| 117 | + | t.Skipf("Vulkan unavailable: %v", err) | |
| 118 | + | } | |
| 119 | + | defer r.Close() | |
| 120 | + | scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{ | |
| 121 | + | {ID: 1, Rect: ports.Rect{X: 2, Y: 2, W: 16, H: 16}}, | |
| 122 | + | {ID: 2, Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 16}}, | |
| 123 | + | }} | |
| 124 | + | if err := r.Render(scene, nil); err != nil { | |
| 125 | + | t.Fatal(err) | |
| 126 | + | } | |
| 127 | + | c := windowColor(2) | |
| 128 | + | if got, want := r.Pixels().RGBAAt(10, 10), (color.RGBA{c[0], c[1], c[2], 255}); got != want { | |
| 129 | + | t.Errorf("overlap = %v, want %v", got, want) | |
| 130 | + | } | |
| 131 | + | scene.Windows = []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 2, Y: 2, W: 16, H: 16}, Focused: true}} | |
| 132 | + | contents := map[ports.WindowID]ports.SurfaceContent{1: {Width: 16, Height: 16, Stride: 64, Opaque: true, Pixels: make([]byte, 16*16*4)}} | |
| 133 | + | for i := 0; i < len(contents[1].Pixels); i += 4 { | |
| 134 | + | copy(contents[1].Pixels[i:i+4], []byte{3, 5, 7, 0}) | |
| 135 | + | } | |
| 136 | + | if err := r.Render(scene, contents); err != nil { | |
| 137 | + | t.Fatal(err) | |
| 138 | + | } | |
| 139 | + | for _, tc := range []struct { | |
| 140 | + | x, y int | |
| 141 | + | want color.RGBA | |
| 142 | + | }{{3, 3, color.RGBA{255, 255, 255, 255}}, {10, 10, color.RGBA{7, 5, 3, 255}}} { | |
| 143 | + | if got := r.Pixels().RGBAAt(tc.x, tc.y); got != tc.want { | |
| 144 | + | t.Errorf("pixel (%d,%d) = %v, want %v", tc.x, tc.y, got, tc.want) | |
| 145 | + | } | |
| 146 | + | } | |
| 147 | + | } | |
| @@ -110,3 +110,38 @@ func TestRendererContents(t *testing.T) { | |||||
| 110 | c := windowColor(1) | 110 | c := windowColor(1) | ||
| 111 | check(0, 0, color.RGBA{c[0], c[1], c[2], 255}) | 111 | check(0, 0, color.RGBA{c[0], c[1], c[2], 255}) | ||
| 112 | } | 112 | } | ||
| 113 | + | ||||
| 114 | + | func TestRendererUploadOrderAndOpaque(t *testing.T) { | |||
| 115 | + | r, err := New(32, 32) | |||
| 116 | + | if err != nil { | |||
| 117 | + | t.Skipf("Vulkan unavailable: %v", err) | |||
| 118 | + | } | |||
| 119 | + | defer r.Close() | |||
| 120 | + | scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{ | |||
| 121 | + | {ID: 1, Rect: ports.Rect{X: 2, Y: 2, W: 16, H: 16}}, | |||
| 122 | + | {ID: 2, Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 16}}, | |||
| 123 | + | }} | |||
| 124 | + | if err := r.Render(scene, nil); err != nil { | |||
| 125 | + | t.Fatal(err) | |||
| 126 | + | } | |||
| 127 | + | c := windowColor(2) | |||
| 128 | + | if got, want := r.Pixels().RGBAAt(10, 10), (color.RGBA{c[0], c[1], c[2], 255}); got != want { | |||
| 129 | + | t.Errorf("overlap = %v, want %v", got, want) | |||
| 130 | + | } | |||
| 131 | + | scene.Windows = []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 2, Y: 2, W: 16, H: 16}, Focused: true}} | |||
| 132 | + | contents := map[ports.WindowID]ports.SurfaceContent{1: {Width: 16, Height: 16, Stride: 64, Opaque: true, Pixels: make([]byte, 16*16*4)}} | |||
| 133 | + | for i := 0; i < len(contents[1].Pixels); i += 4 { | |||
| 134 | + | copy(contents[1].Pixels[i:i+4], []byte{3, 5, 7, 0}) | |||
| 135 | + | } | |||
| 136 | + | if err := r.Render(scene, contents); err != nil { | |||
| 137 | + | t.Fatal(err) | |||
| 138 | + | } | |||
| 139 | + | for _, tc := range []struct { | |||
| 140 | + | x, y int | |||
| 141 | + | want color.RGBA | |||
| 142 | + | }{{3, 3, color.RGBA{255, 255, 255, 255}}, {10, 10, color.RGBA{7, 5, 3, 255}}} { | |||
| 143 | + | if got := r.Pixels().RGBAAt(tc.x, tc.y); got != tc.want { | |||
| 144 | + | t.Errorf("pixel (%d,%d) = %v, want %v", tc.x, tc.y, got, tc.want) | |||
| 145 | + | } | |||
| 146 | + | } | |||
| 147 | + | } | |||
M internal/adapters/wayland/globals.go +3 -3
| @@ -176,7 +176,7 @@ func (p *pool) CreateBuffer(r *wayland.ShmPool, id uint32, offset, width, height | |||
| 176 | 176 | p.shm.PostError(uint32(wayland.ShmErrorInvalidFormat), "unsupported format") | |
| 177 | 177 | return | |
| 178 | 178 | } | |
| 179 | - | if width <= 0 || height <= 0 || int64(stride) < int64(width)*4 || offset < 0 || stride <= 0 || int64(offset)+int64(stride)*int64(height) > int64(p.size) { | |
| 179 | + | if width <= 0 || height <= 0 || int64(stride) < int64(width)*4 || offset < 0 || stride <= 0 || int64(offset)+int64(height-1)*int64(stride)+int64(width)*4 > int64(p.size) { | |
| 180 | 180 | p.shm.PostError(uint32(wayland.ShmErrorInvalidStride), "invalid buffer dimensions") | |
| 181 | 181 | return | |
| 182 | 182 | } | |
| @@ -228,11 +228,11 @@ func (b *buffer) content(id ports.WindowID) (content ports.SurfaceContent, ok bo | |||
| 228 | 228 | ok = false | |
| 229 | 229 | } | |
| 230 | 230 | }() | |
| 231 | - | size := b.stride * b.height | |
| 231 | + | size := (b.height-1)*b.stride + b.width*4 | |
| 232 | 232 | pixels := make([]byte, size) | |
| 233 | 233 | for y := 0; y < b.height; y++ { | |
| 234 | 234 | start := b.offset + y*b.stride | |
| 235 | - | copy(pixels[y*b.stride:(y+1)*b.stride], b.pool.data[start:start+b.stride]) | |
| 235 | + | copy(pixels[y*b.stride:y*b.stride+b.width*4], b.pool.data[start:start+b.width*4]) | |
| 236 | 236 | } | |
| 237 | 237 | return ports.SurfaceContent{ID: id, Width: b.width, Height: b.height, Stride: b.stride, Opaque: b.format == uint32(wayland.ShmFormatXrgb8888), Pixels: pixels}, true | |
| 238 | 238 | } | |
| @@ -176,7 +176,7 @@ func (p *pool) CreateBuffer(r *wayland.ShmPool, id uint32, offset, width, height | |||||
| 176 | p.shm.PostError(uint32(wayland.ShmErrorInvalidFormat), "unsupported format") | 176 | p.shm.PostError(uint32(wayland.ShmErrorInvalidFormat), "unsupported format") | ||
| 177 | return | 177 | return | ||
| 178 | } | 178 | } | ||
| 179 | - | if width <= 0 || height <= 0 || int64(stride) < int64(width)*4 || offset < 0 || stride <= 0 || int64(offset)+int64(stride)*int64(height) > int64(p.size) { | 179 | + | if width <= 0 || height <= 0 || int64(stride) < int64(width)*4 || offset < 0 || stride <= 0 || int64(offset)+int64(height-1)*int64(stride)+int64(width)*4 > int64(p.size) { |
| 180 | p.shm.PostError(uint32(wayland.ShmErrorInvalidStride), "invalid buffer dimensions") | 180 | p.shm.PostError(uint32(wayland.ShmErrorInvalidStride), "invalid buffer dimensions") | ||
| 181 | return | 181 | return | ||
| 182 | } | 182 | } | ||
| @@ -228,11 +228,11 @@ func (b *buffer) content(id ports.WindowID) (content ports.SurfaceContent, ok bo | |||||
| 228 | ok = false | 228 | ok = false | ||
| 229 | } | 229 | } | ||
| 230 | }() | 230 | }() | ||
| 231 | - | size := b.stride * b.height | 231 | + | size := (b.height-1)*b.stride + b.width*4 |
| 232 | pixels := make([]byte, size) | 232 | pixels := make([]byte, size) | ||
| 233 | for y := 0; y < b.height; y++ { | 233 | for y := 0; y < b.height; y++ { | ||
| 234 | start := b.offset + y*b.stride | 234 | start := b.offset + y*b.stride | ||
| 235 | - | copy(pixels[y*b.stride:(y+1)*b.stride], b.pool.data[start:start+b.stride]) | 235 | + | copy(pixels[y*b.stride:y*b.stride+b.width*4], b.pool.data[start:start+b.width*4]) |
| 236 | } | 236 | } | ||
| 237 | return ports.SurfaceContent{ID: id, Width: b.width, Height: b.height, Stride: b.stride, Opaque: b.format == uint32(wayland.ShmFormatXrgb8888), Pixels: pixels}, true | 237 | return ports.SurfaceContent{ID: id, Width: b.width, Height: b.height, Stride: b.stride, Opaque: b.format == uint32(wayland.ShmFormatXrgb8888), Pixels: pixels}, true | ||
| 238 | } | 238 | } | ||
M internal/adapters/wayland/protocol_test.go +53 -0
| @@ -280,3 +280,56 @@ func TestSHMInvalidStride(t *testing.T) { | |||
| 280 | 280 | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, c.AllocateID(), int32(0), int32(2), int32(1), int32(4), uint32(wayland.ShmFormatArgb8888)) | |
| 281 | 281 | expectProtocolError(t, c, shm, uint32(wayland.ShmErrorInvalidStride)) | |
| 282 | 282 | } | |
| 283 | + | ||
| 284 | + | func TestSHMUnpaddedLastRow(t *testing.T) { | |
| 285 | + | for _, tc := range []struct { | |
| 286 | + | name string | |
| 287 | + | size int32 | |
| 288 | + | valid bool | |
| 289 | + | }{{"exact", 28, true}, {"short", 27, false}} { | |
| 290 | + | t.Run(tc.name, func(t *testing.T) { | |
| 291 | + | s, _, _, dir := lifecycleServer(t) | |
| 292 | + | c := protocolClient(t, s, dir) | |
| 293 | + | shm := bindProtocol(t, c, "wl_shm") | |
| 294 | + | fd, err := unix.MemfdCreate("unpadded-last-row", 0) | |
| 295 | + | if err != nil { | |
| 296 | + | t.Fatal(err) | |
| 297 | + | } | |
| 298 | + | defer unix.Close(fd) | |
| 299 | + | if err := unix.Ftruncate(fd, int64(tc.size)); err != nil { | |
| 300 | + | t.Fatal(err) | |
| 301 | + | } | |
| 302 | + | pool := c.AllocateID() | |
| 303 | + | registerProtocol(t, c, shm) | |
| 304 | + | registerProtocol(t, c, pool) | |
| 305 | + | if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, tc.size); err != nil { | |
| 306 | + | t.Fatal(err) | |
| 307 | + | } | |
| 308 | + | buffer := c.AllocateID() | |
| 309 | + | registerProtocol(t, c, buffer) | |
| 310 | + | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(4), int32(2), int32(2), int32(16), uint32(wayland.ShmFormatXrgb8888)) | |
| 311 | + | if !tc.valid { | |
| 312 | + | expectProtocolError(t, c, shm, uint32(wayland.ShmErrorInvalidStride)) | |
| 313 | + | return | |
| 314 | + | } | |
| 315 | + | if err := c.Roundtrip(); err != nil { | |
| 316 | + | t.Fatal(err) | |
| 317 | + | } | |
| 318 | + | }) | |
| 319 | + | } | |
| 320 | + | } | |
| 321 | + | ||
| 322 | + | func TestBufferContentUnpaddedLastRow(t *testing.T) { | |
| 323 | + | p := &pool{data: make([]byte, 28)} | |
| 324 | + | b := &buffer{pool: p, offset: 4, width: 2, height: 2, stride: 16} | |
| 325 | + | copy(p.data[20:], []byte{1, 2, 3, 4, 5, 6, 7, 8}) | |
| 326 | + | content, ok := b.content(1) | |
| 327 | + | if !ok || len(content.Pixels) != 24 { | |
| 328 | + | t.Fatalf("content ok=%v size=%d", ok, len(content.Pixels)) | |
| 329 | + | } | |
| 330 | + | for i, v := range []byte{1, 2, 3, 4, 5, 6, 7, 8} { | |
| 331 | + | if content.Pixels[16+i] != v { | |
| 332 | + | t.Fatalf("last row byte %d = %d, want %d", i, content.Pixels[16+i], v) | |
| 333 | + | } | |
| 334 | + | } | |
| 335 | + | } | |
| @@ -280,3 +280,56 @@ func TestSHMInvalidStride(t *testing.T) { | |||||
| 280 | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, c.AllocateID(), int32(0), int32(2), int32(1), int32(4), uint32(wayland.ShmFormatArgb8888)) | 280 | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, c.AllocateID(), int32(0), int32(2), int32(1), int32(4), uint32(wayland.ShmFormatArgb8888)) | ||
| 281 | expectProtocolError(t, c, shm, uint32(wayland.ShmErrorInvalidStride)) | 281 | expectProtocolError(t, c, shm, uint32(wayland.ShmErrorInvalidStride)) | ||
| 282 | } | 282 | } | ||
| 283 | + | ||||
| 284 | + | func TestSHMUnpaddedLastRow(t *testing.T) { | |||
| 285 | + | for _, tc := range []struct { | |||
| 286 | + | name string | |||
| 287 | + | size int32 | |||
| 288 | + | valid bool | |||
| 289 | + | }{{"exact", 28, true}, {"short", 27, false}} { | |||
| 290 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 291 | + | s, _, _, dir := lifecycleServer(t) | |||
| 292 | + | c := protocolClient(t, s, dir) | |||
| 293 | + | shm := bindProtocol(t, c, "wl_shm") | |||
| 294 | + | fd, err := unix.MemfdCreate("unpadded-last-row", 0) | |||
| 295 | + | if err != nil { | |||
| 296 | + | t.Fatal(err) | |||
| 297 | + | } | |||
| 298 | + | defer unix.Close(fd) | |||
| 299 | + | if err := unix.Ftruncate(fd, int64(tc.size)); err != nil { | |||
| 300 | + | t.Fatal(err) | |||
| 301 | + | } | |||
| 302 | + | pool := c.AllocateID() | |||
| 303 | + | registerProtocol(t, c, shm) | |||
| 304 | + | registerProtocol(t, c, pool) | |||
| 305 | + | if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, tc.size); err != nil { | |||
| 306 | + | t.Fatal(err) | |||
| 307 | + | } | |||
| 308 | + | buffer := c.AllocateID() | |||
| 309 | + | registerProtocol(t, c, buffer) | |||
| 310 | + | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(4), int32(2), int32(2), int32(16), uint32(wayland.ShmFormatXrgb8888)) | |||
| 311 | + | if !tc.valid { | |||
| 312 | + | expectProtocolError(t, c, shm, uint32(wayland.ShmErrorInvalidStride)) | |||
| 313 | + | return | |||
| 314 | + | } | |||
| 315 | + | if err := c.Roundtrip(); err != nil { | |||
| 316 | + | t.Fatal(err) | |||
| 317 | + | } | |||
| 318 | + | }) | |||
| 319 | + | } | |||
| 320 | + | } | |||
| 321 | + | ||||
| 322 | + | func TestBufferContentUnpaddedLastRow(t *testing.T) { | |||
| 323 | + | p := &pool{data: make([]byte, 28)} | |||
| 324 | + | b := &buffer{pool: p, offset: 4, width: 2, height: 2, stride: 16} | |||
| 325 | + | copy(p.data[20:], []byte{1, 2, 3, 4, 5, 6, 7, 8}) | |||
| 326 | + | content, ok := b.content(1) | |||
| 327 | + | if !ok || len(content.Pixels) != 24 { | |||
| 328 | + | t.Fatalf("content ok=%v size=%d", ok, len(content.Pixels)) | |||
| 329 | + | } | |||
| 330 | + | for i, v := range []byte{1, 2, 3, 4, 5, 6, 7, 8} { | |||
| 331 | + | if content.Pixels[16+i] != v { | |||
| 332 | + | t.Fatalf("last row byte %d = %d, want %d", i, content.Pixels[16+i], v) | |||
| 333 | + | } | |||
| 334 | + | } | |||
| 335 | + | } | |||