Some optimizations (#387)

- Fixed broken image cache for the first stage RGBA frames. It was not a thread-safe one, which led to image tearing (parts of old images in multiple consecutive frames).
- 180 flip function for the OpenGL coordinate system has been moved into the rotation part.
- Optimized YUV converter.
- Optimized color converters:
  - Use __restrict pointers.
  - Draw image pixels with the faster bitwise operators and pointer arithmetic as 32/16bit LE numbers (may break on ARM devices like RPi). 
  - Pass uints for less num conversions.
  - Much faster XRGB -> RGBA conversion with Go's stdlib 32bit flip.
- Wrapped RGBA images into a custom struct in order to bypass opacity tests for the standard Go png functions, which needed for the PNG file export. Before that we set RGBx opacity byte explicitly during the pixel format conversions (much slower).
- Made Libretro core shutdown more deterministic. When we run a C core separately from the main Go process we have to make sure that the C core is not doing anything in its syscall while we stopping the emulator. Basically, a blocking call may be suspended on the Go's side while the other goroutines have no knowledge of that.
- Less info level logs.
- Added recording user label.
- Enabled RTCP sender reports by default, which may help with A/V sync.
- Check onMessage webrtc handler if it's set. May crash the program if not.
- Fixed some make dirs permissions.
- Enabled console colors (since MS has finally fixed their bloody Terminal).
- Disable log in some tests.
- Updated deps.
This commit is contained in:
sergystepanov 2023-01-31 22:22:03 +03:00 committed by GitHub
parent c641065564
commit 2b81c3fb87
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 373 additions and 232 deletions

View file

@ -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)

View file

@ -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 {

4
go.mod
View file

@ -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

18
go.sum
View file

@ -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=

View file

@ -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 }

View file

@ -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)

View file

@ -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.

View file

@ -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") })

View file

@ -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
}

View file

@ -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)

View file

@ -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 {

View file

@ -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]))

View file

@ -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
}

View file

@ -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)
}
})
}
}

View file

@ -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{}
}

View file

@ -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]

View file

@ -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{}{}
}

View file

@ -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() {

View file

@ -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.

View file

@ -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")
}

View file

@ -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)

View file

@ -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;

View file

@ -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) })
}
})

View file

@ -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()

View file

@ -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)
}

View file

@ -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

View file

@ -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()