Compare commits

...
76 Commits
Author SHA1 Message Date
Jean-André Santoni 1337ec455c Simplify core teardown 2026-10-19 13:11:48 +02:00
Jean-André Santoni 36a07d8fd6 Fix libco 2026-10-19 13:11:48 +02:00
Jean-André Santoni 33e3d215e6 Fix software frame uploads 2026-10-19 13:11:36 +02:00
Jean-André Santoni 920201f5f7 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
2026-10-18 21:30:04 +02:00
Jean-André Santoni c242440b06 Fix dolphin 2026-05-18 10:04:23 +02:00
Jean-André Santoni 36e74260a2 Fix order of destoy 2026-05-18 01:51:02 +02:00
Jean-André Santoni 52d1455923 Fix cleaning the display after closing the core 2026-05-18 01:34:32 +02:00
Jean-André Santoni 066b8af747 Reduce font.go churn 2026-05-18 01:25:50 +02:00
Jean-André Santoni fa026a036c Reduce font.go churn 2026-05-18 01:22:13 +02:00
Jean-André Santoni 5c4c2637ce Fix fullscreen toggle on flycast 2026-05-18 01:17:45 +02:00
Jean-André Santoni c68bffcaa2 Harden texture size 2026-05-18 01:14:55 +02:00
Jean-André Santoni 15b47eaf0d Reconfigure hints 2026-05-18 01:00:56 +02:00
Jean-André Santoni 6c53b45b40 Shared context 2026-05-18 00:52:39 +02:00
Jean-André Santoni bd0e8860f5 Return the fb from C 2026-05-18 00:22:02 +02:00
Jean-André Santoni c6e310c3c4 Start with a clean context 2026-05-18 00:01:37 +02:00
Jean-André Santoni 80c6a1ac13 Fix swanstation 2026-05-17 23:30:11 +02:00
Jean-André Santoni 054b69690c Merge remote-tracking branch 'origin/master' into kivutar/gl
# Conflicts:
#	Makefile
#	go.mod
#	go.sum
#	video/font.go
#	video/video.go
2026-05-17 23:27:34 +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
29 changed files with 1513 additions and 329 deletions

No files matched your search

+1 -1
View File
@@ -11,7 +11,7 @@ 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 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 swanstation,$(CORES))
+48 -19
View File
@@ -35,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
@@ -51,6 +52,7 @@ func Load(sofile string) error {
if err != nil {
return err
}
vid.SetHWRenderContext(nil)
state.Core.SetEnvironment(environment)
state.Core.Init()
state.Core.SetVideoRefresh(vid.Refresh)
@@ -218,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)
@@ -251,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")
}
@@ -283,30 +286,56 @@ func LoadGame(gamePath string) error {
log.Println("[Core]: Game loaded: " + gamePath)
savefiles.LoadSRAM()
if state.Core.HWRenderCallback != nil {
if err := vid.ResetHWContext(); err != nil {
Unload()
return fmt.Errorf("create hardware context: %w", err)
}
}
return nil
}
// Unload unloads a libretro core
// Unload closes the game and core before destroying their GL context.
func Unload() {
if state.Core != nil {
UnloadGame()
savefiles.SaveSRAM()
vid.RunContextDestroy()
vid.BeginCore()
if state.CoreRunning {
state.Core.UnloadGame()
}
state.Core.Deinit()
vid.EndCore()
vid.ResetCoreState()
state.GamePath = ""
state.CoreRunning = false
state.CorePath = ""
state.Core = nil
vid.SetHWRenderContext(nil)
Options = nil
}
}
// UnloadGame unloads a game.
func UnloadGame() {
if state.CoreRunning {
savefiles.SaveSRAM()
state.Core.UnloadGame()
state.GamePath = ""
state.CoreRunning = false
vid.ResetPitch()
vid.ResetRot()
}
// Reset resets the game using its hardware context, if any.
func Reset() {
vid.BeginCore()
defer vid.EndCore()
state.Core.Reset()
}
// Serialize captures core state with the core's GL context current.
func Serialize() ([]byte, error) {
vid.BeginCore()
defer vid.EndCore()
return state.Core.Serialize(state.Core.SerializeSize())
}
// Unserialize restores core state with the core's GL context current.
func Unserialize(data []byte) error {
vid.BeginCore()
defer vid.EndCore()
return state.Core.Unserialize(data, uint(len(data)))
}
// getGameInfo opens a rom and return the libretro.GameInfo needed to launch it
-2
View File
@@ -235,7 +235,6 @@ func Test_coreLoadGame(t *testing.T) {
}
})
UnloadGame()
Unload()
t.Run("Global state should be cleared by Unload", func(t *testing.T) {
@@ -252,7 +251,6 @@ func Test_coreLoadGame(t *testing.T) {
}
func Test_coreLoadGameWithoutCore(t *testing.T) {
UnloadGame()
Unload()
err := LoadGame("testdata/Polar Rescue (USA).vec")
+21
View File
@@ -143,6 +143,25 @@ func environment(cmd uint32, data unsafe.Pointer) bool {
state.Core.SetFrameTimeCallback(data)
case libretro.EnvironmentSetAudioCallback:
state.Core.SetAudioCallback(data)
case libretro.EnvironmentSetHWRender:
hw := libretro.SetHWRenderCallback(
data,
vid.CurrentFramebuffer,
vid.ProcAddress)
if (hw.HWContextType != libretro.HWContextOpenGL && hw.HWContextType != libretro.HWContextOpenGLCore) || hw.ContextReset == nil {
return false
}
state.Core.HWRenderCallback = hw
vid.SetHWRenderContext(hw)
log.Println("[Env]: HWContextType:", hw.HWContextType)
if vid != nil && vid.Window != nil && !state.CoreRunning {
vid.ReconfigureHWContext(settings.Current.VideoFullscreen)
state.MenuContextResetNeeded = true
}
return true
case libretro.EnvironmentSetHWSharedContext:
// All HW cores already get an isolated context shared with the frontend.
return true
case libretro.EnvironmentGetCanDupe:
libretro.SetBool(data, true)
case libretro.EnvironmentSetPixelFormat:
@@ -151,6 +170,8 @@ func environment(cmd uint32, data unsafe.Pointer) bool {
return environmentGetSystemDirectory(data)
case libretro.EnvironmentGetSaveDirectory:
return environmentGetSaveDirectory(data)
case libretro.EnvironmentGetPrefferedHWRender:
libretro.SetUint(data, uint(libretro.HWContextOpenGL))
case libretro.EnvironmentShutdown:
vid.SetShouldClose(true)
case libretro.EnvironmentGetCoreOptionsVersion:
+1
View File
@@ -7,6 +7,7 @@ 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
+3
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=
@@ -175,6 +177,7 @@ 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=
+47 -137
View File
@@ -6,121 +6,36 @@ package libretro
#include <stdarg.h>
#include <stdio.h>
#include <pthread.h>
#include <stdint.h>
#ifdef __APPLE__
#include <mach/semaphore.h>
#include <mach/task.h>
#include <mach/mach_init.h>
#define SEM_T semaphore_t
#define SEM_INIT(x) semaphore_create(mach_task_self(), &x, SYNC_POLICY_FIFO, 0);
#define SEM_POST(x) semaphore_signal(x)
#define SEM_WAIT(x) semaphore_wait(x)
#else
#include <semaphore.h>
#define SEM_T sem_t
#define SEM_INIT(x) sem_init(&x, 0, 0)
#define SEM_POST(x) sem_post(&x)
#define SEM_WAIT(x) sem_wait(&x)
#endif
#if 0
#define print_sema(...) (printf(__VA_ARGS__))
#else
#define print_sema(...) do {} while (0)
#endif
enum {
CMD_F,
CMD_SERIALIZE,
};
struct thread_cmd_t {
int cmd;
void* f;
void* arg1;
void* arg2;
void* arg3;
void* arg4;
void* res;
};
struct thread_cmd_t s_job;
static pthread_t s_thread;
static SEM_T s_sem_do;
static SEM_T s_sem_done;
static bool s_use_thread = false;
static uintptr_t s_current_framebuffer = 0;
void* emu_thread_loop(void *a0) {
print_sema("begin thread\n");
SEM_POST(s_sem_done);
print_sema("signal thread\n");
while (1) {
print_sema("wait do\n");
SEM_WAIT(s_sem_do);
print_sema("do\n");
switch (s_job.cmd) {
case CMD_F:
((void (*)(void))s_job.f)();
break;
case CMD_SERIALIZE: {
bool res = ((bool (*)(void*, size_t))s_job.f)(s_job.arg1, *(size_t*)s_job.arg2);
*(bool*)s_job.res = res;
break;
}
default:
break;
}
print_sema("signal done\n");
SEM_POST(s_sem_done);
}
// Go can track libco's alternate callback stacks only on a C-created thread.
static void *emu_thread_loop(void *unused) {
void coreThreadLoop();
coreThreadLoop();
return NULL;
}
void thread_sync() {
// Fire the job
print_sema("signal do\n");
SEM_POST(s_sem_do);
// Wait the result
print_sema("wait done\n");
SEM_WAIT(s_sem_done);
print_sema("done\n");
int cothread_init() {
return pthread_create(&s_thread, NULL, emu_thread_loop, NULL);
}
void run_wrapper(void *f) {
if (s_use_thread) {
s_job.cmd = CMD_F;
s_job.f = f;
thread_sync();
} else {
((void (*)(void))f)();
}
void cothread_deinit() {
pthread_join(s_thread, NULL);
}
void cothread_init() {
s_use_thread = true;
SEM_INIT(s_sem_do);
SEM_INIT(s_sem_done);
print_sema("create thread\n");
pthread_create(&s_thread, NULL, emu_thread_loop, NULL);
print_sema("wait thread\n");
SEM_WAIT(s_sem_done);
bool cothread_is_current() {
return pthread_equal(pthread_self(), s_thread);
}
void bridge_retro_init(void *f) {
run_wrapper(f);
((void (*)(void))f)();
}
void bridge_retro_deinit(void *f) {
run_wrapper(f);
((void (*)(void))f)();
}
unsigned bridge_retro_api_version(void *f) {
@@ -131,6 +46,14 @@ void bridge_retro_frame_time_callback(retro_frame_time_callback_t f, retro_usec_
f(usec);
}
void bridge_retro_hw_context_reset(retro_hw_context_reset_t f) {
f();
}
void bridge_retro_hw_context_destroy(retro_hw_context_reset_t f) {
f();
}
void bridge_retro_audio_callback(retro_audio_callback_t f) {
f();
}
@@ -152,7 +75,7 @@ void bridge_retro_set_environment(void *f, void *callback) {
}
void bridge_retro_set_video_refresh(void *f, void *callback) {
((bool (*)(retro_video_refresh_t))f)((retro_video_refresh_t)callback);
((void (*)(retro_video_refresh_t))f)((retro_video_refresh_t)callback);
}
void bridge_retro_set_controller_port_device(void *f, unsigned port, unsigned device) {
@@ -160,23 +83,23 @@ void bridge_retro_set_controller_port_device(void *f, unsigned port, unsigned de
}
void bridge_retro_set_input_poll(void *f, void *callback) {
((bool (*)(retro_input_poll_t))f)((retro_input_poll_t)callback);
((void (*)(retro_input_poll_t))f)((retro_input_poll_t)callback);
}
void bridge_retro_set_input_state(void *f, void *callback) {
((bool (*)(retro_input_state_t))f)((retro_input_state_t)callback);
((void (*)(retro_input_state_t))f)((retro_input_state_t)callback);
}
void bridge_retro_set_audio_sample(void *f, void *callback) {
((bool (*)(retro_audio_sample_t))f)((retro_audio_sample_t)callback);
((void (*)(retro_audio_sample_t))f)((retro_audio_sample_t)callback);
}
void bridge_retro_set_audio_sample_batch(void *f, void *callback) {
((bool (*)(retro_audio_sample_batch_t))f)((retro_audio_sample_batch_t)callback);
((void (*)(retro_audio_sample_batch_t))f)((retro_audio_sample_batch_t)callback);
}
bool bridge_retro_load_game(void *f, struct retro_game_info *gi) {
return ((bool (*)(struct retro_game_info *))f)(gi);
return ((bool (*)(const struct retro_game_info *))f)(gi);
}
size_t bridge_retro_serialize_size(void *f) {
@@ -184,49 +107,23 @@ size_t bridge_retro_serialize_size(void *f) {
}
bool bridge_retro_serialize(void *f, void *data, size_t size) {
if (s_use_thread) {
bool res;
s_job.cmd = CMD_SERIALIZE;
s_job.f = f;
s_job.arg1 = data;
s_job.arg2 = &size;
s_job.res = &res;
thread_sync();
return s_job.res;
} else {
return ((bool (*)(void*, size_t))f)(data, size);
}
return ((bool (*)(void*, size_t))f)(data, size);
}
bool bridge_retro_unserialize(void *f, void *data, size_t size) {
if (s_use_thread) {
bool res;
s_job.cmd = CMD_SERIALIZE; // Same command format for both serialize & unserialize
s_job.f = f;
s_job.arg1 = data;
s_job.arg2 = &size;
s_job.res = &res;
thread_sync();
return s_job.res;
} else {
return ((bool (*)(void*, size_t))f)(data, size);
}
return ((bool (*)(const void*, size_t))f)(data, size);
}
void bridge_retro_unload_game(void *f) {
run_wrapper(f);
((void (*)(void))f)();
}
void bridge_retro_run(void *f) {
run_wrapper(f);
((void (*)(void))f)();
}
void bridge_retro_reset(void *f) {
run_wrapper(f);
((void (*)(void))f)();
}
size_t bridge_retro_get_memory_size(void *f, unsigned id) {
@@ -303,5 +200,18 @@ int64_t coreGetTimeUsec_cgo() {
return coreGetTimeUsec();
}
uintptr_t coreGetCurrentFramebuffer_cgo() {
return s_current_framebuffer;
}
void coreSetCurrentFramebuffer_cgo(uintptr_t fb) {
s_current_framebuffer = fb;
}
uintptr_t coreGetProcAddress_cgo(const char *sym) {
uintptr_t coreGetProcAddress(const char *sym);
return coreGetProcAddress(sym);
}
*/
import "C"
+252 -48
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);
@@ -36,6 +38,8 @@ void bridge_retro_unload_game(void *f);
void bridge_retro_run(void *f);
void bridge_retro_reset(void *f);
void bridge_retro_frame_time_callback(retro_frame_time_callback_t f, retro_usec_t usec);
void bridge_retro_hw_context_reset(retro_hw_context_reset_t f);
void bridge_retro_hw_context_destroy(retro_hw_context_reset_t f);
void bridge_retro_audio_callback(retro_audio_callback_t f);
void bridge_retro_audio_set_state(retro_audio_set_state_callback_t f, bool state);
size_t bridge_retro_get_memory_size(void *f, unsigned id);
@@ -54,10 +58,15 @@ size_t coreAudioSampleBatch_cgo(const int16_t *data, size_t frames);
int16_t coreInputState_cgo(unsigned port, unsigned device, unsigned index, unsigned id);
void coreLog_cgo(enum retro_log_level level, const char *msg);
int64_t coreGetTimeUsec_cgo();
uintptr_t coreGetCurrentFramebuffer_cgo();
void coreSetCurrentFramebuffer_cgo(uintptr_t fb);
uintptr_t coreGetProcAddress_cgo(const char *sym);
*/
import "C"
import (
"errors"
"fmt"
"runtime"
"strings"
"unsafe"
)
@@ -219,6 +228,20 @@ type FrameTimeCallback struct {
Reference int64
}
// HWRenderCallback sets an interface to let a libretro core render with
// hardware acceleration.
type HWRenderCallback struct {
HWContextType uint32
ContextReset func()
Depth bool
Stencil bool
BottomLeftOrigin bool
VersionMajor, VersionMinor uint
CacheContext bool
ContextDestroy func()
DebugContext bool
}
// AudioCallback stores the audio callback itself and the SetState callback
type AudioCallback struct {
Callback func()
@@ -412,28 +435,125 @@ const (
MemoryVideoRAM = uint32(C.RETRO_MEMORY_VIDEO_RAM)
)
// Hardware contexts
const (
HWContextNone = uint32(C.RETRO_HW_CONTEXT_NONE)
HWContextOpenGL = uint32(C.RETRO_HW_CONTEXT_OPENGL)
HWContextOpenGLES2 = uint32(C.RETRO_HW_CONTEXT_OPENGLES2)
HWContextOpenGLCore = uint32(C.RETRO_HW_CONTEXT_OPENGL_CORE)
HWContextOpenGLES3 = uint32(C.RETRO_HW_CONTEXT_OPENGLES3)
HWContextOpenGLESVersion = uint32(C.RETRO_HW_CONTEXT_OPENGLES_VERSION)
HWContextVulkan = uint32(C.RETRO_HW_CONTEXT_VULKAN)
HWContextDummy = uint32(C.RETRO_HW_CONTEXT_DUMMY)
)
// Pass this to retro_video_refresh_t if rendering to hardware.
// Passing NULL to retro_video_refresh_t is still a frame dupe as normal.
var (
HWFrameBufferValid = unsafe.Pointer(C.RETRO_HW_FRAME_BUFFER_VALID)
)
type (
environmentFunc func(uint32, unsafe.Pointer) bool
videoRefreshFunc func(unsafe.Pointer, int32, int32, int32)
audioSampleFunc func(int16, int16)
audioSampleBatchFunc func([]byte, int32) int32
inputPollFunc func()
inputStateFunc func(uint, uint32, uint, uint) int16
logFunc func(uint32, string)
getTimeUsecFunc func() int64
environmentFunc func(uint32, unsafe.Pointer) bool
videoRefreshFunc func(unsafe.Pointer, int32, int32, int32)
audioSampleFunc func(int16, int16)
audioSampleBatchFunc func([]byte, int32) int32
inputPollFunc func()
inputStateFunc func(uint, uint32, uint, uint) int16
logFunc func(uint32, string)
getTimeUsecFunc func() int64
getCurrentFramebufferFunc func() uintptr
getProcAddressFunc func(string) uintptr
)
var (
environment environmentFunc
videoRefresh videoRefreshFunc
audioSample audioSampleFunc
audioSampleBatch audioSampleBatchFunc
inputPoll inputPollFunc
inputState inputStateFunc
log logFunc
getTimeUsec getTimeUsecFunc
environment environmentFunc
videoRefresh videoRefreshFunc
audioSample audioSampleFunc
audioSampleBatch audioSampleBatchFunc
inputPoll inputPollFunc
inputState inputStateFunc
log logFunc
getTimeUsec getTimeUsecFunc
getCurrentFramebuffer getCurrentFramebufferFunc
getProcAddress getProcAddressFunc
releaseContext func()
acquireContext func()
coreCalls chan func()
frontendCalls chan func()
)
// SetContextCallbacks hands the current graphics context between the calling
// thread and the native core thread. Calls remain synchronous.
func SetContextCallbacks(release, acquire func()) {
releaseContext, acquireContext = release, acquire
}
func coreReleaseContext() {
if releaseContext != nil {
releaseContext()
}
}
func coreAcquireContext() {
if acquireContext != nil {
acquireContext()
}
}
//export coreThreadLoop
func coreThreadLoop() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
for call := range coreCalls {
call()
}
}
// Calls are synchronous, but must service callbacks on the waiting thread.
// Pumping both directions also permits a callback to make a nested core call.
func threadCall(calls, callbacks chan func(), f func()) {
coreReleaseContext()
done := make(chan struct{})
calls <- func() {
coreAcquireContext()
f()
coreReleaseContext()
close(done)
}
for {
select {
case callback := <-callbacks:
callback()
case <-done:
coreAcquireContext()
return
}
}
}
// The frontend calls from its locked OS thread; core callbacks can reenter here.
func coreCall(f func()) {
if coreCalls == nil || bool(C.cothread_is_current()) {
f()
return
}
threadCall(coreCalls, frontendCalls, f)
}
func coreResult[T any](f func() T) (result T) {
coreCall(func() { result = f() })
return
}
func frontendCall(f func()) {
if coreCalls == nil || !bool(C.cothread_is_current()) {
f()
return
}
threadCall(frontendCalls, coreCalls, f)
}
// Load dynamically loads a libretro core at the given path and returns a Core instance
func Load(sofile string) (*Core, error) {
core := Core{}
@@ -444,7 +564,12 @@ func Load(sofile string) (*Core, error) {
return nil, err
}
C.cothread_init()
coreCalls, frontendCalls = make(chan func()), make(chan func())
if err := C.cothread_init(); err != 0 {
coreCalls, frontendCalls = nil, nil
DlClose(core.handle)
return nil, fmt.Errorf("start core thread: %s", C.GoString(C.strerror(err)))
}
core.symRetroInit = DlSym(core.handle, "retro_init")
core.symRetroDeinit = DlSym(core.handle, "retro_deinit")
@@ -473,18 +598,21 @@ func Load(sofile string) (*Core, error) {
// Init takes care of the library global initialization
func (core *Core) Init() {
C.bridge_retro_init(core.symRetroInit)
coreCall(func() { C.bridge_retro_init(core.symRetroInit) })
}
// APIVersion returns the RETRO_API_VERSION.
// Used to validate ABI compatibility when the API is revised.
func (core *Core) APIVersion() uint {
return uint(C.bridge_retro_api_version(core.symRetroAPIVersion))
return coreResult(func() uint { return uint(C.bridge_retro_api_version(core.symRetroAPIVersion)) })
}
// Deinit takes care of the library global deinitialization
func (core *Core) Deinit() {
C.bridge_retro_deinit(core.symRetroDeinit)
coreCall(func() { C.bridge_retro_deinit(core.symRetroDeinit) })
close(coreCalls)
C.cothread_deinit()
coreCalls, frontendCalls = nil, nil
DlClose(core.handle)
core.MemoryMap = nil
environment = nil
@@ -495,6 +623,8 @@ func (core *Core) Deinit() {
inputState = nil
log = nil
getTimeUsec = nil
getCurrentFramebuffer = nil
getProcAddress = nil
}
// Run runs the game for one video frame.
@@ -504,12 +634,12 @@ func (core *Core) Deinit() {
// a frame if GET_CAN_DUPE returns true.
// In this case, the video callback can take a NULL argument for data.
func (core *Core) Run() {
C.bridge_retro_run(core.symRetroRun)
coreCall(func() { C.bridge_retro_run(core.symRetroRun) })
}
// Reset resets the current game.
func (core *Core) Reset() {
C.bridge_retro_reset(core.symRetroReset)
coreCall(func() { C.bridge_retro_reset(core.symRetroReset) })
}
// GetSystemInfo returns statically known system info. Pointers provided in *info
@@ -517,7 +647,7 @@ func (core *Core) Reset() {
// Can be called at any time, even before retro_init().
func (core *Core) GetSystemInfo() SystemInfo {
rsi := C.struct_retro_system_info{}
C.bridge_retro_get_system_info(core.symRetroGetSystemInfo, &rsi)
coreCall(func() { C.bridge_retro_get_system_info(core.symRetroGetSystemInfo, &rsi) })
return SystemInfo{
LibraryName: C.GoString(rsi.library_name),
LibraryVersion: C.GoString(rsi.library_version),
@@ -535,7 +665,7 @@ func (core *Core) GetSystemInfo() SystemInfo {
// desire a particular aspect ratio.
func (core *Core) GetSystemAVInfo() SystemAVInfo {
avi := C.struct_retro_system_av_info{}
C.bridge_retro_get_system_av_info(core.symRetroGetSystemAVInfo, &avi)
coreCall(func() { C.bridge_retro_get_system_av_info(core.symRetroGetSystemAVInfo, &avi) })
return SystemAVInfo{
Geometry: GameGeometry{
AspectRatio: float64(avi.geometry.aspect_ratio),
@@ -557,7 +687,7 @@ func (core *Core) LoadGame(gi GameInfo) bool {
rgi.path = C.CString(gi.Path)
rgi.size = C.size_t(gi.Size)
rgi.data = gi.Data
return bool(C.bridge_retro_load_game(core.symRetroLoadGame, &rgi))
return coreResult(func() bool { return bool(C.bridge_retro_load_game(core.symRetroLoadGame, &rgi)) })
}
// SerializeSize returns the amount of data the implementation requires to serialize
@@ -566,18 +696,18 @@ func (core *Core) LoadGame(gi GameInfo) bool {
// returned size is never allowed to be larger than a previous returned
// value, to ensure that the frontend can allocate a save state buffer once.
func (core *Core) SerializeSize() uint {
return uint(C.bridge_retro_serialize_size(core.symRetroSerializeSize))
return coreResult(func() uint { return uint(C.bridge_retro_serialize_size(core.symRetroSerializeSize)) })
}
// Serialize serializes internal state and returns the state as a byte slice.
func (core *Core) Serialize(size uint) ([]byte, error) {
data := C.malloc(C.size_t(size))
ok := bool(C.bridge_retro_serialize(core.symRetroSerialize, data, C.size_t(size)))
defer C.free(data)
ok := coreResult(func() bool { return bool(C.bridge_retro_serialize(core.symRetroSerialize, data, C.size_t(size))) })
if !ok {
return nil, errors.New("retro_serialize failed")
}
bytes := C.GoBytes(data, C.int(size))
C.free(data)
return bytes, nil
}
@@ -589,7 +719,9 @@ func (core *Core) Unserialize(bytes []byte, size uint) error {
if uint(len(bytes)) > size {
size = uint(len(bytes))
}
ok := bool(C.bridge_retro_unserialize(core.symRetroUnserialize, unsafe.Pointer(&bytes[0]), C.size_t(size)))
ok := coreResult(func() bool {
return bool(C.bridge_retro_unserialize(core.symRetroUnserialize, unsafe.Pointer(&bytes[0]), C.size_t(size)))
})
if !ok {
return errors.New("retro_unserialize failed")
}
@@ -598,49 +730,51 @@ func (core *Core) Unserialize(bytes []byte, size uint) error {
// UnloadGame unloads a currently loaded game
func (core *Core) UnloadGame() {
C.bridge_retro_unload_game(core.symRetroUnloadGame)
coreCall(func() { C.bridge_retro_unload_game(core.symRetroUnloadGame) })
}
// SetEnvironment sets the environment callback.
// Must be called before Init
func (core *Core) SetEnvironment(f environmentFunc) {
environment = f
C.bridge_retro_set_environment(core.symRetroSetEnvironment, C.coreEnvironment_cgo)
coreCall(func() { C.bridge_retro_set_environment(core.symRetroSetEnvironment, C.coreEnvironment_cgo) })
}
// SetVideoRefresh sets the video refresh callback.
// Must be set before the first Run call
func (core *Core) SetVideoRefresh(f videoRefreshFunc) {
videoRefresh = f
C.bridge_retro_set_video_refresh(core.symRetroSetVideoRefresh, C.coreVideoRefresh_cgo)
coreCall(func() { C.bridge_retro_set_video_refresh(core.symRetroSetVideoRefresh, C.coreVideoRefresh_cgo) })
}
// SetAudioSample sets the audio sample callback.
// Must be set before the first Run call
func (core *Core) SetAudioSample(f audioSampleFunc) {
audioSample = f
C.bridge_retro_set_audio_sample(core.symRetroSetAudioSample, C.coreAudioSample_cgo)
coreCall(func() { C.bridge_retro_set_audio_sample(core.symRetroSetAudioSample, C.coreAudioSample_cgo) })
}
// SetAudioSampleBatch sets the audio sample batch callback.
// Must be set before the first Run call
func (core *Core) SetAudioSampleBatch(f audioSampleBatchFunc) {
audioSampleBatch = f
C.bridge_retro_set_audio_sample_batch(core.symRetroSetAudioSampleBatch, C.coreAudioSampleBatch_cgo)
coreCall(func() {
C.bridge_retro_set_audio_sample_batch(core.symRetroSetAudioSampleBatch, C.coreAudioSampleBatch_cgo)
})
}
// SetInputPoll sets the input poll callback.
// Must be set before the first Run call
func (core *Core) SetInputPoll(f inputPollFunc) {
inputPoll = f
C.bridge_retro_set_input_poll(core.symRetroSetInputPoll, C.coreInputPoll_cgo)
coreCall(func() { C.bridge_retro_set_input_poll(core.symRetroSetInputPoll, C.coreInputPoll_cgo) })
}
// SetInputState sets the input state callback.
// Must be set before the first Run call
func (core *Core) SetInputState(f inputStateFunc) {
inputState = f
C.bridge_retro_set_input_state(core.symRetroSetInputState, C.coreInputState_cgo)
coreCall(func() { C.bridge_retro_set_input_state(core.symRetroSetInputState, C.coreInputState_cgo) })
}
// BindLogCallback binds f to the log callback
@@ -659,7 +793,9 @@ func (core *Core) BindPerfCallback(data unsafe.Pointer, f getTimeUsecFunc) {
// SetControllerPortDevice sets the device type attached to a controller port
func (core *Core) SetControllerPortDevice(port uint, device uint32) {
C.bridge_retro_set_controller_port_device(core.symRetroSetControllerPortDevice, C.unsigned(port), C.unsigned(device))
coreCall(func() {
C.bridge_retro_set_controller_port_device(core.symRetroSetControllerPortDevice, C.unsigned(port), C.unsigned(device))
})
}
//export coreEnvironment
@@ -667,6 +803,12 @@ func coreEnvironment(cmd C.unsigned, data unsafe.Pointer) bool {
if environment == nil {
return false
}
// Only window operations need the frontend thread. Routine queries stay here.
if cmd == C.RETRO_ENVIRONMENT_SET_HW_RENDER || cmd == C.RETRO_ENVIRONMENT_SHUTDOWN {
var result bool
frontendCall(func() { result = environment(uint32(cmd), data) })
return result
}
return environment(uint32(cmd), data)
}
@@ -691,6 +833,12 @@ func coreInputState(port C.unsigned, device C.unsigned, index C.unsigned, id C.u
if inputState == nil {
return 0
}
// Mouse state is queried through GLFW rather than the polled input snapshot.
if device == C.RETRO_DEVICE_MOUSE {
var result C.int16_t
frontendCall(func() { result = C.int16_t(inputState(uint(port), uint32(device), uint(index), uint(id))) })
return result
}
return C.int16_t(inputState(uint(port), uint32(device), uint(index), uint(id)))
}
@@ -726,6 +874,21 @@ 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)))
}
// SetCurrentFramebufferValue updates the C-side framebuffer callback target
func SetCurrentFramebufferValue(fb uintptr) {
C.coreSetCurrentFramebuffer_cgo(C.uintptr_t(fb))
}
// SetData is a setter for the data of a GameInfo type
func (gi *GameInfo) SetData(bytes []byte) {
cstr := C.CString(string(bytes))
@@ -883,20 +1046,59 @@ func (core *Core) SetFrameTimeCallback(data unsafe.Pointer) {
ftc := &FrameTimeCallback{}
ftc.Reference = int64(c.reference)
ftc.Callback = func(usec int64) {
C.bridge_retro_frame_time_callback(c.callback, C.retro_usec_t(usec))
coreCall(func() { C.bridge_retro_frame_time_callback(c.callback, C.retro_usec_t(usec)) })
}
core.FrameTimeCallback = ftc
}
// SetHWRenderCallback is an environment callback helper to set the HWRenderCallback
func SetHWRenderCallback(
data unsafe.Pointer,
currentFramebuffer getCurrentFramebufferFunc,
procAddress getProcAddressFunc,
) *HWRenderCallback {
c := (*C.struct_retro_hw_render_callback)(data)
hwrc := HWRenderCallback{}
hwrc.HWContextType = uint32(c.context_type)
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.Depth = bool(c.depth)
hwrc.Stencil = bool(c.stencil)
hwrc.BottomLeftOrigin = bool(c.bottom_left_origin)
hwrc.VersionMajor = uint(c.version_major)
hwrc.VersionMinor = uint(c.version_minor)
hwrc.CacheContext = bool(c.cache_context)
if destroy != nil {
hwrc.ContextDestroy = func() {
coreCall(func() { C.bridge_retro_hw_context_destroy(destroy) })
}
}
hwrc.DebugContext = bool(c.debug_context)
return &hwrc
}
// SetAudioCallback is an environment callback helper to set the AudioCallback
func (core *Core) SetAudioCallback(data unsafe.Pointer) {
c := *(*C.struct_retro_audio_callback)(data)
auc := &AudioCallback{}
auc.Callback = func() {
C.bridge_retro_audio_callback(c.callback)
coreCall(func() { C.bridge_retro_audio_callback(c.callback) })
}
auc.SetState = func(state bool) {
C.bridge_retro_audio_set_state(c.set_state, C.bool(state))
coreCall(func() { C.bridge_retro_audio_set_state(c.set_state, C.bool(state)) })
}
core.AudioCallback = auc
}
@@ -904,13 +1106,15 @@ func (core *Core) SetAudioCallback(data unsafe.Pointer) {
// GetMemorySize returns the size of a region of the memory.
// See memory constants.
func (core *Core) GetMemorySize(id uint32) uint {
return uint(C.bridge_retro_get_memory_size(core.symRetroGetMemorySize, C.unsigned(id)))
return coreResult(func() uint { return uint(C.bridge_retro_get_memory_size(core.symRetroGetMemorySize, C.unsigned(id))) })
}
// GetMemoryData returns the size of a region of the memory.
// See memory constants.
func (core *Core) GetMemoryData(id uint32) unsafe.Pointer {
return C.bridge_retro_get_memory_data(core.symRetroGetMemoryData, C.unsigned(id))
return coreResult(func() unsafe.Pointer {
return C.bridge_retro_get_memory_data(core.symRetroGetMemoryData, C.unsigned(id))
})
}
// DiskControlCallback is an interface which frontend can use to eject and insert disk images
@@ -930,19 +1134,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
}
+1
View File
@@ -31,6 +31,7 @@ type Core struct {
AudioCallback *AudioCallback
FrameTimeCallback *FrameTimeCallback
DiskControlCallback *DiskControlCallback
HWRenderCallback *HWRenderCallback
MemoryMap []MemoryDescriptor
}
+36 -14
View File
@@ -30,20 +30,40 @@ 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()
for !vid.Window.ShouldClose() {
currTime = time.Now()
dt := float32(currTime.Sub(prevTime)) / 1000000000
if state.MenuContextResetNeeded {
m.ContextReset()
state.MenuContextResetNeeded = false
}
glfw.PollEvents()
m.ProcessHotkeys()
ntf.Process(dt)
vid.ResizeViewport()
m.UpdatePalette()
input.Poll()
if !state.MenuActive {
if state.CoreRunning {
vid.BeginCore()
state.Core.Run()
if state.Core.FrameTimeCallback != nil {
state.Core.FrameTimeCallback.Callback(state.Core.FrameTimeCallback.Reference)
@@ -51,18 +71,24 @@ func runLoop(vid *video.Video, m *menu.Menu) {
if state.Core.AudioCallback != nil {
state.Core.AudioCallback.Callback()
}
vid.EndCore()
}
vid.BeginFrontendFrame()
vid.Render()
vid.CachePresentedFrame()
m.RenderNotifications()
frame++
if frame%600 == 0 { // save sram about every 10 sec
savefiles.SaveSRAM()
}
} else {
m.Update(dt)
vid.Render()
vid.BeginFrontendFrame()
vid.RenderPresentedFrame()
fulfillPendingScreenshot(vid)
m.Render(dt)
m.RenderNotifications()
}
m.RenderNotifications()
if state.FastForward {
glfw.SwapInterval(0)
} else {
@@ -128,18 +154,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.ReconfigureVideo(settings.Current.VideoFullscreen)
err := settings.Save()
if err != nil {
ntf.DisplayAndLogf(ntf.Error, "Menu", "Error saving settings: %v", err)
+15
View File
@@ -6,6 +6,8 @@ package menu
import (
"path/filepath"
"github.com/libretro/ludo/core"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
@@ -109,6 +111,19 @@ func (m *Menu) ContextReset() {
}
}
// ReconfigureVideo recreates the window and restores any running HW-render core state.
func (m *Menu) ReconfigureVideo(fullscreen bool) {
m.Reconfigure(fullscreen)
if state.CoreRunning && state.Core.HWRenderCallback != nil {
if err := m.ResetHWContext(); err != nil {
core.Unload()
state.MenuActive = true
ntf.DisplayAndLog(ntf.Error, "Video", err.Error())
}
}
m.ContextReset()
}
// WarpToQuickMenu loads the contextual menu for games that are launched from
// the command line interface or from 'Load Game'.
func (m *Menu) WarpToQuickMenu() {
+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.")
}
}
},
})
+14 -11
View File
@@ -26,17 +26,20 @@ 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())
return
}
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.ReconfigureVideo(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.ReconfigureVideo(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())
+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)
}
+10
View File
@@ -33,3 +33,13 @@ var LudOS bool
// FastForward will run the core as fast as possible
var FastForward bool
// PendingScreenshotName is the next screenshot capture requested by the UI.
var PendingScreenshotName string
// PendingScreenshotDone is called after the deferred screenshot capture completes.
var PendingScreenshotDone func(error)
// MenuContextResetNeeded requests re-uploading menu GL assets after the
// frontend window/context is recreated.
var MenuContextResetNeeded bool
+188
View File
@@ -0,0 +1,188 @@
//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 := gl.Init(); 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()
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)
}
}
+3 -1
View File
@@ -19,8 +19,10 @@ COMPAT_ATTRIBUTE vec2 vertTexCoord;
COMPAT_VARYING vec2 fragTexCoord;
uniform mat4 MVP;
void main() {
fragTexCoord = vertTexCoord;
gl_Position = vec4(vert, 0.0, 1.0);
gl_Position = vec4(vert, 0.0, 1.0) * MVP;
}
` + "\x00"
+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)
+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()
}
+58 -32
View File
@@ -10,44 +10,57 @@ import (
"github.com/go-gl/gl/v2.1/gl"
"github.com/libretro/ludo/settings"
"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)
bindVertexArray(video.vao)
gl.BindTexture(gl.TEXTURE_2D, video.texID)
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
func (video *Video) screenshotOutputSize() (int32, int32) {
width := int32(video.Geom.BaseWidth)
height := int32(video.Geom.BaseHeight)
if width < 1 {
width = video.width
}
if height < 1 {
height = video.height
}
if width < 1 {
width = video.presentW
}
if height < 1 {
height = video.presentH
}
if width < 1 {
width = 1
}
if height < 1 {
height = 1
}
return width, height
}
// 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 }()
func (video *Video) captureBackbufferScreenshot(name string) error {
outW, outH := video.screenshotOutputSize()
gl.UseProgram(video.defaultProgram)
x := video.presentX
y := video.presentY
w := video.presentW
h := video.presentH
if w < 1 || h < 1 {
fbw, fbh := video.Window.GetFramebufferSize()
x, y, w, h = int32(0), int32(0), int32(fbw), int32(fbh)
}
video.renderScreenshot()
var oldReadBuffer int32
gl.GetIntegerv(gl.READ_BUFFER, &oldReadBuffer)
gl.ReadBuffer(gl.BACK)
img := image.NewRGBA(image.Rect(0, 0, video.Geom.BaseWidth, video.Geom.BaseHeight))
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))
_, 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 int32(flipped.Bounds().Dx()) != outW || int32(flipped.Bounds().Dy()) != outH {
flipped = imaging.Resize(flipped, int(outW), int(outH), imaging.Linear)
}
err := os.MkdirAll(settings.Current.ScreenshotsDirectory, os.ModePerm)
if err != nil {
@@ -59,8 +72,21 @@ func (video *Video) TakeScreenshot(name string) error {
if err != nil {
return err
}
flipped := imaging.FlipV(img)
defer fd.Close()
return png.Encode(fd, flipped)
}
// TakeScreenshot captures a screenshot without advancing the core.
func (video *Video) TakeScreenshot(name string) error {
video.BeginFrontendFrame()
video.Render()
return video.captureBackbufferScreenshot(name)
}
// 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)
}
+12
View File
@@ -5,6 +5,7 @@ import (
"strings"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/mathgl/mgl32"
)
func newProgram(vertexShaderSource, fragmentShaderSource string) (uint32, error) {
@@ -39,6 +40,17 @@ func newProgram(vertexShaderSource, fragmentShaderSource string) (uint32, error)
gl.DeleteShader(vertexShader)
gl.DeleteShader(fragmentShader)
// Only the core rendering code uses MVPs so we set it to identity
// to avoid issues with other parts of the program
gl.UseProgram(program)
mvp := gl.GetUniformLocation(program, gl.Str("MVP\x00"))
if mvp != -1 {
IdentityMatrix := mgl32.Ident4()
gl.UniformMatrix4fv(mvp, 1, false, &IdentityMatrix[0])
}
gl.UseProgram(0)
return program, nil
}
+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 := gl.Init(); 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)
}
}
+519 -38
View File
@@ -10,6 +10,7 @@ import (
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/go-gl/mathgl/mgl32"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
@@ -17,11 +18,13 @@ import (
// Video holds the state of the video package
type Video struct {
Window *glfw.Window
Geom libretro.GameGeometry
Font *Font
FontSm *Font
FontLg *Font
Window *glfw.Window
HWWindow *glfw.Window // hidden core context; shares objects, not GL state, with Window
Geom libretro.GameGeometry
Font *Font
FontSm *Font
FontLg *Font
title string
program uint32 // current program used for the game quad
defaultProgram uint32 // default program used for the game quad
@@ -34,6 +37,20 @@ type Video struct {
vao uint32 // vertex array object
vbo uint32 // vertex buffer object
texID uint32 // texture id
fboID uint32 // framebuffer id for hw-render cores
rboID uint32 // depth/stencil renderbuffer for hw-render cores
identityMat mgl32.Mat4
orthoMat mgl32.Mat4
texWidth int32
texHeight int32
presentX int32
presentY int32
presentW int32
presentH int32
cachedFrameTexID uint32
cachedFrameW int32
cachedFrameH int32
cachedFrameValid bool
pitch int32 // pitch set by the refresh callback
pixFmt uint32 // format set by the environment callback
@@ -44,11 +61,19 @@ type Video struct {
needUpload bool // true when the texture needs to be uploaded to the GPU
data unsafe.Pointer
hwContextType uint32
hwContextMajor int
hwContextMinor int
hwDebugContext bool
hwContextConfigured bool
hwSync bool
}
// Init instanciates the video package
func Init(fullscreen bool) *Video {
vid := &Video{}
vid := &Video{title: "Ludo"}
vid.identityMat = mgl32.Ident4()
vid.Configure(fullscreen)
return vid
}
@@ -56,11 +81,167 @@ func Init(fullscreen bool) *Video {
// Reconfigure destroys and recreates the window with new attributes
func (video *Video) Reconfigure(fullscreen bool) {
if video.Window != nil {
video.RunContextDestroy()
video.DestroySharedHWContext()
video.Window.Destroy()
}
video.Configure(fullscreen)
}
// ReconfigureHWContext recreates the visible window using the currently stored
// HW context hints while preserving the current window size when windowed.
func (video *Video) ReconfigureHWContext(fullscreen bool) {
width := 384 * 2
height := 240 * 2
if video.Window != nil && !fullscreen {
width, height = video.Window.GetSize()
if width < 1 {
width = 384 * 2
}
if height < 1 {
height = 240 * 2
}
}
if video.Window != nil {
video.RunContextDestroy()
video.DestroySharedHWContext()
video.Window.Destroy()
}
video.configureWithSize(width, height, fullscreen)
}
// CreateSharedHWContext creates a hidden context shared with the visible frontend context.
func (video *Video) CreateSharedHWContext() error {
if video.HWWindow != nil {
return nil
}
video.configureWindowHints()
glfw.WindowHint(glfw.Visible, glfw.False)
glfw.WindowHint(glfw.Focused, glfw.False)
width, height := video.Window.GetSize()
if width < 1 {
width = 1
}
if height < 1 {
height = 1
}
hwWindow, err := glfw.CreateWindow(width, height, "", nil, video.Window)
if err != nil {
return err
}
video.HWWindow = hwWindow
major := video.Window.GetAttrib(glfw.ContextVersionMajor)
minor := video.Window.GetAttrib(glfw.ContextVersionMinor)
video.hwSync = major > 3 || (major == 3 && minor >= 2) || glfw.ExtensionSupported("GL_ARB_sync")
return nil
}
// DestroySharedHWContext destroys the hidden shared context if it exists.
func (video *Video) DestroySharedHWContext() {
if video.HWWindow != nil {
video.BeginCore()
video.destroyFramebuffer()
libretro.SetCurrentFramebufferValue(0)
video.EndCore()
video.HWWindow.Destroy()
video.HWWindow = nil
}
}
// MakeFrontendContextCurrent switches back to the visible frontend context.
func (video *Video) MakeFrontendContextCurrent() {
if video.Window != nil {
video.Window.MakeContextCurrent()
}
}
// RunContextDestroy notifies the active core before its GL context is destroyed.
func (video *Video) RunContextDestroy() {
if !state.CoreRunning || state.Core == nil || video.HWWindow == nil {
return
}
hw := state.Core.HWRenderCallback
if hw != nil && hw.ContextDestroy != nil {
video.BeginCore()
hw.ContextDestroy()
video.EndCore()
}
}
// ResetHWContext creates the core's render target and resets its renderer.
func (video *Video) ResetHWContext() error {
if err := video.CreateSharedHWContext(); err != nil {
return err
}
video.BeginCore()
defer video.EndCore()
video.InitFramebuffer()
state.Core.HWRenderCallback.ContextReset()
return nil
}
// BeginCore enters the isolated context for run, reset, serialization or teardown.
func (video *Video) BeginCore() {
if video.HWWindow != nil {
video.switchContext(video.HWWindow)
}
}
// EndCore returns to the frontend after the core's GL work.
func (video *Video) EndCore() {
if video.HWWindow != nil {
video.switchContext(video.Window)
}
}
// ContextCallbacks returns paired release/acquire callbacks for synchronous
// context handoffs between the frontend and native core threads.
func (video *Video) ContextCallbacks() (release, acquire func()) {
var current *glfw.Window
release = func() {
current = nil
if video.Window == nil || state.Core == nil || state.Core.HWRenderCallback == nil {
return
}
current = glfw.GetCurrentContext()
// Before context_reset, HW negotiation may still use the visible context.
// Once the hidden context exists, leave the menu context on its own thread.
if video.HWWindow != nil && current != video.HWWindow {
current = nil
}
if current != nil {
glfw.DetachCurrentContext()
}
}
acquire = func() {
if current != nil {
current.MakeContextCurrent()
}
}
return
}
func (video *Video) switchContext(window *glfw.Window) {
if glfw.GetCurrentContext() == window {
return
}
// Order shared-texture writes and reads in both directions without a CPU wait.
if video.hwSync {
fence := gl.FenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0)
gl.Flush()
window.MakeContextCurrent()
gl.WaitSync(fence, 0, gl.TIMEOUT_IGNORED)
gl.DeleteSync(fence)
} else {
gl.Finish()
window.MakeContextCurrent()
}
}
// GetFramebufferSize retrieves the size, in pixels, of the framebuffer of the specified window.
func (video *Video) GetFramebufferSize() (int, int) {
if video.Window == nil {
@@ -71,12 +252,69 @@ func (video *Video) GetFramebufferSize() (int, int) {
// SetTitle sets the window title, encoded as UTF-8, of the window.
func (video *Video) SetTitle(title string) {
video.title = title
if video.Window == nil {
return
}
video.Window.SetTitle(title)
}
// SetHWRenderContext stores the context requested by the current libretro core.
func (video *Video) SetHWRenderContext(hw *libretro.HWRenderCallback) {
if hw == nil {
video.hwContextType = 0
video.hwContextMajor = 0
video.hwContextMinor = 0
video.hwDebugContext = false
video.hwContextConfigured = false
return
}
video.hwContextType = hw.HWContextType
video.hwContextMajor = int(hw.VersionMajor)
video.hwContextMinor = int(hw.VersionMinor)
video.hwDebugContext = hw.DebugContext
video.hwContextConfigured = true
}
func (video *Video) configureWindowHints() {
glfw.DefaultWindowHints()
if !video.hwContextConfigured {
return
}
switch video.hwContextType {
case libretro.HWContextOpenGLCore:
major := video.hwContextMajor
minor := video.hwContextMinor
glfw.WindowHint(glfw.ClientAPI, glfw.OpenGLAPI)
glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLAnyProfile)
if major > 0 {
glfw.WindowHint(glfw.ContextVersionMajor, major)
glfw.WindowHint(glfw.ContextVersionMinor, minor)
if major > 3 || (major == 3 && minor >= 2) {
glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True)
}
}
case libretro.HWContextOpenGL:
glfw.WindowHint(glfw.ClientAPI, glfw.OpenGLAPI)
if video.hwContextMajor > 0 {
glfw.WindowHint(glfw.ContextVersionMajor, video.hwContextMajor)
glfw.WindowHint(glfw.ContextVersionMinor, video.hwContextMinor)
if video.hwContextMajor > 3 || (video.hwContextMajor == 3 && video.hwContextMinor >= 2) {
glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCompatProfile)
glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.False)
}
}
}
if video.hwDebugContext {
glfw.WindowHint(glfw.OpenGLDebugContext, glfw.True)
}
}
// SetShouldClose sets the value of the close flag of the window.
func (video *Video) SetShouldClose(b bool) {
if video.Window == nil {
@@ -92,9 +330,53 @@ func panicOnErr(v uint32, err error) uint32 {
return v
}
func (video *Video) ensurePixelFormatDefaults() {
if video.pixFmt == 0 {
video.pixFmt = gl.UNSIGNED_SHORT_5_5_5_1
video.pixType = gl.BGRA
video.bpp = 2
}
}
func (video *Video) resetGameTexture(width, height int32) {
if width < 1 {
width = 1
}
if height < 1 {
height = 1
}
if video.texID != 0 {
gl.DeleteTextures(1, &video.texID)
video.texID = 0
}
gl.GenTextures(1, &video.texID)
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, video.texID)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, video.pixType, video.pixFmt, nil)
video.texWidth = width
video.texHeight = height
}
// Configure instanciates the video package
func (video *Video) Configure(fullscreen bool) {
var width, height int
video.configureWithSize(384*2, 240*2, fullscreen)
}
func (video *Video) configureWithSize(width, height int, fullscreen bool) {
// Object names from the old share group must not be reused in this context.
video.texID = 0
video.cachedFrameTexID = 0
video.cachedFrameW, video.cachedFrameH = 0, 0
video.cachedFrameValid = false
video.width, video.height = 0, 0
video.data = nil
video.needUpload = false
var m *glfw.Monitor
if fullscreen {
@@ -103,13 +385,12 @@ func (video *Video) Configure(fullscreen bool) {
vm := vms[len(vms)-1]
width = vm.Width
height = vm.Height
} else {
width = 384 * 2
height = 240 * 2
}
video.configureWindowHints()
var err error
video.Window, err = glfw.CreateWindow(width, height, "Ludo", m, nil)
video.Window, err = glfw.CreateWindow(width, height, video.title, m, nil)
if err != nil {
panic("Window creation failed:" + err.Error())
}
@@ -152,8 +433,9 @@ func (video *Video) Configure(fullscreen bool) {
video.borderProgram = panicOnErr(newProgram(vertexShader, borderFragmentShader))
video.circleProgram = panicOnErr(newProgram(vertexShader, circleFragmentShader))
video.UpdateFilter(settings.Current.VideoFilter)
video.program = video.defaultProgram
gl.UseProgram(video.program)
textureUniform := gl.GetUniformLocation(video.program, gl.Str("Texture\x00"))
gl.Uniform1i(textureUniform, 0)
@@ -174,26 +456,30 @@ func (video *Video) Configure(fullscreen bool) {
gl.VertexAttribPointerWithOffset(texCoordAttrib, 2, gl.FLOAT, false, 4*4, 2*4)
// Some cores won't call SetPixelFormat, provide default values
if video.pixFmt == 0 {
video.pixFmt = gl.UNSIGNED_SHORT_5_5_5_1
video.pixType = gl.BGRA
video.bpp = 2
video.ensurePixelFormatDefaults()
if video.Geom.MaxWidth == 0 || video.Geom.MaxHeight == 0 {
video.Geom.MaxWidth = video.Geom.BaseWidth
video.Geom.MaxHeight = video.Geom.BaseHeight
}
gl.GenTextures(1, &video.texID)
gl.ActiveTexture(gl.TEXTURE0)
if video.texID == 0 && state.Verbose {
log.Println("[Video]: Failed to create the vid texture")
video.texWidth = int32(video.Geom.MaxWidth)
video.texHeight = int32(video.Geom.MaxHeight)
if video.texWidth < 1 {
video.texWidth = 1
}
gl.BindTexture(gl.TEXTURE_2D, video.texID)
if video.texHeight < 1 {
video.texHeight = 1
}
video.resetGameTexture(video.texWidth, video.texHeight)
video.UpdateFilter(settings.Current.VideoFilter)
video.coreRatioViewport(fbw, fbh)
video.coreRatioViewport(fbw, fbh, video.Geom.BaseWidth, video.Geom.BaseHeight)
if e := gl.GetError(); e != gl.NO_ERROR {
bindVertexArray(0)
for e := gl.GetError(); e != gl.NO_ERROR; e = gl.NO_ERROR {
log.Printf("[Video] OpenGL error: %d\n", e)
}
}
@@ -206,6 +492,7 @@ func (video *Video) Configure(fullscreen bool) {
// CRT: zfast-crt
// LCD: zfast-lcd
func (video *Video) UpdateFilter(filter string) {
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, video.texID)
switch filter {
case "Smooth":
@@ -234,8 +521,9 @@ func (video *Video) UpdateFilter(filter string) {
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
gl.UseProgram(video.program)
gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("TextureSize\x00")), float32(video.width), float32(video.height))
gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("TextureSize\x00")), float32(video.texWidth), float32(video.texHeight))
gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("InputSize\x00")), float32(video.width), float32(video.height))
gl.UseProgram(0)
}
// SetPixelFormat is a callback passed to the libretro implementation.
@@ -283,9 +571,38 @@ func (video *Video) ResetRot() {
video.rot = 0
}
// ResetCoreState clears per-core video state and destroys its GL context.
func (video *Video) ResetCoreState() {
video.DestroySharedHWContext()
video.MakeFrontendContextCurrent()
libretro.SetCurrentFramebufferValue(0)
video.ResetPitch()
video.ResetRot()
video.needUpload = false
video.data = nil
video.width = 0
video.height = 0
video.pixFmt = 0
video.pixType = 0
video.bpp = 0
video.ensurePixelFormatDefaults()
if video.texID != 0 {
video.resetGameTexture(1, 1)
}
video.texWidth = 0
video.texHeight = 0
if video.cachedFrameTexID != 0 {
gl.DeleteTextures(1, &video.cachedFrameTexID)
video.cachedFrameTexID = 0
}
video.cachedFrameW = 0
video.cachedFrameH = 0
video.cachedFrameValid = false
}
// coreRatioViewport configures the vertex array to display the game at the center of the window
// while preserving the original ascpect ratio of the game or core
func (video *Video) coreRatioViewport(fbWidth int, fbHeight int) (x, y, w, h float32) {
func (video *Video) coreRatioViewport(fbWidth, fbHeight, clipWidth, clipHeight int) (x, y, w, h float32) {
// Scale the content to fit in the viewport.
fbw := float32(fbWidth)
fbh := float32(fbHeight)
@@ -307,7 +624,20 @@ func (video *Video) coreRatioViewport(fbWidth int, fbHeight int) (x, y, w, h flo
x = (fbw - w) / 2
y = (fbh - h) / 2
va := video.vertexArray(x, y, w, h, 1.0)
va := vertexArrayForFramebuffer(x, y, w, h, 1.0, fbw, fbh)
if clipWidth == 0 {
clipWidth = int(video.texWidth)
}
if clipHeight == 0 {
clipHeight = int(video.texHeight)
}
va[3] = float32(clipHeight) / float32(video.texHeight)
va[10] = float32(clipWidth) / float32(video.texWidth)
va[11] = va[3]
va[14] = va[10]
va = rotateUV(va, video.rot)
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW)
@@ -315,6 +645,29 @@ func (video *Video) coreRatioViewport(fbWidth int, fbHeight int) (x, y, w, h flo
return
}
// BeginFrontendFrame sets the state used by Ludo's game quad and menu.
// Core GL state lives in HWWindow and is never modified here.
func (video *Video) BeginFrontendFrame() {
// Render directly to the screen.
bindBackbuffer()
gl.DrawBuffer(gl.BACK)
gl.ReadBuffer(gl.BACK)
// Establish a frontend-owned GL baseline before drawing the game quad/menu.
gl.Disable(gl.DEPTH_TEST)
gl.Disable(gl.CULL_FACE)
gl.Disable(gl.DITHER)
gl.Disable(gl.SCISSOR_TEST)
gl.Disable(gl.STENCIL_TEST)
gl.Disable(gl.BLEND)
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
gl.BlendEquation(gl.FUNC_ADD)
gl.ColorMask(true, true, true, true)
gl.DepthMask(true)
video.ResizeViewport()
}
// ResizeViewport resizes the GL viewport to the framebuffer size
func (video *Video) ResizeViewport() {
fbw, fbh := video.Window.GetFramebufferSize()
@@ -330,45 +683,157 @@ func (video *Video) ResizeViewport() {
}
}
// Render the current frame
// Render draws the current game frame using the frontend context/state.
func (video *Video) Render() {
if !state.CoreRunning {
gl.ClearColor(1, 1, 1, 1)
gl.Clear(gl.COLOR_BUFFER_BIT)
return
}
gl.ClearColor(0, 0, 0, 1)
gl.Clear(gl.COLOR_BUFFER_BIT)
// Early return to not render the first frame of a newly loaded game with the
// previous game pitch. A sane pitch must be set by video.Refresh first.
if video.pitch == 0 {
// Wait for a valid refresh, not merely an allocated HW framebuffer.
if video.width == 0 || video.height == 0 {
return
}
video.uploadTexture()
fbw, fbh := video.Window.GetFramebufferSize()
_, _, w, h := video.coreRatioViewport(fbw, fbh)
x, y, w, h := video.coreRatioViewport(fbw, fbh, int(video.width), int(video.height))
video.presentX = int32(x)
video.presentY = int32(y)
video.presentW = int32(w)
video.presentH = int32(h)
gl.UseProgram(video.program)
if loc := gl.GetUniformLocation(video.program, gl.Str("color\x00")); loc >= 0 {
gl.Uniform4f(loc, 1, 1, 1, 1)
}
gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("OutputSize\x00")), w, h)
bindVertexArray(video.vao)
if state.Core.HWRenderCallback != nil {
gl.UniformMatrix4fv(gl.GetUniformLocation(video.program, gl.Str("MVP\x00")), 1, false, &video.orthoMat[0])
}
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, video.texID)
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
bindVertexArray(video.vao)
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
bindVertexArray(0)
// Reset MVP to identity to avoid menu issues
gl.UniformMatrix4fv(gl.GetUniformLocation(video.program, gl.Str("MVP\x00")), 1, false, &video.identityMat[0])
gl.UseProgram(0)
}
func (video *Video) ensureCachedFrameTexture(width, height int32) {
if width < 1 || height < 1 {
return
}
if video.cachedFrameTexID == 0 {
gl.GenTextures(1, &video.cachedFrameTexID)
}
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, video.cachedFrameTexID)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
if video.cachedFrameW != width || video.cachedFrameH != height {
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, nil)
video.cachedFrameW = width
video.cachedFrameH = height
}
}
// CachePresentedFrame copies the just-rendered frontend backbuffer so menu-mode
// rendering does not depend on the core keeping its paused framebuffer alive.
func (video *Video) CachePresentedFrame() {
if !state.CoreRunning || video.width == 0 || video.height == 0 {
video.cachedFrameValid = false
return
}
fbw, fbh := video.Window.GetFramebufferSize()
if fbw < 1 || fbh < 1 {
video.cachedFrameValid = false
return
}
video.ensureCachedFrameTexture(int32(fbw), int32(fbh))
if video.cachedFrameTexID == 0 {
video.cachedFrameValid = false
return
}
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, video.cachedFrameTexID)
gl.CopyTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, 0, 0, int32(fbw), int32(fbh))
video.cachedFrameValid = true
}
// RenderCachedFrame draws the last cached frontend-presented game frame.
func (video *Video) RenderCachedFrame() bool {
if !video.cachedFrameValid || video.cachedFrameTexID == 0 || video.cachedFrameW < 1 || video.cachedFrameH < 1 {
return false
}
gl.ClearColor(0, 0, 0, 1)
gl.Clear(gl.COLOR_BUFFER_BIT)
fbw, fbh := video.Window.GetFramebufferSize()
va := vertexArrayForFramebuffer(0, 0, float32(fbw), float32(fbh), 1.0, float32(fbw), float32(fbh))
// A backbuffer copy always has bottom-left origin, regardless of the core.
va[3] = 0
va[7] = 1
va[11] = 0
va[15] = 1
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW)
gl.UseProgram(video.defaultProgram)
gl.Uniform4f(gl.GetUniformLocation(video.defaultProgram, gl.Str("color\x00")), 1, 1, 1, 1)
gl.Uniform2f(gl.GetUniformLocation(video.defaultProgram, gl.Str("OutputSize\x00")), float32(fbw), float32(fbh))
gl.Uniform2f(gl.GetUniformLocation(video.defaultProgram, gl.Str("TextureSize\x00")), float32(video.cachedFrameW), float32(video.cachedFrameH))
gl.Uniform2f(gl.GetUniformLocation(video.defaultProgram, gl.Str("InputSize\x00")), float32(video.cachedFrameW), float32(video.cachedFrameH))
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, video.cachedFrameTexID)
bindVertexArray(video.vao)
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
bindVertexArray(0)
gl.UseProgram(0)
return true
}
// RenderPresentedFrame redraws the current game image for menu-mode frontend
// rendering, preferring the cached last-good frame when available.
func (video *Video) RenderPresentedFrame() {
if !video.RenderCachedFrame() {
video.Render()
video.CachePresentedFrame()
}
}
// Refresh the texture framebuffer
func (video *Video) Refresh(data unsafe.Pointer, width int32, height int32, pitch int32) {
// NULL duplicates the last frame; its dimensions need not be supplied.
if data == nil {
return
}
video.needUpload = true
video.width = width
video.height = height
video.pitch = pitch
video.data = data // maybe need a full copy
video.data = data
}
func (video *Video) uploadTexture() {
@@ -378,13 +843,29 @@ func (video *Video) uploadTexture() {
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, video.texID)
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, video.pitch/video.bpp)
if video.data != libretro.HWFrameBufferValid {
video.texWidth = video.width
video.texHeight = video.height
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, video.pitch/video.bpp)
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, video.texWidth, video.texHeight, 0, video.pixType, video.pixFmt, video.data)
}
gl.UseProgram(video.program)
gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("TextureSize\x00")), float32(video.width), float32(video.height))
gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("TextureSize\x00")), float32(video.texWidth), float32(video.texHeight))
gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("InputSize\x00")), float32(video.width), float32(video.height))
gl.UseProgram(0)
video.needUpload = false
}
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, video.width, video.height, 0, video.pixType, video.pixFmt, video.data)
// CurrentFramebuffer returns the current FBO ID
func (video *Video) CurrentFramebuffer() uintptr {
return uintptr(video.fboID)
}
// ProcAddress returns the address of the proc from GLFW
func (video *Video) ProcAddress(procName string) uintptr {
return uintptr(glfw.GetProcAddress(procName))
}
// SetRotation rotates the game image as requested by the core
+78
View File
@@ -1,11 +1,89 @@
//go:build darwin
// +build darwin
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, 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_COMPONENT16
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)
}
}
// 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 := gl.CheckFramebufferStatusEXT(gl.FRAMEBUFFER_EXT); st != gl.FRAMEBUFFER_COMPLETE_EXT {
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() {
gl.BindFramebufferEXT(gl.FRAMEBUFFER_EXT, 0)
}
func (video *Video) destroyFramebuffer() {
bindBackbuffer()
if video.rboID != 0 {
gl.DeleteRenderbuffersEXT(1, &video.rboID)
video.rboID = 0
}
if video.fboID != 0 {
gl.DeleteFramebuffersEXT(1, &video.fboID)
video.fboID = 0
}
}
func genVertexArrays(n int32, arrays *uint32) {
gl.GenVertexArraysAPPLE(n, arrays)
}
+79
View File
@@ -1,11 +1,90 @@
//go:build !darwin
// +build !darwin
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)
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_COMPONENT16
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)
}
}
// 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 := gl.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() {
gl.BindFramebuffer(gl.FRAMEBUFFER, 0)
}
func (video *Video) destroyFramebuffer() {
bindBackbuffer()
if video.rboID != 0 {
gl.DeleteRenderbuffers(1, &video.rboID)
video.rboID = 0
}
if video.fboID != 0 {
gl.DeleteFramebuffers(1, &video.fboID)
video.fboID = 0
}
}
func genVertexArrays(n int32, arrays *uint32) {
gl.GenVertexArrays(n, arrays)
}