From 8d6aa732d74a43bd8e28381f69ce322e431e63f4 Mon Sep 17 00:00:00 2001 From: Norwin Date: Sun, 2 Jan 2022 11:14:11 +0100 Subject: [PATCH] new offset implementation allows specifying a mask image for the random offset. Offsetter interface could also be used for a random walk / DVD-bounce incremental offset. --- main.go | 2 +- pixelflut/canvas.go | 2 +- pixelflut/commands.go | 6 +-- pixelflut/flut.go | 13 ++++--- pixelflut/net.go | 86 ++++++++++++++++++++++++++++++++++++++----- rpc/repl.go | 20 +++++++--- 6 files changed, 103 insertions(+), 26 deletions(-) diff --git a/main.go b/main.go index 7bc0e56..3a8850a 100644 --- a/main.go +++ b/main.go @@ -75,7 +75,7 @@ func taskFromFlags(stop chan bool, wg *sync.WaitGroup) { FlutTaskOpts: pixelflut.FlutTaskOpts{ Address: *address, MaxConns: *connections, - Offset: image.Pt(*x, *y), + Offset: pixelflut.RandOffsetter{Point: image.Pt(*x, *y)}, RenderOrder: pixelflut.NewOrder(*order), }, Img: img, diff --git a/pixelflut/canvas.go b/pixelflut/canvas.go index 52f57fe..8d84b12 100644 --- a/pixelflut/canvas.go +++ b/pixelflut/canvas.go @@ -45,7 +45,7 @@ func FetchImage(bounds *image.Rectangle, address string, conns int, stop chan bo } go readPixels(img, conn, stop) - go bombConn(cmds[i], 0, 0, conn, stop) + go bombConn(cmds[i], &RandOffsetter{}, conn, stop) } return img diff --git a/pixelflut/commands.go b/pixelflut/commands.go index 02791a8..f5136f8 100644 --- a/pixelflut/commands.go +++ b/pixelflut/commands.go @@ -39,7 +39,7 @@ func OffsetCmd(x, y int) []byte { } // CommandsFromImage converts an image to the respective pixelflut commands -func commandsFromImage(img *image.NRGBA, order RenderOrder, offset image.Point) (cmds commands) { +func commandsFromImage(img *image.NRGBA, order RenderOrder, offset RandOffsetter) (cmds commands) { b := img.Bounds() cmds = make([][]byte, b.Size().X*b.Size().Y) numCmds := 0 @@ -74,9 +74,9 @@ func commandsFromImage(img *image.NRGBA, order RenderOrder, offset image.Point) var cmd []byte cmd = append(cmd, []byte("PX ")...) - cmd = strconv.AppendUint(cmd, uint64(x+offset.X), 10) + cmd = strconv.AppendUint(cmd, uint64(x), 10) cmd = append(cmd, ' ') - cmd = strconv.AppendUint(cmd, uint64(y+offset.Y), 10) + cmd = strconv.AppendUint(cmd, uint64(y), 10) cmd = append(cmd, ' ') appendColor(&cmd, c) cmd = append(cmd, '\n') diff --git a/pixelflut/flut.go b/pixelflut/flut.go index 7cba65f..6a005b2 100644 --- a/pixelflut/flut.go +++ b/pixelflut/flut.go @@ -20,11 +20,10 @@ type FlutTask struct { type FlutTaskOpts struct { Address string MaxConns int - Offset image.Point Paused bool RGBSplit bool // @cleanup: replace with `FX: []Effect` - RandOffset bool RenderOrder RenderOrder + Offset RandOffsetter } // FlutTaskData contains the actual pixeldata to flut, separated because of size @@ -36,8 +35,8 @@ func (t FlutTask) String() string { img = t.Img.Bounds().Size().String() } return fmt.Sprintf( - " %d conns @ %s\n img %v offset %v\n order %s rgbsplit %v randoffset %v paused %v", - t.MaxConns, t.Address, img, t.Offset, t.RenderOrder, t.RGBSplit, t.RandOffset, t.Paused, + " %d conns @ %s\n img %v offset %v\n order %s rgbsplit %v paused %v", + t.MaxConns, t.Address, img, t.Offset, t.RenderOrder, t.RGBSplit, t.Paused, ) } @@ -84,10 +83,11 @@ func Flut(t FlutTask, stop chan bool, wg *sync.WaitGroup) { var messages [][]byte var maxOffsetX, maxOffsetY int - if t.RandOffset { + if t.Offset.Random { maxX, maxY := CanvasSize(t.Address) maxOffsetX = maxX - t.Img.Bounds().Canon().Dx() maxOffsetY = maxY - t.Img.Bounds().Canon().Dy() + t.Offset.SetMaximumOffset(image.Pt(maxOffsetX, maxOffsetY)) messages = cmds.Chunk(1) // each connection should send the full img } else { messages = cmds.Chunk(t.MaxConns) @@ -102,7 +102,7 @@ func Flut(t FlutTask, stop chan bool, wg *sync.WaitGroup) { time.Sleep(50 * time.Millisecond) // avoid crashing the server - go bombAddress(msg, t.Address, maxOffsetX, maxOffsetY, stop, &bombWg) + go bombAddress(msg, t.Address, &t.Offset, stop, &bombWg) } bombWg.Wait() if wg != nil { @@ -114,6 +114,7 @@ func generateCommands(t FlutTask) (cmds commands) { if t.RGBSplit { white := color.NRGBA{0xff, 0xff, 0xff, 0xff} imgmod := render.ImgColorFilter(t.Img, white, color.NRGBA{0xff, 0, 0, 0xff}) + // FIXME: this offset is ignored with the latest changes, restore this behaviour. cmds = append(cmds, commandsFromImage(imgmod, t.RenderOrder, t.Offset.Add(image.Pt(-10, -10)))...) imgmod = render.ImgColorFilter(t.Img, white, color.NRGBA{0, 0xff, 0, 0xff}) cmds = append(cmds, commandsFromImage(imgmod, t.RenderOrder, t.Offset.Add(image.Pt(10, 0)))...) diff --git a/pixelflut/net.go b/pixelflut/net.go index 3d39690..3155110 100644 --- a/pixelflut/net.go +++ b/pixelflut/net.go @@ -2,6 +2,7 @@ package pixelflut import ( "fmt" + "image" "math/rand" "net" "sync" @@ -13,6 +14,71 @@ const ( timeoutMax = 10 * time.Second ) +type Offsetter interface { + Next() (x, y int) + SetMaximumOffset(max image.Point) +} + +type RandOffsetter struct { + image.Point + Mask *image.NRGBA + Random bool + Max image.Point +} + +func (r RandOffsetter) String() string { + mask := "nil" + if r.Mask != nil { + mask = r.Mask.Bounds().Size().String() + } + return fmt.Sprintf("[%v + random %v, mask %v]", r.Point, r.Random, mask) +} + +// override image.Point interface +func (r RandOffsetter) Add(p image.Point) RandOffsetter { + r.Point = r.Point.Add(p) + return r +} + +func (r *RandOffsetter) SetMaximumOffset(max image.Point) { + if r.Mask != nil { + mask := r.Mask.Bounds().Canon().Max.Sub(r.Point) + r.Max.X = clamp(mask.X, 1, max.X) + r.Max.Y = clamp(mask.Y, 1, max.Y) + } else { + r.Max.X = clamp(max.X, 1, max.X) + r.Max.Y = clamp(max.Y, 1, max.Y) + } +} + +func clamp(val, min, max int) int { + if val < min { + return min + } + if val > max { + return max + } + return val +} + +func (r RandOffsetter) Next() (x, y int) { + if r.Random { + for i := 0; i < 1000; i++ { + x := rand.Intn(r.Max.X) + y := rand.Intn(r.Max.Y) + if r.Mask != nil { + if _, _, _, a := r.Mask.At(x, y).RGBA(); a != 0 { + return r.X + x, r.Y + y + } + } else { + return r.X + x, r.Y + y + } + } + } + + return r.X, r.Y +} + // Performance contains pixelflut metrics type Performance struct { Enabled bool @@ -94,7 +160,7 @@ func initPerfReporter() *Performance { // bombAddress opens a TCP connection to `address`, and writes `message` repeatedly, until `stop` is closed. // It retries with exponential backoff on network errors. -func bombAddress(message []byte, address string, maxOffsetX, maxOffsetY int, stop chan bool, wg *sync.WaitGroup) { +func bombAddress(message []byte, address string, offsetter Offsetter, stop chan bool, wg *sync.WaitGroup) { wg.Add(1) defer wg.Done() @@ -115,7 +181,7 @@ func bombAddress(message []byte, address string, maxOffsetX, maxOffsetY int, sto fmt.Printf("[net] bombing %s with new connection\n", address) - err = bombConn(message, maxOffsetX, maxOffsetY, conn, stop) + err = bombConn(message, offsetter, conn, stop) conn.Close() timeout = timeoutMin if err == nil { @@ -127,25 +193,25 @@ func bombAddress(message []byte, address string, maxOffsetX, maxOffsetY int, sto // bombConn writes the given message to the given connection in a tight loop, until `stop` is closed. // Does no transformation on the given message, so make sure packet splitting / nagle works. -func bombConn(message []byte, maxOffsetX, maxOffsetY int, conn net.Conn, stop chan bool) error { +func bombConn(message []byte, offsetter Offsetter, conn net.Conn, stop chan bool) error { PerformanceReporter.connsReporter <- 1 defer func() { PerformanceReporter.connsReporter <- -1 }() var msg = make([]byte, len(message)+16) // leave some space for offset cmd msg = message - randOffset := maxOffsetX > 0 && maxOffsetY > 0 + // randOffset := maxOffsetX > 0 && maxOffsetY > 0 for { select { case <-stop: return nil default: - if randOffset { - msg = append( - OffsetCmd(rand.Intn(maxOffsetX), rand.Intn(maxOffsetY)), - message..., - ) - } + // if randOffset { + msg = append( + OffsetCmd(offsetter.Next()), + message..., + ) + // } b, err := conn.Write(msg) if err != nil { return err diff --git a/rpc/repl.go b/rpc/repl.go index 89a35b6..a4222c9 100644 --- a/rpc/repl.go +++ b/rpc/repl.go @@ -67,15 +67,25 @@ func RunREPL(f Fluter) { t.Paused = false case "offset", "of": - if len(args) == 1 && args[0] == "rand" { - t.RandOffset = true - t.Offset = image.Point{} + if len(args) >= 1 && args[0][0] == 'r' { + t.Offset.Random = true + t.Offset.Mask = nil + if len(args) >= 2 { + fmt.Println(strings.Join(args[1:], " ")) + mask, err := render.ReadImage(strings.Join(args[1:], " ")) + if err != nil { + fmt.Printf("couldn't read mask image: %s\n", err) + continue + } + t.Offset.Mask = mask + } } else if len(args) == 2 { - t.RandOffset = false + t.Offset.Random = false + t.Offset.Mask = nil x, err := strconv.Atoi(args[0]) y, err2 := strconv.Atoi(args[1]) if err == nil && err2 == nil { - t.Offset = image.Pt(x, y) + t.Offset.Point = image.Pt(x, y) } }