Skip to content

Commit 31d948e

Browse files
committed
Speed up rendering and encoding
1 parent c160e30 commit 31d948e

File tree

3 files changed

+183
-24
lines changed

3 files changed

+183
-24
lines changed

013-cloud-gaming/game/snake_renderer.go

+38-23
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"image"
55
"image/color"
66
"log"
7+
"sync"
78
"time"
89
)
910

@@ -13,7 +14,10 @@ var (
1314
foodColor = color.RGBA{R: 0, G: 255, B: 0, A: 255}
1415
)
1516

16-
const bytesPerPixel = 3 // RGB: 3 bytes per pixel
17+
const (
18+
numWorkers = 4
19+
bytesPerPixel = 3 // RGB: 3 bytes per pixel
20+
)
1721

1822
func StartFrameRenderer(gameStateCh chan *Snake, canvasCh chan *Canvas) {
1923
defer func() {
@@ -22,33 +26,44 @@ func StartFrameRenderer(gameStateCh chan *Snake, canvasCh chan *Canvas) {
2226
}
2327
}()
2428

25-
for {
26-
start := time.Now()
27-
gameState, ok := <-gameStateCh
28-
if !ok {
29-
break
30-
}
29+
var wg sync.WaitGroup
30+
workerCh := make(chan *Snake)
31+
32+
for i := 0; i < numWorkers; i++ {
33+
wg.Add(1)
34+
go func() {
35+
defer wg.Done()
36+
for {
37+
gameState, ok := <-workerCh
38+
if !ok {
39+
return
40+
}
3141

32-
img := image.NewRGBA(image.Rect(0, 0, FRAME_WIDTH, FRAME_HEIGHT))
33-
matrix := gameState.GetMatrix()
42+
img := image.NewRGBA(image.Rect(0, 0, FRAME_WIDTH, FRAME_HEIGHT))
43+
matrix := gameState.GetMatrix()
3444

35-
for y := 0; y < len(matrix); y++ {
36-
for x := 0; x < len(matrix[0]); x++ {
37-
rectMin := image.Point{X: x * CHUNK_SIZE, Y: y * CHUNK_SIZE}
38-
rectMax := image.Point{X: rectMin.X + CHUNK_SIZE, Y: rectMin.Y + CHUNK_SIZE}
39-
switch matrix[y][x] {
40-
case HEAD:
41-
drawRectangle(img, rectMin, rectMax, snakeHeadColor)
42-
case BODY:
43-
drawRectangle(img, rectMin, rectMax, snakeBodyColor)
44-
case FOOD:
45-
drawRectangle(img, rectMin, rectMax, foodColor)
45+
for y := 0; y < len(matrix); y++ {
46+
for x := 0; x < len(matrix[0]); x++ {
47+
rectMin := image.Point{X: x * CHUNK_SIZE, Y: y * CHUNK_SIZE}
48+
rectMax := image.Point{X: rectMin.X + CHUNK_SIZE, Y: rectMin.Y + CHUNK_SIZE}
49+
switch matrix[y][x] {
50+
case HEAD:
51+
drawRectangle(img, rectMin, rectMax, snakeHeadColor)
52+
case BODY:
53+
drawRectangle(img, rectMin, rectMax, snakeBodyColor)
54+
case FOOD:
55+
drawRectangle(img, rectMin, rectMax, foodColor)
56+
}
57+
}
4658
}
59+
60+
canvasCh <- &Canvas{Data: convertRGBAtoRGB(img), Timestamp: time.Now()}
4761
}
48-
}
62+
}()
63+
}
4964

50-
canvasCh <- &Canvas{Data: convertRGBAtoRGB(img), Timestamp: time.Now()}
51-
log.Println("StartFrameRenderer", time.Since(start))
65+
for gameState := range gameStateCh {
66+
workerCh <- gameState
5267
}
5368
}
5469

+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package service
2+
3+
import (
4+
"cloud-gaming/game"
5+
"fmt"
6+
"io"
7+
"log"
8+
"os"
9+
"os/exec"
10+
"sync"
11+
"sync/atomic"
12+
"time"
13+
)
14+
15+
type Encoder struct {
16+
encodedFrameCh chan *Streamable
17+
canvasCh <-chan *game.Canvas
18+
closeSignal <-chan bool
19+
windowWidth int
20+
windowHeight int
21+
closed int32
22+
wg sync.WaitGroup
23+
}
24+
25+
type EncoderOptions struct {
26+
EncodedFrameChannel chan *Streamable
27+
CanvasChannel <-chan *game.Canvas
28+
CloseSignal <-chan bool
29+
WindowWidth int
30+
WindowHeight int
31+
}
32+
33+
type Streamable struct {
34+
Data []byte
35+
Timestamp time.Time
36+
}
37+
38+
func NewEncoder(options *EncoderOptions) *Encoder {
39+
return &Encoder{
40+
encodedFrameCh: options.EncodedFrameChannel,
41+
canvasCh: options.CanvasChannel,
42+
closeSignal: options.CloseSignal,
43+
windowWidth: options.WindowWidth,
44+
windowHeight: options.WindowHeight,
45+
}
46+
}
47+
48+
func (e *Encoder) isClosed() bool {
49+
return atomic.LoadInt32(&e.closed) == 1
50+
}
51+
52+
func (e *Encoder) markAsClosed() {
53+
atomic.StoreInt32(&e.closed, 1)
54+
}
55+
56+
const ffmpegBaseCommand = "ffmpeg -hide_banner -loglevel error -threads 0 -re -f rawvideo -pixel_format rgb24 -video_size %dx%d -framerate %v -r %v -i pipe:0 -pix_fmt yuv420p -c:v h264_videotoolbox -f h264 pipe:1"
57+
58+
func (e *Encoder) Start() {
59+
ffmpegCommand := fmt.Sprintf(ffmpegBaseCommand, e.windowWidth, e.windowHeight, game.FPS, game.FPS)
60+
cmd := exec.Command("bash", "-c", ffmpegCommand)
61+
cmd.Stderr = os.Stderr
62+
inPipe, err := cmd.StdinPipe()
63+
if err != nil {
64+
panic(err)
65+
}
66+
67+
outPipe, err := cmd.StdoutPipe()
68+
if err != nil {
69+
panic(err)
70+
}
71+
72+
err = cmd.Start()
73+
if err != nil {
74+
panic(err)
75+
}
76+
77+
e.wg.Add(2)
78+
79+
go e.writeToFFmpeg(inPipe)
80+
go e.streamToWebRTCTrack(outPipe)
81+
82+
go func() {
83+
_, ok := <-e.closeSignal
84+
if !ok {
85+
e.markAsClosed()
86+
fmt.Println("Closing encoder")
87+
88+
cmd.Wait()
89+
e.wg.Wait()
90+
close(e.encodedFrameCh)
91+
}
92+
}()
93+
94+
}
95+
96+
func (e *Encoder) writeToFFmpeg(inPipe io.WriteCloser) {
97+
defer e.wg.Done()
98+
99+
for canvas := range e.canvasCh {
100+
if e.isClosed() {
101+
return
102+
}
103+
select {
104+
case <-e.encodedFrameCh: // Check if there's a backlog.
105+
continue
106+
default:
107+
_, err := inPipe.Write(canvas.Data)
108+
if err != nil {
109+
panic(err)
110+
}
111+
}
112+
}
113+
114+
inPipe.Close()
115+
}
116+
117+
func (e *Encoder) streamToWebRTCTrack(outPipe io.Reader) {
118+
defer e.wg.Done()
119+
120+
buf := make([]byte, 1024*8)
121+
for {
122+
if e.isClosed() {
123+
return
124+
}
125+
126+
timestamp := time.Now()
127+
n, err := outPipe.Read(buf)
128+
if err == io.EOF {
129+
break
130+
} else if err != nil {
131+
log.Println(err)
132+
continue
133+
}
134+
135+
e.encodedFrameCh <- &Streamable{Data: buf[:n], Timestamp: timestamp}
136+
}
137+
}

013-cloud-gaming/service/worker.go

+8-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,14 @@ func (w *Worker) onDataChannel(p *PeerConnState) {
4141
go gameLoop.Start()
4242
go game.StartFrameRenderer(gameStateCh, canvasCh)
4343

44-
go w.startEncoder(canvasCh, encodedFrameCh)
44+
encoder := NewEncoder(&EncoderOptions{
45+
EncodedFrameChannel: encodedFrameCh,
46+
CanvasChannel: canvasCh,
47+
WindowWidth: game.FRAME_WIDTH,
48+
WindowHeight: game.FRAME_HEIGHT,
49+
})
50+
encoder.Start()
51+
4552
go w.startStreaming(encodedFrameCh, senders)
4653
go w.closeConnection(closeSignal, dataCh, p, gameStateCh, cmdCh, canvasCh, encodedFrameCh)
4754

0 commit comments

Comments
 (0)