diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bde12597..65e92ea1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,9 +1,8 @@ -# ------------------------------------------------------------------------ -# Build workflow for multiple OSes (Linux x64, macOS x64, Windows x64) -# ------------------------------------------------------------------------ +# ------------------------------------------------------------ +# Build workflow (Linux x64, macOS x64, Windows x64) +# ------------------------------------------------------------ name: build -# run only when pushing into the master only on: push: branches: @@ -13,34 +12,27 @@ on: pull_request: branches: - master -env: - go-version: 1.14 + jobs: + build: name: Build strategy: matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ ubuntu-latest, macos-latest, windows-latest ] runs-on: ${{ matrix.os }} steps: - - name: Get the source - uses: actions/checkout@v2 - - name: Set up Go - uses: actions/setup-go@v1 + - uses: actions/checkout@v2 + + - uses: actions/setup-go@v2 with: - go-version: ${{ env.go-version }} - - name: Set up Go environment - shell: bash - # add Go's bin folder into environment (to be able to call its tools) - run: | - echo "::set-env name=GOPATH::$(go env GOPATH)" - echo "::add-path::$(go env GOPATH)/bin" + go-version: ^1.15 - name: Get Linux dev libraries and tools if: matrix.os == 'ubuntu-latest' run: | sudo apt-get update - sudo apt-get install -y make pkg-config libvpx-dev libopus-dev libopusfile-dev libsdl2-dev + sudo apt-get install -y make pkg-config libvpx-dev libopus-dev libopusfile-dev libsdl2-dev libgl1-mesa-glx - name: Get MacOS dev libraries and tools if: matrix.os == 'macos-latest' @@ -54,9 +46,16 @@ jobs: msystem: MINGW64 path-type: inherit update: true + install: > + mingw-w64-x86_64-gcc + mingw-w64-x86_64-pkg-config + mingw-w64-x86_64-dlfcn + mingw-w64-x86_64-libvpx + mingw-w64-x86_64-opusfile + mingw-w64-x86_64-SDL2 - name: Load Go modules maybe? - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} @@ -66,14 +65,14 @@ jobs: - name: Build Windows app if: matrix.os == 'windows-latest' shell: msys2 {0} - run: > - pacman -S --noconfirm --needed make - mingw-w64-x86_64-gcc - mingw-w64-x86_64-pkg-config - mingw-w64-x86_64-dlfcn - mingw-w64-x86_64-libvpx - mingw-w64-x86_64-opusfile - mingw-w64-x86_64-SDL2 + run: | + wget -q https://github.com/pal1000/mesa-dist-win/releases/download/20.2.1/mesa3d-20.2.1-release-mingw.7z + "/c/Program Files/7-Zip/7z.exe" x mesa3d-20.2.1-release-mingw.7z -omesa + echo -e " 2\r\n 8\r\n " >> commands + ./mesa/systemwidedeploy.cmd < ./commands + + wget -q https://buildbot.libretro.com/nightly/windows/x86_64/latest/mupen64plus_next_libretro.dll.zip + "/c/Program Files/7-Zip/7z.exe" x mupen64plus_next_libretro.dll.zip -oassets/emulator/libretro/cores make build @@ -87,6 +86,30 @@ jobs: run: | make build + - name: Verify core rendering (windows-latest) + if: matrix.os == 'windows-latest' && always() + shell: msys2 {0} + env: + MESA_GL_VERSION_OVERRIDE: 3.3COMPAT + run: | + go test -run TestAllEmulatorRooms ./pkg/worker/room -v -renderFrames -autoGlContext -outputPath "../../../_rendered" + + - name: Verify core rendering (ubuntu-latest) + if: matrix.os == 'ubuntu-latest' && always() + env: + MESA_GL_VERSION_OVERRIDE: 3.3COMPAT + run: | + xvfb-run --auto-servernum go test -run TestAllEmulatorRooms ./pkg/worker/room -v -renderFrames -autoGlContext -outputPath "../../../_rendered" + + - name: Verify core rendering (macos-latest) + if: matrix.os == 'macos-latest' && always() + run: | + go test -run TestAllEmulatorRooms ./pkg/worker/room -v -renderFrames -outputPath "../../../_rendered" + + - uses: actions/upload-artifact@v2 + with: + path: _rendered/*.png + docker_build_check: name: Build (docker) runs-on: ubuntu-latest diff --git a/.github/workflows/docker_publish.yml b/.github/workflows/docker_publish.yml.disabled similarity index 100% rename from .github/workflows/docker_publish.yml rename to .github/workflows/docker_publish.yml.disabled diff --git a/.github/workflows/release.yml.disabled b/.github/workflows/release.yml.disabled index 5fe98f63..af3e64ed 100644 --- a/.github/workflows/release.yml.disabled +++ b/.github/workflows/release.yml.disabled @@ -24,7 +24,7 @@ on: tags: - 'v*' env: - go-version: 1.14 + go-version: 1.15 app-name: cloud-game app-arch: x86_64 jobs: @@ -41,15 +41,9 @@ jobs: - name: Get the source uses: actions/checkout@v2 - name: Set up Go - uses: actions/setup-go@v1 + uses: actions/setup-go@v2 with: go-version: ${{ env.go-version }} - - name: Set up Go environment - shell: bash - # add Go's bin folder into environment (to be able to call its tools) - run: | - echo "::set-env name=GOPATH::$(go env GOPATH)" - echo "::add-path::$(go env GOPATH)/bin" - name: Get Linux dev libraries and tools if: matrix.os == 'ubuntu-latest' @@ -71,7 +65,7 @@ jobs: update: true - name: Load Go modules maybe? - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} diff --git a/assets/games/Sample Demo by Florian (PD).z64 b/assets/games/Sample Demo by Florian (PD).z64 new file mode 100644 index 00000000..122d8d7f Binary files /dev/null and b/assets/games/Sample Demo by Florian (PD).z64 differ diff --git a/cmd/worker/main.go b/cmd/worker/main.go index 073d615e..999b6828 100644 --- a/cmd/worker/main.go +++ b/cmd/worker/main.go @@ -7,8 +7,8 @@ import ( "os/signal" "time" - "github.com/faiface/mainthread" config "github.com/giongto35/cloud-game/v2/pkg/config/worker" + "github.com/giongto35/cloud-game/v2/pkg/thread" "github.com/giongto35/cloud-game/v2/pkg/util/logging" "github.com/giongto35/cloud-game/v2/pkg/worker" "github.com/golang/glog" @@ -45,6 +45,5 @@ func run() { } func main() { - // enables mainthread package and runs run in a separate goroutine - mainthread.Run(run) + thread.MainWrapMaybe(run) } diff --git a/go.mod b/go.mod index 4ec5798f..5ecfa87c 100644 --- a/go.mod +++ b/go.mod @@ -3,33 +3,36 @@ module github.com/giongto35/cloud-game/v2 go 1.13 require ( - cloud.google.com/go v0.67.0 // indirect + cloud.google.com/go v0.70.0 // indirect cloud.google.com/go/storage v1.12.0 - github.com/disintegration/imaging v1.6.2 github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3 github.com/fsnotify/fsnotify v1.4.9 github.com/gen2brain/x264-go v0.0.0-20200605131102-0523307cbe23 github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7 github.com/gofrs/uuid v3.3.0+incompatible github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b - github.com/google/uuid v1.1.2 // indirect github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.4.2 + github.com/lucas-clemente/quic-go v0.18.1 // indirect github.com/marten-seemann/qtls-go1-15 v0.1.1 // indirect github.com/pion/dtls/v2 v2.0.3 // indirect github.com/pion/quic v0.1.4 // indirect github.com/pion/sctp v1.7.11 // indirect github.com/pion/srtp v1.5.2 // indirect + github.com/pion/turn/v2 v2.0.5 // indirect github.com/pion/webrtc/v2 v2.2.26 - github.com/prometheus/client_golang v1.7.1 - github.com/prometheus/common v0.14.0 // indirect - github.com/prometheus/procfs v0.2.0 // indirect + github.com/prometheus/client_golang v1.8.0 github.com/spf13/pflag v1.0.5 github.com/veandco/go-sdl2 v0.4.4 - golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 + golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 - golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c // indirect - golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d // indirect - google.golang.org/genproto v0.0.0-20201002142447-3860012362da // indirect - gopkg.in/hraban/opus.v2 v2.0.0-20200710132758-e28f8214483b + golang.org/x/net v0.0.0-20201031054903-ff519b6c9102 // indirect + golang.org/x/sys v0.0.0-20201101102859-da207088b7d1 // indirect + golang.org/x/text v0.3.4 // indirect + golang.org/x/tools v0.0.0-20201031021630-582c62ec74d0 // indirect + google.golang.org/api v0.34.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20201030142918-24207fddd1c3 // indirect + google.golang.org/grpc v1.33.1 // indirect + gopkg.in/hraban/opus.v2 v2.0.0-20201025103112-d779bb1cc5a2 ) diff --git a/go.sum b/go.sum index ad8e12cd..81d17ff9 100644 --- a/go.sum +++ b/go.sum @@ -17,8 +17,8 @@ cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOY cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.66.0 h1:DZeAkuQGQqnm9Xv36SbMJEU8aFBz4wL04UpMWPWwjzg= cloud.google.com/go v0.66.0/go.mod h1:dgqGAjKCDxyhGTtC9dAREQGUJpkceNm1yt590Qno0Ko= -cloud.google.com/go v0.67.0 h1:YIkzmqUfVGiGPpT98L8sVvUIkDno6UlrDxw4NR6z5ak= -cloud.google.com/go v0.67.0/go.mod h1:YNan/mUhNZFrYUor0vqrsQ0Ffl7Xtm/ACOy/vsTS858= +cloud.google.com/go v0.70.0 h1:ujhG1RejZYi+HYfJNlgBh3j/bVKD8DewM7AkJ5UPyBc= +cloud.google.com/go v0.70.0/go.mod h1:/UTKYRQTWjVnSe7nGvoSzxEFUELzSI/yAYd0JQT6cRo= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -100,8 +100,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= -github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= @@ -181,6 +179,8 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -209,6 +209,7 @@ github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201009210932-67992a1a5a35/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= @@ -261,6 +262,7 @@ github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= @@ -294,6 +296,8 @@ github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9 h1:tbuodU github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw= github.com/lucas-clemente/quic-go v0.18.0 h1:JhQDdqxdwdmGdKsKgXi1+coHRoGhvU6z0rNzOJqZ/4o= github.com/lucas-clemente/quic-go v0.18.0/go.mod h1:yXttHsSNxQi8AWijC/vLP+OJczXqzHSOcJrM5ITUlCg= +github.com/lucas-clemente/quic-go v0.18.1 h1:DMR7guC0NtVS8zNZR3IO7NARZvZygkSC56GGtC6cyys= +github.com/lucas-clemente/quic-go v0.18.1/go.mod h1:yXttHsSNxQi8AWijC/vLP+OJczXqzHSOcJrM5ITUlCg= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -416,6 +420,8 @@ github.com/pion/transport v0.10.1 h1:2W+yJT+0mOQ160ThZYUx5Zp2skzshiNgxrNE9GUfhJM github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A= github.com/pion/turn/v2 v2.0.4 h1:oDguhEv2L/4rxwbL9clGLgtzQPjtuZwCdoM7Te8vQVk= github.com/pion/turn/v2 v2.0.4/go.mod h1:1812p4DcGVbYVBTiraUmP50XoKye++AMkbfp+N27mog= +github.com/pion/turn/v2 v2.0.5 h1:iwMHqDfPEDEOFzwWKT56eFmh6DYC6o/+xnLAEzgISbA= +github.com/pion/turn/v2 v2.0.5/go.mod h1:APg43CFyt/14Uy7heYUOGWdkem/Wu4PhCO/bjyrTqMw= github.com/pion/udp v0.1.0 h1:uGxQsNyrqG3GLINv36Ff60covYmfrLoxzwnCsIYspXI= github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths= github.com/pion/webrtc/v2 v2.2.26 h1:01hWE26pL3LgqfxvQ1fr6O4ZtyRFFJmQEZK39pHWfFc= @@ -436,6 +442,8 @@ github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5Fsn github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.8.0 h1:zvJNkoCFAnYFNC24FV8nW4JdRJ3GIFcLbg65lL/JDcw= +github.com/prometheus/client_golang v1.8.0/go.mod h1:O9VU6huf47PktckDQfMTX0Y8tY0/7TSWwj+ITvv0TnM= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= @@ -543,6 +551,8 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= @@ -568,8 +578,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE= -golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -582,8 +592,6 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U= -golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -651,9 +659,10 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA= golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c h1:dk0ukUIHmGHqASjP0iue2261isepFCC6XRCSd1nHgDw= -golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c/go.mod h1:iQL9McJNjoIa5mjH6nYTCTZXUN6RP+XW3eib7Ya3XcI= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102 h1:42cLlJJdEh+ySyeUUbEQ5bsTiq8voBeTuweGVkY6Puw= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -673,6 +682,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -723,6 +733,9 @@ golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201101102859-da207088b7d1 h1:a/mKvvZr9Jcc8oKfcmgzyp7OwF73JPWsQLvH1z2Kxck= +golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -730,6 +743,8 @@ golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -787,9 +802,9 @@ golang.org/x/tools v0.0.0-20200915173823-2db8f0ff891c h1:AQsh/7arPVFDBraQa8x7GoV golang.org/x/tools v0.0.0-20200915173823-2db8f0ff891c/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20200918232735-d647fc253266 h1:k7tVuG0g1JwmD3Jh8oAl1vQ1C3jb4Hi/dUl1wWDBJpQ= golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= -golang.org/x/tools v0.0.0-20200929161345-d7fc70abf50f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= -golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d h1:vWQvJ/Z0Lu+9/8oQ/pAYXNzbc7CMnBl+tULGVHOy3oE= -golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/tools v0.0.0-20201017001424-6003fad69a88/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/tools v0.0.0-20201031021630-582c62ec74d0 h1:obBdJPIfkOi5/rVh102giHaq0G8BZGE4eGB+NU6SgBo= +golang.org/x/tools v0.0.0-20201031021630-582c62ec74d0/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= @@ -820,6 +835,9 @@ google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz513 google.golang.org/api v0.31.0/go.mod h1:CL+9IBCa2WWU6gRuBWaKqGWLFFwbEUXkfeMkHLQWYWo= google.golang.org/api v0.32.0 h1:Le77IccnTqEa8ryp9wIpX5W3zYm7Gf9LhOp9PHcwFts= google.golang.org/api v0.32.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.33.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.34.0 h1:k40adF3uR+6x/+hO5Dh4ZFUqFp67vxvbpafFiJxl10A= +google.golang.org/api v0.34.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -830,6 +848,8 @@ google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= @@ -868,9 +888,9 @@ google.golang.org/genproto v0.0.0-20200831141814-d751682dd103/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200914193844-75d14daec038/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200921151605-7abf4a1a14d5/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200929141702-51c3e5b607fe/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201002142447-3860012362da h1:DTQYk4u7nICKkkVZsBv0/0po0ChISxAJ5CTAfUhO0PQ= -google.golang.org/genproto v0.0.0-20201002142447-3860012362da/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201030142918-24207fddd1c3 h1:sg8vLDNIxFPHTchfhH1E3AI32BL3f23oie38xUWnJM8= +google.golang.org/genproto v0.0.0-20201030142918-24207fddd1c3/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -894,6 +914,8 @@ google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.32.0 h1:zWTV+LMdc3kaiJMSTOFz2UgSBgx8RNQoTGiZu3fR9S0= google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1 h1:DGeFlSan2f+WEtCERJ4J9GJWk15TxUi8QGagfI87Xyc= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -915,8 +937,8 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= -gopkg.in/hraban/opus.v2 v2.0.0-20200710132758-e28f8214483b h1:ThVo35Ms4RdZape8OwJFcICfT0+oQ2iVn7yGXRDwA08= -gopkg.in/hraban/opus.v2 v2.0.0-20200710132758-e28f8214483b/go.mod h1:/L5E7a21VWl8DeuCPKxQBdVG5cy+L0MRZ08B1wnqt7g= +gopkg.in/hraban/opus.v2 v2.0.0-20201025103112-d779bb1cc5a2 h1:sxrRNhZ+cNxxLwPw/vV8gNsz+bbqRQiZHBYBJfpyNoQ= +gopkg.in/hraban/opus.v2 v2.0.0-20201025103112-d779bb1cc5a2/go.mod h1:/L5E7a21VWl8DeuCPKxQBdVG5cy+L0MRZ08B1wnqt7g= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= diff --git a/pkg/config/config.go b/pkg/config/config.go index faf392d4..7f9c6323 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -4,7 +4,7 @@ import ( "flag" "time" - "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/image" + "github.com/giongto35/cloud-game/v2/pkg/emulator/image" ) const DefaultSTUNTURN = `[{"urls":"stun:stun-turn.webgame2d.com:3478"},{"urls":"turn:stun-turn.webgame2d.com:3478","username":"root","credential":"root"}]` @@ -25,12 +25,9 @@ var HttpsKey = flag.String("httpsKey", "", "Https Key") var HttpsChain = flag.String("httpsChain", "", "Https Chain") var WSWait = 20 * time.Second -var MatchWorkerRandom = false var ProdEnv = "prod" var StagingEnv = "staging" -const NumKeys = 10 - var FileTypeToEmulator = map[string]string{ "gba": "gba", "gbc": "gba", @@ -64,6 +61,7 @@ type EmulatorMeta struct { Rotation image.Rotate IsGlAllowed bool UsesLibCo bool + AutoGlContext bool HasMultitap bool } diff --git a/pkg/emulator/type.go b/pkg/emulator/emulator.go similarity index 88% rename from pkg/emulator/type.go rename to pkg/emulator/emulator.go index 97ce5437..daa2a289 100644 --- a/pkg/emulator/type.go +++ b/pkg/emulator/emulator.go @@ -7,10 +7,11 @@ type CloudEmulator interface { // LoadMeta returns meta data of emulator. Refer below LoadMeta(path string) config.EmulatorMeta // Start is called after LoadGame - - SetViewport(width int, height int) - Start() + // SetViewport sets viewport size + SetViewport(width int, height int) + // GetViewport debug encoder image + GetViewport() interface{} // SaveGame save game state, saveExtraFunc is callback to do extra step. Ex: save to google cloud SaveGame(saveExtraFunc func() error) error // LoadGame load game state diff --git a/pkg/emulator/graphics/context.go b/pkg/emulator/graphics/context.go new file mode 100644 index 00000000..6ac446c7 --- /dev/null +++ b/pkg/emulator/graphics/context.go @@ -0,0 +1,18 @@ +package graphics + +import "math" + +type Context int + +const ( + CtxNone Context = iota + CtxOpenGl + CtxOpenGlEs2 + CtxOpenGlCore + CtxOpenGlEs3 + CtxOpenGlEsVersion + CtxVulkan + + CtxUnknown = math.MaxInt32 - 1 + CtxDummy = math.MaxInt32 +) diff --git a/pkg/emulator/graphics/opengl.go b/pkg/emulator/graphics/opengl.go new file mode 100644 index 00000000..ad2ebeda --- /dev/null +++ b/pkg/emulator/graphics/opengl.go @@ -0,0 +1,151 @@ +package graphics + +import ( + "log" + "unsafe" + + "github.com/go-gl/gl/v2.1/gl" +) + +type offscreenSetup struct { + tex uint32 + fbo uint32 + rbo uint32 + + width int32 + height int32 + + pixType uint32 + pixFormat uint32 + + hasDepth bool + hasStencil bool +} + +var opt = offscreenSetup{} + +// OpenGL pixel format +type PixelFormat int + +const ( + UnsignedShort5551 PixelFormat = iota + UnsignedShort565 + UnsignedInt8888Rev +) + +func initContext(getProcAddr func(name string) unsafe.Pointer) { + if err := gl.InitWithProcAddrFunc(getProcAddr); err != nil { + panic(err) + } +} + +func initFramebuffer(w int, h int, hasDepth bool, hasStencil bool) { + opt.width = int32(w) + opt.height = int32(h) + opt.hasDepth = hasDepth + opt.hasStencil = hasStencil + + // texture init + gl.GenTextures(1, &opt.tex) + if opt.tex < 0 { + log.Printf("[OpenGL] GenTextures: 0x%X", opt.tex) + panic("OpenGL texture initialization has failed") + } + + gl.BindTexture(gl.TEXTURE_2D, opt.tex) + + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) + + gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, opt.width, opt.height, 0, opt.pixType, opt.pixFormat, nil) + gl.BindTexture(gl.TEXTURE_2D, 0) + + // framebuffer init + gl.GenFramebuffers(1, &opt.fbo) + gl.BindFramebuffer(gl.FRAMEBUFFER, opt.fbo) + + gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, opt.tex, 0) + + // more buffers init + if opt.hasDepth { + gl.GenRenderbuffers(1, &opt.rbo) + gl.BindRenderbuffer(gl.RENDERBUFFER, opt.rbo) + if opt.hasStencil { + gl.RenderbufferStorage(gl.RENDERBUFFER, gl.DEPTH24_STENCIL8, opt.width, opt.height) + gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, opt.rbo) + } else { + gl.RenderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT24, opt.width, opt.height) + gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, opt.rbo) + } + gl.BindRenderbuffer(gl.RENDERBUFFER, 0) + } + + status := gl.CheckFramebufferStatus(gl.FRAMEBUFFER) + if status != gl.FRAMEBUFFER_COMPLETE { + if e := gl.GetError(); e != gl.NO_ERROR { + log.Printf("[OpenGL] GL error: 0x%X, Frame status: 0x%X", e, status) + panic("OpenGL error") + } + log.Printf("[OpenGL] frame status: 0x%X", status) + panic("OpenGL framebuffer is invalid") + } +} + +func destroyFramebuffer() { + if opt.hasDepth { + gl.DeleteRenderbuffers(1, &opt.rbo) + } + gl.DeleteFramebuffers(1, &opt.fbo) + gl.DeleteTextures(1, &opt.tex) +} + +func ReadFramebuffer(bytes int, w int, h int) []byte { + data := make([]byte, bytes) + gl.BindFramebuffer(gl.FRAMEBUFFER, opt.fbo) + gl.ReadPixels(0, 0, int32(w), int32(h), opt.pixType, opt.pixFormat, gl.Ptr(&data[0])) + gl.BindFramebuffer(gl.FRAMEBUFFER, 0) + return data +} + +func getFbo() uint32 { + return opt.fbo +} + +func SetPixelFormat(format PixelFormat) { + switch format { + case UnsignedShort5551: + opt.pixFormat = gl.UNSIGNED_SHORT_5_5_5_1 + opt.pixType = gl.BGRA + break + case UnsignedShort565: + opt.pixFormat = gl.UNSIGNED_SHORT_5_6_5 + opt.pixType = gl.RGB + break + case UnsignedInt8888Rev: + opt.pixFormat = gl.UNSIGNED_INT_8_8_8_8_REV + opt.pixType = gl.BGRA + break + default: + log.Fatalf("[opengl] Error! Unknown pixel type %v", format) + } +} + +// PrintDriverInfo prints OpenGL information. +func PrintDriverInfo() { + // OpenGL info + log.Printf("[OpenGL] Version: %v", get(gl.VERSION)) + log.Printf("[OpenGL] Vendor: %v", get(gl.VENDOR)) + // This string is often the name of the GPU. + // In the case of Mesa3d, it would be i.e "Gallium 0.4 on NVA8". + // It might even say "Direct3D" if the Windows Direct3D wrapper is being used. + log.Printf("[OpenGL] Renderer: %v", get(gl.RENDERER)) + log.Printf("[OpenGL] GLSL Version: %v", get(gl.SHADING_LANGUAGE_VERSION)) +} + +func getDriverError() uint32 { + return gl.GetError() +} + +func get(name uint32) string { + return gl.GoStr(gl.GetString(name)) +} diff --git a/pkg/emulator/graphics/sdl.go b/pkg/emulator/graphics/sdl.go new file mode 100644 index 00000000..38c03d0b --- /dev/null +++ b/pkg/emulator/graphics/sdl.go @@ -0,0 +1,135 @@ +package graphics + +import ( + "log" + "unsafe" + + "github.com/giongto35/cloud-game/v2/pkg/thread" + "github.com/veandco/go-sdl2/sdl" +) + +type data struct { + w *sdl.Window + glWCtx sdl.GLContext +} + +// singleton state for SDL +var state = data{} + +type Config struct { + Ctx Context + W int + H int + Gl GlConfig +} +type GlConfig struct { + AutoContext bool + VersionMajor uint + VersionMinor uint + HasDepth bool + HasStencil bool +} + +// Init initializes SDL/OpenGL context. +// Uses main thread lock (see thread/mainthread). +func Init(cfg Config) { + log.Printf("[SDL] [OpenGL] initialization...") + if err := sdl.Init(sdl.INIT_VIDEO); err != nil { + log.Printf("[SDL] error: %v", err) + panic("SDL initialization failed") + } + + if cfg.Gl.AutoContext { + log.Printf("[OpenGL] CONTEXT_AUTO (type: %v v%v.%v)", cfg.Ctx, cfg.Gl.VersionMajor, cfg.Gl.VersionMinor) + } else { + switch cfg.Ctx { + case CtxOpenGlCore: + setAttribute(sdl.GL_CONTEXT_PROFILE_MASK, sdl.GL_CONTEXT_PROFILE_CORE) + log.Printf("[OpenGL] CONTEXT_PROFILE_CORE") + break + case CtxOpenGlEs2: + setAttribute(sdl.GL_CONTEXT_PROFILE_MASK, sdl.GL_CONTEXT_PROFILE_ES) + setAttribute(sdl.GL_CONTEXT_MAJOR_VERSION, 3) + setAttribute(sdl.GL_CONTEXT_MINOR_VERSION, 0) + log.Printf("[OpenGL] CONTEXT_PROFILE_ES 3.0") + break + case CtxOpenGl: + if cfg.Gl.VersionMajor >= 3 { + setAttribute(sdl.GL_CONTEXT_PROFILE_MASK, sdl.GL_CONTEXT_PROFILE_COMPATIBILITY) + } + log.Printf("[OpenGL] CONTEXT_PROFILE_COMPATIBILITY") + break + default: + log.Printf("Unsupported hw context: %v", cfg.Ctx) + } + } + + // In OSX 10.14+ window creation and context creation must happen in the main thread + thread.MainMaybe(createWindow) + + BindContext() + + initContext(sdl.GLGetProcAddress) + PrintDriverInfo() + initFramebuffer(cfg.W, cfg.H, cfg.Gl.HasDepth, cfg.Gl.HasStencil) +} + +// Deinit destroys SDL/OpenGL context. +// Uses main thread lock (see thread/mainthread). +func Deinit() { + log.Printf("[SDL] [OpenGL] deinitialization...") + destroyFramebuffer() + // In OSX 10.14+ window deletion must happen in the main thread + thread.MainMaybe(destroyWindow) + sdl.Quit() + log.Printf("[SDL] [OpenGL] deinitialized (%v, %v)", sdl.GetError(), getDriverError()) +} + +// createWindow creates fake SDL window for OpenGL initialization purposes. +func createWindow() { + var winTitle = "CloudRetro dummy window" + var winWidth, winHeight int32 = 1, 1 + + var err error + if state.w, err = sdl.CreateWindow( + winTitle, + sdl.WINDOWPOS_UNDEFINED, sdl.WINDOWPOS_UNDEFINED, + winWidth, winHeight, + sdl.WINDOW_OPENGL|sdl.WINDOW_HIDDEN, + ); err != nil { + panic(err) + } + if state.glWCtx, err = state.w.GLCreateContext(); err != nil { + panic(err) + } +} + +// destroyWindow destroys previously created SDL window. +func destroyWindow() { + BindContext() + sdl.GLDeleteContext(state.glWCtx) + if err := state.w.Destroy(); err != nil { + log.Printf("[SDL] couldn't destroy the window, error: %v", err) + } +} + +// BindContext explicitly binds context to current thread. +func BindContext() { + if err := state.w.GLMakeCurrent(state.glWCtx); err != nil { + log.Printf("[SDL] error: %v", err) + } +} + +func GetGlFbo() uint32 { + return getFbo() +} + +func GetGlProcAddress(proc string) unsafe.Pointer { + return sdl.GLGetProcAddress(proc) +} + +func setAttribute(attr sdl.GLattr, value int) { + if err := sdl.GLSetAttribute(attr, value); err != nil { + log.Printf("[SDL] attribute error: %v", err) + } +} diff --git a/pkg/emulator/libretro/image/color.go b/pkg/emulator/image/color.go similarity index 89% rename from pkg/emulator/libretro/image/color.go rename to pkg/emulator/image/color.go index becedde1..8ed950f5 100644 --- a/pkg/emulator/libretro/image/color.go +++ b/pkg/emulator/image/color.go @@ -6,11 +6,11 @@ import ( const ( // BIT_FORMAT_SHORT_5_5_5_1 has 5 bits R, 5 bits G, 5 bits B, 1 bit alpha - BIT_FORMAT_SHORT_5_5_5_1 = iota + BitFormatShort5551 = iota // BIT_FORMAT_INT_8_8_8_8_REV has 8 bits R, 8 bits G, 8 bits B, 8 bit alpha - BIT_FORMAT_INT_8_8_8_8_REV + BitFormatInt8888Rev // BIT_FORMAT_SHORT_5_6_5 has 5 bits R, 6 bits G, 5 bits - BIT_FORMAT_SHORT_5_6_5 + BitFormatShort565 ) type Format func(data []byte, index int) color.RGBA diff --git a/pkg/emulator/libretro/image/draw.go b/pkg/emulator/image/draw.go similarity index 71% rename from pkg/emulator/libretro/image/draw.go rename to pkg/emulator/image/draw.go index 7a9d5c01..08aa21e0 100644 --- a/pkg/emulator/libretro/image/draw.go +++ b/pkg/emulator/image/draw.go @@ -16,7 +16,7 @@ var canvas = imageCache{ 0, } -func DrawRgbaImage(pixFormat Format, rotationFn Rotate, scaleType, w, h, packedW, bpp int, data []byte, dest *image.RGBA) { +func DrawRgbaImage(pixFormat Format, rotationFn Rotate, scaleType int, flipV bool, w, h, packedW, bpp int, data []byte, dest *image.RGBA) { if pixFormat == nil { dest = nil } @@ -28,15 +28,19 @@ func DrawRgbaImage(pixFormat Format, rotationFn Rotate, scaleType, w, h, packedW } src := getCanvas(ww, hh) - drawImage(pixFormat, w, h, packedW, bpp, rotationFn, data, src) + drawImage(pixFormat, w, h, packedW, bpp, flipV, rotationFn, data, src) Resize(scaleType, src, dest) } -func drawImage(toRGBA Format, w, h, packedW, bpp int, rotationFn Rotate, data []byte, image *image.RGBA) { +func drawImage(toRGBA Format, w, h, packedW, bpp int, flipV bool, rotationFn Rotate, data []byte, image *image.RGBA) { for y := 0; y < h; y++ { + yy := y + if flipV { + yy = (h - 1) - y + } for x := 0; x < w; x++ { src := toRGBA(data, (x+y*packedW)*bpp) - dx, dy := rotationFn.Call(x, y, w, h) + dx, dy := rotationFn.Call(x, yy, w, h) i := dx*4 + dy*image.Stride dst := image.Pix[i : i+4 : i+4] dst[0] = src.R diff --git a/pkg/emulator/libretro/image/rotation.go b/pkg/emulator/image/rotation.go similarity index 100% rename from pkg/emulator/libretro/image/rotation.go rename to pkg/emulator/image/rotation.go diff --git a/pkg/emulator/libretro/image/rotation_test.go b/pkg/emulator/image/rotation_test.go similarity index 100% rename from pkg/emulator/libretro/image/rotation_test.go rename to pkg/emulator/image/rotation_test.go diff --git a/pkg/emulator/libretro/image/scale.go b/pkg/emulator/image/scale.go similarity index 100% rename from pkg/emulator/libretro/image/scale.go rename to pkg/emulator/image/scale.go diff --git a/pkg/emulator/libretro/nanoarch/cfuncs.go b/pkg/emulator/libretro/nanoarch/cfuncs.go index 66433fb9..936b12a4 100644 --- a/pkg/emulator/libretro/nanoarch/cfuncs.go +++ b/pkg/emulator/libretro/nanoarch/cfuncs.go @@ -7,11 +7,15 @@ package nanoarch #include #include +void coreLog(enum retro_log_level level, const char *msg); + void bridge_retro_init(void *f) { + coreLog(RETRO_LOG_INFO, "[Libretro] Initialization...\n"); return ((void (*)(void))f)(); } void bridge_retro_deinit(void *f) { + coreLog(RETRO_LOG_INFO, "[Libretro] Deinitialiazation...\n"); return ((void (*)(void))f)(); } @@ -52,10 +56,12 @@ void bridge_retro_set_audio_sample_batch(void *f, void *callback) { } bool bridge_retro_load_game(void *f, struct retro_game_info *gi) { + coreLog(RETRO_LOG_INFO, "[Libretro] Loading the game...\n"); return ((bool (*)(struct retro_game_info *))f)(gi); } void bridge_retro_unload_game(void *f) { + coreLog(RETRO_LOG_INFO, "[Libretro] Unloading the game...\n"); return ((void (*)(void))f)(); } @@ -124,7 +130,6 @@ void coreLog_cgo(enum retro_log_level level, const char *fmt, ...) { vsnprintf(msg, sizeof(msg), fmt, va); va_end(va); - void coreLog(enum retro_log_level level, const char *msg); coreLog(level, msg); } diff --git a/pkg/emulator/libretro/nanoarch/naemulator.go b/pkg/emulator/libretro/nanoarch/naemulator.go index 01497d9a..d67c8c35 100644 --- a/pkg/emulator/libretro/nanoarch/naemulator.go +++ b/pkg/emulator/libretro/nanoarch/naemulator.go @@ -52,7 +52,7 @@ import "C" const numAxes = 4 -type constrollerState struct { +type controllerState struct { keyState uint16 axes [numAxes]int16 } @@ -70,7 +70,7 @@ type naEmulator struct { gameName string isSavingLoading bool - controllersMap map[string][]constrollerState + controllersMap map[string][]controllerState done chan struct{} // lock to lock uninteruptable operation @@ -113,7 +113,7 @@ func NewNAEmulator(etype string, roomID string, inputChannel <-chan InputEvent) imageChannel: imageChannel, audioChannel: audioChannel, inputChannel: inputChannel, - controllersMap: map[string][]constrollerState{}, + controllersMap: map[string][]controllerState{}, roomID: roomID, done: make(chan struct{}, 1), lock: &sync.Mutex{}, @@ -178,7 +178,7 @@ func (na *naEmulator) listenInput() { } if _, ok := na.controllersMap[inpEvent.ConnID]; !ok { - na.controllersMap[inpEvent.ConnID] = make([]constrollerState, maxPort) + na.controllersMap[inpEvent.ConnID] = make([]controllerState, maxPort) } na.controllersMap[inpEvent.ConnID][inpEvent.PlayerIdx].keyState = inpBitmap @@ -267,7 +267,21 @@ func (na *naEmulator) GetHashPath() string { return util.GetSavePath(na.roomID) } +func (*naEmulator) GetViewport() interface{} { + return outputImg +} + func (na *naEmulator) Close() { // Unload and deinit in the core. close(na.done) } + +// GetLock makes the emulator exclusively locked. +func (na *naEmulator) GetLock() { + na.lock.Lock() +} + +// ReleaseLock removes an exclusive lock from the emulator. +func (na *naEmulator) ReleaseLock() { + na.lock.Unlock() +} diff --git a/pkg/emulator/libretro/nanoarch/nanoarch.go b/pkg/emulator/libretro/nanoarch/nanoarch.go index 8c5428dd..6b44a552 100644 --- a/pkg/emulator/libretro/nanoarch/nanoarch.go +++ b/pkg/emulator/libretro/nanoarch/nanoarch.go @@ -3,8 +3,6 @@ package nanoarch import ( "bufio" "errors" - "fmt" - stdimage "image" "log" "math/rand" "os" @@ -14,12 +12,10 @@ import ( "time" "unsafe" - "github.com/disintegration/imaging" - "github.com/faiface/mainthread" "github.com/giongto35/cloud-game/v2/pkg/config" - "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/image" - "github.com/go-gl/gl/v4.1-core/gl" - "github.com/veandco/go-sdl2/sdl" + "github.com/giongto35/cloud-game/v2/pkg/emulator/graphics" + "github.com/giongto35/cloud-game/v2/pkg/emulator/image" + "github.com/giongto35/cloud-game/v2/pkg/thread" ) /* @@ -72,31 +68,28 @@ import "C" var mu sync.Mutex var video struct { - pitch uint32 - pixFmt uint32 - bpp uint32 - rotation image.Angle - fbo uint32 - rbo uint32 - tex uint32 - hw *C.struct_retro_hw_render_callback - window *sdl.Window - context sdl.GLContext - isGl bool - max_width int32 - max_height int32 - base_width int32 - base_height int32 + pitch uint32 + pixFmt uint32 + bpp uint32 + rotation image.Angle + + baseWidth int32 + baseHeight int32 + maxWidth int32 + maxHeight int32 + + hw *C.struct_retro_hw_render_callback + isGl bool + autoGlContext bool } // default core pix format converter var pixelFormatConverterFn = image.Rgb565 var rotationFn = image.GetRotation(image.Angle(0)) -const bufSize = 1024 * 4 -const joypadNumKeys = int(C.RETRO_DEVICE_ID_JOYPAD_R3 + 1) +//const joypadNumKeys = int(C.RETRO_DEVICE_ID_JOYPAD_R3 + 1) +//var joy [joypadNumKeys]bool -var joy [joypadNumKeys]bool var isGlAllowed bool var usesLibCo bool var coreConfig ConfigProperties @@ -144,39 +137,37 @@ type CloudEmulator interface { //export coreVideoRefresh func coreVideoRefresh(data unsafe.Pointer, width C.unsigned, height C.unsigned, pitch C.size_t) { // some cores can return nothing + // !to add duplicate if can dup if data == nil { return } + // divide by 8333 to give us the equivalent of a 120fps resolution timestamp := uint32(time.Now().UnixNano()/8333) + seed - - if data == C.RETRO_HW_FRAME_BUFFER_VALID { - im := stdimage.NewNRGBA(stdimage.Rect(0, 0, int(width), int(height))) - gl.BindFramebuffer(gl.FRAMEBUFFER, video.fbo) - gl.ReadPixels(0, 0, int32(width), int32(height), gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(im.Pix)) - gl.BindFramebuffer(gl.FRAMEBUFFER, 0) - im = imaging.FlipV(im) - rgba := &stdimage.RGBA{ - Pix: im.Pix, - Stride: im.Stride, - Rect: im.Rect, - } - NAEmulator.imageChannel <- GameFrame{Image: rgba, Timestamp: timestamp} - return - } + // if Libretro renders frame with OpenGL context + isOpenGLRender := data == C.RETRO_HW_FRAME_BUFFER_VALID // calculate real frame width in pixels from packed data (realWidth >= width) packedWidth := int(uint32(pitch) / video.bpp) - - // convert data from C + if packedWidth < 1 { + packedWidth = int(width) + } + // calculate space for the video frame bytes := int(height) * packedWidth * int(video.bpp) - data_ := (*[1 << 30]byte)(data)[:bytes:bytes] + + var data_ []byte + if isOpenGLRender { + data_ = graphics.ReadFramebuffer(bytes, int(width), int(height)) + } else { + data_ = (*[1 << 30]byte)(data)[:bytes:bytes] + } // the image is being resized and de-rotated image.DrawRgbaImage( pixelFormatConverterFn, rotationFn, image.ScaleNearestNeighbour, + isOpenGLRender, int(width), int(height), packedWidth, int(video.bpp), data_, outputImg, @@ -210,7 +201,7 @@ func coreInputState(port C.unsigned, device C.unsigned, index C.unsigned, id C.u return 0 } - // map from id to controll key + // map from id to control key key, ok := bindKeysMap[int(id)] if !ok { return 0 @@ -225,13 +216,14 @@ func coreInputState(port C.unsigned, device C.unsigned, index C.unsigned, id C.u return 0 } -func audioWrite2(buf unsafe.Pointer, frames C.size_t) C.size_t { +func audioWrite(buf unsafe.Pointer, frames C.size_t) C.size_t { // !to make it mono/stereo independent samples := int(frames) * 2 pcm := (*[(1 << 30) - 1]int16)(buf)[:samples:samples] p := make([]int16, samples) - // copy because pcm slice refer to buf underlying pointer, and buf pointer is the same in continuos frames + // copy because pcm slice refer to buf underlying pointer, + // and buf pointer is the same in continuous frames copy(p, pcm) select { @@ -245,27 +237,27 @@ func audioWrite2(buf unsafe.Pointer, frames C.size_t) C.size_t { //export coreAudioSample func coreAudioSample(left C.int16_t, right C.int16_t) { buf := []C.int16_t{left, right} - audioWrite2(unsafe.Pointer(&buf), 1) + audioWrite(unsafe.Pointer(&buf), 1) } //export coreAudioSampleBatch func coreAudioSampleBatch(data unsafe.Pointer, frames C.size_t) C.size_t { - return audioWrite2(data, frames) + return audioWrite(data, frames) } //export coreLog -func coreLog(level C.enum_retro_log_level, msg *C.char) { - fmt.Print("[Log]: ", C.GoString(msg)) +func coreLog(_ C.enum_retro_log_level, msg *C.char) { + log.Printf("[Log] %v", C.GoString(msg)) } //export coreGetCurrentFramebuffer func coreGetCurrentFramebuffer() C.uintptr_t { - return (C.uintptr_t)(video.fbo) + return (C.uintptr_t)(graphics.GetGlFbo()) } //export coreGetProcAddress func coreGetProcAddress(sym *C.char) C.retro_proc_address_t { - return (C.retro_proc_address_t)(sdl.GLGetProcAddress(C.GoString(sym))) + return (C.retro_proc_address_t)(graphics.GetGlProcAddress(C.GoString(sym))) } //export coreEnvironment @@ -316,16 +308,15 @@ func coreEnvironment(cmd C.unsigned, data unsafe.Pointer) C.bool { variable := (*C.struct_retro_variable)(data) key := C.GoString(variable.key) if val, ok := coreConfig[key]; ok { - fmt.Printf("[Env]: get variable: key:%v value:%v\n", key, C.GoString(val)) + log.Printf("[Env]: get variable: key:%v value:%v", key, C.GoString(val)) variable.value = val return true } // fmt.Printf("[Env]: get variable: key:%v not found\n", key) return false case C.RETRO_ENVIRONMENT_SET_HW_RENDER: + video.isGl = isGlAllowed if isGlAllowed { - video.isGl = true - // runtime.LockOSThread() video.hw = (*C.struct_retro_hw_render_callback)(data) video.hw.get_current_framebuffer = (C.retro_hw_get_current_framebuffer_t)(C.coreGetCurrentFramebuffer_cgo) video.hw.get_proc_address = (C.retro_hw_get_proc_address_t)(C.coreGetProcAddress_cgo) @@ -355,128 +346,51 @@ func coreEnvironment(cmd C.unsigned, data unsafe.Pointer) C.bool { return true } -func init() { -} - -var sdlInitialized = false - //export initVideo func initVideo() { - // create_window() - var winTitle string = "CloudRetro" - var winWidth, winHeight int32 = 1, 1 - var err error - - if !sdlInitialized { - sdlInitialized = true - if err = sdl.Init(sdl.INIT_EVERYTHING); err != nil { - panic(err) - } - } - + var context graphics.Context switch video.hw.context_type { - case C.RETRO_HW_CONTEXT_OPENGL_CORE: - fmt.Println("RETRO_HW_CONTEXT_OPENGL_CORE") - sdl.GLSetAttribute(sdl.GL_CONTEXT_PROFILE_MASK, sdl.GL_CONTEXT_PROFILE_CORE) - break - case C.RETRO_HW_CONTEXT_OPENGLES2: - fmt.Println("RETRO_HW_CONTEXT_OPENGLES2") - sdl.GLSetAttribute(sdl.GL_CONTEXT_PROFILE_MASK, sdl.GL_CONTEXT_PROFILE_ES) - sdl.GLSetAttribute(sdl.GL_CONTEXT_MAJOR_VERSION, 3) - sdl.GLSetAttribute(sdl.GL_CONTEXT_MINOR_VERSION, 0) - break + case C.RETRO_HW_CONTEXT_NONE: + context = graphics.CtxNone case C.RETRO_HW_CONTEXT_OPENGL: - fmt.Println("RETRO_HW_CONTEXT_OPENGL") - if video.hw.version_major >= 3 { - sdl.GLSetAttribute(sdl.GL_CONTEXT_PROFILE_MASK, sdl.GL_CONTEXT_PROFILE_COMPATIBILITY) - } - break + context = graphics.CtxOpenGl + case C.RETRO_HW_CONTEXT_OPENGLES2: + context = graphics.CtxOpenGlEs2 + case C.RETRO_HW_CONTEXT_OPENGL_CORE: + context = graphics.CtxOpenGlCore + case C.RETRO_HW_CONTEXT_OPENGLES3: + context = graphics.CtxOpenGlEs3 + case C.RETRO_HW_CONTEXT_OPENGLES_VERSION: + context = graphics.CtxOpenGlEsVersion + case C.RETRO_HW_CONTEXT_VULKAN: + context = graphics.CtxVulkan + case C.RETRO_HW_CONTEXT_DUMMY: + context = graphics.CtxDummy default: - fmt.Println("Unsupported hw context:", video.hw.context_type) + context = graphics.CtxUnknown } - // In OSX 10.14+ window creation and context creation must happen in the main thread - mainthread.Call(func() { - video.window, err = sdl.CreateWindow(winTitle, sdl.WINDOWPOS_UNDEFINED, sdl.WINDOWPOS_UNDEFINED, winWidth, winHeight, sdl.WINDOW_OPENGL) - if err != nil { - panic(err) - } - - video.context, err = video.window.GLCreateContext() - if err != nil { - panic(err) - } + graphics.Init(graphics.Config{ + Ctx: context, + W: int(video.maxWidth), + H: int(video.maxHeight), + Gl: graphics.GlConfig{ + AutoContext: video.autoGlContext, + VersionMajor: uint(video.hw.version_major), + VersionMinor: uint(video.hw.version_minor), + HasDepth: bool(video.hw.depth), + HasStencil: bool(video.hw.stencil), + }, }) - // Bind context to current thread - video.window.GLMakeCurrent(video.context) - - if err = gl.InitWithProcAddrFunc(sdl.GLGetProcAddress); err != nil { - panic(err) - } - - version := gl.GoStr(gl.GetString(gl.VERSION)) - fmt.Println("OpenGL version: ", version) - - // init_texture() - gl.GenTextures(1, &video.tex) - if video.tex < 0 { - panic(fmt.Sprintf("GenTextures: 0x%X", video.tex)) - } - - gl.BindTexture(gl.TEXTURE_2D, video.tex) - - gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) - gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) - - gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, video.max_width, video.max_height, 0, gl.RGBA, gl.UNSIGNED_BYTE, nil) - - gl.BindTexture(gl.TEXTURE_2D, 0) - - //init_framebuffer() - gl.GenFramebuffers(1, &video.fbo) - gl.BindFramebuffer(gl.FRAMEBUFFER, video.fbo) - - gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, video.tex, 0) - - if video.hw.depth { - gl.GenRenderbuffers(1, &video.rbo) - gl.BindRenderbuffer(gl.RENDERBUFFER, video.rbo) - if video.hw.stencil { - gl.RenderbufferStorage(gl.RENDERBUFFER, gl.DEPTH24_STENCIL8, video.base_width, video.base_height) - gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, video.rbo) - } else { - gl.RenderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT24, video.base_width, video.base_height) - gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, video.rbo) - } - gl.BindRenderbuffer(gl.RENDERBUFFER, 0) - } - - status := gl.CheckFramebufferStatus(gl.FRAMEBUFFER) - if status != gl.FRAMEBUFFER_COMPLETE { - if e := gl.GetError(); e != gl.NO_ERROR { - panic(fmt.Sprintf("GL error: 0x%X, Frame status: 0x%X", e, status)) - } - panic(fmt.Sprintf("Frame status: 0x%X", status)) - } - C.bridge_context_reset(video.hw.context_reset) } //export deinitVideo func deinitVideo() { C.bridge_context_reset(video.hw.context_destroy) - if video.hw.depth { - gl.DeleteRenderbuffers(1, &video.rbo) - } - gl.DeleteFramebuffers(1, &video.fbo) - gl.DeleteTextures(1, &video.tex) - // In OSX 10.14+ window deletion must happen in the main thread - mainthread.Call(func() { - video.window.GLMakeCurrent(video.context) - sdl.GLDeleteContext(video.context) - video.window.Destroy() - }) + graphics.Deinit() video.isGl = false + video.autoGlContext = false } var retroHandle unsafe.Pointer @@ -494,8 +408,6 @@ var retroSetAudioSampleBatch unsafe.Pointer var retroRun unsafe.Pointer var retroLoadGame unsafe.Pointer var retroUnloadGame unsafe.Pointer -var retroGetMemorySize unsafe.Pointer -var retroGetMemoryData unsafe.Pointer var retroSerializeSize unsafe.Pointer var retroSerialize unsafe.Pointer var retroUnserialize unsafe.Pointer @@ -511,6 +423,7 @@ func loadFunction(handle unsafe.Pointer, name string) unsafe.Pointer { func coreLoad(meta config.EmulatorMeta) { isGlAllowed = meta.IsGlAllowed usesLibCo = meta.UsesLibCo + video.autoGlContext = meta.AutoGlContext coreConfig = ScanConfigFile(meta.Config) multitap.supported = meta.HasMultitap @@ -531,7 +444,9 @@ func coreLoad(meta config.EmulatorMeta) { if retroHandle == nil { err := C.dlerror() - log.Fatalf("error loading %s, err %+v", meta.Path, *err) + if err != nil { + log.Fatalf("error core load: %s, %v", meta.Path, C.GoString(err)) + } } retroInit = loadFunction(retroHandle, "retro_init") @@ -565,7 +480,7 @@ func coreLoad(meta config.EmulatorMeta) { C.bridge_retro_init(retroInit) v := C.bridge_retro_api_version(retroAPIVersion) - fmt.Println("Libretro API version:", v) + log.Printf("Libretro API version: %v", v) } func slurp(path string, size int64) ([]byte, error) { @@ -596,7 +511,7 @@ func coreLoadGame(filename string) { size := fi.Size() - fmt.Println("ROM size:", size) + log.Printf("ROM size: %v", size) csFilename := C.CString(filename) defer C.free(unsafe.Pointer(csFilename)) @@ -610,11 +525,11 @@ func coreLoadGame(filename string) { C.bridge_retro_get_system_info(retroGetSystemInfo, &si) var libName = C.GoString(si.library_name) - fmt.Println(" library_name:", libName) - fmt.Println(" library_version:", C.GoString(si.library_version)) - fmt.Println(" valid_extensions:", C.GoString(si.valid_extensions)) - fmt.Println(" need_fullpath:", si.need_fullpath) - fmt.Println(" block_extract:", si.block_extract) + log.Printf(" library_name: %v", libName) + log.Printf(" library_version: %v", C.GoString(si.library_version)) + log.Printf(" valid_extensions: %v", C.GoString(si.valid_extensions)) + log.Printf(" need_fullpath: %v", si.need_fullpath) + log.Printf(" block_extract: %v", si.block_extract) if !si.need_fullpath { bytes, err := slurp(filename, size) @@ -650,22 +565,21 @@ func coreLoadGame(filename string) { } NAEmulator.meta.Ratio = ratio - fmt.Println("-----------------------------------") - fmt.Println("--- System audio and video info ---") - fmt.Println("-----------------------------------") - fmt.Println(" Aspect ratio: ", ratio) - fmt.Println(" Base width: ", avi.geometry.base_width) /* Nominal video width of game. */ - fmt.Println(" Base height: ", avi.geometry.base_height) /* Nominal video height of game. */ - fmt.Println(" Max width: ", avi.geometry.max_width) /* Maximum possible width of game. */ - fmt.Println(" Max height: ", avi.geometry.max_height) /* Maximum possible height of game. */ - fmt.Println(" Sample rate: ", avi.timing.sample_rate) /* Sampling rate of audio. */ - fmt.Println(" FPS: ", avi.timing.fps) /* FPS of video content. */ - fmt.Println("-----------------------------------") + log.Printf("-----------------------------------") + log.Printf("--- Core audio and video info ---") + log.Printf("-----------------------------------") + log.Printf(" Frame: %vx%v (%vx%v)", + avi.geometry.base_width, avi.geometry.base_height, + avi.geometry.max_width, avi.geometry.max_height) + log.Printf(" AR: %v", ratio) + log.Printf(" FPS: %v", avi.timing.fps) + log.Printf(" Audio: %vHz", avi.timing.sample_rate) + log.Printf("-----------------------------------") - video.max_width = int32(avi.geometry.max_width) - video.max_height = int32(avi.geometry.max_height) - video.base_width = int32(avi.geometry.base_width) - video.base_height = int32(avi.geometry.base_height) + video.maxWidth = int32(avi.geometry.max_width) + video.maxHeight = int32(avi.geometry.max_height) + video.baseWidth = int32(avi.geometry.base_width) + video.baseHeight = int32(avi.geometry.base_height) if video.isGl { if usesLibCo { C.bridge_execute(C.initVideo_cgo) @@ -715,7 +629,7 @@ func serialize(size uint) ([]byte, error) { return bytes, nil } -// unserialize unserializes internal state from a byte slice. +// unserialize deserializes internal state from a byte slice. func unserialize(bytes []byte, size uint) error { if len(bytes) == 0 { return nil @@ -729,28 +643,34 @@ func unserialize(bytes []byte, size uint) error { func nanoarchShutdown() { if usesLibCo { - C.bridge_execute(retroUnloadGame) - C.bridge_execute(retroDeinit) - if video.isGl { - C.bridge_execute(C.deinitVideo_cgo) - } + thread.MainMaybe(func() { + C.bridge_execute(retroUnloadGame) + C.bridge_execute(retroDeinit) + if video.isGl { + C.bridge_execute(C.deinitVideo_cgo) + } + }) } else { if video.isGl { - // running inside a go routine, lock the thread to make sure the OpenGL context stays current - runtime.LockOSThread() - video.window.GLMakeCurrent(video.context) + thread.MainMaybe(func() { + // running inside a go routine, lock the thread to make sure the OpenGL context stays current + runtime.LockOSThread() + graphics.BindContext() + }) } C.bridge_retro_unload_game(retroUnloadGame) C.bridge_retro_deinit(retroDeinit) if video.isGl { - deinitVideo() - runtime.UnlockOSThread() + thread.MainMaybe(func() { + deinitVideo() + runtime.UnlockOSThread() + }) } } setRotation(0) if r := C.dlclose(retroHandle); r != 0 { - fmt.Println("error closing core") + log.Printf("couldn't close the core") } for _, element := range coreConfig { C.free(unsafe.Pointer(element)) @@ -764,7 +684,7 @@ func nanoarchRun() { if video.isGl { // running inside a go routine, lock the thread to make sure the OpenGL context stays current runtime.LockOSThread() - video.window.GLMakeCurrent(video.context) + graphics.BindContext() } C.bridge_retro_run(retroRun) if video.isGl { @@ -776,18 +696,21 @@ func nanoarchRun() { func videoSetPixelFormat(format uint32) C.bool { switch format { case C.RETRO_PIXEL_FORMAT_0RGB1555: - video.pixFmt = image.BIT_FORMAT_SHORT_5_5_5_1 + video.pixFmt = image.BitFormatShort5551 + graphics.SetPixelFormat(graphics.UnsignedShort5551) video.bpp = 2 // format is not implemented pixelFormatConverterFn = nil break case C.RETRO_PIXEL_FORMAT_XRGB8888: - video.pixFmt = image.BIT_FORMAT_INT_8_8_8_8_REV + video.pixFmt = image.BitFormatInt8888Rev + graphics.SetPixelFormat(graphics.UnsignedInt8888Rev) video.bpp = 4 pixelFormatConverterFn = image.Rgba8888 break case C.RETRO_PIXEL_FORMAT_RGB565: - video.pixFmt = image.BIT_FORMAT_SHORT_5_6_5 + video.pixFmt = image.BitFormatShort565 + graphics.SetPixelFormat(graphics.UnsignedShort565) video.bpp = 2 pixelFormatConverterFn = image.Rgb565 break diff --git a/pkg/emulator/libretro/nanoarch/nanoarch_test.go b/pkg/emulator/libretro/nanoarch/nanoarch_test.go new file mode 100644 index 00000000..1175661d --- /dev/null +++ b/pkg/emulator/libretro/nanoarch/nanoarch_test.go @@ -0,0 +1,232 @@ +package nanoarch + +import ( + "crypto/md5" + "fmt" + "image" + "io/ioutil" + "log" + "os" + "path" + "path/filepath" + "runtime" + "strings" + "sync" + "testing" + + "github.com/giongto35/cloud-game/v2/pkg/config" +) + +type testRun struct { + room string + system string + rom string + emulationTicks int + + gl bool + libCo bool +} + +// EmulatorMock contains naEmulator mocking data. +type EmulatorMock struct { + naEmulator + + // Libretro compiled lib core name + core string + // draw canvas instance + canvas *image.RGBA + // shared core paths (can't be changed) + paths EmulatorPaths + + // channels + imageInCh <-chan GameFrame + audioInCh <-chan []int16 + inputOutCh chan<- InputEvent +} + +// EmulatorPaths defines various emulator file paths. +type EmulatorPaths struct { + assets string + cores string + games string + save string +} + +// GetEmulatorMock returns a properly stubbed emulator instance. +// Due to extensive use of globals -- one mock instance is allowed per a test run. +// Don't forget to init one image channel consumer, it will lock-out otherwise. +// Make sure you call shutdownEmulator(). +func GetEmulatorMock(room string, system string) *EmulatorMock { + assetsPath := getAssetsPath() + metadata := config.EmulatorConfig[system] + + images := make(chan GameFrame, 30) + audio := make(chan []int16, 30) + inputs := make(chan InputEvent, 100) + + // an emu + emu := &EmulatorMock{ + naEmulator: naEmulator{ + imageChannel: images, + audioChannel: audio, + inputChannel: inputs, + + meta: metadata, + controllersMap: map[string][]controllerState{}, + roomID: room, + done: make(chan struct{}, 1), + lock: &sync.Mutex{}, + }, + + canvas: image.NewRGBA(image.Rect(0, 0, metadata.Width, metadata.Height)), + core: path.Base(metadata.Path), + + paths: EmulatorPaths{ + assets: cleanPath(assetsPath), + cores: cleanPath(assetsPath + "emulator/libretro/cores/"), + games: cleanPath(assetsPath + "games/"), + }, + + imageInCh: images, + audioInCh: audio, + inputOutCh: inputs, + } + + // stub globals + NAEmulator = &emu.naEmulator + outputImg = emu.canvas + + emu.paths.save = cleanPath(emu.GetHashPath()) + + return emu +} + +// GetDefaultEmulatorMock returns initialized emulator mock with default params. +// Spawns audio/image channels consumers. +// Don't forget to close emulator mock with shutdownEmulator(). +func GetDefaultEmulatorMock(room string, system string, rom string) *EmulatorMock { + mock := GetEmulatorMock(room, system) + mock.loadRom(rom) + go mock.handleVideo(func(_ GameFrame) {}) + go mock.handleAudio(func(_ []int16) {}) + + return mock +} + +// loadRom loads a ROM into the emulator. +// The rom will be loaded from emulators' games path. +func (emu *EmulatorMock) loadRom(game string) { + fmt.Printf("%v %v\n", emu.paths.cores, emu.core) + coreLoad(config.EmulatorMeta{ + Path: emu.paths.cores + emu.core, + }) + coreLoadGame(emu.paths.games + game) +} + +// shutdownEmulator closes the emulator and cleans its resources. +func (emu *EmulatorMock) shutdownEmulator() { + _ = os.Remove(emu.GetHashPath()) + + close(emu.imageChannel) + close(emu.audioChannel) + close(emu.inputOutCh) + + nanoarchShutdown() +} + +// emulateOneFrame emulates one frame with exclusive lock. +func (emu *EmulatorMock) emulateOneFrame() { + emu.GetLock() + nanoarchRun() + emu.ReleaseLock() +} + +// Who needs generics anyway? +// handleVideo is a custom message handler for the video channel. +func (emu *EmulatorMock) handleVideo(handler func(image GameFrame)) { + for frame := range emu.imageInCh { + handler(frame) + } +} + +// handleAudio is a custom message handler for the audio channel. +func (emu *EmulatorMock) handleAudio(handler func(sample []int16)) { + for frame := range emu.audioInCh { + handler(frame) + } +} + +// handleInput is a custom message handler for the input channel. +func (emu *EmulatorMock) handleInput(handler func(event InputEvent)) { + for event := range emu.inputChannel { + handler(event) + } +} + +// getSavePath returns the full path to the emulator latest save. +func (emu *EmulatorMock) getSavePath() string { + return cleanPath(emu.GetHashPath()) +} + +// dumpState returns the current emulator state and +// the latest saved state for its session. +// Locks the emulator. +func (emu *EmulatorMock) dumpState() (string, string) { + emu.GetLock() + bytes, _ := ioutil.ReadFile(emu.paths.save) + persistedStateHash := getHash(bytes) + emu.ReleaseLock() + + stateHash := emu.getStateHash() + fmt.Printf("mem: %v, dat: %v\n", stateHash, persistedStateHash) + return stateHash, persistedStateHash +} + +// getStateHash returns the current emulator state hash. +// Locks the emulator. +func (emu *EmulatorMock) getStateHash() string { + emu.GetLock() + state, _ := getState() + emu.ReleaseLock() + + return getHash(state) +} + +// getAssetsPath returns absolute path to the assets directory. +func getAssetsPath() string { + appName := "cloud-game" + // get app path at runtime + _, b, _, _ := runtime.Caller(0) + return filepath.Dir(strings.SplitAfter(b, appName)[0]) + "/" + appName + "/assets/" +} + +// getHash returns MD5 hash. +func getHash(bytes []byte) string { + return fmt.Sprintf("%x", md5.Sum(bytes)) +} + +// cleanPath returns a proper file path for current OS. +func cleanPath(path string) string { + return filepath.FromSlash(path) +} + +// benchmarkEmulator is a generic function for +// measuring emulator performance for one emulation frame. +func benchmarkEmulator(system string, rom string, b *testing.B) { + log.SetOutput(ioutil.Discard) + os.Stdout, _ = os.Open(os.DevNull) + + s := GetDefaultEmulatorMock("bench_"+system+"_performance", system, rom) + for i := 0; i < b.N; i++ { + s.emulateOneFrame() + } + s.shutdownEmulator() +} + +func BenchmarkEmulatorGba(b *testing.B) { + benchmarkEmulator("gba", "Sushi The Cat.gba", b) +} + +func BenchmarkEmulatorNes(b *testing.B) { + benchmarkEmulator("nes", "Super Mario Bros.nes", b) +} diff --git a/pkg/emulator/libretro/nanoarch/savestates.go b/pkg/emulator/libretro/nanoarch/savestates.go index c70e62cf..6ec5ae5f 100644 --- a/pkg/emulator/libretro/nanoarch/savestates.go +++ b/pkg/emulator/libretro/nanoarch/savestates.go @@ -1,67 +1,63 @@ -// Package savestates takes care of serializing and unserializing the game RAM -// to the host filesystem. +// Package savestates enables emulator state manipulation. package nanoarch -/* -#include "libretro.h" -#cgo LDFLAGS: -ldl -#include -#include -#include -#include - -bool bridge_retro_serialize(void *f, void *data, size_t size); -bool bridge_retro_unserialize(void *f, void *data, size_t size); -size_t bridge_retro_serialize_size(void *f); -*/ -import "C" - import ( "io/ioutil" ) -func (na *naEmulator) GetLock() { - //atomic.CompareAndSwapInt32(&na.saveLock, 0, 1) - na.lock.Lock() -} +type state []byte -func (na *naEmulator) ReleaseLock() { - //atomic.CompareAndSwapInt32(&na.saveLock, 1, 0) - na.lock.Unlock() -} - -// Save the current state to the filesystem. name is the name of the -// savestate file to save to, without extension. +// Save writes the current state to the filesystem. +// Deadlock warning: locks the emulator. func (na *naEmulator) Save() error { - path := na.GetHashPath() - na.GetLock() defer na.ReleaseLock() - s := serializeSize() - bytes, err := serialize(s) - if err != nil { + if state, err := getState(); err == nil { + return state.toFile(na.GetHashPath()) + } else { return err } - if err != nil { - return err - } - - return ioutil.WriteFile(path, bytes, 0644) } -// Load the state from the filesystem +// Load restores the state from the filesystem. +// Deadlock warning: locks the emulator. func (na *naEmulator) Load() error { - path := na.GetHashPath() - na.GetLock() defer na.ReleaseLock() - s := serializeSize() - bytes, err := ioutil.ReadFile(path) - if err != nil { + path := na.GetHashPath() + if state, err := fromFile(path); err == nil { + return restoreState(state) + } else { return err } - err = unserialize(bytes, s) - return err +} + +// getState returns the current emulator state. +func getState() (state, error) { + if dat, err := serialize(serializeSize()); err == nil { + return dat, nil + } else { + return state{}, err + } +} + +// restoreState restores an emulator state. +func restoreState(dat state) error { + return unserialize(dat, serializeSize()) +} + +// toFile writes the state to a file with the path. +func (st state) toFile(path string) error { + return ioutil.WriteFile(path, st, 0644) +} + +// fromFile reads the state from a file with the path. +func fromFile(path string) (state, error) { + if bytes, err := ioutil.ReadFile(path); err == nil { + return bytes, nil + } else { + return state{}, err + } } diff --git a/pkg/emulator/libretro/nanoarch/savestates_test.go b/pkg/emulator/libretro/nanoarch/savestates_test.go new file mode 100644 index 00000000..5f5779f7 --- /dev/null +++ b/pkg/emulator/libretro/nanoarch/savestates_test.go @@ -0,0 +1,233 @@ +package nanoarch + +import ( + "fmt" + "math/rand" + "sync" + "testing" + "time" +) + +// Tests a successful emulator state save. +func TestSave(t *testing.T) { + tests := []testRun{ + { + room: "test_save_ok_00", + system: "gba", + rom: "Sushi The Cat.gba", + emulationTicks: 100, + }, + { + room: "test_save_ok_01", + system: "gba", + rom: "anguna.gba", + emulationTicks: 10, + }, + } + + for _, test := range tests { + t.Logf("Testing [%v] save with [%v]\n", test.system, test.rom) + + mock := GetDefaultEmulatorMock(test.room, test.system, test.rom) + + for test.emulationTicks > 0 { + mock.emulateOneFrame() + test.emulationTicks-- + } + + fmt.Printf("[%-14v] ", "before save") + snapshot1, _ := mock.dumpState() + if err := mock.Save(); err != nil { + t.Errorf("Save fail %v", err) + } + fmt.Printf("[%-14v] ", "after save") + snapshot1, snapshot2 := mock.dumpState() + + if snapshot1 != snapshot2 { + t.Errorf("It seems rom state save has failed: %v != %v", snapshot1, snapshot2) + } + + mock.shutdownEmulator() + } +} + +// Tests save and restore function: +// +// Emulate n ticks. +// Call save (a). +// Emulate n ticks again. +// Call load from the save (b). +// Compare states (a) and (b), should be =. +// +func TestLoad(t *testing.T) { + tests := []testRun{ + { + room: "test_load_00", + system: "nes", + rom: "Super Mario Bros.nes", + emulationTicks: 100, + }, + { + room: "test_load_01", + system: "gba", + rom: "Sushi The Cat.gba", + emulationTicks: 1000, + }, + { + room: "test_load_02", + system: "gba", + rom: "anguna.gba", + emulationTicks: 100, + }, + } + + for _, test := range tests { + t.Logf("Testing [%v] load with [%v]\n", test.system, test.rom) + + mock := GetDefaultEmulatorMock(test.room, test.system, test.rom) + + fmt.Printf("[%-14v] ", "initial") + mock.dumpState() + + for ticks := test.emulationTicks; ticks > 0; ticks-- { + mock.emulateOneFrame() + } + fmt.Printf("[%-14v] ", fmt.Sprintf("emulated %d", test.emulationTicks)) + mock.dumpState() + + if err := mock.Save(); err != nil { + t.Errorf("Save fail %v", err) + } + fmt.Printf("[%-14v] ", "saved") + snapshot1, _ := mock.dumpState() + + for ticks := test.emulationTicks; ticks > 0; ticks-- { + mock.emulateOneFrame() + } + fmt.Printf("[%-14v] ", fmt.Sprintf("emulated %d", test.emulationTicks)) + mock.dumpState() + + if err := mock.Load(); err != nil { + t.Errorf("Load fail %v", err) + } + fmt.Printf("[%-14v] ", "restored") + snapshot2, _ := mock.dumpState() + + if snapshot1 != snapshot2 { + t.Errorf("It seems rom state restore has failed: %v != %v", snapshot1, snapshot2) + } + + mock.shutdownEmulator() + } +} + +func TestStateConcurrency(t *testing.T) { + tests := []struct { + run testRun + // determine random + seed int + }{ + { + run: testRun{ + room: "test_concurrency_00", + system: "gba", + rom: "Sushi The Cat.gba", + emulationTicks: 120, + }, + seed: 42, + }, + { + run: testRun{ + room: "test_concurrency_01", + system: "gba", + rom: "anguna.gba", + emulationTicks: 300, + }, + seed: 42 + 42, + }, + } + + for _, test := range tests { + t.Logf("Testing [%v] concurrency with [%v]\n", test.run.system, test.run.rom) + + mock := GetEmulatorMock(test.run.room, test.run.system) + ops := &sync.WaitGroup{} + // quantum lock + qLock := &sync.Mutex{} + op := 0 + + mock.loadRom(test.run.rom) + go mock.handleVideo(func(frame GameFrame) { + if len(frame.Image.Pix) == 0 { + t.Errorf("It seems that rom video frame was empty, which is strange!") + } + }) + go mock.handleAudio(func(_ []int16) {}) + go mock.handleInput(func(_ InputEvent) {}) + + rand.Seed(int64(test.seed)) + t.Logf("Random seed is [%v]\n", test.seed) + t.Logf("Save path is [%v]\n", mock.paths.save) + + _ = mock.Save() + + // emulation fps ROM cap + ticker := time.NewTicker(time.Second / time.Duration(mock.meta.Fps)) + t.Logf("FPS limit is [%v]\n", mock.meta.Fps) + + for range ticker.C { + select { + case <-mock.done: + mock.shutdownEmulator() + return + default: + } + + op++ + if op > test.run.emulationTicks { + mock.Close() + } else { + qLock.Lock() + mock.emulateOneFrame() + qLock.Unlock() + + if lucky() && !lucky() { + ops.Add(1) + go func() { + qLock.Lock() + defer qLock.Unlock() + + mock.dumpState() + // remove save to reproduce the bug + _ = mock.Save() + _, snapshot1 := mock.dumpState() + _ = mock.Load() + snapshot2, _ := mock.dumpState() + + // Bug or feature? + // When you load a state from the file + // without immediate preceding save, + // it won't be in the loaded state + // even without calling retro_run. + // But if you pause the threads with a debugger + // and run the code step by step, then it will work as expected. + // Possible background emulation? + + if snapshot1 != snapshot2 { + t.Errorf("States are inconsistent %v != %v on tick %v\n", snapshot1, snapshot2, op) + } + ops.Done() + }() + } + } + } + + ops.Wait() + ticker.Stop() + } +} + +// lucky returns random boolean. +func lucky() bool { + return rand.Intn(2) == 1 +} diff --git a/pkg/thread/thread.go b/pkg/thread/thread.go new file mode 100644 index 00000000..5b1c3e34 --- /dev/null +++ b/pkg/thread/thread.go @@ -0,0 +1,32 @@ +// This package used for locking goroutines to +// the main OS thread. +// See: https://github.com/golang/go/wiki/LockOSThread +package thread + +import ( + "runtime" + + "github.com/faiface/mainthread" +) + +var isMacOs = runtime.GOOS == "darwin" + +// MainWrapMaybe enables functions to be executed in the main thread. +// Enabled for macOS only. +func MainWrapMaybe(f func()) { + if isMacOs { + mainthread.Run(f) + } else { + f() + } +} + +// MainMaybe calls a function on the main thread. +// Enabled for macOS only. +func MainMaybe(f func()) { + if isMacOs { + mainthread.Call(f) + } else { + f() + } +} diff --git a/pkg/worker/cloud-storage/storage.go b/pkg/worker/cloud-storage/storage.go index a261e176..07f46b3d 100644 --- a/pkg/worker/cloud-storage/storage.go +++ b/pkg/worker/cloud-storage/storage.go @@ -76,7 +76,7 @@ func (c *Client) SaveFile(name string, srcFile string) (err error) { return nil } -// Loadfile load file from GCP +// Loadfile loads file from GCP func (c *Client) LoadFile(name string) (data []byte, err error) { // Bypass if client is nil if c == nil { diff --git a/pkg/worker/cloud-storage/storage_test.go b/pkg/worker/cloud-storage/storage_test.go index 16e739f1..cd0e774e 100644 --- a/pkg/worker/cloud-storage/storage_test.go +++ b/pkg/worker/cloud-storage/storage_test.go @@ -3,14 +3,28 @@ package storage import ( "io/ioutil" "log" + "os" "testing" ) func TestSaveGame(t *testing.T) { client := NewInitClient() + if client == nil { + t.Skip("Cloud storage is not initialized") + } data := []byte("Test Hello") - ioutil.WriteFile("/tmp/TempFile", data, 0644) - err := client.SaveFile("Test", "/tmp/TempFile") + + file, err := ioutil.TempFile("", "test_cloud_save") + if err != nil { + t.Errorf("Temp dir is not accessable %v", err) + } + defer os.Remove(file.Name()) + + if err = ioutil.WriteFile(file.Name(), data, 0644); err != nil { + t.Errorf("File is not writable %v", err) + } + + err = client.SaveFile("Test", file.Name()) if err != nil { log.Panic(err) } diff --git a/pkg/worker/room/media.go b/pkg/worker/room/media.go index 809ae3b9..550a3ebd 100644 --- a/pkg/worker/room/media.go +++ b/pkg/worker/room/media.go @@ -146,6 +146,9 @@ func (r *Room) startVideo(width, height int, videoEncoderType string) { fmt.Println("error create new encoder", err) return } + + r.encoder = enc + einput := enc.GetInputChan() eoutput := enc.GetOutputChan() diff --git a/pkg/worker/room/room.go b/pkg/worker/room/room.go index c20f5234..75e363d5 100644 --- a/pkg/worker/room/room.go +++ b/pkg/worker/room/room.go @@ -20,6 +20,7 @@ import ( "github.com/giongto35/cloud-game/v2/pkg/config/worker" "github.com/giongto35/cloud-game/v2/pkg/emulator" "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/nanoarch" + "github.com/giongto35/cloud-game/v2/pkg/encoder" "github.com/giongto35/cloud-game/v2/pkg/games" "github.com/giongto35/cloud-game/v2/pkg/util" "github.com/giongto35/cloud-game/v2/pkg/webrtc" @@ -57,8 +58,8 @@ type Room struct { onlineStorage *storage.Client // GameName gameName string - // Meta of game - //meta emulator.Meta + + encoder encoder.Encoder } const separator = "___" @@ -236,12 +237,6 @@ func resizeToAspect(ratio float64, sw int, sh int) (dw int, dh int) { return } -// getEmulator creates new emulator and run it -func getEmulator(emuName string, roomID string, imageChannel chan<- nanoarch.GameFrame, audioChannel chan<- []int16, inputChannel <-chan int) emulator.CloudEmulator { - - return nanoarch.NAEmulator -} - // getGameNameFromRoomID parse roomID to get roomID and gameName func GetGameNameFromRoomID(roomID string) string { parts := strings.Split(roomID, separator) @@ -371,7 +366,9 @@ func (r *Room) Close() { // the lock is holding before coming to close, so it will cause deadlock if SaveGame is synchronous go func() { // Save before close, so save can have correct state (Not sure) may again cause deadlock - r.SaveGame() + if err := r.SaveGame(); err != nil { + log.Println("[error] couldn't save the game during closing") + } r.director.Close() }() } else { @@ -432,8 +429,11 @@ func (r *Room) saveOnlineRoomToLocal(roomID string, savepath string) error { if err != nil { return err } + // Save the data fetched from gcloud to local server - ioutil.WriteFile(savepath, data, 0644) + if data != nil { + _ = ioutil.WriteFile(savepath, data, 0644) + } return nil } diff --git a/pkg/worker/room/room_test.go b/pkg/worker/room/room_test.go new file mode 100644 index 00000000..b26ca68b --- /dev/null +++ b/pkg/worker/room/room_test.go @@ -0,0 +1,359 @@ +package room + +import ( + "flag" + "fmt" + "hash/crc32" + "image" + "image/color" + "image/draw" + "image/png" + "io/ioutil" + "log" + "os" + "path/filepath" + "runtime" + "sync" + "testing" + "time" + + "github.com/giongto35/cloud-game/v2/pkg/config" + "github.com/giongto35/cloud-game/v2/pkg/config/worker" + "github.com/giongto35/cloud-game/v2/pkg/encoder" + "github.com/giongto35/cloud-game/v2/pkg/games" + "github.com/giongto35/cloud-game/v2/pkg/thread" + storage "github.com/giongto35/cloud-game/v2/pkg/worker/cloud-storage" + "golang.org/x/image/font" + "golang.org/x/image/font/basicfont" + "golang.org/x/image/math/fixed" +) + +var ( + renderFrames bool + outputPath string + autoGlContext bool +) + +type roomMock struct { + Room +} + +type roomMockConfig struct { + roomName string + gamesPath string + game games.GameMetadata + codec string + autoGlContext bool +} + +// Restricts a re-config call +// to only one invocation. +var configOnce sync.Once + +// Store absolute path to test games +var whereIsGames = getAppPath() + "assets/games/" +var testTempDir = filepath.Join(os.TempDir(), "cloud-game-core-tests") + +func init() { + runtime.LockOSThread() +} + +func TestMain(m *testing.M) { + flag.BoolVar(&renderFrames, "renderFrames", false, "Render frames for eye testing purposes") + flag.StringVar(&outputPath, "outputPath", "./", "Output path for generated files") + flag.BoolVar(&autoGlContext, "autoGlContext", false, "Set auto GL context choose for headless machines") + + thread.MainWrapMaybe(func() { os.Exit(m.Run()) }) +} + +func TestRoom(t *testing.T) { + tests := []struct { + roomName string + game games.GameMetadata + codec string + frames int + }{ + { + game: games.GameMetadata{ + Name: "Super Mario Bros", + Type: "nes", + Path: "Super Mario Bros.nes", + }, + codec: config.CODEC_VP8, + frames: 5, + }, + } + + for _, test := range tests { + room := getRoomMock(roomMockConfig{ + roomName: test.roomName, + gamesPath: whereIsGames, + game: test.game, + codec: test.codec, + }) + t.Logf("The game [%v] has been loaded", test.game.Name) + waitNFrames(test.frames, room.encoder.GetOutputChan()) + room.Close() + } +} + +func TestRoomWithGL(t *testing.T) { + tests := []struct { + game games.GameMetadata + codec string + frames int + }{ + { + game: games.GameMetadata{ + Name: "Sample Demo by Florian (PD)", + Type: "n64", + Path: "Sample Demo by Florian (PD).z64", + }, + codec: config.CODEC_VP8, + frames: 50, + }, + } + + run := func() { + for _, test := range tests { + room := getRoomMock(roomMockConfig{ + gamesPath: whereIsGames, + game: test.game, + codec: test.codec, + }) + t.Logf("The game [%v] has been loaded", test.game.Name) + waitNFrames(test.frames, room.encoder.GetOutputChan()) + room.Close() + } + } + + thread.MainMaybe(run) +} + +func TestAllEmulatorRooms(t *testing.T) { + tests := []struct { + game games.GameMetadata + frames int + }{ + { + game: games.GameMetadata{Name: "Sushi", Type: "gba", Path: "Sushi The Cat.gba"}, + frames: 100, + }, + { + game: games.GameMetadata{Name: "Mario", Type: "nes", Path: "Super Mario Bros.nes"}, + frames: 50, + }, + { + game: games.GameMetadata{Name: "Florian Demo", Type: "n64", Path: "Sample Demo by Florian (PD).z64"}, + frames: 50, + }, + } + + crc32q := crc32.MakeTable(0xD5828281) + + for _, test := range tests { + room := getRoomMock(roomMockConfig{ + gamesPath: whereIsGames, + game: test.game, + codec: config.CODEC_VP8, + autoGlContext: autoGlContext, + }) + t.Logf("The game [%v] has been loaded", test.game.Name) + waitNFrames(test.frames, room.encoder.GetOutputChan()) + + if renderFrames { + img := room.director.GetViewport().(*image.RGBA) + tag := fmt.Sprintf("%v-%v-0x%08x", runtime.GOOS, test.game.Type, crc32.Checksum(img.Pix, crc32q)) + dumpCanvas(img, tag, fmt.Sprintf("%v [%v]", tag, test.frames), outputPath) + } + + room.Close() + // hack: wait room destruction + time.Sleep(2 * time.Second) + } +} + +// enforce image.RGBA to remove alpha channel when encoding PNGs +type opaqueRGBA struct { + *image.RGBA +} + +func (*opaqueRGBA) Opaque() bool { + return true +} + +func dumpCanvas(f *image.RGBA, name string, caption string, path string) { + frame := *f + + // 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) + (&font.Drawer{ + Dst: &frame, + Src: image.NewUniform(color.RGBA{R: 255, G: 255, B: 255, A: 255}), + Face: basicfont.Face7x13, + Dot: fixed.Point26_6{X: fixed.Int26_6(10 * 64), Y: fixed.Int26_6(20 * 64)}, + }).DrawString(caption) + } + + var outPath string + if len(path) > 0 { + outPath = path + } else { + outPath = testTempDir + } + + // really like Go's error handling + if err := os.MkdirAll(outPath, 0770); err != nil { + log.Printf("Couldn't create target dir for the output images, %v", err) + return + } + + if f, err := os.Create(filepath.Join(outPath, name+".png")); err == nil { + if err = png.Encode(f, &opaqueRGBA{&frame}); err != nil { + log.Printf("Couldn't encode the image, %v", err) + } + _ = f.Close() + } else { + log.Printf("Couldn't create the image, %v", err) + } +} + +// getRoomMock returns mocked Room struct. +func getRoomMock(cfg roomMockConfig) roomMock { + configOnce.Do(func() { fixEmulators(cfg.autoGlContext) }) + cfg.game.Path = cfg.gamesPath + cfg.game.Path + room := NewRoom(cfg.roomName, cfg.game, cfg.codec, storage.NewInitClient(), worker.NewDefaultConfig()) + + // loop-wait the room initialization + var init sync.WaitGroup + init.Add(1) + wasted := 0 + go func() { + sleepDeltaMs := 10 + for room.director == nil || room.encoder == nil { + time.Sleep(time.Duration(sleepDeltaMs) * time.Millisecond) + wasted++ + if wasted > 1000 { + break + } + } + init.Done() + }() + init.Wait() + + return roomMock{*room} +} + +// fixEmulators makes absolute game paths in global GameList and passes GL context config. +func fixEmulators(autoGlContext bool) { + appPath := getAppPath() + + for k, conf := range config.EmulatorConfig { + conf.Path = appPath + conf.Path + if len(conf.Config) > 0 { + conf.Config = appPath + conf.Config + } + + if conf.IsGlAllowed && autoGlContext { + conf.AutoGlContext = true + } + config.EmulatorConfig[k] = conf + } +} + +// getAppPath returns absolute path to the assets directory. +func getAppPath() string { + p, _ := filepath.Abs("../../../") + return p + string(filepath.Separator) +} + +func waitNFrames(n int, ch chan encoder.OutFrame) { + var frames sync.WaitGroup + frames.Add(n) + + done := false + go func() { + for range ch { + if done { + break + } + frames.Done() + } + }() + + frames.Wait() + done = true +} + +// 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 string, frames int, suppressOutput bool, b *testing.B) { + if suppressOutput { + log.SetOutput(ioutil.Discard) + os.Stdout, _ = os.Open(os.DevNull) + } + + for i := 0; i < b.N; i++ { + room := getRoomMock(roomMockConfig{ + gamesPath: whereIsGames, + game: rom, + codec: codec, + }) + waitNFrames(frames, room.encoder.GetOutputChan()) + room.Close() + } +} + +// Measures emulation performance of various +// emulators and encoding options. +func BenchmarkRoom(b *testing.B) { + benches := []struct { + system string + game games.GameMetadata + codecs []string + frames int + }{ + // warm up + { + system: "gba", + game: games.GameMetadata{ + Name: "Sushi The Cat", + Type: "gba", + Path: "Sushi The Cat.gba", + }, + codecs: []string{"vp8"}, + frames: 50, + }, + { + system: "gba", + game: games.GameMetadata{ + Name: "Sushi The Cat", + Type: "gba", + Path: "Sushi The Cat.gba", + }, + codecs: []string{"vp8", "x264"}, + frames: 100, + }, + { + system: "nes", + game: games.GameMetadata{ + Name: "Super Mario Bros", + Type: "nes", + Path: "Super Mario Bros.nes", + }, + codecs: []string{"vp8", "x264"}, + frames: 100, + }, + } + + for _, bench := range benches { + for _, codec := range bench.codecs { + b.Run(fmt.Sprintf("%s-%s-%d", bench.system, codec, bench.frames), func(b *testing.B) { + benchmarkRoom(bench.game, codec, bench.frames, true, b) + }) + // hack: wait room destruction + time.Sleep(5 * time.Second) + } + } +}