diff --git a/.dockerignore b/.dockerignore
index 3cb82b19..9c6459c7 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,18 +1,9 @@
-.git/
-.github/
-.idea/
-.vscode/
-.gitignore
-
-.editorconfig
-.env
-docker-compose.yml
-Dockerfile
-
-LICENSE
-README.md
-bin/
-docs/
-release/
-
-assets/games/
+/**
+!cmd/
+!pkg/
+!scripts/
+!web/
+!go.mod
+!go.sum
+!LICENSE
+!Makefile
diff --git a/.editorconfig b/.editorconfig
deleted file mode 100644
index 4b5665da..00000000
--- a/.editorconfig
+++ /dev/null
@@ -1,4 +0,0 @@
-root = true
-
-[*.md]
-trim_trailing_whitespace = false
diff --git a/.env b/.env
deleted file mode 100644
index 45ec980e..00000000
--- a/.env
+++ /dev/null
@@ -1 +0,0 @@
-CLOUD_GAME_GAMES_PATH=./assets/games
diff --git a/.gitattributes b/.gitattributes
deleted file mode 100644
index 02f84227..00000000
--- a/.gitattributes
+++ /dev/null
@@ -1,16 +0,0 @@
-* linguist-vendored
-*.go linguist-vendored=false
-* text=auto eol=lf
-
-
-# Explicitly declare text files you want to always be normalized and converted
-# to native line endings on checkout.
-*.c text
-*.h text
-
-# Declare files that will always have CRLF line endings on checkout.
-*.sln text eol=crlf
-
-# Denote all files that are truly binary and should not be modified.
-*.png binary
-*.jpg binary
\ No newline at end of file
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 190c1935..40fed385 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,5 +1,5 @@
# ------------------------------------------------------------
-# Build workflow (Linux x64, macOS x64, Windows x64)
+# Build and test workflow (Linux x64, macOS x64, Windows x64)
# ------------------------------------------------------------
name: build
@@ -16,101 +16,73 @@ on:
jobs:
build:
- name: Build
strategy:
matrix:
- os: [ ubuntu-latest, macos-latest, windows-latest ]
- step: [ build, check ]
+ os: [ ubuntu-latest, windows-latest ]
runs-on: ${{ matrix.os }}
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- - uses: actions/setup-go@v2
+ - uses: actions/setup-go@v5
with:
- go-version: ^1.17
+ go-version: 'stable'
- - name: Get Linux dev libraries and tools
+ - name: Linux
if: matrix.os == 'ubuntu-latest'
+ env:
+ MESA_GL_VERSION_OVERRIDE: 3.3COMPAT
run: |
sudo apt-get -qq update
- sudo apt-get -qq install -y make pkg-config libvpx-dev libx264-dev libopus-dev libsdl2-dev libgl1-mesa-glx
+ sudo apt-get -qq install -y \
+ make pkg-config \
+ libvpx-dev libx264-dev libopus-dev libyuv-dev libjpeg-turbo8-dev \
+ libsdl2-dev libgl1 libglx-mesa0 libspeexdsp-dev
+
+ make build
+ xvfb-run --auto-servernum make test verify-cores
- - name: Get MacOS dev libraries and tools
- if: matrix.os == 'macos-latest'
+ - name: macOS
+ if: matrix.os == 'macos-12'
run: |
- brew install pkg-config libvpx x264 opus sdl2
+ brew install libvpx x264 sdl2 speexdsp
+ make build test verify-cores
- - name: Get Windows dev libraries and tools
+ - uses: msys2/setup-msys2@v2
if: matrix.os == 'windows-latest'
- uses: msys2/setup-msys2@v2
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-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: Get Windows OpenGL drivers
- if: matrix.step == 'check' && matrix.os == 'windows-latest'
+ - 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
- - name: Build Windows app
- if: matrix.step == 'build' && matrix.os == 'windows-latest'
- shell: msys2 {0}
- run: |
- make build
-
- - name: Build Linux app
- if: matrix.step == 'build' && matrix.os == 'ubuntu-latest'
- run: |
- make build
-
- - name: Build macOS app
- if: matrix.step == 'build' && matrix.os == 'macos-latest'
- run: |
- make build
-
- - name: Verify core rendering (windows-latest)
- if: matrix.step == 'check' && matrix.os == 'windows-latest' && always()
- shell: msys2 {0}
- env:
- MESA_GL_VERSION_OVERRIDE: 3.3COMPAT
- run: |
- GL_CTX=-autoGlContext make verify-cores
-
- - name: Verify core rendering (ubuntu-latest)
- if: matrix.step == 'check' && matrix.os == 'ubuntu-latest' && always()
- env:
- MESA_GL_VERSION_OVERRIDE: 3.3COMPAT
- run: |
- GL_CTX=-autoGlContext xvfb-run --auto-servernum make verify-cores
-
- - name: Verify core rendering (macos-latest)
- if: matrix.step == 'check' && matrix.os == 'macos-latest' && always()
- run: |
- make verify-cores
-
- - uses: actions/upload-artifact@v2
- if: matrix.step == 'check' && always()
+ - uses: actions/upload-artifact@v4
+ if: always()
with:
- name: emulator-test-frames
+ name: emulator-test-frames-${{ matrix.os }}
path: _rendered/*.png
-
- build_docker:
- name: Build (docker)
- runs-on: ubuntu-latest
- if: github.event_name == 'pull_request'
- steps:
- - uses: actions/checkout@v2
- - run: docker build --build-arg VERSION=$(./scripts/version.sh) .
diff --git a/.github/workflows/cd/cloudretro.io/config.yaml b/.github/workflows/cd/cloudretro.io/config.yaml
new file mode 100644
index 00000000..ac29d636
--- /dev/null
+++ b/.github/workflows/cd/cloudretro.io/config.yaml
@@ -0,0 +1,40 @@
+version: 4
+
+coordinator:
+ debug: true
+ server:
+ address:
+ frameOptions: SAMEORIGIN
+ https: true
+ tls:
+ domain: cloudretro.io
+ analytics:
+ inject: true
+ gtag: UA-145078282-1
+
+worker:
+ debug: true
+ network:
+ coordinatorAddress: cloudretro.io
+ publicAddress: cloudretro.io
+ secure: true
+ server:
+ https: true
+ tls:
+ address: :444
+# domain: cloudretro.io
+
+emulator:
+ libretro:
+ logLevel: 1
+ cores:
+ list:
+ dos:
+ uniqueSaveDir: true
+ mame:
+ options:
+ "fbneo-diagnostic-input": "Hold Start"
+ nes:
+ scale: 2
+ snes:
+ scale: 2
diff --git a/.github/workflows/cd/cloudretro.io/coordinator.env b/.github/workflows/cd/cloudretro.io/coordinator.env
deleted file mode 100644
index 6ac274e9..00000000
--- a/.github/workflows/cd/cloudretro.io/coordinator.env
+++ /dev/null
@@ -1,6 +0,0 @@
-CLOUD_GAME_COORDINATOR_ANALYTICS_GTAG=UA-145078282-1
-CLOUD_GAME_COORDINATOR_ANALYTICS_INJECT=true
-CLOUD_GAME_COORDINATOR_SERVER_ADDRESS=
-CLOUD_GAME_COORDINATOR_SERVER_HTTPS=true
-CLOUD_GAME_COORDINATOR_SERVER_TLS_DOMAIN=cloudretro.io
-CLOUD_GAME_ENVIRONMENT=prod
diff --git a/.github/workflows/cd/cloudretro.io/script.env b/.github/workflows/cd/cloudretro.io/script.env
index e38ca5c0..c806ac50 100644
--- a/.github/workflows/cd/cloudretro.io/script.env
+++ b/.github/workflows/cd/cloudretro.io/script.env
@@ -1,6 +1,6 @@
-COORDINATORS="167.172.70.98 cloudretro.io"
-DOCKER_IMAGE_TAG=dev
-DO_ADDRESS_LIST="cloud-gaming cloud-gaming-eu cloud-gaming-usw"
-SPLIT_HOSTS=1
+COORDINATORS="138.68.48.200"
+DOCKER_IMAGE_TAG=master
+#DO_ADDRESS_LIST="cloud-gaming cloud-gaming-eu cloud-gaming-usw"
+#SPLIT_HOSTS=1
USER=root
-WORKERS=${WORKERS:-5}
+WORKERS=${WORKERS:-4}
diff --git a/.github/workflows/cd/cloudretro.io/worker.env b/.github/workflows/cd/cloudretro.io/worker.env
deleted file mode 100644
index 606ec637..00000000
--- a/.github/workflows/cd/cloudretro.io/worker.env
+++ /dev/null
@@ -1,7 +0,0 @@
-CLOUD_GAME_WORKER_NETWORK_COORDINATORADDRESS=cloudretro.io
-CLOUD_GAME_WORKER_NETWORK_PUBLICADDRESS=cloudretro.io
-CLOUD_GAME_WORKER_NETWORK_SECURE=true
-CLOUD_GAME_WORKER_SERVER_ADDRESS=:80
-CLOUD_GAME_WORKER_SERVER_HTTPS=true
-CLOUD_GAME_WORKER_SERVER_TLS_ADDRESS=:443
-CLOUD_GAME_WORKER_SERVER_TLS_DOMAIN=cloudretro.io
diff --git a/.github/workflows/cd/deploy-app.sh b/.github/workflows/cd/deploy-app.sh
index 6caa13fc..cc7b3c85 100755
--- a/.github/workflows/cd/deploy-app.sh
+++ b/.github/workflows/cd/deploy-app.sh
@@ -54,6 +54,10 @@ IP_LIST=${IP_LIST:-}
# a list of machines mark some addresses to deploy only a coordinator there
COORDINATORS=${COORDINATORS:-}
+if [ -z "$SPLIT_HOSTS" ]; then
+ IP_LIST+=$COORDINATORS
+fi
+
# Digital Ocean operations
#DO_TOKEN
DO_ADDRESS_LIST=${DO_ADDRESS_LIST:-}
@@ -64,7 +68,7 @@ REMOTE_WORK_DIR=${REMOTE_WORK_DIR:-"/cloud-game"}
DOCKER_IMAGE_TAG=${DOCKER_IMAGE_TAG:-latest}
echo "Docker tag:$DOCKER_IMAGE_TAG"
# the total number of worker replicas to deploy
-WORKERS=${WORKERS:-5}
+WORKERS=${WORKERS:-4}
USER=${USER:-root}
compose_src=$(cat $LOCAL_WORK_DIR/docker-compose.yml)
@@ -124,7 +128,7 @@ echo "IPs:" $IP_LIST
# Run command builder
#
-# By default it will run docker-compose with both coordinator and worker apps.
+# By default it will run docker compose with both coordinator and worker apps.
# With the SPLIT_HOSTS parameter specified, it will run either coordinator app
# if the current server address is found in the IP_LIST variable, otherwise it
# will run just the worker app.
@@ -142,28 +146,39 @@ for ip in $IP_LIST; do
fi
# build run command
- cmd="ZONE=\$zone docker-compose up -d --remove-orphans --scale worker=\${workers:-$WORKERS}"
+ cmd="ZONE=\$zone docker compose up -d --remove-orphans"
if [ ! -z "$SPLIT_HOSTS" ]; then
cmd+=" worker"
deploy_coordinator=0
deploy_worker=1
+ else
+ cmd+=" worker"
fi
# override run command
if [ ! -z "$SPLIT_HOSTS" ]; then
for addr in $COORDINATORS; do
if [ "$ip" == $addr ]; then
- cmd="docker-compose up -d --remove-orphans coordinator"
+ cmd="docker compose up -d --remove-orphans coordinator"
deploy_coordinator=1
deploy_worker=0
break
fi
done
+ else
+ cmd+=" coordinator"
fi
# build Docker container env file
run_env=""
+ custom_config=""
if [[ ! -z "${ENV_DIR}" ]]; then
+ env_f=$ENV_DIR/config.yaml
+ if [[ -e "$env_f" ]]; then
+ echo "config.yaml found"
+ custom_config=$(cat $env_f)
+ fi
+
if [ $deploy_coordinator == 1 ]; then
env_f=$ENV_DIR/coordinator.env
if [[ -e "$env_f" ]]; then
@@ -190,13 +205,13 @@ for ip in $IP_LIST; do
run="#!/bin/bash"$'\n'
run+=$(remote_run_commands "$ENV_DIR")$'\n'
run+=$(remote_run_commands "$PROVIDER_DIR")$'\n'
- run+="IMAGE_TAG=$DOCKER_IMAGE_TAG APP_DIR=$REMOTE_WORK_DIR $cmd"
+ run+="IMAGE_TAG=$DOCKER_IMAGE_TAG APP_DIR=$REMOTE_WORK_DIR WORKER_REPLICAS=$WORKERS $cmd"
echo ""
echo "run.sh:"$'\n'"$run"
echo ""
- # !to add docker-compose install / warning
+ # !to add docker compose install / warning
# custom scripts
remote_sudo_run_once $ip "$PROVIDER_DIR" "$ssh_i"
@@ -205,13 +220,13 @@ for ip in $IP_LIST; do
echo "Update the remote host"
ssh -o ConnectTimeout=10 $USER@$ip ${ssh_i:-} "\
- docker-compose -v; \
+ docker compose version; \
mkdir -p $REMOTE_WORK_DIR; \
cd $REMOTE_WORK_DIR; \
+ mkdir -p $REMOTE_WORK_DIR/home; \
+ echo \"$custom_config\" > $REMOTE_WORK_DIR/home/config.yaml; \
echo '$compose_src' > ./docker-compose.yml; \
- echo '$run_env' > ./run.env; \
- IMAGE_TAG=$DOCKER_IMAGE_TAG docker-compose pull coordinator; \
- echo '$run' > ./run.sh; \
- chmod +x ./run.sh; \
- ./run.sh"
+ docker compose down; \
+ IMAGE_TAG=$DOCKER_IMAGE_TAG docker compose pull; \
+ docker compose up -d;"
done
diff --git a/.github/workflows/cd/docker-compose.yml b/.github/workflows/cd/docker-compose.yml
index ec68b8ac..02d94786 100644
--- a/.github/workflows/cd/docker-compose.yml
+++ b/.github/workflows/cd/docker-compose.yml
@@ -1,36 +1,93 @@
-version: "3.4"
-
-x-params:
- &default-params
- env_file: run.env
- image: ghcr.io/giongto35/cloud-game/cloud-game:${IMAGE_TAG:-latest}
+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
logging:
- driver: "json-file"
- options:
- max-size: "64m"
- max-file: "5"
- compress: "true"
+ 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:
coordinator:
<<: *default-params
- command: coordinator --v=5
- 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
-
- worker:
- <<: *default-params
+ command: ./coordinator
environment:
- - MESA_GL_VERSION_OVERRIDE=3.3
- entrypoint: [ "/bin/sh", "-c", "xvfb-run -a $$@", "" ]
- command: worker --v=5 --zone=${ZONE:-}
+ - 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}/cores:/usr/local/share/cloud-game/assets/cores
- ${APP_DIR:-/cloud-game}/games:/usr/local/share/cloud-game/assets/games
- ${APP_DIR:-/cloud-game}/home:/root/.cr
+
+ worker01:
+ <<: [ *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=: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" ]
+
+volumes:
+ x11:
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index c58fc810..dd296ead 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -16,7 +16,7 @@ jobs:
key: ${{ secrets.SSH_PRIVATE_KEY }}
known_hosts: 'PLACEHOLDER'
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Deploy to all servers
env:
diff --git a/.github/workflows/docker_build.yml b/.github/workflows/docker_build.yml
new file mode 100644
index 00000000..65bc5856
--- /dev/null
+++ b/.github/workflows/docker_build.yml
@@ -0,0 +1,12 @@
+name: docker_build
+on:
+ pull_request:
+ branches:
+ - master
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - run: DOCKER_BUILDKIT=1 docker build --build-arg VERSION=$(./scripts/version.sh) .
diff --git a/.github/workflows/docker_publish.yml b/.github/workflows/docker_publish.yml
new file mode 100644
index 00000000..7ba231eb
--- /dev/null
+++ b/.github/workflows/docker_publish.yml
@@ -0,0 +1,48 @@
+# ----------------------------------------------------------------------------------
+# Publish Docker image from the current master branch or v* into Github repository
+# ----------------------------------------------------------------------------------
+
+name: docker-publish
+
+on:
+ push:
+ branches:
+ - master
+ tags:
+ - 'v*'
+
+jobs:
+ docker-publish:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - run: echo "V=$(./scripts/version.sh)" >> $GITHUB_ENV
+ - name: Docker meta
+ id: meta
+ uses: docker/metadata-action@v4
+ with:
+ images: ghcr.io/${{ github.repository }}/cloud-game
+ tags: |
+ type=ref,event=branch
+ type=semver,pattern={{version}}
+ type=semver,pattern={{major}}.{{minor}}
+ type=semver,pattern={{major}}
+ - uses: docker/setup-buildx-action@v2
+ - uses: docker/login-action@v2
+ with:
+ registry: ghcr.io
+ username: ${{ github.repository_owner }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ - uses: docker/build-push-action@v4
+ with:
+ build-args: VERSION=${{ env.V }}
+ context: .
+ push: true
+ provenance: false
+ sbom: false
+ tags: |
+ ${{ steps.meta.outputs.tags }}
+ labels: |
+ ${{ steps.meta.outputs.labels }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
diff --git a/.github/workflows/docker_publish_stable.yml b/.github/workflows/docker_publish_stable.yml
deleted file mode 100644
index ea1b1c43..00000000
--- a/.github/workflows/docker_publish_stable.yml
+++ /dev/null
@@ -1,31 +0,0 @@
-# ------------------------------------------------------------------------
-# Publish Docker image from the stable snapshot into Github repository
-# ------------------------------------------------------------------------
-
-name: publish-stable
-
-on:
- push:
- tags:
- - 'v*'
-
-jobs:
- docker-publish-stable:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
-
- - run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
-
- - run: echo "V=$(./scripts/version.sh)" >> $GITHUB_ENV
-
- - uses: docker/build-push-action@v1
- with:
- username: ${{ github.repository_owner }}
- password: ${{ secrets.GITHUB_TOKEN }}
- add_git_labels: true
- tags: latest,${{ env.TAG }}
- build_args: VERSION=${{ env.V }}
-
- registry: docker.pkg.github.com
- repository: ${{ github.REPOSITORY }}/cloud-game
diff --git a/.github/workflows/docker_publish_unstable.yml b/.github/workflows/docker_publish_unstable.yml
deleted file mode 100644
index c5bdba5d..00000000
--- a/.github/workflows/docker_publish_unstable.yml
+++ /dev/null
@@ -1,29 +0,0 @@
-# ----------------------------------------------------------------------------
-# Publish Docker image from the current master branch into Github repository
-# ----------------------------------------------------------------------------
-
-name: publish-unstable
-
-on:
- push:
- branches:
- - master
-
-jobs:
- docker-publish-unstable:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
-
- - run: echo "V=$(./scripts/version.sh)" >> $GITHUB_ENV
-
- - uses: docker/build-push-action@v1
- with:
- username: ${{ github.repository_owner }}
- password: ${{ secrets.GITHUB_TOKEN }}
- add_git_labels: true
- tags: dev
- build_args: VERSION=${{ env.V }}
-
- registry: docker.pkg.github.com
- repository: ${{ github.REPOSITORY }}/cloud-game
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml_
similarity index 98%
rename from .github/workflows/release.yml
rename to .github/workflows/release.yml_
index 22a0f7a6..4f7d1d27 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml_
@@ -37,11 +37,11 @@ jobs:
env:
release-dir: _release
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- - uses: actions/setup-go@v2
+ - uses: actions/setup-go@v4
with:
- go-version: ^1.17
+ go-version: ^1.20
- name: Get Linux dev libraries and tools
if: matrix.os == 'ubuntu-latest'
diff --git a/.gitignore b/.gitignore
index 3e771139..c2ecdfe2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,6 +45,11 @@ Network Trash Folder
Temporary Items
.apdisk
+### SSL
+*.crt
+*.csr
+*.key
+
### Production
DockerfileProd
key.json
@@ -62,5 +67,15 @@ _output/
./build
release/
vendor/
+tests/
+!tests/e2e/
+*.exe
.dockerignore
+
+### Libretro
+fbneo/
+hi/
+nvram/
+*.mcd
+
diff --git a/DESIGNv2.md b/DESIGNv2.md
new file mode 100644
index 00000000..838677f5
--- /dev/null
+++ b/DESIGNv2.md
@@ -0,0 +1,79 @@
+# Cloud Gaming Service Design Document
+
+Cloud Gaming Service contains multiple workers for gaming streams and a coordinator for distributing traffic and pairing
+up connections.
+
+## Coordinator
+
+Coordinator is a web-frontend, load balancer and signalling server for WebRTC.
+
+```
+ WORKERS
+ ┌──────────────────────────────────┐
+ │ │
+ │ REGION 1 REGION 2 REGION N │
+ │ (US) (DE) (XX) │
+ │ ┌──────┐ ┌──────┐ ┌──────┐ |
+ COORDINATOR │ │WORKER│ │WORKER│ │WORKER│ |
+┌───────────┐ │ └──────┘ └──────┘ └──────┘ |
+│ │ ───────────────────────HEALTH────────────────────► │ • • • |
+│ HTTP/WS │ ◄─────────────────────REG/DEREG─────────────────── │ • • • |
+│┌─────────┐│ │ • • • |
+│| |│ USER │ ┌──────┐* ┌──────┐ ┌──────┐ |
+│└─────────┘│ ┌──────┐ │ │WORKER│ │WORKER│ │WORKER│ |
+│ │ ◄──(1)CONNECT───────── │ │ ────(3)SELECT────► │ └──────┘ └──────┘ └──────┘ |
+│ │ ───(2)LIST WORKERS───► │ │ ◄───(4)STREAM───── │ │
+└───────────┘ └──────┘ │ * MULTIPLAYER │
+ │ ┌──────┐────► ONE GAME │
+ │ ┌───►│WORKER│◄──┐ │
+ │ │ └──────┘ │ │
+ │ │ ▲ ▲ │ │
+ │ ┌┴─┐ │ │ ┌┴─┐ |
+ │ │U1│ ┌─┴┐ ┌┴─┐ │U4│ |
+ │ └──┘ │U2│ │U3│ └──┘ |
+ │ └──┘ └──┘ |
+ │ |
+ └──────────────────────────────────┘
+```
+
+- (1) A user opens the main page of the app in the browser, i.e. connects to the coordinator.
+- (2) The coordinator searches and serves a list of most suitable workers to the user.
+- (3) The user proceeds with latency check of each worker from the list, then coordinator collects user-to-worker
+ latency data and picks the best candidate.
+- (4) The coordinator sets up peer-to-peer connection between a worker and the user based on the WebRTC protocol and a
+ game hosted on the worker is streamed to the user.
+
+## Worker
+
+Worker is responsible for running and streaming games to users.
+
+```
+ WORKER
+┌─────────────────────────────────────────────────────────────────┐
+│ EMULATOR WEBRTC │ BROWSER
+│ ┌─────────────────┐ ENCODER ┌────────┐ │ ┌──────────┐
+│ │ │ ┌─────────┐ | DMUX | | ───RTP──► | WEBRTC |
+│ │ AUDIO SAMPLES │ ──PCM──► │ │ ──OPUS──► │ ┌──► │ │ ◄──SCTP── | |
+│ │ VIDEO FRAMES │ ──RGB──► │ │ ──H264──► │ └──► | | └──────────┘ COORDINATOR
+│ │ │ └─────────┘ │ │ │ • ┌─────────────┐
+│ │ │ | MUX | | ───TCP──────── • ───────► | WEBSOCKET |
+│ │ │ │ ┌── │ │ • └─────────────┘
+| | | BINARY | ▼ | | BROWSER
+│ │ INPUT STATE │ ◄───────────────────────────── │ • │ │ ┌──────────┐
+│ │ │ │ ▲ │ │ ───RTP──► | WEBRTC |
+│ └─────────────────┘ HTTP/WS | └── | │ ◄──SCTP── │ │
+│ ┌─────────┐ └────────┘ │ └──────────┘
+| | | |
+| └─────────┘ |
+└─────────────────────────────────────────────────────────────────┘
+```
+
+- After coordinator matches the most appropriate server (peer 1) to the user (peer 2), a WebRTC peer-to-peer handshake
+ will be conducted. The coordinator will help initiate the session between the two peers over a WebSocket connection.
+- The worker either spawns new rooms running game emulators or connects users to existing rooms.
+- Raw image and audio streams from the emulator are captured and encoded to a WebRTC-supported streaming format. Next,
+ these stream are piped out (dmux) to all users in the room.
+- On the other hand, input from players is sent to the worker over WebRTC DataChannel. The game logic on the emulator
+ will be updated based on the input stream of all players, for that each stream is multiplexed (mux) into one.
+- Game states (saves) are stored in cloud storage, so all distributed workers can keep game states in sync and players
+ can continue their games where they left off.
diff --git a/Dockerfile b/Dockerfile
index 13a921f4..1cb760b2 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,61 +1,101 @@
-# The base cloud-game image
-ARG BUILD_PATH=/go/src/github.com/giongto35/cloud-game
+ARG BUILD_PATH=/tmp/cloud-game
+ARG VERSION=master
-# build image
-FROM debian:bullseye-slim AS build
+# base build stage
+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 ./
+RUN tar -C /usr/local -xzf $GO_DIST && \
+ rm $GO_DIST
+ENV PATH="${PATH}:/usr/local/go/bin"
+
+RUN apt-get -q update && apt-get -q install --no-install-recommends -y \
+ ca-certificates \
+ make \
+ upx \
+&& rm -rf /var/lib/apt/lists/*
+
+# next conditional build stage
+FROM build0 AS build_coordinator
ARG BUILD_PATH
+ARG VERSION
+ENV GIT_VERSION=${VERSION}
+
WORKDIR ${BUILD_PATH}
-# system libs layer
-RUN apt-get -qq update && apt-get -qq install --no-install-recommends -y \
- gcc \
- ca-certificates \
+# by default we ignore all except some folders and files, see .dockerignore
+COPY . ./
+RUN --mount=type=cache,target=/root/.cache/go-build make build.coordinator
+RUN find ./bin/* | xargs upx --best --lzma
+
+WORKDIR /usr/local/share/cloud-game
+RUN mv ${BUILD_PATH}/bin/* ./ && \
+ mv ${BUILD_PATH}/web ./web && \
+ mv ${BUILD_PATH}/LICENSE ./
+RUN ${BUILD_PATH}/scripts/version.sh ./web/index.html ${VERSION} && \
+ ${BUILD_PATH}/scripts/mkdirs.sh
+
+# next worker build stage
+FROM build0 AS build_worker
+ARG BUILD_PATH
+ARG VERSION
+ENV GIT_VERSION=${VERSION}
+
+WORKDIR ${BUILD_PATH}
+
+# install deps
+RUN apt-get -q update && apt-get -q install --no-install-recommends -y \
+ build-essential \
libopus-dev \
libsdl2-dev \
libvpx-dev \
+ libyuv-dev \
+ libjpeg-turbo8-dev \
libx264-dev \
- make \
+ libspeexdsp-dev \
pkg-config \
- wget \
- && rm -rf /var/lib/apt/lists/*
+&& rm -rf /var/lib/apt/lists/*
-# go setup layer
-ARG GO=go1.17.linux-amd64.tar.gz
-RUN wget -q https://golang.org/dl/$GO \
- && rm -rf /usr/local/go \
- && tar -C /usr/local -xzf $GO \
- && rm $GO
-ENV PATH="${PATH}:/usr/local/go/bin"
+# by default we ignore all except some folders and files, see .dockerignore
+COPY . ./
+RUN --mount=type=cache,target=/root/.cache/go-build make GO_TAGS=static,st build.worker
+RUN find ./bin/* | xargs upx --best --lzma
-# go deps layer
-COPY go.mod go.sum ./
-RUN go mod download
+WORKDIR /usr/local/share/cloud-game
+RUN mv ${BUILD_PATH}/bin/* ./ && \
+ mv ${BUILD_PATH}/LICENSE ./
+RUN ${BUILD_PATH}/scripts/mkdirs.sh worker
-# app build layer
-COPY pkg ./pkg
-COPY cmd ./cmd
-COPY Makefile .
-COPY scripts/version.sh scripts/version.sh
-ARG VERSION
-RUN GIT_VERSION=${VERSION} make build
+FROM scratch AS coordinator
+
+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:plucky AS worker
+
+RUN apt-get -q update && apt-get -q install --no-install-recommends -y \
+ curl \
+ libx11-6 \
+ libxext6 \
+ && apt-get autoremove \
+ && rm -rf /var/lib/apt/lists/* /var/log/* /usr/share/bug /usr/share/doc /usr/share/doc-base \
+ /usr/share/X11/locale/*
+
+COPY --from=build_worker /usr/local/share/cloud-game /cloud-game
+COPY --from=build_worker /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
+
+ADD https://github.com/sergystepanov/mesa-llvmpipe/releases/download/v1.0.0/libGL.so.1.5.0 \
+ /usr/lib/x86_64-linux-gnu/
+RUN cd /usr/lib/x86_64-linux-gnu && \
+ ln -s libGL.so.1.5.0 libGL.so.1 && \
+ ln -s libGL.so.1 libGL.so
+
+FROM worker AS cloud-game
-# base image
-FROM debian:bullseye-slim
-ARG BUILD_PATH
WORKDIR /usr/local/share/cloud-game
-COPY scripts/install.sh install.sh
-RUN bash install.sh && \
- rm -rf /var/lib/apt/lists/* install.sh
-
-COPY --from=build ${BUILD_PATH}/bin/ ./
-RUN cp -s $(pwd)/* /usr/local/bin
-COPY assets/cores ./assets/cores
-COPY configs ./configs
-COPY web ./web
-ARG VERSION
-COPY scripts/version.sh version.sh
-RUN bash ./version.sh ./web/index.html ${VERSION} && \
- rm -rf version.sh
-
-EXPOSE 8000 9000
+COPY --from=coordinator /cloud-game ./
+COPY --from=worker /cloud-game ./
diff --git a/Makefile b/Makefile
index 384c1de2..1fbe81de 100644
--- a/Makefile
+++ b/Makefile
@@ -1,11 +1,13 @@
-# Makefile includes some useful commands to build or format incentives
-# More commands could be added
-
-# Variables
PROJECT = cloud-game
REPO_ROOT = github.com/giongto35
ROOT = ${REPO_ROOT}/${PROJECT}
+CGO_CFLAGS='-g -O3'
+CGO_LDFLAGS='-g -O3'
+GO_TAGS=
+
+.PHONY: clean test
+
fmt:
@goimports -w cmd pkg tests
@gofmt -s -w cmd pkg tests
@@ -13,74 +15,54 @@ fmt:
compile: fmt
@go install ./cmd/...
-check: fmt
- @golangci-lint run cmd/... pkg/...
-# @staticcheck -checks="all,-S1*" ./cmd/... ./pkg/... ./tests/...
-
-dep:
- go mod download
-# go mod tidy
-
-# NOTE: there is problem with go mod vendor when it delete github.com/gen2brain/x264-go/x264c causing unable to build. https://github.com/golang/go/issues/26366
-#build.cross: build
-# CGO_ENABLED=1 GOOS=darwin GOARC=amd64 go build --ldflags '-linkmode external -extldflags "-static"' -o bin/coordinator-darwin ./cmd/coordinator
-# CGO_ENABLED=1 GOOS=darwin GOARC=amd64 go build --ldflags '-linkmode external -extldflags "-static"' -o bin/worker-darwin ./cmd/worker
-# CC=arm-linux-musleabihf-gcc GOOS=linux GOARC=amd64 CGO_ENABLED=1 go build --ldflags '-linkmode external -extldflags "-static"' -o bin/coordinator-linu ./cmd/coordinator
-# CC=arm-linux-musleabihf-gcc GOOS=linux GOARC=amd64 CGO_ENABLED=1 go build --ldflags '-linkmode external -extldflags "-static"' -o bin/worker-linux ./cmd/worker
-
-# A user can invoke tests in different ways:
-# - make test runs all tests;
-# - make test TEST_TIMEOUT=10 runs all tests with a timeout of 10 seconds;
-# - make test TEST_PKG=./model/... only runs tests for the model package;
-# - make test TEST_ARGS="-v -short" runs tests with the specified arguments;
-# - make test-race runs tests with race detector enabled.
-TEST_TIMEOUT = 60
-TEST_PKGS ?= ./cmd/... ./pkg/...
-TEST_TARGETS := test-short test-verbose test-race test-cover
-.PHONY: $(TEST_TARGETS) test tests
-test-short: TEST_ARGS=-short
-test-verbose: TEST_ARGS=-v
-test-race: TEST_ARGS=-race
-test-cover: TEST_ARGS=-cover
-$(TEST_TARGETS): test
-
-test: compile
- @go test -timeout $(TEST_TIMEOUT)s $(TEST_ARGS) $(TEST_PKGS)
-
-test-e2e: compile
- @go test ./tests/e2e/...
-
-cover:
- @go test -v -covermode=count -coverprofile=coverage.out $(TEST_PKGS)
-# @$(GOPATH)/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $(COVERALLS_TOKEN)
-
clean:
@rm -rf bin
@rm -rf build
@go clean ./cmd/*
-build:
+
+build.coordinator:
mkdir -p bin/
- CGO_ENABLED=0 go build -ldflags "-w -s -X 'main.Version=$(GIT_VERSION)'" -o bin/ ./cmd/coordinator
- go build -buildmode=exe -tags static -ldflags "-w -s -X 'main.Version=$(GIT_VERSION)'" $(EXT_WFLAGS) -o bin/ ./cmd/worker
+ go build -ldflags "-w -s -X 'main.Version=$(GIT_VERSION)'" -o bin/ ./cmd/coordinator
+
+build.worker:
+ mkdir -p bin/
+ CGO_CFLAGS=${CGO_CFLAGS} CGO_LDFLAGS=${CGO_LDFLAGS} \
+ go build -pgo=auto -buildmode=exe $(if $(GO_TAGS),-tags $(GO_TAGS),) \
+ -ldflags "-w -s -X 'main.Version=$(GIT_VERSION)'" $(EXT_WFLAGS) \
+ -o bin/ ./cmd/worker
+
+build: build.coordinator build.worker
+
+test:
+ go test -v ./pkg/...
verify-cores:
- go test -run TestAllEmulatorRooms ./pkg/worker/room -v -renderFrames $(GL_CTX) -outputPath "../../../_rendered"
+ go test -run TestAll ./pkg/worker/room -v -renderFrames $(GL_CTX) -outputPath "./_rendered"
dev.build: compile build
dev.build-local:
mkdir -p bin/
- CGO_ENABLED=0 go build -o bin/ ./cmd/coordinator
- go build -buildmode=exe -o bin/ ./cmd/worker
+ go build -o bin/ ./cmd/coordinator
+ CGO_CFLAGS=${CGO_CFLAGS} CGO_LDFLAGS=${CGO_LDFLAGS} go build -pgo=auto -o bin/ ./cmd/worker
dev.run: dev.build-local
- ./bin/coordinator --v=5 &
- ./bin/worker --v=5
+ifeq ($(OS),Windows_NT)
+ ./bin/coordinator.exe & ./bin/worker.exe
+else
+ ./bin/coordinator & ./bin/worker
+endif
+
+dev.run.debug:
+ go build -race -o bin/ ./cmd/coordinator
+ CGO_CFLAGS=${CGO_CFLAGS} CGO_LDFLAGS=${CGO_LDFLAGS} \
+ go build -race -gcflags=all=-d=checkptr -o bin/ ./cmd/worker
+ ./bin/coordinator & ./bin/worker
dev.run-docker:
docker rm cloud-game-local -f || true
- CLOUD_GAME_GAMES_PATH=$(PWD)/assets/games docker-compose up --build
+ docker compose up --build
# RELEASE
# Builds the app for new release.
@@ -97,8 +79,8 @@ dev.run-docker:
# Config params:
# - RELEASE_DIR: the name of the output folder (default: release).
# - CONFIG_DIR: search dir for core config files.
-# - DLIB_TOOL: the name of a dynamic lib copy tool (with params) (e.g., ldd -x -y; defalut: ldd).
-# - DLIB_SEARCH_PATTERN: a grep filter of the output of the DLIB_TOOL (e.g., mylib.so; default: .*so).
+# - DLIB_TOOL: the name of a dynamic lib copy tool (with params) (e.g., ldd -x -y; default: ldd).
+# - DLIB_SEARCH_PATTERN: a grep filter of the output of the DLIB_TOOL (e.g., my_lib.so; default: .*so).
# Be aware that this search pattern will return only matched regular expression part and not the whole line.
# de. -> abc def ghj -> def
# Makefile special symbols should be escaped with \.
diff --git a/README.md b/README.md
index 39a4448c..1054d2ab 100644
--- a/README.md
+++ b/README.md
@@ -11,37 +11,27 @@ on generic solution for cloudgaming
Discord: [Join Us](https://discord.gg/sXRQZa2zeP)
-## Announcement
+
-**(Currently, I'm working on [CloudMorph](https://github.com/giongto35/cloud-morph): It offers more generic solution to
-run any offline games/application on browser in Cloud Gaming
-approach: [https://github.com/giongto35/cloud-morph](https://github.com/giongto35/cloud-morph))**
+## 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))**
## Introduction
CloudRetro provides an open-source cloud gaming platform for retro games. It started as an experiment for testing cloud
-gaming performance with [WebRTC](https://github.com/pion/webrtc/) and [libretro](https://www.libretro.com/), and now it
+gaming performance with [WebRTC](https://github.com/pion/webrtc/) and [Libretro](https://www.libretro.com/), and now it
aims to deliver the most modern and convenient gaming experience through the technology.
Theoretically, in cloud gaming, games are run on remote servers and media are streamed to the player optimally to ensure
the most comfortable user interaction. It opens the ability to play any retro games on web-browser directly, which are
fully compatible with multi-platform like Desktop, Android, ~~IOS~~.
-## Try the service at **[cloudretro.io](https://cloudretro.io)**
-Direct play an existing
-game: **[Pokemon Emerald](https://cloudretro.io/?id=4a5073a4b05ad0fe___Pokemon%20-%20Emerald%20Version%20(U))**
-
In ideal network condition and less resource contention on servers, the game will run smoothly as in the video demo.
Because I only hosted the platform on limited servers in US East, US West, Eu, Singapore, you may experience some
latency issues + connection problem. You can try hosting the service following the instruction the next section to have
a better sense of performance.
-| Screenshot | Screenshot |
-| :--------------------------------------------: | :--------------------------------------------: |
-|  |  |
-|  |  |
-
## Feature
1. **Cloud gaming**: Game logic and storage is hosted on cloud service. It reduces the cumbersome of game
@@ -63,23 +53,27 @@ a better sense of performance.
## Development environment
-* Install Golang https://golang.org/doc/install.
-
+* 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
+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
+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}
+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
@@ -96,38 +90,33 @@ Because the coordinator and workers need to run simultaneously. Workers connect
__Additionally, you may install and configure an `X Server` display in order to be able to run OpenGL cores.__
__See the `docker-compose.yml` file for Xvfb example config.__
-__Minimum supported libx264 (x264 codec) version is v160!__
-
## Run with Docker
-Use makefile script: `make dev.run-docker` or Docker Compose directly: `docker-compose up --build`
-(`CLOUD_GAME_GAMES_PATH` is env variable for games on your host). It will spawn a docker environment and you can access
-the service on `localhost:8000`.
+Use makefile script: `make dev.run-docker` or Docker Compose directly: `docker compose up --build`.
+It will spawn a docker environment and you can access the service on `localhost:8000`.
## Configuration
-The configuration parameters are stored in the [`configs/config.yaml`](configs/config.yaml) file which is shared for all
-application instances on the same host system. It is possible to specify individual configuration files for each
-instance as well as override some parameters, for that purpose, please refer to the list of command-line options of the
-apps.
+The default configuration file is stored in the [`pkg/configs/config.yaml`](pkg/config/config.yaml) file.
+This configuration file will be embedded into the applications and loaded automatically during startup.
+In order to change the default parameters you can specify environment variables with the `CLOUD_GAME_` prefix, or place
+a custom `config.yaml` file into one of these places: just near the application, `.cr` folder in user's home, or
+specify own directory with `-w-conf` application param (`worker -w-conf /usr/conf`).
## Deployment
-See an example of [deployment scripts](.github/workflows/cd) if you want to try to host your own cloud-retro copy in the cloud.
-This script (deploy-app.sh) allows pushing configured application to the group of servers automatically.
-The cloud server should be any Debian-based system with the docker-compose application [installed](https://docs.docker.com/compose/install/).
+See an example of [deployment scripts](.github/workflows/cd) if you want to try to host your own cloud-retro copy in the
+cloud. This script (deploy-app.sh) allows pushing configured application to the group of servers automatically. The
+cloud server should be any Debian-based system with the docker-compose
+application [installed](https://docs.docker.com/compose/install/).
## Technical documents
+- [Design document v2](DESIGNv2.md)
- [webrtchacks Blog: Open Source Cloud Gaming with WebRTC](https://webrtchacks.com/open-source-cloud-gaming-with-webrtc/)
- [Wiki (outdated)](https://github.com/giongto35/cloud-game/wiki)
-
- [Code Pointer Wiki](https://github.com/giongto35/cloud-game/wiki/Code-Deep-Dive)
-| High level | Worker internal |
-| :----------------------------------: | :-----------------------------------------: |
-|  |  |
-
## FAQ
- [FAQ](https://github.com/giongto35/cloud-game/wiki/FAQ)
@@ -136,7 +125,7 @@ The cloud server should be any Debian-based system with the docker-compose appli
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)
@@ -144,11 +133,6 @@ By clicking these deep link, you can join the game directly and play it together
And you can host the new game by yourself by accessing [cloudretro.io](https://cloudretro.io) and click "share" button
to generate a permanent link to your game.
-
-
-Synchronize a game session on multiple devices
-
-
## Credits
We are very much thankful to [everyone](https://github.com/giongto35/cloud-game/graphs/contributors) we've been lucky to
@@ -158,7 +142,8 @@ Thanks:
* [Pion](https://github.com/pion) team for the incredible Golang WebRTC library and their support.
* [Libretro](https://www.libretro.com) team for the greatest emulation lib.
-* [kivutar](https://github.com/kivutar) for [go-nanoarch](https://github.com/libretro/go-nanoarch), [ludo](https://github.com/libretro/ludo), and all.
+* [kivutar](https://github.com/kivutar) for [go-nanoarch](https://github.com/libretro/go-nanoarch)
+ and [ludo](https://github.com/libretro/ludo).
* [gen2brain](https://github.com/gen2brain) for the [h264](https://github.com/gen2brain/x264-go) and VPX encoder.
* [poi5305](https://github.com/poi5305) for the [YUV video encoding](https://github.com/poi5305/go-yuv2webRTC).
* [fogleman](https://github.com/fogleman) for the [NES emulator](https://github.com/fogleman/nes).
@@ -171,13 +156,19 @@ Thanks:
* [Linear Video game controller background Gadgets seamless pattern](https://stock.adobe.com/ru/images/linear-video-game-controller-background-gadgets-seamless-pattern/241143639)
by [Anna](https://stock.adobe.com/contributor/208277224/anna)
+# Announcement
+
+**[CloudMorph](https://github.com/giongto35/cloud-morph) is a sibling project that offers a more generic to
+run any offline games/application on browser in Cloud Gaming
+approach: [https://github.com/giongto35/cloud-morph](https://github.com/giongto35/cloud-morph))**
+
## Team
Authors:
+
- Nguyen Huu Thanh (https://www.linkedin.com/in/huuthanhnguyen)
- Tri Dang Minh (https://trich.im)
Maintainers:
+
- Sergey Stepanov (https://github.com/sergystepanov)
-
-
diff --git a/assets/cores/mupen64plus_next_libretro.cfg b/assets/cores/mupen64plus_next_libretro.cfg
deleted file mode 100644
index 6fa02e37..00000000
--- a/assets/cores/mupen64plus_next_libretro.cfg
+++ /dev/null
@@ -1,53 +0,0 @@
-mupen64plus-169screensize = 480x270
-mupen64plus-43screensize = 320x240
-mupen64plus-alt-map = False
-mupen64plus-aspect = 4:3
-mupen64plus-astick-deadzone = 15
-mupen64plus-astick-sensitivity = 100
-mupen64plus-BackgroundMode = OnePiece
-mupen64plus-BilinearMode = standard
-mupen64plus-CorrectTexrectCoords = Off
-mupen64plus-CountPerOp = 0
-mupen64plus-cpucore = dynamic_recompiler
-mupen64plus-CropMode = Auto
-mupen64plus-d-cbutton = C3
-mupen64plus-EnableCopyColorToRDRAM = Off
-mupen64plus-EnableCopyDepthToRDRAM = Software
-mupen64plus-EnableEnhancedHighResStorage = False
-mupen64plus-EnableEnhancedTextureStorage = False
-mupen64plus-EnableFBEmulation = True
-mupen64plus-EnableFragmentDepthWrite = False
-mupen64plus-EnableHWLighting = False
-mupen64plus-EnableLegacyBlending = True
-mupen64plus-EnableLODEmulation = True
-mupen64plus-EnableNativeResTexrects = Disabled
-mupen64plus-EnableOverscan = Enabled
-mupen64plus-EnableShadersStorage = True
-mupen64plus-EnableTextureCache = True
-mupen64plus-ForceDisableExtraMem = False
-mupen64plus-FrameDuping = False
-mupen64plus-Framerate = Original
-mupen64plus-FXAA = 0
-mupen64plus-l-cbutton = C2
-mupen64plus-MaxTxCacheSize = 8000
-mupen64plus-NoiseEmulation = True
-mupen64plus-OverscanBottom = 0
-mupen64plus-OverscanLeft = 0
-mupen64plus-OverscanRight = 0
-mupen64plus-OverscanTop = 0
-mupen64plus-pak1 = memory
-mupen64plus-pak2 = none
-mupen64plus-pak3 = none
-mupen64plus-pak4 = none
-mupen64plus-r-cbutton = C1
-mupen64plus-rdp-plugin = gliden64
-mupen64plus-rsp-plugin = hle
-mupen64plus-rspmode = HLE
-mupen64plus-txCacheCompression = True
-mupen64plus-txEnhancementMode = None
-mupen64plus-txFilterIgnoreBG = True
-mupen64plus-txFilterMode = None
-mupen64plus-txHiresEnable = False
-mupen64plus-txHiresFullAlphaChannel = False
-mupen64plus-u-cbutton = C4
-mupen64plus-virefresh = Auto
diff --git a/assets/games/Super Mario Bros.nes b/assets/games/Super Mario Bros.nes
deleted file mode 100644
index 878ef21b..00000000
Binary files a/assets/games/Super Mario Bros.nes and /dev/null differ
diff --git a/assets/games/dos/rogue.conf b/assets/games/dos/rogue.conf
new file mode 100644
index 00000000..015eb847
--- /dev/null
+++ b/assets/games/dos/rogue.conf
@@ -0,0 +1,2 @@
+[autoexec]
+ROGUE.EXE
\ No newline at end of file
diff --git a/assets/games/dos/rogue.zip b/assets/games/dos/rogue.zip
new file mode 100644
index 00000000..53129bd8
Binary files /dev/null and b/assets/games/dos/rogue.zip differ
diff --git a/assets/games/Sushi The Cat.gba b/assets/games/gba/Sushi The Cat.gba
similarity index 100%
rename from assets/games/Sushi The Cat.gba
rename to assets/games/gba/Sushi The Cat.gba
diff --git a/assets/games/anguna.gba b/assets/games/gba/anguna.gba
similarity index 100%
rename from assets/games/anguna.gba
rename to assets/games/gba/anguna.gba
diff --git a/assets/games/Sample Demo by Florian (PD).z64 b/assets/games/n64/Sample Demo by Florian (PD).z64
similarity index 100%
rename from assets/games/Sample Demo by Florian (PD).z64
rename to assets/games/n64/Sample Demo by Florian (PD).z64
diff --git a/assets/games/nes/Alwa's Awakening (Demo).nes b/assets/games/nes/Alwa's Awakening (Demo).nes
new file mode 100644
index 00000000..7c2f5155
Binary files /dev/null and b/assets/games/nes/Alwa's Awakening (Demo).nes differ
diff --git a/cmd/coordinator/main.go b/cmd/coordinator/main.go
index 94223f31..95609982 100644
--- a/cmd/coordinator/main.go
+++ b/cmd/coordinator/main.go
@@ -1,40 +1,32 @@
package main
import (
- "context"
- goflag "flag"
- "math/rand"
- "time"
-
- config "github.com/giongto35/cloud-game/v2/pkg/config/coordinator"
- "github.com/giongto35/cloud-game/v2/pkg/coordinator"
- "github.com/giongto35/cloud-game/v2/pkg/os"
- "github.com/giongto35/cloud-game/v2/pkg/util/logging"
- "github.com/golang/glog"
- flag "github.com/spf13/pflag"
+ "github.com/giongto35/cloud-game/v3/pkg/config"
+ "github.com/giongto35/cloud-game/v3/pkg/coordinator"
+ "github.com/giongto35/cloud-game/v3/pkg/logger"
+ "github.com/giongto35/cloud-game/v3/pkg/os"
)
-var Version = ""
-
-func init() {
- rand.Seed(time.Now().UTC().UnixNano())
-}
+var Version = "?"
func main() {
- conf := config.NewConfig()
- flag.CommandLine.AddGoFlagSet(goflag.CommandLine)
+ conf, paths := config.NewCoordinatorConfig()
conf.ParseFlags()
- logging.Init()
- defer logging.Flush()
-
- glog.Infof("[coordinator] version: %v", Version)
- glog.V(4).Infof("Coordinator configs %v", conf)
- c := coordinator.New(conf)
+ log := logger.NewConsole(conf.Coordinator.Debug, "c", false)
+ log.Info().Msgf("version %s", Version)
+ log.Info().Msgf("conf: v%v, loaded: %v", conf.Version, paths)
+ if log.GetLevel() < logger.InfoLevel {
+ log.Debug().Msgf("conf: %+v", conf)
+ }
+ c, err := coordinator.New(conf, log)
+ if err != nil {
+ log.Error().Err(err).Msgf("init fail")
+ return
+ }
c.Start()
-
- ctx, cancelCtx := context.WithCancel(context.Background())
- defer c.Shutdown(ctx)
<-os.ExpectTermination()
- cancelCtx()
+ if err := c.Stop(); err != nil {
+ log.Error().Err(err).Msg("shutdown fail")
+ }
}
diff --git a/cmd/worker/default.pgo b/cmd/worker/default.pgo
new file mode 100644
index 00000000..c659757c
Binary files /dev/null and b/cmd/worker/default.pgo differ
diff --git a/cmd/worker/main.go b/cmd/worker/main.go
index 162c311e..a20cd819 100644
--- a/cmd/worker/main.go
+++ b/cmd/worker/main.go
@@ -1,45 +1,40 @@
package main
import (
- "context"
- goflag "flag"
- "math/rand"
"time"
- config "github.com/giongto35/cloud-game/v2/pkg/config/worker"
- "github.com/giongto35/cloud-game/v2/pkg/os"
- "github.com/giongto35/cloud-game/v2/pkg/thread"
- "github.com/giongto35/cloud-game/v2/pkg/util/logging"
- "github.com/giongto35/cloud-game/v2/pkg/worker"
- "github.com/golang/glog"
- flag "github.com/spf13/pflag"
+ "github.com/giongto35/cloud-game/v3/pkg/config"
+ "github.com/giongto35/cloud-game/v3/pkg/logger"
+ "github.com/giongto35/cloud-game/v3/pkg/os"
+ "github.com/giongto35/cloud-game/v3/pkg/worker"
+ "github.com/giongto35/cloud-game/v3/pkg/worker/thread"
)
-var Version = ""
-
-func init() {
- rand.Seed(time.Now().UTC().UnixNano())
-}
+var Version = "?"
func run() {
- conf := config.NewConfig()
- flag.CommandLine.AddGoFlagSet(goflag.CommandLine)
+ conf, paths := config.NewWorkerConfig()
conf.ParseFlags()
- logging.Init()
- defer logging.Flush()
+ log := logger.NewConsole(conf.Worker.Debug, "w", false)
+ log.Info().Msgf("version %s", Version)
+ log.Info().Msgf("conf: v%v, loaded: %v", conf.Version, paths)
+ if log.GetLevel() < logger.InfoLevel {
+ log.Debug().Msgf("conf: %+v", conf)
+ }
- glog.Infof("[worker] version: %v", Version)
- glog.V(4).Infof("[worker] Local configuration %+v", conf)
- wrk := worker.New(conf)
- wrk.Start()
-
- ctx, cancelCtx := context.WithCancel(context.Background())
- defer wrk.Shutdown(ctx)
- <-os.ExpectTermination()
- cancelCtx()
+ done := os.ExpectTermination()
+ w, err := worker.New(conf, log)
+ if err != nil {
+ log.Error().Err(err).Msgf("init fail")
+ return
+ }
+ w.Start(done)
+ <-done
+ time.Sleep(100 * time.Millisecond) // hack
+ if err := w.Stop(); err != nil {
+ log.Error().Err(err).Msg("shutdown fail")
+ }
}
-func main() {
- thread.MainWrapMaybe(run)
-}
+func main() { thread.Wrap(run) }
diff --git a/configs/config.yaml b/configs/config.yaml
deleted file mode 100644
index 3fa0d0b8..00000000
--- a/configs/config.yaml
+++ /dev/null
@@ -1,221 +0,0 @@
-#
-# Application configuration file
-#
-
-# application environment (dev, staging, prod)
-# deprecated
-environment: dev
-
-coordinator:
- # address if the server want to connect directly to debug
- debugHost:
- # games library
- library:
- # some directory which is gonna be the 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
- profilingEnabled: false
- metricEnabled: false
- urlPrefix: /coordinator
- # HTTP(S) server config
- server:
- address: :8000
- https: false
- # Letsencrypt or self cert config
- tls:
- address: :443
- # allowed host name
- domain:
- # if both are set then will use certs
- # and Letsencryt instead
- httpsCert:
- httpsKey:
- analytics:
- inject: false
- gtag:
-
-worker:
- network:
- # a coordinator address to connect to
- coordinatorAddress: localhost:8000
- # where to connect
- endpoint: /wso
- # ping endpoint
- pingEndpoint: /echo
- # set public ping address (IP or hostname)
- publicAddress:
- # make coordinator connection secure (wss)
- secure: false
- # ISO Alpha-2 country code to group workers by zones
- zone:
- monitoring:
- # monitoring server port
- port: 6602
- profilingEnabled: false
- # monitoring server URL prefix
- metricEnabled: false
- urlPrefix: /worker
- server:
- address: :9000
- https: false
- tls:
- address: :444
- # LetsEncrypt config
- # allowed host name
- domain:
- # Own certs config
- httpsCert:
- httpsKey:
-
-emulator:
- # set output viewport scale factor
- scale: 1
-
- aspectRatio:
- # enable aspect ratio changing
- # (experimental)
- keep: false
- # recalculate emulator game frame size to the given WxH
- width: 320
- height: 240
-
- # save directory for emulator states
- # special tag {user} will be replaced with current user's home dir
- storage: "{user}/.cr/save"
-
- libretro:
- cores:
- paths:
- libs: assets/cores
- configs: assets/cores
- # Config params for Libretro cores repository,
- # available types are:
- # - buildbot (the default Libretro nightly repository)
- # - github (GitHub raw repository with a similar structure to buildbot)
- # - raw (just a link to a zip file extracted as is)
- repo:
- # enable auto-download for the list of cores (list->lib)
- sync: true
- # external cross-process mutex lock
- extLock: "{user}/.cr/cloud-game.lock"
- main:
- type: buildbot
- url: https://buildbot.libretro.com/nightly
- # if repo has file compression
- compression: zip
- # a secondary repo to use i.e. for not found in the main cores
- secondary:
- type: github
- url: https://github.com/sergystepanov/libretro-spiegel/blob/main
- compression: zip
- # Libretro core configuration
- #
- # The emulator selection will happen in this order:
- # - based on the folder name in the folder param
- # - based on the folder name (core name) in the list (i.e. nes, snes)
- # - based on the rom names in the roms param
- #
- # Available config params:
- # - lib (string)
- # - config (string)
- # - roms ([]string)
- # - folder (string)
- # By default emulator selection is based on the folder named as cores
- # in the list (i.e. nes, snes) but if you specify folder param,
- # then it will try to load the ROM file from that folder first.
- # - width (int) -- broken
- # - height (int) -- broken
- # - ratio (float)
- # - isGlAllowed (bool)
- # - usesLibCo (bool)
- # - hasMultitap (bool)
- list:
- gba:
- lib: mgba_libretro
- roms: [ "gba", "gbc" ]
- pcsx:
- lib: pcsx_rearmed_libretro
- roms: [ "cue" ]
- # example of folder override
- folder: psx
- # MAME core requires additional manual setup, please read:
- # https://docs.libretro.com/library/fbneo/
- mame:
- lib: fbneo_libretro
- roms: [ "zip" ]
- nes:
- lib: nestopia_libretro
- roms: [ "nes" ]
- snes:
- lib: snes9x_libretro
- roms: [ "smc", "sfc", "swc", "fig", "bs" ]
- hasMultitap: true
- n64:
- lib: mupen64plus_next_libretro
- config: mupen64plus_next_libretro.cfg
- roms: [ "n64", "v64", "z64" ]
- isGlAllowed: true
- usesLibCo: true
-
-encoder:
- audio:
- channels: 2
- # audio frame duration needed for WebRTC (Opus)
- frame: 20
- frequency: 48000
- video:
- # h264, vpx (VP8)
- codec: h264
- # see: https://trac.ffmpeg.org/wiki/Encode/H.264
- h264:
- # Constant Rate Factor (CRF) 0-51 (default: 23)
- crf: 17
- # ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow, placebo
- preset: veryfast
- # baseline, main, high, high10, high422, high444
- profile: main
- # film, animation, grain, stillimage, psnr, ssim, fastdecode, zerolatency
- tune: zerolatency
- # 0-3
- logLevel: 0
- # see: https://www.webmproject.org/docs/encoder-parameters
- vpx:
- # target bitrate (KBit/s)
- bitrate: 1200
- # force keyframe interval
- keyframeInterval: 5
- # run without a game
- # (experimental)
- withoutGame: false
-
-webrtc:
- # turn off default Pion interceptors for performance reasons
- # (experimental)
- disableDefaultInterceptors:
- # a list of STUN/TURN servers for the client
- iceServers:
- - url: stun:stun.l.google.com:19302
- # instead of random unlimited port range for
- # WebRTC UDP connections, these params
- # define ICE candidates port range explicitly
- icePorts:
- min:
- max:
- # override ICE candidate IP, see: https://github.com/pion/webrtc/issues/835,
- # can be used for Docker bridged network internal IP override
- iceIpMap:
diff --git a/docker-compose.yml b/docker-compose.yml
index 93b5ccfa..dff2c59a 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,21 +1,33 @@
-version: '3'
services:
cloud-game:
build: .
image: cloud-game-local
container_name: cloud-game-local
- privileged: true
environment:
- DISPLAY=:99
- - MESA_GL_VERSION_OVERRIDE=3.3
- network_mode: "host"
+ - MESA_GL_VERSION_OVERRIDE=4.5
+ - CLOUD_GAME_WEBRTC_SINGLEPORT=8443
+ # - CLOUD_GAME_WEBRTC_ICEIPMAP=127.0.0.1
+ - CLOUD_GAME_COORDINATOR_DEBUG=true
+ - CLOUD_GAME_WORKER_DEBUG=true
+ # - PION_LOG_TRACE=all
+ ports:
+ - "8000:8000"
+ - "9000:9000"
+ - "8443:8443/udp"
command: >
- bash -c "Xvfb :99 & coordinator --v=5 & worker --coordinatorhost localhost:8000"
+ bash -c "./coordinator & ./worker"
volumes:
- # keep cores persistent in the cloud-game_cores volume
- - cores:/usr/local/share/cloud-game/assets/cores
- - ${CLOUD_GAME_GAMES_PATH}:/usr/local/share/cloud-game/assets/games
+ - ./assets/cores:/usr/local/share/cloud-game/assets/cores
+ - ./assets/games:/usr/local/share/cloud-game/assets/games
+ - x11:/tmp/.X11-unix
+
+ xvfb:
+ image: kcollins/xvfb:latest
+ volumes:
+ - x11:/tmp/.X11-unix
+ command: [ ":99", "-screen", "0", "320x240x16" ]
volumes:
- cores:
+ x11:
diff --git a/docs/STREAMING.md b/docs/STREAMING.md
deleted file mode 100644
index 5eeb54ad..00000000
--- a/docs/STREAMING.md
+++ /dev/null
@@ -1,47 +0,0 @@
-## Streaming process description
-
-This document describes the step-by-step process of media streaming in all parts of the application.
-
-```
-┌──────────────┐ ┌───────────────┐ ┌──────────────┐
-│ USER AGENT │ │ COORDINATOR │ │ WORKER...n │
-├──────────────┤ ├───────────────┤ ├──────────────┤
-│ TCP/WS ├──1──►│ WS ──────► WS │◄───┤ TCP/WS │
-│ │ │ ▲ 2 │ │ │ │
-│ │ │ └───────────┘ │ │ │
-│ │ └───────────────┘ │ │
-│ UDP/RTP │◄─────────────3────────────┤ UDP/RTP │
-│ AUDIO < │ OPUS │ AUDIO │
-│ VIDEO < │ VP8/H264 │ VIDEO │
-│ DATA > │ 010101 │ DATA │
-└──────────────┘ └──────────────┘
-```
-
-The app is based on WebRTC technology which allows the server to stream media and exchange data with ultra-low latencies. An essential part of these types of P2P connections is the signaling process. It's implemented as a custom text-based messaging protocol on top of WebSocket (quite similarly to [WAMP](https://wamp-proto.org)). The app supports both STUN and TURN protocols for NAT traversal or ICE. In terms of supported codecs, it can stream h264, VP8, and OPUS media.
-
-The streaming process begins when a user opens the main application page (index.html) served by the coordinator.
-- The user's browser tries to open a new WebSocket connection to the coordinator — socket.init(roomId, zone) [web/js/network/socket.js:32](https://github.com/giongto35/cloud-game/blob/ae5260fb4726fd34cc0b0b05100dcc8457f52883/web/js/network/socket.js#L32)
-> In the initial WebSocket Upgrade request query it may send two params: roomId — an identification number for existing game rooms stored in the URL query of the application page (i.e. app.com/?id=xxxxxx), zone — or, more precisely, region — serves the purpose of CDN and geographical segmentation of the streaming.
-- On the coordinator side this request goes into a dedicated handler (/ws) — func (o *Server) WS(w http.ResponseWriter, r *http.Request) [pkg/coordinator/handlers.go:150](https://github.com/giongto35/cloud-game/blob/ae5260fb4726fd34cc0b0b05100dcc8457f52883/pkg/coordinator/handlers.go#L150)
-- There, it unconditionally accepts the WebSocket connection and tags it with some ID, so it will be listening to messages from the user's side. Here a new client connection should be considered as established.
-- Next, given provided query params, the coordinator tries to find a suitable worker whose job — directly stream games to a user.
-> This process of choosing the right worker is following: if there is no roomId param, then the coordinator gathers the full list of available workers, filters them by a zone value (if provided), returns the user a list of public URLs, which he can ping and send results back to the coordinator. After that, the coordinator links the fastest one with the user. Alternatively, if the user did provide some roomId, then the coordinator directly assigns a worker with that room (workers have 1:1 mapping to rooms or games).
-> All the information exchange initiated from the worker side is handled in a separate endpoint (/wso) [pkg/coordinator/handlers.go#L81](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/pkg/coordinator/handlers.go#L81).
-- Coordinator sends to the user ICE servers and the list of games available for playing. That's handled in [web/js/network/socket.js:57](https://github.com/giongto35/cloud-game/blob/ae5260fb4726fd34cc0b0b05100dcc8457f52883/web/js/network/socket.js#L57).
-- From this point, the user's browser begins to initialize WebRTC connection to the worker — web/js/controller.js:413 → [web/js/network/rtcp.js:16](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/web/js/network/rtcp.js#L16).
-- First, it sends init request through the WebSocket connection to the coordinator handler in [pkg/coordinator/useragenthandlers.go:17](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/pkg/coordinator/useragenthandlers.go#L17).
-> Following a standard WebRTC call [negotiation procedure](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Signaling_and_video_calling), the coordinator acts as a mediator between users and workers. The signaling protocol here is a text messaging through WebSocket transport.
-- Coordinator notifies the user's worker that it wants to establish a new PeerConnection (call). That part is being handled in [pkg/worker/internalhandlers.go:42](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/pkg/worker/internalhandlers.go#L42). It is worth noting that it is a worker who makes SDP offer and waits for an SDP answer.
-- Worker initializes new WebRTC connection handler in func (w *WebRTC) StartClient(isMobile bool, iceCB OnIceCallback) (string, error) [pkg/webrtc/webrtc.go:103](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/pkg/webrtc/webrtc.go#L103).
-- Then through the coordinator it makes simultaneously an SDP offer as well as sends ICE candidates that are handled on the coordinator side (from the user) in [pkg/coordinator/useragenthandlers.go](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/pkg/coordinator/useragenthandlers.go),
-(from the worker) in [pkg/coordinator/internalhandlers.go](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/pkg/coordinator/internalhandlers.go), and on the user side both in [web/js/network/socket.js:56](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/web/js/network/socket.js#L56) and inside [web/js/network/rtcp.js](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/web/js/network/rtcp.js).
- - Browser on the user's side after SDP offer links remote streams to the HTML Video element in [web/js/controller.js:417](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/web/js/controller.js#L417), makes SDP answer and gathers remote ICE candidates until it's done (if receive an empty ICE candidate).
- - For the user's side a successful WebRTC connection should be considered established when WebRTC datachannel is opened here [web/js/network/rtcp.js:31](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/web/js/network/rtcp.js#L31).
- *And that should be it for the streaming part.*
- > At this point all the connections should be successfully established and the user's ready for a game to start. The coordinator should notify the worker about that fact and the worker starts pushing media frames, listen to the input through the direct to the user WebRTC data channel.
- - Then the user may send the game start request to the coordinator in [web/js/controller.js:153](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/web/js/controller.js#L153).
-
-#### Streaming requirements
-- Workers should not have any closed UDP ports to be able to provide suitable ICE candidates.
-- Coordinator should have at least one non-blocked TCP port (default: 8000) for HTTP/WebSocket signaling connections from users and workers.
-- Browser should not block WebRTC and support it (check [here](https://test.webrtc.org/)).
diff --git a/docs/designdoc/README.md b/docs/designdoc/README.md
deleted file mode 100644
index b15cd503..00000000
--- a/docs/designdoc/README.md
+++ /dev/null
@@ -1,25 +0,0 @@
-# Web-based Cloud Gaming Service Design Document
-
-Web-based Cloud Gaming Service contains multiple workers for gaming stream and a coordinator (Coordinator) for distributing traffic and pairing up connection.
-
-## Worker
-
-Worker is responsible for streaming game to frontend
-
-
-- After Coordinator matches the most appropriate server to the user, webRTC peer-to-peer handshake will be conducted. The coordinator will exchange the signature (WebRTC Session Remote Description) between two peers over Web Socket connection.
-- On worker, each user session will spawn a new room running a gaming emulator. Image stream and audio stream from emulator is captured and encoded to WebRTC streaming format. We applied Vp8 for Video compression and Opus for audio compression to ensure the smoothest experience. After finish encoded, these stream is then piped out to user and observers joining that room.
-- On the other hand, input from users is sent to workers over WebRTC DataChannel. Game logic on the emulator will be updated based on the input stream.
-- Game state is stored in cloud storage, so all workers can collaborate and keep the same understanding with each other. It allows user can continue from the saved state in the next time.
-
-## Coordinator
-
-Coordinator is loadbalancer and coordinator, which is in charge of picking the most suitable workers for a user. Every time a user connects to Coordinator, it will collect all the metric from all workers, i.e free CPU resources and latency from worker to user. Coordinator will decide the best candidate based on the metric and setup peer-to-peer connection between worker and user based on WebRTC protocol
-
-
-
-1. A user connected to Coordinator .
-2. Coordinator will find the most suitable worker to serve the user.
-3. Coordinator collects all latencies from workers to users as well as CPU usage on each machine.
-4. Coordinator setup peer-to-peer handshake between worker and user by exchanging Session Description Protocol.
-5. A game is hosted on worker and streamed to the user.
diff --git a/docs/designdoc/implementation/README.md b/docs/designdoc/implementation/README.md
deleted file mode 100644
index 17626a35..00000000
--- a/docs/designdoc/implementation/README.md
+++ /dev/null
@@ -1,43 +0,0 @@
-# Web-based Cloud Gaming Service Implementation Document
-
-## Code structure
-```
-.
-├── cmd: service entrypoint
-│ ├── main.go: Spawn coordinator or worker based on flag
-│ └── main_test.go
-├── static: static file for front end
-│ ├── js
-│ │ └── ws.js: client logic
-│ ├── game.html: frontend with gameboy ui
-│ └── index_ws.html: raw frontend without ui
-├── coordinator: coordinator
-│ ├── handlers.go: coordinator entrypoint
-│ ├── browser.go: router listening to browser
-│ └── worker.go: router listening to worker
-├── games: roms list, no code logic
-├── worker: integration between emulator + webrtc (communication)
-│ ├── room:
-│ │ ├── room.go: room logic
-│ │ └── media.go: video + audio encoding
-│ ├── handlers.go: worker entrypoint
-│ └── coordinator.go: router listening to coordinator
-├── emulator: emulator internal
-│ ├── nes: NES device internal
-│ ├── director.go: coordinator of views
-│ └── gameview.go: in game logic
-├── cws
-│ └── cws.go: socket multiplexer library, used for signaling
-└── webrtc: webrtc streaming logic
-```
-
-## Room
-Room is a fundamental part of the system. Each user session will spawn a room with a game running inside. There is a pipeline to encode images and audio and stream them out from emulator to user. The pipeline also listens to all input and streams to the emulator.
-
-## Worker
-Worker is an instance that can be provisioned to scale up the traffic. There are multiple rooms inside a worker. Worker will listen to coordinator events in `coordinator.go`.
-
-## Coordinator
-Coordinator is the coordinator, which handles all communication with workers and frontend.
-Coordinator will pair up a worker and a user for peer streaming. In WebRTC handshaking, two peers need to exchange their signature (Session Description Protocol) to initiate a peerconnection.
-Events come from frontend will be handled in `coordinator/browser.go`. Events come from worker will be handled in `coordinator/worker.go`. Coordinator stays in the middle and relays handshake packages between workers and user.
diff --git a/docs/img/coordinator.png b/docs/img/coordinator.png
deleted file mode 100644
index 55eebeb4..00000000
Binary files a/docs/img/coordinator.png and /dev/null differ
diff --git a/docs/img/crowdplay.gif b/docs/img/crowdplay.gif
deleted file mode 100644
index 66514a5e..00000000
Binary files a/docs/img/crowdplay.gif and /dev/null differ
diff --git a/docs/img/landing-page-dark.png b/docs/img/landing-page-dark.png
deleted file mode 100644
index 21abc792..00000000
Binary files a/docs/img/landing-page-dark.png and /dev/null differ
diff --git a/docs/img/landing-page-front.png b/docs/img/landing-page-front.png
deleted file mode 100644
index 700ea116..00000000
Binary files a/docs/img/landing-page-front.png and /dev/null differ
diff --git a/docs/img/landing-page-gb.png b/docs/img/landing-page-gb.png
deleted file mode 100644
index 250f2a4d..00000000
Binary files a/docs/img/landing-page-gb.png and /dev/null differ
diff --git a/docs/img/landing-page-ps-hm.png b/docs/img/landing-page-ps-hm.png
deleted file mode 100644
index d06a9295..00000000
Binary files a/docs/img/landing-page-ps-hm.png and /dev/null differ
diff --git a/docs/img/landing-page-ps-x4.png b/docs/img/landing-page-ps-x4.png
deleted file mode 100644
index 4d173dbf..00000000
Binary files a/docs/img/landing-page-ps-x4.png and /dev/null differ
diff --git a/docs/img/landing-page.gif b/docs/img/landing-page.gif
deleted file mode 100644
index 691f4d9e..00000000
Binary files a/docs/img/landing-page.gif and /dev/null differ
diff --git a/docs/img/landing-page2.png b/docs/img/landing-page2.png
deleted file mode 100644
index 1dfabd5c..00000000
Binary files a/docs/img/landing-page2.png and /dev/null differ
diff --git a/docs/img/multiplatform.png b/docs/img/multiplatform.png
deleted file mode 100644
index 40fcf4f1..00000000
Binary files a/docs/img/multiplatform.png and /dev/null differ
diff --git a/docs/img/overview.png b/docs/img/overview.png
deleted file mode 100644
index aa7ef1df..00000000
Binary files a/docs/img/overview.png and /dev/null differ
diff --git a/docs/img/worker-internal.png b/docs/img/worker-internal.png
deleted file mode 100644
index 63c89d7c..00000000
Binary files a/docs/img/worker-internal.png and /dev/null differ
diff --git a/docs/userguide/instruction/README.md b/docs/userguide/instruction/README.md
deleted file mode 100644
index f3245c2e..00000000
--- a/docs/userguide/instruction/README.md
+++ /dev/null
@@ -1,28 +0,0 @@
-# Web-based Cloud Gaming Service Game Instruction
-
-The game can be played on Desktop, Mobile (Android only). You can plug joystick to play with the game.
-
-Click question mark on the top left to see game instruction.
-
-## Key map on Desktop
-Game keymap follows
-Arrow keys to move
-H -> Show help
-C -> Start
-V -> Select
-Z -> A
-X -> B
-S -> Save (Save state)
-A -> Load (Load previous saved state)
-W -> Share your running game to other or you can keep it to continue playing the next time. Multiple people can access the same game for multiplayer or observation.
-F -> Full screen
-Q -> Quit the current game and go to menu screen.**NOTE**: we are facing some issue with quit, so it's better to refresh the page.
-
-## Mobile play
-You can play the game on Android device. Make sure your Android has the version that support WebRTC. IOS doesn't support WebRTC streaming now.
-
-The keys map are equivalent to Desktop. Press the button to fire input.
-
-## Joystick
-The game also accepts joystick, so you can try plug in one and experience. It will be very fun!
-
diff --git a/go.mod b/go.mod
index 5578a13f..5cfb890b 100644
--- a/go.mod
+++ b/go.mod
@@ -1,37 +1,62 @@
-module github.com/giongto35/cloud-game/v2
+module github.com/giongto35/cloud-game/v3
-go 1.13
+go 1.25
require (
- cloud.google.com/go v0.91.1 // indirect
- cloud.google.com/go/storage v1.16.0
- github.com/cavaliercoder/grab v1.0.1-0.20201108051000-98a5bfe305ec
- github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3
- github.com/fsnotify/fsnotify v1.4.9
- github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f
- github.com/gofrs/flock v0.8.1
- github.com/gofrs/uuid v4.0.0+incompatible
- github.com/golang/glog v0.0.0-20210429001901-424d2337a529
- github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
- github.com/gorilla/websocket v1.4.2
- github.com/kkyr/fig v0.3.0
- github.com/pion/ice/v2 v2.1.12 // indirect
- github.com/pion/interceptor v0.0.15
- github.com/pion/rtp v1.7.1
- github.com/pion/srtp/v2 v2.0.5 // indirect
- github.com/pion/webrtc/v3 v3.0.32
- github.com/prometheus/client_golang v1.11.0
- github.com/prometheus/common v0.30.0 // indirect
- github.com/prometheus/procfs v0.7.2 // indirect
- github.com/spf13/pflag v1.0.5
- github.com/veandco/go-sdl2 v0.4.8
- golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e
- golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d
- golang.org/x/mod v0.5.0 // indirect
- golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d // indirect
- golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5 // indirect
- golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71 // indirect
- golang.org/x/text v0.3.7 // indirect
- google.golang.org/api v0.54.0 // indirect
- google.golang.org/genproto v0.0.0-20210816143620-e15ff196659d // indirect
+ github.com/VictoriaMetrics/metrics v1.40.2
+ github.com/cavaliergopher/grab/v3 v3.0.1
+ 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/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/reflectwalk v1.0.2 // 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.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
+ 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
)
diff --git a/go.sum b/go.sum
index 33f69722..e104a47a 100644
--- a/go.sum
+++ b/go.sum
@@ -1,762 +1,133 @@
-cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
-cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
-cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
-cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
-cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
-cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
-cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
-cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
-cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
-cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
-cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
-cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
-cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
-cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
-cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
-cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
-cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
-cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
-cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
-cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
-cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
-cloud.google.com/go v0.88.0/go.mod h1:dnKwfYbP9hQhefiUvpbcAyoGSHUrOxR20JVElLiUvEY=
-cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
-cloud.google.com/go v0.91.1 h1:w+u8ttN/QtYrpvgXNUd2G6kwqrqCIQbkINlXQjHP1ek=
-cloud.google.com/go v0.91.1/go.mod h1:V358WZfbFQkmC3gv5XCxzZq2e3h7OGvQR0IXtj77ylI=
-cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
-cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
-cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
-cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
-cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
-cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
-cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
-cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
-cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
-cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
-cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
-cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
-cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
-cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
-cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
-cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
-cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
-cloud.google.com/go/storage v1.16.0 h1:1UwAux2OZP4310YXg5ohqBEpV16Y93uZG4+qOX7K2Kg=
-cloud.google.com/go/storage v1.16.0/go.mod h1:ieKBmUyzcftN5tbxwnXClMKH00CfcQ+xL6NN0r5QfmE=
-dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
-github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
-github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
-github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
-github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
-github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
-github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
-github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
-github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
-github.com/cavaliercoder/grab v1.0.1-0.20201108051000-98a5bfe305ec h1:4XvMn0XuV7qxCH22gbnR79r+xTUaLOSA0GW/egpO3SQ=
-github.com/cavaliercoder/grab v1.0.1-0.20201108051000-98a5bfe305ec/go.mod h1:NbXoa59CCAGqtRm7kRrcZIk2dTCJMRVF8QI3BOD7isY=
-github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
-github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
-github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
-github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
-github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
-github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
-github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
-github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
-github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
-github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
-github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
-github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3 h1:baVdMKlASEHrj19iqjARrPbaRisD7EuZEVJj6ZMLl1Q=
-github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3/go.mod h1:VEPNJUlxl5KdWjDvz6Q1l+rJlxF2i6xqDeGuGAxa87M=
-github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
-github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
-github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
-github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f h1:s0O46d8fPwk9kU4k1jj76wBquMVETx7uveQD9MCIQoU=
-github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f/go.mod h1:wjpnOv6ONl2SuJSxqCPVaPZibGFdSci9HFocT9qtVYM=
-github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
-github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
-github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
-github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
-github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
-github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
-github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
-github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
-github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
-github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
-github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
-github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/glog v0.0.0-20210429001901-424d2337a529 h1:2voWjNECnrZRbfwXxHB1/j8wa6xdKn85B5NzgVL/pTU=
-github.com/golang/glog v0.0.0-20210429001901-424d2337a529/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
-github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
-github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
-github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
-github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
-github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
-github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
-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.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
-github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
-github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
-github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
-github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
-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.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.4/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/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
-github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
-github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
-github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
-github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
-github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ=
-github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
-github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210715191844-86eeefc3e471/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210804190019-f964ff605595/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
-github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
-github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
-github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
-github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
-github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
-github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
-github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
-github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
-github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
-github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
-github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
-github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
-github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
-github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/kkyr/fig v0.3.0 h1:5bd1amYKp/gsK2bGEUJYzcCrQPKOZp6HZD9K21v9Guo=
-github.com/kkyr/fig v0.3.0/go.mod h1:fEnrLjwg/iwSr8ksJF4DxrDmCUir5CaVMLORGYMcz30=
-github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
-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/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
-github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
-github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
-github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
-github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-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.1/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=
-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.11.0/go.mod h1:azGKhqFUon9Vuj0YmTfLSmx0FUwqXYSTl5re8lQLTUg=
-github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ=
-github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
-github.com/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXmf0=
-github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg=
-github.com/pion/dtls/v2 v2.0.9 h1:7Ow+V++YSZQMYzggI0P9vLJz/hUFcffsfGMfT/Qy+u8=
-github.com/pion/dtls/v2 v2.0.9/go.mod h1:O0Wr7si/Zj5/EBFlDzDd6UtVxx25CE1r7XM7BQKYQho=
-github.com/pion/ice/v2 v2.1.10/go.mod h1:kV4EODVD5ux2z8XncbLHIOtcXKtYXVgLVCeVqnpoeP0=
-github.com/pion/ice/v2 v2.1.12 h1:ZDBuZz+fEI7iDifZCYFVzI4p0Foy0YhdSSZ87ZtRcRE=
-github.com/pion/ice/v2 v2.1.12/go.mod h1:ovgYHUmwYLlRvcCLI67PnQ5YGe+upXZbGgllBDG/ktU=
-github.com/pion/interceptor v0.0.13/go.mod h1:svsW2QoLHLoGLUr4pDoSopGBEWk8FZwlfxId/OKRKzo=
-github.com/pion/interceptor v0.0.15 h1:pQFkBUL8akUHiGoFr+pM94Q/15x7sLFh0K3Nj+DCC6s=
-github.com/pion/interceptor v0.0.15/go.mod h1:pg3J253eGi5bqyKzA74+ej5Y19ez2jkWANVnF+Z9Dfk=
-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.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw=
-github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g=
+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.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/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/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
+github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+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.6 h1:1zvwBbyd0TeEuuWftrd/4d++m+/kZSeiguxU61LFWpo=
-github.com/pion/rtcp v1.2.6/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0=
-github.com/pion/rtp v1.6.2/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
-github.com/pion/rtp v1.6.5/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
-github.com/pion/rtp v1.7.0/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
-github.com/pion/rtp v1.7.1 h1:hCaxfVgPGt13eF/Tu9RhVn04c+dAcRZmhdDWqUE13oY=
-github.com/pion/rtp v1.7.1/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
-github.com/pion/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0=
-github.com/pion/sctp v1.7.12 h1:GsatLufywVruXbZZT1CKg+Jr8ZTkwiPnmUC/oO9+uuY=
-github.com/pion/sctp v1.7.12/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
-github.com/pion/sdp/v3 v3.0.4 h1:2Kf+dgrzJflNCSw3TV5v2VLeI0s/qkzy2r5jlR0wzf8=
-github.com/pion/sdp/v3 v3.0.4/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk=
-github.com/pion/srtp/v2 v2.0.2/go.mod h1:VEyLv4CuxrwGY8cxM+Ng3bmVy8ckz/1t6A0q/msKOw0=
-github.com/pion/srtp/v2 v2.0.5 h1:ks3wcTvIUE/GHndO3FAvROQ9opy0uLELpwHJaQ1yqhQ=
-github.com/pion/srtp/v2 v2.0.5/go.mod h1:8k6AJlal740mrZ6WYxc4Dg6qDqqhxoRG2GSjlUhDF0A=
-github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
-github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
-github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A=
-github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
-github.com/pion/transport v0.12.3 h1:vdBfvfU/0Wq8kd2yhUMSDB/x+O4Z9MYVl2fJ5BT4JZw=
-github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A=
-github.com/pion/turn/v2 v2.0.5 h1:iwMHqDfPEDEOFzwWKT56eFmh6DYC6o/+xnLAEzgISbA=
-github.com/pion/turn/v2 v2.0.5/go.mod h1:APg43CFyt/14Uy7heYUOGWdkem/Wu4PhCO/bjyrTqMw=
-github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
-github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
-github.com/pion/webrtc/v3 v3.0.32 h1:5J+zNep9am8Swh6kEMp+LaGXNvn6qQWpGkLBnVW44L4=
-github.com/pion/webrtc/v3 v3.0.32/go.mod h1:wX3V5dQQUGCifhT1mYftC2kCrDQX6ZJ3B7Yad0R9JK0=
-github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
-github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
-github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
-github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ=
-github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
-github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
-github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
-github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
-github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
-github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
-github.com/prometheus/common v0.30.0 h1:JEkYlQnpzrzQFxi6gnukFPdQ+ac82oRhzMcIduJu/Ug=
-github.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
-github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
-github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
-github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
-github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
-github.com/prometheus/procfs v0.7.2 h1:zE6zJjRS9S916ptrZ326OU0++1XRwHgxkvCFflxx6Fo=
-github.com/prometheus/procfs v0.7.2/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
-github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
-github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
-github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
-github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
-github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
-github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
-github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
-github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
-github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
-github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/veandco/go-sdl2 v0.4.8 h1:A26KeX6R1CGt/BQGEov6oxYmVGMMEWDVqTvK1tXvahE=
-github.com/veandco/go-sdl2 v0.4.8/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY=
-github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
-go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
-go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
-go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
-go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
-go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
-go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
-golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-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-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
-golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e h1:VvfwVmMH40bpMeizC9/K7ipM5Qjucuu16RWfneFPyhQ=
-golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
-golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
-golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
-golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
-golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
-golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
-golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
-golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs=
-golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
-golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
-golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
-golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug=
-golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
-golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
-golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
-golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
-golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
-golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
-golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.5.0 h1:UG21uOlmZabA4fW5i7ZX6bjw1xELEGg/ZLgZq9auk/Q=
-golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
-golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
-golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d h1:LO7XpTYMwTqxjLcGWPijK3vRXg1aWdlNOVOHRq45d7c=
-golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210615190721-d04028783cf1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5 h1:Ati8dO7+U7mxpkPSxBZQEvzHVUYB/MqCklCN8ig5w/o=
-golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/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-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/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-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/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-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71 h1:ikCpsnYR+Ew0vu99XlDp55lGgDJdIMx3f4a18jfse/s=
-golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
-golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
-golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
-golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
-golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
-golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
-golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA=
-golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
-google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
-google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
-google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
-google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
-google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
-google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
-google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
-google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
-google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
-google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
-google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
-google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
-google.golang.org/api v0.49.0/go.mod h1:BECiH72wsfwUvOVn3+btPD5WHi0LzavZReBndi42L18=
-google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
-google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
-google.golang.org/api v0.52.0/go.mod h1:Him/adpjt0sxtkWViy0b6xyKW/SD71CwdJ7HqJo7SrU=
-google.golang.org/api v0.54.0 h1:ECJUVngj71QI6XEm7b1sAf8BljU5inEhMbKPR8Lxhhk=
-google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
-google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
-google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
-google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
-google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
-google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
-google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
-google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
-google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
-google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
-google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
-google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
-google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
-google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
-google.golang.org/genproto v0.0.0-20210617175327-b9e0b3197ced/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
-google.golang.org/genproto v0.0.0-20210624174822-c5cf32407d0a/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
-google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
-google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
-google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
-google.golang.org/genproto v0.0.0-20210721163202-f1cecdd8b78a/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
-google.golang.org/genproto v0.0.0-20210722135532-667f2b7c528f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
-google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
-google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
-google.golang.org/genproto v0.0.0-20210811021853-ddbe55d93216/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
-google.golang.org/genproto v0.0.0-20210816143620-e15ff196659d h1:fPtHPeysWvGVJwQFKu3B7H2DB2sOEsW7UTayKkWESKw=
-google.golang.org/genproto v0.0.0-20210816143620-e15ff196659d/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
-google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
-google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
-google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
-google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
-google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
-google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
-google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
-google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
-google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
-google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
-google.golang.org/grpc v1.40.0 h1:AGJ0Ih4mHjSeibYkFGh1dD9KJ/eOtZ93I6hoHhukQ5Q=
-google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
-google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
-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.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
-google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
-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=
-google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
-google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+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.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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+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-20180628173108-788fd7840127/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
-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.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.5/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 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
-gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
-honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
-rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
-rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+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=
diff --git a/pkg/api/api.go b/pkg/api/api.go
new file mode 100644
index 00000000..6605a188
--- /dev/null
+++ b/pkg/api/api.go
@@ -0,0 +1,202 @@
+// Package api defines the general API for both coordinator and worker applications.
+//
+// Each API call (request and response) is a JSON-encoded "packet" of the following structure:
+//
+// id - (optional) a globally unique packet id;
+// t - (required) one of the predefined unique packet types;
+// p - (optional) packet payload with arbitrary data.
+//
+// The basic idea behind this API is that the packets differentiate by their predefined types
+// with which it is possible to unwrap the payload into distinct request/response data structures.
+// And the id field is used for tracking packets through a chain of different network points (apps, devices),
+// for example, passing a packet from a browser forward to a worker and back through a coordinator.
+//
+// Example:
+//
+// {"t":4,"p":{"ice":[{"urls":"stun:stun.l.google.com:19302"}],"games":["Sushi The Cat"],"wid":"cfv68irdrc3ifu3jn6bg"}}
+package api
+
+import (
+ "encoding/json"
+ "fmt"
+ "strings"
+)
+
+type (
+ Id interface {
+ String() string
+ }
+ Stateful struct {
+ Id string `json:"id"`
+ }
+ Room struct {
+ Rid string `json:"room_id"`
+ }
+ StatefulRoom struct {
+ Id string `json:"id"`
+ Rid string `json:"room_id"`
+ }
+ PT uint8
+)
+
+type In[I Id] struct {
+ Id I `json:"id,omitempty"`
+ T PT `json:"t"`
+ Payload json.RawMessage `json:"p,omitempty"` // should be json.RawMessage for 2-pass unmarshal
+}
+
+func (i In[I]) GetId() I { return i.Id }
+func (i In[I]) GetPayload() []byte { return i.Payload }
+func (i In[I]) GetType() PT { return i.T }
+
+type Out struct {
+ Id string `json:"id,omitempty"` // string because omitempty won't work as intended with arrays
+ T uint8 `json:"t"`
+ Payload any `json:"p,omitempty"`
+}
+
+func (o *Out) SetId(s string) { o.Id = s }
+func (o *Out) SetType(u uint8) { o.T = u }
+func (o *Out) SetPayload(a any) { o.Payload = a }
+func (o *Out) SetGetId(s fmt.Stringer) { o.Id = s.String() }
+func (o *Out) GetPayload() any { return o.Payload }
+
+// Packet codes:
+//
+// x, 1xx - user codes
+// 15x - webrtc data exchange codes
+// 2xx - worker codes
+const (
+ CheckLatency PT = 3
+ InitSession PT = 4
+ WebrtcInit PT = 100
+ WebrtcOffer PT = 101
+ WebrtcAnswer PT = 102
+ WebrtcIce PT = 103
+ StartGame PT = 104
+ QuitGame PT = 105
+ SaveGame PT = 106
+ LoadGame PT = 107
+ ChangePlayer PT = 108
+ 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 {
+ switch p {
+ case CheckLatency:
+ return "CheckLatency"
+ case InitSession:
+ return "InitSession"
+ case WebrtcInit:
+ return "WebrtcInit"
+ case WebrtcOffer:
+ return "WebrtcOffer"
+ case WebrtcAnswer:
+ return "WebrtcAnswer"
+ case WebrtcIce:
+ return "WebrtcIce"
+ case StartGame:
+ return "StartGame"
+ case ChangePlayer:
+ return "ChangePlayer"
+ case QuitGame:
+ return "QuitGame"
+ case SaveGame:
+ return "SaveGame"
+ case LoadGame:
+ return "LoadGame"
+ 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"
+ }
+}
+
+// Various codes
+const (
+ EMPTY = ""
+ OK = "ok"
+)
+
+var (
+ ErrForbidden = fmt.Errorf("forbidden")
+ ErrMalformed = fmt.Errorf("malformed")
+)
+
+var (
+ EmptyPacket = Out{Payload: ""}
+ ErrPacket = Out{Payload: "err"}
+ 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 {
+ return nil
+ }
+ return out
+}
+
+func UnwrapChecked[T any](bytes []byte, err error) (*T, error) {
+ if err != nil {
+ return nil, err
+ }
+ 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]
+}
diff --git a/pkg/api/coordinator.go b/pkg/api/coordinator.go
new file mode 100644
index 00000000..9cdf22b7
--- /dev/null
+++ b/pkg/api/coordinator.go
@@ -0,0 +1,42 @@
+package api
+
+type (
+ CloseRoomRequest string
+ ConnectionRequest[T Id] struct {
+ Addr string `json:"addr,omitempty"`
+ Id T `json:"id,omitempty"`
+ IsHTTPS bool `json:"is_https,omitempty"`
+ PingURL string `json:"ping_url,omitempty"`
+ Port string `json:"port,omitempty"`
+ Tag string `json:"tag,omitempty"`
+ Zone string `json:"zone,omitempty"`
+ }
+ GetWorkerListResponse struct {
+ Servers []Server `json:"servers"`
+ }
+ RegisterRoomRequest string
+)
+
+const (
+ DataQueryParam = "data"
+ RoomIdQueryParam = "room_id"
+ ZoneQueryParam = "zone"
+ WorkerIdParam = "wid"
+)
+
+// Server contains a list of server groups.
+// Server is a separate machine that may contain
+// multiple sub-processes.
+type Server struct {
+ Addr string `json:"addr,omitempty"`
+ Id Id `json:"id,omitempty"`
+ IsBusy bool `json:"is_busy,omitempty"`
+ InGroup bool `json:"in_group,omitempty"`
+ Machine string `json:"machine,omitempty"`
+ 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"`
+}
diff --git a/pkg/api/user.go b/pkg/api/user.go
new file mode 100644
index 00000000..262375b7
--- /dev/null
+++ b/pkg/api/user.go
@@ -0,0 +1,36 @@
+package api
+
+type (
+ ChangePlayerUserRequest int
+ CheckLatencyUserResponse []string
+ CheckLatencyUserRequest map[string]int64
+ GameStartUserRequest struct {
+ GameName string `json:"game_name"`
+ RoomId string `json:"room_id"`
+ Record bool `json:"record,omitempty"`
+ 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"`
+ Credential string `json:"credential,omitempty"`
+ }
+ InitSessionUserResponse struct {
+ Ice []IceServer `json:"ice"`
+ Games []AppMeta `json:"games"`
+ Wid string `json:"wid"`
+ }
+ AppMeta struct {
+ Alias string `json:"alias,omitempty"`
+ Title string `json:"title"`
+ System string `json:"system"`
+ }
+ WebrtcAnswerUserRequest string
+ WebrtcUserIceCandidate string
+)
diff --git a/pkg/api/worker.go b/pkg/api/worker.go
new file mode 100644
index 00000000..c498009d
--- /dev/null
+++ b/pkg/api/worker.go
@@ -0,0 +1,70 @@
+package api
+
+type (
+ ChangePlayerRequest struct {
+ StatefulRoom
+ Index int `json:"index"`
+ }
+ ChangePlayerResponse int
+ GameQuitRequest StatefulRoom
+ LoadGameRequest StatefulRoom
+ LoadGameResponse string
+ ResetGameRequest StatefulRoom
+ ResetGameResponse string
+ SaveGameRequest StatefulRoom
+ SaveGameResponse string
+ StartGameRequest struct {
+ StatefulRoom
+ Record bool
+ RecordUser string
+ 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"`
+ System string `json:"system"`
+ Type string `json:"type"`
+ }
+ StartGameResponse struct {
+ Room
+ AV *AppVideoInfo `json:"av"`
+ Record bool `json:"record"`
+ KbMouse bool `json:"kb_mouse"`
+ }
+ RecordGameRequest struct {
+ StatefulRoom
+ Active bool `json:"active"`
+ User string `json:"user"`
+ }
+ RecordGameResponse string
+ TerminateSessionRequest Stateful
+ WebrtcAnswerRequest struct {
+ Stateful
+ Sdp string `json:"sdp"`
+ }
+ WebrtcIceCandidateRequest struct {
+ Stateful
+ Candidate string `json:"candidate"` // Base64-encoded ICE candidate
+ }
+ 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
+ }
+)
diff --git a/pkg/codec/codec.go b/pkg/codec/codec.go
deleted file mode 100644
index 1cc33559..00000000
--- a/pkg/codec/codec.go
+++ /dev/null
@@ -1,8 +0,0 @@
-package codec
-
-type VideoCodec string
-
-const (
- H264 VideoCodec = "h264"
- VPX VideoCodec = "vpx"
-)
diff --git a/pkg/com/com.go b/pkg/com/com.go
new file mode 100644
index 00000000..8b475622
--- /dev/null
+++ b/pkg/com/com.go
@@ -0,0 +1,121 @@
+package com
+
+import "github.com/giongto35/cloud-game/v3/pkg/logger"
+
+type stringer interface {
+ comparable
+ String() string
+}
+
+type NetClient[K stringer] interface {
+ Disconnect()
+ Id() K
+}
+
+type NetMap[K stringer, T NetClient[K]] struct{ Map[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)}}
+}
+
+func (m *NetMap[K, T]) Add(client T) bool { return m.Put(client.Id(), client) }
+func (m *NetMap[K, T]) Empty() bool { return m.Map.Len() == 0 }
+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
+ rpc *RPC[T, P]
+ sock *Connection
+ log *logger.Logger // a special logger for showing x -> y directions
+}
+
+func NewConnection[T ~uint8, P Packet[T], X any, P2 Packet2[X]](conn *Connection, id Uid, log *logger.Logger) *SocketClient[T, P, X, P2] {
+ if id.IsNil() {
+ id = NewUid()
+ }
+ dir := logger.MarkOut
+ if conn.IsServer() {
+ dir = logger.MarkIn
+ }
+ dirClLog := log.Extend(log.With().
+ Str("cid", id.Short()).
+ Str(logger.DirectionField, dir),
+ )
+ dirClLog.Debug().Msg("Connect")
+ return &SocketClient[T, P, X, P2]{sock: conn, id: id, log: dirClLog}
+}
+
+func (c *SocketClient[T, P, _, _]) ProcessPackets(fn func(in P) error) chan struct{} {
+ c.rpc = NewRPC[T, P]()
+ c.rpc.Handler = func(p P) {
+ c.log.Debug().Str(logger.DirectionField, logger.MarkIn).Msgf("%v", p.GetType())
+ if err := fn(p); err != nil { // 3rd handler
+ c.log.Error().Err(err).Send()
+ }
+ }
+ c.sock.conn.SetMessageHandler(c.handleMessage) // 1st handler
+ 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()
+ return
+ }
+ if err = c.rpc.handleMessage(message); err != nil { // 2nd handler
+ c.log.Error().Err(err).Send()
+ return
+ }
+}
+
+func (c *SocketClient[_, P, X, P2]) Route(in P, out P2) {
+ rq := P2(new(X))
+ rq.SetId(in.GetId().String())
+ rq.SetType(uint8(in.GetType()))
+ rq.SetPayload(out.GetPayload())
+ if err := c.rpc.Send(c.sock.conn, rq); err != nil {
+ c.log.Error().Err(err).Msgf("message route fail")
+ }
+}
+
+// Send makes a blocking call.
+func (c *SocketClient[T, P, X, P2]) Send(t T, data any) ([]byte, error) {
+ c.log.Debug().Str(logger.DirectionField, logger.MarkOut).Msgf("ᵇ%v", t)
+ rq := P2(new(X))
+ rq.SetType(uint8(t))
+ rq.SetPayload(data)
+ return c.rpc.Call(c.sock.conn, rq)
+}
+
+// Notify just sends a message and goes further.
+func (c *SocketClient[T, P, X, P2]) Notify(t T, data any) {
+ c.log.Debug().Str(logger.DirectionField, logger.MarkOut).Msgf("%v", t)
+ rq := P2(new(X))
+ rq.SetType(uint8(t))
+ rq.SetPayload(data)
+ if err := c.rpc.Send(c.sock.conn, rq); err != nil {
+ c.log.Error().Err(err).Msgf("notify fail")
+ }
+}
+
+func (c *SocketClient[_, _, _, _]) Disconnect() {
+ c.sock.conn.Close()
+ c.rpc.Cleanup()
+ c.log.Debug().Str(logger.DirectionField, logger.MarkCross).Msg("Close")
+}
+
+func (c *SocketClient[_, _, _, _]) Id() Uid { return c.id }
+func (c *SocketClient[_, _, _, _]) String() string { return c.Id().String() }
diff --git a/pkg/com/map.go b/pkg/com/map.go
new file mode 100644
index 00000000..ce2c5cd5
--- /dev/null
+++ b/pkg/com/map.go
@@ -0,0 +1,127 @@
+package com
+
+import (
+ "fmt"
+ "iter"
+ "sync"
+)
+
+// Map defines a concurrent-safe map structure.
+// Keep in mind that the underlying map structure will grow indefinitely.
+type Map[K comparable, V any] struct {
+ m map[K]V
+ mu sync.RWMutex
+}
+
+func (m *Map[K, _]) Len() int {
+ m.mu.RLock()
+ defer m.mu.RUnlock()
+ return len(m.m)
+}
+
+func (m *Map[K, _]) Has(key K) bool {
+ m.mu.RLock()
+ _, ok := m.m[key]
+ m.mu.RUnlock()
+ return ok
+}
+
+// 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.Get(key)
+ return v
+}
+
+func (m *Map[K, V]) String() string {
+ m.mu.RLock()
+ defer m.mu.RUnlock()
+ return fmt.Sprintf("%v", m.m)
+}
+
+// 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()
+
+ 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
+ }
+ }
+ }
+}
diff --git a/pkg/com/map_test.go b/pkg/com/map_test.go
new file mode 100644
index 00000000..15af76c4
--- /dev/null
+++ b/pkg/com/map_test.go
@@ -0,0 +1,63 @@
+package com
+
+import "testing"
+
+func TestMap_Base(t *testing.T) {
+ // map map
+ m := Map[int, int]{m: make(map[int]int)}
+
+ if m.Len() > 0 {
+ t.Errorf("should be empty, %v %v", m.Len(), m.m)
+ }
+ k := 0
+ m.Put(k, 0)
+ if m.Len() == 0 {
+ t.Errorf("should not be empty, %v", m.m)
+ }
+ if !m.Has(k) {
+ t.Errorf("should have the key %v, %v", k, m.m)
+ }
+ 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.Get(k + 1)
+ if ok {
+ t.Errorf("should not find anything, %v %v", ok, m.m)
+ }
+ m.Put(1, 1)
+ v, ok = m.FindBy(func(v int) bool { return v == 1 })
+ if v != 1 && !ok {
+ t.Errorf("should have the key %v and ok, %v %v", 1, ok, m.m)
+ }
+ sum := 0
+ for v := range m.Values() {
+ sum += v
+ }
+ if sum != 1 {
+ t.Errorf("shoud have exact sum of 1, but have %v", sum)
+ }
+ m.Remove(1)
+ if !m.Has(0) || m.Len() > 1 {
+ t.Errorf("should remove only one element, but has %v", m.m)
+ }
+ m.Put(3, 3)
+ v = m.Pop(3)
+ if v != 3 {
+ t.Errorf("should have value %v, but has %v %v", 3, v, m.m)
+ }
+ m.Remove(3)
+ m.Remove(0)
+ if m.Len() != 0 {
+ t.Errorf("should be completely empty, but %v", m.m)
+ }
+}
+
+func TestMap_Concurrency(t *testing.T) {
+ m := Map[int, int]{m: make(map[int]int)}
+ for i := range 100 {
+ go m.Put(i, i)
+ go m.Has(i)
+ go m.Pop(i)
+ }
+}
diff --git a/pkg/com/net.go b/pkg/com/net.go
new file mode 100644
index 00000000..722ce9b5
--- /dev/null
+++ b/pkg/com/net.go
@@ -0,0 +1,178 @@
+package com
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+ "net/url"
+ "time"
+
+ "github.com/giongto35/cloud-game/v3/pkg/network/websocket"
+ "github.com/goccy/go-json"
+ "github.com/rs/xid"
+)
+
+type Uid struct {
+ xid.ID
+}
+
+var NilUid = Uid{xid.NilID()}
+
+func NewUid() Uid { return Uid{xid.New()} }
+
+func UidFromString(id string) (Uid, error) {
+ x, err := xid.FromString(id)
+ if err != nil {
+ return NilUid, err
+ }
+ return Uid{x}, nil
+}
+
+func (u Uid) Short() string { return u.String()[:3] + "." + u.String()[len(u.String())-3:] }
+
+type HasCallId interface {
+ SetGetId(fmt.Stringer)
+}
+
+type Writer interface {
+ Write([]byte)
+}
+
+type Packet[T ~uint8] interface {
+ GetId() Uid
+ GetType() T
+ GetPayload() []byte
+}
+
+type Packet2[T any] interface {
+ SetId(string)
+ SetType(uint8)
+ SetPayload(any)
+ SetGetId(fmt.Stringer)
+ GetPayload() any
+ *T // non-interface type constraint element
+}
+
+type Transport interface {
+ SetMessageHandler(func([]byte, error))
+}
+
+type RPC[T ~uint8, P Packet[T]] struct {
+ CallTimeout time.Duration
+ Handler func(P)
+ Transport Transport
+
+ calls Map[Uid, *request]
+}
+
+type request struct {
+ done chan struct{}
+ err error
+ response []byte
+}
+
+const DefaultCallTimeout = 10 * time.Second
+
+var errCanceled = errors.New("canceled")
+var errTimeout = errors.New("timeout")
+
+type (
+ Client struct {
+ websocket.Client
+ }
+ Server struct {
+ websocket.Server
+ }
+ Connection struct {
+ conn *websocket.Connection
+ }
+)
+
+func (c *Client) Connect(addr url.URL) (*Connection, error) { return connect(c.Client.Connect(addr)) }
+
+func (s *Server) Origin(host string) { s.Upgrader = websocket.NewUpgrader(host) }
+
+func (s *Server) Connect(w http.ResponseWriter, r *http.Request) (*Connection, error) {
+ return connect(s.Server.Connect(w, r, nil))
+}
+
+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 {
+ return nil, err
+ }
+ return &Connection{conn: conn}, nil
+}
+
+func NewRPC[T ~uint8, P Packet[T]]() *RPC[T, P] {
+ return &RPC[T, P]{calls: Map[Uid, *request]{m: make(map[Uid]*request, 10)}}
+}
+
+func (t *RPC[_, _]) Send(w Writer, packet any) error {
+ r, err := json.Marshal(packet)
+ if err != nil {
+ return err
+ }
+ w.Write(r)
+ return nil
+}
+
+func (t *RPC[_, _]) Call(w Writer, rq HasCallId) ([]byte, error) {
+ id := NewUid()
+ // set new request id for the external request structure as string
+ rq.SetGetId(id)
+
+ r, err := json.Marshal(rq)
+ if err != nil {
+ return nil, err
+ }
+ task := &request{done: make(chan struct{})}
+ t.calls.Put(id, task)
+ w.Write(r)
+ select {
+ case <-task.done:
+ case <-time.After(t.callTimeout()):
+ task.err = errTimeout
+ }
+ return task.response, task.err
+}
+
+func (t *RPC[_, P]) handleMessage(message []byte) error {
+ res := *new(P)
+ if err := json.Unmarshal(message, &res); err != nil {
+ return err
+ }
+ // if we have an id, then unblock blocking call with that id
+ id := res.GetId()
+ if id != NilUid {
+ if blocked := t.calls.Pop(id); blocked != nil {
+ blocked.response = res.GetPayload()
+ close(blocked.done)
+ return nil
+ }
+ }
+ if t.Handler != nil {
+ t.Handler(res)
+ }
+ return nil
+}
+
+func (t *RPC[_, _]) callTimeout() time.Duration {
+ if t.CallTimeout > 0 {
+ return t.CallTimeout
+ }
+ return DefaultCallTimeout
+}
+
+func (t *RPC[_, _]) Cleanup() {
+ // drain cancels all what's left in the task queue.
+ for task := range t.calls.Values() {
+ if task.err == nil {
+ task.err = errCanceled
+ }
+ close(task.done)
+ }
+}
diff --git a/pkg/com/net_test.go b/pkg/com/net_test.go
new file mode 100644
index 00000000..2e0a6fc5
--- /dev/null
+++ b/pkg/com/net_test.go
@@ -0,0 +1,225 @@
+package com
+
+import (
+ "encoding/json"
+ "fmt"
+ "math/rand/v2"
+ "net"
+ "net/http"
+ "net/url"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/giongto35/cloud-game/v3/pkg/logger"
+ "github.com/giongto35/cloud-game/v3/pkg/network/websocket"
+)
+
+type TestIn struct {
+ Id Uid
+ T uint8
+ Payload json.RawMessage
+}
+
+func (i TestIn) GetId() Uid { return i.Id }
+func (i TestIn) GetType() uint8 { return i.T }
+func (i TestIn) GetPayload() []byte { return i.Payload }
+
+type TestOut struct {
+ Id string
+ T uint8
+ Payload any
+}
+
+func (o *TestOut) SetId(s string) { o.Id = s }
+func (o *TestOut) SetType(u uint8) { o.T = u }
+func (o *TestOut) SetPayload(a any) { o.Payload = a }
+func (o *TestOut) SetGetId(stringer fmt.Stringer) { o.Id = stringer.String() }
+func (o *TestOut) GetPayload() any { return o.Payload }
+
+func TestWebsocket(t *testing.T) {
+ testCases := []struct {
+ name string
+ test func(t *testing.T)
+ }{
+ {"If WebSocket implementation is OK in general", testWebsocket},
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, tc.test)
+ }
+}
+
+func testWebsocket(t *testing.T) {
+ 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 })
+
+ if server.conn == nil {
+ t.Fatalf("couldn't make new socket")
+ }
+
+ calls := []struct {
+ packet TestOut
+ concurrent bool
+ value any
+ }{
+ {packet: TestOut{T: 10, Payload: "test"}, value: "test", concurrent: true},
+ {packet: TestOut{T: 10, Payload: "test2"}, value: "test2"},
+ {packet: TestOut{T: 11, Payload: "test3"}, value: "test3"},
+ {packet: TestOut{T: 99, Payload: ""}, value: ""},
+ {packet: TestOut{T: 0}},
+ {packet: TestOut{T: 12, Payload: 123}, value: 123},
+ {packet: TestOut{T: 10, Payload: false}, value: false},
+ {packet: TestOut{T: 10, Payload: true}, value: true},
+ {packet: TestOut{T: 11, Payload: []string{"test", "test", "test"}}, value: []string{"test", "test", "test"}},
+ {packet: TestOut{T: 22, Payload: []string{}}, value: []string{}},
+ }
+
+ const n = 42
+ var wait sync.WaitGroup
+ wait.Add(n * len(calls))
+
+ // test
+ for _, call := range calls {
+ if call.concurrent {
+ for range n {
+ packet := call.packet
+ go func() {
+ defer wait.Done()
+ 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 {
+ t.Errorf("%v", err)
+ return
+ }
+ }()
+ }
+ } else {
+ for range n {
+ packet := call.packet
+ vv, err := client.rpc.Call(client.sock.conn, &packet)
+ err = checkCall(vv, err, call.value)
+ if err != nil {
+ wait.Done()
+ t.Fatalf("%v", err)
+ } else {
+ wait.Done()
+ }
+ }
+ }
+ }
+ wait.Wait()
+
+ client.sock.conn.Close()
+ client.rpc.Cleanup()
+ <-clDone
+ server.conn.Close()
+ <-server.done
+}
+
+func newClient(t *testing.T, addr url.URL) *SocketClient[uint8, TestIn, TestOut, *TestOut] {
+ connector := Client{}
+ conn, err := connector.Connect(addr)
+ if err != nil {
+ t.Fatalf("error: couldn't connect to %v because of %v", addr.String(), err)
+ }
+ rpc := new(RPC[uint8, TestIn])
+ rpc.calls = Map[Uid, *request]{m: make(map[Uid]*request, 10)}
+ return &SocketClient[uint8, TestIn, TestOut, *TestOut]{sock: conn, log: logger.Default(), rpc: rpc}
+}
+
+func checkCall(v []byte, err error, need any) error {
+ if err != nil {
+ return err
+ }
+ var value any
+ if v != nil {
+ if err = json.Unmarshal(v, &value); err != nil {
+ return fmt.Errorf("can't unmarshal %v", v)
+ }
+ }
+
+ nice := true
+ // cast values after default unmarshal
+ switch value.(type) {
+ default:
+ nice = value == need
+ case bool:
+ nice = value == need.(bool)
+ case float64:
+ nice = value == float64(need.(int))
+ case string:
+ nice = value == need.(string)
+ case []any:
+ // let's assume that's strings
+ vv := value.([]any)
+ for i := 0; i < len(need.([]string)); i++ {
+ if vv[i].(string) != need.([]string)[i] {
+ nice = false
+ break
+ }
+ }
+ case map[string]any:
+ // ???
+ }
+
+ if !nice {
+ return fmt.Errorf("expected %v, but got %v", need, v)
+ }
+ return nil
+}
+
+type serverHandler struct {
+ conn *websocket.Connection // ws server reference made dynamically on HTTP request
+ done chan struct{}
+}
+
+func (s *serverHandler) serve(t *testing.T) func(w http.ResponseWriter, r *http.Request) {
+ connector := Server{}
+
+ return func(w http.ResponseWriter, r *http.Request) {
+ sock, err := connector.Server.Connect(w, r, nil)
+ if err != nil {
+ t.Fatalf("couldn't init socket server")
+ }
+ s.conn = sock
+ s.conn.SetMessageHandler(func(m []byte, err error) { s.conn.Write(m) }) // echo
+ s.done = s.conn.Listen()
+ }
+}
+
+func newServer(addr string, t *testing.T) *serverHandler {
+ var wg sync.WaitGroup
+ handler := serverHandler{}
+ http.HandleFunc("/ws", handler.serve(t))
+ wg.Add(1)
+ go func() {
+ wg.Done()
+ if err := http.ListenAndServe(addr, nil); err != nil {
+ t.Errorf("no server, %v", err)
+ return
+ }
+ }()
+ 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
+}
diff --git a/pkg/config/config.yaml b/pkg/config/config.yaml
new file mode 100644
index 00000000..c6332696
--- /dev/null
+++ b/pkg/config/config.yaml
@@ -0,0 +1,446 @@
+# The main config file
+
+# Note.
+# Be aware that when this configuration is being overwritten
+# by another configuration, any empty nested part
+# in the further configurations will reset (empty out) all the values.
+# For example:
+# the main config second config result
+# ... ... ...
+# list: list: list:
+# gba: gba: gba:
+# lib: mgba_libretro lib: ""
+# roms: [ "gba", "gbc" ] roms: []
+# ... ...
+#
+# So do not leave empty nested keys.
+
+# 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
+ # - allows selecting worker instances
+ debug: false
+ # selects free workers:
+ # - empty value (default, any free)
+ # - ping (with the lowest ping)
+ selector:
+ monitoring:
+ port: 6601
+ # enable Go profiler HTTP server
+ profilingEnabled: false
+ metricEnabled: false
+ urlPrefix: /coordinator
+ # a custom Origins for incoming Websocket connections:
+ # "" -- checks same origin policy
+ # "*" -- allows all
+ # "your address" -- checks for that address
+ 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:
+ address: :443
+ # allowed host name
+ domain:
+ # if both are set then will use certs
+ # and Letsencryt instead
+ httpsCert:
+ httpsKey:
+ analytics:
+ inject: false
+ gtag:
+
+worker:
+ # show more logs
+ debug: false
+ library:
+ # root folder for the library (where games are stored)
+ basePath: assets/games
+ network:
+ # a coordinator address to connect to
+ coordinatorAddress: localhost:8000
+ # where to connect
+ endpoint: /wso
+ # ping endpoint
+ pingEndpoint: /echo
+ # set public ping address (IP or hostname)
+ publicAddress:
+ # make coordinator connection secure (wss)
+ secure: false
+ # ISO Alpha-2 country code to group workers by zones
+ zone:
+ monitoring:
+ # monitoring server port
+ port: 6602
+ profilingEnabled: false
+ # monitoring server URL prefix
+ metricEnabled: false
+ urlPrefix: /worker
+ server:
+ address: :9000
+ https: false
+ tls:
+ address: :444
+ # LetsEncrypt config
+ # allowed host name
+ domain:
+ # Own certs config
+ httpsCert:
+ httpsKey:
+ # optional server tag
+ tag:
+
+emulator:
+ # set the total number of threads for the image processing
+ # (removed)
+ threads: 0
+
+ # enable autosave for emulator states if set to a non-zero value of seconds
+ autosaveSec: 0
+
+ # save directory for emulator states
+ # special tag {user} will be replaced with current user's home dir
+ storage: "{user}/.cr/save"
+
+ # 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:
+ paths:
+ libs: assets/cores
+ # Config params for Libretro cores repository,
+ # available types are:
+ # - buildbot (the default Libretro nightly repository)
+ # - github (GitHub raw repository with a similar structure to buildbot)
+ # - raw (just a link to a zip file extracted as is)
+ repo:
+ # enable auto-download for the list of cores (list->lib)
+ 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
+ # if repo has file compression
+ compression: zip
+ # a secondary repo to use i.e. for not found in the main cores
+ secondary:
+ type: github
+ url: https://github.com/sergystepanov/libretro-spiegel/raw/main
+ compression: zip
+ # Libretro core configuration
+ #
+ # The emulator selection will happen in this order:
+ # - based on the folder name in the folder param
+ # - based on the folder name (core name) in the list (i.e. nes, snes)
+ # - based on the rom names in the roms param
+ #
+ # Available config params:
+ # - altRepo (bool) prioritize secondary repo as the download source
+ # - lib (string)
+ # - roms ([]string)
+ # - scale (int) scales the output video frames by this factor.
+ # - folder (string)
+ # By default emulator selection is based on the folder named as cores
+ # in the list (i.e. nes, snes) but if you specify folder param,
+ # then it will try to load the ROM file from that folder first.
+ # - width (int) -- broken
+ # - height (int) -- broken
+ # - ratio (float)
+ # - isGlAllowed (bool)
+ # - usesLibCo (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.
+ # By default, we assume that cores output frames at a constant rate which equals
+ # their tick rate (1/system FPS), but OpenGL cores like N64 may have significant
+ # frame rendering time inconsistencies. In general, VFR for CFR cores leads to
+ # 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" ]
+ # example of folder override
+ 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" ]
+ 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" ]
+ isGlAllowed: true
+ usesLibCo: true
+ vfr: true
+ # see: https://github.com/libretro/mupen64plus-libretro-nx/blob/master/libretro/libretro_core_options.h
+ options:
+ "mupen64plus-169screensize": 640x360
+ "mupen64plus-43screensize": 320x240
+ "mupen64plus-EnableCopyColorToRDRAM": Off
+ "mupen64plus-EnableCopyDepthToRDRAM": Off
+ "mupen64plus-EnableEnhancedTextureStorage": True
+ "mupen64plus-EnableFBEmulation": True
+ "mupen64plus-EnableLegacyBlending": True
+ "mupen64plus-FrameDuping": True
+ "mupen64plus-MaxTxCacheSize": 8000
+ "mupen64plus-ThreadedRenderer": False
+ "mupen64plus-cpucore": dynamic_recompiler
+ "mupen64plus-pak1": memory
+ "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) 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: 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
+ profile: baseline
+ # film, animation, grain, stillimage, psnr, ssim, fastdecode, zerolatency
+ tune: zerolatency
+ # 0-3
+ logLevel: 0
+ # see: https://www.webmproject.org/docs/encoder-parameters
+ vpx:
+ # target bitrate (KBit/s)
+ bitrate: 1200
+ # force keyframe interval
+ keyframeInterval: 5
+
+# game recording
+# (experimental)
+# recording allows export RAW a/v streams of games
+# by default, it will export audio as WAV files,
+# video as a list of PNG-encoded images, and
+# one additional FFMPEG concat demux file
+recording:
+ enabled: false
+ # name contains the name of the recording dir (or zip)
+ # format:
+ # %date:go_time_format% -- refer: https://go.dev/src/time/format.go
+ # %user% -- user name who started the recording
+ # %game% -- game name (game ROM name)
+ # %rand:len% -- a random string of given length
+ # as example: 20210101101010_yeE_user1_badApple
+ name: "%date:20060102150405%_%rand:3%_%user%_%game%"
+ # zip and remove recording dir on completion
+ zip: true
+ # 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)
+ # - s3 (S3 API compatible object storage)
+ provider:
+ s3Endpoint:
+ s3BucketName:
+ s3AccessKeyId:
+ s3SecretAccessKey:
+
+webrtc:
+ # turn off default Pion interceptors (see: https://github.com/pion/interceptor)
+ # (performance)
+ disableDefaultInterceptors: false
+ # indicates the role of the DTLS transport (see: https://github.com/pion/webrtc/blob/master/dtlsrole.go)
+ # (debug)
+ # - (default)
+ # - 1 (auto)
+ # - 2 (client)
+ # - 3 (server)
+ dtlsRole:
+ # a list of STUN/TURN servers to use
+ iceServers:
+ - urls: stun:stun.l.google.com:19302
+ # configures whether the ice agent should be a lite agent (true/false)
+ # (performance)
+ # don't use iceServers when enabled
+ iceLite: false
+ # ICE configuration
+ # by default, ICE ports are random and unlimited
+ # alternatives:
+ # 1. instead of random unlimited port range for
+ # WebRTC connections, these params limit port range of ICE connections
+ icePorts:
+ min:
+ max:
+ # 2. select a single port to forward all ICE connections there
+ singlePort:
+ # override ICE candidate IP, see: https://github.com/pion/webrtc/issues/835,
+ # can be used for Docker bridged network internal IP override
+ iceIpMap:
+ # set additional log level for WebRTC separately
+ # -1 - trace, 6 - nothing, ..., debug - 0
+ logLevel: 6
diff --git a/pkg/config/coordinator.go b/pkg/config/coordinator.go
new file mode 100644
index 00000000..6a41cce0
--- /dev/null
+++ b/pkg/config/coordinator.go
@@ -0,0 +1,52 @@
+package config
+
+import "flag"
+
+type CoordinatorConfig struct {
+ Coordinator Coordinator
+ Emulator Emulator
+ Library Library
+ Recording Recording
+ Version Version
+ Webrtc Webrtc
+}
+
+type Coordinator struct {
+ Analytics Analytics
+ Debug bool
+ Library Library
+ MaxWsSize int64
+ Monitoring Monitoring
+ Origin struct {
+ UserWs string
+ WorkerWs string
+ }
+ Selector string
+ Server Server
+}
+
+// Analytics is optional Google Analytics
+type Analytics struct {
+ Inject bool
+ Gtag string
+}
+
+const SelectByPing = "ping"
+
+// allows custom config path
+var coordinatorConfigPath string
+
+func NewCoordinatorConfig() (conf CoordinatorConfig, paths []string) {
+ paths, err := LoadConfig(&conf, coordinatorConfigPath)
+ if err != nil {
+ panic(err)
+ }
+ return
+}
+
+func (c *CoordinatorConfig) ParseFlags() {
+ c.Coordinator.Server.WithFlags()
+ flag.IntVar(&c.Coordinator.Monitoring.Port, "monitoring.port", c.Coordinator.Monitoring.Port, "Monitoring server port")
+ flag.StringVar(&coordinatorConfigPath, "c-conf", coordinatorConfigPath, "Set custom configuration file path")
+ flag.Parse()
+}
diff --git a/pkg/config/coordinator/config.go b/pkg/config/coordinator/config.go
deleted file mode 100644
index 80eb12c8..00000000
--- a/pkg/config/coordinator/config.go
+++ /dev/null
@@ -1,49 +0,0 @@
-package coordinator
-
-import (
- "github.com/giongto35/cloud-game/v2/pkg/config"
- "github.com/giongto35/cloud-game/v2/pkg/config/emulator"
- "github.com/giongto35/cloud-game/v2/pkg/config/monitoring"
- "github.com/giongto35/cloud-game/v2/pkg/config/shared"
- webrtcConfig "github.com/giongto35/cloud-game/v2/pkg/config/webrtc"
- "github.com/giongto35/cloud-game/v2/pkg/games"
- flag "github.com/spf13/pflag"
-)
-
-type Config struct {
- Coordinator struct {
- DebugHost string
- Library games.Config
- Monitoring monitoring.Config
- Server shared.Server
- Analytics Analytics
- }
- Emulator emulator.Emulator
- Environment shared.Environment
- Webrtc webrtcConfig.Webrtc
-}
-
-// Analytics is optional Google Analytics
-type Analytics struct {
- Inject bool
- Gtag string
-}
-
-// allows custom config path
-var configPath string
-
-func NewConfig() (conf Config) {
- err := config.LoadConfig(&conf, configPath)
- if err != nil {
- panic(err)
- }
- return
-}
-
-func (c *Config) ParseFlags() {
- c.Environment.WithFlags()
- c.Coordinator.Server.WithFlags()
- flag.IntVar(&c.Coordinator.Monitoring.Port, "monitoring.port", c.Coordinator.Monitoring.Port, "Monitoring server port")
- flag.StringVarP(&configPath, "conf", "c", configPath, "Set custom configuration file path")
- flag.Parse()
-}
diff --git a/pkg/config/emulator.go b/pkg/config/emulator.go
new file mode 100644
index 00000000..6a0ad9bb
--- /dev/null
+++ b/pkg/config/emulator.go
@@ -0,0 +1,154 @@
+package config
+
+import (
+ "errors"
+ "path"
+ "path/filepath"
+ "runtime"
+ "strings"
+)
+
+type Emulator struct {
+ FailFast bool
+ Threads int
+ Storage string
+ LocalPath string
+ Libretro LibretroConfig
+ AutosaveSec int
+ SkipLateFrames bool
+ LogDroppedFrames bool
+}
+
+type LibretroConfig struct {
+ Cores struct {
+ Paths struct {
+ Libs string
+ }
+ 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
+ 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 {
+ Id string
+ Name string
+ AltRepo bool
+}
+
+// GetLibretroCoreConfig returns a core config with expanded paths.
+func (e Emulator) GetLibretroCoreConfig(emulator string) LibretroCoreConfig {
+ cores := e.Libretro.Cores
+ conf := cores.List[emulator]
+ conf.Lib = path.Join(cores.Paths.Libs, conf.Lib)
+ return conf
+}
+
+// GetEmulator tries to find a suitable emulator.
+// !to remove quadratic complexity
+func (e Emulator) GetEmulator(rom string, path string) string {
+ found := ""
+ for emu, core := range e.Libretro.Cores.List {
+ for _, romName := range core.Roms {
+ if rom == romName {
+ found = emu
+ if p := strings.SplitN(filepath.ToSlash(path), "/", 2); len(p) > 1 {
+ folder := p[0]
+ if (folder != "" && folder == core.Folder) || folder == emu {
+ return emu
+ }
+ }
+ }
+ }
+ }
+ return found
+}
+
+func (e Emulator) GetSupportedExtensions() []string {
+ var extensions []string
+ for _, core := range e.Libretro.Cores.List {
+ extensions = append(extensions, core.Roms...)
+ }
+ 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})
+ }
+ return
+}
+
+func (l *LibretroConfig) GetCoresStorePath() string {
+ pth, err := filepath.Abs(l.Cores.Paths.Libs)
+ if err != nil {
+ return ""
+ }
+ return pth
+}
diff --git a/pkg/config/emulator/config.go b/pkg/config/emulator/config.go
deleted file mode 100644
index 35cf870e..00000000
--- a/pkg/config/emulator/config.go
+++ /dev/null
@@ -1,111 +0,0 @@
-package emulator
-
-import (
- "path"
- "path/filepath"
- "strings"
-)
-
-type Emulator struct {
- Scale int
- AspectRatio struct {
- Keep bool
- Width int
- Height int
- }
- Storage string
- Libretro LibretroConfig
-}
-
-type LibretroConfig struct {
- Cores struct {
- Paths struct {
- Libs string
- Configs string
- }
- Repo struct {
- Sync bool
- ExtLock string
- Main LibretroRepoConfig
- Secondary LibretroRepoConfig
- }
- List map[string]LibretroCoreConfig
- }
-}
-
-type LibretroRepoConfig struct {
- Type string
- Url string
- Compression string
-}
-
-type LibretroCoreConfig struct {
- Lib string
- Config string
- Roms []string
- Folder string
- Width int
- Height int
- Ratio float64
- IsGlAllowed bool
- UsesLibCo bool
- HasMultitap bool
-
- // hack: keep it here to pass it down the emulator
- AutoGlContext bool
-}
-
-// GetLibretroCoreConfig returns a core config with expanded paths.
-func (e Emulator) GetLibretroCoreConfig(emulator string) LibretroCoreConfig {
- cores := e.Libretro.Cores
- conf := cores.List[emulator]
- conf.Lib = path.Join(cores.Paths.Libs, conf.Lib)
- if conf.Config != "" {
- conf.Config = path.Join(cores.Paths.Configs, conf.Config)
- }
- return conf
-}
-
-// GetEmulator tries to find a suitable emulator.
-// !to remove quadratic complexity
-func (e Emulator) GetEmulator(rom string, path string) string {
- found := ""
- for emu, core := range e.Libretro.Cores.List {
- for _, romName := range core.Roms {
- if rom == romName {
- found = emu
- if p := strings.SplitN(filepath.ToSlash(path), "/", 2); len(p) > 1 {
- folder := p[0]
- if (folder != "" && folder == core.Folder) || folder == emu {
- return emu
- }
- }
- }
- }
- }
- return found
-}
-
-func (e Emulator) GetSupportedExtensions() []string {
- var extensions []string
- for _, core := range e.Libretro.Cores.List {
- extensions = append(extensions, core.Roms...)
- }
- return extensions
-}
-
-func (l *LibretroConfig) GetCores() []string {
- var cores []string
- for _, core := range l.Cores.List {
- cores = append(cores, core.Lib)
- }
- return cores
-}
-
-func (l *LibretroConfig) GetCoresStorePath() string {
- pth, err := filepath.Abs(l.Cores.Paths.Libs)
- if err != nil {
- return ""
- }
- return pth
-}
diff --git a/pkg/config/emulator/config_test.go b/pkg/config/emulator_test.go
similarity index 87%
rename from pkg/config/emulator/config_test.go
rename to pkg/config/emulator_test.go
index ebef77c2..09afd2d4 100644
--- a/pkg/config/emulator/config_test.go
+++ b/pkg/config/emulator_test.go
@@ -1,4 +1,4 @@
-package emulator
+package config
import "testing"
@@ -29,20 +29,17 @@ func TestGetEmulator(t *testing.T) {
},
{
rom: "nes",
- path: "test/game.nes",
+ path: "test2/game.nes",
config: map[string]LibretroCoreConfig{
- "snes": {Roms: []string{"nes"}},
+ "snes": {Roms: []string{"snes"}},
"nes": {Roms: []string{"nes"}},
},
emulator: "nes",
},
}
- emu := Emulator{
- Libretro: LibretroConfig{},
- }
-
for _, test := range tests {
+ emu := Emulator{Libretro: LibretroConfig{}}
emu.Libretro.Cores.List = test.config
em := emu.GetEmulator(test.rom, test.path)
if test.emulator != em {
diff --git a/pkg/config/encoder/config.go b/pkg/config/encoder/config.go
deleted file mode 100644
index 3a782597..00000000
--- a/pkg/config/encoder/config.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package encoder
-
-type Encoder struct {
- Audio Audio
- Video Video
- WithoutGame bool
-}
-
-type Audio struct {
- Channels int
- Frame int
- Frequency int
-}
-
-type Video struct {
- Codec string
- H264 struct {
- Crf uint8
- Preset string
- Profile string
- Tune string
- LogLevel int
- }
- Vpx struct {
- Bitrate uint
- KeyframeInterval uint
- }
-}
-
-func (a *Audio) GetFrameDuration() int {
- return a.Frequency * a.Frame / 1000 * a.Channels
-}
diff --git a/pkg/config/loader.go b/pkg/config/loader.go
index 0d665222..a2fb6bd8 100644
--- a/pkg/config/loader.go
+++ b/pkg/config/loader.go
@@ -1,26 +1,163 @@
package config
import (
+ "bytes"
+ "embed"
"os"
+ "path/filepath"
+ "strings"
- "github.com/kkyr/fig"
+ "github.com/knadh/koanf/maps"
+ "github.com/knadh/koanf/v2"
+ "gopkg.in/yaml.v3"
)
+const EnvPrefix = "CLOUD_GAME_"
+
+var (
+ //go:embed config.yaml
+ conf embed.FS
+)
+
+type Kv = map[string]any
+type Bytes []byte
+
+func (b *Bytes) ReadBytes() ([]byte, error) { return *b, nil }
+func (b *Bytes) Read() (Kv, error) { return nil, nil }
+
+type File string
+
+func (f *File) ReadBytes() ([]byte, error) { return os.ReadFile(string(*f)) }
+func (f *File) Read() (Kv, error) { return nil, nil }
+
+type YAML struct{}
+
+func (p *YAML) Marshal(Kv) ([]byte, error) { return nil, nil }
+func (p *YAML) Unmarshal(b []byte) (Kv, error) {
+ var out Kv
+ klw := keysToLower(b)
+ decoder := yaml.NewDecoder(bytes.NewReader(klw))
+ if err := decoder.Decode(&out); err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// keysToLower iterates YAML bytes and tries to lower the keys.
+// Used for merging with environment vars which are lowered as well.
+func keysToLower(in []byte) []byte {
+ l, r, ignore := 0, 0, false
+ for i, b := range in {
+ switch b {
+ case '#': // skip comments
+ ignore = true
+ case ':': // lower left chunk before the next : symbol
+ if ignore {
+ continue
+ }
+ r = i
+ ignore = true
+ for j := l; j <= r; j++ {
+ c := in[j]
+ // we skip the line with the first explicit " string symbol
+ if c == '"' {
+ break
+ }
+ if 'A' <= c && c <= 'Z' {
+ in[j] += 'a' - 'A'
+ }
+ }
+ case '\n':
+ l = i
+ ignore = false
+ }
+ }
+ return in
+}
+
+type Env string
+
+func (e *Env) ReadBytes() ([]byte, error) { return nil, nil }
+func (e *Env) Read() (Kv, error) {
+ var keys []string
+ for _, k := range os.Environ() {
+ if strings.HasPrefix(k, string(*e)) {
+ keys = append(keys, k)
+ }
+ }
+ 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
+ }
+ // convert VAR_VAR to VAR.VAR or if we need to preserve _
+ // i.e. VAR_VAR__KEY_HAS_SLASHES to VAR.VAR.KEY_HAS_SLASHES
+ // with the result: VAR: { VAR: { KEY_HAS_SLASHES: '' } } }
+ x := strings.Index(n, "__")
+ var key string
+ if x == -1 {
+ key = strings.Replace(n, "_", ".", -1)
+ } else {
+ key = strings.Replace(n[:x+1], "_", ".", -1) + n[x+2:]
+ }
+ if len(parts) > 1 {
+ mp[key] = parts[1]
+ }
+ }
+ return maps.Unflatten(mp, "."), nil
+}
+
// LoadConfig loads a configuration file into the given struct.
// The path param specifies a custom path to the configuration file.
// Reads and puts environment variables with the prefix CLOUD_GAME_.
-// Params from the config should be in uppercase separated with _.
-func LoadConfig(config interface{}, path string) error {
- envPrefix := "CLOUD_GAME"
- dirs := []string{path}
- if path == "" {
- dirs = append(dirs, ".", "configs", "../../../configs")
- if home, err := os.UserHomeDir(); err == nil {
- dirs = append(dirs, home+"/.cr")
+func LoadConfig(config any, path string) (loaded []string, err error) {
+ dirs := []string{".", "configs", "../../../configs"}
+ if path != "" {
+ dirs = append([]string{path}, dirs...)
+ }
+
+ homeDir := ""
+ if home, err := os.UserHomeDir(); err == nil {
+ homeDir = home + "/.cr"
+ dirs = append(dirs, homeDir)
+ }
+
+ k := koanf.New("_") // move to global scope if configs become dynamic
+ defer k.Delete("")
+ data, err := conf.ReadFile("config.yaml")
+ if err != nil {
+ return nil, err
+ }
+ conf := Bytes(data)
+ if err := k.Load(&conf, &YAML{}); err != nil {
+ return nil, err
+ }
+ loaded = append(loaded, "default")
+
+ for _, dir := range dirs {
+ path := filepath.Join(filepath.Clean(dir), "config.yaml")
+ f := File(path)
+ if _, err := os.Stat(string(f)); !os.IsNotExist(err) {
+ if err := k.Load(&f, &YAML{}); err != nil {
+ return loaded, err
+ }
+ loaded = append(loaded, path)
}
}
- if err := fig.Load(config, fig.Dirs(dirs...), fig.UseEnv(envPrefix)); err != nil {
- return err
+
+ env := Env(EnvPrefix)
+ if err := k.Load(&env, nil); err != nil {
+ return loaded, err
}
- return nil
+
+ if err := k.Unmarshal("", config); err != nil {
+ return loaded, err
+ }
+
+ return loaded, nil
}
diff --git a/pkg/config/loader_test.go b/pkg/config/loader_test.go
new file mode 100644
index 00000000..08e17dd3
--- /dev/null
+++ b/pkg/config/loader_test.go
@@ -0,0 +1,63 @@
+package config
+
+import (
+ "os"
+ "reflect"
+ "testing"
+)
+
+func TestConfigEnv(t *testing.T) {
+ var out WorkerConfig
+
+ _ = 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() {
+ _ = os.Unsetenv("CLOUD_GAME_EMULATOR_LIBRETRO_CORES_LIST_PCSX_OPTIONS__PCSX_REARMED_DRC")
+ }()
+
+ _, err := LoadConfig(&out, "")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ 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"]
+ if v != "x" {
+ t.Errorf("%v is not x", v)
+ }
+}
+
+func Test_keysToLower(t *testing.T) {
+ type args struct {
+ in []byte
+ }
+ tests := []struct {
+ name string
+ args args
+ want []byte
+ }{
+ {name: "empty", args: args{in: []byte{}}, want: []byte{}},
+ {name: "case", args: args{
+ in: []byte("KEY:1\n#Comment with:\n KeY123_NamE: 1\n\n\n\nAAA:123\n \"KeyKey\":2\n"),
+ },
+ want: []byte("key:1\n#Comment with:\n key123_name: 1\n\n\n\naaa:123\n \"KeyKey\":2\n"),
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := keysToLower(tt.args.in); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("keysToLower() = %v, want %v", string(got), string(tt.want))
+ }
+ })
+ }
+}
diff --git a/pkg/config/monitoring/config.go b/pkg/config/monitoring/config.go
deleted file mode 100644
index da89ead0..00000000
--- a/pkg/config/monitoring/config.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package monitoring
-
-type Config struct {
- Port int
- URLPrefix string
- MetricEnabled bool `json:"metric_enabled"`
- ProfilingEnabled bool `json:"profiling_enabled"`
-}
-
-func (c *Config) IsEnabled() bool { return c.MetricEnabled || c.ProfilingEnabled }
diff --git a/pkg/config/shared.go b/pkg/config/shared.go
new file mode 100644
index 00000000..e49eb3ce
--- /dev/null
+++ b/pkg/config/shared.go
@@ -0,0 +1,66 @@
+package config
+
+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
+ // a list of supported file extensions
+ Supported []string
+ // a list of ignored words in the files
+ Ignored []string
+ // print some additional info
+ Verbose bool
+ // enable directory changes watch
+ WatchMode bool
+}
+
+func (l Library) GetSupportedExtensions() []string { return l.Supported }
+
+type Monitoring struct {
+ Port int
+ URLPrefix string
+ MetricEnabled bool `json:"metric_enabled"`
+ ProfilingEnabled bool `json:"profiling_enabled"`
+}
+
+func (c *Monitoring) IsEnabled() bool { return c.MetricEnabled || c.ProfilingEnabled }
+
+type Server struct {
+ Address string
+ CacheControl string
+ FrameOptions string
+ Https bool
+ Tls struct {
+ Address string
+ Domain string
+ HttpsKey string
+ HttpsCert string
+ }
+}
+
+type Recording struct {
+ Enabled bool
+ Name string
+ Folder string
+ Zip bool
+}
+
+func (s *Server) WithFlags() {
+ flag.StringVar(&s.Address, "address", s.Address, "HTTP server address (host:port)")
+ flag.StringVar(&s.Tls.Address, "httpsAddress", s.Tls.Address, "HTTPS server address (host:port)")
+ flag.StringVar(&s.Tls.HttpsKey, "httpsKey", s.Tls.HttpsKey, "HTTPS key")
+ flag.StringVar(&s.Tls.HttpsCert, "httpsCert", s.Tls.HttpsCert, "HTTPS chain")
+}
+
+func (s *Server) GetAddr() string {
+ if s.Https {
+ return s.Tls.Address
+ }
+ return s.Address
+}
diff --git a/pkg/config/shared/config.go b/pkg/config/shared/config.go
deleted file mode 100644
index 129f71bb..00000000
--- a/pkg/config/shared/config.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package shared
-
-import (
- "github.com/giongto35/cloud-game/v2/pkg/environment"
- flag "github.com/spf13/pflag"
-)
-
-type Environment environment.Env
-
-type Server struct {
- Address string
- Https bool
- Tls struct {
- Address string
- Domain string
- HttpsKey string
- HttpsCert string
- }
-}
-
-func (s *Server) WithFlags() {
- flag.StringVar(&s.Address, "address", s.Address, "HTTP server address (host:port)")
- flag.StringVar(&s.Tls.Address, "httpsAddress", s.Tls.Address, "HTTPS server address (host:port)")
- flag.StringVar(&s.Tls.HttpsKey, "httpsKey", s.Tls.HttpsKey, "HTTPS key")
- flag.StringVar(&s.Tls.HttpsCert, "httpsCert", s.Tls.HttpsCert, "HTTPS chain")
-}
-
-func (s *Server) GetAddr() string {
- if s.Https {
- return s.Tls.Address
- }
- return s.Address
-}
-
-func (env *Environment) Get() environment.Env {
- return (environment.Env)(*env)
-}
-
-func (env *Environment) WithFlags() {
- flag.StringVar((*string)(env), "env", string(*env), "Specify environment type: [dev, staging, prod]")
-}
diff --git a/pkg/config/webrtc.go b/pkg/config/webrtc.go
new file mode 100644
index 00000000..bc0b8e14
--- /dev/null
+++ b/pkg/config/webrtc.go
@@ -0,0 +1,26 @@
+package config
+
+type Webrtc struct {
+ DisableDefaultInterceptors bool
+ DtlsRole byte
+ IceServers []IceServer
+ IcePorts struct {
+ Min uint16
+ Max uint16
+ }
+ IceIpMap string
+ IceLite bool
+ SinglePort int
+ LogLevel int
+}
+
+type IceServer struct {
+ Urls string `json:"urls,omitempty"`
+ Username string `json:"username,omitempty"`
+ Credential string `json:"credential,omitempty"`
+}
+
+func (w *Webrtc) HasDtlsRole() bool { return w.DtlsRole > 0 }
+func (w *Webrtc) HasPortRange() bool { return w.IcePorts.Min > 0 && w.IcePorts.Max > 0 }
+func (w *Webrtc) HasSinglePort() bool { return w.SinglePort > 0 }
+func (w *Webrtc) HasIceIpMap() bool { return w.IceIpMap != "" }
diff --git a/pkg/config/webrtc/config.go b/pkg/config/webrtc/config.go
deleted file mode 100644
index a0daeb13..00000000
--- a/pkg/config/webrtc/config.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package webrtc
-
-import "github.com/giongto35/cloud-game/v2/pkg/config/encoder"
-
-type Webrtc struct {
- DisableDefaultInterceptors bool
- IceServers []IceServer
- IcePorts struct {
- Min uint16
- Max uint16
- }
- IceIpMap string
-}
-
-type IceServer struct {
- Url string
- Username string
- Credential string
-}
-
-type Config struct {
- Encoder encoder.Encoder
- Webrtc Webrtc
-}
diff --git a/pkg/config/worker/config.go b/pkg/config/worker.go
similarity index 52%
rename from pkg/config/worker/config.go
rename to pkg/config/worker.go
index ccd5ea10..014ce644 100644
--- a/pkg/config/worker/config.go
+++ b/pkg/config/worker.go
@@ -1,31 +1,38 @@
-package worker
+package config
import (
- "log"
+ "flag"
+ "fmt"
"net"
"net/url"
+ "path/filepath"
"strings"
- "github.com/giongto35/cloud-game/v2/pkg/config"
- "github.com/giongto35/cloud-game/v2/pkg/config/emulator"
- "github.com/giongto35/cloud-game/v2/pkg/config/encoder"
- "github.com/giongto35/cloud-game/v2/pkg/config/monitoring"
- "github.com/giongto35/cloud-game/v2/pkg/config/shared"
- webrtcConfig "github.com/giongto35/cloud-game/v2/pkg/config/webrtc"
- "github.com/giongto35/cloud-game/v2/pkg/environment"
- flag "github.com/spf13/pflag"
+ "github.com/giongto35/cloud-game/v3/pkg/os"
)
-type Config struct {
- Encoder encoder.Encoder
- Emulator emulator.Emulator
- Environment shared.Environment
- Worker Worker
- Webrtc webrtcConfig.Webrtc
+type WorkerConfig struct {
+ Encoder Encoder
+ Emulator Emulator
+ Library Library
+ Recording Recording
+ Storage Storage
+ Worker Worker
+ Webrtc Webrtc
+ Version Version
+}
+
+type Storage struct {
+ Provider string
+ S3Endpoint string
+ S3BucketName string
+ S3AccessKeyId string
+ S3SecretAccessKey string
}
type Worker struct {
- Monitoring monitoring.Config
+ Debug bool
+ Monitoring Monitoring
Network struct {
CoordinatorAddress string
Endpoint string
@@ -34,43 +41,85 @@ type Worker struct {
Secure bool
Zone string
}
- Server shared.Server
+ Server Server
+ Tag string
+}
+
+type Encoder struct {
+ Audio Audio
+ Video Video
+}
+
+type Audio struct {
+ Frames []float32
+ Resampler int
+}
+
+type Video struct {
+ Codec string
+ Threads int
+ H264 struct {
+ Mode string
+ Crf uint8
+ MaxRate int
+ BufSize int
+ LogLevel int32
+ Preset string
+ Profile string
+ Tune string
+ }
+ Vpx struct {
+ Bitrate uint
+ KeyframeInterval uint
+ }
}
// allows custom config path
-var configPath string
+var workerConfigPath string
-func NewConfig() (conf Config) {
- _ = config.LoadConfig(&conf, configPath)
+func NewWorkerConfig() (conf WorkerConfig, paths []string) {
+ paths, err := LoadConfig(&conf, workerConfigPath)
+ if err != nil {
+ panic(err)
+ }
conf.expandSpecialTags()
+ conf.fixValues()
return
}
// ParseFlags updates config values from passed runtime flags.
// Define own flags with default value set to the current config param.
// Don't forget to call flag.Parse().
-func (c *Config) ParseFlags() {
- c.Environment.WithFlags()
+func (c *WorkerConfig) ParseFlags() {
c.Worker.Server.WithFlags()
flag.IntVar(&c.Worker.Monitoring.Port, "monitoring.port", c.Worker.Monitoring.Port, "Monitoring server port")
flag.StringVar(&c.Worker.Network.CoordinatorAddress, "coordinatorhost", c.Worker.Network.CoordinatorAddress, "Worker URL to connect")
flag.StringVar(&c.Worker.Network.Zone, "zone", c.Worker.Network.Zone, "Worker network zone (us, eu, etc.)")
- flag.StringVarP(&configPath, "conf", "c", configPath, "Set custom configuration file path")
+ flag.StringVar(&workerConfigPath, "w-conf", workerConfigPath, "Set custom configuration file path")
flag.Parse()
}
// expandSpecialTags replaces all the special tags in the config.
-func (c *Config) expandSpecialTags() {
+func (c *WorkerConfig) expandSpecialTags() {
tag := "{user}"
for _, dir := range []*string{&c.Emulator.Storage, &c.Emulator.Libretro.Cores.Repo.ExtLock} {
if *dir == "" || !strings.Contains(*dir, tag) {
continue
}
- userHomeDir, err := environment.GetUserHome()
+ userHomeDir, err := os.GetUserHome()
if err != nil {
- log.Fatalln("couldn't read user home directory", err)
+ panic(fmt.Sprintf("couldn't read user home directory, %v", err))
}
*dir = strings.Replace(*dir, tag, userHomeDir, -1)
+ *dir = filepath.FromSlash(*dir)
+ }
+}
+
+// fixValues tries to fix some values otherwise hard to set externally.
+func (c *WorkerConfig) fixValues() {
+ // with ICE lite we clear ICE servers
+ if c.Webrtc.IceLite {
+ c.Webrtc.IceServers = []IceServer{}
}
}
@@ -78,7 +127,7 @@ func (c *Config) expandSpecialTags() {
func (w *Worker) GetAddr() string { return w.Server.GetAddr() }
// GetPingAddr returns exposed to clients server ping endpoint address.
-func (w *Worker) GetPingAddr(address string) string {
+func (w *Worker) GetPingAddr(address string) url.URL {
_, srcPort, _ := net.SplitHostPort(w.GetAddr())
dstHost, _, _ := net.SplitHostPort(address)
address = net.JoinHostPort(dstHost, srcPort)
@@ -98,5 +147,10 @@ func (w *Worker) GetPingAddr(address string) string {
if w.Server.Https {
pingURL.Scheme = "https"
}
- return pingURL.String()
+ return pingURL
+}
+
+func (w *Worker) GetPort(address string) string {
+ _, port, _ := net.SplitHostPort(address)
+ return port
}
diff --git a/pkg/coordinator/browser.go b/pkg/coordinator/browser.go
deleted file mode 100644
index 41ad610f..00000000
--- a/pkg/coordinator/browser.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package coordinator
-
-import (
- "fmt"
- "log"
-
- "github.com/giongto35/cloud-game/v2/pkg/cws"
- "github.com/gorilla/websocket"
-)
-
-type BrowserClient struct {
- *cws.Client
- SessionID string
- RoomID string
- WorkerID string // TODO: how about pointer to workerClient?
-}
-
-// NewCoordinatorClient returns a client connecting to browser.
-// This connection exchanges information between browser and coordinator.
-func NewBrowserClient(c *websocket.Conn, browserID string) *BrowserClient {
- return &BrowserClient{
- Client: cws.NewClient(c),
- SessionID: browserID,
- }
-}
-
-// Register new log
-func (bc *BrowserClient) Printf(format string, args ...interface{}) {
- log.Printf(fmt.Sprintf("Browser %s] %s", bc.SessionID, format), args...)
-}
-
-func (bc *BrowserClient) Println(args ...interface{}) {
- log.Println(fmt.Sprintf("Browser %s] %s", bc.SessionID, fmt.Sprint(args...)))
-}
diff --git a/pkg/coordinator/coordinator.go b/pkg/coordinator/coordinator.go
index 05ccd265..ffc5c7de 100644
--- a/pkg/coordinator/coordinator.go
+++ b/pkg/coordinator/coordinator.go
@@ -1,27 +1,119 @@
package coordinator
import (
- "log"
+ "errors"
+ "fmt"
+ "html/template"
"net/http"
+ "strings"
- "github.com/giongto35/cloud-game/v2/pkg/config/coordinator"
- "github.com/giongto35/cloud-game/v2/pkg/games"
- "github.com/giongto35/cloud-game/v2/pkg/monitoring"
- "github.com/giongto35/cloud-game/v2/pkg/service"
+ "github.com/giongto35/cloud-game/v3/pkg/config"
+ "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"
)
-func New(conf coordinator.Config) (services service.Group) {
- srv := NewServer(conf, games.NewLibWhitelisted(conf.Coordinator.Library, conf.Emulator))
- httpSrv, err := NewHTTPServer(conf, func(mux *http.ServeMux) {
- mux.HandleFunc("/ws", srv.WS)
- mux.HandleFunc("/wso", srv.WSO)
+type Coordinator struct {
+ hub *Hub
+ services [2]interface {
+ Run()
+ Stop() error
+ }
+}
+
+func New(conf config.CoordinatorConfig, log *logger.Logger) (*Coordinator, error) {
+ 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())
+ return mux
})
if err != nil {
- log.Fatalf("http init fail: %v", err)
+ return nil, fmt.Errorf("http init fail: %w", err)
}
- services.Add(srv, httpSrv)
+ coordinator.services[0] = h
if conf.Coordinator.Monitoring.IsEnabled() {
- services.Add(monitoring.New(conf.Coordinator.Monitoring, httpSrv.GetHost(), "cord"))
+ coordinator.services[1] = monitoring.New(conf.Coordinator.Monitoring, h.GetHost(), log)
}
- return
+ return coordinator, nil
+}
+
+func (c *Coordinator) Start() {
+ for _, s := range c.services {
+ if s != nil {
+ s.Run()
+ }
+ }
+}
+
+func (c *Coordinator) Stop() error {
+ var err error
+ for _, s := range c.services {
+ if s != nil {
+ err0 := s.Stop()
+ err = errors.Join(err, err0)
+ }
+ }
+ return err
+}
+
+func NewHTTPServer(conf config.CoordinatorConfig, log *logger.Logger, fnMux func(*httpx.Mux) *httpx.Mux) (*httpx.Server, error) {
+ return httpx.NewServer(
+ conf.Coordinator.Server.GetAddr(),
+ func(s *httpx.Server) httpx.Handler { return fnMux(s.Mux().Handle("/", index(conf, log))) },
+ httpx.WithServerConfig(conf.Coordinator.Server),
+ httpx.WithLogger(log),
+ )
+}
+
+func index(conf config.CoordinatorConfig, log *logger.Logger) httpx.Handler {
+ const indexHTML = "./web/index.html"
+
+ indexTpl := template.Must(template.ParseFiles(indexHTML))
+
+ // render index page with some tpl values
+ tplData := struct {
+ Analytics config.Analytics
+ Recording config.Recording
+ }{conf.Coordinator.Analytics, conf.Recording}
+
+ handler := func(tpl *template.Template, w httpx.ResponseWriter, r *httpx.Request) {
+ if err := tpl.Execute(w, tplData); err != nil {
+ log.Error().Err(err).Msg("error with the analytics template file")
+ }
+ }
+
+ h := httpx.FileServer("./web")
+
+ 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)
+ return
+ }
+ h.ServeHTTP(w, r)
+ })
+ }
+
+ 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
+ }
+ h.ServeHTTP(w, r)
+ })
}
diff --git a/pkg/coordinator/handlers.go b/pkg/coordinator/handlers.go
deleted file mode 100644
index df8b4e1e..00000000
--- a/pkg/coordinator/handlers.go
+++ /dev/null
@@ -1,381 +0,0 @@
-package coordinator
-
-import (
- "encoding/json"
- "errors"
- "log"
- "math"
- "net/http"
- "strings"
-
- "github.com/giongto35/cloud-game/v2/pkg/config/coordinator"
- "github.com/giongto35/cloud-game/v2/pkg/cws"
- "github.com/giongto35/cloud-game/v2/pkg/cws/api"
- "github.com/giongto35/cloud-game/v2/pkg/environment"
- "github.com/giongto35/cloud-game/v2/pkg/games"
- "github.com/giongto35/cloud-game/v2/pkg/ice"
- "github.com/giongto35/cloud-game/v2/pkg/service"
- "github.com/giongto35/cloud-game/v2/pkg/util"
- "github.com/gofrs/uuid"
- "github.com/gorilla/websocket"
-)
-
-type Server struct {
- service.Service
-
- cfg coordinator.Config
- // games library
- library games.GameLibrary
- // roomToWorker map roomID to workerID
- roomToWorker map[string]string
- // workerClients are the map workerID to worker Client
- workerClients map[string]*WorkerClient
- // browserClients are the map sessionID to browser Client
- browserClients map[string]*BrowserClient
-}
-
-var upgrader = websocket.Upgrader{}
-
-func NewServer(cfg coordinator.Config, library games.GameLibrary) *Server {
- // scan the lib right away
- library.Scan()
-
- return &Server{
- cfg: cfg,
- library: library,
- // Mapping roomID to server
- roomToWorker: map[string]string{},
- // Mapping workerID to worker
- workerClients: map[string]*WorkerClient{},
- // Mapping sessionID to browser
- browserClients: map[string]*BrowserClient{},
- }
-}
-
-// WSO handles all connections from a new worker to coordinator
-func (s *Server) WSO(w http.ResponseWriter, r *http.Request) {
- log.Println("Coordinator: A worker is connecting...")
-
- connRt, err := GetConnectionRequest(r.URL.Query().Get("data"))
- if err != nil {
- log.Printf("Coordinator: got a malformed request: %v", err.Error())
- return
- }
-
- if connRt.PingAddr == "" {
- log.Printf("Warning! Ping address is not set.")
- }
-
- if s.cfg.Coordinator.Server.Https && !connRt.IsHTTPS {
- log.Printf("Warning! Unsecure connection. The worker may not work properly without HTTPS on its side!")
- }
-
- // be aware of ReadBufferSize, WriteBufferSize (default 4096)
- // https://pkg.go.dev/github.com/gorilla/websocket?tab=doc#Upgrader
- c, err := upgrader.Upgrade(w, r, nil)
- if err != nil {
- log.Println("Coordinator: [!] WS upgrade:", err)
- return
- }
-
- // Generate workerID
- var workerID string
- for {
- workerID = uuid.Must(uuid.NewV4()).String()
- // check duplicate
- if _, ok := s.workerClients[workerID]; !ok {
- break
- }
- }
-
- // Create a workerClient instance
- wc := NewWorkerClient(c, workerID)
- wc.Println("Generated worker ID")
- wc.Zone = connRt.Zone
- wc.PingServer = connRt.PingAddr
-
- // Register to workersClients map the client connection
- address := util.GetRemoteAddress(c)
- public := util.IsPublicIP(address)
-
- wc.Printf("addr: %v | zone: %v | pub: %v | ping: %v", address, wc.Zone, public, wc.PingServer)
-
- // In case worker and coordinator in the same host
- if !public && s.cfg.Environment.Get() == environment.Production {
- // Don't accept private IP for worker's address in prod mode
- // However, if the worker in the same host with coordinator, we can get public IP of worker
- wc.Printf("[!] Address %s is invalid", address)
-
- address = util.GetHostPublicIP()
- wc.Printf("Find public address: %s", address)
-
- if address == "" || !util.IsPublicIP(address) {
- // Skip this worker because we cannot find public IP
- wc.Println("[!] Unable to find public address, reject worker")
- return
- }
- }
-
- // Create a workerClient instance
- wc.Address = address
- wc.StunTurnServer = ice.ToJson(s.cfg.Webrtc.IceServers, ice.Replacement{From: "server-ip", To: address})
-
- // Attach to Server instance with workerID, add defer
- s.workerClients[workerID] = wc
- defer s.cleanWorker(wc, workerID)
-
- wc.Send(api.ServerIdPacket(workerID), nil)
-
- s.workerRoutes(wc)
- wc.Listen()
-}
-
-// WSO handles all connections from user/frontend to coordinator
-func (s *Server) WS(w http.ResponseWriter, r *http.Request) {
- log.Println("Coordinator: A user is connecting...")
- defer func() {
- if r := recover(); r != nil {
- log.Println("Warn: Something wrong. Recovered in ", r)
- }
- }()
-
- // be aware of ReadBufferSize, WriteBufferSize (default 4096)
- // https://pkg.go.dev/github.com/gorilla/websocket?tab=doc#Upgrader
- c, err := upgrader.Upgrade(w, r, nil)
- if err != nil {
- log.Println("Coordinator: [!] WS upgrade:", err)
- return
- }
-
- // Generate sessionID for browserClient
- var sessionID string
- for {
- sessionID = uuid.Must(uuid.NewV4()).String()
- // check duplicate
- if _, ok := s.browserClients[sessionID]; !ok {
- break
- }
- }
-
- // Create browserClient instance
- bc := NewBrowserClient(c, sessionID)
- bc.Println("Generated worker ID")
-
- // Run browser listener first (to capture ping)
- go bc.Listen()
-
- /* Create a session - mapping browserClient with workerClient */
- var wc *WorkerClient
-
- // get roomID if it is embeded in request. Server will pair the frontend with the server running the room. It only happens when we are trying to access a running room over share link.
- // TODO: Update link to the wiki
- roomID := r.URL.Query().Get("room_id")
- // zone param is to pick worker in that zone only
- // if there is no zone param, we can pic
- userZone := r.URL.Query().Get("zone")
-
- bc.Printf("Get Room %s Zone %s From URL %v", roomID, userZone, r.URL)
-
- if roomID != "" {
- bc.Printf("Detected roomID %v from URL", roomID)
- if workerID, ok := s.roomToWorker[roomID]; ok {
- wc = s.workerClients[workerID]
- if userZone != "" && wc.Zone != userZone {
- // if there is zone param, we need to ensure ther worker in that zone
- // if not we consider the room is missing
- wc = nil
- } else {
- bc.Printf("Found running server with id=%v client=%v", workerID, wc)
- }
- }
- }
-
- // If there is no existing server to connect to, we find the best possible worker for the frontend
- if wc == nil {
- // Get best server for frontend to connect to
- wc, err = s.getBestWorkerClient(bc, userZone)
- if err != nil {
- return
- }
- }
-
- // Assign available worker to browserClient
- bc.WorkerID = wc.WorkerID
-
- wc.ChangeUserQuantityBy(1)
- defer wc.ChangeUserQuantityBy(-1)
-
- // Everything is cool
- // Attach to Server instance with sessionID
- s.browserClients[sessionID] = bc
- defer s.cleanBrowser(bc, sessionID)
-
- // Routing browserClient message
- s.useragentRoutes(bc)
-
- bc.Send(cws.WSPacket{
- ID: "init",
- Data: createInitPackage(wc.StunTurnServer, s.library.GetAll()),
- }, nil)
-
- // If peerconnection is done (client.Done is signalled), we close peerconnection
- <-bc.Done
-
- // Notify worker to clean session
- wc.Send(api.TerminateSessionPacket(sessionID), nil)
-}
-
-func (s *Server) getBestWorkerClient(client *BrowserClient, zone string) (*WorkerClient, error) {
- conf := s.cfg.Coordinator
- if conf.DebugHost != "" {
- client.Println("Connecting to debug host instead prod servers", conf.DebugHost)
- wc := s.getWorkerFromAddress(conf.DebugHost)
- if wc != nil {
- return wc, nil
- }
- // if there is not debugHost, continue usual flow
- client.Println("Not found, connecting to all available servers")
- }
-
- workerClients := s.getAvailableWorkers()
-
- serverID, err := s.findBestServerFromBrowser(workerClients, client, zone)
- if err != nil {
- log.Println(err)
- return nil, err
- }
-
- return s.workerClients[serverID], nil
-}
-
-// getAvailableWorkers returns the list of available worker
-func (s *Server) getAvailableWorkers() map[string]*WorkerClient {
- workerClients := map[string]*WorkerClient{}
- for k, w := range s.workerClients {
- if w.HasGameSlot() {
- workerClients[k] = w
- }
- }
-
- return workerClients
-}
-
-// getWorkerFromAddress returns the worker has given address
-func (s *Server) getWorkerFromAddress(address string) *WorkerClient {
- for _, w := range s.workerClients {
- if w.HasGameSlot() && w.Address == address {
- return w
- }
- }
-
- return nil
-}
-
-// findBestServerFromBrowser returns the best server for a session
-// All workers addresses are sent to user and user will ping to get latency
-func (s *Server) findBestServerFromBrowser(workerClients map[string]*WorkerClient, client *BrowserClient, zone string) (string, error) {
- // TODO: Find best Server by latency, currently return by ping
- if len(workerClients) == 0 {
- return "", errors.New("no server found")
- }
-
- latencies := s.getLatencyMapFromBrowser(workerClients, client)
- client.Println("Latency map", latencies)
-
- if len(latencies) == 0 {
- return "", errors.New("no server found")
- }
-
- var bestWorker *WorkerClient
- var minLatency int64 = math.MaxInt64
-
- // get the worker with lowest latency to user
- for wc, l := range latencies {
- if zone != "" && wc.Zone != zone {
- // skip worker not in the zone if zone param is given
- continue
- }
-
- if l < minLatency {
- bestWorker = wc
- minLatency = l
- }
- }
-
- return bestWorker.WorkerID, nil
-}
-
-// getLatencyMapFromBrowser get all latencies from worker to user
-func (s *Server) getLatencyMapFromBrowser(workerClients map[string]*WorkerClient, client *BrowserClient) map[*WorkerClient]int64 {
- var workersList []*WorkerClient
- var addressList []string
- uniqueAddresses := map[string]bool{}
- latencyMap := map[*WorkerClient]int64{}
-
- // addressList is the list of worker addresses
- for _, workerClient := range workerClients {
- if _, ok := uniqueAddresses[workerClient.PingServer]; !ok {
- addressList = append(addressList, workerClient.PingServer)
- }
- uniqueAddresses[workerClient.PingServer] = true
- workersList = append(workersList, workerClient)
- }
-
- // send this address to user and get back latency
- client.Println("Send sync", addressList, strings.Join(addressList, ","))
- data := client.SyncSend(cws.WSPacket{
- ID: "checkLatency",
- Data: strings.Join(addressList, ","),
- })
-
- respLatency := map[string]int64{}
- err := json.Unmarshal([]byte(data.Data), &respLatency)
- if err != nil {
- log.Println(err)
- return latencyMap
- }
-
- for _, workerClient := range workersList {
- if latency, ok := respLatency[workerClient.PingServer]; ok {
- latencyMap[workerClient] = latency
- }
- }
- return latencyMap
-}
-
-// cleanBrowser is called when a browser is disconnected
-func (s *Server) cleanBrowser(bc *BrowserClient, sessionID string) {
- bc.Println("Disconnect from coordinator")
- delete(s.browserClients, sessionID)
- bc.Close()
-}
-
-// cleanWorker is called when a worker is disconnected
-// connection from worker to coordinator is also closed
-func (s *Server) cleanWorker(wc *WorkerClient, workerID string) {
- wc.Println("Unregister worker from coordinator")
- // Remove workerID from workerClients
- delete(s.workerClients, workerID)
- // Clean all rooms connecting to that server
- for roomID, roomServer := range s.roomToWorker {
- if roomServer == workerID {
- wc.Printf("Remove room %s", roomID)
- delete(s.roomToWorker, roomID)
- }
- }
-
- wc.Close()
-}
-
-// createInitPackage returns serverhost + game list in encoded wspacket format
-// This package will be sent to initialize
-func createInitPackage(stunturn string, games []games.GameMetadata) string {
- var gameName []string
- for _, game := range games {
- gameName = append(gameName, game.Name)
- }
-
- initPackage := append([]string{stunturn}, gameName...)
- encodedList, _ := json.Marshal(initPackage)
- return string(encodedList)
-}
diff --git a/pkg/coordinator/http.go b/pkg/coordinator/http.go
deleted file mode 100644
index 99fd5260..00000000
--- a/pkg/coordinator/http.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package coordinator
-
-import (
- "html/template"
- "log"
- "net/http"
-
- "github.com/giongto35/cloud-game/v2/pkg/config/coordinator"
- "github.com/giongto35/cloud-game/v2/pkg/network/httpx"
-)
-
-func NewHTTPServer(conf coordinator.Config, fnMux func(mux *http.ServeMux)) (*httpx.Server, error) {
- return httpx.NewServer(
- conf.Coordinator.Server.GetAddr(),
- func(*httpx.Server) http.Handler {
- h := http.NewServeMux()
- h.Handle("/", index(conf))
- h.Handle("/static/", static("./web"))
- fnMux(h)
- return h
- },
- httpx.WithServerConfig(conf.Coordinator.Server),
- )
-}
-
-func index(conf coordinator.Config) http.Handler {
- tpl, err := template.ParseFiles("./web/index.html")
- if err != nil {
- log.Fatal(err)
- }
-
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- // return 404 on unknown
- if r.URL.Path != "/" {
- http.NotFound(w, r)
- return
- }
- // render index page with some tpl values
- if err = tpl.Execute(w, conf.Coordinator.Analytics); err != nil {
- log.Fatal(err)
- }
- })
-}
-
-func static(dir string) http.Handler {
- return http.StripPrefix("/static/", http.FileServer(http.Dir(dir)))
-}
diff --git a/pkg/coordinator/hub.go b/pkg/coordinator/hub.go
new file mode 100644
index 00000000..9e646ced
--- /dev/null
+++ b/pkg/coordinator/hub.go
@@ -0,0 +1,337 @@
+package coordinator
+
+import (
+ "bytes"
+ "encoding/base64"
+ "fmt"
+ "net/http"
+ "net/url"
+
+ "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/logger"
+)
+
+type Connection interface {
+ Disconnect()
+ Id() com.Uid
+ ProcessPackets(func(api.In[com.Uid]) error) chan struct{}
+
+ Send(api.PT, any) ([]byte, error)
+ Notify(api.PT, any)
+}
+
+type Hub struct {
+ conf config.CoordinatorConfig
+ log *logger.Logger
+ users com.NetMap[com.Uid, *User]
+ workers com.NetMap[com.Uid, *Worker]
+}
+
+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](),
+ log: log,
+ }
+}
+
+// handleUserConnection handles all connections from user/frontend.
+func (h *Hub) handleUserConnection() http.HandlerFunc {
+ var connector com.Server
+ connector.Origin(h.conf.Coordinator.Origin.UserWs)
+
+ log := h.log.Extend(h.log.With().
+ Str(logger.ClientField, "u").
+ Str(logger.DirectionField, logger.MarkIn),
+ )
+
+ return func(w http.ResponseWriter, r *http.Request) {
+ h.log.Debug().Msgf("Handshake %v", r.Host)
+
+ conn, err := connector.Connect(w, r)
+ if err != nil {
+ h.log.Error().Err(err).Msg("user connection fail")
+ return
+ }
+
+ user := NewUser(conn, log)
+ defer h.users.RemoveDisconnect(user)
+ 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
+ }
+
+ // 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 := worker.AppNames()
+ list := make([]api.AppMeta, len(apps))
+ for i := range apps {
+ 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
+ }
+}
+
+func RequestToHandshake(data string) (*api.ConnectionRequest[com.Uid], error) {
+ if data == "" {
+ return nil, api.ErrMalformed
+ }
+ handshake, err := api.UnwrapChecked[api.ConnectionRequest[com.Uid]](base64.URLEncoding.DecodeString(data))
+ if err != nil || handshake == nil {
+ return nil, fmt.Errorf("%w (%v)", err, handshake)
+ }
+ return handshake, nil
+}
+
+// handleWorkerConnection handles all connections from a new worker to coordinator.
+func (h *Hub) handleWorkerConnection() http.HandlerFunc {
+ var connector com.Server
+ connector.Origin(h.conf.Coordinator.Origin.WorkerWs)
+
+ log := h.log.Extend(h.log.With().
+ Str(logger.ClientField, "w").
+ 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)
+
+ handshake, err := RequestToHandshake(r.URL.Query().Get(api.DataQueryParam))
+ if err != nil {
+ h.log.Error().Err(err).Msg("handshake fail")
+ return
+ }
+
+ if handshake.PingURL == "" {
+ h.log.Warn().Msg("Ping address is not set")
+ }
+
+ if h.conf.Coordinator.Server.Https && !handshake.IsHTTPS {
+ h.log.Warn().Msg("Unsecure worker connection. Unsecure to secure may be bad.")
+ }
+
+ // set connection uid from the handshake
+ if handshake.Id != com.NilUid {
+ h.log.Debug().Msgf("Worker uid will be set to %v", handshake.Id)
+ }
+
+ conn, err := connector.Connect(w, r)
+ if err != nil {
+ 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)
+ done := worker.HandleRequests(&h.users)
+ h.workers.Add(worker)
+ log.Info().
+ Str(logger.DirectionField, logger.MarkPlus).
+ Msgf("worker %s", worker.PrintInfo())
+ <-done
+ }
+}
+
+func (h *Hub) GetServerList() (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(),
+ Machine: string(w.Id().Machine()),
+ PingURL: w.PingServer,
+ Port: w.Port,
+ Tag: w.Tag,
+ Zone: w.Zone,
+ }
+ if debug {
+ server.Room = w.RoomId
+ }
+ r = append(r, server)
+ }
+ return
+}
+
+// findWorkerFor searches a free worker for the user depending on
+// various conditions.
+func (h *Hub) findWorkerFor(usr *User, q url.Values, log *logger.Logger) *Worker {
+ log.Debug().Msg("Search available workers")
+ 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 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.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:
+ log.Debug().Msgf("Searching fastest free worker...")
+ if worker = h.findFastestWorker(zone,
+ func(servers []string) (map[string]int64, error) { return usr.CheckLatency(servers) }); worker != nil {
+ log.Debug().Msg("The fastest worker has been found")
+ }
+ default:
+ log.Debug().Msgf("Searching any free worker...")
+ if worker = h.find1stFreeWorker(zone); worker != nil {
+ log.Debug().Msgf("Found next free worker")
+ }
+ }
+ }
+ return 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 {
+ 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
+ for w := range h.workers.Values() {
+ if w.HasSlot() && w.In(region) {
+ workers = append(workers, w)
+ }
+ }
+ return workers
+}
+
+func (h *Hub) find1stFreeWorker(region string) *Worker {
+ workers := h.getAvailableWorkers(region)
+ if len(workers) > 0 {
+ return workers[0]
+ }
+ return nil
+}
+
+// findFastestWorker returns the best server for a session.
+// All workers addresses are sent to user and user will ping to get latency.
+// !to rewrite
+func (h *Hub) findFastestWorker(region string, fn func(addresses []string) (map[string]int64, error)) *Worker {
+ workers := h.getAvailableWorkers(region)
+ if len(workers) == 0 {
+ return nil
+ }
+
+ var addresses []string
+ group := map[string][]struct{}{}
+ for _, w := range workers {
+ if _, ok := group[w.PingServer]; !ok {
+ addresses = append(addresses, w.PingServer)
+ }
+ group[w.PingServer] = append(group[w.PingServer], struct{}{})
+ }
+
+ latencies, err := fn(addresses)
+ if len(latencies) == 0 || err != nil {
+ return nil
+ }
+
+ workers = h.getAvailableWorkers(region)
+ if len(workers) == 0 {
+ return nil
+ }
+
+ var bestWorker *Worker
+ var minLatency int64 = 1<<31 - 1
+ // get a worker with the lowest latency
+ for addr, ping := range latencies {
+ if ping < minLatency {
+ for _, w := range workers {
+ if w.PingServer == addr {
+ bestWorker = w
+ }
+ }
+ minLatency = ping
+ }
+ }
+ return bestWorker
+}
+
+func (h *Hub) findWorkerById(id string, useAllWorkers bool) *Worker {
+ if id == "" {
+ return nil
+ }
+
+ uid, err := com.UidFromString(id)
+ if err != nil {
+ return nil
+ }
+
+ for _, w := range h.getAvailableWorkers("") {
+ if w.Id() == com.NilUid {
+ continue
+ }
+ if useAllWorkers {
+ if uid == w.Id() {
+ return w
+ }
+ } else {
+ // select any worker on the same machine when workers are grouped on the client
+ if bytes.Equal(uid.Machine(), w.Id().Machine()) {
+ return w
+ }
+ }
+ }
+
+ return nil
+}
diff --git a/pkg/coordinator/internalhandlers.go b/pkg/coordinator/internalhandlers.go
deleted file mode 100644
index 9e502fad..00000000
--- a/pkg/coordinator/internalhandlers.go
+++ /dev/null
@@ -1,75 +0,0 @@
-package coordinator
-
-import (
- "encoding/base64"
- "encoding/json"
- "log"
-
- "github.com/giongto35/cloud-game/v2/pkg/cws"
- "github.com/giongto35/cloud-game/v2/pkg/cws/api"
-)
-
-func (wc *WorkerClient) handleHeartbeat() cws.PacketHandler {
- return func(resp cws.WSPacket) cws.WSPacket {
- return resp
- }
-}
-
-func GetConnectionRequest(data string) (api.ConnectionRequest, error) {
- req := api.ConnectionRequest{}
- if data == "" {
- return req, nil
- }
- decodeString, err := base64.URLEncoding.DecodeString(data)
- if err != nil {
- return req, err
- }
- err = json.Unmarshal(decodeString, &req)
- return req, err
-}
-
-// handleRegisterRoom event from a worker, when worker created a new room.
-// RoomID is global so it is managed by coordinator.
-func (wc *WorkerClient) handleRegisterRoom(s *Server) cws.PacketHandler {
- return func(resp cws.WSPacket) cws.WSPacket {
- log.Printf("Coordinator: Received registerRoom room %s from worker %s", resp.Data, wc.WorkerID)
- s.roomToWorker[resp.Data] = wc.WorkerID
- log.Printf("Coordinator: Current room list is: %+v", s.roomToWorker)
- return api.RegisterRoomPacket(api.NoData)
- }
-}
-
-// handleGetRoom returns the server ID based on requested roomID.
-func (wc *WorkerClient) handleGetRoom(s *Server) cws.PacketHandler {
- return func(resp cws.WSPacket) cws.WSPacket {
- log.Println("Coordinator: Received a get room request")
- log.Println("Result: ", s.roomToWorker[resp.Data])
- return api.GetRoomPacket(s.roomToWorker[resp.Data])
- }
-}
-
-// handleCloseRoom event from a worker, when worker close a room.
-func (wc *WorkerClient) handleCloseRoom(s *Server) cws.PacketHandler {
- return func(resp cws.WSPacket) cws.WSPacket {
- log.Printf("Coordinator: Received closeRoom room %s from worker %s", resp.Data, wc.WorkerID)
- delete(s.roomToWorker, resp.Data)
- log.Printf("Coordinator: Current room list is: %+v", s.roomToWorker)
- return api.CloseRoomPacket(api.NoData)
- }
-}
-
-// handleIceCandidate passes an ICE candidate (WebRTC) to the browser.
-func (wc *WorkerClient) handleIceCandidate(s *Server) cws.PacketHandler {
- return func(resp cws.WSPacket) cws.WSPacket {
- wc.Println("Received IceCandidate from worker -> relay to browser")
- bc, ok := s.browserClients[resp.SessionID]
- if ok {
- // Remove SessionID while sending back to browser
- resp.SessionID = ""
- bc.Send(resp, nil)
- } else {
- wc.Println("Error: unknown SessionID:", resp.SessionID)
- }
- return cws.EmptyPacket
- }
-}
diff --git a/pkg/coordinator/routes.go b/pkg/coordinator/routes.go
deleted file mode 100644
index 5652fb9a..00000000
--- a/pkg/coordinator/routes.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package coordinator
-
-import "github.com/giongto35/cloud-game/v2/pkg/cws/api"
-
-// workerRoutes adds all worker request routes.
-func (s *Server) workerRoutes(wc *WorkerClient) {
- if wc == nil {
- return
- }
- wc.Receive(api.Heartbeat, wc.handleHeartbeat())
- wc.Receive(api.RegisterRoom, wc.handleRegisterRoom(s))
- wc.Receive(api.GetRoom, wc.handleGetRoom(s))
- wc.Receive(api.CloseRoom, wc.handleCloseRoom(s))
- wc.Receive(api.IceCandidate, wc.handleIceCandidate(s))
-}
-
-// useragentRoutes adds all useragent (browser) request routes.
-func (s *Server) useragentRoutes(bc *BrowserClient) {
- if bc == nil {
- return
- }
- bc.Receive(api.Heartbeat, bc.handleHeartbeat())
- bc.Receive(api.InitWebrtc, bc.handleInitWebrtc(s))
- bc.Receive(api.Answer, bc.handleAnswer(s))
- bc.Receive(api.IceCandidate, bc.handleIceCandidate(s))
- bc.Receive(api.GameStart, bc.handleGameStart(s))
- bc.Receive(api.GameQuit, bc.handleGameQuit(s))
- bc.Receive(api.GameSave, bc.handleGameSave(s))
- bc.Receive(api.GameLoad, bc.handleGameLoad(s))
- bc.Receive(api.GamePlayerSelect, bc.handleGamePlayerSelect(s))
- bc.Receive(api.GameMultitap, bc.handleGameMultitap(s))
-}
diff --git a/pkg/coordinator/user.go b/pkg/coordinator/user.go
new file mode 100644
index 00000000..e1efef49
--- /dev/null
+++ b/pkg/coordinator/user.go
@@ -0,0 +1,81 @@
+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/config"
+ "github.com/giongto35/cloud-game/v3/pkg/logger"
+)
+
+type User struct {
+ Connection
+ w *Worker // linked worker
+ log *logger.Logger
+}
+
+type HasServerInfo interface {
+ GetServerList() []api.Server
+}
+
+func NewUser(sock *com.Connection, log *logger.Logger) *User {
+ conn := com.NewConnection[api.PT, api.In[com.Uid], api.Out, *api.Out](sock, com.NewUid(), log)
+ return &User{
+ Connection: conn,
+ log: log.Extend(log.With().
+ Str(logger.ClientField, logger.MarkNone).
+ Str(logger.DirectionField, logger.MarkNone).
+ Str("cid", conn.Id().Short())),
+ }
+}
+
+func (u *User) Bind(w *Worker) bool {
+ u.w = w
+ // 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.TerminateSession(u.Id().String())
+ }
+}
+
+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:
+ err = api.Do(x, u.HandleWebrtcAnswer)
+ case api.WebrtcIce:
+ err = api.Do(x, u.HandleWebrtcIceCandidate)
+ case api.StartGame:
+ err = api.Do(x, func(d api.GameStartUserRequest) { u.HandleStartGame(d, conf) })
+ case api.QuitGame:
+ err = api.Do(x, u.HandleQuitGame)
+ case api.SaveGame:
+ err = u.HandleSaveGame()
+ case api.LoadGame:
+ err = u.HandleLoadGame()
+ case api.ChangePlayer:
+ 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
+ }
+ 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
+ })
+}
diff --git a/pkg/coordinator/useragenthandlers.go b/pkg/coordinator/useragenthandlers.go
deleted file mode 100644
index fbbcb7b4..00000000
--- a/pkg/coordinator/useragenthandlers.go
+++ /dev/null
@@ -1,212 +0,0 @@
-package coordinator
-
-import (
- "errors"
- "fmt"
-
- "github.com/giongto35/cloud-game/v2/pkg/cws"
- "github.com/giongto35/cloud-game/v2/pkg/cws/api"
- "github.com/giongto35/cloud-game/v2/pkg/games"
- "github.com/giongto35/cloud-game/v2/pkg/session"
-)
-
-func (bc *BrowserClient) handleHeartbeat() cws.PacketHandler {
- return func(resp cws.WSPacket) cws.WSPacket { return resp }
-}
-
-func (bc *BrowserClient) handleInitWebrtc(o *Server) cws.PacketHandler {
- return func(resp cws.WSPacket) (req cws.WSPacket) {
- // initWebrtc now only sends signal to worker, asks it to createOffer
- bc.Printf("Received init_webrtc request -> relay to worker: %s", bc.WorkerID)
- // relay request to target worker
- // worker creates a PeerConnection, and createOffer
- // send SDP back to browser
- resp.SessionID = bc.SessionID
- wc, ok := o.workerClients[bc.WorkerID]
- if !ok {
- return cws.EmptyPacket
- }
- sdp := wc.SyncSend(resp)
- bc.Println("Received SDP from worker -> sending back to browser")
- return sdp
- }
-}
-
-func (bc *BrowserClient) handleAnswer(o *Server) cws.PacketHandler {
- return func(resp cws.WSPacket) (req cws.WSPacket) {
- // contains SDP of browser createAnswer
- // forward to worker
- bc.Println("Received browser answered SDP -> relay to worker")
- resp.SessionID = bc.SessionID
- wc, ok := o.workerClients[bc.WorkerID]
- if !ok {
- return cws.EmptyPacket
- }
- wc.Send(resp, nil)
- // no need to response
- return cws.EmptyPacket
- }
-}
-
-func (bc *BrowserClient) handleIceCandidate(o *Server) cws.PacketHandler {
- return func(resp cws.WSPacket) (req cws.WSPacket) {
- // contains ICE candidate of browser
- // forward to worker
- bc.Println("Received IceCandidate from browser -> relay to worker")
- resp.SessionID = bc.SessionID
- wc, ok := o.workerClients[bc.WorkerID]
- if !ok {
- return cws.EmptyPacket
- }
- wc.Send(resp, nil)
- return cws.EmptyPacket
- }
-}
-
-func (bc *BrowserClient) handleGameStart(o *Server) cws.PacketHandler {
- return func(resp cws.WSPacket) (req cws.WSPacket) {
- bc.Println("Received start request from a browser -> relay to worker")
-
- // TODO: Async
- resp.SessionID = bc.SessionID
- wc, ok := o.workerClients[bc.WorkerID]
- if !ok {
- return cws.EmptyPacket
- }
-
- // +injects game data into the original game request
- gameStartCall, err := newGameStartCall(resp.RoomID, resp.Data, o.library)
- if err != nil {
- return cws.EmptyPacket
- }
- if packet, err := gameStartCall.To(); err != nil {
- return cws.EmptyPacket
- } else {
- resp.Data = packet
- }
- workerResp := wc.SyncSend(resp)
-
- // Response from worker contains initialized roomID. Set roomID to the session
- bc.RoomID = workerResp.RoomID
- bc.Println("Received room response from browser: ", workerResp.RoomID)
-
- return workerResp
- }
-}
-
-func (bc *BrowserClient) handleGameQuit(o *Server) cws.PacketHandler {
- return func(resp cws.WSPacket) (req cws.WSPacket) {
- bc.Println("Received quit request from a browser -> relay to worker")
-
- // TODO: Async
- resp.SessionID = bc.SessionID
- wc, ok := o.workerClients[bc.WorkerID]
- if !ok {
- return cws.EmptyPacket
- }
- // Send but, waiting
- wc.SyncSend(resp)
-
- return cws.EmptyPacket
- }
-}
-
-func (bc *BrowserClient) handleGameSave(o *Server) cws.PacketHandler {
- return func(resp cws.WSPacket) (req cws.WSPacket) {
- bc.Println("Received save request from a browser -> relay to worker")
-
- // TODO: Async
- resp.SessionID = bc.SessionID
- resp.RoomID = bc.RoomID
- wc, ok := o.workerClients[bc.WorkerID]
- if !ok {
- return cws.EmptyPacket
- }
- resp = wc.SyncSend(resp)
-
- return resp
- }
-}
-
-func (bc *BrowserClient) handleGameLoad(o *Server) cws.PacketHandler {
- return func(resp cws.WSPacket) (req cws.WSPacket) {
- bc.Println("Received load request from a browser -> relay to worker")
-
- // TODO: Async
- resp.SessionID = bc.SessionID
- resp.RoomID = bc.RoomID
- wc, ok := o.workerClients[bc.WorkerID]
- if !ok {
- return cws.EmptyPacket
- }
- resp = wc.SyncSend(resp)
-
- return resp
- }
-}
-
-func (bc *BrowserClient) handleGamePlayerSelect(o *Server) cws.PacketHandler {
- return func(resp cws.WSPacket) (req cws.WSPacket) {
- bc.Println("Received update player index request from a browser -> relay to worker")
-
- // TODO: Async
- resp.SessionID = bc.SessionID
- resp.RoomID = bc.RoomID
- wc, ok := o.workerClients[bc.WorkerID]
- if !ok {
- return cws.EmptyPacket
- }
- resp = wc.SyncSend(resp)
-
- return resp
- }
-}
-
-func (bc *BrowserClient) handleGameMultitap(o *Server) cws.PacketHandler {
- return func(resp cws.WSPacket) (req cws.WSPacket) {
- bc.Println("Received multitap request from a browser -> relay to worker")
-
- // TODO: Async
- resp.SessionID = bc.SessionID
- resp.RoomID = bc.RoomID
- wc, ok := o.workerClients[bc.WorkerID]
- if !ok {
- return cws.EmptyPacket
- }
- resp = wc.SyncSend(resp)
-
- return resp
- }
-}
-
-// newGameStartCall gathers data for a new game start call of the worker
-func newGameStartCall(roomId string, data string, library games.GameLibrary) (api.GameStartCall, error) {
- request := api.GameStartRequest{}
- if err := request.From(data); err != nil {
- return api.GameStartCall{}, errors.New("invalid request")
- }
-
- // the name of the game either in the `room id` field or
- // it's in the initial request
- game := request.GameName
- if roomId != "" {
- // ! should be moved into coordinator
- name := session.GetGameNameFromRoomID(roomId)
- if name == "" {
- return api.GameStartCall{}, errors.New("couldn't decode game name from the room id")
- }
- game = name
- }
-
- gameInfo := library.FindGameByName(game)
- if gameInfo.Path == "" {
- return api.GameStartCall{}, fmt.Errorf("couldn't find game info for the game %v", game)
- }
-
- return api.GameStartCall{
- Name: gameInfo.Name,
- Base: gameInfo.Base,
- Path: gameInfo.Path,
- Type: gameInfo.Type,
- }, nil
-}
diff --git a/pkg/coordinator/userapi.go b/pkg/coordinator/userapi.go
new file mode 100644
index 00000000..fd8b7235
--- /dev/null
+++ b/pkg/coordinator/userapi.go
@@ -0,0 +1,38 @@
+package coordinator
+
+import (
+ "unsafe"
+
+ "github.com/giongto35/cloud-game/v3/pkg/api"
+ "github.com/giongto35/cloud-game/v3/pkg/config"
+)
+
+// 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) {
+ dat, err := api.UnwrapChecked[api.CheckLatencyUserRequest](u.Send(api.CheckLatency, req))
+ if dat == nil {
+ return api.CheckLatencyUserRequest{}, err
+ }
+ return *dat, nil
+}
+
+// InitSession signals the user that the app is ready to go.
+func (u *User) InitSession(wid string, ice []config.IceServer, games []api.AppMeta) {
+ u.Notify(api.InitSession, api.InitSessionUserResponse{
+ Ice: *(*[]api.IceServer)(unsafe.Pointer(&ice)), // don't do this at home
+ Games: games,
+ Wid: wid,
+ })
+}
+
+// SendWebrtcOffer sends SDP offer back to the user.
+func (u *User) SendWebrtcOffer(sdp string) { u.Notify(api.WebrtcOffer, sdp) }
+
+// SendWebrtcIceCandidate sends remote ICE candidate back to the user.
+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(av *api.AppVideoInfo, kbMouse bool) {
+ u.Notify(api.StartGame, api.GameStartUserResponse{RoomId: u.w.RoomId, Av: av, KbMouse: kbMouse})
+}
diff --git a/pkg/coordinator/userhandlers.go b/pkg/coordinator/userhandlers.go
new file mode 100644
index 00000000..6dddd30e
--- /dev/null
+++ b/pkg/coordinator/userhandlers.go
@@ -0,0 +1,196 @@
+package coordinator
+
+import (
+ "sort"
+ "time"
+
+ "github.com/giongto35/cloud-game/v3/pkg/api"
+ "github.com/giongto35/cloud-game/v3/pkg/config"
+)
+
+func (u *User) HandleWebrtcInit() {
+ 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
+ }
+ u.SendWebrtcOffer(string(*resp))
+}
+
+func (u *User) HandleWebrtcAnswer(rq api.WebrtcAnswerUserRequest) {
+ u.w.WebrtcAnswer(u.Id().String(), string(rq))
+}
+
+func (u *User) HandleWebrtcIceCandidate(rq api.WebrtcUserIceCandidate) {
+ u.w.WebrtcIceCandidate(u.Id().String(), string(rq))
+}
+
+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
+ }
+ }
+
+ 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
+ }
+ if startGameResp.Rid == "" {
+ u.log.Error().Msg("there is no room")
+ return
+ }
+ u.log.Info().Str("id", startGameResp.Rid).Msg("Received room response from worker")
+ u.StartGame(startGameResp.AV, startGameResp.KbMouse)
+
+ // send back recording status
+ if conf.Recording.Enabled && rq.Record {
+ u.Notify(api.RecordGame, api.OK)
+ }
+}
+
+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().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().String())
+ if err != nil {
+ return err
+ }
+ u.Notify(api.LoadGame, resp)
+ return nil
+}
+
+func (u *User) HandleChangePlayer(rq api.ChangePlayerUserRequest) {
+ 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)
+ return
+ }
+ u.Notify(api.ChangePlayer, rq)
+}
+
+func (u *User) HandleRecordGame(rq api.RecordGameRequest) {
+ if u.w == nil {
+ return
+ }
+
+ u.log.Debug().Msgf("??? room: %v, rec: %v user: %v", u.w.RoomId, rq.Active, rq.User)
+
+ if u.w.RoomId == "" {
+ u.log.Error().Msg("Recording in the empty room is not allowed!")
+ return
+ }
+
+ 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
+ }
+ u.Notify(api.RecordGame, resp)
+}
+
+func (u *User) handleGetWorkerList(debug bool, info HasServerInfo) {
+ response := api.GetWorkerListResponse{}
+ servers := info.GetServerList()
+
+ if debug {
+ response.Servers = servers
+ } else {
+ 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}
+ }
+ v := unique[mid]
+ if v != nil {
+ v.Replicas++
+ }
+ }
+ for _, v := range unique {
+ response.Servers = append(response.Servers, *v)
+ }
+ }
+ if len(response.Servers) > 0 {
+ sort.SliceStable(response.Servers, func(i, j int) bool {
+ if response.Servers[i].Addr != response.Servers[j].Addr {
+ return response.Servers[i].Addr < response.Servers[j].Addr
+ }
+ return response.Servers[i].Port < response.Servers[j].Port
+ })
+ }
+ u.Notify(api.GetWorkerList, response)
+}
diff --git a/pkg/coordinator/worker.go b/pkg/coordinator/worker.go
index 83b661f6..137d7777 100644
--- a/pkg/coordinator/worker.go
+++ b/pkg/coordinator/worker.go
@@ -1,63 +1,191 @@
package coordinator
import (
+ "errors"
"fmt"
- "log"
- "sync"
+ "sync/atomic"
- "github.com/giongto35/cloud-game/v2/pkg/cws"
- "github.com/gorilla/websocket"
+ "github.com/giongto35/cloud-game/v3/pkg/api"
+ "github.com/giongto35/cloud-game/v3/pkg/com"
+ "github.com/giongto35/cloud-game/v3/pkg/logger"
)
-type WorkerClient struct {
- *cws.Client
+type Worker struct {
+ AppLibrary
+ Connection
+ RegionalClient
+ Session
+ slotted
- WorkerID string
- Address string // ip address of worker
- // public server used for ping check
- PingServer string
- StunTurnServer string
- userCount int // may be atomic
- Zone string
+ Addr string
+ PingServer string
+ Port string
+ RoomId string // room reference
+ Tag string
+ Zone string
- mu sync.Mutex
+ Lib []api.GameInfo
+ Sessions map[string]struct{}
+
+ log *logger.Logger
}
-// NewWorkerClient returns a client connecting to worker.
-// This connection exchanges information between workers and server.
-func NewWorkerClient(c *websocket.Conn, workerID string) *WorkerClient {
- return &WorkerClient{
- Client: cws.NewClient(c),
- WorkerID: workerID,
+type RegionalClient interface {
+ In(region string) bool
+}
+
+type HasUserRegistry interface {
+ 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 {
+ conn := com.NewConnection[api.PT, api.In[com.Uid], api.Out, *api.Out](sock, handshake.Id, log)
+ return &Worker{
+ Connection: conn,
+ Addr: handshake.Addr,
+ PingServer: handshake.PingURL,
+ Port: handshake.Port,
+ Tag: handshake.Tag,
+ Zone: handshake.Zone,
+ log: log.Extend(log.With().
+ Str(logger.ClientField, logger.MarkNone).
+ Str(logger.DirectionField, logger.MarkNone).
+ Str("cid", conn.Id().Short())),
}
}
-// ChangeUserQuantityBy increases or decreases the total amount of
-// users connected to the current worker.
-// We count users to determine when the worker becomes new game ready.
-func (wc *WorkerClient) ChangeUserQuantityBy(n int) {
- wc.mu.Lock()
- wc.userCount += n
- // just to be on a safe side
- if wc.userCount < 0 {
- wc.userCount = 0
+func (w *Worker) HandleRequests(users HasUserRegistry) chan struct{} {
+ return w.ProcessPackets(func(p api.In[com.Uid]) (err error) {
+ switch p.T {
+ case api.RegisterRoom:
+ 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:
+ err = api.Do(p, w.HandleCloseRoom)
+ case api.IceCandidate:
+ 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)
+ }
+ 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
}
- wc.mu.Unlock()
+
+ w.Sessions[id] = struct{}{}
}
-// HasGameSlot tells whether the current worker has a
-// free slot to start a new game.
-// Workers support only one game at a time.
-func (wc *WorkerClient) HasGameSlot() bool {
- wc.mu.Lock()
- defer wc.mu.Unlock()
- return wc.userCount == 0
+func (w *Worker) HadSession(id string) bool {
+ _, ok := w.Sessions[id]
+ return ok
}
-func (wc *WorkerClient) Printf(format string, args ...interface{}) {
- log.Printf(fmt.Sprintf("Worker %s] %s", wc.WorkerID, format), args...)
+func (w *Worker) SetSessions(sessions map[string]struct{}) {
+ w.Sessions = sessions
}
-func (wc *WorkerClient) Println(args ...interface{}) {
- log.Println(fmt.Sprintf("Worker %s] %s", wc.WorkerID, fmt.Sprint(args...)))
+// 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 }
+
+// slotted used for tracking user slots and the availability.
+type slotted int32
+
+// HasSlot checks if the current worker has a free slot to start a new game.
+// Workers support only one game at a time, so it returns true in case if
+// there are no players in the room (worker).
+func (s *slotted) HasSlot() bool { return atomic.LoadInt32((*int32)(s)) == 0 }
+
+// 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() {
+ 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
+ }
+ }
+}
+
+func (s *slotted) FreeSlots() { atomic.StoreInt32((*int32)(s), 0) }
+
+func (w *Worker) Disconnect() {
+ w.Connection.Disconnect()
+ w.RoomId = ""
+ w.FreeSlots()
+}
+
+func (w *Worker) PrintInfo() string {
+ return fmt.Sprintf("id: %v, addr: %v, port: %v, zone: %v, ping addr: %v, tag: %v",
+ w.Id(), w.Addr, w.Port, w.Zone, w.PingServer, w.Tag)
}
diff --git a/pkg/coordinator/worker_test.go b/pkg/coordinator/worker_test.go
new file mode 100644
index 00000000..fe4f7a1a
--- /dev/null
+++ b/pkg/coordinator/worker_test.go
@@ -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")
+ }
+}
diff --git a/pkg/coordinator/workerapi.go b/pkg/coordinator/workerapi.go
new file mode 100644
index 00000000..ccf8c700
--- /dev/null
+++ b/pkg/coordinator/workerapi.go
@@ -0,0 +1,68 @@
+package coordinator
+
+import "github.com/giongto35/cloud-game/v3/pkg/api"
+
+func (w *Worker) WebrtcInit(id string) (*api.WebrtcInitResponse, error) {
+ return api.UnwrapChecked[api.WebrtcInitResponse](
+ w.Send(api.WebrtcInit, api.WebrtcInitRequest{Id: id}))
+}
+
+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 string, candidate string) {
+ w.Notify(api.WebrtcIce,
+ api.WebrtcIceCandidateRequest{Stateful: api.Stateful{Id: id}, Candidate: candidate})
+}
+
+func (w *Worker) StartGame(id string, req api.GameStartUserRequest) (*api.StartGameResponse, error) {
+ return api.UnwrapChecked[api.StartGameResponse](
+ 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 string) {
+ w.Notify(api.QuitGame, api.GameQuitRequest{Id: id, Rid: w.RoomId})
+}
+
+func (w *Worker) SaveGame(id string) (*api.SaveGameResponse, error) {
+ return api.UnwrapChecked[api.SaveGameResponse](
+ w.Send(api.SaveGame, api.SaveGameRequest{Id: id, Rid: w.RoomId}))
+}
+
+func (w *Worker) LoadGame(id string) (*api.LoadGameResponse, error) {
+ return api.UnwrapChecked[api.LoadGameResponse](
+ w.Send(api.LoadGame, api.LoadGameRequest{Id: id, Rid: w.RoomId}))
+}
+
+func (w *Worker) ChangePlayer(id string, index int) (*api.ChangePlayerResponse, error) {
+ return api.UnwrapChecked[api.ChangePlayerResponse](
+ w.Send(api.ChangePlayer, api.ChangePlayerRequest{
+ StatefulRoom: api.StatefulRoom{Id: id, Rid: w.RoomId},
+ Index: index,
+ }))
+}
+
+func (w *Worker) ResetGame(id string) {
+ w.Notify(api.ResetGame, api.ResetGameRequest{Id: id, Rid: w.RoomId})
+}
+
+func (w *Worker) RecordGame(id string, rec bool, recUser string) (*api.RecordGameResponse, error) {
+ return api.UnwrapChecked[api.RecordGameResponse](
+ w.Send(api.RecordGame, api.RecordGameRequest{
+ StatefulRoom: api.StatefulRoom{Id: id, Rid: w.RoomId},
+ Active: rec,
+ User: recUser,
+ }))
+}
+
+func (w *Worker) TerminateSession(id string) {
+ _, _ = w.Send(api.TerminateSession, api.TerminateSessionRequest{Id: id})
+}
diff --git a/pkg/coordinator/workerhandlers.go b/pkg/coordinator/workerhandlers.go
new file mode 100644
index 00000000..35609e06
--- /dev/null
+++ b/pkg/coordinator/workerhandlers.go
@@ -0,0 +1,39 @@
+package coordinator
+
+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, users HasUserRegistry) error {
+ if usr := users.Find(rq.Id); usr != nil {
+ usr.SendWebrtcIceCandidate(rq.Candidate)
+ } else {
+ 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
+}
diff --git a/pkg/cws/api/api.go b/pkg/cws/api/api.go
deleted file mode 100644
index d261a15a..00000000
--- a/pkg/cws/api/api.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package api
-
-import "encoding/json"
-
-// This list of postfixes is used in the API:
-// - *Request postfix denotes clients calls (i.e. from a browser to the HTTP-server).
-// - *Call postfix denotes IPC calls (from the coordinator to a worker).
-
-func from(source interface{}, data string) error {
- err := json.Unmarshal([]byte(data), source)
- if err != nil {
- return err
- }
- return nil
-}
-
-func to(target interface{}) (string, error) {
- b, err := json.Marshal(target)
- if err != nil {
- return "", err
- }
- return string(b), nil
-}
diff --git a/pkg/cws/api/coordinator.go b/pkg/cws/api/coordinator.go
deleted file mode 100644
index 2e5340fe..00000000
--- a/pkg/cws/api/coordinator.go
+++ /dev/null
@@ -1,54 +0,0 @@
-package api
-
-import "github.com/giongto35/cloud-game/v2/pkg/cws"
-
-const (
- GetRoom = "get_room"
- CloseRoom = "close_room"
- RegisterRoom = "register_room"
- Heartbeat = "heartbeat"
- IceCandidate = "ice_candidate"
-
- NoData = ""
-
- InitWebrtc = "init_webrtc"
- Answer = "answer"
-
- GameStart = "start"
- GameQuit = "quit"
- GameSave = "save"
- GameLoad = "load"
- GamePlayerSelect = "player_index"
- GameMultitap = "multitap"
-)
-
-type GameStartRequest struct {
- GameName string `json:"game_name"`
-}
-
-func (packet *GameStartRequest) From(data string) error { return from(packet, data) }
-
-type GameStartCall struct {
- Name string `json:"name"`
- Base string `json:"base"`
- Path string `json:"path"`
- Type string `json:"type"`
-}
-
-func (packet *GameStartCall) From(data string) error { return from(packet, data) }
-func (packet *GameStartCall) To() (string, error) { return to(packet) }
-
-type ConnectionRequest struct {
- Zone string `json:"zone,omitempty"`
- PingAddr string `json:"ping_addr,omitempty"`
- IsHTTPS bool `json:"is_https,omitempty"`
-}
-
-// packets
-
-func RegisterRoomPacket(data string) cws.WSPacket { return cws.WSPacket{ID: RegisterRoom, Data: data} }
-func GetRoomPacket(data string) cws.WSPacket { return cws.WSPacket{ID: GetRoom, Data: data} }
-func CloseRoomPacket(data string) cws.WSPacket { return cws.WSPacket{ID: CloseRoom, Data: data} }
-func IceCandidatePacket(data string, sessionId string) cws.WSPacket {
- return cws.WSPacket{ID: IceCandidate, Data: data, SessionID: sessionId}
-}
diff --git a/pkg/cws/api/worker.go b/pkg/cws/api/worker.go
deleted file mode 100644
index 0bd9f707..00000000
--- a/pkg/cws/api/worker.go
+++ /dev/null
@@ -1,21 +0,0 @@
-package api
-
-import "github.com/giongto35/cloud-game/v2/pkg/cws"
-
-const (
- ServerId = "server_id"
- TerminateSession = "terminateSession"
-)
-
-type ConfPushCall struct {
- Data []byte `json:"data"`
-}
-
-func (packet *ConfPushCall) From(data string) error { return from(packet, data) }
-func (packet *ConfPushCall) To() (string, error) { return to(packet) }
-
-func ServerIdPacket(id string) cws.WSPacket { return cws.WSPacket{ID: ServerId, Data: id} }
-func ConfigRequestPacket(conf []byte) cws.WSPacket { return cws.WSPacket{Data: string(conf)} }
-func TerminateSessionPacket(sessionId string) cws.WSPacket {
- return cws.WSPacket{ID: TerminateSession, SessionID: sessionId}
-}
diff --git a/pkg/cws/cws.go b/pkg/cws/cws.go
deleted file mode 100644
index 8bad0e21..00000000
--- a/pkg/cws/cws.go
+++ /dev/null
@@ -1,221 +0,0 @@
-package cws
-
-import (
- "encoding/json"
- "log"
- "runtime/debug"
- "sync"
- "time"
-
- "github.com/gofrs/uuid"
- "github.com/gorilla/websocket"
-)
-
-type (
- Client struct {
- id string
-
- conn *websocket.Conn
-
- sendLock sync.Mutex
- // sendCallback is callback based on packetID
- sendCallback map[string]func(req WSPacket)
- sendCallbackLock sync.Mutex
- // recvCallback is callback when receive based on ID of the packet
- recvCallback map[string]func(req WSPacket)
-
- Done chan struct{}
- }
-
- WSPacket struct {
- ID string `json:"id"`
- // TODO: Make Data generic: map[string]interface{} for more usecases
- Data string `json:"data"`
-
- RoomID string `json:"room_id"`
- PlayerIndex int `json:"player_index"`
-
- PacketID string `json:"packet_id"`
- // Globally ID of a browser session
- SessionID string `json:"session_id"`
- }
-
- PacketHandler func(resp WSPacket) (req WSPacket)
-)
-
-var (
- EmptyPacket = WSPacket{}
- HeartbeatPacket = WSPacket{ID: "heartbeat"}
-)
-
-const WSWait = 20 * time.Second
-
-func NewClient(conn *websocket.Conn) *Client {
- id := uuid.Must(uuid.NewV4()).String()
- sendCallback := map[string]func(WSPacket){}
- recvCallback := map[string]func(WSPacket){}
-
- return &Client{
- id: id,
- conn: conn,
-
- sendCallback: sendCallback,
- recvCallback: recvCallback,
-
- Done: make(chan struct{}),
- }
-}
-
-// Send sends a packet and trigger callback when the packet comes back
-func (c *Client) Send(request WSPacket, callback func(response WSPacket)) {
- request.PacketID = uuid.Must(uuid.NewV4()).String()
- data, err := json.Marshal(request)
- if err != nil {
- return
- }
-
- // TODO: Consider using lock free
- // Wrap callback with sessionID and packetID
- if callback != nil {
- wrapperCallback := func(resp WSPacket) {
- defer func() {
- if err := recover(); err != nil {
- log.Println("Recovered from err in client callback ", err)
- }
- }()
-
- resp.PacketID = request.PacketID
- resp.SessionID = request.SessionID
- callback(resp)
- }
- c.sendCallbackLock.Lock()
- c.sendCallback[request.PacketID] = wrapperCallback
- c.sendCallbackLock.Unlock()
- }
-
- c.sendLock.Lock()
- c.conn.SetWriteDeadline(time.Now().Add(WSWait))
- c.conn.WriteMessage(websocket.TextMessage, data)
- c.sendLock.Unlock()
-}
-
-// Receive receive and response back
-func (c *Client) Receive(id string, f PacketHandler) {
- c.recvCallback[id] = func(response WSPacket) {
- defer func() {
- if err := recover(); err != nil {
- log.Println("Recovered from err ", err)
- log.Println(debug.Stack())
- }
- }()
-
- req := f(response)
- // Add Meta data
- req.PacketID = response.PacketID
- req.SessionID = response.SessionID
-
- // Skip response if it is EmptyPacket
- if response == EmptyPacket {
- return
- }
- resp, err := json.Marshal(req)
- if err != nil {
- log.Println("[!] json marshal error:", err)
- }
- c.sendLock.Lock()
- c.conn.SetWriteDeadline(time.Now().Add(WSWait))
- c.conn.WriteMessage(websocket.TextMessage, resp)
- c.sendLock.Unlock()
- }
-}
-
-// SyncSend sends a packet and wait for callback till the packet comes back
-func (c *Client) SyncSend(request WSPacket) (response WSPacket) {
- res := make(chan WSPacket)
- f := func(resp WSPacket) {
- res <- resp
- }
- c.Send(request, f)
- return <-res
-}
-
-// SendAwait sends some packet while waiting for a tile-limited response
-//func (c *Client) SendAwait(packet WSPacket) WSPacket {
-// ch := make(chan WSPacket)
-// defer close(ch)
-// c.Send(packet, func(response WSPacket) { ch <- response })
-//
-// for {
-// select {
-// case packet := <-ch:
-// return packet
-// case <-time.After(config.WsIpcTimeout):
-// log.Printf("Packet receive timeout!")
-// return EmptyPacket
-// }
-// }
-//}
-
-// Heartbeat maintains connection to coordinator.
-// Blocking.
-func (c *Client) Heartbeat() {
- // send heartbeat every 1s
- t := time.NewTicker(time.Second)
- // don't wait 1 second
- c.Send(HeartbeatPacket, nil)
- for {
- select {
- case <-c.Done:
- t.Stop()
- log.Printf("Close heartbeat")
- return
- case <-t.C:
- c.Send(HeartbeatPacket, nil)
- }
- }
-}
-
-func (c *Client) Listen() {
- for {
- c.conn.SetReadDeadline(time.Now().Add(WSWait))
- _, rawMsg, err := c.conn.ReadMessage()
- if err != nil {
- log.Println("[!] read:", err)
- // TODO: Check explicit disconnect error to break
- close(c.Done)
- break
- }
- wspacket := WSPacket{}
- err = json.Unmarshal(rawMsg, &wspacket)
-
- if err != nil {
- log.Println("Warn: error decoding", rawMsg)
- continue
- }
-
- // Check if some async send is waiting for the response based on packetID
- // TODO: Change to read lock.
- //c.sendCallbackLock.Lock()
- callback, ok := c.sendCallback[wspacket.PacketID]
- //c.sendCallbackLock.Unlock()
- if ok {
- go callback(wspacket)
- //c.sendCallbackLock.Lock()
- delete(c.sendCallback, wspacket.PacketID)
- //c.sendCallbackLock.Unlock()
- // Skip receiveCallback to avoid duplication
- continue
- }
- // Check if some receiver with the ID is registered
- if callback, ok := c.recvCallback[wspacket.ID]; ok {
- go callback(wspacket)
- }
- }
-}
-
-func (c *Client) Close() {
- if c == nil || c.conn == nil {
- return
- }
- c.conn.Close()
-}
diff --git a/pkg/downloader/backend/backend.go b/pkg/downloader/backend/backend.go
deleted file mode 100644
index 717b0654..00000000
--- a/pkg/downloader/backend/backend.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package backend
-
-type Download struct {
- Key string
- Address string
-}
-
-type Client interface {
- Request(dest string, urls ...Download) ([]string, []string)
-}
diff --git a/pkg/downloader/backend/grab.go b/pkg/downloader/backend/grab.go
deleted file mode 100644
index 0afb9d70..00000000
--- a/pkg/downloader/backend/grab.go
+++ /dev/null
@@ -1,58 +0,0 @@
-package backend
-
-import (
- "crypto/tls"
- "log"
- "net/http"
-
- "github.com/cavaliercoder/grab"
-)
-
-type GrabDownloader struct {
- client *grab.Client
- concurrency int
-}
-
-func NewGrabDownloader() GrabDownloader {
- client := grab.Client{
- UserAgent: "Cloud-Game/2.2",
- HTTPClient: &http.Client{
- Transport: &http.Transport{
- Proxy: http.ProxyFromEnvironment,
- TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
- },
- },
- }
- return GrabDownloader{
- client: &client,
- concurrency: 5,
- }
-}
-
-func (d GrabDownloader) Request(dest string, urls ...Download) (ok []string, nook []string) {
- reqs := make([]*grab.Request, 0)
- for _, url := range urls {
- req, err := grab.NewRequest(dest, url.Address)
- if err != nil {
- log.Printf("error: couldn't make request URL: %v, %v", url, err)
- } else {
- req.Label = url.Key
- reqs = append(reqs, req)
- }
- }
-
- // check each response
- for resp := range d.client.DoBatch(d.concurrency, reqs...) {
- r := resp.Request
- if err := resp.Err(); err != nil {
- log.Printf("error: download [%s] %s failed: %v\n", r.Label, r.URL(), err)
- if resp.HTTPResponse.StatusCode == 404 {
- nook = append(nook, resp.Request.Label)
- }
- } else {
- log.Printf("Downloaded [%v] [%s] -> %s\n", resp.HTTPResponse.Status, r.Label, resp.Filename)
- ok = append(ok, resp.Filename)
- }
- }
- return
-}
diff --git a/pkg/downloader/downloader.go b/pkg/downloader/downloader.go
deleted file mode 100644
index 66697063..00000000
--- a/pkg/downloader/downloader.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package downloader
-
-import (
- "github.com/giongto35/cloud-game/v2/pkg/downloader/backend"
- "github.com/giongto35/cloud-game/v2/pkg/downloader/pipe"
-)
-
-type Downloader struct {
- backend backend.Client
- // pipe contains a sequential list of
- // operations applied to some files and
- // each operation will return a list of
- // successfully processed files
- pipe []Process
-}
-
-
-type Process func(string, []string) []string
-
-func NewDefaultDownloader() Downloader {
- return Downloader{
- backend: backend.NewGrabDownloader(),
- pipe: []Process{
- pipe.Unpack,
- pipe.Delete,
- }}
-}
-
-// Download tries to download specified with URLs list of files and
-// put them into the destination folder.
-// It will return a partial or full list of downloaded files,
-// a list of processed files if some pipe processing functions are set.
-func (d *Downloader) Download(dest string, urls ...backend.Download) ([]string, []string) {
- files, fails := d.backend.Request(dest, urls...)
- for _, op := range d.pipe {
- files = op(dest, files)
- }
- return files, fails
-}
diff --git a/pkg/downloader/pipe/pipe.go b/pkg/downloader/pipe/pipe.go
deleted file mode 100644
index 14be06fd..00000000
--- a/pkg/downloader/pipe/pipe.go
+++ /dev/null
@@ -1,29 +0,0 @@
-package pipe
-
-import (
- "os"
-
- "github.com/giongto35/cloud-game/v2/pkg/extractor"
-)
-
-func Unpack(dest string, files []string) []string {
- var res []string
- for _, file := range files {
- if unpack := extractor.NewFromExt(file); unpack != nil {
- if _, err := unpack.Extract(file, dest); err == nil {
- res = append(res, file)
- }
- }
- }
- return res
-}
-
-func Delete(_ string, files []string) []string {
- var res []string
- for _, file := range files {
- if e := os.Remove(file); e == nil {
- res = append(res, file)
- }
- }
- return res
-}
diff --git a/pkg/emulator/emulator.go b/pkg/emulator/emulator.go
deleted file mode 100644
index f9fe0a79..00000000
--- a/pkg/emulator/emulator.go
+++ /dev/null
@@ -1,43 +0,0 @@
-package emulator
-
-import "github.com/giongto35/cloud-game/v2/pkg/emulator/image"
-
-// CloudEmulator is the interface of cloud emulator. Currently NES emulator and RetroArch implements this in codebase
-type CloudEmulator interface {
- // LoadMeta returns meta data of emulator. Refer below
- LoadMeta(path string) Metadata
- // Start is called after LoadGame
- Start()
- // SetViewport sets viewport size
- SetViewport(width int, height int)
- // GetViewport debug encoder image
- GetViewport() interface{}
- // SaveGame save game state, saveExtraFunc is callback to do extra step. Ex: save to google cloud
- SaveGame(saveExtraFunc func() error) error
- // LoadGame load game state
- LoadGame() error
- // GetHashPath returns the path emulator will save state to
- GetHashPath() string
- // Close will be called when the game is done
- Close()
-
- ToggleMultitap() error
-}
-
-type Metadata struct {
- // the full path to some emulator lib
- LibPath string
- // the full path to the emulator config
- ConfigPath string
-
- AudioSampleRate int
- Fps float64
- BaseWidth int
- BaseHeight int
- Ratio float64
- Rotation image.Rotate
- IsGlAllowed bool
- UsesLibCo bool
- AutoGlContext bool
- HasMultitap bool
-}
diff --git a/pkg/emulator/graphics/context.go b/pkg/emulator/graphics/context.go
deleted file mode 100644
index 6ac446c7..00000000
--- a/pkg/emulator/graphics/context.go
+++ /dev/null
@@ -1,18 +0,0 @@
-package graphics
-
-import "math"
-
-type Context int
-
-const (
- CtxNone Context = iota
- CtxOpenGl
- CtxOpenGlEs2
- CtxOpenGlCore
- CtxOpenGlEs3
- CtxOpenGlEsVersion
- CtxVulkan
-
- CtxUnknown = math.MaxInt32 - 1
- CtxDummy = math.MaxInt32
-)
diff --git a/pkg/emulator/graphics/opengl.go b/pkg/emulator/graphics/opengl.go
deleted file mode 100644
index 83f76990..00000000
--- a/pkg/emulator/graphics/opengl.go
+++ /dev/null
@@ -1,143 +0,0 @@
-package graphics
-
-import (
- "log"
- "unsafe"
-
- "github.com/go-gl/gl/v2.1/gl"
-)
-
-type offscreenSetup struct {
- tex uint32
- fbo uint32
- rbo uint32
-
- width int32
- height int32
-
- pixType uint32
- pixFormat uint32
-
- hasDepth bool
- hasStencil bool
-}
-
-var opt = offscreenSetup{}
-
-// OpenGL pixel format
-type PixelFormat int
-
-const (
- UnsignedShort5551 PixelFormat = iota
- UnsignedShort565
- UnsignedInt8888Rev
-)
-
-func initContext(getProcAddr func(name string) unsafe.Pointer) {
- if err := gl.InitWithProcAddrFunc(getProcAddr); err != nil {
- panic(err)
- }
-}
-
-func initFramebuffer(w int, h int, hasDepth bool, hasStencil bool) {
- opt.width = int32(w)
- opt.height = int32(h)
- opt.hasDepth = hasDepth
- opt.hasStencil = hasStencil
-
- // texture init
- gl.GenTextures(1, &opt.tex)
- gl.BindTexture(gl.TEXTURE_2D, opt.tex)
-
- gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
- gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
-
- gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, opt.width, opt.height, 0, opt.pixType, opt.pixFormat, nil)
- gl.BindTexture(gl.TEXTURE_2D, 0)
-
- // framebuffer init
- gl.GenFramebuffers(1, &opt.fbo)
- gl.BindFramebuffer(gl.FRAMEBUFFER, opt.fbo)
-
- gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, opt.tex, 0)
-
- // more buffers init
- if opt.hasDepth {
- gl.GenRenderbuffers(1, &opt.rbo)
- gl.BindRenderbuffer(gl.RENDERBUFFER, opt.rbo)
- if opt.hasStencil {
- gl.RenderbufferStorage(gl.RENDERBUFFER, gl.DEPTH24_STENCIL8, opt.width, opt.height)
- gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, opt.rbo)
- } else {
- gl.RenderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT24, opt.width, opt.height)
- gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, opt.rbo)
- }
- gl.BindRenderbuffer(gl.RENDERBUFFER, 0)
- }
-
- status := gl.CheckFramebufferStatus(gl.FRAMEBUFFER)
- if status != gl.FRAMEBUFFER_COMPLETE {
- if e := gl.GetError(); e != gl.NO_ERROR {
- log.Printf("[OpenGL] GL error: 0x%X, Frame status: 0x%X", e, status)
- panic("OpenGL error")
- }
- log.Printf("[OpenGL] frame status: 0x%X", status)
- panic("OpenGL framebuffer is invalid")
- }
-}
-
-func destroyFramebuffer() {
- if opt.hasDepth {
- gl.DeleteRenderbuffers(1, &opt.rbo)
- }
- gl.DeleteFramebuffers(1, &opt.fbo)
- gl.DeleteTextures(1, &opt.tex)
-}
-
-func ReadFramebuffer(bytes int, w int, h int) []byte {
- data := make([]byte, bytes)
- gl.BindFramebuffer(gl.FRAMEBUFFER, opt.fbo)
- gl.ReadPixels(0, 0, int32(w), int32(h), opt.pixType, opt.pixFormat, gl.Ptr(&data[0]))
- gl.BindFramebuffer(gl.FRAMEBUFFER, 0)
- return data
-}
-
-func getFbo() uint32 {
- return opt.fbo
-}
-
-func SetPixelFormat(format PixelFormat) {
- switch format {
- case UnsignedShort5551:
- opt.pixFormat = gl.UNSIGNED_SHORT_5_5_5_1
- opt.pixType = gl.BGRA
- case UnsignedShort565:
- opt.pixFormat = gl.UNSIGNED_SHORT_5_6_5
- opt.pixType = gl.RGB
- case UnsignedInt8888Rev:
- opt.pixFormat = gl.UNSIGNED_INT_8_8_8_8_REV
- opt.pixType = gl.BGRA
- default:
- log.Fatalf("[opengl] Error! Unknown pixel type %v", format)
- }
-}
-
-// PrintDriverInfo prints OpenGL information.
-func PrintDriverInfo() {
- // OpenGL info
- log.Printf("[OpenGL] Version: %v", get(gl.VERSION))
- log.Printf("[OpenGL] Vendor: %v", get(gl.VENDOR))
- // This string is often the name of the GPU.
- // In the case of Mesa3d, it would be i.e "Gallium 0.4 on NVA8".
- // It might even say "Direct3D" if the Windows Direct3D wrapper is being used.
- log.Printf("[OpenGL] Renderer: %v", get(gl.RENDERER))
- log.Printf("[OpenGL] GLSL Version: %v", get(gl.SHADING_LANGUAGE_VERSION))
-}
-
-func getDriverError() uint32 {
- return gl.GetError()
-}
-
-func get(name uint32) string {
- return gl.GoStr(gl.GetString(name))
-}
diff --git a/pkg/emulator/graphics/sdl.go b/pkg/emulator/graphics/sdl.go
deleted file mode 100644
index 7a67d63b..00000000
--- a/pkg/emulator/graphics/sdl.go
+++ /dev/null
@@ -1,132 +0,0 @@
-package graphics
-
-import (
- "log"
- "unsafe"
-
- "github.com/giongto35/cloud-game/v2/pkg/thread"
- "github.com/veandco/go-sdl2/sdl"
-)
-
-type data struct {
- w *sdl.Window
- glWCtx sdl.GLContext
-}
-
-// singleton state for SDL
-var state = data{}
-
-type Config struct {
- Ctx Context
- W int
- H int
- Gl GlConfig
-}
-type GlConfig struct {
- AutoContext bool
- VersionMajor uint
- VersionMinor uint
- HasDepth bool
- HasStencil bool
-}
-
-// Init initializes SDL/OpenGL context.
-// Uses main thread lock (see thread/mainthread).
-func Init(cfg Config) {
- log.Printf("[SDL] [OpenGL] initialization...")
- if err := sdl.Init(sdl.INIT_VIDEO); err != nil {
- log.Printf("[SDL] error: %v", err)
- panic("SDL initialization failed")
- }
-
- if cfg.Gl.AutoContext {
- log.Printf("[OpenGL] CONTEXT_AUTO (type: %v v%v.%v)", cfg.Ctx, cfg.Gl.VersionMajor, cfg.Gl.VersionMinor)
- } else {
- switch cfg.Ctx {
- case CtxOpenGlCore:
- setAttribute(sdl.GL_CONTEXT_PROFILE_MASK, sdl.GL_CONTEXT_PROFILE_CORE)
- log.Printf("[OpenGL] CONTEXT_PROFILE_CORE")
- case CtxOpenGlEs2:
- setAttribute(sdl.GL_CONTEXT_PROFILE_MASK, sdl.GL_CONTEXT_PROFILE_ES)
- setAttribute(sdl.GL_CONTEXT_MAJOR_VERSION, 3)
- setAttribute(sdl.GL_CONTEXT_MINOR_VERSION, 0)
- log.Printf("[OpenGL] CONTEXT_PROFILE_ES 3.0")
- case CtxOpenGl:
- if cfg.Gl.VersionMajor >= 3 {
- setAttribute(sdl.GL_CONTEXT_PROFILE_MASK, sdl.GL_CONTEXT_PROFILE_COMPATIBILITY)
- }
- log.Printf("[OpenGL] CONTEXT_PROFILE_COMPATIBILITY")
- default:
- log.Printf("Unsupported hw context: %v", cfg.Ctx)
- }
- }
-
- // In OSX 10.14+ window creation and context creation must happen in the main thread
- thread.MainMaybe(createWindow)
-
- BindContext()
-
- initContext(sdl.GLGetProcAddress)
- PrintDriverInfo()
- initFramebuffer(cfg.W, cfg.H, cfg.Gl.HasDepth, cfg.Gl.HasStencil)
-}
-
-// Deinit destroys SDL/OpenGL context.
-// Uses main thread lock (see thread/mainthread).
-func Deinit() {
- log.Printf("[SDL] [OpenGL] deinitialization...")
- destroyFramebuffer()
- // In OSX 10.14+ window deletion must happen in the main thread
- thread.MainMaybe(destroyWindow)
- sdl.Quit()
- log.Printf("[SDL] [OpenGL] deinitialized (%v, %v)", sdl.GetError(), getDriverError())
-}
-
-// createWindow creates fake SDL window for OpenGL initialization purposes.
-func createWindow() {
- var winTitle = "CloudRetro dummy window"
- var winWidth, winHeight int32 = 1, 1
-
- var err error
- if state.w, err = sdl.CreateWindow(
- winTitle,
- sdl.WINDOWPOS_UNDEFINED, sdl.WINDOWPOS_UNDEFINED,
- winWidth, winHeight,
- sdl.WINDOW_OPENGL|sdl.WINDOW_HIDDEN,
- ); err != nil {
- panic(err)
- }
- if state.glWCtx, err = state.w.GLCreateContext(); err != nil {
- panic(err)
- }
-}
-
-// destroyWindow destroys previously created SDL window.
-func destroyWindow() {
- BindContext()
- sdl.GLDeleteContext(state.glWCtx)
- if err := state.w.Destroy(); err != nil {
- log.Printf("[SDL] couldn't destroy the window, error: %v", err)
- }
-}
-
-// BindContext explicitly binds context to current thread.
-func BindContext() {
- if err := state.w.GLMakeCurrent(state.glWCtx); err != nil {
- log.Printf("[SDL] error: %v", err)
- }
-}
-
-func GetGlFbo() uint32 {
- return getFbo()
-}
-
-func GetGlProcAddress(proc string) unsafe.Pointer {
- return sdl.GLGetProcAddress(proc)
-}
-
-func setAttribute(attr sdl.GLattr, value int) {
- if err := sdl.GLSetAttribute(attr, value); err != nil {
- log.Printf("[SDL] attribute error: %v", err)
- }
-}
diff --git a/pkg/emulator/image/color.go b/pkg/emulator/image/color.go
deleted file mode 100644
index 8ed950f5..00000000
--- a/pkg/emulator/image/color.go
+++ /dev/null
@@ -1,36 +0,0 @@
-package image
-
-import (
- "image/color"
-)
-
-const (
- // BIT_FORMAT_SHORT_5_5_5_1 has 5 bits R, 5 bits G, 5 bits B, 1 bit alpha
- BitFormatShort5551 = iota
- // BIT_FORMAT_INT_8_8_8_8_REV has 8 bits R, 8 bits G, 8 bits B, 8 bit alpha
- BitFormatInt8888Rev
- // BIT_FORMAT_SHORT_5_6_5 has 5 bits R, 6 bits G, 5 bits
- BitFormatShort565
-)
-
-type Format func(data []byte, index int) color.RGBA
-
-func Rgb565(data []byte, index int) color.RGBA {
- pixel := (int)(data[index]) + ((int)(data[index+1]) << 8)
-
- return color.RGBA{
- R: byte(((pixel>>11)*255 + 15) / 31),
- G: byte((((pixel>>5)&0x3F)*255 + 31) / 63),
- B: byte(((pixel&0x1F)*255 + 15) / 31),
- A: 255,
- }
-}
-
-func Rgba8888(data []byte, index int) color.RGBA {
- return color.RGBA{
- R: data[index+2],
- G: data[index+1],
- B: data[index],
- A: data[index+3],
- }
-}
diff --git a/pkg/emulator/image/draw.go b/pkg/emulator/image/draw.go
deleted file mode 100644
index 08aa21e0..00000000
--- a/pkg/emulator/image/draw.go
+++ /dev/null
@@ -1,63 +0,0 @@
-package image
-
-import (
- "image"
-)
-
-type imageCache struct {
- image *image.RGBA
- w int
- h int
-}
-
-var canvas = imageCache{
- image.NewRGBA(image.Rectangle{}),
- 0,
- 0,
-}
-
-func DrawRgbaImage(pixFormat Format, rotationFn Rotate, scaleType int, flipV bool, w, h, packedW, bpp int, data []byte, dest *image.RGBA) {
- if pixFormat == nil {
- dest = nil
- }
-
- // !to implement own image interfaces img.Pix = bytes[]
- ww, hh := w, h
- if rotationFn.IsEven {
- ww, hh = hh, ww
- }
- src := getCanvas(ww, hh)
-
- drawImage(pixFormat, w, h, packedW, bpp, flipV, rotationFn, data, src)
- Resize(scaleType, src, dest)
-}
-
-func drawImage(toRGBA Format, w, h, packedW, bpp int, flipV bool, rotationFn Rotate, data []byte, image *image.RGBA) {
- for y := 0; y < h; y++ {
- yy := y
- if flipV {
- yy = (h - 1) - y
- }
- for x := 0; x < w; x++ {
- src := toRGBA(data, (x+y*packedW)*bpp)
- dx, dy := rotationFn.Call(x, yy, w, h)
- i := dx*4 + dy*image.Stride
- dst := image.Pix[i : i+4 : i+4]
- dst[0] = src.R
- dst[1] = src.G
- dst[2] = src.B
- dst[3] = src.A
- }
- }
-}
-
-func getCanvas(w, h int) *image.RGBA {
- if canvas.w == w && canvas.h == h {
- return canvas.image
- }
-
- canvas.w, canvas.h = w, h
- canvas.image = image.NewRGBA(image.Rect(0, 0, w, h))
-
- return canvas.image
-}
diff --git a/pkg/emulator/image/rotation.go b/pkg/emulator/image/rotation.go
deleted file mode 100644
index 5f6e0bfd..00000000
--- a/pkg/emulator/image/rotation.go
+++ /dev/null
@@ -1,96 +0,0 @@
-// This package contains functions for
-// Pi/2 step rotation of points in a 2-dimensional space.
-package image
-
-type Angle uint
-
-const (
- Angle0 Angle = iota
- Angle90
- Angle180
- Angle270
-)
-
-// A helper to choose appropriate rotation by its angle
-var Angles = [4]Rotate{
- Angle0: {Call: Rotate0, IsEven: false},
- Angle90: {Call: Rotate90, IsEven: true},
- Angle180: {Call: Rotate180, IsEven: false},
- Angle270: {Call: Rotate270, IsEven: true},
-}
-
-func GetRotation(angle Angle) Rotate {
- return Angles[angle]
-}
-
-// An interface for rotation of a given point
-// with the coordinates x, y in the matrix of w x h.
-// Returns a pair of new coordinates x, y in the resulting
-// matrix.
-// Be aware that w / h values are 0 index-based and
-// it's meant to be used with h corresponded
-// to matrix height and y coordinate, and with w to x coordinate.
-type Rotate struct {
- Call func(x, y, w, h int) (int, int)
- IsEven bool
-}
-
-// 0° or the original orientation
-/* Example: */
-/* 1 2 3 1 2 3 */
-/* 4 5 6 -> 4 5 6 */
-/* 7 8 9 7 8 9 */
-func Rotate0(x, y, _, _ int) (int, int) {
- return x, y
-}
-
-// 90° CCW or 270° CW
-/* Example: */
-/* 1 2 3 3 6 9 */
-/* 4 5 6 -> 2 5 8 */
-/* 7 8 9 1 4 7 */
-func Rotate90(x, y, w, _ int) (int, int) {
- return y, (w - 1) - x
-}
-
-// 180° CCW
-/* Example: */
-/* 1 2 3 9 8 7 */
-/* 4 5 6 -> 6 5 4 */
-/* 7 8 9 3 2 1 */
-func Rotate180(x, y, w, h int) (int, int) {
- return (w - 1) - x, (h - 1) - y
-}
-
-// 270° CCW or 90° CW
-/* Example: */
-/* 1 2 3 7 4 1 */
-/* 4 5 6 -> 8 5 2 */
-/* 7 8 9 9 6 3 */
-func Rotate270(x, y, _, h int) (int, int) {
- return (h - 1) - y, x
-}
-
-/*
-[1 2 3 4 5 6 7 8 9]
-[7 4 1 8 5 2 9 6 3]
-*/
-func ExampleRotate(data []uint8, w int, h int, angle Angle) []uint8 {
- dest := make([]uint8, len(data))
- rotationFn := Angles[angle]
-
- for y := 0; y < h; y++ {
- for x := 0; x < w; x++ {
- nx, ny := rotationFn.Call(x, y, w, h)
- stride := w
- if rotationFn.IsEven {
- stride = h
- }
- //fmt.Printf("%v:%v (%v) -> %v:%v (%v)\n", x, y, n1, nx, ny, n2)
-
- dest[nx+ny*stride] = data[x+y*w]
- }
- }
-
- return dest
-}
diff --git a/pkg/emulator/image/rotation_test.go b/pkg/emulator/image/rotation_test.go
deleted file mode 100644
index ec00ea6c..00000000
--- a/pkg/emulator/image/rotation_test.go
+++ /dev/null
@@ -1,247 +0,0 @@
-package image
-
-import (
- "bytes"
- "testing"
-)
-
-type dimensions struct {
- w int
- h int
-}
-
-func TestRotate(t *testing.T) {
- tests := []struct {
- // packed bytes from a 2D matrix
- input []byte
- // original matrix's width
- w int
- // original matrix's height
- h int
- // rotation algorithm
- rotateHow []Angle
- expected [][]byte
- }{
- {
- // a cross
- []byte{
- 0, 1, 0,
- 1, 1, 1,
- 0, 1, 0,
- },
- 3, 3, []Angle{Angle0, Angle90, Angle180, Angle270},
- [][]byte{
- {
- 0, 1, 0,
- 1, 1, 1,
- 0, 1, 0,
- },
- {
- 0, 1, 0,
- 1, 1, 1,
- 0, 1, 0,
- },
- {
- 0, 1, 0,
- 1, 1, 1,
- 0, 1, 0,
- },
- {
- 0, 1, 0,
- 1, 1, 1,
- 0, 1, 0,
- },
- },
- },
- {
- []byte{
- 1, 2,
- 3, 4,
- 5, 6,
- 7, 8,
- },
- 2, 4, []Angle{Angle0, Angle90, Angle180, Angle270},
- [][]byte{
- {
- 1, 2,
- 3, 4,
- 5, 6,
- 7, 8,
- },
- {
- 2, 4, 6, 8,
- 1, 3, 5, 7,
- },
- {
- 8, 7,
- 6, 5,
- 4, 3,
- 2, 1,
- },
- {
- 7, 5, 3, 1,
- 8, 6, 4, 2,
- },
- },
- },
- {
- // a square
- []byte{
- 1, 0, 0, 0, 0, 0, 0, 0,
- 0, 1, 1, 1, 1, 1, 1, 0,
- 0, 1, 1, 1, 1, 1, 1, 0,
- 0, 1, 0, 0, 0, 0, 1, 0,
- 0, 1, 1, 1, 1, 1, 1, 0,
- 0, 0, 0, 0, 0, 0, 0, 1,
- },
- 8, 6, []Angle{Angle0, Angle90, Angle180, Angle270},
- [][]byte{
- {
- // L // R
- 1, 0, 0, 0, 0, 0, 0, 0,
- 0, 1, 1, 1, 1, 1, 1, 0,
- 0, 1, 1, 1, 1, 1, 1, 0,
- 0, 1, 0, 0, 0, 0, 1, 0,
- 0, 1, 1, 1, 1, 1, 1, 0,
- 0, 0, 0, 0, 0, 0, 0, 1,
- },
- {
- 0, 0, 0, 0, 0, 1,
- 0, 1, 1, 1, 1, 0,
- 0, 1, 1, 0, 1, 0,
- 0, 1, 1, 0, 1, 0,
- 0, 1, 1, 0, 1, 0,
- 0, 1, 1, 0, 1, 0,
- 0, 1, 1, 1, 1, 0,
- 1, 0, 0, 0, 0, 0,
- },
-
- {
- 1, 0, 0, 0, 0, 0, 0, 0,
- 0, 1, 1, 1, 1, 1, 1, 0,
- 0, 1, 0, 0, 0, 0, 1, 0,
- 0, 1, 1, 1, 1, 1, 1, 0,
- 0, 1, 1, 1, 1, 1, 1, 0,
- 0, 0, 0, 0, 0, 0, 0, 1,
- },
- {
- 0, 0, 0, 0, 0, 1,
- 0, 1, 1, 1, 1, 0,
- 0, 1, 0, 1, 1, 0,
- 0, 1, 0, 1, 1, 0,
- 0, 1, 0, 1, 1, 0,
- 0, 1, 0, 1, 1, 0,
- 0, 1, 1, 1, 1, 0,
- 1, 0, 0, 0, 0, 0,
- },
- },
- },
- }
-
- for _, test := range tests {
- for i, rot := range test.rotateHow {
- if output := ExampleRotate(test.input, test.w, test.h, rot); !bytes.Equal(output, test.expected[i]) {
- t.Errorf(
- "Test fail for angle %v with %v that should be \n%v but it's \n%v",
- rot, test.input, test.expected[i], output)
- }
- }
- }
-}
-
-func TestBoundsAfterRotation(t *testing.T) {
- tests := []struct {
- dim []dimensions
- rotateHow []Angle
- }{
- {
- // a combinatorics lib would be nice instead
- []dimensions{
- // square
- {w: 100, h: 100},
- // even w/h
- {w: 100, h: 50},
- // even h/w
- {w: 50, h: 100},
- // odd even w/h
- {w: 77, h: 32},
- // even odd h/w
- {w: 32, h: 77},
- // just odd
- {w: 13, h: 19},
- },
- []Angle{Angle0, Angle90, Angle180, Angle270},
- },
- }
-
- for _, test := range tests {
- for _, rot := range test.rotateHow {
- rotationFn := Angles[rot]
- for _, dim := range test.dim {
-
- for y := 0; y < dim.h; y++ {
- for x := 0; x < dim.w; x++ {
-
- xx, yy := rotationFn.Call(x, y, dim.w, dim.h)
-
- if rotationFn.IsEven {
- yy, xx = xx, yy
- }
-
- if xx < 0 || xx > dim.w {
- t.Errorf("Rot %v, coordinate x should be in range [0; %v]: %v", rot, dim.w-1, xx)
- }
-
- if yy < 0 || yy > dim.h {
- t.Errorf("Rot %v, coordinate y should be in range [0; %v]: %v", rot, dim.h-1, yy)
- }
- }
- }
- }
- }
- }
-}
-
-func BenchmarkDirect(b *testing.B) {
- for i := 0; i < b.N; i++ {
- _, _ = Rotate90(1, 1, 2, 2)
- }
-}
-
-func BenchmarkLiteral(b *testing.B) {
- fn := Rotate90
- for i := 0; i < b.N; i++ {
- _, _ = fn(1, 1, 2, 2)
- }
-}
-
-func BenchmarkAssign(b *testing.B) {
- fn := Angles[Angle90].Call
- for i := 0; i < b.N; i++ {
- _, _ = fn(1, 1, 2, 2)
- }
-}
-
-func BenchmarkMapReassign(b *testing.B) {
- fn := Angles[Angle90].Call
- for i := 0; i < b.N; i++ {
- fn2 := fn
- _, _ = fn2(1, 1, 2, 2)
- }
-}
-
-func BenchmarkMapDirect(b *testing.B) {
- for i := 0; i < b.N; i++ {
- _, _ = Angles[Angle90].Call(1, 1, 2, 2)
- }
-}
-
-func BenchmarkNewMapDirect(b *testing.B) {
- fns := map[Angle]func(x, y, w, h int) (int, int){
- Angle90: Rotate90,
- }
-
- for i := 0; i < b.N; i++ {
- _, _ = fns[Angle90](1, 1, 2, 2)
- }
-}
diff --git a/pkg/emulator/image/scale.go b/pkg/emulator/image/scale.go
deleted file mode 100644
index 8ab8dbf5..00000000
--- a/pkg/emulator/image/scale.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package image
-
-import (
- "image"
-
- "golang.org/x/image/draw"
-)
-
-const (
- // skips image interpolation
- ScaleNot = iota
- // nearest neighbour interpolation
- ScaleNearestNeighbour
- // bilinear interpolation
- ScaleBilinear
-)
-
-func Resize(scaleType int, src *image.RGBA, out *image.RGBA) {
- // !to do set it once instead switching on each iteration
- // !to do skip resize if w=vw h=vh
- switch scaleType {
- case ScaleBilinear:
- draw.ApproxBiLinear.Scale(out, out.Bounds(), src, src.Bounds(), draw.Src, nil)
- case ScaleNot:
- fallthrough
- case ScaleNearestNeighbour:
- fallthrough
- default:
- draw.NearestNeighbor.Scale(out, out.Bounds(), src, src.Bounds(), draw.Src, nil)
- }
-}
diff --git a/pkg/emulator/libretro/core/core.go b/pkg/emulator/libretro/core/core.go
deleted file mode 100644
index 02c9ad02..00000000
--- a/pkg/emulator/libretro/core/core.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package core
-
-import (
- "errors"
- "runtime"
-)
-
-// See: https://gist.github.com/asukakenji/f15ba7e588ac42795f421b48b8aede63.
-var libretroOsArchMap = map[string]ArchInfo{
- "linux:amd64": {Os: "linux", Arch: "x86_64", LibExt: ".so"},
- "linux:arm": {Os: "linux", Arch: "armv7-neon-hf", LibExt: ".armv7-neon-hf.so"},
- "windows:amd64": {Os: "windows", Arch: "x86_64", LibExt: ".dll"},
- "darwin:amd64": {Os: "osx", Arch: "x86_64", Vendor: "apple", LibExt: ".dylib"},
-}
-
-// ArchInfo contains Libretro core lib platform info.
-// And cores are just C-compiled libraries.
-// See: https://buildbot.libretro.com/nightly.
-type ArchInfo 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 GetCoreExt() (ArchInfo, error) {
- key := runtime.GOOS + ":" + runtime.GOARCH
- if arch, ok := libretroOsArchMap[key]; ok {
- return arch, nil
- } else {
- return ArchInfo{}, errors.New("core mapping not found for " + key)
- }
-}
diff --git a/pkg/emulator/libretro/manager/manager.go b/pkg/emulator/libretro/manager/manager.go
deleted file mode 100644
index 3a89ae8b..00000000
--- a/pkg/emulator/libretro/manager/manager.go
+++ /dev/null
@@ -1,42 +0,0 @@
-package manager
-
-import (
- "io/ioutil"
- "log"
- "path/filepath"
- "strings"
-
- "github.com/giongto35/cloud-game/v2/pkg/config/emulator"
- "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/core"
-)
-
-type Manager interface {
- Sync() error
-}
-
-type BasicManager struct {
- Conf emulator.LibretroConfig
-}
-
-func (m BasicManager) GetInstalled() (installed []string) {
- dir := m.Conf.GetCoresStorePath()
- arch, err := core.GetCoreExt()
- if err != nil {
- log.Printf("error: %v", err)
- return
- }
-
- files, err := ioutil.ReadDir(dir)
- if err != nil {
- log.Printf("error: couldn't get installed cores, %v", err)
- return
- }
-
- for _, file := range files {
- name := file.Name()
- if filepath.Ext(name) == arch.LibExt {
- installed = append(installed, strings.TrimSuffix(name, arch.LibExt))
- }
- }
- return
-}
diff --git a/pkg/emulator/libretro/manager/remotehttp/manager.go b/pkg/emulator/libretro/manager/remotehttp/manager.go
deleted file mode 100644
index f05ba802..00000000
--- a/pkg/emulator/libretro/manager/remotehttp/manager.go
+++ /dev/null
@@ -1,116 +0,0 @@
-package remotehttp
-
-import (
- "log"
- "os"
- "strings"
-
- "github.com/giongto35/cloud-game/v2/pkg/config/emulator"
- "github.com/giongto35/cloud-game/v2/pkg/downloader"
- "github.com/giongto35/cloud-game/v2/pkg/downloader/backend"
- "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/core"
- "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/manager"
- "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/repo"
- "github.com/gofrs/flock"
-)
-
-type Manager struct {
- manager.BasicManager
-
- arch core.ArchInfo
- repo repo.Repository
- client downloader.Downloader
- fmu *flock.Flock
-}
-
-func NewRemoteHttpManager(conf emulator.LibretroConfig) Manager {
- repoConf := conf.Cores.Repo.Main
- // used for synchronization of multiple process
- fileLock := conf.Cores.Repo.ExtLock
- if fileLock == "" {
- fileLock = os.TempDir() + string(os.PathSeparator) + "cloud_game.lock"
- }
-
- arch, err := core.GetCoreExt()
- if err != nil {
- log.Printf("error: %v", err)
- }
-
- return Manager{
- BasicManager: manager.BasicManager{
- Conf: conf,
- },
- arch: arch,
- repo: repo.New(repoConf.Type, repoConf.Url, repoConf.Compression, "buildbot"),
- client: downloader.NewDefaultDownloader(),
- fmu: flock.New(fileLock),
- }
-}
-
-func (m *Manager) Sync() error {
- declared := m.Conf.GetCores()
-
- // IPC lock if multiple worker processes on the same machine
- m.fmu.Lock()
- defer m.fmu.Unlock()
-
- installed := m.GetInstalled()
- download := diff(declared, installed)
-
- _, failed := m.download(download)
- if len(failed) > 0 {
- log.Printf("[core-dl] error: unable to download some cores, trying 2nd repository")
- conf := m.Conf.Cores.Repo.Secondary
- if conf.Type != "" {
- if fallback := repo.New(conf.Type, conf.Url, conf.Compression, ""); fallback != nil {
- defer m.setRepo(m.repo)
- m.setRepo(fallback)
- _, _ = m.download(failed)
- }
- }
- }
-
- return nil
-}
-
-func (m *Manager) getCoreUrls(names []string, repo repo.Repository) (urls []backend.Download) {
- for _, c := range names {
- urls = append(urls, backend.Download{Key: c, Address: repo.GetCoreUrl(c, m.arch)})
- }
- return
-}
-
-func (m *Manager) setRepo(repo repo.Repository) {
- m.repo = repo
-}
-
-func (m *Manager) download(cores []string) (succeeded []string, failed []string) {
- if len(cores) > 0 && m.repo != nil {
- dir := m.Conf.GetCoresStorePath()
- log.Printf("[core-dl] <<< download: %v", strings.Join(cores, ", "))
- _, failed = m.client.Download(dir, m.getCoreUrls(cores, m.repo)...)
- }
- return
-}
-
-// diff returns a list of not installed cores.
-func diff(declared, installed []string) (diff []string) {
- if len(declared) == 0 {
- return
- }
-
- if len(installed) == 0 {
- return declared
- }
-
- v := map[string]struct{}{}
- for _, x := range installed {
- v[x] = struct{}{}
- }
- for _, x := range declared {
- if _, ok := v[x]; !ok {
- diff = append(diff, x)
- }
- }
- return
-}
diff --git a/pkg/emulator/libretro/nanoarch/cfuncs.go b/pkg/emulator/libretro/nanoarch/cfuncs.go
deleted file mode 100644
index 936b12a4..00000000
--- a/pkg/emulator/libretro/nanoarch/cfuncs.go
+++ /dev/null
@@ -1,204 +0,0 @@
-package nanoarch
-
-/*
-#include "libretro.h"
-#include
-#include
-#include
-#include
-
-void coreLog(enum retro_log_level level, const char *msg);
-
-void bridge_retro_init(void *f) {
- coreLog(RETRO_LOG_INFO, "[Libretro] Initialization...\n");
- return ((void (*)(void))f)();
-}
-
-void bridge_retro_deinit(void *f) {
- coreLog(RETRO_LOG_INFO, "[Libretro] Deinitialiazation...\n");
- return ((void (*)(void))f)();
-}
-
-unsigned bridge_retro_api_version(void *f) {
- return ((unsigned (*)(void))f)();
-}
-
-void bridge_retro_get_system_info(void *f, struct retro_system_info *si) {
- return ((void (*)(struct retro_system_info *))f)(si);
-}
-
-void bridge_retro_get_system_av_info(void *f, struct retro_system_av_info *si) {
- return ((void (*)(struct retro_system_av_info *))f)(si);
-}
-
-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);
-}
-
-bool bridge_retro_load_game(void *f, struct retro_game_info *gi) {
- coreLog(RETRO_LOG_INFO, "[Libretro] Loading the game...\n");
- return ((bool (*)(struct retro_game_info *))f)(gi);
-}
-
-void bridge_retro_unload_game(void *f) {
- coreLog(RETRO_LOG_INFO, "[Libretro] Unloading the game...\n");
- return ((void (*)(void))f)();
-}
-
-void bridge_retro_run(void *f) {
- return ((void (*)(void))f)();
-}
-
-size_t bridge_retro_get_memory_size(void *f, unsigned id) {
- return ((size_t (*)(unsigned))f)(id);
-}
-
-void* bridge_retro_get_memory_data(void *f, unsigned id) {
- return ((void* (*)(unsigned))f)(id);
-}
-
-size_t bridge_retro_serialize_size(void *f) {
- return ((size_t (*)(void))f)();
-}
-
-bool bridge_retro_serialize(void *f, void *data, size_t size) {
- return ((bool (*)(void*, size_t))f)(data, size);
-}
-
-bool bridge_retro_unserialize(void *f, void *data, size_t size) {
- return ((bool (*)(void*, size_t))f)(data, size);
-}
-
-void bridge_retro_set_controller_port_device(void *f, unsigned port, unsigned device) {
- return ((void (*)(unsigned, unsigned))f)(port, device);
-}
-
-bool coreEnvironment_cgo(unsigned cmd, void *data) {
- bool coreEnvironment(unsigned, void*);
- return coreEnvironment(cmd, data);
-}
-
-void coreVideoRefresh_cgo(void *data, unsigned width, unsigned height, size_t pitch) {
- void coreVideoRefresh(void*, unsigned, unsigned, size_t);
- return coreVideoRefresh(data, width, height, pitch);
-}
-
-void coreInputPoll_cgo() {
- void coreInputPoll();
- return coreInputPoll();
-}
-
-int16_t coreInputState_cgo(unsigned port, unsigned device, unsigned index, unsigned id) {
- int16_t coreInputState(unsigned, unsigned, unsigned, unsigned);
- return coreInputState(port, device, index, id);
-}
-
-void coreAudioSample_cgo(int16_t left, int16_t right) {
- void coreAudioSample(int16_t, int16_t);
- coreAudioSample(left, right);
-}
-
-size_t coreAudioSampleBatch_cgo(const int16_t *data, size_t frames) {
- size_t coreAudioSampleBatch(const int16_t*, size_t);
- return coreAudioSampleBatch(data, frames);
-}
-
-void coreLog_cgo(enum retro_log_level level, const char *fmt, ...) {
- char msg[4096] = {0};
- va_list va;
- va_start(va, fmt);
- vsnprintf(msg, sizeof(msg), fmt, va);
- va_end(va);
-
- coreLog(level, msg);
-}
-
-uintptr_t coreGetCurrentFramebuffer_cgo() {
- uintptr_t coreGetCurrentFramebuffer();
- return coreGetCurrentFramebuffer();
-}
-
-retro_proc_address_t coreGetProcAddress_cgo(const char *sym) {
- retro_proc_address_t coreGetProcAddress(const char *sym);
- return coreGetProcAddress(sym);
-}
-
-void bridge_context_reset(retro_hw_context_reset_t f) {
- f();
-}
-
-void initVideo_cgo() {
- void initVideo();
- return initVideo();
-}
-
-void deinitVideo_cgo() {
- void deinitVideo();
- return deinitVideo();
-}
-
-void* function;
-pthread_t thread;
-int initialized = 0;
-pthread_mutex_t run_mutex;
-pthread_cond_t run_cv;
-pthread_mutex_t done_mutex;
-pthread_cond_t done_cv;
-
-void *run_loop(void *unused) {
- pthread_mutex_lock(&done_mutex);
- pthread_mutex_lock(&run_mutex);
- pthread_cond_signal(&done_cv);
- pthread_mutex_unlock(&done_mutex);
- while(1) {
- pthread_cond_wait(&run_cv, &run_mutex);
- ((void (*)(void))function)();
- pthread_mutex_lock(&done_mutex);
- pthread_cond_signal(&done_cv);
- pthread_mutex_unlock(&done_mutex);
- }
- pthread_mutex_unlock(&run_mutex);
-}
-
-void bridge_execute(void *f) {
- if (!initialized) {
- initialized = 1;
- pthread_mutex_init(&run_mutex, NULL);
- pthread_cond_init(&run_cv, NULL);
- pthread_mutex_init(&done_mutex, NULL);
- pthread_cond_init(&done_cv, NULL);
- pthread_mutex_lock(&done_mutex);
- pthread_create(&thread, NULL, run_loop, NULL);
- pthread_cond_wait(&done_cv, &done_mutex);
- pthread_mutex_unlock(&done_mutex);
- }
- pthread_mutex_lock(&run_mutex);
- pthread_mutex_lock(&done_mutex);
- function = f;
- pthread_cond_signal(&run_cv);
- pthread_mutex_unlock(&run_mutex);
- pthread_cond_wait(&done_cv, &done_mutex);
- pthread_mutex_unlock(&done_mutex);
-}
-*/
-import "C"
diff --git a/pkg/emulator/libretro/nanoarch/configscanner.go b/pkg/emulator/libretro/nanoarch/configscanner.go
deleted file mode 100644
index c61a816d..00000000
--- a/pkg/emulator/libretro/nanoarch/configscanner.go
+++ /dev/null
@@ -1,46 +0,0 @@
-package nanoarch
-
-import (
- "bufio"
- "log"
- "os"
- "strings"
-)
-
-import "C"
-
-type ConfigProperties map[string]*C.char
-
-func ScanConfigFile(filename string) ConfigProperties {
- config := ConfigProperties{}
-
- if len(filename) == 0 {
- return config
- }
- file, err := os.Open(filename)
- if err != nil {
- log.Printf("warning: couldn't find the %v config file", filename)
- return config
- }
- defer file.Close()
-
- scanner := bufio.NewScanner(file)
- for scanner.Scan() {
- line := scanner.Text()
- if equal := strings.Index(line, "="); equal >= 0 {
- if key := strings.TrimSpace(line[:equal]); len(key) > 0 {
- value := ""
- if len(line) > equal {
- value = strings.TrimSpace(line[equal+1:])
- }
- config[key] = C.CString(value)
- }
- }
- }
-
- if err := scanner.Err(); err != nil {
- log.Fatal(err)
- }
-
- return config
-}
diff --git a/pkg/emulator/libretro/nanoarch/input.go b/pkg/emulator/libretro/nanoarch/input.go
deleted file mode 100644
index b218350c..00000000
--- a/pkg/emulator/libretro/nanoarch/input.go
+++ /dev/null
@@ -1,97 +0,0 @@
-package nanoarch
-
-import "sync"
-
-const (
- // how many axes on the D-pad
- dpadAxesNum = 4
- // the upper limit on how many controllers (players)
- // are possible for one play session (emulator instance)
- controllersNum = 8
-)
-
-const (
- InputTerminate = 0xFFFF
-)
-
-type Players struct {
- session playerSession
-}
-
-type playerSession struct {
- sync.RWMutex
-
- state map[string][]controllerState
-}
-
-type controllerState struct {
- keyState uint16
- axes [dpadAxesNum]int16
-}
-
-func NewPlayerSessionInput() Players {
- return Players{
- session: playerSession{
- state: map[string][]controllerState{},
- },
- }
-}
-
-// close terminates user input session.
-func (ps *playerSession) close(id string) {
- ps.Lock()
- defer ps.Unlock()
-
- delete(ps.state, id)
-}
-
-// setInput sets input state for some player in a game session.
-func (ps *playerSession) setInput(id string, player int, buttons uint16, dpad []byte) {
- ps.Lock()
- defer ps.Unlock()
-
- if _, ok := ps.state[id]; !ok {
- ps.state[id] = make([]controllerState, controllersNum)
- }
-
- ps.state[id][player].keyState = buttons
- for i, axes := 0, len(dpad); i < dpadAxesNum && (i+1)*2+1 < axes; i++ {
- axis := (i + 1) * 2
- ps.state[id][player].axes[i] = int16(dpad[axis+1])<<8 + int16(dpad[axis])
- }
-}
-
-// isKeyPressed checks if some button is pressed by any player.
-func (p *Players) isKeyPressed(player uint, key int) (pressed bool) {
- p.session.RLock()
- defer p.session.RUnlock()
-
- for k := range p.session.state {
- if ((p.session.state[k][player].keyState >> uint(key)) & 1) == 1 {
- return true
- }
- }
- return
-}
-
-// isDpadTouched checks if D-pad is used by any player.
-func (p *Players) isDpadTouched(player uint, axis uint) (shift int16) {
- p.session.RLock()
- defer p.session.RUnlock()
-
- for k := range p.session.state {
- value := p.session.state[k][player].axes[axis]
- if value != 0 {
- return value
- }
- }
- return
-}
-
-type InputEvent struct {
- RawState []byte
- PlayerIdx int
- ConnID string
-}
-
-func (ie InputEvent) bitmap() uint16 { return uint16(ie.RawState[1])<<8 + uint16(ie.RawState[0]) }
diff --git a/pkg/emulator/libretro/nanoarch/input_test.go b/pkg/emulator/libretro/nanoarch/input_test.go
deleted file mode 100644
index 771c417b..00000000
--- a/pkg/emulator/libretro/nanoarch/input_test.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package nanoarch
-
-import (
- "math/rand"
- "testing"
-)
-
-func TestConcurrentInput(t *testing.T) {
- players := NewPlayerSessionInput()
-
- session := "mad-test-session"
- events := 1000
- go func() {
- for i := 0; i < events*2; i++ {
- player := rand.Intn(controllersNum)
- go players.session.setInput(session, player, 100, []byte{})
- // here it usually crashes
- go players.session.close(session)
- }
- }()
- go func() {
- for i := 0; i < events*2; i++ {
- player := rand.Intn(controllersNum)
- go players.isKeyPressed(uint(player), 100)
- }
- }()
-}
diff --git a/pkg/emulator/libretro/nanoarch/libretro.h b/pkg/emulator/libretro/nanoarch/libretro.h
deleted file mode 100644
index 50bdf5b8..00000000
--- a/pkg/emulator/libretro/nanoarch/libretro.h
+++ /dev/null
@@ -1,2167 +0,0 @@
-/* Copyright (C) 2010-2017 The RetroArch team
- *
- * ---------------------------------------------------------------------------------------
- * The following license statement only applies to this libretro API header (libretro.h).
- * ---------------------------------------------------------------------------------------
- *
- * Permission is hereby granted, free of charge,
- * to any person obtaining a copy of this software and associated documentation files (the "Software"),
- * to deal in the Software without restriction, including without limitation the rights to
- * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
- * and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
- * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
- * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
- * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- */
-
-#ifndef LIBRETRO_H__
-#define LIBRETRO_H__
-
-#include
-#include
-#include
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-#ifndef __cplusplus
-#if defined(_MSC_VER) && !defined(SN_TARGET_PS3)
-/* Hack applied for MSVC when compiling in C89 mode
- * as it isn't C99-compliant. */
-#define bool unsigned char
-#define true 1
-#define false 0
-#else
-#include
-#endif
-#endif
-
-#ifndef RETRO_CALLCONV
-# if defined(__GNUC__) && defined(__i386__) && !defined(__x86_64__)
-# define RETRO_CALLCONV __attribute__((cdecl))
-# elif defined(_MSC_VER) && defined(_M_X86) && !defined(_M_X64)
-# define RETRO_CALLCONV __cdecl
-# else
-# define RETRO_CALLCONV /* all other platforms only have one calling convention each */
-# endif
-#endif
-
-#ifndef RETRO_API
-# if defined(_WIN32) || defined(__CYGWIN__) || defined(__MINGW32__)
-# ifdef RETRO_IMPORT_SYMBOLS
-# ifdef __GNUC__
-# define RETRO_API RETRO_CALLCONV __attribute__((__dllimport__))
-# else
-# define RETRO_API RETRO_CALLCONV __declspec(dllimport)
-# endif
-# else
-# ifdef __GNUC__
-# define RETRO_API RETRO_CALLCONV __attribute__((__dllexport__))
-# else
-# define RETRO_API RETRO_CALLCONV __declspec(dllexport)
-# endif
-# endif
-# else
-# if defined(__GNUC__) && __GNUC__ >= 4 && !defined(__CELLOS_LV2__)
-# define RETRO_API RETRO_CALLCONV __attribute__((__visibility__("default")))
-# else
-# define RETRO_API RETRO_CALLCONV
-# endif
-# endif
-#endif
-
-/* Used for checking API/ABI mismatches that can break libretro
- * implementations.
- * It is not incremented for compatible changes to the API.
- */
-#define RETRO_API_VERSION 1
-
-/*
- * Libretro's fundamental device abstractions.
- *
- * Libretro's input system consists of some standardized device types,
- * such as a joypad (with/without analog), mouse, keyboard, lightgun
- * and a pointer.
- *
- * The functionality of these devices are fixed, and individual cores
- * map their own concept of a controller to libretro's abstractions.
- * This makes it possible for frontends to map the abstract types to a
- * real input device, and not having to worry about binding input
- * correctly to arbitrary controller layouts.
- */
-
-#define RETRO_DEVICE_TYPE_SHIFT 8
-#define RETRO_DEVICE_MASK ((1 << RETRO_DEVICE_TYPE_SHIFT) - 1)
-#define RETRO_DEVICE_SUBCLASS(base, id) (((id + 1) << RETRO_DEVICE_TYPE_SHIFT) | base)
-
-/* Input disabled. */
-#define RETRO_DEVICE_NONE 0
-
-/* The JOYPAD is called RetroPad. It is essentially a Super Nintendo
- * controller, but with additional L2/R2/L3/R3 buttons, similar to a
- * PS1 DualShock. */
-#define RETRO_DEVICE_JOYPAD 1
-
-/* The mouse is a simple mouse, similar to Super Nintendo's mouse.
- * X and Y coordinates are reported relatively to last poll (poll callback).
- * It is up to the libretro implementation to keep track of where the mouse
- * pointer is supposed to be on the screen.
- * The frontend must make sure not to interfere with its own hardware
- * mouse pointer.
- */
-#define RETRO_DEVICE_MOUSE 2
-
-/* KEYBOARD device lets one poll for raw key pressed.
- * It is poll based, so input callback will return with the current
- * pressed state.
- * For event/text based keyboard input, see
- * RETRO_ENVIRONMENT_SET_KEYBOARD_CALLBACK.
- */
-#define RETRO_DEVICE_KEYBOARD 3
-
-/* Lightgun X/Y coordinates are reported relatively to last poll,
- * similar to mouse. */
-#define RETRO_DEVICE_LIGHTGUN 4
-
-/* The ANALOG device is an extension to JOYPAD (RetroPad).
- * Similar to DualShock it adds two analog sticks.
- * This is treated as a separate device type as it returns values in the
- * full analog range of [-0x8000, 0x7fff]. Positive X axis is right.
- * Positive Y axis is down.
- * Only use ANALOG type when polling for analog values of the axes.
- */
-#define RETRO_DEVICE_ANALOG 5
-
-/* Abstracts the concept of a pointing mechanism, e.g. touch.
- * This allows libretro to query in absolute coordinates where on the
- * screen a mouse (or something similar) is being placed.
- * For a touch centric device, coordinates reported are the coordinates
- * of the press.
- *
- * Coordinates in X and Y are reported as:
- * [-0x7fff, 0x7fff]: -0x7fff corresponds to the far left/top of the screen,
- * and 0x7fff corresponds to the far right/bottom of the screen.
- * The "screen" is here defined as area that is passed to the frontend and
- * later displayed on the monitor.
- *
- * The frontend is free to scale/resize this screen as it sees fit, however,
- * (X, Y) = (-0x7fff, -0x7fff) will correspond to the top-left pixel of the
- * game image, etc.
- *
- * To check if the pointer coordinates are valid (e.g. a touch display
- * actually being touched), PRESSED returns 1 or 0.
- *
- * If using a mouse on a desktop, PRESSED will usually correspond to the
- * left mouse button, but this is a frontend decision.
- * PRESSED will only return 1 if the pointer is inside the game screen.
- *
- * For multi-touch, the index variable can be used to successively query
- * more presses.
- * If index = 0 returns true for _PRESSED, coordinates can be extracted
- * with _X, _Y for index = 0. One can then query _PRESSED, _X, _Y with
- * index = 1, and so on.
- * Eventually _PRESSED will return false for an index. No further presses
- * are registered at this point. */
-#define RETRO_DEVICE_POINTER 6
-
-/* Buttons for the RetroPad (JOYPAD).
- * The placement of these is equivalent to placements on the
- * Super Nintendo controller.
- * L2/R2/L3/R3 buttons correspond to the PS1 DualShock. */
-#define RETRO_DEVICE_ID_JOYPAD_B 0
-#define RETRO_DEVICE_ID_JOYPAD_Y 1
-#define RETRO_DEVICE_ID_JOYPAD_SELECT 2
-#define RETRO_DEVICE_ID_JOYPAD_START 3
-#define RETRO_DEVICE_ID_JOYPAD_UP 4
-#define RETRO_DEVICE_ID_JOYPAD_DOWN 5
-#define RETRO_DEVICE_ID_JOYPAD_LEFT 6
-#define RETRO_DEVICE_ID_JOYPAD_RIGHT 7
-#define RETRO_DEVICE_ID_JOYPAD_A 8
-#define RETRO_DEVICE_ID_JOYPAD_X 9
-#define RETRO_DEVICE_ID_JOYPAD_L 10
-#define RETRO_DEVICE_ID_JOYPAD_R 11
-#define RETRO_DEVICE_ID_JOYPAD_L2 12
-#define RETRO_DEVICE_ID_JOYPAD_R2 13
-#define RETRO_DEVICE_ID_JOYPAD_L3 14
-#define RETRO_DEVICE_ID_JOYPAD_R3 15
-
-/* Index / Id values for ANALOG device. */
-#define RETRO_DEVICE_INDEX_ANALOG_LEFT 0
-#define RETRO_DEVICE_INDEX_ANALOG_RIGHT 1
-#define RETRO_DEVICE_ID_ANALOG_X 0
-#define RETRO_DEVICE_ID_ANALOG_Y 1
-
-/* Id values for MOUSE. */
-#define RETRO_DEVICE_ID_MOUSE_X 0
-#define RETRO_DEVICE_ID_MOUSE_Y 1
-#define RETRO_DEVICE_ID_MOUSE_LEFT 2
-#define RETRO_DEVICE_ID_MOUSE_RIGHT 3
-#define RETRO_DEVICE_ID_MOUSE_WHEELUP 4
-#define RETRO_DEVICE_ID_MOUSE_WHEELDOWN 5
-#define RETRO_DEVICE_ID_MOUSE_MIDDLE 6
-#define RETRO_DEVICE_ID_MOUSE_HORIZ_WHEELUP 7
-#define RETRO_DEVICE_ID_MOUSE_HORIZ_WHEELDOWN 8
-
-/* Id values for LIGHTGUN types. */
-#define RETRO_DEVICE_ID_LIGHTGUN_X 0
-#define RETRO_DEVICE_ID_LIGHTGUN_Y 1
-#define RETRO_DEVICE_ID_LIGHTGUN_TRIGGER 2
-#define RETRO_DEVICE_ID_LIGHTGUN_CURSOR 3
-#define RETRO_DEVICE_ID_LIGHTGUN_TURBO 4
-#define RETRO_DEVICE_ID_LIGHTGUN_PAUSE 5
-#define RETRO_DEVICE_ID_LIGHTGUN_START 6
-
-/* Id values for POINTER. */
-#define RETRO_DEVICE_ID_POINTER_X 0
-#define RETRO_DEVICE_ID_POINTER_Y 1
-#define RETRO_DEVICE_ID_POINTER_PRESSED 2
-
-/* Returned from retro_get_region(). */
-#define RETRO_REGION_NTSC 0
-#define RETRO_REGION_PAL 1
-
-/* Id values for LANGUAGE */
-enum retro_language
-{
- RETRO_LANGUAGE_ENGLISH = 0,
- RETRO_LANGUAGE_JAPANESE = 1,
- RETRO_LANGUAGE_FRENCH = 2,
- RETRO_LANGUAGE_SPANISH = 3,
- RETRO_LANGUAGE_GERMAN = 4,
- RETRO_LANGUAGE_ITALIAN = 5,
- RETRO_LANGUAGE_DUTCH = 6,
- RETRO_LANGUAGE_PORTUGUESE_BRAZIL = 7,
- RETRO_LANGUAGE_PORTUGUESE_PORTUGAL = 8,
- RETRO_LANGUAGE_RUSSIAN = 9,
- RETRO_LANGUAGE_KOREAN = 10,
- RETRO_LANGUAGE_CHINESE_TRADITIONAL = 11,
- RETRO_LANGUAGE_CHINESE_SIMPLIFIED = 12,
- RETRO_LANGUAGE_ESPERANTO = 13,
- RETRO_LANGUAGE_POLISH = 14,
- RETRO_LANGUAGE_VIETNAMESE = 15,
- RETRO_LANGUAGE_LAST,
-
- /* Ensure sizeof(enum) == sizeof(int) */
- RETRO_LANGUAGE_DUMMY = INT_MAX
-};
-
-/* Passed to retro_get_memory_data/size().
- * If the memory type doesn't apply to the
- * implementation NULL/0 can be returned.
- */
-#define RETRO_MEMORY_MASK 0xff
-
-/* Regular save RAM. This RAM is usually found on a game cartridge,
- * backed up by a battery.
- * If save game data is too complex for a single memory buffer,
- * the SAVE_DIRECTORY (preferably) or SYSTEM_DIRECTORY environment
- * callback can be used. */
-#define RETRO_MEMORY_SAVE_RAM 0
-
-/* Some games have a built-in clock to keep track of time.
- * This memory is usually just a couple of bytes to keep track of time.
- */
-#define RETRO_MEMORY_RTC 1
-
-/* System ram lets a frontend peek into a game systems main RAM. */
-#define RETRO_MEMORY_SYSTEM_RAM 2
-
-/* Video ram lets a frontend peek into a game systems video RAM (VRAM). */
-#define RETRO_MEMORY_VIDEO_RAM 3
-
-/* Keysyms used for ID in input state callback when polling RETRO_KEYBOARD. */
-enum retro_key
-{
- RETROK_UNKNOWN = 0,
- RETROK_FIRST = 0,
- RETROK_BACKSPACE = 8,
- RETROK_TAB = 9,
- RETROK_CLEAR = 12,
- RETROK_RETURN = 13,
- RETROK_PAUSE = 19,
- RETROK_ESCAPE = 27,
- RETROK_SPACE = 32,
- RETROK_EXCLAIM = 33,
- RETROK_QUOTEDBL = 34,
- RETROK_HASH = 35,
- RETROK_DOLLAR = 36,
- RETROK_AMPERSAND = 38,
- RETROK_QUOTE = 39,
- RETROK_LEFTPAREN = 40,
- RETROK_RIGHTPAREN = 41,
- RETROK_ASTERISK = 42,
- RETROK_PLUS = 43,
- RETROK_COMMA = 44,
- RETROK_MINUS = 45,
- RETROK_PERIOD = 46,
- RETROK_SLASH = 47,
- RETROK_0 = 48,
- RETROK_1 = 49,
- RETROK_2 = 50,
- RETROK_3 = 51,
- RETROK_4 = 52,
- RETROK_5 = 53,
- RETROK_6 = 54,
- RETROK_7 = 55,
- RETROK_8 = 56,
- RETROK_9 = 57,
- RETROK_COLON = 58,
- RETROK_SEMICOLON = 59,
- RETROK_LESS = 60,
- RETROK_EQUALS = 61,
- RETROK_GREATER = 62,
- RETROK_QUESTION = 63,
- RETROK_AT = 64,
- RETROK_LEFTBRACKET = 91,
- RETROK_BACKSLASH = 92,
- RETROK_RIGHTBRACKET = 93,
- RETROK_CARET = 94,
- RETROK_UNDERSCORE = 95,
- RETROK_BACKQUOTE = 96,
- RETROK_a = 97,
- RETROK_b = 98,
- RETROK_c = 99,
- RETROK_d = 100,
- RETROK_e = 101,
- RETROK_f = 102,
- RETROK_g = 103,
- RETROK_h = 104,
- RETROK_i = 105,
- RETROK_j = 106,
- RETROK_k = 107,
- RETROK_l = 108,
- RETROK_m = 109,
- RETROK_n = 110,
- RETROK_o = 111,
- RETROK_p = 112,
- RETROK_q = 113,
- RETROK_r = 114,
- RETROK_s = 115,
- RETROK_t = 116,
- RETROK_u = 117,
- RETROK_v = 118,
- RETROK_w = 119,
- RETROK_x = 120,
- RETROK_y = 121,
- RETROK_z = 122,
- RETROK_DELETE = 127,
-
- RETROK_KP0 = 256,
- RETROK_KP1 = 257,
- RETROK_KP2 = 258,
- RETROK_KP3 = 259,
- RETROK_KP4 = 260,
- RETROK_KP5 = 261,
- RETROK_KP6 = 262,
- RETROK_KP7 = 263,
- RETROK_KP8 = 264,
- RETROK_KP9 = 265,
- RETROK_KP_PERIOD = 266,
- RETROK_KP_DIVIDE = 267,
- RETROK_KP_MULTIPLY = 268,
- RETROK_KP_MINUS = 269,
- RETROK_KP_PLUS = 270,
- RETROK_KP_ENTER = 271,
- RETROK_KP_EQUALS = 272,
-
- RETROK_UP = 273,
- RETROK_DOWN = 274,
- RETROK_RIGHT = 275,
- RETROK_LEFT = 276,
- RETROK_INSERT = 277,
- RETROK_HOME = 278,
- RETROK_END = 279,
- RETROK_PAGEUP = 280,
- RETROK_PAGEDOWN = 281,
-
- RETROK_F1 = 282,
- RETROK_F2 = 283,
- RETROK_F3 = 284,
- RETROK_F4 = 285,
- RETROK_F5 = 286,
- RETROK_F6 = 287,
- RETROK_F7 = 288,
- RETROK_F8 = 289,
- RETROK_F9 = 290,
- RETROK_F10 = 291,
- RETROK_F11 = 292,
- RETROK_F12 = 293,
- RETROK_F13 = 294,
- RETROK_F14 = 295,
- RETROK_F15 = 296,
-
- RETROK_NUMLOCK = 300,
- RETROK_CAPSLOCK = 301,
- RETROK_SCROLLOCK = 302,
- RETROK_RSHIFT = 303,
- RETROK_LSHIFT = 304,
- RETROK_RCTRL = 305,
- RETROK_LCTRL = 306,
- RETROK_RALT = 307,
- RETROK_LALT = 308,
- RETROK_RMETA = 309,
- RETROK_LMETA = 310,
- RETROK_LSUPER = 311,
- RETROK_RSUPER = 312,
- RETROK_MODE = 313,
- RETROK_COMPOSE = 314,
-
- RETROK_HELP = 315,
- RETROK_PRINT = 316,
- RETROK_SYSREQ = 317,
- RETROK_BREAK = 318,
- RETROK_MENU = 319,
- RETROK_POWER = 320,
- RETROK_EURO = 321,
- RETROK_UNDO = 322,
-
- RETROK_LAST,
-
- RETROK_DUMMY = INT_MAX /* Ensure sizeof(enum) == sizeof(int) */
-};
-
-enum retro_mod
-{
- RETROKMOD_NONE = 0x0000,
-
- RETROKMOD_SHIFT = 0x01,
- RETROKMOD_CTRL = 0x02,
- RETROKMOD_ALT = 0x04,
- RETROKMOD_META = 0x08,
-
- RETROKMOD_NUMLOCK = 0x10,
- RETROKMOD_CAPSLOCK = 0x20,
- RETROKMOD_SCROLLOCK = 0x40,
-
- RETROKMOD_DUMMY = INT_MAX /* Ensure sizeof(enum) == sizeof(int) */
-};
-
-/* If set, this call is not part of the public libretro API yet. It can
- * change or be removed at any time. */
-#define RETRO_ENVIRONMENT_EXPERIMENTAL 0x10000
-/* Environment callback to be used internally in frontend. */
-#define RETRO_ENVIRONMENT_PRIVATE 0x20000
-
-/* Environment commands. */
-#define RETRO_ENVIRONMENT_SET_ROTATION 1 /* const unsigned * --
- * Sets screen rotation of graphics.
- * Is only implemented if rotation can be accelerated by hardware.
- * Valid values are 0, 1, 2, 3, which rotates screen by 0, 90, 180,
- * 270 degrees counter-clockwise respectively.
- */
-#define RETRO_ENVIRONMENT_GET_OVERSCAN 2 /* bool * --
- * Boolean value whether or not the implementation should use overscan,
- * or crop away overscan.
- */
-#define RETRO_ENVIRONMENT_GET_CAN_DUPE 3 /* bool * --
- * Boolean value whether or not frontend supports frame duping,
- * passing NULL to video frame callback.
- */
-
- /* Environ 4, 5 are no longer supported (GET_VARIABLE / SET_VARIABLES),
- * and reserved to avoid possible ABI clash.
- */
-
-#define RETRO_ENVIRONMENT_SET_MESSAGE 6 /* const struct retro_message * --
- * Sets a message to be displayed in implementation-specific manner
- * for a certain amount of 'frames'.
- * Should not be used for trivial messages, which should simply be
- * logged via RETRO_ENVIRONMENT_GET_LOG_INTERFACE (or as a
- * fallback, stderr).
- */
-#define RETRO_ENVIRONMENT_SHUTDOWN 7 /* N/A (NULL) --
- * Requests the frontend to shutdown.
- * Should only be used if game has a specific
- * way to shutdown the game from a menu item or similar.
- */
-#define RETRO_ENVIRONMENT_SET_PERFORMANCE_LEVEL 8
- /* const unsigned * --
- * Gives a hint to the frontend how demanding this implementation
- * is on a system. E.g. reporting a level of 2 means
- * this implementation should run decently on all frontends
- * of level 2 and up.
- *
- * It can be used by the frontend to potentially warn
- * about too demanding implementations.
- *
- * The levels are "floating".
- *
- * This function can be called on a per-game basis,
- * as certain games an implementation can play might be
- * particularly demanding.
- * If called, it should be called in retro_load_game().
- */
-#define RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY 9
- /* const char ** --
- * Returns the "system" directory of the frontend.
- * This directory can be used to store system specific
- * content such as BIOSes, configuration data, etc.
- * The returned value can be NULL.
- * If so, no such directory is defined,
- * and it's up to the implementation to find a suitable directory.
- *
- * NOTE: Some cores used this folder also for "save" data such as
- * memory cards, etc, for lack of a better place to put it.
- * This is now discouraged, and if possible, cores should try to
- * use the new GET_SAVE_DIRECTORY.
- */
-#define RETRO_ENVIRONMENT_SET_PIXEL_FORMAT 10
- /* const enum retro_pixel_format * --
- * Sets the internal pixel format used by the implementation.
- * The default pixel format is RETRO_PIXEL_FORMAT_0RGB1555.
- * This pixel format however, is deprecated (see enum retro_pixel_format).
- * If the call returns false, the frontend does not support this pixel
- * format.
- *
- * This function should be called inside retro_load_game() or
- * retro_get_system_av_info().
- */
-#define RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS 11
- /* const struct retro_input_descriptor * --
- * Sets an array of retro_input_descriptors.
- * It is up to the frontend to present this in a usable way.
- * The array is terminated by retro_input_descriptor::description
- * being set to NULL.
- * This function can be called at any time, but it is recommended
- * to call it as early as possible.
- */
-#define RETRO_ENVIRONMENT_SET_KEYBOARD_CALLBACK 12
- /* const struct retro_keyboard_callback * --
- * Sets a callback function used to notify core about keyboard events.
- */
-#define RETRO_ENVIRONMENT_SET_DISK_CONTROL_INTERFACE 13
- /* const struct retro_disk_control_callback * --
- * Sets an interface which frontend can use to eject and insert
- * disk images.
- * This is used for games which consist of multiple images and
- * must be manually swapped out by the user (e.g. PSX).
- */
-#define RETRO_ENVIRONMENT_SET_HW_RENDER 14
- /* struct retro_hw_render_callback * --
- * Sets an interface to let a libretro core render with
- * hardware acceleration.
- * Should be called in retro_load_game().
- * If successful, libretro cores will be able to render to a
- * frontend-provided framebuffer.
- * The size of this framebuffer will be at least as large as
- * max_width/max_height provided in get_av_info().
- * If HW rendering is used, pass only RETRO_HW_FRAME_BUFFER_VALID or
- * NULL to retro_video_refresh_t.
- */
-#define RETRO_ENVIRONMENT_GET_VARIABLE 15
- /* struct retro_variable * --
- * Interface to acquire user-defined information from environment
- * that cannot feasibly be supported in a multi-system way.
- * 'key' should be set to a key which has already been set by
- * SET_VARIABLES.
- * 'data' will be set to a value or NULL.
- */
-#define RETRO_ENVIRONMENT_SET_VARIABLES 16
- /* const struct retro_variable * --
- * Allows an implementation to signal the environment
- * which variables it might want to check for later using
- * GET_VARIABLE.
- * This allows the frontend to present these variables to
- * a user dynamically.
- * This should be called as early as possible (ideally in
- * retro_set_environment).
- *
- * 'data' points to an array of retro_variable structs
- * terminated by a { NULL, NULL } element.
- * retro_variable::key should be namespaced to not collide
- * with other implementations' keys. E.g. A core called
- * 'foo' should use keys named as 'foo_option'.
- * retro_variable::value should contain a human readable
- * description of the key as well as a '|' delimited list
- * of expected values.
- *
- * The number of possible options should be very limited,
- * i.e. it should be feasible to cycle through options
- * without a keyboard.
- *
- * First entry should be treated as a default.
- *
- * Example entry:
- * { "foo_option", "Speed hack coprocessor X; false|true" }
- *
- * Text before first ';' is description. This ';' must be
- * followed by a space, and followed by a list of possible
- * values split up with '|'.
- *
- * Only strings are operated on. The possible values will
- * generally be displayed and stored as-is by the frontend.
- */
-#define RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE 17
- /* bool * --
- * Result is set to true if some variables are updated by
- * frontend since last call to RETRO_ENVIRONMENT_GET_VARIABLE.
- * Variables should be queried with GET_VARIABLE.
- */
-#define RETRO_ENVIRONMENT_SET_SUPPORT_NO_GAME 18
- /* const bool * --
- * If true, the libretro implementation supports calls to
- * retro_load_game() with NULL as argument.
- * Used by cores which can run without particular game data.
- * This should be called within retro_set_environment() only.
- */
-#define RETRO_ENVIRONMENT_GET_LIBRETRO_PATH 19
- /* const char ** --
- * Retrieves the absolute path from where this libretro
- * implementation was loaded.
- * NULL is returned if the libretro was loaded statically
- * (i.e. linked statically to frontend), or if the path cannot be
- * determined.
- * Mostly useful in cooperation with SET_SUPPORT_NO_GAME as assets can
- * be loaded without ugly hacks.
- */
-
- /* Environment 20 was an obsolete version of SET_AUDIO_CALLBACK.
- * It was not used by any known core at the time,
- * and was removed from the API. */
-#define RETRO_ENVIRONMENT_SET_AUDIO_CALLBACK 22
- /* const struct retro_audio_callback * --
- * Sets an interface which is used to notify a libretro core about audio
- * being available for writing.
- * The callback can be called from any thread, so a core using this must
- * have a thread safe audio implementation.
- * It is intended for games where audio and video are completely
- * asynchronous and audio can be generated on the fly.
- * This interface is not recommended for use with emulators which have
- * highly synchronous audio.
- *
- * The callback only notifies about writability; the libretro core still
- * has to call the normal audio callbacks
- * to write audio. The audio callbacks must be called from within the
- * notification callback.
- * The amount of audio data to write is up to the implementation.
- * Generally, the audio callback will be called continously in a loop.
- *
- * Due to thread safety guarantees and lack of sync between audio and
- * video, a frontend can selectively disallow this interface based on
- * internal configuration. A core using this interface must also
- * implement the "normal" audio interface.
- *
- * A libretro core using SET_AUDIO_CALLBACK should also make use of
- * SET_FRAME_TIME_CALLBACK.
- */
-#define RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK 21
- /* const struct retro_frame_time_callback * --
- * Lets the core know how much time has passed since last
- * invocation of retro_run().
- * The frontend can tamper with the timing to fake fast-forward,
- * slow-motion, frame stepping, etc.
- * In this case the delta time will use the reference value
- * in frame_time_callback..
- */
-#define RETRO_ENVIRONMENT_GET_RUMBLE_INTERFACE 23
- /* struct retro_rumble_interface * --
- * Gets an interface which is used by a libretro core to set
- * state of rumble motors in controllers.
- * A strong and weak motor is supported, and they can be
- * controlled indepedently.
- */
-#define RETRO_ENVIRONMENT_GET_INPUT_DEVICE_CAPABILITIES 24
- /* uint64_t * --
- * Gets a bitmask telling which device type are expected to be
- * handled properly in a call to retro_input_state_t.
- * Devices which are not handled or recognized always return
- * 0 in retro_input_state_t.
- * Example bitmask: caps = (1 << RETRO_DEVICE_JOYPAD) | (1 << RETRO_DEVICE_ANALOG).
- * Should only be called in retro_run().
- */
-#define RETRO_ENVIRONMENT_GET_SENSOR_INTERFACE (25 | RETRO_ENVIRONMENT_EXPERIMENTAL)
- /* struct retro_sensor_interface * --
- * Gets access to the sensor interface.
- * The purpose of this interface is to allow
- * setting state related to sensors such as polling rate,
- * enabling/disable it entirely, etc.
- * Reading sensor state is done via the normal
- * input_state_callback API.
- */
-#define RETRO_ENVIRONMENT_GET_CAMERA_INTERFACE (26 | RETRO_ENVIRONMENT_EXPERIMENTAL)
- /* struct retro_camera_callback * --
- * Gets an interface to a video camera driver.
- * A libretro core can use this interface to get access to a
- * video camera.
- * New video frames are delivered in a callback in same
- * thread as retro_run().
- *
- * GET_CAMERA_INTERFACE should be called in retro_load_game().
- *
- * Depending on the camera implementation used, camera frames
- * will be delivered as a raw framebuffer,
- * or as an OpenGL texture directly.
- *
- * The core has to tell the frontend here which types of
- * buffers can be handled properly.
- * An OpenGL texture can only be handled when using a
- * libretro GL core (SET_HW_RENDER).
- * It is recommended to use a libretro GL core when
- * using camera interface.
- *
- * The camera is not started automatically. The retrieved start/stop
- * functions must be used to explicitly
- * start and stop the camera driver.
- */
-#define RETRO_ENVIRONMENT_GET_LOG_INTERFACE 27
- /* struct retro_log_callback * --
- * Gets an interface for logging. This is useful for
- * logging in a cross-platform way
- * as certain platforms cannot use use stderr for logging.
- * It also allows the frontend to
- * show logging information in a more suitable way.
- * If this interface is not used, libretro cores should
- * log to stderr as desired.
- */
-#define RETRO_ENVIRONMENT_GET_PERF_INTERFACE 28
- /* struct retro_perf_callback * --
- * Gets an interface for performance counters. This is useful
- * for performance logging in a cross-platform way and for detecting
- * architecture-specific features, such as SIMD support.
- */
-#define RETRO_ENVIRONMENT_GET_LOCATION_INTERFACE 29
- /* struct retro_location_callback * --
- * Gets access to the location interface.
- * The purpose of this interface is to be able to retrieve
- * location-based information from the host device,
- * such as current latitude / longitude.
- */
-#define RETRO_ENVIRONMENT_GET_CONTENT_DIRECTORY 30 /* Old name, kept for compatibility. */
-#define RETRO_ENVIRONMENT_GET_CORE_ASSETS_DIRECTORY 30
- /* const char ** --
- * Returns the "core assets" directory of the frontend.
- * This directory can be used to store specific assets that the
- * core relies upon, such as art assets,
- * input data, etc etc.
- * The returned value can be NULL.
- * If so, no such directory is defined,
- * and it's up to the implementation to find a suitable directory.
- */
-#define RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY 31
- /* const char ** --
- * Returns the "save" directory of the frontend.
- * This directory can be used to store SRAM, memory cards,
- * high scores, etc, if the libretro core
- * cannot use the regular memory interface (retro_get_memory_data()).
- *
- * NOTE: libretro cores used to check GET_SYSTEM_DIRECTORY for
- * similar things before.
- * They should still check GET_SYSTEM_DIRECTORY if they want to
- * be backwards compatible.
- * The path here can be NULL. It should only be non-NULL if the
- * frontend user has set a specific save path.
- */
-#define RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO 32
- /* const struct retro_system_av_info * --
- * Sets a new av_info structure. This can only be called from
- * within retro_run().
- * This should *only* be used if the core is completely altering the
- * internal resolutions, aspect ratios, timings, sampling rate, etc.
- * Calling this can require a full reinitialization of video/audio
- * drivers in the frontend,
- *
- * so it is important to call it very sparingly, and usually only with
- * the users explicit consent.
- * An eventual driver reinitialize will happen so that video and
- * audio callbacks
- * happening after this call within the same retro_run() call will
- * target the newly initialized driver.
- *
- * This callback makes it possible to support configurable resolutions
- * in games, which can be useful to
- * avoid setting the "worst case" in max_width/max_height.
- *
- * ***HIGHLY RECOMMENDED*** Do not call this callback every time
- * resolution changes in an emulator core if it's
- * expected to be a temporary change, for the reasons of possible
- * driver reinitialization.
- * This call is not a free pass for not trying to provide
- * correct values in retro_get_system_av_info(). If you need to change
- * things like aspect ratio or nominal width/height,
- * use RETRO_ENVIRONMENT_SET_GEOMETRY, which is a softer variant
- * of SET_SYSTEM_AV_INFO.
- *
- * If this returns false, the frontend does not acknowledge a
- * changed av_info struct.
- */
-#define RETRO_ENVIRONMENT_SET_PROC_ADDRESS_CALLBACK 33
- /* const struct retro_get_proc_address_interface * --
- * Allows a libretro core to announce support for the
- * get_proc_address() interface.
- * This interface allows for a standard way to extend libretro where
- * use of environment calls are too indirect,
- * e.g. for cases where the frontend wants to call directly into the core.
- *
- * If a core wants to expose this interface, SET_PROC_ADDRESS_CALLBACK
- * **MUST** be called from within retro_set_environment().
- */
-#define RETRO_ENVIRONMENT_SET_SUBSYSTEM_INFO 34
- /* const struct retro_subsystem_info * --
- * This environment call introduces the concept of libretro "subsystems".
- * A subsystem is a variant of a libretro core which supports
- * different kinds of games.
- * The purpose of this is to support e.g. emulators which might
- * have special needs, e.g. Super Nintendo's Super GameBoy, Sufami Turbo.
- * It can also be used to pick among subsystems in an explicit way
- * if the libretro implementation is a multi-system emulator itself.
- *
- * Loading a game via a subsystem is done with retro_load_game_special(),
- * and this environment call allows a libretro core to expose which
- * subsystems are supported for use with retro_load_game_special().
- * A core passes an array of retro_game_special_info which is terminated
- * with a zeroed out retro_game_special_info struct.
- *
- * If a core wants to use this functionality, SET_SUBSYSTEM_INFO
- * **MUST** be called from within retro_set_environment().
- */
-#define RETRO_ENVIRONMENT_SET_CONTROLLER_INFO 35
- /* const struct retro_controller_info * --
- * This environment call lets a libretro core tell the frontend
- * which controller types are recognized in calls to
- * retro_set_controller_port_device().
- *
- * Some emulators such as Super Nintendo
- * support multiple lightgun types which must be specifically
- * selected from.
- * It is therefore sometimes necessary for a frontend to be able
- * to tell the core about a special kind of input device which is
- * not covered by the libretro input API.
- *
- * In order for a frontend to understand the workings of an input device,
- * it must be a specialized type
- * of the generic device types already defined in the libretro API.
- *
- * Which devices are supported can vary per input port.
- * The core must pass an array of const struct retro_controller_info which
- * is terminated with a blanked out struct. Each element of the struct
- * corresponds to an ascending port index to
- * retro_set_controller_port_device().
- * Even if special device types are set in the libretro core,
- * libretro should only poll input based on the base input device types.
- */
-#define RETRO_ENVIRONMENT_SET_MEMORY_MAPS (36 | RETRO_ENVIRONMENT_EXPERIMENTAL)
- /* const struct retro_memory_map * --
- * This environment call lets a libretro core tell the frontend
- * about the memory maps this core emulates.
- * This can be used to implement, for example, cheats in a core-agnostic way.
- *
- * Should only be used by emulators; it doesn't make much sense for
- * anything else.
- * It is recommended to expose all relevant pointers through
- * retro_get_memory_* as well.
- *
- * Can be called from retro_init and retro_load_game.
- */
-#define RETRO_ENVIRONMENT_SET_GEOMETRY 37
- /* const struct retro_game_geometry * --
- * This environment call is similar to SET_SYSTEM_AV_INFO for changing
- * video parameters, but provides a guarantee that drivers will not be
- * reinitialized.
- * This can only be called from within retro_run().
- *
- * The purpose of this call is to allow a core to alter nominal
- * width/heights as well as aspect ratios on-the-fly, which can be
- * useful for some emulators to change in run-time.
- *
- * max_width/max_height arguments are ignored and cannot be changed
- * with this call as this could potentially require a reinitialization or a
- * non-constant time operation.
- * If max_width/max_height are to be changed, SET_SYSTEM_AV_INFO is required.
- *
- * A frontend must guarantee that this environment call completes in
- * constant time.
- */
-#define RETRO_ENVIRONMENT_GET_USERNAME 38
- /* const char **
- * Returns the specified username of the frontend, if specified by the user.
- * This username can be used as a nickname for a core that has online facilities
- * or any other mode where personalization of the user is desirable.
- * The returned value can be NULL.
- * If this environ callback is used by a core that requires a valid username,
- * a default username should be specified by the core.
- */
-#define RETRO_ENVIRONMENT_GET_LANGUAGE 39
- /* unsigned * --
- * Returns the specified language of the frontend, if specified by the user.
- * It can be used by the core for localization purposes.
- */
-#define RETRO_ENVIRONMENT_GET_CURRENT_SOFTWARE_FRAMEBUFFER (40 | RETRO_ENVIRONMENT_EXPERIMENTAL)
- /* struct retro_framebuffer * --
- * Returns a preallocated framebuffer which the core can use for rendering
- * the frame into when not using SET_HW_RENDER.
- * The framebuffer returned from this call must not be used
- * after the current call to retro_run() returns.
- *
- * The goal of this call is to allow zero-copy behavior where a core
- * can render directly into video memory, avoiding extra bandwidth cost by copying
- * memory from core to video memory.
- *
- * If this call succeeds and the core renders into it,
- * the framebuffer pointer and pitch can be passed to retro_video_refresh_t.
- * If the buffer from GET_CURRENT_SOFTWARE_FRAMEBUFFER is to be used,
- * the core must pass the exact
- * same pointer as returned by GET_CURRENT_SOFTWARE_FRAMEBUFFER;
- * i.e. passing a pointer which is offset from the
- * buffer is undefined. The width, height and pitch parameters
- * must also match exactly to the values obtained from GET_CURRENT_SOFTWARE_FRAMEBUFFER.
- *
- * It is possible for a frontend to return a different pixel format
- * than the one used in SET_PIXEL_FORMAT. This can happen if the frontend
- * needs to perform conversion.
- *
- * It is still valid for a core to render to a different buffer
- * even if GET_CURRENT_SOFTWARE_FRAMEBUFFER succeeds.
- *
- * A frontend must make sure that the pointer obtained from this function is
- * writeable (and readable).
- */
-
-enum retro_hw_render_interface_type
-{
- RETRO_HW_RENDER_INTERFACE_VULKAN = 0,
- RETRO_HW_RENDER_INTERFACE_DUMMY = INT_MAX
-};
-
-/* Base struct. All retro_hw_render_interface_* types
- * contain at least these fields. */
-struct retro_hw_render_interface
-{
- enum retro_hw_render_interface_type interface_type;
- unsigned interface_version;
-};
-#define RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE (41 | RETRO_ENVIRONMENT_EXPERIMENTAL)
- /* const struct retro_hw_render_interface ** --
- * Returns an API specific rendering interface for accessing API specific data.
- * Not all HW rendering APIs support or need this.
- * The contents of the returned pointer is specific to the rendering API
- * being used. See the various headers like libretro_vulkan.h, etc.
- *
- * GET_HW_RENDER_INTERFACE cannot be called before context_reset has been called.
- * Similarly, after context_destroyed callback returns,
- * the contents of the HW_RENDER_INTERFACE are invalidated.
- */
-
-#define RETRO_ENVIRONMENT_SET_SUPPORT_ACHIEVEMENTS (42 | RETRO_ENVIRONMENT_EXPERIMENTAL)
- /* const bool * --
- * If true, the libretro implementation supports achievements
- * either via memory descriptors set with RETRO_ENVIRONMENT_SET_MEMORY_MAPS
- * or via retro_get_memory_data/retro_get_memory_size.
- *
- * This must be called before the first call to retro_run.
- */
-
-enum retro_hw_render_context_negotiation_interface_type
-{
- RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_VULKAN = 0,
- RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_DUMMY = INT_MAX
-};
-
-/* Base struct. All retro_hw_render_context_negotiation_interface_* types
- * contain at least these fields. */
-struct retro_hw_render_context_negotiation_interface
-{
- enum retro_hw_render_context_negotiation_interface_type interface_type;
- unsigned interface_version;
-};
-#define RETRO_ENVIRONMENT_SET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE (43 | RETRO_ENVIRONMENT_EXPERIMENTAL)
- /* const struct retro_hw_render_context_negotiation_interface * --
- * Sets an interface which lets the libretro core negotiate with frontend how a context is created.
- * The semantics of this interface depends on which API is used in SET_HW_RENDER earlier.
- * This interface will be used when the frontend is trying to create a HW rendering context,
- * so it will be used after SET_HW_RENDER, but before the context_reset callback.
- */
-
-/* Serialized state is incomplete in some way. Set if serialization is
- * usable in typical end-user cases but should not be relied upon to
- * implement frame-sensitive frontend features such as netplay or
- * rerecording. */
-#define RETRO_SERIALIZATION_QUIRK_INCOMPLETE (1 << 0)
-/* The core must spend some time initializing before serialization is
- * supported. retro_serialize() will initially fail; retro_unserialize()
- * and retro_serialize_size() may or may not work correctly either. */
-#define RETRO_SERIALIZATION_QUIRK_MUST_INITIALIZE (1 << 1)
-/* Serialization size may change within a session. */
-#define RETRO_SERIALIZATION_QUIRK_CORE_VARIABLE_SIZE (1 << 2)
-/* Set by the frontend to acknowledge that it supports variable-sized
- * states. */
-#define RETRO_SERIALIZATION_QUIRK_FRONT_VARIABLE_SIZE (1 << 3)
-/* Serialized state can only be loaded during the same session. */
-#define RETRO_SERIALIZATION_QUIRK_SINGLE_SESSION (1 << 4)
-/* Serialized state cannot be loaded on an architecture with a different
- * endianness from the one it was saved on. */
-#define RETRO_SERIALIZATION_QUIRK_ENDIAN_DEPENDENT (1 << 5)
-/* Serialized state cannot be loaded on a different platform from the one it
- * was saved on for reasons other than endianness, such as word size
- * dependence */
-#define RETRO_SERIALIZATION_QUIRK_PLATFORM_DEPENDENT (1 << 6)
-
-#define RETRO_ENVIRONMENT_SET_SERIALIZATION_QUIRKS 44
- /* uint64_t * --
- * Sets quirk flags associated with serialization. The frontend will zero any flags it doesn't
- * recognize or support. Should be set in either retro_init or retro_load_game, but not both.
- */
-
-
-#define RETRO_MEMDESC_CONST (1 << 0) /* The frontend will never change this memory area once retro_load_game has returned. */
-#define RETRO_MEMDESC_BIGENDIAN (1 << 1) /* The memory area contains big endian data. Default is little endian. */
-#define RETRO_MEMDESC_ALIGN_2 (1 << 16) /* All memory access in this area is aligned to their own size, or 2, whichever is smaller. */
-#define RETRO_MEMDESC_ALIGN_4 (2 << 16)
-#define RETRO_MEMDESC_ALIGN_8 (3 << 16)
-#define RETRO_MEMDESC_MINSIZE_2 (1 << 24) /* All memory in this region is accessed at least 2 bytes at the time. */
-#define RETRO_MEMDESC_MINSIZE_4 (2 << 24)
-#define RETRO_MEMDESC_MINSIZE_8 (3 << 24)
-struct retro_memory_descriptor
-{
- uint64_t flags;
-
- /* Pointer to the start of the relevant ROM or RAM chip.
- * It's strongly recommended to use 'offset' if possible, rather than
- * doing math on the pointer.
- *
- * If the same byte is mapped my multiple descriptors, their descriptors
- * must have the same pointer.
- * If 'start' does not point to the first byte in the pointer, put the
- * difference in 'offset' instead.
- *
- * May be NULL if there's nothing usable here (e.g. hardware registers and
- * open bus). No flags should be set if the pointer is NULL.
- * It's recommended to minimize the number of descriptors if possible,
- * but not mandatory. */
- void *ptr;
- size_t offset;
-
- /* This is the location in the emulated address space
- * where the mapping starts. */
- size_t start;
-
- /* Which bits must be same as in 'start' for this mapping to apply.
- * The first memory descriptor to claim a certain byte is the one
- * that applies.
- * A bit which is set in 'start' must also be set in this.
- * Can be zero, in which case each byte is assumed mapped exactly once.
- * In this case, 'len' must be a power of two. */
- size_t select;
-
- /* If this is nonzero, the set bits are assumed not connected to the
- * memory chip's address pins. */
- size_t disconnect;
-
- /* This one tells the size of the current memory area.
- * If, after start+disconnect are applied, the address is higher than
- * this, the highest bit of the address is cleared.
- *
- * If the address is still too high, the next highest bit is cleared.
- * Can be zero, in which case it's assumed to be infinite (as limited
- * by 'select' and 'disconnect'). */
- size_t len;
-
- /* To go from emulated address to physical address, the following
- * order applies:
- * Subtract 'start', pick off 'disconnect', apply 'len', add 'offset'. */
-
- /* The address space name must consist of only a-zA-Z0-9_-,
- * should be as short as feasible (maximum length is 8 plus the NUL),
- * and may not be any other address space plus one or more 0-9A-F
- * at the end.
- * However, multiple memory descriptors for the same address space is
- * allowed, and the address space name can be empty. NULL is treated
- * as empty.
- *
- * Address space names are case sensitive, but avoid lowercase if possible.
- * The same pointer may exist in multiple address spaces.
- *
- * Examples:
- * blank+blank - valid (multiple things may be mapped in the same namespace)
- * 'Sp'+'Sp' - valid (multiple things may be mapped in the same namespace)
- * 'A'+'B' - valid (neither is a prefix of each other)
- * 'S'+blank - valid ('S' is not in 0-9A-F)
- * 'a'+blank - valid ('a' is not in 0-9A-F)
- * 'a'+'A' - valid (neither is a prefix of each other)
- * 'AR'+blank - valid ('R' is not in 0-9A-F)
- * 'ARB'+blank - valid (the B can't be part of the address either, because
- * there is no namespace 'AR')
- * blank+'B' - not valid, because it's ambigous which address space B1234
- * would refer to.
- * The length can't be used for that purpose; the frontend may want
- * to append arbitrary data to an address, without a separator. */
- const char *addrspace;
-
- /* TODO: When finalizing this one, add a description field, which should be
- * "WRAM" or something roughly equally long. */
-
- /* TODO: When finalizing this one, replace 'select' with 'limit', which tells
- * which bits can vary and still refer to the same address (limit = ~select).
- * TODO: limit? range? vary? something else? */
-
- /* TODO: When finalizing this one, if 'len' is above what 'select' (or
- * 'limit') allows, it's bankswitched. Bankswitched data must have both 'len'
- * and 'select' != 0, and the mappings don't tell how the system switches the
- * banks. */
-
- /* TODO: When finalizing this one, fix the 'len' bit removal order.
- * For len=0x1800, pointer 0x1C00 should go to 0x1400, not 0x0C00.
- * Algorithm: Take bits highest to lowest, but if it goes above len, clear
- * the most recent addition and continue on the next bit.
- * TODO: Can the above be optimized? Is "remove the lowest bit set in both
- * pointer and 'len'" equivalent? */
-
- /* TODO: Some emulators (MAME?) emulate big endian systems by only accessing
- * the emulated memory in 32-bit chunks, native endian. But that's nothing
- * compared to Darek Mihocka
- * (section Emulation 103 - Nearly Free Byte Reversal) - he flips the ENTIRE
- * RAM backwards! I'll want to represent both of those, via some flags.
- *
- * I suspect MAME either didn't think of that idea, or don't want the #ifdef.
- * Not sure which, nor do I really care. */
-
- /* TODO: Some of those flags are unused and/or don't really make sense. Clean
- * them up. */
-};
-
-/* The frontend may use the largest value of 'start'+'select' in a
- * certain namespace to infer the size of the address space.
- *
- * If the address space is larger than that, a mapping with .ptr=NULL
- * should be at the end of the array, with .select set to all ones for
- * as long as the address space is big.
- *
- * Sample descriptors (minus .ptr, and RETRO_MEMFLAG_ on the flags):
- * SNES WRAM:
- * .start=0x7E0000, .len=0x20000
- * (Note that this must be mapped before the ROM in most cases; some of the
- * ROM mappers
- * try to claim $7E0000, or at least $7E8000.)
- * SNES SPC700 RAM:
- * .addrspace="S", .len=0x10000
- * SNES WRAM mirrors:
- * .flags=MIRROR, .start=0x000000, .select=0xC0E000, .len=0x2000
- * .flags=MIRROR, .start=0x800000, .select=0xC0E000, .len=0x2000
- * SNES WRAM mirrors, alternate equivalent descriptor:
- * .flags=MIRROR, .select=0x40E000, .disconnect=~0x1FFF
- * (Various similar constructions can be created by combining parts of
- * the above two.)
- * SNES LoROM (512KB, mirrored a couple of times):
- * .flags=CONST, .start=0x008000, .select=0x408000, .disconnect=0x8000, .len=512*1024
- * .flags=CONST, .start=0x400000, .select=0x400000, .disconnect=0x8000, .len=512*1024
- * SNES HiROM (4MB):
- * .flags=CONST, .start=0x400000, .select=0x400000, .len=4*1024*1024
- * .flags=CONST, .offset=0x8000, .start=0x008000, .select=0x408000, .len=4*1024*1024
- * SNES ExHiROM (8MB):
- * .flags=CONST, .offset=0, .start=0xC00000, .select=0xC00000, .len=4*1024*1024
- * .flags=CONST, .offset=4*1024*1024, .start=0x400000, .select=0xC00000, .len=4*1024*1024
- * .flags=CONST, .offset=0x8000, .start=0x808000, .select=0xC08000, .len=4*1024*1024
- * .flags=CONST, .offset=4*1024*1024+0x8000, .start=0x008000, .select=0xC08000, .len=4*1024*1024
- * Clarify the size of the address space:
- * .ptr=NULL, .select=0xFFFFFF
- * .len can be implied by .select in many of them, but was included for clarity.
- */
-
-struct retro_memory_map
-{
- const struct retro_memory_descriptor *descriptors;
- unsigned num_descriptors;
-};
-
-struct retro_controller_description
-{
- /* Human-readable description of the controller. Even if using a generic
- * input device type, this can be set to the particular device type the
- * core uses. */
- const char *desc;
-
- /* Device type passed to retro_set_controller_port_device(). If the device
- * type is a sub-class of a generic input device type, use the
- * RETRO_DEVICE_SUBCLASS macro to create an ID.
- *
- * E.g. RETRO_DEVICE_SUBCLASS(RETRO_DEVICE_JOYPAD, 1). */
- unsigned id;
-};
-
-struct retro_controller_info
-{
- const struct retro_controller_description *types;
- unsigned num_types;
-};
-
-struct retro_subsystem_memory_info
-{
- /* The extension associated with a memory type, e.g. "psram". */
- const char *extension;
-
- /* The memory type for retro_get_memory(). This should be at
- * least 0x100 to avoid conflict with standardized
- * libretro memory types. */
- unsigned type;
-};
-
-struct retro_subsystem_rom_info
-{
- /* Describes what the content is (SGB BIOS, GB ROM, etc). */
- const char *desc;
-
- /* Same definition as retro_get_system_info(). */
- const char *valid_extensions;
-
- /* Same definition as retro_get_system_info(). */
- bool need_fullpath;
-
- /* Same definition as retro_get_system_info(). */
- bool block_extract;
-
- /* This is set if the content is required to load a game.
- * If this is set to false, a zeroed-out retro_game_info can be passed. */
- bool required;
-
- /* Content can have multiple associated persistent
- * memory types (retro_get_memory()). */
- const struct retro_subsystem_memory_info *memory;
- unsigned num_memory;
-};
-
-struct retro_subsystem_info
-{
- /* Human-readable string of the subsystem type, e.g. "Super GameBoy" */
- const char *desc;
-
- /* A computer friendly short string identifier for the subsystem type.
- * This name must be [a-z].
- * E.g. if desc is "Super GameBoy", this can be "sgb".
- * This identifier can be used for command-line interfaces, etc.
- */
- const char *ident;
-
- /* Infos for each content file. The first entry is assumed to be the
- * "most significant" content for frontend purposes.
- * E.g. with Super GameBoy, the first content should be the GameBoy ROM,
- * as it is the most "significant" content to a user.
- * If a frontend creates new file paths based on the content used
- * (e.g. savestates), it should use the path for the first ROM to do so. */
- const struct retro_subsystem_rom_info *roms;
-
- /* Number of content files associated with a subsystem. */
- unsigned num_roms;
-
- /* The type passed to retro_load_game_special(). */
- unsigned id;
-};
-
-typedef void (RETRO_CALLCONV *retro_proc_address_t)(void);
-
-/* libretro API extension functions:
- * (None here so far).
- *
- * Get a symbol from a libretro core.
- * Cores should only return symbols which are actual
- * extensions to the libretro API.
- *
- * Frontends should not use this to obtain symbols to standard
- * libretro entry points (static linking or dlsym).
- *
- * The symbol name must be equal to the function name,
- * e.g. if void retro_foo(void); exists, the symbol must be called "retro_foo".
- * The returned function pointer must be cast to the corresponding type.
- */
-typedef retro_proc_address_t (RETRO_CALLCONV *retro_get_proc_address_t)(const char *sym);
-
-struct retro_get_proc_address_interface
-{
- retro_get_proc_address_t get_proc_address;
-};
-
-enum retro_log_level
-{
- RETRO_LOG_DEBUG = 0,
- RETRO_LOG_INFO,
- RETRO_LOG_WARN,
- RETRO_LOG_ERROR,
-
- RETRO_LOG_DUMMY = INT_MAX
-};
-
-/* Logging function. Takes log level argument as well. */
-typedef void (RETRO_CALLCONV *retro_log_printf_t)(enum retro_log_level level,
- const char *fmt, ...);
-
-struct retro_log_callback
-{
- retro_log_printf_t log;
-};
-
-/* Performance related functions */
-
-/* ID values for SIMD CPU features */
-#define RETRO_SIMD_SSE (1 << 0)
-#define RETRO_SIMD_SSE2 (1 << 1)
-#define RETRO_SIMD_VMX (1 << 2)
-#define RETRO_SIMD_VMX128 (1 << 3)
-#define RETRO_SIMD_AVX (1 << 4)
-#define RETRO_SIMD_NEON (1 << 5)
-#define RETRO_SIMD_SSE3 (1 << 6)
-#define RETRO_SIMD_SSSE3 (1 << 7)
-#define RETRO_SIMD_MMX (1 << 8)
-#define RETRO_SIMD_MMXEXT (1 << 9)
-#define RETRO_SIMD_SSE4 (1 << 10)
-#define RETRO_SIMD_SSE42 (1 << 11)
-#define RETRO_SIMD_AVX2 (1 << 12)
-#define RETRO_SIMD_VFPU (1 << 13)
-#define RETRO_SIMD_PS (1 << 14)
-#define RETRO_SIMD_AES (1 << 15)
-#define RETRO_SIMD_VFPV3 (1 << 16)
-#define RETRO_SIMD_VFPV4 (1 << 17)
-#define RETRO_SIMD_POPCNT (1 << 18)
-#define RETRO_SIMD_MOVBE (1 << 19)
-#define RETRO_SIMD_CMOV (1 << 20)
-#define RETRO_SIMD_ASIMD (1 << 21)
-
-typedef uint64_t retro_perf_tick_t;
-typedef int64_t retro_time_t;
-
-struct retro_perf_counter
-{
- const char *ident;
- retro_perf_tick_t start;
- retro_perf_tick_t total;
- retro_perf_tick_t call_cnt;
-
- bool registered;
-};
-
-/* Returns current time in microseconds.
- * Tries to use the most accurate timer available.
- */
-typedef retro_time_t (RETRO_CALLCONV *retro_perf_get_time_usec_t)(void);
-
-/* A simple counter. Usually nanoseconds, but can also be CPU cycles.
- * Can be used directly if desired (when creating a more sophisticated
- * performance counter system).
- * */
-typedef retro_perf_tick_t (RETRO_CALLCONV *retro_perf_get_counter_t)(void);
-
-/* Returns a bit-mask of detected CPU features (RETRO_SIMD_*). */
-typedef uint64_t (RETRO_CALLCONV *retro_get_cpu_features_t)(void);
-
-/* Asks frontend to log and/or display the state of performance counters.
- * Performance counters can always be poked into manually as well.
- */
-typedef void (RETRO_CALLCONV *retro_perf_log_t)(void);
-
-/* Register a performance counter.
- * ident field must be set with a discrete value and other values in
- * retro_perf_counter must be 0.
- * Registering can be called multiple times. To avoid calling to
- * frontend redundantly, you can check registered field first. */
-typedef void (RETRO_CALLCONV *retro_perf_register_t)(struct retro_perf_counter *counter);
-
-/* Starts a registered counter. */
-typedef void (RETRO_CALLCONV *retro_perf_start_t)(struct retro_perf_counter *counter);
-
-/* Stops a registered counter. */
-typedef void (RETRO_CALLCONV *retro_perf_stop_t)(struct retro_perf_counter *counter);
-
-/* For convenience it can be useful to wrap register, start and stop in macros.
- * E.g.:
- * #ifdef LOG_PERFORMANCE
- * #define RETRO_PERFORMANCE_INIT(perf_cb, name) static struct retro_perf_counter name = {#name}; if (!name.registered) perf_cb.perf_register(&(name))
- * #define RETRO_PERFORMANCE_START(perf_cb, name) perf_cb.perf_start(&(name))
- * #define RETRO_PERFORMANCE_STOP(perf_cb, name) perf_cb.perf_stop(&(name))
- * #else
- * ... Blank macros ...
- * #endif
- *
- * These can then be used mid-functions around code snippets.
- *
- * extern struct retro_perf_callback perf_cb; * Somewhere in the core.
- *
- * void do_some_heavy_work(void)
- * {
- * RETRO_PERFORMANCE_INIT(cb, work_1;
- * RETRO_PERFORMANCE_START(cb, work_1);
- * heavy_work_1();
- * RETRO_PERFORMANCE_STOP(cb, work_1);
- *
- * RETRO_PERFORMANCE_INIT(cb, work_2);
- * RETRO_PERFORMANCE_START(cb, work_2);
- * heavy_work_2();
- * RETRO_PERFORMANCE_STOP(cb, work_2);
- * }
- *
- * void retro_deinit(void)
- * {
- * perf_cb.perf_log(); * Log all perf counters here for example.
- * }
- */
-
-struct retro_perf_callback
-{
- retro_perf_get_time_usec_t get_time_usec;
- retro_get_cpu_features_t get_cpu_features;
-
- retro_perf_get_counter_t get_perf_counter;
- retro_perf_register_t perf_register;
- retro_perf_start_t perf_start;
- retro_perf_stop_t perf_stop;
- retro_perf_log_t perf_log;
-};
-
-/* FIXME: Document the sensor API and work out behavior.
- * It will be marked as experimental until then.
- */
-enum retro_sensor_action
-{
- RETRO_SENSOR_ACCELEROMETER_ENABLE = 0,
- RETRO_SENSOR_ACCELEROMETER_DISABLE,
-
- RETRO_SENSOR_DUMMY = INT_MAX
-};
-
-/* Id values for SENSOR types. */
-#define RETRO_SENSOR_ACCELEROMETER_X 0
-#define RETRO_SENSOR_ACCELEROMETER_Y 1
-#define RETRO_SENSOR_ACCELEROMETER_Z 2
-
-typedef bool (RETRO_CALLCONV *retro_set_sensor_state_t)(unsigned port,
- enum retro_sensor_action action, unsigned rate);
-
-typedef float (RETRO_CALLCONV *retro_sensor_get_input_t)(unsigned port, unsigned id);
-
-struct retro_sensor_interface
-{
- retro_set_sensor_state_t set_sensor_state;
- retro_sensor_get_input_t get_sensor_input;
-};
-
-enum retro_camera_buffer
-{
- RETRO_CAMERA_BUFFER_OPENGL_TEXTURE = 0,
- RETRO_CAMERA_BUFFER_RAW_FRAMEBUFFER,
-
- RETRO_CAMERA_BUFFER_DUMMY = INT_MAX
-};
-
-/* Starts the camera driver. Can only be called in retro_run(). */
-typedef bool (RETRO_CALLCONV *retro_camera_start_t)(void);
-
-/* Stops the camera driver. Can only be called in retro_run(). */
-typedef void (RETRO_CALLCONV *retro_camera_stop_t)(void);
-
-/* Callback which signals when the camera driver is initialized
- * and/or deinitialized.
- * retro_camera_start_t can be called in initialized callback.
- */
-typedef void (RETRO_CALLCONV *retro_camera_lifetime_status_t)(void);
-
-/* A callback for raw framebuffer data. buffer points to an XRGB8888 buffer.
- * Width, height and pitch are similar to retro_video_refresh_t.
- * First pixel is top-left origin.
- */
-typedef void (RETRO_CALLCONV *retro_camera_frame_raw_framebuffer_t)(const uint32_t *buffer,
- unsigned width, unsigned height, size_t pitch);
-
-/* A callback for when OpenGL textures are used.
- *
- * texture_id is a texture owned by camera driver.
- * Its state or content should be considered immutable, except for things like
- * texture filtering and clamping.
- *
- * texture_target is the texture target for the GL texture.
- * These can include e.g. GL_TEXTURE_2D, GL_TEXTURE_RECTANGLE, and possibly
- * more depending on extensions.
- *
- * affine points to a packed 3x3 column-major matrix used to apply an affine
- * transform to texture coordinates. (affine_matrix * vec3(coord_x, coord_y, 1.0))
- * After transform, normalized texture coord (0, 0) should be bottom-left
- * and (1, 1) should be top-right (or (width, height) for RECTANGLE).
- *
- * GL-specific typedefs are avoided here to avoid relying on gl.h in
- * the API definition.
- */
-typedef void (RETRO_CALLCONV *retro_camera_frame_opengl_texture_t)(unsigned texture_id,
- unsigned texture_target, const float *affine);
-
-struct retro_camera_callback
-{
- /* Set by libretro core.
- * Example bitmask: caps = (1 << RETRO_CAMERA_BUFFER_OPENGL_TEXTURE) | (1 << RETRO_CAMERA_BUFFER_RAW_FRAMEBUFFER).
- */
- uint64_t caps;
-
- /* Desired resolution for camera. Is only used as a hint. */
- unsigned width;
- unsigned height;
-
- /* Set by frontend. */
- retro_camera_start_t start;
- retro_camera_stop_t stop;
-
- /* Set by libretro core if raw framebuffer callbacks will be used. */
- retro_camera_frame_raw_framebuffer_t frame_raw_framebuffer;
-
- /* Set by libretro core if OpenGL texture callbacks will be used. */
- retro_camera_frame_opengl_texture_t frame_opengl_texture;
-
- /* Set by libretro core. Called after camera driver is initialized and
- * ready to be started.
- * Can be NULL, in which this callback is not called.
- */
- retro_camera_lifetime_status_t initialized;
-
- /* Set by libretro core. Called right before camera driver is
- * deinitialized.
- * Can be NULL, in which this callback is not called.
- */
- retro_camera_lifetime_status_t deinitialized;
-};
-
-/* Sets the interval of time and/or distance at which to update/poll
- * location-based data.
- *
- * To ensure compatibility with all location-based implementations,
- * values for both interval_ms and interval_distance should be provided.
- *
- * interval_ms is the interval expressed in milliseconds.
- * interval_distance is the distance interval expressed in meters.
- */
-typedef void (RETRO_CALLCONV *retro_location_set_interval_t)(unsigned interval_ms,
- unsigned interval_distance);
-
-/* Start location services. The device will start listening for changes to the
- * current location at regular intervals (which are defined with
- * retro_location_set_interval_t). */
-typedef bool (RETRO_CALLCONV *retro_location_start_t)(void);
-
-/* Stop location services. The device will stop listening for changes
- * to the current location. */
-typedef void (RETRO_CALLCONV *retro_location_stop_t)(void);
-
-/* Get the position of the current location. Will set parameters to
- * 0 if no new location update has happened since the last time. */
-typedef bool (RETRO_CALLCONV *retro_location_get_position_t)(double *lat, double *lon,
- double *horiz_accuracy, double *vert_accuracy);
-
-/* Callback which signals when the location driver is initialized
- * and/or deinitialized.
- * retro_location_start_t can be called in initialized callback.
- */
-typedef void (RETRO_CALLCONV *retro_location_lifetime_status_t)(void);
-
-struct retro_location_callback
-{
- retro_location_start_t start;
- retro_location_stop_t stop;
- retro_location_get_position_t get_position;
- retro_location_set_interval_t set_interval;
-
- retro_location_lifetime_status_t initialized;
- retro_location_lifetime_status_t deinitialized;
-};
-
-enum retro_rumble_effect
-{
- RETRO_RUMBLE_STRONG = 0,
- RETRO_RUMBLE_WEAK = 1,
-
- RETRO_RUMBLE_DUMMY = INT_MAX
-};
-
-/* Sets rumble state for joypad plugged in port 'port'.
- * Rumble effects are controlled independently,
- * and setting e.g. strong rumble does not override weak rumble.
- * Strength has a range of [0, 0xffff].
- *
- * Returns true if rumble state request was honored.
- * Calling this before first retro_run() is likely to return false. */
-typedef bool (RETRO_CALLCONV *retro_set_rumble_state_t)(unsigned port,
- enum retro_rumble_effect effect, uint16_t strength);
-
-struct retro_rumble_interface
-{
- retro_set_rumble_state_t set_rumble_state;
-};
-
-/* Notifies libretro that audio data should be written. */
-typedef void (RETRO_CALLCONV *retro_audio_callback_t)(void);
-
-/* True: Audio driver in frontend is active, and callback is
- * expected to be called regularily.
- * False: Audio driver in frontend is paused or inactive.
- * Audio callback will not be called until set_state has been
- * called with true.
- * Initial state is false (inactive).
- */
-typedef void (RETRO_CALLCONV *retro_audio_set_state_callback_t)(bool enabled);
-
-struct retro_audio_callback
-{
- retro_audio_callback_t callback;
- retro_audio_set_state_callback_t set_state;
-};
-
-/* Notifies a libretro core of time spent since last invocation
- * of retro_run() in microseconds.
- *
- * It will be called right before retro_run() every frame.
- * The frontend can tamper with timing to support cases like
- * fast-forward, slow-motion and framestepping.
- *
- * In those scenarios the reference frame time value will be used. */
-typedef int64_t retro_usec_t;
-typedef void (RETRO_CALLCONV *retro_frame_time_callback_t)(retro_usec_t usec);
-struct retro_frame_time_callback
-{
- retro_frame_time_callback_t callback;
- /* Represents the time of one frame. It is computed as
- * 1000000 / fps, but the implementation will resolve the
- * rounding to ensure that framestepping, etc is exact. */
- retro_usec_t reference;
-};
-
-/* Pass this to retro_video_refresh_t if rendering to hardware.
- * Passing NULL to retro_video_refresh_t is still a frame dupe as normal.
- * */
-#define RETRO_HW_FRAME_BUFFER_VALID ((void*)-1)
-
-/* Invalidates the current HW context.
- * Any GL state is lost, and must not be deinitialized explicitly.
- * If explicit deinitialization is desired by the libretro core,
- * it should implement context_destroy callback.
- * If called, all GPU resources must be reinitialized.
- * Usually called when frontend reinits video driver.
- * Also called first time video driver is initialized,
- * allowing libretro core to initialize resources.
- */
-typedef void (RETRO_CALLCONV *retro_hw_context_reset_t)(void);
-
-/* Gets current framebuffer which is to be rendered to.
- * Could change every frame potentially.
- */
-typedef uintptr_t (RETRO_CALLCONV *retro_hw_get_current_framebuffer_t)(void);
-
-/* Get a symbol from HW context. */
-typedef retro_proc_address_t (RETRO_CALLCONV *retro_hw_get_proc_address_t)(const char *sym);
-
-enum retro_hw_context_type
-{
- RETRO_HW_CONTEXT_NONE = 0,
- /* OpenGL 2.x. Driver can choose to use latest compatibility context. */
- RETRO_HW_CONTEXT_OPENGL = 1,
- /* OpenGL ES 2.0. */
- RETRO_HW_CONTEXT_OPENGLES2 = 2,
- /* Modern desktop core GL context. Use version_major/
- * version_minor fields to set GL version. */
- RETRO_HW_CONTEXT_OPENGL_CORE = 3,
- /* OpenGL ES 3.0 */
- RETRO_HW_CONTEXT_OPENGLES3 = 4,
- /* OpenGL ES 3.1+. Set version_major/version_minor. For GLES2 and GLES3,
- * use the corresponding enums directly. */
- RETRO_HW_CONTEXT_OPENGLES_VERSION = 5,
-
- /* Vulkan, see RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE. */
- RETRO_HW_CONTEXT_VULKAN = 6,
-
- RETRO_HW_CONTEXT_DUMMY = INT_MAX
-};
-
-struct retro_hw_render_callback
-{
- /* Which API to use. Set by libretro core. */
- enum retro_hw_context_type context_type;
-
- /* Called when a context has been created or when it has been reset.
- * An OpenGL context is only valid after context_reset() has been called.
- *
- * When context_reset is called, OpenGL resources in the libretro
- * implementation are guaranteed to be invalid.
- *
- * It is possible that context_reset is called multiple times during an
- * application lifecycle.
- * If context_reset is called without any notification (context_destroy),
- * the OpenGL context was lost and resources should just be recreated
- * without any attempt to "free" old resources.
- */
- retro_hw_context_reset_t context_reset;
-
- /* Set by frontend.
- * TODO: This is rather obsolete. The frontend should not
- * be providing preallocated framebuffers. */
- retro_hw_get_current_framebuffer_t get_current_framebuffer;
-
- /* Set by frontend.
- * Can return all relevant functions, including glClear on Windows. */
- retro_hw_get_proc_address_t get_proc_address;
-
- /* Set if render buffers should have depth component attached.
- * TODO: Obsolete. */
- bool depth;
-
- /* Set if stencil buffers should be attached.
- * TODO: Obsolete. */
- bool stencil;
-
- /* If depth and stencil are true, a packed 24/8 buffer will be added.
- * Only attaching stencil is invalid and will be ignored. */
-
- /* Use conventional bottom-left origin convention. If false,
- * standard libretro top-left origin semantics are used.
- * TODO: Move to GL specific interface. */
- bool bottom_left_origin;
-
- /* Major version number for core GL context or GLES 3.1+. */
- unsigned version_major;
-
- /* Minor version number for core GL context or GLES 3.1+. */
- unsigned version_minor;
-
- /* If this is true, the frontend will go very far to avoid
- * resetting context in scenarios like toggling fullscreen, etc.
- * TODO: Obsolete? Maybe frontend should just always assume this ...
- */
- bool cache_context;
-
- /* The reset callback might still be called in extreme situations
- * such as if the context is lost beyond recovery.
- *
- * For optimal stability, set this to false, and allow context to be
- * reset at any time.
- */
-
- /* A callback to be called before the context is destroyed in a
- * controlled way by the frontend. */
- retro_hw_context_reset_t context_destroy;
-
- /* OpenGL resources can be deinitialized cleanly at this step.
- * context_destroy can be set to NULL, in which resources will
- * just be destroyed without any notification.
- *
- * Even when context_destroy is non-NULL, it is possible that
- * context_reset is called without any destroy notification.
- * This happens if context is lost by external factors (such as
- * notified by GL_ARB_robustness).
- *
- * In this case, the context is assumed to be already dead,
- * and the libretro implementation must not try to free any OpenGL
- * resources in the subsequent context_reset.
- */
-
- /* Creates a debug context. */
- bool debug_context;
-};
-
-/* Callback type passed in RETRO_ENVIRONMENT_SET_KEYBOARD_CALLBACK.
- * Called by the frontend in response to keyboard events.
- * down is set if the key is being pressed, or false if it is being released.
- * keycode is the RETROK value of the char.
- * character is the text character of the pressed key. (UTF-32).
- * key_modifiers is a set of RETROKMOD values or'ed together.
- *
- * The pressed/keycode state can be indepedent of the character.
- * It is also possible that multiple characters are generated from a
- * single keypress.
- * Keycode events should be treated separately from character events.
- * However, when possible, the frontend should try to synchronize these.
- * If only a character is posted, keycode should be RETROK_UNKNOWN.
- *
- * Similarily if only a keycode event is generated with no corresponding
- * character, character should be 0.
- */
-typedef void (RETRO_CALLCONV *retro_keyboard_event_t)(bool down, unsigned keycode,
- uint32_t character, uint16_t key_modifiers);
-
-struct retro_keyboard_callback
-{
- retro_keyboard_event_t callback;
-};
-
-/* Callbacks for RETRO_ENVIRONMENT_SET_DISK_CONTROL_INTERFACE.
- * Should be set for implementations which can swap out multiple disk
- * images in runtime.
- *
- * If the implementation can do this automatically, it should strive to do so.
- * However, there are cases where the user must manually do so.
- *
- * Overview: To swap a disk image, eject the disk image with
- * set_eject_state(true).
- * Set the disk index with set_image_index(index). Insert the disk again
- * with set_eject_state(false).
- */
-
-/* If ejected is true, "ejects" the virtual disk tray.
- * When ejected, the disk image index can be set.
- */
-typedef bool (RETRO_CALLCONV *retro_set_eject_state_t)(bool ejected);
-
-/* Gets current eject state. The initial state is 'not ejected'. */
-typedef bool (RETRO_CALLCONV *retro_get_eject_state_t)(void);
-
-/* Gets current disk index. First disk is index 0.
- * If return value is >= get_num_images(), no disk is currently inserted.
- */
-typedef unsigned (RETRO_CALLCONV *retro_get_image_index_t)(void);
-
-/* Sets image index. Can only be called when disk is ejected.
- * The implementation supports setting "no disk" by using an
- * index >= get_num_images().
- */
-typedef bool (RETRO_CALLCONV *retro_set_image_index_t)(unsigned index);
-
-/* Gets total number of images which are available to use. */
-typedef unsigned (RETRO_CALLCONV *retro_get_num_images_t)(void);
-
-struct retro_game_info;
-
-/* Replaces the disk image associated with index.
- * Arguments to pass in info have same requirements as retro_load_game().
- * Virtual disk tray must be ejected when calling this.
- *
- * Replacing a disk image with info = NULL will remove the disk image
- * from the internal list.
- * As a result, calls to get_image_index() can change.
- *
- * E.g. replace_image_index(1, NULL), and previous get_image_index()
- * returned 4 before.
- * Index 1 will be removed, and the new index is 3.
- */
-typedef bool (RETRO_CALLCONV *retro_replace_image_index_t)(unsigned index,
- const struct retro_game_info *info);
-
-/* Adds a new valid index (get_num_images()) to the internal disk list.
- * This will increment subsequent return values from get_num_images() by 1.
- * This image index cannot be used until a disk image has been set
- * with replace_image_index. */
-typedef bool (RETRO_CALLCONV *retro_add_image_index_t)(void);
-
-struct retro_disk_control_callback
-{
- retro_set_eject_state_t set_eject_state;
- retro_get_eject_state_t get_eject_state;
-
- retro_get_image_index_t get_image_index;
- retro_set_image_index_t set_image_index;
- retro_get_num_images_t get_num_images;
-
- retro_replace_image_index_t replace_image_index;
- retro_add_image_index_t add_image_index;
-};
-
-enum retro_pixel_format
-{
- /* 0RGB1555, native endian.
- * 0 bit must be set to 0.
- * This pixel format is default for compatibility concerns only.
- * If a 15/16-bit pixel format is desired, consider using RGB565. */
- RETRO_PIXEL_FORMAT_0RGB1555 = 0,
-
- /* XRGB8888, native endian.
- * X bits are ignored. */
- RETRO_PIXEL_FORMAT_XRGB8888 = 1,
-
- /* RGB565, native endian.
- * This pixel format is the recommended format to use if a 15/16-bit
- * format is desired as it is the pixel format that is typically
- * available on a wide range of low-power devices.
- *
- * It is also natively supported in APIs like OpenGL ES. */
- RETRO_PIXEL_FORMAT_RGB565 = 2,
-
- /* Ensure sizeof() == sizeof(int). */
- RETRO_PIXEL_FORMAT_UNKNOWN = INT_MAX
-};
-
-struct retro_message
-{
- const char *msg; /* Message to be displayed. */
- unsigned frames; /* Duration in frames of message. */
-};
-
-/* Describes how the libretro implementation maps a libretro input bind
- * to its internal input system through a human readable string.
- * This string can be used to better let a user configure input. */
-struct retro_input_descriptor
-{
- /* Associates given parameters with a description. */
- unsigned port;
- unsigned device;
- unsigned index;
- unsigned id;
-
- /* Human readable description for parameters.
- * The pointer must remain valid until
- * retro_unload_game() is called. */
- const char *description;
-};
-
-struct retro_system_info
-{
- /* All pointers are owned by libretro implementation, and pointers must
- * remain valid until retro_deinit() is called. */
-
- const char *library_name; /* Descriptive name of library. Should not
- * contain any version numbers, etc. */
- const char *library_version; /* Descriptive version of core. */
-
- const char *valid_extensions; /* A string listing probably content
- * extensions the core will be able to
- * load, separated with pipe.
- * I.e. "bin|rom|iso".
- * Typically used for a GUI to filter
- * out extensions. */
-
- /* If true, retro_load_game() is guaranteed to provide a valid pathname
- * in retro_game_info::path.
- * ::data and ::size are both invalid.
- *
- * If false, ::data and ::size are guaranteed to be valid, but ::path
- * might not be valid.
- *
- * This is typically set to true for libretro implementations that must
- * load from file.
- * Implementations should strive for setting this to false, as it allows
- * the frontend to perform patching, etc. */
- bool need_fullpath;
-
- /* If true, the frontend is not allowed to extract any archives before
- * loading the real content.
- * Necessary for certain libretro implementations that load games
- * from zipped archives. */
- bool block_extract;
-};
-
-struct retro_game_geometry
-{
- unsigned base_width; /* Nominal video width of game. */
- unsigned base_height; /* Nominal video height of game. */
- unsigned max_width; /* Maximum possible width of game. */
- unsigned max_height; /* Maximum possible height of game. */
-
- float aspect_ratio; /* Nominal aspect ratio of game. If
- * aspect_ratio is <= 0.0, an aspect ratio
- * of base_width / base_height is assumed.
- * A frontend could override this setting,
- * if desired. */
-};
-
-struct retro_system_timing
-{
- double fps; /* FPS of video content. */
- double sample_rate; /* Sampling rate of audio. */
-};
-
-struct retro_system_av_info
-{
- struct retro_game_geometry geometry;
- struct retro_system_timing timing;
-};
-
-struct retro_variable
-{
- /* Variable to query in RETRO_ENVIRONMENT_GET_VARIABLE.
- * If NULL, obtains the complete environment string if more
- * complex parsing is necessary.
- * The environment string is formatted as key-value pairs
- * delimited by semicolons as so:
- * "key1=value1;key2=value2;..."
- */
- const char *key;
-
- /* Value to be obtained. If key does not exist, it is set to NULL. */
- const char *value;
-};
-
-struct retro_game_info
-{
- const char *path; /* Path to game, UTF-8 encoded.
- * Sometimes used as a reference for building other paths.
- * May be NULL if game was loaded from stdin or similar,
- * but in this case some cores will be unable to load `data`.
- * So, it is preferable to fabricate something here instead
- * of passing NULL, which will help more cores to succeed.
- * retro_system_info::need_fullpath requires
- * that this path is valid. */
- const void *data; /* Memory buffer of loaded game. Will be NULL
- * if need_fullpath was set. */
- size_t size; /* Size of memory buffer. */
- const char *meta; /* String of implementation specific meta-data. */
-};
-
-#define RETRO_MEMORY_ACCESS_WRITE (1 << 0)
- /* The core will write to the buffer provided by retro_framebuffer::data. */
-#define RETRO_MEMORY_ACCESS_READ (1 << 1)
- /* The core will read from retro_framebuffer::data. */
-#define RETRO_MEMORY_TYPE_CACHED (1 << 0)
- /* The memory in data is cached.
- * If not cached, random writes and/or reading from the buffer is expected to be very slow. */
-struct retro_framebuffer
-{
- void *data; /* The framebuffer which the core can render into.
- Set by frontend in GET_CURRENT_SOFTWARE_FRAMEBUFFER.
- The initial contents of data are unspecified. */
- unsigned width; /* The framebuffer width used by the core. Set by core. */
- unsigned height; /* The framebuffer height used by the core. Set by core. */
- size_t pitch; /* The number of bytes between the beginning of a scanline,
- and beginning of the next scanline.
- Set by frontend in GET_CURRENT_SOFTWARE_FRAMEBUFFER. */
- enum retro_pixel_format format; /* The pixel format the core must use to render into data.
- This format could differ from the format used in
- SET_PIXEL_FORMAT.
- Set by frontend in GET_CURRENT_SOFTWARE_FRAMEBUFFER. */
-
- unsigned access_flags; /* How the core will access the memory in the framebuffer.
- RETRO_MEMORY_ACCESS_* flags.
- Set by core. */
- unsigned memory_flags; /* Flags telling core how the memory has been mapped.
- RETRO_MEMORY_TYPE_* flags.
- Set by frontend in GET_CURRENT_SOFTWARE_FRAMEBUFFER. */
-};
-
-/* Callbacks */
-
-/* Environment callback. Gives implementations a way of performing
- * uncommon tasks. Extensible. */
-typedef bool (RETRO_CALLCONV *retro_environment_t)(unsigned cmd, void *data);
-
-/* Render a frame. Pixel format is 15-bit 0RGB1555 native endian
- * unless changed (see RETRO_ENVIRONMENT_SET_PIXEL_FORMAT).
- *
- * Width and height specify dimensions of buffer.
- * Pitch specifices length in bytes between two lines in buffer.
- *
- * For performance reasons, it is highly recommended to have a frame
- * that is packed in memory, i.e. pitch == width * byte_per_pixel.
- * Certain graphic APIs, such as OpenGL ES, do not like textures
- * that are not packed in memory.
- */
-typedef void (RETRO_CALLCONV *retro_video_refresh_t)(const void *data, unsigned width,
- unsigned height, size_t pitch);
-
-/* Renders a single audio frame. Should only be used if implementation
- * generates a single sample at a time.
- * Format is signed 16-bit native endian.
- */
-typedef void (RETRO_CALLCONV *retro_audio_sample_t)(int16_t left, int16_t right);
-
-/* Renders multiple audio frames in one go.
- *
- * One frame is defined as a sample of left and right channels, interleaved.
- * I.e. int16_t buf[4] = { l, r, l, r }; would be 2 frames.
- * Only one of the audio callbacks must ever be used.
- */
-typedef size_t (RETRO_CALLCONV *retro_audio_sample_batch_t)(const int16_t *data,
- size_t frames);
-
-/* Polls input. */
-typedef void (RETRO_CALLCONV *retro_input_poll_t)(void);
-
-/* Queries for input for player 'port'. device will be masked with
- * RETRO_DEVICE_MASK.
- *
- * Specialization of devices such as RETRO_DEVICE_JOYPAD_MULTITAP that
- * have been set with retro_set_controller_port_device()
- * will still use the higher level RETRO_DEVICE_JOYPAD to request input.
- */
-typedef int16_t (RETRO_CALLCONV *retro_input_state_t)(unsigned port, unsigned device,
- unsigned index, unsigned id);
-
-/* Sets callbacks. retro_set_environment() is guaranteed to be called
- * before retro_init().
- *
- * The rest of the set_* functions are guaranteed to have been called
- * before the first call to retro_run() is made. */
-RETRO_API void retro_set_environment(retro_environment_t);
-RETRO_API void retro_set_video_refresh(retro_video_refresh_t);
-RETRO_API void retro_set_audio_sample(retro_audio_sample_t);
-RETRO_API void retro_set_audio_sample_batch(retro_audio_sample_batch_t);
-RETRO_API void retro_set_input_poll(retro_input_poll_t);
-RETRO_API void retro_set_input_state(retro_input_state_t);
-
-/* Library global initialization/deinitialization. */
-RETRO_API void retro_init(void);
-RETRO_API void retro_deinit(void);
-
-/* Must return RETRO_API_VERSION. Used to validate ABI compatibility
- * when the API is revised. */
-RETRO_API unsigned retro_api_version(void);
-
-/* Gets statically known system info. Pointers provided in *info
- * must be statically allocated.
- * Can be called at any time, even before retro_init(). */
-RETRO_API void retro_get_system_info(struct retro_system_info *info);
-
-/* Gets information about system audio/video timings and geometry.
- * Can be called only after retro_load_game() has successfully completed.
- * NOTE: The implementation of this function might not initialize every
- * variable if needed.
- * E.g. geom.aspect_ratio might not be initialized if core doesn't
- * desire a particular aspect ratio. */
-RETRO_API void retro_get_system_av_info(struct retro_system_av_info *info);
-
-/* Sets device to be used for player 'port'.
- * By default, RETRO_DEVICE_JOYPAD is assumed to be plugged into all
- * available ports.
- * Setting a particular device type is not a guarantee that libretro cores
- * will only poll input based on that particular device type. It is only a
- * hint to the libretro core when a core cannot automatically detect the
- * appropriate input device type on its own. It is also relevant when a
- * core can change its behavior depending on device type. */
-RETRO_API void retro_set_controller_port_device(unsigned port, unsigned device);
-
-/* Resets the current game. */
-RETRO_API void retro_reset(void);
-
-/* Runs the game for one video frame.
- * During retro_run(), input_poll callback must be called at least once.
- *
- * If a frame is not rendered for reasons where a game "dropped" a frame,
- * this still counts as a frame, and retro_run() should explicitly dupe
- * a frame if GET_CAN_DUPE returns true.
- * In this case, the video callback can take a NULL argument for data.
- */
-RETRO_API void retro_run(void);
-
-/* Returns the amount of data the implementation requires to serialize
- * internal state (save states).
- * Between calls to retro_load_game() and retro_unload_game(), the
- * returned size is never allowed to be larger than a previous returned
- * value, to ensure that the frontend can allocate a save state buffer once.
- */
-RETRO_API size_t retro_serialize_size(void);
-
-/* Serializes internal state. If failed, or size is lower than
- * retro_serialize_size(), it should return false, true otherwise. */
-RETRO_API bool retro_serialize(void *data, size_t size);
-RETRO_API bool retro_unserialize(const void *data, size_t size);
-
-RETRO_API void retro_cheat_reset(void);
-RETRO_API void retro_cheat_set(unsigned index, bool enabled, const char *code);
-
-/* Loads a game. */
-RETRO_API bool retro_load_game(const struct retro_game_info *game);
-
-/* Loads a "special" kind of game. Should not be used,
- * except in extreme cases. */
-RETRO_API bool retro_load_game_special(
- unsigned game_type,
- const struct retro_game_info *info, size_t num_info
-);
-
-/* Unloads a currently loaded game. */
-RETRO_API void retro_unload_game(void);
-
-/* Gets region of game. */
-RETRO_API unsigned retro_get_region(void);
-
-/* Gets region of memory. */
-RETRO_API void *retro_get_memory_data(unsigned id);
-RETRO_API size_t retro_get_memory_size(unsigned id);
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif
-
diff --git a/pkg/emulator/libretro/nanoarch/naemulator.go b/pkg/emulator/libretro/nanoarch/naemulator.go
deleted file mode 100644
index 284668f5..00000000
--- a/pkg/emulator/libretro/nanoarch/naemulator.go
+++ /dev/null
@@ -1,264 +0,0 @@
-package nanoarch
-
-import (
- "bytes"
- "encoding/gob"
- "fmt"
- "image"
- "log"
- "net"
- "sync"
- "time"
-
- config "github.com/giongto35/cloud-game/v2/pkg/config/emulator"
- "github.com/giongto35/cloud-game/v2/pkg/emulator"
-)
-
-/*
-#include "libretro.h"
-#cgo LDFLAGS: -ldl
-#include
-#include
-#include
-#include
-
-void bridge_retro_deinit(void *f);
-unsigned bridge_retro_api_version(void *f);
-void bridge_retro_get_system_info(void *f, struct retro_system_info *si);
-void bridge_retro_get_system_av_info(void *f, struct retro_system_av_info *si);
-bool bridge_retro_set_environment(void *f, void *callback);
-void bridge_retro_set_video_refresh(void *f, void *callback);
-void bridge_retro_set_input_poll(void *f, void *callback);
-void bridge_retro_set_input_state(void *f, void *callback);
-void bridge_retro_set_audio_sample(void *f, void *callback);
-void bridge_retro_set_audio_sample_batch(void *f, void *callback);
-bool bridge_retro_load_game(void *f, struct retro_game_info *gi);
-void bridge_retro_run(void *f);
-size_t bridge_retro_get_memory_size(void *f, unsigned id);
-void* bridge_retro_get_memory_data(void *f, unsigned id);
-bool bridge_retro_serialize(void *f, void *data, size_t size);
-bool bridge_retro_unserialize(void *f, void *data, size_t size);
-size_t bridge_retro_serialize_size(void *f);
-
-bool coreEnvironment_cgo(unsigned cmd, void *data);
-void coreVideoRefresh_cgo(void *data, unsigned width, unsigned height, size_t pitch);
-void coreInputPoll_cgo();
-void coreAudioSample_cgo(int16_t left, int16_t right);
-size_t coreAudioSampleBatch_cgo(const int16_t *data, size_t frames);
-int16_t coreInputState_cgo(unsigned port, unsigned device, unsigned index, unsigned id);
-void coreLog_cgo(enum retro_log_level level, const char *msg);
-*/
-import "C"
-
-// naEmulator implements CloudEmulator
-type naEmulator struct {
- sync.Mutex
-
- imageChannel chan<- GameFrame
- audioChannel chan<- []int16
- inputChannel <-chan InputEvent
- videoExporter *VideoExporter
-
- meta emulator.Metadata
- gamePath string
- roomID string
- gameName string
- isSavingLoading bool
- storage Storage
-
- players Players
-
- done chan struct{}
-}
-
-type Storage struct {
- // save path without the dir slash in the end
- Path string
- // contains the name of the main save file
- // e.g. abc<...>293.dat
- // needed for Google Cloud save/restore which
- // doesn't support multiple files
- MainSave string
-}
-
-// VideoExporter produces image frame to unix socket
-type VideoExporter struct {
- sock net.Conn
- imageChannel chan<- GameFrame
-}
-
-// GameFrame contains image and timeframe
-type GameFrame struct {
- Image *image.RGBA
- Timestamp uint32
-}
-
-var NAEmulator *naEmulator
-var outputImg *image.RGBA
-
-// NAEmulator implements CloudEmulator interface based on NanoArch(golang RetroArch)
-func NewNAEmulator(roomID string, inputChannel <-chan InputEvent, storage Storage, conf config.LibretroCoreConfig) (*naEmulator, chan GameFrame, chan []int16) {
- imageChannel := make(chan GameFrame, 30)
- audioChannel := make(chan []int16, 30)
-
- return &naEmulator{
- meta: emulator.Metadata{
- LibPath: conf.Lib,
- ConfigPath: conf.Config,
- Ratio: conf.Ratio,
- IsGlAllowed: conf.IsGlAllowed,
- UsesLibCo: conf.UsesLibCo,
- HasMultitap: conf.HasMultitap,
- AutoGlContext: conf.AutoGlContext,
- },
- storage: storage,
- imageChannel: imageChannel,
- audioChannel: audioChannel,
- inputChannel: inputChannel,
- players: NewPlayerSessionInput(),
- roomID: roomID,
- done: make(chan struct{}, 1),
- }, imageChannel, audioChannel
-}
-
-// NewVideoExporter creates new video Exporter that produces to unix socket
-func NewVideoExporter(roomID string, imgChannel chan GameFrame) *VideoExporter {
- sockAddr := fmt.Sprintf("/tmp/cloudretro-retro-%s.sock", roomID)
-
- go func(sockAddr string) {
- log.Println("Dialing to ", sockAddr)
- conn, err := net.Dial("unix", sockAddr)
- if err != nil {
- log.Fatal("accept error: ", err)
- }
-
- defer conn.Close()
-
- for img := range imgChannel {
- reqBodyBytes := new(bytes.Buffer)
- gob.NewEncoder(reqBodyBytes).Encode(img)
- //fmt.Printf("%+v %+v %+v \n", img.Image.Stride, img.Image.Rect.Max.X, len(img.Image.Pix))
- // conn.Write(img.Image.Pix)
- b := reqBodyBytes.Bytes()
- fmt.Printf("Bytes %d\n", len(b))
- conn.Write(b)
- }
- }(sockAddr)
-
- return &VideoExporter{imageChannel: imgChannel}
-}
-
-// Init initialize new RetroArch cloud emulator
-// withImageChan returns an image stream as Channel for output else it will write to unix socket
-func Init(roomID string, withImageChannel bool, inputChannel <-chan InputEvent, storage Storage, config config.LibretroCoreConfig) (*naEmulator, chan GameFrame, chan []int16) {
- emu, imageChannel, audioChannel := NewNAEmulator(roomID, inputChannel, storage, config)
- // Set to global NAEmulator
- NAEmulator = emu
- if !withImageChannel {
- NAEmulator.videoExporter = NewVideoExporter(roomID, imageChannel)
- }
-
- go NAEmulator.listenInput()
-
- return emu, imageChannel, audioChannel
-}
-
-// listenInput handles user input.
-// The user input is encoded as bitmap that we decode
-// and send into the game emulator.
-func (na *naEmulator) listenInput() {
- for in := range NAEmulator.inputChannel {
- bitmap := in.bitmap()
- if bitmap == InputTerminate {
- na.players.session.close(in.ConnID)
- continue
- }
- na.players.session.setInput(in.ConnID, in.PlayerIdx, bitmap, in.RawState)
- }
-}
-
-func (na *naEmulator) LoadMeta(path string) emulator.Metadata {
- coreLoad(na.meta)
- coreLoadGame(path)
- na.gamePath = path
- return na.meta
-}
-
-func (na *naEmulator) SetViewport(width int, height int) {
- // outputImg is tmp img used for decoding and reuse in encoding flow
- outputImg = image.NewRGBA(image.Rect(0, 0, width, height))
-}
-
-func (na *naEmulator) Start() {
- na.playGame()
- ticker := time.NewTicker(time.Second / time.Duration(na.meta.Fps))
-
- for range ticker.C {
- select {
- // Slow response here
- case <-na.done:
- nanoarchShutdown()
- close(na.imageChannel)
- close(na.audioChannel)
- log.Println("Closed Director")
- return
- default:
- }
-
- na.Lock()
- nanoarchRun()
- na.Unlock()
- }
-}
-
-func (na *naEmulator) playGame() {
- // When start game, we also try loading if there was a saved state
- na.LoadGame()
-}
-
-func (na *naEmulator) SaveGame(saveExtraFunc func() error) error {
- if na.roomID != "" {
- err := na.Save()
- if err != nil {
- return err
- }
- err = saveExtraFunc()
- if err != nil {
- return err
- }
- }
-
- return nil
-}
-
-func (na *naEmulator) LoadGame() error {
- if na.roomID != "" {
- err := na.Load()
- if err != nil {
- log.Println("Error: Cannot load", err)
- return err
- }
- }
-
- return nil
-}
-
-func (na *naEmulator) ToggleMultitap() error {
- if na.roomID != "" {
- toggleMultitap()
- }
-
- return nil
-}
-
-func (na *naEmulator) GetHashPath() string { return na.storage.Path + "/" + na.storage.MainSave }
-
-func (na *naEmulator) GetSRAMPath() string { return na.storage.Path + "/" + na.roomID + ".srm" }
-
-func (*naEmulator) GetViewport() interface{} {
- return outputImg
-}
-
-func (na *naEmulator) Close() {
- close(na.done)
-}
diff --git a/pkg/emulator/libretro/nanoarch/nanoarch.go b/pkg/emulator/libretro/nanoarch/nanoarch.go
deleted file mode 100644
index 69cead52..00000000
--- a/pkg/emulator/libretro/nanoarch/nanoarch.go
+++ /dev/null
@@ -1,674 +0,0 @@
-package nanoarch
-
-import (
- "bufio"
- "log"
- "math/rand"
- "os"
- "os/user"
- "runtime"
- "sync"
- "time"
- "unsafe"
-
- "github.com/giongto35/cloud-game/v2/pkg/emulator"
- "github.com/giongto35/cloud-game/v2/pkg/emulator/graphics"
- "github.com/giongto35/cloud-game/v2/pkg/emulator/image"
- "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/core"
- "github.com/giongto35/cloud-game/v2/pkg/thread"
-)
-
-/*
-#include "libretro.h"
-#include
-#include
-#include
-
-void bridge_retro_init(void *f);
-void bridge_retro_deinit(void *f);
-unsigned bridge_retro_api_version(void *f);
-void bridge_retro_get_system_info(void *f, struct retro_system_info *si);
-void bridge_retro_get_system_av_info(void *f, struct retro_system_av_info *si);
-bool bridge_retro_set_environment(void *f, void *callback);
-void bridge_retro_set_video_refresh(void *f, void *callback);
-void bridge_retro_set_input_poll(void *f, void *callback);
-void bridge_retro_set_input_state(void *f, void *callback);
-void bridge_retro_set_audio_sample(void *f, void *callback);
-void bridge_retro_set_audio_sample_batch(void *f, void *callback);
-bool bridge_retro_load_game(void *f, struct retro_game_info *gi);
-void bridge_retro_unload_game(void *f);
-void bridge_retro_run(void *f);
-void bridge_retro_set_controller_port_device(void *f, unsigned port, unsigned device);
-
-bool coreEnvironment_cgo(unsigned cmd, void *data);
-void coreVideoRefresh_cgo(void *data, unsigned width, unsigned height, size_t pitch);
-void coreInputPoll_cgo();
-void coreAudioSample_cgo(int16_t left, int16_t right);
-size_t coreAudioSampleBatch_cgo(const int16_t *data, size_t frames);
-int16_t coreInputState_cgo(unsigned port, unsigned device, unsigned index, unsigned id);
-void coreLog_cgo(enum retro_log_level level, const char *msg);
-uintptr_t coreGetCurrentFramebuffer_cgo();
-retro_proc_address_t coreGetProcAddress_cgo(const char *sym);
-
-void bridge_context_reset(retro_hw_context_reset_t f);
-
-void initVideo_cgo();
-void deinitVideo_cgo();
-void bridge_execute(void *f);
-*/
-import "C"
-
-var mu sync.Mutex
-
-var video struct {
- pitch uint32
- pixFmt uint32
- bpp uint32
- rotation image.Angle
-
- baseWidth int32
- baseHeight int32
- maxWidth int32
- maxHeight int32
-
- hw *C.struct_retro_hw_render_callback
- isGl bool
- autoGlContext bool
-}
-
-// default core pix format converter
-var pixelFormatConverterFn = image.Rgb565
-var rotationFn = image.GetRotation(image.Angle(0))
-
-//const joypadNumKeys = int(C.RETRO_DEVICE_ID_JOYPAD_R3 + 1)
-//var joy [joypadNumKeys]bool
-
-var isGlAllowed bool
-var usesLibCo bool
-var coreConfig ConfigProperties
-
-var multitap struct {
- supported bool
- enabled bool
- value C.unsigned
-}
-
-var systemDirectory = C.CString("./pkg/emulator/libretro/system")
-var saveDirectory = C.CString(".")
-var currentUser *C.char
-
-var seed = rand.New(rand.NewSource(time.Now().UnixNano())).Uint32()
-
-var bindKeysMap = map[int]int{
- C.RETRO_DEVICE_ID_JOYPAD_A: 0,
- C.RETRO_DEVICE_ID_JOYPAD_B: 1,
- C.RETRO_DEVICE_ID_JOYPAD_X: 2,
- C.RETRO_DEVICE_ID_JOYPAD_Y: 3,
- C.RETRO_DEVICE_ID_JOYPAD_L: 4,
- C.RETRO_DEVICE_ID_JOYPAD_R: 5,
- C.RETRO_DEVICE_ID_JOYPAD_SELECT: 6,
- C.RETRO_DEVICE_ID_JOYPAD_START: 7,
- C.RETRO_DEVICE_ID_JOYPAD_UP: 8,
- C.RETRO_DEVICE_ID_JOYPAD_DOWN: 9,
- C.RETRO_DEVICE_ID_JOYPAD_LEFT: 10,
- C.RETRO_DEVICE_ID_JOYPAD_RIGHT: 11,
- C.RETRO_DEVICE_ID_JOYPAD_R2: 12,
- C.RETRO_DEVICE_ID_JOYPAD_L2: 13,
- C.RETRO_DEVICE_ID_JOYPAD_R3: 14,
- C.RETRO_DEVICE_ID_JOYPAD_L3: 15,
-}
-
-type CloudEmulator interface {
- Start(path string)
- SaveGame(saveExtraFunc func() error) error
- LoadGame() error
- GetHashPath() string
- Close()
- ToggleMultitap() error
-}
-
-//export coreVideoRefresh
-func coreVideoRefresh(data unsafe.Pointer, width C.unsigned, height C.unsigned, pitch C.size_t) {
- // some cores can return nothing
- // !to add duplicate if can dup
- if data == nil {
- return
- }
-
- // divide by 8333 to give us the equivalent of a 120fps resolution
- timestamp := uint32(time.Now().UnixNano()/8333) + seed
- // if Libretro renders frame with OpenGL context
- isOpenGLRender := data == C.RETRO_HW_FRAME_BUFFER_VALID
-
- // calculate real frame width in pixels from packed data (realWidth >= width)
- packedWidth := int(uint32(pitch) / video.bpp)
- if packedWidth < 1 {
- packedWidth = int(width)
- }
- // calculate space for the video frame
- bytes := int(height) * packedWidth * int(video.bpp)
-
- var data_ []byte
- if isOpenGLRender {
- data_ = graphics.ReadFramebuffer(bytes, int(width), int(height))
- } else {
- data_ = (*[1 << 30]byte)(data)[:bytes:bytes]
- }
-
- // the image is being resized and de-rotated
- image.DrawRgbaImage(
- pixelFormatConverterFn,
- rotationFn,
- image.ScaleNearestNeighbour,
- isOpenGLRender,
- int(width), int(height), packedWidth, int(video.bpp),
- data_,
- outputImg,
- )
-
- // the image is pushed into a channel
- // where it will be distributed with fan-out
- NAEmulator.imageChannel <- GameFrame{Image: outputImg, Timestamp: timestamp}
-}
-
-//export coreInputPoll
-func coreInputPoll() {
-}
-
-//export coreInputState
-func coreInputState(port C.unsigned, device C.unsigned, index C.unsigned, id C.unsigned) C.int16_t {
- 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 := NAEmulator.players.isDpadTouched(uint(port), uint(axis))
- if value != 0 {
- return (C.int16_t)(value)
- }
- }
-
- if id >= 255 || index > 0 || device != C.RETRO_DEVICE_JOYPAD {
- return 0
- }
-
- // map from id to control key
- key, ok := bindKeysMap[int(id)]
- if !ok {
- return 0
- }
-
- if NAEmulator.players.isKeyPressed(uint(port), key) {
- return 1
- }
-
- return 0
-}
-
-func audioWrite(buf unsafe.Pointer, frames C.size_t) C.size_t {
- // !to make it mono/stereo independent
- samples := int(frames) * 2
- pcm := (*[(1 << 30) - 1]int16)(buf)[:samples:samples]
-
- p := make([]int16, samples)
- // copy because pcm slice refer to buf underlying pointer,
- // and buf pointer is the same in continuous frames
- copy(p, pcm)
-
- select {
- case NAEmulator.audioChannel <- p:
- default:
- }
-
- return frames
-}
-
-//export coreAudioSample
-func coreAudioSample(left C.int16_t, right C.int16_t) {
- buf := []C.int16_t{left, right}
- audioWrite(unsafe.Pointer(&buf), 1)
-}
-
-//export coreAudioSampleBatch
-func coreAudioSampleBatch(data unsafe.Pointer, frames C.size_t) C.size_t {
- return audioWrite(data, frames)
-}
-
-//export coreLog
-func coreLog(_ C.enum_retro_log_level, msg *C.char) {
- log.Printf("[Log] %v", C.GoString(msg))
-}
-
-//export coreGetCurrentFramebuffer
-func coreGetCurrentFramebuffer() C.uintptr_t {
- return (C.uintptr_t)(graphics.GetGlFbo())
-}
-
-//export coreGetProcAddress
-func coreGetProcAddress(sym *C.char) C.retro_proc_address_t {
- return (C.retro_proc_address_t)(graphics.GetGlProcAddress(C.GoString(sym)))
-}
-
-//export coreEnvironment
-func coreEnvironment(cmd C.unsigned, data unsafe.Pointer) C.bool {
- switch cmd {
- case C.RETRO_ENVIRONMENT_GET_USERNAME:
- username := (**C.char)(data)
- if currentUser == nil {
- currentUserGo, err := user.Current()
- if err != nil {
- currentUser = C.CString("")
- } else {
- currentUser = C.CString(currentUserGo.Username)
- }
- }
- *username = currentUser
- case C.RETRO_ENVIRONMENT_GET_LOG_INTERFACE:
- cb := (*C.struct_retro_log_callback)(data)
- cb.log = (C.retro_log_printf_t)(C.coreLog_cgo)
- case C.RETRO_ENVIRONMENT_GET_CAN_DUPE:
- bval := (*C.bool)(data)
- *bval = C.bool(true)
- case C.RETRO_ENVIRONMENT_SET_PIXEL_FORMAT:
- return videoSetPixelFormat(*(*C.enum_retro_pixel_format)(data))
- case C.RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY:
- path := (**C.char)(data)
- *path = systemDirectory
- return true
- case C.RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY:
- path := (**C.char)(data)
- *path = saveDirectory
- return true
- case C.RETRO_ENVIRONMENT_SHUTDOWN:
- //window.SetShouldClose(true)
- return true
- /*
- Sets screen rotation of graphics.
- Valid values are 0, 1, 2, 3, which rotates screen by 0, 90, 180, 270 degrees
- ccw respectively.
- */
- case C.RETRO_ENVIRONMENT_SET_ROTATION:
- setRotation(*(*int)(data) % 4)
- return true
- case C.RETRO_ENVIRONMENT_GET_VARIABLE:
- variable := (*C.struct_retro_variable)(data)
- key := C.GoString(variable.key)
- if val, ok := coreConfig[key]; ok {
- log.Printf("[Env]: get variable: key:%v value:%v", key, C.GoString(val))
- variable.value = val
- return true
- }
- // fmt.Printf("[Env]: get variable: key:%v not found\n", key)
- return false
- case C.RETRO_ENVIRONMENT_SET_HW_RENDER:
- video.isGl = isGlAllowed
- if isGlAllowed {
- video.hw = (*C.struct_retro_hw_render_callback)(data)
- video.hw.get_current_framebuffer = (C.retro_hw_get_current_framebuffer_t)(C.coreGetCurrentFramebuffer_cgo)
- video.hw.get_proc_address = (C.retro_hw_get_proc_address_t)(C.coreGetProcAddress_cgo)
- return true
- }
- return false
- case C.RETRO_ENVIRONMENT_SET_CONTROLLER_INFO:
- if multitap.supported {
- 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" {
- multitap.value = types[j].id
- return true
- }
- }
- }
- }
- return false
- default:
- //fmt.Println("[Env]: command not implemented", cmd)
- return false
- }
- return true
-}
-
-//export initVideo
-func initVideo() {
- var context graphics.Context
- switch video.hw.context_type {
- case C.RETRO_HW_CONTEXT_NONE:
- context = graphics.CtxNone
- case C.RETRO_HW_CONTEXT_OPENGL:
- context = graphics.CtxOpenGl
- case C.RETRO_HW_CONTEXT_OPENGLES2:
- context = graphics.CtxOpenGlEs2
- case C.RETRO_HW_CONTEXT_OPENGL_CORE:
- context = graphics.CtxOpenGlCore
- case C.RETRO_HW_CONTEXT_OPENGLES3:
- context = graphics.CtxOpenGlEs3
- case C.RETRO_HW_CONTEXT_OPENGLES_VERSION:
- context = graphics.CtxOpenGlEsVersion
- case C.RETRO_HW_CONTEXT_VULKAN:
- context = graphics.CtxVulkan
- case C.RETRO_HW_CONTEXT_DUMMY:
- context = graphics.CtxDummy
- default:
- context = graphics.CtxUnknown
- }
-
- graphics.Init(graphics.Config{
- Ctx: context,
- W: int(video.maxWidth),
- H: int(video.maxHeight),
- Gl: graphics.GlConfig{
- AutoContext: video.autoGlContext,
- VersionMajor: uint(video.hw.version_major),
- VersionMinor: uint(video.hw.version_minor),
- HasDepth: bool(video.hw.depth),
- HasStencil: bool(video.hw.stencil),
- },
- })
- C.bridge_context_reset(video.hw.context_reset)
-}
-
-//export deinitVideo
-func deinitVideo() {
- C.bridge_context_reset(video.hw.context_destroy)
- graphics.Deinit()
- video.isGl = false
- video.autoGlContext = false
-}
-
-var (
- retroAPIVersion unsafe.Pointer
- retroDeinit unsafe.Pointer
- retroGetSystemAVInfo unsafe.Pointer
- retroGetSystemInfo unsafe.Pointer
- retroHandle unsafe.Pointer
- retroInit unsafe.Pointer
- retroLoadGame unsafe.Pointer
- retroRun unsafe.Pointer
- retroSetAudioSample unsafe.Pointer
- retroSetAudioSampleBatch unsafe.Pointer
- retroSetControllerPortDevice unsafe.Pointer
- retroSetEnvironment unsafe.Pointer
- retroSetInputPoll unsafe.Pointer
- retroSetInputState unsafe.Pointer
- retroSetVideoRefresh unsafe.Pointer
- retroUnloadGame unsafe.Pointer
-)
-
-func coreLoad(meta emulator.Metadata) {
- isGlAllowed = meta.IsGlAllowed
- usesLibCo = meta.UsesLibCo
- video.autoGlContext = meta.AutoGlContext
- coreConfig = ScanConfigFile(meta.ConfigPath)
-
- multitap.supported = meta.HasMultitap
- multitap.enabled = false
- multitap.value = 0
-
- filePath := meta.LibPath
- if arch, err := core.GetCoreExt(); err == nil {
- filePath = filePath + arch.LibExt
- } else {
- log.Printf("warning: %v", err)
- }
-
- mu.Lock()
- var err error
- retroHandle, err = loadLib(filePath)
- // fallback to sequential lib loader (first successfully loaded)
- if err != nil {
- retroHandle, err = loadLibRollingRollingRolling(filePath)
- if err != nil {
- log.Fatalf("error core load: %s, %v", filePath, err)
- }
- }
-
- retroInit = loadFunction(retroHandle, "retro_init")
- retroDeinit = loadFunction(retroHandle, "retro_deinit")
- retroAPIVersion = loadFunction(retroHandle, "retro_api_version")
- retroGetSystemInfo = loadFunction(retroHandle, "retro_get_system_info")
- retroGetSystemAVInfo = loadFunction(retroHandle, "retro_get_system_av_info")
- retroSetEnvironment = loadFunction(retroHandle, "retro_set_environment")
- retroSetVideoRefresh = loadFunction(retroHandle, "retro_set_video_refresh")
- retroSetInputPoll = loadFunction(retroHandle, "retro_set_input_poll")
- retroSetInputState = loadFunction(retroHandle, "retro_set_input_state")
- retroSetAudioSample = loadFunction(retroHandle, "retro_set_audio_sample")
- retroSetAudioSampleBatch = loadFunction(retroHandle, "retro_set_audio_sample_batch")
- retroRun = loadFunction(retroHandle, "retro_run")
- retroLoadGame = loadFunction(retroHandle, "retro_load_game")
- retroUnloadGame = loadFunction(retroHandle, "retro_unload_game")
- retroSerializeSize = loadFunction(retroHandle, "retro_serialize_size")
- retroSerialize = loadFunction(retroHandle, "retro_serialize")
- retroUnserialize = loadFunction(retroHandle, "retro_unserialize")
- retroSetControllerPortDevice = loadFunction(retroHandle, "retro_set_controller_port_device")
- retroGetMemorySize = loadFunction(retroHandle, "retro_get_memory_size")
- retroGetMemoryData = loadFunction(retroHandle, "retro_get_memory_data")
-
- mu.Unlock()
-
- C.bridge_retro_set_environment(retroSetEnvironment, C.coreEnvironment_cgo)
- C.bridge_retro_set_video_refresh(retroSetVideoRefresh, C.coreVideoRefresh_cgo)
- C.bridge_retro_set_input_poll(retroSetInputPoll, C.coreInputPoll_cgo)
- C.bridge_retro_set_input_state(retroSetInputState, C.coreInputState_cgo)
- C.bridge_retro_set_audio_sample(retroSetAudioSample, C.coreAudioSample_cgo)
- C.bridge_retro_set_audio_sample_batch(retroSetAudioSampleBatch, C.coreAudioSampleBatch_cgo)
-
- C.bridge_retro_init(retroInit)
-
- v := C.bridge_retro_api_version(retroAPIVersion)
- log.Printf("Libretro API version: %v", v)
-}
-
-func slurp(path string, size int64) ([]byte, error) {
- f, err := os.Open(path)
- if err != nil {
- return nil, err
- }
- defer f.Close()
- bytes := make([]byte, size)
- buffer := bufio.NewReader(f)
- _, err = buffer.Read(bytes)
- if err != nil {
- return nil, err
- }
- return bytes, nil
-}
-
-func coreLoadGame(filename string) {
- file, err := os.Open(filename)
- if err != nil {
- panic(err)
- }
-
- fi, err := file.Stat()
- if err != nil {
- panic(err)
- }
- _ = file.Close()
-
- size := fi.Size()
- log.Printf("ROM size: %v", size)
-
- csFilename := C.CString(filename)
- defer C.free(unsafe.Pointer(csFilename))
- gi := C.struct_retro_game_info{
- path: csFilename,
- size: C.size_t(size),
- }
-
- si := C.struct_retro_system_info{}
- C.bridge_retro_get_system_info(retroGetSystemInfo, &si)
- log.Printf(" library_name: %v", C.GoString(si.library_name))
- log.Printf(" library_version: %v", C.GoString(si.library_version))
- log.Printf(" valid_extensions: %v", C.GoString(si.valid_extensions))
- log.Printf(" need_fullpath: %v", bool(si.need_fullpath))
- log.Printf(" block_extract: %v", bool(si.block_extract))
-
- if !si.need_fullpath {
- bytes, err := slurp(filename, size)
- if err != nil {
- panic(err)
- }
- cstr := C.CString(string(bytes))
- defer C.free(unsafe.Pointer(cstr))
- gi.data = unsafe.Pointer(cstr)
- }
-
- ok := C.bridge_retro_load_game(retroLoadGame, &gi)
- if !ok {
- log.Fatal("The core failed to load the content.")
- }
-
- avi := C.struct_retro_system_av_info{}
- C.bridge_retro_get_system_av_info(retroGetSystemAVInfo, &avi)
-
- // Append the library name to the window title.
- NAEmulator.meta.AudioSampleRate = int(avi.timing.sample_rate)
- NAEmulator.meta.Fps = float64(avi.timing.fps)
- NAEmulator.meta.BaseWidth = int(avi.geometry.base_width)
- NAEmulator.meta.BaseHeight = int(avi.geometry.base_height)
- // set aspect ratio
- /* Nominal aspect ratio of game. If aspect_ratio is <= 0.0,
- an aspect ratio of base_width / base_height is assumed.
- * A frontend could override this setting, if desired. */
- ratio := float64(avi.geometry.aspect_ratio)
- if ratio <= 0.0 {
- ratio = float64(avi.geometry.base_width) / float64(avi.geometry.base_height)
- }
- NAEmulator.meta.Ratio = ratio
-
- log.Printf("-----------------------------------")
- log.Printf("--- Core audio and video info ---")
- log.Printf("-----------------------------------")
- log.Printf(" Frame: %vx%v (%vx%v)",
- avi.geometry.base_width, avi.geometry.base_height,
- avi.geometry.max_width, avi.geometry.max_height)
- log.Printf(" AR: %v", ratio)
- log.Printf(" FPS: %v", avi.timing.fps)
- log.Printf(" Audio: %vHz", avi.timing.sample_rate)
- log.Printf("-----------------------------------")
-
- video.maxWidth = int32(avi.geometry.max_width)
- video.maxHeight = int32(avi.geometry.max_height)
- video.baseWidth = int32(avi.geometry.base_width)
- video.baseHeight = int32(avi.geometry.base_height)
- if video.isGl {
- if usesLibCo {
- C.bridge_execute(C.initVideo_cgo)
- } else {
- runtime.LockOSThread()
- initVideo()
- runtime.UnlockOSThread()
- }
- }
-
- // set default controller types on all ports
- maxPort := 4 // controllersNum
- for i := 0; i < maxPort; i++ {
- C.bridge_retro_set_controller_port_device(retroSetControllerPortDevice, C.uint(i), C.RETRO_DEVICE_JOYPAD)
- }
-}
-
-func toggleMultitap() {
- if multitap.supported && multitap.value != 0 {
- // 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
- if multitap.enabled {
- C.bridge_retro_set_controller_port_device(retroSetControllerPortDevice, 1, C.RETRO_DEVICE_JOYPAD)
- } else {
- C.bridge_retro_set_controller_port_device(retroSetControllerPortDevice, 1, multitap.value)
- }
- multitap.enabled = !multitap.enabled
- }
-}
-
-func nanoarchShutdown() {
- if usesLibCo {
- thread.MainMaybe(func() {
- C.bridge_execute(retroUnloadGame)
- C.bridge_execute(retroDeinit)
- if video.isGl {
- C.bridge_execute(C.deinitVideo_cgo)
- }
- })
- } else {
- if video.isGl {
- thread.MainMaybe(func() {
- // running inside a go routine, lock the thread to make sure the OpenGL context stays current
- runtime.LockOSThread()
- graphics.BindContext()
- })
- }
- C.bridge_retro_unload_game(retroUnloadGame)
- C.bridge_retro_deinit(retroDeinit)
- if video.isGl {
- thread.MainMaybe(func() {
- deinitVideo()
- runtime.UnlockOSThread()
- })
- }
- }
-
- setRotation(0)
- if err := closeLib(retroHandle); err != nil {
- log.Printf("error when close: %v", err)
- }
- for _, element := range coreConfig {
- C.free(unsafe.Pointer(element))
- }
-}
-
-func nanoarchRun() {
- if usesLibCo {
- C.bridge_execute(retroRun)
- } else {
- if video.isGl {
- // running inside a go routine, lock the thread to make sure the OpenGL context stays current
- runtime.LockOSThread()
- graphics.BindContext()
- }
- C.bridge_retro_run(retroRun)
- if video.isGl {
- runtime.UnlockOSThread()
- }
- }
-}
-
-func videoSetPixelFormat(format uint32) C.bool {
- switch format {
- case C.RETRO_PIXEL_FORMAT_0RGB1555:
- video.pixFmt = image.BitFormatShort5551
- graphics.SetPixelFormat(graphics.UnsignedShort5551)
- video.bpp = 2
- // format is not implemented
- pixelFormatConverterFn = nil
- case C.RETRO_PIXEL_FORMAT_XRGB8888:
- video.pixFmt = image.BitFormatInt8888Rev
- graphics.SetPixelFormat(graphics.UnsignedInt8888Rev)
- video.bpp = 4
- pixelFormatConverterFn = image.Rgba8888
- case C.RETRO_PIXEL_FORMAT_RGB565:
- video.pixFmt = image.BitFormatShort565
- graphics.SetPixelFormat(graphics.UnsignedShort565)
- video.bpp = 2
- pixelFormatConverterFn = image.Rgb565
- default:
- log.Fatalf("Unknown pixel type %v", format)
- }
- return true
-}
-
-func setRotation(rotation int) {
- if rotation == int(video.rotation) {
- return
- }
- video.rotation = image.Angle(rotation)
- rotationFn = image.GetRotation(video.rotation)
- NAEmulator.meta.Rotation = rotationFn
- log.Printf("[Env]: the game video is rotated %v°", map[int]int{0: 0, 1: 90, 2: 180, 3: 270}[rotation])
-}
diff --git a/pkg/emulator/libretro/nanoarch/nanoarch_test.go b/pkg/emulator/libretro/nanoarch/nanoarch_test.go
deleted file mode 100644
index 8f7f7383..00000000
--- a/pkg/emulator/libretro/nanoarch/nanoarch_test.go
+++ /dev/null
@@ -1,239 +0,0 @@
-package nanoarch
-
-import (
- "crypto/md5"
- "fmt"
- "image"
- "io/ioutil"
- "log"
- "os"
- "path"
- "path/filepath"
- "testing"
-
- "github.com/giongto35/cloud-game/v2/pkg/config"
- "github.com/giongto35/cloud-game/v2/pkg/config/worker"
- "github.com/giongto35/cloud-game/v2/pkg/emulator"
-)
-
-type testRun struct {
- room string
- system string
- rom string
- emulationTicks int
-}
-
-// EmulatorMock contains naEmulator mocking data.
-type EmulatorMock struct {
- naEmulator
-
- // Libretro compiled lib core name
- core string
- // draw canvas instance
- canvas *image.RGBA
- // shared core paths (can't be changed)
- paths EmulatorPaths
-
- // channels
- imageInCh <-chan GameFrame
- audioInCh <-chan []int16
- inputOutCh chan<- InputEvent
-}
-
-// EmulatorPaths defines various emulator file paths.
-type EmulatorPaths struct {
- assets string
- cores string
- games string
- save string
-}
-
-// GetEmulatorMock returns a properly stubbed emulator instance.
-// Due to extensive use of globals -- one mock instance is allowed per a test run.
-// Don't forget to init one image channel consumer, it will lock-out otherwise.
-// Make sure you call shutdownEmulator().
-func GetEmulatorMock(room string, system string) *EmulatorMock {
- rootPath := getRootPath()
- configPath := rootPath + "configs/"
-
- var conf worker.Config
- if err := config.LoadConfig(&conf, configPath); err != nil {
- panic(err)
- }
-
- meta := conf.Emulator.GetLibretroCoreConfig(system)
-
- images := make(chan GameFrame, 30)
- audio := make(chan []int16, 30)
- inputs := make(chan InputEvent, 100)
-
- store := Storage{
- Path: os.TempDir(),
- MainSave: room + ".dat",
- }
-
- // an emu
- emu := &EmulatorMock{
- naEmulator: naEmulator{
- imageChannel: images,
- audioChannel: audio,
- inputChannel: inputs,
- storage: store,
-
- meta: emulator.Metadata{
- LibPath: meta.Lib,
- ConfigPath: meta.Config,
- Ratio: meta.Ratio,
- IsGlAllowed: meta.IsGlAllowed,
- UsesLibCo: meta.UsesLibCo,
- HasMultitap: meta.HasMultitap,
- },
- players: NewPlayerSessionInput(),
- roomID: room,
- done: make(chan struct{}, 1),
- },
-
- canvas: image.NewRGBA(image.Rect(0, 0, meta.Width, meta.Height)),
- core: path.Base(meta.Lib),
-
- paths: EmulatorPaths{
- assets: cleanPath(rootPath),
- cores: cleanPath(rootPath + "assets/cores/"),
- games: cleanPath(rootPath + "assets/games/"),
- },
-
- imageInCh: images,
- audioInCh: audio,
- inputOutCh: inputs,
- }
-
- // stub globals
- NAEmulator = &emu.naEmulator
- outputImg = emu.canvas
-
- emu.paths.save = cleanPath(emu.GetHashPath())
-
- return emu
-}
-
-// GetDefaultEmulatorMock returns initialized emulator mock with default params.
-// Spawns audio/image channels consumers.
-// Don't forget to close emulator mock with shutdownEmulator().
-func GetDefaultEmulatorMock(room string, system string, rom string) *EmulatorMock {
- mock := GetEmulatorMock(room, system)
- mock.loadRom(rom)
- go mock.handleVideo(func(_ GameFrame) {})
- go mock.handleAudio(func(_ []int16) {})
-
- return mock
-}
-
-// loadRom loads a ROM into the emulator.
-// The rom will be loaded from emulators' games path.
-func (emu *EmulatorMock) loadRom(game string) {
- fmt.Printf("%v %v\n", emu.paths.cores, emu.core)
- coreLoad(emulator.Metadata{LibPath: emu.paths.cores + emu.core})
- coreLoadGame(emu.paths.games + game)
-}
-
-// shutdownEmulator closes the emulator and cleans its resources.
-func (emu *EmulatorMock) shutdownEmulator() {
- _ = os.Remove(emu.GetHashPath())
- _ = os.Remove(emu.GetSRAMPath())
-
- close(emu.imageChannel)
- close(emu.audioChannel)
- close(emu.inputOutCh)
-
- nanoarchShutdown()
-}
-
-// emulateOneFrame emulates one frame with exclusive lock.
-func (emu *EmulatorMock) emulateOneFrame() {
- emu.Lock()
- nanoarchRun()
- emu.Unlock()
-}
-
-// Who needs generics anyway?
-// handleVideo is a custom message handler for the video channel.
-func (emu *EmulatorMock) handleVideo(handler func(image GameFrame)) {
- for frame := range emu.imageInCh {
- handler(frame)
- }
-}
-
-// handleAudio is a custom message handler for the audio channel.
-func (emu *EmulatorMock) handleAudio(handler func(sample []int16)) {
- for frame := range emu.audioInCh {
- handler(frame)
- }
-}
-
-// handleInput is a custom message handler for the input channel.
-func (emu *EmulatorMock) handleInput(handler func(event InputEvent)) {
- for event := range emu.inputChannel {
- handler(event)
- }
-}
-
-// dumpState returns the current emulator state and
-// the latest saved state for its session.
-// Locks the emulator.
-func (emu *EmulatorMock) dumpState() (string, string) {
- emu.Lock()
- bytes, _ := ioutil.ReadFile(emu.paths.save)
- persistedStateHash := getHash(bytes)
- emu.Unlock()
-
- stateHash := emu.getStateHash()
- fmt.Printf("mem: %v, dat: %v\n", stateHash, persistedStateHash)
- return stateHash, persistedStateHash
-}
-
-// getStateHash returns the current emulator state hash.
-// Locks the emulator.
-func (emu *EmulatorMock) getStateHash() string {
- emu.Lock()
- state, _ := getSaveState()
- emu.Unlock()
-
- return getHash(state)
-}
-
-// getRootPath returns absolute path to the root directory.
-func getRootPath() string {
- p, _ := filepath.Abs("../../../../")
- return p + string(filepath.Separator)
-}
-
-// getHash returns MD5 hash.
-func getHash(bytes []byte) string {
- return fmt.Sprintf("%x", md5.Sum(bytes))
-}
-
-// cleanPath returns a proper file path for current OS.
-func cleanPath(path string) string {
- return filepath.FromSlash(path)
-}
-
-// benchmarkEmulator is a generic function for
-// measuring emulator performance for one emulation frame.
-func benchmarkEmulator(system string, rom string, b *testing.B) {
- log.SetOutput(ioutil.Discard)
- os.Stdout, _ = os.Open(os.DevNull)
-
- s := GetDefaultEmulatorMock("bench_"+system+"_performance", system, rom)
- for i := 0; i < b.N; i++ {
- s.emulateOneFrame()
- }
- s.shutdownEmulator()
-}
-
-func BenchmarkEmulatorGba(b *testing.B) {
- benchmarkEmulator("gba", "Sushi The Cat.gba", b)
-}
-
-func BenchmarkEmulatorNes(b *testing.B) {
- benchmarkEmulator("nes", "Super Mario Bros.nes", b)
-}
diff --git a/pkg/emulator/libretro/nanoarch/persistence.go b/pkg/emulator/libretro/nanoarch/persistence.go
deleted file mode 100644
index f05f0bee..00000000
--- a/pkg/emulator/libretro/nanoarch/persistence.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package nanoarch
-
-import "io/ioutil"
-
-// Save writes the current state to the filesystem.
-// Deadlock warning: locks the emulator.
-func (na *naEmulator) Save() (err error) {
- na.Lock()
- defer na.Unlock()
-
- if sramState := getSaveRAM(); sramState != nil {
- err = toFile(na.GetSRAMPath(), sramState)
- }
- if saveState, err := getSaveState(); err == nil {
- return toFile(na.GetHashPath(), saveState)
- }
- return
-}
-
-// Load restores the state from the filesystem.
-// Deadlock warning: locks the emulator.
-func (na *naEmulator) Load() (err error) {
- na.Lock()
- defer na.Unlock()
-
- if sramState, err := fromFile(na.GetSRAMPath()); err == nil {
- restoreSaveRAM(sramState)
- }
- if saveState, err := fromFile(na.GetHashPath()); err == nil {
- return restoreSaveState(saveState)
- }
- return
-}
-
-// toFile writes the state to a file with the path.
-func toFile(path string, data []byte) error {
- return ioutil.WriteFile(path, data, 0644)
-}
-
-// fromFile reads the state from a file with the path.
-func fromFile(path string) ([]byte, error) {
- if bytes, err := ioutil.ReadFile(path); err == nil {
- return bytes, nil
- } else {
- return []byte{}, err
- }
-}
diff --git a/pkg/emulator/libretro/nanoarch/persistence_test.go b/pkg/emulator/libretro/nanoarch/persistence_test.go
deleted file mode 100644
index b8be4de8..00000000
--- a/pkg/emulator/libretro/nanoarch/persistence_test.go
+++ /dev/null
@@ -1,233 +0,0 @@
-package nanoarch
-
-import (
- "fmt"
- "math/rand"
- "sync"
- "testing"
- "time"
-)
-
-// Tests a successful emulator state save.
-func TestSave(t *testing.T) {
- tests := []testRun{
- {
- room: "test_save_ok_00",
- system: "gba",
- rom: "Sushi The Cat.gba",
- emulationTicks: 100,
- },
- {
- room: "test_save_ok_01",
- system: "gba",
- rom: "anguna.gba",
- emulationTicks: 10,
- },
- }
-
- for _, test := range tests {
- t.Logf("Testing [%v] save with [%v]\n", test.system, test.rom)
-
- mock := GetDefaultEmulatorMock(test.room, test.system, test.rom)
-
- for test.emulationTicks > 0 {
- mock.emulateOneFrame()
- test.emulationTicks--
- }
-
- fmt.Printf("[%-14v] ", "before save")
- _, _ = mock.dumpState()
- if err := mock.Save(); err != nil {
- t.Errorf("Save fail %v", err)
- }
- fmt.Printf("[%-14v] ", "after save")
- snapshot1, snapshot2 := mock.dumpState()
-
- if snapshot1 != snapshot2 {
- t.Errorf("It seems rom state save has failed: %v != %v", snapshot1, snapshot2)
- }
-
- mock.shutdownEmulator()
- }
-}
-
-// Tests save and restore function:
-//
-// Emulate n ticks.
-// Call save (a).
-// Emulate n ticks again.
-// Call load from the save (b).
-// Compare states (a) and (b), should be =.
-//
-func TestLoad(t *testing.T) {
- tests := []testRun{
- {
- room: "test_load_00",
- system: "nes",
- rom: "Super Mario Bros.nes",
- emulationTicks: 100,
- },
- {
- room: "test_load_01",
- system: "gba",
- rom: "Sushi The Cat.gba",
- emulationTicks: 1000,
- },
- {
- room: "test_load_02",
- system: "gba",
- rom: "anguna.gba",
- emulationTicks: 100,
- },
- }
-
- for _, test := range tests {
- t.Logf("Testing [%v] load with [%v]\n", test.system, test.rom)
-
- mock := GetDefaultEmulatorMock(test.room, test.system, test.rom)
-
- fmt.Printf("[%-14v] ", "initial")
- mock.dumpState()
-
- for ticks := test.emulationTicks; ticks > 0; ticks-- {
- mock.emulateOneFrame()
- }
- fmt.Printf("[%-14v] ", fmt.Sprintf("emulated %d", test.emulationTicks))
- mock.dumpState()
-
- if err := mock.Save(); err != nil {
- t.Errorf("Save fail %v", err)
- }
- fmt.Printf("[%-14v] ", "saved")
- snapshot1, _ := mock.dumpState()
-
- for ticks := test.emulationTicks; ticks > 0; ticks-- {
- mock.emulateOneFrame()
- }
- fmt.Printf("[%-14v] ", fmt.Sprintf("emulated %d", test.emulationTicks))
- mock.dumpState()
-
- if err := mock.Load(); err != nil {
- t.Errorf("Load fail %v", err)
- }
- fmt.Printf("[%-14v] ", "restored")
- snapshot2, _ := mock.dumpState()
-
- if snapshot1 != snapshot2 {
- t.Errorf("It seems rom state restore has failed: %v != %v", snapshot1, snapshot2)
- }
-
- mock.shutdownEmulator()
- }
-}
-
-func TestStateConcurrency(t *testing.T) {
- tests := []struct {
- run testRun
- // determine random
- seed int
- }{
- {
- run: testRun{
- room: "test_concurrency_00",
- system: "gba",
- rom: "Sushi The Cat.gba",
- emulationTicks: 120,
- },
- seed: 42,
- },
- {
- run: testRun{
- room: "test_concurrency_01",
- system: "gba",
- rom: "anguna.gba",
- emulationTicks: 300,
- },
- seed: 42 + 42,
- },
- }
-
- for _, test := range tests {
- t.Logf("Testing [%v] concurrency with [%v]\n", test.run.system, test.run.rom)
-
- mock := GetEmulatorMock(test.run.room, test.run.system)
- ops := &sync.WaitGroup{}
- // quantum lock
- qLock := &sync.Mutex{}
- op := 0
-
- mock.loadRom(test.run.rom)
- go mock.handleVideo(func(frame GameFrame) {
- if len(frame.Image.Pix) == 0 {
- t.Errorf("It seems that rom video frame was empty, which is strange!")
- }
- })
- go mock.handleAudio(func(_ []int16) {})
- go mock.handleInput(func(_ InputEvent) {})
-
- rand.Seed(int64(test.seed))
- t.Logf("Random seed is [%v]\n", test.seed)
- t.Logf("Save path is [%v]\n", mock.paths.save)
-
- _ = mock.Save()
-
- // emulation fps ROM cap
- ticker := time.NewTicker(time.Second / time.Duration(mock.meta.Fps))
- t.Logf("FPS limit is [%v]\n", mock.meta.Fps)
-
- for range ticker.C {
- select {
- case <-mock.done:
- mock.shutdownEmulator()
- return
- default:
- }
-
- op++
- if op > test.run.emulationTicks {
- mock.Close()
- } else {
- qLock.Lock()
- mock.emulateOneFrame()
- qLock.Unlock()
-
- if lucky() && !lucky() {
- ops.Add(1)
- go func() {
- qLock.Lock()
- defer qLock.Unlock()
-
- mock.dumpState()
- // remove save to reproduce the bug
- _ = mock.Save()
- _, snapshot1 := mock.dumpState()
- _ = mock.Load()
- snapshot2, _ := mock.dumpState()
-
- // Bug or feature?
- // When you load a state from the file
- // without immediate preceding save,
- // it won't be in the loaded state
- // even without calling retro_run.
- // But if you pause the threads with a debugger
- // and run the code step by step, then it will work as expected.
- // Possible background emulation?
-
- if snapshot1 != snapshot2 {
- t.Errorf("States are inconsistent %v != %v on tick %v\n", snapshot1, snapshot2, op)
- }
- ops.Done()
- }()
- }
- }
- }
-
- ops.Wait()
- ticker.Stop()
- }
-}
-
-// lucky returns random boolean.
-func lucky() bool {
- return rand.Intn(2) == 1
-}
diff --git a/pkg/emulator/libretro/nanoarch/state.go b/pkg/emulator/libretro/nanoarch/state.go
deleted file mode 100644
index cb655be3..00000000
--- a/pkg/emulator/libretro/nanoarch/state.go
+++ /dev/null
@@ -1,100 +0,0 @@
-package nanoarch
-
-/*
-#include "libretro.h"
-#include
-
-size_t bridge_retro_get_memory_size(void *f, unsigned id);
-void* bridge_retro_get_memory_data(void *f, unsigned id);
-bool bridge_retro_serialize(void *f, void *data, size_t size);
-bool bridge_retro_unserialize(void *f, void *data, size_t size);
-size_t bridge_retro_serialize_size(void *f);
-*/
-import "C"
-import (
- "errors"
- "unsafe"
-)
-
-// !global emulator lib state
-var (
- retroGetMemoryData unsafe.Pointer
- retroGetMemorySize unsafe.Pointer
- retroSerialize unsafe.Pointer
- retroSerializeSize unsafe.Pointer
- retroUnserialize unsafe.Pointer
-)
-
-// defines any memory state of the emulator
-type state []byte
-
-type mem struct {
- ptr unsafe.Pointer
- size uint
-}
-
-// saveStateSize returns the amount of data the implementation requires
-// to serialize internal state (save states).
-func saveStateSize() uint { return uint(C.bridge_retro_serialize_size(retroSerializeSize)) }
-
-// getSaveState returns emulator internal state.
-func getSaveState() (state, error) {
- size := saveStateSize()
- data := C.malloc(C.size_t(size))
- defer C.free(data)
- if !bool(C.bridge_retro_serialize(retroSerialize, data, C.size_t(size))) {
- return nil, errors.New("retro_serialize failed")
- }
- return C.GoBytes(data, C.int(size)), nil
-}
-
-// restoreSaveState restores emulator internal state.
-func restoreSaveState(st state) error {
- if len(st) == 0 {
- return nil
- }
- size := saveStateSize()
- if !bool(C.bridge_retro_unserialize(retroUnserialize, unsafe.Pointer(&st[0]), C.size_t(size))) {
- return errors.New("retro_unserialize failed")
- }
- return nil
-}
-
-// getSaveRAM returns the game save RAM (cartridge) data or a nil slice.
-func getSaveRAM() state {
- mem := ptSaveRAM()
- if mem == nil {
- return nil
- }
- return C.GoBytes(mem.ptr, C.int(mem.size))
-}
-
-// restoreSaveRAM restores game save RAM.
-func restoreSaveRAM(st state) {
- if len(st) == 0 {
- return
- }
- if mem := ptSaveRAM(); mem != nil {
- sram := (*[1 << 30]byte)(mem.ptr)[:mem.size:mem.size]
- copy(sram, st)
- }
-}
-
-// getMemorySize returns memory region size.
-func getMemorySize(id uint) uint {
- return uint(C.bridge_retro_get_memory_size(retroGetMemorySize, C.uint(id)))
-}
-
-// getMemoryData returns a pointer to memory data.
-func getMemoryData(id uint) unsafe.Pointer {
- return C.bridge_retro_get_memory_data(retroGetMemoryData, C.uint(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)
- if ptr == nil || size == 0 {
- return nil
- }
- return &mem{ptr: ptr, size: size}
-}
diff --git a/pkg/emulator/libretro/repo/buildbot/repository.go b/pkg/emulator/libretro/repo/buildbot/repository.go
deleted file mode 100644
index 6ced435c..00000000
--- a/pkg/emulator/libretro/repo/buildbot/repository.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package buildbot
-
-import (
- "strings"
-
- "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/core"
- "github.com/giongto35/cloud-game/v2/pkg/emulator/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 core.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.LibExt)
- if r.Compression != "" {
- sb.WriteString("." + r.Compression)
- }
- return sb.String()
-}
diff --git a/pkg/emulator/libretro/repo/buildbot/repository_test.go b/pkg/emulator/libretro/repo/buildbot/repository_test.go
deleted file mode 100644
index 37cb302c..00000000
--- a/pkg/emulator/libretro/repo/buildbot/repository_test.go
+++ /dev/null
@@ -1,56 +0,0 @@
-package buildbot
-
-import (
- "testing"
-
- "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/core"
-)
-
-func TestBuildbotRepo(t *testing.T) {
- testAddress := "http://test.me"
- tests := []struct {
- file string
- compression string
- arch core.ArchInfo
- resultUrl string
- }{
- {
- file: "uber_core",
- arch: core.ArchInfo{
- Os: "linux",
- Arch: "x86_64",
- LibExt: ".so",
- },
- resultUrl: testAddress + "/" + "linux/x86_64/latest/uber_core.so",
- },
- {
- file: "uber_core",
- compression: "zip",
- arch: core.ArchInfo{
- Os: "linux",
- Arch: "x86_64",
- LibExt: ".so",
- },
- resultUrl: testAddress + "/" + "linux/x86_64/latest/uber_core.so.zip",
- },
- {
- file: "uber_core",
- arch: core.ArchInfo{
- Os: "osx",
- Arch: "x86_64",
- Vendor: "apple",
- LibExt: ".dylib",
- },
- resultUrl: testAddress + "/" + "apple/osx/x86_64/latest/uber_core.dylib",
- },
- }
-
- for _, test := range tests {
- repo := NewBuildbotRepo(testAddress, test.compression)
- url := repo.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)
- }
- }
-}
diff --git a/pkg/emulator/libretro/repo/github/repository.go b/pkg/emulator/libretro/repo/github/repository.go
deleted file mode 100644
index 01687126..00000000
--- a/pkg/emulator/libretro/repo/github/repository.go
+++ /dev/null
@@ -1,18 +0,0 @@
-package github
-
-import (
- "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/core"
- "github.com/giongto35/cloud-game/v2/pkg/emulator/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 core.ArchInfo) string {
- return r.RepoBuildbot.GetCoreUrl(file, info) + "?raw=true"
-}
diff --git a/pkg/emulator/libretro/repo/github/repository_test.go b/pkg/emulator/libretro/repo/github/repository_test.go
deleted file mode 100644
index 01dea346..00000000
--- a/pkg/emulator/libretro/repo/github/repository_test.go
+++ /dev/null
@@ -1,56 +0,0 @@
-package github
-
-import (
- "testing"
-
- "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/core"
-)
-
-func TestBuildbotRepo(t *testing.T) {
- testAddress := "http://test.me"
- tests := []struct {
- file string
- compression string
- arch core.ArchInfo
- resultUrl string
- }{
- {
- file: "uber_core",
- arch: core.ArchInfo{
- Os: "linux",
- Arch: "x86_64",
- LibExt: ".so",
- },
- resultUrl: testAddress + "/" + "linux/x86_64/latest/uber_core.so?raw=true",
- },
- {
- file: "uber_core",
- compression: "zip",
- arch: core.ArchInfo{
- Os: "linux",
- Arch: "x86_64",
- LibExt: ".so",
- },
- resultUrl: testAddress + "/" + "linux/x86_64/latest/uber_core.so.zip?raw=true",
- },
- {
- file: "uber_core",
- arch: core.ArchInfo{
- 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 {
- repo := NewGithubRepo(testAddress, test.compression)
- url := repo.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)
- }
- }
-}
diff --git a/pkg/emulator/libretro/repo/raw/repository.go b/pkg/emulator/libretro/repo/raw/repository.go
deleted file mode 100644
index a00c3b30..00000000
--- a/pkg/emulator/libretro/repo/raw/repository.go
+++ /dev/null
@@ -1,18 +0,0 @@
-package raw
-
-import "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/core"
-
-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, _ core.ArchInfo) string {
- return r.Address
-}
diff --git a/pkg/emulator/libretro/repo/repository.go b/pkg/emulator/libretro/repo/repository.go
deleted file mode 100644
index f3eaa44c..00000000
--- a/pkg/emulator/libretro/repo/repository.go
+++ /dev/null
@@ -1,36 +0,0 @@
-package repo
-
-import (
- "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/core"
- "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/repo/buildbot"
- "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/repo/github"
- "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/repo/raw"
-)
-
-type (
- Data struct {
- Url string
- Compression string
- }
-
- Repository interface {
- GetCoreUrl(file string, info core.ArchInfo) (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
-}
diff --git a/pkg/encoder/color/bgra/bgra.go b/pkg/encoder/color/bgra/bgra.go
new file mode 100644
index 00000000..39a50c22
--- /dev/null
+++ b/pkg/encoder/color/bgra/bgra.go
@@ -0,0 +1,56 @@
+package bgra
+
+import (
+ "image"
+ "image/color"
+)
+
+type BGRA struct {
+ image.RGBA
+}
+
+var BGRAModel = color.ModelFunc(func(c color.Color) color.Color {
+ if _, ok := c.(BGRAColor); ok {
+ return c
+ }
+ r, g, b, a := c.RGBA()
+ return BGRAColor{uint8(r >> 8), uint8(g >> 8), uint8(b >> 8), uint8(a >> 8)}
+})
+
+// BGRAColor represents a BGRA color.
+type BGRAColor struct {
+ R, G, B, A uint8
+}
+
+func (c BGRAColor) RGBA() (r, g, b, a uint32) {
+ r = uint32(c.B)
+ r |= r << 8
+ g = uint32(c.G)
+ g |= g << 8
+ b = uint32(c.R)
+ b |= b << 8
+ a = uint32(255) //uint32(c.A)
+ a |= a << 8
+ return
+}
+
+func NewBGRA(r image.Rectangle) *BGRA {
+ return &BGRA{*image.NewRGBA(r)}
+}
+
+func (p *BGRA) ColorModel() color.Model { return BGRAModel }
+func (p *BGRA) At(x, y int) color.Color {
+ i := p.PixOffset(x, y)
+ s := p.Pix[i : i+4 : i+4]
+ return BGRAColor{s[0], s[1], s[2], s[3]}
+}
+
+func (p *BGRA) Set(x, y int, c color.Color) {
+ i := p.PixOffset(x, y)
+ c1 := BGRAModel.Convert(c).(BGRAColor)
+ s := p.Pix[i : i+4 : i+4]
+ s[0] = c1.R
+ s[1] = c1.G
+ s[2] = c1.B
+ s[3] = 255
+}
diff --git a/pkg/encoder/color/rgb565/rgb565.go b/pkg/encoder/color/rgb565/rgb565.go
new file mode 100644
index 00000000..11c66c8b
--- /dev/null
+++ b/pkg/encoder/color/rgb565/rgb565.go
@@ -0,0 +1,62 @@
+package rgb565
+
+import (
+ "encoding/binary"
+ "image"
+ "image/color"
+ "math"
+)
+
+// RGB565 is an in-memory image whose At method returns RGB565 values.
+type RGB565 struct {
+ // Pix holds the image's pixels, as RGB565 values in big-endian format. The pixel at
+ // (x, y) starts at Pix[(y-p.Rect.Min.Y)*p.Stride + (x-p.Rect.Min.X)*2].
+ Pix []uint8
+ // Stride is the Pix stride (in bytes) between vertically adjacent pixels.
+ Stride int
+ // Rect is the image's bounds.
+ Rect image.Rectangle
+}
+
+// Model is the model for RGB565 colors.
+var Model = color.ModelFunc(func(c color.Color) color.Color {
+ //if _, ok := c.(Color); ok {
+ // return c
+ //}
+ r, g, b, _ := c.RGBA()
+ return Color(uint16((r<<8)&rMask | (g<<3)&gMask | (b>>3)&bMask))
+})
+
+const (
+ rMask = 0b1111100000000000
+ gMask = 0b0000011111100000
+ bMask = 0b0000000000011111
+)
+
+// Color represents an RGB565 color.
+type Color uint16
+
+func (c Color) RGBA() (r, g, b, a uint32) {
+ return uint32(math.Round(float64(c&rMask>>11)*255.0/31.0)) << 8,
+ uint32(math.Round(float64(c&gMask>>5)*255.0/63.0)) << 8,
+ uint32(math.Round(float64(c&bMask)*255.0/31.0)) << 8,
+ 0xffff
+}
+
+func NewRGB565(r image.Rectangle) *RGB565 {
+ return &RGB565{Pix: make([]uint8, r.Dx()*r.Dy()<<1), Stride: r.Dx() << 1, Rect: r}
+}
+
+func (p *RGB565) Bounds() image.Rectangle { return p.Rect }
+func (p *RGB565) ColorModel() color.Model { return Model }
+func (p *RGB565) PixOffset(x, y int) int { return (x-p.Rect.Min.X)<<1 + (y-p.Rect.Min.Y)*p.Stride }
+
+func (p *RGB565) At(x, y int) color.Color {
+ i := p.PixOffset(x, y)
+ return Color(binary.LittleEndian.Uint16(p.Pix[i : i+2]))
+}
+
+func (p *RGB565) Set(x, y int, c color.Color) {
+ i := p.PixOffset(x, y)
+ binary.LittleEndian.PutUint16(p.Pix[i:i+2], uint16(Model.Convert(c).(Color)))
+}
diff --git a/pkg/encoder/color/rgba/rgba.go b/pkg/encoder/color/rgba/rgba.go
new file mode 100644
index 00000000..5bb2e9bc
--- /dev/null
+++ b/pkg/encoder/color/rgba/rgba.go
@@ -0,0 +1,24 @@
+package rgba
+
+import (
+ "image"
+ "image/color"
+)
+
+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 := range sh {
+ yy := y
+ if flipped {
+ yy = sh - y
+ }
+ for x := range sw {
+ px := img.At(x, y)
+ rgba := color.RGBAModel.Convert(px).(color.RGBA)
+ dst.Set(x, yy, rgba)
+ }
+ }
+ return dst
+}
diff --git a/pkg/encoder/encoder.go b/pkg/encoder/encoder.go
new file mode 100644
index 00000000..0372c2c5
--- /dev/null
+++ b/pkg/encoder/encoder.go
@@ -0,0 +1,146 @@
+package encoder
+
+import (
+ "fmt"
+ "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"
+)
+
+type (
+ InFrame yuv.RawFrame
+ OutFrame []byte
+ Encoder interface {
+ Encode([]byte) []byte
+ IntraRefresh()
+ Info() string
+ SetFlip(bool)
+ Shutdown() error
+ }
+)
+
+type Video struct {
+ codec Encoder
+ log *logger.Logger
+ stopped atomic.Bool
+ y yuv.Conv
+ pf yuv.PixFmt
+ rot uint
+}
+
+type VideoCodec string
+
+const (
+ H264 VideoCodec = "h264"
+ VP8 VideoCodec = "vp8"
+ VP9 VideoCodec = "vp9"
+ VPX VideoCodec = "vpx"
+)
+
+// NewVideoEncoder returns new video encoder.
+// By default, it waits for RGBA images on the input channel,
+// converts them into YUV I420 format,
+// encodes with provided video encoder, and
+// puts the result into the output channel.
+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 {
+ if v.stopped.Load() {
+ return nil
+ }
+
+ yCbCr := v.y.Process(yuv.RawFrame(frame), v.rot, v.pf)
+ //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("%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:
+ v.pf = yuv.PixFmt(yuv.FourccRgbp)
+ default:
+ v.pf = yuv.PixFmt(yuv.FourccAbgr)
+ }
+}
+
+// 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) {
+ if v == nil {
+ return
+ }
+ v.codec.SetFlip(b)
+}
+
+func (v *Video) Stop() {
+ 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 {
+ if v.log != nil {
+ v.log.Error().Err(err).Msg("failed to close the encoder")
+ }
+ }
+}
diff --git a/pkg/encoder/h264/libx264.go b/pkg/encoder/h264/libx264.go
deleted file mode 100644
index ca019be4..00000000
--- a/pkg/encoder/h264/libx264.go
+++ /dev/null
@@ -1,872 +0,0 @@
-// Implements cgo bindings for [x264](https://www.videolan.org/developers/x264.html) library.
-package h264
-
-/*
-#cgo pkg-config: x264
-#cgo CFLAGS: -Wall -O3
-
-#include "stdint.h"
-#include "x264.h"
-#include
-*/
-import "C"
-import "unsafe"
-
-const Build = C.X264_BUILD
-
-/* T is opaque handler for encoder */
-type T struct{}
-
-/****************************************************************************
- * NAL structure and functions
- ****************************************************************************/
-
-/* enum nal_unit_type_e */
-const (
- NalUnknown = 0
- NalSlice = 1
- NalSliceDpa = 2
- NalSliceDpb = 3
- NalSliceDpc = 4
- NalSliceIdr = 5 /* ref_idc != 0 */
- NalSei = 6 /* ref_idc == 0 */
- NalSps = 7
- NalPps = 8
- NalAud = 9
- NalFiller = 12
- /* ref_idc == 0 for 6,9,10,11,12 */
-)
-
-/* enum nal_priority_e */
-const (
- NalPriorityDisposable = 0
- NalPriorityLow = 1
- NalPriorityHigh = 2
- NalPriorityHighest = 3
-)
-
-/* 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 an 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
-}
-
-/****************************************************************************
- * Encoder parameters
- ****************************************************************************/
-/* CPU flags */
-
-const (
- /* x86 */
- CpuMmx uint32 = 1 << 0
- CpuMmx2 uint32 = 1 << 1 /* MMX2 aka MMXEXT aka ISSE */
- CpuMmxext = CpuMmx2
- CpuSse uint32 = 1 << 2
- CpuSse2 uint32 = 1 << 3
- CpuLzcnt uint32 = 1 << 4
- CpuSse3 uint32 = 1 << 5
- CpuSsse3 uint32 = 1 << 6
- CpuSse4 uint32 = 1 << 7 /* SSE4.1 */
- CpuSse42 uint32 = 1 << 8 /* SSE4.2 */
- CpuAvx uint32 = 1 << 9 /* Requires OS support even if YMM registers aren't used */
- CpuXop uint32 = 1 << 10 /* AMD XOP */
- CpuFma4 uint32 = 1 << 11 /* AMD FMA4 */
- CpuFma3 uint32 = 1 << 12
- CpuBmi1 uint32 = 1 << 13
- CpuBmi2 uint32 = 1 << 14
- CpuAvx2 uint32 = 1 << 15
- CpuAvx512 uint32 = 1 << 16 /* AVX-512 {F, CD, BW, DQ, VL}, requires OS support */
- /* x86 modifiers */
- CpuCacheline32 uint32 = 1 << 17 /* avoid memory loads that span the border between two cachelines */
- CpuCacheline64 uint32 = 1 << 18 /* 32/64 is the size of a cacheline in bytes */
- CpuSse2IsSlow uint32 = 1 << 19 /* avoid most SSE2 functions on Athlon64 */
- CpuSse2IsFast uint32 = 1 << 20 /* a few functions are only faster on Core2 and Phenom */
- CpuSlowShuffle uint32 = 1 << 21 /* The Conroe has a slow shuffle unit (relative to overall SSE performance) */
- CpuStackMod4 uint32 = 1 << 22 /* if stack is only mod4 and not mod16 */
- CpuSlowAtom uint32 = 1 << 23 /* The Atom is terrible: slow SSE unaligned loads, slow
- * SIMD multiplies, slow SIMD variable shifts, slow pshufb,
- * cacheline split penalties -- gather everything here that
- * isn't shared by other CPUs to avoid making half a dozen
- * new SLOW flags. */
- CpuSlowPshufb uint32 = 1 << 24 /* such as on the Intel Atom */
- CpuSlowPalignr uint32 = 1 << 25 /* such as on the AMD Bobcat */
-
- /* PowerPC */
- CpuAltivec uint32 = 0x0000001
-
- /* ARM and AArch64 */
- CpuArmv6 uint32 = 0x0000001
- CpuNeon uint32 = 0x0000002 /* ARM NEON */
- CpuFastNeonMrc uint32 = 0x0000004 /* Transfer from NEON to ARM register is fast (Cortex-A9) */
- CpuArmv8 uint32 = 0x0000008
-
- /* MIPS */
- CpuMsa uint32 = 0x0000001 /* MIPS MSA */
-
- /* Analyse flags */
- AnalyseI4x4 uint32 = 0x0001 /* Analyse i4x4 */
- AnalyseI8x8 uint32 = 0x0002 /* Analyse i8x8 (requires 8x8 transform) */
- AnalysePsub16x16 uint32 = 0x0010 /* Analyse p16x8, p8x16 and p8x8 */
- AnalysePsub8x8 uint32 = 0x0020 /* Analyse p8x4, p4x8, p4x4 */
- AnalyseBsub16x16 uint32 = 0x0100 /* Analyse b16x8, b8x16 and b8x8 */
-
- DirectPredNone = 0
- DirectPredSpatial = 1
- DirectPredTemporal = 2
- DirectPredAuto = 3
- MeDia = 0
- MeHex = 1
- MeUmh = 2
- MeEsa = 3
- MeTesa = 4
- CqmFlat = 0
- CqmJvt = 1
- CqmCustom = 2
- RcCqp = 0
- RcCrf = 1
- RcAbr = 2
- QpAuto = 0
- AqNone = 0
- AqVariance = 1
- AqAutovariance = 2
- AqAutovarianceBiased = 3
- BAdaptNone = 0
- BAdaptFast = 1
- BAdaptTrellis = 2
- WeightpNone = 0
- WeightpSimple = 1
- WeightpSmart = 2
- BPyramidNone = 0
- BPyramidStrict = 1
- BPyramidNormal = 2
- KeyintMinAuto = 0
- KeyintMaxInfinite = 1 << 30
-
- /* AVC-Intra flavors */
- AvcintraFlavorPanasonic = 0
- AvcintraFlavorSony = 1
-
- /* !to add missing names */
- /* static const char * const x264_direct_pred_names[] = { "none", "spatial", "temporal", "auto", 0 }; */
- /* static const char * const x264_motion_est_names[] = { "dia", "hex", "umh", "esa", "tesa", 0 }; */
- /* static const char * const x264_b_pyramid_names[] = { "none", "strict", "normal", 0 }; */
- /* static const char * const x264_overscan_names[] = { "undef", "show", "crop", 0 }; */
- /* static const char * const x264_vidformat_names[] = { "component", "pal", "ntsc", "secam", "mac", "undef", 0 }; */
- /* static const char * const x264_fullrange_names[] = { "off", "on", 0 }; */
- /* static const char * const x264_colorprim_names[] = { "", "bt709", "undef", "", "bt470m", "bt470bg", "smpte170m", "smpte240m", "film", "bt2020", "smpte428", "smpte431", "smpte432", 0 }; */
- /* static const char * const x264_transfer_names[] = { "", "bt709", "undef", "", "bt470m", "bt470bg", "smpte170m", "smpte240m", "linear", "log100", "log316", "iec61966-2-4", "bt1361e", "iec61966-2-1", "bt2020-10", "bt2020-12", "smpte2084", "smpte428", "arib-std-b67", 0 }; */
- /* static const char * const x264_colmatrix_names[] = { "GBR", "bt709", "undef", "", "fcc", "bt470bg", "smpte170m", "smpte240m", "YCgCo", "bt2020nc", "bt2020c", "smpte2085", "chroma-derived-nc", "chroma-derived-c", "ICtCp", 0 }; */
- /* static const char * const x264_nal_hrd_names[] = { "none", "vbr", "cbr", 0 }; */
- /* static const char * const x264_avcintra_flavor_names[] = { "panasonic", "sony", 0 }; */
-
- /* Colorspace type */
- CspMask = 0x00ff /* */
- CspNone = 0x0000 /* Invalid mode */
- CspI400 = 0x0001 /* monochrome 4:0:0 */
- CspI420 = 0x0002 /* yuv 4:2:0 planar */
- 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 */
- CspVflip = 0x1000 /* the csp is vertically flipped */
- CspHighDepth = 0x2000 /* the csp has a depth of 16 bits per pixel component */
-
- /* Slice type */
- TypeAuto = 0x0000 /* Let x264 choose the right type */
- TypeIdr = 0x0001
- TypeI = 0x0002
- TypeP = 0x0003
- TypeBref = 0x0004 /* Non-disposable B-frame */
- TypeB = 0x0005
- TypeKeyframe = 0x0006 /* IDR or I depending on b_open_gop option */
- /* !to reimplement macro */
- /* #define IS_X264_TYPE_I(x) ((x)==X264_TYPE_I || (x)==X264_TYPE_IDR || (x)==X264_TYPE_KEYFRAME) */
- /* #define IS_X264_TYPE_B(x) ((x)==X264_TYPE_B || (x)==X264_TYPE_BREF) */
-
- /* Log level */
- LogNone = -1
- LogError = 0
- LogWarning = 1
- LogInfo = 2
- LogDebug = 3
-
- /* Threading */
- ThreadsAuto = 0 /* Automatically select optimal number of threads */
- SyncLookaheadAuto = -1 /* Automatically select optimal lookahead thread buffer size */
-
- /* HRD */
- NalHrdNone = 0
- NalHrdVbr = 1
- NalHrdCbr = 2
-)
-
-const (
- /* The macroblock is constant and remains unchanged from the previous frame. */
- MbinfoConstant = 1 << 0
- /* More flags may be added in the future. */
-)
-
-/* Zones: override ratecontrol or other options for specific sections of the video.
- * See x264_encoder_reconfig() for which options can be changed.
- * If zones overlap, whichever comes later in the list takes precedence. */
-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
-
-const (
- PicStructAuto = iota // automatically decide (default)
- PicStructProgressive = 1 // progressive frame
- // "TOP" and "BOTTOM" are not supported in x264 (PAFF only)
- PicStructTopBottom = 4 // top field followed by bottom
- PicStructBottomTop = 5 // bottom field followed by top
- PicStructTopBottomTop = 6 // top field, bottom field, top field repeated
- PicStructBottomTopBottom = 7 // bottom field, top field, bottom field repeated
- PicStructDouble = 8 // double frame
- PicStructTriple = 9 // triple frame
-)
-
-type Hrd struct {
- CpbInitialArrivalTime float64
- CpbFinalArrivalTime float64
- CpbRemovalTime float64
-
- DpbOutputTime float64
-}
-
-/* Arbitrary user SEI:
- * Payload size is in bytes and the payload pointer must be valid.
- * Payload types and syntax can be found in Annex D of the H.264 Specification.
- * SEI payload alignment bits as described in Annex D must be included at the
- * end of the payload if needed.
- * The payload should not be NAL-encapsulated.
- * Payloads are written first in order of input, apart from in the case when HRD
- * is enabled where payloads are written after the Buffering Period SEI. */
-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]unsafe.Pointer /* Pointers to each plane */
-}
-
-/* All arrays of data here are ordered as follows:
- * each array contains one offset per macroblock, in raster scan order. In interlaced
- * mode, top-field MBs and bottom-field MBs are interleaved at the row level.
- * Macroblocks are 16x16 blocks of pixels (with respect to the luma plane). For the
- * purposes of calculating the number of macroblocks, width and height are rounded up to
- * the nearest 16. If in interlaced mode, height is rounded up to the nearest 32 instead. */
-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 (p *Picture) freePlane(n int) {
- C.free(p.Img.Plane[n])
-}
-
-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)) }
-
-// NalEncode - encode Nal.
-func NalEncode(h *T, dst []byte, nal *Nal) {
- ch := h.cptr()
- cdst := (*C.uint8_t)(unsafe.Pointer(&dst[0]))
- cnal := nal.cptr()
- C.x264_nal_encode(ch, cdst, cnal)
-}
-
-// ParamDefault - fill Param with default values and do CPU detection.
-func ParamDefault(param *Param) {
- C.x264_param_default(param.cptr())
-}
-
-// ParamParse - set one parameter by name. Returns 0 on success.
-func ParamParse(param *Param, name string, value string) int32 {
- cparam := param.cptr()
-
- cname := C.CString(name)
- defer C.free(unsafe.Pointer(cname))
-
- cvalue := C.CString(value)
- defer C.free(unsafe.Pointer(cvalue))
-
- ret := C.x264_param_parse(cparam, cname, cvalue)
- v := (int32)(ret)
- return v
-}
-
-// 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 {
- cparam := param.cptr()
-
- cpreset := C.CString(preset)
- defer C.free(unsafe.Pointer(cpreset))
-
- ctune := C.CString(tune)
- defer C.free(unsafe.Pointer(ctune))
-
- ret := C.x264_param_default_preset(cparam, cpreset, ctune)
- v := (int32)(ret)
- return v
-}
-
-// ParamApplyFastfirstpass - if first-pass mode is set (rc.b_stat_read == 0, rc.b_stat_write == 1),
-// modify the encoder settings to disable options generally not useful on the first pass.
-func ParamApplyFastfirstpass(param *Param) {
- cparam := param.cptr()
- C.x264_param_apply_fastfirstpass(cparam)
-}
-
-// 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 {
- cparam := param.cptr()
-
- cprofile := C.CString(profile)
- defer C.free(unsafe.Pointer(cprofile))
-
- ret := C.x264_param_apply_profile(cparam, cprofile)
- v := (int32)(ret)
- return v
-}
-
-// PictureInit - initialize an Picture. Needs to be done if the calling application
-// allocates its own Picture as opposed to using PictureAlloc.
-func PictureInit(pic *Picture) {
- cpic := pic.cptr()
- C.x264_picture_init(cpic)
-}
-
-// PictureAlloc - alloc data for a Picture. You must call PictureClean on it.
-// Returns 0 on success, or -1 on malloc failure or invalid colorspace.
-func PictureAlloc(pic *Picture, iCsp int32, iWidth int32, iHeight int32) int32 {
- cpic := pic.cptr()
-
- ciCsp := (C.int)(iCsp)
- ciWidth := (C.int)(iWidth)
- ciHeight := (C.int)(iHeight)
-
- ret := C.x264_picture_alloc(cpic, ciCsp, ciWidth, ciHeight)
- v := (int32)(ret)
- return v
-}
-
-// PictureClean - free associated resource for a Picture allocated with PictureAlloc ONLY.
-func PictureClean(pic *Picture) {
- cpic := pic.cptr()
- C.x264_picture_clean(cpic)
-}
-
-// EncoderOpen - create a new encoder handler, all parameters from Param are copied.
-func EncoderOpen(param *Param) *T {
- cparam := param.cptr()
-
- ret := C.x264_encoder_open(cparam)
- v := *(**T)(unsafe.Pointer(&ret))
- return v
-}
-
-// EncoderReconfig - various parameters from Param are copied.
-// Returns 0 on success, negative on parameter validation error.
-func EncoderReconfig(enc *T, param *Param) int32 {
- cenc := enc.cptr()
- cparam := param.cptr()
-
- ret := C.x264_encoder_reconfig(cenc, cparam)
- v := (int32)(ret)
- return v
-}
-
-// EncoderParameters - copies the current internal set of parameters to the pointer provided.
-func EncoderParameters(enc *T, param *Param) {
- cenc := enc.cptr()
- cparam := param.cptr()
-
- C.x264_encoder_parameters(cenc, cparam)
-}
-
-// EncoderHeaders - return the SPS and PPS that will be used for the whole stream.
-// Returns the number of bytes in the returned NALs or negative on error.
-func EncoderHeaders(enc *T, ppNal []*Nal, piNal *int32) int32 {
- cenc := enc.cptr()
-
- cppNal := (**C.x264_nal_t)(unsafe.Pointer(&ppNal[0]))
- cpiNal := (*C.int)(unsafe.Pointer(piNal))
-
- ret := C.x264_encoder_headers(cenc, cppNal, cpiNal)
- v := (int32)(ret)
- return v
-}
-
-// 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()
-
- ret := C.x264_encoder_encode(cenc, cppNal, cpiNal, cpicIn, cpicOut)
- v := (int32)(ret)
- return v
-}
-
-// EncoderClose - close an encoder handler.
-func EncoderClose(enc *T) {
- cenc := enc.cptr()
- C.x264_encoder_close(cenc)
-}
-
-// EncoderDelayedFrames - return the number of currently delayed (buffered) frames.
-// This should be used at the end of the stream, to know when you have all the encoded frames.
-func EncoderDelayedFrames(enc *T) int32 {
- cenc := enc.cptr()
-
- ret := C.x264_encoder_delayed_frames(cenc)
- v := (int32)(ret)
- return v
-}
-
-// EncoderMaximumDelayedFrames - return the maximum number of delayed (buffered) frames that can occur with the current parameters.
-func EncoderMaximumDelayedFrames(enc *T) int32 {
- cenc := enc.cptr()
-
- ret := C.x264_encoder_maximum_delayed_frames(cenc)
- v := (int32)(ret)
- return v
-}
-
-// 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) {
- cenc := enc.cptr()
- C.x264_encoder_intra_refresh(cenc)
-}
-
-// EncoderInvalidateReference - An interactive error resilience tool, designed for use in a low-latency one-encoder-few-clients system.
-// Should not be called during an EncoderEncode, but multiple calls can be made simultaneously.
-//
-// Returns 0 on success, negative on failure.
-func EncoderInvalidateReference(enc *T, pts int) int32 {
- cenc := enc.cptr()
- cpts := (C.int64_t)(pts)
-
- ret := C.x264_encoder_invalidate_reference(cenc, cpts)
- v := (int32)(ret)
- return v
-}
diff --git a/pkg/encoder/h264/options.go b/pkg/encoder/h264/options.go
deleted file mode 100644
index f14e475f..00000000
--- a/pkg/encoder/h264/options.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package h264
-
-type Options struct {
- // 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 worst quality possible.
- Crf uint8
- // film, animation, grain, stillimage, psnr, ssim, fastdecode, zerolatency.
- Tune string
- // ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow, placebo.
- Preset string
- // baseline, main, high, high10, high422, high444.
- Profile string
- LogLevel int32
-}
-
-type Option func(*Options)
-
-func WithOptions(arg Options) Option {
- return func(args *Options) {
- args.Crf = arg.Crf
- args.Tune = arg.Tune
- args.Preset = arg.Preset
- args.Profile = arg.Profile
- args.LogLevel = arg.LogLevel
- }
-}
-func Crf(arg uint8) Option { return func(args *Options) { args.Crf = arg } }
-func Tune(arg string) Option { return func(args *Options) { args.Tune = arg } }
-func Preset(arg string) Option { return func(args *Options) { args.Preset = arg } }
-func Profile(arg string) Option { return func(args *Options) { args.Profile = arg } }
-func LogLevel(arg int32) Option { return func(args *Options) { args.LogLevel = arg } }
diff --git a/pkg/encoder/h264/x264.go b/pkg/encoder/h264/x264.go
index 6b14e71a..58259ff4 100644
--- a/pkg/encoder/h264/x264.go
+++ b/pkg/encoder/h264/x264.go
@@ -1,126 +1,206 @@
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
+
+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"
- "log"
+ "strings"
+ "unsafe"
)
type H264 struct {
- ref *T
-
- width int32
- lumaSize int32
- chromaSize int32
- csp int32
- nnals int32
- nals []*Nal
-
- // keep monotonic pts to suppress warnings
- pts int64
+ h *C.h264
}
-func NewEncoder(width, height int, options ...Option) (encoder *H264, err error) {
- libVersion := int(Build)
+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
+ // vbv-maxrate
+ MaxRate int
+ // vbv-bufsize
+ BufSize int
+ LogLevel int32
+ // ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow, placebo.
+ Preset string
+ // baseline, main, high, high10, high422, high444.
+ Profile string
+ // film, animation, grain, stillimage, psnr, ssim, fastdecode, zerolatency.
+ Tune string
+}
- if libVersion < 150 {
- return nil, fmt.Errorf("x264: the library version should be newer than v150, you have got version %v", libVersion)
+func NewEncoder(w, h int, th int, opts *Options) (encoder *H264, err error) {
+ ver := Version()
+
+ if ver < 150 {
+ return nil, fmt.Errorf("x264: the library version should be newer than v150, you have got version %v", ver)
}
- if libVersion < 160 {
- log.Printf("x264: warning, installed version of libx264 %v is older than minimally supported v160, expect bugs", libVersion)
+ if opts == nil {
+ opts = &Options{
+ Mode: "crf",
+ Crf: 23,
+ Tune: "zerolatency",
+ Preset: "superfast",
+ Profile: "baseline",
+ }
}
- opts := &Options{
- Crf: 12,
- Tune: "zerolatency",
- Preset: "superfast",
- Profile: "baseline",
- }
+ param := C.x264_param_t{}
- for _, opt := range options {
- opt(opts)
- }
-
- if opts.LogLevel > 0 {
- log.Printf("x264: build v%v", Build)
- }
-
- param := Param{}
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(width)
- param.IHeight = int32(height)
- param.ILogLevel = opts.LogLevel
-
- param.Rc.IRcMethod = RcCrf
- param.Rc.FRfConstant = float32(opts.Crf)
-
- encoder = &H264{
- csp: param.ICsp,
- lumaSize: int32(width * height),
- chromaSize: int32(width*height) / 4,
- nals: make([]*Nal, 1),
- width: int32(width),
+ 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")
- return
+ 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 (e *H264) Encode(yuv []byte) []byte {
- var picIn, picOut Picture
+ 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)
+}
- picIn.Img.ICsp = e.csp
- picIn.Img.IPlane = 3
- picIn.Img.IStride[0] = e.width
- picIn.Img.IStride[1] = e.width / 2
- picIn.Img.IStride[2] = e.width / 2
+func (e *H264) IntraRefresh() {
+ // !to implement
+}
- picIn.Img.Plane[0] = C.CBytes(yuv[:e.lumaSize])
- picIn.Img.Plane[1] = C.CBytes(yuv[e.lumaSize : e.lumaSize+e.chromaSize])
- picIn.Img.Plane[2] = C.CBytes(yuv[e.lumaSize+e.chromaSize:])
+func (e *H264) Info() string { return fmt.Sprintf("x264: v%v", Version()) }
- picIn.IPts = e.pts
- e.pts++
-
- defer func() {
- picIn.freePlane(0)
- picIn.freePlane(1)
- picIn.freePlane(2)
- }()
-
- if ret := EncoderEncode(e.ref, e.nals, &e.nnals, &picIn, &picOut); ret > 0 {
- return C.GoBytes(e.nals[0].PPayload, C.int(ret))
- // ret should be equal to writer writes
+func (e *H264) SetFlip(b bool) {
+ if b {
+ (*e.h).pic.img.i_csp |= C.X264_CSP_VFLIP
+ } else {
+ (*e.h).pic.img.i_csp &= ^C.X264_CSP_VFLIP
}
- return []byte{}
}
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) }
diff --git a/pkg/encoder/h264/x264_test.go b/pkg/encoder/h264/x264_test.go
new file mode 100644
index 00000000..822e4cec
--- /dev/null
+++ b/pkg/encoder/h264/x264_test.go
@@ -0,0 +1,29 @@
+package h264
+
+import "testing"
+
+func TestH264Encode(t *testing.T) {
+ h264, err := NewEncoder(120, 120, 0, nil)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ data := make([]byte, 120*120*1.5)
+ h264.Encode(data)
+ if err := h264.Shutdown(); err != nil {
+ t.Error(err)
+ }
+}
+
+func Benchmark(b *testing.B) {
+ w, h := 1920, 1080
+ 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 b.Loop() {
+ h264.Encode(data)
+ }
+}
diff --git a/pkg/encoder/opus/buffer.go b/pkg/encoder/opus/buffer.go
deleted file mode 100644
index 1f91ffbe..00000000
--- a/pkg/encoder/opus/buffer.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package opus
-
-type Buffer struct {
- Data []int16
- idx int
-}
-
-func (b *Buffer) Write(samples []int16) (written int) {
- w := copy(b.Data[b.idx:], samples)
- b.idx += w
- return w
-}
-
-func (b *Buffer) Full() bool {
- full := b.idx == len(b.Data)
- if full {
- b.idx = 0
- }
- return full
-}
diff --git a/pkg/encoder/opus/encoder.go b/pkg/encoder/opus/encoder.go
deleted file mode 100644
index b2a7d436..00000000
--- a/pkg/encoder/opus/encoder.go
+++ /dev/null
@@ -1,136 +0,0 @@
-package opus
-
-import (
- "fmt"
- "log"
-)
-
-type Encoder struct {
- *LibOpusEncoder
-
- channels int
- inFrequency int
- outFrequency int
- // OPUS output buffer, 1K should be enough
- outBufferSize int
-
- buffer Buffer
- onFullBuffer func(data []byte)
- resampleBufSize int
-}
-
-func NewEncoder(inputSampleRate, outputSampleRate, channels int, options ...func(*Encoder) error) (Encoder, error) {
- encoder, err := NewOpusEncoder(
- outputSampleRate,
- channels,
- // be aware that low delay option is not optimized for voice
- AppRestrictedLowdelay,
- )
- if err != nil {
- return Encoder{}, err
- }
- enc := &Encoder{
- LibOpusEncoder: encoder,
- buffer: Buffer{Data: make([]int16, inputSampleRate*20/1000*channels)},
- channels: channels,
- inFrequency: inputSampleRate,
- outFrequency: outputSampleRate,
- outBufferSize: 1024,
- onFullBuffer: func(data []byte) {},
- }
-
- _ = enc.SetMaxBandwidth(FullBand)
- _ = enc.SetBitrate(192000)
- _ = enc.SetComplexity(10)
-
- for _, option := range options {
- err := option(enc)
- if err != nil {
- return Encoder{}, err
- }
- }
- return *enc, nil
-}
-
-func SampleBuffer(ms int, resampling bool) func(*Encoder) error {
- return func(e *Encoder) (err error) {
- e.buffer = Buffer{Data: make([]int16, e.inFrequency*ms/1000*e.channels)}
- if resampling {
- e.resampleBufSize = e.outFrequency * ms / 1000 * e.channels
- }
- return
- }
-}
-
-func CallbackOnFullBuffer(fn func(_ []byte)) func(*Encoder) error {
- return func(e *Encoder) (err error) {
- e.onFullBuffer = fn
- return
- }
-}
-
-func (e *Encoder) BufferWrite(samples []int16) (written int) {
- n := len(samples)
- for k := n / len(e.buffer.Data); written < n || k >= 0; k-- {
- written += e.buffer.Write(samples[written:])
- if e.buffer.Full() {
- data, err := e.Encode(e.buffer.Data)
- if err != nil {
- log.Println("[!] Failed to encode", err)
- continue
- }
- e.onFullBuffer(data)
- }
- }
- return
-}
-
-func (e *Encoder) Encode(pcm []int16) ([]byte, error) {
- if e.resampleBufSize > 0 {
- pcm = resampleFn(pcm, e.resampleBufSize)
- }
- data := make([]byte, e.outBufferSize)
- n, err := e.LibOpusEncoder.Encode(pcm, data)
- if err != nil {
- return []byte{}, err
- }
- return data[:n], nil
-}
-
-func (e *Encoder) GetInfo() string {
- bitrate, _ := e.LibOpusEncoder.Bitrate()
- complexity, _ := e.LibOpusEncoder.Complexity()
- dtx, _ := e.LibOpusEncoder.DTX()
- fec, _ := e.LibOpusEncoder.FEC()
- maxBandwidth, _ := e.LibOpusEncoder.MaxBandwidth()
- lossPercent, _ := e.LibOpusEncoder.PacketLossPerc()
- sampleRate, _ := e.LibOpusEncoder.SampleRate()
- return fmt.Sprintf(
- "%v, Bitrate: %v bps, Complexity: %v, DTX: %v, FEC: %v, Max bandwidth: *%v, Loss%%: %v, Rate: %v Hz",
- CodecVersion(), bitrate, complexity, dtx, fec, maxBandwidth, lossPercent, sampleRate,
- )
-}
-
-// resampleFn does a simple linear interpolation of audio samples.
-func resampleFn(pcm []int16, size int) []int16 {
- r, l, audio := make([]int16, size/2), make([]int16, size/2), make([]int16, size)
- // ratio is basically the destination sample rate
- // divided by the origin sample rate (i.e. 48000/44100)
- ratio := float32(size) / float32(len(pcm))
- for i, n := 0, len(pcm)-1; i < n; i += 2 {
- idx := int(float32(i/2) * ratio)
- r[idx], l[idx] = pcm[i], pcm[i+1]
- }
- for i, n := 1, len(r); i < n; i++ {
- if r[i] == 0 {
- r[i] = r[i-1]
- }
- if l[i] == 0 {
- l[i] = l[i-1]
- }
- }
- for i := 0; i < size-1; i += 2 {
- audio[i], audio[i+1] = r[i/2], l[i/2]
- }
- return audio
-}
diff --git a/pkg/encoder/opus/encoder_test.go b/pkg/encoder/opus/encoder_test.go
deleted file mode 100644
index a61ee9c7..00000000
--- a/pkg/encoder/opus/encoder_test.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package opus
-
-import (
- "math/rand"
- "testing"
- "time"
-)
-
-var resampleData []int16
-
-func init() {
- rand.Seed(time.Now().Unix())
- l := rand.Perm(getBufferLength(44000))
- for _, n := range l {
- resampleData = append(resampleData, int16(n))
- }
-}
-
-func BenchmarkResample(b *testing.B) {
- sr := 48000
- bs := getBufferLength(sr)
- for i := 0; i < b.N; i++ {
- resampleFn(resampleData, bs)
- }
-}
-
-func getBufferLength(sampleRate int) int { return sampleRate * 20 / 1000 * 2 }
diff --git a/pkg/encoder/opus/opus.go b/pkg/encoder/opus/opus.go
index 086aa9a3..95b111f1 100644
--- a/pkg/encoder/opus/opus.go
+++ b/pkg/encoder/opus/opus.go
@@ -2,22 +2,24 @@ package opus
/*
#cgo pkg-config: opus
+#cgo st LDFLAGS: -l:libopus.a
#include
-int bridge_encoder_get_bitrate(OpusEncoder *st, opus_int32 *bitrate) { return opus_encoder_ctl(st, OPUS_GET_BITRATE(bitrate)); }
-int bridge_encoder_get_complexity(OpusEncoder *st, opus_int32 *complexity) { return opus_encoder_ctl(st, OPUS_GET_COMPLEXITY(complexity)); }
-int bridge_encoder_get_dtx(OpusEncoder *st, opus_int32 *dtx) { return opus_encoder_ctl(st, OPUS_GET_DTX(dtx)); }
-int bridge_encoder_get_inband_fec(OpusEncoder *st, opus_int32 *fec) { return opus_encoder_ctl(st, OPUS_GET_INBAND_FEC(fec)); }
-int bridge_encoder_get_max_bandwidth(OpusEncoder *st, opus_int32 *max_bw) { return opus_encoder_ctl(st, OPUS_GET_MAX_BANDWIDTH(max_bw)); }
-int bridge_encoder_get_packet_loss_perc(OpusEncoder *st, opus_int32 *loss_perc) { return opus_encoder_ctl(st, OPUS_GET_PACKET_LOSS_PERC(loss_perc)); }
-int bridge_encoder_get_sample_rate(OpusEncoder *st, opus_int32 *sample_rate) { return opus_encoder_ctl(st, OPUS_GET_SAMPLE_RATE(sample_rate)); }
-int bridge_encoder_set_bitrate(OpusEncoder *st, opus_int32 bitrate) { return opus_encoder_ctl(st, OPUS_SET_BITRATE(bitrate)); }
-int bridge_encoder_set_complexity(OpusEncoder *st, opus_int32 complexity) { return opus_encoder_ctl(st, OPUS_SET_COMPLEXITY(complexity)); }
-int bridge_encoder_set_dtx(OpusEncoder *st, opus_int32 use_dtx) { return opus_encoder_ctl(st, OPUS_SET_DTX(use_dtx)); }
-int bridge_encoder_set_inband_fec(OpusEncoder *st, opus_int32 fec) { return opus_encoder_ctl(st, OPUS_SET_INBAND_FEC(fec)); }
-int bridge_encoder_set_max_bandwidth(OpusEncoder *st, opus_int32 max_bw) { return opus_encoder_ctl(st, OPUS_SET_MAX_BANDWIDTH(max_bw)); }
-int bridge_encoder_set_packet_loss_perc(OpusEncoder *st, opus_int32 loss_perc) { return opus_encoder_ctl(st, OPUS_SET_PACKET_LOSS_PERC(loss_perc)); }
+int get_bitrate(OpusEncoder *st, opus_int32 *bitrate) { return opus_encoder_ctl(st, OPUS_GET_BITRATE(bitrate)); }
+int get_complexity(OpusEncoder *st, opus_int32 *complexity) { return opus_encoder_ctl(st, OPUS_GET_COMPLEXITY(complexity)); }
+int get_dtx(OpusEncoder *st, opus_int32 *dtx) { return opus_encoder_ctl(st, OPUS_GET_DTX(dtx)); }
+int get_inband_fec(OpusEncoder *st, opus_int32 *fec) { return opus_encoder_ctl(st, OPUS_GET_INBAND_FEC(fec)); }
+int get_max_bandwidth(OpusEncoder *st, opus_int32 *max_bw) { return opus_encoder_ctl(st, OPUS_GET_MAX_BANDWIDTH(max_bw)); }
+int get_packet_loss_perc(OpusEncoder *st, opus_int32 *loss_perc) { return opus_encoder_ctl(st, OPUS_GET_PACKET_LOSS_PERC(loss_perc)); }
+int get_sample_rate(OpusEncoder *st, opus_int32 *sample_rate) { return opus_encoder_ctl(st, OPUS_GET_SAMPLE_RATE(sample_rate)); }
+int set_bitrate(OpusEncoder *st, opus_int32 bitrate) { return opus_encoder_ctl(st, OPUS_SET_BITRATE(bitrate)); }
+int set_complexity(OpusEncoder *st, opus_int32 complexity) { return opus_encoder_ctl(st, OPUS_SET_COMPLEXITY(complexity)); }
+int set_dtx(OpusEncoder *st, opus_int32 use_dtx) { return opus_encoder_ctl(st, OPUS_SET_DTX(use_dtx)); }
+int set_inband_fec(OpusEncoder *st, opus_int32 fec) { return opus_encoder_ctl(st, OPUS_SET_INBAND_FEC(fec)); }
+int set_max_bandwidth(OpusEncoder *st, opus_int32 max_bw) { return opus_encoder_ctl(st, OPUS_SET_MAX_BANDWIDTH(max_bw)); }
+int set_packet_loss_perc(OpusEncoder *st, opus_int32 loss_perc) { return opus_encoder_ctl(st, OPUS_SET_PACKET_LOSS_PERC(loss_perc)); }
+int reset_state(OpusEncoder *st) { return opus_encoder_ctl(st, OPUS_RESET_STATE); }
*/
import "C"
import (
@@ -33,161 +35,159 @@ type (
)
const (
- // Optimize encoding for VoIP
- AppVoIP = Application(C.OPUS_APPLICATION_VOIP)
- // Optimize encoding for non-voice signals like music
- AppAudio = Application(C.OPUS_APPLICATION_AUDIO)
- // Optimize encoding for low latency applications
- AppRestrictedLowdelay = Application(C.OPUS_APPLICATION_RESTRICTED_LOWDELAY)
-
- // Auto/default setting
- BitrateAuto = Bitrate(-1000)
- BitrateMax = Bitrate(-1)
-
- // 20 kHz bandpass
+ // AppRestrictedLowDelay optimizes encoding for low latency applications
+ AppRestrictedLowDelay = Application(C.OPUS_APPLICATION_RESTRICTED_LOWDELAY)
+ // FullBand is 20 kHz bandpass
FullBand = Bandwidth(C.OPUS_BANDWIDTH_FULLBAND)
)
+const stereo = C.int(2)
-const (
- ErrorOK = Error(C.OPUS_OK)
- ErrorBadArg = Error(C.OPUS_BAD_ARG)
- ErrorBufferTooSmall = Error(C.OPUS_BUFFER_TOO_SMALL)
- ErrorInternalError = Error(C.OPUS_INTERNAL_ERROR)
- ErrorInvalidPacket = Error(C.OPUS_INVALID_PACKET)
- ErrorUnimplemented = Error(C.OPUS_UNIMPLEMENTED)
- ErrorInvalidState = Error(C.OPUS_INVALID_STATE)
- ErrorAllocFail = Error(C.OPUS_ALLOC_FAIL)
-)
-
-type LibOpusEncoder struct {
- buf []byte
- channels int
- ptr *C.struct_OpusEncoder
+type Encoder struct {
+ mem []byte
+ out []byte
+ st *C.struct_OpusEncoder
}
-// NewOpusEncoder creates new Opus encoder.
-func NewOpusEncoder(sampleRate int, channels int, app Application) (*LibOpusEncoder, error) {
- var enc LibOpusEncoder
- if enc.ptr != nil {
- return nil, fmt.Errorf("opus: encoder reinit")
+func NewEncoder(outFq int) (*Encoder, error) {
+ mem := make([]byte, C.opus_encoder_get_size(stereo))
+ out := make([]byte, 1024)
+ enc := Encoder{
+ mem: mem,
+ st: (*C.OpusEncoder)(unsafe.Pointer(&mem[0])),
+ out: out,
}
- enc.channels = channels
- // !to check mem leak
- enc.buf = make([]byte, C.opus_encoder_get_size(C.int(channels)))
- enc.ptr = (*C.OpusEncoder)(unsafe.Pointer(&enc.buf[0]))
- err := unwrap(C.opus_encoder_init(enc.ptr, C.opus_int32(sampleRate), C.int(channels), C.int(app)))
+ err := unwrap(C.opus_encoder_init(enc.st, C.opus_int32(outFq), stereo, C.int(AppRestrictedLowDelay)))
if err != nil {
return nil, fmt.Errorf("opus: initializatoin error (%v)", err)
}
+ _ = enc.SetMaxBandwidth(FullBand)
+ _ = enc.SetBitrate(96000)
+ _ = enc.SetComplexity(5)
+
return &enc, nil
}
-// Encode converts raw PCM samples into the supplied Opus buffer.
-// Returns the number of bytes converted.
-func (enc *LibOpusEncoder) Encode(pcm []int16, data []byte) (rez int, err error) {
+func (e *Encoder) Reset() error { return e.ResetState() }
+
+func (e *Encoder) Encode(pcm []int16) ([]byte, error) {
if len(pcm) == 0 {
- return
+ return nil, nil
}
- samples := C.int(len(pcm) / enc.channels)
- n := C.opus_encode(enc.ptr, (*C.opus_int16)(&pcm[0]), samples, (*C.uchar)(&data[0]), C.opus_int32(cap(data)))
- if n > 0 {
- rez = int(n)
+ n := C.opus_encode(e.st, (*C.opus_int16)(&pcm[0]), C.int(len(pcm)>>1), (*C.uchar)(&e.out[0]), C.opus_int32(cap(pcm)))
+ err := unwrap(n)
+ // n = 1 is DTX
+ if err != nil || n == 1 {
+ return []byte{}, err
}
- return rez, unwrap(n)
+ return e.out[:int(n)], nil
+}
+
+func (e *Encoder) GetInfo() string {
+ bitrate, _ := e.Bitrate()
+ complexity, _ := e.Complexity()
+ dtx, _ := e.DTX()
+ fec, _ := e.FEC()
+ maxBandwidth, _ := e.MaxBandwidth()
+ lossPercent, _ := e.PacketLossPerc()
+ sampleRate, _ := e.SampleRate()
+ return fmt.Sprintf(
+ "%v, Bitrate: %v bps, Complexity: %v, DTX: %v, FEC: %v, Max bandwidth: *%v, Loss%%: %v, Rate: %v Hz",
+ CodecVersion(), bitrate, complexity, dtx, fec, maxBandwidth, lossPercent, sampleRate,
+ )
}
// SampleRate returns the sample rate of the encoder.
-func (enc *LibOpusEncoder) SampleRate() (int, error) {
+func (e *Encoder) SampleRate() (int, error) {
var sampleRate C.opus_int32
- res := C.bridge_encoder_get_sample_rate(enc.ptr, &sampleRate)
+ res := C.get_sample_rate(e.st, &sampleRate)
return int(sampleRate), unwrap(res)
}
// Bitrate returns the bitrate of the encoder.
-func (enc *LibOpusEncoder) Bitrate() (int, error) {
+func (e *Encoder) Bitrate() (int, error) {
var bitrate C.opus_int32
- res := C.bridge_encoder_get_bitrate(enc.ptr, &bitrate)
+ res := C.get_bitrate(e.st, &bitrate)
return int(bitrate), unwrap(res)
}
// SetBitrate sets the bitrate of the encoder.
// BitrateMax / BitrateAuto can be used here.
-func (enc *LibOpusEncoder) SetBitrate(b Bitrate) error {
- return unwrap(C.bridge_encoder_set_bitrate(enc.ptr, C.opus_int32(b)))
+func (e *Encoder) SetBitrate(b Bitrate) error {
+ return unwrap(C.set_bitrate(e.st, C.opus_int32(b)))
}
// Complexity returns the value of the complexity.
-func (enc *LibOpusEncoder) Complexity() (int, error) {
+func (e *Encoder) Complexity() (int, error) {
var complexity C.opus_int32
- res := C.bridge_encoder_get_complexity(enc.ptr, &complexity)
+ res := C.get_complexity(e.st, &complexity)
return int(complexity), unwrap(res)
}
// SetComplexity sets the complexity factor for the encoder.
// Complexity is a value from 1 to 10, where 1 is the lowest complexity and 10 is the highest.
-func (enc *LibOpusEncoder) SetComplexity(complexity int) error {
- return unwrap(C.bridge_encoder_set_complexity(enc.ptr, C.opus_int32(complexity)))
+func (e *Encoder) SetComplexity(complexity int) error {
+ return unwrap(C.set_complexity(e.st, C.opus_int32(complexity)))
}
// DTX says if discontinuous transmission is enabled.
-func (enc *LibOpusEncoder) DTX() (bool, error) {
+func (e *Encoder) DTX() (bool, error) {
var dtx C.opus_int32
- res := C.bridge_encoder_get_dtx(enc.ptr, &dtx)
+ res := C.get_dtx(e.st, &dtx)
return dtx > 0, unwrap(res)
}
// SetDTX switches discontinuous transmission.
-func (enc *LibOpusEncoder) SetDTX(dtx bool) error {
+func (e *Encoder) SetDTX(dtx bool) error {
var i int
if dtx {
i = 1
}
- return unwrap(C.bridge_encoder_set_dtx(enc.ptr, C.opus_int32(i)))
+ return unwrap(C.set_dtx(e.st, C.opus_int32(i)))
}
// MaxBandwidth returns the maximum allowed bandpass value.
-func (enc *LibOpusEncoder) MaxBandwidth() (Bandwidth, error) {
+func (e *Encoder) MaxBandwidth() (Bandwidth, error) {
var b C.opus_int32
- res := C.bridge_encoder_get_max_bandwidth(enc.ptr, &b)
+ res := C.get_max_bandwidth(e.st, &b)
return Bandwidth(b), unwrap(res)
}
// SetMaxBandwidth sets the upper limit of the bandpass.
-func (enc *LibOpusEncoder) SetMaxBandwidth(b Bandwidth) error {
- return unwrap(C.bridge_encoder_set_max_bandwidth(enc.ptr, C.opus_int32(b)))
+func (e *Encoder) SetMaxBandwidth(b Bandwidth) error {
+ return unwrap(C.set_max_bandwidth(e.st, C.opus_int32(b)))
}
// FEC says if forward error correction (FEC) is enabled.
-func (enc *LibOpusEncoder) FEC() (bool, error) {
+func (e *Encoder) FEC() (bool, error) {
var fec C.opus_int32
- res := C.bridge_encoder_get_inband_fec(enc.ptr, &fec)
+ res := C.get_inband_fec(e.st, &fec)
return fec > 0, unwrap(res)
}
// SetFEC switches the forward error correction (FEC).
-func (enc *LibOpusEncoder) SetFEC(fec bool) error {
+func (e *Encoder) SetFEC(fec bool) error {
var i int
if fec {
i = 1
}
- return unwrap(C.bridge_encoder_set_inband_fec(enc.ptr, C.opus_int32(i)))
+ return unwrap(C.set_inband_fec(e.st, C.opus_int32(i)))
}
// PacketLossPerc returns configured packet loss percentage.
-func (enc *LibOpusEncoder) PacketLossPerc() (int, error) {
+func (e *Encoder) PacketLossPerc() (int, error) {
var lossPerc C.opus_int32
- res := C.bridge_encoder_get_packet_loss_perc(enc.ptr, &lossPerc)
+ res := C.get_packet_loss_perc(e.st, &lossPerc)
return int(lossPerc), unwrap(res)
}
// SetPacketLossPerc sets expected packet loss percentage.
-func (enc *LibOpusEncoder) SetPacketLossPerc(lossPerc int) error {
- return unwrap(C.bridge_encoder_set_packet_loss_perc(enc.ptr, C.opus_int32(lossPerc)))
+func (e *Encoder) SetPacketLossPerc(lossPerc int) error {
+ return unwrap(C.set_packet_loss_perc(e.st, C.opus_int32(lossPerc)))
}
-func (e Error) Error() string {
- return fmt.Sprintf("opus: %v", C.GoString(C.opus_strerror(C.int(e))))
-}
+func (e *Encoder) ResetState() error { return unwrap(C.reset_state(e.st)) }
+
+func (e Error) Error() string { return fmt.Sprintf("opus: %v", C.GoString(C.opus_strerror(C.int(e)))) }
func unwrap(error C.int) (err error) {
if error < C.OPUS_OK {
diff --git a/pkg/encoder/pipe.go b/pkg/encoder/pipe.go
deleted file mode 100644
index 3ff2f39c..00000000
--- a/pkg/encoder/pipe.go
+++ /dev/null
@@ -1,65 +0,0 @@
-package encoder
-
-import (
- "log"
-
- "github.com/giongto35/cloud-game/v2/pkg/encoder/yuv"
-)
-
-type VideoPipe struct {
- Input chan InFrame
- Output chan OutFrame
- done chan struct{}
-
- encoder Encoder
-
- // frame size
- w, h int
-}
-
-// NewVideoPipe returns new video encoder pipe.
-// By default it waits for RGBA images on the input channel,
-// converts them into YUV I420 format,
-// encodes with provided video encoder, and
-// puts the result into the output channel.
-func NewVideoPipe(enc Encoder, w, h int) *VideoPipe {
- return &VideoPipe{
- Input: make(chan InFrame, 1),
- Output: make(chan OutFrame, 2),
- done: make(chan struct{}),
-
- encoder: enc,
-
- w: w,
- h: h,
- }
-}
-
-// Start begins video encoding pipe.
-// Should be wrapped into a goroutine.
-func (vp *VideoPipe) Start() {
- defer func() {
- if r := recover(); r != nil {
- log.Println("Warn: Recovered panic in encoding ", r)
- }
- close(vp.Output)
- close(vp.done)
- }()
-
- yuvProc := yuv.NewYuvImgProcessor(vp.w, vp.h)
- for img := range vp.Input {
- yCbCr := yuvProc.Process(img.Image).Get()
- frame := vp.encoder.Encode(yCbCr)
- if len(frame) > 0 {
- vp.Output <- OutFrame{Data: frame, Timestamp: img.Timestamp}
- }
- }
-}
-
-func (vp *VideoPipe) Stop() {
- close(vp.Input)
- <-vp.done
- if err := vp.encoder.Shutdown(); err != nil {
- log.Println("error: failed to close the encoder")
- }
-}
diff --git a/pkg/encoder/type.go b/pkg/encoder/type.go
deleted file mode 100644
index 83696959..00000000
--- a/pkg/encoder/type.go
+++ /dev/null
@@ -1,18 +0,0 @@
-package encoder
-
-import "image"
-
-type InFrame struct {
- Image *image.RGBA
- Timestamp uint32
-}
-
-type OutFrame struct {
- Data []byte
- Timestamp uint32
-}
-
-type Encoder interface {
- Encode(input []byte) []byte
- Shutdown() error
-}
diff --git a/pkg/encoder/vpx/libvpx.go b/pkg/encoder/vpx/libvpx.go
index 4b3a95c7..c175c14f 100644
--- a/pkg/encoder/vpx/libvpx.go
+++ b/pkg/encoder/vpx/libvpx.go
@@ -2,6 +2,7 @@ package vpx
/*
#cgo pkg-config: vpx
+#cgo st LDFLAGS: -l:libvpx.a
#include "vpx/vpx_encoder.h"
#include "vpx/vpx_image.h"
@@ -11,6 +12,7 @@ package vpx
#include
#define VP8_FOURCC 0x30385056
+#define VP9_FOURCC 0x30395056
typedef struct VpxInterface {
const char *const name;
@@ -41,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)
@@ -83,29 +88,43 @@ type Vpx struct {
image C.vpx_image_t
codecCtx C.vpx_codec_ctx_t
kfi C.int
+ flipped bool
+ v int
}
-func NewEncoder(width, height int, options ...Option) (*Vpx, error) {
- encoder := &C.vpx_encoders[0]
+func (vpx *Vpx) SetFlip(b bool) { vpx.flipped = b }
+
+type Options struct {
+ // Target bandwidth to use for this stream, in kilobits per second.
+ Bitrate uint
+ // Force keyframe interval.
+ KeyframeInterval uint
+}
+
+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")
}
- opts := &Options{
- Bitrate: 1200,
- KeyframeInt: 5,
- }
-
- for _, opt := range options {
- opt(opts)
+ if opts == nil {
+ opts = &Options{
+ Bitrate: 1200,
+ KeyframeInterval: 5,
+ }
}
vpx := Vpx{
frameCount: C.int(0),
- kfi: C.int(opts.KeyframeInt),
+ kfi: C.int(opts.KeyframeInterval),
+ v: version,
}
- if C.vpx_img_alloc(&vpx.image, C.VPX_IMG_FMT_I420, C.uint(width), C.uint(height), 1) == nil {
+ if C.vpx_img_alloc(&vpx.image, C.VPX_IMG_FMT_I420, C.uint(w), C.uint(h), 1) == nil {
return nil, fmt.Errorf("vpx_img_alloc failed")
}
@@ -114,10 +133,14 @@ func NewEncoder(width, height int, options ...Option) (*Vpx, error) {
return nil, fmt.Errorf("failed to get default codec config")
}
- cfg.g_w = C.uint(width)
- cfg.g_h = C.uint(height)
+ 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")
@@ -126,10 +149,13 @@ func NewEncoder(width, height int, options ...Option) (*Vpx, error) {
return &vpx, nil
}
+// 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 {
- var iter C.vpx_codec_iter_t
C.vpx_img_read(&vpx.image, unsafe.Pointer(&yuv[0]))
+ if vpx.flipped {
+ C.vpx_img_flip(&vpx.image)
+ }
var flags C.int
if vpx.kfi > 0 && vpx.frameCount%vpx.kfi == 0 {
@@ -140,6 +166,7 @@ func (vpx *Vpx) Encode(yuv []byte) []byte {
}
vpx.frameCount++
+ var iter C.vpx_codec_iter_t
fb := C.get_frame_buffer(&vpx.codecCtx, &iter)
if fb.ptr == nil {
return []byte{}
@@ -147,10 +174,19 @@ func (vpx *Vpx) Encode(yuv []byte) []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
+}
+
func (vpx *Vpx) Shutdown() error {
- if &vpx.image != nil {
- C.vpx_img_free(&vpx.image)
- }
+ //if &vpx.image != nil {
+ C.vpx_img_free(&vpx.image)
+ //}
C.vpx_codec_destroy(&vpx.codecCtx)
+ vpx.flipped = false
return nil
}
diff --git a/pkg/encoder/vpx/options.go b/pkg/encoder/vpx/options.go
deleted file mode 100644
index e582985b..00000000
--- a/pkg/encoder/vpx/options.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package vpx
-
-type Options struct {
- // Target bandwidth to use for this stream, in kilobits per second.
- Bitrate uint
- // Force keyframe interval.
- KeyframeInt uint
-}
-
-type Option func(*Options)
-
-func WithOptions(arg Options) Option {
- return func(args *Options) {
- args.Bitrate = arg.Bitrate
- args.KeyframeInt = arg.KeyframeInt
- }
-}
diff --git a/pkg/encoder/yuv/libyuv/libyuv.go b/pkg/encoder/yuv/libyuv/libyuv.go
new file mode 100644
index 00000000..0848c095
--- /dev/null
+++ b/pkg/encoder/yuv/libyuv/libyuv.go
@@ -0,0 +1,274 @@
+// Package libyuv contains the wrapper for: https://chromium.googlesource.com/libyuv/libyuv.
+// MacOS libs are from: https://packages.macports.org/libyuv/.
+package libyuv
+
+/*
+#cgo !darwin,!st LDFLAGS: -lyuv
+#cgo !darwin,st LDFLAGS: -l:libyuv.a -l:libjpeg.a -l:libstdc++.a -static-libgcc
+
+#cgo darwin CFLAGS: -DINCLUDE_LIBYUV_VERSION_H_
+#cgo darwin LDFLAGS: -L${SRCDIR} -lstdc++
+#cgo darwin,amd64 LDFLAGS: -lyuv_darwin_x86_64 -ljpeg -lstdc++
+#cgo darwin,arm64 LDFLAGS: -lyuv_darwin_arm64 -ljpeg -lstdc++
+
+#include // for uintptr_t and C99 types
+#include
+
+#if !defined(LIBYUV_API)
+#define LIBYUV_API
+#endif // LIBYUV_API
+
+#ifndef INCLUDE_LIBYUV_VERSION_H_
+#include "libyuv/version.h"
+#else
+#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" {
+#endif
+
+#define FOURCC(a, b, c, d) \
+ (((uint32_t)(a)) | ((uint32_t)(b) << 8) | ((uint32_t)(c) << 16) | ((uint32_t)(d) << 24))
+
+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,
+};
+
+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;
+
+ 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.
+} 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);
+
+#ifdef __cplusplus
+} // extern "C"
+} // namespace libyuv
+#endif
+*/
+import "C"
+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
+ ch := (dh + 1) / 2
+ i0 := dw * dh
+ i1 := i0 + cw*ch
+ yStride := dw
+ cStride := cw
+
+ 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) {
+ srcWidthUV, dstWidthUV := (w+1)>>1, (dw+1)>>1
+ srcHeightUV, dstHeightUV := (h+1)>>1, (dh+1)>>1
+
+ srcYPlaneSize, dstYPlaneSize := w*h, dw*dh
+ srcUVPlaneSize, dstUVPlaneSize := srcWidthUV*srcHeightUV, dstWidthUV*dstHeightUV
+
+ srcStrideY, dstStrideY := w, dw
+ srcStrideU, dstStrideU := srcWidthUV, dstWidthUV
+ srcStrideV, dstStrideV := srcWidthUV, dstWidthUV
+
+ srcY := (*C.uchar)(&src[0])
+ srcU := (*C.uchar)(&src[srcYPlaneSize])
+ srcV := (*C.uchar)(&src[srcYPlaneSize+srcUVPlaneSize])
+
+ dstY := (*C.uchar)(&dst[0])
+ dstU := (*C.uchar)(&dst[dstYPlaneSize])
+ dstV := (*C.uchar)(&dst[dstYPlaneSize+dstUVPlaneSize])
+
+ C.I420Scale(
+ srcY,
+ C.int(srcStrideY),
+ srcU,
+ C.int(srcStrideU),
+ srcV,
+ C.int(srcStrideV),
+ C.int(w),
+ C.int(h),
+ dstY,
+ C.int(dstStrideY),
+ dstU,
+ C.int(dstStrideU),
+ dstV,
+ C.int(dstStrideV),
+ C.int(dw),
+ C.int(dh),
+ C.enum_FilterMode(C.kFilterNone))
+}
+
+func Version() string { return fmt.Sprintf("%v", int(C.LIBYUV_VERSION)) }
diff --git a/pkg/encoder/yuv/libyuv/libyuv_darwin_arm64.a b/pkg/encoder/yuv/libyuv/libyuv_darwin_arm64.a
new file mode 100644
index 00000000..f399a41c
Binary files /dev/null and b/pkg/encoder/yuv/libyuv/libyuv_darwin_arm64.a differ
diff --git a/pkg/encoder/yuv/libyuv/libyuv_darwin_x86_64.a b/pkg/encoder/yuv/libyuv/libyuv_darwin_x86_64.a
new file mode 100644
index 00000000..63cd5c74
Binary files /dev/null and b/pkg/encoder/yuv/libyuv/libyuv_darwin_x86_64.a differ
diff --git a/pkg/encoder/yuv/options.go b/pkg/encoder/yuv/options.go
deleted file mode 100644
index 1f53ecfa..00000000
--- a/pkg/encoder/yuv/options.go
+++ /dev/null
@@ -1,42 +0,0 @@
-package yuv
-
-type Options struct {
- ChromaP ChromaPos
- Threaded bool
- Threads int
-}
-
-func (o *Options) override(options ...Option) {
- for _, opt := range options {
- opt(o)
- }
-}
-
-type Option func(*Options)
-
-func Threaded(t bool) Option {
- return func(opts *Options) {
- opts.Threaded = t
- }
-}
-
-func Threads(t int) Option {
- return func(opts *Options) {
- opts.Threads = t
- }
-}
-
-func ChromaP(cp ChromaPos) Option {
- return func(opts *Options) {
- opts.ChromaP = cp
- }
-}
-
-// WithOptions used for config files.
-func WithOptions(arg Options) Option {
- return func(args *Options) {
- args.ChromaP = arg.ChromaP
- args.Threaded = arg.Threaded
- args.Threads = arg.Threads
- }
-}
diff --git a/pkg/encoder/yuv/yuv.c b/pkg/encoder/yuv/yuv.c
deleted file mode 100644
index d6ccb2ce..00000000
--- a/pkg/encoder/yuv/yuv.c
+++ /dev/null
@@ -1,143 +0,0 @@
-#include "yuv.h"
-
-// based on: https://stackoverflow.com/questions/9465815/rgb-to-yuv420-algorithm-efficiency
-
-// Converts RGBA image to YUV (I420) with BT.601 studio color range.
-void rgbaToYuv(void *destination, void *source, int width, int height, chromaPos chroma) {
- const int image_size = width * height;
- unsigned char *rgba = source;
- unsigned char *dst_y = destination;
- unsigned char *dst_u = destination + image_size;
- unsigned char *dst_v = destination + image_size + image_size / 4;
-
- int r1, g1, b1, stride;
- // Y plane
- for (int y = 0; y < height; ++y) {
- stride = 4 * y * width;
- for (int x = 0; x < width; ++x) {
- r1 = 4 * x + stride;
- g1 = r1 + 1;
- b1 = g1 + 1;
- *dst_y++ = ((66 * rgba[r1] + 129 * rgba[g1] + 25 * rgba[b1]) >> 8) + 16;
- }
- }
-
- // U+V plane
- if (chroma == TOP_LEFT) {
- for (int y = 0; y < height; y += 2) {
- stride = 4 * y * width;
- for (int x = 0; x < width; x += 2) {
- r1 = 4 * x + stride;
- g1 = r1 + 1;
- b1 = g1 + 1;
- *dst_u++ = ((-38 * rgba[r1] + -74 * rgba[g1] + 112 * rgba[b1]) >> 8) + 128;
- *dst_v++ = ((112 * rgba[r1] + -94 * rgba[g1] + -18 * rgba[b1]) >> 8) + 128;
- }
- }
- } else if (chroma == BETWEEN_FOUR) {
- int r2, g2, b2, r3, g3, b3, r4, g4, b4;
-
- for (int y = 0; y < height; y += 2) {
- stride = 4 * y * width;
- for (int x = 0; x < width; x += 2) {
- // (1 2) x x
- // x x x x
- r1 = 4 * x + stride;
- g1 = r1 + 1;
- b1 = g1 + 1;
- r2 = r1 + 4;
- g2 = r2 + 1;
- b2 = g2 + 1;
- // x x x x
- // (3 4) x x
- r3 = r1 + 4 * width;
-
- g3 = r3 + 1;
- b3 = g3 + 1;
- r4 = r3 + 4;
- g4 = r4 + 1;
- b4 = g4 + 1;
- *dst_u++ = (((-38 * rgba[r1] + -74 * rgba[g1] + 112 * rgba[b1]) >> 8) +
- ((-38 * rgba[r2] + -74 * rgba[g2] + 112 * rgba[b2]) >> 8) +
- ((-38 * rgba[r3] + -74 * rgba[g3] + 112 * rgba[b3]) >> 8) +
- ((-38 * rgba[r4] + -74 * rgba[g4] + 112 * rgba[b4]) >> 8) + 512) >> 2;
- *dst_v++ = (((112 * rgba[r1] + -94 * rgba[g1] + -18 * rgba[b1]) >> 8) +
- ((112 * rgba[r2] + -94 * rgba[g2] + -18 * rgba[b2]) >> 8) +
- ((112 * rgba[r3] + -94 * rgba[g3] + -18 * rgba[b3]) >> 8) +
- ((112 * rgba[r4] + -94 * rgba[g4] + -18 * rgba[b4]) >> 8) + 512) >> 2;
- }
- }
- }
-}
-
-void chroma(void *destination, void *source, int pos, int deu, int dev, int width, int height, chromaPos chroma) {
- unsigned char *rgba = source + 4 * pos;
- unsigned char *dst_u = destination + deu + pos / 4;
- unsigned char *dst_v = destination + dev + pos / 4;
-
- int r1, g1, b1, stride;
-
- // U+V plane
- if (chroma == TOP_LEFT) {
- for (int y = 0; y < height; y += 2) {
- stride = 4 * y * width;
- for (int x = 0; x < width; x += 2) {
- r1 = 4 * x + stride;
- g1 = r1 + 1;
- b1 = g1 + 1;
- *dst_u++ = ((-38 * rgba[r1] + -74 * rgba[g1] + 112 * rgba[b1]) >> 8) + 128;
- *dst_v++ = ((112 * rgba[r1] + -94 * rgba[g1] + -18 * rgba[b1]) >> 8) + 128;
- }
- }
- } else if (chroma == BETWEEN_FOUR) {
- int r2, g2, b2, r3, g3, b3, r4, g4, b4;
-
- for (int y = 0; y < height; y += 2) {
- stride = 4 * y * width;
- for (int x = 0; x < width; x += 2) {
- // (1 2) x x
- // x x x x
- r1 = 4 * x + stride;
- g1 = r1 + 1;
- b1 = g1 + 1;
- r2 = r1 + 4;
- g2 = r2 + 1;
- b2 = g2 + 1;
- // x x x x
- // (3 4) x x
- r3 = r1 + 4 * width;
- g3 = r3 + 1;
- b3 = g3 + 1;
- r4 = r3 + 4;
- g4 = r4 + 1;
- b4 = g4 + 1;
- *dst_u++ = (((-38 * rgba[r1] + -74 * rgba[g1] + 112 * rgba[b1]) >> 8) +
- ((-38 * rgba[r2] + -74 * rgba[g2] + 112 * rgba[b2]) >> 8) +
- ((-38 * rgba[r3] + -74 * rgba[g3] + 112 * rgba[b3]) >> 8) +
- ((-38 * rgba[r4] + -74 * rgba[g4] + 112 * rgba[b4]) >> 8) + 512) >> 2;
- *dst_v++ = (((112 * rgba[r1] + -94 * rgba[g1] + -18 * rgba[b1]) >> 8) +
- ((112 * rgba[r2] + -94 * rgba[g2] + -18 * rgba[b2]) >> 8) +
- ((112 * rgba[r3] + -94 * rgba[g3] + -18 * rgba[b3]) >> 8) +
- ((112 * rgba[r4] + -94 * rgba[g4] + -18 * rgba[b4]) >> 8) + 512) >> 2;
- }
- }
- }
-}
-
-void luma(void *destination, void *source, int pos, int width, int height) {
- unsigned char *rgba = source + 4 * pos;
- unsigned char *dst_y = destination + pos;
-
- int x, y, r1, g1, b1, stride;
-
- // Y plane
- for (y = 0; y < height; ++y) {
- stride = 4 * y * width;
- for (x = 0; x < width; ++x) {
- r1 = 4 * x + stride;
- g1 = r1 + 1;
- b1 = g1 + 1;
- *dst_y++ = 16 + ((66 * rgba[r1] + 129 * rgba[g1] + 25 * rgba[b1]) >> 8);
- }
- }
-}
diff --git a/pkg/encoder/yuv/yuv.go b/pkg/encoder/yuv/yuv.go
index 5c453fc7..4718c7c1 100644
--- a/pkg/encoder/yuv/yuv.go
+++ b/pkg/encoder/yuv/yuv.go
@@ -2,137 +2,91 @@ package yuv
import (
"image"
- "runtime"
- "sync"
- "unsafe"
+
+ "github.com/giongto35/cloud-game/v3/pkg/encoder/yuv/libyuv"
)
-/*
-#cgo CFLAGS: -Wall -O3
-#include "yuv.h"
-*/
-import "C"
-
-type ImgProcessor interface {
- Process(rgba *image.RGBA) ImgProcessor
- Get() []byte
+type Conv struct {
+ w, h int
+ sw, sh int
+ scale float64
+ frame []byte
+ frameSc []byte
}
-type processor struct {
- Data []byte
- w, h int
- pos ChromaPos
-
- // cache
- dst unsafe.Pointer
- ww C.int
- chroma C.chromaPos
+type RawFrame struct {
+ Data []byte
+ Stride int
+ W, H int
}
-type threadedProcessor struct {
- *processor
+type PixFmt uint32
- // threading
- threads int
- chunk int
+const FourccRgbp = libyuv.FourccRgbp
+const FourccArgb = libyuv.FourccArgb
+const FourccAbgr = libyuv.FourccAbgr
+const FourccRgb0 = libyuv.FourccRgb0
- // cache
- chromaU C.int
- chromaV C.int
-}
-
-type ChromaPos uint8
-
-const (
- TopLeft ChromaPos = iota
- BetweenFour
-)
-
-// NewYuvImgProcessor creates new YUV image converter from RGBA.
-func NewYuvImgProcessor(w, h int, options ...Option) ImgProcessor {
- opts := &Options{
- ChromaP: BetweenFour,
- Threaded: true,
- Threads: runtime.NumCPU(),
- }
- opts.override(options...)
-
- bufSize := int(float32(w*h) * 1.5)
- buf := make([]byte, bufSize)
-
- processor := processor{
- Data: buf,
- chroma: C.chromaPos(opts.ChromaP),
- dst: unsafe.Pointer(&buf[0]),
- h: h,
- pos: opts.ChromaP,
- w: w,
- ww: C.int(w),
+func NewYuvConv(w, h int, scale float64) Conv {
+ if scale < 1 {
+ scale = 1
}
- if opts.Threaded {
- // chunks the image evenly
- chunk := h / opts.Threads
- if chunk%2 != 0 {
- chunk--
- }
+ sw, sh := round(w, scale), round(h, scale)
+ conv := Conv{w: w, h: h, sw: sw, sh: sh, scale: scale}
+ bufSize := int(float64(w) * float64(h) * 1.5)
- return &threadedProcessor{
- chromaU: C.int(w * h),
- chromaV: C.int(w*h + w*h/4),
- chunk: chunk,
- processor: &processor,
- threads: opts.Threads,
- }
+ 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 &processor
+
+ return conv
}
-func (yuv *processor) Get() []byte {
- return yuv.Data
+// Process converts an image to YUV I420 format inside the internal buffer.
+func (c *Conv) Process(frame RawFrame, rot uint, pf PixFmt) []byte {
+ cx, cy := c.w, c.h // crop
+ if rot == 90 || rot == 270 {
+ cx, cy = cy, cx
+ }
+
+ var stride int
+ switch pf {
+ case PixFmt(libyuv.FourccRgbp), PixFmt(libyuv.FourccRgb0):
+ stride = frame.Stride >> 1
+ default:
+ stride = frame.Stride >> 2
+ }
+
+ libyuv.Y420(frame.Data, c.frame, frame.W, frame.H, stride, c.w, c.h, rot, uint32(pf), cx, cy)
+
+ if c.scale > 1 {
+ libyuv.Y420Scale(c.frame, c.frameSc, c.w, c.h, c.sw, c.sh)
+ return c.frameSc
+ }
+
+ return c.frame
}
-// Process converts RGBA colorspace into YUV I420 format inside the internal buffer.
-// Non-threaded version.
-func (yuv *processor) Process(rgba *image.RGBA) ImgProcessor {
- C.rgbaToYuv(yuv.dst, unsafe.Pointer(&rgba.Pix[0]), yuv.ww, C.int(yuv.h), yuv.chroma)
- return yuv
-}
-
-func (yuv *threadedProcessor) Get() []byte {
- return yuv.Data
-}
-
-// Process converts RGBA colorspace into YUV I420 format inside the internal buffer.
-// Threaded version.
-//
-// We divide the input image into chunks by the number of available CPUs.
-// Each chunk should contain 2, 4, 6, and etc. rows of the image.
-//
-// 8x4 CPU (2)
-// x x x x x x x x | Coroutine 1
-// x x x x x x x x | Coroutine 1
-// x x x x x x x x | Coroutine 2
-// x x x x x x x x | Coroutine 2
-//
-func (yuv *threadedProcessor) Process(rgba *image.RGBA) ImgProcessor {
- src := unsafe.Pointer(&rgba.Pix[0])
- wg := sync.WaitGroup{}
- wg.Add(2 * yuv.threads)
- for i := 0; i < yuv.threads; i++ {
- pos, hh := C.int(yuv.w*i*yuv.chunk), C.int(yuv.chunk)
- // we need to know how many pixels left
- // if the image can't be divided evenly
- // between all the threads
- if i == yuv.threads-1 {
- hh = C.int(yuv.h - i*yuv.chunk)
- }
- go func() { defer wg.Done(); C.luma(yuv.dst, src, pos, yuv.ww, hh) }()
- go func() {
- defer wg.Done()
- C.chroma(yuv.dst, src, pos, yuv.chromaU, yuv.chromaV, yuv.ww, hh, yuv.chroma)
- }()
- }
- wg.Wait()
+func (c *Conv) Version() string { return libyuv.Version() }
+func round(x int, scale float64) int { return (int(float64(x)*scale) + 1) & ^1 }
+
+func ToYCbCr(bytes []byte, w, h int) *image.YCbCr {
+ cw, ch := (w+1)/2, (h+1)/2
+
+ i0 := w*h + 0*cw*ch
+ i1 := w*h + 1*cw*ch
+ i2 := w*h + 2*cw*ch
+
+ yuv := image.NewYCbCr(image.Rect(0, 0, w, h), image.YCbCrSubsampleRatio420)
+ yuv.Y = bytes[:i0:i0]
+ yuv.Cb = bytes[i0:i1:i1]
+ yuv.Cr = bytes[i1:i2:i2]
return yuv
}
diff --git a/pkg/encoder/yuv/yuv.h b/pkg/encoder/yuv/yuv.h
deleted file mode 100644
index 931b857e..00000000
--- a/pkg/encoder/yuv/yuv.h
+++ /dev/null
@@ -1,24 +0,0 @@
-
-typedef enum {
- // It will take each TL pixel for chroma values.
- // XO X XO X
- // X X X X
- TOP_LEFT = 0,
- // It will take an average color from the 2x2 pixel group for chroma values.
- // X X X X
- // O O
- // X X X X
- BETWEEN_FOUR = 1
-} chromaPos;
-
-// Converts RGBA image to YUV (I420) with BT.601 studio color range.
-void rgbaToYuv(void *destination, void *source, int width, int height, chromaPos chroma);
-
-// Converts RGBA image chunk to YUV (I420) chroma with BT.601 studio color range.
-// pos contains a shift value for chunks.
-// deu, dev contains constant shifts for U, V planes in the resulting array.
-// chroma (0, 1) selects chroma estimation algorithm.
-void chroma(void *destination, void *source, int pos, int deu, int dev, int width, int height, chromaPos chroma);
-
-// Converts RGBA image chunk to YUV (I420) luma with BT.601 studio color range.
-void luma(void *destination, void *source, int pos, int width, int height);
diff --git a/pkg/encoder/yuv/yuv_test.go b/pkg/encoder/yuv/yuv_test.go
index 977e3a2b..4e0ebbf7 100644
--- a/pkg/encoder/yuv/yuv_test.go
+++ b/pkg/encoder/yuv/yuv_test.go
@@ -1,202 +1,284 @@
package yuv
import (
+ "archive/zip"
+ "fmt"
"image"
"image/color"
- "math/rand"
- "reflect"
+ "image/png"
+ "io"
+ "math"
+ "math/rand/v2"
+ "os"
+ "path/filepath"
"testing"
- "time"
+
+ "github.com/giongto35/cloud-game/v3/pkg/encoder/yuv/libyuv"
+ _ "github.com/giongto35/cloud-game/v3/test"
)
-func TestYuv(t *testing.T) {
- size1, size2 := 32, 32
- for i := 1; i < 100; i++ {
- img := generateImage(size1, size2, randomColor())
- pc := NewYuvImgProcessor(size1, size2, Threaded(false))
- pct := NewYuvImgProcessor(size1, size2, Threaded(true))
-
- pc.Process(img)
- pct.Process(img)
-
- if !reflect.DeepEqual(pc.Get(), pct.Get()) {
- t.Fatalf("couldn't convert %v, \n %v \n %v", img.Pix, pc.Get(), pct.Get())
- }
- }
-}
-
func TestYuvPredefined(t *testing.T) {
+ t.Skip("Skipped because on Windows some colors are different")
im := []uint8{101, 0, 106, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 101, 0, 106, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 101, 0, 106, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 101, 0, 106, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 101, 0, 106, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 101, 0, 106, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 101, 0, 106, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 101, 0, 106, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 101, 0, 106, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 101, 0, 106, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 101, 0, 106, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 101, 0, 106, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 101, 0, 106, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 101, 0, 106, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 101, 0, 106, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 101, 0, 106, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 101, 0, 106, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255, 18, 226, 78, 255}
should := []byte{
- 52, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 52, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 52, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 52, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 52, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 52, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 52, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 52, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 52, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 52,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 52, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 52, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 52, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 52, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 52, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 52, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 52, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
- 142, 142, 142, 142, 126, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94,
- 126, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 126, 94, 94, 94, 94,
- 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 126, 94, 94, 94, 94, 94, 94, 94, 94, 94,
- 94, 94, 94, 94, 94, 94, 126, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94,
- 94, 126, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 126, 94, 94, 94,
- 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 126, 94, 94, 94, 94, 94, 94, 94, 94,
- 94, 94, 94, 94, 94, 94, 94, 110, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94,
- 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94,
- 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94,
- 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94,
- 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94,
- 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94,
- 94, 94, 94, 94, 94, 94, 94, 94, 94, 105, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47,
- 47, 47, 47, 47, 105, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 105,
- 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 105, 47, 47, 47, 47, 47,
- 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 105, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47,
- 47, 47, 47, 47, 47, 105, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47,
- 105, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 105, 47, 47, 47, 47,
- 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 76, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47,
- 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47,
- 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47,
- 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47,
- 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47,
- 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47,
- 47, 47, 47, 47, 47, 47, 47,
+ 52, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 52, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 52, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 52, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 52, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 52, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 52, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 52, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 52, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 52, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 52, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 52, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 52, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 52, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 52, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 52, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 52, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 52, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 52, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 52, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 52, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 52, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 52, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 52, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 52, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 52, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 52, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 52, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 52, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 52, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 52, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 52, 142, 142, 142, 142, 142, 142, 142,
+ 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142, 142,
+ 110, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 110, 94, 94, 94, 94, 94, 94, 94,
+ 110, 94, 94, 94, 110, 94, 94, 94, 94, 94, 94, 94, 110, 94, 94, 94, 94, 94, 94, 94,
+ 94, 94, 94, 94, 110, 94, 94, 94, 94, 94, 94, 94, 110, 94, 94, 94, 110, 94, 94, 94,
+ 94, 94, 94, 94, 110, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 110, 94, 94, 94,
+ 94, 94, 94, 94, 110, 94, 94, 94, 110, 94, 94, 94, 94, 94, 94, 94, 110, 94, 94, 94,
+ 94, 94, 94, 94, 94, 94, 94, 94, 110, 94, 94, 94, 94, 94, 94, 94, 110, 94, 94, 94,
+ 110, 94, 94, 94, 94, 94, 94, 94, 110, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94,
+ 110, 94, 94, 94, 94, 94, 94, 94, 110, 94, 94, 94, 110, 94, 94, 94, 94, 94, 94, 94,
+ 110, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 110, 94, 94, 94, 94, 94, 94, 94,
+ 110, 94, 94, 94, 110, 94, 94, 94, 94, 94, 94, 94, 110, 94, 94, 94, 94, 94, 94, 94,
+ 94, 94, 94, 94, 110, 94, 94, 94, 94, 94, 94, 94, 110, 94, 94, 94, 110, 94, 94, 94,
+ 94, 94, 94, 94, 110, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 94, 110, 94, 94, 94,
+ 94, 94, 94, 94, 110, 94, 94, 94, 110, 94, 94, 94, 94, 94, 94, 94, 76, 47, 47, 47,
+ 47, 47, 47, 47, 47, 47, 47, 47, 76, 47, 47, 47, 47, 47, 47, 47, 76, 47, 47, 47, 76,
+ 47, 47, 47, 47, 47, 47, 47, 76, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 76, 47,
+ 47, 47, 47, 47, 47, 47, 76, 47, 47, 47, 76, 47, 47, 47, 47, 47, 47, 47, 76, 47, 47,
+ 47, 47, 47, 47, 47, 47, 47, 47, 47, 76, 47, 47, 47, 47, 47, 47, 47, 76, 47, 47, 47,
+ 76, 47, 47, 47, 47, 47, 47, 47, 76, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 76,
+ 47, 47, 47, 47, 47, 47, 47, 76, 47, 47, 47, 76, 47, 47, 47, 47, 47, 47, 47, 76, 47,
+ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 76, 47, 47, 47, 47, 47, 47, 47, 76, 47, 47,
+ 47, 76, 47, 47, 47, 47, 47, 47, 47, 76, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47,
+ 76, 47, 47, 47, 47, 47, 47, 47, 76, 47, 47, 47, 76, 47, 47, 47, 47, 47, 47, 47, 76,
+ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 76, 47, 47, 47, 47, 47, 47, 47, 76, 47,
+ 47, 47, 76, 47, 47, 47, 47, 47, 47, 47, 76, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47,
+ 47, 76, 47, 47, 47, 47, 47, 47, 47, 76, 47, 47, 47, 76, 47, 47, 47, 47, 47, 47, 47,
}
- pc := NewYuvImgProcessor(32, 32, Threaded(false))
- pct := NewYuvImgProcessor(32, 32, Threaded(true))
+ pc := NewYuvConv(32, 32, 1)
+ frame := RawFrame{Data: im, Stride: 32, W: 32, H: 32}
+ a := pc.Process(frame, 0, PixFmt(libyuv.FourccAbgr))
- img := image.NewRGBA(image.Rect(0, 0, 32, 32))
- img.Pix = im
+ v := libyuv.Version()
+ t.Logf("%v", v)
- pc.Process(img)
- pct.Process(img)
-
- if !reflect.DeepEqual(pc.Get(), should) {
- t.Fatalf("couldn't convert with base %v \n %v", pc.Get(), should)
+ if len(a) != len(should) {
+ t.Fatalf("different size a: %v, o: %v", len(a), len(should))
}
- if !reflect.DeepEqual(pct.Get(), should) {
- for i := 0; i < len(pct.Get()); i++ {
- if pct.Get()[i] != should[i] {
- t.Logf("index is: %v", i)
+ for i := range a {
+ if a[i] != should[i] {
+ t.Fatalf("diff in %vth, %v != %v \n%v\n%v", i, a[i], should[i], im, should)
+ }
+ }
+}
+
+func TestYuvScale(t *testing.T) {
+ name := "001_alsa_ABGR_256_240_1024.raw"
+ path := filepath.Join("./test/testdata/raw/", name)
+
+ data, err := ReadZip(path + ".zip")
+ if err != nil {
+ t.Error(err)
+ }
+
+ pf, w, h, stride := PixFmt(libyuv.FourccArgb), 256, 240, 1024
+ scale := 2
+
+ conv := NewYuvConv(w, h, float64(scale))
+ frame := RawFrame{Data: data, Stride: stride, W: w, H: h}
+ out := conv.Process(frame, 0, pf)
+
+ d := float64(len(out)) / float64(len(data))
+ if d != 1.5 {
+ t.Errorf("Scaled not by factor %v, %v", scale, d)
+ }
+
+ // save as RGBA
+ //sw, sh := w*scale, h*scale
+ //yuv := ToYCbCr(out, sw, sh)
+ //if f, err := os.Create(filepath.Join("./", name+".png")); err == nil {
+ // if err = png.Encode(f, yuv); err != nil {
+ // t.Logf("Couldn't encode the image, %v", err)
+ // }
+ // _ = f.Close()
+ //}
+}
+
+func BenchmarkYuv(b *testing.B) {
+ tests := []struct {
+ w int
+ h int
+ }{
+ {w: 1920, h: 1080},
+ {w: 320, h: 240},
+ }
+ r1 := rand.Float32()
+
+ for _, test := range tests {
+ w, h := test.w, test.h
+ frame := genFrame(w, h, r1)
+ b.Run(fmt.Sprintf("%vx%v YUV", w, h), func(b *testing.B) {
+ pc := NewYuvConv(w, h, 1)
+ for i := 0; i < b.N; i++ {
+ pc.Process(frame, 0, PixFmt(libyuv.FourccAbgr))
+ b.SetBytes(int64(len(frame.Data)))
}
- }
- t.Fatalf("couldn't convert with threaded %v \n %v", pct.Get(), should)
+ b.ReportAllocs()
+ })
}
}
-func generateImage(w, h int, pixelColor color.RGBA) *image.RGBA {
- img := image.NewRGBA(image.Rect(0, 0, w, h))
- for x := 0; x < w; x++ {
- for y := 0; y < h; y++ {
- img.Set(x, y, randomColor())
- }
- }
- return img
-}
-
-func randomColor() color.RGBA {
- rnd := rand.New(rand.NewSource(time.Now().Unix()))
- return color.RGBA{
- R: uint8(rnd.Intn(255)),
- G: uint8(rnd.Intn(255)),
- B: uint8(rnd.Intn(255)),
- A: 255,
- }
-}
-
-func BenchmarkTopLeft(b *testing.B) {
- benchmarkConverter(1920, 1080, 0, true, b)
-}
-
-func BenchmarkBetweenFour(b *testing.B) {
- benchmarkConverter(1920, 1080, 1, true, b)
-}
-
-func BenchmarkBetweenFourNonThreaded(b *testing.B) {
- benchmarkConverter(1920, 1080, 1, false, b)
-}
-
-func benchmarkConverter(w, h int, chroma ChromaPos, threaded bool, b *testing.B) {
- b.StopTimer()
-
- pc := NewYuvImgProcessor(w, h, ChromaP(chroma), Threaded(threaded))
-
- image1 := genTestImage(w, h, rand.New(rand.NewSource(int64(1))).Float32())
- image2 := genTestImage(w, h, rand.New(rand.NewSource(int64(2))).Float32())
-
- for i := 0; i < b.N; i++ {
- im := image1
- if i%2 == 0 {
- im = image2
- }
- b.StartTimer()
- pc.Process(im)
- b.StopTimer()
- b.SetBytes(int64(len(im.Pix)))
- }
-}
-
-func genTestImage(w, h int, seed float32) *image.RGBA {
+func genFrame(w, h int, seed float32) RawFrame {
img := image.NewRGBA(image.Rectangle{Max: image.Point{X: w, Y: h}})
- for x := 0; x < w; x++ {
- for y := 0; y < h; y++ {
+ for x := range w {
+ for y := range h {
col := color.RGBA{R: uint8(seed * 255), G: uint8(seed * 255), B: uint8(seed * 255), A: 0xff}
img.Set(x, y, col)
}
}
- return img
+ return RawFrame{
+ Data: img.Pix,
+ Stride: img.Stride,
+ W: img.Bounds().Dx(),
+ H: img.Bounds().Dy(),
+ }
+}
+
+func TestGen24bitFull(t *testing.T) {
+ t.Skip()
+ const tau = 2 * math.Pi
+ const deg = 3 * math.Pi / 2
+ //const depth = 1 << 24
+ var wh = 255 //int(math.Sqrt(depth))
+
+ img := image.NewRGBA(image.Rectangle{Max: image.Point{X: wh, Y: wh}})
+
+ centerX, centerY := wh/2, wh/2
+ radius := centerX
+ //if centerY < radius {
+ // radius = centerY
+ //}
+
+ for y := range wh {
+ dy := float64(y - centerY)
+ for x := range wh {
+ dx := float64(x - centerX)
+ dist := math.Sqrt(dx*dx + dy*dy)
+ if dist <= float64(radius) {
+ hue := (math.Atan2(dx, dy) + deg) / tau
+ r, g, b := hsb2rgb(hue, linear(0, float64(centerX), dist), 1)
+ img.Set(x, y, color.RGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: 255})
+ }
+ }
+ }
+
+ f, _ := os.Create("out_image.png")
+ defer func() { _ = f.Close() }()
+
+ // Encode to `PNG` with `DefaultCompression` level
+ // then save to file
+ _ = png.Encode(f, img)
+}
+
+func linear(a, b, x float64) float64 { return (x - a) / (b - a) }
+
+func hsb2rgb(hue, s, bri float64) (r, g, b int) {
+ u := int(bri*255 + 0.5)
+ if s == 0 {
+ return u, u, u
+ }
+ h := (hue - math.Floor(hue)) * 6
+ f := h - math.Floor(h)
+ p := int(bri*(1-s)*255 + 0.5)
+ q := int(bri*(1-s*f)*255 + 0.5)
+ t := int(bri*(1-s*(1-f))*255 + 0.5)
+ switch int(h) {
+ case 0:
+ r, g, b = u, t, p
+ case 1:
+ r, g, b = q, u, p
+ case 2:
+ r, g, b = p, u, t
+ case 3:
+ r, g, b = p, q, u
+ case 4:
+ r, g, b = t, p, u
+ case 5:
+ r, g, b = u, p, q
+ }
+ return
+}
+
+func ReadZip(path string) ([]byte, error) {
+ zf, err := zip.OpenReader(path)
+ if err != nil {
+ return nil, err
+ }
+ defer func() { _ = zf.Close() }()
+
+ f, err := zf.File[0].Open()
+ if err != nil {
+ return nil, err
+ }
+ defer func() { _ = f.Close() }()
+
+ return io.ReadAll(f)
}
diff --git a/pkg/environment/env.go b/pkg/environment/env.go
deleted file mode 100644
index 57a443cc..00000000
--- a/pkg/environment/env.go
+++ /dev/null
@@ -1,28 +0,0 @@
-package environment
-
-import "os/user"
-
-type Env string
-
-const (
- Dev Env = "dev"
- Staging Env = "staging"
- Production Env = "prod"
-)
-
-func (env *Env) AnyOf(what ...Env) bool {
- for _, cur := range what {
- if *env == cur {
- return true
- }
- }
- return false
-}
-
-func GetUserHome() (string, error) {
- me, err := user.Current()
- if err != nil {
- return "", err
- }
- return me.HomeDir, nil
-}
diff --git a/pkg/extractor/extractor.go b/pkg/extractor/extractor.go
deleted file mode 100644
index 686d776f..00000000
--- a/pkg/extractor/extractor.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package extractor
-
-import (
- "path/filepath"
-
- "github.com/giongto35/cloud-game/v2/pkg/extractor/zip"
-)
-
-type Extractor interface {
- Extract(src string, dest string) ([]string, error)
-}
-
-const (
- zipExt = ".zip"
-)
-
-func NewFromExt(path string) Extractor {
- switch filepath.Ext(path) {
- case zipExt:
- return zip.New()
- default:
- return nil
- }
-}
diff --git a/pkg/extractor/zip/extractor.go b/pkg/extractor/zip/extractor.go
deleted file mode 100644
index f9fee462..00000000
--- a/pkg/extractor/zip/extractor.go
+++ /dev/null
@@ -1,67 +0,0 @@
-package zip
-
-import (
- "archive/zip"
- "io"
- "log"
- "os"
- "path/filepath"
- "strings"
-)
-
-type Extractor struct{}
-
-func New() Extractor { return Extractor{} }
-
-func (e Extractor) Extract(src string, dest string) (files []string, err error) {
- r, err := zip.OpenReader(src)
- if err != nil {
- return files, err
- }
- defer r.Close()
-
- for _, f := range r.File {
- path := filepath.Join(dest, f.Name)
-
- // negate ZipSlip vulnerability (http://bit.ly/2MsjAWE)
- if !strings.HasPrefix(path, filepath.Clean(dest)+string(os.PathSeparator)) {
- log.Printf("warning: %s is illegal path", path)
- continue
- }
- // remake directory
- if f.FileInfo().IsDir() {
- if err := os.MkdirAll(path, os.ModePerm); err != nil {
- log.Printf("error: %v", err)
- }
- continue
- }
- // make file
- if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
- log.Printf("error: %v", err)
- continue
- }
- out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
- if err != nil {
- log.Printf("error: %v", err)
- continue
- }
- rc, err := f.Open()
- if err != nil {
- log.Printf("error: %v", err)
- continue
- }
-
- if _, err = io.Copy(out, rc); err != nil {
- log.Printf("error: %v", err)
- _ = out.Close()
- _ = rc.Close()
- continue
- }
-
- _ = out.Close()
- _ = rc.Close()
-
- files = append(files, path)
- }
- return files, nil
-}
diff --git a/pkg/games/game_library.go b/pkg/games/game_library.go
deleted file mode 100644
index dfd06228..00000000
--- a/pkg/games/game_library.go
+++ /dev/null
@@ -1,301 +0,0 @@
-package games
-
-import (
- "crypto/md5"
- "fmt"
- "io"
- "log"
- "os"
- "path/filepath"
- "strings"
- "sync"
- "time"
-
- "github.com/fsnotify/fsnotify"
-)
-
-// Config is an external configuration
-type Config struct {
- // some directory which is going to be
- // the root folder for the library
- BasePath string
- // a list of supported file extensions
- Supported []string
- // a list of ignored words in the files
- Ignored []string
- // print some additional info
- Verbose bool
- // enable directory changes watch
- WatchMode bool
-}
-
-// libConf is an optimized internal library configuration
-type libConf struct {
- path string
- supported map[string]bool
- ignored map[string]bool
- verbose bool
- watchMode bool
-}
-
-type library struct {
- config libConf
- // indicates repo source existence
- hasSource bool
- // scan time
- lastScanDuration time.Duration
- // library entries
- // !should be a tree-based structure
- // game name -> game meta
- // games with duplicate names are merged
- games map[string]GameMetadata
-
- // to restrict parallel execution
- // or throttling
- // !CAS would be better
- mu sync.Mutex
- isScanning bool
- isScanningDelayed bool
-}
-
-type GameLibrary interface {
- GetAll() []GameMetadata
- FindGameByName(name string) GameMetadata
- Scan()
-}
-
-type FileExtensionWhitelist interface {
- GetSupportedExtensions() []string
-}
-
-type GameMetadata struct {
- uid string
- // the display name of the game
- Name string
- // the game file extension (e.g. nes, n64)
- Type string
- Base string
- // the game path relative to the library base path
- Path string
-}
-
-func (c Config) GetSupportedExtensions() []string { return c.Supported }
-
-func NewLib(conf Config) GameLibrary { return NewLibWhitelisted(conf, conf) }
-
-func NewLibWhitelisted(conf Config, filter FileExtensionWhitelist) GameLibrary {
- hasSource := true
- dir, err := filepath.Abs(conf.BasePath)
- if err != nil {
- hasSource = false
- log.Printf("[lib] invalid source: %v (%v)\n", conf.BasePath, err)
- }
-
- if len(conf.Supported) == 0 {
- conf.Supported = filter.GetSupportedExtensions()
- }
-
- library := &library{
- config: libConf{
- path: dir,
- supported: toMap(conf.Supported),
- ignored: toMap(conf.Ignored),
- verbose: conf.Verbose,
- watchMode: conf.WatchMode,
- },
- mu: sync.Mutex{},
- games: map[string]GameMetadata{},
- hasSource: hasSource,
- }
-
- if conf.WatchMode && hasSource {
- go library.watch()
- }
-
- return library
-}
-
-func (lib *library) GetAll() []GameMetadata {
- var res []GameMetadata
- for _, value := range lib.games {
- res = append(res, value)
- }
- return res
-}
-
-// FindGameByName returns some game info with its full filepath
-func (lib *library) FindGameByName(name string) GameMetadata {
- var game GameMetadata
- if val, ok := lib.games[name]; ok {
- val.Base = lib.config.path
- return val
- }
- return game
-}
-
-func (lib *library) Scan() {
- if !lib.hasSource {
- log.Printf("[lib] scan... skipped (no source)\n")
- return
- }
-
- // scan throttling
- lib.mu.Lock()
- if lib.isScanning {
- defer lib.mu.Unlock()
- lib.isScanningDelayed = true
- log.Printf("[lib] scan... delayed\n")
- return
- }
- lib.isScanning = true
- lib.mu.Unlock()
-
- log.Printf("[lib] scan... started\n")
-
- start := time.Now()
- var games []GameMetadata
- dir := lib.config.path
- err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
- if err != nil {
- return err
- }
-
- if info != nil && !info.IsDir() && lib.isFileExtensionSupported(path) {
- meta := getMetadata(path, dir)
- meta.uid = hash(path)
-
- if !lib.config.ignored[meta.Name] {
- games = append(games, meta)
- }
- }
- return nil
- })
-
- if err != nil {
- log.Printf("[lib] scan error with %q: %v\n", dir, err)
- }
-
- if len(games) > 0 {
- lib.set(games)
- }
-
- lib.lastScanDuration = time.Since(start)
- if lib.config.verbose {
- lib.dumpLibrary()
- }
-
- // run scan again if delayed
- lib.mu.Lock()
- defer lib.mu.Unlock()
- lib.isScanning = false
- if lib.isScanningDelayed {
- lib.isScanningDelayed = false
- go lib.Scan()
- }
-
- log.Printf("[lib] scan... completed\n")
-}
-
-// watch adds the ability to rescan the entire library
-// during filesystem changes in a watched directory.
-// !to add incremental library change
-func (lib *library) watch() {
- watcher, err := fsnotify.NewWatcher()
- if err != nil {
- log.Printf("[lib] watcher has failed: %v", err)
- return
- }
-
- done := make(chan bool)
- go func(repo *library) {
- for {
- select {
- case event, ok := <-watcher.Events:
- if !ok {
- return
- }
- if event.Op == fsnotify.Create || event.Op == fsnotify.Remove {
- // !to try to add the proper file/dir add/remove scan logic
- // which is tricky
- repo.Scan()
- }
- case _, ok := <-watcher.Errors:
- if !ok {
- return
- }
- }
- }
- }(lib)
-
- if err = watcher.Add(lib.config.path); err != nil {
- log.Printf("[lib] watch error %v", err)
- }
- <-done
- _ = watcher.Close()
- log.Printf("[lib] the watch has ended\n")
-}
-
-func (lib *library) set(games []GameMetadata) {
- res := make(map[string]GameMetadata)
- for _, value := range games {
- res[value.Name] = value
- }
- lib.games = res
-}
-
-func (lib *library) isFileExtensionSupported(path string) bool {
- ext := filepath.Ext(path)
- if ext == "" {
- return false
- }
- return lib.config.supported[ext[1:]]
-}
-
-// getMetadata returns game info from a path
-func getMetadata(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:],
- Path: relPath,
- }
-}
-
-// dumpLibrary printouts the current library snapshot of games
-func (lib *library) dumpLibrary() {
- var gameList strings.Builder
- for _, game := range lib.games {
- gameList.WriteString(" " + game.Name + " (" + game.Path + ")" + "\n")
- }
-
- log.Printf("\n"+
- "--------------------------------------------\n"+
- "--- The Library of ROMs ---\n"+
- "--------------------------------------------\n"+
- "%v"+
- "--------------------------------------------\n"+
- "--- ROMs: %03d %26s ---\n"+
- "--------------------------------------------\n",
- gameList.String(), len(lib.games), lib.lastScanDuration)
-}
-
-// hash makes an MD5 hash of the string
-func hash(str string) string {
- h := md5.New()
- _, err := io.WriteString(h, str)
- if err != nil {
- return ""
- }
- return fmt.Sprintf("%x", h.Sum(nil))
-}
-
-func toMap(list []string) map[string]bool {
- res := make(map[string]bool)
- for _, s := range list {
- res[s] = true
- }
- return res
-}
diff --git a/pkg/games/game_library_test.go b/pkg/games/game_library_test.go
deleted file mode 100644
index 638bc277..00000000
--- a/pkg/games/game_library_test.go
+++ /dev/null
@@ -1,57 +0,0 @@
-package games
-
-import (
- "testing"
-)
-
-func TestLibraryScan(t *testing.T) {
- tests := []struct {
- directory string
- expected []string
- }{
- {
- directory: "../../assets/games",
- expected: []string{
- "Super Mario Bros", "Sushi The Cat", "anguna",
- },
- },
- }
-
- for _, test := range tests {
- library := NewLib(Config{
- BasePath: test.directory,
- Supported: []string{"gba", "zip", "nes"},
- Ignored: []string{"neogeo", "pgm"},
- })
- library.Scan()
- games := library.GetAll()
-
- list := _map(games, func(meta GameMetadata) string {
- return meta.Name
- })
-
- // ^2 complexity (;
- all := true
- for _, expect := range test.expected {
- found := false
- for _, game := range list {
- if game == expect {
- found = true
- break
- }
- }
- all = all && found
- }
- if !all {
- t.Errorf("Test fail for dir %v with %v != %v", test.directory, list, test.expected)
- }
- }
-}
-
-func _map(vs []GameMetadata, f func(info GameMetadata) string) []string {
- vsm := make([]string, len(vs))
- for i, v := range vs {
- vsm[i] = f(v)
- }
- return vsm
-}
diff --git a/pkg/games/launcher.go b/pkg/games/launcher.go
new file mode 100644
index 00000000..8850ea7f
--- /dev/null
+++ b/pkg/games/launcher.go
@@ -0,0 +1,64 @@
+package games
+
+import (
+ "fmt"
+ "math/rand/v2"
+ "strconv"
+ "strings"
+)
+
+type Launcher interface {
+ FindAppByName(name string) (AppMeta, error)
+ ExtractAppNameFromUrl(name string) string
+ GetAppNames() []AppMeta
+}
+
+type AppMeta struct {
+ Alias string
+ Base string
+ Name string
+ Path string
+ System string
+ Type string
+}
+
+type GameLauncher struct {
+ lib GameLibrary
+}
+
+func NewGameLauncher(lib GameLibrary) GameLauncher { return GameLauncher{lib: lib} }
+
+func (gl GameLauncher) FindAppByName(name string) (AppMeta, error) {
+ game := gl.lib.FindGameByName(name)
+ if game.Path == "" {
+ return AppMeta{}, fmt.Errorf("couldn't find game info for the game %v", name)
+ }
+ return AppMeta(game), nil
+}
+
+func (gl GameLauncher) ExtractAppNameFromUrl(name string) string { return ExtractGame(name) }
+
+func (gl GameLauncher) GetAppNames() (apps []AppMeta) {
+ for _, game := range gl.lib.GetAll() {
+ apps = append(apps, AppMeta{Alias: game.Alias, Name: game.Name, System: game.System})
+ }
+ return
+}
+
+const separator = "___"
+
+// ExtractGame parses game room link returning the name of the game "encoded" there.
+func ExtractGame(roomID string) string {
+ parts := strings.Split(roomID, separator)
+ if len(parts) > 1 {
+ return parts[1]
+ }
+ return ""
+}
+
+// GenerateRoomID generate a unique room ID containing 16 digits.
+// 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.Int64(), 16) + separator + title
+}
diff --git a/pkg/games/library.go b/pkg/games/library.go
new file mode 100644
index 00000000..5586f464
--- /dev/null
+++ b/pkg/games/library.go
@@ -0,0 +1,389 @@
+package games
+
+import (
+ "bufio"
+ "fmt"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/fsnotify/fsnotify"
+ "github.com/giongto35/cloud-game/v3/pkg/config"
+ "github.com/giongto35/cloud-game/v3/pkg/logger"
+)
+
+// libConf is an optimized internal library configuration
+type libConf struct {
+ aliasFile string
+ path string
+ supported map[string]struct{}
+ ignored []string
+ verbose bool
+ watchMode bool
+ sessionPath string
+}
+
+type library struct {
+ config libConf
+ // indicates repo source existence
+ hasSource bool
+ // scan time
+ lastScanDuration time.Duration
+ // library entries
+ // !should be a tree-based structure
+ // game name -> game meta
+ // games with duplicate names are merged
+ 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
+ // for file watch mode
+ mu sync.Mutex
+ isScanning bool
+ isScanningDelayed bool
+}
+
+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
+ System string
+ Type string // the game file extension (e.g. nes, n64)
+}
+
+func (g GameMetadata) FullPath(base string) string {
+ if base == "" {
+ return filepath.Join(g.Base, g.Path)
+ }
+ return filepath.Join(base, g.Path)
+}
+
+func NewLib(conf config.Library, emu WithEmulatorInfo, log *logger.Logger) GameLibrary {
+ hasSource := true
+ dir, err := filepath.Abs(conf.BasePath)
+ if err != nil {
+ hasSource = false
+ log.Error().Err(err).Str("dir", conf.BasePath).Msg("Lib has invalid source")
+ }
+
+ if len(conf.Supported) == 0 {
+ conf.Supported = emu.GetSupportedExtensions()
+ }
+
+ library := &library{
+ config: libConf{
+ 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{},
+ hasSource: hasSource,
+ log: log,
+ emuConf: emu,
+ }
+
+ if conf.WatchMode && hasSource {
+ go library.watch()
+ }
+
+ return library
+}
+
+func (lib *library) Sessions() []string {
+ return lib.sessions
+}
+
+func (lib *library) GetAll() []GameMetadata {
+ var res []GameMetadata
+ for _, value := range lib.games {
+ res = append(res, value)
+ }
+ return res
+}
+
+// FindGameByName returns some game info with its full filepath
+func (lib *library) FindGameByName(name string) GameMetadata {
+ var game GameMetadata
+ if val, ok := lib.games[name]; ok {
+ val.Base = lib.config.path
+ return val
+ }
+ 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)")
+ return
+ }
+
+ // scan throttling
+ lib.mu.Lock()
+ if lib.isScanning {
+ defer lib.mu.Unlock()
+ lib.isScanningDelayed = true
+ lib.log.Debug().Msg("Lib scan... delayed")
+ return
+ }
+ lib.isScanning = true
+ lib.mu.Unlock()
+
+ 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
+ err := filepath.WalkDir(dir, func(path string, info fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+
+ if info == nil || info.IsDir() || !lib.isExtAllowed(path) {
+ return nil
+ }
+
+ meta := metadata(path, dir)
+ meta.System = lib.emuConf.GetEmulator(meta.Type, meta.Path)
+
+ 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
+ })
+
+ if err != nil {
+ lib.log.Error().Err(err).Str("dir", dir).Msgf("Lib scan... failed")
+ return
+ }
+
+ if len(games) > 0 {
+ 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()
+ }
+
+ // run scan again if delayed
+ lib.mu.Lock()
+ defer lib.mu.Unlock()
+ lib.isScanning = false
+ if lib.isScanningDelayed {
+ lib.isScanningDelayed = false
+ go lib.Scan()
+ }
+
+ lib.log.Info().Msg("Lib scan... completed")
+}
+
+// watch adds the ability to rescan the entire library
+// during filesystem changes in a watched directory.
+// !to add incremental library change
+func (lib *library) watch() {
+ watcher, err := fsnotify.NewWatcher()
+ if err != nil {
+ lib.log.Error().Err(err).Msg("Lib watcher has failed")
+ return
+ }
+
+ done := make(chan bool)
+ go func(repo *library) {
+ for {
+ select {
+ case event, ok := <-watcher.Events:
+ if !ok {
+ return
+ }
+ if event.Op == fsnotify.Create || event.Op == fsnotify.Remove {
+ // !to try to add the proper file/dir add/remove scan logic
+ // which is tricky
+ repo.Scan()
+ }
+ case _, ok := <-watcher.Errors:
+ if !ok {
+ return
+ }
+ }
+ }
+ }(lib)
+
+ if err = watcher.Add(lib.config.path); err != nil {
+ lib.log.Error().Err(err).Msg("Lib watch error")
+ }
+ <-done
+ _ = watcher.Close()
+ lib.log.Info().Msg("Lib watch has ended")
+}
+
+func (lib *library) set(games []GameMetadata) {
+ res := make(map[string]GameMetadata)
+ for _, value := range games {
+ res[value.Name] = value
+ }
+ lib.games = res
+}
+
+func (lib *library) isExtAllowed(path string) bool {
+ ext := strings.ToLower(filepath.Ext(path))
+ if ext == "" {
+ return false
+ }
+ _, ok := lib.config.supported[ext[1:]]
+ return ok
+}
+
+// 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: strings.ToLower(ext[1:]),
+ Path: relPath,
+ }
+}
+
+// dumpLibrary printouts the current library snapshot of games
+func (lib *library) dumpLibrary() {
+ var gameList strings.Builder
+
+ // 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"+
+ "--------------------------------------------\n"+
+ "--- The Library of ROMs ---\n"+
+ "--------------------------------------------\n"+
+ "%v"+
+ "--------------------------------------------\n"+
+ "--- ROMs: %03d --- Saves: %04d %10s ---\n"+
+ "--------------------------------------------",
+ gameList.String(), len(lib.games), len(lib.sessions), lib.lastScanDuration)
+}
+
+func toMap(list []string) map[string]struct{} {
+ res := make(map[string]struct{}, len(list))
+ for _, s := range list {
+ res[s] = struct{}{}
+ }
+ return res
+}
diff --git a/pkg/games/library_test.go b/pkg/games/library_test.go
new file mode 100644
index 00000000..28975647
--- /dev/null
+++ b/pkg/games/library_test.go
@@ -0,0 +1,124 @@
+package games
+
+import (
+ "os"
+ "path/filepath"
+ "reflect"
+ "testing"
+
+ "github.com/giongto35/cloud-game/v3/pkg/config"
+ "github.com/giongto35/cloud-game/v3/pkg/logger"
+)
+
+func TestLibraryScan(t *testing.T) {
+ tests := []struct {
+ directory string
+ expected []struct {
+ name string
+ system string
+ }
+ }{
+ {
+ directory: "../../assets/games",
+ expected: []struct {
+ name string
+ system string
+ }{
+ {name: "Alwa's Awakening (Demo)", system: "nes"},
+ {name: "Sushi The Cat", system: "gba"},
+ {name: "anguna", system: "gba"},
+ },
+ },
+ }
+
+ emuConf := config.Emulator{Libretro: config.LibretroConfig{}}
+ emuConf.Libretro.Cores.List = map[string]config.LibretroCoreConfig{
+ "nes": {Roms: []string{"nes"}},
+ "gba": {Roms: []string{"gba"}},
+ }
+
+ l := logger.NewConsole(false, "w", false)
+ for _, test := range tests {
+ library := NewLib(config.Library{
+ BasePath: test.directory,
+ Supported: []string{"gba", "zip", "nes"},
+ }, emuConf, l)
+ library.Scan()
+ games := library.GetAll()
+
+ all := true
+ for _, expect := range test.expected {
+ found := false
+ for _, game := range games {
+ if game.Name == expect.name && (expect.system != "" && expect.system == game.System) {
+ found = true
+ break
+ }
+ }
+ all = all && found
+ }
+ if !all {
+ t.Errorf("Test fail for dir %v with %v != %v", test.directory, games, test.expected)
+ }
+ }
+}
+
+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)
+ library := NewLib(config.Library{
+ BasePath: "../../assets/games",
+ Supported: []string{"gba", "zip", "nes"},
+ }, config.Emulator{}, log)
+
+ for b.Loop() {
+ library.Scan()
+ _ = library.GetAll()
+ }
+}
diff --git a/pkg/ice/ice.go b/pkg/ice/ice.go
deleted file mode 100644
index 5f813251..00000000
--- a/pkg/ice/ice.go
+++ /dev/null
@@ -1,57 +0,0 @@
-package ice
-
-import (
- "strings"
-
- "github.com/giongto35/cloud-game/v2/pkg/config/webrtc"
-)
-
-type Replacement struct {
- From string
- To string
-}
-
-func NewIceServer(url string) webrtc.IceServer {
- return webrtc.IceServer{
- Url: url,
- }
-}
-
-func NewIceServerCredentials(url string, user string, credential string) webrtc.IceServer {
- return webrtc.IceServer{
- Url: url,
- Username: user,
- Credential: credential,
- }
-}
-
-func ToJson(iceServers []webrtc.IceServer, replacements ...Replacement) string {
- var sb strings.Builder
- sn, n := len(iceServers), len(replacements)
- if sn > 0 {
- sb.Grow(sn * 64)
- }
- sb.WriteString("[")
- for i, ice := range iceServers {
- if i > 0 {
- sb.WriteString(",{")
- } else {
- sb.WriteString("{")
- }
- if n > 0 {
- for _, replacement := range replacements {
- ice.Url = strings.Replace(ice.Url, "{"+replacement.From+"}", replacement.To, -1)
- }
- }
- sb.WriteString("\"urls\":\"" + ice.Url + "\"")
- if ice.Username != "" {
- sb.WriteString(",\"username\":\"" + ice.Username + "\"")
- }
- if ice.Credential != "" {
- sb.WriteString(",\"credential\":\"" + ice.Credential + "\"")
- }
- sb.WriteString("}")
- }
- sb.WriteString("]")
- return sb.String()
-}
diff --git a/pkg/ice/ice_test.go b/pkg/ice/ice_test.go
deleted file mode 100644
index e5f027be..00000000
--- a/pkg/ice/ice_test.go
+++ /dev/null
@@ -1,78 +0,0 @@
-package ice
-
-import (
- "testing"
-
- "github.com/giongto35/cloud-game/v2/pkg/config/webrtc"
-)
-
-func TestIce(t *testing.T) {
- tests := []struct {
- input []webrtc.IceServer
- replacements []Replacement
- output string
- }{
- {
- input: []webrtc.IceServer{
- NewIceServer("stun:stun.l.google.com:19302"),
- NewIceServer("stun:{server-ip}:3478"),
- NewIceServerCredentials("turn:{server-ip}:3478", "root", "root"),
- },
- replacements: []Replacement{
- {
- From: "server-ip",
- To: "localhost",
- },
- },
- output: "[" +
- "{\"urls\":\"stun:stun.l.google.com:19302\"}," +
- "{\"urls\":\"stun:localhost:3478\"}," +
- "{\"urls\":\"turn:localhost:3478\",\"username\":\"root\",\"credential\":\"root\"}" +
- "]",
- },
- {
- input: []webrtc.IceServer{
- NewIceServer("stun:stun.l.google.com:19302"),
- },
- output: "[{\"urls\":\"stun:stun.l.google.com:19302\"}]",
- },
- {
- input: []webrtc.IceServer{},
- replacements: []Replacement{},
- output: "[]",
- },
- }
-
- for _, test := range tests {
- result := ToJson(test.input, test.replacements...)
-
- if result != test.output {
- t.Errorf("Not exactly what is expected")
- }
- }
-}
-
-func BenchmarkIces(b *testing.B) {
- benches := []struct {
- name string
- f func(iceServers []webrtc.IceServer, replacements ...Replacement) string
- }{
- {name: "toJson", f: ToJson},
- }
- servers := []webrtc.IceServer{
- NewIceServer("stun:stun.l.google.com:19302"),
- NewIceServer("stun:{server-ip}:3478"),
- NewIceServerCredentials("turn:{server-ip}:3478", "root", "root"),
- }
- replacements := []Replacement{
- {From: "server-ip", To: "localhost"},
- }
-
- for _, bench := range benches {
- b.Run(bench.name, func(b *testing.B) {
- for n := 0; n < b.N; n++ {
- bench.f(servers, replacements...)
- }
- })
- }
-}
diff --git a/pkg/lock/lock.go b/pkg/lock/lock.go
deleted file mode 100644
index 482ad100..00000000
--- a/pkg/lock/lock.go
+++ /dev/null
@@ -1,48 +0,0 @@
-package lock
-
-import (
- "sync/atomic"
- "time"
-)
-
-type TimeLock struct {
- l chan struct{}
- locked int32
-}
-
-// NewLock returns new lock (mutex) with a timeout option.
-func NewLock() *TimeLock {
- return &TimeLock{l: make(chan struct{}, 1)}
-}
-
-// Lock unconditionally blocks the execution.
-func (tl *TimeLock) Lock() {
- if tl.isLocked() {
- return
- }
- tl.lock()
- <-tl.l
-}
-
-// LockFor blocks the execution at most for
-// the given period of time.
-func (tl *TimeLock) LockFor(d time.Duration) {
- tl.lock()
- select {
- case <-tl.l:
- case <-time.After(d):
- }
-}
-
-// Unlock removes the current block if any.
-func (tl *TimeLock) Unlock() {
- if !tl.isLocked() {
- return
- }
- tl.unlock()
- tl.l <- struct{}{}
-}
-
-func (tl *TimeLock) isLocked() bool { return atomic.LoadInt32(&tl.locked) == 1 }
-func (tl *TimeLock) lock() { atomic.StoreInt32(&tl.locked, 1) }
-func (tl *TimeLock) unlock() { atomic.StoreInt32(&tl.locked, 0) }
diff --git a/pkg/lock/lock_test.go b/pkg/lock/lock_test.go
deleted file mode 100644
index 7d26170a..00000000
--- a/pkg/lock/lock_test.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package lock
-
-import (
- "testing"
- "time"
-)
-
-func TestLock(t *testing.T) {
- a := 1
- lock := NewLock()
- wait := time.Millisecond * 10
-
- lock.Unlock()
- lock.Unlock()
- lock.Unlock()
-
- go func(timeLock *TimeLock) {
- time.Sleep(time.Second * 1)
- lock.Unlock()
- }(lock)
-
- lock.LockFor(time.Second * 30)
- lock.LockFor(wait)
- lock.LockFor(wait)
- lock.LockFor(wait)
- lock.LockFor(wait)
- lock.LockFor(time.Millisecond * 10)
- go func(timeLock *TimeLock) {
- time.Sleep(time.Millisecond * 1)
- lock.Unlock()
- }(lock)
- lock.Lock()
-
- a -= 1
- if a != 0 {
- t.Errorf("lock test failed because a != 0")
- }
-}
diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go
new file mode 100644
index 00000000..53c3a2d2
--- /dev/null
+++ b/pkg/logger/logger.go
@@ -0,0 +1,195 @@
+package logger
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "strconv"
+ "time"
+
+ "github.com/rs/zerolog"
+ "github.com/rs/zerolog/log"
+)
+
+// Level defines log levels.
+type Level int8
+
+const (
+ DebugLevel Level = iota
+ InfoLevel
+ WarnLevel
+ ErrorLevel
+ FatalLevel
+ PanicLevel
+ NoLevel
+ Disabled
+ TraceLevel Level = -1
+ // Values less than TraceLevel are handled as numbers.
+)
+
+const (
+ ClientField = "c"
+ DirectionField = "d"
+ MarkNone = " "
+ MarkIn = "←"
+ MarkOut = "→"
+ MarkPlus = "+"
+ MarkCross = "x"
+)
+
+func (l Level) String() string {
+ switch l {
+ case TraceLevel:
+ return zerolog.LevelTraceValue
+ case DebugLevel:
+ return zerolog.LevelDebugValue
+ case InfoLevel:
+ return zerolog.LevelInfoValue
+ case WarnLevel:
+ return zerolog.LevelWarnValue
+ case ErrorLevel:
+ return zerolog.LevelErrorValue
+ case FatalLevel:
+ return zerolog.LevelFatalValue
+ case PanicLevel:
+ return zerolog.LevelPanicValue
+ case Disabled:
+ return "disabled"
+ case NoLevel:
+ return ""
+ }
+ return strconv.Itoa(int(l))
+}
+
+var pid = os.Getpid()
+
+type Logger struct {
+ logger *zerolog.Logger
+}
+
+func New(isDebug bool) *Logger {
+ logLevel := zerolog.InfoLevel
+ if isDebug {
+ logLevel = zerolog.DebugLevel
+ }
+ zerolog.SetGlobalLevel(logLevel)
+ logger := zerolog.New(os.Stderr).With().Timestamp().Fields(map[string]any{"pid": pid}).Logger()
+ return &Logger{logger: &logger}
+}
+
+func NewConsole(isDebug bool, tag string, noColor bool) *Logger {
+ logLevel := zerolog.InfoLevel
+ if isDebug {
+ logLevel = zerolog.DebugLevel
+ }
+ zerolog.SetGlobalLevel(logLevel)
+ zerolog.TimeFieldFormat = time.RFC3339Nano
+ output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: "15:04:05.0000", NoColor: noColor,
+ PartsOrder: []string{
+ zerolog.TimestampFieldName,
+ "pid",
+ zerolog.LevelFieldName,
+ zerolog.CallerFieldName,
+ "s",
+ DirectionField,
+ ClientField,
+ "m",
+ zerolog.MessageFieldName,
+ },
+ FieldsExclude: []string{"s", ClientField, DirectionField, "m", "pid"},
+ }
+
+ if output.NoColor {
+ output.FormatMessage = func(i any) string {
+ if i == nil {
+ return ""
+ }
+ return fmt.Sprintf("%v", i)
+ }
+ }
+
+ //multi := zerolog.MultiLevelWriter(output, os.Stdout)
+ logger := zerolog.New(output).With().
+ Str("pid", fmt.Sprintf("%4x", pid)).
+ Str("s", tag).
+ Str("m", "").
+ Str(DirectionField, MarkNone).
+ Str(ClientField, MarkNone).
+ // Str("tag", tag). use when a file writer
+ Timestamp().Logger()
+ return &Logger{logger: &logger}
+}
+
+func SetGlobalLevel(l Level) {
+ zerolog.SetGlobalLevel(zerolog.Level(l))
+}
+
+func Default() *Logger { return &Logger{logger: &log.Logger} }
+
+// GetLevel returns the current Level of l.
+func (l *Logger) GetLevel() Level { return Level(l.logger.GetLevel()) }
+
+// Output duplicates the global logger and sets w as its output.
+func (l *Logger) Output(w io.Writer) zerolog.Logger { return l.logger.Output(w) }
+
+// With creates a child logger with the field added to its context.
+func (l *Logger) With() zerolog.Context { return l.logger.With() }
+
+// Level creates a child logger with the minimum accepted level set to level.
+func (l *Logger) Level(level Level) zerolog.Logger { return l.logger.Level(zerolog.Level(level)) }
+
+// Sample returns a logger with the s sampler.
+func (l *Logger) Sample(s zerolog.Sampler) zerolog.Logger { return l.logger.Sample(s) }
+
+// Hook returns a logger with the h Hook.
+func (l *Logger) Hook(h zerolog.Hook) zerolog.Logger { return l.logger.Hook(h) }
+
+// Debug starts a new message with debug level.
+// You must call Msg on the returned event in order to send the event.
+func (l *Logger) Debug() *zerolog.Event { return l.logger.Debug() }
+
+func (l *Logger) Trace() *zerolog.Event { return l.logger.Trace() }
+
+// Info starts a new message with info level.
+// You must call Msg on the returned event in order to send the event.
+func (l *Logger) Info() *zerolog.Event { return l.logger.Info() }
+
+// Warn starts a new message with warn level.
+// You must call Msg on the returned event in order to send the event.
+func (l *Logger) Warn() *zerolog.Event { return l.logger.Warn() }
+
+// Error starts a new message with error level.
+func (l *Logger) Error() *zerolog.Event { return l.logger.Error() }
+
+// Fatal starts a new message with fatal level. The os.Exit(1) function
+// is called by the Msg method.
+// You must call Msg on the returned event in order to send the event.
+func (l *Logger) Fatal() *zerolog.Event { return l.logger.Fatal() }
+
+// Panic starts a new message with panic level. The message is also sent
+// to the panic function.
+// You must call Msg on the returned event in order to send the event.
+func (l *Logger) Panic() *zerolog.Event { return l.logger.Panic() }
+
+// WithLevel starts a new message with level.
+// You must call Msg on the returned event in order to send the event.
+func (l *Logger) WithLevel(level zerolog.Level) *zerolog.Event { return l.logger.WithLevel(level) }
+
+// Log starts a new message with no level. Setting zerolog.GlobalLevel to
+// zerolog.Disabled will still disable events produced by this method.
+// You must call Msg on the returned event in order to send the event.
+func (l *Logger) Log() *zerolog.Event { return l.logger.Log() }
+
+// Print sends a log event using debug level and no extra field.
+// Arguments are handled in the manner of fmt.Print.
+func (l *Logger) Print(v ...any) { l.logger.Print(v...) }
+
+// Printf sends a log event using debug level and no extra field.
+// Arguments are handled in the manner of fmt.Printf.
+func (l *Logger) Printf(format string, v ...any) { l.logger.Printf(format, v...) }
+
+// Extend adds some additional context to the existing logger.
+func (l *Logger) Extend(ctx zerolog.Context) *Logger {
+ logger := ctx.Logger()
+ return &Logger{logger: &logger}
+}
diff --git a/pkg/monitoring/monitoring.go b/pkg/monitoring/monitoring.go
index 257a744b..6ed7d3da 100644
--- a/pkg/monitoring/monitoring.go
+++ b/pkg/monitoring/monitoring.go
@@ -1,64 +1,64 @@
package monitoring
import (
- "context"
"fmt"
- "log"
- "math"
"net"
- "net/http"
"net/http/pprof"
"strconv"
- "strings"
- "github.com/giongto35/cloud-game/v2/pkg/config/monitoring"
- "github.com/giongto35/cloud-game/v2/pkg/network/httpx"
- "github.com/giongto35/cloud-game/v2/pkg/service"
- "github.com/prometheus/client_golang/prometheus/promhttp"
+ "github.com/VictoriaMetrics/metrics"
+ "github.com/giongto35/cloud-game/v3/pkg/config"
+ "github.com/giongto35/cloud-game/v3/pkg/logger"
+ "github.com/giongto35/cloud-game/v3/pkg/network/httpx"
)
const debugEndpoint = "/debug/pprof"
const metricsEndpoint = "/metrics"
type Monitoring struct {
- service.RunnableService
-
- conf monitoring.Config
- tag string
+ conf config.Monitoring
server *httpx.Server
+ log *logger.Logger
}
// New creates new monitoring service.
// The tag param specifies owner label for logs.
-func New(conf monitoring.Config, baseAddr string, tag string) *Monitoring {
+func New(conf config.Monitoring, baseAddr string, log *logger.Logger) *Monitoring {
serv, err := httpx.NewServer(
net.JoinHostPort(baseAddr, strconv.Itoa(conf.Port)),
- func(*httpx.Server) http.Handler {
- h := http.NewServeMux()
+ func(s *httpx.Server) httpx.Handler {
+ h := s.Mux()
if conf.ProfilingEnabled {
- prefix := conf.URLPrefix + debugEndpoint
- h.HandleFunc(prefix+"/", pprof.Index)
- h.HandleFunc(prefix+"/cmdline", pprof.Cmdline)
- h.HandleFunc(prefix+"/profile", pprof.Profile)
- h.HandleFunc(prefix+"/symbol", pprof.Symbol)
- h.HandleFunc(prefix+"/trace", pprof.Trace)
- h.Handle(prefix+"/allocs", pprof.Handler("allocs"))
- h.Handle(prefix+"/block", pprof.Handler("block"))
- h.Handle(prefix+"/goroutine", pprof.Handler("goroutine"))
- h.Handle(prefix+"/heap", pprof.Handler("heap"))
- h.Handle(prefix+"/mutex", pprof.Handler("mutex"))
- h.Handle(prefix+"/threadcreate", pprof.Handler("threadcreate"))
+ h.Prefix(conf.URLPrefix + debugEndpoint)
+ h.HandleFunc("/", pprof.Index).
+ HandleFunc("/cmdline", pprof.Cmdline).
+ HandleFunc("/profile", pprof.Profile).
+ HandleFunc("/symbol", pprof.Symbol).
+ HandleFunc("/trace", pprof.Trace).
+ Handle("/allocs", pprof.Handler("allocs")).
+ Handle("/block", pprof.Handler("block")).
+ Handle("/goroutine", pprof.Handler("goroutine")).
+ Handle("/heap", pprof.Handler("heap")).
+ Handle("/mutex", pprof.Handler("mutex")).
+ Handle("/threadcreate", pprof.Handler("threadcreate"))
}
if conf.MetricEnabled {
- h.Handle(conf.URLPrefix+metricsEndpoint, promhttp.Handler())
+ h.Prefix(conf.URLPrefix)
+ h.HandleFunc(metricsEndpoint, func(w httpx.ResponseWriter, _ *httpx.Request) {
+ metrics.WritePrometheus(w, true)
+ })
}
+ h.Prefix("")
return h
},
- httpx.WithPortRoll(true))
+ httpx.WithPortRoll(true),
+ httpx.HttpsRedirect(false),
+ httpx.WithLogger(log),
+ )
if err != nil {
- log.Fatalf("couldn't start monitoring server: %v", err)
+ log.Error().Err(err).Msg("couldn't start monitoring server")
}
- return &Monitoring{conf: conf, tag: tag, server: serv}
+ return &Monitoring{conf: conf, server: serv, log: log}
}
func (m *Monitoring) Run() {
@@ -66,9 +66,9 @@ func (m *Monitoring) Run() {
m.server.Run()
}
-func (m *Monitoring) Shutdown(ctx context.Context) error {
- log.Printf("[%v] Shutting down monitoring server", m.tag)
- return m.server.Shutdown(ctx)
+func (m *Monitoring) Stop() error {
+ m.log.Info().Msg("Shutting down monitoring server")
+ return m.server.Stop()
}
func (m *Monitoring) String() string {
@@ -84,28 +84,12 @@ func (m *Monitoring) GetProfilingAddress() string {
}
func (m *Monitoring) printInfo() {
- length, pad := 42, 20
- var table, records strings.Builder
- table.Grow(length * 4)
- records.Grow(length * 2)
-
+ message := m.log.Info()
if m.conf.ProfilingEnabled {
- addr := m.GetProfilingAddress()
- length = int(math.Max(float64(length), float64(len(addr)+pad)))
- records.WriteString(" Profiling " + addr + "\n")
+ message = message.Str("profiler", m.GetProfilingAddress())
}
if m.conf.MetricEnabled {
- addr := m.GetMetricsPublicAddress()
- length = int(math.Max(float64(length), float64(len(addr)+pad)))
- records.WriteString(" Prometheus " + addr + "\n")
+ message = message.Str("prometheus", m.GetMetricsPublicAddress())
}
-
- title := "Monitoring"
- edge := strings.Repeat("-", length)
- c := (length-len(title)-3)/2 + 1 + len(title) - 3
- table.WriteString(fmt.Sprintf("[%s]\n", m.tag))
- table.WriteString(fmt.Sprintf("%s\n---%*s%*s\n%s\n", edge, c, title, length-(c+len(title))+6+1, "---", edge))
- table.WriteString(records.String())
- table.WriteString(edge)
- log.Printf(table.String())
+ message.Msg("Monitoring")
}
diff --git a/pkg/network/address.go b/pkg/network/address.go
new file mode 100644
index 00000000..318c9cd5
--- /dev/null
+++ b/pkg/network/address.go
@@ -0,0 +1,29 @@
+package network
+
+import (
+ "errors"
+ "net"
+ "strconv"
+ "strings"
+)
+
+type Address string
+
+func (a *Address) Port() (int, error) {
+ if len(string(*a)) == 0 {
+ return 0, errors.New("no address")
+ }
+ 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)
+}
diff --git a/pkg/network/address_test.go b/pkg/network/address_test.go
new file mode 100644
index 00000000..f3f26935
--- /dev/null
+++ b/pkg/network/address_test.go
@@ -0,0 +1,26 @@
+package network
+
+import (
+ "testing"
+)
+
+func TestAddressPort(t *testing.T) {
+ tests := []struct {
+ input Address
+ port int
+ err string
+ }{
+ {input: "", port: 0, err: "no address"},
+ {input: ":", port: 0, err: "port is not a number"},
+ {input: "https://garbage.com:99a9a", port: 0, err: "port is not a number"},
+ {input: ":9000", port: 9000},
+ {input: "not-garbage:9999", port: 9999},
+ }
+
+ for _, test := range tests {
+ port, err := test.input.Port()
+ if port != test.port || (err != nil && test.err != err.Error()) {
+ t.Errorf("Test fail for expected port %v but got %v with error %v", test.port, port, err)
+ }
+ }
+}
diff --git a/pkg/network/httpx/listener.go b/pkg/network/httpx/listener.go
index 156aa739..045cf641 100644
--- a/pkg/network/httpx/listener.go
+++ b/pkg/network/httpx/listener.go
@@ -3,10 +3,9 @@ package httpx
import (
"errors"
"net"
- "os"
- "runtime"
"strconv"
- "syscall"
+
+ "github.com/giongto35/cloud-game/v3/pkg/network/socket"
)
const listenAttempts = 42
@@ -25,7 +24,7 @@ func NewListener(address string, withNextFreePort bool) (*Listener, error) {
func listener(address string, withNextFreePort bool) (net.Listener, error) {
listener, err := net.Listen("tcp", address)
- if err == nil || !withNextFreePort || !isPortBusyError(err) {
+ if err == nil || !withNextFreePort || !socket.IsPortBusyError(err) {
return listener, err
}
// we will roll next available port
@@ -58,22 +57,3 @@ func (l Listener) GetPort() int {
}
return 0
}
-
-func isPortBusyError(err error) bool {
- var eOsSyscall *os.SyscallError
- if !errors.As(err, &eOsSyscall) {
- return false
- }
- var errErrno syscall.Errno
- if !errors.As(eOsSyscall, &errErrno) {
- return false
- }
- if errErrno == syscall.EADDRINUSE {
- return true
- }
- const WSAEADDRINUSE = 10048
- if runtime.GOOS == "windows" && errErrno == WSAEADDRINUSE {
- return true
- }
- return false
-}
diff --git a/pkg/network/httpx/listener_test.go b/pkg/network/httpx/listener_test.go
index 7a8900f4..c8d06eb4 100644
--- a/pkg/network/httpx/listener_test.go
+++ b/pkg/network/httpx/listener_test.go
@@ -13,7 +13,6 @@ func TestListenerCreation(t *testing.T) {
random bool
error bool
}{
- {addr: ":80", port: "80"},
{addr: ":", random: true},
{addr: ":0", random: true},
{addr: "", random: true},
@@ -38,14 +37,14 @@ func TestListenerCreation(t *testing.T) {
continue
}
- defer ls.Close()
-
addr := ls.Addr().(*net.TCPAddr)
port := ls.GetPort()
hasPort := port > 0
isPortSame := strings.HasSuffix(addr.String(), ":"+test.port)
+ _ = ls.Close()
+
if test.random {
if !hasPort {
t.Errorf("expected a random port, got %v", port)
@@ -64,7 +63,7 @@ func TestFailOnPortInUse(t *testing.T) {
if err != nil {
t.Errorf("expected no error, got %v", err)
}
- defer a.Close()
+ defer func() { _ = a.Close() }()
_, err = NewListener(":3333", false)
if err == nil {
t.Errorf("expected busy port error, but got none")
@@ -76,10 +75,10 @@ func TestListenerPortRoll(t *testing.T) {
if err != nil {
t.Errorf("expected no error, got %v", err)
}
- defer a.Close()
+ defer func() { _ = a.Close() }()
b, err := NewListener("127.0.0.1:3333", true)
if err != nil {
t.Errorf("expected no port error, but got %v", err)
}
- b.Close()
+ _ = b.Close()
}
diff --git a/pkg/network/httpx/options.go b/pkg/network/httpx/options.go
index aa6fa666..0a30de7e 100644
--- a/pkg/network/httpx/options.go
+++ b/pkg/network/httpx/options.go
@@ -3,7 +3,8 @@ package httpx
import (
"time"
- "github.com/giongto35/cloud-game/v2/pkg/config/shared"
+ "github.com/giongto35/cloud-game/v3/pkg/config"
+ "github.com/giongto35/cloud-game/v3/pkg/logger"
)
type (
@@ -18,6 +19,7 @@ type (
IdleTimeout time.Duration
ReadTimeout time.Duration
WriteTimeout time.Duration
+ Logger *logger.Logger
Zone string
}
Option func(*Options)
@@ -45,7 +47,7 @@ func HttpsRedirect(redirect bool) Option {
func WithPortRoll(roll bool) Option { return func(opts *Options) { opts.PortRoll = roll } }
func WithZone(zone string) Option { return func(opts *Options) { opts.Zone = zone } }
-func WithServerConfig(conf shared.Server) Option {
+func WithServerConfig(conf config.Server) Option {
return func(opts *Options) {
opts.Https = conf.Https
opts.HttpsCert = conf.Tls.HttpsCert
@@ -54,3 +56,4 @@ func WithServerConfig(conf shared.Server) Option {
opts.HttpsRedirectAddress = conf.Address
}
}
+func WithLogger(log *logger.Logger) Option { return func(opts *Options) { opts.Logger = log } }
diff --git a/pkg/network/httpx/server.go b/pkg/network/httpx/server.go
index 24cbd4b3..5486c05c 100644
--- a/pkg/network/httpx/server.go
+++ b/pkg/network/httpx/server.go
@@ -1,37 +1,76 @@
package httpx
import (
- "context"
- "log"
+ "errors"
+ "fmt"
"net/http"
"net/url"
"time"
- "github.com/giongto35/cloud-game/v2/pkg/service"
+ "github.com/giongto35/cloud-game/v3/pkg/logger"
"golang.org/x/crypto/acme/autocert"
)
type Server struct {
http.Server
- service.RunnableService
autoCert *autocert.Manager
opts Options
listener *Listener
redirect *Server
+ log *logger.Logger
}
-func NewServer(address string, handler func(*Server) http.Handler, options ...Option) (*Server, error) {
+type (
+ Mux struct {
+ *http.ServeMux
+ prefix string
+ }
+ Handler = http.Handler
+ HandlerFunc = http.HandlerFunc
+ ResponseWriter = http.ResponseWriter
+ Request = http.Request
+)
+
+// NewServeMux allocates and returns a new ServeMux.
+func NewServeMux(prefix string) *Mux {
+ return &Mux{ServeMux: http.NewServeMux(), prefix: prefix}
+}
+
+func (m *Mux) Prefix(v string) { m.prefix = v }
+
+func (m *Mux) HandleW(pattern string, h func(http.ResponseWriter)) *Mux {
+ m.ServeMux.HandleFunc(m.prefix+pattern, func(w http.ResponseWriter, _ *http.Request) { h(w) })
+ return m
+}
+
+func (m *Mux) Handle(pattern string, handler Handler) *Mux {
+ m.ServeMux.Handle(m.prefix+pattern, handler)
+ return m
+}
+
+func (m *Mux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) *Mux {
+ m.ServeMux.HandleFunc(m.prefix+pattern, handler)
+ return m
+}
+
+func (m *Mux) ServeHTTP(w ResponseWriter, r *Request) { m.ServeMux.ServeHTTP(w, r) }
+
+func NewServer(address string, handler func(*Server) Handler, options ...Option) (*Server, error) {
opts := &Options{
Https: false,
HttpsRedirect: true,
IdleTimeout: 120 * time.Second,
- ReadTimeout: 5 * time.Second,
- WriteTimeout: 5 * time.Second,
+ ReadTimeout: 500 * time.Second,
+ WriteTimeout: 500 * time.Second,
}
opts.override(options...)
+ if opts.Logger == nil {
+ opts.Logger = logger.Default()
+ }
+
server := &Server{
Server: http.Server{
Addr: address,
@@ -40,6 +79,7 @@ func NewServer(address string, handler func(*Server) http.Handler, options ...Op
WriteTimeout: opts.WriteTimeout,
},
opts: *opts,
+ log: opts.Logger,
}
// (╯°□°)╯︵ ┻━┻
server.Handler = handler(server)
@@ -55,7 +95,7 @@ func NewServer(address string, handler func(*Server) http.Handler, options ...Op
if opts.Https {
addr = ":https"
}
- log.Printf("Warning! Empty server address has been changed to %v", addr)
+ opts.Logger.Warn().Msgf("Empty server address has been changed to %v", addr)
}
listener, err := NewListener(addr, server.opts.PortRoll)
if err != nil {
@@ -64,23 +104,28 @@ func NewServer(address string, handler func(*Server) http.Handler, options ...Op
server.listener = listener
addr = buildAddress(server.Addr, opts.Zone, *listener)
- log.Printf("[server] address was set to %v (%v)", addr, server.Addr)
+ opts.Logger.Info().Msgf("httpx %v (%v)", addr, server.Addr)
server.Addr = addr
return server, nil
}
-func (s *Server) Run() {
+func (s *Server) MuxX(prefix string) *Mux { return NewServeMux(prefix) }
+func (s *Server) Mux() *Mux { return s.MuxX("") }
+
+func (s *Server) Run() { go s.run() }
+
+func (s *Server) run() {
protocol := s.GetProtocol()
- log.Printf("Starting %s server on %s", protocol, s.Addr)
+ 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 {
- log.Fatalf("couldn't init redirection server: %v", err)
+ 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
@@ -89,21 +134,19 @@ func (s *Server) Run() {
} else {
err = s.Serve(*s.listener)
}
- switch err {
- case http.ErrServerClosed:
- log.Printf("%s server was closed", protocol)
+
+ if errors.Is(err, http.ErrServerClosed) {
+ s.log.Debug().Msgf("%s server was closed", protocol)
return
- default:
- log.Printf("error: %s", err)
}
+ s.log.Error().Err(err)
}
-func (s *Server) Shutdown(ctx context.Context) (err error) {
+func (s *Server) Stop() error {
if s.redirect != nil {
- err = s.redirect.Shutdown(ctx)
+ _ = s.redirect.Stop()
}
- err = s.Server.Shutdown(ctx)
- return
+ return s.Server.Close()
}
func (s *Server) GetHost() string { return extractHost(s.Addr) }
@@ -122,23 +165,29 @@ 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) http.Handler {
- h := http.NewServeMux()
-
- h.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ srv, err := NewServer(s.opts.HttpsRedirectAddress, func(serv *Server) Handler {
+ h := NewServeMux("")
+ h.Handle("/", HandlerFunc(func(w ResponseWriter, r *Request) {
httpsURL := url.URL{Scheme: "https", Host: addr, Path: r.URL.Path, RawQuery: r.URL.RawQuery}
rdr := httpsURL.String()
- log.Printf("Redirect: http://%s%s -> %s", r.Host, r.URL.String(), rdr)
+ if s.log.GetLevel() < logger.InfoLevel {
+ s.log.Debug().
+ Str("from", fmt.Sprintf("http://%s%s", r.Host, r.URL.String())).
+ Str("to", rdr).
+ Msg("Redirect")
+ }
http.Redirect(w, r, rdr, http.StatusFound)
}))
-
- // do we need this after all?
if serv.autoCert != nil {
return serv.autoCert.HTTPHandler(h)
}
return h
- })
- log.Printf("Starting HTTP->HTTPS redirection server on %s", addr)
+ },
+ WithLogger(s.log),
+ )
return srv, err
}
+
+func FileServer(dir string) http.Handler { return http.FileServer(http.Dir(dir)) }
diff --git a/pkg/network/retry.go b/pkg/network/retry.go
new file mode 100644
index 00000000..9fb706dc
--- /dev/null
+++ b/pkg/network/retry.go
@@ -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 }
diff --git a/pkg/network/socket/socket.go b/pkg/network/socket/socket.go
new file mode 100644
index 00000000..bceb3fdd
--- /dev/null
+++ b/pkg/network/socket/socket.go
@@ -0,0 +1,87 @@
+package socket
+
+import (
+ "errors"
+ "net"
+ "os"
+ "runtime"
+ "syscall"
+)
+
+const listenAttempts = 42
+const udpBufferSize = 16 * 1024 * 1024
+
+// NewSocket creates either TCP or UDP socket listener on a given port.
+// The proto param supports on of these values:
+// udp, udp4, udp6, tcp, tcp4, tcp6
+// The function result will be either *net.UDPConn for UDPs or
+// *net.TCPListener for TCPs.
+func NewSocket(proto string, port int) (any, error) {
+ if listener, err := socket(proto, port); err != nil {
+ return nil, err
+ } else {
+ return listener, nil
+ }
+}
+
+// NewSocketPortRoll creates either TCP or UDP socket listener on the next free port.
+// See: NewSocket.
+func NewSocketPortRoll(proto string, port int) (listener any, err error) {
+ if listener, err = NewSocket(proto, port); err == nil {
+ return listener, nil
+ }
+ if IsPortBusyError(err) {
+ for i := port + 1; i < port+listenAttempts; i++ {
+ listener, err := socket(proto, i)
+ if err == nil {
+ return listener, nil
+ }
+ }
+ return nil, errors.New("no available ports")
+ }
+ return nil, err
+}
+
+func socket(proto string, port int) (any, error) {
+ switch proto {
+ case "udp", "udp4", "udp6":
+ if l, err := net.ListenUDP(proto, &net.UDPAddr{Port: port}); err == nil {
+ _ = l.SetReadBuffer(udpBufferSize)
+ _ = l.SetWriteBuffer(udpBufferSize)
+ return l, nil
+ } else {
+ return nil, err
+ }
+ case "tcp", "tcp4", "tcp6":
+ if l, err := net.ListenTCP(proto, &net.TCPAddr{Port: port}); err == nil {
+ return l, nil
+ } else {
+ return nil, err
+ }
+ }
+ return nil, errors.New("socket error")
+}
+
+// IsPortBusyError tests if the given error is one of
+// the port busy errors.
+func IsPortBusyError(err error) bool {
+ if err == nil {
+ return false
+ }
+ var eOsSyscall *os.SyscallError
+ if !errors.As(err, &eOsSyscall) {
+ return false
+ }
+ var errErrno syscall.Errno
+ if !errors.As(eOsSyscall, &errErrno) {
+ return false
+ }
+ if errErrno == syscall.EADDRINUSE {
+ return true
+ }
+ const WSAEADDRINUSE = 10048
+ if runtime.GOOS == "windows" && errErrno == WSAEADDRINUSE {
+ return true
+ }
+ return false
+}
diff --git a/pkg/network/socket/socket_test.go b/pkg/network/socket/socket_test.go
new file mode 100644
index 00000000..c9c785d8
--- /dev/null
+++ b/pkg/network/socket/socket_test.go
@@ -0,0 +1,31 @@
+package socket
+
+import (
+ "net"
+ "testing"
+)
+
+func TestFailOnPortInUse(t *testing.T) {
+ l, err := NewSocket("udp", 1234)
+ if err != nil {
+ t.Errorf("expected no error, got %v", err)
+ }
+ defer l.(*net.UDPConn).Close()
+ _, err = NewSocket("udp", 1234)
+ if err == nil {
+ t.Errorf("expected busy port error, but got none")
+ }
+}
+
+func TestListenerPortRoll(t *testing.T) {
+ l, err := NewSocketPortRoll("udp", 1234)
+ if err != nil {
+ t.Errorf("expected no error, got %v", err)
+ }
+ defer l.(*net.UDPConn).Close()
+ l2, err := NewSocketPortRoll("udp", 1234)
+ if err != nil {
+ t.Errorf("expected no port error, but got one")
+ }
+ l2.(*net.UDPConn).Close()
+}
diff --git a/pkg/network/webrtc/factory.go b/pkg/network/webrtc/factory.go
new file mode 100644
index 00000000..c8b37ab8
--- /dev/null
+++ b/pkg/network/webrtc/factory.go
@@ -0,0 +1,100 @@
+package webrtc
+
+import (
+ "fmt"
+ "net"
+
+ "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/v4"
+ "github.com/pion/interceptor"
+ "github.com/pion/interceptor/pkg/report"
+ "github.com/pion/webrtc/v4"
+)
+
+type ApiFactory struct {
+ api *webrtc.API
+ conf webrtc.Configuration
+}
+
+type ModApiFun func(m *webrtc.MediaEngine, i *interceptor.Registry, s *webrtc.SettingEngine)
+
+func NewApiFactory(conf config.Webrtc, log *logger.Logger, mod ModApiFun) (api *ApiFactory, err error) {
+ m := &webrtc.MediaEngine{}
+ if err = m.RegisterDefaultCodecs(); err != nil {
+ return
+ }
+ i := &interceptor.Registry{}
+
+ if conf.DisableDefaultInterceptors {
+ sender, err := report.NewSenderInterceptor()
+ if err != nil {
+ return nil, err
+ }
+ i.Add(sender)
+ } else if err = webrtc.RegisterDefaultInterceptors(m, i); err != nil {
+ return
+ }
+ customLogger := NewPionLogger(log, conf.LogLevel)
+ s := webrtc.SettingEngine{LoggerFactory: customLogger}
+ s.SetIncludeLoopbackCandidate(true)
+ if conf.HasDtlsRole() {
+ log.Info().Msgf("A custom DTLS role [%v]", conf.DtlsRole)
+ err = s.SetAnsweringDTLSRole(webrtc.DTLSRole(conf.DtlsRole))
+ if err != nil {
+ return
+ }
+ }
+ if conf.IceLite {
+ s.SetLite(conf.IceLite)
+ }
+ if conf.HasPortRange() {
+ if err = s.SetEphemeralUDPPortRange(conf.IcePorts.Min, conf.IcePorts.Max); err != nil {
+ return
+ }
+ }
+ if conf.HasSinglePort() {
+ var l any
+ l, err = socket.NewSocketPortRoll("udp", conf.SinglePort)
+ if err != nil {
+ return
+ }
+ udp, ok := l.(*net.UDPConn)
+ if !ok {
+ err = fmt.Errorf("use of not a UDP socket")
+ return
+ }
+ s.SetICEUDPMux(webrtc.NewICEUDPMux(customLogger, udp))
+ log.Info().Msgf("The single port mode is active for %s", udp.LocalAddr())
+ }
+ if conf.HasIceIpMap() {
+ s.SetNAT1To1IPs([]string{conf.IceIpMap}, webrtc.ICECandidateTypeHost)
+ log.Info().Msgf("The NAT mapping is active for %v", conf.IceIpMap)
+ }
+
+ s.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
+ s.EnableSCTPZeroChecksum(true)
+
+ if mod != nil {
+ mod(m, i, &s)
+ }
+
+ c := webrtc.Configuration{ICEServers: []webrtc.ICEServer{}}
+ for _, server := range conf.IceServers {
+ c.ICEServers = append(c.ICEServers, webrtc.ICEServer{
+ URLs: []string{server.Urls},
+ Username: server.Username,
+ Credential: server.Credential,
+ })
+ }
+
+ return &ApiFactory{
+ api: webrtc.NewAPI(webrtc.WithMediaEngine(m), webrtc.WithInterceptorRegistry(i), webrtc.WithSettingEngine(s)),
+ conf: c,
+ }, err
+}
+
+func (a *ApiFactory) NewPeer() (*webrtc.PeerConnection, error) {
+ return a.api.NewPeerConnection(a.conf)
+}
diff --git a/pkg/network/webrtc/pionlogger.go b/pkg/network/webrtc/pionlogger.go
new file mode 100644
index 00000000..ec546f3a
--- /dev/null
+++ b/pkg/network/webrtc/pionlogger.go
@@ -0,0 +1,33 @@
+package webrtc
+
+import (
+ "github.com/pion/logging"
+ "github.com/rs/zerolog"
+
+ "github.com/giongto35/cloud-game/v3/pkg/logger"
+)
+
+type PionLog struct {
+ log *logger.Logger
+}
+
+const trace = zerolog.Level(logger.TraceLevel)
+
+func NewPionLogger(root *logger.Logger, level int) *PionLog {
+ return &PionLog{log: root.Extend(root.Level(logger.Level(level)).With())}
+}
+
+func (p PionLog) NewLogger(scope string) logging.LeveledLogger {
+ return PionLog{log: p.log.Extend(p.log.With().Str("mod", scope))}
+}
+
+func (p PionLog) Debug(msg string) { p.log.Debug().Msg(msg) }
+func (p PionLog) Debugf(format string, args ...any) { p.log.Debug().Msgf(format, args...) }
+func (p PionLog) Error(msg string) { p.log.Error().Msg(msg) }
+func (p PionLog) Errorf(format string, args ...any) { p.log.Error().Msgf(format, args...) }
+func (p PionLog) Info(msg string) { p.log.Info().Msg(msg) }
+func (p PionLog) Infof(format string, args ...any) { p.log.Info().Msgf(format, args...) }
+func (p PionLog) Trace(msg string) { p.log.WithLevel(trace).Msg(msg) }
+func (p PionLog) Tracef(format string, args ...any) { p.log.WithLevel(trace).Msgf(format, args...) }
+func (p PionLog) Warn(msg string) { p.log.Warn().Msg(msg) }
+func (p PionLog) Warnf(format string, args ...any) { p.log.Warn().Msgf(format, args...) }
diff --git a/pkg/network/webrtc/webrtc.go b/pkg/network/webrtc/webrtc.go
new file mode 100644
index 00000000..37b99e79
--- /dev/null
+++ b/pkg/network/webrtc/webrtc.go
@@ -0,0 +1,267 @@
+package webrtc
+
+import (
+ "fmt"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/giongto35/cloud-game/v3/pkg/logger"
+ "github.com/pion/webrtc/v4"
+ "github.com/pion/webrtc/v4/pkg/media"
+)
+
+type Peer struct {
+ api *ApiFactory
+ conn *webrtc.PeerConnection
+ log *logger.Logger
+ OnMessage func(data []byte)
+
+ a *webrtc.TrackLocalStaticSample
+ v *webrtc.TrackLocalStaticSample
+ d *webrtc.DataChannel
+}
+
+var samplePool sync.Pool
+
+type Decoder func(data string, obj any) error
+
+func New(log *logger.Logger, api *ApiFactory) *Peer { return &Peer{api: api, log: log} }
+
+func (p *Peer) NewCall(vCodec, aCodec string, onICECandidate func(ice any)) (sdp any, err error) {
+ if p.conn != nil && p.conn.ConnectionState() == webrtc.PeerConnectionStateConnected {
+ return
+ }
+ p.log.Debug().Msg("WebRTC start")
+ if p.conn, err = p.api.NewPeer(); err != nil {
+ return
+ }
+ p.conn.OnICECandidate(p.handleICECandidate(onICECandidate))
+ // plug in the [video] track (out)
+ video, err := newTrack("video", "video", vCodec)
+ if err != nil {
+ return "", err
+ }
+ 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", "audio", aCodec)
+ if err != nil {
+ return "", err
+ }
+ 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
+
+ err = p.AddChannel("data", func(data []byte) {
+ if len(data) == 0 || p.OnMessage == nil {
+ return
+ }
+ p.OnMessage(data)
+ })
+ if err != nil {
+ return "", err
+ }
+
+ 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.Debug().Msg("Created Offer")
+
+ err = p.conn.SetLocalDescription(offer)
+ if err != nil {
+ return "", err
+ }
+
+ return offer, nil
+}
+
+func (p *Peer) SendAudio(dat []byte, dur int32) {
+ if err := p.send(dat, int64(dur), p.a.WriteSample); err != nil {
+ p.log.Error().Err(err).Send()
+ }
+}
+
+func (p *Peer) SendVideo(data []byte, dur int32) {
+ if err := p.send(data, int64(dur), p.v.WriteSample); err != nil {
+ p.log.Error().Err(err).Send()
+ }
+}
+
+func (p *Peer) SendData(data []byte) { _ = p.d.Send(data) }
+
+func (p *Peer) send(data []byte, duration int64, fn func(media.Sample) error) error {
+ sample, _ := samplePool.Get().(*media.Sample)
+ if sample == nil {
+ sample = new(media.Sample)
+ }
+ sample.Data = data
+ sample.Duration = time.Duration(duration)
+ err := fn(*sample)
+ if err != nil {
+ return err
+ }
+ samplePool.Put(sample)
+ return nil
+}
+
+func (p *Peer) SetRemoteSDP(sdp string, decoder Decoder) error {
+ var answer webrtc.SessionDescription
+ if err := decoder(sdp, &answer); err != nil {
+ return err
+ }
+ if err := p.conn.SetRemoteDescription(answer); err != nil {
+ p.log.Error().Err(err).Msg("Set remote description from peer failed")
+ return err
+ }
+ p.log.Debug().Msg("Set Remote Description")
+ return nil
+}
+
+func newTrack(id string, label string, codec string) (*webrtc.TrackLocalStaticSample, error) {
+ codec = strings.ToLower(codec)
+ var mime string
+ switch id {
+ case "audio":
+ switch codec {
+ case "opus":
+ mime = webrtc.MimeTypeOpus
+ }
+ case "video":
+ switch codec {
+ case "h264":
+ mime = webrtc.MimeTypeH264
+ case "vpx", "vp8":
+ mime = webrtc.MimeTypeVP8
+ case "vp9":
+ mime = webrtc.MimeTypeVP9
+ }
+ }
+ if mime == "" {
+ return nil, fmt.Errorf("unsupported codec %s:%s", id, codec)
+ }
+ return webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: mime}, id, label)
+}
+
+func (p *Peer) handleICECandidate(callback func(any)) func(*webrtc.ICECandidate) {
+ return func(ice *webrtc.ICECandidate) {
+ // ICE gathering finish condition
+ if ice == nil {
+ callback(nil)
+ p.log.Debug().Msg("ICE gathering was complete probably")
+ return
+ }
+ candidate := ice.ToJSON()
+ p.log.Debug().Str("candidate", candidate.Candidate).Msg("ICE")
+ callback(&candidate)
+ }
+}
+
+func (p *Peer) handleICEState(onConnect func()) func(webrtc.ICEConnectionState) {
+ return func(state webrtc.ICEConnectionState) {
+ p.log.Debug().Str(".state", state.String()).Msg("ICE")
+ switch state {
+ case webrtc.ICEConnectionStateChecking:
+ // nothing
+ case webrtc.ICEConnectionStateConnected:
+ onConnect()
+ case webrtc.ICEConnectionStateFailed:
+ p.log.Error().Msgf("WebRTC connection fail! connection: %v, ice: %v, gathering: %v, signalling: %v",
+ p.conn.ConnectionState(), p.conn.ICEConnectionState(), p.conn.ICEGatheringState(),
+ p.conn.SignalingState())
+ p.Disconnect()
+ case webrtc.ICEConnectionStateClosed,
+ webrtc.ICEConnectionStateDisconnected:
+ p.Disconnect()
+ default:
+ p.log.Debug().Msg("ICE state is not handled!")
+ }
+ }
+}
+
+func (p *Peer) AddCandidate(candidate string, decoder Decoder) error {
+ // !to add test when the connection is closed but it is still
+ // receiving ice candidates
+
+ var iceCandidate webrtc.ICECandidateInit
+ if err := decoder(candidate, &iceCandidate); err != nil {
+ return err
+ }
+ if err := p.conn.AddICECandidate(iceCandidate); err != nil {
+ return err
+ }
+ p.log.Debug().Str("candidate", iceCandidate.Candidate).Msg("Ice")
+ 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
+ }
+ if p.conn.ConnectionState() < webrtc.PeerConnectionStateDisconnected {
+ // ignore this due to DTLS fatal: conn is closed
+ _ = p.conn.Close()
+ }
+ p.log.Debug().Msg("WebRTC stop")
+}
+
+// addDataChannel creates new WebRTC data channel.
+// Default params -- ordered: true, negotiated: false.
+func (p *Peer) addDataChannel(label string) (*webrtc.DataChannel, error) {
+ ch, err := p.conn.CreateDataChannel(label, nil)
+ if err != nil {
+ return nil, err
+ }
+ ch.OnOpen(func() {
+ p.log.Debug().Uint16("id", *ch.ID()).Msgf("Data channel [%v] opened", ch.Label())
+ })
+ ch.OnError(p.logx)
+ 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) }
diff --git a/pkg/network/websocket/websocket.go b/pkg/network/websocket/websocket.go
index 18687162..85fa3b9e 100644
--- a/pkg/network/websocket/websocket.go
+++ b/pkg/network/websocket/websocket.go
@@ -2,16 +2,257 @@ package websocket
import (
"crypto/tls"
+ "net"
+ "net/http"
"net/url"
+ "sync"
+ "time"
"github.com/gorilla/websocket"
)
-func Connect(address url.URL) (*websocket.Conn, error) {
- dialer := websocket.Dialer{}
+const (
+ maxMessageSize = 10 * 1024
+ pingTime = pongTime * 9 / 10
+ pongTime = 7 * time.Second
+ writeWait = 5 * time.Second
+)
+
+type Client struct {
+ Dialer *websocket.Dialer
+}
+
+type Server struct {
+ Upgrader *Upgrader
+}
+
+type Connection struct {
+ alive bool
+ callback MessageHandler
+ conn deadlineConn
+ done chan struct{}
+ errorHandler ErrorHandler
+ once sync.Once
+ pingPong bool
+ send chan []byte
+ messSize int64
+}
+
+type deadlineConn struct {
+ *websocket.Conn
+ wt time.Duration
+ mu sync.Mutex // needed for concurrent writes of Gorilla
+}
+
+type MessageHandler func([]byte, error)
+type ErrorHandler func(err error)
+
+type Upgrader struct {
+ websocket.Upgrader
+ Origin string
+}
+
+var DefaultDialer = websocket.DefaultDialer
+var DefaultUpgrader = Upgrader{Upgrader: websocket.Upgrader{
+ ReadBufferSize: 2048,
+ WriteBufferSize: 2048,
+ WriteBufferPool: &sync.Pool{},
+ EnableCompression: true,
+}}
+
+func NewUpgrader(origin string) *Upgrader {
+ u := DefaultUpgrader
+ switch {
+ case origin == "*":
+ u.CheckOrigin = func(r *http.Request) bool { return true }
+ case origin != "":
+ u.CheckOrigin = func(r *http.Request) bool { return r.Header.Get("Origin") == origin }
+ }
+ return &u
+}
+
+func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*websocket.Conn, error) {
+ if u.Origin != "" {
+ w.Header().Set("Access-Control-Allow-Origin", u.Origin)
+ }
+ return u.Upgrader.Upgrade(w, r, responseHeader)
+}
+
+func (s *Server) Connect(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Connection, error) {
+ u := s.Upgrader
+ if u == nil {
+ u = &DefaultUpgrader
+ }
+ conn, err := u.Upgrade(w, r, responseHeader)
+ if err != nil {
+ return nil, err
+ }
+ return newSocket(conn, true), nil
+}
+
+func (c *Client) Connect(address url.URL) (*Connection, error) {
+ dialer := c.Dialer
+ if dialer == nil {
+ dialer = DefaultDialer
+ }
if address.Scheme == "wss" {
dialer.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
- ws, _, err := dialer.Dial(address.String(), nil)
- return ws, err
+ conn, _, err := dialer.Dial(address.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+ return newSocket(conn, false), nil
+}
+
+func (conn *deadlineConn) write(t int, mess []byte) error {
+ conn.mu.Lock()
+ defer conn.mu.Unlock()
+ if err := conn.SetWriteDeadline(time.Now().Add(conn.wt)); err != nil {
+ return err
+ }
+ return conn.WriteMessage(t, mess)
+}
+
+func (conn *deadlineConn) writeControl(messageType int, data []byte, deadline time.Time) error {
+ conn.mu.Lock()
+ defer conn.mu.Unlock()
+ return conn.Conn.WriteControl(messageType, data, deadline)
+}
+
+// reader pumps messages from the websocket connection to the SetMessageHandler callback.
+// Blocking, must be called as goroutine. Serializes all websocket reads.
+func (c *Connection) reader() {
+ defer func() {
+ close(c.send)
+ c.close()
+ }()
+
+ 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 })
+ } else {
+ c.conn.SetPingHandler(func(string) error {
+ _ = c.conn.SetReadDeadline(time.Now().Add(pongTime))
+ err := c.conn.writeControl(websocket.PongMessage, nil, time.Now().Add(writeWait))
+ if err == websocket.ErrCloseSent {
+ return nil
+ } else if e, ok := err.(net.Error); ok && e.Timeout() {
+ return nil
+ }
+ return err
+ })
+ }
+ for {
+ _, 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
+ }
+ c.callback(message, err)
+ }
+}
+
+// writer pumps messages from the send channel to the websocket connection.
+// Blocking, must be called as goroutine. Serializes all websocket writes.
+func (c *Connection) writer() {
+ defer c.close()
+
+ if c.pingPong {
+ ticker := time.NewTicker(pingTime)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case message, ok := <-c.send:
+ if !c.handleMessage(message, ok) {
+ return
+ }
+ case <-ticker.C:
+ if err := c.conn.write(websocket.PingMessage, nil); err != nil {
+ return
+ }
+ }
+ }
+ } else {
+ for message := range c.send {
+ if !c.handleMessage(message, true) {
+ return
+ }
+ }
+ }
+}
+
+func (c *Connection) handleMessage(message []byte, ok bool) bool {
+ if !ok {
+ _ = c.conn.write(websocket.CloseMessage, nil)
+ return false
+ }
+ if err := c.conn.write(websocket.TextMessage, message); err != nil {
+ return false
+ }
+ return true
+}
+
+func (c *Connection) close() {
+ c.once.Do(func() {
+ c.alive = false
+ _ = c.conn.Close()
+ close(c.done)
+ })
+}
+
+func newSocket(conn *websocket.Conn, pingPong bool) *Connection {
+ return &Connection{
+ callback: func(message []byte, err error) {},
+ conn: deadlineConn{Conn: conn, wt: writeWait},
+ done: make(chan struct{}, 1),
+ once: sync.Once{},
+ pingPong: pingPong,
+ send: make(chan []byte),
+ }
+}
+
+// IsServer returns true if the connection has server capabilities and not just a client.
+// For now, we assume every connection with ping/pong handler is a server.
+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
+ }
+ c.alive = true
+ go c.writer()
+ go c.reader()
+ return c.done
+}
+
+func (c *Connection) Write(data []byte) {
+ if c.alive {
+ c.send <- data
+ }
+}
+
+func (c *Connection) Close() {
+ if c.alive {
+ _ = c.conn.write(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
+ }
}
diff --git a/pkg/os/flock.go b/pkg/os/flock.go
new file mode 100644
index 00000000..5dd7d499
--- /dev/null
+++ b/pkg/os/flock.go
@@ -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() }
diff --git a/pkg/os/os.go b/pkg/os/os.go
index ea4e0ecd..42e8a100 100644
--- a/pkg/os/os.go
+++ b/pkg/os/os.go
@@ -1,14 +1,35 @@
package os
import (
+ "bufio"
+ "bytes"
+ "errors"
+ "io"
+ "io/fs"
"os"
"os/signal"
+ "os/user"
"syscall"
)
-type Signal struct {
- event chan os.Signal
- done chan struct{}
+const ReadChunk = 1024
+
+var ErrNotExist = os.ErrNotExist
+
+func Exists(path string) bool {
+ _, err := os.Stat(path)
+ return !errors.Is(err, fs.ErrNotExist)
+}
+
+func CheckCreateDir(path string) error {
+ if !Exists(path) {
+ return os.MkdirAll(path, os.ModeDir|0755)
+ }
+ return nil
+}
+
+func MakeDirAll(path string) error {
+ return os.MkdirAll(path, os.ModeDir|os.ModePerm)
}
func ExpectTermination() chan struct{} {
@@ -21,3 +42,84 @@ func ExpectTermination() chan struct{} {
}()
return done
}
+
+func GetUserHome() (string, error) {
+ me, err := user.Current()
+ if err != nil {
+ return "", err
+ }
+ 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)
+}
+
+func ReadFile(name string) (dat []byte, err error) {
+ f, err := os.Open(name)
+ if err != nil {
+ return nil, err
+ }
+ defer func() { _ = f.Close() }()
+
+ r := bufio.NewReader(f)
+ buf := bytes.NewBuffer(make([]byte, 0))
+ chunk := make([]byte, ReadChunk)
+
+ c := 0
+ for {
+ if c, err = r.Read(chunk); err != nil {
+ break
+ }
+ buf.Write(chunk[:c])
+ }
+
+ if err == io.EOF {
+ err = nil
+ }
+
+ return buf.Bytes(), err
+}
+
+func StatSize(path string) (int64, error) {
+ fi, err := os.Stat(path)
+ if err != nil {
+ return 0, err
+ }
+ return fi.Size(), nil
+}
+
+func RemoveAll(path string) error {
+ return os.RemoveAll(path)
+}
diff --git a/pkg/resampler/simple.go b/pkg/resampler/simple.go
new file mode 100644
index 00000000..39e509c0
--- /dev/null
+++ b/pkg/resampler/simple.go
@@ -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]
+ }
+}
diff --git a/pkg/resampler/speex.go b/pkg/resampler/speex.go
new file mode 100644
index 00000000..b62d2be1
--- /dev/null
+++ b/pkg/resampler/speex.go
@@ -0,0 +1,106 @@
+package resampler
+
+/*
+ #cgo pkg-config: speexdsp
+ #cgo st LDFLAGS: -l:libspeexdsp.a
+
+ #include
+ #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))
+}
diff --git a/pkg/resampler/speex_resampler.h b/pkg/resampler/speex_resampler.h
new file mode 100644
index 00000000..9e046ed7
--- /dev/null
+++ b/pkg/resampler/speex_resampler.h
@@ -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
\ No newline at end of file
diff --git a/pkg/service/service.go b/pkg/service/service.go
deleted file mode 100644
index 84437a1a..00000000
--- a/pkg/service/service.go
+++ /dev/null
@@ -1,48 +0,0 @@
-package service
-
-import (
- "context"
- "log"
-)
-
-// Service defines a generic service.
-type Service interface{}
-
-// RunnableService defines a service that can be run.
-type RunnableService interface {
- Service
-
- Run()
- Shutdown(ctx context.Context) error
-}
-
-// Group is a container for managing a bunch of services.
-type Group struct {
- list []Service
-}
-
-func (g *Group) Add(services ...Service) {
- for _, s := range services {
- g.list = append(g.list, s)
- }
-}
-
-// Start starts each service in the group.
-func (g *Group) Start() {
- for _, s := range g.list {
- if v, ok := s.(RunnableService); ok {
- go v.Run()
- }
- }
-}
-
-// Shutdown terminates a group of services.
-func (g *Group) Shutdown(ctx context.Context) {
- for _, s := range g.list {
- if v, ok := s.(RunnableService); ok {
- if err := v.Shutdown(ctx); err != nil && err != context.Canceled {
- log.Printf("error: failed to stop [%s] because of %v", s, err)
- }
- }
- }
-}
diff --git a/pkg/session/session.go b/pkg/session/session.go
deleted file mode 100644
index f8fd2b58..00000000
--- a/pkg/session/session.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package session
-
-import (
- "math/rand"
- "strconv"
- "strings"
-)
-
-const separator = "___"
-
-// getGameNameFromRoomID parse roomID to get roomID and gameName
-func GetGameNameFromRoomID(roomID string) string {
- parts := strings.Split(roomID, separator)
- if len(parts) > 1 {
- return parts[1]
- }
- return ""
-}
-
-// generateRoomID generate a unique room ID containing 16 digits
-func GenerateRoomID(gameName string) string {
- // RoomID contains random number + gameName
- // Next time when we only get roomID, we can launch game based on gameName
- roomID := strconv.FormatInt(rand.Int63(), 16) + separator + gameName
- return roomID
-}
-
diff --git a/pkg/thread/thread.go b/pkg/thread/thread.go
deleted file mode 100644
index 5b1c3e34..00000000
--- a/pkg/thread/thread.go
+++ /dev/null
@@ -1,32 +0,0 @@
-// This package used for locking goroutines to
-// the main OS thread.
-// See: https://github.com/golang/go/wiki/LockOSThread
-package thread
-
-import (
- "runtime"
-
- "github.com/faiface/mainthread"
-)
-
-var isMacOs = runtime.GOOS == "darwin"
-
-// MainWrapMaybe enables functions to be executed in the main thread.
-// Enabled for macOS only.
-func MainWrapMaybe(f func()) {
- if isMacOs {
- mainthread.Run(f)
- } else {
- f()
- }
-}
-
-// MainMaybe calls a function on the main thread.
-// Enabled for macOS only.
-func MainMaybe(f func()) {
- if isMacOs {
- mainthread.Call(f)
- } else {
- f()
- }
-}
diff --git a/pkg/util/logging/init.go b/pkg/util/logging/init.go
deleted file mode 100644
index ebefcd33..00000000
--- a/pkg/util/logging/init.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package logging
-
-import (
- "flag"
- "log"
-
- "github.com/golang/glog"
- "github.com/spf13/pflag"
-)
-
-func init() {
- _ = flag.Set("logtostderr", "true")
-}
-
-// LogWriter serves as a bridge between the standard log package and the glog package.
-type LogWriter struct{}
-
-// Write implements the io.Writer interface.
-func (writer LogWriter) Write(data []byte) (n int, err error) {
- glog.InfoDepth(3, string(data))
- return len(data), nil
-}
-
-// Init initializes logs the way we want.
-func Init() {
- log.SetOutput(LogWriter{})
- log.SetFlags(0)
-
- pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
- pflag.Parse()
- // Convinces goflags that we have called Parse() to avoid noisy logs.
- _ = flag.CommandLine.Parse([]string{})
-}
-
-// Flush flushes logs immediately.
-func Flush() {
- glog.Flush()
-}
diff --git a/pkg/util/netutil.go b/pkg/util/netutil.go
deleted file mode 100644
index 855a7e78..00000000
--- a/pkg/util/netutil.go
+++ /dev/null
@@ -1,55 +0,0 @@
-package util
-
-import (
- "net"
- "strings"
-
- "github.com/gorilla/websocket"
-)
-
-// GetRemoteAddress returns public address of websocket connection
-func GetRemoteAddress(conn *websocket.Conn) string {
- var remoteAddr string
- // log.Println("Address :", conn.RemoteAddr().String())
- if parts := strings.Split(conn.RemoteAddr().String(), ":"); len(parts) == 2 {
- remoteAddr = parts[0]
- }
- if remoteAddr == "" {
- return "localhost"
- }
-
- return remoteAddr
-}
-
-// IsPublicIP checks if address is public address
-func IsPublicIP(address string) bool {
- ip := net.ParseIP(address)
- if ip.IsLoopback() || ip.IsLinkLocalMulticast() || ip.IsLinkLocalUnicast() {
- return false
- }
- if ip4 := ip.To4(); ip4 != nil {
- switch {
- case ip4[0] == 10:
- return false
- case ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31:
- return false
- case ip4[0] == 192 && ip4[1] == 168:
- return false
- default:
- return true
- }
- }
- return false
-}
-
-// GetHostPublicIP to get the public ip address. Only work if not behind NAT
-func GetHostPublicIP() string {
- conn, err := net.Dial("udp", "8.8.8.8:80")
- if err != nil {
- return ""
- }
- defer conn.Close()
- localAddr := conn.LocalAddr().String()
- idx := strings.LastIndex(localAddr, ":")
- return localAddr[0:idx]
-}
diff --git a/pkg/webrtc/connection.go b/pkg/webrtc/connection.go
deleted file mode 100644
index 7b46709f..00000000
--- a/pkg/webrtc/connection.go
+++ /dev/null
@@ -1,107 +0,0 @@
-package webrtc
-
-import (
- conf "github.com/giongto35/cloud-game/v2/pkg/config/webrtc"
- "github.com/pion/interceptor"
- . "github.com/pion/webrtc/v3"
-)
-
-func NewInterceptedPeerConnection(conf conf.Webrtc, ics []interceptor.Interceptor, vCodec string) (*PeerConnection, error) {
- m := &MediaEngine{}
- //if err := m.RegisterDefaultCodecs(); err != nil {
- // return nil, err
- //}
-
- if err := RegisterCodecs(m, vCodec); err != nil {
- return nil, err
- }
-
- i := &interceptor.Registry{}
- if !conf.DisableDefaultInterceptors {
- if err := RegisterDefaultInterceptors(m, i); err != nil {
- return nil, err
- }
- }
- for _, itc := range ics {
- i.Add(itc)
- }
-
- settingEngine := SettingEngine{}
- if conf.IcePorts.Min > 0 && conf.IcePorts.Max > 0 {
- if err := settingEngine.SetEphemeralUDPPortRange(conf.IcePorts.Min, conf.IcePorts.Max); err != nil {
- return nil, err
- }
- }
- if conf.IceIpMap != "" {
- settingEngine.SetNAT1To1IPs([]string{conf.IceIpMap}, ICECandidateTypeHost)
- }
-
- peerConf := Configuration{ICEServers: []ICEServer{}}
- for _, server := range conf.IceServers {
- peerConf.ICEServers = append(peerConf.ICEServers, ICEServer{
- URLs: []string{server.Url},
- Username: server.Username,
- Credential: server.Credential,
- })
- }
-
- api := NewAPI(WithMediaEngine(m), WithInterceptorRegistry(i), WithSettingEngine(settingEngine))
- return api.NewPeerConnection(peerConf)
-}
-
-// RegisterCodecs registers the default codecs supported by WebRTC.
-func RegisterCodecs(m *MediaEngine, vCodec string) error {
- audioRTPCodecParameters := []RTPCodecParameters{
- {
- RTPCodecCapability: RTPCodecCapability{MimeType: MimeTypeOpus, ClockRate: 48000, Channels: 2},
- PayloadType: 111,
- },
- }
- for _, codec := range audioRTPCodecParameters {
- if err := m.RegisterCodec(codec, RTPCodecTypeAudio); err != nil {
- return err
- }
- }
-
- videoRTCPFeedback := []RTCPFeedback{
- {"goog-remb", ""},
- {"ccm", "fir"},
- {"nack", ""},
- {"nack", "pli"},
- }
- video := RTPCodecCapability{MimeType: vCodec, ClockRate: 90000, RTCPFeedback: videoRTCPFeedback}
- var videoRTPCodecParameters []RTPCodecParameters
- if vCodec == MimeTypeH264 {
- videoRTPCodecParameters = []RTPCodecParameters{
- {RTPCodecCapability: RTPCodecCapability{
- MimeType: video.MimeType, ClockRate: video.ClockRate, RTCPFeedback: video.RTCPFeedback,
- //SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f",
- }, PayloadType: 102},
- {RTPCodecCapability: RTPCodecCapability{
- MimeType: video.MimeType, ClockRate: video.ClockRate, RTCPFeedback: video.RTCPFeedback,
- SDPFmtpLine: "level-asymmetry-allowed=1;profile-level-id=42e01f",
- }, PayloadType: 108},
- {RTPCodecCapability: video, PayloadType: 123},
- {RTPCodecCapability: RTPCodecCapability{
- MimeType: video.MimeType, ClockRate: video.ClockRate, RTCPFeedback: video.RTCPFeedback,
- SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
- }, PayloadType: 125},
- {RTPCodecCapability: RTPCodecCapability{
- MimeType: video.MimeType, ClockRate: video.ClockRate, RTCPFeedback: video.RTCPFeedback,
- SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f",
- }, PayloadType: 127},
- }
- } else {
- videoRTPCodecParameters = []RTPCodecParameters{
- {RTPCodecCapability: video, PayloadType: 96},
- }
- }
-
- for _, codec := range videoRTPCodecParameters {
- if err := m.RegisterCodec(codec, RTPCodecTypeVideo); err != nil {
- return err
- }
- }
-
- return nil
-}
diff --git a/pkg/webrtc/interceptor/retime.go b/pkg/webrtc/interceptor/retime.go
deleted file mode 100644
index a3b2f47d..00000000
--- a/pkg/webrtc/interceptor/retime.go
+++ /dev/null
@@ -1,36 +0,0 @@
-package interceptor
-
-import (
- "strings"
- "sync/atomic"
-
- . "github.com/pion/interceptor"
- "github.com/pion/rtp"
-)
-
-// ReTime interceptor replaces timestamps of all outgoing video packets.
-type ReTime struct {
- NoOp
- timestamp uint32
-}
-
-// BindLocalStream modifies any outgoing RTP packets.
-func (i *ReTime) BindLocalStream(info *StreamInfo, writer RTPWriter) RTPWriter {
- // use with video packets only
- if strings.HasPrefix(info.MimeType, "video/") {
- return RTPWriterFunc(func(header *rtp.Header, payload []byte, attributes Attributes) (int, error) {
- h := *header
- h.Timestamp = i.GetTimestamp()
- return writer.Write(&h, payload, attributes)
- })
- }
- return writer
-}
-
-func (i *ReTime) SetTimestamp(ts uint32) {
- atomic.StoreUint32(&i.timestamp, ts)
-}
-
-func (i *ReTime) GetTimestamp() uint32 {
- return atomic.LoadUint32(&i.timestamp)
-}
diff --git a/pkg/webrtc/webrtc.go b/pkg/webrtc/webrtc.go
deleted file mode 100644
index 40a59873..00000000
--- a/pkg/webrtc/webrtc.go
+++ /dev/null
@@ -1,393 +0,0 @@
-// credit to https://github.com/poi5305/go-yuv2webRTC/blob/master/webrtc/webrtc.go
-package webrtc
-
-import (
- "encoding/base64"
- "encoding/json"
- "fmt"
- "log"
- "runtime/debug"
- "time"
-
- "github.com/giongto35/cloud-game/v2/pkg/codec"
- webrtcConfig "github.com/giongto35/cloud-game/v2/pkg/config/webrtc"
- itc "github.com/giongto35/cloud-game/v2/pkg/webrtc/interceptor"
- "github.com/gofrs/uuid"
- "github.com/pion/interceptor"
- "github.com/pion/webrtc/v3"
- "github.com/pion/webrtc/v3/pkg/media"
-)
-
-type WebFrame struct {
- Data []byte
- Timestamp uint32
-}
-
-// WebRTC connection
-type WebRTC struct {
- ID string
-
- connection *webrtc.PeerConnection
- cfg webrtcConfig.Config
- tsInterceptor itc.ReTime
- isConnected bool
- // for yuvI420 image
- ImageChannel chan WebFrame
- AudioChannel chan []byte
- //VoiceInChannel chan []byte
- //VoiceOutChannel chan []byte
- InputChannel chan []byte
-
- Done bool
- //lastTime time.Time
- //curFPS int
-
- RoomID string
-
- // store thing related to game
- GameMeta GameMeta
-}
-
-// Game Meta
-type GameMeta struct {
- PlayerIndex int
-}
-
-type OnIceCallback func(candidate string)
-
-// Encode encodes the input in base64
-func Encode(obj interface{}) (string, error) {
- b, err := json.Marshal(obj)
- if err != nil {
- return "", err
- }
-
- return base64.StdEncoding.EncodeToString(b), nil
-}
-
-// Decode decodes the input from base64
-func Decode(in string, obj interface{}) error {
- b, err := base64.StdEncoding.DecodeString(in)
- if err != nil {
- return err
- }
-
- err = json.Unmarshal(b, obj)
- if err != nil {
- return err
- }
-
- return nil
-}
-
-// NewWebRTC create
-func NewWebRTC() *WebRTC {
- w := &WebRTC{
- ID: uuid.Must(uuid.NewV4()).String(),
-
- ImageChannel: make(chan WebFrame, 30),
- AudioChannel: make(chan []byte, 1),
- //VoiceInChannel: make(chan []byte, 1),
- //VoiceOutChannel: make(chan []byte, 1),
- InputChannel: make(chan []byte, 100),
- }
- return w
-}
-
-func (w *WebRTC) WithConfig(conf webrtcConfig.Config) *WebRTC {
- w.cfg = conf
- return w
-}
-
-// StartClient start webrtc
-func (w *WebRTC) StartClient(iceCB OnIceCallback) (string, error) {
- defer func() {
- if err := recover(); err != nil {
- log.Println(err)
- w.StopClient()
- }
- }()
- var err error
- var videoTrack *webrtc.TrackLocalStaticSample
-
- // reset client
- if w.isConnected {
- w.StopClient()
- time.Sleep(2 * time.Second)
- }
-
- log.Println("=== StartClient ===")
-
- videoCodec := w.getVideoCodec()
- w.tsInterceptor = itc.ReTime{}
- w.connection, err = NewInterceptedPeerConnection(w.cfg.Webrtc, []interceptor.Interceptor{&w.tsInterceptor}, videoCodec)
- if err != nil {
- return "", err
- }
-
- // add video track
- rtpCodec := webrtc.RTPCodecCapability{MimeType: videoCodec}
- if videoTrack, err = webrtc.NewTrackLocalStaticSample(rtpCodec, "video", "game-video"); err != nil {
- return "", err
- }
-
- if _, err = w.connection.AddTrack(videoTrack); err != nil {
- return "", err
- }
- log.Println("Add video track")
-
- // add audio track
- opusTrack, err := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus}, "audio", "game-audio")
- if err != nil {
- return "", err
- }
- _, err = w.connection.AddTrack(opusTrack)
- if err != nil {
- return "", err
- }
-
- //_, err = w.connection.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio, webrtc.RtpTransceiverInit{Direction: webrtc.RTPTransceiverDirectionRecvonly})
-
- // create data channel for input, and register callbacks
- // order: true, negotiated: false, id: random
- inputTrack, err := w.connection.CreateDataChannel("game-input", nil)
- if err != nil {
- return "", err
- }
-
- inputTrack.OnOpen(func() {
- log.Printf("Data channel '%s'-'%d' open.\n", inputTrack.Label(), inputTrack.ID())
- })
-
- // Register text message handling
- inputTrack.OnMessage(func(msg webrtc.DataChannelMessage) {
- // TODO: Can add recover here
- w.InputChannel <- msg.Data
- })
-
- inputTrack.OnClose(func() {
- log.Println("Data channel closed")
- log.Println("Closed webrtc")
- })
-
- // WebRTC state callback
- w.connection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
- log.Printf("ICE Connection State has changed: %s\n", connectionState.String())
- if connectionState == webrtc.ICEConnectionStateConnected {
- go func() {
- w.isConnected = true
- log.Println("ConnectionStateConnected")
- w.startStreaming(videoTrack, opusTrack)
- }()
-
- }
- if connectionState == webrtc.ICEConnectionStateFailed || connectionState == webrtc.ICEConnectionStateClosed || connectionState == webrtc.ICEConnectionStateDisconnected {
- w.StopClient()
- }
- })
-
- w.connection.OnICECandidate(func(iceCandidate *webrtc.ICECandidate) {
- if iceCandidate != nil {
- log.Println("OnIceCandidate:", iceCandidate.ToJSON().Candidate)
- candidate, err := Encode(iceCandidate.ToJSON())
- if err != nil {
- log.Println("Encode IceCandidate failed: " + iceCandidate.ToJSON().Candidate)
- return
- }
- iceCB(candidate)
- } else {
- // finish, send null
- iceCB("")
- }
-
- })
-
- w.connection.OnTrack(func(remoteTrack *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
- //NOTE: High CPU due to constantly for loop. Turn it off first, Fix it later.
- //rtpBuf := make([]byte, 1400)
-
- //log.Println("Received Voice from Client")
- //for {
- //if w.RoomID == "" {
- //// skip sending voice when game is not running
- //continue
- //}
-
- //i, err := remoteTrack.Read(rtpBuf)
- //// TODO: can receive track but the voice doesn't work
- //if err == nil {
- //w.VoiceInChannel <- rtpBuf[:i]
- //}
- //}
-
- })
-
- // Stream provider supposes to send offer
- offer, err := w.connection.CreateOffer(nil)
- if err != nil {
- return "", err
- }
- log.Println("Created Offer")
-
- err = w.connection.SetLocalDescription(offer)
- if err != nil {
- return "", err
- }
-
- localSession, err := Encode(offer)
- if err != nil {
- return "", err
- }
-
- return localSession, nil
-}
-
-func (w *WebRTC) getVideoCodec() string {
- switch w.cfg.Encoder.Video.Codec {
- case string(codec.H264):
- return webrtc.MimeTypeH264
- case string(codec.VPX):
- return webrtc.MimeTypeVP8
- default:
- return webrtc.MimeTypeH264
- }
-}
-
-func (w *WebRTC) AttachRoomID(roomID string) {
- w.RoomID = roomID
-}
-
-func (w *WebRTC) SetRemoteSDP(remoteSDP string) error {
- var answer webrtc.SessionDescription
- err := Decode(remoteSDP, &answer)
- if err != nil {
- log.Println("Decode remote sdp from peer failed")
- return err
- }
-
- err = w.connection.SetRemoteDescription(answer)
- if err != nil {
- log.Println("Set remote description from peer failed")
- return err
- }
-
- log.Println("Set Remote Description")
- return nil
-}
-
-func (w *WebRTC) AddCandidate(candidate string) error {
- var iceCandidate webrtc.ICECandidateInit
- err := Decode(candidate, &iceCandidate)
- if err != nil {
- log.Println("Decode Ice candidate from peer failed")
- return err
- }
- log.Println("Decoded Ice: " + iceCandidate.Candidate)
-
- err = w.connection.AddICECandidate(iceCandidate)
- if err != nil {
- log.Println("Add Ice candidate from peer failed")
- return err
- }
-
- log.Println("Add Ice Candidate: " + iceCandidate.Candidate)
- return nil
-}
-
-// StopClient disconnect
-func (w *WebRTC) StopClient() {
- // if stopped, bypass
- if !w.isConnected {
- return
- }
-
- log.Println("===StopClient===")
- w.isConnected = false
- if w.connection != nil {
- w.connection.Close()
- }
- w.connection = nil
- //close(w.InputChannel)
- // webrtc is producer, so we close
- // NOTE: ImageChannel is waiting for input. Close in writer is not correct for this
- close(w.ImageChannel)
- close(w.AudioChannel)
- //close(w.VoiceInChannel)
- //close(w.VoiceOutChannel)
-}
-
-// IsConnected comment
-func (w *WebRTC) IsConnected() bool {
- return w.isConnected
-}
-
-func (w *WebRTC) startStreaming(vp8Track *webrtc.TrackLocalStaticSample, opusTrack *webrtc.TrackLocalStaticSample) {
- log.Println("Start streaming")
- // receive frame buffer
- go func() {
- defer func() {
- if r := recover(); r != nil {
- fmt.Println("Recovered from err", r)
- log.Println(debug.Stack())
- }
- }()
-
- for data := range w.ImageChannel {
- w.tsInterceptor.SetTimestamp(data.Timestamp)
- if err := vp8Track.WriteSample(media.Sample{Data: data.Data}); err != nil {
- log.Println("Warn: Err write sample: ", err)
- break
- }
- }
- }()
-
- // send audio
- go func() {
- defer func() {
- if r := recover(); r != nil {
- fmt.Println("Recovered from err", r)
- log.Println(debug.Stack())
- }
- }()
-
- audioDuration := time.Duration(w.cfg.Encoder.Audio.Frame) * time.Millisecond
- for data := range w.AudioChannel {
- if !w.isConnected {
- return
- }
- err := opusTrack.WriteSample(media.Sample{Data: data, Duration: audioDuration})
- if err != nil {
- log.Println("Warn: Err write sample: ", err)
- }
- }
- }()
-
- //// send voice
- //go func() {
- // defer func() {
- // if r := recover(); r != nil {
- // fmt.Println("Recovered from err", r)
- // log.Println(debug.Stack())
- // }
- // }()
- //
- // for data := range w.VoiceOutChannel {
- // if !w.isConnected {
- // return
- // }
- // // !to pass duration from the input
- // err := opusTrack.WriteSample(media.Sample{Data: data})
- // if err != nil {
- // log.Println("Warn: Err write sample: ", err)
- // }
- // }
- //}()
-}
-
-//func (w *WebRTC) calculateFPS() int {
-// elapsedTime := time.Now().Sub(w.lastTime)
-// w.lastTime = time.Now()
-// curFPS := time.Second / elapsedTime
-// w.curFPS = int(float32(w.curFPS)*0.9 + float32(curFPS)*0.1)
-// return w.curFPS
-//}
diff --git a/pkg/worker/caged/app/app.go b/pkg/worker/caged/app/app.go
new file mode 100644
index 00000000..74d89432
--- /dev/null
+++ b/pkg/worker/caged/app/app.go
@@ -0,0 +1,34 @@
+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))
+ SetDataCb(func([]byte))
+ Input(port int, device byte, data []byte)
+ KbMouseSupport() bool
+}
+
+type Audio struct {
+ Data []int16
+ Duration int32 // up to 6y nanosecond-wise
+}
+
+type Video struct {
+ Frame RawFrame
+ Duration int32
+}
+
+type RawFrame struct {
+ Data []byte
+ Stride int
+ W, H int
+}
diff --git a/pkg/worker/caged/caged.go b/pkg/worker/caged/caged.go
new file mode 100644
index 00000000..85ede127
--- /dev/null
+++ b/pkg/worker/caged/caged.go
@@ -0,0 +1,67 @@
+package caged
+
+import (
+ "errors"
+ "reflect"
+
+ "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/app"
+ "github.com/giongto35/cloud-game/v3/pkg/worker/caged/libretro"
+)
+
+type Manager struct {
+ list map[ModName]app.App
+ log *logger.Logger
+}
+
+const (
+ RetroPad = libretro.RetroPad
+ Keyboard = libretro.Keyboard
+ Mouse = libretro.Mouse
+)
+
+type ModName string
+
+const Libretro ModName = "libretro"
+
+func NewManager(log *logger.Logger) *Manager {
+ return &Manager{log: log, list: make(map[ModName]app.App)}
+}
+
+func (m *Manager) Get(name ModName) app.App { return m.list[name] }
+
+func (m *Manager) Load(name ModName, conf any) error {
+ if name == Libretro {
+ caged, err := m.loadLibretro(conf)
+ if err != nil {
+ return err
+ }
+ m.list[name] = caged
+ }
+ return nil
+}
+
+func (m *Manager) loadLibretro(conf any) (*libretro.Caged, error) {
+ s := reflect.ValueOf(conf)
+
+ e := s.FieldByName("Emulator")
+ if !e.IsValid() {
+ return nil, errors.New("no emulator conf")
+ }
+ r := s.FieldByName("Recording")
+ if !r.IsValid() {
+ return nil, errors.New("no recording conf")
+ }
+
+ c := libretro.CagedConf{
+ Emulator: e.Interface().(config.Emulator),
+ Recording: r.Interface().(config.Recording),
+ }
+
+ caged := libretro.Cage(c, m.log)
+ if err := caged.Init(); err != nil {
+ return nil, err
+ }
+ return &caged, nil
+}
diff --git a/pkg/worker/caged/libretro/caged.go b/pkg/worker/caged/libretro/caged.go
new file mode 100644
index 00000000..3d21db11
--- /dev/null
+++ b/pkg/worker/caged/libretro/caged.go
@@ -0,0 +1,102 @@
+package libretro
+
+import (
+ "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/worker/caged/libretro/manager"
+ "github.com/giongto35/cloud-game/v3/pkg/worker/cloud"
+)
+
+type Caged struct {
+ Emulator
+
+ base *Frontend // maintains the root for mad embedding
+ conf CagedConf
+ log *logger.Logger
+}
+
+type CagedConf struct {
+ Emulator config.Emulator
+ Recording config.Recording
+}
+
+func (c *Caged) Name() string { return "libretro" }
+
+func Cage(conf CagedConf, log *logger.Logger) Caged {
+ return Caged{conf: conf, log: log}
+}
+
+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
+}
+
+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
+}
+
+// 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
+ }
+ c.ViewportRecalculate()
+ return nil
+}
+
+func (c *Caged) EnableRecording(nowait bool, user string, game string) {
+ if c.conf.Recording.Enabled {
+ // !to fix races with canvas pool when recording
+ c.base.DisableCanvasPool = true
+ c.Emulator = WithRecording(c.Emulator, nowait, user, game, c.conf.Recording, c.log)
+ }
+}
+
+func (c *Caged) EnableCloudStorage(uid string, storage cloud.Storage) {
+ 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) 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() }
diff --git a/pkg/worker/caged/libretro/cloud.go b/pkg/worker/caged/libretro/cloud.go
new file mode 100644
index 00000000..67f8da14
--- /dev/null
+++ b/pkg/worker/caged/libretro/cloud.go
@@ -0,0 +1,60 @@
+package libretro
+
+import (
+ "github.com/giongto35/cloud-game/v3/pkg/os"
+ "github.com/giongto35/cloud-game/v3/pkg/worker/cloud"
+)
+
+type CloudFrontend struct {
+ Emulator
+ uid string
+ storage cloud.Storage // a cloud storage to store room state online
+}
+
+// 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}
+
+ 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.SaveStateName())
+ if err == nil {
+ return true
+ }
+ return c.Emulator.HasSave()
+}
+
+func (c *CloudFrontend) SaveGameState() error {
+ if err := c.Emulator.SaveGameState(); err != nil {
+ return err
+ }
+ path := c.Emulator.HashPath()
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return err
+ }
+ return c.storage.Save(c.SaveStateName(), data, map[string]string{
+ "uid": c.uid,
+ "type": "cloudretro-main-save",
+ })
+}
diff --git a/pkg/worker/caged/libretro/frontend.go b/pkg/worker/caged/libretro/frontend.go
new file mode 100644
index 00000000..b3baecde
--- /dev/null
+++ b/pkg/worker/caged/libretro/frontend.go
@@ -0,0 +1,545 @@
+package libretro
+
+import (
+ "errors"
+ "fmt"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "sync"
+ "time"
+ "unsafe"
+
+ "github.com/giongto35/cloud-game/v3/pkg/config"
+ "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
+ Flipped() bool
+ Rotation() uint
+ PixFormat() uint32
+ AudioSampleRate() int
+ IsPortrait() bool
+ // Start is called after LoadGame
+ Start()
+ // 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()
+ // Input passes input to the emulator
+ 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{}
+ 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
+
+ // 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
+}
+
+type Device byte
+
+const (
+ 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.
+func NewFrontend(conf config.Emulator, log *logger.Logger) (*Frontend, error) {
+ path, err := filepath.Abs(conf.LocalPath)
+ if err != nil {
+ return nil, fmt.Errorf("failed to use emulator path: %v, %w", conf.LocalPath, err)
+ }
+ if err := os.CheckCreateDir(path); err != nil {
+ return nil, fmt.Errorf("failed to create local path: %v, %w", conf.LocalPath, err)
+ }
+ log.Info().Msgf("Emulator save path is %v", path)
+
+ // we use the global Nanoarch instance from nanoarch
+ nano := nanoarch.NewNano(path)
+
+ log = log.Extend(log.With().Str("m", "Libretro"))
+ 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)
+ if err := os.CheckCreateDir(conf.Storage); err != nil {
+ return nil, fmt.Errorf("failed to create local storage path: %v, %w", conf.Storage, err)
+ }
+
+ var store Storage = &StateStorage{Path: conf.Storage}
+ if conf.Libretro.SaveCompression {
+ store = &ZipStorage{Storage: store}
+ }
+
+ // set global link to the Libretro
+ f := &Frontend{
+ conf: conf,
+ done: make(chan struct{}),
+ 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,
+ 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()
+}
+
+func (f *Frontend) handleAudio(audio unsafe.Pointer, samples int) {
+ fr, _ := audioPool.Get().(*app.Audio)
+ if fr == nil {
+ fr = new(app.Audio)
+ }
+ // !to look if we need a copy
+ fr.Data = unsafe.Slice((*int16)(audio), samples)
+ // due to audio buffering for opus fixed frames and const duration up in the hierarchy,
+ // we skip Duration here
+ f.onAudio(*fr)
+ audioPool.Put(fr)
+}
+
+func (f *Frontend) handleVideo(data []byte, delta int32, fi nanoarch.FrameInfo) {
+ if f.conf.SkipLateFrames && f.skipVideo {
+ return
+ }
+
+ fr, _ := videoPool.Get().(*app.Video)
+ if fr == nil {
+ fr = new(app.Video)
+ }
+ fr.Frame.Data = data
+ fr.Frame.W = int(fi.W)
+ 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 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.OnVideo = f.handleVideo
+ f.nano.OnAudio = f.handleAudio
+ f.nano.OnDup = f.handleDup
+}
+
+func (f *Frontend) SetVideoChangeCb(fn func()) {
+ if f.nano != nil {
+ f.nano.OnSystemAvInfo = fn
+ }
+}
+
+func (f *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 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, 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")
+ }
+ }
+
+ 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 <-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) LoadGame(path string) error {
+ if f.UniqueSaveDir {
+ f.copyFsMaybe(path)
+ }
+ return f.nano.LoadGame(path)
+}
+
+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()
+ nw, nh = w, h
+
+ if f.IsPortrait() {
+ nw, nh = nh, nw
+ }
+
+ f.log.Debug().Msgf("viewport: %dx%d -> %dx%d", w, h, nw, nh)
+
+ return
+}
+
+func (f *Frontend) Close() {
+ f.log.Debug().Msgf("frontend close")
+ close(f.done)
+
+ 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)
+ }
+ }
+
+ f.UniqueSaveDir = false
+ f.SaveStateFs = ""
+
+ f.mui.Unlock()
+ f.log.Debug().Msgf("frontend closed")
+}
+
+// Save writes the current state to the filesystem.
+func (f *Frontend) Save() error {
+ f.mu.Lock()
+ defer f.mu.Unlock()
+
+ ss, err := nanoarch.SaveState()
+ if err != nil {
+ return err
+ }
+ if err := f.storage.Save(f.HashPath(), ss); err != nil {
+ return err
+ }
+ ss = nil
+
+ if sram := nanoarch.SaveRAM(); sram != nil {
+ if err := f.storage.Save(f.SRAMPath(), sram); err != nil {
+ return err
+ }
+ sram = nil
+ }
+ return nil
+}
+
+// Load restores the state from the filesystem.
+func (f *Frontend) Load() error {
+ f.mu.Lock()
+ defer f.mu.Unlock()
+
+ ss, err := f.storage.Load(f.HashPath())
+ if err != nil && !errors.Is(err, os.ErrNotExist) {
+ return err
+ }
+ if err := nanoarch.RestoreSaveState(ss); err != nil {
+ return err
+ }
+
+ sram, err := f.storage.Load(f.SRAMPath())
+ if err != nil && !errors.Is(err, os.ErrNotExist) {
+ return err
+ }
+ if sram != nil {
+ nanoarch.RestoreSaveRAM(sram)
+ }
+ 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)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-ticker.C:
+ if f.nano.IsStopped() {
+ return
+ }
+ if err := f.Save(); err != nil {
+ f.log.Error().Msgf("Autosave failed: %v", err)
+ } else {
+ f.log.Debug().Msgf("Autosave done")
+ }
+ case <-f.done:
+ return
+ }
+ }
+}
+
+func (f *Frontend) copyFsMaybe(path string) {
+ if f.SaveStateFs == "" {
+ return
+ }
+
+ 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)
+ }
+ }
+}
diff --git a/pkg/worker/caged/libretro/frontend_test.go b/pkg/worker/caged/libretro/frontend_test.go
new file mode 100644
index 00000000..2cacd5a4
--- /dev/null
+++ b/pkg/worker/caged/libretro/frontend_test.go
@@ -0,0 +1,377 @@
+package libretro
+
+import (
+ "crypto/md5"
+ "fmt"
+ "io"
+ "log"
+ "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"
+ "github.com/giongto35/cloud-game/v3/pkg/worker/caged/app"
+ "github.com/giongto35/cloud-game/v3/pkg/worker/caged/libretro/manager"
+ "github.com/giongto35/cloud-game/v3/pkg/worker/caged/libretro/nanoarch"
+ "github.com/giongto35/cloud-game/v3/pkg/worker/thread"
+
+ _ "github.com/giongto35/cloud-game/v3/test"
+)
+
+type TestFrontend struct {
+ *Frontend
+
+ corePath string
+ coreExt string
+ gamePath string
+ system string
+}
+
+type testRun struct {
+ name string
+ room string
+ system string
+ rom string
+ frames int
+}
+
+type game struct {
+ rom string
+ system string
+}
+
+var (
+ 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.
+func TestMain(m *testing.M) {
+ thread.Wrap(func() { os.Exit(m.Run()) })
+}
+
+// EmulatorMock returns a properly stubbed emulator instance.
+// Due to extensive use of globals -- one mock instance is allowed per a test run.
+// Don't forget to init one image channel consumer, it will lock-out otherwise.
+// Make sure you call Shutdown().
+func EmulatorMock(room string, system string) *TestFrontend {
+ var conf config.WorkerConfig
+ if _, err := config.LoadConfig(&conf, ""); err != nil {
+ panic(err)
+ }
+
+ conf.Emulator.Libretro.Cores.Repo.ExtLock = expand("tests", ".cr", "cloud-game.lock")
+ conf.Emulator.LocalPath = expand("tests", conf.Emulator.LocalPath)
+ conf.Emulator.Storage = expand("tests", "storage")
+
+ l := logger.Default()
+ l2 := l.Extend(l.Level(logger.WarnLevel).With())
+
+ 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{
+ conf: conf.Emulator,
+ storage: &StateStorage{
+ Path: os.TempDir(),
+ MainSave: room,
+ },
+ done: make(chan struct{}),
+ th: conf.Emulator.Threads,
+ log: l2,
+ SaveOnClose: false,
+ },
+ corePath: expand(conf.Emulator.GetLibretroCoreConfig(system).Lib),
+ coreExt: arch.Ext,
+ gamePath: expand(conf.Library.BasePath),
+ system: system,
+ }
+ emu.linkNano(nano)
+
+ return emu
+}
+
+// DefaultFrontend returns initialized emulator mock with default params.
+// Spawns audio/image channels consumers.
+// Don't forget to close emulator mock with Shutdown().
+func DefaultFrontend(room string, system string, rom string) *TestFrontend {
+ mock := EmulatorMock(room, system)
+ mock.loadRom(rom)
+ mock.SetVideoCb(func(app.Video) {})
+ mock.SetAudioCb(func(app.Audio) {})
+ return mock
+}
+
+// loadRom loads a ROM into the emulator.
+// The rom will be loaded from emulators' games path.
+func (emu *TestFrontend) loadRom(game string) {
+ 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)
+ }
+ emu.ViewportRecalculate()
+}
+
+// Shutdown closes the emulator and cleans its resources.
+func (emu *TestFrontend) Shutdown() {
+ _ = os.Remove(emu.HashPath())
+ _ = os.Remove(emu.SRAMPath())
+ emu.Frontend.Close()
+ emu.Frontend.Shutdown()
+}
+
+// dumpState returns both current and previous emulator save state as MD5 hash string.
+func (emu *TestFrontend) dumpState() (cur string, prev string) {
+ emu.mu.Lock()
+ b, _ := os.ReadFile(emu.HashPath())
+ prev = hash(b)
+ emu.mu.Unlock()
+
+ emu.mu.Lock()
+ b, _ = nanoarch.SaveState()
+ emu.mu.Unlock()
+ cur = hash(b)
+
+ return
+}
+
+func (emu *TestFrontend) save() ([]byte, error) {
+ emu.mu.Lock()
+ defer emu.mu.Unlock()
+
+ return nanoarch.SaveState()
+}
+
+func BenchmarkEmulators(b *testing.B) {
+ log.SetOutput(io.Discard)
+ os.Stdout, _ = os.Open(os.DevNull)
+
+ benchmarks := []struct {
+ name string
+ system string
+ rom string
+ }{
+ {name: "GBA Sushi", system: sushi.system, rom: sushi.rom},
+ {name: "NES Alwa", system: alwa.system, rom: alwa.rom},
+ }
+
+ for _, bench := range benchmarks {
+ b.Run(bench.name, func(b *testing.B) {
+ s := DefaultFrontend("bench_"+bench.system+"_performance", bench.system, bench.rom)
+ for range b.N {
+ s.nano.Run()
+ }
+ s.Shutdown()
+ })
+ }
+}
+
+func TestSavePersistence(t *testing.T) {
+ tests := []testRun{
+ {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.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)
+
+ for test.frames > 0 {
+ front.Tick()
+ test.frames--
+ }
+
+ for range 10 {
+ v, _ := front.save()
+ if v == nil || len(v) == 0 {
+ t.Errorf("couldn't persist the state")
+ t.Fail()
+ }
+ }
+
+ front.Shutdown()
+ })
+ }
+}
+
+// Tests save and restore function:
+//
+// Emulate n ticks.
+// Call save (a).
+// Emulate n ticks again.
+// Call load from the save (b).
+// Compare states (a) and (b), should be =.
+func TestLoad(t *testing.T) {
+ tests := []testRun{
+ {room: "test_load_00", system: 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 {
+ t.Logf("Testing [%v] load with [%v]\n", test.system, test.rom)
+
+ mock := DefaultFrontend(test.room, test.system, test.rom)
+
+ mock.dumpState()
+
+ for ticks := test.frames; ticks > 0; ticks-- {
+ mock.Tick()
+ }
+ mock.dumpState()
+
+ if err := mock.Save(); err != nil {
+ t.Errorf("Save fail %v", err)
+ }
+ snapshot1, _ := mock.dumpState()
+
+ for ticks := test.frames; ticks > 0; ticks-- {
+ mock.Tick()
+ }
+ mock.dumpState()
+
+ if err := mock.Load(); err != nil {
+ t.Errorf("Load fail %v", err)
+ }
+ snapshot2, _ := mock.dumpState()
+
+ if snapshot1 != snapshot2 {
+ t.Errorf("It seems rom state restore has failed: %v != %v", snapshot1, snapshot2)
+ }
+
+ mock.Shutdown()
+ }
+}
+
+func TestStateConcurrency(t *testing.T) {
+ tests := []struct {
+ run testRun
+ seed int
+ }{
+ {
+ run: testRun{room: "test_concurrency_00", system: alwa.system, rom: alwa.rom, frames: 120},
+ seed: 42,
+ },
+ {
+ run: testRun{room: "test_concurrency_01", system: alwa.system, rom: alwa.rom, frames: 300},
+ seed: 42 + 42,
+ },
+ }
+
+ for _, test := range tests {
+ t.Logf("Testing [%v] concurrency with [%v]\n", test.run.system, test.run.rom)
+
+ mock := EmulatorMock(test.run.room, test.run.system)
+
+ ops := &sync.WaitGroup{}
+ // quantum lock
+ qLock := &sync.Mutex{}
+
+ mock.loadRom(test.run.rom)
+ mock.SetVideoCb(func(v app.Video) {
+ if len(v.Frame.Data) == 0 {
+ t.Errorf("It seems that rom video frame was empty, which is strange!")
+ }
+ })
+ mock.SetAudioCb(func(app.Audio) {})
+
+ t.Logf("Random seed is [%v]\n", test.seed)
+ t.Logf("Save path is [%v]\n", mock.HashPath())
+
+ _ = mock.Save()
+
+ for i := range test.run.frames {
+ qLock.Lock()
+ mock.Tick()
+ qLock.Unlock()
+
+ if lucky() && !lucky() {
+ ops.Go(func() {
+ qLock.Lock()
+ defer qLock.Unlock()
+
+ mock.dumpState()
+ // remove save to reproduce the bug
+ _ = mock.Save()
+ _, snapshot1 := mock.dumpState()
+ _ = mock.Load()
+ snapshot2, _ := mock.dumpState()
+
+ if snapshot1 != snapshot2 {
+ t.Errorf("States are inconsistent %v != %v on tick %v\n", snapshot1, snapshot2, i+1)
+ }
+ })
+ }
+ }
+
+ ops.Wait()
+ mock.Shutdown()
+ }
+}
+
+func TestStartStop(t *testing.T) {
+ f1 := DefaultFrontend("sushi", sushi.system, sushi.rom)
+ go f1.Start()
+ time.Sleep(1 * time.Second)
+ f1.Close()
+
+ 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.
+func expand(p ...string) string {
+ ph, _ := filepath.Abs(filepath.FromSlash(filepath.Join(p...)))
+ return ph
+}
+
+// hash returns MD5 hash.
+func hash(bytes []byte) string { return fmt.Sprintf("%x", md5.Sum(bytes)) }
+
+// lucky returns random boolean.
+func lucky() bool { return rand.IntN(2) == 1 }
diff --git a/pkg/worker/caged/libretro/graphics/gl/KHR/khrplatform.h b/pkg/worker/caged/libretro/graphics/gl/KHR/khrplatform.h
new file mode 100644
index 00000000..01646449
--- /dev/null
+++ b/pkg/worker/caged/libretro/graphics/gl/KHR/khrplatform.h
@@ -0,0 +1,311 @@
+#ifndef __khrplatform_h_
+#define __khrplatform_h_
+
+/*
+** Copyright (c) 2008-2018 The Khronos Group Inc.
+**
+** Permission is hereby granted, free of charge, to any person obtaining a
+** copy of this software and/or associated documentation files (the
+** "Materials"), to deal in the Materials without restriction, including
+** without limitation the rights to use, copy, modify, merge, publish,
+** distribute, sublicense, and/or sell copies of the Materials, and to
+** permit persons to whom the Materials are furnished to do so, subject to
+** the following conditions:
+**
+** The above copyright notice and this permission notice shall be included
+** in all copies or substantial portions of the Materials.
+**
+** THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+** MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
+*/
+
+/* Khronos platform-specific types and definitions.
+ *
+ * The master copy of khrplatform.h is maintained in the Khronos EGL
+ * Registry repository at https://github.com/KhronosGroup/EGL-Registry
+ * The last semantic modification to khrplatform.h was at commit ID:
+ * 67a3e0864c2d75ea5287b9f3d2eb74a745936692
+ *
+ * Adopters may modify this file to suit their platform. Adopters are
+ * encouraged to submit platform specific modifications to the Khronos
+ * group so that they can be included in future versions of this file.
+ * Please submit changes by filing pull requests or issues on
+ * the EGL Registry repository linked above.
+ *
+ *
+ * See the Implementer's Guidelines for information about where this file
+ * should be located on your system and for more details of its use:
+ * http://www.khronos.org/registry/implementers_guide.pdf
+ *
+ * This file should be included as
+ * #include
+ * by Khronos client API header files that use its types and defines.
+ *
+ * The types in khrplatform.h should only be used to define API-specific types.
+ *
+ * Types defined in khrplatform.h:
+ * khronos_int8_t signed 8 bit
+ * khronos_uint8_t unsigned 8 bit
+ * khronos_int16_t signed 16 bit
+ * khronos_uint16_t unsigned 16 bit
+ * khronos_int32_t signed 32 bit
+ * khronos_uint32_t unsigned 32 bit
+ * khronos_int64_t signed 64 bit
+ * khronos_uint64_t unsigned 64 bit
+ * khronos_intptr_t signed same number of bits as a pointer
+ * khronos_uintptr_t unsigned same number of bits as a pointer
+ * khronos_ssize_t signed size
+ * khronos_usize_t unsigned size
+ * khronos_float_t signed 32 bit floating point
+ * khronos_time_ns_t unsigned 64 bit time in nanoseconds
+ * khronos_utime_nanoseconds_t unsigned time interval or absolute time in
+ * nanoseconds
+ * khronos_stime_nanoseconds_t signed time interval in nanoseconds
+ * khronos_boolean_enum_t enumerated boolean type. This should
+ * only be used as a base type when a client API's boolean type is
+ * an enum. Client APIs which use an integer or other type for
+ * booleans cannot use this as the base type for their boolean.
+ *
+ * Tokens defined in khrplatform.h:
+ *
+ * KHRONOS_FALSE, KHRONOS_TRUE Enumerated boolean false/true values.
+ *
+ * KHRONOS_SUPPORT_INT64 is 1 if 64 bit integers are supported; otherwise 0.
+ * KHRONOS_SUPPORT_FLOAT is 1 if floats are supported; otherwise 0.
+ *
+ * Calling convention macros defined in this file:
+ * KHRONOS_APICALL
+ * KHRONOS_APIENTRY
+ * KHRONOS_APIATTRIBUTES
+ *
+ * These may be used in function prototypes as:
+ *
+ * KHRONOS_APICALL void KHRONOS_APIENTRY funcname(
+ * int arg1,
+ * int arg2) KHRONOS_APIATTRIBUTES;
+ */
+
+#if defined(__SCITECH_SNAP__) && !defined(KHRONOS_STATIC)
+# define KHRONOS_STATIC 1
+#endif
+
+/*-------------------------------------------------------------------------
+ * Definition of KHRONOS_APICALL
+ *-------------------------------------------------------------------------
+ * This precedes the return type of the function in the function prototype.
+ */
+#if defined(KHRONOS_STATIC)
+ /* If the preprocessor constant KHRONOS_STATIC is defined, make the
+ * header compatible with static linking. */
+# define KHRONOS_APICALL
+#elif defined(_WIN32)
+# define KHRONOS_APICALL __declspec(dllimport)
+#elif defined (__SYMBIAN32__)
+# define KHRONOS_APICALL IMPORT_C
+#elif defined(__ANDROID__)
+# define KHRONOS_APICALL __attribute__((visibility("default")))
+#else
+# define KHRONOS_APICALL
+#endif
+
+/*-------------------------------------------------------------------------
+ * Definition of KHRONOS_APIENTRY
+ *-------------------------------------------------------------------------
+ * This follows the return type of the function and precedes the function
+ * name in the function prototype.
+ */
+#if defined(_WIN32) && !defined(_WIN32_WCE) && !defined(__SCITECH_SNAP__)
+ /* Win32 but not WinCE */
+# define KHRONOS_APIENTRY __stdcall
+#else
+# define KHRONOS_APIENTRY
+#endif
+
+/*-------------------------------------------------------------------------
+ * Definition of KHRONOS_APIATTRIBUTES
+ *-------------------------------------------------------------------------
+ * This follows the closing parenthesis of the function prototype arguments.
+ */
+#if defined (__ARMCC_2__)
+#define KHRONOS_APIATTRIBUTES __softfp
+#else
+#define KHRONOS_APIATTRIBUTES
+#endif
+
+/*-------------------------------------------------------------------------
+ * basic type definitions
+ *-----------------------------------------------------------------------*/
+#if (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || defined(__GNUC__) || defined(__SCO__) || defined(__USLC__)
+
+
+/*
+ * Using
+ */
+#include
+typedef int32_t khronos_int32_t;
+typedef uint32_t khronos_uint32_t;
+typedef int64_t khronos_int64_t;
+typedef uint64_t khronos_uint64_t;
+#define KHRONOS_SUPPORT_INT64 1
+#define KHRONOS_SUPPORT_FLOAT 1
+/*
+ * To support platform where unsigned long cannot be used interchangeably with
+ * inptr_t (e.g. CHERI-extended ISAs), we can use the stdint.h intptr_t.
+ * Ideally, we could just use (u)intptr_t everywhere, but this could result in
+ * ABI breakage if khronos_uintptr_t is changed from unsigned long to
+ * unsigned long long or similar (this results in different C++ name mangling).
+ * To avoid changes for existing platforms, we restrict usage of intptr_t to
+ * platforms where the size of a pointer is larger than the size of long.
+ */
+#if defined(__SIZEOF_LONG__) && defined(__SIZEOF_POINTER__)
+#if __SIZEOF_POINTER__ > __SIZEOF_LONG__
+#define KHRONOS_USE_INTPTR_T
+#endif
+#endif
+
+#elif defined(__VMS ) || defined(__sgi)
+
+/*
+ * Using
+ */
+#include
+typedef int32_t khronos_int32_t;
+typedef uint32_t khronos_uint32_t;
+typedef int64_t khronos_int64_t;
+typedef uint64_t khronos_uint64_t;
+#define KHRONOS_SUPPORT_INT64 1
+#define KHRONOS_SUPPORT_FLOAT 1
+
+#elif defined(_WIN32) && !defined(__SCITECH_SNAP__)
+
+/*
+ * Win32
+ */
+typedef __int32 khronos_int32_t;
+typedef unsigned __int32 khronos_uint32_t;
+typedef __int64 khronos_int64_t;
+typedef unsigned __int64 khronos_uint64_t;
+#define KHRONOS_SUPPORT_INT64 1
+#define KHRONOS_SUPPORT_FLOAT 1
+
+#elif defined(__sun__) || defined(__digital__)
+
+/*
+ * Sun or Digital
+ */
+typedef int khronos_int32_t;
+typedef unsigned int khronos_uint32_t;
+#if defined(__arch64__) || defined(_LP64)
+typedef long int khronos_int64_t;
+typedef unsigned long int khronos_uint64_t;
+#else
+typedef long long int khronos_int64_t;
+typedef unsigned long long int khronos_uint64_t;
+#endif /* __arch64__ */
+#define KHRONOS_SUPPORT_INT64 1
+#define KHRONOS_SUPPORT_FLOAT 1
+
+#elif 0
+
+/*
+ * Hypothetical platform with no float or int64 support
+ */
+typedef int khronos_int32_t;
+typedef unsigned int khronos_uint32_t;
+#define KHRONOS_SUPPORT_INT64 0
+#define KHRONOS_SUPPORT_FLOAT 0
+
+#else
+
+/*
+ * Generic fallback
+ */
+#include
+typedef int32_t khronos_int32_t;
+typedef uint32_t khronos_uint32_t;
+typedef int64_t khronos_int64_t;
+typedef uint64_t khronos_uint64_t;
+#define KHRONOS_SUPPORT_INT64 1
+#define KHRONOS_SUPPORT_FLOAT 1
+
+#endif
+
+
+/*
+ * Types that are (so far) the same on all platforms
+ */
+typedef signed char khronos_int8_t;
+typedef unsigned char khronos_uint8_t;
+typedef signed short int khronos_int16_t;
+typedef unsigned short int khronos_uint16_t;
+
+/*
+ * Types that differ between LLP64 and LP64 architectures - in LLP64,
+ * pointers are 64 bits, but 'long' is still 32 bits. Win64 appears
+ * to be the only LLP64 architecture in current use.
+ */
+#ifdef KHRONOS_USE_INTPTR_T
+typedef intptr_t khronos_intptr_t;
+typedef uintptr_t khronos_uintptr_t;
+#elif defined(_WIN64)
+typedef signed long long int khronos_intptr_t;
+typedef unsigned long long int khronos_uintptr_t;
+#else
+typedef signed long int khronos_intptr_t;
+typedef unsigned long int khronos_uintptr_t;
+#endif
+
+#if defined(_WIN64)
+typedef signed long long int khronos_ssize_t;
+typedef unsigned long long int khronos_usize_t;
+#else
+typedef signed long int khronos_ssize_t;
+typedef unsigned long int khronos_usize_t;
+#endif
+
+#if KHRONOS_SUPPORT_FLOAT
+/*
+ * Float type
+ */
+typedef float khronos_float_t;
+#endif
+
+#if KHRONOS_SUPPORT_INT64
+/* Time types
+ *
+ * These types can be used to represent a time interval in nanoseconds or
+ * an absolute Unadjusted System Time. Unadjusted System Time is the number
+ * of nanoseconds since some arbitrary system event (e.g. since the last
+ * time the system booted). The Unadjusted System Time is an unsigned
+ * 64 bit value that wraps back to 0 every 584 years. Time intervals
+ * may be either signed or unsigned.
+ */
+typedef khronos_uint64_t khronos_utime_nanoseconds_t;
+typedef khronos_int64_t khronos_stime_nanoseconds_t;
+#endif
+
+/*
+ * Dummy value used to pad enum types to 32 bits.
+ */
+#ifndef KHRONOS_MAX_ENUM
+#define KHRONOS_MAX_ENUM 0x7FFFFFFF
+#endif
+
+/*
+ * Enumerated boolean type
+ *
+ * Values other than zero should be considered to be true. Therefore
+ * comparisons should not be made against KHRONOS_TRUE.
+ */
+typedef enum {
+ KHRONOS_FALSE = 0,
+ KHRONOS_TRUE = 1,
+ KHRONOS_BOOLEAN_ENUM_FORCE_SIZE = KHRONOS_MAX_ENUM
+} khronos_boolean_enum_t;
+
+#endif /* __khrplatform_h_ */
diff --git a/pkg/worker/caged/libretro/graphics/gl/gl.go b/pkg/worker/caged/libretro/graphics/gl/gl.go
new file mode 100644
index 00000000..46e3842e
--- /dev/null
+++ b/pkg/worker/caged/libretro/graphics/gl/gl.go
@@ -0,0 +1,274 @@
+package gl
+
+// Custom OpenGL bindings
+// Based on https://github.com/go-gl/gl/tree/master/v2.1/gl
+
+/*
+#cgo egl,windows LDFLAGS: -lEGL
+#cgo egl,darwin LDFLAGS: -lEGL
+#cgo !gles2,darwin LDFLAGS: -framework OpenGL
+#cgo gles2,darwin LDFLAGS: -lGLESv2
+#cgo !gles2,windows LDFLAGS: -lopengl32
+#cgo gles2,windows LDFLAGS: -lGLESv2
+#cgo !egl,linux !egl,freebsd !egl,openbsd pkg-config: gl
+#cgo egl,linux egl,freebsd egl,openbsd pkg-config: egl
+
+#if defined(_WIN32) && !defined(APIENTRY) && !defined(__CYGWIN__) && !defined(__SCITECH_SNAP__)
+#ifndef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN 1
+#endif
+
+#include
+
+#endif
+#ifndef APIENTRY
+#define APIENTRY
+#endif
+#ifndef APIENTRYP
+#define APIENTRYP APIENTRY*
+#endif
+#ifndef GLAPI
+#define GLAPI extern
+#endif
+
+#include
+
+typedef unsigned int GLenum;
+typedef unsigned char GLboolean;
+typedef unsigned int GLbitfield;
+typedef khronos_int8_t GLbyte;
+typedef khronos_uint8_t GLubyte;
+typedef khronos_int16_t GLshort;
+typedef khronos_uint16_t GLushort;
+typedef int GLint;
+typedef unsigned int GLuint;
+typedef khronos_int32_t GLclampx;
+typedef int GLsizei;
+typedef khronos_float_t GLfloat;
+typedef khronos_float_t GLclampf;
+typedef double GLdouble;
+typedef double GLclampd;
+typedef void *GLeglClientBufferEXT;
+typedef void *GLeglImageOES;
+typedef char GLchar;
+typedef char GLcharARB;
+
+
+#ifdef __APPLE__
+typedef void *GLhandleARB;
+#else
+typedef unsigned int GLhandleARB;
+#endif
+
+typedef const GLubyte *(APIENTRYP GPGETSTRING)(GLenum name);
+typedef GLenum (APIENTRYP GPCHECKFRAMEBUFFERSTATUS)(GLenum target);
+typedef GLenum (APIENTRYP GPGETERROR)();
+typedef void (APIENTRYP GPBINDFRAMEBUFFER)(GLenum target, GLuint framebuffer);
+typedef void (APIENTRYP GPBINDRENDERBUFFER)(GLenum target, GLuint renderbuffer);
+typedef void (APIENTRYP GPBINDTEXTURE)(GLenum target, GLuint texture);
+typedef void (APIENTRYP GPDELETEFRAMEBUFFERS)(GLsizei n, const GLuint *framebuffers);
+typedef void (APIENTRYP GPDELETERENDERBUFFERS)(GLsizei n, const GLuint *renderbuffers);
+typedef void (APIENTRYP GPDELETETEXTURES)(GLsizei n, const GLuint* textures);
+typedef void (APIENTRYP GPFRAMEBUFFERRENDERBUFFER)(GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer);
+typedef void (APIENTRYP GPFRAMEBUFFERTEXTURE2D)(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level);
+typedef void (APIENTRYP GPGENFRAMEBUFFERS)(GLsizei n, GLuint *framebuffers);
+typedef void (APIENTRYP GPGENRENDERBUFFERS)(GLsizei n, GLuint *renderbuffers);
+typedef void (APIENTRYP GPGENTEXTURES)(GLsizei n, GLuint *textures);
+typedef void (APIENTRYP GPREADPIXELS)(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void *pixels);
+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)(); }
+static void bindTexture(GPBINDTEXTURE ptr, GLenum target, GLuint texture) { (*ptr)(target, texture); }
+static void bindFramebuffer(GPBINDFRAMEBUFFER ptr, GLenum target, GLuint framebuffer) { (*ptr)(target, framebuffer); }
+static void bindRenderbuffer(GPBINDRENDERBUFFER ptr, GLenum target, GLuint buf) { (*ptr)(target, buf); }
+static void texParameteri(GPTEXPARAMETERI ptr, GLenum target, GLenum pname, GLint param) {
+ (*ptr)(target, pname, param);
+}
+static void texImage2D(GPTEXIMAGE2D ptr, GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const void *pixels) {
+ (*ptr)(target, level, internalformat, width, height, border, format, type, pixels);
+}
+static void genFramebuffers(GPGENFRAMEBUFFERS ptr, GLsizei n, GLuint *framebuffers) { (*ptr)(n, framebuffers); }
+static void genTextures(GPGENTEXTURES ptr, GLsizei n, GLuint *textures) { (*ptr)(n, textures); }
+static void framebufferTexture2D(GPFRAMEBUFFERTEXTURE2D ptr, GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level) {
+ (*ptr)(target, attachment, textarget, texture, level);
+}
+static void genRenderbuffers(GPGENRENDERBUFFERS ptr, GLsizei n, GLuint *renderbuffers) { (*ptr)(n, renderbuffers); }
+static void renderbufferStorage(GPRENDERBUFFERSTORAGE ptr, GLenum target, GLenum internalformat, GLsizei width, GLsizei height) {
+ (*ptr)(target, internalformat, width, height);
+}
+static void framebufferRenderbuffer(GPFRAMEBUFFERRENDERBUFFER ptr, GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer) {
+ (*ptr)(target, attachment, renderbuffertarget, renderbuffer);
+}
+static GLenum checkFramebufferStatus(GPCHECKFRAMEBUFFERSTATUS ptr, GLenum target) { return (*ptr)(target); }
+static void deleteRenderbuffers(GPDELETERENDERBUFFERS ptr, GLsizei n, const GLuint *renderbuffers) {
+ (*ptr)(n, renderbuffers);
+}
+static void deleteFramebuffers(GPDELETEFRAMEBUFFERS ptr, GLsizei n, const GLuint *framebuffers) {
+ (*ptr)(n, framebuffers);
+}
+static void deleteTextures(GPDELETETEXTURES ptr, GLsizei n, const GLuint *textures) { (*ptr)(n, textures); }
+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 (
+ "errors"
+ "unsafe"
+)
+
+const (
+ VENDOR = 0x1F00
+ VERSION = 0x1F02
+ RENDERER = 0x1F01
+ ShadingLanguageVersion = 0x8B8C
+ Texture2d = 0x0DE1
+ RENDERBUFFER = 0x8D41
+ FRAMEBUFFER = 0x8D40
+ TextureMinFilter = 0x2801
+ TextureMagFilter = 0x2800
+ NEAREST = 0x2600
+ RGBA8 = 0x8058
+ BGRA = 0x80E1
+ RGB = 0x1907
+ ColorAttachment0 = 0x8CE0
+ Depth24Stencil8 = 0x88F0
+ DepthStencilAttachment = 0x821A
+ DepthComponent24 = 0x81A6
+ DepthAttachment = 0x8D00
+ FramebufferComplete = 0x8CD5
+
+ UnsignedShort5551 = 0x8034
+ UnsignedShort565 = 0x8363
+ UnsignedInt8888Rev = 0x8367
+
+ PackAlignment = 0x0D05
+)
+
+var (
+ gpGetString C.GPGETSTRING
+ gpGenTextures C.GPGENTEXTURES
+ gpGetError C.GPGETERROR
+ gpBindTexture C.GPBINDTEXTURE
+ gpBindFramebuffer C.GPBINDFRAMEBUFFER
+ gpTexParameteri C.GPTEXPARAMETERI
+ gpTexImage2D C.GPTEXIMAGE2D
+ gpGenFramebuffers C.GPGENFRAMEBUFFERS
+ gpFramebufferTexture2D C.GPFRAMEBUFFERTEXTURE2D
+ gpGenRenderbuffers C.GPGENRENDERBUFFERS
+ gpBindRenderbuffer C.GPBINDRENDERBUFFER
+ gpRenderbufferStorage C.GPRENDERBUFFERSTORAGE
+ gpFramebufferRenderbuffer C.GPFRAMEBUFFERRENDERBUFFER
+ gpCheckFramebufferStatus C.GPCHECKFRAMEBUFFERSTATUS
+ gpDeleteRenderbuffers C.GPDELETERENDERBUFFERS
+ gpDeleteFramebuffers C.GPDELETEFRAMEBUFFERS
+ gpDeleteTextures C.GPDELETETEXTURES
+ gpReadPixels C.GPREADPIXELS
+ gpPixelStorei C.GPPIXELSTOREI
+)
+
+func InitWithProcAddrFunc(getProcAddr func(name string) unsafe.Pointer) error {
+ if gpGetString = (C.GPGETSTRING)(getProcAddr("glGetString")); gpGetString == nil {
+ return errors.New("glGetString")
+ }
+ if gpGenTextures = (C.GPGENTEXTURES)(getProcAddr("glGenTextures")); gpGenTextures == nil {
+ return errors.New("glGenTextures")
+ }
+ if gpGetError = (C.GPGETERROR)(getProcAddr("glGetError")); gpGetError == nil {
+ return errors.New("glGetError")
+ }
+ if gpBindTexture = (C.GPBINDTEXTURE)(getProcAddr("glBindTexture")); gpBindTexture == nil {
+ return errors.New("glBindTexture")
+ }
+ if gpBindFramebuffer = (C.GPBINDFRAMEBUFFER)(getProcAddr("glBindFramebuffer")); gpBindFramebuffer == nil {
+ return errors.New("glBindFramebuffer")
+ }
+ if gpTexParameteri = (C.GPTEXPARAMETERI)(getProcAddr("glTexParameteri")); gpTexParameteri == nil {
+ return errors.New("glTexParameteri")
+ }
+ if gpTexImage2D = (C.GPTEXIMAGE2D)(getProcAddr("glTexImage2D")); gpTexImage2D == nil {
+ return errors.New("glTexImage2D")
+ }
+ gpGenFramebuffers = (C.GPGENFRAMEBUFFERS)(getProcAddr("glGenFramebuffers"))
+ gpFramebufferTexture2D = (C.GPFRAMEBUFFERTEXTURE2D)(getProcAddr("glFramebufferTexture2D"))
+ gpGenRenderbuffers = (C.GPGENRENDERBUFFERS)(getProcAddr("glGenRenderbuffers"))
+ gpBindRenderbuffer = (C.GPBINDRENDERBUFFER)(getProcAddr("glBindRenderbuffer"))
+ gpRenderbufferStorage = (C.GPRENDERBUFFERSTORAGE)(getProcAddr("glRenderbufferStorage"))
+ gpFramebufferRenderbuffer = (C.GPFRAMEBUFFERRENDERBUFFER)(getProcAddr("glFramebufferRenderbuffer"))
+ gpCheckFramebufferStatus = (C.GPCHECKFRAMEBUFFERSTATUS)(getProcAddr("glCheckFramebufferStatus"))
+ gpDeleteRenderbuffers = (C.GPDELETERENDERBUFFERS)(getProcAddr("glDeleteRenderbuffers"))
+ gpDeleteFramebuffers = (C.GPDELETEFRAMEBUFFERS)(getProcAddr("glDeleteFramebuffers"))
+ if gpDeleteTextures = (C.GPDELETETEXTURES)(getProcAddr("glDeleteTextures")); gpDeleteTextures == nil {
+ return errors.New("glDeleteTextures")
+ }
+ gpReadPixels = (C.GPREADPIXELS)(getProcAddr("glReadPixels"))
+ if gpReadPixels == nil {
+ return errors.New("glReadPixels")
+ }
+ if gpPixelStorei = (C.GPPIXELSTOREI)(getProcAddr("glPixelStorei")); gpPixelStorei == nil {
+ return errors.New("glPixelStorei")
+ }
+ return nil
+}
+
+func GetString(name uint32) *uint8 { return (*uint8)(C.getString(gpGetString, (C.GLenum)(name))) }
+func GenTextures(n int32, textures *uint32) {
+ C.genTextures(gpGenTextures, (C.GLsizei)(n), (*C.GLuint)(unsafe.Pointer(textures)))
+}
+func BindTexture(target uint32, texture uint32) {
+ C.bindTexture(gpBindTexture, (C.GLenum)(target), (C.GLuint)(texture))
+}
+func BindFramebuffer(target uint32, framebuffer uint32) {
+ C.bindFramebuffer(gpBindFramebuffer, (C.GLenum)(target), (C.GLuint)(framebuffer))
+}
+func TexParameteri(target uint32, name uint32, param int32) {
+ C.texParameteri(gpTexParameteri, (C.GLenum)(target), (C.GLenum)(name), (C.GLint)(param))
+}
+func TexImage2D(target uint32, level int32, internalformat int32, width int32, height int32, border int32, format uint32, xtype uint32, pixels unsafe.Pointer) {
+ C.texImage2D(gpTexImage2D, (C.GLenum)(target), (C.GLint)(level), (C.GLint)(internalformat), (C.GLsizei)(width), (C.GLsizei)(height), (C.GLint)(border), (C.GLenum)(format), (C.GLenum)(xtype), pixels)
+}
+func GenFramebuffers(n int32, framebuffers *uint32) {
+ C.genFramebuffers(gpGenFramebuffers, (C.GLsizei)(n), (*C.GLuint)(unsafe.Pointer(framebuffers)))
+}
+func FramebufferTexture2D(target uint32, attachment uint32, texTarget uint32, texture uint32, level int32) {
+ C.framebufferTexture2D(gpFramebufferTexture2D, (C.GLenum)(target), (C.GLenum)(attachment), (C.GLenum)(texTarget), (C.GLuint)(texture), (C.GLint)(level))
+}
+func GenRenderbuffers(n int32, renderbuffers *uint32) {
+ C.genRenderbuffers(gpGenRenderbuffers, (C.GLsizei)(n), (*C.GLuint)(unsafe.Pointer(renderbuffers)))
+}
+func BindRenderbuffer(target uint32, renderbuffer uint32) {
+ C.bindRenderbuffer(gpBindRenderbuffer, (C.GLenum)(target), (C.GLuint)(renderbuffer))
+}
+func RenderbufferStorage(target uint32, internalformat uint32, width int32, height int32) {
+ C.renderbufferStorage(gpRenderbufferStorage, (C.GLenum)(target), (C.GLenum)(internalformat), (C.GLsizei)(width), (C.GLsizei)(height))
+}
+func FramebufferRenderbuffer(target uint32, attachment uint32, renderbufferTarget uint32, renderbuffer uint32) {
+ C.framebufferRenderbuffer(gpFramebufferRenderbuffer, (C.GLenum)(target), (C.GLenum)(attachment), (C.GLenum)(renderbufferTarget), (C.GLuint)(renderbuffer))
+}
+func CheckFramebufferStatus(target uint32) uint32 {
+ return (uint32)(C.checkFramebufferStatus(gpCheckFramebufferStatus, (C.GLenum)(target)))
+}
+func DeleteRenderbuffers(n int32, renderbuffers *uint32) {
+ C.deleteRenderbuffers(gpDeleteRenderbuffers, (C.GLsizei)(n), (*C.GLuint)(unsafe.Pointer(renderbuffers)))
+}
+func DeleteFramebuffers(n int32, framebuffers *uint32) {
+ C.deleteFramebuffers(gpDeleteFramebuffers, (C.GLsizei)(n), (*C.GLuint)(unsafe.Pointer(framebuffers)))
+}
+func DeleteTextures(n int32, textures *uint32) {
+ C.deleteTextures(gpDeleteTextures, (C.GLsizei)(n), (*C.GLuint)(unsafe.Pointer(textures)))
+}
+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)) }
+
+func GoStr(str *uint8) string { return C.GoString((*C.char)(unsafe.Pointer(str))) }
diff --git a/pkg/worker/caged/libretro/graphics/opengl.go b/pkg/worker/caged/libretro/graphics/opengl.go
new file mode 100644
index 00000000..fca78a6b
--- /dev/null
+++ b/pkg/worker/caged/libretro/graphics/opengl.go
@@ -0,0 +1,122 @@
+package graphics
+
+import (
+ "errors"
+ "fmt"
+ "math"
+ "unsafe"
+
+ "github.com/giongto35/cloud-game/v3/pkg/worker/caged/libretro/graphics/gl"
+)
+
+type Context int
+
+const (
+ CtxNone Context = iota
+ CtxOpenGl
+ CtxOpenGlEs2
+ CtxOpenGlCore
+ CtxOpenGlEs3
+ CtxOpenGlEsVersion
+ CtxVulkan
+ CtxUnknown = math.MaxInt32 - 1
+ CtxDummy = math.MaxInt32
+)
+
+type PixelFormat int
+
+const (
+ UnsignedShort5551 PixelFormat = iota
+ UnsignedShort565
+ UnsignedInt8888Rev
+)
+
+var (
+ 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(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, w, h, 0, pixType, pixFormat, nil)
+ gl.BindTexture(gl.Texture2d, 0)
+
+ gl.GenFramebuffers(1, &fbo)
+ gl.BindFramebuffer(gl.FRAMEBUFFER, fbo)
+ gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.ColorAttachment0, gl.Texture2d, tex, 0)
+
+ 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("framebuffer incomplete: 0x%X", status)
+ }
+ return nil
+}
+
+func destroyFramebuffer() {
+ if hasDepth {
+ gl.DeleteRenderbuffers(1, &rbo)
+ }
+ gl.DeleteFramebuffers(1, &fbo)
+ gl.DeleteTextures(1, &tex)
+}
+
+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 SetBuffer(size int) {
+ buf = make([]byte, size)
+ bufPtr = unsafe.Pointer(&buf[0])
+}
+
+func SetPixelFormat(format PixelFormat) error {
+ switch format {
+ case UnsignedShort5551:
+ pixFormat, pixType = gl.UnsignedShort5551, gl.BGRA
+ case UnsignedShort565:
+ pixFormat, pixType = gl.UnsignedShort565, gl.RGB
+ case UnsignedInt8888Rev:
+ pixFormat, pixType = gl.UnsignedInt8888Rev, gl.BGRA
+ default:
+ return errors.New("unknown pixel format")
+ }
+ return nil
+}
+
+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 GlFbo() uint32 { return fbo }
diff --git a/pkg/worker/caged/libretro/graphics/sdl.go b/pkg/worker/caged/libretro/graphics/sdl.go
new file mode 100644
index 00000000..7c885d88
--- /dev/null
+++ b/pkg/worker/caged/libretro/graphics/sdl.go
@@ -0,0 +1,100 @@
+package graphics
+
+import (
+ "fmt"
+ "unsafe"
+
+ "github.com/veandco/go-sdl2/sdl"
+)
+
+type SDL struct {
+ w *sdl.Window
+ ctx sdl.GLContext
+}
+
+type Config struct {
+ Ctx Context
+ W, H int
+ GLAutoContext bool
+ GLVersionMajor uint
+ GLVersionMinor uint
+ GLHasDepth bool
+ GLHasStencil bool
+}
+
+func NewSDLContext(cfg Config) (*SDL, error) {
+ if err := sdl.Init(sdl.INIT_VIDEO); err != nil {
+ return nil, fmt.Errorf("sdl: %w", err)
+ }
+
+ if !cfg.GLAutoContext {
+ if err := setGLAttrs(cfg.Ctx); err != nil {
+ return nil, err
+ }
+ }
+
+ 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: %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("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)
+ }
+}
+
+func (s *SDL) Deinit() error {
+ destroyFramebuffer()
+ sdl.GLDeleteContext(s.ctx)
+ err := s.w.Destroy()
+ sdl.Quit()
+ return err
+}
+
+func (s *SDL) BindContext() error { return s.w.GLMakeCurrent(s.ctx) }
+func GlProcAddress(proc string) unsafe.Pointer { return sdl.GLGetProcAddress(proc) }
+
+func TryInit() error {
+ if err := sdl.Init(sdl.INIT_VIDEO); err != nil {
+ return err
+ }
+ sdl.Quit()
+ return nil
+}
diff --git a/pkg/worker/caged/libretro/manager/downloader.go b/pkg/worker/caged/libretro/manager/downloader.go
new file mode 100644
index 00000000..d3e97409
--- /dev/null
+++ b/pkg/worker/caged/libretro/manager/downloader.go
@@ -0,0 +1,63 @@
+package manager
+
+import (
+ "os"
+
+ "github.com/giongto35/cloud-game/v3/pkg/logger"
+ "github.com/giongto35/cloud-game/v3/pkg/worker/compression"
+)
+
+type Download struct {
+ Key string
+ Address string
+}
+
+type Client interface {
+ Request(dest string, urls ...Download) ([]string, []string)
+}
+
+type Downloader struct {
+ backend Client
+ // pipe contains a sequential list of
+ // operations applied to some files and
+ // each operation will return a list of
+ // successfully processed files
+ pipe []Process
+ log *logger.Logger
+}
+
+type Process func(string, []string, *logger.Logger) []string
+
+func NewDefaultDownloader(log *logger.Logger) Downloader {
+ return Downloader{
+ backend: NewGrabDownloader(log),
+ pipe: []Process{unpackDelete},
+ log: log,
+ }
+}
+
+// Download tries to download specified with URLs list of files and
+// put them into the destination folder.
+// It will return a partial or full list of downloaded files,
+// a list of processed files if some pipe processing functions are set.
+func (d *Downloader) Download(dest string, urls ...Download) ([]string, []string) {
+ files, fails := d.backend.Request(dest, urls...)
+ for _, op := range d.pipe {
+ files = op(dest, files, d.log)
+ }
+ return files, fails
+}
+
+func unpackDelete(dest string, files []string, log *logger.Logger) []string {
+ var res []string
+ for _, file := range files {
+ if unpack := compression.NewFromExt(file, log); unpack != nil {
+ if _, err := unpack.Extract(file, dest); err == nil {
+ if e := os.Remove(file); e == nil {
+ res = append(res, file)
+ }
+ }
+ }
+ }
+ return res
+}
diff --git a/pkg/worker/caged/libretro/manager/grab.go b/pkg/worker/caged/libretro/manager/grab.go
new file mode 100644
index 00000000..f7d40a05
--- /dev/null
+++ b/pkg/worker/caged/libretro/manager/grab.go
@@ -0,0 +1,63 @@
+package manager
+
+import (
+ "crypto/tls"
+ "net/http"
+
+ "github.com/cavaliergopher/grab/v3"
+ "github.com/giongto35/cloud-game/v3/pkg/logger"
+)
+
+type GrabDownloader struct {
+ client *grab.Client
+ parallelism int
+ log *logger.Logger
+}
+
+func NewGrabDownloader(log *logger.Logger) GrabDownloader {
+ return GrabDownloader{
+ client: &grab.Client{
+ UserAgent: "Cloud-Game/3.0",
+ HTTPClient: &http.Client{
+ Transport: &http.Transport{
+ Proxy: http.ProxyFromEnvironment,
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+ },
+ },
+ },
+ parallelism: 5,
+ log: log,
+ }
+}
+
+func (d GrabDownloader) Request(dest string, urls ...Download) (ok []string, nook []string) {
+ reqs := make([]*grab.Request, 0)
+ for _, url := range urls {
+ req, err := grab.NewRequest(dest, url.Address)
+ if err != nil {
+ d.log.Error().Err(err).Msgf("couldn't make request URL: %v, %v", url, err)
+ } else {
+ req.Label = url.Key
+ reqs = append(reqs, req)
+ }
+ }
+
+ // check each response
+ for resp := range d.client.DoBatch(d.parallelism, reqs...) {
+ 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 != nil && resp.HTTPResponse.StatusCode == 404 {
+ nook = append(nook, resp.Request.Label)
+ }
+ } else {
+ 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)
+ }
+ }
+ return
+}
diff --git a/pkg/worker/caged/libretro/manager/http.go b/pkg/worker/caged/libretro/manager/http.go
new file mode 100644
index 00000000..ff57a2de
--- /dev/null
+++ b/pkg/worker/caged/libretro/manager/http.go
@@ -0,0 +1,164 @@
+package manager
+
+import (
+ "github.com/giongto35/cloud-game/v3/pkg/config"
+ "github.com/giongto35/cloud-game/v3/pkg/logger"
+ "github.com/giongto35/cloud-game/v3/pkg/os"
+)
+
+type Manager struct {
+ BasicManager
+
+ arch ArchInfo
+ repo Repository
+ altRepo Repository
+ client Downloader
+ 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
+ flock, err := os.NewFileLock(conf.Cores.Repo.ExtLock)
+ if err != nil {
+ log.Error().Err(err).Msgf("couldn't make file lock")
+ }
+
+ 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: ArchInfo(arch),
+ client: NewDefaultDownloader(log),
+ fmu: flock,
+ log: log,
+ }
+
+ if repoConf.Type != "" {
+ m.repo = NewRepo(repoConf.Type, repoConf.Url, repoConf.Compression, "buildbot")
+ }
+ if altRepoConf.Type != "" {
+ m.altRepo = NewRepo(altRepoConf.Type, altRepoConf.Url, altRepoConf.Compression, "")
+ }
+
+ return m
+}
+
+func CheckCores(conf config.Emulator, log *logger.Logger) error {
+ if !conf.Libretro.Cores.Repo.Sync {
+ return nil
+ }
+ log.Info().Msg("Starting Libretro cores sync...")
+ coreManager := NewRemoteHttpManager(conf.Libretro, log)
+ // make a dir for cores
+ if err := os.MakeDirAll(coreManager.Conf.GetCoresStorePath()); err != nil {
+ return err
+ }
+ if err := coreManager.Sync(); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (m *Manager) Sync() error {
+ // IPC lock if multiple worker processes on the same machine
+ err := m.fmu.Lock()
+ if err != nil {
+ m.log.Error().Err(err).Msg("file lock fail")
+ }
+ defer func() {
+ err := m.fmu.Unlock()
+ if err != nil {
+ m.log.Error().Err(err).Msg("file unlock fail")
+ }
+ }()
+
+ installed, err := m.GetInstalled(m.arch.Ext)
+ if err != nil {
+ return err
+ }
+ download := diff(m.Conf.GetCores(), installed)
+ if failed := m.download(download); len(failed) > 0 {
+ m.log.Warn().Msgf("[core-dl] error: unable to download these cores: %v", failed)
+ }
+ return nil
+}
+
+func (m *Manager) getCoreUrls(names []string, repo Repository) (urls []Download) {
+ for _, c := range names {
+ urls = append(urls, Download{Key: c, Address: repo.CoreUrl(c, m.arch)})
+ }
+ return
+}
+
+func (m *Manager) download(cores []config.CoreInfo) (failed []string) {
+ if len(cores) == 0 || m.repo == nil {
+ return
+ }
+ var prime, second, fail []string
+ for _, n := range cores {
+ if n.Name == "" {
+ fail = append(fail, n.Id)
+ continue
+ }
+ if !n.AltRepo {
+ prime = append(prime, n.Name)
+ } else {
+ second = append(second, n.Name)
+ }
+ }
+
+ if len(prime) == 0 && len(second) == 0 {
+ m.log.Warn().Msgf("[core-dl] couldn't find info for %v cores, check the config", fail)
+ return
+ }
+
+ m.log.Info().Msgf("[core-dl] <<< download | main: %v | alt: %v", prime, second)
+ primeFails := m.down(prime, m.repo)
+ if len(primeFails) > 0 && m.altRepo != nil {
+ m.log.Warn().Msgf("[core-dl] error: unable to download some cores, trying 2nd repository")
+ failed = append(failed, m.down(primeFails, m.altRepo)...)
+ }
+ if m.altRepo != nil {
+ altFails := m.down(second, m.altRepo)
+ if len(altFails) > 0 {
+ m.log.Error().Msgf("[core-dl] error: unable to download some cores, trying 1st repository")
+ failed = append(failed, m.down(altFails, m.repo)...)
+ }
+ }
+ return
+}
+
+func (m *Manager) down(cores []string, repo Repository) (failed []string) {
+ if len(cores) == 0 || repo == nil {
+ return
+ }
+ _, failed = m.client.Download(m.Conf.GetCoresStorePath(), m.getCoreUrls(cores, repo)...)
+ return
+}
+
+// diff returns a list of not installed cores.
+func diff(declared, installed []config.CoreInfo) (diff []config.CoreInfo) {
+ if len(declared) == 0 {
+ return
+ }
+ if len(installed) == 0 {
+ return declared
+ }
+ v := map[string]struct{}{}
+ for _, x := range installed {
+ v[x.Name] = struct{}{}
+ }
+ for _, x := range declared {
+ if _, ok := v[x.Name]; !ok {
+ diff = append(diff, x)
+ }
+ }
+ return
+}
diff --git a/pkg/emulator/libretro/manager/remotehttp/manager_test.go b/pkg/worker/caged/libretro/manager/http_test.go
similarity index 70%
rename from pkg/emulator/libretro/manager/remotehttp/manager_test.go
rename to pkg/worker/caged/libretro/manager/http_test.go
index 6cf9be69..5d447d9c 100644
--- a/pkg/emulator/libretro/manager/remotehttp/manager_test.go
+++ b/pkg/worker/caged/libretro/manager/http_test.go
@@ -1,8 +1,10 @@
-package remotehttp
+package manager
import (
"reflect"
"testing"
+
+ "github.com/giongto35/cloud-game/v3/pkg/config"
)
func TestDiff(t *testing.T) {
@@ -37,9 +39,16 @@ func TestDiff(t *testing.T) {
},
}
+ toCoreInfo := func(names []string) (r []config.CoreInfo) {
+ for _, n := range names {
+ r = append(r, config.CoreInfo{Name: n})
+ }
+ return
+ }
+
for _, test := range tests {
- difference := diff(test.declared, test.installed)
- if !reflect.DeepEqual(test.diff, difference) {
+ difference := diff(toCoreInfo(test.declared), toCoreInfo(test.installed))
+ if !reflect.DeepEqual(toCoreInfo(test.diff), difference) {
t.Errorf("wrong diff for %v <- %v = %v != %v",
test.declared, test.installed, test.diff, difference)
}
diff --git a/pkg/worker/caged/libretro/manager/manager.go b/pkg/worker/caged/libretro/manager/manager.go
new file mode 100644
index 00000000..9ccfa022
--- /dev/null
+++ b/pkg/worker/caged/libretro/manager/manager.go
@@ -0,0 +1,32 @@
+package manager
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/giongto35/cloud-game/v3/pkg/config"
+)
+
+type BasicManager struct {
+ Conf config.LibretroConfig
+}
+
+func (m BasicManager) GetInstalled(libExt string) (installed []config.CoreInfo, err error) {
+ if libExt == "" {
+ return
+ }
+ dir := m.Conf.GetCoresStorePath()
+ files, err := os.ReadDir(dir)
+ if err != nil {
+ return
+ }
+
+ for _, file := range files {
+ name := file.Name()
+ if filepath.Ext(name) == libExt {
+ installed = append(installed, config.CoreInfo{Name: strings.TrimSuffix(name, libExt)})
+ }
+ }
+ return
+}
diff --git a/pkg/worker/caged/libretro/manager/repository.go b/pkg/worker/caged/libretro/manager/repository.go
new file mode 100644
index 00000000..3dbe0686
--- /dev/null
+++ b/pkg/worker/caged/libretro/manager/repository.go
@@ -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
+}
diff --git a/pkg/worker/caged/libretro/manager/repository_test.go b/pkg/worker/caged/libretro/manager/repository_test.go
new file mode 100644
index 00000000..bff2c16a
--- /dev/null
+++ b/pkg/worker/caged/libretro/manager/repository_test.go
@@ -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)
+ }
+ }
+}
diff --git a/pkg/worker/caged/libretro/nanoarch/input.go b/pkg/worker/caged/libretro/nanoarch/input.go
new file mode 100644
index 00000000..eb6080c5
--- /dev/null
+++ b/pkg/worker/caged/libretro/nanoarch/input.go
@@ -0,0 +1,167 @@
+package nanoarch
+
+import (
+ "encoding/binary"
+ "sync/atomic"
+)
+
+/*
+#include
+#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()))
+}
diff --git a/pkg/worker/caged/libretro/nanoarch/input_test.go b/pkg/worker/caged/libretro/nanoarch/input_test.go
new file mode 100644
index 00000000..1df81da7
--- /dev/null
+++ b/pkg/worker/caged/libretro/nanoarch/input_test.go
@@ -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")
+ }
+}
diff --git a/pkg/worker/caged/libretro/nanoarch/libretro.h b/pkg/worker/caged/libretro/nanoarch/libretro.h
new file mode 100644
index 00000000..c549976d
--- /dev/null
+++ b/pkg/worker/caged/libretro/nanoarch/libretro.h
@@ -0,0 +1,7846 @@
+/*!
+ * libretro.h is a simple API that allows for the creation of games and emulators.
+ *
+ * @file libretro.h
+ * @version 1
+ * @author libretro
+ * @copyright Copyright (C) 2010-2024 The RetroArch team
+ *
+ * @paragraph LICENSE
+ * The following license statement only applies to this libretro API header (libretro.h).
+ *
+ * Copyright (C) 2010-2024 The RetroArch team
+ *
+ * Permission is hereby granted, free of charge,
+ * to any person obtaining a copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
+ * and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef LIBRETRO_H__
+#define LIBRETRO_H__
+
+#include
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifndef __cplusplus
+#if defined(_MSC_VER) && _MSC_VER < 1800 && !defined(SN_TARGET_PS3)
+/* Hack applied for MSVC when compiling in C89 mode
+ * as it isn't C99-compliant. */
+#define bool unsigned char
+#define true 1
+#define false 0
+#else
+#include
+#endif
+#endif
+
+#ifndef RETRO_CALLCONV
+# if defined(__GNUC__) && defined(__i386__) && !defined(__x86_64__)
+# define RETRO_CALLCONV __attribute__((cdecl))
+# elif defined(_MSC_VER) && defined(_M_X86) && !defined(_M_X64)
+# define RETRO_CALLCONV __cdecl
+# else
+# define RETRO_CALLCONV /* all other platforms only have one calling convention each */
+# endif
+#endif
+
+#ifndef RETRO_API
+# if defined(_WIN32) || defined(__CYGWIN__) || defined(__MINGW32__)
+# ifdef RETRO_IMPORT_SYMBOLS
+# ifdef __GNUC__
+# define RETRO_API RETRO_CALLCONV __attribute__((__dllimport__))
+# else
+# define RETRO_API RETRO_CALLCONV __declspec(dllimport)
+# endif
+# else
+# ifdef __GNUC__
+# define RETRO_API RETRO_CALLCONV __attribute__((__dllexport__))
+# else
+# define RETRO_API RETRO_CALLCONV __declspec(dllexport)
+# endif
+# endif
+# else
+# if defined(__GNUC__) && __GNUC__ >= 4
+# define RETRO_API RETRO_CALLCONV __attribute__((__visibility__("default")))
+# else
+# define RETRO_API RETRO_CALLCONV
+# endif
+# endif
+#endif
+
+/**
+ * The major version of the libretro API and ABI.
+ * Cores may support multiple versions,
+ * or they may reject cores with unsupported versions.
+ * It is only incremented for incompatible API/ABI changes;
+ * this generally implies a function was removed or changed,
+ * or that a \c struct had fields removed or changed.
+ * @note A design goal of libretro is to avoid having to increase this value at all costs.
+ * This is why there are APIs that are "extended" or "V2".
+ */
+#define RETRO_API_VERSION 1
+
+/**
+ * @defgroup RETRO_DEVICE Input Devices
+ * @brief Libretro's fundamental device abstractions.
+ *
+ * Libretro's input system consists of abstractions over standard device types,
+ * such as a joypad (with or without analog), mouse, keyboard, light gun, or an abstract pointer.
+ * Instead of managing input devices themselves,
+ * cores need only to map their own concept of a controller to libretro's abstractions.
+ * This makes it possible for frontends to map the abstract types to a real input device
+ * without having to worry about the correct use of arbitrary (real) controller layouts.
+ * @{
+ */
+
+#define RETRO_DEVICE_TYPE_SHIFT 8
+#define RETRO_DEVICE_MASK ((1 << RETRO_DEVICE_TYPE_SHIFT) - 1)
+
+/**
+ * Defines an ID for a subclass of a known device type.
+ *
+ * To define a subclass ID, use this macro like so:
+ * @code{c}
+ * #define RETRO_DEVICE_SUPER_SCOPE RETRO_DEVICE_SUBCLASS(RETRO_DEVICE_LIGHTGUN, 1)
+ * #define RETRO_DEVICE_JUSTIFIER RETRO_DEVICE_SUBCLASS(RETRO_DEVICE_LIGHTGUN, 2)
+ * @endcode
+ *
+ * Correct use of this macro allows a frontend to select a suitable physical device
+ * to map to the emulated device.
+ *
+ * @note Cores must use the base ID when polling for input,
+ * and frontends must only accept the base ID for this purpose.
+ * Polling for input using subclass IDs is reserved for future definition.
+ *
+ * @param base One of the \ref RETRO_DEVICE "base device types".
+ * @param id A unique ID, with respect to \c base.
+ * Must be a non-negative integer.
+ * @return A unique subclass ID.
+ * @see retro_controller_description
+ * @see retro_set_controller_port_device
+ */
+#define RETRO_DEVICE_SUBCLASS(base, id) (((id + 1) << RETRO_DEVICE_TYPE_SHIFT) | base)
+
+/**
+ * @defgroup RETRO_DEVICE Input Device Classes
+ * @{
+ */
+
+/**
+ * Indicates no input.
+ *
+ * When provided as the \c device argument to \c retro_input_state_t,
+ * all other arguments are ignored and zero is returned.
+ *
+ * @see retro_input_state_t
+ */
+#define RETRO_DEVICE_NONE 0
+
+/**
+ * An abstraction around a game controller, known as a "RetroPad".
+ *
+ * The RetroPad is modelled after a SNES controller,
+ * but with additional L2/R2/L3/R3 buttons
+ * (similar to a PlayStation controller).
+ *
+ * When provided as the \c device argument to \c retro_input_state_t,
+ * the \c id argument denotes the button (including D-Pad directions) to query.
+ * The result of said query will be 1 if the button is down, 0 if not.
+ *
+ * There is one exception; if \c RETRO_DEVICE_ID_JOYPAD_MASK is queried
+ * (and the frontend supports this query),
+ * the result will be a bitmask of all pressed buttons.
+ *
+ * @see retro_input_state_t
+ * @see RETRO_DEVICE_ANALOG
+ * @see RETRO_DEVICE_ID_JOYPAD
+ * @see RETRO_DEVICE_ID_JOYPAD_MASK
+ * @see RETRO_ENVIRONMENT_GET_INPUT_BITMASKS
+ */
+#define RETRO_DEVICE_JOYPAD 1
+
+/**
+ * An abstraction around a mouse, similar to the SNES Mouse but with more buttons.
+ *
+ * When provided as the \c device argument to \c retro_input_state_t,
+ * the \c id argument denotes the button or axis to query.
+ * For buttons, the result of said query
+ * will be 1 if the button is down or 0 if not.
+ * For mouse wheel axes, the result
+ * will be 1 if the wheel was rotated in that direction and 0 if not.
+ * For the mouse pointer axis, the result will be thee mouse's movement
+ * relative to the last poll.
+ * The core is responsible for tracking the mouse's position,
+ * and the frontend is responsible for preventing interference
+ * by the real hardware pointer (if applicable).
+ *
+ * @note This should only be used for cores that emulate mouse input,
+ * such as for home computers
+ * or consoles with mouse attachments.
+ * Cores that emulate light guns should use \c RETRO_DEVICE_LIGHTGUN,
+ * and cores that emulate touch screens should use \c RETRO_DEVICE_POINTER.
+ *
+ * @see RETRO_DEVICE_POINTER
+ * @see RETRO_DEVICE_LIGHTGUN
+ */
+#define RETRO_DEVICE_MOUSE 2
+
+/**
+ * An abstraction around a keyboard.
+ *
+ * When provided as the \c device argument to \c retro_input_state_t,
+ * the \c id argument denotes the key to poll.
+ *
+ * @note This should only be used for cores that emulate keyboard input,
+ * such as for home computers
+ * or consoles with keyboard attachments.
+ * Cores that emulate gamepads should use \c RETRO_DEVICE_JOYPAD or \c RETRO_DEVICE_ANALOG,
+ * and leave keyboard compatibility to the frontend.
+ *
+ * @see RETRO_ENVIRONMENT_SET_KEYBOARD_CALLBACK
+ * @see retro_key
+ */
+#define RETRO_DEVICE_KEYBOARD 3
+
+/**
+ * An abstraction around a light gun, similar to the PlayStation's Guncon.
+ *
+ * When provided as the \c device argument to \c retro_input_state_t,
+ * the \c id argument denotes one of several possible inputs.
+ *
+ * The gun's coordinates are reported in screen space (similar to the pointer)
+ * in the range of [-0x8000, 0x7fff].
+ * Zero is the center of the game's screen
+ * and -0x8000 represents out-of-bounds.
+ * The trigger and various auxiliary buttons are also reported.
+ *
+ * @note A forced off-screen shot can be requested for auto-reloading
+ * function in some games.
+ *
+ * @see RETRO_DEVICE_POINTER
+ */
+#define RETRO_DEVICE_LIGHTGUN 4
+
+/**
+ * An extension of the RetroPad that supports analog input.
+ *
+ * The analog RetroPad provides two virtual analog sticks (similar to DualShock controllers)
+ * and allows any button to be treated as analog (similar to Xbox shoulder triggers).
+ *
+ * When provided as the \c device argument to \c retro_input_state_t,
+ * the \c id argument denotes an analog axis or an analog button.
+ *
+ * Analog axes are reported in the range of [-0x8000, 0x7fff],
+ * with the X axis being positive towards the right
+ * and the Y axis being positive towards the bottom.
+ *
+ * Analog buttons are reported in the range of [0, 0x7fff],
+ * where 0 is unpressed and 0x7fff is fully pressed.
+ *
+ * @note Cores should only use this type if they need analog input.
+ * Otherwise, \c RETRO_DEVICE_JOYPAD should be used.
+ * @see RETRO_DEVICE_JOYPAD
+ */
+#define RETRO_DEVICE_ANALOG 5
+
+/**
+ * Input Device: Pointer.
+ *
+ * Abstracts the concept of a pointing mechanism, e.g. touch.
+ * This allows libretro to query in absolute coordinates where on the
+ * screen a mouse (or something similar) is being placed.
+ * For a touch centric device, coordinates reported are the coordinates
+ * of the press.
+ *
+ * Coordinates in X and Y are reported as:
+ * [-0x7fff, 0x7fff]: -0x7fff corresponds to the far left/top of the screen,
+ * and 0x7fff corresponds to the far right/bottom of the screen.
+ * The "screen" is here defined as area that is passed to the frontend and
+ * later displayed on the monitor. If the pointer is outside this screen,
+ * such as in the black surrounding areas when actual display is larger,
+ * edge position is reported. An explicit edge detection is also provided,
+ * that will return 1 if the pointer is near the screen edge or actually outside it.
+ *
+ * The frontend is free to scale/resize this screen as it sees fit, however,
+ * (X, Y) = (-0x7fff, -0x7fff) will correspond to the top-left pixel of the
+ * game image, etc.
+ *
+ * To check if the pointer coordinates are valid (e.g. a touch display
+ * actually being touched), \c RETRO_DEVICE_ID_POINTER_PRESSED returns 1 or 0.
+ *
+ * If using a mouse on a desktop, \c RETRO_DEVICE_ID_POINTER_PRESSED will
+ * usually correspond to the left mouse button, but this is a frontend decision.
+ * \c RETRO_DEVICE_ID_POINTER_PRESSED will only return 1 if the pointer is
+ * inside the game screen.
+ *
+ * For multi-touch, the index variable can be used to successively query
+ * more presses.
+ * If index = 0 returns true for \c _PRESSED, coordinates can be extracted
+ * with \c _X, \c _Y for index = 0. One can then query \c _PRESSED, \c _X, \c _Y with
+ * index = 1, and so on.
+ * Eventually \c _PRESSED will return false for an index. No further presses
+ * are registered at this point.
+ *
+ * @see RETRO_DEVICE_MOUSE
+ * @see RETRO_DEVICE_ID_POINTER_X
+ * @see RETRO_DEVICE_ID_POINTER_Y
+ * @see RETRO_DEVICE_ID_POINTER_PRESSED
+ */
+#define RETRO_DEVICE_POINTER 6
+
+/** @} */
+
+/** @defgroup RETRO_DEVICE_ID_JOYPAD RetroPad Input
+ * @brief Digital buttons for the RetroPad.
+ *
+ * Button placement is comparable to that of a SNES controller,
+ * combined with the shoulder buttons of a PlayStation controller.
+ * These values can also be used for the \c id field of \c RETRO_DEVICE_INDEX_ANALOG_BUTTON
+ * to represent analog buttons (usually shoulder triggers).
+ * @{
+ */
+
+/** The equivalent of the SNES controller's south face button. */
+#define RETRO_DEVICE_ID_JOYPAD_B 0
+
+/** The equivalent of the SNES controller's west face button. */
+#define RETRO_DEVICE_ID_JOYPAD_Y 1
+
+/** The equivalent of the SNES controller's left-center button. */
+#define RETRO_DEVICE_ID_JOYPAD_SELECT 2
+
+/** The equivalent of the SNES controller's right-center button. */
+#define RETRO_DEVICE_ID_JOYPAD_START 3
+
+/** Up on the RetroPad's D-pad. */
+#define RETRO_DEVICE_ID_JOYPAD_UP 4
+
+/** Down on the RetroPad's D-pad. */
+#define RETRO_DEVICE_ID_JOYPAD_DOWN 5
+
+/** Left on the RetroPad's D-pad. */
+#define RETRO_DEVICE_ID_JOYPAD_LEFT 6
+
+/** Right on the RetroPad's D-pad. */
+#define RETRO_DEVICE_ID_JOYPAD_RIGHT 7
+
+/** The equivalent of the SNES controller's east face button. */
+#define RETRO_DEVICE_ID_JOYPAD_A 8
+
+/** The equivalent of the SNES controller's north face button. */
+#define RETRO_DEVICE_ID_JOYPAD_X 9
+
+/** The equivalent of the SNES controller's left shoulder button. */
+#define RETRO_DEVICE_ID_JOYPAD_L 10
+
+/** The equivalent of the SNES controller's right shoulder button. */
+#define RETRO_DEVICE_ID_JOYPAD_R 11
+
+/** The equivalent of the PlayStation's rear left shoulder button. */
+#define RETRO_DEVICE_ID_JOYPAD_L2 12
+
+/** The equivalent of the PlayStation's rear right shoulder button. */
+#define RETRO_DEVICE_ID_JOYPAD_R2 13
+
+/**
+ * The equivalent of the PlayStation's left analog stick button,
+ * although the actual button need not be in this position.
+ */
+#define RETRO_DEVICE_ID_JOYPAD_L3 14
+
+/**
+ * The equivalent of the PlayStation's right analog stick button,
+ * although the actual button need not be in this position.
+ */
+#define RETRO_DEVICE_ID_JOYPAD_R3 15
+
+/**
+ * Represents a bitmask that describes the state of all \c RETRO_DEVICE_ID_JOYPAD button constants,
+ * rather than the state of a single button.
+ *
+ * @see RETRO_ENVIRONMENT_GET_INPUT_BITMASKS
+ * @see RETRO_DEVICE_JOYPAD
+ */
+#define RETRO_DEVICE_ID_JOYPAD_MASK 256
+
+/** @} */
+
+/** @defgroup RETRO_DEVICE_ID_ANALOG Analog RetroPad Input
+ * @{
+ */
+
+/* Index / Id values for ANALOG device. */
+#define RETRO_DEVICE_INDEX_ANALOG_LEFT 0
+#define RETRO_DEVICE_INDEX_ANALOG_RIGHT 1
+#define RETRO_DEVICE_INDEX_ANALOG_BUTTON 2
+#define RETRO_DEVICE_ID_ANALOG_X 0
+#define RETRO_DEVICE_ID_ANALOG_Y 1
+
+/** @} */
+
+/* Id values for MOUSE. */
+#define RETRO_DEVICE_ID_MOUSE_X 0
+#define RETRO_DEVICE_ID_MOUSE_Y 1
+#define RETRO_DEVICE_ID_MOUSE_LEFT 2
+#define RETRO_DEVICE_ID_MOUSE_RIGHT 3
+#define RETRO_DEVICE_ID_MOUSE_WHEELUP 4
+#define RETRO_DEVICE_ID_MOUSE_WHEELDOWN 5
+#define RETRO_DEVICE_ID_MOUSE_MIDDLE 6
+#define RETRO_DEVICE_ID_MOUSE_HORIZ_WHEELUP 7
+#define RETRO_DEVICE_ID_MOUSE_HORIZ_WHEELDOWN 8
+#define RETRO_DEVICE_ID_MOUSE_BUTTON_4 9
+#define RETRO_DEVICE_ID_MOUSE_BUTTON_5 10
+
+/* Id values for LIGHTGUN. */
+#define RETRO_DEVICE_ID_LIGHTGUN_SCREEN_X 13 /*Absolute Position*/
+#define RETRO_DEVICE_ID_LIGHTGUN_SCREEN_Y 14 /*Absolute Position*/
+/** Indicates if lightgun points off the screen or near the edge */
+#define RETRO_DEVICE_ID_LIGHTGUN_IS_OFFSCREEN 15 /*Status Check*/
+#define RETRO_DEVICE_ID_LIGHTGUN_TRIGGER 2
+#define RETRO_DEVICE_ID_LIGHTGUN_RELOAD 16 /*Forced off-screen shot*/
+#define RETRO_DEVICE_ID_LIGHTGUN_AUX_A 3
+#define RETRO_DEVICE_ID_LIGHTGUN_AUX_B 4
+#define RETRO_DEVICE_ID_LIGHTGUN_START 6
+#define RETRO_DEVICE_ID_LIGHTGUN_SELECT 7
+#define RETRO_DEVICE_ID_LIGHTGUN_AUX_C 8
+#define RETRO_DEVICE_ID_LIGHTGUN_DPAD_UP 9
+#define RETRO_DEVICE_ID_LIGHTGUN_DPAD_DOWN 10
+#define RETRO_DEVICE_ID_LIGHTGUN_DPAD_LEFT 11
+#define RETRO_DEVICE_ID_LIGHTGUN_DPAD_RIGHT 12
+/* deprecated */
+#define RETRO_DEVICE_ID_LIGHTGUN_X 0 /*Relative Position*/
+#define RETRO_DEVICE_ID_LIGHTGUN_Y 1 /*Relative Position*/
+#define RETRO_DEVICE_ID_LIGHTGUN_CURSOR 3 /*Use Aux:A instead*/
+#define RETRO_DEVICE_ID_LIGHTGUN_TURBO 4 /*Use Aux:B instead*/
+#define RETRO_DEVICE_ID_LIGHTGUN_PAUSE 5 /*Use Start instead*/
+
+/* Id values for POINTER. */
+#define RETRO_DEVICE_ID_POINTER_X 0
+#define RETRO_DEVICE_ID_POINTER_Y 1
+#define RETRO_DEVICE_ID_POINTER_PRESSED 2
+#define RETRO_DEVICE_ID_POINTER_COUNT 3
+/** Indicates if pointer is off the screen or near the edge */
+#define RETRO_DEVICE_ID_POINTER_IS_OFFSCREEN 15
+/** @} */
+
+/* Returned from retro_get_region(). */
+#define RETRO_REGION_NTSC 0
+#define RETRO_REGION_PAL 1
+
+/**
+ * Identifiers for supported languages.
+ * @see RETRO_ENVIRONMENT_GET_LANGUAGE
+ */
+enum retro_language
+{
+ RETRO_LANGUAGE_ENGLISH = 0,
+ RETRO_LANGUAGE_JAPANESE = 1,
+ RETRO_LANGUAGE_FRENCH = 2,
+ RETRO_LANGUAGE_SPANISH = 3,
+ RETRO_LANGUAGE_GERMAN = 4,
+ RETRO_LANGUAGE_ITALIAN = 5,
+ RETRO_LANGUAGE_DUTCH = 6,
+ RETRO_LANGUAGE_PORTUGUESE_BRAZIL = 7,
+ RETRO_LANGUAGE_PORTUGUESE_PORTUGAL = 8,
+ RETRO_LANGUAGE_RUSSIAN = 9,
+ RETRO_LANGUAGE_KOREAN = 10,
+ RETRO_LANGUAGE_CHINESE_TRADITIONAL = 11,
+ RETRO_LANGUAGE_CHINESE_SIMPLIFIED = 12,
+ RETRO_LANGUAGE_ESPERANTO = 13,
+ RETRO_LANGUAGE_POLISH = 14,
+ RETRO_LANGUAGE_VIETNAMESE = 15,
+ RETRO_LANGUAGE_ARABIC = 16,
+ RETRO_LANGUAGE_GREEK = 17,
+ RETRO_LANGUAGE_TURKISH = 18,
+ RETRO_LANGUAGE_SLOVAK = 19,
+ RETRO_LANGUAGE_PERSIAN = 20,
+ RETRO_LANGUAGE_HEBREW = 21,
+ RETRO_LANGUAGE_ASTURIAN = 22,
+ RETRO_LANGUAGE_FINNISH = 23,
+ RETRO_LANGUAGE_INDONESIAN = 24,
+ RETRO_LANGUAGE_SWEDISH = 25,
+ RETRO_LANGUAGE_UKRAINIAN = 26,
+ RETRO_LANGUAGE_CZECH = 27,
+ RETRO_LANGUAGE_CATALAN_VALENCIA = 28,
+ RETRO_LANGUAGE_CATALAN = 29,
+ RETRO_LANGUAGE_BRITISH_ENGLISH = 30,
+ RETRO_LANGUAGE_HUNGARIAN = 31,
+ RETRO_LANGUAGE_BELARUSIAN = 32,
+ RETRO_LANGUAGE_GALICIAN = 33,
+ RETRO_LANGUAGE_NORWEGIAN = 34,
+ RETRO_LANGUAGE_IRISH = 35,
+ RETRO_LANGUAGE_LAST,
+
+ /** Defined to ensure that sizeof(retro_language) == sizeof(int). Do not use. */
+ RETRO_LANGUAGE_DUMMY = INT_MAX
+};
+
+/** @defgroup RETRO_MEMORY Memory Types
+ * @{
+ */
+
+/* Passed to retro_get_memory_data/size().
+ * If the memory type doesn't apply to the
+ * implementation NULL/0 can be returned.
+ */
+#define RETRO_MEMORY_MASK 0xff
+
+/* Regular save RAM. This RAM is usually found on a game cartridge,
+ * backed up by a battery.
+ * If save game data is too complex for a single memory buffer,
+ * the SAVE_DIRECTORY (preferably) or SYSTEM_DIRECTORY environment
+ * callback can be used. */
+#define RETRO_MEMORY_SAVE_RAM 0
+
+/* Some games have a built-in clock to keep track of time.
+ * This memory is usually just a couple of bytes to keep track of time.
+ */
+#define RETRO_MEMORY_RTC 1
+
+/* System ram lets a frontend peek into a game systems main RAM. */
+#define RETRO_MEMORY_SYSTEM_RAM 2
+
+/* Video ram lets a frontend peek into a game systems video RAM (VRAM). */
+#define RETRO_MEMORY_VIDEO_RAM 3
+
+/** @} */
+
+/* Keysyms used for ID in input state callback when polling RETRO_KEYBOARD. */
+enum retro_key
+{
+ RETROK_UNKNOWN = 0,
+ RETROK_FIRST = 0,
+ RETROK_BACKSPACE = 8,
+ RETROK_TAB = 9,
+ RETROK_CLEAR = 12,
+ RETROK_RETURN = 13,
+ RETROK_PAUSE = 19,
+ RETROK_ESCAPE = 27,
+ RETROK_SPACE = 32,
+ RETROK_EXCLAIM = 33,
+ RETROK_QUOTEDBL = 34,
+ RETROK_HASH = 35,
+ RETROK_DOLLAR = 36,
+ RETROK_AMPERSAND = 38,
+ RETROK_QUOTE = 39,
+ RETROK_LEFTPAREN = 40,
+ RETROK_RIGHTPAREN = 41,
+ RETROK_ASTERISK = 42,
+ RETROK_PLUS = 43,
+ RETROK_COMMA = 44,
+ RETROK_MINUS = 45,
+ RETROK_PERIOD = 46,
+ RETROK_SLASH = 47,
+ RETROK_0 = 48,
+ RETROK_1 = 49,
+ RETROK_2 = 50,
+ RETROK_3 = 51,
+ RETROK_4 = 52,
+ RETROK_5 = 53,
+ RETROK_6 = 54,
+ RETROK_7 = 55,
+ RETROK_8 = 56,
+ RETROK_9 = 57,
+ RETROK_COLON = 58,
+ RETROK_SEMICOLON = 59,
+ RETROK_LESS = 60,
+ RETROK_EQUALS = 61,
+ RETROK_GREATER = 62,
+ RETROK_QUESTION = 63,
+ RETROK_AT = 64,
+ RETROK_LEFTBRACKET = 91,
+ RETROK_BACKSLASH = 92,
+ RETROK_RIGHTBRACKET = 93,
+ RETROK_CARET = 94,
+ RETROK_UNDERSCORE = 95,
+ RETROK_BACKQUOTE = 96,
+ RETROK_a = 97,
+ RETROK_b = 98,
+ RETROK_c = 99,
+ RETROK_d = 100,
+ RETROK_e = 101,
+ RETROK_f = 102,
+ RETROK_g = 103,
+ RETROK_h = 104,
+ RETROK_i = 105,
+ RETROK_j = 106,
+ RETROK_k = 107,
+ RETROK_l = 108,
+ RETROK_m = 109,
+ RETROK_n = 110,
+ RETROK_o = 111,
+ RETROK_p = 112,
+ RETROK_q = 113,
+ RETROK_r = 114,
+ RETROK_s = 115,
+ RETROK_t = 116,
+ RETROK_u = 117,
+ RETROK_v = 118,
+ RETROK_w = 119,
+ RETROK_x = 120,
+ RETROK_y = 121,
+ RETROK_z = 122,
+ RETROK_LEFTBRACE = 123,
+ RETROK_BAR = 124,
+ RETROK_RIGHTBRACE = 125,
+ RETROK_TILDE = 126,
+ RETROK_DELETE = 127,
+
+ RETROK_KP0 = 256,
+ RETROK_KP1 = 257,
+ RETROK_KP2 = 258,
+ RETROK_KP3 = 259,
+ RETROK_KP4 = 260,
+ RETROK_KP5 = 261,
+ RETROK_KP6 = 262,
+ RETROK_KP7 = 263,
+ RETROK_KP8 = 264,
+ RETROK_KP9 = 265,
+ RETROK_KP_PERIOD = 266,
+ RETROK_KP_DIVIDE = 267,
+ RETROK_KP_MULTIPLY = 268,
+ RETROK_KP_MINUS = 269,
+ RETROK_KP_PLUS = 270,
+ RETROK_KP_ENTER = 271,
+ RETROK_KP_EQUALS = 272,
+
+ RETROK_UP = 273,
+ RETROK_DOWN = 274,
+ RETROK_RIGHT = 275,
+ RETROK_LEFT = 276,
+ RETROK_INSERT = 277,
+ RETROK_HOME = 278,
+ RETROK_END = 279,
+ RETROK_PAGEUP = 280,
+ RETROK_PAGEDOWN = 281,
+
+ RETROK_F1 = 282,
+ RETROK_F2 = 283,
+ RETROK_F3 = 284,
+ RETROK_F4 = 285,
+ RETROK_F5 = 286,
+ RETROK_F6 = 287,
+ RETROK_F7 = 288,
+ RETROK_F8 = 289,
+ RETROK_F9 = 290,
+ RETROK_F10 = 291,
+ RETROK_F11 = 292,
+ RETROK_F12 = 293,
+ RETROK_F13 = 294,
+ RETROK_F14 = 295,
+ RETROK_F15 = 296,
+
+ RETROK_NUMLOCK = 300,
+ RETROK_CAPSLOCK = 301,
+ RETROK_SCROLLOCK = 302,
+ RETROK_RSHIFT = 303,
+ RETROK_LSHIFT = 304,
+ RETROK_RCTRL = 305,
+ RETROK_LCTRL = 306,
+ RETROK_RALT = 307,
+ RETROK_LALT = 308,
+ RETROK_RMETA = 309,
+ RETROK_LMETA = 310,
+ RETROK_LSUPER = 311,
+ RETROK_RSUPER = 312,
+ RETROK_MODE = 313,
+ RETROK_COMPOSE = 314,
+
+ RETROK_HELP = 315,
+ RETROK_PRINT = 316,
+ RETROK_SYSREQ = 317,
+ RETROK_BREAK = 318,
+ RETROK_MENU = 319,
+ RETROK_POWER = 320,
+ RETROK_EURO = 321,
+ RETROK_UNDO = 322,
+ RETROK_OEM_102 = 323,
+
+ RETROK_BROWSER_BACK = 324,
+ RETROK_BROWSER_FORWARD = 325,
+ RETROK_BROWSER_REFRESH = 326,
+ RETROK_BROWSER_STOP = 327,
+ RETROK_BROWSER_SEARCH = 328,
+ RETROK_BROWSER_FAVORITES = 329,
+ RETROK_BROWSER_HOME = 330,
+ RETROK_VOLUME_MUTE = 331,
+ RETROK_VOLUME_DOWN = 332,
+ RETROK_VOLUME_UP = 333,
+ RETROK_MEDIA_NEXT = 334,
+ RETROK_MEDIA_PREV = 335,
+ RETROK_MEDIA_STOP = 336,
+ RETROK_MEDIA_PLAY_PAUSE = 337,
+ RETROK_LAUNCH_MAIL = 338,
+ RETROK_LAUNCH_MEDIA = 339,
+ RETROK_LAUNCH_APP1 = 340,
+ RETROK_LAUNCH_APP2 = 341,
+
+ RETROK_LAST,
+
+ RETROK_DUMMY = INT_MAX /* Ensure sizeof(enum) == sizeof(int) */
+};
+
+enum retro_mod
+{
+ RETROKMOD_NONE = 0x0000,
+
+ RETROKMOD_SHIFT = 0x01,
+ RETROKMOD_CTRL = 0x02,
+ RETROKMOD_ALT = 0x04,
+ RETROKMOD_META = 0x08,
+
+ RETROKMOD_NUMLOCK = 0x10,
+ RETROKMOD_CAPSLOCK = 0x20,
+ RETROKMOD_SCROLLOCK = 0x40,
+
+ RETROKMOD_DUMMY = INT_MAX /* Ensure sizeof(enum) == sizeof(int) */
+};
+
+/**
+ * @defgroup RETRO_ENVIRONMENT Environment Callbacks
+ * @{
+ */
+
+/**
+ * This bit indicates that the associated environment call is experimental,
+ * and may be changed or removed in the future.
+ * Frontends should mask out this bit before handling the environment call.
+ */
+#define RETRO_ENVIRONMENT_EXPERIMENTAL 0x10000
+
+/** Frontend-internal environment callbacks should include this bit. */
+#define RETRO_ENVIRONMENT_PRIVATE 0x20000
+
+/* Environment commands. */
+/**
+ * Requests the frontend to set the screen rotation.
+ *
+ * @param[in] data const unsigned*.
+ * Valid values are 0, 1, 2, and 3.
+ * These numbers respectively set the screen rotation to 0, 90, 180, and 270 degrees counter-clockwise.
+ * @returns \c true if the screen rotation was set successfully.
+ */
+#define RETRO_ENVIRONMENT_SET_ROTATION 1
+
+/**
+ * Queries whether the core should use overscan or not.
+ *
+ * @param[out] data bool*.
+ * Set to \c true if the core should use overscan,
+ * \c false if it should be cropped away.
+ * @returns \c true if the environment call is available.
+ * Does \em not indicate whether overscan should be used.
+ * @deprecated As of 2019 this callback is considered deprecated in favor of
+ * using core options to manage overscan in a more nuanced, core-specific way.
+ */
+#define RETRO_ENVIRONMENT_GET_OVERSCAN 2
+
+/**
+ * Queries whether the frontend supports frame duping,
+ * in the form of passing \c NULL to the video frame callback.
+ *
+ * @param[out] data bool*.
+ * Set to \c true if the frontend supports frame duping.
+ * @returns \c true if the environment call is available.
+ * @see retro_video_refresh_t
+ */
+#define RETRO_ENVIRONMENT_GET_CAN_DUPE 3
+
+/*
+ * Environ 4, 5 are no longer supported (GET_VARIABLE / SET_VARIABLES),
+ * and reserved to avoid possible ABI clash.
+ */
+
+/**
+ * @brief Displays a user-facing message for a short time.
+ *
+ * Use this callback to convey important status messages,
+ * such as errors or the result of long-running operations.
+ * For trivial messages or logging, use \c RETRO_ENVIRONMENT_GET_LOG_INTERFACE or \c stderr.
+ *
+ * \code{.c}
+ * void set_message_example(void)
+ * {
+ * struct retro_message msg;
+ * msg.frames = 60 * 5; // 5 seconds
+ * msg.msg = "Hello world!";
+ *
+ * environ_cb(RETRO_ENVIRONMENT_SET_MESSAGE, &msg);
+ * }
+ * \endcode
+ *
+ * @deprecated Prefer using \c RETRO_ENVIRONMENT_SET_MESSAGE_EXT for new code,
+ * as it offers more features.
+ * Only use this environment call for compatibility with older cores or frontends.
+ *
+ * @param[in] data const struct retro_message*.
+ * Details about the message to show to the user.
+ * Behavior is undefined if NULL.
+ * @returns \c true if the environment call is available.
+ * @see retro_message
+ * @see RETRO_ENVIRONMENT_GET_LOG_INTERFACE
+ * @see RETRO_ENVIRONMENT_SET_MESSAGE_EXT
+ * @see RETRO_ENVIRONMENT_SET_MESSAGE
+ * @see RETRO_ENVIRONMENT_GET_MESSAGE_INTERFACE_VERSION
+ * @note The frontend must make its own copy of the message and the underlying string.
+ */
+#define RETRO_ENVIRONMENT_SET_MESSAGE 6
+
+/**
+ * Requests the frontend to shutdown the core.
+ * Should only be used if the core can exit on its own,
+ * such as from a menu item in a game
+ * or an emulated power-off in an emulator.
+ *
+ * @param data Ignored.
+ * @returns \c true if the environment call is available.
+ */
+#define RETRO_ENVIRONMENT_SHUTDOWN 7
+
+/**
+ * Gives a hint to the frontend of how demanding this core is on the system.
+ * For example, reporting a level of 2 means that
+ * this implementation should run decently on frontends
+ * of level 2 and above.
+ *
+ * It can be used by the frontend to potentially warn
+ * about too demanding implementations.
+ *
+ * The levels are "floating".
+ *
+ * This function can be called on a per-game basis,
+ * as a core may have different demands for different games or settings.
+ * If called, it should be called in retro_load_game().
+ * @param[in] data const unsigned*.
+*/
+#define RETRO_ENVIRONMENT_SET_PERFORMANCE_LEVEL 8
+
+/**
+ * Returns the path to the frontend's system directory,
+ * which can be used to store system-specific configuration
+ * such as BIOS files or cached data.
+ *
+ * @param[out] data const char**.
+ * Pointer to the \c char* in which the system directory will be saved.
+ * The string is managed by the frontend and must not be modified or freed by the core.
+ * May be \c NULL if no system directory is defined,
+ * in which case the core should find an alternative directory.
+ * @return \c true if the environment call is available,
+ * even if the value returned in \c data is NULL.
+ * @note Historically, some cores would use this folder for save data such as memory cards or SRAM.
+ * This is now discouraged in favor of \c RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY.
+ * @see RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY
+ */
+#define RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY 9
+
+/**
+ * Sets the internal pixel format used by the frontend for rendering.
+ * The default pixel format is \c RETRO_PIXEL_FORMAT_0RGB1555 for compatibility reasons,
+ * although it's considered deprecated and shouldn't be used by new code.
+ *
+ * @param[in] data const enum retro_pixel_format *.
+ * Pointer to the pixel format to use.
+ * @returns \c true if the pixel format was set successfully,
+ * \c false if it's not supported or this callback is unavailable.
+ * @note This function should be called inside \c retro_load_game()
+ * or retro_get_system_av_info().
+ * @see retro_pixel_format
+ */
+#define RETRO_ENVIRONMENT_SET_PIXEL_FORMAT 10
+
+/**
+ * Sets an array of input descriptors for the frontend
+ * to present to the user for configuring the core's controls.
+ *
+ * This function can be called at any time,
+ * preferably early in the core's life cycle.
+ * Ideally, no later than \c retro_load_game().
+ *
+ * @param[in] data const struct retro_input_descriptor *.
+ * An array of input descriptors terminated by one whose
+ * \c retro_input_descriptor::description field is set to \c NULL.
+ * Behavior is undefined if \c NULL.
+ * @return \c true if the environment call is recognized.
+ * @see retro_input_descriptor
+ */
+#define RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS 11
+
+/**
+ * Sets a callback function used to notify the core about keyboard events.
+ * This should only be used for cores that specifically need keyboard input,
+ * such as for home computer emulators or games with text entry.
+ *
+ * @param[in] data const struct retro_keyboard_callback *.
+ * Pointer to the callback function.
+ * Behavior is undefined if NULL.
+ * @return \c true if the environment call is recognized.
+ * @see retro_keyboard_callback
+ * @see retro_key
+ */
+#define RETRO_ENVIRONMENT_SET_KEYBOARD_CALLBACK 12
+
+/**
+ * Sets an interface that the frontend can use to insert and remove disks
+ * from the emulated console's disk drive.
+ * Can be used for optical disks, floppy disks, or any other game storage medium
+ * that can be swapped at runtime.
+ *
+ * This is intended for multi-disk games that expect the player
+ * to manually swap disks at certain points in the game.
+ *
+ * @deprecated Prefer using \c RETRO_ENVIRONMENT_SET_DISK_CONTROL_EXT_INTERFACE
+ * over this environment call, as it supports additional features.
+ * Only use this callback to maintain compatibility
+ * with older cores or frontends.
+ *
+ * @param[in] data const struct retro_disk_control_callback *.
+ * Pointer to the callback functions to use.
+ * May be \c NULL, in which case the existing disk callback is deregistered.
+ * @return \c true if this environment call is available,
+ * even if \c data is \c NULL.
+ * @see retro_disk_control_callback
+ * @see RETRO_ENVIRONMENT_SET_DISK_CONTROL_EXT_INTERFACE
+ */
+#define RETRO_ENVIRONMENT_SET_DISK_CONTROL_INTERFACE 13
+
+/**
+ * Requests that a frontend enable a particular hardware rendering API.
+ *
+ * If successful, the frontend will create a context (and other related resources)
+ * that the core can use for rendering.
+ * The framebuffer will be at least as large as
+ * the maximum dimensions provided in retro_get_system_av_info.
+ *
+ * @param[in, out] data struct retro_hw_render_callback *.
+ * Pointer to the hardware render callback struct.
+ * Used to define callbacks for the hardware-rendering life cycle,
+ * as well as to request a particular rendering API.
+ * @return \c true if the environment call is recognized
+ * and the requested rendering API is supported.
+ * \c false if \c data is \c NULL
+ * or the frontend can't provide the requested rendering API.
+ * @see retro_hw_render_callback
+ * @see retro_video_refresh_t
+ * @see RETRO_ENVIRONMENT_GET_PREFERRED_HW_RENDER
+ * @note Should be called in retro_load_game().
+ * @note If HW rendering is used, pass only \c RETRO_HW_FRAME_BUFFER_VALID or
+ * \c NULL to retro_video_refresh_t.
+ */
+#define RETRO_ENVIRONMENT_SET_HW_RENDER 14
+
+/**
+ * Retrieves a core option's value from the frontend.
+ * \c retro_variable::key should be set to an option key
+ * that was previously set in \c RETRO_ENVIRONMENT_SET_VARIABLES
+ * (or a similar environment call).
+ *
+ * @param[in,out] data struct retro_variable *.
+ * Pointer to a single \c retro_variable struct.
+ * See the documentation for \c retro_variable for details
+ * on which fields are set by the frontend or core.
+ * May be \c NULL.
+ * @returns \c true if the environment call is available,
+ * even if \c data is \c NULL or the key it specifies is not found.
+ * @note Passing \c NULL in to \c data can be useful to
+ * test for support of this environment call without looking up any variables.
+ * @see retro_variable
+ * @see RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2
+ * @see RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE
+ */
+#define RETRO_ENVIRONMENT_GET_VARIABLE 15
+
+/**
+ * Notifies the frontend of the core's available options.
+ *
+ * The core may check these options later using \c RETRO_ENVIRONMENT_GET_VARIABLE.
+ * The frontend may also present these options to the user
+ * in its own configuration UI.
+ *
+ * This should be called the first time as early as possible,
+ * ideally in \c retro_set_environment.
+ * The core may later call this function again
+ * to communicate updated options to the frontend,
+ * but the number of core options must not change.
+ *
+ * Here's an example that sets two options.
+ *
+ * @code
+ * void set_variables_example(void)
+ * {
+ * struct retro_variable options[] = {
+ * { "foo_speedhack", "Speed hack; false|true" }, // false by default
+ * { "foo_displayscale", "Display scale factor; 1|2|3|4" }, // 1 by default
+ * { NULL, NULL },
+ * };
+ *
+ * environ_cb(RETRO_ENVIRONMENT_SET_VARIABLES, &options);
+ * }
+ * @endcode
+ *
+ * The possible values will generally be displayed and stored as-is by the frontend.
+ *
+ * @deprecated Prefer using \c RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2 for new code,
+ * as it offers more features such as categories and translation.
+ * Only use this environment call to maintain compatibility
+ * with older frontends or cores.
+ * @note Keep the available options (and their possible values) as low as possible;
+ * it should be feasible to cycle through them without a keyboard.
+ * @param[in] data const struct retro_variable *.
+ * Pointer to an array of \c retro_variable structs that define available core options,
+ * terminated by a { NULL, NULL } element.
+ * The frontend must maintain its own copy of this array.
+ *
+ * @returns \c true if the environment call is available,
+ * even if \c data is NULL.
+ * @see retro_variable
+ * @see RETRO_ENVIRONMENT_GET_VARIABLE
+ * @see RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE
+ * @see RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2
+ */
+#define RETRO_ENVIRONMENT_SET_VARIABLES 16
+
+/**
+ * Queries whether at least one core option was updated by the frontend
+ * since the last call to \ref RETRO_ENVIRONMENT_GET_VARIABLE.
+ * This typically means that the user opened the core options menu and made some changes.
+ *
+ * Cores usually call this each frame before the core's main emulation logic.
+ * Specific options can then be queried with \ref RETRO_ENVIRONMENT_GET_VARIABLE.
+ *
+ * @param[out] data bool *.
+ * Set to \c true if at least one core option was updated
+ * since the last call to \ref RETRO_ENVIRONMENT_GET_VARIABLE.
+ * Behavior is undefined if this pointer is \c NULL.
+ * @returns \c true if the environment call is available.
+ * @see RETRO_ENVIRONMENT_GET_VARIABLE
+ * @see RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2
+ */
+#define RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE 17
+
+/**
+ * Notifies the frontend that this core can run without loading any content,
+ * such as when emulating a console that has built-in software.
+ * When a core is loaded without content,
+ * \c retro_load_game receives an argument of NULL.
+ * This should be called within \c retro_set_environment() only.
+ *
+ * @param[in] data const bool *.
+ * Pointer to a single \c bool that indicates whether this frontend can run without content.
+ * Can point to a value of \c false but this isn't necessary,
+ * as contentless support is opt-in.
+ * The behavior is undefined if \c data is NULL.
+ * @returns \c true if the environment call is available.
+ * @see retro_load_game
+ */
+#define RETRO_ENVIRONMENT_SET_SUPPORT_NO_GAME 18
+
+/**
+ * Retrieves the absolute path from which this core was loaded.
+ * Useful when loading assets from paths relative to the core,
+ * as is sometimes the case when using RETRO_ENVIRONMENT_SET_SUPPORT_NO_GAME.
+ *
+ * @param[out] data const char **.
+ * Pointer to a string in which the core's path will be saved.
+ * The string is managed by the frontend and must not be modified or freed by the core.
+ * May be \c NULL if the core is statically linked to the frontend
+ * or if the core's path otherwise cannot be determined.
+ * Behavior is undefined if \c data is NULL.
+ * @returns \c true if the environment call is available.
+ */
+#define RETRO_ENVIRONMENT_GET_LIBRETRO_PATH 19
+
+/* Environment call 20 was an obsolete version of SET_AUDIO_CALLBACK.
+ * It was not used by any known core at the time, and was removed from the API.
+ * The number 20 is reserved to prevent ABI clashes.
+ */
+
+/**
+ * Sets a callback that notifies the core of how much time has passed
+ * since the last iteration of retro_run.
+ * If the frontend is not running the core in real time
+ * (e.g. it's frame-stepping or running in slow motion),
+ * then the reference value will be provided to the callback instead.
+ *
+ * @param[in] data const struct retro_frame_time_callback *.
+ * Pointer to a single \c retro_frame_time_callback struct.
+ * Behavior is undefined if \c data is NULL.
+ * @returns \c true if the environment call is available.
+ * @note Frontends may disable this environment call in certain situations.
+ * It will return \c false in those cases.
+ * @see retro_frame_time_callback
+ */
+#define RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK 21
+
+/**
+ * Registers a set of functions that the frontend can use
+ * to tell the core it's ready for audio output.
+ *
+ * It is intended for games that feature asynchronous audio.
+ * It should not be used for emulators unless their audio is asynchronous.
+ *
+ *
+ * The callback only notifies about writability; the libretro core still
+ * has to call the normal audio callbacks
+ * to write audio. The audio callbacks must be called from within the
+ * notification callback.
+ * The amount of audio data to write is up to the core.
+ * Generally, the audio callback will be called continuously in a loop.
+ *
+ * A frontend may disable this callback in certain situations.
+ * The core must be able to render audio with the "normal" interface.
+ *
+ * @param[in] data const struct retro_audio_callback *.
+ * Pointer to a set of functions that the frontend will call to notify the core
+ * when it's ready to receive audio data.
+ * May be \c NULL, in which case the frontend will return
+ * whether this environment callback is available.
+ * @return \c true if this environment call is available,
+ * even if \c data is \c NULL.
+ * @warning The provided callbacks can be invoked from any thread,
+ * so their implementations \em must be thread-safe.
+ * @note If a core uses this callback,
+ * it should also use RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK.
+ * @see retro_audio_callback
+ * @see retro_audio_sample_t
+ * @see retro_audio_sample_batch_t
+ * @see RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK
+ */
+#define RETRO_ENVIRONMENT_SET_AUDIO_CALLBACK 22
+
+/**
+ * Gets an interface that a core can use to access a controller's rumble motors.
+ *
+ * The interface supports two independently-controlled motors,
+ * one strong and one weak.
+ *
+ * Should be called from either \c retro_init() or \c retro_load_game(),
+ * but not from \c retro_set_environment().
+ *
+ * @param[out] data struct retro_rumble_interface *.
+ * Pointer to the interface struct.
+ * Behavior is undefined if \c NULL.
+ * @returns \c true if the environment call is available,
+ * even if the current device doesn't support vibration.
+ * @see retro_rumble_interface
+ * @defgroup GET_RUMBLE_INTERFACE Rumble Interface
+ */
+#define RETRO_ENVIRONMENT_GET_RUMBLE_INTERFACE 23
+
+/**
+ * Returns the frontend's supported input device types.
+ *
+ * The supported device types are returned as a bitmask,
+ * with each value of \ref RETRO_DEVICE corresponding to a bit.
+ *
+ * Should only be called in \c retro_run().
+ *
+ * @code
+ * #define REQUIRED_DEVICES ((1 << RETRO_DEVICE_JOYPAD) | (1 << RETRO_DEVICE_ANALOG))
+ * void get_input_device_capabilities_example(void)
+ * {
+ * uint64_t capabilities;
+ * environ_cb(RETRO_ENVIRONMENT_GET_INPUT_DEVICE_CAPABILITIES, &capabilities);
+ * if ((capabilities & REQUIRED_DEVICES) == REQUIRED_DEVICES)
+ * printf("Joypad and analog device types are supported");
+ * }
+ * @endcode
+ *
+ * @param[out] data uint64_t *.
+ * Pointer to a bitmask of supported input device types.
+ * If the frontend supports a particular \c RETRO_DEVICE_* type,
+ * then the bit (1 << RETRO_DEVICE_*) will be set.
+ *
+ * Each bit represents a \c RETRO_DEVICE constant,
+ * e.g. bit 1 represents \c RETRO_DEVICE_JOYPAD,
+ * bit 2 represents \c RETRO_DEVICE_MOUSE, and so on.
+ *
+ * Bits that do not correspond to known device types will be set to zero
+ * and are reserved for future use.
+ *
+ * Behavior is undefined if \c NULL.
+ * @returns \c true if the environment call is available.
+ * @note If the frontend supports multiple input drivers,
+ * availability of this environment call (and the reported capabilities)
+ * may depend on the active driver.
+ * @see RETRO_DEVICE
+ */
+#define RETRO_ENVIRONMENT_GET_INPUT_DEVICE_CAPABILITIES 24
+
+/**
+ * Returns an interface that the core can use to access and configure available sensors,
+ * such as an accelerometer or gyroscope.
+ *
+ * @param[out] data struct retro_sensor_interface *.
+ * Pointer to the sensor interface that the frontend will populate.
+ * Behavior is undefined if is \c NULL.
+ * @returns \c true if the environment call is available,
+ * even if the device doesn't have any supported sensors.
+ * @see retro_sensor_interface
+ * @see retro_sensor_action
+ * @see RETRO_SENSOR
+ * @addtogroup RETRO_SENSOR
+ */
+#define RETRO_ENVIRONMENT_GET_SENSOR_INTERFACE (25 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+
+/**
+ * Gets an interface to the device's video camera.
+ *
+ * The frontend delivers new video frames via a user-defined callback
+ * that runs in the same thread as \c retro_run().
+ * Should be called in \c retro_load_game().
+ *
+ * @param[in,out] data struct retro_camera_callback *.
+ * Pointer to the camera driver interface.
+ * Some fields in the struct must be filled in by the core,
+ * others are provided by the frontend.
+ * Behavior is undefined if \c NULL.
+ * @returns \c true if this environment call is available,
+ * even if an actual camera isn't.
+ * @note This API only supports one video camera at a time.
+ * If the device provides multiple cameras (e.g. inner/outer cameras on a phone),
+ * the frontend will choose one to use.
+ * @see retro_camera_callback
+ * @see RETRO_ENVIRONMENT_SET_HW_RENDER
+ */
+#define RETRO_ENVIRONMENT_GET_CAMERA_INTERFACE (26 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+
+/**
+ * Gets an interface that the core can use for cross-platform logging.
+ * Certain platforms don't have a console or stderr,
+ * or they have their own preferred logging methods.
+ * The frontend itself may also display log output.
+ *
+ * @attention This should not be used for information that the player must immediately see,
+ * such as major errors or warnings.
+ * In most cases, this is best for information that will help you (the developer)
+ * identify problems when debugging or providing support.
+ * Unless a core or frontend is intended for advanced users,
+ * the player might not check (or even know about) their logs.
+ *
+ * @param[out] data struct retro_log_callback *.
+ * Pointer to the callback where the function pointer will be saved.
+ * Behavior is undefined if \c data is NULL.
+ * @returns \c true if the environment call is available.
+ * @see retro_log_callback
+ * @note Cores can fall back to \c stderr if this interface is not available.
+ */
+#define RETRO_ENVIRONMENT_GET_LOG_INTERFACE 27
+
+/**
+ * Returns an interface that the core can use for profiling code
+ * and to access performance-related information.
+ *
+ * This callback supports performance counters, a high-resolution timer,
+ * and listing available CPU features (mostly SIMD instructions).
+ *
+ * @param[out] data struct retro_perf_callback *.
+ * Pointer to the callback interface.
+ * Behavior is undefined if \c NULL.
+ * @returns \c true if the environment call is available.
+ * @see retro_perf_callback
+ */
+#define RETRO_ENVIRONMENT_GET_PERF_INTERFACE 28
+
+/**
+ * Returns an interface that the core can use to retrieve the device's location,
+ * including its current latitude and longitude.
+ *
+ * @param[out] data struct retro_location_callback *.
+ * Pointer to the callback interface.
+ * Behavior is undefined if \c NULL.
+ * @return \c true if the environment call is available,
+ * even if there's no location information available.
+ * @see retro_location_callback
+ */
+#define RETRO_ENVIRONMENT_GET_LOCATION_INTERFACE 29
+
+/**
+ * @deprecated An obsolete alias to \c RETRO_ENVIRONMENT_GET_CORE_ASSETS_DIRECTORY kept for compatibility.
+ * @see RETRO_ENVIRONMENT_GET_CORE_ASSETS_DIRECTORY
+ **/
+#define RETRO_ENVIRONMENT_GET_CONTENT_DIRECTORY 30
+
+/**
+ * Returns the frontend's "core assets" directory,
+ * which can be used to store assets that the core needs
+ * such as art assets or level data.
+ *
+ * @param[out] data const char **.
+ * Pointer to a string in which the core assets directory will be saved.
+ * This string is managed by the frontend and must not be modified or freed by the core.
+ * May be \c NULL if no core assets directory is defined,
+ * in which case the core should find an alternative directory.
+ * Behavior is undefined if \c data is NULL.
+ * @returns \c true if the environment call is available,
+ * even if the value returned in \c data is NULL.
+ */
+#define RETRO_ENVIRONMENT_GET_CORE_ASSETS_DIRECTORY 30
+
+/**
+ * Returns the frontend's save data directory, if available.
+ * This directory should be used to store game-specific save data,
+ * including memory card images.
+ *
+ * Although libretro provides an interface for cores to expose SRAM to the frontend,
+ * not all cores can support it correctly.
+ * In this case, cores should use this environment callback
+ * to save their game data to disk manually.
+ *
+ * Cores that use this environment callback
+ * should flush their save data to disk periodically and when unloading.
+ *
+ * @param[out] data const char **.
+ * Pointer to the string in which the save data directory will be saved.
+ * This string is managed by the frontend and must not be modified or freed by the core.
+ * May return \c NULL if no save data directory is defined.
+ * Behavior is undefined if \c data is NULL.
+ * @returns \c true if the environment call is available,
+ * even if the value returned in \c data is NULL.
+ * @note Early libretro cores used \c RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY for save data.
+ * This is still supported for backwards compatibility,
+ * but new cores should use this environment call instead.
+ * \c RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY should be used for game-agnostic data
+ * such as BIOS files or core-specific configuration.
+ * @note The returned directory may or may not be the same
+ * as the one used for \c retro_get_memory_data.
+ *
+ * @see retro_get_memory_data
+ * @see RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY
+ */
+#define RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY 31
+
+/**
+ * Sets new video and audio parameters for the core.
+ * This can only be called from within retro_run.
+ *
+ * This environment call may entail a full reinitialization of the frontend's audio/video drivers,
+ * hence it should \em only be used if the core needs to make drastic changes
+ * to audio/video parameters.
+ *
+ * This environment call should \em not be used when:
+ *
+ *
Changing the emulated system's internal resolution,
+ * within the limits defined by the existing values of \c max_width and \c max_height.
+ * Use \c RETRO_ENVIRONMENT_SET_GEOMETRY instead,
+ * and adjust \c retro_get_system_av_info to account for
+ * supported scale factors and screen layouts
+ * when computing \c max_width and \c max_height.
+ * Only use this environment call if \c max_width or \c max_height needs to increase.
+ *
Adjusting the screen's aspect ratio,
+ * e.g. when changing the layout of the screen(s).
+ * Use \c RETRO_ENVIRONMENT_SET_GEOMETRY or \c RETRO_ENVIRONMENT_SET_ROTATION instead.
+ *
+ *
+ * The frontend will reinitialize its audio and video drivers within this callback;
+ * after that happens, audio and video callbacks will target the newly-initialized driver,
+ * even within the same \c retro_run call.
+ *
+ * This callback makes it possible to support configurable resolutions
+ * while avoiding the need to compute the "worst case" values of \c max_width and \c max_height.
+ *
+ * @param[in] data const struct retro_system_av_info *.
+ * Pointer to the new video and audio parameters that the frontend should adopt.
+ * @returns \c true if the environment call is available
+ * and the new av_info struct was accepted.
+ * \c false if the environment call is unavailable or \c data is NULL.
+ * @see retro_system_av_info
+ * @see RETRO_ENVIRONMENT_SET_GEOMETRY
+ */
+#define RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO 32
+
+/**
+ * Provides an interface that a frontend can use
+ * to get function pointers from the core.
+ *
+ * This allows cores to define their own extensions to the libretro API,
+ * or to expose implementations of a frontend's libretro extensions.
+ *
+ * @param[in] data const struct retro_get_proc_address_interface *.
+ * Pointer to the interface that the frontend can use to get function pointers from the core.
+ * The frontend must maintain its own copy of this interface.
+ * @returns \c true if the environment call is available
+ * and the returned interface was accepted.
+ * @note The provided interface may be called at any time,
+ * even before this environment call returns.
+ * @note Extensions should be prefixed with the name of the frontend or core that defines them.
+ * For example, a frontend named "foo" that defines a debugging extension
+ * should expect the core to define functions prefixed with "foo_debug_".
+ * @warning If a core wants to use this environment call,
+ * it \em must do so from within \c retro_set_environment().
+ * @see retro_get_proc_address_interface
+ */
+#define RETRO_ENVIRONMENT_SET_PROC_ADDRESS_CALLBACK 33
+
+/**
+ * Registers a core's ability to handle "subsystems",
+ * which are secondary platforms that augment a core's primary emulated hardware.
+ *
+ * A core doesn't need to emulate a secondary platform
+ * in order to use it as a subsystem;
+ * as long as it can load a secondary file for some practical use,
+ * then this environment call is most likely suitable.
+ *
+ * Possible use cases of a subsystem include:
+ *
+ * \li Installing software onto an emulated console's internal storage,
+ * such as the Nintendo DSi.
+ * \li Emulating accessories that are used to support another console's games,
+ * such as the Super Game Boy or the N64 Transfer Pak.
+ * \li Inserting a secondary ROM into a console
+ * that features multiple cartridge ports,
+ * such as the Nintendo DS's Slot-2.
+ * \li Loading a save data file created and used by another core.
+ *
+ * Cores should \em not use subsystems for:
+ *
+ * \li Emulators that support multiple "primary" platforms,
+ * such as a Game Boy/Game Boy Advance core
+ * or a Sega Genesis/Sega CD/32X core.
+ * Use \c retro_system_content_info_override, \c retro_system_info,
+ * and/or runtime detection instead.
+ * \li Selecting different memory card images.
+ * Use dynamically-populated core options instead.
+ * \li Different variants of a single console,
+ * such the Game Boy vs. the Game Boy Color.
+ * Use core options or runtime detection instead.
+ * \li Games that span multiple disks.
+ * Use \c RETRO_ENVIRONMENT_SET_DISK_CONTROL_EXT_INTERFACE
+ * and m3u-formatted playlists instead.
+ * \li Console system files (BIOS, firmware, etc.).
+ * Use \c RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY
+ * and a common naming convention instead.
+ *
+ * When the frontend loads a game via a subsystem,
+ * it must call \c retro_load_game_special() instead of \c retro_load_game().
+ *
+ * @param[in] data const struct retro_subsystem_info *.
+ * Pointer to an array of subsystem descriptors,
+ * terminated by a zeroed-out \c retro_subsystem_info struct.
+ * The frontend should maintain its own copy
+ * of this array and the strings within it.
+ * Behavior is undefined if \c NULL.
+ * @returns \c true if this environment call is available.
+ * @note This environment call \em must be called from within \c retro_set_environment(),
+ * as frontends may need the registered information before loading a game.
+ * @see retro_subsystem_info
+ * @see retro_load_game_special
+ */
+#define RETRO_ENVIRONMENT_SET_SUBSYSTEM_INFO 34
+
+/**
+ * Declares one or more types of controllers supported by this core.
+ * The frontend may then allow the player to select one of these controllers in its menu.
+ *
+ * Many consoles had controllers that came in different versions,
+ * were extensible with peripherals,
+ * or could be held in multiple ways;
+ * this environment call can be used to represent these differences
+ * and adjust the core's behavior to match.
+ *
+ * Possible use cases include:
+ *
+ * \li Supporting different classes of a single controller that supported their own sets of games.
+ * For example, the SNES had two different lightguns (the Super Scope and the Justifier)
+ * whose games were incompatible with each other.
+ * \li Representing a platform's alternative controllers.
+ * For example, several platforms had music/rhythm games that included controllers
+ * shaped like musical instruments.
+ * \li Representing variants of a standard controller with additional inputs.
+ * For example, numerous consoles in the 90's introduced 6-button controllers for fighting games,
+ * steering wheels for racing games,
+ * or analog sticks for 3D platformers.
+ * \li Representing add-ons for consoles or standard controllers.
+ * For example, the 3DS had a Circle Pad Pro attachment that added a second analog stick.
+ * \li Selecting different configurations for a single controller.
+ * For example, the Wii Remote could be held sideways like a traditional game pad
+ * or in one hand like a wand.
+ * \li Providing multiple ways to simulate the experience of using a particular controller.
+ * For example, the Game Boy Advance featured several games
+ * with motion or light sensors in their cartridges;
+ * a core could provide controller configurations
+ * that allow emulating the sensors with either analog axes
+ * or with their host device's sensors.
+ *
+ * Should be called in retro_load_game.
+ * The frontend must maintain its own copy of the provided array,
+ * including all strings and subobjects.
+ * A core may exclude certain controllers for known incompatible games.
+ *
+ * When the frontend changes the active device for a particular port,
+ * it must call \c retro_set_controller_port_device() with that port's index
+ * and one of the IDs defined in its retro_controller_info::types field.
+ *
+ * Input ports are generally associated with different players
+ * (and the frontend's UI may reflect this with "Player 1" labels),
+ * but this is not required.
+ * Some games use multiple controllers for a single player,
+ * or some cores may use port indexes to represent an emulated console's
+ * alternative input peripherals.
+ *
+ * @param[in] data const struct retro_controller_info *.
+ * Pointer to an array of controller types defined by this core,
+ * terminated by a zeroed-out \c retro_controller_info.
+ * Each element of this array represents a controller port on the emulated device.
+ * Behavior is undefined if \c NULL.
+ * @returns \c true if this environment call is available.
+ * @see retro_controller_info
+ * @see retro_set_controller_port_device
+ * @see RETRO_DEVICE_SUBCLASS
+ */
+#define RETRO_ENVIRONMENT_SET_CONTROLLER_INFO 35
+
+/**
+ * Notifies the frontend of the address spaces used by the core's emulated hardware,
+ * and of the memory maps within these spaces.
+ * This can be used by the frontend to provide cheats, achievements, or debugging capabilities.
+ * Should only be used by emulators, as it makes little sense for game engines.
+ *
+ * @note Cores should also expose these address spaces
+ * through retro_get_memory_data and \c retro_get_memory_size if applicable;
+ * this environment call is not intended to replace those two functions,
+ * as the emulated hardware may feature memory regions outside of its own address space
+ * that are nevertheless useful for the frontend.
+ *
+ * @param[in] data const struct retro_memory_map *.
+ * Pointer to a single memory-map listing.
+ * The frontend must maintain its own copy of this object and its contents,
+ * including strings and nested objects.
+ * Behavior is undefined if \c NULL.
+ * @returns \c true if this environment call is available.
+ * @see retro_memory_map
+ * @see retro_get_memory_data
+ * @see retro_memory_descriptor
+ */
+#define RETRO_ENVIRONMENT_SET_MEMORY_MAPS (36 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+
+/**
+ * Resizes the viewport without reinitializing the video driver.
+ *
+ * Similar to \c RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO,
+ * but any changes that would require video reinitialization will not be performed.
+ * Can only be called from within \c retro_run().
+ *
+ * This environment call allows a core to revise the size of the viewport at will,
+ * which can be useful for emulated platforms that support dynamic resolution changes
+ * or for cores that support multiple screen layouts.
+ *
+ * A frontend must guarantee that this environment call completes in
+ * constant time.
+ *
+ * @param[in] data const struct retro_game_geometry *.
+ * Pointer to the new video parameters that the frontend should adopt.
+ * \c retro_game_geometry::max_width and \c retro_game_geometry::max_height
+ * will be ignored.
+ * Behavior is undefined if \c data is NULL.
+ * @return \c true if the environment call is available.
+ * @see RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO
+ */
+#define RETRO_ENVIRONMENT_SET_GEOMETRY 37
+
+/**
+ * Returns the name of the user, if possible.
+ * This callback is suitable for cores that offer personalization,
+ * such as online facilities or user profiles on the emulated system.
+ * @param[out] data const char **.
+ * Pointer to the user name string.
+ * May be \c NULL, in which case the core should use a default name.
+ * The returned pointer is owned by the frontend and must not be modified or freed by the core.
+ * Behavior is undefined if \c NULL.
+ * @returns \c true if the environment call is available,
+ * even if the frontend couldn't provide a name.
+ */
+#define RETRO_ENVIRONMENT_GET_USERNAME 38
+
+/**
+ * Returns the frontend's configured language.
+ * It can be used to localize the core's UI,
+ * or to customize the emulated firmware if applicable.
+ *
+ * @param[out] data retro_language *.
+ * Pointer to the language identifier.
+ * Behavior is undefined if \c NULL.
+ * @returns \c true if the environment call is available.
+ * @note The returned language may not be the same as the operating system's language.
+ * Cores should fall back to the operating system's language (or to English)
+ * if the environment call is unavailable or the returned language is unsupported.
+ * @see retro_language
+ * @see RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2_INTL
+ */
+#define RETRO_ENVIRONMENT_GET_LANGUAGE 39
+
+/**
+ * Returns a frontend-managed framebuffer
+ * that the core may render directly into
+ *
+ * This environment call is provided as an optimization
+ * for cores that use software rendering
+ * (i.e. that don't use \refitem RETRO_ENVIRONMENT_SET_HW_RENDER "a graphics hardware API");
+ * specifically, the intended use case is to allow a core
+ * to render directly into frontend-managed video memory,
+ * avoiding the bandwidth use that copying a whole framebuffer from core to video memory entails.
+ *
+ * Must be called every frame if used,
+ * as this may return a different framebuffer each frame
+ * (e.g. for swap chains).
+ * However, a core may render to a different buffer even if this call succeeds.
+ *
+ * @param[in,out] data struct retro_framebuffer *.
+ * Pointer to a frontend's frame buffer and accompanying data.
+ * Some fields are set by the core, others are set by the frontend.
+ * Only guaranteed to be valid for the duration of the current \c retro_run call,
+ * and must not be used afterwards.
+ * Behavior is undefined if \c NULL.
+ * @return \c true if the environment call was recognized
+ * and the framebuffer was successfully returned.
+ * @see retro_framebuffer
+ */
+#define RETRO_ENVIRONMENT_GET_CURRENT_SOFTWARE_FRAMEBUFFER (40 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+
+/**
+ * Returns an interface for accessing the data of specific rendering APIs.
+ * Not all hardware rendering APIs support or need this.
+ *
+ * The details of these interfaces are specific to each rendering API.
+ *
+ * @note \c retro_hw_render_callback::context_reset must be called by the frontend
+ * before this environment call can be used.
+ * Additionally, the contents of the returned interface are invalidated
+ * after \c retro_hw_render_callback::context_destroyed has been called.
+ * @param[out] data const struct retro_hw_render_interface **.
+ * The render interface for the currently-enabled hardware rendering API, if any.
+ * The frontend will store a pointer to the interface at the address provided here.
+ * The returned interface is owned by the frontend and must not be modified or freed by the core.
+ * Behavior is undefined if \c NULL.
+ * @return \c true if this environment call is available,
+ * the active graphics API has a libretro rendering interface,
+ * and the frontend is able to return said interface.
+ * \c false otherwise.
+ * @see RETRO_ENVIRONMENT_SET_HW_RENDER
+ * @see retro_hw_render_interface
+ * @note Since not every libretro-supported hardware rendering API
+ * has a \c retro_hw_render_interface implementation,
+ * a result of \c false is not necessarily an error.
+ */
+#define RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE (41 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+
+/**
+ * Explicitly notifies the frontend of whether this core supports achievements.
+ * The core must expose its emulated address space via
+ * \c retro_get_memory_data or \c RETRO_ENVIRONMENT_GET_MEMORY_MAPS.
+ * Must be called before the first call to retro_run.
+ *
+ * If \ref retro_get_memory_data returns a valid address
+ * but this environment call is not used,
+ * the frontend (at its discretion) may or may not opt in the core to its achievements support.
+ * whether this core is opted in to the frontend's achievement support
+ * is left to the frontend's discretion.
+ * @param[in] data const bool *.
+ * Pointer to a single \c bool that indicates whether this core supports achievements.
+ * Behavior is undefined if \c data is NULL.
+ * @returns \c true if the environment call is available.
+ * @see RETRO_ENVIRONMENT_SET_MEMORY_MAPS
+ * @see retro_get_memory_data
+ */
+#define RETRO_ENVIRONMENT_SET_SUPPORT_ACHIEVEMENTS (42 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+
+/**
+ * Defines an interface that the frontend can use
+ * to ask the core for the parameters it needs for a hardware rendering context.
+ * The exact semantics depend on \ref RETRO_ENVIRONMENT_SET_HW_RENDER "the active rendering API".
+ * Will be used some time after \c RETRO_ENVIRONMENT_SET_HW_RENDER is called,
+ * but before \c retro_hw_render_callback::context_reset is called.
+ *
+ * @param[in] data const struct retro_hw_render_context_negotiation_interface *.
+ * Pointer to the context negotiation interface.
+ * Will be populated by the frontend.
+ * Behavior is undefined if \c NULL.
+ * @return \c true if this environment call is supported,
+ * even if the current graphics API doesn't use
+ * a context negotiation interface (in which case the argument is ignored).
+ * @see retro_hw_render_context_negotiation_interface
+ * @see RETRO_ENVIRONMENT_GET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_SUPPORT
+ * @see RETRO_ENVIRONMENT_SET_HW_RENDER
+ */
+#define RETRO_ENVIRONMENT_SET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE (43 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+
+/**
+ * Notifies the frontend of any quirks associated with serialization.
+ *
+ * Should be set in either \c retro_init or \c retro_load_game, but not both.
+ * @param[in, out] data uint64_t *.
+ * Pointer to the core's serialization quirks.
+ * The frontend will set the flags of the quirks it supports
+ * and clear the flags of those it doesn't.
+ * Behavior is undefined if \c NULL.
+ * @return \c true if this environment call is supported.
+ * @see retro_serialize
+ * @see retro_unserialize
+ * @see RETRO_SERIALIZATION_QUIRK
+ */
+#define RETRO_ENVIRONMENT_SET_SERIALIZATION_QUIRKS 44
+
+/**
+ * The frontend will try to use a "shared" context when setting up a hardware context.
+ * Mostly applicable to OpenGL.
+ *
+ * In order for this to have any effect,
+ * the core must call \c RETRO_ENVIRONMENT_SET_HW_RENDER at some point
+ * if it hasn't already.
+ *
+ * @param data Ignored.
+ * @returns \c true if the environment call is available
+ * and the frontend supports shared hardware contexts.
+ */
+#define RETRO_ENVIRONMENT_SET_HW_SHARED_CONTEXT (44 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+
+/**
+ * Returns an interface that the core can use to access the file system.
+ * Should be called as early as possible.
+ *
+ * @param[in,out] data struct retro_vfs_interface_info *.
+ * Information about the desired VFS interface,
+ * as well as the interface itself.
+ * Behavior is undefined if \c NULL.
+ * @return \c true if this environment call is available
+ * and the frontend can provide a VFS interface of the requested version or newer.
+ * @see retro_vfs_interface_info
+ * @see file_path
+ * @see retro_dirent
+ * @see file_stream
+ */
+#define RETRO_ENVIRONMENT_GET_VFS_INTERFACE (45 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+
+/**
+ * Returns an interface that the core can use
+ * to set the state of any accessible device LEDs.
+ *
+ * @param[out] data struct retro_led_interface *.
+ * Pointer to the LED interface that the frontend will populate.
+ * May be \c NULL, in which case the frontend will only return
+ * whether this environment callback is available.
+ * @returns \c true if the environment call is available,
+ * even if \c data is \c NULL
+ * or no LEDs are accessible.
+ * @see retro_led_interface
+ */
+#define RETRO_ENVIRONMENT_GET_LED_INTERFACE (46 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+
+/**
+ * Returns hints about certain steps that the core may skip for this frame.
+ *
+ * A frontend may not need a core to generate audio or video in certain situations;
+ * this environment call sets a bitmask that indicates
+ * which steps the core may skip for this frame.
+ *
+ * This can be used to increase performance for some frontend features.
+ *
+ * @note Emulation accuracy should not be compromised;
+ * for example, if a core emulates a platform that supports display capture
+ * (i.e. looking at its own VRAM), then it should perform its rendering as normal
+ * unless it can prove that the emulated game is not using display capture.
+ *
+ * @param[out] data retro_av_enable_flags *.
+ * Pointer to the bitmask of steps that the frontend will skip.
+ * Other bits are set to zero and are reserved for future use.
+ * If \c NULL, the frontend will only return whether this environment callback is available.
+ * @returns \c true if the environment call is available,
+ * regardless of the value output to \c data.
+ * If \c false, the core should assume that the frontend will not skip any steps.
+ * @see retro_av_enable_flags
+ */
+#define RETRO_ENVIRONMENT_GET_AUDIO_VIDEO_ENABLE (47 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+
+/**
+ * Gets an interface that the core can use for raw MIDI I/O.
+ *
+ * @param[out] data struct retro_midi_interface *.
+ * Pointer to the MIDI interface.
+ * May be \c NULL.
+ * @return \c true if the environment call is available,
+ * even if \c data is \c NULL.
+ * @see retro_midi_interface
+ */
+#define RETRO_ENVIRONMENT_GET_MIDI_INTERFACE (48 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+
+/**
+ * Asks the frontend if it's currently in fast-forward mode.
+ * @param[out] data bool *.
+ * Set to \c true if the frontend is currently fast-forwarding its main loop.
+ * Behavior is undefined if \c data is NULL.
+ * @returns \c true if this environment call is available,
+ * regardless of the value returned in \c data.
+ *
+ * @see RETRO_ENVIRONMENT_SET_FASTFORWARDING_OVERRIDE
+ */
+#define RETRO_ENVIRONMENT_GET_FASTFORWARDING (49 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+
+/**
+ * Returns the refresh rate the frontend is targeting, in Hz.
+ * The intended use case is for the core to use the result to select an ideal refresh rate.
+ *
+ * @param[out] data float *.
+ * Pointer to the \c float in which the frontend will store its target refresh rate.
+ * Behavior is undefined if \c data is NULL.
+ * @return \c true if this environment call is available,
+ * regardless of the value returned in \c data.
+*/
+#define RETRO_ENVIRONMENT_GET_TARGET_REFRESH_RATE (50 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+
+/**
+ * Returns whether the frontend can return the state of all buttons at once as a bitmask,
+ * rather than requiring a series of individual calls to \c retro_input_state_t.
+ *
+ * If this callback returns \c true,
+ * you can get the state of all buttons by passing \c RETRO_DEVICE_ID_JOYPAD_MASK
+ * as the \c id parameter to \c retro_input_state_t.
+ * Bit #N represents the RETRO_DEVICE_ID_JOYPAD constant of value N,
+ * e.g. (1 << RETRO_DEVICE_ID_JOYPAD_A) represents the A button.
+ *
+ * @param data Ignored.
+ * @returns \c true if the frontend can report the complete digital joypad state as a bitmask.
+ * @see retro_input_state_t
+ * @see RETRO_DEVICE_JOYPAD
+ * @see RETRO_DEVICE_ID_JOYPAD_MASK
+ */
+#define RETRO_ENVIRONMENT_GET_INPUT_BITMASKS (51 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+
+/**
+ * Returns the version of the core options API supported by the frontend.
+ *
+ * Over the years, libretro has used several interfaces
+ * for allowing cores to define customizable options.
+ * \ref SET_CORE_OPTIONS_V2 "Version 2 of the interface"
+ * is currently preferred due to its extra features,
+ * but cores and frontends should strive to support
+ * versions \ref RETRO_ENVIRONMENT_SET_CORE_OPTIONS "1"
+ * and \ref RETRO_ENVIRONMENT_SET_VARIABLES "0" as well.
+ * This environment call provides the information that cores need for that purpose.
+ *
+ * If this environment call returns \c false,
+ * then the core should assume version 0 of the core options API.
+ *
+ * @param[out] data unsigned *.
+ * Pointer to the integer that will store the frontend's
+ * supported core options API version.
+ * Behavior is undefined if \c NULL.
+ * @returns \c true if the environment call is available,
+ * \c false otherwise.
+ * @see RETRO_ENVIRONMENT_SET_VARIABLES
+ * @see RETRO_ENVIRONMENT_SET_CORE_OPTIONS
+ * @see RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2
+ */
+#define RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION 52
+
+/**
+ * @copybrief RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2
+ *
+ * @deprecated This environment call has been superseded
+ * by RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2,
+ * which supports categorizing options into groups.
+ * This environment call should only be used to maintain compatibility
+ * with older cores and frontends.
+ *
+ * This environment call is intended to replace \c RETRO_ENVIRONMENT_SET_VARIABLES,
+ * and should only be called if \c RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION
+ * returns an API version of at least 1.
+ *
+ * This should be called the first time as early as possible,
+ * ideally in \c retro_set_environment (but \c retro_load_game is acceptable).
+ * It may then be called again later to update
+ * the core's options and their associated values,
+ * as long as the number of options doesn't change
+ * from the number given in the first call.
+ *
+ * The core can retrieve option values at any time with \c RETRO_ENVIRONMENT_GET_VARIABLE.
+ * If a saved value for a core option doesn't match the option definition's values,
+ * the frontend may treat it as incorrect and revert to the default.
+ *
+ * Core options and their values are usually defined in a large static array,
+ * but they may be generated at runtime based on the loaded game or system state.
+ * Here are some use cases for that:
+ *
+ * @li Selecting a particular file from one of the
+ * \ref RETRO_ENVIRONMENT_GET_ASSET_DIRECTORY "frontend's"
+ * \ref RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY "content"
+ * \ref RETRO_ENVIRONMENT_GET_CORE_ASSETS_DIRECTORY "directories",
+ * such as a memory card image or figurine data file.
+ * @li Excluding options that are not relevant to the current game,
+ * for cores that define a large number of possible options.
+ * @li Choosing a default value at runtime for a specific game,
+ * such as a BIOS file whose region matches that of the loaded content.
+ *
+ * @note A guiding principle of libretro's API design is that
+ * all common interactions (gameplay, menu navigation, etc.)
+ * should be possible without a keyboard.
+ * This implies that cores should keep the number of options and values
+ * as low as possible.
+ *
+ * Example entry:
+ * @code
+ * {
+ * "foo_option",
+ * "Speed hack coprocessor X",
+ * "Provides increased performance at the expense of reduced accuracy",
+ * {
+ * { "false", NULL },
+ * { "true", NULL },
+ * { "unstable", "Turbo (Unstable)" },
+ * { NULL, NULL },
+ * },
+ * "false"
+ * }
+ * @endcode
+ *
+ * @param[in] data const struct retro_core_option_definition *.
+ * Pointer to one or more core option definitions,
+ * terminated by a \ref retro_core_option_definition whose values are all zero.
+ * May be \c NULL, in which case the frontend will remove all existing core options.
+ * The frontend must maintain its own copy of this object,
+ * including all strings and subobjects.
+ * @return \c true if this environment call is available.
+ *
+ * @see retro_core_option_definition
+ * @see RETRO_ENVIRONMENT_GET_VARIABLE
+ * @see RETRO_ENVIRONMENT_SET_CORE_OPTIONS_INTL
+ */
+#define RETRO_ENVIRONMENT_SET_CORE_OPTIONS 53
+
+/**
+ * A variant of \ref RETRO_ENVIRONMENT_SET_CORE_OPTIONS
+ * that supports internationalization.
+ *
+ * @deprecated This environment call has been superseded
+ * by \ref RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2_INTL,
+ * which supports categorizing options into groups
+ * (plus translating the groups themselves).
+ * Only use this environment call to maintain compatibility
+ * with older cores and frontends.
+ *
+ * This should be called instead of \c RETRO_ENVIRONMENT_SET_CORE_OPTIONS
+ * if the core provides translations for its options.
+ * General use is largely the same,
+ * but see \ref retro_core_options_intl for some important details.
+ *
+ * @param[in] data const struct retro_core_options_intl *.
+ * Pointer to a core's option values and their translations.
+ * @see retro_core_options_intl
+ * @see RETRO_ENVIRONMENT_SET_CORE_OPTIONS
+ */
+#define RETRO_ENVIRONMENT_SET_CORE_OPTIONS_INTL 54
+
+/**
+ * Notifies the frontend that it should show or hide the named core option.
+ *
+ * Some core options aren't relevant in all scenarios,
+ * such as a submenu for hardware rendering flags
+ * when the software renderer is configured.
+ * This environment call asks the frontend to stop (or start)
+ * showing the named core option to the player.
+ * This is only a hint, not a requirement;
+ * the frontend may ignore this environment call.
+ * By default, all core options are visible.
+ *
+ * @note This environment call must \em only affect a core option's visibility,
+ * not its functionality or availability.
+ * \ref RETRO_ENVIRONMENT_GET_VARIABLE "Getting an invisible core option"
+ * must behave normally.
+ *
+ * @param[in] data const struct retro_core_option_display *.
+ * Pointer to a descriptor for the option that the frontend should show or hide.
+ * May be \c NULL, in which case the frontend will only return
+ * whether this environment callback is available.
+ * @return \c true if this environment call is available,
+ * even if \c data is \c NULL
+ * or the specified option doesn't exist.
+ * @see retro_core_option_display
+ * @see RETRO_ENVIRONMENT_SET_CORE_OPTIONS_UPDATE_DISPLAY_CALLBACK
+ */
+#define RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY 55
+
+/**
+ * Returns the frontend's preferred hardware rendering API.
+ * Cores should use this information to decide which API to use with \c RETRO_ENVIRONMENT_SET_HW_RENDER.
+ * @param[out] data retro_hw_context_type *.
+ * Pointer to the hardware context type.
+ * Behavior is undefined if \c data is NULL.
+ * This value will be set even if the environment call returns false,
+ * unless the frontend doesn't implement it.
+ * @returns \c true if the environment call is available
+ * and the frontend is able to use a hardware rendering API besides the one returned.
+ * If \c false is returned and the core cannot use the preferred rendering API,
+ * then it should exit or fall back to software rendering.
+ * @note The returned value does not indicate which API is currently in use.
+ * For example, the frontend may return \c RETRO_HW_CONTEXT_OPENGL
+ * while a Direct3D context from a previous session is active;
+ * this would signal that the frontend's current preference is for OpenGL,
+ * possibly because the user changed their frontend's video driver while a game is running.
+ * @see retro_hw_context_type
+ * @see RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE
+ * @see RETRO_ENVIRONMENT_SET_HW_RENDER
+ */
+#define RETRO_ENVIRONMENT_GET_PREFERRED_HW_RENDER 56
+
+/**
+ * Returns the minimum version of the disk control interface supported by the frontend.
+ *
+ * If this environment call returns \c false or \c data is 0 or greater,
+ * then cores may use disk control callbacks
+ * with \c RETRO_ENVIRONMENT_SET_DISK_CONTROL_INTERFACE.
+ * If the reported version is 1 or greater,
+ * then cores should use \c RETRO_ENVIRONMENT_SET_DISK_CONTROL_EXT_INTERFACE instead.
+ *
+ * @param[out] data unsigned *.
+ * Pointer to the unsigned integer that the frontend's supported disk control interface version will be stored in.
+ * Behavior is undefined if \c NULL.
+ * @return \c true if this environment call is available.
+ * @see RETRO_ENVIRONMENT_SET_DISK_CONTROL_EXT_INTERFACE
+ */
+#define RETRO_ENVIRONMENT_GET_DISK_CONTROL_INTERFACE_VERSION 57
+
+/**
+ * @copybrief RETRO_ENVIRONMENT_SET_DISK_CONTROL_INTERFACE
+ *
+ * This is intended for multi-disk games that expect the player
+ * to manually swap disks at certain points in the game.
+ * This version of the disk control interface provides
+ * more information about disk images.
+ * Should be called in \c retro_init.
+ *
+ * @param[in] data const struct retro_disk_control_ext_callback *.
+ * Pointer to the callback functions to use.
+ * May be \c NULL, in which case the existing disk callback is deregistered.
+ * @return \c true if this environment call is available,
+ * even if \c data is \c NULL.
+ * @see retro_disk_control_ext_callback
+ */
+#define RETRO_ENVIRONMENT_SET_DISK_CONTROL_EXT_INTERFACE 58
+
+/**
+ * Returns the version of the message interface supported by the frontend.
+ *
+ * A version of 0 indicates that the frontend
+ * only supports the legacy \c RETRO_ENVIRONMENT_SET_MESSAGE interface.
+ * A version of 1 indicates that the frontend
+ * supports \c RETRO_ENVIRONMENT_SET_MESSAGE_EXT as well.
+ * If this environment call returns \c false,
+ * the core should behave as if it had returned 0.
+ *
+ * @param[out] data unsigned *.
+ * Pointer to the result returned by the frontend.
+ * Behavior is undefined if \c NULL.
+ * @return \c true if this environment call is available.
+ * @see RETRO_ENVIRONMENT_SET_MESSAGE_EXT
+ * @see RETRO_ENVIRONMENT_SET_MESSAGE
+ */
+#define RETRO_ENVIRONMENT_GET_MESSAGE_INTERFACE_VERSION 59
+
+/**
+ * Displays a user-facing message for a short time.
+ *
+ * Use this callback to convey important status messages,
+ * such as errors or the result of long-running operations.
+ * For trivial messages or logging, use \c RETRO_ENVIRONMENT_GET_LOG_INTERFACE or \c stderr.
+ *
+ * This environment call supersedes \c RETRO_ENVIRONMENT_SET_MESSAGE,
+ * as it provides many more ways to customize
+ * how a message is presented to the player.
+ * However, a frontend that supports this environment call
+ * must still support \c RETRO_ENVIRONMENT_SET_MESSAGE.
+ *
+ * @param[in] data const struct retro_message_ext *.
+ * Pointer to the message to display to the player.
+ * Behavior is undefined if \c NULL.
+ * @returns \c true if this environment call is available.
+ * @see retro_message_ext
+ * @see RETRO_ENVIRONMENT_GET_MESSAGE_INTERFACE_VERSION
+ */
+#define RETRO_ENVIRONMENT_SET_MESSAGE_EXT 60
+
+/**
+ * Returns the number of active input devices currently provided by the frontend.
+ *
+ * This may change between frames,
+ * but will remain constant for the duration of each frame.
+ *
+ * If this callback returns \c true,
+ * a core need not poll any input device
+ * with an index greater than or equal to the returned value.
+ *
+ * If callback returns \c false,
+ * the number of active input devices is unknown.
+ * In this case, all input devices should be considered active.
+ *
+ * @param[out] data unsigned *.
+ * Pointer to the result returned by the frontend.
+ * Behavior is undefined if \c NULL.
+ * @return \c true if this environment call is available.
+ */
+#define RETRO_ENVIRONMENT_GET_INPUT_MAX_USERS 61
+
+/**
+ * Registers a callback that the frontend can use to notify the core
+ * of the audio output buffer's occupancy.
+ * Can be used by a core to attempt frame-skipping to avoid buffer under-runs
+ * (i.e. "crackling" sounds).
+ *
+ * @param[in] data const struct retro_audio_buffer_status_callback *.
+ * Pointer to the the buffer status callback,
+ * or \c NULL to unregister any existing callback.
+ * @return \c true if this environment call is available,
+ * even if \c data is \c NULL.
+ *
+ * @see retro_audio_buffer_status_callback
+ */
+#define RETRO_ENVIRONMENT_SET_AUDIO_BUFFER_STATUS_CALLBACK 62
+
+/**
+ * Requests a minimum frontend audio latency in milliseconds.
+ *
+ * This is a hint; the frontend may assign a different audio latency
+ * to accommodate hardware limits,
+ * although it should try to honor requests up to 512ms.
+ *
+ * This callback has no effect if the requested latency
+ * is less than the frontend's current audio latency.
+ * If value is zero or \c data is \c NULL,
+ * the frontend should set its default audio latency.
+ *
+ * May be used by a core to increase audio latency and
+ * reduce the risk of buffer under-runs (crackling)
+ * when performing 'intensive' operations.
+ *
+ * A core using RETRO_ENVIRONMENT_SET_AUDIO_BUFFER_STATUS_CALLBACK
+ * to implement audio-buffer-based frame skipping can get good results
+ * by setting the audio latency to a high (typically 6x or 8x)
+ * integer multiple of the expected frame time.
+ *
+ * This can only be called from within \c retro_run().
+ *
+ * @warning This environment call may require the frontend to reinitialize its audio system.
+ * This environment call should be used sparingly.
+ * If the driver is reinitialized,
+ * \ref retro_audio_callback_t "all audio callbacks" will be updated
+ * to target the newly-initialized driver.
+ *
+ * @param[in] data const unsigned *.
+ * Minimum audio latency, in milliseconds.
+ * @return \c true if this environment call is available,
+ * even if \c data is \c NULL.
+ *
+ * @see RETRO_ENVIRONMENT_SET_AUDIO_BUFFER_STATUS_CALLBACK
+ */
+#define RETRO_ENVIRONMENT_SET_MINIMUM_AUDIO_LATENCY 63
+
+/**
+ * Allows the core to tell the frontend when it should enable fast-forwarding,
+ * rather than relying solely on the frontend and user interaction.
+ *
+ * Possible use cases include:
+ *
+ * \li Temporarily disabling a core's fastforward support
+ * while investigating a related bug.
+ * \li Disabling fastforward during netplay sessions,
+ * or when using an emulated console's network features.
+ * \li Automatically speeding up the game when in a loading screen
+ * that cannot be shortened with high-level emulation.
+ *
+ * @param[in] data const struct retro_fastforwarding_override *.
+ * Pointer to the parameters that decide when and how
+ * the frontend is allowed to enable fast-forward mode.
+ * May be \c NULL, in which case the frontend will return \c true
+ * without updating the fastforward state,
+ * which can be used to detect support for this environment call.
+ * @return \c true if this environment call is available,
+ * even if \c data is \c NULL.
+ *
+ * @see retro_fastforwarding_override
+ * @see RETRO_ENVIRONMENT_GET_FASTFORWARDING
+ */
+#define RETRO_ENVIRONMENT_SET_FASTFORWARDING_OVERRIDE 64
+
+#define RETRO_ENVIRONMENT_SET_CONTENT_INFO_OVERRIDE 65
+ /* const struct retro_system_content_info_override * --
+ * Allows an implementation to override 'global' content
+ * info parameters reported by retro_get_system_info().
+ * Overrides also affect subsystem content info parameters
+ * set via RETRO_ENVIRONMENT_SET_SUBSYSTEM_INFO.
+ * This function must be called inside retro_set_environment().
+ * If callback returns false, content info overrides
+ * are unsupported by the frontend, and will be ignored.
+ * If callback returns true, extended game info may be
+ * retrieved by calling RETRO_ENVIRONMENT_GET_GAME_INFO_EXT
+ * in retro_load_game() or retro_load_game_special().
+ *
+ * 'data' points to an array of retro_system_content_info_override
+ * structs terminated by a { NULL, false, false } element.
+ * If 'data' is NULL, no changes will be made to the frontend;
+ * a core may therefore pass NULL in order to test whether
+ * the RETRO_ENVIRONMENT_SET_CONTENT_INFO_OVERRIDE and
+ * RETRO_ENVIRONMENT_GET_GAME_INFO_EXT callbacks are supported
+ * by the frontend.
+ *
+ * For struct member descriptions, see the definition of
+ * struct retro_system_content_info_override.
+ *
+ * Example:
+ *
+ * - struct retro_system_info:
+ * {
+ * "My Core", // library_name
+ * "v1.0", // library_version
+ * "m3u|md|cue|iso|chd|sms|gg|sg", // valid_extensions
+ * true, // need_fullpath
+ * false // block_extract
+ * }
+ *
+ * - Array of struct retro_system_content_info_override:
+ * {
+ * {
+ * "md|sms|gg", // extensions
+ * false, // need_fullpath
+ * true // persistent_data
+ * },
+ * {
+ * "sg", // extensions
+ * false, // need_fullpath
+ * false // persistent_data
+ * },
+ * { NULL, false, false }
+ * }
+ *
+ * Result:
+ * - Files of type m3u, cue, iso, chd will not be
+ * loaded by the frontend. Frontend will pass a
+ * valid path to the core, and core will handle
+ * loading internally
+ * - Files of type md, sms, gg will be loaded by
+ * the frontend. A valid memory buffer will be
+ * passed to the core. This memory buffer will
+ * remain valid until retro_deinit() returns
+ * - Files of type sg will be loaded by the frontend.
+ * A valid memory buffer will be passed to the core.
+ * This memory buffer will remain valid until
+ * retro_load_game() (or retro_load_game_special())
+ * returns
+ *
+ * NOTE: If an extension is listed multiple times in
+ * an array of retro_system_content_info_override
+ * structs, only the first instance will be registered
+ */
+
+#define RETRO_ENVIRONMENT_GET_GAME_INFO_EXT 66
+ /* const struct retro_game_info_ext ** --
+ * Allows an implementation to fetch extended game
+ * information, providing additional content path
+ * and memory buffer status details.
+ * This function may only be called inside
+ * retro_load_game() or retro_load_game_special().
+ * If callback returns false, extended game information
+ * is unsupported by the frontend. In this case, only
+ * regular retro_game_info will be available.
+ * RETRO_ENVIRONMENT_GET_GAME_INFO_EXT is guaranteed
+ * to return true if RETRO_ENVIRONMENT_SET_CONTENT_INFO_OVERRIDE
+ * returns true.
+ *
+ * 'data' points to an array of retro_game_info_ext structs.
+ *
+ * For struct member descriptions, see the definition of
+ * struct retro_game_info_ext.
+ *
+ * - If function is called inside retro_load_game(),
+ * the retro_game_info_ext array is guaranteed to
+ * have a size of 1 - i.e. the returned pointer may
+ * be used to access directly the members of the
+ * first retro_game_info_ext struct, for example:
+ *
+ * struct retro_game_info_ext *game_info_ext;
+ * if (environ_cb(RETRO_ENVIRONMENT_GET_GAME_INFO_EXT, &game_info_ext))
+ * printf("Content Directory: %s\n", game_info_ext->dir);
+ *
+ * - If the function is called inside retro_load_game_special(),
+ * the retro_game_info_ext array is guaranteed to have a
+ * size equal to the num_info argument passed to
+ * retro_load_game_special()
+ */
+
+/**
+ * Defines a set of core options that can be shown and configured by the frontend,
+ * so that the player may customize their gameplay experience to their liking.
+ *
+ * @note This environment call is intended to replace
+ * \c RETRO_ENVIRONMENT_SET_VARIABLES and \c RETRO_ENVIRONMENT_SET_CORE_OPTIONS,
+ * and should only be called if \c RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION
+ * returns an API version of at least 2.
+ *
+ * This should be called the first time as early as possible,
+ * ideally in \c retro_set_environment (but \c retro_load_game is acceptable).
+ * It may then be called again later to update
+ * the core's options and their associated values,
+ * as long as the number of options doesn't change
+ * from the number given in the first call.
+ *
+ * The core can retrieve option values at any time with \c RETRO_ENVIRONMENT_GET_VARIABLE.
+ * If a saved value for a core option doesn't match the option definition's values,
+ * the frontend may treat it as incorrect and revert to the default.
+ *
+ * Core options and their values are usually defined in a large static array,
+ * but they may be generated at runtime based on the loaded game or system state.
+ * Here are some use cases for that:
+ *
+ * @li Selecting a particular file from one of the
+ * \ref RETRO_ENVIRONMENT_GET_ASSET_DIRECTORY "frontend's"
+ * \ref RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY "content"
+ * \ref RETRO_ENVIRONMENT_GET_CORE_ASSETS_DIRECTORY "directories",
+ * such as a memory card image or figurine data file.
+ * @li Excluding options that are not relevant to the current game,
+ * for cores that define a large number of possible options.
+ * @li Choosing a default value at runtime for a specific game,
+ * such as a BIOS file whose region matches that of the loaded content.
+ *
+ * @note A guiding principle of libretro's API design is that
+ * all common interactions (gameplay, menu navigation, etc.)
+ * should be possible without a keyboard.
+ * This implies that cores should keep the number of options and values
+ * as low as possible.
+ *
+ * @param[in] data const struct retro_core_options_v2 *.
+ * Pointer to a core's options and their associated categories.
+ * May be \c NULL, in which case the frontend will remove all existing core options.
+ * The frontend must maintain its own copy of this object,
+ * including all strings and subobjects.
+ * @return \c true if this environment call is available
+ * and the frontend supports categories.
+ * Note that this environment call is guaranteed to successfully register
+ * the provided core options,
+ * so the return value does not indicate success or failure.
+ *
+ * @see retro_core_options_v2
+ * @see retro_core_option_v2_category
+ * @see retro_core_option_v2_definition
+ * @see RETRO_ENVIRONMENT_GET_VARIABLE
+ * @see RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2_INTL
+ */
+#define RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2 67
+
+/**
+ * A variant of \ref RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2
+ * that supports internationalization.
+ *
+ * This should be called instead of \c RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2
+ * if the core provides translations for its options.
+ * General use is largely the same,
+ * but see \ref retro_core_options_v2_intl for some important details.
+ *
+ * @param[in] data const struct retro_core_options_v2_intl *.
+ * Pointer to a core's option values and categories,
+ * plus a translation for each option and category.
+ * @see retro_core_options_v2_intl
+ * @see RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2
+ */
+#define RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2_INTL 68
+
+/**
+ * Registers a callback that the frontend can use
+ * to notify the core that at least one core option
+ * should be made hidden or visible.
+ * Allows a frontend to signal that a core must update
+ * the visibility of any dynamically hidden core options,
+ * and enables the frontend to detect visibility changes.
+ * Used by the frontend to update the menu display status
+ * of core options without requiring a call of retro_run().
+ * Must be called in retro_set_environment().
+ *
+ * @param[in] data const struct retro_core_options_update_display_callback *.
+ * The callback that the frontend should use.
+ * May be \c NULL, in which case the frontend will unset any existing callback.
+ * Can be used to query visibility support.
+ * @return \c true if this environment call is available,
+ * even if \c data is \c NULL.
+ * @see retro_core_options_update_display_callback
+ */
+#define RETRO_ENVIRONMENT_SET_CORE_OPTIONS_UPDATE_DISPLAY_CALLBACK 69
+
+/**
+ * Forcibly sets a core option's value.
+ *
+ * After changing a core option value with this callback,
+ * it will be reflected in the frontend
+ * and \ref RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE will return \c true.
+ * \ref retro_variable::key must match
+ * a \ref RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2 "previously-set core option",
+ * and \ref retro_variable::value must match one of its defined values.
+ *
+ * Possible use cases include:
+ *
+ * @li Allowing the player to set certain core options
+ * without entering the frontend's option menu,
+ * using an in-core hotkey.
+ * @li Adjusting invalid combinations of settings.
+ * @li Migrating settings from older releases of a core.
+ *
+ * @param[in] data const struct retro_variable *.
+ * Pointer to a single option that the core is changing.
+ * May be \c NULL, in which case the frontend will return \c true
+ * to indicate that this environment call is available.
+ * @return \c true if this environment call is available
+ * and the option named by \c key was successfully
+ * set to the given \c value.
+ * \c false if the \c key or \c value fields are \c NULL, empty,
+ * or don't match a previously set option.
+ *
+ * @see RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2
+ * @see RETRO_ENVIRONMENT_GET_VARIABLE
+ * @see RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE
+ */
+#define RETRO_ENVIRONMENT_SET_VARIABLE 70
+
+#define RETRO_ENVIRONMENT_GET_THROTTLE_STATE (71 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+ /* struct retro_throttle_state * --
+ * Allows an implementation to get details on the actual rate
+ * the frontend is attempting to call retro_run().
+ */
+
+/**
+ * Returns information about how the frontend will use savestates.
+ *
+ * @param[out] data retro_savestate_context *.
+ * Pointer to the current savestate context.
+ * May be \c NULL, in which case the environment call
+ * will return \c true to indicate its availability.
+ * @returns \c true if the environment call is available,
+ * even if \c data is \c NULL.
+ * @see retro_savestate_context
+ */
+#define RETRO_ENVIRONMENT_GET_SAVESTATE_CONTEXT (72 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+
+/**
+ * Before calling \c SET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE, will query which interface is supported.
+ *
+ * Frontend looks at \c retro_hw_render_interface_type and returns the maximum supported
+ * context negotiation interface version. If the \c retro_hw_render_interface_type is not
+ * supported or recognized by the frontend, a version of 0 must be returned in
+ * \c retro_hw_render_interface's \c interface_version and \c true is returned by frontend.
+ *
+ * If this environment call returns true with a \c interface_version greater than 0,
+ * a core can always use a negotiation interface version larger than what the frontend returns,
+ * but only earlier versions of the interface will be used by the frontend.
+ *
+ * A frontend must not reject a negotiation interface version that is larger than what the
+ * frontend supports. Instead, the frontend will use the older entry points that it recognizes.
+ * If this is incompatible with a particular core's requirements, it can error out early.
+ *
+ * @note Regarding backwards compatibility, this environment call was introduced after Vulkan v1
+ * context negotiation. If this environment call is not supported by frontend, i.e. the environment
+ * call returns \c false , only Vulkan v1 context negotiation is supported (if Vulkan HW rendering
+ * is supported at all). If a core uses Vulkan negotiation interface with version > 1, negotiation
+ * may fail unexpectedly. All future updates to the context negotiation interface implies that
+ * frontend must support this environment call to query support.
+ *
+ * @param[out] data struct retro_hw_render_context_negotiation_interface *.
+ * @return \c true if the environment call is available.
+ * @see SET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE
+ * @see retro_hw_render_interface_type
+ * @see retro_hw_render_context_negotiation_interface
+ */
+#define RETRO_ENVIRONMENT_GET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_SUPPORT (73 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+
+/**
+ * Asks the frontend whether JIT compilation can be used.
+ * Primarily used by iOS and tvOS.
+ * @param[out] data bool *.
+ * Set to \c true if the frontend has verified that JIT compilation is possible.
+ * @return \c true if the environment call is available.
+ */
+#define RETRO_ENVIRONMENT_GET_JIT_CAPABLE 74
+
+/**
+ * Returns an interface that the core can use to receive microphone input.
+ *
+ * @param[out] data retro_microphone_interface *.
+ * Pointer to the microphone interface.
+ * @return \true if microphone support is available,
+ * even if no microphones are plugged in.
+ * \c false if microphone support is disabled unavailable,
+ * or if \c data is \c NULL.
+ * @see retro_microphone_interface
+ */
+#define RETRO_ENVIRONMENT_GET_MICROPHONE_INTERFACE (75 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+
+/* Environment 76 was an obsolete version of RETRO_ENVIRONMENT_SET_NETPACKET_INTERFACE.
+* It was not used by any known core at the time, and was removed from the API. */
+
+/**
+ * Returns the device's current power state as reported by the frontend.
+ *
+ * This is useful for emulating the battery level in handheld consoles,
+ * or for reducing power consumption when on battery power.
+ *
+ * @note This environment call describes the power state for the entire device,
+ * not for individual peripherals like controllers.
+ *
+ * @param[out] data .
+ * Indicates whether the frontend can provide this information, even if the parameter
+ * is \c NULL. If the frontend does not support this functionality, then the provided
+ * argument will remain unchanged.
+ * @return \c true if the environment call is available.
+ * @see retro_device_power
+ */
+#define RETRO_ENVIRONMENT_GET_DEVICE_POWER (77 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+
+#define RETRO_ENVIRONMENT_SET_NETPACKET_INTERFACE 78
+ /* const struct retro_netpacket_callback * --
+ * When set, a core gains control over network packets sent and
+ * received during a multiplayer session. This can be used to
+ * emulate multiplayer games that were originally played on two
+ * or more separate consoles or computers connected together.
+ *
+ * The frontend will take care of connecting players together,
+ * and the core only needs to send the actual data as needed for
+ * the emulation, while handshake and connection management happen
+ * in the background.
+ *
+ * When two or more players are connected and this interface has
+ * been set, time manipulation features (such as pausing, slow motion,
+ * fast forward, rewinding, save state loading, etc.) are disabled to
+ * avoid interrupting communication.
+ *
+ * Should be set in either retro_init or retro_load_game, but not both.
+ *
+ * When not set, a frontend may use state serialization-based
+ * multiplayer, where a deterministic core supporting multiple
+ * input devices does not need to take any action on its own.
+ */
+
+/**
+ * Returns the device's current power state as reported by the frontend.
+ * This is useful for emulating the battery level in handheld consoles,
+ * or for reducing power consumption when on battery power.
+ *
+ * The return value indicates whether the frontend can provide this information,
+ * even if the parameter is \c NULL.
+ *
+ * If the frontend does not support this functionality,
+ * then the provided argument will remain unchanged.
+ * @param[out] data retro_device_power *.
+ * Pointer to the information that the frontend returns about its power state.
+ * May be \c NULL.
+ * @return \c true if the environment call is available,
+ * even if \c data is \c NULL.
+ * @see retro_device_power
+ * @note This environment call describes the power state for the entire device,
+ * not for individual peripherals like controllers.
+*/
+#define RETRO_ENVIRONMENT_GET_DEVICE_POWER (77 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+
+/**
+ * Returns the "playlist" directory of the frontend.
+ *
+ * This directory can be used to store core generated playlists, in case
+ * this internal functionality is available (e.g. internal core game detection
+ * engine).
+ *
+ * @param[out] data const char **.
+ * May be \c NULL. If so, no such directory is defined, and it's up to the
+ * implementation to find a suitable directory.
+ * @return \c true if the environment call is available.
+ */
+#define RETRO_ENVIRONMENT_GET_PLAYLIST_DIRECTORY 79
+
+/**
+ * Returns the "file browser" start directory of the frontend.
+ *
+ * This directory can serve as a start directory for the core in case it
+ * provides an internal way of loading content.
+ *
+ * @param[out] data const char **.
+ * May be \c NULL. If so, no such directory is defined, and it's up to the
+ * implementation to find a suitable directory.
+ * @return \c true if the environment call is available.
+ */
+#define RETRO_ENVIRONMENT_GET_FILE_BROWSER_START_DIRECTORY 80
+
+/**
+ * Returns the audio sample rate the frontend is targeting, in Hz.
+ * The intended use case is for the core to use the result to select an ideal sample rate.
+ *
+ * @param[out] data unsigned *.
+ * Pointer to the \c unsigned integer in which the frontend will store its target sample rate.
+ * Behavior is undefined if \c data is NULL.
+ * @return \c true if this environment call is available,
+ * regardless of the value returned in \c data.
+*/
+#define RETRO_ENVIRONMENT_GET_TARGET_SAMPLE_RATE (81 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+
+/**@}*/
+
+/**
+ * @defgroup GET_VFS_INTERFACE File System Interface
+ * @brief File system functionality.
+ *
+ * @section File Paths
+ * File paths passed to all libretro filesystem APIs shall be well formed UNIX-style,
+ * using "/" (unquoted forward slash) as the directory separator
+ * regardless of the platform's native separator.
+ *
+ * Paths shall also include at least one forward slash
+ * (e.g. use "./game.bin" instead of "game.bin").
+ *
+ * Other than the directory separator, cores shall not make assumptions about path format.
+ * The following paths are all valid:
+ * @li \c C:/path/game.bin
+ * @li \c http://example.com/game.bin
+ * @li \c #game/game.bin
+ * @li \c ./game.bin
+ *
+ * Cores may replace the basename or remove path components from the end, and/or add new components;
+ * however, cores shall not append "./", "../" or multiple consecutive forward slashes ("//") to paths they request from the front end.
+ *
+ * The frontend is encouraged to do the best it can when given an ill-formed path,
+ * but it is allowed to give up.
+ *
+ * Frontends are encouraged, but not required, to support native file system paths
+ * (including replacing the directory separator, if applicable).
+ *
+ * Cores are allowed to try using them, but must remain functional if the frontend rejects such requests.
+ *
+ * Cores are encouraged to use the libretro-common filestream functions for file I/O,
+ * as they seamlessly integrate with VFS,
+ * deal with directory separator replacement as appropriate
+ * and provide platform-specific fallbacks
+ * in cases where front ends do not provide their own VFS interface.
+ *
+ * @see RETRO_ENVIRONMENT_GET_VFS_INTERFACE
+ * @see retro_vfs_interface_info
+ * @see file_path
+ * @see retro_dirent
+ * @see file_stream
+ *
+ * @{
+ */
+
+/**
+ * Opaque file handle.
+ * @since VFS API v1
+ */
+struct retro_vfs_file_handle;
+
+/**
+ * Opaque directory handle.
+ * @since VFS API v3
+ */
+struct retro_vfs_dir_handle;
+
+/** @defgroup RETRO_VFS_FILE_ACCESS File Access Flags
+ * File access flags.
+ * @since VFS API v1
+ * @{
+ */
+
+/** Opens a file for read-only access. */
+#define RETRO_VFS_FILE_ACCESS_READ (1 << 0)
+
+/**
+ * Opens a file for write-only access.
+ * Any existing file at this path will be discarded and overwritten
+ * unless \c RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING is also specified.
+ */
+#define RETRO_VFS_FILE_ACCESS_WRITE (1 << 1)
+
+/**
+ * Opens a file for reading and writing.
+ * Any existing file at this path will be discarded and overwritten
+ * unless \c RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING is also specified.
+ */
+#define RETRO_VFS_FILE_ACCESS_READ_WRITE (RETRO_VFS_FILE_ACCESS_READ | RETRO_VFS_FILE_ACCESS_WRITE)
+
+/**
+ * Opens a file without discarding its existing contents.
+ * Only meaningful if \c RETRO_VFS_FILE_ACCESS_WRITE is specified.
+ */
+#define RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING (1 << 2) /* Prevents discarding content of existing files opened for writing */
+
+/** @} */
+
+/** @defgroup RETRO_VFS_FILE_ACCESS_HINT File Access Hints
+ *
+ * Hints to the frontend for how a file will be accessed.
+ * The VFS implementation may use these to optimize performance,
+ * react to external interference (such as concurrent writes),
+ * or it may ignore them entirely.
+ *
+ * Hint flags do not change the behavior of each VFS function
+ * unless otherwise noted.
+ * @{
+ */
+
+/** No particular hints are given. */
+#define RETRO_VFS_FILE_ACCESS_HINT_NONE (0)
+
+/**
+ * Indicates that the file will be accessed frequently.
+ *
+ * The frontend should cache it or map it into memory.
+ */
+#define RETRO_VFS_FILE_ACCESS_HINT_FREQUENT_ACCESS (1 << 0)
+
+/** @} */
+
+/** @defgroup RETRO_VFS_SEEK_POSITION File Seek Positions
+ * File access flags and hints.
+ * @{
+ */
+
+/**
+ * Indicates a seek relative to the start of the file.
+ */
+#define RETRO_VFS_SEEK_POSITION_START 0
+
+/**
+ * Indicates a seek relative to the current stream position.
+ */
+#define RETRO_VFS_SEEK_POSITION_CURRENT 1
+
+/**
+ * Indicates a seek relative to the end of the file.
+ * @note The offset passed to \c retro_vfs_seek_t should be negative.
+ */
+#define RETRO_VFS_SEEK_POSITION_END 2
+
+/** @} */
+
+/** @defgroup RETRO_VFS_STAT File Status Flags
+ * File stat flags.
+ * @see retro_vfs_stat_t
+ * @since VFS API v3
+ * @{
+ */
+
+/** Indicates that the given path refers to a valid file. */
+#define RETRO_VFS_STAT_IS_VALID (1 << 0)
+
+/** Indicates that the given path refers to a directory. */
+#define RETRO_VFS_STAT_IS_DIRECTORY (1 << 1)
+
+/**
+ * Indicates that the given path refers to a character special file,
+ * such as \c /dev/null.
+ */
+#define RETRO_VFS_STAT_IS_CHARACTER_SPECIAL (1 << 2)
+
+/** @} */
+
+/**
+ * Returns the path that was used to open this file.
+ *
+ * @param stream The opened file handle to get the path of.
+ * Behavior is undefined if \c NULL or closed.
+ * @return The path that was used to open \c stream.
+ * The string is owned by \c stream and must not be modified.
+ * @since VFS API v1
+ * @see filestream_get_path
+ */
+typedef const char *(RETRO_CALLCONV *retro_vfs_get_path_t)(struct retro_vfs_file_handle *stream);
+
+/**
+ * Open a file for reading or writing.
+ *
+ * @param path The path to open.
+ * @param mode A bitwise combination of \c RETRO_VFS_FILE_ACCESS flags.
+ * At a minimum, one of \c RETRO_VFS_FILE_ACCESS_READ or \c RETRO_VFS_FILE_ACCESS_WRITE must be specified.
+ * @param hints A bitwise combination of \c RETRO_VFS_FILE_ACCESS_HINT flags.
+ * @return A handle to the opened file,
+ * or \c NULL upon failure.
+ * Note that this will return \c NULL if \c path names a directory.
+ * The returned file handle must be closed with \c retro_vfs_close_t.
+ * @since VFS API v1
+ * @see File Paths
+ * @see RETRO_VFS_FILE_ACCESS
+ * @see RETRO_VFS_FILE_ACCESS_HINT
+ * @see retro_vfs_close_t
+ * @see filestream_open
+ */
+typedef struct retro_vfs_file_handle *(RETRO_CALLCONV *retro_vfs_open_t)(const char *path, unsigned mode, unsigned hints);
+
+/**
+ * Close the file and release its resources.
+ * All files returned by \c retro_vfs_open_t must be closed with this function.
+ *
+ * @param stream The file handle to close.
+ * Behavior is undefined if already closed.
+ * Upon completion of this function, \c stream is no longer valid
+ * (even if it returns failure).
+ * @return 0 on success, -1 on failure or if \c stream is \c NULL.
+ * @see retro_vfs_open_t
+ * @see filestream_close
+ * @since VFS API v1
+ */
+typedef int (RETRO_CALLCONV *retro_vfs_close_t)(struct retro_vfs_file_handle *stream);
+
+/**
+ * Return the size of the file in bytes.
+ *
+ * @param stream The file to query the size of.
+ * @return Size of the file in bytes, or -1 if there was an error.
+ * @see filestream_get_size
+ * @since VFS API v1
+ */
+typedef int64_t (RETRO_CALLCONV *retro_vfs_size_t)(struct retro_vfs_file_handle *stream);
+
+/**
+ * Set the file's length.
+ *
+ * @param stream The file whose length will be adjusted.
+ * @param length The new length of the file, in bytes.
+ * If shorter than the original length, the extra bytes will be discarded.
+ * If longer, the file's padding is unspecified (and likely platform-dependent).
+ * @return 0 on success,
+ * -1 on failure.
+ * @see filestream_truncate
+ * @since VFS API v2
+ */
+typedef int64_t (RETRO_CALLCONV *retro_vfs_truncate_t)(struct retro_vfs_file_handle *stream, int64_t length);
+
+/**
+ * Gets the given file's current read/write position.
+ * This position is advanced with each call to \c retro_vfs_read_t or \c retro_vfs_write_t.
+ *
+ * @param stream The file to query the position of.
+ * @return The current stream position in bytes
+ * or -1 if there was an error.
+ * @see filestream_tell
+ * @since VFS API v1
+ */
+typedef int64_t (RETRO_CALLCONV *retro_vfs_tell_t)(struct retro_vfs_file_handle *stream);
+
+/**
+ * Sets the given file handle's current read/write position.
+ *
+ * @param stream The file to set the position of.
+ * @param offset The new position, in bytes.
+ * @param seek_position The position to seek from.
+ * @return The new position,
+ * or -1 if there was an error.
+ * @since VFS API v1
+ * @see File Seek Positions
+ * @see filestream_seek
+ */
+typedef int64_t (RETRO_CALLCONV *retro_vfs_seek_t)(struct retro_vfs_file_handle *stream, int64_t offset, int seek_position);
+
+/**
+ * Read data from a file, if it was opened for reading.
+ *
+ * @param stream The file to read from.
+ * @param s The buffer to read into.
+ * @param len The number of bytes to read.
+ * The buffer pointed to by \c s must be this large.
+ * @return The number of bytes read,
+ * or -1 if there was an error.
+ * @since VFS API v1
+ * @see filestream_read
+ */
+typedef int64_t (RETRO_CALLCONV *retro_vfs_read_t)(struct retro_vfs_file_handle *stream, void *s, uint64_t len);
+
+/**
+ * Write data to a file, if it was opened for writing.
+ *
+ * @param stream The file handle to write to.
+ * @param s The buffer to write from.
+ * @param len The number of bytes to write.
+ * The buffer pointed to by \c s must be this large.
+ * @return The number of bytes written,
+ * or -1 if there was an error.
+ * @since VFS API v1
+ * @see filestream_write
+ */
+typedef int64_t (RETRO_CALLCONV *retro_vfs_write_t)(struct retro_vfs_file_handle *stream, const void *s, uint64_t len);
+
+/**
+ * Flush pending writes to the file, if applicable.
+ *
+ * This does not mean that the changes will be immediately persisted to disk;
+ * that may be scheduled for later, depending on the platform.
+ *
+ * @param stream The file handle to flush.
+ * @return 0 on success, -1 on failure.
+ * @since VFS API v1
+ * @see filestream_flush
+ */
+typedef int (RETRO_CALLCONV *retro_vfs_flush_t)(struct retro_vfs_file_handle *stream);
+
+/**
+ * Deletes the file at the given path.
+ *
+ * @param path The path to the file that will be deleted.
+ * @return 0 on success, -1 on failure.
+ * @see filestream_delete
+ * @since VFS API v1
+ */
+typedef int (RETRO_CALLCONV *retro_vfs_remove_t)(const char *path);
+
+/**
+ * Rename the specified file.
+ *
+ * @param old_path Path to an existing file.
+ * @param new_path The destination path.
+ * Must not name an existing file.
+ * @return 0 on success, -1 on failure
+ * @see filestream_rename
+ * @since VFS API v1
+ */
+typedef int (RETRO_CALLCONV *retro_vfs_rename_t)(const char *old_path, const char *new_path);
+
+/**
+ * Gets information about the given file.
+ *
+ * @param path The path to the file to query.
+ * @param[out] size The reported size of the file in bytes.
+ * May be \c NULL, in which case this value is ignored.
+ * @return A bitmask of \c RETRO_VFS_STAT flags,
+ * or 0 if \c path doesn't refer to a valid file.
+ * @see path_stat
+ * @see path_get_size
+ * @see RETRO_VFS_STAT
+ * @since VFS API v3
+ */
+typedef int (RETRO_CALLCONV *retro_vfs_stat_t)(const char *path, int32_t *size);
+
+/**
+ * Creates a directory at the given path.
+ *
+ * @param dir The desired location of the new directory.
+ * @return 0 if the directory was created,
+ * -2 if the directory already exists,
+ * or -1 if some other error occurred.
+ * @see path_mkdir
+ * @since VFS API v3
+ */
+typedef int (RETRO_CALLCONV *retro_vfs_mkdir_t)(const char *dir);
+
+/**
+ * Opens a handle to a directory so its contents can be inspected.
+ *
+ * @param dir The path to the directory to open.
+ * Must be an existing directory.
+ * @param include_hidden Whether to include hidden files in the directory listing.
+ * The exact semantics of this flag will depend on the platform.
+ * @return A handle to the opened directory,
+ * or \c NULL if there was an error.
+ * @see retro_opendir
+ * @since VFS API v3
+ */
+typedef struct retro_vfs_dir_handle *(RETRO_CALLCONV *retro_vfs_opendir_t)(const char *dir, bool include_hidden);
+
+/**
+ * Gets the next dirent ("directory entry")
+ * within the given directory.
+ *
+ * @param[in,out] dirstream The directory to read from.
+ * Updated to point to the next file, directory, or other path.
+ * @return \c true when the next dirent was retrieved,
+ * \c false if there are no more dirents to read.
+ * @note This API iterates over all files and directories within \c dirstream.
+ * Remember to check what the type of the current dirent is.
+ * @note This function does not recurse,
+ * i.e. it does not return the contents of subdirectories.
+ * @note This may include "." and ".." on Unix-like platforms.
+ * @see retro_readdir
+ * @see retro_vfs_dirent_is_dir_t
+ * @since VFS API v3
+ */
+typedef bool (RETRO_CALLCONV *retro_vfs_readdir_t)(struct retro_vfs_dir_handle *dirstream);
+
+/**
+ * Gets the filename of the current dirent.
+ *
+ * The returned string pointer is valid
+ * until the next call to \c retro_vfs_readdir_t or \c retro_vfs_closedir_t.
+ *
+ * @param dirstream The directory to read from.
+ * @return The current dirent's name,
+ * or \c NULL if there was an error.
+ * @note This function only returns the file's \em name,
+ * not a complete path to it.
+ * @see retro_dirent_get_name
+ * @since VFS API v3
+ */
+typedef const char *(RETRO_CALLCONV *retro_vfs_dirent_get_name_t)(struct retro_vfs_dir_handle *dirstream);
+
+/**
+ * Checks whether the current dirent names a directory.
+ *
+ * @param dirstream The directory to read from.
+ * @return \c true if \c dirstream's current dirent points to a directory,
+ * \c false if not or if there was an error.
+ * @see retro_dirent_is_dir
+ * @since VFS API v3
+ */
+typedef bool (RETRO_CALLCONV *retro_vfs_dirent_is_dir_t)(struct retro_vfs_dir_handle *dirstream);
+
+/**
+ * Closes the given directory and release its resources.
+ *
+ * Must be called on any \c retro_vfs_dir_handle returned by \c retro_vfs_open_t.
+ *
+ * @param dirstream The directory to close.
+ * When this function returns (even failure),
+ * \c dirstream will no longer be valid and must not be used.
+ * @return 0 on success, -1 on failure.
+ * @see retro_closedir
+ * @since VFS API v3
+ */
+typedef int (RETRO_CALLCONV *retro_vfs_closedir_t)(struct retro_vfs_dir_handle *dirstream);
+
+/**
+ * File system interface exposed by the frontend.
+ *
+ * @see dirent_vfs_init
+ * @see filestream_vfs_init
+ * @see path_vfs_init
+ * @see RETRO_ENVIRONMENT_GET_VFS_INTERFACE
+ */
+struct retro_vfs_interface
+{
+ /* VFS API v1 */
+ /** @copydoc retro_vfs_get_path_t */
+ retro_vfs_get_path_t get_path;
+
+ /** @copydoc retro_vfs_open_t */
+ retro_vfs_open_t open;
+
+ /** @copydoc retro_vfs_close_t */
+ retro_vfs_close_t close;
+
+ /** @copydoc retro_vfs_size_t */
+ retro_vfs_size_t size;
+
+ /** @copydoc retro_vfs_tell_t */
+ retro_vfs_tell_t tell;
+
+ /** @copydoc retro_vfs_seek_t */
+ retro_vfs_seek_t seek;
+
+ /** @copydoc retro_vfs_read_t */
+ retro_vfs_read_t read;
+
+ /** @copydoc retro_vfs_write_t */
+ retro_vfs_write_t write;
+
+ /** @copydoc retro_vfs_flush_t */
+ retro_vfs_flush_t flush;
+
+ /** @copydoc retro_vfs_remove_t */
+ retro_vfs_remove_t remove;
+
+ /** @copydoc retro_vfs_rename_t */
+ retro_vfs_rename_t rename;
+ /* VFS API v2 */
+
+ /** @copydoc retro_vfs_truncate_t */
+ retro_vfs_truncate_t truncate;
+ /* VFS API v3 */
+
+ /** @copydoc retro_vfs_stat_t */
+ retro_vfs_stat_t stat;
+
+ /** @copydoc retro_vfs_mkdir_t */
+ retro_vfs_mkdir_t mkdir;
+
+ /** @copydoc retro_vfs_opendir_t */
+ retro_vfs_opendir_t opendir;
+
+ /** @copydoc retro_vfs_readdir_t */
+ retro_vfs_readdir_t readdir;
+
+ /** @copydoc retro_vfs_dirent_get_name_t */
+ retro_vfs_dirent_get_name_t dirent_get_name;
+
+ /** @copydoc retro_vfs_dirent_is_dir_t */
+ retro_vfs_dirent_is_dir_t dirent_is_dir;
+
+ /** @copydoc retro_vfs_closedir_t */
+ retro_vfs_closedir_t closedir;
+};
+
+/**
+ * Represents a request by the core for the frontend's file system interface,
+ * as well as the interface itself returned by the frontend.
+ *
+ * You do not need to use these functions directly;
+ * you may pass this struct to \c dirent_vfs_init,
+ * \c filestream_vfs_init, or \c path_vfs_init
+ * so that you can use the wrappers provided by these modules.
+ *
+ * @see dirent_vfs_init
+ * @see filestream_vfs_init
+ * @see path_vfs_init
+ * @see RETRO_ENVIRONMENT_GET_VFS_INTERFACE
+ */
+struct retro_vfs_interface_info
+{
+ /**
+ * The minimum version of the VFS API that the core requires.
+ * libretro-common's wrapper API initializers will check this value as well.
+ *
+ * Set to the core's desired VFS version when requesting an interface,
+ * and set by the frontend to indicate its actual API version.
+ *
+ * If the core asks for a newer VFS API version than the frontend supports,
+ * the frontend must return \c false within the \c RETRO_ENVIRONMENT_GET_VFS_INTERFACE call.
+ * @since VFS API v1
+ */
+ uint32_t required_interface_version;
+
+ /**
+ * Set by the frontend.
+ * The frontend will set this to the VFS interface it provides.
+ *
+ * The interface is owned by the frontend
+ * and must not be modified or freed by the core.
+ * @since VFS API v1 */
+ struct retro_vfs_interface *iface;
+};
+
+/** @} */
+
+/** @defgroup GET_HW_RENDER_INTERFACE Hardware Rendering Interface
+ * @{
+ */
+
+/**
+ * Describes the hardware rendering API supported by
+ * a particular subtype of \c retro_hw_render_interface.
+ *
+ * Not every rendering API supported by libretro has its own interface,
+ * or even needs one.
+ *
+ * @see RETRO_ENVIRONMENT_SET_HW_RENDER
+ * @see RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE
+ */
+enum retro_hw_render_interface_type
+{
+ /**
+ * Indicates a \c retro_hw_render_interface for Vulkan.
+ * @see retro_hw_render_interface_vulkan
+ */
+ RETRO_HW_RENDER_INTERFACE_VULKAN = 0,
+
+ /** Indicates a \c retro_hw_render_interface for Direct3D 9. */
+ RETRO_HW_RENDER_INTERFACE_D3D9 = 1,
+
+ /** Indicates a \c retro_hw_render_interface for Direct3D 10. */
+ RETRO_HW_RENDER_INTERFACE_D3D10 = 2,
+
+ /**
+ * Indicates a \c retro_hw_render_interface for Direct3D 11.
+ * @see retro_hw_render_interface_d3d11
+ */
+ RETRO_HW_RENDER_INTERFACE_D3D11 = 3,
+
+ /**
+ * Indicates a \c retro_hw_render_interface for Direct3D 12.
+ * @see retro_hw_render_interface_d3d12
+ */
+ RETRO_HW_RENDER_INTERFACE_D3D12 = 4,
+
+ /**
+ * Indicates a \c retro_hw_render_interface for
+ * the PlayStation's 2 PSKit API.
+ * @see retro_hw_render_interface_gskit_ps2
+ */
+ RETRO_HW_RENDER_INTERFACE_GSKIT_PS2 = 5,
+
+ /** @private Defined to ensure sizeof(retro_hw_render_interface_type) == sizeof(int).
+ * Do not use. */
+ RETRO_HW_RENDER_INTERFACE_DUMMY = INT_MAX
+};
+
+/**
+ * Base render interface type.
+ * All \c retro_hw_render_interface implementations
+ * will start with these two fields set to particular values.
+ *
+ * @see retro_hw_render_interface_type
+ * @see RETRO_ENVIRONMENT_SET_HW_RENDER
+ * @see RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE
+ */
+struct retro_hw_render_interface
+{
+ /**
+ * Denotes the particular rendering API that this interface is for.
+ * Each interface requires this field to be set to a particular value.
+ * Use it to cast this interface to the appropriate pointer.
+ */
+ enum retro_hw_render_interface_type interface_type;
+
+ /**
+ * The version of this rendering interface.
+ * @note This is not related to the version of the API itself.
+ */
+ unsigned interface_version;
+};
+
+/** @} */
+
+/**
+ * @defgroup GET_LED_INTERFACE LED Interface
+ * @{
+ */
+
+/** @copydoc retro_led_interface::set_led_state */
+typedef void (RETRO_CALLCONV *retro_set_led_state_t)(int led, int state);
+
+/**
+ * Interface that the core can use to set the state of available LEDs.
+ * @see RETRO_ENVIRONMENT_GET_LED_INTERFACE
+ */
+struct retro_led_interface
+{
+ /**
+ * Sets the state of an LED.
+ *
+ * @param led The LED to set the state of.
+ * @param state The state to set the LED to.
+ * \c true to enable, \c false to disable.
+ */
+ retro_set_led_state_t set_led_state;
+};
+
+/** @} */
+
+/** @defgroup GET_AUDIO_VIDEO_ENABLE Skipped A/V Steps
+ * @{
+ */
+
+/**
+ * Flags that define A/V steps that the core may skip for this frame.
+ *
+ * @see RETRO_ENVIRONMENT_GET_AUDIO_VIDEO_ENABLE
+ */
+enum retro_av_enable_flags
+{
+ /**
+ * If set, the core should render video output with \c retro_video_refresh_t as normal.
+ *
+ * Otherwise, the frontend will discard any video data received this frame,
+ * including frames presented via hardware acceleration.
+ * \c retro_video_refresh_t will do nothing.
+ *
+ * @note After running the frame, the video output of the next frame
+ * should be no different than if video was enabled,
+ * and saving and loading state should have no issues.
+ * This implies that the emulated console's graphics pipeline state
+ * should not be affected by this flag.
+ *
+ * @note If emulating a platform that supports display capture
+ * (i.e. reading its own VRAM),
+ * the core may not be able to completely skip rendering,
+ * as the VRAM is part of the graphics pipeline's state.
+ */
+ RETRO_AV_ENABLE_VIDEO = (1 << 0),
+
+ /**
+ * If set, the core should render audio output
+ * with \c retro_audio_sample_t or \c retro_audio_sample_batch_t as normal.
+ *
+ * Otherwise, the frontend will discard any audio data received this frame.
+ * The core should skip audio rendering if possible.
+ *
+ * @note After running the frame, the audio output of the next frame
+ * should be no different than if audio was enabled,
+ * and saving and loading state should have no issues.
+ * This implies that the emulated console's audio pipeline state
+ * should not be affected by this flag.
+ */
+ RETRO_AV_ENABLE_AUDIO = (1 << 1),
+
+ /**
+ * If set, indicates that any savestates taken this frame
+ * are guaranteed to be created by the same binary that will load them,
+ * and will not be written to or read from the disk.
+ *
+ * The core may use these guarantees to:
+ *
+ * @li Assume that loading state will succeed.
+ * @li Update its memory buffers in-place if possible.
+ * @li Skip clearing memory.
+ * @li Skip resetting the system.
+ * @li Skip validation steps.
+ *
+ * @deprecated Use \c RETRO_ENVIRONMENT_GET_SAVESTATE_CONTEXT instead,
+ * except for compatibility purposes.
+ */
+ RETRO_AV_ENABLE_FAST_SAVESTATES = (1 << 2),
+
+ /**
+ * If set, indicates that the frontend will never need audio from the core.
+ * Used by a frontend for implementing runahead via a secondary core instance.
+ *
+ * The core may stop synthesizing audio if it can do so
+ * without compromising emulation accuracy.
+ *
+ * Audio output for the next frame does not matter,
+ * and the frontend will never need an accurate audio state in the future.
+ *
+ * State will never be saved while this flag is set.
+ */
+ RETRO_AV_ENABLE_HARD_DISABLE_AUDIO = (1 << 3),
+
+ /**
+ * @private Defined to ensure sizeof(retro_av_enable_flags) == sizeof(int).
+ * Do not use.
+ */
+ RETRO_AV_ENABLE_DUMMY = INT_MAX
+};
+
+/** @} */
+
+/**
+ * @defgroup GET_MIDI_INTERFACE MIDI Interface
+ * @{
+ */
+
+/** @copydoc retro_midi_interface::input_enabled */
+typedef bool (RETRO_CALLCONV *retro_midi_input_enabled_t)(void);
+
+/** @copydoc retro_midi_interface::output_enabled */
+typedef bool (RETRO_CALLCONV *retro_midi_output_enabled_t)(void);
+
+/** @copydoc retro_midi_interface::read */
+typedef bool (RETRO_CALLCONV *retro_midi_read_t)(uint8_t *byte);
+
+/** @copydoc retro_midi_interface::write */
+typedef bool (RETRO_CALLCONV *retro_midi_write_t)(uint8_t byte, uint32_t delta_time);
+
+/** @copydoc retro_midi_interface::flush */
+typedef bool (RETRO_CALLCONV *retro_midi_flush_t)(void);
+
+/**
+ * Interface that the core can use for raw MIDI I/O.
+ */
+struct retro_midi_interface
+{
+ /**
+ * Retrieves the current state of MIDI input.
+ *
+ * @return \c true if MIDI input is enabled.
+ */
+ retro_midi_input_enabled_t input_enabled;
+
+ /**
+ * Retrieves the current state of MIDI output.
+ * @return \c true if MIDI output is enabled.
+ */
+ retro_midi_output_enabled_t output_enabled;
+
+ /**
+ * Reads a byte from the MIDI input stream.
+ *
+ * @param[out] byte The byte received from the input stream.
+ * @return \c true if a byte was read,
+ * \c false if MIDI input is disabled or \c byte is \c NULL.
+ */
+ retro_midi_read_t read;
+
+ /**
+ * Writes a byte to the output stream.
+ *
+ * @param byte The byte to write to the output stream.
+ * @param delta_time Time since the previous write, in microseconds.
+ * @return \c true if c\ byte was written, false otherwise.
+ */
+ retro_midi_write_t write;
+
+ /**
+ * Flushes previously-written data.
+ *
+ * @return \c true if successful.
+ */
+ retro_midi_flush_t flush;
+};
+
+/** @} */
+
+/** @defgroup SET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE Render Context Negotiation
+ * @{
+ */
+
+/**
+ * Describes the hardware rendering API used by
+ * a particular subtype of \c retro_hw_render_context_negotiation_interface.
+ *
+ * Not every rendering API supported by libretro has a context negotiation interface,
+ * or even needs one.
+ *
+ * @see RETRO_ENVIRONMENT_SET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE
+ * @see RETRO_ENVIRONMENT_SET_HW_RENDER
+ * @see RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE
+ */
+enum retro_hw_render_context_negotiation_interface_type
+{
+ /**
+ * Denotes a context negotiation interface for Vulkan.
+ * @see retro_hw_render_context_negotiation_interface_vulkan
+ */
+ RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_VULKAN = 0,
+
+ /**
+ * @private Defined to ensure sizeof(retro_hw_render_context_negotiation_interface_type) == sizeof(int).
+ * Do not use.
+ */
+ RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_DUMMY = INT_MAX
+};
+
+/**
+ * Base context negotiation interface type.
+ * All \c retro_hw_render_context_negotiation_interface implementations
+ * will start with these two fields set to particular values.
+ *
+ * @see retro_hw_render_interface_type
+ * @see RETRO_ENVIRONMENT_SET_HW_RENDER
+ * @see RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE
+ * @see RETRO_ENVIRONMENT_SET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE
+ */
+struct retro_hw_render_context_negotiation_interface
+{
+ /**
+ * Denotes the particular rendering API that this interface is for.
+ * Each interface requires this field to be set to a particular value.
+ * Use it to cast this interface to the appropriate pointer.
+ */
+ enum retro_hw_render_context_negotiation_interface_type interface_type;
+
+ /**
+ * The version of this negotiation interface.
+ * @note This is not related to the version of the API itself.
+ */
+ unsigned interface_version;
+};
+
+/** @} */
+
+/** @defgroup RETRO_SERIALIZATION_QUIRK Serialization Quirks
+ * @{
+ */
+
+/**
+ * Indicates that serialized state is incomplete in some way.
+ *
+ * Set if serialization is usable for the common case of saving and loading game state,
+ * but should not be relied upon for frame-sensitive frontend features
+ * such as netplay or rerecording.
+ */
+#define RETRO_SERIALIZATION_QUIRK_INCOMPLETE (1 << 0)
+
+/**
+ * Indicates that core must spend some time initializing before serialization can be done.
+ *
+ * \c retro_serialize(), \c retro_unserialize(), and \c retro_serialize_size() will initially fail.
+ */
+#define RETRO_SERIALIZATION_QUIRK_MUST_INITIALIZE (1 << 1)
+
+/** Set by the core to indicate that serialization size may change within a session. */
+#define RETRO_SERIALIZATION_QUIRK_CORE_VARIABLE_SIZE (1 << 2)
+
+/** Set by the frontend to acknowledge that it supports variable-sized states. */
+#define RETRO_SERIALIZATION_QUIRK_FRONT_VARIABLE_SIZE (1 << 3)
+
+/** Serialized state can only be loaded during the same session. */
+#define RETRO_SERIALIZATION_QUIRK_SINGLE_SESSION (1 << 4)
+
+/**
+ * Serialized state cannot be loaded on an architecture
+ * with a different endianness from the one it was saved on.
+ */
+#define RETRO_SERIALIZATION_QUIRK_ENDIAN_DEPENDENT (1 << 5)
+
+/**
+ * Serialized state cannot be loaded on a different platform
+ * from the one it was saved on for reasons other than endianness,
+ * such as word size dependence.
+ */
+#define RETRO_SERIALIZATION_QUIRK_PLATFORM_DEPENDENT (1 << 6)
+
+/** @} */
+
+/** @defgroup SET_MEMORY_MAPS Memory Descriptors
+ * @{
+ */
+
+/** @defgroup RETRO_MEMDESC Memory Descriptor Flags
+ * Information about how the emulated hardware uses this portion of its address space.
+ * @{
+ */
+
+/**
+ * Indicates that this memory area won't be modified
+ * once \c retro_load_game has returned.
+ */
+#define RETRO_MEMDESC_CONST (1 << 0)
+
+/**
+ * Indicates a memory area with big-endian byte ordering,
+ * as opposed to the default of little-endian.
+ */
+#define RETRO_MEMDESC_BIGENDIAN (1 << 1)
+
+/**
+ * Indicates a memory area that is used for the emulated system's main RAM.
+ */
+#define RETRO_MEMDESC_SYSTEM_RAM (1 << 2)
+
+/**
+ * Indicates a memory area that is used for the emulated system's save RAM,
+ * usually found on a game cartridge as battery-backed RAM or flash memory.
+ */
+#define RETRO_MEMDESC_SAVE_RAM (1 << 3)
+
+/**
+ * Indicates a memory area that is used for the emulated system's video RAM,
+ * usually found on a console's GPU (or local equivalent).
+ */
+#define RETRO_MEMDESC_VIDEO_RAM (1 << 4)
+
+/**
+ * Indicates a memory area that requires all accesses
+ * to be aligned to 2 bytes or their own size
+ * (whichever is smaller).
+ */
+#define RETRO_MEMDESC_ALIGN_2 (1 << 16)
+
+/**
+ * Indicates a memory area that requires all accesses
+ * to be aligned to 4 bytes or their own size
+ * (whichever is smaller).
+ */
+#define RETRO_MEMDESC_ALIGN_4 (2 << 16)
+
+/**
+ * Indicates a memory area that requires all accesses
+ * to be aligned to 8 bytes or their own size
+ * (whichever is smaller).
+ */
+#define RETRO_MEMDESC_ALIGN_8 (3 << 16)
+
+/**
+ * Indicates a memory area that requires all accesses
+ * to be at least 2 bytes long.
+ */
+#define RETRO_MEMDESC_MINSIZE_2 (1 << 24)
+
+/**
+ * Indicates a memory area that requires all accesses
+ * to be at least 4 bytes long.
+ */
+#define RETRO_MEMDESC_MINSIZE_4 (2 << 24)
+
+/**
+ * Indicates a memory area that requires all accesses
+ * to be at least 8 bytes long.
+ */
+#define RETRO_MEMDESC_MINSIZE_8 (3 << 24)
+
+/** @} */
+
+/**
+ * A mapping from a region of the emulated console's address space
+ * to the host's address space.
+ *
+ * Can be used to map an address in the console's address space
+ * to the host's address space, like so:
+ *
+ * @code
+ * void* emu_to_host(void* addr, struct retro_memory_descriptor* descriptor)
+ * {
+ * return descriptor->ptr + (addr & ~descriptor->disconnect) - descriptor->start;
+ * }
+ * @endcode
+ *
+ * @see RETRO_ENVIRONMENT_SET_MEMORY_MAPS
+ */
+struct retro_memory_descriptor
+{
+ /**
+ * A bitwise \c OR of one or more \ref RETRO_MEMDESC "flags"
+ * that describe how the emulated system uses this descriptor's address range.
+ *
+ * @note If \c ptr is \c NULL,
+ * then no flags should be set.
+ * @see RETRO_MEMDESC
+ */
+ uint64_t flags;
+
+ /**
+ * Pointer to the start of this memory region's buffer
+ * within the \em host's address space.
+ * The address listed here must be valid for the duration of the session;
+ * it must not be freed or modified by the frontend
+ * and it must not be moved by the core.
+ *
+ * May be \c NULL to indicate a lack of accessible memory
+ * at the emulated address given in \c start.
+ *
+ * @note Overlapping descriptors that include the same byte
+ * must have the same \c ptr value.
+ */
+ void *ptr;
+
+ /**
+ * The offset of this memory region,
+ * relative to the address given by \c ptr.
+ *
+ * @note It is recommended to use this field for address calculations
+ * instead of performing arithmetic on \c ptr.
+ */
+ size_t offset;
+
+ /**
+ * The starting address of this memory region
+ * within the emulated hardware's address space.
+ *
+ * @note Not represented as a pointer
+ * because it's unlikely to be valid on the host device.
+ */
+ size_t start;
+
+ /**
+ * A bitmask that specifies which bits of an address must match
+ * the bits of the \ref start address.
+ *
+ * Combines with \c disconnect to map an address to a memory block.
+ *
+ * If multiple memory descriptors can claim a particular byte,
+ * the first one defined in the \ref retro_memory_descriptor array applies.
+ * A bit which is set in \c start must also be set in this.
+ *
+ * Can be zero, in which case \c start and \c len represent
+ * the complete mapping for this region of memory
+ * (i.e. each byte is mapped exactly once).
+ * In this case, \c len must be a power of two.
+ */
+ size_t select;
+
+ /**
+ * A bitmask of bits that are \em not used for addressing.
+ *
+ * Any set bits are assumed to be disconnected from
+ * the emulated memory chip's address pins,
+ * and are therefore ignored when memory-mapping.
+ */
+ size_t disconnect;
+
+ /**
+ * The length of this memory region, in bytes.
+ *
+ * If applying \ref start and \ref disconnect to an address
+ * results in a value higher than this,
+ * the highest bit of the address is cleared.
+ *
+ * If the address is still too high, the next highest bit is cleared.
+ * Can be zero, in which case it's assumed to be
+ * bounded only by \ref select and \ref disconnect.
+ */
+ size_t len;
+
+ /**
+ * A short name for this address space.
+ *
+ * Names must meet the following requirements:
+ *
+ * \li Characters must be in the set [a-zA-Z0-9_-].
+ * \li No more than 8 characters, plus a \c NULL terminator.
+ * \li Names are case-sensitive, but lowercase characters are discouraged.
+ * \li A name must not be the same as another name plus a character in the set \c [A-F0-9]
+ * (i.e. if an address space named "RAM" exists,
+ * then the names "RAM0", "RAM1", ..., "RAMF" are forbidden).
+ * This is to allow addresses to be named by each descriptor unambiguously,
+ * even if the areas overlap.
+ * \li May be \c NULL or empty (both are considered equivalent).
+ *
+ * Here are some examples of pairs of address space names:
+ *
+ * \li \em blank + \em blank: valid (multiple things may be mapped in the same namespace)
+ * \li \c Sp + \c Sp: valid (multiple things may be mapped in the same namespace)
+ * \li \c SRAM + \c VRAM: valid (neither is a prefix of the other)
+ * \li \c V + \em blank: valid (\c V is not in \c [A-F0-9])
+ * \li \c a + \em blank: valid but discouraged (\c a is not in \c [A-F0-9])
+ * \li \c a + \c A: valid but discouraged (neither is a prefix of the other)
+ * \li \c AR + \em blank: valid (\c R is not in \c [A-F0-9])
+ * \li \c ARB + \em blank: valid (there's no \c AR namespace,
+ * so the \c B doesn't cause ambiguity).
+ * \li \em blank + \c B: invalid, because it's ambiguous which address space \c B1234 would refer to.
+ *
+ * The length of the address space's name can't be used to disambugiate,
+ * as extra information may be appended to it without a separator.
+ */
+ const char *addrspace;
+
+ /* TODO: When finalizing this one, add a description field, which should be
+ * "WRAM" or something roughly equally long. */
+
+ /* TODO: When finalizing this one, replace 'select' with 'limit', which tells
+ * which bits can vary and still refer to the same address (limit = ~select).
+ * TODO: limit? range? vary? something else? */
+
+ /* TODO: When finalizing this one, if 'len' is above what 'select' (or
+ * 'limit') allows, it's bankswitched. Bankswitched data must have both 'len'
+ * and 'select' != 0, and the mappings don't tell how the system switches the
+ * banks. */
+
+ /* TODO: When finalizing this one, fix the 'len' bit removal order.
+ * For len=0x1800, pointer 0x1C00 should go to 0x1400, not 0x0C00.
+ * Algorithm: Take bits highest to lowest, but if it goes above len, clear
+ * the most recent addition and continue on the next bit.
+ * TODO: Can the above be optimized? Is "remove the lowest bit set in both
+ * pointer and 'len'" equivalent? */
+
+ /* TODO: Some emulators (MAME?) emulate big endian systems by only accessing
+ * the emulated memory in 32-bit chunks, native endian. But that's nothing
+ * compared to Darek Mihocka
+ * (section Emulation 103 - Nearly Free Byte Reversal) - he flips the ENTIRE
+ * RAM backwards! I'll want to represent both of those, via some flags.
+ *
+ * I suspect MAME either didn't think of that idea, or don't want the #ifdef.
+ * Not sure which, nor do I really care. */
+
+ /* TODO: Some of those flags are unused and/or don't really make sense. Clean
+ * them up. */
+};
+
+/**
+ * A list of regions within the emulated console's address space.
+ *
+ * The frontend may use the largest value of
+ * \ref retro_memory_descriptor::start + \ref retro_memory_descriptor::select
+ * in a certain namespace to infer the overall size of the address space.
+ * If the address space is larger than that,
+ * the last mapping in \ref descriptors should have \ref retro_memory_descriptor::ptr set to \c NULL
+ * and \ref retro_memory_descriptor::select should have all bits used in the address space set to 1.
+ *
+ * Here's an example set of descriptors for the SNES.
+ *
+ * @code{.c}
+ * struct retro_memory_map snes_descriptors = retro_memory_map
+ * {
+ * .descriptors = (struct retro_memory_descriptor[])
+ * {
+ * // WRAM; must usually be mapped before the ROM,
+ * // as some SNES ROM mappers try to claim 0x7E0000
+ * { .addrspace="WRAM", .start=0x7E0000, .len=0x20000 },
+ *
+ * // SPC700 RAM
+ * { .addrspace="SPC700", .len=0x10000 },
+ *
+ * // WRAM mirrors
+ * { .addrspace="WRAM", .start=0x000000, .select=0xC0E000, .len=0x2000 },
+ * { .addrspace="WRAM", .start=0x800000, .select=0xC0E000, .len=0x2000 },
+ *
+ * // WRAM mirror, alternate equivalent descriptor
+ * // (Various similar constructions can be created by combining parts of the above two.)
+ * { .addrspace="WRAM", .select=0x40E000, .disconnect=~0x1FFF },
+ *
+ * // LoROM (512KB, mirrored a couple of times)
+ * { .addrspace="LoROM", .start=0x008000, .select=0x408000, .disconnect=0x8000, .len=512*1024, .flags=RETRO_MEMDESC_CONST },
+ * { .addrspace="LoROM", .start=0x400000, .select=0x400000, .disconnect=0x8000, .len=512*1024, .flags=RETRO_MEMDESC_CONST },
+ *
+ * // HiROM (4MB)
+ * { .addrspace="HiROM", .start=0x400000, .select=0x400000, .len=4*1024*1024, .flags=RETRO_MEMDESC_CONST },
+ * { .addrspace="HiROM", .start=0x008000, .select=0x408000, .len=4*1024*1024, .offset=0x8000, .flags=RETRO_MEMDESC_CONST },
+ *
+ * // ExHiROM (8MB)
+ * { .addrspace="ExHiROM", .start=0xC00000, .select=0xC00000, .len=4*1024*1024, .offset=0, .flags=RETRO_MEMDESC_CONST },
+ * { .addrspace="ExHiROM", .start=0x400000, .select=0xC00000, .len=4*1024*1024, .offset=4*1024*1024, .flags=RETRO_MEMDESC_CONST },
+ * { .addrspace="ExHiROM", .start=0x808000, .select=0xC08000, .len=4*1024*1024, .offset=0x8000, .flags=RETRO_MEMDESC_CONST },
+ * { .addrspace="ExHiROM", .start=0x008000, .select=0xC08000, .len=4*1024*1024, .offset=4*1024*1024+0x8000, .flags=RETRO_MEMDESC_CONST },
+ *
+ * // Clarifying the full size of the address space
+ * { .select=0xFFFFFF, .ptr=NULL },
+ * },
+ * .num_descriptors = 14,
+ * };
+ * @endcode
+ *
+ * @see RETRO_ENVIRONMENT_SET_MEMORY_MAPS
+ */
+struct retro_memory_map
+{
+ /**
+ * Pointer to an array of memory descriptors,
+ * each of which describes part of the emulated console's address space.
+ */
+ const struct retro_memory_descriptor *descriptors;
+
+ /** The number of descriptors in \c descriptors. */
+ unsigned num_descriptors;
+};
+
+/** @} */
+
+/** @defgroup SET_CONTROLLER_INFO Controller Info
+ * @{
+ */
+
+/**
+ * Details about a controller (or controller configuration)
+ * supported by one of a core's emulated input ports.
+ *
+ * @see RETRO_ENVIRONMENT_SET_CONTROLLER_INFO
+ */
+struct retro_controller_description
+{
+ /**
+ * A human-readable label for the controller or configuration
+ * represented by this device type.
+ * Most likely the device's original brand name.
+ */
+ const char *desc;
+
+ /**
+ * A unique identifier that will be passed to \c retro_set_controller_port_device()'s \c device parameter.
+ * May be the ID of one of \ref RETRO_DEVICE "the generic controller types",
+ * or a subclass ID defined with \c RETRO_DEVICE_SUBCLASS.
+ *
+ * @see RETRO_DEVICE_SUBCLASS
+ */
+ unsigned id;
+};
+
+/**
+ * Lists the types of controllers supported by
+ * one of core's emulated input ports.
+ *
+ * @see RETRO_ENVIRONMENT_SET_CONTROLLER_INFO
+ */
+struct retro_controller_info
+{
+
+ /**
+ * A pointer to an array of device types supported by this controller port.
+ *
+ * @note Ports that support the same devices
+ * may share the same underlying array.
+ */
+ const struct retro_controller_description *types;
+
+ /** The number of elements in \c types. */
+ unsigned num_types;
+};
+
+/** @} */
+
+/** @defgroup SET_SUBSYSTEM_INFO Subsystems
+ * @{
+ */
+
+/**
+ * Information about a type of memory associated with a subsystem.
+ * Usually used for SRAM (save RAM).
+ *
+ * @see RETRO_ENVIRONMENT_SET_SUBSYSTEM_INFO
+ * @see retro_get_memory_data
+ * @see retro_get_memory_size
+ */
+struct retro_subsystem_memory_info
+{
+ /**
+ * The file extension the frontend should use
+ * to save this memory region to disk, e.g. "srm" or "sav".
+ */
+ const char *extension;
+
+ /**
+ * A constant that identifies this type of memory.
+ * Should be at least 0x100 (256) to avoid conflict
+ * with the standard libretro memory types,
+ * unless a subsystem uses the main platform's memory region.
+ * @see RETRO_MEMORY
+ */
+ unsigned type;
+};
+
+/**
+ * Information about a type of ROM that a subsystem may use.
+ * Subsystems may use one or more ROMs at once,
+ * possibly of different types.
+ *
+ * @see RETRO_ENVIRONMENT_SET_SUBSYSTEM_INFO
+ * @see retro_subsystem_info
+ */
+struct retro_subsystem_rom_info
+{
+ /**
+ * Human-readable description of what the content represents,
+ * e.g. "Game Boy ROM".
+ */
+ const char *desc;
+
+ /** @copydoc retro_system_info::valid_extensions */
+ const char *valid_extensions;
+
+ /** @copydoc retro_system_info::need_fullpath */
+ bool need_fullpath;
+
+ /** @copydoc retro_system_info::block_extract */
+ bool block_extract;
+
+ /**
+ * Indicates whether this particular subsystem ROM is required.
+ * If \c true and the user doesn't provide a ROM,
+ * the frontend should not load the core.
+ * If \c false and the user doesn't provide a ROM,
+ * the frontend should pass a zeroed-out \c retro_game_info
+ * to the corresponding entry in \c retro_load_game_special().
+ */
+ bool required;
+
+ /**
+ * Pointer to an array of memory descriptors that this subsystem ROM type uses.
+ * Useful for secondary cartridges that have their own save data.
+ * May be \c NULL, in which case this subsystem ROM's memory is not persisted by the frontend
+ * and \c num_memory should be zero.
+ */
+ const struct retro_subsystem_memory_info *memory;
+
+ /** The number of elements in the array pointed to by \c memory. */
+ unsigned num_memory;
+};
+
+/**
+ * Information about a secondary platform that a core supports.
+ * @see RETRO_ENVIRONMENT_SET_SUBSYSTEM_INFO
+ */
+struct retro_subsystem_info
+{
+ /**
+ * A human-readable description of the subsystem type,
+ * usually the brand name of the original platform
+ * (e.g. "Super Game Boy").
+ */
+ const char *desc;
+
+ /**
+ * A short machine-friendly identifier for the subsystem,
+ * usually an abbreviation of the platform name.
+ * For example, a Super Game Boy subsystem for a SNES core
+ * might use an identifier of "sgb".
+ * This identifier can be used for command-line interfaces,
+ * configuration, or other purposes.
+ * Must use lower-case alphabetical characters only (i.e. from a-z).
+ */
+ const char *ident;
+
+ /**
+ * The list of ROM types that this subsystem may use.
+ *
+ * The first entry is considered to be the "most significant" content,
+ * for the purposes of the frontend's categorization.
+ * E.g. with Super GameBoy, the first content should be the GameBoy ROM,
+ * as it is the most "significant" content to a user.
+ *
+ * If a frontend creates new files based on the content used (e.g. for savestates),
+ * it should derive the filenames from the name of the first ROM in this list.
+ *
+ * @note \c roms can have a single element,
+ * but this is usually a sign that the core should broaden its
+ * primary system info instead.
+ *
+ * @see \c retro_system_info
+ */
+ const struct retro_subsystem_rom_info *roms;
+
+ /** The length of the array given in \c roms. */
+ unsigned num_roms;
+
+ /** A unique identifier passed to retro_load_game_special(). */
+ unsigned id;
+};
+
+/** @} */
+
+/** @defgroup SET_PROC_ADDRESS_CALLBACK Core Function Pointers
+ * @{ */
+
+/**
+ * The function pointer type that \c retro_get_proc_address_t returns.
+ *
+ * Despite the signature shown here, the original function may include any parameters and return type
+ * that respects the calling convention and C ABI.
+ *
+ * The frontend is expected to cast the function pointer to the correct type.
+ */
+typedef void (RETRO_CALLCONV *retro_proc_address_t)(void);
+
+/**
+ * Get a symbol from a libretro core.
+ *
+ * Cores should only return symbols that serve as libretro extensions.
+ * Frontends should not use this to obtain symbols to standard libretro entry points;
+ * instead, they should link to the core statically or use \c dlsym (or local equivalent).
+ *
+ * The symbol name must be equal to the function name.
+ * e.g. if void retro_foo(void); exists, the symbol in the compiled library must be called \c retro_foo.
+ * The returned function pointer must be cast to the corresponding type.
+ *
+ * @param \c sym The name of the symbol to look up.
+ * @return Pointer to the exposed function with the name given in \c sym,
+ * or \c NULL if one couldn't be found.
+ * @note The frontend is expected to know the returned pointer's type in advance
+ * so that it can be cast correctly.
+ * @note The core doesn't need to expose every possible function through this interface.
+ * It's enough to only expose the ones that it expects the frontend to use.
+ * @note The functions exposed through this interface
+ * don't need to be publicly exposed in the compiled library
+ * (e.g. via \c __declspec(dllexport)).
+ * @see RETRO_ENVIRONMENT_SET_PROC_ADDRESS_CALLBACK
+ */
+typedef retro_proc_address_t (RETRO_CALLCONV *retro_get_proc_address_t)(const char *sym);
+
+/**
+ * An interface that the frontend can use to get function pointers from the core.
+ *
+ * @note The returned function pointer will be invalidated once the core is unloaded.
+ * How and when that happens is up to the frontend.
+ *
+ * @see retro_get_proc_address_t
+ * @see RETRO_ENVIRONMENT_SET_PROC_ADDRESS_CALLBACK
+ */
+struct retro_get_proc_address_interface
+{
+ /** Set by the core. */
+ retro_get_proc_address_t get_proc_address;
+};
+
+/** @} */
+
+/** @defgroup GET_LOG_INTERFACE Logging
+ * @{
+ */
+
+/**
+ * The severity of a given message.
+ * The frontend may log messages differently depending on the level.
+ * It may also ignore log messages of a certain level.
+ * @see retro_log_callback
+ */
+enum retro_log_level
+{
+ /** The logged message is most likely not interesting to the user. */
+ RETRO_LOG_DEBUG = 0,
+
+ /** Information about the core operating normally. */
+ RETRO_LOG_INFO,
+
+ /** Indicates a potential problem, possibly one that the core can recover from. */
+ RETRO_LOG_WARN,
+
+ /** Indicates a degraded experience, if not failure. */
+ RETRO_LOG_ERROR,
+
+ /** Defined to ensure that sizeof(enum retro_log_level) == sizeof(int). Do not use. */
+ RETRO_LOG_DUMMY = INT_MAX
+};
+
+/**
+ * Logs a message to the frontend.
+ *
+ * @param level The log level of the message.
+ * @param fmt The format string to log.
+ * Same format as \c printf.
+ * Behavior is undefined if this is \c NULL.
+ * @param ... Zero or more arguments used by the format string.
+ * Behavior is undefined if these don't match the ones expected by \c fmt.
+ * @see retro_log_level
+ * @see retro_log_callback
+ * @see RETRO_ENVIRONMENT_GET_LOG_INTERFACE
+ * @see printf
+ */
+typedef void (RETRO_CALLCONV *retro_log_printf_t)(enum retro_log_level level,
+ const char *fmt, ...);
+
+/**
+ * Details about how to make log messages.
+ *
+ * @see retro_log_printf_t
+ * @see RETRO_ENVIRONMENT_GET_LOG_INTERFACE
+ */
+struct retro_log_callback
+{
+ /**
+ * Called when logging a message.
+ *
+ * @note Set by the frontend.
+ */
+ retro_log_printf_t log;
+};
+
+/** @} */
+
+/** @defgroup GET_PERF_INTERFACE Performance Interface
+ * @{
+ */
+
+/** @defgroup RETRO_SIMD CPU Features
+ * @{
+ */
+
+/**
+ * Indicates CPU support for the SSE instruction set.
+ *
+ * @see https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html#ssetechs=SSE
+ */
+#define RETRO_SIMD_SSE (1 << 0)
+
+/**
+ * Indicates CPU support for the SSE2 instruction set.
+ *
+ * @see https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html#ssetechs=SSE2
+ */
+#define RETRO_SIMD_SSE2 (1 << 1)
+
+/** Indicates CPU support for the AltiVec (aka VMX or Velocity Engine) instruction set. */
+#define RETRO_SIMD_VMX (1 << 2)
+
+/** Indicates CPU support for the VMX128 instruction set. Xbox 360 only. */
+#define RETRO_SIMD_VMX128 (1 << 3)
+
+/**
+ * Indicates CPU support for the AVX instruction set.
+ *
+ * @see https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html#avxnewtechs=AVX
+ */
+#define RETRO_SIMD_AVX (1 << 4)
+
+/**
+ * Indicates CPU support for the NEON instruction set.
+ * @see https://developer.arm.com/architectures/instruction-sets/intrinsics/#f:@navigationhierarchiessimdisa=[Neon]
+ */
+#define RETRO_SIMD_NEON (1 << 5)
+
+/**
+ * Indicates CPU support for the SSE3 instruction set.
+ *
+ * @see https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html#ssetechs=SSE3
+ */
+#define RETRO_SIMD_SSE3 (1 << 6)
+
+/**
+ * Indicates CPU support for the SSSE3 instruction set.
+ *
+ * @see https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html#ssetechs=SSSE3
+ */
+#define RETRO_SIMD_SSSE3 (1 << 7)
+
+/**
+ * Indicates CPU support for the MMX instruction set.
+ * @see https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html#techs=MMX
+ */
+#define RETRO_SIMD_MMX (1 << 8)
+
+/** Indicates CPU support for the MMXEXT instruction set. */
+#define RETRO_SIMD_MMXEXT (1 << 9)
+
+/**
+ * Indicates CPU support for the SSE4 instruction set.
+ *
+ * @see https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html#ssetechs=SSE4_1
+ */
+#define RETRO_SIMD_SSE4 (1 << 10)
+
+/**
+ * Indicates CPU support for the SSE4.2 instruction set.
+ *
+ * @see https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html#ssetechs=SSE4_2
+ */
+#define RETRO_SIMD_SSE42 (1 << 11)
+
+/**
+ * Indicates CPU support for the AVX2 instruction set.
+ *
+ * @see https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html#avxnewtechs=AVX2
+ */
+#define RETRO_SIMD_AVX2 (1 << 12)
+
+/** Indicates CPU support for the VFPU instruction set. PS2 and PSP only.
+ *
+ * @see https://pspdev.github.io/vfpu-docs
+ */
+#define RETRO_SIMD_VFPU (1 << 13)
+
+/**
+ * Indicates CPU support for Gekko SIMD extensions. GameCube only.
+ */
+#define RETRO_SIMD_PS (1 << 14)
+
+/**
+ * Indicates CPU support for AES instructions.
+ *
+ * @see https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html#aestechs=AES&othertechs=AES
+ */
+#define RETRO_SIMD_AES (1 << 15)
+
+/**
+ * Indicates CPU support for the VFPv3 instruction set.
+ */
+#define RETRO_SIMD_VFPV3 (1 << 16)
+
+/**
+ * Indicates CPU support for the VFPv4 instruction set.
+ */
+#define RETRO_SIMD_VFPV4 (1 << 17)
+
+/** Indicates CPU support for the POPCNT instruction. */
+#define RETRO_SIMD_POPCNT (1 << 18)
+
+/** Indicates CPU support for the MOVBE instruction. */
+#define RETRO_SIMD_MOVBE (1 << 19)
+
+/** Indicates CPU support for the CMOV instruction. */
+#define RETRO_SIMD_CMOV (1 << 20)
+
+/** Indicates CPU support for the ASIMD instruction set. */
+#define RETRO_SIMD_ASIMD (1 << 21)
+
+/** @} */
+
+/**
+ * An abstract unit of ticks.
+ *
+ * Usually nanoseconds or CPU cycles,
+ * but it depends on the platform and the frontend.
+ */
+typedef uint64_t retro_perf_tick_t;
+
+/** Time in microseconds. */
+typedef int64_t retro_time_t;
+
+/**
+ * A performance counter.
+ *
+ * Use this to measure the execution time of a region of code.
+ * @see retro_perf_callback
+ */
+struct retro_perf_counter
+{
+ /**
+ * A human-readable identifier for the counter.
+ *
+ * May be displayed by the frontend.
+ * Behavior is undefined if this is \c NULL.
+ */
+ const char *ident;
+
+ /**
+ * The time of the most recent call to \c retro_perf_callback::perf_start
+ * on this performance counter.
+ *
+ * @see retro_perf_start_t
+ */
+ retro_perf_tick_t start;
+
+ /**
+ * The total time spent within this performance counter's measured code,
+ * i.e. between calls to \c retro_perf_callback::perf_start and \c retro_perf_callback::perf_stop.
+ *
+ * Updated after each call to \c retro_perf_callback::perf_stop.
+ * @see retro_perf_stop_t
+ */
+ retro_perf_tick_t total;
+
+ /**
+ * The number of times this performance counter has been started.
+ *
+ * Updated after each call to \c retro_perf_callback::perf_start.
+ * @see retro_perf_start_t
+ */
+ retro_perf_tick_t call_cnt;
+
+ /**
+ * \c true if this performance counter has been registered by the frontend.
+ * Must be initialized to \c false by the core before registering it.
+ * @see retro_perf_register_t
+ */
+ bool registered;
+};
+
+/**
+ * @returns The current system time in microseconds.
+ * @note Accuracy may vary by platform.
+ * The frontend should use the most accurate timer possible.
+ * @see RETRO_ENVIRONMENT_GET_PERF_INTERFACE
+ */
+typedef retro_time_t (RETRO_CALLCONV *retro_perf_get_time_usec_t)(void);
+
+/**
+ * @returns The number of ticks since some unspecified epoch.
+ * The exact meaning of a "tick" depends on the platform,
+ * but it usually refers to nanoseconds or CPU cycles.
+ * @see RETRO_ENVIRONMENT_GET_PERF_INTERFACE
+ */
+typedef retro_perf_tick_t (RETRO_CALLCONV *retro_perf_get_counter_t)(void);
+
+/**
+ * Returns a bitmask of detected CPU features.
+ *
+ * Use this for runtime dispatching of CPU-specific code.
+ *
+ * @returns A bitmask of detected CPU features.
+ * @see RETRO_ENVIRONMENT_GET_PERF_INTERFACE
+ * @see RETRO_SIMD
+ */
+typedef uint64_t (RETRO_CALLCONV *retro_get_cpu_features_t)(void);
+
+/**
+ * Asks the frontend to log or display the state of performance counters.
+ * How this is done depends on the frontend.
+ * Performance counters can be reviewed manually as well.
+ *
+ * @see RETRO_ENVIRONMENT_GET_PERF_INTERFACE
+ * @see retro_perf_counter
+ */
+typedef void (RETRO_CALLCONV *retro_perf_log_t)(void);
+
+/**
+ * Registers a new performance counter.
+ *
+ * If \c counter has already been registered beforehand,
+ * this function does nothing.
+ *
+ * @param counter The counter to register.
+ * \c counter::ident must be set to a unique identifier,
+ * and all other values in \c counter must be set to zero or \c false.
+ * Behavior is undefined if \c NULL.
+ * @post If \c counter is successfully registered,
+ * then \c counter::registered will be set to \c true.
+ * Otherwise, it will be set to \c false.
+ * Registration may fail if the frontend's maximum number of counters (if any) has been reached.
+ * @note The counter is owned by the core and must not be freed by the frontend.
+ * The frontend must also clean up any references to a core's performance counters
+ * before unloading it, otherwise undefined behavior may occur.
+ * @see retro_perf_start_t
+ * @see retro_perf_stop_t
+ */
+typedef void (RETRO_CALLCONV *retro_perf_register_t)(struct retro_perf_counter *counter);
+
+/**
+ * Starts a registered performance counter.
+ *
+ * Call this just before the code you want to measure.
+ *
+ * @param counter The counter to start.
+ * Behavior is undefined if \c NULL.
+ * @see retro_perf_stop_t
+ */
+typedef void (RETRO_CALLCONV *retro_perf_start_t)(struct retro_perf_counter *counter);
+
+/**
+ * Stops a registered performance counter.
+ *
+ * Call this just after the code you want to measure.
+ *
+ * @param counter The counter to stop.
+ * Behavior is undefined if \c NULL.
+ * @see retro_perf_start_t
+ * @see retro_perf_stop_t
+ */
+typedef void (RETRO_CALLCONV *retro_perf_stop_t)(struct retro_perf_counter *counter);
+
+/**
+ * An interface that the core can use to get performance information.
+ *
+ * Here's a usage example:
+ *
+ * @code{.c}
+ * #ifdef PROFILING
+ * // Wrapper macros to simplify using performance counters.
+ * // Optional; tailor these to your project's needs.
+ * #define RETRO_PERFORMANCE_INIT(perf_cb, name) static struct retro_perf_counter name = {#name}; if (!name.registered) perf_cb.perf_register(&(name))
+ * #define RETRO_PERFORMANCE_START(perf_cb, name) perf_cb.perf_start(&(name))
+ * #define RETRO_PERFORMANCE_STOP(perf_cb, name) perf_cb.perf_stop(&(name))
+ * #else
+ * // Exclude the performance counters if profiling is disabled.
+ * #define RETRO_PERFORMANCE_INIT(perf_cb, name) ((void)0)
+ * #define RETRO_PERFORMANCE_START(perf_cb, name) ((void)0)
+ * #define RETRO_PERFORMANCE_STOP(perf_cb, name) ((void)0)
+ * #endif
+ *
+ * // Defined somewhere else in the core.
+ * extern struct retro_perf_callback perf_cb;
+ *
+ * void retro_run(void)
+ * {
+ * RETRO_PERFORMANCE_INIT(cb, interesting);
+ * RETRO_PERFORMANCE_START(cb, interesting);
+ * interesting_work();
+ * RETRO_PERFORMANCE_STOP(cb, interesting);
+ *
+ * RETRO_PERFORMANCE_INIT(cb, maybe_slow);
+ * RETRO_PERFORMANCE_START(cb, maybe_slow);
+ * more_interesting_work();
+ * RETRO_PERFORMANCE_STOP(cb, maybe_slow);
+ * }
+ *
+ * void retro_deinit(void)
+ * {
+ * // Asks the frontend to log the results of all performance counters.
+ * perf_cb.perf_log();
+ * }
+ * @endcode
+ *
+ * All functions are set by the frontend.
+ *
+ * @see RETRO_ENVIRONMENT_GET_PERF_INTERFACE
+ */
+struct retro_perf_callback
+{
+ /** @copydoc retro_perf_get_time_usec_t */
+ retro_perf_get_time_usec_t get_time_usec;
+
+ /** @copydoc retro_perf_get_counter_t */
+ retro_get_cpu_features_t get_cpu_features;
+
+ /** @copydoc retro_perf_get_counter_t */
+ retro_perf_get_counter_t get_perf_counter;
+
+ /** @copydoc retro_perf_register_t */
+ retro_perf_register_t perf_register;
+
+ /** @copydoc retro_perf_start_t */
+ retro_perf_start_t perf_start;
+
+ /** @copydoc retro_perf_stop_t */
+ retro_perf_stop_t perf_stop;
+
+ /** @copydoc retro_perf_log_t */
+ retro_perf_log_t perf_log;
+};
+
+/** @} */
+
+/**
+ * @defgroup RETRO_SENSOR Sensor Interface
+ * @{
+ */
+
+/**
+ * Defines actions that can be performed on sensors.
+ * @note Cores should only enable sensors while they're actively being used;
+ * depending on the frontend and platform,
+ * enabling these sensors may impact battery life.
+ *
+ * @see RETRO_ENVIRONMENT_GET_SENSOR_INTERFACE
+ * @see retro_sensor_interface
+ * @see retro_set_sensor_state_t
+ */
+enum retro_sensor_action
+{
+ /** Enables accelerometer input, if one exists. */
+ RETRO_SENSOR_ACCELEROMETER_ENABLE = 0,
+
+ /** Disables accelerometer input, if one exists. */
+ RETRO_SENSOR_ACCELEROMETER_DISABLE,
+
+ /** Enables gyroscope input, if one exists. */
+ RETRO_SENSOR_GYROSCOPE_ENABLE,
+
+ /** Disables gyroscope input, if one exists. */
+ RETRO_SENSOR_GYROSCOPE_DISABLE,
+
+ /** Enables ambient light input, if a luminance sensor exists. */
+ RETRO_SENSOR_ILLUMINANCE_ENABLE,
+
+ /** Disables ambient light input, if a luminance sensor exists. */
+ RETRO_SENSOR_ILLUMINANCE_DISABLE,
+
+ /** @private Defined to ensure sizeof(enum retro_sensor_action) == sizeof(int). Do not use. */
+ RETRO_SENSOR_DUMMY = INT_MAX
+};
+
+/** @defgroup RETRO_SENSOR_ID Sensor Value IDs
+ * @{
+ */
+/* Id values for SENSOR types. */
+
+/**
+ * Returns the device's acceleration along its local X axis minus the effect of gravity, in m/s^2.
+ *
+ * Positive values mean that the device is accelerating to the right.
+ * assuming the user is looking at it head-on.
+ */
+#define RETRO_SENSOR_ACCELEROMETER_X 0
+
+/**
+ * Returns the device's acceleration along its local Y axis minus the effect of gravity, in m/s^2.
+ *
+ * Positive values mean that the device is accelerating upwards,
+ * assuming the user is looking at it head-on.
+ */
+#define RETRO_SENSOR_ACCELEROMETER_Y 1
+
+/**
+ * Returns the the device's acceleration along its local Z axis minus the effect of gravity, in m/s^2.
+ *
+ * Positive values indicate forward acceleration towards the user,
+ * assuming the user is looking at the device head-on.
+ */
+#define RETRO_SENSOR_ACCELEROMETER_Z 2
+
+/**
+ * Returns the angular velocity of the device around its local X axis, in radians per second.
+ *
+ * Positive values indicate counter-clockwise rotation.
+ *
+ * @note A radian is about 57 degrees, and a full 360-degree rotation is 2*pi radians.
+ * @see https://developer.android.com/reference/android/hardware/SensorEvent#sensor.type_gyroscope
+ * for guidance on using this value to derive a device's orientation.
+ */
+#define RETRO_SENSOR_GYROSCOPE_X 3
+
+/**
+ * Returns the angular velocity of the device around its local Z axis, in radians per second.
+ *
+ * Positive values indicate counter-clockwise rotation.
+ *
+ * @note A radian is about 57 degrees, and a full 360-degree rotation is 2*pi radians.
+ * @see https://developer.android.com/reference/android/hardware/SensorEvent#sensor.type_gyroscope
+ * for guidance on using this value to derive a device's orientation.
+ */
+#define RETRO_SENSOR_GYROSCOPE_Y 4
+
+/**
+ * Returns the angular velocity of the device around its local Z axis, in radians per second.
+ *
+ * Positive values indicate counter-clockwise rotation.
+ *
+ * @note A radian is about 57 degrees, and a full 360-degree rotation is 2*pi radians.
+ * @see https://developer.android.com/reference/android/hardware/SensorEvent#sensor.type_gyroscope
+ * for guidance on using this value to derive a device's orientation.
+ */
+#define RETRO_SENSOR_GYROSCOPE_Z 5
+
+/**
+ * Returns the ambient illuminance (light intensity) of the device's environment, in lux.
+ *
+ * @see https://en.wikipedia.org/wiki/Lux for a table of common lux values.
+ */
+#define RETRO_SENSOR_ILLUMINANCE 6
+/** @} */
+
+/**
+ * Adjusts the state of a sensor.
+ *
+ * @param port The device port of the controller that owns the sensor given in \c action.
+ * @param action The action to perform on the sensor.
+ * Different devices support different sensors.
+ * @param rate The rate at which the underlying sensor should be updated, in Hz.
+ * This should be treated as a hint,
+ * as some device sensors may not support the requested rate
+ * (if it's configurable at all).
+ * @returns \c true if the sensor state was successfully adjusted, \c false otherwise.
+ * @note If one of the \c RETRO_SENSOR_*_ENABLE actions fails,
+ * this likely means that the given sensor is not available
+ * on the provided \c port.
+ * @see retro_sensor_action
+ */
+typedef bool (RETRO_CALLCONV *retro_set_sensor_state_t)(unsigned port,
+ enum retro_sensor_action action, unsigned rate);
+
+/**
+ * Retrieves the current value reported by sensor.
+ * @param port The device port of the controller that owns the sensor given in \c id.
+ * @param id The sensor value to query.
+ * @returns The current sensor value.
+ * Exact semantics depend on the value given in \c id,
+ * but will return 0 for invalid arguments.
+ *
+ * @see RETRO_SENSOR_ID
+ */
+typedef float (RETRO_CALLCONV *retro_sensor_get_input_t)(unsigned port, unsigned id);
+
+/**
+ * An interface that cores can use to access device sensors.
+ *
+ * All function pointers are set by the frontend.
+ */
+struct retro_sensor_interface
+{
+ /** @copydoc retro_set_sensor_state_t */
+ retro_set_sensor_state_t set_sensor_state;
+
+ /** @copydoc retro_sensor_get_input_t */
+ retro_sensor_get_input_t get_sensor_input;
+};
+
+/** @} */
+
+/** @defgroup GET_CAMERA_INTERFACE Camera Interface
+ * @{
+ */
+
+/**
+ * Denotes the type of buffer in which the camera will store its input.
+ *
+ * Different camera drivers may support different buffer types.
+ *
+ * @see RETRO_ENVIRONMENT_GET_CAMERA_INTERFACE
+ * @see retro_camera_callback
+ */
+enum retro_camera_buffer
+{
+ /**
+ * Indicates that camera frames should be delivered to the core as an OpenGL texture.
+ *
+ * Requires that the core is using an OpenGL context via \c RETRO_ENVIRONMENT_SET_HW_RENDER.
+ *
+ * @see retro_camera_frame_opengl_texture_t
+ */
+ RETRO_CAMERA_BUFFER_OPENGL_TEXTURE = 0,
+
+ /**
+ * Indicates that camera frames should be delivered to the core as a raw buffer in memory.
+ *
+ * @see retro_camera_frame_raw_framebuffer_t
+ */
+ RETRO_CAMERA_BUFFER_RAW_FRAMEBUFFER,
+
+ /**
+ * @private Defined to ensure sizeof(enum retro_camera_buffer) == sizeof(int).
+ * Do not use.
+ */
+ RETRO_CAMERA_BUFFER_DUMMY = INT_MAX
+};
+
+/**
+ * Starts an initialized camera.
+ * The camera is disabled by default,
+ * and must be enabled with this function before being used.
+ *
+ * Set by the frontend.
+ *
+ * @returns \c true if the camera was successfully started, \c false otherwise.
+ * Failure may occur if no actual camera is available,
+ * or if the frontend doesn't have permission to access it.
+ * @note Must be called in \c retro_run().
+ * @see retro_camera_callback
+ */
+typedef bool (RETRO_CALLCONV *retro_camera_start_t)(void);
+
+/**
+ * Stops the running camera.
+ *
+ * Set by the frontend.
+ *
+ * @note Must be called in \c retro_run().
+ * @warning The frontend may close the camera on its own when unloading the core,
+ * but this behavior is not guaranteed.
+ * Cores should clean up the camera before exiting.
+ * @see retro_camera_callback
+ */
+typedef void (RETRO_CALLCONV *retro_camera_stop_t)(void);
+
+/**
+ * Called by the frontend to report the state of the camera driver.
+ *
+ * @see retro_camera_callback
+ */
+typedef void (RETRO_CALLCONV *retro_camera_lifetime_status_t)(void);
+
+/**
+ * Called by the frontend to report a new camera frame,
+ * delivered as a raw buffer in memory.
+ *
+ * Set by the core.
+ *
+ * @param buffer Pointer to the camera's most recent video frame.
+ * Each pixel is in XRGB8888 format.
+ * The first pixel represents the top-left corner of the image
+ * (i.e. the Y axis goes downward).
+ * @param width The width of the frame given in \c buffer, in pixels.
+ * @param height The height of the frame given in \c buffer, in pixels.
+ * @param pitch The width of the frame given in \c buffer, in bytes.
+ * @warning \c buffer may be invalidated when this function returns,
+ * so the core should make its own copy of \c buffer if necessary.
+ * @see RETRO_CAMERA_BUFFER_RAW_FRAMEBUFFER
+ */
+typedef void (RETRO_CALLCONV *retro_camera_frame_raw_framebuffer_t)(const uint32_t *buffer,
+ unsigned width, unsigned height, size_t pitch);
+
+/**
+ * Called by the frontend to report a new camera frame,
+ * delivered as an OpenGL texture.
+ *
+ * @param texture_id The ID of the OpenGL texture that represents the camera's most recent frame.
+ * Owned by the frontend, and must not be modified by the core.
+ * @param texture_target The type of the texture given in \c texture_id.
+ * Usually either \c GL_TEXTURE_2D or \c GL_TEXTURE_RECTANGLE,
+ * but other types are allowed.
+ * @param affine A pointer to a 3x3 column-major affine matrix
+ * that can be used to transform pixel coordinates to texture coordinates.
+ * After transformation, the bottom-left corner should have coordinates of (0, 0)
+ * and the top-right corner should have coordinates of (1, 1)
+ * (or (width, height) for \c GL_TEXTURE_RECTANGLE).
+ *
+ * @note GL-specific typedefs (e.g. \c GLfloat and \c GLuint) are avoided here
+ * so that the API doesn't rely on gl.h.
+ * @warning \c texture_id and \c affine may be invalidated when this function returns,
+ * so the core should make its own copy of them if necessary.
+ */
+typedef void (RETRO_CALLCONV *retro_camera_frame_opengl_texture_t)(unsigned texture_id,
+ unsigned texture_target, const float *affine);
+
+/**
+ * An interface that the core can use to access a device's camera.
+ *
+ * @see RETRO_ENVIRONMENT_GET_CAMERA_INTERFACE
+ */
+struct retro_camera_callback
+{
+ /**
+ * Requested camera capabilities,
+ * given as a bitmask of \c retro_camera_buffer values.
+ * Set by the core.
+ *
+ * Here's a usage example:
+ * @code
+ * // Requesting support for camera data delivered as both an OpenGL texture and a pixel buffer:
+ * struct retro_camera_callback callback;
+ * callback.caps = (1 << RETRO_CAMERA_BUFFER_OPENGL_TEXTURE) | (1 << RETRO_CAMERA_BUFFER_RAW_FRAMEBUFFER);
+ * @endcode
+ */
+ uint64_t caps;
+
+ /**
+ * The desired width of the camera frame, in pixels.
+ * This is only a hint; the frontend may provide a different size.
+ * Set by the core.
+ * Use zero to let the frontend decide.
+ */
+ unsigned width;
+
+ /**
+ * The desired height of the camera frame, in pixels.
+ * This is only a hint; the frontend may provide a different size.
+ * Set by the core.
+ * Use zero to let the frontend decide.
+ */
+ unsigned height;
+
+ /**
+ * @copydoc retro_camera_start_t
+ * @see retro_camera_callback
+ */
+ retro_camera_start_t start;
+
+ /**
+ * @copydoc retro_camera_stop_t
+ * @see retro_camera_callback
+ */
+ retro_camera_stop_t stop;
+
+ /**
+ * @copydoc retro_camera_frame_raw_framebuffer_t
+ * @note If \c NULL, this function will not be called.
+ */
+ retro_camera_frame_raw_framebuffer_t frame_raw_framebuffer;
+
+ /**
+ * @copydoc retro_camera_frame_opengl_texture_t
+ * @note If \c NULL, this function will not be called.
+ */
+ retro_camera_frame_opengl_texture_t frame_opengl_texture;
+
+ /**
+ * Core-defined callback invoked by the frontend right after the camera driver is initialized
+ * (\em not when calling \c start).
+ * May be \c NULL, in which case this function is skipped.
+ */
+ retro_camera_lifetime_status_t initialized;
+
+ /**
+ * Core-defined callback invoked by the frontend
+ * right before the video camera driver is deinitialized
+ * (\em not when calling \c stop).
+ * May be \c NULL, in which case this function is skipped.
+ */
+ retro_camera_lifetime_status_t deinitialized;
+};
+
+/** @} */
+
+/** @defgroup GET_LOCATION_INTERFACE Location Interface
+ * @{
+ */
+
+/** @copydoc retro_location_callback::set_interval */
+typedef void (RETRO_CALLCONV *retro_location_set_interval_t)(unsigned interval_ms,
+ unsigned interval_distance);
+
+/** @copydoc retro_location_callback::start */
+typedef bool (RETRO_CALLCONV *retro_location_start_t)(void);
+
+/** @copydoc retro_location_callback::stop */
+typedef void (RETRO_CALLCONV *retro_location_stop_t)(void);
+
+/** @copydoc retro_location_callback::get_position */
+typedef bool (RETRO_CALLCONV *retro_location_get_position_t)(double *lat, double *lon,
+ double *horiz_accuracy, double *vert_accuracy);
+
+/** Function type that reports the status of the location service. */
+typedef void (RETRO_CALLCONV *retro_location_lifetime_status_t)(void);
+
+/**
+ * An interface that the core can use to access a device's location.
+ *
+ * @note It is the frontend's responsibility to request the necessary permissions
+ * from the operating system.
+ * @see RETRO_ENVIRONMENT_GET_LOCATION_INTERFACE
+ */
+struct retro_location_callback
+{
+ /**
+ * Starts listening the device's location service.
+ *
+ * The frontend will report changes to the device's location
+ * at the interval defined by \c set_interval.
+ * Set by the frontend.
+ *
+ * @return true if location services were successfully started, false otherwise.
+ * Note that this will return \c false if location services are disabled
+ * or the frontend doesn't have permission to use them.
+ * @note The device's location service may or may not have been enabled
+ * before the core calls this function.
+ */
+ retro_location_start_t start;
+
+ /**
+ * Stop listening to the device's location service.
+ *
+ * Set by the frontend.
+ *
+ * @note The location service itself may or may not
+ * be turned off by this function,
+ * depending on the platform and the frontend.
+ * @post The core will stop receiving location service updates.
+ */
+ retro_location_stop_t stop;
+
+ /**
+ * Returns the device's current coordinates.
+ *
+ * Set by the frontend.
+ *
+ * @param[out] lat Pointer to latitude, in degrees.
+ * Will be set to 0 if no change has occurred since the last call.
+ * Behavior is undefined if \c NULL.
+ * @param[out] lon Pointer to longitude, in degrees.
+ * Will be set to 0 if no change has occurred since the last call.
+ * Behavior is undefined if \c NULL.
+ * @param[out] horiz_accuracy Pointer to horizontal accuracy.
+ * Will be set to 0 if no change has occurred since the last call.
+ * Behavior is undefined if \c NULL.
+ * @param[out] vert_accuracy Pointer to vertical accuracy.
+ * Will be set to 0 if no change has occurred since the last call.
+ * Behavior is undefined if \c NULL.
+ */
+ retro_location_get_position_t get_position;
+
+ /**
+ * Sets the rate at which the location service should report updates.
+ *
+ * This is only a hint; the actual rate may differ.
+ * Sets the interval of time and/or distance at which to update/poll
+ * location-based data.
+ *
+ * Some platforms may only support one of the two parameters;
+ * cores should provide both to ensure compatibility.
+ *
+ * Set by the frontend.
+ *
+ * @param interval_ms The desired period of time between location updates, in milliseconds.
+ * @param interval_distance The desired distance between location updates, in meters.
+ */
+ retro_location_set_interval_t set_interval;
+
+ /** Called when the location service is initialized. Set by the core. Optional. */
+ retro_location_lifetime_status_t initialized;
+
+ /** Called when the location service is deinitialized. Set by the core. Optional. */
+ retro_location_lifetime_status_t deinitialized;
+};
+
+/** @} */
+
+/** @addtogroup GET_RUMBLE_INTERFACE
+ * @{ */
+
+/**
+ * The type of rumble motor in a controller.
+ *
+ * Both motors can be controlled independently,
+ * and the strong motor does not override the weak motor.
+ * @see RETRO_ENVIRONMENT_GET_RUMBLE_INTERFACE
+ */
+enum retro_rumble_effect
+{
+ RETRO_RUMBLE_STRONG = 0,
+ RETRO_RUMBLE_WEAK = 1,
+
+ /** @private Defined to ensure sizeof(enum retro_rumble_effect) == sizeof(int). Do not use. */
+ RETRO_RUMBLE_DUMMY = INT_MAX
+};
+
+/**
+ * Requests a rumble state change for a controller.
+ * Set by the frontend.
+ *
+ * @param port The controller port to set the rumble state for.
+ * @param effect The rumble motor to set the strength of.
+ * @param strength The desired intensity of the rumble motor, ranging from \c 0 to \c 0xffff (inclusive).
+ * @return \c true if the requested rumble state was honored.
+ * If the controller doesn't support rumble, will return \c false.
+ * @note Calling this before the first \c retro_run() may return \c false.
+ * @see RETRO_ENVIRONMENT_GET_RUMBLE_INTERFACE
+ */
+typedef bool (RETRO_CALLCONV *retro_set_rumble_state_t)(unsigned port,
+ enum retro_rumble_effect effect, uint16_t strength);
+
+/**
+ * An interface that the core can use to set the rumble state of a controller.
+ * @see RETRO_ENVIRONMENT_GET_RUMBLE_INTERFACE
+ */
+struct retro_rumble_interface
+{
+ /** @copydoc retro_set_rumble_state_t */
+ retro_set_rumble_state_t set_rumble_state;
+};
+
+/** @} */
+
+/**
+ * Called by the frontend to request audio samples.
+ * The core should render audio within this function
+ * using the callback provided by \c retro_set_audio_sample or \c retro_set_audio_sample_batch.
+ *
+ * @warning This function may be called by any thread,
+ * therefore it must be thread-safe.
+ * @see RETRO_ENVIRONMENT_SET_AUDIO_CALLBACK
+ * @see retro_audio_callback
+ * @see retro_audio_sample_batch_t
+ * @see retro_audio_sample_t
+ */
+typedef void (RETRO_CALLCONV *retro_audio_callback_t)(void);
+
+/**
+ * Called by the frontend to notify the core that it should pause or resume audio rendering.
+ * The initial state of the audio driver after registering this callback is \c false (inactive).
+ *
+ * @param enabled \c true if the frontend's audio driver is active.
+ * If so, the registered audio callback will be called regularly.
+ * If not, the audio callback will not be invoked until the next time
+ * the frontend calls this function with \c true.
+ * @warning This function may be called by any thread,
+ * therefore it must be thread-safe.
+ * @note Even if no audio samples are rendered,
+ * the core should continue to update its emulated platform's audio engine if necessary.
+ * @see RETRO_ENVIRONMENT_SET_AUDIO_CALLBACK
+ * @see retro_audio_callback
+ * @see retro_audio_callback_t
+ */
+typedef void (RETRO_CALLCONV *retro_audio_set_state_callback_t)(bool enabled);
+
+/**
+ * An interface that the frontend uses to request audio samples from the core.
+ * @note To unregister a callback, pass a \c retro_audio_callback_t
+ * with both fields set to NULL.
+ * @see RETRO_ENVIRONMENT_SET_AUDIO_CALLBACK
+ */
+struct retro_audio_callback
+{
+ /** @see retro_audio_callback_t */
+ retro_audio_callback_t callback;
+
+ /** @see retro_audio_set_state_callback_t */
+ retro_audio_set_state_callback_t set_state;
+};
+
+typedef int64_t retro_usec_t;
+
+/**
+ * Called right before each iteration of \c retro_run
+ * if registered via RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK.
+ *
+ * @param usec Time since the last call to retro_run, in microseconds.
+ * If the frontend is manipulating the frame time
+ * (e.g. via fast-forward or slow motion),
+ * this value will be the reference value initially provided to the environment call.
+ * @see RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK
+ * @see retro_frame_time_callback
+ */
+typedef void (RETRO_CALLCONV *retro_frame_time_callback_t)(retro_usec_t usec);
+
+/**
+ * @see RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK
+ */
+struct retro_frame_time_callback
+{
+ /**
+ * Called to notify the core of the current frame time.
+ * If NULL, the frontend will clear its registered callback.
+ */
+ retro_frame_time_callback_t callback;
+
+ /**
+ * The ideal duration of one frame, in microseconds.
+ * Compute it as 1000000 / fps.
+ * The frontend will resolve rounding to ensure that framestepping, etc is exact.
+ */
+ retro_usec_t reference;
+};
+
+/** @defgroup SET_AUDIO_BUFFER_STATUS_CALLBACK Audio Buffer Occupancy
+ * @{
+ */
+
+/**
+ * Notifies a libretro core of how full the frontend's audio buffer is.
+ * Set by the core, called by the frontend.
+ * It will be called right before \c retro_run() every frame.
+ *
+ * @param active \c true if the frontend's audio buffer is currently in use,
+ * \c false if audio is disabled in the frontend.
+ * @param occupancy A value between 0 and 100 (inclusive),
+ * corresponding to the frontend's audio buffer occupancy percentage.
+ * @param underrun_likely \c true if the frontend expects an audio buffer underrun
+ * during the next frame, which indicates that a core should attempt frame-skipping.
+ */
+typedef void (RETRO_CALLCONV *retro_audio_buffer_status_callback_t)(
+ bool active, unsigned occupancy, bool underrun_likely);
+
+/**
+ * A callback to register with the frontend to receive audio buffer occupancy information.
+ */
+struct retro_audio_buffer_status_callback
+{
+ /** @copydoc retro_audio_buffer_status_callback_t */
+ retro_audio_buffer_status_callback_t callback;
+};
+
+/** @} */
+
+/* Pass this to retro_video_refresh_t if rendering to hardware.
+ * Passing NULL to retro_video_refresh_t is still a frame dupe as normal.
+ * */
+#define RETRO_HW_FRAME_BUFFER_VALID ((void*)-1)
+
+/* Invalidates the current HW context.
+ * Any GL state is lost, and must not be deinitialized explicitly.
+ * If explicit deinitialization is desired by the libretro core,
+ * it should implement context_destroy callback.
+ * If called, all GPU resources must be reinitialized.
+ * Usually called when frontend reinits video driver.
+ * Also called first time video driver is initialized,
+ * allowing libretro core to initialize resources.
+ */
+typedef void (RETRO_CALLCONV *retro_hw_context_reset_t)(void);
+
+/* Gets current framebuffer which is to be rendered to.
+ * Could change every frame potentially.
+ */
+typedef uintptr_t (RETRO_CALLCONV *retro_hw_get_current_framebuffer_t)(void);
+
+/* Get a symbol from HW context. */
+typedef retro_proc_address_t (RETRO_CALLCONV *retro_hw_get_proc_address_t)(const char *sym);
+
+enum retro_hw_context_type
+{
+ RETRO_HW_CONTEXT_NONE = 0,
+ /* OpenGL 2.x. Driver can choose to use latest compatibility context. */
+ RETRO_HW_CONTEXT_OPENGL = 1,
+ /* OpenGL ES 2.0. */
+ RETRO_HW_CONTEXT_OPENGLES2 = 2,
+ /* Modern desktop core GL context. Use version_major/
+ * version_minor fields to set GL version. */
+ RETRO_HW_CONTEXT_OPENGL_CORE = 3,
+ /* OpenGL ES 3.0 */
+ RETRO_HW_CONTEXT_OPENGLES3 = 4,
+ /* OpenGL ES 3.1+. Set version_major/version_minor. For GLES2 and GLES3,
+ * use the corresponding enums directly. */
+ RETRO_HW_CONTEXT_OPENGLES_VERSION = 5,
+
+ /* Vulkan, see RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE. */
+ RETRO_HW_CONTEXT_VULKAN = 6,
+
+ /* Direct3D11, see RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE */
+ RETRO_HW_CONTEXT_D3D11 = 7,
+
+ /* Direct3D10, see RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE */
+ RETRO_HW_CONTEXT_D3D10 = 8,
+
+ /* Direct3D12, see RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE */
+ RETRO_HW_CONTEXT_D3D12 = 9,
+
+ /* Direct3D9, see RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE */
+ RETRO_HW_CONTEXT_D3D9 = 10,
+
+ /** Dummy value to ensure sizeof(enum retro_hw_context_type) == sizeof(int). Do not use. */
+ RETRO_HW_CONTEXT_DUMMY = INT_MAX
+};
+
+struct retro_hw_render_callback
+{
+ /* Which API to use. Set by libretro core. */
+ enum retro_hw_context_type context_type;
+
+ /* Called when a context has been created or when it has been reset.
+ * An OpenGL context is only valid after context_reset() has been called.
+ *
+ * When context_reset is called, OpenGL resources in the libretro
+ * implementation are guaranteed to be invalid.
+ *
+ * It is possible that context_reset is called multiple times during an
+ * application lifecycle.
+ * If context_reset is called without any notification (context_destroy),
+ * the OpenGL context was lost and resources should just be recreated
+ * without any attempt to "free" old resources.
+ */
+ retro_hw_context_reset_t context_reset;
+
+ /* Set by frontend.
+ * TODO: This is rather obsolete. The frontend should not
+ * be providing preallocated framebuffers. */
+ retro_hw_get_current_framebuffer_t get_current_framebuffer;
+
+ /* Set by frontend.
+ * Can return all relevant functions, including glClear on Windows. */
+ retro_hw_get_proc_address_t get_proc_address;
+
+ /* Set if render buffers should have depth component attached.
+ * TODO: Obsolete. */
+ bool depth;
+
+ /* Set if stencil buffers should be attached.
+ * TODO: Obsolete. */
+ bool stencil;
+
+ /* If depth and stencil are true, a packed 24/8 buffer will be added.
+ * Only attaching stencil is invalid and will be ignored. */
+
+ /* Use conventional bottom-left origin convention. If false,
+ * standard libretro top-left origin semantics are used.
+ * TODO: Move to GL specific interface. */
+ bool bottom_left_origin;
+
+ /* Major version number for core GL context or GLES 3.1+. */
+ unsigned version_major;
+
+ /* Minor version number for core GL context or GLES 3.1+. */
+ unsigned version_minor;
+
+ /* If this is true, the frontend will go very far to avoid
+ * resetting context in scenarios like toggling fullscreen, etc.
+ * TODO: Obsolete? Maybe frontend should just always assume this ...
+ */
+ bool cache_context;
+
+ /* The reset callback might still be called in extreme situations
+ * such as if the context is lost beyond recovery.
+ *
+ * For optimal stability, set this to false, and allow context to be
+ * reset at any time.
+ */
+
+ /* A callback to be called before the context is destroyed in a
+ * controlled way by the frontend. */
+ retro_hw_context_reset_t context_destroy;
+
+ /* OpenGL resources can be deinitialized cleanly at this step.
+ * context_destroy can be set to NULL, in which resources will
+ * just be destroyed without any notification.
+ *
+ * Even when context_destroy is non-NULL, it is possible that
+ * context_reset is called without any destroy notification.
+ * This happens if context is lost by external factors (such as
+ * notified by GL_ARB_robustness).
+ *
+ * In this case, the context is assumed to be already dead,
+ * and the libretro implementation must not try to free any OpenGL
+ * resources in the subsequent context_reset.
+ */
+
+ /* Creates a debug context. */
+ bool debug_context;
+};
+
+/* Callback type passed in RETRO_ENVIRONMENT_SET_KEYBOARD_CALLBACK.
+ * Called by the frontend in response to keyboard events.
+ * down is set if the key is being pressed, or false if it is being released.
+ * keycode is the RETROK value of the char.
+ * character is the text character of the pressed key. (UTF-32).
+ * key_modifiers is a set of RETROKMOD values or'ed together.
+ *
+ * The pressed/keycode state can be independent of the character.
+ * It is also possible that multiple characters are generated from a
+ * single keypress.
+ * Keycode events should be treated separately from character events.
+ * However, when possible, the frontend should try to synchronize these.
+ * If only a character is posted, keycode should be RETROK_UNKNOWN.
+ *
+ * Similarly if only a keycode event is generated with no corresponding
+ * character, character should be 0.
+ */
+typedef void (RETRO_CALLCONV *retro_keyboard_event_t)(bool down, unsigned keycode,
+ uint32_t character, uint16_t key_modifiers);
+
+struct retro_keyboard_callback
+{
+ retro_keyboard_event_t callback;
+};
+
+/** @defgroup SET_DISK_CONTROL_INTERFACE Disk Control
+ *
+ * Callbacks for inserting and removing disks from the emulated console at runtime.
+ * Should be provided by cores that support doing so.
+ * Cores should automate this process if possible,
+ * but some cases require the player's manual input.
+ *
+ * The steps for swapping disk images are generally as follows:
+ *
+ * \li Eject the emulated console's disk drive with \c set_eject_state(true).
+ * \li Insert the new disk image with \c set_image_index(index).
+ * \li Close the virtual disk tray with \c set_eject_state(false).
+ *
+ * @{
+ */
+
+/**
+ * Called by the frontend to open or close the emulated console's virtual disk tray.
+ *
+ * The frontend may only set the disk image index
+ * while the emulated tray is opened.
+ *
+ * If the emulated console's disk tray is already in the state given by \c ejected,
+ * then this function should return \c true without doing anything.
+ * The core should return \c false if it couldn't change the disk tray's state;
+ * this may happen if the console itself limits when the disk tray can be open or closed
+ * (e.g. to wait for the disc to stop spinning).
+ *
+ * @param ejected \c true if the virtual disk tray should be "ejected",
+ * \c false if it should be "closed".
+ * @return \c true if the virtual disk tray's state has been set to the given state,
+ * false if there was an error.
+ * @see retro_get_eject_state_t
+ */
+typedef bool (RETRO_CALLCONV *retro_set_eject_state_t)(bool ejected);
+
+/**
+ * Gets the current ejected state of the disk drive.
+ * The initial state is closed, i.e. \c false.
+ *
+ * @return \c true if the virtual disk tray is "ejected",
+ * i.e. it's open and a disk can be inserted.
+ * @see retro_set_eject_state_t
+ */
+typedef bool (RETRO_CALLCONV *retro_get_eject_state_t)(void);
+
+/**
+ * Gets the index of the current disk image,
+ * as determined by however the frontend orders disk images
+ * (such as m3u-formatted playlists or special directories).
+ *
+ * @return The index of the current disk image
+ * (starting with 0 for the first disk),
+ * or a value greater than or equal to \c get_num_images() if no disk is inserted.
+ * @see retro_get_num_images_t
+ */
+typedef unsigned (RETRO_CALLCONV *retro_get_image_index_t)(void);
+
+/**
+ * Inserts the disk image at the given index into the emulated console's drive.
+ * Can only be called while the disk tray is ejected
+ * (i.e. \c retro_get_eject_state_t returns \c true).
+ *
+ * If the emulated disk tray is ejected
+ * and already contains the disk image named by \c index,
+ * then this function should do nothing and return \c true.
+ *
+ * @param index The index of the disk image to insert,
+ * starting from 0 for the first disk.
+ * A value greater than or equal to \c get_num_images()
+ * represents the frontend removing the disk without inserting a new one.
+ * @return \c true if the disk image was successfully set.
+ * \c false if the disk tray isn't ejected or there was another error
+ * inserting a new disk image.
+ */
+typedef bool (RETRO_CALLCONV *retro_set_image_index_t)(unsigned index);
+
+/**
+ * @return The number of disk images which are available to use.
+ * These are most likely defined in a playlist file.
+ */
+typedef unsigned (RETRO_CALLCONV *retro_get_num_images_t)(void);
+
+struct retro_game_info;
+
+/**
+ * Replaces the disk image at the given index with a new disk.
+ *
+ * Replaces the disk image associated with index.
+ * Arguments to pass in info have same requirements as retro_load_game().
+ * Virtual disk tray must be ejected when calling this.
+ *
+ * Passing \c NULL to this function indicates
+ * that the frontend has removed this disk image from its internal list.
+ * As a result, calls to this function can change the number of available disk indexes.
+ *
+ * For example, calling replace_image_index(1, NULL)
+ * will remove the disk image at index 1,
+ * and the disk image at index 2 (if any)
+ * will be moved to the newly-available index 1.
+ *
+ * @param index The index of the disk image to replace.
+ * @param info Details about the new disk image,
+ * or \c NULL if the disk image at the given index should be discarded.
+ * The semantics of each field are the same as in \c retro_load_game.
+ * @return \c true if the disk image was successfully replaced
+ * or removed from the playlist,
+ * \c false if the tray is not ejected
+ * or if there was an error.
+ */
+typedef bool (RETRO_CALLCONV *retro_replace_image_index_t)(unsigned index,
+ const struct retro_game_info *info);
+
+/**
+ * Adds a new index to the core's internal disk list.
+ * This will increment the return value from \c get_num_images() by 1.
+ * This image index cannot be used until a disk image has been set
+ * with \c replace_image_index.
+ *
+ * @return \c true if the core has added space for a new disk image
+ * and is ready to receive one.
+ */
+typedef bool (RETRO_CALLCONV *retro_add_image_index_t)(void);
+
+/**
+ * Sets the disk image that will be inserted into the emulated disk drive
+ * before \c retro_load_game is called.
+ *
+ * \c retro_load_game does not provide a way to ensure
+ * that a particular disk image in a playlist is inserted into the console;
+ * this function makes up for that.
+ * Frontends should call it immediately before \c retro_load_game,
+ * and the core should use the arguments
+ * to validate the disk image in \c retro_load_game.
+ *
+ * When content is loaded, the core should verify that the
+ * disk specified by \c index can be found at \c path.
+ * This is to guard against auto-selecting the wrong image
+ * if (for example) the user should modify an existing M3U playlist.
+ * We have to let the core handle this because
+ * \c set_initial_image() must be called before loading content,
+ * i.e. the frontend cannot access image paths in advance
+ * and thus cannot perform the error check itself.
+ * If \c index is invalid (i.e. index >= get_num_images())
+ * or the disk image doesn't match the value given in \c path,
+ * the core should ignore the arguments
+ * and insert the disk at index 0 into the virtual disk tray.
+ *
+ * @warning If \c RETRO_ENVIRONMENT_SET_DISK_CONTROL_EXT_INTERFACE is called within \c retro_load_game,
+ * then this function may not be executed.
+ * Set the disk control interface in \c retro_init if possible.
+ *
+ * @param index The index of the disk image within the playlist to set.
+ * @param path The path of the disk image to set as the first.
+ * The core should not load this path immediately;
+ * instead, it should use it within \c retro_load_game
+ * to verify that the correct disk image was loaded.
+ * @return \c true if the initial disk index was set,
+ * \c false if the arguments are invalid
+ * or the core doesn't support this function.
+ */
+typedef bool (RETRO_CALLCONV *retro_set_initial_image_t)(unsigned index, const char *path);
+
+/**
+ * Returns the path of the disk image at the given index
+ * on the host's file system.
+ *
+ * @param index The index of the disk image to get the path of.
+ * @param s A buffer to store the path in.
+ * @param len The size of \c s, in bytes.
+ * @return \c true if the disk image's location was successfully
+ * queried and copied into \c s,
+ * \c false if the index is invalid
+ * or the core couldn't locate the disk image.
+ */
+typedef bool (RETRO_CALLCONV *retro_get_image_path_t)(unsigned index, char *s, size_t len);
+
+/**
+ * Returns a friendly label for the given disk image.
+ *
+ * In the simplest case, this may be the disk image's file name
+ * with the extension omitted.
+ * For cores or games with more complex content requirements,
+ * the label can be used to provide information to help the player
+ * select a disk image to insert;
+ * for example, a core may label different kinds of disks
+ * (save data, level disk, installation disk, bonus content, etc.).
+ * with names that correspond to in-game prompts,
+ * so that the frontend can provide better guidance to the player.
+ *
+ * @param index The index of the disk image to return a label for.
+ * @param s A buffer to store the resulting label in.
+ * @param len The length of \c s, in bytes.
+ * @return \c true if the disk image at \c index is valid
+ * and a label was copied into \c s.
+ */
+typedef bool (RETRO_CALLCONV *retro_get_image_label_t)(unsigned index, char *s, size_t len);
+
+/**
+ * An interface that the frontend can use to exchange disks
+ * within the emulated console's disk drive.
+ *
+ * All function pointers are required.
+ *
+ * @deprecated This struct is superseded by \ref retro_disk_control_ext_callback.
+ * Only use this one to maintain compatibility
+ * with older cores and frontends.
+ *
+ * @see RETRO_ENVIRONMENT_SET_DISK_CONTROL_EXT_INTERFACE
+ * @see retro_disk_control_ext_callback
+ */
+struct retro_disk_control_callback
+{
+ /** @copydoc retro_set_eject_state_t */
+ retro_set_eject_state_t set_eject_state;
+
+ /** @copydoc retro_get_eject_state_t */
+ retro_get_eject_state_t get_eject_state;
+
+ /** @copydoc retro_get_image_index_t */
+ retro_get_image_index_t get_image_index;
+
+ /** @copydoc retro_set_image_index_t */
+ retro_set_image_index_t set_image_index;
+
+ /** @copydoc retro_get_num_images_t */
+ retro_get_num_images_t get_num_images;
+
+ /** @copydoc retro_replace_image_index_t */
+ retro_replace_image_index_t replace_image_index;
+
+ /** @copydoc retro_add_image_index_t */
+ retro_add_image_index_t add_image_index;
+};
+
+/**
+ * @copybrief retro_disk_control_callback
+ *
+ * All function pointers are required unless otherwise noted.
+ *
+ * @see RETRO_ENVIRONMENT_SET_DISK_CONTROL_EXT_INTERFACE
+ */
+struct retro_disk_control_ext_callback
+{
+ /** @copydoc retro_set_eject_state_t */
+ retro_set_eject_state_t set_eject_state;
+
+ /** @copydoc retro_get_eject_state_t */
+ retro_get_eject_state_t get_eject_state;
+
+ /** @copydoc retro_get_image_index_t */
+ retro_get_image_index_t get_image_index;
+
+ /** @copydoc retro_set_image_index_t */
+ retro_set_image_index_t set_image_index;
+
+ /** @copydoc retro_get_num_images_t */
+ retro_get_num_images_t get_num_images;
+
+ /** @copydoc retro_replace_image_index_t */
+ retro_replace_image_index_t replace_image_index;
+
+ /** @copydoc retro_add_image_index_t */
+ retro_add_image_index_t add_image_index;
+
+ /** @copydoc retro_set_initial_image_t
+ *
+ * Optional; not called if \c NULL.
+ *
+ * @note The frontend will only try to record/restore the last-used disk index
+ * if both \c set_initial_image and \c get_image_path are implemented.
+ */
+ retro_set_initial_image_t set_initial_image;
+
+ /**
+ * @copydoc retro_get_image_path_t
+ *
+ * Optional; not called if \c NULL.
+ */
+ retro_get_image_path_t get_image_path;
+
+ /**
+ * @copydoc retro_get_image_label_t
+ *
+ * Optional; not called if \c NULL.
+ */
+ retro_get_image_label_t get_image_label;
+};
+
+/** @} */
+
+/* Definitions for RETRO_ENVIRONMENT_SET_NETPACKET_INTERFACE.
+ * A core can set it if sending and receiving custom network packets
+ * during a multiplayer session is desired.
+ */
+
+/* Netpacket flags for retro_netpacket_send_t */
+#define RETRO_NETPACKET_UNRELIABLE 0 /* Packet to be sent unreliable, depending on network quality it might not arrive. */
+#define RETRO_NETPACKET_RELIABLE (1 << 0) /* Reliable packets are guaranteed to arrive at the target in the order they were sent. */
+#define RETRO_NETPACKET_UNSEQUENCED (1 << 1) /* Packet will not be sequenced with other packets and may arrive out of order. Cannot be set on reliable packets. */
+#define RETRO_NETPACKET_FLUSH_HINT (1 << 2) /* Request the packet and any previously buffered ones to be sent immediately */
+
+/* Broadcast client_id for retro_netpacket_send_t */
+#define RETRO_NETPACKET_BROADCAST 0xFFFF
+
+/* Used by the core to send a packet to one or all connected players.
+ * A single packet sent via this interface can contain up to 64 KB of data.
+ *
+ * The client_id RETRO_NETPACKET_BROADCAST sends the packet as a broadcast to
+ * all connected players. This is supported from the host as well as clients.
+* Otherwise, the argument indicates the player to send the packet to.
+ *
+ * A frontend must support sending reliable packets (RETRO_NETPACKET_RELIABLE).
+ * Unreliable packets might not be supported by the frontend, but the flags can
+ * still be specified. Reliable transmission will be used instead.
+ *
+ * Calling this with the flag RETRO_NETPACKET_FLUSH_HINT will send off the
+ * packet and any previously buffered ones immediately and without blocking.
+ * To only flush previously queued packets, buf or len can be passed as NULL/0.
+ *
+ * This function is not guaranteed to be thread-safe and must be called during
+ * retro_run or any of the netpacket callbacks passed with this interface.
+ */
+typedef void (RETRO_CALLCONV *retro_netpacket_send_t)(int flags, const void* buf, size_t len, uint16_t client_id);
+
+/* Optionally read any incoming packets without waiting for the end of the
+ * frame. While polling, retro_netpacket_receive_t and retro_netpacket_stop_t
+ * can be called. The core can perform this in a loop to do a blocking read,
+ * i.e., wait for incoming data, but needs to handle stop getting called and
+ * also give up after a short while to avoid freezing on a connection problem.
+ * It is a good idea to manually flush outgoing packets before calling this.
+ *
+ * This function is not guaranteed to be thread-safe and must be called during
+ * retro_run or any of the netpacket callbacks passed with this interface.
+ */
+typedef void (RETRO_CALLCONV *retro_netpacket_poll_receive_t)(void);
+
+/* Called by the frontend to signify that a multiplayer session has started.
+ * If client_id is 0 the local player is the host of the session and at this
+ * point no other player has connected yet.
+ *
+ * If client_id is > 0 the local player is a client connected to a host and
+ * at this point is already fully connected to the host.
+ *
+ * The core must store the function pointer send_fn and use it whenever it
+ * wants to send a packet. Optionally poll_receive_fn can be stored and used
+ * when regular receiving between frames is not enough. These function pointers
+ * remain valid until the frontend calls retro_netpacket_stop_t.
+ */
+typedef void (RETRO_CALLCONV *retro_netpacket_start_t)(uint16_t client_id, retro_netpacket_send_t send_fn, retro_netpacket_poll_receive_t poll_receive_fn);
+
+/* Called by the frontend when a new packet arrives which has been sent from
+ * another player with retro_netpacket_send_t. The client_id argument indicates
+ * who has sent the packet.
+ */
+typedef void (RETRO_CALLCONV *retro_netpacket_receive_t)(const void* buf, size_t len, uint16_t client_id);
+
+/* Called by the frontend when the multiplayer session has ended.
+ * Once this gets called the function pointers passed to
+ * retro_netpacket_start_t will not be valid anymore.
+ */
+typedef void (RETRO_CALLCONV *retro_netpacket_stop_t)(void);
+
+/* Called by the frontend every frame (between calls to retro_run while
+ * updating the state of the multiplayer session.
+ * This is a good place for the core to call retro_netpacket_send_t from.
+ */
+typedef void (RETRO_CALLCONV *retro_netpacket_poll_t)(void);
+
+/* Called by the frontend when a new player connects to the hosted session.
+ * This is only called on the host side, not for clients connected to the host.
+ * If this function returns false, the newly connected player gets dropped.
+ * This can be used for example to limit the number of players.
+ */
+typedef bool (RETRO_CALLCONV *retro_netpacket_connected_t)(uint16_t client_id);
+
+/* Called by the frontend when a player leaves or disconnects from the hosted session.
+ * This is only called on the host side, not for clients connected to the host.
+ */
+typedef void (RETRO_CALLCONV *retro_netpacket_disconnected_t)(uint16_t client_id);
+
+/**
+ * A callback interface for giving a core the ability to send and receive custom
+ * network packets during a multiplayer session between two or more instances
+ * of a libretro frontend.
+ *
+ * Normally during connection handshake the frontend will compare library_version
+ * used by both parties and show a warning if there is a difference. When the core
+ * supplies protocol_version, the frontend will check against this instead.
+ *
+ * @see RETRO_ENVIRONMENT_SET_NETPACKET_INTERFACE
+ */
+struct retro_netpacket_callback
+{
+ retro_netpacket_start_t start;
+ retro_netpacket_receive_t receive;
+ retro_netpacket_stop_t stop; /* Optional - may be NULL */
+ retro_netpacket_poll_t poll; /* Optional - may be NULL */
+ retro_netpacket_connected_t connected; /* Optional - may be NULL */
+ retro_netpacket_disconnected_t disconnected; /* Optional - may be NULL */
+ const char* protocol_version; /* Optional - if not NULL will be used instead of core version to decide if communication is compatible */
+};
+
+/**
+ * The pixel format used for rendering.
+ * @see RETRO_ENVIRONMENT_SET_PIXEL_FORMAT
+ */
+enum retro_pixel_format
+{
+ /**
+ * 0RGB1555, native endian.
+ * Used as the default if \c RETRO_ENVIRONMENT_SET_PIXEL_FORMAT is not called.
+ * The most significant bit must be set to 0.
+ * @deprecated This format remains supported to maintain compatibility.
+ * New code should use RETRO_PIXEL_FORMAT_RGB565 instead.
+ * @see RETRO_PIXEL_FORMAT_RGB565
+ */
+ RETRO_PIXEL_FORMAT_0RGB1555 = 0,
+
+ /**
+ * XRGB8888, native endian.
+ * The most significant byte (the X) is ignored.
+ */
+ RETRO_PIXEL_FORMAT_XRGB8888 = 1,
+
+ /**
+ * RGB565, native endian.
+ * This format is recommended if 16-bit pixels are desired,
+ * as it is available on a variety of devices and APIs.
+ */
+ RETRO_PIXEL_FORMAT_RGB565 = 2,
+
+ /** Defined to ensure that sizeof(retro_pixel_format) == sizeof(int). Do not use. */
+ RETRO_PIXEL_FORMAT_UNKNOWN = INT_MAX
+};
+
+/** @defgroup GET_SAVESTATE_CONTEXT Savestate Context
+ * @{
+ */
+
+/**
+ * Details about how the frontend will use savestates.
+ *
+ * @see RETRO_ENVIRONMENT_GET_SAVESTATE_CONTEXT
+ * @see retro_serialize
+ */
+enum retro_savestate_context
+{
+ /**
+ * Standard savestate written to disk.
+ * May be loaded at any time,
+ * even in a separate session or on another device.
+ *
+ * Should not contain any pointers to code or data.
+ */
+ RETRO_SAVESTATE_CONTEXT_NORMAL = 0,
+
+ /**
+ * The savestate is guaranteed to be loaded
+ * within the same session, address space, and binary.
+ * Will not be written to disk or sent over the network;
+ * therefore, internal pointers to code or data are acceptable.
+ * May still be loaded or saved at any time.
+ *
+ * @note This context generally implies the use of runahead or rewinding,
+ * which may work by taking savestates multiple times per second.
+ * Savestate code that runs in this context should be fast.
+ */
+ RETRO_SAVESTATE_CONTEXT_RUNAHEAD_SAME_INSTANCE = 1,
+
+ /**
+ * The savestate is guaranteed to be loaded
+ * in the same session and by the same binary,
+ * but possibly by a different address space
+ * (e.g. for "second instance" runahead)
+ *
+ * Will not be written to disk or sent over the network,
+ * but may be loaded in a different address space.
+ * Therefore, the savestate must not contain pointers.
+ */
+ RETRO_SAVESTATE_CONTEXT_RUNAHEAD_SAME_BINARY = 2,
+
+ /**
+ * The savestate will not be written to disk,
+ * but no other guarantees are made.
+ * The savestate will almost certainly be loaded
+ * by a separate binary, device, and address space.
+ *
+ * This context is intended for use with frontends that support rollback netplay.
+ * Serialized state should omit any data that would unnecessarily increase bandwidth usage.
+ * Must not contain pointers, and integers must be saved in big-endian format.
+ * @see retro_endianness.h
+ * @see network_stream
+ */
+ RETRO_SAVESTATE_CONTEXT_ROLLBACK_NETPLAY = 3,
+
+ /**
+ * @private Defined to ensure sizeof(retro_savestate_context) == sizeof(int).
+ * Do not use.
+ */
+ RETRO_SAVESTATE_CONTEXT_UNKNOWN = INT_MAX
+};
+
+/** @} */
+
+/** @defgroup SET_MESSAGE User-Visible Messages
+ *
+ * @{
+ */
+
+/**
+ * Defines a message that the frontend will display to the user,
+ * as determined by RETRO_ENVIRONMENT_SET_MESSAGE.
+ *
+ * @deprecated This struct is superseded by \ref retro_message_ext,
+ * which provides more control over how a message is presented.
+ * Only use it for compatibility with older cores and frontends.
+ *
+ * @see RETRO_ENVIRONMENT_SET_MESSAGE
+ * @see retro_message_ext
+ */
+struct retro_message
+{
+ /**
+ * Null-terminated message to be displayed.
+ * If \c NULL or empty, the message will be ignored.
+ */
+ const char *msg;
+
+ /** Duration to display \c msg in frames. */
+ unsigned frames;
+};
+
+/**
+ * The method that the frontend will use to display a message to the player.
+ * @see retro_message_ext
+ */
+enum retro_message_target
+{
+ /**
+ * Indicates that the frontend should display the given message
+ * using all other targets defined by \c retro_message_target at once.
+ */
+ RETRO_MESSAGE_TARGET_ALL = 0,
+
+ /**
+ * Indicates that the frontend should display the given message
+ * using the frontend's on-screen display, if available.
+ *
+ * @attention If the frontend allows players to customize or disable notifications,
+ * then they may not see messages sent to this target.
+ */
+ RETRO_MESSAGE_TARGET_OSD,
+
+ /**
+ * Indicates that the frontend should log the message
+ * via its usual logging mechanism, if available.
+ *
+ * This is not intended to be a substitute for \c RETRO_ENVIRONMENT_SET_LOG_INTERFACE.
+ * It is intended for the common use case of
+ * logging a player-facing message.
+ *
+ * This target should not be used for messages
+ * of type \c RETRO_MESSAGE_TYPE_STATUS or \c RETRO_MESSAGE_TYPE_PROGRESS,
+ * as it may add unnecessary noise to a log file.
+ *
+ * @see RETRO_ENVIRONMENT_SET_LOG_INTERFACE
+ */
+ RETRO_MESSAGE_TARGET_LOG
+};
+
+/**
+ * A broad category for the type of message that the frontend will display.
+ *
+ * Each message type has its own use case,
+ * therefore the frontend should present each one differently.
+ *
+ * @note This is a hint that the frontend may ignore.
+ * The frontend should fall back to \c RETRO_MESSAGE_TYPE_NOTIFICATION
+ * for message types that it doesn't support.
+ */
+enum retro_message_type
+{
+ /**
+ * A standard on-screen message.
+ *
+ * Suitable for a variety of use cases,
+ * such as messages about errors
+ * or other important events.
+ *
+ * Frontends that display their own messages
+ * should display this type of core-generated message the same way.
+ */
+ RETRO_MESSAGE_TYPE_NOTIFICATION = 0,
+
+ /**
+ * An on-screen message that should be visually distinct
+ * from \c RETRO_MESSAGE_TYPE_NOTIFICATION messages.
+ *
+ * The exact meaning of "visually distinct" is left to the frontend,
+ * but this usually implies that the frontend shows the message
+ * in a way that it doesn't typically use for its own notices.
+ */
+ RETRO_MESSAGE_TYPE_NOTIFICATION_ALT,
+
+ /**
+ * Indicates a frequently-updated status display,
+ * rather than a standard notification.
+ * Status messages are intended to be displayed permanently while a core is running
+ * in a way that doesn't suggest user action is required.
+ *
+ * Here are some possible use cases for status messages:
+ *
+ * @li An internal framerate counter.
+ * @li Debugging information.
+ * Remember to let the player disable it in the core options.
+ * @li Core-specific state, such as when a microphone is active.
+ *
+ * The status message is displayed for the given duration,
+ * unless another status message of equal or greater priority is shown.
+ */
+ RETRO_MESSAGE_TYPE_STATUS,
+
+ /**
+ * Denotes a message that reports the progress
+ * of a long-running asynchronous task,
+ * such as when a core loads large files from disk or the network.
+ *
+ * The frontend should display messages of this type as a progress bar
+ * (or a progress spinner for indefinite tasks),
+ * where \c retro_message_ext::msg is the progress bar's title
+ * and \c retro_message_ext::progress sets the progress bar's length.
+ *
+ * This message type shouldn't be used for tasks that are expected to complete quickly.
+ */
+ RETRO_MESSAGE_TYPE_PROGRESS
+};
+
+/**
+ * A core-provided message that the frontend will display to the player.
+ *
+ * @note The frontend is encouraged store these messages in a queue.
+ * However, it should not empty the queue of core-submitted messages upon exit;
+ * if a core exits with an error, it may want to use this API
+ * to show an error message to the player.
+ *
+ * The frontend should maintain its own copy of the submitted message
+ * and all subobjects, including strings.
+ *
+ * @see RETRO_ENVIRONMENT_SET_MESSAGE_EXT
+ */
+struct retro_message_ext
+{
+ /**
+ * The \c NULL-terminated text of a message to show to the player.
+ * Must not be \c NULL.
+ *
+ * @note The frontend must honor newlines in this string
+ * when rendering text to \c RETRO_MESSAGE_TARGET_OSD.
+ */
+ const char *msg;
+
+ /**
+ * The duration that \c msg will be displayed on-screen, in milliseconds.
+ *
+ * Ignored for \c RETRO_MESSAGE_TARGET_LOG.
+ */
+ unsigned duration;
+
+ /**
+ * The relative importance of this message
+ * when targeting \c RETRO_MESSAGE_TARGET_OSD.
+ * Higher values indicate higher priority.
+ *
+ * The frontend should use this to prioritize messages
+ * when it can't show all active messages at once,
+ * or to remove messages from its queue if it's full.
+ *
+ * The relative display order of messages with the same priority
+ * is left to the frontend's discretion,
+ * although we suggest breaking ties
+ * in favor of the most recently-submitted message.
+ *
+ * Frontends may handle deprioritized messages at their discretion;
+ * such messages may have their \c duration altered,
+ * be hidden without being delayed,
+ * or even be discarded entirely.
+ *
+ * @note In the reference frontend (RetroArch),
+ * the same priority values are used for frontend-generated notifications,
+ * which are typically between 0 and 3 depending upon importance.
+ *
+ * Ignored for \c RETRO_MESSAGE_TARGET_LOG.
+ */
+ unsigned priority;
+
+ /**
+ * The severity level of this message.
+ *
+ * The frontend may use this to filter or customize messages
+ * depending on the player's preferences.
+ * Here are some ideas:
+ *
+ * @li Use this to prioritize errors and warnings
+ * over higher-ranking info and debug messages.
+ * @li Render warnings or errors with extra visual feedback,
+ * e.g. with brighter colors or accompanying sound effects.
+ *
+ * @see RETRO_ENVIRONMENT_SET_LOG_INTERFACE
+ */
+ enum retro_log_level level;
+
+ /**
+ * The intended destination of this message.
+ *
+ * @see retro_message_target
+ */
+ enum retro_message_target target;
+
+ /**
+ * The intended semantics of this message.
+ *
+ * Ignored for \c RETRO_MESSAGE_TARGET_LOG.
+ *
+ * @see retro_message_type
+ */
+ enum retro_message_type type;
+
+ /**
+ * The progress of an asynchronous task.
+ *
+ * A value between 0 and 100 (inclusive) indicates the task's percentage,
+ * and a value of -1 indicates a task of unknown completion.
+ *
+ * @note Since message type is a hint, a frontend may ignore progress values.
+ * Where relevant, a core should include progress percentage within the message string,
+ * such that the message intent remains clear when displayed
+ * as a standard frontend-generated notification.
+ *
+ * Ignored for \c RETRO_MESSAGE_TARGET_LOG and for
+ * message types other than \c RETRO_MESSAGE_TYPE_PROGRESS.
+ */
+ int8_t progress;
+};
+
+/** @} */
+
+/* Describes how the libretro implementation maps a libretro input bind
+ * to its internal input system through a human readable string.
+ * This string can be used to better let a user configure input. */
+struct retro_input_descriptor
+{
+ /* Associates given parameters with a description. */
+ unsigned port;
+ unsigned device;
+ unsigned index;
+ unsigned id;
+
+ /* Human readable description for parameters.
+ * The pointer must remain valid until
+ * retro_unload_game() is called. */
+ const char *description;
+};
+
+/**
+ * Contains basic information about the core.
+ *
+ * @see retro_get_system_info
+ * @warning All pointers are owned by the core
+ * and must remain valid throughout its lifetime.
+ */
+struct retro_system_info
+{
+ /**
+ * Descriptive name of the library.
+ *
+ * @note Should not contain any version numbers, etc.
+ */
+ const char *library_name;
+
+ /**
+ * Descriptive version of the core.
+ */
+ const char *library_version;
+
+ /**
+ * A pipe-delimited string list of file extensions that this core can load, e.g. "bin|rom|iso".
+ * Typically used by a frontend for filtering or core selection.
+ */
+ const char *valid_extensions;
+
+ /* Libretro cores that need to have direct access to their content
+ * files, including cores which use the path of the content files to
+ * determine the paths of other files, should set need_fullpath to true.
+ *
+ * Cores should strive for setting need_fullpath to false,
+ * as it allows the frontend to perform patching, etc.
+ *
+ * If need_fullpath is true and retro_load_game() is called:
+ * - retro_game_info::path is guaranteed to have a valid path
+ * - retro_game_info::data and retro_game_info::size are invalid
+ *
+ * If need_fullpath is false and retro_load_game() is called:
+ * - retro_game_info::path may be NULL
+ * - retro_game_info::data and retro_game_info::size are guaranteed
+ * to be valid
+ *
+ * See also:
+ * - RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY
+ * - RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY
+ */
+ bool need_fullpath;
+
+ /* If true, the frontend is not allowed to extract any archives before
+ * loading the real content.
+ * Necessary for certain libretro implementations that load games
+ * from zipped archives. */
+ bool block_extract;
+};
+
+/* Defines overrides which modify frontend handling of
+ * specific content file types.
+ * An array of retro_system_content_info_override is
+ * passed to RETRO_ENVIRONMENT_SET_CONTENT_INFO_OVERRIDE
+ * NOTE: In the following descriptions, references to
+ * retro_load_game() may be replaced with
+ * retro_load_game_special() */
+struct retro_system_content_info_override
+{
+ /* A list of file extensions for which the override
+ * should apply, delimited by a 'pipe' character
+ * (e.g. "md|sms|gg")
+ * Permitted file extensions are limited to those
+ * included in retro_system_info::valid_extensions
+ * and/or retro_subsystem_rom_info::valid_extensions */
+ const char *extensions;
+
+ /* Overrides the need_fullpath value set in
+ * retro_system_info and/or retro_subsystem_rom_info.
+ * To reiterate:
+ *
+ * If need_fullpath is true and retro_load_game() is called:
+ * - retro_game_info::path is guaranteed to contain a valid
+ * path to an existent file
+ * - retro_game_info::data and retro_game_info::size are invalid
+ *
+ * If need_fullpath is false and retro_load_game() is called:
+ * - retro_game_info::path may be NULL
+ * - retro_game_info::data and retro_game_info::size are guaranteed
+ * to be valid
+ *
+ * In addition:
+ *
+ * If need_fullpath is true and retro_load_game() is called:
+ * - retro_game_info_ext::full_path is guaranteed to contain a valid
+ * path to an existent file
+ * - retro_game_info_ext::archive_path may be NULL
+ * - retro_game_info_ext::archive_file may be NULL
+ * - retro_game_info_ext::dir is guaranteed to contain a valid path
+ * to the directory in which the content file exists
+ * - retro_game_info_ext::name is guaranteed to contain the
+ * basename of the content file, without extension
+ * - retro_game_info_ext::ext is guaranteed to contain the
+ * extension of the content file in lower case format
+ * - retro_game_info_ext::data and retro_game_info_ext::size
+ * are invalid
+ *
+ * If need_fullpath is false and retro_load_game() is called:
+ * - If retro_game_info_ext::file_in_archive is false:
+ * - retro_game_info_ext::full_path is guaranteed to contain
+ * a valid path to an existent file
+ * - retro_game_info_ext::archive_path may be NULL
+ * - retro_game_info_ext::archive_file may be NULL
+ * - retro_game_info_ext::dir is guaranteed to contain a
+ * valid path to the directory in which the content file exists
+ * - retro_game_info_ext::name is guaranteed to contain the
+ * basename of the content file, without extension
+ * - retro_game_info_ext::ext is guaranteed to contain the
+ * extension of the content file in lower case format
+ * - If retro_game_info_ext::file_in_archive is true:
+ * - retro_game_info_ext::full_path may be NULL
+ * - retro_game_info_ext::archive_path is guaranteed to
+ * contain a valid path to an existent compressed file
+ * inside which the content file is located
+ * - retro_game_info_ext::archive_file is guaranteed to
+ * contain a valid path to an existent content file
+ * inside the compressed file referred to by
+ * retro_game_info_ext::archive_path
+ * e.g. for a compressed file '/path/to/foo.zip'
+ * containing 'bar.sfc'
+ * > retro_game_info_ext::archive_path will be '/path/to/foo.zip'
+ * > retro_game_info_ext::archive_file will be 'bar.sfc'
+ * - retro_game_info_ext::dir is guaranteed to contain a
+ * valid path to the directory in which the compressed file
+ * (containing the content file) exists
+ * - retro_game_info_ext::name is guaranteed to contain
+ * EITHER
+ * 1) the basename of the compressed file (containing
+ * the content file), without extension
+ * OR
+ * 2) the basename of the content file inside the
+ * compressed file, without extension
+ * In either case, a core should consider 'name' to
+ * be the canonical name/ID of the the content file
+ * - retro_game_info_ext::ext is guaranteed to contain the
+ * extension of the content file inside the compressed file,
+ * in lower case format
+ * - retro_game_info_ext::data and retro_game_info_ext::size are
+ * guaranteed to be valid */
+ bool need_fullpath;
+
+ /* If need_fullpath is false, specifies whether the content
+ * data buffer available in retro_load_game() is 'persistent'
+ *
+ * If persistent_data is false and retro_load_game() is called:
+ * - retro_game_info::data and retro_game_info::size
+ * are valid only until retro_load_game() returns
+ * - retro_game_info_ext::data and retro_game_info_ext::size
+ * are valid only until retro_load_game() returns
+ *
+ * If persistent_data is true and retro_load_game() is called:
+ * - retro_game_info::data and retro_game_info::size
+ * are valid until retro_deinit() returns
+ * - retro_game_info_ext::data and retro_game_info_ext::size
+ * are valid until retro_deinit() returns */
+ bool persistent_data;
+};
+
+/* Similar to retro_game_info, but provides extended
+ * information about the source content file and
+ * game memory buffer status.
+ * And array of retro_game_info_ext is returned by
+ * RETRO_ENVIRONMENT_GET_GAME_INFO_EXT
+ * NOTE: In the following descriptions, references to
+ * retro_load_game() may be replaced with
+ * retro_load_game_special() */
+struct retro_game_info_ext
+{
+ /* - If file_in_archive is false, contains a valid
+ * path to an existent content file (UTF-8 encoded)
+ * - If file_in_archive is true, may be NULL */
+ const char *full_path;
+
+ /* - If file_in_archive is false, may be NULL
+ * - If file_in_archive is true, contains a valid path
+ * to an existent compressed file inside which the
+ * content file is located (UTF-8 encoded) */
+ const char *archive_path;
+
+ /* - If file_in_archive is false, may be NULL
+ * - If file_in_archive is true, contain a valid path
+ * to an existent content file inside the compressed
+ * file referred to by archive_path (UTF-8 encoded)
+ * e.g. for a compressed file '/path/to/foo.zip'
+ * containing 'bar.sfc'
+ * > archive_path will be '/path/to/foo.zip'
+ * > archive_file will be 'bar.sfc' */
+ const char *archive_file;
+
+ /* - If file_in_archive is false, contains a valid path
+ * to the directory in which the content file exists
+ * (UTF-8 encoded)
+ * - If file_in_archive is true, contains a valid path
+ * to the directory in which the compressed file
+ * (containing the content file) exists (UTF-8 encoded) */
+ const char *dir;
+
+ /* Contains the canonical name/ID of the content file
+ * (UTF-8 encoded). Intended for use when identifying
+ * 'complementary' content named after the loaded file -
+ * i.e. companion data of a different format (a CD image
+ * required by a ROM), texture packs, internally handled
+ * save files, etc.
+ * - If file_in_archive is false, contains the basename
+ * of the content file, without extension
+ * - If file_in_archive is true, then string is
+ * implementation specific. A frontend may choose to
+ * set a name value of:
+ * EITHER
+ * 1) the basename of the compressed file (containing
+ * the content file), without extension
+ * OR
+ * 2) the basename of the content file inside the
+ * compressed file, without extension
+ * RetroArch sets the 'name' value according to (1).
+ * A frontend that supports routine loading of
+ * content from archives containing multiple unrelated
+ * content files may set the 'name' value according
+ * to (2). */
+ const char *name;
+
+ /* - If file_in_archive is false, contains the extension
+ * of the content file in lower case format
+ * - If file_in_archive is true, contains the extension
+ * of the content file inside the compressed file,
+ * in lower case format */
+ const char *ext;
+
+ /* String of implementation specific meta-data. */
+ const char *meta;
+
+ /* Memory buffer of loaded game content. Will be NULL:
+ * IF
+ * - retro_system_info::need_fullpath is true and
+ * retro_system_content_info_override::need_fullpath
+ * is unset
+ * OR
+ * - retro_system_content_info_override::need_fullpath
+ * is true */
+ const void *data;
+
+ /* Size of game content memory buffer, in bytes */
+ size_t size;
+
+ /* True if loaded content file is inside a compressed
+ * archive */
+ bool file_in_archive;
+
+ /* - If data is NULL, value is unset/ignored
+ * - If data is non-NULL:
+ * - If persistent_data is false, data and size are
+ * valid only until retro_load_game() returns
+ * - If persistent_data is true, data and size are
+ * are valid until retro_deinit() returns */
+ bool persistent_data;
+};
+
+/**
+ * Parameters describing the size and shape of the video frame.
+ * @see retro_system_av_info
+ * @see RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO
+ * @see RETRO_ENVIRONMENT_SET_GEOMETRY
+ * @see retro_get_system_av_info
+ */
+struct retro_game_geometry
+{
+ /**
+ * Nominal video width of game, in pixels.
+ * This will typically be the emulated platform's native video width
+ * (or its smallest, if the original hardware supports multiple resolutions).
+ */
+ unsigned base_width;
+
+ /**
+ * Nominal video height of game, in pixels.
+ * This will typically be the emulated platform's native video height
+ * (or its smallest, if the original hardware supports multiple resolutions).
+ */
+ unsigned base_height;
+
+ /**
+ * Maximum possible width of the game screen, in pixels.
+ * This will typically be the emulated platform's maximum video width.
+ * For cores that emulate platforms with multiple screens (such as the Nintendo DS),
+ * this should assume the core's widest possible screen layout (e.g. side-by-side).
+ * For cores that support upscaling the resolution,
+ * this should assume the highest supported scale factor is active.
+ */
+ unsigned max_width;
+
+ /**
+ * Maximum possible height of the game screen, in pixels.
+ * This will typically be the emulated platform's maximum video height.
+ * For cores that emulate platforms with multiple screens (such as the Nintendo DS),
+ * this should assume the core's tallest possible screen layout (e.g. vertical).
+ * For cores that support upscaling the resolution,
+ * this should assume the highest supported scale factor is active.
+ */
+ unsigned max_height; /* Maximum possible height of game. */
+
+ /**
+ * Nominal aspect ratio of game.
+ * If zero or less,
+ * an aspect ratio of base_width / base_height is assumed.
+ *
+ * @note A frontend may ignore this setting.
+ */
+ float aspect_ratio;
+};
+
+/**
+ * Parameters describing the timing of the video and audio.
+ * @see retro_system_av_info
+ * @see RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO
+ * @see retro_get_system_av_info
+ */
+struct retro_system_timing
+{
+ /** Video output refresh rate, in frames per second. */
+ double fps;
+
+ /** The audio output sample rate, in Hz. */
+ double sample_rate;
+};
+
+/**
+ * Configures how the core's audio and video should be updated.
+ * @see RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO
+ * @see retro_get_system_av_info
+ */
+struct retro_system_av_info
+{
+ /** Parameters describing the size and shape of the video frame. */
+ struct retro_game_geometry geometry;
+
+ /** Parameters describing the timing of the video and audio. */
+ struct retro_system_timing timing;
+};
+
+/** @defgroup SET_CORE_OPTIONS Core Options
+ * @{
+ */
+
+/**
+ * Represents \ref RETRO_ENVIRONMENT_GET_VARIABLE "a core option query".
+ *
+ * @note In \ref RETRO_ENVIRONMENT_SET_VARIABLES
+ * (which is a deprecated API),
+ * this \c struct serves as an option definition.
+ *
+ * @see RETRO_ENVIRONMENT_GET_VARIABLE
+ */
+struct retro_variable
+{
+ /**
+ * A unique key identifying this option.
+ *
+ * Should be a key for an option that was previously defined
+ * with \ref RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2 or similar.
+ *
+ * Should be prefixed with the core's name
+ * to minimize the risk of collisions with another core's options,
+ * as frontends are not required to use a namespacing scheme for storing options.
+ * For example, a core named "foo" might define an option named "foo_option".
+ *
+ * @note In \ref RETRO_ENVIRONMENT_SET_VARIABLES
+ * (which is a deprecated API),
+ * this field is used to define an option
+ * named by this key.
+ */
+ const char *key;
+
+ /**
+ * Value to be obtained.
+ *
+ * Set by the frontend to \c NULL if
+ * the option named by \ref key does not exist.
+ *
+ * @note In \ref RETRO_ENVIRONMENT_SET_VARIABLES
+ * (which is a deprecated API),
+ * this field is set by the core to define the possible values
+ * for an option named by \ref key.
+ * When used this way, it must be formatted as follows:
+ * @li The text before the first ';' is the option's human-readable title.
+ * @li A single space follows the ';'.
+ * @li The rest of the string is a '|'-delimited list of possible values,
+ * with the first one being the default.
+ */
+ const char *value;
+};
+
+/**
+ * An argument that's used to show or hide a core option in the frontend.
+ *
+ * @see RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY
+ */
+struct retro_core_option_display
+{
+ /**
+ * The key for a core option that was defined with \ref RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2,
+ * \ref RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2_INTL,
+ * or their legacy equivalents.
+ */
+ const char *key;
+
+ /**
+ * Whether the option named by \c key
+ * should be displayed to the player in the frontend's core options menu.
+ *
+ * @note This value is a hint, \em not a requirement;
+ * the frontend is free to ignore this field.
+ */
+ bool visible;
+};
+
+/**
+ * The maximum number of choices that can be defined for a given core option.
+ *
+ * This limit was chosen as a compromise between
+ * a core's flexibility and a streamlined user experience.
+ *
+ * @note A guiding principle of libretro's API design is that
+ * all common interactions (gameplay, menu navigation, etc.)
+ * should be possible without a keyboard.
+ *
+ * If you need more than 128 choices for a core option,
+ * consider simplifying your option structure.
+ * Here are some ideas:
+ *
+ * \li If a core option represents a numeric value,
+ * consider reducing the option's granularity
+ * (e.g. define time limits in increments of 5 seconds instead of 1 second).
+ * Providing a fixed set of values based on experimentation
+ * is also a good idea.
+ * \li If a core option represents a dynamically-built list of files,
+ * consider leaving out files that won't be useful.
+ * For example, if a core allows the player to choose a specific BIOS file,
+ * it can omit files of the wrong length or without a valid header.
+ *
+ * @see retro_core_option_definition
+ * @see retro_core_option_v2_definition
+ */
+#define RETRO_NUM_CORE_OPTION_VALUES_MAX 128
+
+/**
+ * A descriptor for a particular choice within a core option.
+ *
+ * @note All option values are represented as strings.
+ * If you need to represent any other type,
+ * parse the string in \ref value.
+ *
+ * @see retro_core_option_v2_category
+ */
+struct retro_core_option_value
+{
+ /**
+ * The option value that the frontend will serialize.
+ *
+ * Must not be \c NULL or empty.
+ * No other hard limits are placed on this value's contents,
+ * but here are some suggestions:
+ *
+ * \li If the value represents a number,
+ * don't include any non-digit characters (units, separators, etc.).
+ * Instead, include that information in \c label.
+ * This will simplify parsing.
+ * \li If the value represents a file path,
+ * store it as a relative path with respect to one of the common libretro directories
+ * (e.g. \ref RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY "the system directory"
+ * or \ref RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY "the save directory"),
+ * and use forward slashes (\c "/") as directory separators.
+ * This will simplify cloud storage if supported by the frontend,
+ * as the same file may be used on multiple devices.
+ */
+ const char *value;
+
+ /**
+ * Human-readable name for \c value that the frontend should show to players.
+ *
+ * May be \c NULL, in which case the frontend
+ * should display \c value itself.
+ *
+ * Here are some guidelines for writing a good label:
+ *
+ * \li Make the option labels obvious
+ * so that they don't need to be explained in the description.
+ * \li Keep labels short, and don't use unnecessary words.
+ * For example, "OpenGL" is a better label than "OpenGL Mode".
+ * \li If the option represents a number,
+ * consider adding units, separators, or other punctuation
+ * into the label itself.
+ * For example, "5 seconds" is a better label than "5".
+ * \li If the option represents a number, use intuitive units
+ * that don't take a lot of digits to express.
+ * For example, prefer "1 minute" over "60 seconds" or "60,000 milliseconds".
+ */
+ const char *label;
+};
+
+/**
+ * @copybrief retro_core_option_v2_definition
+ *
+ * @deprecated Use \ref retro_core_option_v2_definition instead,
+ * as it supports categorizing options into groups.
+ * Only use this \c struct to support older frontends or cores.
+ *
+ * @see RETRO_ENVIRONMENT_SET_CORE_OPTIONS
+ * @see RETRO_ENVIRONMENT_SET_CORE_OPTIONS_INTL
+ */
+struct retro_core_option_definition
+{
+ /** @copydoc retro_core_option_v2_definition::key */
+ const char *key;
+
+ /** @copydoc retro_core_option_v2_definition::desc */
+ const char *desc;
+
+ /** @copydoc retro_core_option_v2_definition::info */
+ const char *info;
+
+ /** @copydoc retro_core_option_v2_definition::values */
+ struct retro_core_option_value values[RETRO_NUM_CORE_OPTION_VALUES_MAX];
+
+ /** @copydoc retro_core_option_v2_definition::default_value */
+ const char *default_value;
+};
+
+#ifdef __PS3__
+#undef local
+#endif
+
+/**
+ * A variant of \ref retro_core_options that supports internationalization.
+ *
+ * @deprecated Use \ref retro_core_options_v2_intl instead,
+ * as it supports categorizing options into groups.
+ * Only use this \c struct to support older frontends or cores.
+ *
+ * @see retro_core_options
+ * @see RETRO_ENVIRONMENT_SET_CORE_OPTIONS_INTL
+ * @see RETRO_ENVIRONMENT_GET_LANGUAGE
+ * @see retro_language
+ */
+struct retro_core_options_intl
+{
+ /** @copydoc retro_core_options_v2_intl::us */
+ struct retro_core_option_definition *us;
+
+ /** @copydoc retro_core_options_v2_intl::local */
+ struct retro_core_option_definition *local;
+};
+
+/**
+ * A descriptor for a group of related core options.
+ *
+ * Here's an example category:
+ *
+ * @code
+ * {
+ * "cpu",
+ * "CPU Emulation",
+ * "Settings for CPU quirks."
+ * }
+ * @endcode
+ *
+ * @see retro_core_options_v2
+ * @see RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2
+ * @see RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2_INTL
+ */
+struct retro_core_option_v2_category
+{
+ /**
+ * A string that uniquely identifies this category within the core's options.
+ * Any \c retro_core_option_v2_definition whose \c category_key matches this
+ * is considered to be within this category.
+ * Different cores may use the same category keys,
+ * so namespacing them is not necessary.
+ * Valid characters are [a-zA-Z0-9_-].
+ *
+ * Frontends should use this category to organize core options,
+ * but may customize this category's presentation in other ways.
+ * For example, a frontend may use common keys like "audio" or "gfx"
+ * to select an appropriate icon in its UI.
+ *
+ * Required; must not be \c NULL.
+ */
+ const char *key;
+
+ /**
+ * A brief human-readable name for this category,
+ * intended for the frontend to display to the player.
+ * This should be a name that's concise and descriptive, such as "Audio" or "Video".
+ *
+ * Required; must not be \c NULL.
+ */
+ const char *desc;
+
+ /**
+ * A human-readable description for this category,
+ * intended for the frontend to display to the player
+ * as secondary help text (e.g. a sublabel or a tooltip).
+ * Optional; may be \c NULL or an empty string.
+ */
+ const char *info;
+};
+
+/**
+ * A descriptor for a particular core option and the values it may take.
+ *
+ * Supports categorizing options into groups,
+ * so as not to overwhelm the player.
+ *
+ * @see retro_core_option_v2_category
+ * @see RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2
+ * @see RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2_INTL
+ */
+struct retro_core_option_v2_definition
+{
+ /**
+ * A unique identifier for this option that cores may use
+ * \ref RETRO_ENVIRONMENT_GET_VARIABLE "to query its value from the frontend".
+ * Must be unique within this core.
+ *
+ * Should be unique globally;
+ * the recommended method for doing so
+ * is to prefix each option with the core's name.
+ * For example, an option that controls the resolution for a core named "foo"
+ * should be named \c "foo_resolution".
+ *
+ * Valid key characters are in the set [a-zA-Z0-9_-].
+ */
+ const char *key;
+
+ /**
+ * A human-readable name for this option,
+ * intended to be displayed by frontends that don't support
+ * categorizing core options.
+ *
+ * Required; must not be \c NULL or empty.
+ */
+ const char *desc;
+
+ /**
+ * A human-readable name for this option,
+ * intended to be displayed by frontends that support
+ * categorizing core options.
+ *
+ * This version may be slightly more concise than \ref desc,
+ * as it can rely on the structure of the options menu.
+ * For example, "Interface" is a good \c desc_categorized,
+ * as it can be displayed as a sublabel for a "Network" category.
+ * For \c desc, "Network Interface" would be more suitable.
+ *
+ * Optional; if this field or \c category_key is empty or \c NULL,
+ * \c desc will be used instead.
+ */
+ const char *desc_categorized;
+
+ /**
+ * A human-readable description of this option and its effects,
+ * intended to be displayed by frontends that don't support
+ * categorizing core options.
+ *
+ * @details Intended to be displayed as secondary help text,
+ * such as a tooltip or a sublabel.
+ *
+ * Here are some suggestions for writing a good description:
+ *
+ * \li Avoid technical jargon unless this option is meant for advanced users.
+ * If unavoidable, suggest one of the default options for those unsure.
+ * \li Don't repeat the option name in the description;
+ * instead, describe what the option name means.
+ * \li If an option requires a core restart or game reset to take effect,
+ * be sure to say so.
+ * \li Try to make the option labels obvious
+ * so that they don't need to be explained in the description.
+ *
+ * Optional; may be \c NULL.
+ */
+ const char *info;
+
+ /**
+ * @brief A human-readable description of this option and its effects,
+ * intended to be displayed by frontends that support
+ * categorizing core options.
+ *
+ * This version is provided to accommodate descriptions
+ * that reference other options by name,
+ * as options may have different user-facing names
+ * depending on whether the frontend supports categorization.
+ *
+ * @copydetails info
+ *
+ * If empty or \c NULL, \c info will be used instead.
+ * Will be ignored if \c category_key is empty or \c NULL.
+ */
+ const char *info_categorized;
+
+ /**
+ * The key of the category that this option belongs to.
+ *
+ * Optional; if equal to \ref retro_core_option_v2_category::key "a defined category",
+ * then this option shall be displayed by the frontend
+ * next to other options in this same category,
+ * assuming it supports doing so.
+ * Option categories are intended to be displayed in a submenu,
+ * but this isn't a hard requirement.
+ *
+ * If \c NULL, empty, or not equal to a defined category,
+ * then this option is considered uncategorized
+ * and the frontend shall display it outside of any category
+ * (most likely at a top-level menu).
+ *
+ * @see retro_core_option_v2_category
+ */
+ const char *category_key;
+
+ /**
+ * One or more possible values for this option,
+ * up to the limit of \ref RETRO_NUM_CORE_OPTION_VALUES_MAX.
+ *
+ * Terminated by a \c { NULL, NULL } element,
+ * although frontends should work even if all elements are used.
+ */
+ struct retro_core_option_value values[RETRO_NUM_CORE_OPTION_VALUES_MAX];
+
+ /**
+ * The default value for this core option.
+ * Used if it hasn't been set, e.g. for new cores.
+ * Must equal one of the \ref value members in the \c values array,
+ * or else this option will be ignored.
+ */
+ const char *default_value;
+};
+
+/**
+ * A set of core option descriptors and the categories that group them,
+ * suitable for enabling a core to be customized.
+ *
+ * @see RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2
+ */
+struct retro_core_options_v2
+{
+ /**
+ * An array of \ref retro_core_option_v2_category "option categories",
+ * terminated by a zeroed-out category \c struct.
+ *
+ * Will be ignored if the frontend doesn't support core option categories.
+ *
+ * If \c NULL or ignored, all options will be treated as uncategorized.
+ * This most likely means that a frontend will display them at a top-level menu
+ * without any kind of hierarchy or grouping.
+ */
+ struct retro_core_option_v2_category *categories;
+
+ /**
+ * An array of \ref retro_core_option_v2_definition "core option descriptors",
+ * terminated by a zeroed-out definition \c struct.
+ *
+ * Required; must not be \c NULL.
+ */
+ struct retro_core_option_v2_definition *definitions;
+};
+
+/**
+ * A variant of \ref retro_core_options_v2 that supports internationalization.
+ *
+ * @see retro_core_options_v2
+ * @see RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2_INTL
+ * @see RETRO_ENVIRONMENT_GET_LANGUAGE
+ * @see retro_language
+ */
+struct retro_core_options_v2_intl
+{
+ /**
+ * Pointer to a core options set
+ * whose text is written in American English.
+ *
+ * This may be passed to \c RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2 as-is
+ * if not using \c RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2_INTL.
+ *
+ * Required; must not be \c NULL.
+ */
+ struct retro_core_options_v2 *us;
+
+ /**
+ * Pointer to a core options set
+ * whose text is written in one of libretro's \ref retro_language "supported languages",
+ * most likely the one returned by \ref RETRO_ENVIRONMENT_GET_LANGUAGE.
+ *
+ * Structure is the same, but usage is slightly different:
+ *
+ * \li All text (except for keys and option values)
+ * should be written in whichever language
+ * is returned by \c RETRO_ENVIRONMENT_GET_LANGUAGE.
+ * \li All fields besides keys and option values may be \c NULL,
+ * in which case the corresponding string in \c us
+ * is used instead.
+ * \li All \ref retro_core_option_v2_definition::default_value "default option values"
+ * are taken from \c us.
+ * The defaults in this field are ignored.
+ *
+ * May be \c NULL, in which case \c us is used instead.
+ */
+ struct retro_core_options_v2 *local;
+};
+
+/**
+ * Called by the frontend to determine if any core option's visibility has changed.
+ *
+ * Each time a frontend sets a core option,
+ * it should call this function to see if
+ * any core option should be made visible or invisible.
+ *
+ * May also be called after \ref retro_load_game "loading a game",
+ * to determine what the initial visibility of each option should be.
+ *
+ * Within this function, the core must update the visibility
+ * of any dynamically-hidden options
+ * using \ref RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY.
+ *
+ * @note All core options are visible by default,
+ * even during this function's first call.
+ *
+ * @return \c true if any core option's visibility was adjusted
+ * since the last call to this function.
+ * @see RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY
+ * @see retro_core_option_display
+ */
+typedef bool (RETRO_CALLCONV *retro_core_options_update_display_callback_t)(void);
+
+/**
+ * Callback registered by the core for the frontend to use
+ * when setting the visibility of each core option.
+ *
+ * @see RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY
+ * @see retro_core_option_display
+ */
+struct retro_core_options_update_display_callback
+{
+ /**
+ * @copydoc retro_core_options_update_display_callback_t
+ *
+ * Set by the core.
+ */
+ retro_core_options_update_display_callback_t callback;
+};
+
+/** @} */
+
+struct retro_game_info
+{
+ const char *path; /* Path to game, UTF-8 encoded.
+ * Sometimes used as a reference for building other paths.
+ * May be NULL if game was loaded from stdin or similar,
+ * but in this case some cores will be unable to load `data`.
+ * So, it is preferable to fabricate something here instead
+ * of passing NULL, which will help more cores to succeed.
+ * retro_system_info::need_fullpath requires
+ * that this path is valid. */
+ const void *data; /* Memory buffer of loaded game. Will be NULL
+ * if need_fullpath was set. */
+ size_t size; /* Size of memory buffer. */
+ const char *meta; /* String of implementation specific meta-data. */
+};
+
+/** @defgroup GET_CURRENT_SOFTWARE_FRAMEBUFFER Frontend-Owned Framebuffers
+ * @{
+ */
+
+/** @defgroup RETRO_MEMORY_ACCESS Framebuffer Memory Access Types
+ * @{
+ */
+
+/** Indicates that core will write to the framebuffer returned by the frontend. */
+#define RETRO_MEMORY_ACCESS_WRITE (1 << 0)
+
+/** Indicates that the core will read from the framebuffer returned by the frontend. */
+#define RETRO_MEMORY_ACCESS_READ (1 << 1)
+
+/** @} */
+
+/** @defgroup RETRO_MEMORY_TYPE Framebuffer Memory Types
+ * @{
+ */
+
+/**
+ * Indicates that the returned framebuffer's memory is cached.
+ * If not set, random access to the buffer may be very slow.
+ */
+#define RETRO_MEMORY_TYPE_CACHED (1 << 0)
+
+/** @} */
+
+/**
+ * A frame buffer owned by the frontend that a core may use for rendering.
+ *
+ * @see GET_CURRENT_SOFTWARE_FRAMEBUFFER
+ * @see retro_video_refresh_t
+ */
+struct retro_framebuffer
+{
+ /**
+ * Pointer to the beginning of the framebuffer provided by the frontend.
+ * The initial contents of this buffer are unspecified,
+ * as is the means used to map the memory;
+ * this may be defined in software,
+ * or it may be GPU memory mapped to RAM.
+ *
+ * If the framebuffer is used,
+ * this pointer must be passed to \c retro_video_refresh_t as-is.
+ * It is undefined behavior to pass an offset to this pointer.
+ *
+ * @warning This pointer is only guaranteed to be valid
+ * for the duration of the same \c retro_run iteration
+ * \ref GET_CURRENT_SOFTWARE_FRAMEBUFFER "that requested the framebuffer".
+ * Reuse of this pointer is undefined.
+ */
+ void *data;
+
+ /**
+ * The width of the framebuffer given in \c data, in pixels.
+ * Set by the core.
+ *
+ * @warning If the framebuffer is used,
+ * this value must be passed to \c retro_video_refresh_t as-is.
+ * It is undefined behavior to try to render \c data with any other width.
+ */
+ unsigned width;
+
+ /**
+ * The height of the framebuffer given in \c data, in pixels.
+ * Set by the core.
+ *
+ * @warning If the framebuffer is used,
+ * this value must be passed to \c retro_video_refresh_t as-is.
+ * It is undefined behavior to try to render \c data with any other height.
+ */
+ unsigned height;
+
+ /**
+ * The distance between the start of one scanline and the beginning of the next, in bytes.
+ * In practice this is usually equal to \c width times the pixel size,
+ * but that's not guaranteed.
+ * Sometimes called the "stride".
+ *
+ * @setby{frontend}
+ * @warning If the framebuffer is used,
+ * this value must be passed to \c retro_video_refresh_t as-is.
+ * It is undefined to try to render \c data with any other pitch.
+ */
+ size_t pitch;
+
+ /**
+ * The pixel format of the returned framebuffer.
+ * May be different than the format specified by the core in \c RETRO_ENVIRONMENT_SET_PIXEL_FORMAT,
+ * e.g. due to conversions.
+ * Set by the frontend.
+ *
+ * @see RETRO_ENVIRONMENT_SET_PIXEL_FORMAT
+ */
+ enum retro_pixel_format format;
+
+ /**
+ * One or more \ref RETRO_MEMORY_ACCESS "memory access flags"
+ * that specify how the core will access the memory in \c data.
+ *
+ * @setby{core}
+ */
+ unsigned access_flags;
+
+ /**
+ * Zero or more \ref RETRO_MEMORY_TYPE "memory type flags"
+ * that describe how the framebuffer's memory has been mapped.
+ *
+ * @setby{frontend}
+ */
+ unsigned memory_flags;
+};
+
+/** @} */
+
+/** @defgroup SET_FASTFORWARDING_OVERRIDE Fast-Forward Override
+ * @{
+ */
+
+/**
+ * Parameters that govern when and how the core takes control
+ * of fast-forwarding mode.
+ */
+struct retro_fastforwarding_override
+{
+ /**
+ * The factor by which the core will be sped up
+ * when \c fastforward is \c true.
+ * This value is used as follows:
+ *
+ * @li A value greater than 1.0 will run the core at
+ * the specified multiple of normal speed.
+ * For example, a value of 5.0
+ * combined with a normal target rate of 60 FPS
+ * will result in a target rate of 300 FPS.
+ * The actual rate may be lower if the host's hardware can't keep up.
+ * @li A value of 1.0 will run the core at normal speed.
+ * @li A value between 0.0 (inclusive) and 1.0 (exclusive)
+ * will run the core as fast as the host system can manage.
+ * @li A negative value will let the frontend choose a factor.
+ * @li An infinite value or \c NaN results in undefined behavior.
+ *
+ * @attention Setting this value to less than 1.0 will \em not
+ * slow down the core.
+ */
+ float ratio;
+
+ /**
+ * If \c true, the frontend should activate fast-forwarding
+ * until this field is set to \c false or the core is unloaded.
+ */
+ bool fastforward;
+
+ /**
+ * If \c true, the frontend should display an on-screen notification or icon
+ * while \c fastforward is \c true (where supported).
+ * Otherwise, the frontend should not display any such notification.
+ */
+ bool notification;
+
+ /**
+ * If \c true, the core has exclusive control
+ * over enabling and disabling fast-forwarding
+ * via the \c fastforward field.
+ * The frontend will not be able to start or stop fast-forwarding
+ * until this field is set to \c false or the core is unloaded.
+ */
+ bool inhibit_toggle;
+};
+
+/** @} */
+
+/**
+ * During normal operation.
+ *
+ * @note Rate will be equal to the core's internal FPS.
+ */
+#define RETRO_THROTTLE_NONE 0
+
+/**
+ * While paused or stepping single frames.
+ *
+ * @note Rate will be 0.
+ */
+#define RETRO_THROTTLE_FRAME_STEPPING 1
+
+/**
+ * During fast forwarding.
+ *
+ * @note Rate will be 0 if not specifically limited to a maximum speed.
+ */
+#define RETRO_THROTTLE_FAST_FORWARD 2
+
+/**
+ * During slow motion.
+ *
+ * @note Rate will be less than the core's internal FPS.
+ */
+#define RETRO_THROTTLE_SLOW_MOTION 3
+
+/**
+ * While rewinding recorded save states.
+ *
+ * @note Rate can vary depending on the rewind speed or be 0 if the frontend
+ * is not aiming for a specific rate.
+ */
+#define RETRO_THROTTLE_REWINDING 4
+
+/**
+ * While vsync is active in the video driver, and the target refresh rate is lower than the core's internal FPS.
+ *
+ * @note Rate is the target refresh rate.
+ */
+#define RETRO_THROTTLE_VSYNC 5
+
+/**
+ * When the frontend does not throttle in any way.
+ *
+ * @note Rate will be 0. An example could be if no vsync or audio output is active.
+ */
+#define RETRO_THROTTLE_UNBLOCKED 6
+
+/**
+ * Details about the actual rate an implementation is calling \c retro_run() at.
+ *
+ * @see RETRO_ENVIRONMENT_GET_THROTTLE_STATE
+ */
+struct retro_throttle_state
+{
+ /**
+ * The current throttling mode.
+ *
+ * @note Should be one of the \c RETRO_THROTTLE_* values.
+ * @see RETRO_THROTTLE_NONE
+ * @see RETRO_THROTTLE_FRAME_STEPPING
+ * @see RETRO_THROTTLE_FAST_FORWARD
+ * @see RETRO_THROTTLE_SLOW_MOTION
+ * @see RETRO_THROTTLE_REWINDING
+ * @see RETRO_THROTTLE_VSYNC
+ * @see RETRO_THROTTLE_UNBLOCKED
+ */
+ unsigned mode;
+
+ /**
+ * How many times per second the frontend aims to call retro_run.
+ *
+ * @note Depending on the mode, it can be 0 if there is no known fixed rate.
+ * This won't be accurate if the total processing time of the core and
+ * the frontend is longer than what is available for one frame.
+ */
+ float rate;
+};
+
+/** @defgroup GET_MICROPHONE_INTERFACE Microphone Interface
+ * @{
+ */
+
+/**
+ * Opaque handle to a microphone that's been opened for use.
+ * The underlying object is accessed or created with \c retro_microphone_interface_t.
+ */
+typedef struct retro_microphone retro_microphone_t;
+
+/**
+ * Parameters for configuring a microphone.
+ * Some of these might not be honored,
+ * depending on the available hardware and driver configuration.
+ */
+typedef struct retro_microphone_params
+{
+ /**
+ * The desired sample rate of the microphone's input, in Hz.
+ * The microphone's input will be resampled,
+ * so cores can ask for whichever frequency they need.
+ *
+ * If zero, some reasonable default will be provided by the frontend
+ * (usually from its config file).
+ *
+ * @see retro_get_mic_rate_t
+ */
+ unsigned rate;
+} retro_microphone_params_t;
+
+/**
+ * @copydoc retro_microphone_interface::open_mic
+ */
+typedef retro_microphone_t *(RETRO_CALLCONV *retro_open_mic_t)(const retro_microphone_params_t *params);
+
+/**
+ * @copydoc retro_microphone_interface::close_mic
+ */
+typedef void (RETRO_CALLCONV *retro_close_mic_t)(retro_microphone_t *microphone);
+
+/**
+ * @copydoc retro_microphone_interface::get_params
+ */
+typedef bool (RETRO_CALLCONV *retro_get_mic_params_t)(const retro_microphone_t *microphone, retro_microphone_params_t *params);
+
+/**
+ * @copydoc retro_microphone_interface::set_mic_state
+ */
+typedef bool (RETRO_CALLCONV *retro_set_mic_state_t)(retro_microphone_t *microphone, bool state);
+
+/**
+ * @copydoc retro_microphone_interface::get_mic_state
+ */
+typedef bool (RETRO_CALLCONV *retro_get_mic_state_t)(const retro_microphone_t *microphone);
+
+/**
+ * @copydoc retro_microphone_interface::read_mic
+ */
+typedef int (RETRO_CALLCONV *retro_read_mic_t)(retro_microphone_t *microphone, int16_t* samples, size_t num_samples);
+
+/**
+ * The current version of the microphone interface.
+ * Will be incremented whenever \c retro_microphone_interface or \c retro_microphone_params_t
+ * receive new fields.
+ *
+ * Frontends using cores built against older mic interface versions
+ * should not access fields introduced in newer versions.
+ */
+#define RETRO_MICROPHONE_INTERFACE_VERSION 1
+
+/**
+ * An interface for querying the microphone and accessing data read from it.
+ *
+ * @see RETRO_ENVIRONMENT_GET_MICROPHONE_INTERFACE
+ */
+struct retro_microphone_interface
+{
+ /**
+ * The version of this microphone interface.
+ * Set by the core to request a particular version,
+ * and set by the frontend to indicate the returned version.
+ * 0 indicates that the interface is invalid or uninitialized.
+ */
+ unsigned interface_version;
+
+ /**
+ * Initializes a new microphone.
+ * Assuming that microphone support is enabled and provided by the frontend,
+ * cores may call this function whenever necessary.
+ * A microphone could be opened throughout a core's lifetime,
+ * or it could wait until a microphone is plugged in to the emulated device.
+ *
+ * The returned handle will be valid until it's freed,
+ * even if the audio driver is reinitialized.
+ *
+ * This function is not guaranteed to be thread-safe.
+ *
+ * @param[in] args Parameters used to create the microphone.
+ * May be \c NULL, in which case the default value of each parameter will be used.
+ *
+ * @returns Pointer to the newly-opened microphone,
+ * or \c NULL if one couldn't be opened.
+ * This likely means that no microphone is plugged in and recognized,
+ * or the maximum number of supported microphones has been reached.
+ *
+ * @note Microphones are \em inactive by default;
+ * to begin capturing audio, call \c set_mic_state.
+ * @see retro_microphone_params_t
+ */
+ retro_open_mic_t open_mic;
+
+ /**
+ * Closes a microphone that was initialized with \c open_mic.
+ * Calling this function will stop all microphone activity
+ * and free up the resources that it allocated.
+ * Afterwards, the handle is invalid and must not be used.
+ *
+ * A frontend may close opened microphones when unloading content,
+ * but this behavior is not guaranteed.
+ * Cores should close their microphones when exiting, just to be safe.
+ *
+ * @param microphone Pointer to the microphone that was allocated by \c open_mic.
+ * If \c NULL, this function does nothing.
+ *
+ * @note The handle might be reused if another microphone is opened later.
+ */
+ retro_close_mic_t close_mic;
+
+ /**
+ * Returns the configured parameters of this microphone.
+ * These may differ from what was requested depending on
+ * the driver and device configuration.
+ *
+ * Cores should check these values before they start fetching samples.
+ *
+ * Will not change after the mic was opened.
+ *
+ * @param[in] microphone Opaque handle to the microphone
+ * whose parameters will be retrieved.
+ * @param[out] params The parameters object that the
+ * microphone's parameters will be copied to.
+ *
+ * @return \c true if the parameters were retrieved,
+ * \c false if there was an error.
+ */
+ retro_get_mic_params_t get_params;
+
+ /**
+ * Enables or disables the given microphone.
+ * Microphones are disabled by default
+ * and must be explicitly enabled before they can be used.
+ * Disabled microphones will not process incoming audio samples,
+ * and will therefore have minimal impact on overall performance.
+ * Cores may enable microphones throughout their lifetime,
+ * or only for periods where they're needed.
+ *
+ * Cores that accept microphone input should be able to operate without it;
+ * we suggest substituting silence in this case.
+ *
+ * @param microphone Opaque handle to the microphone
+ * whose state will be adjusted.
+ * This will have been provided by \c open_mic.
+ * @param state \c true if the microphone should receive audio input,
+ * \c false if it should be idle.
+ * @returns \c true if the microphone's state was successfully set,
+ * \c false if \c microphone is invalid
+ * or if there was an error.
+ */
+ retro_set_mic_state_t set_mic_state;
+
+ /**
+ * Queries the active state of a microphone at the given index.
+ * Will return whether the microphone is enabled,
+ * even if the driver is paused.
+ *
+ * @param microphone Opaque handle to the microphone
+ * whose state will be queried.
+ * @return \c true if the provided \c microphone is valid and active,
+ * \c false if not or if there was an error.
+ */
+ retro_get_mic_state_t get_mic_state;
+
+ /**
+ * Retrieves the input processed by the microphone since the last call.
+ * \em Must be called every frame unless \c microphone is disabled,
+ * similar to how \c retro_audio_sample_batch_t works.
+ *
+ * @param[in] microphone Opaque handle to the microphone
+ * whose recent input will be retrieved.
+ * @param[out] samples The buffer that will be used to store the microphone's data.
+ * Microphone input is in mono (i.e. one number per sample).
+ * Should be large enough to accommodate the expected number of samples per frame;
+ * for example, a 44.1kHz sample rate at 60 FPS would require space for 735 samples.
+ * @param[in] num_samples The size of the data buffer in samples (\em not bytes).
+ * Microphone input is in mono, so a "frame" and a "sample" are equivalent in length here.
+ *
+ * @return The number of samples that were copied into \c samples.
+ * If \c microphone is pending driver initialization,
+ * this function will copy silence of the requested length into \c samples.
+ *
+ * Will return -1 if the microphone is disabled,
+ * the audio driver is paused,
+ * or there was an error.
+ */
+ retro_read_mic_t read_mic;
+};
+
+/** @} */
+
+/** @defgroup GET_DEVICE_POWER Device Power
+ * @{
+ */
+
+/**
+ * Describes how a device is being powered.
+ * @see RETRO_ENVIRONMENT_GET_DEVICE_POWER
+ */
+enum retro_power_state
+{
+ /**
+ * Indicates that the frontend cannot report its power state at this time,
+ * most likely due to a lack of support.
+ *
+ * \c RETRO_ENVIRONMENT_GET_DEVICE_POWER will not return this value;
+ * instead, the environment callback will return \c false.
+ */
+ RETRO_POWERSTATE_UNKNOWN = 0,
+
+ /**
+ * Indicates that the device is running on its battery.
+ * Usually applies to portable devices such as handhelds, laptops, and smartphones.
+ */
+ RETRO_POWERSTATE_DISCHARGING,
+
+ /**
+ * Indicates that the device's battery is currently charging.
+ */
+ RETRO_POWERSTATE_CHARGING,
+
+ /**
+ * Indicates that the device is connected to a power source
+ * and that its battery has finished charging.
+ */
+ RETRO_POWERSTATE_CHARGED,
+
+ /**
+ * Indicates that the device is connected to a power source
+ * and that it does not have a battery.
+ * This usually suggests a desktop computer or a non-portable game console.
+ */
+ RETRO_POWERSTATE_PLUGGED_IN
+};
+
+/**
+ * Indicates that an estimate is not available for the battery level or time remaining,
+ * even if the actual power state is known.
+ */
+#define RETRO_POWERSTATE_NO_ESTIMATE (-1)
+
+/**
+ * Describes the power state of the device running the frontend.
+ * @see RETRO_ENVIRONMENT_GET_DEVICE_POWER
+ */
+struct retro_device_power
+{
+ /**
+ * The current state of the frontend's power usage.
+ */
+ enum retro_power_state state;
+
+ /**
+ * A rough estimate of the amount of time remaining (in seconds)
+ * before the device powers off.
+ * This value depends on a variety of factors,
+ * so it is not guaranteed to be accurate.
+ *
+ * Will be set to \c RETRO_POWERSTATE_NO_ESTIMATE if \c state does not equal \c RETRO_POWERSTATE_DISCHARGING.
+ * May still be set to \c RETRO_POWERSTATE_NO_ESTIMATE if the frontend is unable to provide an estimate.
+ */
+ int seconds;
+
+ /**
+ * The approximate percentage of battery charge,
+ * ranging from 0 to 100 (inclusive).
+ * The device may power off before this reaches 0.
+ *
+ * The user might have configured their device
+ * to stop charging before the battery is full,
+ * so do not assume that this will be 100 in the \c RETRO_POWERSTATE_CHARGED state.
+ */
+ int8_t percent;
+};
+
+/** @} */
+
+/**
+ * @defgroup Callbacks
+ * @{
+ */
+
+/**
+ * Environment callback to give implementations a way of performing uncommon tasks.
+ *
+ * @note Extensible.
+ *
+ * @param cmd The command to run.
+ * @param data A pointer to the data associated with the command.
+ *
+ * @return Varies by callback,
+ * but will always return \c false if the command is not recognized.
+ *
+ * @see RETRO_ENVIRONMENT_SET_ROTATION
+ * @see retro_set_environment()
+ */
+typedef bool (RETRO_CALLCONV *retro_environment_t)(unsigned cmd, void *data);
+
+/**
+ * Render a frame.
+ *
+ * @note For performance reasons, it is highly recommended to have a frame
+ * that is packed in memory, i.e. pitch == width * byte_per_pixel.
+ * Certain graphic APIs, such as OpenGL ES, do not like textures
+ * that are not packed in memory.
+ *
+ * @param data A pointer to the frame buffer data with a pixel format of 15-bit \c 0RGB1555 native endian, unless changed with \c RETRO_ENVIRONMENT_SET_PIXEL_FORMAT.
+ * @param width The width of the frame buffer, in pixels.
+ * @param height The height frame buffer, in pixels.
+ * @param pitch The width of the frame buffer, in bytes.
+ *
+ * @see retro_set_video_refresh()
+ * @see RETRO_ENVIRONMENT_SET_PIXEL_FORMAT
+ * @see retro_pixel_format
+ */
+typedef void (RETRO_CALLCONV *retro_video_refresh_t)(const void *data, unsigned width,
+ unsigned height, size_t pitch);
+
+/**
+ * Renders a single audio frame. Should only be used if implementation generates a single sample at a time.
+ *
+ * @param left The left audio sample represented as a signed 16-bit native endian.
+ * @param right The right audio sample represented as a signed 16-bit native endian.
+ *
+ * @see retro_set_audio_sample()
+ * @see retro_set_audio_sample_batch()
+ */
+typedef void (RETRO_CALLCONV *retro_audio_sample_t)(int16_t left, int16_t right);
+
+/**
+ * Renders multiple audio frames in one go.
+ *
+ * @note Only one of the audio callbacks must ever be used.
+ *
+ * @param data A pointer to the audio sample data pairs to render.
+ * @param frames The number of frames that are represented in the data. One frame
+ * is defined as a sample of left and right channels, interleaved.
+ * For example: int16_t buf[4] = { l, r, l, r }; would be 2 frames.
+ *
+ * @return The number of frames that were processed.
+ *
+ * @see retro_set_audio_sample_batch()
+ * @see retro_set_audio_sample()
+ */
+typedef size_t (RETRO_CALLCONV *retro_audio_sample_batch_t)(const int16_t *data,
+ size_t frames);
+
+/**
+ * Polls input.
+ *
+ * @see retro_set_input_poll()
+ */
+typedef void (RETRO_CALLCONV *retro_input_poll_t)(void);
+
+/**
+ * Queries for input for player 'port'.
+ *
+ * @param port Which player 'port' to query.
+ * @param device Which device to query for. Will be masked with \c RETRO_DEVICE_MASK.
+ * @param index The input index to retrieve.
+ * The exact semantics depend on the device type given in \c device.
+ * @param id The ID of which value to query, like \c RETRO_DEVICE_ID_JOYPAD_B.
+ * @returns Depends on the provided arguments,
+ * but will return 0 if their values are unsupported
+ * by the frontend or the backing physical device.
+ * @note Specialization of devices such as \c RETRO_DEVICE_JOYPAD_MULTITAP that
+ * have been set with \c retro_set_controller_port_device() will still use the
+ * higher level \c RETRO_DEVICE_JOYPAD to request input.
+ *
+ * @see retro_set_input_state()
+ * @see RETRO_DEVICE_NONE
+ * @see RETRO_DEVICE_JOYPAD
+ * @see RETRO_DEVICE_MOUSE
+ * @see RETRO_DEVICE_KEYBOARD
+ * @see RETRO_DEVICE_LIGHTGUN
+ * @see RETRO_DEVICE_ANALOG
+ * @see RETRO_DEVICE_POINTER
+ */
+typedef int16_t (RETRO_CALLCONV *retro_input_state_t)(unsigned port, unsigned device,
+ unsigned index, unsigned id);
+
+/**
+ * Sets the environment callback.
+ *
+ * @param cb The function which is used when making environment calls.
+ *
+ * @note Guaranteed to be called before \c retro_init().
+ *
+ * @see RETRO_ENVIRONMENT
+ */
+RETRO_API void retro_set_environment(retro_environment_t cb);
+
+/**
+ * Sets the video refresh callback.
+ *
+ * @param cb The function which is used when rendering a frame.
+ *
+ * @note Guaranteed to have been called before the first call to \c retro_run() is made.
+ */
+RETRO_API void retro_set_video_refresh(retro_video_refresh_t cb);
+
+/**
+ * Sets the audio sample callback.
+ *
+ * @param cb The function which is used when rendering a single audio frame.
+ *
+ * @note Guaranteed to have been called before the first call to \c retro_run() is made.
+ */
+RETRO_API void retro_set_audio_sample(retro_audio_sample_t cb);
+
+/**
+ * Sets the audio sample batch callback.
+ *
+ * @param cb The function which is used when rendering multiple audio frames in one go.
+ *
+ * @note Guaranteed to have been called before the first call to \c retro_run() is made.
+ */
+RETRO_API void retro_set_audio_sample_batch(retro_audio_sample_batch_t cb);
+
+/**
+ * Sets the input poll callback.
+ *
+ * @param cb The function which is used to poll the active input.
+ *
+ * @note Guaranteed to have been called before the first call to \c retro_run() is made.
+ */
+RETRO_API void retro_set_input_poll(retro_input_poll_t cb);
+
+/**
+ * Sets the input state callback.
+ *
+ * @param cb The function which is used to query the input state.
+ *
+ *@note Guaranteed to have been called before the first call to \c retro_run() is made.
+ */
+RETRO_API void retro_set_input_state(retro_input_state_t cb);
+
+/**
+ * @}
+ */
+
+/**
+ * Called by the frontend when initializing a libretro core.
+ *
+ * @warning There are many possible "gotchas" with global state in dynamic libraries.
+ * Here are some to keep in mind:
+ *
+ *
Do not assume that the core was loaded by the operating system
+ * for the first time within this call.
+ * It may have been statically linked or retained from a previous session.
+ * Consequently, cores must not rely on global variables being initialized
+ * to their default values before this function is called;
+ * this also goes for object constructors in C++.
+ *
Although C++ requires that constructors be called for global variables,
+ * it does not require that their destructors be called
+ * if stored within a dynamic library's global scope.
+ *
If the core is statically linked to the frontend,
+ * global variables may be initialized when the frontend itself is initially executed.
+ *
+ * @see retro_deinit
+ */
+RETRO_API void retro_init(void);
+
+/**
+ * Called by the frontend when deinitializing a libretro core.
+ * The core must release all of its allocated resources before this function returns.
+ *
+ * @warning There are many possible "gotchas" with global state in dynamic libraries.
+ * Here are some to keep in mind:
+ *
+ *
Do not assume that the operating system will unload the core after this function returns,
+ * as the core may be linked statically or retained in memory.
+ * Cores should use this function to clean up all allocated resources
+ * and reset all global variables to their default states.
+ *
Do not assume that this core won't be loaded again after this function returns.
+ * It may be kept in memory by the frontend for later use,
+ * or it may be statically linked.
+ * Therefore, all global variables should be reset to their default states within this function.
+ *
C++ does not require that destructors be called
+ * for variables within a dynamic library's global scope.
+ * Therefore, global objects that own dynamically-managed resources
+ * (such as \c std::string or std::vector)
+ * should be kept behind pointers that are explicitly deallocated within this function.
+ *
+ * @see retro_init
+ */
+RETRO_API void retro_deinit(void);
+
+/**
+ * Retrieves which version of the libretro API is being used.
+ *
+ * @note This is used to validate ABI compatibility when the API is revised.
+ *
+ * @return Must return \c RETRO_API_VERSION.
+ *
+ * @see RETRO_API_VERSION
+ */
+RETRO_API unsigned retro_api_version(void);
+
+/**
+ * Gets statically known system info.
+ *
+ * @note Can be called at any time, even before retro_init().
+ *
+ * @param info A pointer to a \c retro_system_info where the info is to be loaded into. This must be statically allocated.
+ */
+RETRO_API void retro_get_system_info(struct retro_system_info *info);
+
+/**
+ * Gets information about system audio/video timings and geometry.
+ *
+ * @note Can be called only after \c retro_load_game() has successfully completed.
+ *
+ * @note The implementation of this function might not initialize every variable
+ * if needed. For example, \c geom.aspect_ratio might not be initialized if
+ * the core doesn't desire a particular aspect ratio.
+ *
+ * @param info A pointer to a \c retro_system_av_info where the audio/video information should be loaded into.
+ *
+ * @see retro_system_av_info
+ */
+RETRO_API void retro_get_system_av_info(struct retro_system_av_info *info);
+
+/**
+ * Sets device to be used for player 'port'.
+ *
+ * By default, \c RETRO_DEVICE_JOYPAD is assumed to be plugged into all
+ * available ports.
+ *
+ * @note Setting a particular device type is not a guarantee that libretro cores
+ * will only poll input based on that particular device type. It is only a
+ * hint to the libretro core when a core cannot automatically detect the
+ * appropriate input device type on its own. It is also relevant when a
+ * core can change its behavior depending on device type.
+ *
+ * @note As part of the core's implementation of retro_set_controller_port_device,
+ * the core should call \c RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS to notify the
+ * frontend if the descriptions for any controls have changed as a
+ * result of changing the device type.
+ *
+ * @param port Which port to set the device for, usually indicates the player number.
+ * @param device Which device the given port is using. By default, \c RETRO_DEVICE_JOYPAD is assumed for all ports.
+ *
+ * @see RETRO_DEVICE_NONE
+ * @see RETRO_DEVICE_JOYPAD
+ * @see RETRO_DEVICE_MOUSE
+ * @see RETRO_DEVICE_KEYBOARD
+ * @see RETRO_DEVICE_LIGHTGUN
+ * @see RETRO_DEVICE_ANALOG
+ * @see RETRO_DEVICE_POINTER
+ * @see RETRO_ENVIRONMENT_SET_CONTROLLER_INFO
+ */
+RETRO_API void retro_set_controller_port_device(unsigned port, unsigned device);
+
+/**
+ * Resets the currently-loaded game.
+ * Cores should treat this as a soft reset (i.e. an emulated reset button) if possible,
+ * but hard resets are acceptable.
+ */
+RETRO_API void retro_reset(void);
+
+/**
+ * Runs the game for one video frame.
+ *
+ * During \c retro_run(), the \c retro_input_poll_t callback must be called at least once.
+ *
+ * @note If a frame is not rendered for reasons where a game "dropped" a frame,
+ * this still counts as a frame, and \c retro_run() should explicitly dupe
+ * a frame if \c RETRO_ENVIRONMENT_GET_CAN_DUPE returns true. In this case,
+ * the video callback can take a NULL argument for data.
+ *
+ * @see retro_input_poll_t
+ */
+RETRO_API void retro_run(void);
+
+/**
+ * Returns the amount of data the implementation requires to serialize internal state (save states).
+ *
+ * @note Between calls to \c retro_load_game() and \c retro_unload_game(), the
+ * returned size is never allowed to be larger than a previous returned
+ * value, to ensure that the frontend can allocate a save state buffer once.
+ *
+ * @return The amount of data the implementation requires to serialize the internal state.
+ *
+ * @see retro_serialize()
+ */
+RETRO_API size_t retro_serialize_size(void);
+
+/**
+ * Serializes the internal state.
+ *
+ * @param data A pointer to where the serialized data should be saved to.
+ * @param size The size of the memory.
+ *
+ * @return If failed, or size is lower than \c retro_serialize_size(), it
+ * should return false. On success, it will return true.
+ *
+ * @see retro_serialize_size()
+ * @see retro_unserialize()
+ */
+RETRO_API bool retro_serialize(void *data, size_t len);
+
+/**
+ * Unserialize the given state data, and load it into the internal state.
+ *
+ * @return Returns true if loading the state was successful, false otherwise.
+ *
+ * @see retro_serialize()
+ */
+RETRO_API bool retro_unserialize(const void *data, size_t len);
+
+/**
+ * Reset all the active cheats to their default disabled state.
+ *
+ * @see retro_cheat_set()
+ */
+RETRO_API void retro_cheat_reset(void);
+
+/**
+ * Enable or disable a cheat.
+ *
+ * @param index The index of the cheat to act upon.
+ * @param enabled Whether to enable or disable the cheat.
+ * @param code A string of the code used for the cheat.
+ *
+ * @see retro_cheat_reset()
+ */
+RETRO_API void retro_cheat_set(unsigned index, bool enabled, const char *code);
+
+/**
+ * Loads a game.
+ *
+ * @param game A pointer to a \c retro_game_info detailing information about the game to load.
+ * May be \c NULL if the core is loaded without content.
+ *
+ * @return Will return true when the game was loaded successfully, or false otherwise.
+ *
+ * @see retro_game_info
+ * @see RETRO_ENVIRONMENT_SET_SUPPORT_NO_GAME
+ */
+RETRO_API bool retro_load_game(const struct retro_game_info *game);
+
+/**
+ * Called when the frontend has loaded one or more "special" content files,
+ * typically through subsystems.
+ *
+ * @note Only necessary for cores that support subsystems.
+ * Others may return \c false or delegate to retro_load_game.
+ *
+ * @param game_type The type of game to load,
+ * as determined by \c retro_subsystem_info.
+ * @param info A pointer to an array of \c retro_game_info objects
+ * providing information about the loaded content.
+ * @param num_info The number of \c retro_game_info objects passed into the info parameter.
+ * @return \c true if loading is successful, false otherwise.
+ * If the core returns \c false,
+ * the frontend should abort the core
+ * and return to its main menu (if applicable).
+ *
+ * @see RETRO_ENVIRONMENT_GET_GAME_INFO_EXT
+ * @see RETRO_ENVIRONMENT_SET_SUBSYSTEM_INFO
+ * @see retro_load_game()
+ * @see retro_subsystem_info
+ */
+RETRO_API bool retro_load_game_special(
+ unsigned game_type,
+ const struct retro_game_info *info, size_t num_info
+);
+
+/**
+ * Unloads the currently loaded game.
+ *
+ * @note This is called before \c retro_deinit(void).
+ *
+ * @see retro_load_game()
+ * @see retro_deinit()
+ */
+RETRO_API void retro_unload_game(void);
+
+/**
+ * Gets the region of the actively loaded content as either \c RETRO_REGION_NTSC or \c RETRO_REGION_PAL.
+ * @note This refers to the region of the content's intended television standard,
+ * not necessarily the region of the content's origin.
+ * For emulated consoles that don't use either standard
+ * (e.g. handhelds or post-HD platforms),
+ * the core should return \c RETRO_REGION_NTSC.
+ * @return The region of the actively loaded content.
+ *
+ * @see RETRO_REGION_NTSC
+ * @see RETRO_REGION_PAL
+ */
+RETRO_API unsigned retro_get_region(void);
+
+/**
+ * Get a region of memory.
+ *
+ * @param id The ID for the memory block that's desired to retrieve. Can be \c RETRO_MEMORY_SAVE_RAM, \c RETRO_MEMORY_RTC, \c RETRO_MEMORY_SYSTEM_RAM, or \c RETRO_MEMORY_VIDEO_RAM.
+ *
+ * @return A pointer to the desired region of memory, or NULL when not available.
+ *
+ * @see RETRO_MEMORY_SAVE_RAM
+ * @see RETRO_MEMORY_RTC
+ * @see RETRO_MEMORY_SYSTEM_RAM
+ * @see RETRO_MEMORY_VIDEO_RAM
+ */
+RETRO_API void *retro_get_memory_data(unsigned id);
+
+/**
+ * Gets the size of the given region of memory.
+ *
+ * @param id The ID for the memory block to check the size of. Can be RETRO_MEMORY_SAVE_RAM, RETRO_MEMORY_RTC, RETRO_MEMORY_SYSTEM_RAM, or RETRO_MEMORY_VIDEO_RAM.
+ *
+ * @return The size of the region in memory, or 0 when not available.
+ *
+ * @see RETRO_MEMORY_SAVE_RAM
+ * @see RETRO_MEMORY_RTC
+ * @see RETRO_MEMORY_SYSTEM_RAM
+ * @see RETRO_MEMORY_VIDEO_RAM
+ */
+RETRO_API size_t retro_get_memory_size(unsigned id);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
\ No newline at end of file
diff --git a/pkg/emulator/libretro/nanoarch/loader.go b/pkg/worker/caged/libretro/nanoarch/loader.go
similarity index 90%
rename from pkg/emulator/libretro/nanoarch/loader.go
rename to pkg/worker/caged/libretro/nanoarch/loader.go
index f5f0e61c..d7d0c662 100644
--- a/pkg/emulator/libretro/nanoarch/loader.go
+++ b/pkg/worker/caged/libretro/nanoarch/loader.go
@@ -2,7 +2,7 @@ package nanoarch
import (
"errors"
- "io/ioutil"
+ "os"
"path"
"strconv"
"strings"
@@ -16,17 +16,14 @@ import (
*/
import "C"
-func open(file string) unsafe.Pointer {
- cs := C.CString(file)
- defer C.free(unsafe.Pointer(cs))
- return C.dlopen(cs, C.RTLD_LAZY)
-}
-
func loadFunction(handle unsafe.Pointer, name string) unsafe.Pointer {
cs := C.CString(name)
defer C.free(unsafe.Pointer(cs))
- pointer := C.dlsym(handle, cs)
- return pointer
+ 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) {
@@ -44,7 +41,7 @@ func loadLib(filepath string) (handle unsafe.Pointer, err error) {
func loadLibRollingRollingRolling(filepath string) (handle unsafe.Pointer, err error) {
dir, lib := path.Dir(filepath), path.Base(filepath)
- files, err := ioutil.ReadDir(dir)
+ files, err := os.ReadDir(dir)
if err != nil {
return nil, errors.New("couldn't find 'n load the lib")
}
@@ -60,11 +57,16 @@ func loadLibRollingRollingRolling(filepath string) (handle unsafe.Pointer, err e
return nil, errors.New("couldn't find 'n load the lib")
}
+func open(file string) unsafe.Pointer {
+ cs := C.CString(file)
+ defer C.free(unsafe.Pointer(cs))
+ return C.dlopen(cs, C.RTLD_LAZY)
+}
+
func closeLib(handle unsafe.Pointer) (err error) {
if handle == nil {
return
}
-
code := int(C.dlclose(handle))
if code != 0 {
return errors.New("couldn't close the lib (" + strconv.Itoa(code) + ")")
diff --git a/pkg/worker/caged/libretro/nanoarch/nanoarch.c b/pkg/worker/caged/libretro/nanoarch/nanoarch.c
new file mode 100644
index 00000000..63d3b4d4
--- /dev/null
+++ b/pkg/worker/caged/libretro/nanoarch/nanoarch.c
@@ -0,0 +1,405 @@
+#include "libretro.h"
+#include
+#include
+#include
+#include
+#include
+
+#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;
+} call_def_t;
+
+call_def_t call;
+
+enum call_type {
+ CALL_VOID = -1,
+ CALL_SERIALIZE = 1,
+ CALL_UNSERIALIZE = 2,
+};
+
+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;
+ va_start(va, fmt);
+ vsnprintf(msg, sizeof(msg), fmt, va);
+ va_end(va);
+ void coreLog(enum retro_log_level level, const char *msg);
+ coreLog(level, msg);
+}
+
+void bridge_call(void *f) {
+ ((void (*)(void)) f)();
+}
+
+void bridge_set_callback(void *f, void *callback) {
+ ((void (*)(void *))f)(callback);
+}
+
+unsigned bridge_retro_api_version(void *f) {
+ return ((unsigned (*)(void)) f)();
+}
+
+void bridge_retro_get_system_info(void *f, struct retro_system_info *si) {
+ ((void (*)(struct retro_system_info *)) f)(si);
+}
+
+void bridge_retro_get_system_av_info(void *f, struct retro_system_av_info *si) {
+ ((void (*)(struct retro_system_av_info *)) f)(si);
+}
+
+bool bridge_retro_set_environment(void *f, void *callback) {
+ return ((bool (*)(retro_environment_t)) f)((retro_environment_t) callback);
+}
+
+void bridge_retro_set_input_state(void *f, void *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) {
+ return ((bool (*)(struct retro_game_info *)) f)(gi);
+}
+
+size_t bridge_retro_get_memory_size(void *f, unsigned id) {
+ return ((size_t (*)(unsigned)) f)(id);
+}
+
+void *bridge_retro_get_memory_data(void *f, unsigned id) {
+ return ((void *(*)(unsigned)) f)(id);
+}
+
+size_t bridge_retro_serialize_size(void *f) {
+ return ((size_t (*)(void)) f)();
+}
+
+bool bridge_retro_serialize(void *f, void *data, size_t size) {
+ return ((bool (*)(void *, size_t)) f)(data, size);
+}
+
+bool bridge_retro_unserialize(void *f, void *data, size_t size) {
+ return ((bool (*)(void *, size_t)) f)(data, size);
+}
+
+void bridge_retro_set_controller_port_device(void *f, unsigned port, unsigned device) {
+ ((void (*)(unsigned, unsigned)) f)(port, device);
+}
+
+static bool clear_all_thread_waits_cb(unsigned v, void *data) {
+ core_log_cgo(RETRO_LOG_DEBUG, "CLEAR_ALL_THREAD_WAITS_CB (%d)\n", v);
+ return true;
+}
+
+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);
+}
+
+void core_video_refresh_cgo(void *data, unsigned width, unsigned height, size_t pitch) {
+ void coreVideoRefresh(void *, unsigned, unsigned, size_t);
+ coreVideoRefresh(data, width, height, pitch);
+}
+
+void core_input_poll_cgo() {
+}
+
+int16_t core_input_state_cgo(unsigned port, unsigned device, unsigned index, unsigned id) {
+ if (port >= INPUT_MAX_PORTS) {
+ return 0;
+ }
+
+ 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) {
+ size_t coreAudioSampleBatch(const int16_t *, size_t);
+ 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();
+}
+
+retro_proc_address_t core_get_proc_address_cgo(const char *sym) {
+ retro_proc_address_t coreGetProcAddress(const char *sym);
+ return coreGetProcAddress(sym);
+}
+
+void bridge_context_reset(retro_hw_context_reset_t f) {
+ f();
+}
+
+void init_video_cgo() {
+ void initVideo();
+ initVideo();
+}
+
+void deinit_video_cgo() {
+ void deinitVideo();
+ deinitVideo();
+}
+
+typedef struct {
+ pthread_mutex_t m;
+ pthread_cond_t cond;
+} mutex_t;
+
+void mutex_init(mutex_t *m) {
+ pthread_mutex_init(&m->m, NULL);
+ pthread_cond_init(&m->cond, NULL);
+}
+
+void mutex_destroy(mutex_t *m) {
+ pthread_mutex_trylock(&m->m);
+ pthread_mutex_unlock(&m->m);
+ pthread_mutex_destroy(&m->m);
+ pthread_cond_signal(&m->cond);
+ pthread_cond_destroy(&m->cond);
+}
+
+void mutex_lock(mutex_t *m) { pthread_mutex_lock(&m->m); }
+void mutex_wait(mutex_t *m) { pthread_cond_wait(&m->cond, &m->m); }
+void mutex_unlock(mutex_t *m) { pthread_mutex_unlock(&m->m); }
+void mutex_signal(mutex_t *m) { pthread_cond_signal(&m->cond); }
+
+static pthread_t thread;
+mutex_t run_mutex, done_mutex;
+
+void *run_loop(void *unused) {
+ core_log_cgo(RETRO_LOG_DEBUG, "UnLibCo run loop start\n");
+ mutex_lock(&done_mutex);
+ mutex_lock(&run_mutex);
+ mutex_signal(&done_mutex);
+ mutex_unlock(&done_mutex);
+ while (initialized) {
+ mutex_wait(&run_mutex);
+ switch (call.type) {
+ case CALL_SERIALIZE:
+ case CALL_UNSERIALIZE:
+ *(bool*)call.result = ((bool (*)(void*, size_t))call.fn)(call.arg1, *(size_t*)call.arg2);
+ break;
+ default:
+ ((void (*)(void)) call.fn)();
+ }
+ mutex_lock(&done_mutex);
+ mutex_signal(&done_mutex);
+ mutex_unlock(&done_mutex);
+ }
+ mutex_destroy(&run_mutex);
+ 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() {
+ initialized = 0;
+}
+
+void *same_thread_with_args(void *f, int type, ...) {
+ if (!initialized) {
+ initialized = 1;
+ mutex_init(&run_mutex);
+ mutex_init(&done_mutex);
+ mutex_lock(&done_mutex);
+ pthread_create(&thread, NULL, run_loop, NULL);
+ mutex_wait(&done_mutex);
+ mutex_unlock(&done_mutex);
+ }
+ mutex_lock(&run_mutex);
+ mutex_lock(&done_mutex);
+
+ call.type = type;
+ call.fn = f;
+
+ if (type != CALL_VOID) {
+ va_list args;
+ va_start(args, type);
+ switch (type) {
+ case CALL_SERIALIZE:
+ case CALL_UNSERIALIZE:
+ call.arg1 = va_arg(args, void*);
+ size_t size;
+ size = va_arg(args, size_t);
+ call.arg2 = &size;
+ bool result;
+ call.result = &result;
+ break;
+ }
+ va_end(args);
+ }
+ mutex_signal(&run_mutex);
+ mutex_unlock(&run_mutex);
+ mutex_wait(&done_mutex);
+ mutex_unlock(&done_mutex);
+ return call.result;
+}
+
+void *same_thread_with_args2(void *f, int type, void *arg1, void *arg2) {
+ return same_thread_with_args(f, type, arg1, arg2);
+}
+
+void same_thread(void *f) {
+ same_thread_with_args(f, CALL_VOID);
+}
diff --git a/pkg/worker/caged/libretro/nanoarch/nanoarch.go b/pkg/worker/caged/libretro/nanoarch/nanoarch.go
new file mode 100644
index 00000000..5d34dca3
--- /dev/null
+++ b/pkg/worker/caged/libretro/nanoarch/nanoarch.go
@@ -0,0 +1,943 @@
+package nanoarch
+
+import (
+ "errors"
+ "fmt"
+ "maps"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "sync"
+ "sync/atomic"
+ "time"
+ "unsafe"
+
+ "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/thread"
+)
+
+/*
+#include "libretro.h"
+#include "nanoarch.h"
+#include
+*/
+import "C"
+
+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
+ RGB565 = PixFmt{C: 2, BPP: 2} // BIT_FORMAT_SHORT_5_6_5 has 5 bits R, 6 bits G, 5 bits
+)
+
+type Nanoarch struct {
+ Handlers
+
+ keyboard KeyboardState
+ mouse MouseState
+ retropad InputState
+
+ keyboardCb *C.struct_retro_keyboard_callback
+ LastFrameTime int64
+ LibCo bool
+ 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
+ }
+ tickTime int64
+ cSaveDirectory *C.char
+ cSystemDirectory *C.char
+ cUserName *C.char
+ Video struct {
+ gl struct {
+ enabled bool
+ autoCtx bool
+ }
+ hw *C.struct_retro_hw_render_callback
+ PixFmt PixFmt
+ }
+ vfr bool
+ Aspect bool
+ sdlCtx *graphics.SDL
+ hackSkipHwContextDestroy bool
+ hackSkipSameThreadSave bool
+ limiter func(func())
+ log *logger.Logger
+}
+
+type Handlers struct {
+ OnAudio func(ptr unsafe.Pointer, frames int)
+ OnVideo func(data []byte, delta int32, fi FrameInfo)
+ OnDup func()
+ OnSystemAvInfo func()
+}
+
+type FrameInfo struct {
+ W uint
+ H uint
+ Stride uint
+}
+
+type Metadata struct {
+ 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 {
+ C uint32
+ BPP uint
+}
+
+func (p PixFmt) String() string {
+ switch p.C {
+ case 0:
+ return "RGBA5551/2"
+ case 1:
+ return "RGBA8888Rev/4"
+ case 2:
+ return "RGB565/2"
+ default:
+ return fmt.Sprintf("Unknown (%v/%v)", p.C, p.BPP)
+ }
+}
+
+// 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{},
+ limiter: func(fn func()) { fn() },
+ Handlers: Handlers{
+ OnAudio: func(unsafe.Pointer, int) {},
+ OnVideo: func([]byte, int32, FrameInfo) {},
+ OnDup: func() {},
+ },
+}
+
+// init provides a global single instance lock
+// !to remove when isolated properly
+func init() { Nan0.reserved <- struct{}{} }
+
+func NewNano(localPath string) *Nanoarch {
+ nano := &Nan0
+ nano.cSaveDirectory = C.CString(localPath + "/legacy_save")
+ nano.cSystemDirectory = C.CString(localPath + "/system")
+ nano.cUserName = C.CString("retro")
+ return nano
+}
+
+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) DeleteSaveDir() error {
+ if n.cSaveDirectory == nil {
+ return nil
+ }
+
+ dir := C.GoString(n.cSaveDirectory)
+ return os.RemoveAll(dir)
+}
+
+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")
+
+ // reset controllers
+ n.retropad = InputState{}
+ n.keyboardCb = nil
+ n.keyboard = KeyboardState{}
+ n.mouse = MouseState{}
+
+ n.options = maps.Clone(meta.Options)
+ n.options4rom = meta.Options4rom
+
+ 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", corePath)
+ coreLib, err = loadLibRollingRollingRolling(corePath)
+ if err != nil {
+ 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")
+ retroGetSystemInfo = loadFunction(coreLib, "retro_get_system_info")
+ retroGetSystemAVInfo = loadFunction(coreLib, "retro_get_system_av_info")
+ retroSetEnvironment = loadFunction(coreLib, "retro_set_environment")
+ retroSetVideoRefresh = loadFunction(coreLib, "retro_set_video_refresh")
+ retroSetInputPoll = loadFunction(coreLib, "retro_set_input_poll")
+ 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")
+ retroSerializeSize = loadFunction(coreLib, "retro_serialize_size")
+ retroSerialize = loadFunction(coreLib, "retro_serialize")
+ retroUnserialize = loadFunction(coreLib, "retro_unserialize")
+ retroSetControllerPortDevice = loadFunction(coreLib, "retro_set_controller_port_device")
+ retroGetMemorySize = loadFunction(coreLib, "retro_get_memory_size")
+ retroGetMemoryData = loadFunction(coreLib, "retro_get_memory_data")
+
+ C.bridge_retro_set_environment(retroSetEnvironment, C.core_environment_cgo)
+ C.bridge_retro_set_input_state(retroSetInputState, C.core_input_state_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_call(retroInit)
+ }
+
+ 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.sys.i.need_fullpath) // big ROMs are loaded by cores later
+ if big {
+ size, err := os.StatSize(path)
+ if err != nil {
+ return err
+ }
+ game.size = C.size_t(size)
+ } else {
+ bytes, err := os.ReadFile(path)
+ if err != nil {
+ return err
+ }
+ // !to pin in 1.21
+ ptr := unsafe.Pointer(C.CBytes(bytes))
+ game.data = ptr
+ game.size = C.size_t(len(bytes))
+ defer C.free(ptr)
+ }
+ fp := C.CString(path)
+ defer C.free(unsafe.Pointer(fp))
+ game.path = fp
+
+ 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)
+ }
+
+ 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]",
+ 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.sys.av.timing.fps))
+ if n.vfr {
+ n.log.Info().Msgf("variable framerate (VFR) is enabled")
+ }
+
+ n.Stopped.Store(false)
+
+ if n.Video.gl.enabled {
+ 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
+ // 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
+}
+
+func (n *Nanoarch) Shutdown() {
+ if n.LibCo {
+ thread.Main(func() {
+ C.same_thread(retroUnloadGame)
+ C.same_thread(retroDeinit)
+ if n.Video.gl.enabled {
+ C.same_thread(C.deinit_video_cgo)
+ }
+ C.same_thread(C.same_thread_stop)
+ })
+ } else {
+ if n.Video.gl.enabled {
+ thread.Main(func() {
+ // 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 switch fail")
+ }
+ })
+ }
+ C.bridge_call(retroUnloadGame)
+ C.bridge_call(retroDeinit)
+ if n.Video.gl.enabled {
+ thread.Main(func() {
+ deinitVideo()
+ runtime.UnlockOSThread()
+ })
+ }
+ }
+
+ 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 {
+ runtime.LockOSThread()
+ if err := n.sdlCtx.BindContext(); err != nil {
+ n.log.Error().Err(err).Msg("ctx bind fail")
+ }
+ }
+ C.bridge_call(retroRun)
+ if n.Video.gl.enabled {
+ runtime.UnlockOSThread()
+ }
+ }
+}
+
+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 {
+ case C.RETRO_PIXEL_FORMAT_0RGB1555:
+ Nan0.Video.PixFmt = RGBA5551
+ if err := graphics.SetPixelFormat(graphics.UnsignedShort5551); err != nil {
+ return false, fmt.Errorf("unknown pixel format %v", Nan0.Video.PixFmt)
+ }
+ case C.RETRO_PIXEL_FORMAT_XRGB8888:
+ Nan0.Video.PixFmt = RGBA8888Rev
+ if err := graphics.SetPixelFormat(graphics.UnsignedInt8888Rev); err != nil {
+ return false, fmt.Errorf("unknown pixel format %v", Nan0.Video.PixFmt)
+ }
+ case C.RETRO_PIXEL_FORMAT_RGB565:
+ Nan0.Video.PixFmt = RGB565
+ if err := graphics.SetPixelFormat(graphics.UnsignedShort565); err != nil {
+ return false, fmt.Errorf("unknown pixel format %v", Nan0.Video.PixFmt)
+ }
+ default:
+ return false, fmt.Errorf("unknown pixel type %v", format)
+ }
+ Nan0.log.Info().Msgf("Pixel format: %v", Nan0.Video.PixFmt)
+
+ return true, nil
+}
+
+func setRotation(rot uint) {
+ Nan0.Rot = rot
+ Nan0.log.Debug().Msgf("Image rotated %v°", rot)
+}
+
+func printOpenGLDriverInfo() {
+ var openGLInfo strings.Builder
+ openGLInfo.Grow(128)
+ 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())
+}
+
+// State defines any memory state of the emulator
+type State []byte
+
+type mem struct {
+ ptr unsafe.Pointer
+ size uint
+}
+
+const (
+ CallSerialize = 1
+ CallUnserialize = 2
+)
+
+// SaveState returns emulator internal state.
+func SaveState() (State, error) {
+ size := C.bridge_retro_serialize_size(retroSerializeSize)
+ data := make([]byte, uint(size))
+ rez := false
+
+ 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]), 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 {
+ 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
+}
+
+// SaveRAM returns the game save RAM (cartridge) data or a nil slice.
+func SaveRAM() State {
+ memory := ptSaveRAM()
+ if memory == nil {
+ return nil
+ }
+ return C.GoBytes(memory.ptr, C.int(memory.size))
+}
+
+// RestoreSaveRAM restores game save RAM.
+func RestoreSaveRAM(st State) {
+ if len(st) > 0 {
+ if memory := ptSaveRAM(); memory != nil {
+ //noinspection GoRedundantConversion
+ copy(unsafe.Slice((*byte)(memory.ptr), memory.size), st)
+ }
+ }
+}
+
+// memorySize returns memory region size.
+func memorySize(id C.uint) uint {
+ return uint(C.bridge_retro_get_memory_size(retroGetMemorySize, id))
+}
+
+// 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 := memoryData(C.RETRO_MEMORY_SAVE_RAM), memorySize(C.RETRO_MEMORY_SAVE_RAM)
+ if ptr == nil || size == 0 {
+ return nil
+ }
+ return &mem{ptr: ptr, size: size}
+}
+
+func byteCountBinary(b int64) string {
+ const unit = 1024
+ if b < unit {
+ return fmt.Sprintf("%d B", b)
+ }
+ div, exp := int64(unit), 0
+ for n := b / unit; n >= unit; n /= unit {
+ div *= unit
+ exp++
+ }
+ return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
+}
+
+func (m Metadata) HasHack(h string) bool {
+ for _, n := range m.Hacks {
+ if h == n {
+ return true
+ }
+ }
+ return false
+}
+
+var (
+ 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
+ retroSetControllerPortDevice unsafe.Pointer
+ retroSetEnvironment unsafe.Pointer
+ retroSetInputPoll unsafe.Pointer
+ retroSetInputState unsafe.Pointer
+ retroSetVideoRefresh unsafe.Pointer
+ retroUnloadGame unsafe.Pointer
+ retroGetMemoryData unsafe.Pointer
+ retroGetMemorySize unsafe.Pointer
+ retroSerialize unsafe.Pointer
+ retroSerializeSize unsafe.Pointer
+ retroUnserialize unsafe.Pointer
+)
+
+//export coreVideoRefresh
+func coreVideoRefresh(data unsafe.Pointer, width, height uint, packed uint) {
+ if Nan0.Stopped.Load() {
+ return
+ }
+
+ // some frames can be rendered slower or faster than internal 1/fps core tick
+ // so track actual frame render time for proper RTP packet timestamps
+ // (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
+ dt := Nan0.tickTime
+ if Nan0.vfr {
+ t := time.Now().UnixNano()
+ dt = t - Nan0.LastFrameTime
+ Nan0.LastFrameTime = t
+ }
+
+ // 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 * bpp
+ }
+ // calculate space for the video frame
+ bytes := packed * height
+
+ var data_ []byte
+ if data != C.RETRO_HW_FRAME_BUFFER_VALID {
+ //noinspection GoRedundantConversion
+ data_ = unsafe.Slice((*byte)(data), bytes)
+ } else {
+ // if Libretro renders frame with OpenGL context
+ data_ = graphics.ReadFramebuffer(bytes, width, height)
+ }
+
+ // some cores or games have a variable output frame size, i.e. PSX Rearmed
+ // also we have an option of xN output frame magnification
+ // so, it may be rescaled
+
+ Nan0.Handlers.OnVideo(data_, int32(dt), FrameInfo{W: width, H: height, Stride: packed})
+}
+
+//export coreAudioSampleBatch
+func coreAudioSampleBatch(data unsafe.Pointer, frames C.size_t) C.size_t {
+ if Nan0.Stopped.Load() {
+ return frames
+ }
+ Nan0.Handlers.OnAudio(data, int(frames)<<1)
+ return frames
+}
+
+func m(m *C.char) string { return strings.TrimRight(C.GoString(m), "\n") }
+
+//export coreLog
+func coreLog(level C.enum_retro_log_level, msg *C.char) {
+ switch level {
+ // with debug level cores have too much logs
+ case C.RETRO_LOG_DEBUG:
+ Nan0.log.Debug().MsgFunc(func() string { return m(msg) })
+ case C.RETRO_LOG_INFO:
+ Nan0.log.Info().MsgFunc(func() string { return m(msg) })
+ case C.RETRO_LOG_WARN:
+ Nan0.log.Warn().MsgFunc(func() string { return m(msg) })
+ case C.RETRO_LOG_ERROR:
+ Nan0.log.Error().MsgFunc(func() string { return m(msg) })
+ default:
+ Nan0.log.Log().MsgFunc(func() string { return m(msg) })
+ // RETRO_LOG_DUMMY = INT_MAX
+ }
+}
+
+//export coreGetCurrentFramebuffer
+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.GlProcAddress(C.GoString(sym)))
+}
+
+//export coreEnvironment
+func coreEnvironment(cmd C.unsigned, data unsafe.Pointer) C.bool {
+
+ // 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)
+ 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)
+ 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:
+ 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
+ case C.RETRO_ENVIRONMENT_GET_LOG_INTERFACE:
+ cb := (*C.struct_retro_log_callback)(data)
+ cb.log = (C.retro_log_printf_t)(C.core_log_cgo)
+ return true
+ case C.RETRO_ENVIRONMENT_SET_PIXEL_FORMAT:
+ res, err := videoSetPixelFormat(*(*C.enum_retro_pixel_format)(data))
+ if err != nil {
+ Nan0.log.Fatal().Err(err).Msg("pix format failed")
+ }
+ return res
+ case C.RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY:
+ *(**C.char)(data) = Nan0.cSystemDirectory
+ return true
+ case C.RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY:
+ *(**C.char)(data) = Nan0.cSaveDirectory
+ return true
+ case C.RETRO_ENVIRONMENT_SET_MESSAGE:
+ // only with the Libretro debug mode
+ if Nan0.log.GetLevel() < logger.InfoLevel {
+ message := (*C.struct_retro_message)(data)
+ msg := C.GoString(message.msg)
+ Nan0.log.Debug().Msgf("message: %v", msg)
+ return true
+ }
+ return false
+ case C.RETRO_ENVIRONMENT_GET_VARIABLE:
+ if Nan0.options == nil {
+ return false
+ }
+ rv := (*C.struct_retro_variable)(data)
+ key := C.GoString(rv.key)
+ if v, ok := Nan0.options[key]; ok {
+ // make Go strings null-terminated copies ;_;
+ 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
+ rv.value = (*C.char)(ptr)
+ Nan0.log.Debug().Msgf("Set %v=%v", key, v)
+ return true
+ }
+ return false
+ case C.RETRO_ENVIRONMENT_SET_HW_RENDER:
+ if Nan0.Video.gl.enabled {
+ Nan0.Video.hw = (*C.struct_retro_hw_render_callback)(data)
+ Nan0.Video.hw.get_current_framebuffer = (C.retro_hw_get_current_framebuffer_t)(C.core_get_current_framebuffer_cgo)
+ Nan0.Video.hw.get_proc_address = (C.retro_hw_get_proc_address_t)(C.core_get_proc_address_cgo)
+ return true
+ }
+ return false
+ case C.RETRO_ENVIRONMENT_SET_CONTROLLER_INFO:
+ if Nan0.log.GetLevel() > logger.DebugLevel {
+ return false
+ }
+
+ 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 true
+ 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
+}
+
+//export initVideo
+func initVideo() {
+ var context graphics.Context
+ switch Nan0.Video.hw.context_type {
+ case C.RETRO_HW_CONTEXT_NONE:
+ context = graphics.CtxNone
+ case C.RETRO_HW_CONTEXT_OPENGL:
+ context = graphics.CtxOpenGl
+ case C.RETRO_HW_CONTEXT_OPENGLES2:
+ context = graphics.CtxOpenGlEs2
+ case C.RETRO_HW_CONTEXT_OPENGL_CORE:
+ context = graphics.CtxOpenGlCore
+ case C.RETRO_HW_CONTEXT_OPENGLES3:
+ context = graphics.CtxOpenGlEs3
+ case C.RETRO_HW_CONTEXT_OPENGLES_VERSION:
+ context = graphics.CtxOpenGlEsVersion
+ case C.RETRO_HW_CONTEXT_VULKAN:
+ context = graphics.CtxVulkan
+ case C.RETRO_HW_CONTEXT_DUMMY:
+ context = graphics.CtxDummy
+ default:
+ context = graphics.CtxUnknown
+ }
+
+ 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)
+ }
+ })
+
+ if Nan0.log.GetLevel() < logger.InfoLevel {
+ printOpenGLDriverInfo()
+ }
+}
+
+//export deinitVideo
+func deinitVideo() {
+ if !Nan0.hackSkipHwContextDestroy {
+ C.bridge_context_reset(Nan0.Video.hw.context_destroy)
+ }
+ 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
+}
diff --git a/pkg/worker/caged/libretro/nanoarch/nanoarch.h b/pkg/worker/caged/libretro/nanoarch/nanoarch.h
new file mode 100644
index 00000000..d8e09265
--- /dev/null
+++ b/pkg/worker/caged/libretro/nanoarch/nanoarch.h
@@ -0,0 +1,38 @@
+#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);
+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);
+bool bridge_retro_set_environment(void *f, void *callback);
+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_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_set_controller_port_device(void *f, unsigned port, unsigned device);
+void bridge_retro_set_input_state(void *f, void *callback);
+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);
+retro_proc_address_t core_get_proc_address_cgo(const char *sym);
+size_t core_audio_sample_batch_cgo(const int16_t *data, size_t frames);
+uintptr_t core_get_current_framebuffer_cgo();
+void core_audio_sample_cgo(int16_t left, int16_t right);
+void core_input_poll_cgo();
+void core_log_cgo(int level, const char *msg);
+void core_video_refresh_cgo(void *data, unsigned width, unsigned height, size_t pitch);
+void init_video_cgo();
+void deinit_video_cgo();
+
+void same_thread(void *f);
+void *same_thread_with_args2(void *f, int type, void *arg1, void *arg2);
+void same_thread_stop();
+
+#endif
diff --git a/pkg/worker/caged/libretro/nanoarch/nanoarch_test.go b/pkg/worker/caged/libretro/nanoarch/nanoarch_test.go
new file mode 100644
index 00000000..c92c89e8
--- /dev/null
+++ b/pkg/worker/caged/libretro/nanoarch/nanoarch_test.go
@@ -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")
+ }
+}
diff --git a/pkg/worker/caged/libretro/recording.go b/pkg/worker/caged/libretro/recording.go
new file mode 100644
index 00000000..64734536
--- /dev/null
+++ b/pkg/worker/caged/libretro/recording.go
@@ -0,0 +1,72 @@
+package libretro
+
+import (
+ "time"
+
+ "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/app"
+ "github.com/giongto35/cloud-game/v3/pkg/worker/recorder"
+)
+
+type RecordingFrontend struct {
+ Emulator
+ rec *recorder.Recording
+}
+
+func WithRecording(fe Emulator, rec bool, user string, game string, conf config.Recording, log *logger.Logger) *RecordingFrontend {
+ rr := &RecordingFrontend{Emulator: fe, rec: recorder.NewRecording(
+ recorder.Meta{UserName: user},
+ log,
+ recorder.Options{
+ Dir: conf.Folder,
+ Game: game,
+ Name: conf.Name,
+ Zip: conf.Zip,
+ Vsync: true,
+ Flip: fe.Flipped(),
+ })}
+ rr.ToggleRecording(rec, user)
+ return rr
+}
+
+func (r *RecordingFrontend) SetAudioCb(fn func(app.Audio)) {
+ r.Emulator.SetAudioCb(func(audio app.Audio) {
+ if r.IsRecording() {
+ pcm := audio.Data
+ // example: 1600 = x / 1000 * 48000 * 2
+ l := time.Duration(float64(len(pcm)) / float64(r.AudioSampleRate()<<1) * 1000000000)
+ r.rec.WriteAudio(recorder.Audio{Samples: pcm, Duration: l})
+ }
+ fn(audio)
+ })
+}
+
+func (r *RecordingFrontend) SetVideoCb(fn func(app.Video)) {
+ r.Emulator.SetVideoCb(func(v app.Video) {
+ if r.IsRecording() {
+ r.rec.WriteVideo(recorder.Video{Frame: recorder.Frame(v.Frame), Duration: time.Duration(v.Duration)})
+ }
+ fn(v)
+ })
+}
+
+func (r *RecordingFrontend) LoadGame(path string) error {
+ err := r.Emulator.LoadGame(path)
+ if err != nil {
+ return err
+ }
+ r.rec.SetFramerate(float64(r.Emulator.FPS()))
+ r.rec.SetAudioFrequency(r.Emulator.AudioSampleRate())
+ r.rec.SetPixFormat(r.Emulator.PixFormat())
+ return nil
+}
+
+func (r *RecordingFrontend) ToggleRecording(active bool, user string) {
+ if r.rec != nil {
+ r.rec.Set(active, user)
+ }
+}
+
+func (r *RecordingFrontend) IsRecording() bool { return r.rec != nil && r.rec.Enabled() }
+func (r *RecordingFrontend) Close() { r.Emulator.Close(); r.ToggleRecording(false, "") }
diff --git a/pkg/worker/caged/libretro/storage.go b/pkg/worker/caged/libretro/storage.go
new file mode 100644
index 00000000..fc58faaf
--- /dev/null
+++ b/pkg/worker/caged/libretro/storage.go
@@ -0,0 +1,79 @@
+package libretro
+
+import (
+ "path/filepath"
+ "strings"
+
+ "github.com/giongto35/cloud-game/v3/pkg/os"
+ "github.com/giongto35/cloud-game/v3/pkg/worker/compression/zip"
+)
+
+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
+ }
+ StateStorage struct {
+ // save path without the dir slash in the end
+ Path string
+ // contains the name of the main save file
+ // e.g. abc<...>293.dat
+ // needed for Google Cloud save/restore which
+ // doesn't support multiple files
+ MainSave string
+ NonBlock bool
+ }
+ ZipStorage struct {
+ Storage
+ }
+)
+
+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 }
+
+// Load loads a zip file with the path specified.
+func (z *ZipStorage) Load(path string) ([]byte, error) {
+ data, err := z.Storage.Load(path)
+ if err != nil {
+ return nil, err
+ }
+ d, _, err := zip.Read(data)
+ if err != nil {
+ return nil, err
+ }
+ return d, nil
+}
+
+// Save saves the array of bytes into a file with the specified path.
+func (z *ZipStorage) Save(path string, data []byte) error {
+ _, name := filepath.Split(path)
+ if name == "" || name == "." {
+ return zip.ErrorInvalidName
+ }
+ name = strings.TrimSuffix(name, zip.Ext)
+ compress, err := zip.Compress(data, name)
+ if err != nil {
+ return err
+ }
+ return z.Storage.Save(path, compress)
+}
diff --git a/pkg/worker/caged/libretro/storage_test.go b/pkg/worker/caged/libretro/storage_test.go
new file mode 100644
index 00000000..f4a421b0
--- /dev/null
+++ b/pkg/worker/caged/libretro/storage_test.go
@@ -0,0 +1,36 @@
+package libretro
+
+import (
+ "os"
+ "path/filepath"
+ "reflect"
+ "testing"
+)
+
+func TestZipStorage(t *testing.T) {
+ testDir := os.TempDir()
+ fileName := "test-state"
+ destPath := filepath.Join(testDir, fileName) + ".zip"
+ expect := []byte{1, 2, 3, 4}
+ z := &ZipStorage{
+ Storage: &StateStorage{
+ Path: testDir,
+ MainSave: fileName,
+ },
+ }
+ if err := z.Save(destPath, expect); err != nil {
+ t.Errorf("Zip storage error = %v", err)
+ }
+ defer func() {
+ if err := os.Remove(destPath); err != nil {
+ t.Errorf("Zip storage couldn't remove %v", destPath)
+ }
+ }()
+ d, err := z.Load(destPath)
+ if err != nil {
+ t.Errorf("Zip storage error = %v", err)
+ }
+ if !reflect.DeepEqual(d, expect) {
+ t.Errorf("Zip storage got = %v, want %v", d, expect)
+ }
+}
diff --git a/pkg/worker/cloud-storage/storage.go b/pkg/worker/cloud-storage/storage.go
deleted file mode 100644
index 07f46b3d..00000000
--- a/pkg/worker/cloud-storage/storage.go
+++ /dev/null
@@ -1,97 +0,0 @@
-package storage
-
-import (
- "context"
- "errors"
- "io"
- "io/ioutil"
- "log"
- "os"
-
- "cloud.google.com/go/storage"
-)
-
-// TODO: Add interface, abstract out Gstorage
-type Client struct {
- bucket *storage.BucketHandle
- gclient *storage.Client
-}
-
-// NewInitClient returns nil of client is not initialized
-func NewInitClient() *Client {
- bucketName := "game-save"
-
- client, err := NewClient(bucketName)
- if err != nil {
- log.Printf("Warn: Failed to create client: %v", err)
- } else {
- log.Println("Online storage is initialized")
- }
-
- return client
-}
-
-// NewClient inits a new Client accessing to GCP
-func NewClient(bucketName string) (*Client, error) {
- ctx := context.Background()
-
- // Sets your Google Cloud Platform project ID.
-
- // Creates a client.
- gclient, err := storage.NewClient(ctx)
- if err != nil {
- return nil, err
- }
-
- // Creates a Bucket instance.
- bucket := gclient.Bucket(bucketName)
-
- return &Client{
- bucket: bucket,
- gclient: gclient,
- }, nil
-}
-
-// Savefile save srcFile to GCP
-func (c *Client) SaveFile(name string, srcFile string) (err error) {
- // Bypass if client is nil
- if c == nil {
- return nil
- }
-
- reader, err := os.Open(srcFile)
- if err != nil {
- return err
- }
-
- // Copy source file to GCP
- wc := c.bucket.Object(name).NewWriter(context.Background())
- if _, err = io.Copy(wc, reader); err != nil {
- return err
- }
- if err := wc.Close(); err != nil {
- return err
- }
-
- return nil
-}
-
-// Loadfile loads file from GCP
-func (c *Client) LoadFile(name string) (data []byte, err error) {
- // Bypass if client is nil
- if c == nil {
- return nil, errors.New("cloud storage was not initialized")
- }
-
- rc, err := c.bucket.Object(name).NewReader(context.Background())
- if err != nil {
- return nil, err
- }
- defer rc.Close()
-
- data, err = ioutil.ReadAll(rc)
- if err != nil {
- return nil, err
- }
- return data, nil
-}
diff --git a/pkg/worker/cloud-storage/storage_test.go b/pkg/worker/cloud-storage/storage_test.go
deleted file mode 100644
index cd0e774e..00000000
--- a/pkg/worker/cloud-storage/storage_test.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package storage
-
-import (
- "io/ioutil"
- "log"
- "os"
- "testing"
-)
-
-func TestSaveGame(t *testing.T) {
- client := NewInitClient()
- if client == nil {
- t.Skip("Cloud storage is not initialized")
- }
- data := []byte("Test Hello")
-
- file, err := ioutil.TempFile("", "test_cloud_save")
- if err != nil {
- t.Errorf("Temp dir is not accessable %v", err)
- }
- defer os.Remove(file.Name())
-
- if err = ioutil.WriteFile(file.Name(), data, 0644); err != nil {
- t.Errorf("File is not writable %v", err)
- }
-
- err = client.SaveFile("Test", file.Name())
- if err != nil {
- log.Panic(err)
- }
- loadData, err := client.LoadFile("Test")
- if err != nil {
- log.Panic(err)
- }
- if string(data) != string(loadData) {
- log.Panic("Failed")
- }
-}
diff --git a/pkg/worker/cloud/s3.go b/pkg/worker/cloud/s3.go
new file mode 100644
index 00000000..bc5227f7
--- /dev/null
+++ b/pkg/worker/cloud/s3.go
@@ -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
+}
diff --git a/pkg/worker/cloud/s3_test.go b/pkg/worker/cloud/s3_test.go
new file mode 100644
index 00000000..9701cd9c
--- /dev/null
+++ b/pkg/worker/cloud/s3_test.go
@@ -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")
+ }
+}
diff --git a/pkg/worker/cloud/store.go b/pkg/worker/cloud/store.go
new file mode 100644
index 00000000..538983cf
--- /dev/null
+++ b/pkg/worker/cloud/store.go
@@ -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
+}
diff --git a/pkg/worker/compression/compression.go b/pkg/worker/compression/compression.go
new file mode 100644
index 00000000..e9e719a1
--- /dev/null
+++ b/pkg/worker/compression/compression.go
@@ -0,0 +1,21 @@
+package compression
+
+import (
+ "path/filepath"
+
+ "github.com/giongto35/cloud-game/v3/pkg/logger"
+ "github.com/giongto35/cloud-game/v3/pkg/worker/compression/zip"
+)
+
+type Extractor interface {
+ Extract(src string, dest string) ([]string, error)
+}
+
+func NewFromExt(path string, log *logger.Logger) Extractor {
+ switch filepath.Ext(path) {
+ case zip.Ext:
+ return zip.New(log)
+ default:
+ return nil
+ }
+}
diff --git a/pkg/worker/compression/zip/compression.go b/pkg/worker/compression/zip/compression.go
new file mode 100644
index 00000000..ad80acb0
--- /dev/null
+++ b/pkg/worker/compression/zip/compression.go
@@ -0,0 +1,134 @@
+package zip
+
+import (
+ "archive/zip"
+ "bytes"
+ "errors"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/giongto35/cloud-game/v3/pkg/logger"
+)
+
+const Ext = ".zip"
+
+var (
+ ErrorNotFound = errors.New("not found")
+ ErrorInvalidName = errors.New("invalid name")
+)
+
+type Extractor struct {
+ log *logger.Logger
+}
+
+func New(log *logger.Logger) Extractor {
+ return Extractor{
+ log: log,
+ }
+}
+
+// Compress compresses the bytes (a single file) with a name specified into a ZIP file (as bytes).
+func Compress(data []byte, name string) ([]byte, error) {
+ buf := new(bytes.Buffer)
+ w := zip.NewWriter(buf)
+
+ //w.RegisterCompressor(zip.Deflate, func(out io.Writer) (io.WriteCloser, error) {
+ // return flate.NewWriter(out, flate.BestCompression)
+ //})
+
+ z, err := w.Create(name)
+ if err != nil {
+ return nil, err
+ }
+ _, err = z.Write(data)
+ if err != nil {
+ return nil, err
+ }
+ err = w.Close()
+ if err != nil {
+ return nil, err
+ }
+ return buf.Bytes(), nil
+}
+
+// Read reads a single ZIP file from the bytes array.
+// It will return un-compressed data and the name of that file.
+func Read(zd []byte) ([]byte, string, error) {
+ r, err := zip.NewReader(bytes.NewReader(zd), int64(len(zd)))
+ if err != nil {
+ return nil, "", err
+ }
+ for _, f := range r.File {
+ if f.FileInfo().IsDir() {
+ continue
+ }
+ rc, err := f.Open()
+ if err != nil {
+ return nil, "", err
+ }
+ b, err := io.ReadAll(rc)
+ if err != nil {
+ return nil, "", err
+ }
+ if err := rc.Close(); err != nil {
+ return nil, "", err
+ }
+ return b, f.FileInfo().Name(), nil
+ }
+ return nil, "", ErrorNotFound
+}
+
+func (e Extractor) Extract(src string, dest string) (files []string, err error) {
+ r, err := zip.OpenReader(src)
+ if err != nil {
+ return files, err
+ }
+ defer r.Close()
+
+ for _, f := range r.File {
+ path := filepath.Join(dest, f.Name)
+
+ // negate ZipSlip vulnerability (http://bit.ly/2MsjAWE)
+ if !strings.HasPrefix(path, filepath.Clean(dest)+string(os.PathSeparator)) {
+ e.log.Warn().Msgf("%s is illegal path", path)
+ continue
+ }
+ // remake directory
+ if f.FileInfo().IsDir() {
+ if err := os.MkdirAll(path, os.ModePerm); err != nil {
+ e.log.Error().Err(err)
+ }
+ continue
+ }
+ // make file
+ if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
+ e.log.Error().Err(err)
+ continue
+ }
+ out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
+ if err != nil {
+ e.log.Error().Err(err)
+ continue
+ }
+ rc, err := f.Open()
+ if err != nil {
+ e.log.Error().Err(err)
+ continue
+ }
+
+ if _, err = io.Copy(out, rc); err != nil {
+ e.log.Error().Err(err)
+ _ = out.Close()
+ _ = rc.Close()
+ continue
+ }
+
+ _ = out.Close()
+ _ = rc.Close()
+
+ files = append(files, path)
+ }
+ return files, nil
+}
diff --git a/pkg/worker/compression/zip/compression_test.go b/pkg/worker/compression/zip/compression_test.go
new file mode 100644
index 00000000..f193ca15
--- /dev/null
+++ b/pkg/worker/compression/zip/compression_test.go
@@ -0,0 +1,77 @@
+package zip
+
+import (
+ cr "crypto/rand"
+ "fmt"
+ "reflect"
+ "testing"
+)
+
+func TestCompression(t *testing.T) {
+ type args struct {
+ data []byte
+ name string
+ }
+ tests := []struct {
+ name string
+ args args
+ want []byte
+ wantName string
+ wantErr bool
+ }{
+ {
+ name: "a simple compression/decompression check",
+ args: args{
+ data: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
+ name: "test",
+ },
+ want: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
+ wantName: "test",
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := Compress(tt.args.data, tt.args.name)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("Compress() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ got, name, err := Read(got)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("Compress() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if name != tt.wantName {
+ t.Errorf("Compress() got name = %v, want %v", name, tt.wantName)
+ }
+ if !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("Compress() got = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func BenchmarkCompressions(b *testing.B) {
+ benchmarks := []struct {
+ name string
+ size int
+ }{
+ {name: "compress", size: 1024 * 1024 * 1},
+ {name: "compress", size: 1024 * 1024 * 2},
+ }
+ for _, bm := range benchmarks {
+ b.Run(fmt.Sprintf("%v %v", bm.name, bm.size), func(b *testing.B) {
+ b.StopTimer()
+ dat := make([]byte, bm.size)
+ _, err := cr.Read(dat)
+ if err != nil {
+ b.Fatal(err)
+ }
+ b.StartTimer()
+ for i := 0; i < b.N; i++ {
+ _, _ = Compress(dat, "test")
+ }
+ })
+ }
+}
diff --git a/pkg/worker/coordinator.go b/pkg/worker/coordinator.go
index 45951bd1..bd5cd3e1 100644
--- a/pkg/worker/coordinator.go
+++ b/pkg/worker/coordinator.go
@@ -1,25 +1,142 @@
package worker
import (
- "github.com/giongto35/cloud-game/v2/pkg/cws"
- "github.com/gorilla/websocket"
+ "net/url"
+
+ "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/logger"
+ "github.com/giongto35/cloud-game/v3/pkg/network/webrtc"
)
-// CoordinatorClient maintains connection to coordinator.
-// We expect only one CoordinatorClient for each server.
-type CoordinatorClient struct {
- *cws.Client
+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)
+ Route(api.In[com.Uid], *api.Out)
}
-// NewCoordinatorClient returns a client connecting to coordinator
-// for coordination between different server.
-func NewCoordinatorClient(oc *websocket.Conn) *CoordinatorClient {
- if oc == nil {
- return nil
+type coordinator struct {
+ Connection
+ log *logger.Logger
+}
+
+var connector com.Client
+
+func newCoordinatorConnection(host string, conf config.Worker, addr string, log *logger.Logger) (*coordinator, error) {
+ scheme := "ws"
+ if conf.Network.Secure {
+ scheme = "wss"
+ }
+ address := url.URL{Scheme: scheme, Host: host, Path: conf.Network.Endpoint}
+
+ log.Debug().
+ Str(logger.ClientField, "c").
+ Str(logger.DirectionField, logger.MarkOut).
+ Msgf("Handshake %s", address.String())
+
+ id := com.NewUid()
+ req, err := buildConnQuery(id, conf, addr)
+ if req != "" && err == nil {
+ address.RawQuery = "data=" + req
+ } else {
+ return nil, err
}
- oClient := &CoordinatorClient{
- Client: cws.NewClient(oc),
+ conn, err := connector.Connect(address)
+ if err != nil {
+ return nil, err
}
- return oClient
+
+ clog := log.Extend(log.With().Str(logger.ClientField, "c"))
+ client := com.NewConnection[api.PT, api.In[com.Uid], api.Out, *api.Out](conn, id, clog)
+
+ return &coordinator{
+ Connection: client,
+ log: log.Extend(log.With().Str("cid", client.Id().Short())),
+ }, nil
+}
+
+func (c *coordinator) HandleRequests(w *Worker) chan struct{} {
+ ap, err := webrtc.NewApiFactory(w.conf.Webrtc, c.log, nil)
+ if err != nil {
+ c.log.Panic().Err(err).Msg("WebRTC API creation has been failed")
+ }
+
+ return c.ProcessPackets(func(x api.In[com.Uid]) (err error) {
+ var out api.Out
+
+ switch x.T {
+ case api.WebrtcInit:
+ err = api.Do(x, func(d api.WebrtcInitRequest) { out = c.HandleWebrtcInit(d, w, ap) })
+ case api.StartGame:
+ err = api.Do(x, func(d api.StartGameRequest) { out = c.HandleGameStart(d, w) })
+ case api.SaveGame:
+ err = api.Do(x, func(d api.SaveGameRequest) { out = c.HandleSaveGame(d, w) })
+ case api.LoadGame:
+ err = api.Do(x, func(d api.LoadGameRequest) { out = c.HandleLoadGame(d, w) })
+ case api.ChangePlayer:
+ err = api.Do(x, func(d api.ChangePlayerRequest) { out = c.HandleChangePlayer(d, w) })
+ case api.RecordGame:
+ 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 != (api.Out{}) {
+ w.cord.Route(x, &out)
+ }
+ return
+ })
+}
+
+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 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})
}
diff --git a/pkg/worker/coordinatorhandlers.go b/pkg/worker/coordinatorhandlers.go
new file mode 100644
index 00000000..d8e30a0e
--- /dev/null
+++ b/pkg/worker/coordinatorhandlers.go
@@ -0,0 +1,314 @@
+package worker
+
+import (
+ "encoding/base64"
+
+ "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/network/webrtc"
+ "github.com/giongto35/cloud-game/v3/pkg/worker/caged"
+ "github.com/giongto35/cloud-game/v3/pkg/worker/media"
+ "github.com/giongto35/cloud-game/v3/pkg/worker/room"
+ "github.com/goccy/go-json"
+)
+
+// buildConnQuery builds initial connection data query to a coordinator.
+func buildConnQuery(id com.Uid, conf config.Worker, address string) (string, error) {
+ addr := conf.GetPingAddr(address)
+ return toBase64Json(api.ConnectionRequest[com.Uid]{
+ Addr: addr.Hostname(),
+ Id: id,
+ IsHTTPS: conf.Server.Https,
+ PingURL: addr.String(),
+ Port: conf.GetPort(address),
+ Tag: conf.Tag,
+ Zone: conf.Network.Zone,
+ })
+}
+
+func (c *coordinator) HandleWebrtcInit(rq api.WebrtcInitRequest, w *Worker, factory *webrtc.ApiFactory) api.Out {
+ peer := webrtc.New(c.log, factory)
+ localSDP, err := peer.NewCall(w.conf.Encoder.Video.Codec, "opus", func(data any) {
+ candidate, err := toBase64Json(data)
+ if err != nil {
+ c.log.Error().Err(err).Msgf("ICE candidate encode fail for [%v]", data)
+ return
+ }
+ c.IceCandidate(candidate, rq.Id)
+ })
+ if err != nil {
+ c.log.Error().Err(err).Msg("cannot create new webrtc session")
+ return api.EmptyPacket
+ }
+ sdp, err := toBase64Json(localSDP)
+ if err != nil {
+ c.log.Error().Err(err).Msgf("SDP encode fail fro [%v]", localSDP)
+ return api.EmptyPacket
+ }
+
+ user := room.NewGameSession(rq.Id, peer) // use user uid from the coordinator
+ c.log.Info().Msgf("Peer connection: %s", user.Id())
+ w.router.AddUser(user)
+
+ return api.Out{Payload: sdp}
+}
+
+func (c *coordinator) HandleWebrtcAnswer(rq api.WebrtcAnswerRequest, w *Worker) {
+ if user := w.router.FindUser(rq.Id); user != nil {
+ if err := room.WithWebRTC(user.Session).SetRemoteSDP(rq.Sdp, fromBase64Json); err != nil {
+ c.log.Error().Err(err).Msgf("cannot set remote SDP of client [%v]", rq.Id)
+ }
+ }
+}
+
+func (c *coordinator) HandleWebrtcIceCandidate(rs api.WebrtcIceCandidateRequest, w *Worker) {
+ if user := w.router.FindUser(rs.Id); user != nil {
+ if err := room.WithWebRTC(user.Session).AddCandidate(rs.Candidate, fromBase64Json); err != nil {
+ c.log.Error().Err(err).Msgf("cannot add ICE candidate of the client [%v]", rs.Id)
+ }
+ }
+}
+
+func (c *coordinator) HandleGameStart(rq api.StartGameRequest, w *Worker) api.Out {
+ user := w.router.FindUser(rq.Id)
+ if user == nil {
+ c.log.Error().Msgf("no user [%v]", rq.Id)
+ return api.EmptyPacket
+ }
+ user.Index = rq.PlayerIndex
+
+ r := w.router.FindRoom(rq.Rid)
+
+ // +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
+ gameName := rq.Game
+ if rq.Rid != "" {
+ name := w.launcher.ExtractAppNameFromUrl(rq.Rid)
+ if name == "" {
+ c.log.Warn().Msg("couldn't decode game name from the room id")
+ return api.EmptyPacket
+ }
+ gameName = name
+ }
+
+ gameInfo, err := w.launcher.FindAppByName(gameName)
+ if err != nil {
+ c.log.Error().Err(err).Send()
+ return api.EmptyPacket
+ }
+
+ if r == nil { // new room
+ uid := rq.Rid
+ if uid == "" {
+ uid = games.GenerateRoomID(gameName)
+ }
+ game := games.GameMetadata(gameInfo)
+
+ r = room.NewRoom[*room.GameSession](uid, nil, w.router.Users(), nil)
+ r.HandleClose = func() {
+ c.CloseRoom(uid)
+ c.log.Debug().Msgf("room close request %v sent", uid)
+ }
+
+ if other := w.router.Room(); other != nil {
+ c.log.Error().Msgf("concurrent room creation: %v / %v", uid, w.router.Room().Id())
+ return api.EmptyPacket
+ }
+
+ w.router.SetRoom(r)
+ c.log.Info().Str("room", r.Id()).Str("game", game.Name).Msg("New room")
+
+ // start the emulator
+ app := room.WithEmulator(w.mana.Get(caged.Libretro))
+ app.ReloadFrontend()
+ app.SetSessionId(uid)
+ app.SetSaveOnClose(true)
+ app.EnableCloudStorage(uid, w.storage)
+ app.EnableRecording(rq.Record, rq.RecordUser, gameName)
+
+ r.SetApp(app)
+
+ m := media.NewWebRtcMediaPipe(w.conf.Encoder.Audio, w.conf.Encoder.Video, w.log)
+
+ // recreate the video encoder
+ app.VideoChangeCb(func() {
+ app.ViewportRecalculate()
+ m.VideoW, m.VideoH = app.ViewportSize()
+ m.VideoScale = app.Scale()
+
+ if m.IsInitialized() {
+ if err := m.Reinit(); err != nil {
+ c.log.Error().Err(err).Msgf("reinit fail")
+ }
+ }
+
+ data, err := api.Wrap(api.Out{
+ T: uint8(api.AppVideoChange),
+ Payload: api.AppVideoInfo{
+ W: m.VideoW,
+ H: m.VideoH,
+ A: app.AspectRatio(),
+ S: int(app.Scale()),
+ }})
+ if err != nil {
+ c.log.Error().Err(err).Msgf("wrap")
+ }
+ r.Send(data)
+ })
+
+ w.log.Info().Msgf("Starting the game: %v", gameName)
+ if err := app.Load(game, w.conf.Library.BasePath); err != nil {
+ c.log.Error().Err(err).Msgf("couldn't load the game %v", game)
+ r.Close()
+ w.router.SetRoom(nil)
+ return api.EmptyPacket
+ }
+
+ m.AudioSrcHz = app.AudioSampleRate()
+ m.AudioFrames = w.conf.Encoder.Audio.Frames
+ m.VideoW, m.VideoH = app.ViewportSize()
+ m.VideoScale = app.Scale()
+
+ r.SetMedia(m)
+
+ if err := m.Init(); err != nil {
+ c.log.Error().Err(err).Msgf("couldn't init the media")
+ r.Close()
+ w.router.SetRoom(nil)
+ return api.EmptyPacket
+ }
+
+ if app.Flipped() {
+ m.SetVideoFlip(true)
+ }
+ m.SetPixFmt(app.PixFormat())
+ m.SetRot(app.Rotation())
+
+ r.BindAppMedia()
+ r.StartApp()
+ }
+
+ c.log.Debug().Msg("Start session input poll")
+
+ needsKbMouse := r.App().KbMouseSupport()
+
+ s := room.WithWebRTC(user.Session)
+ s.OnMessage = func(data []byte) { r.App().Input(user.Index, byte(caged.RetroPad), data) }
+ if needsKbMouse {
+ _ = s.AddChannel("keyboard", func(data []byte) { r.App().Input(user.Index, byte(caged.Keyboard), data) })
+ _ = s.AddChannel("mouse", func(data []byte) { r.App().Input(user.Index, byte(caged.Mouse), data) })
+ }
+
+ c.RegisterRoom(r.Id())
+
+ response := api.StartGameResponse{
+ Room: api.Room{Rid: r.Id()},
+ Record: w.conf.Recording.Enabled,
+ KbMouse: needsKbMouse,
+ }
+ if r.App().AspectEnabled() {
+ ww, hh := r.App().ViewportSize()
+ response.AV = &api.AppVideoInfo{W: ww, H: hh, A: r.App().AspectRatio(), S: int(r.App().Scale())}
+ }
+
+ return api.Out{Payload: response}
+}
+
+// HandleTerminateSession handles cases when a user has been disconnected from the websocket of coordinator.
+func (c *coordinator) HandleTerminateSession(rq api.TerminateSessionRequest, w *Worker) {
+ if user := w.router.FindUser(rq.Id); user != nil {
+ w.router.Remove(user)
+ c.log.Debug().Msgf(">>> users: %v", w.router.Users())
+ user.Disconnect()
+ }
+}
+
+// HandleQuitGame handles cases when a user manually exits the game.
+func (c *coordinator) HandleQuitGame(rq api.GameQuitRequest, w *Worker) {
+ if user := w.router.FindUser(rq.Id); user != nil {
+ w.router.Remove(user)
+ c.log.Debug().Msgf(">>> users: %v", w.router.Users())
+ }
+}
+
+func (c *coordinator) HandleResetGame(rq api.ResetGameRequest, w *Worker) api.Out {
+ if r := w.router.FindRoom(rq.Rid); r != nil {
+ room.WithEmulator(r.App()).Reset()
+ return api.OkPacket
+ }
+ return api.ErrPacket
+}
+
+func (c *coordinator) HandleSaveGame(rq api.SaveGameRequest, w *Worker) api.Out {
+ r := w.router.FindRoom(rq.Rid)
+ if r == nil {
+ return api.ErrPacket
+ }
+ if err := room.WithEmulator(r.App()).SaveGameState(); err != nil {
+ c.log.Error().Err(err).Msg("cannot save game state")
+ return api.ErrPacket
+ }
+ return api.OkPacket
+}
+
+func (c *coordinator) HandleLoadGame(rq api.LoadGameRequest, w *Worker) api.Out {
+ r := w.router.FindRoom(rq.Rid)
+ if r == nil {
+ return api.ErrPacket
+ }
+ if err := room.WithEmulator(r.App()).RestoreGameState(); err != nil {
+ c.log.Error().Err(err).Msg("cannot load game state")
+ return api.ErrPacket
+ }
+ return api.OkPacket
+}
+
+func (c *coordinator) HandleChangePlayer(rq api.ChangePlayerRequest, w *Worker) api.Out {
+ user := w.router.FindUser(rq.Id)
+ if user == nil || w.router.FindRoom(rq.Rid) == nil {
+ return api.Out{Payload: -1} // semi-predicates
+ }
+ user.Index = rq.Index
+ w.log.Info().Msgf("Updated player index to: %d", rq.Index)
+ return api.Out{Payload: rq.Index}
+}
+
+func (c *coordinator) HandleRecordGame(rq api.RecordGameRequest, w *Worker) api.Out {
+ if !w.conf.Recording.Enabled {
+ return api.ErrPacket
+ }
+ r := w.router.FindRoom(rq.Rid)
+ if r == nil {
+ return api.ErrPacket
+ }
+ room.WithRecorder(r.App()).ToggleRecording(rq.Active, rq.User)
+ return api.OkPacket
+}
+
+// fromBase64Json decodes data from a URL-encoded Base64+JSON string.
+func fromBase64Json(data string, obj any) error {
+ b, err := base64.URLEncoding.DecodeString(data)
+ if err != nil {
+ return err
+ }
+ err = json.Unmarshal(b, obj)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// toBase64Json encodes data to a URL-encoded Base64+JSON string.
+func toBase64Json(data any) (string, error) {
+ if data == nil {
+ return "", nil
+ }
+ b, err := json.Marshal(data)
+ if err != nil {
+ return "", err
+ }
+ return base64.URLEncoding.EncodeToString(b), nil
+}
diff --git a/pkg/worker/handlers.go b/pkg/worker/handlers.go
deleted file mode 100644
index 9954bd8a..00000000
--- a/pkg/worker/handlers.go
+++ /dev/null
@@ -1,213 +0,0 @@
-package worker
-
-import (
- "context"
- "encoding/base64"
- "encoding/json"
- "log"
- "net/url"
- "os"
- "time"
-
- "github.com/giongto35/cloud-game/v2/pkg/config/worker"
- "github.com/giongto35/cloud-game/v2/pkg/cws/api"
- "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/manager/remotehttp"
- "github.com/giongto35/cloud-game/v2/pkg/games"
- "github.com/giongto35/cloud-game/v2/pkg/network/websocket"
- "github.com/giongto35/cloud-game/v2/pkg/service"
- "github.com/giongto35/cloud-game/v2/pkg/webrtc"
- storage "github.com/giongto35/cloud-game/v2/pkg/worker/cloud-storage"
- "github.com/giongto35/cloud-game/v2/pkg/worker/room"
-)
-
-type Handler struct {
- service.RunnableService
-
- address string
- // Client that connects to coordinator
- oClient *CoordinatorClient
- cfg worker.Config
- // Rooms map : RoomID -> Room
- rooms map[string]*room.Room
- // global ID of the current server
- serverID string
- // onlineStorage is client accessing to online storage (GCP)
- onlineStorage *storage.Client
- // sessions handles all sessions server is handler (key is sessionID)
- sessions map[string]*Session
-}
-
-// NewHandler returns a new server
-func NewHandler(conf worker.Config, address string) *Handler {
- // Create offline storage folder
- createOfflineStorage(conf.Emulator.Storage)
- // Init online storage
- onlineStorage := storage.NewInitClient()
- return &Handler{
- address: address,
- cfg: conf,
- onlineStorage: onlineStorage,
- rooms: map[string]*room.Room{},
- sessions: map[string]*Session{},
- }
-}
-
-// Run starts a Handler running logic
-func (h *Handler) Run() {
- coordinatorAddress := h.cfg.Worker.Network.CoordinatorAddress
- for {
- conn, err := newCoordinatorConnection(coordinatorAddress, h.cfg.Worker, h.address)
- if err != nil {
- log.Printf("Cannot connect to coordinator. %v Retrying...", err)
- time.Sleep(time.Second)
- continue
- }
- log.Printf("[worker] connected to: %v", coordinatorAddress)
-
- h.oClient = conn
- go h.oClient.Heartbeat()
- h.routes()
- h.oClient.Listen()
- // If cannot listen, reconnect to coordinator
- }
-}
-
-func (h *Handler) Shutdown(context.Context) error { return nil }
-
-func (h *Handler) Prepare() {
- if !h.cfg.Emulator.Libretro.Cores.Repo.Sync {
- return
- }
-
- log.Printf("Starting Libretro cores sync...")
- coreManager := remotehttp.NewRemoteHttpManager(h.cfg.Emulator.Libretro)
- // make a dir for cores
- dir := coreManager.Conf.GetCoresStorePath()
- if err := os.MkdirAll(dir, os.ModeDir); err != nil {
- log.Printf("error: couldn't make %v directory", dir)
- return
- }
- if err := coreManager.Sync(); err != nil {
- log.Printf("error: cores sync has failed, %v", err)
- }
-}
-
-func newCoordinatorConnection(host string, conf worker.Worker, addr string) (*CoordinatorClient, error) {
- scheme := "ws"
- if conf.Network.Secure {
- scheme = "wss"
- }
- address := url.URL{Scheme: scheme, Host: host, Path: conf.Network.Endpoint}
-
- req, err := MakeConnectionRequest(conf, addr)
- if req != "" && err == nil {
- address.RawQuery = "data=" + req
- }
-
- conn, err := websocket.Connect(address)
- if err != nil {
- return nil, err
- }
- return NewCoordinatorClient(conn), nil
-}
-
-func MakeConnectionRequest(conf worker.Worker, address string) (string, error) {
- req := api.ConnectionRequest{
- Zone: conf.Network.Zone,
- PingAddr: conf.GetPingAddr(address),
- IsHTTPS: conf.Server.Https,
- }
- rez, err := json.Marshal(req)
- if err != nil {
- return "", err
- }
- return base64.URLEncoding.EncodeToString(rez), nil
-}
-
-func (h *Handler) GetCoordinatorClient() *CoordinatorClient {
- return h.oClient
-}
-
-// detachPeerConn detaches a peerconnection from the current room.
-func (h *Handler) detachPeerConn(pc *webrtc.WebRTC) {
- log.Printf("[worker] closing peer connection")
- gameRoom := h.getRoom(pc.RoomID)
- if gameRoom == nil || gameRoom.IsEmpty() {
- return
- }
- gameRoom.RemoveSession(pc)
- if gameRoom.IsEmpty() {
- log.Printf("[worker] closing an empty room")
- gameRoom.Close()
- pc.InputChannel <- []byte{0xFF, 0xFF}
- close(pc.InputChannel)
- }
-}
-
-func (h *Handler) getRoom(roomID string) (r *room.Room) {
- r, ok := h.rooms[roomID]
- if !ok {
- return nil
- }
- return
-}
-
-// getRoom returns session from sessionID
-func (h *Handler) getSession(sessionID string) *Session {
- session, ok := h.sessions[sessionID]
- if !ok {
- return nil
- }
-
- return session
-}
-
-// detachRoom detach room from Handler
-func (h *Handler) detachRoom(roomID string) {
- delete(h.rooms, roomID)
-}
-
-// createNewRoom creates a new room
-// Return nil in case of room is existed
-func (h *Handler) createNewRoom(game games.GameMetadata, roomID string) *room.Room {
- // If the roomID doesn't have any running sessions (room was closed)
- // we spawn a new room
- if !h.isRoomBusy(roomID) {
- newRoom := room.NewRoom(roomID, game, h.onlineStorage, h.cfg)
- // TODO: Might have race condition (and it has (:)
- h.rooms[newRoom.ID] = newRoom
- return newRoom
- }
- return nil
-}
-
-// isRoomBusy check if there is any running sessions.
-// TODO: If we remove sessions from room anytime a session is closed,
-// we can check if the sessions list is empty or not.
-func (h *Handler) isRoomBusy(roomID string) bool {
- if roomID == "" {
- return false
- }
- // If no roomID is registered
- r, ok := h.rooms[roomID]
- if !ok {
- return false
- }
- return r.IsRunningSessions()
-}
-
-func (h *Handler) Close() {
- if h.oClient != nil {
- h.oClient.Close()
- }
- for _, r := range h.rooms {
- r.Close()
- }
-}
-
-func createOfflineStorage(path string) {
- log.Printf("Set storage: %v", path)
- if err := os.MkdirAll(path, 0755); err != nil {
- log.Println("Failed to create offline storage, err: ", err)
- }
-}
diff --git a/pkg/worker/http.go b/pkg/worker/http.go
deleted file mode 100644
index f7de3fa8..00000000
--- a/pkg/worker/http.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package worker
-
-import (
- "net/http"
-
- "github.com/giongto35/cloud-game/v2/pkg/config/worker"
- "github.com/giongto35/cloud-game/v2/pkg/network/httpx"
-)
-
-func NewHTTPServer(conf worker.Config) (*httpx.Server, error) {
- srv, err := httpx.NewServer(
- conf.Worker.GetAddr(),
- func(*httpx.Server) http.Handler {
- h := http.NewServeMux()
- h.HandleFunc(conf.Worker.Network.PingEndpoint, func(w http.ResponseWriter, _ *http.Request) {
- w.Header().Set("Access-Control-Allow-Origin", "*")
- _, _ = w.Write([]byte{0x65, 0x63, 0x68, 0x6f}) // echo
- })
- return h
- },
- httpx.WithServerConfig(conf.Worker.Server),
- // no need just for one route
- httpx.HttpsRedirect(false),
- httpx.WithPortRoll(true),
- httpx.WithZone(conf.Worker.Network.Zone),
- )
- if err != nil {
- return nil, err
- }
- return srv, nil
-}
diff --git a/pkg/worker/internalhandlers.go b/pkg/worker/internalhandlers.go
deleted file mode 100644
index 58755b3a..00000000
--- a/pkg/worker/internalhandlers.go
+++ /dev/null
@@ -1,271 +0,0 @@
-package worker
-
-import (
- "log"
- "strconv"
-
- webrtcConfig "github.com/giongto35/cloud-game/v2/pkg/config/webrtc"
- "github.com/giongto35/cloud-game/v2/pkg/cws"
- "github.com/giongto35/cloud-game/v2/pkg/cws/api"
- "github.com/giongto35/cloud-game/v2/pkg/games"
- "github.com/giongto35/cloud-game/v2/pkg/webrtc"
- "github.com/giongto35/cloud-game/v2/pkg/worker/room"
-)
-
-func (h *Handler) handleServerId() cws.PacketHandler {
- return func(resp cws.WSPacket) (req cws.WSPacket) {
- log.Printf("[worker] new id: %s", resp.Data)
- h.serverID = resp.Data
- return
- }
-}
-
-func (h *Handler) handleTerminateSession() cws.PacketHandler {
- return func(resp cws.WSPacket) (req cws.WSPacket) {
- log.Println("Received a terminate session ", resp.SessionID)
- session := h.getSession(resp.SessionID)
- if session != nil {
- session.Close()
- delete(h.sessions, resp.SessionID)
- h.detachPeerConn(session.peerconnection)
- } else {
- log.Printf("Error: No session for ID: %s\n", resp.SessionID)
- }
-
- return cws.EmptyPacket
- }
-}
-
-func (h *Handler) handleInitWebrtc() cws.PacketHandler {
- return func(resp cws.WSPacket) (req cws.WSPacket) {
- log.Println("Received a request to createOffer from browser via coordinator")
-
- peerconnection := webrtc.NewWebRTC().WithConfig(
- webrtcConfig.Config{Encoder: h.cfg.Encoder, Webrtc: h.cfg.Webrtc},
- )
-
- localSession, err := peerconnection.StartClient(
- // send back candidate string to browser
- func(cd string) { h.oClient.Send(api.IceCandidatePacket(cd, resp.SessionID), nil) },
- )
-
- // localSession, err := peerconnection.StartClient(initPacket.IsMobile, iceCandidates[resp.SessionID])
- // h.peerconnections[resp.SessionID] = peerconnection
-
- // Create new sessions when we have new peerconnection initialized
- session := &Session{
- peerconnection: peerconnection,
- }
- h.sessions[resp.SessionID] = session
- log.Println("Start peerconnection", resp.SessionID)
-
- if err != nil {
- log.Println("Error: Cannot create new webrtc session", err)
- return cws.EmptyPacket
- }
-
- return cws.WSPacket{ID: "offer", Data: localSession}
- }
-}
-
-func (h *Handler) handleAnswer() cws.PacketHandler {
- return func(resp cws.WSPacket) (req cws.WSPacket) {
- log.Println("Received answer SDP from browser")
- session := h.getSession(resp.SessionID)
- if session != nil {
- peerconnection := session.peerconnection
- err := peerconnection.SetRemoteSDP(resp.Data)
- if err != nil {
- log.Printf("Error: cannot set RemoteSDP of client: %v beacuse %v", resp.SessionID, err)
- }
- } else {
- log.Printf("Error: No session for ID: %s\n", resp.SessionID)
- }
- return cws.EmptyPacket
- }
-}
-
-func (h *Handler) handleIceCandidate() cws.PacketHandler {
- return func(resp cws.WSPacket) (req cws.WSPacket) {
- log.Println("Received remote Ice Candidate from browser")
- session := h.getSession(resp.SessionID)
-
- if session != nil {
- peerconnection := session.peerconnection
-
- err := peerconnection.AddCandidate(resp.Data)
- if err != nil {
- log.Println("Error: Cannot add IceCandidate of client: " + resp.SessionID)
- }
- } else {
- log.Printf("Error: No session for ID: %s\n", resp.SessionID)
- }
-
- return cws.EmptyPacket
- }
-}
-
-func (h *Handler) handleGameStart() cws.PacketHandler {
- return func(resp cws.WSPacket) (req cws.WSPacket) {
- log.Println("Received a start request from coordinator")
- session := h.getSession(resp.SessionID)
- if session == nil {
- log.Printf("error: no session with id: %s", resp.SessionID)
- return cws.EmptyPacket
- }
-
- // TODO: Standardize for all types of packet. Make WSPacket generic
- rom := api.GameStartCall{}
- if err := rom.From(resp.Data); err != nil {
- return cws.EmptyPacket
- }
- game := games.GameMetadata{Name: rom.Name, Type: rom.Type, Base: rom.Base, Path: rom.Path}
- room := h.startGameHandler(game, resp.RoomID, resp.PlayerIndex, session.peerconnection)
- session.RoomID = room.ID
- // TODO: can data race (and it does)
- h.rooms[room.ID] = room
- return cws.WSPacket{ID: api.GameStart, RoomID: room.ID}
- }
-}
-
-func (h *Handler) handleGameQuit() cws.PacketHandler {
- return func(resp cws.WSPacket) (req cws.WSPacket) {
- log.Println("Received a quit request from coordinator")
- session := h.getSession(resp.SessionID)
-
- if session != nil {
- room := h.getRoom(session.RoomID)
- // Defensive coding, check if the peerconnection is in room
- if room.IsPCInRoom(session.peerconnection) {
- h.detachPeerConn(session.peerconnection)
- }
- } else {
- log.Printf("Error: No session for ID: %s\n", resp.SessionID)
- }
-
- return cws.EmptyPacket
- }
-}
-
-func (h *Handler) handleGameSave() cws.PacketHandler {
- return func(resp cws.WSPacket) (req cws.WSPacket) {
- log.Println("Received a save game from coordinator")
- log.Println("RoomID:", resp.RoomID)
- req.ID = api.GameSave
- req.Data = "ok"
- if resp.RoomID != "" {
- room := h.getRoom(resp.RoomID)
- if room == nil {
- return
- }
- err := room.SaveGame()
- if err != nil {
- log.Println("[!] Cannot save game state: ", err)
- req.Data = "error"
- }
- } else {
- req.Data = "error"
- }
-
- return req
- }
-}
-
-func (h *Handler) handleGameLoad() cws.PacketHandler {
- return func(resp cws.WSPacket) (req cws.WSPacket) {
- log.Println("Received a load game from coordinator")
- log.Println("Loading game state")
- req.ID = api.GameLoad
- req.Data = "ok"
- if resp.RoomID != "" {
- room := h.getRoom(resp.RoomID)
- err := room.LoadGame()
- if err != nil {
- log.Println("[!] Cannot load game state: ", err)
- req.Data = "error"
- }
- } else {
- req.Data = "error"
- }
-
- return req
- }
-}
-
-func (h *Handler) handleGamePlayerSelect() cws.PacketHandler {
- return func(resp cws.WSPacket) (req cws.WSPacket) {
- log.Println("Received an update player index event from coordinator")
- req.ID = api.GamePlayerSelect
-
- room := h.getRoom(resp.RoomID)
- session := h.getSession(resp.SessionID)
- idx, err := strconv.Atoi(resp.Data)
- log.Printf("Got session %v and room %v", session, room)
-
- if room != nil && session != nil && err == nil {
- room.UpdatePlayerIndex(session.peerconnection, idx)
- req.Data = strconv.Itoa(idx)
- } else {
- req.Data = "error"
- }
-
- return req
- }
-}
-
-func (h *Handler) handleGameMultitap() cws.PacketHandler {
- return func(resp cws.WSPacket) (req cws.WSPacket) {
- log.Println("Received a multitap toggle from coordinator")
- req.ID = api.GameMultitap
- req.Data = "ok"
- if resp.RoomID != "" {
- room := h.getRoom(resp.RoomID)
- err := room.ToggleMultitap()
- if err != nil {
- log.Println("[!] Could not toggle multitap state: ", err)
- req.Data = "error"
- }
- } else {
- req.Data = "error"
- }
-
- return req
- }
-}
-
-// startGameHandler starts a game if roomID is given, if not create new room
-func (h *Handler) startGameHandler(game games.GameMetadata, existedRoomID string, playerIndex int, peerconnection *webrtc.WebRTC) *room.Room {
- log.Printf("Loading game: %v\n", game.Name)
- // If we are connecting to coordinator, request corresponding serverID based on roomID
- // TODO: check if existedRoomID is in the current server
- room := h.getRoom(existedRoomID)
- // If room is not running
- if room == nil {
- log.Println("Got Room from local ", room, " ID: ", existedRoomID)
- // Create new room and update player index
- room = h.createNewRoom(game, existedRoomID)
- room.UpdatePlayerIndex(peerconnection, playerIndex)
-
- // Wait for done signal from room
- go func() {
- <-room.Done
- h.detachRoom(room.ID)
- // send signal to coordinator that the room is closed, coordinator will remove that room
- h.oClient.Send(api.CloseRoomPacket(room.ID), nil)
- }()
- }
-
- // Attach peerconnection to room. If PC is already in room, don't detach
- log.Println("Is PC in room", room.IsPCInRoom(peerconnection))
- if !room.IsPCInRoom(peerconnection) {
- h.detachPeerConn(peerconnection)
- room.AddConnectionToRoom(peerconnection)
- }
-
- // Register room to coordinator if we are connecting to coordinator
- if room != nil && h.oClient != nil {
- h.oClient.Send(api.RegisterRoomPacket(room.ID), nil)
- }
-
- return room
-}
diff --git a/pkg/worker/media/buffer.go b/pkg/worker/media/buffer.go
new file mode 100644
index 00000000..e13bb1f0
--- /dev/null
+++ b/pkg/worker/media/buffer.go
@@ -0,0 +1,143 @@
+package media
+
+import (
+ "errors"
+
+ "github.com/giongto35/cloud-game/v3/pkg/resampler"
+)
+
+type ResampleAlgo uint8
+
+const (
+ ResampleNearest ResampleAlgo = iota
+ ResampleLinear
+ ResampleSpeex
+)
+
+type buffer struct {
+ raw samples
+ scratch samples
+ buckets []bucket
+ srcHz int
+ dstHz int
+ bi int
+ algo ResampleAlgo
+
+ resampler *resampler.Resampler
+}
+
+type bucket struct {
+ mem samples
+ ms float32
+ p int
+ dst int
+}
+
+func newBuffer(frames []float32, hz int) (*buffer, error) {
+ if hz < 2000 || len(frames) == 0 {
+ return nil, errors.New("invalid params")
+ }
+
+ buckets := make([]bucket, len(frames))
+ var total int
+ for i, ms := range frames {
+ n := stereoSamples(hz, ms)
+ buckets[i] = bucket{ms: ms, dst: n}
+ total += n
+ }
+ if total == 0 {
+ return nil, errors.New("zero buffer size")
+ }
+
+ raw := make(samples, total)
+ for i, off := 0, 0; i < len(buckets); i++ {
+ buckets[i].mem = raw[off : off+buckets[i].dst]
+ off += buckets[i].dst
+ }
+
+ return &buffer{
+ raw: raw,
+ scratch: make(samples, 5760),
+ buckets: buckets,
+ srcHz: hz,
+ dstHz: hz,
+ bi: len(buckets) - 1,
+ }, nil
+}
+
+func (b *buffer) close() {
+ if b.resampler != nil {
+ b.resampler.Destroy()
+ b.resampler = nil
+ }
+}
+
+func (b *buffer) resample(hz int, algo ResampleAlgo) error {
+ b.algo, b.dstHz = algo, hz
+ for i := range b.buckets {
+ b.buckets[i].dst = stereoSamples(hz, b.buckets[i].ms)
+ }
+ if algo == ResampleSpeex {
+ var err error
+ b.resampler, err = resampler.Init(2, b.srcHz, hz, resampler.QualityMax)
+ return err
+ }
+ return nil
+}
+
+func (b *buffer) write(s samples, onFull func(samples, float32)) int {
+ n := len(s)
+ for i := 0; i < n; {
+ cur := &b.buckets[b.bi]
+ c := copy(cur.mem[cur.p:], s[i:])
+ i += c
+ cur.p += c
+ if cur.p == len(cur.mem) {
+ onFull(b.stretch(cur.mem, cur.dst), cur.ms)
+ b.choose(n - i)
+ b.buckets[b.bi].p = 0
+ }
+ }
+ return n
+}
+
+func (b *buffer) choose(rem int) {
+ for i := len(b.buckets) - 1; i >= 0; i-- {
+ if rem >= len(b.buckets[i].mem) {
+ b.bi = i
+ return
+ }
+ }
+ b.bi = 0
+}
+
+func (b *buffer) stretch(src samples, size int) samples {
+ if len(src) == size {
+ return src
+ }
+
+ if cap(b.scratch) < size {
+ b.scratch = make(samples, size)
+ }
+ out := b.scratch[:size]
+
+ if b.algo == ResampleSpeex && b.resampler != nil {
+ if n, _ := b.resampler.Process(out, src); n > 0 {
+ for i := n; i < size; i += 2 {
+ out[i], out[i+1] = out[n-2], out[n-1]
+ }
+ return out
+ }
+ }
+
+ if b.algo == ResampleNearest {
+ resampler.Nearest(out, src)
+ } else {
+ resampler.Linear(out, src)
+ }
+ return out
+}
+
+func stereoSamples(hz int, ms float32) int {
+ return int(float32(hz)*ms/1000+0.5) * 2
+}
diff --git a/pkg/worker/media/buffer_test.go b/pkg/worker/media/buffer_test.go
new file mode 100644
index 00000000..6c8d300a
--- /dev/null
+++ b/pkg/worker/media/buffer_test.go
@@ -0,0 +1,318 @@
+package media
+
+import (
+ "reflect"
+ "testing"
+
+ "github.com/giongto35/cloud-game/v3/pkg/resampler"
+)
+
+func mustBuffer(t *testing.T, frames []float32, hz int) *buffer {
+ t.Helper()
+ buf, err := newBuffer(frames, hz)
+ if err != nil {
+ t.Fatalf("failed to create buffer: %v", err)
+ }
+ return buf
+}
+
+func samplesOf(v int16, n int) samples {
+ s := make(samples, n)
+ for i := range s {
+ s[i] = v
+ }
+ return s
+}
+
+func ramp(pairs int) samples {
+ s := make(samples, pairs*2)
+ for i := range pairs {
+ s[i*2], s[i*2+1] = int16(i), int16(i)
+ }
+ return s
+}
+
+func TestNewBuffer(t *testing.T) {
+ tests := []struct {
+ name string
+ frames []float32
+ hz int
+ wantErr bool
+ }{
+ {"valid single", []float32{10}, 48000, false},
+ {"valid multi", []float32{10, 20}, 48000, false},
+ {"hz too low", []float32{10}, 1999, true},
+ {"empty frames", []float32{}, 48000, true},
+ {"nil frames", nil, 48000, true},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ buf, err := newBuffer(tt.frames, tt.hz)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("err = %v, wantErr %v", err, tt.wantErr)
+ }
+ if buf != nil {
+ buf.close()
+ }
+ })
+ }
+}
+
+func TestBufferBucketSizes(t *testing.T) {
+ buf := mustBuffer(t, []float32{10, 20}, 48000)
+ defer buf.close()
+
+ if len(buf.buckets) != 2 {
+ t.Fatalf("got %d buckets, want 2", len(buf.buckets))
+ }
+ if n := len(buf.buckets[0].mem); n != 960 {
+ t.Errorf("bucket[0] = %d, want 960", n)
+ }
+ if n := len(buf.buckets[1].mem); n != 1920 {
+ t.Errorf("bucket[1] = %d, want 1920", n)
+ }
+}
+
+func TestBufferClose(t *testing.T) {
+ buf := mustBuffer(t, []float32{10}, 48000)
+ buf.close()
+ buf.close() // idempotent
+ if buf.resampler != nil {
+ t.Error("resampler should be nil after close")
+ }
+}
+
+func TestBufferWrite(t *testing.T) {
+ tests := []struct {
+ name string
+ writes []struct {
+ v int16
+ n int
+ }
+ want samples
+ }{
+ {
+ name: "overflow triggers callback",
+ writes: []struct {
+ v int16
+ n int
+ }{{1, 10}, {2, 20}, {3, 30}},
+ want: samples{
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ },
+ },
+ {
+ name: "partial fill",
+ writes: []struct {
+ v int16
+ n int
+ }{{1, 3}, {2, 18}, {3, 2}},
+ want: samples{1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2},
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ buf := mustBuffer(t, []float32{10, 5}, 2000)
+ defer buf.close()
+
+ var got samples
+ for _, w := range tt.writes {
+ buf.write(samplesOf(w.v, w.n), func(s samples, _ float32) { got = s })
+ }
+ if !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("\ngot: %v\nwant: %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestBufferWriteExact(t *testing.T) {
+ buf := mustBuffer(t, []float32{10}, 2000) // 40 samples
+ defer buf.close()
+
+ calls := 0
+ buf.write(samplesOf(1, 40), func(_ samples, ms float32) {
+ calls++
+ if ms != 10 {
+ t.Errorf("ms = %v, want 10", ms)
+ }
+ })
+ if calls != 1 {
+ t.Errorf("calls = %d, want 1", calls)
+ }
+}
+
+func TestBufferWriteReturn(t *testing.T) {
+ buf := mustBuffer(t, []float32{10}, 2000)
+ defer buf.close()
+
+ if n := buf.write(samplesOf(1, 100), func(samples, float32) {}); n != 100 {
+ t.Errorf("return = %d, want 100", n)
+ }
+}
+
+func TestBufferChoose(t *testing.T) {
+ buf := mustBuffer(t, []float32{20, 10, 5}, 48000) // 1920, 960, 480
+ defer buf.close()
+
+ tests := []struct{ rem, want int }{
+ {10000, 2}, {500, 2}, {479, 0}, {0, 0},
+ }
+ for _, tt := range tests {
+ buf.choose(tt.rem)
+ if buf.bi != tt.want {
+ t.Errorf("choose(%d) = %d, want %d", tt.rem, buf.bi, tt.want)
+ }
+ }
+}
+
+func TestStereoSamples(t *testing.T) {
+ tests := []struct {
+ hz int
+ ms float32
+ want int
+ }{
+ {16000, 5, 160},
+ {32768, 10, 656},
+ {32768, 2.5, 164},
+ {32768, 5, 328},
+ {44100, 10, 882},
+ {48000, 10, 960},
+ {48000, 2.5, 240},
+ }
+ for _, tt := range tests {
+ if got := stereoSamples(tt.hz, tt.ms); got != tt.want {
+ t.Errorf("stereoSamples(%d, %.0f) = %d, want %d", tt.hz, tt.ms, got, tt.want)
+ }
+ }
+}
+
+func TestStretchPassthrough(t *testing.T) {
+ buf := mustBuffer(t, []float32{10}, 48000)
+ defer buf.close()
+
+ src := samples{1, 2, 3, 4}
+ if res := buf.stretch(src, 4); &res[0] != &src[0] {
+ t.Error("expected zero-copy when sizes match")
+ }
+}
+
+func TestLinear(t *testing.T) {
+ t.Run("interpolation", func(t *testing.T) {
+ out := make(samples, 8)
+ resampler.Linear(out, samples{0, 0, 100, 100})
+ if out[2] <= 0 || out[2] >= 100 {
+ t.Errorf("middle value %d not interpolated", out[2])
+ }
+ })
+
+ t.Run("sizes", func(t *testing.T) {
+ cases := []struct{ srcPairs, dstSize int }{
+ {4, 16}, {8, 8}, {4, 8},
+ }
+ for _, tc := range cases {
+ out := make(samples, tc.dstSize)
+ resampler.Linear(out, ramp(tc.srcPairs))
+ if len(out) != tc.dstSize {
+ t.Errorf("len = %d, want %d", len(out), tc.dstSize)
+ }
+ }
+ })
+}
+
+func TestNearest(t *testing.T) {
+ tests := []struct {
+ src samples
+ want samples
+ }{
+ {samples{10, 20, 30, 40}, samples{10, 20, 10, 20, 30, 40, 30, 40}},
+ {samples{10, 20, 30, 40, 50, 60, 70, 80}, samples{10, 20, 50, 60}},
+ }
+ for _, tt := range tests {
+ out := make(samples, len(tt.want))
+ resampler.Nearest(out, tt.src)
+ if !reflect.DeepEqual(out, tt.want) {
+ t.Errorf("nearest(%v) = %v, want %v", tt.src, out, tt.want)
+ }
+ }
+}
+
+func TestSpeex(t *testing.T) {
+ buf := mustBuffer(t, []float32{10}, 48000)
+ defer buf.close()
+
+ if err := buf.resample(24000, ResampleSpeex); err != nil {
+ t.Fatal(err)
+ }
+
+ t.Run("stretch", func(t *testing.T) {
+ res := buf.stretch(samplesOf(1000, 960), 480)
+ if len(res) != 480 {
+ t.Errorf("len = %d, want 480", len(res))
+ }
+ for _, s := range res {
+ if s != 0 {
+ return
+ }
+ }
+ t.Error("output is silent")
+ })
+
+ t.Run("write", func(t *testing.T) {
+ calls := 0
+ buf.write(samplesOf(5000, 960), func(s samples, ms float32) {
+ calls++
+ if len(s) != 480 {
+ t.Errorf("len = %d, want 480", len(s))
+ }
+ if ms != 10 {
+ t.Errorf("ms = %v, want 10", ms)
+ }
+ })
+ if calls != 1 {
+ t.Errorf("calls = %d, want 1", calls)
+ }
+ })
+}
+
+func BenchmarkStretch(b *testing.B) {
+ src := samplesOf(1000, 1920) // 20ms @ 48kHz
+
+ b.Run("speex", func(b *testing.B) {
+ buf, _ := newBuffer([]float32{20}, 48000)
+ defer buf.close()
+ _ = buf.resample(24000, ResampleSpeex)
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ buf.stretch(src, 960)
+ }
+ })
+
+ b.Run("linear", func(b *testing.B) {
+ buf, _ := newBuffer([]float32{20}, 48000)
+ defer buf.close()
+ _ = buf.resample(24000, ResampleLinear)
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ buf.stretch(src, 960)
+ }
+ })
+
+ b.Run("nearest", func(b *testing.B) {
+ buf, _ := newBuffer([]float32{20}, 48000)
+ defer buf.close()
+ _ = buf.resample(24000, ResampleNearest)
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ buf.stretch(src, 960)
+ }
+ })
+}
diff --git a/pkg/worker/media/media.go b/pkg/worker/media/media.go
new file mode 100644
index 00000000..0d1407d6
--- /dev/null
+++ b/pkg/worker/media/media.go
@@ -0,0 +1,194 @@
+package media
+
+import (
+ "fmt"
+ "sync"
+ "time"
+
+ "github.com/giongto35/cloud-game/v3/pkg/config"
+ "github.com/giongto35/cloud-game/v3/pkg/encoder"
+ "github.com/giongto35/cloud-game/v3/pkg/encoder/opus"
+ "github.com/giongto35/cloud-game/v3/pkg/logger"
+ "github.com/giongto35/cloud-game/v3/pkg/worker/caged/app"
+)
+
+const audioHz = 48000
+
+type samples []int16
+
+var (
+ encoderOnce = sync.Once{}
+ opusCoder *opus.Encoder
+)
+
+func DefaultOpus() (*opus.Encoder, error) {
+ var err error
+ encoderOnce.Do(func() { opusCoder, err = opus.NewEncoder(audioHz) })
+ if err != nil {
+ return nil, err
+ }
+ if err = opusCoder.Reset(); err != nil {
+ return nil, err
+ }
+ return opusCoder, nil
+}
+
+type WebrtcMediaPipe struct {
+ a *opus.Encoder
+ v *encoder.Video
+ onAudio func([]byte, float32)
+ audioBuf *buffer
+ log *logger.Logger
+
+ mua sync.RWMutex
+ muv sync.RWMutex
+
+ aConf config.Audio
+ vConf config.Video
+
+ AudioSrcHz int
+ AudioFrames []float32
+ VideoW, VideoH int
+ VideoScale float64
+
+ initialized bool
+
+ // keep the old settings for reinit
+ oldPf uint32
+ oldRot uint
+ oldFlip bool
+}
+
+func NewWebRtcMediaPipe(ac config.Audio, vc config.Video, log *logger.Logger) *WebrtcMediaPipe {
+ return &WebrtcMediaPipe{log: log, aConf: ac, vConf: vc}
+}
+
+func (wmp *WebrtcMediaPipe) SetAudioCb(cb func([]byte, int32)) {
+ wmp.onAudio = func(bytes []byte, ms float32) {
+ cb(bytes, int32(time.Duration(ms)*time.Millisecond))
+ }
+}
+func (wmp *WebrtcMediaPipe) Destroy() {
+ v := wmp.Video()
+ if v != nil {
+ v.Stop()
+ }
+}
+func (wmp *WebrtcMediaPipe) PushAudio(audio []int16) {
+ wmp.audioBuf.write(audio, wmp.encodeAudio)
+}
+
+func (wmp *WebrtcMediaPipe) Init() error {
+ if err := wmp.initAudio(wmp.AudioSrcHz, wmp.AudioFrames); err != nil {
+ return err
+ }
+ if err := wmp.initVideo(wmp.VideoW, wmp.VideoH, wmp.VideoScale, wmp.vConf); err != nil {
+ return err
+ }
+
+ a := wmp.Audio()
+ v := wmp.Video()
+
+ if v == nil || a == nil {
+ return fmt.Errorf("could intit the encoders, v=%v a=%v", v != nil, a != nil)
+ }
+
+ wmp.log.Debug().Msgf("%v", v.Info())
+ wmp.initialized = true
+ return nil
+}
+
+func (wmp *WebrtcMediaPipe) initAudio(srcHz int, frameSizes []float32) error {
+ au, err := DefaultOpus()
+ if err != nil {
+ return fmt.Errorf("opus fail: %w", err)
+ }
+ wmp.log.Debug().Msgf("Opus: %v", au.GetInfo())
+ wmp.SetAudio(au)
+ buf, err := newBuffer(frameSizes, srcHz)
+ if err != nil {
+ return err
+ }
+ wmp.log.Debug().Msgf("Opus frames (ms): %v", frameSizes)
+ dstHz, _ := au.SampleRate()
+ if srcHz != dstHz {
+ buf.resample(dstHz, ResampleAlgo(wmp.aConf.Resampler))
+ wmp.log.Debug().Msgf("Resample %vHz -> %vHz", srcHz, dstHz)
+ }
+ wmp.audioBuf = buf
+ return nil
+}
+
+func (wmp *WebrtcMediaPipe) encodeAudio(pcm samples, ms float32) {
+ data, err := wmp.Audio().Encode(pcm)
+ if err != nil {
+ wmp.log.Error().Err(err).Msgf("opus encode fail")
+ return
+ }
+ wmp.onAudio(data, ms)
+}
+
+func (wmp *WebrtcMediaPipe) initVideo(w, h int, scale float64, conf config.Video) (err error) {
+ sw, sh := round(w, scale), round(h, scale)
+ enc, err := encoder.NewVideoEncoder(w, h, sw, sh, scale, conf, wmp.log)
+ if err != nil {
+ return err
+ }
+ if enc == nil {
+ return fmt.Errorf("broken video encoder init")
+ }
+ wmp.SetVideo(enc)
+ wmp.log.Debug().Msgf("media scale: %vx%v -> %vx%v", w, h, sw, sh)
+ return err
+}
+
+func round(x int, scale float64) int { return (int(float64(x)*scale) + 1) & ^1 }
+
+func (wmp *WebrtcMediaPipe) ProcessVideo(v app.Video) []byte {
+ return wmp.Video().Encode(encoder.InFrame(v.Frame))
+}
+
+func (wmp *WebrtcMediaPipe) Reinit() error {
+ if !wmp.initialized {
+ return nil
+ }
+
+ wmp.Video().Stop()
+ if err := wmp.initVideo(wmp.VideoW, wmp.VideoH, wmp.VideoScale, wmp.vConf); err != nil {
+ return err
+ }
+ // restore old
+ wmp.SetPixFmt(wmp.oldPf)
+ wmp.SetRot(wmp.oldRot)
+ wmp.SetVideoFlip(wmp.oldFlip)
+ return nil
+}
+
+func (wmp *WebrtcMediaPipe) IsInitialized() bool { return wmp.initialized }
+func (wmp *WebrtcMediaPipe) SetPixFmt(f uint32) { wmp.oldPf = f; wmp.v.SetPixFormat(f) }
+func (wmp *WebrtcMediaPipe) SetVideoFlip(b bool) { wmp.oldFlip = b; wmp.v.SetFlip(b) }
+func (wmp *WebrtcMediaPipe) SetRot(r uint) { wmp.oldRot = r; wmp.v.SetRot(r) }
+
+func (wmp *WebrtcMediaPipe) Video() *encoder.Video {
+ wmp.muv.RLock()
+ defer wmp.muv.RUnlock()
+ return wmp.v
+}
+
+func (wmp *WebrtcMediaPipe) SetVideo(e *encoder.Video) {
+ wmp.muv.Lock()
+ wmp.v = e
+ wmp.muv.Unlock()
+}
+
+func (wmp *WebrtcMediaPipe) Audio() *opus.Encoder {
+ wmp.mua.RLock()
+ defer wmp.mua.RUnlock()
+ return wmp.a
+}
+
+func (wmp *WebrtcMediaPipe) SetAudio(e *opus.Encoder) {
+ wmp.mua.Lock()
+ wmp.a = e
+ wmp.mua.Unlock()
+}
diff --git a/pkg/worker/media/media_test.go b/pkg/worker/media/media_test.go
new file mode 100644
index 00000000..a0fd9399
--- /dev/null
+++ b/pkg/worker/media/media_test.go
@@ -0,0 +1,112 @@
+package media
+
+import (
+ "image"
+ "math/rand/v2"
+ "testing"
+
+ "github.com/giongto35/cloud-game/v3/pkg/config"
+ "github.com/giongto35/cloud-game/v3/pkg/encoder"
+ "github.com/giongto35/cloud-game/v3/pkg/logger"
+)
+
+var l = logger.New(false)
+
+func TestEncoders(t *testing.T) {
+ tests := []struct {
+ n int
+ w, h int
+ codec encoder.VideoCodec
+ frames int
+ }{
+ {n: 3, w: 1920, h: 1080, codec: encoder.H264, frames: 60},
+ {n: 3, w: 1920, h: 1080, codec: encoder.VP8, frames: 60},
+ }
+
+ for _, test := range tests {
+ a := genTestImage(test.w, test.h, rand.Float32())
+ b := genTestImage(test.w, test.h, rand.Float32())
+ for i := 0; i < test.n; i++ {
+ run(test.w, test.h, test.codec, test.frames, a, b, t)
+ }
+ }
+}
+
+func BenchmarkH264(b *testing.B) { run(640, 480, encoder.H264, b.N, nil, nil, b) }
+func BenchmarkVP8(b *testing.B) { run(1920, 1080, encoder.VP8, b.N, nil, nil, b) }
+
+func run(w, h int, cod encoder.VideoCodec, count int, a *image.RGBA, b *image.RGBA, backend testing.TB) {
+ conf := config.Video{
+ Codec: string(cod),
+ Threads: 0,
+ H264: struct {
+ Mode string
+ Crf uint8
+ MaxRate int
+ BufSize int
+ LogLevel int32
+ Preset string
+ Profile string
+ Tune string
+ }{
+ Crf: 30,
+ LogLevel: 0,
+ Preset: "ultrafast",
+ Profile: "baseline",
+ Tune: "zerolatency",
+ },
+ Vpx: struct {
+ Bitrate uint
+ KeyframeInterval uint
+ }{
+ Bitrate: 1000,
+ KeyframeInterval: 5,
+ },
+ }
+
+ logger.SetGlobalLevel(logger.Disabled)
+ ve, err := encoder.NewVideoEncoder(w, h, w, h, 1, conf, l)
+ if err != nil {
+ backend.Error(err)
+ return
+ }
+ defer ve.Stop()
+
+ if a == nil {
+ a = genTestImage(w, h, rand.Float32())
+ }
+ if b == nil {
+ b = genTestImage(w, h, rand.Float32())
+ }
+
+ for i := range count {
+ im := a
+ if i%2 == 0 {
+ im = b
+ }
+ out := ve.Encode(encoder.InFrame{
+ Data: im.Pix,
+ Stride: im.Stride,
+ W: im.Bounds().Dx(),
+ H: im.Bounds().Dy(),
+ })
+ if out == nil {
+ backend.Fatalf("encoder closed abnormally")
+ }
+ }
+}
+
+func genTestImage(w, h int, seed float32) *image.RGBA {
+ img := image.NewRGBA(image.Rectangle{Max: image.Point{X: w, Y: h}})
+ for x := range w {
+ for y := range h {
+ i := img.PixOffset(x, y)
+ s := img.Pix[i : i+4 : i+4]
+ s[0] = uint8(seed * 255)
+ s[1] = uint8(seed * 255)
+ s[2] = uint8(seed * 255)
+ s[3] = 0xff
+ }
+ }
+ return img
+}
diff --git a/pkg/worker/recorder/ffmpegmux.go b/pkg/worker/recorder/ffmpegmux.go
new file mode 100644
index 00000000..ba543551
--- /dev/null
+++ b/pkg/worker/recorder/ffmpegmux.go
@@ -0,0 +1,98 @@
+package recorder
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+)
+
+const demuxFile = "input.txt"
+
+// createFfmpegMuxFile makes FFMPEG concat demuxer file.
+//
+// ffmpeg concat demuxer, see: https://ffmpeg.org/ffmpeg-formats.html#concat
+// example:
+//
+// !to change
+//
+// - can't read pix_fmt from ffconcat
+// - maybe change raw output to yuv420?
+// - frame durations and size can change dynamically
+// - or maybe merge encoded streams
+//
+// new:
+//
+// ffmpeg -f image2 -framerate 59 -video_size 384x224 -pixel_format rgb565le \
+// -i "./f%07d__384x224__768.raw" \
+// -ac 2 -channel_layout stereo -i audio.wav -b:a 192K \
+// -c:v libx264 -pix_fmt yuv420p -crf 20 \
+// output.mp4
+//
+// old:
+//
+// ffmpeg -f concat -i input.txt \
+// -ac 2 -channel_layout stereo -i audio.wav \
+// -b:a 192K -crf 23 -vf fps=30 -pix_fmt yuv420p \
+// out.mp4
+func createFfmpegMuxFile(dir string, fPattern string, frameTimes []time.Duration, opts Options) (er error) {
+ demux, err := newFile(dir, demuxFile)
+ if err != nil {
+ return err
+ }
+ defer func() { er = demux.Close() }()
+
+ b := strings.Builder{}
+
+ b.WriteString("ffconcat version 1.0\n")
+ b.WriteString(meta("v", "1"))
+ b.WriteString(meta("date", time.Now().Format("20060102")))
+ b.WriteString(meta("game", opts.Game))
+ b.WriteString(meta("fps", opts.Fps))
+ b.WriteString(meta("freq", opts.Frequency))
+ b.WriteString(meta("pix", opts.Pix))
+ _, err = demux.WriteString(fmt.Sprintf("%s\n", b.String()))
+ if err != nil {
+ return err
+ }
+ files, err := os.ReadDir(dir)
+ if err != nil {
+ return err
+ }
+ i := 0
+ sync := opts.Vsync && len(frameTimes) > 0
+ ext := filepath.Ext(fPattern)
+ for _, file := range files {
+ name := file.Name()
+ if !strings.HasSuffix(strings.ToLower(name), ext) {
+ continue
+ }
+ dur := 1 / opts.Fps
+ if sync && i < len(frameTimes) {
+ dur = frameTimes[i].Seconds()
+ if dur == 0 {
+ dur = 1 / opts.Fps
+ }
+ i++
+ }
+ w, h, s := ExtractFileInfo(file.Name())
+ inf := fmt.Sprintf("file %v\nduration %f\n%s%s%s", name, dur,
+ metaf("width", w), metaf("height", h), metaf("stride", s))
+ if _, err := demux.WriteString(inf); err != nil {
+ er = err
+ }
+ }
+ if err = demux.Flush(); err != nil {
+ er = err
+ }
+ return er
+}
+
+// meta adds stream_meta key value line.
+func meta(key string, value any) string { return fmt.Sprintf("stream_meta %s '%v'\n", key, value) }
+
+// metaf adds file_packet_meta key value line.
+func metaf(key string, value any) string {
+ return fmt.Sprintf("file_packet_meta %s '%v'\n", key, value)
+}
diff --git a/pkg/worker/recorder/file.go b/pkg/worker/recorder/file.go
new file mode 100644
index 00000000..2c983583
--- /dev/null
+++ b/pkg/worker/recorder/file.go
@@ -0,0 +1,74 @@
+package recorder
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "sync"
+)
+
+var defaultBufferSize = 4096
+
+type file struct {
+ io.Closer
+ sync.Mutex
+
+ f *os.File
+ w *bufio.Writer
+}
+
+func newFile(dir string, name string) (*file, error) {
+ f, err := os.OpenFile(filepath.Join(dir, name), os.O_CREATE|os.O_WRONLY, 0644)
+ if err != nil {
+ return nil, err
+ }
+ return &file{f: f, w: bufio.NewWriterSize(f, defaultBufferSize)}, nil
+}
+
+func (f *file) Flush() error {
+ f.Lock()
+ defer f.Unlock()
+ return f.w.Flush()
+}
+
+func (f *file) Close() error { return f.f.Close() }
+
+func (f *file) Size() (int64, error) {
+ f.Lock()
+ defer f.Unlock()
+ inf, err := f.f.Stat()
+ if err != nil {
+ return -1, err
+ }
+ return inf.Size(), nil
+}
+
+func (f *file) Write(data []byte) error {
+ f.Lock()
+ n, err := f.w.Write(data)
+ f.Unlock()
+ if err != nil {
+ if n < len(data) {
+ return fmt.Errorf("write size mismatch [%v!=%v], %v", n, len(data), err)
+ }
+ return err
+ }
+ return nil
+}
+
+// WriteAtStart writes data into beginning of the file.
+// Make sure that underling file doesn't use the O_APPEND directive.
+func (f *file) WriteAtStart(data []byte) error {
+ if _, err := f.f.Seek(0, 0); err != nil {
+ return err
+ }
+ return f.Write(data)
+}
+
+func (f *file) WriteString(s string) (int, error) {
+ f.Lock()
+ defer f.Unlock()
+ return f.w.WriteString(s)
+}
diff --git a/pkg/worker/recorder/options.go b/pkg/worker/recorder/options.go
new file mode 100644
index 00000000..fe4ca7ce
--- /dev/null
+++ b/pkg/worker/recorder/options.go
@@ -0,0 +1,20 @@
+package recorder
+
+type Options struct {
+ Dir string
+ Fps float64
+ W int
+ H int
+ Stride int
+ Flip bool
+ Frequency int
+ Pix string
+ Game string
+ Name string
+ Zip bool
+ Vsync bool
+}
+
+type Meta struct {
+ UserName string
+}
diff --git a/pkg/worker/recorder/rawstream.go b/pkg/worker/recorder/rawstream.go
new file mode 100644
index 00000000..26b8875c
--- /dev/null
+++ b/pkg/worker/recorder/rawstream.go
@@ -0,0 +1,66 @@
+package recorder
+
+import (
+ "fmt"
+ "log"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+ "sync/atomic"
+)
+
+type rawStream struct {
+ dir string
+ id uint32
+ wg sync.WaitGroup
+}
+
+const videoFile = "f%07d__%dx%d__%d.raw"
+
+func newRawStream(dir string) (*rawStream, error) {
+ return &rawStream{dir: dir}, nil
+}
+
+func (p *rawStream) Close() error {
+ atomic.StoreUint32(&p.id, 0)
+ p.wg.Wait()
+ return nil
+}
+
+func (p *rawStream) Write(data Video) {
+ i := atomic.AddUint32(&p.id, 1)
+ fileName := fmt.Sprintf(videoFile, i, data.Frame.W, data.Frame.H, data.Frame.Stride)
+ p.wg.Add(1)
+ go p.saveFrame(fileName, data.Frame)
+}
+
+func (p *rawStream) saveFrame(fileName string, frame Frame) {
+ file, err := os.Create(filepath.Join(p.dir, fileName))
+ if err != nil {
+ log.Printf("c err: %v", err)
+ }
+ if _, err = file.Write(frame.Data); err != nil {
+ log.Printf("f err: %v", err)
+ }
+
+ if err = file.Close(); err != nil {
+ log.Printf("fc err: %v", err)
+ }
+ p.wg.Done()
+}
+
+func ExtractFileInfo(name string) (w, h, st string) {
+ s1 := strings.Split(name, "__")
+ if len(s1) > 1 {
+ s12 := strings.Split(s1[1], "x")
+ if len(s12) > 1 {
+ w, h = s12[0], s12[1]
+ }
+ s21 := strings.TrimSuffix(s1[2], filepath.Ext(s1[2]))
+ if s21 != "" {
+ st = s21
+ }
+ }
+ return
+}
diff --git a/pkg/worker/recorder/recorder.go b/pkg/worker/recorder/recorder.go
new file mode 100644
index 00000000..8082ab50
--- /dev/null
+++ b/pkg/worker/recorder/recorder.go
@@ -0,0 +1,222 @@
+package recorder
+
+import (
+ "io"
+ "math/rand/v2"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strconv"
+ "sync"
+ "time"
+
+ "github.com/giongto35/cloud-game/v3/pkg/logger"
+ oss "github.com/giongto35/cloud-game/v3/pkg/os"
+)
+
+type Recording struct {
+ sync.Mutex
+
+ enabled bool
+
+ audio audioStream
+ video videoStream
+
+ dir string
+ saveDir string
+ meta Meta
+ opts Options
+ log *logger.Logger
+
+ vsync []time.Duration
+}
+
+// naming regexp
+var (
+ reDate = regexp.MustCompile(`%date:(.*?)%`)
+ reUser = regexp.MustCompile(`%user%`)
+ reGame = regexp.MustCompile(`%game%`)
+ reRand = regexp.MustCompile(`%rand:(\d+)%`)
+)
+
+// stream represent an output stream of the recording.
+type stream interface {
+ io.Closer
+}
+
+type audioStream interface {
+ stream
+ Write(data Audio)
+}
+type videoStream interface {
+ stream
+ Write(data Video)
+}
+
+type (
+ Audio struct {
+ Samples []int16
+ Duration time.Duration
+ }
+ Video struct {
+ Frame Frame
+ Duration time.Duration
+ }
+ Frame struct {
+ Data []byte
+ Stride int
+ W, H int
+ }
+)
+
+// NewRecording creates new media recorder for the emulator.
+func NewRecording(meta Meta, log *logger.Logger, opts Options) *Recording {
+ savePath, err := filepath.Abs(opts.Dir)
+ if err != nil {
+ log.Error().Err(err).Send()
+ }
+ if err := oss.CheckCreateDir(savePath); err != nil {
+ log.Error().Err(err).Send()
+ }
+ return &Recording{dir: savePath, meta: meta, opts: opts, log: log, vsync: []time.Duration{}}
+}
+
+func (r *Recording) Start() {
+ r.Lock()
+ defer r.Unlock()
+ r.enabled = true
+
+ r.saveDir = parseName(r.opts.Name, r.opts.Game, r.meta.UserName)
+ path := filepath.Join(r.dir, r.saveDir)
+
+ r.log.Info().Msgf("[recording] path will be [%v]", path)
+
+ if err := oss.CheckCreateDir(path); err != nil {
+ r.log.Fatal().Err(err)
+ }
+
+ audio, err := newWavStream(path, r.opts)
+ if err != nil {
+ r.log.Fatal().Err(err)
+ return
+ }
+ r.audio = audio
+ video, err := newRawStream(path)
+ if err != nil {
+ r.log.Fatal().Err(err)
+ return
+ }
+ r.video = video
+}
+
+func (r *Recording) Stop() (err error) {
+ r.Lock()
+ defer r.Unlock()
+ r.enabled = false
+ if r.audio != nil {
+ err = r.audio.Close()
+ }
+ if r.video != nil {
+ err = r.video.Close()
+ }
+
+ path := filepath.Join(r.dir, r.saveDir)
+ // FFMPEG
+ err = createFfmpegMuxFile(path, videoFile, r.vsync, r.opts)
+
+ if err == nil && r.opts.Zip && r.saveDir != "" {
+ src := filepath.Join(r.dir, r.saveDir)
+ dst := filepath.Join(src, "..", r.saveDir)
+ go func() {
+ if err := compress(src, dst); err != nil {
+ r.log.Error().Err(err).Msg("error during result compress")
+ return
+ }
+ if err := os.RemoveAll(src); err != nil {
+ r.log.Error().Err(err).Msg("error during result compress")
+ }
+ }()
+ }
+ r.vsync = []time.Duration{}
+ return err
+}
+
+func (r *Recording) Set(enable bool, user string) {
+ r.Lock()
+ r.meta.UserName = user
+ if !r.enabled && enable {
+ r.Unlock()
+ r.Start()
+ r.log.Debug().Msgf("[REC] set: +, user: %v", user)
+ r.Lock()
+ } else {
+ if r.enabled && !enable {
+ r.Unlock()
+ if err := r.Stop(); err != nil {
+ r.log.Error().Err(err).Msg("failed to stop recording")
+ }
+ r.log.Debug().Msg("recording has stopped")
+ r.Lock()
+ }
+ }
+ r.enabled = enable
+ r.Unlock()
+}
+
+func (r *Recording) SetFramerate(fps float64) { r.opts.Fps = fps }
+func (r *Recording) SetAudioFrequency(fq int) { r.opts.Frequency = fq }
+func (r *Recording) SetPixFormat(fmt uint32) {
+ pix := ""
+ switch fmt {
+ case 0:
+ pix = "rgb1555"
+ case 1:
+ pix = "brga"
+ case 2:
+ pix = "rgb565le"
+ }
+ r.opts.Pix = pix
+}
+
+func (r *Recording) Enabled() bool {
+ r.Lock()
+ defer r.Unlock()
+ return r.enabled
+}
+
+func (r *Recording) WriteVideo(frame Video) { r.video.Write(frame) }
+
+func (r *Recording) WriteAudio(audio Audio) {
+ r.audio.Write(audio)
+ r.Lock()
+ r.vsync = append(r.vsync, audio.Duration)
+ r.Unlock()
+}
+
+func parseName(name, game, user string) (out string) {
+ if d := reDate.FindStringSubmatch(name); d != nil {
+ out = reDate.ReplaceAllString(name, time.Now().Format(d[1]))
+ } else {
+ out = name
+ }
+ if rnd := reRand.FindStringSubmatch(out); rnd != nil {
+ out = reRand.ReplaceAllString(out, random(rnd[1]))
+ }
+ out = reUser.ReplaceAllString(out, user)
+ out = reGame.ReplaceAllString(out, game)
+ return
+}
+
+const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+
+func random(num string) string {
+ n, err := strconv.Atoi(num)
+ if err != nil {
+ return ""
+ }
+ b := make([]byte, n)
+ for i := range b {
+ b[i] = letterBytes[rand.Int64()%int64(len(letterBytes))]
+ }
+ return string(b)
+}
diff --git a/pkg/worker/recorder/recorder_test.go b/pkg/worker/recorder/recorder_test.go
new file mode 100644
index 00000000..d968cc34
--- /dev/null
+++ b/pkg/worker/recorder/recorder_test.go
@@ -0,0 +1,157 @@
+package recorder
+
+import (
+ "fmt"
+ "image"
+ "image/color"
+ "log"
+ "math/rand/v2"
+ "os"
+ "sync"
+ "sync/atomic"
+ "testing"
+ "time"
+
+ "github.com/giongto35/cloud-game/v3/pkg/logger"
+)
+
+func TestName(t *testing.T) {
+ dir, err := os.MkdirTemp("", "rec_test_")
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer func() {
+ if err := os.RemoveAll(dir); err != nil {
+ t.Fatal(err)
+ }
+ }()
+
+ recorder := NewRecording(
+ Meta{UserName: "test"},
+ logger.Default(),
+ Options{
+ Dir: dir,
+ Fps: 60,
+ Frequency: 10,
+ Game: fmt.Sprintf("test_game_%v", rand.Int()),
+ Name: "test",
+ Zip: false,
+ })
+ recorder.Set(true, "test_user")
+
+ iterations := 222
+
+ var imgWg, audioWg sync.WaitGroup
+ imgWg.Add(iterations)
+ audioWg.Add(iterations)
+ frame := genFrame(100, 100)
+
+ for range 222 {
+ go func() {
+ recorder.WriteVideo(Video{Frame: frame, Duration: 16 * time.Millisecond})
+ imgWg.Done()
+ }()
+ go func() {
+ recorder.WriteAudio(Audio{[]int16{0, 0, 0, 0, 0, 1, 11, 11, 11, 1}, 1})
+ audioWg.Done()
+ }()
+ }
+
+ imgWg.Wait()
+ audioWg.Wait()
+ if err := recorder.Stop(); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func BenchmarkNewRecording100x100(b *testing.B) {
+ benchmarkRecorder(100, 100, b)
+}
+
+func BenchmarkNewRecording320x240(b *testing.B) {
+ benchmarkRecorder(320, 240, b)
+}
+
+func benchmarkRecorder(w, h int, b *testing.B) {
+ b.StopTimer()
+
+ dir, err := os.MkdirTemp("", "rec_bench_")
+ if err != nil {
+ b.Fatal(err)
+ }
+ defer func() {
+ if err := os.RemoveAll(dir); err != nil {
+ b.Fatal(err)
+ }
+ }()
+
+ frame1 := genFrame(w, h)
+ frame2 := genFrame(w, h)
+
+ var bytes int64 = 0
+
+ var ticks sync.WaitGroup
+ ticks.Add(b.N * 2)
+
+ b.StartTimer()
+
+ recorder := NewRecording(
+ Meta{UserName: "test"},
+ logger.Default(),
+ Options{
+ Dir: dir,
+ Fps: 60,
+ Frequency: 10,
+ Game: fmt.Sprintf("test_game_%v", rand.Int()),
+ Name: "",
+ Zip: false,
+ })
+ recorder.Set(true, "test_user")
+ samples := []int16{0, 0, 0, 0, 0, 1, 11, 11, 11, 1}
+
+ for i := 0; i < b.N; i++ {
+ f := frame1
+ if i%2 == 0 {
+ f = frame2
+ }
+ go func() {
+ recorder.WriteVideo(Video{Frame: f, Duration: 16 * time.Millisecond})
+ atomic.AddInt64(&bytes, int64(len(f.Data)))
+ ticks.Done()
+ }()
+ go func() {
+ recorder.WriteAudio(Audio{samples, 1})
+ atomic.AddInt64(&bytes, int64(len(samples)*2))
+ ticks.Done()
+ }()
+ }
+ ticks.Wait()
+ b.SetBytes(bytes / int64(b.N))
+ if err := recorder.Stop(); err != nil {
+ b.Fatal(err)
+ }
+}
+
+func genFrame(w, h int) Frame {
+ img := image.NewRGBA(image.Rect(0, 0, w, h))
+ for x := range w {
+ for y := range h {
+ img.Set(x, y, randomColor())
+ }
+ }
+ return Frame{
+ Data: img.Pix,
+ Stride: img.Stride,
+ W: img.Bounds().Dx(),
+ H: img.Bounds().Dy(),
+ }
+}
+
+func randomColor() color.RGBA {
+ return color.RGBA{
+ R: uint8(rand.IntN(256)),
+ G: uint8(rand.IntN(256)),
+ B: uint8(rand.IntN(256)),
+ A: 255,
+ }
+}
diff --git a/pkg/worker/recorder/wavstream.go b/pkg/worker/recorder/wavstream.go
new file mode 100644
index 00000000..7b4e0b09
--- /dev/null
+++ b/pkg/worker/recorder/wavstream.go
@@ -0,0 +1,104 @@
+package recorder
+
+import (
+ "encoding/binary"
+ "errors"
+)
+
+type wavStream struct {
+ frequency int
+ wav *file
+}
+
+const (
+ audioFile = "audio.wav"
+ audioFileRIFFSize = 44
+)
+
+func newWavStream(dir string, opts Options) (*wavStream, error) {
+ wav, err := newFile(dir, audioFile)
+ if err != nil {
+ return nil, err
+ }
+ // add pad for RIFF
+ if err = wav.Write(make([]byte, audioFileRIFFSize)); err != nil {
+ return nil, err
+ }
+ return &wavStream{
+ frequency: opts.Frequency,
+ wav: wav,
+ }, nil
+}
+
+func (w *wavStream) Close() (err error) {
+ err = w.wav.Flush()
+ size, er := w.wav.Size()
+ if er != nil {
+ err = errors.Join(err, er)
+ }
+ if size > 0 {
+ // write an actual RIFF header
+ if er = w.wav.WriteAtStart(rIFFWavHeader(uint32(size), w.frequency)); er != nil {
+ err = errors.Join(err, er)
+ }
+ if er = w.wav.Flush(); er != nil {
+ err = errors.Join(err, er)
+ }
+ }
+ if er = w.wav.Close(); er != nil {
+ err = errors.Join(err, er)
+ }
+ return
+}
+
+func (w *wavStream) Write(data Audio) {
+ pcm := data.Samples
+ bs := make([]byte, len(pcm)*2)
+ // int & 0xFF + (int >> 8) & 0xFF
+ for i, ln := 0, len(pcm); i < ln; i++ {
+ binary.LittleEndian.PutUint16(bs[i*2:i*2+2], uint16(pcm[i]))
+ }
+ _ = w.wav.Write(bs)
+}
+
+// rIFFWavHeader creates RIFF WAV header.
+// See: http://soundfile.sapp.org/doc/WaveFormat
+func rIFFWavHeader(fSize uint32, fq int) []byte {
+ const (
+ bits byte = 16
+ ch byte = 2
+ chunk = 36
+ )
+ aSize := fSize - audioFileRIFFSize
+ bitrate := uint32(fq*int(ch*bits)) >> 3
+ size := aSize + chunk
+ header := [audioFileRIFFSize]byte{
+ // ChunkID
+ 'R', 'I', 'F', 'F',
+ // ChunkSize
+ byte(size & 0xff), byte((size >> 8) & 0xff), byte((size >> 16) & 0xff), byte((size >> 24) & 0xff),
+ // Format
+ 'W', 'A', 'V', 'E',
+ // Subchunk1ID
+ 'f', 'm', 't', ' ',
+ // Subchunk1Size
+ bits, 0, 0, 0,
+ // AudioFormat
+ 1, 0,
+ // NumChannels
+ ch, 0,
+ // SampleRate
+ byte(fq & 0xff), byte((fq >> 8) & 0xff), byte((fq >> 16) & 0xff), byte((fq >> 24) & 0xff),
+ // ByteRate == SampleRate * NumChannels * BitsPerSample/8
+ byte(bitrate & 0xff), byte((bitrate >> 8) & 0xff), byte((bitrate >> 16) & 0xff), byte((bitrate >> 24) & 0xff),
+ // BlockAlign == NumChannels * BitsPerSample/8
+ ch * bits >> 3, 0,
+ // BitsPerSample
+ 16, 0,
+ // Subchunk2ID
+ 'd', 'a', 't', 'a',
+ // Subchunk2Size == NumSamples * NumChannels * BitsPerSample/8
+ byte(aSize & 0xff), byte((aSize >> 8) & 0xff), byte((aSize >> 16) & 0xff), byte((aSize >> 24) & 0xff),
+ }
+ return header[:]
+}
diff --git a/pkg/worker/recorder/zipfile.go b/pkg/worker/recorder/zipfile.go
new file mode 100644
index 00000000..8ce442ae
--- /dev/null
+++ b/pkg/worker/recorder/zipfile.go
@@ -0,0 +1,62 @@
+package recorder
+
+import (
+ "archive/zip"
+ "io"
+ "os"
+ "path/filepath"
+)
+
+func compress(source, dest string) (err error) {
+ f, err := os.Create(dest + ".zip")
+ if err != nil {
+ return err
+ }
+ defer func() { err = f.Close() }()
+
+ // !to handle errors properly
+ writer := zip.NewWriter(f)
+ defer func() {
+ err = writer.Flush()
+ err = writer.Close()
+ }()
+
+ return filepath.Walk(source, func(path string, info os.FileInfo, err error) (er error) {
+ if err != nil {
+ return err
+ }
+
+ header, err := zip.FileInfoHeader(info)
+ if err != nil {
+ return err
+ }
+
+ header.Method = zip.Deflate
+
+ header.Name, err = filepath.Rel(filepath.Dir(source), path)
+ if err != nil {
+ return err
+ }
+ if info.IsDir() {
+ header.Name += "/"
+ }
+
+ headerWriter, err := writer.CreateHeader(header)
+ if err != nil {
+ return err
+ }
+
+ if info.IsDir() {
+ return nil
+ }
+
+ f, err := os.Open(path)
+ if err != nil {
+ return err
+ }
+ defer func() { er = f.Close() }()
+
+ _, err = io.Copy(headerWriter, f)
+ return err
+ })
+}
diff --git a/pkg/worker/room/cast.go b/pkg/worker/room/cast.go
new file mode 100644
index 00000000..81a6c57d
--- /dev/null
+++ b/pkg/worker/room/cast.go
@@ -0,0 +1,22 @@
+package room
+
+import (
+ "github.com/giongto35/cloud-game/v3/pkg/com"
+ "github.com/giongto35/cloud-game/v3/pkg/network/webrtc"
+ "github.com/giongto35/cloud-game/v3/pkg/worker/caged/libretro"
+)
+
+type GameRouter struct {
+ Router[*GameSession]
+}
+
+func NewGameRouter() *GameRouter {
+ u := com.NewNetMap[SessionKey, *GameSession]()
+ return &GameRouter{Router: Router[*GameSession]{users: &u}}
+}
+
+func WithEmulator(wtf any) *libretro.Caged { return wtf.(*libretro.Caged) }
+func WithRecorder(wtf any) *libretro.RecordingFrontend {
+ return (WithEmulator(wtf).Emulator).(*libretro.RecordingFrontend)
+}
+func WithWebRTC(wtf Session) *webrtc.Peer { return wtf.(*webrtc.Peer) }
diff --git a/pkg/worker/room/cast_test.go b/pkg/worker/room/cast_test.go
new file mode 100644
index 00000000..cf50dc69
--- /dev/null
+++ b/pkg/worker/room/cast_test.go
@@ -0,0 +1,36 @@
+package room
+
+import (
+ "testing"
+
+ "github.com/giongto35/cloud-game/v3/pkg/network/webrtc"
+ "github.com/giongto35/cloud-game/v3/pkg/worker/caged/libretro"
+)
+
+func TestGoodWithRecorder(t *testing.T) {
+ WithRecorder(&libretro.Caged{Emulator: &libretro.RecordingFrontend{}})
+}
+
+func TestBadWithRecorder(t *testing.T) {
+ defer func() { _ = recover() }()
+ WithEmulator(libretro.Caged{})
+ t.Errorf("no panic")
+}
+
+func TestGoodWithEmulator(t *testing.T) { WithEmulator(&libretro.Caged{}) }
+
+func TestBadWithEmulator(t *testing.T) {
+ defer func() { _ = recover() }()
+ WithEmulator(libretro.Caged{}) // not a pointer
+ t.Errorf("no panic")
+}
+
+func TestGoodWithWebRTCCast(t *testing.T) {
+ WithWebRTC(GameSession{AppSession: AppSession{Session: &webrtc.Peer{}}}.Session)
+}
+
+func TestBadWithWebRTCCast(t *testing.T) {
+ defer func() { _ = recover() }()
+ WithWebRTC(GameSession{}) // not a Session due to deep nesting
+ t.Errorf("no panic")
+}
diff --git a/pkg/worker/room/media.go b/pkg/worker/room/media.go
deleted file mode 100644
index 7a38141a..00000000
--- a/pkg/worker/room/media.go
+++ /dev/null
@@ -1,131 +0,0 @@
-package room
-
-import (
- "fmt"
- "log"
-
- "github.com/giongto35/cloud-game/v2/pkg/codec"
- encoderConfig "github.com/giongto35/cloud-game/v2/pkg/config/encoder"
- "github.com/giongto35/cloud-game/v2/pkg/encoder"
- "github.com/giongto35/cloud-game/v2/pkg/encoder/h264"
- "github.com/giongto35/cloud-game/v2/pkg/encoder/opus"
- "github.com/giongto35/cloud-game/v2/pkg/encoder/vpx"
- "github.com/giongto35/cloud-game/v2/pkg/webrtc"
-)
-
-//func (r *Room) startVoice() {
-// // broadcast voice
-// go func() {
-// for sample := range r.voiceInChannel {
-// r.voiceOutChannel <- sample
-// }
-// }()
-//
-// // fanout voice
-// go func() {
-// for sample := range r.voiceOutChannel {
-// for _, webRTC := range r.rtcSessions {
-// if webRTC.IsConnected() {
-// // NOTE: can block here
-// webRTC.VoiceOutChannel <- sample
-// }
-// }
-// }
-// for _, webRTC := range r.rtcSessions {
-// close(webRTC.VoiceOutChannel)
-// }
-// }()
-//}
-
-func (r *Room) startAudio(sampleRate int, audio encoderConfig.Audio) {
- sound, err := opus.NewEncoder(
- sampleRate,
- audio.Frequency,
- audio.Channels,
- opus.SampleBuffer(audio.Frame, sampleRate != audio.Frequency),
- // we use callback on full buffer in order to
- // send data to all the clients ASAP
- opus.CallbackOnFullBuffer(r.broadcastAudio),
- )
- if err != nil {
- log.Fatalf("error: cannot create audio encoder, %v", err)
- }
- log.Printf("OPUS: %v", sound.GetInfo())
-
- for samples := range r.audioChannel {
- sound.BufferWrite(samples)
- }
-
- log.Println("Room ", r.ID, " audio channel closed")
-}
-
-func (r *Room) broadcastAudio(audio []byte) {
- for _, webRTC := range r.rtcSessions {
- if webRTC.IsConnected() {
- // NOTE: can block here
- webRTC.AudioChannel <- audio
- }
- }
-}
-
-// startVideo processes imageChannel images with an encoder (codec) then pushes the result to WebRTC.
-func (r *Room) startVideo(width, height int, video encoderConfig.Video) {
- var enc encoder.Encoder
- var err error
-
- log.Println("Video codec:", video.Codec)
- if video.Codec == string(codec.H264) {
- enc, err = h264.NewEncoder(width, height, h264.WithOptions(h264.Options{
- Crf: video.H264.Crf,
- Tune: video.H264.Tune,
- Preset: video.H264.Preset,
- Profile: video.H264.Profile,
- LogLevel: int32(video.H264.LogLevel),
- }))
- } else {
- enc, err = vpx.NewEncoder(width, height, vpx.WithOptions(vpx.Options{
- Bitrate: video.Vpx.Bitrate,
- KeyframeInt: video.Vpx.KeyframeInterval,
- }))
- }
-
- if err != nil {
- fmt.Println("error create new encoder", err)
- return
- }
-
- r.vPipe = encoder.NewVideoPipe(enc, width, height)
- einput, eoutput := r.vPipe.Input, r.vPipe.Output
-
- go r.vPipe.Start()
- defer r.vPipe.Stop()
-
- go func() {
- defer func() {
- if r := recover(); r != nil {
- fmt.Println("Recovered when sent to close Image Channel")
- }
- }()
-
- // fanout Screen
- for data := range eoutput {
- // TODO: r.rtcSessions is rarely updated. Lock will hold down perf
- for _, webRTC := range r.rtcSessions {
- if !webRTC.IsConnected() {
- continue
- }
- // encode frame
- // fanout imageChannel
- // NOTE: can block here
- webRTC.ImageChannel <- webrtc.WebFrame{Data: data.Data, Timestamp: data.Timestamp}
- }
- }
- }()
-
- for image := range r.imageChannel {
- if len(einput) < cap(einput) {
- einput <- encoder.InFrame{Image: image.Image, Timestamp: image.Timestamp}
- }
- }
- log.Println("Room ", r.ID, " video channel closed")
-}
diff --git a/pkg/worker/room/media_test.go b/pkg/worker/room/media_test.go
deleted file mode 100644
index 76906056..00000000
--- a/pkg/worker/room/media_test.go
+++ /dev/null
@@ -1,87 +0,0 @@
-package room
-
-import (
- "image"
- "math/rand"
- "testing"
- "time"
-
- "github.com/giongto35/cloud-game/v2/pkg/codec"
- "github.com/giongto35/cloud-game/v2/pkg/encoder"
- "github.com/giongto35/cloud-game/v2/pkg/encoder/h264"
- "github.com/giongto35/cloud-game/v2/pkg/encoder/vpx"
-)
-
-func TestEncoders(t *testing.T) {
- tests := []struct {
- n int
- w, h int
- codec codec.VideoCodec
- frames int
- }{
- {n: 3, w: 1920, h: 1080, codec: codec.H264, frames: 60 * 2},
- {n: 3, w: 1920, h: 1080, codec: codec.VPX, frames: 60 * 2},
- }
-
- for _, test := range tests {
- a := genTestImage(test.w, test.h, rand.New(rand.NewSource(int64(1))).Float32())
- b := genTestImage(test.w, test.h, rand.New(rand.NewSource(int64(2))).Float32())
- for i := 0; i < test.n; i++ {
- run(test.w, test.h, test.codec, test.frames, a, b, t)
- }
- }
-}
-
-func BenchmarkH264(b *testing.B) { run(1920, 1080, codec.H264, b.N, nil, nil, b) }
-func BenchmarkVP8(b *testing.B) { run(1920, 1080, codec.VPX, b.N, nil, nil, b) }
-
-func run(w, h int, cod codec.VideoCodec, count int, a *image.RGBA, b *image.RGBA, backend testing.TB) {
- var enc encoder.Encoder
- if cod == codec.H264 {
- enc, _ = h264.NewEncoder(w, h)
- } else {
- enc, _ = vpx.NewEncoder(w, h)
- }
-
- pipe := encoder.NewVideoPipe(enc, w, h)
- go pipe.Start()
- defer pipe.Stop()
-
- if a == nil {
- a = genTestImage(w, h, rand.New(rand.NewSource(int64(1))).Float32())
- }
- if b == nil {
- b = genTestImage(w, h, rand.New(rand.NewSource(int64(2))).Float32())
- }
-
- for i := 0; i < count; i++ {
- im := a
- if i%2 == 0 {
- im = b
- }
- pipe.Input <- encoder.InFrame{Image: im}
- select {
- case _, ok := <-pipe.Output:
- if !ok {
- backend.Fatalf("encoder closed abnormally")
- }
- case <-time.After(5 * time.Second):
- backend.Fatalf("encoder didn't produce an image")
- }
- }
-}
-
-func genTestImage(w, h int, seed float32) *image.RGBA {
- img := image.NewRGBA(image.Rectangle{Max: image.Point{X: w, Y: h}})
- for x := 0; x < w; x++ {
- for y := 0; y < h; y++ {
- i := img.PixOffset(x, y)
- s := img.Pix[i : i+4 : i+4]
- s[0] = uint8(seed * 255)
- s[1] = uint8(seed * 255)
- s[2] = uint8(seed * 255)
- s[3] = 0xff
- }
- }
- return img
-}
diff --git a/pkg/worker/room/room.go b/pkg/worker/room/room.go
index 65039dc8..88380683 100644
--- a/pkg/worker/room/room.go
+++ b/pkg/worker/room/room.go
@@ -1,414 +1,173 @@
package room
import (
- "bytes"
- "encoding/gob"
- "fmt"
- "io"
- "io/ioutil"
- "log"
- "math"
- "net"
- "os"
- "path/filepath"
+ "iter"
"sync"
- "github.com/giongto35/cloud-game/v2/pkg/config/worker"
- "github.com/giongto35/cloud-game/v2/pkg/emulator"
- "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/nanoarch"
- "github.com/giongto35/cloud-game/v2/pkg/encoder"
- "github.com/giongto35/cloud-game/v2/pkg/games"
- "github.com/giongto35/cloud-game/v2/pkg/session"
- "github.com/giongto35/cloud-game/v2/pkg/webrtc"
- storage "github.com/giongto35/cloud-game/v2/pkg/worker/cloud-storage"
+ "github.com/giongto35/cloud-game/v3/pkg/worker/caged/app"
)
-// Room is a game session. multi webRTC sessions can connect to a same game.
-// A room stores all the channel for interaction between all webRTCs session and emulator
-type Room struct {
- ID string
-
- // imageChannel is image stream received from director
- imageChannel <-chan nanoarch.GameFrame
- // audioChannel is audio stream received from director
- audioChannel <-chan []int16
- // inputChannel is input stream send to director. This inputChannel is combined
- // input from webRTC + connection info (player index)
- inputChannel chan<- nanoarch.InputEvent
- // voiceInChannel is voice stream received from users
- //voiceInChannel chan []byte
- // voiceOutChannel is voice stream routed to all users
- //voiceOutChannel chan []byte
- //voiceSample [][]byte
- // State of room
- IsRunning bool
- // Done channel is to fire exit event when room is closed
- Done chan struct{}
- // List of peer connections in the room
- rtcSessions []*webrtc.WebRTC
- // NOTE: Not in use, lock rtcSessions
- sessionsLock *sync.Mutex
- // Director is emulator
- director emulator.CloudEmulator
- // Cloud storage to store room state online
- onlineStorage *storage.Client
-
- vPipe *encoder.VideoPipe
+type MediaPipe interface {
+ // Destroy frees all allocated resources.
+ Destroy()
+ // Init initializes the pipe: allocates needed resources.
+ Init() error
+ // Reinit initializes video and audio pipes with the new settings.
+ Reinit() error
+ // PushAudio pushes the 16bit PCM audio frames into an encoder.
+ // Because we need to fill the buffer, the SetAudioCb should be
+ // used in order to get the result.
+ PushAudio([]int16)
+ // ProcessVideo returns encoded video frame.
+ ProcessVideo(app.Video) []byte
+ // SetAudioCb sets a callback for encoded audio data with its frame duration (ns).
+ SetAudioCb(func(data []byte, duration int32))
}
-const (
- bufSize = 245969
- SocketAddrTmpl = "/tmp/cloudretro-retro-%s.sock"
-)
-
-// NewVideoImporter return image Channel from stream
-func NewVideoImporter(roomID string) chan nanoarch.GameFrame {
- sockAddr := fmt.Sprintf(SocketAddrTmpl, roomID)
- imgChan := make(chan nanoarch.GameFrame)
-
- l, err := net.Listen("unix", sockAddr)
- if err != nil {
- log.Fatal("listen error:", err)
- }
-
- log.Println("Creating uds server", sockAddr)
- go func(l net.Listener) {
- defer l.Close()
-
- conn, err := l.Accept()
- if err != nil {
- log.Fatal("Accept error: ", err)
- }
- defer conn.Close()
-
- log.Println("Received new conn")
- log.Println("Spawn Importer")
-
- fullBuf := make([]byte, bufSize*2)
- fullBuf = fullBuf[:0]
-
- for {
- // TODO: Not reallocate
- buf := make([]byte, bufSize)
- l, err := conn.Read(buf)
- if err != nil {
- if err != io.EOF {
- log.Printf("error: %v", err)
- }
- continue
- }
-
- buf = buf[:l]
- fullBuf = append(fullBuf, buf...)
- if len(fullBuf) >= bufSize {
- buff := bytes.NewBuffer(fullBuf)
- dec := gob.NewDecoder(buff)
-
- frame := nanoarch.GameFrame{}
- err := dec.Decode(&frame)
- if err != nil {
- log.Fatalf("%v", err)
- }
- imgChan <- frame
- fullBuf = fullBuf[bufSize:]
- }
- }
- }(l)
-
- return imgChan
+type SessionManager[T Session] interface {
+ Add(T) bool
+ Empty() bool
+ Find(string) T
+ RemoveL(T) int
+ // Reset used for proper cleanup of the resources if needed.
+ Reset()
+ Values() iter.Seq[T]
}
-// NewRoom creates a new room
-func NewRoom(roomID string, game games.GameMetadata, onlineStorage *storage.Client, cfg worker.Config) *Room {
- if roomID == "" {
- roomID = session.GenerateRoomID(game.Name)
+type Session interface {
+ Disconnect()
+ SendAudio([]byte, int32)
+ SendVideo([]byte, int32)
+ SendData([]byte)
+}
+
+type SessionKey string
+
+func (s SessionKey) String() string { return string(s) }
+func (s SessionKey) Id() string { return s.String() }
+
+type Room[T Session] struct {
+ app app.App
+ id string
+ media MediaPipe
+ users SessionManager[T]
+
+ closed bool
+ HandleClose func()
+}
+
+func NewRoom[T Session](id string, app app.App, um SessionManager[T], media MediaPipe) *Room[T] {
+ room := &Room[T]{id: id, app: app, users: um, media: media}
+ if app != nil && media != nil {
+ room.InitVideo()
+ room.InitAudio()
}
-
- log.Println("New room: ", roomID, game)
- inputChannel := make(chan nanoarch.InputEvent, 100)
-
- room := &Room{
- ID: roomID,
-
- inputChannel: inputChannel,
- imageChannel: nil,
- //voiceInChannel: make(chan []byte, 1),
- //voiceOutChannel: make(chan []byte, 1),
- rtcSessions: []*webrtc.WebRTC{},
- sessionsLock: &sync.Mutex{},
- IsRunning: true,
- onlineStorage: onlineStorage,
-
- Done: make(chan struct{}, 1),
- }
-
- // Check if room is on local storage, if not, pull from GCS to local storage
- go func(game games.GameMetadata, roomID string) {
- store := nanoarch.Storage{
- Path: cfg.Emulator.Storage,
- MainSave: roomID + ".dat",
- }
-
- // Check room is on local or fetch from server
- log.Printf("Check %s on online storage: %v", roomID, isGameOnLocal(store.MainSave))
- if err := room.saveOnlineRoomToLocal(roomID, store.MainSave); err != nil {
- log.Printf("Warn: Room %s is not in online storage, error %s", roomID, err)
- }
-
- // If not then load room or create room from local.
- log.Printf("Room %s started. GameName: %s, WithGame: %t", roomID, game.Name, cfg.Encoder.WithoutGame)
-
- // Spawn new emulator and plug-in all channels
- emuName := cfg.Emulator.GetEmulator(game.Type, game.Path)
- libretroConfig := cfg.Emulator.GetLibretroCoreConfig(emuName)
-
- if cfg.Encoder.WithoutGame {
- // Run without game, image stream is communicated over a unix socket
- imageChannel := NewVideoImporter(roomID)
- director, _, audioChannel := nanoarch.Init(roomID, false, inputChannel, store, libretroConfig)
- room.imageChannel = imageChannel
- room.director = director
- room.audioChannel = audioChannel
- } else {
- // Run without game, image stream is communicated over image channel
- director, imageChannel, audioChannel := nanoarch.Init(roomID, true, inputChannel, store, libretroConfig)
- room.imageChannel = imageChannel
- room.director = director
- room.audioChannel = audioChannel
- }
-
- gameMeta := room.director.LoadMeta(filepath.Join(game.Base, game.Path))
-
- // nwidth, nheight are the WebRTC output size
- var nwidth, nheight int
- emu, ar := cfg.Emulator, cfg.Emulator.AspectRatio
-
- if ar.Keep {
- baseAspectRatio := float64(gameMeta.BaseWidth) / float64(ar.Height)
- nwidth, nheight = resizeToAspect(baseAspectRatio, ar.Width, ar.Height)
- log.Printf("Viewport size will be changed from %dx%d (%f) -> %dx%d", ar.Width, ar.Height,
- baseAspectRatio, nwidth, nheight)
- } else {
- nwidth, nheight = gameMeta.BaseWidth, gameMeta.BaseHeight
- log.Printf("Viewport custom size is disabled, base size will be used instead %dx%d", nwidth, nheight)
- }
-
- if emu.Scale > 1 {
- nwidth, nheight = nwidth*emu.Scale, nheight*emu.Scale
- log.Printf("Viewport size has scaled to %dx%d", nwidth, nheight)
- }
-
- // set game frame size considering its orientation
- encoderW, encoderH := nwidth, nheight
- if gameMeta.Rotation.IsEven {
- encoderW, encoderH = nheight, nwidth
- }
-
- room.director.SetViewport(encoderW, encoderH)
-
- // Spawn video and audio encoding for webRTC
- go room.startVideo(encoderW, encoderH, cfg.Encoder.Video)
- go room.startAudio(gameMeta.AudioSampleRate, cfg.Encoder.Audio)
- //go room.startVoice()
- room.director.Start()
- }(game, roomID)
return room
}
-func resizeToAspect(ratio float64, sw int, sh int) (dw int, dh int) {
- // ratio is always > 0
- dw = int(math.Round(float64(sh)*ratio/2) * 2)
- dh = sh
- if dw > sw {
- dw = sw
- dh = int(math.Round(float64(sw)/ratio/2) * 2)
- }
- return
-}
-
-func isGameOnLocal(path string) bool {
- _, err := os.Open(path)
- return err == nil
-}
-
-func (r *Room) AddConnectionToRoom(peerconnection *webrtc.WebRTC) {
- peerconnection.AttachRoomID(r.ID)
- r.rtcSessions = append(r.rtcSessions, peerconnection)
-
- go r.startWebRTCSession(peerconnection)
-}
-
-func (r *Room) UpdatePlayerIndex(peerconnection *webrtc.WebRTC, playerIndex int) {
- log.Println("Updated player Index to: ", playerIndex)
- peerconnection.GameMeta.PlayerIndex = playerIndex
-}
-
-func (r *Room) startWebRTCSession(peerconnection *webrtc.WebRTC) {
- defer func() {
- if r := recover(); r != nil {
- log.Println("Warn: Recovered when sent to close inputChannel")
+func (r *Room[T]) InitAudio() {
+ r.app.SetAudioCb(func(a app.Audio) { r.media.PushAudio(a.Data) })
+ r.media.SetAudioCb(func(d []byte, l int32) {
+ for u := range r.users.Values() {
+ u.SendAudio(d, l)
}
- }()
-
- log.Println("Start WebRTC session")
- //go func() {
- //
- // // set up voice input and output. A room has multiple voice input and only one combined voice output.
- // for voiceInput := range peerconnection.VoiceInChannel {
- // // NOTE: when room is no longer running. InputChannel needs to have extra event to go inside the loop
- // if peerconnection.Done || !peerconnection.IsConnected() || !r.IsRunning {
- // break
- // }
- //
- // if peerconnection.IsConnected() {
- // r.voiceInChannel <- voiceInput
- // }
- //
- // }
- //}()
-
- // bug: when input channel here = nil, skip and finish
- for input := range peerconnection.InputChannel {
- // NOTE: when room is no longer running. InputChannel needs to have extra event to go inside the loop
- if peerconnection.Done || !peerconnection.IsConnected() || !r.IsRunning {
- break
- }
-
- if peerconnection.IsConnected() {
- select {
- case r.inputChannel <- nanoarch.InputEvent{RawState: input, PlayerIdx: peerconnection.GameMeta.PlayerIndex, ConnID: peerconnection.ID}:
- default:
- }
- }
- }
- log.Printf("[worker] peer connection is done")
+ })
}
-// RemoveSession removes a peerconnection from room and return true if there is no more room
-func (r *Room) RemoveSession(w *webrtc.WebRTC) {
- log.Println("Cleaning session: ", w.ID)
- // TODO: get list of r.rtcSessions in lock
- for i, s := range r.rtcSessions {
- log.Println("found session: ", w.ID)
- if s.ID == w.ID {
- r.rtcSessions = append(r.rtcSessions[:i], r.rtcSessions[i+1:]...)
- s.RoomID = ""
- log.Println("Removed session ", s.ID, " from room: ", r.ID)
- break
+func (r *Room[T]) InitVideo() {
+ r.app.SetVideoCb(func(v app.Video) {
+ data := r.media.ProcessVideo(v)
+ for u := range r.users.Values() {
+ u.SendVideo(data, v.Duration)
}
- }
- // Detach input. Send end signal
- select {
- case r.inputChannel <- nanoarch.InputEvent{RawState: []byte{0xFF, 0xFF}, ConnID: w.ID}:
- default:
+ })
+}
+
+func (r *Room[T]) App() app.App { return r.app }
+func (r *Room[T]) BindAppMedia() { r.InitAudio(); r.InitVideo() }
+func (r *Room[T]) Id() string { return r.id }
+func (r *Room[T]) SetApp(app app.App) { r.app = app }
+func (r *Room[T]) SetMedia(m MediaPipe) { r.media = m }
+func (r *Room[T]) StartApp() { r.app.Start() }
+func (r *Room[T]) Send(data []byte) {
+ for u := range r.users.Values() {
+ u.SendData(data)
}
}
-// TODO: Reuse for remove Session
-func (r *Room) IsPCInRoom(w *webrtc.WebRTC) bool {
- if r == nil {
- return false
- }
- for _, s := range r.rtcSessions {
- if s.ID == w.ID {
- return true
- }
- }
- return false
-}
-
-func (r *Room) Close() {
- if !r.IsRunning {
+func (r *Room[T]) Close() {
+ if r == nil || r.closed {
return
}
+ r.closed = true
- r.IsRunning = false
- log.Println("Closing room and director of room ", r.ID)
-
- // Save game before quit. Only save for game which was previous saved to avoid flooding database
- if r.isRoomExisted() {
- log.Println("Saved Game before closing room")
- // use goroutine here because SaveGame attempt to acquire a emulator lock.
- // the lock is holding before coming to close, so it will cause deadlock if SaveGame is synchronous
- go func() {
- // Save before close, so save can have correct state (Not sure) may again cause deadlock
- if err := r.SaveGame(); err != nil {
- log.Println("[error] couldn't save the game during closing")
- }
- r.director.Close()
- }()
- } else {
- r.director.Close()
+ if r.app != nil {
+ r.app.Close()
+ }
+ if r.media != nil {
+ r.media.Destroy()
+ }
+ if r.HandleClose != nil {
+ r.HandleClose()
}
- log.Println("Closing input of room ", r.ID)
- close(r.inputChannel)
- //close(r.voiceOutChannel)
- //close(r.voiceInChannel)
- close(r.Done)
- // Close here is a bit wrong because this read channel
- // Just dont close it, let it be gc
- //close(r.imageChannel)
- //close(r.audioChannel)
}
-func (r *Room) isRoomExisted() bool {
- // Check if room is in online storage
- _, err := r.onlineStorage.LoadFile(r.ID)
- if err == nil {
- return true
- }
- return isGameOnLocal(r.director.GetHashPath())
+// Router tracks and routes freshly connected users to an app room.
+// Rooms and users has 1-to-n relationship.
+type Router[T Session] struct {
+ room *Room[T]
+ users SessionManager[T]
+ mu sync.Mutex
}
-// SaveGame will save game to local and trigger a callback to store game on onlineStorage, so the game can be accessed later
-func (r *Room) SaveGame() error {
- onlineSaveFunc := func() error {
- // Try to save the game to gCloud
- if err := r.onlineStorage.SaveFile(r.ID, r.director.GetHashPath()); err != nil {
- return err
- }
-
- return nil
- }
-
- // TODO: Move to game view
- if err := r.director.SaveGame(onlineSaveFunc); err != nil {
- return err
- }
-
- return nil
-}
-
-// saveOnlineRoomToLocal save online room to local.
-// !Supports only one file of main save state.
-func (r *Room) saveOnlineRoomToLocal(roomID string, savePath string) error {
- log.Println("Check if game is on cloud storage")
- // If the game is not on local server
- // Try to load from gcloud
- data, err := r.onlineStorage.LoadFile(roomID)
- if err != nil {
- return err
- }
- // Save the data fetched from gcloud to local server
- if data != nil {
- _ = ioutil.WriteFile(savePath, data, 0644)
+func (r *Router[T]) FindRoom(id string) *Room[T] {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ if r.room != nil && r.room.Id() == id {
+ return r.room
}
return nil
}
-func (r *Room) LoadGame() error { return r.director.LoadGame() }
-
-func (r *Room) ToggleMultitap() error { return r.director.ToggleMultitap() }
-
-func (r *Room) IsEmpty() bool { return len(r.rtcSessions) == 0 }
-
-func (r *Room) IsRunningSessions() bool {
- // If there is running session
- for _, s := range r.rtcSessions {
- if s.IsConnected() {
- return true
- }
+func (r *Router[T]) Remove(user T) {
+ if left := r.users.RemoveL(user); left == 0 {
+ r.Close()
+ r.SetRoom(nil) // !to remove
}
-
- return false
+}
+
+func (r *Router[T]) AddUser(user T) { r.users.Add(user) }
+func (r *Router[T]) Close() { r.mu.Lock(); r.room.Close(); r.room = nil; r.mu.Unlock() }
+func (r *Router[T]) FindUser(uid string) T { return r.users.Find(uid) }
+func (r *Router[T]) Room() *Room[T] { r.mu.Lock(); defer r.mu.Unlock(); return r.room }
+func (r *Router[T]) SetRoom(room *Room[T]) { r.mu.Lock(); r.room = room; r.mu.Unlock() }
+func (r *Router[T]) HasRoom() bool { r.mu.Lock(); defer r.mu.Unlock(); return r.room != nil }
+func (r *Router[T]) Users() SessionManager[T] { return r.users }
+func (r *Router[T]) Reset() {
+ r.mu.Lock()
+ if r.room != nil {
+ r.room.Close()
+ r.room = nil
+ }
+ for u := range r.users.Values() {
+ u.Disconnect()
+ }
+ r.users.Reset()
+ r.mu.Unlock()
+}
+
+type AppSession struct {
+ Session
+ uid SessionKey
+}
+
+func (p AppSession) Id() SessionKey { return p.uid }
+
+type GameSession struct {
+ AppSession
+ Index int // track user Index (i.e. player 1,2,3,4 select)
+}
+
+func NewGameSession(id string, s Session) *GameSession {
+ return &GameSession{AppSession: AppSession{uid: SessionKey(id), Session: s}}
}
diff --git a/pkg/worker/room/room_test.go b/pkg/worker/room/room_test.go
index 1f5ee55e..7a537d69 100644
--- a/pkg/worker/room/room_test.go
+++ b/pkg/worker/room/room_test.go
@@ -8,7 +8,6 @@ import (
"image/color"
"image/draw"
"image/png"
- "io/ioutil"
"log"
"os"
"path/filepath"
@@ -17,17 +16,23 @@ import (
"testing"
"time"
- "github.com/giongto35/cloud-game/v2/pkg/codec"
- "github.com/giongto35/cloud-game/v2/pkg/config"
- "github.com/giongto35/cloud-game/v2/pkg/config/worker"
- "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/manager/remotehttp"
- "github.com/giongto35/cloud-game/v2/pkg/encoder"
- "github.com/giongto35/cloud-game/v2/pkg/games"
- "github.com/giongto35/cloud-game/v2/pkg/thread"
- storage "github.com/giongto35/cloud-game/v2/pkg/worker/cloud-storage"
+ "github.com/giongto35/cloud-game/v3/pkg/com"
+ "github.com/giongto35/cloud-game/v3/pkg/config"
+ "github.com/giongto35/cloud-game/v3/pkg/encoder"
+ "github.com/giongto35/cloud-game/v3/pkg/encoder/color/bgra"
+ "github.com/giongto35/cloud-game/v3/pkg/encoder/color/rgb565"
+ "github.com/giongto35/cloud-game/v3/pkg/encoder/color/rgba"
+ "github.com/giongto35/cloud-game/v3/pkg/games"
+ "github.com/giongto35/cloud-game/v3/pkg/logger"
+ "github.com/giongto35/cloud-game/v3/pkg/worker/caged"
+ "github.com/giongto35/cloud-game/v3/pkg/worker/caged/app"
+ "github.com/giongto35/cloud-game/v3/pkg/worker/media"
+ "github.com/giongto35/cloud-game/v3/pkg/worker/thread"
"golang.org/x/image/font"
"golang.org/x/image/font/basicfont"
"golang.org/x/image/math/fixed"
+
+ _ "github.com/giongto35/cloud-game/v3/test"
)
var (
@@ -36,184 +41,149 @@ var (
autoGlContext bool
)
-type roomMock struct {
- Room
+type testRoom struct {
+ *Room[*GameSession]
+ started bool
}
-type roomMockConfig struct {
+type codec = encoder.VideoCodec
+
+type conf struct {
roomName string
- gamesPath string
game games.GameMetadata
- vCodec codec.VideoCodec
+ codec codec
autoGlContext bool
+ autoAppStart bool
+ noLog bool
+}
+
+func (r testRoom) Close() {
+ r.Room.Close()
+ time.Sleep(2 * time.Second) // hack: wait room destruction (atm impossible to tell)
+}
+
+func (r testRoom) WaitFrame(n int) app.RawFrame {
+ var wg sync.WaitGroup
+ wg.Add(1)
+ target := app.RawFrame{}
+ WithEmulator(r.app).SetVideoCb(func(v app.Video) {
+ if n == 1 {
+ target = v.Frame
+ target.Data = make([]byte, len(v.Frame.Data))
+ copy(target.Data, v.Frame.Data)
+ wg.Done()
+ }
+ n--
+ })
+ if !r.started {
+ r.StartApp()
+ }
+ wg.Wait()
+ return target
+}
+
+type testParams struct {
+ system string
+ game games.GameMetadata
+ codecs []codec
+ frames int
+ color int
}
// Store absolute path to test games
-var whereIsGames = getRootPath() + "assets/games/"
-var whereIsConfigs = getRootPath() + "configs/"
var testTempDir = filepath.Join(os.TempDir(), "cloud-game-core-tests")
-func init() {
- runtime.LockOSThread()
-}
+// games
+var (
+ alwas = games.GameMetadata{Name: "Alwa's Awakening (Demo)", Type: "nes", Path: "nes/Alwa's Awakening (Demo).nes", System: "nes"}
+ sushi = games.GameMetadata{Name: "Sushi The Cat", Type: "gba", Path: "gba/Sushi The Cat.gba", System: "gba"}
+ fd = games.GameMetadata{Name: "Florian Demo", Type: "n64", Path: "n64/Sample Demo by Florian (PD).z64", System: "n64"}
+ rogue = games.GameMetadata{Name: "Rogue", Type: "dos", Path: "dos/rogue.zip", System: "dos"}
+)
func TestMain(m *testing.M) {
flag.BoolVar(&renderFrames, "renderFrames", false, "Render frames for eye testing purposes")
flag.StringVar(&outputPath, "outputPath", "./", "Output path for generated files")
flag.BoolVar(&autoGlContext, "autoGlContext", false, "Set auto GL context choose for headless machines")
- thread.MainWrapMaybe(func() { os.Exit(m.Run()) })
+ thread.Wrap(func() { os.Exit(m.Run()) })
}
func TestRoom(t *testing.T) {
- tests := []struct {
- roomName string
- game games.GameMetadata
- vCodec codec.VideoCodec
- frames int
- }{
- {
- game: games.GameMetadata{
- Name: "Super Mario Bros",
- Type: "nes",
- Path: "Super Mario Bros.nes",
- },
- vCodec: codec.VPX,
- frames: 5,
- },
+ tests := []testParams{
+ {game: alwas, codecs: []codec{encoder.H264, encoder.VP8, encoder.VP9}, frames: 300},
}
for _, test := range tests {
- room := getRoomMock(roomMockConfig{
- roomName: test.roomName,
- gamesPath: whereIsGames,
- game: test.game,
- vCodec: test.vCodec,
- })
- t.Logf("The game [%v] has been loaded", test.game.Name)
- waitNFrames(test.frames, room.vPipe.Output)
- room.Close()
- }
- // hack: wait room destruction
- time.Sleep(2 * time.Second)
-}
-
-func TestRoomWithGL(t *testing.T) {
- tests := []struct {
- game games.GameMetadata
- vCodec codec.VideoCodec
- frames int
- }{
- {
- game: games.GameMetadata{
- Name: "Sample Demo by Florian (PD)",
- Type: "n64",
- Path: "Sample Demo by Florian (PD).z64",
- },
- vCodec: codec.VPX,
- frames: 50,
- },
- }
-
- run := func() {
- for _, test := range tests {
- room := getRoomMock(roomMockConfig{
- gamesPath: whereIsGames,
- game: test.game,
- vCodec: test.vCodec,
- })
- t.Logf("The game [%v] has been loaded", test.game.Name)
- waitNFrames(test.frames, room.vPipe.Output)
+ for _, codec := range test.codecs {
+ room := room(conf{codec: codec, game: test.game})
+ room.WaitFrame(test.frames)
room.Close()
}
- // hack: wait room destruction
- time.Sleep(2 * time.Second)
}
-
- thread.MainMaybe(run)
}
-func TestAllEmulatorRooms(t *testing.T) {
- tests := []struct {
- game games.GameMetadata
- frames int
- }{
- {
- game: games.GameMetadata{Name: "Sushi", Type: "gba", Path: "Sushi The Cat.gba"},
- frames: 100,
- },
- {
- game: games.GameMetadata{Name: "Mario", Type: "nes", Path: "Super Mario Bros.nes"},
- frames: 50,
- },
- {
- game: games.GameMetadata{Name: "Florian Demo", Type: "n64", Path: "Sample Demo by Florian (PD).z64"},
- frames: 50,
- },
+func TestAll(t *testing.T) {
+ tests := []testParams{
+ {game: sushi, frames: 150, color: 2},
+ {game: alwas, frames: 50, color: 1},
+ {game: fd, frames: 50, system: "gl", color: 1},
+ {game: rogue, frames: 33, color: 1},
}
crc32q := crc32.MakeTable(0xD5828281)
for _, test := range tests {
- room := getRoomMock(roomMockConfig{
- gamesPath: whereIsGames,
- game: test.game,
- vCodec: codec.VPX,
- autoGlContext: autoGlContext,
- })
- t.Logf("The game [%v] has been loaded", test.game.Name)
- waitNFrames(test.frames, room.vPipe.Output)
+ var frame app.RawFrame
+ room := room(conf{game: test.game, codec: encoder.VP8, autoGlContext: autoGlContext, autoAppStart: false})
+ flip := test.system == "gl"
+ thread.Main(func() { frame = room.WaitFrame(test.frames) })
+ room.Close()
if renderFrames {
- img := room.director.GetViewport().(*image.RGBA)
- tag := fmt.Sprintf("%v-%v-0x%08x", runtime.GOOS, test.game.Type, crc32.Checksum(img.Pix, crc32q))
- dumpCanvas(img, tag, fmt.Sprintf("%v [%v]", tag, test.frames), outputPath)
+ rect := image.Rect(0, 0, frame.W, frame.H)
+ var src image.Image
+ src1 := bgra.NewBGRA(rect)
+ src1.Pix = frame.Data
+ src1.Stride = frame.Stride
+ src = src1
+ if test.color == 2 {
+ src2 := rgb565.NewRGB565(rect)
+ src2.Pix = frame.Data
+ src2.Stride = frame.Stride
+ src = src2
+ }
+ dst := rgba.ToRGBA(src, flip)
+ tag := fmt.Sprintf("%v-%v-0x%08x", runtime.GOOS, test.game.Type, crc32.Checksum(frame.Data, crc32q))
+ dumpCanvas(dst, tag, fmt.Sprintf("%v [%v]", tag, test.frames), outputPath)
}
-
- room.Close()
- // hack: wait room destruction
- time.Sleep(2 * time.Second)
}
}
-// enforce image.RGBA to remove alpha channel when encoding PNGs
-type opaqueRGBA struct {
- *image.RGBA
-}
-
-func (*opaqueRGBA) Opaque() bool {
- return true
-}
-
-func dumpCanvas(f *image.RGBA, name string, caption string, path string) {
- frame := *f
-
+func dumpCanvas(frame *image.RGBA, name string, caption string, path string) {
// slap 'em caption
- if len(caption) > 0 {
- draw.Draw(&frame, image.Rect(8, 8, 8+len(caption)*7+3, 24), &image.Uniform{C: color.RGBA{}}, image.Point{}, draw.Src)
+ if caption != "" {
+ draw.Draw(frame, image.Rect(8, 8, 8+len(caption)*7+3, 24), &image.Uniform{C: color.RGBA{}}, image.Point{}, draw.Src)
(&font.Drawer{
- Dst: &frame,
+ Dst: frame,
Src: image.NewUniform(color.RGBA{R: 255, G: 255, B: 255, A: 255}),
Face: basicfont.Face7x13,
Dot: fixed.Point26_6{X: fixed.Int26_6(10 * 64), Y: fixed.Int26_6(20 * 64)},
}).DrawString(caption)
}
- var outPath string
- if len(path) > 0 {
+ outPath := testTempDir
+ if path != "" {
outPath = path
- } else {
- outPath = testTempDir
}
- // really like Go's error handling
if err := os.MkdirAll(outPath, 0770); err != nil {
log.Printf("Couldn't create target dir for the output images, %v", err)
return
}
if f, err := os.Create(filepath.Join(outPath, name+".png")); err == nil {
- if err = png.Encode(f, &opaqueRGBA{&frame}); err != nil {
+ if err = png.Encode(f, frame); err != nil {
log.Printf("Couldn't encode the image, %v", err)
}
_ = f.Close()
@@ -222,154 +192,86 @@ func dumpCanvas(f *image.RGBA, name string, caption string, path string) {
}
}
-// getRoomMock returns mocked Room struct.
-func getRoomMock(cfg roomMockConfig) roomMock {
- cfg.game.Path = cfg.gamesPath + cfg.game.Path
-
- var conf worker.Config
- if err := config.LoadConfig(&conf, whereIsConfigs); err != nil {
+// room returns mocked Room struct.
+func room(cfg conf) testRoom {
+ var conf config.WorkerConfig
+ if _, err := config.LoadConfig(&conf, ""); err != nil {
panic(err)
}
- fixEmulators(&conf, cfg.autoGlContext)
- // sync cores
- coreManager := remotehttp.NewRemoteHttpManager(conf.Emulator.Libretro)
- if err := coreManager.Sync(); err != nil {
- log.Printf("error: cores sync has failed, %v", err)
- }
- conf.Encoder.Video.Codec = string(cfg.vCodec)
- room := NewRoom(cfg.roomName, cfg.game, storage.NewInitClient(), conf)
+ conf.Emulator.Libretro.Cores.Repo.ExtLock = expand("tests", ".cr", "cloud-game.lock")
+ conf.Emulator.LocalPath = expand("tests", conf.Emulator.LocalPath)
+ conf.Emulator.Storage = expand("tests", "storage")
- // loop-wait the room initialization
- var init sync.WaitGroup
- init.Add(1)
- wasted := 0
- go func() {
- sleepDeltaMs := 10
- for room.director == nil || room.vPipe == nil {
- time.Sleep(time.Duration(sleepDeltaMs) * time.Millisecond)
- wasted++
- if wasted > 1000 {
- break
- }
- }
- init.Done()
- }()
- init.Wait()
+ conf.Encoder.Video.Codec = string(cfg.codec)
- return roomMock{*room}
-}
-
-// fixEmulators makes absolute game paths in global GameList and passes GL context config.
-// hack: emulator paths should be absolute and visible to the tests.
-func fixEmulators(config *worker.Config, autoGlContext bool) {
- rootPath := getRootPath()
-
- config.Emulator.Libretro.Cores.Paths.Libs =
- filepath.FromSlash(rootPath + config.Emulator.Libretro.Cores.Paths.Libs)
- config.Emulator.Libretro.Cores.Paths.Configs =
- filepath.FromSlash(rootPath + config.Emulator.Libretro.Cores.Paths.Configs)
-
- for k, conf := range config.Emulator.Libretro.Cores.List {
- if conf.IsGlAllowed && autoGlContext {
- conf.AutoGlContext = true
- }
- config.Emulator.Libretro.Cores.List[k] = conf
- }
-}
-
-// getRootPath returns absolute path to the assets directory.
-func getRootPath() string {
- p, _ := filepath.Abs("../../../")
- return p + string(filepath.Separator)
-}
-
-func waitNFrames(n int, ch chan encoder.OutFrame) {
- var frames sync.WaitGroup
- frames.Add(n)
-
- done := false
- go func() {
- for range ch {
- if done {
- break
- }
- frames.Done()
- }
- }()
-
- frames.Wait()
- done = true
-}
-
-// benchmarkRoom measures app performance for n emulation frames.
-// Measure period: the room initialization, n emulated and encoded frames, the room shutdown.
-func benchmarkRoom(rom games.GameMetadata, codec codec.VideoCodec, frames int, suppressOutput bool, b *testing.B) {
- if suppressOutput {
- log.SetOutput(ioutil.Discard)
- os.Stdout, _ = os.Open(os.DevNull)
+ l := logger.NewConsole(conf.Worker.Debug, "w", false)
+ if cfg.noLog {
+ logger.SetGlobalLevel(logger.Disabled)
}
- for i := 0; i < b.N; i++ {
- room := getRoomMock(roomMockConfig{
- gamesPath: whereIsGames,
- game: rom,
- vCodec: codec,
- })
- waitNFrames(frames, room.vPipe.Output)
- room.Close()
+ id := cfg.roomName
+ if id == "" {
+ id = games.GenerateRoomID(cfg.game.Name)
}
+
+ manager := caged.NewManager(l)
+ if err := manager.Load(caged.Libretro, conf); err != nil {
+ l.Fatal().Msgf("couldn't cage libretro: %v", err)
+ }
+
+ emu := WithEmulator(manager.Get(caged.Libretro))
+ emu.ReloadFrontend()
+ emu.SetSessionId(id)
+ if err := emu.Load(cfg.game, conf.Library.BasePath); err != nil {
+ l.Fatal().Err(err).Msgf("couldn't load the game %v", cfg.game)
+ }
+
+ m := media.NewWebRtcMediaPipe(conf.Encoder.Audio, conf.Encoder.Video, l)
+ m.AudioSrcHz = emu.AudioSampleRate()
+ m.AudioFrames = conf.Encoder.Audio.Frames
+ m.VideoW, m.VideoH = emu.ViewportSize()
+ m.VideoScale = emu.Scale()
+ if err := m.Init(); err != nil {
+ l.Fatal().Err(err).Msgf("no init")
+ }
+
+ room := NewRoom[*GameSession](id, emu, &com.NetMap[SessionKey, *GameSession]{}, m)
+ if cfg.autoAppStart {
+ room.StartApp()
+ }
+
+ return testRoom{Room: room, started: cfg.autoAppStart}
}
// Measures emulation performance of various
// emulators and encoding options.
func BenchmarkRoom(b *testing.B) {
- benches := []struct {
- system string
- game games.GameMetadata
- codecs []codec.VideoCodec
- frames int
- }{
+ benches := []testParams{
// warm up
- {
- system: "gba",
- game: games.GameMetadata{
- Name: "Sushi The Cat",
- Type: "gba",
- Path: "Sushi The Cat.gba",
- },
- codecs: []codec.VideoCodec{codec.VPX},
- frames: 50,
- },
- {
- system: "gba",
- game: games.GameMetadata{
- Name: "Sushi The Cat",
- Type: "gba",
- Path: "Sushi The Cat.gba",
- },
- codecs: []codec.VideoCodec{codec.VPX, codec.H264},
- frames: 100,
- },
- {
- system: "nes",
- game: games.GameMetadata{
- Name: "Super Mario Bros",
- Type: "nes",
- Path: "Super Mario Bros.nes",
- },
- codecs: []codec.VideoCodec{codec.VPX, codec.H264},
- frames: 100,
- },
+ {system: "gba", game: sushi, codecs: []codec{encoder.VP8, encoder.VP9}, frames: 50},
+ {system: "gba", game: sushi, codecs: []codec{encoder.VP8, encoder.H264}, frames: 100},
+ {system: "nes", game: alwas, codecs: []codec{encoder.VP8, encoder.H264}, frames: 100},
}
for _, bench := range benches {
for _, cod := range bench.codecs {
b.Run(fmt.Sprintf("%s-%v-%d", bench.system, cod, bench.frames), func(b *testing.B) {
- benchmarkRoom(bench.game, cod, bench.frames, true, b)
+ for i := 0; i < b.N; i++ {
+ b.StopTimer()
+ room := room(conf{game: bench.game, codec: cod, noLog: true})
+ b.StartTimer()
+ room.WaitFrame(bench.frames)
+ b.StopTimer()
+ room.Room.Close()
+ }
})
- // hack: wait room destruction
- time.Sleep(5 * time.Second)
}
}
}
+
+// expand joins a list of file path elements.
+func expand(p ...string) string {
+ ph, _ := filepath.Abs(filepath.FromSlash(filepath.Join(p...)))
+ return ph
+}
diff --git a/pkg/worker/room/router_test.go b/pkg/worker/room/router_test.go
new file mode 100644
index 00000000..d4f2e621
--- /dev/null
+++ b/pkg/worker/room/router_test.go
@@ -0,0 +1,82 @@
+package room
+
+import (
+ "testing"
+
+ "github.com/giongto35/cloud-game/v3/pkg/com"
+)
+
+type sKey string
+
+func (s sKey) String() string { return string(s) }
+
+type tSession struct {
+ id sKey
+ connected bool
+}
+
+func (t *tSession) SendAudio([]byte, int32) {}
+func (t *tSession) SendVideo([]byte, int32) {}
+func (t *tSession) SendData([]byte) {}
+func (t *tSession) Connect() { t.connected = true }
+func (t *tSession) Disconnect() { t.connected = false }
+func (t *tSession) Id() sKey { return t.id }
+
+type lookMap struct {
+ com.NetMap[sKey, *tSession]
+ prev com.NetMap[sKey, *tSession] // we could use pointers in the original :3
+}
+
+func (l *lookMap) Reset() {
+ l.prev = com.NewNetMap[sKey, *tSession]()
+ for s := range l.Map.Values() {
+ l.prev.Add(s)
+ }
+ l.NetMap.Reset()
+}
+
+func TestRouter(t *testing.T) {
+ router := newTestRouter()
+
+ var r *Room[*tSession]
+
+ router.SetRoom(&Room[*tSession]{id: "test001"})
+ room := router.FindRoom("test001")
+ if room == nil {
+ t.Errorf("no room, but should be")
+ }
+ router.SetRoom(r)
+ room = router.FindRoom("x")
+ if room != nil {
+ t.Errorf("a room, but should not be")
+ }
+ router.SetRoom(nil)
+ router.Close()
+}
+
+func TestRouterReset(t *testing.T) {
+ u := lookMap{NetMap: com.NewNetMap[sKey, *tSession]()}
+ router := Router[*tSession]{users: &u}
+
+ router.AddUser(&tSession{id: "1", connected: true})
+ router.AddUser(&tSession{id: "2", connected: false})
+ router.AddUser(&tSession{id: "3", connected: true})
+
+ router.Reset()
+
+ disconnected := true
+ for u := range u.prev.Values() {
+ disconnected = disconnected && !u.connected
+ }
+ if !disconnected {
+ t.Errorf("not all users were disconnected, but should")
+ }
+ if !router.Users().Empty() {
+ t.Errorf("has users after reset, but should not")
+ }
+}
+
+func newTestRouter() *Router[*tSession] {
+ u := com.NewNetMap[sKey, *tSession]()
+ return &Router[*tSession]{users: &u}
+}
diff --git a/pkg/worker/routes.go b/pkg/worker/routes.go
deleted file mode 100644
index 07c157a6..00000000
--- a/pkg/worker/routes.go
+++ /dev/null
@@ -1,22 +0,0 @@
-package worker
-
-import "github.com/giongto35/cloud-game/v2/pkg/cws/api"
-
-func (h *Handler) routes() {
- if h.oClient == nil {
- return
- }
-
- h.oClient.Receive(api.ServerId, h.handleServerId())
- h.oClient.Receive(api.TerminateSession, h.handleTerminateSession())
- h.oClient.Receive(api.InitWebrtc, h.handleInitWebrtc())
- h.oClient.Receive(api.Answer, h.handleAnswer())
- h.oClient.Receive(api.IceCandidate, h.handleIceCandidate())
-
- h.oClient.Receive(api.GameStart, h.handleGameStart())
- h.oClient.Receive(api.GameQuit, h.handleGameQuit())
- h.oClient.Receive(api.GameSave, h.handleGameSave())
- h.oClient.Receive(api.GameLoad, h.handleGameLoad())
- h.oClient.Receive(api.GamePlayerSelect, h.handleGamePlayerSelect())
- h.oClient.Receive(api.GameMultitap, h.handleGameMultitap())
-}
diff --git a/pkg/worker/session.go b/pkg/worker/session.go
deleted file mode 100644
index 9815f8aa..00000000
--- a/pkg/worker/session.go
+++ /dev/null
@@ -1,21 +0,0 @@
-package worker
-
-import "github.com/giongto35/cloud-game/v2/pkg/webrtc"
-
-// Session represents a session connected from the browser to the current server
-// It requires one connection to browser and one connection to the coordinator
-// connection to browser is 1-1. connection to coordinator is n - 1
-// Peerconnection can be from other server to ensure better latency
-type Session struct {
- ID string
- peerconnection *webrtc.WebRTC
-
- // Should I make direct reference
- RoomID string
-}
-
-// Close close a session
-func (s *Session) Close() {
- // TODO: Use event base
- s.peerconnection.StopClient()
-}
diff --git a/pkg/worker/thread/mainthread_darwin.go b/pkg/worker/thread/mainthread_darwin.go
new file mode 100644
index 00000000..53ac7585
--- /dev/null
+++ b/pkg/worker/thread/mainthread_darwin.go
@@ -0,0 +1,56 @@
+package thread
+
+import (
+ "runtime"
+ "sync"
+)
+
+type fun struct {
+ fn func()
+ done chan struct{}
+}
+
+var dPool = sync.Pool{New: func() any { return make(chan struct{}) }}
+var fq = make(chan fun, runtime.GOMAXPROCS(0))
+
+var isGraphics = false
+
+func init() {
+ runtime.LockOSThread()
+}
+
+// Run is a wrapper for the main function.
+// Run returns when run (argument) function finishes.
+func Run(run func()) {
+ done := make(chan struct{})
+ go func() {
+ run()
+ done <- struct{}{}
+ }()
+ for {
+ select {
+ case f := <-fq:
+ f.fn()
+ f.done <- struct{}{}
+ case <-done:
+ return
+ }
+ }
+}
+
+// Call queues function f on the main thread and blocks until the function f finishes.
+func Call(f func()) {
+ if !isGraphics {
+ f()
+ return
+ }
+
+ done := dPool.Get().(chan struct{})
+ defer dPool.Put(done)
+ fq <- fun{fn: f, done: done}
+ <-done
+}
+
+func Switch(s bool) {
+ isGraphics = s
+}
diff --git a/pkg/worker/thread/mainthread_darwin_test.go b/pkg/worker/thread/mainthread_darwin_test.go
new file mode 100644
index 00000000..15ce9328
--- /dev/null
+++ b/pkg/worker/thread/mainthread_darwin_test.go
@@ -0,0 +1,14 @@
+package thread
+
+import (
+ "os"
+ "testing"
+)
+
+func TestMain(m *testing.M) {
+ Wrap(func() { os.Exit(m.Run()) })
+}
+
+func TestMainThread(t *testing.T) {
+ _ = 10
+}
diff --git a/pkg/worker/thread/thread.go b/pkg/worker/thread/thread.go
new file mode 100644
index 00000000..3cd824ab
--- /dev/null
+++ b/pkg/worker/thread/thread.go
@@ -0,0 +1,7 @@
+//go:build !darwin
+
+package thread
+
+func Wrap(f func()) { f() }
+func Main(f func()) { f() }
+func SwitchGraphics(s bool) {}
diff --git a/pkg/worker/thread/thread_darwin.go b/pkg/worker/thread/thread_darwin.go
new file mode 100644
index 00000000..120c7af1
--- /dev/null
+++ b/pkg/worker/thread/thread_darwin.go
@@ -0,0 +1,12 @@
+// This package used for locking goroutines to
+// the main OS thread.
+// See: https://github.com/golang/go/wiki/LockOSThread
+package thread
+
+// Wrap enables functions to be executed in the main thread.
+func Wrap(f func()) { Run(f) }
+
+// Main calls a function on the main thread.
+func Main(f func()) { Call(f) }
+
+func SwitchGraphics(s bool) { Switch(s) }
diff --git a/pkg/worker/worker.go b/pkg/worker/worker.go
index 85553e01..0da257b2 100644
--- a/pkg/worker/worker.go
+++ b/pkg/worker/worker.go
@@ -1,28 +1,142 @@
package worker
import (
- "log"
+ "errors"
+ "fmt"
- "github.com/giongto35/cloud-game/v2/pkg/config/worker"
- "github.com/giongto35/cloud-game/v2/pkg/monitoring"
- "github.com/giongto35/cloud-game/v2/pkg/service"
+ "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"
+ "github.com/giongto35/cloud-game/v3/pkg/network/httpx"
+ "github.com/giongto35/cloud-game/v3/pkg/worker/caged"
+ "github.com/giongto35/cloud-game/v3/pkg/worker/cloud"
+ "github.com/giongto35/cloud-game/v3/pkg/worker/room"
)
-func New(conf worker.Config) (services service.Group) {
- httpSrv, err := NewHTTPServer(conf)
- if err != nil {
- log.Fatalf("http init fail: %v", err)
+type Worker struct {
+ address string
+ conf config.WorkerConfig
+ cord *coordinator
+ lib games.GameLibrary
+ launcher games.Launcher
+ log *logger.Logger
+ mana *caged.Manager
+ router *room.GameRouter
+ services [2]interface {
+ Run()
+ Stop() error
}
-
- mainHandler := NewHandler(conf, httpSrv.Addr)
- mainHandler.Prepare()
-
- services.Add(
- httpSrv,
- mainHandler,
- )
- if conf.Worker.Monitoring.IsEnabled() {
- services.Add(monitoring.New(conf.Worker.Monitoring, httpSrv.GetHost(), "worker"))
- }
- return
+ storage cloud.Storage
+}
+
+func New(conf config.WorkerConfig, log *logger.Logger) (*Worker, error) {
+ manager := caged.NewManager(log)
+ if err := manager.Load(caged.Libretro, conf); err != nil {
+ return nil, fmt.Errorf("couldn't cage libretro: %v", err)
+ }
+
+ library := games.NewLib(conf.Library, conf.Emulator, log)
+ library.Scan()
+
+ worker := &Worker{
+ conf: conf,
+ lib: library,
+ launcher: games.NewGameLauncher(library),
+ log: log,
+ mana: manager,
+ router: room.NewGameRouter(),
+ }
+
+ h, err := httpx.NewServer(
+ conf.Worker.GetAddr(),
+ func(s *httpx.Server) httpx.Handler {
+ return s.Mux().HandleW(conf.Worker.Network.PingEndpoint, func(w httpx.ResponseWriter) {
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ _, _ = w.Write([]byte{0x65, 0x63, 0x68, 0x6f}) // echo
+ })
+ },
+ httpx.WithServerConfig(conf.Worker.Server),
+ httpx.HttpsRedirect(false),
+ httpx.WithPortRoll(true),
+ httpx.WithZone(conf.Worker.Network.Zone),
+ httpx.WithLogger(log),
+ )
+ if err != nil {
+ return nil, fmt.Errorf("http init fail: %w", err)
+ }
+ worker.address = h.Addr
+ worker.services[0] = h
+ if conf.Worker.Monitoring.IsEnabled() {
+ worker.services[1] = monitoring.New(conf.Worker.Monitoring, h.GetHost(), log)
+ }
+ st, err := cloud.Store(conf.Storage, log)
+ if err != nil {
+ log.Warn().Err(err).Msgf("cloud storage fail, using no storage")
+ }
+ worker.storage = st
+
+ return worker, nil
+}
+
+func (w *Worker) Reset() { w.router.Reset() }
+
+func (w *Worker) Start(done chan struct{}) {
+ for _, s := range w.services {
+ if s != nil {
+ s.Run()
+ }
+ }
+
+ // !to restore alive worker info when coordinator connection was lost
+ retry := network.NewRetry()
+
+ onRetryFail := func(err error) {
+ w.log.Warn().Err(err).Msgf("socket fail. Retrying in %v", retry.Time())
+ retry.Fail().Multiply(2)
+ }
+
+ go func() {
+ remoteAddr := w.conf.Worker.Network.CoordinatorAddress
+ defer func() {
+ if w.cord != nil {
+ w.cord.Disconnect()
+ }
+ w.Reset()
+ }()
+
+ for {
+ select {
+ case <-done:
+ return
+ default:
+ w.Reset()
+ cord, err := newCoordinatorConnection(remoteAddr, w.conf.Worker, w.address, w.log)
+ if err != nil {
+ onRetryFail(err)
+ continue
+ }
+ cord.SetErrorHandler(onRetryFail)
+ w.cord = cord
+ w.cord.log.Info().Msgf("Connected to the coordinator %v", remoteAddr)
+ wait := w.cord.HandleRequests(w)
+ w.cord.SendLibrary(w)
+ w.cord.SendPrevSessions(w)
+ <-wait
+ retry.Success()
+ }
+ }
+ }()
+}
+
+func (w *Worker) Stop() error {
+ var err error
+ for _, s := range w.services {
+ if s != nil {
+ err0 := s.Stop()
+ err = errors.Join(err, err0)
+ }
+ }
+ return err
}
diff --git a/scripts/install.sh b/scripts/install.sh
deleted file mode 100755
index da52858c..00000000
--- a/scripts/install.sh
+++ /dev/null
@@ -1,18 +0,0 @@
-#!/usr/bin/env bash
-
-echo This script should install application dependencies for Debian-based systems
-if [ $(id -u) -ne 0 ]
-then
- echo "error: run with sudo or root"
- exit 1
-fi
-
-apt-get -qq update
-apt-get -qq install -y \
- ca-certificates \
- libvpx6 \
- libx264-160 \
- libopus0 \
- libgl1-mesa-glx \
- xvfb \
- xauth
diff --git a/scripts/mkdirs.sh b/scripts/mkdirs.sh
new file mode 100755
index 00000000..2ddfb767
--- /dev/null
+++ b/scripts/mkdirs.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env sh
+
+app="$1"
+
+echo Making application runtime directories
+mkdir -p ./assets/cache
+mkdir -p ./assets/games
+mkdir -p ./.cr
+if [ "$app" = "worker" ]; then
+ mkdir -p ./assets/cores
+ mkdir -p ./libretro
+fi
+
+
diff --git a/scripts/version.sh b/scripts/version.sh
index 8a33daa6..3e273791 100755
--- a/scripts/version.sh
+++ b/scripts/version.sh
@@ -1,4 +1,4 @@
-#!/bin/sh
+#!/usr/bin/env sh
file="$1"
version="$2"
diff --git a/test/test.go b/test/test.go
new file mode 100644
index 00000000..b80b425a
--- /dev/null
+++ b/test/test.go
@@ -0,0 +1,17 @@
+package test
+
+import (
+ "os"
+ "path"
+ "runtime"
+)
+
+// runs tests from the root dir when imported
+
+func init() {
+ _, filename, _, _ := runtime.Caller(0)
+ dir := path.Join(path.Dir(filename), "..")
+ if err := os.Chdir(dir); err != nil {
+ panic(err)
+ }
+}
diff --git a/test/testdata/raw/000_name_fourcc_width_height_stride b/test/testdata/raw/000_name_fourcc_width_height_stride
new file mode 100644
index 00000000..e69de29b
diff --git a/test/testdata/raw/001_alsa_ABGR_256_240_1024.raw.zip b/test/testdata/raw/001_alsa_ABGR_256_240_1024.raw.zip
new file mode 100644
index 00000000..a85e7d7b
Binary files /dev/null and b/test/testdata/raw/001_alsa_ABGR_256_240_1024.raw.zip differ
diff --git a/tests/e2e/main_test.go b/tests/e2e/main_test.go
deleted file mode 100644
index dffc7cad..00000000
--- a/tests/e2e/main_test.go
+++ /dev/null
@@ -1,516 +0,0 @@
-package e2e
-
-// import (
-// "fmt"
-// "log"
-// "net/http"
-// "net/http/httptest"
-// "os"
-// "strings"
-// "testing"
-// "time"
-//
-// "github.com/giongto35/cloud-game/pkg/cws"
-// "github.com/giongto35/cloud-game/pkg/overlord"
-// "github.com/giongto35/cloud-game/pkg/util"
-// gamertc "github.com/giongto35/cloud-game/pkg/webrtc"
-// "github.com/giongto35/cloud-game/pkg/worker"
-// "github.com/gorilla/websocket"
-// "github.com/pion/webrtc/v2"
-// )
-//
-// var host = "http://localhost:8000"
-//
-// // Test is in cmd, so gamePath is in parent path
-// var testGamePath = "../games"
-// var webrtcconfig = webrtc.Configuration{ICEServers: []webrtc.ICEServer{{
-// URLs: []string{"stun:stun.l.google.com:19302"},
-// }}}
-//
-// func initCoordinator() (*httptest.Server, *httptest.Server) {
-// server := overlord.NewServer()
-// overlordWorker := httptest.NewServer(http.HandlerFunc(server.WSO))
-// overlordBrowser := httptest.NewServer(http.HandlerFunc(server.WS))
-// return overlordWorker, overlordBrowser
-// }
-//
-// func initWorker(t *testing.T, overlordURL string) *worker.Handler {
-// fmt.Println("Spawn new worker")
-// if overlordURL == "" {
-// return nil
-// } else {
-// overlordURL = "ws" + strings.TrimPrefix(overlordURL, "http")
-// fmt.Println("connecting to overlord: ", overlordURL)
-// }
-//
-// handler := worker.NewHandler(overlordURL, testGamePath)
-//
-// go handler.Run()
-// time.Sleep(time.Second)
-// return handler
-// }
-//
-// func initClient(t *testing.T, host string) (client *cws.Client) {
-// // Convert http://127.0.0.1 to ws://127.0.0.
-// u := "ws" + strings.TrimPrefix(host, "http")
-//
-// fmt.Println("Connecting to", u)
-// ws, _, err := websocket.DefaultDialer.Dial(u, nil)
-// if err != nil {
-// t.Fatalf("%v", err)
-// }
-//
-// handshakedone := make(chan struct{})
-// // Simulate peerconnection initialization from client
-// fmt.Println("Simulating PeerConnection")
-// peerConnection, err := webrtc.NewPeerConnection(webrtcconfig)
-// if err != nil {
-// t.Fatalf("%v", err)
-// }
-//
-// offer, err := peerConnection.CreateOffer(nil)
-// if err != nil {
-// t.Fatalf("%v", err)
-// }
-//
-// // Sets the LocalDescription, and starts our UDP listeners
-// err = peerConnection.SetLocalDescription(offer)
-// if err != nil {
-// panic(err)
-// }
-// // Send offer to overlord
-// log.Println("Browser Client")
-// client = cws.NewClient(ws)
-// go client.Listen()
-//
-// fmt.Println("Sending offer...")
-// client.Send(cws.WSPacket{
-// ID: "init_webrtc",
-// Data: gamertc.Encode(offer),
-// }, nil)
-// fmt.Println("Waiting sdp...")
-//
-// client.Receive("sdp", func(resp cws.WSPacket) cws.WSPacket {
-// log.Println("Received SDP", resp.Data, "client: ", client)
-// answer := webrtc.SessionDescription{}
-// gamertc.Decode(resp.Data, &answer)
-// // Apply the answer as the remote description
-// err = peerConnection.SetRemoteDescription(answer)
-// if err != nil {
-// panic(err)
-// }
-//
-// // TODO: may block in the second call
-// handshakedone <- struct{}{}
-//
-// return cws.EmptyPacket
-// })
-//
-// // Request Offer routing
-// client.Receive("requestOffer", func(resp cws.WSPacket) cws.WSPacket {
-// log.Println("Frontend received requestOffer")
-// peerConnection, err = webrtc.NewPeerConnection(webrtcconfig)
-// if err != nil {
-// t.Fatalf("%v", err)
-// }
-//
-// log.Println("Recreating offer")
-// offer, err := peerConnection.CreateOffer(nil)
-// if err != nil {
-// t.Fatalf("%v", err)
-// }
-//
-// log.Println("Set localDesc")
-// err = peerConnection.SetLocalDescription(offer)
-// if err != nil {
-// panic(err)
-// }
-// log.Println("return offer")
-// return cws.WSPacket{
-// ID: "init_webrtc",
-// Data: gamertc.Encode(offer),
-// }
-// })
-//
-// <-handshakedone
-// return client
-// // If receive roomID, the server is running correctly
-// }
-//
-// func TestSingleServerOneCoordinator(t *testing.T) {
-// /*
-// Case scenario:
-// - A server X are initilized
-// - Client join room with coordinator
-// Expected behavior:
-// - Room received not empty.
-// */
-//
-// oworker, obrowser := initCoordinator()
-// defer obrowser.Close()
-// defer oworker.Close()
-//
-// // Init worker
-// worker := initWorker(t, oworker.URL)
-// defer worker.Close()
-//
-// // connect overlord
-// client := initClient(t, obrowser.URL)
-// defer client.Close()
-//
-// fmt.Println("Sending start...")
-// roomID := make(chan string)
-// client.Send(cws.WSPacket{
-// ID: "start",
-// Data: "Contra.nes",
-// RoomID: "",
-// PlayerIndex: 1,
-// }, func(resp cws.WSPacket) {
-// roomID <- resp.RoomID
-// })
-//
-// respRoomID := <-roomID
-// if respRoomID == "" {
-// fmt.Println("RoomID should not be empty")
-// t.Fail()
-// }
-// time.Sleep(time.Second)
-// fmt.Println("Done")
-// }
-//
-// func TestTwoServerOneCoordinator(t *testing.T) {
-// /*
-// Case scenario:
-// - Two server X, Y are initilized
-// - Client A creates a room on server X
-// - Client B creates a room on server Y
-// - Client B join a room created by A
-// Expected behavior:
-// - Bridge connection will be conducted between server Y and X
-// - Client B can join a room hosted on A
-// */
-//
-// oworker, obrowser := initCoordinator()
-// defer obrowser.Close()
-// defer oworker.Close()
-//
-// worker1 := initWorker(t, oworker.URL)
-// defer worker1.Close()
-//
-// worker2 := initWorker(t, oworker.URL)
-// defer worker2.Close()
-//
-// client1 := initClient(t, obrowser.URL)
-// defer client1.Close()
-//
-// roomID := make(chan string)
-// client1.Send(cws.WSPacket{
-// ID: "start",
-// Data: "Contra.nes",
-// RoomID: "",
-// PlayerIndex: 1,
-// }, func(resp cws.WSPacket) {
-// fmt.Println("RoomID:", resp.RoomID)
-// roomID <- resp.RoomID
-// })
-//
-// remoteRoomID := <-roomID
-// if remoteRoomID == "" {
-// fmt.Println("RoomID should not be empty")
-// t.Fail()
-// }
-// fmt.Println("Done create a room in server 1")
-//
-// // ------------------------------------
-// // Client2 trying to create a random room and later join the the room on server1
-// client2 := initClient(t, obrowser.URL)
-// defer client2.Close()
-// // Wait
-// // Doing the same create local room.
-// localRoomID := make(chan string)
-// client2.Send(cws.WSPacket{
-// ID: "start",
-// Data: "Contra.nes",
-// RoomID: "",
-// PlayerIndex: 1,
-// }, func(resp cws.WSPacket) {
-// fmt.Println("RoomID:", resp.RoomID)
-// localRoomID <- resp.RoomID
-// })
-//
-// <-localRoomID
-//
-// fmt.Println("Request the room from server 1", remoteRoomID)
-// log.Println("Server2 trying to join server1 room")
-// // After trying loging in to one session, login to other with the roomID
-// bridgeRoom := make(chan string)
-// client2.Send(cws.WSPacket{
-// ID: "start",
-// Data: "Contra.nes",
-// RoomID: remoteRoomID,
-// PlayerIndex: 1,
-// }, func(resp cws.WSPacket) {
-// fmt.Println("RoomID:", resp.RoomID)
-// bridgeRoom <- resp.RoomID
-// })
-//
-// <-bridgeRoom
-// //respRoomID := <-bridgeRoom
-// //if respRoomID == "" {
-// //fmt.Println("The room ID should be equal to the saved room")
-// //t.Fail()
-// //}
-// // If receive roomID, the server is running correctly
-// time.Sleep(time.Second)
-// fmt.Println("Done")
-// }
-//
-// func TestReconnectRoom(t *testing.T) {
-// /*
-// Case scenario:
-// - A server X is initialized connecting to overlord
-// - Client A creates a room K on server X
-// - Server X is turned down, Client is closed
-// - Spawn a new server and a new client connecting to the same room K
-// Expected behavior:
-// - The game should be continue
-// TODO: Current test just make sure the game is running, not check if the game is the same
-// */
-//
-// oworker, obrowser := initCoordinator()
-// defer obrowser.Close()
-// defer oworker.Close()
-//
-// // Init worker
-// worker := initWorker(t, oworker.URL)
-//
-// client := initClient(t, obrowser.URL)
-//
-// fmt.Println("Sending start...")
-// roomID := make(chan string)
-// client.Send(cws.WSPacket{
-// ID: "start",
-// Data: "Contra.nes",
-// RoomID: "",
-// PlayerIndex: 1,
-// }, func(resp cws.WSPacket) {
-// fmt.Println("RoomID:", resp.RoomID)
-// roomID <- resp.RoomID
-// })
-//
-// saveRoomID := <-roomID
-// if saveRoomID == "" {
-// fmt.Println("RoomID should not be empty")
-// t.Fail()
-// }
-//
-// log.Println("Closing room and server")
-// client.Close()
-// worker.GetCoordinatorClient().Close()
-// worker.Close()
-//
-// // Close server and reconnect
-//
-// log.Println("Server respawn")
-// // Init slave server again
-// worker = initWorker(t, oworker.URL)
-// defer worker.Close()
-//
-// client = initClient(t, obrowser.URL)
-// defer client.Close()
-//
-// fmt.Println("Re-access room ", saveRoomID)
-// roomID = make(chan string)
-// client.Send(cws.WSPacket{
-// ID: "start",
-// Data: "Contra.nes",
-// RoomID: saveRoomID,
-// PlayerIndex: 1,
-// }, func(resp cws.WSPacket) {
-// fmt.Println("RoomID:", resp.RoomID)
-// roomID <- resp.RoomID
-// })
-//
-// respRoomID := <-roomID
-// if respRoomID == "" || respRoomID != saveRoomID {
-// fmt.Println("The room ID should be equal to the saved room")
-// t.Fail()
-// }
-//
-// time.Sleep(time.Second)
-// fmt.Println("Done")
-// }
-//
-// func TestReconnectRoomNoLocal(t *testing.T) {
-// /*
-// Case scenario:
-// - A server X is initialized connecting to overlord
-// - Client A creates a room K on server X
-// - Server X is turned down, Client is closed
-// - room on local is deleted
-// - Spawn a new server and a new client connecting to the same room K
-// Expected behavior:
-// - room on local storage is refetched from cloud storage
-// - The game should be continue where it is closed
-// TODO: Current test just make sure the game is running, not check if the game is the same
-// */
-// // This test only run if GCP storage is set
-//
-// oworker, obrowser := initCoordinator()
-// defer obrowser.Close()
-// defer oworker.Close()
-//
-// // Init worker
-// ggCredential := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")
-// if ggCredential == "" {
-// return
-// }
-//
-// worker := initWorker(t, oworker.URL)
-//
-// client := initClient(t, obrowser.URL)
-//
-// fmt.Println("Sending start...")
-// roomID := make(chan string)
-// client.Send(cws.WSPacket{
-// ID: "start",
-// Data: "Contra.nes",
-// RoomID: "",
-// PlayerIndex: 1,
-// }, func(resp cws.WSPacket) {
-// fmt.Println("RoomID:", resp.RoomID)
-// roomID <- resp.RoomID
-// })
-//
-// saveRoomID := <-roomID
-// if saveRoomID == "" {
-// fmt.Println("RoomID should not be empty")
-// t.Fail()
-// }
-//
-// log.Println("Closing room and server")
-// client.Close()
-// worker.GetCoordinatorClient().Close()
-// worker.Close()
-// // Remove room on local
-// path := util.GetSavePath(saveRoomID)
-// os.Remove(path)
-// // Expect Google cloud call
-//
-// // Close server and reconnect
-//
-// log.Println("Server respawn")
-// // Init slave server again
-// worker = initWorker(t, oworker.URL)
-// defer worker.Close()
-//
-// client = initClient(t, obrowser.URL)
-// defer client.Close()
-//
-// fmt.Println("Re-access room ", saveRoomID)
-// roomID = make(chan string)
-// client.Send(cws.WSPacket{
-// ID: "start",
-// Data: "Contra.nes",
-// RoomID: saveRoomID,
-// PlayerIndex: 1,
-// }, func(resp cws.WSPacket) {
-// fmt.Println("RoomID:", resp.RoomID)
-// roomID <- resp.RoomID
-// })
-//
-// respRoomID := <-roomID
-// if respRoomID == "" || respRoomID != saveRoomID {
-// fmt.Println("The room ID should be equal to the saved room")
-// t.Fail()
-// }
-//
-// time.Sleep(time.Second)
-// fmt.Println("Done")
-// }
-
-//func TestRejoinNoCoordinatorMultiple(t *testing.T) {
-//[>
-//Case scenario:
-//- A server X without connecting to overlord
-//- Client A keeps creating a new room
-//Expected behavior:
-//- The game should running normally
-//*/
-
-//// Init slave server
-//s := initServer(t, nil)
-//defer s.Close()
-
-//fmt.Println("Num goRoutine before start: ", runtime.NumGoroutine())
-//client := initClient(t, s.URL)
-//for i := 0; i < 100; i++ {
-//fmt.Println("Sending start...")
-//// Keep starting game
-//roomID := make(chan string)
-//client.Send(cws.WSPacket{
-//ID: "start",
-//Data: "Contra.nes",
-//RoomID: "",
-//PlayerIndex: 1,
-//}, func(resp cws.WSPacket) {
-//fmt.Println("RoomID:", resp.RoomID)
-//roomID <- resp.RoomID
-//})
-
-//respRoomID := <-roomID
-//if respRoomID == "" {
-//fmt.Println("The room ID should be equal to the saved room")
-//t.Fail()
-//}
-//}
-//time.Sleep(time.Second)
-//fmt.Println("Num goRoutine should be small: ", runtime.NumGoroutine())
-//fmt.Println("Done")
-
-//}
-
-//func TestRejoinWithCoordinatorMultiple(t *testing.T) {
-//[>
-//Case scenario:
-//- A server X is initialized connecting to overlord
-//- Client A keeps creating a new room
-//Expected behavior:
-//- The game should running normally
-//*/
-
-//// Init slave server
-//o := initCoordinator()
-//defer o.Close()
-
-//oconn := connectTestCoordinatorServer(t, o.URL)
-//// Init slave server
-//s := initServer(t, oconn)
-//defer s.Close()
-
-//fmt.Println("Num goRoutine before start: ", runtime.NumGoroutine())
-//client := initClient(t, s.URL)
-//for i := 0; i < 100; i++ {
-//fmt.Println("Sending start...")
-//// Keep starting game
-//roomID := make(chan string)
-//client.Send(cws.WSPacket{
-//ID: "start",
-//Data: "Contra.nes",
-//RoomID: "",
-//PlayerIndex: 1,
-//}, func(resp cws.WSPacket) {
-//fmt.Println("RoomID:", resp.RoomID)
-//roomID <- resp.RoomID
-//})
-
-//respRoomID := <-roomID
-//if respRoomID == "" {
-//fmt.Println("The room ID should be equal to the saved room")
-//t.Fail()
-//}
-//}
-//fmt.Println("Num goRoutine should be small: ", runtime.NumGoroutine())
-//fmt.Println("Done")
-
-//}
diff --git a/web/css/main.css b/web/css/main.css
index 759289ea..4b95690d 100644
--- a/web/css/main.css
+++ b/web/css/main.css
@@ -1,3 +1,13 @@
+@font-face {
+ font-family: '6809';
+ src: url('/fonts/6809-Chargen.woff2');
+}
+
+/*noinspection CssInvalidPseudoSelector*/
+.no-media-controls::-webkit-media-controls {
+ display: none !important;
+}
+
html {
/* force full size for Firefox */
width: 100%;
@@ -5,19 +15,16 @@ html {
}
body {
- background-image: url('/static/img/background.jpg');
+ background-image: url('/img/background.jpg');
background-repeat: repeat;
}
#gamebody {
- overflow: hidden;
- display: block;
- width: 556px;
- height: 278px;
+ display: flex;
- /*-webkit-box-shadow: inset 0px 0px 2px 2px rgba(219, 222, 222, 1);*/
- /*-moz-box-shadow: inset 0px 0px 2px 2px rgba(219, 222, 222, 1);*/
- /*box-shadow: inset 0px 0px 2px 2px rgba(219, 222, 222, 1);*/
+ overflow: hidden;
+ width: 640px;
+ height: 286px;
position: absolute;
top: 50%;
@@ -25,26 +32,17 @@ body {
margin-right: -50%;
transform: translate(-50%, -50%);
- background-image: url('/static/img/ui/bg.png');
+ background-image: url('/img/ui/bg.jpg');
background-repeat: no-repeat;
background-size: 100% 100%;
-}
+ border-radius: 24px;
-#ui-emulator-bg {
- width: 100%;
- height: 100%;
- display: block;
- position: absolute;
-
- background-image: url('/static/img/ui/bg.png');
- background-repeat: no-repeat;
- background-size: 100% 100%;
+ user-select: none;
}
#help-overlay {
width: 100%;
height: 100%;
- display: none;
position: absolute;
top: 0;
}
@@ -61,14 +59,18 @@ body {
#help-overlay-detail {
width: 100%;
height: 100%;
- display: block;
position: absolute;
- background-image: url('/static/img/help_overlay.png');
+ background-image: url('/img/help_overlay.png');
background-repeat: no-repeat;
background-size: 100% 100%;
}
+#controls-right {
+ position: absolute;
+ left: 70px;
+}
+
#circle-pad-holder {
display: block;
@@ -78,19 +80,19 @@ body {
top: 155px;
left: 22px;
background-size: contain;
- background-image: url('/static/img/ui/bt MOVE.png');
+ background-image: url('/img/ui/bt MOVE.png');
+ z-index: 1;
}
.dpad-empty {
- background-image: url('/static/img/ui/bt MOVE EMPTY.png') !important;
+ background-image: url('/img/ui/bt MOVE EMPTY.png') !important;
}
#guide-txt {
- color: #bababa;
+ color: #979797;
font-size: 8px;
- top: 263px;
- left: 30px;
- width: 1000px;
+ top: 269px;
+ left: 101px;
position: absolute;
user-select: none;
@@ -98,7 +100,7 @@ body {
#circle-pad {
display: block;
- width: 70px;
+ width: 69px;
height: 70px;
position: absolute;
background-size: contain;
@@ -108,13 +110,13 @@ body {
pointer-events: none;
opacity: 0.5;
- background-image: url('/static/img/ui/bong.png');
+ background-image: url('/img/ui/bong.png');
}
.bong-full {
opacity: 1.0 !important;
- background-image: url('/static/img/ui/bong full.png') !important;
+ background-image: url('/img/ui/bong full.png') !important;
}
.dpad {
@@ -127,71 +129,40 @@ body {
#player-index {
background-repeat: no-repeat;
background-size: contain;
- background-image: url('/static/img/ui/bt PlayerIndex.png');
+ background-image: url('/img/ui/bt PlayerIndex.png');
}
#btn-up {
top: 18px;
left: 50%;
- border-bottom: 0px;
+ border-bottom: 0;
border-radius: 3px;
transform: translateX(-50%);
- -webkit-transform: translateX(-50%);
- -moz-transform: translateX(-50%);
}
#btn-down {
bottom: 18px;
left: 50%;
- border-top: 0px;
+ border-top: 0;
border-radius: 3px;
transform: translateX(-50%);
- -webkit-transform: translateX(-50%);
- -moz-transform: translateX(-50%);
}
#btn-left {
left: 18px;
top: 50%;
- border-right: 0px;
+ border-right: 0;
border-radius: 3px;
transform: translateY(-50%);
- -webkit-transform: translateY(-50%);
- -moz-transform: translateY(-50%);
}
#btn-right {
right: 18px;
top: 50%;
- border-left: 0px;
+ border-left: 0;
border-radius: 3px;
transform: translateY(-50%);
- -webkit-transform: translateY(-50%);
- -moz-transform: translateY(-50%);
-}
-
-
-#bottom-screen {
- display: flex;
- align-items: center;
- justify-content: center;
-
- width: 256px;
- height: 240px;
- position: absolute;
- top: 20px;
- left: 150px;
- overflow: hidden;
- background-color: #333;
-
- -moz-border-radius: 5px 5px 5px 5px;
- -webkit-border-radius: 5px 5px 5px 5px;
- border-radius: 5px 5px 5px 5px;
-
- -webkit-box-shadow: 0px 0px 2px 2px rgba(219, 222, 222, 1);
- -moz-box-shadow: 0px 0px 2px 2px rgba(219, 222, 222, 1);
- box-shadow: 0px 0px 2px 2px rgba(25, 25, 25, 1);
}
#color-button-holder {
@@ -225,28 +196,28 @@ body {
}
#btn-help {
- padding-top: 0px;
- width: 28px;
+ padding-top: 0;
+ width: 20px;
height: 28px;
- top: 10px;
+ top: 16px;
left: 23px;
- background-image: url('/static/img/ui/Help.png');
+ background-image: url('/img/ui/Help.png');
}
#btn-load {
top: 20px;
left: 435px;
- background-image: url('/static/img/ui/bt LOAD.png');
+ background-image: url('/img/ui/bt LOAD.png');
}
#btn-save {
top: 60px;
left: 435px;
- background-image: url('/static/img/ui/bt SAVE.png');
+ background-image: url('/img/ui/bt SAVE.png');
}
#btn-join {
@@ -255,7 +226,7 @@ body {
left: 22px;
height: 25px;
- background-image: url('/static/img/ui/bt SHARE.png');
+ background-image: url('/img/ui/bt SHARE.png');
}
#btn-quit {
@@ -264,16 +235,66 @@ body {
left: 75px;
height: 25px;
- background-image: url('/static/img/ui/bt QUIT.png');
+ background-image: url('/img/ui/bt QUIT.png');
}
+#btn-rec {
+ width: 36px;
+ top: 2px;
+ left: 373px;
+ height: 9px;
+
+ background-image: url('/img/ui/bt REC.png');
+}
+
+.record-user {
+ position: absolute;
+ padding-left: 6px;
+ font-size: 12px;
+ width: 109px;
+ top: 4px;
+ left: 262px;
+ color: #888888;
+ border-radius: 6px;
+ outline: none;
+ border-top-style: hidden;
+ border-right-style: hidden;
+ border-left-style: hidden;
+ border-bottom-style: hidden;
+ background-image: url('/img/ui/FrameTEXT.png');
+ background-size: cover;
+}
+
+.record:before {
+ position: absolute;
+ content: '';
+ background-color: #FF0000;
+ border-radius: 50%;
+ width: 5px;
+ height: 5px;
+ pointer-events: none;
+ left: 4px;
+ top: 7px;
+}
+
+.record.blink:before {
+ animation: blinker 1s linear infinite;
+}
+
+@keyframes blinker {
+ 50% {
+ opacity: 0;
+ }
+}
+
+
#btn-select {
width: 46px;
top: 100px;
left: 435px;
height: 25px;
- background-image: url('/static/img/ui/bt SELECT.png');
+ background-image: url('/img/ui/bt SELECT.png');
}
#btn-start {
@@ -281,7 +302,7 @@ body {
top: 100px;
left: 489px;
- background-image: url('/static/img/ui/bt START.png');
+ background-image: url('/img/ui/bt START.png');
}
#btn-a {
@@ -289,10 +310,8 @@ body {
right: 0;
transform: translateY(-50%);
- -webkit-transform: translateY(-50%);
- -moz-transform: translateY(-50%);
- background-image: url('/static/img/ui/bt A.png');
+ background-image: url('/img/ui/bt A.png');
}
@@ -301,20 +320,16 @@ body {
left: 50%;
transform: translateX(-50%);
- -webkit-transform: translateX(-50%);
- -moz-transform: translateX(-50%);
- background-image: url('/static/img/ui/bt B.png');
+ background-image: url('/img/ui/bt B.png');
}
#btn-x {
top: 0;
left: 50%;
transform: translateX(-50%);
- -webkit-transform: translateX(-50%);
- -moz-transform: translateX(-50%);
- background-image: url('/static/img/ui/bt X.png');
+ background-image: url('/img/ui/bt X.png');
}
@@ -322,25 +337,24 @@ body {
top: 50%;
left: 0;
transform: translateY(-50%);
- -webkit-transform: translateY(-50%);
- -moz-transform: translateY(-50%);
- background-image: url('/static/img/ui/bt Y.png');
+ background-image: url('/img/ui/bt Y.png');
}
#btn-settings {
width: 65px;
height: 21px;
- top: 9%;
+ top: 26px;
left: 55px;
padding: 0;
transform: translateY(-50%);
- background-image: url('/static/img/ui/bt OPTIONS.png');
+ background-image: url('/img/ui/bt OPTIONS.png');
opacity: .7;
+ z-index: 0;
}
#lights-holder {
@@ -352,54 +366,13 @@ body {
left: 460px;
}
-@-webkit-keyframes blink {
- 0% {
- background: #7a7e7d;
- }
-
- 100% {
- background: yellow;
- }
-}
-
-@-moz-keyframes blink {
- 0% {
- background: #7a7e7d;
- }
-
- 100% {
- background: yellow;
- }
-}
-
-@keyframes blink {
- 0% {
- background: #7a7e7d;
- }
-
- 100% {
- background: yellow;
- }
-}
-
-
-*.unselectable {
- -moz-user-select: -moz-none;
- -khtml-user-select: none;
- -webkit-user-select: none;
- -ms-user-select: none;
- user-select: none;
-}
-
#room-txt {
position: absolute;
width: 59px;
- top: 45px;
+ top: 48px;
left: 23px;
color: #bababa;
- padding-left: 0px;
- -moz-border-radius: 6px;
- -webkit-border-radius: 6px;
+ padding-left: 0;
border-radius: 6px;
outline: none;
@@ -408,7 +381,7 @@ body {
border-left-style: hidden;
border-bottom-style: hidden;
- background-image: url('/static/img/ui/FrameTEXT.png');
+ background-image: url('/img/ui/FrameTEXT.png');
background-size: cover;
}
@@ -422,22 +395,12 @@ body {
opacity: 0.75;
}
-#bottom-screen {
- position: absolute;
- /* popups under the screen fix */
- z-index: -1;
-}
-
.game-screen {
- width: 100%;
- height: 100%;
- background-color: #222222;
- position: absolute;
- display: flex;
-}
-
-[hidden] {
- display: none !important;
+ position: relative;
+ object-fit: contain;
+ width: inherit;
+ height: inherit;
+ background-color: #101010;
}
#menu-screen {
@@ -445,12 +408,12 @@ body {
display: block;
overflow: hidden;
- width: 256px;
+ width: 320px;
height: 240px;
- /* background-color: gray; */
- background-image: url('/static/img/screen_background5.png');
+ background-image: url('/img/screen_background5.png');
background-size: cover;
+ z-index: 1;
}
#menu-item-choice {
@@ -461,7 +424,7 @@ body {
height: 36px;
background-color: #FFCF9E;
opacity: 0.75;
- font-family: 'Roboto';
+ mix-blend-mode: lighten;
top: 50%;
left: 0;
@@ -472,25 +435,15 @@ body {
#menu-container {
display: block;
position: absolute;
- /* background-color: red; */
width: 100%;
top: 102px; /* 240px - 36 / 2 */
left: 0;
+ /*z-index: 1;*/
}
-@font-face {
- font-family: 'Roboto'; /*a name to be used later*/
- src: url('/static/fonts/Roboto-Regular.ttf'); /*URL to font*/
-}
-
-@font-face {
- font-family: '6809'; /*a name to be used later*/
- src: url('/static/fonts/6809 chargen.ttf'); /*URL to font*/
-}
-
.menu-item {
display: block;
position: relative;
@@ -498,22 +451,19 @@ body {
width: 100%;
height: 36px; /* 35 + 1 border = 36px */
- font-family: '6809';
+ font-family: '6809', monospace;
font-size: 19px;
- /* border-top: 1px dashed blue; */
}
-.menu-item div {
+.menu-item div:first-child {
overflow: hidden;
display: block;
position: absolute;
left: 15px;
- top: 5px;
- width: 226px;
+ top: 7px;
+ width: 288px;
height: 25px;
-
- /* background-color: yellow; */
}
.menu-item div span {
@@ -529,64 +479,14 @@ body {
.menu-item div .pick {
overflow: unset;
- -moz-animation: horizontally 4s linear infinite alternate;
- -webkit-animation: horizontally 4s linear infinite alternate;
- animation: horizontally 4s linear infinite alternate;
}
-
-@-moz-keyframes horizontally {
- 0% {
- transform: translateX(0%);
- }
- 25% {
- transform: translateX(-20%);
- }
- 50% {
- transform: translateX(0%);
- }
- 75% {
- transform: translateX(20%);
- }
- 100% {
- transform: translateX(0%);
- }
-}
-
-@-webkit-keyframes horizontally {
- 0% {
- transform: translateX(0%);
- }
- 25% {
- transform: translateX(-20%);
- }
- 50% {
- transform: translateX(0%);
- }
- 75% {
- transform: translateX(20%);
- }
- 100% {
- transform: translateX(0%);
- }
-}
-
-@keyframes horizontally {
- 0% {
- transform: translateX(0%);
- }
- 25% {
- transform: translateX(-20%);
- }
- 50% {
- transform: translateX(0%);
- }
- 75% {
- transform: translateX(20%);
- }
- 100% {
- transform: translateX(0%);
- }
+.menu-item__info {
+ color: white;
+ opacity: .55;
+ font-size: 30%;
+ text-align: center;
+ padding-top: 3px;
}
#noti-box {
@@ -610,7 +510,7 @@ body {
display: block;
margin-top: 10px;
text-align: center;
- font-family: 'Roboto';
+ font-family: 'Arial', sans-serif;
font-size: 15px;
color: #bababa;
@@ -630,13 +530,12 @@ body {
height: 25px; /* Specified height */
outline: none; /* Remove outline */
/*opacity: 0.7; [> Set transparency (for mouse-over effects on hover) <]*/
- -webkit-transition: .2s; /* 0.2 seconds transition on hover */
transition: opacity .2s;
background-color: transparent;
background-repeat: no-repeat;
background-size: contain;
- background-image: url('/static/img/ui/FramePlayerIndex.png');
+ background-image: url('/img/ui/FramePlayerIndex.png');
}
/* Mouse-over effects */
@@ -655,7 +554,7 @@ body {
background-color: transparent;
background-repeat: no-repeat;
background-size: contain;
- background-image: url('/static/img/ui/bt PlayerIndex.png');
+ background-image: url('/img/ui/bt PlayerIndex.png');
}
.slider::-moz-range-thumb {
@@ -664,76 +563,24 @@ body {
height: 25px; /* Slider handle height */
cursor: pointer; /* Cursor on hover */
- border: 0px;
+ border: 0;
background-color: transparent;
background-repeat: no-repeat;
background-size: contain;
- background-image: url('/static/img/ui/bt PlayerIndex.png');
+ background-image: url('/img/ui/bt PlayerIndex.png');
}
* {
touch-action: manipulation;
}
-#stats-overlay {
- position: absolute;
- z-index: 200;
- backface-visibility: hidden;
-
- display: flex;
- flex-direction: column;
- justify-content: space-around;
-
- top: 1.1em;
- right: 1.1em;
- color: #fff;
- background: #000;
- opacity: .765;
- padding: .5em 1em .1em 1em;
-
- font-family: monospace;
- font-size: 40%;
-
- width: 70px;
-
- visibility: hidden;
-}
-
-#stats-overlay > div {
- display: flex;
- flex-flow: wrap;
- justify-content: space-between;
-
- margin-bottom: .7em;
-}
-
-#stats-overlay > div > div {
- display: inline-block;
- font-weight: 500;
-}
-
-#stats-overlay .graph {
- width: 100%;
- /* artifacts with pixelated option */
- /*image-rendering: pixelated;*/
- image-rendering: optimizeSpeed;
-}
-
-.no-select {
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
-}
-
.dpad-toggle-label {
- position: relative;
+ position: absolute;
display: inline-block;
width: 35px;
height: 20px;
- top: 44px;
+ top: 45px;
left: 85px;
}
@@ -751,7 +598,6 @@ body {
right: 0;
bottom: 0;
background-color: #515151;
- -webkit-transition: .4s;
transition: .4s;
border-radius: 20px;
}
@@ -764,7 +610,6 @@ body {
left: 3px;
bottom: 3px;
background-color: #5f5f5f;
- -webkit-transition: .4s;
transition: .4s;
border-radius: 50%;
}
@@ -774,12 +619,17 @@ input:checked + .dpad-toggle-slider {
}
input:checked + .dpad-toggle-slider:before {
- -webkit-transform: translateX(15px);
- -ms-transform: translateX(15px);
transform: translateX(15px);
}
-#version {
+.source {
+ position: fixed;
+ bottom: 5px;
+ right: 5px;
color: #dddddd;
text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;
}
+
+.source a {
+ color: #dddddd;
+}
diff --git a/web/css/ui.css b/web/css/ui.css
index d537a0d8..1b5d1e79 100644
--- a/web/css/ui.css
+++ b/web/css/ui.css
@@ -1,67 +1,6 @@
-.hidden {
- display: none;
-}
-
-.modal-window {
- position: fixed;
-
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
-
- background-color: rgba(0, 0, 0, 0.7);
-
- z-index: 9999;
- visibility: hidden;
- opacity: 0;
- pointer-events: none;
-
- -webkit-transition: all 0.2s;
- transition: all 0.2s;
-}
-
-.modal-visible {
- visibility: visible;
- opacity: 1;
- pointer-events: auto;
-}
-
-
-.modal-window > div {
- width: 42vw;
- position: absolute;
- top: 50%;
- left: 50%;
- -webkit-transform: translate(-50%, -50%);
- transform: translate(-50%, -50%);
- padding: 2em;
- background: #ffffff;
-}
-
-.modal-window header {
- font-weight: bold;
-}
-
-.modal-window h1 {
- font-size: 150%;
- margin: 0 0 15px;
-}
-
-.semi-button {
- cursor: pointer;
-}
-
-
-#app-settings {
- font-family: monospace;
-}
-
-#settings-data {
- overflow-y: auto;
- height: 50vh;
- padding: 1em 0;
+.hidden, [hidden] {
+ display: none !important;
}
.container {
@@ -73,10 +12,6 @@
height: 100vh;
}
-.modal-window div:not(:last-of-type) {
- margin-bottom: 15px;
-}
-
.btn2 {
font-size: 80%;
padding: .2em .4em;
@@ -88,6 +23,10 @@
height: 1rem;
}
+.settings {
+ padding: 0 1em 1em 1em;
+}
+
.settings__controls {
color: #aaa;
font-size: 80%;
@@ -104,9 +43,23 @@
grid-template-rows: auto;
}
-.settings__option-name {
+.settings__option-title {
background-color: beige;
- padding: 1em;
+ margin-top: .5em;
+ padding: .5em;
+}
+
+.settings__option-name {
+}
+
+.settings__option-desc {
+ font-size: 61%;
+ color: #444;
+ font-family: monospace;
+}
+
+.settings__option-value {
+ padding: .5em;
}
.restart-needed-asterisk:after {
@@ -114,13 +67,37 @@
color: red;
}
-.settings__option-value {
+.restart-needed-asterisk-b:before {
+ content: '*';
+ color: red;
+}
+.settings__option-value select, .settings__option-value input {
+ font-family: '6809', monospace;
+ font-size: 90%;
+}
+
+.settings__option-value input:not([type='checkbox']) {
+ width: 6em;
+}
+
+.settings__option-value option {
+ font-size: 150%;
+}
+
+.settings__info {
+ font-size: 80%;
}
.keyboard-bindings .settings__option-value {
display: grid;
- grid-template-columns: 25% 25% auto auto;
+ grid-template-columns: 20% 20% 20% 20% auto;
+ row-gap: 5px;
+}
+
+.settings__option-checkbox label {
+ display: inline-flex;
+ align-items: center;
}
.binding-element {
@@ -128,3 +105,262 @@
flex-direction: column;
align-items: center;
}
+
+.binding-element button {
+ font-family: '6809', monospace;
+ min-width: 6em;
+}
+
+.binding-element div {
+ font-size: 80%;
+}
+
+/* Server list styling */
+
+.server-list div {
+ display: grid;
+ grid-template-columns: .2fr 1.2fr 1fr .5fr .2fr;
+ justify-items: start;
+
+
+ padding: 0 1em 0 1em;
+}
+
+.server-list div:not(.server-list__header):hover {
+ background-color: #e7e7e7;
+}
+
+.server-list .server-list__header {
+ font-weight: bold;
+ padding: 1em 1em .6em 1em;
+}
+
+.server-list .server-list__header span {
+ border-bottom: 1px dashed black;
+}
+
+/* Panel element */
+.panel {
+ display: flex;
+ flex-grow: 1;
+ flex-direction: column;
+
+ background-color: white;
+ font-size: 12px;
+
+ font-family: '6809', monospace;
+
+ z-index: 1;
+ position: relative;
+
+ cursor: default;
+}
+
+.panel__header {
+ display: flex;
+ justify-content: space-between;
+
+ align-items: center;
+ padding: .5rem .8rem;
+
+ background-color: #f5f5f5;
+
+ user-select: none;
+}
+
+.panel__header__title {
+ letter-spacing: 1px;
+}
+
+.panel__header__controls {
+ display: flex;
+ gap: 0.3rem;
+}
+
+.panel__content {
+ padding-bottom: .5rem;
+ overflow-y: auto;
+}
+
+.panel__footer {
+ display: flex;
+ /* Push the buttons to the right */
+ justify-content: flex-end;
+ /* Border */
+ border-top: 1px solid rgba(0, 0, 0.3);
+}
+
+.panel__button {
+ background-color: #ededed;
+
+ padding: 2px 4px;
+ min-width: 0.7rem;
+ text-align: center;
+}
+
+.panel__button:hover {
+ background-color: #7a7e7d;
+ color: white;
+
+ cursor: pointer;
+}
+
+.panel__button.bold {
+ font-weight: bold;
+}
+
+.panel__button_separator {
+ width: .5em;
+}
+
+
+.app-button {
+ position: absolute;
+ top: 34px;
+ left: 45px;
+ cursor: pointer;
+ font-size: 70%;
+ color: #606060;
+ font-family: '6809', sans-serif;
+}
+
+.app-button:hover {
+ color: #7e7e7e;
+}
+
+.app-button.fs {
+ position: relative;
+ top: 0;
+ left: 0;
+}
+
+#mirror-stream {
+ image-rendering: pixelated;
+ width: 100%;
+ height: 100%;
+}
+
+#screen {
+ display: flex;
+ /*align-items: center;*/
+ justify-content: center;
+
+ min-width: 0 !important;
+ min-height: 0 !important;
+
+ position: absolute;
+ /* popups under the screen fix */
+ z-index: -1;
+
+ width: 320px;
+ height: 240px;
+ top: 23px;
+ left: 150px;
+ overflow: hidden;
+ background-color: #000000;
+
+ border-radius: 5px 5px 5px 5px;
+ box-shadow: 0 0 2px 2px rgba(25, 25, 25, 1);
+}
+
+.screen__footer {
+ position: absolute;
+ bottom: 0;
+ display: flex;
+ flex-direction: row;
+ border-top: 1px solid #1b1b1b;
+ width: calc(100% - .6rem);
+ justify-content: space-between;
+
+ background-color: #00000022;
+
+ height: 13px;
+ font-size: .6rem;
+ color: #ffffff;
+
+ opacity: .3;
+
+ padding: 0 .2em;
+
+ cursor: default;
+}
+
+.hover:hover {
+ opacity: .567;
+}
+
+.with-footer {
+ height: calc(100% - 14px);
+}
+
+.kbm-button {
+ top: 265px;
+ left: 542px;
+
+ text-align: center;
+ font-size: 70%;
+
+ opacity: .5;
+ filter: contrast(.3);
+}
+
+.kbm-button-fs {
+ width: 1em;
+ text-align: center;
+ font-size: 110%;
+ /*color: #ffffff;*/
+ /*opacity: .5;*/
+ filter: contrast(.3);
+}
+
+.no-pointer {
+ cursor: none;
+}
+
+#stats-overlay {
+ cursor: default;
+
+ display: flex;
+ flex-direction: row;
+ justify-content: end;
+
+ color: #fff;
+ background: #000;
+ /*opacity: .3;*/
+
+ font-size: 10px;
+ font-family: monospace;
+ min-width: 18em;
+
+ gap: 5px;
+ visibility: hidden;
+}
+
+#stats-overlay > div {
+ display: flex;
+ flex-flow: wrap;
+ justify-content: space-between;
+ align-items: center;
+}
+
+#stats-overlay > div > div {
+ display: inline-block;
+ font-weight: 500;
+}
+
+#stats-overlay .graph {
+ width: 100%;
+ /* artifacts with pixelated option */
+ /*image-rendering: pixelated;*/
+ image-rendering: optimizeSpeed;
+}
+
+.stats-bitrate {
+ min-width: 3.3rem;
+}
+
+#play-stream {
+ color: brown;
+ align-content: center;
+ font-size: 200%;
+}
diff --git a/web/fonts/6809 chargen.ttf b/web/fonts/6809 chargen.ttf
deleted file mode 100644
index bb816087..00000000
Binary files a/web/fonts/6809 chargen.ttf and /dev/null differ
diff --git a/web/fonts/6809-Chargen.woff2 b/web/fonts/6809-Chargen.woff2
new file mode 100644
index 00000000..e3e3b406
Binary files /dev/null and b/web/fonts/6809-Chargen.woff2 differ
diff --git a/web/fonts/8-Bit-Madness.ttf b/web/fonts/8-Bit-Madness.ttf
deleted file mode 100644
index ea2e0b97..00000000
Binary files a/web/fonts/8-Bit-Madness.ttf and /dev/null differ
diff --git a/web/fonts/Roboto-Regular.ttf b/web/fonts/Roboto-Regular.ttf
deleted file mode 100644
index 2b6392ff..00000000
Binary files a/web/fonts/Roboto-Regular.ttf and /dev/null differ
diff --git a/web/fonts/Roboto-Thin.ttf b/web/fonts/Roboto-Thin.ttf
deleted file mode 100644
index 4e797cf7..00000000
Binary files a/web/fonts/Roboto-Thin.ttf and /dev/null differ
diff --git a/web/img/background.jpg b/web/img/background.jpg
index 68df6eec..1f726014 100644
Binary files a/web/img/background.jpg and b/web/img/background.jpg differ
diff --git a/web/img/help_overlay.png b/web/img/help_overlay.png
index 20c006d2..256d3237 100644
Binary files a/web/img/help_overlay.png and b/web/img/help_overlay.png differ
diff --git a/web/img/screen_background5.png b/web/img/screen_background5.png
index 6222bea8..f1017218 100644
Binary files a/web/img/screen_background5.png and b/web/img/screen_background5.png differ
diff --git a/web/img/ui/FramePlayerIndex.png b/web/img/ui/FramePlayerIndex.png
index c8a78a86..e94dac7b 100644
Binary files a/web/img/ui/FramePlayerIndex.png and b/web/img/ui/FramePlayerIndex.png differ
diff --git a/web/img/ui/FrameTEXT.png b/web/img/ui/FrameTEXT.png
index 185fe2e2..a08997da 100644
Binary files a/web/img/ui/FrameTEXT.png and b/web/img/ui/FrameTEXT.png differ
diff --git a/web/img/ui/Help.png b/web/img/ui/Help.png
index bb9d072a..d999f7c6 100644
Binary files a/web/img/ui/Help.png and b/web/img/ui/Help.png differ
diff --git a/web/img/ui/bg.jpg b/web/img/ui/bg.jpg
new file mode 100644
index 00000000..d396a3ff
Binary files /dev/null and b/web/img/ui/bg.jpg differ
diff --git a/web/img/ui/bg.png b/web/img/ui/bg.png
deleted file mode 100644
index c87100fe..00000000
Binary files a/web/img/ui/bg.png and /dev/null differ
diff --git a/web/img/ui/bong full.png b/web/img/ui/bong full.png
index 8304178e..d31d7afa 100644
Binary files a/web/img/ui/bong full.png and b/web/img/ui/bong full.png differ
diff --git a/web/img/ui/bong.png b/web/img/ui/bong.png
index b9e2d473..34894842 100644
Binary files a/web/img/ui/bong.png and b/web/img/ui/bong.png differ
diff --git a/web/img/ui/bt A.png b/web/img/ui/bt A.png
index 63c644c2..9b01b8d9 100644
Binary files a/web/img/ui/bt A.png and b/web/img/ui/bt A.png differ
diff --git a/web/img/ui/bt B.png b/web/img/ui/bt B.png
index 7458ebd1..4ec8a616 100644
Binary files a/web/img/ui/bt B.png and b/web/img/ui/bt B.png differ
diff --git a/web/img/ui/bt LOAD.png b/web/img/ui/bt LOAD.png
index 0e999820..074cdb5d 100644
Binary files a/web/img/ui/bt LOAD.png and b/web/img/ui/bt LOAD.png differ
diff --git a/web/img/ui/bt MOVE EMPTY.png b/web/img/ui/bt MOVE EMPTY.png
index 2a40d66f..533de364 100644
Binary files a/web/img/ui/bt MOVE EMPTY.png and b/web/img/ui/bt MOVE EMPTY.png differ
diff --git a/web/img/ui/bt MOVE.png b/web/img/ui/bt MOVE.png
index d05cd3b8..bf51d4df 100644
Binary files a/web/img/ui/bt MOVE.png and b/web/img/ui/bt MOVE.png differ
diff --git a/web/img/ui/bt OPTIONS.png b/web/img/ui/bt OPTIONS.png
index 15390f4b..24b4618d 100644
Binary files a/web/img/ui/bt OPTIONS.png and b/web/img/ui/bt OPTIONS.png differ
diff --git a/web/img/ui/bt PlayerIndex.png b/web/img/ui/bt PlayerIndex.png
index c4453680..f3973dc8 100644
Binary files a/web/img/ui/bt PlayerIndex.png and b/web/img/ui/bt PlayerIndex.png differ
diff --git a/web/img/ui/bt QUIT.png b/web/img/ui/bt QUIT.png
index 86f2d0e0..62878bc3 100644
Binary files a/web/img/ui/bt QUIT.png and b/web/img/ui/bt QUIT.png differ
diff --git a/web/img/ui/bt REC.png b/web/img/ui/bt REC.png
new file mode 100644
index 00000000..c4f0c69b
Binary files /dev/null and b/web/img/ui/bt REC.png differ
diff --git a/web/img/ui/bt SAVE.png b/web/img/ui/bt SAVE.png
index 0e8bb08e..9db4f68a 100644
Binary files a/web/img/ui/bt SAVE.png and b/web/img/ui/bt SAVE.png differ
diff --git a/web/img/ui/bt SELECT.png b/web/img/ui/bt SELECT.png
index 12ced4c4..d9006eac 100644
Binary files a/web/img/ui/bt SELECT.png and b/web/img/ui/bt SELECT.png differ
diff --git a/web/img/ui/bt SHARE.png b/web/img/ui/bt SHARE.png
index 60691834..89db69de 100644
Binary files a/web/img/ui/bt SHARE.png and b/web/img/ui/bt SHARE.png differ
diff --git a/web/img/ui/bt START.png b/web/img/ui/bt START.png
index 88e2f850..896ab8b5 100644
Binary files a/web/img/ui/bt START.png and b/web/img/ui/bt START.png differ
diff --git a/web/img/ui/bt X.png b/web/img/ui/bt X.png
index f1631c12..d3fbf8f0 100644
Binary files a/web/img/ui/bt X.png and b/web/img/ui/bt X.png differ
diff --git a/web/img/ui/bt Y.png b/web/img/ui/bt Y.png
index 6da62c85..e0e7335d 100644
Binary files a/web/img/ui/bt Y.png and b/web/img/ui/bt Y.png differ
diff --git a/web/img/ui/frame.png b/web/img/ui/frame.png
index f48c5129..3cec1c88 100644
Binary files a/web/img/ui/frame.png and b/web/img/ui/frame.png differ
diff --git a/web/index.html b/web/index.html
index 5df32206..4bd8c591 100644
--- a/web/index.html
+++ b/web/index.html
@@ -1,5 +1,5 @@
-
-
+
+
@@ -9,145 +9,137 @@
-
-
-
+
+
+ Cloud Retro
-