Compare commits

..
63 Commits
Author SHA1 Message Date
Jean-André Santoni 5798e14105 Clean 2026-05-17 23:13:31 +02:00
Jean-André Santoni 99f215025b Add back context destroy 2026-05-17 23:03:46 +02:00
Jean-André Santoni 1bd4ad6986 Support GL cores on Linux 2026-05-17 22:22:25 +02:00
Jean-André Santoni 4795525132 Merge origin/master into kivutar/gl 2026-05-17 16:30:56 +02:00
Jean-André Santoni 3edf65f9fd Merge remote-tracking branch 'origin/master' into kivutar/gl 2022-08-05 11:53:07 +02:00
Jean-André Santoni e01cc6bb86 Merge remote-tracking branch 'origin/master' into kivutar/gl 2022-02-10 21:25:25 +01:00
Jean-André Santoni 3e084bf7f1 Merge remote-tracking branch 'origin/master' into kivutar/gl 2021-12-14 11:05:13 +01:00
Jean-André Santoni 2bcbb1d24c Merge remote-tracking branch 'origin/master' into kivutar/gl 2021-11-24 13:12:40 +07:00
Jean-André Santoni 7d48bb840b Merge remote-tracking branch 'origin/master' into kivutar/gl 2021-11-14 10:12:38 +07:00
Jean-André Santoni cb8649a129 Forgot to use program before get uniform 2021-10-22 20:47:24 +07:00
Jean-André Santoni d74969f50e Fix 2021-10-22 20:09:45 +07:00
Jean-André Santoni a95767339e Merge remote-tracking branch 'origin/master' into kivutar/gl 2021-10-22 19:47:59 +07:00
Jean-André Santoni 346dbd8a33 Merge remote-tracking branch 'origin/master' into kivutar/gl 2021-09-30 14:24:57 +07:00
Jean-André Santoni c95a3a7a14 Merge branch 'master' into kivutar/gl 2021-05-14 19:59:41 +07:00
Jean-André Santoni 8647e6f893 Merge branch 'master' into kivutar/gl 2021-05-14 11:09:32 +07:00
Jean-André Santoni a43843bab1 Fix 2021-04-17 15:40:33 +07:00
Jean-André Santoni 6097d396df Merge branch 'master' into kivutar/gl 2021-04-17 14:56:26 +07:00
Jean-André Santoni e4c5845b57 Merge branch 'master' into kivutar/gl 2021-01-28 10:57:07 +07:00
Jean-André Santoni da0092ef53 Fixes 2021-01-05 22:38:01 +07:00
Jean-André Santoni d8afc6031a Merge remote-tracking branch 'origin/master' into kivutar/gl 2021-01-05 22:33:41 +07:00
Jean-André Santoni 1e0c4a0966 Uncomment gl.Enable(gl.TEXTURE_2D) 2020-07-27 00:10:20 +07:00
Jean-André Santoni e01a76b08d Merge remote-tracking branch 'origin/master' into kivutar/gl 2020-07-26 23:35:51 +07:00
Jean-André Santoni 73d70cb1f9 Implement libretro.EnvironmentGetPrefferedHWRenderer 2020-03-01 17:37:04 +07:00
Jean-André Santoni 8b8d49eef4 Merge remote-tracking branch 'origin/master' into kivutar/gl 2020-03-01 17:24:24 +07:00
Jean-André Santoni 008a70c851 Fix FBO on OSX (#313)
* Fix FBO on OSX

* Remove useless code

* Use GL v2.1 for fonts drawing

* Fix fonts on OSX with GL 2.1

* Rename

* Same thing for GenVertexArrays

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

* Fix texture/framebuffer allocation and clipping

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

* Fix context destruction

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

No files matched your search

+9 -12
View File
@@ -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)))
+1 -1
View File
@@ -4,7 +4,7 @@ Ludo is a work in progress libretro frontend written in go.
<img src="https://raw.githubusercontent.com/kivutar/ludo-assets/master/illustration.png" />
It supports software-rendered and desktop OpenGL libretro cores. Hardware-rendered cores may require a newer OpenGL version than the frontend itself.
It is able to launch most non GL libretro cores.
It works on OSX, Linux, Linux ARM and Windows. You can download releases [here](https://github.com/libretro/ludo/releases)
+37 -46
View File
@@ -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
View File
@@ -1,10 +1,10 @@
package core
import (
"log"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
@@ -15,34 +15,6 @@ import (
"github.com/libretro/ludo/video"
)
var mainThread = make(chan func())
func init() {
// GLFW initialization and input setup must run on the main OS thread.
runtime.LockOSThread()
}
func TestMain(m *testing.M) {
var code int
go func() {
code = m.Run()
close(mainThread)
}()
for f := range mainThread {
f()
}
os.Exit(code)
}
func onMainThread(f func()) {
done := make(chan struct{})
mainThread <- func() {
f()
close(done)
}
<-done
}
type WindowMock struct{}
func (m WindowMock) GetFramebufferSize() (width, height int) { return 320, 240 }
@@ -229,21 +201,17 @@ func Test_coreLoadGame(t *testing.T) {
Init(&video.Video{})
var initErr error
onMainThread(func() { initErr = glfw.Init() })
if initErr != nil {
t.Fatalf("glfw.Init() error = %v", initErr)
if err := glfw.Init(); err != nil {
log.Fatalln("failed to initialize glfw")
}
t.Cleanup(func() { onMainThread(glfw.Terminate) })
t.Cleanup(glfw.Terminate)
if err := Load("testdata/vecx_libretro" + ext); err != nil {
t.Fatalf("Load() error = %v", err)
}
var loadGameErr error
got := utils.CaptureOutput(func() {
onMainThread(func() { loadGameErr = LoadGame("testdata/Polar Rescue (USA).vec") })
})
got := utils.CaptureOutput(func() { loadGameErr = LoadGame("testdata/Polar Rescue (USA).vec") })
if loadGameErr != nil {
t.Fatalf("LoadGame() error = %v", loadGameErr)
}
@@ -267,6 +235,7 @@ func Test_coreLoadGame(t *testing.T) {
}
})
UnloadGame()
Unload()
t.Run("Global state should be cleared by Unload", func(t *testing.T) {
@@ -283,6 +252,7 @@ func Test_coreLoadGame(t *testing.T) {
}
func Test_coreLoadGameWithoutCore(t *testing.T) {
UnloadGame()
Unload()
err := LoadGame("testdata/Polar Rescue (USA).vec")
+11 -17
View File
@@ -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)
+3 -4
View File
@@ -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
+6 -9
View File
@@ -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
View File
@@ -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
View File
@@ -14,9 +14,7 @@ package libretro
#include <stdio.h>
#include <string.h>
int cothread_init();
void cothread_deinit();
bool cothread_is_current();
void cothread_init();
void bridge_retro_init(void *f);
void bridge_retro_deinit(void *f);
@@ -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
}
+2 -2
View File
@@ -28,8 +28,8 @@ func DlOpen(path string) (DlHandle, error) {
defer C.free(unsafe.Pointer(cpath))
h := C.dlopen(cpath, C.RTLD_LAZY|C.RTLD_GLOBAL)
if h == nil {
cerr := C.dlerror()
cerr := C.dlerror()
if h == nil || cerr != nil {
err := C.GoString(cerr)
return nil, errors.New(err)
}
+18 -41
View File
@@ -30,22 +30,6 @@ func init() {
var frame = 0
func fulfillPendingScreenshot(vid *video.Video) {
if state.PendingScreenshotName == "" {
return
}
name := state.PendingScreenshotName
done := state.PendingScreenshotDone
state.PendingScreenshotName = ""
state.PendingScreenshotDone = nil
err := vid.TakeScreenshotInFrontendFrame(name)
if done != nil {
done(err)
}
}
func runLoop(vid *video.Video, m *menu.Menu) {
var currTime time.Time
prevTime := time.Now()
@@ -55,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
View File
@@ -196,7 +196,8 @@ func (m *Menu) ProcessHotkeys() {
// Toggle fullscreen if ActionFullscreenToggle is released
if input.Released[0][input.ActionFullscreenToggle] == 1 {
settings.Current.VideoFullscreen = !settings.Current.VideoFullscreen
m.SetFullscreen(settings.Current.VideoFullscreen)
m.Reconfigure(settings.Current.VideoFullscreen)
m.ContextReset()
err := settings.Save()
if err != nil {
ntf.DisplayAndLogf(ntf.Error, "Menu", "Error saving settings: %v", err)
+4 -6
View File
@@ -102,12 +102,10 @@ func (m *Menu) ContextReset() {
m.icons[filename] = video.NewImage(path)
}
// Every retained scene's texture names belong to the old context.
for _, scene := range m.stack {
list := scene.Entry()
for i := range list.children {
list.children[i].thumbnail = 0
}
currentScreenIndex := len(m.stack) - 1
curList := m.stack[currentScreenIndex].Entry()
for i := range curList.children {
curList.children[i].thumbnail = 0
}
}
-1
View File
@@ -11,7 +11,6 @@ var severityBgColor map[ntf.Severity]video.Color
// RenderNotifications draws the list of notification messages on the viewport
func (m *Menu) RenderNotifications() {
fbw, fbh := m.GetFramebufferSize()
m.ratio = float32(fbw) / 1920
m.Font.UpdateResolution(fbw, fbh)
var h float32 = 75
stack := h
+1 -1
View File
@@ -16,7 +16,7 @@ func buildCoreOptions() Scene {
var list sceneCoreOptions
list.label = "Core Options"
if core.Options == nil || len(core.Options.Vars) == 0 {
if core.Options == nil {
list.children = append(list.children, entry{
label: "No options",
icon: "subsetting",
+2 -2
View File
@@ -180,7 +180,7 @@ func gameExplorerCb(path string) {
// Shutdown the operating system
func cleanShutdown() {
core.Unload()
core.UnloadGame()
if err := exec.Command("/usr/sbin/shutdown", "-P", "now").Run(); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
@@ -188,7 +188,7 @@ func cleanShutdown() {
// Reboots the operating system
func cleanReboot() {
core.Unload()
core.UnloadGame()
if err := exec.Command("/usr/sbin/shutdown", "-r", "now").Run(); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
+6 -9
View File
@@ -1,7 +1,6 @@
package menu
import (
"github.com/libretro/ludo/core"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
@@ -28,7 +27,7 @@ func buildQuickMenu() Scene {
label: "Reset",
icon: "reset",
callbackOK: func() {
core.Reset()
state.Core.Reset()
state.MenuActive = false
state.FastForward = false
},
@@ -48,13 +47,11 @@ func buildQuickMenu() Scene {
icon: "screenshot",
callbackOK: func() {
name := utils.DatedName(state.GamePath)
state.PendingScreenshotName = name
state.PendingScreenshotDone = func(err error) {
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
ntf.DisplayAndLog(ntf.Success, "Menu", "Took a screenshot.")
}
err := menu.TakeScreenshot(name)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
ntf.DisplayAndLog(ntf.Success, "Menu", "Took a screenshot.")
}
},
})
+11 -13
View File
@@ -26,19 +26,17 @@ func buildSavestates() Scene {
icon: "savestate",
callbackOK: func() {
name := utils.DatedName(state.GamePath)
state.PendingScreenshotName = name
state.PendingScreenshotDone = func(err error) {
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
err = savestates.Save(name)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
menu.stack[len(menu.stack)-1] = buildSavestates()
menu.tweens.FastForward()
ntf.DisplayAndLog(ntf.Success, "Menu", "State saved.")
}
err := menu.TakeScreenshot(name)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
err = savestates.Save(name)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
menu.stack[len(menu.stack)-1] = buildSavestates()
menu.tweens.FastForward()
ntf.DisplayAndLog(ntf.Success, "Menu", "State saved.")
}
},
})
+4 -2
View File
@@ -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) {
+1 -1
View File
@@ -90,7 +90,7 @@ func (s *sceneUpdater) update(dt float32) {
s.children[0].icon = "reload"
s.children[0].callbackOK = func() {
cmd := exec.Command("/usr/sbin/shutdown", "-r", "now")
core.Unload()
core.UnloadGame()
err := cmd.Run()
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
+3 -7
View File
@@ -50,17 +50,13 @@ func New(vars []VariableInterface) (*Options, error) {
// Cache core options
for _, v := range vars {
choices := v.Choices()
// A core may expose an empty selector, e.g. when no BIOS files are found.
if len(choices) == 0 {
continue
}
v := v
o.Vars = append(o.Vars, &Variable{
Key: v.Key(),
Desc: v.Desc(),
Choices: choices,
Choices: v.Choices(),
Default: v.DefaultValue(),
Choice: utils.IndexOfString(v.DefaultValue(), choices),
Choice: utils.IndexOfString(v.DefaultValue(), v.Choices()),
})
}
o.Updated = true
+10 -1
View File
@@ -33,6 +33,7 @@ func ipsAllocTargetData(patch, source []byte) ([]byte, error) {
size |= int(patch[offset]) << 8
offset++
size |= int(patch[offset]) << 0
offset++
targetLength = size
prov := make([]byte, targetLength)
return prov, nil
@@ -116,7 +117,15 @@ func applyIPS(patch, source []byte) (*[]byte, error) {
offset++
if address == EOF {
if offset == len(patch) || offset == len(patch)-3 {
if offset == len(patch) {
return &targetData, nil
} else if offset == len(patch)-3 {
size := int(patch[offset]) << 16
offset++
size |= int(patch[offset]) << 8
offset++
size |= int(patch[offset]) << 0
offset++
return &targetData, nil
}
}
+6 -3
View File
@@ -6,14 +6,15 @@ import (
"os"
"path/filepath"
"github.com/libretro/ludo/core"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
)
// Save the current state to the filesystem. name is the name of the
// savestate file to save to, without extension.
func Save(name string) error {
bytes, err := core.Serialize()
s := state.Core.SerializeSize()
bytes, err := state.Core.Serialize(s)
if err != nil {
return err
}
@@ -27,9 +28,11 @@ func Save(name string) error {
// Load the state from the filesystem
func Load(path string) error {
s := state.Core.SerializeSize()
bytes, err := os.ReadFile(path)
if err != nil {
return err
}
return core.Unserialize(bytes)
err = state.Core.Unserialize(bytes, s)
return err
}
-6
View File
@@ -43,17 +43,13 @@ func defaultSettings() Settings {
"Nintendo - Game Boy Advance": "mgba_libretro",
"Nintendo - Game Boy Color": "gambatte_libretro",
"Nintendo - Game Boy": "gambatte_libretro",
"Nintendo - GameCube": "dolphin_libretro",
"Nintendo - Nintendo 3DS": "citra_libretro",
"Nintendo - Nintendo 64": "mupen64plus_next_libretro",
"Nintendo - Nintendo Entertainment System": "fceumm_libretro",
"Nintendo - Nintendo DS": "melonds_libretro",
"Nintendo - Pokemon Mini": "pokemini_libretro",
"Nintendo - Super Nintendo Entertainment System": "snes9x_libretro",
"Nintendo - Virtual Boy": "mednafen_vb_libretro",
"Nintendo - Wii": "dolphin_libretro",
"Sega - 32X": "picodrive_libretro",
"Sega - Dreamcast": "flycast_libretro",
"Sega - Game Gear": "genesis_plus_gx_libretro",
"Sega - Master System - Mark III": "genesis_plus_gx_libretro",
"Sega - Mega Drive - Genesis": "genesis_plus_gx_libretro",
@@ -64,8 +60,6 @@ func defaultSettings() Settings {
"SNK - Neo Geo Pocket Color": "mednafen_ngp_libretro",
"SNK - Neo Geo Pocket": "mednafen_ngp_libretro",
"Sony - PlayStation": playstationCore,
"Sony - PlayStation 2": "pcsx2_libretro",
"Sony - PlayStation Portable": "ppsspp_libretro",
},
FileDirectory: usr.HomeDir,
CoresDirectory: "./cores",
+1 -9
View File
@@ -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
-190
View File
@@ -1,190 +0,0 @@
//go:build gltest && !darwin
package video
import (
"runtime"
"testing"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/go-gl/mathgl/mgl32"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/state"
)
// Run with go test -tags 'wayland gltest' ./video on a graphical session.
func TestHardwareContextIsolation(t *testing.T) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if err := glfw.Init(); err != nil {
t.Fatal(err)
}
defer glfw.Terminate()
glfw.DefaultWindowHints()
glfw.WindowHint(glfw.Visible, glfw.False)
glfw.WindowHint(glfw.ContextVersionMajor, 3)
glfw.WindowHint(glfw.ContextVersionMinor, 3)
glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
w, err := glfw.CreateWindow(128, 96, "Ludo GL test", nil, nil)
if err != nil {
t.Fatal(err)
}
defer w.Destroy()
w.MakeContextCurrent()
if err := initGL(); err != nil {
t.Fatal(err)
}
t.Log("GL:", gl.GoStr(gl.GetString(gl.VERSION)))
oldCore, oldRunning := state.Core, state.CoreRunning
defer func() { state.Core, state.CoreRunning = oldCore, oldRunning }()
hw := &libretro.HWRenderCallback{HWContextType: libretro.HWContextOpenGLCore, VersionMajor: 3, VersionMinor: 3}
state.Core = &libretro.Core{HWRenderCallback: hw}
state.CoreRunning = true
v := &Video{Window: w, identityMat: mgl32.Ident4(), Geom: libretro.GameGeometry{MaxWidth: 64, MaxHeight: 64}}
v.SetHWRenderContext(hw)
v.ensurePixelFormatDefaults()
v.resetGameTexture(64, 64)
hw.ContextReset = func() {
if glfw.GetCurrentContext() != v.HWWindow {
t.Fatal("context_reset ran outside the HW context")
}
}
if err := v.ResetHWContext(); err != nil {
t.Fatal(err)
}
defer v.DestroySharedHWContext()
if glfw.GetCurrentContext() != w || v.HWWindow == nil || v.HWWindow == w {
t.Fatal("reset did not return to an independent frontend context")
}
v.BeginCore()
var readFBO, texture, pbo uint32
gl.GenFramebuffers(1, &readFBO)
gl.BindFramebuffer(gl.READ_FRAMEBUFFER, readFBO)
gl.GenTextures(1, &texture)
gl.ActiveTexture(gl.TEXTURE1)
gl.BindTexture(gl.TEXTURE_2D, texture)
gl.GenBuffers(1, &pbo)
gl.BindBuffer(gl.PIXEL_PACK_BUFFER, pbo)
gl.PixelStorei(gl.PACK_ROW_LENGTH, 256)
gl.Enable(gl.BLEND)
gl.Enable(gl.SCISSOR_TEST)
gl.BlendEquation(gl.FUNC_REVERSE_SUBTRACT)
v.EndCore()
v.BeginFrontendFrame()
checkInt := func(name uint32, want int32) {
t.Helper()
var got int32
gl.GetIntegerv(name, &got)
if got != want {
t.Errorf("GL state %#x: got %d, want %d", name, got, want)
}
}
checkInt(gl.PIXEL_PACK_BUFFER_BINDING, 0)
checkInt(gl.PACK_ROW_LENGTH, 0)
checkInt(gl.READ_FRAMEBUFFER_BINDING, 0)
checkInt(gl.BLEND_EQUATION_RGB, gl.FUNC_ADD)
v.BeginCore()
checkInt(gl.PIXEL_PACK_BUFFER_BINDING, int32(pbo))
checkInt(gl.PACK_ROW_LENGTH, 256)
checkInt(gl.READ_FRAMEBUFFER_BINDING, int32(readFBO))
checkInt(gl.DRAW_FRAMEBUFFER_BINDING, int32(v.fboID))
checkInt(gl.BLEND_EQUATION_RGB, gl.FUNC_REVERSE_SUBTRACT)
checkInt(gl.ACTIVE_TEXTURE, gl.TEXTURE1)
checkInt(gl.TEXTURE_BINDING_2D, int32(texture))
if !gl.IsEnabled(gl.BLEND) || !gl.IsEnabled(gl.SCISSOR_TEST) {
t.Error("frontend rendering changed core enables")
}
gl.BindBuffer(gl.PIXEL_PACK_BUFFER, 0)
gl.PixelStorei(gl.PACK_ROW_LENGTH, 0)
gl.Disable(gl.SCISSOR_TEST)
gl.Disable(gl.BLEND)
gl.BindFramebuffer(gl.READ_FRAMEBUFFER, v.fboID)
// Verify the produced pixels are visible through the shared texture.
gl.ClearColor(1, 0, 0, 1)
gl.Clear(gl.COLOR_BUFFER_BIT)
v.EndCore()
var frontendFBO uint32
gl.GenFramebuffers(1, &frontendFBO)
gl.BindFramebuffer(gl.FRAMEBUFFER, frontendFBO)
gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, v.texID, 0)
var pixel [4]byte
gl.ReadPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(&pixel[0]))
if pixel != [4]byte{255, 0, 0, 255} {
t.Errorf("shared frame: got %v, want red", pixel)
}
v.BeginFrontendFrame()
// Backbuffer snapshots must retain their orientation for either core origin.
v.defaultProgram, err = newProgram(vertexShader, defaultFragmentShader)
if err != nil {
t.Fatal(err)
}
gl.GenVertexArrays(1, &v.vao)
gl.BindVertexArray(v.vao)
gl.GenBuffers(1, &v.vbo)
gl.BindBuffer(gl.ARRAY_BUFFER, v.vbo)
vert := uint32(gl.GetAttribLocation(v.defaultProgram, gl.Str("vert\x00")))
uv := uint32(gl.GetAttribLocation(v.defaultProgram, gl.Str("vertTexCoord\x00")))
gl.EnableVertexAttribArray(vert)
gl.VertexAttribPointerWithOffset(vert, 2, gl.FLOAT, false, 16, 0)
gl.EnableVertexAttribArray(uv)
gl.VertexAttribPointerWithOffset(uv, 2, gl.FLOAT, false, 16, 8)
v.width, v.height = 64, 64
for _, bottomLeft := range []bool{false, true} {
hw.BottomLeftOrigin = bottomLeft
v.BeginFrontendFrame()
fbw, fbh := w.GetFramebufferSize()
v.Geom.AspectRatio = float64(fbw) / float64(fbh)
v.presentW, v.presentH = int32(fbw), int32(fbh)
gl.ClearColor(1, 0, 0, 1)
gl.Clear(gl.COLOR_BUFFER_BIT)
gl.Enable(gl.SCISSOR_TEST)
gl.Scissor(0, int32(fbh/2), int32(fbw), int32(fbh/2))
gl.ClearColor(0, 0, 1, 1)
gl.Clear(gl.COLOR_BUFFER_BIT)
gl.Disable(gl.SCISSOR_TEST)
v.CachePresentedFrame()
if !v.RenderCachedFrame() {
t.Fatal("cached frame unavailable")
}
gl.ReadPixels(1, 1, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(&pixel[0]))
if pixel != [4]byte{255, 0, 0, 255} {
t.Errorf("cached frame, bottom-left=%v: got %v, want red", bottomLeft, pixel)
}
}
hw.ContextDestroy = func() {
if glfw.GetCurrentContext() != v.HWWindow {
t.Error("context_destroy ran outside the HW context")
}
}
v.RunContextDestroy()
v.ResetCoreState()
if !gl.IsFramebuffer(frontendFBO) {
t.Error("HW teardown deleted a frontend framebuffer with the same name")
}
if v.fboID != 0 || v.HWWindow != nil || v.cachedFrameValid || v.width != 0 {
t.Error("HW teardown left stale core resources")
}
// A subsequent core must get a fresh context, including on the GL 2.x sync fallback.
if err := v.ResetHWContext(); err != nil {
t.Fatal(err)
}
v.hwSync = false
v.BeginCore()
checkInt(gl.PACK_ROW_LENGTH, 0)
checkInt(gl.PIXEL_PACK_BUFFER_BINDING, 0)
checkInt(gl.BLEND_EQUATION_RGB, gl.FUNC_ADD)
if gl.IsEnabled(gl.BLEND) || gl.IsEnabled(gl.SCISSOR_TEST) {
t.Error("new core inherited the previous core's enables")
}
v.EndCore()
if e := gl.GetError(); e != gl.NO_ERROR {
t.Errorf("GL error: %#x", e)
}
}
+5
View File
@@ -4,9 +4,14 @@ var vertexShader = `
#if __VERSION__ >= 130
#define COMPAT_VARYING out
#define COMPAT_ATTRIBUTE in
#define COMPAT_TEXTURE texture
#define COMPAT_FRAGCOLOR FragColor
out vec4 COMPAT_FRAGCOLOR;
#else
#define COMPAT_VARYING varying
#define COMPAT_ATTRIBUTE attribute
#define COMPAT_TEXTURE texture2D
#define COMPAT_FRAGCOLOR gl_FragColor
#endif
COMPAT_ATTRIBUTE vec2 vert;
-83
View File
@@ -1,83 +0,0 @@
package video
import (
"log"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/mathgl/mgl32"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/state"
)
// InitFramebuffer initializes and configures the video frame buffer based on
// informations from the HWRenderCallback of the libretro core.
func (video *Video) InitFramebuffer() {
width := int32(video.Geom.MaxWidth)
height := int32(video.Geom.MaxHeight)
video.texWidth = width
video.texHeight = height
log.Printf("[Video]: Initializing HW render (%v x %v).\n", width, height)
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, video.texID)
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, nil)
genFramebuffers(1, &video.fboID)
bindFramebuffer(gl.FRAMEBUFFER, video.fboID)
libretro.SetCurrentFramebufferValue(uintptr(video.fboID))
framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, video.texID, 0)
hw := state.Core.HWRenderCallback
if hw.Depth {
genRenderbuffers(1, &video.rboID)
bindRenderbuffer(gl.RENDERBUFFER, video.rboID)
format := gl.DEPTH_COMPONENT16
if hw.Stencil {
format = gl.DEPTH24_STENCIL8
}
renderbufferStorage(gl.RENDERBUFFER, uint32(format), width, height)
bindRenderbuffer(gl.RENDERBUFFER, 0)
// Separate attachment points work with both core and EXT framebuffers.
framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, video.rboID)
if hw.Stencil {
framebufferRenderbuffer(gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, gl.RENDERBUFFER, video.rboID)
}
}
// Default origin is top left
video.orthoMat = mgl32.Ortho2D(-1, 1, -1, 1)
if hw.BottomLeftOrigin {
video.orthoMat = mgl32.Ortho2D(-1, 1, 1, -1)
}
if st := checkFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE {
log.Fatalf("[Video] Framebuffer is not complete. Error: %v\n", st)
}
gl.ClearColor(0, 0, 0, 1)
if hw.Depth && hw.Stencil {
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT)
} else if hw.Depth {
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
} else {
gl.Clear(gl.COLOR_BUFFER_BIT)
}
}
func bindBackbuffer() {
bindFramebuffer(gl.FRAMEBUFFER, 0)
}
func (video *Video) destroyFramebuffer() {
bindBackbuffer()
if video.rboID != 0 {
deleteRenderbuffers(1, &video.rboID)
video.rboID = 0
}
if video.fboID != 0 {
deleteFramebuffers(1, &video.fboID)
video.fboID = 0
}
}
+5 -6
View File
@@ -124,7 +124,11 @@ func (video *Video) ScissorEnd() {
gl.Disable(gl.SCISSOR_TEST)
}
func vertexArrayForFramebuffer(x, y, w, h, scale, ffbw, ffbh float32) []float32 {
func (video *Video) vertexArray(x, y, w, h, scale float32) []float32 {
fbw, fbh := video.Window.GetFramebufferSize()
ffbw := float32(fbw)
ffbh := float32(fbh)
w *= scale
h *= scale
@@ -139,11 +143,6 @@ func vertexArrayForFramebuffer(x, y, w, h, scale, ffbw, ffbh float32) []float32
}
}
func (video *Video) vertexArray(x, y, w, h, scale float32) []float32 {
fbw, fbh := video.Window.GetFramebufferSize()
return vertexArrayForFramebuffer(x, y, w, h, scale, float32(fbw), float32(fbh))
}
// DrawBorder draws a colored rectangle border
func (video *Video) DrawBorder(x, y, w, h, borderWidth float32, c Color) {
va := video.vertexArray(x, y, w, h, 1.0)
-63
View File
@@ -1,63 +0,0 @@
package video
import (
"errors"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/glfw/v3.4/glfw"
)
var (
glslVersion = "#version 120\n"
genVertexArrays = gl.GenVertexArrays
bindVertexArray = gl.BindVertexArray
genFramebuffers = gl.GenFramebuffers
bindFramebuffer = gl.BindFramebuffer
deleteFramebuffers = gl.DeleteFramebuffers
framebufferTexture2D = gl.FramebufferTexture2D
checkFramebufferStatus = gl.CheckFramebufferStatus
genRenderbuffers = gl.GenRenderbuffers
bindRenderbuffer = gl.BindRenderbuffer
deleteRenderbuffers = gl.DeleteRenderbuffers
renderbufferStorage = gl.RenderbufferStorage
framebufferRenderbuffer = gl.FramebufferRenderbuffer
)
// The frontend and hidden core context use the same hints. Select their API
// from the actual context capabilities, including macOS's legacy extensions.
func initGL() error {
if err := gl.Init(); err != nil {
return err
}
window := glfw.GetCurrentContext()
major, minor := window.GetAttrib(glfw.ContextVersionMajor), window.GetAttrib(glfw.ContextVersionMinor)
glslVersion = "#version 120\n"
if major > 3 || (major == 3 && minor >= 2) {
glslVersion = "#version 150\n"
} else if major == 3 {
glslVersion = "#version 130\n"
}
genVertexArrays, bindVertexArray = gl.GenVertexArrays, gl.BindVertexArray
genFramebuffers, bindFramebuffer, deleteFramebuffers = gl.GenFramebuffers, gl.BindFramebuffer, gl.DeleteFramebuffers
framebufferTexture2D, checkFramebufferStatus = gl.FramebufferTexture2D, gl.CheckFramebufferStatus
genRenderbuffers, bindRenderbuffer, deleteRenderbuffers = gl.GenRenderbuffers, gl.BindRenderbuffer, gl.DeleteRenderbuffers
renderbufferStorage, framebufferRenderbuffer = gl.RenderbufferStorage, gl.FramebufferRenderbuffer
if major < 3 && !glfw.ExtensionSupported("GL_ARB_vertex_array_object") {
if !glfw.ExtensionSupported("GL_APPLE_vertex_array_object") {
return errors.New("OpenGL vertex array objects are not supported")
}
genVertexArrays, bindVertexArray = gl.GenVertexArraysAPPLE, gl.BindVertexArrayAPPLE
}
if major < 3 && !glfw.ExtensionSupported("GL_ARB_framebuffer_object") {
if !glfw.ExtensionSupported("GL_EXT_framebuffer_object") {
return errors.New("OpenGL framebuffer objects are not supported")
}
genFramebuffers, bindFramebuffer, deleteFramebuffers = gl.GenFramebuffersEXT, gl.BindFramebufferEXT, gl.DeleteFramebuffersEXT
framebufferTexture2D, checkFramebufferStatus = gl.FramebufferTexture2DEXT, gl.CheckFramebufferStatusEXT
genRenderbuffers, bindRenderbuffer, deleteRenderbuffers = gl.GenRenderbuffersEXT, gl.BindRenderbufferEXT, gl.DeleteRenderbuffersEXT
renderbufferStorage, framebufferRenderbuffer = gl.RenderbufferStorageEXT, gl.FramebufferRenderbufferEXT
}
return nil
}
-25
View File
@@ -1,25 +0,0 @@
package video
import (
"testing"
"github.com/libretro/ludo/libretro"
)
func TestDuplicateFramePreservesDimensions(t *testing.T) {
v := &Video{}
v.Refresh(libretro.HWFrameBufferValid, 320, 240, 0)
v.Refresh(nil, 0, 0, 0)
if v.width != 320 || v.height != 240 || v.data != libretro.HWFrameBufferValid || !v.needUpload {
t.Fatal("NULL refresh discarded the pending frame")
}
v.needUpload = false
v.Refresh(nil, 640, 480, 0)
if v.width != 320 || v.height != 240 || v.needUpload {
t.Fatal("NULL refresh changed the previously presented frame")
}
}
func TestContextDestroyWithoutCore(t *testing.T) {
(&Video{}).RunContextDestroy()
}
+33 -31
View File
@@ -1,7 +1,6 @@
package video
import (
"errors"
"image"
"image/png"
"os"
@@ -14,33 +13,41 @@ import (
"github.com/libretro/ludo/state"
)
func (video *Video) captureBackbufferScreenshot(name string) error {
outW, outH := video.Geom.BaseWidth, video.Geom.BaseHeight
if outW < 1 || outH < 1 {
return errors.New("invalid screenshot output size")
}
// During the TakeScreenshot step, we need to render the current game frame at
// the right resolution to later capture it using ReadPixels. renderScreenshot
// taking care of this.
func (video *Video) renderScreenshot() {
va := video.vertexArray(0, 0, float32(video.Geom.BaseWidth), float32(video.Geom.BaseHeight), 1.0)
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW)
x := video.presentX
y := video.presentY
w := video.presentW
h := video.presentH
if !state.CoreRunning || video.width < 1 || video.height < 1 || w < 1 || h < 1 {
return errors.New("no game frame to capture")
}
bindVertexArray(video.vao)
var oldReadBuffer int32
gl.GetIntegerv(gl.READ_BUFFER, &oldReadBuffer)
gl.ReadBuffer(gl.BACK)
gl.BindTexture(gl.TEXTURE_2D, video.texID)
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
img := image.NewRGBA(image.Rect(0, 0, int(w), int(h)))
gl.Finish()
gl.ReadPixels(x, y, w, h, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(img.Pix))
gl.ReadBuffer(uint32(oldReadBuffer))
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
}
flipped := imaging.FlipV(img)
if flipped.Bounds().Dx() != outW || flipped.Bounds().Dy() != outH {
flipped = imaging.Resize(flipped, outW, outH, imaging.Linear)
}
// TakeScreenshot captures the ouput of video.Render and writes it to a file
func (video *Video) TakeScreenshot(name string) error {
state.MenuActive = false
defer func() { state.MenuActive = true }()
gl.UseProgram(video.defaultProgram)
video.renderScreenshot()
img := image.NewRGBA(image.Rect(0, 0, video.Geom.BaseWidth, video.Geom.BaseHeight))
_, fbh := video.Window.GetFramebufferSize()
gl.ReadPixels(
0, int32(fbh-video.Geom.BaseHeight),
int32(video.Geom.BaseWidth), int32(video.Geom.BaseHeight),
gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(img.Pix))
gl.UseProgram(video.program)
err := os.MkdirAll(settings.Current.ScreenshotsDirectory, os.ModePerm)
if err != nil {
@@ -52,13 +59,8 @@ func (video *Video) captureBackbufferScreenshot(name string) error {
if err != nil {
return err
}
defer fd.Close()
flipped := imaging.FlipV(img)
return png.Encode(fd, flipped)
}
// TakeScreenshotInFrontendFrame captures a screenshot while the caller already
// owns a frontend frame and has already rendered the current game frame.
func (video *Video) TakeScreenshotInFrontendFrame(name string) error {
return video.captureBackbufferScreenshot(name)
}
+20 -7
View File
@@ -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)
-78
View File
@@ -1,78 +0,0 @@
//go:build gltest && linux
package video
import (
"bytes"
"runtime"
"testing"
"unsafe"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/libretro/ludo/libretro"
"golang.org/x/sys/unix"
)
func TestSoftwareFrameUploadBounds(t *testing.T) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if err := glfw.Init(); err != nil {
t.Fatal(err)
}
defer glfw.Terminate()
glfw.DefaultWindowHints()
glfw.WindowHint(glfw.Visible, glfw.False)
w, err := glfw.CreateWindow(32, 32, "Ludo upload test", nil, nil)
if err != nil {
t.Fatal(err)
}
defer w.Destroy()
w.MakeContextCurrent()
if err := initGL(); err != nil {
t.Fatal(err)
}
v := &Video{Geom: libretro.GameGeometry{MaxWidth: 64, MaxHeight: 64}}
v.SetPixelFormat(libretro.PixelFormatXRGB8888)
v.resetGameTexture(1, 1)
defer gl.DeleteTextures(1, &v.texID)
v.program, err = newProgram(vertexShader, defaultFragmentShader)
if err != nil {
t.Fatal(err)
}
defer gl.DeleteProgram(v.program)
const width, height, pitch = 32, 32, 32 * 4
page := unix.Getpagesize()
memory, err := unix.Mmap(-1, 0, page*2, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_ANON|unix.MAP_PRIVATE)
if err != nil {
t.Fatal(err)
}
defer unix.Munmap(memory)
if err := unix.Mprotect(memory[page:], unix.PROT_NONE); err != nil {
t.Fatal(err)
}
// The core supplies only the current frame, ending at a protected page.
frame := memory[page-pitch*height : page]
for i := range frame {
frame[i] = byte(i)
}
v.Refresh(unsafe.Pointer(&frame[0]), width, height, pitch)
v.uploadTexture()
var texWidth, texHeight int32
gl.GetTexLevelParameteriv(gl.TEXTURE_2D, 0, gl.TEXTURE_WIDTH, &texWidth)
gl.GetTexLevelParameteriv(gl.TEXTURE_2D, 0, gl.TEXTURE_HEIGHT, &texHeight)
if texWidth != width || texHeight != height {
t.Fatalf("texture size = %dx%d, want %dx%d", texWidth, texHeight, width, height)
}
pixels := make([]byte, len(frame))
gl.GetTexImage(gl.TEXTURE_2D, 0, v.pixType, v.pixFmt, gl.Ptr(pixels))
if !bytes.Equal(pixels, frame) {
t.Error("uploaded pixels differ from the supplied frame")
}
if err := gl.GetError(); err != gl.NO_ERROR {
t.Errorf("GL error: %#x", err)
}
}
+461 -523
View File
File diff suppressed because it is too large. Load diff
+72
View File
@@ -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)
}
+73
View File
@@ -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)
}