From e2803e727f81331bf037639afcbf8c058dc3487e Mon Sep 17 00:00:00 2001 From: dayu <506228475@qq.com> Date: Wed, 21 Oct 2026 17:12:45 +0800 Subject: [PATCH] WIP: add portrait FxPlug controls, render adapter and face selection OSC --- beatygril/beatygril.xcodeproj/project.pbxproj | 42 +- beatygril/beatygril/Plugin/Info.plist | 31 +- beatygril/beatygril/Plugin/MetalDeviceCache.h | 28 - beatygril/beatygril/Plugin/MetalDeviceCache.m | 339 ----------- beatygril/beatygril/Plugin/PGBeautyTypes.h | 30 + beatygril/beatygril/Plugin/PGFaceSelectOSC.m | 64 ++ beatygril/beatygril/Plugin/PGParameterIDs.h | 17 + .../Plugin/TileableRemoteBrightness.metal | 83 --- .../TileableRemoteBrightnessShaderTypes.h | 32 - beatygril/beatygril/Plugin/beatygrilPlugIn.h | 16 +- beatygril/beatygril/Plugin/beatygrilPlugIn.m | 572 +++++++++--------- scripts/checkpoint.py | 36 ++ 12 files changed, 476 insertions(+), 814 deletions(-) delete mode 100644 beatygril/beatygril/Plugin/MetalDeviceCache.h delete mode 100644 beatygril/beatygril/Plugin/MetalDeviceCache.m create mode 100644 beatygril/beatygril/Plugin/PGBeautyTypes.h create mode 100644 beatygril/beatygril/Plugin/PGFaceSelectOSC.m create mode 100644 beatygril/beatygril/Plugin/PGParameterIDs.h delete mode 100644 beatygril/beatygril/Plugin/TileableRemoteBrightness.metal delete mode 100644 beatygril/beatygril/Plugin/TileableRemoteBrightnessShaderTypes.h create mode 100644 scripts/checkpoint.py diff --git a/beatygril/beatygril.xcodeproj/project.pbxproj b/beatygril/beatygril.xcodeproj/project.pbxproj index d662172..d435b38 100644 --- a/beatygril/beatygril.xcodeproj/project.pbxproj +++ b/beatygril/beatygril.xcodeproj/project.pbxproj @@ -252,7 +252,7 @@ shellPath = /bin/sh; shellScript = ( "mkdir -p \"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}\"", - "rsync --archive --links --whole-file --no-owner --no-group --checksum --exclude='Headers' \"/Library/Developer/Frameworks/FxPlug.framework/\" \"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FxPlug.framework/\"", + "rsync --archive --links --whole-file --no-owner --no-group --checksum --exclude='Headers' \"/Library/Developer/SDKs/FxPlug.sdk/Library/Frameworks/FxPlug.framework/\" \"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FxPlug.framework/\"", "if [ \"$?\" -ne 0 ]; then echo \"error: rsync failed; couldn't embed FxPlug.framework\"; exit 1; fi", "codesign --force --timestamp --options runtime --sign \"${CODE_SIGN_IDENTITY}\" \"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/FxPlug.framework\"", ); @@ -264,7 +264,7 @@ shellPath = /bin/sh; shellScript = ( "mkdir -p \"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}\"", - "rsync --archive --links --whole-file --no-owner --no-group --checksum --exclude='Headers' \"/Library/Developer/Frameworks/PluginManager.framework/\" \"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PluginManager.framework/\"", + "rsync --archive --links --whole-file --no-owner --no-group --checksum --exclude='Headers' \"/Library/Developer/SDKs/FxPlug.sdk/Library/Frameworks/PluginManager.framework/\" \"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PluginManager.framework/\"", "if [ \"$?\" -ne 0 ]; then echo \"error: rsync failed; couldn't embed PluginManager.framework\"; exit 1; fi", "codesign --force --timestamp --options runtime --sign \"${CODE_SIGN_IDENTITY}\" \"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/PluginManager.framework\"", "", @@ -303,7 +303,9 @@ 6945FCCD3088B7220013E108 /* Debug configuration for PBXNativeTarget "XPC Service" */ = { isa = XCBuildConfiguration; buildSettings = { - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; + CODE_SIGN_IDENTITY = "Developer ID Application"; + ENABLE_HARDENED_RUNTIME = YES; DEVELOPMENT_TEAM = B3W25FX5H5; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_PRECOMPILE_PREFIX_HEADER = YES; @@ -313,7 +315,7 @@ "@loader_path/../Frameworks", ); MACH_O_TYPE = mh_execute; - PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_BUNDLE_IDENTIFIER = com.dayu.beatygril.fxplug; PRODUCT_NAME = "beatygril $(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "beatygril/Plugin/XPC Service-Bridging-Header.h"; WRAPPER_EXTENSION = pluginkit; @@ -323,7 +325,9 @@ 6945FCCE3088B7220013E108 /* Release configuration for PBXNativeTarget "XPC Service" */ = { isa = XCBuildConfiguration; buildSettings = { - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; + CODE_SIGN_IDENTITY = "Developer ID Application"; + ENABLE_HARDENED_RUNTIME = YES; DEVELOPMENT_TEAM = B3W25FX5H5; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_PRECOMPILE_PREFIX_HEADER = YES; @@ -333,7 +337,7 @@ "@loader_path/../Frameworks", ); MACH_O_TYPE = mh_execute; - PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_BUNDLE_IDENTIFIER = com.dayu.beatygril.fxplug; PRODUCT_NAME = "beatygril $(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "beatygril/Plugin/XPC Service-Bridging-Header.h"; WRAPPER_EXTENSION = pluginkit; @@ -344,12 +348,13 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_ENTITLEMENTS = "beatygril/Wrapper Application/SandboxEntitlements.entitlements"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = B3W25FX5H5; + CODE_SIGN_STYLE = Manual; + CODE_SIGN_IDENTITY = "Developer ID Application"; ENABLE_HARDENED_RUNTIME = YES; + DEVELOPMENT_TEAM = B3W25FX5H5; GCC_PRECOMPILE_PREFIX_HEADER = YES; INFOPLIST_FILE = "beatygril/Wrapper Application/Info.plist"; - PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_BUNDLE_IDENTIFIER = com.dayu.beatygril; PRODUCT_NAME = beatygril; SWIFT_OBJC_BRIDGING_HEADER = "beatygril/Wrapper Application/Wrapper Application-Bridging-Header.h"; }; @@ -359,12 +364,13 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_ENTITLEMENTS = "beatygril/Wrapper Application/SandboxEntitlements.entitlements"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = B3W25FX5H5; + CODE_SIGN_STYLE = Manual; + CODE_SIGN_IDENTITY = "Developer ID Application"; ENABLE_HARDENED_RUNTIME = YES; + DEVELOPMENT_TEAM = B3W25FX5H5; GCC_PRECOMPILE_PREFIX_HEADER = YES; INFOPLIST_FILE = "beatygril/Wrapper Application/Info.plist"; - PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_BUNDLE_IDENTIFIER = com.dayu.beatygril; PRODUCT_NAME = beatygril; SWIFT_OBJC_BRIDGING_HEADER = "beatygril/Wrapper Application/Wrapper Application-Bridging-Header.h"; }; @@ -380,7 +386,7 @@ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = NO; + CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; @@ -411,7 +417,7 @@ ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; FRAMEWORK_SEARCH_PATHS = ( - /Library/Frameworks, + /Library/Developer/SDKs/FxPlug.sdk/Library/Frameworks, "$(inherited)", ); GCC_C_LANGUAGE_STANDARD = gnu17; @@ -430,7 +436,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MACOSX_DEPLOYMENT_TARGET = 27.2; + MACOSX_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -449,7 +455,7 @@ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = NO; + CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; @@ -480,7 +486,7 @@ ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; FRAMEWORK_SEARCH_PATHS = ( - /Library/Frameworks, + /Library/Developer/SDKs/FxPlug.sdk/Library/Frameworks, "$(inherited)", ); GCC_C_LANGUAGE_STANDARD = gnu17; @@ -493,7 +499,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MACOSX_DEPLOYMENT_TARGET = 27.2; + MACOSX_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; diff --git a/beatygril/beatygril/Plugin/Info.plist b/beatygril/beatygril/Plugin/Info.plist index 64d0b7e..13a12ad 100644 --- a/beatygril/beatygril/Plugin/Info.plist +++ b/beatygril/beatygril/Plugin/Info.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - beatygrilXPCService + 大鱼人像美化 CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -48,7 +48,7 @@ groupName - beatygril Group + 大鱼 uuid 0461C6B0-B8B7-4DE8-B882-03C635553ECB @@ -59,20 +59,41 @@ className beatygrilPlugIn displayName - beatygril::FxPlug Name + 大鱼人像美化 group 0461C6B0-B8B7-4DE8-B882-03C635553ECB infoString - beatygril::FxPlug Description + 人像磨皮、肤色、脸型和人脸跟踪 protocolNames FxFilter + FxAnalyzer uuid FB14B0E6-59B2-4836-916F-CD0C559E9241 version 1.0 + + className + PGFaceSelectOSC + displayName + 人脸选择 + group + 0461C6B0-B8B7-4DE8-B882-03C635553ECB + uuid + E0E432C8-3D71-4ABA-B006-E7F519E06C87 + version + 1.0 + protocolNames + + FxOnScreenControl + + supportedPlugins + + FB14B0E6-59B2-4836-916F-CD0C559E9241 + + XPCService @@ -88,5 +109,7 @@ + LSMinimumSystemVersion + 12.0 diff --git a/beatygril/beatygril/Plugin/MetalDeviceCache.h b/beatygril/beatygril/Plugin/MetalDeviceCache.h deleted file mode 100644 index a7c4944..0000000 --- a/beatygril/beatygril/Plugin/MetalDeviceCache.h +++ /dev/null @@ -1,28 +0,0 @@ -// -// MetalDeviceCache.h -// beatygril -// -// Created by dayu on 2026/10/21. -// - -#import -#import - -@class MetalDeviceCacheItem; - -@interface MetalDeviceCache : NSObject -{ - NSMutableArray* deviceCaches; -} - -+ (MetalDeviceCache*)deviceCache; -+ (MTLPixelFormat)MTLPixelFormatForImageTile:(FxImageTile*)imageTile; - -- (id)deviceWithRegistryID:(uint64_t)registryID; -- (id)pipelineStateWithRegistryID:(uint64_t)registryID - pixelFormat:(MTLPixelFormat)pixFormat; -- (id)commandQueueWithRegistryID:(uint64_t)registryID - pixelFormat:(MTLPixelFormat)pixFormat; -- (void)returnCommandQueueToCache:(id)commandQueue; - -@end diff --git a/beatygril/beatygril/Plugin/MetalDeviceCache.m b/beatygril/beatygril/Plugin/MetalDeviceCache.m deleted file mode 100644 index 6d5b1c6..0000000 --- a/beatygril/beatygril/Plugin/MetalDeviceCache.m +++ /dev/null @@ -1,339 +0,0 @@ -// -// MetalDeviceCache.m -// beatygril -// -// Created by dayu on 2026/10/21. -// - -#import "MetalDeviceCache.h" - -const NSUInteger kMaxCommandQueues = 5; -static NSString* kKey_InUse = @"InUse"; -static NSString* kKey_CommandQueue = @"CommandQueue"; - -static MetalDeviceCache* gDeviceCache = nil; - -@interface MetalDeviceCacheItem : NSObject - -@property (readonly) id gpuDevice; -@property (readonly) id pipelineState; -@property (retain) NSMutableArray* commandQueueCache; -@property (readonly) NSLock* commandQueueCacheLock; -@property (readonly) MTLPixelFormat pixelFormat; - -- (instancetype)initWithDevice:(id)device - pixelFormat:(MTLPixelFormat)pixFormat; -- (id)getNextFreeCommandQueue; -- (void)returnCommandQueue:(id)commandQueue; -- (BOOL)containsCommandQueue:(id)commandQueue; - -@end - -@implementation MetalDeviceCacheItem - -- (instancetype)initWithDevice:(id)device - pixelFormat:(MTLPixelFormat)pixFormat; -{ - self = [super init]; - - if (self != nil) - { - _gpuDevice = [device retain]; - - _commandQueueCache = [[NSMutableArray alloc] initWithCapacity:kMaxCommandQueues]; - for (NSUInteger i = 0; (_commandQueueCache != nil) && (i < kMaxCommandQueues); i++) - { - NSMutableDictionary* commandDict = [NSMutableDictionary dictionary]; - [commandDict setObject:[NSNumber numberWithBool:NO] - forKey:kKey_InUse]; - - id commandQueue = [_gpuDevice newCommandQueue]; - [commandDict setObject:commandQueue - forKey:kKey_CommandQueue]; - - [_commandQueueCache addObject:commandDict]; - } - - // Load all the shader files with a .metal file extension in the project - id defaultLibrary = [[_gpuDevice newDefaultLibrary] autorelease]; - - // Load the vertex function from the library - id vertexFunction = [[defaultLibrary newFunctionWithName:@"vertexShader"] autorelease]; - - // Load the fragment function from the library - id fragmentFunction = [[defaultLibrary newFunctionWithName:@"fragmentShader"] autorelease]; - - // Configure a pipeline descriptor that is used to create a pipeline state - MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[[MTLRenderPipelineDescriptor alloc] init] autorelease]; - pipelineStateDescriptor.label = @"Simple Pipeline"; - pipelineStateDescriptor.vertexFunction = vertexFunction; - pipelineStateDescriptor.fragmentFunction = fragmentFunction; - pipelineStateDescriptor.colorAttachments[0].pixelFormat = pixFormat; - _pixelFormat = pixFormat; - - NSError* error = nil; - _pipelineState = [_gpuDevice newRenderPipelineStateWithDescriptor:pipelineStateDescriptor - error:&error]; - if (error != nil) - { - NSLog (@"Error generating brightness pipeline state: %@", error); - } - - if (_commandQueueCache != nil) - { - _commandQueueCacheLock = [[NSLock alloc] init]; - } - - if ((_gpuDevice == nil) || (_commandQueueCache == nil) || (_commandQueueCacheLock == nil) || - (_pipelineState == nil)) - { - [self release]; - self = nil; - } - } - - return self; -} - -- (void)dealloc -{ - [_gpuDevice release]; - [_commandQueueCache release]; - [_commandQueueCacheLock release]; - [_pipelineState release]; - - [super dealloc]; -} - -- (id)getNextFreeCommandQueue -{ - id result = nil; - - [_commandQueueCacheLock lock]; - NSUInteger index = 0; - while ((result == nil) && (index < kMaxCommandQueues)) - { - NSMutableDictionary* nextCommandQueue = [_commandQueueCache objectAtIndex:index]; - NSNumber* inUse = [nextCommandQueue objectForKey:kKey_InUse]; - if (![inUse boolValue]) - { - [nextCommandQueue setObject:[NSNumber numberWithBool:YES] - forKey:kKey_InUse]; - result = [nextCommandQueue objectForKey:kKey_CommandQueue]; - } - index++; - } - [_commandQueueCacheLock unlock]; - - return result; -} - -- (void)returnCommandQueue:(id)commandQueue -{ - [_commandQueueCacheLock lock]; - - BOOL found = false; - NSUInteger index = 0; - while ((!found) && (index < kMaxCommandQueues)) - { - NSMutableDictionary* nextCommandQueuDict = [_commandQueueCache objectAtIndex:index]; - id nextCommandQueue = [nextCommandQueuDict objectForKey:kKey_CommandQueue]; - if (nextCommandQueue == commandQueue) - { - found = YES; - [nextCommandQueuDict setObject:[NSNumber numberWithBool:NO] - forKey:kKey_InUse]; - } - index++; - } - - [_commandQueueCacheLock unlock]; -} - -- (BOOL)containsCommandQueue:(id)commandQueue -{ - BOOL found = NO; - NSUInteger index = 0; - while ((!found) && (index < kMaxCommandQueues)) - { - NSMutableDictionary* nextCommandQueuDict = [_commandQueueCache objectAtIndex:index]; - id nextCommandQueue = [nextCommandQueuDict objectForKey:kKey_CommandQueue]; - if (nextCommandQueue == commandQueue) - { - found = YES; - } - index++; - } - - return found; -} - -@end - -@implementation MetalDeviceCache - -+ (MTLPixelFormat)MTLPixelFormatForImageTile:(FxImageTile*)imageTile -{ - MTLPixelFormat result = MTLPixelFormatRGBA16Float; - - switch (imageTile.ioSurface.pixelFormat) - { - case kCVPixelFormatType_128RGBAFloat: - result = MTLPixelFormatRGBA32Float; - break; - - case kCVPixelFormatType_32BGRA: - result = MTLPixelFormatBGRA8Unorm; - break; - - default: - break; - } - - return result; -} - -+ (MetalDeviceCache*)deviceCache; -{ - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - gDeviceCache = [[MetalDeviceCache alloc] init]; - }); - - return gDeviceCache; -} - -- (instancetype)init -{ - self = [super init]; - - if (self != nil) - { - NSArray>* devices = MTLCopyAllDevices(); - - deviceCaches = [[NSMutableArray alloc] initWithCapacity:devices.count]; - - for (id nextDevice in devices) - { - MetalDeviceCacheItem* newCacheItem = [[[MetalDeviceCacheItem alloc] initWithDevice:nextDevice - pixelFormat:MTLPixelFormatRGBA16Float] - autorelease]; - [deviceCaches addObject:newCacheItem]; - } - - [devices release]; - } - - return self; -} - -- (void)dealloc -{ - [deviceCaches release]; - - [super dealloc]; -} - -- (id)deviceWithRegistryID:(uint64_t)registryID -{ - for (MetalDeviceCacheItem* nextCacheItem in deviceCaches) - { - if (nextCacheItem.gpuDevice.registryID == registryID) - { - return nextCacheItem.gpuDevice; - } - } - - return nil; -} - -- (id)pipelineStateWithRegistryID:(uint64_t)registryID - pixelFormat:(MTLPixelFormat)pixFormat -{ - for (MetalDeviceCacheItem* nextCacheItem in deviceCaches) - { - if ((nextCacheItem.gpuDevice.registryID == registryID) && - (nextCacheItem.pixelFormat == pixFormat)) - { - return nextCacheItem.pipelineState; - } - } - // Didn't find one, so create one with the right settings - NSArray>* devices = MTLCopyAllDevices(); - id device = nil; - for (id nextDevice in devices) - { - if (nextDevice.registryID == registryID) - { - device = nextDevice; - } - } - - id result = nil; - if (device != nil) - { - MetalDeviceCacheItem* newCacheItem = [[[MetalDeviceCacheItem alloc] initWithDevice:device - pixelFormat:pixFormat] - autorelease]; - if (newCacheItem != nil) - { - [deviceCaches addObject:newCacheItem]; - result = newCacheItem.pipelineState; - } - } - [devices release]; - return result; -} - -- (id)commandQueueWithRegistryID:(uint64_t)registryID - pixelFormat:(MTLPixelFormat)pixFormat; -{ - for (MetalDeviceCacheItem* nextCacheItem in deviceCaches) - { - if ((nextCacheItem.gpuDevice.registryID == registryID) && - (nextCacheItem.pixelFormat == pixFormat)) - { - return [nextCacheItem getNextFreeCommandQueue]; - } - } - - // Didn't find one, so create one with the right settings - NSArray>* devices = MTLCopyAllDevices(); - id device = nil; - for (id nextDevice in devices) - { - if (nextDevice.registryID == registryID) - { - device = nextDevice; - } - } - - id result = nil; - if (device != nil) - { - MetalDeviceCacheItem* newCacheItem = [[[MetalDeviceCacheItem alloc] initWithDevice:device - pixelFormat:pixFormat] - autorelease]; - if (newCacheItem != nil) - { - [deviceCaches addObject:newCacheItem]; - result = [newCacheItem getNextFreeCommandQueue]; - } - } - [devices release]; - return result; -} - -- (void)returnCommandQueueToCache:(id)commandQueue; -{ - for (MetalDeviceCacheItem* nextCacheItem in deviceCaches) - { - if ([nextCacheItem containsCommandQueue:commandQueue]) - { - [nextCacheItem returnCommandQueue:commandQueue]; - break; - } - } -} - -@end diff --git a/beatygril/beatygril/Plugin/PGBeautyTypes.h b/beatygril/beatygril/Plugin/PGBeautyTypes.h new file mode 100644 index 0000000..e1cd3ef --- /dev/null +++ b/beatygril/beatygril/Plugin/PGBeautyTypes.h @@ -0,0 +1,30 @@ +#pragma once +#include + +// Persisted FxPlug render state. Append fields and bump version for migrations. +typedef struct { + uint32_t version; + float amount, skinBrightness, skinHue, skinSaturation, skinEvenTone; + float midtoneDetail, highPassDetail, teethWhitening, eyeBrightening; + float radius, sharpness, mix, maskThreshold, maskSoftness, maskEdgeProtect; + float faceThin, faceBigEye, faceShort, faceNarrow, faceChin, faceV; + float faceJaw, faceCheekbone, faceSmallHead, faceSmallHeadRange; + float faceJumpGuardRange, selectionX, selectionY; + uint32_t featureProtection, skinTonePreset, showMask, invertMask, debugChannel; + uint32_t showFaceBoxes, showTrackBox, singleFace, fastMultiFace, trackingEnabled, targetIndex; +} PGBeautySettings; + +static inline PGBeautySettings PGBeautyDefaults(void) { + PGBeautySettings s = {0}; + s.version = 1; + s.amount = 1.0f; + s.radius = 8.0f; + s.mix = 1.0f; + s.maskThreshold = 148.0f; + s.maskSoftness = 0.6f; + s.maskEdgeProtect = 0.4f; + s.featureProtection = 1; + s.faceJumpGuardRange = 1.8f; + s.selectionX = s.selectionY = 0.5f; + return s; +} diff --git a/beatygril/beatygril/Plugin/PGFaceSelectOSC.m b/beatygril/beatygril/Plugin/PGFaceSelectOSC.m new file mode 100644 index 0000000..e6ebc07 --- /dev/null +++ b/beatygril/beatygril/Plugin/PGFaceSelectOSC.m @@ -0,0 +1,64 @@ +#import +#import +#import +#import "PGParameterIDs.h" + +@interface PGFaceSelectOSC : NSObject +@property (assign) id apiManager; +@end +@implementation PGFaceSelectOSC +- (instancetype)initWithAPIManager:(id)api { + if((self=[super init])) _apiManager=api; return self; +} +- (FxDrawingCoordinates)drawingCoordinates {return kFxDrawingCoordinates_OBJECT;} +- (id)oscAPI {return [_apiManager apiForProtocol:@protocol(FxOnScreenControlAPI_v4)];} +- (BOOL)selectionEnabledAtTime:(CMTime)time { + BOOL selected=NO;id api=[_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)]; + [api getBoolValue:&selected fromParameter:PGSingleFace atTime:time];return selected; +} +- (void)drawOSCWithWidth:(NSInteger)width height:(NSInteger)height activePart:(NSInteger)part destinationImage:(FxImageTile *)tile atTime:(CMTime)time { + if(!tile.ioSurface||width<=0||height<=0) return; + CIImage *output=[[CIImage imageWithColor:CIColor.clearColor] imageByCroppingToRect:CGRectMake(0,0,width,height)]; + if([self selectionEnabledAtTime:time]) { + double x=.5,y=.5; + id params=[_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)]; + [params getXValue:&x YValue:&y fromParameter:PGSelection atTime:time]; + CGRect bounds=[[self oscAPI] objectBounds];double canvasX=width*.5,canvasY=height*.5; + [[self oscAPI] convertPointFromSpace:kFxDrawingCoordinates_OBJECT fromX:bounds.origin.x+x*bounds.size.width fromY:bounds.origin.y+y*bounds.size.height toSpace:kFxDrawingCoordinates_CANVAS toX:&canvasX toY:&canvasY]; + CGFloat scale=MAX(1,[[self oscAPI] backingScaleFactor]); + size_t surfaceW=IOSurfaceGetWidth((__bridge IOSurfaceRef)tile.ioSurface),surfaceH=IOSurfaceGetHeight((__bridge IOSurfaceRef)tile.ioSurface); + CGFloat sx=(CGFloat)surfaceW/width,sy=(CGFloat)surfaceH/height; + canvasX*=sx;canvasY*=sy;CGFloat radius=10*scale; + CIColor *color=part?[CIColor colorWithRed:1 green:.8 blue:.2]:[CIColor colorWithRed:.1 green:.9 blue:.8]; + CIImage *ring=[CIFilter filterWithName:@"CIRadialGradient" withInputParameters:@{@"inputCenter":[CIVector vectorWithX:canvasX Y:canvasY],@"inputRadius0":@(radius-2),@"inputRadius1":@(radius),@"inputColor0":color,@"inputColor1":CIColor.clearColor}].outputImage; + CIImage *hole=[CIFilter filterWithName:@"CIRadialGradient" withInputParameters:@{@"inputCenter":[CIVector vectorWithX:canvasX Y:canvasY],@"inputRadius0":@(radius-3),@"inputRadius1":@(radius-2),@"inputColor0":CIColor.blackColor,@"inputColor1":CIColor.whiteColor}].outputImage; + ring=[ring imageByApplyingFilter:@"CIBlendWithMask" withInputParameters:@{kCIInputBackgroundImageKey:output,kCIInputMaskImageKey:hole}]; + CIImage *v=[[CIImage imageWithColor:color] imageByCroppingToRect:CGRectMake(canvasX-scale,canvasY-16*scale,2*scale,32*scale)]; + CIImage *h=[[CIImage imageWithColor:color] imageByCroppingToRect:CGRectMake(canvasX-16*scale,canvasY-scale,32*scale,2*scale)]; + output=[[h imageByCompositingOverImage:v] imageByCompositingOverImage:ring]; + } + CIContext *context=[CIContext contextWithOptions:@{kCIContextCacheIntermediates:@NO}]; + size_t w=IOSurfaceGetWidth((__bridge IOSurfaceRef)tile.ioSurface),h=IOSurfaceGetHeight((__bridge IOSurfaceRef)tile.ioSurface); + if(tile.imageOrigin==kFxImageOrigin_TOP_LEFT) output=[output imageByApplyingTransform:CGAffineTransformMake(1,0,0,-1,0,h)]; + [context render:output toIOSurface:(__bridge IOSurfaceRef)tile.ioSurface bounds:CGRectMake(0,0,w,h) colorSpace:tile.colorSpace]; +} +- (void)hitTestOSCAtMousePositionX:(double)x mousePositionY:(double)y activePart:(NSInteger *)part atTime:(CMTime)time { + if(part) *part=([self selectionEnabledAtTime:time]&&CGRectContainsPoint([[self oscAPI] objectBounds],CGPointMake(x,y)))?1:0; +} +- (void)setSelectionX:(double)x y:(double)y time:(CMTime)time update:(BOOL *)update { + CGRect bounds=[[self oscAPI] objectBounds];if(bounds.size.width<=0||bounds.size.height<=0) return; + id api=[_apiManager apiForProtocol:@protocol(FxParameterSettingAPI_v5)]; + BOOL ok=[api setXValue:MIN(1,MAX(0,(x-bounds.origin.x)/bounds.size.width)) YValue:MIN(1,MAX(0,(y-bounds.origin.y)/bounds.size.height)) toParameter:PGSelection atTime:time]; + if(update) *update=ok; +} +- (void)mouseDownAtPositionX:(double)x positionY:(double)y activePart:(NSInteger)part modifiers:(FxModifierKeys)keys forceUpdate:(BOOL *)update atTime:(CMTime)time { + id undo=[_apiManager apiForProtocol:@protocol(FxUndoAPI)];[undo startUndoGroup:@"选择人脸"]; + [self setSelectionX:x y:y time:time update:update]; +} +- (void)mouseDraggedAtPositionX:(double)x positionY:(double)y activePart:(NSInteger)part modifiers:(FxModifierKeys)keys forceUpdate:(BOOL *)update atTime:(CMTime)time {[self setSelectionX:x y:y time:time update:update];} +- (void)mouseUpAtPositionX:(double)x positionY:(double)y activePart:(NSInteger)part modifiers:(FxModifierKeys)keys forceUpdate:(BOOL *)update atTime:(CMTime)time { + [self setSelectionX:x y:y time:time update:update];id undo=[_apiManager apiForProtocol:@protocol(FxUndoAPI)];[undo endUndoGroup]; +} +- (void)keyDownAtPositionX:(double)x positionY:(double)y keyPressed:(unsigned short)key modifiers:(FxModifierKeys)modifiers forceUpdate:(BOOL *)update didHandle:(BOOL *)handled atTime:(CMTime)time {if(handled)*handled=NO;if(update)*update=NO;} +- (void)keyUpAtPositionX:(double)x positionY:(double)y keyPressed:(unsigned short)key modifiers:(FxModifierKeys)modifiers forceUpdate:(BOOL *)update didHandle:(BOOL *)handled atTime:(CMTime)time {if(handled)*handled=NO;if(update)*update=NO;} +@end diff --git a/beatygril/beatygril/Plugin/PGParameterIDs.h b/beatygril/beatygril/Plugin/PGParameterIDs.h new file mode 100644 index 0000000..4cc0e20 --- /dev/null +++ b/beatygril/beatygril/Plugin/PGParameterIDs.h @@ -0,0 +1,17 @@ +#pragma once +// Existing brightness sample ID 1 is migrated to the portrait amount control. +enum { + PGAmount=1, PGBrightness=2, PGRadius=3, PGSharpness=4, PGMix=5, + PGThreshold=7, PGSoftness=8, PGEdgeProtect=9, PGShowMask=10, + PGMidtone=12, PGHue=14, PGSaturation=15, PGHighPass=17, PGThin=18, + PGFaceBoxes=19, PGSingleFace=20, PGSelection=21, PGShort=22, + PGNarrow=23, PGChin=24, PGVFace=25, PGJaw=26, PGCheekbone=27, + PGSmallHead=28, PGSmallHeadRange=29, PGFeatureProtect=30, PGTeeth=31, + PGEyeBright=32, PGBigEye=33, PGTrackBoxes=35, PGFastMulti=39, + PGJumpGuard=40, PGEvenTone=41, PGTonePreset=42, PGInvertMask=43, + PGDebugChannel=44, PGTrackEnabled=45, PGTarget=46, + PGAnalyzeForward=50, PGAnalyzeBackward=51, PGClearTrack=52, PGStatus=53, + PGTrackData1=60, PGTrackData2=61, PGTrackData3=62, + PGSmoothingEnabled=70, PGColorEnabled=71, PGShapeEnabled=72, + PGDetailEnabled=73, PGHelp=74 +}; diff --git a/beatygril/beatygril/Plugin/TileableRemoteBrightness.metal b/beatygril/beatygril/Plugin/TileableRemoteBrightness.metal deleted file mode 100644 index 530ea30..0000000 --- a/beatygril/beatygril/Plugin/TileableRemoteBrightness.metal +++ /dev/null @@ -1,83 +0,0 @@ -// -// TileableRemoteBrightness.metal -// beatygril -// -// Created by dayu on 2026/10/21. -// - -#include -#include - -using namespace metal; - -#include "TileableRemoteBrightnessShaderTypes.h" - -typedef struct -{ - // The [[position]] attribute of this member indicates that this value is the clip space - // position of the vertex when this structure is returned from the vertex function - float4 clipSpacePosition [[position]]; - - // Since this member does not have a special attribute, the rasterizer interpolates - // its value with the values of the other triangle vertices and then passes - // the interpolated value to the fragment shader for each fragment in the triangle - float2 textureCoordinate; - -} RasterizerData; - -vertex RasterizerData -vertexShader(uint vertexID [[vertex_id]], - constant Vertex2D *vertexArray [[buffer(BVI_Vertices)]], - constant vector_uint2 *viewportSizePointer [[buffer(BVI_ViewportSize)]]) -{ - RasterizerData out; - - // Index into our array of positions to get the current vertex - // Our positions are specified in pixel dimensions (i.e. a value of 100 is 100 pixels from - // the origin) - float2 pixelSpacePosition = vertexArray[vertexID].position.xy; - - // Get the size of the drawable so that we can convert to normalized device coordinates, - float2 viewportSize = float2(*viewportSizePointer); - - // The output position of every vertex shader is in clip space (also known as normalized device - // coordinate space, or NDC). A value of (-1.0, -1.0) in clip-space represents the - // lower-left corner of the viewport whereas (1.0, 1.0) represents the upper-right corner of - // the viewport. - - // In order to convert from positions in pixel space to positions in clip space we divide the - // pixel coordinates by half the size of the viewport. - out.clipSpacePosition.xy = pixelSpacePosition / (viewportSize / 2.0); - - // Set the z component of our clip space position 0 (since we're only rendering in - // 2-Dimensions for this sample) - out.clipSpacePosition.z = 0.0; - - // Set the w component to 1.0 since we don't need a perspective divide, which is also not - // necessary when rendering in 2-Dimensions - out.clipSpacePosition.w = 1.0; - - // Pass our input textureCoordinate straight to our output RasterizerData. This value will be - // interpolated with the other textureCoordinate values in the vertices that make up the - // triangle. - out.textureCoordinate = vertexArray[vertexID].textureCoordinate; - - return out; -} - -// Fragment function -fragment float4 fragmentShader(RasterizerData in [[stage_in]], - texture2d colorTexture [[ texture(BTI_InputImage) ]], - constant float* brightness [[ buffer(BFI_Brightness) ]]) -{ - constexpr sampler textureSampler (mag_filter::linear, - min_filter::linear); - - // Sample the texture to obtain a color - half4 colorSample = colorTexture.sample(textureSampler, in.textureCoordinate); - const half hBrightness = static_cast(*brightness); - colorSample.rgb = colorSample.rgb * hBrightness; - - // We return the color of the texture - return float4(colorSample); -} diff --git a/beatygril/beatygril/Plugin/TileableRemoteBrightnessShaderTypes.h b/beatygril/beatygril/Plugin/TileableRemoteBrightnessShaderTypes.h deleted file mode 100644 index a7458f6..0000000 --- a/beatygril/beatygril/Plugin/TileableRemoteBrightnessShaderTypes.h +++ /dev/null @@ -1,32 +0,0 @@ -// -// TileableRemoteBrightnessShaderTypes.h -// beatygril -// -// Created by dayu on 2026/10/21. -// - -#ifndef TileableRemoteBrightnessShaderTypes_h -#define TileableRemoteBrightnessShaderTypes_h - -#import - -typedef enum BrightnessVertexInputIndex { - BVI_Vertices = 0, - BVI_ViewportSize = 1 -} BrightnessVertexInputIndex; - -typedef enum BrightnessTextureIndex { - BTI_InputImage = 0 -} BrightnessTextureIndex; - -typedef enum BrightnessFragmentIndex { - BFI_Brightness = 0 -} BrightnessFragmentIndex; - -typedef struct Vertex2D { - vector_float2 position; - vector_float2 textureCoordinate; -} Vertex2D; - - -#endif /* TileableRemoteBrightnessShaderTypes_h */ diff --git a/beatygril/beatygril/Plugin/beatygrilPlugIn.h b/beatygril/beatygril/Plugin/beatygrilPlugIn.h index d76b50d..12dca07 100644 --- a/beatygril/beatygril/Plugin/beatygrilPlugIn.h +++ b/beatygril/beatygril/Plugin/beatygrilPlugIn.h @@ -1,13 +1,11 @@ -// -// beatygrilPlugIn.h -// beatygril -// -// Created by dayu on 2026/10/21. -// - #import #import +#import "PGBeautyTypes.h" -@interface beatygrilPlugIn : NSObject -@property (assign) id apiManager; +@interface beatygrilPlugIn : NSObject +@property (nonatomic, assign) id apiManager; +- (PGBeautySettings)settingsAtTime:(CMTime)time; +- (void)analyzeForward; +- (void)analyzeBackward; +- (void)clearTracking; @end diff --git a/beatygril/beatygril/Plugin/beatygrilPlugIn.m b/beatygril/beatygril/Plugin/beatygrilPlugIn.m index 2812ebb..f559130 100644 --- a/beatygril/beatygril/Plugin/beatygrilPlugIn.m +++ b/beatygril/beatygril/Plugin/beatygrilPlugIn.m @@ -1,320 +1,290 @@ -// -// beatygrilPlugIn.m -// beatygril -// -// Created by dayu on 2026/10/21. -// - #import "beatygrilPlugIn.h" -#import -#import "TileableRemoteBrightnessShaderTypes.h" -#import "MetalDeviceCache.h" +#import "PGParameterIDs.h" +#import "PGBeautyRenderer.h" +#import "PGFaceProcessor.h" +#import +#import +#import +#import + +typedef struct { UInt32 identifier; const char *name; size_t offset; float min, max, step; BOOL percent; } PGSlider; +#define PGS(ID, NAME, FIELD, MIN, MAX, STEP, PERCENT) {ID, NAME, offsetof(PGBeautySettings, FIELD), MIN, MAX, STEP, PERCENT} +static const PGSlider PGSliders[] = { + PGS(PGAmount,"磨皮强度",amount,0,1,.01,YES), + PGS(PGMidtone,"中频磨皮",midtoneDetail,0,1,.01,YES), + PGS(PGRadius,"磨皮半径",radius,1,30,.5,NO), + PGS(PGBrightness,"肤色提亮",skinBrightness,0,1,.01,YES), + PGS(PGHue,"肤色色相",skinHue,-30,30,1,NO), + PGS(PGSaturation,"肤色饱和度",skinSaturation,-100,100,1,NO), + PGS(PGEvenTone,"均匀肤色",skinEvenTone,0,1,.01,YES), + PGS(PGTeeth,"牙齿美白",teethWhitening,0,1,.01,YES), + PGS(PGEyeBright,"眼睛提亮",eyeBrightening,0,1,.01,YES), + PGS(PGHighPass,"高频细节平滑",highPassDetail,0,1,.01,YES), + PGS(PGSharpness,"锐化",sharpness,0,1,.01,YES), + PGS(PGThin,"瘦脸",faceThin,0,1,.01,YES), + PGS(PGBigEye,"大眼",faceBigEye,0,1,.01,YES), + PGS(PGShort,"短脸",faceShort,-1,1,.01,NO), + PGS(PGNarrow,"窄脸",faceNarrow,-1,1,.01,NO), + PGS(PGChin,"下巴",faceChin,-1,1,.01,NO), + PGS(PGVFace,"V 脸",faceV,0,1,.01,YES), + PGS(PGJaw,"下颌",faceJaw,0,1,.01,YES), + PGS(PGCheekbone,"颧骨",faceCheekbone,0,1,.01,YES), + PGS(PGSmallHead,"小头",faceSmallHead,0,1,.01,YES), + PGS(PGSmallHeadRange,"小头范围",faceSmallHeadRange,0,1,.01,YES), + PGS(PGThreshold,"蒙版阈值",maskThreshold,0,255,1,NO), + PGS(PGSoftness,"蒙版柔度",maskSoftness,0,1,.01,YES), + PGS(PGEdgeProtect,"边缘保护",maskEdgeProtect,0,1,.01,YES), + PGS(PGJumpGuard,"防跳脸范围",faceJumpGuardRange,1,4,.05,NO), + PGS(PGMix,"整体混合",mix,0,1,.01,YES), +}; +#undef PGS + +static BOOL PGFail(NSError **error, NSString *message) { + if (error) *error = [NSError errorWithDomain:FxPlugErrorDomain code:kFxError_InvalidParameter + userInfo:@{NSLocalizedDescriptionKey:message}]; + return NO; +} +static CIImage *PGImageFromTile(FxImageTile *tile) { + if (!tile.ioSurface) return nil; + NSDictionary *options = tile.colorSpace ? @{kCIImageColorSpace:(__bridge id)tile.colorSpace} : @{}; + CIImage *image = [CIImage imageWithIOSurface:(__bridge IOSurfaceRef)tile.ioSurface options:options]; + if (tile.imageOrigin == kFxImageOrigin_TOP_LEFT) { + image = [image imageByApplyingTransform:CGAffineTransformMake(1,0,0,-1,0,image.extent.size.height)]; + } + FxRect b = tile.tilePixelBounds; + return [image imageByApplyingTransform:CGAffineTransformMakeTranslation(b.left,b.bottom)]; +} + +@interface beatygrilPlugIn () +@property (strong) PGBeautyRenderer *beautyRenderer; +@property (strong) PGFaceProcessor *faceProcessor; +@property (strong) NSMutableDictionary *contexts; +@property PGBeautySettings analysisSettings; +@property CMTime analysisTime; +@property NSUInteger analyzedFrames; +@property BOOL analyzing; +@property (strong) NSArray *loadedTracks; +@end @implementation beatygrilPlugIn - -//--------------------------------------------------------- -// initWithAPIManager: -// -// This method is called when a plug-in is first loaded, and -// is a good point to conduct any checks for anti-piracy or -// system compatibility. Returning NULL means that a plug-in -// chooses not to be accessible for some reason. -//--------------------------------------------------------- - -- (nullable instancetype)initWithAPIManager:(id)newApiManager; -{ - self = [super init]; - if (self != nil) - { - _apiManager = newApiManager; +- (instancetype)initWithAPIManager:(id)manager { + if ((self=[super init])) { + _apiManager=manager; + _beautyRenderer=[PGBeautyRenderer new]; _faceProcessor=[PGFaceProcessor new]; + _contexts=[NSMutableDictionary new]; _analysisSettings=PGBeautyDefaults(); + _analysisTime=kCMTimeZero; } return self; } - -//--------------------------------------------------------- -// properties -// -// This method should return an NSDictionary defining the -// properties of the effect. -//--------------------------------------------------------- - -- (BOOL)properties:(NSDictionary * _Nonnull *)properties - error:(NSError * _Nullable *)error -{ - *properties = @{ - kFxPropertyKey_MayRemapTime : [NSNumber numberWithBool:NO], - kFxPropertyKey_PixelTransformSupport : [NSNumber numberWithInt:kFxPixelTransform_ScaleTranslate], - kFxPropertyKey_VariesWhenParamsAreStatic : [NSNumber numberWithBool:NO] - }; - +- (BOOL)properties:(NSDictionary **)properties error:(NSError **)error { + if (!properties) return PGFail(error,@"宿主未提供属性输出位置。"); + *properties=@{kFxPropertyKey_MayRemapTime:@NO, + kFxPropertyKey_PixelTransformSupport:@(kFxPixelTransform_ScaleTranslate), + kFxPropertyKey_VariesWhenParamsAreStatic:@NO, + @"NeedsFullBuffer":@YES, @"ChangesOutputSize":@NO, + kFxPropertyKey_DesiredProcessingColorInfo:@(kFxImageColorInfo_RGB_GAMMA_VIDEO)}; return YES; } - -//--------------------------------------------------------- -// addParametersWithError -// -// This method is where a plug-in defines its list of parameters. -//--------------------------------------------------------- - -- (BOOL)addParametersWithError:(NSError**)error -{ - id paramAPI = [_apiManager apiForProtocol:@protocol(FxParameterCreationAPI_v5)]; - if (paramAPI == nil) - { - NSDictionary* userInfo = @{ - NSLocalizedDescriptionKey : @"Unable to obtain an FxPlug API Object" - }; - if (error != NULL) - *error = [NSError errorWithDomain:FxPlugErrorDomain - code:kFxError_APIUnavailable - userInfo:userInfo]; - - return NO; +- (BOOL)addSlider:(UInt32)identifier api:(id)api { + PGBeautySettings defaults=PGBeautyDefaults(); + for (NSUInteger i=0;i paramGetAPI = [_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)]; - if (paramGetAPI != nil) - { - double brightness = 1.0; - [paramGetAPI getFloatValue:&brightness - fromParameter:1 - atTime:renderTime]; - - *pluginState = [NSData dataWithBytes:&brightness - length:sizeof(brightness)]; - - if (*pluginState != nil) - { - succeeded = YES; +- (BOOL)addParametersWithError:(NSError **)error { + id api=[_apiManager apiForProtocol:@protocol(FxParameterCreationAPI_v5)]; + if (!api) return PGFail(error,@"无法取得 FxPlug 参数创建接口。"); + __block BOOL ok=YES; + void (^slider)(UInt32)=^(UInt32 identifier){ok=[self addSlider:identifier api:api]&&ok;}; + void (^toggle)(NSString *,UInt32,BOOL)=^(NSString *name,UInt32 identifier,BOOL value){ok=[api addToggleButtonWithName:name parameterID:identifier defaultValue:value parameterFlags:kFxParameterFlag_DEFAULT]&&ok;}; + void (^group)(NSString *,UInt32)=^(NSString *name,UInt32 identifier){ok=[api startParameterSubGroup:name parameterID:identifier parameterFlags:kFxParameterFlag_DEFAULT]&&ok;}; + void (^end)(void)=^{ok=[api endParameterSubGroup]&&ok;}; + group(@"磨皮",100); toggle(@"启用磨皮",PGSmoothingEnabled,YES); + slider(PGAmount); slider(PGMidtone); slider(PGRadius); toggle(@"保护五官",PGFeatureProtect,YES); end(); + group(@"肤色",101); toggle(@"启用肤色",PGColorEnabled,YES); + ok=[api addPopupMenuWithName:@"肤色风格" parameterID:PGTonePreset defaultValue:0 menuEntries:@[@"原色",@"自然白",@"冷白",@"暖白",@"红润白",@"小麦",@"古铜"] parameterFlags:0]&&ok; + slider(PGBrightness); slider(PGHue); slider(PGSaturation); slider(PGEvenTone); slider(PGTeeth); slider(PGEyeBright); end(); + group(@"细节",102); toggle(@"启用细节",PGDetailEnabled,YES); slider(PGHighPass); slider(PGSharpness); end(); + group(@"脸型",103); toggle(@"启用脸型",PGShapeEnabled,YES); + UInt32 shape[]={PGThin,PGBigEye,PGShort,PGNarrow,PGChin,PGVFace,PGJaw,PGCheekbone,PGSmallHead,PGSmallHeadRange}; + for(NSUInteger i=0;i api=[_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)]; + if(!api) return s; + for(NSUInteger i=0;i *)trackStrings { + id api=[_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)]; + NSMutableArray *tracks=[NSMutableArray new]; + for(UInt32 identifier=PGTrackData1;identifier<=PGTrackData3;identifier++) { + NSString *value=@"";[api getStringParameterValue:&value fromParameter:identifier]; + [tracks addObject:value?:@""]; + } + return tracks; +} +- (BOOL)pluginState:(NSData **)state atTime:(CMTime)time quality:(FxQuality)quality error:(NSError **)error { + if(!state) return PGFail(error,@"缺少渲染状态输出位置。"); + if(![_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)]) return PGFail(error,@"无法读取宿主参数。"); + PGBeautySettings s=[self settingsAtTime:time]; + @synchronized(self) {_analysisTime=time;} + *state=[NSPropertyListSerialization dataWithPropertyList:@{@"settings":[NSData dataWithBytes:&s length:sizeof(s)],@"tracks":[self trackStrings]} format:NSPropertyListBinaryFormat_v1_0 options:0 error:error]; + return *state!=nil; +} +- (BOOL)destinationImageRect:(FxRect *)rect sourceImages:(NSArray *)sources destinationImage:(FxImageTile *)destination pluginState:(NSData *)state atTime:(CMTime)time error:(NSError **)error { + if(!rect||sources.count==0) return PGFail(error,@"缺少输入图像。"); + *rect=sources.firstObject.imagePixelBounds;return YES; +} +- (BOOL)sourceTileRect:(FxRect *)rect sourceImageIndex:(NSUInteger)index sourceImages:(NSArray *)sources destinationTileRect:(FxRect)destinationRect destinationImage:(FxImageTile *)destination pluginState:(NSData *)state atTime:(CMTime)time error:(NSError **)error { + if(!rect||index>=sources.count) return PGFail(error,@"输入图像索引无效。"); + *rect=sources[index].imagePixelBounds;return YES; +} +- (CIContext *)contextForTile:(FxImageTile *)tile { + NSNumber *key=@(tile.deviceRegistryID); + @synchronized(_contexts) { + if(!_contexts[key]) { + id device=nil; + for(id candidate in MTLCopyAllDevices()) if(candidate.registryID==tile.deviceRegistryID) {device=candidate;break;} + if(!device) device=MTLCreateSystemDefaultDevice(); + if(!device) return nil; + _contexts[key]=[CIContext contextWithMTLDevice:device options:@{kCIContextCacheIntermediates:@NO}]; } + return _contexts[key]; } - else - { - if (error != NULL) - *error = [NSError errorWithDomain:FxPlugErrorDomain - code:kFxError_ThirdPartyDeveloperStart + 20 - userInfo:@{ - NSLocalizedDescriptionKey : - @"Unable to retrieve FxParameterRetrievalAPI_v6 in \ - [-pluginStateAtTime:]" }]; - } - - return succeeded; } - -//--------------------------------------------------------- -// destinationImageRect:sourceImages:destinationImage:pluginState:atTime:error -// -// This method will calculate the rectangular bounds of the output -// image given the various inputs and plug-in state -// at the given render time. -// It will pass in an array of images, the plug-in state -// returned from your plug-in's -pluginStateAtTime:error: method, -// and the render time. -//--------------------------------------------------------- - -- (BOOL)destinationImageRect:(FxRect *)destinationImageRect - sourceImages:(NSArray *)sourceImages - destinationImage:(nonnull FxImageTile *)destinationImage - pluginState:(NSData *)pluginState - atTime:(CMTime)renderTime - error:(NSError * _Nullable *)outError -{ - if (sourceImages.count < 1) - { - NSLog (@"No inputImages list"); - return NO; +- (BOOL)renderDestinationImage:(FxImageTile *)destination sourceImages:(NSArray *)sources pluginState:(NSData *)state atTime:(CMTime)time error:(NSError **)error { + @autoreleasepool { + if(sources.count==0||!sources.firstObject.ioSurface||!destination.ioSurface||state.length>16*1024*1024) return PGFail(error,@"宿主传入的画面或状态无效。"); + NSDictionary *plist=[NSPropertyListSerialization propertyListWithData:state options:NSPropertyListImmutable format:nil error:error]; + if(![plist isKindOfClass:NSDictionary.class]||![plist[@"settings"] isKindOfClass:NSData.class]||[plist[@"settings"] length]!=sizeof(PGBeautySettings)) return PGFail(error,@"渲染状态版本无效。"); + PGBeautySettings s;[plist[@"settings"] getBytes:&s length:sizeof(s)]; + if(s.version!=1) return PGFail(error,@"此版本不支持保存的渲染状态。"); + CIImage *input=PGImageFromTile(sources.firstObject); if(!input) return PGFail(error,@"无法读取 IOSurface。"); + CIContext *context=[self contextForTile:destination]; if(!context) return PGFail(error,@"没有可用的 Metal 图形设备。"); + CIImage *output=nil; + @synchronized(_faceProcessor) { + NSArray *tracks=plist[@"tracks"]; + if(![tracks isKindOfClass:NSArray.class]||tracks.count!=3) return PGFail(error,@"跟踪状态无效。"); + if(![_loadedTracks isEqualToArray:tracks]) { + for(NSUInteger i=0;i<3;i++) { + if(![tracks[i] isKindOfClass:NSString.class]) return PGFail(error,@"跟踪记录无效。"); + [_faceProcessor setTrackingData:[[NSData alloc] initWithBase64EncodedString:tracks[i] options:0] forTarget:i]; + } + _loadedTracks=[tracks copy]; + } + PGFaceFrame *frame=[_faceProcessor processImage:input settings:s time:time error:error]; + if(!frame) return NO; + output=[_beautyRenderer processImage:frame.image settings:s skinMask:frame.skinMask teethMask:frame.teethMask eyeMask:frame.eyeMask error:error]; + } + if(!output) return NO; + // Blend from the original frame so zero mix also disables geometry. + if(s.mix<1 && !s.showMask) { + output=[output imageByApplyingFilter:@"CIDissolveTransition" withInputParameters:@{kCIInputTargetImageKey:input,kCIInputTimeKey:@(1-s.mix)}]; + } + FxRect b=destination.tilePixelBounds; + output=[output imageByApplyingTransform:CGAffineTransformMakeTranslation(-b.left,-b.bottom)]; + size_t width=IOSurfaceGetWidth((__bridge IOSurfaceRef)destination.ioSurface),height=IOSurfaceGetHeight((__bridge IOSurfaceRef)destination.ioSurface); + if(destination.imageOrigin==kFxImageOrigin_TOP_LEFT) output=[output imageByApplyingTransform:CGAffineTransformMake(1,0,0,-1,0,height)]; + @try {[context render:output toIOSurface:(__bridge IOSurfaceRef)destination.ioSurface bounds:CGRectMake(0,0,width,height) colorSpace:destination.colorSpace];} + @catch(NSException *exception) {return PGFail(error,exception.reason?:@"图像处理失败。");} + return YES; } - - // In the case of a filter that only changed RGB values, - // the output rect is the same as the input rect. - *destinationImageRect = sourceImages [ 0 ].imagePixelBounds; - - return YES; - } - -//--------------------------------------------------------- -// sourceTileRect:sourceImageIndex:sourceImages:destinationTileRect:destinationImage:pluginState:atTime:error -// -// Calculate tile of the source image we need -// to render the given output tile. -//--------------------------------------------------------- - -- (BOOL)sourceTileRect:(FxRect *)sourceTileRect - sourceImageIndex:(NSUInteger)sourceImageIndex - sourceImages:(NSArray *)sourceImages - destinationTileRect:(FxRect)destinationTileRect - destinationImage:(FxImageTile *)destinationImage - pluginState:(NSData *)pluginState - atTime:(CMTime)renderTime - error:(NSError * _Nullable *)outError -{ - // Since this is a color-only filter, the input tile will be the same size as the output tile - *sourceTileRect = destinationTileRect; - - return YES; +- (void)setStatus:(NSString *)status { + id api=[_apiManager apiForProtocol:@protocol(FxParameterSettingAPI_v5)]; + [api setStringParameterValue:status toParameter:PGStatus]; } - -//--------------------------------------------------------- -// renderDestinationImage:sourceImages:pluginState:atTime:error: -// -// The host will call this method when it wants your plug-in to render an image -// tile of the output image. It will pass in each of the input tiles needed as well -// as the plug-in state needed for the calculations. Your plug-in should do all its -// rendering in this method. It should not attempt to use the FxParameterRetrievalAPI* -// object as it is invalid at this time. Note that this method will be called on -// multiple threads at the same time. -//--------------------------------------------------------- - -- (BOOL)renderDestinationImage:(FxImageTile *)destinationImage - sourceImages:(NSArray *)sourceImages - pluginState:(NSData *)pluginState - atTime:(CMTime)renderTime - error:(NSError * _Nullable *)outError -{ - if ((pluginState == nil) || (sourceImages [ 0 ].ioSurface == nil) || (destinationImage.ioSurface == nil)) - { - NSDictionary* userInfo = @{ - NSLocalizedDescriptionKey : @"Invalid plugin state received from host" - }; - if (outError != NULL) - *outError = [NSError errorWithDomain:FxPlugErrorDomain - code:kFxError_InvalidParameter - userInfo:userInfo]; - return NO; +- (void)startAnalysisForward:(BOOL)forward { + NSError *error=nil; + PGBeautySettings s=[self settingsAtTime:_analysisTime]; + @synchronized(_faceProcessor) {[_faceProcessor setTrackingData:nil forTarget:s.targetIndex];} + _analysisSettings=s;_analysisSettings.trackingEnabled=1;_analysisSettings.singleFace=1; + id api=[_apiManager apiForProtocol:@protocol(FxAnalysisAPI)]; + BOOL ok=forward?[api startForwardAnalysis:kFxAnalysisLocation_CPU error:&error]:[api startBackwardAnalysis:kFxAnalysisLocation_CPU error:&error]; + [self setStatus:ok?@"正在分析所选人脸…":(error.localizedDescription?:@"当前宿主不提供帧分析接口。")]; +} +- (void)analyzeForward {[self startAnalysisForward:YES];} +- (void)analyzeBackward {[self startAnalysisForward:NO];} +- (void)clearTracking { + PGBeautySettings s=[self settingsAtTime:_analysisTime]; + @synchronized(_faceProcessor) {[_faceProcessor setTrackingData:nil forTarget:s.targetIndex];_loadedTracks=nil;} + id api=[_apiManager apiForProtocol:@protocol(FxParameterSettingAPI_v5)]; + [api setStringParameterValue:@"" toParameter:PGTrackData1+s.targetIndex]; + [self setStatus:@"已清除当前目标的分析数据"]; +} +- (BOOL)desiredAnalysisTimeRange:(CMTimeRange *)desired forInputWithTimeRange:(CMTimeRange)range error:(NSError **)error { + if(!desired) return PGFail(error,@"缺少分析时间范围。");*desired=range;return YES; +} +- (BOOL)setupAnalysisForTimeRange:(CMTimeRange)range frameDuration:(CMTime)duration error:(NSError **)error { + _analyzedFrames=0;_analyzing=YES;return YES; +} +- (BOOL)analyzeFrame:(FxImageTile *)frame atTime:(CMTime)time error:(NSError **)error { + CIImage *image=PGImageFromTile(frame);if(!image) return PGFail(error,@"无法读取分析帧。"); + BOOL ok=NO; + @synchronized(_faceProcessor) {ok=[_faceProcessor analyzeImage:image settings:_analysisSettings time:time error:error];} + if(ok) _analyzedFrames++; + return ok; +} +- (BOOL)cleanupAnalysis:(NSError **)error { + id api=[_apiManager apiForProtocol:@protocol(FxParameterSettingAPI_v5)]; + NSData *data=nil; + @synchronized(_faceProcessor) {data=[_faceProcessor trackingDataForTarget:_analysisSettings.targetIndex];_loadedTracks=nil;} + if(data) { + if(![api setStringParameterValue:[data base64EncodedStringWithOptions:0] toParameter:PGTrackData1+_analysisSettings.targetIndex]) return PGFail(error,@"无法将分析数据保存到工程。"); + [api setBoolValue:YES toParameter:PGTrackEnabled atTime:_analysisTime]; } - - // This is where you would access parameter values and other info about the source tile - // from the pluginState. - double brightness = 0.0; - [pluginState getBytes:&brightness - length:sizeof(brightness)]; - - // Set up the renderer, in this case we are using Metal. - - MetalDeviceCache* deviceCache = [MetalDeviceCache deviceCache]; - MTLPixelFormat pixelFormat = [MetalDeviceCache MTLPixelFormatForImageTile:destinationImage]; - id commandQueue = [deviceCache commandQueueWithRegistryID:sourceImages[0].deviceRegistryID - pixelFormat:pixelFormat]; - if (commandQueue == nil) - { - return NO; - } - - id commandBuffer = [commandQueue commandBuffer]; - commandBuffer.label = @"DynamicRegXPC Command Buffer"; - [commandBuffer enqueue]; - - id inputTexture = [sourceImages[0] metalTextureForDevice:[deviceCache deviceWithRegistryID:sourceImages[0].deviceRegistryID]]; - id outputTexture = [destinationImage metalTextureForDevice:[deviceCache deviceWithRegistryID:destinationImage.deviceRegistryID]]; - - MTLRenderPassColorAttachmentDescriptor* colorAttachmentDescriptor = [[MTLRenderPassColorAttachmentDescriptor alloc] init]; - colorAttachmentDescriptor.texture = outputTexture; - colorAttachmentDescriptor.clearColor = MTLClearColorMake(1.0, 0.5, 0.0, 1.0); - colorAttachmentDescriptor.loadAction = MTLLoadActionClear; - MTLRenderPassDescriptor* renderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor]; - renderPassDescriptor.colorAttachments [ 0 ] = colorAttachmentDescriptor; - id commandEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor]; - - // Rendering - float outputWidth = (float)(destinationImage.tilePixelBounds.right - destinationImage.tilePixelBounds.left); - float outputHeight = (float)(destinationImage.tilePixelBounds.top - destinationImage.tilePixelBounds.bottom); - Vertex2D vertices[] = { - { { outputWidth / 2.0, -outputHeight / 2.0 }, { 1.0, 1.0 } }, - { { -outputWidth / 2.0, -outputHeight / 2.0 }, { 0.0, 1.0 } }, - { { outputWidth / 2.0, outputHeight / 2.0 }, { 1.0, 0.0 } }, - { { -outputWidth / 2.0, outputHeight / 2.0 }, { 0.0, 0.0 } } - }; - - MTLViewport viewport = { - 0, 0, outputWidth, outputHeight, -1.0, 1.0 - }; - [commandEncoder setViewport:viewport]; - - id pipelineState = [deviceCache pipelineStateWithRegistryID:sourceImages[0].deviceRegistryID - pixelFormat:pixelFormat]; - [commandEncoder setRenderPipelineState:pipelineState]; - - [commandEncoder setVertexBytes:vertices - length:sizeof(vertices) - atIndex:BVI_Vertices]; - - simd_uint2 viewportSize = { - (unsigned int)(outputWidth), - (unsigned int)(outputHeight) - }; - [commandEncoder setVertexBytes:&viewportSize - length:sizeof(viewportSize) - atIndex:BVI_ViewportSize]; - - [commandEncoder setFragmentTexture:inputTexture - atIndex:BTI_InputImage]; - - float fragmentBrightness = (float)brightness; - [commandEncoder setFragmentBytes:&fragmentBrightness - length:sizeof(fragmentBrightness) - atIndex:BFI_Brightness]; - - [commandEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip - vertexStart:0 - vertexCount:4]; - - [commandEncoder endEncoding]; - [commandBuffer commit]; - - [commandBuffer waitUntilCompleted]; - - [colorAttachmentDescriptor release]; - - [deviceCache returnCommandQueueToCache:commandQueue]; - - return YES; - + [self setStatus:[NSString stringWithFormat:@"目标 %u:已分析 %lu 帧",_analysisSettings.targetIndex+1,(unsigned long)_analyzedFrames]]; + _analyzing=NO;return YES; } - @end diff --git a/scripts/checkpoint.py b/scripts/checkpoint.py new file mode 100644 index 0000000..8248d17 --- /dev/null +++ b/scripts/checkpoint.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +"""Save only explicitly listed project files, under the shared dayu Git lock.""" +import argparse, hashlib, json, os, pathlib, subprocess, time +p=argparse.ArgumentParser();p.add_argument('message');p.add_argument('files',nargs='+');p.add_argument('--tag');a=p.parse_args() +root=pathlib.Path(__file__).resolve().parent.parent +run=lambda *args,**kw:subprocess.run(args,cwd=root,check=True,text=True,**kw) +remote=run('git','remote','get-url','origin',capture_output=True).stdout.strip() +if not remote.startswith('https://dayu.media/git/') or '@' in remote:raise SystemExit('Unexpected project remote') +lock=pathlib.Path('/Users/dayu/.codex/git-backup-locks')/(hashlib.sha256(remote.lower().rstrip('/').encode()).hexdigest()+'.lock') +lock.parent.mkdir(parents=True,exist_ok=True) +for n in range(120): + try:lock.mkdir();break + except FileExistsError: + if n==119:raise SystemExit('Repository lock still held; inspect owner.json') + time.sleep(.5) +(lock/'owner.json').write_text(json.dumps({'pid':os.getpid(),'cwd':str(root),'remote':remote})) +try: + run('git','add','--',*a.files) + run('git','diff','--cached','--check') + run('git','diff','--cached','--stat') + changed=subprocess.run(['git','diff','--cached','--quiet'],cwd=root).returncode + if changed:run('git','commit','-m',a.message) + branch=run('git','branch','--show-current',capture_output=True).stdout.strip() + if not branch:raise SystemExit('Detached HEAD: create a durable branch before saving') + run('git','push','-u','origin',branch) + sha=run('git','rev-parse','HEAD',capture_output=True).stdout.strip() + observed=run('git','ls-remote','origin','refs/heads/'+branch,capture_output=True).stdout.split()[0] + if observed!=sha:raise SystemExit('Remote checkpoint SHA mismatch') + if a.tag: + run('git','tag',a.tag) + run('git','push','origin','refs/tags/'+a.tag) + observed=run('git','ls-remote','origin','refs/tags/'+a.tag,capture_output=True).stdout.split()[0] + if observed!=sha:raise SystemExit('Remote tag SHA mismatch') + print(json.dumps({'branch':branch,'sha':sha,'remote_verified':True,'tag':a.tag})) +finally: + (lock/'owner.json').unlink();lock.rmdir()