Compare commits

..
5 Commits
Author SHA1 Message Date
Jean-André Santoni d141526f8f Add back all jobs 2026-04-11 21:07:19 +02:00
Jean-André Santoni 15f82255ca Attempt to fix go vet 2026-04-11 20:47:27 +02:00
Jean-André Santoni 49e864e99b Try to go further 2026-04-11 20:31:20 +02:00
Jean-André Santoni 0da7ebd87a Try to get everythin from msys2 2026-04-11 19:41:23 +02:00
Jean-André Santoni d9df33ba7e Try msys2 2026-04-11 19:31:24 +02:00
49 changed files with 675 additions and 2396 deletions

No files matched your search

+1 -1
View File
@@ -210,7 +210,7 @@ jobs:
asset_paths: '["./Ludo-*.tar.gz*", "./*.deb"]'
build_osx_x86_64:
runs-on: macos-26-intel
runs-on: macos-15-intel
steps:
- uses: actions/checkout@v4
with:
+1 -1
View File
@@ -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-26-intel
runs-on: macos-15-intel
steps:
- uses: actions/checkout@v4
with:
+11 -12
View File
@@ -11,22 +11,21 @@ ifeq ($(OS), Linux)
BUNDLENAME = $(APP)-$(OS)-$(DISPDRIVER)-$(ARCH)-$(VERSION)
endif
CORES = ppsspp dolphin atari800 bluemsx citra swanstation fbneo fceumm flycast gambatte gearsystem genesis_plus_gx handy lutro mednafen_ngp mednafen_pce mednafen_pce_fast mednafen_pcfx mednafen_psx mednafen_saturn mednafen_supergrafx mednafen_vb mednafen_wswan mgba melonds mupen64plus_next np2kai o2em pcsx2 pcsx_rearmed picodrive pokemini prosystem snes9x stella2014 vecx virtualjaguar
CORES = 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
ifeq ($(ARCH), arm)
CORES := $(filter-out citra dolphin flycast mednafen_pcfx mednafen_saturn melonds mupen64plus_next pcsx2 ppsspp swanstation,$(CORES))
CORES := $(filter-out swanstation,$(CORES))
CORES := $(filter-out mupen64plus_next,$(CORES))
CORES := $(filter-out mednafen_pcfx,$(CORES))
CORES := $(filter-out mednafen_saturn,$(CORES))
CORES := $(filter-out melonds,$(CORES))
endif
ifeq ($(ARCH), arm64)
CORES := $(filter-out pcsx2,$(CORES))
ifeq ($(OS), Linux)
CORES := $(filter-out mupen64plus_next,$(CORES))
endif
endif
ifeq ($(OS), OSX)
CORES := $(filter-out pcsx2,$(CORES))
ifeq ($(ARCH), x86_64)
CORES := $(filter-out citra,$(CORES))
endif
CORES := $(filter-out lutro,$(CORES))
CORES := $(filter-out mednafen_saturn,$(CORES))
CORES := $(filter-out mupen64plus_next,$(CORES))
CORES := $(filter-out picodrive,$(CORES))
CORES := $(filter-out stella2014,$(CORES))
endif
DYLIBS = $(addprefix cores/, $(addsuffix _libretro.dylib,$(CORES)))
+1 -1
View File
@@ -4,7 +4,7 @@ Ludo is a work in progress libretro frontend written in go.
<img src="https://raw.githubusercontent.com/kivutar/ludo-assets/master/illustration.png" />
It supports software-rendered and desktop OpenGL libretro cores. Hardware-rendered cores may require a newer OpenGL version than the frontend itself.
It is able to launch most non GL libretro cores.
It works on OSX, Linux, Linux ARM and Windows. You can download releases [here](https://github.com/libretro/ludo/releases)
+59 -166
View File
@@ -4,13 +4,9 @@
package core
import (
"context"
"errors"
"fmt"
"io"
"log"
"os"
"path"
"path/filepath"
"strings"
@@ -23,7 +19,7 @@ import (
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/video"
"github.com/mholt/archives"
"github.com/mholt/archiver/v3"
)
var vid *video.Video
@@ -35,7 +31,6 @@ var Options *options.Options
// Call Init before calling other functions of this package.
func Init(v *video.Video) {
vid = v
libretro.SetContextCallbacks(v.ContextCallbacks())
}
// Load loads a libretro core
@@ -52,7 +47,6 @@ func Load(sofile string) error {
if err != nil {
return err
}
vid.SetHWRenderContext(nil)
state.Core.SetEnvironment(environment)
state.Core.Init()
state.Core.SetVideoRefresh(vid.Refresh)
@@ -84,134 +78,60 @@ func unarchiveGame(filename string) (string, int64, error) {
path := ""
size := int64(0)
dst := os.TempDir()
ctx := context.Background()
file, err := os.Open(filename)
if err != nil {
return path, size, err
var err error
// What a wonderful API, all this boilerplate to set an option
switch filepath.Ext(filename) {
case ".zip":
un := archiver.Zip{
OverwriteExisting: true,
}
err = un.Unarchive(filename, dst)
case ".tar":
un := archiver.Tar{
OverwriteExisting: true,
}
err = un.Unarchive(filename, dst)
case ".rar":
un := archiver.Rar{
OverwriteExisting: true,
}
err = un.Unarchive(filename, dst)
default:
// Unarchive with default option
err = archiver.Unarchive(filename, dst)
}
defer file.Close()
format, stream, err := archives.Identify(ctx, filepath.Base(filename), file)
if err != nil {
return path, size, err
}
extPriority := 0
extPrefered := map[string]int{
".cue": 1,
".m3u": 2,
".pbp": 3,
}
extPriority := 0 // current priority
// Give priority of some extensions (use upper case to be case insensitive)
extPrefered := make(map[string]int)
extPrefered[".cue"] = 1
extPrefered[".m3u"] = 2
extPrefered[".pbp"] = 3
selectGame := func(gamePath string, fsize int64) {
ext := filepath.Ext(gamePath)
err = archiver.Walk(filename, func(f archiver.File) error {
fname := f.Name()
ext := filepath.Ext(fname)
if size == 0 {
path = gamePath
size = fsize
// By default select the first file of the archive
path = filepath.Join(dst, fname)
size = f.Size()
log.Println("first file in archive:", path, size)
}
// Check if a file (based on extension) has a higher priority
priority, ok := extPrefered[strings.ToLower(ext)]
if ok && priority > extPriority {
extPriority = priority
path = gamePath
size = fsize
path = filepath.Join(dst, fname)
size = f.Size()
log.Println("find a better file in archive:", path, size)
}
}
if ex, ok := format.(archives.Extractor); ok {
err = ex.Extract(ctx, stream, func(ctx context.Context, f archives.FileInfo) error {
fname, err := archiveOutputPath(dst, f.NameInArchive)
if err != nil {
return err
}
if f.IsDir() {
return os.MkdirAll(fname, os.ModePerm)
}
if err := os.MkdirAll(filepath.Dir(fname), os.ModePerm); err != nil {
return err
}
in, err := f.Open()
if err != nil {
return err
}
defer in.Close()
mode := f.Mode().Perm()
if mode == 0 {
mode = 0644
}
out, err := os.OpenFile(fname, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, mode)
if err != nil {
return err
}
_, copyErr := io.Copy(out, in)
closeErr := out.Close()
if copyErr != nil {
return copyErr
}
if closeErr != nil {
return closeErr
}
selectGame(fname, f.Size())
return nil
})
return path, size, err
}
decomp, ok := format.(archives.Decompressor)
if !ok {
return path, size, fmt.Errorf("unsupported archive format: %T", format)
}
rc, err := decomp.OpenReader(stream)
if err != nil {
return path, size, err
}
defer rc.Close()
fname := strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename))
if fname == "" {
return path, size, errors.New("could not determine extracted filename")
}
path = filepath.Join(dst, fname)
out, err := os.Create(path)
if err != nil {
return "", 0, err
}
size, err = io.Copy(out, rc)
closeErr := out.Close()
if err != nil {
return "", 0, err
}
if closeErr != nil {
return "", 0, closeErr
}
return path, size, nil
}
func archiveOutputPath(dst, name string) (string, error) {
clean := path.Clean(strings.TrimPrefix(name, "/"))
if clean == "." || clean == "" {
return "", errors.New("invalid archive path")
}
if clean == ".." || strings.HasPrefix(clean, "../") {
return "", fmt.Errorf("unsafe archive path: %s", name)
}
fname := filepath.Join(dst, filepath.FromSlash(clean))
rel, err := filepath.Rel(dst, fname)
if err != nil {
return "", err
}
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return "", fmt.Errorf("unsafe archive path: %s", name)
}
return fname, nil
return nil
})
return path, size, err
}
// LoadGame loads a game. A core has to be loaded first.
@@ -220,15 +140,14 @@ func LoadGame(gamePath string) error {
return err
}
if state.Core == nil {
return errors.New("no core loaded")
// 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()
}
// Reload the core to fully tear down the previous game and its GL context.
if state.CoreRunning {
if err := Load(state.CorePath); err != nil {
return err
}
if state.Core == nil {
return errors.New("no core loaded")
}
si := state.Core.GetSystemInfo()
@@ -254,7 +173,7 @@ func LoadGame(gamePath string) error {
ok := state.Core.LoadGame(*gi)
if !ok {
Unload()
state.CoreRunning = false
return errors.New("failed to load the game")
}
@@ -286,56 +205,30 @@ 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 closes the game and core before destroying their GL context.
// Unload unloads a libretro core
func Unload() {
if state.Core != nil {
savefiles.SaveSRAM()
vid.RunContextDestroy()
vid.BeginCore()
if state.CoreRunning {
state.Core.UnloadGame()
}
UnloadGame()
state.Core.Deinit()
vid.EndCore()
vid.ResetCoreState()
state.GamePath = ""
state.CoreRunning = false
state.CorePath = ""
state.Core = nil
vid.SetHWRenderContext(nil)
Options = nil
}
}
// Reset resets the game using its hardware context, if any.
func Reset() {
vid.BeginCore()
defer vid.EndCore()
state.Core.Reset()
}
// Serialize captures core state with the core's GL context current.
func Serialize() ([]byte, error) {
vid.BeginCore()
defer vid.EndCore()
return state.Core.Serialize(state.Core.SerializeSize())
}
// Unserialize restores core state with the core's GL context current.
func Unserialize(data []byte) error {
vid.BeginCore()
defer vid.EndCore()
return state.Core.Unserialize(data, uint(len(data)))
// UnloadGame unloads a game.
func UnloadGame() {
if state.CoreRunning {
savefiles.SaveSRAM()
state.Core.UnloadGame()
state.GamePath = ""
state.CoreRunning = false
vid.ResetPitch()
vid.ResetRot()
}
}
// getGameInfo opens a rom and return the libretro.GameInfo needed to launch it
+7 -37
View File
@@ -1,10 +1,10 @@
package core
import (
"log"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
@@ -15,34 +15,6 @@ 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 }
@@ -229,21 +201,17 @@ func Test_coreLoadGame(t *testing.T) {
Init(&video.Video{})
var initErr error
onMainThread(func() { initErr = glfw.Init() })
if initErr != nil {
t.Fatalf("glfw.Init() error = %v", initErr)
if err := glfw.Init(); err != nil {
log.Fatalln("failed to initialize glfw")
}
t.Cleanup(func() { onMainThread(glfw.Terminate) })
t.Cleanup(glfw.Terminate)
if err := Load("testdata/vecx_libretro" + ext); err != nil {
t.Fatalf("Load() error = %v", err)
}
var loadGameErr error
got := utils.CaptureOutput(func() {
onMainThread(func() { loadGameErr = LoadGame("testdata/Polar Rescue (USA).vec") })
})
got := utils.CaptureOutput(func() { loadGameErr = LoadGame("testdata/Polar Rescue (USA).vec") })
if loadGameErr != nil {
t.Fatalf("LoadGame() error = %v", loadGameErr)
}
@@ -267,6 +235,7 @@ func Test_coreLoadGame(t *testing.T) {
}
})
UnloadGame()
Unload()
t.Run("Global state should be cleared by Unload", func(t *testing.T) {
@@ -283,6 +252,7 @@ func Test_coreLoadGame(t *testing.T) {
}
func Test_coreLoadGameWithoutCore(t *testing.T) {
UnloadGame()
Unload()
err := LoadGame("testdata/Polar Rescue (USA).vec")
-26
View File
@@ -143,30 +143,6 @@ 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:
@@ -175,8 +151,6 @@ 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:
+8 -17
View File
@@ -7,39 +7,30 @@ 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/archives v0.1.5
github.com/mholt/archiver/v3 v3.5.1
github.com/pelletier/go-toml v1.9.5
github.com/tanema/gween v0.0.0-20250522035225-e874ee3ae01a
github.com/youpy/go-wav v0.3.2
golang.org/x/image v0.41.0
golang.org/x/image v0.39.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/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/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/klauspost/pgzip v1.2.6 // indirect
github.com/mikelolasagasti/xz v1.0.1 // indirect
github.com/minio/minlz v1.0.1 // indirect
github.com/nwaples/rardecode/v2 v2.2.0 // indirect
github.com/nwaples/rardecode v1.1.3 // 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
+25 -276
View File
@@ -1,343 +1,92 @@
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.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY=
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276 h1:IO5P06Pcj9K04d+l4nrf3c2U56+dAotIFG6u4P1wAHI=
github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/mathgl v1.0.0 h1:t9DznWJlXxxjeeKLIdovCOVJQk/GzDEL7h/h+Ro2B68=
github.com/go-gl/mathgl v1.0.0/go.mod h1:yhpkQzEiH9yPyxDUGzkmgScbaBVlhC06qodikEM0ZwQ=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/kivutar/glfw/v3.4/glfw v0.0.0-20260410202638-4349e4d5a0ee h1:PuDX5smvEEVGSd2s66iGqGtmrSldeBusjnLgIi92Sl4=
github.com/kivutar/glfw/v3.4/glfw v0.0.0-20260410202638-4349e4d5a0ee/go.mod h1:T5Dn0JwIJOX1euPZ/iT4tq6nFYtmukjcYa7937HuYK8=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw=
github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/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/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/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/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.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/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/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.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/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
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=
+137 -47
View File
@@ -6,36 +6,121 @@ 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 uintptr_t s_current_framebuffer = 0;
static SEM_T s_sem_do;
static SEM_T s_sem_done;
static bool s_use_thread = false;
// 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* 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);
}
}
int cothread_init() {
return pthread_create(&s_thread, NULL, emu_thread_loop, 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");
}
void cothread_deinit() {
pthread_join(s_thread, 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)();
}
}
bool cothread_is_current() {
return pthread_equal(pthread_self(), s_thread);
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);
}
void bridge_retro_init(void *f) {
((void (*)(void))f)();
run_wrapper(f);
}
void bridge_retro_deinit(void *f) {
((void (*)(void))f)();
run_wrapper(f);
}
unsigned bridge_retro_api_version(void *f) {
@@ -46,14 +131,6 @@ 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();
}
@@ -75,7 +152,7 @@ void bridge_retro_set_environment(void *f, void *callback) {
}
void bridge_retro_set_video_refresh(void *f, void *callback) {
((void (*)(retro_video_refresh_t))f)((retro_video_refresh_t)callback);
((bool (*)(retro_video_refresh_t))f)((retro_video_refresh_t)callback);
}
void bridge_retro_set_controller_port_device(void *f, unsigned port, unsigned device) {
@@ -83,23 +160,23 @@ void bridge_retro_set_controller_port_device(void *f, unsigned port, unsigned de
}
void bridge_retro_set_input_poll(void *f, void *callback) {
((void (*)(retro_input_poll_t))f)((retro_input_poll_t)callback);
((bool (*)(retro_input_poll_t))f)((retro_input_poll_t)callback);
}
void bridge_retro_set_input_state(void *f, void *callback) {
((void (*)(retro_input_state_t))f)((retro_input_state_t)callback);
((bool (*)(retro_input_state_t))f)((retro_input_state_t)callback);
}
void bridge_retro_set_audio_sample(void *f, void *callback) {
((void (*)(retro_audio_sample_t))f)((retro_audio_sample_t)callback);
((bool (*)(retro_audio_sample_t))f)((retro_audio_sample_t)callback);
}
void bridge_retro_set_audio_sample_batch(void *f, void *callback) {
((void (*)(retro_audio_sample_batch_t))f)((retro_audio_sample_batch_t)callback);
((bool (*)(retro_audio_sample_batch_t))f)((retro_audio_sample_batch_t)callback);
}
bool bridge_retro_load_game(void *f, struct retro_game_info *gi) {
return ((bool (*)(const struct retro_game_info *))f)(gi);
return ((bool (*)(struct retro_game_info *))f)(gi);
}
size_t bridge_retro_serialize_size(void *f) {
@@ -107,23 +184,49 @@ size_t bridge_retro_serialize_size(void *f) {
}
bool bridge_retro_serialize(void *f, void *data, size_t size) {
return ((bool (*)(void*, size_t))f)(data, size);
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);
}
}
bool bridge_retro_unserialize(void *f, void *data, size_t size) {
return ((bool (*)(const void*, size_t))f)(data, 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);
}
}
void bridge_retro_unload_game(void *f) {
((void (*)(void))f)();
run_wrapper(f);
}
void bridge_retro_run(void *f) {
((void (*)(void))f)();
run_wrapper(f);
}
void bridge_retro_reset(void *f) {
((void (*)(void))f)();
run_wrapper(f);
}
size_t bridge_retro_get_memory_size(void *f, unsigned id) {
@@ -200,18 +303,5 @@ 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"
+40 -236
View File
@@ -14,9 +14,7 @@ package libretro
#include <stdio.h>
#include <string.h>
int cothread_init();
void cothread_deinit();
bool cothread_is_current();
void cothread_init();
void bridge_retro_init(void *f);
void bridge_retro_deinit(void *f);
@@ -38,8 +36,6 @@ 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);
@@ -58,15 +54,10 @@ 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"
)
@@ -228,20 +219,6 @@ 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()
@@ -435,35 +412,15 @@ 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
getCurrentFramebufferFunc func() uintptr
getProcAddressFunc func(string) uintptr
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
)
var (
@@ -475,84 +432,8 @@ 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{}
@@ -563,12 +444,7 @@ func Load(sofile string) (*Core, error) {
return nil, err
}
coreCalls, frontendCalls = make(chan func()), make(chan func())
if err := C.cothread_init(); err != 0 {
coreCalls, frontendCalls = nil, nil
DlClose(core.handle)
return nil, fmt.Errorf("start core thread: %s", C.GoString(C.strerror(err)))
}
C.cothread_init()
core.symRetroInit = DlSym(core.handle, "retro_init")
core.symRetroDeinit = DlSym(core.handle, "retro_deinit")
@@ -597,21 +473,18 @@ func Load(sofile string) (*Core, error) {
// Init takes care of the library global initialization
func (core *Core) Init() {
coreCall(func() { C.bridge_retro_init(core.symRetroInit) })
C.bridge_retro_init(core.symRetroInit)
}
// APIVersion returns the RETRO_API_VERSION.
// Used to validate ABI compatibility when the API is revised.
func (core *Core) APIVersion() uint {
return coreResult(func() uint { return uint(C.bridge_retro_api_version(core.symRetroAPIVersion)) })
return uint(C.bridge_retro_api_version(core.symRetroAPIVersion))
}
// Deinit takes care of the library global deinitialization
func (core *Core) Deinit() {
coreCall(func() { C.bridge_retro_deinit(core.symRetroDeinit) })
close(coreCalls)
C.cothread_deinit()
coreCalls, frontendCalls = nil, nil
C.bridge_retro_deinit(core.symRetroDeinit)
DlClose(core.handle)
core.MemoryMap = nil
environment = nil
@@ -622,7 +495,6 @@ func (core *Core) Deinit() {
inputState = nil
log = nil
getTimeUsec = nil
getProcAddress = nil
}
// Run runs the game for one video frame.
@@ -632,12 +504,12 @@ func (core *Core) Deinit() {
// a frame if GET_CAN_DUPE returns true.
// In this case, the video callback can take a NULL argument for data.
func (core *Core) Run() {
coreCall(func() { C.bridge_retro_run(core.symRetroRun) })
C.bridge_retro_run(core.symRetroRun)
}
// Reset resets the current game.
func (core *Core) Reset() {
coreCall(func() { C.bridge_retro_reset(core.symRetroReset) })
C.bridge_retro_reset(core.symRetroReset)
}
// GetSystemInfo returns statically known system info. Pointers provided in *info
@@ -645,7 +517,7 @@ func (core *Core) Reset() {
// Can be called at any time, even before retro_init().
func (core *Core) GetSystemInfo() SystemInfo {
rsi := C.struct_retro_system_info{}
coreCall(func() { C.bridge_retro_get_system_info(core.symRetroGetSystemInfo, &rsi) })
C.bridge_retro_get_system_info(core.symRetroGetSystemInfo, &rsi)
return SystemInfo{
LibraryName: C.GoString(rsi.library_name),
LibraryVersion: C.GoString(rsi.library_version),
@@ -663,7 +535,7 @@ func (core *Core) GetSystemInfo() SystemInfo {
// desire a particular aspect ratio.
func (core *Core) GetSystemAVInfo() SystemAVInfo {
avi := C.struct_retro_system_av_info{}
coreCall(func() { C.bridge_retro_get_system_av_info(core.symRetroGetSystemAVInfo, &avi) })
C.bridge_retro_get_system_av_info(core.symRetroGetSystemAVInfo, &avi)
return SystemAVInfo{
Geometry: GameGeometry{
AspectRatio: float64(avi.geometry.aspect_ratio),
@@ -685,7 +557,7 @@ func (core *Core) LoadGame(gi GameInfo) bool {
rgi.path = C.CString(gi.Path)
rgi.size = C.size_t(gi.Size)
rgi.data = gi.Data
return coreResult(func() bool { return bool(C.bridge_retro_load_game(core.symRetroLoadGame, &rgi)) })
return bool(C.bridge_retro_load_game(core.symRetroLoadGame, &rgi))
}
// SerializeSize returns the amount of data the implementation requires to serialize
@@ -694,18 +566,18 @@ func (core *Core) LoadGame(gi GameInfo) bool {
// returned size is never allowed to be larger than a previous returned
// value, to ensure that the frontend can allocate a save state buffer once.
func (core *Core) SerializeSize() uint {
return coreResult(func() uint { return uint(C.bridge_retro_serialize_size(core.symRetroSerializeSize)) })
return uint(C.bridge_retro_serialize_size(core.symRetroSerializeSize))
}
// Serialize serializes internal state and returns the state as a byte slice.
func (core *Core) Serialize(size uint) ([]byte, error) {
data := C.malloc(C.size_t(size))
defer C.free(data)
ok := coreResult(func() bool { return bool(C.bridge_retro_serialize(core.symRetroSerialize, data, C.size_t(size))) })
ok := bool(C.bridge_retro_serialize(core.symRetroSerialize, data, C.size_t(size)))
if !ok {
return nil, errors.New("retro_serialize failed")
}
bytes := C.GoBytes(data, C.int(size))
C.free(data)
return bytes, nil
}
@@ -717,9 +589,7 @@ func (core *Core) Unserialize(bytes []byte, size uint) error {
if uint(len(bytes)) > size {
size = uint(len(bytes))
}
ok := coreResult(func() bool {
return bool(C.bridge_retro_unserialize(core.symRetroUnserialize, unsafe.Pointer(&bytes[0]), C.size_t(size)))
})
ok := bool(C.bridge_retro_unserialize(core.symRetroUnserialize, unsafe.Pointer(&bytes[0]), C.size_t(size)))
if !ok {
return errors.New("retro_unserialize failed")
}
@@ -728,51 +598,49 @@ func (core *Core) Unserialize(bytes []byte, size uint) error {
// UnloadGame unloads a currently loaded game
func (core *Core) UnloadGame() {
coreCall(func() { C.bridge_retro_unload_game(core.symRetroUnloadGame) })
C.bridge_retro_unload_game(core.symRetroUnloadGame)
}
// SetEnvironment sets the environment callback.
// Must be called before Init
func (core *Core) SetEnvironment(f environmentFunc) {
environment = f
coreCall(func() { C.bridge_retro_set_environment(core.symRetroSetEnvironment, C.coreEnvironment_cgo) })
C.bridge_retro_set_environment(core.symRetroSetEnvironment, C.coreEnvironment_cgo)
}
// SetVideoRefresh sets the video refresh callback.
// Must be set before the first Run call
func (core *Core) SetVideoRefresh(f videoRefreshFunc) {
videoRefresh = f
coreCall(func() { C.bridge_retro_set_video_refresh(core.symRetroSetVideoRefresh, C.coreVideoRefresh_cgo) })
C.bridge_retro_set_video_refresh(core.symRetroSetVideoRefresh, C.coreVideoRefresh_cgo)
}
// SetAudioSample sets the audio sample callback.
// Must be set before the first Run call
func (core *Core) SetAudioSample(f audioSampleFunc) {
audioSample = f
coreCall(func() { C.bridge_retro_set_audio_sample(core.symRetroSetAudioSample, C.coreAudioSample_cgo) })
C.bridge_retro_set_audio_sample(core.symRetroSetAudioSample, C.coreAudioSample_cgo)
}
// SetAudioSampleBatch sets the audio sample batch callback.
// Must be set before the first Run call
func (core *Core) SetAudioSampleBatch(f audioSampleBatchFunc) {
audioSampleBatch = f
coreCall(func() {
C.bridge_retro_set_audio_sample_batch(core.symRetroSetAudioSampleBatch, C.coreAudioSampleBatch_cgo)
})
C.bridge_retro_set_audio_sample_batch(core.symRetroSetAudioSampleBatch, C.coreAudioSampleBatch_cgo)
}
// SetInputPoll sets the input poll callback.
// Must be set before the first Run call
func (core *Core) SetInputPoll(f inputPollFunc) {
inputPoll = f
coreCall(func() { C.bridge_retro_set_input_poll(core.symRetroSetInputPoll, C.coreInputPoll_cgo) })
C.bridge_retro_set_input_poll(core.symRetroSetInputPoll, C.coreInputPoll_cgo)
}
// SetInputState sets the input state callback.
// Must be set before the first Run call
func (core *Core) SetInputState(f inputStateFunc) {
inputState = f
coreCall(func() { C.bridge_retro_set_input_state(core.symRetroSetInputState, C.coreInputState_cgo) })
C.bridge_retro_set_input_state(core.symRetroSetInputState, C.coreInputState_cgo)
}
// BindLogCallback binds f to the log callback
@@ -791,9 +659,7 @@ func (core *Core) BindPerfCallback(data unsafe.Pointer, f getTimeUsecFunc) {
// SetControllerPortDevice sets the device type attached to a controller port
func (core *Core) SetControllerPortDevice(port uint, device uint32) {
coreCall(func() {
C.bridge_retro_set_controller_port_device(core.symRetroSetControllerPortDevice, C.unsigned(port), C.unsigned(device))
})
C.bridge_retro_set_controller_port_device(core.symRetroSetControllerPortDevice, C.unsigned(port), C.unsigned(device))
}
//export coreEnvironment
@@ -801,12 +667,6 @@ func coreEnvironment(cmd C.unsigned, data unsafe.Pointer) bool {
if environment == nil {
return false
}
// Only window operations need the frontend thread. Routine queries stay here.
if cmd == C.RETRO_ENVIRONMENT_SET_HW_RENDER || cmd == C.RETRO_ENVIRONMENT_SHUTDOWN {
var result bool
frontendCall(func() { result = environment(uint32(cmd), data) })
return result
}
return environment(uint32(cmd), data)
}
@@ -831,12 +691,6 @@ func coreInputState(port C.unsigned, device C.unsigned, index C.unsigned, id C.u
if inputState == nil {
return 0
}
// Mouse state is queried through GLFW rather than the polled input snapshot.
if device == C.RETRO_DEVICE_MOUSE {
var result C.int16_t
frontendCall(func() { result = C.int16_t(inputState(uint(port), uint32(device), uint(index), uint(id))) })
return result
}
return C.int16_t(inputState(uint(port), uint32(device), uint(index), uint(id)))
}
@@ -872,16 +726,6 @@ 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))
@@ -1039,58 +883,20 @@ func (core *Core) SetFrameTimeCallback(data unsafe.Pointer) {
ftc := &FrameTimeCallback{}
ftc.Reference = int64(c.reference)
ftc.Callback = func(usec int64) {
coreCall(func() { C.bridge_retro_frame_time_callback(c.callback, C.retro_usec_t(usec)) })
C.bridge_retro_frame_time_callback(c.callback, C.retro_usec_t(usec))
}
core.FrameTimeCallback = ftc
}
// 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() {
coreCall(func() { C.bridge_retro_audio_callback(c.callback) })
C.bridge_retro_audio_callback(c.callback)
}
auc.SetState = func(state bool) {
coreCall(func() { C.bridge_retro_audio_set_state(c.set_state, C.bool(state)) })
C.bridge_retro_audio_set_state(c.set_state, C.bool(state))
}
core.AudioCallback = auc
}
@@ -1098,15 +904,13 @@ func (core *Core) SetAudioCallback(data unsafe.Pointer) {
// GetMemorySize returns the size of a region of the memory.
// See memory constants.
func (core *Core) GetMemorySize(id uint32) uint {
return coreResult(func() uint { return uint(C.bridge_retro_get_memory_size(core.symRetroGetMemorySize, C.unsigned(id))) })
return uint(C.bridge_retro_get_memory_size(core.symRetroGetMemorySize, C.unsigned(id)))
}
// GetMemoryData returns the size of a region of the memory.
// See memory constants.
func (core *Core) GetMemoryData(id uint32) unsafe.Pointer {
return coreResult(func() unsafe.Pointer {
return C.bridge_retro_get_memory_data(core.symRetroGetMemoryData, C.unsigned(id))
})
return C.bridge_retro_get_memory_data(core.symRetroGetMemoryData, C.unsigned(id))
}
// DiskControlCallback is an interface which frontend can use to eject and insert disk images
@@ -1126,19 +930,19 @@ func (core *Core) SetDiskControlCallback(data unsafe.Pointer) {
c := *(*C.struct_retro_disk_control_callback)(data)
dcc := &DiskControlCallback{}
dcc.SetEjectState = func(state bool) {
coreCall(func() { C.bridge_retro_set_eject_state(c.set_eject_state, C.bool(state)) })
C.bridge_retro_set_eject_state(c.set_eject_state, C.bool(state))
}
dcc.GetEjectState = func() bool {
return coreResult(func() bool { return bool(C.bridge_retro_get_eject_state(c.get_eject_state)) })
return bool(C.bridge_retro_get_eject_state(c.get_eject_state))
}
dcc.GetImageIndex = func() uint {
return coreResult(func() uint { return uint(C.bridge_retro_get_image_index(c.get_image_index)) })
return uint(C.bridge_retro_get_image_index(c.get_image_index))
}
dcc.SetImageIndex = func(index uint) {
coreCall(func() { C.bridge_retro_set_image_index(c.set_image_index, C.uint(index)) })
C.bridge_retro_set_image_index(c.set_image_index, C.uint(index))
}
dcc.GetNumImages = func() uint {
return coreResult(func() uint { return uint(C.bridge_retro_get_num_images(c.get_num_images)) })
return uint(C.bridge_retro_get_num_images(c.get_num_images))
}
core.DiskControlCallback = dcc
}
-1
View File
@@ -31,7 +31,6 @@ type Core struct {
AudioCallback *AudioCallback
FrameTimeCallback *FrameTimeCallback
DiskControlCallback *DiskControlCallback
HWRenderCallback *HWRenderCallback
MemoryMap []MemoryDescriptor
}
+2 -2
View File
@@ -28,8 +28,8 @@ func DlOpen(path string) (DlHandle, error) {
defer C.free(unsafe.Pointer(cpath))
h := C.dlopen(cpath, C.RTLD_LAZY|C.RTLD_GLOBAL)
if h == nil {
cerr := C.dlerror()
cerr := C.dlerror()
if h == nil || cerr != nil {
err := C.GoString(cerr)
return nil, errors.New(err)
}
+16 -41
View File
@@ -30,22 +30,6 @@ func init() {
var frame = 0
func fulfillPendingScreenshot(vid *video.Video) {
if state.PendingScreenshotName == "" {
return
}
name := state.PendingScreenshotName
done := state.PendingScreenshotDone
state.PendingScreenshotName = ""
state.PendingScreenshotDone = nil
err := vid.TakeScreenshotInFrontendFrame(name)
if done != nil {
done(err)
}
}
func runLoop(vid *video.Video, m *menu.Menu) {
var currTime time.Time
prevTime := time.Now()
@@ -55,19 +39,11 @@ func runLoop(vid *video.Video, m *menu.Menu) {
glfw.PollEvents()
m.ProcessHotkeys()
ntf.Process(dt)
vid.ResizeViewport()
m.UpdatePalette()
input.Poll()
menuActive := state.MenuActive
if menuActive {
m.Update(dt)
}
if state.MenuContextResetNeeded {
m.ContextReset()
state.MenuContextResetNeeded = false
}
if !menuActive {
if !state.MenuActive {
if state.CoreRunning {
vid.BeginCore()
state.Core.Run()
if state.Core.FrameTimeCallback != nil {
state.Core.FrameTimeCallback.Callback(state.Core.FrameTimeCallback.Reference)
@@ -75,23 +51,18 @@ func runLoop(vid *video.Video, m *menu.Menu) {
if state.Core.AudioCallback != nil {
state.Core.AudioCallback.Callback()
}
vid.EndCore()
}
vid.BeginFrontendFrame()
vid.Render()
vid.CachePresentedFrame()
m.RenderNotifications()
frame++
if frame%600 == 0 { // save sram about every 10 sec
savefiles.SaveSRAM()
}
} else {
vid.BeginFrontendFrame()
vid.RenderPresentedFrame()
fulfillPendingScreenshot(vid)
m.Update(dt)
vid.Render()
m.Render(dt)
m.RenderNotifications()
}
m.RenderNotifications()
if state.FastForward {
glfw.SwapInterval(0)
} else {
@@ -157,14 +128,18 @@ func main() {
input.Init(vid)
if len(state.CorePath) > 0 {
if err := core.Load(state.CorePath); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else if len(gamePath) > 0 {
if err := core.LoadGame(gamePath); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
m.WarpToQuickMenu()
err := core.Load(state.CorePath)
if err == nil {
if len(gamePath) > 0 {
err := core.LoadGame(gamePath)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
m.WarpToQuickMenu()
}
}
} else {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
}
+4 -4
View File
@@ -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, menu.ratio, label)
*stack += menu.Font.Width(menu.ratio, label)
menu.Font.Print(*stack, float32(h)-30*menu.ratio, 0.5*menu.ratio, label)
*stack += menu.Font.Width(0.5*menu.ratio, label)
*stack += 32 * menu.ratio
}
// Used to easily compose different hint bars based on the context.
func stackHintRight(stack *float32, icon uint32, label string, h int) {
*stack -= menu.Font.Width(menu.ratio, label)
*stack -= menu.Font.Width(0.5*menu.ratio, label)
menu.Font.SetColor(hintTextColor)
menu.Font.Print(*stack, float32(h)-30*menu.ratio, menu.ratio, label)
menu.Font.Print(*stack, float32(h)-30*menu.ratio, 0.5*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
+3 -2
View File
@@ -143,7 +143,7 @@ func genericInput(list *entry, dt float32) {
// indexed allows jumping directly to the next letter in playlists
func indexed(list *entry, offset int) int {
curr := firstRune(list.children[list.ptr].label)
curr := list.children[list.ptr].label[0]
for i, t := range list.indexes {
if curr == t.Char {
if i+offset < 0 {
@@ -196,7 +196,8 @@ func (m *Menu) ProcessHotkeys() {
// Toggle fullscreen if ActionFullscreenToggle is released
if input.Released[0][input.ActionFullscreenToggle] == 1 {
settings.Current.VideoFullscreen = !settings.Current.VideoFullscreen
m.SetFullscreen(settings.Current.VideoFullscreen)
m.Reconfigure(settings.Current.VideoFullscreen)
m.ContextReset()
err := settings.Save()
if err != nil {
ntf.DisplayAndLogf(ntf.Error, "Menu", "Error saving settings: %v", err)
+4 -6
View File
@@ -102,12 +102,10 @@ func (m *Menu) ContextReset() {
m.icons[filename] = video.NewImage(path)
}
// Every retained scene's texture names belong to the old context.
for _, scene := range m.stack {
list := scene.Entry()
for i := range list.children {
list.children[i].thumbnail = 0
}
currentScreenIndex := len(m.stack) - 1
curList := m.stack[currentScreenIndex].Entry()
for i := range curList.children {
curList.children[i].thumbnail = 0
}
}
+2 -3
View File
@@ -11,7 +11,6 @@ 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
@@ -21,7 +20,7 @@ func (m *Menu) RenderNotifications() {
fading = 1
}
offset := fading*h - h
lw := m.Font.Width(m.ratio, n.Message)
lw := m.Font.Width(0.5*m.ratio, n.Message)
fg := severityFgColor[n.Severity]
bg := severityBgColor[n.Severity]
m.DrawRect(
@@ -36,7 +35,7 @@ func (m *Menu) RenderNotifications() {
m.Font.Print(
45*m.ratio,
(stack+offset)*m.ratio,
m.ratio,
0.5*m.ratio,
n.Message,
)
stack += h + offset
+5 -12
View File
@@ -35,7 +35,7 @@ type entry struct {
children []entry // children entries
ptr int // index of the active child
indexes []struct {
Char rune
Char byte
Index int
}
}
@@ -53,13 +53,6 @@ type Scene interface {
Entry() *entry
}
func firstRune(s string) rune {
for _, r := range s {
return r
}
return 0
}
// genericSegueMount is the smooth transition of the menu entries first appearance
func genericSegueMount(list *entry) {
for i := range list.children {
@@ -205,21 +198,21 @@ func genericRender(list *entry) {
menu.Font.Print(
(670+1)*menu.ratio,
float32(h)*e.yp+fontOffset+1*menu.ratio,
menu.ratio, e.label)
0.5*menu.ratio, e.label)
menu.Font.SetColor(textColor.Alpha(e.labelAlpha))
menu.Font.Print(
670*menu.ratio,
float32(h)*e.yp+fontOffset,
menu.ratio, e.label)
0.5*menu.ratio, e.label)
if e.widget != nil {
e.widget(&e)
} else if e.stringValue != nil {
lw := menu.Font.Width(menu.ratio, e.stringValue())
lw := menu.Font.Width(0.5*menu.ratio, e.stringValue())
menu.Font.Print(
float32(w)-lw-128*menu.ratio,
float32(h)*e.yp+fontOffset,
menu.ratio, e.stringValue())
0.5*menu.ratio, e.stringValue())
}
}
}
+3 -3
View File
@@ -16,7 +16,7 @@ func buildCoreOptions() Scene {
var list sceneCoreOptions
list.label = "Core Options"
if core.Options == nil || len(core.Options.Vars) == 0 {
if core.Options == nil {
list.children = append(list.children, entry{
label: "No options",
icon: "subsetting",
@@ -28,11 +28,11 @@ func buildCoreOptions() Scene {
for _, v := range core.Options.Vars {
v := v
list.children = append(list.children, entry{
label: strings.ReplaceAll(v.Desc, "%", "%%"),
label: strings.Replace(v.Desc, "%", "%%", -1),
icon: "subsetting",
stringValue: func() string {
val := v.Choices[v.Choice]
return "◀ " + strings.ReplaceAll(val, "%", "%%") + " ▶"
return strings.Replace(val, "%", "%%", -1)
},
incr: func(direction int) {
v.Choice += direction
+12 -12
View File
@@ -70,16 +70,16 @@ func (s *sceneDialog) render() {
white,
)
menu.FontLg.SetColor(titleTextColor)
lw1 := menu.FontLg.Width(menu.ratio, s.title)
menu.FontLg.Print(fw/2-lw1/2, fh/2-120*menu.ratio+20*menu.ratio, menu.ratio, s.title)
menu.Font.SetColor(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.Font.SetColor(dialogTextColor)
lw2 := menu.Font.Width(menu.ratio, s.line1)
menu.Font.Print(fw/2-lw2/2, fh/2-30*menu.ratio+20*menu.ratio, menu.ratio, s.line1)
lw3 := menu.Font.Width(menu.ratio, s.line2)
menu.Font.Print(fw/2-lw3/2, fh/2+30*menu.ratio+20*menu.ratio, menu.ratio, s.line2)
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)
menu.FontSm.SetColor(dialogTextColor)
menu.Font.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.FontSm.Print(
menu.Font.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,
menu.ratio,
0.4*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.FontSm.Print(
menu.Font.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,
menu.ratio,
0.4*menu.ratio,
"YES")
}
+1 -1
View File
@@ -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 strings.HasPrefix(name, ".") && !settings.Current.ShowHiddenFiles {
if name[:1] == "." && !settings.Current.ShowHiddenFiles {
return
}
+6 -6
View File
@@ -203,13 +203,13 @@ func (s *sceneHistory) render() {
menu.Font.Print(
(840+1)*menu.ratio,
float32(h)*e.yp+fontOffset-slOffset+1*menu.ratio,
menu.ratio, e.label)
0.5*menu.ratio, e.label)
menu.Font.SetColor(textColor.Alpha(e.labelAlpha))
menu.Font.Print(
840*menu.ratio,
float32(h)*e.yp+fontOffset-slOffset,
menu.ratio, e.label)
stack += float32(int(menu.Font.Width(menu.ratio, e.label)))
0.5*menu.ratio, e.label)
stack += float32(int(menu.Font.Width(0.5*menu.ratio, e.label)))
stack += 10
for _, tag := range e.tags {
@@ -225,11 +225,11 @@ func (s *sceneHistory) render() {
}
}
menu.FontSm.SetColor(mutedTextColor.Alpha(e.subLabelAlpha))
menu.FontSm.Print(
menu.Font.SetColor(mutedTextColor.Alpha(e.subLabelAlpha))
menu.Font.Print(
840*menu.ratio,
float32(h)*e.yp+fontOffset+60*menu.ratio-slOffset,
menu.ratio, e.subLabel)
0.5*menu.ratio, e.subLabel)
}
}
+2 -2
View File
@@ -180,7 +180,7 @@ func gameExplorerCb(path string) {
// Shutdown the operating system
func cleanShutdown() {
core.Unload()
core.UnloadGame()
if err := exec.Command("/usr/sbin/shutdown", "-P", "now").Run(); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
@@ -188,7 +188,7 @@ func cleanShutdown() {
// Reboots the operating system
func cleanReboot() {
core.Unload()
core.UnloadGame()
if err := exec.Command("/usr/sbin/shutdown", "-r", "now").Run(); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
+6 -6
View File
@@ -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 rune
var last byte
for i := 0; i < len(list.children); i++ {
char := firstRune(list.children[i].label)
char := list.children[i].label[0]
if char != last {
list.indexes = append(list.indexes, struct {
Char rune
Char byte
Index int
}{char, i})
last = char
@@ -241,14 +241,14 @@ func (s *scenePlaylist) render() {
menu.Font.Print(
(840+1)*menu.ratio,
float32(h)*e.yp+fontOffset+1*menu.ratio,
menu.ratio, e.label)
0.5*menu.ratio, e.label)
menu.Font.SetColor(textColor.Alpha(e.labelAlpha))
menu.Font.Print(
840*menu.ratio,
float32(h)*e.yp+fontOffset,
menu.ratio, e.label)
0.5*menu.ratio, e.label)
stack += float32(int(menu.Font.Width(menu.ratio, e.label)))
stack += float32(int(menu.Font.Width(0.5*menu.ratio, e.label)))
stack += 10
for _, tag := range e.tags {
+6 -9
View File
@@ -1,7 +1,6 @@
package menu
import (
"github.com/libretro/ludo/core"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
@@ -28,7 +27,7 @@ func buildQuickMenu() Scene {
label: "Reset",
icon: "reset",
callbackOK: func() {
core.Reset()
state.Core.Reset()
state.MenuActive = false
state.FastForward = false
},
@@ -48,13 +47,11 @@ func buildQuickMenu() Scene {
icon: "screenshot",
callbackOK: func() {
name := utils.DatedName(state.GamePath)
state.PendingScreenshotName = name
state.PendingScreenshotDone = func(err error) {
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
ntf.DisplayAndLog(ntf.Success, "Menu", "Took a screenshot.")
}
err := menu.TakeScreenshot(name)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
ntf.DisplayAndLog(ntf.Success, "Menu", "Took a screenshot.")
}
},
})
+13 -15
View File
@@ -26,19 +26,17 @@ func buildSavestates() Scene {
icon: "savestate",
callbackOK: func() {
name := utils.DatedName(state.GamePath)
state.PendingScreenshotName = name
state.PendingScreenshotDone = func(err error) {
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
err = savestates.Save(name)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
menu.stack[len(menu.stack)-1] = buildSavestates()
menu.tweens.FastForward()
ntf.DisplayAndLog(ntf.Success, "Menu", "State saved.")
}
err := menu.TakeScreenshot(name)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
err = savestates.Save(name)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
menu.stack[len(menu.stack)-1] = buildSavestates()
menu.tweens.FastForward()
ntf.DisplayAndLog(ntf.Success, "Menu", "State saved.")
}
},
})
@@ -159,13 +157,13 @@ func (s *sceneSavestates) render() {
menu.Font.Print(
(840+1)*menu.ratio,
float32(h)*e.yp+fontOffset+1*menu.ratio,
menu.ratio, e.label)
0.5*menu.ratio, e.label)
menu.Font.SetColor(textColor.Alpha(e.labelAlpha))
menu.Font.Print(
840*menu.ratio,
float32(h)*e.yp+fontOffset,
menu.ratio, e.label)
0.5*menu.ratio, e.label)
}
}
}
+5 -3
View File
@@ -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,7 +164,8 @@ var incrCallbacks = map[string]callbackIncrement{
v := f.Value().(bool)
v = !v
f.Set(v)
menu.SetFullscreen(settings.Current.VideoFullscreen)
menu.Reconfigure(settings.Current.VideoFullscreen)
menu.ContextReset()
settings.Save()
},
"VideoMonitorIndex": func(f *structs.Field, direction int) {
@@ -177,7 +178,8 @@ var incrCallbacks = map[string]callbackIncrement{
v = len(glfw.GetMonitors()) - 1
}
f.Set(v)
menu.SetFullscreen(settings.Current.VideoFullscreen)
menu.Reconfigure(settings.Current.VideoFullscreen)
menu.ContextReset()
settings.Save()
},
"VideoFilter": func(f *structs.Field, direction int) {
+8 -8
View File
@@ -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(menu.ratio, e.label)
lw := menu.Font.Width(0.5*menu.ratio, e.label)
menu.Font.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
menu.Font.Print(x-lw/2+1*menu.ratio, float32(int(float32(h)/2+(250+1)*menu.ratio)), menu.ratio, e.label)
menu.Font.Print(x-lw/2+1*menu.ratio, float32(int(float32(h)/2+(250+1)*menu.ratio)), 0.5*menu.ratio, e.label)
menu.Font.SetColor(lablColor.Alpha(e.labelAlpha))
menu.Font.Print(x-lw/2, float32(int(float32(h)/2+250*menu.ratio)), menu.ratio, e.label)
lw = menu.FontSm.Width(menu.ratio, e.subLabel)
menu.FontSm.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
menu.FontSm.Print(x-lw/2+1*menu.ratio, float32(int(float32(h)/2+(330+1)*menu.ratio)), menu.ratio, e.subLabel)
menu.FontSm.SetColor(lablColor.Alpha(e.labelAlpha))
menu.FontSm.Print(x-lw/2, float32(int(float32(h)/2+330*menu.ratio)), menu.ratio, e.subLabel)
menu.Font.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.DrawImage(menu.icons["hexagon"],
+1 -1
View File
@@ -90,7 +90,7 @@ func (s *sceneUpdater) update(dt float32) {
s.children[0].icon = "reload"
s.children[0].callbackOK = func() {
cmd := exec.Command("/usr/sbin/shutdown", "-r", "now")
core.Unload()
core.UnloadGame()
err := cmd.Run()
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
+3 -7
View File
@@ -50,17 +50,13 @@ func New(vars []VariableInterface) (*Options, error) {
// Cache core options
for _, v := range vars {
choices := v.Choices()
// A core may expose an empty selector, e.g. when no BIOS files are found.
if len(choices) == 0 {
continue
}
v := v
o.Vars = append(o.Vars, &Variable{
Key: v.Key(),
Desc: v.Desc(),
Choices: choices,
Choices: v.Choices(),
Default: v.DefaultValue(),
Choice: utils.IndexOfString(v.DefaultValue(), choices),
Choice: utils.IndexOfString(v.DefaultValue(), v.Choices()),
})
}
o.Updated = true
+10 -1
View File
@@ -33,6 +33,7 @@ func ipsAllocTargetData(patch, source []byte) ([]byte, error) {
size |= int(patch[offset]) << 8
offset++
size |= int(patch[offset]) << 0
offset++
targetLength = size
prov := make([]byte, targetLength)
return prov, nil
@@ -116,7 +117,15 @@ func applyIPS(patch, source []byte) (*[]byte, error) {
offset++
if address == EOF {
if offset == len(patch) || offset == len(patch)-3 {
if offset == len(patch) {
return &targetData, nil
} else if offset == len(patch)-3 {
size := int(patch[offset]) << 16
offset++
size |= int(patch[offset]) << 8
offset++
size |= int(patch[offset]) << 0
offset++
return &targetData, nil
}
}
+6 -3
View File
@@ -6,14 +6,15 @@ 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 {
bytes, err := core.Serialize()
s := state.Core.SerializeSize()
bytes, err := state.Core.Serialize(s)
if err != nil {
return err
}
@@ -27,9 +28,11 @@ 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
}
return core.Unserialize(bytes)
err = state.Core.Unserialize(bytes, s)
return err
}
-6
View File
@@ -43,17 +43,13 @@ func defaultSettings() Settings {
"Nintendo - Game Boy Advance": "mgba_libretro",
"Nintendo - Game Boy Color": "gambatte_libretro",
"Nintendo - Game Boy": "gambatte_libretro",
"Nintendo - GameCube": "dolphin_libretro",
"Nintendo - Nintendo 3DS": "citra_libretro",
"Nintendo - Nintendo 64": "mupen64plus_next_libretro",
"Nintendo - Nintendo Entertainment System": "fceumm_libretro",
"Nintendo - Nintendo DS": "melonds_libretro",
"Nintendo - Pokemon Mini": "pokemini_libretro",
"Nintendo - Super Nintendo Entertainment System": "snes9x_libretro",
"Nintendo - Virtual Boy": "mednafen_vb_libretro",
"Nintendo - Wii": "dolphin_libretro",
"Sega - 32X": "picodrive_libretro",
"Sega - Dreamcast": "flycast_libretro",
"Sega - Game Gear": "genesis_plus_gx_libretro",
"Sega - Master System - Mark III": "genesis_plus_gx_libretro",
"Sega - Mega Drive - Genesis": "genesis_plus_gx_libretro",
@@ -64,8 +60,6 @@ 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",
+3 -3
View File
@@ -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"`
-10
View File
@@ -33,13 +33,3 @@ var LudOS bool
// FastForward will run the core as fast as possible
var FastForward bool
// PendingScreenshotName is the next screenshot capture requested by the UI.
var PendingScreenshotName string
// PendingScreenshotDone is called after the deferred screenshot capture completes.
var PendingScreenshotDone func(error)
// MenuContextResetNeeded requests re-uploading menu GL assets after the
// frontend window/context is recreated.
var MenuContextResetNeeded bool
-190
View File
@@ -1,190 +0,0 @@
//go:build gltest && !darwin
package video
import (
"runtime"
"testing"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/go-gl/mathgl/mgl32"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/state"
)
// Run with go test -tags 'wayland gltest' ./video on a graphical session.
func TestHardwareContextIsolation(t *testing.T) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if err := glfw.Init(); err != nil {
t.Fatal(err)
}
defer glfw.Terminate()
glfw.DefaultWindowHints()
glfw.WindowHint(glfw.Visible, glfw.False)
glfw.WindowHint(glfw.ContextVersionMajor, 3)
glfw.WindowHint(glfw.ContextVersionMinor, 3)
glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
w, err := glfw.CreateWindow(128, 96, "Ludo GL test", nil, nil)
if err != nil {
t.Fatal(err)
}
defer w.Destroy()
w.MakeContextCurrent()
if err := initGL(); err != nil {
t.Fatal(err)
}
t.Log("GL:", gl.GoStr(gl.GetString(gl.VERSION)))
oldCore, oldRunning := state.Core, state.CoreRunning
defer func() { state.Core, state.CoreRunning = oldCore, oldRunning }()
hw := &libretro.HWRenderCallback{HWContextType: libretro.HWContextOpenGLCore, VersionMajor: 3, VersionMinor: 3}
state.Core = &libretro.Core{HWRenderCallback: hw}
state.CoreRunning = true
v := &Video{Window: w, identityMat: mgl32.Ident4(), Geom: libretro.GameGeometry{MaxWidth: 64, MaxHeight: 64}}
v.SetHWRenderContext(hw)
v.ensurePixelFormatDefaults()
v.resetGameTexture(64, 64)
hw.ContextReset = func() {
if glfw.GetCurrentContext() != v.HWWindow {
t.Fatal("context_reset ran outside the HW context")
}
}
if err := v.ResetHWContext(); err != nil {
t.Fatal(err)
}
defer v.DestroySharedHWContext()
if glfw.GetCurrentContext() != w || v.HWWindow == nil || v.HWWindow == w {
t.Fatal("reset did not return to an independent frontend context")
}
v.BeginCore()
var readFBO, texture, pbo uint32
gl.GenFramebuffers(1, &readFBO)
gl.BindFramebuffer(gl.READ_FRAMEBUFFER, readFBO)
gl.GenTextures(1, &texture)
gl.ActiveTexture(gl.TEXTURE1)
gl.BindTexture(gl.TEXTURE_2D, texture)
gl.GenBuffers(1, &pbo)
gl.BindBuffer(gl.PIXEL_PACK_BUFFER, pbo)
gl.PixelStorei(gl.PACK_ROW_LENGTH, 256)
gl.Enable(gl.BLEND)
gl.Enable(gl.SCISSOR_TEST)
gl.BlendEquation(gl.FUNC_REVERSE_SUBTRACT)
v.EndCore()
v.BeginFrontendFrame()
checkInt := func(name uint32, want int32) {
t.Helper()
var got int32
gl.GetIntegerv(name, &got)
if got != want {
t.Errorf("GL state %#x: got %d, want %d", name, got, want)
}
}
checkInt(gl.PIXEL_PACK_BUFFER_BINDING, 0)
checkInt(gl.PACK_ROW_LENGTH, 0)
checkInt(gl.READ_FRAMEBUFFER_BINDING, 0)
checkInt(gl.BLEND_EQUATION_RGB, gl.FUNC_ADD)
v.BeginCore()
checkInt(gl.PIXEL_PACK_BUFFER_BINDING, int32(pbo))
checkInt(gl.PACK_ROW_LENGTH, 256)
checkInt(gl.READ_FRAMEBUFFER_BINDING, int32(readFBO))
checkInt(gl.DRAW_FRAMEBUFFER_BINDING, int32(v.fboID))
checkInt(gl.BLEND_EQUATION_RGB, gl.FUNC_REVERSE_SUBTRACT)
checkInt(gl.ACTIVE_TEXTURE, gl.TEXTURE1)
checkInt(gl.TEXTURE_BINDING_2D, int32(texture))
if !gl.IsEnabled(gl.BLEND) || !gl.IsEnabled(gl.SCISSOR_TEST) {
t.Error("frontend rendering changed core enables")
}
gl.BindBuffer(gl.PIXEL_PACK_BUFFER, 0)
gl.PixelStorei(gl.PACK_ROW_LENGTH, 0)
gl.Disable(gl.SCISSOR_TEST)
gl.Disable(gl.BLEND)
gl.BindFramebuffer(gl.READ_FRAMEBUFFER, v.fboID)
// Verify the produced pixels are visible through the shared texture.
gl.ClearColor(1, 0, 0, 1)
gl.Clear(gl.COLOR_BUFFER_BIT)
v.EndCore()
var frontendFBO uint32
gl.GenFramebuffers(1, &frontendFBO)
gl.BindFramebuffer(gl.FRAMEBUFFER, frontendFBO)
gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, v.texID, 0)
var pixel [4]byte
gl.ReadPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(&pixel[0]))
if pixel != [4]byte{255, 0, 0, 255} {
t.Errorf("shared frame: got %v, want red", pixel)
}
v.BeginFrontendFrame()
// Backbuffer snapshots must retain their orientation for either core origin.
v.defaultProgram, err = newProgram(vertexShader, defaultFragmentShader)
if err != nil {
t.Fatal(err)
}
gl.GenVertexArrays(1, &v.vao)
gl.BindVertexArray(v.vao)
gl.GenBuffers(1, &v.vbo)
gl.BindBuffer(gl.ARRAY_BUFFER, v.vbo)
vert := uint32(gl.GetAttribLocation(v.defaultProgram, gl.Str("vert\x00")))
uv := uint32(gl.GetAttribLocation(v.defaultProgram, gl.Str("vertTexCoord\x00")))
gl.EnableVertexAttribArray(vert)
gl.VertexAttribPointerWithOffset(vert, 2, gl.FLOAT, false, 16, 0)
gl.EnableVertexAttribArray(uv)
gl.VertexAttribPointerWithOffset(uv, 2, gl.FLOAT, false, 16, 8)
v.width, v.height = 64, 64
for _, bottomLeft := range []bool{false, true} {
hw.BottomLeftOrigin = bottomLeft
v.BeginFrontendFrame()
fbw, fbh := w.GetFramebufferSize()
v.Geom.AspectRatio = float64(fbw) / float64(fbh)
v.presentW, v.presentH = int32(fbw), int32(fbh)
gl.ClearColor(1, 0, 0, 1)
gl.Clear(gl.COLOR_BUFFER_BIT)
gl.Enable(gl.SCISSOR_TEST)
gl.Scissor(0, int32(fbh/2), int32(fbw), int32(fbh/2))
gl.ClearColor(0, 0, 1, 1)
gl.Clear(gl.COLOR_BUFFER_BIT)
gl.Disable(gl.SCISSOR_TEST)
v.CachePresentedFrame()
if !v.RenderCachedFrame() {
t.Fatal("cached frame unavailable")
}
gl.ReadPixels(1, 1, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(&pixel[0]))
if pixel != [4]byte{255, 0, 0, 255} {
t.Errorf("cached frame, bottom-left=%v: got %v, want red", bottomLeft, pixel)
}
}
hw.ContextDestroy = func() {
if glfw.GetCurrentContext() != v.HWWindow {
t.Error("context_destroy ran outside the HW context")
}
}
v.RunContextDestroy()
v.ResetCoreState()
if !gl.IsFramebuffer(frontendFBO) {
t.Error("HW teardown deleted a frontend framebuffer with the same name")
}
if v.fboID != 0 || v.HWWindow != nil || v.cachedFrameValid || v.width != 0 {
t.Error("HW teardown left stale core resources")
}
// A subsequent core must get a fresh context, including on the GL 2.x sync fallback.
if err := v.ResetHWContext(); err != nil {
t.Fatal(err)
}
v.hwSync = false
v.BeginCore()
checkInt(gl.PACK_ROW_LENGTH, 0)
checkInt(gl.PIXEL_PACK_BUFFER_BINDING, 0)
checkInt(gl.BLEND_EQUATION_RGB, gl.FUNC_ADD)
if gl.IsEnabled(gl.BLEND) || gl.IsEnabled(gl.SCISSOR_TEST) {
t.Error("new core inherited the previous core's enables")
}
v.EndCore()
if e := gl.GetError(); e != gl.NO_ERROR {
t.Errorf("GL error: %#x", e)
}
}
+6 -3
View File
@@ -4,9 +4,14 @@ var vertexShader = `
#if __VERSION__ >= 130
#define COMPAT_VARYING out
#define COMPAT_ATTRIBUTE in
#define COMPAT_TEXTURE texture
#define COMPAT_FRAGCOLOR FragColor
out vec4 COMPAT_FRAGCOLOR;
#else
#define COMPAT_VARYING varying
#define COMPAT_ATTRIBUTE attribute
#define COMPAT_TEXTURE texture2D
#define COMPAT_FRAGCOLOR gl_FragColor
#endif
COMPAT_ATTRIBUTE vec2 vert;
@@ -14,10 +19,8 @@ COMPAT_ATTRIBUTE vec2 vertTexCoord;
COMPAT_VARYING vec2 fragTexCoord;
uniform mat4 MVP;
void main() {
fragTexCoord = vertTexCoord;
gl_Position = vec4(vert, 0.0, 1.0) * MVP;
gl_Position = vec4(vert, 0.0, 1.0);
}
` + "\x00"
+129 -326
View File
@@ -5,7 +5,6 @@ import (
"image"
"image/draw"
"io"
"log"
"os"
"github.com/go-gl/gl/v2.1/gl"
@@ -15,7 +14,7 @@ import (
"golang.org/x/image/math/fixed"
)
type glyph struct {
type character struct {
x, y int
width int // Glyph width
height int // Glyph height
@@ -34,11 +33,9 @@ const (
TopToBottom // E.g.: Chinese
)
const fontRenderScale = 1
// A Font allows rendering of text to an OpenGL context.
type Font struct {
glyphs map[rune]*glyph
fontChar []*character
vao uint32
vbo uint32
program uint32
@@ -46,248 +43,19 @@ type Font struct {
color Color
atlasWidth float32
atlasHeight float32
atlas *image.RGBA
face font.Face
ttf *truetype.Font
scale int32
lineHeight float32
margin int
nextX int
nextY int
}
type point [4]float32
type glyphMetrics struct {
bounds fixed.Rectangle26_6
advance fixed.Int26_6
width int
height int
ascent int
descent int
}
func ceilFixed26_6(v fixed.Int26_6) int32 {
return int32((v + 63) >> 6)
}
func appendRange(runes []rune, start, end rune) []rune {
for r := start; r <= end; r++ {
runes = append(runes, r)
func max(a, b float32) float32 {
if a > b {
return a
}
return runes
}
func defaultRunes() []rune {
runes := make([]rune, 0, 512)
runes = appendRange(runes, 32, 126)
runes = append(runes,
'◀', '▶', '【', '】',
'「', '」', '『', '』',
'。', '、', '・', 'ー',
'!', '?',
'(', ')', '[', ']', '{', '}',
)
runes = appendRange(runes, 0xFF01, 0xFF0F) // fullwidth punctuation
runes = appendRange(runes, 0xFF10, 0xFF19) // fullwidth digits
runes = appendRange(runes, 0xFF1A, 0xFF20) // fullwidth : ; < = > ? @
runes = appendRange(runes, 0xFF21, 0xFF3A) // fullwidth A-Z
runes = appendRange(runes, 0xFF3B, 0xFF40) // fullwidth [ \ ] ^ _
runes = appendRange(runes, 0xFF41, 0xFF5A) // fullwidth a-z
runes = appendRange(runes, 0xFF5B, 0xFF65) // fullwidth { | } ~ and katakana punctuation
runes = appendRange(runes, 0x00A0, 0x017F)
runes = appendRange(runes, 0x3000, 0x303F)
runes = appendRange(runes, 0x3040, 0x30FF)
return runes
}
func getGlyphMetrics(face font.Face, ttf *truetype.Font, ch rune, scale int32) (glyphMetrics, error) {
gBnd, gAdv, ok := face.GlyphBounds(ch)
if !ok {
return glyphMetrics{}, fmt.Errorf("ttf face glyphBounds error")
}
gh := ceilFixed26_6(gBnd.Max.Y - gBnd.Min.Y)
gw := ceilFixed26_6(gBnd.Max.X - gBnd.Min.X)
// If gylph has no dimensions set to a max value
if gw == 0 || gh == 0 {
gBnd = ttf.Bounds(fixed.Int26_6(scale))
gw = ceilFixed26_6(gBnd.Max.X - gBnd.Min.X)
gh = ceilFixed26_6(gBnd.Max.Y - gBnd.Min.Y)
// Above can sometimes yield 0 for font smaller than 48pt, 1 is minimum
if gw == 0 || gh == 0 {
gw = 1
gh = 1
}
}
// The glyph's ascent and descent equal -bounds.Min.Y and +bounds.Max.Y.
gAscent := int(ceilFixed26_6(-gBnd.Min.Y))
gdescent := int(ceilFixed26_6(gBnd.Max.Y))
return glyphMetrics{
bounds: gBnd,
advance: gAdv,
width: int(gw),
height: int(gh),
ascent: gAscent,
descent: gdescent,
}, nil
}
func wrapGlyph(x, y *int, glyphWidth, lineHeight, atlasWidth, margin int) {
if *x+glyphWidth+margin > atlasWidth {
*x = margin
*y += lineHeight + margin
}
}
func appendGlyph(face font.Face, ttf *truetype.Font, ch rune, x, y *int, lineHeight, atlasWidth float32, atlas *image.RGBA, fg *image.Uniform, scale int32, margin int) (*glyph, error) {
char := new(glyph)
metrics, err := getGlyphMetrics(face, ttf, ch, scale)
if err != nil {
return nil, err
}
wrapGlyph(x, y, metrics.width, int(lineHeight), int(atlasWidth), margin)
if *y+metrics.height+margin > atlas.Rect.Dy() {
return nil, fmt.Errorf("glyph atlas full")
}
// Set w,h and adv, bearing V and bearing H in char
char.x = *x
char.y = *y
char.width = metrics.width
char.height = metrics.height
char.advance = int(metrics.advance)
char.bearingV = metrics.descent
char.bearingH = (int(metrics.bounds.Min.X) >> 6)
clip := image.Rect(*x, *y, *x+metrics.width, *y+metrics.height)
// Create a freetype context for drawing
c := freetype.NewContext()
c.SetDPI(72)
c.SetFont(ttf)
c.SetFontSize(float64(scale))
c.SetClip(clip)
c.SetDst(atlas)
c.SetSrc(fg)
c.SetHinting(font.HintingFull)
// Set the glyph dot
px := 0 - (int(metrics.bounds.Min.X) >> 6) + *x
py := (metrics.ascent) + *y
pt := freetype.Pt(px, py)
// Draw the text from mask to image
_, err = c.DrawString(string(ch), pt)
*x += metrics.width + margin
return char, err
}
func getMaxTextureSize() int32 {
var maxTextureSize int32
gl.GetIntegerv(gl.MAX_TEXTURE_SIZE, &maxTextureSize)
if maxTextureSize == 0 {
maxTextureSize = 4096
}
return maxTextureSize
}
func calculateAtlasSize(face font.Face, ttf *truetype.Font, runes []rune, scale int32, margin int) (float32, float32, float32, error) {
maxTextureSize := getMaxTextureSize()
lineHeight := float32(face.Metrics().Height.Ceil())
if lineHeight < 1 {
lineHeight = 1
}
width := 2048
if width > int(maxTextureSize) {
width = int(maxTextureSize)
}
for {
x := margin
y := margin
maxY := 0
for _, r := range runes {
metrics, err := getGlyphMetrics(face, ttf, r, scale)
if err != nil {
continue
}
wrapGlyph(&x, &y, metrics.width, int(lineHeight), width, margin)
x += metrics.width + margin
if y+int(lineHeight) > maxY {
maxY = y + int(lineHeight)
}
}
height := maxY + margin
if height <= int(maxTextureSize) || width >= int(maxTextureSize) {
if height > int(maxTextureSize) {
return 0, 0, 0, fmt.Errorf("glyph atlas %dx%d exceeds max texture size %d", width, height, maxTextureSize)
}
return float32(width), float32(height), lineHeight, nil
}
width *= 2
if width > int(maxTextureSize) {
width = int(maxTextureSize)
}
}
}
func reserveAtlasHeight(baseHeight int, lineHeight float32, margin int, maxTextureSize int32) int {
height := baseHeight * 2
extraRowsHeight := baseHeight + 32*(int(lineHeight)+margin)
if extraRowsHeight > height {
height = extraRowsHeight
}
if height > int(maxTextureSize) {
height = int(maxTextureSize)
}
return height
}
func (f *Font) uploadGlyphRect(g *glyph) {
if f.atlas == nil || g == nil {
return
}
offset := g.y*f.atlas.Stride + g.x*4
if offset < 0 || offset >= len(f.atlas.Pix) {
return
}
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, f.textureID)
gl.PixelStorei(gl.UNPACK_ALIGNMENT, 1)
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, int32(f.atlas.Stride/4))
gl.TexSubImage2D(
gl.TEXTURE_2D,
0,
int32(g.x),
int32(g.y),
int32(g.width),
int32(g.height),
gl.RGBA,
gl.UNSIGNED_BYTE,
gl.Ptr(f.atlas.Pix[offset:]),
)
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, 0)
gl.BindTexture(gl.TEXTURE_2D, 0)
return b
}
// LoadTrueTypeFont builds a set of textures based on a ttf files gylphs
func LoadTrueTypeFont(program uint32, r io.Reader, scale int32, dir Direction) (*Font, error) {
func LoadTrueTypeFont(program uint32, r io.Reader, scale int32, low, high rune, dir Direction) (*Font, error) {
data, err := io.ReadAll(r)
if err != nil {
return nil, err
@@ -301,63 +69,120 @@ func LoadTrueTypeFont(program uint32, r io.Reader, scale int32, dir Direction) (
// Make Font stuct type
f := new(Font)
f.glyphs = make(map[rune]*glyph)
f.fontChar = make([]*character, 0, high-low+1)
f.program = program // Set shader program
f.SetColor(Color{R: 1, G: 1, B: 1, A: 1}) // Set default white
f.ttf = ttf
f.scale = scale
// Create new face
face := truetype.NewFace(ttf, &truetype.Options{
ttfFace := truetype.NewFace(ttf, &truetype.Options{
Size: float64(scale),
DPI: 72,
Hinting: font.HintingFull,
})
f.face = face
margin := 4
runes := defaultRunes()
var lineHeight float32
f.atlasWidth, f.atlasHeight, lineHeight, err = calculateAtlasSize(face, ttf, runes, scale, margin)
if err != nil {
return nil, err
f.atlasWidth = 1024
f.atlasHeight = 1024
for ch := low; ch <= high; ch++ {
gBnd, _, ok := ttfFace.GlyphBounds(ch)
if !ok {
return nil, fmt.Errorf("ttf face glyphBounds error")
}
gh := int32((gBnd.Max.Y - gBnd.Min.Y) >> 6)
lineHeight = max(lineHeight, float32(gh))
}
f.atlasHeight = float32(reserveAtlasHeight(int(f.atlasHeight), lineHeight, margin, getMaxTextureSize()))
f.lineHeight = lineHeight
f.margin = margin
// Create image to draw glyph
fg, bg := image.White, image.Black
rect := image.Rect(0, 0, int(f.atlasWidth), int(f.atlasHeight))
atlas := image.NewRGBA(rect)
draw.Draw(atlas, atlas.Bounds(), bg, image.Point{}, draw.Src)
f.atlas = atlas
rgba := image.NewRGBA(rect)
draw.Draw(rgba, rgba.Bounds(), bg, image.Point{}, draw.Src)
margin := 4
x := margin
y := margin
for _, r := range runes {
glyph, err := appendGlyph(face, ttf, r, &x, &y, lineHeight, f.atlasWidth, atlas, fg, scale, margin)
if err != nil {
continue
// Make each gylph
for ch := low; ch <= high; ch++ {
char := new(character)
gBnd, gAdv, ok := ttfFace.GlyphBounds(ch)
if !ok {
return nil, fmt.Errorf("ttf face glyphBounds error")
}
f.glyphs[r] = glyph
gh := int32((gBnd.Max.Y - gBnd.Min.Y) >> 6)
gw := int32((gBnd.Max.X - gBnd.Min.X) >> 6)
// If gylph has no dimensions set to a max value
if gw == 0 || gh == 0 {
gBnd = ttf.Bounds(fixed.Int26_6(scale))
gw = int32((gBnd.Max.X - gBnd.Min.X) >> 6)
gh = int32((gBnd.Max.Y - gBnd.Min.Y) >> 6)
// Above can sometimes yield 0 for font smaller than 48pt, 1 is minimum
if gw == 0 || gh == 0 {
gw = 1
gh = 1
}
}
// The glyph's ascent and descent equal -bounds.Min.Y and +bounds.Max.Y.
gAscent := int(-gBnd.Min.Y) >> 6
gdescent := int(gBnd.Max.Y) >> 6
// Set w,h and adv, bearing V and bearing H in char
char.x = x
char.y = y
char.width = int(gw)
char.height = int(gh)
char.advance = int(gAdv)
char.bearingV = gdescent
char.bearingH = (int(gBnd.Min.X) >> 6)
clip := image.Rect(x, y, x+int(gw), y+int(gh))
// Create a freetype context for drawing
c := freetype.NewContext()
c.SetDPI(72)
c.SetFont(ttf)
c.SetFontSize(float64(scale))
c.SetClip(clip)
c.SetDst(rgba)
c.SetSrc(fg)
c.SetHinting(font.HintingFull)
// Set the glyph dot
px := 0 - (int(gBnd.Min.X) >> 6) + x
py := (gAscent) + y
pt := freetype.Pt(px, py)
x += int(gw) + margin
if x+int(gw)+margin > int(f.atlasWidth) {
x = 0
y += int(lineHeight) + margin
}
// Draw the text from mask to image
_, err = c.DrawString(string(ch), pt)
if err != nil {
return nil, err
}
// Add char to fontChar list
f.fontChar = append(f.fontChar, char)
}
f.nextX = x
f.nextY = y
// Generate texture
gl.GenTextures(1, &f.textureID)
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, f.textureID)
gl.PixelStorei(gl.UNPACK_ALIGNMENT, 1)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
gl.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.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.BindTexture(gl.TEXTURE_2D, 0)
// Configure VAO/VBO for texture quads
@@ -380,25 +205,6 @@ func LoadTrueTypeFont(program uint32, r io.Reader, scale int32, dir Direction) (
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)
@@ -420,16 +226,7 @@ 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))
atlasScale := int32(float32(scale) / fontRenderScale)
if atlasScale < 1 {
atlasScale = 1
}
font, err := LoadTrueTypeFont(program, fd, atlasScale, LeftToRight)
if err != nil {
return nil, err
}
return font, nil
return LoadTrueTypeFont(program, fd, scale, 32, 256, LeftToRight)
}
// SetColor allows you to set the text color to be used when you draw the text
@@ -448,14 +245,13 @@ 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)
@@ -467,23 +263,26 @@ func (f *Font) Print(x, y float32, scale float32, text string) error {
var coords []point
// Iterate through all glyphs in string
// Iterate through all characters in string
for i := range indices {
// Get rune
runeIndex := indices[i]
var ch *glyph
// Skip runes that are not in font glyph map
ch = f.ensureGlyph(runeIndex)
if ch == nil {
ch = f.glyphs['?'] // fallback
var ch *character
// Skip runes that are not in font chacter range
if int(runeIndex)-int(lowChar) > len(f.fontChar) || runeIndex < lowChar {
// Print a ?
ch = f.fontChar[int(rune('?'))-int(lowChar)]
} else {
// Find rune in fontChar list
ch = f.fontChar[runeIndex-lowChar]
}
// Calculate position and size for current rune
xpos := x + float32(ch.bearingH)*scale
ypos := y - float32(ch.height-ch.bearingV)*scale
w := float32(ch.width) * scale
h := float32(ch.height) * scale
xpos := x - 1 + float32(ch.bearingH)*scale
ypos := y - 2 - float32(ch.height-ch.bearingV)*scale
w := float32(ch.width+2) * scale
h := float32(ch.height+2) * scale
// Set quad positions
var x1 = xpos
@@ -491,12 +290,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) / f.atlasWidth, float32(ch.y) / f.atlasHeight})
coords = append(coords, point{x2, y1, float32(ch.x+ch.width) / f.atlasWidth, float32(ch.y) / f.atlasHeight})
coords = append(coords, point{x1, y2, float32(ch.x) / f.atlasWidth, float32(ch.y+ch.height) / f.atlasHeight})
coords = append(coords, point{x2, y1, float32(ch.x+ch.width) / f.atlasWidth, float32(ch.y) / f.atlasHeight})
coords = append(coords, point{x1, y2, float32(ch.x) / f.atlasWidth, float32(ch.y+ch.height) / f.atlasHeight})
coords = append(coords, point{x2, y2, float32(ch.x+ch.width) / f.atlasWidth, float32(ch.y+ch.height) / f.atlasHeight})
coords = append(coords, point{x1, y1, float32(ch.x-1) / f.atlasWidth, float32(ch.y-1) / f.atlasHeight})
coords = append(coords, point{x2, y1, float32(ch.x+ch.width+1) / f.atlasWidth, float32(ch.y-1) / f.atlasHeight})
coords = append(coords, point{x1, y2, float32(ch.x-1) / f.atlasWidth, float32(ch.y+ch.height+1) / f.atlasHeight})
coords = append(coords, point{x2, y1, float32(ch.x+ch.width+1) / f.atlasWidth, float32(ch.y-1) / f.atlasHeight})
coords = append(coords, point{x1, y2, float32(ch.x-1) / f.atlasWidth, float32(ch.y+ch.height+1) / f.atlasHeight})
coords = append(coords, point{x2, y2, float32(ch.x+ch.width+1) / f.atlasWidth, float32(ch.y+ch.height+1) / f.atlasHeight})
// Now advance cursors for next glyph (note that advance is number of 1/64 pixels)
x += float32((ch.advance >> 6)) * scale // Bitshift by 6 to get value in pixels (2^6 = 64 (divide amount of 1/64th pixels by 64 to get amount of pixels))
@@ -524,7 +323,6 @@ 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)
@@ -532,21 +330,26 @@ func (f *Font) Width(scale float32, text string) float32 {
return 0
}
// Iterate through all glyphs in string
lowChar := rune(32)
// Iterate through all characters in string
for i := range indices {
// Get rune
runeIndex := indices[i]
if f.face != nil {
if advance, ok := f.face.GlyphAdvance(runeIndex); ok {
width += float32(advance.Round()) * scale
continue
}
// Skip runes that are not in font chacter range
if int(runeIndex)-int(lowChar) > len(f.fontChar) || runeIndex < lowChar {
fmt.Printf("%c %d\n", runeIndex, runeIndex)
continue
}
if ch, ok := f.glyphs['?']; ok {
width += float32((ch.advance >> 6)) * scale
}
// Find rune in fontChar list
ch := f.fontChar[runeIndex-lowChar]
// Now advance cursors for next glyph (note that advance is number of 1/64 pixels)
width += float32((ch.advance >> 6)) * scale // Bitshift by 6 to get value in pixels (2^6 = 64 (divide amount of 1/64th pixels by 64 to get amount of pixels))
}
return width
-83
View File
@@ -1,83 +0,0 @@
package video
import (
"log"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/mathgl/mgl32"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/state"
)
// InitFramebuffer initializes and configures the video frame buffer based on
// informations from the HWRenderCallback of the libretro core.
func (video *Video) InitFramebuffer() {
width := int32(video.Geom.MaxWidth)
height := int32(video.Geom.MaxHeight)
video.texWidth = width
video.texHeight = height
log.Printf("[Video]: Initializing HW render (%v x %v).\n", width, height)
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, video.texID)
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, nil)
genFramebuffers(1, &video.fboID)
bindFramebuffer(gl.FRAMEBUFFER, video.fboID)
libretro.SetCurrentFramebufferValue(uintptr(video.fboID))
framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, video.texID, 0)
hw := state.Core.HWRenderCallback
if hw.Depth {
genRenderbuffers(1, &video.rboID)
bindRenderbuffer(gl.RENDERBUFFER, video.rboID)
format := gl.DEPTH_COMPONENT16
if hw.Stencil {
format = gl.DEPTH24_STENCIL8
}
renderbufferStorage(gl.RENDERBUFFER, uint32(format), width, height)
bindRenderbuffer(gl.RENDERBUFFER, 0)
// Separate attachment points work with both core and EXT framebuffers.
framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, video.rboID)
if hw.Stencil {
framebufferRenderbuffer(gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, gl.RENDERBUFFER, video.rboID)
}
}
// Default origin is top left
video.orthoMat = mgl32.Ortho2D(-1, 1, -1, 1)
if hw.BottomLeftOrigin {
video.orthoMat = mgl32.Ortho2D(-1, 1, 1, -1)
}
if st := checkFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE {
log.Fatalf("[Video] Framebuffer is not complete. Error: %v\n", st)
}
gl.ClearColor(0, 0, 0, 1)
if hw.Depth && hw.Stencil {
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT)
} else if hw.Depth {
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
} else {
gl.Clear(gl.COLOR_BUFFER_BIT)
}
}
func bindBackbuffer() {
bindFramebuffer(gl.FRAMEBUFFER, 0)
}
func (video *Video) destroyFramebuffer() {
bindBackbuffer()
if video.rboID != 0 {
deleteRenderbuffers(1, &video.rboID)
video.rboID = 0
}
if video.fboID != 0 {
deleteFramebuffers(1, &video.fboID)
video.fboID = 0
}
}
+5 -7
View File
@@ -124,7 +124,11 @@ func (video *Video) ScissorEnd() {
gl.Disable(gl.SCISSOR_TEST)
}
func vertexArrayForFramebuffer(x, y, w, h, scale, ffbw, ffbh float32) []float32 {
func (video *Video) vertexArray(x, y, w, h, scale float32) []float32 {
fbw, fbh := video.Window.GetFramebufferSize()
ffbw := float32(fbw)
ffbh := float32(fbh)
w *= scale
h *= scale
@@ -139,11 +143,6 @@ func vertexArrayForFramebuffer(x, y, w, h, scale, ffbw, ffbh float32) []float32
}
}
func (video *Video) vertexArray(x, y, w, h, scale float32) []float32 {
fbw, fbh := video.Window.GetFramebufferSize()
return vertexArrayForFramebuffer(x, y, w, h, scale, float32(fbw), float32(fbh))
}
// DrawBorder draws a colored rectangle border
func (video *Video) DrawBorder(x, y, w, h, borderWidth float32, c Color) {
va := video.vertexArray(x, y, w, h, 1.0)
@@ -220,7 +219,6 @@ func textureLoad(nrgba *image.NRGBA) uint32 {
gl.RGBA,
gl.UNSIGNED_BYTE,
gl.Ptr(nrgba.Pix))
gl.ActiveTexture(gl.TEXTURE0)
return texture
}
-63
View File
@@ -1,63 +0,0 @@
package video
import (
"errors"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/glfw/v3.4/glfw"
)
var (
glslVersion = "#version 120\n"
genVertexArrays = gl.GenVertexArrays
bindVertexArray = gl.BindVertexArray
genFramebuffers = gl.GenFramebuffers
bindFramebuffer = gl.BindFramebuffer
deleteFramebuffers = gl.DeleteFramebuffers
framebufferTexture2D = gl.FramebufferTexture2D
checkFramebufferStatus = gl.CheckFramebufferStatus
genRenderbuffers = gl.GenRenderbuffers
bindRenderbuffer = gl.BindRenderbuffer
deleteRenderbuffers = gl.DeleteRenderbuffers
renderbufferStorage = gl.RenderbufferStorage
framebufferRenderbuffer = gl.FramebufferRenderbuffer
)
// The frontend and hidden core context use the same hints. Select their API
// from the actual context capabilities, including macOS's legacy extensions.
func initGL() error {
if err := gl.Init(); err != nil {
return err
}
window := glfw.GetCurrentContext()
major, minor := window.GetAttrib(glfw.ContextVersionMajor), window.GetAttrib(glfw.ContextVersionMinor)
glslVersion = "#version 120\n"
if major > 3 || (major == 3 && minor >= 2) {
glslVersion = "#version 150\n"
} else if major == 3 {
glslVersion = "#version 130\n"
}
genVertexArrays, bindVertexArray = gl.GenVertexArrays, gl.BindVertexArray
genFramebuffers, bindFramebuffer, deleteFramebuffers = gl.GenFramebuffers, gl.BindFramebuffer, gl.DeleteFramebuffers
framebufferTexture2D, checkFramebufferStatus = gl.FramebufferTexture2D, gl.CheckFramebufferStatus
genRenderbuffers, bindRenderbuffer, deleteRenderbuffers = gl.GenRenderbuffers, gl.BindRenderbuffer, gl.DeleteRenderbuffers
renderbufferStorage, framebufferRenderbuffer = gl.RenderbufferStorage, gl.FramebufferRenderbuffer
if major < 3 && !glfw.ExtensionSupported("GL_ARB_vertex_array_object") {
if !glfw.ExtensionSupported("GL_APPLE_vertex_array_object") {
return errors.New("OpenGL vertex array objects are not supported")
}
genVertexArrays, bindVertexArray = gl.GenVertexArraysAPPLE, gl.BindVertexArrayAPPLE
}
if major < 3 && !glfw.ExtensionSupported("GL_ARB_framebuffer_object") {
if !glfw.ExtensionSupported("GL_EXT_framebuffer_object") {
return errors.New("OpenGL framebuffer objects are not supported")
}
genFramebuffers, bindFramebuffer, deleteFramebuffers = gl.GenFramebuffersEXT, gl.BindFramebufferEXT, gl.DeleteFramebuffersEXT
framebufferTexture2D, checkFramebufferStatus = gl.FramebufferTexture2DEXT, gl.CheckFramebufferStatusEXT
genRenderbuffers, bindRenderbuffer, deleteRenderbuffers = gl.GenRenderbuffersEXT, gl.BindRenderbufferEXT, gl.DeleteRenderbuffersEXT
renderbufferStorage, framebufferRenderbuffer = gl.RenderbufferStorageEXT, gl.FramebufferRenderbufferEXT
}
return nil
}
-25
View File
@@ -1,25 +0,0 @@
package video
import (
"testing"
"github.com/libretro/ludo/libretro"
)
func TestDuplicateFramePreservesDimensions(t *testing.T) {
v := &Video{}
v.Refresh(libretro.HWFrameBufferValid, 320, 240, 0)
v.Refresh(nil, 0, 0, 0)
if v.width != 320 || v.height != 240 || v.data != libretro.HWFrameBufferValid || !v.needUpload {
t.Fatal("NULL refresh discarded the pending frame")
}
v.needUpload = false
v.Refresh(nil, 640, 480, 0)
if v.width != 320 || v.height != 240 || v.needUpload {
t.Fatal("NULL refresh changed the previously presented frame")
}
}
func TestContextDestroyWithoutCore(t *testing.T) {
(&Video{}).RunContextDestroy()
}
+33 -31
View File
@@ -1,7 +1,6 @@
package video
import (
"errors"
"image"
"image/png"
"os"
@@ -14,33 +13,41 @@ import (
"github.com/libretro/ludo/state"
)
func (video *Video) captureBackbufferScreenshot(name string) error {
outW, outH := video.Geom.BaseWidth, video.Geom.BaseHeight
if outW < 1 || outH < 1 {
return errors.New("invalid screenshot output size")
}
// During the TakeScreenshot step, we need to render the current game frame at
// the right resolution to later capture it using ReadPixels. renderScreenshot
// taking care of this.
func (video *Video) renderScreenshot() {
va := video.vertexArray(0, 0, float32(video.Geom.BaseWidth), float32(video.Geom.BaseHeight), 1.0)
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW)
x := video.presentX
y := video.presentY
w := video.presentW
h := video.presentH
if !state.CoreRunning || video.width < 1 || video.height < 1 || w < 1 || h < 1 {
return errors.New("no game frame to capture")
}
bindVertexArray(video.vao)
var oldReadBuffer int32
gl.GetIntegerv(gl.READ_BUFFER, &oldReadBuffer)
gl.ReadBuffer(gl.BACK)
gl.BindTexture(gl.TEXTURE_2D, video.texID)
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
img := image.NewRGBA(image.Rect(0, 0, int(w), int(h)))
gl.Finish()
gl.ReadPixels(x, y, w, h, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(img.Pix))
gl.ReadBuffer(uint32(oldReadBuffer))
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
}
flipped := imaging.FlipV(img)
if flipped.Bounds().Dx() != outW || flipped.Bounds().Dy() != outH {
flipped = imaging.Resize(flipped, outW, outH, imaging.Linear)
}
// TakeScreenshot captures the ouput of video.Render and writes it to a file
func (video *Video) TakeScreenshot(name string) error {
state.MenuActive = false
defer func() { state.MenuActive = true }()
gl.UseProgram(video.defaultProgram)
video.renderScreenshot()
img := image.NewRGBA(image.Rect(0, 0, video.Geom.BaseWidth, video.Geom.BaseHeight))
_, fbh := video.Window.GetFramebufferSize()
gl.ReadPixels(
0, int32(fbh-video.Geom.BaseHeight),
int32(video.Geom.BaseWidth), int32(video.Geom.BaseHeight),
gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(img.Pix))
gl.UseProgram(video.program)
err := os.MkdirAll(settings.Current.ScreenshotsDirectory, os.ModePerm)
if err != nil {
@@ -52,13 +59,8 @@ func (video *Video) captureBackbufferScreenshot(name string) error {
if err != nil {
return err
}
defer fd.Close()
flipped := imaging.FlipV(img)
return png.Encode(fd, flipped)
}
// TakeScreenshotInFrontendFrame captures a screenshot while the caller already
// owns a frontend frame and has already rendered the current game frame.
func (video *Video) TakeScreenshotInFrontendFrame(name string) error {
return video.captureBackbufferScreenshot(name)
}
+1 -16
View File
@@ -5,7 +5,6 @@ import (
"strings"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/mathgl/mgl32"
)
func newProgram(vertexShaderSource, fragmentShaderSource string) (uint32, error) {
@@ -23,9 +22,6 @@ func newProgram(vertexShaderSource, fragmentShaderSource string) (uint32, error)
gl.AttachShader(program, vertexShader)
gl.AttachShader(program, fragmentShader)
// Game and menu programs share a VAO, so their attribute locations must match.
gl.BindAttribLocation(program, 0, gl.Str("vert\x00"))
gl.BindAttribLocation(program, 1, gl.Str("vertTexCoord\x00"))
gl.LinkProgram(program)
var status int32
@@ -43,24 +39,13 @@ 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(glslVersion + source)
csources, free := gl.Strs(source)
gl.ShaderSource(shader, 1, csources, nil)
free()
gl.CompileShader(shader)
-78
View File
@@ -1,78 +0,0 @@
//go:build gltest && linux
package video
import (
"bytes"
"runtime"
"testing"
"unsafe"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/libretro/ludo/libretro"
"golang.org/x/sys/unix"
)
func TestSoftwareFrameUploadBounds(t *testing.T) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if err := glfw.Init(); err != nil {
t.Fatal(err)
}
defer glfw.Terminate()
glfw.DefaultWindowHints()
glfw.WindowHint(glfw.Visible, glfw.False)
w, err := glfw.CreateWindow(32, 32, "Ludo upload test", nil, nil)
if err != nil {
t.Fatal(err)
}
defer w.Destroy()
w.MakeContextCurrent()
if err := initGL(); err != nil {
t.Fatal(err)
}
v := &Video{Geom: libretro.GameGeometry{MaxWidth: 64, MaxHeight: 64}}
v.SetPixelFormat(libretro.PixelFormatXRGB8888)
v.resetGameTexture(1, 1)
defer gl.DeleteTextures(1, &v.texID)
v.program, err = newProgram(vertexShader, defaultFragmentShader)
if err != nil {
t.Fatal(err)
}
defer gl.DeleteProgram(v.program)
const width, height, pitch = 32, 32, 32 * 4
page := unix.Getpagesize()
memory, err := unix.Mmap(-1, 0, page*2, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_ANON|unix.MAP_PRIVATE)
if err != nil {
t.Fatal(err)
}
defer unix.Munmap(memory)
if err := unix.Mprotect(memory[page:], unix.PROT_NONE); err != nil {
t.Fatal(err)
}
// The core supplies only the current frame, ending at a protected page.
frame := memory[page-pitch*height : page]
for i := range frame {
frame[i] = byte(i)
}
v.Refresh(unsafe.Pointer(&frame[0]), width, height, pitch)
v.uploadTexture()
var texWidth, texHeight int32
gl.GetTexLevelParameteriv(gl.TEXTURE_2D, 0, gl.TEXTURE_WIDTH, &texWidth)
gl.GetTexLevelParameteriv(gl.TEXTURE_2D, 0, gl.TEXTURE_HEIGHT, &texHeight)
if texWidth != width || texHeight != height {
t.Fatalf("texture size = %dx%d, want %dx%d", texWidth, texHeight, width, height)
}
pixels := make([]byte, len(frame))
gl.GetTexImage(gl.TEXTURE_2D, 0, v.pixType, v.pixFmt, gl.Ptr(pixels))
if !bytes.Equal(pixels, frame) {
t.Error("uploaded pixels differ from the supplied frame")
}
if err := gl.GetError(); err != gl.NO_ERROR {
t.Errorf("GL error: %#x", err)
}
}
+60 -587
View File
@@ -4,14 +4,12 @@
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"
@@ -19,16 +17,9 @@ import (
// Video holds the state of the video package
type Video struct {
Window *glfw.Window
HWWindow *glfw.Window // hidden core context; shares objects, not GL state, with Window
Geom libretro.GameGeometry
Font *Font
FontSm *Font
FontLg *Font
title string
windowedX, windowedY int
windowedWidth, windowedHeight int
Window *glfw.Window
Geom libretro.GameGeometry
Font *Font
program uint32 // current program used for the game quad
defaultProgram uint32 // default program used for the game quad
@@ -41,20 +32,6 @@ 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
@@ -65,190 +42,21 @@ 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{title: "Ludo"}
vid.identityMat = mgl32.Ident4()
vid := &Video{}
vid.Configure(fullscreen)
return vid
}
// SetFullscreen changes the window mode or monitor without recreating GL contexts.
func (video *Video) SetFullscreen(fullscreen bool) {
if fullscreen {
if video.Window.GetMonitor() == nil {
video.windowedWidth, video.windowedHeight = video.Window.GetSize()
// Wayland does not expose window positions.
if glfw.GetPlatform() != glfw.PlatformWayland {
video.windowedX, video.windowedY = video.Window.GetPos()
}
}
monitor := glfw.GetMonitors()[settings.Current.VideoMonitorIndex]
modes := monitor.GetVideoModes()
mode := modes[len(modes)-1]
video.Window.SetMonitor(monitor, 0, 0, mode.Width, mode.Height, mode.RefreshRate)
} else if video.Window.GetMonitor() != nil {
video.Window.SetMonitor(nil, video.windowedX, video.windowedY,
video.windowedWidth, video.windowedHeight, glfw.DontCare)
}
}
// ReconfigureHWContext recreates the visible window using the currently stored
// HW context hints while preserving the current window size when windowed.
func (video *Video) ReconfigureHWContext(fullscreen bool) error {
width := 384 * 2
height := 240 * 2
if video.Window != nil && !fullscreen {
width, height = video.Window.GetSize()
if width < 1 {
width = 384 * 2
}
if height < 1 {
height = 240 * 2
}
}
return video.configureWithSize(width, height, fullscreen)
}
// CreateSharedHWContext creates a hidden context shared with the visible frontend context.
func (video *Video) CreateSharedHWContext() error {
if video.HWWindow != nil {
return nil
}
video.configureWindowHints()
glfw.WindowHint(glfw.Visible, glfw.False)
glfw.WindowHint(glfw.Focused, glfw.False)
width, height := video.Window.GetSize()
if width < 1 {
width = 1
}
if height < 1 {
height = 1
}
hwWindow, err := glfw.CreateWindow(width, height, "", nil, video.Window)
if err != nil {
return err
}
video.HWWindow = hwWindow
major := video.Window.GetAttrib(glfw.ContextVersionMajor)
minor := video.Window.GetAttrib(glfw.ContextVersionMinor)
video.hwSync = major > 3 || (major == 3 && minor >= 2) || glfw.ExtensionSupported("GL_ARB_sync")
return nil
}
// DestroySharedHWContext destroys the hidden shared context if it exists.
func (video *Video) DestroySharedHWContext() {
if video.HWWindow != nil {
video.BeginCore()
video.destroyFramebuffer()
libretro.SetCurrentFramebufferValue(0)
video.EndCore()
video.HWWindow.Destroy()
video.HWWindow = nil
}
}
// MakeFrontendContextCurrent switches back to the visible frontend context.
func (video *Video) MakeFrontendContextCurrent() {
// Reconfigure destroys and recreates the window with new attributes
func (video *Video) Reconfigure(fullscreen bool) {
if video.Window != nil {
video.Window.MakeContextCurrent()
}
}
// RunContextDestroy notifies the active core before its GL context is destroyed.
func (video *Video) RunContextDestroy() {
if !state.CoreRunning || state.Core == nil || video.HWWindow == nil {
return
}
hw := state.Core.HWRenderCallback
if hw != nil && hw.ContextDestroy != nil {
video.BeginCore()
hw.ContextDestroy()
video.EndCore()
}
}
// ResetHWContext creates the core's render target and resets its renderer.
func (video *Video) ResetHWContext() error {
if err := video.CreateSharedHWContext(); err != nil {
return err
}
video.BeginCore()
defer video.EndCore()
video.InitFramebuffer()
state.Core.HWRenderCallback.ContextReset()
return nil
}
// BeginCore enters the isolated context for run, reset, serialization or teardown.
func (video *Video) BeginCore() {
if video.HWWindow != nil {
video.switchContext(video.HWWindow)
}
}
// EndCore returns to the frontend after the core's GL work.
func (video *Video) EndCore() {
if video.HWWindow != nil {
video.switchContext(video.Window)
}
}
// ContextCallbacks returns paired release/acquire callbacks for synchronous
// context handoffs between the frontend and native core threads.
func (video *Video) ContextCallbacks() (release, acquire func()) {
var current *glfw.Window
release = func() {
current = nil
if video.Window == nil || state.Core == nil || state.Core.HWRenderCallback == nil {
return
}
current = glfw.GetCurrentContext()
// Before context_reset, HW negotiation may still use the visible context.
// Once the hidden context exists, leave the menu context on its own thread.
if video.HWWindow != nil && current != video.HWWindow {
current = nil
}
if current != nil {
glfw.DetachCurrentContext()
}
}
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.Window.Destroy()
}
video.Configure(fullscreen)
}
// GetFramebufferSize retrieves the size, in pixels, of the framebuffer of the specified window.
@@ -261,69 +69,12 @@ func (video *Video) GetFramebufferSize() (int, int) {
// SetTitle sets the window title, encoded as UTF-8, of the window.
func (video *Video) SetTitle(title string) {
video.title = title
if video.Window == nil {
return
}
video.Window.SetTitle(title)
}
// SetHWRenderContext stores the context requested by the current libretro core.
func (video *Video) SetHWRenderContext(hw *libretro.HWRenderCallback) {
if hw == nil {
video.hwContextType = 0
video.hwContextMajor = 0
video.hwContextMinor = 0
video.hwDebugContext = false
video.hwContextConfigured = false
return
}
video.hwContextType = hw.HWContextType
video.hwContextMajor = int(hw.VersionMajor)
video.hwContextMinor = int(hw.VersionMinor)
video.hwDebugContext = hw.DebugContext
video.hwContextConfigured = true
}
func (video *Video) configureWindowHints() {
glfw.DefaultWindowHints()
if !video.hwContextConfigured {
return
}
switch video.hwContextType {
case libretro.HWContextOpenGLCore:
major := video.hwContextMajor
minor := video.hwContextMinor
glfw.WindowHint(glfw.ClientAPI, glfw.OpenGLAPI)
glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLAnyProfile)
if major > 0 {
glfw.WindowHint(glfw.ContextVersionMajor, major)
glfw.WindowHint(glfw.ContextVersionMinor, minor)
if major > 3 || (major == 3 && minor >= 2) {
glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True)
}
}
case libretro.HWContextOpenGL:
glfw.WindowHint(glfw.ClientAPI, glfw.OpenGLAPI)
if video.hwContextMajor > 0 {
glfw.WindowHint(glfw.ContextVersionMajor, video.hwContextMajor)
glfw.WindowHint(glfw.ContextVersionMinor, video.hwContextMinor)
if video.hwContextMajor > 3 || (video.hwContextMajor == 3 && video.hwContextMinor >= 2) {
glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCompatProfile)
glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.False)
}
}
}
if video.hwDebugContext {
glfw.WindowHint(glfw.OpenGLDebugContext, glfw.True)
}
}
// SetShouldClose sets the value of the close flag of the window.
func (video *Video) SetShouldClose(b bool) {
if video.Window == nil {
@@ -339,85 +90,37 @@ 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) {
if err := video.configureWithSize(384*2, 240*2, fullscreen); err != nil {
var width, height int
var m *glfw.Monitor
if fullscreen {
m = glfw.GetMonitors()[settings.Current.VideoMonitorIndex]
vms := m.GetVideoModes()
vm := vms[len(vms)-1]
width = vm.Width
height = vm.Height
} else {
width = 384 * 2
height = 240 * 2
}
var err error
video.Window, err = glfw.CreateWindow(width, height, "Ludo", m, nil)
if err != nil {
panic("Window creation failed:" + err.Error())
}
}
func (video *Video) configureWithSize(width, height int, fullscreen bool) error {
if fullscreen && video.windowedWidth != 0 {
width, height = video.windowedWidth, video.windowedHeight
}
video.configureWindowHints()
window, err := glfw.CreateWindow(width, height, video.title, nil, nil)
if err != nil {
return err
}
// GLFW may report a platform error without returning it through the Go binding.
if window == nil {
return errors.New("window creation failed")
}
// Keep the old context and its resources until the replacement exists.
if video.Window != nil {
video.RunContextDestroy()
video.DestroySharedHWContext()
video.Window.Destroy()
}
video.Window = window
// Object names from the old share group must not be reused in this context.
video.texID = 0
video.cachedFrameTexID = 0
video.cachedFrameW, video.cachedFrameH = 0, 0
video.cachedFrameValid = false
video.width, video.height = 0, 0
video.data = nil
video.needUpload = false
video.Window.MakeContextCurrent()
// Force a minimum size for the window.
video.Window.SetSizeLimits(160, 120, glfw.DontCare, glfw.DontCare)
video.SetFullscreen(fullscreen)
video.Window.SetInputMode(glfw.CursorMode, glfw.CursorHidden)
if err := initGL(); err != nil {
// Initialize Glow
if err := gl.Init(); err != nil {
panic(err)
}
@@ -425,15 +128,7 @@ func (video *Video) configureWithSize(width, height int, fullscreen bool) error
// LoadFont (fontfile, font scale, window width, window height)
fontPath := filepath.Join(settings.Current.AssetsDirectory, "font.ttf")
video.Font, err = LoadFont(fontPath, int32(36), fbw, fbh)
if err != nil {
panic(err)
}
video.FontSm, err = LoadFont(fontPath, int32(29), fbw, fbh)
if err != nil {
panic(err)
}
video.FontLg, err = LoadFont(fontPath, int32(50), fbw, fbh)
video.Font, err = LoadFont(fontPath, int32(36*2), fbw, fbh)
if err != nil {
panic(err)
}
@@ -447,9 +142,8 @@ func (video *Video) configureWithSize(width, height int, fullscreen bool) error
video.borderProgram = panicOnErr(newProgram(vertexShader, borderFragmentShader))
video.circleProgram = panicOnErr(newProgram(vertexShader, circleFragmentShader))
video.program = video.defaultProgram
video.UpdateFilter(settings.Current.VideoFilter)
gl.UseProgram(video.program)
textureUniform := gl.GetUniformLocation(video.program, gl.Str("Texture\x00"))
gl.Uniform1i(textureUniform, 0)
@@ -470,33 +164,28 @@ func (video *Video) configureWithSize(width, height int, fullscreen bool) error
gl.VertexAttribPointerWithOffset(texCoordAttrib, 2, gl.FLOAT, false, 4*4, 2*4)
// Some cores won't call SetPixelFormat, provide default values
video.ensurePixelFormatDefaults()
if video.Geom.MaxWidth == 0 || video.Geom.MaxHeight == 0 {
video.Geom.MaxWidth = video.Geom.BaseWidth
video.Geom.MaxHeight = video.Geom.BaseHeight
if video.pixFmt == 0 {
video.pixFmt = gl.UNSIGNED_SHORT_5_5_5_1
video.pixType = gl.BGRA
video.bpp = 2
}
video.texWidth = int32(video.Geom.MaxWidth)
video.texHeight = int32(video.Geom.MaxHeight)
if video.texWidth < 1 {
video.texWidth = 1
gl.GenTextures(1, &video.texID)
gl.ActiveTexture(gl.TEXTURE0)
if video.texID == 0 && state.Verbose {
log.Println("[Video]: Failed to create the vid texture")
}
if video.texHeight < 1 {
video.texHeight = 1
}
video.resetGameTexture(video.texWidth, video.texHeight)
gl.BindTexture(gl.TEXTURE_2D, video.texID)
video.UpdateFilter(settings.Current.VideoFilter)
video.coreRatioViewport(fbw, fbh, video.Geom.BaseWidth, video.Geom.BaseHeight)
video.coreRatioViewport(fbw, fbh)
bindVertexArray(0)
for e := gl.GetError(); e != gl.NO_ERROR; e = gl.NO_ERROR {
if e := gl.GetError(); e != gl.NO_ERROR {
log.Printf("[Video] OpenGL error: %d\n", e)
}
return nil
}
// UpdateFilter configures the game texture filter and shader. We currently
@@ -507,7 +196,6 @@ func (video *Video) configureWithSize(width, height int, fullscreen bool) error
// 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":
@@ -536,9 +224,8 @@ func (video *Video) UpdateFilter(filter string) {
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
gl.UseProgram(video.program)
gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("TextureSize\x00")), float32(video.texWidth), float32(video.texHeight))
gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("TextureSize\x00")), float32(video.width), float32(video.height))
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.
@@ -586,38 +273,9 @@ func (video *Video) ResetRot() {
video.rot = 0
}
// ResetCoreState clears per-core video state and destroys its GL context.
func (video *Video) ResetCoreState() {
video.DestroySharedHWContext()
video.MakeFrontendContextCurrent()
libretro.SetCurrentFramebufferValue(0)
video.ResetPitch()
video.ResetRot()
video.needUpload = false
video.data = nil
video.width = 0
video.height = 0
video.pixFmt = 0
video.pixType = 0
video.bpp = 0
video.ensurePixelFormatDefaults()
if video.texID != 0 {
video.resetGameTexture(1, 1)
video.UpdateFilter(settings.Current.VideoFilter)
}
video.texWidth = 0
video.texHeight = 0
if video.cachedFrameTexID != 0 {
gl.DeleteTextures(1, &video.cachedFrameTexID)
video.cachedFrameTexID = 0
}
video.cachedFrameW = 0
video.cachedFrameH = 0
video.cachedFrameValid = false
}
// gameViewport fits the game to the framebuffer and records its screenshot bounds.
func (video *Video) gameViewport(fbWidth, fbHeight int) (x, y, w, h float32) {
// coreRatioViewport configures the vertex array to display the game at the center of the window
// while preserving the original ascpect ratio of the game or core
func (video *Video) coreRatioViewport(fbWidth int, fbHeight int) (x, y, w, h float32) {
// Scale the content to fit in the viewport.
fbw := float32(fbWidth)
fbh := float32(fbHeight)
@@ -635,36 +293,11 @@ func (video *Video) gameViewport(fbWidth, fbHeight int) (x, y, w, h float32) {
w = fbw
}
// Keep centered, pixel-aligned bounds so the cached copy needs no resampling.
x = float32(int32((fbw - w) / 2))
y = float32(int32((fbh - h) / 2))
w = fbw - 2*x
h = fbh - 2*y
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]
// Place the content in the middle of the window.
x = (fbw - w) / 2
y = (fbh - h) / 2
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)
@@ -672,194 +305,51 @@ func (video *Video) coreRatioViewport(fbWidth, fbHeight, clipWidth, clipHeight i
return
}
// BeginFrontendFrame sets the state used by Ludo's game quad and menu.
// Core GL state lives in HWWindow and is never modified here.
func (video *Video) BeginFrontendFrame() {
// Render directly to the screen.
bindBackbuffer()
gl.DrawBuffer(gl.BACK)
gl.ReadBuffer(gl.BACK)
// Establish a frontend-owned GL baseline before drawing the game quad/menu.
gl.Disable(gl.DEPTH_TEST)
gl.Disable(gl.CULL_FACE)
gl.Disable(gl.DITHER)
gl.Disable(gl.SCISSOR_TEST)
gl.Disable(gl.STENCIL_TEST)
gl.Disable(gl.BLEND)
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
gl.BlendEquation(gl.FUNC_ADD)
gl.ColorMask(true, true, true, true)
gl.DepthMask(true)
video.ResizeViewport()
}
// ResizeViewport resizes the GL viewport to the framebuffer size
func (video *Video) ResizeViewport() {
fbw, fbh := video.Window.GetFramebufferSize()
gl.Viewport(0, 0, int32(fbw), int32(fbh))
if video.Font != nil {
video.Font.UpdateResolution(fbw, fbh)
}
if video.FontSm != nil {
video.FontSm.UpdateResolution(fbw, fbh)
}
if video.FontLg != nil {
video.FontLg.UpdateResolution(fbw, fbh)
}
}
// Render draws the current game frame using the frontend context/state.
// Render the current frame
func (video *Video) Render() {
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)
// Wait for a valid refresh, not merely an allocated HW framebuffer.
if video.width == 0 || video.height == 0 {
// Early return to not render the first frame of a newly loaded game with the
// previous game pitch. A sane pitch must be set by video.Refresh first.
if video.pitch == 0 {
return
}
video.uploadTexture()
fbw, fbh := video.Window.GetFramebufferSize()
_, _, w, h := video.coreRatioViewport(fbw, fbh, int(video.width), int(video.height))
_, _, w, h := video.coreRatioViewport(fbw, fbh)
gl.UseProgram(video.program)
if loc := gl.GetUniformLocation(video.program, gl.Str("color\x00")); loc >= 0 {
gl.Uniform4f(loc, 1, 1, 1, 1)
}
gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("OutputSize\x00")), w, h)
if state.Core.HWRenderCallback != nil {
gl.UniformMatrix4fv(gl.GetUniformLocation(video.program, gl.Str("MVP\x00")), 1, false, &video.orthoMat[0])
}
bindVertexArray(video.vao)
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
video.data = data // maybe need a full copy
}
func (video *Video) uploadTexture() {
@@ -867,31 +357,14 @@ func (video *Video) uploadTexture() {
return
}
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, video.texID)
if video.data != libretro.HWFrameBufferValid {
video.texWidth = video.width
video.texHeight = video.height
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, video.pitch/video.bpp)
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, video.texWidth, video.texHeight, 0, video.pixType, video.pixFmt, video.data)
}
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, video.pitch/video.bpp)
gl.UseProgram(video.program)
gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("TextureSize\x00")), float32(video.texWidth), float32(video.texHeight))
gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("TextureSize\x00")), float32(video.width), float32(video.height))
gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("InputSize\x00")), float32(video.width), float32(video.height))
gl.UseProgram(0)
video.needUpload = false
}
// CurrentFramebuffer returns the current FBO ID
func (video *Video) CurrentFramebuffer() uintptr {
return uintptr(video.fboID)
}
// ProcAddress returns the address of the proc from GLFW
func (video *Video) ProcAddress(procName string) uintptr {
return uintptr(glfw.GetProcAddress(procName))
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, video.width, video.height, 0, video.pixType, video.pixFmt, video.data)
}
// SetRotation rotates the game image as requested by the core
+15
View File
@@ -0,0 +1,15 @@
// +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)
}
+15
View File
@@ -0,0 +1,15 @@
// +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)
}