mirror of
https://github.com/giongto35/cloud-game.git
synced 2026-01-23 02:34:42 +00:00
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:
parent
50762e95c7
commit
bd6e146e64
29 changed files with 1509 additions and 350 deletions
79
.github/workflows/build.yml
vendored
79
.github/workflows/build.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
12
.github/workflows/release.yml.disabled
vendored
12
.github/workflows/release.yml.disabled
vendored
|
|
@ -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') }}
|
||||
|
|
|
|||
BIN
assets/games/Sample Demo by Florian (PD).z64
vendored
Normal file
BIN
assets/games/Sample Demo by Florian (PD).z64
vendored
Normal file
Binary file not shown.
|
|
@ -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
25
go.mod
vendored
|
|
@ -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
60
go.sum
vendored
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
18
pkg/emulator/graphics/context.go
Normal file
18
pkg/emulator/graphics/context.go
Normal 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
|
||||
)
|
||||
151
pkg/emulator/graphics/opengl.go
Normal file
151
pkg/emulator/graphics/opengl.go
Normal 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))
|
||||
}
|
||||
135
pkg/emulator/graphics/sdl.go
Normal file
135
pkg/emulator/graphics/sdl.go
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
232
pkg/emulator/libretro/nanoarch/nanoarch_test.go
Normal file
232
pkg/emulator/libretro/nanoarch/nanoarch_test.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
233
pkg/emulator/libretro/nanoarch/savestates_test.go
Normal file
233
pkg/emulator/libretro/nanoarch/savestates_test.go
Normal 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
32
pkg/thread/thread.go
Normal 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
359
pkg/worker/room/room_test.go
Normal file
359
pkg/worker/room/room_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue