Compare commits

..
61 Commits
Author SHA1 Message Date
Jean-André Santoni 71dc28f4a5 Remove extensions and specify shader version 2023-03-15 17:28:51 +07:00
Jean-André Santoni 16cd399681 Try gl 4.1 core 2023-03-15 17:19:10 +07:00
Jean-André Santoni 3edf65f9fd Merge remote-tracking branch 'origin/master' into kivutar/gl 2022-08-05 11:53:07 +02:00
Jean-André Santoni e01cc6bb86 Merge remote-tracking branch 'origin/master' into kivutar/gl 2022-02-10 21:25:25 +01:00
Jean-André Santoni 3e084bf7f1 Merge remote-tracking branch 'origin/master' into kivutar/gl 2021-12-14 11:05:13 +01:00
Jean-André Santoni 2bcbb1d24c Merge remote-tracking branch 'origin/master' into kivutar/gl 2021-11-24 13:12:40 +07:00
Jean-André Santoni 7d48bb840b Merge remote-tracking branch 'origin/master' into kivutar/gl 2021-11-14 10:12:38 +07:00
Jean-André Santoni cb8649a129 Forgot to use program before get uniform 2021-10-22 20:47:24 +07:00
Jean-André Santoni d74969f50e Fix 2021-10-22 20:09:45 +07:00
Jean-André Santoni a95767339e Merge remote-tracking branch 'origin/master' into kivutar/gl 2021-10-22 19:47:59 +07:00
Jean-André Santoni 346dbd8a33 Merge remote-tracking branch 'origin/master' into kivutar/gl 2021-09-30 14:24:57 +07:00
Jean-André Santoni c95a3a7a14 Merge branch 'master' into kivutar/gl 2021-05-14 19:59:41 +07:00
Jean-André Santoni 8647e6f893 Merge branch 'master' into kivutar/gl 2021-05-14 11:09:32 +07:00
Jean-André Santoni a43843bab1 Fix 2021-04-17 15:40:33 +07:00
Jean-André Santoni 6097d396df Merge branch 'master' into kivutar/gl 2021-04-17 14:56:26 +07:00
Jean-André Santoni e4c5845b57 Merge branch 'master' into kivutar/gl 2021-01-28 10:57:07 +07:00
Jean-André Santoni da0092ef53 Fixes 2021-01-05 22:38:01 +07:00
Jean-André Santoni d8afc6031a Merge remote-tracking branch 'origin/master' into kivutar/gl 2021-01-05 22:33:41 +07:00
Jean-André Santoni 1e0c4a0966 Uncomment gl.Enable(gl.TEXTURE_2D) 2020-07-27 00:10:20 +07:00
Jean-André Santoni e01a76b08d Merge remote-tracking branch 'origin/master' into kivutar/gl 2020-07-26 23:35:51 +07:00
Jean-André Santoni 73d70cb1f9 Implement libretro.EnvironmentGetPrefferedHWRenderer 2020-03-01 17:37:04 +07:00
Jean-André Santoni 8b8d49eef4 Merge remote-tracking branch 'origin/master' into kivutar/gl 2020-03-01 17:24:24 +07:00
Jean-André Santoni 008a70c851 Fix FBO on OSX (#313)
* Fix FBO on OSX

* Remove useless code

* Use GL v2.1 for fonts drawing

* Fix fonts on OSX with GL 2.1

* Rename

* Same thing for GenVertexArrays

* Make fl0w work on OSX
2020-02-29 15:40:18 +07:00
Jean-André Santoni 1d8f8e8fac Merge remote-tracking branch 'origin/master' into kivutar/gl 2020-02-29 12:25:12 +07:00
Jean-André Santoni 1146cb6f99 Merge remote-tracking branch 'origin/master' into kivutar/gl 2020-02-26 21:17:16 +07:00
Jean-André Santoni dbfbe3ace5 Temporarily fix tests 2020-02-24 19:25:47 +07:00
Jean-André Santoni e35cba220c Fix shader live update 2020-02-24 18:56:29 +07:00
Jean-André Santoni f32af42ba9 Call gl.TexSubImage2D after binding the texture 2020-02-24 18:42:12 +07:00
Higor Eurípedes 827ae5461d Fix sharp-bilinear and zfast-crt filters (#311)
TextureSize uniform was receiving the input size instead of the actual texture
size.
2020-02-24 04:23:24 +07:00
Jean-André Santoni 40396a4b86 Merge remote-tracking branch 'origin/master' into kivutar/gl 2020-02-07 10:09:37 +07:00
Higor Eurípedes ff9a3c8f4b OpenGL core support improvements (#308)
* Fix sample batch callback and OpenGL core support

* Fix texture/framebuffer allocation and clipping

The libretro api requires the framebuffers to be at least as large as
max_width/max_height.

* Fix context destruction

Fixes Flycast
2020-02-07 09:50:07 +07:00
Jean-André Santoni b8144699f6 Unbind vertex array 2019-11-24 21:02:46 +07:00
Jean-André Santoni 42d43d63c3 Active texture fixes 2019-11-24 20:56:12 +07:00
Jean-André Santoni 9c5c3f5962 Small fix 2019-11-24 20:52:40 +07:00
Jean-André Santoni 2dd9150510 Various rendering improvements 2019-11-24 20:35:25 +07:00
Jean-André Santoni d127589d50 Fix an important memory bug 2019-11-22 12:05:28 +07:00
Jean-André Santoni 05adbe4f6a Debug 2019-11-22 11:11:09 +07:00
Jean-André Santoni ab13ef0ebc Fix compat with non GL cores 2019-11-22 10:56:19 +07:00
Jean-André Santoni 8f51dd3b2d Merge remote-tracking branch 'origin/master' into kivutar/gl 2019-11-22 10:45:16 +07:00
Jean-André Santoni 4618fb63bf Merge remote-tracking branch 'origin/master' into kivutar/gl 2019-10-31 11:51:50 +07:00
Jean-André Santoni 2d55e97431 Lint fix 2019-10-27 12:39:57 +07:00
Jean-André Santoni c7030756e3 Set c.get_current_framebuffer and c.get_proc_address properly 2019-10-27 12:31:28 +07:00
Jean-André Santoni 614c5c2ed9 Merge remote-tracking branch 'origin/master' into kivutar/gl 2019-10-27 11:26:52 +07:00
Jean-André Santoni 466c2ef696 Cast C.retro_hw_context_reset_t 2019-10-27 11:21:17 +07:00
Jean-André Santoni 4914b16606 Fix gl.Clear 2019-10-26 17:20:20 +07:00
Jean-André Santoni 341ad35519 Fix framebuffer creation 2019-10-26 17:14:40 +07:00
Jean-André Santoni 933c86b852 Update dependency for GetProcAddress 2019-10-08 10:55:36 +07:00
Jean-André Santoni 681dfbc6ab Merge branch 'master' into kivutar/gl 2019-10-08 09:51:48 +07:00
Jean-André Santoni f241ef4995 List hardware contexts in the libretro binding 2019-03-12 23:58:12 +07:00
Jean-André Santoni 6e4b4b634e Remove useless code 2019-03-12 21:32:28 +07:00
Jean-André Santoni 87175ca350 Call vid.InitFramebuffer and Core.HWRenderCallback.ContextReset during LoadGame 2019-03-12 19:58:20 +07:00
Jean-André Santoni 7eef38cf2e Comment 2019-03-12 19:00:31 +07:00
Jean-André Santoni dc56ebfff9 Merge branch 'master' into kivutar/gl 2019-03-12 18:52:09 +07:00
Jean-André Santoni d3fd407e77 Move InitFramebuffer and call HWRenderCallback.ContextReset 2019-02-22 16:32:59 +07:00
Jean-André Santoni a7cf8d1f99 Implement more of InitFramebuffer 2019-02-22 16:27:24 +07:00
Jean-André Santoni a7da8f7cb9 Fix display of non gl cores 2019-02-22 16:18:06 +07:00
Jean-André Santoni 967b0da04f Merge remote-tracking branch 'origin/master' into kivutar/gl 2019-02-22 15:55:55 +07:00
Jean-André Santoni c6c0c9c819 Try to implement ProcAddress 2018-12-30 13:03:25 +07:00
Jean-André Santoni d41ab5d9d7 TODO 2018-12-30 12:38:54 +07:00
Jean-André Santoni 7e794694f5 Start implementing InitFramebuffer 2018-12-30 12:33:24 +07:00
Jean-André Santoni d861e3d47d Improve the libretro binding to support GL cores 2018-12-30 11:46:35 +07:00
73 changed files with 2067 additions and 5509 deletions

No files matched your search

+46 -242
View File
@@ -1,8 +1,6 @@
name: CD
on:
workflow_dispatch:
types: [created]
release:
types: [created]
@@ -11,121 +9,19 @@ defaults:
shell: bash
jobs:
build_linux_wayland_x86_64:
build_linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
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 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'
- uses: actions/setup-go@v2
- 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: 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
- 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
- name: Upload Release Asset
uses: alexellis/[email protected]
@@ -134,36 +30,31 @@ jobs:
with:
asset_paths: '["./Ludo-*.tar.gz*", "./*.deb"]'
build_linux_x11_armhf:
runs-on: ubuntu-24.04
build_linux_arm:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
with:
submodules: true
- uses: actions/setup-go@v5
with:
go-version: '1.25.0'
- uses: actions/setup-go@v2
- 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: 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: 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
- 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
- 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
- name: Upload Release Asset
uses: alexellis/[email protected]
@@ -172,65 +63,28 @@ jobs:
with:
asset_paths: '["./Ludo-*.tar.gz*", "./*.deb"]'
build_linux_x11_arm64:
runs-on: ubuntu-24.04
build_osx:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
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 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'
- uses: actions/setup-go@v2
- run: echo "VERSION=$(echo ${{ github.event.release.tag_name }} | cut -c 2-)" >> $GITHUB_ENV
- run: echo "/Users/runner/go/bin" >> $GITHUB_PATH
- run: go install honnef.co/go/tools/cmd/staticcheck@latest
- run: go get golang.org/x/lint/golint
- run: go get honnef.co/go/tools/cmd/staticcheck
- run: brew install openal-soft
- run: printf '%s' '${{ secrets.OSXCERT }}' | base64 --decode > dev.p12
- run: echo ${{ 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 import dev.p12 -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: xcrun notarytool submit Ludo-OSX-x86_64-${VERSION}.dmg --apple-id ${{ secrets.APPLEID }} --password ${{ secrets.APPLEIDPASS }} --team-id ZE9XE938Z2 --wait
- 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 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
@@ -242,77 +96,27 @@ jobs:
with:
asset_paths: '["./Ludo-*.dmg*"]'
build_osx_arm64:
runs-on: macos-15
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 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:
build_windows:
runs-on: windows-latest
defaults:
run:
shell: msys2 {0}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
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 .
- uses: actions/setup-go@v2
- 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: 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: sha256sum Ludo-Windows-x86_64-${VERSION}.zip > Ludo-Windows-x86_64-${VERSION}.zip.sha256
- run: hashdeep -c sha256 Ludo-Windows-x86_64-${VERSION}.zip > Ludo-Windows-x86_64-${VERSION}.zip.sha256
- name: Upload Release Asset
uses: alexellis/[email protected]
+42 -177
View File
@@ -1,8 +1,6 @@
name: CI
on:
workflow_dispatch:
branches: [master]
push:
branches: [master]
pull_request:
@@ -13,216 +11,83 @@ defaults:
shell: bash
jobs:
build_linux_wayland_x86_64:
build_linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
with:
submodules: true
- uses: actions/setup-go@v5
with:
go-version: '1.25.0'
- uses: actions/setup-go@v2
- run: echo "/home/runner/go/bin" >> $GITHUB_PATH
- run: go install honnef.co/go/tools/cmd/staticcheck@latest
- run: go get golang.org/x/lint/golint
- run: go get honnef.co/go/tools/cmd/staticcheck
- 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: sudo apt-get install libopenal-dev xorg-dev libgl1-mesa-dev -y --allow-unauthenticated
- run: go get .
- run: xvfb-run -a go test -v -race ./...
- run: go vet ./...
- run: staticcheck ./...
- run: go build -tags wayland -v
- run: golint -set_exit_status ./...
- run: go build -v
build_linux_wayland_armhf:
runs-on: ubuntu-24.04
build_linux_arm:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
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 armhf
- run: sudo rm -rf /etc/apt/sources.list.d/*
- 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
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: 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
- uses: actions/setup-go@v2
- 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: 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: 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
- 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
build_linux_x11_arm64:
runs-on: ubuntu-24.04
build_osx:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
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'
- uses: actions/setup-go@v2
- run: echo "/Users/runner/go/bin" >> $GITHUB_PATH
- run: go install honnef.co/go/tools/cmd/staticcheck@latest
- run: go get golang.org/x/lint/golint
- run: go get honnef.co/go/tools/cmd/staticcheck
- 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_osx_arm64:
runs-on: macos-15
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 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: go build -v
build_windows_x86_64:
build_windows:
runs-on: windows-latest
defaults:
run:
shell: msys2 {0}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
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"
- 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
- run: go get .
- run: go test -v -race ./...
- run: go vet ./...
#- run: go test -v -race ./...
#- run: go vet ./...
- run: staticcheck ./...
- run: golint -set_exit_status ./...
- run: go build -v -ldflags '-H=windowsgui'
+27 -50
View File
@@ -2,31 +2,18 @@ APP ?= Ludo
ARCH ?= x86_64
VERSION ?= dev
BUNDLENAME = $(APP)-$(OS)-$(ARCH)-$(VERSION)
DISPDRIVER ?= x11
BUILDBOTURL_BASE ?= https://buildbot.libretro.com/nightly
ifeq ($(OS), Linux)
TAGS = -tags=$(DISPDRIVER)
BUNDLENAME = $(APP)-$(OS)-$(DISPDRIVER)-$(ARCH)-$(VERSION)
endif
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
CORES = ppsspp dolphin atari800 bluemsx swanstation fbneo fceumm 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 np2kai o2em 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))
CORES := $(filter-out swanstation,$(CORES))
CORES := $(filter-out mednafen_pcfx,$(CORES))
CORES := $(filter-out mednafen_saturn,$(CORES))
CORES := $(filter-out melonds,$(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
ifeq ($(OS), Windows)
CORES += mupen64plus_next
endif
DYLIBS = $(addprefix cores/, $(addsuffix _libretro.dylib,$(CORES)))
@@ -34,34 +21,26 @@ DLLS = $(addprefix cores/, $(addsuffix _libretro.dll,$(CORES)))
SOBJS = $(addprefix cores/, $(addsuffix _libretro.so,$(CORES)))
ifeq ($(OS), OSX)
BUILDBOTURL=$(BUILDBOTURL_BASE)/apple/osx/$(ARCH)/latest
BUILDBOTURL=http://buildbot.libretro.com/nightly/apple/osx/$(ARCH)/latest
endif
ifeq ($(OS), Linux)
ifeq ($(ARCH), arm)
BUILDBOTURL=$(BUILDBOTURL_BASE)/linux/armv7-neon-hf/latest
else ifeq ($(ARCH), arm64)
BUILDBOTURL=$(BUILDBOTURL_BASE)/linux/aarch64/latest
BUILDBOTURL=http://buildbot.libretro.com/nightly/linux/armv7-neon-hf/latest
else
BUILDBOTURL=$(BUILDBOTURL_BASE)/linux/$(ARCH)/latest
BUILDBOTURL=http://buildbot.libretro.com/nightly/linux/$(ARCH)/latest
endif
endif
ifeq ($(OS), Windows)
BUILDBOTURL=$(BUILDBOTURL_BASE)/windows/$(ARCH)/latest
BUILDBOTURL=http://buildbot.libretro.com/nightly/windows/$(ARCH)/latest
endif
ludo:
go build $(TAGS)
go build
ludo.exe:
go build -ldflags '-H=windowsgui'
cores/%_libretro.dylib cores/%_libretro.dll:
mkdir -p cores
wget -c $(BUILDBOTURL)/$(@F).zip -O $@.zip;\
unzip $@.zip -d cores
rm $@.zip
cores/%_libretro.so:
cores/%_libretro.dylib cores/%_libretro.dll cores/%_libretro.so:
mkdir -p cores
wget -c $(BUILDBOTURL)/$(@F).zip -O $@.zip;\
unzip $@.zip -d cores
@@ -77,7 +56,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 "Developer ID Application: Jean-Andre Santoni (ZE9XE938Z2)" \
codesign --force --options runtime --verbose --timestamp --sign "7069CC8A4AE9AFF0493CC539BBA4FA345F0A668B" \
--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
@@ -91,28 +70,29 @@ $(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 "Developer ID Application: Jean-Andre Santoni (ZE9XE938Z2)" \
codesign --force --options runtime --verbose --timestamp --sign "7069CC8A4AE9AFF0493CC539BBA4FA345F0A668B" \
--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 "Developer ID Application: Jean-Andre Santoni (ZE9XE938Z2)" \
codesign --force --options runtime --verbose --timestamp --sign "7069CC8A4AE9AFF0493CC539BBA4FA345F0A668B" \
--entitlements pkg/entitlements.xml $(APP).app
empty.sparseimage:
rm -rf empty.sparseimage
hdiutil create -fs HFSX -layout SPUD -size 300m empty.sparseimage -type SPARSE -volname $(BUNDLENAME)
empty.dmg:
mkdir -p template
hdiutil create -fs HFSX -layout SPUD -size 200m empty.dmg -srcfolder template -format UDRW -volname $(BUNDLENAME) -quiet
rmdir template
# For OSX
dmg: empty.sparseimage $(APP).app
dmg: empty.dmg $(APP).app
mkdir -p wc
hdiutil attach empty.sparseimage -noautoopen -quiet -mountpoint wc
hdiutil attach empty.dmg -noautoopen -quiet -mountpoint wc
rm -rf wc/$(APP).app
ditto -rsrc $(APP).app wc/$(APP).app
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.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)" \
hdiutil convert empty.dmg -quiet -format UDZO -imagekey zlib-level=9 -o $(BUNDLENAME).dmg
codesign --force --options runtime --verbose --timestamp --sign "7069CC8A4AE9AFF0493CC539BBA4FA345F0A668B" \
--entitlements pkg/entitlements.xml $(BUNDLENAME).dmg
# For Windows
@@ -140,10 +120,7 @@ DEB_ARCH = amd64
ifeq ($(ARCH), arm)
DEB_ARCH = armhf
endif
ifeq ($(ARCH), arm64)
DEB_ARCH = arm64
endif
DEB_ROOT = ludo-$(DISPDRIVER)_$(VERSION)-1_$(DEB_ARCH)
DEB_ROOT = ludo_$(VERSION)-1_$(DEB_ARCH)
deb: ludo $(SOBJS)
mkdir -p $(DEB_ROOT)/DEBIAN
@@ -170,4 +147,4 @@ deb: ludo $(SOBJS)
dpkg-deb --build $(DEB_ROOT)
clean:
rm -rf Ludo.app ludo wc *.dmg *.sparseimage *.deb $(BUNDLENAME)-* cores/
rm -rf Ludo.app ludo wc *.dmg *.deb $(BUNDLENAME)-* cores/
+1 -1
View File
@@ -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 supports software-rendered and desktop OpenGL libretro cores. Hardware-rendered cores may require a newer OpenGL version than the frontend itself.
It is able to launch most non GL libretro cores.
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: 605aeae14a...e72d3737fb.
+60 -166
View File
@@ -4,13 +4,10 @@
package core
import (
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"strings"
@@ -23,7 +20,7 @@ import (
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/video"
"github.com/mholt/archives"
"github.com/mholt/archiver/v3"
)
var vid *video.Video
@@ -35,7 +32,6 @@ 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
@@ -52,7 +48,6 @@ 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)
@@ -84,134 +79,59 @@ 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
var err error
// What a wonderful API, all this boilerplate to set an option
switch filepath.Ext(filename) {
case ".zip":
un := archiver.Zip{
OverwriteExisting: true,
}
err = un.Unarchive(filename, dst)
case ".tar":
un := archiver.Tar{
OverwriteExisting: true,
}
err = un.Unarchive(filename, dst)
case ".rar":
un := archiver.Rar{
OverwriteExisting: true,
}
err = un.Unarchive(filename, dst)
default:
// Unarchive with default option
err = archiver.Unarchive(filename, dst)
}
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,
}
extPriority := 0 // current priority
// Give priority of some extensions (use upper case to be case insensitive)
extPrefered := make(map[string]int)
extPrefered[".cue"] = 1
extPrefered[".m3u"] = 2
selectGame := func(gamePath string, fsize int64) {
ext := filepath.Ext(gamePath)
err = archiver.Walk(filename, func(f archiver.File) error {
fname := f.Name()
ext := filepath.Ext(fname)
if size == 0 {
path = gamePath
size = fsize
// By default select the first file of the archive
path = filepath.Join(dst, fname)
size = f.Size()
log.Println("first file in archive:", path, size)
}
// Check if a file (based on extension) has a higher priority
priority, ok := extPrefered[strings.ToLower(ext)]
if ok && priority > extPriority {
extPriority = priority
path = gamePath
size = fsize
path = filepath.Join(dst, fname)
size = f.Size()
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
}
size, err = io.Copy(out, rc)
closeErr := out.Close()
if err != nil {
return "", 0, err
}
if closeErr != nil {
return "", 0, closeErr
}
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
return nil
})
return path, size, err
}
// LoadGame loads a game. A core has to be loaded first.
@@ -220,15 +140,10 @@ func LoadGame(gamePath string) error {
return err
}
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
}
// 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()
}
si := state.Core.GetSystemInfo()
@@ -239,7 +154,7 @@ func LoadGame(gamePath string) error {
}
if !si.NeedFullpath {
bytes, err := os.ReadFile(gi.Path)
bytes, err := ioutil.ReadFile(gi.Path)
if err != nil {
return err
}
@@ -254,7 +169,7 @@ func LoadGame(gamePath string) error {
ok := state.Core.LoadGame(*gi)
if !ok {
Unload()
state.CoreRunning = false
return errors.New("failed to load the game")
}
@@ -287,55 +202,34 @@ func LoadGame(gamePath string) error {
savefiles.LoadSRAM()
if state.Core.HWRenderCallback != nil {
if err := vid.ResetHWContext(); err != nil {
Unload()
return fmt.Errorf("create hardware context: %w", err)
}
vid.InitFramebuffer()
state.Core.HWRenderCallback.ContextReset()
}
return nil
}
// Unload closes the game and core before destroying their GL context.
// Unload unloads a libretro core
func Unload() {
if state.Core != nil {
savefiles.SaveSRAM()
vid.RunContextDestroy()
vid.BeginCore()
if state.CoreRunning {
state.Core.UnloadGame()
}
UnloadGame()
state.Core.Deinit()
vid.EndCore()
vid.ResetCoreState()
state.GamePath = ""
state.CoreRunning = false
state.CorePath = ""
state.Core = nil
vid.SetHWRenderContext(nil)
Options = nil
}
}
// 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)))
// UnloadGame unloads a game.
func UnloadGame() {
if state.CoreRunning {
savefiles.SaveSRAM()
state.Core.UnloadGame()
state.GamePath = ""
state.CoreRunning = false
vid.ResetPitch()
vid.ResetRot()
}
}
// getGameInfo opens a rom and return the libretro.GameInfo needed to launch it
+17 -73
View File
@@ -1,48 +1,20 @@
package core
import (
"log"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/go-gl/glfw/v3.3/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 }
@@ -63,11 +35,7 @@ func Test_coreLoad(t *testing.T) {
Init(&video.Video{})
var loadErr error
out := utils.CaptureOutput(func() { loadErr = Load("testdata/vecx_libretro" + ext) })
if loadErr != nil {
t.Fatalf("Load() error = %v", loadErr)
}
out := utils.CaptureOutput(func() { Load("testdata/vecx_libretro" + ext) })
t.Run("The core is loaded", func(t *testing.T) {
if state.Core == nil {
@@ -77,16 +45,14 @@ func Test_coreLoad(t *testing.T) {
t.Run("Logs information about the loaded core", func(t *testing.T) {
got := out
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)
}
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)
}
})
@@ -229,24 +195,13 @@ func Test_coreLoadGame(t *testing.T) {
Init(&video.Video{})
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)
if err := glfw.Init(); err != nil {
log.Fatalln("failed to initialize glfw")
}
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)
}
Load("testdata/vecx_libretro" + ext)
got := utils.CaptureOutput(func() { LoadGame("testdata/Polar Rescue (USA).vec") })
t.Run("Logs information about the loaded game", func(t *testing.T) {
want := `[Core]: Game loaded: testdata/Polar Rescue (USA).vec`
@@ -267,6 +222,7 @@ func Test_coreLoadGame(t *testing.T) {
}
})
UnloadGame()
Unload()
t.Run("Global state should be cleared by Unload", func(t *testing.T) {
@@ -281,15 +237,3 @@ 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")
}
}
+3 -19
View File
@@ -144,28 +144,11 @@ func environment(cmd uint32, data unsafe.Pointer) bool {
case libretro.EnvironmentSetAudioCallback:
state.Core.SetAudioCallback(data)
case libretro.EnvironmentSetHWRender:
hw := libretro.SetHWRenderCallback(
state.Core.HWRenderCallback = 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.
log.Println("[Env]: HWContextType:", state.Core.HWRenderCallback.HWContextType)
return true
case libretro.EnvironmentGetCanDupe:
libretro.SetBool(data, true)
@@ -176,6 +159,7 @@ func environment(cmd uint32, data unsafe.Pointer) bool {
case libretro.EnvironmentGetSaveDirectory:
return environmentGetSaveDirectory(data)
case libretro.EnvironmentGetPrefferedHWRender:
log.Println("[Env]: EnvironmentGetPrefferedHWRenderer: 1")
libretro.SetUint(data, uint(libretro.HWContextOpenGL))
case libretro.EnvironmentShutdown:
vid.SetShouldClose(true)
Binary file not shown.
+10 -22
View File
@@ -36,7 +36,6 @@ 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"`
}
@@ -64,13 +63,7 @@ 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, size int64, games chan (Game)) {
// skip empty file
if size == 0 || crc == 0 {
return
}
once := sync.Once{}
func (db *DB) FindByCRC(romPath string, romName string, crc uint32, games chan (Game)) {
var wg sync.WaitGroup
wg.Add(len(*db))
// For every Dat in the DB
@@ -81,13 +74,11 @@ func (db *DB) FindByCRC(romPath string, romName string, crc uint32, size int64,
if len(game.ROMs) == 0 {
continue
}
// 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
})
// If the checksums match
if crc == uint32(game.ROMs[0].CRC) {
game.Path = romPath
game.System = system
games <- game
}
}
wg.Done()
@@ -110,13 +101,10 @@ func (db *DB) FindByROMName(romPath string, romName string, crc uint32, games ch
continue
}
// If the checksums match
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
if romName == game.ROMs[0].Name {
game.Path = romPath
game.System = system
games <- game
}
}
wg.Done()
Submodule database updated: c31e8f126d...aad97888ed.
+22 -37
View File
@@ -1,47 +1,32 @@
module github.com/libretro/ludo
require (
github.com/adrg/xdg v0.5.3
github.com/adrg/xdg v0.4.0
github.com/andybalholm/brotli v1.0.4 // indirect
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-20260331235117-4566fea9a276
github.com/go-gl/glfw/v3.4/glfw v0.1.0-pre.1
github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec
github.com/go-gl/mathgl v1.0.0
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
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
github.com/golang/snappy v0.0.4 // indirect
github.com/klauspost/compress v1.13.6 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0
github.com/mholt/archiver/v3 v3.5.1
github.com/nwaples/rardecode v1.1.2 // indirect
github.com/pelletier/go-toml v1.9.4
github.com/pierrec/lz4/v4 v4.1.12 // indirect
github.com/tanema/gween v0.0.0-20200427131925-c89ae23cc63c
github.com/ulikunitz/xz v0.5.10 // indirect
github.com/youpy/go-wav v0.3.0
golang.org/x/image v0.0.0-20211028202545-6944b10bf410
golang.org/x/mobile v0.0.0-20211207041440-4e6c2922fdee
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)
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
go 1.13
+73 -284
View File
@@ -1,343 +1,132 @@
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/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/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
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/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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY=
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/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-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/gl v0.0.0-20211210172815-726fda9656d6 h1:zDw5v7qm4yH7N8C8uWd+8Ii9rROdgWxQuGoJ9WDXxfk=
github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec h1:3FLiRYO6PlQFDpUU7OEFlWgjGD1jnBIVSJ5SYRWk+9c=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec/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/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/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/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/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
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/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
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/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/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/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/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/nwaples/rardecode v1.1.2 h1:Cj0yZY6T1Zx1R7AhTbyGSALm44/Mmq+BAPc4B/p/d3M=
github.com/nwaples/rardecode v1.1.2/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM=
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.12 h1:44l88ehTZAUGW4VlO1QC4zkilL99M6Y9MXNwEs0uzP8=
github.com/pierrec/lz4/v4 v4.1.12/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
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.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/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/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/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
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.3.0 h1:ma20lCWmR/Y2P8tMpsRkbuawQ6d8+ckWaoOncEvMe0A=
github.com/youpy/go-wav v0.3.0/go.mod h1:RqzyxsQMqUl31ygGi4W8YIQl5gbTXB4Al6slg/Zn2Ro=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/zaf/g711 v0.0.0-20190814101024-76a4a538f52b h1:QqixIpc5WFIqTLxB3Hq8qs0qImAgBdq0p6rq2Qdl634=
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/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/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
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.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/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/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/mobile v0.0.0-20211207041440-4e6c2922fdee h1:/tShaw8UTf0XzI8DOZwQHzC7d6Vi3EtrBnftiZ4vAvU=
golang.org/x/mobile v0.0.0-20211207041440-4e6c2922fdee/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ=
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.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/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
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/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
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/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.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/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-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/tools v0.1.8-0.20211022200916-316ba0b74098/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
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=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/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=
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 -1
View File
@@ -1,7 +1,7 @@
package input
import (
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/go-gl/glfw/v3.3/glfw"
"github.com/libretro/ludo/libretro"
)
+1 -1
View File
@@ -1,7 +1,7 @@
package input
import (
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/go-gl/glfw/v3.3/glfw"
"github.com/libretro/ludo/libretro"
)
+5 -15
View File
@@ -6,7 +6,7 @@ package input
import (
"log"
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/go-gl/glfw/v3.3/glfw"
lr "github.com/libretro/ludo/libretro"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/settings"
@@ -54,14 +54,14 @@ func joystickCallback(joy glfw.Joystick, event glfw.PeripheralEvent) {
switch event {
case glfw.Connected:
if joy.IsGamepad() {
ntf.DisplayAndLogf(ntf.Info, "Input", "Joystick #%d plugged: %s.", joy, glfw.Joystick.GetName(joy))
ntf.DisplayAndLog(ntf.Info, "Input", "Joystick #%d plugged: %s.", joy, glfw.Joystick.GetName(joy))
} else {
ntf.DisplayAndLogf(ntf.Warning, "Input", "Joystick #%d plugged: %s but not configured.", joy, glfw.Joystick.GetName(joy))
ntf.DisplayAndLog(ntf.Warning, "Input", "Joystick #%d plugged: %s but not configured.", joy, glfw.Joystick.GetName(joy))
}
case glfw.Disconnected:
ntf.DisplayAndLogf(ntf.Info, "Input", "Joystick #%d unplugged.", joy)
ntf.DisplayAndLog(ntf.Info, "Input", "Joystick #%d unplugged.", joy)
default:
ntf.DisplayAndLogf(ntf.Warning, "Input", "Joystick #%d unhandled event: %d.", joy, event)
ntf.DisplayAndLog(ntf.Warning, "Input", "Joystick #%d unhandled event: %d.", joy, event)
}
}
@@ -80,16 +80,6 @@ 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
+742 -1723
View File
File diff suppressed because it is too large. Load diff
+15 -42
View File
@@ -5,37 +5,13 @@ 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) {
((void (*)(void))f)();
return ((void (*)(void))f)();
}
void bridge_retro_deinit(void *f) {
((void (*)(void))f)();
return ((void (*)(void))f)();
}
unsigned bridge_retro_api_version(void *f) {
@@ -75,7 +51,7 @@ void bridge_retro_set_environment(void *f, void *callback) {
}
void bridge_retro_set_video_refresh(void *f, void *callback) {
((void (*)(retro_video_refresh_t))f)((retro_video_refresh_t)callback);
((bool (*)(retro_video_refresh_t))f)((retro_video_refresh_t)callback);
}
void bridge_retro_set_controller_port_device(void *f, unsigned port, unsigned device) {
@@ -83,23 +59,23 @@ void bridge_retro_set_controller_port_device(void *f, unsigned port, unsigned de
}
void bridge_retro_set_input_poll(void *f, void *callback) {
((void (*)(retro_input_poll_t))f)((retro_input_poll_t)callback);
((bool (*)(retro_input_poll_t))f)((retro_input_poll_t)callback);
}
void bridge_retro_set_input_state(void *f, void *callback) {
((void (*)(retro_input_state_t))f)((retro_input_state_t)callback);
((bool (*)(retro_input_state_t))f)((retro_input_state_t)callback);
}
void bridge_retro_set_audio_sample(void *f, void *callback) {
((void (*)(retro_audio_sample_t))f)((retro_audio_sample_t)callback);
((bool (*)(retro_audio_sample_t))f)((retro_audio_sample_t)callback);
}
void bridge_retro_set_audio_sample_batch(void *f, void *callback) {
((void (*)(retro_audio_sample_batch_t))f)((retro_audio_sample_batch_t)callback);
((bool (*)(retro_audio_sample_batch_t))f)((retro_audio_sample_batch_t)callback);
}
bool bridge_retro_load_game(void *f, struct retro_game_info *gi) {
return ((bool (*)(const struct retro_game_info *))f)(gi);
return ((bool (*)(struct retro_game_info *))f)(gi);
}
size_t bridge_retro_serialize_size(void *f) {
@@ -107,23 +83,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 (*)(const void*, size_t))f)(data, size);
return ((bool (*)(void*, size_t))f)(data, size);
}
void bridge_retro_unload_game(void *f) {
((void (*)(void))f)();
return ((void (*)(void))f)();
}
void bridge_retro_run(void *f) {
((void (*)(void))f)();
return ((void (*)(void))f)();
}
void bridge_retro_reset(void *f) {
((void (*)(void))f)();
return ((void (*)(void))f)();
}
size_t bridge_retro_get_memory_size(void *f, unsigned id) {
@@ -201,11 +177,8 @@ int64_t coreGetTimeUsec_cgo() {
}
uintptr_t coreGetCurrentFramebuffer_cgo() {
return s_current_framebuffer;
}
void coreSetCurrentFramebuffer_cgo(uintptr_t fb) {
s_current_framebuffer = fb;
uintptr_t coreGetCurrentFramebuffer();
return coreGetCurrentFramebuffer();
}
uintptr_t coreGetProcAddress_cgo(const char *sym) {
+52 -172
View File
@@ -4,8 +4,7 @@ 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
/*
@@ -14,10 +13,6 @@ 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);
@@ -59,14 +54,11 @@ int16_t coreInputState_cgo(unsigned port, unsigned device, unsigned index, unsig
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"
)
@@ -467,92 +459,18 @@ type (
)
var (
environment environmentFunc
videoRefresh videoRefreshFunc
audioSample audioSampleFunc
audioSampleBatch audioSampleBatchFunc
inputPoll inputPollFunc
inputState inputStateFunc
log logFunc
getTimeUsec getTimeUsecFunc
getProcAddress getProcAddressFunc
releaseContext func()
acquireContext func()
coreCalls chan func()
frontendCalls chan func()
environment environmentFunc
videoRefresh videoRefreshFunc
audioSample audioSampleFunc
audioSampleBatch audioSampleBatchFunc
inputPoll inputPollFunc
inputState inputStateFunc
log logFunc
getTimeUsec getTimeUsecFunc
getCurrentFramebuffer getCurrentFramebufferFunc
getProcAddress getProcAddressFunc
)
// 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{}
@@ -563,13 +481,6 @@ func Load(sofile string) (*Core, error) {
return nil, err
}
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")
@@ -597,21 +508,18 @@ func Load(sofile string) (*Core, error) {
// Init takes care of the library global initialization
func (core *Core) Init() {
coreCall(func() { C.bridge_retro_init(core.symRetroInit) })
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 coreResult(func() uint { return uint(C.bridge_retro_api_version(core.symRetroAPIVersion)) })
return uint(C.bridge_retro_api_version(core.symRetroAPIVersion))
}
// Deinit takes care of the library global deinitialization
func (core *Core) Deinit() {
coreCall(func() { C.bridge_retro_deinit(core.symRetroDeinit) })
close(coreCalls)
C.cothread_deinit()
coreCalls, frontendCalls = nil, nil
C.bridge_retro_deinit(core.symRetroDeinit)
DlClose(core.handle)
core.MemoryMap = nil
environment = nil
@@ -622,6 +530,7 @@ func (core *Core) Deinit() {
inputState = nil
log = nil
getTimeUsec = nil
getCurrentFramebuffer = nil
getProcAddress = nil
}
@@ -632,12 +541,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() {
coreCall(func() { C.bridge_retro_run(core.symRetroRun) })
C.bridge_retro_run(core.symRetroRun)
}
// Reset resets the current game.
func (core *Core) Reset() {
coreCall(func() { C.bridge_retro_reset(core.symRetroReset) })
C.bridge_retro_reset(core.symRetroReset)
}
// GetSystemInfo returns statically known system info. Pointers provided in *info
@@ -645,7 +554,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{}
coreCall(func() { C.bridge_retro_get_system_info(core.symRetroGetSystemInfo, &rsi) })
C.bridge_retro_get_system_info(core.symRetroGetSystemInfo, &rsi)
return SystemInfo{
LibraryName: C.GoString(rsi.library_name),
LibraryVersion: C.GoString(rsi.library_version),
@@ -663,7 +572,7 @@ func (core *Core) GetSystemInfo() SystemInfo {
// desire a particular aspect ratio.
func (core *Core) GetSystemAVInfo() SystemAVInfo {
avi := C.struct_retro_system_av_info{}
coreCall(func() { C.bridge_retro_get_system_av_info(core.symRetroGetSystemAVInfo, &avi) })
C.bridge_retro_get_system_av_info(core.symRetroGetSystemAVInfo, &avi)
return SystemAVInfo{
Geometry: GameGeometry{
AspectRatio: float64(avi.geometry.aspect_ratio),
@@ -685,7 +594,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 coreResult(func() bool { return bool(C.bridge_retro_load_game(core.symRetroLoadGame, &rgi)) })
return bool(C.bridge_retro_load_game(core.symRetroLoadGame, &rgi))
}
// SerializeSize returns the amount of data the implementation requires to serialize
@@ -694,18 +603,18 @@ 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 coreResult(func() uint { return uint(C.bridge_retro_serialize_size(core.symRetroSerializeSize)) })
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))
defer C.free(data)
ok := coreResult(func() bool { return bool(C.bridge_retro_serialize(core.symRetroSerialize, data, C.size_t(size))) })
ok := bool(C.bridge_retro_serialize(core.symRetroSerialize, data, C.size_t(size)))
if !ok {
return nil, errors.New("retro_serialize failed")
}
bytes := C.GoBytes(data, C.int(size))
C.free(data)
return bytes, nil
}
@@ -717,9 +626,7 @@ func (core *Core) Unserialize(bytes []byte, size uint) error {
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)))
})
ok := bool(C.bridge_retro_unserialize(core.symRetroUnserialize, unsafe.Pointer(&bytes[0]), C.size_t(size)))
if !ok {
return errors.New("retro_unserialize failed")
}
@@ -728,51 +635,49 @@ func (core *Core) Unserialize(bytes []byte, size uint) error {
// UnloadGame unloads a currently loaded game
func (core *Core) UnloadGame() {
coreCall(func() { C.bridge_retro_unload_game(core.symRetroUnloadGame) })
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
coreCall(func() { C.bridge_retro_set_environment(core.symRetroSetEnvironment, C.coreEnvironment_cgo) })
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
coreCall(func() { C.bridge_retro_set_video_refresh(core.symRetroSetVideoRefresh, C.coreVideoRefresh_cgo) })
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
coreCall(func() { C.bridge_retro_set_audio_sample(core.symRetroSetAudioSample, C.coreAudioSample_cgo) })
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
coreCall(func() {
C.bridge_retro_set_audio_sample_batch(core.symRetroSetAudioSampleBatch, C.coreAudioSampleBatch_cgo)
})
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
coreCall(func() { C.bridge_retro_set_input_poll(core.symRetroSetInputPoll, C.coreInputPoll_cgo) })
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
coreCall(func() { C.bridge_retro_set_input_state(core.symRetroSetInputState, C.coreInputState_cgo) })
C.bridge_retro_set_input_state(core.symRetroSetInputState, C.coreInputState_cgo)
}
// BindLogCallback binds f to the log callback
@@ -791,9 +696,7 @@ 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) {
coreCall(func() {
C.bridge_retro_set_controller_port_device(core.symRetroSetControllerPortDevice, C.unsigned(port), C.unsigned(device))
})
C.bridge_retro_set_controller_port_device(core.symRetroSetControllerPortDevice, C.unsigned(port), C.unsigned(device))
}
//export coreEnvironment
@@ -801,12 +704,6 @@ 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)
}
@@ -831,12 +728,6 @@ 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)))
}
@@ -872,16 +763,16 @@ func coreGetTimeUsec() C.uint64_t {
return C.uint64_t(getTimeUsec())
}
//export coreGetCurrentFramebuffer
func coreGetCurrentFramebuffer() C.uintptr_t {
return C.uintptr_t(getCurrentFramebuffer())
}
//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))
@@ -1039,7 +930,7 @@ func (core *Core) SetFrameTimeCallback(data unsafe.Pointer) {
ftc := &FrameTimeCallback{}
ftc.Reference = int64(c.reference)
ftc.Callback = func(usec int64) {
coreCall(func() { C.bridge_retro_frame_time_callback(c.callback, C.retro_usec_t(usec)) })
C.bridge_retro_frame_time_callback(c.callback, C.retro_usec_t(usec))
}
core.FrameTimeCallback = ftc
}
@@ -1054,17 +945,13 @@ func SetHWRenderCallback(
hwrc := HWRenderCallback{}
hwrc.HWContextType = uint32(c.context_type)
getCurrentFramebuffer = currentFramebuffer
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.ContextReset = func() {
C.bridge_retro_hw_context_reset((C.retro_hw_context_reset_t)(c.context_reset))
}
hwrc.Depth = bool(c.depth)
@@ -1073,10 +960,8 @@ func SetHWRenderCallback(
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.ContextDestroy = func() {
C.bridge_retro_hw_context_destroy((C.retro_hw_context_reset_t)(c.context_destroy))
}
hwrc.DebugContext = bool(c.debug_context)
return &hwrc
@@ -1087,10 +972,10 @@ func (core *Core) SetAudioCallback(data unsafe.Pointer) {
c := *(*C.struct_retro_audio_callback)(data)
auc := &AudioCallback{}
auc.Callback = func() {
coreCall(func() { C.bridge_retro_audio_callback(c.callback) })
C.bridge_retro_audio_callback(c.callback)
}
auc.SetState = func(state bool) {
coreCall(func() { C.bridge_retro_audio_set_state(c.set_state, C.bool(state)) })
C.bridge_retro_audio_set_state(c.set_state, C.bool(state))
}
core.AudioCallback = auc
}
@@ -1098,15 +983,13 @@ 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 coreResult(func() uint { return uint(C.bridge_retro_get_memory_size(core.symRetroGetMemorySize, C.unsigned(id))) })
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 coreResult(func() unsafe.Pointer {
return C.bridge_retro_get_memory_data(core.symRetroGetMemoryData, C.unsigned(id))
})
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
@@ -1120,25 +1003,22 @@ 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) {
coreCall(func() { C.bridge_retro_set_eject_state(c.set_eject_state, C.bool(state)) })
C.bridge_retro_set_eject_state(c.set_eject_state, C.bool(state))
}
dcc.GetEjectState = func() bool {
return coreResult(func() bool { return bool(C.bridge_retro_get_eject_state(c.get_eject_state)) })
return bool(C.bridge_retro_get_eject_state(c.get_eject_state))
}
dcc.GetImageIndex = func() uint {
return coreResult(func() uint { return uint(C.bridge_retro_get_image_index(c.get_image_index)) })
return uint(C.bridge_retro_get_image_index(c.get_image_index))
}
dcc.SetImageIndex = func(index uint) {
coreCall(func() { C.bridge_retro_set_image_index(c.set_image_index, C.uint(index)) })
C.bridge_retro_set_image_index(c.set_image_index, C.uint(index))
}
dcc.GetNumImages = func() uint {
return coreResult(func() uint { return uint(C.bridge_retro_get_num_images(c.get_num_images)) })
return uint(C.bridge_retro_get_num_images(c.get_num_images))
}
core.DiskControlCallback = dcc
}
+2 -2
View File
@@ -28,8 +28,8 @@ func DlOpen(path string) (DlHandle, error) {
defer C.free(unsafe.Pointer(cpath))
h := C.dlopen(cpath, C.RTLD_LAZY|C.RTLD_GLOBAL)
if h == nil {
cerr := C.dlerror()
cerr := C.dlerror()
if h == nil || cerr != nil {
err := C.GoString(cerr)
return nil, errors.New(err)
}
+1 -9
View File
@@ -1,13 +1,5 @@
package libretro
/*
#include <stdint.h>
static void *symbol_from_addr(uintptr_t addr) {
return (void *)addr;
}
*/
import "C"
import (
"syscall"
"unsafe"
@@ -19,7 +11,7 @@ type DlHandle = *syscall.DLL
// DlSym loads a symbol from a dynamic library
func DlSym(handle DlHandle, name string) unsafe.Pointer {
proc := handle.MustFindProc(name)
return C.symbol_from_addr(C.uintptr_t(proc.Addr()))
return unsafe.Pointer(proc.Addr())
}
// DlOpen opens a dynamic library
+2 -2
View File
@@ -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.Updatef(ntf.Info, "Downloading update %.0f%% ", 100*resp.Progress())
n.Update(ntf.Info, "Downloading update %.0f%%%% ", 100*resp.Progress())
progress = resp.Progress()
case <-resp.Done:
+17 -42
View File
@@ -8,7 +8,7 @@ import (
"runtime"
"time"
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/go-gl/glfw/v3.3/glfw"
"github.com/libretro/ludo/audio"
"github.com/libretro/ludo/core"
"github.com/libretro/ludo/history"
@@ -30,22 +30,6 @@ 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) {
var currTime time.Time
prevTime := time.Now()
@@ -55,19 +39,11 @@ func runLoop(vid *video.Video, m *menu.Menu) {
glfw.PollEvents()
m.ProcessHotkeys()
ntf.Process(dt)
vid.ResizeViewport()
m.UpdatePalette()
input.Poll()
menuActive := state.MenuActive
if menuActive {
m.Update(dt)
}
if state.MenuContextResetNeeded {
m.ContextReset()
state.MenuContextResetNeeded = false
}
if !menuActive {
if !state.MenuActive {
if state.CoreRunning {
vid.BeginCore()
state.Core.Run()
if state.Core.FrameTimeCallback != nil {
state.Core.FrameTimeCallback.Callback(state.Core.FrameTimeCallback.Reference)
@@ -75,23 +51,18 @@ 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 {
vid.BeginFrontendFrame()
vid.RenderPresentedFrame()
fulfillPendingScreenshot(vid)
m.Update(dt)
vid.Render()
m.Render(dt)
m.RenderNotifications()
}
m.RenderNotifications()
if state.FastForward {
glfw.SwapInterval(0)
} else {
@@ -157,14 +128,18 @@ func main() {
input.Init(vid)
if len(state.CorePath) > 0 {
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()
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 {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
}
+8 -18
View File
@@ -1,31 +1,21 @@
package menu
import (
"github.com/libretro/ludo/input"
"github.com/go-gl/glfw/v3.3/glfw"
)
// Used to easily compose different hint bars based on the context.
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)
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)
*stack += 70 * menu.ratio
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
menu.Font.Printf(*stack, float32(h)-23*menu.ratio, 0.4*menu.ratio, label)
*stack += menu.Font.Width(0.4*menu.ratio, label)
}
func hintIcons() (arrows, upDown, leftRight, a, b, x, y, start, slct, guide uint32) {
if input.JoypadPlugged() {
if glfw.Joystick(0).IsGamepad() {
return menu.icons["pad-arrows"],
menu.icons["pad-up-down"],
menu.icons["pad-left-right"],
+6 -5
View File
@@ -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 := firstRune(list.children[list.ptr].label)
curr := list.children[list.ptr].label[0]
for i, t := range list.indexes {
if curr == t.Char {
if i+offset < 0 {
@@ -193,13 +193,14 @@ func (m *Menu) ProcessHotkeys() {
}
}
// Toggle fullscreen if ActionFullscreenToggle is released
if input.Released[0][input.ActionFullscreenToggle] == 1 {
// Toggle fullscreen if ActionFullscreenToggle is pressed
if input.Pressed[0][input.ActionFullscreenToggle] == 1 {
settings.Current.VideoFullscreen = !settings.Current.VideoFullscreen
m.SetFullscreen(settings.Current.VideoFullscreen)
m.Reconfigure(settings.Current.VideoFullscreen)
m.ContextReset()
err := settings.Save()
if err != nil {
ntf.DisplayAndLogf(ntf.Error, "Menu", "Error saving settings: %v", err)
ntf.DisplayAndLog(ntf.Error, "Menu", "Error saving settings: %s", err)
}
}
+4 -6
View File
@@ -102,12 +102,10 @@ func (m *Menu) ContextReset() {
m.icons[filename] = video.NewImage(path)
}
// 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
}
currentScreenIndex := len(m.stack) - 1
curList := m.stack[currentScreenIndex].Entry()
for i := range curList.children {
curList.children[i].thumbnail = 0
}
}
+5 -5
View File
@@ -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") // this entry will be pushed at the end by sort+prettify
os.Create(tmp + "File 4.img")
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[2].callbackOK()
children[3].callbackOK()
if exec != 1 {
t.Errorf("buildExplorer = %v, want %v", exec, 1)
}
})
t.Run("Folders have folder icon", func(t *testing.T) {
if children[3].icon != "folder" {
if children[4].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[3].callbackOK()
children[4].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[4].label != want {
if children[3].label != want {
t.Errorf("buildExplorer = %v, want %v", children[3].label, want)
}
})
+16 -6
View File
@@ -5,13 +5,23 @@ import (
"github.com/libretro/ludo/video"
)
var severityFgColor map[ntf.Severity]video.Color
var severityBgColor map[ntf.Severity]video.Color
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,
}
// 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
@@ -21,7 +31,7 @@ func (m *Menu) RenderNotifications() {
fading = 1
}
offset := fading*h - h
lw := m.Font.Width(m.ratio, n.Message)
lw := m.Font.Width(0.5*m.ratio, n.Message)
fg := severityFgColor[n.Severity]
bg := severityBgColor[n.Severity]
m.DrawRect(
@@ -33,10 +43,10 @@ func (m *Menu) RenderNotifications() {
bg.Alpha(fading),
)
m.Font.SetColor(fg.Alpha(fading))
m.Font.Print(
m.Font.Printf(
45*m.ratio,
(stack+offset)*m.ratio,
m.ratio,
0.5*m.ratio,
n.Message,
)
stack += h + offset
+27 -506
View File
@@ -1,527 +1,48 @@
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"
)
// Global variables for the menu theme
var white = video.Color{R: 1, G: 1, B: 1, A: 1}
var black = video.Color{R: 0, G: 0, B: 0, 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 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 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 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 tabHexaColors func(int) video.Color
var tabIconColors func(int) 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}
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 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}
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 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}
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 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}
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"]
}
}
var bgColor = white
var cursorBg = cyan
var textColor = black
// 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 {
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,
bgColor = darkerGrey
cursorBg = darkBlue
textColor = white
}
}
+18 -32
View File
@@ -35,7 +35,7 @@ type entry struct {
children []entry // children entries
ptr int // index of the active child
indexes []struct {
Char rune
Char byte
Index int
}
}
@@ -53,13 +53,6 @@ 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 {
@@ -160,10 +153,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, 0, cursorBgColor.Alpha(list.cursor.alpha))
70*menu.ratio, 70*menu.ratio, 1, cursorBg.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, cursorBgColor.Alpha(list.cursor.alpha))
float32(w)-630*menu.ratio, 100*menu.ratio, 1, cursorBg.Alpha(list.cursor.alpha))
}
// thumbnailDrawCursor draws the blinking rectangular background of the active
@@ -172,10 +165,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, 0, cursorBgColor.Alpha(list.cursor.alpha))
100*menu.ratio, 100*menu.ratio, 1, cursorBg.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, cursorBgColor.Alpha(list.cursor.alpha))
float32(w)-630*menu.ratio, 240*menu.ratio, 0.2, cursorBg.Alpha(list.cursor.alpha))
}
// genericRender renders a vertical list of menu entries
@@ -198,28 +191,23 @@ 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, 0, textColor.Alpha(e.iconAlpha))
0.5, 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.Print(
menu.Font.Printf(
670*menu.ratio,
float32(h)*e.yp+fontOffset,
menu.ratio, e.label)
0.5*menu.ratio, e.label)
if e.widget != nil {
e.widget(&e)
} else if e.stringValue != nil {
lw := menu.Font.Width(menu.ratio, e.stringValue())
menu.Font.Print(
lw := menu.Font.Width(0.5*menu.ratio, e.stringValue())
menu.Font.Printf(
float32(w)-lw-128*menu.ratio,
float32(h)*e.yp+fontOffset,
menu.ratio, e.stringValue())
0.5*menu.ratio, e.stringValue())
}
}
}
@@ -275,18 +263,16 @@ func askDeleteSavestateConfirmation(cb func()) {
}
func genericDrawHintBar() {
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)
w, h := menu.GetFramebufferSize()
menu.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, lightGrey)
_, upDown, _, a, b, _, _, _, _, guide := hintIcons()
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)
var stack float32
if state.CoreRunning {
stackHintRight(&rstack, guide, "Resume", h)
stackHint(&stack, guide, "RESUME", h)
}
stackHint(&stack, upDown, "NAVIGATE", h)
stackHint(&stack, b, "BACK", h)
stackHint(&stack, a, "OK", h)
}
+8 -16
View File
@@ -33,7 +33,7 @@ func buildCoreDiskControl() Scene {
state.Core.DiskControlCallback.SetEjectState(true)
state.Core.DiskControlCallback.SetImageIndex(index)
state.Core.DiskControlCallback.SetEjectState(false)
ntf.DisplayAndLogf(ntf.Success, "Menu", "Switched to disk %d.", index+1)
ntf.DisplayAndLog(ntf.Success, "Menu", "Switched to disk %d.", index+1)
state.MenuActive = false
},
})
@@ -77,23 +77,15 @@ 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)
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()
_, 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)
var stack float32
if state.CoreRunning {
stackHintRight(&rstack, guide, "Resume", h)
stackHint(&stack, guide, "RESUME", h)
}
stackHint(&stack, upDown, "NAVIGATE", h)
stackHint(&stack, b, "BACK", h)
stackHint(&stack, leftRight, "SET", h)
}
+10 -12
View File
@@ -16,7 +16,7 @@ func buildCoreOptions() Scene {
var list sceneCoreOptions
list.label = "Core Options"
if core.Options == nil || len(core.Options.Vars) == 0 {
if core.Options == nil {
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.ReplaceAll(v.Desc, "%", "%%"),
label: strings.Replace(v.Desc, "%", "%%", -1),
icon: "subsetting",
stringValue: func() string {
val := v.Choices[v.Choice]
return "◀ " + strings.ReplaceAll(val, "%", "%%") + " ▶"
return strings.Replace(val, "%", "%%", -1)
},
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.DisplayAndLogf(ntf.Error, "Core", "Error saving core options: %v", err)
ntf.DisplayAndLog(ntf.Error, "Core", "Error saving core options: %v", err.Error())
}
},
})
@@ -81,17 +81,15 @@ func (s *sceneCoreOptions) render() {
func (s *sceneCoreOptions) drawHintBar() {
w, h := menu.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)
menu.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, lightGrey)
_, upDown, leftRight, _, b, _, _, _, _, guide := hintIcons()
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)
var stack float32
if state.CoreRunning {
stackHintRight(&rstack, guide, "Resume", h)
stackHint(&stack, guide, "RESUME", h)
}
stackHint(&stack, upDown, "NAVIGATE", h)
stackHint(&stack, b, "BACK", h)
stackHint(&stack, leftRight, "SET", h)
}
+15 -15
View File
@@ -70,16 +70,16 @@ func (s *sceneDialog) render() {
white,
)
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(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.FontSm.SetColor(dialogTextColor)
menu.Font.SetColor(darkGrey)
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, 0, dialogTextColor)
menu.FontSm.Print(
70*menu.ratio, 70*menu.ratio, 1.0, darkGrey)
menu.Font.Printf(
fw/2-width/2*menu.ratio+margin*menu.ratio+70*menu.ratio,
fh/2+height/2*menu.ratio-23*menu.ratio-margin*menu.ratio,
menu.ratio,
0.4*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, 0, dialogTextColor)
menu.FontSm.Print(
70*menu.ratio, 70*menu.ratio, 1.0, darkGrey)
menu.Font.Printf(
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,
menu.ratio,
0.4*menu.ratio,
"YES")
}
+3 -14
View File
@@ -1,13 +1,12 @@
package menu
import (
"io/ioutil"
"log"
"os"
"path/filepath"
"regexp"
"runtime"
"sort"
"strings"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/settings"
@@ -75,7 +74,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 strings.HasPrefix(name, ".") && !settings.Current.ShowHiddenFiles {
if name[:1] == "." && !settings.Current.ShowHiddenFiles {
return
}
@@ -136,16 +135,7 @@ func buildExplorer(path string, exts []string, cb func(string), dirAction *entry
appendFolder(&list, "..", filepath.Clean(path+"/.."), exts, cb, dirAction, prettifier)
}
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()))
})
files, err := ioutil.ReadDir(path)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
@@ -165,7 +155,6 @@ 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 {
+19 -27
View File
@@ -52,7 +52,7 @@ func loadHistoryEntry(list Scene, game history.Game) {
}
corePath := game.CorePath
if _, err := os.Stat(corePath); os.IsNotExist(err) {
ntf.DisplayAndLogf(ntf.Error, "Menu", "Core not found: %s", filepath.Base(corePath))
ntf.DisplayAndLog(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, 0, white.Alpha(e.iconAlpha))
e.scale, white.Alpha(e.iconAlpha))
}
// Offset on Y to vertically center label + sublabel if there is a sublabel
@@ -198,18 +198,13 @@ func (s *sceneHistory) render() {
slOffset = 30 * menu.ratio * e.subLabelAlpha
}
stack := 840 * menu.ratio
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(
stack := 840 * menu.ratio
menu.Font.Printf(
840*menu.ratio,
float32(h)*e.yp+fontOffset-slOffset,
menu.ratio, e.label)
stack += float32(int(menu.Font.Width(menu.ratio, e.label)))
0.5*menu.ratio, e.label)
stack += float32(int(menu.Font.Width(0.5*menu.ratio, e.label)))
stack += 10
for _, tag := range e.tags {
@@ -218,18 +213,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, 0, white.Alpha(e.tagAlpha))
60*menu.ratio, 44*menu.ratio, 1.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.FontSm.SetColor(mutedTextColor.Alpha(e.subLabelAlpha))
menu.FontSm.Print(
menu.Font.SetColor(mediumGrey.Alpha(e.subLabelAlpha))
menu.Font.Printf(
840*menu.ratio,
float32(h)*e.yp+fontOffset+60*menu.ratio-slOffset,
menu.ratio, e.subLabel)
0.5*menu.ratio, e.subLabel)
}
}
@@ -238,23 +233,20 @@ func (s *sceneHistory) render() {
func (s *sceneHistory) drawHintBar() {
w, h := menu.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)
menu.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, lightGrey)
_, upDown, _, a, b, x, _, _, _, guide := hintIcons()
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)
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)
list := menu.stack[len(menu.stack)-1].Entry()
if list.children[list.ptr].callbackX != nil {
stackHintRight(&rstack, x, "Delete", h)
}
if state.CoreRunning {
stackHintRight(&rstack, guide, "Resume", h)
stackHint(&stack, x, "DELETE", h)
}
}
+11 -13
View File
@@ -162,7 +162,7 @@ func (s *sceneKeyboard) render() {
// Label
menu.Font.SetColor(black)
menu.Font.Print(
menu.Font.Printf(
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.Print(
menu.Font.Printf(
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.Print(
menu.Font.Printf(
x+ksz/2-gw/2,
y+ksz*0.6,
ksz/200, key)
@@ -205,17 +205,15 @@ func (s *sceneKeyboard) render() {
func (s *sceneKeyboard) drawHintBar() {
w, h := menu.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)
menu.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, lightGrey)
arrows, _, _, a, b, x, y, start, _, _ := hintIcons()
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)
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)
}
+35 -36
View File
@@ -18,39 +18,38 @@ type sceneMain struct {
}
var prettyCoreNames = map[string]string{
"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)",
"atari800_libretro": "Atari 800 (Atari 5200)",
"bluemsx_libretro": "BlueMSX (MSX)",
"fbneo_libretro": "FBNeo (Arcade)",
"fceumm_libretro": "Fceumm (NES)",
"gambatte_libretro": "Gambatte (Game Boy)",
"gearsystem_libretro": "GearSystem (Master System)",
"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)",
}
func prettifyCoreName(in string) string {
@@ -160,7 +159,7 @@ func coreExplorerCb(path string) {
ntf.DisplayAndLog(ntf.Error, "Core", err.Error())
return
}
ntf.DisplayAndLogf(ntf.Success, "Core", "Core loaded: %s", filepath.Base(path))
ntf.DisplayAndLog(ntf.Success, "Core", "Core loaded: %s", filepath.Base(path))
}
// triggered when a game is selected in the file explorer of Load Game
@@ -180,7 +179,7 @@ func gameExplorerCb(path string) {
// Shutdown the operating system
func cleanShutdown() {
core.Unload()
core.UnloadGame()
if err := exec.Command("/usr/sbin/shutdown", "-P", "now").Run(); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
@@ -188,7 +187,7 @@ func cleanShutdown() {
// Reboots the operating system
func cleanReboot() {
core.Unload()
core.UnloadGame()
if err := exec.Command("/usr/sbin/shutdown", "-r", "now").Run(); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
+19 -32
View File
@@ -23,13 +23,9 @@ 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,
@@ -57,12 +53,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 rune
var last byte
for i := 0; i < len(list.children); i++ {
char := firstRune(list.children[i].label)
char := list.children[i].label[0]
if char != last {
list.indexes = append(list.indexes, struct {
Char rune
Char byte
Index int
}{char, i})
last = char
@@ -98,7 +94,7 @@ func loadPlaylistEntry(list *scenePlaylist, playlist string, game playlists.Game
return
}
if _, err := os.Stat(corePath); os.IsNotExist(err) {
ntf.DisplayAndLogf(ntf.Error, "Menu", "Core not found: %s", filepath.Base(corePath))
ntf.DisplayAndLog(ntf.Error, "Menu", "Core not found: %s", filepath.Base(corePath))
return
}
if state.CorePath != corePath {
@@ -233,22 +229,16 @@ 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, 0, white.Alpha(e.iconAlpha))
e.scale, white.Alpha(e.iconAlpha))
}
stack := 840 * menu.ratio
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(
stack := 840 * menu.ratio
menu.Font.Printf(
840*menu.ratio,
float32(h)*e.yp+fontOffset,
menu.ratio, e.label)
stack += float32(int(menu.Font.Width(menu.ratio, e.label)))
0.5*menu.ratio, e.label)
stack += float32(int(menu.Font.Width(0.5*menu.ratio, e.label)))
stack += 10
for _, tag := range e.tags {
@@ -257,7 +247,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, 0, white.Alpha(e.tagAlpha))
60*menu.ratio, 44*menu.ratio, 1.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
@@ -271,23 +261,20 @@ func (s *scenePlaylist) render() {
func (s *scenePlaylist) drawHintBar() {
w, h := menu.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)
menu.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, lightGrey)
_, upDown, _, a, b, x, _, _, _, guide := hintIcons()
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)
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)
list := menu.stack[len(menu.stack)-1].Entry()
if list.children[list.ptr].callbackX != nil {
stackHintRight(&rstack, x, "Delete", h)
}
if state.CoreRunning {
stackHintRight(&rstack, guide, "Resume", h)
stackHint(&stack, x, "DELETE", h)
}
}
+6 -9
View File
@@ -1,7 +1,6 @@
package menu
import (
"github.com/libretro/ludo/core"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
@@ -28,7 +27,7 @@ func buildQuickMenu() Scene {
label: "Reset",
icon: "reset",
callbackOK: func() {
core.Reset()
state.Core.Reset()
state.MenuActive = false
state.FastForward = false
},
@@ -48,13 +47,11 @@ func buildQuickMenu() Scene {
icon: "screenshot",
callbackOK: func() {
name := utils.DatedName(state.GamePath)
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.")
}
err := menu.TakeScreenshot(name)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
ntf.DisplayAndLog(ntf.Success, "Menu", "Took a screenshot.")
}
},
})
+26 -38
View File
@@ -26,26 +26,22 @@ func buildSavestates() Scene {
icon: "savestate",
callbackOK: func() {
name := utils.DatedName(state.GamePath)
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.")
}
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.")
}
},
})
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 {
@@ -108,7 +104,7 @@ func removeSavestateEntry(s []entry, path string) []entry {
func deleteSavestateEntry(list *sceneSavestates, path string) {
err := os.Remove(path)
if err != nil {
ntf.DisplayAndLogf(ntf.Error, "Menu", "Could not delete savestate: %v", err)
ntf.DisplayAndLog(ntf.Error, "Menu", "Could not delete savestate: %s", err.Error())
return
}
list.children = removeSavestateEntry(list.children, path)
@@ -152,48 +148,40 @@ 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, 0, textColor.Alpha(e.iconAlpha))
e.scale, 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.Print(
menu.Font.Printf(
840*menu.ratio,
float32(h)*e.yp+fontOffset,
menu.ratio, e.label)
0.5*menu.ratio, e.label)
}
}
}
func (s *sceneSavestates) drawHintBar() {
w, h := menu.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)
menu.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, lightGrey)
ptr := menu.stack[len(menu.stack)-1].Entry().ptr
_, upDown, _, a, b, x, _, _, _, guide := hintIcons()
lstack := float32(75) * menu.ratio
rstack := float32(w) - 96*menu.ratio
stackHintLeft(&lstack, upDown, "Navigate", h)
if ptr == 0 {
stackHintRight(&rstack, a, "Save", h)
} else {
stackHintRight(&rstack, a, "Load", h)
}
stackHintRight(&rstack, b, "Back", h)
var stack float32
if state.CoreRunning {
stackHintRight(&rstack, guide, "Resume", h)
stackHint(&stack, guide, "RESUME", h)
}
stackHint(&stack, upDown, "NAVIGATE", h)
stackHint(&stack, b, "BACK", h)
if ptr == 0 {
stackHint(&stack, a, "SAVE", h)
} else {
stackHint(&stack, a, "LOAD", h)
}
list := menu.stack[len(menu.stack)-1].Entry()
if list.children[list.ptr].callbackX != nil {
stackHintRight(&rstack, x, "Delete", h)
stackHint(&stack, x, "DELETE", h)
}
}
+18 -33
View File
@@ -6,7 +6,7 @@ import (
"path/filepath"
"github.com/fatih/structs"
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/go-gl/glfw/v3.3/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.DisplayAndLogf(ntf.Success, "Settings", "%s set to %s", f.Tag("label"), f.Value().(string))
ntf.DisplayAndLog(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, 0, textColor.Alpha(e.iconAlpha))
1.25, textColor.Alpha(e.iconAlpha))
},
// Range widget for audio volume and similat float settings
@@ -164,7 +164,8 @@ var incrCallbacks = map[string]callbackIncrement{
v := f.Value().(bool)
v = !v
f.Set(v)
menu.SetFullscreen(settings.Current.VideoFullscreen)
menu.Reconfigure(settings.Current.VideoFullscreen)
menu.ContextReset()
settings.Save()
},
"VideoMonitorIndex": func(f *structs.Field, direction int) {
@@ -177,7 +178,8 @@ var incrCallbacks = map[string]callbackIncrement{
v = len(glfw.GetMonitors()) - 1
}
f.Set(v)
menu.SetFullscreen(settings.Current.VideoFullscreen)
menu.Reconfigure(settings.Current.VideoFullscreen)
menu.ContextReset()
settings.Save()
},
"VideoFilter": func(f *structs.Field, direction int) {
@@ -201,21 +203,6 @@ 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
@@ -287,22 +274,20 @@ func (s *sceneSettings) render() {
func (s *sceneSettings) drawHintBar() {
w, h := menu.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)
menu.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, lightGrey)
_, upDown, leftRight, a, b, _, _, _, _, guide := hintIcons()
lstack := float32(75) * menu.ratio
rstack := float32(w) - 96*menu.ratio
var stack float32
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 {
stackHintRight(&rstack, guide, "Resume", h)
stackHint(&stack, guide, "RESUME", h)
}
stackHint(&stack, upDown, "NAVIGATE", h)
stackHint(&stack, b, "BACK", h)
if list.children[list.ptr].callbackOK != nil {
stackHint(&stack, a, "SET", h)
} else {
stackHint(&stack, leftRight, "SET", h)
}
}
+29 -36
View File
@@ -3,8 +3,7 @@ package menu
import (
"fmt"
"os"
//"os/user"
"os/user"
"sort"
"github.com/libretro/ludo/audio"
@@ -13,9 +12,10 @@ 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(settings.Current.FileDirectory, nil,
usr, _ := user.Current()
menu.Push(buildExplorer(usr.HomeDir, 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.DisplayAndLogf(ntf.Error, "Menu", "Could not delete playlist: %v", err)
ntf.DisplayAndLog(ntf.Error, "Menu", "Could not delete playlist: %s", err.Error())
return
}
menu.stack[0].Entry().ptr++
@@ -284,54 +284,47 @@ func (tabs sceneTabs) render() {
stackWidth := 710 * menu.ratio
for i, e := range tabs.children {
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
}
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}
x := -menu.scroll*menu.ratio + stackWidth + e.width/2*menu.ratio
stackWidth += e.width*menu.ratio + e.margin*menu.ratio
if e.labelAlpha > 0 {
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.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)
}
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, 0, hexaColor)
440*menu.ratio, 440*menu.ratio, e.scale, c)
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, 0, iconColor)
256*menu.ratio, 256*menu.ratio, e.scale, white.Alpha(e.iconAlpha))
}
}
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)
func (tabs sceneTabs) drawHintBar() {
w, h := menu.GetFramebufferSize()
menu.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, lightGrey)
_, _, leftRight, a, _, _, _, _, _, guide := hintIcons()
_, _, leftRight, a, _, x, _, _, _, guide := hintIcons()
lstack := float32(75) * menu.ratio
rstack := float32(w) - 96*menu.ratio
stackHintLeft(&lstack, leftRight, "Navigate", h)
stackHintRight(&rstack, a, "Ok", h)
var stack float32
if state.CoreRunning {
stackHintRight(&rstack, guide, "Resume", h)
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)
}
}
+1 -1
View File
@@ -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.Unload()
core.UnloadGame()
err := cmd.Run()
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
+6 -8
View File
@@ -85,17 +85,15 @@ func (s *sceneWiFi) render() {
}
func (s *sceneWiFi) 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)
w, h := menu.GetFramebufferSize()
menu.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, lightGrey)
_, upDown, _, a, b, _, _, _, _, _ := hintIcons()
lstack := float32(75) * menu.ratio
rstack := float32(w) - 96*menu.ratio
stackHintLeft(&lstack, upDown, "Navigate", h)
var stack float32
stackHint(&stack, upDown, "NAVIGATE", h)
stackHint(&stack, b, "BACK", h)
if s.children[0].callbackOK != nil {
stackHintRight(&rstack, a, "Connect", h)
stackHint(&stack, a, "CONNECT", h)
}
stackHintRight(&rstack, b, "Back", h)
}
+3 -3
View File
@@ -7,7 +7,7 @@ import (
"path/filepath"
"strings"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/gl/v4.1-core/gl"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/video"
)
@@ -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, 0,
x, y, w, h, scale,
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, 0,
x, y, w, h, scale,
color,
)
}
+8 -15
View File
@@ -58,16 +58,12 @@ 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) *Notification {
func DisplayAndLog(severity Severity, prefix, message string, vars ...interface{}) *Notification {
msg := fmt.Sprintf(message, vars...)
if state.Verbose {
log.Print("[" + prefix + "]: " + message + "\n")
log.Print("[" + prefix + "]: " + msg + "\n")
}
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...))
return Display(severity, msg, Medium)
}
// Process iterates over the notifications, update them, delete the old ones.
@@ -90,13 +86,10 @@ func Clear() {
// Update the message of a given notification. Also resets the delay before
// disapearing.
func (n *Notification) Update(severity Severity, message string) {
func (n *Notification) Update(severity Severity, message string, vars ...interface{}) {
msg := fmt.Sprintf(message, vars...)
n.Duration = Medium
n.Message = message
n.Message = msg
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...))
}
+3 -3
View File
@@ -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) {
DisplayAndLogf(Info, "Tests", "Joypad #%d loaded with name %s.", 3, "Foo")
DisplayAndLog(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() { DisplayAndLogf(Info, "Test", "Joypad #%d loaded with name %s.", 3, "Foo") })
got := utils.CaptureOutput(func() { DisplayAndLog(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() { DisplayAndLogf(Info, "Test", "Joypad #%d loaded with name %s.", 3, "Foo") })
got := utils.CaptureOutput(func() { DisplayAndLog(Info, "Test", "Joypad #%d loaded with name %s.", 3, "Foo") })
want := ""
if !reflect.DeepEqual(got, want) {
t.Errorf("got = %v, want %v", got, want)
+5 -8
View File
@@ -6,6 +6,7 @@ package options
import (
"bytes"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
@@ -50,17 +51,13 @@ func New(vars []VariableInterface) (*Options, error) {
// Cache core options
for _, v := range vars {
choices := v.Choices()
// A core may expose an empty selector, e.g. when no BIOS files are found.
if len(choices) == 0 {
continue
}
v := v
o.Vars = append(o.Vars, &Variable{
Key: v.Key(),
Desc: v.Desc(),
Choices: choices,
Choices: v.Choices(),
Default: v.DefaultValue(),
Choice: utils.IndexOfString(v.DefaultValue(), choices),
Choice: utils.IndexOfString(v.DefaultValue(), v.Choices()),
})
}
o.Updated = true
@@ -103,7 +100,7 @@ func (o *Options) load() error {
defer o.Unlock()
name := utils.FileName(state.CorePath)
b, err := os.ReadFile(filepath.Join(xdg.ConfigHome, "ludo", name+".toml"))
b, err := ioutil.ReadFile(filepath.Join(xdg.ConfigHome, "ludo", name+".toml"))
if err != nil {
return err
}
+10 -1
View File
@@ -33,6 +33,7 @@ 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
@@ -116,7 +117,15 @@ func applyIPS(patch, source []byte) (*[]byte, error) {
offset++
if address == EOF {
if offset == len(patch) || offset == len(patch)-3 {
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++
return &targetData, nil
}
}
+3 -2
View File
@@ -4,6 +4,7 @@
package patch
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
@@ -14,7 +15,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 := os.ReadFile(upsFile)
pbytes, err := ioutil.ReadFile(upsFile)
if err != nil {
return nil, err
}
@@ -29,7 +30,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 := os.ReadFile(ipsFile)
pbytes, err := ioutil.ReadFile(ipsFile)
if err != nil {
return nil, err
}
-1
View File
@@ -138,7 +138,6 @@ 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",
+2 -1
View File
@@ -4,6 +4,7 @@ package savefiles
import (
"C"
"errors"
"io/ioutil"
"os"
"path/filepath"
"sync"
@@ -83,7 +84,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 := os.ReadFile(path())
source, err := ioutil.ReadFile(path())
if err != nil {
return err
}
+9 -5
View File
@@ -3,17 +3,19 @@
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 {
bytes, err := core.Serialize()
s := state.Core.SerializeSize()
bytes, err := state.Core.Serialize(s)
if err != nil {
return err
}
@@ -22,14 +24,16 @@ func Save(name string) error {
if err != nil {
return err
}
return os.WriteFile(path, bytes, 0644)
return ioutil.WriteFile(path, bytes, 0644)
}
// Load the state from the filesystem
func Load(path string) error {
bytes, err := os.ReadFile(path)
s := state.Core.SerializeSize()
bytes, err := ioutil.ReadFile(path)
if err != nil {
return err
}
return core.Unserialize(bytes)
err = state.Core.Unserialize(bytes, s)
return err
}
+14 -20
View File
@@ -6,7 +6,7 @@ package scanner
import (
"archive/zip"
"hash/crc32"
"io"
"io/ioutil"
"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 := os.ReadDir(dir)
files, err := ioutil.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, _ := os.ReadFile(filepath.Join(dir, name))
bytes, _ := ioutil.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.DisplayAndLogf(ntf.Info, "Menu", "Scanning %s", dir)
n := ntf.DisplayAndLog(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.Updatef(ntf.Success, "Done scanning. %d new games found.", i)
n.Update(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 := io.ReadAll(h)
bytes, err := ioutil.ReadAll(h)
if err != nil {
return 0, 0, err
}
@@ -114,7 +114,6 @@ 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 headerSize, ok := headerSizes[romExt]; ok {
crc, crcHeaderless, err := checksumHeaderless(rom, headerSize)
@@ -122,36 +121,31 @@ func Scan(dir string, roms []string, games chan (dat.Game), n *ntf.Notification)
n.Update(ntf.Error, err.Error())
continue
}
state.DB.FindByCRC(f, rom.Name, crc, size, games)
state.DB.FindByCRC(f, rom.Name, crcHeaderless, size-int64(headerSize), games)
state.DB.FindByCRC(f, rom.Name, crc, games)
state.DB.FindByCRC(f, rom.Name, crcHeaderless, 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, size, games)
state.DB.FindByCRC(f, rom.Name, rom.CRC32, games)
n.Update(ntf.Info, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+f)
}
}
z.Close()
case ".cue", ".pbp", ".m3u":
case ".cue":
// 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", ".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
}
s, err := os.Stat(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)
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)
state.DB.FindByCRC(f, utils.FileName(f), crc, 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)
state.DB.FindByCRC(f, utils.FileName(f), crcHeaderless, games)
}
n.Update(ntf.Info, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+f)
}
+1 -11
View File
@@ -2,18 +2,15 @@ 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,
@@ -43,17 +40,13 @@ 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",
@@ -64,10 +57,7 @@ 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",
+5 -6
View File
@@ -6,6 +6,7 @@ import (
"bytes"
"errors"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
@@ -22,10 +23,9 @@ 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,7 +36,6 @@ 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"`
@@ -74,14 +73,14 @@ func Load() error {
// If /etc/ludo.toml exists, override the defaults
if _, err := os.Stat("/etc/ludo.toml"); !os.IsNotExist(err) {
b, _ := os.ReadFile("/etc/ludo.toml")
b, _ := ioutil.ReadFile("/etc/ludo.toml")
err = toml.Unmarshal(b, &Current)
if err != nil {
return err
}
}
b, err := os.ReadFile(filepath.Join(xdg.ConfigHome, "ludo", "settings.toml"))
b, err := ioutil.ReadFile(filepath.Join(xdg.ConfigHome, "ludo", "settings.toml"))
if err != nil {
return err
}
-10
View File
@@ -33,13 +33,3 @@ 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
-190
View File
@@ -1,190 +0,0 @@
//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)
}
}
+1 -2
View File
@@ -18,11 +18,10 @@ 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 = c * color;
COMPAT_FRAGCOLOR = vec4(c.rgb, 1.0);
}
` + "\x00"
+5
View File
@@ -4,9 +4,14 @@ 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;
+29
View File
@@ -0,0 +1,29 @@
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"
+152 -357
View File
@@ -5,17 +5,17 @@ import (
"image"
"image/draw"
"io"
"log"
"io/ioutil"
"os"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/gl/v4.1-core/gl"
"github.com/golang/freetype"
"github.com/golang/freetype/truetype"
"golang.org/x/image/font"
"golang.org/x/image/math/fixed"
)
type glyph struct {
type character struct {
x, y int
width int // Glyph width
height int // Glyph height
@@ -34,11 +34,9 @@ const (
TopToBottom // E.g.: Chinese
)
const fontRenderScale = 1
// A Font allows rendering of text to an OpenGL context.
type Font struct {
glyphs map[rune]*glyph
fontChar []*character
vao uint32
vbo uint32
program uint32
@@ -46,249 +44,20 @@ 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
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)
func max(a, b float32) float32 {
if a > b {
return a
}
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)
return b
}
// LoadTrueTypeFont builds a set of textures based on a ttf files gylphs
func LoadTrueTypeFont(program uint32, r io.Reader, scale int32, dir Direction) (*Font, error) {
data, err := io.ReadAll(r)
func LoadTrueTypeFont(program uint32, r io.Reader, scale int32, low, high rune, dir Direction) (*Font, error) {
data, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
@@ -301,69 +70,118 @@ func LoadTrueTypeFont(program uint32, r io.Reader, scale int32, dir Direction) (
// Make Font stuct type
f := new(Font)
f.glyphs = make(map[rune]*glyph)
f.fontChar = make([]*character, 0, high-low+1)
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
face := truetype.NewFace(ttf, &truetype.Options{
ttfFace := 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, f.atlasHeight, lineHeight, err = calculateAtlasSize(face, ttf, runes, scale, margin)
if err != nil {
return nil, err
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.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))
atlas := image.NewRGBA(rect)
draw.Draw(atlas, atlas.Bounds(), bg, image.Point{}, draw.Src)
f.atlas = atlas
rgba := image.NewRGBA(rect)
draw.Draw(rgba, rgba.Bounds(), bg, image.Point{}, draw.Src)
margin := 4
x := margin
y := margin
for _, r := range runes {
glyph, err := appendGlyph(face, ttf, r, &x, &y, lineHeight, f.atlasWidth, atlas, fg, scale, margin)
if err != nil {
continue
// 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")
}
f.glyphs[r] = glyph
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)
if err != nil {
return nil, err
}
// Add char to fontChar list
f.fontChar = append(f.fontChar, char)
}
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)
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(atlas.Rect.Dx()), int32(atlas.Rect.Dy()), 0, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(atlas.Pix))
gl.BindTexture(gl.TEXTURE_2D, 0)
textureUniform := gl.GetUniformLocation(f.program, gl.Str("Texture\x00"))
gl.Uniform1i(textureUniform, 0)
// Configure VAO/VBO for texture quads
genVertexArrays(1, &f.vao)
gl.GenBuffers(1, &f.vbo)
bindVertexArray(f.vao)
gl.GenBuffers(1, &f.vbo)
gl.BindBuffer(gl.ARRAY_BUFFER, f.vbo)
vertAttrib := uint32(gl.GetAttribLocation(f.program, gl.Str("vert\x00")))
@@ -374,31 +192,22 @@ func LoadTrueTypeFont(program uint32, r io.Reader, scale int32, dir Direction) (
gl.EnableVertexAttribArray(texCoordAttrib)
gl.VertexAttribPointerWithOffset(texCoordAttrib, 2, gl.FLOAT, false, 4*4, 2*4)
// Generate texture
gl.ActiveTexture(gl.TEXTURE0)
gl.GenTextures(1, &f.textureID)
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_MAG_FILTER, gl.LINEAR)
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.BindBuffer(gl.ARRAY_BUFFER, 0)
bindVertexArray(0)
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)
@@ -417,19 +226,9 @@ func LoadFont(file string, scale int32, windowWidth int, windowHeight int) (*Fon
gl.UseProgram(program)
// Set screen resolution
resUniform := gl.GetUniformLocation(program, gl.Str("resolution\x00"))
gl.Uniform2f(resUniform, float32(windowWidth), float32(windowHeight))
gl.Uniform2f(gl.GetUniformLocation(program, gl.Str("resolution\x00")), float32(windowWidth), float32(windowHeight))
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
return LoadTrueTypeFont(program, fd, scale, 32, 256, LeftToRight)
}
// SetColor allows you to set the text color to be used when you draw the text
@@ -445,45 +244,38 @@ func (f *Font) UpdateResolution(windowWidth int, windowHeight int) {
gl.UseProgram(0)
}
// 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))
// 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...))
if len(indices) == 0 {
return nil
}
// Setup blending mode
gl.Enable(gl.BLEND)
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
// Activate corresponding render state
gl.UseProgram(f.program)
// Set text color
gl.Uniform4f(gl.GetUniformLocation(f.program, gl.Str("textColor\x00")), f.color.R, f.color.G, f.color.B, f.color.A)
lowChar := rune(32)
var coords []point
// Iterate through all glyphs in string
// Iterate through all characters in string
for i := range indices {
// Get rune
runeIndex := indices[i]
var ch *glyph
// Skip runes that are not in font glyph map
ch = f.ensureGlyph(runeIndex)
if ch == nil {
ch = f.glyphs['?'] // fallback
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]
}
// Calculate position and size for current rune
xpos := x + float32(ch.bearingH)*scale
ypos := y - float32(ch.height-ch.bearingV)*scale
w := float32(ch.width) * scale
h := float32(ch.height) * scale
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
// Set quad positions
var x1 = xpos
@@ -491,17 +283,21 @@ func (f *Font) Print(x, y float32, scale float32, text string) error {
var y1 = ypos
var y2 = ypos + h
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})
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})
// 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))
}
gl.UseProgram(f.program)
gl.Uniform4f(gl.GetUniformLocation(f.program, gl.Str("color\x00")), f.color.R, f.color.G, f.color.B, f.color.A)
gl.Enable(gl.BLEND)
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
bindVertexArray(f.vao)
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, f.textureID)
@@ -516,37 +312,36 @@ func (f *Font) Print(x, y float32, scale float32, text string) error {
return nil
}
// 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...))
}
// Width returns the width of a piece of text in pixels.
func (f *Font) Width(scale float32, text string) float32 {
// 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
scale *= fontRenderScale
indices := []rune(text)
indices := []rune(fmt.Sprintf(fs, argv...))
if len(indices) == 0 {
return 0
}
// Iterate through all glyphs in string
lowChar := rune(32)
// Iterate through all characters in string
for i := range indices {
// Get rune
runeIndex := indices[i]
if f.face != nil {
if advance, ok := f.face.GlyphAdvance(runeIndex); ok {
width += float32(advance.Round()) * scale
continue
}
// 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 ch, ok := f.glyphs['?']; ok {
width += float32((ch.advance >> 6)) * scale
}
// 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))
}
return width
+6 -5
View File
@@ -6,7 +6,7 @@ var fontFragmentShader = `
#define COMPAT_ATTRIBUTE in
#define COMPAT_TEXTURE texture
#define COMPAT_FRAGCOLOR FragColor
out vec4 FragColor;
out vec4 COMPAT_FRAGCOLOR;
#else
#define COMPAT_VARYING varying
#define COMPAT_ATTRIBUTE attribute
@@ -14,12 +14,13 @@ out vec4 FragColor;
#define COMPAT_FRAGCOLOR gl_FragColor
#endif
uniform sampler2D tex;
uniform vec4 textColor;
uniform sampler2D Texture;
uniform vec4 color;
COMPAT_VARYING vec2 fragTexCoord;
void main() {
vec4 sampled = vec4(1.0, 1.0, 1.0, COMPAT_TEXTURE(tex, fragTexCoord).r);
COMPAT_FRAGCOLOR = min(textColor, vec4(1.0, 1.0, 1.0, 1.0)) * sampled;
vec4 sampled = vec4(1.0, 1.0, 1.0, COMPAT_TEXTURE(Texture, fragTexCoord).r);
COMPAT_FRAGCOLOR = min(color, vec4(1.0, 1.0, 1.0, 1.0)) * sampled;
}
` + "\x00"
-83
View File
@@ -1,83 +0,0 @@
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
}
}
+27 -52
View File
@@ -1,13 +1,11 @@
package video
import (
"fmt"
"image"
"image/draw"
"log"
"os"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/gl/v4.1-core/gl"
)
// Color is an RGBA type that we use in the menu
@@ -21,32 +19,6 @@ 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
@@ -94,11 +66,12 @@ 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, scale, r float32, c Color) {
func (video *Video) DrawImage(image uint32, x, y, w, h float32, scale float32, c Color) {
va := video.vertexArray(x, y, w, h, scale)
gl.UseProgram(video.defaultProgram)
gl.Uniform4f(gl.GetUniformLocation(video.defaultProgram, gl.Str("color\x00")), c.R, c.G, c.B, c.A)
gl.UseProgram(video.demulProgram)
gl.Uniform4f(gl.GetUniformLocation(video.demulProgram, 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)
@@ -124,7 +97,11 @@ func (video *Video) ScissorEnd() {
gl.Disable(gl.SCISSOR_TEST)
}
func vertexArrayForFramebuffer(x, y, w, h, scale, ffbw, ffbh float32) []float32 {
func (video *Video) vertexArray(x, y, w, h, scale float32) []float32 {
fbw, fbh := video.Window.GetFramebufferSize()
ffbw := float32(fbw)
ffbh := float32(fbh)
w *= scale
h *= scale
@@ -139,13 +116,9 @@ func vertexArrayForFramebuffer(x, y, w, h, scale, ffbw, ffbh 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)
@@ -165,6 +138,7 @@ 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)
@@ -184,6 +158,7 @@ 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)
@@ -200,27 +175,28 @@ func (video *Video) DrawCircle(x, y, r float32, c Color) {
gl.Disable(gl.BLEND)
}
func textureLoad(nrgba *image.NRGBA) uint32 {
func textureLoad(rgba *image.RGBA) 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)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_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, (int32)(nrgba.Stride/4))
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, 0)
gl.TexImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
int32(nrgba.Rect.Dx()),
int32(nrgba.Rect.Dy()),
int32(rgba.Rect.Size().X),
int32(rgba.Rect.Size().Y),
0,
gl.RGBA,
gl.UNSIGNED_BYTE,
gl.Ptr(nrgba.Pix))
gl.ActiveTexture(gl.TEXTURE0)
gl.Ptr(rgba.Pix))
gl.GenerateMipmap(gl.TEXTURE_2D)
return texture
}
@@ -228,19 +204,18 @@ func textureLoad(nrgba *image.NRGBA) 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
}
nrgba := image.NewNRGBA(img.Bounds())
draw.Draw(nrgba, nrgba.Bounds(), img, image.Point{0, 0}, draw.Src)
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)
return textureLoad(nrgba)
return textureLoad(rgba)
}
-63
View File
@@ -1,63 +0,0 @@
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
}
-25
View File
@@ -1,25 +0,0 @@
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()
}
+34 -32
View File
@@ -1,46 +1,53 @@
package video
import (
"errors"
"image"
"image/png"
"os"
"path/filepath"
"github.com/disintegration/imaging"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/gl/v4.1-core/gl"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
)
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")
}
// 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)
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")
}
bindVertexArray(video.vao)
var oldReadBuffer int32
gl.GetIntegerv(gl.READ_BUFFER, &oldReadBuffer)
gl.ReadBuffer(gl.BACK)
gl.BindTexture(gl.TEXTURE_2D, video.texID)
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
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))
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
}
flipped := imaging.FlipV(img)
if flipped.Bounds().Dx() != outW || flipped.Bounds().Dy() != outH {
flipped = imaging.Resize(flipped, outW, outH, imaging.Linear)
}
// 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)
err := os.MkdirAll(settings.Current.ScreenshotsDirectory, os.ModePerm)
if err != nil {
@@ -52,13 +59,8 @@ func (video *Video) captureBackbufferScreenshot(name string) error {
if err != nil {
return err
}
defer fd.Close()
flipped := imaging.FlipV(img)
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)
}
+4 -5
View File
@@ -4,7 +4,7 @@ import (
"fmt"
"strings"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/gl/v4.1-core/gl"
"github.com/go-gl/mathgl/mgl32"
)
@@ -23,9 +23,6 @@ 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
@@ -60,7 +57,9 @@ func newProgram(vertexShaderSource, fragmentShaderSource string) (uint32, error)
func compileShader(source string, shaderType uint32) (uint32, error) {
shader := gl.CreateShader(shaderType)
csources, free := gl.Strs(glslVersion + source)
source = "#version 410\n" + source
csources, free := gl.Strs(source)
gl.ShaderSource(shader, 1, csources, nil)
free()
gl.CompileShader(shader)
-78
View File
@@ -1,78 +0,0 @@
//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)
}
}
+155 -581
View File
@@ -4,13 +4,13 @@
package video
import (
"errors"
"fmt"
"log"
"path/filepath"
"unsafe"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/go-gl/gl/v4.1-core/gl"
"github.com/go-gl/glfw/v3.3/glfw"
"github.com/go-gl/mathgl/mgl32"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/settings"
@@ -19,16 +19,9 @@ import (
// Video holds the state of the video package
type Video struct {
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
Window *glfw.Window
Geom libretro.GameGeometry
Font *Font
program uint32 // current program used for the game quad
defaultProgram uint32 // default program used for the game quad
@@ -38,217 +31,45 @@ 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
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
demulProgram uint32 // program to draw premultiplied alpha images
vao uint32
vbo uint32
texID uint32
fboID uint32
rboID uint32
identityMat mgl32.Mat4 // just a cache
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 // 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
pixType uint32
bpp int32
width, height int32 // dimensions set by the refresh callback
rot uint
}
// Init instanciates the video package
func Init(fullscreen bool) *Video {
vid := &Video{title: "Ludo"}
vid := &Video{}
vid.identityMat = mgl32.Ident4()
vid.Configure(fullscreen)
return vid
}
// 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() {
// Reconfigure destroys and recreates the window with new attributes
func (video *Video) Reconfigure(fullscreen bool) {
if video.Window != nil {
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()
// This is the expected frontend behavior and Flycast requires this
// for fullscreen toggling to work, but ppsspp breaks. OTOH, ppsspp
// breaks in those situations even if we don't call context_destroy
// so ignore it.
hw := state.Core.HWRenderCallback
if state.CoreRunning && hw != nil && hw.ContextDestroy != nil {
state.Core.HWRenderCallback.ContextDestroy()
}
video.Window.Destroy()
}
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.
@@ -261,69 +82,12 @@ 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 {
@@ -332,122 +96,102 @@ 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) {
if err := video.configureWithSize(384*2, 240*2, fullscreen); err != nil {
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 = 384 * 2
height = 240 * 2
}
glfw.WindowHint(glfw.ContextVersionMajor, 4)
glfw.WindowHint(glfw.ContextVersionMinor, 1)
glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True)
var err error
video.Window, err = glfw.CreateWindow(width, height, "Ludo", m, nil)
if 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)
if err := initGL(); err != nil {
// Initialize Glow
if err := gl.Init(); err != nil {
panic(err)
}
version := gl.GoStr(gl.GetString(gl.VERSION))
fmt.Println("OpenGL version", version)
shversion := gl.GoStr(gl.GetString(gl.SHADING_LANGUAGE_VERSION))
fmt.Println("OpenGL shading version", shversion)
fbw, fbh := video.Window.GetFramebufferSize()
// LoadFont (fontfile, font scale, window width, window height)
fontPath := filepath.Join(settings.Current.AssetsDirectory, "font.ttf")
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)
video.Font, err = LoadFont(fontPath, int32(36*2), fbw, fbh)
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))
// Configure the vertex and fragment shaders
video.defaultProgram, err = newProgram(vertexShader, defaultFragmentShader)
if err != nil {
panic(err)
}
video.program = video.defaultProgram
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)
gl.UseProgram(video.program)
textureUniform := gl.GetUniformLocation(video.program, gl.Str("Texture\x00"))
@@ -470,22 +214,25 @@ func (video *Video) configureWithSize(width, height int, fullscreen bool) error
gl.VertexAttribPointerWithOffset(texCoordAttrib, 2, gl.FLOAT, false, 4*4, 2*4)
// Some cores won't call SetPixelFormat, provide default values
video.ensurePixelFormatDefaults()
if video.pixFmt == 0 {
video.pixFmt = gl.UNSIGNED_SHORT_5_5_5_1
video.pixType = gl.BGRA
video.bpp = 2
}
if video.Geom.MaxWidth == 0 || video.Geom.MaxHeight == 0 {
video.Geom.MaxWidth = video.Geom.BaseWidth
video.Geom.MaxHeight = video.Geom.BaseHeight
}
video.texWidth = int32(video.Geom.MaxWidth)
video.texHeight = int32(video.Geom.MaxHeight)
if video.texWidth < 1 {
video.texWidth = 1
gl.GenTextures(1, &video.texID)
if video.texID == 0 && state.Verbose {
log.Fatalln("[Video]: Failed to create the vid texture")
}
if video.texHeight < 1 {
video.texHeight = 1
}
video.resetGameTexture(video.texWidth, video.texHeight)
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, video.texID)
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, int32(video.Geom.MaxWidth), int32(video.Geom.MaxHeight), 0, video.pixType, video.pixFmt, nil)
video.UpdateFilter(settings.Current.VideoFilter)
@@ -496,7 +243,6 @@ func (video *Video) configureWithSize(width, height int, fullscreen bool) error
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
@@ -536,7 +282,7 @@ 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.texWidth), float32(video.texHeight))
gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("TextureSize\x00")), float32(video.Geom.MaxWidth), float32(video.Geom.MaxHeight))
gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("InputSize\x00")), float32(video.width), float32(video.height))
gl.UseProgram(0)
}
@@ -549,7 +295,7 @@ func (video *Video) SetPixelFormat(format uint32) bool {
}
// PixelStorei also needs to be updated whenever bpp changes
defer func() { video.needUpload = true }()
defer gl.PixelStorei(gl.UNPACK_ROW_LENGTH, video.pitch/video.bpp)
switch format {
case libretro.PixelFormat0RGB1555:
@@ -586,38 +332,9 @@ func (video *Video) ResetRot() {
video.rot = 0
}
// 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) {
// 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, fbHeight, clipWidth, clipHeight int) (x, y, w, h float32) {
// Scale the content to fit in the viewport.
fbw := float32(fbWidth)
fbh := float32(fbHeight)
@@ -635,33 +352,14 @@ func (video *Video) gameViewport(fbWidth, fbHeight int) (x, y, w, h float32) {
w = fbw
}
// 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
// Place the content in the middle of the window.
x = (fbw - w) / 2
y = (fbh - h) / 2
video.presentX = int32(x)
video.presentY = int32(y)
video.presentW = int32(w)
video.presentH = int32(h)
return
}
va := video.vertexArray(x, y, w, h, 1.0)
// 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[3] = float32(clipHeight) / float32(video.Geom.MaxHeight)
va[10] = float32(clipWidth) / float32(video.Geom.MaxWidth)
va[11] = va[3]
va[14] = va[10]
@@ -672,46 +370,30 @@ func (video *Video) coreRatioViewport(fbWidth, fbHeight, clipWidth, clipHeight i
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 draws the current game frame using the frontend context/state.
// Render the current frame
func (video *Video) Render() {
// Render directly to the screen
bindBackbuffer()
// We can't trust the core to leave the OpenGL in the same state as
// before retro_run() was called so we restore some state manually.
gl.Disable(gl.DEPTH_TEST)
gl.Disable(gl.CULL_FACE)
gl.Disable(gl.DITHER)
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.Enable(gl.TEXTURE_2D)
video.ResizeViewport()
if !state.CoreRunning {
gl.ClearColor(1, 1, 1, 1)
gl.Clear(gl.COLOR_BUFFER_BIT)
@@ -721,20 +403,16 @@ func (video *Video) Render() {
gl.ClearColor(0, 0, 0, 1)
gl.Clear(gl.COLOR_BUFFER_BIT)
// Wait for a valid refresh, not merely an allocated HW framebuffer.
if video.width == 0 || video.height == 0 {
// 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 state.Core.HWRenderCallback == nil && video.pitch == 0 {
return
}
video.uploadTexture()
fbw, fbh := video.Window.GetFramebufferSize()
_, _, 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)
if state.Core.HWRenderCallback != nil {
@@ -746,10 +424,7 @@ func (video *Video) Render() {
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
@@ -757,131 +432,30 @@ func (video *Video) Render() {
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) {
// NULL duplicates the last frame; its dimensions need not be supplied.
if data == nil {
return
}
video.needUpload = true
bindBackbuffer()
video.width = width
video.height = height
video.pitch = pitch
video.data = data
}
func (video *Video) uploadTexture() {
if !video.needUpload || video.data == nil {
return
}
gl.BindTexture(gl.TEXTURE_2D, video.texID)
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, video.pitch/video.bpp)
gl.UseProgram(video.program)
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)
if data != nil && data != libretro.HWFrameBufferValid {
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, int32(video.Geom.MaxWidth), int32(video.Geom.MaxHeight), 0, video.pixType, video.pixFmt, 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.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("TextureSize\x00")), float32(video.Geom.MaxWidth), float32(video.Geom.MaxHeight))
gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("InputSize\x00")), float32(width), float32(height))
gl.UseProgram(0)
video.needUpload = false
}
// CurrentFramebuffer returns the current FBO ID
+80
View File
@@ -0,0 +1,80 @@
//go:build darwin
// +build darwin
package video
import (
"log"
"github.com/go-gl/gl/v4.1-core/gl"
"github.com/go-gl/mathgl/mgl32"
"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)
log.Printf("[Video]: Initializing HW render (%v x %v).\n", width, height)
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, video.texID)
gl.TexStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8, width, height)
gl.GenFramebuffers(1, &video.fboID)
gl.BindFramebuffer(gl.FRAMEBUFFER, video.fboID)
gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, video.texID, 0)
hw := state.Core.HWRenderCallback
if hw.Depth {
gl.GenRenderbuffers(1, &video.rboID)
gl.BindRenderbuffer(gl.RENDERBUFFER, video.rboID)
format := gl.DEPTH_COMPONENT16
if hw.Stencil {
format = gl.DEPTH24_STENCIL8
}
gl.RenderbufferStorage(gl.RENDERBUFFER, uint32(format), width, height)
gl.BindRenderbuffer(gl.RENDERBUFFER, 0)
if hw.Stencil {
gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, video.rboID)
} else {
gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_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 := gl.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE {
log.Fatalf("[Video] Framebuffer is not complete. Error: %v\n", st)
}
bindBackbuffer()
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() {
gl.BindFramebuffer(gl.FRAMEBUFFER, 0)
}
func genVertexArrays(n int32, arrays *uint32) {
gl.GenVertexArrays(n, arrays)
}
func bindVertexArray(array uint32) {
gl.BindVertexArray(array)
}
+80
View File
@@ -0,0 +1,80 @@
//go:build !darwin
// +build !darwin
package video
import (
"log"
"github.com/go-gl/gl/v4.1-core/gl"
"github.com/go-gl/mathgl/mgl32"
"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)
log.Printf("[Video]: Initializing HW render (%v x %v).\n", width, height)
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, video.texID)
gl.TexStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8, width, height)
gl.GenFramebuffers(1, &video.fboID)
gl.BindFramebuffer(gl.FRAMEBUFFER, video.fboID)
gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, video.texID, 0)
hw := state.Core.HWRenderCallback
if hw.Depth {
gl.GenRenderbuffers(1, &video.rboID)
gl.BindRenderbuffer(gl.RENDERBUFFER, video.rboID)
format := gl.DEPTH_COMPONENT16
if hw.Stencil {
format = gl.DEPTH24_STENCIL8
}
gl.RenderbufferStorage(gl.RENDERBUFFER, uint32(format), width, height)
gl.BindRenderbuffer(gl.RENDERBUFFER, 0)
if hw.Stencil {
gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, video.rboID)
} else {
gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_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 := gl.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE {
log.Fatalf("[Video] Framebuffer is not complete. Error: %v\n", st)
}
bindBackbuffer()
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() {
gl.BindFramebuffer(gl.FRAMEBUFFER, 0)
}
func genVertexArrays(n int32, arrays *uint32) {
gl.GenVertexArrays(n, arrays)
}
func bindVertexArray(array uint32) {
gl.BindVertexArray(array)
}