mirror of
https://github.com/OpenEmu/OpenEmuKit.git
synced 2025-11-01 11:08:14 +00:00
342 lines
10 KiB
Objective-C
342 lines
10 KiB
Objective-C
//
|
|
// OEOpenGL2DGameRenderer.m
|
|
// OpenEmu
|
|
//
|
|
// Created by Alexander Strange on 10/18/15.
|
|
//
|
|
//
|
|
|
|
#import "OEOpenGL2GameRenderer.h"
|
|
#import <OpenGL/gl.h>
|
|
#import <OpenGL/glext.h>
|
|
#import <stdatomic.h>
|
|
#import "OECoreVideoTexture.h"
|
|
#import "OELogging.h"
|
|
|
|
@implementation OEOpenGL2GameRenderer
|
|
{
|
|
OECoreVideoTexture *_texture;
|
|
|
|
// GL stuff
|
|
CGLContextObj _glContext;
|
|
CGLPixelFormatObj _glPixelFormat;
|
|
GLuint _coreVideoFBO; // Framebuffer object which the Core Video pixel buffer is tied to
|
|
GLuint _depthStencilRB; // FBO RenderBuffer Attachment for depth and stencil buffer
|
|
|
|
// Double buffered FBO rendering (3D mode)
|
|
BOOL _isDoubleBufferFBOMode;
|
|
GLuint _alternateFBO; // 3D games may render into this FBO which is blit into the IOSurface. Used if game accidentally syncs surface.
|
|
GLuint _tempRB[2]; // Color and depth buffers backing alternate FBO.
|
|
|
|
// Alternate-thread rendering (3D mode)
|
|
CGLContextObj _alternateContext; // Alternate thread's GL context.
|
|
dispatch_semaphore_t _renderingThreadCanProceedSemaphore;
|
|
dispatch_semaphore_t _executeThreadCanProceedSemaphore;
|
|
|
|
atomic_int _isFPSLimiting; // Enable the "fake vsync" locking to prevent the GPU thread running ahead.
|
|
}
|
|
|
|
@synthesize gameCore=_gameCore;
|
|
|
|
- (nonnull instancetype)initWithInteropTexture:(OECoreVideoTexture *)texture
|
|
{
|
|
self = [super init];
|
|
|
|
_texture = texture;
|
|
_renderingThreadCanProceedSemaphore = dispatch_semaphore_create(0);
|
|
_executeThreadCanProceedSemaphore = dispatch_semaphore_create(0);
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
if (_alternateContext) {
|
|
// Wake up the rendering thread.
|
|
dispatch_semaphore_signal(_renderingThreadCanProceedSemaphore);
|
|
}
|
|
|
|
[self destroyGLResources];
|
|
}
|
|
|
|
- (OEIntSize)surfaceSize {
|
|
CGSize size = _texture.size;
|
|
return OEIntSizeMake(size.width, size.height);
|
|
}
|
|
|
|
- (void)updateRenderer
|
|
{
|
|
[self destroyGLResources];
|
|
[self setupVideo];
|
|
}
|
|
|
|
// Properties
|
|
- (BOOL)canChangeBufferSize
|
|
{
|
|
// 3D games can only change buffer size.
|
|
// We'll be in trouble if a game core does software vector drawing.
|
|
|
|
// TODO: Test alternate threads - might need to call glViewport() again on that thread.
|
|
// TODO: Implement for double buffered FBO - need to reallocate alternateFBO.
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (id)presentationFramebuffer
|
|
{
|
|
GLuint fbo = _isDoubleBufferFBOMode ? _alternateFBO : _coreVideoFBO;
|
|
|
|
return @(fbo);
|
|
}
|
|
|
|
- (void)setupVideo
|
|
{
|
|
[self setupGLContext];
|
|
[self setupFramebuffer];
|
|
|
|
if (_gameCore.needsDoubleBufferedFBO)
|
|
[self setupDoubleBufferedFBO];
|
|
if (_gameCore.hasAlternateRenderingThread)
|
|
[self setupAlternateRenderingThread];
|
|
|
|
[self clearFramebuffer];
|
|
glFlushRenderAPPLE();
|
|
}
|
|
|
|
- (void)setupGLContext
|
|
{
|
|
// init our context.
|
|
static const CGLPixelFormatAttribute attributes[] = {
|
|
kCGLPFAAccelerated,
|
|
kCGLPFAAllowOfflineRenderers,
|
|
kCGLPFANoRecovery,
|
|
kCGLPFAColorSize, 24,
|
|
kCGLPFADepthSize, 24,
|
|
0 };
|
|
|
|
CGLError err = kCGLNoError;
|
|
GLint numPixelFormats = 0;
|
|
|
|
err = CGLChoosePixelFormat(attributes, &_glPixelFormat, &numPixelFormats);
|
|
if(err != kCGLNoError)
|
|
{
|
|
os_log_error(OE_LOG_RENDERER, "Error choosing pixel format %{public}s", CGLErrorString(err));
|
|
[[NSApplication sharedApplication] terminate:nil];
|
|
}
|
|
|
|
err = CGLCreateContext(_glPixelFormat, NULL, &_glContext);
|
|
if(err != kCGLNoError)
|
|
{
|
|
os_log_error(OE_LOG_RENDERER, "Error creating context %{public}s", CGLErrorString(err));
|
|
[[NSApplication sharedApplication] terminate:nil];
|
|
}
|
|
|
|
CGLSetCurrentContext(_glContext);
|
|
_texture.openGLContext = _glContext;
|
|
}
|
|
|
|
- (void)setupFramebuffer
|
|
{
|
|
GLenum status;
|
|
|
|
glGenFramebuffersEXT(1, &_coreVideoFBO);
|
|
glBindFramebufferEXT(GL_FRAMEBUFFER, _coreVideoFBO);
|
|
glFramebufferTexture2DEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_RECTANGLE_ARB, _texture.openGLTexture, 0);
|
|
status = glGetError();
|
|
if(status != 0)
|
|
{
|
|
os_log_error(OE_LOG_RENDERER, "Setup failed: create interop texture FBO 1, OpenGL error %04X", status);
|
|
}
|
|
|
|
// Complete the FBO
|
|
glGenRenderbuffersEXT(1, &_depthStencilRB);
|
|
glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, _depthStencilRB);
|
|
glRenderbufferStorage(GL_RENDERBUFFER_EXT, GL_DEPTH24_STENCIL8, (GLsizei)_texture.size.width, (GLsizei)_texture.size.height);
|
|
glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER_EXT, _depthStencilRB);
|
|
status = glGetError();
|
|
if(status != 0)
|
|
{
|
|
os_log_error(OE_LOG_RENDERER, "Setup failed: create ioSurface FBO 2, OpenGL error %04X", status);
|
|
}
|
|
|
|
status = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT);
|
|
if(status != GL_FRAMEBUFFER_COMPLETE_EXT)
|
|
{
|
|
os_log_error(OE_LOG_RENDERER, "Cannot create FBO, OpenGL error %04X", status);
|
|
}
|
|
}
|
|
|
|
- (void)setupAlternateRenderingThread
|
|
{
|
|
if(_alternateContext == NULL)
|
|
CGLCreateContext(_glPixelFormat, _glContext, &_alternateContext);
|
|
|
|
os_log_debug(OE_LOG_DEFAULT, "Setup GL2.1 3D 'alternate-threaded' rendering");
|
|
}
|
|
|
|
- (void)setupDoubleBufferedFBO
|
|
{
|
|
// Clear the other one while we're on this one.
|
|
[self clearFramebuffer];
|
|
|
|
glGenFramebuffersEXT(1, &_alternateFBO);
|
|
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, _alternateFBO);
|
|
|
|
glGenRenderbuffersEXT(2, _tempRB);
|
|
glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, _tempRB[0]);
|
|
glRenderbufferStorage(GL_RENDERBUFFER_EXT, GL_RGB8, (GLsizei)_texture.size.width, (GLsizei)_texture.size.height);
|
|
glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_RENDERBUFFER_EXT, _tempRB[0]);
|
|
|
|
glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, _tempRB[1]);
|
|
glRenderbufferStorage(GL_RENDERBUFFER_EXT, GL_DEPTH32F_STENCIL8, (GLsizei)_texture.size.width, (GLsizei)_texture.size.height);
|
|
glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER_EXT, _tempRB[1]);
|
|
|
|
GLenum status = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT);
|
|
if(status != GL_FRAMEBUFFER_COMPLETE_EXT)
|
|
{
|
|
os_log_error(OE_LOG_RENDERER, "Cannot create temp FBO. OpenGL error %04X", status);
|
|
|
|
glDeleteFramebuffersEXT(1, &_alternateFBO);
|
|
}
|
|
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
|
|
_isDoubleBufferFBOMode = YES;
|
|
|
|
os_log_debug(OE_LOG_DEFAULT, "Setup GL2.1 3D 'double-buffered FBO' rendering");
|
|
}
|
|
|
|
- (void)clearFramebuffer
|
|
{
|
|
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
}
|
|
|
|
- (void)bindFBO:(GLuint)fbo
|
|
{
|
|
// Bind our FBO / and thus our IOSurface
|
|
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbo);
|
|
// Assume FBOs JUST WORK, because we checked on startExecution
|
|
GLenum status = glGetError();
|
|
if(status)
|
|
{
|
|
os_log_error(OE_LOG_RENDERER, "bindFBO: OpenGL error %04X", status);
|
|
}
|
|
|
|
status = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT);
|
|
if(status != GL_FRAMEBUFFER_COMPLETE_EXT)
|
|
{
|
|
os_log_error(OE_LOG_RENDERER, "OpenGL error %04X in draw, check FBO", status);
|
|
}
|
|
}
|
|
|
|
- (void)presentDoubleBufferedFBO
|
|
{
|
|
glBindFramebufferEXT(GL_READ_FRAMEBUFFER_EXT, _alternateFBO);
|
|
glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER_EXT, _coreVideoFBO);
|
|
|
|
glBlitFramebufferEXT(0, 0, _texture.size.width, _texture.size.height,
|
|
0, 0, _texture.size.width, _texture.size.height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
|
GLenum status = glGetError();
|
|
if(status)
|
|
{
|
|
os_log_error(OE_LOG_RENDERER, "glBlitFramebufferEXT: OpenGL error %04X", status);
|
|
}
|
|
|
|
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, _alternateFBO);
|
|
}
|
|
|
|
- (void)destroyGLResources
|
|
{
|
|
if (_glContext) {
|
|
if (_alternateContext)
|
|
CGLReleaseContext(_alternateContext);
|
|
CGLReleasePixelFormat(_glPixelFormat);
|
|
CGLReleaseContext(_glContext);
|
|
}
|
|
|
|
_alternateContext = nil;
|
|
_texture.openGLContext = nil;
|
|
_glContext = nil;
|
|
_glPixelFormat = nil;
|
|
}
|
|
|
|
- (void)resumeFPSLimiting
|
|
{
|
|
if (_isFPSLimiting == 1) return;
|
|
|
|
atomic_fetch_add(&_isFPSLimiting, 1);
|
|
}
|
|
|
|
- (void)suspendFPSLimiting
|
|
{
|
|
if (_isFPSLimiting == 0) return;
|
|
|
|
atomic_fetch_sub(&_isFPSLimiting, 1);
|
|
|
|
// Wake up the rendering thread one last time.
|
|
// After this, it'll skip checking the semaphore until resumed.
|
|
dispatch_semaphore_signal(_renderingThreadCanProceedSemaphore);
|
|
}
|
|
|
|
// Execution
|
|
- (void)willExecuteFrame
|
|
{
|
|
if (_alternateContext) {
|
|
// Tell the rendering thread to go ahead.
|
|
if (_isFPSLimiting) dispatch_semaphore_signal(_renderingThreadCanProceedSemaphore);
|
|
return;
|
|
}
|
|
|
|
CGLSetCurrentContext(_glContext);
|
|
[self bindFBO:_isDoubleBufferFBOMode ? _alternateFBO : _coreVideoFBO];
|
|
|
|
// Save state in case the game messes it up.
|
|
glPushAttrib(GL_ALL_ATTRIB_BITS);
|
|
glPushClientAttrib(GL_CLIENT_ALL_ATTRIB_BITS);
|
|
}
|
|
|
|
- (void)didExecuteFrame
|
|
{
|
|
if (_alternateContext) {
|
|
// Wait for the rendering thread to complete this frame.
|
|
// Most cores with rendering threads don't seem to handle timing themselves - they're probably relying on Vsync.
|
|
if (_isFPSLimiting) dispatch_semaphore_wait(_executeThreadCanProceedSemaphore, DISPATCH_TIME_FOREVER);
|
|
|
|
// Don't do any other work.
|
|
return;
|
|
}
|
|
|
|
// Reset anything the core did.
|
|
glPopAttrib();
|
|
glPopClientAttrib();
|
|
|
|
// Update the IOSurface.
|
|
glFlushRenderAPPLE();
|
|
}
|
|
|
|
- (void)willRenderFrameOnAlternateThread
|
|
{
|
|
CGLSetCurrentContext(_alternateContext);
|
|
|
|
[self bindFBO:_isDoubleBufferFBOMode ? _alternateFBO : _coreVideoFBO];
|
|
}
|
|
|
|
- (void)didRenderFrameOnAlternateThread
|
|
{
|
|
// Update the IOSurface.
|
|
glFlushRenderAPPLE();
|
|
|
|
// Do FPS limiting, but only once setup is over.
|
|
if (_isFPSLimiting) {
|
|
// Technically the above should be a glFinish(), but I'm hoping the GPU work
|
|
// is fast enough that it's not needed.
|
|
dispatch_semaphore_signal(_executeThreadCanProceedSemaphore);
|
|
|
|
// Wait to be allowed to start next frame.
|
|
dispatch_semaphore_wait(_renderingThreadCanProceedSemaphore, DISPATCH_TIME_FOREVER);
|
|
}
|
|
}
|
|
|
|
@end
|