diff --git a/Makefile b/Makefile index b815b138..46d0cee0 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ ARCH ?= x86_64 VERSION ?= dev BUNDLENAME = $(APP)-$(OS)-$(ARCH)-$(VERSION) -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 np2kai o2em pcsx_rearmed picodrive pokemini prosystem snes9x stella2014 vecx virtualjaguar +CORES = ppsspp dolphin atari800 bluemsx swanstation fbneo fceumm gambatte gearsystem genesis_plus_gx handy lutro mednafen_ngp mednafen_pce mednafen_pce_fast mednafen_pcfx mednafen_psx mednafen_saturn mednafen_supergrafx mednafen_vb mednafen_wswan mgba melonds np2kai o2em pcsx_rearmed picodrive pokemini prosystem snes9x stella2014 vecx virtualjaguar ifeq ($(ARCH), arm) CORES := $(filter-out swanstation,$(CORES)) diff --git a/core/core.go b/core/core.go index e3aea607..855250c6 100644 --- a/core/core.go +++ b/core/core.go @@ -201,6 +201,11 @@ func LoadGame(gamePath string) error { log.Println("[Core]: Game loaded: " + gamePath) savefiles.LoadSRAM() + if state.Core.HWRenderCallback != nil { + vid.InitFramebuffer() + state.Core.HWRenderCallback.ContextReset() + } + return nil } diff --git a/core/environment.go b/core/environment.go index 7c7a40dc..72f38cc3 100644 --- a/core/environment.go +++ b/core/environment.go @@ -143,6 +143,13 @@ func environment(cmd uint32, data unsafe.Pointer) bool { state.Core.SetFrameTimeCallback(data) case libretro.EnvironmentSetAudioCallback: state.Core.SetAudioCallback(data) + case libretro.EnvironmentSetHWRender: + state.Core.HWRenderCallback = libretro.SetHWRenderCallback( + data, + vid.CurrentFramebuffer, + vid.ProcAddress) + log.Println("[Env]: HWContextType:", state.Core.HWRenderCallback.HWContextType) + return true case libretro.EnvironmentGetCanDupe: libretro.SetBool(data, true) case libretro.EnvironmentSetPixelFormat: @@ -151,6 +158,9 @@ func environment(cmd uint32, data unsafe.Pointer) bool { return environmentGetSystemDirectory(data) case libretro.EnvironmentGetSaveDirectory: return environmentGetSaveDirectory(data) + case libretro.EnvironmentGetPrefferedHWRender: + log.Println("[Env]: EnvironmentGetPrefferedHWRenderer: 1") + libretro.SetUint(data, uint(libretro.HWContextOpenGL)) case libretro.EnvironmentShutdown: vid.SetShouldClose(true) case libretro.EnvironmentGetCoreOptionsVersion: diff --git a/go.mod b/go.mod index 8e2c558d..1bdd82a9 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/fatih/structs v1.1.0 github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec + github.com/go-gl/mathgl v1.0.0 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 github.com/golang/snappy v0.0.4 // indirect github.com/klauspost/compress v1.13.6 // indirect diff --git a/go.sum b/go.sum index a7824bdf..3a86adaf 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,8 @@ github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 h1:zDw5v7qm4yH7N8C8uWd+8I github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec h1:3FLiRYO6PlQFDpUU7OEFlWgjGD1jnBIVSJ5SYRWk+9c= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec/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/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -83,6 +85,7 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= 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.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ= diff --git a/libretro/cfuncs.go b/libretro/cfuncs.go index 4364dc6e..7f48330d 100644 --- a/libretro/cfuncs.go +++ b/libretro/cfuncs.go @@ -22,6 +22,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(); } @@ -168,5 +176,15 @@ int64_t coreGetTimeUsec_cgo() { return coreGetTimeUsec(); } +uintptr_t coreGetCurrentFramebuffer_cgo() { + uintptr_t coreGetCurrentFramebuffer(); + return coreGetCurrentFramebuffer(); +} + +uintptr_t coreGetProcAddress_cgo(const char *sym) { + uintptr_t coreGetProcAddress(const char *sym); + return coreGetProcAddress(sym); +} + */ import "C" diff --git a/libretro/libretro.go b/libretro/libretro.go index 33708efd..93ab8851 100644 --- a/libretro/libretro.go +++ b/libretro/libretro.go @@ -33,6 +33,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); @@ -51,6 +53,8 @@ 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(); +uintptr_t coreGetProcAddress_cgo(const char *sym); */ import "C" import ( @@ -216,6 +220,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() @@ -409,26 +427,48 @@ const ( MemoryVideoRAM = uint32(C.RETRO_MEMORY_VIDEO_RAM) ) +// Hardware contexts +const ( + HWContextNone = uint32(C.RETRO_HW_CONTEXT_NONE) + HWContextOpenGL = uint32(C.RETRO_HW_CONTEXT_OPENGL) + HWContextOpenGLES2 = uint32(C.RETRO_HW_CONTEXT_OPENGLES2) + HWContextOpenGLCore = uint32(C.RETRO_HW_CONTEXT_OPENGL_CORE) + HWContextOpenGLES3 = uint32(C.RETRO_HW_CONTEXT_OPENGLES3) + HWContextOpenGLESVersion = uint32(C.RETRO_HW_CONTEXT_OPENGLES_VERSION) + HWContextVulkan = uint32(C.RETRO_HW_CONTEXT_VULKAN) + HWContextDummy = uint32(C.RETRO_HW_CONTEXT_DUMMY) +) + +// Pass this to retro_video_refresh_t if rendering to hardware. +// Passing NULL to retro_video_refresh_t is still a frame dupe as normal. +var ( + HWFrameBufferValid = unsafe.Pointer(C.RETRO_HW_FRAME_BUFFER_VALID) +) + type ( - environmentFunc func(uint32, unsafe.Pointer) bool - videoRefreshFunc func(unsafe.Pointer, int32, int32, int32) - audioSampleFunc func(int16, int16) - audioSampleBatchFunc func([]byte, int32) int32 - inputPollFunc func() - inputStateFunc func(uint, uint32, uint, uint) int16 - logFunc func(uint32, string) - getTimeUsecFunc func() int64 + environmentFunc func(uint32, unsafe.Pointer) bool + videoRefreshFunc func(unsafe.Pointer, int32, int32, int32) + audioSampleFunc func(int16, int16) + audioSampleBatchFunc func([]byte, int32) int32 + inputPollFunc func() + inputStateFunc func(uint, uint32, uint, uint) int16 + logFunc func(uint32, string) + getTimeUsecFunc func() int64 + getCurrentFramebufferFunc func() uintptr + getProcAddressFunc func(string) uintptr ) var ( - environment environmentFunc - videoRefresh videoRefreshFunc - audioSample audioSampleFunc - audioSampleBatch audioSampleBatchFunc - inputPoll inputPollFunc - inputState inputStateFunc - log logFunc - getTimeUsec getTimeUsecFunc + environment environmentFunc + videoRefresh videoRefreshFunc + audioSample audioSampleFunc + audioSampleBatch audioSampleBatchFunc + inputPoll inputPollFunc + inputState inputStateFunc + log logFunc + getTimeUsec getTimeUsecFunc + getCurrentFramebuffer getCurrentFramebufferFunc + getProcAddress getProcAddressFunc ) // Load dynamically loads a libretro core at the given path and returns a Core instance @@ -490,6 +530,8 @@ func (core *Core) Deinit() { inputState = nil log = nil getTimeUsec = nil + getCurrentFramebuffer = nil + getProcAddress = nil } // Run runs the game for one video frame. @@ -721,6 +763,16 @@ func coreGetTimeUsec() C.uint64_t { return C.uint64_t(getTimeUsec()) } +//export coreGetCurrentFramebuffer +func coreGetCurrentFramebuffer() C.uintptr_t { + return C.uintptr_t(getCurrentFramebuffer()) +} + +//export coreGetProcAddress +func coreGetProcAddress(sym *C.char) C.uintptr_t { + return C.uintptr_t(getProcAddress(C.GoString(sym))) +} + // SetData is a setter for the data of a GameInfo type func (gi *GameInfo) SetData(bytes []byte) { cstr := C.CString(string(bytes)) @@ -883,6 +935,38 @@ func (core *Core) SetFrameTimeCallback(data unsafe.Pointer) { core.FrameTimeCallback = ftc } +// SetHWRenderCallback is an environment callback helper to set the HWRenderCallback +func SetHWRenderCallback( + data unsafe.Pointer, + currentFramebuffer getCurrentFramebufferFunc, + procAddress getProcAddressFunc, +) *HWRenderCallback { + c := (*C.struct_retro_hw_render_callback)(data) + hwrc := HWRenderCallback{} + hwrc.HWContextType = uint32(c.context_type) + + getCurrentFramebuffer = currentFramebuffer + getProcAddress = procAddress + c.get_current_framebuffer = (C.retro_hw_get_current_framebuffer_t)(C.coreGetCurrentFramebuffer_cgo) + c.get_proc_address = (C.retro_hw_get_proc_address_t)(C.coreGetProcAddress_cgo) + + hwrc.ContextReset = func() { + C.bridge_retro_hw_context_reset((C.retro_hw_context_reset_t)(c.context_reset)) + } + + 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) + hwrc.ContextDestroy = func() { + C.bridge_retro_hw_context_destroy((C.retro_hw_context_reset_t)(c.context_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) diff --git a/libretro/libretro_core.go b/libretro/libretro_core.go index cdc38392..43c13edf 100644 --- a/libretro/libretro_core.go +++ b/libretro/libretro_core.go @@ -31,6 +31,7 @@ type Core struct { AudioCallback *AudioCallback FrameTimeCallback *FrameTimeCallback DiskControlCallback *DiskControlCallback + HWRenderCallback *HWRenderCallback MemoryMap []MemoryDescriptor } diff --git a/video/default_vert_shader.go b/video/default_vert_shader.go index 7ba28e70..2c7e881e 100644 --- a/video/default_vert_shader.go +++ b/video/default_vert_shader.go @@ -19,8 +19,10 @@ COMPAT_ATTRIBUTE vec2 vertTexCoord; COMPAT_VARYING vec2 fragTexCoord; +uniform mat4 MVP; + void main() { fragTexCoord = vertTexCoord; - gl_Position = vec4(vert, 0.0, 1.0); + gl_Position = vec4(vert, 0.0, 1.0) * MVP; } ` + "\x00" diff --git a/video/font.go b/video/font.go index 2a139782..de63f3ba 100644 --- a/video/font.go +++ b/video/font.go @@ -174,22 +174,14 @@ func LoadTrueTypeFont(program uint32, r io.Reader, scale int32, low, high rune, f.fontChar = append(f.fontChar, char) } - // Generate texture - gl.GenTextures(1, &f.textureID) - gl.BindTexture(gl.TEXTURE_2D, f.textureID) - gl.PixelStorei(gl.UNPACK_ALIGNMENT, 1) - gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR) - gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) - - gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, int32(rgba.Rect.Dx()), int32(rgba.Rect.Dy()), 0, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(rgba.Pix)) - - gl.GenerateMipmap(gl.TEXTURE_2D) - gl.BindTexture(gl.TEXTURE_2D, 0) + textureUniform := gl.GetUniformLocation(f.program, gl.Str("Texture\x00")) + gl.Uniform1i(textureUniform, 0) // Configure VAO/VBO for texture quads genVertexArrays(1, &f.vao) - gl.GenBuffers(1, &f.vbo) bindVertexArray(f.vao) + + gl.GenBuffers(1, &f.vbo) gl.BindBuffer(gl.ARRAY_BUFFER, f.vbo) vertAttrib := uint32(gl.GetAttribLocation(f.program, gl.Str("vert\x00"))) @@ -200,6 +192,16 @@ func LoadTrueTypeFont(program uint32, r io.Reader, scale int32, low, high rune, gl.EnableVertexAttribArray(texCoordAttrib) gl.VertexAttribPointerWithOffset(texCoordAttrib, 2, gl.FLOAT, false, 4*4, 2*4) + // Generate texture + gl.ActiveTexture(gl.TEXTURE0) + gl.GenTextures(1, &f.textureID) + gl.BindTexture(gl.TEXTURE_2D, f.textureID) + gl.PixelStorei(gl.UNPACK_ALIGNMENT, 1) + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR) + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) + gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, int32(rgba.Rect.Dx()), int32(rgba.Rect.Dy()), 0, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(rgba.Pix)) + gl.GenerateMipmap(gl.TEXTURE_2D) + gl.BindBuffer(gl.ARRAY_BUFFER, 0) bindVertexArray(0) @@ -224,8 +226,7 @@ func LoadFont(file string, scale int32, windowWidth int, windowHeight int) (*Fon gl.UseProgram(program) // Set screen resolution - resUniform := gl.GetUniformLocation(program, gl.Str("resolution\x00")) - gl.Uniform2f(resUniform, float32(windowWidth), float32(windowHeight)) + gl.Uniform2f(gl.GetUniformLocation(program, gl.Str("resolution\x00")), float32(windowWidth), float32(windowHeight)) return LoadTrueTypeFont(program, fd, scale, 32, 256, LeftToRight) } @@ -253,15 +254,6 @@ func (f *Font) Printf(x, y float32, scale float32, fs string, argv ...interface{ lowChar := rune(32) - // Setup blending mode - gl.Enable(gl.BLEND) - gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) - - // Activate corresponding render state - gl.UseProgram(f.program) - // Set text color - gl.Uniform4f(gl.GetUniformLocation(f.program, gl.Str("textColor\x00")), f.color.R, f.color.G, f.color.B, f.color.A) - var coords []point // Iterate through all characters in string @@ -302,6 +294,10 @@ func (f *Font) Printf(x, y float32, scale float32, fs string, argv ...interface{ x += float32((ch.advance >> 6)) * scale // Bitshift by 6 to get value in pixels (2^6 = 64 (divide amount of 1/64th pixels by 64 to get amount of pixels)) } + gl.UseProgram(f.program) + gl.Uniform4f(gl.GetUniformLocation(f.program, gl.Str("color\x00")), f.color.R, f.color.G, f.color.B, f.color.A) + gl.Enable(gl.BLEND) + gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) bindVertexArray(f.vao) gl.ActiveTexture(gl.TEXTURE0) gl.BindTexture(gl.TEXTURE_2D, f.textureID) diff --git a/video/font_frag_shader.go b/video/font_frag_shader.go index fedf344b..30f9f4ea 100644 --- a/video/font_frag_shader.go +++ b/video/font_frag_shader.go @@ -6,7 +6,7 @@ var fontFragmentShader = ` #define COMPAT_ATTRIBUTE in #define COMPAT_TEXTURE texture #define COMPAT_FRAGCOLOR FragColor -out vec4 FragColor; +out vec4 COMPAT_FRAGCOLOR; #else #define COMPAT_VARYING varying #define COMPAT_ATTRIBUTE attribute @@ -14,12 +14,13 @@ out vec4 FragColor; #define COMPAT_FRAGCOLOR gl_FragColor #endif -uniform sampler2D tex; -uniform vec4 textColor; +uniform sampler2D Texture; +uniform vec4 color; + COMPAT_VARYING vec2 fragTexCoord; void main() { - vec4 sampled = vec4(1.0, 1.0, 1.0, COMPAT_TEXTURE(tex, fragTexCoord).r); - COMPAT_FRAGCOLOR = min(textColor, vec4(1.0, 1.0, 1.0, 1.0)) * sampled; + vec4 sampled = vec4(1.0, 1.0, 1.0, COMPAT_TEXTURE(Texture, fragTexCoord).r); + COMPAT_FRAGCOLOR = min(color, vec4(1.0, 1.0, 1.0, 1.0)) * sampled; } ` + "\x00" diff --git a/video/shader_utils.go b/video/shader_utils.go index fb5e86ae..7faffb50 100644 --- a/video/shader_utils.go +++ b/video/shader_utils.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/go-gl/gl/v2.1/gl" + "github.com/go-gl/mathgl/mgl32" ) func newProgram(vertexShaderSource, fragmentShaderSource string) (uint32, error) { @@ -39,6 +40,17 @@ func newProgram(vertexShaderSource, fragmentShaderSource string) (uint32, error) gl.DeleteShader(vertexShader) gl.DeleteShader(fragmentShader) + // Only the core rendering code uses MVPs so we set it to identity + // to avoid issues with other parts of the program + gl.UseProgram(program) + mvp := gl.GetUniformLocation(program, gl.Str("MVP\x00")) + + if mvp != -1 { + IdentityMatrix := mgl32.Ident4() + gl.UniformMatrix4fv(mvp, 1, false, &IdentityMatrix[0]) + } + gl.UseProgram(0) + return program, nil } diff --git a/video/video.go b/video/video.go index 532325a3..b0b22e65 100644 --- a/video/video.go +++ b/video/video.go @@ -10,6 +10,7 @@ import ( "github.com/go-gl/gl/v2.1/gl" "github.com/go-gl/glfw/v3.3/glfw" + "github.com/go-gl/mathgl/mgl32" "github.com/libretro/ludo/libretro" "github.com/libretro/ludo/settings" "github.com/libretro/ludo/state" @@ -33,6 +34,10 @@ type Video struct { vao uint32 vbo uint32 texID uint32 + fboID uint32 + rboID uint32 + identityMat mgl32.Mat4 // just a cache + orthoMat mgl32.Mat4 pitch int32 // pitch set by the refresh callback pixFmt uint32 // format set by the environment callback @@ -45,6 +50,7 @@ type Video struct { // Init instanciates the video package func Init(fullscreen bool) *Video { vid := &Video{} + vid.identityMat = mgl32.Ident4() vid.Configure(fullscreen) return vid } @@ -52,6 +58,14 @@ func Init(fullscreen bool) *Video { // Reconfigure destroys and recreates the window with new attributes func (video *Video) Reconfigure(fullscreen bool) { if video.Window != nil { + // This is the expected frontend behavior and Flycast requires this + // for fullscreen toggling to work, but ppsspp breaks. OTOH, ppsspp + // breaks in those situations even if we don't call context_destroy + // so ignore it. + hw := state.Core.HWRenderCallback + if state.CoreRunning && hw != nil && hw.ContextDestroy != nil { + state.Core.HWRenderCallback.ContextDestroy() + } video.Window.Destroy() } video.Configure(fullscreen) @@ -167,6 +181,7 @@ func (video *Video) Configure(fullscreen bool) { video.UpdateFilter(settings.Current.VideoFilter) + gl.UseProgram(video.program) textureUniform := gl.GetUniformLocation(video.program, gl.Str("Texture\x00")) gl.Uniform1i(textureUniform, 0) @@ -193,20 +208,27 @@ func (video *Video) Configure(fullscreen bool) { video.bpp = 2 } - gl.GenTextures(1, &video.texID) + if video.Geom.MaxWidth == 0 || video.Geom.MaxHeight == 0 { + video.Geom.MaxWidth = video.Geom.BaseWidth + video.Geom.MaxHeight = video.Geom.BaseHeight + } - gl.ActiveTexture(gl.TEXTURE0) + gl.GenTextures(1, &video.texID) if video.texID == 0 && state.Verbose { - log.Println("[Video]: Failed to create the vid texture") + log.Fatalln("[Video]: Failed to create the vid texture") } + gl.ActiveTexture(gl.TEXTURE0) gl.BindTexture(gl.TEXTURE_2D, video.texID) + gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, int32(video.Geom.MaxWidth), int32(video.Geom.MaxHeight), 0, video.pixType, video.pixFmt, nil) 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) } } @@ -219,6 +241,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": @@ -247,8 +270,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.Geom.MaxWidth), float32(video.Geom.MaxHeight)) 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. @@ -298,7 +322,7 @@ func (video *Video) ResetRot() { // coreRatioViewport configures the vertex array to display the game at the center of the window // while preserving the original ascpect ratio of the game or core -func (video *Video) coreRatioViewport(fbWidth int, fbHeight int) (x, y, w, h float32) { +func (video *Video) coreRatioViewport(fbWidth, fbHeight, clipWidth, clipHeight int) (x, y, w, h float32) { // Scale the content to fit in the viewport. fbw := float32(fbWidth) fbh := float32(fbHeight) @@ -321,6 +345,12 @@ func (video *Video) coreRatioViewport(fbWidth int, fbHeight int) (x, y, w, h flo y = (fbh - h) / 2 va := video.vertexArray(x, y, w, h, 1.0) + + va[3] = float32(clipHeight) / float32(video.Geom.MaxHeight) + va[10] = float32(clipWidth) / float32(video.Geom.MaxWidth) + va[11] = va[3] + va[14] = va[10] + va = rotateUV(va, video.rot) gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo) gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW) @@ -336,36 +366,64 @@ func (video *Video) ResizeViewport() { // Render the current frame func (video *Video) Render() { + // Render directly to the screen + bindBackbuffer() + + // We can't trust the core to leave the OpenGL in the same state as + // before retro_run() was called so we restore some state manually. + gl.Disable(gl.DEPTH_TEST) + gl.Disable(gl.CULL_FACE) + gl.Disable(gl.DITHER) + 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.Enable(gl.TEXTURE_2D) + + video.ResizeViewport() + 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 { + if state.Core.HWRenderCallback == nil && video.pitch == 0 { return } 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) gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("OutputSize\x00")), w, h) - bindVertexArray(video.vao) + if state.Core.HWRenderCallback != nil { + gl.UniformMatrix4fv(gl.GetUniformLocation(video.program, gl.Str("MVP\x00")), 1, false, &video.orthoMat[0]) + } + gl.ActiveTexture(gl.TEXTURE0) gl.BindTexture(gl.TEXTURE_2D, video.texID) gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo) + bindVertexArray(video.vao) gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4) + bindVertexArray(0) + + // Reset MVP to identity to avoid menu issues + gl.UniformMatrix4fv(gl.GetUniformLocation(video.program, gl.Str("MVP\x00")), 1, false, &video.identityMat[0]) + gl.UseProgram(0) } // Refresh the texture framebuffer func (video *Video) Refresh(data unsafe.Pointer, width int32, height int32, pitch int32) { + bindBackbuffer() + video.width = width video.height = height video.pitch = pitch @@ -374,13 +432,28 @@ func (video *Video) Refresh(data unsafe.Pointer, width int32, height int32, pitc gl.PixelStorei(gl.UNPACK_ROW_LENGTH, video.pitch/video.bpp) gl.UseProgram(video.program) - gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("TextureSize\x00")), float32(width), float32(height)) - gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("InputSize\x00")), float32(width), float32(height)) - if data == nil { - return + gl.ActiveTexture(gl.TEXTURE0) + gl.BindTexture(gl.TEXTURE_2D, video.texID) + + if data != nil && data != libretro.HWFrameBufferValid { + gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, int32(video.Geom.MaxWidth), int32(video.Geom.MaxHeight), 0, video.pixType, video.pixFmt, data) } - gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, video.pixType, video.pixFmt, data) + + gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("TextureSize\x00")), float32(video.Geom.MaxWidth), float32(video.Geom.MaxHeight)) + gl.Uniform2f(gl.GetUniformLocation(video.program, gl.Str("InputSize\x00")), float32(width), float32(height)) + + gl.UseProgram(0) +} + +// 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 diff --git a/video/video_darwin.go b/video/video_darwin.go index e518b31b..a5c272fc 100644 --- a/video/video_darwin.go +++ b/video/video_darwin.go @@ -3,9 +3,72 @@ package video import ( + "log" + "github.com/go-gl/gl/v2.1/gl" + "github.com/go-gl/mathgl/mgl32" + "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) + + log.Printf("[Video]: Initializing HW render (%v x %v).\n", width, height) + + gl.ActiveTexture(gl.TEXTURE0) + gl.BindTexture(gl.TEXTURE_2D, video.texID) + gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, video.pixType, video.pixFmt, nil) + + gl.GenFramebuffersEXT(1, &video.fboID) + gl.BindFramebufferEXT(gl.FRAMEBUFFER_EXT, video.fboID) + gl.FramebufferTexture2DEXT(gl.FRAMEBUFFER_EXT, gl.COLOR_ATTACHMENT0_EXT, gl.TEXTURE_2D, video.texID, 0) + + hw := state.Core.HWRenderCallback + if hw.Depth { + gl.GenRenderbuffersEXT(1, &video.rboID) + gl.BindRenderbufferEXT(gl.RENDERBUFFER_EXT, video.rboID) + format := gl.DEPTH_COMPONENT16 + if hw.Stencil { + format = gl.DEPTH24_STENCIL8_EXT + } + gl.RenderbufferStorageEXT(gl.RENDERBUFFER_EXT, uint32(format), width, height) + gl.BindRenderbufferEXT(gl.RENDERBUFFER_EXT, 0) + + gl.FramebufferRenderbufferEXT(gl.FRAMEBUFFER_EXT, gl.DEPTH_ATTACHMENT_EXT, gl.RENDERBUFFER_EXT, video.rboID) + if hw.Stencil { + gl.FramebufferRenderbufferEXT(gl.FRAMEBUFFER_EXT, gl.STENCIL_ATTACHMENT_EXT, gl.RENDERBUFFER_EXT, video.rboID) + } + } + + // Default origin is top left + video.orthoMat = mgl32.Ortho2D(-1, 1, -1, 1) + if hw.BottomLeftOrigin { + video.orthoMat = mgl32.Ortho2D(-1, 1, 1, -1) + } + + if st := gl.CheckFramebufferStatusEXT(gl.FRAMEBUFFER_EXT); st != gl.FRAMEBUFFER_COMPLETE_EXT { + log.Fatalf("[Video] Framebuffer is not complete. Error: %v\n", st) + } + + bindBackbuffer() + + gl.ClearColor(0, 0, 0, 1) + if hw.Depth && hw.Stencil { + gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT) + } else if hw.Depth { + gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) + } else { + gl.Clear(gl.COLOR_BUFFER_BIT) + } +} + +func bindBackbuffer() { + gl.BindFramebufferEXT(gl.FRAMEBUFFER_EXT, 0) +} + func genVertexArrays(n int32, arrays *uint32) { gl.GenVertexArraysAPPLE(n, arrays) } diff --git a/video/video_notdarwin.go b/video/video_notdarwin.go index d3399a89..c5e6415b 100644 --- a/video/video_notdarwin.go +++ b/video/video_notdarwin.go @@ -3,9 +3,73 @@ package video import ( + "log" + "github.com/go-gl/gl/v2.1/gl" + "github.com/go-gl/mathgl/mgl32" + "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) + + log.Printf("[Video]: Initializing HW render (%v x %v).\n", width, height) + + gl.ActiveTexture(gl.TEXTURE0) + gl.BindTexture(gl.TEXTURE_2D, video.texID) + gl.TexStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8, width, height) + + gl.GenFramebuffers(1, &video.fboID) + gl.BindFramebuffer(gl.FRAMEBUFFER, video.fboID) + gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, video.texID, 0) + + hw := state.Core.HWRenderCallback + if hw.Depth { + gl.GenRenderbuffers(1, &video.rboID) + gl.BindRenderbuffer(gl.RENDERBUFFER, video.rboID) + format := gl.DEPTH_COMPONENT16 + if hw.Stencil { + format = gl.DEPTH24_STENCIL8 + } + gl.RenderbufferStorage(gl.RENDERBUFFER, uint32(format), width, height) + gl.BindRenderbuffer(gl.RENDERBUFFER, 0) + + if hw.Stencil { + gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, video.rboID) + } else { + gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, video.rboID) + } + } + + // Default origin is top left + video.orthoMat = mgl32.Ortho2D(-1, 1, -1, 1) + if hw.BottomLeftOrigin { + video.orthoMat = mgl32.Ortho2D(-1, 1, 1, -1) + } + + if st := gl.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE { + log.Fatalf("[Video] Framebuffer is not complete. Error: %v\n", st) + } + + bindBackbuffer() + + gl.ClearColor(0, 0, 0, 1) + if hw.Depth && hw.Stencil { + gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT) + } else if hw.Depth { + gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) + } else { + gl.Clear(gl.COLOR_BUFFER_BIT) + } +} + +func bindBackbuffer() { + gl.BindFramebuffer(gl.FRAMEBUFFER, 0) +} + func genVertexArrays(n int32, arrays *uint32) { gl.GenVertexArrays(n, arrays) }