merge: draw layer surfaces
3 files · +94 -0
M internal/adapters/headless/output_test.go +34 -0
| @@ -141,3 +141,37 @@ func TestWritePNGAtomic(t *testing.T) { | |||
| 141 | 141 | t.Fatalf("unexpected files: %v", entries) | |
| 142 | 142 | } | |
| 143 | 143 | } | |
| 144 | + | ||
| 145 | + | func TestRunPassesLayerContent(t *testing.T) { | |
| 146 | + | scenes := make(chan ports.Scene, 1) | |
| 147 | + | contents := make(chan ports.SurfaceContent, 1) | |
| 148 | + | layer := ports.SceneLayer{ID: 42, Layer: ports.LayerTop, Rect: ports.Rect{W: 2, H: 2}} | |
| 149 | + | scenes <- ports.Scene{Layers: []ports.SceneLayer{layer}} | |
| 150 | + | contents <- ports.SurfaceContent{ID: 42, Width: 1, Height: 1, Stride: 4, Pixels: []byte{0, 0, 255, 255}} | |
| 151 | + | r := new(fakeRenderer) | |
| 152 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 153 | + | done := make(chan error, 1) | |
| 154 | + | go func() { | |
| 155 | + | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (Renderer, error) { return r, nil }}, scenes, contents) | |
| 156 | + | }() | |
| 157 | + | deadline := time.After(3 * time.Second) | |
| 158 | + | for { | |
| 159 | + | r.mu.Lock() | |
| 160 | + | passed := len(r.contents) > 0 && len(r.frames[0].Layers) == 1 && r.contents[0][42].Pixels != nil | |
| 161 | + | r.mu.Unlock() | |
| 162 | + | if passed { | |
| 163 | + | break | |
| 164 | + | } | |
| 165 | + | select { | |
| 166 | + | case <-deadline: | |
| 167 | + | cancel() | |
| 168 | + | <-done | |
| 169 | + | t.Fatal("layer content not passed to renderer") | |
| 170 | + | case <-time.After(10 * time.Millisecond): | |
| 171 | + | } | |
| 172 | + | } | |
| 173 | + | cancel() | |
| 174 | + | if err := <-done; err != nil { | |
| 175 | + | t.Fatal(err) | |
| 176 | + | } | |
| 177 | + | } | |
| @@ -141,3 +141,37 @@ func TestWritePNGAtomic(t *testing.T) { | |||||
| 141 | t.Fatalf("unexpected files: %v", entries) | 141 | t.Fatalf("unexpected files: %v", entries) | ||
| 142 | } | 142 | } | ||
| 143 | } | 143 | } | ||
| 144 | + | ||||
| 145 | + | func TestRunPassesLayerContent(t *testing.T) { | |||
| 146 | + | scenes := make(chan ports.Scene, 1) | |||
| 147 | + | contents := make(chan ports.SurfaceContent, 1) | |||
| 148 | + | layer := ports.SceneLayer{ID: 42, Layer: ports.LayerTop, Rect: ports.Rect{W: 2, H: 2}} | |||
| 149 | + | scenes <- ports.Scene{Layers: []ports.SceneLayer{layer}} | |||
| 150 | + | contents <- ports.SurfaceContent{ID: 42, Width: 1, Height: 1, Stride: 4, Pixels: []byte{0, 0, 255, 255}} | |||
| 151 | + | r := new(fakeRenderer) | |||
| 152 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 153 | + | done := make(chan error, 1) | |||
| 154 | + | go func() { | |||
| 155 | + | done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (Renderer, error) { return r, nil }}, scenes, contents) | |||
| 156 | + | }() | |||
| 157 | + | deadline := time.After(3 * time.Second) | |||
| 158 | + | for { | |||
| 159 | + | r.mu.Lock() | |||
| 160 | + | passed := len(r.contents) > 0 && len(r.frames[0].Layers) == 1 && r.contents[0][42].Pixels != nil | |||
| 161 | + | r.mu.Unlock() | |||
| 162 | + | if passed { | |||
| 163 | + | break | |||
| 164 | + | } | |||
| 165 | + | select { | |||
| 166 | + | case <-deadline: | |||
| 167 | + | cancel() | |||
| 168 | + | <-done | |||
| 169 | + | t.Fatal("layer content not passed to renderer") | |||
| 170 | + | case <-time.After(10 * time.Millisecond): | |||
| 171 | + | } | |||
| 172 | + | } | |||
| 173 | + | cancel() | |||
| 174 | + | if err := <-done; err != nil { | |||
| 175 | + | t.Fatal(err) | |||
| 176 | + | } | |||
| 177 | + | } | |||
M internal/adapters/vulkan/renderer.go +22 -0
| @@ -336,6 +336,27 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 336 | 336 | uploads = append(uploads, u) | |
| 337 | 337 | used += n | |
| 338 | 338 | } | |
| 339 | + | fullscreen := false | |
| 340 | + | for _, w := range s.Windows { | |
| 341 | + | fullscreen = fullscreen || (!w.Hidden && w.Fullscreen) | |
| 342 | + | } | |
| 343 | + | addLayers := func(afterWindows bool) { | |
| 344 | + | for _, layer := range s.Layers { | |
| 345 | + | if (layer.Layer >= ports.LayerTop) != afterWindows || | |
| 346 | + | (fullscreen && (layer.Layer == ports.LayerBottom || layer.Layer == ports.LayerTop)) || | |
| 347 | + | layer.Rect.W <= 0 || layer.Rect.H <= 0 { | |
| 348 | + | continue | |
| 349 | + | } | |
| 350 | + | content := contents[layer.ID] | |
| 351 | + | width, height := min(content.Width, layer.Rect.W), min(content.Height, layer.Rect.H) | |
| 352 | + | if content.Pixels == nil || width <= 0 || height <= 0 || content.Stride < width*4 || len(content.Pixels) < (height-1)*content.Stride+width*4 { | |
| 353 | + | continue | |
| 354 | + | } | |
| 355 | + | x, y := layer.Rect.X, layer.Rect.Y | |
| 356 | + | add(image.Rect(x, y, x+width, y+height), [3]uint8{}, &content, image.Pt(x, y)) | |
| 357 | + | } | |
| 358 | + | } | |
| 359 | + | addLayers(false) | |
| 339 | 360 | for _, w := range s.Windows { | |
| 340 | 361 | if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 { | |
| 341 | 362 | continue | |
| @@ -358,6 +379,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 358 | 379 | } | |
| 359 | 380 | } | |
| 360 | 381 | } | |
| 382 | + | addLayers(true) | |
| 361 | 383 | if err := r.ensureStaging(used); err != nil { | |
| 362 | 384 | return err | |
| 363 | 385 | } | |
| @@ -336,6 +336,27 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 336 | uploads = append(uploads, u) | 336 | uploads = append(uploads, u) | ||
| 337 | used += n | 337 | used += n | ||
| 338 | } | 338 | } | ||
| 339 | + | fullscreen := false | |||
| 340 | + | for _, w := range s.Windows { | |||
| 341 | + | fullscreen = fullscreen || (!w.Hidden && w.Fullscreen) | |||
| 342 | + | } | |||
| 343 | + | addLayers := func(afterWindows bool) { | |||
| 344 | + | for _, layer := range s.Layers { | |||
| 345 | + | if (layer.Layer >= ports.LayerTop) != afterWindows || | |||
| 346 | + | (fullscreen && (layer.Layer == ports.LayerBottom || layer.Layer == ports.LayerTop)) || | |||
| 347 | + | layer.Rect.W <= 0 || layer.Rect.H <= 0 { | |||
| 348 | + | continue | |||
| 349 | + | } | |||
| 350 | + | content := contents[layer.ID] | |||
| 351 | + | width, height := min(content.Width, layer.Rect.W), min(content.Height, layer.Rect.H) | |||
| 352 | + | if content.Pixels == nil || width <= 0 || height <= 0 || content.Stride < width*4 || len(content.Pixels) < (height-1)*content.Stride+width*4 { | |||
| 353 | + | continue | |||
| 354 | + | } | |||
| 355 | + | x, y := layer.Rect.X, layer.Rect.Y | |||
| 356 | + | add(image.Rect(x, y, x+width, y+height), [3]uint8{}, &content, image.Pt(x, y)) | |||
| 357 | + | } | |||
| 358 | + | } | |||
| 359 | + | addLayers(false) | |||
| 339 | for _, w := range s.Windows { | 360 | for _, w := range s.Windows { | ||
| 340 | if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 { | 361 | if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 { | ||
| 341 | continue | 362 | continue | ||
| @@ -358,6 +379,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 358 | } | 379 | } | ||
| 359 | } | 380 | } | ||
| 360 | } | 381 | } | ||
| 382 | + | addLayers(true) | |||
| 361 | if err := r.ensureStaging(used); err != nil { | 383 | if err := r.ensureStaging(used); err != nil { | ||
| 362 | return err | 384 | return err | ||
| 363 | } | 385 | } | ||
M internal/adapters/vulkan/renderer_test.go +38 -0
| @@ -145,3 +145,41 @@ func TestRendererUploadOrderAndOpaque(t *testing.T) { | |||
| 145 | 145 | } | |
| 146 | 146 | } | |
| 147 | 147 | } | |
| 148 | + | ||
| 149 | + | func TestRendererLayers(t *testing.T) { | |
| 150 | + | r, err := New(64, 48) | |
| 151 | + | if err != nil { | |
| 152 | + | t.Skipf("Vulkan unavailable: %v", err) | |
| 153 | + | } | |
| 154 | + | defer r.Close() | |
| 155 | + | red := make([]byte, 64*8*4) | |
| 156 | + | for i := 0; i < len(red); i += 4 { | |
| 157 | + | copy(red[i:i+4], []byte{0, 0, 255, 0}) | |
| 158 | + | } | |
| 159 | + | contents := map[ports.WindowID]ports.SurfaceContent{2: {ID: 2, Width: 64, Height: 8, Stride: 64 * 4, Opaque: true, Pixels: red}} | |
| 160 | + | scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 48}}}, Layers: []ports.SceneLayer{{ID: 2, Layer: ports.LayerTop, Rect: ports.Rect{W: 64, H: 8}}}} | |
| 161 | + | wc := windowColor(1) | |
| 162 | + | window := color.RGBA{wc[0], wc[1], wc[2], 255} | |
| 163 | + | check := func(want color.RGBA) { | |
| 164 | + | t.Helper() | |
| 165 | + | if err := r.Render(scene, contents); err != nil { | |
| 166 | + | t.Fatal(err) | |
| 167 | + | } | |
| 168 | + | if got := r.Pixels().RGBAAt(5, 2); got != want { | |
| 169 | + | t.Errorf("pixel = %v, want %v", got, want) | |
| 170 | + | } | |
| 171 | + | } | |
| 172 | + | check(color.RGBA{255, 0, 0, 255}) | |
| 173 | + | scene.Layers[0].Layer = ports.LayerBottom | |
| 174 | + | check(window) | |
| 175 | + | scene.Windows[0].Fullscreen = true | |
| 176 | + | scene.Layers[0].Layer = ports.LayerTop | |
| 177 | + | check(window) | |
| 178 | + | scene.Layers[0].Layer = ports.LayerOverlay | |
| 179 | + | check(color.RGBA{255, 0, 0, 255}) | |
| 180 | + | scene.Windows[0].Hidden = true | |
| 181 | + | scene.Layers[0].Layer = ports.LayerBottom | |
| 182 | + | check(color.RGBA{255, 0, 0, 255}) | |
| 183 | + | delete(contents, 2) | |
| 184 | + | check(color.RGBA{16, 32, 48, 255}) | |
| 185 | + | } | |
| @@ -145,3 +145,41 @@ func TestRendererUploadOrderAndOpaque(t *testing.T) { | |||||
| 145 | } | 145 | } | ||
| 146 | } | 146 | } | ||
| 147 | } | 147 | } | ||
| 148 | + | ||||
| 149 | + | func TestRendererLayers(t *testing.T) { | |||
| 150 | + | r, err := New(64, 48) | |||
| 151 | + | if err != nil { | |||
| 152 | + | t.Skipf("Vulkan unavailable: %v", err) | |||
| 153 | + | } | |||
| 154 | + | defer r.Close() | |||
| 155 | + | red := make([]byte, 64*8*4) | |||
| 156 | + | for i := 0; i < len(red); i += 4 { | |||
| 157 | + | copy(red[i:i+4], []byte{0, 0, 255, 0}) | |||
| 158 | + | } | |||
| 159 | + | contents := map[ports.WindowID]ports.SurfaceContent{2: {ID: 2, Width: 64, Height: 8, Stride: 64 * 4, Opaque: true, Pixels: red}} | |||
| 160 | + | scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 48}}}, Layers: []ports.SceneLayer{{ID: 2, Layer: ports.LayerTop, Rect: ports.Rect{W: 64, H: 8}}}} | |||
| 161 | + | wc := windowColor(1) | |||
| 162 | + | window := color.RGBA{wc[0], wc[1], wc[2], 255} | |||
| 163 | + | check := func(want color.RGBA) { | |||
| 164 | + | t.Helper() | |||
| 165 | + | if err := r.Render(scene, contents); err != nil { | |||
| 166 | + | t.Fatal(err) | |||
| 167 | + | } | |||
| 168 | + | if got := r.Pixels().RGBAAt(5, 2); got != want { | |||
| 169 | + | t.Errorf("pixel = %v, want %v", got, want) | |||
| 170 | + | } | |||
| 171 | + | } | |||
| 172 | + | check(color.RGBA{255, 0, 0, 255}) | |||
| 173 | + | scene.Layers[0].Layer = ports.LayerBottom | |||
| 174 | + | check(window) | |||
| 175 | + | scene.Windows[0].Fullscreen = true | |||
| 176 | + | scene.Layers[0].Layer = ports.LayerTop | |||
| 177 | + | check(window) | |||
| 178 | + | scene.Layers[0].Layer = ports.LayerOverlay | |||
| 179 | + | check(color.RGBA{255, 0, 0, 255}) | |||
| 180 | + | scene.Windows[0].Hidden = true | |||
| 181 | + | scene.Layers[0].Layer = ports.LayerBottom | |||
| 182 | + | check(color.RGBA{255, 0, 0, 255}) | |||
| 183 | + | delete(contents, 2) | |||
| 184 | + | check(color.RGBA{16, 32, 48, 255}) | |||
| 185 | + | } | |||