Fix FxPlug CI orientation and preserve incompatible legacy tracks; prepare tested 1.0.6 Release
这个提交包含在:
1 父节点
16525d62e5
当前提交
ad54726b17
18 个文件已更改
+12145
-25
没有与搜索条件相匹配的文件
+211
-2
@@ -7,6 +7,9 @@
|
||||
#import <ImageIO/ImageIO.h>
|
||||
#import "beatygrilPlugIn.h"
|
||||
#import "PGParameterIDs.h"
|
||||
#import "PGFaceProcessor.h"
|
||||
#import "PGBeautyRenderer.h"
|
||||
#include <math.h>
|
||||
|
||||
// This adapter simulates host callbacks, but uses real IOSurface buffers and the
|
||||
// production FxPlug entry points. It does not simulate FCP plugin discovery/UI.
|
||||
@@ -173,6 +176,12 @@ static NSUInteger MaximumDifference(NSData *a, NSData *b) {
|
||||
for (NSUInteger i = 0; i < a.length; i++) maximum = MAX(maximum, (NSUInteger)abs(x[i] - y[i]));
|
||||
return maximum;
|
||||
}
|
||||
static double MeanDifference(NSData *a, NSData *b) {
|
||||
if (!a.length || a.length != b.length) return INFINITY;
|
||||
const uint8_t *x = a.bytes, *y = b.bytes; double total = 0;
|
||||
for (NSUInteger i = 0; i < a.length; i++) total += abs(x[i]-y[i]);
|
||||
return total/a.length;
|
||||
}
|
||||
static NSData *ExpectedCrop(PGHostTile *source, NSUInteger x, NSUInteger y, NSUInteger width, NSUInteger height, BOOL flip) {
|
||||
NSData *sourceBytes = SurfaceBytes(source); size_t sourceWidth = IOSurfaceGetWidth((__bridge IOSurfaceRef)source.storage);
|
||||
NSMutableData *result = [NSMutableData dataWithLength:width * height * 4];
|
||||
@@ -200,6 +209,204 @@ static void RejectState(beatygrilPlugIn *plugin, PGHostTile *destination, PGHost
|
||||
Check(!rendered && !escaped && error != nil, description);
|
||||
}
|
||||
static NSData *PlistState(id value) { return [NSPropertyListSerialization dataWithPropertyList:value format:NSPropertyListBinaryFormat_v1_0 options:0 error:nil]; }
|
||||
// Decode independently of CI/IOSurface, so a pair of adapter flips cannot
|
||||
// silently cancel in our test input and expected output. ImageIO + Quartz gives
|
||||
// the NASA fixture top-first BGRA bytes, also cross-checked against Pillow.
|
||||
static NSData *DecodedPortrait(NSString *path, size_t *width, size_t *height) {
|
||||
CGImageSourceRef source = CGImageSourceCreateWithURL((__bridge CFURLRef)[NSURL fileURLWithPath:path], NULL);
|
||||
CGImageRef image = source ? CGImageSourceCreateImageAtIndex(source, 0, NULL) : NULL;
|
||||
if (source) CFRelease(source);
|
||||
if (!image) return nil;
|
||||
*width = CGImageGetWidth(image); *height = CGImageGetHeight(image);
|
||||
NSMutableData *bytes = [NSMutableData dataWithLength:*width * *height * 4];
|
||||
CGColorSpaceRef space = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
|
||||
CGContextRef context = CGBitmapContextCreate(bytes.mutableBytes, *width, *height, 8, *width * 4, space,
|
||||
kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
|
||||
if (context) {
|
||||
CGContextSetBlendMode(context, kCGBlendModeCopy);
|
||||
CGContextSetInterpolationQuality(context, kCGInterpolationNone);
|
||||
CGContextDrawImage(context, CGRectMake(0, 0, *width, *height), image);
|
||||
CGContextRelease(context);
|
||||
}
|
||||
CGColorSpaceRelease(space); CGImageRelease(image);
|
||||
return context ? bytes : nil;
|
||||
}
|
||||
static BOOL StoreSurfaceBytes(PGHostTile *tile, NSData *bytes) {
|
||||
IOSurfaceRef surface = (__bridge IOSurfaceRef)tile.storage;
|
||||
size_t width = IOSurfaceGetWidth(surface), height = IOSurfaceGetHeight(surface);
|
||||
if (bytes.length != width * height * 4) return NO;
|
||||
IOSurfaceLock(surface, 0, NULL);
|
||||
for (size_t row = 0; row < height; row++)
|
||||
memcpy((uint8_t *)IOSurfaceGetBaseAddress(surface) + row * IOSurfaceGetBytesPerRow(surface),
|
||||
(const uint8_t *)bytes.bytes + row * width * 4, width * 4);
|
||||
IOSurfaceUnlock(surface, 0, NULL);
|
||||
return YES;
|
||||
}
|
||||
static CIContext *NumericContext(void) {
|
||||
return [CIContext contextWithMTLDevice:MTLCreateSystemDefaultDevice() options:@{
|
||||
kCIContextWorkingColorSpace:NSNull.null, kCIContextOutputColorSpace:NSNull.null}];
|
||||
}
|
||||
static NSData *ImageBytes(CIContext *context, CIImage *image) {
|
||||
if (!image) return nil;
|
||||
size_t width = (size_t)image.extent.size.width, height = (size_t)image.extent.size.height;
|
||||
NSMutableData *bytes = [NSMutableData dataWithLength:width * height * 4];
|
||||
[context render:image toBitmap:bytes.mutableBytes rowBytes:width * 4 bounds:image.extent format:kCIFormatBGRA8 colorSpace:nil];
|
||||
return bytes;
|
||||
}
|
||||
static CIImage *ProcessedReference(CIImage *image, PGBeautySettings settings, CMTime time, NSData *track) {
|
||||
PGFaceProcessor *faces = [PGFaceProcessor new];
|
||||
if (track) [faces setTrackingData:track forTarget:settings.targetIndex];
|
||||
NSError *error = nil;
|
||||
PGFaceFrame *frame = [faces processImage:image settings:settings time:time error:&error];
|
||||
if (!frame) return nil;
|
||||
return [[PGBeautyRenderer new] processImage:frame.image settings:settings skinMask:frame.skinMask
|
||||
colorMask:frame.colorMask teethMask:frame.teethMask eyeMask:frame.eyeMask error:&error];
|
||||
}
|
||||
static CGPoint LandmarkCenter(VNFaceLandmarkRegion2D *region, CGRect box, CGSize size) {
|
||||
CGPoint center = CGPointZero;
|
||||
if (!region.pointCount || !region.normalizedPoints) return center;
|
||||
for (NSUInteger i = 0; i < region.pointCount; i++) {
|
||||
center.x += region.normalizedPoints[i].x; center.y += region.normalizedPoints[i].y;
|
||||
}
|
||||
center.x /= region.pointCount; center.y /= region.pointCount;
|
||||
return CGPointMake((box.origin.x + center.x * box.size.width) * size.width,
|
||||
(1 - box.origin.y - center.y * box.size.height) * size.height);
|
||||
}
|
||||
static double ChangeMass(NSData *baseline, NSData *changed, size_t width, CGRect region) {
|
||||
if (baseline.length != changed.length || !width) return INFINITY;
|
||||
const uint8_t *a = baseline.bytes, *b = changed.bytes;
|
||||
size_t height = baseline.length / (width * 4); double total = 0;
|
||||
for (size_t row = 0; row < height; row++) for (size_t x = 0; x < width; x++)
|
||||
if (CGRectContainsPoint(region, CGPointMake(x + .5, row + .5))) {
|
||||
size_t i = (row * width + x) * 4;
|
||||
for (size_t c = 0; c < 3; c++) total += abs(a[i+c] - b[i+c]);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
static void PortraitAnatomyTests(NSString *fixture, CIImage *portrait) {
|
||||
size_t width = 0, height = 0;
|
||||
NSData *canonical = DecodedPortrait(fixture, &width, &height);
|
||||
Check(canonical != nil && width == 512 && height == 512, @"independent ImageIO/Quartz decodes the 512px NASA fixture");
|
||||
if (!canonical || width != 512 || height != 512 || !portrait) return;
|
||||
// Asymmetric samples pin the decoder's physical row/column order. These
|
||||
// values match Pillow's RGB decode exactly, independently of Core Image.
|
||||
const size_t positions[][2] = {{16,16},{100,250},{205,100},{222,178},{480,450}};
|
||||
const uint8_t samples[][4] = {{35,6,14,255},{93,132,234,255},{11,35,58,255},{42,73,102,255},{2,1,3,255}};
|
||||
BOOL samplesMatch = YES;
|
||||
for (size_t i = 0; i < 5; i++)
|
||||
samplesMatch &= memcmp((const uint8_t *)canonical.bytes + (positions[i][1]*width + positions[i][0])*4, samples[i], 4) == 0;
|
||||
Check(samplesMatch, @"independent asymmetric pixels establish top-first BGRA and the horizontal direction");
|
||||
CIContext *context = NumericContext();
|
||||
Check(MaximumDifference(canonical, ImageBytes(context, portrait)) <= 1, @"upright CI file decode agrees with independent physical source bytes");
|
||||
PGFaceFrame *detected = [[PGFaceProcessor new] processImage:portrait settings:PGBeautyDefaults() time:kCMTimeZero error:nil];
|
||||
VNFaceObservation *face = detected.faces.firstObject;
|
||||
Check(face.landmarks.allPoints.pointCount == 76, @"real portrait provides the 76-point anatomy used by chin pins");
|
||||
if (face.landmarks.allPoints.pointCount != 76) return;
|
||||
CGSize size = CGSizeMake(width, height); CGRect box = face.boundingBox;
|
||||
CGPoint left = LandmarkCenter(face.landmarks.leftEye, box, size), right = LandmarkCenter(face.landmarks.rightEye, box, size);
|
||||
CGPoint chin = face.landmarks.allPoints.normalizedPoints[67];
|
||||
chin = CGPointMake((box.origin.x + chin.x * box.size.width)*width, (1-box.origin.y-chin.y*box.size.height)*height);
|
||||
CGFloat fw = box.size.width*width, fh = box.size.height*height;
|
||||
CGRect leftEye = CGRectMake(left.x-fw*.24, left.y-fh*.17, fw*.48, fh*.34);
|
||||
CGRect rightEye = CGRectMake(right.x-fw*.24, right.y-fh*.17, fw*.48, fh*.34);
|
||||
CGRect chinArea = CGRectMake(chin.x-fw*.30, chin.y-fh*.22, fw*.60, fh*.44);
|
||||
CGRect upsideDownFace = CGRectMake(box.origin.x*width, box.origin.y*height, fw, fh);
|
||||
const UInt32 controls[] = {0, PGBigEye, PGChin, PGChin};
|
||||
const double values[] = {0, 1, 1, -1};
|
||||
NSArray<NSString *> *names = @[@"default", @"big-eye", @"chin-positive", @"chin-negative"];
|
||||
for (NSUInteger effect = 0; effect < 4; effect++) {
|
||||
PGBeautySettings settings = PGBeautyDefaults();
|
||||
if (effect == 1) settings.faceBigEye = 1;
|
||||
else if (effect > 1) settings.faceChin = values[effect];
|
||||
NSData *expected = ImageBytes(context, ProcessedReference(portrait, settings, kCMTimeZero, nil));
|
||||
Check(expected.length == canonical.length, [names[effect] stringByAppendingString:@" independent upright face/beauty chain renders"]);
|
||||
if (expected.length != canonical.length) continue;
|
||||
NSData *originBaseline = nil;
|
||||
for (NSUInteger sourceTop = 0; sourceTop < 2; sourceTop++) for (NSUInteger destinationTop = 0; destinationTop < 2; destinationTop++) {
|
||||
@autoreleasepool {
|
||||
PGHostAPI *api = [PGHostAPI new]; beatygrilPlugIn *plugin = [[beatygrilPlugIn alloc] initWithAPIManager:api];
|
||||
Check([plugin addParametersWithError:nil], @"anatomy case creates the real plugin parameters");
|
||||
if (effect) api.values[@(controls[effect])] = @(values[effect]);
|
||||
PGHostTile *input = Tile(width, height, 37, -21, sourceTop ? kFxImageOrigin_TOP_LEFT : kFxImageOrigin_BOTTOM_LEFT);
|
||||
PGHostTile *output = Tile(width, height, 37, -21, destinationTop ? kFxImageOrigin_TOP_LEFT : kFxImageOrigin_BOTTOM_LEFT);
|
||||
// imageOrigin changes coordinate semantics, never these bytes.
|
||||
Check(StoreSurfaceBytes(input, canonical), @"all origin combinations receive identical independently decoded physical rows");
|
||||
Check(Render(plugin, output, @[input], Snapshot(plugin, kCMTimeZero), kCMTimeZero, nil, NULL), @"active anatomy case renders with nonzero tile origin");
|
||||
NSData *actual = SurfaceBytes(output); NSUInteger difference = MaximumDifference(actual, expected);
|
||||
NSString *label = [NSString stringWithFormat:@"%@ source%@/destination%@", names[effect], sourceTop?@"TOP":@"BOTTOM", destinationTop?@"TOP":@"BOTTOM"];
|
||||
// Vision's file-image vs IOSurface path and translated float
|
||||
// coordinates can round by a few byte levels at high contrast.
|
||||
// Bound that error tightly; changing only origin tags must be
|
||||
// exactly invariant, independently of this reference tolerance.
|
||||
Check(difference <= 4 && MeanDifference(actual,expected) < .005, [label stringByAppendingString:@" matches independent upright CI output"]);
|
||||
if (!originBaseline) originBaseline = actual;
|
||||
Check(MaximumDifference(actual,originBaseline) == 0, [label stringByAppendingString:@" is byte-identical across coordinate tags"]);
|
||||
double lm = ChangeMass(canonical, actual, width, leftEye), rm = ChangeMass(canonical, actual, width, rightEye);
|
||||
double cm = ChangeMass(canonical, actual, width, chinArea), um = ChangeMass(canonical, actual, width, upsideDownFace);
|
||||
BOOL located = effect == 0 ? MaximumDifference(canonical, actual) <= 1 :
|
||||
(effect == 1 ? lm > 100 && rm > 100 && cm < (lm+rm)*.01 && um == 0 :
|
||||
cm > 100 && cm > 4*(lm+rm) && um == 0);
|
||||
Check(located, [label stringByAppendingString:effect == 1 ? @" changes the two real eye regions" : (effect ? @" changes the real chin rather than eyes or mirrored anatomy" : @" preserves the independently decoded photo")]);
|
||||
printf("Anatomy %-13s %lu/%lu max=%lu mean=%.7f eye=%.0f/%.0f chin=%.0f inverted=%.0f\n", names[effect].UTF8String,
|
||||
(unsigned long)sourceTop, (unsigned long)destinationTop, (unsigned long)difference, MeanDifference(actual,expected), lm, rm, cm, um);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
static void TrackingCoordinateTests(NSDictionary *archive, NSDictionary *hostValues, PGHostTile *source, CIImage *portrait, CMTime time) {
|
||||
Check([archive[@"hostCoordinateSpace"] isEqual:@"iosurface-ci-y-up-v1"], @"new host analysis stamps its explicit IOSurface/CI coordinate contract");
|
||||
NSMutableDictionary *forged = [NSPropertyListSerialization propertyListWithData:PlistState(archive)
|
||||
options:NSPropertyListMutableContainers format:nil error:nil];
|
||||
if (![forged[@"frames"] isKindOfClass:NSDictionary.class]) { Check(NO, @"analysis archive supplies frames for compatibility regression"); return; }
|
||||
// Retain fully valid topology but move the saved face away from the actual
|
||||
// portrait. A falsely replayed legacy archive must produce a visible error.
|
||||
for (NSMutableDictionary *frame in [forged[@"frames"] allValues]) for (NSMutableDictionary *face in frame[@"faces"]) {
|
||||
NSMutableArray *box = [face[@"box"] mutableCopy]; box[0] = @([box[0] doubleValue]+.2); box[1] = @([box[1] doubleValue]-.25); face[@"box"] = box;
|
||||
for (NSString *name in @[@"leftEye",@"rightEye",@"leftBrow",@"rightBrow",@"nose",@"lips",@"mouth",@"contour",@"points76"]) {
|
||||
if (!face[name]) continue;
|
||||
NSMutableArray *points = [NSMutableArray new];
|
||||
for (NSArray *point in face[name]) [points addObject:@[@([point[0] doubleValue]+.2),@([point[1] doubleValue]-.25)]];
|
||||
face[name] = points;
|
||||
}
|
||||
}
|
||||
[forged removeObjectForKey:@"hostCoordinateSpace"];
|
||||
NSString *legacyString = [PlistState(forged) base64EncodedStringWithOptions:0];
|
||||
NSMutableDictionary *values = [hostValues mutableCopy];
|
||||
values[@(PGTarget)] = @2; values[@(PGTrackEnabled)] = @YES; values[@(PGBigEye)] = @1; values[@(PGChin)] = @1;
|
||||
values[@(PGTrackData3)] = legacyString;
|
||||
PGHostAPI *legacyAPI = [PGHostAPI new]; legacyAPI.hostTime = time;
|
||||
beatygrilPlugIn *legacy = [[beatygrilPlugIn alloc] initWithAPIManager:legacyAPI]; [legacy addParametersWithError:nil]; legacyAPI.values = [values mutableCopy];
|
||||
PGHostTile *output = Tile(512,512,0,0,kFxImageOrigin_TOP_LEFT);
|
||||
Check(Render(legacy, output, @[source], Snapshot(legacy,time), time, nil, NULL), @"unmarked old coordinate archive renders through the real adapter");
|
||||
NSData *legacyBytes = SurfaceBytes(output);
|
||||
PGBeautySettings realtime = [legacy settingsAtTime:time]; realtime.trackingEnabled = 0;
|
||||
CIContext *context = NumericContext();
|
||||
NSData *expectedLive = ImageBytes(context, ProcessedReference(portrait,realtime,time,nil));
|
||||
printf("Legacy fallback upright comparison: max=%lu mean=%.7f\n",(unsigned long)MaximumDifference(legacyBytes,expectedLive),MeanDifference(legacyBytes,expectedLive));
|
||||
Check(expectedLive.length && MaximumDifference(legacyBytes,expectedLive) <= 4 && MeanDifference(legacyBytes,expectedLive) < .005, @"unmarked forged face geometry falls back to realtime upright detection");
|
||||
Check([legacyAPI.values[@(PGTrackData3)] isEqualToString:legacyString] && [legacyAPI.values[@(PGTrackEnabled)] boolValue], @"legacy fallback preserves the user's original archive string and enabled parameter");
|
||||
|
||||
forged[@"hostCoordinateSpace"] = @"iosurface-ci-y-up-v1";
|
||||
NSData *trustedTrack = PlistState(forged);
|
||||
PGHostAPI *trustedAPI = [PGHostAPI new]; trustedAPI.hostTime = time;
|
||||
beatygrilPlugIn *trusted = [[beatygrilPlugIn alloc] initWithAPIManager:trustedAPI]; [trusted addParametersWithError:nil];
|
||||
trustedAPI.values = [values mutableCopy]; trustedAPI.values[@(PGTrackData3)] = [trustedTrack base64EncodedStringWithOptions:0];
|
||||
Check(Render(trusted, output, @[source], Snapshot(trusted,time), time, nil, NULL), @"fresh plugin restores a marked coordinate archive");
|
||||
NSData *trustedBytes = SurfaceBytes(output);
|
||||
PGBeautySettings tracked = [trusted settingsAtTime:time];
|
||||
NSData *expectedSaved = ImageBytes(context, ProcessedReference(portrait,tracked,time,trustedTrack));
|
||||
Check(expectedSaved.length && MaximumDifference(trustedBytes,expectedSaved) <= 1, @"marked saved geometry matches independent exact-time archive replay");
|
||||
Check(MaximumDifference(trustedBytes,expectedLive) > 5, @"forged marked geometry is observably different, making the legacy non-replay assertion meaningful");
|
||||
|
||||
// Reanalysis of an unmarked target starts a fresh compatible archive.
|
||||
[legacy analyzeForward];
|
||||
CMTime next = CMTimeAdd(time,CMTimeMake(10,24));
|
||||
[legacy setupAnalysisForTimeRange:CMTimeRangeMake(time,CMTimeMake(240,24)) frameDuration:CMTimeMake(1,24) error:nil];
|
||||
BOOL analyzed = [legacy analyzeFrame:source atTime:next error:nil] && [legacy cleanupAnalysis:nil];
|
||||
NSData *resaved = [[NSData alloc] initWithBase64EncodedString:legacyAPI.values[@(PGTrackData3)] options:0];
|
||||
NSDictionary *fresh = resaved ? [NSPropertyListSerialization propertyListWithData:resaved options:0 format:nil error:nil] : nil;
|
||||
Check(analyzed && [fresh[@"frames"] count] == 1 && [fresh[@"hostCoordinateSpace"] isEqual:@"iosurface-ci-y-up-v1"], @"reanalysis replaces incompatible geometry with one newly marked frame");
|
||||
}
|
||||
static void WritePortrait(PGHostTile *tile, CIImage *image) {
|
||||
CIContext *context = [CIContext contextWithOptions:@{kCIContextWorkingColorSpace:NSNull.null, kCIContextOutputColorSpace:NSNull.null}];
|
||||
[context render:image toIOSurface:(__bridge IOSurfaceRef)tile.storage bounds:image.extent colorSpace:nil];
|
||||
@@ -283,7 +490,7 @@ int main(int argc, const char *argv[]) {
|
||||
Check(MaximumDifference(SurfaceBytes(input), SurfaceBytes(output)) <= 1, top ? @"top-left tile preserves sample rows at nonzero origin" : @"bottom-left tile preserves sample rows at nonzero origin");
|
||||
output.origin = top ? kFxImageOrigin_BOTTOM_LEFT : kFxImageOrigin_TOP_LEFT;
|
||||
Check(Render(plugin, output, @[input], state, kCMTimeZero, &error, NULL), @"opposite-origin tile renders");
|
||||
Check(MaximumDifference(ExpectedCrop(input, 0, 0, 48, 32, YES), SurfaceBytes(output)) <= 1, @"opposite image origins flip rows exactly once");
|
||||
Check(MaximumDifference(SurfaceBytes(input), SurfaceBytes(output)) <= 1, @"opposite coordinate tags preserve the same physical IOSurface rows");
|
||||
}
|
||||
PGHostTile *full = Tile(48, 32, 37, -21, kFxImageOrigin_BOTTOM_LEFT), *crop = Tile(16, 12, 43, -17, kFxImageOrigin_BOTTOM_LEFT); Fill(full);
|
||||
Check(Render(plugin, crop, @[full], state, kCMTimeZero, &error, NULL), @"destination sub-tile renders from full input buffer");
|
||||
@@ -298,11 +505,12 @@ int main(int argc, const char *argv[]) {
|
||||
Check(cropDifference <= 1, @"destination sub-tile translates global pixel bounds correctly");
|
||||
full.origin = kFxImageOrigin_TOP_LEFT; crop.origin = kFxImageOrigin_TOP_LEFT;
|
||||
Check(Render(plugin, crop, @[full], state, kCMTimeZero, &error, NULL), @"top-left destination sub-tile renders");
|
||||
Check(MaximumDifference(ExpectedCrop(full, 6, 4, 16, 12, NO), SurfaceBytes(crop)) <= 1, @"top-left sub-tile selects correct physical source rows");
|
||||
Check(MaximumDifference(ExpectedCrop(full, 6, 16, 16, 12, NO), SurfaceBytes(crop)) <= 1, @"top-left coordinate tag does not reverse the sub-tile's physical source rows");
|
||||
|
||||
NSString *fixture = argc > 1 ? @(argv[1]) : @"tests/fixtures/astronaut.png";
|
||||
CIImage *portrait = [CIImage imageWithContentsOfURL:[NSURL fileURLWithPath:fixture] options:@{kCIImageColorSpace:NSNull.null}];
|
||||
Check(portrait != nil, @"NASA portrait opens for full adapter test");
|
||||
PortraitAnatomyTests(fixture, portrait);
|
||||
PGHostTile *portraitSource = Tile(512, 512, 0, 0, kFxImageOrigin_BOTTOM_LEFT), *portraitOutput = Tile(512, 512, 0, 0, kFxImageOrigin_BOTTOM_LEFT);
|
||||
WritePortrait(portraitSource, portrait);
|
||||
api.values[@(PGThin)] = @0.8; api.values[@(PGBigEye)] = @0.6; api.values[@(PGMidtone)] = @0.8; api.values[@(PGBrightness)] = @0.25; api.values[@(PGMix)] = @0;
|
||||
@@ -339,6 +547,7 @@ int main(int argc, const char *argv[]) {
|
||||
NSData *tracking = [[NSData alloc] initWithBase64EncodedString:target options:0];
|
||||
NSDictionary *trackingArchive = tracking ? [NSPropertyListSerialization propertyListWithData:tracking options:0 format:nil error:nil] : nil;
|
||||
Check([trackingArchive[@"frames"] count] == 2, @"old render snapshot does not erase either newly analyzed frame");
|
||||
TrackingCoordinateTests(trackingArchive,api.values,portraitSource,portrait,api.hostTime);
|
||||
Check([api.values[@(PGTrackEnabled)] boolValue] && CMTimeCompare(api.lastSetBoolTime, api.hostTime) == 0, @"analysis enables saved target at host seed time");
|
||||
NSData *trackedState = Snapshot(plugin, api.hostTime);
|
||||
beatygrilPlugIn *restoredPlugin = [[beatygrilPlugIn alloc] initWithAPIManager:api];
|
||||
|
||||
在新工单中引用
屏蔽一个用户