mirror of
https://github.com/giongto35/cloud-game.git
synced 2026-01-23 02:34:42 +00:00
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:
parent
c641065564
commit
2b81c3fb87
27 changed files with 373 additions and 232 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
4
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
|
||||
|
|
|
|||
18
go.sum
18
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=
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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") })
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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]))
|
||||
|
|
|
|||
167
pkg/worker/emulator/image/canvas.go
Normal file
167
pkg/worker/emulator/image/canvas.go
Normal 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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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{}
|
||||
}
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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{}{}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) })
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue