mirror of
https://github.com/libretro/ludo.git
synced 2026-11-07 04:05:58 -10:00
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:
1 parent
13347e0051
commit
c47ad16a70
37 files changed
+1543
-402
No files matched your search
@@ -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)))
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
@@ -31,6 +31,7 @@ type Core struct {
|
||||
AudioCallback *AudioCallback
|
||||
FrameTimeCallback *FrameTimeCallback
|
||||
DiskControlCallback *DiskControlCallback
|
||||
HWRenderCallback *HWRenderCallback
|
||||
|
||||
MemoryMap []MemoryDescriptor
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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.")
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user