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