diff --git a/GLLara.xcodeproj/project.pbxproj b/GLLara.xcodeproj/project.pbxproj index 4f90096..0c75caf 100644 --- a/GLLara.xcodeproj/project.pbxproj +++ b/GLLara.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -228,6 +228,7 @@ 526E0FAA1C169E3500F198BF /* testStaticTRLShadeless.png in Resources */ = {isa = PBXBuildFile; fileRef = 526E0F991C169E3500F198BF /* testStaticTRLShadeless.png */; }; 526E0FAB1C169E3500F198BF /* testStaticTRUDiffuse.png in Resources */ = {isa = PBXBuildFile; fileRef = 526E0F9A1C169E3500F198BF /* testStaticTRUDiffuse.png */; }; 526E0FAC1C169E3500F198BF /* testStaticTRUDiffuseLightmap.png in Resources */ = {isa = PBXBuildFile; fileRef = 526E0F9B1C169E3500F198BF /* testStaticTRUDiffuseLightmap.png */; }; + 5272709C2BE77A7D00EE52B5 /* GLLTexture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5272709A2BE600C300EE52B5 /* GLLTexture.swift */; }; 5274446427FCC9C100E5A3FD /* GLLModelMesh.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5274446327FCC9C100E5A3FD /* GLLModelMesh.swift */; }; 5274446627FD64F000E5A3FD /* GLLModelMeshObj.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5274446527FD64F000E5A3FD /* GLLModelMeshObj.swift */; }; 5274446827FD6F7F00E5A3FD /* GLLVertexAttribAccessorSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5274446727FD6F7F00E5A3FD /* GLLVertexAttribAccessorSet.swift */; }; @@ -255,7 +256,6 @@ 529692D115F2374F00DF2FA3 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 529692D015F2374F00DF2FA3 /* libz.dylib */; }; 529692DA15F2625200DF2FA3 /* GLLItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 529692D915F2625200DF2FA3 /* GLLItem.m */; }; 529692DD15F2759400DF2FA3 /* GLLItemBone.m in Sources */ = {isa = PBXBuildFile; fileRef = 529692DC15F2759400DF2FA3 /* GLLItemBone.m */; }; - 529692E915F28BFB00DF2FA3 /* GLLTexture.m in Sources */ = {isa = PBXBuildFile; fileRef = 529692E815F28BFB00DF2FA3 /* GLLTexture.m */; }; 529693F515F2B58B00DF2FA3 /* GLLASCIIScanner.m in Sources */ = {isa = PBXBuildFile; fileRef = 529693F415F2B58B00DF2FA3 /* GLLASCIIScanner.m */; }; 529693F815F2D02900DF2FA3 /* xnaLaraDefault.modelparams.plist in Resources */ = {isa = PBXBuildFile; fileRef = 529693F715F2D02900DF2FA3 /* xnaLaraDefault.modelparams.plist */; }; 529693FA15F36DDB00DF2FA3 /* lara.modelparams.plist in Resources */ = {isa = PBXBuildFile; fileRef = 529693F915F36DDB00DF2FA3 /* lara.modelparams.plist */; }; @@ -668,6 +668,7 @@ 526E0F991C169E3500F198BF /* testStaticTRLShadeless.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = testStaticTRLShadeless.png; sourceTree = ""; }; 526E0F9A1C169E3500F198BF /* testStaticTRUDiffuse.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = testStaticTRUDiffuse.png; sourceTree = ""; }; 526E0F9B1C169E3500F198BF /* testStaticTRUDiffuseLightmap.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = testStaticTRUDiffuseLightmap.png; sourceTree = ""; }; + 5272709A2BE600C300EE52B5 /* GLLTexture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GLLTexture.swift; sourceTree = ""; }; 5274446327FCC9C100E5A3FD /* GLLModelMesh.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GLLModelMesh.swift; sourceTree = ""; }; 5274446527FD64F000E5A3FD /* GLLModelMeshObj.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GLLModelMeshObj.swift; sourceTree = ""; }; 5274446727FD6F7F00E5A3FD /* GLLVertexAttribAccessorSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GLLVertexAttribAccessorSet.swift; sourceTree = ""; }; @@ -713,8 +714,6 @@ 529692DC15F2759400DF2FA3 /* GLLItemBone.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GLLItemBone.m; sourceTree = ""; }; 529692DF15F27D5900DF2FA3 /* Source Code Overview.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "Source Code Overview.md"; sourceTree = ""; }; 529692E515F289EB00DF2FA3 /* OpenGL.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenGL.framework; path = System/Library/Frameworks/OpenGL.framework; sourceTree = SDKROOT; }; - 529692E715F28BFB00DF2FA3 /* GLLTexture.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GLLTexture.h; sourceTree = ""; }; - 529692E815F28BFB00DF2FA3 /* GLLTexture.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GLLTexture.m; sourceTree = ""; }; 529693F315F2B58A00DF2FA3 /* GLLASCIIScanner.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GLLASCIIScanner.h; sourceTree = ""; }; 529693F415F2B58B00DF2FA3 /* GLLASCIIScanner.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GLLASCIIScanner.m; sourceTree = ""; }; 529693F715F2D02900DF2FA3 /* xnaLaraDefault.modelparams.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = xnaLaraDefault.modelparams.plist; sourceTree = ""; }; @@ -1393,12 +1392,11 @@ 5296942115F4115800DF2FA3 /* Render resources */ = { isa = PBXGroup; children = ( - 529692E715F28BFB00DF2FA3 /* GLLTexture.h */, - 529692E815F28BFB00DF2FA3 /* GLLTexture.m */, 52152CEE16B66951001AE54C /* GLLDDSFile.swift */, 52C516FE2871998C000EB8C2 /* GLLPipelineStateInformation.swift */, 52CDFEA3287369B100BC4298 /* GLLVertexAttribAccessor.swift */, 52C6115A2877080900ED8112 /* GLLResourceManager.swift */, + 5272709A2BE600C300EE52B5 /* GLLTexture.swift */, ); name = "Render resources"; sourceTree = ""; @@ -1573,9 +1571,10 @@ 525ACD3E15F0F1A700534E7D /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; CLASSPREFIX = GLL; LastTestingUpgradeCheck = 0620; - LastUpgradeCheck = 1420; + LastUpgradeCheck = 1520; ORGANIZATIONNAME = "Torsten Kammer"; TargetAttributes = { 523DE0DD1604A5ED00BB9F61 = { @@ -1866,7 +1865,6 @@ 529692DA15F2625200DF2FA3 /* GLLItem.m in Sources */, 529692DD15F2759400DF2FA3 /* GLLItemBone.m in Sources */, 523BBB052880BCB300B2D52E /* GLLGameControllerManager.swift in Sources */, - 529692E915F28BFB00DF2FA3 /* GLLTexture.m in Sources */, 529693F515F2B58B00DF2FA3 /* GLLASCIIScanner.m in Sources */, 5274448B280606A200E5A3FD /* XnaLaraShader.metal in Sources */, 521102E7288DBF72001BE4BC /* HUD.swift in Sources */, @@ -1903,6 +1901,7 @@ 52C9F6161600022B003272E1 /* GLLModelObj.swift in Sources */, 5274447427FE428000E5A3FD /* GLLModelXNALara.swift in Sources */, 52B6C5372BE2AB0E005E53CE /* ObjFile.swift in Sources */, + 5272709C2BE77A7D00EE52B5 /* GLLTexture.swift in Sources */, 526D5AE62060596000DCDE85 /* GLLNotifications.m in Sources */, 5274447027FE21F100E5A3FD /* GLLModelMesh+OBJExport.swift in Sources */, 52B6C53C2BE56696005E53CE /* GLLItemMeshTexture.swift in Sources */, @@ -2377,6 +2376,7 @@ ENABLE_HARDENED_RUNTIME = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_ENABLE_OBJC_EXCEPTIONS = YES; @@ -2438,6 +2438,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_HARDENED_RUNTIME = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_ENABLE_OBJC_EXCEPTIONS = YES; GCC_NO_COMMON_BLOCKS = YES; @@ -2523,7 +2524,11 @@ "$(inherited)", ); INFOPLIST_FILE = "GLLaraTests/GLLaraTests-Info.plist"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = "de.ferroequinologist.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "GLLaraTests/GLLaraTests-Bridging-Header.h"; @@ -2546,7 +2551,11 @@ "$(inherited)", ); INFOPLIST_FILE = "GLLaraTests/GLLaraTests-Info.plist"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = "de.ferroequinologist.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "GLLaraTests/GLLaraTests-Bridging-Header.h"; diff --git a/GLLara/GLLDocument.m b/GLLara/GLLDocument.m index 451b007..f328a35 100644 --- a/GLLara/GLLDocument.m +++ b/GLLara/GLLDocument.m @@ -21,7 +21,6 @@ #import "GLLLogarithmicValueTransformer.h" #import "GLLRenderWindowController.h" #import "GLLSelection.h" -#import "GLLTexture.h" #import "GLLara-Swift.h" diff --git a/GLLara/GLLItemDragDestination.swift b/GLLara/GLLItemDragDestination.swift index 119e686..b589749 100644 --- a/GLLara/GLLItemDragDestination.swift +++ b/GLLara/GLLItemDragDestination.swift @@ -13,7 +13,7 @@ import UniformTypeIdentifiers @objc weak var document: GLLDocument? = nil @objc func itemDraggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { - guard let document = document, let pasteboardItems = sender.draggingPasteboard.pasteboardItems else { + guard document != nil, let pasteboardItems = sender.draggingPasteboard.pasteboardItems else { return [] } diff --git a/GLLara/GLLItemMesh+MeshExport.swift b/GLLara/GLLItemMesh+MeshExport.swift index f2664ac..3fcfc01 100644 --- a/GLLara/GLLItemMesh+MeshExport.swift +++ b/GLLara/GLLItemMesh+MeshExport.swift @@ -51,7 +51,7 @@ extension GLLItemMesh { } private func textureUrls(description: XnaLaraShaderDescription) -> [URL] { - return description.textureUniformsInOrder.map { texture(withIdentifier: $0).textureURL as! URL } + return description.textureUniformsInOrder.map { texture(withIdentifier: $0).textureURL! as URL } } func writeASCII() throws -> String { diff --git a/GLLara/GLLItemMeshState.swift b/GLLara/GLLItemMeshState.swift index 66cab90..00047fc 100644 --- a/GLLara/GLLItemMeshState.swift +++ b/GLLara/GLLItemMeshState.swift @@ -345,7 +345,7 @@ class GLLItemMeshState { /// TODO Ugly let textures = loadedTextures.map { $0.texture } - commandEncoder.useResources(textures, usage: .sample) + commandEncoder.useResources(textures, usage: .read) commandEncoder.setRenderPipelineState(pipelineStateInformation.pipelineState) commandEncoder.setFragmentBuffer(fragmentArgumentBuffer, offset: 0, index: Int(GLLFragmentBufferIndexArguments.rawValue)) diff --git a/GLLara/GLLTexture.h b/GLLara/GLLTexture.h deleted file mode 100644 index 8d7b58b..0000000 --- a/GLLara/GLLTexture.h +++ /dev/null @@ -1,42 +0,0 @@ -// -// GLLTexture.h -// GLLara -// -// Created by Torsten Kammer on 01.09.12. -// Copyright (c) 2012 Torsten Kammer. All rights reserved. -// - -#import -#import - -extern NSString *GLLTextureChangeNotification; - -/*! - * @abstract A texture. - * @discussion Nothing much to see here. This uses hand-written code to load DDS files and ImageIO to load everything else, and vImage to unpremultiply whatever comes from ImageIO. - */ -@interface GLLTexture : NSObject - -- (id)initWithURL:(NSURL *)url device:(id)device error:(NSError *__autoreleasing *)error __attribute__((nonnull(1))); - -/*! - * @abstract Load from data (assuming this is part of some other file) - * @discussion Intended in particular for glTF (binary glTF and data URIs in it), - * where the file may start sort of randomly, and where updating the texture - * independent of the model is not possible anyway. - * @param data The data to load - * @param url The URL to use for error messages - * @param error Output error - */ -- (id)initWithData:(NSData *)data sourceURL:(NSURL *)url device:(id)device error:(NSError *__autoreleasing *)error __attribute__((nonnull(1))); - -- (void)unload; - -@property (nonatomic, assign, readonly) NSUInteger width; -@property (nonatomic, assign, readonly) NSUInteger height; - -@property (nonatomic) id device; -@property (nonatomic) NSURL *url; -@property (nonatomic, readonly) id texture; - -@end diff --git a/GLLara/GLLTexture.m b/GLLara/GLLTexture.m deleted file mode 100644 index 3bd61e2..0000000 --- a/GLLara/GLLTexture.m +++ /dev/null @@ -1,609 +0,0 @@ -// -// GLLTexture.m -// GLLara -// -// Created by Torsten Kammer on 01.09.12. -// Copyright (c) 2012 Torsten Kammer. All rights reserved. -// - -#import "GLLTexture.h" - -#import -#import -#import -#import - -#import "GLLara-Swift.h" -#import "GLLNotifications.h" -#import "GLLTiming.h" - -NSString *GLLTextureChangeNotification = @"GLL Texture Change Notification"; - -enum GLLTextureOrder { - GLLTextureOrderARGB, - GLLTextureOrderBGRA -}; - -static int numMipmapLevels(long width, long height) { - long widerDimension = MAX(width, height); - int firstBit = flsl(widerDimension); // Computes floor(log2(x)). We want ceil(log2(x)) - int numberOfLevels = firstBit; - if ((widerDimension & ~(1 << firstBit)) == 0) - return firstBit - 1; - return numberOfLevels; -} - -static NSOperationQueue *imageInformationQueue = nil; - -@interface GLLTexture () - -- (BOOL)_loadDDSTextureWithData:(NSData *)data error:(NSError *__autoreleasing*)error; -- (BOOL)_loadCGCompatibleTexture:(NSData *)data error:(NSError *__autoreleasing*)error; -- (BOOL)_loadPDFTextureWithData:(NSData *)data error:(NSError *__autoreleasing*)error; -- (void)_loadAndFreePremultipliedARGBData:(void *)data; -- (void)_loadAndFreeUnpremultipliedData:(vImage_Buffer *)unpremultipliedBufferData order:(enum GLLTextureOrder)textureOrder; -- (void)_loadDefaultTexture; - -- (BOOL)_loadDataError:(NSError *__autoreleasing*)error; -- (BOOL)_loadWithData:(NSData *)data error:(NSError *__autoreleasing*)error; - -- (void)_setupGCDObserving; - -@property (nonatomic, assign, readwrite) NSUInteger width; -@property (nonatomic, assign, readwrite) NSUInteger height; - -@end - -@implementation GLLTexture - -+ (void)initialize -{ - imageInformationQueue = [[NSOperationQueue alloc] init]; - imageInformationQueue.maxConcurrentOperationCount = 1; -} - -+ (NSSet *)keyPathsForValuesAffectingPresentedItemURL -{ - return [NSSet setWithObject:@"url"]; -} - -- (id)initWithURL:(NSURL *)url device:(id)device error:(NSError *__autoreleasing *)error -{ - NSParameterAssert(url); - - if (!(self = [super init])) return nil; - - [NSFileCoordinator addFilePresenter:self]; - - self.device = device; - self.url = url.absoluteURL; - - [self _setupGCDObserving]; - - BOOL success = [self _loadDataError:error]; - if (!success) return nil; - - return self; -} - -- (id)initWithData:(NSData *)data sourceURL:(NSURL*)url device:(id)device error:(NSError *__autoreleasing *)error __attribute__((nonnull(1))) -{ - NSParameterAssert(data); - - if (!(self = [super init])) return nil; - - self.device = device; - self.url = url.absoluteURL; - - BOOL success = [self _loadWithData:data error:error]; - if (!success) return nil; - - return self; -} - -- (void)unload; -{ - self.url = nil; -} - -- (void)_setupGCDObserving; -{ - // Inspired by http://www.davidhamrick.com/2011/10/13/Monitoring-Files-With-GCD-Being-Edited-With-A-Text-Editor.html because Photoshop follows the same annoying pattern. - int fileHandle = open(self.url.path.fileSystemRepresentation, O_EVTONLY); - - __block __weak id weakSelf = self; - __block dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, fileHandle, DISPATCH_VNODE_DELETE | DISPATCH_VNODE_WRITE | DISPATCH_VNODE_EXTEND | DISPATCH_VNODE_ATTRIB | DISPATCH_VNODE_LINK | DISPATCH_VNODE_RENAME | DISPATCH_VNODE_REVOKE, dispatch_get_main_queue()); - dispatch_source_set_event_handler(source, ^(){ - __strong id self = weakSelf; - if (dispatch_source_get_data(source)) - { - dispatch_source_cancel(source); - [self _setupGCDObserving]; - } - [self _loadDataError:NULL]; - }); - dispatch_source_set_cancel_handler(source, ^(){ - close(fileHandle); - }); - dispatch_resume(source); -} - -#pragma mark - File Presenter - -- (NSURL *)presentedItemURL -{ - return self.url; -} - -- (NSOperationQueue *)presentedItemOperationQueue -{ - return imageInformationQueue; -} - -- (void)accommodatePresentedItemDeletionWithCompletionHandler:(void (^)(NSError *errorOrNil))completionHandler -{ - dispatch_async(dispatch_get_main_queue(), ^{ - [self _loadDefaultTexture]; - [[NSNotificationCenter defaultCenter] postNotificationName:GLLDrawStateChangedNotification object:self]; - }); - - completionHandler(nil); -} - -- (void)presentedItemDidChange -{ - dispatch_async(dispatch_get_main_queue(), ^{ - BOOL success = [self _loadDataError:NULL]; - if (!success) - { - // Load default - [self _loadDefaultTexture]; - } - [[NSNotificationCenter defaultCenter] postNotificationName:GLLDrawStateChangedNotification object:self]; - }); -} - -- (void)presentedItemDidMoveToURL:(NSURL *)newURL -{ - self.url = newURL; -} - -#pragma mark - Private methods - -- (BOOL)_loadDataError:(NSError *__autoreleasing*)error; -{ - NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:self]; - - __block NSError *internalError = nil; - NSError *coordinationError; - [coordinator coordinateReadingItemAtURL:self.url options:NSFileCoordinatorReadingResolvesSymbolicLink error:&coordinationError byAccessor:^(NSURL *newURL){ - - if (!newURL) - { - [self _loadDefaultTexture]; - return; - } - - GLLBeginTiming("texture"); - NSData *data = [NSData dataWithContentsOfURL:newURL options:0 error:&internalError]; - - BOOL result = [self _loadWithData:data error:&internalError]; - if (!result) { - NSLog(@"Error loading texture %@: %@", self.url, internalError); - } - - GLLEndTiming("texture"); - }]; - - if (coordinationError) - { - if (error) *error = coordinationError; - return NO; - } - else if (internalError) - { - if (error) *error = internalError; - return NO; - } - else return YES; -} - -- (BOOL)_loadWithData:(NSData *)data error:(NSError *__autoreleasing*)error; -{ - // Ensure that memcmp does not error out. - if (data.length < 4) return NO; - - // Load texture - - BOOL result = YES; - if (memcmp(data.bytes, "DDS ", 4) == 0) - result = [self _loadDDSTextureWithData:data error:error]; - else - result = [self _loadCGCompatibleTexture:data error:error]; - - dispatch_async(dispatch_get_main_queue(), ^() { - [[NSNotificationCenter defaultCenter] postNotificationName:GLLTextureChangeNotification object:self]; - [[NSNotificationCenter defaultCenter] postNotificationName:GLLDrawStateChangedNotification object:self]; - }); - - return result; -} - -- (BOOL)_loadDDSTextureWithData:(NSData *)data error:(NSError *__autoreleasing*)error; -{ - NSError *ddsLoadingError = nil; - GLLDDSFile *file = [[GLLDDSFile alloc] initWithData:data error:&ddsLoadingError]; - if (!file) - { - if (error) - *error = [NSError errorWithDomain:@"Textures" code:12 userInfo:@{ - NSLocalizedDescriptionKey : [NSString stringWithFormat:NSLocalizedString(@"DDS File %@ couldn't be opened: %@", @"DDSOpenData returned NULL"), self.url.lastPathComponent, ddsLoadingError.userInfo[NSLocalizedDescriptionKey]], - NSLocalizedRecoverySuggestionErrorKey : ddsLoadingError.userInfo[NSLocalizedRecoverySuggestionErrorKey], - NSUnderlyingErrorKey : ddsLoadingError - }]; - return NO; - } - - self.height = file.height; - self.width = file.width; - - // Find pixel format - MTLTextureDescriptor *descriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:file.width height:file.height mipmapped:file.hasMipmaps]; - if (self.device.hasUnifiedMemory) { - descriptor.storageMode = MTLStorageModeShared; - } - - if (file.numMipmaps != 0) { - if (file.numMipmaps != (NSInteger) descriptor.mipmapLevelCount) { - NSLog(@"Unexpectedly few mipmaps on %@", self.url.lastPathComponent); - } - descriptor.mipmapLevelCount = file.numMipmaps; - } else { - descriptor.mipmapLevelCount = 1; - } - - BOOL expand24BitFormat = NO; - switch (file.dataFormat) { - case GLLDDSDataFormatDxt1: - descriptor.pixelFormat = MTLPixelFormatBC1_RGBA; - break; - case GLLDDSDataFormatDxt3: - descriptor.pixelFormat = MTLPixelFormatBC2_RGBA; - break; - case GLLDDSDataFormatDxt5: - descriptor.pixelFormat = MTLPixelFormatBC3_RGBA; - break; - case GLLDDSDataFormatBgr8: - descriptor.pixelFormat = MTLPixelFormatBGRA8Unorm; - expand24BitFormat = YES; - break; - case GLLDDSDataFormatBgra8: - descriptor.pixelFormat = MTLPixelFormatBGRA8Unorm; - break; - case GLLDDSDataFormatRgba8: - descriptor.pixelFormat = MTLPixelFormatRGBA8Unorm; - break; - case GLLDDSDataFormatArgb4: - // TODO Does this need swizzling? Probably, right? - descriptor.pixelFormat = MTLPixelFormatABGR4Unorm; - MTLTextureSwizzleChannels channels = { - .red = MTLTextureSwizzleGreen, - .green = MTLTextureSwizzleRed, - .blue = MTLTextureSwizzleAlpha, - .alpha = MTLTextureSwizzleBlue, - }; - descriptor.swizzle = channels; - break; - case GLLDDSDataFormatRgb565: - descriptor.pixelFormat = MTLPixelFormatB5G6R5Unorm; - break; - case GLLDDSDataFormatArgb1555: - descriptor.pixelFormat = MTLPixelFormatBGR5A1Unorm; - break; - case GLLDDSDataFormatBgrx8: - descriptor.pixelFormat = MTLPixelFormatBGRA8Unorm; - break; - default: - if (error) - *error = [NSError errorWithDomain:@"Textures" code:12 userInfo:@{ - NSLocalizedDescriptionKey : [NSString stringWithFormat:NSLocalizedString(@"DDS File %@ couldn't be opened: Pixel format is not supported", @"Can't find pixel format"), self.url.lastPathComponent] - }]; - return NO; - } - - _texture = [self.device newTextureWithDescriptor:descriptor]; - _texture.label = self.url.lastPathComponent; - - for (NSUInteger i = 0; i < descriptor.mipmapLevelCount; i++) { - NSUInteger levelWidth = MAX(self.width >> i, 1); - NSUInteger levelHeight = MAX(self.height >> i, 1); - - MTLRegion region = MTLRegionMake2D(0, 0, levelWidth, levelHeight); - - NSData *data = [file dataWithMipmapLevel:i]; - if (expand24BitFormat) { - // Metal does not support 24 bit texture formats, so we need to expand this data manually. - // Grr - NSUInteger pixels = levelWidth * levelHeight; - const uint8_t *originalBytes = data.bytes; - uint8_t *resizedData = calloc(sizeof(uint8_t [4]), pixels); - for (NSUInteger i = 0; i < pixels; i++) { - resizedData[i*4 + 0] = originalBytes[i*3 + 0]; - resizedData[i*4 + 1] = originalBytes[i*3 + 1]; - resizedData[i*4 + 2] = originalBytes[i*3 + 2]; - resizedData[i*4 + 3] = 0xFF; - } - [_texture replaceRegion:region mipmapLevel:i withBytes:resizedData bytesPerRow:levelWidth * 4]; - free(resizedData); - } else { - NSUInteger bytesPerRow = data.length / levelHeight; - if (descriptor.pixelFormat == MTLPixelFormatBC1_RGBA) { - NSUInteger blocksPerRow = MAX(1, levelWidth / 4); - NSUInteger blockSize = 8; - bytesPerRow = blocksPerRow * blockSize; - } else if (descriptor.pixelFormat == MTLPixelFormatBC2_RGBA || descriptor.pixelFormat == MTLPixelFormatBC3_RGBA) { - NSUInteger blocksPerRow = MAX(1, levelWidth / 4); - NSUInteger blockSize = 16; - bytesPerRow = blocksPerRow * blockSize; - } - [_texture replaceRegion:region mipmapLevel:i withBytes:data.bytes bytesPerRow:bytesPerRow]; - } - } - - return YES; -} -- (BOOL)_loadCGCompatibleTexture:(NSData *)data error:(NSError *__autoreleasing*)error; -{ - CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef) data, NULL); - CGImageSourceStatus status = CGImageSourceGetStatus(source); - NSString *errorStringFormat = nil; - switch (status) { - case kCGImageStatusUnexpectedEOF: - case kCGImageStatusReadingHeader: - case kCGImageStatusIncomplete: - errorStringFormat = NSLocalizedString(@"Texture file %@ could not be loaded due to unexpected file.", @"texture status unexpectedEOF"); - break; - case kCGImageStatusInvalidData: - errorStringFormat = NSLocalizedString(@"Texture file %@ could not be loaded because the data is invalid.", @"texture status invalidData"); - break; - case kCGImageStatusUnknownType: - errorStringFormat = NSLocalizedString(@"Texture file %@ could not be loaded because the type is not supported.", @"texture status unknownType"); - break; - case kCGImageStatusComplete: - // All good - break; - } - if (errorStringFormat) { - if (error) - *error = [NSError errorWithDomain:@"Textures" code:13 userInfo:@{ - NSLocalizedDescriptionKey : [NSString stringWithFormat:errorStringFormat, self.url.lastPathComponent] - }]; - [self _loadDefaultTexture]; - CFRelease(source); - return NO; - } - - NSString *sourceType = (__bridge NSString*) CGImageSourceGetType(source); - if ([sourceType isEqual:UTTypePDF.identifier]) { - BOOL result = [self _loadPDFTextureWithData:data error:error]; - CFRelease(source); - return result; - } - - CFDictionaryRef dict = CGImageSourceCopyPropertiesAtIndex(source, 0, NULL); - if (!dict) { - if (error) - *error = [NSError errorWithDomain:@"Textures" code:13 userInfo:@{ - NSLocalizedDescriptionKey : [NSString stringWithFormat:NSLocalizedString(@"Texture file %@ could not be loaded because the properties could not be loaded.", @"texture status probably a PDF"), self.url.lastPathComponent] - }]; - [self _loadDefaultTexture]; - CFRelease(source); - return NO; - } - - CFIndex width, height; - CFNumberGetValue(CFDictionaryGetValue(dict, kCGImagePropertyPixelWidth), kCFNumberCFIndexType, &width); - CFNumberGetValue(CFDictionaryGetValue(dict, kCGImagePropertyPixelHeight), kCFNumberCFIndexType, &height); - CFRelease(dict); - - self.height = height; - self.width = width; - - CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - vImage_Buffer buffer = { .height = 0, .width = 0, .rowBytes = 0, .data = 0}; - vImage_CGImageFormat format = { - .version = 0, - .decode = 0, - .bitsPerPixel = 32, - .bitsPerComponent = 8, - .bitmapInfo = kCGImageAlphaFirst | kCGBitmapByteOrderDefault, - .colorSpace = colorSpace - }; - CGFloat backgroundColor[] = { 0, 0, 0, 0 }; - - CGImageRef cgImage = CGImageSourceCreateImageAtIndex(source, 0, NULL); - CFRelease(source); - - vImage_Error result = vImageBuffer_InitWithCGImage(&buffer, &format, backgroundColor, cgImage, kvImageNoFlags); - CGImageRelease(cgImage); - if (result != kvImageNoError) { - if (error) - *error = [NSError errorWithDomain:@"Textures" code:13 userInfo:@{ - NSLocalizedDescriptionKey : [NSString stringWithFormat:NSLocalizedString(@"Texture file %@ could not be loaded because the properties could not be loaded.", @"texture status probably a PDF"), self.url.lastPathComponent] - }]; - [self _loadDefaultTexture]; - return NO; - } - - [self _loadAndFreeUnpremultipliedData:&buffer order:GLLTextureOrderARGB]; - - return YES; -} - -// Just for fun -- (BOOL)_loadPDFTextureWithData:(NSData *)data error:(NSError *__autoreleasing*)error { - CGDataProviderRef dataProvider = CGDataProviderCreateWithCFData((__bridge CFDataRef) data); - CGPDFDocumentRef document = CGPDFDocumentCreateWithProvider(dataProvider); - CGDataProviderRelease(dataProvider); - - if (!document) { - if (*error) - *error = [NSError errorWithDomain:@"Texture" code:14 userInfo:@{ NSLocalizedDescriptionKey : [NSString stringWithFormat:NSLocalizedString(@"PDF Texture file %@ could not be loaded.", @"texture status pdf not loaded"), self.url.lastPathComponent] }]; - [self _loadDefaultTexture]; - return NO; - } - - size_t numberOfPages = CGPDFDocumentGetNumberOfPages(document); - if (numberOfPages == 0) { - if (*error) - *error = [NSError errorWithDomain:@"Texture" code:14 userInfo:@{ NSLocalizedDescriptionKey : [NSString stringWithFormat:NSLocalizedString(@"PDF Texture file %@ has no pages.", @"texture status pdf no pages"), self.url.lastPathComponent] }]; - [self _loadDefaultTexture]; - CGPDFDocumentRelease(document); - return NO; - } - - CGPDFPageRef page = CGPDFDocumentGetPage(document, 1); - if (!page) { - if (*error) - *error = [NSError errorWithDomain:@"Texture" code:14 userInfo:@{ NSLocalizedDescriptionKey : [NSString stringWithFormat:NSLocalizedString(@"Could not load first page of PDF file %@.", @"texture status pdf no pages"), self.url.lastPathComponent] }]; - [self _loadDefaultTexture]; - CGPDFDocumentRelease(document); - return NO; - } - - // Find user unit, if any - CGPDFReal userUnit = 1.0; - CGPDFDictionaryRef pageDictionary = CGPDFPageGetDictionary(page); - if (pageDictionary) { - if (!CGPDFDictionaryGetNumber(pageDictionary, "UserUnit", &userUnit)) - userUnit = 1.0; - } - // Unit is userUnit / 72 inch. We want 300 DPI. - CGFloat scale = (userUnit / 72.0) * 300.0; - - // Limit size - const CGFloat maxSize = 2048.0; - CGRect boxRect = CGPDFPageGetBoxRect(page, kCGPDFCropBox); - if (boxRect.size.width * scale > maxSize) - scale = maxSize / boxRect.size.width; - if (boxRect.size.height * scale > maxSize) - scale = maxSize / boxRect.size.height; - - // TODO Should go via actual resolution, as far as it is specified in the - // PDF; and maybe limit max size, too. - self.height = (NSUInteger) (boxRect.size.width * scale); - self.width = (NSUInteger) (boxRect.size.height * scale); - - unsigned char *bufferData = calloc(self.width * self.height, 4); - - CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGContextRef cgContext = CGBitmapContextCreate(bufferData, self.width, self.height, 8, self.width * 4, colorSpace, kCGImageAlphaPremultipliedFirst); - NSAssert(cgContext != NULL, @"Could not create CG Context"); - - CGColorSpaceRelease(colorSpace); - - CGContextScaleCTM(cgContext, scale, scale); - - CGContextDrawPDFPage(cgContext, page); - CGContextRelease(cgContext); - CGPDFDocumentRelease(document); - - [self _loadAndFreePremultipliedARGBData:bufferData]; - return YES; -} - -- (void)_loadAndFreePremultipliedARGBData:(void *)bufferData; { - // Unpremultiply the texture data. I wish I could get it unpremultiplied from the start, but CGImage doesn't allow that. Just using premultiplied sounds swell, but it messes up my blending in OpenGL. - unsigned char *unpremultipliedBufferData = calloc(self.width * self.height, 4); - vImage_Buffer input = { .height = self.height, .width = self.width, .rowBytes = 4*self.width, .data = bufferData }; - vImage_Buffer output = { .height = self.height, .width = self.width, .rowBytes = 4*self.width, .data = unpremultipliedBufferData }; - vImageUnpremultiplyData_ARGB8888(&input, &output, 0); - free(bufferData); - - [self _loadAndFreeUnpremultipliedData:&output order:GLLTextureOrderARGB]; -} - -- (void)_loadAndFreeUnpremultipliedData:(vImage_Buffer *)unpremultipliedBufferData order:(enum GLLTextureOrder)order; { - int numberOfLevels = numMipmapLevels(self.width, self.height); - - MTLTextureDescriptor* descriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm width:self.width height:self.height mipmapped:YES]; - if (self.device.hasUnifiedMemory) { - descriptor.storageMode = MTLStorageModeShared; - } - if (order == GLLTextureOrderARGB) { - // Metal does not support any alpha-first formats, and we need ARGB for the unpremultiply to work, so swizzle - // A -> B - // R -> G - // G -> R - // B -> A - MTLTextureSwizzleChannels channels = { - .alpha = MTLTextureSwizzleBlue, - .red = MTLTextureSwizzleGreen, - .green = MTLTextureSwizzleRed, - .blue = MTLTextureSwizzleAlpha - }; - descriptor.swizzle = channels; - } - _texture = [_device newTextureWithDescriptor:descriptor]; - _texture.label = self.url.lastPathComponent; - - MTLRegion region = MTLRegionMake2D(0, 0, self.width, self.height); - [_texture replaceRegion:region mipmapLevel:0 withBytes:unpremultipliedBufferData->data bytesPerRow:unpremultipliedBufferData->rowBytes]; - - // Load mipmaps - vImage_Buffer lastBuffer = *unpremultipliedBufferData; - uint8_t *tempBuffer = NULL; - size_t tempBufferSize = 0; - for (int i = 1; i < numberOfLevels; i++) { - vImage_Buffer smallerBuffer; - smallerBuffer.width = MAX(self.width >> i, 1UL); - smallerBuffer.height = MAX(self.height >> i, 1UL); - smallerBuffer.rowBytes = smallerBuffer.width * 4; - smallerBuffer.data = calloc(smallerBuffer.height * smallerBuffer.width, 4); - - size_t newTempSize = vImageScale_ARGB8888(&lastBuffer, &smallerBuffer, 0, kvImageGetTempBufferSize); - if (newTempSize > tempBufferSize) { - tempBufferSize = newTempSize; - free(tempBuffer); - tempBuffer = malloc(newTempSize); - } - - vImageScale_ARGB8888(&lastBuffer, &smallerBuffer, tempBuffer, kvImageEdgeExtend); - free(lastBuffer.data); - - MTLRegion region = MTLRegionMake2D(0, 0, smallerBuffer.width, smallerBuffer.height); - [_texture replaceRegion:region mipmapLevel:i withBytes:smallerBuffer.data bytesPerRow:smallerBuffer.rowBytes]; - - lastBuffer = smallerBuffer; - } - free(tempBuffer); - free(lastBuffer.data); -} - -- (void)_loadDefaultTexture; -{ - const uint8_t defaultTexture[16] = { - 255, 255, 255, 255, - 255, 0, 0, 255, - 255, 0, 0, 255, - 255, 255, 255, 255 - }; - self.width = 2; - self.height = 2; - - MTLTextureDescriptor *descriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:self.width height:self.height mipmapped:YES]; - - _texture = [self.device newTextureWithDescriptor:descriptor]; - _texture.label = @"default-texture"; - MTLRegion region = MTLRegionMake2D(0, 0, self.width, self.height); - - [self.texture replaceRegion:region mipmapLevel:0 withBytes:defaultTexture bytesPerRow:2*4]; - - const uint8_t defaultTextureSmall[4] = { - 255, 128, 128, 255 - }; - MTLRegion regionLevel1 = MTLRegionMake2D(0, 0, 1, 1); - [self.texture replaceRegion:regionLevel1 mipmapLevel:1 withBytes:defaultTextureSmall bytesPerRow:4]; -} - -@end diff --git a/GLLara/GLLTexture.swift b/GLLara/GLLTexture.swift new file mode 100644 index 0000000..9aa61f6 --- /dev/null +++ b/GLLara/GLLTexture.swift @@ -0,0 +1,412 @@ +// +// GLLTexture.swift +// GLLara +// +// Created by Torsten Kammer on 04.05.24. +// Copyright © 2024 Torsten Kammer. All rights reserved. +// + +import Foundation +import Metal +import CoreGraphics +import UniformTypeIdentifiers +import System + +@objc class GLLTexture: NSObject, NSFilePresenter { + + private static let informationQueue: OperationQueue = { + let queue = OperationQueue() + queue.maxConcurrentOperationCount = 1 + return queue + }() + + static let changeNotification = "GLL Texture Change Notification" + + @objc var width: Int = 0 + @objc var height: Int = 0 + var device: MTLDevice + var url: URL + var texture: MTLTexture! = nil + + init(url: URL, device: MTLDevice) throws { + self.url = url + self.device = device + + super.init() + + NSFileCoordinator.addFilePresenter(self) + setupGCDObserving() + try loadFile() + } + + /** + * @abstract Load from data (assuming this is part of some other file) + * @discussion Intended in particular for glTF (binary glTF and data URIs in it), + * where the file may start sort of randomly, and where updating the texture + * independent of the model is not possible anyway. + */ + init(data: Data, sourceURL: URL, device: MTLDevice) throws { + self.url = sourceURL + self.device = device + + super.init() + + NSFileCoordinator.addFilePresenter(self) + setupGCDObserving() + try loadData(data: data) + } + + /// We need this to observe low-level changes that don't go through an NSFilePresenter + private func setupGCDObserving() { + guard let path = FilePath(url) else { + return + } + guard let filehandle = try? FileDescriptor.open(path, .readOnly, options: [.eventOnly]) else { + return + } + + let dispatchSource = DispatchSource.makeFileSystemObjectSource(fileDescriptor: filehandle.rawValue, eventMask: [.delete, .write, .extend, .attrib, .link, .rename, .revoke], queue: DispatchQueue.main) + dispatchSource.setEventHandler { [weak self] in + guard let self else { + return + } + dispatchSource.cancel() + setupGCDObserving() + do { + try loadFile() + } catch let error as NSError { + print("Error reloading file \(error)") + } + } + dispatchSource.setCancelHandler { try? filehandle.close() } + dispatchSource.resume() + } + + private func loadFile() throws { + let coordinator = NSFileCoordinator(filePresenter: self) + var coordinationError: NSError? = nil + var internalError: NSError? = nil + coordinator.coordinate(readingItemAt: url, options: [.resolvesSymbolicLink], error: &coordinationError) { newUrl in + do { + let data = try Data(contentsOf: newUrl) + try loadData(data: data) + } catch let error as NSError { + internalError = error + } + } + if let coordinationError { + throw coordinationError + } + if let internalError { + throw internalError + } + } + + private func loadData(data: Data) throws { + if data.count < 4 { + throw NSError(domain: "Textures", code: 12, userInfo: [ + NSLocalizedDescriptionKey: String(format: NSLocalizedString("Texture file %@ couldn't be opened because it is too short.", comment: "Data count smaller 4"), url.lastPathComponent) + ]) + } + + if data[0] == Character("D").asciiValue! && data[1] == Character("D").asciiValue! && data[2] == Character("S").asciiValue! && data[3] == Character(" ").asciiValue! { + try loadDDSTexture(data: data) + } else { + try loadCGCompatibleTexture(data: data) + } + + DispatchQueue.main.async { + NotificationCenter.default.post(name: Notification.Name(GLLTexture.changeNotification), object: self) + } + } + + func loadDDSTexture(data: Data) throws { + do { + let ddsFile = try GLLDDSFile(data: data) + + height = ddsFile.height + width = ddsFile.width + let descriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .rgba8Unorm, width: width, height: height, mipmapped: ddsFile.hasMipmaps) + if device.hasUnifiedMemory { + descriptor.storageMode = .shared + } + + if ddsFile.numMipmaps != 0 { + if ddsFile.numMipmaps != descriptor.mipmapLevelCount { + print("Unexpectedly few mipmaps in \(url)") + } + descriptor.mipmapLevelCount = ddsFile.numMipmaps + } else { + descriptor.mipmapLevelCount = 1 + } + + var expand24BitFormat = false + switch ddsFile.dataFormat { + case .dxt1: + descriptor.pixelFormat = .bc1_rgba + case .dxt3: + descriptor.pixelFormat = .bc2_rgba + case .dxt5: + descriptor.pixelFormat = .bc3_rgba + case .bgr8: + descriptor.pixelFormat = .bgra8Unorm + expand24BitFormat = true + case .bgra8: + descriptor.pixelFormat = .bgra8Unorm + case .rgba8: + descriptor.pixelFormat = .rgba8Unorm + break; + case .argb4: + // TODO Does this need swizzling? Probably, right? + descriptor.pixelFormat = .abgr4Unorm + descriptor.swizzle = MTLTextureSwizzleChannels(red: .green, green: .red, blue: .alpha, alpha: .blue) + case .rgb565: + descriptor.pixelFormat = .b5g6r5Unorm + break; + case .argb1555: + descriptor.pixelFormat = .bgr5A1Unorm + break; + case .bgrx8: + descriptor.pixelFormat = .bgra8Unorm + default: + throw NSError(domain:"Textures", code:12, userInfo:[ + NSLocalizedDescriptionKey : String(format:NSLocalizedString("DDS File %@ couldn't be opened: Pixel format is not supported", comment: "Can't find pixel format"), self.url.lastPathComponent) + ]); + } + + texture = device.makeTexture(descriptor: descriptor) + texture.label = self.url.lastPathComponent + + for i in 0 ..< descriptor.mipmapLevelCount { + let levelWidth = max(width >> i, 1) + let levelHeight = max(height >> i, 1) + let region = MTLRegionMake2D(0, 0, levelWidth, levelHeight) + + guard let data = ddsFile.data(mipmapLevel: i) else { + throw NSError(domain:"Textures", code:12, userInfo:[ + NSLocalizedDescriptionKey : String(format:NSLocalizedString("DDS File %@ couldn't be opened: No data for mipmap level %ld", comment: "Can't find load mipmap level"), self.url.lastPathComponent, i) + ]); + } + if expand24BitFormat { + // Metal does not support 24 bit texture formats, so we need to expand this data manually. + // Grr + let pixels = levelWidth * levelHeight; + var resizedData = Array(repeating: 0, count: pixels * 4) + for i in 0 ..< pixels { + resizedData[i*4 + 0] = data[i*3 + 0] + resizedData[i*4 + 1] = data[i*3 + 1] + resizedData[i*4 + 2] = data[i*3 + 2] + resizedData[i*4 + 3] = 0xFF + } + resizedData.withUnsafeBytes { bytes in + texture.replace(region: region, mipmapLevel: i, withBytes: bytes.baseAddress!, bytesPerRow: levelWidth * 4) + } + } else { + var bytesPerRow = data.count / levelHeight + if descriptor.pixelFormat == .bc1_rgba { + let blocksPerRow = max(1, levelWidth/4) + let blockSize = 8 + bytesPerRow = blocksPerRow * blockSize + } else if descriptor.pixelFormat == .bc2_rgba || descriptor.pixelFormat == .bc3_rgba { + let blocksPerRow = max(1, levelWidth/4) + let blockSize = 16 + bytesPerRow = blocksPerRow * blockSize + } + + data.withUnsafeBytes { bytes in + texture.replace(region: region, mipmapLevel: i, withBytes: bytes.baseAddress!, bytesPerRow: bytesPerRow) + } + } + } + + } catch let error as NSError { + // Nicer error-message + throw NSError(domain: "Textures", code: 12, userInfo: [ + NSLocalizedDescriptionKey: String(format: NSLocalizedString("DDS File %@ couldn't be opened: %@", comment: "DDSOpenData returned NULL"), self.url.lastPathComponent, error.localizedDescription), + NSLocalizedRecoverySuggestionErrorKey: error.localizedRecoverySuggestion ?? "" + ]) + } + } + + private func loadCGCompatibleTexture(data: Data) throws { + let source = CGImageSourceCreateWithData(data as CFData, nil)! + let status = CGImageSourceGetStatus(source) + switch status { + case .statusUnexpectedEOF: + throw textureError(description: NSLocalizedString("Texture file %@ could not be loaded due to unexpected file.", comment: "texture status unexpectedEOF")) + case .statusInvalidData: + throw textureError(description: NSLocalizedString("Texture file %@ could not be loaded because the data is invalid.", comment: "texture status invalidData")) + case .statusUnknownType: + throw textureError(description: NSLocalizedString("Texture file %@ could not be loaded because the type is not supported.", comment: "texture status unknownType")) + case .statusReadingHeader: + throw textureError(description: NSLocalizedString("Texture file %@ could not be loaded due to unexpected file.", comment: "texture status unexpectedEOF")) + case .statusIncomplete: + throw textureError(description: NSLocalizedString("Texture file %@ could not be loaded due to unexpected file.", comment: "texture status unexpectedEOF")) + case .statusComplete: + // All good + break + @unknown default: + throw textureError(description: NSLocalizedString("Texture file %@ could not be loaded due to an unexpected status.", comment: "texture status unknown cgimagesource status")) + } + + let sourceType = CGImageSourceGetType(source) + if sourceType as? String == UTType.pdf.identifier { + try loadPdfTexture(data: data) + return + } + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let image = CGImageSourceCreateImageAtIndex(source, 0, nil)! + let format = vImage_CGImageFormat(bitsPerComponent: 8, bitsPerPixel: 32, colorSpace: colorSpace, bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.first.rawValue | CGBitmapInfo.byteOrderDefault.rawValue))! + var buffer = try vImage_Buffer(cgImage: image, format: format) + height = Int(buffer.height) + width = Int(buffer.width) + + try loadAndFree(unpremultipliedARGB: &buffer) + } + + /// Just for fun + private func loadPdfTexture(data: Data) throws { + guard let dataProvider = CGDataProvider(data: data as CFData), let document = CGPDFDocument(dataProvider) else { + throw textureError(description: NSLocalizedString("PDF Texture file %@ could not be loaded.", comment: "texture status pdf not loaded")) + } + + let numberOfPages = document.numberOfPages + if numberOfPages == 0 { + throw textureError(description: NSLocalizedString("PDF Texture file %@ has no pages.", comment: "texture status pdf no pages")) + } + + // PDF pages start at 1 + guard let page = document.page(at: 1) else { + throw textureError(description: NSLocalizedString("Could not load first page of PDF file %@.", comment: "texture status pdf no pages")) + } + + // Find user unit, if any + var userUnit: CGPDFReal = 1.0 + if let pageDictionary = page.dictionary { + if !withUnsafeMutablePointer(to: &userUnit, { bytes in + CGPDFDictionaryGetNumber(pageDictionary, "UserUnit", bytes) + }) { + userUnit = 1.0 + } + } + // Unit is userUnit / 72 inch. We want 300 DPI. + var scale: CGFloat = (userUnit / 72.0) * 300.0; + + // Limit size + let maxSize: CGFloat = 2048 + let boxRect = page.getBoxRect(.cropBox) + if boxRect.size.width * scale > maxSize { + scale = maxSize / boxRect.size.width + } + if boxRect.size.height * scale > maxSize { + scale = maxSize / boxRect.size.height + } + + width = Int(boxRect.size.width * scale) + height = Int(boxRect.size.height * scale) + + var buffer = try vImage_Buffer(width: width, height: height, bitsPerPixel: 32) + let colorSpace = CGColorSpaceCreateDeviceRGB() + let context = CGContext(data: buffer.data, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width * 4, space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue)! + context.scaleBy(x: scale, y: scale) + context.drawPDFPage(page) + + try loadAndFree(premultipliedARGB: &buffer) + } + + private func loadAndFree(premultipliedARGB inputBuffer: inout vImage_Buffer) throws { + // Unpremultiply the texture data. I wish I could get it unpremultiplied from the start, but CGImage doesn't allow that. Just using premultiplied sounds swell, but it messes up my blending in OpenGL. + + // Copy of buffer does not copy allocation (I think) + var outputBuffer = try vImage_Buffer(width: width, height: height, bitsPerPixel: 32) + vImageUnpremultiplyData_ARGB8888(&inputBuffer, &outputBuffer, 0) + inputBuffer.free() + + try loadAndFree(unpremultipliedARGB: &outputBuffer) + } + + private var numMipmapLevels: Int { + let rulingDimension = max(width, height) + let firstBit = flsl(rulingDimension) // Computes floor(log2(x)). We want ceil(log2(x)) + if (rulingDimension & ~(1 << firstBit)) == 0 { + return Int(firstBit - 1) + } + return Int(firstBit) + } + + private func loadAndFree(unpremultipliedARGB inputBuffer: inout vImage_Buffer) throws { + let descriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .bgra8Unorm, width: width, height: height, mipmapped: true) + if device.hasUnifiedMemory { + descriptor.storageMode = .shared + } + // Metal does not support any alpha-first formats, and we need ARGB for the other steps to work, so swizzle + // A -> B + // R -> G + // G -> R + // B -> A + descriptor.swizzle = MTLTextureSwizzleChannels(red: .green, green: .red, blue: .alpha, alpha: .blue) + texture = device.makeTexture(descriptor: descriptor) + texture.label = url.lastPathComponent + + let region = MTLRegionMake2D(0, 0, width, height) + texture.replace(region: region, mipmapLevel: 0, withBytes: inputBuffer.data, bytesPerRow: inputBuffer.rowBytes) + + // Load mipmaps + var lastBuffer = inputBuffer + var tempBuffer = UnsafeMutableRawBufferPointer.allocate(byteCount: width * height * 4, alignment: 1024) + for i in 1 ..< numMipmapLevels { + var smallerBuffer = try vImage_Buffer(width: max(width >> i, 1), height: max(height >> i, 1), bitsPerPixel: 32) + let minTempSize = vImageScale_ARGB8888(&lastBuffer, &smallerBuffer, nil, vImage_Flags(kvImageGetTempBufferSize)) + if minTempSize > tempBuffer.count { + tempBuffer.deallocate() + tempBuffer = UnsafeMutableRawBufferPointer.allocate(byteCount: minTempSize, alignment: 1024) + } + + vImageScale_ARGB8888(&lastBuffer, &smallerBuffer, tempBuffer.baseAddress, vImage_Flags(kvImageEdgeExtend)) + lastBuffer.free() + + let region = MTLRegionMake2D(0, 0, Int(smallerBuffer.width), Int(smallerBuffer.height)) + texture.replace(region: region, mipmapLevel: i, withBytes: smallerBuffer.data, bytesPerRow: smallerBuffer.rowBytes) + + lastBuffer = smallerBuffer + } + tempBuffer.deallocate() + lastBuffer.free() + } + + private func textureError(description: String, recoverySuggestion: String? = nil) -> NSError { + var userInfo: [String: Any] = [ + NSLocalizedDescriptionKey: String(format: description, url.lastPathComponent) + ] + if let recoverySuggestion { + userInfo[NSLocalizedRecoverySuggestionErrorKey] = recoverySuggestion + } + return NSError(domain: "Textures", code: 13, userInfo: userInfo) + } + + // MARK: - File presenter + var presentedItemOperationQueue: OperationQueue { + return GLLTexture.informationQueue + } + + var presentedItemURL: URL? { + return url + } + + func presentedItemDidMove(to newURL: URL) { + url = newURL + } + + func presentedItemDidChange() { + DispatchQueue.main.async { + do { + try self.loadFile() + } catch let error as NSError { + print("Error with changed file: \(error)") + } + } + } + + +} diff --git a/GLLara/GLLVertexAttrib.swift b/GLLara/GLLVertexAttrib.swift index d1eeaf2..3772c6d 100644 --- a/GLLara/GLLVertexAttrib.swift +++ b/GLLara/GLLVertexAttrib.swift @@ -126,6 +126,10 @@ struct GLLVertexAttrib: Hashable, Comparable { return 2 case .half: return 2 + case .floatRG11B10: + return 4 + case .floatRGB9E5: + return 4 @unknown default: return 0 } diff --git a/GLLara/GLLView.swift b/GLLara/GLLView.swift index a326a8b..79a597a 100644 --- a/GLLara/GLLView.swift +++ b/GLLara/GLLView.swift @@ -22,7 +22,7 @@ import Combine autoResizeDrawable = true sampleCount = 1 - notificationObservers.append(NotificationCenter.default.addObserver(forName: NSNotification.Name.GLLTextureChange, object: nil, queue: OperationQueue.main) { [weak self] notification in + notificationObservers.append(NotificationCenter.default.addObserver(forName: NSNotification.Name(GLLTexture.changeNotification), object: nil, queue: OperationQueue.main) { [weak self] notification in self?.unpause() }) notificationObservers.append(NotificationCenter.default.addObserver(forName: UserDefaults.didChangeNotification, object: nil, queue: OperationQueue.main) { [weak self] notification in diff --git a/GLLara/GLLara-Bridging-Header.h b/GLLara/GLLara-Bridging-Header.h index d1cee43..0485af8 100644 --- a/GLLara/GLLara-Bridging-Header.h +++ b/GLLara/GLLara-Bridging-Header.h @@ -22,7 +22,6 @@ #import "GLLRenderParameters.h" #import "GLLSelection.h" #import "GLLSkeletonDrawerVertexFormat.h" -#import "GLLTexture.h" #import "HUDShared.h" #import "NSColor+Color32Bit.h" #import "simd_matrix.h"