mirror of
https://github.com/libretro/ludo.git
synced 2026-11-09 03:09:35 -10:00
Compare commits
63
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5798e14105 | ||
|
|
99f215025b | ||
|
|
1bd4ad6986 | ||
|
|
4795525132 | ||
|
|
3edf65f9fd | ||
|
|
e01cc6bb86 | ||
|
|
3e084bf7f1 | ||
|
|
2bcbb1d24c | ||
|
|
7d48bb840b | ||
|
|
cb8649a129 | ||
|
|
d74969f50e | ||
|
|
a95767339e | ||
|
|
346dbd8a33 | ||
|
|
c95a3a7a14 | ||
|
|
8647e6f893 | ||
|
|
a43843bab1 | ||
|
|
6097d396df | ||
|
|
e4c5845b57 | ||
|
|
da0092ef53 | ||
|
|
d8afc6031a | ||
|
|
1e0c4a0966 | ||
|
|
e01a76b08d | ||
|
|
73d70cb1f9 | ||
|
|
8b8d49eef4 | ||
|
|
008a70c851 | ||
|
|
1d8f8e8fac | ||
|
|
1146cb6f99 | ||
|
|
dbfbe3ace5 | ||
|
|
e35cba220c | ||
|
|
f32af42ba9 | ||
|
|
827ae5461d | ||
|
|
40396a4b86 | ||
|
|
ff9a3c8f4b | ||
|
|
b8144699f6 | ||
|
|
42d43d63c3 | ||
|
|
9c5c3f5962 | ||
|
|
2dd9150510 | ||
|
|
d127589d50 | ||
|
|
05adbe4f6a | ||
|
|
ab13ef0ebc | ||
|
|
8f51dd3b2d | ||
|
|
4618fb63bf | ||
|
|
2d55e97431 | ||
|
|
c7030756e3 | ||
|
|
614c5c2ed9 | ||
|
|
466c2ef696 | ||
|
|
4914b16606 | ||
|
|
341ad35519 | ||
|
|
933c86b852 | ||
|
|
681dfbc6ab | ||
|
|
f241ef4995 | ||
|
|
6e4b4b634e | ||
|
|
87175ca350 | ||
|
|
7eef38cf2e | ||
|
|
dc56ebfff9 | ||
|
|
d3fd407e77 | ||
|
|
a7cf8d1f99 | ||
|
|
a7da8f7cb9 | ||
|
|
967b0da04f | ||
|
|
c6c0c9c819 | ||
|
|
d41ab5d9d7 | ||
|
|
7e794694f5 | ||
|
|
d861e3d47d |
No files matched your search
@@ -11,22 +11,19 @@ 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 mednafen_saturn,$(CORES))
|
||||
CORES := $(filter-out mupen64plus_next,$(CORES))
|
||||
CORES := $(filter-out picodrive,$(CORES))
|
||||
endif
|
||||
|
||||
DYLIBS = $(addprefix cores/, $(addsuffix _libretro.dylib,$(CORES)))
|
||||
|
||||
@@ -4,7 +4,7 @@ Ludo is a work in progress libretro frontend written in go.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/kivutar/ludo-assets/master/illustration.png" />
|
||||
|
||||
It 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)
|
||||
|
||||
|
||||
+37
-46
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/libretro/ludo/options"
|
||||
"github.com/libretro/ludo/patch"
|
||||
"github.com/libretro/ludo/savefiles"
|
||||
"github.com/libretro/ludo/settings"
|
||||
"github.com/libretro/ludo/state"
|
||||
"github.com/libretro/ludo/video"
|
||||
|
||||
@@ -35,7 +36,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,9 +52,11 @@ func Load(sofile string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vid.SetHWRenderContext(nil)
|
||||
|
||||
state.Core.SetEnvironment(environment)
|
||||
state.Core.Init()
|
||||
state.CoreSetSharedContext = false
|
||||
vid.SetHWRenderContext(nil)
|
||||
state.Core.SetVideoRefresh(vid.Refresh)
|
||||
state.Core.SetInputPoll(func() {})
|
||||
state.Core.SetInputState(input.State)
|
||||
@@ -220,15 +222,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 +255,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")
|
||||
}
|
||||
|
||||
@@ -267,13 +268,19 @@ func LoadGame(gamePath string) error {
|
||||
vid.SetTitle("Ludo - " + si.LibraryName)
|
||||
}
|
||||
|
||||
if state.Core.HWRenderCallback != nil {
|
||||
vid.SetHWRenderContext(state.Core.HWRenderCallback)
|
||||
vid.Reconfigure(settings.Current.VideoFullscreen)
|
||||
} else if vid.Window == nil {
|
||||
vid.Configure(settings.Current.VideoFullscreen)
|
||||
}
|
||||
|
||||
input.Init(vid)
|
||||
audio.Reconfigure(int32(avi.Timing.SampleRate))
|
||||
if state.Core.AudioCallback != nil {
|
||||
state.Core.AudioCallback.SetState(true)
|
||||
}
|
||||
|
||||
state.CoreRunning = true
|
||||
state.FastForward = false
|
||||
state.GamePath = gamePath
|
||||
|
||||
@@ -287,55 +294,39 @@ func LoadGame(gamePath string) error {
|
||||
savefiles.LoadSRAM()
|
||||
|
||||
if state.Core.HWRenderCallback != nil {
|
||||
if err := vid.ResetHWContext(); err != nil {
|
||||
Unload()
|
||||
return fmt.Errorf("create hardware context: %w", err)
|
||||
}
|
||||
vid.MakeHardwareContextCurrent()
|
||||
vid.InitFramebuffer()
|
||||
vid.PrepareCoreContext()
|
||||
state.Core.HWRenderCallback.ContextReset()
|
||||
}
|
||||
|
||||
state.CoreRunning = true
|
||||
|
||||
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)
|
||||
state.CoreSetSharedContext = false
|
||||
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
@@ -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")
|
||||
|
||||
+11
-17
@@ -144,28 +144,21 @@ func environment(cmd uint32, data unsafe.Pointer) bool {
|
||||
case libretro.EnvironmentSetAudioCallback:
|
||||
state.Core.SetAudioCallback(data)
|
||||
case libretro.EnvironmentSetHWRender:
|
||||
hw := libretro.SetHWRenderCallback(
|
||||
state.Core.HWRenderCallback = libretro.SetHWRenderCallback(
|
||||
data,
|
||||
vid.CurrentFramebuffer,
|
||||
vid.ProcAddress)
|
||||
if (hw.HWContextType != libretro.HWContextOpenGL && hw.HWContextType != libretro.HWContextOpenGLCore) || hw.ContextReset == nil {
|
||||
return false
|
||||
}
|
||||
vid.SetHWRenderContext(hw)
|
||||
if vid != nil && vid.Window != nil && !state.CoreRunning {
|
||||
if err := vid.ReconfigureHWContext(settings.Current.VideoFullscreen); err != nil {
|
||||
vid.SetHWRenderContext(state.Core.HWRenderCallback)
|
||||
log.Printf("[Env]: Could not create HW context %d (%d.%d): %v",
|
||||
hw.HWContextType, hw.VersionMajor, hw.VersionMinor, err)
|
||||
return false
|
||||
}
|
||||
state.MenuContextResetNeeded = true
|
||||
}
|
||||
state.Core.HWRenderCallback = hw
|
||||
log.Println("[Env]: HWContextType:", hw.HWContextType)
|
||||
log.Printf(
|
||||
"[Env]: HWContextType: %d Version: %d.%d Debug: %t",
|
||||
state.Core.HWRenderCallback.HWContextType,
|
||||
state.Core.HWRenderCallback.VersionMajor,
|
||||
state.Core.HWRenderCallback.VersionMinor,
|
||||
state.Core.HWRenderCallback.DebugContext,
|
||||
)
|
||||
return true
|
||||
case libretro.EnvironmentSetHWSharedContext:
|
||||
// All HW cores already get an isolated context shared with the frontend.
|
||||
state.CoreSetSharedContext = true
|
||||
log.Println("[Env]: SET_HW_SHARED_CONTEXT")
|
||||
return true
|
||||
case libretro.EnvironmentGetCanDupe:
|
||||
libretro.SetBool(data, true)
|
||||
@@ -176,6 +169,7 @@ func environment(cmd uint32, data unsafe.Pointer) bool {
|
||||
case libretro.EnvironmentGetSaveDirectory:
|
||||
return environmentGetSaveDirectory(data)
|
||||
case libretro.EnvironmentGetPrefferedHWRender:
|
||||
log.Println("[Env]: EnvironmentGetPrefferedHWRenderer:", libretro.HWContextOpenGL)
|
||||
libretro.SetUint(data, uint(libretro.HWContextOpenGL))
|
||||
case libretro.EnvironmentShutdown:
|
||||
vid.SetShouldClose(true)
|
||||
|
||||
@@ -7,14 +7,13 @@ 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/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
|
||||
)
|
||||
|
||||
@@ -26,7 +25,7 @@ require (
|
||||
github.com/bodgit/windows v1.0.1 // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/klauspost/compress v1.18.7 // indirect
|
||||
github.com/klauspost/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
|
||||
@@ -39,7 +38,7 @@ require (
|
||||
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
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
)
|
||||
|
||||
go 1.25.0
|
||||
|
||||
@@ -52,8 +52,6 @@ github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276 h1:IO5P06Pcj9K04d+l4nrf3c
|
||||
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=
|
||||
@@ -96,8 +94,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
|
||||
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.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.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
@@ -177,11 +175,10 @@ golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u0
|
||||
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/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
|
||||
golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
|
||||
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=
|
||||
@@ -259,8 +256,8 @@ 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/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
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=
|
||||
|
||||
+137
-24
@@ -8,34 +8,121 @@ package libretro
|
||||
#include <pthread.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <mach/semaphore.h>
|
||||
#include <mach/task.h>
|
||||
#include <mach/mach_init.h>
|
||||
#define SEM_T semaphore_t
|
||||
#define SEM_INIT(x) semaphore_create(mach_task_self(), &x, SYNC_POLICY_FIFO, 0);
|
||||
#define SEM_POST(x) semaphore_signal(x)
|
||||
#define SEM_WAIT(x) semaphore_wait(x)
|
||||
#else
|
||||
#include <semaphore.h>
|
||||
#define SEM_T sem_t
|
||||
#define SEM_INIT(x) sem_init(&x, 0, 0)
|
||||
#define SEM_POST(x) sem_post(&x)
|
||||
#define SEM_WAIT(x) sem_wait(&x)
|
||||
#endif
|
||||
|
||||
#if 0
|
||||
#define print_sema(...) (printf(__VA_ARGS__))
|
||||
#else
|
||||
#define print_sema(...) do {} while (0)
|
||||
#endif
|
||||
|
||||
enum {
|
||||
CMD_F,
|
||||
CMD_SERIALIZE,
|
||||
};
|
||||
|
||||
struct thread_cmd_t {
|
||||
int cmd;
|
||||
void* f;
|
||||
void* arg1;
|
||||
void* arg2;
|
||||
void* arg3;
|
||||
void* arg4;
|
||||
void* res;
|
||||
};
|
||||
|
||||
struct thread_cmd_t s_job;
|
||||
static pthread_t s_thread;
|
||||
static SEM_T s_sem_do;
|
||||
static SEM_T s_sem_done;
|
||||
static bool s_use_thread = false;
|
||||
static uintptr_t s_current_framebuffer = 0;
|
||||
|
||||
// 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) {
|
||||
@@ -75,7 +162,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 +170,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 +194,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) {
|
||||
|
||||
+53
-160
@@ -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);
|
||||
@@ -65,8 +63,6 @@ uintptr_t coreGetProcAddress_cgo(const char *sym);
|
||||
import "C"
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
"unsafe"
|
||||
)
|
||||
@@ -467,92 +463,18 @@ type (
|
||||
)
|
||||
|
||||
var (
|
||||
environment environmentFunc
|
||||
videoRefresh videoRefreshFunc
|
||||
audioSample audioSampleFunc
|
||||
audioSampleBatch audioSampleBatchFunc
|
||||
inputPoll inputPollFunc
|
||||
inputState inputStateFunc
|
||||
log logFunc
|
||||
getTimeUsec getTimeUsecFunc
|
||||
getProcAddress getProcAddressFunc
|
||||
releaseContext func()
|
||||
acquireContext func()
|
||||
coreCalls chan func()
|
||||
frontendCalls chan func()
|
||||
environment environmentFunc
|
||||
videoRefresh videoRefreshFunc
|
||||
audioSample audioSampleFunc
|
||||
audioSampleBatch audioSampleBatchFunc
|
||||
inputPoll inputPollFunc
|
||||
inputState inputStateFunc
|
||||
log logFunc
|
||||
getTimeUsec getTimeUsecFunc
|
||||
getCurrentFramebuffer getCurrentFramebufferFunc
|
||||
getProcAddress getProcAddressFunc
|
||||
)
|
||||
|
||||
// SetContextCallbacks hands the current graphics context between the calling
|
||||
// thread and the native core thread. Calls remain synchronous.
|
||||
func SetContextCallbacks(release, acquire func()) {
|
||||
releaseContext, acquireContext = release, acquire
|
||||
}
|
||||
|
||||
func coreReleaseContext() {
|
||||
if releaseContext != nil {
|
||||
releaseContext()
|
||||
}
|
||||
}
|
||||
|
||||
func coreAcquireContext() {
|
||||
if acquireContext != nil {
|
||||
acquireContext()
|
||||
}
|
||||
}
|
||||
|
||||
//export coreThreadLoop
|
||||
func coreThreadLoop() {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
for call := range coreCalls {
|
||||
call()
|
||||
}
|
||||
}
|
||||
|
||||
// Calls are synchronous, but must service callbacks on the waiting thread.
|
||||
// Pumping both directions also permits a callback to make a nested core call.
|
||||
func threadCall(calls, callbacks chan func(), f func()) {
|
||||
coreReleaseContext()
|
||||
done := make(chan struct{})
|
||||
calls <- func() {
|
||||
coreAcquireContext()
|
||||
f()
|
||||
coreReleaseContext()
|
||||
close(done)
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case callback := <-callbacks:
|
||||
callback()
|
||||
case <-done:
|
||||
coreAcquireContext()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The frontend calls from its locked OS thread; core callbacks can reenter here.
|
||||
func coreCall(f func()) {
|
||||
if coreCalls == nil || bool(C.cothread_is_current()) {
|
||||
f()
|
||||
return
|
||||
}
|
||||
threadCall(coreCalls, frontendCalls, f)
|
||||
}
|
||||
|
||||
func coreResult[T any](f func() T) (result T) {
|
||||
coreCall(func() { result = f() })
|
||||
return
|
||||
}
|
||||
|
||||
func frontendCall(f func()) {
|
||||
if coreCalls == nil || !bool(C.cothread_is_current()) {
|
||||
f()
|
||||
return
|
||||
}
|
||||
threadCall(frontendCalls, coreCalls, f)
|
||||
}
|
||||
|
||||
// Load dynamically loads a libretro core at the given path and returns a Core instance
|
||||
func Load(sofile string) (*Core, error) {
|
||||
core := Core{}
|
||||
@@ -563,13 +485,6 @@ func Load(sofile string) (*Core, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
coreCalls, frontendCalls = make(chan func()), make(chan func())
|
||||
if err := C.cothread_init(); err != 0 {
|
||||
coreCalls, frontendCalls = nil, nil
|
||||
DlClose(core.handle)
|
||||
return nil, fmt.Errorf("start core thread: %s", C.GoString(C.strerror(err)))
|
||||
}
|
||||
|
||||
core.symRetroInit = DlSym(core.handle, "retro_init")
|
||||
core.symRetroDeinit = DlSym(core.handle, "retro_deinit")
|
||||
core.symRetroAPIVersion = DlSym(core.handle, "retro_api_version")
|
||||
@@ -597,21 +512,18 @@ func Load(sofile string) (*Core, error) {
|
||||
|
||||
// Init takes care of the library global initialization
|
||||
func (core *Core) Init() {
|
||||
coreCall(func() { C.bridge_retro_init(core.symRetroInit) })
|
||||
C.bridge_retro_init(core.symRetroInit)
|
||||
}
|
||||
|
||||
// APIVersion returns the RETRO_API_VERSION.
|
||||
// Used to validate ABI compatibility when the API is revised.
|
||||
func (core *Core) APIVersion() uint {
|
||||
return coreResult(func() uint { return uint(C.bridge_retro_api_version(core.symRetroAPIVersion)) })
|
||||
return uint(C.bridge_retro_api_version(core.symRetroAPIVersion))
|
||||
}
|
||||
|
||||
// Deinit takes care of the library global deinitialization
|
||||
func (core *Core) Deinit() {
|
||||
coreCall(func() { C.bridge_retro_deinit(core.symRetroDeinit) })
|
||||
close(coreCalls)
|
||||
C.cothread_deinit()
|
||||
coreCalls, frontendCalls = nil, nil
|
||||
C.bridge_retro_deinit(core.symRetroDeinit)
|
||||
DlClose(core.handle)
|
||||
core.MemoryMap = nil
|
||||
environment = nil
|
||||
@@ -622,6 +534,7 @@ func (core *Core) Deinit() {
|
||||
inputState = nil
|
||||
log = nil
|
||||
getTimeUsec = nil
|
||||
getCurrentFramebuffer = nil
|
||||
getProcAddress = nil
|
||||
}
|
||||
|
||||
@@ -632,12 +545,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 +558,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 +576,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 +598,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 +607,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 +630,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 +639,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 +700,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 +708,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 +732,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,6 +767,11 @@ func coreGetTimeUsec() C.uint64_t {
|
||||
return C.uint64_t(getTimeUsec())
|
||||
}
|
||||
|
||||
//export coreGetCurrentFramebuffer
|
||||
func coreGetCurrentFramebuffer() C.uintptr_t {
|
||||
return C.uintptr_t(getCurrentFramebuffer())
|
||||
}
|
||||
|
||||
//export coreGetProcAddress
|
||||
func coreGetProcAddress(sym *C.char) C.uintptr_t {
|
||||
return C.uintptr_t(getProcAddress(C.GoString(sym)))
|
||||
@@ -1039,7 +939,7 @@ func (core *Core) SetFrameTimeCallback(data unsafe.Pointer) {
|
||||
ftc := &FrameTimeCallback{}
|
||||
ftc.Reference = int64(c.reference)
|
||||
ftc.Callback = func(usec int64) {
|
||||
coreCall(func() { C.bridge_retro_frame_time_callback(c.callback, C.retro_usec_t(usec)) })
|
||||
C.bridge_retro_frame_time_callback(c.callback, C.retro_usec_t(usec))
|
||||
}
|
||||
core.FrameTimeCallback = ftc
|
||||
}
|
||||
@@ -1054,17 +954,14 @@ func SetHWRenderCallback(
|
||||
hwrc := HWRenderCallback{}
|
||||
hwrc.HWContextType = uint32(c.context_type)
|
||||
|
||||
getCurrentFramebuffer = currentFramebuffer
|
||||
getProcAddress = procAddress
|
||||
SetCurrentFramebufferValue(currentFramebuffer())
|
||||
c.get_current_framebuffer = (C.retro_hw_get_current_framebuffer_t)(C.coreGetCurrentFramebuffer_cgo)
|
||||
c.get_proc_address = (C.retro_hw_get_proc_address_t)(C.coreGetProcAddress_cgo)
|
||||
|
||||
// The request may be stack-allocated by the core. Keep callback values, not c.
|
||||
reset, destroy := c.context_reset, c.context_destroy
|
||||
if reset != nil {
|
||||
hwrc.ContextReset = func() {
|
||||
coreCall(func() { C.bridge_retro_hw_context_reset(reset) })
|
||||
}
|
||||
hwrc.ContextReset = func() {
|
||||
C.bridge_retro_hw_context_reset((C.retro_hw_context_reset_t)(c.context_reset))
|
||||
}
|
||||
|
||||
hwrc.Depth = bool(c.depth)
|
||||
@@ -1073,12 +970,10 @@ func SetHWRenderCallback(
|
||||
hwrc.VersionMajor = uint(c.version_major)
|
||||
hwrc.VersionMinor = uint(c.version_minor)
|
||||
hwrc.CacheContext = bool(c.cache_context)
|
||||
if destroy != nil {
|
||||
hwrc.ContextDestroy = func() {
|
||||
coreCall(func() { C.bridge_retro_hw_context_destroy(destroy) })
|
||||
}
|
||||
}
|
||||
hwrc.DebugContext = bool(c.debug_context)
|
||||
hwrc.ContextDestroy = func() {
|
||||
C.bridge_retro_hw_context_destroy((C.retro_hw_context_reset_t)(c.context_destroy))
|
||||
}
|
||||
return &hwrc
|
||||
}
|
||||
|
||||
@@ -1087,10 +982,10 @@ func (core *Core) SetAudioCallback(data unsafe.Pointer) {
|
||||
c := *(*C.struct_retro_audio_callback)(data)
|
||||
auc := &AudioCallback{}
|
||||
auc.Callback = func() {
|
||||
coreCall(func() { C.bridge_retro_audio_callback(c.callback) })
|
||||
C.bridge_retro_audio_callback(c.callback)
|
||||
}
|
||||
auc.SetState = func(state bool) {
|
||||
coreCall(func() { C.bridge_retro_audio_set_state(c.set_state, C.bool(state)) })
|
||||
C.bridge_retro_audio_set_state(c.set_state, C.bool(state))
|
||||
}
|
||||
core.AudioCallback = auc
|
||||
}
|
||||
@@ -1098,15 +993,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 +1019,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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,43 +39,32 @@ 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()
|
||||
vid.BeginCoreFrame()
|
||||
state.Core.Run()
|
||||
vid.EndCoreFrame()
|
||||
if state.Core.FrameTimeCallback != nil {
|
||||
state.Core.FrameTimeCallback.Callback(state.Core.FrameTimeCallback.Reference)
|
||||
}
|
||||
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 +130,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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
+2
-2
@@ -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
-9
@@ -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.")
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
+11
-13
@@ -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.")
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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,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
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
+1
-9
@@ -34,12 +34,4 @@ 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
|
||||
var CoreSetSharedContext bool
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
-6
@@ -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)
|
||||
|
||||
-63
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
+20
-7
@@ -5,9 +5,25 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
"github.com/go-gl/mathgl/mgl32"
|
||||
)
|
||||
|
||||
func identityMatrix() [16]float32 {
|
||||
return [16]float32{
|
||||
1, 0, 0, 0,
|
||||
0, 1, 0, 0,
|
||||
0, 0, 1, 0,
|
||||
0, 0, 0, 1,
|
||||
}
|
||||
}
|
||||
|
||||
func orthoMatrix(bottomLeftOrigin bool) [16]float32 {
|
||||
m := identityMatrix()
|
||||
if bottomLeftOrigin {
|
||||
m[5] = -1
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func newProgram(vertexShaderSource, fragmentShaderSource string) (uint32, error) {
|
||||
vertexShader, err := compileShader(vertexShaderSource, gl.VERTEX_SHADER)
|
||||
if err != nil {
|
||||
@@ -23,7 +39,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)
|
||||
@@ -43,14 +58,12 @@ 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])
|
||||
identity := identityMatrix()
|
||||
gl.UniformMatrix4fv(mvp, 1, false, &identity[0])
|
||||
}
|
||||
gl.UseProgram(0)
|
||||
|
||||
@@ -60,7 +73,7 @@ func newProgram(vertexShaderSource, fragmentShaderSource string) (uint32, error)
|
||||
func compileShader(source string, shaderType uint32) (uint32, error) {
|
||||
shader := gl.CreateShader(shaderType)
|
||||
|
||||
csources, free := gl.Strs(glslVersion + source)
|
||||
csources, free := gl.Strs(source)
|
||||
gl.ShaderSource(shader, 1, csources, nil)
|
||||
free()
|
||||
gl.CompileShader(shader)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+461
-523
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,72 @@
|
||||
// +build darwin
|
||||
|
||||
package video
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
"github.com/libretro/ludo/state"
|
||||
)
|
||||
|
||||
// InitFramebuffer initializes and configures the video frame buffer based on
|
||||
// information 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, video.pixType, video.pixFmt, nil)
|
||||
|
||||
gl.GenFramebuffersEXT(1, &video.fboID)
|
||||
gl.BindFramebufferEXT(gl.FRAMEBUFFER_EXT, video.fboID)
|
||||
libretro.SetCurrentFramebufferValue(uintptr(video.fboID))
|
||||
gl.FramebufferTexture2DEXT(gl.FRAMEBUFFER_EXT, gl.COLOR_ATTACHMENT0_EXT, gl.TEXTURE_2D, video.texID, 0)
|
||||
|
||||
hw := state.Core.HWRenderCallback
|
||||
if hw.Depth {
|
||||
gl.GenRenderbuffersEXT(1, &video.rboID)
|
||||
gl.BindRenderbufferEXT(gl.RENDERBUFFER_EXT, video.rboID)
|
||||
format := gl.DEPTH_COMPONENT24
|
||||
if hw.Stencil {
|
||||
format = gl.DEPTH24_STENCIL8_EXT
|
||||
}
|
||||
gl.RenderbufferStorageEXT(gl.RENDERBUFFER_EXT, uint32(format), width, height)
|
||||
gl.BindRenderbufferEXT(gl.RENDERBUFFER_EXT, 0)
|
||||
|
||||
gl.FramebufferRenderbufferEXT(gl.FRAMEBUFFER_EXT, gl.DEPTH_ATTACHMENT_EXT, gl.RENDERBUFFER_EXT, video.rboID)
|
||||
if hw.Stencil {
|
||||
gl.FramebufferRenderbufferEXT(gl.FRAMEBUFFER_EXT, gl.STENCIL_ATTACHMENT_EXT, gl.RENDERBUFFER_EXT, video.rboID)
|
||||
}
|
||||
}
|
||||
|
||||
video.orthoMat = orthoMatrix(hw.BottomLeftOrigin)
|
||||
|
||||
if st := gl.CheckFramebufferStatusEXT(gl.FRAMEBUFFER_EXT); st != gl.FRAMEBUFFER_COMPLETE_EXT {
|
||||
log.Fatalf("[Video] Framebuffer is not complete. Error: %v\n", st)
|
||||
}
|
||||
|
||||
bindBackbuffer()
|
||||
gl.BindTexture(gl.TEXTURE_2D, 0)
|
||||
|
||||
gl.ClearColor(0, 0, 0, 1)
|
||||
gl.Clear(gl.COLOR_BUFFER_BIT)
|
||||
}
|
||||
|
||||
func bindBackbuffer() {
|
||||
gl.BindFramebufferEXT(gl.FRAMEBUFFER_EXT, 0)
|
||||
}
|
||||
|
||||
func genVertexArrays(n int32, arrays *uint32) {
|
||||
gl.GenVertexArraysAPPLE(n, arrays)
|
||||
}
|
||||
|
||||
func bindVertexArray(array uint32) {
|
||||
gl.BindVertexArrayAPPLE(array)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// +build !darwin
|
||||
|
||||
package video
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
"github.com/libretro/ludo/libretro"
|
||||
"github.com/libretro/ludo/state"
|
||||
)
|
||||
|
||||
// InitFramebuffer initializes and configures the video frame buffer based on
|
||||
// information 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, video.pixType, video.pixFmt, nil)
|
||||
|
||||
gl.GenFramebuffers(1, &video.fboID)
|
||||
gl.BindFramebuffer(gl.FRAMEBUFFER, video.fboID)
|
||||
libretro.SetCurrentFramebufferValue(uintptr(video.fboID))
|
||||
gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, video.texID, 0)
|
||||
|
||||
hw := state.Core.HWRenderCallback
|
||||
if hw.Depth {
|
||||
gl.GenRenderbuffers(1, &video.rboID)
|
||||
gl.BindRenderbuffer(gl.RENDERBUFFER, video.rboID)
|
||||
format := gl.DEPTH_COMPONENT24
|
||||
if hw.Stencil {
|
||||
format = gl.DEPTH24_STENCIL8
|
||||
}
|
||||
gl.RenderbufferStorage(gl.RENDERBUFFER, uint32(format), width, height)
|
||||
gl.BindRenderbuffer(gl.RENDERBUFFER, 0)
|
||||
|
||||
if hw.Stencil {
|
||||
gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, video.rboID)
|
||||
} else {
|
||||
gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, video.rboID)
|
||||
}
|
||||
}
|
||||
|
||||
video.orthoMat = orthoMatrix(hw.BottomLeftOrigin)
|
||||
|
||||
if st := gl.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE {
|
||||
log.Fatalf("[Video] Framebuffer is not complete. Error: %v\n", st)
|
||||
}
|
||||
|
||||
bindBackbuffer()
|
||||
gl.BindTexture(gl.TEXTURE_2D, 0)
|
||||
|
||||
gl.ClearColor(0, 0, 0, 1)
|
||||
gl.Clear(gl.COLOR_BUFFER_BIT)
|
||||
}
|
||||
|
||||
func bindBackbuffer() {
|
||||
gl.BindFramebuffer(gl.FRAMEBUFFER, 0)
|
||||
}
|
||||
|
||||
func genVertexArrays(n int32, arrays *uint32) {
|
||||
gl.GenVertexArrays(n, arrays)
|
||||
}
|
||||
|
||||
func bindVertexArray(array uint32) {
|
||||
gl.BindVertexArray(array)
|
||||
}
|
||||
Reference in new issue
Block a user