Support GL cores (#91)

* Improve the libretro binding to support GL cores

* Start implementing InitFramebuffer

* TODO

* Try to implement ProcAddress

* Fix display of non gl cores

* Implement more of InitFramebuffer

* Move InitFramebuffer and call HWRenderCallback.ContextReset

* Comment

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

* Remove useless code

* List hardware contexts in the libretro binding

* Update dependency for GetProcAddress

* Fix framebuffer creation

* Fix gl.Clear

* Cast C.retro_hw_context_reset_t

* Set c.get_current_framebuffer and c.get_proc_address properly

* Lint fix

* Fix compat with non GL cores

* Debug

* Fix an important memory bug

* Various rendering improvements

* Small fix

* Active texture fixes

* Unbind vertex array

* OpenGL core support improvements (#308)

* Fix sample batch callback and OpenGL core support

* Fix texture/framebuffer allocation and clipping

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

* Fix context destruction

Fixes Flycast

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

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

* Call gl.TexSubImage2D after binding the texture

* Fix shader live update

* Temporarily fix tests

* Fix FBO on OSX (#313)

* Fix FBO on OSX

* Remove useless code

* Use GL v2.1 for fonts drawing

* Fix fonts on OSX with GL 2.1

* Rename

* Same thing for GenVertexArrays

* Make fl0w work on OSX

* Implement libretro.EnvironmentGetPrefferedHWRenderer

* Uncomment gl.Enable(gl.TEXTURE_2D)

* Fixes

* Fix

* Fix

* Forgot to use program before get uniform

* Fix swanstation

* Start with a clean context

* Return the fb from C

* Shared context

* Reconfigure hints

* Harden texture size

* Fix fullscreen toggle on flycast

* Reduce font.go churn

* Reduce font.go churn

* Fix cleaning the display after closing the core

* Fix order of destoy

* Fix dolphin

* Launch more GL cores (#591)

* lrps2 booting

* Fix viewport

* Fix sampler

* Refactor

* Fix screenshots

* Fix dolphin screenshots

* Fixes

* Fix launching swanstation with -L

* Isolate core GL contexts

* Fix software frame uploads

* Fix libco

* Simplify screenshot sizing

* Add more GL cores

* Clean up frame capture

* Fix macOS GL profiles

* Preserve contexts on fullscreen toggle

* Fix screenshot cropping after resize

* Fix texture state on core switches

* Handle unsupported GL contexts

* Save states despite thumbnail errors

* Fix core loading error check

* Fix notification scaling

* Handle empty core options

---------

Co-authored-by: Higor Eurípedes <[email protected]>
This commit is contained in:
Jean-André SantoniandHigor Eurípedes authored and GitHub committed 2026-10-22 14:58:24 +02:00
1 parent 13347e0051
commit c47ad16a70
37 files changed
+1543 -402

No files matched your search

+12 -9
View File
@@ -11,19 +11,22 @@ ifeq ($(OS), Linux)
BUNDLENAME = $(APP)-$(OS)-$(DISPDRIVER)-$(ARCH)-$(VERSION)
endif
CORES = atari800 bluemsx swanstation fbneo fceumm gambatte gearsystem genesis_plus_gx handy lutro mednafen_ngp mednafen_pce mednafen_pce_fast mednafen_pcfx mednafen_psx mednafen_saturn mednafen_supergrafx mednafen_vb mednafen_wswan mgba melonds mupen64plus_next np2kai o2em pcsx_rearmed picodrive pokemini prosystem snes9x stella2014 vecx virtualjaguar
CORES = ppsspp dolphin atari800 bluemsx citra swanstation fbneo fceumm flycast gambatte gearsystem genesis_plus_gx handy lutro mednafen_ngp mednafen_pce mednafen_pce_fast mednafen_pcfx mednafen_psx mednafen_saturn mednafen_supergrafx mednafen_vb mednafen_wswan mgba melonds mupen64plus_next np2kai o2em pcsx2 pcsx_rearmed picodrive pokemini prosystem snes9x stella2014 vecx virtualjaguar
ifeq ($(ARCH), arm)
CORES := $(filter-out swanstation,$(CORES))
CORES := $(filter-out mupen64plus_next,$(CORES))
CORES := $(filter-out mednafen_pcfx,$(CORES))
CORES := $(filter-out mednafen_saturn,$(CORES))
CORES := $(filter-out melonds,$(CORES))
CORES := $(filter-out citra dolphin flycast mednafen_pcfx mednafen_saturn melonds mupen64plus_next pcsx2 ppsspp swanstation,$(CORES))
endif
ifeq ($(ARCH), arm64)
CORES := $(filter-out mednafen_saturn,$(CORES))
CORES := $(filter-out mupen64plus_next,$(CORES))
CORES := $(filter-out picodrive,$(CORES))
CORES := $(filter-out pcsx2,$(CORES))
ifeq ($(OS), Linux)
CORES := $(filter-out mupen64plus_next,$(CORES))
endif
endif
ifeq ($(OS), OSX)
CORES := $(filter-out pcsx2,$(CORES))
ifeq ($(ARCH), x86_64)
CORES := $(filter-out citra,$(CORES))
endif
endif
DYLIBS = $(addprefix cores/, $(addsuffix _libretro.dylib,$(CORES)))
+1 -1
View File
@@ -4,7 +4,7 @@ Ludo is a work in progress libretro frontend written in go.
<img src="https://raw.githubusercontent.com/kivutar/ludo-assets/master/illustration.png" />
It is able to launch most non GL libretro cores.
It supports software-rendered and desktop OpenGL libretro cores. Hardware-rendered cores may require a newer OpenGL version than the frontend itself.
It works on OSX, Linux, Linux ARM and Windows. You can download releases [here](https://github.com/libretro/ludo/releases)
+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
@@ -267,7 +267,6 @@ func Test_coreLoadGame(t *testing.T) {
}
})
UnloadGame()
Unload()
t.Run("Global state should be cleared by Unload", func(t *testing.T) {
@@ -284,7 +283,6 @@ func Test_coreLoadGame(t *testing.T) {
}
func Test_coreLoadGameWithoutCore(t *testing.T) {
UnloadGame()
Unload()
err := LoadGame("testdata/Polar Rescue (USA).vec")
+26
View File
@@ -143,6 +143,30 @@ func environment(cmd uint32, data unsafe.Pointer) bool {
state.Core.SetFrameTimeCallback(data)
case libretro.EnvironmentSetAudioCallback:
state.Core.SetAudioCallback(data)
case libretro.EnvironmentSetHWRender:
hw := libretro.SetHWRenderCallback(
data,
vid.CurrentFramebuffer,
vid.ProcAddress)
if (hw.HWContextType != libretro.HWContextOpenGL && hw.HWContextType != libretro.HWContextOpenGLCore) || hw.ContextReset == nil {
return false
}
vid.SetHWRenderContext(hw)
if vid != nil && vid.Window != nil && !state.CoreRunning {
if err := vid.ReconfigureHWContext(settings.Current.VideoFullscreen); err != nil {
vid.SetHWRenderContext(state.Core.HWRenderCallback)
log.Printf("[Env]: Could not create HW context %d (%d.%d): %v",
hw.HWContextType, hw.VersionMajor, hw.VersionMinor, err)
return false
}
state.MenuContextResetNeeded = true
}
state.Core.HWRenderCallback = hw
log.Println("[Env]: HWContextType:", hw.HWContextType)
return true
case libretro.EnvironmentSetHWSharedContext:
// All HW cores already get an isolated context shared with the frontend.
return true
case libretro.EnvironmentGetCanDupe:
libretro.SetBool(data, true)
case libretro.EnvironmentSetPixelFormat:
@@ -151,6 +175,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.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
+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"
+236 -40
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,15 +435,35 @@ 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 (
@@ -432,8 +475,84 @@ var (
inputState inputStateFunc
log logFunc
getTimeUsec getTimeUsecFunc
getProcAddress getProcAddressFunc
releaseContext func()
acquireContext func()
coreCalls chan func()
frontendCalls chan func()
)
// SetContextCallbacks hands the current graphics context between the calling
// thread and the native core thread. Calls remain synchronous.
func SetContextCallbacks(release, acquire func()) {
releaseContext, acquireContext = release, acquire
}
func coreReleaseContext() {
if releaseContext != nil {
releaseContext()
}
}
func coreAcquireContext() {
if acquireContext != nil {
acquireContext()
}
}
//export coreThreadLoop
func coreThreadLoop() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
for call := range coreCalls {
call()
}
}
// Calls are synchronous, but must service callbacks on the waiting thread.
// Pumping both directions also permits a callback to make a nested core call.
func threadCall(calls, callbacks chan func(), f func()) {
coreReleaseContext()
done := make(chan struct{})
calls <- func() {
coreAcquireContext()
f()
coreReleaseContext()
close(done)
}
for {
select {
case callback := <-callbacks:
callback()
case <-done:
coreAcquireContext()
return
}
}
}
// The frontend calls from its locked OS thread; core callbacks can reenter here.
func coreCall(f func()) {
if coreCalls == nil || bool(C.cothread_is_current()) {
f()
return
}
threadCall(coreCalls, frontendCalls, f)
}
func coreResult[T any](f func() T) (result T) {
coreCall(func() { result = f() })
return
}
func frontendCall(f func()) {
if coreCalls == nil || !bool(C.cothread_is_current()) {
f()
return
}
threadCall(frontendCalls, coreCalls, f)
}
// Load dynamically loads a libretro core at the given path and returns a Core instance
func Load(sofile string) (*Core, error) {
core := Core{}
@@ -444,7 +563,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 +597,21 @@ func Load(sofile string) (*Core, error) {
// Init takes care of the library global initialization
func (core *Core) Init() {
C.bridge_retro_init(core.symRetroInit)
coreCall(func() { C.bridge_retro_init(core.symRetroInit) })
}
// APIVersion returns the RETRO_API_VERSION.
// Used to validate ABI compatibility when the API is revised.
func (core *Core) APIVersion() uint {
return uint(C.bridge_retro_api_version(core.symRetroAPIVersion))
return coreResult(func() uint { return uint(C.bridge_retro_api_version(core.symRetroAPIVersion)) })
}
// Deinit takes care of the library global deinitialization
func (core *Core) Deinit() {
C.bridge_retro_deinit(core.symRetroDeinit)
coreCall(func() { C.bridge_retro_deinit(core.symRetroDeinit) })
close(coreCalls)
C.cothread_deinit()
coreCalls, frontendCalls = nil, nil
DlClose(core.handle)
core.MemoryMap = nil
environment = nil
@@ -495,6 +622,7 @@ func (core *Core) Deinit() {
inputState = nil
log = nil
getTimeUsec = nil
getProcAddress = nil
}
// Run runs the game for one video frame.
@@ -504,12 +632,12 @@ func (core *Core) Deinit() {
// a frame if GET_CAN_DUPE returns true.
// In this case, the video callback can take a NULL argument for data.
func (core *Core) Run() {
C.bridge_retro_run(core.symRetroRun)
coreCall(func() { C.bridge_retro_run(core.symRetroRun) })
}
// Reset resets the current game.
func (core *Core) Reset() {
C.bridge_retro_reset(core.symRetroReset)
coreCall(func() { C.bridge_retro_reset(core.symRetroReset) })
}
// GetSystemInfo returns statically known system info. Pointers provided in *info
@@ -517,7 +645,7 @@ func (core *Core) Reset() {
// Can be called at any time, even before retro_init().
func (core *Core) GetSystemInfo() SystemInfo {
rsi := C.struct_retro_system_info{}
C.bridge_retro_get_system_info(core.symRetroGetSystemInfo, &rsi)
coreCall(func() { C.bridge_retro_get_system_info(core.symRetroGetSystemInfo, &rsi) })
return SystemInfo{
LibraryName: C.GoString(rsi.library_name),
LibraryVersion: C.GoString(rsi.library_version),
@@ -535,7 +663,7 @@ func (core *Core) GetSystemInfo() SystemInfo {
// desire a particular aspect ratio.
func (core *Core) GetSystemAVInfo() SystemAVInfo {
avi := C.struct_retro_system_av_info{}
C.bridge_retro_get_system_av_info(core.symRetroGetSystemAVInfo, &avi)
coreCall(func() { C.bridge_retro_get_system_av_info(core.symRetroGetSystemAVInfo, &avi) })
return SystemAVInfo{
Geometry: GameGeometry{
AspectRatio: float64(avi.geometry.aspect_ratio),
@@ -557,7 +685,7 @@ func (core *Core) LoadGame(gi GameInfo) bool {
rgi.path = C.CString(gi.Path)
rgi.size = C.size_t(gi.Size)
rgi.data = gi.Data
return bool(C.bridge_retro_load_game(core.symRetroLoadGame, &rgi))
return coreResult(func() bool { return bool(C.bridge_retro_load_game(core.symRetroLoadGame, &rgi)) })
}
// SerializeSize returns the amount of data the implementation requires to serialize
@@ -566,18 +694,18 @@ func (core *Core) LoadGame(gi GameInfo) bool {
// returned size is never allowed to be larger than a previous returned
// value, to ensure that the frontend can allocate a save state buffer once.
func (core *Core) SerializeSize() uint {
return uint(C.bridge_retro_serialize_size(core.symRetroSerializeSize))
return coreResult(func() uint { return uint(C.bridge_retro_serialize_size(core.symRetroSerializeSize)) })
}
// Serialize serializes internal state and returns the state as a byte slice.
func (core *Core) Serialize(size uint) ([]byte, error) {
data := C.malloc(C.size_t(size))
ok := bool(C.bridge_retro_serialize(core.symRetroSerialize, data, C.size_t(size)))
defer C.free(data)
ok := coreResult(func() bool { return bool(C.bridge_retro_serialize(core.symRetroSerialize, data, C.size_t(size))) })
if !ok {
return nil, errors.New("retro_serialize failed")
}
bytes := C.GoBytes(data, C.int(size))
C.free(data)
return bytes, nil
}
@@ -589,7 +717,9 @@ func (core *Core) Unserialize(bytes []byte, size uint) error {
if uint(len(bytes)) > size {
size = uint(len(bytes))
}
ok := bool(C.bridge_retro_unserialize(core.symRetroUnserialize, unsafe.Pointer(&bytes[0]), C.size_t(size)))
ok := coreResult(func() bool {
return bool(C.bridge_retro_unserialize(core.symRetroUnserialize, unsafe.Pointer(&bytes[0]), C.size_t(size)))
})
if !ok {
return errors.New("retro_unserialize failed")
}
@@ -598,49 +728,51 @@ func (core *Core) Unserialize(bytes []byte, size uint) error {
// UnloadGame unloads a currently loaded game
func (core *Core) UnloadGame() {
C.bridge_retro_unload_game(core.symRetroUnloadGame)
coreCall(func() { C.bridge_retro_unload_game(core.symRetroUnloadGame) })
}
// SetEnvironment sets the environment callback.
// Must be called before Init
func (core *Core) SetEnvironment(f environmentFunc) {
environment = f
C.bridge_retro_set_environment(core.symRetroSetEnvironment, C.coreEnvironment_cgo)
coreCall(func() { C.bridge_retro_set_environment(core.symRetroSetEnvironment, C.coreEnvironment_cgo) })
}
// SetVideoRefresh sets the video refresh callback.
// Must be set before the first Run call
func (core *Core) SetVideoRefresh(f videoRefreshFunc) {
videoRefresh = f
C.bridge_retro_set_video_refresh(core.symRetroSetVideoRefresh, C.coreVideoRefresh_cgo)
coreCall(func() { C.bridge_retro_set_video_refresh(core.symRetroSetVideoRefresh, C.coreVideoRefresh_cgo) })
}
// SetAudioSample sets the audio sample callback.
// Must be set before the first Run call
func (core *Core) SetAudioSample(f audioSampleFunc) {
audioSample = f
C.bridge_retro_set_audio_sample(core.symRetroSetAudioSample, C.coreAudioSample_cgo)
coreCall(func() { C.bridge_retro_set_audio_sample(core.symRetroSetAudioSample, C.coreAudioSample_cgo) })
}
// SetAudioSampleBatch sets the audio sample batch callback.
// Must be set before the first Run call
func (core *Core) SetAudioSampleBatch(f audioSampleBatchFunc) {
audioSampleBatch = f
C.bridge_retro_set_audio_sample_batch(core.symRetroSetAudioSampleBatch, C.coreAudioSampleBatch_cgo)
coreCall(func() {
C.bridge_retro_set_audio_sample_batch(core.symRetroSetAudioSampleBatch, C.coreAudioSampleBatch_cgo)
})
}
// SetInputPoll sets the input poll callback.
// Must be set before the first Run call
func (core *Core) SetInputPoll(f inputPollFunc) {
inputPoll = f
C.bridge_retro_set_input_poll(core.symRetroSetInputPoll, C.coreInputPoll_cgo)
coreCall(func() { C.bridge_retro_set_input_poll(core.symRetroSetInputPoll, C.coreInputPoll_cgo) })
}
// SetInputState sets the input state callback.
// Must be set before the first Run call
func (core *Core) SetInputState(f inputStateFunc) {
inputState = f
C.bridge_retro_set_input_state(core.symRetroSetInputState, C.coreInputState_cgo)
coreCall(func() { C.bridge_retro_set_input_state(core.symRetroSetInputState, C.coreInputState_cgo) })
}
// BindLogCallback binds f to the log callback
@@ -659,7 +791,9 @@ func (core *Core) BindPerfCallback(data unsafe.Pointer, f getTimeUsecFunc) {
// SetControllerPortDevice sets the device type attached to a controller port
func (core *Core) SetControllerPortDevice(port uint, device uint32) {
C.bridge_retro_set_controller_port_device(core.symRetroSetControllerPortDevice, C.unsigned(port), C.unsigned(device))
coreCall(func() {
C.bridge_retro_set_controller_port_device(core.symRetroSetControllerPortDevice, C.unsigned(port), C.unsigned(device))
})
}
//export coreEnvironment
@@ -667,6 +801,12 @@ func coreEnvironment(cmd C.unsigned, data unsafe.Pointer) bool {
if environment == nil {
return false
}
// Only window operations need the frontend thread. Routine queries stay here.
if cmd == C.RETRO_ENVIRONMENT_SET_HW_RENDER || cmd == C.RETRO_ENVIRONMENT_SHUTDOWN {
var result bool
frontendCall(func() { result = environment(uint32(cmd), data) })
return result
}
return environment(uint32(cmd), data)
}
@@ -691,6 +831,12 @@ func coreInputState(port C.unsigned, device C.unsigned, index C.unsigned, id C.u
if inputState == nil {
return 0
}
// Mouse state is queried through GLFW rather than the polled input snapshot.
if device == C.RETRO_DEVICE_MOUSE {
var result C.int16_t
frontendCall(func() { result = C.int16_t(inputState(uint(port), uint32(device), uint(index), uint(id))) })
return result
}
return C.int16_t(inputState(uint(port), uint32(device), uint(index), uint(id)))
}
@@ -726,6 +872,16 @@ func coreGetTimeUsec() C.uint64_t {
return C.uint64_t(getTimeUsec())
}
//export coreGetProcAddress
func coreGetProcAddress(sym *C.char) C.uintptr_t {
return C.uintptr_t(getProcAddress(C.GoString(sym)))
}
// SetCurrentFramebufferValue updates the C-side framebuffer callback target
func SetCurrentFramebufferValue(fb uintptr) {
C.coreSetCurrentFramebuffer_cgo(C.uintptr_t(fb))
}
// SetData is a setter for the data of a GameInfo type
func (gi *GameInfo) SetData(bytes []byte) {
cstr := C.CString(string(bytes))
@@ -883,20 +1039,58 @@ 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)
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 +1098,15 @@ func (core *Core) SetAudioCallback(data unsafe.Pointer) {
// GetMemorySize returns the size of a region of the memory.
// See memory constants.
func (core *Core) GetMemorySize(id uint32) uint {
return uint(C.bridge_retro_get_memory_size(core.symRetroGetMemorySize, C.unsigned(id)))
return coreResult(func() uint { return uint(C.bridge_retro_get_memory_size(core.symRetroGetMemorySize, C.unsigned(id))) })
}
// GetMemoryData returns the size of a region of the memory.
// See memory constants.
func (core *Core) GetMemoryData(id uint32) unsafe.Pointer {
return C.bridge_retro_get_memory_data(core.symRetroGetMemoryData, C.unsigned(id))
return coreResult(func() unsafe.Pointer {
return C.bridge_retro_get_memory_data(core.symRetroGetMemoryData, C.unsigned(id))
})
}
// DiskControlCallback is an interface which frontend can use to eject and insert disk images
@@ -930,19 +1126,19 @@ func (core *Core) SetDiskControlCallback(data unsafe.Pointer) {
c := *(*C.struct_retro_disk_control_callback)(data)
dcc := &DiskControlCallback{}
dcc.SetEjectState = func(state bool) {
C.bridge_retro_set_eject_state(c.set_eject_state, C.bool(state))
coreCall(func() { C.bridge_retro_set_eject_state(c.set_eject_state, C.bool(state)) })
}
dcc.GetEjectState = func() bool {
return bool(C.bridge_retro_get_eject_state(c.get_eject_state))
return coreResult(func() bool { return bool(C.bridge_retro_get_eject_state(c.get_eject_state)) })
}
dcc.GetImageIndex = func() uint {
return uint(C.bridge_retro_get_image_index(c.get_image_index))
return coreResult(func() uint { return uint(C.bridge_retro_get_image_index(c.get_image_index)) })
}
dcc.SetImageIndex = func(index uint) {
C.bridge_retro_set_image_index(c.set_image_index, C.uint(index))
coreCall(func() { C.bridge_retro_set_image_index(c.set_image_index, C.uint(index)) })
}
dcc.GetNumImages = func() uint {
return uint(C.bridge_retro_get_num_images(c.get_num_images))
return coreResult(func() uint { return uint(C.bridge_retro_get_num_images(c.get_num_images)) })
}
core.DiskControlCallback = dcc
}
+1
View File
@@ -31,6 +31,7 @@ type Core struct {
AudioCallback *AudioCallback
FrameTimeCallback *FrameTimeCallback
DiskControlCallback *DiskControlCallback
HWRenderCallback *HWRenderCallback
MemoryMap []MemoryDescriptor
}
+2 -2
View File
@@ -28,8 +28,8 @@ func DlOpen(path string) (DlHandle, error) {
defer C.free(unsafe.Pointer(cpath))
h := C.dlopen(cpath, C.RTLD_LAZY|C.RTLD_GLOBAL)
cerr := C.dlerror()
if h == nil || cerr != nil {
if h == nil {
cerr := C.dlerror()
err := C.GoString(cerr)
return nil, errors.New(err)
}
+41 -16
View File
@@ -30,6 +30,22 @@ func init() {
var frame = 0
func fulfillPendingScreenshot(vid *video.Video) {
if state.PendingScreenshotName == "" {
return
}
name := state.PendingScreenshotName
done := state.PendingScreenshotDone
state.PendingScreenshotName = ""
state.PendingScreenshotDone = nil
err := vid.TakeScreenshotInFrontendFrame(name)
if done != nil {
done(err)
}
}
func runLoop(vid *video.Video, m *menu.Menu) {
var currTime time.Time
prevTime := time.Now()
@@ -39,11 +55,19 @@ func runLoop(vid *video.Video, m *menu.Menu) {
glfw.PollEvents()
m.ProcessHotkeys()
ntf.Process(dt)
vid.ResizeViewport()
m.UpdatePalette()
input.Poll()
if !state.MenuActive {
menuActive := state.MenuActive
if menuActive {
m.Update(dt)
}
if state.MenuContextResetNeeded {
m.ContextReset()
state.MenuContextResetNeeded = false
}
if !menuActive {
if state.CoreRunning {
vid.BeginCore()
state.Core.Run()
if state.Core.FrameTimeCallback != nil {
state.Core.FrameTimeCallback.Callback(state.Core.FrameTimeCallback.Reference)
@@ -51,18 +75,23 @@ 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 +157,14 @@ func main() {
input.Init(vid)
if len(state.CorePath) > 0 {
err := core.Load(state.CorePath)
if err == nil {
if len(gamePath) > 0 {
err := core.LoadGame(gamePath)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
m.WarpToQuickMenu()
}
}
} else {
if err := core.Load(state.CorePath); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else if len(gamePath) > 0 {
if err := core.LoadGame(gamePath); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
m.WarpToQuickMenu()
}
}
}
+1 -2
View File
@@ -196,8 +196,7 @@ func (m *Menu) ProcessHotkeys() {
// Toggle fullscreen if ActionFullscreenToggle is released
if input.Released[0][input.ActionFullscreenToggle] == 1 {
settings.Current.VideoFullscreen = !settings.Current.VideoFullscreen
m.Reconfigure(settings.Current.VideoFullscreen)
m.ContextReset()
m.SetFullscreen(settings.Current.VideoFullscreen)
err := settings.Save()
if err != nil {
ntf.DisplayAndLogf(ntf.Error, "Menu", "Error saving settings: %v", err)
+6 -4
View File
@@ -102,10 +102,12 @@ func (m *Menu) ContextReset() {
m.icons[filename] = video.NewImage(path)
}
currentScreenIndex := len(m.stack) - 1
curList := m.stack[currentScreenIndex].Entry()
for i := range curList.children {
curList.children[i].thumbnail = 0
// Every retained scene's texture names belong to the old context.
for _, scene := range m.stack {
list := scene.Entry()
for i := range list.children {
list.children[i].thumbnail = 0
}
}
}
+1
View File
@@ -11,6 +11,7 @@ var severityBgColor map[ntf.Severity]video.Color
// RenderNotifications draws the list of notification messages on the viewport
func (m *Menu) RenderNotifications() {
fbw, fbh := m.GetFramebufferSize()
m.ratio = float32(fbw) / 1920
m.Font.UpdateResolution(fbw, fbh)
var h float32 = 75
stack := h
+1 -1
View File
@@ -16,7 +16,7 @@ func buildCoreOptions() Scene {
var list sceneCoreOptions
list.label = "Core Options"
if core.Options == nil {
if core.Options == nil || len(core.Options.Vars) == 0 {
list.children = append(list.children, entry{
label: "No options",
icon: "subsetting",
+2 -2
View File
@@ -180,7 +180,7 @@ func gameExplorerCb(path string) {
// Shutdown the operating system
func cleanShutdown() {
core.UnloadGame()
core.Unload()
if err := exec.Command("/usr/sbin/shutdown", "-P", "now").Run(); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
@@ -188,7 +188,7 @@ func cleanShutdown() {
// Reboots the operating system
func cleanReboot() {
core.UnloadGame()
core.Unload()
if err := exec.Command("/usr/sbin/shutdown", "-r", "now").Run(); err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
+9 -6
View File
@@ -1,6 +1,7 @@
package menu
import (
"github.com/libretro/ludo/core"
ntf "github.com/libretro/ludo/notifications"
"github.com/libretro/ludo/state"
"github.com/libretro/ludo/utils"
@@ -27,7 +28,7 @@ func buildQuickMenu() Scene {
label: "Reset",
icon: "reset",
callbackOK: func() {
state.Core.Reset()
core.Reset()
state.MenuActive = false
state.FastForward = false
},
@@ -47,11 +48,13 @@ func buildQuickMenu() Scene {
icon: "screenshot",
callbackOK: func() {
name := utils.DatedName(state.GamePath)
err := menu.TakeScreenshot(name)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
ntf.DisplayAndLog(ntf.Success, "Menu", "Took a screenshot.")
state.PendingScreenshotName = name
state.PendingScreenshotDone = func(err error) {
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
ntf.DisplayAndLog(ntf.Success, "Menu", "Took a screenshot.")
}
}
},
})
+13 -11
View File
@@ -26,17 +26,19 @@ func buildSavestates() Scene {
icon: "savestate",
callbackOK: func() {
name := utils.DatedName(state.GamePath)
err := menu.TakeScreenshot(name)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
err = savestates.Save(name)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
menu.stack[len(menu.stack)-1] = buildSavestates()
menu.tweens.FastForward()
ntf.DisplayAndLog(ntf.Success, "Menu", "State saved.")
state.PendingScreenshotName = name
state.PendingScreenshotDone = func(err error) {
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
}
err = savestates.Save(name)
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
} else {
menu.stack[len(menu.stack)-1] = buildSavestates()
menu.tweens.FastForward()
ntf.DisplayAndLog(ntf.Success, "Menu", "State saved.")
}
}
},
})
+2 -4
View File
@@ -164,8 +164,7 @@ var incrCallbacks = map[string]callbackIncrement{
v := f.Value().(bool)
v = !v
f.Set(v)
menu.Reconfigure(settings.Current.VideoFullscreen)
menu.ContextReset()
menu.SetFullscreen(settings.Current.VideoFullscreen)
settings.Save()
},
"VideoMonitorIndex": func(f *structs.Field, direction int) {
@@ -178,8 +177,7 @@ var incrCallbacks = map[string]callbackIncrement{
v = len(glfw.GetMonitors()) - 1
}
f.Set(v)
menu.Reconfigure(settings.Current.VideoFullscreen)
menu.ContextReset()
menu.SetFullscreen(settings.Current.VideoFullscreen)
settings.Save()
},
"VideoFilter": func(f *structs.Field, direction int) {
+1 -1
View File
@@ -90,7 +90,7 @@ func (s *sceneUpdater) update(dt float32) {
s.children[0].icon = "reload"
s.children[0].callbackOK = func() {
cmd := exec.Command("/usr/sbin/shutdown", "-r", "now")
core.UnloadGame()
core.Unload()
err := cmd.Run()
if err != nil {
ntf.DisplayAndLog(ntf.Error, "Menu", err.Error())
+7 -3
View File
@@ -50,13 +50,17 @@ func New(vars []VariableInterface) (*Options, error) {
// Cache core options
for _, v := range vars {
v := v
choices := v.Choices()
// A core may expose an empty selector, e.g. when no BIOS files are found.
if len(choices) == 0 {
continue
}
o.Vars = append(o.Vars, &Variable{
Key: v.Key(),
Desc: v.Desc(),
Choices: v.Choices(),
Choices: choices,
Default: v.DefaultValue(),
Choice: utils.IndexOfString(v.DefaultValue(), v.Choices()),
Choice: utils.IndexOfString(v.DefaultValue(), choices),
})
}
o.Updated = true
+3 -6
View File
@@ -6,15 +6,14 @@ import (
"os"
"path/filepath"
"github.com/libretro/ludo/core"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
)
// Save the current state to the filesystem. name is the name of the
// savestate file to save to, without extension.
func Save(name string) error {
s := state.Core.SerializeSize()
bytes, err := state.Core.Serialize(s)
bytes, err := core.Serialize()
if err != nil {
return err
}
@@ -28,11 +27,9 @@ func Save(name string) error {
// Load the state from the filesystem
func Load(path string) error {
s := state.Core.SerializeSize()
bytes, err := os.ReadFile(path)
if err != nil {
return err
}
err = state.Core.Unserialize(bytes, s)
return err
return core.Unserialize(bytes)
}
+6
View File
@@ -43,13 +43,17 @@ func defaultSettings() Settings {
"Nintendo - Game Boy Advance": "mgba_libretro",
"Nintendo - Game Boy Color": "gambatte_libretro",
"Nintendo - Game Boy": "gambatte_libretro",
"Nintendo - GameCube": "dolphin_libretro",
"Nintendo - Nintendo 3DS": "citra_libretro",
"Nintendo - Nintendo 64": "mupen64plus_next_libretro",
"Nintendo - Nintendo Entertainment System": "fceumm_libretro",
"Nintendo - Nintendo DS": "melonds_libretro",
"Nintendo - Pokemon Mini": "pokemini_libretro",
"Nintendo - Super Nintendo Entertainment System": "snes9x_libretro",
"Nintendo - Virtual Boy": "mednafen_vb_libretro",
"Nintendo - Wii": "dolphin_libretro",
"Sega - 32X": "picodrive_libretro",
"Sega - Dreamcast": "flycast_libretro",
"Sega - Game Gear": "genesis_plus_gx_libretro",
"Sega - Master System - Mark III": "genesis_plus_gx_libretro",
"Sega - Mega Drive - Genesis": "genesis_plus_gx_libretro",
@@ -60,6 +64,8 @@ func defaultSettings() Settings {
"SNK - Neo Geo Pocket Color": "mednafen_ngp_libretro",
"SNK - Neo Geo Pocket": "mednafen_ngp_libretro",
"Sony - PlayStation": playstationCore,
"Sony - PlayStation 2": "pcsx2_libretro",
"Sony - PlayStation Portable": "ppsspp_libretro",
},
FileDirectory: usr.HomeDir,
CoresDirectory: "./cores",
+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
+190
View File
@@ -0,0 +1,190 @@
//go:build gltest && !darwin
package video
import (
"runtime"
"testing"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/go-gl/mathgl/mgl32"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/state"
)
// Run with go test -tags 'wayland gltest' ./video on a graphical session.
func TestHardwareContextIsolation(t *testing.T) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if err := glfw.Init(); err != nil {
t.Fatal(err)
}
defer glfw.Terminate()
glfw.DefaultWindowHints()
glfw.WindowHint(glfw.Visible, glfw.False)
glfw.WindowHint(glfw.ContextVersionMajor, 3)
glfw.WindowHint(glfw.ContextVersionMinor, 3)
glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
w, err := glfw.CreateWindow(128, 96, "Ludo GL test", nil, nil)
if err != nil {
t.Fatal(err)
}
defer w.Destroy()
w.MakeContextCurrent()
if err := initGL(); err != nil {
t.Fatal(err)
}
t.Log("GL:", gl.GoStr(gl.GetString(gl.VERSION)))
oldCore, oldRunning := state.Core, state.CoreRunning
defer func() { state.Core, state.CoreRunning = oldCore, oldRunning }()
hw := &libretro.HWRenderCallback{HWContextType: libretro.HWContextOpenGLCore, VersionMajor: 3, VersionMinor: 3}
state.Core = &libretro.Core{HWRenderCallback: hw}
state.CoreRunning = true
v := &Video{Window: w, identityMat: mgl32.Ident4(), Geom: libretro.GameGeometry{MaxWidth: 64, MaxHeight: 64}}
v.SetHWRenderContext(hw)
v.ensurePixelFormatDefaults()
v.resetGameTexture(64, 64)
hw.ContextReset = func() {
if glfw.GetCurrentContext() != v.HWWindow {
t.Fatal("context_reset ran outside the HW context")
}
}
if err := v.ResetHWContext(); err != nil {
t.Fatal(err)
}
defer v.DestroySharedHWContext()
if glfw.GetCurrentContext() != w || v.HWWindow == nil || v.HWWindow == w {
t.Fatal("reset did not return to an independent frontend context")
}
v.BeginCore()
var readFBO, texture, pbo uint32
gl.GenFramebuffers(1, &readFBO)
gl.BindFramebuffer(gl.READ_FRAMEBUFFER, readFBO)
gl.GenTextures(1, &texture)
gl.ActiveTexture(gl.TEXTURE1)
gl.BindTexture(gl.TEXTURE_2D, texture)
gl.GenBuffers(1, &pbo)
gl.BindBuffer(gl.PIXEL_PACK_BUFFER, pbo)
gl.PixelStorei(gl.PACK_ROW_LENGTH, 256)
gl.Enable(gl.BLEND)
gl.Enable(gl.SCISSOR_TEST)
gl.BlendEquation(gl.FUNC_REVERSE_SUBTRACT)
v.EndCore()
v.BeginFrontendFrame()
checkInt := func(name uint32, want int32) {
t.Helper()
var got int32
gl.GetIntegerv(name, &got)
if got != want {
t.Errorf("GL state %#x: got %d, want %d", name, got, want)
}
}
checkInt(gl.PIXEL_PACK_BUFFER_BINDING, 0)
checkInt(gl.PACK_ROW_LENGTH, 0)
checkInt(gl.READ_FRAMEBUFFER_BINDING, 0)
checkInt(gl.BLEND_EQUATION_RGB, gl.FUNC_ADD)
v.BeginCore()
checkInt(gl.PIXEL_PACK_BUFFER_BINDING, int32(pbo))
checkInt(gl.PACK_ROW_LENGTH, 256)
checkInt(gl.READ_FRAMEBUFFER_BINDING, int32(readFBO))
checkInt(gl.DRAW_FRAMEBUFFER_BINDING, int32(v.fboID))
checkInt(gl.BLEND_EQUATION_RGB, gl.FUNC_REVERSE_SUBTRACT)
checkInt(gl.ACTIVE_TEXTURE, gl.TEXTURE1)
checkInt(gl.TEXTURE_BINDING_2D, int32(texture))
if !gl.IsEnabled(gl.BLEND) || !gl.IsEnabled(gl.SCISSOR_TEST) {
t.Error("frontend rendering changed core enables")
}
gl.BindBuffer(gl.PIXEL_PACK_BUFFER, 0)
gl.PixelStorei(gl.PACK_ROW_LENGTH, 0)
gl.Disable(gl.SCISSOR_TEST)
gl.Disable(gl.BLEND)
gl.BindFramebuffer(gl.READ_FRAMEBUFFER, v.fboID)
// Verify the produced pixels are visible through the shared texture.
gl.ClearColor(1, 0, 0, 1)
gl.Clear(gl.COLOR_BUFFER_BIT)
v.EndCore()
var frontendFBO uint32
gl.GenFramebuffers(1, &frontendFBO)
gl.BindFramebuffer(gl.FRAMEBUFFER, frontendFBO)
gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, v.texID, 0)
var pixel [4]byte
gl.ReadPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(&pixel[0]))
if pixel != [4]byte{255, 0, 0, 255} {
t.Errorf("shared frame: got %v, want red", pixel)
}
v.BeginFrontendFrame()
// Backbuffer snapshots must retain their orientation for either core origin.
v.defaultProgram, err = newProgram(vertexShader, defaultFragmentShader)
if err != nil {
t.Fatal(err)
}
gl.GenVertexArrays(1, &v.vao)
gl.BindVertexArray(v.vao)
gl.GenBuffers(1, &v.vbo)
gl.BindBuffer(gl.ARRAY_BUFFER, v.vbo)
vert := uint32(gl.GetAttribLocation(v.defaultProgram, gl.Str("vert\x00")))
uv := uint32(gl.GetAttribLocation(v.defaultProgram, gl.Str("vertTexCoord\x00")))
gl.EnableVertexAttribArray(vert)
gl.VertexAttribPointerWithOffset(vert, 2, gl.FLOAT, false, 16, 0)
gl.EnableVertexAttribArray(uv)
gl.VertexAttribPointerWithOffset(uv, 2, gl.FLOAT, false, 16, 8)
v.width, v.height = 64, 64
for _, bottomLeft := range []bool{false, true} {
hw.BottomLeftOrigin = bottomLeft
v.BeginFrontendFrame()
fbw, fbh := w.GetFramebufferSize()
v.Geom.AspectRatio = float64(fbw) / float64(fbh)
v.presentW, v.presentH = int32(fbw), int32(fbh)
gl.ClearColor(1, 0, 0, 1)
gl.Clear(gl.COLOR_BUFFER_BIT)
gl.Enable(gl.SCISSOR_TEST)
gl.Scissor(0, int32(fbh/2), int32(fbw), int32(fbh/2))
gl.ClearColor(0, 0, 1, 1)
gl.Clear(gl.COLOR_BUFFER_BIT)
gl.Disable(gl.SCISSOR_TEST)
v.CachePresentedFrame()
if !v.RenderCachedFrame() {
t.Fatal("cached frame unavailable")
}
gl.ReadPixels(1, 1, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(&pixel[0]))
if pixel != [4]byte{255, 0, 0, 255} {
t.Errorf("cached frame, bottom-left=%v: got %v, want red", bottomLeft, pixel)
}
}
hw.ContextDestroy = func() {
if glfw.GetCurrentContext() != v.HWWindow {
t.Error("context_destroy ran outside the HW context")
}
}
v.RunContextDestroy()
v.ResetCoreState()
if !gl.IsFramebuffer(frontendFBO) {
t.Error("HW teardown deleted a frontend framebuffer with the same name")
}
if v.fboID != 0 || v.HWWindow != nil || v.cachedFrameValid || v.width != 0 {
t.Error("HW teardown left stale core resources")
}
// A subsequent core must get a fresh context, including on the GL 2.x sync fallback.
if err := v.ResetHWContext(); err != nil {
t.Fatal(err)
}
v.hwSync = false
v.BeginCore()
checkInt(gl.PACK_ROW_LENGTH, 0)
checkInt(gl.PIXEL_PACK_BUFFER_BINDING, 0)
checkInt(gl.BLEND_EQUATION_RGB, gl.FUNC_ADD)
if gl.IsEnabled(gl.BLEND) || gl.IsEnabled(gl.SCISSOR_TEST) {
t.Error("new core inherited the previous core's enables")
}
v.EndCore()
if e := gl.GetError(); e != gl.NO_ERROR {
t.Errorf("GL error: %#x", e)
}
}
+3 -6
View File
@@ -4,14 +4,9 @@ var vertexShader = `
#if __VERSION__ >= 130
#define COMPAT_VARYING out
#define COMPAT_ATTRIBUTE in
#define COMPAT_TEXTURE texture
#define COMPAT_FRAGCOLOR FragColor
out vec4 COMPAT_FRAGCOLOR;
#else
#define COMPAT_VARYING varying
#define COMPAT_ATTRIBUTE attribute
#define COMPAT_TEXTURE texture2D
#define COMPAT_FRAGCOLOR gl_FragColor
#endif
COMPAT_ATTRIBUTE vec2 vert;
@@ -19,8 +14,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"
+83
View File
@@ -0,0 +1,83 @@
package video
import (
"log"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/mathgl/mgl32"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/state"
)
// InitFramebuffer initializes and configures the video frame buffer based on
// informations from the HWRenderCallback of the libretro core.
func (video *Video) InitFramebuffer() {
width := int32(video.Geom.MaxWidth)
height := int32(video.Geom.MaxHeight)
video.texWidth = width
video.texHeight = height
log.Printf("[Video]: Initializing HW render (%v x %v).\n", width, height)
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, video.texID)
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, nil)
genFramebuffers(1, &video.fboID)
bindFramebuffer(gl.FRAMEBUFFER, video.fboID)
libretro.SetCurrentFramebufferValue(uintptr(video.fboID))
framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, video.texID, 0)
hw := state.Core.HWRenderCallback
if hw.Depth {
genRenderbuffers(1, &video.rboID)
bindRenderbuffer(gl.RENDERBUFFER, video.rboID)
format := gl.DEPTH_COMPONENT16
if hw.Stencil {
format = gl.DEPTH24_STENCIL8
}
renderbufferStorage(gl.RENDERBUFFER, uint32(format), width, height)
bindRenderbuffer(gl.RENDERBUFFER, 0)
// Separate attachment points work with both core and EXT framebuffers.
framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, video.rboID)
if hw.Stencil {
framebufferRenderbuffer(gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, gl.RENDERBUFFER, video.rboID)
}
}
// Default origin is top left
video.orthoMat = mgl32.Ortho2D(-1, 1, -1, 1)
if hw.BottomLeftOrigin {
video.orthoMat = mgl32.Ortho2D(-1, 1, 1, -1)
}
if st := checkFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE {
log.Fatalf("[Video] Framebuffer is not complete. Error: %v\n", st)
}
gl.ClearColor(0, 0, 0, 1)
if hw.Depth && hw.Stencil {
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT)
} else if hw.Depth {
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
} else {
gl.Clear(gl.COLOR_BUFFER_BIT)
}
}
func bindBackbuffer() {
bindFramebuffer(gl.FRAMEBUFFER, 0)
}
func (video *Video) destroyFramebuffer() {
bindBackbuffer()
if video.rboID != 0 {
deleteRenderbuffers(1, &video.rboID)
video.rboID = 0
}
if video.fboID != 0 {
deleteFramebuffers(1, &video.fboID)
video.fboID = 0
}
}
+6 -5
View File
@@ -124,11 +124,7 @@ func (video *Video) ScissorEnd() {
gl.Disable(gl.SCISSOR_TEST)
}
func (video *Video) vertexArray(x, y, w, h, scale float32) []float32 {
fbw, fbh := video.Window.GetFramebufferSize()
ffbw := float32(fbw)
ffbh := float32(fbh)
func vertexArrayForFramebuffer(x, y, w, h, scale, ffbw, ffbh float32) []float32 {
w *= scale
h *= scale
@@ -143,6 +139,11 @@ func (video *Video) vertexArray(x, y, w, h, scale float32) []float32 {
}
}
func (video *Video) vertexArray(x, y, w, h, scale float32) []float32 {
fbw, fbh := video.Window.GetFramebufferSize()
return vertexArrayForFramebuffer(x, y, w, h, scale, float32(fbw), float32(fbh))
}
// DrawBorder draws a colored rectangle border
func (video *Video) DrawBorder(x, y, w, h, borderWidth float32, c Color) {
va := video.vertexArray(x, y, w, h, 1.0)
+63
View File
@@ -0,0 +1,63 @@
package video
import (
"errors"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/glfw/v3.4/glfw"
)
var (
glslVersion = "#version 120\n"
genVertexArrays = gl.GenVertexArrays
bindVertexArray = gl.BindVertexArray
genFramebuffers = gl.GenFramebuffers
bindFramebuffer = gl.BindFramebuffer
deleteFramebuffers = gl.DeleteFramebuffers
framebufferTexture2D = gl.FramebufferTexture2D
checkFramebufferStatus = gl.CheckFramebufferStatus
genRenderbuffers = gl.GenRenderbuffers
bindRenderbuffer = gl.BindRenderbuffer
deleteRenderbuffers = gl.DeleteRenderbuffers
renderbufferStorage = gl.RenderbufferStorage
framebufferRenderbuffer = gl.FramebufferRenderbuffer
)
// The frontend and hidden core context use the same hints. Select their API
// from the actual context capabilities, including macOS's legacy extensions.
func initGL() error {
if err := gl.Init(); err != nil {
return err
}
window := glfw.GetCurrentContext()
major, minor := window.GetAttrib(glfw.ContextVersionMajor), window.GetAttrib(glfw.ContextVersionMinor)
glslVersion = "#version 120\n"
if major > 3 || (major == 3 && minor >= 2) {
glslVersion = "#version 150\n"
} else if major == 3 {
glslVersion = "#version 130\n"
}
genVertexArrays, bindVertexArray = gl.GenVertexArrays, gl.BindVertexArray
genFramebuffers, bindFramebuffer, deleteFramebuffers = gl.GenFramebuffers, gl.BindFramebuffer, gl.DeleteFramebuffers
framebufferTexture2D, checkFramebufferStatus = gl.FramebufferTexture2D, gl.CheckFramebufferStatus
genRenderbuffers, bindRenderbuffer, deleteRenderbuffers = gl.GenRenderbuffers, gl.BindRenderbuffer, gl.DeleteRenderbuffers
renderbufferStorage, framebufferRenderbuffer = gl.RenderbufferStorage, gl.FramebufferRenderbuffer
if major < 3 && !glfw.ExtensionSupported("GL_ARB_vertex_array_object") {
if !glfw.ExtensionSupported("GL_APPLE_vertex_array_object") {
return errors.New("OpenGL vertex array objects are not supported")
}
genVertexArrays, bindVertexArray = gl.GenVertexArraysAPPLE, gl.BindVertexArrayAPPLE
}
if major < 3 && !glfw.ExtensionSupported("GL_ARB_framebuffer_object") {
if !glfw.ExtensionSupported("GL_EXT_framebuffer_object") {
return errors.New("OpenGL framebuffer objects are not supported")
}
genFramebuffers, bindFramebuffer, deleteFramebuffers = gl.GenFramebuffersEXT, gl.BindFramebufferEXT, gl.DeleteFramebuffersEXT
framebufferTexture2D, checkFramebufferStatus = gl.FramebufferTexture2DEXT, gl.CheckFramebufferStatusEXT
genRenderbuffers, bindRenderbuffer, deleteRenderbuffers = gl.GenRenderbuffersEXT, gl.BindRenderbufferEXT, gl.DeleteRenderbuffersEXT
renderbufferStorage, framebufferRenderbuffer = gl.RenderbufferStorageEXT, gl.FramebufferRenderbufferEXT
}
return nil
}
+25
View File
@@ -0,0 +1,25 @@
package video
import (
"testing"
"github.com/libretro/ludo/libretro"
)
func TestDuplicateFramePreservesDimensions(t *testing.T) {
v := &Video{}
v.Refresh(libretro.HWFrameBufferValid, 320, 240, 0)
v.Refresh(nil, 0, 0, 0)
if v.width != 320 || v.height != 240 || v.data != libretro.HWFrameBufferValid || !v.needUpload {
t.Fatal("NULL refresh discarded the pending frame")
}
v.needUpload = false
v.Refresh(nil, 640, 480, 0)
if v.width != 320 || v.height != 240 || v.needUpload {
t.Fatal("NULL refresh changed the previously presented frame")
}
}
func TestContextDestroyWithoutCore(t *testing.T) {
(&Video{}).RunContextDestroy()
}
+31 -33
View File
@@ -1,6 +1,7 @@
package video
import (
"errors"
"image"
"image/png"
"os"
@@ -13,41 +14,33 @@ import (
"github.com/libretro/ludo/state"
)
// During the TakeScreenshot step, we need to render the current game frame at
// the right resolution to later capture it using ReadPixels. renderScreenshot
// taking care of this.
func (video *Video) renderScreenshot() {
va := video.vertexArray(0, 0, float32(video.Geom.BaseWidth), float32(video.Geom.BaseHeight), 1.0)
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW)
func (video *Video) captureBackbufferScreenshot(name string) error {
outW, outH := video.Geom.BaseWidth, video.Geom.BaseHeight
if outW < 1 || outH < 1 {
return errors.New("invalid screenshot output size")
}
bindVertexArray(video.vao)
x := video.presentX
y := video.presentY
w := video.presentW
h := video.presentH
if !state.CoreRunning || video.width < 1 || video.height < 1 || w < 1 || h < 1 {
return errors.New("no game frame to capture")
}
gl.BindTexture(gl.TEXTURE_2D, video.texID)
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
var oldReadBuffer int32
gl.GetIntegerv(gl.READ_BUFFER, &oldReadBuffer)
gl.ReadBuffer(gl.BACK)
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
}
img := image.NewRGBA(image.Rect(0, 0, int(w), int(h)))
gl.Finish()
gl.ReadPixels(x, y, w, h, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(img.Pix))
gl.ReadBuffer(uint32(oldReadBuffer))
// TakeScreenshot captures the ouput of video.Render and writes it to a file
func (video *Video) TakeScreenshot(name string) error {
state.MenuActive = false
defer func() { state.MenuActive = true }()
gl.UseProgram(video.defaultProgram)
video.renderScreenshot()
img := image.NewRGBA(image.Rect(0, 0, video.Geom.BaseWidth, video.Geom.BaseHeight))
_, fbh := video.Window.GetFramebufferSize()
gl.ReadPixels(
0, int32(fbh-video.Geom.BaseHeight),
int32(video.Geom.BaseWidth), int32(video.Geom.BaseHeight),
gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(img.Pix))
gl.UseProgram(video.program)
flipped := imaging.FlipV(img)
if flipped.Bounds().Dx() != outW || flipped.Bounds().Dy() != outH {
flipped = imaging.Resize(flipped, outW, outH, imaging.Linear)
}
err := os.MkdirAll(settings.Current.ScreenshotsDirectory, os.ModePerm)
if err != nil {
@@ -59,8 +52,13 @@ func (video *Video) TakeScreenshot(name string) error {
if err != nil {
return err
}
flipped := imaging.FlipV(img)
defer fd.Close()
return png.Encode(fd, flipped)
}
// TakeScreenshotInFrontendFrame captures a screenshot while the caller already
// owns a frontend frame and has already rendered the current game frame.
func (video *Video) TakeScreenshotInFrontendFrame(name string) error {
return video.captureBackbufferScreenshot(name)
}
+16 -1
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) {
@@ -22,6 +23,9 @@ func newProgram(vertexShaderSource, fragmentShaderSource string) (uint32, error)
gl.AttachShader(program, vertexShader)
gl.AttachShader(program, fragmentShader)
// Game and menu programs share a VAO, so their attribute locations must match.
gl.BindAttribLocation(program, 0, gl.Str("vert\x00"))
gl.BindAttribLocation(program, 1, gl.Str("vertTexCoord\x00"))
gl.LinkProgram(program)
var status int32
@@ -39,13 +43,24 @@ func newProgram(vertexShaderSource, fragmentShaderSource string) (uint32, error)
gl.DeleteShader(vertexShader)
gl.DeleteShader(fragmentShader)
// Only the core rendering code uses MVPs so we set it to identity
// to avoid issues with other parts of the program
gl.UseProgram(program)
mvp := gl.GetUniformLocation(program, gl.Str("MVP\x00"))
if mvp != -1 {
IdentityMatrix := mgl32.Ident4()
gl.UniformMatrix4fv(mvp, 1, false, &IdentityMatrix[0])
}
gl.UseProgram(0)
return program, nil
}
func compileShader(source string, shaderType uint32) (uint32, error) {
shader := gl.CreateShader(shaderType)
csources, free := gl.Strs(source)
csources, free := gl.Strs(glslVersion + source)
gl.ShaderSource(shader, 1, csources, nil)
free()
gl.CompileShader(shader)
+78
View File
@@ -0,0 +1,78 @@
//go:build gltest && linux
package video
import (
"bytes"
"runtime"
"testing"
"unsafe"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/libretro/ludo/libretro"
"golang.org/x/sys/unix"
)
func TestSoftwareFrameUploadBounds(t *testing.T) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if err := glfw.Init(); err != nil {
t.Fatal(err)
}
defer glfw.Terminate()
glfw.DefaultWindowHints()
glfw.WindowHint(glfw.Visible, glfw.False)
w, err := glfw.CreateWindow(32, 32, "Ludo upload test", nil, nil)
if err != nil {
t.Fatal(err)
}
defer w.Destroy()
w.MakeContextCurrent()
if err := initGL(); err != nil {
t.Fatal(err)
}
v := &Video{Geom: libretro.GameGeometry{MaxWidth: 64, MaxHeight: 64}}
v.SetPixelFormat(libretro.PixelFormatXRGB8888)
v.resetGameTexture(1, 1)
defer gl.DeleteTextures(1, &v.texID)
v.program, err = newProgram(vertexShader, defaultFragmentShader)
if err != nil {
t.Fatal(err)
}
defer gl.DeleteProgram(v.program)
const width, height, pitch = 32, 32, 32 * 4
page := unix.Getpagesize()
memory, err := unix.Mmap(-1, 0, page*2, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_ANON|unix.MAP_PRIVATE)
if err != nil {
t.Fatal(err)
}
defer unix.Munmap(memory)
if err := unix.Mprotect(memory[page:], unix.PROT_NONE); err != nil {
t.Fatal(err)
}
// The core supplies only the current frame, ending at a protected page.
frame := memory[page-pitch*height : page]
for i := range frame {
frame[i] = byte(i)
}
v.Refresh(unsafe.Pointer(&frame[0]), width, height, pitch)
v.uploadTexture()
var texWidth, texHeight int32
gl.GetTexLevelParameteriv(gl.TEXTURE_2D, 0, gl.TEXTURE_WIDTH, &texWidth)
gl.GetTexLevelParameteriv(gl.TEXTURE_2D, 0, gl.TEXTURE_HEIGHT, &texHeight)
if texWidth != width || texHeight != height {
t.Fatalf("texture size = %dx%d, want %dx%d", texWidth, texHeight, width, height)
}
pixels := make([]byte, len(frame))
gl.GetTexImage(gl.TEXTURE_2D, 0, v.pixType, v.pixFmt, gl.Ptr(pixels))
if !bytes.Equal(pixels, frame) {
t.Error("uploaded pixels differ from the supplied frame")
}
if err := gl.GetError(); err != gl.NO_ERROR {
t.Errorf("GL error: %#x", err)
}
}
+568 -61
View File
@@ -4,12 +4,14 @@
package video
import (
"errors"
"log"
"path/filepath"
"unsafe"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/glfw/v3.4/glfw"
"github.com/go-gl/mathgl/mgl32"
"github.com/libretro/ludo/libretro"
"github.com/libretro/ludo/settings"
"github.com/libretro/ludo/state"
@@ -17,11 +19,16 @@ 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
windowedX, windowedY int
windowedWidth, windowedHeight int
program uint32 // current program used for the game quad
defaultProgram uint32 // default program used for the game quad
@@ -34,6 +41,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,21 +65,190 @@ 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
}
// Reconfigure destroys and recreates the window with new attributes
func (video *Video) Reconfigure(fullscreen bool) {
// SetFullscreen changes the window mode or monitor without recreating GL contexts.
func (video *Video) SetFullscreen(fullscreen bool) {
if fullscreen {
if video.Window.GetMonitor() == nil {
video.windowedWidth, video.windowedHeight = video.Window.GetSize()
// Wayland does not expose window positions.
if glfw.GetPlatform() != glfw.PlatformWayland {
video.windowedX, video.windowedY = video.Window.GetPos()
}
}
monitor := glfw.GetMonitors()[settings.Current.VideoMonitorIndex]
modes := monitor.GetVideoModes()
mode := modes[len(modes)-1]
video.Window.SetMonitor(monitor, 0, 0, mode.Width, mode.Height, mode.RefreshRate)
} else if video.Window.GetMonitor() != nil {
video.Window.SetMonitor(nil, video.windowedX, video.windowedY,
video.windowedWidth, video.windowedHeight, glfw.DontCare)
}
}
// ReconfigureHWContext recreates the visible window using the currently stored
// HW context hints while preserving the current window size when windowed.
func (video *Video) ReconfigureHWContext(fullscreen bool) error {
width := 384 * 2
height := 240 * 2
if video.Window != nil && !fullscreen {
width, height = video.Window.GetSize()
if width < 1 {
width = 384 * 2
}
if height < 1 {
height = 240 * 2
}
}
return video.configureWithSize(width, height, fullscreen)
}
// CreateSharedHWContext creates a hidden context shared with the visible frontend context.
func (video *Video) CreateSharedHWContext() error {
if video.HWWindow != nil {
return nil
}
video.configureWindowHints()
glfw.WindowHint(glfw.Visible, glfw.False)
glfw.WindowHint(glfw.Focused, glfw.False)
width, height := video.Window.GetSize()
if width < 1 {
width = 1
}
if height < 1 {
height = 1
}
hwWindow, err := glfw.CreateWindow(width, height, "", nil, video.Window)
if err != nil {
return err
}
video.HWWindow = hwWindow
major := video.Window.GetAttrib(glfw.ContextVersionMajor)
minor := video.Window.GetAttrib(glfw.ContextVersionMinor)
video.hwSync = major > 3 || (major == 3 && minor >= 2) || glfw.ExtensionSupported("GL_ARB_sync")
return nil
}
// DestroySharedHWContext destroys the hidden shared context if it exists.
func (video *Video) DestroySharedHWContext() {
if video.HWWindow != nil {
video.BeginCore()
video.destroyFramebuffer()
libretro.SetCurrentFramebufferValue(0)
video.EndCore()
video.HWWindow.Destroy()
video.HWWindow = nil
}
}
// MakeFrontendContextCurrent switches back to the visible frontend context.
func (video *Video) MakeFrontendContextCurrent() {
if video.Window != nil {
video.Window.Destroy()
video.Window.MakeContextCurrent()
}
}
// RunContextDestroy notifies the active core before its GL context is destroyed.
func (video *Video) RunContextDestroy() {
if !state.CoreRunning || state.Core == nil || video.HWWindow == nil {
return
}
hw := state.Core.HWRenderCallback
if hw != nil && hw.ContextDestroy != nil {
video.BeginCore()
hw.ContextDestroy()
video.EndCore()
}
}
// ResetHWContext creates the core's render target and resets its renderer.
func (video *Video) ResetHWContext() error {
if err := video.CreateSharedHWContext(); err != nil {
return err
}
video.BeginCore()
defer video.EndCore()
video.InitFramebuffer()
state.Core.HWRenderCallback.ContextReset()
return nil
}
// BeginCore enters the isolated context for run, reset, serialization or teardown.
func (video *Video) BeginCore() {
if video.HWWindow != nil {
video.switchContext(video.HWWindow)
}
}
// EndCore returns to the frontend after the core's GL work.
func (video *Video) EndCore() {
if video.HWWindow != nil {
video.switchContext(video.Window)
}
}
// ContextCallbacks returns paired release/acquire callbacks for synchronous
// context handoffs between the frontend and native core threads.
func (video *Video) ContextCallbacks() (release, acquire func()) {
var current *glfw.Window
release = func() {
current = nil
if video.Window == nil || state.Core == nil || state.Core.HWRenderCallback == nil {
return
}
current = glfw.GetCurrentContext()
// Before context_reset, HW negotiation may still use the visible context.
// Once the hidden context exists, leave the menu context on its own thread.
if video.HWWindow != nil && current != video.HWWindow {
current = nil
}
if current != nil {
glfw.DetachCurrentContext()
}
}
acquire = func() {
if current != nil {
current.MakeContextCurrent()
}
}
return
}
func (video *Video) switchContext(window *glfw.Window) {
if glfw.GetCurrentContext() == window {
return
}
// Order shared-texture writes and reads in both directions without a CPU wait.
if video.hwSync {
fence := gl.FenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0)
gl.Flush()
window.MakeContextCurrent()
gl.WaitSync(fence, 0, gl.TIMEOUT_IGNORED)
gl.DeleteSync(fence)
} else {
gl.Finish()
window.MakeContextCurrent()
}
video.Configure(fullscreen)
}
// GetFramebufferSize retrieves the size, in pixels, of the framebuffer of the specified window.
@@ -71,12 +261,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,37 +339,85 @@ 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
var m *glfw.Monitor
if fullscreen {
m = glfw.GetMonitors()[settings.Current.VideoMonitorIndex]
vms := m.GetVideoModes()
vm := vms[len(vms)-1]
width = vm.Width
height = vm.Height
} else {
width = 384 * 2
height = 240 * 2
}
var err error
video.Window, err = glfw.CreateWindow(width, height, "Ludo", m, nil)
if err != nil {
if err := video.configureWithSize(384*2, 240*2, fullscreen); err != nil {
panic("Window creation failed:" + err.Error())
}
}
func (video *Video) configureWithSize(width, height int, fullscreen bool) error {
if fullscreen && video.windowedWidth != 0 {
width, height = video.windowedWidth, video.windowedHeight
}
video.configureWindowHints()
window, err := glfw.CreateWindow(width, height, video.title, nil, nil)
if err != nil {
return err
}
// GLFW may report a platform error without returning it through the Go binding.
if window == nil {
return errors.New("window creation failed")
}
// Keep the old context and its resources until the replacement exists.
if video.Window != nil {
video.RunContextDestroy()
video.DestroySharedHWContext()
video.Window.Destroy()
}
video.Window = window
// Object names from the old share group must not be reused in this context.
video.texID = 0
video.cachedFrameTexID = 0
video.cachedFrameW, video.cachedFrameH = 0, 0
video.cachedFrameValid = false
video.width, video.height = 0, 0
video.data = nil
video.needUpload = false
video.Window.MakeContextCurrent()
// Force a minimum size for the window.
video.Window.SetSizeLimits(160, 120, glfw.DontCare, glfw.DontCare)
video.SetFullscreen(fullscreen)
video.Window.SetInputMode(glfw.CursorMode, glfw.CursorHidden)
// Initialize Glow
if err := gl.Init(); err != nil {
if err := initGL(); err != nil {
panic(err)
}
@@ -152,8 +447,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,28 +470,33 @@ 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)
}
return nil
}
// UpdateFilter configures the game texture filter and shader. We currently
@@ -206,6 +507,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 +536,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 +586,38 @@ func (video *Video) ResetRot() {
video.rot = 0
}
// 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) {
// ResetCoreState clears per-core video state and destroys its GL context.
func (video *Video) ResetCoreState() {
video.DestroySharedHWContext()
video.MakeFrontendContextCurrent()
libretro.SetCurrentFramebufferValue(0)
video.ResetPitch()
video.ResetRot()
video.needUpload = false
video.data = nil
video.width = 0
video.height = 0
video.pixFmt = 0
video.pixType = 0
video.bpp = 0
video.ensurePixelFormatDefaults()
if video.texID != 0 {
video.resetGameTexture(1, 1)
video.UpdateFilter(settings.Current.VideoFilter)
}
video.texWidth = 0
video.texHeight = 0
if video.cachedFrameTexID != 0 {
gl.DeleteTextures(1, &video.cachedFrameTexID)
video.cachedFrameTexID = 0
}
video.cachedFrameW = 0
video.cachedFrameH = 0
video.cachedFrameValid = false
}
// gameViewport fits the game to the framebuffer and records its screenshot bounds.
func (video *Video) gameViewport(fbWidth, fbHeight int) (x, y, w, h float32) {
// Scale the content to fit in the viewport.
fbw := float32(fbWidth)
fbh := float32(fbHeight)
@@ -303,11 +635,36 @@ func (video *Video) coreRatioViewport(fbWidth int, fbHeight int) (x, y, w, h flo
w = fbw
}
// Place the content in the middle of the window.
x = (fbw - w) / 2
y = (fbh - h) / 2
// Keep centered, pixel-aligned bounds so the cached copy needs no resampling.
x = float32(int32((fbw - w) / 2))
y = float32(int32((fbh - h) / 2))
w = fbw - 2*x
h = fbh - 2*y
video.presentX = int32(x)
video.presentY = int32(y)
video.presentW = int32(w)
video.presentH = int32(h)
return
}
// coreRatioViewport configures the game quad's placement and core texture coordinates.
func (video *Video) coreRatioViewport(fbWidth, fbHeight, clipWidth, clipHeight int) (x, y, w, h float32) {
x, y, w, h = video.gameViewport(fbWidth, fbHeight)
va := vertexArrayForFramebuffer(x, y, w, h, 1.0, float32(fbWidth), float32(fbHeight))
if clipWidth == 0 {
clipWidth = int(video.texWidth)
}
if clipHeight == 0 {
clipHeight = int(video.texHeight)
}
va[3] = float32(clipHeight) / float32(video.texHeight)
va[10] = float32(clipWidth) / float32(video.texWidth)
va[11] = va[3]
va[14] = va[10]
va := video.vertexArray(x, y, w, h, 1.0)
va = rotateUV(va, video.rot)
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW)
@@ -315,6 +672,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 +710,156 @@ 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)
_, _, w, h := video.coreRatioViewport(fbw, fbh, int(video.width), int(video.height))
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)
// Preserve opaque alpha even when the core leaves its alpha channel unset.
gl.ColorMask(true, true, true, false)
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
gl.ColorMask(true, true, true, true)
bindVertexArray(0)
// Reset MVP to identity to avoid menu issues
gl.UniformMatrix4fv(gl.GetUniformLocation(video.program, gl.Str("MVP\x00")), 1, false, &video.identityMat[0])
gl.UseProgram(0)
}
func (video *Video) ensureCachedFrameTexture(width, height int32) {
if width < 1 || height < 1 {
return
}
if video.cachedFrameTexID == 0 {
gl.GenTextures(1, &video.cachedFrameTexID)
}
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, video.cachedFrameTexID)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
if video.cachedFrameW != width || video.cachedFrameH != height {
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, nil)
video.cachedFrameW = width
video.cachedFrameH = height
}
}
// CachePresentedFrame copies the game rectangle, without borders or UI, so menu-mode
// rendering does not depend on the core keeping its paused framebuffer alive.
func (video *Video) CachePresentedFrame() {
if !state.CoreRunning || video.width == 0 || video.height == 0 {
video.cachedFrameValid = false
return
}
if video.presentW < 1 || video.presentH < 1 {
video.cachedFrameValid = false
return
}
video.ensureCachedFrameTexture(video.presentW, video.presentH)
if video.cachedFrameTexID == 0 {
video.cachedFrameValid = false
return
}
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, video.cachedFrameTexID)
gl.CopyTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, video.presentX, video.presentY, video.presentW, video.presentH)
video.cachedFrameValid = true
}
// RenderCachedFrame draws the last cached frontend-presented game frame.
func (video *Video) RenderCachedFrame() bool {
if !video.cachedFrameValid || video.cachedFrameTexID == 0 || video.cachedFrameW < 1 || video.cachedFrameH < 1 {
return false
}
gl.ClearColor(0, 0, 0, 1)
gl.Clear(gl.COLOR_BUFFER_BIT)
fbw, fbh := video.Window.GetFramebufferSize()
x, y, w, h := video.gameViewport(fbw, fbh)
va := vertexArrayForFramebuffer(x, y, w, h, 1.0, float32(fbw), float32(fbh))
// A backbuffer copy always has bottom-left origin, regardless of the core.
va[3] = 0
va[7] = 1
va[11] = 0
va[15] = 1
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW)
gl.UseProgram(video.defaultProgram)
gl.Uniform4f(gl.GetUniformLocation(video.defaultProgram, gl.Str("color\x00")), 1, 1, 1, 1)
gl.Uniform2f(gl.GetUniformLocation(video.defaultProgram, gl.Str("OutputSize\x00")), w, h)
gl.Uniform2f(gl.GetUniformLocation(video.defaultProgram, gl.Str("TextureSize\x00")), float32(video.cachedFrameW), float32(video.cachedFrameH))
gl.Uniform2f(gl.GetUniformLocation(video.defaultProgram, gl.Str("InputSize\x00")), float32(video.cachedFrameW), float32(video.cachedFrameH))
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, video.cachedFrameTexID)
bindVertexArray(video.vao)
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
bindVertexArray(0)
gl.UseProgram(0)
return true
}
// RenderPresentedFrame redraws the current game image for menu-mode frontend
// rendering, preferring the cached last-good frame when available.
func (video *Video) RenderPresentedFrame() {
if !video.RenderCachedFrame() {
video.Render()
video.CachePresentedFrame()
}
}
// Refresh the texture framebuffer
func (video *Video) Refresh(data unsafe.Pointer, width int32, height int32, pitch int32) {
// NULL duplicates the last frame; its dimensions need not be supplied.
if data == nil {
return
}
video.needUpload = true
video.width = width
video.height = height
video.pitch = pitch
video.data = data // maybe need a full copy
video.data = data
}
func (video *Video) uploadTexture() {
@@ -378,13 +869,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
-15
View File
@@ -1,15 +0,0 @@
// +build darwin
package video
import (
"github.com/go-gl/gl/v2.1/gl"
)
func genVertexArrays(n int32, arrays *uint32) {
gl.GenVertexArraysAPPLE(n, arrays)
}
func bindVertexArray(array uint32) {
gl.BindVertexArrayAPPLE(array)
}
-15
View File
@@ -1,15 +0,0 @@
// +build !darwin
package video
import (
"github.com/go-gl/gl/v2.1/gl"
)
func genVertexArrays(n int32, arrays *uint32) {
gl.GenVertexArrays(n, arrays)
}
func bindVertexArray(array uint32) {
gl.BindVertexArray(array)
}