diff --git a/cmd/coordinator/main.go b/cmd/coordinator/main.go index 816390e6..294d6957 100644 --- a/cmd/coordinator/main.go +++ b/cmd/coordinator/main.go @@ -17,7 +17,7 @@ func main() { conf := config.NewConfig() conf.ParseFlags() - log := logger.NewConsole(conf.Coordinator.Debug, "c", true) + log := logger.NewConsole(conf.Coordinator.Debug, "c", false) log.Info().Msgf("version %s", Version) log.Info().Msgf("conf version: %v", conf.Version) diff --git a/cmd/worker/main.go b/cmd/worker/main.go index b4fb1352..cf3e4322 100644 --- a/cmd/worker/main.go +++ b/cmd/worker/main.go @@ -18,7 +18,7 @@ func run() { conf := config.NewConfig() conf.ParseFlags() - log := logger.NewConsole(conf.Worker.Debug, "w", true) + log := logger.NewConsole(conf.Worker.Debug, "w", false) log.Info().Msgf("version %s", Version) log.Info().Msgf("conf version: %v", conf.Version) if log.GetLevel() < logger.InfoLevel { diff --git a/go.mod b/go.mod index 8476f070..d25524ec 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/pion/webrtc/v3 v3.1.50 github.com/rs/xid v1.4.0 github.com/rs/zerolog v1.28.0 - github.com/veandco/go-sdl2 v0.4.28 + github.com/veandco/go-sdl2 v0.4.29 golang.org/x/crypto v0.5.0 golang.org/x/image v0.3.0 ) @@ -39,7 +39,7 @@ require ( github.com/pion/stun v0.3.5 // indirect github.com/pion/transport v0.14.1 // indirect github.com/pion/turn/v2 v2.0.9 // indirect - github.com/pion/udp v0.1.1 // indirect + github.com/pion/udp v0.1.2 // indirect github.com/valyala/fastrand v1.1.0 // indirect github.com/valyala/histogram v1.2.0 // indirect golang.org/x/net v0.5.0 // indirect diff --git a/go.sum b/go.sum index 6bf97308..5ad9240e 100644 --- a/go.sum +++ b/go.sum @@ -45,7 +45,6 @@ github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -68,7 +67,6 @@ github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0= github.com/pion/dtls/v2 v2.1.5 h1:jlh2vtIyUBShchoTDqpCCqiYCyRFJ/lvf/gQ8TALs+c= github.com/pion/dtls/v2 v2.1.5/go.mod h1:BqCE7xPZbPSubGasRoDFJeTsyJtdD1FanJYL0JGheqY= -github.com/pion/ice/v2 v2.2.12 h1:n3M3lUMKQM5IoofhJo73D3qVla+mJN2nVvbSPq32Nig= github.com/pion/ice/v2 v2.2.12/go.mod h1:z2KXVFyRkmjetRlaVRgjO9U3ShKwzhlUylvxKfHfd5A= github.com/pion/ice/v2 v2.2.13 h1:NvLtzwcyob6wXgFqLmVQbGB3s9zzWmOegNMKYig5l9M= github.com/pion/ice/v2 v2.2.13/go.mod h1:eFO4/1zCI+a3OFVt7l7kP+5jWCuZo8FwU2UwEa3+164= @@ -90,7 +88,6 @@ github.com/pion/sctp v1.8.5 h1:JCc25nghnXWOlSn3OVtEnA9PjQ2JsxQbG+CXZ1UkJKQ= github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0= github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw= github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw= -github.com/pion/srtp/v2 v2.0.10 h1:b8ZvEuI+mrL8hbr/f1YiJFB34UMrOac3R3N1yq2UN0w= github.com/pion/srtp/v2 v2.0.10/go.mod h1:XEeSWaK9PfuMs7zxXyiN252AHPbH12NX5q/CFDWtUuA= github.com/pion/srtp/v2 v2.0.11 h1:6cEEgT1oCLWgE+BynbfaSMAxtsqU0M096x9dNH6olY0= github.com/pion/srtp/v2 v2.0.11/go.mod h1:vzHprzbuVoYJ9NfaRMycnFrkHcLSaLVuBZDOtFQNZjY= @@ -104,8 +101,9 @@ github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99 github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw= github.com/pion/turn/v2 v2.0.9 h1:jcDPw0Vfd5I4iTc7s0Upfc2aMnyu2lgJ9vV0SUrNC1o= github.com/pion/turn/v2 v2.0.9/go.mod h1:DQlwUwx7hL8Xya6TTAabbd9DdKXTNR96Xf5g5Qqso/M= -github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o= github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M= +github.com/pion/udp v0.1.2 h1:Bl1ifOcoVYg9gnk1+9yyTX8XgAUORiDvM7UqBb3skhg= +github.com/pion/udp v0.1.2/go.mod h1:CuqU2J4MmF3sjqKfk1SaIhuNXdum5PJRqd2LHuLMQSk= github.com/pion/webrtc/v3 v3.1.50 h1:wLMo1+re4WMZ9Kun9qcGcY+XoHkE3i0CXrrc0sjhVCk= github.com/pion/webrtc/v3 v3.1.50/go.mod h1:y9n09weIXB+sjb9mi0GBBewNxo4TKUQm5qdtT5v3/X4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -130,8 +128,8 @@ github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ= github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ= github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY= -github.com/veandco/go-sdl2 v0.4.28 h1:kLXyC0MNbQp6aQcow27Nozaos6XT9j1db7hMm2PPPas= -github.com/veandco/go-sdl2 v0.4.28/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY= +github.com/veandco/go-sdl2 v0.4.29 h1:YjqquD+q3E4o1zJ6fTLZTcqA/c4Efy9Tgqw+LLxUNdw= +github.com/veandco/go-sdl2 v0.4.29/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -140,12 +138,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= -golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= -golang.org/x/image v0.2.0 h1:/DcQ0w3VHKCC5p0/P2B0JpAZ9Z++V2KOo2fyU89CXBQ= -golang.org/x/image v0.2.0/go.mod h1:la7oBXb9w3YFjBqaAwtynVioc1ZvOnNteUNrifGNmAI= golang.org/x/image v0.3.0 h1:HTDXbdK9bjfSWkPzDJIw89W8CAtfFGduujWs33NLLsg= golang.org/x/image v0.3.0/go.mod h1:fXd9211C/0VTlYuAcOhW8dY/RtEJqODXOWBDpmYBf+A= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -167,8 +161,6 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= -golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -198,7 +190,6 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -212,7 +203,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= diff --git a/pkg/com/com.go b/pkg/com/com.go index 347675cd..5899f2a5 100644 --- a/pkg/com/com.go +++ b/pkg/com/com.go @@ -52,7 +52,7 @@ func New(conn *Client, tag string, id network.Uid, log *logger.Logger) SocketCli if conn.IsServer() { dir = "←" } - l.Info().Str("c", tag).Str("d", dir).Msg("Connect") + l.Debug().Str("c", tag).Str("d", dir).Msg("Connect") return SocketClient{id: id, wire: conn, Tag: tag, Log: l} } @@ -60,7 +60,7 @@ func (c *SocketClient) SetId(id network.Uid) { c.id = id } func (c *SocketClient) OnPacket(fn func(p In) error) { logFn := func(p In) { - c.Log.Info().Str("c", c.Tag).Str("d", "←").Msgf("%s", p.T) + c.Log.Debug().Str("c", c.Tag).Str("d", "←").Msgf("%s", p.T) if err := fn(p); err != nil { c.Log.Error().Err(err).Send() } @@ -70,19 +70,19 @@ func (c *SocketClient) OnPacket(fn func(p In) error) { // Send makes a blocking call. func (c *SocketClient) Send(t api.PT, data any) ([]byte, error) { - c.Log.Info().Str("c", c.Tag).Str("d", "→").Msgf("ᵇ%s", t) + c.Log.Debug().Str("c", c.Tag).Str("d", "→").Msgf("ᵇ%s", t) return c.wire.Call(t, data) } // Notify just sends a message and goes further. func (c *SocketClient) Notify(t api.PT, data any) { - c.Log.Info().Str("c", c.Tag).Str("d", "→").Msgf("%s", t) + c.Log.Debug().Str("c", c.Tag).Str("d", "→").Msgf("%s", t) _ = c.wire.Send(t, data) } func (c *SocketClient) Close() { c.wire.Close() - c.Log.Info().Str("c", c.Tag).Str("d", "x").Msg("Close") + c.Log.Debug().Str("c", c.Tag).Str("d", "x").Msg("Close") } func (c *SocketClient) Id() network.Uid { return c.id } diff --git a/pkg/coordinator/hub.go b/pkg/coordinator/hub.go index 8c9c4ce9..369a1ab0 100644 --- a/pkg/coordinator/hub.go +++ b/pkg/coordinator/hub.go @@ -44,7 +44,7 @@ func NewHub(conf coordinator.Config, lib games.GameLibrary, log *logger.Logger) // handleUserConnection handles all connections from user/frontend. func (h *Hub) handleUserConnection(w http.ResponseWriter, r *http.Request) { - h.log.Info().Str("c", "u").Str("d", "←").Msgf("Handshake %v", r.Host) + h.log.Debug().Str("c", "u").Str("d", "←").Msgf("Handshake %v", r.Host) conn, err := h.uConn.NewClientServer(w, r, h.log) if err != nil { h.log.Error().Err(err).Msg("couldn't init user connection") @@ -95,7 +95,7 @@ func (h *Hub) handleUserConnection(w http.ResponseWriter, r *http.Request) { // handleWorkerConnection handles all connections from a new worker to coordinator. func (h *Hub) handleWorkerConnection(w http.ResponseWriter, r *http.Request) { - h.log.Info().Str("c", "w").Str("d", "←").Msgf("Handshake %v", r.Host) + h.log.Debug().Str("c", "w").Str("d", "←").Msgf("Handshake %v", r.Host) data := r.URL.Query().Get(api.DataQueryParam) handshake, err := GetConnectionRequest(data) diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index ff299756..02945982 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -110,6 +110,10 @@ func NewConsole(isDebug bool, tag string, noColor bool) *Logger { return &Logger{logger: &logger} } +func SetGlobalLevel(l Level) { + zerolog.SetGlobalLevel(zerolog.Level(l)) +} + func Default() *Logger { return &Logger{logger: &log.Logger} } // GetLevel returns the current Level of l. diff --git a/pkg/network/webrtc/webrtc.go b/pkg/network/webrtc/webrtc.go index ddf5df45..5cc9ce15 100644 --- a/pkg/network/webrtc/webrtc.go +++ b/pkg/network/webrtc/webrtc.go @@ -206,7 +206,9 @@ func (p *Peer) addInputChannel(label string) error { p.logx(ch.Send(mess.Data)) return } - p.OnMessage(mess.Data) + if p.OnMessage != nil { + p.OnMessage(mess.Data) + } }) p.dTrack = ch ch.OnClose(func() { p.log.Debug().Msg("Data channel [input] has been closed") }) diff --git a/pkg/os/os.go b/pkg/os/os.go index e9fb31f4..117e41ad 100644 --- a/pkg/os/os.go +++ b/pkg/os/os.go @@ -9,6 +9,8 @@ import ( "syscall" ) +var ErrNotExist = os.ErrNotExist + func Exists(path string) bool { _, err := os.Stat(path) return !errors.Is(err, fs.ErrNotExist) @@ -16,7 +18,7 @@ func Exists(path string) bool { func CheckCreateDir(path string) error { if !Exists(path) { - return os.Mkdir(path, os.ModeDir) + return os.MkdirAll(path, os.ModeDir|0755) } return nil } diff --git a/pkg/worker/coordinator.go b/pkg/worker/coordinator.go index b12353a0..73372555 100644 --- a/pkg/worker/coordinator.go +++ b/pkg/worker/coordinator.go @@ -25,7 +25,7 @@ func connect(host string, conf worker.Worker, addr string, log *logger.Logger) ( } address := url.URL{Scheme: scheme, Host: host, Path: conf.Network.Endpoint} - log.Info().Str("c", "c").Str("d", "→").Msgf("Handshake %s", address.String()) + log.Debug().Str("c", "c").Str("d", "→").Msgf("Handshake %s", address.String()) id := network.NewUid() req, err := buildConnQuery(id, conf, addr) diff --git a/pkg/worker/emulator/emulator.go b/pkg/worker/emulator/emulator.go index 319d1abc..4cc9a87d 100644 --- a/pkg/worker/emulator/emulator.go +++ b/pkg/worker/emulator/emulator.go @@ -1,7 +1,6 @@ package emulator import ( - img "image" "time" "github.com/giongto35/cloud-game/v2/pkg/worker/emulator/image" @@ -59,7 +58,7 @@ type Metadata struct { type ( GameFrame struct { - Data *img.RGBA + Data *image.Frame Duration time.Duration } GameAudio struct { diff --git a/pkg/worker/emulator/graphics/opengl.go b/pkg/worker/emulator/graphics/opengl.go index 0de9911e..bc3c56e1 100644 --- a/pkg/worker/emulator/graphics/opengl.go +++ b/pkg/worker/emulator/graphics/opengl.go @@ -93,7 +93,7 @@ func destroyFramebuffer() { gl.DeleteTextures(1, &opt.tex) } -func ReadFramebuffer(bytes int, w int, h int) []byte { +func ReadFramebuffer(bytes, w, h uint) []byte { data := buf[:bytes] gl.BindFramebuffer(gl.FRAMEBUFFER, opt.fbo) gl.ReadPixels(0, 0, int32(w), int32(h), opt.pixType, opt.pixFormat, unsafe.Pointer(&data[0])) diff --git a/pkg/worker/emulator/image/canvas.go b/pkg/worker/emulator/image/canvas.go new file mode 100644 index 00000000..83e2d1a4 --- /dev/null +++ b/pkg/worker/emulator/image/canvas.go @@ -0,0 +1,167 @@ +package image + +import ( + "image" + "math/bits" + "sync" + "unsafe" +) + +// Canvas is a stateful drawing surface, i.e. image.RGBA +type Canvas struct { + w, h int + vertical bool + pool sync.Pool + wg sync.WaitGroup +} + +type Frame struct { + *image.RGBA +} + +func (f *Frame) Opaque() bool { return true } +func (f *Frame) Copy() Frame { + return Frame{&image.RGBA{ + Pix: append([]uint8{}, f.Pix...), + Stride: f.Stride, + Rect: f.Rect, + }} +} + +const ( + BitFormatShort5551 = iota // BIT_FORMAT_SHORT_5_5_5_1 has 5 bits R, 5 bits G, 5 bits B, 1 bit alpha + BitFormatInt8888Rev // BIT_FORMAT_INT_8_8_8_8_REV has 8 bits R, 8 bits G, 8 bits B, 8 bit alpha + BitFormatShort565 // BIT_FORMAT_SHORT_5_6_5 has 5 bits R, 6 bits G, 5 bits +) + +func NewCanvas(w, h, size int) *Canvas { + return &Canvas{ + w: w, + h: h, + vertical: h > w, // input is inverted + pool: sync.Pool{New: func() any { + return &Frame{&image.RGBA{ + Pix: make([]uint8, size<<2), + Rect: image.Rectangle{Max: image.Point{X: w, Y: h}}, + }} + }}, + } +} + +func (c *Canvas) Get(w, h int) *Frame { + i := c.pool.Get().(*Frame) + if c.vertical { + w, h = h, w + } + i.Stride = w << 2 + i.Pix = i.Pix[:i.Stride*h] + i.Rect.Max.X = w + i.Rect.Max.Y = h + return i +} + +func (c *Canvas) Put(i *Frame) { c.pool.Put(i) } +func (c *Canvas) Clear() { c.wg = sync.WaitGroup{} } + +func (c *Canvas) Draw(encoding uint32, rot *Rotate, w, h, packedW, bpp int, data []byte, th int) *Frame { + dst := c.Get(w, h) + if th == 0 { + frame(encoding, dst, data, 0, h, h, w, packedW, bpp, rot) + } else { + hn := h / th + c.wg.Add(th) + for i := 0; i < th; i++ { + xx := hn * i + go func() { + frame(encoding, dst, data, xx, hn, h, w, packedW, bpp, rot) + c.wg.Done() + }() + } + c.wg.Wait() + } + + // rescale + if dst.Rect.Dx() != c.w || dst.Rect.Dy() != c.h { + out := c.Get(c.w, c.h) + Resize(ScaleNearestNeighbour, dst.RGBA, out.RGBA) + c.Put(dst) + return out + } + + return dst +} + +func frame(encoding uint32, dst *Frame, data []byte, yy int, hn int, h int, w int, pwb int, bpp int, rot *Rotate) { + sPtr := unsafe.Pointer(&data[yy*pwb]) + dPtr := unsafe.Pointer(&dst.Pix[yy*dst.Stride]) + // some cores can zero-right-pad rows to the packed width value + pad := pwb - w*bpp + yn := yy + hn + + if rot == nil { + // LE, BE might not work + switch encoding { + case BitFormatShort565: + for y := yy; y < yn; y++ { + for x := 0; x < w; x++ { + i565((*uint32)(dPtr), uint32(*(*uint16)(sPtr))) + sPtr = unsafe.Add(sPtr, uintptr(bpp)) + dPtr = unsafe.Add(dPtr, uintptr(4)) + } + if pad > 0 { + sPtr = unsafe.Add(sPtr, uintptr(pad)) + } + } + case BitFormatInt8888Rev: + for y := yy; y < yn; y++ { + for x := 0; x < w; x++ { + ix8888((*uint32)(dPtr), *(*uint32)(sPtr)) + sPtr = unsafe.Add(sPtr, uintptr(bpp)) + dPtr = unsafe.Add(dPtr, uintptr(4)) + } + if pad > 0 { + sPtr = unsafe.Add(sPtr, uintptr(pad)) + } + } + } + } else { + switch encoding { + case BitFormatShort565: + for y := yy; y < yn; y++ { + for x, k := 0, 0; x < w; x++ { + dx, dy := rot.Call(x, y, w, h) + k = dx<<2 + dy*dst.Stride + dPtr = unsafe.Pointer(&dst.Pix[k]) + i565((*uint32)(dPtr), uint32(*(*uint16)(sPtr))) + sPtr = unsafe.Add(sPtr, uintptr(bpp)) + } + if pad > 0 { + sPtr = unsafe.Add(sPtr, uintptr(pad)) + } + } + case BitFormatInt8888Rev: + for y := yy; y < yn; y++ { + for x, k := 0, 0; x < w; x++ { + dx, dy := rot.Call(x, y, w, h) + k = dx<<2 + dy*dst.Stride + dPtr = unsafe.Pointer(&dst.Pix[k]) + ix8888((*uint32)(dPtr), *(*uint32)(sPtr)) + sPtr = unsafe.Add(sPtr, uintptr(bpp)) + } + if pad > 0 { + sPtr = unsafe.Add(sPtr, uintptr(pad)) + } + } + } + } +} + +func i565(dst *uint32, px uint32) { + *dst = (px >> 8 & 0xf8) | ((px >> 3 & 0xfc) << 8) | ((px << 3 & 0xfc) << 16) // | 0xff000000 + // setting the last byte to 255 allows saving RGBA images to PNG not as black squares +} + +func ix8888(dst *uint32, px uint32) { + //*dst = ((px >> 16) & 0xff) | (px & 0xff00) | ((px << 16) & 0xff0000) + 0xff000000 + *dst = bits.ReverseBytes32(px << 8) //| 0xff000000 +} diff --git a/pkg/worker/emulator/image/draw_test.go b/pkg/worker/emulator/image/canvas_test.go similarity index 58% rename from pkg/worker/emulator/image/draw_test.go rename to pkg/worker/emulator/image/canvas_test.go index 1f6f0bed..9b076f7d 100644 --- a/pkg/worker/emulator/image/draw_test.go +++ b/pkg/worker/emulator/image/canvas_test.go @@ -10,7 +10,6 @@ func BenchmarkDraw(b *testing.B) { encoding uint32 rot *Rotate scaleType int - flipV bool w int h int packedW int @@ -30,7 +29,6 @@ func BenchmarkDraw(b *testing.B) { encoding: BitFormatInt8888Rev, rot: nil, scaleType: ScaleNearestNeighbour, - flipV: false, w: 256, h: 240, packedW: 256, @@ -47,7 +45,6 @@ func BenchmarkDraw(b *testing.B) { encoding: BitFormatInt8888Rev, rot: nil, scaleType: ScaleNearestNeighbour, - flipV: false, w: 256, h: 240, packedW: 256, @@ -61,11 +58,47 @@ func BenchmarkDraw(b *testing.B) { } for _, bn := range tests { + c := NewCanvas(bn.args.dw, bn.args.dh, bn.args.dw*bn.args.dh) + img := c.Get(bn.args.dw, bn.args.dh) + c.Put(img) + img2 := c.Get(bn.args.dw, bn.args.dh) + c.Put(img2) + b.ResetTimer() b.Run(fmt.Sprintf("%v", bn.name), func(b *testing.B) { for i := 0; i < b.N; i++ { - DrawRgbaImage(bn.args.encoding, bn.args.rot, bn.args.scaleType, bn.args.flipV, bn.args.w, bn.args.h, bn.args.packedW, bn.args.bpp, bn.args.data, bn.args.dw, bn.args.dh, bn.args.th) + p := c.Draw(bn.args.encoding, bn.args.rot, bn.args.w, bn.args.h, bn.args.packedW, bn.args.bpp, bn.args.data, bn.args.th) + c.Put(p) } b.ReportAllocs() }) } } + +func Test_ix8888(t *testing.T) { + type args struct { + dst *uint32 + px uint32 + expect uint32 + } + tests := []struct { + name string + args args + }{ + { + name: "", + args: args{ + dst: new(uint32), + px: 0x11223344, + expect: 0xff443322, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ix8888(tt.args.dst, tt.args.px) + if *tt.args.dst != tt.args.expect { + t.Errorf("nope, %x %x", *tt.args.dst, tt.args.expect) + } + }) + } +} diff --git a/pkg/worker/emulator/image/draw.go b/pkg/worker/emulator/image/draw.go deleted file mode 100644 index 5cc9595b..00000000 --- a/pkg/worker/emulator/image/draw.go +++ /dev/null @@ -1,93 +0,0 @@ -package image - -import ( - "image" - "sync" - "unsafe" -) - -const ( - BitFormatShort5551 = iota // BIT_FORMAT_SHORT_5_5_5_1 has 5 bits R, 5 bits G, 5 bits B, 1 bit alpha - BitFormatInt8888Rev // BIT_FORMAT_INT_8_8_8_8_REV has 8 bits R, 8 bits G, 8 bits B, 8 bit alpha - BitFormatShort565 // BIT_FORMAT_SHORT_5_6_5 has 5 bits R, 6 bits G, 5 bits -) - -var wg sync.WaitGroup - -func DrawRgbaImage(encoding uint32, rot *Rotate, scaleType int, flipV bool, w, h, packedW, bpp int, - data []byte, dw, dh, th int) *image.RGBA { - // !to implement own image interfaces img.Pix = bytes[] - ww, hh := w, h - if rot != nil && rot.IsEven { - ww, hh = hh, ww - } - - src := image.NewRGBA(image.Rect(0, 0, ww, hh)) - - pwb := packedW * bpp - if th == 0 { - frame(encoding, src, data, 0, h, flipV, h, w, pwb, bpp, rot) - } else { - hn := h / th - wg.Add(th) - for i := 0; i < th; i++ { - xx := hn * i - go func() { - frame(encoding, src, data, xx, hn, flipV, h, w, pwb, bpp, rot) - wg.Done() - }() - } - wg.Wait() - } - - if ww == dw && hh == dh { - return src - } else { - out := image.NewRGBA(image.Rect(0, 0, dw, dh)) - Resize(scaleType, src, out) - return out - } -} - -func frame(encoding uint32, src *image.RGBA, data []byte, xx int, hn int, flipV bool, h int, w int, pwb int, bpp int, rot *Rotate) { - var px uint32 - var dst *uint32 - for y, yy, l, lx, row := xx, 0, xx+hn, 0, 0; y < l; y++ { - yy = y - if flipV { - yy = (h - 1) - yy - } - row = yy * src.Stride - lx = y * pwb - for x, k := 0, 0; x < w; x++ { - if rot == nil { - k = x<<2 + row - } else { - dx, dy := rot.Call(x, yy, w, h) - k = dx<<2 + dy*src.Stride - } - dst = (*uint32)(unsafe.Pointer(&src.Pix[k])) - px = *(*uint32)(unsafe.Pointer(&data[x*bpp+lx])) - // LE, BE might not work - switch encoding { - case BitFormatShort565: - i565(dst, px) - case BitFormatInt8888Rev: - ix8888(dst, px) - } - } - } -} - -func i565(dst *uint32, px uint32) { - *dst = ((px >> 8) & 0xf8) | (((px >> 3) & 0xfc) << 8) | (((px << 3) & 0xfc) << 16) + 0xff000000 - // setting the last byte to 255 allows saving RGBA images to PNG not as black squares -} - -func ix8888(dst *uint32, px uint32) { - *dst = ((px >> 16) & 0xff) | (px & 0xff00) | ((px << 16) & 0xff0000) + 0xff000000 -} - -func Clear() { - wg = sync.WaitGroup{} -} diff --git a/pkg/worker/emulator/image/rotation.go b/pkg/worker/emulator/image/rotation.go index 80a2e9ec..950c89a2 100644 --- a/pkg/worker/emulator/image/rotation.go +++ b/pkg/worker/emulator/image/rotation.go @@ -8,14 +8,16 @@ const ( Angle90 Angle180 Angle270 + Flip180 ) // Angles is a helper to choose appropriate rotation based on its angle. -var Angles = [4]Rotate{ +var Angles = [5]Rotate{ Angle0: {Angle: Angle0, Call: Rotate0}, Angle90: {Angle: Angle90, Call: Rotate90, IsEven: true}, Angle180: {Angle: Angle180, Call: Rotate180}, Angle270: {Angle: Angle270, Call: Rotate270, IsEven: true}, + Flip180: {Angle: Flip180, Call: Invert180}, } func GetRotation(angle Angle) Rotate { return Angles[angle] } @@ -61,6 +63,8 @@ func Rotate180(x, y, w, h int) (int, int) { return (w - 1) - x, (h - 1) - y } // 7 8 9 9 6 3 func Rotate270(x, y, _, h int) (int, int) { return (h - 1) - y, x } +func Invert180(x, y, _, h int) (int, int) { return x, (h - 1) - y } + // ExampleRotate is an example of rotation usage. // // [1 2 3 4 5 6 7 8 9] diff --git a/pkg/worker/emulator/libretro/frontend.go b/pkg/worker/emulator/libretro/frontend.go index c5f80a05..4e4b64c6 100644 --- a/pkg/worker/emulator/libretro/frontend.go +++ b/pkg/worker/emulator/libretro/frontend.go @@ -3,7 +3,6 @@ package libretro import ( "errors" "fmt" - "os" "path/filepath" "sync" "sync/atomic" @@ -11,7 +10,9 @@ import ( conf "github.com/giongto35/cloud-game/v2/pkg/config/emulator" "github.com/giongto35/cloud-game/v2/pkg/logger" + "github.com/giongto35/cloud-game/v2/pkg/os" "github.com/giongto35/cloud-game/v2/pkg/worker/emulator" + "github.com/giongto35/cloud-game/v2/pkg/worker/emulator/image" ) type Frontend struct { @@ -28,6 +29,10 @@ type Frontend struct { // draw threads th int + stopped atomic.Bool + + canvas *image.Canvas + done chan struct{} log *logger.Logger @@ -53,6 +58,11 @@ const ( KeyReleased = 0 ) +var ( + noAudio = func(*emulator.GameAudio) {} + noVideo = func(*emulator.GameFrame) {} +) + // NewFrontend implements Emulator interface for a Libretro frontend. func NewFrontend(conf conf.Emulator, log *logger.Logger) (*Frontend, error) { log = log.Extend(log.With().Str("m", "Libretro")) @@ -61,7 +71,7 @@ func NewFrontend(conf conf.Emulator, log *logger.Logger) (*Frontend, error) { // Check if room is on local storage, if not, pull from GCS to local storage log.Info().Msgf("Local storage path: %v", conf.Storage) - if err := os.MkdirAll(conf.Storage, 0755); err != nil && !os.IsExist(err) { + if err := os.CheckCreateDir(conf.Storage); err != nil { return nil, fmt.Errorf("failed to create local storage path: %v, %w", conf.Storage, err) } @@ -69,7 +79,7 @@ func NewFrontend(conf conf.Emulator, log *logger.Logger) (*Frontend, error) { if err != nil { return nil, fmt.Errorf("failed to use emulator path: %v, %w", conf.LocalPath, err) } - if err := os.MkdirAll(path, 0755); err != nil && !os.IsExist(err) { + if err := os.CheckCreateDir(path); err != nil { return nil, fmt.Errorf("failed to create local path: %v, %w", conf.LocalPath, err) } log.Info().Msgf("Emulator save path is %v", path) @@ -88,22 +98,22 @@ func NewFrontend(conf conf.Emulator, log *logger.Logger) (*Frontend, error) { done: make(chan struct{}), th: conf.Threads, log: log, + onAudio: noAudio, + onVideo: noVideo, } return frontend, nil } -func (f *Frontend) Input(player int, data []byte) { f.input.setInput(player, data) } - func (f *Frontend) LoadMetadata(emu string) { - libretroConf := f.conf.GetLibretroCoreConfig(emu) + config := f.conf.GetLibretroCoreConfig(emu) f.mu.Lock() coreLoad(emulator.Metadata{ - LibPath: libretroConf.Lib, - ConfigPath: libretroConf.Config, - IsGlAllowed: libretroConf.IsGlAllowed, - UsesLibCo: libretroConf.UsesLibCo, - HasMultitap: libretroConf.HasMultitap, - AutoGlContext: libretroConf.AutoGlContext, + LibPath: config.Lib, + ConfigPath: config.Config, + IsGlAllowed: config.IsGlAllowed, + UsesLibCo: config.UsesLibCo, + HasMultitap: config.HasMultitap, + AutoGlContext: config.AutoGlContext, }) f.mu.Unlock() } @@ -120,18 +130,22 @@ func (f *Frontend) Start() { defer func() { ticker.Stop() nanoarchShutdown() + f.mu.Lock() + frontend.canvas.Clear() + f.SetAudio(noAudio) + f.SetVideo(noVideo) + f.mu.Unlock() f.log.Debug().Msgf("run loop finished") }() // start time for the first frame lastFrameTime = time.Now().UnixNano() for { - f.mu.Lock() - run() - f.mu.Unlock() select { case <-ticker.C: - continue + f.mu.Lock() + run() + f.mu.Unlock() case <-f.done: return } @@ -150,19 +164,24 @@ func (f *Frontend) GetFps() uint { return uint(nano.sys func (f *Frontend) GetHashPath() string { return f.storage.GetSavePath() } func (f *Frontend) GetSRAMPath() string { return f.storage.GetSRAMPath() } func (f *Frontend) GetSampleRate() uint { return uint(nano.sysAvInfo.timing.sample_rate) } +func (f *Frontend) Input(player int, data []byte) { f.input.setInput(player, data) } func (f *Frontend) LoadGame(path string) error { return LoadGame(path) } func (f *Frontend) LoadGameState() error { return f.Load() } func (f *Frontend) HasVerticalFrame() bool { return nano.rot != nil && nano.rot.IsEven } func (f *Frontend) SaveGameState() error { return f.Save() } func (f *Frontend) SetMainSaveName(name string) { f.storage.SetMainSaveName(name) } -func (f *Frontend) SetViewport(width int, height int) { f.vw, f.vh = width, height } -func (f *Frontend) ToggleMultitap() { toggleMultitap() } +func (f *Frontend) SetViewport(width int, height int) { + f.mu.Lock() + f.vw, f.vh = width, height + size := int(nano.sysAvInfo.geometry.max_width * nano.sysAvInfo.geometry.max_height) + f.canvas = image.NewCanvas(width, height, size) + f.mu.Unlock() +} +func (f *Frontend) ToggleMultitap() { toggleMultitap() } func (f *Frontend) Close() { - f.mu.Lock() - f.SetViewport(0, 0) - f.mu.Unlock() close(f.done) + frontend.stopped.Store(true) nano.reserved <- struct{}{} } diff --git a/pkg/worker/emulator/libretro/nanoarch.go b/pkg/worker/emulator/libretro/nanoarch.go index 3e2bde0f..86f22caf 100644 --- a/pkg/worker/emulator/libretro/nanoarch.go +++ b/pkg/worker/emulator/libretro/nanoarch.go @@ -38,7 +38,7 @@ type ( } video struct { pixFmt uint32 - bpp int + bpp uint hw *C.struct_retro_hw_render_callback isGl bool autoGlContext bool @@ -101,7 +101,21 @@ func Init(localPath string) { } //export coreVideoRefresh -func coreVideoRefresh(data unsafe.Pointer, width C.unsigned, height C.unsigned, pitch C.size_t) { +func coreVideoRefresh(data unsafe.Pointer, width, height uint, packed uint) { + if frontend.stopped.Load() { + libretroLogger.Warn().Msgf(">>> skip video") + return + } + + // some frames can be rendered slower or faster than internal 1/fps core tick + // so track actual frame render time for proper RTP packet timestamps + // (and proper frame display time, for example: 1->1/60=16.6ms, 2->10ms, 3->23ms, 4->16.6ms) + // this is useful only for cores with variable framerate, for the fixed framerate cores this adds stutter + // !to find docs on Libretro refresh sync and frame times + t := time.Now().UnixNano() + dt := t - lastFrameTime + lastFrameTime = t + // some cores can return nothing // !to add duplicate if can dup if data == nil { @@ -109,52 +123,34 @@ func coreVideoRefresh(data unsafe.Pointer, width C.unsigned, height C.unsigned, } // calculate real frame width in pixels from packed data (realWidth >= width) - packedWidth := int(pitch) / nano.v.bpp - if packedWidth < 1 { - packedWidth = int(width) + // some cores or games output zero pitch, i.e. N64 Mupen + if packed == 0 { + packed = width } // calculate space for the video frame - bytes := int(height) * packedWidth * nano.v.bpp + bytes := packed * height - // if Libretro renders frame with OpenGL context - isOpenGLRender := data == C.RETRO_HW_FRAME_BUFFER_VALID var data_ []byte - if isOpenGLRender { - data_ = graphics.ReadFramebuffer(bytes, int(width), int(height)) - } else { + if data != C.RETRO_HW_FRAME_BUFFER_VALID { data_ = unsafe.Slice((*byte)(data), bytes) + } else { + // if Libretro renders frame with OpenGL context + data_ = graphics.ReadFramebuffer(bytes, width, height) } - // the image is being resized and de-rotated - frame := image.DrawRgbaImage( - nano.v.pixFmt, - nano.rot, - image.ScaleNearestNeighbour, - isOpenGLRender, - int(width), int(height), packedWidth, nano.v.bpp, - data_, - frontend.vw, - frontend.vh, - frontend.th, - ) - - t := time.Now().UnixNano() - dt := time.Duration(t - lastFrameTime) - lastFrameTime = t - - if len(frame.Pix) == 0 { - // this should not be happening, will crash yuv - libretroLogger.Error().Msgf("skip empty frame %v", frame.Bounds()) - return - } + // some cores or games have a variable output frame size, i.e. PSX Rearmed + // also we have an option of xN output frame magnification + // so, it may be rescaled fr, _ := videoPool.Get().(*emulator.GameFrame) if fr == nil { fr = &emulator.GameFrame{} } - fr.Data = frame - fr.Duration = dt + fr.Data = frontend.canvas. + Draw(nano.v.pixFmt, nano.rot, int(width), int(height), int(packed), int(nano.v.bpp), data_, frontend.th) + fr.Duration = time.Duration(dt) frontend.onVideo(fr) + frontend.canvas.Put(fr.Data) videoPool.Put(fr) } @@ -189,6 +185,11 @@ func coreInputState(port C.unsigned, device C.unsigned, index C.unsigned, id C.u } func audioWrite(buf unsafe.Pointer, frames C.size_t) C.size_t { + if frontend.stopped.Load() { + libretroLogger.Warn().Msgf(">>> skip audio") + return 0 + } + samples := int(frames) << 1 src := unsafe.Slice((*int16)(buf), samples) dst, _ := audioCopyPool.Get().(*[]int16) @@ -510,8 +511,10 @@ func LoadGame(path string) error { ) if nano.v.isGl { - bufS := int(nano.sysAvInfo.geometry.max_width*nano.sysAvInfo.geometry.max_height) * nano.v.bpp - graphics.SetBuffer(bufS) + // flip Y coordinates of OpenGL + setRotation(uint(image.Flip180)) + bufS := uint(nano.sysAvInfo.geometry.max_width*nano.sysAvInfo.geometry.max_height) * nano.v.bpp + graphics.SetBuffer(int(bufS)) libretroLogger.Info().Msgf("Set buffer: %v", byteCountBinary(int64(bufS))) if usesLibCo { C.bridge_execute(C.initVideo_cgo) @@ -579,7 +582,6 @@ func nanoarchShutdown() { libretroLogger.Error().Err(err).Msg("lib close failed") } coreConfig.Free() - image.Clear() } func run() { diff --git a/pkg/worker/emulator/libretro/nanoarch_test.go b/pkg/worker/emulator/libretro/nanoarch_test.go index 13507aae..876d3e50 100644 --- a/pkg/worker/emulator/libretro/nanoarch_test.go +++ b/pkg/worker/emulator/libretro/nanoarch_test.go @@ -111,7 +111,7 @@ func (emu *EmulatorMock) loadRom(game string) { if err != nil { log.Fatal(err) } - emu.vw, emu.vh = emu.GetFrameSize() + emu.SetViewport(emu.GetFrameSize()) } // shutdownEmulator closes the emulator and cleans its resources. diff --git a/pkg/worker/encoder/encoder.go b/pkg/worker/encoder/encoder.go index 8c20b5fc..9549dce8 100644 --- a/pkg/worker/encoder/encoder.go +++ b/pkg/worker/encoder/encoder.go @@ -2,6 +2,8 @@ package encoder import ( "image" + "sync" + "sync/atomic" "github.com/giongto35/cloud-game/v2/pkg/logger" "github.com/giongto35/cloud-game/v2/pkg/worker/encoder/yuv" @@ -20,12 +22,10 @@ type ( type VideoEncoder struct { encoder Encoder - - y yuv.ImgProcessor - - // frame size - w, h int - log *logger.Logger + log *logger.Logger + stopped atomic.Bool + y yuv.ImgProcessor + mu sync.Mutex } type VideoCodec string @@ -45,10 +45,16 @@ func NewVideoEncoder(enc Encoder, w, h int, concurrency int, log *logger.Logger) if concurrency > 0 { log.Info().Msgf("Use concurrent image processor: %v", concurrency) } - return &VideoEncoder{encoder: enc, y: y, w: w, h: h, log: log} + return &VideoEncoder{encoder: enc, y: y, log: log} } -func (vp VideoEncoder) Encode(img InFrame) OutFrame { +func (vp *VideoEncoder) Encode(img InFrame) OutFrame { + vp.mu.Lock() + defer vp.mu.Unlock() + if vp.stopped.Load() { + return nil + } + yCbCr := vp.y.Process(img) vp.encoder.LoadBuf(yCbCr) vp.y.Put(&yCbCr) @@ -59,11 +65,11 @@ func (vp VideoEncoder) Encode(img InFrame) OutFrame { return nil } -// Start begins video encoding pipe. -// Should be wrapped into a goroutine. -func (vp VideoEncoder) Start() {} +func (vp *VideoEncoder) Stop() { + vp.stopped.Store(true) + vp.mu.Lock() + defer vp.mu.Unlock() -func (vp VideoEncoder) Stop() { if err := vp.encoder.Shutdown(); err != nil { vp.log.Error().Err(err).Msg("failed to close the encoder") } diff --git a/pkg/worker/encoder/h264/x264.go b/pkg/worker/encoder/h264/x264.go index ca18adcb..a33948bd 100644 --- a/pkg/worker/encoder/h264/x264.go +++ b/pkg/worker/encoder/h264/x264.go @@ -80,6 +80,8 @@ func NewEncoder(w, h int, opts *Options) (encoder *H264, err error) { param.IWidth = int32(w) param.IHeight = int32(h) param.ILogLevel = opts.LogLevel + param.ISyncLookahead = 0 + param.IThreads = 1 param.Rc.IRcMethod = RcCrf param.Rc.FRfConstant = float32(opts.Crf) diff --git a/pkg/worker/encoder/yuv/yuv.c b/pkg/worker/encoder/yuv/yuv.c index 6763aa2c..c4d918dc 100644 --- a/pkg/worker/encoder/yuv/yuv.c +++ b/pkg/worker/encoder/yuv/yuv.c @@ -6,7 +6,7 @@ #ifdef Y601_STUDIO // 66*R+129*G+25*B -static __inline int Y(uint8_t *rgb) { +static __inline int Y(uint8_t *__restrict rgb) { int R = *rgb; int G = *(rgb+1); int B = *(rgb+2); @@ -14,7 +14,7 @@ static __inline int Y(uint8_t *rgb) { } // 112*B-38*R-74G -static __inline int U(uint8_t *rgb) { +static __inline int U(uint8_t *__restrict rgb) { int R = *rgb; int G = *(rgb+1); int B = *(rgb+2); @@ -22,7 +22,7 @@ static __inline int U(uint8_t *rgb) { } // 112*R-94*G-18*B -static __inline int V(uint8_t *rgb) { +static __inline int V(uint8_t *__restrict rgb) { int R = 56**(rgb); int G = 47**(rgb+1); int B = *(rgb+2); @@ -62,9 +62,9 @@ static __inline int V(uint8_t *rgb) { static const int Y_MIN = 0; #endif -static __inline void _y(uint8_t *p, uint8_t *y, int size) { +static __inline void _y(uint8_t *__restrict p, uint8_t *__restrict y, int size) { do { - *y++ = Y_MIN + Y(p); + *y++ = Y(p) + Y_MIN; p += 4; } while (--size); } @@ -73,7 +73,7 @@ static __inline void _y(uint8_t *p, uint8_t *y, int size) { // X X X X // O O // X X X X -static __inline void _4uv(uint8_t *p, uint8_t *u, uint8_t *v, const int w, const int h) { +static __inline void _4uv(uint8_t * __restrict p, uint8_t * __restrict u, uint8_t * __restrict v, const int w, const int h) { uint8_t *p2, *p3, *p4; const int row = w << 2; const int next = 4; @@ -99,13 +99,13 @@ static __inline void _4uv(uint8_t *p, uint8_t *u, uint8_t *v, const int w, const x -= 2; } p += row; - y -=2; + y -= 2; x = w; } } // Converts RGBA image to YUV (I420) with BT.601 studio color range. -void rgbaToYuv(void *destination, void *source, const int w, const int h) { +void rgbaToYuv(void *__restrict destination, void *__restrict source, const int w, const int h) { const int image_size = w * h; uint8_t *src = source; uint8_t *dst_y = destination; @@ -113,16 +113,16 @@ void rgbaToYuv(void *destination, void *source, const int w, const int h) { uint8_t *dst_v = destination + image_size + image_size / 4; _y(src, dst_y, image_size); src = source; - _4uv(src, dst_u, dst_v, w, h); + _4uv(source, dst_u, dst_v, w, h); } -void luma(void *destination, void *source, const int pos, const int w, const int h) { +void luma(void *__restrict destination, void *__restrict source, const int pos, const int w, const int h) { uint8_t *rgba = source + 4 * pos; uint8_t *dst = destination + pos; _y(rgba, dst, w*h); } -void chroma(void *dst, void *source, const int pos, const int deu, const int dev, const int w, const int h) { +void chroma(void *__restrict dst, void *__restrict source, const int pos, const int deu, const int dev, const int w, const int h) { uint8_t *src = source + 4 * pos; uint8_t *dst_u = dst + deu + pos / 4; uint8_t *dst_v = dst + dev + pos / 4; diff --git a/pkg/worker/media.go b/pkg/worker/media.go index 6767047e..fc579e3b 100644 --- a/pkg/worker/media.go +++ b/pkg/worker/media.go @@ -132,7 +132,7 @@ func (r *Room) initVideo(width, height int, conf conf.Video) { r.vEncoder = encoder.NewVideoEncoder(enc, width, height, conf.Concurrency, r.log) r.emulator.SetVideo(func(frame *emulator.GameFrame) { - if fr := r.vEncoder.Encode(frame.Data); fr != nil { + if fr := r.vEncoder.Encode(frame.Data.RGBA); fr != nil { r.handleSample(fr, frame.Duration, func(u *Session, s *webrtc.Sample) { _ = u.SendVideo(s) }) } }) diff --git a/pkg/worker/media_test.go b/pkg/worker/media_test.go index 8ec9c270..c4e772c0 100644 --- a/pkg/worker/media_test.go +++ b/pkg/worker/media_test.go @@ -47,6 +47,7 @@ func run(w, h int, cod encoder.VideoCodec, count int, a *image.RGBA, b *image.RG enc, _ = vpx.NewEncoder(w, h, nil) } + logger.SetGlobalLevel(logger.Disabled) ve := encoder.NewVideoEncoder(enc, w, h, 8, l) defer ve.Stop() diff --git a/pkg/worker/recording.go b/pkg/worker/recording.go index 8efa30a4..c3b890af 100644 --- a/pkg/worker/recording.go +++ b/pkg/worker/recording.go @@ -12,8 +12,6 @@ type RecordingRoom struct { } func WithRecording(room GamingRoom, rec bool, recUser string, game string, conf worker.Config) *RecordingRoom { - room.GetLog().Info().Msgf("RECORD: %v %v", rec, recUser) - rr := &RecordingRoom{GamingRoom: room, rec: recorder.NewRecording( recorder.Meta{UserName: recUser}, room.GetLog(), @@ -57,7 +55,7 @@ func (r *RecordingRoom) ToggleRecording(active bool, user string) { if r.rec == nil { return } - r.GetLog().Debug().Msgf("[REC] set: %v, %v", active, user) + r.GetLog().Debug().Msgf("[REC] set: %v, user: %v", active, user) r.rec.Set(active, user) } diff --git a/pkg/worker/room.go b/pkg/worker/room.go index c3b9aba0..f64ef440 100644 --- a/pkg/worker/room.go +++ b/pkg/worker/room.go @@ -62,18 +62,18 @@ func NewRoom(id string, game games.GameMetadata, onClose func(*Room), conf worke log.Fatal().Err(err).Msgf("couldn't load the game %v", game) } // calc output frame size and rotation - fw, fh := room.emulator.GetFrameSize() - w, h := room.whatsFrame(conf.Emulator, fw, fh) + w, h := room.whatsFrame(conf.Emulator) if room.emulator.HasVerticalFrame() { w, h = h, w } room.emulator.SetViewport(w, h) - log.Info().Str("game", game.Name).Msg("The room is open") - room.initVideo(w, h, conf.Encoder.Video) room.initAudio(int(room.emulator.GetSampleRate()), conf.Encoder.Audio) - log.Info().Str("room", room.GetId()).Msg("New room") + + log.Info().Str("room", room.GetId()). + Str("game", game.Name). + Msg("New room") return room } @@ -110,7 +110,8 @@ func (r *Room) EnableAutosave(periodSec int) { } } -func (r *Room) whatsFrame(conf conf.Emulator, w, h int) (ww int, hh int) { +func (r *Room) whatsFrame(conf conf.Emulator) (ww int, hh int) { + w, h := r.emulator.GetFrameSize() // nwidth, nheight are the WebRTC output size var nwidth, nheight int emu, ar := conf, conf.AspectRatio diff --git a/pkg/worker/room_test.go b/pkg/worker/room_test.go index 87d743dd..ea345c1f 100644 --- a/pkg/worker/room_test.go +++ b/pkg/worker/room_test.go @@ -8,7 +8,6 @@ import ( "image/color" "image/draw" "image/png" - "io" "log" "os" "path/filepath" @@ -23,6 +22,7 @@ import ( "github.com/giongto35/cloud-game/v2/pkg/games" "github.com/giongto35/cloud-game/v2/pkg/logger" "github.com/giongto35/cloud-game/v2/pkg/worker/emulator" + image2 "github.com/giongto35/cloud-game/v2/pkg/worker/emulator/image" "github.com/giongto35/cloud-game/v2/pkg/worker/emulator/libretro/manager/remotehttp" "github.com/giongto35/cloud-game/v2/pkg/worker/encoder" "github.com/giongto35/cloud-game/v2/pkg/worker/thread" @@ -49,6 +49,7 @@ type roomMockConfig struct { vCodec encoder.VideoCodec autoGlContext bool dontStartEmulator bool + noLog bool } // Store absolute path to test games @@ -180,7 +181,7 @@ func TestAllEmulatorRooms(t *testing.T) { } } -func dumpCanvas(frame *image.RGBA, name string, caption string, path string) { +func dumpCanvas(frame *image2.Frame, name string, caption string, path string) { // slap 'em caption if len(caption) > 0 { draw.Draw(frame, image.Rect(8, 8, 8+len(caption)*7+3, 24), &image.Uniform{C: color.RGBA{}}, image.Point{}, draw.Src) @@ -225,6 +226,9 @@ func getRoomMock(cfg roomMockConfig) roomMock { } fixEmulators(&conf, cfg.autoGlContext) l := logger.NewConsole(conf.Worker.Debug, "w", true) + if cfg.noLog { + logger.SetGlobalLevel(logger.Disabled) + } // sync cores coreManager := remotehttp.NewRemoteHttpManager(conf.Emulator.Libretro, l) @@ -288,12 +292,16 @@ func waitNFrames(n int, room roomMock) *emulator.GameFrame { var i = int32(n) wg := sync.WaitGroup{} wg.Add(n) - var frame *emulator.GameFrame + var frame emulator.GameFrame handler := room.emulator.GetVideo() room.emulator.SetVideo(func(video *emulator.GameFrame) { handler(video) if atomic.AddInt32(&i, -1) >= 0 { - frame = video + v := video.Data.Copy() + frame = emulator.GameFrame{ + Duration: video.Duration, + Data: &v, + } wg.Done() } }) @@ -301,22 +309,18 @@ func waitNFrames(n int, room roomMock) *emulator.GameFrame { room.StartEmulator() } wg.Wait() - return frame + return &frame } // benchmarkRoom measures app performance for n emulation frames. // Measure period: the room initialization, n emulated and encoded frames, the room shutdown. func benchmarkRoom(rom games.GameMetadata, codec encoder.VideoCodec, frames int, suppressOutput bool, b *testing.B) { - if suppressOutput { - log.SetOutput(io.Discard) - os.Stdout, _ = os.Open(os.DevNull) - } - for i := 0; i < b.N; i++ { room := getRoomMock(roomMockConfig{ gamesPath: whereIsGames, game: rom, vCodec: codec, + noLog: suppressOutput, }) waitNFrames(frames, room) room.Close()