You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
536 lines
15 KiB
536 lines
15 KiB
/*****************************************************************************
|
|
* VLCCVOpenGLProvider.m: iOS OpenGL ES offscreen provider backed by
|
|
* CVPixelBuffer supporting both iOS/tvOS and MacOSX
|
|
*****************************************************************************
|
|
* Copyright (C) 2021 Videolabs
|
|
*
|
|
* Authors: Alexandre Janniaux <ajanni@videolabs.io>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU Lesser General Public License as published by
|
|
* the Free Software Foundation; either version 2.1 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public License
|
|
* along with this program; if not, write to the Free Software Foundation,
|
|
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
|
|
*****************************************************************************/
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
# import "config.h"
|
|
#endif
|
|
|
|
#import <TargetConditionals.h>
|
|
|
|
#if TARGET_OS_IPHONE
|
|
# import <OpenGLES/EAGL.h>
|
|
# import <OpenGLES/ES2/gl.h>
|
|
# import <CoreVideo/CVOpenGLESTextureCache.h>
|
|
#else
|
|
# import <Cocoa/Cocoa.h>
|
|
# import <OpenGL/OpenGL.h>
|
|
# import <OpenGL/gl.h>
|
|
#endif
|
|
|
|
#import <CoreVideo/CoreVideo.h>
|
|
#import <dlfcn.h>
|
|
|
|
#import <vlc_common.h>
|
|
#import <vlc_filter.h>
|
|
#import <vlc_picture.h>
|
|
#import <vlc_plugin.h>
|
|
#import <vlc_opengl.h>
|
|
#import <vlc_picture_pool.h>
|
|
|
|
#import "codec/vt_utils.h"
|
|
#import "video_output/opengl/vout_helper.h"
|
|
|
|
#define BUFFER_COUNT 3
|
|
|
|
struct vlc_cvbuffer {
|
|
CVPixelBufferRef cvpx;
|
|
GLuint fbo;
|
|
|
|
#if TARGET_OS_IPHONE
|
|
CVOpenGLESTextureRef texture;
|
|
#else
|
|
CVOpenGLTextureRef texture;
|
|
#endif
|
|
};
|
|
|
|
@interface VLCCVOpenGLProvider: NSObject
|
|
{
|
|
vlc_gl_t *_gl;
|
|
|
|
#if TARGET_OS_IPHONE
|
|
EAGLContext* _context;
|
|
EAGLContext* _previousContext;
|
|
CVOpenGLESTextureCacheRef _textureCache;
|
|
#else
|
|
CGLContextObj _context;
|
|
CGLContextObj _previousContext;
|
|
CVOpenGLTextureCacheRef _textureCache;
|
|
#endif
|
|
|
|
/* Framebuffers are stored into the vlc_cvbuffer object for convenience
|
|
* when getting a picture from the pool, but it's also stored there for
|
|
* allocation/deallocation, since:
|
|
* 1/ The pictures won't need the framebuffer after being swapped.
|
|
* 2/ The picture cannot release the framebuffer at release since it
|
|
* needs an OpenGL context.
|
|
* 3/ We can delete the framebuffer as soon as we won't draw into the
|
|
* frame anymore.
|
|
* Note that framebuffers could be reused too when resizing, but this is
|
|
* a bit inconvenient to implement. */
|
|
GLuint _fbos[BUFFER_COUNT];
|
|
|
|
struct vlc_video_context *_vctx_out;
|
|
picture_pool_t *_pool;
|
|
picture_t *_currentPicture;
|
|
size_t _countCurrent;
|
|
|
|
video_format_t _fmt_out;
|
|
}
|
|
|
|
- (id)initWithGL:(vlc_gl_t*)gl width:(unsigned)width height:(unsigned)height;
|
|
- (void)makeCurrent;
|
|
- (void)releaseCurrent;
|
|
- (picture_t*)swap;
|
|
- (int)resize:(CGSize)size;
|
|
|
|
@end
|
|
|
|
/*****************************************************************************
|
|
* vout opengl callbacks
|
|
*****************************************************************************/
|
|
static void *GetSymbol(vlc_gl_t *gl, const char *name)
|
|
{
|
|
VLC_UNUSED(gl);
|
|
|
|
return dlsym(RTLD_DEFAULT, name);
|
|
}
|
|
|
|
static int MakeCurrent(vlc_gl_t *gl)
|
|
{
|
|
VLCCVOpenGLProvider *context = (__bridge VLCCVOpenGLProvider*) gl->sys;
|
|
|
|
[context makeCurrent];
|
|
return VLC_SUCCESS;
|
|
}
|
|
|
|
static void ReleaseCurrent(vlc_gl_t *gl)
|
|
{
|
|
VLCCVOpenGLProvider *context = (__bridge VLCCVOpenGLProvider*)gl->sys;
|
|
[context releaseCurrent];
|
|
}
|
|
|
|
static picture_t* Swap(vlc_gl_t *gl)
|
|
{
|
|
VLCCVOpenGLProvider *context = (__bridge VLCCVOpenGLProvider*)gl->sys;
|
|
return [context swap];
|
|
}
|
|
|
|
static void Resize(vlc_gl_t *gl, unsigned width, unsigned height)
|
|
{
|
|
VLCCVOpenGLProvider *context = (__bridge VLCCVOpenGLProvider*)gl->sys;
|
|
[context resize:CGSizeMake(width, height)];
|
|
}
|
|
|
|
static void Close(vlc_gl_t *gl)
|
|
{
|
|
VLCCVOpenGLProvider *context = (__bridge_transfer VLCCVOpenGLProvider*)gl->sys;
|
|
gl->sys = nil;
|
|
|
|
/* context has been transferred and gl->sys won't track it now, so we can
|
|
* let ARC release it. */
|
|
(void)context;
|
|
}
|
|
|
|
static void FreeCVBuffer(picture_t *picture)
|
|
{
|
|
struct vlc_cvbuffer *buffer = picture->p_sys;
|
|
if (buffer->texture)
|
|
CFRelease(buffer->texture);
|
|
if (buffer->cvpx)
|
|
CFRelease(buffer->cvpx);
|
|
free(buffer);
|
|
}
|
|
|
|
|
|
@implementation VLCCVOpenGLProvider
|
|
|
|
- (picture_t *)initBuffer
|
|
{
|
|
struct vlc_cvbuffer *buffer = malloc(sizeof *buffer);
|
|
if (buffer == NULL)
|
|
return NULL;
|
|
buffer->texture = NULL;
|
|
buffer->cvpx = NULL;
|
|
buffer->fbo = 0;
|
|
|
|
/* CoreVideo functions will use this variable for error reporting. */
|
|
CVReturn cvret;
|
|
unsigned width = _fmt_out.i_visible_width;
|
|
unsigned height = _fmt_out.i_visible_height;
|
|
|
|
/* The buffer are IOSurface-backed, we don't map them to CPU memory. */
|
|
const picture_resource_t resource = {
|
|
.p_sys = buffer,
|
|
.pf_destroy = FreeCVBuffer,
|
|
};
|
|
picture_t *picture = picture_NewFromResource(&_fmt_out, &resource);
|
|
if (picture == NULL)
|
|
{
|
|
free(buffer);
|
|
return NULL;
|
|
}
|
|
|
|
NSDictionary *cvpx_attr = @{
|
|
(__bridge NSString*)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA),
|
|
(__bridge NSString*)kCVPixelBufferWidthKey: @(width),
|
|
(__bridge NSString*)kCVPixelBufferHeightKey: @(height),
|
|
(__bridge NSString*)kCVPixelBufferBytesPerRowAlignmentKey: @(16),
|
|
/* Necessary for having iosurface-backed CVPixelBuffer, but
|
|
* note that iOS simulator won't be able to display them. */
|
|
(__bridge NSString*)kCVPixelBufferIOSurfacePropertiesKey: @{},
|
|
#if TARGET_OS_IPHONE
|
|
(__bridge NSString*)kCVPixelBufferOpenGLESCompatibilityKey : @YES,
|
|
(__bridge NSString*)kCVPixelBufferIOSurfaceOpenGLESFBOCompatibilityKey: @YES,
|
|
(__bridge NSString*)kCVPixelBufferIOSurfaceOpenGLESTextureCompatibilityKey: @YES,
|
|
#endif
|
|
};
|
|
|
|
cvret = CVPixelBufferCreate(kCFAllocatorDefault, width, height,
|
|
kCVPixelFormatType_32BGRA, (__bridge CFDictionaryRef)cvpx_attr,
|
|
&buffer->cvpx);
|
|
|
|
if (cvret != kCVReturnSuccess)
|
|
{
|
|
picture_Release(picture);
|
|
return nil;
|
|
}
|
|
|
|
/* The CVPX buffer will be hold by the picture_t. */
|
|
cvpxpic_attach(picture, buffer->cvpx, _vctx_out, NULL);
|
|
|
|
#if TARGET_OS_IPHONE
|
|
cvret = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
|
|
_textureCache, buffer->cvpx,
|
|
nil, // CFDictionaryRef textureAttributes
|
|
GL_TEXTURE_2D, // GLenum target
|
|
GL_RGBA, // GLint internalFormat
|
|
_fmt_out.i_visible_width, // GLsizei width
|
|
_fmt_out.i_visible_height, // GLsizei height
|
|
GL_BGRA, // GLenum format for native data
|
|
GL_UNSIGNED_BYTE, // GLenum type
|
|
0, // size_t planeIndex
|
|
&buffer->texture);
|
|
|
|
if (cvret != kCVReturnSuccess)
|
|
{
|
|
picture_Release(picture);
|
|
return nil;
|
|
}
|
|
|
|
assert(CVOpenGLESTextureGetTarget(buffer->texture)
|
|
== GL_TEXTURE_2D);
|
|
|
|
GLuint name = CVOpenGLESTextureGetName(buffer->texture);
|
|
GLenum target = CVOpenGLESTextureGetTarget(buffer->texture);
|
|
|
|
#else
|
|
|
|
cvret = CVOpenGLTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
|
|
_textureCache, buffer->cvpx,
|
|
nil, &buffer->texture);
|
|
|
|
if (cvret != kCVReturnSuccess)
|
|
{
|
|
picture_Release(picture);
|
|
return nil;
|
|
}
|
|
|
|
assert(CVOpenGLTextureGetTarget(buffer->texture)
|
|
== GL_TEXTURE_RECTANGLE);
|
|
|
|
GLenum target = CVOpenGLTextureGetTarget(buffer->texture);
|
|
GLuint name = CVOpenGLTextureGetName(buffer->texture);
|
|
#endif
|
|
|
|
glGenFramebuffers(1, &buffer->fbo);
|
|
|
|
glBindTexture(target, name);
|
|
glTexParameteri(target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
glTexParameteri(target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
glBindFramebuffer(GL_FRAMEBUFFER, buffer->fbo);
|
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
|
target, name, 0);
|
|
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
|
assert(status == GL_FRAMEBUFFER_COMPLETE);
|
|
assert(cvret == kCVReturnSuccess);
|
|
|
|
return picture;
|
|
}
|
|
|
|
- (id)initWithGL:(vlc_gl_t*)gl width:(unsigned)width height:(unsigned)height
|
|
{
|
|
_gl = gl;
|
|
_context = nil;
|
|
_previousContext = nil;
|
|
_textureCache = nil;
|
|
_pool = nil;
|
|
_currentPicture = nil;
|
|
|
|
/* CoreVideo functions will use this variable for error reporting. */
|
|
CVReturn cvret;
|
|
|
|
video_format_Init(&_fmt_out, VLC_CODEC_CVPX_BGRA);
|
|
|
|
_fmt_out.i_visible_width
|
|
= _fmt_out.i_width
|
|
= width;
|
|
|
|
_fmt_out.i_visible_height
|
|
= _fmt_out.i_height
|
|
= height;
|
|
|
|
static struct vlc_video_context_operations vctx_ops =
|
|
{ .destroy = NULL };
|
|
|
|
_vctx_out = vlc_video_context_CreateCVPX(
|
|
gl->device, CVPX_VIDEO_CONTEXT_DEFAULT, sizeof(VLCCVOpenGLProvider*),
|
|
&vctx_ops);
|
|
|
|
if (_vctx_out == NULL)
|
|
return nil;
|
|
|
|
#if TARGET_OS_IPHONE
|
|
_context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
|
|
if (_context == nil)
|
|
return nil;
|
|
|
|
cvret = CVOpenGLESTextureCacheCreate(kCFAllocatorDefault,
|
|
nil, _context, nil, &_textureCache);
|
|
|
|
#else
|
|
CGLPixelFormatAttribute pixel_attr[] = {
|
|
kCGLPFAAccelerated,
|
|
kCGLPFAAllowOfflineRenderers,
|
|
0,
|
|
};
|
|
|
|
CGLPixelFormatObj pixelFormat;
|
|
GLint numPixelFormats = 0;
|
|
|
|
CGLChoosePixelFormat(pixel_attr, &pixelFormat, &numPixelFormats);
|
|
CGLError cglerr = CGLCreateContext(pixelFormat, NULL, &_context);
|
|
CGLDestroyPixelFormat(pixelFormat);
|
|
if (cglerr != kCGLNoError)
|
|
return nil;
|
|
|
|
cvret = CVOpenGLTextureCacheCreate(kCFAllocatorDefault,
|
|
nil, _context, pixelFormat, nil, &_textureCache);
|
|
#endif
|
|
if (cvret != kCVReturnSuccess)
|
|
return nil;
|
|
|
|
/* OpenGL context is now current, so we can use OpenGL functions. */
|
|
if ([self resize:CGSizeMake(width, height)] != VLC_SUCCESS)
|
|
return nil;
|
|
|
|
static const struct vlc_gl_operations gl_ops =
|
|
{
|
|
.make_current = MakeCurrent,
|
|
.release_current = ReleaseCurrent,
|
|
.resize = Resize,
|
|
.swap_offscreen = Swap,
|
|
.get_proc_address = GetSymbol,
|
|
.close = Close,
|
|
};
|
|
gl->ops = &gl_ops;
|
|
gl->orientation = ORIENT_VFLIPPED;
|
|
gl->offscreen_vctx_out = _vctx_out;
|
|
gl->offscreen_chroma_out = VLC_CODEC_CVPX_BGRA;
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)makeCurrent
|
|
{
|
|
/* TODO: check if it works with app inactive */
|
|
if (_countCurrent++ > 0)
|
|
return;
|
|
|
|
#if TARGET_OS_IPHONE
|
|
_previousContext = [EAGLContext currentContext];
|
|
[EAGLContext setCurrentContext:_context];
|
|
#else
|
|
_previousContext = CGLGetCurrentContext();
|
|
CGLSetCurrentContext(_context);
|
|
#endif
|
|
}
|
|
|
|
- (void)releaseCurrent
|
|
{
|
|
if (--_countCurrent > 0)
|
|
return;
|
|
|
|
#if TARGET_OS_IPHONE
|
|
[EAGLContext setCurrentContext:_previousContext];
|
|
#else
|
|
CGLSetCurrentContext(_previousContext);
|
|
#endif
|
|
|
|
_previousContext = nil;
|
|
}
|
|
|
|
- (picture_t*)swap
|
|
{
|
|
/* EAGLContext has no formal swap operation but we swap the backing
|
|
* CVPX buffer ourselves. */
|
|
|
|
/* Note:
|
|
* The result must be used after completion of the rendering, meaning it must wait
|
|
* for completion of a fence or call glFinish:
|
|
* https://www.khronos.org/registry/OpenGL/extensions/APPLE/APPLE_sync.txt
|
|
* In practice, it seems that implicit sync is enough, probably because of the
|
|
* actual context changes, but it might need a proper synchronization mechanism
|
|
* in the future. */
|
|
|
|
[self makeCurrent];
|
|
|
|
picture_t *output = _currentPicture;
|
|
output->p_sys = NULL;
|
|
|
|
picture_t *next_picture = picture_pool_Wait(_pool);
|
|
struct vlc_cvbuffer *buffer = next_picture->p_sys;
|
|
assert(buffer != NULL);
|
|
|
|
_currentPicture = next_picture;
|
|
// TODO: rebind at makeCurrent instead, if not binded?
|
|
glFinish();
|
|
glBindFramebuffer(GL_FRAMEBUFFER, buffer->fbo);
|
|
|
|
[self releaseCurrent];
|
|
|
|
return output;
|
|
}
|
|
|
|
- (int)resize:(CGSize)size
|
|
{
|
|
[self makeCurrent];
|
|
|
|
if (_pool)
|
|
{
|
|
glDeleteFramebuffers(BUFFER_COUNT, _fbos);
|
|
picture_pool_Release(_pool);
|
|
}
|
|
|
|
if (_currentPicture)
|
|
picture_Release(_currentPicture);
|
|
|
|
picture_t *pictures[BUFFER_COUNT];
|
|
|
|
/* OpenGL context is now current, so we can use OpenGL functions.
|
|
* Allocate the pictures and store the framebuffer for later deallocation
|
|
* since framebuffers can only be deleted within an OpenGL context. */
|
|
size_t bufferCount;
|
|
for (bufferCount = 0; bufferCount < BUFFER_COUNT; ++bufferCount)
|
|
{
|
|
picture_t *picture = [self initBuffer];
|
|
if (picture == NULL)
|
|
break;
|
|
struct vlc_cvbuffer *buffer = picture->p_sys;
|
|
_fbos[bufferCount] = buffer->fbo;
|
|
pictures[bufferCount] = picture;
|
|
}
|
|
if (bufferCount != BUFFER_COUNT)
|
|
{
|
|
for (size_t i=0; i<bufferCount; ++i)
|
|
picture_Release(pictures[i]);
|
|
glDeleteFramebuffers(bufferCount, _fbos);
|
|
return VLC_ENOMEM;
|
|
}
|
|
|
|
_pool = picture_pool_New(BUFFER_COUNT, pictures);
|
|
if (_pool == nil)
|
|
{
|
|
for (size_t i=0; i<BUFFER_COUNT; ++i)
|
|
picture_Release(pictures[i]);
|
|
glDeleteFramebuffers(BUFFER_COUNT, _fbos);
|
|
return VLC_ENOMEM;
|
|
}
|
|
|
|
_currentPicture = picture_pool_Wait(_pool);
|
|
struct vlc_cvbuffer *buffer = _currentPicture->p_sys;
|
|
assert(buffer != NULL);
|
|
glBindFramebuffer(GL_FRAMEBUFFER, buffer->fbo);
|
|
|
|
[self releaseCurrent];
|
|
return VLC_SUCCESS;
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
/* Delete OpenGL resources */
|
|
if (_context != nil && _pool != nil)
|
|
{
|
|
[self makeCurrent];
|
|
glFinish();
|
|
/* Delete native resources */
|
|
glDeleteFramebuffers(BUFFER_COUNT, _fbos);
|
|
[self releaseCurrent];
|
|
}
|
|
|
|
if (_textureCache != nil)
|
|
CFRelease(_textureCache);
|
|
|
|
if (_currentPicture != nil)
|
|
picture_Release(_currentPicture);
|
|
|
|
if (_pool != nil)
|
|
picture_pool_Release(_pool);
|
|
|
|
if (_vctx_out != nil)
|
|
vlc_video_context_Release(_vctx_out);
|
|
|
|
video_format_Clean(&_fmt_out);
|
|
}
|
|
|
|
@end
|
|
|
|
static int Open(vlc_gl_t *gl, unsigned width, unsigned height,
|
|
const struct vlc_gl_cfg *gl_cfg)
|
|
{
|
|
if (gl_cfg->need_alpha)
|
|
{
|
|
msg_Err(gl, "Cannot support alpha yet");
|
|
return VLC_ENOTSUP;
|
|
}
|
|
|
|
VLCCVOpenGLProvider *sys = [[VLCCVOpenGLProvider alloc] initWithGL:gl width:width height:height];
|
|
if (sys == nil)
|
|
return VLC_EGENERIC;;
|
|
|
|
gl->sys = (__bridge_retained void*)sys;
|
|
|
|
return VLC_SUCCESS;
|
|
}
|
|
|
|
vlc_module_begin()
|
|
set_shortname( N_("cvpx_gl") )
|
|
set_description( N_("OpenGL backed by CVPixelBuffer") )
|
|
#if TARGET_OS_IPHONE
|
|
set_callback_opengl_es2_offscreen( Open, 100 )
|
|
#else
|
|
set_callback_opengl_offscreen( Open, 100 )
|
|
#endif
|
|
add_shortcut( "cvpx_gl" )
|
|
vlc_module_end()
|
|
|