mirror of
https://github.com/libretro/ludo.git
synced 2026-11-07 17:27:59 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a2dc452c7 | ||
|
|
c47ad16a70 | ||
|
|
13347e0051 | ||
|
|
31951498f7 | ||
|
|
2c451ecd6a | ||
|
|
8ac8018272 | ||
|
|
35471f0873 | ||
|
|
5cb7b67c76 | ||
|
|
98a4419151 | ||
|
|
6d41afd5ee | ||
|
|
601bdfacaa |
No files matched your search
+26
-14
@@ -210,7 +210,7 @@ jobs:
|
||||
asset_paths: '["./Ludo-*.tar.gz*", "./*.deb"]'
|
||||
|
||||
build_osx_x86_64:
|
||||
runs-on: macos-15-intel
|
||||
runs-on: macos-26-intel
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -277,6 +277,9 @@ jobs:
|
||||
|
||||
build_windows_x86_64:
|
||||
runs-on: windows-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: msys2 {0}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -284,23 +287,32 @@ jobs:
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: echo "VERSION=$(echo ${{ github.event.release.tag_name }} | cut -c 2-)" >> $GITHUB_ENV
|
||||
- run: choco install wget make hashdeep --ignore-checksums
|
||||
- run: wget --no-check-certificate http://www.openal-soft.org/openal-binaries/openal-soft-1.21.0-bin.zip
|
||||
- run: wget https://github.com/electron/rcedit/releases/download/v1.1.1/rcedit-x64.exe
|
||||
- run: curl -L -o mingw64.zip https://github.com/brechtsanders/winlibs_mingw/releases/download/13.2.0posix-17.0.6-11.0.1-ucrt-r5/winlibs-x86_64-posix-seh-gcc-13.2.0-llvm-17.0.6-mingw-w64ucrt-11.0.1-r5.zip
|
||||
- run: unzip mingw64.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: cp "mingw64/bin/libwinpthread-1.dll" .
|
||||
- run: cp /c/openal-soft-1.21.0-bin/bin/Win64/soft_oal.dll OpenAL32.dll
|
||||
- uses: msys2/setup-msys2@v2
|
||||
with:
|
||||
msystem: UCRT64
|
||||
update: true
|
||||
path-type: inherit
|
||||
install: >-
|
||||
mingw-w64-ucrt-x86_64-toolchain
|
||||
mingw-w64-ucrt-x86_64-openal
|
||||
make
|
||||
p7zip
|
||||
mingw-w64-ucrt-x86_64-rcedit
|
||||
- run: echo "VERSION=$(echo ${{ github.event.release.tag_name }} | cut -c 2-)" >> "$GITHUB_ENV"
|
||||
- run: cp /ucrt64/bin/rcedit.exe ./rcedit-x64
|
||||
- run: cp /ucrt64/lib/libopenal.dll.a ./libOpenAL32.dll.a
|
||||
- run: echo "CGO_LDFLAGS=-L$PWD" >> "$GITHUB_ENV"
|
||||
- run: cp /ucrt64/bin/libgcc_s_seh-1.dll .
|
||||
- run: cp /ucrt64/bin/libmysofa.dll .
|
||||
- run: cp /ucrt64/bin/libopenal-1.dll .
|
||||
- run: cp /ucrt64/bin/libstdc++-6.dll .
|
||||
- run: cp /ucrt64/bin/libwinpthread-1.dll .
|
||||
- run: cp /ucrt64/bin/zlib1.dll .
|
||||
- run: cp /c/Windows/System32/VCRUNTIME140.dll .
|
||||
- run: PATH="/c/mingw64/bin:$PATH"
|
||||
- run: mkdir -p ./Ludo-Windows-x86_64-${VERSION}/
|
||||
- run: cp *.dll ./Ludo-Windows-x86_64-${VERSION}/
|
||||
- run: OS=Windows ARCH=x86_64 VERSION=$VERSION make zip
|
||||
- run: hashdeep -c sha256 Ludo-Windows-x86_64-${VERSION}.zip > Ludo-Windows-x86_64-${VERSION}.zip.sha256
|
||||
- run: sha256sum Ludo-Windows-x86_64-${VERSION}.zip > Ludo-Windows-x86_64-${VERSION}.zip.sha256
|
||||
|
||||
- name: Upload Release Asset
|
||||
uses: alexellis/[email protected]
|
||||
|
||||
+19
-13
@@ -160,7 +160,7 @@ jobs:
|
||||
- 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-15-intel
|
||||
runs-on: macos-26-intel
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -197,6 +197,9 @@ jobs:
|
||||
|
||||
build_windows_x86_64:
|
||||
runs-on: windows-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: msys2 {0}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -204,19 +207,22 @@ jobs:
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.0'
|
||||
- run: echo "/c/Users/runneradmin/go/bin" >> $GITHUB_PATH
|
||||
- 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
|
||||
- run: choco install wget --ignore-checksums
|
||||
- run: curl -L -o mingw64.zip https://github.com/brechtsanders/winlibs_mingw/releases/download/13.2.0posix-17.0.6-11.0.1-ucrt-r5/winlibs-x86_64-posix-seh-gcc-13.2.0-llvm-17.0.6-mingw-w64ucrt-11.0.1-r5.zip
|
||||
- run: unzip mingw64.zip
|
||||
- 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: cp "mingw64/bin/libwinpthread-1.dll" .
|
||||
- run: PATH="/c/mingw64/bin:$PATH"
|
||||
# golang.org/x/mobile/exp/audio/al hardcodes -lOpenAL32 on Windows,
|
||||
# while the MSYS2 package installs libopenal.dll.a.
|
||||
- run: cp /ucrt64/lib/libopenal.dll.a ./libOpenAL32.dll.a
|
||||
- run: echo "CGO_LDFLAGS=-L$PWD" >> "$GITHUB_ENV"
|
||||
- run: go get .
|
||||
#- run: go test -v -race ./...
|
||||
#- run: go vet ./...
|
||||
- run: go test -v -race ./...
|
||||
- run: go vet ./...
|
||||
- run: staticcheck ./...
|
||||
- run: go build -v -ldflags '-H=windowsgui'
|
||||
@@ -11,14 +11,22 @@ ifeq ($(OS), Linux)
|
||||
BUNDLENAME = $(APP)-$(OS)-$(DISPDRIVER)-$(ARCH)-$(VERSION)
|
||||
endif
|
||||
|
||||
CORES = 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 mupen64plus_next np2kai o2em pcsx_rearmed picodrive pokemini prosystem snes9x stella2014 vecx virtualjaguar
|
||||
CORES = ppsspp dolphin atari800 bluemsx citra swanstation fbneo fceumm flycast gambatte gearsystem genesis_plus_gx handy lutro mednafen_ngp mednafen_pce mednafen_pce_fast mednafen_pcfx mednafen_psx mednafen_saturn mednafen_supergrafx mednafen_vb mednafen_wswan mgba melonds mupen64plus_next np2kai o2em pcsx2 pcsx_rearmed picodrive pokemini prosystem snes9x stella2014 vecx virtualjaguar
|
||||
|
||||
ifeq ($(ARCH), arm)
|
||||
CORES := $(filter-out swanstation,$(CORES))
|
||||
CORES := $(filter-out mupen64plus_next,$(CORES))
|
||||
CORES := $(filter-out mednafen_pcfx,$(CORES))
|
||||
CORES := $(filter-out mednafen_saturn,$(CORES))
|
||||
CORES := $(filter-out melonds,$(CORES))
|
||||
CORES := $(filter-out citra dolphin flycast mednafen_pcfx mednafen_saturn melonds mupen64plus_next pcsx2 ppsspp swanstation,$(CORES))
|
||||
endif
|
||||
ifeq ($(ARCH), arm64)
|
||||
CORES := $(filter-out pcsx2,$(CORES))
|
||||
ifeq ($(OS), Linux)
|
||||
CORES := $(filter-out mupen64plus_next,$(CORES))
|
||||
endif
|
||||
endif
|
||||
ifeq ($(OS), OSX)
|
||||
CORES := $(filter-out pcsx2,$(CORES))
|
||||
ifeq ($(ARCH), x86_64)
|
||||
CORES := $(filter-out citra,$(CORES))
|
||||
endif
|
||||
endif
|
||||
|
||||
DYLIBS = $(addprefix cores/, $(addsuffix _libretro.dylib,$(CORES)))
|
||||
|
||||
@@ -4,7 +4,7 @@ Ludo is a work in progress libretro frontend written in go.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/kivutar/ludo-assets/master/illustration.png" />
|
||||
|
||||
It is able to launch most non GL libretro cores.
|
||||
It supports software-rendered and desktop OpenGL libretro cores. Hardware-rendered cores may require a newer OpenGL version than the frontend itself.
|
||||
|
||||
It works on OSX, Linux, Linux ARM and Windows. You can download releases [here](https://github.com/libretro/ludo/releases)
|
||||
|
||||
|
||||
+166
-59
@@ -4,9 +4,13 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
@@ -19,7 +23,7 @@ import (
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/video"
|
||||
|
||||
"github.com/mholt/archiver/v3"
|
||||
"github.com/mholt/archives"
|
||||
)
|
||||
|
||||
var vid *video.Video
|
||||
@@ -31,6 +35,7 @@ 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
|
||||
@@ -47,6 +52,7 @@ 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)
|
||||
@@ -78,60 +84,134 @@ func unarchiveGame(filename string) (string, int64, error) {
|
||||
path := ""
|
||||
size := int64(0)
|
||||
dst := os.TempDir()
|
||||
ctx := context.Background()
|
||||
|
||||
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)
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return path, size, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
format, stream, err := archives.Identify(ctx, filepath.Base(filename), file)
|
||||
if err != nil {
|
||||
return path, size, err
|
||||
}
|
||||
|
||||
extPriority := 0 // 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
|
||||
extPrefered[".pbp"] = 3
|
||||
extPriority := 0
|
||||
extPrefered := map[string]int{
|
||||
".cue": 1,
|
||||
".m3u": 2,
|
||||
".pbp": 3,
|
||||
}
|
||||
|
||||
err = archiver.Walk(filename, func(f archiver.File) error {
|
||||
fname := f.Name()
|
||||
ext := filepath.Ext(fname)
|
||||
selectGame := func(gamePath string, fsize int64) {
|
||||
ext := filepath.Ext(gamePath)
|
||||
if size == 0 {
|
||||
// By default select the first file of the archive
|
||||
path = filepath.Join(dst, fname)
|
||||
size = f.Size()
|
||||
path = gamePath
|
||||
size = fsize
|
||||
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 = filepath.Join(dst, fname)
|
||||
size = f.Size()
|
||||
path = gamePath
|
||||
size = fsize
|
||||
log.Println("find a better file in archive:", path, size)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return path, size, err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// LoadGame loads a game. A core has to be loaded first.
|
||||
@@ -140,16 +220,17 @@ func LoadGame(gamePath string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// If we're loading a new game on the same core, save the RAM of the previous
|
||||
// game before closing it.
|
||||
if state.GamePath != gamePath {
|
||||
UnloadGame()
|
||||
}
|
||||
|
||||
if state.Core == nil {
|
||||
return errors.New("no core loaded")
|
||||
}
|
||||
|
||||
// Reload the core to fully tear down the previous game and its GL context.
|
||||
if state.CoreRunning {
|
||||
if err := Load(state.CorePath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
si := state.Core.GetSystemInfo()
|
||||
|
||||
gi, err := getGameInfo(gamePath, si.BlockExtract)
|
||||
@@ -173,7 +254,7 @@ func LoadGame(gamePath string) error {
|
||||
|
||||
ok := state.Core.LoadGame(*gi)
|
||||
if !ok {
|
||||
state.CoreRunning = false
|
||||
Unload()
|
||||
return errors.New("failed to load the game")
|
||||
}
|
||||
|
||||
@@ -205,30 +286,56 @@ func LoadGame(gamePath string) error {
|
||||
log.Println("[Core]: Game loaded: " + gamePath)
|
||||
savefiles.LoadSRAM()
|
||||
|
||||
if state.Core.HWRenderCallback != nil {
|
||||
if err := vid.ResetHWContext(); err != nil {
|
||||
Unload()
|
||||
return fmt.Errorf("create hardware context: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unload unloads a libretro core
|
||||
// Unload closes the game and core before destroying their GL context.
|
||||
func Unload() {
|
||||
if state.Core != nil {
|
||||
UnloadGame()
|
||||
savefiles.SaveSRAM()
|
||||
vid.RunContextDestroy()
|
||||
vid.BeginCore()
|
||||
if state.CoreRunning {
|
||||
state.Core.UnloadGame()
|
||||
}
|
||||
state.Core.Deinit()
|
||||
vid.EndCore()
|
||||
vid.ResetCoreState()
|
||||
state.GamePath = ""
|
||||
state.CoreRunning = false
|
||||
state.CorePath = ""
|
||||
state.Core = nil
|
||||
vid.SetHWRenderContext(nil)
|
||||
Options = nil
|
||||
}
|
||||
}
|
||||
|
||||
// UnloadGame unloads a game.
|
||||
func UnloadGame() {
|
||||
if state.CoreRunning {
|
||||
savefiles.SaveSRAM()
|
||||
state.Core.UnloadGame()
|
||||
state.GamePath = ""
|
||||
state.CoreRunning = false
|
||||
vid.ResetPitch()
|
||||
vid.ResetRot()
|
||||
}
|
||||
// Reset resets the game using its hardware context, if any.
|
||||
func Reset() {
|
||||
vid.BeginCore()
|
||||
defer vid.EndCore()
|
||||
state.Core.Reset()
|
||||
}
|
||||
|
||||
// Serialize captures core state with the core's GL context current.
|
||||
func Serialize() ([]byte, error) {
|
||||
vid.BeginCore()
|
||||
defer vid.EndCore()
|
||||
return state.Core.Serialize(state.Core.SerializeSize())
|
||||
}
|
||||
|
||||
// Unserialize restores core state with the core's GL context current.
|
||||
func Unserialize(data []byte) error {
|
||||
vid.BeginCore()
|
||||
defer vid.EndCore()
|
||||
return state.Core.Unserialize(data, uint(len(data)))
|
||||
}
|
||||
|
||||
// getGameInfo opens a rom and return the libretro.GameInfo needed to launch it
|
||||
|
||||
+37
-7
@@ -1,10 +1,10 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -15,6 +15,34 @@ import (
|
||||
"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 }
|
||||
@@ -201,17 +229,21 @@ func Test_coreLoadGame(t *testing.T) {
|
||||
|
||||
Init(&video.Video{})
|
||||
|
||||
if err := glfw.Init(); err != nil {
|
||||
log.Fatalln("failed to initialize glfw")
|
||||
var initErr error
|
||||
onMainThread(func() { initErr = glfw.Init() })
|
||||
if initErr != nil {
|
||||
t.Fatalf("glfw.Init() error = %v", initErr)
|
||||
}
|
||||
t.Cleanup(glfw.Terminate)
|
||||
t.Cleanup(func() { onMainThread(glfw.Terminate) })
|
||||
|
||||
if err := Load("testdata/vecx_libretro" + ext); err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
|
||||
var loadGameErr error
|
||||
got := utils.CaptureOutput(func() { loadGameErr = LoadGame("testdata/Polar Rescue (USA).vec") })
|
||||
got := utils.CaptureOutput(func() {
|
||||
onMainThread(func() { loadGameErr = LoadGame("testdata/Polar Rescue (USA).vec") })
|
||||
})
|
||||
if loadGameErr != nil {
|
||||
t.Fatalf("LoadGame() error = %v", loadGameErr)
|
||||
}
|
||||
@@ -235,7 +267,6 @@ func Test_coreLoadGame(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
UnloadGame()
|
||||
Unload()
|
||||
|
||||
t.Run("Global state should be cleared by Unload", func(t *testing.T) {
|
||||
@@ -252,7 +283,6 @@ func Test_coreLoadGame(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_coreLoadGameWithoutCore(t *testing.T) {
|
||||
UnloadGame()
|
||||
Unload()
|
||||
|
||||
err := LoadGame("testdata/Polar Rescue (USA).vec")
|
||||
|
||||
@@ -143,6 +143,30 @@ func environment(cmd uint32, data unsafe.Pointer) bool {
|
||||
state.Core.SetFrameTimeCallback(data)
|
||||
case libretro.EnvironmentSetAudioCallback:
|
||||
state.Core.SetAudioCallback(data)
|
||||
case libretro.EnvironmentSetHWRender:
|
||||
hw := libretro.SetHWRenderCallback(
|
||||
data,
|
||||
vid.CurrentFramebuffer,
|
||||
vid.ProcAddress)
|
||||
if (hw.HWContextType != libretro.HWContextOpenGL && hw.HWContextType != libretro.HWContextOpenGLCore) || hw.ContextReset == nil {
|
||||
return false
|
||||
}
|
||||
vid.SetHWRenderContext(hw)
|
||||
if vid != nil && vid.Window != nil && !state.CoreRunning {
|
||||
if err := vid.ReconfigureHWContext(settings.Current.VideoFullscreen); err != nil {
|
||||
vid.SetHWRenderContext(state.Core.HWRenderCallback)
|
||||
log.Printf("[Env]: Could not create HW context %d (%d.%d): %v",
|
||||
hw.HWContextType, hw.VersionMajor, hw.VersionMinor, err)
|
||||
return false
|
||||
}
|
||||
state.MenuContextResetNeeded = true
|
||||
}
|
||||
state.Core.HWRenderCallback = hw
|
||||
log.Println("[Env]: HWContextType:", hw.HWContextType)
|
||||
return true
|
||||
case libretro.EnvironmentSetHWSharedContext:
|
||||
// All HW cores already get an isolated context shared with the frontend.
|
||||
return true
|
||||
case libretro.EnvironmentGetCanDupe:
|
||||
libretro.SetBool(data, true)
|
||||
case libretro.EnvironmentSetPixelFormat:
|
||||
@@ -151,6 +175,8 @@ func environment(cmd uint32, data unsafe.Pointer) bool {
|
||||
return environmentGetSystemDirectory(data)
|
||||
case libretro.EnvironmentGetSaveDirectory:
|
||||
return environmentGetSaveDirectory(data)
|
||||
case libretro.EnvironmentGetPrefferedHWRender:
|
||||
libretro.SetUint(data, uint(libretro.HWContextOpenGL))
|
||||
case libretro.EnvironmentShutdown:
|
||||
vid.SetShouldClose(true)
|
||||
case libretro.EnvironmentGetCoreOptionsVersion:
|
||||
|
||||
@@ -7,30 +7,39 @@ require (
|
||||
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/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/archiver/v3 v3.5.1
|
||||
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.39.0
|
||||
golang.org/x/image v0.41.0
|
||||
golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/STARRY-S/zip v0.2.3 // indirect
|
||||
github.com/andybalholm/brotli v1.2.1 // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/klauspost/compress v1.18.5 // 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/nwaples/rardecode v1.1.3 // 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/xi2/xz v0.0.0-20171230120015-48954b6210f8 // 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
|
||||
|
||||
@@ -1,92 +1,343 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/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.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
|
||||
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
|
||||
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
|
||||
github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4=
|
||||
github.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8=
|
||||
github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
|
||||
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
|
||||
github.com/cavaliercoder/grab v2.0.0+incompatible h1:wZHbBQx56+Yxjx2TCGDcenhh3cJn7cCLMfkEPmySTSE=
|
||||
github.com/cavaliercoder/grab v2.0.0+incompatible/go.mod h1:tTBkfNqSBfuMmMBFaO2phgyhdYhiZQ/+iXCZDzcDsMI=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY=
|
||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
|
||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276 h1:IO5P06Pcj9K04d+l4nrf3c2U56+dAotIFG6u4P1wAHI=
|
||||
github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/mathgl v1.0.0 h1:t9DznWJlXxxjeeKLIdovCOVJQk/GzDEL7h/h+Ro2B68=
|
||||
github.com/go-gl/mathgl v1.0.0/go.mod h1:yhpkQzEiH9yPyxDUGzkmgScbaBVlhC06qodikEM0ZwQ=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
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.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw=
|
||||
github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/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/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.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc=
|
||||
github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
||||
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.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
|
||||
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
|
||||
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
|
||||
github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tanema/gween v0.0.0-20250522035225-e874ee3ae01a h1:xINbk646UroMWqLNbMrce7FisMOWQXRG7a+JLQ5xz9o=
|
||||
github.com/tanema/gween v0.0.0-20250522035225-e874ee3ae01a/go.mod h1:XXpz+9IVhUY5vTC5gXRNSjLDVwQWa5KM43NrH1GJa4M=
|
||||
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.9/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/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/youpy/go-riff v0.1.0 h1:vZO/37nI4tIET8tQI0Qn0Y79qQh99aEpponTPiPut7k=
|
||||
github.com/youpy/go-riff v0.1.0/go.mod h1:83nxdDV4Z9RzrTut9losK7ve4hUnxUR8ASSz4BsKXwQ=
|
||||
github.com/youpy/go-wav v0.3.2 h1:NLM8L/7yZ0Bntadw/0h95OyUsen+DQIVf9gay+SUsMU=
|
||||
github.com/youpy/go-wav v0.3.2/go.mod h1:0FCieAXAeSdcxFfwLpRuEo0PFmAoc+8NU34h7TUvk50=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zaf/g711 v0.0.0-20190814101024-76a4a538f52b/go.mod h1:T2h1zV50R/q0CVYnsQOQ6L7P4a2ZxH47ixWcMXFGyx8=
|
||||
github.com/zaf/g711 v1.4.0 h1:XZYkjjiAg9QTBnHqEg37m2I9q3IIDv5JRYXs2N8ma7c=
|
||||
github.com/zaf/g711 v1.4.0/go.mod h1:eCDXt3dSp/kYYAoooba7ukD/Q75jvAaS4WOMr0l1Roo=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
|
||||
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
|
||||
golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
|
||||
golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
|
||||
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b h1:Qt2eaXcZ8x20iAcoZ6AceeMMtnjuPHvC51KRCH1DKSQ=
|
||||
golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b/go.mod h1:5Fu78lew5ucMXt8w2KYcwvxu2rkC/liHzUvaoiI+H/M=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
+47
-137
@@ -6,121 +6,36 @@ package libretro
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <pthread.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <mach/semaphore.h>
|
||||
#include <mach/task.h>
|
||||
#include <mach/mach_init.h>
|
||||
#define SEM_T semaphore_t
|
||||
#define SEM_INIT(x) semaphore_create(mach_task_self(), &x, SYNC_POLICY_FIFO, 0);
|
||||
#define SEM_POST(x) semaphore_signal(x)
|
||||
#define SEM_WAIT(x) semaphore_wait(x)
|
||||
#else
|
||||
#include <semaphore.h>
|
||||
#define SEM_T sem_t
|
||||
#define SEM_INIT(x) sem_init(&x, 0, 0)
|
||||
#define SEM_POST(x) sem_post(&x)
|
||||
#define SEM_WAIT(x) sem_wait(&x)
|
||||
#endif
|
||||
|
||||
#if 0
|
||||
#define print_sema(...) (printf(__VA_ARGS__))
|
||||
#else
|
||||
#define print_sema(...) do {} while (0)
|
||||
#endif
|
||||
|
||||
enum {
|
||||
CMD_F,
|
||||
CMD_SERIALIZE,
|
||||
};
|
||||
|
||||
struct thread_cmd_t {
|
||||
int cmd;
|
||||
void* f;
|
||||
void* arg1;
|
||||
void* arg2;
|
||||
void* arg3;
|
||||
void* arg4;
|
||||
void* res;
|
||||
};
|
||||
|
||||
struct thread_cmd_t s_job;
|
||||
static pthread_t s_thread;
|
||||
static SEM_T s_sem_do;
|
||||
static SEM_T s_sem_done;
|
||||
static bool s_use_thread = false;
|
||||
static uintptr_t s_current_framebuffer = 0;
|
||||
|
||||
void* emu_thread_loop(void *a0) {
|
||||
print_sema("begin thread\n");
|
||||
|
||||
SEM_POST(s_sem_done);
|
||||
|
||||
print_sema("signal thread\n");
|
||||
|
||||
while (1) {
|
||||
print_sema("wait do\n");
|
||||
SEM_WAIT(s_sem_do);
|
||||
|
||||
print_sema("do\n");
|
||||
switch (s_job.cmd) {
|
||||
case CMD_F:
|
||||
((void (*)(void))s_job.f)();
|
||||
break;
|
||||
case CMD_SERIALIZE: {
|
||||
bool res = ((bool (*)(void*, size_t))s_job.f)(s_job.arg1, *(size_t*)s_job.arg2);
|
||||
*(bool*)s_job.res = res;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
print_sema("signal done\n");
|
||||
SEM_POST(s_sem_done);
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
|
||||
void thread_sync() {
|
||||
// Fire the job
|
||||
print_sema("signal do\n");
|
||||
SEM_POST(s_sem_do);
|
||||
|
||||
// Wait the result
|
||||
print_sema("wait done\n");
|
||||
SEM_WAIT(s_sem_done);
|
||||
|
||||
print_sema("done\n");
|
||||
int cothread_init() {
|
||||
return pthread_create(&s_thread, NULL, emu_thread_loop, NULL);
|
||||
}
|
||||
|
||||
void run_wrapper(void *f) {
|
||||
if (s_use_thread) {
|
||||
s_job.cmd = CMD_F;
|
||||
s_job.f = f;
|
||||
thread_sync();
|
||||
} else {
|
||||
((void (*)(void))f)();
|
||||
}
|
||||
void cothread_deinit() {
|
||||
pthread_join(s_thread, NULL);
|
||||
}
|
||||
|
||||
void cothread_init() {
|
||||
s_use_thread = true;
|
||||
|
||||
SEM_INIT(s_sem_do);
|
||||
SEM_INIT(s_sem_done);
|
||||
|
||||
print_sema("create thread\n");
|
||||
pthread_create(&s_thread, NULL, emu_thread_loop, NULL);
|
||||
|
||||
print_sema("wait thread\n");
|
||||
SEM_WAIT(s_sem_done);
|
||||
bool cothread_is_current() {
|
||||
return pthread_equal(pthread_self(), s_thread);
|
||||
}
|
||||
|
||||
void bridge_retro_init(void *f) {
|
||||
run_wrapper(f);
|
||||
((void (*)(void))f)();
|
||||
}
|
||||
|
||||
void bridge_retro_deinit(void *f) {
|
||||
run_wrapper(f);
|
||||
((void (*)(void))f)();
|
||||
}
|
||||
|
||||
unsigned bridge_retro_api_version(void *f) {
|
||||
@@ -131,6 +46,14 @@ void bridge_retro_frame_time_callback(retro_frame_time_callback_t f, retro_usec_
|
||||
f(usec);
|
||||
}
|
||||
|
||||
void bridge_retro_hw_context_reset(retro_hw_context_reset_t f) {
|
||||
f();
|
||||
}
|
||||
|
||||
void bridge_retro_hw_context_destroy(retro_hw_context_reset_t f) {
|
||||
f();
|
||||
}
|
||||
|
||||
void bridge_retro_audio_callback(retro_audio_callback_t f) {
|
||||
f();
|
||||
}
|
||||
@@ -152,7 +75,7 @@ void bridge_retro_set_environment(void *f, void *callback) {
|
||||
}
|
||||
|
||||
void bridge_retro_set_video_refresh(void *f, void *callback) {
|
||||
((bool (*)(retro_video_refresh_t))f)((retro_video_refresh_t)callback);
|
||||
((void (*)(retro_video_refresh_t))f)((retro_video_refresh_t)callback);
|
||||
}
|
||||
|
||||
void bridge_retro_set_controller_port_device(void *f, unsigned port, unsigned device) {
|
||||
@@ -160,23 +83,23 @@ void bridge_retro_set_controller_port_device(void *f, unsigned port, unsigned de
|
||||
}
|
||||
|
||||
void bridge_retro_set_input_poll(void *f, void *callback) {
|
||||
((bool (*)(retro_input_poll_t))f)((retro_input_poll_t)callback);
|
||||
((void (*)(retro_input_poll_t))f)((retro_input_poll_t)callback);
|
||||
}
|
||||
|
||||
void bridge_retro_set_input_state(void *f, void *callback) {
|
||||
((bool (*)(retro_input_state_t))f)((retro_input_state_t)callback);
|
||||
((void (*)(retro_input_state_t))f)((retro_input_state_t)callback);
|
||||
}
|
||||
|
||||
void bridge_retro_set_audio_sample(void *f, void *callback) {
|
||||
((bool (*)(retro_audio_sample_t))f)((retro_audio_sample_t)callback);
|
||||
((void (*)(retro_audio_sample_t))f)((retro_audio_sample_t)callback);
|
||||
}
|
||||
|
||||
void bridge_retro_set_audio_sample_batch(void *f, void *callback) {
|
||||
((bool (*)(retro_audio_sample_batch_t))f)((retro_audio_sample_batch_t)callback);
|
||||
((void (*)(retro_audio_sample_batch_t))f)((retro_audio_sample_batch_t)callback);
|
||||
}
|
||||
|
||||
bool bridge_retro_load_game(void *f, struct retro_game_info *gi) {
|
||||
return ((bool (*)(struct retro_game_info *))f)(gi);
|
||||
return ((bool (*)(const struct retro_game_info *))f)(gi);
|
||||
}
|
||||
|
||||
size_t bridge_retro_serialize_size(void *f) {
|
||||
@@ -184,49 +107,23 @@ size_t bridge_retro_serialize_size(void *f) {
|
||||
}
|
||||
|
||||
bool bridge_retro_serialize(void *f, void *data, size_t size) {
|
||||
if (s_use_thread) {
|
||||
bool res;
|
||||
s_job.cmd = CMD_SERIALIZE;
|
||||
s_job.f = f;
|
||||
s_job.arg1 = data;
|
||||
s_job.arg2 = &size;
|
||||
s_job.res = &res;
|
||||
|
||||
thread_sync();
|
||||
|
||||
return s_job.res;
|
||||
} else {
|
||||
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) {
|
||||
if (s_use_thread) {
|
||||
bool res;
|
||||
s_job.cmd = CMD_SERIALIZE; // Same command format for both serialize & unserialize
|
||||
s_job.f = f;
|
||||
s_job.arg1 = data;
|
||||
s_job.arg2 = &size;
|
||||
s_job.res = &res;
|
||||
|
||||
thread_sync();
|
||||
|
||||
return s_job.res;
|
||||
} else {
|
||||
return ((bool (*)(void*, size_t))f)(data, size);
|
||||
}
|
||||
return ((bool (*)(const void*, size_t))f)(data, size);
|
||||
}
|
||||
|
||||
void bridge_retro_unload_game(void *f) {
|
||||
run_wrapper(f);
|
||||
((void (*)(void))f)();
|
||||
}
|
||||
|
||||
void bridge_retro_run(void *f) {
|
||||
run_wrapper(f);
|
||||
((void (*)(void))f)();
|
||||
}
|
||||
|
||||
void bridge_retro_reset(void *f) {
|
||||
run_wrapper(f);
|
||||
((void (*)(void))f)();
|
||||
}
|
||||
|
||||
size_t bridge_retro_get_memory_size(void *f, unsigned id) {
|
||||
@@ -303,5 +200,18 @@ int64_t coreGetTimeUsec_cgo() {
|
||||
return coreGetTimeUsec();
|
||||
}
|
||||
|
||||
uintptr_t coreGetCurrentFramebuffer_cgo() {
|
||||
return s_current_framebuffer;
|
||||
}
|
||||
|
||||
void coreSetCurrentFramebuffer_cgo(uintptr_t fb) {
|
||||
s_current_framebuffer = fb;
|
||||
}
|
||||
|
||||
uintptr_t coreGetProcAddress_cgo(const char *sym) {
|
||||
uintptr_t coreGetProcAddress(const char *sym);
|
||||
return coreGetProcAddress(sym);
|
||||
}
|
||||
|
||||
*/
|
||||
import "C"
|
||||
+236
-40
@@ -14,7 +14,9 @@ package libretro
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
void cothread_init();
|
||||
int cothread_init();
|
||||
void cothread_deinit();
|
||||
bool cothread_is_current();
|
||||
|
||||
void bridge_retro_init(void *f);
|
||||
void bridge_retro_deinit(void *f);
|
||||
@@ -36,6 +38,8 @@ void bridge_retro_unload_game(void *f);
|
||||
void bridge_retro_run(void *f);
|
||||
void bridge_retro_reset(void *f);
|
||||
void bridge_retro_frame_time_callback(retro_frame_time_callback_t f, retro_usec_t usec);
|
||||
void bridge_retro_hw_context_reset(retro_hw_context_reset_t f);
|
||||
void bridge_retro_hw_context_destroy(retro_hw_context_reset_t f);
|
||||
void bridge_retro_audio_callback(retro_audio_callback_t f);
|
||||
void bridge_retro_audio_set_state(retro_audio_set_state_callback_t f, bool state);
|
||||
size_t bridge_retro_get_memory_size(void *f, unsigned id);
|
||||
@@ -54,10 +58,15 @@ size_t coreAudioSampleBatch_cgo(const int16_t *data, size_t frames);
|
||||
int16_t coreInputState_cgo(unsigned port, unsigned device, unsigned index, unsigned id);
|
||||
void coreLog_cgo(enum retro_log_level level, const char *msg);
|
||||
int64_t coreGetTimeUsec_cgo();
|
||||
uintptr_t coreGetCurrentFramebuffer_cgo();
|
||||
void coreSetCurrentFramebuffer_cgo(uintptr_t fb);
|
||||
uintptr_t coreGetProcAddress_cgo(const char *sym);
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
"unsafe"
|
||||
)
|
||||
@@ -219,6 +228,20 @@ type FrameTimeCallback struct {
|
||||
Reference int64
|
||||
}
|
||||
|
||||
// HWRenderCallback sets an interface to let a libretro core render with
|
||||
// hardware acceleration.
|
||||
type HWRenderCallback struct {
|
||||
HWContextType uint32
|
||||
ContextReset func()
|
||||
Depth bool
|
||||
Stencil bool
|
||||
BottomLeftOrigin bool
|
||||
VersionMajor, VersionMinor uint
|
||||
CacheContext bool
|
||||
ContextDestroy func()
|
||||
DebugContext bool
|
||||
}
|
||||
|
||||
// AudioCallback stores the audio callback itself and the SetState callback
|
||||
type AudioCallback struct {
|
||||
Callback func()
|
||||
@@ -412,15 +435,35 @@ const (
|
||||
MemoryVideoRAM = uint32(C.RETRO_MEMORY_VIDEO_RAM)
|
||||
)
|
||||
|
||||
// Hardware contexts
|
||||
const (
|
||||
HWContextNone = uint32(C.RETRO_HW_CONTEXT_NONE)
|
||||
HWContextOpenGL = uint32(C.RETRO_HW_CONTEXT_OPENGL)
|
||||
HWContextOpenGLES2 = uint32(C.RETRO_HW_CONTEXT_OPENGLES2)
|
||||
HWContextOpenGLCore = uint32(C.RETRO_HW_CONTEXT_OPENGL_CORE)
|
||||
HWContextOpenGLES3 = uint32(C.RETRO_HW_CONTEXT_OPENGLES3)
|
||||
HWContextOpenGLESVersion = uint32(C.RETRO_HW_CONTEXT_OPENGLES_VERSION)
|
||||
HWContextVulkan = uint32(C.RETRO_HW_CONTEXT_VULKAN)
|
||||
HWContextDummy = uint32(C.RETRO_HW_CONTEXT_DUMMY)
|
||||
)
|
||||
|
||||
// Pass this to retro_video_refresh_t if rendering to hardware.
|
||||
// Passing NULL to retro_video_refresh_t is still a frame dupe as normal.
|
||||
var (
|
||||
HWFrameBufferValid = unsafe.Pointer(C.RETRO_HW_FRAME_BUFFER_VALID)
|
||||
)
|
||||
|
||||
type (
|
||||
environmentFunc func(uint32, unsafe.Pointer) bool
|
||||
videoRefreshFunc func(unsafe.Pointer, int32, int32, int32)
|
||||
audioSampleFunc func(int16, int16)
|
||||
audioSampleBatchFunc func([]byte, int32) int32
|
||||
inputPollFunc func()
|
||||
inputStateFunc func(uint, uint32, uint, uint) int16
|
||||
logFunc func(uint32, string)
|
||||
getTimeUsecFunc func() int64
|
||||
environmentFunc func(uint32, unsafe.Pointer) bool
|
||||
videoRefreshFunc func(unsafe.Pointer, int32, int32, int32)
|
||||
audioSampleFunc func(int16, int16)
|
||||
audioSampleBatchFunc func([]byte, int32) int32
|
||||
inputPollFunc func()
|
||||
inputStateFunc func(uint, uint32, uint, uint) int16
|
||||
logFunc func(uint32, string)
|
||||
getTimeUsecFunc func() int64
|
||||
getCurrentFramebufferFunc func() uintptr
|
||||
getProcAddressFunc func(string) uintptr
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -432,8 +475,84 @@ var (
|
||||
inputState inputStateFunc
|
||||
log logFunc
|
||||
getTimeUsec getTimeUsecFunc
|
||||
getProcAddress getProcAddressFunc
|
||||
releaseContext func()
|
||||
acquireContext func()
|
||||
coreCalls chan func()
|
||||
frontendCalls chan func()
|
||||
)
|
||||
|
||||
// SetContextCallbacks hands the current graphics context between the calling
|
||||
// thread and the native core thread. Calls remain synchronous.
|
||||
func SetContextCallbacks(release, acquire func()) {
|
||||
releaseContext, acquireContext = release, acquire
|
||||
}
|
||||
|
||||
func coreReleaseContext() {
|
||||
if releaseContext != nil {
|
||||
releaseContext()
|
||||
}
|
||||
}
|
||||
|
||||
func coreAcquireContext() {
|
||||
if acquireContext != nil {
|
||||
acquireContext()
|
||||
}
|
||||
}
|
||||
|
||||
//export coreThreadLoop
|
||||
func coreThreadLoop() {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
for call := range coreCalls {
|
||||
call()
|
||||
}
|
||||
}
|
||||
|
||||
// Calls are synchronous, but must service callbacks on the waiting thread.
|
||||
// Pumping both directions also permits a callback to make a nested core call.
|
||||
func threadCall(calls, callbacks chan func(), f func()) {
|
||||
coreReleaseContext()
|
||||
done := make(chan struct{})
|
||||
calls <- func() {
|
||||
coreAcquireContext()
|
||||
f()
|
||||
coreReleaseContext()
|
||||
close(done)
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case callback := <-callbacks:
|
||||
callback()
|
||||
case <-done:
|
||||
coreAcquireContext()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The frontend calls from its locked OS thread; core callbacks can reenter here.
|
||||
func coreCall(f func()) {
|
||||
if coreCalls == nil || bool(C.cothread_is_current()) {
|
||||
f()
|
||||
return
|
||||
}
|
||||
threadCall(coreCalls, frontendCalls, f)
|
||||
}
|
||||
|
||||
func coreResult[T any](f func() T) (result T) {
|
||||
coreCall(func() { result = f() })
|
||||
return
|
||||
}
|
||||
|
||||
func frontendCall(f func()) {
|
||||
if coreCalls == nil || !bool(C.cothread_is_current()) {
|
||||
f()
|
||||
return
|
||||
}
|
||||
threadCall(frontendCalls, coreCalls, f)
|
||||
}
|
||||
|
||||
// Load dynamically loads a libretro core at the given path and returns a Core instance
|
||||
func Load(sofile string) (*Core, error) {
|
||||
core := Core{}
|
||||
@@ -444,7 +563,12 @@ func Load(sofile string) (*Core, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
C.cothread_init()
|
||||
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")
|
||||
@@ -473,18 +597,21 @@ func Load(sofile string) (*Core, error) {
|
||||
|
||||
// Init takes care of the library global initialization
|
||||
func (core *Core) Init() {
|
||||
C.bridge_retro_init(core.symRetroInit)
|
||||
coreCall(func() { C.bridge_retro_init(core.symRetroInit) })
|
||||
}
|
||||
|
||||
// APIVersion returns the RETRO_API_VERSION.
|
||||
// Used to validate ABI compatibility when the API is revised.
|
||||
func (core *Core) APIVersion() uint {
|
||||
return uint(C.bridge_retro_api_version(core.symRetroAPIVersion))
|
||||
return coreResult(func() uint { return uint(C.bridge_retro_api_version(core.symRetroAPIVersion)) })
|
||||
}
|
||||
|
||||
// Deinit takes care of the library global deinitialization
|
||||
func (core *Core) Deinit() {
|
||||
C.bridge_retro_deinit(core.symRetroDeinit)
|
||||
coreCall(func() { C.bridge_retro_deinit(core.symRetroDeinit) })
|
||||
close(coreCalls)
|
||||
C.cothread_deinit()
|
||||
coreCalls, frontendCalls = nil, nil
|
||||
DlClose(core.handle)
|
||||
core.MemoryMap = nil
|
||||
environment = nil
|
||||
@@ -495,6 +622,7 @@ func (core *Core) Deinit() {
|
||||
inputState = nil
|
||||
log = nil
|
||||
getTimeUsec = nil
|
||||
getProcAddress = nil
|
||||
}
|
||||
|
||||
// Run runs the game for one video frame.
|
||||
@@ -504,12 +632,12 @@ func (core *Core) Deinit() {
|
||||
// a frame if GET_CAN_DUPE returns true.
|
||||
// In this case, the video callback can take a NULL argument for data.
|
||||
func (core *Core) Run() {
|
||||
C.bridge_retro_run(core.symRetroRun)
|
||||
coreCall(func() { C.bridge_retro_run(core.symRetroRun) })
|
||||
}
|
||||
|
||||
// Reset resets the current game.
|
||||
func (core *Core) Reset() {
|
||||
C.bridge_retro_reset(core.symRetroReset)
|
||||
coreCall(func() { C.bridge_retro_reset(core.symRetroReset) })
|
||||
}
|
||||
|
||||
// GetSystemInfo returns statically known system info. Pointers provided in *info
|
||||
@@ -517,7 +645,7 @@ func (core *Core) Reset() {
|
||||
// Can be called at any time, even before retro_init().
|
||||
func (core *Core) GetSystemInfo() SystemInfo {
|
||||
rsi := C.struct_retro_system_info{}
|
||||
C.bridge_retro_get_system_info(core.symRetroGetSystemInfo, &rsi)
|
||||
coreCall(func() { C.bridge_retro_get_system_info(core.symRetroGetSystemInfo, &rsi) })
|
||||
return SystemInfo{
|
||||
LibraryName: C.GoString(rsi.library_name),
|
||||
LibraryVersion: C.GoString(rsi.library_version),
|
||||
@@ -535,7 +663,7 @@ func (core *Core) GetSystemInfo() SystemInfo {
|
||||
// desire a particular aspect ratio.
|
||||
func (core *Core) GetSystemAVInfo() SystemAVInfo {
|
||||
avi := C.struct_retro_system_av_info{}
|
||||
C.bridge_retro_get_system_av_info(core.symRetroGetSystemAVInfo, &avi)
|
||||
coreCall(func() { C.bridge_retro_get_system_av_info(core.symRetroGetSystemAVInfo, &avi) })
|
||||
return SystemAVInfo{
|
||||
Geometry: GameGeometry{
|
||||
AspectRatio: float64(avi.geometry.aspect_ratio),
|
||||
@@ -557,7 +685,7 @@ func (core *Core) LoadGame(gi GameInfo) bool {
|
||||
rgi.path = C.CString(gi.Path)
|
||||
rgi.size = C.size_t(gi.Size)
|
||||
rgi.data = gi.Data
|
||||
return bool(C.bridge_retro_load_game(core.symRetroLoadGame, &rgi))
|
||||
return coreResult(func() bool { return bool(C.bridge_retro_load_game(core.symRetroLoadGame, &rgi)) })
|
||||
}
|
||||
|
||||
// SerializeSize returns the amount of data the implementation requires to serialize
|
||||
@@ -566,18 +694,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 uint(C.bridge_retro_serialize_size(core.symRetroSerializeSize))
|
||||
return coreResult(func() uint { return uint(C.bridge_retro_serialize_size(core.symRetroSerializeSize)) })
|
||||
}
|
||||
|
||||
// Serialize serializes internal state and returns the state as a byte slice.
|
||||
func (core *Core) Serialize(size uint) ([]byte, error) {
|
||||
data := C.malloc(C.size_t(size))
|
||||
ok := bool(C.bridge_retro_serialize(core.symRetroSerialize, data, C.size_t(size)))
|
||||
defer C.free(data)
|
||||
ok := coreResult(func() bool { return bool(C.bridge_retro_serialize(core.symRetroSerialize, data, C.size_t(size))) })
|
||||
if !ok {
|
||||
return nil, errors.New("retro_serialize failed")
|
||||
}
|
||||
bytes := C.GoBytes(data, C.int(size))
|
||||
C.free(data)
|
||||
return bytes, nil
|
||||
}
|
||||
|
||||
@@ -589,7 +717,9 @@ func (core *Core) Unserialize(bytes []byte, size uint) error {
|
||||
if uint(len(bytes)) > size {
|
||||
size = uint(len(bytes))
|
||||
}
|
||||
ok := bool(C.bridge_retro_unserialize(core.symRetroUnserialize, unsafe.Pointer(&bytes[0]), C.size_t(size)))
|
||||
ok := coreResult(func() bool {
|
||||
return bool(C.bridge_retro_unserialize(core.symRetroUnserialize, unsafe.Pointer(&bytes[0]), C.size_t(size)))
|
||||
})
|
||||
if !ok {
|
||||
return errors.New("retro_unserialize failed")
|
||||
}
|
||||
@@ -598,49 +728,51 @@ func (core *Core) Unserialize(bytes []byte, size uint) error {
|
||||
|
||||
// UnloadGame unloads a currently loaded game
|
||||
func (core *Core) UnloadGame() {
|
||||
C.bridge_retro_unload_game(core.symRetroUnloadGame)
|
||||
coreCall(func() { C.bridge_retro_unload_game(core.symRetroUnloadGame) })
|
||||
}
|
||||
|
||||
// SetEnvironment sets the environment callback.
|
||||
// Must be called before Init
|
||||
func (core *Core) SetEnvironment(f environmentFunc) {
|
||||
environment = f
|
||||
C.bridge_retro_set_environment(core.symRetroSetEnvironment, C.coreEnvironment_cgo)
|
||||
coreCall(func() { C.bridge_retro_set_environment(core.symRetroSetEnvironment, C.coreEnvironment_cgo) })
|
||||
}
|
||||
|
||||
// SetVideoRefresh sets the video refresh callback.
|
||||
// Must be set before the first Run call
|
||||
func (core *Core) SetVideoRefresh(f videoRefreshFunc) {
|
||||
videoRefresh = f
|
||||
C.bridge_retro_set_video_refresh(core.symRetroSetVideoRefresh, C.coreVideoRefresh_cgo)
|
||||
coreCall(func() { C.bridge_retro_set_video_refresh(core.symRetroSetVideoRefresh, C.coreVideoRefresh_cgo) })
|
||||
}
|
||||
|
||||
// SetAudioSample sets the audio sample callback.
|
||||
// Must be set before the first Run call
|
||||
func (core *Core) SetAudioSample(f audioSampleFunc) {
|
||||
audioSample = f
|
||||
C.bridge_retro_set_audio_sample(core.symRetroSetAudioSample, C.coreAudioSample_cgo)
|
||||
coreCall(func() { C.bridge_retro_set_audio_sample(core.symRetroSetAudioSample, C.coreAudioSample_cgo) })
|
||||
}
|
||||
|
||||
// SetAudioSampleBatch sets the audio sample batch callback.
|
||||
// Must be set before the first Run call
|
||||
func (core *Core) SetAudioSampleBatch(f audioSampleBatchFunc) {
|
||||
audioSampleBatch = f
|
||||
C.bridge_retro_set_audio_sample_batch(core.symRetroSetAudioSampleBatch, C.coreAudioSampleBatch_cgo)
|
||||
coreCall(func() {
|
||||
C.bridge_retro_set_audio_sample_batch(core.symRetroSetAudioSampleBatch, C.coreAudioSampleBatch_cgo)
|
||||
})
|
||||
}
|
||||
|
||||
// SetInputPoll sets the input poll callback.
|
||||
// Must be set before the first Run call
|
||||
func (core *Core) SetInputPoll(f inputPollFunc) {
|
||||
inputPoll = f
|
||||
C.bridge_retro_set_input_poll(core.symRetroSetInputPoll, C.coreInputPoll_cgo)
|
||||
coreCall(func() { C.bridge_retro_set_input_poll(core.symRetroSetInputPoll, C.coreInputPoll_cgo) })
|
||||
}
|
||||
|
||||
// SetInputState sets the input state callback.
|
||||
// Must be set before the first Run call
|
||||
func (core *Core) SetInputState(f inputStateFunc) {
|
||||
inputState = f
|
||||
C.bridge_retro_set_input_state(core.symRetroSetInputState, C.coreInputState_cgo)
|
||||
coreCall(func() { C.bridge_retro_set_input_state(core.symRetroSetInputState, C.coreInputState_cgo) })
|
||||
}
|
||||
|
||||
// BindLogCallback binds f to the log callback
|
||||
@@ -659,7 +791,9 @@ func (core *Core) BindPerfCallback(data unsafe.Pointer, f getTimeUsecFunc) {
|
||||
|
||||
// SetControllerPortDevice sets the device type attached to a controller port
|
||||
func (core *Core) SetControllerPortDevice(port uint, device uint32) {
|
||||
C.bridge_retro_set_controller_port_device(core.symRetroSetControllerPortDevice, C.unsigned(port), C.unsigned(device))
|
||||
coreCall(func() {
|
||||
C.bridge_retro_set_controller_port_device(core.symRetroSetControllerPortDevice, C.unsigned(port), C.unsigned(device))
|
||||
})
|
||||
}
|
||||
|
||||
//export coreEnvironment
|
||||
@@ -667,6 +801,12 @@ func coreEnvironment(cmd C.unsigned, data unsafe.Pointer) bool {
|
||||
if environment == nil {
|
||||
return false
|
||||
}
|
||||
// Only window operations need the frontend thread. Routine queries stay here.
|
||||
if cmd == C.RETRO_ENVIRONMENT_SET_HW_RENDER || cmd == C.RETRO_ENVIRONMENT_SHUTDOWN {
|
||||
var result bool
|
||||
frontendCall(func() { result = environment(uint32(cmd), data) })
|
||||
return result
|
||||
}
|
||||
return environment(uint32(cmd), data)
|
||||
}
|
||||
|
||||
@@ -691,6 +831,12 @@ func coreInputState(port C.unsigned, device C.unsigned, index C.unsigned, id C.u
|
||||
if inputState == nil {
|
||||
return 0
|
||||
}
|
||||
// Mouse state is queried through GLFW rather than the polled input snapshot.
|
||||
if device == C.RETRO_DEVICE_MOUSE {
|
||||
var result C.int16_t
|
||||
frontendCall(func() { result = C.int16_t(inputState(uint(port), uint32(device), uint(index), uint(id))) })
|
||||
return result
|
||||
}
|
||||
return C.int16_t(inputState(uint(port), uint32(device), uint(index), uint(id)))
|
||||
}
|
||||
|
||||
@@ -726,6 +872,16 @@ func coreGetTimeUsec() C.uint64_t {
|
||||
return C.uint64_t(getTimeUsec())
|
||||
}
|
||||
|
||||
//export coreGetProcAddress
|
||||
func coreGetProcAddress(sym *C.char) C.uintptr_t {
|
||||
return C.uintptr_t(getProcAddress(C.GoString(sym)))
|
||||
}
|
||||
|
||||
// SetCurrentFramebufferValue updates the C-side framebuffer callback target
|
||||
func SetCurrentFramebufferValue(fb uintptr) {
|
||||
C.coreSetCurrentFramebuffer_cgo(C.uintptr_t(fb))
|
||||
}
|
||||
|
||||
// SetData is a setter for the data of a GameInfo type
|
||||
func (gi *GameInfo) SetData(bytes []byte) {
|
||||
cstr := C.CString(string(bytes))
|
||||
@@ -883,20 +1039,58 @@ func (core *Core) SetFrameTimeCallback(data unsafe.Pointer) {
|
||||
ftc := &FrameTimeCallback{}
|
||||
ftc.Reference = int64(c.reference)
|
||||
ftc.Callback = func(usec int64) {
|
||||
C.bridge_retro_frame_time_callback(c.callback, C.retro_usec_t(usec))
|
||||
coreCall(func() { C.bridge_retro_frame_time_callback(c.callback, C.retro_usec_t(usec)) })
|
||||
}
|
||||
core.FrameTimeCallback = ftc
|
||||
}
|
||||
|
||||
// SetHWRenderCallback is an environment callback helper to set the HWRenderCallback
|
||||
func SetHWRenderCallback(
|
||||
data unsafe.Pointer,
|
||||
currentFramebuffer getCurrentFramebufferFunc,
|
||||
procAddress getProcAddressFunc,
|
||||
) *HWRenderCallback {
|
||||
c := (*C.struct_retro_hw_render_callback)(data)
|
||||
hwrc := HWRenderCallback{}
|
||||
hwrc.HWContextType = uint32(c.context_type)
|
||||
|
||||
getProcAddress = procAddress
|
||||
SetCurrentFramebufferValue(currentFramebuffer())
|
||||
c.get_current_framebuffer = (C.retro_hw_get_current_framebuffer_t)(C.coreGetCurrentFramebuffer_cgo)
|
||||
c.get_proc_address = (C.retro_hw_get_proc_address_t)(C.coreGetProcAddress_cgo)
|
||||
|
||||
// The request may be stack-allocated by the core. Keep callback values, not c.
|
||||
reset, destroy := c.context_reset, c.context_destroy
|
||||
if reset != nil {
|
||||
hwrc.ContextReset = func() {
|
||||
coreCall(func() { C.bridge_retro_hw_context_reset(reset) })
|
||||
}
|
||||
}
|
||||
|
||||
hwrc.Depth = bool(c.depth)
|
||||
hwrc.Stencil = bool(c.stencil)
|
||||
hwrc.BottomLeftOrigin = bool(c.bottom_left_origin)
|
||||
hwrc.VersionMajor = uint(c.version_major)
|
||||
hwrc.VersionMinor = uint(c.version_minor)
|
||||
hwrc.CacheContext = bool(c.cache_context)
|
||||
if destroy != nil {
|
||||
hwrc.ContextDestroy = func() {
|
||||
coreCall(func() { C.bridge_retro_hw_context_destroy(destroy) })
|
||||
}
|
||||
}
|
||||
hwrc.DebugContext = bool(c.debug_context)
|
||||
return &hwrc
|
||||
}
|
||||
|
||||
// SetAudioCallback is an environment callback helper to set the AudioCallback
|
||||
func (core *Core) SetAudioCallback(data unsafe.Pointer) {
|
||||
c := *(*C.struct_retro_audio_callback)(data)
|
||||
auc := &AudioCallback{}
|
||||
auc.Callback = func() {
|
||||
C.bridge_retro_audio_callback(c.callback)
|
||||
coreCall(func() { C.bridge_retro_audio_callback(c.callback) })
|
||||
}
|
||||
auc.SetState = func(state bool) {
|
||||
C.bridge_retro_audio_set_state(c.set_state, C.bool(state))
|
||||
coreCall(func() { C.bridge_retro_audio_set_state(c.set_state, C.bool(state)) })
|
||||
}
|
||||
core.AudioCallback = auc
|
||||
}
|
||||
@@ -904,13 +1098,15 @@ func (core *Core) SetAudioCallback(data unsafe.Pointer) {
|
||||
// GetMemorySize returns the size of a region of the memory.
|
||||
// See memory constants.
|
||||
func (core *Core) GetMemorySize(id uint32) uint {
|
||||
return uint(C.bridge_retro_get_memory_size(core.symRetroGetMemorySize, C.unsigned(id)))
|
||||
return coreResult(func() uint { return uint(C.bridge_retro_get_memory_size(core.symRetroGetMemorySize, C.unsigned(id))) })
|
||||
}
|
||||
|
||||
// GetMemoryData returns the size of a region of the memory.
|
||||
// See memory constants.
|
||||
func (core *Core) GetMemoryData(id uint32) unsafe.Pointer {
|
||||
return C.bridge_retro_get_memory_data(core.symRetroGetMemoryData, C.unsigned(id))
|
||||
return coreResult(func() unsafe.Pointer {
|
||||
return C.bridge_retro_get_memory_data(core.symRetroGetMemoryData, C.unsigned(id))
|
||||
})
|
||||
}
|
||||
|
||||
// DiskControlCallback is an interface which frontend can use to eject and insert disk images
|
||||
@@ -930,19 +1126,19 @@ func (core *Core) SetDiskControlCallback(data unsafe.Pointer) {
|
||||
c := *(*C.struct_retro_disk_control_callback)(data)
|
||||
dcc := &DiskControlCallback{}
|
||||
dcc.SetEjectState = func(state bool) {
|
||||
C.bridge_retro_set_eject_state(c.set_eject_state, C.bool(state))
|
||||
coreCall(func() { C.bridge_retro_set_eject_state(c.set_eject_state, C.bool(state)) })
|
||||
}
|
||||
dcc.GetEjectState = func() bool {
|
||||
return bool(C.bridge_retro_get_eject_state(c.get_eject_state))
|
||||
return coreResult(func() bool { return bool(C.bridge_retro_get_eject_state(c.get_eject_state)) })
|
||||
}
|
||||
dcc.GetImageIndex = func() uint {
|
||||
return uint(C.bridge_retro_get_image_index(c.get_image_index))
|
||||
return coreResult(func() uint { return uint(C.bridge_retro_get_image_index(c.get_image_index)) })
|
||||
}
|
||||
dcc.SetImageIndex = func(index uint) {
|
||||
C.bridge_retro_set_image_index(c.set_image_index, C.uint(index))
|
||||
coreCall(func() { C.bridge_retro_set_image_index(c.set_image_index, C.uint(index)) })
|
||||
}
|
||||
dcc.GetNumImages = func() uint {
|
||||
return uint(C.bridge_retro_get_num_images(c.get_num_images))
|
||||
return coreResult(func() uint { return uint(C.bridge_retro_get_num_images(c.get_num_images)) })
|
||||
}
|
||||
core.DiskControlCallback = dcc
|
||||
}
|
||||
@@ -31,6 +31,7 @@ type Core struct {
|
||||
AudioCallback *AudioCallback
|
||||
FrameTimeCallback *FrameTimeCallback
|
||||
DiskControlCallback *DiskControlCallback
|
||||
HWRenderCallback *HWRenderCallback
|
||||
|
||||
MemoryMap []MemoryDescriptor
|
||||
}
|
||||
@@ -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)
|
||||
cerr := C.dlerror()
|
||||
if h == nil || cerr != nil {
|
||||
if h == nil {
|
||||
cerr := C.dlerror()
|
||||
err := C.GoString(cerr)
|
||||
return nil, errors.New(err)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
package libretro
|
||||
|
||||
/*
|
||||
#include <stdint.h>
|
||||
|
||||
static void *symbol_from_addr(uintptr_t addr) {
|
||||
return (void *)addr;
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
@@ -11,7 +19,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 unsafe.Pointer(proc.Addr())
|
||||
return C.symbol_from_addr(C.uintptr_t(proc.Addr()))
|
||||
}
|
||||
|
||||
// DlOpen opens a dynamic library
|
||||
|
||||
@@ -30,6 +30,22 @@ 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()
|
||||
@@ -39,11 +55,19 @@ func runLoop(vid *video.Video, m *menu.Menu) {
|
||||
glfw.PollEvents()
|
||||
m.ProcessHotkeys()
|
||||
ntf.Process(dt)
|
||||
vid.ResizeViewport()
|
||||
m.UpdatePalette()
|
||||
input.Poll()
|
||||
if !state.MenuActive {
|
||||
menuActive := state.MenuActive
|
||||
if menuActive {
|
||||
m.Update(dt)
|
||||
}
|
||||
if state.MenuContextResetNeeded {
|
||||
m.ContextReset()
|
||||
state.MenuContextResetNeeded = false
|
||||
}
|
||||
if !menuActive {
|
||||
if state.CoreRunning {
|
||||
vid.BeginCore()
|
||||
state.Core.Run()
|
||||
if state.Core.FrameTimeCallback != nil {
|
||||
state.Core.FrameTimeCallback.Callback(state.Core.FrameTimeCallback.Reference)
|
||||
@@ -51,18 +75,23 @@ func runLoop(vid *video.Video, m *menu.Menu) {
|
||||
if state.Core.AudioCallback != nil {
|
||||
state.Core.AudioCallback.Callback()
|
||||
}
|
||||
vid.EndCore()
|
||||
}
|
||||
vid.BeginFrontendFrame()
|
||||
vid.Render()
|
||||
vid.CachePresentedFrame()
|
||||
m.RenderNotifications()
|
||||
frame++
|
||||
if frame%600 == 0 { // save sram about every 10 sec
|
||||
savefiles.SaveSRAM()
|
||||
}
|
||||
} else {
|
||||
m.Update(dt)
|
||||
vid.Render()
|
||||
vid.BeginFrontendFrame()
|
||||
vid.RenderPresentedFrame()
|
||||
fulfillPendingScreenshot(vid)
|
||||
m.Render(dt)
|
||||
m.RenderNotifications()
|
||||
}
|
||||
m.RenderNotifications()
|
||||
if state.FastForward {
|
||||
glfw.SwapInterval(0)
|
||||
} else {
|
||||
@@ -128,18 +157,14 @@ func main() {
|
||||
input.Init(vid)
|
||||
|
||||
if len(state.CorePath) > 0 {
|
||||
err := core.Load(state.CorePath)
|
||||
if err == nil {
|
||||
if len(gamePath) > 0 {
|
||||
err := core.LoadGame(gamePath)
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
} else {
|
||||
m.WarpToQuickMenu()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := core.Load(state.CorePath); err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
} else if len(gamePath) > 0 {
|
||||
if err := core.LoadGame(gamePath); err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
} else {
|
||||
m.WarpToQuickMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -9,16 +9,16 @@ func stackHintLeft(stack *float32, icon uint32, label string, h int) {
|
||||
menu.Font.SetColor(hintTextColor)
|
||||
menu.DrawImage(icon, *stack, float32(h)-79*menu.ratio, 70*menu.ratio, 70*menu.ratio, 1.0, 0, hintTextColor)
|
||||
*stack += 70 * menu.ratio
|
||||
menu.Font.Print(*stack, float32(h)-30*menu.ratio, 0.5*menu.ratio, label)
|
||||
*stack += menu.Font.Width(0.5*menu.ratio, label)
|
||||
menu.Font.Print(*stack, float32(h)-30*menu.ratio, menu.ratio, label)
|
||||
*stack += menu.Font.Width(menu.ratio, label)
|
||||
*stack += 32 * menu.ratio
|
||||
}
|
||||
|
||||
// Used to easily compose different hint bars based on the context.
|
||||
func stackHintRight(stack *float32, icon uint32, label string, h int) {
|
||||
*stack -= menu.Font.Width(0.5*menu.ratio, label)
|
||||
*stack -= menu.Font.Width(menu.ratio, label)
|
||||
menu.Font.SetColor(hintTextColor)
|
||||
menu.Font.Print(*stack, float32(h)-30*menu.ratio, 0.5*menu.ratio, label)
|
||||
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
|
||||
|
||||
+2
-3
@@ -143,7 +143,7 @@ func genericInput(list *entry, dt float32) {
|
||||
|
||||
// indexed allows jumping directly to the next letter in playlists
|
||||
func indexed(list *entry, offset int) int {
|
||||
curr := list.children[list.ptr].label[0]
|
||||
curr := firstRune(list.children[list.ptr].label)
|
||||
for i, t := range list.indexes {
|
||||
if curr == t.Char {
|
||||
if i+offset < 0 {
|
||||
@@ -196,8 +196,7 @@ func (m *Menu) ProcessHotkeys() {
|
||||
// Toggle fullscreen if ActionFullscreenToggle is released
|
||||
if input.Released[0][input.ActionFullscreenToggle] == 1 {
|
||||
settings.Current.VideoFullscreen = !settings.Current.VideoFullscreen
|
||||
m.Reconfigure(settings.Current.VideoFullscreen)
|
||||
m.ContextReset()
|
||||
m.SetFullscreen(settings.Current.VideoFullscreen)
|
||||
err := settings.Save()
|
||||
if err != nil {
|
||||
ntf.DisplayAndLogf(ntf.Error, "Menu", "Error saving settings: %v", err)
|
||||
|
||||
+6
-4
@@ -102,10 +102,12 @@ func (m *Menu) ContextReset() {
|
||||
m.icons[filename] = video.NewImage(path)
|
||||
}
|
||||
|
||||
currentScreenIndex := len(m.stack) - 1
|
||||
curList := m.stack[currentScreenIndex].Entry()
|
||||
for i := range curList.children {
|
||||
curList.children[i].thumbnail = 0
|
||||
// Every retained scene's texture names belong to the old context.
|
||||
for _, scene := range m.stack {
|
||||
list := scene.Entry()
|
||||
for i := range list.children {
|
||||
list.children[i].thumbnail = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ var severityBgColor map[ntf.Severity]video.Color
|
||||
// RenderNotifications draws the list of notification messages on the viewport
|
||||
func (m *Menu) RenderNotifications() {
|
||||
fbw, fbh := m.GetFramebufferSize()
|
||||
m.ratio = float32(fbw) / 1920
|
||||
m.Font.UpdateResolution(fbw, fbh)
|
||||
var h float32 = 75
|
||||
stack := h
|
||||
@@ -20,7 +21,7 @@ func (m *Menu) RenderNotifications() {
|
||||
fading = 1
|
||||
}
|
||||
offset := fading*h - h
|
||||
lw := m.Font.Width(0.5*m.ratio, n.Message)
|
||||
lw := m.Font.Width(m.ratio, n.Message)
|
||||
fg := severityFgColor[n.Severity]
|
||||
bg := severityBgColor[n.Severity]
|
||||
m.DrawRect(
|
||||
@@ -35,7 +36,7 @@ func (m *Menu) RenderNotifications() {
|
||||
m.Font.Print(
|
||||
45*m.ratio,
|
||||
(stack+offset)*m.ratio,
|
||||
0.5*m.ratio,
|
||||
m.ratio,
|
||||
n.Message,
|
||||
)
|
||||
stack += h + offset
|
||||
|
||||
+12
-5
@@ -35,7 +35,7 @@ type entry struct {
|
||||
children []entry // children entries
|
||||
ptr int // index of the active child
|
||||
indexes []struct {
|
||||
Char byte
|
||||
Char rune
|
||||
Index int
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,13 @@ type Scene interface {
|
||||
Entry() *entry
|
||||
}
|
||||
|
||||
func firstRune(s string) rune {
|
||||
for _, r := range s {
|
||||
return r
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// genericSegueMount is the smooth transition of the menu entries first appearance
|
||||
func genericSegueMount(list *entry) {
|
||||
for i := range list.children {
|
||||
@@ -198,21 +205,21 @@ func genericRender(list *entry) {
|
||||
menu.Font.Print(
|
||||
(670+1)*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset+1*menu.ratio,
|
||||
0.5*menu.ratio, e.label)
|
||||
menu.ratio, e.label)
|
||||
menu.Font.SetColor(textColor.Alpha(e.labelAlpha))
|
||||
menu.Font.Print(
|
||||
670*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset,
|
||||
0.5*menu.ratio, e.label)
|
||||
menu.ratio, e.label)
|
||||
|
||||
if e.widget != nil {
|
||||
e.widget(&e)
|
||||
} else if e.stringValue != nil {
|
||||
lw := menu.Font.Width(0.5*menu.ratio, e.stringValue())
|
||||
lw := menu.Font.Width(menu.ratio, e.stringValue())
|
||||
menu.Font.Print(
|
||||
float32(w)-lw-128*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset,
|
||||
0.5*menu.ratio, e.stringValue())
|
||||
menu.ratio, e.stringValue())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ func buildCoreOptions() Scene {
|
||||
var list sceneCoreOptions
|
||||
list.label = "Core Options"
|
||||
|
||||
if core.Options == nil {
|
||||
if core.Options == nil || len(core.Options.Vars) == 0 {
|
||||
list.children = append(list.children, entry{
|
||||
label: "No options",
|
||||
icon: "subsetting",
|
||||
@@ -28,11 +28,11 @@ func buildCoreOptions() Scene {
|
||||
for _, v := range core.Options.Vars {
|
||||
v := v
|
||||
list.children = append(list.children, entry{
|
||||
label: strings.Replace(v.Desc, "%", "%%", -1),
|
||||
label: strings.ReplaceAll(v.Desc, "%", "%%"),
|
||||
icon: "subsetting",
|
||||
stringValue: func() string {
|
||||
val := v.Choices[v.Choice]
|
||||
return strings.Replace(val, "%", "%%", -1)
|
||||
return "◀ " + strings.ReplaceAll(val, "%", "%%") + " ▶"
|
||||
},
|
||||
incr: func(direction int) {
|
||||
v.Choice += direction
|
||||
|
||||
+12
-12
@@ -70,16 +70,16 @@ func (s *sceneDialog) render() {
|
||||
white,
|
||||
)
|
||||
|
||||
menu.Font.SetColor(titleTextColor)
|
||||
lw1 := menu.Font.Width(0.7*menu.ratio, s.title)
|
||||
menu.Font.Print(fw/2-lw1/2, fh/2-120*menu.ratio+20*menu.ratio, 0.7*menu.ratio, s.title)
|
||||
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(0.5*menu.ratio, s.line1)
|
||||
menu.Font.Print(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.Print(fw/2-lw3/2, fh/2+30*menu.ratio+20*menu.ratio, 0.5*menu.ratio, s.line2)
|
||||
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(dialogTextColor)
|
||||
menu.FontSm.SetColor(dialogTextColor)
|
||||
|
||||
var margin float32 = 15
|
||||
|
||||
@@ -90,10 +90,10 @@ func (s *sceneDialog) render() {
|
||||
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.Font.Print(
|
||||
menu.FontSm.Print(
|
||||
fw/2-width/2*menu.ratio+margin*menu.ratio+70*menu.ratio,
|
||||
fh/2+height/2*menu.ratio-23*menu.ratio-margin*menu.ratio,
|
||||
0.4*menu.ratio,
|
||||
menu.ratio,
|
||||
"NO")
|
||||
|
||||
menu.DrawImage(
|
||||
@@ -101,10 +101,10 @@ func (s *sceneDialog) render() {
|
||||
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.Font.Print(
|
||||
menu.FontSm.Print(
|
||||
fw/2+width/2*menu.ratio-150*menu.ratio-margin*menu.ratio+70*menu.ratio,
|
||||
fh/2+height/2*menu.ratio-23*menu.ratio-margin*menu.ratio,
|
||||
0.4*menu.ratio,
|
||||
menu.ratio,
|
||||
"YES")
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ func explorerIcon(f os.FileInfo) string {
|
||||
|
||||
func appendNode(list *sceneExplorer, fullPath string, name string, f os.FileInfo, exts []string, cb func(string), dirAction *entry, prettifier Prettifier) {
|
||||
// Check whether or not we are to display hidden files.
|
||||
if name[:1] == "." && !settings.Current.ShowHiddenFiles {
|
||||
if strings.HasPrefix(name, ".") && !settings.Current.ShowHiddenFiles {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -203,13 +203,13 @@ func (s *sceneHistory) render() {
|
||||
menu.Font.Print(
|
||||
(840+1)*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset-slOffset+1*menu.ratio,
|
||||
0.5*menu.ratio, e.label)
|
||||
menu.ratio, e.label)
|
||||
menu.Font.SetColor(textColor.Alpha(e.labelAlpha))
|
||||
menu.Font.Print(
|
||||
840*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset-slOffset,
|
||||
0.5*menu.ratio, e.label)
|
||||
stack += float32(int(menu.Font.Width(0.5*menu.ratio, e.label)))
|
||||
menu.ratio, e.label)
|
||||
stack += float32(int(menu.Font.Width(menu.ratio, e.label)))
|
||||
stack += 10
|
||||
|
||||
for _, tag := range e.tags {
|
||||
@@ -225,11 +225,11 @@ func (s *sceneHistory) render() {
|
||||
}
|
||||
}
|
||||
|
||||
menu.Font.SetColor(mutedTextColor.Alpha(e.subLabelAlpha))
|
||||
menu.Font.Print(
|
||||
menu.FontSm.SetColor(mutedTextColor.Alpha(e.subLabelAlpha))
|
||||
menu.FontSm.Print(
|
||||
840*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset+60*menu.ratio-slOffset,
|
||||
0.5*menu.ratio, e.subLabel)
|
||||
menu.ratio, e.subLabel)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -180,7 +180,7 @@ func gameExplorerCb(path string) {
|
||||
|
||||
// Shutdown the operating system
|
||||
func cleanShutdown() {
|
||||
core.UnloadGame()
|
||||
core.Unload()
|
||||
if err := exec.Command("/usr/sbin/shutdown", "-P", "now").Run(); err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
}
|
||||
@@ -188,7 +188,7 @@ func cleanShutdown() {
|
||||
|
||||
// Reboots the operating system
|
||||
func cleanReboot() {
|
||||
core.UnloadGame()
|
||||
core.Unload()
|
||||
if err := exec.Command("/usr/sbin/shutdown", "-r", "now").Run(); err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
}
|
||||
|
||||
@@ -57,12 +57,12 @@ func buildPlaylist(path string) Scene {
|
||||
// Index first letters of entries to allow quick jump to the next or previous
|
||||
// letter
|
||||
func buildIndexes(list *entry) {
|
||||
var last byte
|
||||
var last rune
|
||||
for i := 0; i < len(list.children); i++ {
|
||||
char := list.children[i].label[0]
|
||||
char := firstRune(list.children[i].label)
|
||||
if char != last {
|
||||
list.indexes = append(list.indexes, struct {
|
||||
Char byte
|
||||
Char rune
|
||||
Index int
|
||||
}{char, i})
|
||||
last = char
|
||||
@@ -241,14 +241,14 @@ func (s *scenePlaylist) render() {
|
||||
menu.Font.Print(
|
||||
(840+1)*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset+1*menu.ratio,
|
||||
0.5*menu.ratio, e.label)
|
||||
menu.ratio, e.label)
|
||||
menu.Font.SetColor(textColor.Alpha(e.labelAlpha))
|
||||
menu.Font.Print(
|
||||
840*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset,
|
||||
0.5*menu.ratio, e.label)
|
||||
menu.ratio, e.label)
|
||||
|
||||
stack += float32(int(menu.Font.Width(0.5*menu.ratio, e.label)))
|
||||
stack += float32(int(menu.Font.Width(menu.ratio, e.label)))
|
||||
stack += 10
|
||||
|
||||
for _, tag := range e.tags {
|
||||
|
||||
+9
-6
@@ -1,6 +1,7 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"github.com/libretro/ludo/core"
|
||||
ntf "github.com/libretro/ludo/notifications"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/utils"
|
||||
@@ -27,7 +28,7 @@ func buildQuickMenu() Scene {
|
||||
label: "Reset",
|
||||
icon: "reset",
|
||||
callbackOK: func() {
|
||||
state.Core.Reset()
|
||||
core.Reset()
|
||||
state.MenuActive = false
|
||||
state.FastForward = false
|
||||
},
|
||||
@@ -47,11 +48,13 @@ func buildQuickMenu() Scene {
|
||||
icon: "screenshot",
|
||||
callbackOK: func() {
|
||||
name := utils.DatedName(state.GamePath)
|
||||
err := menu.TakeScreenshot(name)
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
} else {
|
||||
ntf.DisplayAndLog(ntf.Success, "Menu", "Took a screenshot.")
|
||||
state.PendingScreenshotName = name
|
||||
state.PendingScreenshotDone = func(err error) {
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
} else {
|
||||
ntf.DisplayAndLog(ntf.Success, "Menu", "Took a screenshot.")
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
+15
-13
@@ -26,17 +26,19 @@ func buildSavestates() Scene {
|
||||
icon: "savestate",
|
||||
callbackOK: func() {
|
||||
name := utils.DatedName(state.GamePath)
|
||||
err := menu.TakeScreenshot(name)
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
}
|
||||
err = savestates.Save(name)
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
} else {
|
||||
menu.stack[len(menu.stack)-1] = buildSavestates()
|
||||
menu.tweens.FastForward()
|
||||
ntf.DisplayAndLog(ntf.Success, "Menu", "State saved.")
|
||||
state.PendingScreenshotName = name
|
||||
state.PendingScreenshotDone = func(err error) {
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
}
|
||||
err = savestates.Save(name)
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
} else {
|
||||
menu.stack[len(menu.stack)-1] = buildSavestates()
|
||||
menu.tweens.FastForward()
|
||||
ntf.DisplayAndLog(ntf.Success, "Menu", "State saved.")
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -157,13 +159,13 @@ func (s *sceneSavestates) render() {
|
||||
menu.Font.Print(
|
||||
(840+1)*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset+1*menu.ratio,
|
||||
0.5*menu.ratio, e.label)
|
||||
menu.ratio, e.label)
|
||||
|
||||
menu.Font.SetColor(textColor.Alpha(e.labelAlpha))
|
||||
menu.Font.Print(
|
||||
840*menu.ratio,
|
||||
float32(h)*e.yp+fontOffset,
|
||||
0.5*menu.ratio, e.label)
|
||||
menu.ratio, e.label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
@@ -164,8 +164,7 @@ var incrCallbacks = map[string]callbackIncrement{
|
||||
v := f.Value().(bool)
|
||||
v = !v
|
||||
f.Set(v)
|
||||
menu.Reconfigure(settings.Current.VideoFullscreen)
|
||||
menu.ContextReset()
|
||||
menu.SetFullscreen(settings.Current.VideoFullscreen)
|
||||
settings.Save()
|
||||
},
|
||||
"VideoMonitorIndex": func(f *structs.Field, direction int) {
|
||||
@@ -178,8 +177,7 @@ var incrCallbacks = map[string]callbackIncrement{
|
||||
v = len(glfw.GetMonitors()) - 1
|
||||
}
|
||||
f.Set(v)
|
||||
menu.Reconfigure(settings.Current.VideoFullscreen)
|
||||
menu.ContextReset()
|
||||
menu.SetFullscreen(settings.Current.VideoFullscreen)
|
||||
settings.Save()
|
||||
},
|
||||
"VideoFilter": func(f *structs.Field, direction int) {
|
||||
|
||||
+8
-8
@@ -298,16 +298,16 @@ func (tabs sceneTabs) render() {
|
||||
stackWidth += e.width*menu.ratio + e.margin*menu.ratio
|
||||
|
||||
if e.labelAlpha > 0 {
|
||||
lw := menu.Font.Width(0.5*menu.ratio, e.label)
|
||||
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)), 0.5*menu.ratio, e.label)
|
||||
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)), 0.5*menu.ratio, e.label)
|
||||
lw = menu.Font.Width(0.4*menu.ratio, e.subLabel)
|
||||
menu.Font.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
|
||||
menu.Font.Print(x-lw/2+1*menu.ratio, float32(int(float32(h)/2+(330+1)*menu.ratio)), 0.4*menu.ratio, e.subLabel)
|
||||
menu.Font.SetColor(lablColor.Alpha(e.labelAlpha))
|
||||
menu.Font.Print(x-lw/2, float32(int(float32(h)/2+330*menu.ratio)), 0.4*menu.ratio, e.subLabel)
|
||||
menu.Font.Print(x-lw/2, float32(int(float32(h)/2+250*menu.ratio)), menu.ratio, e.label)
|
||||
lw = menu.FontSm.Width(menu.ratio, e.subLabel)
|
||||
menu.FontSm.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
|
||||
menu.FontSm.Print(x-lw/2+1*menu.ratio, float32(int(float32(h)/2+(330+1)*menu.ratio)), menu.ratio, e.subLabel)
|
||||
menu.FontSm.SetColor(lablColor.Alpha(e.labelAlpha))
|
||||
menu.FontSm.Print(x-lw/2, float32(int(float32(h)/2+330*menu.ratio)), menu.ratio, e.subLabel)
|
||||
}
|
||||
|
||||
menu.DrawImage(menu.icons["hexagon"],
|
||||
|
||||
@@ -90,7 +90,7 @@ func (s *sceneUpdater) update(dt float32) {
|
||||
s.children[0].icon = "reload"
|
||||
s.children[0].callbackOK = func() {
|
||||
cmd := exec.Command("/usr/sbin/shutdown", "-r", "now")
|
||||
core.UnloadGame()
|
||||
core.Unload()
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
|
||||
|
||||
+7
-3
@@ -50,13 +50,17 @@ func New(vars []VariableInterface) (*Options, error) {
|
||||
|
||||
// Cache core options
|
||||
for _, v := range vars {
|
||||
v := v
|
||||
choices := v.Choices()
|
||||
// A core may expose an empty selector, e.g. when no BIOS files are found.
|
||||
if len(choices) == 0 {
|
||||
continue
|
||||
}
|
||||
o.Vars = append(o.Vars, &Variable{
|
||||
Key: v.Key(),
|
||||
Desc: v.Desc(),
|
||||
Choices: v.Choices(),
|
||||
Choices: choices,
|
||||
Default: v.DefaultValue(),
|
||||
Choice: utils.IndexOfString(v.DefaultValue(), v.Choices()),
|
||||
Choice: utils.IndexOfString(v.DefaultValue(), choices),
|
||||
})
|
||||
}
|
||||
o.Updated = true
|
||||
|
||||
+1
-10
@@ -33,7 +33,6 @@ func ipsAllocTargetData(patch, source []byte) ([]byte, error) {
|
||||
size |= int(patch[offset]) << 8
|
||||
offset++
|
||||
size |= int(patch[offset]) << 0
|
||||
offset++
|
||||
targetLength = size
|
||||
prov := make([]byte, targetLength)
|
||||
return prov, nil
|
||||
@@ -117,15 +116,7 @@ func applyIPS(patch, source []byte) (*[]byte, error) {
|
||||
offset++
|
||||
|
||||
if address == EOF {
|
||||
if offset == len(patch) {
|
||||
return &targetData, nil
|
||||
} else if offset == len(patch)-3 {
|
||||
size := int(patch[offset]) << 16
|
||||
offset++
|
||||
size |= int(patch[offset]) << 8
|
||||
offset++
|
||||
size |= int(patch[offset]) << 0
|
||||
offset++
|
||||
if offset == len(patch) || offset == len(patch)-3 {
|
||||
return &targetData, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,15 +6,14 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/libretro/ludo/core"
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/state"
|
||||
)
|
||||
|
||||
// Save the current state to the filesystem. name is the name of the
|
||||
// savestate file to save to, without extension.
|
||||
func Save(name string) error {
|
||||
s := state.Core.SerializeSize()
|
||||
bytes, err := state.Core.Serialize(s)
|
||||
bytes, err := core.Serialize()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -28,11 +27,9 @@ func Save(name string) error {
|
||||
|
||||
// Load the state from the filesystem
|
||||
func Load(path string) error {
|
||||
s := state.Core.SerializeSize()
|
||||
bytes, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = state.Core.Unserialize(bytes, s)
|
||||
return err
|
||||
return core.Unserialize(bytes)
|
||||
}
|
||||
@@ -43,13 +43,17 @@ func defaultSettings() Settings {
|
||||
"Nintendo - Game Boy Advance": "mgba_libretro",
|
||||
"Nintendo - Game Boy Color": "gambatte_libretro",
|
||||
"Nintendo - Game Boy": "gambatte_libretro",
|
||||
"Nintendo - GameCube": "dolphin_libretro",
|
||||
"Nintendo - Nintendo 3DS": "citra_libretro",
|
||||
"Nintendo - Nintendo 64": "mupen64plus_next_libretro",
|
||||
"Nintendo - Nintendo Entertainment System": "fceumm_libretro",
|
||||
"Nintendo - Nintendo DS": "melonds_libretro",
|
||||
"Nintendo - Pokemon Mini": "pokemini_libretro",
|
||||
"Nintendo - Super Nintendo Entertainment System": "snes9x_libretro",
|
||||
"Nintendo - Virtual Boy": "mednafen_vb_libretro",
|
||||
"Nintendo - Wii": "dolphin_libretro",
|
||||
"Sega - 32X": "picodrive_libretro",
|
||||
"Sega - Dreamcast": "flycast_libretro",
|
||||
"Sega - Game Gear": "genesis_plus_gx_libretro",
|
||||
"Sega - Master System - Mark III": "genesis_plus_gx_libretro",
|
||||
"Sega - Mega Drive - Genesis": "genesis_plus_gx_libretro",
|
||||
@@ -60,6 +64,8 @@ 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",
|
||||
|
||||
@@ -22,10 +22,10 @@ import (
|
||||
// Widget sets the graphical representation of the value.
|
||||
type Settings struct {
|
||||
VideoFullscreen bool `hide:"ludos" toml:"video_fullscreen" label:"Video Fullscreen" fmt:"%t" widget:"switch"`
|
||||
VideoMonitorIndex int `toml:"video_monitor_index" label:"Video Monitor Index" fmt:"%d"`
|
||||
VideoFilter string `toml:"video_filter" label:"Video Filter" fmt:"<%s>"`
|
||||
VideoMonitorIndex int `toml:"video_monitor_index" label:"Video Monitor Index" fmt:"◀ %d ▶"`
|
||||
VideoFilter string `toml:"video_filter" label:"Video Filter" fmt:"◀ %s ▶"`
|
||||
VideoDarkMode bool `toml:"video_dark_mode" label:"Video Dark Mode" fmt:"%t" widget:"switch"`
|
||||
VideoTheme string `toml:"video_theme" label:"Video Theme" fmt:"<%s>"`
|
||||
VideoTheme string `toml:"video_theme" label:"Video Theme" fmt:"◀ %s ▶"`
|
||||
|
||||
AudioVolume float32 `toml:"audio_volume" label:"Audio Volume" fmt:"%.1f" widget:"range"`
|
||||
|
||||
|
||||
@@ -33,3 +33,13 @@ var LudOS bool
|
||||
|
||||
// FastForward will run the core as fast as possible
|
||||
var FastForward bool
|
||||
|
||||
// PendingScreenshotName is the next screenshot capture requested by the UI.
|
||||
var PendingScreenshotName string
|
||||
|
||||
// PendingScreenshotDone is called after the deferred screenshot capture completes.
|
||||
var PendingScreenshotDone func(error)
|
||||
|
||||
// MenuContextResetNeeded requests re-uploading menu GL assets after the
|
||||
// frontend window/context is recreated.
|
||||
var MenuContextResetNeeded bool
|
||||
@@ -0,0 +1,190 @@
|
||||
//go:build gltest && !darwin
|
||||
|
||||
package video
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
"github.com/go-gl/glfw/v3.4/glfw"
|
||||
"github.com/go-gl/mathgl/mgl32"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
"github.com/libretro/ludo/state"
|
||||
)
|
||||
|
||||
// Run with go test -tags 'wayland gltest' ./video on a graphical session.
|
||||
func TestHardwareContextIsolation(t *testing.T) {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
if err := glfw.Init(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer glfw.Terminate()
|
||||
glfw.DefaultWindowHints()
|
||||
glfw.WindowHint(glfw.Visible, glfw.False)
|
||||
glfw.WindowHint(glfw.ContextVersionMajor, 3)
|
||||
glfw.WindowHint(glfw.ContextVersionMinor, 3)
|
||||
glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
|
||||
w, err := glfw.CreateWindow(128, 96, "Ludo GL test", nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer w.Destroy()
|
||||
w.MakeContextCurrent()
|
||||
if err := initGL(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("GL:", gl.GoStr(gl.GetString(gl.VERSION)))
|
||||
|
||||
oldCore, oldRunning := state.Core, state.CoreRunning
|
||||
defer func() { state.Core, state.CoreRunning = oldCore, oldRunning }()
|
||||
hw := &libretro.HWRenderCallback{HWContextType: libretro.HWContextOpenGLCore, VersionMajor: 3, VersionMinor: 3}
|
||||
state.Core = &libretro.Core{HWRenderCallback: hw}
|
||||
state.CoreRunning = true
|
||||
v := &Video{Window: w, identityMat: mgl32.Ident4(), Geom: libretro.GameGeometry{MaxWidth: 64, MaxHeight: 64}}
|
||||
v.SetHWRenderContext(hw)
|
||||
v.ensurePixelFormatDefaults()
|
||||
v.resetGameTexture(64, 64)
|
||||
hw.ContextReset = func() {
|
||||
if glfw.GetCurrentContext() != v.HWWindow {
|
||||
t.Fatal("context_reset ran outside the HW context")
|
||||
}
|
||||
}
|
||||
if err := v.ResetHWContext(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer v.DestroySharedHWContext()
|
||||
if glfw.GetCurrentContext() != w || v.HWWindow == nil || v.HWWindow == w {
|
||||
t.Fatal("reset did not return to an independent frontend context")
|
||||
}
|
||||
|
||||
v.BeginCore()
|
||||
var readFBO, texture, pbo uint32
|
||||
gl.GenFramebuffers(1, &readFBO)
|
||||
gl.BindFramebuffer(gl.READ_FRAMEBUFFER, readFBO)
|
||||
gl.GenTextures(1, &texture)
|
||||
gl.ActiveTexture(gl.TEXTURE1)
|
||||
gl.BindTexture(gl.TEXTURE_2D, texture)
|
||||
gl.GenBuffers(1, &pbo)
|
||||
gl.BindBuffer(gl.PIXEL_PACK_BUFFER, pbo)
|
||||
gl.PixelStorei(gl.PACK_ROW_LENGTH, 256)
|
||||
gl.Enable(gl.BLEND)
|
||||
gl.Enable(gl.SCISSOR_TEST)
|
||||
gl.BlendEquation(gl.FUNC_REVERSE_SUBTRACT)
|
||||
v.EndCore()
|
||||
v.BeginFrontendFrame()
|
||||
checkInt := func(name uint32, want int32) {
|
||||
t.Helper()
|
||||
var got int32
|
||||
gl.GetIntegerv(name, &got)
|
||||
if got != want {
|
||||
t.Errorf("GL state %#x: got %d, want %d", name, got, want)
|
||||
}
|
||||
}
|
||||
checkInt(gl.PIXEL_PACK_BUFFER_BINDING, 0)
|
||||
checkInt(gl.PACK_ROW_LENGTH, 0)
|
||||
checkInt(gl.READ_FRAMEBUFFER_BINDING, 0)
|
||||
checkInt(gl.BLEND_EQUATION_RGB, gl.FUNC_ADD)
|
||||
v.BeginCore()
|
||||
checkInt(gl.PIXEL_PACK_BUFFER_BINDING, int32(pbo))
|
||||
checkInt(gl.PACK_ROW_LENGTH, 256)
|
||||
checkInt(gl.READ_FRAMEBUFFER_BINDING, int32(readFBO))
|
||||
checkInt(gl.DRAW_FRAMEBUFFER_BINDING, int32(v.fboID))
|
||||
checkInt(gl.BLEND_EQUATION_RGB, gl.FUNC_REVERSE_SUBTRACT)
|
||||
checkInt(gl.ACTIVE_TEXTURE, gl.TEXTURE1)
|
||||
checkInt(gl.TEXTURE_BINDING_2D, int32(texture))
|
||||
if !gl.IsEnabled(gl.BLEND) || !gl.IsEnabled(gl.SCISSOR_TEST) {
|
||||
t.Error("frontend rendering changed core enables")
|
||||
}
|
||||
gl.BindBuffer(gl.PIXEL_PACK_BUFFER, 0)
|
||||
gl.PixelStorei(gl.PACK_ROW_LENGTH, 0)
|
||||
gl.Disable(gl.SCISSOR_TEST)
|
||||
gl.Disable(gl.BLEND)
|
||||
gl.BindFramebuffer(gl.READ_FRAMEBUFFER, v.fboID)
|
||||
|
||||
// Verify the produced pixels are visible through the shared texture.
|
||||
gl.ClearColor(1, 0, 0, 1)
|
||||
gl.Clear(gl.COLOR_BUFFER_BIT)
|
||||
v.EndCore()
|
||||
var frontendFBO uint32
|
||||
gl.GenFramebuffers(1, &frontendFBO)
|
||||
gl.BindFramebuffer(gl.FRAMEBUFFER, frontendFBO)
|
||||
gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, v.texID, 0)
|
||||
var pixel [4]byte
|
||||
gl.ReadPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(&pixel[0]))
|
||||
if pixel != [4]byte{255, 0, 0, 255} {
|
||||
t.Errorf("shared frame: got %v, want red", pixel)
|
||||
}
|
||||
v.BeginFrontendFrame()
|
||||
|
||||
// Backbuffer snapshots must retain their orientation for either core origin.
|
||||
v.defaultProgram, err = newProgram(vertexShader, defaultFragmentShader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gl.GenVertexArrays(1, &v.vao)
|
||||
gl.BindVertexArray(v.vao)
|
||||
gl.GenBuffers(1, &v.vbo)
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, v.vbo)
|
||||
vert := uint32(gl.GetAttribLocation(v.defaultProgram, gl.Str("vert\x00")))
|
||||
uv := uint32(gl.GetAttribLocation(v.defaultProgram, gl.Str("vertTexCoord\x00")))
|
||||
gl.EnableVertexAttribArray(vert)
|
||||
gl.VertexAttribPointerWithOffset(vert, 2, gl.FLOAT, false, 16, 0)
|
||||
gl.EnableVertexAttribArray(uv)
|
||||
gl.VertexAttribPointerWithOffset(uv, 2, gl.FLOAT, false, 16, 8)
|
||||
v.width, v.height = 64, 64
|
||||
for _, bottomLeft := range []bool{false, true} {
|
||||
hw.BottomLeftOrigin = bottomLeft
|
||||
v.BeginFrontendFrame()
|
||||
fbw, fbh := w.GetFramebufferSize()
|
||||
v.Geom.AspectRatio = float64(fbw) / float64(fbh)
|
||||
v.presentW, v.presentH = int32(fbw), int32(fbh)
|
||||
gl.ClearColor(1, 0, 0, 1)
|
||||
gl.Clear(gl.COLOR_BUFFER_BIT)
|
||||
gl.Enable(gl.SCISSOR_TEST)
|
||||
gl.Scissor(0, int32(fbh/2), int32(fbw), int32(fbh/2))
|
||||
gl.ClearColor(0, 0, 1, 1)
|
||||
gl.Clear(gl.COLOR_BUFFER_BIT)
|
||||
gl.Disable(gl.SCISSOR_TEST)
|
||||
v.CachePresentedFrame()
|
||||
if !v.RenderCachedFrame() {
|
||||
t.Fatal("cached frame unavailable")
|
||||
}
|
||||
gl.ReadPixels(1, 1, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(&pixel[0]))
|
||||
if pixel != [4]byte{255, 0, 0, 255} {
|
||||
t.Errorf("cached frame, bottom-left=%v: got %v, want red", bottomLeft, pixel)
|
||||
}
|
||||
}
|
||||
|
||||
hw.ContextDestroy = func() {
|
||||
if glfw.GetCurrentContext() != v.HWWindow {
|
||||
t.Error("context_destroy ran outside the HW context")
|
||||
}
|
||||
}
|
||||
v.RunContextDestroy()
|
||||
v.ResetCoreState()
|
||||
if !gl.IsFramebuffer(frontendFBO) {
|
||||
t.Error("HW teardown deleted a frontend framebuffer with the same name")
|
||||
}
|
||||
if v.fboID != 0 || v.HWWindow != nil || v.cachedFrameValid || v.width != 0 {
|
||||
t.Error("HW teardown left stale core resources")
|
||||
}
|
||||
|
||||
// A subsequent core must get a fresh context, including on the GL 2.x sync fallback.
|
||||
if err := v.ResetHWContext(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
v.hwSync = false
|
||||
v.BeginCore()
|
||||
checkInt(gl.PACK_ROW_LENGTH, 0)
|
||||
checkInt(gl.PIXEL_PACK_BUFFER_BINDING, 0)
|
||||
checkInt(gl.BLEND_EQUATION_RGB, gl.FUNC_ADD)
|
||||
if gl.IsEnabled(gl.BLEND) || gl.IsEnabled(gl.SCISSOR_TEST) {
|
||||
t.Error("new core inherited the previous core's enables")
|
||||
}
|
||||
v.EndCore()
|
||||
if e := gl.GetError(); e != gl.NO_ERROR {
|
||||
t.Errorf("GL error: %#x", e)
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,9 @@ var vertexShader = `
|
||||
#if __VERSION__ >= 130
|
||||
#define COMPAT_VARYING out
|
||||
#define COMPAT_ATTRIBUTE in
|
||||
#define COMPAT_TEXTURE texture
|
||||
#define COMPAT_FRAGCOLOR FragColor
|
||||
out vec4 COMPAT_FRAGCOLOR;
|
||||
#else
|
||||
#define COMPAT_VARYING varying
|
||||
#define COMPAT_ATTRIBUTE attribute
|
||||
#define COMPAT_TEXTURE texture2D
|
||||
#define COMPAT_FRAGCOLOR gl_FragColor
|
||||
#endif
|
||||
|
||||
COMPAT_ATTRIBUTE vec2 vert;
|
||||
@@ -19,8 +14,10 @@ COMPAT_ATTRIBUTE vec2 vertTexCoord;
|
||||
|
||||
COMPAT_VARYING vec2 fragTexCoord;
|
||||
|
||||
uniform mat4 MVP;
|
||||
|
||||
void main() {
|
||||
fragTexCoord = vertTexCoord;
|
||||
gl_Position = vec4(vert, 0.0, 1.0);
|
||||
gl_Position = vec4(vert, 0.0, 1.0) * MVP;
|
||||
}
|
||||
` + "\x00"
|
||||
+325
-128
@@ -5,6 +5,7 @@ import (
|
||||
"image"
|
||||
"image/draw"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
@@ -14,7 +15,7 @@ import (
|
||||
"golang.org/x/image/math/fixed"
|
||||
)
|
||||
|
||||
type character struct {
|
||||
type glyph struct {
|
||||
x, y int
|
||||
width int // Glyph width
|
||||
height int // Glyph height
|
||||
@@ -33,9 +34,11 @@ const (
|
||||
TopToBottom // E.g.: Chinese
|
||||
)
|
||||
|
||||
const fontRenderScale = 1
|
||||
|
||||
// A Font allows rendering of text to an OpenGL context.
|
||||
type Font struct {
|
||||
fontChar []*character
|
||||
glyphs map[rune]*glyph
|
||||
vao uint32
|
||||
vbo uint32
|
||||
program uint32
|
||||
@@ -43,19 +46,248 @@ type Font struct {
|
||||
color Color
|
||||
atlasWidth float32
|
||||
atlasHeight float32
|
||||
atlas *image.RGBA
|
||||
face font.Face
|
||||
ttf *truetype.Font
|
||||
scale int32
|
||||
lineHeight float32
|
||||
margin int
|
||||
nextX int
|
||||
nextY int
|
||||
}
|
||||
|
||||
type point [4]float32
|
||||
|
||||
func max(a, b float32) float32 {
|
||||
if a > b {
|
||||
return a
|
||||
type glyphMetrics struct {
|
||||
bounds fixed.Rectangle26_6
|
||||
advance fixed.Int26_6
|
||||
width int
|
||||
height int
|
||||
ascent int
|
||||
descent int
|
||||
}
|
||||
|
||||
func ceilFixed26_6(v fixed.Int26_6) int32 {
|
||||
return int32((v + 63) >> 6)
|
||||
}
|
||||
|
||||
func appendRange(runes []rune, start, end rune) []rune {
|
||||
for r := start; r <= end; r++ {
|
||||
runes = append(runes, r)
|
||||
}
|
||||
return b
|
||||
return runes
|
||||
}
|
||||
|
||||
func defaultRunes() []rune {
|
||||
runes := make([]rune, 0, 512)
|
||||
runes = appendRange(runes, 32, 126)
|
||||
runes = append(runes,
|
||||
'◀', '▶', '【', '】',
|
||||
'「', '」', '『', '』',
|
||||
'。', '、', '・', 'ー',
|
||||
'!', '?',
|
||||
'(', ')', '[', ']', '{', '}',
|
||||
)
|
||||
runes = appendRange(runes, 0xFF01, 0xFF0F) // fullwidth punctuation
|
||||
runes = appendRange(runes, 0xFF10, 0xFF19) // fullwidth digits
|
||||
runes = appendRange(runes, 0xFF1A, 0xFF20) // fullwidth : ; < = > ? @
|
||||
runes = appendRange(runes, 0xFF21, 0xFF3A) // fullwidth A-Z
|
||||
runes = appendRange(runes, 0xFF3B, 0xFF40) // fullwidth [ \ ] ^ _
|
||||
runes = appendRange(runes, 0xFF41, 0xFF5A) // fullwidth a-z
|
||||
runes = appendRange(runes, 0xFF5B, 0xFF65) // fullwidth { | } ~ and katakana punctuation
|
||||
runes = appendRange(runes, 0x00A0, 0x017F)
|
||||
runes = appendRange(runes, 0x3000, 0x303F)
|
||||
runes = appendRange(runes, 0x3040, 0x30FF)
|
||||
return runes
|
||||
}
|
||||
|
||||
func getGlyphMetrics(face font.Face, ttf *truetype.Font, ch rune, scale int32) (glyphMetrics, error) {
|
||||
gBnd, gAdv, ok := face.GlyphBounds(ch)
|
||||
if !ok {
|
||||
return glyphMetrics{}, fmt.Errorf("ttf face glyphBounds error")
|
||||
}
|
||||
|
||||
gh := ceilFixed26_6(gBnd.Max.Y - gBnd.Min.Y)
|
||||
gw := ceilFixed26_6(gBnd.Max.X - gBnd.Min.X)
|
||||
|
||||
// If gylph has no dimensions set to a max value
|
||||
if gw == 0 || gh == 0 {
|
||||
gBnd = ttf.Bounds(fixed.Int26_6(scale))
|
||||
gw = ceilFixed26_6(gBnd.Max.X - gBnd.Min.X)
|
||||
gh = ceilFixed26_6(gBnd.Max.Y - gBnd.Min.Y)
|
||||
|
||||
// Above can sometimes yield 0 for font smaller than 48pt, 1 is minimum
|
||||
if gw == 0 || gh == 0 {
|
||||
gw = 1
|
||||
gh = 1
|
||||
}
|
||||
}
|
||||
|
||||
// The glyph's ascent and descent equal -bounds.Min.Y and +bounds.Max.Y.
|
||||
gAscent := int(ceilFixed26_6(-gBnd.Min.Y))
|
||||
gdescent := int(ceilFixed26_6(gBnd.Max.Y))
|
||||
|
||||
return glyphMetrics{
|
||||
bounds: gBnd,
|
||||
advance: gAdv,
|
||||
width: int(gw),
|
||||
height: int(gh),
|
||||
ascent: gAscent,
|
||||
descent: gdescent,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func wrapGlyph(x, y *int, glyphWidth, lineHeight, atlasWidth, margin int) {
|
||||
if *x+glyphWidth+margin > atlasWidth {
|
||||
*x = margin
|
||||
*y += lineHeight + margin
|
||||
}
|
||||
}
|
||||
|
||||
func appendGlyph(face font.Face, ttf *truetype.Font, ch rune, x, y *int, lineHeight, atlasWidth float32, atlas *image.RGBA, fg *image.Uniform, scale int32, margin int) (*glyph, error) {
|
||||
char := new(glyph)
|
||||
|
||||
metrics, err := getGlyphMetrics(face, ttf, ch, scale)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wrapGlyph(x, y, metrics.width, int(lineHeight), int(atlasWidth), margin)
|
||||
if *y+metrics.height+margin > atlas.Rect.Dy() {
|
||||
return nil, fmt.Errorf("glyph atlas full")
|
||||
}
|
||||
|
||||
// Set w,h and adv, bearing V and bearing H in char
|
||||
char.x = *x
|
||||
char.y = *y
|
||||
char.width = metrics.width
|
||||
char.height = metrics.height
|
||||
char.advance = int(metrics.advance)
|
||||
char.bearingV = metrics.descent
|
||||
char.bearingH = (int(metrics.bounds.Min.X) >> 6)
|
||||
|
||||
clip := image.Rect(*x, *y, *x+metrics.width, *y+metrics.height)
|
||||
|
||||
// Create a freetype context for drawing
|
||||
c := freetype.NewContext()
|
||||
c.SetDPI(72)
|
||||
c.SetFont(ttf)
|
||||
c.SetFontSize(float64(scale))
|
||||
c.SetClip(clip)
|
||||
c.SetDst(atlas)
|
||||
c.SetSrc(fg)
|
||||
c.SetHinting(font.HintingFull)
|
||||
|
||||
// Set the glyph dot
|
||||
px := 0 - (int(metrics.bounds.Min.X) >> 6) + *x
|
||||
py := (metrics.ascent) + *y
|
||||
pt := freetype.Pt(px, py)
|
||||
|
||||
// Draw the text from mask to image
|
||||
_, err = c.DrawString(string(ch), pt)
|
||||
*x += metrics.width + margin
|
||||
return char, err
|
||||
}
|
||||
|
||||
func getMaxTextureSize() int32 {
|
||||
var maxTextureSize int32
|
||||
gl.GetIntegerv(gl.MAX_TEXTURE_SIZE, &maxTextureSize)
|
||||
if maxTextureSize == 0 {
|
||||
maxTextureSize = 4096
|
||||
}
|
||||
return maxTextureSize
|
||||
}
|
||||
|
||||
func calculateAtlasSize(face font.Face, ttf *truetype.Font, runes []rune, scale int32, margin int) (float32, float32, float32, error) {
|
||||
maxTextureSize := getMaxTextureSize()
|
||||
lineHeight := float32(face.Metrics().Height.Ceil())
|
||||
if lineHeight < 1 {
|
||||
lineHeight = 1
|
||||
}
|
||||
|
||||
width := 2048
|
||||
if width > int(maxTextureSize) {
|
||||
width = int(maxTextureSize)
|
||||
}
|
||||
|
||||
for {
|
||||
x := margin
|
||||
y := margin
|
||||
maxY := 0
|
||||
|
||||
for _, r := range runes {
|
||||
metrics, err := getGlyphMetrics(face, ttf, r, scale)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
wrapGlyph(&x, &y, metrics.width, int(lineHeight), width, margin)
|
||||
x += metrics.width + margin
|
||||
|
||||
if y+int(lineHeight) > maxY {
|
||||
maxY = y + int(lineHeight)
|
||||
}
|
||||
}
|
||||
|
||||
height := maxY + margin
|
||||
|
||||
if height <= int(maxTextureSize) || width >= int(maxTextureSize) {
|
||||
if height > int(maxTextureSize) {
|
||||
return 0, 0, 0, fmt.Errorf("glyph atlas %dx%d exceeds max texture size %d", width, height, maxTextureSize)
|
||||
}
|
||||
return float32(width), float32(height), lineHeight, nil
|
||||
}
|
||||
|
||||
width *= 2
|
||||
if width > int(maxTextureSize) {
|
||||
width = int(maxTextureSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func reserveAtlasHeight(baseHeight int, lineHeight float32, margin int, maxTextureSize int32) int {
|
||||
height := baseHeight * 2
|
||||
extraRowsHeight := baseHeight + 32*(int(lineHeight)+margin)
|
||||
if extraRowsHeight > height {
|
||||
height = extraRowsHeight
|
||||
}
|
||||
if height > int(maxTextureSize) {
|
||||
height = int(maxTextureSize)
|
||||
}
|
||||
return height
|
||||
}
|
||||
|
||||
func (f *Font) uploadGlyphRect(g *glyph) {
|
||||
if f.atlas == nil || g == nil {
|
||||
return
|
||||
}
|
||||
|
||||
offset := g.y*f.atlas.Stride + g.x*4
|
||||
if offset < 0 || offset >= len(f.atlas.Pix) {
|
||||
return
|
||||
}
|
||||
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, f.textureID)
|
||||
gl.PixelStorei(gl.UNPACK_ALIGNMENT, 1)
|
||||
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, int32(f.atlas.Stride/4))
|
||||
gl.TexSubImage2D(
|
||||
gl.TEXTURE_2D,
|
||||
0,
|
||||
int32(g.x),
|
||||
int32(g.y),
|
||||
int32(g.width),
|
||||
int32(g.height),
|
||||
gl.RGBA,
|
||||
gl.UNSIGNED_BYTE,
|
||||
gl.Ptr(f.atlas.Pix[offset:]),
|
||||
)
|
||||
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, 0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, 0)
|
||||
}
|
||||
|
||||
// LoadTrueTypeFont builds a set of textures based on a ttf files gylphs
|
||||
func LoadTrueTypeFont(program uint32, r io.Reader, scale int32, low, high rune, dir Direction) (*Font, error) {
|
||||
func LoadTrueTypeFont(program uint32, r io.Reader, scale int32, dir Direction) (*Font, error) {
|
||||
data, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -69,120 +301,63 @@ func LoadTrueTypeFont(program uint32, r io.Reader, scale int32, low, high rune,
|
||||
|
||||
// Make Font stuct type
|
||||
f := new(Font)
|
||||
f.fontChar = make([]*character, 0, high-low+1)
|
||||
f.glyphs = make(map[rune]*glyph)
|
||||
f.program = program // Set shader program
|
||||
f.SetColor(Color{R: 1, G: 1, B: 1, A: 1}) // Set default white
|
||||
f.ttf = ttf
|
||||
f.scale = scale
|
||||
|
||||
// Create new face
|
||||
ttfFace := truetype.NewFace(ttf, &truetype.Options{
|
||||
face := truetype.NewFace(ttf, &truetype.Options{
|
||||
Size: float64(scale),
|
||||
DPI: 72,
|
||||
Hinting: font.HintingFull,
|
||||
})
|
||||
f.face = face
|
||||
|
||||
margin := 4
|
||||
runes := defaultRunes()
|
||||
var lineHeight float32
|
||||
f.atlasWidth = 1024
|
||||
f.atlasHeight = 1024
|
||||
for ch := low; ch <= high; ch++ {
|
||||
gBnd, _, ok := ttfFace.GlyphBounds(ch)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("ttf face glyphBounds error")
|
||||
}
|
||||
gh := int32((gBnd.Max.Y - gBnd.Min.Y) >> 6)
|
||||
lineHeight = max(lineHeight, float32(gh))
|
||||
|
||||
f.atlasWidth, f.atlasHeight, lineHeight, err = calculateAtlasSize(face, ttf, runes, scale, margin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.atlasHeight = float32(reserveAtlasHeight(int(f.atlasHeight), lineHeight, margin, getMaxTextureSize()))
|
||||
f.lineHeight = lineHeight
|
||||
f.margin = margin
|
||||
|
||||
// Create image to draw glyph
|
||||
fg, bg := image.White, image.Black
|
||||
rect := image.Rect(0, 0, int(f.atlasWidth), int(f.atlasHeight))
|
||||
rgba := image.NewRGBA(rect)
|
||||
draw.Draw(rgba, rgba.Bounds(), bg, image.Point{}, draw.Src)
|
||||
atlas := image.NewRGBA(rect)
|
||||
draw.Draw(atlas, atlas.Bounds(), bg, image.Point{}, draw.Src)
|
||||
f.atlas = atlas
|
||||
|
||||
margin := 4
|
||||
x := margin
|
||||
y := margin
|
||||
|
||||
// Make each gylph
|
||||
for ch := low; ch <= high; ch++ {
|
||||
char := new(character)
|
||||
|
||||
gBnd, gAdv, ok := ttfFace.GlyphBounds(ch)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("ttf face glyphBounds error")
|
||||
}
|
||||
|
||||
gh := int32((gBnd.Max.Y - gBnd.Min.Y) >> 6)
|
||||
gw := int32((gBnd.Max.X - gBnd.Min.X) >> 6)
|
||||
|
||||
// If gylph has no dimensions set to a max value
|
||||
if gw == 0 || gh == 0 {
|
||||
gBnd = ttf.Bounds(fixed.Int26_6(scale))
|
||||
gw = int32((gBnd.Max.X - gBnd.Min.X) >> 6)
|
||||
gh = int32((gBnd.Max.Y - gBnd.Min.Y) >> 6)
|
||||
|
||||
// Above can sometimes yield 0 for font smaller than 48pt, 1 is minimum
|
||||
if gw == 0 || gh == 0 {
|
||||
gw = 1
|
||||
gh = 1
|
||||
}
|
||||
}
|
||||
|
||||
// The glyph's ascent and descent equal -bounds.Min.Y and +bounds.Max.Y.
|
||||
gAscent := int(-gBnd.Min.Y) >> 6
|
||||
gdescent := int(gBnd.Max.Y) >> 6
|
||||
|
||||
// Set w,h and adv, bearing V and bearing H in char
|
||||
char.x = x
|
||||
char.y = y
|
||||
char.width = int(gw)
|
||||
char.height = int(gh)
|
||||
char.advance = int(gAdv)
|
||||
char.bearingV = gdescent
|
||||
char.bearingH = (int(gBnd.Min.X) >> 6)
|
||||
|
||||
clip := image.Rect(x, y, x+int(gw), y+int(gh))
|
||||
|
||||
// Create a freetype context for drawing
|
||||
c := freetype.NewContext()
|
||||
c.SetDPI(72)
|
||||
c.SetFont(ttf)
|
||||
c.SetFontSize(float64(scale))
|
||||
c.SetClip(clip)
|
||||
c.SetDst(rgba)
|
||||
c.SetSrc(fg)
|
||||
c.SetHinting(font.HintingFull)
|
||||
|
||||
// Set the glyph dot
|
||||
px := 0 - (int(gBnd.Min.X) >> 6) + x
|
||||
py := (gAscent) + y
|
||||
pt := freetype.Pt(px, py)
|
||||
|
||||
x += int(gw) + margin
|
||||
if x+int(gw)+margin > int(f.atlasWidth) {
|
||||
x = 0
|
||||
y += int(lineHeight) + margin
|
||||
}
|
||||
|
||||
// Draw the text from mask to image
|
||||
_, err = c.DrawString(string(ch), pt)
|
||||
for _, r := range runes {
|
||||
glyph, err := appendGlyph(face, ttf, r, &x, &y, lineHeight, f.atlasWidth, atlas, fg, scale, margin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
continue
|
||||
}
|
||||
|
||||
// Add char to fontChar list
|
||||
f.fontChar = append(f.fontChar, char)
|
||||
f.glyphs[r] = glyph
|
||||
}
|
||||
f.nextX = x
|
||||
f.nextY = y
|
||||
|
||||
// Generate texture
|
||||
gl.GenTextures(1, &f.textureID)
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, f.textureID)
|
||||
gl.PixelStorei(gl.UNPACK_ALIGNMENT, 1)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
|
||||
|
||||
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, int32(rgba.Rect.Dx()), int32(rgba.Rect.Dy()), 0, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(rgba.Pix))
|
||||
|
||||
gl.GenerateMipmap(gl.TEXTURE_2D)
|
||||
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, int32(atlas.Rect.Dx()), int32(atlas.Rect.Dy()), 0, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(atlas.Pix))
|
||||
gl.BindTexture(gl.TEXTURE_2D, 0)
|
||||
|
||||
// Configure VAO/VBO for texture quads
|
||||
@@ -205,6 +380,25 @@ func LoadTrueTypeFont(program uint32, r io.Reader, scale int32, low, high rune,
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (f *Font) ensureGlyph(r rune) *glyph {
|
||||
if glyph, ok := f.glyphs[r]; ok {
|
||||
return glyph
|
||||
}
|
||||
if f.atlas == nil || f.face == nil || f.ttf == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
glyph, err := appendGlyph(f.face, f.ttf, r, &f.nextX, &f.nextY, f.lineHeight, f.atlasWidth, f.atlas, image.White, f.scale, f.margin)
|
||||
if err != nil {
|
||||
log.Printf("font: failed to append U+%04X %q: %v", r, string(r), err)
|
||||
return nil
|
||||
}
|
||||
f.glyphs[r] = glyph
|
||||
f.uploadGlyphRect(glyph)
|
||||
|
||||
return glyph
|
||||
}
|
||||
|
||||
// LoadFont loads the specified font at the given scale.
|
||||
func LoadFont(file string, scale int32, windowWidth int, windowHeight int) (*Font, error) {
|
||||
fd, err := os.Open(file)
|
||||
@@ -226,7 +420,16 @@ func LoadFont(file string, scale int32, windowWidth int, windowHeight int) (*Fon
|
||||
resUniform := gl.GetUniformLocation(program, gl.Str("resolution\x00"))
|
||||
gl.Uniform2f(resUniform, float32(windowWidth), float32(windowHeight))
|
||||
|
||||
return LoadTrueTypeFont(program, fd, scale, 32, 256, LeftToRight)
|
||||
atlasScale := int32(float32(scale) / fontRenderScale)
|
||||
if atlasScale < 1 {
|
||||
atlasScale = 1
|
||||
}
|
||||
|
||||
font, err := LoadTrueTypeFont(program, fd, atlasScale, LeftToRight)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return font, nil
|
||||
}
|
||||
|
||||
// SetColor allows you to set the text color to be used when you draw the text
|
||||
@@ -245,13 +448,14 @@ func (f *Font) UpdateResolution(windowWidth int, windowHeight int) {
|
||||
// Print draws a string to the screen.
|
||||
func (f *Font) Print(x, y float32, scale float32, text string) error {
|
||||
indices := []rune(text)
|
||||
scale *= fontRenderScale
|
||||
x = float32(int(x + 0.5))
|
||||
y = float32(int(y + 0.5))
|
||||
|
||||
if len(indices) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
lowChar := rune(32)
|
||||
|
||||
// Setup blending mode
|
||||
gl.Enable(gl.BLEND)
|
||||
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
|
||||
@@ -263,26 +467,23 @@ func (f *Font) Print(x, y float32, scale float32, text string) error {
|
||||
|
||||
var coords []point
|
||||
|
||||
// Iterate through all characters in string
|
||||
// Iterate through all glyphs in string
|
||||
for i := range indices {
|
||||
// Get rune
|
||||
runeIndex := indices[i]
|
||||
|
||||
var ch *character
|
||||
// Skip runes that are not in font chacter range
|
||||
if int(runeIndex)-int(lowChar) > len(f.fontChar) || runeIndex < lowChar {
|
||||
// Print a ?
|
||||
ch = f.fontChar[int(rune('?'))-int(lowChar)]
|
||||
} else {
|
||||
// Find rune in fontChar list
|
||||
ch = f.fontChar[runeIndex-lowChar]
|
||||
var ch *glyph
|
||||
// Skip runes that are not in font glyph map
|
||||
ch = f.ensureGlyph(runeIndex)
|
||||
if ch == nil {
|
||||
ch = f.glyphs['?'] // fallback
|
||||
}
|
||||
|
||||
// Calculate position and size for current rune
|
||||
xpos := x - 1 + float32(ch.bearingH)*scale
|
||||
ypos := y - 2 - float32(ch.height-ch.bearingV)*scale
|
||||
w := float32(ch.width+2) * scale
|
||||
h := float32(ch.height+2) * scale
|
||||
xpos := x + float32(ch.bearingH)*scale
|
||||
ypos := y - float32(ch.height-ch.bearingV)*scale
|
||||
w := float32(ch.width) * scale
|
||||
h := float32(ch.height) * scale
|
||||
|
||||
// Set quad positions
|
||||
var x1 = xpos
|
||||
@@ -290,12 +491,12 @@ 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-1) / f.atlasWidth, float32(ch.y-1) / f.atlasHeight})
|
||||
coords = append(coords, point{x2, y1, float32(ch.x+ch.width+1) / f.atlasWidth, float32(ch.y-1) / f.atlasHeight})
|
||||
coords = append(coords, point{x1, y2, float32(ch.x-1) / f.atlasWidth, float32(ch.y+ch.height+1) / f.atlasHeight})
|
||||
coords = append(coords, point{x2, y1, float32(ch.x+ch.width+1) / f.atlasWidth, float32(ch.y-1) / f.atlasHeight})
|
||||
coords = append(coords, point{x1, y2, float32(ch.x-1) / f.atlasWidth, float32(ch.y+ch.height+1) / f.atlasHeight})
|
||||
coords = append(coords, point{x2, y2, float32(ch.x+ch.width+1) / f.atlasWidth, float32(ch.y+ch.height+1) / f.atlasHeight})
|
||||
coords = append(coords, point{x1, y1, float32(ch.x) / f.atlasWidth, float32(ch.y) / f.atlasHeight})
|
||||
coords = append(coords, point{x2, y1, float32(ch.x+ch.width) / f.atlasWidth, float32(ch.y) / f.atlasHeight})
|
||||
coords = append(coords, point{x1, y2, float32(ch.x) / f.atlasWidth, float32(ch.y+ch.height) / f.atlasHeight})
|
||||
coords = append(coords, point{x2, y1, float32(ch.x+ch.width) / f.atlasWidth, float32(ch.y) / f.atlasHeight})
|
||||
coords = append(coords, point{x1, y2, float32(ch.x) / f.atlasWidth, float32(ch.y+ch.height) / f.atlasHeight})
|
||||
coords = append(coords, point{x2, y2, float32(ch.x+ch.width) / f.atlasWidth, float32(ch.y+ch.height) / f.atlasHeight})
|
||||
|
||||
// Now advance cursors for next glyph (note that advance is number of 1/64 pixels)
|
||||
x += float32((ch.advance >> 6)) * scale // Bitshift by 6 to get value in pixels (2^6 = 64 (divide amount of 1/64th pixels by 64 to get amount of pixels))
|
||||
@@ -323,6 +524,7 @@ func (f *Font) Printf(x, y float32, scale float32, format string, argv ...interf
|
||||
// Width returns the width of a piece of text in pixels.
|
||||
func (f *Font) Width(scale float32, text string) float32 {
|
||||
var width float32
|
||||
scale *= fontRenderScale
|
||||
|
||||
indices := []rune(text)
|
||||
|
||||
@@ -330,26 +532,21 @@ func (f *Font) Width(scale float32, text string) float32 {
|
||||
return 0
|
||||
}
|
||||
|
||||
lowChar := rune(32)
|
||||
|
||||
// Iterate through all characters in string
|
||||
// Iterate through all glyphs in string
|
||||
for i := range indices {
|
||||
|
||||
// Get rune
|
||||
runeIndex := indices[i]
|
||||
|
||||
// Skip runes that are not in font chacter range
|
||||
if int(runeIndex)-int(lowChar) > len(f.fontChar) || runeIndex < lowChar {
|
||||
fmt.Printf("%c %d\n", runeIndex, runeIndex)
|
||||
continue
|
||||
if f.face != nil {
|
||||
if advance, ok := f.face.GlyphAdvance(runeIndex); ok {
|
||||
width += float32(advance.Round()) * scale
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Find rune in fontChar list
|
||||
ch := f.fontChar[runeIndex-lowChar]
|
||||
|
||||
// Now advance cursors for next glyph (note that advance is number of 1/64 pixels)
|
||||
width += float32((ch.advance >> 6)) * scale // Bitshift by 6 to get value in pixels (2^6 = 64 (divide amount of 1/64th pixels by 64 to get amount of pixels))
|
||||
|
||||
if ch, ok := f.glyphs['?']; ok {
|
||||
width += float32((ch.advance >> 6)) * scale
|
||||
}
|
||||
}
|
||||
|
||||
return width
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package video
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
"github.com/go-gl/mathgl/mgl32"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
"github.com/libretro/ludo/state"
|
||||
)
|
||||
|
||||
// InitFramebuffer initializes and configures the video frame buffer based on
|
||||
// informations from the HWRenderCallback of the libretro core.
|
||||
func (video *Video) InitFramebuffer() {
|
||||
width := int32(video.Geom.MaxWidth)
|
||||
height := int32(video.Geom.MaxHeight)
|
||||
video.texWidth = width
|
||||
video.texHeight = height
|
||||
|
||||
log.Printf("[Video]: Initializing HW render (%v x %v).\n", width, height)
|
||||
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, video.texID)
|
||||
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, nil)
|
||||
|
||||
genFramebuffers(1, &video.fboID)
|
||||
bindFramebuffer(gl.FRAMEBUFFER, video.fboID)
|
||||
libretro.SetCurrentFramebufferValue(uintptr(video.fboID))
|
||||
framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, video.texID, 0)
|
||||
|
||||
hw := state.Core.HWRenderCallback
|
||||
if hw.Depth {
|
||||
genRenderbuffers(1, &video.rboID)
|
||||
bindRenderbuffer(gl.RENDERBUFFER, video.rboID)
|
||||
format := gl.DEPTH_COMPONENT16
|
||||
if hw.Stencil {
|
||||
format = gl.DEPTH24_STENCIL8
|
||||
}
|
||||
renderbufferStorage(gl.RENDERBUFFER, uint32(format), width, height)
|
||||
bindRenderbuffer(gl.RENDERBUFFER, 0)
|
||||
|
||||
// Separate attachment points work with both core and EXT framebuffers.
|
||||
framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, video.rboID)
|
||||
if hw.Stencil {
|
||||
framebufferRenderbuffer(gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, gl.RENDERBUFFER, video.rboID)
|
||||
}
|
||||
}
|
||||
|
||||
// Default origin is top left
|
||||
video.orthoMat = mgl32.Ortho2D(-1, 1, -1, 1)
|
||||
if hw.BottomLeftOrigin {
|
||||
video.orthoMat = mgl32.Ortho2D(-1, 1, 1, -1)
|
||||
}
|
||||
|
||||
if st := checkFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE {
|
||||
log.Fatalf("[Video] Framebuffer is not complete. Error: %v\n", st)
|
||||
}
|
||||
|
||||
gl.ClearColor(0, 0, 0, 1)
|
||||
if hw.Depth && hw.Stencil {
|
||||
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT)
|
||||
} else if hw.Depth {
|
||||
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
|
||||
} else {
|
||||
gl.Clear(gl.COLOR_BUFFER_BIT)
|
||||
}
|
||||
}
|
||||
|
||||
func bindBackbuffer() {
|
||||
bindFramebuffer(gl.FRAMEBUFFER, 0)
|
||||
}
|
||||
|
||||
func (video *Video) destroyFramebuffer() {
|
||||
bindBackbuffer()
|
||||
if video.rboID != 0 {
|
||||
deleteRenderbuffers(1, &video.rboID)
|
||||
video.rboID = 0
|
||||
}
|
||||
if video.fboID != 0 {
|
||||
deleteFramebuffers(1, &video.fboID)
|
||||
video.fboID = 0
|
||||
}
|
||||
}
|
||||
+7
-5
@@ -124,11 +124,7 @@ func (video *Video) ScissorEnd() {
|
||||
gl.Disable(gl.SCISSOR_TEST)
|
||||
}
|
||||
|
||||
func (video *Video) vertexArray(x, y, w, h, scale float32) []float32 {
|
||||
fbw, fbh := video.Window.GetFramebufferSize()
|
||||
ffbw := float32(fbw)
|
||||
ffbh := float32(fbh)
|
||||
|
||||
func vertexArrayForFramebuffer(x, y, w, h, scale, ffbw, ffbh float32) []float32 {
|
||||
w *= scale
|
||||
h *= scale
|
||||
|
||||
@@ -143,6 +139,11 @@ func (video *Video) vertexArray(x, y, w, h, scale float32) []float32 {
|
||||
}
|
||||
}
|
||||
|
||||
func (video *Video) vertexArray(x, y, w, h, scale float32) []float32 {
|
||||
fbw, fbh := video.Window.GetFramebufferSize()
|
||||
return vertexArrayForFramebuffer(x, y, w, h, scale, float32(fbw), float32(fbh))
|
||||
}
|
||||
|
||||
// DrawBorder draws a colored rectangle border
|
||||
func (video *Video) DrawBorder(x, y, w, h, borderWidth float32, c Color) {
|
||||
va := video.vertexArray(x, y, w, h, 1.0)
|
||||
@@ -219,6 +220,7 @@ func textureLoad(nrgba *image.NRGBA) uint32 {
|
||||
gl.RGBA,
|
||||
gl.UNSIGNED_BYTE,
|
||||
gl.Ptr(nrgba.Pix))
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
return texture
|
||||
}
|
||||
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package video
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
"github.com/go-gl/glfw/v3.4/glfw"
|
||||
)
|
||||
|
||||
var (
|
||||
glslVersion = "#version 120\n"
|
||||
genVertexArrays = gl.GenVertexArrays
|
||||
bindVertexArray = gl.BindVertexArray
|
||||
genFramebuffers = gl.GenFramebuffers
|
||||
bindFramebuffer = gl.BindFramebuffer
|
||||
deleteFramebuffers = gl.DeleteFramebuffers
|
||||
framebufferTexture2D = gl.FramebufferTexture2D
|
||||
checkFramebufferStatus = gl.CheckFramebufferStatus
|
||||
genRenderbuffers = gl.GenRenderbuffers
|
||||
bindRenderbuffer = gl.BindRenderbuffer
|
||||
deleteRenderbuffers = gl.DeleteRenderbuffers
|
||||
renderbufferStorage = gl.RenderbufferStorage
|
||||
framebufferRenderbuffer = gl.FramebufferRenderbuffer
|
||||
)
|
||||
|
||||
// The frontend and hidden core context use the same hints. Select their API
|
||||
// from the actual context capabilities, including macOS's legacy extensions.
|
||||
func initGL() error {
|
||||
if err := gl.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
window := glfw.GetCurrentContext()
|
||||
major, minor := window.GetAttrib(glfw.ContextVersionMajor), window.GetAttrib(glfw.ContextVersionMinor)
|
||||
glslVersion = "#version 120\n"
|
||||
if major > 3 || (major == 3 && minor >= 2) {
|
||||
glslVersion = "#version 150\n"
|
||||
} else if major == 3 {
|
||||
glslVersion = "#version 130\n"
|
||||
}
|
||||
|
||||
genVertexArrays, bindVertexArray = gl.GenVertexArrays, gl.BindVertexArray
|
||||
genFramebuffers, bindFramebuffer, deleteFramebuffers = gl.GenFramebuffers, gl.BindFramebuffer, gl.DeleteFramebuffers
|
||||
framebufferTexture2D, checkFramebufferStatus = gl.FramebufferTexture2D, gl.CheckFramebufferStatus
|
||||
genRenderbuffers, bindRenderbuffer, deleteRenderbuffers = gl.GenRenderbuffers, gl.BindRenderbuffer, gl.DeleteRenderbuffers
|
||||
renderbufferStorage, framebufferRenderbuffer = gl.RenderbufferStorage, gl.FramebufferRenderbuffer
|
||||
|
||||
if major < 3 && !glfw.ExtensionSupported("GL_ARB_vertex_array_object") {
|
||||
if !glfw.ExtensionSupported("GL_APPLE_vertex_array_object") {
|
||||
return errors.New("OpenGL vertex array objects are not supported")
|
||||
}
|
||||
genVertexArrays, bindVertexArray = gl.GenVertexArraysAPPLE, gl.BindVertexArrayAPPLE
|
||||
}
|
||||
if major < 3 && !glfw.ExtensionSupported("GL_ARB_framebuffer_object") {
|
||||
if !glfw.ExtensionSupported("GL_EXT_framebuffer_object") {
|
||||
return errors.New("OpenGL framebuffer objects are not supported")
|
||||
}
|
||||
genFramebuffers, bindFramebuffer, deleteFramebuffers = gl.GenFramebuffersEXT, gl.BindFramebufferEXT, gl.DeleteFramebuffersEXT
|
||||
framebufferTexture2D, checkFramebufferStatus = gl.FramebufferTexture2DEXT, gl.CheckFramebufferStatusEXT
|
||||
genRenderbuffers, bindRenderbuffer, deleteRenderbuffers = gl.GenRenderbuffersEXT, gl.BindRenderbufferEXT, gl.DeleteRenderbuffersEXT
|
||||
renderbufferStorage, framebufferRenderbuffer = gl.RenderbufferStorageEXT, gl.FramebufferRenderbufferEXT
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package video
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/libretro/ludo/libretro"
|
||||
)
|
||||
|
||||
func TestDuplicateFramePreservesDimensions(t *testing.T) {
|
||||
v := &Video{}
|
||||
v.Refresh(libretro.HWFrameBufferValid, 320, 240, 0)
|
||||
v.Refresh(nil, 0, 0, 0)
|
||||
if v.width != 320 || v.height != 240 || v.data != libretro.HWFrameBufferValid || !v.needUpload {
|
||||
t.Fatal("NULL refresh discarded the pending frame")
|
||||
}
|
||||
v.needUpload = false
|
||||
v.Refresh(nil, 640, 480, 0)
|
||||
if v.width != 320 || v.height != 240 || v.needUpload {
|
||||
t.Fatal("NULL refresh changed the previously presented frame")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextDestroyWithoutCore(t *testing.T) {
|
||||
(&Video{}).RunContextDestroy()
|
||||
}
|
||||
+31
-33
@@ -1,6 +1,7 @@
|
||||
package video
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"image"
|
||||
"image/png"
|
||||
"os"
|
||||
@@ -13,41 +14,33 @@ import (
|
||||
"github.com/libretro/ludo/state"
|
||||
)
|
||||
|
||||
// During the TakeScreenshot step, we need to render the current game frame at
|
||||
// the right resolution to later capture it using ReadPixels. renderScreenshot
|
||||
// taking care of this.
|
||||
func (video *Video) renderScreenshot() {
|
||||
va := video.vertexArray(0, 0, float32(video.Geom.BaseWidth), float32(video.Geom.BaseHeight), 1.0)
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
|
||||
gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW)
|
||||
func (video *Video) captureBackbufferScreenshot(name string) error {
|
||||
outW, outH := video.Geom.BaseWidth, video.Geom.BaseHeight
|
||||
if outW < 1 || outH < 1 {
|
||||
return errors.New("invalid screenshot output size")
|
||||
}
|
||||
|
||||
bindVertexArray(video.vao)
|
||||
x := video.presentX
|
||||
y := video.presentY
|
||||
w := video.presentW
|
||||
h := video.presentH
|
||||
if !state.CoreRunning || video.width < 1 || video.height < 1 || w < 1 || h < 1 {
|
||||
return errors.New("no game frame to capture")
|
||||
}
|
||||
|
||||
gl.BindTexture(gl.TEXTURE_2D, video.texID)
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
|
||||
var oldReadBuffer int32
|
||||
gl.GetIntegerv(gl.READ_BUFFER, &oldReadBuffer)
|
||||
gl.ReadBuffer(gl.BACK)
|
||||
|
||||
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
|
||||
}
|
||||
img := image.NewRGBA(image.Rect(0, 0, int(w), int(h)))
|
||||
gl.Finish()
|
||||
gl.ReadPixels(x, y, w, h, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(img.Pix))
|
||||
gl.ReadBuffer(uint32(oldReadBuffer))
|
||||
|
||||
// TakeScreenshot captures the ouput of video.Render and writes it to a file
|
||||
func (video *Video) TakeScreenshot(name string) error {
|
||||
state.MenuActive = false
|
||||
defer func() { state.MenuActive = true }()
|
||||
|
||||
gl.UseProgram(video.defaultProgram)
|
||||
|
||||
video.renderScreenshot()
|
||||
|
||||
img := image.NewRGBA(image.Rect(0, 0, video.Geom.BaseWidth, video.Geom.BaseHeight))
|
||||
|
||||
_, fbh := video.Window.GetFramebufferSize()
|
||||
|
||||
gl.ReadPixels(
|
||||
0, int32(fbh-video.Geom.BaseHeight),
|
||||
int32(video.Geom.BaseWidth), int32(video.Geom.BaseHeight),
|
||||
gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(img.Pix))
|
||||
|
||||
gl.UseProgram(video.program)
|
||||
flipped := imaging.FlipV(img)
|
||||
if flipped.Bounds().Dx() != outW || flipped.Bounds().Dy() != outH {
|
||||
flipped = imaging.Resize(flipped, outW, outH, imaging.Linear)
|
||||
}
|
||||
|
||||
err := os.MkdirAll(settings.Current.ScreenshotsDirectory, os.ModePerm)
|
||||
if err != nil {
|
||||
@@ -59,8 +52,13 @@ func (video *Video) TakeScreenshot(name string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
flipped := imaging.FlipV(img)
|
||||
defer fd.Close()
|
||||
|
||||
return png.Encode(fd, flipped)
|
||||
}
|
||||
|
||||
// TakeScreenshotInFrontendFrame captures a screenshot while the caller already
|
||||
// owns a frontend frame and has already rendered the current game frame.
|
||||
func (video *Video) TakeScreenshotInFrontendFrame(name string) error {
|
||||
return video.captureBackbufferScreenshot(name)
|
||||
}
|
||||
+16
-1
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
"github.com/go-gl/mathgl/mgl32"
|
||||
)
|
||||
|
||||
func newProgram(vertexShaderSource, fragmentShaderSource string) (uint32, error) {
|
||||
@@ -22,6 +23,9 @@ func newProgram(vertexShaderSource, fragmentShaderSource string) (uint32, error)
|
||||
|
||||
gl.AttachShader(program, vertexShader)
|
||||
gl.AttachShader(program, fragmentShader)
|
||||
// Game and menu programs share a VAO, so their attribute locations must match.
|
||||
gl.BindAttribLocation(program, 0, gl.Str("vert\x00"))
|
||||
gl.BindAttribLocation(program, 1, gl.Str("vertTexCoord\x00"))
|
||||
gl.LinkProgram(program)
|
||||
|
||||
var status int32
|
||||
@@ -39,13 +43,24 @@ func newProgram(vertexShaderSource, fragmentShaderSource string) (uint32, error)
|
||||
gl.DeleteShader(vertexShader)
|
||||
gl.DeleteShader(fragmentShader)
|
||||
|
||||
// Only the core rendering code uses MVPs so we set it to identity
|
||||
// to avoid issues with other parts of the program
|
||||
gl.UseProgram(program)
|
||||
mvp := gl.GetUniformLocation(program, gl.Str("MVP\x00"))
|
||||
|
||||
if mvp != -1 {
|
||||
IdentityMatrix := mgl32.Ident4()
|
||||
gl.UniformMatrix4fv(mvp, 1, false, &IdentityMatrix[0])
|
||||
}
|
||||
gl.UseProgram(0)
|
||||
|
||||
return program, nil
|
||||
}
|
||||
|
||||
func compileShader(source string, shaderType uint32) (uint32, error) {
|
||||
shader := gl.CreateShader(shaderType)
|
||||
|
||||
csources, free := gl.Strs(source)
|
||||
csources, free := gl.Strs(glslVersion + source)
|
||||
gl.ShaderSource(shader, 1, csources, nil)
|
||||
free()
|
||||
gl.CompileShader(shader)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
//go:build gltest && linux
|
||||
|
||||
package video
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"runtime"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
"github.com/go-gl/glfw/v3.4/glfw"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func TestSoftwareFrameUploadBounds(t *testing.T) {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
if err := glfw.Init(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer glfw.Terminate()
|
||||
glfw.DefaultWindowHints()
|
||||
glfw.WindowHint(glfw.Visible, glfw.False)
|
||||
w, err := glfw.CreateWindow(32, 32, "Ludo upload test", nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer w.Destroy()
|
||||
w.MakeContextCurrent()
|
||||
if err := initGL(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
v := &Video{Geom: libretro.GameGeometry{MaxWidth: 64, MaxHeight: 64}}
|
||||
v.SetPixelFormat(libretro.PixelFormatXRGB8888)
|
||||
v.resetGameTexture(1, 1)
|
||||
defer gl.DeleteTextures(1, &v.texID)
|
||||
v.program, err = newProgram(vertexShader, defaultFragmentShader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer gl.DeleteProgram(v.program)
|
||||
|
||||
const width, height, pitch = 32, 32, 32 * 4
|
||||
page := unix.Getpagesize()
|
||||
memory, err := unix.Mmap(-1, 0, page*2, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_ANON|unix.MAP_PRIVATE)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer unix.Munmap(memory)
|
||||
if err := unix.Mprotect(memory[page:], unix.PROT_NONE); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The core supplies only the current frame, ending at a protected page.
|
||||
frame := memory[page-pitch*height : page]
|
||||
for i := range frame {
|
||||
frame[i] = byte(i)
|
||||
}
|
||||
v.Refresh(unsafe.Pointer(&frame[0]), width, height, pitch)
|
||||
v.uploadTexture()
|
||||
|
||||
var texWidth, texHeight int32
|
||||
gl.GetTexLevelParameteriv(gl.TEXTURE_2D, 0, gl.TEXTURE_WIDTH, &texWidth)
|
||||
gl.GetTexLevelParameteriv(gl.TEXTURE_2D, 0, gl.TEXTURE_HEIGHT, &texHeight)
|
||||
if texWidth != width || texHeight != height {
|
||||
t.Fatalf("texture size = %dx%d, want %dx%d", texWidth, texHeight, width, height)
|
||||
}
|
||||
pixels := make([]byte, len(frame))
|
||||
gl.GetTexImage(gl.TEXTURE_2D, 0, v.pixType, v.pixFmt, gl.Ptr(pixels))
|
||||
if !bytes.Equal(pixels, frame) {
|
||||
t.Error("uploaded pixels differ from the supplied frame")
|
||||
}
|
||||
if err := gl.GetError(); err != gl.NO_ERROR {
|
||||
t.Errorf("GL error: %#x", err)
|
||||
}
|
||||
}
|
||||
+587
-60
@@ -4,12 +4,14 @@
|
||||
package video
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"unsafe"
|
||||
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
"github.com/go-gl/glfw/v3.4/glfw"
|
||||
"github.com/go-gl/mathgl/mgl32"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/state"
|
||||
@@ -17,9 +19,16 @@ import (
|
||||
|
||||
// Video holds the state of the video package
|
||||
type Video struct {
|
||||
Window *glfw.Window
|
||||
Geom libretro.GameGeometry
|
||||
Font *Font
|
||||
Window *glfw.Window
|
||||
HWWindow *glfw.Window // hidden core context; shares objects, not GL state, with Window
|
||||
Geom libretro.GameGeometry
|
||||
Font *Font
|
||||
FontSm *Font
|
||||
FontLg *Font
|
||||
title string
|
||||
|
||||
windowedX, windowedY int
|
||||
windowedWidth, windowedHeight int
|
||||
|
||||
program uint32 // current program used for the game quad
|
||||
defaultProgram uint32 // default program used for the game quad
|
||||
@@ -32,6 +41,20 @@ type Video struct {
|
||||
vao uint32 // vertex array object
|
||||
vbo uint32 // vertex buffer object
|
||||
texID uint32 // texture id
|
||||
fboID uint32 // framebuffer id for hw-render cores
|
||||
rboID uint32 // depth/stencil renderbuffer for hw-render cores
|
||||
identityMat mgl32.Mat4
|
||||
orthoMat mgl32.Mat4
|
||||
texWidth int32
|
||||
texHeight int32
|
||||
presentX int32
|
||||
presentY int32
|
||||
presentW int32
|
||||
presentH int32
|
||||
cachedFrameTexID uint32
|
||||
cachedFrameW int32
|
||||
cachedFrameH int32
|
||||
cachedFrameValid bool
|
||||
|
||||
pitch int32 // pitch set by the refresh callback
|
||||
pixFmt uint32 // format set by the environment callback
|
||||
@@ -42,21 +65,190 @@ type Video struct {
|
||||
|
||||
needUpload bool // true when the texture needs to be uploaded to the GPU
|
||||
data unsafe.Pointer
|
||||
|
||||
hwContextType uint32
|
||||
hwContextMajor int
|
||||
hwContextMinor int
|
||||
hwDebugContext bool
|
||||
hwContextConfigured bool
|
||||
hwSync bool
|
||||
}
|
||||
|
||||
// Init instanciates the video package
|
||||
func Init(fullscreen bool) *Video {
|
||||
vid := &Video{}
|
||||
vid := &Video{title: "Ludo"}
|
||||
vid.identityMat = mgl32.Ident4()
|
||||
vid.Configure(fullscreen)
|
||||
return vid
|
||||
}
|
||||
|
||||
// Reconfigure destroys and recreates the window with new attributes
|
||||
func (video *Video) Reconfigure(fullscreen bool) {
|
||||
// SetFullscreen changes the window mode or monitor without recreating GL contexts.
|
||||
func (video *Video) SetFullscreen(fullscreen bool) {
|
||||
if fullscreen {
|
||||
if video.Window.GetMonitor() == nil {
|
||||
video.windowedWidth, video.windowedHeight = video.Window.GetSize()
|
||||
// Wayland does not expose window positions.
|
||||
if glfw.GetPlatform() != glfw.PlatformWayland {
|
||||
video.windowedX, video.windowedY = video.Window.GetPos()
|
||||
}
|
||||
}
|
||||
monitor := glfw.GetMonitors()[settings.Current.VideoMonitorIndex]
|
||||
modes := monitor.GetVideoModes()
|
||||
mode := modes[len(modes)-1]
|
||||
video.Window.SetMonitor(monitor, 0, 0, mode.Width, mode.Height, mode.RefreshRate)
|
||||
} else if video.Window.GetMonitor() != nil {
|
||||
video.Window.SetMonitor(nil, video.windowedX, video.windowedY,
|
||||
video.windowedWidth, video.windowedHeight, glfw.DontCare)
|
||||
}
|
||||
}
|
||||
|
||||
// ReconfigureHWContext recreates the visible window using the currently stored
|
||||
// HW context hints while preserving the current window size when windowed.
|
||||
func (video *Video) ReconfigureHWContext(fullscreen bool) error {
|
||||
width := 384 * 2
|
||||
height := 240 * 2
|
||||
if video.Window != nil && !fullscreen {
|
||||
width, height = video.Window.GetSize()
|
||||
if width < 1 {
|
||||
width = 384 * 2
|
||||
}
|
||||
if height < 1 {
|
||||
height = 240 * 2
|
||||
}
|
||||
}
|
||||
return video.configureWithSize(width, height, fullscreen)
|
||||
}
|
||||
|
||||
// CreateSharedHWContext creates a hidden context shared with the visible frontend context.
|
||||
func (video *Video) CreateSharedHWContext() error {
|
||||
if video.HWWindow != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
video.configureWindowHints()
|
||||
glfw.WindowHint(glfw.Visible, glfw.False)
|
||||
glfw.WindowHint(glfw.Focused, glfw.False)
|
||||
|
||||
width, height := video.Window.GetSize()
|
||||
if width < 1 {
|
||||
width = 1
|
||||
}
|
||||
if height < 1 {
|
||||
height = 1
|
||||
}
|
||||
|
||||
hwWindow, err := glfw.CreateWindow(width, height, "", nil, video.Window)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
video.HWWindow = hwWindow
|
||||
major := video.Window.GetAttrib(glfw.ContextVersionMajor)
|
||||
minor := video.Window.GetAttrib(glfw.ContextVersionMinor)
|
||||
video.hwSync = major > 3 || (major == 3 && minor >= 2) || glfw.ExtensionSupported("GL_ARB_sync")
|
||||
return nil
|
||||
}
|
||||
|
||||
// DestroySharedHWContext destroys the hidden shared context if it exists.
|
||||
func (video *Video) DestroySharedHWContext() {
|
||||
if video.HWWindow != nil {
|
||||
video.BeginCore()
|
||||
video.destroyFramebuffer()
|
||||
libretro.SetCurrentFramebufferValue(0)
|
||||
video.EndCore()
|
||||
video.HWWindow.Destroy()
|
||||
video.HWWindow = nil
|
||||
}
|
||||
}
|
||||
|
||||
// MakeFrontendContextCurrent switches back to the visible frontend context.
|
||||
func (video *Video) MakeFrontendContextCurrent() {
|
||||
if video.Window != nil {
|
||||
video.Window.Destroy()
|
||||
video.Window.MakeContextCurrent()
|
||||
}
|
||||
}
|
||||
|
||||
// RunContextDestroy notifies the active core before its GL context is destroyed.
|
||||
func (video *Video) RunContextDestroy() {
|
||||
if !state.CoreRunning || state.Core == nil || video.HWWindow == nil {
|
||||
return
|
||||
}
|
||||
hw := state.Core.HWRenderCallback
|
||||
if hw != nil && hw.ContextDestroy != nil {
|
||||
video.BeginCore()
|
||||
hw.ContextDestroy()
|
||||
video.EndCore()
|
||||
}
|
||||
}
|
||||
|
||||
// ResetHWContext creates the core's render target and resets its renderer.
|
||||
func (video *Video) ResetHWContext() error {
|
||||
if err := video.CreateSharedHWContext(); err != nil {
|
||||
return err
|
||||
}
|
||||
video.BeginCore()
|
||||
defer video.EndCore()
|
||||
video.InitFramebuffer()
|
||||
state.Core.HWRenderCallback.ContextReset()
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeginCore enters the isolated context for run, reset, serialization or teardown.
|
||||
func (video *Video) BeginCore() {
|
||||
if video.HWWindow != nil {
|
||||
video.switchContext(video.HWWindow)
|
||||
}
|
||||
}
|
||||
|
||||
// EndCore returns to the frontend after the core's GL work.
|
||||
func (video *Video) EndCore() {
|
||||
if video.HWWindow != nil {
|
||||
video.switchContext(video.Window)
|
||||
}
|
||||
}
|
||||
|
||||
// ContextCallbacks returns paired release/acquire callbacks for synchronous
|
||||
// context handoffs between the frontend and native core threads.
|
||||
func (video *Video) ContextCallbacks() (release, acquire func()) {
|
||||
var current *glfw.Window
|
||||
release = func() {
|
||||
current = nil
|
||||
if video.Window == nil || state.Core == nil || state.Core.HWRenderCallback == nil {
|
||||
return
|
||||
}
|
||||
current = glfw.GetCurrentContext()
|
||||
// Before context_reset, HW negotiation may still use the visible context.
|
||||
// Once the hidden context exists, leave the menu context on its own thread.
|
||||
if video.HWWindow != nil && current != video.HWWindow {
|
||||
current = nil
|
||||
}
|
||||
if current != nil {
|
||||
glfw.DetachCurrentContext()
|
||||
}
|
||||
}
|
||||
acquire = func() {
|
||||
if current != nil {
|
||||
current.MakeContextCurrent()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (video *Video) switchContext(window *glfw.Window) {
|
||||
if glfw.GetCurrentContext() == window {
|
||||
return
|
||||
}
|
||||
// Order shared-texture writes and reads in both directions without a CPU wait.
|
||||
if video.hwSync {
|
||||
fence := gl.FenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0)
|
||||
gl.Flush()
|
||||
window.MakeContextCurrent()
|
||||
gl.WaitSync(fence, 0, gl.TIMEOUT_IGNORED)
|
||||
gl.DeleteSync(fence)
|
||||
} else {
|
||||
gl.Finish()
|
||||
window.MakeContextCurrent()
|
||||
}
|
||||
video.Configure(fullscreen)
|
||||
}
|
||||
|
||||
// GetFramebufferSize retrieves the size, in pixels, of the framebuffer of the specified window.
|
||||
@@ -69,12 +261,69 @@ func (video *Video) GetFramebufferSize() (int, int) {
|
||||
|
||||
// SetTitle sets the window title, encoded as UTF-8, of the window.
|
||||
func (video *Video) SetTitle(title string) {
|
||||
video.title = title
|
||||
if video.Window == nil {
|
||||
return
|
||||
}
|
||||
video.Window.SetTitle(title)
|
||||
}
|
||||
|
||||
// SetHWRenderContext stores the context requested by the current libretro core.
|
||||
func (video *Video) SetHWRenderContext(hw *libretro.HWRenderCallback) {
|
||||
if hw == nil {
|
||||
video.hwContextType = 0
|
||||
video.hwContextMajor = 0
|
||||
video.hwContextMinor = 0
|
||||
video.hwDebugContext = false
|
||||
video.hwContextConfigured = false
|
||||
return
|
||||
}
|
||||
|
||||
video.hwContextType = hw.HWContextType
|
||||
video.hwContextMajor = int(hw.VersionMajor)
|
||||
video.hwContextMinor = int(hw.VersionMinor)
|
||||
video.hwDebugContext = hw.DebugContext
|
||||
video.hwContextConfigured = true
|
||||
}
|
||||
|
||||
func (video *Video) configureWindowHints() {
|
||||
glfw.DefaultWindowHints()
|
||||
|
||||
if !video.hwContextConfigured {
|
||||
return
|
||||
}
|
||||
|
||||
switch video.hwContextType {
|
||||
case libretro.HWContextOpenGLCore:
|
||||
major := video.hwContextMajor
|
||||
minor := video.hwContextMinor
|
||||
glfw.WindowHint(glfw.ClientAPI, glfw.OpenGLAPI)
|
||||
glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLAnyProfile)
|
||||
if major > 0 {
|
||||
glfw.WindowHint(glfw.ContextVersionMajor, major)
|
||||
glfw.WindowHint(glfw.ContextVersionMinor, minor)
|
||||
if major > 3 || (major == 3 && minor >= 2) {
|
||||
glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
|
||||
glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True)
|
||||
}
|
||||
}
|
||||
case libretro.HWContextOpenGL:
|
||||
glfw.WindowHint(glfw.ClientAPI, glfw.OpenGLAPI)
|
||||
if video.hwContextMajor > 0 {
|
||||
glfw.WindowHint(glfw.ContextVersionMajor, video.hwContextMajor)
|
||||
glfw.WindowHint(glfw.ContextVersionMinor, video.hwContextMinor)
|
||||
if video.hwContextMajor > 3 || (video.hwContextMajor == 3 && video.hwContextMinor >= 2) {
|
||||
glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCompatProfile)
|
||||
glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.False)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if video.hwDebugContext {
|
||||
glfw.WindowHint(glfw.OpenGLDebugContext, glfw.True)
|
||||
}
|
||||
}
|
||||
|
||||
// SetShouldClose sets the value of the close flag of the window.
|
||||
func (video *Video) SetShouldClose(b bool) {
|
||||
if video.Window == nil {
|
||||
@@ -90,37 +339,85 @@ func panicOnErr(v uint32, err error) uint32 {
|
||||
return v
|
||||
}
|
||||
|
||||
func (video *Video) ensurePixelFormatDefaults() {
|
||||
if video.pixFmt == 0 {
|
||||
video.pixFmt = gl.UNSIGNED_SHORT_5_5_5_1
|
||||
video.pixType = gl.BGRA
|
||||
video.bpp = 2
|
||||
}
|
||||
}
|
||||
|
||||
func (video *Video) resetGameTexture(width, height int32) {
|
||||
if width < 1 {
|
||||
width = 1
|
||||
}
|
||||
if height < 1 {
|
||||
height = 1
|
||||
}
|
||||
|
||||
if video.texID != 0 {
|
||||
gl.DeleteTextures(1, &video.texID)
|
||||
video.texID = 0
|
||||
}
|
||||
|
||||
gl.GenTextures(1, &video.texID)
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, video.texID)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
|
||||
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, video.pixType, video.pixFmt, nil)
|
||||
video.texWidth = width
|
||||
video.texHeight = height
|
||||
}
|
||||
|
||||
// Configure instanciates the video package
|
||||
func (video *Video) Configure(fullscreen bool) {
|
||||
var width, height int
|
||||
var m *glfw.Monitor
|
||||
|
||||
if fullscreen {
|
||||
m = glfw.GetMonitors()[settings.Current.VideoMonitorIndex]
|
||||
vms := m.GetVideoModes()
|
||||
vm := vms[len(vms)-1]
|
||||
width = vm.Width
|
||||
height = vm.Height
|
||||
} else {
|
||||
width = 384 * 2
|
||||
height = 240 * 2
|
||||
}
|
||||
|
||||
var err error
|
||||
video.Window, err = glfw.CreateWindow(width, height, "Ludo", m, nil)
|
||||
if err != nil {
|
||||
if err := video.configureWithSize(384*2, 240*2, fullscreen); err != nil {
|
||||
panic("Window creation failed:" + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (video *Video) configureWithSize(width, height int, fullscreen bool) error {
|
||||
if fullscreen && video.windowedWidth != 0 {
|
||||
width, height = video.windowedWidth, video.windowedHeight
|
||||
}
|
||||
video.configureWindowHints()
|
||||
window, err := glfw.CreateWindow(width, height, video.title, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// GLFW may report a platform error without returning it through the Go binding.
|
||||
if window == nil {
|
||||
return errors.New("window creation failed")
|
||||
}
|
||||
|
||||
// Keep the old context and its resources until the replacement exists.
|
||||
if video.Window != nil {
|
||||
video.RunContextDestroy()
|
||||
video.DestroySharedHWContext()
|
||||
video.Window.Destroy()
|
||||
}
|
||||
video.Window = window
|
||||
|
||||
// Object names from the old share group must not be reused in this context.
|
||||
video.texID = 0
|
||||
video.cachedFrameTexID = 0
|
||||
video.cachedFrameW, video.cachedFrameH = 0, 0
|
||||
video.cachedFrameValid = false
|
||||
video.width, video.height = 0, 0
|
||||
video.data = nil
|
||||
video.needUpload = false
|
||||
video.Window.MakeContextCurrent()
|
||||
|
||||
// Force a minimum size for the window.
|
||||
video.Window.SetSizeLimits(160, 120, glfw.DontCare, glfw.DontCare)
|
||||
video.SetFullscreen(fullscreen)
|
||||
|
||||
video.Window.SetInputMode(glfw.CursorMode, glfw.CursorHidden)
|
||||
|
||||
// Initialize Glow
|
||||
if err := gl.Init(); err != nil {
|
||||
if err := initGL(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -128,7 +425,15 @@ func (video *Video) Configure(fullscreen bool) {
|
||||
|
||||
// LoadFont (fontfile, font scale, window width, window height)
|
||||
fontPath := filepath.Join(settings.Current.AssetsDirectory, "font.ttf")
|
||||
video.Font, err = LoadFont(fontPath, int32(36*2), fbw, fbh)
|
||||
video.Font, err = LoadFont(fontPath, int32(36), fbw, fbh)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
video.FontSm, err = LoadFont(fontPath, int32(29), fbw, fbh)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
video.FontLg, err = LoadFont(fontPath, int32(50), fbw, fbh)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -142,8 +447,9 @@ func (video *Video) Configure(fullscreen bool) {
|
||||
video.borderProgram = panicOnErr(newProgram(vertexShader, borderFragmentShader))
|
||||
video.circleProgram = panicOnErr(newProgram(vertexShader, circleFragmentShader))
|
||||
|
||||
video.UpdateFilter(settings.Current.VideoFilter)
|
||||
video.program = video.defaultProgram
|
||||
|
||||
gl.UseProgram(video.program)
|
||||
textureUniform := gl.GetUniformLocation(video.program, gl.Str("Texture\x00"))
|
||||
gl.Uniform1i(textureUniform, 0)
|
||||
|
||||
@@ -164,28 +470,33 @@ func (video *Video) Configure(fullscreen bool) {
|
||||
gl.VertexAttribPointerWithOffset(texCoordAttrib, 2, gl.FLOAT, false, 4*4, 2*4)
|
||||
|
||||
// Some cores won't call SetPixelFormat, provide default values
|
||||
if video.pixFmt == 0 {
|
||||
video.pixFmt = gl.UNSIGNED_SHORT_5_5_5_1
|
||||
video.pixType = gl.BGRA
|
||||
video.bpp = 2
|
||||
video.ensurePixelFormatDefaults()
|
||||
|
||||
if video.Geom.MaxWidth == 0 || video.Geom.MaxHeight == 0 {
|
||||
video.Geom.MaxWidth = video.Geom.BaseWidth
|
||||
video.Geom.MaxHeight = video.Geom.BaseHeight
|
||||
}
|
||||
|
||||
gl.GenTextures(1, &video.texID)
|
||||
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
if video.texID == 0 && state.Verbose {
|
||||
log.Println("[Video]: Failed to create the vid texture")
|
||||
video.texWidth = int32(video.Geom.MaxWidth)
|
||||
video.texHeight = int32(video.Geom.MaxHeight)
|
||||
if video.texWidth < 1 {
|
||||
video.texWidth = 1
|
||||
}
|
||||
|
||||
gl.BindTexture(gl.TEXTURE_2D, video.texID)
|
||||
if video.texHeight < 1 {
|
||||
video.texHeight = 1
|
||||
}
|
||||
video.resetGameTexture(video.texWidth, video.texHeight)
|
||||
|
||||
video.UpdateFilter(settings.Current.VideoFilter)
|
||||
|
||||
video.coreRatioViewport(fbw, fbh)
|
||||
video.coreRatioViewport(fbw, fbh, video.Geom.BaseWidth, video.Geom.BaseHeight)
|
||||
|
||||
if e := gl.GetError(); e != gl.NO_ERROR {
|
||||
bindVertexArray(0)
|
||||
|
||||
for e := gl.GetError(); e != gl.NO_ERROR; e = gl.NO_ERROR {
|
||||
log.Printf("[Video] OpenGL error: %d\n", e)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateFilter configures the game texture filter and shader. We currently
|
||||
@@ -196,6 +507,7 @@ func (video *Video) Configure(fullscreen bool) {
|
||||
// CRT: zfast-crt
|
||||
// LCD: zfast-lcd
|
||||
func (video *Video) UpdateFilter(filter string) {
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, video.texID)
|
||||
switch filter {
|
||||
case "Smooth":
|
||||
@@ -224,8 +536,9 @@ func (video *Video) UpdateFilter(filter string) {
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
|
||||
gl.UseProgram(video.program)
|
||||
gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("TextureSize\x00")), float32(video.width), float32(video.height))
|
||||
gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("TextureSize\x00")), float32(video.texWidth), float32(video.texHeight))
|
||||
gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("InputSize\x00")), float32(video.width), float32(video.height))
|
||||
gl.UseProgram(0)
|
||||
}
|
||||
|
||||
// SetPixelFormat is a callback passed to the libretro implementation.
|
||||
@@ -273,9 +586,38 @@ func (video *Video) ResetRot() {
|
||||
video.rot = 0
|
||||
}
|
||||
|
||||
// coreRatioViewport configures the vertex array to display the game at the center of the window
|
||||
// while preserving the original ascpect ratio of the game or core
|
||||
func (video *Video) coreRatioViewport(fbWidth int, fbHeight int) (x, y, w, h float32) {
|
||||
// ResetCoreState clears per-core video state and destroys its GL context.
|
||||
func (video *Video) ResetCoreState() {
|
||||
video.DestroySharedHWContext()
|
||||
video.MakeFrontendContextCurrent()
|
||||
libretro.SetCurrentFramebufferValue(0)
|
||||
video.ResetPitch()
|
||||
video.ResetRot()
|
||||
video.needUpload = false
|
||||
video.data = nil
|
||||
video.width = 0
|
||||
video.height = 0
|
||||
video.pixFmt = 0
|
||||
video.pixType = 0
|
||||
video.bpp = 0
|
||||
video.ensurePixelFormatDefaults()
|
||||
if video.texID != 0 {
|
||||
video.resetGameTexture(1, 1)
|
||||
video.UpdateFilter(settings.Current.VideoFilter)
|
||||
}
|
||||
video.texWidth = 0
|
||||
video.texHeight = 0
|
||||
if video.cachedFrameTexID != 0 {
|
||||
gl.DeleteTextures(1, &video.cachedFrameTexID)
|
||||
video.cachedFrameTexID = 0
|
||||
}
|
||||
video.cachedFrameW = 0
|
||||
video.cachedFrameH = 0
|
||||
video.cachedFrameValid = false
|
||||
}
|
||||
|
||||
// gameViewport fits the game to the framebuffer and records its screenshot bounds.
|
||||
func (video *Video) gameViewport(fbWidth, fbHeight int) (x, y, w, h float32) {
|
||||
// Scale the content to fit in the viewport.
|
||||
fbw := float32(fbWidth)
|
||||
fbh := float32(fbHeight)
|
||||
@@ -293,11 +635,36 @@ func (video *Video) coreRatioViewport(fbWidth int, fbHeight int) (x, y, w, h flo
|
||||
w = fbw
|
||||
}
|
||||
|
||||
// Place the content in the middle of the window.
|
||||
x = (fbw - w) / 2
|
||||
y = (fbh - h) / 2
|
||||
// Keep centered, pixel-aligned bounds so the cached copy needs no resampling.
|
||||
x = float32(int32((fbw - w) / 2))
|
||||
y = float32(int32((fbh - h) / 2))
|
||||
w = fbw - 2*x
|
||||
h = fbh - 2*y
|
||||
|
||||
video.presentX = int32(x)
|
||||
video.presentY = int32(y)
|
||||
video.presentW = int32(w)
|
||||
video.presentH = int32(h)
|
||||
return
|
||||
}
|
||||
|
||||
// coreRatioViewport configures the game quad's placement and core texture coordinates.
|
||||
func (video *Video) coreRatioViewport(fbWidth, fbHeight, clipWidth, clipHeight int) (x, y, w, h float32) {
|
||||
x, y, w, h = video.gameViewport(fbWidth, fbHeight)
|
||||
va := vertexArrayForFramebuffer(x, y, w, h, 1.0, float32(fbWidth), float32(fbHeight))
|
||||
|
||||
if clipWidth == 0 {
|
||||
clipWidth = int(video.texWidth)
|
||||
}
|
||||
if clipHeight == 0 {
|
||||
clipHeight = int(video.texHeight)
|
||||
}
|
||||
|
||||
va[3] = float32(clipHeight) / float32(video.texHeight)
|
||||
va[10] = float32(clipWidth) / float32(video.texWidth)
|
||||
va[11] = va[3]
|
||||
va[14] = va[10]
|
||||
|
||||
va := video.vertexArray(x, y, w, h, 1.0)
|
||||
va = rotateUV(va, video.rot)
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
|
||||
gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW)
|
||||
@@ -305,51 +672,194 @@ func (video *Video) coreRatioViewport(fbWidth int, fbHeight int) (x, y, w, h flo
|
||||
return
|
||||
}
|
||||
|
||||
// BeginFrontendFrame sets the state used by Ludo's game quad and menu.
|
||||
// Core GL state lives in HWWindow and is never modified here.
|
||||
func (video *Video) BeginFrontendFrame() {
|
||||
// Render directly to the screen.
|
||||
bindBackbuffer()
|
||||
gl.DrawBuffer(gl.BACK)
|
||||
gl.ReadBuffer(gl.BACK)
|
||||
|
||||
// Establish a frontend-owned GL baseline before drawing the game quad/menu.
|
||||
gl.Disable(gl.DEPTH_TEST)
|
||||
gl.Disable(gl.CULL_FACE)
|
||||
gl.Disable(gl.DITHER)
|
||||
gl.Disable(gl.SCISSOR_TEST)
|
||||
gl.Disable(gl.STENCIL_TEST)
|
||||
gl.Disable(gl.BLEND)
|
||||
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
|
||||
gl.BlendEquation(gl.FUNC_ADD)
|
||||
gl.ColorMask(true, true, true, true)
|
||||
gl.DepthMask(true)
|
||||
|
||||
video.ResizeViewport()
|
||||
}
|
||||
|
||||
// ResizeViewport resizes the GL viewport to the framebuffer size
|
||||
func (video *Video) ResizeViewport() {
|
||||
fbw, fbh := video.Window.GetFramebufferSize()
|
||||
gl.Viewport(0, 0, int32(fbw), int32(fbh))
|
||||
if video.Font != nil {
|
||||
video.Font.UpdateResolution(fbw, fbh)
|
||||
}
|
||||
if video.FontSm != nil {
|
||||
video.FontSm.UpdateResolution(fbw, fbh)
|
||||
}
|
||||
if video.FontLg != nil {
|
||||
video.FontLg.UpdateResolution(fbw, fbh)
|
||||
}
|
||||
}
|
||||
|
||||
// Render the current frame
|
||||
// Render draws the current game frame using the frontend context/state.
|
||||
func (video *Video) Render() {
|
||||
if !state.CoreRunning {
|
||||
gl.ClearColor(1, 1, 1, 1)
|
||||
gl.Clear(gl.COLOR_BUFFER_BIT)
|
||||
return
|
||||
}
|
||||
|
||||
gl.ClearColor(0, 0, 0, 1)
|
||||
gl.Clear(gl.COLOR_BUFFER_BIT)
|
||||
|
||||
// Early return to not render the first frame of a newly loaded game with the
|
||||
// previous game pitch. A sane pitch must be set by video.Refresh first.
|
||||
if video.pitch == 0 {
|
||||
// Wait for a valid refresh, not merely an allocated HW framebuffer.
|
||||
if video.width == 0 || video.height == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
video.uploadTexture()
|
||||
|
||||
fbw, fbh := video.Window.GetFramebufferSize()
|
||||
_, _, w, h := video.coreRatioViewport(fbw, fbh)
|
||||
_, _, w, h := video.coreRatioViewport(fbw, fbh, int(video.width), int(video.height))
|
||||
|
||||
gl.UseProgram(video.program)
|
||||
if loc := gl.GetUniformLocation(video.program, gl.Str("color\x00")); loc >= 0 {
|
||||
gl.Uniform4f(loc, 1, 1, 1, 1)
|
||||
}
|
||||
gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("OutputSize\x00")), w, h)
|
||||
|
||||
bindVertexArray(video.vao)
|
||||
if state.Core.HWRenderCallback != nil {
|
||||
gl.UniformMatrix4fv(gl.GetUniformLocation(video.program, gl.Str("MVP\x00")), 1, false, &video.orthoMat[0])
|
||||
}
|
||||
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, video.texID)
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
|
||||
|
||||
bindVertexArray(video.vao)
|
||||
// Preserve opaque alpha even when the core leaves its alpha channel unset.
|
||||
gl.ColorMask(true, true, true, false)
|
||||
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
|
||||
gl.ColorMask(true, true, true, true)
|
||||
bindVertexArray(0)
|
||||
|
||||
// Reset MVP to identity to avoid menu issues
|
||||
gl.UniformMatrix4fv(gl.GetUniformLocation(video.program, gl.Str("MVP\x00")), 1, false, &video.identityMat[0])
|
||||
gl.UseProgram(0)
|
||||
}
|
||||
|
||||
func (video *Video) ensureCachedFrameTexture(width, height int32) {
|
||||
if width < 1 || height < 1 {
|
||||
return
|
||||
}
|
||||
|
||||
if video.cachedFrameTexID == 0 {
|
||||
gl.GenTextures(1, &video.cachedFrameTexID)
|
||||
}
|
||||
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, video.cachedFrameTexID)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
|
||||
|
||||
if video.cachedFrameW != width || video.cachedFrameH != height {
|
||||
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, nil)
|
||||
video.cachedFrameW = width
|
||||
video.cachedFrameH = height
|
||||
}
|
||||
}
|
||||
|
||||
// CachePresentedFrame copies the game rectangle, without borders or UI, so menu-mode
|
||||
// rendering does not depend on the core keeping its paused framebuffer alive.
|
||||
func (video *Video) CachePresentedFrame() {
|
||||
if !state.CoreRunning || video.width == 0 || video.height == 0 {
|
||||
video.cachedFrameValid = false
|
||||
return
|
||||
}
|
||||
|
||||
if video.presentW < 1 || video.presentH < 1 {
|
||||
video.cachedFrameValid = false
|
||||
return
|
||||
}
|
||||
|
||||
video.ensureCachedFrameTexture(video.presentW, video.presentH)
|
||||
if video.cachedFrameTexID == 0 {
|
||||
video.cachedFrameValid = false
|
||||
return
|
||||
}
|
||||
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, video.cachedFrameTexID)
|
||||
gl.CopyTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, video.presentX, video.presentY, video.presentW, video.presentH)
|
||||
video.cachedFrameValid = true
|
||||
}
|
||||
|
||||
// RenderCachedFrame draws the last cached frontend-presented game frame.
|
||||
func (video *Video) RenderCachedFrame() bool {
|
||||
if !video.cachedFrameValid || video.cachedFrameTexID == 0 || video.cachedFrameW < 1 || video.cachedFrameH < 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
gl.ClearColor(0, 0, 0, 1)
|
||||
gl.Clear(gl.COLOR_BUFFER_BIT)
|
||||
|
||||
fbw, fbh := video.Window.GetFramebufferSize()
|
||||
x, y, w, h := video.gameViewport(fbw, fbh)
|
||||
va := vertexArrayForFramebuffer(x, y, w, h, 1.0, float32(fbw), float32(fbh))
|
||||
// A backbuffer copy always has bottom-left origin, regardless of the core.
|
||||
va[3] = 0
|
||||
va[7] = 1
|
||||
va[11] = 0
|
||||
va[15] = 1
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
|
||||
gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW)
|
||||
|
||||
gl.UseProgram(video.defaultProgram)
|
||||
gl.Uniform4f(gl.GetUniformLocation(video.defaultProgram, gl.Str("color\x00")), 1, 1, 1, 1)
|
||||
gl.Uniform2f(gl.GetUniformLocation(video.defaultProgram, gl.Str("OutputSize\x00")), w, h)
|
||||
gl.Uniform2f(gl.GetUniformLocation(video.defaultProgram, gl.Str("TextureSize\x00")), float32(video.cachedFrameW), float32(video.cachedFrameH))
|
||||
gl.Uniform2f(gl.GetUniformLocation(video.defaultProgram, gl.Str("InputSize\x00")), float32(video.cachedFrameW), float32(video.cachedFrameH))
|
||||
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, video.cachedFrameTexID)
|
||||
bindVertexArray(video.vao)
|
||||
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
|
||||
bindVertexArray(0)
|
||||
gl.UseProgram(0)
|
||||
return true
|
||||
}
|
||||
|
||||
// RenderPresentedFrame redraws the current game image for menu-mode frontend
|
||||
// rendering, preferring the cached last-good frame when available.
|
||||
func (video *Video) RenderPresentedFrame() {
|
||||
if !video.RenderCachedFrame() {
|
||||
video.Render()
|
||||
video.CachePresentedFrame()
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh the texture framebuffer
|
||||
func (video *Video) Refresh(data unsafe.Pointer, width int32, height int32, pitch int32) {
|
||||
// NULL duplicates the last frame; its dimensions need not be supplied.
|
||||
if data == nil {
|
||||
return
|
||||
}
|
||||
video.needUpload = true
|
||||
video.width = width
|
||||
video.height = height
|
||||
video.pitch = pitch
|
||||
video.data = data // maybe need a full copy
|
||||
video.data = data
|
||||
}
|
||||
|
||||
func (video *Video) uploadTexture() {
|
||||
@@ -357,14 +867,31 @@ func (video *Video) uploadTexture() {
|
||||
return
|
||||
}
|
||||
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, video.texID)
|
||||
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, video.pitch/video.bpp)
|
||||
|
||||
if video.data != libretro.HWFrameBufferValid {
|
||||
video.texWidth = video.width
|
||||
video.texHeight = video.height
|
||||
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, video.pitch/video.bpp)
|
||||
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, video.texWidth, video.texHeight, 0, video.pixType, video.pixFmt, video.data)
|
||||
}
|
||||
|
||||
gl.UseProgram(video.program)
|
||||
gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("TextureSize\x00")), float32(video.width), float32(video.height))
|
||||
gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("TextureSize\x00")), float32(video.texWidth), float32(video.texHeight))
|
||||
gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("InputSize\x00")), float32(video.width), float32(video.height))
|
||||
gl.UseProgram(0)
|
||||
video.needUpload = false
|
||||
}
|
||||
|
||||
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, video.width, video.height, 0, video.pixType, video.pixFmt, video.data)
|
||||
// CurrentFramebuffer returns the current FBO ID
|
||||
func (video *Video) CurrentFramebuffer() uintptr {
|
||||
return uintptr(video.fboID)
|
||||
}
|
||||
|
||||
// ProcAddress returns the address of the proc from GLFW
|
||||
func (video *Video) ProcAddress(procName string) uintptr {
|
||||
return uintptr(glfw.GetProcAddress(procName))
|
||||
}
|
||||
|
||||
// SetRotation rotates the game image as requested by the core
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
// +build darwin
|
||||
|
||||
package video
|
||||
|
||||
import (
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
)
|
||||
|
||||
func genVertexArrays(n int32, arrays *uint32) {
|
||||
gl.GenVertexArraysAPPLE(n, arrays)
|
||||
}
|
||||
|
||||
func bindVertexArray(array uint32) {
|
||||
gl.BindVertexArrayAPPLE(array)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// +build !darwin
|
||||
|
||||
package video
|
||||
|
||||
import (
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
)
|
||||
|
||||
func genVertexArrays(n int32, arrays *uint32) {
|
||||
gl.GenVertexArrays(n, arrays)
|
||||
}
|
||||
|
||||
func bindVertexArray(array uint32) {
|
||||
gl.BindVertexArray(array)
|
||||
}
|
||||
Reference in new issue
Block a user