Compare commits

..
4 Commits
Author SHA1 Message Date
dependabot[bot] 7a2dc452c7 Bump github.com/klauspost/compress from 1.18.5 to 1.18.7 (#598)
Bumps [github.com/klauspost/compress](https://github.com/klauspost/compress) from 1.18.5 to 1.18.7.
- [Release notes](https://github.com/klauspost/compress/releases)
- [Commits](https://github.com/klauspost/compress/compare/v1.18.5...v1.18.7)

---
updated-dependencies:
- dependency-name: github.com/klauspost/compress
  dependency-version: 1.18.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-22 14:59:26 +02:00
Jean-André SantoniandHigor Eurípedes c47ad16a70 Support GL cores (#91)
* Improve the libretro binding to support GL cores

* Start implementing InitFramebuffer

* TODO

* Try to implement ProcAddress

* Fix display of non gl cores

* Implement more of InitFramebuffer

* Move InitFramebuffer and call HWRenderCallback.ContextReset

* Comment

* Call vid.InitFramebuffer and Core.HWRenderCallback.ContextReset during LoadGame

* Remove useless code

* List hardware contexts in the libretro binding

* Update dependency for GetProcAddress

* Fix framebuffer creation

* Fix gl.Clear

* Cast C.retro_hw_context_reset_t

* Set c.get_current_framebuffer and c.get_proc_address properly

* Lint fix

* Fix compat with non GL cores

* Debug

* Fix an important memory bug

* Various rendering improvements

* Small fix

* Active texture fixes

* Unbind vertex array

* OpenGL core support improvements (#308)

* Fix sample batch callback and OpenGL core support

* Fix texture/framebuffer allocation and clipping

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

* Fix context destruction

Fixes Flycast

* Fix sharp-bilinear and zfast-crt filters (#311)

TextureSize uniform was receiving the input size instead of the actual texture
size.

* Call gl.TexSubImage2D after binding the texture

* Fix shader live update

* Temporarily fix tests

* Fix FBO on OSX (#313)

* Fix FBO on OSX

* Remove useless code

* Use GL v2.1 for fonts drawing

* Fix fonts on OSX with GL 2.1

* Rename

* Same thing for GenVertexArrays

* Make fl0w work on OSX

* Implement libretro.EnvironmentGetPrefferedHWRenderer

* Uncomment gl.Enable(gl.TEXTURE_2D)

* Fixes

* Fix

* Fix

* Forgot to use program before get uniform

* Fix swanstation

* Start with a clean context

* Return the fb from C

* Shared context

* Reconfigure hints

* Harden texture size

* Fix fullscreen toggle on flycast

* Reduce font.go churn

* Reduce font.go churn

* Fix cleaning the display after closing the core

* Fix order of destoy

* Fix dolphin

* Launch more GL cores (#591)

* lrps2 booting

* Fix viewport

* Fix sampler

* Refactor

* Fix screenshots

* Fix dolphin screenshots

* Fixes

* Fix launching swanstation with -L

* Isolate core GL contexts

* Fix software frame uploads

* Fix libco

* Simplify screenshot sizing

* Add more GL cores

* Clean up frame capture

* Fix macOS GL profiles

* Preserve contexts on fullscreen toggle

* Fix screenshot cropping after resize

* Fix texture state on core switches

* Handle unsupported GL contexts

* Save states despite thumbnail errors

* Fix core loading error check

* Fix notification scaling

* Handle empty core options

---------

Co-authored-by: Higor Eurípedes <[email protected]>
2026-10-22 14:58:24 +02:00
Jean-André Santoni 13347e0051 Fix CI (#597) 2026-10-19 15:45:44 +02:00
dependabot[bot] 31951498f7 Bump golang.org/x/image from 0.39.0 to 0.41.0 (#593)
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.39.0 to 0.41.0.
- [Commits](https://github.com/golang/image/compare/v0.39.0...v0.41.0)

---
updated-dependencies:
- dependency-name: golang.org/x/image
  dependency-version: 0.41.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-19 23:16:25 +07:00
37 changed files with 1423 additions and 1006 deletions

No files matched your search

+12 -9
View File
@@ -11,19 +11,22 @@ ifeq ($(OS), Linux)
BUNDLENAME = $(APP)-$(OS)-$(DISPDRIVER)-$(ARCH)-$(VERSION)
endif
CORES = atari800 bluemsx swanstation fbneo fceumm gambatte gearsystem genesis_plus_gx handy lutro mednafen_ngp mednafen_pce mednafen_pce_fast mednafen_pcfx mednafen_psx mednafen_saturn mednafen_supergrafx mednafen_vb mednafen_wswan mgba melonds mupen64plus_next np2kai o2em pcsx_rearmed picodrive pokemini prosystem snes9x stella2014 vecx virtualjaguar
CORES = ppsspp dolphin atari800 bluemsx citra swanstation fbneo fceumm flycast gambatte gearsystem genesis_plus_gx handy lutro mednafen_ngp mednafen_pce mednafen_pce_fast mednafen_pcfx mednafen_psx mednafen_saturn mednafen_supergrafx mednafen_vb mednafen_wswan mgba melonds mupen64plus_next np2kai o2em pcsx2 pcsx_rearmed picodrive pokemini prosystem snes9x stella2014 vecx virtualjaguar
ifeq ($(ARCH), arm)
CORES := $(filter-out swanstation,$(CORES))
CORES := $(filter-out mupen64plus_next,$(CORES))
CORES := $(filter-out mednafen_pcfx,$(CORES))
CORES := $(filter-out mednafen_saturn,$(CORES))
CORES := $(filter-out melonds,$(CORES))
CORES := $(filter-out citra dolphin flycast mednafen_pcfx mednafen_saturn melonds mupen64plus_next pcsx2 ppsspp swanstation,$(CORES))
endif
ifeq ($(ARCH), arm64)
CORES := $(filter-out mednafen_saturn,$(CORES))
CORES := $(filter-out mupen64plus_next,$(CORES))
CORES := $(filter-out picodrive,$(CORES))
CORES := $(filter-out pcsx2,$(CORES))
ifeq ($(OS), Linux)
CORES := $(filter-out mupen64plus_next,$(CORES))
endif
endif
ifeq ($(OS), OSX)
CORES := $(filter-out pcsx2,$(CORES))
ifeq ($(ARCH), x86_64)
CORES := $(filter-out citra,$(CORES))
endif
endif
DYLIBS = $(addprefix cores/, $(addsuffix _libretro.dylib,$(CORES)))
+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 is able to launch most non GL libretro cores.
It supports software-rendered and desktop OpenGL libretro cores. Hardware-rendered cores may require a newer OpenGL version than the frontend itself.
It works on OSX, Linux, Linux ARM and Windows. You can download releases [here](https://github.com/libretro/ludo/releases)
+46 -37
View File
@@ -20,7 +20,6 @@ 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"
@@ -36,6 +35,7 @@ var Options *options.Options
// Call Init before calling other functions of this package.
func Init(v *video.Video) {
vid = v
libretro.SetContextCallbacks(v.ContextCallbacks())
}
// Load loads a libretro core
@@ -52,11 +52,9 @@ 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)
@@ -222,16 +220,17 @@ func LoadGame(gamePath string) error {
return err
}
// If we're loading a new game on the same core, save the RAM of the previous
// game before closing it.
if state.GamePath != gamePath {
UnloadGame()
}
if state.Core == nil {
return errors.New("no core loaded")
}
// Reload the core to fully tear down the previous game and its GL context.
if state.CoreRunning {
if err := Load(state.CorePath); err != nil {
return err
}
}
si := state.Core.GetSystemInfo()
gi, err := getGameInfo(gamePath, si.BlockExtract)
@@ -255,7 +254,7 @@ func LoadGame(gamePath string) error {
ok := state.Core.LoadGame(*gi)
if !ok {
state.CoreRunning = false
Unload()
return errors.New("failed to load the game")
}
@@ -268,19 +267,13 @@ 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
@@ -294,39 +287,55 @@ func LoadGame(gamePath string) error {
savefiles.LoadSRAM()
if state.Core.HWRenderCallback != nil {
vid.MakeHardwareContextCurrent()
vid.InitFramebuffer()
vid.PrepareCoreContext()
state.Core.HWRenderCallback.ContextReset()
if err := vid.ResetHWContext(); err != nil {
Unload()
return fmt.Errorf("create hardware context: %w", err)
}
}
state.CoreRunning = true
return nil
}
// Unload unloads a libretro core
// Unload closes the game and core before destroying their GL context.
func Unload() {
if state.Core != nil {
UnloadGame()
savefiles.SaveSRAM()
vid.RunContextDestroy()
vid.BeginCore()
if state.CoreRunning {
state.Core.UnloadGame()
}
state.Core.Deinit()
vid.EndCore()
vid.ResetCoreState()
state.GamePath = ""
state.CoreRunning = false
state.CorePath = ""
state.Core = nil
state.CoreSetSharedContext = false
vid.SetHWRenderContext(nil)
Options = nil
}
}
// UnloadGame unloads a game.
func UnloadGame() {
if state.CoreRunning {
savefiles.SaveSRAM()
state.Core.UnloadGame()
state.GamePath = ""
state.CoreRunning = false
vid.ResetPitch()
vid.ResetRot()
}
// Reset resets the game using its hardware context, if any.
func Reset() {
vid.BeginCore()
defer vid.EndCore()
state.Core.Reset()
}
// Serialize captures core state with the core's GL context current.
func Serialize() ([]byte, error) {
vid.BeginCore()
defer vid.EndCore()
return state.Core.Serialize(state.Core.SerializeSize())
}
// Unserialize restores core state with the core's GL context current.
func Unserialize(data []byte) error {
vid.BeginCore()
defer vid.EndCore()
return state.Core.Unserialize(data, uint(len(data)))
}
// getGameInfo opens a rom and return the libretro.GameInfo needed to launch it
+37 -7
View File
@@ -1,10 +1,10 @@
package core
import (
"log"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
@@ -15,6 +15,34 @@ import (
"github.com/libretro/ludo/video"
)
var mainThread = make(chan func())
func init() {
// GLFW initialization and input setup must run on the main OS thread.
runtime.LockOSThread()
}
func TestMain(m *testing.M) {
var code int
go func() {
code = m.Run()
close(mainThread)
}()
for f := range mainThread {
f()
}
os.Exit(code)
}
func onMainThread(f func()) {
done := make(chan struct{})
mainThread <- func() {
f()
close(done)
}
<-done
}
type WindowMock struct{}
func (m WindowMock) GetFramebufferSize() (width, height int) { return 320, 240 }
@@ -201,17 +229,21 @@ func Test_coreLoadGame(t *testing.T) {
Init(&video.Video{})
if err := glfw.Init(); err != nil {
log.Fatalln("failed to initialize glfw")
var initErr error
onMainThread(func() { initErr = glfw.Init() })
if initErr != nil {
t.Fatalf("glfw.Init() error = %v", initErr)
}
t.Cleanup(glfw.Terminate)
t.Cleanup(func() { onMainThread(glfw.Terminate) })
if err := Load("testdata/vecx_libretro" + ext); err != nil {
t.Fatalf("Load() error = %v", err)
}
var loadGameErr error
got := utils.CaptureOutput(func() { loadGameErr = LoadGame("testdata/Polar Rescue (USA).vec") })
got := utils.CaptureOutput(func() {
onMainThread(func() { loadGameErr = LoadGame("testdata/Polar Rescue (USA).vec") })
})
if loadGameErr != nil {
t.Fatalf("LoadGame() error = %v", loadGameErr)
}
@@ -235,7 +267,6 @@ func Test_coreLoadGame(t *testing.T) {
}
})
UnloadGame()
Unload()
t.Run("Global state should be cleared by Unload", func(t *testing.T) {
@@ -252,7 +283,6 @@ func Test_coreLoadGame(t *testing.T) {
}
func Test_coreLoadGameWithoutCore(t *testing.T) {
UnloadGame()
Unload()
err := LoadGame("testdata/Polar Rescue (USA).vec")
+17 -11
View File
@@ -144,21 +144,28 @@ func environment(cmd uint32, data unsafe.Pointer) bool {
case libretro.EnvironmentSetAudioCallback:
state.Core.SetAudioCallback(data)
case libretro.EnvironmentSetHWRender:
state.Core.HWRenderCallback = libretro.SetHWRenderCallback(
hw := libretro.SetHWRenderCallback(
data,
vid.CurrentFramebuffer,
vid.ProcAddress)
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,
)
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:
state.CoreSetSharedContext = true
log.Println("[Env]: SET_HW_SHARED_CONTEXT")
// All HW cores already get an isolated context shared with the frontend.
return true
case libretro.EnvironmentGetCanDupe:
libretro.SetBool(data, true)
@@ -169,7 +176,6 @@ 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)
+4 -3
View File
@@ -7,13 +7,14 @@ 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.39.0
golang.org/x/image v0.41.0
golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b
)
@@ -25,7 +26,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.5 // indirect
github.com/klauspost/compress v1.18.7 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/mikelolasagasti/xz v1.0.1 // indirect
github.com/minio/minlz v1.0.1 // indirect
@@ -38,7 +39,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.36.0 // indirect
golang.org/x/text v0.37.0 // indirect
)
go 1.25.0
+9 -6
View File
@@ -52,6 +52,8 @@ 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=
@@ -94,8 +96,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.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw=
github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
@@ -175,10 +177,11 @@ 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.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -256,8 +259,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.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
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=
+24 -137
View File
@@ -8,121 +8,34 @@ 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;
void* emu_thread_loop(void *a0) {
print_sema("begin thread\n");
SEM_POST(s_sem_done);
print_sema("signal thread\n");
while (1) {
print_sema("wait do\n");
SEM_WAIT(s_sem_do);
print_sema("do\n");
switch (s_job.cmd) {
case CMD_F:
((void (*)(void))s_job.f)();
break;
case CMD_SERIALIZE: {
bool res = ((bool (*)(void*, size_t))s_job.f)(s_job.arg1, *(size_t*)s_job.arg2);
*(bool*)s_job.res = res;
break;
}
default:
break;
}
print_sema("signal done\n");
SEM_POST(s_sem_done);
}
// Go can track libco's alternate callback stacks only on a C-created thread.
static void *emu_thread_loop(void *unused) {
void coreThreadLoop();
coreThreadLoop();
return NULL;
}
void thread_sync() {
// Fire the job
print_sema("signal do\n");
SEM_POST(s_sem_do);
// Wait the result
print_sema("wait done\n");
SEM_WAIT(s_sem_done);
print_sema("done\n");
int cothread_init() {
return pthread_create(&s_thread, NULL, emu_thread_loop, NULL);
}
void run_wrapper(void *f) {
if (s_use_thread) {
s_job.cmd = CMD_F;
s_job.f = f;
thread_sync();
} else {
((void (*)(void))f)();
}
void cothread_deinit() {
pthread_join(s_thread, NULL);
}
void cothread_init() {
s_use_thread = true;
SEM_INIT(s_sem_do);
SEM_INIT(s_sem_done);
print_sema("create thread\n");
pthread_create(&s_thread, NULL, emu_thread_loop, NULL);
print_sema("wait thread\n");
SEM_WAIT(s_sem_done);
bool cothread_is_current() {
return pthread_equal(pthread_self(), s_thread);
}
void bridge_retro_init(void *f) {
run_wrapper(f);
((void (*)(void))f)();
}
void bridge_retro_deinit(void *f) {
run_wrapper(f);
((void (*)(void))f)();
}
unsigned bridge_retro_api_version(void *f) {
@@ -162,7 +75,7 @@ void bridge_retro_set_environment(void *f, void *callback) {
}
void bridge_retro_set_video_refresh(void *f, void *callback) {
((bool (*)(retro_video_refresh_t))f)((retro_video_refresh_t)callback);
((void (*)(retro_video_refresh_t))f)((retro_video_refresh_t)callback);
}
void bridge_retro_set_controller_port_device(void *f, unsigned port, unsigned device) {
@@ -170,23 +83,23 @@ void bridge_retro_set_controller_port_device(void *f, unsigned port, unsigned de
}
void bridge_retro_set_input_poll(void *f, void *callback) {
((bool (*)(retro_input_poll_t))f)((retro_input_poll_t)callback);
((void (*)(retro_input_poll_t))f)((retro_input_poll_t)callback);
}
void bridge_retro_set_input_state(void *f, void *callback) {
((bool (*)(retro_input_state_t))f)((retro_input_state_t)callback);
((void (*)(retro_input_state_t))f)((retro_input_state_t)callback);
}
void bridge_retro_set_audio_sample(void *f, void *callback) {
((bool (*)(retro_audio_sample_t))f)((retro_audio_sample_t)callback);
((void (*)(retro_audio_sample_t))f)((retro_audio_sample_t)callback);
}
void bridge_retro_set_audio_sample_batch(void *f, void *callback) {
((bool (*)(retro_audio_sample_batch_t))f)((retro_audio_sample_batch_t)callback);
((void (*)(retro_audio_sample_batch_t))f)((retro_audio_sample_batch_t)callback);
}
bool bridge_retro_load_game(void *f, struct retro_game_info *gi) {
return ((bool (*)(struct retro_game_info *))f)(gi);
return ((bool (*)(const struct retro_game_info *))f)(gi);
}
size_t bridge_retro_serialize_size(void *f) {
@@ -194,49 +107,23 @@ size_t bridge_retro_serialize_size(void *f) {
}
bool bridge_retro_serialize(void *f, void *data, size_t size) {
if (s_use_thread) {
bool res;
s_job.cmd = CMD_SERIALIZE;
s_job.f = f;
s_job.arg1 = data;
s_job.arg2 = &size;
s_job.res = &res;
thread_sync();
return s_job.res;
} else {
return ((bool (*)(void*, size_t))f)(data, size);
}
return ((bool (*)(void*, size_t))f)(data, size);
}
bool bridge_retro_unserialize(void *f, void *data, size_t size) {
if (s_use_thread) {
bool res;
s_job.cmd = CMD_SERIALIZE; // Same command format for both serialize & unserialize
s_job.f = f;
s_job.arg1 = data;
s_job.arg2 = &size;
s_job.res = &res;
thread_sync();
return s_job.res;
} else {
return ((bool (*)(void*, size_t))f)(data, size);
}
return ((bool (*)(const void*, size_t))f)(data, size);
}
void bridge_retro_unload_game(void *f) {
run_wrapper(f);
((void (*)(void))f)();
}
void bridge_retro_run(void *f) {
run_wrapper(f);
((void (*)(void))f)();
}
void bridge_retro_reset(void *f) {
run_wrapper(f);
((void (*)(void))f)();
}
size_t bridge_retro_get_memory_size(void *f, unsigned id) {
+160 -53
View File
@@ -14,7 +14,9 @@ package libretro
#include <stdio.h>
#include <string.h>
void cothread_init();
int cothread_init();
void cothread_deinit();
bool cothread_is_current();
void bridge_retro_init(void *f);
void bridge_retro_deinit(void *f);
@@ -63,6 +65,8 @@ uintptr_t coreGetProcAddress_cgo(const char *sym);
import "C"
import (
"errors"
"fmt"
"runtime"
"strings"
"unsafe"
)
@@ -463,18 +467,92 @@ type (
)
var (
environment environmentFunc
videoRefresh videoRefreshFunc
audioSample audioSampleFunc
audioSampleBatch audioSampleBatchFunc
inputPoll inputPollFunc
inputState inputStateFunc
log logFunc
getTimeUsec getTimeUsecFunc
getCurrentFramebuffer getCurrentFramebufferFunc
getProcAddress getProcAddressFunc
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()
)
// 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{}
@@ -485,6 +563,13 @@ 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")
@@ -512,18 +597,21 @@ func Load(sofile string) (*Core, error) {
// Init takes care of the library global initialization
func (core *Core) Init() {
C.bridge_retro_init(core.symRetroInit)
coreCall(func() { C.bridge_retro_init(core.symRetroInit) })
}
// APIVersion returns the RETRO_API_VERSION.
// Used to validate ABI compatibility when the API is revised.
func (core *Core) APIVersion() uint {
return uint(C.bridge_retro_api_version(core.symRetroAPIVersion))
return coreResult(func() uint { return uint(C.bridge_retro_api_version(core.symRetroAPIVersion)) })
}
// Deinit takes care of the library global deinitialization
func (core *Core) Deinit() {
C.bridge_retro_deinit(core.symRetroDeinit)
coreCall(func() { C.bridge_retro_deinit(core.symRetroDeinit) })
close(coreCalls)
C.cothread_deinit()
coreCalls, frontendCalls = nil, nil
DlClose(core.handle)
core.MemoryMap = nil
environment = nil
@@ -534,7 +622,6 @@ func (core *Core) Deinit() {
inputState = nil
log = nil
getTimeUsec = nil
getCurrentFramebuffer = nil
getProcAddress = nil
}
@@ -545,12 +632,12 @@ func (core *Core) Deinit() {
// a frame if GET_CAN_DUPE returns true.
// In this case, the video callback can take a NULL argument for data.
func (core *Core) Run() {
C.bridge_retro_run(core.symRetroRun)
coreCall(func() { C.bridge_retro_run(core.symRetroRun) })
}
// Reset resets the current game.
func (core *Core) Reset() {
C.bridge_retro_reset(core.symRetroReset)
coreCall(func() { C.bridge_retro_reset(core.symRetroReset) })
}
// GetSystemInfo returns statically known system info. Pointers provided in *info
@@ -558,7 +645,7 @@ func (core *Core) Reset() {
// Can be called at any time, even before retro_init().
func (core *Core) GetSystemInfo() SystemInfo {
rsi := C.struct_retro_system_info{}
C.bridge_retro_get_system_info(core.symRetroGetSystemInfo, &rsi)
coreCall(func() { C.bridge_retro_get_system_info(core.symRetroGetSystemInfo, &rsi) })
return SystemInfo{
LibraryName: C.GoString(rsi.library_name),
LibraryVersion: C.GoString(rsi.library_version),
@@ -576,7 +663,7 @@ func (core *Core) GetSystemInfo() SystemInfo {
// desire a particular aspect ratio.
func (core *Core) GetSystemAVInfo() SystemAVInfo {
avi := C.struct_retro_system_av_info{}
C.bridge_retro_get_system_av_info(core.symRetroGetSystemAVInfo, &avi)
coreCall(func() { C.bridge_retro_get_system_av_info(core.symRetroGetSystemAVInfo, &avi) })
return SystemAVInfo{
Geometry: GameGeometry{
AspectRatio: float64(avi.geometry.aspect_ratio),
@@ -598,7 +685,7 @@ func (core *Core) LoadGame(gi GameInfo) bool {
rgi.path = C.CString(gi.Path)
rgi.size = C.size_t(gi.Size)
rgi.data = gi.Data
return bool(C.bridge_retro_load_game(core.symRetroLoadGame, &rgi))
return coreResult(func() bool { return bool(C.bridge_retro_load_game(core.symRetroLoadGame, &rgi)) })
}
// SerializeSize returns the amount of data the implementation requires to serialize
@@ -607,18 +694,18 @@ func (core *Core) LoadGame(gi GameInfo) bool {
// returned size is never allowed to be larger than a previous returned
// value, to ensure that the frontend can allocate a save state buffer once.
func (core *Core) SerializeSize() uint {
return uint(C.bridge_retro_serialize_size(core.symRetroSerializeSize))
return coreResult(func() uint { return uint(C.bridge_retro_serialize_size(core.symRetroSerializeSize)) })
}
// Serialize serializes internal state and returns the state as a byte slice.
func (core *Core) Serialize(size uint) ([]byte, error) {
data := C.malloc(C.size_t(size))
ok := bool(C.bridge_retro_serialize(core.symRetroSerialize, data, C.size_t(size)))
defer C.free(data)
ok := coreResult(func() bool { return bool(C.bridge_retro_serialize(core.symRetroSerialize, data, C.size_t(size))) })
if !ok {
return nil, errors.New("retro_serialize failed")
}
bytes := C.GoBytes(data, C.int(size))
C.free(data)
return bytes, nil
}
@@ -630,7 +717,9 @@ func (core *Core) Unserialize(bytes []byte, size uint) error {
if uint(len(bytes)) > size {
size = uint(len(bytes))
}
ok := bool(C.bridge_retro_unserialize(core.symRetroUnserialize, unsafe.Pointer(&bytes[0]), C.size_t(size)))
ok := coreResult(func() bool {
return bool(C.bridge_retro_unserialize(core.symRetroUnserialize, unsafe.Pointer(&bytes[0]), C.size_t(size)))
})
if !ok {
return errors.New("retro_unserialize failed")
}
@@ -639,49 +728,51 @@ func (core *Core) Unserialize(bytes []byte, size uint) error {
// UnloadGame unloads a currently loaded game
func (core *Core) UnloadGame() {
C.bridge_retro_unload_game(core.symRetroUnloadGame)
coreCall(func() { C.bridge_retro_unload_game(core.symRetroUnloadGame) })
}
// SetEnvironment sets the environment callback.
// Must be called before Init
func (core *Core) SetEnvironment(f environmentFunc) {
environment = f
C.bridge_retro_set_environment(core.symRetroSetEnvironment, C.coreEnvironment_cgo)
coreCall(func() { C.bridge_retro_set_environment(core.symRetroSetEnvironment, C.coreEnvironment_cgo) })
}
// SetVideoRefresh sets the video refresh callback.
// Must be set before the first Run call
func (core *Core) SetVideoRefresh(f videoRefreshFunc) {
videoRefresh = f
C.bridge_retro_set_video_refresh(core.symRetroSetVideoRefresh, C.coreVideoRefresh_cgo)
coreCall(func() { C.bridge_retro_set_video_refresh(core.symRetroSetVideoRefresh, C.coreVideoRefresh_cgo) })
}
// SetAudioSample sets the audio sample callback.
// Must be set before the first Run call
func (core *Core) SetAudioSample(f audioSampleFunc) {
audioSample = f
C.bridge_retro_set_audio_sample(core.symRetroSetAudioSample, C.coreAudioSample_cgo)
coreCall(func() { C.bridge_retro_set_audio_sample(core.symRetroSetAudioSample, C.coreAudioSample_cgo) })
}
// SetAudioSampleBatch sets the audio sample batch callback.
// Must be set before the first Run call
func (core *Core) SetAudioSampleBatch(f audioSampleBatchFunc) {
audioSampleBatch = f
C.bridge_retro_set_audio_sample_batch(core.symRetroSetAudioSampleBatch, C.coreAudioSampleBatch_cgo)
coreCall(func() {
C.bridge_retro_set_audio_sample_batch(core.symRetroSetAudioSampleBatch, C.coreAudioSampleBatch_cgo)
})
}
// SetInputPoll sets the input poll callback.
// Must be set before the first Run call
func (core *Core) SetInputPoll(f inputPollFunc) {
inputPoll = f
C.bridge_retro_set_input_poll(core.symRetroSetInputPoll, C.coreInputPoll_cgo)
coreCall(func() { C.bridge_retro_set_input_poll(core.symRetroSetInputPoll, C.coreInputPoll_cgo) })
}
// SetInputState sets the input state callback.
// Must be set before the first Run call
func (core *Core) SetInputState(f inputStateFunc) {
inputState = f
C.bridge_retro_set_input_state(core.symRetroSetInputState, C.coreInputState_cgo)
coreCall(func() { C.bridge_retro_set_input_state(core.symRetroSetInputState, C.coreInputState_cgo) })
}
// BindLogCallback binds f to the log callback
@@ -700,7 +791,9 @@ func (core *Core) BindPerfCallback(data unsafe.Pointer, f getTimeUsecFunc) {
// SetControllerPortDevice sets the device type attached to a controller port
func (core *Core) SetControllerPortDevice(port uint, device uint32) {
C.bridge_retro_set_controller_port_device(core.symRetroSetControllerPortDevice, C.unsigned(port), C.unsigned(device))
coreCall(func() {
C.bridge_retro_set_controller_port_device(core.symRetroSetControllerPortDevice, C.unsigned(port), C.unsigned(device))
})
}
//export coreEnvironment
@@ -708,6 +801,12 @@ func coreEnvironment(cmd C.unsigned, data unsafe.Pointer) bool {
if environment == nil {
return false
}
// Only window operations need the frontend thread. Routine queries stay here.
if cmd == C.RETRO_ENVIRONMENT_SET_HW_RENDER || cmd == C.RETRO_ENVIRONMENT_SHUTDOWN {
var result bool
frontendCall(func() { result = environment(uint32(cmd), data) })
return result
}
return environment(uint32(cmd), data)
}
@@ -732,6 +831,12 @@ func coreInputState(port C.unsigned, device C.unsigned, index C.unsigned, id C.u
if inputState == nil {
return 0
}
// Mouse state is queried through GLFW rather than the polled input snapshot.
if device == C.RETRO_DEVICE_MOUSE {
var result C.int16_t
frontendCall(func() { result = C.int16_t(inputState(uint(port), uint32(device), uint(index), uint(id))) })
return result
}
return C.int16_t(inputState(uint(port), uint32(device), uint(index), uint(id)))
}
@@ -767,11 +872,6 @@ 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)))
@@ -939,7 +1039,7 @@ func (core *Core) SetFrameTimeCallback(data unsafe.Pointer) {
ftc := &FrameTimeCallback{}
ftc.Reference = int64(c.reference)
ftc.Callback = func(usec int64) {
C.bridge_retro_frame_time_callback(c.callback, C.retro_usec_t(usec))
coreCall(func() { C.bridge_retro_frame_time_callback(c.callback, C.retro_usec_t(usec)) })
}
core.FrameTimeCallback = ftc
}
@@ -954,14 +1054,17 @@ 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)
hwrc.ContextReset = func() {
C.bridge_retro_hw_context_reset((C.retro_hw_context_reset_t)(c.context_reset))
// 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)
@@ -970,10 +1073,12 @@ func SetHWRenderCallback(
hwrc.VersionMajor = uint(c.version_major)
hwrc.VersionMinor = uint(c.version_minor)
hwrc.CacheContext = bool(c.cache_context)
hwrc.DebugContext = bool(c.debug_context)
hwrc.ContextDestroy = func() {
C.bridge_retro_hw_context_destroy((C.retro_hw_context_reset_t)(c.context_destroy))
if destroy != nil {
hwrc.ContextDestroy = func() {
coreCall(func() { C.bridge_retro_hw_context_destroy(destroy) })
}
}
hwrc.DebugContext = bool(c.debug_context)
return &hwrc
}
@@ -982,10 +1087,10 @@ func (core *Core) SetAudioCallback(data unsafe.Pointer) {
c := *(*C.struct_retro_audio_callback)(data)
auc := &AudioCallback{}
auc.Callback = func() {
C.bridge_retro_audio_callback(c.callback)
coreCall(func() { C.bridge_retro_audio_callback(c.callback) })
}
auc.SetState = func(state bool) {
C.bridge_retro_audio_set_state(c.set_state, C.bool(state))
coreCall(func() { C.bridge_retro_audio_set_state(c.set_state, C.bool(state)) })
}
core.AudioCallback = auc
}
@@ -993,13 +1098,15 @@ func (core *Core) SetAudioCallback(data unsafe.Pointer) {
// GetMemorySize returns the size of a region of the memory.
// See memory constants.
func (core *Core) GetMemorySize(id uint32) uint {
return uint(C.bridge_retro_get_memory_size(core.symRetroGetMemorySize, C.unsigned(id)))
return coreResult(func() uint { return uint(C.bridge_retro_get_memory_size(core.symRetroGetMemorySize, C.unsigned(id))) })
}
// GetMemoryData returns the size of a region of the memory.
// See memory constants.
func (core *Core) GetMemoryData(id uint32) unsafe.Pointer {
return C.bridge_retro_get_memory_data(core.symRetroGetMemoryData, C.unsigned(id))
return coreResult(func() unsafe.Pointer {
return C.bridge_retro_get_memory_data(core.symRetroGetMemoryData, C.unsigned(id))
})
}
// DiskControlCallback is an interface which frontend can use to eject and insert disk images
@@ -1019,19 +1126,19 @@ func (core *Core) SetDiskControlCallback(data unsafe.Pointer) {
c := *(*C.struct_retro_disk_control_callback)(data)
dcc := &DiskControlCallback{}
dcc.SetEjectState = func(state bool) {
C.bridge_retro_set_eject_state(c.set_eject_state, C.bool(state))
coreCall(func() { C.bridge_retro_set_eject_state(c.set_eject_state, C.bool(state)) })
}
dcc.GetEjectState = func() bool {
return bool(C.bridge_retro_get_eject_state(c.get_eject_state))
return coreResult(func() bool { return bool(C.bridge_retro_get_eject_state(c.get_eject_state)) })
}
dcc.GetImageIndex = func() uint {
return uint(C.bridge_retro_get_image_index(c.get_image_index))
return coreResult(func() uint { return uint(C.bridge_retro_get_image_index(c.get_image_index)) })
}
dcc.SetImageIndex = func(index uint) {
C.bridge_retro_set_image_index(c.set_image_index, C.uint(index))
coreCall(func() { C.bridge_retro_set_image_index(c.set_image_index, C.uint(index)) })
}
dcc.GetNumImages = func() uint {
return uint(C.bridge_retro_get_num_images(c.get_num_images))
return coreResult(func() uint { return uint(C.bridge_retro_get_num_images(c.get_num_images)) })
}
core.DiskControlCallback = dcc
}
+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)
cerr := C.dlerror()
if h == nil || cerr != nil {
if h == nil {
cerr := C.dlerror()
err := C.GoString(cerr)
return nil, errors.New(err)
}
+41 -18
View File
@@ -30,6 +30,22 @@ func init() {
var frame = 0
func fulfillPendingScreenshot(vid *video.Video) {
if state.PendingScreenshotName == "" {
return
}
name := state.PendingScreenshotName
done := state.PendingScreenshotDone
state.PendingScreenshotName = ""
state.PendingScreenshotDone = nil
err := vid.TakeScreenshotInFrontendFrame(name)
if done != nil {
done(err)
}
}
func runLoop(vid *video.Video, m *menu.Menu) {
var currTime time.Time
prevTime := time.Now()
@@ -39,32 +55,43 @@ func runLoop(vid *video.Video, m *menu.Menu) {
glfw.PollEvents()
m.ProcessHotkeys()
ntf.Process(dt)
vid.ResizeViewport()
m.UpdatePalette()
input.Poll()
if !state.MenuActive {
menuActive := state.MenuActive
if menuActive {
m.Update(dt)
}
if state.MenuContextResetNeeded {
m.ContextReset()
state.MenuContextResetNeeded = false
}
if !menuActive {
if state.CoreRunning {
vid.BeginCoreFrame()
vid.BeginCore()
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 {
m.Update(dt)
vid.Render()
vid.BeginFrontendFrame()
vid.RenderPresentedFrame()
fulfillPendingScreenshot(vid)
m.Render(dt)
m.RenderNotifications()
}
m.RenderNotifications()
if state.FastForward {
glfw.SwapInterval(0)
} else {
@@ -130,18 +157,14 @@ func main() {
input.Init(vid)
if len(state.CorePath) > 0 {
err := core.Load(state.CorePath)
if err == nil {
if len(gamePath) > 0 {
err := core.LoadGame(gamePath)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
m.WarpToQuickMenu()
}
}
} else {
if err := core.Load(state.CorePath); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else if len(gamePath) > 0 {
if err := core.LoadGame(gamePath); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
m.WarpToQuickMenu()
}
}
}
+1 -2
View File
@@ -196,8 +196,7 @@ func (m *Menu) ProcessHotkeys() {
// Toggle fullscreen if ActionFullscreenToggle is released
if input.Released[0][input.ActionFullscreenToggle] == 1 {
settings.Current.VideoFullscreen = !settings.Current.VideoFullscreen
m.Reconfigure(settings.Current.VideoFullscreen)
m.ContextReset()
m.SetFullscreen(settings.Current.VideoFullscreen)
err := settings.Save()
if err != nil {
ntf.DisplayAndLogf(ntf.Error, "Menu", "Error saving settings: %v", err)
+6 -4
View File
@@ -102,10 +102,12 @@ func (m *Menu) ContextReset() {
m.icons[filename] = video.NewImage(path)
}
currentScreenIndex := len(m.stack) - 1
curList := m.stack[currentScreenIndex].Entry()
for i := range curList.children {
curList.children[i].thumbnail = 0
// Every retained scene's texture names belong to the old context.
for _, scene := range m.stack {
list := scene.Entry()
for i := range list.children {
list.children[i].thumbnail = 0
}
}
}
+1
View File
@@ -11,6 +11,7 @@ var severityBgColor map[ntf.Severity]video.Color
// RenderNotifications draws the list of notification messages on the viewport
func (m *Menu) RenderNotifications() {
fbw, fbh := m.GetFramebufferSize()
m.ratio = float32(fbw) / 1920
m.Font.UpdateResolution(fbw, fbh)
var h float32 = 75
stack := h
+1 -1
View File
@@ -16,7 +16,7 @@ func buildCoreOptions() Scene {
var list sceneCoreOptions
list.label = "Core Options"
if core.Options == nil {
if core.Options == nil || len(core.Options.Vars) == 0 {
list.children = append(list.children, entry{
label: "No options",
icon: "subsetting",
+2 -2
View File
@@ -180,7 +180,7 @@ func gameExplorerCb(path string) {
// Shutdown the operating system
func cleanShutdown() {
core.UnloadGame()
core.Unload()
if err := exec.Command("/usr/sbin/shutdown", "-P", "now").Run(); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
@@ -188,7 +188,7 @@ func cleanShutdown() {
// Reboots the operating system
func cleanReboot() {
core.UnloadGame()
core.Unload()
if err := exec.Command("/usr/sbin/shutdown", "-r", "now").Run(); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
+9 -6
View File
@@ -1,6 +1,7 @@
package menu
import (
"github.com/libretro/ludo/core"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
@@ -27,7 +28,7 @@ func buildQuickMenu() Scene {
label: "Reset",
icon: "reset",
callbackOK: func() {
state.Core.Reset()
core.Reset()
state.MenuActive = false
state.FastForward = false
},
@@ -47,11 +48,13 @@ func buildQuickMenu() Scene {
icon: "screenshot",
callbackOK: func() {
name := utils.DatedName(state.GamePath)
err := menu.TakeScreenshot(name)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
ntf.DisplayAndLog(ntf.Success, "Menu", "Took a screenshot.")
state.PendingScreenshotName = name
state.PendingScreenshotDone = func(err error) {
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
ntf.DisplayAndLog(ntf.Success, "Menu", "Took a screenshot.")
}
}
},
})
+13 -11
View File
@@ -26,17 +26,19 @@ func buildSavestates() Scene {
icon: "savestate",
callbackOK: func() {
name := utils.DatedName(state.GamePath)
err := menu.TakeScreenshot(name)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
err = savestates.Save(name)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
menu.stack[len(menu.stack)-1] = buildSavestates()
menu.tweens.FastForward()
ntf.DisplayAndLog(ntf.Success, "Menu", "State saved.")
state.PendingScreenshotName = name
state.PendingScreenshotDone = func(err error) {
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
err = savestates.Save(name)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
menu.stack[len(menu.stack)-1] = buildSavestates()
menu.tweens.FastForward()
ntf.DisplayAndLog(ntf.Success, "Menu", "State saved.")
}
}
},
})
+2 -4
View File
@@ -164,8 +164,7 @@ var incrCallbacks = map[string]callbackIncrement{
v := f.Value().(bool)
v = !v
f.Set(v)
menu.Reconfigure(settings.Current.VideoFullscreen)
menu.ContextReset()
menu.SetFullscreen(settings.Current.VideoFullscreen)
settings.Save()
},
"VideoMonitorIndex": func(f *structs.Field, direction int) {
@@ -178,8 +177,7 @@ var incrCallbacks = map[string]callbackIncrement{
v = len(glfw.GetMonitors()) - 1
}
f.Set(v)
menu.Reconfigure(settings.Current.VideoFullscreen)
menu.ContextReset()
menu.SetFullscreen(settings.Current.VideoFullscreen)
settings.Save()
},
"VideoFilter": func(f *structs.Field, direction int) {
+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.UnloadGame()
core.Unload()
err := cmd.Run()
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
+7 -3
View File
@@ -50,13 +50,17 @@ func New(vars []VariableInterface) (*Options, error) {
// Cache core options
for _, v := range vars {
v := v
choices := v.Choices()
// A core may expose an empty selector, e.g. when no BIOS files are found.
if len(choices) == 0 {
continue
}
o.Vars = append(o.Vars, &Variable{
Key: v.Key(),
Desc: v.Desc(),
Choices: v.Choices(),
Choices: choices,
Default: v.DefaultValue(),
Choice: utils.IndexOfString(v.DefaultValue(), v.Choices()),
Choice: utils.IndexOfString(v.DefaultValue(), choices),
})
}
o.Updated = true
+1 -10
View File
@@ -33,7 +33,6 @@ func ipsAllocTargetData(patch, source []byte) ([]byte, error) {
size |= int(patch[offset]) << 8
offset++
size |= int(patch[offset]) << 0
offset++
targetLength = size
prov := make([]byte, targetLength)
return prov, nil
@@ -117,15 +116,7 @@ func applyIPS(patch, source []byte) (*[]byte, error) {
offset++
if address == EOF {
if offset == len(patch) {
return &targetData, nil
} else if offset == len(patch)-3 {
size := int(patch[offset]) << 16
offset++
size |= int(patch[offset]) << 8
offset++
size |= int(patch[offset]) << 0
offset++
if offset == len(patch) || offset == len(patch)-3 {
return &targetData, nil
}
}
+3 -6
View File
@@ -6,15 +6,14 @@ import (
"os"
"path/filepath"
"github.com/libretro/ludo/core"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
)
// Save the current state to the filesystem. name is the name of the
// savestate file to save to, without extension.
func Save(name string) error {
s := state.Core.SerializeSize()
bytes, err := state.Core.Serialize(s)
bytes, err := core.Serialize()
if err != nil {
return err
}
@@ -28,11 +27,9 @@ func Save(name string) error {
// Load the state from the filesystem
func Load(path string) error {
s := state.Core.SerializeSize()
bytes, err := os.ReadFile(path)
if err != nil {
return err
}
err = state.Core.Unserialize(bytes, s)
return err
return core.Unserialize(bytes)
}
+6
View File
@@ -43,13 +43,17 @@ func defaultSettings() Settings {
"Nintendo - Game Boy Advance": "mgba_libretro",
"Nintendo - Game Boy Color": "gambatte_libretro",
"Nintendo - Game Boy": "gambatte_libretro",
"Nintendo - GameCube": "dolphin_libretro",
"Nintendo - Nintendo 3DS": "citra_libretro",
"Nintendo - Nintendo 64": "mupen64plus_next_libretro",
"Nintendo - Nintendo Entertainment System": "fceumm_libretro",
"Nintendo - Nintendo DS": "melonds_libretro",
"Nintendo - Pokemon Mini": "pokemini_libretro",
"Nintendo - Super Nintendo Entertainment System": "snes9x_libretro",
"Nintendo - Virtual Boy": "mednafen_vb_libretro",
"Nintendo - Wii": "dolphin_libretro",
"Sega - 32X": "picodrive_libretro",
"Sega - Dreamcast": "flycast_libretro",
"Sega - Game Gear": "genesis_plus_gx_libretro",
"Sega - Master System - Mark III": "genesis_plus_gx_libretro",
"Sega - Mega Drive - Genesis": "genesis_plus_gx_libretro",
@@ -60,6 +64,8 @@ func defaultSettings() Settings {
"SNK - Neo Geo Pocket Color": "mednafen_ngp_libretro",
"SNK - Neo Geo Pocket": "mednafen_ngp_libretro",
"Sony - PlayStation": playstationCore,
"Sony - PlayStation 2": "pcsx2_libretro",
"Sony - PlayStation Portable": "ppsspp_libretro",
},
FileDirectory: usr.HomeDir,
CoresDirectory: "./cores",
+9 -1
View File
@@ -34,4 +34,12 @@ var LudOS bool
// FastForward will run the core as fast as possible
var FastForward bool
var CoreSetSharedContext 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
@@ -0,0 +1,190 @@
//go:build gltest && !darwin
package video
import (
"runtime"
"testing"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/go-gl/mathgl/mgl32"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/state"
)
// Run with go test -tags 'wayland gltest' ./video on a graphical session.
func TestHardwareContextIsolation(t *testing.T) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if err := glfw.Init(); err != nil {
t.Fatal(err)
}
defer glfw.Terminate()
glfw.DefaultWindowHints()
glfw.WindowHint(glfw.Visible, glfw.False)
glfw.WindowHint(glfw.ContextVersionMajor, 3)
glfw.WindowHint(glfw.ContextVersionMinor, 3)
glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
w, err := glfw.CreateWindow(128, 96, "Ludo GL test", nil, nil)
if err != nil {
t.Fatal(err)
}
defer w.Destroy()
w.MakeContextCurrent()
if err := initGL(); err != nil {
t.Fatal(err)
}
t.Log("GL:", gl.GoStr(gl.GetString(gl.VERSION)))
oldCore, oldRunning := state.Core, state.CoreRunning
defer func() { state.Core, state.CoreRunning = oldCore, oldRunning }()
hw := &libretro.HWRenderCallback{HWContextType: libretro.HWContextOpenGLCore, VersionMajor: 3, VersionMinor: 3}
state.Core = &libretro.Core{HWRenderCallback: hw}
state.CoreRunning = true
v := &Video{Window: w, identityMat: mgl32.Ident4(), Geom: libretro.GameGeometry{MaxWidth: 64, MaxHeight: 64}}
v.SetHWRenderContext(hw)
v.ensurePixelFormatDefaults()
v.resetGameTexture(64, 64)
hw.ContextReset = func() {
if glfw.GetCurrentContext() != v.HWWindow {
t.Fatal("context_reset ran outside the HW context")
}
}
if err := v.ResetHWContext(); err != nil {
t.Fatal(err)
}
defer v.DestroySharedHWContext()
if glfw.GetCurrentContext() != w || v.HWWindow == nil || v.HWWindow == w {
t.Fatal("reset did not return to an independent frontend context")
}
v.BeginCore()
var readFBO, texture, pbo uint32
gl.GenFramebuffers(1, &readFBO)
gl.BindFramebuffer(gl.READ_FRAMEBUFFER, readFBO)
gl.GenTextures(1, &texture)
gl.ActiveTexture(gl.TEXTURE1)
gl.BindTexture(gl.TEXTURE_2D, texture)
gl.GenBuffers(1, &pbo)
gl.BindBuffer(gl.PIXEL_PACK_BUFFER, pbo)
gl.PixelStorei(gl.PACK_ROW_LENGTH, 256)
gl.Enable(gl.BLEND)
gl.Enable(gl.SCISSOR_TEST)
gl.BlendEquation(gl.FUNC_REVERSE_SUBTRACT)
v.EndCore()
v.BeginFrontendFrame()
checkInt := func(name uint32, want int32) {
t.Helper()
var got int32
gl.GetIntegerv(name, &got)
if got != want {
t.Errorf("GL state %#x: got %d, want %d", name, got, want)
}
}
checkInt(gl.PIXEL_PACK_BUFFER_BINDING, 0)
checkInt(gl.PACK_ROW_LENGTH, 0)
checkInt(gl.READ_FRAMEBUFFER_BINDING, 0)
checkInt(gl.BLEND_EQUATION_RGB, gl.FUNC_ADD)
v.BeginCore()
checkInt(gl.PIXEL_PACK_BUFFER_BINDING, int32(pbo))
checkInt(gl.PACK_ROW_LENGTH, 256)
checkInt(gl.READ_FRAMEBUFFER_BINDING, int32(readFBO))
checkInt(gl.DRAW_FRAMEBUFFER_BINDING, int32(v.fboID))
checkInt(gl.BLEND_EQUATION_RGB, gl.FUNC_REVERSE_SUBTRACT)
checkInt(gl.ACTIVE_TEXTURE, gl.TEXTURE1)
checkInt(gl.TEXTURE_BINDING_2D, int32(texture))
if !gl.IsEnabled(gl.BLEND) || !gl.IsEnabled(gl.SCISSOR_TEST) {
t.Error("frontend rendering changed core enables")
}
gl.BindBuffer(gl.PIXEL_PACK_BUFFER, 0)
gl.PixelStorei(gl.PACK_ROW_LENGTH, 0)
gl.Disable(gl.SCISSOR_TEST)
gl.Disable(gl.BLEND)
gl.BindFramebuffer(gl.READ_FRAMEBUFFER, v.fboID)
// Verify the produced pixels are visible through the shared texture.
gl.ClearColor(1, 0, 0, 1)
gl.Clear(gl.COLOR_BUFFER_BIT)
v.EndCore()
var frontendFBO uint32
gl.GenFramebuffers(1, &frontendFBO)
gl.BindFramebuffer(gl.FRAMEBUFFER, frontendFBO)
gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, v.texID, 0)
var pixel [4]byte
gl.ReadPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(&pixel[0]))
if pixel != [4]byte{255, 0, 0, 255} {
t.Errorf("shared frame: got %v, want red", pixel)
}
v.BeginFrontendFrame()
// Backbuffer snapshots must retain their orientation for either core origin.
v.defaultProgram, err = newProgram(vertexShader, defaultFragmentShader)
if err != nil {
t.Fatal(err)
}
gl.GenVertexArrays(1, &v.vao)
gl.BindVertexArray(v.vao)
gl.GenBuffers(1, &v.vbo)
gl.BindBuffer(gl.ARRAY_BUFFER, v.vbo)
vert := uint32(gl.GetAttribLocation(v.defaultProgram, gl.Str("vert\x00")))
uv := uint32(gl.GetAttribLocation(v.defaultProgram, gl.Str("vertTexCoord\x00")))
gl.EnableVertexAttribArray(vert)
gl.VertexAttribPointerWithOffset(vert, 2, gl.FLOAT, false, 16, 0)
gl.EnableVertexAttribArray(uv)
gl.VertexAttribPointerWithOffset(uv, 2, gl.FLOAT, false, 16, 8)
v.width, v.height = 64, 64
for _, bottomLeft := range []bool{false, true} {
hw.BottomLeftOrigin = bottomLeft
v.BeginFrontendFrame()
fbw, fbh := w.GetFramebufferSize()
v.Geom.AspectRatio = float64(fbw) / float64(fbh)
v.presentW, v.presentH = int32(fbw), int32(fbh)
gl.ClearColor(1, 0, 0, 1)
gl.Clear(gl.COLOR_BUFFER_BIT)
gl.Enable(gl.SCISSOR_TEST)
gl.Scissor(0, int32(fbh/2), int32(fbw), int32(fbh/2))
gl.ClearColor(0, 0, 1, 1)
gl.Clear(gl.COLOR_BUFFER_BIT)
gl.Disable(gl.SCISSOR_TEST)
v.CachePresentedFrame()
if !v.RenderCachedFrame() {
t.Fatal("cached frame unavailable")
}
gl.ReadPixels(1, 1, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(&pixel[0]))
if pixel != [4]byte{255, 0, 0, 255} {
t.Errorf("cached frame, bottom-left=%v: got %v, want red", bottomLeft, pixel)
}
}
hw.ContextDestroy = func() {
if glfw.GetCurrentContext() != v.HWWindow {
t.Error("context_destroy ran outside the HW context")
}
}
v.RunContextDestroy()
v.ResetCoreState()
if !gl.IsFramebuffer(frontendFBO) {
t.Error("HW teardown deleted a frontend framebuffer with the same name")
}
if v.fboID != 0 || v.HWWindow != nil || v.cachedFrameValid || v.width != 0 {
t.Error("HW teardown left stale core resources")
}
// A subsequent core must get a fresh context, including on the GL 2.x sync fallback.
if err := v.ResetHWContext(); err != nil {
t.Fatal(err)
}
v.hwSync = false
v.BeginCore()
checkInt(gl.PACK_ROW_LENGTH, 0)
checkInt(gl.PIXEL_PACK_BUFFER_BINDING, 0)
checkInt(gl.BLEND_EQUATION_RGB, gl.FUNC_ADD)
if gl.IsEnabled(gl.BLEND) || gl.IsEnabled(gl.SCISSOR_TEST) {
t.Error("new core inherited the previous core's enables")
}
v.EndCore()
if e := gl.GetError(); e != gl.NO_ERROR {
t.Errorf("GL error: %#x", e)
}
}
-5
View File
@@ -4,14 +4,9 @@ var vertexShader = `
#if __VERSION__ >= 130
#define COMPAT_VARYING out
#define COMPAT_ATTRIBUTE in
#define COMPAT_TEXTURE texture
#define COMPAT_FRAGCOLOR FragColor
out vec4 COMPAT_FRAGCOLOR;
#else
#define COMPAT_VARYING varying
#define COMPAT_ATTRIBUTE attribute
#define COMPAT_TEXTURE texture2D
#define COMPAT_FRAGCOLOR gl_FragColor
#endif
COMPAT_ATTRIBUTE vec2 vert;
+83
View File
@@ -0,0 +1,83 @@
package video
import (
"log"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/mathgl/mgl32"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/state"
)
// InitFramebuffer initializes and configures the video frame buffer based on
// informations from the HWRenderCallback of the libretro core.
func (video *Video) InitFramebuffer() {
width := int32(video.Geom.MaxWidth)
height := int32(video.Geom.MaxHeight)
video.texWidth = width
video.texHeight = height
log.Printf("[Video]: Initializing HW render (%v x %v).\n", width, height)
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, video.texID)
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, nil)
genFramebuffers(1, &video.fboID)
bindFramebuffer(gl.FRAMEBUFFER, video.fboID)
libretro.SetCurrentFramebufferValue(uintptr(video.fboID))
framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, video.texID, 0)
hw := state.Core.HWRenderCallback
if hw.Depth {
genRenderbuffers(1, &video.rboID)
bindRenderbuffer(gl.RENDERBUFFER, video.rboID)
format := gl.DEPTH_COMPONENT16
if hw.Stencil {
format = gl.DEPTH24_STENCIL8
}
renderbufferStorage(gl.RENDERBUFFER, uint32(format), width, height)
bindRenderbuffer(gl.RENDERBUFFER, 0)
// Separate attachment points work with both core and EXT framebuffers.
framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, video.rboID)
if hw.Stencil {
framebufferRenderbuffer(gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, gl.RENDERBUFFER, video.rboID)
}
}
// Default origin is top left
video.orthoMat = mgl32.Ortho2D(-1, 1, -1, 1)
if hw.BottomLeftOrigin {
video.orthoMat = mgl32.Ortho2D(-1, 1, 1, -1)
}
if st := checkFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE {
log.Fatalf("[Video] Framebuffer is not complete. Error: %v\n", st)
}
gl.ClearColor(0, 0, 0, 1)
if hw.Depth && hw.Stencil {
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT)
} else if hw.Depth {
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
} else {
gl.Clear(gl.COLOR_BUFFER_BIT)
}
}
func bindBackbuffer() {
bindFramebuffer(gl.FRAMEBUFFER, 0)
}
func (video *Video) destroyFramebuffer() {
bindBackbuffer()
if video.rboID != 0 {
deleteRenderbuffers(1, &video.rboID)
video.rboID = 0
}
if video.fboID != 0 {
deleteFramebuffers(1, &video.fboID)
video.fboID = 0
}
}
+6 -5
View File
@@ -124,11 +124,7 @@ func (video *Video) ScissorEnd() {
gl.Disable(gl.SCISSOR_TEST)
}
func (video *Video) vertexArray(x, y, w, h, scale float32) []float32 {
fbw, fbh := video.Window.GetFramebufferSize()
ffbw := float32(fbw)
ffbh := float32(fbh)
func vertexArrayForFramebuffer(x, y, w, h, scale, ffbw, ffbh float32) []float32 {
w *= scale
h *= scale
@@ -143,6 +139,11 @@ func (video *Video) vertexArray(x, y, w, h, scale float32) []float32 {
}
}
func (video *Video) vertexArray(x, y, w, h, scale float32) []float32 {
fbw, fbh := video.Window.GetFramebufferSize()
return vertexArrayForFramebuffer(x, y, w, h, scale, float32(fbw), float32(fbh))
}
// DrawBorder draws a colored rectangle border
func (video *Video) DrawBorder(x, y, w, h, borderWidth float32, c Color) {
va := video.vertexArray(x, y, w, h, 1.0)
+63
View File
@@ -0,0 +1,63 @@
package video
import (
"errors"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/glfw/v3.4/glfw"
)
var (
glslVersion = "#version 120\n"
genVertexArrays = gl.GenVertexArrays
bindVertexArray = gl.BindVertexArray
genFramebuffers = gl.GenFramebuffers
bindFramebuffer = gl.BindFramebuffer
deleteFramebuffers = gl.DeleteFramebuffers
framebufferTexture2D = gl.FramebufferTexture2D
checkFramebufferStatus = gl.CheckFramebufferStatus
genRenderbuffers = gl.GenRenderbuffers
bindRenderbuffer = gl.BindRenderbuffer
deleteRenderbuffers = gl.DeleteRenderbuffers
renderbufferStorage = gl.RenderbufferStorage
framebufferRenderbuffer = gl.FramebufferRenderbuffer
)
// The frontend and hidden core context use the same hints. Select their API
// from the actual context capabilities, including macOS's legacy extensions.
func initGL() error {
if err := gl.Init(); err != nil {
return err
}
window := glfw.GetCurrentContext()
major, minor := window.GetAttrib(glfw.ContextVersionMajor), window.GetAttrib(glfw.ContextVersionMinor)
glslVersion = "#version 120\n"
if major > 3 || (major == 3 && minor >= 2) {
glslVersion = "#version 150\n"
} else if major == 3 {
glslVersion = "#version 130\n"
}
genVertexArrays, bindVertexArray = gl.GenVertexArrays, gl.BindVertexArray
genFramebuffers, bindFramebuffer, deleteFramebuffers = gl.GenFramebuffers, gl.BindFramebuffer, gl.DeleteFramebuffers
framebufferTexture2D, checkFramebufferStatus = gl.FramebufferTexture2D, gl.CheckFramebufferStatus
genRenderbuffers, bindRenderbuffer, deleteRenderbuffers = gl.GenRenderbuffers, gl.BindRenderbuffer, gl.DeleteRenderbuffers
renderbufferStorage, framebufferRenderbuffer = gl.RenderbufferStorage, gl.FramebufferRenderbuffer
if major < 3 && !glfw.ExtensionSupported("GL_ARB_vertex_array_object") {
if !glfw.ExtensionSupported("GL_APPLE_vertex_array_object") {
return errors.New("OpenGL vertex array objects are not supported")
}
genVertexArrays, bindVertexArray = gl.GenVertexArraysAPPLE, gl.BindVertexArrayAPPLE
}
if major < 3 && !glfw.ExtensionSupported("GL_ARB_framebuffer_object") {
if !glfw.ExtensionSupported("GL_EXT_framebuffer_object") {
return errors.New("OpenGL framebuffer objects are not supported")
}
genFramebuffers, bindFramebuffer, deleteFramebuffers = gl.GenFramebuffersEXT, gl.BindFramebufferEXT, gl.DeleteFramebuffersEXT
framebufferTexture2D, checkFramebufferStatus = gl.FramebufferTexture2DEXT, gl.CheckFramebufferStatusEXT
genRenderbuffers, bindRenderbuffer, deleteRenderbuffers = gl.GenRenderbuffersEXT, gl.BindRenderbufferEXT, gl.DeleteRenderbuffersEXT
renderbufferStorage, framebufferRenderbuffer = gl.RenderbufferStorageEXT, gl.FramebufferRenderbufferEXT
}
return nil
}
+25
View File
@@ -0,0 +1,25 @@
package video
import (
"testing"
"github.com/libretro/ludo/libretro"
)
func TestDuplicateFramePreservesDimensions(t *testing.T) {
v := &Video{}
v.Refresh(libretro.HWFrameBufferValid, 320, 240, 0)
v.Refresh(nil, 0, 0, 0)
if v.width != 320 || v.height != 240 || v.data != libretro.HWFrameBufferValid || !v.needUpload {
t.Fatal("NULL refresh discarded the pending frame")
}
v.needUpload = false
v.Refresh(nil, 640, 480, 0)
if v.width != 320 || v.height != 240 || v.needUpload {
t.Fatal("NULL refresh changed the previously presented frame")
}
}
func TestContextDestroyWithoutCore(t *testing.T) {
(&Video{}).RunContextDestroy()
}
+31 -33
View File
@@ -1,6 +1,7 @@
package video
import (
"errors"
"image"
"image/png"
"os"
@@ -13,41 +14,33 @@ import (
"github.com/libretro/ludo/state"
)
// During the TakeScreenshot step, we need to render the current game frame at
// the right resolution to later capture it using ReadPixels. renderScreenshot
// taking care of this.
func (video *Video) renderScreenshot() {
va := video.vertexArray(0, 0, float32(video.Geom.BaseWidth), float32(video.Geom.BaseHeight), 1.0)
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW)
func (video *Video) captureBackbufferScreenshot(name string) error {
outW, outH := video.Geom.BaseWidth, video.Geom.BaseHeight
if outW < 1 || outH < 1 {
return errors.New("invalid screenshot output size")
}
bindVertexArray(video.vao)
x := video.presentX
y := video.presentY
w := video.presentW
h := video.presentH
if !state.CoreRunning || video.width < 1 || video.height < 1 || w < 1 || h < 1 {
return errors.New("no game frame to capture")
}
gl.BindTexture(gl.TEXTURE_2D, video.texID)
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
var oldReadBuffer int32
gl.GetIntegerv(gl.READ_BUFFER, &oldReadBuffer)
gl.ReadBuffer(gl.BACK)
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
}
img := image.NewRGBA(image.Rect(0, 0, int(w), int(h)))
gl.Finish()
gl.ReadPixels(x, y, w, h, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(img.Pix))
gl.ReadBuffer(uint32(oldReadBuffer))
// TakeScreenshot captures the ouput of video.Render and writes it to a file
func (video *Video) TakeScreenshot(name string) error {
state.MenuActive = false
defer func() { state.MenuActive = true }()
gl.UseProgram(video.defaultProgram)
video.renderScreenshot()
img := image.NewRGBA(image.Rect(0, 0, video.Geom.BaseWidth, video.Geom.BaseHeight))
_, fbh := video.Window.GetFramebufferSize()
gl.ReadPixels(
0, int32(fbh-video.Geom.BaseHeight),
int32(video.Geom.BaseWidth), int32(video.Geom.BaseHeight),
gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(img.Pix))
gl.UseProgram(video.program)
flipped := imaging.FlipV(img)
if flipped.Bounds().Dx() != outW || flipped.Bounds().Dy() != outH {
flipped = imaging.Resize(flipped, outW, outH, imaging.Linear)
}
err := os.MkdirAll(settings.Current.ScreenshotsDirectory, os.ModePerm)
if err != nil {
@@ -59,8 +52,13 @@ func (video *Video) TakeScreenshot(name string) error {
if err != nil {
return err
}
flipped := imaging.FlipV(img)
defer fd.Close()
return png.Encode(fd, flipped)
}
// TakeScreenshotInFrontendFrame captures a screenshot while the caller already
// owns a frontend frame and has already rendered the current game frame.
func (video *Video) TakeScreenshotInFrontendFrame(name string) error {
return video.captureBackbufferScreenshot(name)
}
+7 -20
View File
@@ -5,25 +5,9 @@ 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 {
@@ -39,6 +23,7 @@ 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)
@@ -58,12 +43,14 @@ 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 {
identity := identityMatrix()
gl.UniformMatrix4fv(mvp, 1, false, &identity[0])
IdentityMatrix := mgl32.Ident4()
gl.UniformMatrix4fv(mvp, 1, false, &IdentityMatrix[0])
}
gl.UseProgram(0)
@@ -73,7 +60,7 @@ func newProgram(vertexShaderSource, fragmentShaderSource string) (uint32, error)
func compileShader(source string, shaderType uint32) (uint32, error) {
shader := gl.CreateShader(shaderType)
csources, free := gl.Strs(source)
csources, free := gl.Strs(glslVersion + source)
gl.ShaderSource(shader, 1, csources, nil)
free()
gl.CompileShader(shader)
+78
View File
@@ -0,0 +1,78 @@
//go:build gltest && linux
package video
import (
"bytes"
"runtime"
"testing"
"unsafe"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/libretro/ludo/libretro"
"golang.org/x/sys/unix"
)
func TestSoftwareFrameUploadBounds(t *testing.T) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if err := glfw.Init(); err != nil {
t.Fatal(err)
}
defer glfw.Terminate()
glfw.DefaultWindowHints()
glfw.WindowHint(glfw.Visible, glfw.False)
w, err := glfw.CreateWindow(32, 32, "Ludo upload test", nil, nil)
if err != nil {
t.Fatal(err)
}
defer w.Destroy()
w.MakeContextCurrent()
if err := initGL(); err != nil {
t.Fatal(err)
}
v := &Video{Geom: libretro.GameGeometry{MaxWidth: 64, MaxHeight: 64}}
v.SetPixelFormat(libretro.PixelFormatXRGB8888)
v.resetGameTexture(1, 1)
defer gl.DeleteTextures(1, &v.texID)
v.program, err = newProgram(vertexShader, defaultFragmentShader)
if err != nil {
t.Fatal(err)
}
defer gl.DeleteProgram(v.program)
const width, height, pitch = 32, 32, 32 * 4
page := unix.Getpagesize()
memory, err := unix.Mmap(-1, 0, page*2, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_ANON|unix.MAP_PRIVATE)
if err != nil {
t.Fatal(err)
}
defer unix.Munmap(memory)
if err := unix.Mprotect(memory[page:], unix.PROT_NONE); err != nil {
t.Fatal(err)
}
// The core supplies only the current frame, ending at a protected page.
frame := memory[page-pitch*height : page]
for i := range frame {
frame[i] = byte(i)
}
v.Refresh(unsafe.Pointer(&frame[0]), width, height, pitch)
v.uploadTexture()
var texWidth, texHeight int32
gl.GetTexLevelParameteriv(gl.TEXTURE_2D, 0, gl.TEXTURE_WIDTH, &texWidth)
gl.GetTexLevelParameteriv(gl.TEXTURE_2D, 0, gl.TEXTURE_HEIGHT, &texHeight)
if texWidth != width || texHeight != height {
t.Fatalf("texture size = %dx%d, want %dx%d", texWidth, texHeight, width, height)
}
pixels := make([]byte, len(frame))
gl.GetTexImage(gl.TEXTURE_2D, 0, v.pixType, v.pixFmt, gl.Ptr(pixels))
if !bytes.Equal(pixels, frame) {
t.Error("uploaded pixels differ from the supplied frame")
}
if err := gl.GetError(); err != gl.NO_ERROR {
t.Errorf("GL error: %#x", err)
}
}
+525 -463
View File
File diff suppressed because it is too large. Load diff
-72
View File
@@ -1,72 +0,0 @@
// +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)
}
-73
View File
@@ -1,73 +0,0 @@
// +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)
}