Fix errors/misuse with OpenGL-based core API (#237)

* Follow Go standard for naming constants

* Use reformatted pixFormats for Libretro cores

* Use OpenGL 2.1 Core profile bindings for render instead 4.1

* Cleanup the code

* SDL attributes should be set before the sdl.Init call

* Use simple vertical frame flip function instead imaging lib with OpenGL renderer

* Use the separate control flow for the macOS OpenGL context handling

* Add OpenGL pixel type/format switch based on cores callback

* Use unified log instead of fmt

* Clean code

* Remove unnecessary SDL init flag

* Printout errors with SDL / OpenGL functions

* Add CGO Libretro logging output

* Use main thread lock for windows and OpenGL context

* Remove Darwin OS switch

* Add extended OpenGL version info print

* Update Libretro cores info print

* Add game library module (#232)

* Add game library

* Add missing local game lib files

* Add missing return statement

* Use v2 suffix

* Bump the dependencies

* Update Libretro modules to support headless test runners

* Port old savestates tests as example for Libretro cores runner testing

* Add n64 core example game and a test

* Update room tests for various games

* Add frame dump support for CI builds

* Add frame rendering to image output for core testing

* Update ROM frame exporter in tests

* Disable Docker image publishing

* Add frame rendering output for non-gl cores for CI

* Add auto GL context override for headless, gpu-less machines (e.g. Github CI Xeon)

* Add Windows CI headless cores frame render config

* Add missing Mesa OpenGL drivers to Ubuntu CI

* Add mupen n64 core download into CI tests

* Add Linux, macOS, Windows core frame render tests into CI

* Remove unnecessary var

* Add some comments

* Revert Y flip

* Move OpenGL into a separate package

* Add SDL package

* Update modules
This commit is contained in:
sergystepanov 2020-11-04 13:59:12 +03:00 committed by GitHub
parent 50762e95c7
commit bd6e146e64
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 1509 additions and 350 deletions

View file

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

View file

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

Binary file not shown.

View file

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

25
go.mod vendored
View file

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

60
go.sum vendored
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,11 +7,15 @@ package nanoarch
#include <stdarg.h>
#include <stdio.h>
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);
}

View file

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

View file

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

View file

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

View file

@ -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 <stdlib.h>
#include <stdio.h>
#include <dlfcn.h>
#include <string.h>
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
}
}

View file

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

32
pkg/thread/thread.go Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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