mirror of
https://github.com/giongto35/cloud-game.git
synced 2026-01-23 10:35:44 +00:00
Compare commits
240 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e6efc2319 | ||
|
|
1d5bae0c62 | ||
|
|
368bae8c07 | ||
|
|
58a19affcb | ||
|
|
8754a5edfa | ||
|
|
aeb41008c9 | ||
|
|
059e19d790 | ||
|
|
baa9bad6f8 | ||
|
|
94e13cb93b | ||
|
|
c800dd4bf9 | ||
|
|
7c91d200e4 | ||
|
|
d45daeab7a | ||
|
|
b3ccea5f0e | ||
|
|
3178086dd7 | ||
|
|
1e4e5b3c65 | ||
|
|
7c8e74716d | ||
|
|
46a5799079 | ||
|
|
9feb788108 | ||
|
|
e2f3e005ef | ||
|
|
9d54ea4c49 | ||
|
|
671e875f12 | ||
|
|
f708fce112 | ||
|
|
460c466053 | ||
|
|
84ad0a4cac | ||
|
|
129690e901 | ||
|
|
9191861cab | ||
|
|
c05e42f597 | ||
|
|
09a0c9c3f2 | ||
|
|
859d0c8f1a | ||
|
|
baaeaf43b1 | ||
|
|
76b376aef7 | ||
|
|
3df6a24a0a | ||
|
|
efa7a1d7b5 | ||
|
|
5c6406c1e7 | ||
|
|
3392251dda | ||
|
|
bbad4539b1 | ||
|
|
6b0d7c0ce1 | ||
|
|
e03fbadcaa | ||
|
|
42b003db62 | ||
|
|
d8eed66a1d | ||
|
|
8083ba086b | ||
|
|
02210f1f8d | ||
|
|
817a19c757 | ||
|
|
36da07f277 | ||
|
|
83056bbf4f | ||
|
|
37a4a80996 | ||
|
|
9d4256306e | ||
|
|
ddfc9249ec | ||
|
|
a431b7050f | ||
|
|
debd4b23df | ||
|
|
410610349b | ||
|
|
3ac7a559df | ||
|
|
7c878b1ee3 | ||
|
|
a1506d0f31 | ||
|
|
15ff2f3282 | ||
|
|
ffb0abe4da | ||
|
|
3dbf4f9b19 | ||
|
|
b02cd5c4f0 | ||
|
|
0c768bb3d6 | ||
|
|
f78bcf3e4b | ||
|
|
535e725618 | ||
|
|
4aaeda3fbb | ||
|
|
600243c87d | ||
|
|
82aebf6647 | ||
|
|
89ae98b035 | ||
|
|
ed3b195b26 | ||
|
|
f54089e072 | ||
|
|
6bb82b2204 | ||
|
|
d77d69a331 | ||
|
|
297ec9005c | ||
|
|
5649d4410a | ||
|
|
db32479c4e | ||
|
|
8fa53f4e32 | ||
|
|
a7acebc5d0 | ||
|
|
954bb23bb8 | ||
|
|
7134782245 | ||
|
|
5a42dc9857 | ||
|
|
9caf45af78 | ||
|
|
56e3ce328e | ||
|
|
6de1828ffe | ||
|
|
b2e275a6cd | ||
|
|
45dba68b15 | ||
|
|
31c670252c | ||
|
|
1831e44eef | ||
|
|
71f5de3bf9 | ||
|
|
88a0911f93 | ||
|
|
8686c4a6e5 | ||
|
|
68acb5d790 | ||
|
|
2c50ae2290 | ||
|
|
f09500f289 | ||
|
|
1147aeda14 | ||
|
|
7b57f73b26 | ||
|
|
45cc9e8245 | ||
|
|
795771e3d6 | ||
|
|
2ef1a93eaf | ||
|
|
6ccbea8bd9 | ||
|
|
003eb5b995 | ||
|
|
0ab6f58d36 | ||
|
|
763f1e5d11 | ||
|
|
16cf91f669 | ||
|
|
0d8db25c3c | ||
|
|
2084d0958b | ||
|
|
a67a077024 | ||
|
|
f1ece58c7b | ||
|
|
fd34d5a972 | ||
|
|
bdf3598367 | ||
|
|
b9d35fa626 | ||
|
|
7da993a4c7 | ||
|
|
ddb16f899f | ||
|
|
dea9926e4f | ||
|
|
61eb55f736 | ||
|
|
e2521eea94 | ||
|
|
8f859cd600 | ||
|
|
0232384fe2 | ||
|
|
7873631613 | ||
|
|
466257d3be | ||
|
|
1ff7be38eb | ||
|
|
c87b5cec65 | ||
|
|
80afc18892 | ||
|
|
34a947ac6d | ||
|
|
d855e56a2f | ||
|
|
7ee98c1b03 | ||
|
|
af8569a605 | ||
|
|
d6199c9598 | ||
|
|
ba7db72093 | ||
|
|
83b040b39f | ||
|
|
d8a3e82f1e | ||
|
|
c40f9c9127 | ||
|
|
7e612458a0 | ||
|
|
72b791cc5e | ||
|
|
e46b739311 | ||
|
|
daf6a20e1d | ||
|
|
ba45936d77 | ||
|
|
ca64bd127e | ||
|
|
b93eb4911c | ||
|
|
20e9449bb1 | ||
|
|
c2e9d67bcb | ||
|
|
3989a735ac | ||
|
|
ede15c4fe5 | ||
|
|
99976dd560 | ||
|
|
0500550fc0 | ||
|
|
c5c2578d0f | ||
|
|
b843538fea | ||
|
|
9b56ffc87c | ||
|
|
a4f0dbbca8 | ||
|
|
b530f7a6cf | ||
|
|
421e9115cc | ||
|
|
b812887f6e | ||
|
|
b3f677d32f | ||
|
|
b755bcd1bf | ||
|
|
8d79680b81 | ||
|
|
a013192bf8 | ||
|
|
dceb6f9993 | ||
|
|
d922e58278 | ||
|
|
8caad44ade | ||
|
|
22d1bd7620 | ||
|
|
effa5c46c5 | ||
|
|
cebbcdf256 | ||
|
|
f557d16997 | ||
|
|
3e0fcfbfcf | ||
|
|
7377b4f15b | ||
|
|
ecbe7f6ad9 | ||
|
|
084c14175e | ||
|
|
5da77a6b4f | ||
|
|
84f55691eb | ||
|
|
4d5033f03c | ||
|
|
ff6c344a15 | ||
|
|
104498dec0 | ||
|
|
8654604b9b | ||
|
|
2bc64a3be8 | ||
|
|
2aaf37b766 | ||
|
|
47bd72e1cd | ||
|
|
29eedee3ec | ||
|
|
a349fdd0cf | ||
|
|
cf5248ec54 | ||
|
|
4fc53e7220 | ||
|
|
f8392ab0be | ||
|
|
43d3f84993 | ||
|
|
72e846894e | ||
|
|
84d2261391 | ||
|
|
608da9f64b | ||
|
|
91ace06f8b | ||
|
|
cdbb5e98f5 | ||
|
|
92e59672f9 | ||
|
|
3568b7a12a | ||
|
|
4195b7f2dc | ||
|
|
17fe1a938a | ||
|
|
c699455b58 | ||
|
|
000bc4f661 | ||
|
|
9308e1b388 | ||
|
|
1452317d45 | ||
|
|
41bfe4f4d3 | ||
|
|
b79b4c405a | ||
|
|
e7e281083f | ||
|
|
3459c7e8d6 | ||
|
|
6258f9a5e4 | ||
|
|
61b4108dce | ||
|
|
ce7aa1be62 | ||
|
|
e2226e7492 | ||
|
|
b903700077 | ||
|
|
46067dec8f | ||
|
|
11295a28f6 | ||
|
|
4e241d0448 | ||
|
|
a6e56a208c | ||
|
|
ccb0f410ab | ||
|
|
1a44b94c85 | ||
|
|
53e55728db | ||
|
|
53a3624aef | ||
|
|
d6ceaad220 | ||
|
|
e67b98d6fe | ||
|
|
a3f07057f4 | ||
|
|
610e087bcd | ||
|
|
fca46f1a32 | ||
|
|
f7d12e65e5 | ||
|
|
27c9ce681b | ||
|
|
1993950cd7 | ||
|
|
f475dbabb7 | ||
|
|
b7b530fe60 | ||
|
|
4fbfa1d4e3 | ||
|
|
a77069a634 | ||
|
|
aa10008d1b | ||
|
|
e6e537d799 | ||
|
|
2e91feb861 | ||
|
|
d805ba8eb8 | ||
|
|
ad07ad2014 | ||
|
|
5d65ff14d5 | ||
|
|
99ceb5d72c | ||
|
|
7f2f1d70b1 | ||
|
|
07f40351fa | ||
|
|
6525106116 | ||
|
|
3e116fcc52 | ||
|
|
cb968d782a | ||
|
|
10507d9c53 | ||
|
|
10c4cd9b7f | ||
|
|
38dc69e4a2 | ||
|
|
377306dc80 | ||
|
|
da7059dc79 | ||
|
|
9ec6541322 | ||
|
|
e80e31da42 | ||
|
|
a69a934029 |
160 changed files with 16134 additions and 9267 deletions
46
.github/workflows/build.yml
vendored
46
.github/workflows/build.yml
vendored
|
|
@ -18,14 +18,14 @@ jobs:
|
|||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-latest, macos-latest, windows-latest ]
|
||||
os: [ ubuntu-latest, windows-latest ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.20.8
|
||||
go-version: 'stable'
|
||||
|
||||
- name: Linux
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
|
|
@ -36,49 +36,53 @@ jobs:
|
|||
sudo apt-get -qq install -y \
|
||||
make pkg-config \
|
||||
libvpx-dev libx264-dev libopus-dev libyuv-dev libjpeg-turbo8-dev \
|
||||
libsdl2-dev libgl1-mesa-glx
|
||||
libsdl2-dev libgl1 libglx-mesa0 libspeexdsp-dev
|
||||
|
||||
make build
|
||||
xvfb-run --auto-servernum make test verify-cores
|
||||
|
||||
- name: macOS
|
||||
if: matrix.os == 'macos-latest'
|
||||
if: matrix.os == 'macos-12'
|
||||
run: |
|
||||
brew install pkg-config libvpx x264 opus sdl2 jpeg-turbo
|
||||
brew install libvpx x264 sdl2 speexdsp
|
||||
make build test verify-cores
|
||||
|
||||
- uses: msys2/setup-msys2@v2
|
||||
if: matrix.os == 'windows-latest'
|
||||
with:
|
||||
msystem: MINGW64
|
||||
msystem: ucrt64
|
||||
path-type: inherit
|
||||
release: false
|
||||
install: >
|
||||
mingw-w64-x86_64-gcc
|
||||
mingw-w64-x86_64-pkgconf
|
||||
mingw-w64-x86_64-dlfcn
|
||||
mingw-w64-x86_64-libvpx
|
||||
mingw-w64-x86_64-opus
|
||||
mingw-w64-x86_64-x264-git
|
||||
mingw-w64-x86_64-SDL2
|
||||
mingw-w64-x86_64-libyuv
|
||||
mingw-w64-x86_64-libjpeg-turbo
|
||||
mingw-w64-ucrt-x86_64-gcc
|
||||
mingw-w64-ucrt-x86_64-pkgconf
|
||||
mingw-w64-ucrt-x86_64-dlfcn
|
||||
mingw-w64-ucrt-x86_64-libvpx
|
||||
mingw-w64-ucrt-x86_64-opus
|
||||
mingw-w64-ucrt-x86_64-libx264
|
||||
mingw-w64-ucrt-x86_64-SDL2
|
||||
mingw-w64-ucrt-x86_64-libyuv
|
||||
mingw-w64-ucrt-x86_64-libjpeg-turbo
|
||||
mingw-w64-ucrt-x86_64-speexdsp
|
||||
|
||||
- name: Windows
|
||||
if: matrix.os == 'windows-latest'
|
||||
env:
|
||||
MESA_VERSION: '24.0.7'
|
||||
MESA_GL_VERSION_OVERRIDE: 3.3COMPAT
|
||||
shell: msys2 {0}
|
||||
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
|
||||
set MSYSTEM=UCRT64
|
||||
|
||||
wget -q https://github.com/pal1000/mesa-dist-win/releases/download/$MESA_VERSION/mesa3d-$MESA_VERSION-release-msvc.7z
|
||||
"/c/Program Files/7-Zip/7z.exe" x mesa3d-$MESA_VERSION-release-msvc.7z -omesa
|
||||
echo -e " 1\r\n 9\r\n " >> commands
|
||||
./mesa/systemwidedeploy.cmd < ./commands
|
||||
|
||||
make build test verify-cores
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: emulator-test-frames
|
||||
name: emulator-test-frames-${{ matrix.os }}
|
||||
path: _rendered/*.png
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ coordinator:
|
|||
debug: true
|
||||
server:
|
||||
address:
|
||||
frameOptions: SAMEORIGIN
|
||||
https: true
|
||||
tls:
|
||||
domain: cloudretro.io
|
||||
|
|
@ -21,19 +22,19 @@ worker:
|
|||
https: true
|
||||
tls:
|
||||
address: :444
|
||||
domain: cloudretro.io
|
||||
# domain: cloudretro.io
|
||||
|
||||
emulator:
|
||||
libretro:
|
||||
logLevel: 1
|
||||
cores:
|
||||
list:
|
||||
dos:
|
||||
uniqueSaveDir: true
|
||||
mame:
|
||||
options:
|
||||
"fbneo-diagnostic-input": "Hold Start"
|
||||
nes:
|
||||
scale: 2
|
||||
pcsx:
|
||||
altRepo: true
|
||||
snes:
|
||||
scale: 2
|
||||
|
|
|
|||
85
.github/workflows/cd/docker-compose.yml
vendored
85
.github/workflows/cd/docker-compose.yml
vendored
|
|
@ -1,15 +1,30 @@
|
|||
version: "3.9"
|
||||
|
||||
x-params:
|
||||
&default-params
|
||||
x-params: &default-params
|
||||
image: ghcr.io/giongto35/cloud-game/cloud-game:${IMAGE_TAG:-master}
|
||||
network_mode: "host"
|
||||
privileged: true
|
||||
restart: always
|
||||
security_opt:
|
||||
- seccomp:unconfined
|
||||
- seccomp=unconfined
|
||||
logging:
|
||||
driver: "journald"
|
||||
x-worker: &worker
|
||||
depends_on:
|
||||
- coordinator
|
||||
command: ./worker
|
||||
volumes:
|
||||
- ${APP_DIR:-/cloud-game}/cache:/usr/local/share/cloud-game/assets/cache
|
||||
- ${APP_DIR:-/cloud-game}/cores:/usr/local/share/cloud-game/assets/cores
|
||||
- ${APP_DIR:-/cloud-game}/games:/usr/local/share/cloud-game/assets/games
|
||||
- ${APP_DIR:-/cloud-game}/libretro:/usr/local/share/cloud-game/libretro
|
||||
- ${APP_DIR:-/cloud-game}/home:/root/.cr
|
||||
- x11:/tmp/.X11-unix
|
||||
healthcheck:
|
||||
test: curl -f https://cloudretro.io/echo || exit 1
|
||||
interval: 1m
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
start_interval: 5s
|
||||
|
||||
services:
|
||||
|
||||
|
|
@ -17,38 +32,62 @@ services:
|
|||
<<: *default-params
|
||||
command: ./coordinator
|
||||
environment:
|
||||
- CLOUD_GAME_COORDINATOR_LIBRARY_BASEPATH=/usr/local/share/cloud-game/assets/games
|
||||
- CLOUD_GAME_LIBRARY_BASEPATH=/usr/local/share/cloud-game/assets/games
|
||||
volumes:
|
||||
- ${APP_DIR:-/cloud-game}/cache:/usr/local/share/cloud-game/assets/cache
|
||||
- ${APP_DIR:-/cloud-game}/games:/usr/local/share/cloud-game/assets/games
|
||||
- ${APP_DIR:-/cloud-game}/home:/root/.cr
|
||||
|
||||
worker:
|
||||
<<: *default-params
|
||||
depends_on:
|
||||
- coordinator
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: 4
|
||||
worker01:
|
||||
<<: [ *default-params, *worker ]
|
||||
environment:
|
||||
- DISPLAY=:99
|
||||
- MESA_GL_VERSION_OVERRIDE=4.5
|
||||
- CLOUD_GAME_WORKER_LIBRARY_BASEPATH=/usr/local/share/cloud-game/assets/games
|
||||
- CLOUD_GAME_LIBRARY_BASEPATH=/usr/local/share/cloud-game/assets/games
|
||||
- CLOUD_GAME_EMULATOR_LIBRETRO_CORES_PATHS_LIBS=/usr/local/share/cloud-game/assets/cores
|
||||
command: ./worker
|
||||
volumes:
|
||||
- ${APP_DIR:-/cloud-game}/cache:/usr/local/share/cloud-game/assets/cache
|
||||
- ${APP_DIR:-/cloud-game}/cores:/usr/local/share/cloud-game/assets/cores
|
||||
- ${APP_DIR:-/cloud-game}/games:/usr/local/share/cloud-game/assets/games
|
||||
- ${APP_DIR:-/cloud-game}/libretro:/usr/local/share/cloud-game/libretro
|
||||
- ${APP_DIR:-/cloud-game}/home:/root/.cr
|
||||
- x11:/tmp/.X11-unix
|
||||
- CLOUD_GAME_WORKER_SERVER_TLS_DOMAIN=cloudretro.io
|
||||
- CLOUD_GAME_WORKER_SERVER_TLS_ADDRESS=:444
|
||||
healthcheck:
|
||||
test: curl -f https://cloudretro.io:444/echo || exit 1
|
||||
worker02:
|
||||
<<: [ *default-params, *worker ]
|
||||
environment:
|
||||
- CLOUD_GAME_WORKER_SERVER_TLS_ADDRESS=:445
|
||||
- DISPLAY=:99
|
||||
- MESA_GL_VERSION_OVERRIDE=4.5
|
||||
- CLOUD_GAME_LIBRARY_BASEPATH=/usr/local/share/cloud-game/assets/games
|
||||
- CLOUD_GAME_EMULATOR_LIBRETRO_CORES_PATHS_LIBS=/usr/local/share/cloud-game/assets/cores
|
||||
- CLOUD_GAME_WORKER_SERVER_TLS_DOMAIN=cloudretro.io
|
||||
healthcheck:
|
||||
test: curl -f https://cloudretro.io:445/echo || exit 1
|
||||
worker03:
|
||||
<<: [ *default-params, *worker ]
|
||||
environment:
|
||||
- DISPLAY=:99
|
||||
- MESA_GL_VERSION_OVERRIDE=4.5
|
||||
- CLOUD_GAME_LIBRARY_BASEPATH=/usr/local/share/cloud-game/assets/games
|
||||
- CLOUD_GAME_EMULATOR_LIBRETRO_CORES_PATHS_LIBS=/usr/local/share/cloud-game/assets/cores
|
||||
- CLOUD_GAME_WORKER_SERVER_TLS_DOMAIN=cloudretro.io
|
||||
- CLOUD_GAME_WORKER_SERVER_TLS_ADDRESS=:446
|
||||
healthcheck:
|
||||
test: curl -f https://cloudretro.io:446/echo || exit 1
|
||||
worker04:
|
||||
<<: [ *default-params, *worker ]
|
||||
environment:
|
||||
- DISPLAY=:99
|
||||
- MESA_GL_VERSION_OVERRIDE=4.5
|
||||
- CLOUD_GAME_LIBRARY_BASEPATH=/usr/local/share/cloud-game/assets/games
|
||||
- CLOUD_GAME_EMULATOR_LIBRETRO_CORES_PATHS_LIBS=/usr/local/share/cloud-game/assets/cores
|
||||
- CLOUD_GAME_WORKER_SERVER_TLS_DOMAIN=cloudretro.io
|
||||
- CLOUD_GAME_WORKER_SERVER_TLS_ADDRESS=:447
|
||||
healthcheck:
|
||||
test: curl -f https://cloudretro.io:447/echo || exit 1
|
||||
|
||||
xvfb:
|
||||
image: kcollins/xvfb:latest
|
||||
volumes:
|
||||
- x11:/tmp/.X11-unix
|
||||
command: [":99", "-screen", "0", "320x240x16" ]
|
||||
command: [ ":99", "-screen", "0", "320x240x16" ]
|
||||
|
||||
volumes:
|
||||
x11:
|
||||
|
|
|
|||
12
Dockerfile
12
Dockerfile
|
|
@ -2,8 +2,8 @@ ARG BUILD_PATH=/tmp/cloud-game
|
|||
ARG VERSION=master
|
||||
|
||||
# base build stage
|
||||
FROM ubuntu:lunar AS build0
|
||||
ARG GO=1.20.8
|
||||
FROM ubuntu:plucky AS build0
|
||||
ARG GO=1.26rc1
|
||||
ARG GO_DIST=go${GO}.linux-amd64.tar.gz
|
||||
|
||||
ADD https://go.dev/dl/$GO_DIST ./
|
||||
|
|
@ -21,7 +21,7 @@ RUN apt-get -q update && apt-get -q install --no-install-recommends -y \
|
|||
FROM build0 AS build_coordinator
|
||||
ARG BUILD_PATH
|
||||
ARG VERSION
|
||||
ENV GIT_VERSION ${VERSION}
|
||||
ENV GIT_VERSION=${VERSION}
|
||||
|
||||
WORKDIR ${BUILD_PATH}
|
||||
|
||||
|
|
@ -41,7 +41,7 @@ RUN ${BUILD_PATH}/scripts/version.sh ./web/index.html ${VERSION} && \
|
|||
FROM build0 AS build_worker
|
||||
ARG BUILD_PATH
|
||||
ARG VERSION
|
||||
ENV GIT_VERSION ${VERSION}
|
||||
ENV GIT_VERSION=${VERSION}
|
||||
|
||||
WORKDIR ${BUILD_PATH}
|
||||
|
||||
|
|
@ -54,6 +54,7 @@ RUN apt-get -q update && apt-get -q install --no-install-recommends -y \
|
|||
libyuv-dev \
|
||||
libjpeg-turbo8-dev \
|
||||
libx264-dev \
|
||||
libspeexdsp-dev \
|
||||
pkg-config \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
|
@ -73,9 +74,10 @@ COPY --from=build_coordinator /usr/local/share/cloud-game /cloud-game
|
|||
# autocertbot (SSL) requires these on the first run
|
||||
COPY --from=build_coordinator /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
|
||||
FROM ubuntu:lunar AS worker
|
||||
FROM ubuntu:plucky AS worker
|
||||
|
||||
RUN apt-get -q update && apt-get -q install --no-install-recommends -y \
|
||||
curl \
|
||||
libx11-6 \
|
||||
libxext6 \
|
||||
&& apt-get autoremove \
|
||||
|
|
|
|||
4
Makefile
4
Makefile
|
|
@ -2,9 +2,9 @@ PROJECT = cloud-game
|
|||
REPO_ROOT = github.com/giongto35
|
||||
ROOT = ${REPO_ROOT}/${PROJECT}
|
||||
|
||||
CGO_CFLAGS='-g -O3 -funroll-loops'
|
||||
CGO_CFLAGS='-g -O3'
|
||||
CGO_LDFLAGS='-g -O3'
|
||||
GO_TAGS=static
|
||||
GO_TAGS=
|
||||
|
||||
.PHONY: clean test
|
||||
|
||||
|
|
|
|||
18
README.md
18
README.md
|
|
@ -15,8 +15,7 @@ Discord: [Join Us](https://discord.gg/sXRQZa2zeP)
|
|||
|
||||
## Try it at **[cloudretro.io](https://cloudretro.io)**
|
||||
|
||||
Direct play an existing game: *
|
||||
*[Pokemon Emerald](https://cloudretro.io/?id=1bd37d4b5dfda87c___Pokemon%20-%20Emerald%20Version%20(U))**
|
||||
Direct play an existing game: **[Pokemon Emerald](https://cloudretro.io/?id=1bd37d4b5dfda87c___Pokemon%20-%20Emerald%20Version%20(U))**
|
||||
|
||||
## Introduction
|
||||
|
||||
|
|
@ -57,19 +56,24 @@ a better sense of performance.
|
|||
* Install [Go](https://golang.org/doc/install)
|
||||
* Install [libvpx](https://www.webmproject.org/code/), [libx264](https://www.videolan.org/developers/x264.html)
|
||||
, [libopus](http://opus-codec.org/), [pkg-config](https://www.freedesktop.org/wiki/Software/pkg-config/)
|
||||
, [sdl2](https://wiki.libsdl.org/Installation)
|
||||
, [sdl2](https://wiki.libsdl.org/Installation), [libyuv](https://chromium.googlesource.com/libyuv/libyuv/)+[libjpeg-turbo](https://github.com/libjpeg-turbo/libjpeg-turbo)
|
||||
|
||||
```
|
||||
# Ubuntu / Windows (WSL2)
|
||||
apt-get install -y make gcc pkg-config libvpx-dev libx264-dev libopus-dev libsdl2-dev libyuv-dev libjpeg-turbo8-dev
|
||||
apt-get install -y make gcc pkg-config libvpx-dev libx264-dev libopus-dev libsdl2-dev libyuv-dev libjpeg-turbo8-dev libspeexdsp-dev
|
||||
|
||||
# MacOS
|
||||
brew install pkg-config libvpx x264 opus sdl2 jpeg-turbo
|
||||
brew install pkg-config libvpx x264 opus sdl2 jpeg-turbo speexdsp
|
||||
|
||||
# Windows (MSYS2)
|
||||
pacman -Sy --noconfirm --needed git make mingw-w64-x86_64-{gcc,pkgconf,dlfcn,libvpx,opus,x264-git,SDL2,libyuv,libjpeg-turbo}
|
||||
pacman -Sy --noconfirm --needed git make mingw-w64-ucrt-x86_64-{gcc,pkgconf,dlfcn,libvpx,opus,libx264,SDL2,libyuv,libjpeg-turbo,speexdsp}
|
||||
```
|
||||
|
||||
(You don't need to download libyuv on macOS)
|
||||
|
||||
(If you need to use the app on an older version of Ubuntu that does not have libyuv (when it says: unable to locate package libyuv-dev), you can add a custom apt repository:
|
||||
`add sudo add-apt-repository ppa:savoury1/graphics`)
|
||||
|
||||
Because the coordinator and workers need to run simultaneously. Workers connect to the coordinator.
|
||||
|
||||
1. Script
|
||||
|
|
@ -121,7 +125,7 @@ application [installed](https://docs.docker.com/compose/install/).
|
|||
|
||||
By clicking these deep link, you can join the game directly and play it together with other people.
|
||||
|
||||
- [Play Pokemon Emerald](https://cloudretro.io/?id=652e45d78d2b91cd%7CPokemon%20-%20Emerald%20Version%20%28U%29)
|
||||
- [Play Pokemon Emerald](https://cloudretro.io/?id=652e45d78d2b91cd___Pokemon%20-%20Emerald%20Version%20(U))
|
||||
- [Fire Emblem](https://cloudretro.io/?id=314ea4d7f9c94d25___Fire%20Emblem%20%28U%29%20%5B%21%5D)
|
||||
- [Samurai Showdown 4](https://cloudretro.io/?id=733c73064c368832___samsho4)
|
||||
- [Metal Slug X](https://cloudretro.io/?id=2a9c4b3f1c872d28___mslugx)
|
||||
|
|
|
|||
2
assets/games/dos/rogue.conf
Normal file
2
assets/games/dos/rogue.conf
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[autoexec]
|
||||
ROGUE.EXE
|
||||
BIN
assets/games/dos/rogue.zip
Normal file
BIN
assets/games/dos/rogue.zip
Normal file
Binary file not shown.
Binary file not shown.
|
|
@ -1,4 +1,3 @@
|
|||
version: '3'
|
||||
services:
|
||||
|
||||
cloud-game:
|
||||
|
|
|
|||
86
go.mod
86
go.mod
|
|
@ -1,54 +1,62 @@
|
|||
module github.com/giongto35/cloud-game/v3
|
||||
|
||||
go 1.20
|
||||
go 1.25
|
||||
|
||||
require (
|
||||
github.com/VictoriaMetrics/metrics v1.24.0
|
||||
github.com/VictoriaMetrics/metrics v1.40.2
|
||||
github.com/cavaliergopher/grab/v3 v3.0.1
|
||||
github.com/fsnotify/fsnotify v1.6.0
|
||||
github.com/goccy/go-json v0.10.2
|
||||
github.com/gofrs/flock v0.8.1
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/knadh/koanf/maps v0.1.1
|
||||
github.com/knadh/koanf/v2 v2.0.1
|
||||
github.com/pion/ice/v3 v3.0.1
|
||||
github.com/pion/interceptor v0.1.22
|
||||
github.com/pion/logging v0.2.2
|
||||
github.com/pion/webrtc/v4 v4.0.0-beta.5
|
||||
github.com/rs/xid v1.5.0
|
||||
github.com/rs/zerolog v1.31.0
|
||||
github.com/veandco/go-sdl2 v0.4.35
|
||||
golang.org/x/crypto v0.14.0
|
||||
golang.org/x/image v0.13.0
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/goccy/go-json v0.10.5
|
||||
github.com/gofrs/flock v0.13.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/knadh/koanf/maps v0.1.2
|
||||
github.com/knadh/koanf/v2 v2.3.0
|
||||
github.com/minio/minio-go/v7 v7.0.97
|
||||
github.com/pion/ice/v4 v4.1.0
|
||||
github.com/pion/interceptor v0.1.42
|
||||
github.com/pion/logging v0.2.4
|
||||
github.com/pion/webrtc/v4 v4.1.8
|
||||
github.com/rs/xid v1.6.0
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/veandco/go-sdl2 v0.4.40
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/image v0.34.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.3.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/klauspost/crc32 v1.3.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/minio/crc64nvme v1.1.1 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/pion/datachannel v1.5.5 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.7 // indirect
|
||||
github.com/pion/mdns v0.0.9 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pion/datachannel v1.5.10 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.9 // indirect
|
||||
github.com/pion/mdns/v2 v2.1.0 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtcp v1.2.10 // indirect
|
||||
github.com/pion/rtp v1.8.2 // indirect
|
||||
github.com/pion/sctp v1.8.9 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.6 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.0 // indirect
|
||||
github.com/pion/stun/v2 v2.0.0 // indirect
|
||||
github.com/pion/transport/v2 v2.2.4 // indirect
|
||||
github.com/pion/transport/v3 v3.0.1 // indirect
|
||||
github.com/pion/turn/v3 v3.0.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/stretchr/testify v1.8.4 // indirect
|
||||
github.com/pion/rtcp v1.2.16 // indirect
|
||||
github.com/pion/rtp v1.8.27 // indirect
|
||||
github.com/pion/sctp v1.8.41 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.17 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.9 // indirect
|
||||
github.com/pion/stun/v3 v3.0.2 // indirect
|
||||
github.com/pion/transport/v3 v3.1.1 // indirect
|
||||
github.com/pion/turn/v4 v4.1.3 // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||
github.com/tinylib/msgp v1.6.1 // indirect
|
||||
github.com/valyala/fastrand v1.1.0 // indirect
|
||||
github.com/valyala/histogram v1.2.0 // indirect
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
)
|
||||
|
|
|
|||
323
go.sum
323
go.sum
|
|
@ -1,246 +1,133 @@
|
|||
github.com/VictoriaMetrics/metrics v1.24.0 h1:ILavebReOjYctAGY5QU2F9X0MYvkcrG3aEn2RKa1Zkw=
|
||||
github.com/VictoriaMetrics/metrics v1.24.0/go.mod h1:eFT25kvsTidQFHb6U0oa0rTrDRdz4xTYjpL8+UPohys=
|
||||
github.com/VictoriaMetrics/metrics v1.40.2 h1:OVSjKcQEx6JAwGeu8/KQm9Su5qJ72TMEW4xYn5vw3Ac=
|
||||
github.com/VictoriaMetrics/metrics v1.40.2/go.mod h1:XE4uudAAIRaJE614Tl5HMrtoEU6+GDZO4QTnNSsZRuA=
|
||||
github.com/cavaliergopher/grab/v3 v3.0.1 h1:4z7TkBfmPjmLAAmkkAZNX/6QJ1nNFdv3SdIHXju0Fr4=
|
||||
github.com/cavaliergopher/grab/v3 v3.0.1/go.mod h1:1U/KNnD+Ft6JJiYoYBAimKH2XrYptb8Kl3DFGmsjpq4=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs=
|
||||
github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
|
||||
github.com/knadh/koanf/v2 v2.0.1 h1:1dYGITt1I23x8cfx8ZnldtezdyaZtfAuRtIFOiRzK7g=
|
||||
github.com/knadh/koanf/v2 v2.0.1/go.mod h1:ZeiIlIDXTE7w1lMT6UVcNiRAS2/rCeLn/GdLNvY1Dus=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
|
||||
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
|
||||
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
|
||||
github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
|
||||
github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM=
|
||||
github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
|
||||
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.0.97 h1:lqhREPyfgHTB/ciX8k2r8k0D93WaFqxbJX36UZq5occ=
|
||||
github.com/minio/minio-go/v7 v7.0.97/go.mod h1:re5VXuo0pwEtoNLsNuSr0RrLfT/MBtohwdaSmPPSRSk=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8=
|
||||
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
|
||||
github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
|
||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/ice/v3 v3.0.1 h1:dwWGgIFDlYrKrCW13LihifuFabGw375hoU0347S9wNw=
|
||||
github.com/pion/ice/v3 v3.0.1/go.mod h1:j4tfTlj4aSEQN9gP3IdliSHcUTWTu9tlOZL0c59MFXo=
|
||||
github.com/pion/interceptor v0.1.22 h1:khhimAF0/VmGaIfeE+bA3X1jm0lD8C8HOGcU7vpWcPA=
|
||||
github.com/pion/interceptor v0.1.22/go.mod h1:wkbPYAak5zKsfpVDYMtEfWEy8D4zL+rpxCxPImLOg3Y=
|
||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/mdns v0.0.8/go.mod h1:hYE72WX8WDveIhg7fmXgMKivD3Puklk0Ymzog0lSyaI=
|
||||
github.com/pion/mdns v0.0.9 h1:7Ue5KZsqq8EuqStnpPWV33vYYEH0+skdDN5L7EiEsI4=
|
||||
github.com/pion/mdns v0.0.9/go.mod h1:2JA5exfxwzXiCihmxpTKgFUpiQws2MnipoPK09vecIc=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
|
||||
github.com/pion/dtls/v3 v3.0.9 h1:4AijfFRm8mAjd1gfdlB1wzJF3fjjR/VPIpJgkEtvYmM=
|
||||
github.com/pion/dtls/v3 v3.0.9/go.mod h1:abApPjgadS/ra1wvUzHLc3o2HvoxppAh+NZkyApL4Os=
|
||||
github.com/pion/ice/v4 v4.1.0 h1:YlxIii2bTPWyC08/4hdmtYq4srbrY0T9xcTsTjldGqU=
|
||||
github.com/pion/ice/v4 v4.1.0/go.mod h1:5gPbzYxqenvn05k7zKPIZFuSAufolygiy6P1U9HzvZ4=
|
||||
github.com/pion/interceptor v0.1.42 h1:0/4tvNtruXflBxLfApMVoMubUMik57VZ+94U0J7cmkQ=
|
||||
github.com/pion/interceptor v0.1.42/go.mod h1:g6XYTChs9XyolIQFhRHOOUS+bGVGLRfgTCUzH29EfVU=
|
||||
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||
github.com/pion/mdns/v2 v2.1.0 h1:3IJ9+Xio6tWYjhN6WwuY142P/1jA0D5ERaIqawg/fOY=
|
||||
github.com/pion/mdns/v2 v2.1.0/go.mod h1:pcez23GdynwcfRU1977qKU0mDxSeucttSHbCSfFOd9A=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc=
|
||||
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
|
||||
github.com/pion/rtp v1.8.1/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/rtp v1.8.2 h1:oKMM0K1/QYQ5b5qH+ikqDSZRipP5mIxPJcgcvw5sH0w=
|
||||
github.com/pion/rtp v1.8.2/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
|
||||
github.com/pion/sctp v1.8.9 h1:TP5ZVxV5J7rz7uZmbyvnUvsn7EJ2x/5q9uhsTtXbI3g=
|
||||
github.com/pion/sctp v1.8.9/go.mod h1:cMLT45jqw3+jiJCrtHVwfQLnfR0MGZ4rgOJwUOIqLkI=
|
||||
github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw=
|
||||
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
|
||||
github.com/pion/srtp/v3 v3.0.0 h1:dH5nZUTxN+JDu4otle8Dfh5E/MHR6m8/aib7eD22QDc=
|
||||
github.com/pion/srtp/v3 v3.0.0/go.mod h1:WxJGk0scShe0UdUidDgR0kDHywX7JN83JOYPkYiLdpM=
|
||||
github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0=
|
||||
github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ=
|
||||
github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
|
||||
github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
|
||||
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
|
||||
github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo=
|
||||
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM=
|
||||
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
|
||||
github.com/pion/turn/v3 v3.0.1 h1:wLi7BTQr6/Q20R0vt/lHbjv6y4GChFtC33nkYbasoT8=
|
||||
github.com/pion/turn/v3 v3.0.1/go.mod h1:MrJDKgqryDyWy1/4NT9TWfXWGMC7UHT6pJIv1+gMeNE=
|
||||
github.com/pion/webrtc/v4 v4.0.0-beta.5 h1:mW4Z8I50IG2ATa9i6tgClGMTdvTUHrxfAefReI0V2QE=
|
||||
github.com/pion/webrtc/v4 v4.0.0-beta.5/go.mod h1:epqb0qKpAf5GWPMeDmK1W9Za+dJqlDcx4iKp7+aem6I=
|
||||
github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo=
|
||||
github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo=
|
||||
github.com/pion/rtp v1.8.26 h1:VB+ESQFQhBXFytD+Gk8cxB6dXeVf2WQzg4aORvAvAAc=
|
||||
github.com/pion/rtp v1.8.26/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
|
||||
github.com/pion/rtp v1.8.27 h1:kbWTdZr62RDlYjatVAW4qFwrAu9XcGnwMsofCfAHlOU=
|
||||
github.com/pion/rtp v1.8.27/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
|
||||
github.com/pion/sctp v1.8.41 h1:20R4OHAno4Vky3/iE4xccInAScAa83X6nWUfyc65MIs=
|
||||
github.com/pion/sctp v1.8.41/go.mod h1:2wO6HBycUH7iCssuGyc2e9+0giXVW0pyCv3ZuL8LiyY=
|
||||
github.com/pion/sdp/v3 v3.0.16 h1:0dKzYO6gTAvuLaAKQkC02eCPjMIi4NuAr/ibAwrGDCo=
|
||||
github.com/pion/sdp/v3 v3.0.16/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=
|
||||
github.com/pion/sdp/v3 v3.0.17 h1:9SfLAW/fF1XC8yRqQ3iWGzxkySxup4k4V7yN8Fs8nuo=
|
||||
github.com/pion/sdp/v3 v3.0.17/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=
|
||||
github.com/pion/srtp/v3 v3.0.9 h1:lRGF4G61xxj+m/YluB3ZnBpiALSri2lTzba0kGZMrQY=
|
||||
github.com/pion/srtp/v3 v3.0.9/go.mod h1:E+AuWd7Ug2Fp5u38MKnhduvpVkveXJX6J4Lq4rxUYt8=
|
||||
github.com/pion/stun/v3 v3.0.2 h1:BJuGEN2oLrJisiNEJtUTJC4BGbzbfp37LizfqswblFU=
|
||||
github.com/pion/stun/v3 v3.0.2/go.mod h1:JFJKfIWvt178MCF5H/YIgZ4VX3LYE77vca4b9HP60SA=
|
||||
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
|
||||
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
|
||||
github.com/pion/turn/v4 v4.1.3 h1:jVNW0iR05AS94ysEtvzsrk3gKs9Zqxf6HmnsLfRvlzA=
|
||||
github.com/pion/turn/v4 v4.1.3/go.mod h1:TD/eiBUf5f5LwXbCJa35T7dPtTpCHRJ9oJWmyPLVT3A=
|
||||
github.com/pion/webrtc/v4 v4.1.8 h1:ynkjfiURDQ1+8EcJsoa60yumHAmyeYjz08AaOuor+sk=
|
||||
github.com/pion/webrtc/v4 v4.1.8/go.mod h1:KVaARG2RN0lZx0jc7AWTe38JpPv+1/KicOZ9jN52J/s=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
|
||||
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
|
||||
github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8=
|
||||
github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ=
|
||||
github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ=
|
||||
github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY=
|
||||
github.com/veandco/go-sdl2 v0.4.35 h1:NohzsfageDWGtCd9nf7Pc3sokMK/MOK+UA2QMJARWzQ=
|
||||
github.com/veandco/go-sdl2 v0.4.35/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/image v0.13.0 h1:3cge/F/QTkNLauhf2QoE9zp+7sr+ZcL4HnoZmdwg9sg=
|
||||
golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
github.com/veandco/go-sdl2 v0.4.40 h1:fZv6wC3zz1Xt167P09gazawnpa0KY5LM7JAvKpX9d/U=
|
||||
github.com/veandco/go-sdl2 v0.4.40/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY=
|
||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
|
||||
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
|
||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
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=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
|||
|
|
@ -19,21 +19,22 @@ package api
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
Id interface {
|
||||
String() string
|
||||
}
|
||||
Stateful[T Id] struct {
|
||||
Id T `json:"id"`
|
||||
Stateful struct {
|
||||
Id string `json:"id"`
|
||||
}
|
||||
Room struct {
|
||||
Rid string `json:"room_id"` // room id
|
||||
Rid string `json:"room_id"`
|
||||
}
|
||||
StatefulRoom[T Id] struct {
|
||||
Stateful[T]
|
||||
Room
|
||||
StatefulRoom struct {
|
||||
Id string `json:"id"`
|
||||
Rid string `json:"room_id"`
|
||||
}
|
||||
PT uint8
|
||||
)
|
||||
|
|
@ -62,8 +63,9 @@ func (o *Out) GetPayload() any { return o.Payload }
|
|||
|
||||
// Packet codes:
|
||||
//
|
||||
// x, 1xx - user codes
|
||||
// 2xx - worker codes
|
||||
// x, 1xx - user codes
|
||||
// 15x - webrtc data exchange codes
|
||||
// 2xx - worker codes
|
||||
const (
|
||||
CheckLatency PT = 3
|
||||
InitSession PT = 4
|
||||
|
|
@ -76,14 +78,17 @@ const (
|
|||
SaveGame PT = 106
|
||||
LoadGame PT = 107
|
||||
ChangePlayer PT = 108
|
||||
ToggleMultitap PT = 109
|
||||
RecordGame PT = 110
|
||||
GetWorkerList PT = 111
|
||||
ErrNoFreeSlots PT = 112
|
||||
ResetGame PT = 113
|
||||
RegisterRoom PT = 201
|
||||
CloseRoom PT = 202
|
||||
IceCandidate = WebrtcIce
|
||||
TerminateSession PT = 204
|
||||
AppVideoChange PT = 150
|
||||
LibNewGameList PT = 205
|
||||
PrevSessions PT = 206
|
||||
)
|
||||
|
||||
func (p PT) String() string {
|
||||
|
|
@ -110,20 +115,26 @@ func (p PT) String() string {
|
|||
return "SaveGame"
|
||||
case LoadGame:
|
||||
return "LoadGame"
|
||||
case ToggleMultitap:
|
||||
return "ToggleMultitap"
|
||||
case RecordGame:
|
||||
return "RecordGame"
|
||||
case GetWorkerList:
|
||||
return "GetWorkerList"
|
||||
case ErrNoFreeSlots:
|
||||
return "NoFreeSlots"
|
||||
case ResetGame:
|
||||
return "ResetGame"
|
||||
case RegisterRoom:
|
||||
return "RegisterRoom"
|
||||
case CloseRoom:
|
||||
return "CloseRoom"
|
||||
case TerminateSession:
|
||||
return "TerminateSession"
|
||||
case AppVideoChange:
|
||||
return "AppVideoChange"
|
||||
case LibNewGameList:
|
||||
return "LibNewGameList"
|
||||
case PrevSessions:
|
||||
return "PrevSessions"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
|
|
@ -146,6 +157,21 @@ var (
|
|||
OkPacket = Out{Payload: "ok"}
|
||||
)
|
||||
|
||||
func Do[I Id, T any](in In[I], fn func(T)) error {
|
||||
if dat := Unwrap[T](in.Payload); dat != nil {
|
||||
fn(*dat)
|
||||
return nil
|
||||
}
|
||||
return ErrMalformed
|
||||
}
|
||||
|
||||
func DoE[I Id, T any](in In[I], fn func(T) error) error {
|
||||
if dat := Unwrap[T](in.Payload); dat != nil {
|
||||
return fn(*dat)
|
||||
}
|
||||
return ErrMalformed
|
||||
}
|
||||
|
||||
func Unwrap[T any](data []byte) *T {
|
||||
out := new(T)
|
||||
if err := json.Unmarshal(data, out); err != nil {
|
||||
|
|
@ -160,3 +186,17 @@ func UnwrapChecked[T any](bytes []byte, err error) (*T, error) {
|
|||
}
|
||||
return Unwrap[T](bytes), nil
|
||||
}
|
||||
|
||||
func Wrap(t any) ([]byte, error) { return json.Marshal(t) }
|
||||
|
||||
const separator = "___"
|
||||
|
||||
func ExplodeDeepLink(link string) (string, string) {
|
||||
p := strings.SplitN(link, separator, 2)
|
||||
|
||||
if len(p) == 1 {
|
||||
return p[0], ""
|
||||
}
|
||||
|
||||
return p[0], p[1]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ type Server struct {
|
|||
PingURL string `json:"ping_url"`
|
||||
Port string `json:"port,omitempty"`
|
||||
Replicas uint32 `json:"replicas,omitempty"`
|
||||
Room string `json:"room,omitempty"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Zone string `json:"zone,omitempty"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,11 @@ type (
|
|||
RecordUser string `json:"record_user,omitempty"`
|
||||
PlayerIndex int `json:"player_index"`
|
||||
}
|
||||
GameStartUserResponse struct {
|
||||
RoomId string `json:"roomId"`
|
||||
Av *AppVideoInfo `json:"av"`
|
||||
KbMouse bool `json:"kb_mouse"`
|
||||
}
|
||||
IceServer struct {
|
||||
Urls string `json:"urls,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
|
|
@ -22,6 +27,7 @@ type (
|
|||
Wid string `json:"wid"`
|
||||
}
|
||||
AppMeta struct {
|
||||
Alias string `json:"alias,omitempty"`
|
||||
Title string `json:"title"`
|
||||
System string `json:"system"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,27 @@
|
|||
package api
|
||||
|
||||
type (
|
||||
ChangePlayerRequest[T Id] struct {
|
||||
StatefulRoom[T]
|
||||
ChangePlayerRequest struct {
|
||||
StatefulRoom
|
||||
Index int `json:"index"`
|
||||
}
|
||||
ChangePlayerResponse int
|
||||
GameQuitRequest[T Id] struct {
|
||||
StatefulRoom[T]
|
||||
}
|
||||
LoadGameRequest[T Id] struct {
|
||||
StatefulRoom[T]
|
||||
}
|
||||
LoadGameResponse string
|
||||
SaveGameRequest[T Id] struct {
|
||||
StatefulRoom[T]
|
||||
}
|
||||
SaveGameResponse string
|
||||
StartGameRequest[T Id] struct {
|
||||
StatefulRoom[T]
|
||||
ChangePlayerResponse int
|
||||
GameQuitRequest StatefulRoom
|
||||
LoadGameRequest StatefulRoom
|
||||
LoadGameResponse string
|
||||
ResetGameRequest StatefulRoom
|
||||
ResetGameResponse string
|
||||
SaveGameRequest StatefulRoom
|
||||
SaveGameResponse string
|
||||
StartGameRequest struct {
|
||||
StatefulRoom
|
||||
Record bool
|
||||
RecordUser string
|
||||
Game GameInfo `json:"game"`
|
||||
PlayerIndex int `json:"player_index"`
|
||||
Game string `json:"game"`
|
||||
PlayerIndex int `json:"player_index"`
|
||||
}
|
||||
GameInfo struct {
|
||||
Alias string `json:"alias"`
|
||||
Base string `json:"base"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
|
|
@ -33,30 +30,41 @@ type (
|
|||
}
|
||||
StartGameResponse struct {
|
||||
Room
|
||||
Record bool
|
||||
AV *AppVideoInfo `json:"av"`
|
||||
Record bool `json:"record"`
|
||||
KbMouse bool `json:"kb_mouse"`
|
||||
}
|
||||
RecordGameRequest[T Id] struct {
|
||||
StatefulRoom[T]
|
||||
RecordGameRequest struct {
|
||||
StatefulRoom
|
||||
Active bool `json:"active"`
|
||||
User string `json:"user"`
|
||||
}
|
||||
RecordGameResponse string
|
||||
TerminateSessionRequest[T Id] struct {
|
||||
Stateful[T]
|
||||
}
|
||||
ToggleMultitapRequest[T Id] struct {
|
||||
StatefulRoom[T]
|
||||
}
|
||||
WebrtcAnswerRequest[T Id] struct {
|
||||
Stateful[T]
|
||||
RecordGameResponse string
|
||||
TerminateSessionRequest Stateful
|
||||
WebrtcAnswerRequest struct {
|
||||
Stateful
|
||||
Sdp string `json:"sdp"`
|
||||
}
|
||||
WebrtcIceCandidateRequest[T Id] struct {
|
||||
Stateful[T]
|
||||
WebrtcIceCandidateRequest struct {
|
||||
Stateful
|
||||
Candidate string `json:"candidate"` // Base64-encoded ICE candidate
|
||||
}
|
||||
WebrtcInitRequest[T Id] struct {
|
||||
Stateful[T]
|
||||
}
|
||||
WebrtcInitRequest Stateful
|
||||
WebrtcInitResponse string
|
||||
|
||||
AppVideoInfo struct {
|
||||
W int `json:"w"`
|
||||
H int `json:"h"`
|
||||
S int `json:"s"`
|
||||
A float32 `json:"a"`
|
||||
}
|
||||
|
||||
LibGameListInfo struct {
|
||||
T int
|
||||
List []GameInfo
|
||||
}
|
||||
|
||||
PrevSessionInfo struct {
|
||||
List []string
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,14 +2,19 @@ package com
|
|||
|
||||
import "github.com/giongto35/cloud-game/v3/pkg/logger"
|
||||
|
||||
type NetClient[K comparable] interface {
|
||||
type stringer interface {
|
||||
comparable
|
||||
String() string
|
||||
}
|
||||
|
||||
type NetClient[K stringer] interface {
|
||||
Disconnect()
|
||||
Id() K
|
||||
}
|
||||
|
||||
type NetMap[K comparable, T NetClient[K]] struct{ Map[K, T] }
|
||||
type NetMap[K stringer, T NetClient[K]] struct{ Map[K, T] }
|
||||
|
||||
func NewNetMap[K comparable, T NetClient[K]]() NetMap[K, T] {
|
||||
func NewNetMap[K stringer, T NetClient[K]]() NetMap[K, T] {
|
||||
return NetMap[K, T]{Map: Map[K, T]{m: make(map[K]T, 10)}}
|
||||
}
|
||||
|
||||
|
|
@ -19,6 +24,12 @@ func (m *NetMap[K, T]) Remove(client T) { m.Map.Remove(client.Id()) }
|
|||
func (m *NetMap[K, T]) RemoveL(client T) int { return m.Map.RemoveL(client.Id()) }
|
||||
func (m *NetMap[K, T]) Reset() { m.Map = Map[K, T]{m: make(map[K]T, 10)} }
|
||||
func (m *NetMap[K, T]) RemoveDisconnect(client T) { client.Disconnect(); m.Remove(client) }
|
||||
func (m *NetMap[K, T]) Find(id string) T {
|
||||
v, _ := m.Map.FindBy(func(v T) bool {
|
||||
return v.Id().String() == id
|
||||
})
|
||||
return v
|
||||
}
|
||||
|
||||
type SocketClient[T ~uint8, P Packet[T], X any, P2 Packet2[X]] struct {
|
||||
id Uid
|
||||
|
|
@ -55,6 +66,10 @@ func (c *SocketClient[T, P, _, _]) ProcessPackets(fn func(in P) error) chan stru
|
|||
return c.sock.conn.Listen()
|
||||
}
|
||||
|
||||
func (c *SocketClient[T, P, X, P2]) SetErrorHandler(h func(error)) { c.sock.conn.SetErrorHandler(h) }
|
||||
|
||||
func (c *SocketClient[T, P, X, P2]) SetMaxMessageSize(s int64) { c.sock.conn.SetMaxMessageSize(s) }
|
||||
|
||||
func (c *SocketClient[_, _, _, _]) handleMessage(message []byte, err error) {
|
||||
if err != nil {
|
||||
c.log.Error().Err(err).Send()
|
||||
|
|
|
|||
147
pkg/com/map.go
147
pkg/com/map.go
|
|
@ -2,6 +2,7 @@ package com
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"iter"
|
||||
"sync"
|
||||
)
|
||||
|
||||
|
|
@ -9,72 +10,118 @@ import (
|
|||
// Keep in mind that the underlying map structure will grow indefinitely.
|
||||
type Map[K comparable, V any] struct {
|
||||
m map[K]V
|
||||
mu sync.Mutex
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (m *Map[K, _]) Has(key K) bool { _, ok := m.Contains(key); return ok }
|
||||
func (m *Map[_, _]) Len() int { m.mu.Lock(); defer m.mu.Unlock(); return len(m.m) }
|
||||
func (m *Map[K, V]) Pop(key K) V {
|
||||
m.mu.Lock()
|
||||
v := m.m[key]
|
||||
delete(m.m, key)
|
||||
m.mu.Unlock()
|
||||
return v
|
||||
func (m *Map[K, _]) Len() int {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return len(m.m)
|
||||
}
|
||||
func (m *Map[K, V]) Put(key K, v V) bool {
|
||||
m.mu.Lock()
|
||||
|
||||
func (m *Map[K, _]) Has(key K) bool {
|
||||
m.mu.RLock()
|
||||
_, ok := m.m[key]
|
||||
m.m[key] = v
|
||||
m.mu.Unlock()
|
||||
m.mu.RUnlock()
|
||||
return ok
|
||||
}
|
||||
func (m *Map[K, _]) Remove(key K) { m.mu.Lock(); delete(m.m, key); m.mu.Unlock() }
|
||||
func (m *Map[K, _]) RemoveL(key K) int {
|
||||
m.mu.Lock()
|
||||
delete(m.m, key)
|
||||
k := len(m.m)
|
||||
m.mu.Unlock()
|
||||
return k
|
||||
}
|
||||
func (m *Map[K, V]) String() string {
|
||||
m.mu.Lock()
|
||||
s := fmt.Sprintf("%v", m.m)
|
||||
m.mu.Unlock()
|
||||
return s
|
||||
}
|
||||
|
||||
// Contains returns the first value found and a boolean flag if its found or not.
|
||||
func (m *Map[K, V]) Contains(key K) (v V, ok bool) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if vv, ok := m.m[key]; ok {
|
||||
return vv, true
|
||||
}
|
||||
return v, false
|
||||
// Get returns the value and exists flag (standard map comma-ok idiom).
|
||||
func (m *Map[K, V]) Get(key K) (V, bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
val, ok := m.m[key]
|
||||
return val, ok
|
||||
}
|
||||
|
||||
func (m *Map[K, V]) Find(key K) V {
|
||||
v, _ := m.Contains(key)
|
||||
v, _ := m.Get(key)
|
||||
return v
|
||||
}
|
||||
|
||||
// FindBy searches the first key-value with the provided predicate function.
|
||||
func (m *Map[K, V]) FindBy(fn func(v V) bool) (v V, ok bool) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
for _, vv := range m.m {
|
||||
if fn(vv) {
|
||||
return vv, true
|
||||
}
|
||||
}
|
||||
return v, false
|
||||
func (m *Map[K, V]) String() string {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return fmt.Sprintf("%v", m.m)
|
||||
}
|
||||
|
||||
// ForEach processes every element with the provided callback function.
|
||||
func (m *Map[K, V]) ForEach(fn func(v V)) {
|
||||
// FindBy searches for the first value satisfying the predicate.
|
||||
// Note: This holds a Read Lock during iteration.
|
||||
func (m *Map[K, V]) FindBy(predicate func(v V) bool) (V, bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
for _, v := range m.m {
|
||||
if predicate(v) {
|
||||
return v, true
|
||||
}
|
||||
}
|
||||
var zero V
|
||||
return zero, false
|
||||
}
|
||||
|
||||
// Put sets the value and returns true if the key already existed.
|
||||
func (m *Map[K, V]) Put(key K, v V) bool {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
for _, v := range m.m {
|
||||
fn(v)
|
||||
|
||||
if m.m == nil {
|
||||
m.m = make(map[K]V)
|
||||
}
|
||||
|
||||
_, exists := m.m[key]
|
||||
m.m[key] = v
|
||||
return exists
|
||||
}
|
||||
|
||||
func (m *Map[K, V]) Remove(key K) {
|
||||
m.mu.Lock()
|
||||
delete(m.m, key)
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
// Pop returns the value and removes it from the map.
|
||||
// Returns zero value if not found.
|
||||
func (m *Map[K, V]) Pop(key K) V {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
val, ok := m.m[key]
|
||||
if ok {
|
||||
delete(m.m, key)
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// RemoveL removes the key and returns the new length of the map.
|
||||
func (m *Map[K, _]) RemoveL(key K) int {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
delete(m.m, key)
|
||||
return len(m.m)
|
||||
}
|
||||
|
||||
// Clear empties the map.
|
||||
func (m *Map[K, V]) Clear() {
|
||||
m.mu.Lock()
|
||||
m.m = make(map[K]V)
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
// Values returns an iterator for values only.
|
||||
//
|
||||
// Usage: for k, v := range m.Values() { ... }
|
||||
//
|
||||
// Warning: This holds a Read Lock (RLock) during iteration.
|
||||
// Do not call Put/Remove on this map inside the loop (Deadlock).
|
||||
func (m *Map[K, V]) Values() iter.Seq[V] {
|
||||
return func(yield func(V) bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
for _, v := range m.m {
|
||||
if !yield(v) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@ func TestMap_Base(t *testing.T) {
|
|||
if !m.Has(k) {
|
||||
t.Errorf("should have the key %v, %v", k, m.m)
|
||||
}
|
||||
v, ok := m.Contains(k)
|
||||
v, ok := m.Get(k)
|
||||
if v != 0 && !ok {
|
||||
t.Errorf("should have the key %v and ok, %v %v", k, ok, m.m)
|
||||
}
|
||||
_, ok = m.Contains(k + 1)
|
||||
_, ok = m.Get(k + 1)
|
||||
if ok {
|
||||
t.Errorf("should not find anything, %v %v", ok, m.m)
|
||||
}
|
||||
|
|
@ -31,7 +31,9 @@ func TestMap_Base(t *testing.T) {
|
|||
t.Errorf("should have the key %v and ok, %v %v", 1, ok, m.m)
|
||||
}
|
||||
sum := 0
|
||||
m.ForEach(func(v int) { sum += v })
|
||||
for v := range m.Values() {
|
||||
sum += v
|
||||
}
|
||||
if sum != 1 {
|
||||
t.Errorf("shoud have exact sum of 1, but have %v", sum)
|
||||
}
|
||||
|
|
@ -53,8 +55,7 @@ func TestMap_Base(t *testing.T) {
|
|||
|
||||
func TestMap_Concurrency(t *testing.T) {
|
||||
m := Map[int, int]{m: make(map[int]int)}
|
||||
for i := 0; i < 100; i++ {
|
||||
i := i
|
||||
for i := range 100 {
|
||||
go m.Put(i, i)
|
||||
go m.Has(i)
|
||||
go m.Pop(i)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ func UidFromString(id string) (Uid, error) {
|
|||
}
|
||||
|
||||
func (u Uid) Short() string { return u.String()[:3] + "." + u.String()[len(u.String())-3:] }
|
||||
func (u Uid) Id() string { return u.String() }
|
||||
|
||||
type HasCallId interface {
|
||||
SetGetId(fmt.Stringer)
|
||||
|
|
@ -72,7 +71,7 @@ type request struct {
|
|||
response []byte
|
||||
}
|
||||
|
||||
const DefaultCallTimeout = 7 * time.Second
|
||||
const DefaultCallTimeout = 10 * time.Second
|
||||
|
||||
var errCanceled = errors.New("canceled")
|
||||
var errTimeout = errors.New("timeout")
|
||||
|
|
@ -97,7 +96,9 @@ func (s *Server) Connect(w http.ResponseWriter, r *http.Request) (*Connection, e
|
|||
return connect(s.Server.Connect(w, r, nil))
|
||||
}
|
||||
|
||||
func (c Connection) IsServer() bool { return c.conn.IsServer() }
|
||||
func (c *Connection) IsServer() bool { return c.conn.IsServer() }
|
||||
|
||||
func (c *Connection) SetMaxReadSize(s int64) { c.conn.SetMaxMessageSize(s) }
|
||||
|
||||
func connect(conn *websocket.Connection, err error) (*Connection, error) {
|
||||
if err != nil {
|
||||
|
|
@ -168,10 +169,10 @@ func (t *RPC[_, _]) callTimeout() time.Duration {
|
|||
|
||||
func (t *RPC[_, _]) Cleanup() {
|
||||
// drain cancels all what's left in the task queue.
|
||||
t.calls.ForEach(func(task *request) {
|
||||
for task := range t.calls.Values() {
|
||||
if task.err == nil {
|
||||
task.err = errCanceled
|
||||
}
|
||||
close(task.done)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ package com
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"math/rand/v2"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
|
|
@ -49,7 +50,13 @@ func TestWebsocket(t *testing.T) {
|
|||
}
|
||||
|
||||
func testWebsocket(t *testing.T) {
|
||||
addr := ":8989"
|
||||
port, err := getFreePort()
|
||||
if err != nil {
|
||||
t.Logf("couldn't get any free port")
|
||||
t.Skip()
|
||||
}
|
||||
addr := fmt.Sprintf(":%v", port)
|
||||
|
||||
server := newServer(addr, t)
|
||||
client := newClient(t, url.URL{Scheme: "ws", Host: "localhost" + addr, Path: "/ws"})
|
||||
clDone := client.ProcessPackets(func(in TestIn) error { return nil })
|
||||
|
|
@ -81,14 +88,12 @@ func testWebsocket(t *testing.T) {
|
|||
|
||||
// test
|
||||
for _, call := range calls {
|
||||
call := call
|
||||
if call.concurrent {
|
||||
rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
for i := 0; i < n; i++ {
|
||||
for range n {
|
||||
packet := call.packet
|
||||
go func() {
|
||||
defer wait.Done()
|
||||
time.Sleep(time.Duration(rand.Intn(200-100)+100) * time.Millisecond)
|
||||
time.Sleep(time.Duration(rand.IntN(200-100)+100) * time.Millisecond)
|
||||
vv, err := client.rpc.Call(client.sock.conn, &packet)
|
||||
err = checkCall(vv, err, call.value)
|
||||
if err != nil {
|
||||
|
|
@ -98,7 +103,7 @@ func testWebsocket(t *testing.T) {
|
|||
}()
|
||||
}
|
||||
} else {
|
||||
for i := 0; i < n; i++ {
|
||||
for range n {
|
||||
packet := call.packet
|
||||
vv, err := client.rpc.Call(client.sock.conn, &packet)
|
||||
err = checkCall(vv, err, call.value)
|
||||
|
|
@ -206,3 +211,15 @@ func newServer(addr string, t *testing.T) *serverHandler {
|
|||
wg.Wait()
|
||||
return &handler
|
||||
}
|
||||
|
||||
func getFreePort() (port int, err error) {
|
||||
var a *net.TCPAddr
|
||||
var l *net.TCPListener
|
||||
if a, err = net.ResolveTCPAddr("tcp", ":0"); err == nil {
|
||||
if l, err = net.ListenTCP("tcp", a); err == nil {
|
||||
defer func() { _ = l.Close() }()
|
||||
return l.Addr().(*net.TCPAddr).Port, nil
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,27 @@
|
|||
# for the compatibility purposes
|
||||
version: 3
|
||||
|
||||
# new decentralized library of games
|
||||
library:
|
||||
# optional alias file for overriding game names from the basePath path
|
||||
aliasFile: alias.txt
|
||||
# root folder for the library (where games are stored)
|
||||
basePath: assets/games
|
||||
# a list of ignored words in the ROM filenames
|
||||
ignored:
|
||||
- neogeo
|
||||
- pgm
|
||||
# DOSBox filesystem state
|
||||
- .pure
|
||||
# an explicit list of supported file extensions
|
||||
# which overrides Libretro emulator ROMs configs
|
||||
supported:
|
||||
# print some additional info
|
||||
verbose: true
|
||||
# enable library directory live reload
|
||||
# (experimental)
|
||||
watchMode: false
|
||||
|
||||
coordinator:
|
||||
# debugging switch
|
||||
# - shows debug logs
|
||||
|
|
@ -27,22 +48,6 @@ coordinator:
|
|||
# - empty value (default, any free)
|
||||
# - ping (with the lowest ping)
|
||||
selector:
|
||||
# games library
|
||||
library:
|
||||
# root folder for the library (where games are stored)
|
||||
basePath: assets/games
|
||||
# an explicit list of supported file extensions
|
||||
# which overrides Libretro emulator ROMs configs
|
||||
supported:
|
||||
# a list of ignored words in the ROM filenames
|
||||
ignored:
|
||||
- neogeo
|
||||
- pgm
|
||||
# print some additional info
|
||||
verbose: true
|
||||
# enable library directory live reload
|
||||
# (experimental)
|
||||
watchMode: false
|
||||
monitoring:
|
||||
port: 6601
|
||||
# enable Go profiler HTTP server
|
||||
|
|
@ -56,9 +61,13 @@ coordinator:
|
|||
origin:
|
||||
userWs:
|
||||
workerWs:
|
||||
# max websocket message size in bytes
|
||||
maxWsSize: 32000000
|
||||
# HTTP(S) server config
|
||||
server:
|
||||
address: :8000
|
||||
cacheControl: "max-age=259200, must-revalidate"
|
||||
frameOptions: ""
|
||||
https: false
|
||||
# Letsencrypt or self cert config
|
||||
tls:
|
||||
|
|
@ -118,14 +127,6 @@ emulator:
|
|||
# (removed)
|
||||
threads: 0
|
||||
|
||||
aspectRatio:
|
||||
# enable aspect ratio changing
|
||||
# (experimental)
|
||||
keep: false
|
||||
# recalculate emulator game frame size to the given WxH
|
||||
width: 320
|
||||
height: 240
|
||||
|
||||
# enable autosave for emulator states if set to a non-zero value of seconds
|
||||
autosaveSec: 0
|
||||
|
||||
|
|
@ -136,9 +137,23 @@ emulator:
|
|||
# path for storing emulator generated files
|
||||
localPath: "./libretro"
|
||||
|
||||
# checks if the system supports running an emulator at startup
|
||||
failFast: true
|
||||
|
||||
# do not send late video frames
|
||||
skipLateFrames: false
|
||||
|
||||
# log dropped frames (temp)
|
||||
logDroppedFrames: false
|
||||
|
||||
libretro:
|
||||
# use zip compression for emulator save states
|
||||
saveCompression: true
|
||||
# Sets a limiter function for some spammy core callbacks.
|
||||
# 0 - disabled, otherwise -- time in milliseconds for ignoring repeated calls except the last.
|
||||
debounceMs: 0
|
||||
# Allow duplicate frames
|
||||
dup: true
|
||||
# Libretro cores logging level: DEBUG = 0, INFO, WARN, ERROR, DUMMY = INT_MAX
|
||||
logLevel: 1
|
||||
cores:
|
||||
|
|
@ -154,6 +169,32 @@ emulator:
|
|||
sync: true
|
||||
# external cross-process mutex lock
|
||||
extLock: "{user}/.cr/cloud-game.lock"
|
||||
map:
|
||||
darwin:
|
||||
amd64:
|
||||
arch: x86_64
|
||||
ext: .dylib
|
||||
os: osx
|
||||
vendor: apple
|
||||
arm64:
|
||||
arch: arm64
|
||||
ext: .dylib
|
||||
os: osx
|
||||
vendor: apple
|
||||
linux:
|
||||
amd64:
|
||||
arch: x86_64
|
||||
ext: .so
|
||||
os: linux
|
||||
arm:
|
||||
arch: armv7-neon-hf
|
||||
ext: .so
|
||||
os: linux
|
||||
windows:
|
||||
amd64:
|
||||
arch: x86_64
|
||||
ext: .dll
|
||||
os: windows
|
||||
main:
|
||||
type: buildbot
|
||||
url: https://buildbot.libretro.com/nightly
|
||||
|
|
@ -185,7 +226,15 @@ emulator:
|
|||
# - ratio (float)
|
||||
# - isGlAllowed (bool)
|
||||
# - usesLibCo (bool)
|
||||
# - hasMultitap (bool)
|
||||
# - hasMultitap (bool) -- (removed)
|
||||
# - coreAspectRatio (bool) -- (deprecated) correct the aspect ratio on the client with the info from the core.
|
||||
# - hid (map[int][]int)
|
||||
# A list of device IDs to bind to the input ports.
|
||||
# Can be seen in human readable form in the console when worker.debug is enabled.
|
||||
# Some cores allow binding multiple devices to a single port (DosBox), but typically,
|
||||
# you should bind just one device to one port.
|
||||
# - kbMouseSupport (bool) -- (temp) a flag if the core needs the keyboard and mouse on the client
|
||||
# - nonBlockingSave (bool) -- write save file in a non-blocking way, needed for huge save files
|
||||
# - vfr (bool)
|
||||
# (experimental)
|
||||
# Enable variable frame rate only for cores that can't produce a constant frame rate.
|
||||
|
|
@ -195,14 +244,24 @@ emulator:
|
|||
# noticeable video stutter (with the current frame rendering time calculations).
|
||||
# - options ([]string) a list of Libretro core options for tweaking.
|
||||
# All keys of the options should be in the double quotes in order to preserve upper-case symbols.
|
||||
# - options4rom (rom[[]string])
|
||||
# A list of core options to override for a specific core depending on the current ROM name.
|
||||
# - hacks ([]string) a list of hacks.
|
||||
# Available:
|
||||
# - skip_hw_context_destroy -- don't destroy OpenGL context during Libretro core deinit.
|
||||
# May help with crashes, for example, with PPSSPP.
|
||||
# - skip_same_thread_save -- skip thread lock save (used with PPSSPP).
|
||||
# - uniqueSaveDir (bool) -- needed only for cores (like DosBox) that persist their state into one shared file.
|
||||
# This will allow for concurrent reading and saving of current states.
|
||||
# - saveStateFs (string) -- the name of the file that will be initially copied into the save folder.
|
||||
# All * symbols will be replaced to the name of the ROM.
|
||||
list:
|
||||
gba:
|
||||
lib: mgba_libretro
|
||||
roms: [ "gba", "gbc" ]
|
||||
options:
|
||||
mgba_audio_low_pass_filter: disabled
|
||||
mgba_audio_low_pass_range: 50
|
||||
pcsx:
|
||||
lib: pcsx_rearmed_libretro
|
||||
roms: [ "cue", "chd" ]
|
||||
|
|
@ -210,20 +269,28 @@ emulator:
|
|||
folder: psx
|
||||
# see: https://github.com/libretro/pcsx_rearmed/blob/master/frontend/libretro_core_options.h
|
||||
options:
|
||||
"pcsx_rearmed_show_bios_bootlogo": enabled
|
||||
"pcsx_rearmed_drc": enabled
|
||||
"pcsx_rearmed_display_internal_fps": disabled
|
||||
# MAME core requires additional manual setup, please read:
|
||||
# https://docs.libretro.com/library/fbneo/
|
||||
mame:
|
||||
lib: fbneo_libretro
|
||||
folder: mame
|
||||
roms: [ "zip" ]
|
||||
nes:
|
||||
lib: nestopia_libretro
|
||||
roms: [ "nes" ]
|
||||
options:
|
||||
nestopia_aspect: "uncorrected"
|
||||
snes:
|
||||
lib: snes9x_libretro
|
||||
roms: [ "smc", "sfc", "swc", "fig", "bs" ]
|
||||
hasMultitap: true
|
||||
hid:
|
||||
# set the 2nd port to RETRO_DEVICE_JOYPAD_MULTITAP ((1<<8) | 1) as SNES9x requires it
|
||||
# in order to support up to 5-player games
|
||||
# see: https://nintendo.fandom.com/wiki/Super_Multitap
|
||||
1: 257
|
||||
n64:
|
||||
lib: mupen64plus_next_libretro
|
||||
roms: [ "n64", "v64", "z64" ]
|
||||
|
|
@ -239,7 +306,7 @@ emulator:
|
|||
"mupen64plus-EnableEnhancedTextureStorage": True
|
||||
"mupen64plus-EnableFBEmulation": True
|
||||
"mupen64plus-EnableLegacyBlending": True
|
||||
"mupen64plus-FrameDuping": False
|
||||
"mupen64plus-FrameDuping": True
|
||||
"mupen64plus-MaxTxCacheSize": 8000
|
||||
"mupen64plus-ThreadedRenderer": False
|
||||
"mupen64plus-cpucore": dynamic_recompiler
|
||||
|
|
@ -247,20 +314,52 @@ emulator:
|
|||
"mupen64plus-rdp-plugin": gliden64
|
||||
"mupen64plus-rsp-plugin": hle
|
||||
"mupen64plus-astick-sensitivity": 100
|
||||
dos:
|
||||
lib: dosbox_pure_libretro
|
||||
roms: [ "zip", "cue" ]
|
||||
folder: dos
|
||||
kbMouseSupport: true
|
||||
nonBlockingSave: true
|
||||
saveStateFs: "*.pure.zip"
|
||||
hid:
|
||||
0: [ 257, 513 ]
|
||||
1: [ 257, 513 ]
|
||||
2: [ 257, 513 ]
|
||||
3: [ 257, 513 ]
|
||||
options:
|
||||
"dosbox_pure_conf": "outside"
|
||||
"dosbox_pure_force60fps": "true"
|
||||
|
||||
encoder:
|
||||
audio:
|
||||
# audio frame duration needed for WebRTC (Opus)
|
||||
# most of the emulators have ~1400 samples per a video frame,
|
||||
# so we keep the frame buffer roughly half of that size or 2 RTC packets per frame
|
||||
# (deprecated) due to frames
|
||||
frame: 10
|
||||
# dynamic frames for Opus encoder
|
||||
frames:
|
||||
- 10
|
||||
- 5
|
||||
# speex (2), linear (1) or nearest neighbour (0) audio resampler
|
||||
# linear should sound slightly better than 0
|
||||
resampler: 2
|
||||
video:
|
||||
# h264, vpx (VP8)
|
||||
# h264, vpx (vp8) or vp9
|
||||
codec: h264
|
||||
# Threaded encoder if supported, 0 - auto, 1 - nope, >1 - multi-threaded
|
||||
threads: 0
|
||||
# see: https://trac.ffmpeg.org/wiki/Encode/H.264
|
||||
h264:
|
||||
# crf, cbr
|
||||
mode: crf
|
||||
# Constant Rate Factor (CRF) 0-51 (default: 23)
|
||||
crf: 26
|
||||
crf: 23
|
||||
# Rate control options
|
||||
# set the maximum bitrate
|
||||
maxRate: 0
|
||||
# set the expected client buffer size
|
||||
bufSize: 0
|
||||
# ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow, placebo
|
||||
preset: superfast
|
||||
# baseline, main, high, high10, high422, high444
|
||||
|
|
@ -297,19 +396,24 @@ recording:
|
|||
# save directory
|
||||
folder: ./recording
|
||||
|
||||
# cloud storage options
|
||||
# it is mandatory to use a cloud storage when running
|
||||
# a distributed multi-server configuration in order to
|
||||
# share save states between nodes (resume games on a different worker)
|
||||
storage:
|
||||
# cloud storage provider:
|
||||
# - empty (No op storage stub)
|
||||
# - oracle [Oracle Object Storage](https://www.oracle.com/cloud/storage/object-storage.html)
|
||||
# - s3 (S3 API compatible object storage)
|
||||
provider:
|
||||
# this value contains arbitrary key attribute:
|
||||
# - oracle: pre-authenticated URL (see: https://docs.oracle.com/en-us/iaas/Content/Object/Tasks/usingpreauthenticatedrequests.htm)
|
||||
key:
|
||||
s3Endpoint:
|
||||
s3BucketName:
|
||||
s3AccessKeyId:
|
||||
s3SecretAccessKey:
|
||||
|
||||
webrtc:
|
||||
# turn off default Pion interceptors (see: https://github.com/pion/interceptor)
|
||||
# (performance)
|
||||
disableDefaultInterceptors: true
|
||||
disableDefaultInterceptors: false
|
||||
# indicates the role of the DTLS transport (see: https://github.com/pion/webrtc/blob/master/dtlsrole.go)
|
||||
# (debug)
|
||||
# - (default)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import "flag"
|
|||
type CoordinatorConfig struct {
|
||||
Coordinator Coordinator
|
||||
Emulator Emulator
|
||||
Library Library
|
||||
Recording Recording
|
||||
Version Version
|
||||
Webrtc Webrtc
|
||||
|
|
@ -14,6 +15,7 @@ type Coordinator struct {
|
|||
Analytics Analytics
|
||||
Debug bool
|
||||
Library Library
|
||||
MaxWsSize int64
|
||||
Monitoring Monitoring
|
||||
Origin struct {
|
||||
UserWs string
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Emulator struct {
|
||||
Threads int
|
||||
AspectRatio struct {
|
||||
Keep bool
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
Storage string
|
||||
LocalPath string
|
||||
Libretro LibretroConfig
|
||||
AutosaveSec int
|
||||
FailFast bool
|
||||
Threads int
|
||||
Storage string
|
||||
LocalPath string
|
||||
Libretro LibretroConfig
|
||||
AutosaveSec int
|
||||
SkipLateFrames bool
|
||||
LogDroppedFrames bool
|
||||
}
|
||||
|
||||
type LibretroConfig struct {
|
||||
|
|
@ -24,39 +24,72 @@ type LibretroConfig struct {
|
|||
Paths struct {
|
||||
Libs string
|
||||
}
|
||||
Repo struct {
|
||||
Sync bool
|
||||
ExtLock string
|
||||
Main LibretroRepoConfig
|
||||
Secondary LibretroRepoConfig
|
||||
}
|
||||
Repo LibretroRemoteRepo
|
||||
List map[string]LibretroCoreConfig
|
||||
}
|
||||
DebounceMs int
|
||||
Dup bool
|
||||
SaveCompression bool
|
||||
LogLevel int
|
||||
}
|
||||
|
||||
type LibretroRemoteRepo struct {
|
||||
Sync bool
|
||||
ExtLock string
|
||||
Map map[string]map[string]LibretroRepoMapInfo
|
||||
Main LibretroRepoConfig
|
||||
Secondary LibretroRepoConfig
|
||||
}
|
||||
|
||||
// LibretroRepoMapInfo contains Libretro core lib platform info.
|
||||
// And the cores are just C-compiled libraries.
|
||||
// See: https://buildbot.libretro.com/nightly.
|
||||
type LibretroRepoMapInfo struct {
|
||||
Arch string // bottom: x86_64, x86, ...
|
||||
Ext string // platform dependent library file extension (dot-prefixed)
|
||||
Os string // middle: windows, ios, ...
|
||||
Vendor string // top level: apple, nintendo, ...
|
||||
}
|
||||
|
||||
type LibretroRepoConfig struct {
|
||||
Type string
|
||||
Url string
|
||||
Compression string
|
||||
}
|
||||
|
||||
// Guess tries to map OS + CPU architecture to the corresponding remote URL path.
|
||||
// See: https://gist.github.com/asukakenji/f15ba7e588ac42795f421b48b8aede63.
|
||||
func (lrp LibretroRemoteRepo) Guess() (LibretroRepoMapInfo, error) {
|
||||
if os, ok := lrp.Map[runtime.GOOS]; ok {
|
||||
if arch, ok2 := os[runtime.GOARCH]; ok2 {
|
||||
return arch, nil
|
||||
}
|
||||
}
|
||||
return LibretroRepoMapInfo{},
|
||||
errors.New("core mapping not found for " + runtime.GOOS + ":" + runtime.GOARCH)
|
||||
}
|
||||
|
||||
type LibretroCoreConfig struct {
|
||||
AltRepo bool
|
||||
AutoGlContext bool // hack: keep it here to pass it down the emulator
|
||||
Folder string
|
||||
Hacks []string
|
||||
HasMultitap bool
|
||||
Height int
|
||||
IsGlAllowed bool
|
||||
Lib string
|
||||
Options map[string]string
|
||||
Roms []string
|
||||
Scale float64
|
||||
UsesLibCo bool
|
||||
VFR bool
|
||||
Width int
|
||||
AltRepo bool
|
||||
AutoGlContext bool // hack: keep it here to pass it down the emulator
|
||||
CoreAspectRatio bool
|
||||
Folder string
|
||||
Hacks []string
|
||||
Height int
|
||||
Hid map[int][]int
|
||||
IsGlAllowed bool
|
||||
KbMouseSupport bool
|
||||
Lib string
|
||||
NonBlockingSave bool
|
||||
Options map[string]string
|
||||
Options4rom map[string]map[string]string // <(^_^)>
|
||||
Roms []string
|
||||
SaveStateFs string
|
||||
Scale float64
|
||||
UniqueSaveDir bool
|
||||
UsesLibCo bool
|
||||
VFR bool
|
||||
Width int
|
||||
}
|
||||
|
||||
type CoreInfo struct {
|
||||
|
|
@ -101,6 +134,10 @@ func (e Emulator) GetSupportedExtensions() []string {
|
|||
return extensions
|
||||
}
|
||||
|
||||
func (e Emulator) SessionStoragePath() string {
|
||||
return e.Storage
|
||||
}
|
||||
|
||||
func (l *LibretroConfig) GetCores() (cores []CoreInfo) {
|
||||
for k, core := range l.Cores.List {
|
||||
cores = append(cores, CoreInfo{Id: k, Name: core.Lib, AltRepo: core.AltRepo})
|
||||
|
|
|
|||
|
|
@ -88,6 +88,9 @@ func (e *Env) Read() (Kv, error) {
|
|||
mp := make(Kv)
|
||||
for _, k := range keys {
|
||||
parts := strings.SplitN(k, "=", 2)
|
||||
if parts == nil {
|
||||
continue
|
||||
}
|
||||
n := strings.ToLower(strings.TrimPrefix(parts[0], string(*e)))
|
||||
if n == "" {
|
||||
continue
|
||||
|
|
@ -102,7 +105,9 @@ func (e *Env) Read() (Kv, error) {
|
|||
} else {
|
||||
key = strings.Replace(n[:x+1], "_", ".", -1) + n[x+2:]
|
||||
}
|
||||
mp[key] = parts[1]
|
||||
if len(parts) > 1 {
|
||||
mp[key] = parts[1]
|
||||
}
|
||||
}
|
||||
return maps.Unflatten(mp, "."), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,10 @@ import (
|
|||
func TestConfigEnv(t *testing.T) {
|
||||
var out WorkerConfig
|
||||
|
||||
_ = os.Setenv("CLOUD_GAME_ENCODER_AUDIO_FRAME", "33")
|
||||
defer func() { _ = os.Unsetenv("CLOUD_GAME_ENCODER_AUDIO_FRAME") }()
|
||||
_ = os.Setenv("CLOUD_GAME_ENCODER_AUDIO_FRAMES[0]", "10")
|
||||
_ = os.Setenv("CLOUD_GAME_ENCODER_AUDIO_FRAMES[1]", "5")
|
||||
defer func() { _ = os.Unsetenv("CLOUD_GAME_ENCODER_AUDIO_FRAMES[0]") }()
|
||||
defer func() { _ = os.Unsetenv("CLOUD_GAME_ENCODER_AUDIO_FRAMES[1]") }()
|
||||
|
||||
_ = os.Setenv("CLOUD_GAME_EMULATOR_LIBRETRO_CORES_LIST_PCSX_OPTIONS__PCSX_REARMED_DRC", "x")
|
||||
defer func() {
|
||||
|
|
@ -22,8 +24,11 @@ func TestConfigEnv(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if out.Encoder.Audio.Frame != 33 {
|
||||
t.Errorf("%v is not 33", out.Encoder.Audio.Frame)
|
||||
for i, x := range []float32{10, 5} {
|
||||
if out.Encoder.Audio.Frames[i] != x {
|
||||
t.Errorf("%v is not [10, 5]", out.Encoder.Audio.Frames)
|
||||
t.Failed()
|
||||
}
|
||||
}
|
||||
|
||||
v := out.Emulator.Libretro.Cores.List["pcsx"].Options["pcsx_rearmed_drc"]
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import "flag"
|
|||
type Version int
|
||||
|
||||
type Library struct {
|
||||
// filename of the alias' file
|
||||
AliasFile string
|
||||
// some directory which is going to be
|
||||
// the root folder for the library
|
||||
BasePath string
|
||||
|
|
@ -30,9 +32,11 @@ type Monitoring struct {
|
|||
func (c *Monitoring) IsEnabled() bool { return c.MetricEnabled || c.ProfilingEnabled }
|
||||
|
||||
type Server struct {
|
||||
Address string
|
||||
Https bool
|
||||
Tls struct {
|
||||
Address string
|
||||
CacheControl string
|
||||
FrameOptions string
|
||||
Https bool
|
||||
Tls struct {
|
||||
Address string
|
||||
Domain string
|
||||
HttpsKey string
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import (
|
|||
type WorkerConfig struct {
|
||||
Encoder Encoder
|
||||
Emulator Emulator
|
||||
Library Library
|
||||
Recording Recording
|
||||
Storage Storage
|
||||
Worker Worker
|
||||
|
|
@ -22,13 +23,15 @@ type WorkerConfig struct {
|
|||
}
|
||||
|
||||
type Storage struct {
|
||||
Provider string
|
||||
Key string
|
||||
Provider string
|
||||
S3Endpoint string
|
||||
S3BucketName string
|
||||
S3AccessKeyId string
|
||||
S3SecretAccessKey string
|
||||
}
|
||||
|
||||
type Worker struct {
|
||||
Debug bool
|
||||
Library Library
|
||||
Monitoring Monitoring
|
||||
Network struct {
|
||||
CoordinatorAddress string
|
||||
|
|
@ -48,13 +51,18 @@ type Encoder struct {
|
|||
}
|
||||
|
||||
type Audio struct {
|
||||
Frame int
|
||||
Frames []float32
|
||||
Resampler int
|
||||
}
|
||||
|
||||
type Video struct {
|
||||
Codec string
|
||||
H264 struct {
|
||||
Codec string
|
||||
Threads int
|
||||
H264 struct {
|
||||
Mode string
|
||||
Crf uint8
|
||||
MaxRate int
|
||||
BufSize int
|
||||
LogLevel int32
|
||||
Preset string
|
||||
Profile string
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/giongto35/cloud-game/v3/pkg/config"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/games"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/logger"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/monitoring"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/network/httpx"
|
||||
|
|
@ -23,10 +22,7 @@ type Coordinator struct {
|
|||
}
|
||||
|
||||
func New(conf config.CoordinatorConfig, log *logger.Logger) (*Coordinator, error) {
|
||||
coordinator := &Coordinator{}
|
||||
lib := games.NewLib(conf.Coordinator.Library, conf.Emulator, log)
|
||||
lib.Scan()
|
||||
coordinator.hub = NewHub(conf, lib, log)
|
||||
coordinator := &Coordinator{hub: NewHub(conf, log)}
|
||||
h, err := NewHTTPServer(conf, log, func(mux *httpx.Mux) *httpx.Mux {
|
||||
mux.HandleFunc("/ws", coordinator.hub.handleUserConnection())
|
||||
mux.HandleFunc("/wso", coordinator.hub.handleWorkerConnection())
|
||||
|
|
@ -83,7 +79,7 @@ func index(conf config.CoordinatorConfig, log *logger.Logger) httpx.Handler {
|
|||
|
||||
handler := func(tpl *template.Template, w httpx.ResponseWriter, r *httpx.Request) {
|
||||
if err := tpl.Execute(w, tplData); err != nil {
|
||||
log.Fatal().Err(err).Msg("error with the analytics template file")
|
||||
log.Error().Err(err).Msg("error with the analytics template file")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -92,6 +88,12 @@ func index(conf config.CoordinatorConfig, log *logger.Logger) httpx.Handler {
|
|||
if conf.Coordinator.Debug {
|
||||
log.Info().Msgf("Using auto-reloading index.html")
|
||||
return httpx.HandlerFunc(func(w httpx.ResponseWriter, r *httpx.Request) {
|
||||
if conf.Coordinator.Server.CacheControl != "" {
|
||||
w.Header().Add("Cache-Control", conf.Coordinator.Server.CacheControl)
|
||||
}
|
||||
if conf.Coordinator.Server.FrameOptions != "" {
|
||||
w.Header().Add("X-Frame-Options", conf.Coordinator.Server.FrameOptions)
|
||||
}
|
||||
if r.URL.Path == "/" || strings.HasSuffix(r.URL.Path, "/index.html") {
|
||||
tpl := template.Must(template.ParseFiles(indexHTML))
|
||||
handler(tpl, w, r)
|
||||
|
|
@ -102,6 +104,12 @@ func index(conf config.CoordinatorConfig, log *logger.Logger) httpx.Handler {
|
|||
}
|
||||
|
||||
return httpx.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if conf.Coordinator.Server.CacheControl != "" {
|
||||
w.Header().Add("Cache-Control", conf.Coordinator.Server.CacheControl)
|
||||
}
|
||||
if conf.Coordinator.Server.FrameOptions != "" {
|
||||
w.Header().Add("X-Frame-Options", conf.Coordinator.Server.FrameOptions)
|
||||
}
|
||||
if r.URL.Path == "/" || strings.HasSuffix(r.URL.Path, "/index.html") {
|
||||
handler(indexTpl, w, r)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import (
|
|||
"github.com/giongto35/cloud-game/v3/pkg/api"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/com"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/config"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/games"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/logger"
|
||||
)
|
||||
|
||||
|
|
@ -24,20 +23,18 @@ type Connection interface {
|
|||
}
|
||||
|
||||
type Hub struct {
|
||||
conf config.CoordinatorConfig
|
||||
launcher games.Launcher
|
||||
log *logger.Logger
|
||||
users com.NetMap[com.Uid, *User]
|
||||
workers com.NetMap[com.Uid, *Worker]
|
||||
conf config.CoordinatorConfig
|
||||
log *logger.Logger
|
||||
users com.NetMap[com.Uid, *User]
|
||||
workers com.NetMap[com.Uid, *Worker]
|
||||
}
|
||||
|
||||
func NewHub(conf config.CoordinatorConfig, lib games.GameLibrary, log *logger.Logger) *Hub {
|
||||
func NewHub(conf config.CoordinatorConfig, log *logger.Logger) *Hub {
|
||||
return &Hub{
|
||||
conf: conf,
|
||||
users: com.NewNetMap[com.Uid, *User](),
|
||||
workers: com.NewNetMap[com.Uid, *Worker](),
|
||||
launcher: games.NewGameLauncher(lib),
|
||||
log: log,
|
||||
conf: conf,
|
||||
users: com.NewNetMap[com.Uid, *User](),
|
||||
workers: com.NewNetMap[com.Uid, *Worker](),
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -62,21 +59,29 @@ func (h *Hub) handleUserConnection() http.HandlerFunc {
|
|||
|
||||
user := NewUser(conn, log)
|
||||
defer h.users.RemoveDisconnect(user)
|
||||
done := user.HandleRequests(h, h.launcher, h.conf)
|
||||
done := user.HandleRequests(h, h.conf)
|
||||
params := r.URL.Query()
|
||||
|
||||
worker := h.findWorkerFor(user, params, h.log.Extend(h.log.With().Str("cid", user.Id().Short())))
|
||||
if worker == nil {
|
||||
user.Notify(api.ErrNoFreeSlots, "")
|
||||
h.log.Info().Msg("no free workers")
|
||||
return
|
||||
}
|
||||
user.Bind(worker)
|
||||
|
||||
// Link the user to the selected worker. Slot reservation is handled later
|
||||
// on game start; this keeps connections lightweight and lets deep-link
|
||||
// joins share a worker without consuming its single game slot.
|
||||
user.w = worker
|
||||
|
||||
h.users.Add(user)
|
||||
apps := h.launcher.GetAppNames()
|
||||
|
||||
apps := worker.AppNames()
|
||||
list := make([]api.AppMeta, len(apps))
|
||||
for i := range apps {
|
||||
list[i] = api.AppMeta{Title: apps[i].Name, System: apps[i].System}
|
||||
list[i] = api.AppMeta{Alias: apps[i].Alias, Title: apps[i].Name, System: apps[i].System}
|
||||
}
|
||||
|
||||
user.InitSession(worker.Id().String(), h.conf.Webrtc.IceServers, list)
|
||||
log.Info().Str(logger.DirectionField, logger.MarkPlus).Msgf("user %s", user.Id())
|
||||
<-done
|
||||
|
|
@ -104,6 +109,8 @@ func (h *Hub) handleWorkerConnection() http.HandlerFunc {
|
|||
Str(logger.DirectionField, logger.MarkIn),
|
||||
)
|
||||
|
||||
h.log.Debug().Msgf("WS max message size: %vb", h.conf.Coordinator.MaxWsSize)
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
h.log.Debug().Msgf("Handshake %v", r.Host)
|
||||
|
||||
|
|
@ -131,6 +138,7 @@ func (h *Hub) handleWorkerConnection() http.HandlerFunc {
|
|||
log.Error().Err(err).Msg("worker connection fail")
|
||||
return
|
||||
}
|
||||
conn.SetMaxReadSize(h.conf.Coordinator.MaxWsSize)
|
||||
|
||||
worker := NewWorker(conn, *handshake, log)
|
||||
defer h.workers.RemoveDisconnect(worker)
|
||||
|
|
@ -144,8 +152,9 @@ func (h *Hub) handleWorkerConnection() http.HandlerFunc {
|
|||
}
|
||||
|
||||
func (h *Hub) GetServerList() (r []api.Server) {
|
||||
h.workers.ForEach(func(w *Worker) {
|
||||
r = append(r, api.Server{
|
||||
debug := h.conf.Coordinator.Debug
|
||||
for w := range h.workers.Values() {
|
||||
server := api.Server{
|
||||
Addr: w.Addr,
|
||||
Id: w.Id(),
|
||||
IsBusy: !w.HasSlot(),
|
||||
|
|
@ -154,8 +163,12 @@ func (h *Hub) GetServerList() (r []api.Server) {
|
|||
Port: w.Port,
|
||||
Tag: w.Tag,
|
||||
Zone: w.Zone,
|
||||
})
|
||||
})
|
||||
}
|
||||
if debug {
|
||||
server.Room = w.RoomId
|
||||
}
|
||||
r = append(r, server)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -163,15 +176,30 @@ func (h *Hub) GetServerList() (r []api.Server) {
|
|||
// various conditions.
|
||||
func (h *Hub) findWorkerFor(usr *User, q url.Values, log *logger.Logger) *Worker {
|
||||
log.Debug().Msg("Search available workers")
|
||||
roomId := q.Get(api.RoomIdQueryParam)
|
||||
roomIdRaw := q.Get(api.RoomIdQueryParam)
|
||||
sessionId, deepRoomId := api.ExplodeDeepLink(roomIdRaw)
|
||||
roomId := roomIdRaw
|
||||
if deepRoomId != "" {
|
||||
roomId = deepRoomId
|
||||
}
|
||||
zone := q.Get(api.ZoneQueryParam)
|
||||
wid := q.Get(api.WorkerIdParam)
|
||||
|
||||
var worker *Worker
|
||||
if worker = h.findWorkerByRoom(roomId, zone); worker != nil {
|
||||
|
||||
if wid != "" {
|
||||
if worker = h.findWorkerById(wid, h.conf.Coordinator.Debug); worker != nil {
|
||||
log.Debug().Msgf("Worker with id: %v has been found", wid)
|
||||
return worker
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if worker = h.findWorkerByRoom(roomIdRaw, roomId, zone); worker != nil {
|
||||
log.Debug().Str("room", roomId).Msg("An existing worker has been found")
|
||||
} else if worker = h.findWorkerById(wid, h.conf.Coordinator.Debug); worker != nil {
|
||||
log.Debug().Msgf("Worker with id: %v has been found", wid)
|
||||
} else if worker = h.findWorkerByPreviousRoom(sessionId); worker != nil {
|
||||
log.Debug().Msgf("Worker %v with the previous room: %v is found", wid, roomId)
|
||||
} else {
|
||||
switch h.conf.Coordinator.Selector {
|
||||
case config.SelectByPing:
|
||||
|
|
@ -190,23 +218,40 @@ func (h *Hub) findWorkerFor(usr *User, q url.Values, log *logger.Logger) *Worker
|
|||
return worker
|
||||
}
|
||||
|
||||
func (h *Hub) findWorkerByRoom(id string, region string) *Worker {
|
||||
func (h *Hub) findWorkerByPreviousRoom(id string) *Worker {
|
||||
if id == "" {
|
||||
return nil
|
||||
}
|
||||
w, _ := h.workers.FindBy(func(w *Worker) bool {
|
||||
// session and room id are the same
|
||||
return w.HadSession(id) && w.HasSlot()
|
||||
})
|
||||
return w
|
||||
}
|
||||
|
||||
func (h *Hub) findWorkerByRoom(id string, deepId string, region string) *Worker {
|
||||
if id == "" && deepId == "" {
|
||||
return nil
|
||||
}
|
||||
// if there is zone param, we need to ensure the worker in that zone,
|
||||
// if not we consider the room is missing
|
||||
w, _ := h.workers.FindBy(func(w *Worker) bool { return w.RoomId == id && w.In(region) })
|
||||
w, _ := h.workers.FindBy(func(w *Worker) bool {
|
||||
matchId := w.RoomId == id
|
||||
if !matchId && deepId != "" {
|
||||
matchId = w.RoomId == deepId
|
||||
}
|
||||
return matchId && w.In(region)
|
||||
})
|
||||
return w
|
||||
}
|
||||
|
||||
func (h *Hub) getAvailableWorkers(region string) []*Worker {
|
||||
var workers []*Worker
|
||||
h.workers.ForEach(func(w *Worker) {
|
||||
for w := range h.workers.Values() {
|
||||
if w.HasSlot() && w.In(region) {
|
||||
workers = append(workers, w)
|
||||
}
|
||||
})
|
||||
}
|
||||
return workers
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"github.com/giongto35/cloud-game/v3/pkg/api"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/com"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/config"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/games"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/logger"
|
||||
)
|
||||
|
||||
|
|
@ -29,77 +28,54 @@ func NewUser(sock *com.Connection, log *logger.Logger) *User {
|
|||
}
|
||||
}
|
||||
|
||||
func (u *User) Bind(w *Worker) {
|
||||
func (u *User) Bind(w *Worker) bool {
|
||||
u.w = w
|
||||
u.w.Reserve()
|
||||
// Binding only links the worker; slot reservation is handled lazily on
|
||||
// game start to avoid blocking deep-link joins or parallel connections
|
||||
// that haven't started a game yet.
|
||||
return true
|
||||
}
|
||||
|
||||
func (u *User) Disconnect() {
|
||||
u.Connection.Disconnect()
|
||||
if u.w != nil {
|
||||
u.w.UnReserve()
|
||||
u.w.TerminateSession(u.Id())
|
||||
u.w.TerminateSession(u.Id().String())
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) HandleRequests(info HasServerInfo, launcher games.Launcher, conf config.CoordinatorConfig) chan struct{} {
|
||||
return u.ProcessPackets(func(x api.In[com.Uid]) error {
|
||||
payload := x.GetPayload()
|
||||
switch x.GetType() {
|
||||
func (u *User) HandleRequests(info HasServerInfo, conf config.CoordinatorConfig) chan struct{} {
|
||||
return u.ProcessPackets(func(x api.In[com.Uid]) (err error) {
|
||||
switch x.T {
|
||||
case api.WebrtcInit:
|
||||
if u.w != nil {
|
||||
u.HandleWebrtcInit()
|
||||
}
|
||||
case api.WebrtcAnswer:
|
||||
rq := api.Unwrap[api.WebrtcAnswerUserRequest](payload)
|
||||
if rq == nil {
|
||||
return api.ErrMalformed
|
||||
}
|
||||
u.HandleWebrtcAnswer(*rq)
|
||||
err = api.Do(x, u.HandleWebrtcAnswer)
|
||||
case api.WebrtcIce:
|
||||
rq := api.Unwrap[api.WebrtcUserIceCandidate](payload)
|
||||
if rq == nil {
|
||||
return api.ErrMalformed
|
||||
}
|
||||
u.HandleWebrtcIceCandidate(*rq)
|
||||
err = api.Do(x, u.HandleWebrtcIceCandidate)
|
||||
case api.StartGame:
|
||||
rq := api.Unwrap[api.GameStartUserRequest](payload)
|
||||
if rq == nil {
|
||||
return api.ErrMalformed
|
||||
}
|
||||
u.HandleStartGame(*rq, launcher, conf)
|
||||
err = api.Do(x, func(d api.GameStartUserRequest) { u.HandleStartGame(d, conf) })
|
||||
case api.QuitGame:
|
||||
rq := api.Unwrap[api.GameQuitRequest[com.Uid]](payload)
|
||||
if rq == nil {
|
||||
return api.ErrMalformed
|
||||
}
|
||||
u.HandleQuitGame(*rq)
|
||||
err = api.Do(x, u.HandleQuitGame)
|
||||
case api.SaveGame:
|
||||
return u.HandleSaveGame()
|
||||
err = u.HandleSaveGame()
|
||||
case api.LoadGame:
|
||||
return u.HandleLoadGame()
|
||||
err = u.HandleLoadGame()
|
||||
case api.ChangePlayer:
|
||||
rq := api.Unwrap[api.ChangePlayerUserRequest](payload)
|
||||
if rq == nil {
|
||||
return api.ErrMalformed
|
||||
}
|
||||
u.HandleChangePlayer(*rq)
|
||||
case api.ToggleMultitap:
|
||||
u.HandleToggleMultitap()
|
||||
err = api.Do(x, u.HandleChangePlayer)
|
||||
case api.ResetGame:
|
||||
err = api.Do(x, u.HandleResetGame)
|
||||
case api.RecordGame:
|
||||
if !conf.Recording.Enabled {
|
||||
return api.ErrForbidden
|
||||
}
|
||||
rq := api.Unwrap[api.RecordGameRequest[com.Uid]](payload)
|
||||
if rq == nil {
|
||||
return api.ErrMalformed
|
||||
}
|
||||
u.HandleRecordGame(*rq)
|
||||
err = api.Do(x, u.HandleRecordGame)
|
||||
case api.GetWorkerList:
|
||||
u.handleGetWorkerList(conf.Coordinator.Debug, info)
|
||||
default:
|
||||
u.log.Warn().Msgf("Unknown packet: %+v", x)
|
||||
}
|
||||
return nil
|
||||
return
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,15 +10,11 @@ import (
|
|||
// CheckLatency sends a list of server addresses to the user
|
||||
// and waits get back this list with tested ping times for each server.
|
||||
func (u *User) CheckLatency(req api.CheckLatencyUserResponse) (api.CheckLatencyUserRequest, error) {
|
||||
data, err := u.Send(api.CheckLatency, req)
|
||||
if err != nil || data == nil {
|
||||
return nil, err
|
||||
}
|
||||
dat := api.Unwrap[api.CheckLatencyUserRequest](data)
|
||||
dat, err := api.UnwrapChecked[api.CheckLatencyUserRequest](u.Send(api.CheckLatency, req))
|
||||
if dat == nil {
|
||||
return api.CheckLatencyUserRequest{}, err
|
||||
}
|
||||
return *dat, err
|
||||
return *dat, nil
|
||||
}
|
||||
|
||||
// InitSession signals the user that the app is ready to go.
|
||||
|
|
@ -37,4 +33,6 @@ func (u *User) SendWebrtcOffer(sdp string) { u.Notify(api.WebrtcOffer, sdp) }
|
|||
func (u *User) SendWebrtcIceCandidate(candidate string) { u.Notify(api.WebrtcIce, candidate) }
|
||||
|
||||
// StartGame signals the user that everything is ready to start a game.
|
||||
func (u *User) StartGame() { u.Notify(api.StartGame, u.w.RoomId) }
|
||||
func (u *User) StartGame(av *api.AppVideoInfo, kbMouse bool) {
|
||||
u.Notify(api.StartGame, api.GameStartUserResponse{RoomId: u.w.RoomId, Av: av, KbMouse: kbMouse})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,15 +2,15 @@ package coordinator
|
|||
|
||||
import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/giongto35/cloud-game/v3/pkg/api"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/com"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/config"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/games"
|
||||
)
|
||||
|
||||
func (u *User) HandleWebrtcInit() {
|
||||
resp, err := u.w.WebrtcInit(u.Id())
|
||||
uid := u.Id().String()
|
||||
resp, err := u.w.WebrtcInit(uid)
|
||||
if err != nil || resp == nil || *resp == api.EMPTY {
|
||||
u.log.Error().Err(err).Msg("malformed WebRTC init response")
|
||||
return
|
||||
|
|
@ -19,34 +19,64 @@ func (u *User) HandleWebrtcInit() {
|
|||
}
|
||||
|
||||
func (u *User) HandleWebrtcAnswer(rq api.WebrtcAnswerUserRequest) {
|
||||
u.w.WebrtcAnswer(u.Id(), string(rq))
|
||||
u.w.WebrtcAnswer(u.Id().String(), string(rq))
|
||||
}
|
||||
|
||||
func (u *User) HandleWebrtcIceCandidate(rq api.WebrtcUserIceCandidate) {
|
||||
u.w.WebrtcIceCandidate(u.Id(), string(rq))
|
||||
u.w.WebrtcIceCandidate(u.Id().String(), string(rq))
|
||||
}
|
||||
|
||||
func (u *User) HandleStartGame(rq api.GameStartUserRequest, launcher games.Launcher, conf config.CoordinatorConfig) {
|
||||
// +injects game data into the original game request
|
||||
// the name of the game either in the `room id` field or
|
||||
// it's in the initial request
|
||||
game := rq.GameName
|
||||
if rq.RoomId != "" {
|
||||
name := launcher.ExtractAppNameFromUrl(rq.RoomId)
|
||||
if name == "" {
|
||||
u.log.Warn().Msg("couldn't decode game name from the room id")
|
||||
func (u *User) HandleStartGame(rq api.GameStartUserRequest, conf config.CoordinatorConfig) {
|
||||
// Worker slot / room gating:
|
||||
// - If the worker is BUSY (no free slot), we must not create another room.
|
||||
// * If the worker has already reported a room id, only allow requests
|
||||
// for that same room (deep-link joins / reloads).
|
||||
// * If the worker hasn't reported a room yet, deny any new StartGame to
|
||||
// avoid racing concurrent room creation on the worker.
|
||||
// * When the user is starting a NEW game (empty room id), we give the
|
||||
// worker a short grace period to close the previous room and free the
|
||||
// slot before rejecting with "no slots".
|
||||
// - If the worker is FREE, reserve the slot lazily before starting the
|
||||
// game; the room id (if any) comes from the request / worker.
|
||||
|
||||
// Grace period: when there's no room id in the request (new game) but the
|
||||
// worker still appears busy, wait a bit for the previous room to close.
|
||||
if rq.RoomId == "" && !u.w.HasSlot() {
|
||||
const waitTotal = 3 * time.Second
|
||||
const step = 100 * time.Millisecond
|
||||
waited := time.Duration(0)
|
||||
for waited < waitTotal {
|
||||
if u.w.HasSlot() {
|
||||
break
|
||||
}
|
||||
time.Sleep(step)
|
||||
waited += step
|
||||
}
|
||||
}
|
||||
|
||||
busy := !u.w.HasSlot()
|
||||
if busy {
|
||||
if u.w.RoomId == "" {
|
||||
u.Notify(api.ErrNoFreeSlots, "")
|
||||
return
|
||||
}
|
||||
if rq.RoomId == "" {
|
||||
// No room id but worker is busy -> assume user wants to continue
|
||||
// the existing room instead of starting a parallel game.
|
||||
rq.RoomId = u.w.RoomId
|
||||
} else if rq.RoomId != u.w.RoomId {
|
||||
u.Notify(api.ErrNoFreeSlots, "")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Worker is free: try to reserve the single slot for this new room.
|
||||
if !u.w.TryReserve() {
|
||||
u.Notify(api.ErrNoFreeSlots, "")
|
||||
return
|
||||
}
|
||||
game = name
|
||||
}
|
||||
|
||||
gameInfo, err := launcher.FindAppByName(game)
|
||||
if err != nil {
|
||||
u.log.Error().Err(err).Send()
|
||||
return
|
||||
}
|
||||
|
||||
startGameResp, err := u.w.StartGame(u.Id(), gameInfo, rq)
|
||||
startGameResp, err := u.w.StartGame(u.Id().String(), rq)
|
||||
if err != nil || startGameResp == nil {
|
||||
u.log.Error().Err(err).Msg("malformed game start response")
|
||||
return
|
||||
|
|
@ -56,7 +86,7 @@ func (u *User) HandleStartGame(rq api.GameStartUserRequest, launcher games.Launc
|
|||
return
|
||||
}
|
||||
u.log.Info().Str("id", startGameResp.Rid).Msg("Received room response from worker")
|
||||
u.StartGame()
|
||||
u.StartGame(startGameResp.AV, startGameResp.KbMouse)
|
||||
|
||||
// send back recording status
|
||||
if conf.Recording.Enabled && rq.Record {
|
||||
|
|
@ -64,23 +94,37 @@ func (u *User) HandleStartGame(rq api.GameStartUserRequest, launcher games.Launc
|
|||
}
|
||||
}
|
||||
|
||||
func (u *User) HandleQuitGame(rq api.GameQuitRequest[com.Uid]) {
|
||||
if rq.Room.Rid == u.w.RoomId {
|
||||
u.w.QuitGame(u.Id())
|
||||
func (u *User) HandleQuitGame(rq api.GameQuitRequest) {
|
||||
if rq.Rid == u.w.RoomId {
|
||||
u.w.QuitGame(u.Id().String())
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) HandleResetGame(rq api.ResetGameRequest) {
|
||||
if rq.Rid != u.w.RoomId {
|
||||
return
|
||||
}
|
||||
u.w.ResetGame(u.Id().String())
|
||||
}
|
||||
|
||||
func (u *User) HandleSaveGame() error {
|
||||
resp, err := u.w.SaveGame(u.Id())
|
||||
resp, err := u.w.SaveGame(u.Id().String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if *resp == api.OK {
|
||||
if id, _ := api.ExplodeDeepLink(u.w.RoomId); id != "" {
|
||||
u.w.AddSession(id)
|
||||
}
|
||||
}
|
||||
|
||||
u.Notify(api.SaveGame, resp)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) HandleLoadGame() error {
|
||||
resp, err := u.w.LoadGame(u.Id())
|
||||
resp, err := u.w.LoadGame(u.Id().String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -89,7 +133,7 @@ func (u *User) HandleLoadGame() error {
|
|||
}
|
||||
|
||||
func (u *User) HandleChangePlayer(rq api.ChangePlayerUserRequest) {
|
||||
resp, err := u.w.ChangePlayer(u.Id(), int(rq))
|
||||
resp, err := u.w.ChangePlayer(u.Id().String(), int(rq))
|
||||
// !to make it a little less convoluted
|
||||
if err != nil || resp == nil || *resp == -1 {
|
||||
u.log.Error().Err(err).Msgf("player select fail, req: %v", rq)
|
||||
|
|
@ -98,9 +142,7 @@ func (u *User) HandleChangePlayer(rq api.ChangePlayerUserRequest) {
|
|||
u.Notify(api.ChangePlayer, rq)
|
||||
}
|
||||
|
||||
func (u *User) HandleToggleMultitap() { u.w.ToggleMultitap(u.Id()) }
|
||||
|
||||
func (u *User) HandleRecordGame(rq api.RecordGameRequest[com.Uid]) {
|
||||
func (u *User) HandleRecordGame(rq api.RecordGameRequest) {
|
||||
if u.w == nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -112,7 +154,7 @@ func (u *User) HandleRecordGame(rq api.RecordGameRequest[com.Uid]) {
|
|||
return
|
||||
}
|
||||
|
||||
resp, err := u.w.RecordGame(u.Id(), rq.Active, rq.User)
|
||||
resp, err := u.w.RecordGame(u.Id().String(), rq.Active, rq.User)
|
||||
if err != nil {
|
||||
u.log.Error().Err(err).Msg("malformed game record request")
|
||||
return
|
||||
|
|
@ -127,14 +169,16 @@ func (u *User) handleGetWorkerList(debug bool, info HasServerInfo) {
|
|||
if debug {
|
||||
response.Servers = servers
|
||||
} else {
|
||||
// not sure if []byte to string always reversible :/
|
||||
unique := map[string]*api.Server{}
|
||||
for _, s := range servers {
|
||||
mid := s.Machine
|
||||
if _, ok := unique[mid]; !ok {
|
||||
unique[mid] = &api.Server{Addr: s.Addr, PingURL: s.PingURL, Id: s.Id, InGroup: true}
|
||||
}
|
||||
unique[mid].Replicas++
|
||||
v := unique[mid]
|
||||
if v != nil {
|
||||
v.Replicas++
|
||||
}
|
||||
}
|
||||
for _, v := range unique {
|
||||
response.Servers = append(response.Servers, *v)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package coordinator
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
|
||||
|
|
@ -10,8 +11,10 @@ import (
|
|||
)
|
||||
|
||||
type Worker struct {
|
||||
AppLibrary
|
||||
Connection
|
||||
RegionalClient
|
||||
Session
|
||||
slotted
|
||||
|
||||
Addr string
|
||||
|
|
@ -21,6 +24,9 @@ type Worker struct {
|
|||
Tag string
|
||||
Zone string
|
||||
|
||||
Lib []api.GameInfo
|
||||
Sessions map[string]struct{}
|
||||
|
||||
log *logger.Logger
|
||||
}
|
||||
|
||||
|
|
@ -29,7 +35,28 @@ type RegionalClient interface {
|
|||
}
|
||||
|
||||
type HasUserRegistry interface {
|
||||
Find(com.Uid) *User
|
||||
Find(id string) *User
|
||||
}
|
||||
|
||||
type AppLibrary interface {
|
||||
SetLib([]api.GameInfo)
|
||||
AppNames() []api.GameInfo
|
||||
}
|
||||
|
||||
type Session interface {
|
||||
AddSession(id string)
|
||||
// HadSession is true when an old session is found
|
||||
HadSession(id string) bool
|
||||
SetSessions(map[string]struct{})
|
||||
}
|
||||
|
||||
type AppMeta struct {
|
||||
Alias string
|
||||
Base string
|
||||
Name string
|
||||
Path string
|
||||
System string
|
||||
Type string
|
||||
}
|
||||
|
||||
func NewWorker(sock *com.Connection, handshake api.ConnectionRequest[com.Uid], log *logger.Logger) *Worker {
|
||||
|
|
@ -49,39 +76,58 @@ func NewWorker(sock *com.Connection, handshake api.ConnectionRequest[com.Uid], l
|
|||
}
|
||||
|
||||
func (w *Worker) HandleRequests(users HasUserRegistry) chan struct{} {
|
||||
return w.ProcessPackets(func(p api.In[com.Uid]) error {
|
||||
payload := p.GetPayload()
|
||||
switch p.GetType() {
|
||||
return w.ProcessPackets(func(p api.In[com.Uid]) (err error) {
|
||||
switch p.T {
|
||||
case api.RegisterRoom:
|
||||
rq := api.Unwrap[api.RegisterRoomRequest](payload)
|
||||
if rq == nil {
|
||||
return api.ErrMalformed
|
||||
}
|
||||
w.log.Info().Msgf("set room [%v] = %v", w.Id(), *rq)
|
||||
w.HandleRegisterRoom(*rq)
|
||||
err = api.Do(p, func(d api.RegisterRoomRequest) {
|
||||
w.log.Info().Msgf("set room [%v] = %v", w.Id(), d)
|
||||
w.HandleRegisterRoom(d)
|
||||
})
|
||||
case api.CloseRoom:
|
||||
rq := api.Unwrap[api.CloseRoomRequest](payload)
|
||||
if rq == nil {
|
||||
return api.ErrMalformed
|
||||
}
|
||||
w.HandleCloseRoom(*rq)
|
||||
err = api.Do(p, w.HandleCloseRoom)
|
||||
case api.IceCandidate:
|
||||
rq := api.Unwrap[api.WebrtcIceCandidateRequest[com.Uid]](payload)
|
||||
if rq == nil {
|
||||
return api.ErrMalformed
|
||||
}
|
||||
err := w.HandleIceCandidate(*rq, users)
|
||||
if err != nil {
|
||||
w.log.Error().Err(err).Send()
|
||||
return api.ErrMalformed
|
||||
}
|
||||
err = api.DoE(p, func(d api.WebrtcIceCandidateRequest) error {
|
||||
return w.HandleIceCandidate(d, users)
|
||||
})
|
||||
case api.LibNewGameList:
|
||||
err = api.DoE(p, w.HandleLibGameList)
|
||||
case api.PrevSessions:
|
||||
err = api.DoE(p, w.HandlePrevSessionList)
|
||||
default:
|
||||
w.log.Warn().Msgf("Unknown packet: %+v", p)
|
||||
}
|
||||
return nil
|
||||
if err != nil && !errors.Is(err, api.ErrMalformed) {
|
||||
w.log.Error().Err(err).Send()
|
||||
err = api.ErrMalformed
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
func (w *Worker) SetLib(list []api.GameInfo) { w.Lib = list }
|
||||
|
||||
func (w *Worker) AppNames() []api.GameInfo {
|
||||
return w.Lib
|
||||
}
|
||||
|
||||
func (w *Worker) AddSession(id string) {
|
||||
// sessions can be uninitialized until the coordinator pushes them to the worker
|
||||
if w.Sessions == nil {
|
||||
return
|
||||
}
|
||||
|
||||
w.Sessions[id] = struct{}{}
|
||||
}
|
||||
|
||||
func (w *Worker) HadSession(id string) bool {
|
||||
_, ok := w.Sessions[id]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (w *Worker) SetSessions(sessions map[string]struct{}) {
|
||||
w.Sessions = sessions
|
||||
}
|
||||
|
||||
// In say whether some worker from this region (zone).
|
||||
// Empty region always returns true.
|
||||
func (w *Worker) In(region string) bool { return region == "" || region == w.Zone }
|
||||
|
|
@ -94,13 +140,40 @@ type slotted int32
|
|||
// there are no players in the room (worker).
|
||||
func (s *slotted) HasSlot() bool { return atomic.LoadInt32((*int32)(s)) == 0 }
|
||||
|
||||
// Reserve increments user counter of the worker.
|
||||
func (s *slotted) Reserve() { atomic.AddInt32((*int32)(s), 1) }
|
||||
// TryReserve reserves the slot only when it's free.
|
||||
func (s *slotted) TryReserve() bool {
|
||||
for {
|
||||
current := atomic.LoadInt32((*int32)(s))
|
||||
if current != 0 {
|
||||
return false
|
||||
}
|
||||
if atomic.CompareAndSwapInt32((*int32)(s), 0, 1) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UnReserve decrements user counter of the worker.
|
||||
func (s *slotted) UnReserve() {
|
||||
if atomic.AddInt32((*int32)(s), -1) < 0 {
|
||||
atomic.StoreInt32((*int32)(s), 0)
|
||||
for {
|
||||
current := atomic.LoadInt32((*int32)(s))
|
||||
if current <= 0 {
|
||||
// reset to zero
|
||||
if current < 0 {
|
||||
if atomic.CompareAndSwapInt32((*int32)(s), current, 0) {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Regular decrement for positive values
|
||||
newVal := current - 1
|
||||
if atomic.CompareAndSwapInt32((*int32)(s), current, newVal) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
193
pkg/coordinator/worker_test.go
Normal file
193
pkg/coordinator/worker_test.go
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
package coordinator
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSlotted(t *testing.T) {
|
||||
t.Run("UnReserve", func(t *testing.T) {
|
||||
t.Run("BasicDecrement", testUnReserveBasic)
|
||||
t.Run("PreventUnderflow", testUnReserveUnderflow)
|
||||
t.Run("ConcurrentDecrement", testUnReserveConcurrent)
|
||||
})
|
||||
|
||||
t.Run("TryReserve", func(t *testing.T) {
|
||||
t.Run("SuccessWhenZero", testTryReserveSuccess)
|
||||
t.Run("FailWhenNonZero", testTryReserveFailure)
|
||||
t.Run("ConcurrentReservations", testTryReserveConcurrent)
|
||||
})
|
||||
|
||||
t.Run("Integration", func(t *testing.T) {
|
||||
t.Run("ReserveUnreserveFlow", testReserveUnreserveFlow)
|
||||
t.Run("FreeSlots", testFreeSlots)
|
||||
t.Run("HasSlot", testHasSlot)
|
||||
})
|
||||
}
|
||||
|
||||
func testUnReserveBasic(t *testing.T) {
|
||||
t.Parallel()
|
||||
var s slotted
|
||||
|
||||
// Initial state
|
||||
if atomic.LoadInt32((*int32)(&s)) != 0 {
|
||||
t.Fatal("initial state not zero")
|
||||
}
|
||||
|
||||
// Test normal decrement
|
||||
s.TryReserve() // 0 -> 1
|
||||
s.UnReserve()
|
||||
if atomic.LoadInt32((*int32)(&s)) != 0 {
|
||||
t.Error("failed to decrement to zero")
|
||||
}
|
||||
|
||||
// Test multiple decrements
|
||||
s.TryReserve() // 0 -> 1
|
||||
s.TryReserve() // 1 -> 2
|
||||
s.UnReserve()
|
||||
s.UnReserve()
|
||||
if atomic.LoadInt32((*int32)(&s)) != 0 {
|
||||
t.Error("failed to decrement multiple times")
|
||||
}
|
||||
}
|
||||
|
||||
func testUnReserveUnderflow(t *testing.T) {
|
||||
t.Parallel()
|
||||
var s slotted
|
||||
|
||||
t.Run("PreventNewUnderflow", func(t *testing.T) {
|
||||
s.UnReserve() // Start at 0
|
||||
if atomic.LoadInt32((*int32)(&s)) != 0 {
|
||||
t.Error("should remain at 0 when unreserving from 0")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("FixExistingNegative", func(t *testing.T) {
|
||||
atomic.StoreInt32((*int32)(&s), -5)
|
||||
s.UnReserve()
|
||||
if current := atomic.LoadInt32((*int32)(&s)); current != 0 {
|
||||
t.Errorf("should fix negative value to 0, got %d", current)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func testUnReserveConcurrent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var s slotted
|
||||
const workers = 100
|
||||
var wg sync.WaitGroup
|
||||
|
||||
atomic.StoreInt32((*int32)(&s), int32(workers))
|
||||
wg.Add(workers)
|
||||
|
||||
for range workers {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
s.UnReserve()
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if current := atomic.LoadInt32((*int32)(&s)); current != 0 {
|
||||
t.Errorf("unexpected final value: %d (want 0)", current)
|
||||
}
|
||||
}
|
||||
|
||||
func testTryReserveSuccess(t *testing.T) {
|
||||
t.Parallel()
|
||||
var s slotted
|
||||
|
||||
if !s.TryReserve() {
|
||||
t.Error("should succeed when zero")
|
||||
}
|
||||
if atomic.LoadInt32((*int32)(&s)) != 1 {
|
||||
t.Error("failed to increment")
|
||||
}
|
||||
}
|
||||
|
||||
func testTryReserveFailure(t *testing.T) {
|
||||
t.Parallel()
|
||||
var s slotted
|
||||
|
||||
atomic.StoreInt32((*int32)(&s), 1)
|
||||
if s.TryReserve() {
|
||||
t.Error("should fail when non-zero")
|
||||
}
|
||||
}
|
||||
|
||||
func testTryReserveConcurrent(t *testing.T) {
|
||||
t.Parallel()
|
||||
var s slotted
|
||||
const workers = 100
|
||||
var success int32
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(workers)
|
||||
for range workers {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if s.TryReserve() {
|
||||
atomic.AddInt32(&success, 1)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if success != 1 {
|
||||
t.Errorf("unexpected success count: %d (want 1)", success)
|
||||
}
|
||||
if atomic.LoadInt32((*int32)(&s)) != 1 {
|
||||
t.Error("counter not properly incremented")
|
||||
}
|
||||
}
|
||||
|
||||
func testReserveUnreserveFlow(t *testing.T) {
|
||||
t.Parallel()
|
||||
var s slotted
|
||||
|
||||
// Successful reservation
|
||||
if !s.TryReserve() {
|
||||
t.Fatal("failed initial reservation")
|
||||
}
|
||||
|
||||
// Second reservation should fail
|
||||
if s.TryReserve() {
|
||||
t.Error("unexpected successful second reservation")
|
||||
}
|
||||
|
||||
// Unreserve and try again
|
||||
s.UnReserve()
|
||||
if !s.TryReserve() {
|
||||
t.Error("failed reservation after unreserve")
|
||||
}
|
||||
}
|
||||
|
||||
func testFreeSlots(t *testing.T) {
|
||||
t.Parallel()
|
||||
var s slotted
|
||||
|
||||
// Set to arbitrary value
|
||||
atomic.StoreInt32((*int32)(&s), 5)
|
||||
s.FreeSlots()
|
||||
if atomic.LoadInt32((*int32)(&s)) != 0 {
|
||||
t.Error("FreeSlots failed to reset counter")
|
||||
}
|
||||
}
|
||||
|
||||
func testHasSlot(t *testing.T) {
|
||||
t.Parallel()
|
||||
var s slotted
|
||||
|
||||
if !s.HasSlot() {
|
||||
t.Error("should have slot when zero")
|
||||
}
|
||||
|
||||
s.TryReserve()
|
||||
if s.HasSlot() {
|
||||
t.Error("shouldn't have slot when reserved")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,67 +1,68 @@
|
|||
package coordinator
|
||||
|
||||
import (
|
||||
"github.com/giongto35/cloud-game/v3/pkg/api"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/com"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/games"
|
||||
)
|
||||
import "github.com/giongto35/cloud-game/v3/pkg/api"
|
||||
|
||||
func (w *Worker) WebrtcInit(id com.Uid) (*api.WebrtcInitResponse, error) {
|
||||
func (w *Worker) WebrtcInit(id string) (*api.WebrtcInitResponse, error) {
|
||||
return api.UnwrapChecked[api.WebrtcInitResponse](
|
||||
w.Send(api.WebrtcInit, api.WebrtcInitRequest[com.Uid]{Stateful: api.Stateful[com.Uid]{Id: id}}))
|
||||
w.Send(api.WebrtcInit, api.WebrtcInitRequest{Id: id}))
|
||||
}
|
||||
|
||||
func (w *Worker) WebrtcAnswer(id com.Uid, sdp string) {
|
||||
w.Notify(api.WebrtcAnswer, api.WebrtcAnswerRequest[com.Uid]{Stateful: api.Stateful[com.Uid]{Id: id}, Sdp: sdp})
|
||||
func (w *Worker) WebrtcAnswer(id string, sdp string) {
|
||||
w.Notify(api.WebrtcAnswer,
|
||||
api.WebrtcAnswerRequest{Stateful: api.Stateful{Id: id}, Sdp: sdp})
|
||||
}
|
||||
|
||||
func (w *Worker) WebrtcIceCandidate(id com.Uid, can string) {
|
||||
w.Notify(api.WebrtcIce, api.WebrtcIceCandidateRequest[com.Uid]{Stateful: api.Stateful[com.Uid]{Id: id}, Candidate: can})
|
||||
func (w *Worker) WebrtcIceCandidate(id string, candidate string) {
|
||||
w.Notify(api.WebrtcIce,
|
||||
api.WebrtcIceCandidateRequest{Stateful: api.Stateful{Id: id}, Candidate: candidate})
|
||||
}
|
||||
|
||||
func (w *Worker) StartGame(id com.Uid, app games.AppMeta, req api.GameStartUserRequest) (*api.StartGameResponse, error) {
|
||||
func (w *Worker) StartGame(id string, req api.GameStartUserRequest) (*api.StartGameResponse, error) {
|
||||
return api.UnwrapChecked[api.StartGameResponse](
|
||||
w.Send(api.StartGame, api.StartGameRequest[com.Uid]{
|
||||
StatefulRoom: StateRoom(id, req.RoomId),
|
||||
Game: api.GameInfo(app),
|
||||
w.Send(api.StartGame, api.StartGameRequest{
|
||||
StatefulRoom: api.StatefulRoom{Id: id, Rid: req.RoomId},
|
||||
Game: req.GameName,
|
||||
PlayerIndex: req.PlayerIndex,
|
||||
Record: req.Record,
|
||||
RecordUser: req.RecordUser,
|
||||
}))
|
||||
}
|
||||
|
||||
func (w *Worker) QuitGame(id com.Uid) {
|
||||
w.Notify(api.QuitGame, api.GameQuitRequest[com.Uid]{StatefulRoom: StateRoom(id, w.RoomId)})
|
||||
func (w *Worker) QuitGame(id string) {
|
||||
w.Notify(api.QuitGame, api.GameQuitRequest{Id: id, Rid: w.RoomId})
|
||||
}
|
||||
|
||||
func (w *Worker) SaveGame(id com.Uid) (*api.SaveGameResponse, error) {
|
||||
func (w *Worker) SaveGame(id string) (*api.SaveGameResponse, error) {
|
||||
return api.UnwrapChecked[api.SaveGameResponse](
|
||||
w.Send(api.SaveGame, api.SaveGameRequest[com.Uid]{StatefulRoom: StateRoom(id, w.RoomId)}))
|
||||
w.Send(api.SaveGame, api.SaveGameRequest{Id: id, Rid: w.RoomId}))
|
||||
}
|
||||
|
||||
func (w *Worker) LoadGame(id com.Uid) (*api.LoadGameResponse, error) {
|
||||
func (w *Worker) LoadGame(id string) (*api.LoadGameResponse, error) {
|
||||
return api.UnwrapChecked[api.LoadGameResponse](
|
||||
w.Send(api.LoadGame, api.LoadGameRequest[com.Uid]{StatefulRoom: StateRoom(id, w.RoomId)}))
|
||||
w.Send(api.LoadGame, api.LoadGameRequest{Id: id, Rid: w.RoomId}))
|
||||
}
|
||||
|
||||
func (w *Worker) ChangePlayer(id com.Uid, index int) (*api.ChangePlayerResponse, error) {
|
||||
func (w *Worker) ChangePlayer(id string, index int) (*api.ChangePlayerResponse, error) {
|
||||
return api.UnwrapChecked[api.ChangePlayerResponse](
|
||||
w.Send(api.ChangePlayer, api.ChangePlayerRequest[com.Uid]{StatefulRoom: StateRoom(id, w.RoomId), Index: index}))
|
||||
w.Send(api.ChangePlayer, api.ChangePlayerRequest{
|
||||
StatefulRoom: api.StatefulRoom{Id: id, Rid: w.RoomId},
|
||||
Index: index,
|
||||
}))
|
||||
}
|
||||
|
||||
func (w *Worker) ToggleMultitap(id com.Uid) {
|
||||
_, _ = w.Send(api.ToggleMultitap, api.ToggleMultitapRequest[com.Uid]{StatefulRoom: StateRoom(id, w.RoomId)})
|
||||
func (w *Worker) ResetGame(id string) {
|
||||
w.Notify(api.ResetGame, api.ResetGameRequest{Id: id, Rid: w.RoomId})
|
||||
}
|
||||
|
||||
func (w *Worker) RecordGame(id com.Uid, rec bool, recUser string) (*api.RecordGameResponse, error) {
|
||||
func (w *Worker) RecordGame(id string, rec bool, recUser string) (*api.RecordGameResponse, error) {
|
||||
return api.UnwrapChecked[api.RecordGameResponse](
|
||||
w.Send(api.RecordGame, api.RecordGameRequest[com.Uid]{StatefulRoom: StateRoom(id, w.RoomId), Active: rec, User: recUser}))
|
||||
w.Send(api.RecordGame, api.RecordGameRequest{
|
||||
StatefulRoom: api.StatefulRoom{Id: id, Rid: w.RoomId},
|
||||
Active: rec,
|
||||
User: recUser,
|
||||
}))
|
||||
}
|
||||
|
||||
func (w *Worker) TerminateSession(id com.Uid) {
|
||||
_, _ = w.Send(api.TerminateSession, api.TerminateSessionRequest[com.Uid]{Stateful: api.Stateful[com.Uid]{Id: id}})
|
||||
}
|
||||
|
||||
func StateRoom[T api.Id](id T, rid string) api.StatefulRoom[T] {
|
||||
return api.StatefulRoom[T]{Stateful: api.Stateful[T]{Id: id}, Room: api.Room{Rid: rid}}
|
||||
func (w *Worker) TerminateSession(id string) {
|
||||
_, _ = w.Send(api.TerminateSession, api.TerminateSessionRequest{Id: id})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,39 @@
|
|||
package coordinator
|
||||
|
||||
import (
|
||||
"github.com/giongto35/cloud-game/v3/pkg/api"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/com"
|
||||
)
|
||||
import "github.com/giongto35/cloud-game/v3/pkg/api"
|
||||
|
||||
func (w *Worker) HandleRegisterRoom(rq api.RegisterRoomRequest) { w.RoomId = string(rq) }
|
||||
|
||||
func (w *Worker) HandleCloseRoom(rq api.CloseRoomRequest) {
|
||||
if string(rq) == w.RoomId {
|
||||
w.RoomId = ""
|
||||
w.FreeSlots()
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Worker) HandleIceCandidate(rq api.WebrtcIceCandidateRequest[com.Uid], users HasUserRegistry) error {
|
||||
func (w *Worker) HandleIceCandidate(rq api.WebrtcIceCandidateRequest, users HasUserRegistry) error {
|
||||
if usr := users.Find(rq.Id); usr != nil {
|
||||
usr.SendWebrtcIceCandidate(rq.Candidate)
|
||||
} else {
|
||||
w.log.Warn().Str("id", rq.Id.String()).Msg("unknown session")
|
||||
w.log.Warn().Str("id", rq.Id).Msg("unknown session")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Worker) HandleLibGameList(inf api.LibGameListInfo) error {
|
||||
w.SetLib(inf.List)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Worker) HandlePrevSessionList(sess api.PrevSessionInfo) error {
|
||||
if len(sess.List) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
m := make(map[string]struct{})
|
||||
for _, v := range sess.List {
|
||||
m[v] = struct{}{}
|
||||
}
|
||||
w.SetSessions(m)
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@ func ToRGBA(img image.Image, flipped bool) *image.RGBA {
|
|||
bounds := img.Bounds()
|
||||
sw, sh := bounds.Dx(), bounds.Dy()
|
||||
dst := image.NewRGBA(image.Rect(0, 0, sw, sh))
|
||||
for y := 0; y < sh; y++ {
|
||||
for y := range sh {
|
||||
yy := y
|
||||
if flipped {
|
||||
yy = sh - y
|
||||
}
|
||||
for x := 0; x < sw; x++ {
|
||||
for x := range sw {
|
||||
px := img.At(x, y)
|
||||
rgba := color.RGBAModel.Convert(px).(color.RGBA)
|
||||
dst.Set(x, yy, rgba)
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ package encoder
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/giongto35/cloud-game/v3/pkg/config"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/encoder/h264"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/encoder/vpx"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/encoder/yuv"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/logger"
|
||||
)
|
||||
|
|
@ -13,9 +15,9 @@ type (
|
|||
InFrame yuv.RawFrame
|
||||
OutFrame []byte
|
||||
Encoder interface {
|
||||
LoadBuf(input []byte)
|
||||
Encode() []byte
|
||||
Encode([]byte) []byte
|
||||
IntraRefresh()
|
||||
Info() string
|
||||
SetFlip(bool)
|
||||
Shutdown() error
|
||||
}
|
||||
|
|
@ -28,7 +30,6 @@ type Video struct {
|
|||
y yuv.Conv
|
||||
pf yuv.PixFmt
|
||||
rot uint
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type VideoCodec string
|
||||
|
|
@ -36,6 +37,8 @@ type VideoCodec string
|
|||
const (
|
||||
H264 VideoCodec = "h264"
|
||||
VP8 VideoCodec = "vp8"
|
||||
VP9 VideoCodec = "vp9"
|
||||
VPX VideoCodec = "vpx"
|
||||
)
|
||||
|
||||
// NewVideoEncoder returns new video encoder.
|
||||
|
|
@ -43,31 +46,59 @@ const (
|
|||
// converts them into YUV I420 format,
|
||||
// encodes with provided video encoder, and
|
||||
// puts the result into the output channel.
|
||||
func NewVideoEncoder(codec Encoder, w, h int, scale float64, log *logger.Logger) *Video {
|
||||
return &Video{codec: codec, y: yuv.NewYuvConv(w, h, scale), log: log}
|
||||
func NewVideoEncoder(w, h, dw, dh int, scale float64, conf config.Video, log *logger.Logger) (*Video, error) {
|
||||
var enc Encoder
|
||||
var err error
|
||||
codec := VideoCodec(conf.Codec)
|
||||
switch codec {
|
||||
case H264:
|
||||
opts := h264.Options(conf.H264)
|
||||
enc, err = h264.NewEncoder(dw, dh, conf.Threads, &opts)
|
||||
case VP8, VP9, VPX:
|
||||
opts := vpx.Options(conf.Vpx)
|
||||
v := 8
|
||||
if codec == VP9 {
|
||||
v = 9
|
||||
}
|
||||
enc, err = vpx.NewEncoder(dw, dh, conf.Threads, v, &opts)
|
||||
default:
|
||||
err = fmt.Errorf("unsupported codec: %v", conf.Codec)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if enc == nil {
|
||||
return nil, fmt.Errorf("no encoder")
|
||||
}
|
||||
|
||||
return &Video{codec: enc, y: yuv.NewYuvConv(w, h, scale), log: log}, nil
|
||||
}
|
||||
|
||||
func (v *Video) Encode(frame InFrame) OutFrame {
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
if v.stopped.Load() {
|
||||
return nil
|
||||
}
|
||||
|
||||
yCbCr := v.y.Process(yuv.RawFrame(frame), v.rot, v.pf)
|
||||
v.codec.LoadBuf(yCbCr)
|
||||
v.y.Put(&yCbCr)
|
||||
|
||||
if bytes := v.codec.Encode(); len(bytes) > 0 {
|
||||
//defer v.y.Put(&yCbCr)
|
||||
if bytes := v.codec.Encode(yCbCr); len(bytes) > 0 {
|
||||
return bytes
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Video) Info() string { return fmt.Sprintf("libyuv: %v", v.y.Version()) }
|
||||
func (v *Video) Info() string {
|
||||
return fmt.Sprintf("%v, libyuv: %v", v.codec.Info(), v.y.Version())
|
||||
}
|
||||
|
||||
func (v *Video) SetPixFormat(f uint32) {
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch f {
|
||||
case 0:
|
||||
v.pf = yuv.PixFmt(yuv.FourccRgb0)
|
||||
case 1:
|
||||
v.pf = yuv.PixFmt(yuv.FourccArgb)
|
||||
case 2:
|
||||
|
|
@ -77,29 +108,39 @@ func (v *Video) SetPixFormat(f uint32) {
|
|||
}
|
||||
}
|
||||
|
||||
// SetRot sets the rotation angle of the frames.
|
||||
func (v *Video) SetRot(r uint) {
|
||||
switch r {
|
||||
// de-rotate
|
||||
case 90:
|
||||
v.rot = 270
|
||||
case 270:
|
||||
v.rot = 90
|
||||
default:
|
||||
v.rot = r
|
||||
// SetRot sets the de-rotation angle of the frames.
|
||||
func (v *Video) SetRot(a uint) {
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if a > 0 {
|
||||
v.rot = (a + 180) % 360
|
||||
}
|
||||
}
|
||||
|
||||
// SetFlip tells the encoder to flip the frames vertically.
|
||||
func (v *Video) SetFlip(b bool) { v.codec.SetFlip(b) }
|
||||
func (v *Video) SetFlip(b bool) {
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
v.codec.SetFlip(b)
|
||||
}
|
||||
|
||||
func (v *Video) Stop() {
|
||||
v.stopped.Store(true)
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if v.stopped.Swap(true) {
|
||||
return
|
||||
}
|
||||
v.rot = 0
|
||||
|
||||
defer func() { v.codec = nil }()
|
||||
if err := v.codec.Shutdown(); err != nil {
|
||||
v.log.Error().Err(err).Msg("failed to close the encoder")
|
||||
if v.log != nil {
|
||||
v.log.Error().Err(err).Msg("failed to close the encoder")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,528 +0,0 @@
|
|||
// Package h264 implements cgo bindings for [x264](https://www.videolan.org/developers/x264.html) library.
|
||||
package h264
|
||||
|
||||
/*
|
||||
#cgo !st pkg-config: x264
|
||||
#cgo st LDFLAGS: -l:libx264.a
|
||||
|
||||
#include "stdint.h"
|
||||
#include "x264.h"
|
||||
#include <stdlib.h>
|
||||
*/
|
||||
import "C"
|
||||
import "unsafe"
|
||||
|
||||
const Build = C.X264_BUILD
|
||||
|
||||
// T is opaque handler for encoder
|
||||
type T struct{}
|
||||
|
||||
// Nal is The data within the payload is already NAL-encapsulated; the ref_idc and type
|
||||
// are merely in the struct for easy access by the calling application.
|
||||
// All data returned in x264_nal_t, including the data in p_payload, is no longer
|
||||
// valid after the next call to x264_encoder_encode. Thus, it must be used or copied
|
||||
// before calling x264_encoder_encode or x264_encoder_headers again.
|
||||
type Nal struct {
|
||||
IRefIdc int32 /* nal_priority_e */
|
||||
IType int32 /* nal_unit_type_e */
|
||||
BLongStartcode int32
|
||||
IFirstMb int32 /* If this NAL is a slice, the index of the first MB in the slice. */
|
||||
ILastMb int32 /* If this NAL is a slice, the index of the last MB in the slice. */
|
||||
|
||||
/* Size of payload (including any padding) in bytes. */
|
||||
IPayload int32
|
||||
/* If param->b_annexb is set, Annex-B bytestream with startcode.
|
||||
* Otherwise, startcode is replaced with a 4-byte size.
|
||||
* This size is the size used in mp4/similar muxing; it is equal to i_payload-4 */
|
||||
/* C.uint8_t */
|
||||
PPayload unsafe.Pointer
|
||||
|
||||
/* Size of padding in bytes. */
|
||||
IPadding int32
|
||||
}
|
||||
|
||||
const RcCrf = 1
|
||||
|
||||
const (
|
||||
CspI420 = 0x0002 // yuv 4:2:0 planar
|
||||
CspVflip = 0x1000 /* the csp is vertically flipped */
|
||||
|
||||
// CspMask = 0x00ff /* */
|
||||
// CspNone = 0x0000 /* Invalid mode */
|
||||
// CspI400 = 0x0001 /* monochrome 4:0:0 */
|
||||
|
||||
//CspYv12 = 0x0003 /* yvu 4:2:0 planar */
|
||||
//CspNv12 = 0x0004 /* yuv 4:2:0, with one y plane and one packed u+v */
|
||||
//CspNv21 = 0x0005 /* yuv 4:2:0, with one y plane and one packed v+u */
|
||||
//CspI422 = 0x0006 /* yuv 4:2:2 planar */
|
||||
//CspYv16 = 0x0007 /* yvu 4:2:2 planar */
|
||||
//CspNv16 = 0x0008 /* yuv 4:2:2, with one y plane and one packed u+v */
|
||||
//CspYuyv = 0x0009 /* yuyv 4:2:2 packed */
|
||||
//CspUyvy = 0x000a /* uyvy 4:2:2 packed */
|
||||
//CspV210 = 0x000b /* 10-bit yuv 4:2:2 packed in 32 */
|
||||
//CspI444 = 0x000c /* yuv 4:4:4 planar */
|
||||
//CspYv24 = 0x000d /* yvu 4:4:4 planar */
|
||||
//CspBgr = 0x000e /* packed bgr 24bits */
|
||||
//CspBgra = 0x000f /* packed bgr 32bits */
|
||||
//CspRgb = 0x0010 /* packed rgb 24bits */
|
||||
//CspMax = 0x0011 /* end of list */
|
||||
//CspHighDepth = 0x2000 /* the csp has a depth of 16 bits per pixel component */
|
||||
)
|
||||
|
||||
type Zone struct {
|
||||
IStart, IEnd int32 /* range of frame numbers */
|
||||
BForceQp int32 /* whether to use qp vs bitrate factor */
|
||||
IQp int32
|
||||
FBitrateFactor float32
|
||||
Param *Param
|
||||
}
|
||||
|
||||
type Param struct {
|
||||
/* CPU flags */
|
||||
Cpu uint32
|
||||
IThreads int32 /* encode multiple frames in parallel */
|
||||
ILookaheadThreads int32 /* multiple threads for lookahead analysis */
|
||||
BSlicedThreads int32 /* Whether to use slice-based threading. */
|
||||
BDeterministic int32 /* whether to allow non-deterministic optimizations when threaded */
|
||||
BCpuIndependent int32 /* force canonical behavior rather than cpu-dependent optimal algorithms */
|
||||
ISyncLookahead int32 /* threaded lookahead buffer */
|
||||
|
||||
/* Video Properties */
|
||||
IWidth int32
|
||||
IHeight int32
|
||||
ICsp int32 /* CSP of encoded bitstream */
|
||||
IBitdepth int32
|
||||
ILevelIdc int32
|
||||
IFrameTotal int32 /* number of frames to encode if known, else 0 */
|
||||
|
||||
/* NAL HRD
|
||||
* Uses Buffering and Picture Timing SEIs to signal HRD
|
||||
* The HRD in H.264 was not designed with VFR in mind.
|
||||
* It is therefore not recommended to use NAL HRD with VFR.
|
||||
* Furthermore, reconfiguring the VBV (via x264_encoder_reconfig)
|
||||
* will currently generate invalid HRD. */
|
||||
INalHrd int32
|
||||
|
||||
Vui struct {
|
||||
/* they will be reduced to be 0 < x <= 65535 and prime */
|
||||
ISarHeight int32
|
||||
ISarWidth int32
|
||||
|
||||
IOverscan int32 /* 0=undef, 1=no overscan, 2=overscan */
|
||||
|
||||
/* see h264 annex E for the values of the following */
|
||||
IVidformat int32
|
||||
BFullrange int32
|
||||
IColorprim int32
|
||||
ITransfer int32
|
||||
IColmatrix int32
|
||||
IChromaLoc int32 /* both top & bottom */
|
||||
}
|
||||
|
||||
/* Bitstream parameters */
|
||||
IFrameReference int32 /* Maximum number of reference frames */
|
||||
IDpbSize int32 /* Force a DPB size larger than that implied by B-frames and reference frames.
|
||||
* Useful in combination with interactive error resilience. */
|
||||
IKeyintMax int32 /* Force an IDR keyframe at this interval */
|
||||
IKeyintMin int32 /* Scenecuts closer together than this are coded as I, not IDR. */
|
||||
IScenecutThreshold int32 /* how aggressively to insert extra I frames */
|
||||
BIntraRefresh int32 /* Whether or not to use periodic intra refresh instead of IDR frames. */
|
||||
|
||||
IBframe int32 /* how many b-frame between 2 references pictures */
|
||||
IBframeAdaptive int32
|
||||
IBframeBias int32
|
||||
IBframePyramid int32 /* Keep some B-frames as references: 0=off, 1=strict hierarchical, 2=normal */
|
||||
BOpenGop int32
|
||||
BBlurayCompat int32
|
||||
IAvcintraClass int32
|
||||
IAvcintraFlavor int32
|
||||
|
||||
BDeblockingFilter int32
|
||||
IDeblockingFilterAlphac0 int32 /* [-6, 6] -6 light filter, 6 strong */
|
||||
IDeblockingFilterBeta int32 /* [-6, 6] idem */
|
||||
|
||||
BCabac int32
|
||||
ICabacInitIdc int32
|
||||
|
||||
BInterlaced int32
|
||||
BConstrainedIntra int32
|
||||
|
||||
ICqmPreset int32
|
||||
PszCqmFile *int8 /* filename (in UTF-8) of CQM file, JM format */
|
||||
Cqm4iy [16]byte /* used only if i_cqm_preset == X264_CQM_CUSTOM */
|
||||
Cqm4py [16]byte
|
||||
Cqm4ic [16]byte
|
||||
Cqm4pc [16]byte
|
||||
Cqm8iy [64]byte
|
||||
Cqm8py [64]byte
|
||||
Cqm8ic [64]byte
|
||||
Cqm8pc [64]byte
|
||||
|
||||
/* Log */
|
||||
PfLog *[0]byte
|
||||
PLogPrivate unsafe.Pointer
|
||||
ILogLevel int32
|
||||
BFullRecon int32 /* fully reconstruct frames, even when not necessary for encoding. Implied by psz_dump_yuv */
|
||||
PszDumpYuv *int8 /* filename (in UTF-8) for reconstructed frames */
|
||||
|
||||
/* Encoder analyser parameters */
|
||||
Analyse struct {
|
||||
Intra uint32 /* intra partitions */
|
||||
Inter uint32 /* inter partitions */
|
||||
|
||||
BTransform8x8 int32
|
||||
IWeightedPred int32 /* weighting for P-frames */
|
||||
BWeightedBipred int32 /* implicit weighting for B-frames */
|
||||
IDirectMvPred int32 /* spatial vs temporal mv prediction */
|
||||
IChromaQpOffset int32
|
||||
|
||||
IMeMethod int32 /* motion estimation algorithm to use (X264_ME_*) */
|
||||
IMeRange int32 /* integer pixel motion estimation search range (from predicted mv) */
|
||||
IMvRange int32 /* maximum length of a mv (in pixels). -1 = auto, based on level */
|
||||
IMvRangeThread int32 /* minimum space between threads. -1 = auto, based on number of threads. */
|
||||
ISubpelRefine int32 /* subpixel motion estimation quality */
|
||||
BChromaMe int32 /* chroma ME for subpel and mode decision in P-frames */
|
||||
BMixedReferences int32 /* allow each mb partition to have its own reference number */
|
||||
ITrellis int32 /* trellis RD quantization */
|
||||
BFastPskip int32 /* early SKIP detection on P-frames */
|
||||
BDctDecimate int32 /* transform coefficient thresholding on P-frames */
|
||||
INoiseReduction int32 /* adaptive pseudo-deadzone */
|
||||
FPsyRd float32 /* Psy RD strength */
|
||||
FPsyTrellis float32 /* Psy trellis strength */
|
||||
BPsy int32 /* Toggle all psy optimizations */
|
||||
|
||||
BMbInfo int32 /* Use input mb_info data in x264_picture_t */
|
||||
BMbInfoUpdate int32 /* Update the values in mb_info according to the results of encoding. */
|
||||
|
||||
/* the deadzone size that will be used in luma quantization */
|
||||
ILumaDeadzone [2]int32
|
||||
|
||||
BPsnr int32 /* compute and print PSNR stats */
|
||||
BSsim int32 /* compute and print SSIM stats */
|
||||
}
|
||||
|
||||
/* Rate control parameters */
|
||||
Rc struct {
|
||||
IRcMethod int32 /* X264_RC_* */
|
||||
|
||||
IQpConstant int32 /* 0=lossless */
|
||||
IQpMin int32 /* min allowed QP value */
|
||||
IQpMax int32 /* max allowed QP value */
|
||||
IQpStep int32 /* max QP step between frames */
|
||||
|
||||
IBitrate int32
|
||||
FRfConstant float32 /* 1pass VBR, nominal QP */
|
||||
FRfConstantMax float32 /* In CRF mode, maximum CRF as caused by VBV */
|
||||
FRateTolerance float32
|
||||
IVbvMaxBitrate int32
|
||||
IVbvBufferSize int32
|
||||
FVbvBufferInit float32 /* <=1: fraction of buffer_size. >1: kbit */
|
||||
FIpFactor float32
|
||||
FPbFactor float32
|
||||
|
||||
/* VBV filler: force CBR VBV and use filler bytes to ensure hard-CBR.
|
||||
* Implied by NAL-HRD CBR. */
|
||||
BFiller int32
|
||||
|
||||
IAqMode int32 /* psy adaptive QP. (X264_AQ_*) */
|
||||
FAqStrength float32
|
||||
BMbTree int32 /* Macroblock-tree ratecontrol. */
|
||||
ILookahead int32
|
||||
|
||||
/* 2pass */
|
||||
BStatWrite int32 /* Enable stat writing in psz_stat_out */
|
||||
PszStatOut *int8 /* output filename (in UTF-8) of the 2pass stats file */
|
||||
BStatRead int32 /* Read stat from psz_stat_in and use it */
|
||||
PszStatIn *int8 /* input filename (in UTF-8) of the 2pass stats file */
|
||||
|
||||
/* 2pass params (same as ffmpeg ones) */
|
||||
FQcompress float32 /* 0.0 => cbr, 1.0 => constant qp */
|
||||
FQblur float32 /* temporally blur quants */
|
||||
FComplexityBlur float32 /* temporally blur complexity */
|
||||
Zones *Zone /* ratecontrol overrides */
|
||||
IZones int32 /* number of zone_t's */
|
||||
PszZones *int8 /* alternate method of specifying zones */
|
||||
}
|
||||
|
||||
/* Cropping Rectangle parameters: added to those implicitly defined by
|
||||
non-mod16 video resolutions. */
|
||||
CropRect struct {
|
||||
ILeft int32
|
||||
ITop int32
|
||||
IRight int32
|
||||
IBottom int32
|
||||
}
|
||||
|
||||
/* frame packing arrangement flag */
|
||||
IFramePacking int32
|
||||
|
||||
/* alternative transfer SEI */
|
||||
IAlternativeTransfer int32
|
||||
|
||||
/* Muxing parameters */
|
||||
BAud int32 /* generate access unit delimiters */
|
||||
BRepeatHeaders int32 /* put SPS/PPS before each keyframe */
|
||||
BAnnexb int32 /* if set, place start codes (4 bytes) before NAL units,
|
||||
* otherwise place size (4 bytes) before NAL units. */
|
||||
ISpsId int32 /* SPS and PPS id number */
|
||||
BVfrInput int32 /* VFR input. If 1, use timebase and timestamps for ratecontrol purposes.
|
||||
* If 0, use fps only. */
|
||||
BPulldown int32 /* use explicity set timebase for CFR */
|
||||
IFpsNum uint32
|
||||
IFpsDen uint32
|
||||
ITimebaseNum uint32 /* Timebase numerator */
|
||||
ITimebaseDen uint32 /* Timebase denominator */
|
||||
|
||||
BTff int32
|
||||
|
||||
/* Pulldown:
|
||||
* The correct pic_struct must be passed with each input frame.
|
||||
* The input timebase should be the timebase corresponding to the output framerate. This should be constant.
|
||||
* e.g. for 3:2 pulldown timebase should be 1001/30000
|
||||
* The PTS passed with each frame must be the PTS of the frame after pulldown is applied.
|
||||
* Frame doubling and tripling require b_vfr_input set to zero (see H.264 Table D-1)
|
||||
*
|
||||
* Pulldown changes are not clearly defined in H.264. Therefore, it is the calling app's responsibility to manage this.
|
||||
*/
|
||||
|
||||
BPicStruct int32
|
||||
|
||||
/* Fake Interlaced.
|
||||
*
|
||||
* Used only when b_interlaced=0. Setting this flag makes it possible to flag the stream as PAFF interlaced yet
|
||||
* encode all frames progressively. It is useful for encoding 25p and 30p Blu-Ray streams.
|
||||
*/
|
||||
BFakeInterlaced int32
|
||||
|
||||
/* Don't optimize header parameters based on video content, e.g. ensure that splitting an input video, compressing
|
||||
* each part, and stitching them back together will result in identical SPS/PPS. This is necessary for stitching
|
||||
* with container formats that don't allow multiple SPS/PPS. */
|
||||
BStitchable int32
|
||||
|
||||
BOpencl int32 /* use OpenCL when available */
|
||||
IOpenclDevice int32 /* specify count of GPU devices to skip, for CLI users */
|
||||
OpenclDeviceId unsafe.Pointer /* pass explicit cl_device_id as void*, for API users */
|
||||
PszClbinFile *int8 /* filename (in UTF-8) of the compiled OpenCL kernel cache file */
|
||||
|
||||
/* Slicing parameters */
|
||||
iSliceMaxSize int32 /* Max size per slice in bytes; includes estimated NAL overhead. */
|
||||
iSliceMaxMbs int32 /* Max number of MBs per slice; overrides iSliceCount. */
|
||||
iSliceMinMbs int32 /* Min number of MBs per slice */
|
||||
iSliceCount int32 /* Number of slices per frame: forces rectangular slices. */
|
||||
iSliceCountMax int32 /* Absolute cap on slices per frame; stops applying slice-max-size
|
||||
* and slice-max-mbs if this is reached. */
|
||||
|
||||
ParamFree *func(arg unsafe.Pointer)
|
||||
NaluProcess *func(H []T, Nal []Nal, Opaque unsafe.Pointer)
|
||||
|
||||
Opaque unsafe.Pointer
|
||||
}
|
||||
|
||||
/****************************************************************************
|
||||
* H.264 level restriction information
|
||||
****************************************************************************/
|
||||
|
||||
type Level struct {
|
||||
LevelIdc byte
|
||||
Mbps int32 /* max macroblock processing rate (macroblocks/sec) */
|
||||
FrameSize int32 /* max frame size (macroblocks) */
|
||||
Dpb int32 /* max decoded picture buffer (mbs) */
|
||||
Bitrate int32 /* max bitrate (kbit/sec) */
|
||||
Cpb int32 /* max vbv buffer (kbit) */
|
||||
MvRange uint16 /* max vertical mv component range (pixels) */
|
||||
MvsPer2mb byte /* max mvs per 2 consecutive mbs. */
|
||||
SliceRate byte /* ?? */
|
||||
Mincr byte /* min compression ratio */
|
||||
Bipred8x8 byte /* limit bipred to >=8x8 */
|
||||
Direct8x8 byte /* limit b_direct to >=8x8 */
|
||||
FrameOnly byte /* forbid interlacing */
|
||||
}
|
||||
|
||||
type PicStruct int32
|
||||
|
||||
type Hrd struct {
|
||||
CpbInitialArrivalTime float64
|
||||
CpbFinalArrivalTime float64
|
||||
CpbRemovalTime float64
|
||||
|
||||
DpbOutputTime float64
|
||||
}
|
||||
|
||||
type SeiPayload struct {
|
||||
PayloadSize int32
|
||||
PayloadType int32
|
||||
Payload *byte
|
||||
}
|
||||
|
||||
type Sei struct {
|
||||
NumPayloads int32
|
||||
Payloads *SeiPayload
|
||||
/* In: optional callback to free each payload AND x264_sei_payload_t when used. */
|
||||
SeiFree *func(arg0 unsafe.Pointer)
|
||||
}
|
||||
|
||||
type Image struct {
|
||||
ICsp int32 /* Colorspace */
|
||||
IPlane int32 /* Number of image planes */
|
||||
IStride [4]int32 /* Strides for each plane */
|
||||
Plane [4]uintptr /* Pointers to each plane */
|
||||
}
|
||||
|
||||
type ImageProperties struct {
|
||||
/* In: an array of quantizer offsets to be applied to this image during encoding.
|
||||
* These are added on top of the decisions made by x264.
|
||||
* Offsets can be fractional; they are added before QPs are rounded to integer.
|
||||
* Adaptive quantization must be enabled to use this feature. Behavior if quant
|
||||
* offsets differ between encoding passes is undefined. */
|
||||
QuantOffsets *float32
|
||||
/* In: optional callback to free quant_offsets when used.
|
||||
* Useful if one wants to use a different quant_offset array for each frame. */
|
||||
QuantOffsetsFree *func(arg0 unsafe.Pointer)
|
||||
|
||||
/* In: optional array of flags for each macroblock.
|
||||
* Allows specifying additional information for the encoder such as which macroblocks
|
||||
* remain unchanged. Usable flags are listed below.
|
||||
* x264_param_t.analyse.b_mb_info must be set to use this, since x264 needs to track
|
||||
* extra data internally to make full use of this information.
|
||||
*
|
||||
* Out: if b_mb_info_update is set, x264 will update this array as a result of encoding.
|
||||
*
|
||||
* For "MBINFO_CONSTANT", it will remove this flag on any macroblock whose decoded
|
||||
* pixels have changed. This can be useful for e.g. noting which areas of the
|
||||
* frame need to actually be blitted. Note: this intentionally ignores the effects
|
||||
* of deblocking for the current frame, which should be fine unless one needs exact
|
||||
* pixel-perfect accuracy.
|
||||
*
|
||||
* Results for MBINFO_CONSTANT are currently only set for P-frames, and are not
|
||||
* guaranteed to enumerate all blocks which haven't changed. (There may be false
|
||||
* negatives, but no false positives.)
|
||||
*/
|
||||
MbInfo *byte
|
||||
/* In: optional callback to free mb_info when used. */
|
||||
MbInfoFree *func(arg0 unsafe.Pointer)
|
||||
|
||||
/* Out: SSIM of the the frame luma (if x264_param_t.b_ssim is set) */
|
||||
FSsim float64
|
||||
/* Out: Average PSNR of the frame (if x264_param_t.b_psnr is set) */
|
||||
FPsnrAvg float64
|
||||
/* Out: PSNR of Y, U, and V (if x264_param_t.b_psnr is set) */
|
||||
FPsnr [3]float64
|
||||
|
||||
/* Out: Average effective CRF of the encoded frame */
|
||||
FCrfAvg float64
|
||||
}
|
||||
|
||||
type Picture struct {
|
||||
/* In: force picture type (if not auto)
|
||||
* If x264 encoding parameters are violated in the forcing of picture types,
|
||||
* x264 will correct the input picture type and log a warning.
|
||||
* Out: type of the picture encoded */
|
||||
IType int32
|
||||
/* In: force quantizer for != X264_QP_AUTO */
|
||||
IQpplus1 int32
|
||||
/* In: pic_struct, for pulldown/doubling/etc...used only if b_pic_struct=1.
|
||||
* use pic_struct_e for pic_struct inputs
|
||||
* Out: pic_struct element associated with frame */
|
||||
IPicStruct int32
|
||||
/* Out: whether this frame is a keyframe. Important when using modes that result in
|
||||
* SEI recovery points being used instead of IDR frames. */
|
||||
BKeyframe int32
|
||||
/* In: user pts, Out: pts of encoded picture (user)*/
|
||||
IPts int64
|
||||
/* Out: frame dts. When the pts of the first frame is close to zero,
|
||||
* initial frames may have a negative dts which must be dealt with by any muxer */
|
||||
IDts int64
|
||||
/* In: custom encoding parameters to be set from this frame forwards
|
||||
(in coded order, not display order). If NULL, continue using
|
||||
parameters from the previous frame. Some parameters, such as
|
||||
aspect ratio, can only be changed per-GOP due to the limitations
|
||||
of H.264 itself; in this case, the caller must force an IDR frame
|
||||
if it needs the changed parameter to apply immediately. */
|
||||
Param *Param
|
||||
/* In: raw image data */
|
||||
/* Out: reconstructed image data. x264 may skip part of the reconstruction process,
|
||||
e.g. deblocking, in frames where it isn't necessary. To force complete
|
||||
reconstruction, at a small speed cost, set b_full_recon. */
|
||||
Img Image
|
||||
/* In: optional information to modify encoder decisions for this frame
|
||||
* Out: information about the encoded frame */
|
||||
Prop ImageProperties
|
||||
/* Out: HRD timing information. Output only when i_nal_hrd is set. */
|
||||
Hrdiming Hrd
|
||||
/* In: arbitrary user SEI (e.g subtitles, AFDs) */
|
||||
ExtraSei Sei
|
||||
/* private user data. copied from input to output frames. */
|
||||
Opaque unsafe.Pointer
|
||||
}
|
||||
|
||||
func (t *T) cptr() *C.x264_t { return (*C.x264_t)(unsafe.Pointer(t)) }
|
||||
|
||||
func (n *Nal) cptr() *C.x264_nal_t { return (*C.x264_nal_t)(unsafe.Pointer(n)) }
|
||||
|
||||
func (p *Param) cptr() *C.x264_param_t { return (*C.x264_param_t)(unsafe.Pointer(p)) }
|
||||
|
||||
func (p *Picture) cptr() *C.x264_picture_t { return (*C.x264_picture_t)(unsafe.Pointer(p)) }
|
||||
|
||||
// ParamDefault - fill Param with default values and do CPU detection.
|
||||
func ParamDefault(param *Param) { C.x264_param_default(param.cptr()) }
|
||||
|
||||
// ParamDefaultPreset - the same as ParamDefault, but also use the passed preset and tune to modify the default settings
|
||||
// (either can be nil, which implies no preset or no tune, respectively).
|
||||
//
|
||||
// Currently available presets are, ordered from fastest to slowest:
|
||||
// "ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow", "placebo".
|
||||
//
|
||||
// Currently available tunings are:
|
||||
// "film", "animation", "grain", "stillimage", "psnr", "ssim", "fastdecode", "zerolatency".
|
||||
//
|
||||
// Returns 0 on success, negative on failure (e.g. invalid preset/tune name).
|
||||
func ParamDefaultPreset(param *Param, preset string, tune string) int32 {
|
||||
cpreset := C.CString(preset)
|
||||
defer C.free(unsafe.Pointer(cpreset))
|
||||
ctune := C.CString(tune)
|
||||
defer C.free(unsafe.Pointer(ctune))
|
||||
return (int32)(C.x264_param_default_preset(param.cptr(), cpreset, ctune))
|
||||
}
|
||||
|
||||
// ParamApplyProfile - applies the restrictions of the given profile.
|
||||
//
|
||||
// Currently available profiles are, from most to least restrictive:
|
||||
// "baseline", "main", "high", "high10", "high422", "high444".
|
||||
// (can be nil, in which case the function will do nothing).
|
||||
//
|
||||
// Returns 0 on success, negative on failure (e.g. invalid profile name).
|
||||
func ParamApplyProfile(param *Param, profile string) int32 {
|
||||
cprofile := C.CString(profile)
|
||||
defer C.free(unsafe.Pointer(cprofile))
|
||||
return (int32)(C.x264_param_apply_profile(param.cptr(), cprofile))
|
||||
}
|
||||
|
||||
// EncoderOpen - create a new encoder handler, all parameters from Param are copied.
|
||||
func EncoderOpen(param *Param) *T {
|
||||
ret := C.x264_encoder_open(param.cptr())
|
||||
return *(**T)(unsafe.Pointer(&ret))
|
||||
}
|
||||
|
||||
// EncoderEncode - encode one picture.
|
||||
// Returns the number of bytes in the returned NALs, negative on error and zero if no NAL units returned.
|
||||
func EncoderEncode(enc *T, ppNal []*Nal, piNal *int32, picIn *Picture, picOut *Picture) int32 {
|
||||
cenc := enc.cptr()
|
||||
|
||||
cppNal := (**C.x264_nal_t)(unsafe.Pointer(&ppNal[0]))
|
||||
cpiNal := (*C.int)(unsafe.Pointer(piNal))
|
||||
|
||||
cpicIn := picIn.cptr()
|
||||
cpicOut := picOut.cptr()
|
||||
|
||||
return (int32)(C.x264_encoder_encode(cenc, cppNal, cpiNal, cpicIn, cpicOut))
|
||||
}
|
||||
|
||||
// EncoderClose closes an encoder handler.
|
||||
func EncoderClose(enc *T) { C.x264_encoder_close(enc.cptr()) }
|
||||
|
||||
// EncoderIntraRefresh - If an intra refresh is not in progress, begin one with the next P-frame.
|
||||
// If an intra refresh is in progress, begin one as soon as the current one finishes.
|
||||
// Requires that BIntraRefresh be set.
|
||||
//
|
||||
// Should not be called during an x264_encoder_encode.
|
||||
//func EncoderIntraRefresh(enc *T) { C.x264_encoder_intra_refresh(enc.cptr()) }
|
||||
|
|
@ -1,29 +1,93 @@
|
|||
package h264
|
||||
|
||||
/*
|
||||
// See: [x264](https://www.videolan.org/developers/x264.html)
|
||||
#cgo !st pkg-config: x264
|
||||
#cgo st LDFLAGS: -l:libx264.a
|
||||
|
||||
#include "stdint.h"
|
||||
#include "x264.h"
|
||||
#include <stdlib.h>
|
||||
|
||||
typedef struct
|
||||
{
|
||||
x264_t *h;
|
||||
x264_nal_t *nal; // array of NALs
|
||||
int i_nal; // number of NALs
|
||||
int y; // Y size
|
||||
int uv; // U or V size
|
||||
x264_picture_t pic;
|
||||
x264_picture_t pic_out;
|
||||
} h264;
|
||||
|
||||
h264 *h264_new(x264_param_t *param)
|
||||
{
|
||||
h264 tmp;
|
||||
x264_picture_t pic;
|
||||
|
||||
tmp.h = x264_encoder_open(param);
|
||||
if (!tmp.h)
|
||||
return NULL;
|
||||
|
||||
x264_picture_init(&pic);
|
||||
pic.img.i_csp = param->i_csp;
|
||||
pic.img.i_plane = 3;
|
||||
pic.img.i_stride[0] = param->i_width;
|
||||
pic.img.i_stride[1] = param->i_width >> 1;
|
||||
pic.img.i_stride[2] = param->i_width >> 1;
|
||||
tmp.pic = pic;
|
||||
|
||||
// crashes during x264_picture_clean :/
|
||||
//if (x264_picture_alloc(&pic, param->i_csp, param->i_width, param->i_height) < 0)
|
||||
// return NULL;
|
||||
|
||||
tmp.y = param->i_width * param->i_height;
|
||||
tmp.uv = tmp.y >> 2;
|
||||
|
||||
h264 *h = malloc(sizeof(h264));
|
||||
*h = tmp;
|
||||
return h;
|
||||
}
|
||||
|
||||
int h264_encode(h264 *h, uint8_t *yuv)
|
||||
{
|
||||
h->pic.img.plane[0] = yuv;
|
||||
h->pic.img.plane[1] = h->pic.img.plane[0] + h->y;
|
||||
h->pic.img.plane[2] = h->pic.img.plane[1] + h->uv;
|
||||
h->pic.i_pts += 1;
|
||||
return x264_encoder_encode(h->h, &h->nal, &h->i_nal, &h->pic, &h->pic_out);
|
||||
}
|
||||
|
||||
void h264_destroy(h264 *h)
|
||||
{
|
||||
if (h == NULL) return;
|
||||
x264_encoder_close(h->h);
|
||||
free(h);
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type H264 struct {
|
||||
ref *T
|
||||
|
||||
width int32
|
||||
lumaSize int32
|
||||
chromaSize int32
|
||||
csp int32
|
||||
nnals int32
|
||||
nals []*Nal
|
||||
|
||||
in, out *Picture
|
||||
h *C.h264
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Mode string
|
||||
// Constant Rate Factor (CRF)
|
||||
// This method allows the encoder to attempt to achieve a certain output quality for the whole file
|
||||
// when output file size is of less importance.
|
||||
// The range of the CRF scale is 0–51, where 0 is lossless, 23 is the default, and 51 is the worst quality possible.
|
||||
Crf uint8
|
||||
Crf uint8
|
||||
// vbv-maxrate
|
||||
MaxRate int
|
||||
// vbv-bufsize
|
||||
BufSize int
|
||||
LogLevel int32
|
||||
// ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow, placebo.
|
||||
Preset string
|
||||
|
|
@ -33,15 +97,16 @@ type Options struct {
|
|||
Tune string
|
||||
}
|
||||
|
||||
func NewEncoder(w, h int, opts *Options) (encoder *H264, err error) {
|
||||
libVersion := LibVersion()
|
||||
func NewEncoder(w, h int, th int, opts *Options) (encoder *H264, err error) {
|
||||
ver := Version()
|
||||
|
||||
if libVersion < 150 {
|
||||
return nil, fmt.Errorf("x264: the library version should be newer than v150, you have got version %v", libVersion)
|
||||
if ver < 150 {
|
||||
return nil, fmt.Errorf("x264: the library version should be newer than v150, you have got version %v", ver)
|
||||
}
|
||||
|
||||
if opts == nil {
|
||||
opts = &Options{
|
||||
Mode: "crf",
|
||||
Crf: 23,
|
||||
Tune: "zerolatency",
|
||||
Preset: "superfast",
|
||||
|
|
@ -49,94 +114,93 @@ func NewEncoder(w, h int, opts *Options) (encoder *H264, err error) {
|
|||
}
|
||||
}
|
||||
|
||||
param := Param{}
|
||||
param := C.x264_param_t{}
|
||||
|
||||
if opts.Preset != "" && opts.Tune != "" {
|
||||
if ParamDefaultPreset(¶m, opts.Preset, opts.Tune) < 0 {
|
||||
preset := C.CString(opts.Preset)
|
||||
tune := C.CString(opts.Tune)
|
||||
defer C.free(unsafe.Pointer(preset))
|
||||
defer C.free(unsafe.Pointer(tune))
|
||||
if C.x264_param_default_preset(¶m, preset, tune) < 0 {
|
||||
return nil, fmt.Errorf("x264: invalid preset/tune name")
|
||||
}
|
||||
} else {
|
||||
ParamDefault(¶m)
|
||||
C.x264_param_default(¶m)
|
||||
}
|
||||
|
||||
if opts.Profile != "" {
|
||||
if ParamApplyProfile(¶m, opts.Profile) < 0 {
|
||||
profile := C.CString(opts.Profile)
|
||||
defer C.free(unsafe.Pointer(profile))
|
||||
if C.x264_param_apply_profile(¶m, profile) < 0 {
|
||||
return nil, fmt.Errorf("x264: invalid profile name")
|
||||
}
|
||||
}
|
||||
|
||||
// legacy encoder lacks of this param
|
||||
param.IBitdepth = 8
|
||||
|
||||
if libVersion > 155 {
|
||||
param.ICsp = CspI420
|
||||
param.i_bitdepth = 8
|
||||
if ver > 155 {
|
||||
param.i_csp = C.X264_CSP_I420
|
||||
} else {
|
||||
param.ICsp = 1
|
||||
param.i_csp = 1
|
||||
}
|
||||
param.IWidth = int32(w)
|
||||
param.IHeight = int32(h)
|
||||
param.ILogLevel = opts.LogLevel
|
||||
param.ISyncLookahead = 0
|
||||
param.IThreads = 1
|
||||
|
||||
param.Rc.IRcMethod = RcCrf
|
||||
param.Rc.FRfConstant = float32(opts.Crf)
|
||||
|
||||
encoder = &H264{
|
||||
csp: param.ICsp,
|
||||
lumaSize: param.IWidth * param.IHeight,
|
||||
chromaSize: param.IWidth * param.IHeight / 4,
|
||||
nals: make([]*Nal, 1),
|
||||
width: param.IWidth,
|
||||
out: new(Picture),
|
||||
in: &Picture{
|
||||
Img: Image{
|
||||
ICsp: param.ICsp,
|
||||
IPlane: 3,
|
||||
IStride: [4]int32{
|
||||
0: param.IWidth,
|
||||
1: param.IWidth >> 1,
|
||||
2: param.IWidth >> 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
param.i_width = C.int(w)
|
||||
param.i_height = C.int(h)
|
||||
param.i_log_level = C.int(opts.LogLevel)
|
||||
param.i_keyint_max = 120
|
||||
param.i_sync_lookahead = 0
|
||||
param.i_threads = C.int(th)
|
||||
if th != 1 {
|
||||
param.b_sliced_threads = 1
|
||||
}
|
||||
|
||||
if encoder.ref = EncoderOpen(¶m); encoder.ref == nil {
|
||||
err = fmt.Errorf("x264: cannot open the encoder")
|
||||
param.rc.i_rc_method = C.X264_RC_CRF
|
||||
param.rc.f_rf_constant = C.float(opts.Crf)
|
||||
|
||||
if strings.ToLower(opts.Mode) == "cbr" {
|
||||
param.rc.i_rc_method = C.X264_RC_ABR
|
||||
param.i_nal_hrd = C.X264_NAL_HRD_CBR
|
||||
}
|
||||
return
|
||||
|
||||
if opts.MaxRate > 0 {
|
||||
param.rc.i_bitrate = C.int(opts.MaxRate)
|
||||
param.rc.i_vbv_max_bitrate = C.int(opts.MaxRate)
|
||||
}
|
||||
if opts.BufSize > 0 {
|
||||
param.rc.i_vbv_buffer_size = C.int(opts.BufSize)
|
||||
}
|
||||
|
||||
h264 := C.h264_new(¶m)
|
||||
if h264 == nil {
|
||||
return nil, fmt.Errorf("x264: cannot open the encoder")
|
||||
}
|
||||
return &H264{h264}, nil
|
||||
}
|
||||
|
||||
func LibVersion() int { return int(Build) }
|
||||
|
||||
func (e *H264) LoadBuf(yuv []byte) {
|
||||
e.in.Img.Plane[0] = uintptr(unsafe.Pointer(&yuv[0]))
|
||||
e.in.Img.Plane[1] = uintptr(unsafe.Pointer(&yuv[e.lumaSize]))
|
||||
e.in.Img.Plane[2] = uintptr(unsafe.Pointer(&yuv[e.lumaSize+e.chromaSize]))
|
||||
}
|
||||
|
||||
func (e *H264) Encode() []byte {
|
||||
e.in.IPts += 1
|
||||
if ret := EncoderEncode(e.ref, e.nals, &e.nnals, e.in, e.out); ret > 0 {
|
||||
return unsafe.Slice((*byte)(e.nals[0].PPayload), ret)
|
||||
//return C.GoBytes(e.nals[0].PPayload, C.int(ret))
|
||||
}
|
||||
return []byte{}
|
||||
func (e *H264) Encode(yuv []byte) []byte {
|
||||
bytes := C.h264_encode(e.h, (*C.uchar)(unsafe.SliceData(yuv)))
|
||||
// we merge multiple NALs stored in **nal into a single byte stream
|
||||
// ret contains the total size of NALs in bytes, i.e. each e.nal[...].p_payload * i_payload
|
||||
return unsafe.Slice((*byte)(e.h.nal.p_payload), bytes)
|
||||
}
|
||||
|
||||
func (e *H264) IntraRefresh() {
|
||||
// !to implement
|
||||
}
|
||||
|
||||
func (e *H264) Info() string { return fmt.Sprintf("x264: v%v", Version()) }
|
||||
|
||||
func (e *H264) SetFlip(b bool) {
|
||||
if b {
|
||||
e.in.Img.ICsp |= CspVflip
|
||||
(*e.h).pic.img.i_csp |= C.X264_CSP_VFLIP
|
||||
} else {
|
||||
e.in.Img.ICsp &= ^CspVflip
|
||||
(*e.h).pic.img.i_csp &= ^C.X264_CSP_VFLIP
|
||||
}
|
||||
}
|
||||
|
||||
func (e *H264) Shutdown() error {
|
||||
EncoderClose(e.ref)
|
||||
if e.h != nil {
|
||||
C.h264_destroy(e.h)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Version() int { return int(C.X264_BUILD) }
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ package h264
|
|||
import "testing"
|
||||
|
||||
func TestH264Encode(t *testing.T) {
|
||||
h264, err := NewEncoder(120, 120, nil)
|
||||
h264, err := NewEncoder(120, 120, 0, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
data := make([]byte, 120*120*1.5)
|
||||
h264.LoadBuf(data)
|
||||
h264.Encode()
|
||||
h264.Encode(data)
|
||||
if err := h264.Shutdown(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
|
@ -17,13 +17,13 @@ func TestH264Encode(t *testing.T) {
|
|||
|
||||
func Benchmark(b *testing.B) {
|
||||
w, h := 1920, 1080
|
||||
h264, err := NewEncoder(w, h, nil)
|
||||
h264, err := NewEncoder(w, h, 0, nil)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
return
|
||||
}
|
||||
data := make([]byte, int(float64(w)*float64(h)*1.5))
|
||||
for i := 0; i < b.N; i++ {
|
||||
h264.LoadBuf(data)
|
||||
h264.Encode()
|
||||
for b.Loop() {
|
||||
h264.Encode(data)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ package vpx
|
|||
#include <string.h>
|
||||
|
||||
#define VP8_FOURCC 0x30385056
|
||||
#define VP9_FOURCC 0x30395056
|
||||
|
||||
typedef struct VpxInterface {
|
||||
const char *const name;
|
||||
|
|
@ -42,7 +43,10 @@ FrameBuffer get_frame_buffer(vpx_codec_ctx_t *codec, vpx_codec_iter_t *iter) {
|
|||
return fb;
|
||||
}
|
||||
|
||||
const VpxInterface vpx_encoders[] = {{ "vp8", VP8_FOURCC, &vpx_codec_vp8_cx }};
|
||||
const VpxInterface vpx_encoders[] = {
|
||||
{ "vp8", VP8_FOURCC, &vpx_codec_vp8_cx },
|
||||
{ "vp9", VP9_FOURCC, &vpx_codec_vp9_cx },
|
||||
};
|
||||
|
||||
int vpx_img_plane_width(const vpx_image_t *img, int plane) {
|
||||
if (plane > 0 && img->x_chroma_shift > 0)
|
||||
|
|
@ -85,6 +89,7 @@ type Vpx struct {
|
|||
codecCtx C.vpx_codec_ctx_t
|
||||
kfi C.int
|
||||
flipped bool
|
||||
v int
|
||||
}
|
||||
|
||||
func (vpx *Vpx) SetFlip(b bool) { vpx.flipped = b }
|
||||
|
|
@ -96,8 +101,12 @@ type Options struct {
|
|||
KeyframeInterval uint
|
||||
}
|
||||
|
||||
func NewEncoder(w, h int, opts *Options) (*Vpx, error) {
|
||||
encoder := &C.vpx_encoders[0]
|
||||
func NewEncoder(w, h int, th int, version int, opts *Options) (*Vpx, error) {
|
||||
idx := 0
|
||||
if version == 9 {
|
||||
idx = 1
|
||||
}
|
||||
encoder := &C.vpx_encoders[idx]
|
||||
if encoder == nil {
|
||||
return nil, fmt.Errorf("couldn't get the encoder")
|
||||
}
|
||||
|
|
@ -112,6 +121,7 @@ func NewEncoder(w, h int, opts *Options) (*Vpx, error) {
|
|||
vpx := Vpx{
|
||||
frameCount: C.int(0),
|
||||
kfi: C.int(opts.KeyframeInterval),
|
||||
v: version,
|
||||
}
|
||||
|
||||
if C.vpx_img_alloc(&vpx.image, C.VPX_IMG_FMT_I420, C.uint(w), C.uint(h), 1) == nil {
|
||||
|
|
@ -125,8 +135,12 @@ func NewEncoder(w, h int, opts *Options) (*Vpx, error) {
|
|||
|
||||
cfg.g_w = C.uint(w)
|
||||
cfg.g_h = C.uint(h)
|
||||
if th != 0 {
|
||||
cfg.g_threads = C.uint(th)
|
||||
}
|
||||
cfg.g_lag_in_frames = 0
|
||||
cfg.rc_target_bitrate = C.uint(opts.Bitrate)
|
||||
cfg.g_error_resilient = 1
|
||||
cfg.g_error_resilient = C.VPX_ERROR_RESILIENT_DEFAULT
|
||||
|
||||
if C.call_vpx_codec_enc_init(&vpx.codecCtx, encoder, &cfg) != 0 {
|
||||
return nil, fmt.Errorf("failed to initialize encoder")
|
||||
|
|
@ -135,17 +149,13 @@ func NewEncoder(w, h int, opts *Options) (*Vpx, error) {
|
|||
return &vpx, nil
|
||||
}
|
||||
|
||||
func (vpx *Vpx) LoadBuf(yuv []byte) {
|
||||
// Encode encodes yuv image with the VPX8 encoder.
|
||||
// see: https://chromium.googlesource.com/webm/libvpx/+/master/examples/simple_encoder.c
|
||||
func (vpx *Vpx) Encode(yuv []byte) []byte {
|
||||
C.vpx_img_read(&vpx.image, unsafe.Pointer(&yuv[0]))
|
||||
if vpx.flipped {
|
||||
C.vpx_img_flip(&vpx.image)
|
||||
}
|
||||
}
|
||||
|
||||
// Encode encodes yuv image with the VPX8 encoder.
|
||||
// see: https://chromium.googlesource.com/webm/libvpx/+/master/examples/simple_encoder.c
|
||||
func (vpx *Vpx) Encode() []byte {
|
||||
var iter C.vpx_codec_iter_t
|
||||
|
||||
var flags C.int
|
||||
if vpx.kfi > 0 && vpx.frameCount%vpx.kfi == 0 {
|
||||
|
|
@ -156,6 +166,7 @@ func (vpx *Vpx) Encode() []byte {
|
|||
}
|
||||
vpx.frameCount++
|
||||
|
||||
var iter C.vpx_codec_iter_t
|
||||
fb := C.get_frame_buffer(&vpx.codecCtx, &iter)
|
||||
if fb.ptr == nil {
|
||||
return []byte{}
|
||||
|
|
@ -163,6 +174,10 @@ func (vpx *Vpx) Encode() []byte {
|
|||
return C.GoBytes(fb.ptr, fb.size)
|
||||
}
|
||||
|
||||
func (vpx *Vpx) Info() string {
|
||||
return fmt.Sprintf("vpx (%v): %v", vpx.v, C.GoString(C.vpx_codec_version_str()))
|
||||
}
|
||||
|
||||
func (vpx *Vpx) IntraRefresh() {
|
||||
// !to implement
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Package libyuv contains the wrapper for: https://chromium.googlesource.com/libyuv/libyuv.
|
||||
// Libs are downloaded from: https://packages.macports.org/libyuv/.
|
||||
// MacOS libs are from: https://packages.macports.org/libyuv/.
|
||||
package libyuv
|
||||
|
||||
/*
|
||||
|
|
@ -12,6 +12,7 @@ package libyuv
|
|||
#cgo darwin,arm64 LDFLAGS: -lyuv_darwin_arm64 -ljpeg -lstdc++
|
||||
|
||||
#include <stdint.h> // for uintptr_t and C99 types
|
||||
#include <stdlib.h>
|
||||
|
||||
#if !defined(LIBYUV_API)
|
||||
#define LIBYUV_API
|
||||
|
|
@ -23,6 +24,54 @@ package libyuv
|
|||
#define LIBYUV_VERSION 1874 // darwin static libs version
|
||||
#endif // INCLUDE_LIBYUV_VERSION_H_
|
||||
|
||||
// Supported rotation.
|
||||
typedef enum RotationMode {
|
||||
kRotate0 = 0, // No rotation.
|
||||
kRotate90 = 90, // Rotate 90 degrees clockwise.
|
||||
kRotate180 = 180, // Rotate 180 degrees.
|
||||
kRotate270 = 270, // Rotate 270 degrees clockwise.
|
||||
} RotationModeEnum;
|
||||
|
||||
// RGB16 (RGBP fourcc) little endian to I420.
|
||||
LIBYUV_API
|
||||
int RGB565ToI420(const uint8_t* src_rgb565, int src_stride_rgb565, uint8_t* dst_y, int dst_stride_y,
|
||||
uint8_t* dst_u, int dst_stride_u, uint8_t* dst_v, int dst_stride_v, int width, int height);
|
||||
|
||||
// Rotate I420 frame.
|
||||
LIBYUV_API
|
||||
int I420Rotate(const uint8_t* src_y, int src_stride_y, const uint8_t* src_u, int src_stride_u,
|
||||
const uint8_t* src_v, int src_stride_v, uint8_t* dst_y, int dst_stride_y, uint8_t* dst_u,
|
||||
int dst_stride_u, uint8_t* dst_v, int dst_stride_v, int width, int height, enum RotationMode mode);
|
||||
|
||||
// RGB15 (RGBO fourcc) little endian to I420.
|
||||
LIBYUV_API
|
||||
int ARGB1555ToI420(const uint8_t* src_argb1555, int src_stride_argb1555, uint8_t* dst_y, int dst_stride_y,
|
||||
uint8_t* dst_u, int dst_stride_u, uint8_t* dst_v, int dst_stride_v, int width, int height);
|
||||
|
||||
// ABGR little endian (rgba in memory) to I420.
|
||||
LIBYUV_API
|
||||
int ABGRToI420(const uint8_t* src_abgr, int src_stride_abgr, uint8_t* dst_y, int dst_stride_y, uint8_t* dst_u,
|
||||
int dst_stride_u, uint8_t* dst_v, int dst_stride_v, int width, int height);
|
||||
|
||||
// ARGB little endian (bgra in memory) to I420.
|
||||
LIBYUV_API
|
||||
int ARGBToI420(const uint8_t* src_argb, int src_stride_argb, uint8_t* dst_y, int dst_stride_y, uint8_t* dst_u,
|
||||
int dst_stride_u, uint8_t* dst_v, int dst_stride_v, int width, int height);
|
||||
|
||||
|
||||
void ConvertToI420Custom(const uint8_t* sample,
|
||||
uint8_t* dst_y,
|
||||
int dst_stride_y,
|
||||
uint8_t* dst_u,
|
||||
int dst_stride_u,
|
||||
uint8_t* dst_v,
|
||||
int dst_stride_v,
|
||||
int src_width,
|
||||
int src_height,
|
||||
int crop_width,
|
||||
int crop_height,
|
||||
uint32_t fourcc);
|
||||
|
||||
#ifdef __cplusplus
|
||||
namespace libyuv {
|
||||
extern "C" {
|
||||
|
|
@ -35,61 +84,100 @@ enum FourCC {
|
|||
FOURCC_I420 = FOURCC('I', '4', '2', '0'),
|
||||
FOURCC_ARGB = FOURCC('A', 'R', 'G', 'B'),
|
||||
FOURCC_ABGR = FOURCC('A', 'B', 'G', 'R'),
|
||||
FOURCC_RGBO = FOURCC('R', 'G', 'B', 'O'),
|
||||
FOURCC_RGBP = FOURCC('R', 'G', 'B', 'P'), // rgb565 LE.
|
||||
FOURCC_ANY = -1,
|
||||
};
|
||||
|
||||
typedef enum RotationMode {
|
||||
kRotate0 = 0, // No rotation.
|
||||
kRotate90 = 90, // Rotate 90 degrees clockwise.
|
||||
kRotate180 = 180, // Rotate 180 degrees.
|
||||
kRotate270 = 270, // Rotate 270 degrees clockwise.
|
||||
} RotationModeEnum;
|
||||
inline void ConvertToI420Custom(const uint8_t* sample,
|
||||
uint8_t* dst_y,
|
||||
int dst_stride_y,
|
||||
uint8_t* dst_u,
|
||||
int dst_stride_u,
|
||||
uint8_t* dst_v,
|
||||
int dst_stride_v,
|
||||
int src_width,
|
||||
int src_height,
|
||||
int crop_width,
|
||||
int crop_height,
|
||||
uint32_t fourcc) {
|
||||
const int stride = src_width << 1;
|
||||
|
||||
LIBYUV_API
|
||||
int ConvertToI420(const uint8_t* sample,
|
||||
size_t sample_size,
|
||||
uint8_t* dst_y,
|
||||
int dst_stride_y,
|
||||
uint8_t* dst_u,
|
||||
int dst_stride_u,
|
||||
uint8_t* dst_v,
|
||||
int dst_stride_v,
|
||||
int crop_x,
|
||||
int crop_y,
|
||||
int src_width,
|
||||
int src_height,
|
||||
int crop_width,
|
||||
int crop_height,
|
||||
enum RotationMode rotation,
|
||||
uint32_t fourcc);
|
||||
switch (fourcc) {
|
||||
case FOURCC_RGBP:
|
||||
RGB565ToI420(sample, stride, dst_y, dst_stride_y, dst_u,
|
||||
dst_stride_u, dst_v, dst_stride_v, crop_width, crop_height);
|
||||
break;
|
||||
case FOURCC_RGBO:
|
||||
ARGB1555ToI420(sample, stride, dst_y, dst_stride_y, dst_u,
|
||||
dst_stride_u, dst_v, dst_stride_v, crop_width, crop_height);
|
||||
break;
|
||||
case FOURCC_ARGB:
|
||||
ARGBToI420(sample, stride << 1, dst_y, dst_stride_y, dst_u,
|
||||
dst_stride_u, dst_v, dst_stride_v, crop_width, crop_height);
|
||||
break;
|
||||
case FOURCC_ABGR:
|
||||
ABGRToI420(sample, stride << 1, dst_y, dst_stride_y, dst_u,
|
||||
dst_stride_u, dst_v, dst_stride_v, crop_width, crop_height);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void rotateI420(const uint8_t* sample,
|
||||
uint8_t* dst_y,
|
||||
int dst_stride_y,
|
||||
uint8_t* dst_u,
|
||||
int dst_stride_u,
|
||||
uint8_t* dst_v,
|
||||
int dst_stride_v,
|
||||
int src_width,
|
||||
int src_height,
|
||||
int crop_width,
|
||||
int crop_height,
|
||||
enum RotationMode rotation,
|
||||
uint32_t fourcc) {
|
||||
|
||||
uint8_t* tmp_y = dst_y;
|
||||
uint8_t* tmp_u = dst_u;
|
||||
uint8_t* tmp_v = dst_v;
|
||||
int tmp_y_stride = dst_stride_y;
|
||||
int tmp_u_stride = dst_stride_u;
|
||||
int tmp_v_stride = dst_stride_v;
|
||||
|
||||
uint8_t* rotate_buffer = NULL;
|
||||
|
||||
int y_size = crop_width * crop_height;
|
||||
int uv_size = y_size >> 1;
|
||||
rotate_buffer = (uint8_t*)malloc(y_size + y_size);
|
||||
if (!rotate_buffer) {
|
||||
return;
|
||||
}
|
||||
dst_y = rotate_buffer;
|
||||
dst_u = dst_y + y_size;
|
||||
dst_v = dst_u + uv_size;
|
||||
dst_stride_y = crop_width;
|
||||
dst_stride_u = dst_stride_v = crop_width >> 1;
|
||||
ConvertToI420Custom(sample, dst_y, dst_stride_y, dst_u, dst_stride_u, dst_v, dst_stride_v,
|
||||
src_width, src_height, crop_width, crop_height, fourcc);
|
||||
I420Rotate(dst_y, dst_stride_y, dst_u, dst_stride_u, dst_v,
|
||||
dst_stride_v, tmp_y, tmp_y_stride, tmp_u, tmp_u_stride,
|
||||
tmp_v, tmp_v_stride, crop_width, crop_height, rotation);
|
||||
free(rotate_buffer);
|
||||
}
|
||||
|
||||
// Supported filtering.
|
||||
typedef enum FilterMode {
|
||||
kFilterNone = 0, // Point sample; Fastest.
|
||||
kFilterLinear = 1, // Filter horizontally only.
|
||||
kFilterBilinear = 2, // Faster than box, but lower quality scaling down.
|
||||
kFilterBox = 3 // Highest quality.
|
||||
kFilterNone = 0, // Point sample; Fastest.
|
||||
kFilterLinear = 1, // Filter horizontally only.
|
||||
kFilterBilinear = 2, // Faster than box, but lower quality scaling down.
|
||||
kFilterBox = 3 // Highest quality.
|
||||
} FilterModeEnum;
|
||||
|
||||
LIBYUV_API
|
||||
int I420Scale(const uint8_t *src_y,
|
||||
int src_stride_y,
|
||||
const uint8_t *src_u,
|
||||
int src_stride_u,
|
||||
const uint8_t *src_v,
|
||||
int src_stride_v,
|
||||
int src_width,
|
||||
int src_height,
|
||||
uint8_t *dst_y,
|
||||
int dst_stride_y,
|
||||
uint8_t *dst_u,
|
||||
int dst_stride_u,
|
||||
uint8_t *dst_v,
|
||||
int dst_stride_v,
|
||||
int dst_width,
|
||||
int dst_height,
|
||||
enum FilterMode filtering);
|
||||
int I420Scale(const uint8_t *src_y, int src_stride_y, const uint8_t *src_u, int src_stride_u,
|
||||
const uint8_t *src_v, int src_stride_v, int src_width, int src_height, uint8_t *dst_y,
|
||||
int dst_stride_y, uint8_t *dst_u, int dst_stride_u, uint8_t *dst_v, int dst_stride_v,
|
||||
int dst_width, int dst_height, enum FilterMode filtering);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
|
|
@ -102,6 +190,7 @@ import "fmt"
|
|||
const FourccRgbp uint32 = C.FOURCC_RGBP
|
||||
const FourccArgb uint32 = C.FOURCC_ARGB
|
||||
const FourccAbgr uint32 = C.FOURCC_ABGR
|
||||
const FourccRgb0 uint32 = C.FOURCC_RGBO
|
||||
|
||||
func Y420(src []byte, dst []byte, _, h, stride int, dw, dh int, rot uint, pix uint32, cx, cy int) {
|
||||
cw := (dw + 1) / 2
|
||||
|
|
@ -111,23 +200,36 @@ func Y420(src []byte, dst []byte, _, h, stride int, dw, dh int, rot uint, pix ui
|
|||
yStride := dw
|
||||
cStride := cw
|
||||
|
||||
C.ConvertToI420(
|
||||
(*C.uchar)(&src[0]),
|
||||
C.size_t(0),
|
||||
(*C.uchar)(&dst[0]),
|
||||
C.int(yStride),
|
||||
(*C.uchar)(&dst[i0]),
|
||||
C.int(cStride),
|
||||
(*C.uchar)(&dst[i1]),
|
||||
C.int(cStride),
|
||||
C.int(0),
|
||||
C.int(0),
|
||||
C.int(stride),
|
||||
C.int(h),
|
||||
C.int(cx),
|
||||
C.int(cy),
|
||||
C.enum_RotationMode(rot),
|
||||
C.uint32_t(pix))
|
||||
if rot == 0 {
|
||||
C.ConvertToI420Custom(
|
||||
(*C.uchar)(&src[0]),
|
||||
(*C.uchar)(&dst[0]),
|
||||
C.int(yStride),
|
||||
(*C.uchar)(&dst[i0]),
|
||||
C.int(cStride),
|
||||
(*C.uchar)(&dst[i1]),
|
||||
C.int(cStride),
|
||||
C.int(stride),
|
||||
C.int(h),
|
||||
C.int(cx),
|
||||
C.int(cy),
|
||||
C.uint32_t(pix))
|
||||
} else {
|
||||
C.rotateI420(
|
||||
(*C.uchar)(&src[0]),
|
||||
(*C.uchar)(&dst[0]),
|
||||
C.int(yStride),
|
||||
(*C.uchar)(&dst[i0]),
|
||||
C.int(cStride),
|
||||
(*C.uchar)(&dst[i1]),
|
||||
C.int(cStride),
|
||||
C.int(stride),
|
||||
C.int(h),
|
||||
C.int(cx),
|
||||
C.int(cy),
|
||||
C.enum_RotationMode(rot),
|
||||
C.uint32_t(pix))
|
||||
}
|
||||
}
|
||||
|
||||
func Y420Scale(src []byte, dst []byte, w, h int, dw, dh int) {
|
||||
|
|
|
|||
|
|
@ -2,16 +2,16 @@ package yuv
|
|||
|
||||
import (
|
||||
"image"
|
||||
"sync"
|
||||
|
||||
"github.com/giongto35/cloud-game/v3/pkg/encoder/yuv/libyuv"
|
||||
)
|
||||
|
||||
type Conv struct {
|
||||
w, h int
|
||||
sw, sh int
|
||||
scale float64
|
||||
pool sync.Pool
|
||||
w, h int
|
||||
sw, sh int
|
||||
scale float64
|
||||
frame []byte
|
||||
frameSc []byte
|
||||
}
|
||||
|
||||
type RawFrame struct {
|
||||
|
|
@ -25,45 +25,55 @@ type PixFmt uint32
|
|||
const FourccRgbp = libyuv.FourccRgbp
|
||||
const FourccArgb = libyuv.FourccArgb
|
||||
const FourccAbgr = libyuv.FourccAbgr
|
||||
const FourccRgb0 = libyuv.FourccRgb0
|
||||
|
||||
func NewYuvConv(w, h int, scale float64) Conv {
|
||||
if scale < 1 {
|
||||
scale = 1
|
||||
}
|
||||
|
||||
sw, sh := round(w, scale), round(h, scale)
|
||||
bufSize := int(float64(sw) * float64(sh) * 1.5)
|
||||
return Conv{
|
||||
w: w, h: h, sw: sw, sh: sh, scale: scale,
|
||||
pool: sync.Pool{New: func() any { b := make([]byte, bufSize); return &b }},
|
||||
conv := Conv{w: w, h: h, sw: sw, sh: sh, scale: scale}
|
||||
bufSize := int(float64(w) * float64(h) * 1.5)
|
||||
|
||||
if scale == 1 {
|
||||
conv.frame = make([]byte, bufSize)
|
||||
} else {
|
||||
bufSizeSc := int(float64(sw) * float64(sh) * 1.5)
|
||||
// [original frame][scaled frame ]
|
||||
frames := make([]byte, bufSize+bufSizeSc)
|
||||
conv.frame = frames[:bufSize]
|
||||
conv.frameSc = frames[bufSize:]
|
||||
}
|
||||
|
||||
return conv
|
||||
}
|
||||
|
||||
// Process converts an image to YUV I420 format inside the internal buffer.
|
||||
func (c *Conv) Process(frame RawFrame, rot uint, pf PixFmt) []byte {
|
||||
dx, dy := c.w, c.h // dest
|
||||
cx, cy := c.w, c.h // crop
|
||||
if rot == 90 || rot == 270 {
|
||||
cx, cy = cy, cx
|
||||
}
|
||||
|
||||
stride := frame.Stride >> 2
|
||||
if pf == PixFmt(libyuv.FourccRgbp) {
|
||||
var stride int
|
||||
switch pf {
|
||||
case PixFmt(libyuv.FourccRgbp), PixFmt(libyuv.FourccRgb0):
|
||||
stride = frame.Stride >> 1
|
||||
default:
|
||||
stride = frame.Stride >> 2
|
||||
}
|
||||
|
||||
buf := *c.pool.Get().(*[]byte)
|
||||
libyuv.Y420(frame.Data, buf, frame.W, frame.H, stride, dx, dy, rot, uint32(pf), cx, cy)
|
||||
libyuv.Y420(frame.Data, c.frame, frame.W, frame.H, stride, c.w, c.h, rot, uint32(pf), cx, cy)
|
||||
|
||||
if c.scale > 1 {
|
||||
dstBuf := *c.pool.Get().(*[]byte)
|
||||
libyuv.Y420Scale(buf, dstBuf, dx, dy, c.sw, c.sh)
|
||||
c.pool.Put(&buf)
|
||||
return dstBuf
|
||||
libyuv.Y420Scale(c.frame, c.frameSc, c.w, c.h, c.sw, c.sh)
|
||||
return c.frameSc
|
||||
}
|
||||
return buf
|
||||
|
||||
return c.frame
|
||||
}
|
||||
|
||||
func (c *Conv) Put(x *[]byte) { c.pool.Put(x) }
|
||||
func (c *Conv) Version() string { return libyuv.Version() }
|
||||
func round(x int, scale float64) int { return (int(float64(x)*scale) + 1) & ^1 }
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -2,7 +2,7 @@ package games
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"math/rand/v2"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
|
@ -14,6 +14,7 @@ type Launcher interface {
|
|||
}
|
||||
|
||||
type AppMeta struct {
|
||||
Alias string
|
||||
Base string
|
||||
Name string
|
||||
Path string
|
||||
|
|
@ -39,7 +40,7 @@ func (gl GameLauncher) ExtractAppNameFromUrl(name string) string { return Extrac
|
|||
|
||||
func (gl GameLauncher) GetAppNames() (apps []AppMeta) {
|
||||
for _, game := range gl.lib.GetAll() {
|
||||
apps = append(apps, AppMeta{Name: game.Name, System: game.System})
|
||||
apps = append(apps, AppMeta{Alias: game.Alias, Name: game.Name, System: game.System})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -59,5 +60,5 @@ func ExtractGame(roomID string) string {
|
|||
// RoomID contains random number + gameName
|
||||
// Next time when we only get roomID, we can launch game based on gameName
|
||||
func GenerateRoomID(title string) string {
|
||||
return strconv.FormatInt(rand.Int63(), 16) + separator + title
|
||||
return strconv.FormatInt(rand.Int64(), 16) + separator + title
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
package games
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
|
@ -15,11 +18,13 @@ import (
|
|||
|
||||
// libConf is an optimized internal library configuration
|
||||
type libConf struct {
|
||||
path string
|
||||
supported map[string]struct{}
|
||||
ignored map[string]struct{}
|
||||
verbose bool
|
||||
watchMode bool
|
||||
aliasFile string
|
||||
path string
|
||||
supported map[string]struct{}
|
||||
ignored []string
|
||||
verbose bool
|
||||
watchMode bool
|
||||
sessionPath string
|
||||
}
|
||||
|
||||
type library struct {
|
||||
|
|
@ -35,6 +40,9 @@ type library struct {
|
|||
games map[string]GameMetadata
|
||||
log *logger.Logger
|
||||
|
||||
// ids of saved games to find closed sessions
|
||||
sessions []string
|
||||
|
||||
emuConf WithEmulatorInfo
|
||||
|
||||
// to restrict parallel execution or throttling
|
||||
|
|
@ -47,15 +55,18 @@ type library struct {
|
|||
type GameLibrary interface {
|
||||
GetAll() []GameMetadata
|
||||
FindGameByName(name string) GameMetadata
|
||||
Sessions() []string
|
||||
Scan()
|
||||
}
|
||||
|
||||
type WithEmulatorInfo interface {
|
||||
GetSupportedExtensions() []string
|
||||
GetEmulator(rom string, path string) string
|
||||
SessionStoragePath() string
|
||||
}
|
||||
|
||||
type GameMetadata struct {
|
||||
Alias string
|
||||
Base string
|
||||
Name string // the display name of the game
|
||||
Path string // the game path relative to the library base path
|
||||
|
|
@ -84,11 +95,13 @@ func NewLib(conf config.Library, emu WithEmulatorInfo, log *logger.Logger) GameL
|
|||
|
||||
library := &library{
|
||||
config: libConf{
|
||||
path: dir,
|
||||
supported: toMap(conf.Supported),
|
||||
ignored: toMap(conf.Ignored),
|
||||
verbose: conf.Verbose,
|
||||
watchMode: conf.WatchMode,
|
||||
aliasFile: conf.AliasFile,
|
||||
path: dir,
|
||||
supported: toMap(conf.Supported),
|
||||
ignored: conf.Ignored,
|
||||
verbose: conf.Verbose,
|
||||
watchMode: conf.WatchMode,
|
||||
sessionPath: emu.SessionStoragePath(),
|
||||
},
|
||||
mu: sync.Mutex{},
|
||||
games: map[string]GameMetadata{},
|
||||
|
|
@ -104,6 +117,10 @@ func NewLib(conf config.Library, emu WithEmulatorInfo, log *logger.Logger) GameL
|
|||
return library
|
||||
}
|
||||
|
||||
func (lib *library) Sessions() []string {
|
||||
return lib.sessions
|
||||
}
|
||||
|
||||
func (lib *library) GetAll() []GameMetadata {
|
||||
var res []GameMetadata
|
||||
for _, value := range lib.games {
|
||||
|
|
@ -122,6 +139,39 @@ func (lib *library) FindGameByName(name string) GameMetadata {
|
|||
return game
|
||||
}
|
||||
|
||||
func (lib *library) AliasFileMaybe() map[string]string {
|
||||
if lib.config.aliasFile == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
path := filepath.Join(lib.config.path, lib.config.aliasFile)
|
||||
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
lib.log.Error().Msgf("couldn't open alias file, %v", err)
|
||||
return nil
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
aliases := make(map[string]string)
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
if id, alias, found := strings.Cut(scanner.Text(), "="); found {
|
||||
aliases[id] = alias
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
lib.log.Error().Msgf("alias file read error, %v", err)
|
||||
}
|
||||
|
||||
return aliases
|
||||
}
|
||||
|
||||
func (lib *library) Scan() {
|
||||
if !lib.hasSource {
|
||||
lib.log.Info().Msg("Lib scan... skipped (no source)")
|
||||
|
|
@ -141,6 +191,14 @@ func (lib *library) Scan() {
|
|||
|
||||
lib.log.Debug().Msg("Lib scan... started")
|
||||
|
||||
// game name aliases
|
||||
aliases := lib.AliasFileMaybe()
|
||||
|
||||
if aliases != nil {
|
||||
lib.log.Debug().Msgf("Lib game alises found")
|
||||
lib.log.Debug().Msgf(">>> %v", aliases)
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
var games []GameMetadata
|
||||
dir := lib.config.path
|
||||
|
|
@ -149,15 +207,36 @@ func (lib *library) Scan() {
|
|||
return err
|
||||
}
|
||||
|
||||
if info != nil && !info.IsDir() && lib.isExtAllowed(path) {
|
||||
meta := getMetadata(path, dir)
|
||||
if info == nil || info.IsDir() || !lib.isExtAllowed(path) {
|
||||
return nil
|
||||
}
|
||||
|
||||
meta.System = lib.emuConf.GetEmulator(meta.Type, meta.Path)
|
||||
meta := metadata(path, dir)
|
||||
meta.System = lib.emuConf.GetEmulator(meta.Type, meta.Path)
|
||||
|
||||
if _, ok := lib.config.ignored[meta.Name]; !ok {
|
||||
games = append(games, meta)
|
||||
if aliases != nil {
|
||||
if k, ok := aliases[meta.Name]; ok {
|
||||
meta.Alias = k
|
||||
}
|
||||
}
|
||||
|
||||
ignored := false
|
||||
for _, k := range lib.config.ignored {
|
||||
if meta.Name == k {
|
||||
ignored = true
|
||||
break
|
||||
}
|
||||
|
||||
if len(k) > 0 && k[0] == '.' && strings.Contains(meta.Name, k) {
|
||||
ignored = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !ignored {
|
||||
games = append(games, meta)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
|
|
@ -170,6 +249,20 @@ func (lib *library) Scan() {
|
|||
lib.set(games)
|
||||
}
|
||||
|
||||
var sessions []string
|
||||
dir = lib.config.sessionPath
|
||||
err = filepath.WalkDir(dir, func(path string, info fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info != nil && !info.IsDir() {
|
||||
sessions = append(sessions, info.Name())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
lib.sessions = sessions
|
||||
|
||||
lib.lastScanDuration = time.Since(start)
|
||||
if lib.config.verbose {
|
||||
lib.dumpLibrary()
|
||||
|
|
@ -235,7 +328,7 @@ func (lib *library) set(games []GameMetadata) {
|
|||
}
|
||||
|
||||
func (lib *library) isExtAllowed(path string) bool {
|
||||
ext := filepath.Ext(path)
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
if ext == "" {
|
||||
return false
|
||||
}
|
||||
|
|
@ -243,15 +336,15 @@ func (lib *library) isExtAllowed(path string) bool {
|
|||
return ok
|
||||
}
|
||||
|
||||
// getMetadata returns game info from a path
|
||||
func getMetadata(path string, basePath string) GameMetadata {
|
||||
// metadata returns game info from a path
|
||||
func metadata(path string, basePath string) GameMetadata {
|
||||
name := filepath.Base(path)
|
||||
ext := filepath.Ext(name)
|
||||
relPath, _ := filepath.Rel(basePath, path)
|
||||
|
||||
return GameMetadata{
|
||||
Name: strings.TrimSuffix(name, ext),
|
||||
Type: ext[1:],
|
||||
Type: strings.ToLower(ext[1:]),
|
||||
Path: relPath,
|
||||
}
|
||||
}
|
||||
|
|
@ -259,8 +352,21 @@ func getMetadata(path string, basePath string) GameMetadata {
|
|||
// dumpLibrary printouts the current library snapshot of games
|
||||
func (lib *library) dumpLibrary() {
|
||||
var gameList strings.Builder
|
||||
for _, game := range lib.games {
|
||||
gameList.WriteString(fmt.Sprintf(" %5s %s (%s)\n", game.System, game.Name, game.Path))
|
||||
|
||||
// oof
|
||||
keys := make([]string, 0, len(lib.games))
|
||||
for k := range lib.games {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, k := range keys {
|
||||
game := lib.games[k]
|
||||
alias := game.Alias
|
||||
if alias != "" {
|
||||
alias = fmt.Sprintf("[%s] ", game.Alias)
|
||||
}
|
||||
gameList.WriteString(fmt.Sprintf(" %7s %s %s(%s)\n", game.System, game.Name, alias, game.Path))
|
||||
}
|
||||
|
||||
lib.log.Debug().Msgf("Lib dump\n"+
|
||||
|
|
@ -269,9 +375,9 @@ func (lib *library) dumpLibrary() {
|
|||
"--------------------------------------------\n"+
|
||||
"%v"+
|
||||
"--------------------------------------------\n"+
|
||||
"--- ROMs: %03d %26s ---\n"+
|
||||
"--- ROMs: %03d --- Saves: %04d %10s ---\n"+
|
||||
"--------------------------------------------",
|
||||
gameList.String(), len(lib.games), lib.lastScanDuration)
|
||||
gameList.String(), len(lib.games), len(lib.sessions), lib.lastScanDuration)
|
||||
}
|
||||
|
||||
func toMap(list []string) map[string]struct{} {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
package games
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/giongto35/cloud-game/v3/pkg/config"
|
||||
|
|
@ -60,6 +63,52 @@ func TestLibraryScan(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAliasFileMaybe(t *testing.T) {
|
||||
lib := &library{
|
||||
config: libConf{
|
||||
aliasFile: "alias",
|
||||
path: os.TempDir(),
|
||||
},
|
||||
log: logger.NewConsole(false, "w", false),
|
||||
}
|
||||
|
||||
contents := "a=b\nc=d\n"
|
||||
|
||||
path := filepath.Join(lib.config.path, lib.config.aliasFile)
|
||||
if err := os.WriteFile(path, []byte(contents), 0644); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(path); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
want := map[string]string{}
|
||||
want["a"] = "b"
|
||||
want["c"] = "d"
|
||||
|
||||
aliases := lib.AliasFileMaybe()
|
||||
|
||||
if !reflect.DeepEqual(aliases, want) {
|
||||
t.Errorf("AliasFileMaybe() = %v, want %v", aliases, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAliasFileMaybeNot(t *testing.T) {
|
||||
lib := &library{
|
||||
config: libConf{
|
||||
path: os.TempDir(),
|
||||
},
|
||||
log: logger.NewConsole(false, "w", false),
|
||||
}
|
||||
|
||||
aliases := lib.AliasFileMaybe()
|
||||
if aliases != nil {
|
||||
t.Errorf("should be nil, but %v", aliases)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark(b *testing.B) {
|
||||
log := logger.Default()
|
||||
logger.SetGlobalLevel(logger.Disabled)
|
||||
|
|
@ -68,7 +117,7 @@ func Benchmark(b *testing.B) {
|
|||
Supported: []string{"gba", "zip", "nes"},
|
||||
}, config.Emulator{}, log)
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
library.Scan()
|
||||
_ = library.GetAll()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package network
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
|
@ -12,15 +13,17 @@ func (a *Address) Port() (int, error) {
|
|||
if len(string(*a)) == 0 {
|
||||
return 0, errors.New("no address")
|
||||
}
|
||||
parts := strings.Split(string(*a), ":")
|
||||
var port string
|
||||
if len(parts) == 1 {
|
||||
port = parts[0]
|
||||
} else {
|
||||
port = parts[len(parts)-1]
|
||||
addr := replaceAllExceptLast(string(*a), ":", "_")
|
||||
_, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if val, err := strconv.Atoi(port); err == nil {
|
||||
return val, nil
|
||||
}
|
||||
return 0, errors.New("port is not a number")
|
||||
}
|
||||
|
||||
func replaceAllExceptLast(s, c, x string) string {
|
||||
return strings.Replace(s, c, x, strings.Count(s, c)-1)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,12 +120,12 @@ func (s *Server) run() {
|
|||
s.log.Debug().Msgf("Starting %s server on %s", protocol, s.Addr)
|
||||
|
||||
if s.opts.Https && s.opts.HttpsRedirect {
|
||||
rdr, err := s.redirection()
|
||||
if err != nil {
|
||||
if rdr, err := s.redirection(); err == nil {
|
||||
s.redirect = rdr
|
||||
go s.redirect.Run()
|
||||
} else {
|
||||
s.log.Error().Err(err).Msg("couldn't init redirection server")
|
||||
}
|
||||
s.redirect = rdr
|
||||
go s.redirect.Run()
|
||||
}
|
||||
|
||||
var err error
|
||||
|
|
@ -165,6 +165,7 @@ func (s *Server) redirection() (*Server, error) {
|
|||
address = s.opts.HttpsDomain
|
||||
}
|
||||
addr := buildAddress(address, s.opts.Zone, *s.listener)
|
||||
s.log.Info().Str("addr", addr).Msg("Start HTTPS redirect server")
|
||||
|
||||
srv, err := NewServer(s.opts.HttpsRedirectAddress, func(serv *Server) Handler {
|
||||
h := NewServeMux("")
|
||||
|
|
@ -186,7 +187,6 @@ func (s *Server) redirection() (*Server, error) {
|
|||
},
|
||||
WithLogger(s.log),
|
||||
)
|
||||
s.log.Info().Str("addr", addr).Msg("Start HTTPS redirect server")
|
||||
return srv, err
|
||||
}
|
||||
|
||||
|
|
|
|||
19
pkg/network/retry.go
Normal file
19
pkg/network/retry.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package network
|
||||
|
||||
import "time"
|
||||
|
||||
const retry = 10 * time.Second
|
||||
|
||||
type Retry struct {
|
||||
t time.Duration
|
||||
fail bool
|
||||
}
|
||||
|
||||
func NewRetry() Retry {
|
||||
return Retry{t: retry}
|
||||
}
|
||||
|
||||
func (r *Retry) Fail() *Retry { r.fail = true; time.Sleep(r.t); return r }
|
||||
func (r *Retry) Multiply(x int) { r.t *= time.Duration(x) }
|
||||
func (r *Retry) Success() { r.t = retry; r.fail = false }
|
||||
func (r *Retry) Time() time.Duration { return r.t }
|
||||
|
|
@ -7,7 +7,7 @@ import (
|
|||
"github.com/giongto35/cloud-game/v3/pkg/config"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/logger"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/network/socket"
|
||||
"github.com/pion/ice/v3"
|
||||
"github.com/pion/ice/v4"
|
||||
"github.com/pion/interceptor"
|
||||
"github.com/pion/interceptor/pkg/report"
|
||||
"github.com/pion/webrtc/v4"
|
||||
|
|
@ -74,6 +74,7 @@ func NewApiFactory(conf config.Webrtc, log *logger.Logger, mod ModApiFun) (api *
|
|||
}
|
||||
|
||||
s.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
|
||||
s.EnableSCTPZeroChecksum(true)
|
||||
|
||||
if mod != nil {
|
||||
mod(m, i, &s)
|
||||
|
|
|
|||
|
|
@ -32,48 +32,72 @@ func (p *Peer) NewCall(vCodec, aCodec string, onICECandidate func(ice any)) (sdp
|
|||
if p.conn != nil && p.conn.ConnectionState() == webrtc.PeerConnectionStateConnected {
|
||||
return
|
||||
}
|
||||
p.log.Info().Msg("WebRTC start")
|
||||
p.log.Debug().Msg("WebRTC start")
|
||||
if p.conn, err = p.api.NewPeer(); err != nil {
|
||||
return "", err
|
||||
return
|
||||
}
|
||||
p.conn.OnICECandidate(p.handleICECandidate(onICECandidate))
|
||||
// plug in the [video] track (out)
|
||||
video, err := newTrack("video", "game-video", vCodec)
|
||||
video, err := newTrack("video", "video", vCodec)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err = p.conn.AddTrack(video); err != nil {
|
||||
vs, err := p.conn.AddTrack(video)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Read incoming RTCP packets
|
||||
go func() {
|
||||
rtcpBuf := make([]byte, 1500)
|
||||
for {
|
||||
_, _, rtcpErr := vs.Read(rtcpBuf)
|
||||
if rtcpErr != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
p.v = video
|
||||
p.log.Debug().Msgf("Added [%s] track", video.Codec().MimeType)
|
||||
|
||||
// plug in the [audio] track (out)
|
||||
audio, err := newTrack("audio", "game-audio", aCodec)
|
||||
audio, err := newTrack("audio", "audio", aCodec)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err = p.conn.AddTrack(audio); err != nil {
|
||||
as, err := p.conn.AddTrack(audio)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Read incoming RTCP packets
|
||||
go func() {
|
||||
rtcpBuf := make([]byte, 1500)
|
||||
for {
|
||||
_, _, rtcpErr := as.Read(rtcpBuf)
|
||||
if rtcpErr != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
p.log.Debug().Msgf("Added [%s] track", audio.Codec().MimeType)
|
||||
p.a = audio
|
||||
|
||||
// plug in the [input] data channel (in)
|
||||
if err = p.addInputChannel("game-input"); err != nil {
|
||||
err = p.AddChannel("data", func(data []byte) {
|
||||
if len(data) == 0 || p.OnMessage == nil {
|
||||
return
|
||||
}
|
||||
p.OnMessage(data)
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
p.log.Debug().Msg("Added [input/bytes] chan")
|
||||
|
||||
p.conn.OnICEConnectionStateChange(p.handleICEState(func() {
|
||||
p.log.Info().Msg("Start streaming")
|
||||
}))
|
||||
p.conn.OnICEConnectionStateChange(p.handleICEState(func() { p.log.Info().Msg("Connected") }))
|
||||
// Stream provider supposes to send offer
|
||||
offer, err := p.conn.CreateOffer(nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
p.log.Info().Msg("Created Offer")
|
||||
p.log.Debug().Msg("Created Offer")
|
||||
|
||||
err = p.conn.SetLocalDescription(offer)
|
||||
if err != nil {
|
||||
|
|
@ -140,6 +164,8 @@ func newTrack(id string, label string, codec string) (*webrtc.TrackLocalStaticSa
|
|||
mime = webrtc.MimeTypeH264
|
||||
case "vpx", "vp8":
|
||||
mime = webrtc.MimeTypeVP8
|
||||
case "vp9":
|
||||
mime = webrtc.MimeTypeVP9
|
||||
}
|
||||
}
|
||||
if mime == "" {
|
||||
|
|
@ -199,6 +225,19 @@ func (p *Peer) AddCandidate(candidate string, decoder Decoder) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *Peer) AddChannel(label string, onMessage func([]byte)) error {
|
||||
ch, err := p.addDataChannel(label)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if label == "data" {
|
||||
p.d = ch
|
||||
}
|
||||
ch.OnMessage(func(m webrtc.DataChannelMessage) { onMessage(m.Data) })
|
||||
p.log.Debug().Msgf("Added [%v] chan", label)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Peer) Disconnect() {
|
||||
if p.conn == nil {
|
||||
return
|
||||
|
|
@ -210,28 +249,19 @@ func (p *Peer) Disconnect() {
|
|||
p.log.Debug().Msg("WebRTC stop")
|
||||
}
|
||||
|
||||
// addInputChannel creates a new WebRTC data channel for user input.
|
||||
// addDataChannel creates new WebRTC data channel.
|
||||
// Default params -- ordered: true, negotiated: false.
|
||||
func (p *Peer) addInputChannel(label string) error {
|
||||
func (p *Peer) addDataChannel(label string) (*webrtc.DataChannel, error) {
|
||||
ch, err := p.conn.CreateDataChannel(label, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
ch.OnOpen(func() {
|
||||
p.log.Debug().Str("label", ch.Label()).Uint16("id", *ch.ID()).Msg("Data channel [input] opened")
|
||||
p.log.Debug().Uint16("id", *ch.ID()).Msgf("Data channel [%v] opened", ch.Label())
|
||||
})
|
||||
ch.OnError(p.logx)
|
||||
ch.OnMessage(func(m webrtc.DataChannelMessage) {
|
||||
if len(m.Data) == 0 {
|
||||
return
|
||||
}
|
||||
if p.OnMessage != nil {
|
||||
p.OnMessage(m.Data)
|
||||
}
|
||||
})
|
||||
p.d = ch
|
||||
ch.OnClose(func() { p.log.Debug().Msg("Data channel [input] has been closed") })
|
||||
return nil
|
||||
ch.OnClose(func() { p.log.Debug().Msgf("Data channel [%v] has been closed", ch.Label()) })
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
func (p *Peer) logx(err error) { p.log.Error().Err(err) }
|
||||
|
|
|
|||
|
|
@ -27,13 +27,15 @@ type Server struct {
|
|||
}
|
||||
|
||||
type Connection struct {
|
||||
alive bool
|
||||
callback MessageHandler
|
||||
conn deadlineConn
|
||||
done chan struct{}
|
||||
once sync.Once
|
||||
pingPong bool
|
||||
send chan []byte
|
||||
alive bool
|
||||
callback MessageHandler
|
||||
conn deadlineConn
|
||||
done chan struct{}
|
||||
errorHandler ErrorHandler
|
||||
once sync.Once
|
||||
pingPong bool
|
||||
send chan []byte
|
||||
messSize int64
|
||||
}
|
||||
|
||||
type deadlineConn struct {
|
||||
|
|
@ -43,6 +45,7 @@ type deadlineConn struct {
|
|||
}
|
||||
|
||||
type MessageHandler func([]byte, error)
|
||||
type ErrorHandler func(err error)
|
||||
|
||||
type Upgrader struct {
|
||||
websocket.Upgrader
|
||||
|
|
@ -125,7 +128,12 @@ func (c *Connection) reader() {
|
|||
c.close()
|
||||
}()
|
||||
|
||||
c.conn.SetReadLimit(maxMessageSize)
|
||||
var s int64 = maxMessageSize
|
||||
if c.messSize > 0 {
|
||||
s = c.messSize
|
||||
}
|
||||
c.conn.SetReadLimit(s)
|
||||
|
||||
_ = c.conn.SetReadDeadline(time.Now().Add(pongTime))
|
||||
if c.pingPong {
|
||||
c.conn.SetPongHandler(func(string) error { _ = c.conn.SetReadDeadline(time.Now().Add(pongTime)); return nil })
|
||||
|
|
@ -145,6 +153,10 @@ func (c *Connection) reader() {
|
|||
_, message, err := c.conn.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) {
|
||||
if c.errorHandler != nil {
|
||||
c.errorHandler(err)
|
||||
}
|
||||
} else {
|
||||
c.callback(message, err)
|
||||
}
|
||||
break
|
||||
|
|
@ -219,6 +231,10 @@ func (c *Connection) IsServer() bool { return c.pingPong }
|
|||
|
||||
func (c *Connection) SetMessageHandler(fn MessageHandler) { c.callback = fn }
|
||||
|
||||
func (c *Connection) SetErrorHandler(fn ErrorHandler) { c.errorHandler = fn }
|
||||
|
||||
func (c *Connection) SetMaxMessageSize(s int64) { c.messSize = s }
|
||||
|
||||
func (c *Connection) Listen() chan struct{} {
|
||||
if c.alive {
|
||||
return c.done
|
||||
|
|
|
|||
37
pkg/os/flock.go
Normal file
37
pkg/os/flock.go
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
package os
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gofrs/flock"
|
||||
)
|
||||
|
||||
type Flock struct {
|
||||
f *flock.Flock
|
||||
}
|
||||
|
||||
func NewFileLock(path string) (*Flock, error) {
|
||||
if path == "" {
|
||||
path = os.TempDir() + string(os.PathSeparator) + "cloud_game.lock"
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0770); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
f, err := os.Create(path)
|
||||
defer func() { _ = f.Close() }()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
f := Flock{
|
||||
f: flock.New(path),
|
||||
}
|
||||
|
||||
return &f, nil
|
||||
}
|
||||
|
||||
func (f *Flock) Lock() error { return f.f.Lock() }
|
||||
func (f *Flock) Unlock() error { return f.f.Unlock() }
|
||||
39
pkg/os/os.go
39
pkg/os/os.go
|
|
@ -28,6 +28,10 @@ func CheckCreateDir(path string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func MakeDirAll(path string) error {
|
||||
return os.MkdirAll(path, os.ModeDir|os.ModePerm)
|
||||
}
|
||||
|
||||
func ExpectTermination() chan struct{} {
|
||||
signals := make(chan os.Signal, 1)
|
||||
signal.Notify(signals, os.Interrupt, syscall.SIGTERM)
|
||||
|
|
@ -47,6 +51,37 @@ func GetUserHome() (string, error) {
|
|||
return me.HomeDir, nil
|
||||
}
|
||||
|
||||
func CopyFile(from string, to string) (err error) {
|
||||
f, err := os.Open(from)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err2 := f.Close(); err2 != nil {
|
||||
err = errors.Join(err, err2)
|
||||
}
|
||||
}()
|
||||
|
||||
destFile, err := os.Create(to)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err2 := destFile.Close(); err != nil {
|
||||
err = errors.Join(err, err2)
|
||||
}
|
||||
}()
|
||||
|
||||
n, err := f.WriteTo(destFile)
|
||||
if n == 0 {
|
||||
return errors.New("nothing was written")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func WriteFile(name string, data []byte, perm os.FileMode) error {
|
||||
return os.WriteFile(name, data, perm)
|
||||
}
|
||||
|
|
@ -84,3 +119,7 @@ func StatSize(path string) (int64, error) {
|
|||
}
|
||||
return fi.Size(), nil
|
||||
}
|
||||
|
||||
func RemoveAll(path string) error {
|
||||
return os.RemoveAll(path)
|
||||
}
|
||||
|
|
|
|||
62
pkg/resampler/simple.go
Normal file
62
pkg/resampler/simple.go
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
package resampler
|
||||
|
||||
func Linear(dst, src []int16) {
|
||||
nSrc, nDst := len(src), len(dst)
|
||||
if nSrc < 2 || nDst < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
srcPairs, dstPairs := nSrc>>1, nDst>>1
|
||||
|
||||
// replicate single pair input or output
|
||||
if srcPairs == 1 || dstPairs == 1 {
|
||||
for i := range dstPairs {
|
||||
dst[i*2], dst[i*2+1] = src[0], src[1]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ratio := ((srcPairs - 1) << 16) / (dstPairs - 1)
|
||||
lastSrc := nSrc - 2
|
||||
|
||||
// interpolate all pairs except the last
|
||||
for i, pos := 0, 0; i < dstPairs-1; i, pos = i+1, pos+ratio {
|
||||
idx := (pos >> 16) << 1
|
||||
di := i << 1
|
||||
frac := int32(pos & 0xFFFF)
|
||||
l0, r0 := int32(src[idx]), int32(src[idx+1])
|
||||
|
||||
// L = L0 + (L1-L0)*frac
|
||||
dst[di] = int16(l0 + ((int32(src[idx+2])-l0)*frac)>>16)
|
||||
// R = R0 + (R1-R0)*frac
|
||||
dst[di+1] = int16(r0 + ((int32(src[idx+3])-r0)*frac)>>16)
|
||||
}
|
||||
|
||||
// last output pair = last input pair (avoids precision loss at the edge)
|
||||
lastDst := (dstPairs - 1) << 1
|
||||
dst[lastDst], dst[lastDst+1] = src[lastSrc], src[lastSrc+1]
|
||||
}
|
||||
|
||||
func Nearest(dst, src []int16) {
|
||||
nSrc, nDst := len(src), len(dst)
|
||||
if nSrc < 2 || nDst < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
srcPairs, dstPairs := nSrc>>1, nDst>>1
|
||||
|
||||
if srcPairs == 1 || dstPairs == 1 {
|
||||
for i := range dstPairs {
|
||||
dst[i*2], dst[i*2+1] = src[0], src[1]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ratio := (srcPairs << 16) / dstPairs
|
||||
|
||||
for i, pos := 0, 0; i < dstPairs; i, pos = i+1, pos+ratio {
|
||||
si := (pos >> 16) << 1
|
||||
di := i << 1
|
||||
dst[di], dst[di+1] = src[si], src[si+1]
|
||||
}
|
||||
}
|
||||
106
pkg/resampler/speex.go
Normal file
106
pkg/resampler/speex.go
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
package resampler
|
||||
|
||||
/*
|
||||
#cgo pkg-config: speexdsp
|
||||
#cgo st LDFLAGS: -l:libspeexdsp.a
|
||||
|
||||
#include <stdint.h>
|
||||
#include "speex_resampler.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Quality
|
||||
const (
|
||||
QualityMax = 10
|
||||
QualityMin = 0
|
||||
QualityDefault = 4
|
||||
QualityDesktop = 5
|
||||
QualityVoid = 3
|
||||
)
|
||||
|
||||
// Errors
|
||||
const (
|
||||
ErrorSuccess = iota
|
||||
ErrorAllocFailed
|
||||
ErrorBadState
|
||||
ErrorInvalidArg
|
||||
ErrorPtrOverlap
|
||||
ErrorMaxError
|
||||
)
|
||||
|
||||
type Resampler struct {
|
||||
resampler *C.SpeexResamplerState
|
||||
channels int
|
||||
inRate int
|
||||
outRate int
|
||||
}
|
||||
|
||||
func Init(channels, inRate, outRate, quality int) (*Resampler, error) {
|
||||
var err C.int
|
||||
r := &Resampler{
|
||||
channels: channels,
|
||||
inRate: inRate,
|
||||
outRate: outRate,
|
||||
}
|
||||
|
||||
r.resampler = C.speex_resampler_init(
|
||||
C.spx_uint32_t(channels),
|
||||
C.spx_uint32_t(inRate),
|
||||
C.spx_uint32_t(outRate),
|
||||
C.int(quality),
|
||||
&err,
|
||||
)
|
||||
|
||||
if r.resampler == nil {
|
||||
return nil, StrError(int(err))
|
||||
}
|
||||
|
||||
C.speex_resampler_skip_zeros(r.resampler)
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *Resampler) Destroy() {
|
||||
if r.resampler != nil {
|
||||
C.speex_resampler_destroy(r.resampler)
|
||||
r.resampler = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Process performs resampling.
|
||||
// Returns written samples count and error if any.
|
||||
func (r *Resampler) Process(out, in []int16) (int, error) {
|
||||
if len(in) == 0 || len(out) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
inLen := C.spx_uint32_t(len(in) / r.channels)
|
||||
outLen := C.spx_uint32_t(len(out) / r.channels)
|
||||
|
||||
res := C.speex_resampler_process_interleaved_int(
|
||||
r.resampler,
|
||||
(*C.spx_int16_t)(unsafe.Pointer(&in[0])),
|
||||
&inLen,
|
||||
(*C.spx_int16_t)(unsafe.Pointer(&out[0])),
|
||||
&outLen,
|
||||
)
|
||||
|
||||
if res != ErrorSuccess {
|
||||
return 0, StrError(int(res))
|
||||
}
|
||||
|
||||
return int(outLen) * r.channels, nil
|
||||
}
|
||||
|
||||
func StrError(errorCode int) error {
|
||||
cS := C.speex_resampler_strerror(C.int(errorCode))
|
||||
if cS == nil {
|
||||
return nil
|
||||
}
|
||||
return errors.New(C.GoString(cS))
|
||||
}
|
||||
70
pkg/resampler/speex_resampler.h
Normal file
70
pkg/resampler/speex_resampler.h
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
#ifndef SPEEX_RESAMPLER_H
|
||||
#define SPEEX_RESAMPLER_H
|
||||
|
||||
#define spx_int16_t short
|
||||
#define spx_int32_t int
|
||||
#define spx_uint16_t unsigned short
|
||||
#define spx_uint32_t unsigned int
|
||||
|
||||
#define SPEEX_RESAMPLER_QUALITY_MAX 10
|
||||
#define SPEEX_RESAMPLER_QUALITY_MIN 0
|
||||
#define SPEEX_RESAMPLER_QUALITY_DEFAULT 4
|
||||
#define SPEEX_RESAMPLER_QUALITY_VOIP 3
|
||||
#define SPEEX_RESAMPLER_QUALITY_DESKTOP 5
|
||||
enum {
|
||||
RESAMPLER_ERR_SUCCESS = 0,
|
||||
RESAMPLER_ERR_ALLOC_FAILED = 1,
|
||||
RESAMPLER_ERR_BAD_STATE = 2,
|
||||
RESAMPLER_ERR_INVALID_ARG = 3,
|
||||
RESAMPLER_ERR_PTR_OVERLAP = 4,
|
||||
|
||||
RESAMPLER_ERR_MAX_ERROR
|
||||
};
|
||||
struct SpeexResamplerState_;
|
||||
typedef struct SpeexResamplerState_ SpeexResamplerState;
|
||||
/** Create a new resampler with integer input and output rates.
|
||||
* @param nb_channels Number of channels to be processed
|
||||
* @param in_rate Input sampling rate (integer number of Hz).
|
||||
* @param out_rate Output sampling rate (integer number of Hz).
|
||||
* @param quality Resampling quality between 0 and 10, where 0 has poor quality
|
||||
* and 10 has very high quality.
|
||||
* @return Newly created resampler state
|
||||
* @retval NULL Error: not enough memory
|
||||
*/
|
||||
SpeexResamplerState *speex_resampler_init(spx_uint32_t nb_channels,
|
||||
spx_uint32_t in_rate,
|
||||
spx_uint32_t out_rate,
|
||||
int quality,
|
||||
int *err);
|
||||
/** Destroy a resampler state.
|
||||
* @param st Resampler state
|
||||
*/
|
||||
void speex_resampler_destroy(SpeexResamplerState *st);
|
||||
|
||||
|
||||
/** Make sure that the first samples to go out of the resamplers don't have
|
||||
* leading zeros. This is only useful before starting to use a newly created
|
||||
* resampler. It is recommended to use that when resampling an audio file, as
|
||||
* it will generate a file with the same length. For real-time processing,
|
||||
* it is probably easier not to use this call (so that the output duration
|
||||
* is the same for the first frame).
|
||||
* @param st Resampler state
|
||||
*/
|
||||
int speex_resampler_skip_zeros(SpeexResamplerState *st);
|
||||
|
||||
/** Resample an interleaved int array. The input and output buffers must *not* overlap.
|
||||
* @param st Resampler state
|
||||
* @param in Input buffer
|
||||
* @param in_len Number of input samples in the input buffer. Returns the number
|
||||
* of samples processed. This is all per-channel.
|
||||
* @param out Output buffer
|
||||
* @param out_len Size of the output buffer. Returns the number of samples written.
|
||||
* This is all per-channel.
|
||||
*/
|
||||
int speex_resampler_process_interleaved_int(SpeexResamplerState *st,
|
||||
const spx_int16_t *in,
|
||||
spx_uint32_t *in_len,
|
||||
spx_int16_t *out,
|
||||
spx_uint32_t *out_len);
|
||||
const char *speex_resampler_strerror(int err);
|
||||
#endif
|
||||
|
|
@ -2,14 +2,19 @@ package app
|
|||
|
||||
type App interface {
|
||||
AudioSampleRate() int
|
||||
AspectRatio() float32
|
||||
AspectEnabled() bool
|
||||
Init() error
|
||||
ViewportSize() (int, int)
|
||||
Scale() float64
|
||||
Start()
|
||||
Close()
|
||||
|
||||
SetAudioCb(func(Audio))
|
||||
SetVideoCb(func(Video))
|
||||
SendControl(port int, data []byte)
|
||||
SetDataCb(func([]byte))
|
||||
Input(port int, device byte, data []byte)
|
||||
KbMouseSupport() bool
|
||||
}
|
||||
|
||||
type Audio struct {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ type Manager struct {
|
|||
log *logger.Logger
|
||||
}
|
||||
|
||||
const (
|
||||
RetroPad = libretro.RetroPad
|
||||
Keyboard = libretro.Keyboard
|
||||
Mouse = libretro.Mouse
|
||||
)
|
||||
|
||||
type ModName string
|
||||
|
||||
const Libretro ModName = "libretro"
|
||||
|
|
|
|||
|
|
@ -14,9 +14,6 @@ type Caged struct {
|
|||
base *Frontend // maintains the root for mad embedding
|
||||
conf CagedConf
|
||||
log *logger.Logger
|
||||
w, h int
|
||||
|
||||
OnSysInfoChange func()
|
||||
}
|
||||
|
||||
type CagedConf struct {
|
||||
|
|
@ -34,6 +31,13 @@ func (c *Caged) Init() error {
|
|||
if err := manager.CheckCores(c.conf.Emulator, c.log); err != nil {
|
||||
c.log.Warn().Err(err).Msgf("a Libretro cores sync fail")
|
||||
}
|
||||
|
||||
if c.conf.Emulator.FailFast {
|
||||
if err := c.IsSupported(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -41,26 +45,24 @@ func (c *Caged) ReloadFrontend() {
|
|||
frontend, err := NewFrontend(c.conf.Emulator, c.log)
|
||||
if err != nil {
|
||||
c.log.Fatal().Err(err).Send()
|
||||
return
|
||||
}
|
||||
c.Emulator = frontend
|
||||
c.base = frontend
|
||||
}
|
||||
|
||||
func (c *Caged) HandleOnSystemAvInfo(fn func()) {
|
||||
c.base.SetOnAV(func() {
|
||||
w, h := c.ViewportCalc()
|
||||
c.SetViewport(w, h)
|
||||
fn()
|
||||
})
|
||||
}
|
||||
// VideoChangeCb adds a callback when video params are changed by the app.
|
||||
func (c *Caged) VideoChangeCb(fn func()) { c.base.SetVideoChangeCb(fn) }
|
||||
|
||||
func (c *Caged) Load(game games.GameMetadata, path string) error {
|
||||
if c.Emulator == nil {
|
||||
return nil
|
||||
}
|
||||
c.Emulator.LoadCore(game.System)
|
||||
if err := c.Emulator.LoadGame(game.FullPath(path)); err != nil {
|
||||
return err
|
||||
}
|
||||
w, h := c.ViewportCalc()
|
||||
c.SetViewport(w, h)
|
||||
c.ViewportRecalculate()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -73,24 +75,28 @@ func (c *Caged) EnableRecording(nowait bool, user string, game string) {
|
|||
}
|
||||
|
||||
func (c *Caged) EnableCloudStorage(uid string, storage cloud.Storage) {
|
||||
if storage != nil {
|
||||
wc, err := WithCloud(c.Emulator, uid, storage)
|
||||
if err != nil {
|
||||
c.log.Error().Err(err).Msgf("couldn't init %v", wc.HashPath())
|
||||
} else {
|
||||
c.log.Info().Msgf("cloud state %v has been initialized", wc.HashPath())
|
||||
c.Emulator = wc
|
||||
}
|
||||
if storage == nil {
|
||||
return
|
||||
}
|
||||
if wc, err := WithCloud(c.Emulator, uid, storage); err == nil {
|
||||
c.Emulator = wc
|
||||
c.log.Info().Msgf("cloud storage has been initialized")
|
||||
} else {
|
||||
c.log.Error().Err(err).Msgf("couldn't init cloud storage")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Caged) PixFormat() uint32 { return c.Emulator.PixFormat() }
|
||||
func (c *Caged) Rotation() uint { return c.Emulator.Rotation() }
|
||||
func (c *Caged) AudioSampleRate() int { return c.Emulator.AudioSampleRate() }
|
||||
func (c *Caged) ViewportSize() (int, int) { return c.Emulator.ViewportSize() }
|
||||
func (c *Caged) Scale() float64 { return c.Emulator.Scale() }
|
||||
func (c *Caged) SendControl(port int, data []byte) { c.base.Input(port, data) }
|
||||
func (c *Caged) Start() { go c.Emulator.Start() }
|
||||
func (c *Caged) SetSaveOnClose(v bool) { c.base.SaveOnClose = v }
|
||||
func (c *Caged) SetSessionId(name string) { c.base.SetSessionId(name) }
|
||||
func (c *Caged) Close() { c.Emulator.Close() }
|
||||
func (c *Caged) AspectEnabled() bool { return c.base.nano.Aspect }
|
||||
func (c *Caged) AspectRatio() float32 { return c.base.AspectRatio() }
|
||||
func (c *Caged) PixFormat() uint32 { return c.Emulator.PixFormat() }
|
||||
func (c *Caged) Rotation() uint { return c.Emulator.Rotation() }
|
||||
func (c *Caged) AudioSampleRate() int { return c.Emulator.AudioSampleRate() }
|
||||
func (c *Caged) ViewportSize() (int, int) { return c.base.ViewportSize() }
|
||||
func (c *Caged) Scale() float64 { return c.Emulator.Scale() }
|
||||
func (c *Caged) Input(p int, d byte, data []byte) { c.base.Input(p, d, data) }
|
||||
func (c *Caged) KbMouseSupport() bool { return c.base.KbMouseSupport() }
|
||||
func (c *Caged) Start() { go c.Emulator.Start() }
|
||||
func (c *Caged) SetSaveOnClose(v bool) { c.base.SaveOnClose = v }
|
||||
func (c *Caged) SetSessionId(name string) { c.base.SetSessionId(name) }
|
||||
func (c *Caged) Close() { c.Emulator.Close() }
|
||||
func (c *Caged) IsSupported() error { return c.base.IsSupported() }
|
||||
|
|
|
|||
|
|
@ -7,32 +7,37 @@ import (
|
|||
|
||||
type CloudFrontend struct {
|
||||
Emulator
|
||||
stateName string
|
||||
stateLocalPath string
|
||||
storage cloud.Storage // a cloud storage to store room state online
|
||||
uid string
|
||||
storage cloud.Storage // a cloud storage to store room state online
|
||||
}
|
||||
|
||||
func WithCloud(fe Emulator, stateName string, storage cloud.Storage) (*CloudFrontend, error) {
|
||||
r := &CloudFrontend{Emulator: fe, stateLocalPath: fe.HashPath(), stateName: stateName, storage: storage}
|
||||
// WithCloud adds the ability to keep game states in the cloud storage like Amazon S3.
|
||||
// It supports only one file of main save state.
|
||||
func WithCloud(fe Emulator, uid string, storage cloud.Storage) (*CloudFrontend, error) {
|
||||
r := &CloudFrontend{Emulator: fe, uid: uid, storage: storage}
|
||||
|
||||
// saveOnlineRoomToLocal save online room to local.
|
||||
// !Supports only one file of main save state.
|
||||
data, err := r.storage.Load(stateName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// save the data fetched from the cloud to a local directory
|
||||
if data != nil {
|
||||
if err := os.WriteFile(r.stateLocalPath, data, 0644); err != nil {
|
||||
name := fe.SaveStateName()
|
||||
|
||||
if r.storage.Has(name) {
|
||||
data, err := r.storage.Load(fe.SaveStateName())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// save the data fetched from the cloud to a local directory
|
||||
if data != nil {
|
||||
if err := os.WriteFile(fe.HashPath(), data, 0644); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// !to use emulator save/load calls instead of the storage
|
||||
|
||||
func (c *CloudFrontend) HasSave() bool {
|
||||
_, err := c.storage.Load(c.stateName)
|
||||
_, err := c.storage.Load(c.SaveStateName())
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
|
|
@ -43,8 +48,13 @@ func (c *CloudFrontend) SaveGameState() error {
|
|||
if err := c.Emulator.SaveGameState(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.storage.Save(c.stateName, c.stateLocalPath); err != nil {
|
||||
path := c.Emulator.HashPath()
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return c.storage.Save(c.SaveStateName(), data, map[string]string{
|
||||
"uid": c.uid,
|
||||
"type": "cloudretro-main-save",
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ package libretro
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
|
|
@ -14,12 +14,14 @@ import (
|
|||
"github.com/giongto35/cloud-game/v3/pkg/logger"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/os"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/worker/caged/app"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/worker/caged/libretro/graphics"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/worker/caged/libretro/nanoarch"
|
||||
)
|
||||
|
||||
type Emulator interface {
|
||||
SetAudioCb(func(app.Audio))
|
||||
SetVideoCb(func(app.Video))
|
||||
SetDataCb(func([]byte))
|
||||
LoadCore(name string)
|
||||
LoadGame(path string) error
|
||||
FPS() int
|
||||
|
|
@ -30,70 +32,68 @@ type Emulator interface {
|
|||
IsPortrait() bool
|
||||
// Start is called after LoadGame
|
||||
Start()
|
||||
// SetViewport sets viewport size
|
||||
SetViewport(width int, height int)
|
||||
// ViewportCalc calculates the viewport size with the aspect ratio and scale
|
||||
ViewportCalc() (nw int, nh int)
|
||||
ViewportSize() (w, h int)
|
||||
// ViewportRecalculate calculates output resolution with aspect and scale
|
||||
ViewportRecalculate()
|
||||
RestoreGameState() error
|
||||
// SetSessionId sets distinct name for the game session (in order to save/load it later)
|
||||
SetSessionId(name string)
|
||||
SaveGameState() error
|
||||
SaveStateName() string
|
||||
// HashPath returns the path emulator will save state to
|
||||
HashPath() string
|
||||
// HasSave returns true if the current ROM was saved before
|
||||
HasSave() bool
|
||||
// Close will be called when the game is done
|
||||
Close()
|
||||
// ToggleMultitap toggles multitap controller.
|
||||
ToggleMultitap()
|
||||
// Input passes input to the emulator
|
||||
Input(player int, data []byte)
|
||||
Input(player int, device byte, data []byte)
|
||||
// Scale returns set video scale factor
|
||||
Scale() float64
|
||||
Reset()
|
||||
}
|
||||
|
||||
type Frontend struct {
|
||||
conf config.Emulator
|
||||
done chan struct{}
|
||||
input InputState
|
||||
log *logger.Logger
|
||||
nano *nanoarch.Nanoarch
|
||||
onAudio func(app.Audio)
|
||||
onData func([]byte)
|
||||
onVideo func(app.Video)
|
||||
storage Storage
|
||||
scale float64
|
||||
th int // draw threads
|
||||
vw, vh int // out frame size
|
||||
|
||||
mu sync.Mutex
|
||||
// directives
|
||||
|
||||
// skipVideo used when new frame was too late
|
||||
skipVideo bool
|
||||
|
||||
mu sync.Mutex
|
||||
mui sync.Mutex
|
||||
|
||||
DisableCanvasPool bool
|
||||
SaveOnClose bool
|
||||
UniqueSaveDir bool
|
||||
SaveStateFs string
|
||||
}
|
||||
|
||||
// InputState stores full controller state.
|
||||
// It consists of:
|
||||
// - uint16 button values
|
||||
// - int16 analog stick values
|
||||
type (
|
||||
InputState [maxPort]State
|
||||
State struct {
|
||||
keys uint32
|
||||
axes [dpadAxes]int32
|
||||
}
|
||||
)
|
||||
type Device byte
|
||||
|
||||
const (
|
||||
maxPort = 4
|
||||
dpadAxes = 4
|
||||
RetroPad = Device(nanoarch.RetroPad)
|
||||
Keyboard = Device(nanoarch.Keyboard)
|
||||
Mouse = Device(nanoarch.Mouse)
|
||||
)
|
||||
|
||||
var (
|
||||
audioPool sync.Pool
|
||||
noAudio = func(app.Audio) {}
|
||||
noData = func([]byte) {}
|
||||
noVideo = func(app.Video) {}
|
||||
videoPool sync.Pool
|
||||
lastFrame *app.Video
|
||||
)
|
||||
|
||||
// NewFrontend implements Emulator interface for a Libretro frontend.
|
||||
|
|
@ -111,8 +111,8 @@ func NewFrontend(conf config.Emulator, log *logger.Logger) (*Frontend, error) {
|
|||
nano := nanoarch.NewNano(path)
|
||||
|
||||
log = log.Extend(log.With().Str("m", "Libretro"))
|
||||
ll := log.Extend(log.Level(logger.Level(conf.Libretro.LogLevel)).With())
|
||||
nano.SetLogger(ll)
|
||||
level := logger.Level(conf.Libretro.LogLevel)
|
||||
nano.SetLogger(log.Extend(log.Level(level).With()))
|
||||
|
||||
// Check if room is on local storage, if not, pull from GCS to local storage
|
||||
log.Info().Msgf("Local storage path: %v", conf.Storage)
|
||||
|
|
@ -129,36 +129,62 @@ func NewFrontend(conf config.Emulator, log *logger.Logger) (*Frontend, error) {
|
|||
f := &Frontend{
|
||||
conf: conf,
|
||||
done: make(chan struct{}),
|
||||
input: NewGameSessionInput(),
|
||||
log: log,
|
||||
onAudio: noAudio,
|
||||
onData: noData,
|
||||
onVideo: noVideo,
|
||||
storage: store,
|
||||
th: conf.Threads,
|
||||
}
|
||||
f.linkNano(nano)
|
||||
|
||||
if conf.Libretro.DebounceMs > 0 {
|
||||
t := time.Duration(conf.Libretro.DebounceMs) * time.Millisecond
|
||||
f.nano.SetVideoDebounce(t)
|
||||
f.log.Debug().Msgf("set debounce time: %v", t)
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (f *Frontend) LoadCore(emu string) {
|
||||
conf := f.conf.GetLibretroCoreConfig(emu)
|
||||
|
||||
libExt := ""
|
||||
if ar, err := f.conf.Libretro.Cores.Repo.Guess(); err == nil {
|
||||
libExt = ar.Ext
|
||||
} else {
|
||||
f.log.Warn().Err(err).Msg("system arch guesser failed")
|
||||
}
|
||||
|
||||
meta := nanoarch.Metadata{
|
||||
AutoGlContext: conf.AutoGlContext,
|
||||
Hacks: conf.Hacks,
|
||||
HasMultitap: conf.HasMultitap,
|
||||
HasVFR: conf.VFR,
|
||||
IsGlAllowed: conf.IsGlAllowed,
|
||||
LibPath: conf.Lib,
|
||||
Options: conf.Options,
|
||||
UsesLibCo: conf.UsesLibCo,
|
||||
AutoGlContext: conf.AutoGlContext,
|
||||
FrameDup: f.conf.Libretro.Dup,
|
||||
Hacks: conf.Hacks,
|
||||
HasVFR: conf.VFR,
|
||||
Hid: conf.Hid,
|
||||
IsGlAllowed: conf.IsGlAllowed,
|
||||
LibPath: conf.Lib,
|
||||
Options: conf.Options,
|
||||
Options4rom: conf.Options4rom,
|
||||
UsesLibCo: conf.UsesLibCo,
|
||||
CoreAspectRatio: conf.CoreAspectRatio,
|
||||
KbMouseSupport: conf.KbMouseSupport,
|
||||
LibExt: libExt,
|
||||
}
|
||||
f.mu.Lock()
|
||||
f.SaveStateFs = conf.SaveStateFs
|
||||
if conf.UniqueSaveDir {
|
||||
f.UniqueSaveDir = true
|
||||
f.nano.SetSaveDirSuffix(f.storage.MainPath())
|
||||
f.log.Debug().Msgf("Using unique dir for saves: %v", f.storage.MainPath())
|
||||
}
|
||||
scale := 1.0
|
||||
if conf.Scale > 1 {
|
||||
scale = conf.Scale
|
||||
f.log.Debug().Msgf("Scale: x%v", scale)
|
||||
}
|
||||
f.storage.SetNonBlocking(conf.NonBlockingSave)
|
||||
f.scale = scale
|
||||
f.nano.CoreLoad(meta)
|
||||
f.mu.Unlock()
|
||||
|
|
@ -178,7 +204,10 @@ func (f *Frontend) handleAudio(audio unsafe.Pointer, samples int) {
|
|||
}
|
||||
|
||||
func (f *Frontend) handleVideo(data []byte, delta int32, fi nanoarch.FrameInfo) {
|
||||
// !to merge both pools
|
||||
if f.conf.SkipLateFrames && f.skipVideo {
|
||||
return
|
||||
}
|
||||
|
||||
fr, _ := videoPool.Get().(*app.Video)
|
||||
if fr == nil {
|
||||
fr = new(app.Video)
|
||||
|
|
@ -188,137 +217,230 @@ func (f *Frontend) handleVideo(data []byte, delta int32, fi nanoarch.FrameInfo)
|
|||
fr.Frame.H = int(fi.H)
|
||||
fr.Frame.Stride = int(fi.Stride)
|
||||
fr.Duration = delta
|
||||
|
||||
lastFrame = fr
|
||||
f.onVideo(*fr)
|
||||
|
||||
videoPool.Put(fr)
|
||||
}
|
||||
|
||||
func (f *Frontend) handleDup() {
|
||||
if lastFrame != nil {
|
||||
f.onVideo(*lastFrame)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Frontend) Shutdown() {
|
||||
f.mu.Lock()
|
||||
f.nano.Shutdown()
|
||||
f.SetAudioCb(noAudio)
|
||||
f.SetVideoCb(noVideo)
|
||||
lastFrame = nil
|
||||
f.mu.Unlock()
|
||||
f.log.Debug().Msgf("frontend closed")
|
||||
f.log.Debug().Msgf("frontend shutdown done")
|
||||
}
|
||||
|
||||
func (f *Frontend) linkNano(nano *nanoarch.Nanoarch) {
|
||||
f.nano = nano
|
||||
if nano == nil {
|
||||
return
|
||||
}
|
||||
f.nano.WaitReady() // start only when nano is available
|
||||
|
||||
f.nano.OnKeyPress = f.input.isKeyPressed
|
||||
f.nano.OnDpad = f.input.isDpadTouched
|
||||
f.nano.OnVideo = f.handleVideo
|
||||
f.nano.OnAudio = f.handleAudio
|
||||
f.nano.OnDup = f.handleDup
|
||||
}
|
||||
|
||||
func (f *Frontend) SetOnAV(fn func()) { f.nano.OnSystemAvInfo = fn }
|
||||
func (f *Frontend) SetVideoChangeCb(fn func()) {
|
||||
if f.nano != nil {
|
||||
f.nano.OnSystemAvInfo = fn
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Frontend) Start() {
|
||||
f.log.Debug().Msgf("Frontend start")
|
||||
f.log.Debug().Msgf("frontend start")
|
||||
if f.nano.Stopped.Load() {
|
||||
f.log.Warn().Msgf("frontend stopped during the start")
|
||||
f.mui.Lock()
|
||||
defer f.mui.Unlock()
|
||||
f.Shutdown()
|
||||
return
|
||||
}
|
||||
|
||||
// don't jump between threads
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
f.mui.Lock()
|
||||
f.done = make(chan struct{})
|
||||
f.nano.LastFrameTime = time.Now().UnixNano()
|
||||
defer f.Shutdown()
|
||||
|
||||
defer func() {
|
||||
// Save game on quit if it was saved before (shared or click-saved).
|
||||
if f.SaveOnClose && f.HasSave() {
|
||||
f.log.Debug().Msg("save on quit")
|
||||
if err := f.Save(); err != nil {
|
||||
f.log.Error().Err(err).Msg("save on quit failed")
|
||||
}
|
||||
}
|
||||
f.mui.Unlock()
|
||||
f.Shutdown()
|
||||
}()
|
||||
|
||||
if f.HasSave() {
|
||||
// advance 1 frame for Mupen save state
|
||||
if f.nano.LibCo {
|
||||
f.Tick()
|
||||
}
|
||||
// advance 1 frame for Mupen, DOSBox save states
|
||||
// loading will work if autostart is selected for DOSBox apps
|
||||
f.Tick()
|
||||
if err := f.RestoreGameState(); err != nil {
|
||||
f.log.Error().Err(err).Msg("couldn't load a save file")
|
||||
}
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(time.Second / time.Duration(f.nano.VideoFramerate()))
|
||||
defer ticker.Stop()
|
||||
|
||||
if f.conf.AutosaveSec > 0 {
|
||||
// !to sync both for loops, can crash if the emulator starts later
|
||||
go f.autosave(f.conf.AutosaveSec)
|
||||
}
|
||||
|
||||
// The main loop of Libretro
|
||||
|
||||
// calculate the exact duration required for a frame (e.g., 16.666ms = 60 FPS)
|
||||
targetFrameTime := time.Second / time.Duration(f.nano.VideoFramerate())
|
||||
|
||||
// stop sleeping and start spinning in the remaining 1ms
|
||||
const spinThreshold = 1 * time.Millisecond
|
||||
|
||||
// how many frames will be considered not normal
|
||||
const lateFramesThreshold = 3
|
||||
|
||||
lastFrameStart := time.Now()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
f.Tick()
|
||||
case <-f.done:
|
||||
return
|
||||
default:
|
||||
// run one tick of the emulation
|
||||
f.Tick()
|
||||
|
||||
elapsed := time.Since(lastFrameStart)
|
||||
sleepTime := targetFrameTime - elapsed
|
||||
|
||||
if sleepTime > 0 {
|
||||
// SLEEP
|
||||
// if we have plenty of time, sleep to save CPU and
|
||||
// wake up slightly before the target time
|
||||
if sleepTime > spinThreshold {
|
||||
time.Sleep(sleepTime - spinThreshold)
|
||||
}
|
||||
|
||||
// SPIN
|
||||
// if we are close to the target,
|
||||
// burn CPU and check the clock with ns resolution
|
||||
for time.Since(lastFrameStart) < targetFrameTime {
|
||||
// CPU burn!
|
||||
}
|
||||
f.skipVideo = false
|
||||
} else {
|
||||
// lagging behind the target framerate so we don't sleep
|
||||
if f.conf.LogDroppedFrames {
|
||||
// !to make some stats counter instead
|
||||
f.log.Debug().Msgf("[] Frame drop: %v", elapsed)
|
||||
}
|
||||
f.skipVideo = true
|
||||
}
|
||||
|
||||
// timer reset
|
||||
//
|
||||
// adding targetFrameTime to the previous start
|
||||
// prevents drift, if one frame was late,
|
||||
// we try to catch up in the next frame
|
||||
lastFrameStart = lastFrameStart.Add(targetFrameTime)
|
||||
|
||||
// if execution was paused or heavily delayed,
|
||||
// reset lastFrameStart so we don't try to run
|
||||
// a bunch of frames instantly to catch up
|
||||
if time.Since(lastFrameStart) > targetFrameTime*lateFramesThreshold {
|
||||
lastFrameStart = time.Now()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Frontend) PixFormat() uint32 { return f.nano.Video.PixFmt.C }
|
||||
func (f *Frontend) Rotation() uint { return f.nano.Rot }
|
||||
func (f *Frontend) Flipped() bool { return f.nano.IsGL() }
|
||||
func (f *Frontend) FrameSize() (int, int) { return f.nano.GeometryBase() }
|
||||
func (f *Frontend) FPS() int { return f.nano.VideoFramerate() }
|
||||
func (f *Frontend) HashPath() string { return f.storage.GetSavePath() }
|
||||
func (f *Frontend) HasSave() bool { return os.Exists(f.HashPath()) }
|
||||
func (f *Frontend) SRAMPath() string { return f.storage.GetSRAMPath() }
|
||||
func (f *Frontend) AudioSampleRate() int { return f.nano.AudioSampleRate() }
|
||||
func (f *Frontend) Input(player int, data []byte) { f.input.setInput(player, data) }
|
||||
func (f *Frontend) LoadGame(path string) error { return f.nano.LoadGame(path) }
|
||||
func (f *Frontend) RestoreGameState() error { return f.Load() }
|
||||
func (f *Frontend) Scale() float64 { return f.scale }
|
||||
func (f *Frontend) IsPortrait() bool { return f.nano.IsPortrait() }
|
||||
func (f *Frontend) SaveGameState() error { return f.Save() }
|
||||
func (f *Frontend) SetAudioCb(cb func(app.Audio)) { f.onAudio = cb }
|
||||
func (f *Frontend) SetSessionId(name string) { f.storage.SetMainSaveName(name) }
|
||||
func (f *Frontend) SetVideoCb(ff func(app.Video)) { f.onVideo = ff }
|
||||
func (f *Frontend) SetViewport(width int, height int) {
|
||||
f.mu.Lock()
|
||||
f.vw, f.vh = width, height
|
||||
f.mu.Unlock()
|
||||
func (f *Frontend) LoadGame(path string) error {
|
||||
if f.UniqueSaveDir {
|
||||
f.copyFsMaybe(path)
|
||||
}
|
||||
return f.nano.LoadGame(path)
|
||||
}
|
||||
|
||||
// Tick runs one emulation frame.
|
||||
func (f *Frontend) Tick() { f.mu.Lock(); f.nano.Run(); f.mu.Unlock() }
|
||||
func (f *Frontend) ToggleMultitap() { f.nano.ToggleMultitap() }
|
||||
func (f *Frontend) ViewportSize() (int, int) { return f.vw, f.vh }
|
||||
func (f *Frontend) AspectRatio() float32 { return f.nano.AspectRatio() }
|
||||
func (f *Frontend) AudioSampleRate() int { return f.nano.AudioSampleRate() }
|
||||
func (f *Frontend) FPS() int { return f.nano.VideoFramerate() }
|
||||
func (f *Frontend) Flipped() bool { return f.nano.IsGL() }
|
||||
func (f *Frontend) FrameSize() (int, int) { return f.nano.BaseWidth(), f.nano.BaseHeight() }
|
||||
func (f *Frontend) HasSave() bool { return os.Exists(f.HashPath()) }
|
||||
func (f *Frontend) HashPath() string { return f.storage.GetSavePath() }
|
||||
func (f *Frontend) IsPortrait() bool { return f.nano.IsPortrait() }
|
||||
func (f *Frontend) KbMouseSupport() bool { return f.nano.KbMouseSupport() }
|
||||
func (f *Frontend) PixFormat() uint32 { return f.nano.Video.PixFmt.C }
|
||||
func (f *Frontend) Reset() { f.mu.Lock(); defer f.mu.Unlock(); f.nano.Reset() }
|
||||
func (f *Frontend) RestoreGameState() error { return f.Load() }
|
||||
func (f *Frontend) Rotation() uint { return f.nano.Rot }
|
||||
func (f *Frontend) SRAMPath() string { return f.storage.GetSRAMPath() }
|
||||
func (f *Frontend) SaveGameState() error { return f.Save() }
|
||||
func (f *Frontend) SaveStateName() string { return filepath.Base(f.HashPath()) }
|
||||
func (f *Frontend) Scale() float64 { return f.scale }
|
||||
func (f *Frontend) SetAudioCb(cb func(app.Audio)) { f.onAudio = cb }
|
||||
func (f *Frontend) SetSessionId(name string) { f.storage.SetMainSaveName(name) }
|
||||
func (f *Frontend) SetDataCb(cb func([]byte)) { f.onData = cb }
|
||||
func (f *Frontend) SetVideoCb(ff func(app.Video)) { f.onVideo = ff }
|
||||
func (f *Frontend) Tick() { f.mu.Lock(); f.nano.Run(); f.mu.Unlock() }
|
||||
func (f *Frontend) ViewportRecalculate() { f.mu.Lock(); f.vw, f.vh = f.ViewportCalc(); f.mu.Unlock() }
|
||||
func (f *Frontend) ViewportSize() (int, int) { return f.vw, f.vh }
|
||||
|
||||
func (f *Frontend) Input(port int, device byte, data []byte) {
|
||||
switch Device(device) {
|
||||
case RetroPad:
|
||||
f.nano.InputRetropad(port, data)
|
||||
case Keyboard:
|
||||
f.nano.InputKeyboard(port, data)
|
||||
case Mouse:
|
||||
f.nano.InputMouse(port, data)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Frontend) ViewportCalc() (nw int, nh int) {
|
||||
w, h := f.FrameSize()
|
||||
f.log.Debug().Msgf("Viewport source size: %dx%d", w, h)
|
||||
|
||||
aspect, aw, ah := f.conf.AspectRatio.Keep, f.conf.AspectRatio.Width, f.conf.AspectRatio.Height
|
||||
// calc the aspect ratio
|
||||
if aspect && aw > 0 && ah > 0 {
|
||||
ratio := float64(w) / float64(ah)
|
||||
nw = int(math.Round(float64(ah)*ratio/2) * 2)
|
||||
nh = ah
|
||||
if nw > aw {
|
||||
nw = aw
|
||||
nh = int(math.Round(float64(aw)/ratio/2) * 2)
|
||||
}
|
||||
f.log.Debug().Msgf("Viewport aspect change: %dx%d (%f) -> %dx%d", aw, ah, ratio, nw, nh)
|
||||
} else {
|
||||
nw, nh = w, h
|
||||
}
|
||||
nw, nh = w, h
|
||||
|
||||
if f.IsPortrait() {
|
||||
nw, nh = nh, nw
|
||||
f.log.Debug().Msgf("Set portrait mode")
|
||||
}
|
||||
|
||||
f.log.Info().Msgf("Viewport final size: %dx%d", nw, nh)
|
||||
f.log.Debug().Msgf("viewport: %dx%d -> %dx%d", w, h, nw, nh)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (f *Frontend) Close() {
|
||||
f.log.Debug().Msgf("frontend close called")
|
||||
f.log.Debug().Msgf("frontend close")
|
||||
close(f.done)
|
||||
|
||||
// Save game on quit if it was saved before (shared or click-saved).
|
||||
if f.SaveOnClose && f.HasSave() {
|
||||
f.log.Debug().Msg("Save on quit")
|
||||
if err := f.Save(); err != nil {
|
||||
f.log.Error().Err(err).Msg("save on quit failed")
|
||||
f.mui.Lock()
|
||||
f.nano.Close()
|
||||
|
||||
if f.UniqueSaveDir && !f.HasSave() {
|
||||
if err := f.nano.DeleteSaveDir(); err != nil {
|
||||
f.log.Error().Msgf("couldn't delete save dir: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
close(f.done)
|
||||
f.nano.Close()
|
||||
f.UniqueSaveDir = false
|
||||
f.SaveStateFs = ""
|
||||
|
||||
f.mui.Unlock()
|
||||
f.log.Debug().Msgf("frontend closed")
|
||||
}
|
||||
|
||||
// Save writes the current state to the filesystem.
|
||||
|
|
@ -367,6 +489,10 @@ func (f *Frontend) Load() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (f *Frontend) IsSupported() error {
|
||||
return graphics.TryInit()
|
||||
}
|
||||
|
||||
func (f *Frontend) autosave(periodSec int) {
|
||||
f.log.Info().Msgf("Autosave every [%vs]", periodSec)
|
||||
ticker := time.NewTicker(time.Duration(periodSec) * time.Second)
|
||||
|
|
@ -389,23 +515,31 @@ func (f *Frontend) autosave(periodSec int) {
|
|||
}
|
||||
}
|
||||
|
||||
func NewGameSessionInput() InputState { return [maxPort]State{} }
|
||||
func (f *Frontend) copyFsMaybe(path string) {
|
||||
if f.SaveStateFs == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// setInput sets input state for some player in a game session.
|
||||
func (s *InputState) setInput(player int, data []byte) {
|
||||
atomic.StoreUint32(&s[player].keys, uint32(uint16(data[1])<<8+uint16(data[0])))
|
||||
for i, axes := 0, len(data); i < dpadAxes && i<<1+3 < axes; i++ {
|
||||
axis := i<<1 + 2
|
||||
atomic.StoreInt32(&s[player].axes[i], int32(data[axis+1])<<8+int32(data[axis]))
|
||||
fileName := f.SaveStateFs
|
||||
hasPlaceholder := strings.HasPrefix(f.SaveStateFs, "*")
|
||||
if hasPlaceholder {
|
||||
game := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
|
||||
fileName = strings.Replace(f.SaveStateFs, "*", game, 1)
|
||||
}
|
||||
|
||||
fullPath := filepath.Join(f.nano.SaveDir(), fileName)
|
||||
|
||||
if os.Exists(fullPath) {
|
||||
return
|
||||
}
|
||||
|
||||
storePath := filepath.Dir(path)
|
||||
fsPath := filepath.Join(storePath, fileName)
|
||||
if os.Exists(fsPath) {
|
||||
if err := os.CopyFile(fsPath, fullPath); err != nil {
|
||||
f.log.Error().Err(err).Msgf("fs copy fail")
|
||||
} else {
|
||||
f.log.Debug().Msgf("copied fs %v to %v", fsPath, fullPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// isKeyPressed checks if some button is pressed by any player.
|
||||
func (s *InputState) isKeyPressed(port uint, key int) int {
|
||||
return int((atomic.LoadUint32(&s[port].keys) >> uint(key)) & 1)
|
||||
}
|
||||
|
||||
// isDpadTouched checks if D-pad is used by any player.
|
||||
func (s *InputState) isDpadTouched(port uint, axis uint) (shift int16) {
|
||||
return int16(atomic.LoadInt32(&s[port].axes[axis]))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,12 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"math/rand/v2"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/giongto35/cloud-game/v3/pkg/config"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/logger"
|
||||
|
|
@ -25,14 +26,17 @@ type TestFrontend struct {
|
|||
*Frontend
|
||||
|
||||
corePath string
|
||||
coreExt string
|
||||
gamePath string
|
||||
system string
|
||||
}
|
||||
|
||||
type testRun struct {
|
||||
room string
|
||||
system string
|
||||
rom string
|
||||
emulationTicks int
|
||||
name string
|
||||
room string
|
||||
system string
|
||||
rom string
|
||||
frames int
|
||||
}
|
||||
|
||||
type game struct {
|
||||
|
|
@ -41,9 +45,10 @@ type game struct {
|
|||
}
|
||||
|
||||
var (
|
||||
alwa = game{system: "nes", rom: "Alwa's Awakening (Demo).nes"}
|
||||
sushi = game{system: "gba", rom: "Sushi The Cat.gba"}
|
||||
angua = game{system: "gba", rom: "anguna.gba"}
|
||||
alwa = game{system: "nes", rom: "nes/Alwa's Awakening (Demo).nes"}
|
||||
sushi = game{system: "gba", rom: "gba/Sushi The Cat.gba"}
|
||||
angua = game{system: "gba", rom: "gba/anguna.gba"}
|
||||
rogue = game{system: "dos", rom: "dos/rogue.zip"}
|
||||
)
|
||||
|
||||
// TestMain runs all tests in the main thread in macOS.
|
||||
|
|
@ -66,15 +71,20 @@ func EmulatorMock(room string, system string) *TestFrontend {
|
|||
conf.Emulator.Storage = expand("tests", "storage")
|
||||
|
||||
l := logger.Default()
|
||||
l2 := l.Extend(l.Level(logger.ErrorLevel).With())
|
||||
l2 := l.Extend(l.Level(logger.WarnLevel).With())
|
||||
|
||||
if err := manager.CheckCores(conf.Emulator, l); err != nil {
|
||||
if err := manager.CheckCores(conf.Emulator, l2); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
nano := nanoarch.NewNano(conf.Emulator.LocalPath)
|
||||
nano.SetLogger(l2)
|
||||
|
||||
arch, err := conf.Emulator.Libretro.Cores.Repo.Guess()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// an emu
|
||||
emu := &TestFrontend{
|
||||
Frontend: &Frontend{
|
||||
|
|
@ -83,14 +93,15 @@ func EmulatorMock(room string, system string) *TestFrontend {
|
|||
Path: os.TempDir(),
|
||||
MainSave: room,
|
||||
},
|
||||
input: NewGameSessionInput(),
|
||||
done: make(chan struct{}),
|
||||
th: conf.Emulator.Threads,
|
||||
log: l2,
|
||||
SaveOnClose: false,
|
||||
},
|
||||
corePath: expand(conf.Emulator.GetLibretroCoreConfig(system).Lib),
|
||||
gamePath: expand(conf.Worker.Library.BasePath),
|
||||
coreExt: arch.Ext,
|
||||
gamePath: expand(conf.Library.BasePath),
|
||||
system: system,
|
||||
}
|
||||
emu.linkNano(nano)
|
||||
|
||||
|
|
@ -111,23 +122,36 @@ func DefaultFrontend(room string, system string, rom string) *TestFrontend {
|
|||
// loadRom loads a ROM into the emulator.
|
||||
// The rom will be loaded from emulators' games path.
|
||||
func (emu *TestFrontend) loadRom(game string) {
|
||||
emu.nano.CoreLoad(nanoarch.Metadata{LibPath: emu.corePath})
|
||||
|
||||
gamePath := expand(emu.gamePath, game)
|
||||
|
||||
conf := emu.conf.GetLibretroCoreConfig(gamePath)
|
||||
conf := emu.conf.GetLibretroCoreConfig(emu.system)
|
||||
scale := 1.0
|
||||
if conf.Scale > 1 {
|
||||
scale = conf.Scale
|
||||
}
|
||||
emu.scale = scale
|
||||
|
||||
meta := nanoarch.Metadata{
|
||||
AutoGlContext: conf.AutoGlContext,
|
||||
//FrameDup: f.conf.Libretro.Dup,
|
||||
Hacks: conf.Hacks,
|
||||
HasVFR: conf.VFR,
|
||||
Hid: conf.Hid,
|
||||
IsGlAllowed: conf.IsGlAllowed,
|
||||
LibPath: emu.corePath,
|
||||
Options: conf.Options,
|
||||
Options4rom: conf.Options4rom,
|
||||
UsesLibCo: conf.UsesLibCo,
|
||||
CoreAspectRatio: conf.CoreAspectRatio,
|
||||
LibExt: emu.coreExt,
|
||||
}
|
||||
|
||||
emu.nano.CoreLoad(meta)
|
||||
|
||||
gamePath := expand(emu.gamePath, game)
|
||||
err := emu.nano.LoadGame(gamePath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
w, h := emu.FrameSize()
|
||||
emu.SetViewport(w, h)
|
||||
emu.ViewportRecalculate()
|
||||
}
|
||||
|
||||
// Shutdown closes the emulator and cleans its resources.
|
||||
|
|
@ -138,22 +162,26 @@ func (emu *TestFrontend) Shutdown() {
|
|||
emu.Frontend.Shutdown()
|
||||
}
|
||||
|
||||
// dumpState returns the current emulator state and
|
||||
// the latest saved state for its session.
|
||||
// Locks the emulator.
|
||||
func (emu *TestFrontend) dumpState() (string, string) {
|
||||
// dumpState returns both current and previous emulator save state as MD5 hash string.
|
||||
func (emu *TestFrontend) dumpState() (cur string, prev string) {
|
||||
emu.mu.Lock()
|
||||
bytes, _ := os.ReadFile(emu.HashPath())
|
||||
lastStateHash := hash(bytes)
|
||||
b, _ := os.ReadFile(emu.HashPath())
|
||||
prev = hash(b)
|
||||
emu.mu.Unlock()
|
||||
|
||||
emu.mu.Lock()
|
||||
state, _ := nanoarch.SaveState()
|
||||
b, _ = nanoarch.SaveState()
|
||||
emu.mu.Unlock()
|
||||
stateHash := hash(state)
|
||||
cur = hash(b)
|
||||
|
||||
fmt.Printf("mem: %v, dat: %v\n", stateHash, lastStateHash)
|
||||
return stateHash, lastStateHash
|
||||
return
|
||||
}
|
||||
|
||||
func (emu *TestFrontend) save() ([]byte, error) {
|
||||
emu.mu.Lock()
|
||||
defer emu.mu.Unlock()
|
||||
|
||||
return nanoarch.SaveState()
|
||||
}
|
||||
|
||||
func BenchmarkEmulators(b *testing.B) {
|
||||
|
|
@ -172,7 +200,7 @@ func BenchmarkEmulators(b *testing.B) {
|
|||
for _, bench := range benchmarks {
|
||||
b.Run(bench.name, func(b *testing.B) {
|
||||
s := DefaultFrontend("bench_"+bench.system+"_performance", bench.system, bench.rom)
|
||||
for i := 0; i < b.N; i++ {
|
||||
for range b.N {
|
||||
s.nano.Run()
|
||||
}
|
||||
s.Shutdown()
|
||||
|
|
@ -180,36 +208,32 @@ func BenchmarkEmulators(b *testing.B) {
|
|||
}
|
||||
}
|
||||
|
||||
// Tests a successful emulator state save.
|
||||
func TestSave(t *testing.T) {
|
||||
func TestSavePersistence(t *testing.T) {
|
||||
tests := []testRun{
|
||||
{room: "test_save_ok_00", system: sushi.system, rom: sushi.rom, emulationTicks: 100},
|
||||
{room: "test_save_ok_01", system: angua.system, rom: angua.rom, emulationTicks: 10},
|
||||
{system: sushi.system, rom: sushi.rom, frames: 100},
|
||||
{system: angua.system, rom: angua.rom, frames: 100},
|
||||
{system: rogue.system, rom: rogue.rom, frames: 200},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Logf("Testing [%v] save with [%v]\n", test.system, test.rom)
|
||||
t.Run(fmt.Sprintf("If saves persistent on %v - %v", test.system, test.rom), func(t *testing.T) {
|
||||
front := DefaultFrontend(test.room, test.system, test.rom)
|
||||
|
||||
front := DefaultFrontend(test.room, test.system, test.rom)
|
||||
for test.frames > 0 {
|
||||
front.Tick()
|
||||
test.frames--
|
||||
}
|
||||
|
||||
for test.emulationTicks > 0 {
|
||||
front.Tick()
|
||||
test.emulationTicks--
|
||||
}
|
||||
for range 10 {
|
||||
v, _ := front.save()
|
||||
if v == nil || len(v) == 0 {
|
||||
t.Errorf("couldn't persist the state")
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("[%-14v] ", "before save")
|
||||
_, _ = front.dumpState()
|
||||
if err := front.Save(); err != nil {
|
||||
t.Errorf("Save fail %v", err)
|
||||
}
|
||||
fmt.Printf("[%-14v] ", "after save")
|
||||
snapshot1, snapshot2 := front.dumpState()
|
||||
|
||||
if snapshot1 != snapshot2 {
|
||||
t.Errorf("It seems rom state save has failed: %v != %v", snapshot1, snapshot2)
|
||||
}
|
||||
|
||||
front.Shutdown()
|
||||
front.Shutdown()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -222,9 +246,9 @@ func TestSave(t *testing.T) {
|
|||
// Compare states (a) and (b), should be =.
|
||||
func TestLoad(t *testing.T) {
|
||||
tests := []testRun{
|
||||
{room: "test_load_00", system: alwa.system, rom: alwa.rom, emulationTicks: 100},
|
||||
{room: "test_load_01", system: sushi.system, rom: sushi.rom, emulationTicks: 1000},
|
||||
{room: "test_load_02", system: angua.system, rom: angua.rom, emulationTicks: 100},
|
||||
{room: "test_load_00", system: alwa.system, rom: alwa.rom, frames: 100},
|
||||
//{room: "test_load_01", system: sushi.system, rom: sushi.rom, frames: 1000},
|
||||
//{room: "test_load_02", system: angua.system, rom: angua.rom, frames: 100},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
|
@ -232,31 +256,26 @@ func TestLoad(t *testing.T) {
|
|||
|
||||
mock := DefaultFrontend(test.room, test.system, test.rom)
|
||||
|
||||
fmt.Printf("[%-14v] ", "initial")
|
||||
mock.dumpState()
|
||||
|
||||
for ticks := test.emulationTicks; ticks > 0; ticks-- {
|
||||
for ticks := test.frames; ticks > 0; ticks-- {
|
||||
mock.Tick()
|
||||
}
|
||||
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-- {
|
||||
for ticks := test.frames; ticks > 0; ticks-- {
|
||||
mock.Tick()
|
||||
}
|
||||
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 {
|
||||
|
|
@ -273,11 +292,11 @@ func TestStateConcurrency(t *testing.T) {
|
|||
seed int
|
||||
}{
|
||||
{
|
||||
run: testRun{room: "test_concurrency_00", system: sushi.system, rom: sushi.rom, emulationTicks: 120},
|
||||
run: testRun{room: "test_concurrency_00", system: alwa.system, rom: alwa.rom, frames: 120},
|
||||
seed: 42,
|
||||
},
|
||||
{
|
||||
run: testRun{room: "test_concurrency_01", system: angua.system, rom: angua.rom, emulationTicks: 300},
|
||||
run: testRun{room: "test_concurrency_01", system: alwa.system, rom: alwa.rom, frames: 300},
|
||||
seed: 42 + 42,
|
||||
},
|
||||
}
|
||||
|
|
@ -304,15 +323,13 @@ func TestStateConcurrency(t *testing.T) {
|
|||
|
||||
_ = mock.Save()
|
||||
|
||||
for i := 0; i < test.run.emulationTicks; i++ {
|
||||
for i := range test.run.frames {
|
||||
qLock.Lock()
|
||||
mock.Tick()
|
||||
qLock.Unlock()
|
||||
|
||||
i := i
|
||||
if lucky() && !lucky() {
|
||||
ops.Add(1)
|
||||
go func() {
|
||||
ops.Go(func() {
|
||||
qLock.Lock()
|
||||
defer qLock.Unlock()
|
||||
|
||||
|
|
@ -323,20 +340,10 @@ func TestStateConcurrency(t *testing.T) {
|
|||
_ = 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, i+1)
|
||||
}
|
||||
ops.Done()
|
||||
}()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -345,18 +352,16 @@ func TestStateConcurrency(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestConcurrentInput(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
state := NewGameSessionInput()
|
||||
events := 1000
|
||||
wg.Add(2 * events)
|
||||
func TestStartStop(t *testing.T) {
|
||||
f1 := DefaultFrontend("sushi", sushi.system, sushi.rom)
|
||||
go f1.Start()
|
||||
time.Sleep(1 * time.Second)
|
||||
f1.Close()
|
||||
|
||||
for i := 0; i < events; i++ {
|
||||
player := rand.Intn(maxPort)
|
||||
go func() { state.setInput(player, []byte{0, 1}); wg.Done() }()
|
||||
go func() { state.isKeyPressed(uint(player), 100); wg.Done() }()
|
||||
}
|
||||
wg.Wait()
|
||||
f2 := DefaultFrontend("sushi", sushi.system, sushi.rom)
|
||||
go f2.Start()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
f2.Close()
|
||||
}
|
||||
|
||||
// expand joins a list of file path elements.
|
||||
|
|
@ -369,4 +374,4 @@ func expand(p ...string) string {
|
|||
func hash(bytes []byte) string { return fmt.Sprintf("%x", md5.Sum(bytes)) }
|
||||
|
||||
// lucky returns random boolean.
|
||||
func lucky() bool { return rand.Intn(2) == 1 }
|
||||
func lucky() bool { return rand.IntN(2) == 1 }
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ typedef void (APIENTRYP GPREADPIXELS)(GLint x, GLint y, GLsizei width, GLsizei h
|
|||
typedef void (APIENTRYP GPRENDERBUFFERSTORAGE)(GLenum target, GLenum internalformat, GLsizei width, GLsizei height);
|
||||
typedef void (APIENTRYP GPTEXIMAGE2D)(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const void *pixels);
|
||||
typedef void (APIENTRYP GPTEXPARAMETERI)(GLenum target, GLenum pname, GLint param);
|
||||
typedef void (APIENTRYP GPPIXELSTOREI)(GLenum pname, GLint param);
|
||||
|
||||
static const GLubyte *getString(GPGETSTRING ptr, GLenum name) { return (*ptr)(name); }
|
||||
static GLenum getError(GPGETERROR ptr) { return (*ptr)(); }
|
||||
|
|
@ -113,6 +114,7 @@ static void deleteTextures(GPDELETETEXTURES ptr, GLsizei n, const GLuint *textur
|
|||
static void readPixels(GPREADPIXELS ptr, GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void *pixels) {
|
||||
(*ptr)(x, y, width, height, format, type, pixels);
|
||||
}
|
||||
static void pixelStorei(GPPIXELSTOREI ptr, GLenum pname, GLint param) { (*ptr)(pname, param); }
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
|
|
@ -144,6 +146,8 @@ const (
|
|||
UnsignedShort5551 = 0x8034
|
||||
UnsignedShort565 = 0x8363
|
||||
UnsignedInt8888Rev = 0x8367
|
||||
|
||||
PackAlignment = 0x0D05
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -165,6 +169,7 @@ var (
|
|||
gpDeleteFramebuffers C.GPDELETEFRAMEBUFFERS
|
||||
gpDeleteTextures C.GPDELETETEXTURES
|
||||
gpReadPixels C.GPREADPIXELS
|
||||
gpPixelStorei C.GPPIXELSTOREI
|
||||
)
|
||||
|
||||
func InitWithProcAddrFunc(getProcAddr func(name string) unsafe.Pointer) error {
|
||||
|
|
@ -205,6 +210,9 @@ func InitWithProcAddrFunc(getProcAddr func(name string) unsafe.Pointer) error {
|
|||
if gpReadPixels == nil {
|
||||
return errors.New("glReadPixels")
|
||||
}
|
||||
if gpPixelStorei = (C.GPPIXELSTOREI)(getProcAddr("glPixelStorei")); gpPixelStorei == nil {
|
||||
return errors.New("glPixelStorei")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -257,6 +265,9 @@ func DeleteTextures(n int32, textures *uint32) {
|
|||
func ReadPixels(x int32, y int32, width int32, height int32, format uint32, xtype uint32, pixels unsafe.Pointer) {
|
||||
C.readPixels(gpReadPixels, (C.GLint)(x), (C.GLint)(y), (C.GLsizei)(width), (C.GLsizei)(height), (C.GLenum)(format), (C.GLenum)(xtype), pixels)
|
||||
}
|
||||
func PixelStorei(pname uint32, param int32) {
|
||||
C.pixelStorei(gpPixelStorei, (C.GLenum)(pname), (C.GLint)(param))
|
||||
}
|
||||
|
||||
func GetError() uint32 { return (uint32)(C.getError(gpGetError)) }
|
||||
|
||||
|
|
|
|||
|
|
@ -9,24 +9,6 @@ import (
|
|||
"github.com/giongto35/cloud-game/v3/pkg/worker/caged/libretro/graphics/gl"
|
||||
)
|
||||
|
||||
type (
|
||||
offscreenSetup struct {
|
||||
tex uint32
|
||||
fbo uint32
|
||||
rbo uint32
|
||||
|
||||
width int32
|
||||
height int32
|
||||
|
||||
pixType uint32
|
||||
pixFormat uint32
|
||||
|
||||
hasDepth bool
|
||||
hasStencil bool
|
||||
}
|
||||
PixelFormat int
|
||||
)
|
||||
|
||||
type Context int
|
||||
|
||||
const (
|
||||
|
|
@ -37,11 +19,12 @@ const (
|
|||
CtxOpenGlEs3
|
||||
CtxOpenGlEsVersion
|
||||
CtxVulkan
|
||||
|
||||
CtxUnknown = math.MaxInt32 - 1
|
||||
CtxDummy = math.MaxInt32
|
||||
)
|
||||
|
||||
type PixelFormat int
|
||||
|
||||
const (
|
||||
UnsignedShort5551 PixelFormat = iota
|
||||
UnsignedShort565
|
||||
|
|
@ -49,99 +32,91 @@ const (
|
|||
)
|
||||
|
||||
var (
|
||||
opt = offscreenSetup{}
|
||||
buf []byte
|
||||
fbo, tex, rbo uint32
|
||||
hasDepth bool
|
||||
pixType, pixFormat uint32
|
||||
buf []byte
|
||||
bufPtr unsafe.Pointer
|
||||
)
|
||||
|
||||
func initContext(getProcAddr func(name string) unsafe.Pointer) {
|
||||
if err := gl.InitWithProcAddrFunc(getProcAddr); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
gl.PixelStorei(gl.PackAlignment, 1)
|
||||
}
|
||||
|
||||
func initFramebuffer(w int, h int, hasDepth bool, hasStencil bool) error {
|
||||
opt.width = int32(w)
|
||||
opt.height = int32(h)
|
||||
opt.hasDepth = hasDepth
|
||||
opt.hasStencil = hasStencil
|
||||
|
||||
// texture init
|
||||
gl.GenTextures(1, &opt.tex)
|
||||
gl.BindTexture(gl.Texture2d, opt.tex)
|
||||
func initFramebuffer(width, height int, depth, stencil bool) error {
|
||||
w, h := int32(width), int32(height)
|
||||
hasDepth = depth
|
||||
|
||||
gl.GenTextures(1, &tex)
|
||||
gl.BindTexture(gl.Texture2d, tex)
|
||||
gl.TexParameteri(gl.Texture2d, gl.TextureMinFilter, gl.NEAREST)
|
||||
gl.TexParameteri(gl.Texture2d, gl.TextureMagFilter, gl.NEAREST)
|
||||
|
||||
gl.TexImage2D(gl.Texture2d, 0, gl.RGBA8, opt.width, opt.height, 0, opt.pixType, opt.pixFormat, nil)
|
||||
gl.TexImage2D(gl.Texture2d, 0, gl.RGBA8, w, h, 0, pixType, pixFormat, nil)
|
||||
gl.BindTexture(gl.Texture2d, 0)
|
||||
|
||||
// framebuffer init
|
||||
gl.GenFramebuffers(1, &opt.fbo)
|
||||
gl.BindFramebuffer(gl.FRAMEBUFFER, opt.fbo)
|
||||
gl.GenFramebuffers(1, &fbo)
|
||||
gl.BindFramebuffer(gl.FRAMEBUFFER, fbo)
|
||||
gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.ColorAttachment0, gl.Texture2d, tex, 0)
|
||||
|
||||
gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.ColorAttachment0, gl.Texture2d, 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.Depth24Stencil8, opt.width, opt.height)
|
||||
gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DepthStencilAttachment, gl.RENDERBUFFER, opt.rbo)
|
||||
} else {
|
||||
gl.RenderbufferStorage(gl.RENDERBUFFER, gl.DepthComponent24, opt.width, opt.height)
|
||||
gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DepthAttachment, gl.RENDERBUFFER, opt.rbo)
|
||||
if depth {
|
||||
gl.GenRenderbuffers(1, &rbo)
|
||||
gl.BindRenderbuffer(gl.RENDERBUFFER, rbo)
|
||||
format, attachment := uint32(gl.DepthComponent24), uint32(gl.DepthAttachment)
|
||||
if stencil {
|
||||
format, attachment = gl.Depth24Stencil8, gl.DepthStencilAttachment
|
||||
}
|
||||
gl.RenderbufferStorage(gl.RENDERBUFFER, format, w, h)
|
||||
gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, attachment, gl.RENDERBUFFER, rbo)
|
||||
gl.BindRenderbuffer(gl.RENDERBUFFER, 0)
|
||||
}
|
||||
|
||||
if status := gl.CheckFramebufferStatus(gl.FRAMEBUFFER); status != gl.FramebufferComplete {
|
||||
return fmt.Errorf("invalid framebuffer (0x%X)", status)
|
||||
return fmt.Errorf("framebuffer incomplete: 0x%X", status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func destroyFramebuffer() {
|
||||
if opt.hasDepth {
|
||||
gl.DeleteRenderbuffers(1, &opt.rbo)
|
||||
if hasDepth {
|
||||
gl.DeleteRenderbuffers(1, &rbo)
|
||||
}
|
||||
gl.DeleteFramebuffers(1, &opt.fbo)
|
||||
gl.DeleteTextures(1, &opt.tex)
|
||||
gl.DeleteFramebuffers(1, &fbo)
|
||||
gl.DeleteTextures(1, &tex)
|
||||
}
|
||||
|
||||
func ReadFramebuffer(bytes, w, h uint) []byte {
|
||||
data := buf[:bytes]
|
||||
gl.BindFramebuffer(gl.FRAMEBUFFER, opt.fbo)
|
||||
gl.ReadPixels(0, 0, int32(w), int32(h), opt.pixType, opt.pixFormat, unsafe.Pointer(&data[0]))
|
||||
gl.BindFramebuffer(gl.FRAMEBUFFER, 0)
|
||||
return data
|
||||
func ReadFramebuffer(size, w, h uint) []byte {
|
||||
gl.BindFramebuffer(gl.FRAMEBUFFER, fbo)
|
||||
gl.ReadPixels(0, 0, int32(w), int32(h), pixType, pixFormat, bufPtr)
|
||||
return buf[:size]
|
||||
}
|
||||
|
||||
func getFbo() uint32 { return opt.fbo }
|
||||
|
||||
func SetBuffer(size int) { buf = make([]byte, size) }
|
||||
func SetBuffer(size int) {
|
||||
buf = make([]byte, size)
|
||||
bufPtr = unsafe.Pointer(&buf[0])
|
||||
}
|
||||
|
||||
func SetPixelFormat(format PixelFormat) error {
|
||||
switch format {
|
||||
case UnsignedShort5551:
|
||||
opt.pixFormat = gl.UnsignedShort5551
|
||||
opt.pixType = gl.BGRA
|
||||
pixFormat, pixType = gl.UnsignedShort5551, gl.BGRA
|
||||
case UnsignedShort565:
|
||||
opt.pixFormat = gl.UnsignedShort565
|
||||
opt.pixType = gl.RGB
|
||||
pixFormat, pixType = gl.UnsignedShort565, gl.RGB
|
||||
case UnsignedInt8888Rev:
|
||||
opt.pixFormat = gl.UnsignedInt8888Rev
|
||||
opt.pixType = gl.BGRA
|
||||
pixFormat, pixType = gl.UnsignedInt8888Rev, gl.BGRA
|
||||
default:
|
||||
return errors.New("unknown pixel format")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetGLVersionInfo() string { return get(gl.VERSION) }
|
||||
func GetGLVendorInfo() string { return get(gl.VENDOR) }
|
||||
func GetGLRendererInfo() string { return get(gl.RENDERER) }
|
||||
func GetGLSLInfo() string { return get(gl.ShadingLanguageVersion) }
|
||||
func GetGLError() uint32 { return gl.GetError() }
|
||||
func GLInfo() (version, vendor, renderer, glsl string) {
|
||||
return gl.GoStr(gl.GetString(gl.VERSION)),
|
||||
gl.GoStr(gl.GetString(gl.VENDOR)),
|
||||
gl.GoStr(gl.GetString(gl.RENDERER)),
|
||||
gl.GoStr(gl.GetString(gl.ShadingLanguageVersion))
|
||||
}
|
||||
|
||||
func get(name uint32) string { return gl.GoStr(gl.GetString(name)) }
|
||||
func GlFbo() uint32 { return fbo }
|
||||
|
|
|
|||
|
|
@ -4,21 +4,17 @@ import (
|
|||
"fmt"
|
||||
"unsafe"
|
||||
|
||||
"github.com/giongto35/cloud-game/v3/pkg/logger"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/worker/thread"
|
||||
"github.com/veandco/go-sdl2/sdl"
|
||||
)
|
||||
|
||||
type SDL struct {
|
||||
glWCtx sdl.GLContext
|
||||
w *sdl.Window
|
||||
log *logger.Logger
|
||||
w *sdl.Window
|
||||
ctx sdl.GLContext
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Ctx Context
|
||||
W int
|
||||
H int
|
||||
W, H int
|
||||
GLAutoContext bool
|
||||
GLVersionMajor uint
|
||||
GLVersionMinor uint
|
||||
|
|
@ -26,114 +22,79 @@ type Config struct {
|
|||
GLHasStencil bool
|
||||
}
|
||||
|
||||
// NewSDLContext initializes SDL/OpenGL context.
|
||||
// Uses main thread lock (see thread/mainthread).
|
||||
func NewSDLContext(cfg Config, log *logger.Logger) (*SDL, error) {
|
||||
log.Debug().Msg("[SDL/OpenGL] initialization...")
|
||||
|
||||
func NewSDLContext(cfg Config) (*SDL, error) {
|
||||
if err := sdl.Init(sdl.INIT_VIDEO); err != nil {
|
||||
return nil, fmt.Errorf("SDL initialization fail: %w", err)
|
||||
return nil, fmt.Errorf("sdl: %w", err)
|
||||
}
|
||||
|
||||
display := SDL{log: log}
|
||||
|
||||
if cfg.GLAutoContext {
|
||||
log.Debug().Msgf("[OpenGL] CONTEXT_AUTO (type: %v v%v.%v)", cfg.Ctx, cfg.GLVersionMajor, cfg.GLVersionMinor)
|
||||
} else {
|
||||
switch cfg.Ctx {
|
||||
case CtxOpenGlCore:
|
||||
display.setAttribute(sdl.GL_CONTEXT_PROFILE_MASK, sdl.GL_CONTEXT_PROFILE_CORE)
|
||||
log.Debug().Msgf("[OpenGL] CONTEXT_PROFILE_CORE")
|
||||
case CtxOpenGlEs2:
|
||||
display.setAttribute(sdl.GL_CONTEXT_PROFILE_MASK, sdl.GL_CONTEXT_PROFILE_ES)
|
||||
display.setAttribute(sdl.GL_CONTEXT_MAJOR_VERSION, 3)
|
||||
display.setAttribute(sdl.GL_CONTEXT_MINOR_VERSION, 0)
|
||||
log.Debug().Msgf("[OpenGL] CONTEXT_PROFILE_ES 3.0")
|
||||
case CtxOpenGl:
|
||||
if cfg.GLVersionMajor >= 3 {
|
||||
display.setAttribute(sdl.GL_CONTEXT_PROFILE_MASK, sdl.GL_CONTEXT_PROFILE_COMPATIBILITY)
|
||||
}
|
||||
log.Debug().Msgf("[OpenGL] CONTEXT_PROFILE_COMPATIBILITY")
|
||||
default:
|
||||
log.Error().Msgf("[OpenGL] Unsupported hw context: %v", cfg.Ctx)
|
||||
if !cfg.GLAutoContext {
|
||||
if err := setGLAttrs(cfg.Ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
// In OSX 10.14+ window creation and context creation must happen in the main thread
|
||||
thread.Main(func() { display.w, display.glWCtx, err = createWindow() })
|
||||
w, err := sdl.CreateWindow("cloud-retro", sdl.WINDOWPOS_UNDEFINED, sdl.WINDOWPOS_UNDEFINED, 1, 1, sdl.WINDOW_OPENGL|sdl.WINDOW_HIDDEN)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("window fail: %w", err)
|
||||
return nil, fmt.Errorf("window: %w", err)
|
||||
}
|
||||
|
||||
if err := display.BindContext(); err != nil {
|
||||
return nil, fmt.Errorf("bind context fail: %w", err)
|
||||
ctx, err := w.GLCreateContext()
|
||||
if err != nil {
|
||||
err1 := w.Destroy()
|
||||
return nil, fmt.Errorf("gl context: %w, destroy err: %w", err, err1)
|
||||
}
|
||||
|
||||
if err = w.GLMakeCurrent(ctx); err != nil {
|
||||
return nil, fmt.Errorf("gl bind: %w", err)
|
||||
}
|
||||
|
||||
initContext(sdl.GLGetProcAddress)
|
||||
if err := initFramebuffer(cfg.W, cfg.H, cfg.GLHasDepth, cfg.GLHasStencil); err != nil {
|
||||
return nil, fmt.Errorf("OpenGL initialization fail: %w", err)
|
||||
|
||||
if err = initFramebuffer(cfg.W, cfg.H, cfg.GLHasDepth, cfg.GLHasStencil); err != nil {
|
||||
return nil, fmt.Errorf("fbo: %w", err)
|
||||
}
|
||||
|
||||
return &SDL{w: w, ctx: ctx}, nil
|
||||
}
|
||||
|
||||
func setGLAttrs(ctx Context) error {
|
||||
set := sdl.GLSetAttribute
|
||||
switch ctx {
|
||||
case CtxOpenGlCore:
|
||||
return set(sdl.GL_CONTEXT_PROFILE_MASK, sdl.GL_CONTEXT_PROFILE_CORE)
|
||||
case CtxOpenGlEs2:
|
||||
for _, a := range [][2]int{
|
||||
{sdl.GL_CONTEXT_PROFILE_MASK, sdl.GL_CONTEXT_PROFILE_ES},
|
||||
{sdl.GL_CONTEXT_MAJOR_VERSION, 3},
|
||||
{sdl.GL_CONTEXT_MINOR_VERSION, 0},
|
||||
} {
|
||||
if err := set(sdl.GLattr(a[0]), a[1]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
case CtxOpenGl:
|
||||
return set(sdl.GL_CONTEXT_PROFILE_MASK, sdl.GL_CONTEXT_PROFILE_COMPATIBILITY)
|
||||
default:
|
||||
return fmt.Errorf("unsupported gl context: %v", ctx)
|
||||
}
|
||||
return &display, nil
|
||||
}
|
||||
|
||||
// Deinit destroys SDL/OpenGL context.
|
||||
// Uses main thread lock (see thread/mainthread).
|
||||
func (s *SDL) Deinit() error {
|
||||
s.log.Debug().Msg("[SDL/OpenGL] shutdown...")
|
||||
destroyFramebuffer()
|
||||
var err error
|
||||
// In OSX 10.14+ window deletion must happen in the main thread
|
||||
thread.Main(func() {
|
||||
err = s.destroyWindow()
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("[SDL/OpenGL] deinit fail: %w", err)
|
||||
}
|
||||
sdl.GLDeleteContext(s.ctx)
|
||||
err := s.w.Destroy()
|
||||
sdl.Quit()
|
||||
s.log.Debug().Msgf("[SDL/OpenGL] shutdown codes:(%v, %v)", sdl.GetError(), GetGLError())
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
// createWindow creates a fake SDL window just for OpenGL initialization purposes.
|
||||
func createWindow() (*sdl.Window, sdl.GLContext, error) {
|
||||
w, err := sdl.CreateWindow(
|
||||
"CloudRetro dummy window",
|
||||
sdl.WINDOWPOS_UNDEFINED, sdl.WINDOWPOS_UNDEFINED,
|
||||
1, 1,
|
||||
sdl.WINDOW_OPENGL|sdl.WINDOW_HIDDEN,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("window creation fail: %w", err)
|
||||
}
|
||||
glWCtx, err := w.GLCreateContext()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("window OpenGL context fail: %w", err)
|
||||
}
|
||||
return w, glWCtx, nil
|
||||
}
|
||||
func (s *SDL) BindContext() error { return s.w.GLMakeCurrent(s.ctx) }
|
||||
func GlProcAddress(proc string) unsafe.Pointer { return sdl.GLGetProcAddress(proc) }
|
||||
|
||||
// destroyWindow destroys previously created SDL window.
|
||||
func (s *SDL) destroyWindow() error {
|
||||
if err := s.BindContext(); err != nil {
|
||||
func TryInit() error {
|
||||
if err := sdl.Init(sdl.INIT_VIDEO); err != nil {
|
||||
return err
|
||||
}
|
||||
sdl.GLDeleteContext(s.glWCtx)
|
||||
if err := s.w.Destroy(); err != nil {
|
||||
return fmt.Errorf("window destroy fail: %w", err)
|
||||
}
|
||||
sdl.Quit()
|
||||
return nil
|
||||
}
|
||||
|
||||
// BindContext explicitly binds context to current thread.
|
||||
func (s *SDL) BindContext() error { return s.w.GLMakeCurrent(s.glWCtx) }
|
||||
|
||||
// setAttribute tries to set a GL attribute or prints error.
|
||||
func (s *SDL) setAttribute(attr sdl.GLattr, value int) {
|
||||
if err := sdl.GLSetAttribute(attr, value); err != nil {
|
||||
s.log.Error().Err(err).Msg("[SDL] attribute")
|
||||
}
|
||||
}
|
||||
|
||||
func GetGlFbo() uint32 { return getFbo() }
|
||||
|
||||
func GetGlProcAddress(proc string) unsafe.Pointer { return sdl.GLGetProcAddress(proc) }
|
||||
|
|
|
|||
|
|
@ -47,11 +47,15 @@ func (d GrabDownloader) Request(dest string, urls ...Download) (ok []string, noo
|
|||
r := resp.Request
|
||||
if err := resp.Err(); err != nil {
|
||||
d.log.Error().Err(err).Msgf("download [%s] %s has failed: %v", r.Label, r.URL(), err)
|
||||
if resp.HTTPResponse.StatusCode == 404 {
|
||||
if resp.HTTPResponse != nil && resp.HTTPResponse.StatusCode == 404 {
|
||||
nook = append(nook, resp.Request.Label)
|
||||
}
|
||||
} else {
|
||||
d.log.Info().Msgf("Downloaded [%v] [%s] -> %s", resp.HTTPResponse.Status, r.Label, resp.Filename)
|
||||
status := ""
|
||||
if resp.HTTPResponse != nil {
|
||||
status = resp.HTTPResponse.Status
|
||||
}
|
||||
d.log.Info().Msgf("Downloaded [%v] [%s] -> %s", status, r.Label, resp.Filename)
|
||||
ok = append(ok, resp.Filename)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,64 +1,50 @@
|
|||
package manager
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/giongto35/cloud-game/v3/pkg/config"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/logger"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/worker/caged/libretro/repo"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/worker/caged/libretro/repo/arch"
|
||||
"github.com/gofrs/flock"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/os"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
BasicManager
|
||||
|
||||
arch arch.Info
|
||||
repo repo.Repository
|
||||
altRepo repo.Repository
|
||||
arch ArchInfo
|
||||
repo Repository
|
||||
altRepo Repository
|
||||
client Downloader
|
||||
fmu *flock.Flock
|
||||
fmu *os.Flock
|
||||
log *logger.Logger
|
||||
}
|
||||
|
||||
func NewRemoteHttpManager(conf config.LibretroConfig, log *logger.Logger) Manager {
|
||||
repoConf := conf.Cores.Repo.Main
|
||||
altRepoConf := conf.Cores.Repo.Secondary
|
||||
// used for synchronization of multiple process
|
||||
fileLock := conf.Cores.Repo.ExtLock
|
||||
if fileLock == "" {
|
||||
fileLock = os.TempDir() + string(os.PathSeparator) + "cloud_game.lock"
|
||||
}
|
||||
log.Debug().Msgf("Using .lock file: %v", fileLock)
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(fileLock), 0770); err != nil {
|
||||
log.Error().Err(err).Msgf("couldn't create lock")
|
||||
} else {
|
||||
f, err := os.Create(fileLock)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("couldn't create lock")
|
||||
}
|
||||
_ = f.Close()
|
||||
// used for synchronization of multiple process
|
||||
flock, err := os.NewFileLock(conf.Cores.Repo.ExtLock)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("couldn't make file lock")
|
||||
}
|
||||
ar, err := arch.Guess()
|
||||
|
||||
arch, err := conf.Cores.Repo.Guess()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("couldn't get Libretro core file extension")
|
||||
}
|
||||
|
||||
m := Manager{
|
||||
BasicManager: BasicManager{Conf: conf},
|
||||
arch: ar,
|
||||
arch: ArchInfo(arch),
|
||||
client: NewDefaultDownloader(log),
|
||||
fmu: flock.New(fileLock),
|
||||
fmu: flock,
|
||||
log: log,
|
||||
}
|
||||
|
||||
if repoConf.Type != "" {
|
||||
m.repo = repo.New(repoConf.Type, repoConf.Url, repoConf.Compression, "buildbot")
|
||||
m.repo = NewRepo(repoConf.Type, repoConf.Url, repoConf.Compression, "buildbot")
|
||||
}
|
||||
if altRepoConf.Type != "" {
|
||||
m.altRepo = repo.New(altRepoConf.Type, altRepoConf.Url, altRepoConf.Compression, "")
|
||||
m.altRepo = NewRepo(altRepoConf.Type, altRepoConf.Url, altRepoConf.Compression, "")
|
||||
}
|
||||
|
||||
return m
|
||||
|
|
@ -71,8 +57,7 @@ func CheckCores(conf config.Emulator, log *logger.Logger) error {
|
|||
log.Info().Msg("Starting Libretro cores sync...")
|
||||
coreManager := NewRemoteHttpManager(conf.Libretro, log)
|
||||
// make a dir for cores
|
||||
dir := coreManager.Conf.GetCoresStorePath()
|
||||
if err := os.MkdirAll(dir, os.ModeDir|os.ModePerm); err != nil {
|
||||
if err := os.MakeDirAll(coreManager.Conf.GetCoresStorePath()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := coreManager.Sync(); err != nil {
|
||||
|
|
@ -94,7 +79,7 @@ func (m *Manager) Sync() error {
|
|||
}
|
||||
}()
|
||||
|
||||
installed, err := m.GetInstalled(m.arch.LibExt)
|
||||
installed, err := m.GetInstalled(m.arch.Ext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -105,9 +90,9 @@ func (m *Manager) Sync() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) getCoreUrls(names []string, repo repo.Repository) (urls []Download) {
|
||||
func (m *Manager) getCoreUrls(names []string, repo Repository) (urls []Download) {
|
||||
for _, c := range names {
|
||||
urls = append(urls, Download{Key: c, Address: repo.GetCoreUrl(c, m.arch)})
|
||||
urls = append(urls, Download{Key: c, Address: repo.CoreUrl(c, m.arch)})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -150,7 +135,7 @@ func (m *Manager) download(cores []config.CoreInfo) (failed []string) {
|
|||
return
|
||||
}
|
||||
|
||||
func (m *Manager) down(cores []string, repo repo.Repository) (failed []string) {
|
||||
func (m *Manager) down(cores []string, repo Repository) (failed []string) {
|
||||
if len(cores) == 0 || repo == nil {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
65
pkg/worker/caged/libretro/manager/repository.go
Normal file
65
pkg/worker/caged/libretro/manager/repository.go
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
package manager
|
||||
|
||||
import "strings"
|
||||
|
||||
type ArchInfo struct {
|
||||
Arch string
|
||||
Ext string
|
||||
Os string
|
||||
Vendor string
|
||||
}
|
||||
|
||||
type Data struct {
|
||||
Url string
|
||||
Compression string
|
||||
}
|
||||
|
||||
type Repository interface {
|
||||
CoreUrl(file string, info ArchInfo) (url string)
|
||||
}
|
||||
|
||||
// Repo defines a simple zip file containing all the cores that will be extracted as is.
|
||||
type Repo struct {
|
||||
Address string
|
||||
Compression string
|
||||
}
|
||||
|
||||
func (r Repo) CoreUrl(_ string, _ ArchInfo) string { return r.Address }
|
||||
|
||||
type Buildbot struct{ Repo }
|
||||
|
||||
func (r Buildbot) CoreUrl(file string, info ArchInfo) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(r.Address + "/")
|
||||
if info.Vendor != "" {
|
||||
sb.WriteString(info.Vendor + "/")
|
||||
}
|
||||
sb.WriteString(info.Os + "/" + info.Arch + "/latest/" + file + info.Ext)
|
||||
if r.Compression != "" {
|
||||
sb.WriteString("." + r.Compression)
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
type Github struct{ Buildbot }
|
||||
|
||||
func (r Github) CoreUrl(file string, info ArchInfo) string {
|
||||
return r.Buildbot.CoreUrl(file, info) + "?raw=true"
|
||||
}
|
||||
|
||||
func NewRepo(kind string, url string, compression string, defaultRepo string) Repository {
|
||||
var repository Repository
|
||||
switch kind {
|
||||
case "buildbot":
|
||||
repository = Buildbot{Repo{Address: url, Compression: compression}}
|
||||
case "github":
|
||||
repository = Github{Buildbot{Repo{Address: url, Compression: compression}}}
|
||||
case "raw":
|
||||
repository = Repo{Address: url, Compression: "zip"}
|
||||
default:
|
||||
if defaultRepo != "" {
|
||||
repository = NewRepo(defaultRepo, url, compression, "")
|
||||
}
|
||||
}
|
||||
return repository
|
||||
}
|
||||
61
pkg/worker/caged/libretro/manager/repository_test.go
Normal file
61
pkg/worker/caged/libretro/manager/repository_test.go
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
package manager
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCoreUrl(t *testing.T) {
|
||||
testAddress := "https://test.me"
|
||||
tests := []struct {
|
||||
arch ArchInfo
|
||||
compress string
|
||||
f string
|
||||
repo string
|
||||
result string
|
||||
}{
|
||||
{
|
||||
arch: ArchInfo{Arch: "x86_64", Ext: ".so", Os: "linux"},
|
||||
f: "uber_core",
|
||||
repo: "buildbot",
|
||||
result: testAddress + "/" + "linux/x86_64/latest/uber_core.so",
|
||||
},
|
||||
{
|
||||
arch: ArchInfo{Arch: "x86_64", Ext: ".so", Os: "linux"},
|
||||
compress: "zip",
|
||||
f: "uber_core",
|
||||
repo: "buildbot",
|
||||
result: testAddress + "/" + "linux/x86_64/latest/uber_core.so.zip",
|
||||
},
|
||||
{
|
||||
arch: ArchInfo{Arch: "x86_64", Ext: ".dylib", Os: "osx", Vendor: "apple"},
|
||||
f: "uber_core",
|
||||
repo: "buildbot",
|
||||
result: testAddress + "/" + "apple/osx/x86_64/latest/uber_core.dylib",
|
||||
},
|
||||
{
|
||||
arch: ArchInfo{Os: "linux", Arch: "x86_64", Ext: ".so"},
|
||||
f: "uber_core",
|
||||
repo: "github",
|
||||
result: testAddress + "/" + "linux/x86_64/latest/uber_core.so?raw=true",
|
||||
},
|
||||
{
|
||||
arch: ArchInfo{Os: "linux", Arch: "x86_64", Ext: ".so"},
|
||||
compress: "zip",
|
||||
f: "uber_core",
|
||||
repo: "github",
|
||||
result: testAddress + "/" + "linux/x86_64/latest/uber_core.so.zip?raw=true",
|
||||
},
|
||||
{
|
||||
arch: ArchInfo{Os: "osx", Arch: "x86_64", Vendor: "apple", Ext: ".dylib"},
|
||||
f: "uber_core",
|
||||
repo: "github",
|
||||
result: testAddress + "/" + "apple/osx/x86_64/latest/uber_core.dylib?raw=true",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
r := NewRepo(test.repo, testAddress, test.compress, "")
|
||||
url := r.CoreUrl(test.f, test.arch)
|
||||
if url != test.result {
|
||||
t.Errorf("seems that expected link address is incorrect (%v) for file %s %+v", url, test.f, test.arch)
|
||||
}
|
||||
}
|
||||
}
|
||||
167
pkg/worker/caged/libretro/nanoarch/input.go
Normal file
167
pkg/worker/caged/libretro/nanoarch/input.go
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
package nanoarch
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
/*
|
||||
#include <stdint.h>
|
||||
#include "libretro.h"
|
||||
|
||||
void input_cache_set_port(unsigned port, uint32_t buttons,
|
||||
int16_t lx, int16_t ly, int16_t rx, int16_t ry,
|
||||
int16_t l2, int16_t r2);
|
||||
void input_cache_set_keyboard_key(unsigned id, uint8_t pressed);
|
||||
void input_cache_set_mouse(int16_t dx, int16_t dy, uint8_t buttons);
|
||||
void input_cache_clear(void);
|
||||
*/
|
||||
import "C"
|
||||
|
||||
const (
|
||||
maxPort = 4
|
||||
numAxes = 4
|
||||
RetrokLast = int(C.RETROK_LAST)
|
||||
)
|
||||
|
||||
type Device byte
|
||||
|
||||
const (
|
||||
RetroPad Device = iota
|
||||
Keyboard
|
||||
Mouse
|
||||
)
|
||||
|
||||
const (
|
||||
MouseMove = iota
|
||||
MouseButton
|
||||
)
|
||||
|
||||
type MouseBtnState int32
|
||||
|
||||
const (
|
||||
MouseLeft MouseBtnState = 1 << iota
|
||||
MouseRight
|
||||
MouseMiddle
|
||||
)
|
||||
|
||||
// InputState stores controller state for all ports.
|
||||
// - uint16 button bitmask
|
||||
// - int16 analog axes x4 (left stick, right stick)
|
||||
// - int16 analog triggers x2 (L2, R2)
|
||||
type InputState [maxPort]struct {
|
||||
keys uint32 // lower 16 bits used
|
||||
axes int64 // packed: [LX:16][LY:16][RX:16][RY:16]
|
||||
triggers int32 // packed: [L2:16][R2:16]
|
||||
}
|
||||
|
||||
// SetInput sets input state for a player.
|
||||
//
|
||||
// [BTN:2][LX:2][LY:2][RX:2][RY:2][L2:2][R2:2]
|
||||
func (s *InputState) SetInput(port int, data []byte) {
|
||||
if len(data) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
// Buttons
|
||||
atomic.StoreUint32(&s[port].keys, uint32(binary.LittleEndian.Uint16(data)))
|
||||
|
||||
// Axes - pack into int64
|
||||
var packedAxes int64
|
||||
for i := 0; i < numAxes && i*2+3 < len(data); i++ {
|
||||
axis := int64(int16(binary.LittleEndian.Uint16(data[i*2+2:])))
|
||||
packedAxes |= (axis & 0xFFFF) << (i * 16)
|
||||
}
|
||||
atomic.StoreInt64(&s[port].axes, packedAxes)
|
||||
|
||||
// Analog triggers L2, R2 - pack into int32
|
||||
if len(data) >= 14 {
|
||||
l2 := int32(int16(binary.LittleEndian.Uint16(data[10:])))
|
||||
r2 := int32(int16(binary.LittleEndian.Uint16(data[12:])))
|
||||
atomic.StoreInt32(&s[port].triggers, (l2&0xFFFF)|((r2&0xFFFF)<<16))
|
||||
}
|
||||
}
|
||||
|
||||
// SyncToCache syncs input state to C-side cache before Run().
|
||||
func (s *InputState) SyncToCache() {
|
||||
for p := uint(0); p < maxPort; p++ {
|
||||
keys := atomic.LoadUint32(&s[p].keys)
|
||||
axes := atomic.LoadInt64(&s[p].axes)
|
||||
triggers := atomic.LoadInt32(&s[p].triggers)
|
||||
|
||||
C.input_cache_set_port(C.uint(p), C.uint32_t(keys),
|
||||
C.int16_t(axes),
|
||||
C.int16_t(axes>>16),
|
||||
C.int16_t(axes>>32),
|
||||
C.int16_t(axes>>48),
|
||||
C.int16_t(triggers),
|
||||
C.int16_t(triggers>>16))
|
||||
}
|
||||
}
|
||||
|
||||
// KeyboardState tracks keys of the keyboard.
|
||||
type KeyboardState struct {
|
||||
keys [6]atomic.Uint64 // 342 keys packed into 6 uint64s (384 bits)
|
||||
mod atomic.Uint32
|
||||
}
|
||||
|
||||
// SetKey sets keyboard state.
|
||||
//
|
||||
// [KEY:4][P:1][MOD:2]
|
||||
//
|
||||
// KEY - Libretro key code, P - pressed (0/1), MOD - modifier bitmask
|
||||
func (ks *KeyboardState) SetKey(data []byte) (pressed bool, key uint, mod uint16) {
|
||||
if len(data) != 7 {
|
||||
return
|
||||
}
|
||||
key = uint(binary.BigEndian.Uint32(data))
|
||||
mod = binary.BigEndian.Uint16(data[5:])
|
||||
pressed = data[4] == 1
|
||||
|
||||
idx, bit := key/64, uint64(1)<<(key%64)
|
||||
if pressed {
|
||||
ks.keys[idx].Or(bit)
|
||||
} else {
|
||||
ks.keys[idx].And(^bit)
|
||||
}
|
||||
ks.mod.Store(uint32(mod))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SyncToCache syncs keyboard state to C-side cache.
|
||||
func (ks *KeyboardState) SyncToCache() {
|
||||
for id := 0; id < RetrokLast; id++ {
|
||||
pressed := (ks.keys[id/64].Load() >> (id % 64)) & 1
|
||||
C.input_cache_set_keyboard_key(C.uint(id), C.uint8_t(pressed))
|
||||
}
|
||||
}
|
||||
|
||||
// MouseState tracks mouse delta and buttons.
|
||||
type MouseState struct {
|
||||
dx, dy atomic.Int32
|
||||
buttons atomic.Int32
|
||||
}
|
||||
|
||||
// ShiftPos adds relative mouse movement.
|
||||
//
|
||||
// [dx:2][dy:2]
|
||||
func (ms *MouseState) ShiftPos(data []byte) {
|
||||
if len(data) != 4 {
|
||||
return
|
||||
}
|
||||
ms.dx.Add(int32(int16(binary.BigEndian.Uint16(data[:2]))))
|
||||
ms.dy.Add(int32(int16(binary.BigEndian.Uint16(data[2:]))))
|
||||
}
|
||||
|
||||
func (ms *MouseState) SetButtons(b byte) { ms.buttons.Store(int32(b)) }
|
||||
|
||||
func (ms *MouseState) Buttons() (l, r, m bool) {
|
||||
b := MouseBtnState(ms.buttons.Load())
|
||||
return b&MouseLeft != 0, b&MouseRight != 0, b&MouseMiddle != 0
|
||||
}
|
||||
|
||||
// SyncToCache syncs mouse state to C-side cache, consuming deltas.
|
||||
func (ms *MouseState) SyncToCache() {
|
||||
C.input_cache_set_mouse(C.int16_t(ms.dx.Swap(0)), C.int16_t(ms.dy.Swap(0)), C.uint8_t(ms.buttons.Load()))
|
||||
}
|
||||
514
pkg/worker/caged/libretro/nanoarch/input_test.go
Normal file
514
pkg/worker/caged/libretro/nanoarch/input_test.go
Normal file
|
|
@ -0,0 +1,514 @@
|
|||
package nanoarch
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInputState_SetInput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
port int
|
||||
data []byte
|
||||
keys uint32
|
||||
axes [4]int16
|
||||
triggers [2]int16
|
||||
}{
|
||||
{
|
||||
name: "buttons only",
|
||||
port: 0,
|
||||
data: []byte{0xFF, 0x01},
|
||||
keys: 0x01FF,
|
||||
},
|
||||
{
|
||||
name: "buttons and axes",
|
||||
port: 1,
|
||||
data: []byte{0x03, 0x00, 0x10, 0x27, 0xF0, 0xD8, 0x00, 0x80, 0xFF, 0x7F},
|
||||
keys: 0x0003,
|
||||
axes: [4]int16{10000, -10000, -32768, 32767},
|
||||
},
|
||||
{
|
||||
name: "partial axes",
|
||||
port: 2,
|
||||
data: []byte{0x01, 0x00, 0x64, 0x00},
|
||||
keys: 0x0001,
|
||||
axes: [4]int16{100, 0, 0, 0},
|
||||
},
|
||||
{
|
||||
name: "max port",
|
||||
port: 3,
|
||||
data: []byte{0xFF, 0xFF},
|
||||
keys: 0xFFFF,
|
||||
},
|
||||
{
|
||||
name: "full input with triggers",
|
||||
port: 0,
|
||||
data: []byte{
|
||||
0x03, 0x00, // buttons
|
||||
0x10, 0x27, // LX: 10000
|
||||
0xF0, 0xD8, // LY: -10000
|
||||
0x00, 0x80, // RX: -32768
|
||||
0xFF, 0x7F, // RY: 32767
|
||||
0xFF, 0x3F, // L2: 16383
|
||||
0xFF, 0x7F, // R2: 32767
|
||||
},
|
||||
keys: 0x0003,
|
||||
axes: [4]int16{10000, -10000, -32768, 32767},
|
||||
triggers: [2]int16{16383, 32767},
|
||||
},
|
||||
{
|
||||
name: "axes without triggers",
|
||||
port: 1,
|
||||
data: []byte{
|
||||
0x01, 0x00,
|
||||
0x64, 0x00, // LX: 100
|
||||
0xC8, 0x00, // LY: 200
|
||||
0x2C, 0x01, // RX: 300
|
||||
0x90, 0x01, // RY: 400
|
||||
},
|
||||
keys: 0x0001,
|
||||
axes: [4]int16{100, 200, 300, 400},
|
||||
},
|
||||
{
|
||||
name: "zero triggers",
|
||||
port: 2,
|
||||
data: []byte{
|
||||
0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, // L2: 0
|
||||
0x00, 0x00, // R2: 0
|
||||
},
|
||||
keys: 0x0000,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
state := InputState{}
|
||||
state.SetInput(test.port, test.data)
|
||||
|
||||
if state[test.port].keys != test.keys {
|
||||
t.Errorf("keys: got %v, want %v", state[test.port].keys, test.keys)
|
||||
}
|
||||
|
||||
// Check axes from packed int64
|
||||
axes := state[test.port].axes
|
||||
for i, want := range test.axes {
|
||||
got := int16(axes >> (i * 16))
|
||||
if got != want {
|
||||
t.Errorf("axes[%d]: got %v, want %v", i, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// Check triggers from packed int32
|
||||
triggers := state[test.port].triggers
|
||||
l2 := int16(triggers)
|
||||
r2 := int16(triggers >> 16)
|
||||
if l2 != test.triggers[0] {
|
||||
t.Errorf("L2: got %v, want %v", l2, test.triggers[0])
|
||||
}
|
||||
if r2 != test.triggers[1] {
|
||||
t.Errorf("R2: got %v, want %v", r2, test.triggers[1])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInputState_AxisExtraction(t *testing.T) {
|
||||
state := InputState{}
|
||||
data := []byte{
|
||||
0x00, 0x00, // buttons
|
||||
0x01, 0x00, // LX: 1
|
||||
0x02, 0x00, // LY: 2
|
||||
0x03, 0x00, // RX: 3
|
||||
0x04, 0x00, // RY: 4
|
||||
0x05, 0x00, // L2: 5
|
||||
0x06, 0x00, // R2: 6
|
||||
}
|
||||
state.SetInput(0, data)
|
||||
|
||||
axes := state[0].axes
|
||||
expected := []int16{1, 2, 3, 4}
|
||||
for i, want := range expected {
|
||||
got := int16(axes >> (i * 16))
|
||||
if got != want {
|
||||
t.Errorf("axis[%d]: got %v, want %v", i, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
triggers := state[0].triggers
|
||||
if got := int16(triggers); got != 5 {
|
||||
t.Errorf("L2: got %v, want 5", got)
|
||||
}
|
||||
if got := int16(triggers >> 16); got != 6 {
|
||||
t.Errorf("R2: got %v, want 6", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInputState_NegativeAxes(t *testing.T) {
|
||||
state := InputState{}
|
||||
data := []byte{
|
||||
0x00, 0x00, // buttons
|
||||
0x00, 0x80, // LX: -32768
|
||||
0xFF, 0xFF, // LY: -1
|
||||
0x01, 0x80, // RX: -32767
|
||||
0xFE, 0xFF, // RY: -2
|
||||
}
|
||||
state.SetInput(0, data)
|
||||
|
||||
axes := state[0].axes
|
||||
expected := []int16{-32768, -1, -32767, -2}
|
||||
for i, want := range expected {
|
||||
got := int16(axes >> (i * 16))
|
||||
if got != want {
|
||||
t.Errorf("axis[%d]: got %v, want %v", i, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInputState_Concurrent(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
state := InputState{}
|
||||
events := 1000
|
||||
wg.Add(events)
|
||||
|
||||
for range events {
|
||||
player := rand.Intn(maxPort)
|
||||
go func() {
|
||||
// Full 14-byte input
|
||||
state.SetInput(player, []byte{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestKeyboardState_SetKey(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
pressed bool
|
||||
key uint
|
||||
mod uint16
|
||||
}{
|
||||
{
|
||||
name: "key pressed",
|
||||
data: []byte{0, 0, 0, 42, 1, 0, 3},
|
||||
pressed: true,
|
||||
key: 42,
|
||||
mod: 3,
|
||||
},
|
||||
{
|
||||
name: "key released",
|
||||
data: []byte{0, 0, 0, 100, 0, 0, 0},
|
||||
pressed: false,
|
||||
key: 100,
|
||||
mod: 0,
|
||||
},
|
||||
{
|
||||
name: "high key code",
|
||||
data: []byte{0, 0, 1, 50, 1, 0xFF, 0xFF},
|
||||
pressed: true,
|
||||
key: 306,
|
||||
mod: 0xFFFF,
|
||||
},
|
||||
{
|
||||
name: "invalid length",
|
||||
data: []byte{0, 0, 0},
|
||||
pressed: false,
|
||||
key: 0,
|
||||
mod: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ks := KeyboardState{}
|
||||
pressed, key, mod := ks.SetKey(test.data)
|
||||
|
||||
if pressed != test.pressed {
|
||||
t.Errorf("pressed: got %v, want %v", pressed, test.pressed)
|
||||
}
|
||||
if key != test.key {
|
||||
t.Errorf("key: got %v, want %v", key, test.key)
|
||||
}
|
||||
if mod != test.mod {
|
||||
t.Errorf("mod: got %v, want %v", mod, test.mod)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyboardState_IsPressed(t *testing.T) {
|
||||
ks := KeyboardState{}
|
||||
|
||||
// Initially not pressed
|
||||
if ks.keys[0].Load() != 0 {
|
||||
t.Error("key should not be pressed initially")
|
||||
}
|
||||
|
||||
// Press key
|
||||
ks.SetKey([]byte{0, 0, 0, 42, 1, 0, 0})
|
||||
if (ks.keys[42/64].Load()>>(42%64))&1 != 1 {
|
||||
t.Error("key should be pressed")
|
||||
}
|
||||
|
||||
// Release key
|
||||
ks.SetKey([]byte{0, 0, 0, 42, 0, 0, 0})
|
||||
if (ks.keys[42/64].Load()>>(42%64))&1 != 0 {
|
||||
t.Error("key should be released")
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyboardState_MultipleBits(t *testing.T) {
|
||||
ks := KeyboardState{}
|
||||
|
||||
// Press keys in different uint64 slots
|
||||
keys := []uint{0, 63, 64, 127, 128, 200, 300, 341}
|
||||
for _, k := range keys {
|
||||
data := make([]byte, 7)
|
||||
binary.BigEndian.PutUint32(data, uint32(k))
|
||||
data[4] = 1
|
||||
ks.SetKey(data)
|
||||
}
|
||||
|
||||
// Check all pressed
|
||||
for _, k := range keys {
|
||||
if (ks.keys[k/64].Load()>>(k%64))&1 != 1 {
|
||||
t.Errorf("key %d should be pressed", k)
|
||||
}
|
||||
}
|
||||
|
||||
// Release some
|
||||
for _, k := range []uint{0, 128, 341} {
|
||||
data := make([]byte, 7)
|
||||
binary.BigEndian.PutUint32(data, uint32(k))
|
||||
data[4] = 0
|
||||
ks.SetKey(data)
|
||||
}
|
||||
|
||||
// Check states
|
||||
expected := map[uint]uint64{
|
||||
0: 0, 63: 1, 64: 1, 127: 1, 128: 0, 200: 1, 300: 1, 341: 0,
|
||||
}
|
||||
for k, want := range expected {
|
||||
got := (ks.keys[k/64].Load() >> (k % 64)) & 1
|
||||
if got != want {
|
||||
t.Errorf("key %d: got %v, want %v", k, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyboardState_Concurrent(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
ks := KeyboardState{}
|
||||
events := 1000
|
||||
wg.Add(events * 2)
|
||||
|
||||
for range events {
|
||||
key := uint(rand.Intn(RetrokLast))
|
||||
go func() {
|
||||
data := make([]byte, 7)
|
||||
binary.BigEndian.PutUint32(data, uint32(key))
|
||||
data[4] = byte(rand.Intn(2))
|
||||
ks.SetKey(data)
|
||||
wg.Done()
|
||||
}()
|
||||
go func() {
|
||||
_ = (ks.keys[key/64].Load() >> (key % 64)) & 1
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestMouseState_ShiftPos(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
dx int16
|
||||
dy int16
|
||||
rx int16
|
||||
ry int16
|
||||
b func(dx, dy int16) []byte
|
||||
}{
|
||||
{
|
||||
name: "positive values",
|
||||
dx: 100,
|
||||
dy: 200,
|
||||
rx: 100,
|
||||
ry: 200,
|
||||
b: func(dx, dy int16) []byte {
|
||||
data := make([]byte, 4)
|
||||
binary.BigEndian.PutUint16(data, uint16(dx))
|
||||
binary.BigEndian.PutUint16(data[2:], uint16(dy))
|
||||
return data
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "negative values",
|
||||
dx: -10123,
|
||||
dy: 5678,
|
||||
rx: -10123,
|
||||
ry: 5678,
|
||||
b: func(dx, dy int16) []byte {
|
||||
data := make([]byte, 4)
|
||||
binary.BigEndian.PutUint16(data, uint16(dx))
|
||||
binary.BigEndian.PutUint16(data[2:], uint16(dy))
|
||||
return data
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wrong endian",
|
||||
dx: -1234,
|
||||
dy: 5678,
|
||||
rx: 12027,
|
||||
ry: 11798,
|
||||
b: func(dx, dy int16) []byte {
|
||||
data := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint16(data, uint16(dx))
|
||||
binary.LittleEndian.PutUint16(data[2:], uint16(dy))
|
||||
return data
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "max values",
|
||||
dx: 32767,
|
||||
dy: -32768,
|
||||
rx: 32767,
|
||||
ry: -32768,
|
||||
b: func(dx, dy int16) []byte {
|
||||
data := make([]byte, 4)
|
||||
binary.BigEndian.PutUint16(data, uint16(dx))
|
||||
binary.BigEndian.PutUint16(data[2:], uint16(dy))
|
||||
return data
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ms := MouseState{}
|
||||
ms.ShiftPos(test.b(test.dx, test.dy))
|
||||
|
||||
x, y := int16(ms.dx.Swap(0)), int16(ms.dy.Swap(0))
|
||||
|
||||
if x != test.rx || y != test.ry {
|
||||
t.Errorf("got (%v, %v), want (%v, %v)", x, y, test.rx, test.ry)
|
||||
}
|
||||
|
||||
if ms.dx.Load() != 0 || ms.dy.Load() != 0 {
|
||||
t.Error("coordinates weren't cleared")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMouseState_ShiftPosAccumulates(t *testing.T) {
|
||||
ms := MouseState{}
|
||||
|
||||
data := make([]byte, 4)
|
||||
binary.BigEndian.PutUint16(data, uint16(10))
|
||||
binary.BigEndian.PutUint16(data[2:], uint16(20))
|
||||
|
||||
ms.ShiftPos(data)
|
||||
ms.ShiftPos(data)
|
||||
ms.ShiftPos(data)
|
||||
|
||||
if got := ms.dx.Load(); got != 30 {
|
||||
t.Errorf("dx: got %v, want 30", got)
|
||||
}
|
||||
if got := ms.dy.Load(); got != 60 {
|
||||
t.Errorf("dy: got %v, want 60", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMouseState_ShiftPosInvalidLength(t *testing.T) {
|
||||
ms := MouseState{}
|
||||
|
||||
ms.ShiftPos([]byte{1, 2, 3})
|
||||
ms.ShiftPos([]byte{1, 2, 3, 4, 5})
|
||||
|
||||
if ms.dx.Load() != 0 || ms.dy.Load() != 0 {
|
||||
t.Error("invalid data should be ignored")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMouseState_Buttons(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data byte
|
||||
l bool
|
||||
r bool
|
||||
m bool
|
||||
}{
|
||||
{name: "none", data: 0},
|
||||
{name: "left", data: 1, l: true},
|
||||
{name: "right", data: 2, r: true},
|
||||
{name: "middle", data: 4, m: true},
|
||||
{name: "left+right", data: 3, l: true, r: true},
|
||||
{name: "all", data: 7, l: true, r: true, m: true},
|
||||
{name: "left+middle", data: 5, l: true, m: true},
|
||||
}
|
||||
|
||||
ms := MouseState{}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ms.SetButtons(test.data)
|
||||
l, r, m := ms.Buttons()
|
||||
if l != test.l || r != test.r || m != test.m {
|
||||
t.Errorf("got (%v, %v, %v), want (%v, %v, %v)", l, r, m, test.l, test.r, test.m)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMouseState_Concurrent(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
ms := MouseState{}
|
||||
events := 1000
|
||||
wg.Add(events * 3)
|
||||
|
||||
for range events {
|
||||
go func() {
|
||||
data := make([]byte, 4)
|
||||
binary.BigEndian.PutUint16(data, uint16(rand.Int31n(100)-50))
|
||||
binary.BigEndian.PutUint16(data[2:], uint16(rand.Int31n(100)-50))
|
||||
ms.ShiftPos(data)
|
||||
wg.Done()
|
||||
}()
|
||||
go func() {
|
||||
ms.SetButtons(byte(rand.Intn(8)))
|
||||
wg.Done()
|
||||
}()
|
||||
go func() {
|
||||
ms.Buttons()
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestConstants(t *testing.T) {
|
||||
// MouseBtnState
|
||||
if MouseLeft != 1 || MouseRight != 2 || MouseMiddle != 4 {
|
||||
t.Error("invalid MouseBtnState constants")
|
||||
}
|
||||
|
||||
// Device
|
||||
if RetroPad != 0 || Keyboard != 1 || Mouse != 2 {
|
||||
t.Error("invalid Device constants")
|
||||
}
|
||||
|
||||
// Mouse events
|
||||
if MouseMove != 0 || MouseButton != 1 {
|
||||
t.Error("invalid mouse event constants")
|
||||
}
|
||||
|
||||
// Limits
|
||||
if maxPort != 4 || numAxes != 4 || RetrokLast != 342 {
|
||||
t.Error("invalid limit constants")
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -19,7 +19,11 @@ import "C"
|
|||
func loadFunction(handle unsafe.Pointer, name string) unsafe.Pointer {
|
||||
cs := C.CString(name)
|
||||
defer C.free(unsafe.Pointer(cs))
|
||||
return C.dlsym(handle, cs)
|
||||
ptr := C.dlsym(handle, cs)
|
||||
if ptr == nil {
|
||||
panic("lib function not found: " + name)
|
||||
}
|
||||
return ptr
|
||||
}
|
||||
|
||||
func loadLib(filepath string) (handle unsafe.Pointer, err error) {
|
||||
|
|
|
|||
|
|
@ -3,15 +3,18 @@
|
|||
#include <stdbool.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#define RETRO_ENVIRONMENT_GET_CLEAR_ALL_THREAD_WAITS_CB (3 | 0x800000)
|
||||
|
||||
int initialized = 0;
|
||||
|
||||
typedef struct {
|
||||
int type;
|
||||
void* fn;
|
||||
void* arg1;
|
||||
void* arg2;
|
||||
void* result;
|
||||
int type;
|
||||
void* fn;
|
||||
void* arg1;
|
||||
void* arg2;
|
||||
void* result;
|
||||
} call_def_t;
|
||||
|
||||
call_def_t call;
|
||||
|
|
@ -24,6 +27,57 @@ enum call_type {
|
|||
|
||||
void *same_thread_with_args(void *f, int type, ...);
|
||||
|
||||
// Input State Cache
|
||||
|
||||
#define INPUT_MAX_PORTS 4
|
||||
#define INPUT_MAX_KEYS 512
|
||||
|
||||
typedef struct {
|
||||
uint32_t buttons[INPUT_MAX_PORTS];
|
||||
int16_t analog[INPUT_MAX_PORTS][4]; // LX, LY, RX, RY
|
||||
int16_t triggers[INPUT_MAX_PORTS][2]; // L2, R2
|
||||
|
||||
uint8_t keyboard[INPUT_MAX_KEYS];
|
||||
int16_t mouse_x;
|
||||
int16_t mouse_y;
|
||||
uint8_t mouse_buttons;
|
||||
} input_cache_t;
|
||||
|
||||
static input_cache_t input_cache = {0};
|
||||
|
||||
// Update entire port state at once
|
||||
void input_cache_set_port(unsigned port, uint32_t buttons,
|
||||
int16_t lx, int16_t ly, int16_t rx, int16_t ry,
|
||||
int16_t l2, int16_t r2) {
|
||||
if (port < INPUT_MAX_PORTS) {
|
||||
input_cache.buttons[port] = buttons;
|
||||
input_cache.analog[port][0] = lx;
|
||||
input_cache.analog[port][1] = ly;
|
||||
input_cache.analog[port][2] = rx;
|
||||
input_cache.analog[port][3] = ry;
|
||||
input_cache.triggers[port][0] = l2;
|
||||
input_cache.triggers[port][1] = r2;
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard update
|
||||
void input_cache_set_keyboard_key(unsigned id, uint8_t pressed) {
|
||||
if (id < INPUT_MAX_KEYS) {
|
||||
input_cache.keyboard[id] = pressed;
|
||||
}
|
||||
}
|
||||
|
||||
// Mouse update
|
||||
void input_cache_set_mouse(int16_t dx, int16_t dy, uint8_t buttons) {
|
||||
input_cache.mouse_x = dx;
|
||||
input_cache.mouse_y = dy;
|
||||
input_cache.mouse_buttons = buttons;
|
||||
}
|
||||
|
||||
void input_cache_clear(void) {
|
||||
memset(&input_cache, 0, sizeof(input_cache));
|
||||
}
|
||||
|
||||
void core_log_cgo(enum retro_log_level level, const char *fmt, ...) {
|
||||
char msg[2048] = {0};
|
||||
va_list va;
|
||||
|
|
@ -34,14 +88,12 @@ void core_log_cgo(enum retro_log_level level, const char *fmt, ...) {
|
|||
coreLog(level, msg);
|
||||
}
|
||||
|
||||
void bridge_retro_init(void *f) {
|
||||
core_log_cgo(RETRO_LOG_DEBUG, "Initialization...\n");
|
||||
void bridge_call(void *f) {
|
||||
((void (*)(void)) f)();
|
||||
}
|
||||
|
||||
void bridge_retro_deinit(void *f) {
|
||||
core_log_cgo(RETRO_LOG_DEBUG, "Deinitialiazation...\n");
|
||||
((void (*)(void)) f)();
|
||||
void bridge_set_callback(void *f, void *callback) {
|
||||
((void (*)(void *))f)(callback);
|
||||
}
|
||||
|
||||
unsigned bridge_retro_api_version(void *f) {
|
||||
|
|
@ -60,40 +112,14 @@ bool bridge_retro_set_environment(void *f, void *callback) {
|
|||
return ((bool (*)(retro_environment_t)) f)((retro_environment_t) callback);
|
||||
}
|
||||
|
||||
void bridge_retro_set_video_refresh(void *f, void *callback) {
|
||||
((bool (*)(retro_video_refresh_t)) f)((retro_video_refresh_t) callback);
|
||||
}
|
||||
|
||||
void bridge_retro_set_input_poll(void *f, void *callback) {
|
||||
((bool (*)(retro_input_poll_t)) f)((retro_input_poll_t) callback);
|
||||
}
|
||||
|
||||
void bridge_retro_set_input_state(void *f, void *callback) {
|
||||
((bool (*)(retro_input_state_t)) f)((retro_input_state_t) callback);
|
||||
}
|
||||
|
||||
void bridge_retro_set_audio_sample(void *f, void *callback) {
|
||||
((bool (*)(retro_audio_sample_t)) f)((retro_audio_sample_t) callback);
|
||||
}
|
||||
|
||||
void bridge_retro_set_audio_sample_batch(void *f, void *callback) {
|
||||
((bool (*)(retro_audio_sample_batch_t)) f)((retro_audio_sample_batch_t) callback);
|
||||
((int16_t (*)(retro_input_state_t)) f)((retro_input_state_t) callback);
|
||||
}
|
||||
|
||||
bool bridge_retro_load_game(void *f, struct retro_game_info *gi) {
|
||||
core_log_cgo(RETRO_LOG_DEBUG, "Loading the game...\n");
|
||||
return ((bool (*)(struct retro_game_info *)) f)(gi);
|
||||
}
|
||||
|
||||
void bridge_retro_unload_game(void *f) {
|
||||
core_log_cgo(RETRO_LOG_DEBUG, "Unloading the game...\n");
|
||||
((void (*)(void)) f)();
|
||||
}
|
||||
|
||||
void bridge_retro_run(void *f) {
|
||||
((void (*)(void)) f)();
|
||||
}
|
||||
|
||||
size_t bridge_retro_get_memory_size(void *f, unsigned id) {
|
||||
return ((size_t (*)(unsigned)) f)(id);
|
||||
}
|
||||
|
|
@ -123,12 +149,41 @@ static bool clear_all_thread_waits_cb(unsigned v, void *data) {
|
|||
return true;
|
||||
}
|
||||
|
||||
void bridge_clear_all_thread_waits_cb(void *data) {
|
||||
*(retro_environment_t *)data = clear_all_thread_waits_cb;
|
||||
void bridge_retro_keyboard_callback(void *cb, bool down, unsigned keycode, uint32_t character, uint16_t keyModifiers) {
|
||||
(*(retro_keyboard_event_t *) cb)(down, keycode, character, keyModifiers);
|
||||
}
|
||||
|
||||
bool core_environment_cgo(unsigned cmd, void *data) {
|
||||
bool coreEnvironment(unsigned, void *);
|
||||
|
||||
switch (cmd)
|
||||
{
|
||||
case RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE:
|
||||
return false;
|
||||
break;
|
||||
case RETRO_ENVIRONMENT_GET_AUDIO_VIDEO_ENABLE:
|
||||
return false;
|
||||
break;
|
||||
case RETRO_ENVIRONMENT_GET_CLEAR_ALL_THREAD_WAITS_CB:
|
||||
*(retro_environment_t *)data = clear_all_thread_waits_cb;
|
||||
return true;
|
||||
break;
|
||||
case RETRO_ENVIRONMENT_GET_INPUT_MAX_USERS:
|
||||
*(unsigned *)data = 4;
|
||||
core_log_cgo(RETRO_LOG_DEBUG, "Set max users: %d\n", 4);
|
||||
return true;
|
||||
break;
|
||||
case RETRO_ENVIRONMENT_GET_INPUT_BITMASKS:
|
||||
return false;
|
||||
case RETRO_ENVIRONMENT_SHUTDOWN:
|
||||
return false;
|
||||
break;
|
||||
case RETRO_ENVIRONMENT_GET_SAVESTATE_CONTEXT:
|
||||
if (data != NULL) *(int *)data = RETRO_SAVESTATE_CONTEXT_NORMAL;
|
||||
return true;
|
||||
break;
|
||||
}
|
||||
|
||||
return coreEnvironment(cmd, data);
|
||||
}
|
||||
|
||||
|
|
@ -138,18 +193,77 @@ void core_video_refresh_cgo(void *data, unsigned width, unsigned height, size_t
|
|||
}
|
||||
|
||||
void core_input_poll_cgo() {
|
||||
void coreInputPoll();
|
||||
coreInputPoll();
|
||||
}
|
||||
|
||||
int16_t core_input_state_cgo(unsigned port, unsigned device, unsigned index, unsigned id) {
|
||||
int16_t coreInputState(unsigned, unsigned, unsigned, unsigned);
|
||||
return coreInputState(port, device, index, id);
|
||||
}
|
||||
if (port >= INPUT_MAX_PORTS) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
void core_audio_sample_cgo(int16_t left, int16_t right) {
|
||||
void coreAudioSample(int16_t, int16_t);
|
||||
coreAudioSample(left, right);
|
||||
switch (device) {
|
||||
case RETRO_DEVICE_JOYPAD:
|
||||
return (int16_t)((input_cache.buttons[port] >> id) & 1);
|
||||
|
||||
case RETRO_DEVICE_ANALOG:
|
||||
switch (index) {
|
||||
case RETRO_DEVICE_INDEX_ANALOG_LEFT:
|
||||
// id: RETRO_DEVICE_ID_ANALOG_X=0, RETRO_DEVICE_ID_ANALOG_Y=1
|
||||
if (id <= RETRO_DEVICE_ID_ANALOG_Y) {
|
||||
return input_cache.analog[port][id];
|
||||
}
|
||||
break;
|
||||
case RETRO_DEVICE_INDEX_ANALOG_RIGHT:
|
||||
// id: RETRO_DEVICE_ID_ANALOG_X=0, RETRO_DEVICE_ID_ANALOG_Y=1
|
||||
if (id <= RETRO_DEVICE_ID_ANALOG_Y) {
|
||||
return input_cache.analog[port][2 + id];
|
||||
}
|
||||
break;
|
||||
case RETRO_DEVICE_INDEX_ANALOG_BUTTON:
|
||||
// Any button can be queried as analog
|
||||
// id = RETRO_DEVICE_ID_JOYPAD_* (0-15)
|
||||
// For now, only L2/R2 have analog values
|
||||
switch (id) {
|
||||
case RETRO_DEVICE_ID_JOYPAD_L2:
|
||||
return input_cache.triggers[port][0];
|
||||
case RETRO_DEVICE_ID_JOYPAD_R2:
|
||||
return input_cache.triggers[port][1];
|
||||
default:
|
||||
// Other buttons: return digital as 0 or 0x7fff
|
||||
return ((input_cache.buttons[port] >> id) & 1) ? 0x7FFF : 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case RETRO_DEVICE_KEYBOARD:
|
||||
if (id < INPUT_MAX_KEYS) {
|
||||
return input_cache.keyboard[id] ? 1 : 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case RETRO_DEVICE_MOUSE:
|
||||
switch (id) {
|
||||
case RETRO_DEVICE_ID_MOUSE_X: {
|
||||
int16_t x = input_cache.mouse_x;
|
||||
input_cache.mouse_x = 0;
|
||||
return x;
|
||||
}
|
||||
case RETRO_DEVICE_ID_MOUSE_Y: {
|
||||
int16_t y = input_cache.mouse_y;
|
||||
input_cache.mouse_y = 0;
|
||||
return y;
|
||||
}
|
||||
case RETRO_DEVICE_ID_MOUSE_LEFT:
|
||||
return (input_cache.mouse_buttons & 0x01) ? 1 : 0;
|
||||
case RETRO_DEVICE_ID_MOUSE_RIGHT:
|
||||
return (input_cache.mouse_buttons & 0x02) ? 1 : 0;
|
||||
case RETRO_DEVICE_ID_MOUSE_MIDDLE:
|
||||
return (input_cache.mouse_buttons & 0x04) ? 1 : 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t core_audio_sample_batch_cgo(const int16_t *data, size_t frames) {
|
||||
|
|
@ -157,6 +271,11 @@ size_t core_audio_sample_batch_cgo(const int16_t *data, size_t frames) {
|
|||
return coreAudioSampleBatch(data, frames);
|
||||
}
|
||||
|
||||
void core_audio_sample_cgo(int16_t left, int16_t right) {
|
||||
int16_t frame[2] = { left, right };
|
||||
core_audio_sample_batch_cgo(frame, 1);
|
||||
}
|
||||
|
||||
uintptr_t core_get_current_framebuffer_cgo() {
|
||||
uintptr_t coreGetCurrentFramebuffer();
|
||||
return coreGetCurrentFramebuffer();
|
||||
|
|
@ -231,6 +350,7 @@ void *run_loop(void *unused) {
|
|||
mutex_destroy(&done_mutex);
|
||||
pthread_detach(thread);
|
||||
core_log_cgo(RETRO_LOG_DEBUG, "UnLibCo run loop stop\n");
|
||||
pthread_exit(NULL);
|
||||
}
|
||||
|
||||
void same_thread_stop() {
|
||||
|
|
|
|||
|
|
@ -3,8 +3,11 @@ package nanoarch
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
|
@ -12,7 +15,6 @@ import (
|
|||
"github.com/giongto35/cloud-game/v3/pkg/logger"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/os"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/worker/caged/libretro/graphics"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/worker/caged/libretro/repo/arch"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/worker/thread"
|
||||
)
|
||||
|
||||
|
|
@ -20,18 +22,9 @@ import (
|
|||
#include "libretro.h"
|
||||
#include "nanoarch.h"
|
||||
#include <stdlib.h>
|
||||
|
||||
#define RETRO_ENVIRONMENT_GET_CLEAR_ALL_THREAD_WAITS_CB (3 | 0x800000)
|
||||
*/
|
||||
import "C"
|
||||
|
||||
const lastKey = int(C.RETRO_DEVICE_ID_JOYPAD_R3)
|
||||
|
||||
const KeyPressed = 1
|
||||
const KeyReleased = 0
|
||||
|
||||
const MaxPort int = 4
|
||||
|
||||
var (
|
||||
RGBA5551 = PixFmt{C: 0, BPP: 2} // BIT_FORMAT_SHORT_5_5_5_1 has 5 bits R, 5 bits G, 5 bits B, 1 bit alpha
|
||||
RGBA8888Rev = PixFmt{C: 1, BPP: 4} // BIT_FORMAT_INT_8_8_8_8_REV has 8 bits R, 8 bits G, 8 bits B, 8 bit alpha
|
||||
|
|
@ -40,20 +33,26 @@ var (
|
|||
|
||||
type Nanoarch struct {
|
||||
Handlers
|
||||
|
||||
keyboard KeyboardState
|
||||
mouse MouseState
|
||||
retropad InputState
|
||||
|
||||
keyboardCb *C.struct_retro_keyboard_callback
|
||||
LastFrameTime int64
|
||||
LibCo bool
|
||||
multitap struct {
|
||||
supported bool
|
||||
enabled bool
|
||||
value C.unsigned
|
||||
meta Metadata
|
||||
options map[string]string
|
||||
options4rom map[string]map[string]string
|
||||
reserved chan struct{} // limits concurrent use
|
||||
Rot uint
|
||||
serializeSize C.size_t
|
||||
Stopped atomic.Bool
|
||||
sys struct {
|
||||
av C.struct_retro_system_av_info
|
||||
i C.struct_retro_system_info
|
||||
api C.unsigned
|
||||
}
|
||||
options *map[string]string
|
||||
reserved chan struct{} // limits concurrent use
|
||||
Rot uint
|
||||
serializeSize C.size_t
|
||||
stopped atomic.Bool
|
||||
sysAvInfo C.struct_retro_system_av_info
|
||||
sysInfo C.struct_retro_system_info
|
||||
tickTime int64
|
||||
cSaveDirectory *C.char
|
||||
cSystemDirectory *C.char
|
||||
|
|
@ -67,16 +66,18 @@ type Nanoarch struct {
|
|||
PixFmt PixFmt
|
||||
}
|
||||
vfr bool
|
||||
Aspect bool
|
||||
sdlCtx *graphics.SDL
|
||||
hackSkipHwContextDestroy bool
|
||||
hackSkipSameThreadSave bool
|
||||
limiter func(func())
|
||||
log *logger.Logger
|
||||
}
|
||||
|
||||
type Handlers struct {
|
||||
OnDpad func(port uint, axis uint) (shift int16)
|
||||
OnKeyPress func(port uint, key int) int
|
||||
OnAudio func(ptr unsafe.Pointer, frames int)
|
||||
OnVideo func(data []byte, delta int32, fi FrameInfo)
|
||||
OnDup func()
|
||||
OnSystemAvInfo func()
|
||||
}
|
||||
|
||||
|
|
@ -87,14 +88,19 @@ type FrameInfo struct {
|
|||
}
|
||||
|
||||
type Metadata struct {
|
||||
LibPath string // the full path to some emulator lib
|
||||
IsGlAllowed bool
|
||||
UsesLibCo bool
|
||||
AutoGlContext bool
|
||||
HasMultitap bool
|
||||
HasVFR bool
|
||||
Options map[string]string
|
||||
Hacks []string
|
||||
FrameDup bool
|
||||
LibPath string // the full path to some emulator lib
|
||||
IsGlAllowed bool
|
||||
UsesLibCo bool
|
||||
AutoGlContext bool
|
||||
HasVFR bool
|
||||
Options map[string]string
|
||||
Options4rom map[string]map[string]string
|
||||
Hacks []string
|
||||
Hid map[int][]int
|
||||
CoreAspectRatio bool
|
||||
KbMouseSupport bool
|
||||
LibExt string
|
||||
}
|
||||
|
||||
type PixFmt struct {
|
||||
|
|
@ -118,12 +124,12 @@ func (p PixFmt) String() string {
|
|||
// Nan0 is a global link for C callbacks to Go
|
||||
var Nan0 = Nanoarch{
|
||||
reserved: make(chan struct{}, 1), // this thing forbids concurrent use of the emulator
|
||||
stopped: atomic.Bool{},
|
||||
Stopped: atomic.Bool{},
|
||||
limiter: func(fn func()) { fn() },
|
||||
Handlers: Handlers{
|
||||
OnDpad: func(uint, uint) int16 { return 0 },
|
||||
OnKeyPress: func(uint, int) int { return 0 },
|
||||
OnAudio: func(unsafe.Pointer, int) {},
|
||||
OnVideo: func([]byte, int32, FrameInfo) {},
|
||||
OnAudio: func(unsafe.Pointer, int) {},
|
||||
OnVideo: func([]byte, int32, FrameInfo) {},
|
||||
OnDup: func() {},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -139,55 +145,76 @@ func NewNano(localPath string) *Nanoarch {
|
|||
return nano
|
||||
}
|
||||
|
||||
func (n *Nanoarch) AudioSampleRate() int { return int(n.sysAvInfo.timing.sample_rate) }
|
||||
func (n *Nanoarch) VideoFramerate() int { return int(n.sysAvInfo.timing.fps) }
|
||||
func (n *Nanoarch) IsPortrait() bool { return n.Rot == 90 || n.Rot == 270 }
|
||||
func (n *Nanoarch) GeometryBase() (int, int) {
|
||||
return int(n.sysAvInfo.geometry.base_width), int(n.sysAvInfo.geometry.base_height)
|
||||
func (n *Nanoarch) AspectRatio() float32 { return float32(n.sys.av.geometry.aspect_ratio) }
|
||||
func (n *Nanoarch) AudioSampleRate() int { return int(n.sys.av.timing.sample_rate) }
|
||||
func (n *Nanoarch) VideoFramerate() int { return int(n.sys.av.timing.fps) }
|
||||
func (n *Nanoarch) IsPortrait() bool { return 90 == n.Rot%180 }
|
||||
func (n *Nanoarch) KbMouseSupport() bool { return n.meta.KbMouseSupport }
|
||||
func (n *Nanoarch) BaseWidth() int { return int(n.sys.av.geometry.base_width) }
|
||||
func (n *Nanoarch) BaseHeight() int { return int(n.sys.av.geometry.base_height) }
|
||||
func (n *Nanoarch) WaitReady() { <-n.reserved }
|
||||
func (n *Nanoarch) Close() { n.Stopped.Store(true); n.reserved <- struct{}{} }
|
||||
func (n *Nanoarch) SetLogger(log *logger.Logger) { n.log = log }
|
||||
func (n *Nanoarch) SetVideoDebounce(t time.Duration) { n.limiter = NewLimit(t) }
|
||||
func (n *Nanoarch) SaveDir() string { return C.GoString(n.cSaveDirectory) }
|
||||
func (n *Nanoarch) SetSaveDirSuffix(sx string) {
|
||||
dir := C.GoString(n.cSaveDirectory) + "/" + sx
|
||||
err := os.CheckCreateDir(dir)
|
||||
if err != nil {
|
||||
n.log.Error().Msgf("couldn't create %v, %v", dir, err)
|
||||
}
|
||||
if n.cSaveDirectory != nil {
|
||||
C.free(unsafe.Pointer(n.cSaveDirectory))
|
||||
}
|
||||
n.cSaveDirectory = C.CString(dir)
|
||||
}
|
||||
func (n *Nanoarch) GeometryMax() (int, int) {
|
||||
return int(n.sysAvInfo.geometry.max_width), int(n.sysAvInfo.geometry.max_height)
|
||||
func (n *Nanoarch) DeleteSaveDir() error {
|
||||
if n.cSaveDirectory == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
dir := C.GoString(n.cSaveDirectory)
|
||||
return os.RemoveAll(dir)
|
||||
}
|
||||
func (n *Nanoarch) WaitReady() { <-n.reserved }
|
||||
func (n *Nanoarch) Close() { n.stopped.Store(true); n.reserved <- struct{}{} }
|
||||
func (n *Nanoarch) SetLogger(log *logger.Logger) { n.log = log }
|
||||
|
||||
func (n *Nanoarch) CoreLoad(meta Metadata) {
|
||||
var err error
|
||||
n.meta = meta
|
||||
n.LibCo = meta.UsesLibCo
|
||||
n.vfr = meta.HasVFR
|
||||
n.Aspect = meta.CoreAspectRatio
|
||||
n.Video.gl.autoCtx = meta.AutoGlContext
|
||||
n.Video.gl.enabled = meta.IsGlAllowed
|
||||
|
||||
thread.SwitchGraphics(n.Video.gl.enabled)
|
||||
|
||||
// hacks
|
||||
Nan0.hackSkipHwContextDestroy = meta.HasHack("skip_hw_context_destroy")
|
||||
Nan0.hackSkipSameThreadSave = meta.HasHack("skip_same_thread_save")
|
||||
|
||||
n.options = &meta.Options
|
||||
// reset controllers
|
||||
n.retropad = InputState{}
|
||||
n.keyboardCb = nil
|
||||
n.keyboard = KeyboardState{}
|
||||
n.mouse = MouseState{}
|
||||
|
||||
n.multitap.supported = meta.HasMultitap
|
||||
n.multitap.enabled = false
|
||||
n.multitap.value = 0
|
||||
n.options = maps.Clone(meta.Options)
|
||||
n.options4rom = meta.Options4rom
|
||||
|
||||
filePath := meta.LibPath
|
||||
if ar, err := arch.Guess(); err == nil {
|
||||
filePath = filePath + ar.LibExt
|
||||
} else {
|
||||
n.log.Warn().Err(err).Msg("system arch guesser failed")
|
||||
}
|
||||
|
||||
coreLib, err = loadLib(filePath)
|
||||
corePath := meta.LibPath + meta.LibExt
|
||||
coreLib, err = loadLib(corePath)
|
||||
// fallback to sequential lib loader (first successfully loaded)
|
||||
if err != nil {
|
||||
n.log.Error().Err(err).Msgf("load fail: %v", filePath)
|
||||
coreLib, err = loadLibRollingRollingRolling(filePath)
|
||||
n.log.Error().Err(err).Msgf("load fail: %v", corePath)
|
||||
coreLib, err = loadLibRollingRollingRolling(corePath)
|
||||
if err != nil {
|
||||
n.log.Fatal().Err(err).Msgf("core load: %s", filePath)
|
||||
n.log.Fatal().Err(err).Msgf("core load: %s", corePath)
|
||||
}
|
||||
}
|
||||
|
||||
retroInit = loadFunction(coreLib, "retro_init")
|
||||
retroDeinit = loadFunction(coreLib, "retro_deinit")
|
||||
//retroAPIVersion = loadFunction(coreLib, "retro_api_version")
|
||||
retroAPIVersion = loadFunction(coreLib, "retro_api_version")
|
||||
retroGetSystemInfo = loadFunction(coreLib, "retro_get_system_info")
|
||||
retroGetSystemAVInfo = loadFunction(coreLib, "retro_get_system_av_info")
|
||||
retroSetEnvironment = loadFunction(coreLib, "retro_set_environment")
|
||||
|
|
@ -196,6 +223,7 @@ func (n *Nanoarch) CoreLoad(meta Metadata) {
|
|||
retroSetInputState = loadFunction(coreLib, "retro_set_input_state")
|
||||
retroSetAudioSample = loadFunction(coreLib, "retro_set_audio_sample")
|
||||
retroSetAudioSampleBatch = loadFunction(coreLib, "retro_set_audio_sample_batch")
|
||||
retroReset = loadFunction(coreLib, "retro_reset")
|
||||
retroRun = loadFunction(coreLib, "retro_run")
|
||||
retroLoadGame = loadFunction(coreLib, "retro_load_game")
|
||||
retroUnloadGame = loadFunction(coreLib, "retro_unload_game")
|
||||
|
|
@ -207,28 +235,30 @@ func (n *Nanoarch) CoreLoad(meta Metadata) {
|
|||
retroGetMemoryData = loadFunction(coreLib, "retro_get_memory_data")
|
||||
|
||||
C.bridge_retro_set_environment(retroSetEnvironment, C.core_environment_cgo)
|
||||
C.bridge_retro_set_video_refresh(retroSetVideoRefresh, C.core_video_refresh_cgo)
|
||||
C.bridge_retro_set_input_poll(retroSetInputPoll, C.core_input_poll_cgo)
|
||||
C.bridge_retro_set_input_state(retroSetInputState, C.core_input_state_cgo)
|
||||
C.bridge_retro_set_audio_sample(retroSetAudioSample, C.core_audio_sample_cgo)
|
||||
C.bridge_retro_set_audio_sample_batch(retroSetAudioSampleBatch, C.core_audio_sample_batch_cgo)
|
||||
C.bridge_set_callback(retroSetVideoRefresh, C.core_video_refresh_cgo)
|
||||
C.bridge_set_callback(retroSetInputPoll, C.core_input_poll_cgo)
|
||||
C.bridge_set_callback(retroSetAudioSample, C.core_audio_sample_cgo)
|
||||
C.bridge_set_callback(retroSetAudioSampleBatch, C.core_audio_sample_batch_cgo)
|
||||
|
||||
if n.LibCo {
|
||||
C.same_thread(retroInit)
|
||||
} else {
|
||||
C.bridge_retro_init(retroInit)
|
||||
C.bridge_call(retroInit)
|
||||
}
|
||||
|
||||
C.bridge_retro_get_system_info(retroGetSystemInfo, &n.sysInfo)
|
||||
n.log.Debug().Msgf("System >>> %s (%s) [%s] nfp: %v",
|
||||
C.GoString(n.sysInfo.library_name), C.GoString(n.sysInfo.library_version),
|
||||
C.GoString(n.sysInfo.valid_extensions), bool(n.sysInfo.need_fullpath))
|
||||
n.sys.api = C.bridge_retro_api_version(retroAPIVersion)
|
||||
C.bridge_retro_get_system_info(retroGetSystemInfo, &n.sys.i)
|
||||
n.log.Info().Msgf("System >>> %v (%v) [%v] nfp: %v, api: %v",
|
||||
C.GoString(n.sys.i.library_name), C.GoString(n.sys.i.library_version),
|
||||
C.GoString(n.sys.i.valid_extensions), bool(n.sys.i.need_fullpath),
|
||||
uint(n.sys.api))
|
||||
}
|
||||
|
||||
func (n *Nanoarch) LoadGame(path string) error {
|
||||
game := C.struct_retro_game_info{}
|
||||
|
||||
big := bool(n.sysInfo.need_fullpath) // big ROMs are loaded by cores later
|
||||
big := bool(n.sys.i.need_fullpath) // big ROMs are loaded by cores later
|
||||
if big {
|
||||
size, err := os.StatSize(path)
|
||||
if err != nil {
|
||||
|
|
@ -252,70 +282,74 @@ func (n *Nanoarch) LoadGame(path string) error {
|
|||
|
||||
n.log.Debug().Msgf("ROM - big: %v, size: %v", big, byteCountBinary(int64(game.size)))
|
||||
|
||||
// maybe some custom options
|
||||
if n.options4rom != nil {
|
||||
romName := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
|
||||
if _, ok := n.options4rom[romName]; ok {
|
||||
for k, v := range n.options4rom[romName] {
|
||||
n.options[k] = v
|
||||
n.log.Debug().Msgf("Replace: %v=%v", k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ok := C.bridge_retro_load_game(retroLoadGame, &game); !ok {
|
||||
return fmt.Errorf("core failed to load ROM: %v", path)
|
||||
}
|
||||
|
||||
C.bridge_retro_get_system_av_info(retroGetSystemAVInfo, &n.sysAvInfo)
|
||||
var av C.struct_retro_system_av_info
|
||||
C.bridge_retro_get_system_av_info(retroGetSystemAVInfo, &av)
|
||||
n.log.Info().Msgf("System A/V >>> %vx%v (%vx%v), [%vfps], AR [%v], audio [%vHz]",
|
||||
n.sysAvInfo.geometry.base_width, n.sysAvInfo.geometry.base_height,
|
||||
n.sysAvInfo.geometry.max_width, n.sysAvInfo.geometry.max_height,
|
||||
n.sysAvInfo.timing.fps, n.sysAvInfo.geometry.aspect_ratio, n.sysAvInfo.timing.sample_rate,
|
||||
av.geometry.base_width, av.geometry.base_height,
|
||||
av.geometry.max_width, av.geometry.max_height,
|
||||
av.timing.fps, av.geometry.aspect_ratio, av.timing.sample_rate,
|
||||
)
|
||||
if isGeometryDifferent(av.geometry) {
|
||||
geometryChange(av.geometry)
|
||||
}
|
||||
n.sys.av = av
|
||||
|
||||
n.serializeSize = C.bridge_retro_serialize_size(retroSerializeSize)
|
||||
n.log.Info().Msgf("Save file size: %v", byteCountBinary(int64(n.serializeSize)))
|
||||
|
||||
Nan0.tickTime = int64(time.Second / time.Duration(n.sysAvInfo.timing.fps))
|
||||
Nan0.tickTime = int64(time.Second / time.Duration(n.sys.av.timing.fps))
|
||||
if n.vfr {
|
||||
n.log.Info().Msgf("variable framerate (VFR) is enabled")
|
||||
}
|
||||
|
||||
n.stopped.Store(false)
|
||||
n.Stopped.Store(false)
|
||||
|
||||
if n.Video.gl.enabled {
|
||||
//setRotation(image.F180) // flip Y coordinates of OpenGL
|
||||
bufS := uint(n.sysAvInfo.geometry.max_width*n.sysAvInfo.geometry.max_height) * n.Video.PixFmt.BPP
|
||||
graphics.SetBuffer(int(bufS))
|
||||
n.log.Info().Msgf("Set buffer: %v", byteCountBinary(int64(bufS)))
|
||||
if n.LibCo {
|
||||
C.same_thread(C.init_video_cgo)
|
||||
C.same_thread(unsafe.Pointer(Nan0.Video.hw.context_reset))
|
||||
} else {
|
||||
runtime.LockOSThread()
|
||||
initVideo()
|
||||
C.bridge_context_reset(Nan0.Video.hw.context_reset)
|
||||
runtime.UnlockOSThread()
|
||||
}
|
||||
}
|
||||
|
||||
// set default controller types on all ports
|
||||
for i := 0; i < MaxPort; i++ {
|
||||
// needed for nestopia
|
||||
for i := range maxPort {
|
||||
C.bridge_retro_set_controller_port_device(retroSetControllerPortDevice, C.uint(i), C.RETRO_DEVICE_JOYPAD)
|
||||
}
|
||||
|
||||
// map custom devices to ports
|
||||
for k, v := range n.meta.Hid {
|
||||
for _, device := range v {
|
||||
C.bridge_retro_set_controller_port_device(retroSetControllerPortDevice, C.uint(k), C.unsigned(device))
|
||||
n.log.Debug().Msgf("set custom port-device: %v:%v", k, device)
|
||||
}
|
||||
}
|
||||
|
||||
n.LastFrameTime = time.Now().UnixNano()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToggleMultitap toggles multitap controller for cores.
|
||||
//
|
||||
// Official SNES games only support a single multitap device
|
||||
// Most require it to be plugged in player 2 port and Snes9X requires it
|
||||
// to be "plugged" after the game is loaded.
|
||||
// Control this from the browser since player 2 will stop working in some games
|
||||
// if multitap is "plugged" in.
|
||||
func (n *Nanoarch) ToggleMultitap() {
|
||||
if !n.multitap.supported || n.multitap.value == 0 {
|
||||
return
|
||||
}
|
||||
mt := n.multitap.value
|
||||
if n.multitap.enabled {
|
||||
mt = C.RETRO_DEVICE_JOYPAD
|
||||
}
|
||||
C.bridge_retro_set_controller_port_device(retroSetControllerPortDevice, 1, mt)
|
||||
n.multitap.enabled = !n.multitap.enabled
|
||||
}
|
||||
|
||||
func (n *Nanoarch) Shutdown() {
|
||||
if n.LibCo {
|
||||
thread.Main(func() {
|
||||
|
|
@ -336,8 +370,8 @@ func (n *Nanoarch) Shutdown() {
|
|||
}
|
||||
})
|
||||
}
|
||||
C.bridge_retro_unload_game(retroUnloadGame)
|
||||
C.bridge_retro_deinit(retroDeinit)
|
||||
C.bridge_call(retroUnloadGame)
|
||||
C.bridge_call(retroDeinit)
|
||||
if n.Video.gl.enabled {
|
||||
thread.Main(func() {
|
||||
deinitVideo()
|
||||
|
|
@ -347,35 +381,77 @@ func (n *Nanoarch) Shutdown() {
|
|||
}
|
||||
|
||||
setRotation(0)
|
||||
Nan0.sys.av = C.struct_retro_system_av_info{}
|
||||
if err := closeLib(coreLib); err != nil {
|
||||
n.log.Error().Err(err).Msg("lib close failed")
|
||||
}
|
||||
n.options = nil
|
||||
n.options4rom = nil
|
||||
C.free(unsafe.Pointer(n.cUserName))
|
||||
C.free(unsafe.Pointer(n.cSaveDirectory))
|
||||
C.free(unsafe.Pointer(n.cSystemDirectory))
|
||||
}
|
||||
|
||||
func (n *Nanoarch) Reset() {
|
||||
C.bridge_call(retroReset)
|
||||
}
|
||||
|
||||
func (n *Nanoarch) syncInputToCache() {
|
||||
n.retropad.SyncToCache()
|
||||
if n.keyboardCb != nil {
|
||||
n.keyboard.SyncToCache()
|
||||
}
|
||||
n.mouse.SyncToCache()
|
||||
}
|
||||
|
||||
func (n *Nanoarch) Run() {
|
||||
n.syncInputToCache()
|
||||
|
||||
if n.LibCo {
|
||||
C.same_thread(retroRun)
|
||||
} else {
|
||||
if n.Video.gl.enabled {
|
||||
// running inside a go routine, lock the thread to make sure the OpenGL context stays current
|
||||
runtime.LockOSThread()
|
||||
if err := n.sdlCtx.BindContext(); err != nil {
|
||||
n.log.Error().Err(err).Msg("ctx bind fail")
|
||||
}
|
||||
}
|
||||
C.bridge_retro_run(retroRun)
|
||||
C.bridge_call(retroRun)
|
||||
if n.Video.gl.enabled {
|
||||
runtime.UnlockOSThread()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Nanoarch) IsGL() bool { return n.Video.gl.enabled }
|
||||
func (n *Nanoarch) IsStopped() bool { return n.stopped.Load() }
|
||||
func (n *Nanoarch) IsSupported() error { return graphics.TryInit() }
|
||||
func (n *Nanoarch) IsGL() bool { return n.Video.gl.enabled }
|
||||
func (n *Nanoarch) IsStopped() bool { return n.Stopped.Load() }
|
||||
func (n *Nanoarch) InputRetropad(port int, data []byte) { n.retropad.SetInput(port, data) }
|
||||
func (n *Nanoarch) InputKeyboard(_ int, data []byte) {
|
||||
if n.keyboardCb == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// we should preserve the state of pressed buttons for the input poll function (each retro_run)
|
||||
// and explicitly call the retro_keyboard_callback function when a keyboard event happens
|
||||
pressed, key, mod := n.keyboard.SetKey(data)
|
||||
C.bridge_retro_keyboard_callback(unsafe.Pointer(n.keyboardCb), C.bool(pressed),
|
||||
C.unsigned(key), C.uint32_t(0), C.uint16_t(mod))
|
||||
}
|
||||
func (n *Nanoarch) InputMouse(_ int, data []byte) {
|
||||
if len(data) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
t := data[0]
|
||||
state := data[1:]
|
||||
switch t {
|
||||
case MouseMove:
|
||||
n.mouse.ShiftPos(state)
|
||||
case MouseButton:
|
||||
n.mouse.SetButtons(state[0])
|
||||
}
|
||||
}
|
||||
|
||||
func videoSetPixelFormat(format uint32) (C.bool, error) {
|
||||
switch format {
|
||||
|
|
@ -384,8 +460,6 @@ func videoSetPixelFormat(format uint32) (C.bool, error) {
|
|||
if err := graphics.SetPixelFormat(graphics.UnsignedShort5551); err != nil {
|
||||
return false, fmt.Errorf("unknown pixel format %v", Nan0.Video.PixFmt)
|
||||
}
|
||||
// format is not implemented
|
||||
return false, fmt.Errorf("unsupported pixel type %v converter", format)
|
||||
case C.RETRO_PIXEL_FORMAT_XRGB8888:
|
||||
Nan0.Video.PixFmt = RGBA8888Rev
|
||||
if err := graphics.SetPixelFormat(graphics.UnsignedInt8888Rev); err != nil {
|
||||
|
|
@ -412,13 +486,11 @@ func setRotation(rot uint) {
|
|||
func printOpenGLDriverInfo() {
|
||||
var openGLInfo strings.Builder
|
||||
openGLInfo.Grow(128)
|
||||
openGLInfo.WriteString(fmt.Sprintf("\n[OpenGL] Version: %v\n", graphics.GetGLVersionInfo()))
|
||||
openGLInfo.WriteString(fmt.Sprintf("[OpenGL] Vendor: %v\n", graphics.GetGLVendorInfo()))
|
||||
// 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.
|
||||
openGLInfo.WriteString(fmt.Sprintf("[OpenGL] Renderer: %v\n", graphics.GetGLRendererInfo()))
|
||||
openGLInfo.WriteString(fmt.Sprintf("[OpenGL] GLSL Version: %v", graphics.GetGLSLInfo()))
|
||||
version, vendor, renderrer, glsl := graphics.GLInfo()
|
||||
openGLInfo.WriteString(fmt.Sprintf("\n[OpenGL] Version: %v\n", version))
|
||||
openGLInfo.WriteString(fmt.Sprintf("[OpenGL] Vendor: %v\n", vendor))
|
||||
openGLInfo.WriteString(fmt.Sprintf("[OpenGL] Renderer: %v\n", renderrer))
|
||||
openGLInfo.WriteString(fmt.Sprintf("[OpenGL] GLSL Version: %v", glsl))
|
||||
Nan0.log.Debug().Msg(openGLInfo.String())
|
||||
}
|
||||
|
||||
|
|
@ -437,32 +509,42 @@ const (
|
|||
|
||||
// SaveState returns emulator internal state.
|
||||
func SaveState() (State, error) {
|
||||
data := make([]byte, uint(Nan0.serializeSize))
|
||||
size := C.bridge_retro_serialize_size(retroSerializeSize)
|
||||
data := make([]byte, uint(size))
|
||||
rez := false
|
||||
if Nan0.LibCo {
|
||||
rez = *(*bool)(C.same_thread_with_args2(retroSerialize, C.int(CallSerialize), unsafe.Pointer(&data[0]), unsafe.Pointer(&Nan0.serializeSize)))
|
||||
|
||||
if Nan0.LibCo && !Nan0.hackSkipSameThreadSave {
|
||||
rez = *(*bool)(C.same_thread_with_args2(retroSerialize, C.int(CallSerialize), unsafe.Pointer(&data[0]), unsafe.Pointer(&size)))
|
||||
} else {
|
||||
rez = bool(C.bridge_retro_serialize(retroSerialize, unsafe.Pointer(&data[0]), Nan0.serializeSize))
|
||||
rez = bool(C.bridge_retro_serialize(retroSerialize, unsafe.Pointer(&data[0]), size))
|
||||
}
|
||||
|
||||
if !rez {
|
||||
return nil, errors.New("retro_serialize failed")
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// RestoreSaveState restores emulator internal state.
|
||||
func RestoreSaveState(st State) error {
|
||||
if len(st) > 0 {
|
||||
rez := false
|
||||
if Nan0.LibCo {
|
||||
rez = *(*bool)(C.same_thread_with_args2(retroUnserialize, C.int(CallUnserialize), unsafe.Pointer(&st[0]), unsafe.Pointer(&Nan0.serializeSize)))
|
||||
} else {
|
||||
rez = bool(C.bridge_retro_unserialize(retroUnserialize, unsafe.Pointer(&st[0]), Nan0.serializeSize))
|
||||
}
|
||||
if !rez {
|
||||
return errors.New("retro_unserialize failed")
|
||||
}
|
||||
if len(st) <= 0 {
|
||||
return errors.New("empty load state")
|
||||
}
|
||||
|
||||
size := C.size_t(len(st))
|
||||
rez := false
|
||||
|
||||
if Nan0.LibCo {
|
||||
rez = *(*bool)(C.same_thread_with_args2(retroUnserialize, C.int(CallUnserialize), unsafe.Pointer(&st[0]), unsafe.Pointer(&size)))
|
||||
} else {
|
||||
rez = bool(C.bridge_retro_unserialize(retroUnserialize, unsafe.Pointer(&st[0]), size))
|
||||
}
|
||||
|
||||
if !rez {
|
||||
return errors.New("retro_unserialize failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -485,19 +567,19 @@ func RestoreSaveRAM(st State) {
|
|||
}
|
||||
}
|
||||
|
||||
// getMemorySize returns memory region size.
|
||||
func getMemorySize(id C.uint) uint {
|
||||
// memorySize returns memory region size.
|
||||
func memorySize(id C.uint) uint {
|
||||
return uint(C.bridge_retro_get_memory_size(retroGetMemorySize, id))
|
||||
}
|
||||
|
||||
// getMemoryData returns a pointer to memory data.
|
||||
func getMemoryData(id C.uint) unsafe.Pointer {
|
||||
// memoryData returns a pointer to memory data.
|
||||
func memoryData(id C.uint) unsafe.Pointer {
|
||||
return C.bridge_retro_get_memory_data(retroGetMemoryData, id)
|
||||
}
|
||||
|
||||
// ptSaveRam return SRAM memory pointer if core supports it or nil.
|
||||
func ptSaveRAM() *mem {
|
||||
ptr, size := getMemoryData(C.RETRO_MEMORY_SAVE_RAM), getMemorySize(C.RETRO_MEMORY_SAVE_RAM)
|
||||
ptr, size := memoryData(C.RETRO_MEMORY_SAVE_RAM), memorySize(C.RETRO_MEMORY_SAVE_RAM)
|
||||
if ptr == nil || size == 0 {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -527,13 +609,14 @@ func (m Metadata) HasHack(h string) bool {
|
|||
}
|
||||
|
||||
var (
|
||||
//retroAPIVersion unsafe.Pointer
|
||||
retroAPIVersion unsafe.Pointer
|
||||
retroDeinit unsafe.Pointer
|
||||
retroGetSystemAVInfo unsafe.Pointer
|
||||
retroGetSystemInfo unsafe.Pointer
|
||||
coreLib unsafe.Pointer
|
||||
retroInit unsafe.Pointer
|
||||
retroLoadGame unsafe.Pointer
|
||||
retroReset unsafe.Pointer
|
||||
retroRun unsafe.Pointer
|
||||
retroSetAudioSample unsafe.Pointer
|
||||
retroSetAudioSampleBatch unsafe.Pointer
|
||||
|
|
@ -552,8 +635,7 @@ var (
|
|||
|
||||
//export coreVideoRefresh
|
||||
func coreVideoRefresh(data unsafe.Pointer, width, height uint, packed uint) {
|
||||
if Nan0.stopped.Load() {
|
||||
Nan0.log.Warn().Msgf(">>> skip video")
|
||||
if Nan0.Stopped.Load() {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -562,24 +644,24 @@ func coreVideoRefresh(data unsafe.Pointer, width, height uint, packed uint) {
|
|||
// (and proper frame display time, for example: 1->1/60=16.6ms, 2->10ms, 3->23ms, 4->16.6ms)
|
||||
// this is useful only for cores with variable framerate, for the fixed framerate cores this adds stutter
|
||||
// !to find docs on Libretro refresh sync and frame times
|
||||
t := time.Now().UnixNano()
|
||||
dt := Nan0.tickTime
|
||||
// override frame rendering with dynamic frame times
|
||||
if Nan0.vfr {
|
||||
t := time.Now().UnixNano()
|
||||
dt = t - Nan0.LastFrameTime
|
||||
Nan0.LastFrameTime = t
|
||||
}
|
||||
Nan0.LastFrameTime = t
|
||||
|
||||
// some cores can return nothing
|
||||
// !to add duplicate if can dup
|
||||
// when the core returns a duplicate frame
|
||||
if data == nil {
|
||||
Nan0.Handlers.OnDup()
|
||||
return
|
||||
}
|
||||
|
||||
// calculate real frame width in pixels from packed data (realWidth >= width)
|
||||
// some cores or games output zero pitch, i.e. N64 Mupen
|
||||
bpp := Nan0.Video.PixFmt.BPP
|
||||
if packed == 0 {
|
||||
packed = width * Nan0.Video.PixFmt.BPP
|
||||
packed = width * bpp
|
||||
}
|
||||
// calculate space for the video frame
|
||||
bytes := packed * height
|
||||
|
|
@ -600,48 +682,9 @@ func coreVideoRefresh(data unsafe.Pointer, width, height uint, packed uint) {
|
|||
Nan0.Handlers.OnVideo(data_, int32(dt), FrameInfo{W: width, H: height, Stride: packed})
|
||||
}
|
||||
|
||||
//export coreInputPoll
|
||||
func coreInputPoll() {}
|
||||
|
||||
//export coreInputState
|
||||
func coreInputState(port C.unsigned, device C.unsigned, index C.unsigned, id C.unsigned) C.int16_t {
|
||||
if uint(port) >= uint(MaxPort) {
|
||||
return KeyReleased
|
||||
}
|
||||
|
||||
if device == C.RETRO_DEVICE_ANALOG {
|
||||
if index > C.RETRO_DEVICE_INDEX_ANALOG_RIGHT || id > C.RETRO_DEVICE_ID_ANALOG_Y {
|
||||
return 0
|
||||
}
|
||||
axis := index*2 + id
|
||||
value := Nan0.Handlers.OnDpad(uint(port), uint(axis))
|
||||
if value != 0 {
|
||||
return (C.int16_t)(value)
|
||||
}
|
||||
}
|
||||
|
||||
key := int(id)
|
||||
if key > lastKey || index > 0 || device != C.RETRO_DEVICE_JOYPAD {
|
||||
return KeyReleased
|
||||
}
|
||||
if Nan0.Handlers.OnKeyPress(uint(port), key) == KeyPressed {
|
||||
return KeyPressed
|
||||
}
|
||||
return KeyReleased
|
||||
}
|
||||
|
||||
//export coreAudioSample
|
||||
func coreAudioSample(l, r C.int16_t) {
|
||||
frame := []C.int16_t{l, r}
|
||||
coreAudioSampleBatch(unsafe.Pointer(&frame), 1)
|
||||
}
|
||||
|
||||
//export coreAudioSampleBatch
|
||||
func coreAudioSampleBatch(data unsafe.Pointer, frames C.size_t) C.size_t {
|
||||
if Nan0.stopped.Load() {
|
||||
if Nan0.log.GetLevel() < logger.InfoLevel {
|
||||
Nan0.log.Warn().Msgf(">>> skip %v audio frames", frames)
|
||||
}
|
||||
if Nan0.Stopped.Load() {
|
||||
return frames
|
||||
}
|
||||
Nan0.Handlers.OnAudio(data, int(frames)<<1)
|
||||
|
|
@ -669,44 +712,40 @@ func coreLog(level C.enum_retro_log_level, msg *C.char) {
|
|||
}
|
||||
|
||||
//export coreGetCurrentFramebuffer
|
||||
func coreGetCurrentFramebuffer() C.uintptr_t { return (C.uintptr_t)(graphics.GetGlFbo()) }
|
||||
func coreGetCurrentFramebuffer() C.uintptr_t { return (C.uintptr_t)(graphics.GlFbo()) }
|
||||
|
||||
//export coreGetProcAddress
|
||||
func coreGetProcAddress(sym *C.char) C.retro_proc_address_t {
|
||||
return (C.retro_proc_address_t)(graphics.GetGlProcAddress(C.GoString(sym)))
|
||||
return (C.retro_proc_address_t)(graphics.GlProcAddress(C.GoString(sym)))
|
||||
}
|
||||
|
||||
//export coreEnvironment
|
||||
func coreEnvironment(cmd C.unsigned, data unsafe.Pointer) C.bool {
|
||||
// spammy
|
||||
switch cmd {
|
||||
case C.RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE:
|
||||
return false
|
||||
case C.RETRO_ENVIRONMENT_GET_AUDIO_VIDEO_ENABLE:
|
||||
return false
|
||||
}
|
||||
|
||||
// see core_environment_cgo
|
||||
|
||||
switch cmd {
|
||||
case C.RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO:
|
||||
Nan0.log.Debug().Msgf("retro_set_system_av_info")
|
||||
av := *(*C.struct_retro_system_av_info)(data)
|
||||
Nan0.log.Info().Msgf(">>> SET SYS AV INFO: %v", av)
|
||||
Nan0.sysAvInfo = av
|
||||
go func() {
|
||||
if Nan0.OnSystemAvInfo != nil {
|
||||
Nan0.OnSystemAvInfo()
|
||||
}
|
||||
}()
|
||||
if isGeometryDifferent(av.geometry) {
|
||||
geometryChange(av.geometry)
|
||||
}
|
||||
return true
|
||||
case C.RETRO_ENVIRONMENT_SET_GEOMETRY:
|
||||
Nan0.log.Debug().Msgf("retro_set_geometry")
|
||||
geom := *(*C.struct_retro_game_geometry)(data)
|
||||
Nan0.log.Info().Msgf(">>> GEOMETRY: %v", geom)
|
||||
if isGeometryDifferent(geom) {
|
||||
geometryChange(geom)
|
||||
}
|
||||
return true
|
||||
case C.RETRO_ENVIRONMENT_SET_ROTATION:
|
||||
setRotation((*(*uint)(data) % 4) * 90)
|
||||
return true
|
||||
case C.RETRO_ENVIRONMENT_GET_CAN_DUPE:
|
||||
*(*C.bool)(data) = C.bool(true)
|
||||
return true
|
||||
dup := C.bool(Nan0.meta.FrameDup)
|
||||
*(*C.bool)(data) = dup
|
||||
return dup
|
||||
case C.RETRO_ENVIRONMENT_GET_USERNAME:
|
||||
*(**C.char)(data) = Nan0.cUserName
|
||||
return true
|
||||
|
|
@ -735,22 +774,22 @@ func coreEnvironment(cmd C.unsigned, data unsafe.Pointer) C.bool {
|
|||
return true
|
||||
}
|
||||
return false
|
||||
case C.RETRO_ENVIRONMENT_SHUTDOWN:
|
||||
//window.SetShouldClose(true)
|
||||
return false
|
||||
case C.RETRO_ENVIRONMENT_GET_VARIABLE:
|
||||
if (*Nan0.options) == nil {
|
||||
if Nan0.options == nil {
|
||||
return false
|
||||
}
|
||||
rv := (*C.struct_retro_variable)(data)
|
||||
key := C.GoString(rv.key)
|
||||
if v, ok := (*Nan0.options)[key]; ok {
|
||||
if v, ok := Nan0.options[key]; ok {
|
||||
// make Go strings null-terminated copies ;_;
|
||||
(*Nan0.options)[key] = v + "\x00"
|
||||
Nan0.options[key] = v + "\x00"
|
||||
ptr := unsafe.Pointer(unsafe.StringData(Nan0.options[key]))
|
||||
var p runtime.Pinner
|
||||
p.Pin(ptr)
|
||||
defer p.Unpin()
|
||||
// cast to C string and set the value
|
||||
// we hope the string won't be collected while C needs it
|
||||
rv.value = (*C.char)(unsafe.Pointer(unsafe.StringData((*Nan0.options)[key])))
|
||||
Nan0.log.Debug().Msgf("Set %s=%v", key, v)
|
||||
rv.value = (*C.char)(ptr)
|
||||
Nan0.log.Debug().Msgf("Set %v=%v", key, v)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
|
@ -763,30 +802,33 @@ func coreEnvironment(cmd C.unsigned, data unsafe.Pointer) C.bool {
|
|||
}
|
||||
return false
|
||||
case C.RETRO_ENVIRONMENT_SET_CONTROLLER_INFO:
|
||||
// !to rewrite
|
||||
if !Nan0.multitap.supported {
|
||||
if Nan0.log.GetLevel() > logger.DebugLevel {
|
||||
return false
|
||||
}
|
||||
info := (*[100]C.struct_retro_controller_info)(data)
|
||||
var i C.unsigned
|
||||
for i = 0; unsafe.Pointer(info[i].types) != nil; i++ {
|
||||
var j C.unsigned
|
||||
types := (*[100]C.struct_retro_controller_description)(unsafe.Pointer(info[i].types))
|
||||
for j = 0; j < info[i].num_types; j++ {
|
||||
if C.GoString(types[j].desc) == "Multitap" {
|
||||
Nan0.multitap.value = types[j].id
|
||||
return true
|
||||
}
|
||||
|
||||
info := (*[64]C.struct_retro_controller_info)(data)
|
||||
for c, controller := range info {
|
||||
tp := unsafe.Pointer(controller.types)
|
||||
if tp == nil {
|
||||
break
|
||||
}
|
||||
cInfo := strings.Builder{}
|
||||
cInfo.WriteString(fmt.Sprintf("Controller [%v] ", c))
|
||||
cd := (*[32]C.struct_retro_controller_description)(tp)
|
||||
delim := ", "
|
||||
n := int(controller.num_types)
|
||||
for i := range n {
|
||||
if i == n-1 {
|
||||
delim = ""
|
||||
}
|
||||
cInfo.WriteString(fmt.Sprintf("%v: %v%s", cd[i].id, C.GoString(cd[i].desc), delim))
|
||||
}
|
||||
//Nan0.log.Debug().Msgf("%v", cInfo.String())
|
||||
}
|
||||
return false
|
||||
case C.RETRO_ENVIRONMENT_GET_CLEAR_ALL_THREAD_WAITS_CB:
|
||||
C.bridge_clear_all_thread_waits_cb(data)
|
||||
return true
|
||||
case C.RETRO_ENVIRONMENT_GET_SAVESTATE_CONTEXT:
|
||||
if ctx := (*C.int)(data); ctx != nil {
|
||||
*ctx = C.RETRO_SAVESTATE_CONTEXT_NORMAL
|
||||
}
|
||||
case C.RETRO_ENVIRONMENT_SET_KEYBOARD_CALLBACK:
|
||||
Nan0.log.Debug().Msgf("Keyboard event callback was set")
|
||||
Nan0.keyboardCb = (*C.struct_retro_keyboard_callback)(data)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
|
@ -816,22 +858,23 @@ func initVideo() {
|
|||
context = graphics.CtxUnknown
|
||||
}
|
||||
|
||||
sdl, err := graphics.NewSDLContext(graphics.Config{
|
||||
Ctx: context,
|
||||
W: int(Nan0.sysAvInfo.geometry.max_width),
|
||||
H: int(Nan0.sysAvInfo.geometry.max_height),
|
||||
GLAutoContext: Nan0.Video.gl.autoCtx,
|
||||
GLVersionMajor: uint(Nan0.Video.hw.version_major),
|
||||
GLVersionMinor: uint(Nan0.Video.hw.version_minor),
|
||||
GLHasDepth: bool(Nan0.Video.hw.depth),
|
||||
GLHasStencil: bool(Nan0.Video.hw.stencil),
|
||||
}, Nan0.log)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
Nan0.sdlCtx = sdl
|
||||
thread.Main(func() {
|
||||
var err error
|
||||
Nan0.sdlCtx, err = graphics.NewSDLContext(graphics.Config{
|
||||
Ctx: context,
|
||||
W: int(Nan0.sys.av.geometry.max_width),
|
||||
H: int(Nan0.sys.av.geometry.max_height),
|
||||
GLAutoContext: Nan0.Video.gl.autoCtx,
|
||||
GLVersionMajor: uint(Nan0.Video.hw.version_major),
|
||||
GLVersionMinor: uint(Nan0.Video.hw.version_minor),
|
||||
GLHasDepth: bool(Nan0.Video.hw.depth),
|
||||
GLHasStencil: bool(Nan0.Video.hw.stencil),
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
|
||||
C.bridge_context_reset(Nan0.Video.hw.context_reset)
|
||||
if Nan0.log.GetLevel() < logger.InfoLevel {
|
||||
printOpenGLDriverInfo()
|
||||
}
|
||||
|
|
@ -842,10 +885,59 @@ func deinitVideo() {
|
|||
if !Nan0.hackSkipHwContextDestroy {
|
||||
C.bridge_context_reset(Nan0.Video.hw.context_destroy)
|
||||
}
|
||||
if err := Nan0.sdlCtx.Deinit(); err != nil {
|
||||
Nan0.log.Error().Err(err).Msg("deinit fail")
|
||||
}
|
||||
thread.Main(func() {
|
||||
if err := Nan0.sdlCtx.Deinit(); err != nil {
|
||||
Nan0.log.Error().Err(err).Msg("deinit fail")
|
||||
}
|
||||
})
|
||||
Nan0.Video.gl.enabled = false
|
||||
Nan0.Video.gl.autoCtx = false
|
||||
Nan0.hackSkipHwContextDestroy = false
|
||||
Nan0.hackSkipSameThreadSave = false
|
||||
thread.SwitchGraphics(false)
|
||||
}
|
||||
|
||||
type limit struct {
|
||||
d time.Duration
|
||||
t *time.Timer
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewLimit(d time.Duration) func(f func()) {
|
||||
l := &limit{d: d}
|
||||
return func(f func()) { l.push(f) }
|
||||
}
|
||||
|
||||
func (d *limit) push(f func()) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
if d.t != nil {
|
||||
d.t.Stop()
|
||||
}
|
||||
d.t = time.AfterFunc(d.d, f)
|
||||
}
|
||||
|
||||
func geometryChange(geom C.struct_retro_game_geometry) {
|
||||
Nan0.limiter(func() {
|
||||
old := Nan0.sys.av.geometry
|
||||
Nan0.sys.av.geometry = geom
|
||||
|
||||
if Nan0.Video.gl.enabled && (old.max_width != geom.max_width || old.max_height != geom.max_height) {
|
||||
// (for LRPS2) makes the max height bigger increasing SDL2 and OpenGL buffers slightly
|
||||
Nan0.sys.av.geometry.max_height = C.unsigned(float32(Nan0.sys.av.geometry.max_height) * 1.5)
|
||||
bufS := uint(geom.max_width*Nan0.sys.av.geometry.max_height) * Nan0.Video.PixFmt.BPP
|
||||
graphics.SetBuffer(int(bufS))
|
||||
Nan0.log.Debug().Msgf("OpenGL frame buffer: %v", bufS)
|
||||
}
|
||||
|
||||
if Nan0.OnSystemAvInfo != nil {
|
||||
Nan0.log.Debug().Msgf(">>> geometry change %v -> %v", old, geom)
|
||||
go Nan0.OnSystemAvInfo()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func isGeometryDifferent(geom C.struct_retro_game_geometry) bool {
|
||||
return Nan0.sys.av.geometry.base_width != geom.base_width ||
|
||||
Nan0.sys.av.geometry.base_height != geom.base_height
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
#ifndef FRONTEND_H__
|
||||
#define FRONTEND_H__
|
||||
|
||||
void bridge_call(void *f);
|
||||
void bridge_set_callback(void *f, void *callback);
|
||||
|
||||
bool bridge_retro_load_game(void *f, struct retro_game_info *gi);
|
||||
void bridge_retro_unload_game(void *f);
|
||||
bool bridge_retro_serialize(void *f, void *data, size_t size);
|
||||
size_t bridge_retro_serialize_size(void *f);
|
||||
bool bridge_retro_unserialize(void *f, void *data, size_t size);
|
||||
|
|
@ -11,18 +13,11 @@ unsigned bridge_retro_api_version(void *f);
|
|||
size_t bridge_retro_get_memory_size(void *f, unsigned id);
|
||||
void *bridge_retro_get_memory_data(void *f, unsigned id);
|
||||
void bridge_context_reset(retro_hw_context_reset_t f);
|
||||
void bridge_retro_deinit(void *f);
|
||||
void bridge_retro_get_system_av_info(void *f, struct retro_system_av_info *si);
|
||||
void bridge_retro_get_system_info(void *f, struct retro_system_info *si);
|
||||
void bridge_retro_init(void *f);
|
||||
void bridge_retro_run(void *f);
|
||||
void bridge_retro_set_audio_sample(void *f, void *callback);
|
||||
void bridge_retro_set_audio_sample_batch(void *f, void *callback);
|
||||
void bridge_retro_set_controller_port_device(void *f, unsigned port, unsigned device);
|
||||
void bridge_retro_set_input_poll(void *f, void *callback);
|
||||
void bridge_retro_set_input_state(void *f, void *callback);
|
||||
void bridge_retro_set_video_refresh(void *f, void *callback);
|
||||
void bridge_clear_all_thread_waits_cb(void *f);
|
||||
void bridge_retro_keyboard_callback(void *f, bool down, unsigned keycode, uint32_t character, uint16_t keyModifiers);
|
||||
|
||||
bool core_environment_cgo(unsigned cmd, void *data);
|
||||
int16_t core_input_state_cgo(unsigned port, unsigned device, unsigned index, unsigned id);
|
||||
|
|
|
|||
22
pkg/worker/caged/libretro/nanoarch/nanoarch_test.go
Normal file
22
pkg/worker/caged/libretro/nanoarch/nanoarch_test.go
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package nanoarch
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLimit(t *testing.T) {
|
||||
c := atomic.Int32{}
|
||||
lim := NewLimit(50 * time.Millisecond)
|
||||
|
||||
for range 10 {
|
||||
lim(func() {
|
||||
c.Add(1)
|
||||
})
|
||||
}
|
||||
|
||||
if c.Load() > 1 {
|
||||
t.Errorf("should be just 1")
|
||||
}
|
||||
}
|
||||
|
|
@ -15,17 +15,6 @@ type RecordingFrontend struct {
|
|||
}
|
||||
|
||||
func WithRecording(fe Emulator, rec bool, user string, game string, conf config.Recording, log *logger.Logger) *RecordingFrontend {
|
||||
|
||||
pix := ""
|
||||
switch fe.PixFormat() {
|
||||
case 0:
|
||||
pix = "rgb1555"
|
||||
case 1:
|
||||
pix = "brga"
|
||||
case 2:
|
||||
pix = "rgb565"
|
||||
}
|
||||
|
||||
rr := &RecordingFrontend{Emulator: fe, rec: recorder.NewRecording(
|
||||
recorder.Meta{UserName: user},
|
||||
log,
|
||||
|
|
@ -36,7 +25,6 @@ func WithRecording(fe Emulator, rec bool, user string, game string, conf config.
|
|||
Zip: conf.Zip,
|
||||
Vsync: true,
|
||||
Flip: fe.Flipped(),
|
||||
Pix: pix,
|
||||
})}
|
||||
rr.ToggleRecording(rec, user)
|
||||
return rr
|
||||
|
|
@ -70,6 +58,7 @@ func (r *RecordingFrontend) LoadGame(path string) error {
|
|||
}
|
||||
r.rec.SetFramerate(float64(r.Emulator.FPS()))
|
||||
r.rec.SetAudioFrequency(r.Emulator.AudioSampleRate())
|
||||
r.rec.SetPixFormat(r.Emulator.PixFormat())
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
package arch
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// See: https://gist.github.com/asukakenji/f15ba7e588ac42795f421b48b8aede63.
|
||||
var libretroOsArchMap = map[string]Info{
|
||||
"linux:amd64": {Os: "linux", Arch: "x86_64", LibExt: ".so"},
|
||||
"linux:arm": {Os: "linux", Arch: "armv7-neon-hf", LibExt: ".so"},
|
||||
"windows:amd64": {Os: "windows", Arch: "x86_64", LibExt: ".dll"},
|
||||
"darwin:amd64": {Os: "osx", Arch: "x86_64", Vendor: "apple", LibExt: ".dylib"},
|
||||
"darwin:arm64": {Os: "osx", Arch: "arm64", Vendor: "apple", LibExt: ".dylib"},
|
||||
}
|
||||
|
||||
// Info contains Libretro core lib platform info.
|
||||
// And cores are just C-compiled libraries.
|
||||
// See: https://buildbot.libretro.com/nightly.
|
||||
type Info struct {
|
||||
// bottom: x86_64, x86, ...
|
||||
Arch string
|
||||
// middle: windows, ios, ...
|
||||
Os string
|
||||
// top level: apple, nintendo, ...
|
||||
Vendor string
|
||||
|
||||
// platform dependent library file extension (dot-prefixed)
|
||||
LibExt string
|
||||
}
|
||||
|
||||
func Guess() (Info, error) {
|
||||
key := runtime.GOOS + ":" + runtime.GOARCH
|
||||
if arch, ok := libretroOsArchMap[key]; ok {
|
||||
return arch, nil
|
||||
} else {
|
||||
return Info{}, errors.New("core mapping not found for " + key)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
package buildbot
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/giongto35/cloud-game/v3/pkg/worker/caged/libretro/repo/arch"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/worker/caged/libretro/repo/raw"
|
||||
)
|
||||
|
||||
type RepoBuildbot struct {
|
||||
raw.Repo
|
||||
}
|
||||
|
||||
func NewBuildbotRepo(address string, compression string) RepoBuildbot {
|
||||
return RepoBuildbot{
|
||||
Repo: raw.Repo{
|
||||
Address: address,
|
||||
Compression: compression,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r RepoBuildbot) GetCoreUrl(file string, info arch.Info) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(r.Address + "/")
|
||||
if info.Vendor != "" {
|
||||
sb.WriteString(info.Vendor + "/")
|
||||
}
|
||||
sb.WriteString(info.Os + "/" + info.Arch + "/latest/" + file + info.LibExt)
|
||||
if r.Compression != "" {
|
||||
sb.WriteString("." + r.Compression)
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
package buildbot
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/giongto35/cloud-game/v3/pkg/worker/caged/libretro/repo/arch"
|
||||
)
|
||||
|
||||
func TestBuildbotRepo(t *testing.T) {
|
||||
testAddress := "https://test.me"
|
||||
tests := []struct {
|
||||
file string
|
||||
compression string
|
||||
arch arch.Info
|
||||
resultUrl string
|
||||
}{
|
||||
{
|
||||
file: "uber_core",
|
||||
arch: arch.Info{
|
||||
Os: "linux",
|
||||
Arch: "x86_64",
|
||||
LibExt: ".so",
|
||||
},
|
||||
resultUrl: testAddress + "/" + "linux/x86_64/latest/uber_core.so",
|
||||
},
|
||||
{
|
||||
file: "uber_core",
|
||||
compression: "zip",
|
||||
arch: arch.Info{
|
||||
Os: "linux",
|
||||
Arch: "x86_64",
|
||||
LibExt: ".so",
|
||||
},
|
||||
resultUrl: testAddress + "/" + "linux/x86_64/latest/uber_core.so.zip",
|
||||
},
|
||||
{
|
||||
file: "uber_core",
|
||||
arch: arch.Info{
|
||||
Os: "osx",
|
||||
Arch: "x86_64",
|
||||
Vendor: "apple",
|
||||
LibExt: ".dylib",
|
||||
},
|
||||
resultUrl: testAddress + "/" + "apple/osx/x86_64/latest/uber_core.dylib",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
rep := NewBuildbotRepo(testAddress, test.compression)
|
||||
url := rep.GetCoreUrl(test.file, test.arch)
|
||||
if url != test.resultUrl {
|
||||
t.Errorf("seems that expected link address is incorrect (%v) for file %s %+v", url, test.file, test.arch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
package github
|
||||
|
||||
import (
|
||||
"github.com/giongto35/cloud-game/v3/pkg/worker/caged/libretro/repo/arch"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/worker/caged/libretro/repo/buildbot"
|
||||
)
|
||||
|
||||
type RepoGithub struct {
|
||||
buildbot.RepoBuildbot
|
||||
}
|
||||
|
||||
func NewGithubRepo(address string, compression string) RepoGithub {
|
||||
return RepoGithub{RepoBuildbot: buildbot.NewBuildbotRepo(address, compression)}
|
||||
}
|
||||
|
||||
func (r RepoGithub) GetCoreUrl(file string, info arch.Info) string {
|
||||
return r.RepoBuildbot.GetCoreUrl(file, info) + "?raw=true"
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
package github
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/giongto35/cloud-game/v3/pkg/worker/caged/libretro/repo/arch"
|
||||
)
|
||||
|
||||
func TestBuildbotRepo(t *testing.T) {
|
||||
testAddress := "https://test.me"
|
||||
tests := []struct {
|
||||
file string
|
||||
compression string
|
||||
arch arch.Info
|
||||
resultUrl string
|
||||
}{
|
||||
{
|
||||
file: "uber_core",
|
||||
arch: arch.Info{
|
||||
Os: "linux",
|
||||
Arch: "x86_64",
|
||||
LibExt: ".so",
|
||||
},
|
||||
resultUrl: testAddress + "/" + "linux/x86_64/latest/uber_core.so?raw=true",
|
||||
},
|
||||
{
|
||||
file: "uber_core",
|
||||
compression: "zip",
|
||||
arch: arch.Info{
|
||||
Os: "linux",
|
||||
Arch: "x86_64",
|
||||
LibExt: ".so",
|
||||
},
|
||||
resultUrl: testAddress + "/" + "linux/x86_64/latest/uber_core.so.zip?raw=true",
|
||||
},
|
||||
{
|
||||
file: "uber_core",
|
||||
arch: arch.Info{
|
||||
Os: "osx",
|
||||
Arch: "x86_64",
|
||||
Vendor: "apple",
|
||||
LibExt: ".dylib",
|
||||
},
|
||||
resultUrl: testAddress + "/" + "apple/osx/x86_64/latest/uber_core.dylib?raw=true",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
rep := NewGithubRepo(testAddress, test.compression)
|
||||
url := rep.GetCoreUrl(test.file, test.arch)
|
||||
if url != test.resultUrl {
|
||||
t.Errorf("seems that expected link address is incorrect (%v) for file %s %+v", url, test.file, test.arch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
package raw
|
||||
|
||||
import "github.com/giongto35/cloud-game/v3/pkg/worker/caged/libretro/repo/arch"
|
||||
|
||||
type Repo struct {
|
||||
Address string
|
||||
Compression string
|
||||
}
|
||||
|
||||
// NewRawRepo defines a simple zip file containing
|
||||
// all the cores that will be extracted as is.
|
||||
func NewRawRepo(address string) Repo { return Repo{Address: address, Compression: "zip"} }
|
||||
|
||||
func (r Repo) GetCoreUrl(_ string, _ arch.Info) string { return r.Address }
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"github.com/giongto35/cloud-game/v3/pkg/worker/caged/libretro/repo/arch"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/worker/caged/libretro/repo/buildbot"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/worker/caged/libretro/repo/github"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/worker/caged/libretro/repo/raw"
|
||||
)
|
||||
|
||||
type (
|
||||
Data struct {
|
||||
Url string
|
||||
Compression string
|
||||
}
|
||||
|
||||
Repository interface {
|
||||
GetCoreUrl(file string, info arch.Info) (url string)
|
||||
}
|
||||
)
|
||||
|
||||
func New(kind string, url string, compression string, defaultRepo string) Repository {
|
||||
var repository Repository
|
||||
switch kind {
|
||||
case "raw":
|
||||
repository = raw.NewRawRepo(url)
|
||||
case "github":
|
||||
repository = github.NewGithubRepo(url, compression)
|
||||
case "buildbot":
|
||||
repository = buildbot.NewBuildbotRepo(url, compression)
|
||||
default:
|
||||
if defaultRepo != "" {
|
||||
repository = New(defaultRepo, url, compression, "")
|
||||
}
|
||||
}
|
||||
return repository
|
||||
}
|
||||
|
|
@ -10,9 +10,11 @@ import (
|
|||
|
||||
type (
|
||||
Storage interface {
|
||||
MainPath() string
|
||||
GetSavePath() string
|
||||
GetSRAMPath() string
|
||||
SetMainSaveName(name string)
|
||||
SetNonBlocking(v bool)
|
||||
Load(path string) ([]byte, error)
|
||||
Save(path string, data []byte) error
|
||||
}
|
||||
|
|
@ -24,17 +26,27 @@ type (
|
|||
// needed for Google Cloud save/restore which
|
||||
// doesn't support multiple files
|
||||
MainSave string
|
||||
NonBlock bool
|
||||
}
|
||||
ZipStorage struct {
|
||||
Storage
|
||||
}
|
||||
)
|
||||
|
||||
func (s *StateStorage) SetMainSaveName(name string) { s.MainSave = name }
|
||||
func (s *StateStorage) GetSavePath() string { return filepath.Join(s.Path, s.MainSave+".dat") }
|
||||
func (s *StateStorage) GetSRAMPath() string { return filepath.Join(s.Path, s.MainSave+".srm") }
|
||||
func (s *StateStorage) Load(path string) ([]byte, error) { return os.ReadFile(path) }
|
||||
func (s *StateStorage) Save(path string, dat []byte) error { return os.WriteFile(path, dat, 0644) }
|
||||
func (s *StateStorage) MainPath() string { return s.MainSave }
|
||||
func (s *StateStorage) SetMainSaveName(name string) { s.MainSave = name }
|
||||
func (s *StateStorage) SetNonBlocking(v bool) { s.NonBlock = v }
|
||||
func (s *StateStorage) GetSavePath() string { return filepath.Join(s.Path, s.MainSave+".dat") }
|
||||
func (s *StateStorage) GetSRAMPath() string { return filepath.Join(s.Path, s.MainSave+".srm") }
|
||||
func (s *StateStorage) Load(path string) ([]byte, error) { return os.ReadFile(path) }
|
||||
func (s *StateStorage) Save(path string, dat []byte) error {
|
||||
if s.NonBlock {
|
||||
go func() { _ = os.WriteFile(path, dat, 0644) }()
|
||||
return nil
|
||||
}
|
||||
|
||||
return os.WriteFile(path, dat, 0644)
|
||||
}
|
||||
|
||||
func (z *ZipStorage) GetSavePath() string { return z.Storage.GetSavePath() + zip.Ext }
|
||||
func (z *ZipStorage) GetSRAMPath() string { return z.Storage.GetSRAMPath() + zip.Ext }
|
||||
|
|
|
|||
|
|
@ -1,128 +0,0 @@
|
|||
package cloud
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/giongto35/cloud-game/v3/pkg/os"
|
||||
)
|
||||
|
||||
// !to replace all with unified s3 api
|
||||
|
||||
type Storage interface {
|
||||
Save(name string, localPath string) (err error)
|
||||
Load(name string) (data []byte, err error)
|
||||
}
|
||||
|
||||
type OracleDataStorageClient struct {
|
||||
accessURL string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func Store(provider, key string) (Storage, error) {
|
||||
var st Storage
|
||||
var err error
|
||||
switch provider {
|
||||
case "oracle":
|
||||
st, err = NewOracleDataStorageClient(key)
|
||||
case "coordinator":
|
||||
default:
|
||||
}
|
||||
return st, err
|
||||
}
|
||||
|
||||
// NewOracleDataStorageClient returns either a new Oracle Data Storage
|
||||
// client or some error in case of failure.
|
||||
// Oracle infrastructure access is based on pre-authenticated requests,
|
||||
// see: https://docs.oracle.com/en-us/iaas/Content/Object/Tasks/usingpreauthenticatedrequests.htm
|
||||
//
|
||||
// It follows broken Google Cloud Storage client design.
|
||||
func NewOracleDataStorageClient(accessURL string) (*OracleDataStorageClient, error) {
|
||||
if accessURL == "" {
|
||||
return nil, errors.New("pre-authenticated request was not specified")
|
||||
}
|
||||
return &OracleDataStorageClient{
|
||||
accessURL: accessURL,
|
||||
client: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *OracleDataStorageClient) Save(name string, localPath string) (err error) {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
dat, err := os.ReadFile(localPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("PUT", s.accessURL+name, bytes.NewBuffer(dat))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
if resp.StatusCode != 200 {
|
||||
return errors.New(resp.Status)
|
||||
}
|
||||
|
||||
dstMD5 := resp.Header.Get("Opc-Content-Md5")
|
||||
srcMD5 := base64.StdEncoding.EncodeToString(md5Hash(dat))
|
||||
if dstMD5 != srcMD5 {
|
||||
return fmt.Errorf("MD5 mismatch %v != %v", srcMD5, dstMD5)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *OracleDataStorageClient) Load(name string) (data []byte, err error) {
|
||||
if s == nil {
|
||||
return nil, errors.New("cloud storage was not initialized")
|
||||
}
|
||||
|
||||
res, err := s.client.Get(s.accessURL + name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = res.Body.Close()
|
||||
}()
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
return nil, errors.New(res.Status)
|
||||
}
|
||||
|
||||
dat, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dstMD5 := res.Header.Get("Content-Md5")
|
||||
srcMD5 := base64.StdEncoding.EncodeToString(md5Hash(dat))
|
||||
if dstMD5 != srcMD5 {
|
||||
return nil, fmt.Errorf("MD5 mismatch %v != %v", srcMD5, dstMD5)
|
||||
}
|
||||
|
||||
return dat, nil
|
||||
}
|
||||
|
||||
func md5Hash(data []byte) []byte {
|
||||
hash := md5.New()
|
||||
hash.Write(data)
|
||||
return hash.Sum(nil)
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
package cloud
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type rtFunc func(req *http.Request) *http.Response
|
||||
|
||||
func (f rtFunc) RoundTrip(req *http.Request) (*http.Response, error) { return f(req), nil }
|
||||
|
||||
func newTestClient(fn rtFunc) *http.Client {
|
||||
return &http.Client{
|
||||
Transport: fn,
|
||||
}
|
||||
}
|
||||
|
||||
func TestOracleSave(t *testing.T) {
|
||||
client, _ := NewOracleDataStorageClient("test-url/")
|
||||
client.client = newTestClient(func(req *http.Request) *http.Response {
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: io.NopCloser(strings.NewReader("")),
|
||||
Header: map[string][]string{
|
||||
"Opc-Content-Md5": {"CY9rzUYh03PK3k6DJie09g=="},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
tempFile, err := os.CreateTemp("", "oracle_test.file")
|
||||
if err != nil {
|
||||
t.Errorf("%v", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = tempFile.Close()
|
||||
err := os.Remove(tempFile.Name())
|
||||
if err != nil {
|
||||
t.Errorf("%v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
_, err = tempFile.WriteString("test")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = client.Save("oracle_test.file", tempFile.Name())
|
||||
if err != nil {
|
||||
t.Errorf("can't save, err: %v", err)
|
||||
}
|
||||
}
|
||||
91
pkg/worker/cloud/s3.go
Normal file
91
pkg/worker/cloud/s3.go
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
package cloud
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/giongto35/cloud-game/v3/pkg/logger"
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type S3Client struct {
|
||||
c *minio.Client
|
||||
bucket string
|
||||
log *logger.Logger
|
||||
}
|
||||
|
||||
func NewS3Client(endpoint, bucket, key, secret string, log *logger.Logger) (*S3Client, error) {
|
||||
s3Client, err := minio.New(endpoint, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(key, secret, ""),
|
||||
Secure: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
exists, err := s3Client.BucketExists(context.Background(), bucket)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
return nil, errors.New("bucket doesn't exist")
|
||||
}
|
||||
|
||||
return &S3Client{bucket: bucket, c: s3Client, log: log}, nil
|
||||
}
|
||||
|
||||
func (s *S3Client) SetBucket(bucket string) { s.bucket = bucket }
|
||||
|
||||
func (s *S3Client) Save(name string, data []byte, meta map[string]string) error {
|
||||
if s == nil || s.c == nil {
|
||||
return errors.New("s3 client was not initialised")
|
||||
}
|
||||
r := bytes.NewReader(data)
|
||||
opts := minio.PutObjectOptions{
|
||||
ContentType: "application/octet-stream",
|
||||
SendContentMd5: true,
|
||||
}
|
||||
if meta != nil {
|
||||
opts.UserMetadata = meta
|
||||
}
|
||||
|
||||
info, err := s.c.PutObject(context.Background(), s.bucket, name, r, int64(len(data)), opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.log.Debug().Msgf("Uploaded: %v", info)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *S3Client) Load(name string) (data []byte, err error) {
|
||||
if s == nil || s.c == nil {
|
||||
return nil, errors.New("s3 client was not initialised")
|
||||
}
|
||||
|
||||
r, err := s.c.GetObject(context.Background(), s.bucket, name, minio.GetObjectOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { err = errors.Join(err, r.Close()) }()
|
||||
|
||||
stats, err := r.Stat()
|
||||
log.Debug().Msgf("Downloaded: %v", stats)
|
||||
dat, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dat, nil
|
||||
}
|
||||
|
||||
func (s *S3Client) Has(name string) bool {
|
||||
if s == nil || s.c == nil {
|
||||
return false
|
||||
}
|
||||
_, err := s.c.StatObject(context.Background(), s.bucket, name, minio.GetObjectOptions{})
|
||||
return err == nil
|
||||
}
|
||||
55
pkg/worker/cloud/s3_test.go
Normal file
55
pkg/worker/cloud/s3_test.go
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
package cloud
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/giongto35/cloud-game/v3/pkg/logger"
|
||||
)
|
||||
|
||||
func TestS3(t *testing.T) {
|
||||
t.Skip()
|
||||
|
||||
name := "test"
|
||||
s3, err := NewS3Client(
|
||||
"s3.tebi.io",
|
||||
"cloudretro-001",
|
||||
"",
|
||||
"",
|
||||
logger.Default(),
|
||||
)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
buf := make([]byte, 1024*4)
|
||||
// then we can call rand.Read.
|
||||
_, err = rand.Read(buf)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = s3.Save(name, buf, map[string]string{"id": "test"})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
exists := s3.Has(name)
|
||||
if !exists {
|
||||
t.Errorf("don't exist, but shuld")
|
||||
}
|
||||
|
||||
ne := s3.Has(name + "123213")
|
||||
if ne {
|
||||
t.Errorf("exists, but shouldn't")
|
||||
}
|
||||
|
||||
dat, err := s3.Load(name)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if len(dat) == 0 {
|
||||
t.Errorf("should be something")
|
||||
}
|
||||
}
|
||||
24
pkg/worker/cloud/store.go
Normal file
24
pkg/worker/cloud/store.go
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
package cloud
|
||||
|
||||
import (
|
||||
"github.com/giongto35/cloud-game/v3/pkg/config"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/logger"
|
||||
)
|
||||
|
||||
type Storage interface {
|
||||
Save(name string, data []byte, tags map[string]string) (err error)
|
||||
Load(name string) (data []byte, err error)
|
||||
Has(name string) bool
|
||||
}
|
||||
|
||||
func Store(conf config.Storage, log *logger.Logger) (Storage, error) {
|
||||
var st Storage
|
||||
var err error
|
||||
switch conf.Provider {
|
||||
case "s3":
|
||||
st, err = NewS3Client(conf.S3Endpoint, conf.S3BucketName, conf.S3AccessKeyId, conf.S3SecretAccessKey, log)
|
||||
case "coordinator":
|
||||
default:
|
||||
}
|
||||
return st, err
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ type Connection interface {
|
|||
Disconnect()
|
||||
Id() com.Uid
|
||||
ProcessPackets(func(api.In[com.Uid]) error) chan struct{}
|
||||
SetErrorHandler(func(error))
|
||||
|
||||
Send(api.PT, any) ([]byte, error)
|
||||
Notify(api.PT, any)
|
||||
|
|
@ -66,84 +67,41 @@ func (c *coordinator) HandleRequests(w *Worker) chan struct{} {
|
|||
if err != nil {
|
||||
c.log.Panic().Err(err).Msg("WebRTC API creation has been failed")
|
||||
}
|
||||
skipped := api.Out{}
|
||||
|
||||
return c.ProcessPackets(func(x api.In[com.Uid]) (err error) {
|
||||
var out api.Out
|
||||
|
||||
switch x.T {
|
||||
case api.WebrtcInit:
|
||||
if dat := api.Unwrap[api.WebrtcInitRequest[com.Uid]](x.Payload); dat == nil {
|
||||
err, out = api.ErrMalformed, api.EmptyPacket
|
||||
} else {
|
||||
out = c.HandleWebrtcInit(*dat, w, ap)
|
||||
}
|
||||
case api.WebrtcAnswer:
|
||||
dat := api.Unwrap[api.WebrtcAnswerRequest[com.Uid]](x.Payload)
|
||||
if dat == nil {
|
||||
return api.ErrMalformed
|
||||
}
|
||||
c.HandleWebrtcAnswer(*dat, w)
|
||||
case api.WebrtcIce:
|
||||
dat := api.Unwrap[api.WebrtcIceCandidateRequest[com.Uid]](x.Payload)
|
||||
if dat == nil {
|
||||
return api.ErrMalformed
|
||||
}
|
||||
c.HandleWebrtcIceCandidate(*dat, w)
|
||||
err = api.Do(x, func(d api.WebrtcInitRequest) { out = c.HandleWebrtcInit(d, w, ap) })
|
||||
case api.StartGame:
|
||||
if dat := api.Unwrap[api.StartGameRequest[com.Uid]](x.Payload); dat == nil {
|
||||
err, out = api.ErrMalformed, api.EmptyPacket
|
||||
} else {
|
||||
out = c.HandleGameStart(*dat, w)
|
||||
}
|
||||
case api.TerminateSession:
|
||||
dat := api.Unwrap[api.TerminateSessionRequest[com.Uid]](x.Payload)
|
||||
if dat == nil {
|
||||
return api.ErrMalformed
|
||||
}
|
||||
c.HandleTerminateSession(*dat, w)
|
||||
case api.QuitGame:
|
||||
dat := api.Unwrap[api.GameQuitRequest[com.Uid]](x.Payload)
|
||||
if dat == nil {
|
||||
return api.ErrMalformed
|
||||
}
|
||||
c.HandleQuitGame(*dat, w)
|
||||
err = api.Do(x, func(d api.StartGameRequest) { out = c.HandleGameStart(d, w) })
|
||||
case api.SaveGame:
|
||||
if dat := api.Unwrap[api.SaveGameRequest[com.Uid]](x.Payload); dat == nil {
|
||||
err, out = api.ErrMalformed, api.EmptyPacket
|
||||
} else {
|
||||
out = c.HandleSaveGame(*dat, w)
|
||||
}
|
||||
err = api.Do(x, func(d api.SaveGameRequest) { out = c.HandleSaveGame(d, w) })
|
||||
case api.LoadGame:
|
||||
if dat := api.Unwrap[api.LoadGameRequest[com.Uid]](x.Payload); dat == nil {
|
||||
err, out = api.ErrMalformed, api.EmptyPacket
|
||||
} else {
|
||||
out = c.HandleLoadGame(*dat, w)
|
||||
}
|
||||
err = api.Do(x, func(d api.LoadGameRequest) { out = c.HandleLoadGame(d, w) })
|
||||
case api.ChangePlayer:
|
||||
if dat := api.Unwrap[api.ChangePlayerRequest[com.Uid]](x.Payload); dat == nil {
|
||||
err, out = api.ErrMalformed, api.EmptyPacket
|
||||
} else {
|
||||
out = c.HandleChangePlayer(*dat, w)
|
||||
}
|
||||
case api.ToggleMultitap:
|
||||
if dat := api.Unwrap[api.ToggleMultitapRequest[com.Uid]](x.Payload); dat == nil {
|
||||
err, out = api.ErrMalformed, api.EmptyPacket
|
||||
} else {
|
||||
c.HandleToggleMultitap(*dat, w)
|
||||
}
|
||||
err = api.Do(x, func(d api.ChangePlayerRequest) { out = c.HandleChangePlayer(d, w) })
|
||||
case api.RecordGame:
|
||||
if dat := api.Unwrap[api.RecordGameRequest[com.Uid]](x.Payload); dat == nil {
|
||||
err, out = api.ErrMalformed, api.EmptyPacket
|
||||
} else {
|
||||
out = c.HandleRecordGame(*dat, w)
|
||||
}
|
||||
err = api.Do(x, func(d api.RecordGameRequest) { out = c.HandleRecordGame(d, w) })
|
||||
case api.WebrtcAnswer:
|
||||
err = api.Do(x, func(d api.WebrtcAnswerRequest) { c.HandleWebrtcAnswer(d, w) })
|
||||
case api.WebrtcIce:
|
||||
err = api.Do(x, func(d api.WebrtcIceCandidateRequest) { c.HandleWebrtcIceCandidate(d, w) })
|
||||
case api.TerminateSession:
|
||||
err = api.Do(x, func(d api.TerminateSessionRequest) { c.HandleTerminateSession(d, w) })
|
||||
case api.QuitGame:
|
||||
err = api.Do(x, func(d api.GameQuitRequest) { c.HandleQuitGame(d, w) })
|
||||
case api.ResetGame:
|
||||
err = api.Do(x, func(d api.ResetGameRequest) { c.HandleResetGame(d, w) })
|
||||
default:
|
||||
c.log.Warn().Msgf("unhandled packet type %v", x.T)
|
||||
}
|
||||
if out != skipped {
|
||||
|
||||
if out != (api.Out{}) {
|
||||
w.cord.Route(x, &out)
|
||||
}
|
||||
return err
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -151,6 +109,34 @@ func (c *coordinator) RegisterRoom(id string) { c.Notify(api.RegisterRoom, id) }
|
|||
|
||||
// CloseRoom sends a signal to coordinator which will remove that room from its list.
|
||||
func (c *coordinator) CloseRoom(id string) { c.Notify(api.CloseRoom, id) }
|
||||
func (c *coordinator) IceCandidate(candidate string, sessionId com.Uid) {
|
||||
c.Notify(api.WebrtcIce, api.WebrtcIceCandidateRequest[com.Uid]{Stateful: api.Stateful[com.Uid]{Id: sessionId}, Candidate: candidate})
|
||||
func (c *coordinator) IceCandidate(candidate string, sessionId string) {
|
||||
c.Notify(api.WebrtcIce, api.WebrtcIceCandidateRequest{
|
||||
Stateful: api.Stateful{Id: sessionId},
|
||||
Candidate: candidate,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *coordinator) SendLibrary(w *Worker) {
|
||||
g := w.lib.GetAll()
|
||||
|
||||
var gg = make([]api.GameInfo, len(g))
|
||||
for i, g := range g {
|
||||
gg[i] = api.GameInfo(g)
|
||||
}
|
||||
|
||||
c.Notify(api.LibNewGameList, api.LibGameListInfo{T: 1, List: gg})
|
||||
}
|
||||
|
||||
func (c *coordinator) SendPrevSessions(w *Worker) {
|
||||
sessions := w.lib.Sessions()
|
||||
|
||||
// extract ids from save states, i.e. sessions
|
||||
var ids []string
|
||||
|
||||
for _, id := range sessions {
|
||||
x, _ := api.ExplodeDeepLink(id)
|
||||
ids = append(ids, x)
|
||||
}
|
||||
|
||||
c.Notify(api.PrevSessions, api.PrevSessionInfo{List: ids})
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue