From 1e804897c64089af1aae9cea461d2adbb8c73449 Mon Sep 17 00:00:00 2001 From: Dmitry Sergeev Date: Fri, 18 Sep 2026 10:25:35 +0300 Subject: [PATCH] alpha perfomance fix --- internal/app/app.go | 2 +- internal/generator/v210a.go | 14 ++++++- internal/generator/v210a_test.go | 65 +++++++++++++++++++++++++++++--- internal/video/generator.go | 10 +++-- internal/video/generator_test.go | 2 +- 5 files changed, 81 insertions(+), 12 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index a870939..8895333 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -14,7 +14,7 @@ import ( const ( Name = "MXL pattern generator" - Version = "0.1.0" + Version = "0.2.0" ) type Config struct { diff --git a/internal/generator/v210a.go b/internal/generator/v210a.go index 69070f5..060b9ca 100644 --- a/internal/generator/v210a.go +++ b/internal/generator/v210a.go @@ -9,6 +9,7 @@ type V210AGenerator struct { width int height int fillSize int + fillBase []byte alphaBase []byte } @@ -17,6 +18,7 @@ var _ FrameGenerator = (*V210AGenerator)(nil) func NewV210AGenerator( fill FrameGenerator, width, height uint, + fillDynamic bool, ) (*V210AGenerator, error) { if fill == nil { return nil, fmt.Errorf("v210a: fill generator is nil") @@ -38,6 +40,12 @@ func NewV210AGenerator( if err := fillAlphaPlane(g.alphaBase, g.width, g.height, alphaOpaque); err != nil { return nil, fmt.Errorf("v210a: initialize alpha plane: %w", err) } + if !fillDynamic { + g.fillBase = make([]byte, g.fillSize) + if err := fill.GenerateFrame(g.fillBase, 0); err != nil { + return nil, fmt.Errorf("v210a: initialize static fill: %w", err) + } + } return g, nil } @@ -47,8 +55,10 @@ func (g *V210AGenerator) GenerateFrame(dst []byte, frameIndex int) error { return fmt.Errorf("v210a: destination is too small: got %d bytes, need %d", len(dst), need) } - if err := g.fill.GenerateFrame(dst[:g.fillSize], frameIndex); err != nil { - return fmt.Errorf("v210a: generate fill frame %d: %w", frameIndex, err) + if g.fillBase != nil { + copy(dst[:g.fillSize], g.fillBase) + } else if err := g.fill.GenerateFrame(dst[:g.fillSize], frameIndex); err != nil { + return fmt.Errorf("v210a: generate dynamic fill frame %d: %w", frameIndex, err) } alpha := dst[g.fillSize:need] copy(alpha, g.alphaBase) diff --git a/internal/generator/v210a_test.go b/internal/generator/v210a_test.go index 72261a9..acefd0e 100644 --- a/internal/generator/v210a_test.go +++ b/internal/generator/v210a_test.go @@ -32,7 +32,7 @@ func (g *fakeFrameGenerator) Close() error { func TestV210AGeneratorLayoutAndAlpha(t *testing.T) { const width, height = 304, 200 fill := &fakeFrameGenerator{} - g, err := NewV210AGenerator(fill, width, height) + g, err := NewV210AGenerator(fill, width, height, true) if err != nil { t.Fatalf("NewV210AGenerator: %v", err) } @@ -58,7 +58,7 @@ func TestV210AGeneratorLayoutAndAlpha(t *testing.T) { func TestV210AGeneratorRestoresAlphaBase(t *testing.T) { const width, height = 304, 200 - g, err := NewV210AGenerator(&fakeFrameGenerator{}, width, height) + g, err := NewV210AGenerator(&fakeFrameGenerator{}, width, height, true) if err != nil { t.Fatalf("NewV210AGenerator: %v", err) } @@ -82,13 +82,68 @@ func TestV210AGeneratorRestoresAlphaBase(t *testing.T) { } } +func TestV210AGeneratorCachesStaticFill(t *testing.T) { + const width, height = 100, 20 + fill := &fakeFrameGenerator{} + g, err := NewV210AGenerator(fill, width, height, false) + if err != nil { + t.Fatalf("NewV210AGenerator: %v", err) + } + if fill.calls != 1 { + t.Fatalf("fill calls after construction = %d, want 1", fill.calls) + } + + dst := make([]byte, V210AFrameSize(width, height)) + for _, frameIndex := range []int{7, 19} { + if err := g.GenerateFrame(dst, frameIndex); err != nil { + t.Fatalf("GenerateFrame(%d): %v", frameIndex, err) + } + } + if fill.calls != 1 { + t.Errorf("fill calls after two frames = %d, want 1", fill.calls) + } + for i, b := range dst[:V210FrameSize(width, height)] { + if b != 0 { + t.Fatalf("cached fill byte %d = %#x, want frame-zero value 0", i, b) + } + } +} + +func TestV210AGeneratorRegeneratesDynamicFill(t *testing.T) { + const width, height = 100, 20 + fill := &fakeFrameGenerator{} + g, err := NewV210AGenerator(fill, width, height, true) + if err != nil { + t.Fatalf("NewV210AGenerator: %v", err) + } + if fill.calls != 0 { + t.Fatalf("fill calls after construction = %d, want 0", fill.calls) + } + + dst := make([]byte, V210AFrameSize(width, height)) + for _, frameIndex := range []int{7, 19} { + if err := g.GenerateFrame(dst, frameIndex); err != nil { + t.Fatalf("GenerateFrame(%d): %v", frameIndex, err) + } + } + if fill.calls != 2 { + t.Errorf("fill calls after two frames = %d, want 2", fill.calls) + } + if got := dst[0]; got != 19 { + t.Errorf("dynamic fill byte = %#x, want frame-index value 0x13", got) + } +} + func TestV210AGeneratorErrors(t *testing.T) { - if _, err := NewV210AGenerator(nil, 1920, 1080); err == nil || !strings.Contains(err.Error(), "nil") { + if _, err := NewV210AGenerator(nil, 1920, 1080, false); err == nil || !strings.Contains(err.Error(), "nil") { t.Fatalf("nil fill error = %v", err) } fillErr := errors.New("fill failed") - g, err := NewV210AGenerator(&fakeFrameGenerator{generateErr: fillErr}, 100, 20) + if _, err := NewV210AGenerator(&fakeFrameGenerator{generateErr: fillErr}, 100, 20, false); !errors.Is(err, fillErr) { + t.Fatalf("static fill initialization error = %v, want wrapped %v", err, fillErr) + } + g, err := NewV210AGenerator(&fakeFrameGenerator{generateErr: fillErr}, 100, 20, true) if err != nil { t.Fatalf("NewV210AGenerator: %v", err) } @@ -103,7 +158,7 @@ func TestV210AGeneratorErrors(t *testing.T) { func TestV210AGeneratorClosesFill(t *testing.T) { closeErr := errors.New("close failed") fill := &fakeFrameGenerator{closeErr: closeErr} - g, err := NewV210AGenerator(fill, 100, 20) + g, err := NewV210AGenerator(fill, 100, 20, false) if err != nil { t.Fatalf("NewV210AGenerator: %v", err) } diff --git a/internal/video/generator.go b/internal/video/generator.go index f60d15e..58174e2 100644 --- a/internal/video/generator.go +++ b/internal/video/generator.go @@ -21,19 +21,23 @@ func newFrameGenerator(cfg Config, pattern pattern) (generator.FrameGenerator, B if err != nil { return nil, "", err } - gen, err := wrapAlphaGenerator(cfg, fill) + gen, err := wrapAlphaGenerator(cfg, fill, pattern.dynamic) if err != nil { return nil, "", err } return gen, backend, nil } -func wrapAlphaGenerator(cfg Config, fill generator.FrameGenerator) (generator.FrameGenerator, error) { +func wrapAlphaGenerator( + cfg Config, + fill generator.FrameGenerator, + fillDynamic bool, +) (generator.FrameGenerator, error) { if !cfg.HasAlpha() { return fill, nil } - gen, err := generator.NewV210AGenerator(fill, cfg.Width(), cfg.Height()) + gen, err := generator.NewV210AGenerator(fill, cfg.Width(), cfg.Height(), fillDynamic) if err != nil { closeErr := fill.Close() return nil, errors.Join( diff --git a/internal/video/generator_test.go b/internal/video/generator_test.go index cf1cc24..5daec67 100644 --- a/internal/video/generator_test.go +++ b/internal/video/generator_test.go @@ -145,7 +145,7 @@ func TestWrapAlphaGenerator(t *testing.T) { } fill := &stubFrameGenerator{} - gen, err := wrapAlphaGenerator(cfg, fill) + gen, err := wrapAlphaGenerator(cfg, fill, false) if err != nil { t.Fatalf("wrapAlphaGenerator: %v", err) }