WIP: add portrait FxPlug controls, render adapter and face selection OSC

这个提交包含在:
dayu 于 2026-10-21 17:12:45 +08:00 提交
1 父节点 f71826b64f
当前提交 e2803e727f
12 个文件已更改
+476 -814

没有与搜索条件相匹配的文件

+24 -18
查看文件
@@ -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;
+27 -4
查看文件
@@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>beatygrilXPCService</string>
<string>大鱼人像美化</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
@@ -48,7 +48,7 @@
<array>
<dict>
<key>groupName</key>
<string>beatygril Group</string>
<string>大鱼</string>
<key>uuid</key>
<string>0461C6B0-B8B7-4DE8-B882-03C635553ECB</string>
</dict>
@@ -59,20 +59,41 @@
<key>className</key>
<string>beatygrilPlugIn</string>
<key>displayName</key>
<string>beatygril::FxPlug Name</string>
<string>大鱼人像美化</string>
<key>group</key>
<string>0461C6B0-B8B7-4DE8-B882-03C635553ECB</string>
<key>infoString</key>
<string>beatygril::FxPlug Description</string>
<string>人像磨皮、肤色、脸型和人脸跟踪</string>
<key>protocolNames</key>
<array>
<string>FxFilter</string>
<string>FxAnalyzer</string>
</array>
<key>uuid</key>
<string>FB14B0E6-59B2-4836-916F-CD0C559E9241</string>
<key>version</key>
<string>1.0</string>
</dict>
<dict>
<key>className</key>
<string>PGFaceSelectOSC</string>
<key>displayName</key>
<string>人脸选择</string>
<key>group</key>
<string>0461C6B0-B8B7-4DE8-B882-03C635553ECB</string>
<key>uuid</key>
<string>E0E432C8-3D71-4ABA-B006-E7F519E06C87</string>
<key>version</key>
<string>1.0</string>
<key>protocolNames</key>
<array>
<string>FxOnScreenControl</string>
</array>
<key>supportedPlugins</key>
<array>
<string>FB14B0E6-59B2-4836-916F-CD0C559E9241</string>
</array>
</dict>
</array>
<key>XPCService</key>
<dict>
@@ -88,5 +109,7 @@
<true/>
</dict>
</dict>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
</dict>
</plist>
@@ -1,28 +0,0 @@
//
// MetalDeviceCache.h
// beatygril
//
// Created by dayu on 2026/10/21.
//
#import <Metal/Metal.h>
#import <FxPlug/FxPlugSDK.h>
@class MetalDeviceCacheItem;
@interface MetalDeviceCache : NSObject
{
NSMutableArray<MetalDeviceCacheItem*>* deviceCaches;
}
+ (MetalDeviceCache*)deviceCache;
+ (MTLPixelFormat)MTLPixelFormatForImageTile:(FxImageTile*)imageTile;
- (id<MTLDevice>)deviceWithRegistryID:(uint64_t)registryID;
- (id<MTLRenderPipelineState>)pipelineStateWithRegistryID:(uint64_t)registryID
pixelFormat:(MTLPixelFormat)pixFormat;
- (id<MTLCommandQueue>)commandQueueWithRegistryID:(uint64_t)registryID
pixelFormat:(MTLPixelFormat)pixFormat;
- (void)returnCommandQueueToCache:(id<MTLCommandQueue>)commandQueue;
@end
@@ -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<MTLDevice> gpuDevice;
@property (readonly) id<MTLRenderPipelineState> pipelineState;
@property (retain) NSMutableArray<NSMutableDictionary*>* commandQueueCache;
@property (readonly) NSLock* commandQueueCacheLock;
@property (readonly) MTLPixelFormat pixelFormat;
- (instancetype)initWithDevice:(id<MTLDevice>)device
pixelFormat:(MTLPixelFormat)pixFormat;
- (id<MTLCommandQueue>)getNextFreeCommandQueue;
- (void)returnCommandQueue:(id<MTLCommandQueue>)commandQueue;
- (BOOL)containsCommandQueue:(id<MTLCommandQueue>)commandQueue;
@end
@implementation MetalDeviceCacheItem
- (instancetype)initWithDevice:(id<MTLDevice>)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<MTLCommandQueue> 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<MTLLibrary> defaultLibrary = [[_gpuDevice newDefaultLibrary] autorelease];
// Load the vertex function from the library
id<MTLFunction> vertexFunction = [[defaultLibrary newFunctionWithName:@"vertexShader"] autorelease];
// Load the fragment function from the library
id<MTLFunction> 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<MTLCommandQueue>)getNextFreeCommandQueue
{
id<MTLCommandQueue> 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<MTLCommandQueue>)commandQueue
{
[_commandQueueCacheLock lock];
BOOL found = false;
NSUInteger index = 0;
while ((!found) && (index < kMaxCommandQueues))
{
NSMutableDictionary* nextCommandQueuDict = [_commandQueueCache objectAtIndex:index];
id<MTLCommandQueue> nextCommandQueue = [nextCommandQueuDict objectForKey:kKey_CommandQueue];
if (nextCommandQueue == commandQueue)
{
found = YES;
[nextCommandQueuDict setObject:[NSNumber numberWithBool:NO]
forKey:kKey_InUse];
}
index++;
}
[_commandQueueCacheLock unlock];
}
- (BOOL)containsCommandQueue:(id<MTLCommandQueue>)commandQueue
{
BOOL found = NO;
NSUInteger index = 0;
while ((!found) && (index < kMaxCommandQueues))
{
NSMutableDictionary* nextCommandQueuDict = [_commandQueueCache objectAtIndex:index];
id<MTLCommandQueue> 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<id<MTLDevice>>* devices = MTLCopyAllDevices();
deviceCaches = [[NSMutableArray alloc] initWithCapacity:devices.count];
for (id<MTLDevice> 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<MTLDevice>)deviceWithRegistryID:(uint64_t)registryID
{
for (MetalDeviceCacheItem* nextCacheItem in deviceCaches)
{
if (nextCacheItem.gpuDevice.registryID == registryID)
{
return nextCacheItem.gpuDevice;
}
}
return nil;
}
- (id<MTLRenderPipelineState>)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<id<MTLDevice>>* devices = MTLCopyAllDevices();
id<MTLDevice> device = nil;
for (id<MTLDevice> nextDevice in devices)
{
if (nextDevice.registryID == registryID)
{
device = nextDevice;
}
}
id<MTLRenderPipelineState> 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<MTLCommandQueue>)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<id<MTLDevice>>* devices = MTLCopyAllDevices();
id<MTLDevice> device = nil;
for (id<MTLDevice> nextDevice in devices)
{
if (nextDevice.registryID == registryID)
{
device = nextDevice;
}
}
id<MTLCommandQueue> 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<MTLCommandQueue>)commandQueue;
{
for (MetalDeviceCacheItem* nextCacheItem in deviceCaches)
{
if ([nextCacheItem containsCommandQueue:commandQueue])
{
[nextCacheItem returnCommandQueue:commandQueue];
break;
}
}
}
@end
@@ -0,0 +1,30 @@
#pragma once
#include <stdint.h>
// 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;
}
@@ -0,0 +1,64 @@
#import <FxPlug/FxPlugSDK.h>
#import <CoreImage/CoreImage.h>
#import <IOSurface/IOSurface.h>
#import "PGParameterIDs.h"
@interface PGFaceSelectOSC : NSObject <FxOnScreenControl_v4>
@property (assign) id<PROAPIAccessing> apiManager;
@end
@implementation PGFaceSelectOSC
- (instancetype)initWithAPIManager:(id<PROAPIAccessing>)api {
if((self=[super init])) _apiManager=api; return self;
}
- (FxDrawingCoordinates)drawingCoordinates {return kFxDrawingCoordinates_OBJECT;}
- (id<FxOnScreenControlAPI_v4>)oscAPI {return [_apiManager apiForProtocol:@protocol(FxOnScreenControlAPI_v4)];}
- (BOOL)selectionEnabledAtTime:(CMTime)time {
BOOL selected=NO;id<FxParameterRetrievalAPI_v6> 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<FxParameterRetrievalAPI_v6> 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<FxParameterSettingAPI_v5> 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<FxUndoAPI> 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<FxUndoAPI> 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
@@ -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
};
@@ -1,83 +0,0 @@
//
// TileableRemoteBrightness.metal
// beatygril
//
// Created by dayu on 2026/10/21.
//
#include <metal_stdlib>
#include <simd/simd.h>
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<half> 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<half>(*brightness);
colorSample.rgb = colorSample.rgb * hBrightness;
// We return the color of the texture
return float4(colorSample);
}
@@ -1,32 +0,0 @@
//
// TileableRemoteBrightnessShaderTypes.h
// beatygril
//
// Created by dayu on 2026/10/21.
//
#ifndef TileableRemoteBrightnessShaderTypes_h
#define TileableRemoteBrightnessShaderTypes_h
#import <simd/simd.h>
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 */
+7 -9
查看文件
@@ -1,13 +1,11 @@
//
// beatygrilPlugIn.h
// beatygril
//
// Created by dayu on 2026/10/21.
//
#import <Foundation/Foundation.h>
#import <FxPlug/FxPlugSDK.h>
#import "PGBeautyTypes.h"
@interface beatygrilPlugIn : NSObject <FxTileableEffect>
@property (assign) id<PROAPIAccessing> apiManager;
@interface beatygrilPlugIn : NSObject <FxTileableEffect, FxAnalyzer>
@property (nonatomic, assign) id<PROAPIAccessing> apiManager;
- (PGBeautySettings)settingsAtTime:(CMTime)time;
- (void)analyzeForward;
- (void)analyzeBackward;
- (void)clearTracking;
@end
+271 -301
查看文件
@@ -1,320 +1,290 @@
//
// beatygrilPlugIn.m
// beatygril
//
// Created by dayu on 2026/10/21.
//
#import "beatygrilPlugIn.h"
#import <IOSurface/IOSurfaceObjC.h>
#import "TileableRemoteBrightnessShaderTypes.h"
#import "MetalDeviceCache.h"
#import "PGParameterIDs.h"
#import "PGBeautyRenderer.h"
#import "PGFaceProcessor.h"
#import <Metal/Metal.h>
#import <IOSurface/IOSurface.h>
#import <stddef.h>
#import <math.h>
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<NSNumber *,CIContext *> *contexts;
@property PGBeautySettings analysisSettings;
@property CMTime analysisTime;
@property NSUInteger analyzedFrames;
@property BOOL analyzing;
@property (strong) NSArray<NSString *> *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<PROAPIAccessing>)newApiManager;
{
self = [super init];
if (self != nil)
{
_apiManager = newApiManager;
- (instancetype)initWithAPIManager:(id<PROAPIAccessing>)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<FxParameterCreationAPI_v5> 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<FxParameterCreationAPI_v5>)api {
PGBeautySettings defaults=PGBeautyDefaults();
for (NSUInteger i=0;i<sizeof(PGSliders)/sizeof(PGSlider);i++) {
PGSlider spec=PGSliders[i]; if (spec.identifier!=identifier) continue;
NSString *name=[NSString stringWithUTF8String:spec.name];
float value=*(float *)((uint8_t *)&defaults+spec.offset);
if (spec.percent) return [api addPercentSliderWithName:name parameterID:identifier defaultValue:value parameterMin:spec.min parameterMax:spec.max sliderMin:spec.min sliderMax:spec.max delta:spec.step parameterFlags:kFxParameterFlag_DEFAULT];
return [api addFloatSliderWithName:name parameterID:identifier defaultValue:value parameterMin:spec.min parameterMax:spec.max sliderMin:spec.min sliderMax:spec.max delta:spec.step parameterFlags:kFxParameterFlag_DEFAULT];
}
if (![paramAPI addFloatSliderWithName:@"Brightness"
parameterID:1
defaultValue:1.0
parameterMin:0.0
parameterMax:100.0
sliderMin:0.0
sliderMax:10.0
delta:0.1
parameterFlags:kFxParameterFlag_DEFAULT])
{
NSDictionary* userInfo = @{
NSLocalizedDescriptionKey : @"Unable to add brightness slider"
};
if (error != NULL)
*error = [NSError errorWithDomain:FxPlugErrorDomain
code:kFxError_InvalidParameter
userInfo:userInfo];
return NO;
}
- (BOOL)addParametersWithError:(NSError **)error {
id<FxParameterCreationAPI_v5> 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<sizeof(shape)/sizeof(UInt32);i++) slider(shape[i]); end();
group(@"人脸选择与跟踪",104); toggle(@"只处理选中人脸",PGSingleFace,NO);
ok=[api addPointParameterWithName:@"选脸位置" parameterID:PGSelection defaultX:.5 defaultY:.5 parameterFlags:0]&&ok;
toggle(@"多人高速模式",PGFastMulti,NO); slider(PGJumpGuard);
toggle(@"使用跟踪数据",PGTrackEnabled,NO);
ok=[api addPopupMenuWithName:@"跟踪目标" parameterID:PGTarget defaultValue:0 menuEntries:@[@"目标 1",@"目标 2",@"目标 3"] parameterFlags:kFxParameterFlag_NOT_ANIMATABLE]&&ok;
ok=[api addPushButtonWithName:@"向前分析" parameterID:PGAnalyzeForward selector:@selector(analyzeForward) parameterFlags:0]&&ok;
ok=[api addPushButtonWithName:@"向后分析" parameterID:PGAnalyzeBackward selector:@selector(analyzeBackward) parameterFlags:0]&&ok;
ok=[api addPushButtonWithName:@"清除当前目标" parameterID:PGClearTrack selector:@selector(clearTracking) parameterFlags:0]&&ok;
ok=[api addStringParameterWithName:@"跟踪状态" parameterID:PGStatus defaultValue:@"选择画面中的人脸后开始分析" parameterFlags:kFxParameterFlag_NOT_ANIMATABLE|kFxParameterFlag_DISABLED]&&ok;
toggle(@"显示人脸框",PGFaceBoxes,NO); toggle(@"显示跟踪框",PGTrackBoxes,NO); end();
group(@"蒙版与输出",105); slider(PGThreshold); slider(PGSoftness); slider(PGEdgeProtect);
toggle(@"预览蒙版",PGShowMask,NO); toggle(@"反转蒙版",PGInvertMask,NO);
ok=[api addPopupMenuWithName:@"蒙版通道" parameterID:PGDebugChannel defaultValue:0 menuEntries:@[@"皮肤",@"牙齿",@"眼睛"] parameterFlags:0]&&ok;
slider(PGMix); end();
for(UInt32 identifier=PGTrackData1;identifier<=PGTrackData3;identifier++)
ok=[api addStringParameterWithName:@"分析数据" parameterID:identifier defaultValue:@"" parameterFlags:kFxParameterFlag_HIDDEN|kFxParameterFlag_NOT_ANIMATABLE]&&ok;
return ok ? YES : PGFail(error,@"宿主未能创建完整的人像美化参数。");
}
- (PGBeautySettings)settingsAtTime:(CMTime)time {
PGBeautySettings s=PGBeautyDefaults();
id<FxParameterRetrievalAPI_v6> api=[_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)];
if(!api) return s;
for(NSUInteger i=0;i<sizeof(PGSliders)/sizeof(PGSlider);i++) {
PGSlider spec=PGSliders[i]; double value=*(float *)((uint8_t *)&s+spec.offset);
if([api getFloatValue:&value fromParameter:spec.identifier atTime:time] && isfinite(value))
*(float *)((uint8_t *)&s+spec.offset)=(float)fmin(spec.max,fmax(spec.min,value));
}
struct {UInt32 identifier;size_t offset;} toggles[]={
{PGFeatureProtect,offsetof(PGBeautySettings,featureProtection)}, {PGShowMask,offsetof(PGBeautySettings,showMask)},
{PGInvertMask,offsetof(PGBeautySettings,invertMask)}, {PGFaceBoxes,offsetof(PGBeautySettings,showFaceBoxes)},
{PGTrackBoxes,offsetof(PGBeautySettings,showTrackBox)}, {PGSingleFace,offsetof(PGBeautySettings,singleFace)},
{PGFastMulti,offsetof(PGBeautySettings,fastMultiFace)}, {PGTrackEnabled,offsetof(PGBeautySettings,trackingEnabled)}};
for(NSUInteger i=0;i<sizeof(toggles)/sizeof(toggles[0]);i++) {
BOOL value=*(uint32_t *)((uint8_t *)&s+toggles[i].offset)!=0;
[api getBoolValue:&value fromParameter:toggles[i].identifier atTime:time];
*(uint32_t *)((uint8_t *)&s+toggles[i].offset)=value;
}
int value=0; [api getIntValue:&value fromParameter:PGTonePreset atTime:time]; s.skinTonePreset=MIN(6,MAX(0,value));
value=0; [api getIntValue:&value fromParameter:PGDebugChannel atTime:time]; s.debugChannel=MIN(2,MAX(0,value));
value=0; [api getIntValue:&value fromParameter:PGTarget atTime:time]; s.targetIndex=MIN(2,MAX(0,value));
double x=.5,y=.5; [api getXValue:&x YValue:&y fromParameter:PGSelection atTime:time];
s.selectionX=isfinite(x)?fmin(1,fmax(0,x)):.5; s.selectionY=isfinite(y)?fmin(1,fmax(0,y)):.5;
BOOL enabled=YES;
[api getBoolValue:&enabled fromParameter:PGSmoothingEnabled atTime:time]; if(!enabled) s.amount=s.midtoneDetail=0;
enabled=YES; [api getBoolValue:&enabled fromParameter:PGColorEnabled atTime:time];
if(!enabled) {s.skinBrightness=s.skinHue=s.skinSaturation=s.skinEvenTone=s.teethWhitening=s.eyeBrightening=0;s.skinTonePreset=0;}
enabled=YES; [api getBoolValue:&enabled fromParameter:PGDetailEnabled atTime:time]; if(!enabled) s.highPassDetail=s.sharpness=0;
enabled=YES; [api getBoolValue:&enabled fromParameter:PGShapeEnabled atTime:time];
if(!enabled) s.faceThin=s.faceBigEye=s.faceShort=s.faceNarrow=s.faceChin=s.faceV=s.faceJaw=s.faceCheekbone=s.faceSmallHead=0;
if(s.fastMultiFace) s.singleFace=0;
return s;
}
- (NSArray<NSString *> *)trackStrings {
id<FxParameterRetrievalAPI_v6> 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<FxImageTile *> *)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<FxImageTile *> *)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<MTLDevice> device=nil;
for(id<MTLDevice> 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];
}
}
- (BOOL)renderDestinationImage:(FxImageTile *)destination sourceImages:(NSArray<FxImageTile *> *)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;
}
}
//---------------------------------------------------------
// pluginState:atTime:quality:error
//
// Your plug-in should get its parameter values, do any calculations it needs to
// from those values, and package up the result to be used later with rendering.
// The host application will call this method before rendering. The
// FxParameterRetrievalAPI* is valid during this call. Use it to get the values of
// your plug-in's parameters, then put those values or the results of any calculations
// you need to do with those parameters to render into an NSData that you return
// to the host application. The host will pass it back to you during subsequent calls.
// Do not re-use the NSData; always create a new one as this method may be called
// on multiple threads at the same time.
//---------------------------------------------------------
- (BOOL)pluginState:(NSData**)pluginState
atTime:(CMTime)renderTime
quality:(FxQuality)qualityLevel
error:(NSError**)error
{
BOOL succeeded = NO;
id<FxParameterRetrievalAPI_v6> 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;
}
}
else
{
if (error != NULL)
*error = [NSError errorWithDomain:FxPlugErrorDomain
code:kFxError_ThirdPartyDeveloperStart + 20
userInfo:@{
NSLocalizedDescriptionKey :
@"Unable to retrieve FxParameterRetrievalAPI_v6 in \
[-pluginStateAtTime:]" }];
}
return succeeded;
- (void)setStatus:(NSString *)status {
id<FxParameterSettingAPI_v5> api=[_apiManager apiForProtocol:@protocol(FxParameterSettingAPI_v5)];
[api setStringParameterValue:status toParameter:PGStatus];
}
//---------------------------------------------------------
// 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<FxImageTile *> *)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;
}
// 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;
- (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<FxAnalysisAPI> 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?:@"当前宿主不提供帧分析接口。")];
}
//---------------------------------------------------------
// 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<FxImageTile *> *)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)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<FxParameterSettingAPI_v5> api=[_apiManager apiForProtocol:@protocol(FxParameterSettingAPI_v5)];
[api setStringParameterValue:@"" toParameter:PGTrackData1+s.targetIndex];
[self setStatus:@"已清除当前目标的分析数据"];
}
//---------------------------------------------------------
// 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<FxImageTile *> *)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;
- (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<FxParameterSettingAPI_v5> 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<MTLCommandQueue> commandQueue = [deviceCache commandQueueWithRegistryID:sourceImages[0].deviceRegistryID
pixelFormat:pixelFormat];
if (commandQueue == nil)
{
return NO;
}
id<MTLCommandBuffer> commandBuffer = [commandQueue commandBuffer];
commandBuffer.label = @"DynamicRegXPC Command Buffer";
[commandBuffer enqueue];
id<MTLTexture> inputTexture = [sourceImages[0] metalTextureForDevice:[deviceCache deviceWithRegistryID:sourceImages[0].deviceRegistryID]];
id<MTLTexture> 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<MTLRenderCommandEncoder> 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<MTLRenderPipelineState> 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
+36
查看文件
@@ -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()