mirror of
https://github.com/libretro/ludo.git
synced 2026-11-08 08:09:44 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a2dc452c7 | ||
|
|
c47ad16a70 | ||
|
|
13347e0051 | ||
|
|
31951498f7 | ||
|
|
2c451ecd6a | ||
|
|
8ac8018272 | ||
|
|
35471f0873 | ||
|
|
5cb7b67c76 | ||
|
|
98a4419151 | ||
|
|
6d41afd5ee | ||
|
|
601bdfacaa | ||
|
|
2410ddddf0 | ||
|
|
a6b0eb06c0 | ||
|
|
3f2cde1951 | ||
|
|
79d573386e | ||
|
|
d3a0b57176 | ||
|
|
3796ef93f1 | ||
|
|
210e2527e9 | ||
|
|
29599beb88 | ||
|
|
65def6c267 | ||
|
|
ef43012d25 | ||
|
|
b11aa8c827 | ||
|
|
b4afb802ef | ||
|
|
79bc193526 | ||
|
|
2061759bce | ||
|
|
eef10468cb | ||
|
|
d01f19f69f | ||
|
|
f1498bf136 | ||
|
|
7414785d90 | ||
|
|
fcfb74d710 | ||
|
|
6c6da40eb2 | ||
|
|
adee53e601 | ||
|
|
2c9a4fdc36 | ||
|
|
666c643531 | ||
|
|
d327cb9203 | ||
|
|
57d55be994 | ||
|
|
2140a68b38 | ||
|
|
f83948db18 | ||
|
|
5c124536b0 | ||
|
|
a224a303ae | ||
|
|
7d12dc8d16 | ||
|
|
ad635fbeb1 | ||
|
|
85dfd8672d | ||
|
|
41f11eb835 | ||
|
|
22e690f6ed | ||
|
|
7b3506e045 | ||
|
|
cd4ef89fb2 | ||
|
|
ea3f7693e7 | ||
|
|
039faf0f94 | ||
|
|
7f6a3f5ce4 | ||
|
|
7a16a5fe5e | ||
|
|
ca93cbc9cd | ||
|
|
bd879207b0 | ||
|
|
898036ac69 | ||
|
|
f38287bd0c | ||
|
|
c3f884c633 | ||
|
|
3a80b3227b | ||
|
|
518cd09d10 | ||
|
|
9924158ed0 | ||
|
|
819878fdbb | ||
|
|
0fa704c79e | ||
|
|
0827c97561 | ||
|
|
3fd4dbd740 | ||
|
|
098e9a9e56 | ||
|
|
374c9d41f4 | ||
|
|
5533009c90 | ||
|
|
500c8bad7c | ||
|
|
cd8d3cedf8 | ||
|
|
c7d11dd008 | ||
|
|
317d738596 | ||
|
|
9420478922 | ||
|
|
6c2a6cf89a | ||
|
|
35d39f1792 | ||
|
|
c821f48976 | ||
|
|
6cefe03112 | ||
|
|
038765b2b0 | ||
|
|
f7a97a6cb7 | ||
|
|
f5c1d3e4d6 | ||
|
|
969855edde | ||
|
|
9463b4a0be | ||
|
|
d28a4fd5d9 | ||
|
|
a6caa24fae | ||
|
|
0b1f90a72f | ||
|
|
544fe602b7 | ||
|
|
32b8f04a64 |
No files matched your search
+242
-46
@@ -1,6 +1,8 @@
|
||||
name: CD
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
types: [created]
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
@@ -9,19 +11,121 @@ defaults:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
build_linux:
|
||||
build_linux_wayland_x86_64:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: echo "VERSION=$(echo ${{ github.event.release.tag_name }} | cut -c 2-)" >> $GITHUB_ENV
|
||||
- run: sudo apt-get update -q
|
||||
- run: sudo apt-get install libopenal-dev xorg-dev libgl1-mesa-dev libwayland-dev libxkbcommon-dev -y --allow-unauthenticated
|
||||
- run: go build -tags wayland -v
|
||||
- run: OS=Linux ARCH=x86_64 DISPDRIVER=wayland VERSION=$VERSION make tar
|
||||
- run: OS=Linux ARCH=x86_64 DISPDRIVER=wayland VERSION=$VERSION make deb
|
||||
- run: sha256sum Ludo-Linux-wayland-x86_64-${VERSION}.tar.gz > Ludo-Linux-wayland-x86_64-${VERSION}.tar.gz.sha256
|
||||
|
||||
- name: Upload Release Asset
|
||||
uses: alexellis/[email protected]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
asset_paths: '["./Ludo-*.tar.gz*", "./*.deb"]'
|
||||
|
||||
build_linux_wayland_armhf:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: echo "VERSION=$(echo ${{ github.event.release.tag_name }} | cut -c 2-)" >> $GITHUB_ENV
|
||||
- run: sudo apt update -q
|
||||
- run: sudo apt install binutils-multiarch
|
||||
- run: sudo dpkg --add-architecture armhf
|
||||
- run: sudo rm -rf /etc/apt/sources.list.d/*s
|
||||
- run: echo "" | sudo tee /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157
|
||||
- run: sudo apt update -q
|
||||
- run: sudo apt install -f libc6-dev:armhf gcc-arm-linux-gnueabihf libopenal-dev:armhf libgl1-mesa-dev:armhf libxcursor-dev:armhf libxrandr-dev:armhf libxinerama-dev:armhf libxi-dev:armhf libwayland-dev:armhf libxkbcommon-dev:armhf -y --allow-unauthenticated
|
||||
- run: PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig/ GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc go build -tags wayland -v
|
||||
- run: OS=Linux ARCH=arm DISPDRIVER=wayland VERSION=$VERSION make tar
|
||||
- run: OS=Linux ARCH=arm DISPDRIVER=wayland VERSION=$VERSION make deb
|
||||
- run: sha256sum Ludo-Linux-wayland-arm-${VERSION}.tar.gz > Ludo-Linux-wayland-arm-${VERSION}.tar.gz.sha256
|
||||
|
||||
- name: Upload Release Asset
|
||||
uses: alexellis/[email protected]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
asset_paths: '["./Ludo-*.tar.gz*", "./*.deb"]'
|
||||
|
||||
build_linux_wayland_arm64:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: echo "VERSION=$(echo ${{ github.event.release.tag_name }} | cut -c 2-)" >> $GITHUB_ENV
|
||||
- run: sudo apt update -q
|
||||
- run: sudo apt install binutils-multiarch
|
||||
- run: sudo dpkg --add-architecture arm64
|
||||
- run: sudo rm -rf /etc/apt/sources.list.d/*
|
||||
- run: echo "" | sudo tee /etc/apt/sources.list
|
||||
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157
|
||||
- run: sudo apt update -q
|
||||
- run: sudo apt install -f libc6-dev:arm64 gcc-aarch64-linux-gnu libopenal-dev:arm64 libgl1-mesa-dev:arm64 libxcursor-dev:arm64 libxrandr-dev:arm64 libxinerama-dev:arm64 libxi-dev:arm64 libwayland-dev:arm64 libxkbcommon-dev:arm64 -y --allow-unauthenticated
|
||||
- run: PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/ GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc OS=Linux ARCH=arm64 DISPDRIVER=wayland VERSION=$VERSION make tar
|
||||
- run: PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/ GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc OS=Linux ARCH=arm64 DISPDRIVER=wayland VERSION=$VERSION make deb
|
||||
- run: sha256sum Ludo-Linux-wayland-arm64-${VERSION}.tar.gz > Ludo-Linux-wayland-arm64-${VERSION}.tar.gz.sha256
|
||||
|
||||
- name: Upload Release Asset
|
||||
uses: alexellis/[email protected]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
asset_paths: '["./Ludo-*.tar.gz*", "./*.deb"]'
|
||||
|
||||
build_linux_x11_x86_64:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: echo "VERSION=$(echo ${{ github.event.release.tag_name }} | cut -c 2-)" >> $GITHUB_ENV
|
||||
- run: sudo apt-get update -q
|
||||
- run: sudo apt-get install libopenal-dev xorg-dev libgl1-mesa-dev -y --allow-unauthenticated
|
||||
- run: OS=Linux ARCH=x86_64 VERSION=$VERSION make tar
|
||||
- run: OS=Linux ARCH=x86_64 VERSION=$VERSION make deb
|
||||
- run: sha256sum Ludo-Linux-x86_64-${VERSION}.tar.gz > Ludo-Linux-x86_64-${VERSION}.tar.gz.sha256
|
||||
- run: go build -tags x11 -v
|
||||
- run: OS=Linux ARCH=x86_64 DISPDRIVER=x11 VERSION=$VERSION make tar
|
||||
- run: OS=Linux ARCH=x86_64 DISPDRIVER=x11 VERSION=$VERSION make deb
|
||||
- run: sha256sum Ludo-Linux-x11-x86_64-${VERSION}.tar.gz > Ludo-Linux-x11-x86_64-${VERSION}.tar.gz.sha256
|
||||
|
||||
- name: Upload Release Asset
|
||||
uses: alexellis/[email protected]
|
||||
@@ -30,31 +134,36 @@ jobs:
|
||||
with:
|
||||
asset_paths: '["./Ludo-*.tar.gz*", "./*.deb"]'
|
||||
|
||||
build_linux_arm:
|
||||
runs-on: ubuntu-18.04
|
||||
build_linux_x11_armhf:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: echo "VERSION=$(echo ${{ github.event.release.tag_name }} | cut -c 2-)" >> $GITHUB_ENV
|
||||
- run: sudo apt update -q
|
||||
- run: sudo apt install binutils-multiarch
|
||||
- run: sudo dpkg --add-architecture armhf
|
||||
- run: sudo rm -rf /etc/apt/sources.list.d/*s
|
||||
- run: echo "" | sudo tee /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ bionic main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports bionic main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ bionic-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports bionic-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ bionic-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports bionic-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157
|
||||
- run: sudo apt update -q
|
||||
- run: sudo apt install -f libgl1-mesa-dev:amd64 libc6-dev:armhf gcc-arm-linux-gnueabihf libopenal-dev:armhf libgl1-mesa-dev:armhf libxcursor-dev:armhf libxrandr-dev:armhf libxinerama-dev:armhf libxi-dev:armhf -y --allow-unauthenticated
|
||||
- run: export PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig/
|
||||
- run: GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc go build -v
|
||||
- run: OS=Linux ARCH=arm VERSION=$VERSION make tar
|
||||
- run: OS=Linux ARCH=arm VERSION=$VERSION make deb
|
||||
- run: sha256sum Ludo-Linux-arm-${VERSION}.tar.gz > Ludo-Linux-arm-${VERSION}.tar.gz.sha256
|
||||
- run: sudo apt install -f libxxf86vm-dev:armhf libc6-dev:armhf gcc-arm-linux-gnueabihf libopenal-dev:armhf libgl1-mesa-dev:armhf libxcursor-dev:armhf libxrandr-dev:armhf libxinerama-dev:armhf libxi-dev:armhf -y --allow-unauthenticated
|
||||
- run: PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig/ GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc go build -tags x11 -v
|
||||
- run: OS=Linux ARCH=arm DISPDRIVER=x11 VERSION=$VERSION make tar
|
||||
- run: OS=Linux ARCH=arm DISPDRIVER=x11 VERSION=$VERSION make deb
|
||||
- run: sha256sum Ludo-Linux-x11-arm-${VERSION}.tar.gz > Ludo-Linux-x11-arm-${VERSION}.tar.gz.sha256
|
||||
|
||||
- name: Upload Release Asset
|
||||
uses: alexellis/[email protected]
|
||||
@@ -63,28 +172,65 @@ jobs:
|
||||
with:
|
||||
asset_paths: '["./Ludo-*.tar.gz*", "./*.deb"]'
|
||||
|
||||
build_osx:
|
||||
runs-on: macos-latest
|
||||
build_linux_x11_arm64:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: echo "VERSION=$(echo ${{ github.event.release.tag_name }} | cut -c 2-)" >> $GITHUB_ENV
|
||||
- run: sudo apt update -q
|
||||
- run: sudo apt install binutils-multiarch
|
||||
- run: sudo dpkg --add-architecture arm64
|
||||
- run: sudo rm -rf /etc/apt/sources.list.d/*
|
||||
- run: echo "" | sudo tee /etc/apt/sources.list
|
||||
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157
|
||||
- run: sudo apt update -q
|
||||
- run: sudo apt install -f libxxf86vm-dev:arm64 libc6-dev:arm64 gcc-aarch64-linux-gnu libopenal-dev:arm64 libgl1-mesa-dev:arm64 libxcursor-dev:arm64 libxrandr-dev:arm64 libxinerama-dev:arm64 libxi-dev:arm64 -y --allow-unauthenticated
|
||||
- run: PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/ GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc OS=Linux ARCH=arm64 DISPDRIVER=x11 VERSION=$VERSION make tar
|
||||
- run: PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/ GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc OS=Linux ARCH=arm64 DISPDRIVER=x11 VERSION=$VERSION make deb
|
||||
- run: sha256sum Ludo-Linux-x11-arm64-${VERSION}.tar.gz > Ludo-Linux-x11-arm64-${VERSION}.tar.gz.sha256
|
||||
|
||||
- name: Upload Release Asset
|
||||
uses: alexellis/[email protected]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
asset_paths: '["./Ludo-*.tar.gz*", "./*.deb"]'
|
||||
|
||||
build_osx_x86_64:
|
||||
runs-on: macos-26-intel
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: echo "VERSION=$(echo ${{ github.event.release.tag_name }} | cut -c 2-)" >> $GITHUB_ENV
|
||||
- run: echo "/Users/runner/go/bin" >> $GITHUB_PATH
|
||||
- run: go get golang.org/x/lint/golint
|
||||
- run: go get honnef.co/go/tools/cmd/staticcheck
|
||||
- run: go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
- run: brew install openal-soft
|
||||
- run: echo ${{ secrets.OSXCERT }} | base64 --decode > dev.p12
|
||||
- run: printf '%s' '${{ secrets.OSXCERT }}' | base64 --decode > dev.p12
|
||||
- run: security create-keychain -p github build.keychain
|
||||
- run: security default-keychain -s build.keychain
|
||||
- run: security unlock-keychain -p github build.keychain
|
||||
- run: security import dev.p12 -k build.keychain -P ${{ secrets.OSXCERTPASS }} -T /usr/bin/codesign && rm dev.p12
|
||||
- run: security import dev.p12 -f pkcs12 -k build.keychain -P ${{ secrets.OSXCERTPASS }} -T /usr/bin/codesign && rm dev.p12
|
||||
- run: security set-key-partition-list -S "apple-tool:,apple:,codesign:" -s -k github build.keychain
|
||||
- run: security list-keychains -d user -s build.keychain
|
||||
- run: OS=OSX ARCH=x86_64 VERSION=$VERSION make dmg
|
||||
- run: echo "REQUESTUUID=$(xcrun altool --notarize-app -t osx -f Ludo-OSX-x86_64-${VERSION}.dmg --primary-bundle-id com.libretro.ludo -u ${{ secrets.APPLEID }} -p ${{ secrets.APPLEIDPASS }} -itc_provider ZE9XE938Z2 | awk '/RequestUUID/ { print $NF; }')" >> $GITHUB_ENV
|
||||
- run: sleep 180
|
||||
- run: xcrun altool --notarization-info $REQUESTUUID -u ${{ secrets.APPLEID }} -p ${{ secrets.APPLEIDPASS }} -ascprovider ZE9XE938Z2
|
||||
- run: xcrun notarytool submit Ludo-OSX-x86_64-${VERSION}.dmg --apple-id ${{ secrets.APPLEID }} --password ${{ secrets.APPLEIDPASS }} --team-id ZE9XE938Z2 --wait
|
||||
- run: xcrun stapler staple Ludo-OSX-x86_64-${VERSION}.dmg
|
||||
- run: xcrun stapler validate Ludo-OSX-x86_64-${VERSION}.dmg
|
||||
- run: shasum -a 256 Ludo-OSX-x86_64-${VERSION}.dmg > Ludo-OSX-x86_64-${VERSION}.dmg.sha256
|
||||
@@ -96,27 +242,77 @@ jobs:
|
||||
with:
|
||||
asset_paths: '["./Ludo-*.dmg*"]'
|
||||
|
||||
build_windows:
|
||||
runs-on: windows-latest
|
||||
build_osx_arm64:
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: echo "VERSION=$(echo ${{ github.event.release.tag_name }} | cut -c 2-)" >> $GITHUB_ENV
|
||||
- run: choco install wget make hashdeep --ignore-checksums
|
||||
- run: wget --no-check-certificate http://www.openal-soft.org/openal-binaries/openal-soft-1.21.0-bin.zip
|
||||
- run: wget https://github.com/electron/rcedit/releases/download/v1.1.1/rcedit-x64.exe
|
||||
- run: 7z x openal-soft-1.21.0-bin.zip -o/c/
|
||||
- run: echo "CGO_CFLAGS=-I/c/openal-soft-1.21.0-bin/include/" >> $GITHUB_ENV
|
||||
- run: echo "CGO_LDFLAGS=-L/c/openal-soft-1.21.0-bin/libs/Win64/" >> $GITHUB_ENV
|
||||
- run: cp /c/ProgramData/chocolatey/lib/mingw/tools/install/mingw64/x86_64-w64-mingw32/lib/libwinpthread* .
|
||||
- run: cp /c/openal-soft-1.21.0-bin/bin/Win64/soft_oal.dll OpenAL32.dll
|
||||
- run: echo "/Users/runner/go/bin" >> $GITHUB_PATH
|
||||
- run: go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
- run: brew install openal-soft
|
||||
- run: printf '%s' '${{ secrets.OSXCERT }}' | base64 --decode > dev.p12
|
||||
- run: security create-keychain -p github build.keychain
|
||||
- run: security default-keychain -s build.keychain
|
||||
- run: security unlock-keychain -p github build.keychain
|
||||
- run: security import dev.p12 -f pkcs12 -k build.keychain -P ${{ secrets.OSXCERTPASS }} -T /usr/bin/codesign && rm dev.p12
|
||||
- run: security set-key-partition-list -S "apple-tool:,apple:,codesign:" -s -k github build.keychain
|
||||
- run: security list-keychains -d user -s build.keychain
|
||||
- run: OS=OSX ARCH=arm64 VERSION=$VERSION make dmg
|
||||
- run: xcrun notarytool submit Ludo-OSX-arm64-${VERSION}.dmg --apple-id ${{ secrets.APPLEID }} --password ${{ secrets.APPLEIDPASS }} --team-id ZE9XE938Z2 --wait
|
||||
- run: xcrun stapler staple Ludo-OSX-arm64-${VERSION}.dmg
|
||||
- run: xcrun stapler validate Ludo-OSX-arm64-${VERSION}.dmg
|
||||
- run: shasum -a 256 Ludo-OSX-arm64-${VERSION}.dmg > Ludo-OSX-arm64-${VERSION}.dmg.sha256
|
||||
|
||||
- name: Upload Release Asset
|
||||
uses: alexellis/[email protected]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
asset_paths: '["./Ludo-*.dmg*"]'
|
||||
|
||||
build_windows_x86_64:
|
||||
runs-on: windows-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: msys2 {0}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- uses: msys2/setup-msys2@v2
|
||||
with:
|
||||
msystem: UCRT64
|
||||
update: true
|
||||
path-type: inherit
|
||||
install: >-
|
||||
mingw-w64-ucrt-x86_64-toolchain
|
||||
mingw-w64-ucrt-x86_64-openal
|
||||
make
|
||||
p7zip
|
||||
mingw-w64-ucrt-x86_64-rcedit
|
||||
- run: echo "VERSION=$(echo ${{ github.event.release.tag_name }} | cut -c 2-)" >> "$GITHUB_ENV"
|
||||
- run: cp /ucrt64/bin/rcedit.exe ./rcedit-x64
|
||||
- run: cp /ucrt64/lib/libopenal.dll.a ./libOpenAL32.dll.a
|
||||
- run: echo "CGO_LDFLAGS=-L$PWD" >> "$GITHUB_ENV"
|
||||
- run: cp /ucrt64/bin/libgcc_s_seh-1.dll .
|
||||
- run: cp /ucrt64/bin/libmysofa.dll .
|
||||
- run: cp /ucrt64/bin/libopenal-1.dll .
|
||||
- run: cp /ucrt64/bin/libstdc++-6.dll .
|
||||
- run: cp /ucrt64/bin/libwinpthread-1.dll .
|
||||
- run: cp /ucrt64/bin/zlib1.dll .
|
||||
- run: cp /c/Windows/System32/VCRUNTIME140.dll .
|
||||
- run: mkdir -p ./Ludo-Windows-x86_64-${VERSION}/
|
||||
- run: cp *.dll ./Ludo-Windows-x86_64-${VERSION}/
|
||||
- run: OS=Windows ARCH=x86_64 VERSION=$VERSION make zip
|
||||
- run: hashdeep -c sha256 Ludo-Windows-x86_64-${VERSION}.zip > Ludo-Windows-x86_64-${VERSION}.zip.sha256
|
||||
- run: sha256sum Ludo-Windows-x86_64-${VERSION}.zip > Ludo-Windows-x86_64-${VERSION}.zip.sha256
|
||||
|
||||
- name: Upload Release Asset
|
||||
uses: alexellis/[email protected]
|
||||
|
||||
+179
-44
@@ -1,6 +1,8 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
branches: [master]
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
@@ -11,83 +13,216 @@ defaults:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
build_linux:
|
||||
build_linux_wayland_x86_64:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: echo "/home/runner/go/bin" >> $GITHUB_PATH
|
||||
- run: go get golang.org/x/lint/golint
|
||||
- run: go get honnef.co/go/tools/cmd/staticcheck
|
||||
- run: go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
- run: sudo apt-get update -q
|
||||
- run: sudo apt-get install libopenal-dev xorg-dev libgl1-mesa-dev -y --allow-unauthenticated
|
||||
- run: sudo apt-get install libopenal-dev xorg-dev libgl1-mesa-dev libwayland-dev libxkbcommon-dev -y --allow-unauthenticated
|
||||
- run: go get .
|
||||
- run: xvfb-run -a go test -v -race ./...
|
||||
- run: go vet ./...
|
||||
- run: staticcheck ./...
|
||||
- run: golint -set_exit_status ./...
|
||||
- run: go build -v
|
||||
- run: go build -tags wayland -v
|
||||
|
||||
build_linux_arm:
|
||||
runs-on: ubuntu-18.04
|
||||
build_linux_wayland_armhf:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v2
|
||||
- run: sudo apt-get install binutils-multiarch
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: sudo apt update -q
|
||||
- run: sudo apt install binutils-multiarch
|
||||
- run: sudo dpkg --add-architecture armhf
|
||||
- run: sudo rm -rf /etc/apt/sources.list.d/*
|
||||
- run: echo "" | sudo tee /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ bionic main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports bionic main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ bionic-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports bionic-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ bionic-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports bionic-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157
|
||||
- run: sudo apt update -q
|
||||
- run: sudo apt install -f libgl1-mesa-dev:amd64 libc6-dev:armhf gcc-arm-linux-gnueabihf libopenal-dev:armhf libgl1-mesa-dev:armhf libxcursor-dev:armhf libxrandr-dev:armhf libxinerama-dev:armhf libxi-dev:armhf -y --allow-unauthenticated
|
||||
- run: export PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig/
|
||||
- run: GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc go build -v
|
||||
- run: sudo apt install -f libc6-dev:armhf gcc-arm-linux-gnueabihf libopenal-dev:armhf libgl1-mesa-dev:armhf libxcursor-dev:armhf libxrandr-dev:armhf libxinerama-dev:armhf libxi-dev:armhf libwayland-dev:armhf libxkbcommon-dev:armhf -y --allow-unauthenticated
|
||||
- run: PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig/ GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc go build -tags wayland -v
|
||||
|
||||
build_osx:
|
||||
runs-on: macos-latest
|
||||
build_linux_wayland_arm64:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: sudo apt update -q
|
||||
- run: sudo apt install binutils-multiarch
|
||||
- run: sudo dpkg --add-architecture arm64
|
||||
- run: sudo rm -rf /etc/apt/sources.list.d/*
|
||||
- run: echo "" | sudo tee /etc/apt/sources.list
|
||||
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157
|
||||
- run: sudo apt update -q
|
||||
- run: sudo apt install -f libc6-dev:arm64 gcc-aarch64-linux-gnu libopenal-dev:arm64 libgl1-mesa-dev:arm64 libxcursor-dev:arm64 libxrandr-dev:arm64 libxinerama-dev:arm64 libxi-dev:arm64 libwayland-dev:arm64 libxkbcommon-dev:arm64 -y --allow-unauthenticated
|
||||
- run: PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/ GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc go build -tags wayland -v
|
||||
|
||||
build_linux_x11_x86_64:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: echo "/home/runner/go/bin" >> $GITHUB_PATH
|
||||
- run: go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
- run: sudo apt-get update -q
|
||||
- run: sudo apt-get install libopenal-dev xorg-dev libgl1-mesa-dev libxkbcommon-dev -y --allow-unauthenticated
|
||||
- run: go get .
|
||||
- run: xvfb-run -a go test -tags x11 -v -race ./...
|
||||
- run: go vet -tags x11 ./...
|
||||
- run: staticcheck -tags x11 ./...
|
||||
- run: go build -tags x11 -v
|
||||
|
||||
build_linux_x11_armhf:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: sudo apt update -q
|
||||
- run: sudo apt-get install binutils-multiarch
|
||||
- run: sudo dpkg --add-architecture armhf
|
||||
- run: sudo rm -rf /etc/apt/sources.list.d/*s
|
||||
- run: echo "" | sudo tee /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157
|
||||
- run: sudo apt update -q
|
||||
- run: sudo apt install -f libxxf86vm-dev:armhf libc6-dev:armhf gcc-arm-linux-gnueabihf libopenal-dev:armhf libgl1-mesa-dev:armhf libxcursor-dev:armhf libxrandr-dev:armhf libxinerama-dev:armhf libxi-dev:armhf -y --allow-unauthenticated
|
||||
- run: PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig/ GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc go build -tags x11 -v
|
||||
|
||||
build_linux_x11_arm64:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: sudo apt update -q
|
||||
- run: sudo apt install binutils-multiarch
|
||||
- run: sudo dpkg --add-architecture arm64
|
||||
- run: sudo rm -rf /etc/apt/sources.list.d/*
|
||||
- run: echo "" | sudo tee /etc/apt/sources.list
|
||||
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: echo "deb [arch=amd64] http://us.archive.ubuntu.com/ubuntu/ noble-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
- run: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157
|
||||
- run: sudo apt update -q
|
||||
- run: sudo apt install -f libxxf86vm-dev:arm64 libc6-dev:arm64 gcc-aarch64-linux-gnu libopenal-dev:arm64 libgl1-mesa-dev:arm64 libxcursor-dev:arm64 libxrandr-dev:arm64 libxinerama-dev:arm64 libxi-dev:arm64 -y --allow-unauthenticated
|
||||
- run: PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/ GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc go build -tags x11 -v
|
||||
|
||||
build_osx_x86_64:
|
||||
runs-on: macos-26-intel
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: echo "/Users/runner/go/bin" >> $GITHUB_PATH
|
||||
- run: go get golang.org/x/lint/golint
|
||||
- run: go get honnef.co/go/tools/cmd/staticcheck
|
||||
- run: go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
- run: brew install openal-soft
|
||||
- run: go get .
|
||||
- run: go test -v -race ./...
|
||||
- run: go vet ./...
|
||||
- run: staticcheck ./...
|
||||
- run: golint -set_exit_status ./...
|
||||
- run: go build -v
|
||||
|
||||
build_windows:
|
||||
runs-on: windows-latest
|
||||
build_osx_arm64:
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v2
|
||||
- run: echo "/c/Users/runneradmin/go/bin" >> $GITHUB_PATH
|
||||
- run: go get golang.org/x/lint/golint
|
||||
- run: go get honnef.co/go/tools/cmd/staticcheck
|
||||
- run: choco install wget --ignore-checksums
|
||||
- run: wget --no-check-certificate http://www.openal-soft.org/openal-binaries/openal-soft-1.21.0-bin.zip
|
||||
- run: 7z x openal-soft-1.21.0-bin.zip -o/c/
|
||||
- run: echo "CGO_CFLAGS=-I/c/openal-soft-1.21.0-bin/include/" >> $GITHUB_ENV
|
||||
- run: echo "CGO_LDFLAGS=-L/c/openal-soft-1.21.0-bin/libs/Win64/" >> $GITHUB_ENV
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: echo "/Users/runner/go/bin" >> $GITHUB_PATH
|
||||
- run: go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
- run: brew install openal-soft
|
||||
- run: go get .
|
||||
#- run: go test -v -race ./...
|
||||
#- run: go vet ./...
|
||||
- run: go test -v -race ./...
|
||||
- run: go vet ./...
|
||||
- run: staticcheck ./...
|
||||
- run: go build -v
|
||||
|
||||
build_windows_x86_64:
|
||||
runs-on: windows-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: msys2 {0}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- uses: msys2/setup-msys2@v2
|
||||
with:
|
||||
msystem: UCRT64
|
||||
update: true
|
||||
path-type: inherit
|
||||
install: >-
|
||||
mingw-w64-ucrt-x86_64-toolchain
|
||||
mingw-w64-ucrt-x86_64-openal
|
||||
- run: echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
|
||||
- run: go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
# golang.org/x/mobile/exp/audio/al hardcodes -lOpenAL32 on Windows,
|
||||
# while the MSYS2 package installs libopenal.dll.a.
|
||||
- run: cp /ucrt64/lib/libopenal.dll.a ./libOpenAL32.dll.a
|
||||
- run: echo "CGO_LDFLAGS=-L$PWD" >> "$GITHUB_ENV"
|
||||
- run: go get .
|
||||
- run: go test -v -race ./...
|
||||
- run: go vet ./...
|
||||
- run: staticcheck ./...
|
||||
- run: golint -set_exit_status ./...
|
||||
- run: go build -v -ldflags '-H=windowsgui'
|
||||
@@ -2,18 +2,31 @@ APP ?= Ludo
|
||||
ARCH ?= x86_64
|
||||
VERSION ?= dev
|
||||
BUNDLENAME = $(APP)-$(OS)-$(ARCH)-$(VERSION)
|
||||
DISPDRIVER ?= x11
|
||||
|
||||
CORES = atari800 bluemsx swanstation fbneo fceumm gambatte genesis_plus_gx handy lutro mednafen_ngp mednafen_pce mednafen_pce_fast mednafen_pcfx mednafen_psx mednafen_saturn mednafen_supergrafx mednafen_vb mednafen_wswan mgba melonds np2kai o2em pcsx_rearmed picodrive pokemini prosystem snes9x stella2014 vecx virtualjaguar
|
||||
BUILDBOTURL_BASE ?= https://buildbot.libretro.com/nightly
|
||||
|
||||
ifeq ($(ARCH), arm)
|
||||
CORES := $(filter-out swanstation,$(CORES))
|
||||
CORES := $(filter-out mednafen_pcfx,$(CORES))
|
||||
CORES := $(filter-out mednafen_saturn,$(CORES))
|
||||
CORES := $(filter-out melonds,$(CORES))
|
||||
ifeq ($(OS), Linux)
|
||||
TAGS = -tags=$(DISPDRIVER)
|
||||
BUNDLENAME = $(APP)-$(OS)-$(DISPDRIVER)-$(ARCH)-$(VERSION)
|
||||
endif
|
||||
|
||||
ifeq ($(OS), Windows)
|
||||
CORES += mupen64plus_next
|
||||
CORES = ppsspp dolphin atari800 bluemsx citra swanstation fbneo fceumm flycast gambatte gearsystem genesis_plus_gx handy lutro mednafen_ngp mednafen_pce mednafen_pce_fast mednafen_pcfx mednafen_psx mednafen_saturn mednafen_supergrafx mednafen_vb mednafen_wswan mgba melonds mupen64plus_next np2kai o2em pcsx2 pcsx_rearmed picodrive pokemini prosystem snes9x stella2014 vecx virtualjaguar
|
||||
|
||||
ifeq ($(ARCH), arm)
|
||||
CORES := $(filter-out citra dolphin flycast mednafen_pcfx mednafen_saturn melonds mupen64plus_next pcsx2 ppsspp swanstation,$(CORES))
|
||||
endif
|
||||
ifeq ($(ARCH), arm64)
|
||||
CORES := $(filter-out pcsx2,$(CORES))
|
||||
ifeq ($(OS), Linux)
|
||||
CORES := $(filter-out mupen64plus_next,$(CORES))
|
||||
endif
|
||||
endif
|
||||
ifeq ($(OS), OSX)
|
||||
CORES := $(filter-out pcsx2,$(CORES))
|
||||
ifeq ($(ARCH), x86_64)
|
||||
CORES := $(filter-out citra,$(CORES))
|
||||
endif
|
||||
endif
|
||||
|
||||
DYLIBS = $(addprefix cores/, $(addsuffix _libretro.dylib,$(CORES)))
|
||||
@@ -21,32 +34,36 @@ DLLS = $(addprefix cores/, $(addsuffix _libretro.dll,$(CORES)))
|
||||
SOBJS = $(addprefix cores/, $(addsuffix _libretro.so,$(CORES)))
|
||||
|
||||
ifeq ($(OS), OSX)
|
||||
BUILDBOTURL=http://buildbot.libretro.com/nightly/apple/osx/$(ARCH)/latest
|
||||
BUILDBOTURL=$(BUILDBOTURL_BASE)/apple/osx/$(ARCH)/latest
|
||||
endif
|
||||
ifeq ($(OS), Linux)
|
||||
ifeq ($(ARCH), arm)
|
||||
BUILDBOTURL=http://buildbot.libretro.com/nightly/linux/armv7-neon-hf/latest
|
||||
BUILDBOTURL=$(BUILDBOTURL_BASE)/linux/armv7-neon-hf/latest
|
||||
else ifeq ($(ARCH), arm64)
|
||||
BUILDBOTURL=$(BUILDBOTURL_BASE)/linux/aarch64/latest
|
||||
else
|
||||
BUILDBOTURL=http://buildbot.libretro.com/nightly/linux/$(ARCH)/latest
|
||||
BUILDBOTURL=$(BUILDBOTURL_BASE)/linux/$(ARCH)/latest
|
||||
endif
|
||||
endif
|
||||
ifeq ($(OS), Windows)
|
||||
BUILDBOTURL=http://buildbot.libretro.com/nightly/windows/$(ARCH)/latest
|
||||
BUILDBOTURL=$(BUILDBOTURL_BASE)/windows/$(ARCH)/latest
|
||||
endif
|
||||
|
||||
ludo:
|
||||
go build
|
||||
go build $(TAGS)
|
||||
|
||||
ludo.exe:
|
||||
go build -ldflags '-H=windowsgui'
|
||||
|
||||
cores/%_libretro.dylib cores/%_libretro.dll cores/%_libretro.so:
|
||||
cores/%_libretro.dylib cores/%_libretro.dll:
|
||||
mkdir -p cores
|
||||
@if [ $(basename $(@F)) = swanstation_libretro ]; then\
|
||||
wget -c https://github.com/kivutar/swanstation/releases/download/v2021-08-01/swanstation-$(OS)-$(ARCH).zip -O $@.zip;\
|
||||
else\
|
||||
wget -c $(BUILDBOTURL)/$(@F).zip -O $@.zip;\
|
||||
fi
|
||||
wget -c $(BUILDBOTURL)/$(@F).zip -O $@.zip;\
|
||||
unzip $@.zip -d cores
|
||||
rm $@.zip
|
||||
|
||||
cores/%_libretro.so:
|
||||
mkdir -p cores
|
||||
wget -c $(BUILDBOTURL)/$(@F).zip -O $@.zip;\
|
||||
unzip $@.zip -d cores
|
||||
rm $@.zip
|
||||
|
||||
@@ -60,7 +77,7 @@ $(APP).app: ludo $(DYLIBS)
|
||||
cp -r database $(APP).app/Contents/Resources
|
||||
cp -r assets $(APP).app/Contents/Resources
|
||||
cp -r cores $(APP).app/Contents/Resources
|
||||
codesign --force --options runtime --verbose --timestamp --sign "7069CC8A4AE9AFF0493CC539BBA4FA345F0A668B" \
|
||||
codesign --force --options runtime --verbose --timestamp --sign "Developer ID Application: Jean-Andre Santoni (ZE9XE938Z2)" \
|
||||
--entitlements pkg/entitlements.xml $(APP).app/Contents/Resources/cores/*.dylib
|
||||
rm -rf $(APP).app/Contents/Resources/database/.git
|
||||
rm -rf $(APP).app/Contents/Resources/assets/.git
|
||||
@@ -74,29 +91,28 @@ $(APP).app: ludo $(DYLIBS)
|
||||
sips -z 512 512 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/[email protected]
|
||||
sips -z 512 512 assets/icon.png --out $(APP).app/Contents/Resources/$(APP).iconset/icon_512x512.png
|
||||
cp ludo $(APP).app/Contents/MacOS
|
||||
codesign --force --options runtime --verbose --timestamp --sign "7069CC8A4AE9AFF0493CC539BBA4FA345F0A668B" \
|
||||
codesign --force --options runtime --verbose --timestamp --sign "Developer ID Application: Jean-Andre Santoni (ZE9XE938Z2)" \
|
||||
--entitlements pkg/entitlements.xml $(APP).app/Contents/MacOS/ludo
|
||||
iconutil -c icns -o $(APP).app/Contents/Resources/$(APP).icns $(APP).app/Contents/Resources/$(APP).iconset
|
||||
rm -rf $(APP).app/Contents/Resources/$(APP).iconset
|
||||
codesign --force --options runtime --verbose --timestamp --sign "7069CC8A4AE9AFF0493CC539BBA4FA345F0A668B" \
|
||||
codesign --force --options runtime --verbose --timestamp --sign "Developer ID Application: Jean-Andre Santoni (ZE9XE938Z2)" \
|
||||
--entitlements pkg/entitlements.xml $(APP).app
|
||||
|
||||
empty.dmg:
|
||||
mkdir -p template
|
||||
hdiutil create -fs HFSX -layout SPUD -size 200m empty.dmg -srcfolder template -format UDRW -volname $(BUNDLENAME) -quiet
|
||||
rmdir template
|
||||
empty.sparseimage:
|
||||
rm -rf empty.sparseimage
|
||||
hdiutil create -fs HFSX -layout SPUD -size 300m empty.sparseimage -type SPARSE -volname $(BUNDLENAME)
|
||||
|
||||
# For OSX
|
||||
dmg: empty.dmg $(APP).app
|
||||
dmg: empty.sparseimage $(APP).app
|
||||
mkdir -p wc
|
||||
hdiutil attach empty.dmg -noautoopen -quiet -mountpoint wc
|
||||
hdiutil attach empty.sparseimage -noautoopen -quiet -mountpoint wc
|
||||
rm -rf wc/$(APP).app
|
||||
ditto -rsrc $(APP).app wc/$(APP).app
|
||||
ln -s /Applications wc/Applications
|
||||
ln -sf /Applications wc/Applications
|
||||
WC_DEV=`hdiutil info | grep wc | grep "Apple_HFS" | awk '{print $$1}'` && hdiutil detach $$WC_DEV -quiet -force
|
||||
rm -f $(BUNDLENAME)-*.dmg
|
||||
hdiutil convert empty.dmg -quiet -format UDZO -imagekey zlib-level=9 -o $(BUNDLENAME).dmg
|
||||
codesign --force --options runtime --verbose --timestamp --sign "7069CC8A4AE9AFF0493CC539BBA4FA345F0A668B" \
|
||||
hdiutil convert empty.sparseimage -quiet -format UDZO -imagekey zlib-level=9 -o $(BUNDLENAME).dmg
|
||||
codesign --force --options runtime --verbose --timestamp --sign "Developer ID Application: Jean-Andre Santoni (ZE9XE938Z2)" \
|
||||
--entitlements pkg/entitlements.xml $(BUNDLENAME).dmg
|
||||
|
||||
# For Windows
|
||||
@@ -124,7 +140,10 @@ DEB_ARCH = amd64
|
||||
ifeq ($(ARCH), arm)
|
||||
DEB_ARCH = armhf
|
||||
endif
|
||||
DEB_ROOT = ludo_$(VERSION)-1_$(DEB_ARCH)
|
||||
ifeq ($(ARCH), arm64)
|
||||
DEB_ARCH = arm64
|
||||
endif
|
||||
DEB_ROOT = ludo-$(DISPDRIVER)_$(VERSION)-1_$(DEB_ARCH)
|
||||
|
||||
deb: ludo $(SOBJS)
|
||||
mkdir -p $(DEB_ROOT)/DEBIAN
|
||||
@@ -151,4 +170,4 @@ deb: ludo $(SOBJS)
|
||||
dpkg-deb --build $(DEB_ROOT)
|
||||
|
||||
clean:
|
||||
rm -rf Ludo.app ludo wc *.dmg *.deb $(BUNDLENAME)-* cores/
|
||||
rm -rf Ludo.app ludo wc *.dmg *.sparseimage *.deb $(BUNDLENAME)-* cores/
|
||||
@@ -4,7 +4,7 @@ Ludo is a work in progress libretro frontend written in go.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/kivutar/ludo-assets/master/illustration.png" />
|
||||
|
||||
It is able to launch most non GL libretro cores.
|
||||
It supports software-rendered and desktop OpenGL libretro cores. Hardware-rendered cores may require a newer OpenGL version than the frontend itself.
|
||||
|
||||
It works on OSX, Linux, Linux ARM and Windows. You can download releases [here](https://github.com/libretro/ludo/releases)
|
||||
|
||||
|
||||
+1
-1
Submodule assets updated: e72d3737fb...605aeae14a.
+198
-70
@@ -4,13 +4,15 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/libretro/ludo/audio"
|
||||
"github.com/libretro/ludo/input"
|
||||
@@ -20,6 +22,8 @@ import (
|
||||
"github.com/libretro/ludo/savefiles"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/video"
|
||||
|
||||
"github.com/mholt/archives"
|
||||
)
|
||||
|
||||
var vid *video.Video
|
||||
@@ -31,11 +35,12 @@ var Options *options.Options
|
||||
// Call Init before calling other functions of this package.
|
||||
func Init(v *video.Video) {
|
||||
vid = v
|
||||
libretro.SetContextCallbacks(v.ContextCallbacks())
|
||||
}
|
||||
|
||||
// Load loads a libretro core
|
||||
func Load(sofile string) error {
|
||||
// In case the a core is already loaded, we need to close it properly
|
||||
// In case a core is already loaded, we need to close it properly
|
||||
// before loading the new core
|
||||
Unload()
|
||||
|
||||
@@ -47,10 +52,11 @@ func Load(sofile string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vid.SetHWRenderContext(nil)
|
||||
state.Core.SetEnvironment(environment)
|
||||
state.Core.Init()
|
||||
state.Core.SetVideoRefresh(vid.Refresh)
|
||||
state.Core.SetInputPoll(input.Poll)
|
||||
state.Core.SetInputPoll(func() {})
|
||||
state.Core.SetInputState(input.State)
|
||||
state.Core.SetAudioSample(audio.Sample)
|
||||
state.Core.SetAudioSampleBatch(audio.SampleBatch)
|
||||
@@ -71,54 +77,141 @@ func Load(sofile string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// unzipGame unzips a rom to tmpdir and returns the path and size of the extracted ROM.
|
||||
// In case the zip contains more than one file, they are all extracted and the
|
||||
// first one is passed to the libretro core.
|
||||
func unzipGame(filename string) (string, int64, error) {
|
||||
r, err := zip.OpenReader(filename)
|
||||
// unarchiveGame unarchives a rom to tmpdir and returns the path and size of the extracted ROM.
|
||||
// In case the archive contains more than one file, they are all extracted and the
|
||||
// first one or a better match (cue for CDrom) is passed to the libretro core.
|
||||
func unarchiveGame(filename string) (string, int64, error) {
|
||||
path := ""
|
||||
size := int64(0)
|
||||
dst := os.TempDir()
|
||||
ctx := context.Background()
|
||||
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return path, size, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
format, stream, err := archives.Identify(ctx, filepath.Base(filename), file)
|
||||
if err != nil {
|
||||
return path, size, err
|
||||
}
|
||||
|
||||
extPriority := 0
|
||||
extPrefered := map[string]int{
|
||||
".cue": 1,
|
||||
".m3u": 2,
|
||||
".pbp": 3,
|
||||
}
|
||||
|
||||
selectGame := func(gamePath string, fsize int64) {
|
||||
ext := filepath.Ext(gamePath)
|
||||
if size == 0 {
|
||||
path = gamePath
|
||||
size = fsize
|
||||
log.Println("first file in archive:", path, size)
|
||||
}
|
||||
priority, ok := extPrefered[strings.ToLower(ext)]
|
||||
if ok && priority > extPriority {
|
||||
extPriority = priority
|
||||
path = gamePath
|
||||
size = fsize
|
||||
log.Println("find a better file in archive:", path, size)
|
||||
}
|
||||
}
|
||||
|
||||
if ex, ok := format.(archives.Extractor); ok {
|
||||
err = ex.Extract(ctx, stream, func(ctx context.Context, f archives.FileInfo) error {
|
||||
fname, err := archiveOutputPath(dst, f.NameInArchive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if f.IsDir() {
|
||||
return os.MkdirAll(fname, os.ModePerm)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(fname), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
in, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
|
||||
mode := f.Mode().Perm()
|
||||
if mode == 0 {
|
||||
mode = 0644
|
||||
}
|
||||
out, err := os.OpenFile(fname, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, copyErr := io.Copy(out, in)
|
||||
closeErr := out.Close()
|
||||
if copyErr != nil {
|
||||
return copyErr
|
||||
}
|
||||
if closeErr != nil {
|
||||
return closeErr
|
||||
}
|
||||
|
||||
selectGame(fname, f.Size())
|
||||
return nil
|
||||
})
|
||||
return path, size, err
|
||||
}
|
||||
|
||||
decomp, ok := format.(archives.Decompressor)
|
||||
if !ok {
|
||||
return path, size, fmt.Errorf("unsupported archive format: %T", format)
|
||||
}
|
||||
|
||||
rc, err := decomp.OpenReader(stream)
|
||||
if err != nil {
|
||||
return path, size, err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
fname := strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename))
|
||||
if fname == "" {
|
||||
return path, size, errors.New("could not determine extracted filename")
|
||||
}
|
||||
path = filepath.Join(dst, fname)
|
||||
out, err := os.Create(path)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
var mainPath string
|
||||
var mainSize int64
|
||||
for i, cf := range r.File {
|
||||
size := int64(cf.UncompressedSize)
|
||||
rc, err := cf.Open()
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
path := filepath.Join(os.TempDir(), cf.Name)
|
||||
|
||||
if cf.FileInfo().IsDir() {
|
||||
os.MkdirAll(path, os.ModePerm)
|
||||
continue
|
||||
}
|
||||
|
||||
if err = os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
outFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, cf.Mode())
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
if _, err = io.Copy(outFile, rc); err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
outFile.Close()
|
||||
rc.Close()
|
||||
|
||||
if i == 0 {
|
||||
mainPath = path
|
||||
mainSize = size
|
||||
}
|
||||
size, err = io.Copy(out, rc)
|
||||
closeErr := out.Close()
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
if closeErr != nil {
|
||||
return "", 0, closeErr
|
||||
}
|
||||
|
||||
return mainPath, mainSize, nil
|
||||
return path, size, nil
|
||||
}
|
||||
|
||||
func archiveOutputPath(dst, name string) (string, error) {
|
||||
clean := path.Clean(strings.TrimPrefix(name, "/"))
|
||||
if clean == "." || clean == "" {
|
||||
return "", errors.New("invalid archive path")
|
||||
}
|
||||
if clean == ".." || strings.HasPrefix(clean, "../") {
|
||||
return "", fmt.Errorf("unsafe archive path: %s", name)
|
||||
}
|
||||
fname := filepath.Join(dst, filepath.FromSlash(clean))
|
||||
rel, err := filepath.Rel(dst, fname)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
||||
return "", fmt.Errorf("unsafe archive path: %s", name)
|
||||
}
|
||||
return fname, nil
|
||||
}
|
||||
|
||||
// LoadGame loads a game. A core has to be loaded first.
|
||||
@@ -127,10 +220,15 @@ func LoadGame(gamePath string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// If we're loading a new game on the same core, save the RAM of the previous
|
||||
// game before closing it.
|
||||
if state.GamePath != gamePath {
|
||||
UnloadGame()
|
||||
if state.Core == nil {
|
||||
return errors.New("no core loaded")
|
||||
}
|
||||
|
||||
// Reload the core to fully tear down the previous game and its GL context.
|
||||
if state.CoreRunning {
|
||||
if err := Load(state.CorePath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
si := state.Core.GetSystemInfo()
|
||||
@@ -141,7 +239,7 @@ func LoadGame(gamePath string) error {
|
||||
}
|
||||
|
||||
if !si.NeedFullpath {
|
||||
bytes, err := ioutil.ReadFile(gi.Path)
|
||||
bytes, err := os.ReadFile(gi.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -156,7 +254,7 @@ func LoadGame(gamePath string) error {
|
||||
|
||||
ok := state.Core.LoadGame(*gi)
|
||||
if !ok {
|
||||
state.CoreRunning = false
|
||||
Unload()
|
||||
return errors.New("failed to load the game")
|
||||
}
|
||||
|
||||
@@ -188,30 +286,56 @@ func LoadGame(gamePath string) error {
|
||||
log.Println("[Core]: Game loaded: " + gamePath)
|
||||
savefiles.LoadSRAM()
|
||||
|
||||
if state.Core.HWRenderCallback != nil {
|
||||
if err := vid.ResetHWContext(); err != nil {
|
||||
Unload()
|
||||
return fmt.Errorf("create hardware context: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unload unloads a libretro core
|
||||
// Unload closes the game and core before destroying their GL context.
|
||||
func Unload() {
|
||||
if state.Core != nil {
|
||||
UnloadGame()
|
||||
savefiles.SaveSRAM()
|
||||
vid.RunContextDestroy()
|
||||
vid.BeginCore()
|
||||
if state.CoreRunning {
|
||||
state.Core.UnloadGame()
|
||||
}
|
||||
state.Core.Deinit()
|
||||
vid.EndCore()
|
||||
vid.ResetCoreState()
|
||||
state.GamePath = ""
|
||||
state.CoreRunning = false
|
||||
state.CorePath = ""
|
||||
state.Core = nil
|
||||
vid.SetHWRenderContext(nil)
|
||||
Options = nil
|
||||
}
|
||||
}
|
||||
|
||||
// UnloadGame unloads a game.
|
||||
func UnloadGame() {
|
||||
if state.CoreRunning {
|
||||
savefiles.SaveSRAM()
|
||||
state.Core.UnloadGame()
|
||||
state.GamePath = ""
|
||||
state.CoreRunning = false
|
||||
vid.ResetPitch()
|
||||
vid.ResetRot()
|
||||
}
|
||||
// Reset resets the game using its hardware context, if any.
|
||||
func Reset() {
|
||||
vid.BeginCore()
|
||||
defer vid.EndCore()
|
||||
state.Core.Reset()
|
||||
}
|
||||
|
||||
// Serialize captures core state with the core's GL context current.
|
||||
func Serialize() ([]byte, error) {
|
||||
vid.BeginCore()
|
||||
defer vid.EndCore()
|
||||
return state.Core.Serialize(state.Core.SerializeSize())
|
||||
}
|
||||
|
||||
// Unserialize restores core state with the core's GL context current.
|
||||
func Unserialize(data []byte) error {
|
||||
vid.BeginCore()
|
||||
defer vid.EndCore()
|
||||
return state.Core.Unserialize(data, uint(len(data)))
|
||||
}
|
||||
|
||||
// getGameInfo opens a rom and return the libretro.GameInfo needed to launch it
|
||||
@@ -226,12 +350,16 @@ func getGameInfo(filename string, blockExtract bool) (*libretro.GameInfo, error)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if filepath.Ext(filename) == ".zip" && !blockExtract {
|
||||
path, size, err := unzipGame(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if !blockExtract {
|
||||
switch filepath.Ext(filename) {
|
||||
case ".zip", ".zst", ".rar", ".tar":
|
||||
path, size, err := unarchiveGame(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &libretro.GameInfo{Path: path, Size: size}, nil
|
||||
}
|
||||
return &libretro.GameInfo{Path: path, Size: size}, nil
|
||||
}
|
||||
|
||||
return &libretro.GameInfo{Path: filename, Size: fi.Size()}, nil
|
||||
}
|
||||
+79
-24
@@ -1,20 +1,48 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-gl/glfw/v3.3/glfw"
|
||||
"github.com/go-gl/glfw/v3.4/glfw"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/utils"
|
||||
"github.com/libretro/ludo/video"
|
||||
)
|
||||
|
||||
var mainThread = make(chan func())
|
||||
|
||||
func init() {
|
||||
// GLFW initialization and input setup must run on the main OS thread.
|
||||
runtime.LockOSThread()
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
var code int
|
||||
go func() {
|
||||
code = m.Run()
|
||||
close(mainThread)
|
||||
}()
|
||||
for f := range mainThread {
|
||||
f()
|
||||
}
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func onMainThread(f func()) {
|
||||
done := make(chan struct{})
|
||||
mainThread <- func() {
|
||||
f()
|
||||
close(done)
|
||||
}
|
||||
<-done
|
||||
}
|
||||
|
||||
type WindowMock struct{}
|
||||
|
||||
func (m WindowMock) GetFramebufferSize() (width, height int) { return 320, 240 }
|
||||
@@ -35,7 +63,11 @@ func Test_coreLoad(t *testing.T) {
|
||||
|
||||
Init(&video.Video{})
|
||||
|
||||
out := utils.CaptureOutput(func() { Load("testdata/vecx_libretro" + ext) })
|
||||
var loadErr error
|
||||
out := utils.CaptureOutput(func() { loadErr = Load("testdata/vecx_libretro" + ext) })
|
||||
if loadErr != nil {
|
||||
t.Fatalf("Load() error = %v", loadErr)
|
||||
}
|
||||
|
||||
t.Run("The core is loaded", func(t *testing.T) {
|
||||
if state.Core == nil {
|
||||
@@ -45,19 +77,20 @@ func Test_coreLoad(t *testing.T) {
|
||||
|
||||
t.Run("Logs information about the loaded core", func(t *testing.T) {
|
||||
got := out
|
||||
want := `[Core]: Name: VecX
|
||||
[Core]: Version: 1.2 42366f8
|
||||
[Core]: Valid extensions: bin|vec
|
||||
[Core]: Need fullpath: false
|
||||
[Core]: Block extract: false
|
||||
`
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("got = %v, want %v", got, want)
|
||||
for _, want := range []string{
|
||||
`[Core]: Name: VecX`,
|
||||
`[Core]: Version: 1.2 `,
|
||||
`[Core]: Valid extensions: bin|vec`,
|
||||
`[Core]: Need fullpath: false`,
|
||||
`[Core]: Block extract: false`,
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("got = %v, want to contain %v", got, want)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
state.Core.UnloadGame()
|
||||
state.Core.Deinit()
|
||||
Unload()
|
||||
state.GamePath = ""
|
||||
state.Verbose = false
|
||||
}
|
||||
@@ -139,7 +172,7 @@ func Test_getGameInfo(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_unzipGame(t *testing.T) {
|
||||
func Test_unarchiveGame(t *testing.T) {
|
||||
type args struct {
|
||||
filename string
|
||||
}
|
||||
@@ -174,16 +207,16 @@ func Test_unzipGame(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, got1, err := unzipGame(tt.args.filename)
|
||||
got, got1, err := unarchiveGame(tt.args.filename)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("unzipGame() error = %v, wantErr %v", err, tt.wantErr)
|
||||
t.Errorf("unarchiveGame() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("unzipGame() got = %v, want %v", got, tt.want)
|
||||
t.Errorf("unarchiveGame() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
if got1 != tt.want1 {
|
||||
t.Errorf("unzipGame() got1 = %v, want %v", got1, tt.want1)
|
||||
t.Errorf("unarchiveGame() got1 = %v, want %v", got1, tt.want1)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -196,13 +229,24 @@ func Test_coreLoadGame(t *testing.T) {
|
||||
|
||||
Init(&video.Video{})
|
||||
|
||||
if err := glfw.Init(); err != nil {
|
||||
log.Fatalln("failed to initialize glfw")
|
||||
var initErr error
|
||||
onMainThread(func() { initErr = glfw.Init() })
|
||||
if initErr != nil {
|
||||
t.Fatalf("glfw.Init() error = %v", initErr)
|
||||
}
|
||||
t.Cleanup(func() { onMainThread(glfw.Terminate) })
|
||||
|
||||
if err := Load("testdata/vecx_libretro" + ext); err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
|
||||
Load("testdata/vecx_libretro" + ext)
|
||||
|
||||
got := utils.CaptureOutput(func() { LoadGame("testdata/Polar Rescue (USA).vec") })
|
||||
var loadGameErr error
|
||||
got := utils.CaptureOutput(func() {
|
||||
onMainThread(func() { loadGameErr = LoadGame("testdata/Polar Rescue (USA).vec") })
|
||||
})
|
||||
if loadGameErr != nil {
|
||||
t.Fatalf("LoadGame() error = %v", loadGameErr)
|
||||
}
|
||||
|
||||
t.Run("Logs information about the loaded game", func(t *testing.T) {
|
||||
want := `[Core]: Game loaded: testdata/Polar Rescue (USA).vec`
|
||||
@@ -223,7 +267,6 @@ func Test_coreLoadGame(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
UnloadGame()
|
||||
Unload()
|
||||
|
||||
t.Run("Global state should be cleared by Unload", func(t *testing.T) {
|
||||
@@ -238,3 +281,15 @@ func Test_coreLoadGame(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_coreLoadGameWithoutCore(t *testing.T) {
|
||||
Unload()
|
||||
|
||||
err := LoadGame("testdata/Polar Rescue (USA).vec")
|
||||
if err == nil {
|
||||
t.Fatal("LoadGame() error = nil, want not nil")
|
||||
}
|
||||
if err.Error() != "no core loaded" {
|
||||
t.Fatalf("LoadGame() error = %v, want %v", err, "no core loaded")
|
||||
}
|
||||
}
|
||||
@@ -143,6 +143,30 @@ func environment(cmd uint32, data unsafe.Pointer) bool {
|
||||
state.Core.SetFrameTimeCallback(data)
|
||||
case libretro.EnvironmentSetAudioCallback:
|
||||
state.Core.SetAudioCallback(data)
|
||||
case libretro.EnvironmentSetHWRender:
|
||||
hw := libretro.SetHWRenderCallback(
|
||||
data,
|
||||
vid.CurrentFramebuffer,
|
||||
vid.ProcAddress)
|
||||
if (hw.HWContextType != libretro.HWContextOpenGL && hw.HWContextType != libretro.HWContextOpenGLCore) || hw.ContextReset == nil {
|
||||
return false
|
||||
}
|
||||
vid.SetHWRenderContext(hw)
|
||||
if vid != nil && vid.Window != nil && !state.CoreRunning {
|
||||
if err := vid.ReconfigureHWContext(settings.Current.VideoFullscreen); err != nil {
|
||||
vid.SetHWRenderContext(state.Core.HWRenderCallback)
|
||||
log.Printf("[Env]: Could not create HW context %d (%d.%d): %v",
|
||||
hw.HWContextType, hw.VersionMajor, hw.VersionMinor, err)
|
||||
return false
|
||||
}
|
||||
state.MenuContextResetNeeded = true
|
||||
}
|
||||
state.Core.HWRenderCallback = hw
|
||||
log.Println("[Env]: HWContextType:", hw.HWContextType)
|
||||
return true
|
||||
case libretro.EnvironmentSetHWSharedContext:
|
||||
// All HW cores already get an isolated context shared with the frontend.
|
||||
return true
|
||||
case libretro.EnvironmentGetCanDupe:
|
||||
libretro.SetBool(data, true)
|
||||
case libretro.EnvironmentSetPixelFormat:
|
||||
@@ -151,6 +175,8 @@ func environment(cmd uint32, data unsafe.Pointer) bool {
|
||||
return environmentGetSystemDirectory(data)
|
||||
case libretro.EnvironmentGetSaveDirectory:
|
||||
return environmentGetSaveDirectory(data)
|
||||
case libretro.EnvironmentGetPrefferedHWRender:
|
||||
libretro.SetUint(data, uint(libretro.HWContextOpenGL))
|
||||
case libretro.EnvironmentShutdown:
|
||||
vid.SetShouldClose(true)
|
||||
case libretro.EnvironmentGetCoreOptionsVersion:
|
||||
@@ -166,6 +192,8 @@ func environment(cmd uint32, data unsafe.Pointer) bool {
|
||||
case libretro.EnvironmentGetVariableUpdate:
|
||||
libretro.SetBool(data, Options.Updated)
|
||||
Options.Updated = false
|
||||
case libretro.EnvironmentSetMemoryMaps:
|
||||
state.Core.MemoryMap = libretro.GetMemoryMap(data)
|
||||
case libretro.EnvironmentSetGeometry:
|
||||
vid.Geom = libretro.GetGeometry(data)
|
||||
case libretro.EnvironmentSetSystemAVInfo:
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+22
-10
@@ -36,6 +36,7 @@ type CRC uint32
|
||||
type ROM struct {
|
||||
XMLName xml.Name `xml:"rom"`
|
||||
Name string `xml:"name,attr"`
|
||||
Size int64 `xml:"size,attr"`
|
||||
CRC CRC `xml:"crc,attr"`
|
||||
}
|
||||
|
||||
@@ -63,7 +64,13 @@ func Parse(dat []byte) Dat {
|
||||
}
|
||||
|
||||
// FindByCRC loops over the Dats in the DB and concurrently matches CRC checksums.
|
||||
func (db *DB) FindByCRC(romPath string, romName string, crc uint32, games chan (Game)) {
|
||||
func (db *DB) FindByCRC(romPath string, romName string, crc uint32, size int64, games chan (Game)) {
|
||||
// skip empty file
|
||||
if size == 0 || crc == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
once := sync.Once{}
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(*db))
|
||||
// For every Dat in the DB
|
||||
@@ -74,11 +81,13 @@ func (db *DB) FindByCRC(romPath string, romName string, crc uint32, games chan (
|
||||
if len(game.ROMs) == 0 {
|
||||
continue
|
||||
}
|
||||
// If the checksums match
|
||||
if crc == uint32(game.ROMs[0].CRC) {
|
||||
game.Path = romPath
|
||||
game.System = system
|
||||
games <- game
|
||||
// If the checksums and sizes match
|
||||
if crc == uint32(game.ROMs[0].CRC) && size == game.ROMs[0].Size {
|
||||
once.Do(func() {
|
||||
game.Path = romPath
|
||||
game.System = system
|
||||
games <- game
|
||||
})
|
||||
}
|
||||
}
|
||||
wg.Done()
|
||||
@@ -101,10 +110,13 @@ func (db *DB) FindByROMName(romPath string, romName string, crc uint32, games ch
|
||||
continue
|
||||
}
|
||||
// If the checksums match
|
||||
if romName == game.ROMs[0].Name {
|
||||
game.Path = romPath
|
||||
game.System = system
|
||||
games <- game
|
||||
for _, ROM := range game.ROMs {
|
||||
if romName == ROM.Name {
|
||||
game.Path = romPath
|
||||
game.System = system
|
||||
games <- game
|
||||
}
|
||||
// element is the element from someSlice for where we are
|
||||
}
|
||||
}
|
||||
wg.Done()
|
||||
|
||||
+1
-1
Submodule database updated: aad97888ed...c31e8f126d.
@@ -1,24 +1,47 @@
|
||||
module github.com/libretro/ludo
|
||||
|
||||
require (
|
||||
github.com/adrg/xdg v0.3.3 // indirect
|
||||
github.com/adrg/xdg v0.5.3
|
||||
github.com/cavaliercoder/grab v2.0.0+incompatible
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/fatih/structs v1.1.0
|
||||
github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be
|
||||
github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276
|
||||
github.com/go-gl/glfw/v3.4/glfw v0.1.0-pre.1
|
||||
github.com/go-gl/mathgl v1.0.0
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0
|
||||
github.com/pelletier/go-toml v1.9.1
|
||||
github.com/stretchr/testify v1.7.0 // indirect
|
||||
github.com/tanema/gween v0.0.0-20200427131925-c89ae23cc63c
|
||||
github.com/youpy/go-wav v0.1.0
|
||||
golang.org/x/image v0.0.0-20210504121937-7319ad40d33e
|
||||
golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0
|
||||
github.com/mholt/archives v0.1.5
|
||||
github.com/pelletier/go-toml v1.9.5
|
||||
github.com/tanema/gween v0.0.0-20250522035225-e874ee3ae01a
|
||||
github.com/youpy/go-wav v0.3.2
|
||||
golang.org/x/image v0.41.0
|
||||
golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b
|
||||
)
|
||||
|
||||
go 1.13
|
||||
require (
|
||||
github.com/STARRY-S/zip v0.2.3 // indirect
|
||||
github.com/andybalholm/brotli v1.2.1 // indirect
|
||||
github.com/bodgit/plumbing v1.3.0 // indirect
|
||||
github.com/bodgit/sevenzip v1.6.1 // indirect
|
||||
github.com/bodgit/windows v1.0.1 // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/klauspost/compress v1.18.7 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/mikelolasagasti/xz v1.0.1 // indirect
|
||||
github.com/minio/minlz v1.0.1 // indirect
|
||||
github.com/nwaples/rardecode/v2 v2.2.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.26 // indirect
|
||||
github.com/sorairolake/lzip-go v0.3.8 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||
github.com/youpy/go-riff v0.1.0 // indirect
|
||||
github.com/zaf/g711 v1.4.0 // indirect
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
)
|
||||
|
||||
go 1.25.0
|
||||
|
||||
replace github.com/go-gl/glfw/v3.4/glfw => github.com/kivutar/glfw/v3.4/glfw v0.0.0-20260410202638-4349e4d5a0ee
|
||||
@@ -1,80 +1,343 @@
|
||||
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.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
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/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
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/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
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/adrg/xdg v0.3.3 h1:s/tV7MdqQnzB1nKY8aqHvAMD+uCiuEDzVB5HLRY849U=
|
||||
github.com/adrg/xdg v0.3.3/go.mod h1:61xAR2VZcggl2St4O9ohF5qCKe08+JDmE4VNzPFQvOQ=
|
||||
github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
|
||||
github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
|
||||
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
|
||||
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
|
||||
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
|
||||
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
|
||||
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
|
||||
github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4=
|
||||
github.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8=
|
||||
github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
|
||||
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
|
||||
github.com/cavaliercoder/grab v2.0.0+incompatible h1:wZHbBQx56+Yxjx2TCGDcenhh3cJn7cCLMfkEPmySTSE=
|
||||
github.com/cavaliercoder/grab v2.0.0+incompatible/go.mod h1:tTBkfNqSBfuMmMBFaO2phgyhdYhiZQ/+iXCZDzcDsMI=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
|
||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/go-gl/gl v0.0.0-20210501111010-69f74958bac0 h1:7xNa69TzlTrKtlBtE4yyNRFP9oqAneXAs9oKJkVOECs=
|
||||
github.com/go-gl/gl v0.0.0-20210501111010-69f74958bac0/go.mod h1:wjpnOv6ONl2SuJSxqCPVaPZibGFdSci9HFocT9qtVYM=
|
||||
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/v3.3/glfw v0.0.0-20210715014612-ab6297867137 h1:nkvwT/9HzcHRekrCaLk/a4At+RNwr6lu6rl28kBDQ+Q=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210715014612-ab6297867137/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be h1:vEIVIuBApEBQTEJt19GfhoU+zFSV+sNTa9E9FdnRYfk=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276 h1:IO5P06Pcj9K04d+l4nrf3c2U56+dAotIFG6u4P1wAHI=
|
||||
github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
|
||||
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/mathgl v1.0.0 h1:t9DznWJlXxxjeeKLIdovCOVJQk/GzDEL7h/h+Ro2B68=
|
||||
github.com/go-gl/mathgl v1.0.0/go.mod h1:yhpkQzEiH9yPyxDUGzkmgScbaBVlhC06qodikEM0ZwQ=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/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/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/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/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.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
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-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
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/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
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/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kivutar/glfw/v3.4/glfw v0.0.0-20260410202638-4349e4d5a0ee h1:PuDX5smvEEVGSd2s66iGqGtmrSldeBusjnLgIi92Sl4=
|
||||
github.com/kivutar/glfw/v3.4/glfw v0.0.0-20260410202638-4349e4d5a0ee/go.mod h1:T5Dn0JwIJOX1euPZ/iT4tq6nFYtmukjcYa7937HuYK8=
|
||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw=
|
||||
github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
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/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/pelletier/go-toml v1.9.1 h1:a6qW1EVNZWH9WGI6CsYdD8WAylkoXBS5yv0XHlh17Tc=
|
||||
github.com/pelletier/go-toml v1.9.1/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ=
|
||||
github.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4=
|
||||
github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0=
|
||||
github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc=
|
||||
github.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A=
|
||||
github.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
|
||||
github.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKiOqu0A=
|
||||
github.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
|
||||
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
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_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
|
||||
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
|
||||
github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
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/tanema/gween v0.0.0-20200427131925-c89ae23cc63c h1:rZz/p7IbahA51/dieoBBfzTgkTX9C8QFrTgkMh64Khg=
|
||||
github.com/tanema/gween v0.0.0-20200427131925-c89ae23cc63c/go.mod h1:XXpz+9IVhUY5vTC5gXRNSjLDVwQWa5KM43NrH1GJa4M=
|
||||
github.com/youpy/go-riff v0.0.0-20131220112943-557d78c11efb h1:RDh7U5Di6o7fblIBe7rVi9KnrcOXUbLwvvLLdP2InSI=
|
||||
github.com/youpy/go-riff v0.0.0-20131220112943-557d78c11efb/go.mod h1:83nxdDV4Z9RzrTut9losK7ve4hUnxUR8ASSz4BsKXwQ=
|
||||
github.com/youpy/go-wav v0.1.0 h1:MehxSoflnZoDmiILNQ6WMQpTuRuFpRnq213YqQ7WPRk=
|
||||
github.com/youpy/go-wav v0.1.0/go.mod h1:ZyTUfNrGKaH/wPNGf2W9Se6sNtZRlY+b98kckKmYLS8=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tanema/gween v0.0.0-20250522035225-e874ee3ae01a h1:xINbk646UroMWqLNbMrce7FisMOWQXRG7a+JLQ5xz9o=
|
||||
github.com/tanema/gween v0.0.0-20250522035225-e874ee3ae01a/go.mod h1:XXpz+9IVhUY5vTC5gXRNSjLDVwQWa5KM43NrH1GJa4M=
|
||||
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/youpy/go-riff v0.1.0 h1:vZO/37nI4tIET8tQI0Qn0Y79qQh99aEpponTPiPut7k=
|
||||
github.com/youpy/go-riff v0.1.0/go.mod h1:83nxdDV4Z9RzrTut9losK7ve4hUnxUR8ASSz4BsKXwQ=
|
||||
github.com/youpy/go-wav v0.3.2 h1:NLM8L/7yZ0Bntadw/0h95OyUsen+DQIVf9gay+SUsMU=
|
||||
github.com/youpy/go-wav v0.3.2/go.mod h1:0FCieAXAeSdcxFfwLpRuEo0PFmAoc+8NU34h7TUvk50=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zaf/g711 v0.0.0-20190814101024-76a4a538f52b/go.mod h1:T2h1zV50R/q0CVYnsQOQ6L7P4a2ZxH47ixWcMXFGyx8=
|
||||
github.com/zaf/g711 v1.4.0 h1:XZYkjjiAg9QTBnHqEg37m2I9q3IIDv5JRYXs2N8ma7c=
|
||||
github.com/zaf/g711 v1.4.0/go.mod h1:eCDXt3dSp/kYYAoooba7ukD/Q75jvAaS4WOMr0l1Roo=
|
||||
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=
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
|
||||
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
|
||||
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/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/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-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20210504121937-7319ad40d33e h1:PzJMNfFQx+QO9hrC1GwZ4BoPGeNGhfeQEgcQFArEjPk=
|
||||
golang.org/x/image v0.0.0-20210504121937-7319ad40d33e/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
|
||||
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
|
||||
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/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08 h1:h+GZ3ubjuWaQjGe8owMGcmMVCqs0xYJtRG5y2bpHaqU=
|
||||
golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b h1:Qt2eaXcZ8x20iAcoZ6AceeMMtnjuPHvC51KRCH1DKSQ=
|
||||
golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b/go.mod h1:5Fu78lew5ucMXt8w2KYcwvxu2rkC/liHzUvaoiI+H/M=
|
||||
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.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
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-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-20190620200207-3b0461eec859/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-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
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/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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-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-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-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
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/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-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/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-20200130002326-2f3ba24bd6e7/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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
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=
|
||||
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/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/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-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
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.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=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
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/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
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=
|
||||
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=
|
||||
@@ -1,7 +1,7 @@
|
||||
package input
|
||||
|
||||
import (
|
||||
"github.com/go-gl/glfw/v3.3/glfw"
|
||||
"github.com/go-gl/glfw/v3.4/glfw"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package input
|
||||
|
||||
import (
|
||||
"github.com/go-gl/glfw/v3.3/glfw"
|
||||
"github.com/go-gl/glfw/v3.4/glfw"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
)
|
||||
|
||||
|
||||
+38
-5
@@ -6,7 +6,7 @@ package input
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/go-gl/glfw/v3.3/glfw"
|
||||
"github.com/go-gl/glfw/v3.4/glfw"
|
||||
lr "github.com/libretro/ludo/libretro"
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/settings"
|
||||
@@ -32,6 +32,9 @@ var (
|
||||
NewAnalogState AnalogStates // analog input state for the current frame
|
||||
)
|
||||
|
||||
var oldMouseX float64
|
||||
var oldMouseY float64
|
||||
|
||||
// Hot keys
|
||||
const (
|
||||
// ActionMenuToggle toggles the menu UI
|
||||
@@ -51,14 +54,14 @@ func joystickCallback(joy glfw.Joystick, event glfw.PeripheralEvent) {
|
||||
switch event {
|
||||
case glfw.Connected:
|
||||
if joy.IsGamepad() {
|
||||
ntf.DisplayAndLog(ntf.Info, "Input", "Joystick #%d plugged: %s.", joy, glfw.Joystick.GetName(joy))
|
||||
ntf.DisplayAndLogf(ntf.Info, "Input", "Joystick #%d plugged: %s.", joy, glfw.Joystick.GetName(joy))
|
||||
} else {
|
||||
ntf.DisplayAndLog(ntf.Warning, "Input", "Joystick #%d plugged: %s but not configured.", joy, glfw.Joystick.GetName(joy))
|
||||
ntf.DisplayAndLogf(ntf.Warning, "Input", "Joystick #%d plugged: %s but not configured.", joy, glfw.Joystick.GetName(joy))
|
||||
}
|
||||
case glfw.Disconnected:
|
||||
ntf.DisplayAndLog(ntf.Info, "Input", "Joystick #%d unplugged.", joy)
|
||||
ntf.DisplayAndLogf(ntf.Info, "Input", "Joystick #%d unplugged.", joy)
|
||||
default:
|
||||
ntf.DisplayAndLog(ntf.Warning, "Input", "Joystick #%d unhandled event: %d.", joy, event)
|
||||
ntf.DisplayAndLogf(ntf.Warning, "Input", "Joystick #%d unhandled event: %d.", joy, event)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +80,16 @@ func floatToAnalog(v float32) int16 {
|
||||
return int16(v * 32767.0)
|
||||
}
|
||||
|
||||
// JoypadPlugged returns true if at least one joypad is plugged
|
||||
func JoypadPlugged() bool {
|
||||
for joy := glfw.Joystick(0); joy < glfw.JoystickLast; joy++ {
|
||||
if joy.IsGamepad() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// pollJoypads process joypads of all players
|
||||
func pollJoypads(state States, analogState AnalogStates) (States, AnalogStates) {
|
||||
p := 0
|
||||
@@ -190,5 +203,25 @@ func State(port uint, device uint32, index uint, id uint) int16 {
|
||||
return NewAnalogState[port][index][id]
|
||||
}
|
||||
|
||||
if device == lr.DeviceMouse {
|
||||
x, y := vid.Window.GetCursorPos()
|
||||
if id == uint(lr.DeviceIDMouseX) {
|
||||
d := x - oldMouseX
|
||||
oldMouseX = x
|
||||
return int16(d)
|
||||
}
|
||||
if id == uint(lr.DeviceIDMouseY) {
|
||||
d := y - oldMouseY
|
||||
oldMouseY = y
|
||||
return int16(d)
|
||||
}
|
||||
if id == uint(lr.DeviceIDMouseLeft) && vid.Window.GetMouseButton(glfw.MouseButton1) == glfw.Press {
|
||||
return 1
|
||||
}
|
||||
if id == uint(lr.DeviceIDMouseRight) && vid.Window.GetMouseButton(glfw.MouseButton2) == glfw.Press {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
+1723
-742
File diff suppressed because it is too large.
Load diff
+60
-15
@@ -5,13 +5,37 @@ package libretro
|
||||
#include <stdbool.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <pthread.h>
|
||||
#include <stdint.h>
|
||||
|
||||
static pthread_t s_thread;
|
||||
static uintptr_t s_current_framebuffer = 0;
|
||||
|
||||
// Go can track libco's alternate callback stacks only on a C-created thread.
|
||||
static void *emu_thread_loop(void *unused) {
|
||||
void coreThreadLoop();
|
||||
coreThreadLoop();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int cothread_init() {
|
||||
return pthread_create(&s_thread, NULL, emu_thread_loop, NULL);
|
||||
}
|
||||
|
||||
void cothread_deinit() {
|
||||
pthread_join(s_thread, NULL);
|
||||
}
|
||||
|
||||
bool cothread_is_current() {
|
||||
return pthread_equal(pthread_self(), s_thread);
|
||||
}
|
||||
|
||||
void bridge_retro_init(void *f) {
|
||||
return ((void (*)(void))f)();
|
||||
((void (*)(void))f)();
|
||||
}
|
||||
|
||||
void bridge_retro_deinit(void *f) {
|
||||
return ((void (*)(void))f)();
|
||||
((void (*)(void))f)();
|
||||
}
|
||||
|
||||
unsigned bridge_retro_api_version(void *f) {
|
||||
@@ -22,6 +46,14 @@ void bridge_retro_frame_time_callback(retro_frame_time_callback_t f, retro_usec_
|
||||
f(usec);
|
||||
}
|
||||
|
||||
void bridge_retro_hw_context_reset(retro_hw_context_reset_t f) {
|
||||
f();
|
||||
}
|
||||
|
||||
void bridge_retro_hw_context_destroy(retro_hw_context_reset_t f) {
|
||||
f();
|
||||
}
|
||||
|
||||
void bridge_retro_audio_callback(retro_audio_callback_t f) {
|
||||
f();
|
||||
}
|
||||
@@ -38,12 +70,12 @@ 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_environment(void *f, void *callback) {
|
||||
return ((void (*)(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 (*)(retro_video_refresh_t))f)((retro_video_refresh_t)callback);
|
||||
}
|
||||
|
||||
void bridge_retro_set_controller_port_device(void *f, unsigned port, unsigned device) {
|
||||
@@ -51,23 +83,23 @@ void bridge_retro_set_controller_port_device(void *f, unsigned port, unsigned de
|
||||
}
|
||||
|
||||
void bridge_retro_set_input_poll(void *f, void *callback) {
|
||||
((bool (*)(retro_input_poll_t))f)((retro_input_poll_t)callback);
|
||||
((void (*)(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 (*)(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 (*)(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);
|
||||
((void (*)(retro_audio_sample_batch_t))f)((retro_audio_sample_batch_t)callback);
|
||||
}
|
||||
|
||||
bool bridge_retro_load_game(void *f, struct retro_game_info *gi) {
|
||||
return ((bool (*)(struct retro_game_info *))f)(gi);
|
||||
return ((bool (*)(const struct retro_game_info *))f)(gi);
|
||||
}
|
||||
|
||||
size_t bridge_retro_serialize_size(void *f) {
|
||||
@@ -75,23 +107,23 @@ size_t bridge_retro_serialize_size(void *f) {
|
||||
}
|
||||
|
||||
bool bridge_retro_serialize(void *f, void *data, size_t size) {
|
||||
return ((bool (*)(void*, size_t))f)(data, 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);
|
||||
return ((bool (*)(const void*, size_t))f)(data, size);
|
||||
}
|
||||
|
||||
void bridge_retro_unload_game(void *f) {
|
||||
return ((void (*)(void))f)();
|
||||
((void (*)(void))f)();
|
||||
}
|
||||
|
||||
void bridge_retro_run(void *f) {
|
||||
return ((void (*)(void))f)();
|
||||
((void (*)(void))f)();
|
||||
}
|
||||
|
||||
void bridge_retro_reset(void *f) {
|
||||
return ((void (*)(void))f)();
|
||||
((void (*)(void))f)();
|
||||
}
|
||||
|
||||
size_t bridge_retro_get_memory_size(void *f, unsigned id) {
|
||||
@@ -168,5 +200,18 @@ int64_t coreGetTimeUsec_cgo() {
|
||||
return coreGetTimeUsec();
|
||||
}
|
||||
|
||||
uintptr_t coreGetCurrentFramebuffer_cgo() {
|
||||
return s_current_framebuffer;
|
||||
}
|
||||
|
||||
void coreSetCurrentFramebuffer_cgo(uintptr_t fb) {
|
||||
s_current_framebuffer = fb;
|
||||
}
|
||||
|
||||
uintptr_t coreGetProcAddress_cgo(const char *sym) {
|
||||
uintptr_t coreGetProcAddress(const char *sym);
|
||||
return coreGetProcAddress(sym);
|
||||
}
|
||||
|
||||
*/
|
||||
import "C"
|
||||
+320
-61
@@ -4,7 +4,8 @@ Package libretro is a cgo binding for the libretro API.
|
||||
Libretro is a simple but powerful development interface that allows for the easy creation of
|
||||
emulators, games and multimedia applications that can plug straight into any libretro-compatible
|
||||
frontend. This development interface is open to others so that they can run these pluggable emulator
|
||||
and game cores also in their own programs or devices. */
|
||||
and game cores also in their own programs or devices.
|
||||
*/
|
||||
package libretro
|
||||
|
||||
/*
|
||||
@@ -13,12 +14,16 @@ package libretro
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
int cothread_init();
|
||||
void cothread_deinit();
|
||||
bool cothread_is_current();
|
||||
|
||||
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_environment(void *f, void *callback);
|
||||
void bridge_retro_set_video_refresh(void *f, void *callback);
|
||||
void bridge_retro_set_controller_port_device(void *f, unsigned port, unsigned device);
|
||||
void bridge_retro_set_input_poll(void *f, void *callback);
|
||||
@@ -33,6 +38,8 @@ void bridge_retro_unload_game(void *f);
|
||||
void bridge_retro_run(void *f);
|
||||
void bridge_retro_reset(void *f);
|
||||
void bridge_retro_frame_time_callback(retro_frame_time_callback_t f, retro_usec_t usec);
|
||||
void bridge_retro_hw_context_reset(retro_hw_context_reset_t f);
|
||||
void bridge_retro_hw_context_destroy(retro_hw_context_reset_t f);
|
||||
void bridge_retro_audio_callback(retro_audio_callback_t f);
|
||||
void bridge_retro_audio_set_state(retro_audio_set_state_callback_t f, bool state);
|
||||
size_t bridge_retro_get_memory_size(void *f, unsigned id);
|
||||
@@ -51,10 +58,15 @@ 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);
|
||||
int64_t coreGetTimeUsec_cgo();
|
||||
uintptr_t coreGetCurrentFramebuffer_cgo();
|
||||
void coreSetCurrentFramebuffer_cgo(uintptr_t fb);
|
||||
uintptr_t coreGetProcAddress_cgo(const char *sym);
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
"unsafe"
|
||||
)
|
||||
@@ -96,6 +108,18 @@ type SystemAVInfo struct {
|
||||
Timing SystemTiming
|
||||
}
|
||||
|
||||
// MemoryDescriptor stores information about the emulated memory regions
|
||||
type MemoryDescriptor struct {
|
||||
Flags uint64
|
||||
Ptr unsafe.Pointer
|
||||
Offset uintptr
|
||||
Start uintptr
|
||||
Select uintptr
|
||||
Disconnect uintptr
|
||||
Len uintptr
|
||||
Addrspace string
|
||||
}
|
||||
|
||||
// Variable is a key value pair that represents a core option
|
||||
type Variable C.struct_retro_variable
|
||||
|
||||
@@ -204,6 +228,20 @@ type FrameTimeCallback struct {
|
||||
Reference int64
|
||||
}
|
||||
|
||||
// HWRenderCallback sets an interface to let a libretro core render with
|
||||
// hardware acceleration.
|
||||
type HWRenderCallback struct {
|
||||
HWContextType uint32
|
||||
ContextReset func()
|
||||
Depth bool
|
||||
Stencil bool
|
||||
BottomLeftOrigin bool
|
||||
VersionMajor, VersionMinor uint
|
||||
CacheContext bool
|
||||
ContextDestroy func()
|
||||
DebugContext bool
|
||||
}
|
||||
|
||||
// AudioCallback stores the audio callback itself and the SetState callback
|
||||
type AudioCallback struct {
|
||||
Callback func()
|
||||
@@ -295,7 +333,7 @@ const (
|
||||
DeviceIDJoypadMask = uint32(C.RETRO_DEVICE_ID_JOYPAD_MASK)
|
||||
)
|
||||
|
||||
// Index / Id values for ANALOG device.
|
||||
// Index / Id values for analog device
|
||||
const (
|
||||
DeviceIndexAnalogLeft = uint32(C.RETRO_DEVICE_INDEX_ANALOG_LEFT)
|
||||
DeviceIndexAnalogRight = uint32(C.RETRO_DEVICE_INDEX_ANALOG_RIGHT)
|
||||
@@ -304,6 +342,21 @@ const (
|
||||
DeviceIDAnalogY = uint32(C.RETRO_DEVICE_ID_ANALOG_Y)
|
||||
)
|
||||
|
||||
// ID values for the mouse device
|
||||
const (
|
||||
DeviceIDMouseX = uint32(C.RETRO_DEVICE_ID_MOUSE_X)
|
||||
DeviceIDMouseY = uint32(C.RETRO_DEVICE_ID_MOUSE_Y)
|
||||
DeviceIDMouseLeft = uint32(C.RETRO_DEVICE_ID_MOUSE_LEFT)
|
||||
DeviceIDMouseRight = uint32(C.RETRO_DEVICE_ID_MOUSE_RIGHT)
|
||||
DeviceIDMouseWheelUp = uint32(C.RETRO_DEVICE_ID_MOUSE_WHEELUP)
|
||||
DeviceIDMouseWheelDown = uint32(C.RETRO_DEVICE_ID_MOUSE_WHEELDOWN)
|
||||
DeviceIDMouseMiddle = uint32(C.RETRO_DEVICE_ID_MOUSE_MIDDLE)
|
||||
DeviceIDMouseHorizWheelUp = uint32(C.RETRO_DEVICE_ID_MOUSE_HORIZ_WHEELUP)
|
||||
DeviceIDMouseHorizWheelDown = uint32(C.RETRO_DEVICE_ID_MOUSE_HORIZ_WHEELDOWN)
|
||||
DeviceIDMouseButton4 = uint32(C.RETRO_DEVICE_ID_MOUSE_BUTTON_4)
|
||||
DeviceIDMouseButton5 = uint32(C.RETRO_DEVICE_ID_MOUSE_BUTTON_5)
|
||||
)
|
||||
|
||||
// Environment callback API. See libretro.h for details
|
||||
const (
|
||||
EnvironmentSetRotation = uint32(C.RETRO_ENVIRONMENT_SET_ROTATION)
|
||||
@@ -382,15 +435,35 @@ const (
|
||||
MemoryVideoRAM = uint32(C.RETRO_MEMORY_VIDEO_RAM)
|
||||
)
|
||||
|
||||
// Hardware contexts
|
||||
const (
|
||||
HWContextNone = uint32(C.RETRO_HW_CONTEXT_NONE)
|
||||
HWContextOpenGL = uint32(C.RETRO_HW_CONTEXT_OPENGL)
|
||||
HWContextOpenGLES2 = uint32(C.RETRO_HW_CONTEXT_OPENGLES2)
|
||||
HWContextOpenGLCore = uint32(C.RETRO_HW_CONTEXT_OPENGL_CORE)
|
||||
HWContextOpenGLES3 = uint32(C.RETRO_HW_CONTEXT_OPENGLES3)
|
||||
HWContextOpenGLESVersion = uint32(C.RETRO_HW_CONTEXT_OPENGLES_VERSION)
|
||||
HWContextVulkan = uint32(C.RETRO_HW_CONTEXT_VULKAN)
|
||||
HWContextDummy = uint32(C.RETRO_HW_CONTEXT_DUMMY)
|
||||
)
|
||||
|
||||
// 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.
|
||||
var (
|
||||
HWFrameBufferValid = unsafe.Pointer(C.RETRO_HW_FRAME_BUFFER_VALID)
|
||||
)
|
||||
|
||||
type (
|
||||
environmentFunc func(uint32, unsafe.Pointer) bool
|
||||
videoRefreshFunc func(unsafe.Pointer, int32, int32, int32)
|
||||
audioSampleFunc func(int16, int16)
|
||||
audioSampleBatchFunc func([]byte, int32) int32
|
||||
inputPollFunc func()
|
||||
inputStateFunc func(uint, uint32, uint, uint) int16
|
||||
logFunc func(uint32, string)
|
||||
getTimeUsecFunc func() int64
|
||||
environmentFunc func(uint32, unsafe.Pointer) bool
|
||||
videoRefreshFunc func(unsafe.Pointer, int32, int32, int32)
|
||||
audioSampleFunc func(int16, int16)
|
||||
audioSampleBatchFunc func([]byte, int32) int32
|
||||
inputPollFunc func()
|
||||
inputStateFunc func(uint, uint32, uint, uint) int16
|
||||
logFunc func(uint32, string)
|
||||
getTimeUsecFunc func() int64
|
||||
getCurrentFramebufferFunc func() uintptr
|
||||
getProcAddressFunc func(string) uintptr
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -402,8 +475,84 @@ var (
|
||||
inputState inputStateFunc
|
||||
log logFunc
|
||||
getTimeUsec getTimeUsecFunc
|
||||
getProcAddress getProcAddressFunc
|
||||
releaseContext func()
|
||||
acquireContext func()
|
||||
coreCalls chan func()
|
||||
frontendCalls chan func()
|
||||
)
|
||||
|
||||
// SetContextCallbacks hands the current graphics context between the calling
|
||||
// thread and the native core thread. Calls remain synchronous.
|
||||
func SetContextCallbacks(release, acquire func()) {
|
||||
releaseContext, acquireContext = release, acquire
|
||||
}
|
||||
|
||||
func coreReleaseContext() {
|
||||
if releaseContext != nil {
|
||||
releaseContext()
|
||||
}
|
||||
}
|
||||
|
||||
func coreAcquireContext() {
|
||||
if acquireContext != nil {
|
||||
acquireContext()
|
||||
}
|
||||
}
|
||||
|
||||
//export coreThreadLoop
|
||||
func coreThreadLoop() {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
for call := range coreCalls {
|
||||
call()
|
||||
}
|
||||
}
|
||||
|
||||
// Calls are synchronous, but must service callbacks on the waiting thread.
|
||||
// Pumping both directions also permits a callback to make a nested core call.
|
||||
func threadCall(calls, callbacks chan func(), f func()) {
|
||||
coreReleaseContext()
|
||||
done := make(chan struct{})
|
||||
calls <- func() {
|
||||
coreAcquireContext()
|
||||
f()
|
||||
coreReleaseContext()
|
||||
close(done)
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case callback := <-callbacks:
|
||||
callback()
|
||||
case <-done:
|
||||
coreAcquireContext()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The frontend calls from its locked OS thread; core callbacks can reenter here.
|
||||
func coreCall(f func()) {
|
||||
if coreCalls == nil || bool(C.cothread_is_current()) {
|
||||
f()
|
||||
return
|
||||
}
|
||||
threadCall(coreCalls, frontendCalls, f)
|
||||
}
|
||||
|
||||
func coreResult[T any](f func() T) (result T) {
|
||||
coreCall(func() { result = f() })
|
||||
return
|
||||
}
|
||||
|
||||
func frontendCall(f func()) {
|
||||
if coreCalls == nil || !bool(C.cothread_is_current()) {
|
||||
f()
|
||||
return
|
||||
}
|
||||
threadCall(frontendCalls, coreCalls, f)
|
||||
}
|
||||
|
||||
// Load dynamically loads a libretro core at the given path and returns a Core instance
|
||||
func Load(sofile string) (*Core, error) {
|
||||
core := Core{}
|
||||
@@ -414,45 +563,57 @@ func Load(sofile string) (*Core, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
core.symRetroInit = core.DlSym("retro_init")
|
||||
core.symRetroDeinit = core.DlSym("retro_deinit")
|
||||
core.symRetroAPIVersion = core.DlSym("retro_api_version")
|
||||
core.symRetroGetSystemInfo = core.DlSym("retro_get_system_info")
|
||||
core.symRetroGetSystemAVInfo = core.DlSym("retro_get_system_av_info")
|
||||
core.symRetroSetEnvironment = core.DlSym("retro_set_environment")
|
||||
core.symRetroSetVideoRefresh = core.DlSym("retro_set_video_refresh")
|
||||
core.symRetroSetControllerPortDevice = core.DlSym("retro_set_controller_port_device")
|
||||
core.symRetroSetInputPoll = core.DlSym("retro_set_input_poll")
|
||||
core.symRetroSetInputState = core.DlSym("retro_set_input_state")
|
||||
core.symRetroSetAudioSample = core.DlSym("retro_set_audio_sample")
|
||||
core.symRetroSetAudioSampleBatch = core.DlSym("retro_set_audio_sample_batch")
|
||||
core.symRetroRun = core.DlSym("retro_run")
|
||||
core.symRetroReset = core.DlSym("retro_reset")
|
||||
core.symRetroLoadGame = core.DlSym("retro_load_game")
|
||||
core.symRetroUnloadGame = core.DlSym("retro_unload_game")
|
||||
core.symRetroSerializeSize = core.DlSym("retro_serialize_size")
|
||||
core.symRetroSerialize = core.DlSym("retro_serialize")
|
||||
core.symRetroUnserialize = core.DlSym("retro_unserialize")
|
||||
core.symRetroGetMemorySize = core.DlSym("retro_get_memory_size")
|
||||
core.symRetroGetMemoryData = core.DlSym("retro_get_memory_data")
|
||||
coreCalls, frontendCalls = make(chan func()), make(chan func())
|
||||
if err := C.cothread_init(); err != 0 {
|
||||
coreCalls, frontendCalls = nil, nil
|
||||
DlClose(core.handle)
|
||||
return nil, fmt.Errorf("start core thread: %s", C.GoString(C.strerror(err)))
|
||||
}
|
||||
|
||||
core.symRetroInit = DlSym(core.handle, "retro_init")
|
||||
core.symRetroDeinit = DlSym(core.handle, "retro_deinit")
|
||||
core.symRetroAPIVersion = DlSym(core.handle, "retro_api_version")
|
||||
core.symRetroGetSystemInfo = DlSym(core.handle, "retro_get_system_info")
|
||||
core.symRetroGetSystemAVInfo = DlSym(core.handle, "retro_get_system_av_info")
|
||||
core.symRetroSetEnvironment = DlSym(core.handle, "retro_set_environment")
|
||||
core.symRetroSetVideoRefresh = DlSym(core.handle, "retro_set_video_refresh")
|
||||
core.symRetroSetControllerPortDevice = DlSym(core.handle, "retro_set_controller_port_device")
|
||||
core.symRetroSetInputPoll = DlSym(core.handle, "retro_set_input_poll")
|
||||
core.symRetroSetInputState = DlSym(core.handle, "retro_set_input_state")
|
||||
core.symRetroSetAudioSample = DlSym(core.handle, "retro_set_audio_sample")
|
||||
core.symRetroSetAudioSampleBatch = DlSym(core.handle, "retro_set_audio_sample_batch")
|
||||
core.symRetroRun = DlSym(core.handle, "retro_run")
|
||||
core.symRetroReset = DlSym(core.handle, "retro_reset")
|
||||
core.symRetroLoadGame = DlSym(core.handle, "retro_load_game")
|
||||
core.symRetroUnloadGame = DlSym(core.handle, "retro_unload_game")
|
||||
core.symRetroSerializeSize = DlSym(core.handle, "retro_serialize_size")
|
||||
core.symRetroSerialize = DlSym(core.handle, "retro_serialize")
|
||||
core.symRetroUnserialize = DlSym(core.handle, "retro_unserialize")
|
||||
core.symRetroGetMemorySize = DlSym(core.handle, "retro_get_memory_size")
|
||||
core.symRetroGetMemoryData = DlSym(core.handle, "retro_get_memory_data")
|
||||
|
||||
return &core, nil
|
||||
}
|
||||
|
||||
// Init takes care of the library global initialization
|
||||
func (core *Core) Init() {
|
||||
C.bridge_retro_init(core.symRetroInit)
|
||||
coreCall(func() { C.bridge_retro_init(core.symRetroInit) })
|
||||
}
|
||||
|
||||
// APIVersion returns the RETRO_API_VERSION.
|
||||
// Used to validate ABI compatibility when the API is revised.
|
||||
func (core *Core) APIVersion() uint {
|
||||
return uint(C.bridge_retro_api_version(core.symRetroAPIVersion))
|
||||
return coreResult(func() uint { return uint(C.bridge_retro_api_version(core.symRetroAPIVersion)) })
|
||||
}
|
||||
|
||||
// Deinit takes care of the library global deinitialization
|
||||
func (core *Core) Deinit() {
|
||||
C.bridge_retro_deinit(core.symRetroDeinit)
|
||||
coreCall(func() { C.bridge_retro_deinit(core.symRetroDeinit) })
|
||||
close(coreCalls)
|
||||
C.cothread_deinit()
|
||||
coreCalls, frontendCalls = nil, nil
|
||||
DlClose(core.handle)
|
||||
core.MemoryMap = nil
|
||||
environment = nil
|
||||
videoRefresh = nil
|
||||
audioSample = nil
|
||||
@@ -461,6 +622,7 @@ func (core *Core) Deinit() {
|
||||
inputState = nil
|
||||
log = nil
|
||||
getTimeUsec = nil
|
||||
getProcAddress = nil
|
||||
}
|
||||
|
||||
// Run runs the game for one video frame.
|
||||
@@ -470,12 +632,12 @@ func (core *Core) Deinit() {
|
||||
// a frame if GET_CAN_DUPE returns true.
|
||||
// In this case, the video callback can take a NULL argument for data.
|
||||
func (core *Core) Run() {
|
||||
C.bridge_retro_run(core.symRetroRun)
|
||||
coreCall(func() { C.bridge_retro_run(core.symRetroRun) })
|
||||
}
|
||||
|
||||
// Reset resets the current game.
|
||||
func (core *Core) Reset() {
|
||||
C.bridge_retro_reset(core.symRetroReset)
|
||||
coreCall(func() { C.bridge_retro_reset(core.symRetroReset) })
|
||||
}
|
||||
|
||||
// GetSystemInfo returns statically known system info. Pointers provided in *info
|
||||
@@ -483,7 +645,7 @@ func (core *Core) Reset() {
|
||||
// Can be called at any time, even before retro_init().
|
||||
func (core *Core) GetSystemInfo() SystemInfo {
|
||||
rsi := C.struct_retro_system_info{}
|
||||
C.bridge_retro_get_system_info(core.symRetroGetSystemInfo, &rsi)
|
||||
coreCall(func() { C.bridge_retro_get_system_info(core.symRetroGetSystemInfo, &rsi) })
|
||||
return SystemInfo{
|
||||
LibraryName: C.GoString(rsi.library_name),
|
||||
LibraryVersion: C.GoString(rsi.library_version),
|
||||
@@ -501,7 +663,7 @@ func (core *Core) GetSystemInfo() SystemInfo {
|
||||
// desire a particular aspect ratio.
|
||||
func (core *Core) GetSystemAVInfo() SystemAVInfo {
|
||||
avi := C.struct_retro_system_av_info{}
|
||||
C.bridge_retro_get_system_av_info(core.symRetroGetSystemAVInfo, &avi)
|
||||
coreCall(func() { C.bridge_retro_get_system_av_info(core.symRetroGetSystemAVInfo, &avi) })
|
||||
return SystemAVInfo{
|
||||
Geometry: GameGeometry{
|
||||
AspectRatio: float64(avi.geometry.aspect_ratio),
|
||||
@@ -523,7 +685,7 @@ func (core *Core) LoadGame(gi GameInfo) bool {
|
||||
rgi.path = C.CString(gi.Path)
|
||||
rgi.size = C.size_t(gi.Size)
|
||||
rgi.data = gi.Data
|
||||
return bool(C.bridge_retro_load_game(core.symRetroLoadGame, &rgi))
|
||||
return coreResult(func() bool { return bool(C.bridge_retro_load_game(core.symRetroLoadGame, &rgi)) })
|
||||
}
|
||||
|
||||
// SerializeSize returns the amount of data the implementation requires to serialize
|
||||
@@ -532,13 +694,14 @@ func (core *Core) LoadGame(gi GameInfo) bool {
|
||||
// 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.
|
||||
func (core *Core) SerializeSize() uint {
|
||||
return uint(C.bridge_retro_serialize_size(core.symRetroSerializeSize))
|
||||
return coreResult(func() uint { return uint(C.bridge_retro_serialize_size(core.symRetroSerializeSize)) })
|
||||
}
|
||||
|
||||
// Serialize serializes internal state and returns the state as a byte slice.
|
||||
func (core *Core) Serialize(size uint) ([]byte, error) {
|
||||
data := C.malloc(C.size_t(size))
|
||||
ok := bool(C.bridge_retro_serialize(core.symRetroSerialize, data, C.size_t(size)))
|
||||
defer C.free(data)
|
||||
ok := coreResult(func() bool { return bool(C.bridge_retro_serialize(core.symRetroSerialize, data, C.size_t(size))) })
|
||||
if !ok {
|
||||
return nil, errors.New("retro_serialize failed")
|
||||
}
|
||||
@@ -551,7 +714,12 @@ func (core *Core) Unserialize(bytes []byte, size uint) error {
|
||||
if size == 0 || len(bytes) == 0 {
|
||||
return errors.New("retro_unserialize failed")
|
||||
}
|
||||
ok := bool(C.bridge_retro_unserialize(core.symRetroUnserialize, unsafe.Pointer(&bytes[0]), C.size_t(size)))
|
||||
if uint(len(bytes)) > size {
|
||||
size = uint(len(bytes))
|
||||
}
|
||||
ok := coreResult(func() bool {
|
||||
return bool(C.bridge_retro_unserialize(core.symRetroUnserialize, unsafe.Pointer(&bytes[0]), C.size_t(size)))
|
||||
})
|
||||
if !ok {
|
||||
return errors.New("retro_unserialize failed")
|
||||
}
|
||||
@@ -560,49 +728,51 @@ func (core *Core) Unserialize(bytes []byte, size uint) error {
|
||||
|
||||
// UnloadGame unloads a currently loaded game
|
||||
func (core *Core) UnloadGame() {
|
||||
C.bridge_retro_unload_game(core.symRetroUnloadGame)
|
||||
coreCall(func() { C.bridge_retro_unload_game(core.symRetroUnloadGame) })
|
||||
}
|
||||
|
||||
// SetEnvironment sets the environment callback.
|
||||
// Must be called before Init
|
||||
func (core *Core) SetEnvironment(f environmentFunc) {
|
||||
environment = f
|
||||
C.bridge_retro_set_environment(core.symRetroSetEnvironment, C.coreEnvironment_cgo)
|
||||
coreCall(func() { C.bridge_retro_set_environment(core.symRetroSetEnvironment, C.coreEnvironment_cgo) })
|
||||
}
|
||||
|
||||
// SetVideoRefresh sets the video refresh callback.
|
||||
// Must be set before the first Run call
|
||||
func (core *Core) SetVideoRefresh(f videoRefreshFunc) {
|
||||
videoRefresh = f
|
||||
C.bridge_retro_set_video_refresh(core.symRetroSetVideoRefresh, C.coreVideoRefresh_cgo)
|
||||
coreCall(func() { C.bridge_retro_set_video_refresh(core.symRetroSetVideoRefresh, C.coreVideoRefresh_cgo) })
|
||||
}
|
||||
|
||||
// SetAudioSample sets the audio sample callback.
|
||||
// Must be set before the first Run call
|
||||
func (core *Core) SetAudioSample(f audioSampleFunc) {
|
||||
audioSample = f
|
||||
C.bridge_retro_set_audio_sample(core.symRetroSetAudioSample, C.coreAudioSample_cgo)
|
||||
coreCall(func() { C.bridge_retro_set_audio_sample(core.symRetroSetAudioSample, C.coreAudioSample_cgo) })
|
||||
}
|
||||
|
||||
// SetAudioSampleBatch sets the audio sample batch callback.
|
||||
// Must be set before the first Run call
|
||||
func (core *Core) SetAudioSampleBatch(f audioSampleBatchFunc) {
|
||||
audioSampleBatch = f
|
||||
C.bridge_retro_set_audio_sample_batch(core.symRetroSetAudioSampleBatch, C.coreAudioSampleBatch_cgo)
|
||||
coreCall(func() {
|
||||
C.bridge_retro_set_audio_sample_batch(core.symRetroSetAudioSampleBatch, C.coreAudioSampleBatch_cgo)
|
||||
})
|
||||
}
|
||||
|
||||
// SetInputPoll sets the input poll callback.
|
||||
// Must be set before the first Run call
|
||||
func (core *Core) SetInputPoll(f inputPollFunc) {
|
||||
inputPoll = f
|
||||
C.bridge_retro_set_input_poll(core.symRetroSetInputPoll, C.coreInputPoll_cgo)
|
||||
coreCall(func() { C.bridge_retro_set_input_poll(core.symRetroSetInputPoll, C.coreInputPoll_cgo) })
|
||||
}
|
||||
|
||||
// SetInputState sets the input state callback.
|
||||
// Must be set before the first Run call
|
||||
func (core *Core) SetInputState(f inputStateFunc) {
|
||||
inputState = f
|
||||
C.bridge_retro_set_input_state(core.symRetroSetInputState, C.coreInputState_cgo)
|
||||
coreCall(func() { C.bridge_retro_set_input_state(core.symRetroSetInputState, C.coreInputState_cgo) })
|
||||
}
|
||||
|
||||
// BindLogCallback binds f to the log callback
|
||||
@@ -621,7 +791,9 @@ func (core *Core) BindPerfCallback(data unsafe.Pointer, f getTimeUsecFunc) {
|
||||
|
||||
// SetControllerPortDevice sets the device type attached to a controller port
|
||||
func (core *Core) SetControllerPortDevice(port uint, device uint32) {
|
||||
C.bridge_retro_set_controller_port_device(core.symRetroSetControllerPortDevice, C.unsigned(port), C.unsigned(device))
|
||||
coreCall(func() {
|
||||
C.bridge_retro_set_controller_port_device(core.symRetroSetControllerPortDevice, C.unsigned(port), C.unsigned(device))
|
||||
})
|
||||
}
|
||||
|
||||
//export coreEnvironment
|
||||
@@ -629,6 +801,12 @@ func coreEnvironment(cmd C.unsigned, data unsafe.Pointer) bool {
|
||||
if environment == nil {
|
||||
return false
|
||||
}
|
||||
// Only window operations need the frontend thread. Routine queries stay here.
|
||||
if cmd == C.RETRO_ENVIRONMENT_SET_HW_RENDER || cmd == C.RETRO_ENVIRONMENT_SHUTDOWN {
|
||||
var result bool
|
||||
frontendCall(func() { result = environment(uint32(cmd), data) })
|
||||
return result
|
||||
}
|
||||
return environment(uint32(cmd), data)
|
||||
}
|
||||
|
||||
@@ -653,6 +831,12 @@ func coreInputState(port C.unsigned, device C.unsigned, index C.unsigned, id C.u
|
||||
if inputState == nil {
|
||||
return 0
|
||||
}
|
||||
// Mouse state is queried through GLFW rather than the polled input snapshot.
|
||||
if device == C.RETRO_DEVICE_MOUSE {
|
||||
var result C.int16_t
|
||||
frontendCall(func() { result = C.int16_t(inputState(uint(port), uint32(device), uint(index), uint(id))) })
|
||||
return result
|
||||
}
|
||||
return C.int16_t(inputState(uint(port), uint32(device), uint(index), uint(id)))
|
||||
}
|
||||
|
||||
@@ -688,6 +872,16 @@ func coreGetTimeUsec() C.uint64_t {
|
||||
return C.uint64_t(getTimeUsec())
|
||||
}
|
||||
|
||||
//export coreGetProcAddress
|
||||
func coreGetProcAddress(sym *C.char) C.uintptr_t {
|
||||
return C.uintptr_t(getProcAddress(C.GoString(sym)))
|
||||
}
|
||||
|
||||
// SetCurrentFramebufferValue updates the C-side framebuffer callback target
|
||||
func SetCurrentFramebufferValue(fb uintptr) {
|
||||
C.coreSetCurrentFramebuffer_cgo(C.uintptr_t(fb))
|
||||
}
|
||||
|
||||
// SetData is a setter for the data of a GameInfo type
|
||||
func (gi *GameInfo) SetData(bytes []byte) {
|
||||
cstr := C.CString(string(bytes))
|
||||
@@ -767,6 +961,28 @@ func GetCoreOptionsIntl(data unsafe.Pointer) []CoreOptionDefinition {
|
||||
return definitions
|
||||
}
|
||||
|
||||
// GetMemoryMap is an environment callback helper that returns the list of
|
||||
// memory regions EnvironmentSetMemoryMap.
|
||||
func GetMemoryMap(data unsafe.Pointer) []MemoryDescriptor {
|
||||
cMemmap := (*C.struct_retro_memory_map)(data)
|
||||
descriptors := make([]MemoryDescriptor, int(cMemmap.num_descriptors))
|
||||
for i := 0; i < len(descriptors); i++ {
|
||||
cDescriptor := unsafe.Pointer(uintptr(unsafe.Pointer(cMemmap.descriptors)) + uintptr(i)*unsafe.Sizeof(*cMemmap.descriptors))
|
||||
d := *(*C.struct_retro_memory_descriptor)(cDescriptor)
|
||||
|
||||
descriptors[i] = MemoryDescriptor{
|
||||
Flags: uint64(d.flags),
|
||||
Ptr: d.ptr,
|
||||
Offset: uintptr(d.start),
|
||||
Select: uintptr(d._select),
|
||||
Disconnect: uintptr(d.disconnect),
|
||||
Len: uintptr(d.len),
|
||||
Addrspace: C.GoString(d.addrspace),
|
||||
}
|
||||
}
|
||||
return descriptors
|
||||
}
|
||||
|
||||
// GetGeometry is an environment callback helper that returns the game geometry
|
||||
// in EnvironmentSetGeometry.
|
||||
func GetGeometry(data unsafe.Pointer) GameGeometry {
|
||||
@@ -823,20 +1039,58 @@ func (core *Core) SetFrameTimeCallback(data unsafe.Pointer) {
|
||||
ftc := &FrameTimeCallback{}
|
||||
ftc.Reference = int64(c.reference)
|
||||
ftc.Callback = func(usec int64) {
|
||||
C.bridge_retro_frame_time_callback(c.callback, C.retro_usec_t(usec))
|
||||
coreCall(func() { C.bridge_retro_frame_time_callback(c.callback, C.retro_usec_t(usec)) })
|
||||
}
|
||||
core.FrameTimeCallback = ftc
|
||||
}
|
||||
|
||||
// SetHWRenderCallback is an environment callback helper to set the HWRenderCallback
|
||||
func SetHWRenderCallback(
|
||||
data unsafe.Pointer,
|
||||
currentFramebuffer getCurrentFramebufferFunc,
|
||||
procAddress getProcAddressFunc,
|
||||
) *HWRenderCallback {
|
||||
c := (*C.struct_retro_hw_render_callback)(data)
|
||||
hwrc := HWRenderCallback{}
|
||||
hwrc.HWContextType = uint32(c.context_type)
|
||||
|
||||
getProcAddress = procAddress
|
||||
SetCurrentFramebufferValue(currentFramebuffer())
|
||||
c.get_current_framebuffer = (C.retro_hw_get_current_framebuffer_t)(C.coreGetCurrentFramebuffer_cgo)
|
||||
c.get_proc_address = (C.retro_hw_get_proc_address_t)(C.coreGetProcAddress_cgo)
|
||||
|
||||
// The request may be stack-allocated by the core. Keep callback values, not c.
|
||||
reset, destroy := c.context_reset, c.context_destroy
|
||||
if reset != nil {
|
||||
hwrc.ContextReset = func() {
|
||||
coreCall(func() { C.bridge_retro_hw_context_reset(reset) })
|
||||
}
|
||||
}
|
||||
|
||||
hwrc.Depth = bool(c.depth)
|
||||
hwrc.Stencil = bool(c.stencil)
|
||||
hwrc.BottomLeftOrigin = bool(c.bottom_left_origin)
|
||||
hwrc.VersionMajor = uint(c.version_major)
|
||||
hwrc.VersionMinor = uint(c.version_minor)
|
||||
hwrc.CacheContext = bool(c.cache_context)
|
||||
if destroy != nil {
|
||||
hwrc.ContextDestroy = func() {
|
||||
coreCall(func() { C.bridge_retro_hw_context_destroy(destroy) })
|
||||
}
|
||||
}
|
||||
hwrc.DebugContext = bool(c.debug_context)
|
||||
return &hwrc
|
||||
}
|
||||
|
||||
// SetAudioCallback is an environment callback helper to set the AudioCallback
|
||||
func (core *Core) SetAudioCallback(data unsafe.Pointer) {
|
||||
c := *(*C.struct_retro_audio_callback)(data)
|
||||
auc := &AudioCallback{}
|
||||
auc.Callback = func() {
|
||||
C.bridge_retro_audio_callback(c.callback)
|
||||
coreCall(func() { C.bridge_retro_audio_callback(c.callback) })
|
||||
}
|
||||
auc.SetState = func(state bool) {
|
||||
C.bridge_retro_audio_set_state(c.set_state, C.bool(state))
|
||||
coreCall(func() { C.bridge_retro_audio_set_state(c.set_state, C.bool(state)) })
|
||||
}
|
||||
core.AudioCallback = auc
|
||||
}
|
||||
@@ -844,13 +1098,15 @@ func (core *Core) SetAudioCallback(data unsafe.Pointer) {
|
||||
// GetMemorySize returns the size of a region of the memory.
|
||||
// See memory constants.
|
||||
func (core *Core) GetMemorySize(id uint32) uint {
|
||||
return uint(C.bridge_retro_get_memory_size(core.symRetroGetMemorySize, C.unsigned(id)))
|
||||
return coreResult(func() uint { return uint(C.bridge_retro_get_memory_size(core.symRetroGetMemorySize, C.unsigned(id))) })
|
||||
}
|
||||
|
||||
// GetMemoryData returns the size of a region of the memory.
|
||||
// See memory constants.
|
||||
func (core *Core) GetMemoryData(id uint32) unsafe.Pointer {
|
||||
return C.bridge_retro_get_memory_data(core.symRetroGetMemoryData, C.unsigned(id))
|
||||
return coreResult(func() unsafe.Pointer {
|
||||
return C.bridge_retro_get_memory_data(core.symRetroGetMemoryData, C.unsigned(id))
|
||||
})
|
||||
}
|
||||
|
||||
// DiskControlCallback is an interface which frontend can use to eject and insert disk images
|
||||
@@ -864,22 +1120,25 @@ type DiskControlCallback struct {
|
||||
|
||||
// SetDiskControlCallback sets an interface which frontend can use to eject and insert disk images
|
||||
func (core *Core) SetDiskControlCallback(data unsafe.Pointer) {
|
||||
if data == nil {
|
||||
return
|
||||
}
|
||||
c := *(*C.struct_retro_disk_control_callback)(data)
|
||||
dcc := &DiskControlCallback{}
|
||||
dcc.SetEjectState = func(state bool) {
|
||||
C.bridge_retro_set_eject_state(c.set_eject_state, C.bool(state))
|
||||
coreCall(func() { C.bridge_retro_set_eject_state(c.set_eject_state, C.bool(state)) })
|
||||
}
|
||||
dcc.GetEjectState = func() bool {
|
||||
return bool(C.bridge_retro_get_eject_state(c.get_eject_state))
|
||||
return coreResult(func() bool { return bool(C.bridge_retro_get_eject_state(c.get_eject_state)) })
|
||||
}
|
||||
dcc.GetImageIndex = func() uint {
|
||||
return uint(C.bridge_retro_get_image_index(c.get_image_index))
|
||||
return coreResult(func() uint { return uint(C.bridge_retro_get_image_index(c.get_image_index)) })
|
||||
}
|
||||
dcc.SetImageIndex = func(index uint) {
|
||||
C.bridge_retro_set_image_index(c.set_image_index, C.uint(index))
|
||||
coreCall(func() { C.bridge_retro_set_image_index(c.set_image_index, C.uint(index)) })
|
||||
}
|
||||
dcc.GetNumImages = func() uint {
|
||||
return uint(C.bridge_retro_get_num_images(c.get_num_images))
|
||||
return coreResult(func() uint { return uint(C.bridge_retro_get_num_images(c.get_num_images)) })
|
||||
}
|
||||
core.DiskControlCallback = dcc
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package libretro // Core is an instance of a dynamically loaded libretro core
|
||||
|
||||
import "unsafe"
|
||||
|
||||
// Core is an instance of a dynamically loaded libretro core
|
||||
type Core struct {
|
||||
handle DlHandle
|
||||
|
||||
symRetroInit unsafe.Pointer
|
||||
symRetroDeinit unsafe.Pointer
|
||||
symRetroAPIVersion unsafe.Pointer
|
||||
symRetroGetSystemInfo unsafe.Pointer
|
||||
symRetroGetSystemAVInfo unsafe.Pointer
|
||||
symRetroSetEnvironment unsafe.Pointer
|
||||
symRetroSetVideoRefresh unsafe.Pointer
|
||||
symRetroSetControllerPortDevice unsafe.Pointer
|
||||
symRetroSetInputPoll unsafe.Pointer
|
||||
symRetroSetInputState unsafe.Pointer
|
||||
symRetroSetAudioSample unsafe.Pointer
|
||||
symRetroSetAudioSampleBatch unsafe.Pointer
|
||||
symRetroRun unsafe.Pointer
|
||||
symRetroReset unsafe.Pointer
|
||||
symRetroLoadGame unsafe.Pointer
|
||||
symRetroUnloadGame unsafe.Pointer
|
||||
symRetroSerializeSize unsafe.Pointer
|
||||
symRetroSerialize unsafe.Pointer
|
||||
symRetroUnserialize unsafe.Pointer
|
||||
symRetroGetMemorySize unsafe.Pointer
|
||||
symRetroGetMemoryData unsafe.Pointer
|
||||
|
||||
AudioCallback *AudioCallback
|
||||
FrameTimeCallback *FrameTimeCallback
|
||||
DiskControlCallback *DiskControlCallback
|
||||
HWRenderCallback *HWRenderCallback
|
||||
|
||||
MemoryMap []MemoryDescriptor
|
||||
}
|
||||
@@ -14,52 +14,37 @@ import (
|
||||
*/
|
||||
import "C"
|
||||
|
||||
// Core is an instance of a dynalically loaded libretro core
|
||||
type Core struct {
|
||||
handle unsafe.Pointer
|
||||
|
||||
symRetroInit unsafe.Pointer
|
||||
symRetroDeinit unsafe.Pointer
|
||||
symRetroAPIVersion unsafe.Pointer
|
||||
symRetroGetSystemInfo unsafe.Pointer
|
||||
symRetroGetSystemAVInfo unsafe.Pointer
|
||||
symRetroSetEnvironment unsafe.Pointer
|
||||
symRetroSetVideoRefresh unsafe.Pointer
|
||||
symRetroSetControllerPortDevice unsafe.Pointer
|
||||
symRetroSetInputPoll unsafe.Pointer
|
||||
symRetroSetInputState unsafe.Pointer
|
||||
symRetroSetAudioSample unsafe.Pointer
|
||||
symRetroSetAudioSampleBatch unsafe.Pointer
|
||||
symRetroRun unsafe.Pointer
|
||||
symRetroReset unsafe.Pointer
|
||||
symRetroLoadGame unsafe.Pointer
|
||||
symRetroUnloadGame unsafe.Pointer
|
||||
symRetroSerializeSize unsafe.Pointer
|
||||
symRetroSerialize unsafe.Pointer
|
||||
symRetroUnserialize unsafe.Pointer
|
||||
symRetroGetMemorySize unsafe.Pointer
|
||||
symRetroGetMemoryData unsafe.Pointer
|
||||
|
||||
AudioCallback *AudioCallback
|
||||
FrameTimeCallback *FrameTimeCallback
|
||||
DiskControlCallback *DiskControlCallback
|
||||
}
|
||||
// DlHandle is a handle to a dynamic library
|
||||
type DlHandle = unsafe.Pointer
|
||||
|
||||
// DlSym loads a symbol from a dynamic library
|
||||
func (core *Core) DlSym(name string) unsafe.Pointer {
|
||||
return C.dlsym(core.handle, C.CString(name))
|
||||
func DlSym(handle DlHandle, name string) unsafe.Pointer {
|
||||
return C.dlsym(handle, C.CString(name))
|
||||
}
|
||||
|
||||
// DlOpen opens a dynamic library
|
||||
func DlOpen(path string) (unsafe.Pointer, error) {
|
||||
func DlOpen(path string) (DlHandle, error) {
|
||||
cpath := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(cpath))
|
||||
|
||||
h := C.dlopen(cpath, C.RTLD_LAZY|C.RTLD_GLOBAL)
|
||||
cerr := C.dlerror()
|
||||
if h == nil || cerr != nil {
|
||||
if h == nil {
|
||||
cerr := C.dlerror()
|
||||
err := C.GoString(cerr)
|
||||
return nil, errors.New(err)
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// DlClose closes a dynamic library
|
||||
func DlClose(handle DlHandle) error {
|
||||
result := C.dlclose(handle)
|
||||
if int(result) != 0 {
|
||||
cerr := C.dlerror()
|
||||
if cerr != nil {
|
||||
err := C.GoString(cerr)
|
||||
return errors.New(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,48 +1,33 @@
|
||||
package libretro
|
||||
|
||||
/*
|
||||
#include <stdint.h>
|
||||
|
||||
static void *symbol_from_addr(uintptr_t addr) {
|
||||
return (void *)addr;
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Core is an instance of a dynalically loaded libretro core
|
||||
type Core struct {
|
||||
handle *syscall.DLL
|
||||
|
||||
symRetroInit unsafe.Pointer
|
||||
symRetroDeinit unsafe.Pointer
|
||||
symRetroAPIVersion unsafe.Pointer
|
||||
symRetroGetSystemInfo unsafe.Pointer
|
||||
symRetroGetSystemAVInfo unsafe.Pointer
|
||||
symRetroSetEnvironment unsafe.Pointer
|
||||
symRetroSetVideoRefresh unsafe.Pointer
|
||||
symRetroSetControllerPortDevice unsafe.Pointer
|
||||
symRetroSetInputPoll unsafe.Pointer
|
||||
symRetroSetInputState unsafe.Pointer
|
||||
symRetroSetAudioSample unsafe.Pointer
|
||||
symRetroSetAudioSampleBatch unsafe.Pointer
|
||||
symRetroRun unsafe.Pointer
|
||||
symRetroReset unsafe.Pointer
|
||||
symRetroLoadGame unsafe.Pointer
|
||||
symRetroUnloadGame unsafe.Pointer
|
||||
symRetroSerializeSize unsafe.Pointer
|
||||
symRetroSerialize unsafe.Pointer
|
||||
symRetroUnserialize unsafe.Pointer
|
||||
symRetroGetMemorySize unsafe.Pointer
|
||||
symRetroGetMemoryData unsafe.Pointer
|
||||
|
||||
AudioCallback *AudioCallback
|
||||
FrameTimeCallback *FrameTimeCallback
|
||||
DiskControlCallback *DiskControlCallback
|
||||
}
|
||||
// DlHandle is a handle to a dynamic library
|
||||
type DlHandle = *syscall.DLL
|
||||
|
||||
// DlSym loads a symbol from a dynamic library
|
||||
func (core *Core) DlSym(name string) unsafe.Pointer {
|
||||
proc := core.handle.MustFindProc(name)
|
||||
return unsafe.Pointer(proc.Addr())
|
||||
func DlSym(handle DlHandle, name string) unsafe.Pointer {
|
||||
proc := handle.MustFindProc(name)
|
||||
return C.symbol_from_addr(C.uintptr_t(proc.Addr()))
|
||||
}
|
||||
|
||||
// DlOpen opens a dynamic library
|
||||
func DlOpen(path string) (*syscall.DLL, error) {
|
||||
func DlOpen(path string) (DlHandle, error) {
|
||||
return syscall.LoadDLL(path)
|
||||
}
|
||||
|
||||
// DlClose closes a dynamic library
|
||||
func DlClose(handle DlHandle) error {
|
||||
return handle.Release()
|
||||
}
|
||||
+2
-2
@@ -70,7 +70,7 @@ func DownloadRelease(path, url string) {
|
||||
return
|
||||
}
|
||||
|
||||
n := ntf.DisplayAndLog(ntf.Info, "Menu", "Downloading update 0%%")
|
||||
n := ntf.DisplayAndLog(ntf.Info, "Menu", "Downloading update 0%")
|
||||
downloading = true
|
||||
defer func() { downloading = false }()
|
||||
|
||||
@@ -89,7 +89,7 @@ Loop:
|
||||
for {
|
||||
select {
|
||||
case <-t.C:
|
||||
n.Update(ntf.Info, "Downloading update %.0f%%%% ", 100*resp.Progress())
|
||||
n.Updatef(ntf.Info, "Downloading update %.0f%% ", 100*resp.Progress())
|
||||
progress = resp.Progress()
|
||||
|
||||
case <-resp.Done:
|
||||
|
||||
@@ -2,12 +2,13 @@ package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/go-gl/glfw/v3.3/glfw"
|
||||
"github.com/go-gl/glfw/v3.4/glfw"
|
||||
"github.com/libretro/ludo/audio"
|
||||
"github.com/libretro/ludo/core"
|
||||
"github.com/libretro/ludo/history"
|
||||
@@ -29,8 +30,24 @@ func init() {
|
||||
|
||||
var frame = 0
|
||||
|
||||
func fulfillPendingScreenshot(vid *video.Video) {
|
||||
if state.PendingScreenshotName == "" {
|
||||
return
|
||||
}
|
||||
|
||||
name := state.PendingScreenshotName
|
||||
done := state.PendingScreenshotDone
|
||||
state.PendingScreenshotName = ""
|
||||
state.PendingScreenshotDone = nil
|
||||
|
||||
err := vid.TakeScreenshotInFrontendFrame(name)
|
||||
if done != nil {
|
||||
done(err)
|
||||
}
|
||||
}
|
||||
|
||||
func runLoop(vid *video.Video, m *menu.Menu) {
|
||||
currTime := time.Now()
|
||||
var currTime time.Time
|
||||
prevTime := time.Now()
|
||||
for !vid.Window.ShouldClose() {
|
||||
currTime = time.Now()
|
||||
@@ -38,10 +55,19 @@ func runLoop(vid *video.Video, m *menu.Menu) {
|
||||
glfw.PollEvents()
|
||||
m.ProcessHotkeys()
|
||||
ntf.Process(dt)
|
||||
vid.ResizeViewport()
|
||||
m.UpdatePalette()
|
||||
if !state.MenuActive {
|
||||
input.Poll()
|
||||
menuActive := state.MenuActive
|
||||
if menuActive {
|
||||
m.Update(dt)
|
||||
}
|
||||
if state.MenuContextResetNeeded {
|
||||
m.ContextReset()
|
||||
state.MenuContextResetNeeded = false
|
||||
}
|
||||
if !menuActive {
|
||||
if state.CoreRunning {
|
||||
vid.BeginCore()
|
||||
state.Core.Run()
|
||||
if state.Core.FrameTimeCallback != nil {
|
||||
state.Core.FrameTimeCallback.Callback(state.Core.FrameTimeCallback.Reference)
|
||||
@@ -49,19 +75,23 @@ func runLoop(vid *video.Video, m *menu.Menu) {
|
||||
if state.Core.AudioCallback != nil {
|
||||
state.Core.AudioCallback.Callback()
|
||||
}
|
||||
vid.EndCore()
|
||||
}
|
||||
vid.BeginFrontendFrame()
|
||||
vid.Render()
|
||||
vid.CachePresentedFrame()
|
||||
m.RenderNotifications()
|
||||
frame++
|
||||
if frame%600 == 0 { // save sram about every 10 sec
|
||||
savefiles.SaveSRAM()
|
||||
}
|
||||
} else {
|
||||
input.Poll()
|
||||
m.Update(dt)
|
||||
vid.Render()
|
||||
vid.BeginFrontendFrame()
|
||||
vid.RenderPresentedFrame()
|
||||
fulfillPendingScreenshot(vid)
|
||||
m.Render(dt)
|
||||
m.RenderNotifications()
|
||||
}
|
||||
m.RenderNotifications()
|
||||
if state.FastForward {
|
||||
glfw.SwapInterval(0)
|
||||
} else {
|
||||
@@ -79,7 +109,18 @@ func main() {
|
||||
log.Println("[Settings]: Using default settings")
|
||||
}
|
||||
|
||||
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
|
||||
// ExitOnError causes flags to quit after displaying help.
|
||||
// (--help counts as an error)
|
||||
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
|
||||
|
||||
// customize help message
|
||||
flag.CommandLine.Usage = func() {
|
||||
fmt.Printf("Usage: %s [OPTIONS] [content]\n", os.Args[0])
|
||||
fmt.Printf("Options:\n")
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
// set arguments
|
||||
flag.StringVar(&state.CorePath, "L", "", "Path to the libretro core")
|
||||
flag.BoolVar(&state.Verbose, "v", false, "Verbose logs")
|
||||
flag.BoolVar(&state.LudOS, "ludos", false, "Expose the features related to LudOS")
|
||||
@@ -116,18 +157,14 @@ func main() {
|
||||
input.Init(vid)
|
||||
|
||||
if len(state.CorePath) > 0 {
|
||||
err := core.Load(state.CorePath)
|
||||
if err == nil {
|
||||
if len(gamePath) > 0 {
|
||||
err := core.LoadGame(gamePath)
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
} else {
|
||||
m.WarpToQuickMenu()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := core.Load(state.CorePath); err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
} else if len(gamePath) > 0 {
|
||||
if err := core.LoadGame(gamePath); err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
} else {
|
||||
m.WarpToQuickMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+18
-8
@@ -1,21 +1,31 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"github.com/go-gl/glfw/v3.3/glfw"
|
||||
"github.com/libretro/ludo/input"
|
||||
)
|
||||
|
||||
// Used to easily compose different hint bars based on the context.
|
||||
func stackHint(stack *float32, icon uint32, label string, h int) {
|
||||
menu.Font.SetColor(darkGrey)
|
||||
*stack += 30 * menu.ratio
|
||||
menu.DrawImage(icon, *stack, float32(h)-70*menu.ratio, 70*menu.ratio, 70*menu.ratio, 1.0, darkGrey)
|
||||
func stackHintLeft(stack *float32, icon uint32, label string, h int) {
|
||||
menu.Font.SetColor(hintTextColor)
|
||||
menu.DrawImage(icon, *stack, float32(h)-79*menu.ratio, 70*menu.ratio, 70*menu.ratio, 1.0, 0, hintTextColor)
|
||||
*stack += 70 * menu.ratio
|
||||
menu.Font.Printf(*stack, float32(h)-23*menu.ratio, 0.4*menu.ratio, label)
|
||||
*stack += menu.Font.Width(0.4*menu.ratio, label)
|
||||
menu.Font.Print(*stack, float32(h)-30*menu.ratio, menu.ratio, label)
|
||||
*stack += menu.Font.Width(menu.ratio, label)
|
||||
*stack += 32 * menu.ratio
|
||||
}
|
||||
|
||||
// Used to easily compose different hint bars based on the context.
|
||||
func stackHintRight(stack *float32, icon uint32, label string, h int) {
|
||||
*stack -= menu.Font.Width(menu.ratio, label)
|
||||
menu.Font.SetColor(hintTextColor)
|
||||
menu.Font.Print(*stack, float32(h)-30*menu.ratio, menu.ratio, label)
|
||||
*stack -= 70 * menu.ratio
|
||||
menu.DrawImage(icon, *stack, float32(h)-79*menu.ratio, 70*menu.ratio, 70*menu.ratio, 1.0, 0, hintTextColor)
|
||||
*stack -= 32 * menu.ratio
|
||||
}
|
||||
|
||||
func hintIcons() (arrows, upDown, leftRight, a, b, x, y, start, slct, guide uint32) {
|
||||
if glfw.Joystick(0).IsGamepad() {
|
||||
if input.JoypadPlugged() {
|
||||
return menu.icons["pad-arrows"],
|
||||
menu.icons["pad-up-down"],
|
||||
menu.icons["pad-left-right"],
|
||||
|
||||
+5
-6
@@ -143,7 +143,7 @@ func genericInput(list *entry, dt float32) {
|
||||
|
||||
// indexed allows jumping directly to the next letter in playlists
|
||||
func indexed(list *entry, offset int) int {
|
||||
curr := list.children[list.ptr].label[0]
|
||||
curr := firstRune(list.children[list.ptr].label)
|
||||
for i, t := range list.indexes {
|
||||
if curr == t.Char {
|
||||
if i+offset < 0 {
|
||||
@@ -193,14 +193,13 @@ func (m *Menu) ProcessHotkeys() {
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle fullscreen if ActionFullscreenToggle is pressed
|
||||
if input.Pressed[0][input.ActionFullscreenToggle] == 1 {
|
||||
// Toggle fullscreen if ActionFullscreenToggle is released
|
||||
if input.Released[0][input.ActionFullscreenToggle] == 1 {
|
||||
settings.Current.VideoFullscreen = !settings.Current.VideoFullscreen
|
||||
m.Reconfigure(settings.Current.VideoFullscreen)
|
||||
m.ContextReset()
|
||||
m.SetFullscreen(settings.Current.VideoFullscreen)
|
||||
err := settings.Save()
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", "Error saving settings: %s", err)
|
||||
ntf.DisplayAndLogf(ntf.Error, "Menu", "Error saving settings: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-4
@@ -102,10 +102,12 @@ func (m *Menu) ContextReset() {
|
||||
m.icons[filename] = video.NewImage(path)
|
||||
}
|
||||
|
||||
currentScreenIndex := len(m.stack) - 1
|
||||
curList := m.stack[currentScreenIndex].Entry()
|
||||
for i := range curList.children {
|
||||
curList.children[i].thumbnail = 0
|
||||
// Every retained scene's texture names belong to the old context.
|
||||
for _, scene := range m.stack {
|
||||
list := scene.Entry()
|
||||
for i := range list.children {
|
||||
list.children[i].thumbnail = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-5
@@ -114,7 +114,7 @@ func Test_buildExplorer(t *testing.T) {
|
||||
os.Create(tmp + "File 1.txt")
|
||||
os.Create(tmp + "File 2.img")
|
||||
os.Create(tmp + "File 3.txt")
|
||||
os.Create(tmp + "File 4.img")
|
||||
os.Create(tmp + "File 4.img") // this entry will be pushed at the end by sort+prettify
|
||||
os.Create(tmp + "File 5.txt")
|
||||
os.Create(tmp + "File 6.txt")
|
||||
os.Create(tmp + "File 7.txt")
|
||||
@@ -158,14 +158,14 @@ func Test_buildExplorer(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Targeted files have OK callbacks", func(t *testing.T) {
|
||||
children[3].callbackOK()
|
||||
children[2].callbackOK()
|
||||
if exec != 1 {
|
||||
t.Errorf("buildExplorer = %v, want %v", exec, 1)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Folders have folder icon", func(t *testing.T) {
|
||||
if children[4].icon != "folder" {
|
||||
if children[3].icon != "folder" {
|
||||
t.Errorf("buildExplorer = %v, want %v", children[4].icon, "folder")
|
||||
}
|
||||
})
|
||||
@@ -174,7 +174,7 @@ func Test_buildExplorer(t *testing.T) {
|
||||
if len(menu.stack) != 1 {
|
||||
t.Errorf("buildExplorer = %v, want %v", len(menu.stack), 1)
|
||||
}
|
||||
children[4].callbackOK()
|
||||
children[3].callbackOK()
|
||||
if len(menu.stack) != 2 {
|
||||
t.Errorf("buildExplorer = %v, want %v", len(menu.stack), 2)
|
||||
}
|
||||
@@ -182,7 +182,7 @@ func Test_buildExplorer(t *testing.T) {
|
||||
|
||||
t.Run("Prettifier should work", func(t *testing.T) {
|
||||
want := "IMAGE 4"
|
||||
if children[3].label != want {
|
||||
if children[4].label != want {
|
||||
t.Errorf("buildExplorer = %v, want %v", children[3].label, want)
|
||||
}
|
||||
})
|
||||
|
||||
+6
-16
@@ -5,23 +5,13 @@ import (
|
||||
"github.com/libretro/ludo/video"
|
||||
)
|
||||
|
||||
var severityFgColor = map[ntf.Severity]video.Color{
|
||||
ntf.Error: lightDanger,
|
||||
ntf.Warning: lightWarning,
|
||||
ntf.Success: lightSuccess,
|
||||
ntf.Info: lightInfo,
|
||||
}
|
||||
|
||||
var severityBgColor = map[ntf.Severity]video.Color{
|
||||
ntf.Error: darkDanger,
|
||||
ntf.Warning: darkWarning,
|
||||
ntf.Success: darkSuccess,
|
||||
ntf.Info: darkInfo,
|
||||
}
|
||||
var severityFgColor map[ntf.Severity]video.Color
|
||||
var severityBgColor map[ntf.Severity]video.Color
|
||||
|
||||
// RenderNotifications draws the list of notification messages on the viewport
|
||||
func (m *Menu) RenderNotifications() {
|
||||
fbw, fbh := m.GetFramebufferSize()
|
||||
m.ratio = float32(fbw) / 1920
|
||||
m.Font.UpdateResolution(fbw, fbh)
|
||||
var h float32 = 75
|
||||
stack := h
|
||||
@@ -31,7 +21,7 @@ func (m *Menu) RenderNotifications() {
|
||||
fading = 1
|
||||
}
|
||||
offset := fading*h - h
|
||||
lw := m.Font.Width(0.5*m.ratio, n.Message)
|
||||
lw := m.Font.Width(m.ratio, n.Message)
|
||||
fg := severityFgColor[n.Severity]
|
||||
bg := severityBgColor[n.Severity]
|
||||
m.DrawRect(
|
||||
@@ -43,10 +33,10 @@ func (m *Menu) RenderNotifications() {
|
||||
bg.Alpha(fading),
|
||||
)
|
||||
m.Font.SetColor(fg.Alpha(fading))
|
||||
m.Font.Printf(
|
||||
m.Font.Print(
|
||||
45*m.ratio,
|
||||
(stack+offset)*m.ratio,
|
||||
0.5*m.ratio,
|
||||
m.ratio,
|
||||
n.Message,
|
||||
)
|
||||
stack += h + offset
|
||||
|
||||
+506
-27
@@ -1,48 +1,527 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/video"
|
||||
|
||||
colorful "github.com/lucasb-eyer/go-colorful"
|
||||
)
|
||||
|
||||
var white = video.Color{R: 1, G: 1, B: 1, A: 1}
|
||||
var black = video.Color{R: 0, G: 0, B: 0, A: 1}
|
||||
// Global variables for the menu theme
|
||||
|
||||
var orange = video.Color{R: 0.8, G: 0.4, B: 0.1, A: 1}
|
||||
var cyan = video.Color{R: 0.8784, G: 1, B: 1, A: 1}
|
||||
var darkBlue = video.Color{R: 0.1, G: 0.1, B: 0.4, A: 1}
|
||||
var white = video.ColorFromHex("#ffffff")
|
||||
var black = video.ColorFromHex("#000000")
|
||||
var bgColor video.Color
|
||||
var cursorBgColor video.Color
|
||||
var textColor video.Color
|
||||
var mutedTextColor video.Color
|
||||
var textShadowColor video.Color
|
||||
var sepColor video.Color
|
||||
var hintTextColor video.Color
|
||||
var hintBgColor video.Color
|
||||
var titleTextColor video.Color
|
||||
var dialogTextColor video.Color
|
||||
|
||||
var lightGrey = video.Color{R: 0.75, G: 0.75, B: 0.75, A: 1}
|
||||
var mediumGrey = video.Color{R: 0.5, G: 0.5, B: 0.5, A: 1}
|
||||
var darkGrey = video.Color{R: 0.25, G: 0.25, B: 0.25, A: 1}
|
||||
var darkerGrey = video.Color{R: 0.10, G: 0.10, B: 0.10, A: 1}
|
||||
var infoBgColor video.Color
|
||||
var infoTextColor video.Color
|
||||
var successBgColor video.Color
|
||||
var successTextColor video.Color
|
||||
var dangerBgColor video.Color
|
||||
var dangerTextColor video.Color
|
||||
var warningBgColor video.Color
|
||||
var warningTextColor video.Color
|
||||
|
||||
var darkInfo = video.Color{R: 0.04, G: 0.36, B: 0.46, A: 1}
|
||||
var lightInfo = video.Color{R: 0.53, G: 0.89, B: 1.00, A: 1}
|
||||
var tabHexaColors func(int) video.Color
|
||||
var tabIconColors func(int) video.Color
|
||||
|
||||
var darkSuccess = video.Color{R: 0.15, G: 0.46, B: 0.04, A: 1}
|
||||
var lightSuccess = video.Color{R: 0.65, G: 1.00, B: 0.53, A: 1}
|
||||
func applyDefaultLightTheme() {
|
||||
var pal = map[string]video.Color{
|
||||
"background": video.ColorFromHex("#FFFFFF"),
|
||||
"foreground": video.ColorFromHex("#000000"),
|
||||
"selection": video.ColorFromHex("#dfffff"),
|
||||
"lightgrey": video.ColorFromHex("#cccccc"),
|
||||
"mediumgrey": video.ColorFromHex("#888888"),
|
||||
"darkgrey": video.ColorFromHex("#555555"),
|
||||
"darkergrey": video.ColorFromHex("#222222"),
|
||||
"infobg": video.ColorFromHex("#0a5b75"),
|
||||
"infotext": video.ColorFromHex("#87e2ff"),
|
||||
"successbg": video.ColorFromHex("#26750a"),
|
||||
"successtext": video.ColorFromHex("#a5ff87"),
|
||||
"dangerbg": video.ColorFromHex("#750a0a"),
|
||||
"dangertext": video.ColorFromHex("#ff8787"),
|
||||
"warningbg": video.ColorFromHex("#77660a"),
|
||||
"warningtext": video.ColorFromHex("#ffea87"),
|
||||
"orange": video.ColorFromHex("#c1784e"),
|
||||
}
|
||||
|
||||
var darkDanger = video.Color{R: 0.46, G: 0.04, B: 0.04, A: 1}
|
||||
var lightDanger = video.Color{R: 1.00, G: 0.53, B: 0.53, A: 1}
|
||||
bgColor = pal["background"]
|
||||
cursorBgColor = pal["selection"]
|
||||
textColor = pal["foreground"]
|
||||
mutedTextColor = pal["mediumgrey"]
|
||||
textShadowColor = pal["lightgrey"]
|
||||
sepColor = pal["lightgrey"]
|
||||
hintTextColor = pal["darkergrey"]
|
||||
hintBgColor = pal["background"]
|
||||
titleTextColor = pal["orange"]
|
||||
dialogTextColor = pal["darkergrey"]
|
||||
|
||||
var darkWarning = video.Color{R: 0.47, G: 0.40, B: 0.04, A: 1}
|
||||
var lightWarning = video.Color{R: 1.00, G: 0.92, B: 0.53, A: 1}
|
||||
infoBgColor = pal["infobg"]
|
||||
infoTextColor = pal["infotext"]
|
||||
successBgColor = pal["successbg"]
|
||||
successTextColor = pal["successtext"]
|
||||
dangerBgColor = pal["dangerbg"]
|
||||
dangerTextColor = pal["dangertext"]
|
||||
warningBgColor = pal["warningbg"]
|
||||
warningTextColor = pal["warningtext"]
|
||||
|
||||
var bgColor = white
|
||||
var cursorBg = cyan
|
||||
var textColor = black
|
||||
tabHexaColors = func(i int) video.Color {
|
||||
cf := colorful.Hcl(float64(i)*20, 0.5, 0.5)
|
||||
return video.Color{R: float32(cf.R), G: float32(cf.B), B: float32(cf.G), A: 1}
|
||||
}
|
||||
|
||||
tabIconColors = func(i int) video.Color {
|
||||
return pal["background"]
|
||||
}
|
||||
}
|
||||
|
||||
func applyDefaultDarkTheme() {
|
||||
var pal = map[string]video.Color{
|
||||
"background": video.ColorFromHex("#000000"),
|
||||
"foreground": video.ColorFromHex("#FFFFFF"),
|
||||
"selection": video.ColorFromHex("#dfffff"),
|
||||
"lightgrey": video.ColorFromHex("#cccccc"),
|
||||
"mediumgrey": video.ColorFromHex("#888888"),
|
||||
"darkgrey": video.ColorFromHex("#555555"),
|
||||
"darkergrey": video.ColorFromHex("#222222"),
|
||||
"infobg": video.ColorFromHex("#0a5b75"),
|
||||
"infotext": video.ColorFromHex("#87e2ff"),
|
||||
"successbg": video.ColorFromHex("#26750a"),
|
||||
"successtext": video.ColorFromHex("#a5ff87"),
|
||||
"dangerbg": video.ColorFromHex("#750a0a"),
|
||||
"dangertext": video.ColorFromHex("#ff8787"),
|
||||
"warningbg": video.ColorFromHex("#77660a"),
|
||||
"warningtext": video.ColorFromHex("#ffea87"),
|
||||
"orange": video.ColorFromHex("#c1784e"),
|
||||
}
|
||||
|
||||
bgColor = pal["background"]
|
||||
cursorBgColor = pal["darkergrey"]
|
||||
textColor = pal["foreground"]
|
||||
mutedTextColor = pal["mediumgrey"]
|
||||
textShadowColor = pal["darkergrey"]
|
||||
sepColor = pal["darkergrey"]
|
||||
hintTextColor = pal["lightgrey"]
|
||||
hintBgColor = pal["background"]
|
||||
titleTextColor = pal["orange"]
|
||||
dialogTextColor = pal["darkergrey"]
|
||||
|
||||
infoBgColor = pal["infobg"]
|
||||
infoTextColor = pal["infotext"]
|
||||
successBgColor = pal["successbg"]
|
||||
successTextColor = pal["successtext"]
|
||||
dangerBgColor = pal["dangerbg"]
|
||||
dangerTextColor = pal["dangertext"]
|
||||
warningBgColor = pal["warningbg"]
|
||||
warningTextColor = pal["warningtext"]
|
||||
|
||||
tabHexaColors = func(i int) video.Color {
|
||||
cf := colorful.Hcl(float64(i)*20, 0.5, 0.5)
|
||||
return video.Color{R: float32(cf.R), G: float32(cf.B), B: float32(cf.G), A: 1}
|
||||
}
|
||||
|
||||
tabIconColors = func(_ int) video.Color {
|
||||
return pal["darkergrey"]
|
||||
}
|
||||
}
|
||||
|
||||
func applyAlucardTheme() {
|
||||
var dracula = map[string]video.Color{
|
||||
"background": video.ColorFromHex("#F5F5F5"),
|
||||
"foreground": video.ColorFromHex("#1F1F1F"),
|
||||
"selection": video.ColorFromHex("#EBEBEF"),
|
||||
"comment": video.ColorFromHex("#635D97"),
|
||||
"red": video.ColorFromHex("#CB3A2A"),
|
||||
"orange": video.ColorFromHex("#A34D13"),
|
||||
"yellow": video.ColorFromHex("#836E15"),
|
||||
"green": video.ColorFromHex("#16710B"),
|
||||
"purple": video.ColorFromHex("#635D97"),
|
||||
"cyan": video.ColorFromHex("#036A96"),
|
||||
"pink": video.ColorFromHex("#A3134D"),
|
||||
}
|
||||
|
||||
bgColor = dracula["background"]
|
||||
cursorBgColor = dracula["selection"]
|
||||
textColor = dracula["foreground"]
|
||||
mutedTextColor = dracula["purple"]
|
||||
textShadowColor = dracula["selection"]
|
||||
sepColor = dracula["selection"]
|
||||
hintTextColor = dracula["foreground"]
|
||||
hintBgColor = dracula["background"]
|
||||
titleTextColor = dracula["purple"]
|
||||
dialogTextColor = dracula["background"]
|
||||
|
||||
infoBgColor = dracula["selection"]
|
||||
infoTextColor = dracula["purple"]
|
||||
successBgColor = dracula["selection"]
|
||||
successTextColor = dracula["green"]
|
||||
dangerBgColor = dracula["selection"]
|
||||
dangerTextColor = dracula["red"]
|
||||
warningBgColor = dracula["selection"]
|
||||
warningTextColor = dracula["orange"]
|
||||
|
||||
tabHexaColors = func(i int) video.Color {
|
||||
return []video.Color{
|
||||
dracula["purple"],
|
||||
dracula["red"],
|
||||
dracula["pink"],
|
||||
dracula["orange"],
|
||||
dracula["green"],
|
||||
dracula["cyan"],
|
||||
}[i%6]
|
||||
}
|
||||
|
||||
tabIconColors = func(_ int) video.Color {
|
||||
return dracula["selection"]
|
||||
}
|
||||
}
|
||||
|
||||
func applyDraculaTheme() {
|
||||
var dracula = map[string]video.Color{
|
||||
"background": video.ColorFromHex("#282A36"),
|
||||
"foreground": video.ColorFromHex("#F8F8F2"),
|
||||
"selection": video.ColorFromHex("#44475A"),
|
||||
"comment": video.ColorFromHex("#6272A4"),
|
||||
"red": video.ColorFromHex("#FF5555"),
|
||||
"orange": video.ColorFromHex("#FFB86C"),
|
||||
"yellow": video.ColorFromHex("#F1FA8C"),
|
||||
"green": video.ColorFromHex("#50FA7B"),
|
||||
"purple": video.ColorFromHex("#BD93F9"),
|
||||
"cyan": video.ColorFromHex("#8BE9FD"),
|
||||
"pink": video.ColorFromHex("#FF79C6"),
|
||||
}
|
||||
|
||||
bgColor = dracula["background"]
|
||||
cursorBgColor = dracula["selection"]
|
||||
textColor = dracula["foreground"]
|
||||
mutedTextColor = dracula["purple"]
|
||||
textShadowColor = dracula["selection"]
|
||||
sepColor = dracula["selection"]
|
||||
hintTextColor = dracula["foreground"]
|
||||
hintBgColor = dracula["background"]
|
||||
titleTextColor = dracula["purple"]
|
||||
dialogTextColor = dracula["background"]
|
||||
|
||||
infoBgColor = dracula["selection"]
|
||||
infoTextColor = dracula["purple"]
|
||||
successBgColor = dracula["selection"]
|
||||
successTextColor = dracula["green"]
|
||||
dangerBgColor = dracula["selection"]
|
||||
dangerTextColor = dracula["red"]
|
||||
warningBgColor = dracula["selection"]
|
||||
warningTextColor = dracula["orange"]
|
||||
|
||||
tabHexaColors = func(i int) video.Color {
|
||||
return []video.Color{
|
||||
dracula["purple"],
|
||||
dracula["red"],
|
||||
dracula["pink"],
|
||||
dracula["orange"],
|
||||
dracula["green"],
|
||||
dracula["cyan"],
|
||||
}[i%6]
|
||||
}
|
||||
|
||||
tabIconColors = func(_ int) video.Color {
|
||||
return dracula["selection"]
|
||||
}
|
||||
}
|
||||
|
||||
// Catpuccin color palette
|
||||
func applyCatpuccinMochaTheme() {
|
||||
var mocha = map[string]video.Color{
|
||||
"rosewater": video.ColorFromHex("#f5e0dc"),
|
||||
"flamingo": video.ColorFromHex("#f2cdcd"),
|
||||
"pink": video.ColorFromHex("#f5c2e7"),
|
||||
"mauve": video.ColorFromHex("#cba6f7"),
|
||||
"red": video.ColorFromHex("#f38ba8"),
|
||||
"maroon": video.ColorFromHex("#eba0ac"),
|
||||
"peach": video.ColorFromHex("#fab387"),
|
||||
"yellow": video.ColorFromHex("#f9e2af"),
|
||||
"green": video.ColorFromHex("#a6e3a1"),
|
||||
"teal": video.ColorFromHex("#94e2d5"),
|
||||
"sky": video.ColorFromHex("#89dceb"),
|
||||
"sapphire": video.ColorFromHex("#74c7ec"),
|
||||
"blue": video.ColorFromHex("#89b4fa"),
|
||||
"lavender": video.ColorFromHex("#b4befe"),
|
||||
"text": video.ColorFromHex("#cdd6f4"),
|
||||
"subtext1": video.ColorFromHex("#bac2de"),
|
||||
"subtext0": video.ColorFromHex("#a6adc8"),
|
||||
"overlay2": video.ColorFromHex("#9399b2"),
|
||||
"overlay1": video.ColorFromHex("#7f849c"),
|
||||
"overlay0": video.ColorFromHex("#6c7086"),
|
||||
"surface2": video.ColorFromHex("#585b70"),
|
||||
"surface1": video.ColorFromHex("#45475a"),
|
||||
"surface0": video.ColorFromHex("#313244"),
|
||||
"base": video.ColorFromHex("#1e1e2e"),
|
||||
"mantle": video.ColorFromHex("#181825"),
|
||||
"crust": video.ColorFromHex("#11111b"),
|
||||
}
|
||||
|
||||
bgColor = mocha["base"]
|
||||
cursorBgColor = mocha["surface0"]
|
||||
textColor = mocha["text"]
|
||||
mutedTextColor = mocha["subtext0"]
|
||||
textShadowColor = mocha["surface0"]
|
||||
sepColor = mocha["surface1"]
|
||||
hintTextColor = mocha["text"]
|
||||
hintBgColor = mocha["base"]
|
||||
titleTextColor = mocha["mauve"]
|
||||
dialogTextColor = mocha["base"]
|
||||
|
||||
infoBgColor = mocha["sky"]
|
||||
infoTextColor = mocha["base"]
|
||||
successBgColor = mocha["green"]
|
||||
successTextColor = mocha["base"]
|
||||
dangerBgColor = mocha["red"]
|
||||
dangerTextColor = mocha["base"]
|
||||
warningBgColor = mocha["yellow"]
|
||||
warningTextColor = mocha["base"]
|
||||
|
||||
tabHexaColors = func(i int) video.Color {
|
||||
return []video.Color{
|
||||
mocha["mauve"],
|
||||
mocha["blue"],
|
||||
mocha["lavender"],
|
||||
mocha["sky"],
|
||||
mocha["sapphire"],
|
||||
mocha["teal"],
|
||||
mocha["green"],
|
||||
mocha["yellow"],
|
||||
mocha["peach"],
|
||||
mocha["red"],
|
||||
mocha["maroon"],
|
||||
mocha["flamingo"],
|
||||
mocha["pink"],
|
||||
mocha["rosewater"],
|
||||
}[i%13]
|
||||
}
|
||||
|
||||
tabIconColors = func(_ int) video.Color {
|
||||
return mocha["surface0"]
|
||||
}
|
||||
}
|
||||
|
||||
// Catppuccin Latte color palette
|
||||
func applyCatpuccinLatteTheme() {
|
||||
var latte = map[string]video.Color{
|
||||
"rosewater": video.ColorFromHex("#dc8a78"),
|
||||
"flamingo": video.ColorFromHex("#dd7878"),
|
||||
"pink": video.ColorFromHex("#ea76cb"),
|
||||
"mauve": video.ColorFromHex("#8839ef"),
|
||||
"red": video.ColorFromHex("#d20f39"),
|
||||
"maroon": video.ColorFromHex("#e64553"),
|
||||
"peach": video.ColorFromHex("#fe640b"),
|
||||
"yellow": video.ColorFromHex("#df8e1d"),
|
||||
"green": video.ColorFromHex("#40a02b"),
|
||||
"teal": video.ColorFromHex("#179299"),
|
||||
"sky": video.ColorFromHex("#04a5e5"),
|
||||
"sapphire": video.ColorFromHex("#209fb5"),
|
||||
"blue": video.ColorFromHex("#1e66f5"),
|
||||
"lavender": video.ColorFromHex("#7287fd"),
|
||||
"text": video.ColorFromHex("#4c4f69"),
|
||||
"subtext1": video.ColorFromHex("#5c5f77"),
|
||||
"subtext0": video.ColorFromHex("#6c6f85"),
|
||||
"overlay2": video.ColorFromHex("#7c7f93"),
|
||||
"overlay1": video.ColorFromHex("#8c8fa1"),
|
||||
"overlay0": video.ColorFromHex("#9ca0b0"),
|
||||
"surface2": video.ColorFromHex("#acb0be"),
|
||||
"surface1": video.ColorFromHex("#bcc0cc"),
|
||||
"surface0": video.ColorFromHex("#ccd0da"),
|
||||
"base": video.ColorFromHex("#eff1f5"),
|
||||
"mantle": video.ColorFromHex("#ebe9f1"),
|
||||
"crust": video.ColorFromHex("#f5f3f5"),
|
||||
}
|
||||
|
||||
bgColor = latte["base"]
|
||||
cursorBgColor = latte["mantle"]
|
||||
textColor = latte["text"]
|
||||
mutedTextColor = latte["subtext0"]
|
||||
textShadowColor = latte["surface0"]
|
||||
sepColor = latte["surface0"]
|
||||
hintTextColor = latte["text"]
|
||||
hintBgColor = latte["base"]
|
||||
titleTextColor = latte["mauve"]
|
||||
dialogTextColor = latte["base"]
|
||||
infoBgColor = latte["sky"]
|
||||
infoTextColor = latte["base"]
|
||||
successBgColor = latte["green"]
|
||||
successTextColor = latte["base"]
|
||||
dangerBgColor = latte["red"]
|
||||
dangerTextColor = latte["base"]
|
||||
warningBgColor = latte["yellow"]
|
||||
warningTextColor = latte["base"]
|
||||
|
||||
tabHexaColors = func(i int) video.Color {
|
||||
return []video.Color{
|
||||
latte["mauve"],
|
||||
latte["blue"],
|
||||
latte["lavender"],
|
||||
latte["sky"],
|
||||
latte["sapphire"],
|
||||
latte["teal"],
|
||||
latte["green"],
|
||||
latte["yellow"],
|
||||
latte["peach"],
|
||||
latte["red"],
|
||||
latte["maroon"],
|
||||
latte["flamingo"],
|
||||
latte["pink"],
|
||||
latte["rosewater"],
|
||||
}[i%13]
|
||||
}
|
||||
|
||||
tabIconColors = func(_ int) video.Color {
|
||||
return latte["base"]
|
||||
}
|
||||
}
|
||||
|
||||
// rosePine color palette
|
||||
// This is the dark theme color palette used in the menu
|
||||
func applyRosePineTheme() {
|
||||
var rosePine = map[string]video.Color{
|
||||
"base": video.ColorFromHex("#191724"),
|
||||
"text": video.ColorFromHex("#e0def4"),
|
||||
"overlay": video.ColorFromHex("#26233a"),
|
||||
"muted": video.ColorFromHex("#6e6a86"),
|
||||
"love": video.ColorFromHex("#eb6f92"),
|
||||
"gold": video.ColorFromHex("#f6c177"),
|
||||
"rose": video.ColorFromHex("#ebbcba"),
|
||||
"pine": video.ColorFromHex("#31748f"),
|
||||
"foam": video.ColorFromHex("#9ccfd8"),
|
||||
"iris": video.ColorFromHex("#c4a7e7"),
|
||||
"highlightMed": video.ColorFromHex("#403d52"),
|
||||
}
|
||||
|
||||
bgColor = rosePine["base"]
|
||||
cursorBgColor = rosePine["overlay"]
|
||||
textColor = rosePine["text"]
|
||||
mutedTextColor = rosePine["muted"]
|
||||
textShadowColor = rosePine["overlay"]
|
||||
sepColor = rosePine["highlightMed"]
|
||||
hintTextColor = rosePine["text"]
|
||||
hintBgColor = rosePine["base"]
|
||||
titleTextColor = rosePine["pine"]
|
||||
dialogTextColor = rosePine["base"]
|
||||
|
||||
infoBgColor = rosePine["iris"]
|
||||
infoTextColor = rosePine["base"]
|
||||
successBgColor = rosePine["foam"]
|
||||
successTextColor = rosePine["base"]
|
||||
dangerBgColor = rosePine["love"]
|
||||
dangerTextColor = rosePine["base"]
|
||||
warningBgColor = rosePine["gold"]
|
||||
warningTextColor = rosePine["base"]
|
||||
|
||||
tabHexaColors = func(i int) video.Color {
|
||||
return[]video.Color{
|
||||
rosePine["iris"],
|
||||
rosePine["love"],
|
||||
rosePine["rose"],
|
||||
rosePine["gold"],
|
||||
rosePine["foam"],
|
||||
rosePine["pine"],
|
||||
}[i%6]
|
||||
}
|
||||
|
||||
tabIconColors = func(_ int) video.Color {
|
||||
return rosePine["overlay"]
|
||||
}
|
||||
}
|
||||
|
||||
// rosePineDawn color palette
|
||||
// This is the light theme color palette used in the menu
|
||||
func applyRosePineDawnTheme() {
|
||||
var rosePineDawn = map[string]video.Color{
|
||||
"base": video.ColorFromHex("#faf4ed"),
|
||||
"text": video.ColorFromHex("#575279"),
|
||||
"overlay": video.ColorFromHex("#f2e9e1"),
|
||||
"muted": video.ColorFromHex("#9893a5"),
|
||||
"love": video.ColorFromHex("#b4637a"),
|
||||
"gold": video.ColorFromHex("#ea9d34"),
|
||||
"rose": video.ColorFromHex("#d7827e"),
|
||||
"pine": video.ColorFromHex("#286983"),
|
||||
"foam": video.ColorFromHex("#56949f"),
|
||||
"iris": video.ColorFromHex("#907aa9"),
|
||||
"highlightMed": video.ColorFromHex("#dfdad9"),
|
||||
}
|
||||
|
||||
bgColor = rosePineDawn["base"]
|
||||
cursorBgColor = rosePineDawn["overlay"]
|
||||
textColor = rosePineDawn["text"]
|
||||
mutedTextColor = rosePineDawn["muted"]
|
||||
textShadowColor = rosePineDawn["overlay"]
|
||||
sepColor = rosePineDawn["highlightMed"]
|
||||
hintTextColor = rosePineDawn["text"]
|
||||
hintBgColor = rosePineDawn["base"]
|
||||
titleTextColor = rosePineDawn["foam"]
|
||||
dialogTextColor = rosePineDawn["text"]
|
||||
|
||||
infoBgColor = rosePineDawn["iris"]
|
||||
infoTextColor = rosePineDawn["base"]
|
||||
successBgColor = rosePineDawn["foam"]
|
||||
successTextColor = rosePineDawn["base"]
|
||||
dangerBgColor = rosePineDawn["love"]
|
||||
dangerTextColor = rosePineDawn["base"]
|
||||
warningBgColor = rosePineDawn["gold"]
|
||||
warningTextColor = rosePineDawn["base"]
|
||||
|
||||
tabHexaColors = func(i int) video.Color {
|
||||
return []video.Color{
|
||||
rosePineDawn["iris"],
|
||||
rosePineDawn["love"],
|
||||
rosePineDawn["rose"],
|
||||
rosePineDawn["gold"],
|
||||
rosePineDawn["foam"],
|
||||
rosePineDawn["pine"],
|
||||
}[i%6]
|
||||
}
|
||||
|
||||
tabIconColors = func(_ int) video.Color {
|
||||
return rosePineDawn["base"]
|
||||
}
|
||||
}
|
||||
|
||||
// UpdatePalette updates the color palette to honor the dark theme
|
||||
func (m *Menu) UpdatePalette() {
|
||||
bgColor = white
|
||||
cursorBg = cyan
|
||||
textColor = black
|
||||
|
||||
if state.CoreRunning || settings.Current.VideoDarkMode {
|
||||
bgColor = darkerGrey
|
||||
cursorBg = darkBlue
|
||||
textColor = white
|
||||
switch settings.Current.VideoTheme {
|
||||
case "Rose Pine": applyRosePineTheme()
|
||||
case "Dracula": applyDraculaTheme()
|
||||
case "Catppuccin": applyCatpuccinMochaTheme()
|
||||
default: applyDefaultDarkTheme()
|
||||
}
|
||||
} else {
|
||||
switch settings.Current.VideoTheme {
|
||||
case "Rose Pine": applyRosePineDawnTheme()
|
||||
case "Dracula": applyAlucardTheme() // Alucard is a light variant of Dracula
|
||||
case "Catppuccin": applyCatpuccinLatteTheme()
|
||||
default: applyDefaultLightTheme()
|
||||
}
|
||||
}
|
||||
|
||||
severityFgColor = map[ntf.Severity]video.Color{
|
||||
ntf.Error: dangerTextColor,
|
||||
ntf.Warning: warningTextColor,
|
||||
ntf.Success: successTextColor,
|
||||
ntf.Info: infoTextColor,
|
||||
}
|
||||
|
||||
severityBgColor = map[ntf.Severity]video.Color{
|
||||
ntf.Error: dangerBgColor,
|
||||
ntf.Warning: warningBgColor,
|
||||
ntf.Success: successBgColor,
|
||||
ntf.Info: infoBgColor,
|
||||
}
|
||||
}
|
||||
+32
-18
@@ -35,7 +35,7 @@ type entry struct {
|
||||
children []entry // children entries
|
||||
ptr int // index of the active child
|
||||
indexes []struct {
|
||||
Char byte
|
||||
Char rune
|
||||
Index int
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,13 @@ type Scene interface {
|
||||
Entry() *entry
|
||||
}
|
||||
|
||||
func firstRune(s string) rune {
|
||||
for _, r := range s {
|
||||
return r
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// genericSegueMount is the smooth transition of the menu entries first appearance
|
||||
func genericSegueMount(list *entry) {
|
||||
for i := range list.children {
|
||||
@@ -153,10 +160,10 @@ func genericDrawCursor(list *entry) {
|
||||
w, h := menu.GetFramebufferSize()
|
||||
menu.DrawImage(menu.icons["arrow"],
|
||||
530*menu.ratio, float32(h)*list.cursor.yp-35*menu.ratio,
|
||||
70*menu.ratio, 70*menu.ratio, 1, cursorBg.Alpha(list.cursor.alpha))
|
||||
70*menu.ratio, 70*menu.ratio, 1, 0, cursorBgColor.Alpha(list.cursor.alpha))
|
||||
menu.DrawRect(
|
||||
550*menu.ratio, float32(h)*list.cursor.yp-50*menu.ratio,
|
||||
float32(w)-630*menu.ratio, 100*menu.ratio, 1, cursorBg.Alpha(list.cursor.alpha))
|
||||
float32(w)-630*menu.ratio, 100*menu.ratio, 1, cursorBgColor.Alpha(list.cursor.alpha))
|
||||
}
|
||||
|
||||
// thumbnailDrawCursor draws the blinking rectangular background of the active
|
||||
@@ -165,10 +172,10 @@ func thumbnailDrawCursor(list *entry) {
|
||||
w, h := menu.GetFramebufferSize()
|
||||
menu.DrawImage(menu.icons["arrow"],
|
||||
500*menu.ratio, float32(h)*list.cursor.yp-50*menu.ratio,
|
||||
100*menu.ratio, 100*menu.ratio, 1, cursorBg.Alpha(list.cursor.alpha))
|
||||
100*menu.ratio, 100*menu.ratio, 1, 0, cursorBgColor.Alpha(list.cursor.alpha))
|
||||
menu.DrawRect(
|
||||
530*menu.ratio, float32(h)*list.cursor.yp-120*menu.ratio,
|
||||
float32(w)-630*menu.ratio, 240*menu.ratio, 0.2, cursorBg.Alpha(list.cursor.alpha))
|
||||
float32(w)-630*menu.ratio, 240*menu.ratio, 0.2, cursorBgColor.Alpha(list.cursor.alpha))
|
||||
}
|
||||
|
||||
// genericRender renders a vertical list of menu entries
|
||||
@@ -191,23 +198,28 @@ func genericRender(list *entry) {
|
||||
610*menu.ratio-64*0.5*menu.ratio,
|
||||
float32(h)*e.yp-14*menu.ratio-64*0.5*menu.ratio+fontOffset,
|
||||
128*menu.ratio, 128*menu.ratio,
|
||||
0.5, textColor.Alpha(e.iconAlpha))
|
||||
0.5, 0, textColor.Alpha(e.iconAlpha))
|
||||
|
||||
if e.labelAlpha > 0 {
|
||||
menu.Font.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
|
||||
menu.Font.Print(
|
||||
(670+1)*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset+1*menu.ratio,
|
||||
menu.ratio, e.label)
|
||||
menu.Font.SetColor(textColor.Alpha(e.labelAlpha))
|
||||
menu.Font.Printf(
|
||||
menu.Font.Print(
|
||||
670*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset,
|
||||
0.5*menu.ratio, e.label)
|
||||
menu.ratio, e.label)
|
||||
|
||||
if e.widget != nil {
|
||||
e.widget(&e)
|
||||
} else if e.stringValue != nil {
|
||||
lw := menu.Font.Width(0.5*menu.ratio, e.stringValue())
|
||||
menu.Font.Printf(
|
||||
lw := menu.Font.Width(menu.ratio, e.stringValue())
|
||||
menu.Font.Print(
|
||||
float32(w)-lw-128*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset,
|
||||
0.5*menu.ratio, e.stringValue())
|
||||
menu.ratio, e.stringValue())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -263,16 +275,18 @@ func askDeleteSavestateConfirmation(cb func()) {
|
||||
}
|
||||
|
||||
func genericDrawHintBar() {
|
||||
w, h := menu.GetFramebufferSize()
|
||||
menu.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, lightGrey)
|
||||
w, h := menu.Window.GetFramebufferSize()
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
|
||||
|
||||
_, upDown, _, a, b, _, _, _, _, guide := hintIcons()
|
||||
|
||||
var stack float32
|
||||
lstack := float32(75) * menu.ratio
|
||||
rstack := float32(w) - 96*menu.ratio
|
||||
stackHintLeft(&lstack, upDown, "Navigate", h)
|
||||
stackHintRight(&rstack, a, "Ok", h)
|
||||
stackHintRight(&rstack, b, "Back", h)
|
||||
if state.CoreRunning {
|
||||
stackHint(&stack, guide, "RESUME", h)
|
||||
stackHintRight(&rstack, guide, "Resume", h)
|
||||
}
|
||||
stackHint(&stack, upDown, "NAVIGATE", h)
|
||||
stackHint(&stack, b, "BACK", h)
|
||||
stackHint(&stack, a, "OK", h)
|
||||
}
|
||||
@@ -33,7 +33,7 @@ func buildCoreDiskControl() Scene {
|
||||
state.Core.DiskControlCallback.SetEjectState(true)
|
||||
state.Core.DiskControlCallback.SetImageIndex(index)
|
||||
state.Core.DiskControlCallback.SetEjectState(false)
|
||||
ntf.DisplayAndLog(ntf.Success, "Menu", "Switched to disk %d.", index+1)
|
||||
ntf.DisplayAndLogf(ntf.Success, "Menu", "Switched to disk %d.", index+1)
|
||||
state.MenuActive = false
|
||||
},
|
||||
})
|
||||
@@ -77,15 +77,23 @@ func (s *sceneCoreDiskControl) render() {
|
||||
|
||||
func (s *sceneCoreDiskControl) drawHintBar() {
|
||||
w, h := menu.GetFramebufferSize()
|
||||
menu.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, lightGrey)
|
||||
|
||||
_, upDown, leftRight, _, b, _, _, _, _, guide := hintIcons()
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
|
||||
|
||||
var stack float32
|
||||
_, upDown, leftRight, a, b, _, _, _, _, guide := hintIcons()
|
||||
|
||||
lstack := float32(75) * menu.ratio
|
||||
rstack := float32(w) - 96*menu.ratio
|
||||
list := menu.stack[len(menu.stack)-1].Entry()
|
||||
stackHintLeft(&lstack, upDown, "Navigate", h)
|
||||
if list.children[list.ptr].callbackOK != nil {
|
||||
stackHintRight(&rstack, a, "Set", h)
|
||||
} else {
|
||||
stackHintLeft(&lstack, leftRight, "Set", h)
|
||||
}
|
||||
stackHintRight(&rstack, b, "Back", h)
|
||||
if state.CoreRunning {
|
||||
stackHint(&stack, guide, "RESUME", h)
|
||||
stackHintRight(&rstack, guide, "Resume", h)
|
||||
}
|
||||
stackHint(&stack, upDown, "NAVIGATE", h)
|
||||
stackHint(&stack, b, "BACK", h)
|
||||
stackHint(&stack, leftRight, "SET", h)
|
||||
}
|
||||
+12
-10
@@ -16,7 +16,7 @@ func buildCoreOptions() Scene {
|
||||
var list sceneCoreOptions
|
||||
list.label = "Core Options"
|
||||
|
||||
if core.Options == nil {
|
||||
if core.Options == nil || len(core.Options.Vars) == 0 {
|
||||
list.children = append(list.children, entry{
|
||||
label: "No options",
|
||||
icon: "subsetting",
|
||||
@@ -28,11 +28,11 @@ func buildCoreOptions() Scene {
|
||||
for _, v := range core.Options.Vars {
|
||||
v := v
|
||||
list.children = append(list.children, entry{
|
||||
label: strings.Replace(v.Desc, "%", "%%", -1),
|
||||
label: strings.ReplaceAll(v.Desc, "%", "%%"),
|
||||
icon: "subsetting",
|
||||
stringValue: func() string {
|
||||
val := v.Choices[v.Choice]
|
||||
return strings.Replace(val, "%", "%%", -1)
|
||||
return "◀ " + strings.ReplaceAll(val, "%", "%%") + " ▶"
|
||||
},
|
||||
incr: func(direction int) {
|
||||
v.Choice += direction
|
||||
@@ -44,7 +44,7 @@ func buildCoreOptions() Scene {
|
||||
core.Options.Updated = true
|
||||
err := core.Options.Save()
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Core", "Error saving core options: %v", err.Error())
|
||||
ntf.DisplayAndLogf(ntf.Error, "Core", "Error saving core options: %v", err)
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -81,15 +81,17 @@ func (s *sceneCoreOptions) render() {
|
||||
|
||||
func (s *sceneCoreOptions) drawHintBar() {
|
||||
w, h := menu.GetFramebufferSize()
|
||||
menu.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, lightGrey)
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
|
||||
|
||||
_, upDown, leftRight, _, b, _, _, _, _, guide := hintIcons()
|
||||
|
||||
var stack float32
|
||||
lstack := float32(75) * menu.ratio
|
||||
rstack := float32(w) - 96*menu.ratio
|
||||
stackHintLeft(&lstack, upDown, "Navigate", h)
|
||||
stackHintRight(&rstack, leftRight, "Set", h)
|
||||
stackHintRight(&rstack, b, "Back", h)
|
||||
if state.CoreRunning {
|
||||
stackHint(&stack, guide, "RESUME", h)
|
||||
stackHintRight(&rstack, guide, "Resume", h)
|
||||
}
|
||||
stackHint(&stack, upDown, "NAVIGATE", h)
|
||||
stackHint(&stack, b, "BACK", h)
|
||||
stackHint(&stack, leftRight, "SET", h)
|
||||
}
|
||||
+15
-15
@@ -70,16 +70,16 @@ func (s *sceneDialog) render() {
|
||||
white,
|
||||
)
|
||||
|
||||
menu.Font.SetColor(orange)
|
||||
lw1 := menu.Font.Width(0.7*menu.ratio, s.title)
|
||||
menu.Font.Printf(fw/2-lw1/2, fh/2-120*menu.ratio+20*menu.ratio, 0.7*menu.ratio, s.title)
|
||||
menu.Font.SetColor(black)
|
||||
lw2 := menu.Font.Width(0.5*menu.ratio, s.line1)
|
||||
menu.Font.Printf(fw/2-lw2/2, fh/2-30*menu.ratio+20*menu.ratio, 0.5*menu.ratio, s.line1)
|
||||
lw3 := menu.Font.Width(0.5*menu.ratio, s.line2)
|
||||
menu.Font.Printf(fw/2-lw3/2, fh/2+30*menu.ratio+20*menu.ratio, 0.5*menu.ratio, s.line2)
|
||||
menu.FontLg.SetColor(titleTextColor)
|
||||
lw1 := menu.FontLg.Width(menu.ratio, s.title)
|
||||
menu.FontLg.Print(fw/2-lw1/2, fh/2-120*menu.ratio+20*menu.ratio, menu.ratio, s.title)
|
||||
menu.Font.SetColor(dialogTextColor)
|
||||
lw2 := menu.Font.Width(menu.ratio, s.line1)
|
||||
menu.Font.Print(fw/2-lw2/2, fh/2-30*menu.ratio+20*menu.ratio, menu.ratio, s.line1)
|
||||
lw3 := menu.Font.Width(menu.ratio, s.line2)
|
||||
menu.Font.Print(fw/2-lw3/2, fh/2+30*menu.ratio+20*menu.ratio, menu.ratio, s.line2)
|
||||
|
||||
menu.Font.SetColor(darkGrey)
|
||||
menu.FontSm.SetColor(dialogTextColor)
|
||||
|
||||
var margin float32 = 15
|
||||
|
||||
@@ -89,22 +89,22 @@ func (s *sceneDialog) render() {
|
||||
b,
|
||||
fw/2-width/2*menu.ratio+margin*menu.ratio,
|
||||
fh/2+height/2*menu.ratio-70*menu.ratio-margin*menu.ratio,
|
||||
70*menu.ratio, 70*menu.ratio, 1.0, darkGrey)
|
||||
menu.Font.Printf(
|
||||
70*menu.ratio, 70*menu.ratio, 1.0, 0, dialogTextColor)
|
||||
menu.FontSm.Print(
|
||||
fw/2-width/2*menu.ratio+margin*menu.ratio+70*menu.ratio,
|
||||
fh/2+height/2*menu.ratio-23*menu.ratio-margin*menu.ratio,
|
||||
0.4*menu.ratio,
|
||||
menu.ratio,
|
||||
"NO")
|
||||
|
||||
menu.DrawImage(
|
||||
a,
|
||||
fw/2+width/2*menu.ratio-150*menu.ratio-margin*menu.ratio,
|
||||
fh/2+height/2*menu.ratio-70*menu.ratio-margin*menu.ratio,
|
||||
70*menu.ratio, 70*menu.ratio, 1.0, darkGrey)
|
||||
menu.Font.Printf(
|
||||
70*menu.ratio, 70*menu.ratio, 1.0, 0, dialogTextColor)
|
||||
menu.FontSm.Print(
|
||||
fw/2+width/2*menu.ratio-150*menu.ratio-margin*menu.ratio+70*menu.ratio,
|
||||
fh/2+height/2*menu.ratio-23*menu.ratio-margin*menu.ratio,
|
||||
0.4*menu.ratio,
|
||||
menu.ratio,
|
||||
"YES")
|
||||
}
|
||||
|
||||
|
||||
+14
-3
@@ -1,12 +1,13 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/settings"
|
||||
@@ -74,7 +75,7 @@ func explorerIcon(f os.FileInfo) string {
|
||||
|
||||
func appendNode(list *sceneExplorer, fullPath string, name string, f os.FileInfo, exts []string, cb func(string), dirAction *entry, prettifier Prettifier) {
|
||||
// Check whether or not we are to display hidden files.
|
||||
if name[:1] == "." && !settings.Current.ShowHiddenFiles {
|
||||
if strings.HasPrefix(name, ".") && !settings.Current.ShowHiddenFiles {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -135,7 +136,16 @@ func buildExplorer(path string, exts []string, cb func(string), dirAction *entry
|
||||
appendFolder(&list, "..", filepath.Clean(path+"/.."), exts, cb, dirAction, prettifier)
|
||||
}
|
||||
|
||||
files, err := ioutil.ReadDir(path)
|
||||
files, err := os.ReadDir(path)
|
||||
|
||||
// Sort entries by their labels, ignoring case.
|
||||
sort.SliceStable(files, func(i, j int) bool {
|
||||
if prettifier != nil {
|
||||
return strings.ToLower(prettifier(utils.FileName(files[i].Name()))) < strings.ToLower(prettifier(utils.FileName(files[j].Name())))
|
||||
}
|
||||
return strings.ToLower(utils.FileName(files[i].Name())) < strings.ToLower(utils.FileName(files[j].Name()))
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
}
|
||||
@@ -155,6 +165,7 @@ func buildExplorer(path string, exts []string, cb func(string), dirAction *entry
|
||||
}
|
||||
appendNode(&list, fullPath, f.Name(), fi, exts, cb, dirAction, prettifier)
|
||||
}
|
||||
|
||||
buildIndexes(&list.entry)
|
||||
|
||||
if len(files) == 0 {
|
||||
|
||||
+27
-19
@@ -52,7 +52,7 @@ func loadHistoryEntry(list Scene, game history.Game) {
|
||||
}
|
||||
corePath := game.CorePath
|
||||
if _, err := os.Stat(corePath); os.IsNotExist(err) {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", "Core not found: %s", filepath.Base(corePath))
|
||||
ntf.DisplayAndLogf(ntf.Error, "Menu", "Core not found: %s", filepath.Base(corePath))
|
||||
return
|
||||
}
|
||||
if state.CorePath != corePath {
|
||||
@@ -189,7 +189,7 @@ func (s *sceneHistory) render() {
|
||||
680*menu.ratio-25*e.scale*menu.ratio,
|
||||
float32(h)*e.yp-14*menu.ratio-25*e.scale*menu.ratio+fontOffset,
|
||||
50*menu.ratio, 50*menu.ratio,
|
||||
e.scale, white.Alpha(e.iconAlpha))
|
||||
e.scale, 0, white.Alpha(e.iconAlpha))
|
||||
}
|
||||
|
||||
// Offset on Y to vertically center label + sublabel if there is a sublabel
|
||||
@@ -198,13 +198,18 @@ func (s *sceneHistory) render() {
|
||||
slOffset = 30 * menu.ratio * e.subLabelAlpha
|
||||
}
|
||||
|
||||
menu.Font.SetColor(textColor.Alpha(e.labelAlpha))
|
||||
stack := 840 * menu.ratio
|
||||
menu.Font.Printf(
|
||||
menu.Font.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
|
||||
menu.Font.Print(
|
||||
(840+1)*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset-slOffset+1*menu.ratio,
|
||||
menu.ratio, e.label)
|
||||
menu.Font.SetColor(textColor.Alpha(e.labelAlpha))
|
||||
menu.Font.Print(
|
||||
840*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset-slOffset,
|
||||
0.5*menu.ratio, e.label)
|
||||
stack += float32(int(menu.Font.Width(0.5*menu.ratio, e.label)))
|
||||
menu.ratio, e.label)
|
||||
stack += float32(int(menu.Font.Width(menu.ratio, e.label)))
|
||||
stack += 10
|
||||
|
||||
for _, tag := range e.tags {
|
||||
@@ -213,18 +218,18 @@ func (s *sceneHistory) render() {
|
||||
menu.DrawImage(
|
||||
menu.icons[tag],
|
||||
stack, float32(h)*e.yp-22*menu.ratio-slOffset,
|
||||
60*menu.ratio, 44*menu.ratio, 1.0, white.Alpha(e.tagAlpha))
|
||||
60*menu.ratio, 44*menu.ratio, 1.0, 0, white.Alpha(e.tagAlpha))
|
||||
menu.DrawBorder(stack, float32(h)*e.yp-22*menu.ratio-slOffset,
|
||||
60*menu.ratio, 44*menu.ratio, 0.05/menu.ratio, black.Alpha(e.tagAlpha/4))
|
||||
stack += 60 * menu.ratio
|
||||
}
|
||||
}
|
||||
|
||||
menu.Font.SetColor(mediumGrey.Alpha(e.subLabelAlpha))
|
||||
menu.Font.Printf(
|
||||
menu.FontSm.SetColor(mutedTextColor.Alpha(e.subLabelAlpha))
|
||||
menu.FontSm.Print(
|
||||
840*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset+60*menu.ratio-slOffset,
|
||||
0.5*menu.ratio, e.subLabel)
|
||||
menu.ratio, e.subLabel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,20 +238,23 @@ func (s *sceneHistory) render() {
|
||||
|
||||
func (s *sceneHistory) drawHintBar() {
|
||||
w, h := menu.GetFramebufferSize()
|
||||
menu.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, lightGrey)
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
|
||||
|
||||
_, upDown, _, a, b, x, _, _, _, guide := hintIcons()
|
||||
|
||||
var stack float32
|
||||
if state.CoreRunning {
|
||||
stackHint(&stack, guide, "RESUME", h)
|
||||
}
|
||||
stackHint(&stack, upDown, "NAVIGATE", h)
|
||||
stackHint(&stack, b, "BACK", h)
|
||||
stackHint(&stack, a, "RUN", h)
|
||||
lstack := float32(75) * menu.ratio
|
||||
rstack := float32(w) - 96*menu.ratio
|
||||
stackHintLeft(&lstack, upDown, "Navigate", h)
|
||||
stackHintRight(&rstack, b, "Back", h)
|
||||
stackHintRight(&rstack, a, "Run", h)
|
||||
|
||||
list := menu.stack[len(menu.stack)-1].Entry()
|
||||
if list.children[list.ptr].callbackX != nil {
|
||||
stackHint(&stack, x, "DELETE", h)
|
||||
stackHintRight(&rstack, x, "Delete", h)
|
||||
}
|
||||
|
||||
if state.CoreRunning {
|
||||
stackHintRight(&rstack, guide, "Resume", h)
|
||||
}
|
||||
}
|
||||
+13
-11
@@ -162,7 +162,7 @@ func (s *sceneKeyboard) render() {
|
||||
|
||||
// Label
|
||||
menu.Font.SetColor(black)
|
||||
menu.Font.Printf(
|
||||
menu.Font.Print(
|
||||
float32(w)/2-ttw/2,
|
||||
s.y+float32(h)*0.15-ksz/2+ksz*0.6,
|
||||
ksz/260, s.label)
|
||||
@@ -170,7 +170,7 @@ func (s *sceneKeyboard) render() {
|
||||
// Value
|
||||
menu.DrawRect(float32(w)/2-ttw/2, s.y+float32(h)*0.25-ksz/2, ttw, ksz, 0,
|
||||
video.Color{R: 0.95, G: 0.95, B: 0.95, A: 1})
|
||||
menu.Font.Printf(
|
||||
menu.Font.Print(
|
||||
float32(w)/2-ttw/2+ksz/4,
|
||||
s.y+float32(h)*0.25-ksz/2+ksz*0.62,
|
||||
ksz/200, s.value+"|")
|
||||
@@ -196,7 +196,7 @@ func (s *sceneKeyboard) render() {
|
||||
menu.DrawRect(x, y, ksz, ksz, 0.2, c1)
|
||||
menu.DrawRect(x, y, ksz, ksz*0.95, 0.2, c2)
|
||||
|
||||
menu.Font.Printf(
|
||||
menu.Font.Print(
|
||||
x+ksz/2-gw/2,
|
||||
y+ksz*0.6,
|
||||
ksz/200, key)
|
||||
@@ -205,15 +205,17 @@ func (s *sceneKeyboard) render() {
|
||||
|
||||
func (s *sceneKeyboard) drawHintBar() {
|
||||
w, h := menu.GetFramebufferSize()
|
||||
menu.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, lightGrey)
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
|
||||
|
||||
arrows, _, _, a, b, x, y, start, _, _ := hintIcons()
|
||||
|
||||
var stack float32
|
||||
stackHint(&stack, arrows, "SELECT", h)
|
||||
stackHint(&stack, b, "BACK", h)
|
||||
stackHint(&stack, x, "SHIFT", h)
|
||||
stackHint(&stack, y, "DELETE", h)
|
||||
stackHint(&stack, a, "INSERT", h)
|
||||
stackHint(&stack, start, "DONE", h)
|
||||
lstack := float32(75) * menu.ratio
|
||||
rstack := float32(w) - 96*menu.ratio
|
||||
stackHintLeft(&lstack, arrows, "Select", h)
|
||||
stackHintRight(&rstack, start, "Done", h)
|
||||
stackHintRight(&rstack, a, "Insert", h)
|
||||
stackHintRight(&rstack, y, "Delete", h)
|
||||
stackHintRight(&rstack, x, "Shift", h)
|
||||
stackHintRight(&rstack, b, "Back", h)
|
||||
}
|
||||
+36
-34
@@ -18,37 +18,39 @@ type sceneMain struct {
|
||||
}
|
||||
|
||||
var prettyCoreNames = map[string]string{
|
||||
"atari800_libretro": "Atari 800 (Atari 5200)",
|
||||
"bluemsx_libretro": "BlueMSX (MSX)",
|
||||
"fbneo_libretro": "FBNeo (Arcade)",
|
||||
"fceumm_libretro": "Fceumm (NES)",
|
||||
"gambatte_libretro": "Gambatte (Game Boy)",
|
||||
"genesis_plus_gx_libretro": "Genesis Plus GX (Genesis, Master System, Sega CD)",
|
||||
"handy_libretro": "Handy (Atari Lynx)",
|
||||
"lutro_libretro": "Lutro (LÖVE)",
|
||||
"mednafen_ngp_libretro": "Beetle NeoGeo Pocket",
|
||||
"mednafen_pce_fast_libretro": "Beetle PC-Engine Fast",
|
||||
"mednafen_pce_libretro": "Beetle PC-Engine",
|
||||
"mednafen_pcfx_libretro": "Beetle PC-FX",
|
||||
"mednafen_psx_libretro": "Beetle PlayStation",
|
||||
"mednafen_saturn_libretro": "Beetle Saturn",
|
||||
"mednafen_supergrafx_libretro": "Beetle SuperGrafx",
|
||||
"mednafen_vb_libretro": "Beetle VirtualBoy",
|
||||
"mednafen_wswan_libretro": "Beetle WonderSwan",
|
||||
"melonds_libretro": "MelonDS (Nintendo DS)",
|
||||
"mgba_libretro": "mGBA (Game Boy Advance)",
|
||||
"np2kai_libretro": "NP2Kai (PC-98)",
|
||||
"o2em_libretro": "O2EM (Odyssey²)",
|
||||
"pcsx_rearmed_libretro": "PCSX Rearmed (PLayStation)",
|
||||
"picodrive_libretro": "PicoDrive (Genesis, 32X)",
|
||||
"pokemini_libretro": "PokeMini (Pokemon Mini)",
|
||||
"prosystem_libretro": "Prosystem (Atari 7800)",
|
||||
"sameboy_libretro": "SameBoy (Game Boy)",
|
||||
"snes9x_libretro": "Snes9x (SNES)",
|
||||
"stella2014_libretro": "Stella 2014 (Atari 2600)",
|
||||
"swanstation_libretro": "SwanStation (PlayStation)",
|
||||
"vecx_libretro": "VecX (Vectrex)",
|
||||
"virtualjaguar_libretro": "Virtual Jaguar (Atari Jaguar)",
|
||||
"atari800_libretro": "Atari - 5200 (Atari800)",
|
||||
"bluemsx_libretro": "MSX/SVI/ColecoVision/SG-1000 (blueMSX)",
|
||||
"fbneo_libretro": "Arcade (FinalBurn Neo)",
|
||||
"fceumm_libretro": "Nintendo - NES / Famicom (FCEUmm)",
|
||||
"gambatte_libretro": "Nintendo - Game Boy / Color (Gambatte)",
|
||||
"gearsystem_libretro": "Sega - MS/GG/SG-1000 (Gearsystem)",
|
||||
"genesis_plus_gx_libretro": "Sega - MS/GG/MD/CD (Genesis Plus GX)",
|
||||
"handy_libretro": "Atari - Lynx (Handy)",
|
||||
"lutro_libretro": "Lua Engine (Lutro)",
|
||||
"mednafen_ngp_libretro": "SNK - Neo Geo Pocket / Color (Beetle NeoPop)",
|
||||
"mednafen_pce_fast_libretro": "NEC - PC Engine / CD (Beetle PCE FAST)",
|
||||
"mednafen_pce_libretro": "NEC - PC Engine / SuperGrafx / CD (Beetle PCE)",
|
||||
"mednafen_pcfx_libretro": "NEC - PC-FX (Beetle PC-FX)",
|
||||
"mednafen_psx_libretro": "Sony - PlayStation (Beetle PSX)",
|
||||
"mednafen_saturn_libretro": "Sega - Saturn (Beetle Saturn)",
|
||||
"mednafen_supergrafx_libretro": "NEC - PC Engine SuperGrafx (Beetle SuperGrafx)",
|
||||
"mednafen_vb_libretro": "Nintendo - Virtual Boy (Beetle VB)",
|
||||
"mednafen_wswan_libretro": "Bandai - WonderSwan/Color (Beetle Cygne)",
|
||||
"melonds_libretro": "Nintendo - DS (melonDS)",
|
||||
"mgba_libretro": "Nintendo - Game Boy Advance (mGBA)",
|
||||
"mupen64plus_next_libretro": "Nintendo - Nintendo 64 (Mupen64Plus-Next)",
|
||||
"np2kai_libretro": "NEC - PC-98 (Neko Project II Kai)",
|
||||
"o2em_libretro": "Magnavox - Odyssey2 / Phillips Videopac+ (O2EM)",
|
||||
"pcsx_rearmed_libretro": "Sony - PlayStation (PCSX ReARMed)",
|
||||
"picodrive_libretro": "Sega - MS/GG/MD/CD/32X (PicoDrive)",
|
||||
"pokemini_libretro": "Nintendo - Pokemon Mini (PokeMini)",
|
||||
"prosystem_libretro": "Atari - 7800 (ProSystem)",
|
||||
"sameboy_libretro": "Nintendo - Game Boy / Color (SameBoy)",
|
||||
"snes9x_libretro": "Nintendo - SNES / SFC (Snes9x - Current)",
|
||||
"stella2014_libretro": "Atari - 2600 (Stella 2014)",
|
||||
"swanstation_libretro": "Sony - PlayStation (SwanStation)",
|
||||
"vecx_libretro": "GCE - Vectrex (vecx)",
|
||||
"virtualjaguar_libretro": "Atari - Jaguar (Virtual Jaguar)",
|
||||
}
|
||||
|
||||
func prettifyCoreName(in string) string {
|
||||
@@ -158,7 +160,7 @@ func coreExplorerCb(path string) {
|
||||
ntf.DisplayAndLog(ntf.Error, "Core", err.Error())
|
||||
return
|
||||
}
|
||||
ntf.DisplayAndLog(ntf.Success, "Core", "Core loaded: %s", filepath.Base(path))
|
||||
ntf.DisplayAndLogf(ntf.Success, "Core", "Core loaded: %s", filepath.Base(path))
|
||||
}
|
||||
|
||||
// triggered when a game is selected in the file explorer of Load Game
|
||||
@@ -178,7 +180,7 @@ func gameExplorerCb(path string) {
|
||||
|
||||
// Shutdown the operating system
|
||||
func cleanShutdown() {
|
||||
core.UnloadGame()
|
||||
core.Unload()
|
||||
if err := exec.Command("/usr/sbin/shutdown", "-P", "now").Run(); err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
}
|
||||
@@ -186,7 +188,7 @@ func cleanShutdown() {
|
||||
|
||||
// Reboots the operating system
|
||||
func cleanReboot() {
|
||||
core.UnloadGame()
|
||||
core.Unload()
|
||||
if err := exec.Command("/usr/sbin/shutdown", "-r", "now").Run(); err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
}
|
||||
|
||||
+32
-19
@@ -23,9 +23,13 @@ func buildPlaylist(path string) Scene {
|
||||
var list scenePlaylist
|
||||
list.label = utils.FileName(path)
|
||||
|
||||
re := regexp.MustCompile(`\(([Dd]isc [1-9]?)\)`)
|
||||
for _, game := range playlists.Playlists[path] {
|
||||
game := game // needed for callbackOK
|
||||
strippedName, tags := extractTags(game.Name)
|
||||
if match := re.FindStringSubmatch(game.Name); len(match) >= 2 {
|
||||
strippedName = strippedName + " (" + match[1] + ")"
|
||||
}
|
||||
list.children = append(list.children, entry{
|
||||
label: strippedName,
|
||||
gameName: game.Name,
|
||||
@@ -53,12 +57,12 @@ func buildPlaylist(path string) Scene {
|
||||
// Index first letters of entries to allow quick jump to the next or previous
|
||||
// letter
|
||||
func buildIndexes(list *entry) {
|
||||
var last byte
|
||||
var last rune
|
||||
for i := 0; i < len(list.children); i++ {
|
||||
char := list.children[i].label[0]
|
||||
char := firstRune(list.children[i].label)
|
||||
if char != last {
|
||||
list.indexes = append(list.indexes, struct {
|
||||
Char byte
|
||||
Char rune
|
||||
Index int
|
||||
}{char, i})
|
||||
last = char
|
||||
@@ -94,7 +98,7 @@ func loadPlaylistEntry(list *scenePlaylist, playlist string, game playlists.Game
|
||||
return
|
||||
}
|
||||
if _, err := os.Stat(corePath); os.IsNotExist(err) {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", "Core not found: %s", filepath.Base(corePath))
|
||||
ntf.DisplayAndLogf(ntf.Error, "Menu", "Core not found: %s", filepath.Base(corePath))
|
||||
return
|
||||
}
|
||||
if state.CorePath != corePath {
|
||||
@@ -229,16 +233,22 @@ func (s *scenePlaylist) render() {
|
||||
680*menu.ratio-25*e.scale*menu.ratio,
|
||||
float32(h)*e.yp-14*menu.ratio-25*e.scale*menu.ratio+fontOffset,
|
||||
50*menu.ratio, 50*menu.ratio,
|
||||
e.scale, white.Alpha(e.iconAlpha))
|
||||
e.scale, 0, white.Alpha(e.iconAlpha))
|
||||
}
|
||||
|
||||
menu.Font.SetColor(textColor.Alpha(e.labelAlpha))
|
||||
stack := 840 * menu.ratio
|
||||
menu.Font.Printf(
|
||||
menu.Font.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
|
||||
menu.Font.Print(
|
||||
(840+1)*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset+1*menu.ratio,
|
||||
menu.ratio, e.label)
|
||||
menu.Font.SetColor(textColor.Alpha(e.labelAlpha))
|
||||
menu.Font.Print(
|
||||
840*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset,
|
||||
0.5*menu.ratio, e.label)
|
||||
stack += float32(int(menu.Font.Width(0.5*menu.ratio, e.label)))
|
||||
menu.ratio, e.label)
|
||||
|
||||
stack += float32(int(menu.Font.Width(menu.ratio, e.label)))
|
||||
stack += 10
|
||||
|
||||
for _, tag := range e.tags {
|
||||
@@ -247,7 +257,7 @@ func (s *scenePlaylist) render() {
|
||||
menu.DrawImage(
|
||||
menu.icons[tag],
|
||||
stack, float32(h)*e.yp-22*menu.ratio,
|
||||
60*menu.ratio, 44*menu.ratio, 1.0, white.Alpha(e.tagAlpha))
|
||||
60*menu.ratio, 44*menu.ratio, 1.0, 0, white.Alpha(e.tagAlpha))
|
||||
menu.DrawBorder(stack, float32(h)*e.yp-22*menu.ratio,
|
||||
60*menu.ratio, 44*menu.ratio, 0.05/menu.ratio, black.Alpha(e.tagAlpha/4))
|
||||
stack += 60 * menu.ratio
|
||||
@@ -261,20 +271,23 @@ func (s *scenePlaylist) render() {
|
||||
|
||||
func (s *scenePlaylist) drawHintBar() {
|
||||
w, h := menu.GetFramebufferSize()
|
||||
menu.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, lightGrey)
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
|
||||
|
||||
_, upDown, _, a, b, x, _, _, _, guide := hintIcons()
|
||||
|
||||
var stack float32
|
||||
if state.CoreRunning {
|
||||
stackHint(&stack, guide, "RESUME", h)
|
||||
}
|
||||
stackHint(&stack, upDown, "NAVIGATE", h)
|
||||
stackHint(&stack, b, "BACK", h)
|
||||
stackHint(&stack, a, "RUN", h)
|
||||
lstack := float32(75) * menu.ratio
|
||||
rstack := float32(w) - 96*menu.ratio
|
||||
stackHintLeft(&lstack, upDown, "Navigate", h)
|
||||
stackHintRight(&rstack, b, "Back", h)
|
||||
stackHintRight(&rstack, a, "Run", h)
|
||||
|
||||
list := menu.stack[len(menu.stack)-1].Entry()
|
||||
if list.children[list.ptr].callbackX != nil {
|
||||
stackHint(&stack, x, "DELETE", h)
|
||||
stackHintRight(&rstack, x, "Delete", h)
|
||||
}
|
||||
|
||||
if state.CoreRunning {
|
||||
stackHintRight(&rstack, guide, "Resume", h)
|
||||
}
|
||||
}
|
||||
+9
-6
@@ -1,6 +1,7 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"github.com/libretro/ludo/core"
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/utils"
|
||||
@@ -27,7 +28,7 @@ func buildQuickMenu() Scene {
|
||||
label: "Reset",
|
||||
icon: "reset",
|
||||
callbackOK: func() {
|
||||
state.Core.Reset()
|
||||
core.Reset()
|
||||
state.MenuActive = false
|
||||
state.FastForward = false
|
||||
},
|
||||
@@ -47,11 +48,13 @@ func buildQuickMenu() Scene {
|
||||
icon: "screenshot",
|
||||
callbackOK: func() {
|
||||
name := utils.DatedName(state.GamePath)
|
||||
err := menu.TakeScreenshot(name)
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
} else {
|
||||
ntf.DisplayAndLog(ntf.Success, "Menu", "Took a screenshot.")
|
||||
state.PendingScreenshotName = name
|
||||
state.PendingScreenshotDone = func(err error) {
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
} else {
|
||||
ntf.DisplayAndLog(ntf.Success, "Menu", "Took a screenshot.")
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
+37
-25
@@ -26,22 +26,26 @@ func buildSavestates() Scene {
|
||||
icon: "savestate",
|
||||
callbackOK: func() {
|
||||
name := utils.DatedName(state.GamePath)
|
||||
err := menu.TakeScreenshot(name)
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
}
|
||||
err = savestates.Save(name)
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
} else {
|
||||
menu.stack[len(menu.stack)-1] = buildSavestates()
|
||||
menu.tweens.FastForward()
|
||||
ntf.DisplayAndLog(ntf.Success, "Menu", "State saved.")
|
||||
state.PendingScreenshotName = name
|
||||
state.PendingScreenshotDone = func(err error) {
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
}
|
||||
err = savestates.Save(name)
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
} else {
|
||||
menu.stack[len(menu.stack)-1] = buildSavestates()
|
||||
menu.tweens.FastForward()
|
||||
ntf.DisplayAndLog(ntf.Success, "Menu", "State saved.")
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
gameName := utils.FileName(state.GamePath)
|
||||
gameName = strings.Replace(gameName, "[", "\\[", -1)
|
||||
gameName = strings.Replace(gameName, "]", "\\]", -1)
|
||||
paths, _ := filepath.Glob(settings.Current.SavestatesDirectory + "/" + gameName + "@*.state")
|
||||
sort.Sort(sort.Reverse(sort.StringSlice(paths)))
|
||||
for _, path := range paths {
|
||||
@@ -104,7 +108,7 @@ func removeSavestateEntry(s []entry, path string) []entry {
|
||||
func deleteSavestateEntry(list *sceneSavestates, path string) {
|
||||
err := os.Remove(path)
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", "Could not delete savestate: %s", err.Error())
|
||||
ntf.DisplayAndLogf(ntf.Error, "Menu", "Could not delete savestate: %v", err)
|
||||
return
|
||||
}
|
||||
list.children = removeSavestateEntry(list.children, path)
|
||||
@@ -148,40 +152,48 @@ func (s *sceneSavestates) render() {
|
||||
680*menu.ratio-25*e.scale*menu.ratio,
|
||||
float32(h)*e.yp-14*menu.ratio-25*e.scale*menu.ratio+fontOffset,
|
||||
50*menu.ratio, 50*menu.ratio,
|
||||
e.scale, textColor.Alpha(e.iconAlpha))
|
||||
e.scale, 0, textColor.Alpha(e.iconAlpha))
|
||||
}
|
||||
|
||||
menu.Font.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
|
||||
menu.Font.Print(
|
||||
(840+1)*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset+1*menu.ratio,
|
||||
menu.ratio, e.label)
|
||||
|
||||
menu.Font.SetColor(textColor.Alpha(e.labelAlpha))
|
||||
menu.Font.Printf(
|
||||
menu.Font.Print(
|
||||
840*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset,
|
||||
0.5*menu.ratio, e.label)
|
||||
menu.ratio, e.label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sceneSavestates) drawHintBar() {
|
||||
w, h := menu.GetFramebufferSize()
|
||||
menu.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, lightGrey)
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
|
||||
|
||||
ptr := menu.stack[len(menu.stack)-1].Entry().ptr
|
||||
|
||||
_, upDown, _, a, b, x, _, _, _, guide := hintIcons()
|
||||
|
||||
var stack float32
|
||||
if state.CoreRunning {
|
||||
stackHint(&stack, guide, "RESUME", h)
|
||||
}
|
||||
stackHint(&stack, upDown, "NAVIGATE", h)
|
||||
stackHint(&stack, b, "BACK", h)
|
||||
lstack := float32(75) * menu.ratio
|
||||
rstack := float32(w) - 96*menu.ratio
|
||||
stackHintLeft(&lstack, upDown, "Navigate", h)
|
||||
if ptr == 0 {
|
||||
stackHint(&stack, a, "SAVE", h)
|
||||
stackHintRight(&rstack, a, "Save", h)
|
||||
} else {
|
||||
stackHint(&stack, a, "LOAD", h)
|
||||
stackHintRight(&rstack, a, "Load", h)
|
||||
}
|
||||
stackHintRight(&rstack, b, "Back", h)
|
||||
if state.CoreRunning {
|
||||
stackHintRight(&rstack, guide, "Resume", h)
|
||||
}
|
||||
|
||||
list := menu.stack[len(menu.stack)-1].Entry()
|
||||
if list.children[list.ptr].callbackX != nil {
|
||||
stackHint(&stack, x, "DELETE", h)
|
||||
stackHintRight(&rstack, x, "Delete", h)
|
||||
}
|
||||
}
|
||||
+32
-17
@@ -6,7 +6,7 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/fatih/structs"
|
||||
"github.com/go-gl/glfw/v3.3/glfw"
|
||||
"github.com/go-gl/glfw/v3.4/glfw"
|
||||
|
||||
"github.com/libretro/ludo/audio"
|
||||
"github.com/libretro/ludo/ludos"
|
||||
@@ -60,7 +60,7 @@ func buildSettings() Scene {
|
||||
icon: "folder",
|
||||
value: f.Value,
|
||||
stringValue: func() string {
|
||||
return "[" + utils.FileName(f.Value().(string)) + "]"
|
||||
return "【" + utils.FileName(f.Value().(string)) + "】"
|
||||
},
|
||||
widget: widgets[f.Tag("widget")],
|
||||
callbackOK: func() {
|
||||
@@ -117,7 +117,7 @@ func dirExplorerCb(path string, f *structs.Field) {
|
||||
return
|
||||
}
|
||||
f.Set(path)
|
||||
ntf.DisplayAndLog(ntf.Success, "Settings", "%s set to %s", f.Tag("label"), f.Value().(string))
|
||||
ntf.DisplayAndLogf(ntf.Success, "Settings", "%s set to %s", f.Tag("label"), f.Value().(string))
|
||||
err = settings.Save()
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Settings", err.Error())
|
||||
@@ -139,7 +139,7 @@ var widgets = map[string]func(*entry){
|
||||
float32(w)-128*menu.ratio-128*menu.ratio,
|
||||
float32(h)*e.yp-64*1.25*menu.ratio,
|
||||
128*menu.ratio, 128*menu.ratio,
|
||||
1.25, textColor.Alpha(e.iconAlpha))
|
||||
1.25, 0, textColor.Alpha(e.iconAlpha))
|
||||
},
|
||||
|
||||
// Range widget for audio volume and similat float settings
|
||||
@@ -164,8 +164,7 @@ var incrCallbacks = map[string]callbackIncrement{
|
||||
v := f.Value().(bool)
|
||||
v = !v
|
||||
f.Set(v)
|
||||
menu.Reconfigure(settings.Current.VideoFullscreen)
|
||||
menu.ContextReset()
|
||||
menu.SetFullscreen(settings.Current.VideoFullscreen)
|
||||
settings.Save()
|
||||
},
|
||||
"VideoMonitorIndex": func(f *structs.Field, direction int) {
|
||||
@@ -178,8 +177,7 @@ var incrCallbacks = map[string]callbackIncrement{
|
||||
v = len(glfw.GetMonitors()) - 1
|
||||
}
|
||||
f.Set(v)
|
||||
menu.Reconfigure(settings.Current.VideoFullscreen)
|
||||
menu.ContextReset()
|
||||
menu.SetFullscreen(settings.Current.VideoFullscreen)
|
||||
settings.Save()
|
||||
},
|
||||
"VideoFilter": func(f *structs.Field, direction int) {
|
||||
@@ -203,6 +201,21 @@ var incrCallbacks = map[string]callbackIncrement{
|
||||
f.Set(v)
|
||||
settings.Save()
|
||||
},
|
||||
"VideoTheme": func(f *structs.Field, direction int) {
|
||||
themes := []string{"Default", "Rose Pine", "Dracula", "Catppuccin"}
|
||||
v := f.Value().(string)
|
||||
i := utils.IndexOfString(v, themes)
|
||||
i += direction
|
||||
if i < 0 {
|
||||
i = len(themes) - 1
|
||||
}
|
||||
if i > len(themes)-1 {
|
||||
i = 0
|
||||
}
|
||||
f.Set(themes[i])
|
||||
menu.UpdateFilter(themes[i])
|
||||
settings.Save()
|
||||
},
|
||||
"MapAxisToDPad": func(f *structs.Field, direction int) {
|
||||
v := f.Value().(bool)
|
||||
v = !v
|
||||
@@ -274,20 +287,22 @@ func (s *sceneSettings) render() {
|
||||
|
||||
func (s *sceneSettings) drawHintBar() {
|
||||
w, h := menu.GetFramebufferSize()
|
||||
menu.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, lightGrey)
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
|
||||
|
||||
_, upDown, leftRight, a, b, _, _, _, _, guide := hintIcons()
|
||||
|
||||
var stack float32
|
||||
lstack := float32(75) * menu.ratio
|
||||
rstack := float32(w) - 96*menu.ratio
|
||||
list := menu.stack[len(menu.stack)-1].Entry()
|
||||
if state.CoreRunning {
|
||||
stackHint(&stack, guide, "RESUME", h)
|
||||
}
|
||||
stackHint(&stack, upDown, "NAVIGATE", h)
|
||||
stackHint(&stack, b, "BACK", h)
|
||||
stackHintLeft(&lstack, upDown, "Navigate", h)
|
||||
if list.children[list.ptr].callbackOK != nil {
|
||||
stackHint(&stack, a, "SET", h)
|
||||
stackHintRight(&rstack, a, "Set", h)
|
||||
} else {
|
||||
stackHint(&stack, leftRight, "SET", h)
|
||||
stackHintLeft(&lstack, leftRight, "Set", h)
|
||||
}
|
||||
stackHintRight(&rstack, b, "Back", h)
|
||||
if state.CoreRunning {
|
||||
stackHintRight(&rstack, guide, "Resume", h)
|
||||
}
|
||||
}
|
||||
+36
-29
@@ -3,7 +3,8 @@ package menu
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
|
||||
//"os/user"
|
||||
"sort"
|
||||
|
||||
"github.com/libretro/ludo/audio"
|
||||
@@ -12,10 +13,9 @@ import (
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/playlists"
|
||||
"github.com/libretro/ludo/scanner"
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/utils"
|
||||
"github.com/libretro/ludo/video"
|
||||
colorful "github.com/lucasb-eyer/go-colorful"
|
||||
|
||||
"github.com/tanema/gween"
|
||||
"github.com/tanema/gween/ease"
|
||||
@@ -63,8 +63,8 @@ func buildTabs() Scene {
|
||||
subLabel: "Scan your collection",
|
||||
icon: "add",
|
||||
callbackOK: func() {
|
||||
usr, _ := user.Current()
|
||||
menu.Push(buildExplorer(usr.HomeDir, nil,
|
||||
//usr, _ := user.Current()
|
||||
menu.Push(buildExplorer(settings.Current.FileDirectory, nil,
|
||||
func(path string) {
|
||||
scanner.ScanDir(path, refreshTabs)
|
||||
},
|
||||
@@ -160,7 +160,7 @@ func getPlaylists() []entry {
|
||||
func deletePlaylist(path string) {
|
||||
err := os.Remove(path)
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", "Could not delete playlist: %s", err.Error())
|
||||
ntf.DisplayAndLogf(ntf.Error, "Menu", "Could not delete playlist: %v", err)
|
||||
return
|
||||
}
|
||||
menu.stack[0].Entry().ptr++
|
||||
@@ -284,47 +284,54 @@ func (tabs sceneTabs) render() {
|
||||
|
||||
stackWidth := 710 * menu.ratio
|
||||
for i, e := range tabs.children {
|
||||
|
||||
cf := colorful.Hcl(float64(i)*20, 0.5, 0.5)
|
||||
c := video.Color{R: float32(cf.R), G: float32(cf.B), B: float32(cf.G), A: e.iconAlpha}
|
||||
iconColor := tabIconColors(i).Alpha(e.iconAlpha)
|
||||
hexaColor := tabHexaColors(i).Alpha(e.iconAlpha)
|
||||
lablColor := hexaColor
|
||||
if settings.Current.VideoDarkMode || state.CoreRunning {
|
||||
iconColor = tabHexaColors(i).Alpha(e.iconAlpha)
|
||||
hexaColor = tabIconColors(i).Alpha(e.iconAlpha)
|
||||
lablColor = iconColor
|
||||
}
|
||||
|
||||
x := -menu.scroll*menu.ratio + stackWidth + e.width/2*menu.ratio
|
||||
|
||||
stackWidth += e.width*menu.ratio + e.margin*menu.ratio
|
||||
|
||||
if e.labelAlpha > 0 {
|
||||
menu.Font.SetColor(c.Alpha(e.labelAlpha))
|
||||
lw := menu.Font.Width(0.5*menu.ratio, e.label)
|
||||
menu.Font.Printf(x-lw/2, float32(int(float32(h)/2+250*menu.ratio)), 0.5*menu.ratio, e.label)
|
||||
lw = menu.Font.Width(0.4*menu.ratio, e.subLabel)
|
||||
menu.Font.Printf(x-lw/2, float32(int(float32(h)/2+330*menu.ratio)), 0.4*menu.ratio, e.subLabel)
|
||||
lw := menu.Font.Width(menu.ratio, e.label)
|
||||
menu.Font.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
|
||||
menu.Font.Print(x-lw/2+1*menu.ratio, float32(int(float32(h)/2+(250+1)*menu.ratio)), menu.ratio, e.label)
|
||||
menu.Font.SetColor(lablColor.Alpha(e.labelAlpha))
|
||||
menu.Font.Print(x-lw/2, float32(int(float32(h)/2+250*menu.ratio)), menu.ratio, e.label)
|
||||
lw = menu.FontSm.Width(menu.ratio, e.subLabel)
|
||||
menu.FontSm.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
|
||||
menu.FontSm.Print(x-lw/2+1*menu.ratio, float32(int(float32(h)/2+(330+1)*menu.ratio)), menu.ratio, e.subLabel)
|
||||
menu.FontSm.SetColor(lablColor.Alpha(e.labelAlpha))
|
||||
menu.FontSm.Print(x-lw/2, float32(int(float32(h)/2+330*menu.ratio)), menu.ratio, e.subLabel)
|
||||
}
|
||||
|
||||
menu.DrawImage(menu.icons["hexagon"],
|
||||
x-220*e.scale*menu.ratio, float32(h)/2-220*e.scale*menu.ratio,
|
||||
440*menu.ratio, 440*menu.ratio, e.scale, c)
|
||||
440*menu.ratio, 440*menu.ratio, e.scale, 0, hexaColor)
|
||||
|
||||
menu.DrawImage(menu.icons[e.icon],
|
||||
x-128*e.scale*menu.ratio, float32(h)/2-128*e.scale*menu.ratio,
|
||||
256*menu.ratio, 256*menu.ratio, e.scale, white.Alpha(e.iconAlpha))
|
||||
256*menu.ratio, 256*menu.ratio, e.scale, 0, iconColor)
|
||||
}
|
||||
}
|
||||
|
||||
func (tabs sceneTabs) drawHintBar() {
|
||||
w, h := menu.GetFramebufferSize()
|
||||
menu.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, lightGrey)
|
||||
func (s *sceneTabs) drawHintBar() {
|
||||
w, h := menu.Window.GetFramebufferSize()
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
|
||||
|
||||
_, _, leftRight, a, _, x, _, _, _, guide := hintIcons()
|
||||
_, _, leftRight, a, _, _, _, _, _, guide := hintIcons()
|
||||
|
||||
var stack float32
|
||||
lstack := float32(75) * menu.ratio
|
||||
rstack := float32(w) - 96*menu.ratio
|
||||
stackHintLeft(&lstack, leftRight, "Navigate", h)
|
||||
stackHintRight(&rstack, a, "Ok", h)
|
||||
if state.CoreRunning {
|
||||
stackHint(&stack, guide, "RESUME", h)
|
||||
}
|
||||
stackHint(&stack, leftRight, "NAVIGATE", h)
|
||||
stackHint(&stack, a, "OPEN", h)
|
||||
|
||||
list := menu.stack[0].Entry()
|
||||
if list.children[list.ptr].callbackX != nil {
|
||||
stackHint(&stack, x, "DELETE", h)
|
||||
stackHintRight(&rstack, guide, "Resume", h)
|
||||
}
|
||||
}
|
||||
@@ -90,7 +90,7 @@ func (s *sceneUpdater) update(dt float32) {
|
||||
s.children[0].icon = "reload"
|
||||
s.children[0].callbackOK = func() {
|
||||
cmd := exec.Command("/usr/sbin/shutdown", "-r", "now")
|
||||
core.UnloadGame()
|
||||
core.Unload()
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
|
||||
+8
-6
@@ -85,15 +85,17 @@ func (s *sceneWiFi) render() {
|
||||
}
|
||||
|
||||
func (s *sceneWiFi) drawHintBar() {
|
||||
w, h := menu.GetFramebufferSize()
|
||||
menu.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, lightGrey)
|
||||
w, h := menu.Window.GetFramebufferSize()
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor)
|
||||
menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor)
|
||||
|
||||
_, upDown, _, a, b, _, _, _, _, _ := hintIcons()
|
||||
|
||||
var stack float32
|
||||
stackHint(&stack, upDown, "NAVIGATE", h)
|
||||
stackHint(&stack, b, "BACK", h)
|
||||
lstack := float32(75) * menu.ratio
|
||||
rstack := float32(w) - 96*menu.ratio
|
||||
stackHintLeft(&lstack, upDown, "Navigate", h)
|
||||
if s.children[0].callbackOK != nil {
|
||||
stackHint(&stack, a, "CONNECT", h)
|
||||
stackHintRight(&rstack, a, "Connect", h)
|
||||
}
|
||||
stackHintRight(&rstack, b, "Back", h)
|
||||
}
|
||||
+2
-2
@@ -78,7 +78,7 @@ func drawThumbnail(list *entry, i int, system, gameName string, x, y, w, h, scal
|
||||
|
||||
menu.DrawImage(
|
||||
list.children[i].thumbnail,
|
||||
x, y, w, h, scale,
|
||||
x, y, w, h, scale, 0,
|
||||
color,
|
||||
)
|
||||
}
|
||||
@@ -93,7 +93,7 @@ func drawSavestateThumbnail(list *entry, i int, path string, x, y, w, h, scale f
|
||||
|
||||
menu.DrawImage(
|
||||
list.children[i].thumbnail,
|
||||
x, y, w, h, scale,
|
||||
x, y, w, h, scale, 0,
|
||||
color,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -58,12 +58,16 @@ func Display(severity Severity, message string, duration float32) *Notification
|
||||
}
|
||||
|
||||
// DisplayAndLog creates a new notification and also logs the message to stdout.
|
||||
func DisplayAndLog(severity Severity, prefix, message string, vars ...interface{}) *Notification {
|
||||
msg := fmt.Sprintf(message, vars...)
|
||||
func DisplayAndLog(severity Severity, prefix, message string) *Notification {
|
||||
if state.Verbose {
|
||||
log.Print("[" + prefix + "]: " + msg + "\n")
|
||||
log.Print("[" + prefix + "]: " + message + "\n")
|
||||
}
|
||||
return Display(severity, msg, Medium)
|
||||
return Display(severity, message, Medium)
|
||||
}
|
||||
|
||||
// DisplayAndLogf formats a message, then creates a new notification and logs it.
|
||||
func DisplayAndLogf(severity Severity, prefix, format string, vars ...interface{}) *Notification {
|
||||
return DisplayAndLog(severity, prefix, fmt.Sprintf(format, vars...))
|
||||
}
|
||||
|
||||
// Process iterates over the notifications, update them, delete the old ones.
|
||||
@@ -86,10 +90,13 @@ func Clear() {
|
||||
|
||||
// Update the message of a given notification. Also resets the delay before
|
||||
// disapearing.
|
||||
func (n *Notification) Update(severity Severity, message string, vars ...interface{}) {
|
||||
msg := fmt.Sprintf(message, vars...)
|
||||
|
||||
func (n *Notification) Update(severity Severity, message string) {
|
||||
n.Duration = Medium
|
||||
n.Message = msg
|
||||
n.Message = message
|
||||
n.Severity = severity
|
||||
}
|
||||
|
||||
// Updatef formats and updates the message of a given notification.
|
||||
func (n *Notification) Updatef(severity Severity, format string, vars ...interface{}) {
|
||||
n.Update(severity, fmt.Sprintf(format, vars...))
|
||||
}
|
||||
@@ -47,7 +47,7 @@ func Test_Display(t *testing.T) {
|
||||
func Test_DisplayAndLog(t *testing.T) {
|
||||
Clear()
|
||||
t.Run("Format message properly", func(t *testing.T) {
|
||||
DisplayAndLog(Info, "Tests", "Joypad #%d loaded with name %s.", 3, "Foo")
|
||||
DisplayAndLogf(Info, "Tests", "Joypad #%d loaded with name %s.", 3, "Foo")
|
||||
got := notifications[0].Message
|
||||
want := "Joypad #3 loaded with name Foo."
|
||||
if got != want {
|
||||
@@ -68,7 +68,7 @@ func Test_DisplayAndLog(t *testing.T) {
|
||||
Clear()
|
||||
t.Run("Logs to stdout if verbose", func(t *testing.T) {
|
||||
state.Verbose = true
|
||||
got := utils.CaptureOutput(func() { DisplayAndLog(Info, "Test", "Joypad #%d loaded with name %s.", 3, "Foo") })
|
||||
got := utils.CaptureOutput(func() { DisplayAndLogf(Info, "Test", "Joypad #%d loaded with name %s.", 3, "Foo") })
|
||||
want := "[Test]: Joypad #3 loaded with name Foo.\n"
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got = %v, want %v", got, want)
|
||||
@@ -78,7 +78,7 @@ func Test_DisplayAndLog(t *testing.T) {
|
||||
Clear()
|
||||
t.Run("Logs nothing if not verbose", func(t *testing.T) {
|
||||
state.Verbose = false
|
||||
got := utils.CaptureOutput(func() { DisplayAndLog(Info, "Test", "Joypad #%d loaded with name %s.", 3, "Foo") })
|
||||
got := utils.CaptureOutput(func() { DisplayAndLogf(Info, "Test", "Joypad #%d loaded with name %s.", 3, "Foo") })
|
||||
want := ""
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got = %v, want %v", got, want)
|
||||
|
||||
+8
-5
@@ -6,7 +6,6 @@ package options
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -51,13 +50,17 @@ func New(vars []VariableInterface) (*Options, error) {
|
||||
|
||||
// Cache core options
|
||||
for _, v := range vars {
|
||||
v := v
|
||||
choices := v.Choices()
|
||||
// A core may expose an empty selector, e.g. when no BIOS files are found.
|
||||
if len(choices) == 0 {
|
||||
continue
|
||||
}
|
||||
o.Vars = append(o.Vars, &Variable{
|
||||
Key: v.Key(),
|
||||
Desc: v.Desc(),
|
||||
Choices: v.Choices(),
|
||||
Choices: choices,
|
||||
Default: v.DefaultValue(),
|
||||
Choice: utils.IndexOfString(v.DefaultValue(), v.Choices()),
|
||||
Choice: utils.IndexOfString(v.DefaultValue(), choices),
|
||||
})
|
||||
}
|
||||
o.Updated = true
|
||||
@@ -100,7 +103,7 @@ func (o *Options) load() error {
|
||||
defer o.Unlock()
|
||||
|
||||
name := utils.FileName(state.CorePath)
|
||||
b, err := ioutil.ReadFile(filepath.Join(xdg.ConfigHome, "ludo", name+".toml"))
|
||||
b, err := os.ReadFile(filepath.Join(xdg.ConfigHome, "ludo", name+".toml"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+1
-10
@@ -33,7 +33,6 @@ func ipsAllocTargetData(patch, source []byte) ([]byte, error) {
|
||||
size |= int(patch[offset]) << 8
|
||||
offset++
|
||||
size |= int(patch[offset]) << 0
|
||||
offset++
|
||||
targetLength = size
|
||||
prov := make([]byte, targetLength)
|
||||
return prov, nil
|
||||
@@ -117,15 +116,7 @@ func applyIPS(patch, source []byte) (*[]byte, error) {
|
||||
offset++
|
||||
|
||||
if address == EOF {
|
||||
if offset == len(patch) {
|
||||
return &targetData, nil
|
||||
} else if offset == len(patch)-3 {
|
||||
size := int(patch[offset]) << 16
|
||||
offset++
|
||||
size |= int(patch[offset]) << 8
|
||||
offset++
|
||||
size |= int(patch[offset]) << 0
|
||||
offset++
|
||||
if offset == len(patch) || offset == len(patch)-3 {
|
||||
return &targetData, nil
|
||||
}
|
||||
}
|
||||
|
||||
+2
-3
@@ -4,7 +4,6 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -15,7 +14,7 @@ import (
|
||||
func Try(gamePath string, bytes []byte) (*[]byte, error) {
|
||||
upsFile := strings.TrimSuffix(gamePath, filepath.Ext(gamePath)) + ".ups"
|
||||
if _, err := os.Stat(upsFile); !os.IsNotExist(err) {
|
||||
pbytes, err := ioutil.ReadFile(upsFile)
|
||||
pbytes, err := os.ReadFile(upsFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -30,7 +29,7 @@ func Try(gamePath string, bytes []byte) (*[]byte, error) {
|
||||
|
||||
ipsFile := strings.TrimSuffix(gamePath, filepath.Ext(gamePath)) + ".ips"
|
||||
if _, err := os.Stat(ipsFile); !os.IsNotExist(err) {
|
||||
pbytes, err := ioutil.ReadFile(ipsFile)
|
||||
pbytes, err := os.ReadFile(ipsFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -138,6 +138,7 @@ func ShortName(in string) string {
|
||||
"Nintendo - Game Boy Color": "Game Boy Color",
|
||||
"Nintendo - Game Boy": "Game Boy",
|
||||
"Nintendo - Nintendo Entertainment System": "NES / Famicom",
|
||||
"Nintendo - Nintendo 64": "Nintendo 64",
|
||||
"Nintendo - Pokemon Mini": "Pokemon Mini",
|
||||
"Nintendo - Super Nintendo Entertainment System": "Super Nintendo",
|
||||
"Nintendo - Virtual Boy": "Virtual Boy",
|
||||
|
||||
@@ -4,7 +4,6 @@ package savefiles
|
||||
import (
|
||||
"C"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
@@ -84,7 +83,7 @@ func LoadSRAM() error {
|
||||
// this *[1 << 30]byte points to the same memory as ptr, allowing to
|
||||
// overwrite this memory
|
||||
destination := (*[1 << 30]byte)(unsafe.Pointer(ptr))[:len:len]
|
||||
source, err := ioutil.ReadFile(path())
|
||||
source, err := os.ReadFile(path())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -3,19 +3,17 @@
|
||||
package savestates
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/libretro/ludo/core"
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/state"
|
||||
)
|
||||
|
||||
// Save the current state to the filesystem. name is the name of the
|
||||
// savestate file to save to, without extension.
|
||||
func Save(name string) error {
|
||||
s := state.Core.SerializeSize()
|
||||
bytes, err := state.Core.Serialize(s)
|
||||
bytes, err := core.Serialize()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -24,16 +22,14 @@ func Save(name string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(path, bytes, 0644)
|
||||
return os.WriteFile(path, bytes, 0644)
|
||||
}
|
||||
|
||||
// Load the state from the filesystem
|
||||
func Load(path string) error {
|
||||
s := state.Core.SerializeSize()
|
||||
bytes, err := ioutil.ReadFile(path)
|
||||
bytes, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = state.Core.Unserialize(bytes, s)
|
||||
return err
|
||||
return core.Unserialize(bytes)
|
||||
}
|
||||
+26
-16
@@ -6,7 +6,7 @@ package scanner
|
||||
import (
|
||||
"archive/zip"
|
||||
"hash/crc32"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
|
||||
// LoadDB loops over the RDBs in a given directory and parses them
|
||||
func LoadDB(dir string) (dat.DB, error) {
|
||||
files, err := ioutil.ReadDir(dir)
|
||||
files, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return dat.DB{}, err
|
||||
}
|
||||
@@ -33,7 +33,7 @@ func LoadDB(dir string) (dat.DB, error) {
|
||||
continue
|
||||
}
|
||||
system := name[0 : len(name)-4]
|
||||
bytes, _ := ioutil.ReadFile(filepath.Join(dir, name))
|
||||
bytes, _ := os.ReadFile(filepath.Join(dir, name))
|
||||
db[system] = dat.Parse(bytes)
|
||||
}
|
||||
return db, nil
|
||||
@@ -41,7 +41,7 @@ func LoadDB(dir string) (dat.DB, error) {
|
||||
|
||||
// ScanDir scans a full directory, report progress and generate playlists
|
||||
func ScanDir(dir string, doneCb func()) {
|
||||
n := ntf.DisplayAndLog(ntf.Info, "Menu", "Scanning %s", dir)
|
||||
n := ntf.DisplayAndLogf(ntf.Info, "Menu", "Scanning %s", dir)
|
||||
roms, err := utils.AllFilesIn(dir)
|
||||
if err != nil {
|
||||
n.Update(ntf.Error, err.Error())
|
||||
@@ -71,7 +71,7 @@ func ScanDir(dir string, doneCb func()) {
|
||||
i++
|
||||
}
|
||||
doneCb()
|
||||
n.Update(ntf.Success, "Done scanning. %d new games found.", i)
|
||||
n.Updatef(ntf.Success, "Done scanning. %d new games found.", i)
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ func checksumHeaderless(rom *zip.File, headerSize uint) (uint32, uint32, error)
|
||||
return 0, 0, err
|
||||
}
|
||||
defer h.Close()
|
||||
bytes, err := ioutil.ReadAll(h)
|
||||
bytes, err := io.ReadAll(h)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
@@ -114,35 +114,45 @@ func Scan(dir string, roms []string, games chan (dat.Game), n *ntf.Notification)
|
||||
}
|
||||
for _, rom := range z.File {
|
||||
romExt := filepath.Ext(rom.Name)
|
||||
size := int64(rom.UncompressedSize64)
|
||||
// these 4 systems might have headered or headerless roms and need special logic
|
||||
if romExt == ".nes" || romExt == ".a78" || romExt == ".lnx" || romExt == ".fds" {
|
||||
crc, crcHeaderless, err := checksumHeaderless(rom, headerSizes[romExt])
|
||||
if headerSize, ok := headerSizes[romExt]; ok {
|
||||
crc, crcHeaderless, err := checksumHeaderless(rom, headerSize)
|
||||
if err != nil {
|
||||
n.Update(ntf.Error, err.Error())
|
||||
continue
|
||||
}
|
||||
state.DB.FindByCRC(f, rom.Name, crc, games)
|
||||
state.DB.FindByCRC(f, rom.Name, crcHeaderless, games)
|
||||
state.DB.FindByCRC(f, rom.Name, crc, size, games)
|
||||
state.DB.FindByCRC(f, rom.Name, crcHeaderless, size-int64(headerSize), games)
|
||||
n.Update(ntf.Info, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+f)
|
||||
} else if rom.CRC32 > 0 {
|
||||
// Look for a matching game entry in the database
|
||||
state.DB.FindByCRC(f, rom.Name, rom.CRC32, games)
|
||||
state.DB.FindByCRC(f, rom.Name, rom.CRC32, size, games)
|
||||
n.Update(ntf.Info, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+f)
|
||||
}
|
||||
}
|
||||
z.Close()
|
||||
case ".cue":
|
||||
case ".cue", ".pbp", ".m3u":
|
||||
// Look for a matching game entry in the database
|
||||
state.DB.FindByROMName(f, filepath.Base(f), 0, games)
|
||||
n.Update(ntf.Info, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+f)
|
||||
case ".32x", "a52", ".a78", ".col", ".crt", ".d64", ".pce", ".fds", ".gb", ".gba", ".gbc", ".gen", ".gg", ".ipf", ".j64", ".jag", ".lnx", ".md", ".n64", ".nes", ".ngc", ".nds", ".rom", ".sfc", ".sg", ".smc", ".smd", ".sms", ".ws", ".wsc":
|
||||
bytes, err := ioutil.ReadFile(f)
|
||||
case ".32x", ".a26", "a52", ".a78", ".col", ".crt", ".d64", ".pce", ".fds", ".gb", ".gba", ".gbc", ".gen", ".gg", ".ipf", ".j64", ".jag", ".lnx", ".md", ".n64", ".nes", ".ngc", ".nds", ".rom", ".sfc", ".sg", ".smc", ".smd", ".sms", ".ws", ".wsc", ".z64":
|
||||
bytes, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
n.Update(ntf.Error, err.Error())
|
||||
continue
|
||||
}
|
||||
CRC32 := crc32.ChecksumIEEE(bytes)
|
||||
state.DB.FindByCRC(f, utils.FileName(f), CRC32, games)
|
||||
s, err := os.Stat(f)
|
||||
if err != nil {
|
||||
n.Update(ntf.Error, err.Error())
|
||||
continue
|
||||
}
|
||||
crc := crc32.ChecksumIEEE(bytes)
|
||||
state.DB.FindByCRC(f, utils.FileName(f), crc, s.Size(), games)
|
||||
if headerSize, ok := headerSizes[ext]; ok {
|
||||
crcHeaderless := crc32.ChecksumIEEE(bytes[headerSize:])
|
||||
state.DB.FindByCRC(f, utils.FileName(f), crcHeaderless, s.Size()-int64(headerSize), games)
|
||||
}
|
||||
n.Update(ntf.Info, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+f)
|
||||
}
|
||||
}
|
||||
|
||||
+11
-1
@@ -2,15 +2,18 @@ package settings
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"os/user"
|
||||
"github.com/adrg/xdg"
|
||||
)
|
||||
|
||||
func defaultSettings() Settings {
|
||||
usr, _ := user.Current()
|
||||
return Settings{
|
||||
VideoFullscreen: false,
|
||||
VideoMonitorIndex: 0,
|
||||
VideoFilter: "Pixel Perfect",
|
||||
VideoDarkMode: false,
|
||||
VideoTheme: "Default",
|
||||
MapAxisToDPad: false,
|
||||
AudioVolume: 0.5,
|
||||
MenuAudioVolume: 0.25,
|
||||
@@ -40,13 +43,17 @@ func defaultSettings() Settings {
|
||||
"Nintendo - Game Boy Advance": "mgba_libretro",
|
||||
"Nintendo - Game Boy Color": "gambatte_libretro",
|
||||
"Nintendo - Game Boy": "gambatte_libretro",
|
||||
"Nintendo - GameCube": "dolphin_libretro",
|
||||
"Nintendo - Nintendo 3DS": "citra_libretro",
|
||||
"Nintendo - Nintendo 64": "mupen64plus_next_libretro",
|
||||
"Nintendo - Nintendo Entertainment System": "fceumm_libretro",
|
||||
"Nintendo - Nintendo DS": "melonds_libretro",
|
||||
"Nintendo - Pokemon Mini": "pokemini_libretro",
|
||||
"Nintendo - Super Nintendo Entertainment System": "snes9x_libretro",
|
||||
"Nintendo - Virtual Boy": "mednafen_vb_libretro",
|
||||
"Nintendo - Wii": "dolphin_libretro",
|
||||
"Sega - 32X": "picodrive_libretro",
|
||||
"Sega - Dreamcast": "flycast_libretro",
|
||||
"Sega - Game Gear": "genesis_plus_gx_libretro",
|
||||
"Sega - Master System - Mark III": "genesis_plus_gx_libretro",
|
||||
"Sega - Mega Drive - Genesis": "genesis_plus_gx_libretro",
|
||||
@@ -57,7 +64,10 @@ func defaultSettings() Settings {
|
||||
"SNK - Neo Geo Pocket Color": "mednafen_ngp_libretro",
|
||||
"SNK - Neo Geo Pocket": "mednafen_ngp_libretro",
|
||||
"Sony - PlayStation": playstationCore,
|
||||
"Sony - PlayStation 2": "pcsx2_libretro",
|
||||
"Sony - PlayStation Portable": "ppsspp_libretro",
|
||||
},
|
||||
FileDirectory: usr.HomeDir,
|
||||
CoresDirectory: "./cores",
|
||||
AssetsDirectory: "./assets",
|
||||
DatabaseDirectory: "./database",
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -23,9 +22,10 @@ import (
|
||||
// Widget sets the graphical representation of the value.
|
||||
type Settings struct {
|
||||
VideoFullscreen bool `hide:"ludos" toml:"video_fullscreen" label:"Video Fullscreen" fmt:"%t" widget:"switch"`
|
||||
VideoMonitorIndex int `toml:"video_monitor_index" label:"Video Monitor Index" fmt:"%d"`
|
||||
VideoFilter string `toml:"video_filter" label:"Video Filter" fmt:"<%s>"`
|
||||
VideoMonitorIndex int `toml:"video_monitor_index" label:"Video Monitor Index" fmt:"◀ %d ▶"`
|
||||
VideoFilter string `toml:"video_filter" label:"Video Filter" fmt:"◀ %s ▶"`
|
||||
VideoDarkMode bool `toml:"video_dark_mode" label:"Video Dark Mode" fmt:"%t" widget:"switch"`
|
||||
VideoTheme string `toml:"video_theme" label:"Video Theme" fmt:"◀ %s ▶"`
|
||||
|
||||
AudioVolume float32 `toml:"audio_volume" label:"Audio Volume" fmt:"%.1f" widget:"range"`
|
||||
|
||||
@@ -36,6 +36,7 @@ type Settings struct {
|
||||
|
||||
CoreForPlaylist map[string]string `hide:"always" toml:"core_for_playlist"`
|
||||
|
||||
FileDirectory string `hide:"ludos" toml:"files_dir" label:"Files Directory" fmt:"%s" widget:"dir"`
|
||||
CoresDirectory string `hide:"ludos" toml:"cores_dir" label:"Cores Directory" fmt:"%s" widget:"dir"`
|
||||
AssetsDirectory string `hide:"ludos" toml:"assets_dir" label:"Assets Directory" fmt:"%s" widget:"dir"`
|
||||
DatabaseDirectory string `hide:"ludos" toml:"database_dir" label:"Database Directory" fmt:"%s" widget:"dir"`
|
||||
@@ -73,14 +74,14 @@ func Load() error {
|
||||
|
||||
// If /etc/ludo.toml exists, override the defaults
|
||||
if _, err := os.Stat("/etc/ludo.toml"); !os.IsNotExist(err) {
|
||||
b, _ := ioutil.ReadFile("/etc/ludo.toml")
|
||||
b, _ := os.ReadFile("/etc/ludo.toml")
|
||||
err = toml.Unmarshal(b, &Current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadFile(filepath.Join(xdg.ConfigHome, "ludo", "settings.toml"))
|
||||
b, err := os.ReadFile(filepath.Join(xdg.ConfigHome, "ludo", "settings.toml"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -33,3 +33,13 @@ var LudOS bool
|
||||
|
||||
// FastForward will run the core as fast as possible
|
||||
var FastForward bool
|
||||
|
||||
// PendingScreenshotName is the next screenshot capture requested by the UI.
|
||||
var PendingScreenshotName string
|
||||
|
||||
// PendingScreenshotDone is called after the deferred screenshot capture completes.
|
||||
var PendingScreenshotDone func(error)
|
||||
|
||||
// MenuContextResetNeeded requests re-uploading menu GL assets after the
|
||||
// frontend window/context is recreated.
|
||||
var MenuContextResetNeeded bool
|
||||
@@ -0,0 +1,190 @@
|
||||
//go:build gltest && !darwin
|
||||
|
||||
package video
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
"github.com/go-gl/glfw/v3.4/glfw"
|
||||
"github.com/go-gl/mathgl/mgl32"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
"github.com/libretro/ludo/state"
|
||||
)
|
||||
|
||||
// Run with go test -tags 'wayland gltest' ./video on a graphical session.
|
||||
func TestHardwareContextIsolation(t *testing.T) {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
if err := glfw.Init(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer glfw.Terminate()
|
||||
glfw.DefaultWindowHints()
|
||||
glfw.WindowHint(glfw.Visible, glfw.False)
|
||||
glfw.WindowHint(glfw.ContextVersionMajor, 3)
|
||||
glfw.WindowHint(glfw.ContextVersionMinor, 3)
|
||||
glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
|
||||
w, err := glfw.CreateWindow(128, 96, "Ludo GL test", nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer w.Destroy()
|
||||
w.MakeContextCurrent()
|
||||
if err := initGL(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("GL:", gl.GoStr(gl.GetString(gl.VERSION)))
|
||||
|
||||
oldCore, oldRunning := state.Core, state.CoreRunning
|
||||
defer func() { state.Core, state.CoreRunning = oldCore, oldRunning }()
|
||||
hw := &libretro.HWRenderCallback{HWContextType: libretro.HWContextOpenGLCore, VersionMajor: 3, VersionMinor: 3}
|
||||
state.Core = &libretro.Core{HWRenderCallback: hw}
|
||||
state.CoreRunning = true
|
||||
v := &Video{Window: w, identityMat: mgl32.Ident4(), Geom: libretro.GameGeometry{MaxWidth: 64, MaxHeight: 64}}
|
||||
v.SetHWRenderContext(hw)
|
||||
v.ensurePixelFormatDefaults()
|
||||
v.resetGameTexture(64, 64)
|
||||
hw.ContextReset = func() {
|
||||
if glfw.GetCurrentContext() != v.HWWindow {
|
||||
t.Fatal("context_reset ran outside the HW context")
|
||||
}
|
||||
}
|
||||
if err := v.ResetHWContext(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer v.DestroySharedHWContext()
|
||||
if glfw.GetCurrentContext() != w || v.HWWindow == nil || v.HWWindow == w {
|
||||
t.Fatal("reset did not return to an independent frontend context")
|
||||
}
|
||||
|
||||
v.BeginCore()
|
||||
var readFBO, texture, pbo uint32
|
||||
gl.GenFramebuffers(1, &readFBO)
|
||||
gl.BindFramebuffer(gl.READ_FRAMEBUFFER, readFBO)
|
||||
gl.GenTextures(1, &texture)
|
||||
gl.ActiveTexture(gl.TEXTURE1)
|
||||
gl.BindTexture(gl.TEXTURE_2D, texture)
|
||||
gl.GenBuffers(1, &pbo)
|
||||
gl.BindBuffer(gl.PIXEL_PACK_BUFFER, pbo)
|
||||
gl.PixelStorei(gl.PACK_ROW_LENGTH, 256)
|
||||
gl.Enable(gl.BLEND)
|
||||
gl.Enable(gl.SCISSOR_TEST)
|
||||
gl.BlendEquation(gl.FUNC_REVERSE_SUBTRACT)
|
||||
v.EndCore()
|
||||
v.BeginFrontendFrame()
|
||||
checkInt := func(name uint32, want int32) {
|
||||
t.Helper()
|
||||
var got int32
|
||||
gl.GetIntegerv(name, &got)
|
||||
if got != want {
|
||||
t.Errorf("GL state %#x: got %d, want %d", name, got, want)
|
||||
}
|
||||
}
|
||||
checkInt(gl.PIXEL_PACK_BUFFER_BINDING, 0)
|
||||
checkInt(gl.PACK_ROW_LENGTH, 0)
|
||||
checkInt(gl.READ_FRAMEBUFFER_BINDING, 0)
|
||||
checkInt(gl.BLEND_EQUATION_RGB, gl.FUNC_ADD)
|
||||
v.BeginCore()
|
||||
checkInt(gl.PIXEL_PACK_BUFFER_BINDING, int32(pbo))
|
||||
checkInt(gl.PACK_ROW_LENGTH, 256)
|
||||
checkInt(gl.READ_FRAMEBUFFER_BINDING, int32(readFBO))
|
||||
checkInt(gl.DRAW_FRAMEBUFFER_BINDING, int32(v.fboID))
|
||||
checkInt(gl.BLEND_EQUATION_RGB, gl.FUNC_REVERSE_SUBTRACT)
|
||||
checkInt(gl.ACTIVE_TEXTURE, gl.TEXTURE1)
|
||||
checkInt(gl.TEXTURE_BINDING_2D, int32(texture))
|
||||
if !gl.IsEnabled(gl.BLEND) || !gl.IsEnabled(gl.SCISSOR_TEST) {
|
||||
t.Error("frontend rendering changed core enables")
|
||||
}
|
||||
gl.BindBuffer(gl.PIXEL_PACK_BUFFER, 0)
|
||||
gl.PixelStorei(gl.PACK_ROW_LENGTH, 0)
|
||||
gl.Disable(gl.SCISSOR_TEST)
|
||||
gl.Disable(gl.BLEND)
|
||||
gl.BindFramebuffer(gl.READ_FRAMEBUFFER, v.fboID)
|
||||
|
||||
// Verify the produced pixels are visible through the shared texture.
|
||||
gl.ClearColor(1, 0, 0, 1)
|
||||
gl.Clear(gl.COLOR_BUFFER_BIT)
|
||||
v.EndCore()
|
||||
var frontendFBO uint32
|
||||
gl.GenFramebuffers(1, &frontendFBO)
|
||||
gl.BindFramebuffer(gl.FRAMEBUFFER, frontendFBO)
|
||||
gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, v.texID, 0)
|
||||
var pixel [4]byte
|
||||
gl.ReadPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(&pixel[0]))
|
||||
if pixel != [4]byte{255, 0, 0, 255} {
|
||||
t.Errorf("shared frame: got %v, want red", pixel)
|
||||
}
|
||||
v.BeginFrontendFrame()
|
||||
|
||||
// Backbuffer snapshots must retain their orientation for either core origin.
|
||||
v.defaultProgram, err = newProgram(vertexShader, defaultFragmentShader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gl.GenVertexArrays(1, &v.vao)
|
||||
gl.BindVertexArray(v.vao)
|
||||
gl.GenBuffers(1, &v.vbo)
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, v.vbo)
|
||||
vert := uint32(gl.GetAttribLocation(v.defaultProgram, gl.Str("vert\x00")))
|
||||
uv := uint32(gl.GetAttribLocation(v.defaultProgram, gl.Str("vertTexCoord\x00")))
|
||||
gl.EnableVertexAttribArray(vert)
|
||||
gl.VertexAttribPointerWithOffset(vert, 2, gl.FLOAT, false, 16, 0)
|
||||
gl.EnableVertexAttribArray(uv)
|
||||
gl.VertexAttribPointerWithOffset(uv, 2, gl.FLOAT, false, 16, 8)
|
||||
v.width, v.height = 64, 64
|
||||
for _, bottomLeft := range []bool{false, true} {
|
||||
hw.BottomLeftOrigin = bottomLeft
|
||||
v.BeginFrontendFrame()
|
||||
fbw, fbh := w.GetFramebufferSize()
|
||||
v.Geom.AspectRatio = float64(fbw) / float64(fbh)
|
||||
v.presentW, v.presentH = int32(fbw), int32(fbh)
|
||||
gl.ClearColor(1, 0, 0, 1)
|
||||
gl.Clear(gl.COLOR_BUFFER_BIT)
|
||||
gl.Enable(gl.SCISSOR_TEST)
|
||||
gl.Scissor(0, int32(fbh/2), int32(fbw), int32(fbh/2))
|
||||
gl.ClearColor(0, 0, 1, 1)
|
||||
gl.Clear(gl.COLOR_BUFFER_BIT)
|
||||
gl.Disable(gl.SCISSOR_TEST)
|
||||
v.CachePresentedFrame()
|
||||
if !v.RenderCachedFrame() {
|
||||
t.Fatal("cached frame unavailable")
|
||||
}
|
||||
gl.ReadPixels(1, 1, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(&pixel[0]))
|
||||
if pixel != [4]byte{255, 0, 0, 255} {
|
||||
t.Errorf("cached frame, bottom-left=%v: got %v, want red", bottomLeft, pixel)
|
||||
}
|
||||
}
|
||||
|
||||
hw.ContextDestroy = func() {
|
||||
if glfw.GetCurrentContext() != v.HWWindow {
|
||||
t.Error("context_destroy ran outside the HW context")
|
||||
}
|
||||
}
|
||||
v.RunContextDestroy()
|
||||
v.ResetCoreState()
|
||||
if !gl.IsFramebuffer(frontendFBO) {
|
||||
t.Error("HW teardown deleted a frontend framebuffer with the same name")
|
||||
}
|
||||
if v.fboID != 0 || v.HWWindow != nil || v.cachedFrameValid || v.width != 0 {
|
||||
t.Error("HW teardown left stale core resources")
|
||||
}
|
||||
|
||||
// A subsequent core must get a fresh context, including on the GL 2.x sync fallback.
|
||||
if err := v.ResetHWContext(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
v.hwSync = false
|
||||
v.BeginCore()
|
||||
checkInt(gl.PACK_ROW_LENGTH, 0)
|
||||
checkInt(gl.PIXEL_PACK_BUFFER_BINDING, 0)
|
||||
checkInt(gl.BLEND_EQUATION_RGB, gl.FUNC_ADD)
|
||||
if gl.IsEnabled(gl.BLEND) || gl.IsEnabled(gl.SCISSOR_TEST) {
|
||||
t.Error("new core inherited the previous core's enables")
|
||||
}
|
||||
v.EndCore()
|
||||
if e := gl.GetError(); e != gl.NO_ERROR {
|
||||
t.Errorf("GL error: %#x", e)
|
||||
}
|
||||
}
|
||||
@@ -18,10 +18,11 @@ uniform vec2 OutputSize;
|
||||
uniform vec2 TextureSize;
|
||||
uniform vec2 InputSize;
|
||||
uniform sampler2D Texture;
|
||||
uniform vec4 color;
|
||||
COMPAT_VARYING vec2 fragTexCoord;
|
||||
|
||||
void main() {
|
||||
vec4 c = COMPAT_TEXTURE(Texture, fragTexCoord);
|
||||
COMPAT_FRAGCOLOR = vec4(c.rgb, 1.0);
|
||||
COMPAT_FRAGCOLOR = c * color;
|
||||
}
|
||||
` + "\x00"
|
||||
@@ -4,14 +4,9 @@ var vertexShader = `
|
||||
#if __VERSION__ >= 130
|
||||
#define COMPAT_VARYING out
|
||||
#define COMPAT_ATTRIBUTE in
|
||||
#define COMPAT_TEXTURE texture
|
||||
#define COMPAT_FRAGCOLOR FragColor
|
||||
out vec4 COMPAT_FRAGCOLOR;
|
||||
#else
|
||||
#define COMPAT_VARYING varying
|
||||
#define COMPAT_ATTRIBUTE attribute
|
||||
#define COMPAT_TEXTURE texture2D
|
||||
#define COMPAT_FRAGCOLOR gl_FragColor
|
||||
#endif
|
||||
|
||||
COMPAT_ATTRIBUTE vec2 vert;
|
||||
@@ -19,8 +14,10 @@ COMPAT_ATTRIBUTE vec2 vertTexCoord;
|
||||
|
||||
COMPAT_VARYING vec2 fragTexCoord;
|
||||
|
||||
uniform mat4 MVP;
|
||||
|
||||
void main() {
|
||||
fragTexCoord = vertTexCoord;
|
||||
gl_Position = vec4(vert, 0.0, 1.0);
|
||||
gl_Position = vec4(vert, 0.0, 1.0) * MVP;
|
||||
}
|
||||
` + "\x00"
|
||||
@@ -1,29 +0,0 @@
|
||||
package video
|
||||
|
||||
var demulFragmentShader = `
|
||||
#if __VERSION__ >= 130
|
||||
#define COMPAT_VARYING in
|
||||
#define COMPAT_ATTRIBUTE in
|
||||
#define COMPAT_TEXTURE texture
|
||||
#define COMPAT_FRAGCOLOR FragColor
|
||||
out vec4 COMPAT_FRAGCOLOR;
|
||||
#else
|
||||
#define COMPAT_VARYING varying
|
||||
#define COMPAT_ATTRIBUTE attribute
|
||||
#define COMPAT_TEXTURE texture2D
|
||||
#define COMPAT_FRAGCOLOR gl_FragColor
|
||||
#endif
|
||||
|
||||
uniform sampler2D Texture;
|
||||
uniform vec4 color;
|
||||
|
||||
COMPAT_VARYING vec2 fragTexCoord;
|
||||
|
||||
vec4 demultiply(vec4 c) {
|
||||
return vec4(c.rgb/c.a, c.a);
|
||||
}
|
||||
|
||||
void main() {
|
||||
COMPAT_FRAGCOLOR = color * demultiply(COMPAT_TEXTURE(Texture, fragTexCoord));
|
||||
}
|
||||
` + "\x00"
|
||||
+340
-139
@@ -5,7 +5,7 @@ import (
|
||||
"image"
|
||||
"image/draw"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"golang.org/x/image/math/fixed"
|
||||
)
|
||||
|
||||
type character struct {
|
||||
type glyph struct {
|
||||
x, y int
|
||||
width int // Glyph width
|
||||
height int // Glyph height
|
||||
@@ -34,9 +34,11 @@ const (
|
||||
TopToBottom // E.g.: Chinese
|
||||
)
|
||||
|
||||
const fontRenderScale = 1
|
||||
|
||||
// A Font allows rendering of text to an OpenGL context.
|
||||
type Font struct {
|
||||
fontChar []*character
|
||||
glyphs map[rune]*glyph
|
||||
vao uint32
|
||||
vbo uint32
|
||||
program uint32
|
||||
@@ -44,20 +46,249 @@ type Font struct {
|
||||
color Color
|
||||
atlasWidth float32
|
||||
atlasHeight float32
|
||||
atlas *image.RGBA
|
||||
face font.Face
|
||||
ttf *truetype.Font
|
||||
scale int32
|
||||
lineHeight float32
|
||||
margin int
|
||||
nextX int
|
||||
nextY int
|
||||
}
|
||||
|
||||
type point [4]float32
|
||||
|
||||
func max(a, b float32) float32 {
|
||||
if a > b {
|
||||
return a
|
||||
type glyphMetrics struct {
|
||||
bounds fixed.Rectangle26_6
|
||||
advance fixed.Int26_6
|
||||
width int
|
||||
height int
|
||||
ascent int
|
||||
descent int
|
||||
}
|
||||
|
||||
func ceilFixed26_6(v fixed.Int26_6) int32 {
|
||||
return int32((v + 63) >> 6)
|
||||
}
|
||||
|
||||
func appendRange(runes []rune, start, end rune) []rune {
|
||||
for r := start; r <= end; r++ {
|
||||
runes = append(runes, r)
|
||||
}
|
||||
return b
|
||||
return runes
|
||||
}
|
||||
|
||||
func defaultRunes() []rune {
|
||||
runes := make([]rune, 0, 512)
|
||||
runes = appendRange(runes, 32, 126)
|
||||
runes = append(runes,
|
||||
'◀', '▶', '【', '】',
|
||||
'「', '」', '『', '』',
|
||||
'。', '、', '・', 'ー',
|
||||
'!', '?',
|
||||
'(', ')', '[', ']', '{', '}',
|
||||
)
|
||||
runes = appendRange(runes, 0xFF01, 0xFF0F) // fullwidth punctuation
|
||||
runes = appendRange(runes, 0xFF10, 0xFF19) // fullwidth digits
|
||||
runes = appendRange(runes, 0xFF1A, 0xFF20) // fullwidth : ; < = > ? @
|
||||
runes = appendRange(runes, 0xFF21, 0xFF3A) // fullwidth A-Z
|
||||
runes = appendRange(runes, 0xFF3B, 0xFF40) // fullwidth [ \ ] ^ _
|
||||
runes = appendRange(runes, 0xFF41, 0xFF5A) // fullwidth a-z
|
||||
runes = appendRange(runes, 0xFF5B, 0xFF65) // fullwidth { | } ~ and katakana punctuation
|
||||
runes = appendRange(runes, 0x00A0, 0x017F)
|
||||
runes = appendRange(runes, 0x3000, 0x303F)
|
||||
runes = appendRange(runes, 0x3040, 0x30FF)
|
||||
return runes
|
||||
}
|
||||
|
||||
func getGlyphMetrics(face font.Face, ttf *truetype.Font, ch rune, scale int32) (glyphMetrics, error) {
|
||||
gBnd, gAdv, ok := face.GlyphBounds(ch)
|
||||
if !ok {
|
||||
return glyphMetrics{}, fmt.Errorf("ttf face glyphBounds error")
|
||||
}
|
||||
|
||||
gh := ceilFixed26_6(gBnd.Max.Y - gBnd.Min.Y)
|
||||
gw := ceilFixed26_6(gBnd.Max.X - gBnd.Min.X)
|
||||
|
||||
// If gylph has no dimensions set to a max value
|
||||
if gw == 0 || gh == 0 {
|
||||
gBnd = ttf.Bounds(fixed.Int26_6(scale))
|
||||
gw = ceilFixed26_6(gBnd.Max.X - gBnd.Min.X)
|
||||
gh = ceilFixed26_6(gBnd.Max.Y - gBnd.Min.Y)
|
||||
|
||||
// Above can sometimes yield 0 for font smaller than 48pt, 1 is minimum
|
||||
if gw == 0 || gh == 0 {
|
||||
gw = 1
|
||||
gh = 1
|
||||
}
|
||||
}
|
||||
|
||||
// The glyph's ascent and descent equal -bounds.Min.Y and +bounds.Max.Y.
|
||||
gAscent := int(ceilFixed26_6(-gBnd.Min.Y))
|
||||
gdescent := int(ceilFixed26_6(gBnd.Max.Y))
|
||||
|
||||
return glyphMetrics{
|
||||
bounds: gBnd,
|
||||
advance: gAdv,
|
||||
width: int(gw),
|
||||
height: int(gh),
|
||||
ascent: gAscent,
|
||||
descent: gdescent,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func wrapGlyph(x, y *int, glyphWidth, lineHeight, atlasWidth, margin int) {
|
||||
if *x+glyphWidth+margin > atlasWidth {
|
||||
*x = margin
|
||||
*y += lineHeight + margin
|
||||
}
|
||||
}
|
||||
|
||||
func appendGlyph(face font.Face, ttf *truetype.Font, ch rune, x, y *int, lineHeight, atlasWidth float32, atlas *image.RGBA, fg *image.Uniform, scale int32, margin int) (*glyph, error) {
|
||||
char := new(glyph)
|
||||
|
||||
metrics, err := getGlyphMetrics(face, ttf, ch, scale)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wrapGlyph(x, y, metrics.width, int(lineHeight), int(atlasWidth), margin)
|
||||
if *y+metrics.height+margin > atlas.Rect.Dy() {
|
||||
return nil, fmt.Errorf("glyph atlas full")
|
||||
}
|
||||
|
||||
// Set w,h and adv, bearing V and bearing H in char
|
||||
char.x = *x
|
||||
char.y = *y
|
||||
char.width = metrics.width
|
||||
char.height = metrics.height
|
||||
char.advance = int(metrics.advance)
|
||||
char.bearingV = metrics.descent
|
||||
char.bearingH = (int(metrics.bounds.Min.X) >> 6)
|
||||
|
||||
clip := image.Rect(*x, *y, *x+metrics.width, *y+metrics.height)
|
||||
|
||||
// Create a freetype context for drawing
|
||||
c := freetype.NewContext()
|
||||
c.SetDPI(72)
|
||||
c.SetFont(ttf)
|
||||
c.SetFontSize(float64(scale))
|
||||
c.SetClip(clip)
|
||||
c.SetDst(atlas)
|
||||
c.SetSrc(fg)
|
||||
c.SetHinting(font.HintingFull)
|
||||
|
||||
// Set the glyph dot
|
||||
px := 0 - (int(metrics.bounds.Min.X) >> 6) + *x
|
||||
py := (metrics.ascent) + *y
|
||||
pt := freetype.Pt(px, py)
|
||||
|
||||
// Draw the text from mask to image
|
||||
_, err = c.DrawString(string(ch), pt)
|
||||
*x += metrics.width + margin
|
||||
return char, err
|
||||
}
|
||||
|
||||
func getMaxTextureSize() int32 {
|
||||
var maxTextureSize int32
|
||||
gl.GetIntegerv(gl.MAX_TEXTURE_SIZE, &maxTextureSize)
|
||||
if maxTextureSize == 0 {
|
||||
maxTextureSize = 4096
|
||||
}
|
||||
return maxTextureSize
|
||||
}
|
||||
|
||||
func calculateAtlasSize(face font.Face, ttf *truetype.Font, runes []rune, scale int32, margin int) (float32, float32, float32, error) {
|
||||
maxTextureSize := getMaxTextureSize()
|
||||
lineHeight := float32(face.Metrics().Height.Ceil())
|
||||
if lineHeight < 1 {
|
||||
lineHeight = 1
|
||||
}
|
||||
|
||||
width := 2048
|
||||
if width > int(maxTextureSize) {
|
||||
width = int(maxTextureSize)
|
||||
}
|
||||
|
||||
for {
|
||||
x := margin
|
||||
y := margin
|
||||
maxY := 0
|
||||
|
||||
for _, r := range runes {
|
||||
metrics, err := getGlyphMetrics(face, ttf, r, scale)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
wrapGlyph(&x, &y, metrics.width, int(lineHeight), width, margin)
|
||||
x += metrics.width + margin
|
||||
|
||||
if y+int(lineHeight) > maxY {
|
||||
maxY = y + int(lineHeight)
|
||||
}
|
||||
}
|
||||
|
||||
height := maxY + margin
|
||||
|
||||
if height <= int(maxTextureSize) || width >= int(maxTextureSize) {
|
||||
if height > int(maxTextureSize) {
|
||||
return 0, 0, 0, fmt.Errorf("glyph atlas %dx%d exceeds max texture size %d", width, height, maxTextureSize)
|
||||
}
|
||||
return float32(width), float32(height), lineHeight, nil
|
||||
}
|
||||
|
||||
width *= 2
|
||||
if width > int(maxTextureSize) {
|
||||
width = int(maxTextureSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func reserveAtlasHeight(baseHeight int, lineHeight float32, margin int, maxTextureSize int32) int {
|
||||
height := baseHeight * 2
|
||||
extraRowsHeight := baseHeight + 32*(int(lineHeight)+margin)
|
||||
if extraRowsHeight > height {
|
||||
height = extraRowsHeight
|
||||
}
|
||||
if height > int(maxTextureSize) {
|
||||
height = int(maxTextureSize)
|
||||
}
|
||||
return height
|
||||
}
|
||||
|
||||
func (f *Font) uploadGlyphRect(g *glyph) {
|
||||
if f.atlas == nil || g == nil {
|
||||
return
|
||||
}
|
||||
|
||||
offset := g.y*f.atlas.Stride + g.x*4
|
||||
if offset < 0 || offset >= len(f.atlas.Pix) {
|
||||
return
|
||||
}
|
||||
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, f.textureID)
|
||||
gl.PixelStorei(gl.UNPACK_ALIGNMENT, 1)
|
||||
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, int32(f.atlas.Stride/4))
|
||||
gl.TexSubImage2D(
|
||||
gl.TEXTURE_2D,
|
||||
0,
|
||||
int32(g.x),
|
||||
int32(g.y),
|
||||
int32(g.width),
|
||||
int32(g.height),
|
||||
gl.RGBA,
|
||||
gl.UNSIGNED_BYTE,
|
||||
gl.Ptr(f.atlas.Pix[offset:]),
|
||||
)
|
||||
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, 0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, 0)
|
||||
}
|
||||
|
||||
// LoadTrueTypeFont builds a set of textures based on a ttf files gylphs
|
||||
func LoadTrueTypeFont(program uint32, r io.Reader, scale int32, low, high rune, dir Direction) (*Font, error) {
|
||||
data, err := ioutil.ReadAll(r)
|
||||
func LoadTrueTypeFont(program uint32, r io.Reader, scale int32, dir Direction) (*Font, error) {
|
||||
data, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -70,120 +301,63 @@ func LoadTrueTypeFont(program uint32, r io.Reader, scale int32, low, high rune,
|
||||
|
||||
// Make Font stuct type
|
||||
f := new(Font)
|
||||
f.fontChar = make([]*character, 0, high-low+1)
|
||||
f.glyphs = make(map[rune]*glyph)
|
||||
f.program = program // Set shader program
|
||||
f.SetColor(Color{R: 1, G: 1, B: 1, A: 1}) // Set default white
|
||||
f.ttf = ttf
|
||||
f.scale = scale
|
||||
|
||||
// Create new face
|
||||
ttfFace := truetype.NewFace(ttf, &truetype.Options{
|
||||
face := truetype.NewFace(ttf, &truetype.Options{
|
||||
Size: float64(scale),
|
||||
DPI: 72,
|
||||
Hinting: font.HintingFull,
|
||||
})
|
||||
f.face = face
|
||||
|
||||
margin := 4
|
||||
runes := defaultRunes()
|
||||
var lineHeight float32
|
||||
f.atlasWidth = 1024
|
||||
f.atlasHeight = 1024
|
||||
for ch := low; ch <= high; ch++ {
|
||||
gBnd, _, ok := ttfFace.GlyphBounds(ch)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("ttf face glyphBounds error")
|
||||
}
|
||||
gh := int32((gBnd.Max.Y - gBnd.Min.Y) >> 6)
|
||||
lineHeight = max(lineHeight, float32(gh))
|
||||
|
||||
f.atlasWidth, f.atlasHeight, lineHeight, err = calculateAtlasSize(face, ttf, runes, scale, margin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.atlasHeight = float32(reserveAtlasHeight(int(f.atlasHeight), lineHeight, margin, getMaxTextureSize()))
|
||||
f.lineHeight = lineHeight
|
||||
f.margin = margin
|
||||
|
||||
// Create image to draw glyph
|
||||
fg, bg := image.White, image.Black
|
||||
rect := image.Rect(0, 0, int(f.atlasWidth), int(f.atlasHeight))
|
||||
rgba := image.NewRGBA(rect)
|
||||
draw.Draw(rgba, rgba.Bounds(), bg, image.Point{}, draw.Src)
|
||||
atlas := image.NewRGBA(rect)
|
||||
draw.Draw(atlas, atlas.Bounds(), bg, image.Point{}, draw.Src)
|
||||
f.atlas = atlas
|
||||
|
||||
margin := 4
|
||||
x := margin
|
||||
y := margin
|
||||
|
||||
// Make each gylph
|
||||
for ch := low; ch <= high; ch++ {
|
||||
char := new(character)
|
||||
|
||||
gBnd, gAdv, ok := ttfFace.GlyphBounds(ch)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("ttf face glyphBounds error")
|
||||
}
|
||||
|
||||
gh := int32((gBnd.Max.Y - gBnd.Min.Y) >> 6)
|
||||
gw := int32((gBnd.Max.X - gBnd.Min.X) >> 6)
|
||||
|
||||
// If gylph has no dimensions set to a max value
|
||||
if gw == 0 || gh == 0 {
|
||||
gBnd = ttf.Bounds(fixed.Int26_6(scale))
|
||||
gw = int32((gBnd.Max.X - gBnd.Min.X) >> 6)
|
||||
gh = int32((gBnd.Max.Y - gBnd.Min.Y) >> 6)
|
||||
|
||||
// Above can sometimes yield 0 for font smaller than 48pt, 1 is minimum
|
||||
if gw == 0 || gh == 0 {
|
||||
gw = 1
|
||||
gh = 1
|
||||
}
|
||||
}
|
||||
|
||||
// The glyph's ascent and descent equal -bounds.Min.Y and +bounds.Max.Y.
|
||||
gAscent := int(-gBnd.Min.Y) >> 6
|
||||
gdescent := int(gBnd.Max.Y) >> 6
|
||||
|
||||
// Set w,h and adv, bearing V and bearing H in char
|
||||
char.x = x
|
||||
char.y = y
|
||||
char.width = int(gw)
|
||||
char.height = int(gh)
|
||||
char.advance = int(gAdv)
|
||||
char.bearingV = gdescent
|
||||
char.bearingH = (int(gBnd.Min.X) >> 6)
|
||||
|
||||
clip := image.Rect(x, y, x+int(gw), y+int(gh))
|
||||
|
||||
// Create a freetype context for drawing
|
||||
c := freetype.NewContext()
|
||||
c.SetDPI(72)
|
||||
c.SetFont(ttf)
|
||||
c.SetFontSize(float64(scale))
|
||||
c.SetClip(clip)
|
||||
c.SetDst(rgba)
|
||||
c.SetSrc(fg)
|
||||
c.SetHinting(font.HintingFull)
|
||||
|
||||
// Set the glyph dot
|
||||
px := 0 - (int(gBnd.Min.X) >> 6) + x
|
||||
py := (gAscent) + y
|
||||
pt := freetype.Pt(px, py)
|
||||
|
||||
x += int(gw) + margin
|
||||
if x+int(gw)+margin > int(f.atlasWidth) {
|
||||
x = 0
|
||||
y += int(lineHeight) + margin
|
||||
}
|
||||
|
||||
// Draw the text from mask to image
|
||||
_, err = c.DrawString(string(ch), pt)
|
||||
for _, r := range runes {
|
||||
glyph, err := appendGlyph(face, ttf, r, &x, &y, lineHeight, f.atlasWidth, atlas, fg, scale, margin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
continue
|
||||
}
|
||||
|
||||
// Add char to fontChar list
|
||||
f.fontChar = append(f.fontChar, char)
|
||||
f.glyphs[r] = glyph
|
||||
}
|
||||
f.nextX = x
|
||||
f.nextY = y
|
||||
|
||||
// Generate texture
|
||||
gl.GenTextures(1, &f.textureID)
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, f.textureID)
|
||||
gl.PixelStorei(gl.UNPACK_ALIGNMENT, 1)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
|
||||
|
||||
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, int32(rgba.Rect.Dx()), int32(rgba.Rect.Dy()), 0, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(rgba.Pix))
|
||||
|
||||
gl.GenerateMipmap(gl.TEXTURE_2D)
|
||||
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, int32(atlas.Rect.Dx()), int32(atlas.Rect.Dy()), 0, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(atlas.Pix))
|
||||
gl.BindTexture(gl.TEXTURE_2D, 0)
|
||||
|
||||
// Configure VAO/VBO for texture quads
|
||||
@@ -194,11 +368,11 @@ func LoadTrueTypeFont(program uint32, r io.Reader, scale int32, low, high rune,
|
||||
|
||||
vertAttrib := uint32(gl.GetAttribLocation(f.program, gl.Str("vert\x00")))
|
||||
gl.EnableVertexAttribArray(vertAttrib)
|
||||
gl.VertexAttribPointer(vertAttrib, 2, gl.FLOAT, false, 4*4, gl.PtrOffset(0))
|
||||
gl.VertexAttribPointerWithOffset(vertAttrib, 2, gl.FLOAT, false, 4*4, 0)
|
||||
|
||||
texCoordAttrib := uint32(gl.GetAttribLocation(f.program, gl.Str("vertTexCoord\x00")))
|
||||
gl.EnableVertexAttribArray(texCoordAttrib)
|
||||
gl.VertexAttribPointer(texCoordAttrib, 2, gl.FLOAT, false, 4*4, gl.PtrOffset(2*4))
|
||||
gl.VertexAttribPointerWithOffset(texCoordAttrib, 2, gl.FLOAT, false, 4*4, 2*4)
|
||||
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, 0)
|
||||
bindVertexArray(0)
|
||||
@@ -206,6 +380,25 @@ func LoadTrueTypeFont(program uint32, r io.Reader, scale int32, low, high rune,
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (f *Font) ensureGlyph(r rune) *glyph {
|
||||
if glyph, ok := f.glyphs[r]; ok {
|
||||
return glyph
|
||||
}
|
||||
if f.atlas == nil || f.face == nil || f.ttf == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
glyph, err := appendGlyph(f.face, f.ttf, r, &f.nextX, &f.nextY, f.lineHeight, f.atlasWidth, f.atlas, image.White, f.scale, f.margin)
|
||||
if err != nil {
|
||||
log.Printf("font: failed to append U+%04X %q: %v", r, string(r), err)
|
||||
return nil
|
||||
}
|
||||
f.glyphs[r] = glyph
|
||||
f.uploadGlyphRect(glyph)
|
||||
|
||||
return glyph
|
||||
}
|
||||
|
||||
// LoadFont loads the specified font at the given scale.
|
||||
func LoadFont(file string, scale int32, windowWidth int, windowHeight int) (*Font, error) {
|
||||
fd, err := os.Open(file)
|
||||
@@ -227,7 +420,16 @@ func LoadFont(file string, scale int32, windowWidth int, windowHeight int) (*Fon
|
||||
resUniform := gl.GetUniformLocation(program, gl.Str("resolution\x00"))
|
||||
gl.Uniform2f(resUniform, float32(windowWidth), float32(windowHeight))
|
||||
|
||||
return LoadTrueTypeFont(program, fd, scale, 32, 256, LeftToRight)
|
||||
atlasScale := int32(float32(scale) / fontRenderScale)
|
||||
if atlasScale < 1 {
|
||||
atlasScale = 1
|
||||
}
|
||||
|
||||
font, err := LoadTrueTypeFont(program, fd, atlasScale, LeftToRight)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return font, nil
|
||||
}
|
||||
|
||||
// SetColor allows you to set the text color to be used when you draw the text
|
||||
@@ -243,16 +445,17 @@ func (f *Font) UpdateResolution(windowWidth int, windowHeight int) {
|
||||
gl.UseProgram(0)
|
||||
}
|
||||
|
||||
// Printf draws a string to the screen, takes a list of arguments like printf
|
||||
func (f *Font) Printf(x, y float32, scale float32, fs string, argv ...interface{}) error {
|
||||
indices := []rune(fmt.Sprintf(fs, argv...))
|
||||
// Print draws a string to the screen.
|
||||
func (f *Font) Print(x, y float32, scale float32, text string) error {
|
||||
indices := []rune(text)
|
||||
scale *= fontRenderScale
|
||||
x = float32(int(x + 0.5))
|
||||
y = float32(int(y + 0.5))
|
||||
|
||||
if len(indices) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
lowChar := rune(32)
|
||||
|
||||
// Setup blending mode
|
||||
gl.Enable(gl.BLEND)
|
||||
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
|
||||
@@ -264,26 +467,23 @@ func (f *Font) Printf(x, y float32, scale float32, fs string, argv ...interface{
|
||||
|
||||
var coords []point
|
||||
|
||||
// Iterate through all characters in string
|
||||
// Iterate through all glyphs in string
|
||||
for i := range indices {
|
||||
// Get rune
|
||||
runeIndex := indices[i]
|
||||
|
||||
var ch *character
|
||||
// Skip runes that are not in font chacter range
|
||||
if int(runeIndex)-int(lowChar) > len(f.fontChar) || runeIndex < lowChar {
|
||||
// Print a ?
|
||||
ch = f.fontChar[int(rune('?'))-int(lowChar)]
|
||||
} else {
|
||||
// Find rune in fontChar list
|
||||
ch = f.fontChar[runeIndex-lowChar]
|
||||
var ch *glyph
|
||||
// Skip runes that are not in font glyph map
|
||||
ch = f.ensureGlyph(runeIndex)
|
||||
if ch == nil {
|
||||
ch = f.glyphs['?'] // fallback
|
||||
}
|
||||
|
||||
// Calculate position and size for current rune
|
||||
xpos := x - 1 + float32(ch.bearingH)*scale
|
||||
ypos := y - 2 - float32(ch.height-ch.bearingV)*scale
|
||||
w := float32(ch.width+2) * scale
|
||||
h := float32(ch.height+2) * scale
|
||||
xpos := x + float32(ch.bearingH)*scale
|
||||
ypos := y - float32(ch.height-ch.bearingV)*scale
|
||||
w := float32(ch.width) * scale
|
||||
h := float32(ch.height) * scale
|
||||
|
||||
// Set quad positions
|
||||
var x1 = xpos
|
||||
@@ -291,12 +491,12 @@ func (f *Font) Printf(x, y float32, scale float32, fs string, argv ...interface{
|
||||
var y1 = ypos
|
||||
var y2 = ypos + h
|
||||
|
||||
coords = append(coords, point{x1, y1, float32(ch.x-1) / f.atlasWidth, float32(ch.y-1) / f.atlasHeight})
|
||||
coords = append(coords, point{x2, y1, float32(ch.x+ch.width+1) / f.atlasWidth, float32(ch.y-1) / f.atlasHeight})
|
||||
coords = append(coords, point{x1, y2, float32(ch.x-1) / f.atlasWidth, float32(ch.y+ch.height+1) / f.atlasHeight})
|
||||
coords = append(coords, point{x2, y1, float32(ch.x+ch.width+1) / f.atlasWidth, float32(ch.y-1) / f.atlasHeight})
|
||||
coords = append(coords, point{x1, y2, float32(ch.x-1) / f.atlasWidth, float32(ch.y+ch.height+1) / f.atlasHeight})
|
||||
coords = append(coords, point{x2, y2, float32(ch.x+ch.width+1) / f.atlasWidth, float32(ch.y+ch.height+1) / f.atlasHeight})
|
||||
coords = append(coords, point{x1, y1, float32(ch.x) / f.atlasWidth, float32(ch.y) / f.atlasHeight})
|
||||
coords = append(coords, point{x2, y1, float32(ch.x+ch.width) / f.atlasWidth, float32(ch.y) / f.atlasHeight})
|
||||
coords = append(coords, point{x1, y2, float32(ch.x) / f.atlasWidth, float32(ch.y+ch.height) / f.atlasHeight})
|
||||
coords = append(coords, point{x2, y1, float32(ch.x+ch.width) / f.atlasWidth, float32(ch.y) / f.atlasHeight})
|
||||
coords = append(coords, point{x1, y2, float32(ch.x) / f.atlasWidth, float32(ch.y+ch.height) / f.atlasHeight})
|
||||
coords = append(coords, point{x2, y2, float32(ch.x+ch.width) / f.atlasWidth, float32(ch.y+ch.height) / f.atlasHeight})
|
||||
|
||||
// Now advance cursors for next glyph (note that advance is number of 1/64 pixels)
|
||||
x += float32((ch.advance >> 6)) * scale // Bitshift by 6 to get value in pixels (2^6 = 64 (divide amount of 1/64th pixels by 64 to get amount of pixels))
|
||||
@@ -316,36 +516,37 @@ func (f *Font) Printf(x, y float32, scale float32, fs string, argv ...interface{
|
||||
return nil
|
||||
}
|
||||
|
||||
// Width returns the width of a piece of text in pixels
|
||||
func (f *Font) Width(scale float32, fs string, argv ...interface{}) float32 {
|
||||
var width float32
|
||||
// Printf formats then draws a string to the screen.
|
||||
func (f *Font) Printf(x, y float32, scale float32, format string, argv ...interface{}) error {
|
||||
return f.Print(x, y, scale, fmt.Sprintf(format, argv...))
|
||||
}
|
||||
|
||||
indices := []rune(fmt.Sprintf(fs, argv...))
|
||||
// Width returns the width of a piece of text in pixels.
|
||||
func (f *Font) Width(scale float32, text string) float32 {
|
||||
var width float32
|
||||
scale *= fontRenderScale
|
||||
|
||||
indices := []rune(text)
|
||||
|
||||
if len(indices) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
lowChar := rune(32)
|
||||
|
||||
// Iterate through all characters in string
|
||||
// Iterate through all glyphs in string
|
||||
for i := range indices {
|
||||
|
||||
// Get rune
|
||||
runeIndex := indices[i]
|
||||
|
||||
// Skip runes that are not in font chacter range
|
||||
if int(runeIndex)-int(lowChar) > len(f.fontChar) || runeIndex < lowChar {
|
||||
fmt.Printf("%c %d\n", runeIndex, runeIndex)
|
||||
continue
|
||||
if f.face != nil {
|
||||
if advance, ok := f.face.GlyphAdvance(runeIndex); ok {
|
||||
width += float32(advance.Round()) * scale
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Find rune in fontChar list
|
||||
ch := f.fontChar[runeIndex-lowChar]
|
||||
|
||||
// Now advance cursors for next glyph (note that advance is number of 1/64 pixels)
|
||||
width += float32((ch.advance >> 6)) * scale // Bitshift by 6 to get value in pixels (2^6 = 64 (divide amount of 1/64th pixels by 64 to get amount of pixels))
|
||||
|
||||
if ch, ok := f.glyphs['?']; ok {
|
||||
width += float32((ch.advance >> 6)) * scale
|
||||
}
|
||||
}
|
||||
|
||||
return width
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package video
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
"github.com/go-gl/mathgl/mgl32"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
"github.com/libretro/ludo/state"
|
||||
)
|
||||
|
||||
// InitFramebuffer initializes and configures the video frame buffer based on
|
||||
// informations from the HWRenderCallback of the libretro core.
|
||||
func (video *Video) InitFramebuffer() {
|
||||
width := int32(video.Geom.MaxWidth)
|
||||
height := int32(video.Geom.MaxHeight)
|
||||
video.texWidth = width
|
||||
video.texHeight = height
|
||||
|
||||
log.Printf("[Video]: Initializing HW render (%v x %v).\n", width, height)
|
||||
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, video.texID)
|
||||
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, nil)
|
||||
|
||||
genFramebuffers(1, &video.fboID)
|
||||
bindFramebuffer(gl.FRAMEBUFFER, video.fboID)
|
||||
libretro.SetCurrentFramebufferValue(uintptr(video.fboID))
|
||||
framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, video.texID, 0)
|
||||
|
||||
hw := state.Core.HWRenderCallback
|
||||
if hw.Depth {
|
||||
genRenderbuffers(1, &video.rboID)
|
||||
bindRenderbuffer(gl.RENDERBUFFER, video.rboID)
|
||||
format := gl.DEPTH_COMPONENT16
|
||||
if hw.Stencil {
|
||||
format = gl.DEPTH24_STENCIL8
|
||||
}
|
||||
renderbufferStorage(gl.RENDERBUFFER, uint32(format), width, height)
|
||||
bindRenderbuffer(gl.RENDERBUFFER, 0)
|
||||
|
||||
// Separate attachment points work with both core and EXT framebuffers.
|
||||
framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, video.rboID)
|
||||
if hw.Stencil {
|
||||
framebufferRenderbuffer(gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, gl.RENDERBUFFER, video.rboID)
|
||||
}
|
||||
}
|
||||
|
||||
// Default origin is top left
|
||||
video.orthoMat = mgl32.Ortho2D(-1, 1, -1, 1)
|
||||
if hw.BottomLeftOrigin {
|
||||
video.orthoMat = mgl32.Ortho2D(-1, 1, 1, -1)
|
||||
}
|
||||
|
||||
if st := checkFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE {
|
||||
log.Fatalf("[Video] Framebuffer is not complete. Error: %v\n", st)
|
||||
}
|
||||
|
||||
gl.ClearColor(0, 0, 0, 1)
|
||||
if hw.Depth && hw.Stencil {
|
||||
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT)
|
||||
} else if hw.Depth {
|
||||
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
|
||||
} else {
|
||||
gl.Clear(gl.COLOR_BUFFER_BIT)
|
||||
}
|
||||
}
|
||||
|
||||
func bindBackbuffer() {
|
||||
bindFramebuffer(gl.FRAMEBUFFER, 0)
|
||||
}
|
||||
|
||||
func (video *Video) destroyFramebuffer() {
|
||||
bindBackbuffer()
|
||||
if video.rboID != 0 {
|
||||
deleteRenderbuffers(1, &video.rboID)
|
||||
video.rboID = 0
|
||||
}
|
||||
if video.fboID != 0 {
|
||||
deleteFramebuffers(1, &video.fboID)
|
||||
video.fboID = 0
|
||||
}
|
||||
}
|
||||
+51
-26
@@ -1,8 +1,10 @@
|
||||
package video
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/draw"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
@@ -19,6 +21,32 @@ func (color Color) Alpha(alpha float32) Color {
|
||||
return color
|
||||
}
|
||||
|
||||
func ColorFromHex(hex string) Color {
|
||||
if len(hex) != 7 || hex[0] != '#' {
|
||||
log.Printf("Invalid hex color: %s", hex)
|
||||
return Color{R: 0, G: 0, B: 0, A: 1}
|
||||
}
|
||||
var r, g, b uint8
|
||||
_, err := fmt.Sscanf(hex[1:], "%02x%02x%02x", &r, &g, &b)
|
||||
if err != nil {
|
||||
log.Printf("Failed to parse hex color %s: %v", hex, err)
|
||||
return Color{R: 0, G: 0, B: 0, A: 1}
|
||||
}
|
||||
return Color{
|
||||
R: float32(r) / 255.0,
|
||||
G: float32(g) / 255.0,
|
||||
B: float32(b) / 255.0,
|
||||
A: 1.0, // Default alpha value
|
||||
}
|
||||
}
|
||||
|
||||
func ColorToHex(c Color) string {
|
||||
r := uint8(c.R * 255)
|
||||
g := uint8(c.G * 255)
|
||||
b := uint8(c.B * 255)
|
||||
return fmt.Sprintf("#%02x%02x%02x", r, g, b)
|
||||
}
|
||||
|
||||
// XYWHTo4points converts coordinates from (x, y, width, height) to (x1, y1, x2, y2, x3, y3, x4, y4)
|
||||
func XYWHTo4points(x, y, w, h, fbh float32) (x1, y1, x2, y2, x3, y3, x4, y4 float32) {
|
||||
x1 = x
|
||||
@@ -66,12 +94,11 @@ func rotateUV(va []float32, rot uint) []float32 {
|
||||
}
|
||||
|
||||
// DrawImage draws an image with x, y, w, h
|
||||
func (video *Video) DrawImage(image uint32, x, y, w, h float32, scale float32, c Color) {
|
||||
|
||||
func (video *Video) DrawImage(image uint32, x, y, w, h, scale, r float32, c Color) {
|
||||
va := video.vertexArray(x, y, w, h, scale)
|
||||
|
||||
gl.UseProgram(video.demulProgram)
|
||||
gl.Uniform4f(gl.GetUniformLocation(video.demulProgram, gl.Str("color\x00")), c.R, c.G, c.B, c.A)
|
||||
gl.UseProgram(video.defaultProgram)
|
||||
gl.Uniform4f(gl.GetUniformLocation(video.defaultProgram, gl.Str("color\x00")), c.R, c.G, c.B, c.A)
|
||||
gl.Enable(gl.BLEND)
|
||||
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
|
||||
bindVertexArray(video.vao)
|
||||
@@ -97,11 +124,7 @@ func (video *Video) ScissorEnd() {
|
||||
gl.Disable(gl.SCISSOR_TEST)
|
||||
}
|
||||
|
||||
func (video *Video) vertexArray(x, y, w, h, scale float32) []float32 {
|
||||
fbw, fbh := video.Window.GetFramebufferSize()
|
||||
ffbw := float32(fbw)
|
||||
ffbh := float32(fbh)
|
||||
|
||||
func vertexArrayForFramebuffer(x, y, w, h, scale, ffbw, ffbh float32) []float32 {
|
||||
w *= scale
|
||||
h *= scale
|
||||
|
||||
@@ -116,9 +139,13 @@ func (video *Video) vertexArray(x, y, w, h, scale float32) []float32 {
|
||||
}
|
||||
}
|
||||
|
||||
func (video *Video) vertexArray(x, y, w, h, scale float32) []float32 {
|
||||
fbw, fbh := video.Window.GetFramebufferSize()
|
||||
return vertexArrayForFramebuffer(x, y, w, h, scale, float32(fbw), float32(fbh))
|
||||
}
|
||||
|
||||
// DrawBorder draws a colored rectangle border
|
||||
func (video *Video) DrawBorder(x, y, w, h, borderWidth float32, c Color) {
|
||||
|
||||
va := video.vertexArray(x, y, w, h, 1.0)
|
||||
|
||||
gl.UseProgram(video.borderProgram)
|
||||
@@ -138,7 +165,6 @@ func (video *Video) DrawBorder(x, y, w, h, borderWidth float32, c Color) {
|
||||
|
||||
// DrawRect draws a rectangle and supports rounded corners
|
||||
func (video *Video) DrawRect(x, y, w, h, r float32, c Color) {
|
||||
|
||||
va := video.vertexArray(x, y, w, h, 1.0)
|
||||
|
||||
gl.UseProgram(video.roundedProgram)
|
||||
@@ -158,7 +184,6 @@ func (video *Video) DrawRect(x, y, w, h, r float32, c Color) {
|
||||
|
||||
// DrawCircle draws a circle
|
||||
func (video *Video) DrawCircle(x, y, r float32, c Color) {
|
||||
|
||||
va := video.vertexArray(x-r, y-r, r*2, r*2, 1.0)
|
||||
|
||||
gl.UseProgram(video.circleProgram)
|
||||
@@ -175,28 +200,27 @@ func (video *Video) DrawCircle(x, y, r float32, c Color) {
|
||||
gl.Disable(gl.BLEND)
|
||||
}
|
||||
|
||||
func textureLoad(rgba *image.RGBA) uint32 {
|
||||
func textureLoad(nrgba *image.NRGBA) uint32 {
|
||||
var texture uint32
|
||||
gl.GenTextures(1, &texture)
|
||||
gl.ActiveTexture(gl.TEXTURE1)
|
||||
gl.BindTexture(gl.TEXTURE_2D, texture)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
|
||||
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, 0)
|
||||
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, (int32)(nrgba.Stride/4))
|
||||
gl.TexImage2D(
|
||||
gl.TEXTURE_2D,
|
||||
0,
|
||||
gl.RGBA,
|
||||
int32(rgba.Rect.Size().X),
|
||||
int32(rgba.Rect.Size().Y),
|
||||
int32(nrgba.Rect.Dx()),
|
||||
int32(nrgba.Rect.Dy()),
|
||||
0,
|
||||
gl.RGBA,
|
||||
gl.UNSIGNED_BYTE,
|
||||
gl.Ptr(rgba.Pix))
|
||||
gl.GenerateMipmap(gl.TEXTURE_2D)
|
||||
|
||||
gl.Ptr(nrgba.Pix))
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
return texture
|
||||
}
|
||||
|
||||
@@ -204,18 +228,19 @@ func textureLoad(rgba *image.RGBA) uint32 {
|
||||
func NewImage(file string) uint32 {
|
||||
imgFile, err := os.Open(file)
|
||||
if err != nil {
|
||||
log.Printf("failed to open image file %s: %v", file, err)
|
||||
return 0
|
||||
}
|
||||
defer imgFile.Close()
|
||||
|
||||
img, _, err := image.Decode(imgFile)
|
||||
if err != nil {
|
||||
log.Printf("failed to decode image file %s: %v", file, err)
|
||||
return 0
|
||||
}
|
||||
|
||||
rgba := image.NewRGBA(img.Bounds())
|
||||
if rgba.Stride != rgba.Rect.Size().X*4 {
|
||||
return 0
|
||||
}
|
||||
draw.Draw(rgba, rgba.Bounds(), img, image.Point{0, 0}, draw.Src)
|
||||
nrgba := image.NewNRGBA(img.Bounds())
|
||||
draw.Draw(nrgba, nrgba.Bounds(), img, image.Point{0, 0}, draw.Src)
|
||||
|
||||
return textureLoad(rgba)
|
||||
return textureLoad(nrgba)
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package video
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
"github.com/go-gl/glfw/v3.4/glfw"
|
||||
)
|
||||
|
||||
var (
|
||||
glslVersion = "#version 120\n"
|
||||
genVertexArrays = gl.GenVertexArrays
|
||||
bindVertexArray = gl.BindVertexArray
|
||||
genFramebuffers = gl.GenFramebuffers
|
||||
bindFramebuffer = gl.BindFramebuffer
|
||||
deleteFramebuffers = gl.DeleteFramebuffers
|
||||
framebufferTexture2D = gl.FramebufferTexture2D
|
||||
checkFramebufferStatus = gl.CheckFramebufferStatus
|
||||
genRenderbuffers = gl.GenRenderbuffers
|
||||
bindRenderbuffer = gl.BindRenderbuffer
|
||||
deleteRenderbuffers = gl.DeleteRenderbuffers
|
||||
renderbufferStorage = gl.RenderbufferStorage
|
||||
framebufferRenderbuffer = gl.FramebufferRenderbuffer
|
||||
)
|
||||
|
||||
// The frontend and hidden core context use the same hints. Select their API
|
||||
// from the actual context capabilities, including macOS's legacy extensions.
|
||||
func initGL() error {
|
||||
if err := gl.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
window := glfw.GetCurrentContext()
|
||||
major, minor := window.GetAttrib(glfw.ContextVersionMajor), window.GetAttrib(glfw.ContextVersionMinor)
|
||||
glslVersion = "#version 120\n"
|
||||
if major > 3 || (major == 3 && minor >= 2) {
|
||||
glslVersion = "#version 150\n"
|
||||
} else if major == 3 {
|
||||
glslVersion = "#version 130\n"
|
||||
}
|
||||
|
||||
genVertexArrays, bindVertexArray = gl.GenVertexArrays, gl.BindVertexArray
|
||||
genFramebuffers, bindFramebuffer, deleteFramebuffers = gl.GenFramebuffers, gl.BindFramebuffer, gl.DeleteFramebuffers
|
||||
framebufferTexture2D, checkFramebufferStatus = gl.FramebufferTexture2D, gl.CheckFramebufferStatus
|
||||
genRenderbuffers, bindRenderbuffer, deleteRenderbuffers = gl.GenRenderbuffers, gl.BindRenderbuffer, gl.DeleteRenderbuffers
|
||||
renderbufferStorage, framebufferRenderbuffer = gl.RenderbufferStorage, gl.FramebufferRenderbuffer
|
||||
|
||||
if major < 3 && !glfw.ExtensionSupported("GL_ARB_vertex_array_object") {
|
||||
if !glfw.ExtensionSupported("GL_APPLE_vertex_array_object") {
|
||||
return errors.New("OpenGL vertex array objects are not supported")
|
||||
}
|
||||
genVertexArrays, bindVertexArray = gl.GenVertexArraysAPPLE, gl.BindVertexArrayAPPLE
|
||||
}
|
||||
if major < 3 && !glfw.ExtensionSupported("GL_ARB_framebuffer_object") {
|
||||
if !glfw.ExtensionSupported("GL_EXT_framebuffer_object") {
|
||||
return errors.New("OpenGL framebuffer objects are not supported")
|
||||
}
|
||||
genFramebuffers, bindFramebuffer, deleteFramebuffers = gl.GenFramebuffersEXT, gl.BindFramebufferEXT, gl.DeleteFramebuffersEXT
|
||||
framebufferTexture2D, checkFramebufferStatus = gl.FramebufferTexture2DEXT, gl.CheckFramebufferStatusEXT
|
||||
genRenderbuffers, bindRenderbuffer, deleteRenderbuffers = gl.GenRenderbuffersEXT, gl.BindRenderbufferEXT, gl.DeleteRenderbuffersEXT
|
||||
renderbufferStorage, framebufferRenderbuffer = gl.RenderbufferStorageEXT, gl.FramebufferRenderbufferEXT
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package video
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/libretro/ludo/libretro"
|
||||
)
|
||||
|
||||
func TestDuplicateFramePreservesDimensions(t *testing.T) {
|
||||
v := &Video{}
|
||||
v.Refresh(libretro.HWFrameBufferValid, 320, 240, 0)
|
||||
v.Refresh(nil, 0, 0, 0)
|
||||
if v.width != 320 || v.height != 240 || v.data != libretro.HWFrameBufferValid || !v.needUpload {
|
||||
t.Fatal("NULL refresh discarded the pending frame")
|
||||
}
|
||||
v.needUpload = false
|
||||
v.Refresh(nil, 640, 480, 0)
|
||||
if v.width != 320 || v.height != 240 || v.needUpload {
|
||||
t.Fatal("NULL refresh changed the previously presented frame")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextDestroyWithoutCore(t *testing.T) {
|
||||
(&Video{}).RunContextDestroy()
|
||||
}
|
||||
+31
-33
@@ -1,6 +1,7 @@
|
||||
package video
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"image"
|
||||
"image/png"
|
||||
"os"
|
||||
@@ -13,41 +14,33 @@ import (
|
||||
"github.com/libretro/ludo/state"
|
||||
)
|
||||
|
||||
// During the TakeScreenshot step, we need to render the current game frame at
|
||||
// the right resolution to later capture it using ReadPixels. renderScreenshot
|
||||
// taking care of this.
|
||||
func (video *Video) renderScreenshot() {
|
||||
va := video.vertexArray(0, 0, float32(video.Geom.BaseWidth), float32(video.Geom.BaseHeight), 1.0)
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
|
||||
gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW)
|
||||
func (video *Video) captureBackbufferScreenshot(name string) error {
|
||||
outW, outH := video.Geom.BaseWidth, video.Geom.BaseHeight
|
||||
if outW < 1 || outH < 1 {
|
||||
return errors.New("invalid screenshot output size")
|
||||
}
|
||||
|
||||
bindVertexArray(video.vao)
|
||||
x := video.presentX
|
||||
y := video.presentY
|
||||
w := video.presentW
|
||||
h := video.presentH
|
||||
if !state.CoreRunning || video.width < 1 || video.height < 1 || w < 1 || h < 1 {
|
||||
return errors.New("no game frame to capture")
|
||||
}
|
||||
|
||||
gl.BindTexture(gl.TEXTURE_2D, video.texID)
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
|
||||
var oldReadBuffer int32
|
||||
gl.GetIntegerv(gl.READ_BUFFER, &oldReadBuffer)
|
||||
gl.ReadBuffer(gl.BACK)
|
||||
|
||||
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
|
||||
}
|
||||
img := image.NewRGBA(image.Rect(0, 0, int(w), int(h)))
|
||||
gl.Finish()
|
||||
gl.ReadPixels(x, y, w, h, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(img.Pix))
|
||||
gl.ReadBuffer(uint32(oldReadBuffer))
|
||||
|
||||
// TakeScreenshot captures the ouput of video.Render and writes it to a file
|
||||
func (video *Video) TakeScreenshot(name string) error {
|
||||
state.MenuActive = false
|
||||
defer func() { state.MenuActive = true }()
|
||||
|
||||
gl.UseProgram(video.defaultProgram)
|
||||
|
||||
video.renderScreenshot()
|
||||
|
||||
img := image.NewRGBA(image.Rect(0, 0, video.Geom.BaseWidth, video.Geom.BaseHeight))
|
||||
|
||||
_, fbh := video.Window.GetFramebufferSize()
|
||||
|
||||
gl.ReadPixels(
|
||||
0, int32(fbh-video.Geom.BaseHeight),
|
||||
int32(video.Geom.BaseWidth), int32(video.Geom.BaseHeight),
|
||||
gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(img.Pix))
|
||||
|
||||
gl.UseProgram(video.program)
|
||||
flipped := imaging.FlipV(img)
|
||||
if flipped.Bounds().Dx() != outW || flipped.Bounds().Dy() != outH {
|
||||
flipped = imaging.Resize(flipped, outW, outH, imaging.Linear)
|
||||
}
|
||||
|
||||
err := os.MkdirAll(settings.Current.ScreenshotsDirectory, os.ModePerm)
|
||||
if err != nil {
|
||||
@@ -59,8 +52,13 @@ func (video *Video) TakeScreenshot(name string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
flipped := imaging.FlipV(img)
|
||||
defer fd.Close()
|
||||
|
||||
return png.Encode(fd, flipped)
|
||||
}
|
||||
|
||||
// TakeScreenshotInFrontendFrame captures a screenshot while the caller already
|
||||
// owns a frontend frame and has already rendered the current game frame.
|
||||
func (video *Video) TakeScreenshotInFrontendFrame(name string) error {
|
||||
return video.captureBackbufferScreenshot(name)
|
||||
}
|
||||
+16
-1
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
"github.com/go-gl/mathgl/mgl32"
|
||||
)
|
||||
|
||||
func newProgram(vertexShaderSource, fragmentShaderSource string) (uint32, error) {
|
||||
@@ -22,6 +23,9 @@ func newProgram(vertexShaderSource, fragmentShaderSource string) (uint32, error)
|
||||
|
||||
gl.AttachShader(program, vertexShader)
|
||||
gl.AttachShader(program, fragmentShader)
|
||||
// Game and menu programs share a VAO, so their attribute locations must match.
|
||||
gl.BindAttribLocation(program, 0, gl.Str("vert\x00"))
|
||||
gl.BindAttribLocation(program, 1, gl.Str("vertTexCoord\x00"))
|
||||
gl.LinkProgram(program)
|
||||
|
||||
var status int32
|
||||
@@ -39,13 +43,24 @@ func newProgram(vertexShaderSource, fragmentShaderSource string) (uint32, error)
|
||||
gl.DeleteShader(vertexShader)
|
||||
gl.DeleteShader(fragmentShader)
|
||||
|
||||
// Only the core rendering code uses MVPs so we set it to identity
|
||||
// to avoid issues with other parts of the program
|
||||
gl.UseProgram(program)
|
||||
mvp := gl.GetUniformLocation(program, gl.Str("MVP\x00"))
|
||||
|
||||
if mvp != -1 {
|
||||
IdentityMatrix := mgl32.Ident4()
|
||||
gl.UniformMatrix4fv(mvp, 1, false, &IdentityMatrix[0])
|
||||
}
|
||||
gl.UseProgram(0)
|
||||
|
||||
return program, nil
|
||||
}
|
||||
|
||||
func compileShader(source string, shaderType uint32) (uint32, error) {
|
||||
shader := gl.CreateShader(shaderType)
|
||||
|
||||
csources, free := gl.Strs(source)
|
||||
csources, free := gl.Strs(glslVersion + source)
|
||||
gl.ShaderSource(shader, 1, csources, nil)
|
||||
free()
|
||||
gl.CompileShader(shader)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
//go:build gltest && linux
|
||||
|
||||
package video
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"runtime"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
"github.com/go-gl/glfw/v3.4/glfw"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func TestSoftwareFrameUploadBounds(t *testing.T) {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
if err := glfw.Init(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer glfw.Terminate()
|
||||
glfw.DefaultWindowHints()
|
||||
glfw.WindowHint(glfw.Visible, glfw.False)
|
||||
w, err := glfw.CreateWindow(32, 32, "Ludo upload test", nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer w.Destroy()
|
||||
w.MakeContextCurrent()
|
||||
if err := initGL(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
v := &Video{Geom: libretro.GameGeometry{MaxWidth: 64, MaxHeight: 64}}
|
||||
v.SetPixelFormat(libretro.PixelFormatXRGB8888)
|
||||
v.resetGameTexture(1, 1)
|
||||
defer gl.DeleteTextures(1, &v.texID)
|
||||
v.program, err = newProgram(vertexShader, defaultFragmentShader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer gl.DeleteProgram(v.program)
|
||||
|
||||
const width, height, pitch = 32, 32, 32 * 4
|
||||
page := unix.Getpagesize()
|
||||
memory, err := unix.Mmap(-1, 0, page*2, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_ANON|unix.MAP_PRIVATE)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer unix.Munmap(memory)
|
||||
if err := unix.Mprotect(memory[page:], unix.PROT_NONE); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The core supplies only the current frame, ending at a protected page.
|
||||
frame := memory[page-pitch*height : page]
|
||||
for i := range frame {
|
||||
frame[i] = byte(i)
|
||||
}
|
||||
v.Refresh(unsafe.Pointer(&frame[0]), width, height, pitch)
|
||||
v.uploadTexture()
|
||||
|
||||
var texWidth, texHeight int32
|
||||
gl.GetTexLevelParameteriv(gl.TEXTURE_2D, 0, gl.TEXTURE_WIDTH, &texWidth)
|
||||
gl.GetTexLevelParameteriv(gl.TEXTURE_2D, 0, gl.TEXTURE_HEIGHT, &texHeight)
|
||||
if texWidth != width || texHeight != height {
|
||||
t.Fatalf("texture size = %dx%d, want %dx%d", texWidth, texHeight, width, height)
|
||||
}
|
||||
pixels := make([]byte, len(frame))
|
||||
gl.GetTexImage(gl.TEXTURE_2D, 0, v.pixType, v.pixFmt, gl.Ptr(pixels))
|
||||
if !bytes.Equal(pixels, frame) {
|
||||
t.Error("uploaded pixels differ from the supplied frame")
|
||||
}
|
||||
if err := gl.GetError(); err != gl.NO_ERROR {
|
||||
t.Errorf("GL error: %#x", err)
|
||||
}
|
||||
}
|
||||
+631
-120
@@ -4,12 +4,14 @@
|
||||
package video
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"unsafe"
|
||||
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
"github.com/go-gl/glfw/v3.3/glfw"
|
||||
"github.com/go-gl/glfw/v3.4/glfw"
|
||||
"github.com/go-gl/mathgl/mgl32"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/state"
|
||||
@@ -17,9 +19,16 @@ import (
|
||||
|
||||
// Video holds the state of the video package
|
||||
type Video struct {
|
||||
Window *glfw.Window
|
||||
Geom libretro.GameGeometry
|
||||
Font *Font
|
||||
Window *glfw.Window
|
||||
HWWindow *glfw.Window // hidden core context; shares objects, not GL state, with Window
|
||||
Geom libretro.GameGeometry
|
||||
Font *Font
|
||||
FontSm *Font
|
||||
FontLg *Font
|
||||
title string
|
||||
|
||||
windowedX, windowedY int
|
||||
windowedWidth, windowedHeight int
|
||||
|
||||
program uint32 // current program used for the game quad
|
||||
defaultProgram uint32 // default program used for the game quad
|
||||
@@ -29,32 +38,217 @@ type Video struct {
|
||||
roundedProgram uint32 // program to draw rectangles with rounded corners
|
||||
borderProgram uint32 // program to draw rectangles borders
|
||||
circleProgram uint32 // program to draw textured circles
|
||||
demulProgram uint32 // program to draw premultiplied alpha images
|
||||
vao uint32
|
||||
vbo uint32
|
||||
texID uint32
|
||||
vao uint32 // vertex array object
|
||||
vbo uint32 // vertex buffer object
|
||||
texID uint32 // texture id
|
||||
fboID uint32 // framebuffer id for hw-render cores
|
||||
rboID uint32 // depth/stencil renderbuffer for hw-render cores
|
||||
identityMat mgl32.Mat4
|
||||
orthoMat mgl32.Mat4
|
||||
texWidth int32
|
||||
texHeight int32
|
||||
presentX int32
|
||||
presentY int32
|
||||
presentW int32
|
||||
presentH int32
|
||||
cachedFrameTexID uint32
|
||||
cachedFrameW int32
|
||||
cachedFrameH int32
|
||||
cachedFrameValid bool
|
||||
|
||||
pitch int32 // pitch set by the refresh callback
|
||||
pixFmt uint32 // format set by the environment callback
|
||||
pixType uint32
|
||||
bpp int32
|
||||
width, height int32 // dimensions set by the refresh callback
|
||||
rot uint
|
||||
pixType uint32 // pixel type for the core framebuffer
|
||||
bpp int32 // bit per pixel for the core framebuffer
|
||||
width, height int32 // dimensions set by the refresh callback
|
||||
rot uint // rotation index
|
||||
|
||||
needUpload bool // true when the texture needs to be uploaded to the GPU
|
||||
data unsafe.Pointer
|
||||
|
||||
hwContextType uint32
|
||||
hwContextMajor int
|
||||
hwContextMinor int
|
||||
hwDebugContext bool
|
||||
hwContextConfigured bool
|
||||
hwSync bool
|
||||
}
|
||||
|
||||
// Init instanciates the video package
|
||||
func Init(fullscreen bool) *Video {
|
||||
vid := &Video{}
|
||||
vid := &Video{title: "Ludo"}
|
||||
vid.identityMat = mgl32.Ident4()
|
||||
vid.Configure(fullscreen)
|
||||
return vid
|
||||
}
|
||||
|
||||
// Reconfigure destroys and recreates the window with new attributes
|
||||
func (video *Video) Reconfigure(fullscreen bool) {
|
||||
// SetFullscreen changes the window mode or monitor without recreating GL contexts.
|
||||
func (video *Video) SetFullscreen(fullscreen bool) {
|
||||
if fullscreen {
|
||||
if video.Window.GetMonitor() == nil {
|
||||
video.windowedWidth, video.windowedHeight = video.Window.GetSize()
|
||||
// Wayland does not expose window positions.
|
||||
if glfw.GetPlatform() != glfw.PlatformWayland {
|
||||
video.windowedX, video.windowedY = video.Window.GetPos()
|
||||
}
|
||||
}
|
||||
monitor := glfw.GetMonitors()[settings.Current.VideoMonitorIndex]
|
||||
modes := monitor.GetVideoModes()
|
||||
mode := modes[len(modes)-1]
|
||||
video.Window.SetMonitor(monitor, 0, 0, mode.Width, mode.Height, mode.RefreshRate)
|
||||
} else if video.Window.GetMonitor() != nil {
|
||||
video.Window.SetMonitor(nil, video.windowedX, video.windowedY,
|
||||
video.windowedWidth, video.windowedHeight, glfw.DontCare)
|
||||
}
|
||||
}
|
||||
|
||||
// ReconfigureHWContext recreates the visible window using the currently stored
|
||||
// HW context hints while preserving the current window size when windowed.
|
||||
func (video *Video) ReconfigureHWContext(fullscreen bool) error {
|
||||
width := 384 * 2
|
||||
height := 240 * 2
|
||||
if video.Window != nil && !fullscreen {
|
||||
width, height = video.Window.GetSize()
|
||||
if width < 1 {
|
||||
width = 384 * 2
|
||||
}
|
||||
if height < 1 {
|
||||
height = 240 * 2
|
||||
}
|
||||
}
|
||||
return video.configureWithSize(width, height, fullscreen)
|
||||
}
|
||||
|
||||
// CreateSharedHWContext creates a hidden context shared with the visible frontend context.
|
||||
func (video *Video) CreateSharedHWContext() error {
|
||||
if video.HWWindow != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
video.configureWindowHints()
|
||||
glfw.WindowHint(glfw.Visible, glfw.False)
|
||||
glfw.WindowHint(glfw.Focused, glfw.False)
|
||||
|
||||
width, height := video.Window.GetSize()
|
||||
if width < 1 {
|
||||
width = 1
|
||||
}
|
||||
if height < 1 {
|
||||
height = 1
|
||||
}
|
||||
|
||||
hwWindow, err := glfw.CreateWindow(width, height, "", nil, video.Window)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
video.HWWindow = hwWindow
|
||||
major := video.Window.GetAttrib(glfw.ContextVersionMajor)
|
||||
minor := video.Window.GetAttrib(glfw.ContextVersionMinor)
|
||||
video.hwSync = major > 3 || (major == 3 && minor >= 2) || glfw.ExtensionSupported("GL_ARB_sync")
|
||||
return nil
|
||||
}
|
||||
|
||||
// DestroySharedHWContext destroys the hidden shared context if it exists.
|
||||
func (video *Video) DestroySharedHWContext() {
|
||||
if video.HWWindow != nil {
|
||||
video.BeginCore()
|
||||
video.destroyFramebuffer()
|
||||
libretro.SetCurrentFramebufferValue(0)
|
||||
video.EndCore()
|
||||
video.HWWindow.Destroy()
|
||||
video.HWWindow = nil
|
||||
}
|
||||
}
|
||||
|
||||
// MakeFrontendContextCurrent switches back to the visible frontend context.
|
||||
func (video *Video) MakeFrontendContextCurrent() {
|
||||
if video.Window != nil {
|
||||
video.Window.Destroy()
|
||||
video.Window.MakeContextCurrent()
|
||||
}
|
||||
}
|
||||
|
||||
// RunContextDestroy notifies the active core before its GL context is destroyed.
|
||||
func (video *Video) RunContextDestroy() {
|
||||
if !state.CoreRunning || state.Core == nil || video.HWWindow == nil {
|
||||
return
|
||||
}
|
||||
hw := state.Core.HWRenderCallback
|
||||
if hw != nil && hw.ContextDestroy != nil {
|
||||
video.BeginCore()
|
||||
hw.ContextDestroy()
|
||||
video.EndCore()
|
||||
}
|
||||
}
|
||||
|
||||
// ResetHWContext creates the core's render target and resets its renderer.
|
||||
func (video *Video) ResetHWContext() error {
|
||||
if err := video.CreateSharedHWContext(); err != nil {
|
||||
return err
|
||||
}
|
||||
video.BeginCore()
|
||||
defer video.EndCore()
|
||||
video.InitFramebuffer()
|
||||
state.Core.HWRenderCallback.ContextReset()
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeginCore enters the isolated context for run, reset, serialization or teardown.
|
||||
func (video *Video) BeginCore() {
|
||||
if video.HWWindow != nil {
|
||||
video.switchContext(video.HWWindow)
|
||||
}
|
||||
}
|
||||
|
||||
// EndCore returns to the frontend after the core's GL work.
|
||||
func (video *Video) EndCore() {
|
||||
if video.HWWindow != nil {
|
||||
video.switchContext(video.Window)
|
||||
}
|
||||
}
|
||||
|
||||
// ContextCallbacks returns paired release/acquire callbacks for synchronous
|
||||
// context handoffs between the frontend and native core threads.
|
||||
func (video *Video) ContextCallbacks() (release, acquire func()) {
|
||||
var current *glfw.Window
|
||||
release = func() {
|
||||
current = nil
|
||||
if video.Window == nil || state.Core == nil || state.Core.HWRenderCallback == nil {
|
||||
return
|
||||
}
|
||||
current = glfw.GetCurrentContext()
|
||||
// Before context_reset, HW negotiation may still use the visible context.
|
||||
// Once the hidden context exists, leave the menu context on its own thread.
|
||||
if video.HWWindow != nil && current != video.HWWindow {
|
||||
current = nil
|
||||
}
|
||||
if current != nil {
|
||||
glfw.DetachCurrentContext()
|
||||
}
|
||||
}
|
||||
acquire = func() {
|
||||
if current != nil {
|
||||
current.MakeContextCurrent()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (video *Video) switchContext(window *glfw.Window) {
|
||||
if glfw.GetCurrentContext() == window {
|
||||
return
|
||||
}
|
||||
// Order shared-texture writes and reads in both directions without a CPU wait.
|
||||
if video.hwSync {
|
||||
fence := gl.FenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0)
|
||||
gl.Flush()
|
||||
window.MakeContextCurrent()
|
||||
gl.WaitSync(fence, 0, gl.TIMEOUT_IGNORED)
|
||||
gl.DeleteSync(fence)
|
||||
} else {
|
||||
gl.Finish()
|
||||
window.MakeContextCurrent()
|
||||
}
|
||||
video.Configure(fullscreen)
|
||||
}
|
||||
|
||||
// GetFramebufferSize retrieves the size, in pixels, of the framebuffer of the specified window.
|
||||
@@ -67,12 +261,69 @@ func (video *Video) GetFramebufferSize() (int, int) {
|
||||
|
||||
// SetTitle sets the window title, encoded as UTF-8, of the window.
|
||||
func (video *Video) SetTitle(title string) {
|
||||
video.title = title
|
||||
if video.Window == nil {
|
||||
return
|
||||
}
|
||||
video.Window.SetTitle(title)
|
||||
}
|
||||
|
||||
// SetHWRenderContext stores the context requested by the current libretro core.
|
||||
func (video *Video) SetHWRenderContext(hw *libretro.HWRenderCallback) {
|
||||
if hw == nil {
|
||||
video.hwContextType = 0
|
||||
video.hwContextMajor = 0
|
||||
video.hwContextMinor = 0
|
||||
video.hwDebugContext = false
|
||||
video.hwContextConfigured = false
|
||||
return
|
||||
}
|
||||
|
||||
video.hwContextType = hw.HWContextType
|
||||
video.hwContextMajor = int(hw.VersionMajor)
|
||||
video.hwContextMinor = int(hw.VersionMinor)
|
||||
video.hwDebugContext = hw.DebugContext
|
||||
video.hwContextConfigured = true
|
||||
}
|
||||
|
||||
func (video *Video) configureWindowHints() {
|
||||
glfw.DefaultWindowHints()
|
||||
|
||||
if !video.hwContextConfigured {
|
||||
return
|
||||
}
|
||||
|
||||
switch video.hwContextType {
|
||||
case libretro.HWContextOpenGLCore:
|
||||
major := video.hwContextMajor
|
||||
minor := video.hwContextMinor
|
||||
glfw.WindowHint(glfw.ClientAPI, glfw.OpenGLAPI)
|
||||
glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLAnyProfile)
|
||||
if major > 0 {
|
||||
glfw.WindowHint(glfw.ContextVersionMajor, major)
|
||||
glfw.WindowHint(glfw.ContextVersionMinor, minor)
|
||||
if major > 3 || (major == 3 && minor >= 2) {
|
||||
glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
|
||||
glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True)
|
||||
}
|
||||
}
|
||||
case libretro.HWContextOpenGL:
|
||||
glfw.WindowHint(glfw.ClientAPI, glfw.OpenGLAPI)
|
||||
if video.hwContextMajor > 0 {
|
||||
glfw.WindowHint(glfw.ContextVersionMajor, video.hwContextMajor)
|
||||
glfw.WindowHint(glfw.ContextVersionMinor, video.hwContextMinor)
|
||||
if video.hwContextMajor > 3 || (video.hwContextMajor == 3 && video.hwContextMinor >= 2) {
|
||||
glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCompatProfile)
|
||||
glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.False)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if video.hwDebugContext {
|
||||
glfw.WindowHint(glfw.OpenGLDebugContext, glfw.True)
|
||||
}
|
||||
}
|
||||
|
||||
// SetShouldClose sets the value of the close flag of the window.
|
||||
func (video *Video) SetShouldClose(b bool) {
|
||||
if video.Window == nil {
|
||||
@@ -81,37 +332,92 @@ func (video *Video) SetShouldClose(b bool) {
|
||||
video.Window.SetShouldClose(b)
|
||||
}
|
||||
|
||||
func panicOnErr(v uint32, err error) uint32 {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (video *Video) ensurePixelFormatDefaults() {
|
||||
if video.pixFmt == 0 {
|
||||
video.pixFmt = gl.UNSIGNED_SHORT_5_5_5_1
|
||||
video.pixType = gl.BGRA
|
||||
video.bpp = 2
|
||||
}
|
||||
}
|
||||
|
||||
func (video *Video) resetGameTexture(width, height int32) {
|
||||
if width < 1 {
|
||||
width = 1
|
||||
}
|
||||
if height < 1 {
|
||||
height = 1
|
||||
}
|
||||
|
||||
if video.texID != 0 {
|
||||
gl.DeleteTextures(1, &video.texID)
|
||||
video.texID = 0
|
||||
}
|
||||
|
||||
gl.GenTextures(1, &video.texID)
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, video.texID)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
|
||||
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, video.pixType, video.pixFmt, nil)
|
||||
video.texWidth = width
|
||||
video.texHeight = height
|
||||
}
|
||||
|
||||
// Configure instanciates the video package
|
||||
func (video *Video) Configure(fullscreen bool) {
|
||||
var width, height int
|
||||
var m *glfw.Monitor
|
||||
|
||||
if fullscreen {
|
||||
m = glfw.GetMonitors()[settings.Current.VideoMonitorIndex]
|
||||
vms := m.GetVideoModes()
|
||||
vm := vms[len(vms)-1]
|
||||
width = vm.Width
|
||||
height = vm.Height
|
||||
} else {
|
||||
width = 320 * 3
|
||||
height = 180 * 3
|
||||
}
|
||||
|
||||
var err error
|
||||
video.Window, err = glfw.CreateWindow(width, height, "Ludo", m, nil)
|
||||
if err != nil {
|
||||
if err := video.configureWithSize(384*2, 240*2, fullscreen); err != nil {
|
||||
panic("Window creation failed:" + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (video *Video) configureWithSize(width, height int, fullscreen bool) error {
|
||||
if fullscreen && video.windowedWidth != 0 {
|
||||
width, height = video.windowedWidth, video.windowedHeight
|
||||
}
|
||||
video.configureWindowHints()
|
||||
window, err := glfw.CreateWindow(width, height, video.title, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// GLFW may report a platform error without returning it through the Go binding.
|
||||
if window == nil {
|
||||
return errors.New("window creation failed")
|
||||
}
|
||||
|
||||
// Keep the old context and its resources until the replacement exists.
|
||||
if video.Window != nil {
|
||||
video.RunContextDestroy()
|
||||
video.DestroySharedHWContext()
|
||||
video.Window.Destroy()
|
||||
}
|
||||
video.Window = window
|
||||
|
||||
// Object names from the old share group must not be reused in this context.
|
||||
video.texID = 0
|
||||
video.cachedFrameTexID = 0
|
||||
video.cachedFrameW, video.cachedFrameH = 0, 0
|
||||
video.cachedFrameValid = false
|
||||
video.width, video.height = 0, 0
|
||||
video.data = nil
|
||||
video.needUpload = false
|
||||
video.Window.MakeContextCurrent()
|
||||
|
||||
// Force a minimum size for the window.
|
||||
video.Window.SetSizeLimits(160, 120, glfw.DontCare, glfw.DontCare)
|
||||
video.SetFullscreen(fullscreen)
|
||||
|
||||
video.Window.SetInputMode(glfw.CursorMode, glfw.CursorHidden)
|
||||
|
||||
// Initialize Glow
|
||||
if err := gl.Init(); err != nil {
|
||||
if err := initGL(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -119,54 +425,31 @@ func (video *Video) Configure(fullscreen bool) {
|
||||
|
||||
// LoadFont (fontfile, font scale, window width, window height)
|
||||
fontPath := filepath.Join(settings.Current.AssetsDirectory, "font.ttf")
|
||||
video.Font, err = LoadFont(fontPath, int32(36*2), fbw, fbh)
|
||||
video.Font, err = LoadFont(fontPath, int32(36), fbw, fbh)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
video.FontSm, err = LoadFont(fontPath, int32(29), fbw, fbh)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
video.FontLg, err = LoadFont(fontPath, int32(50), fbw, fbh)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Configure the vertex and fragment shaders
|
||||
video.defaultProgram, err = newProgram(vertexShader, defaultFragmentShader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Configure the vertex and fragment shader
|
||||
video.defaultProgram = panicOnErr(newProgram(vertexShader, defaultFragmentShader))
|
||||
video.sharpBilinearProgram = panicOnErr(newProgram(vertexShader, sharpBilinearFragmentShader))
|
||||
video.zfastCRTProgram = panicOnErr(newProgram(vertexShader, zfastCRTFragmentShader))
|
||||
video.zfastLCDProgram = panicOnErr(newProgram(vertexShader, zfastLCDFragmentShader))
|
||||
video.roundedProgram = panicOnErr(newProgram(vertexShader, roundedFragmentShader))
|
||||
video.borderProgram = panicOnErr(newProgram(vertexShader, borderFragmentShader))
|
||||
video.circleProgram = panicOnErr(newProgram(vertexShader, circleFragmentShader))
|
||||
|
||||
video.sharpBilinearProgram, err = newProgram(vertexShader, sharpBilinearFragmentShader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
video.zfastCRTProgram, err = newProgram(vertexShader, zfastCRTFragmentShader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
video.zfastLCDProgram, err = newProgram(vertexShader, zfastLCDFragmentShader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
video.roundedProgram, err = newProgram(vertexShader, roundedFragmentShader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
video.borderProgram, err = newProgram(vertexShader, borderFragmentShader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
video.circleProgram, err = newProgram(vertexShader, circleFragmentShader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
video.demulProgram, err = newProgram(vertexShader, demulFragmentShader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
video.UpdateFilter(settings.Current.VideoFilter)
|
||||
video.program = video.defaultProgram
|
||||
|
||||
gl.UseProgram(video.program)
|
||||
textureUniform := gl.GetUniformLocation(video.program, gl.Str("Texture\x00"))
|
||||
gl.Uniform1i(textureUniform, 0)
|
||||
|
||||
@@ -180,35 +463,40 @@ func (video *Video) Configure(fullscreen bool) {
|
||||
|
||||
vertAttrib := uint32(gl.GetAttribLocation(video.program, gl.Str("vert\x00")))
|
||||
gl.EnableVertexAttribArray(vertAttrib)
|
||||
gl.VertexAttribPointer(vertAttrib, 2, gl.FLOAT, false, 4*4, gl.PtrOffset(0))
|
||||
gl.VertexAttribPointerWithOffset(vertAttrib, 2, gl.FLOAT, false, 4*4, 0)
|
||||
|
||||
texCoordAttrib := uint32(gl.GetAttribLocation(video.program, gl.Str("vertTexCoord\x00")))
|
||||
gl.EnableVertexAttribArray(texCoordAttrib)
|
||||
gl.VertexAttribPointer(texCoordAttrib, 2, gl.FLOAT, false, 4*4, gl.PtrOffset(2*4))
|
||||
gl.VertexAttribPointerWithOffset(texCoordAttrib, 2, gl.FLOAT, false, 4*4, 2*4)
|
||||
|
||||
// Some cores won't call SetPixelFormat, provide default values
|
||||
if video.pixFmt == 0 {
|
||||
video.pixFmt = gl.UNSIGNED_SHORT_5_5_5_1
|
||||
video.pixType = gl.BGRA
|
||||
video.bpp = 2
|
||||
video.ensurePixelFormatDefaults()
|
||||
|
||||
if video.Geom.MaxWidth == 0 || video.Geom.MaxHeight == 0 {
|
||||
video.Geom.MaxWidth = video.Geom.BaseWidth
|
||||
video.Geom.MaxHeight = video.Geom.BaseHeight
|
||||
}
|
||||
|
||||
gl.GenTextures(1, &video.texID)
|
||||
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
if video.texID == 0 && state.Verbose {
|
||||
log.Println("[Video]: Failed to create the vid texture")
|
||||
video.texWidth = int32(video.Geom.MaxWidth)
|
||||
video.texHeight = int32(video.Geom.MaxHeight)
|
||||
if video.texWidth < 1 {
|
||||
video.texWidth = 1
|
||||
}
|
||||
|
||||
gl.BindTexture(gl.TEXTURE_2D, video.texID)
|
||||
if video.texHeight < 1 {
|
||||
video.texHeight = 1
|
||||
}
|
||||
video.resetGameTexture(video.texWidth, video.texHeight)
|
||||
|
||||
video.UpdateFilter(settings.Current.VideoFilter)
|
||||
|
||||
video.coreRatioViewport(fbw, fbh)
|
||||
video.coreRatioViewport(fbw, fbh, video.Geom.BaseWidth, video.Geom.BaseHeight)
|
||||
|
||||
if e := gl.GetError(); e != gl.NO_ERROR {
|
||||
bindVertexArray(0)
|
||||
|
||||
for e := gl.GetError(); e != gl.NO_ERROR; e = gl.NO_ERROR {
|
||||
log.Printf("[Video] OpenGL error: %d\n", e)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateFilter configures the game texture filter and shader. We currently
|
||||
@@ -219,6 +507,7 @@ func (video *Video) Configure(fullscreen bool) {
|
||||
// CRT: zfast-crt
|
||||
// LCD: zfast-lcd
|
||||
func (video *Video) UpdateFilter(filter string) {
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, video.texID)
|
||||
switch filter {
|
||||
case "Smooth":
|
||||
@@ -247,8 +536,9 @@ func (video *Video) UpdateFilter(filter string) {
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
|
||||
gl.UseProgram(video.program)
|
||||
gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("TextureSize\x00")), float32(video.width), float32(video.height))
|
||||
gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("TextureSize\x00")), float32(video.texWidth), float32(video.texHeight))
|
||||
gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("InputSize\x00")), float32(video.width), float32(video.height))
|
||||
gl.UseProgram(0)
|
||||
}
|
||||
|
||||
// SetPixelFormat is a callback passed to the libretro implementation.
|
||||
@@ -259,7 +549,7 @@ func (video *Video) SetPixelFormat(format uint32) bool {
|
||||
}
|
||||
|
||||
// PixelStorei also needs to be updated whenever bpp changes
|
||||
defer gl.PixelStorei(gl.UNPACK_ROW_LENGTH, video.pitch/video.bpp)
|
||||
defer func() { video.needUpload = true }()
|
||||
|
||||
switch format {
|
||||
case libretro.PixelFormat0RGB1555:
|
||||
@@ -296,9 +586,38 @@ func (video *Video) ResetRot() {
|
||||
video.rot = 0
|
||||
}
|
||||
|
||||
// coreRatioViewport configures the vertex array to display the game at the center of the window
|
||||
// while preserving the original ascpect ratio of the game or core
|
||||
func (video *Video) coreRatioViewport(fbWidth int, fbHeight int) (x, y, w, h float32) {
|
||||
// ResetCoreState clears per-core video state and destroys its GL context.
|
||||
func (video *Video) ResetCoreState() {
|
||||
video.DestroySharedHWContext()
|
||||
video.MakeFrontendContextCurrent()
|
||||
libretro.SetCurrentFramebufferValue(0)
|
||||
video.ResetPitch()
|
||||
video.ResetRot()
|
||||
video.needUpload = false
|
||||
video.data = nil
|
||||
video.width = 0
|
||||
video.height = 0
|
||||
video.pixFmt = 0
|
||||
video.pixType = 0
|
||||
video.bpp = 0
|
||||
video.ensurePixelFormatDefaults()
|
||||
if video.texID != 0 {
|
||||
video.resetGameTexture(1, 1)
|
||||
video.UpdateFilter(settings.Current.VideoFilter)
|
||||
}
|
||||
video.texWidth = 0
|
||||
video.texHeight = 0
|
||||
if video.cachedFrameTexID != 0 {
|
||||
gl.DeleteTextures(1, &video.cachedFrameTexID)
|
||||
video.cachedFrameTexID = 0
|
||||
}
|
||||
video.cachedFrameW = 0
|
||||
video.cachedFrameH = 0
|
||||
video.cachedFrameValid = false
|
||||
}
|
||||
|
||||
// gameViewport fits the game to the framebuffer and records its screenshot bounds.
|
||||
func (video *Video) gameViewport(fbWidth, fbHeight int) (x, y, w, h float32) {
|
||||
// Scale the content to fit in the viewport.
|
||||
fbw := float32(fbWidth)
|
||||
fbh := float32(fbHeight)
|
||||
@@ -316,11 +635,36 @@ func (video *Video) coreRatioViewport(fbWidth int, fbHeight int) (x, y, w, h flo
|
||||
w = fbw
|
||||
}
|
||||
|
||||
// Place the content in the middle of the window.
|
||||
x = (fbw - w) / 2
|
||||
y = (fbh - h) / 2
|
||||
// Keep centered, pixel-aligned bounds so the cached copy needs no resampling.
|
||||
x = float32(int32((fbw - w) / 2))
|
||||
y = float32(int32((fbh - h) / 2))
|
||||
w = fbw - 2*x
|
||||
h = fbh - 2*y
|
||||
|
||||
video.presentX = int32(x)
|
||||
video.presentY = int32(y)
|
||||
video.presentW = int32(w)
|
||||
video.presentH = int32(h)
|
||||
return
|
||||
}
|
||||
|
||||
// coreRatioViewport configures the game quad's placement and core texture coordinates.
|
||||
func (video *Video) coreRatioViewport(fbWidth, fbHeight, clipWidth, clipHeight int) (x, y, w, h float32) {
|
||||
x, y, w, h = video.gameViewport(fbWidth, fbHeight)
|
||||
va := vertexArrayForFramebuffer(x, y, w, h, 1.0, float32(fbWidth), float32(fbHeight))
|
||||
|
||||
if clipWidth == 0 {
|
||||
clipWidth = int(video.texWidth)
|
||||
}
|
||||
if clipHeight == 0 {
|
||||
clipHeight = int(video.texHeight)
|
||||
}
|
||||
|
||||
va[3] = float32(clipHeight) / float32(video.texHeight)
|
||||
va[10] = float32(clipWidth) / float32(video.texWidth)
|
||||
va[11] = va[3]
|
||||
va[14] = va[10]
|
||||
|
||||
va := video.vertexArray(x, y, w, h, 1.0)
|
||||
va = rotateUV(va, video.rot)
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
|
||||
gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW)
|
||||
@@ -328,59 +672,226 @@ func (video *Video) coreRatioViewport(fbWidth int, fbHeight int) (x, y, w, h flo
|
||||
return
|
||||
}
|
||||
|
||||
// BeginFrontendFrame sets the state used by Ludo's game quad and menu.
|
||||
// Core GL state lives in HWWindow and is never modified here.
|
||||
func (video *Video) BeginFrontendFrame() {
|
||||
// Render directly to the screen.
|
||||
bindBackbuffer()
|
||||
gl.DrawBuffer(gl.BACK)
|
||||
gl.ReadBuffer(gl.BACK)
|
||||
|
||||
// Establish a frontend-owned GL baseline before drawing the game quad/menu.
|
||||
gl.Disable(gl.DEPTH_TEST)
|
||||
gl.Disable(gl.CULL_FACE)
|
||||
gl.Disable(gl.DITHER)
|
||||
gl.Disable(gl.SCISSOR_TEST)
|
||||
gl.Disable(gl.STENCIL_TEST)
|
||||
gl.Disable(gl.BLEND)
|
||||
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
|
||||
gl.BlendEquation(gl.FUNC_ADD)
|
||||
gl.ColorMask(true, true, true, true)
|
||||
gl.DepthMask(true)
|
||||
|
||||
video.ResizeViewport()
|
||||
}
|
||||
|
||||
// ResizeViewport resizes the GL viewport to the framebuffer size
|
||||
func (video *Video) ResizeViewport() {
|
||||
fbw, fbh := video.Window.GetFramebufferSize()
|
||||
gl.Viewport(0, 0, int32(fbw), int32(fbh))
|
||||
if video.Font != nil {
|
||||
video.Font.UpdateResolution(fbw, fbh)
|
||||
}
|
||||
if video.FontSm != nil {
|
||||
video.FontSm.UpdateResolution(fbw, fbh)
|
||||
}
|
||||
if video.FontLg != nil {
|
||||
video.FontLg.UpdateResolution(fbw, fbh)
|
||||
}
|
||||
}
|
||||
|
||||
// Render the current frame
|
||||
// Render draws the current game frame using the frontend context/state.
|
||||
func (video *Video) Render() {
|
||||
if !state.CoreRunning {
|
||||
gl.ClearColor(1, 1, 1, 1)
|
||||
gl.Clear(gl.COLOR_BUFFER_BIT)
|
||||
return
|
||||
}
|
||||
|
||||
gl.ClearColor(0, 0, 0, 1)
|
||||
gl.Clear(gl.COLOR_BUFFER_BIT)
|
||||
|
||||
// Early return to not render the first frame of a newly loaded game with the
|
||||
// previous game pitch. A sane pitch must be set by video.Refresh first.
|
||||
if video.pitch == 0 {
|
||||
// Wait for a valid refresh, not merely an allocated HW framebuffer.
|
||||
if video.width == 0 || video.height == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
video.uploadTexture()
|
||||
|
||||
fbw, fbh := video.Window.GetFramebufferSize()
|
||||
_, _, w, h := video.coreRatioViewport(fbw, fbh)
|
||||
_, _, w, h := video.coreRatioViewport(fbw, fbh, int(video.width), int(video.height))
|
||||
|
||||
gl.UseProgram(video.program)
|
||||
if loc := gl.GetUniformLocation(video.program, gl.Str("color\x00")); loc >= 0 {
|
||||
gl.Uniform4f(loc, 1, 1, 1, 1)
|
||||
}
|
||||
gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("OutputSize\x00")), w, h)
|
||||
|
||||
bindVertexArray(video.vao)
|
||||
if state.Core.HWRenderCallback != nil {
|
||||
gl.UniformMatrix4fv(gl.GetUniformLocation(video.program, gl.Str("MVP\x00")), 1, false, &video.orthoMat[0])
|
||||
}
|
||||
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, video.texID)
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
|
||||
|
||||
bindVertexArray(video.vao)
|
||||
// Preserve opaque alpha even when the core leaves its alpha channel unset.
|
||||
gl.ColorMask(true, true, true, false)
|
||||
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
|
||||
gl.ColorMask(true, true, true, true)
|
||||
bindVertexArray(0)
|
||||
|
||||
// Reset MVP to identity to avoid menu issues
|
||||
gl.UniformMatrix4fv(gl.GetUniformLocation(video.program, gl.Str("MVP\x00")), 1, false, &video.identityMat[0])
|
||||
gl.UseProgram(0)
|
||||
}
|
||||
|
||||
func (video *Video) ensureCachedFrameTexture(width, height int32) {
|
||||
if width < 1 || height < 1 {
|
||||
return
|
||||
}
|
||||
|
||||
if video.cachedFrameTexID == 0 {
|
||||
gl.GenTextures(1, &video.cachedFrameTexID)
|
||||
}
|
||||
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, video.cachedFrameTexID)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
|
||||
|
||||
if video.cachedFrameW != width || video.cachedFrameH != height {
|
||||
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, nil)
|
||||
video.cachedFrameW = width
|
||||
video.cachedFrameH = height
|
||||
}
|
||||
}
|
||||
|
||||
// CachePresentedFrame copies the game rectangle, without borders or UI, so menu-mode
|
||||
// rendering does not depend on the core keeping its paused framebuffer alive.
|
||||
func (video *Video) CachePresentedFrame() {
|
||||
if !state.CoreRunning || video.width == 0 || video.height == 0 {
|
||||
video.cachedFrameValid = false
|
||||
return
|
||||
}
|
||||
|
||||
if video.presentW < 1 || video.presentH < 1 {
|
||||
video.cachedFrameValid = false
|
||||
return
|
||||
}
|
||||
|
||||
video.ensureCachedFrameTexture(video.presentW, video.presentH)
|
||||
if video.cachedFrameTexID == 0 {
|
||||
video.cachedFrameValid = false
|
||||
return
|
||||
}
|
||||
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, video.cachedFrameTexID)
|
||||
gl.CopyTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, video.presentX, video.presentY, video.presentW, video.presentH)
|
||||
video.cachedFrameValid = true
|
||||
}
|
||||
|
||||
// RenderCachedFrame draws the last cached frontend-presented game frame.
|
||||
func (video *Video) RenderCachedFrame() bool {
|
||||
if !video.cachedFrameValid || video.cachedFrameTexID == 0 || video.cachedFrameW < 1 || video.cachedFrameH < 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
gl.ClearColor(0, 0, 0, 1)
|
||||
gl.Clear(gl.COLOR_BUFFER_BIT)
|
||||
|
||||
fbw, fbh := video.Window.GetFramebufferSize()
|
||||
x, y, w, h := video.gameViewport(fbw, fbh)
|
||||
va := vertexArrayForFramebuffer(x, y, w, h, 1.0, float32(fbw), float32(fbh))
|
||||
// A backbuffer copy always has bottom-left origin, regardless of the core.
|
||||
va[3] = 0
|
||||
va[7] = 1
|
||||
va[11] = 0
|
||||
va[15] = 1
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
|
||||
gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW)
|
||||
|
||||
gl.UseProgram(video.defaultProgram)
|
||||
gl.Uniform4f(gl.GetUniformLocation(video.defaultProgram, gl.Str("color\x00")), 1, 1, 1, 1)
|
||||
gl.Uniform2f(gl.GetUniformLocation(video.defaultProgram, gl.Str("OutputSize\x00")), w, h)
|
||||
gl.Uniform2f(gl.GetUniformLocation(video.defaultProgram, gl.Str("TextureSize\x00")), float32(video.cachedFrameW), float32(video.cachedFrameH))
|
||||
gl.Uniform2f(gl.GetUniformLocation(video.defaultProgram, gl.Str("InputSize\x00")), float32(video.cachedFrameW), float32(video.cachedFrameH))
|
||||
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, video.cachedFrameTexID)
|
||||
bindVertexArray(video.vao)
|
||||
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
|
||||
bindVertexArray(0)
|
||||
gl.UseProgram(0)
|
||||
return true
|
||||
}
|
||||
|
||||
// RenderPresentedFrame redraws the current game image for menu-mode frontend
|
||||
// rendering, preferring the cached last-good frame when available.
|
||||
func (video *Video) RenderPresentedFrame() {
|
||||
if !video.RenderCachedFrame() {
|
||||
video.Render()
|
||||
video.CachePresentedFrame()
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh the texture framebuffer
|
||||
func (video *Video) Refresh(data unsafe.Pointer, width int32, height int32, pitch int32) {
|
||||
video.width = width
|
||||
video.height = height
|
||||
video.pitch = pitch
|
||||
|
||||
gl.BindTexture(gl.TEXTURE_2D, video.texID)
|
||||
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, video.pitch/video.bpp)
|
||||
|
||||
gl.UseProgram(video.program)
|
||||
gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("TextureSize\x00")), float32(width), float32(height))
|
||||
gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("InputSize\x00")), float32(width), float32(height))
|
||||
|
||||
// NULL duplicates the last frame; its dimensions need not be supplied.
|
||||
if data == nil {
|
||||
return
|
||||
}
|
||||
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, video.pixType, video.pixFmt, data)
|
||||
video.needUpload = true
|
||||
video.width = width
|
||||
video.height = height
|
||||
video.pitch = pitch
|
||||
video.data = data
|
||||
}
|
||||
|
||||
func (video *Video) uploadTexture() {
|
||||
if !video.needUpload || video.data == nil {
|
||||
return
|
||||
}
|
||||
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, video.texID)
|
||||
|
||||
if video.data != libretro.HWFrameBufferValid {
|
||||
video.texWidth = video.width
|
||||
video.texHeight = video.height
|
||||
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, video.pitch/video.bpp)
|
||||
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, video.texWidth, video.texHeight, 0, video.pixType, video.pixFmt, video.data)
|
||||
}
|
||||
|
||||
gl.UseProgram(video.program)
|
||||
gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("TextureSize\x00")), float32(video.texWidth), float32(video.texHeight))
|
||||
gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("InputSize\x00")), float32(video.width), float32(video.height))
|
||||
gl.UseProgram(0)
|
||||
video.needUpload = false
|
||||
}
|
||||
|
||||
// CurrentFramebuffer returns the current FBO ID
|
||||
func (video *Video) CurrentFramebuffer() uintptr {
|
||||
return uintptr(video.fboID)
|
||||
}
|
||||
|
||||
// ProcAddress returns the address of the proc from GLFW
|
||||
func (video *Video) ProcAddress(procName string) uintptr {
|
||||
return uintptr(glfw.GetProcAddress(procName))
|
||||
}
|
||||
|
||||
// SetRotation rotates the game image as requested by the core
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
// +build darwin
|
||||
|
||||
package video
|
||||
|
||||
import (
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
)
|
||||
|
||||
func genVertexArrays(n int32, arrays *uint32) {
|
||||
gl.GenVertexArraysAPPLE(n, arrays)
|
||||
}
|
||||
|
||||
func bindVertexArray(array uint32) {
|
||||
gl.BindVertexArrayAPPLE(array)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// +build !darwin
|
||||
|
||||
package video
|
||||
|
||||
import (
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
)
|
||||
|
||||
func genVertexArrays(n int32, arrays *uint32) {
|
||||
gl.GenVertexArrays(n, arrays)
|
||||
}
|
||||
|
||||
func bindVertexArray(array uint32) {
|
||||
gl.BindVertexArray(array)
|
||||
}
|
||||
Reference in new issue
Block a user