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()