mirror of
https://github.com/rive-app/rive-ios.git
synced 2026-01-18 17:11:28 +01:00
This pull request adds support for both visionOS and tvOS to the Apple (colloquially referred to as iOS) runtime. It should _not_ be a breaking change, since the only major API change is an internal one (see `RenderContext` below). I believe we should be able to make this a minor release. Developers who have subclassed `RiveView` or `RiveRendererView` should not see any changes, unless they were explicitly expecting this view to be `MTKView`, which is fully unavailable on visionOS (hence our recreation - see `RiveMTKView` below. ## Premake Scripts The premake scripts were updated to add a few new variants for iOS: - xros (visionOS devices; named after the internal sdk) - xrsimulator (visionOS simulator; named after the internal sdk) - appletvos (tvOS devices; named after the internal sdk) - appletvsimulator (tvOS simulators; named after the internal sdk) The majority of the work here is copy/pasting existing code, and just adding additional filters when these new options are used, primarily used to target the new SDKs / minimums for those SDKs. ## Shaders Shaders are pre-compiled for visionOS and tvOS separately, and the correct shaders are then used later-on at compile time. ## Build scripts Build scripts were updated to support building the new libraries, targeting the new devices, using the new options above. Additionally, they have to point to new output files. The `build_framework` script has been updated to build the new libraries to add to the final output `xcframework`. ## Project Example targets for both visionOS and tvOS, since these truly are the "native" apps, rather than just iPad-on-your-device. These use a new `streaming` riv by the creative team. The tvOS example app adds additional support for remote control, since that behavior can be utilized in multiple ways during development; that is, we don't add any "default" behavior for remote controls. The visionOS app, on the other hand, works out-of-the-box with no additional changes. ## RenderContext `RenderContext` is an internal type; it's forward-declared, so it's unusable outside of the scope of internal development. There have been some "breaking" changes here - the API has been updated to, instead of passing in `MTKView` around, using `id<RiveMetalDrawableView>`. This had to be changed, regardless, since visionOS does not have `MTKView`. The choice to use a protocol was because it forces a little more explicit initialization across platforms, rather than having a parent class that acts as an abstract class, but isn't abstract because it still needs some default values, but those values are different based on device and API availability, etc. We could've passed around `RiveMTKView` as the type, but with a protocol, there's a possibility of being able to use a type that isn't exactly a view, but might want to still act against the drawing process. Personal choice, really. ## RiveRendererView `RiveRendererView` is now a subclass of `RiveMTKView`. `RiveMTKView`'s superclass depends on the device: - On visionOS, this is a `UIView` with an underlying `CAMetalLayer` - On all other platforms, `MTKView` This new class conforms to `RiveMetalDrawableView`, which allows it to be passed to `RenderContext` types. ### RiveMTKView (visionOS) `RiveMTKView` on visionOS is a subclass of `UIView` that is backed by a `CAMetalLayer`, providing the necessary properties of `RiveMetalDrawableView` (compile-time safety here, baby). This is quite a simple recreation of the default `MTKView`, since that type is not available on visionOS (thanks, Apple). ## Other things Additional compile-time checks for platform OS have been added to make sure each new platform compiles with the correct APIs that can be shared, or otherwise newly implemented. Diffs= 6f70a0e803 Add visionOS and tvOS support to Apple runtime (#8107) Co-authored-by: David Skuza <david@rive.app>
480 lines
14 KiB
Plaintext
480 lines
14 KiB
Plaintext
/*
|
|
* Copyright 2023 Rive
|
|
*/
|
|
|
|
#import <RenderContextManager.h>
|
|
#import <RenderContext.h>
|
|
#import <RiveMetalDrawableView.h>
|
|
#import <Rive.h>
|
|
#import <RivePrivateHeaders.h>
|
|
#import <RiveFactory.h>
|
|
#import <rive_renderer_view.hh>
|
|
|
|
#include "utils/auto_cf.hpp"
|
|
#include "cg_factory.hpp"
|
|
#include "cg_renderer.hpp"
|
|
#include "rive/renderer/gpu.hpp"
|
|
|
|
// Values taken from Page 7 of
|
|
// https://developer.apple.com/metal/Metal-Feature-Set-Tables.pdf
|
|
// Last updated 10/2024
|
|
static CGSize Maximum2DTextureSizeFromDevice(id<MTLDevice> device)
|
|
{
|
|
CGSize size = CGSizeZero;
|
|
// Fall back in reverse order of the table in the above linked document.
|
|
// See Page 1 for additional details on GPUs in each family.
|
|
if ([device supportsFamily:MTLGPUFamilyMac2])
|
|
{
|
|
size = CGSizeMake(16384, 16384);
|
|
}
|
|
#if !TARGET_OS_VISION && !TARGET_OS_TV
|
|
else if ([device supportsFamily:MTLGPUFamilyApple9])
|
|
{
|
|
size = CGSizeMake(16384, 16384);
|
|
}
|
|
#endif
|
|
else if ([device supportsFamily:MTLGPUFamilyApple8])
|
|
{
|
|
size = CGSizeMake(16384, 16384);
|
|
}
|
|
else if ([device supportsFamily:MTLGPUFamilyApple7])
|
|
{
|
|
size = CGSizeMake(16384, 16384);
|
|
}
|
|
else if ([device supportsFamily:MTLGPUFamilyApple6])
|
|
{
|
|
size = CGSizeMake(16384, 16384);
|
|
}
|
|
else if ([device supportsFamily:MTLGPUFamilyApple5])
|
|
{
|
|
size = CGSizeMake(16384, 16384);
|
|
}
|
|
else if ([device supportsFamily:MTLGPUFamilyApple4])
|
|
{
|
|
size = CGSizeMake(16384, 16384);
|
|
}
|
|
else if ([device supportsFamily:MTLGPUFamilyApple3])
|
|
{
|
|
size = CGSizeMake(16384, 16384);
|
|
}
|
|
else if ([device supportsFamily:MTLGPUFamilyApple2])
|
|
{
|
|
size = CGSizeMake(8192, 8192);
|
|
}
|
|
else
|
|
{
|
|
// Anything not noted in the linked table above, assume the lowest
|
|
// Wayback Machine shows that MTLGPUFamilyApple1 was 8192x8192
|
|
size = CGSizeMake(8192, 8192);
|
|
}
|
|
return size;
|
|
}
|
|
|
|
@interface RenderContext (Private)
|
|
@property(nonatomic, assign) CGSize maximum2DTextureSize;
|
|
@end
|
|
|
|
@implementation RenderContext
|
|
|
|
- (rive::Factory*)factory
|
|
{
|
|
return nil;
|
|
}
|
|
|
|
- (rive::Renderer*)beginFrame:(id<RiveMetalDrawableView>)view
|
|
{
|
|
return nil;
|
|
}
|
|
|
|
- (void)endFrame:(id<RiveMetalDrawableView>)view
|
|
withCompletion:(_Nullable MTLCommandBufferHandler)completionHandler;
|
|
{}
|
|
|
|
- (BOOL)canDrawInRect:(CGRect)rect
|
|
drawableSize:(CGSize)drawableSize
|
|
scale:(CGFloat)scale;
|
|
{
|
|
// If for some reason the view is not within a window (screen),
|
|
// scale will be -1
|
|
if (scale == -1)
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
BOOL (^CGSizeWithinRange)(CGSize, CGSize) =
|
|
^BOOL(CGSize size, CGSize maxSize) {
|
|
BOOL isWidthValid = size.width > 0 && size.width <= maxSize.width;
|
|
BOOL isHeightValid = size.height > 0 && size.height <= maxSize.height;
|
|
return isWidthValid && isHeightValid;
|
|
};
|
|
|
|
// Convert points to pixels, as Metal works in pixels
|
|
CGFloat pixelWidth = CGRectGetWidth(rect) * scale;
|
|
CGFloat pixelHeight = CGRectGetHeight(rect) * scale;
|
|
CGSize pixelSize = CGSizeMake(pixelWidth, pixelHeight);
|
|
|
|
return CGSizeWithinRange(pixelSize, self.maximum2DTextureSize) &&
|
|
CGSizeWithinRange(drawableSize, self.maximum2DTextureSize);
|
|
}
|
|
|
|
- (void)setMaximum2DTextureSize:(CGSize)maximum2DTextureSize
|
|
{}
|
|
|
|
- (CGSize)maximum2DTextureSize
|
|
{
|
|
return CGSizeZero;
|
|
}
|
|
|
|
@end
|
|
|
|
#include "rive/renderer/metal/render_context_metal_impl.h"
|
|
#include "rive/renderer/rive_render_image.hpp"
|
|
#include "rive/renderer/rive_renderer.hpp"
|
|
|
|
@interface RiveRendererContext : RenderContext
|
|
@end
|
|
|
|
@implementation RiveRendererContext
|
|
{
|
|
rive::gpu::RenderContext* _renderContext;
|
|
std::unique_ptr<rive::RiveRenderer> _renderer;
|
|
rive::rcp<rive::gpu::RenderTargetMetal> _renderTarget;
|
|
CGSize _maximum2DTextureSize;
|
|
}
|
|
|
|
static std::unique_ptr<rive::gpu::RenderContext> make_pls_context_native(
|
|
id<MTLDevice> gpu)
|
|
{
|
|
return rive::gpu::RenderContextMetalImpl::MakeContext(
|
|
gpu, rive::gpu::RenderContextMetalImpl::ContextOptions());
|
|
}
|
|
|
|
- (instancetype)init
|
|
{
|
|
// Make a single static RenderContext, since it is also the factory and any
|
|
// objects it creates may outlive this 'RiveContext' instance.
|
|
static id<MTLDevice> s_plsGPU = MTLCreateSystemDefaultDevice();
|
|
static std::unique_ptr<rive::gpu::RenderContext> s_renderContext =
|
|
make_pls_context_native(s_plsGPU);
|
|
|
|
self = [super init];
|
|
self.metalDevice = s_plsGPU;
|
|
self.metalQueue = [s_plsGPU newCommandQueue];
|
|
self.depthStencilPixelFormat = MTLPixelFormatInvalid;
|
|
self.framebufferOnly = YES;
|
|
_renderContext = s_renderContext.get();
|
|
_renderer = std::make_unique<rive::RiveRenderer>(_renderContext);
|
|
self.maximum2DTextureSize =
|
|
Maximum2DTextureSizeFromDevice(self.metalDevice);
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
// Once nobody is referencing a RiveContext anymore, release the global
|
|
// RenderContext's GPU resource.
|
|
_renderContext->releaseResources();
|
|
}
|
|
|
|
- (rive::Factory*)factory
|
|
{
|
|
return _renderContext;
|
|
}
|
|
|
|
- (rive::Renderer*)beginFrame:(id<RiveMetalDrawableView>)view
|
|
{
|
|
id<CAMetalDrawable> surface = view.currentDrawable;
|
|
if (!surface.texture)
|
|
{
|
|
NSLog(@"error: no surface texture on MTKView");
|
|
return nullptr;
|
|
}
|
|
|
|
switch (view.colorPixelFormat)
|
|
{
|
|
case MTLPixelFormatBGRA8Unorm:
|
|
case MTLPixelFormatRGBA8Unorm:
|
|
break;
|
|
default:
|
|
NSLog(@"error: unsupported colorPixelFormat on MTKView");
|
|
return nullptr;
|
|
}
|
|
|
|
if (_renderTarget == nullptr ||
|
|
_renderTarget->width() != view.drawableSize.width ||
|
|
_renderTarget->height() != view.drawableSize.height)
|
|
{
|
|
_renderTarget =
|
|
_renderContext
|
|
->static_impl_cast<rive::gpu::RenderContextMetalImpl>()
|
|
->makeRenderTarget(view.colorPixelFormat,
|
|
view.drawableSize.width,
|
|
view.drawableSize.height);
|
|
}
|
|
_renderTarget->setTargetTexture(surface.texture);
|
|
|
|
_renderContext->beginFrame({
|
|
.renderTargetWidth = _renderTarget->width(),
|
|
.renderTargetHeight = _renderTarget->height(),
|
|
.loadAction = rive::gpu::LoadAction::clear,
|
|
.clearColor = 0,
|
|
});
|
|
return _renderer.get();
|
|
}
|
|
|
|
- (void)endFrame:(id<RiveMetalDrawableView>)view
|
|
withCompletion:(_Nullable MTLCommandBufferHandler)completionHandler;
|
|
{
|
|
id<MTLCommandBuffer> flushCommandBuffer = [self.metalQueue commandBuffer];
|
|
_renderContext->flush({
|
|
.renderTarget = _renderTarget.get(),
|
|
.externalCommandBuffer = (__bridge void*)flushCommandBuffer,
|
|
});
|
|
|
|
[flushCommandBuffer presentDrawable:view.currentDrawable];
|
|
if (completionHandler)
|
|
{
|
|
[flushCommandBuffer addCompletedHandler:completionHandler];
|
|
}
|
|
[flushCommandBuffer commit];
|
|
}
|
|
|
|
- (void)setMaximum2DTextureSize:(CGSize)maximum2DTextureSize
|
|
{
|
|
_maximum2DTextureSize = maximum2DTextureSize;
|
|
}
|
|
|
|
- (CGSize)maximum2DTextureSize;
|
|
{
|
|
return _maximum2DTextureSize;
|
|
}
|
|
|
|
@end
|
|
|
|
@interface CGRendererContext : RenderContext
|
|
- (rive::Renderer*)beginFrame:(id<RiveMetalDrawableView>)view;
|
|
@end
|
|
|
|
constexpr static int kBufferRingSize = 3;
|
|
|
|
@implementation CGRendererContext
|
|
{
|
|
id<MTLTexture> _renderTargetTexture;
|
|
id<MTLBuffer> _buffers[kBufferRingSize];
|
|
int _currentBufferIdx;
|
|
AutoCF<CGContextRef> _cgContext;
|
|
std::unique_ptr<rive::CGRenderer> _renderer;
|
|
CGSize _maximum2DTextureSize;
|
|
}
|
|
|
|
- (instancetype)init
|
|
{
|
|
self = [super init];
|
|
|
|
_renderTargetTexture = nil;
|
|
for (int i = 0; i < kBufferRingSize; ++i)
|
|
{
|
|
_buffers[i] = nil;
|
|
}
|
|
_currentBufferIdx = -1;
|
|
|
|
self.metalDevice = MTLCreateSystemDefaultDevice();
|
|
if (!self.metalDevice)
|
|
{
|
|
NSLog(@"Metal is not supported on this device");
|
|
return nil;
|
|
}
|
|
self.metalQueue = [self.metalDevice newCommandQueue];
|
|
self.depthStencilPixelFormat = MTLPixelFormatInvalid;
|
|
self.framebufferOnly = NO;
|
|
self.maximum2DTextureSize =
|
|
Maximum2DTextureSizeFromDevice(self.metalDevice);
|
|
return self;
|
|
}
|
|
|
|
- (rive::Factory*)factory
|
|
{
|
|
static rive::CGFactory factory;
|
|
return &factory;
|
|
}
|
|
|
|
- (rive::Renderer*)beginFrame:(id<RiveMetalDrawableView>)view
|
|
{
|
|
uint32_t cgBitmapInfo;
|
|
switch (view.colorPixelFormat)
|
|
{
|
|
case MTLPixelFormatBGRA8Unorm:
|
|
cgBitmapInfo =
|
|
kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst;
|
|
break;
|
|
case MTLPixelFormatRGBA8Unorm:
|
|
cgBitmapInfo =
|
|
kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast;
|
|
break;
|
|
default:
|
|
NSLog(@"error: unsupported colorPixelFormat on MTKView");
|
|
return nullptr;
|
|
}
|
|
|
|
id<CAMetalDrawable> surface = view.currentDrawable;
|
|
_renderTargetTexture = surface.texture;
|
|
if (!_renderTargetTexture)
|
|
{
|
|
NSLog(@"error: no surface texture on MTKView");
|
|
return nullptr;
|
|
}
|
|
|
|
_currentBufferIdx = (_currentBufferIdx + 1) % kBufferRingSize;
|
|
size_t bufferSize =
|
|
_renderTargetTexture.height * _renderTargetTexture.width * 4;
|
|
if (_buffers[_currentBufferIdx] == nil ||
|
|
_buffers[_currentBufferIdx].allocatedSize != bufferSize)
|
|
{
|
|
_buffers[_currentBufferIdx] =
|
|
[self.metalDevice newBufferWithLength:bufferSize
|
|
options:MTLResourceStorageModeShared];
|
|
}
|
|
AutoCF<CGColorSpaceRef> colorSpace = CGColorSpaceCreateDeviceRGB();
|
|
_cgContext =
|
|
AutoCF(CGBitmapContextCreate(_buffers[_currentBufferIdx].contents,
|
|
_renderTargetTexture.width,
|
|
_renderTargetTexture.height,
|
|
8,
|
|
_renderTargetTexture.width * 4,
|
|
colorSpace,
|
|
cgBitmapInfo));
|
|
|
|
_renderer = std::make_unique<rive::CGRenderer>(
|
|
_cgContext, _renderTargetTexture.width, _renderTargetTexture.height);
|
|
return _renderer.get();
|
|
}
|
|
|
|
- (void)endFrame:(id<RiveMetalDrawableView>)view
|
|
withCompletion:(_Nullable MTLCommandBufferHandler)completionHandler;
|
|
{
|
|
if (_cgContext != nil)
|
|
{
|
|
id<MTLCommandBuffer> commandBuffer = [self.metalQueue commandBuffer];
|
|
id<MTLBlitCommandEncoder> blitEncoder =
|
|
[commandBuffer blitCommandEncoder];
|
|
[blitEncoder copyFromBuffer:_buffers[_currentBufferIdx]
|
|
sourceOffset:0
|
|
sourceBytesPerRow:_renderTargetTexture.width * 4
|
|
sourceBytesPerImage:_renderTargetTexture.height *
|
|
_renderTargetTexture.width * 4
|
|
sourceSize:MTLSizeMake(_renderTargetTexture.width,
|
|
_renderTargetTexture.height,
|
|
1)
|
|
toTexture:_renderTargetTexture
|
|
destinationSlice:0
|
|
destinationLevel:0
|
|
destinationOrigin:MTLOriginMake(0, 0, 0)];
|
|
[blitEncoder endEncoding];
|
|
|
|
[commandBuffer presentDrawable:view.currentDrawable];
|
|
if (completionHandler)
|
|
{
|
|
[commandBuffer addCompletedHandler:completionHandler];
|
|
}
|
|
[commandBuffer commit];
|
|
}
|
|
_renderTargetTexture = nil;
|
|
_renderer = nullptr;
|
|
_cgContext = nullptr;
|
|
}
|
|
|
|
- (void)setMaximum2DTextureSize:(CGSize)maximum2DTextureSize
|
|
{
|
|
_maximum2DTextureSize = maximum2DTextureSize;
|
|
}
|
|
|
|
- (CGSize)maximum2DTextureSize;
|
|
{
|
|
return _maximum2DTextureSize;
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation RenderContextManager
|
|
{
|
|
__weak RiveRendererContext* _riveRendererContextWeakPtr;
|
|
__weak CGRendererContext* _cgContextWeakPtr;
|
|
}
|
|
|
|
// The context manager is a singleton.
|
|
+ (RenderContextManager*)shared
|
|
{
|
|
static RenderContextManager* single = nil;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
single = [[self alloc] init];
|
|
});
|
|
return single;
|
|
}
|
|
|
|
- (instancetype)init
|
|
{
|
|
self.defaultRenderer = RendererType::riveRenderer;
|
|
return self;
|
|
}
|
|
|
|
- (RenderContext*)getDefaultContext
|
|
{
|
|
switch (self.defaultRenderer)
|
|
{
|
|
case RendererType::riveRenderer:
|
|
return [self getRiveRendererContext];
|
|
case RendererType::cgRenderer:
|
|
return [self getCGRendererContext];
|
|
}
|
|
RIVE_UNREACHABLE();
|
|
}
|
|
|
|
- (RenderContext*)getRiveRendererContext
|
|
{
|
|
// Convert our weak reference to strong before trying to work with it. A
|
|
// weak pointer is liable to be released out from under us at any moment.
|
|
// https://stackoverflow.com/questions/15674320/understanding-weak-reference
|
|
RiveRendererContext* strongPtr = _riveRendererContextWeakPtr;
|
|
if (strongPtr == nil)
|
|
{
|
|
strongPtr = [[RiveRendererContext alloc] init];
|
|
_riveRendererContextWeakPtr = strongPtr;
|
|
}
|
|
return strongPtr;
|
|
}
|
|
|
|
- (RenderContext*)getCGRendererContext
|
|
{
|
|
// Convert our weak reference to strong before trying to work with it. A
|
|
// weak pointer is liable to be released out from under us at any moment.
|
|
// https://stackoverflow.com/questions/15674320/understanding-weak-reference
|
|
CGRendererContext* strongPtr = _cgContextWeakPtr;
|
|
if (strongPtr == nil)
|
|
{
|
|
strongPtr = [[CGRendererContext alloc] init];
|
|
_cgContextWeakPtr = strongPtr;
|
|
}
|
|
return strongPtr;
|
|
}
|
|
|
|
- (RiveFactory*)getDefaultFactory
|
|
{
|
|
return [[RiveFactory alloc]
|
|
initWithFactory:[[self getDefaultContext] factory]];
|
|
}
|
|
|
|
- (RiveFactory*)getRiveRendererFactory
|
|
{
|
|
return [[RiveFactory alloc]
|
|
initWithFactory:[[self getRiveRendererContext] factory]];
|
|
}
|
|
|
|
- (RiveFactory*)getCGFactory
|
|
{
|
|
return [[RiveFactory alloc]
|
|
initWithFactory:[[self getCGRendererContext] factory]];
|
|
}
|
|
|
|
@end
|