diff --git a/README.md b/README.md
index cd7b943..689be5d 100644
--- a/README.md
+++ b/README.md
@@ -44,3 +44,9 @@ xcodebuild -project beatygril/beatygril.xcodeproj -scheme 'Wrapper Application'
`./scripts/test-inspector.sh`验证参数路由、保存恢复、事务、绕过/重置和离屏布局;`python3 scripts/inspector-template.py audit Templates/大鱼人像美化/大鱼人像美化.moef`验证模板自定义数据和发布入口。离屏截图和模拟宿主检查不能替代Motion/FCP的实际嵌入、键盘/VoiceOver和工程重开验收,仍由用户自测。完整1039项回归通过,Inspector/宿主适配/OSC启用ASan。离屏演示图见[面板预览](docs/verification/v1.1.0/inspector-preview.png)。1.1.0(9)已完成通用Release正式签名、Apple公证、票据粘附、唯一正式目录安装及配套模板同步。签名源码`f79d24eb0185f31feaae58b83b632f92dcb3ca91`的111文件及16项必要资源已独立远端恢复核验,clone内134项Inspector ASan和模板审计通过。
正式安装回执见`docs/verification/v1.1.0/installed-verification.json`,恢复标签为`v1.1.0`。签名、票据、Gatekeeper、双架构身份、PluginKit正式路径及配套模板字节核验通过;已清理本次临时APP、旧APP备份和公证ZIP。有界扫描只发现正式路径的产品外层APP,未把该范围表述为所有卷的完整扫描。未操作Motion/FCP界面,实际面板嵌入与工程重开由用户自测。
+
+## 1.1.2 面板交互与下半脸定位修复
+
+面板改为先在真实视图的宿主 action 内取得读写接口,修复控件一直禁用的问题。短脸改用76点下半脸闭合区域和鼻梁位移轴,下颌骨改为六组相邻轮廓半径的局部位移,移除旧脸框近似场;详见[分析](docs/analysis/短脸与下颌骨分析.md)。其余五项脸型仍保留近似实现,不能将局部算法恢复视为全部效果逐像素一致。
+
+1535项回归通过,含335项宿主适配、98项OSC和141项面板ASan检查;28种控制/原点组合均核验实际解剖位置,改变原点标签时逐字节一致。额外真实帧ASan专项886项通过,含重复基础用例。合并版签名公证及Motion/FCP原生验收进行中。
diff --git a/RECOVERY.md b/RECOVERY.md
index ee49acc..99d24ab 100644
--- a/RECOVERY.md
+++ b/RECOVERY.md
@@ -74,3 +74,7 @@ FCP模板从 `Templates/大鱼人像美化/` 恢复,将 `.moef` 和 `Drop Zone
1.1.0(9)现已安装到固定正式路径;APP与嵌套插件为arm64/x86_64通用版本,Developer ID团队、可信时间戳、Hardened Runtime、严格签名、公证票据、Gatekeeper及PluginKit正式路径全部核验通过。配套`.moef` SHA256为`e1c0cff1dd3541ed97cab2cb39a76ad4f28dc659b3be7991cc7ac92dd8ea090b`,和TIFF一起同步到正式模板目录并逐字节核对。本次临时/旧APP及公证ZIP已清理;扫描范围和回执在`docs/verification/v1.1.0/`。最终恢复标签为`v1.1.0`,后续记录提交不改变签名产品源码f79d24e。原生UI和视觉仍由用户自测。
交付偏好(2026-10-21用户明确更新):后续本产品安装需释放Motion时,可直接结束Motion进程,不再重复请求保存退出;用户说明不在Motion里进行工作。保持不自动操作Motion/FCP界面的约定。本次检查时两个宿主已经退出,没有执行强退。
+
+## 1.1.2 合并修复准备
+
+版本1.1.2(11)合并面板action上下文修复和76点短脸/下颌骨局部场。构建/恢复依赖、正式路径、签名公证入口保持不变;自动检查1535项及额外886项真实帧ASan专项通过。原生验收使用build/native-112独立Motion副本,用户素材、截图和完整AX数据不随源码上传。用户已明确授权接手Motion/FCP进行测试;先前不操作界面的偏好已被本次授权更新。
diff --git a/Templates/大鱼人像美化/README.md b/Templates/大鱼人像美化/README.md
index cc81c48..97fb07b 100644
--- a/Templates/大鱼人像美化/README.md
+++ b/Templates/大鱼人像美化/README.md
@@ -1,6 +1,6 @@
# 大鱼人像美化 FCP 效果模板
-版本:1.1.1。滤镜 UUID:`FB14B0E6-59B2-4836-916F-CD0C559E9241`。需配套同版大鱼人像美化 FxPlug APP。
+版本:1.1.2。滤镜 UUID:`FB14B0E6-59B2-4836-916F-CD0C559E9241`。需配套同版大鱼人像美化 FxPlug APP。
## 三卡片与自动跟随
diff --git a/Templates/大鱼人像美化/VALIDATION.json b/Templates/大鱼人像美化/VALIDATION.json
index d88c137..8ee5cbd 100644
--- a/Templates/大鱼人像美化/VALIDATION.json
+++ b/Templates/大鱼人像美化/VALIDATION.json
@@ -1,6 +1,6 @@
{
- "version": "1.1.1",
- "sha256": "7090c1fbe0d8310c0b092f7c56f72e9c3fe6503aa38753f3fd8d7adcbd9a27b3",
+ "version": "1.1.2",
+ "sha256": "e9fa97dc14323d0daa7d3d61b06a78ba4000a70ab9f5e67ebffd750d49df1e15",
"filterUUID": "FB14B0E6-59B2-4836-916F-CD0C559E9241",
"publishedFunctionalControls": 1,
"informationalControls": 2,
@@ -58,10 +58,10 @@
"legacyValuesPreserved": true,
"publishOSC": 0,
"currentHostValidation": {
- "version": "1.1.1",
- "Motion": "pending native retest",
- "FCP": "1.1.0 reproduced: all parameter controls disabled and status waiting for host connection; 1.1.1 retest pending",
- "scope": "User authorized Motion/FCP interaction for this regression investigation."
+ "version": "1.1.2",
+ "Motion": "pending combined-build retest; 1.1.1 native number/bypass/reset/host undo/save already verified",
+ "FCP": "pending combined-build retest; 1.1.1 native API handshake and brightness commit/reset already verified",
+ "scope": "User explicitly authorized taking over Motion/FCP for native testing."
},
"customParameter": {
"id": 210,
@@ -156,12 +156,12 @@
},
"automatedInspectorValidation": {
"scope": "offscreen AppKit inspector, real production controller and mocked host callbacks; no Motion/FCP UI",
- "fullChecks": 1046,
+ "fullChecks": 1535,
"counts": {
"beauty": 49,
"face": 116,
- "thinChin": 394,
- "hostAdapterASan": 248,
+ "thinChinJawShort": 796,
+ "hostAdapterASan": 335,
"oscASan": 98,
"inspectorASan": 141
},
@@ -188,9 +188,33 @@
"archives": "Own PGInspectorState secure payload; Foundation outer and inner decode and Motion alphabet roundtrip tested"
},
"installation": {
- "version": "1.1.1",
- "build": "10",
+ "version": "1.1.2",
+ "build": "11",
"status": "pending",
- "previousInstalledVersion": "1.1.0"
+ "previousInstalledVersion": "1.1.1 (interim)"
+ },
+ "shortJawValidation": {
+ "version": "1.1.2",
+ "build": "11",
+ "total": 1535,
+ "counts": {
+ "beauty": 49,
+ "face": 116,
+ "thinChinJawShort": 796,
+ "hostAdapterASan": 335,
+ "oscASan": 98,
+ "inspectorASan": 141
+ },
+ "allPassed": true,
+ "extraRealFramesASan": {
+ "checks": 886,
+ "includedRepeatedBaseChecks": true,
+ "addedToTotal": false
+ },
+ "originCombinations": 28,
+ "originTagsExactlyByteIdentical": true,
+ "maxByteDifferenceVsIndependentUprightReference": 2,
+ "meanByteDifferenceLimit": 0.005,
+ "nativeCombinedBuildQA": "pending"
}
}
diff --git a/Templates/大鱼人像美化/大鱼人像美化.moef b/Templates/大鱼人像美化/大鱼人像美化.moef
index f2bfe8f..ed75f3a 100644
--- a/Templates/大鱼人像美化/大鱼人像美化.moef
+++ b/Templates/大鱼人像美化/大鱼人像美化.moef
@@ -46,7 +46,7 @@
1
- 大鱼人像美化 1.1.1:磨皮、美白、瘦脸三卡片自定义面板,自动选择主体并实时跟随。模板局部磨皮默认为50%。
+ 大鱼人像美化 1.1.2:磨皮、美白、瘦脸三卡片自定义面板,自动选择主体并实时跟随。模板局部磨皮默认为50%。
1920
@@ -253,7 +253,7 @@
-
+
8589934608
diff --git a/beatygril/beatygril/Plugin/Info.plist b/beatygril/beatygril/Plugin/Info.plist
index 71c5bb9..ff185ef 100644
--- a/beatygril/beatygril/Plugin/Info.plist
+++ b/beatygril/beatygril/Plugin/Info.plist
@@ -17,9 +17,9 @@
CFBundlePackageType
XPC!
CFBundleShortVersionString
- 1.1.1
+ 1.1.2
CFBundleVersion
- 10
+ 11
NSHumanReadableCopyright
PlugInKit
@@ -72,7 +72,7 @@
uuid
FB14B0E6-59B2-4836-916F-CD0C559E9241
version
- 1.1.1
+ 1.1.2
className
@@ -84,7 +84,7 @@
uuid
E0E432C8-3D71-4ABA-B006-E7F519E06C87
version
- 1.1.1
+ 1.1.2
protocolNames
FxOnScreenControl
diff --git a/beatygril/beatygril/Plugin/PGFaceProcessor.m b/beatygril/beatygril/Plugin/PGFaceProcessor.m
index 639a599..c758790 100644
--- a/beatygril/beatygril/Plugin/PGFaceProcessor.m
+++ b/beatygril/beatygril/Plugin/PGFaceProcessor.m
@@ -14,6 +14,8 @@ static const uint8_t PGThinPins[17][2] = {
{69,49},{65,49},{68,52},{66,52},{67,47}
};
static const uint8_t PGChinPins[3][2] = {{68,38},{67,37},{66,36}};
+// Origin and radius neighbour; all six directions point toward landmark 29.
+static const uint8_t PGJawPins[6][2] = {{72,71},{62,63},{71,70},{63,64},{70,71},{64,63}};
@interface PGFaceFrame ()
@property (nonatomic, copy, readwrite) NSArray *faces;
@@ -125,7 +127,8 @@ static NSDictionary *PGSanitizedGeometry(id value) {
result[name] = points;
}
// Old archives and partially detected faces remain usable by the other
- // controls. Missing or malformed full topology disables local thin/chin pins.
+ // controls. Missing or malformed full topology disables the recovered local
+ // thin/chin/jaw/short fields without discarding otherwise usable old records.
NSArray *points76 = PGSanitizedPoints76(value[@"points76"]);
if (points76) result[@"points76"] = points76;
return [result copy];
@@ -344,10 +347,11 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ
_lock = [[NSLock alloc] init];
_realtime = [PGRealtimeFaceTracker new];
_targets = [NSMutableArray arrayWithObjects:[NSMutableDictionary dictionary], [NSMutableDictionary dictionary], [NSMutableDictionary dictionary], nil];
- // Local thinning and chin use the recovered 17/3-pin coordinate math.
- // The other seven controls retain this project's analytic approximation.
- // Chin follows those analytic body controls and precedes the eye warp;
- // this combined order is not a reconstruction of the full reference chain.
+ // Thin/chin/jaw/short reconstruct the recovered local coordinate math.
+ // The other five controls retain this project's analytic approximation.
+ // Recovered jaw -> chin -> short order follows the reference. The other
+ // analytic body controls still precede jaw, so the full combined chain
+ // (narrow/V/cheek/small-head/eyes) is not claimed reference-equivalent.
// Each pin changes the sampling coordinate, with only one image resample.
NSMutableString *source = [NSMutableString stringWithString:
@"vec2 pgThinPin(vec2 q, vec4 control, float ratio) {"
@@ -355,9 +359,24 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ
" if(radius<=0.0001) return q;"
" float weight=clamp(1.0-length(q-control.xy)/radius,0.0,1.0);"
" return clamp(q-direction*ratio*weight,vec2(0.0),vec2(1.0)); }"
- " kernel vec2 pgFaceWarp(vec2 c, vec2 size, vec2 le, vec2 re, vec2 er, float thinRatio, float bigEye, float shortFace, float narrow, float chinRatio, float vFace, float jaw, float cheek, float smallHead, float headRange, vec2 imageOrigin, vec2 imageSize"];
+ " vec2 pgJawPin(vec2 q, vec4 edge, vec2 anchor, float ratio) {"
+ " float radius=length(edge.xy-edge.zw); if(radius<=0.0001) return q;"
+ " float weight=clamp(1.0-length(q-edge.xy)/radius,0.0,1.0);"
+ " return clamp(q-(anchor-edge.xy)*ratio*weight,vec2(0.0),vec2(1.0)); }"
+ " vec2 pgShortEdge(vec2 q, vec2 a, vec2 b) {"
+ " vec2 d=b-a; float squared=dot(d,d);"
+ " float t=0.0; if(squared>0.000001) t=clamp(dot(q-a,d)/squared,0.0,1.0);"
+ " float distance=length(q-(a+t*d)); float crossing=0.0;"
+ " if((a.y>q.y && b.y<=q.y) || (b.y>q.y && a.y<=q.y)) {"
+ " float dy=b.y-a.y; if(abs(dy)<=0.000001) dy=dy<0.0?-0.000001:0.000001;"
+ " if(q.x0.000001) { vec2 q=(p-imageOrigin)/imageSize; q.y=1.0-q.y;"];
for (NSUInteger i=0; i<17; i++) [source appendFormat:@" q=pgThinPin(q,pin%lu,thinRatio);",(unsigned long)i];
@@ -370,11 +389,21 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ
" float ch=(n.y-0.02)/0.19; float cheekBand = exp(-ch*ch)*body;"
" float head = 1.0-smoothstep(0.38,0.75+0.32*headRange,length(n*vec2(0.9,0.65)));"
" p += (p-c)*smallHead*0.18*head;"
- " p.x += size.x*n.x*(narrow*0.30*body + vFace*0.24*jawBand + jaw*0.20*jawBand + cheek*0.18*cheekBand)*sides;"
- " p.y += size.y*(-shortFace*0.16*lower - vFace*0.025*lower);"
+ " p.x += size.x*n.x*(narrow*0.30*body + vFace*0.24*jawBand + cheek*0.18*cheekBand)*sides;"
+ " p.y += size.y*(-vFace*0.025*lower);"
+ " if(abs(jawRatio)>0.000001) { vec2 q=(p-imageOrigin)/imageSize; q.y=1.0-q.y;"];
+ for (NSUInteger i=0; i<6; i++) [source appendFormat:@" q=pgJawPin(q,jawPin%lu,jawAnchor,jawRatio);",(unsigned long)i];
+ [source appendString:@" q.y=1.0-q.y; p=imageOrigin+q*imageSize; }"
" if(abs(chinRatio)>0.000001) { vec2 q=(p-imageOrigin)/imageSize; q.y=1.0-q.y;"];
for (NSUInteger i=0; i<3; i++) [source appendFormat:@" q=pgThinPin(q,chinPin%lu,chinRatio);",(unsigned long)i];
[source appendString:@" q.y=1.0-q.y; p=imageOrigin+q*imageSize; }"
+ " if(abs(shortRatio)>0.000001 && length(shortAxis)>0.0001) {"
+ " vec2 q=(p-imageOrigin)/imageSize; q.y=1.0-q.y;"
+ " float nearest=10.0; float crossings=0.0;"];
+ for (NSUInteger i=0; i<11; i++) [source appendFormat:@" { vec2 edge=pgShortEdge(q,shortPoint%lu,shortPoint%lu); nearest=min(nearest,edge.x); crossings+=edge.y; }",(unsigned long)i,(unsigned long)((i+1)%11)];
+ [source appendString:@" float weight=mod(crossings,2.0)>0.5?1.0:clamp(1.0-nearest/length(shortAxis),0.0,1.0);"
+ " q=clamp(q-shortAxis*shortRatio*weight,vec2(0.0),vec2(1.0));"
+ " q.y=1.0-q.y; p=imageOrigin+q*imageSize; }"
" vec2 dl = (p-le)/er; float wl = 1.0-smoothstep(0.0,1.0,length(dl));"
" p -= (p-le)*bigEye*0.33*wl*wl;"
" vec2 dr = (p-re)/er; float wr = 1.0-smoothstep(0.0,1.0,length(dr));"
@@ -498,7 +527,7 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ
for (NSDictionary *face in geometry) {
CGRect box = PGArrayRect(face[@"box"]), pixels = PGToPixels(box, extent);
NSArray *points76 = PGSanitizedPoints76(face[@"points76"]);
- float thinRatio = 0, chinRatio = 0;
+ float thinRatio = 0, chinRatio = 0, jawRatio = 0, shortRatio = 0;
if (points76) {
double width = hypot(([points76[75][0] doubleValue]-[points76[59][0] doubleValue])*extent.size.width,
([points76[75][1] doubleValue]-[points76[59][1] doubleValue])*extent.size.height);
@@ -510,19 +539,21 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ
thinRatio = PGClamp(s.faceThin,0,1)*.025*gain;
float chin = PGClamp(s.faceChin,-1,1);
chinRatio = chin*(chin>0 ? .04 : .02)*gain;
+ jawRatio = PGClamp(s.faceJaw,0,1)*.05*gain;
+ shortRatio = PGClamp(s.faceShort,-1,1)*.25*gain;
}
- BOOL otherControls = s.faceBigEye || s.faceShort || s.faceNarrow || s.faceV || s.faceJaw || s.faceCheekbone || s.faceSmallHead;
- // No full topology means no guessed bbox thin/chin deformation. Legacy
+ BOOL otherControls = s.faceBigEye || s.faceNarrow || s.faceV || s.faceCheekbone || s.faceSmallHead;
+ // No full topology means no guessed bbox thin/chin/jaw/short deformation. Legacy
// archives using only those controls remain exact image/mask pass-through.
- if (thinRatio <= .000001 && fabsf(chinRatio) <= .000001 && !otherControls) continue;
+ if (thinRatio <= .000001 && fabsf(chinRatio) <= .000001 && fabsf(jawRatio) <= .000001 && fabsf(shortRatio) <= .000001 && !otherControls) continue;
CGRect le = PGRegionBounds(face, @"leftEye", PGInFace(box, 0.16, 0.53, 0.25, 0.15));
CGRect re = PGRegionBounds(face, @"rightEye", PGInFace(box, 0.59, 0.53, 0.25, 0.15));
CGPoint left = PGCenter(PGToPixels(le, extent)), right = PGCenter(PGToPixels(re, extent));
NSMutableArray *arguments = [@[PGVector(PGCenter(pixels)), [CIVector vectorWithX:pixels.size.width Y:pixels.size.height],
PGVector(left), PGVector(right), [CIVector vectorWithX:MAX(2, pixels.size.width * 0.24) Y:MAX(2, pixels.size.height * 0.17)],
- @(thinRatio), @(PGClamp(s.faceBigEye, 0, 1)), @(PGClamp(s.faceShort, -1, 1)),
+ @(thinRatio), @(PGClamp(s.faceBigEye, 0, 1)), @(shortRatio),
@(PGClamp(s.faceNarrow, -1, 1)), @(chinRatio), @(PGClamp(s.faceV, 0, 1)),
- @(PGClamp(s.faceJaw, 0, 1)), @(PGClamp(s.faceCheekbone, 0, 1)), @(PGClamp(s.faceSmallHead, 0, 1)), @(PGClamp(s.faceSmallHeadRange, 0, 1)),
+ @(jawRatio), @(PGClamp(s.faceCheekbone, 0, 1)), @(PGClamp(s.faceSmallHead, 0, 1)), @(PGClamp(s.faceSmallHeadRange, 0, 1)),
[CIVector vectorWithX:extent.origin.x Y:extent.origin.y], [CIVector vectorWithX:extent.size.width Y:extent.size.height]] mutableCopy];
for (NSUInteger i=0; i<17; i++) {
double ox=0,oy=0,ax=0,ay=0;
@@ -542,6 +573,23 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ
}
[arguments addObject:[CIVector vectorWithX:ox Y:oy Z:ax W:ay]];
}
+ CGPoint anchor=CGPointZero,axis=CGPointZero;
+ if(points76){
+ anchor=CGPointMake([points76[29][0]doubleValue],1-[points76[29][1]doubleValue]);
+ axis=CGPointMake([points76[46][0]doubleValue]-[points76[48][0]doubleValue],
+ [points76[48][1]doubleValue]-[points76[46][1]doubleValue]);
+ }
+ [arguments addObject:PGVector(anchor)];[arguments addObject:PGVector(axis)];
+ for(NSUInteger i=0;i<6;i++){
+ double ox=0,oy=0,nx=0,ny=0;
+ if(points76){NSArray *origin=points76[PGJawPins[i][0]],*neighbour=points76[PGJawPins[i][1]];
+ ox=[origin[0]doubleValue];oy=1-[origin[1]doubleValue];nx=[neighbour[0]doubleValue];ny=1-[neighbour[1]doubleValue];}
+ [arguments addObject:[CIVector vectorWithX:ox Y:oy Z:nx W:ny]];
+ }
+ for(NSUInteger i=62;i<=72;i++){
+ CGPoint point=CGPointZero;if(points76)point=CGPointMake([points76[i][0]doubleValue],1-[points76[i][1]doubleValue]);
+ [arguments addObject:PGVector(point)];
+ }
CIImage *input = [result imageByClampingToExtent];
result = [_warp applyWithExtent:extent roiCallback:^CGRect(int index, CGRect rect) { (void)index; (void)rect; return extent; } inputImage:input arguments:arguments];
if (!result) return nil;
diff --git a/beatygril/beatygril/Wrapper Application/Info.plist b/beatygril/beatygril/Wrapper Application/Info.plist
index f2c29c1..1c849cb 100644
--- a/beatygril/beatygril/Wrapper Application/Info.plist
+++ b/beatygril/beatygril/Wrapper Application/Info.plist
@@ -17,9 +17,9 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.1.1
+ 1.1.2
CFBundleVersion
- 10
+ 11
LSMinimumSystemVersion
$(MACOSX_DEPLOYMENT_TARGET)
NSHumanReadableCopyright
diff --git a/docs/analysis/rea-jaw-short.json b/docs/analysis/rea-jaw-short.json
new file mode 100644
index 0000000..6820910
--- /dev/null
+++ b/docs/analysis/rea-jaw-short.json
@@ -0,0 +1 @@
+{"artifacts":[{"architecture":"arm64","digest":{"sha256":"f01e8c0b7e2676b2961950b0e48efdd2589af218e4b36de7d39f5d1c76852bfb"},"format":"mach-o"}],"captures":[],"environments":[],"providers":[{"id":"ghidra","name":"Ghidra","version":"12.1.4"}],"records":[{"analysis_profile":{"digest":"20565088524a2d0359b9b96f46ac48e679d774c7b0092c56abc06de8fdbec11c","parameters":{"analyzer_preset":"ghidra-default","annotation_policy":"atomic-function-entry-metadata-v1","architecture":"arm64","available_architectures":["arm64"],"compiler_spec_id":"auto-default","decompiler_jump_loads":true,"function_body_evidence":"complete-inclusive-ranges-v1","function_boundary_observations":"ghidra-terminal-call-limitations-v1","function_references":"complete-body-and-entry-reference-manager-v2","import_mode":"ephemeral-source-immutable","instruction_flow_evidence":"decoded-return-pcode-v1","jump_table_evidence":"typed-case-default-blocks-v1","language_id":"auto-from-header","load_image_observations":"source-mappings-entry-context-v2","loader":"auto-from-header","location_resolution":"explicit-address-exact-entry-symbol-first-v3","no_return_repair":"returning-imports-decoded-return-v2","process_launch":"inspected-jvm-launch-support-v1","string_inventory_evidence":"defined-data-coverage-v1","target_format":"mach-o","target_kind":"executable"},"provider":{"id":"ghidra","name":"Ghidra","version":"12.1.4"}},"authority":"shipped-artifact","confidence":"observed","environment":null,"evidence_id":"ev_8bc31392c07788d2482546121297c3f177d527282275ef21fd3e5edef9921544","evidence_links":[],"limitations":["The session serves operations only after Ghidra reports default auto-analysis complete; incomplete analysis does not expose partial results.","The imported Program and temporary project are ephemeral and deleted on close. Annotation operations edit session database metadata; original executable bytes are never written.","Cancelling an active Ghidra request terminates its ephemeral database and discards session annotations. The selected target remains open; the next provider query imports the original artifact again.","Default-space addresses use lowercase 0x-prefixed hexadecimal; other address spaces use :0x.","Assembly is Ghidra Listing text and does not claim textual equivalence with Hopper output.","macOS sessions require a matching executable Ghidra native decompiler; REA checks for it but does not build native components or change Gatekeeper quarantine state."],"locations":[],"normalized_result":"0x1000279f4: stp d9, d8, [sp, #-0x30]!\n0x1000279f8: stp x20, x19, [sp, #0x10]\n0x1000279fc: stp x29, x30, [sp, #0x20]\n0x100027a00: add x29, sp, #0x20\n0x100027a04: fmov d8, d0\n0x100027a08: str xzr, [x8, #0x20]\n0x100027a0c: movi v0.2D, #0x0\n0x100027a10: stp q0, q0, [x8]\n0x100027a14: cbz x0, 0x100027bac\n0x100027a18: mov x20, x0\n0x100027a1c: mov x19, x8\n0x100027a20: bl 0x10001a8c0\n0x100027a24: cbz w0, 0x100027bac\n0x100027a28: fmov d1, 0x3ff0000000000000\n0x100027a2c: fcmp d8, d1\n0x100027a30: fcsel d2, d1, d8, gt\n0x100027a34: fcmp d8, #0.0\n0x100027a38: movi d0, #0x0\n0x100027a3c: fcsel d2, d0, d2, mi\n0x100027a40: fcvt s2, d2\n0x100027a44: ldr d3, [x20, #0x80]\n0x100027a48: fcmp d3, d1\n0x100027a4c: fcsel d4, d1, d3, gt\n0x100027a50: fmov d5, -0x4010000000000000\n0x100027a54: fcmp d3, d5\n0x100027a58: fcsel d4, d5, d4, mi\n0x100027a5c: fcmp d4, #0.0\n0x100027a60: cset w8, gt\n0x100027a64: fcvt d3, s2\n0x100027a68: ldp d5, d2, [x20, #0x60]\n0x100027a6c: fcmp d2, d1\n0x100027a70: fcsel d6, d1, d2, gt\n0x100027a74: adrp x9, 0x10005f000\n0x100027a78: ldr d7, [x9, #0xb80]\n0x100027a7c: fmul d6, d6, d7\n0x100027a80: fcmp d2, #0.0\n0x100027a84: fcsel d2, d0, d6, mi\n0x100027a88: fmul d2, d2, d3\n0x100027a8c: fcvt s2, d2\n0x100027a90: add x9, x20, #0x78\n0x100027a94: ldur q6, [x20, #0x88]\n0x100027a98: ld1 v5, x9, \n0x100027a9c: adrp x9, 0x10005f000\n0x100027aa0: ldr q7, [x9, #0x980]\n0x100027aa4: fcmgt v7.2D, v7.2D, v5.2D\n0x100027aa8: fcmlt v16.2D, v6.2D\n0x100027aac: fmov v17.2D, #0x3ff0000000000000\n0x100027ab0: fcmgt v18.2D, v5.2D, v17.2D\n0x100027ab4: bit v5.16B, v17.16B, v18.16B\n0x100027ab8: fcmgt v18.2D, v6.2D, v17.2D\n0x100027abc: bit v6.16B, v17.16B, v18.16B\n0x100027ac0: adrp x9, 0x10005f000\n0x100027ac4: ldr q18, [x9, #0x990]\n0x100027ac8: fmul v5.2D, v5.2D, v18.2D\n0x100027acc: adrp x9, 0x10005f000\n0x100027ad0: ldr q18, [x9, #0x9a0]\n0x100027ad4: fmul v6.2D, v6.2D, v18.2D\n0x100027ad8: adrp x9, 0x10005f000\n0x100027adc: ldr q18, [x9, #0x9b0]\n0x100027ae0: bit v5.16B, v18.16B, v7.16B\n0x100027ae4: bic v6.16B, v6.16B, v16.16B\n0x100027ae8: fmul v6.2D, v6.2D, v3.D[0x0]\n0x100027aec: fmul v5.2D, v5.2D, v3.D[0x0]\n0x100027af0: fcvtn v5.2S, v5.2D\n0x100027af4: fcvtn2 v5.4S, v6.2D\n0x100027af8: ldr d6, [x20, #0x98]\n0x100027afc: fcmp d6, d1\n0x100027b00: fcsel d7, d1, d6, gt\n0x100027b04: adrp x9, 0x10005f000\n0x100027b08: ldr d16, [x9, #0xe68]\n0x100027b0c: fmul d7, d7, d16\n0x100027b10: fcmp d6, #0.0\n0x100027b14: mov x9, #-0x8000000000000000\n0x100027b18: fmov d6, x9\n0x100027b1c: adrp x9, 0x10005f000\n0x100027b20: add x9, x9, #0x9c0\n0x100027b24: ldr d16, x9, w8, , uxtw, #3\n0x100027b28: fcsel d6, d6, d7, mi\n0x100027b2c: fmul d4, d4, d16\n0x100027b30: ldr d7, [x20, #0x70]\n0x100027b34: add x8, x20, #0xa0\n0x100027b38: ld1 v7, x8, \n0x100027b3c: adrp x8, 0x10005f000\n0x100027b40: ldr q16, [x8, #0x9d0]\n0x100027b44: fcmgt v16.2D, v16.2D, v7.2D\n0x100027b48: fcmgt v18.2D, v7.2D, v17.2D\n0x100027b4c: bit v7.16B, v17.16B, v18.16B\n0x100027b50: adrp x8, 0x10005f000\n0x100027b54: ldr q17, [x8, #0x9e0]\n0x100027b58: fmul v7.2D, v7.2D, v17.2D\n0x100027b5c: adrp x8, 0x10005f000\n0x100027b60: ldr q17, [x8, #0x9f0]\n0x100027b64: mov v6.D[0x1], v4.D[0x0]\n0x100027b68: mov v4.16B, v16.16B\n0x100027b6c: bsl v4.16B, v17.16B, v7.16B\n0x100027b70: fmul v6.2D, v6.2D, v3.D[0x0]\n0x100027b74: fmul v3.2D, v4.2D, v3.D[0x0]\n0x100027b78: fcvtn v4.2S, v6.2D\n0x100027b7c: fcvtn2 v4.4S, v3.2D\n0x100027b80: stp q5, q4, [x19]\n0x100027b84: ldr d3, [x20, #0xa8]\n0x100027b88: fcmp d3, d1\n0x100027b8c: fcsel d1, d1, d3, gt\n0x100027b90: adrp x8, 0x10005f000\n0x100027b94: ldr d4, [x8, #0xc40]\n0x100027b98: fmul d1, d1, d4\n0x100027b9c: fcmp d3, #0.0\n0x100027ba0: fcsel d0, d0, d1, mi\n0x100027ba4: fcvt s0, d0\n0x100027ba8: stp s0, s2, [x19, #0x20]\n0x100027bac: ldp x29, x30, [sp, #0x20]\n0x100027bb0: ldp x20, x19, [sp, #0x10]\n0x100027bb4: ldp d9, d8, [sp], #0x30\n0x100027bb8: ret","operation":"procedure_assembly","parameters":{"procedure":"0x1000279f4"},"predicate_type":"rea.analysis","provider":{"id":"ghidra","name":"Ghidra","version":"12.1.4"},"raw_result":{"limitations":[],"value":"0x1000279f4: stp d9, d8, [sp, #-0x30]!\n0x1000279f8: stp x20, x19, [sp, #0x10]\n0x1000279fc: stp x29, x30, [sp, #0x20]\n0x100027a00: add x29, sp, #0x20\n0x100027a04: fmov d8, d0\n0x100027a08: str xzr, [x8, #0x20]\n0x100027a0c: movi v0.2D, #0x0\n0x100027a10: stp q0, q0, [x8]\n0x100027a14: cbz x0, 0x100027bac\n0x100027a18: mov x20, x0\n0x100027a1c: mov x19, x8\n0x100027a20: bl 0x10001a8c0\n0x100027a24: cbz w0, 0x100027bac\n0x100027a28: fmov d1, 0x3ff0000000000000\n0x100027a2c: fcmp d8, d1\n0x100027a30: fcsel d2, d1, d8, gt\n0x100027a34: fcmp d8, #0.0\n0x100027a38: movi d0, #0x0\n0x100027a3c: fcsel d2, d0, d2, mi\n0x100027a40: fcvt s2, d2\n0x100027a44: ldr d3, [x20, #0x80]\n0x100027a48: fcmp d3, d1\n0x100027a4c: fcsel d4, d1, d3, gt\n0x100027a50: fmov d5, -0x4010000000000000\n0x100027a54: fcmp d3, d5\n0x100027a58: fcsel d4, d5, d4, mi\n0x100027a5c: fcmp d4, #0.0\n0x100027a60: cset w8, gt\n0x100027a64: fcvt d3, s2\n0x100027a68: ldp d5, d2, [x20, #0x60]\n0x100027a6c: fcmp d2, d1\n0x100027a70: fcsel d6, d1, d2, gt\n0x100027a74: adrp x9, 0x10005f000\n0x100027a78: ldr d7, [x9, #0xb80]\n0x100027a7c: fmul d6, d6, d7\n0x100027a80: fcmp d2, #0.0\n0x100027a84: fcsel d2, d0, d6, mi\n0x100027a88: fmul d2, d2, d3\n0x100027a8c: fcvt s2, d2\n0x100027a90: add x9, x20, #0x78\n0x100027a94: ldur q6, [x20, #0x88]\n0x100027a98: ld1 v5, x9, \n0x100027a9c: adrp x9, 0x10005f000\n0x100027aa0: ldr q7, [x9, #0x980]\n0x100027aa4: fcmgt v7.2D, v7.2D, v5.2D\n0x100027aa8: fcmlt v16.2D, v6.2D\n0x100027aac: fmov v17.2D, #0x3ff0000000000000\n0x100027ab0: fcmgt v18.2D, v5.2D, v17.2D\n0x100027ab4: bit v5.16B, v17.16B, v18.16B\n0x100027ab8: fcmgt v18.2D, v6.2D, v17.2D\n0x100027abc: bit v6.16B, v17.16B, v18.16B\n0x100027ac0: adrp x9, 0x10005f000\n0x100027ac4: ldr q18, [x9, #0x990]\n0x100027ac8: fmul v5.2D, v5.2D, v18.2D\n0x100027acc: adrp x9, 0x10005f000\n0x100027ad0: ldr q18, [x9, #0x9a0]\n0x100027ad4: fmul v6.2D, v6.2D, v18.2D\n0x100027ad8: adrp x9, 0x10005f000\n0x100027adc: ldr q18, [x9, #0x9b0]\n0x100027ae0: bit v5.16B, v18.16B, v7.16B\n0x100027ae4: bic v6.16B, v6.16B, v16.16B\n0x100027ae8: fmul v6.2D, v6.2D, v3.D[0x0]\n0x100027aec: fmul v5.2D, v5.2D, v3.D[0x0]\n0x100027af0: fcvtn v5.2S, v5.2D\n0x100027af4: fcvtn2 v5.4S, v6.2D\n0x100027af8: ldr d6, [x20, #0x98]\n0x100027afc: fcmp d6, d1\n0x100027b00: fcsel d7, d1, d6, gt\n0x100027b04: adrp x9, 0x10005f000\n0x100027b08: ldr d16, [x9, #0xe68]\n0x100027b0c: fmul d7, d7, d16\n0x100027b10: fcmp d6, #0.0\n0x100027b14: mov x9, #-0x8000000000000000\n0x100027b18: fmov d6, x9\n0x100027b1c: adrp x9, 0x10005f000\n0x100027b20: add x9, x9, #0x9c0\n0x100027b24: ldr d16, x9, w8, , uxtw, #3\n0x100027b28: fcsel d6, d6, d7, mi\n0x100027b2c: fmul d4, d4, d16\n0x100027b30: ldr d7, [x20, #0x70]\n0x100027b34: add x8, x20, #0xa0\n0x100027b38: ld1 v7, x8, \n0x100027b3c: adrp x8, 0x10005f000\n0x100027b40: ldr q16, [x8, #0x9d0]\n0x100027b44: fcmgt v16.2D, v16.2D, v7.2D\n0x100027b48: fcmgt v18.2D, v7.2D, v17.2D\n0x100027b4c: bit v7.16B, v17.16B, v18.16B\n0x100027b50: adrp x8, 0x10005f000\n0x100027b54: ldr q17, [x8, #0x9e0]\n0x100027b58: fmul v7.2D, v7.2D, v17.2D\n0x100027b5c: adrp x8, 0x10005f000\n0x100027b60: ldr q17, [x8, #0x9f0]\n0x100027b64: mov v6.D[0x1], v4.D[0x0]\n0x100027b68: mov v4.16B, v16.16B\n0x100027b6c: bsl v4.16B, v17.16B, v7.16B\n0x100027b70: fmul v6.2D, v6.2D, v3.D[0x0]\n0x100027b74: fmul v3.2D, v4.2D, v3.D[0x0]\n0x100027b78: fcvtn v4.2S, v6.2D\n0x100027b7c: fcvtn2 v4.4S, v3.2D\n0x100027b80: stp q5, q4, [x19]\n0x100027b84: ldr d3, [x20, #0xa8]\n0x100027b88: fcmp d3, d1\n0x100027b8c: fcsel d1, d1, d3, gt\n0x100027b90: adrp x8, 0x10005f000\n0x100027b94: ldr d4, [x8, #0xc40]\n0x100027b98: fmul d1, d1, d4\n0x100027b9c: fcmp d3, #0.0\n0x100027ba0: fcsel d0, d0, d1, mi\n0x100027ba4: fcvt s0, d0\n0x100027ba8: stp s0, s2, [x19, #0x20]\n0x100027bac: ldp x29, x30, [sp, #0x20]\n0x100027bb0: ldp x20, x19, [sp, #0x10]\n0x100027bb4: ldp d9, d8, [sp], #0x30\n0x100027bb8: ret"},"subject":{"architecture":"arm64","digest":{"sha256":"f01e8c0b7e2676b2961950b0e48efdd2589af218e4b36de7d39f5d1c76852bfb"},"format":"mach-o","local_path":"/private/tmp/paintfcp-rea.b3j9Te/MSE18V2-arm64","name":"MSE18V2-arm64"}},{"analysis_profile":{"digest":"20565088524a2d0359b9b96f46ac48e679d774c7b0092c56abc06de8fdbec11c","parameters":{"analyzer_preset":"ghidra-default","annotation_policy":"atomic-function-entry-metadata-v1","architecture":"arm64","available_architectures":["arm64"],"compiler_spec_id":"auto-default","decompiler_jump_loads":true,"function_body_evidence":"complete-inclusive-ranges-v1","function_boundary_observations":"ghidra-terminal-call-limitations-v1","function_references":"complete-body-and-entry-reference-manager-v2","import_mode":"ephemeral-source-immutable","instruction_flow_evidence":"decoded-return-pcode-v1","jump_table_evidence":"typed-case-default-blocks-v1","language_id":"auto-from-header","load_image_observations":"source-mappings-entry-context-v2","loader":"auto-from-header","location_resolution":"explicit-address-exact-entry-symbol-first-v3","no_return_repair":"returning-imports-decoded-return-v2","process_launch":"inspected-jvm-launch-support-v1","string_inventory_evidence":"defined-data-coverage-v1","target_format":"mach-o","target_kind":"executable"},"provider":{"id":"ghidra","name":"Ghidra","version":"12.1.4"}},"authority":"shipped-artifact","confidence":"observed","environment":null,"evidence_id":"ev_d1b8a49fc62e384710bac234967c3e8cbf7c5127081c9b2c6f6a0bbd80b80967","evidence_links":[],"limitations":["The session serves operations only after Ghidra reports default auto-analysis complete; incomplete analysis does not expose partial results.","The imported Program and temporary project are ephemeral and deleted on close. Annotation operations edit session database metadata; original executable bytes are never written.","Cancelling an active Ghidra request terminates its ephemeral database and discards session annotations. The selected target remains open; the next provider query imports the original artifact again.","Default-space addresses use lowercase 0x-prefixed hexadecimal; other address spaces use :0x.","Bytes reflect Ghidra's initialized memory, including loader fixups. Reads stop at an unmapped or uninitialized byte and report completeness explicitly.","macOS sessions require a matching executable Ghidra native decompiler; REA checks for it but does not build native components or change Gatekeeper quarantine state."],"locations":[],"normalized_result":{"address":"0x10005f980","bytes_hex":"0000000000000000000000000000f0bf9a9999999999993f9a9999999999b9bfa4703d0ad7a3b03f9a9999999999a93f00000000000000009a9999999999b93f7b14ae47e17a943f7b14ae47e17aa43f000000000000f0bf0000000000000000000000000000d03f9a9999999999b9bf000000000000d0bf0000000000000080","complete":true,"requested_bytes":128,"returned_bytes":128},"operation":"read_bytes","parameters":{"address":"0x10005f980","length":128},"predicate_type":"rea.analysis","provider":{"id":"ghidra","name":"Ghidra","version":"12.1.4"},"raw_result":{"address":"0x10005f980","bytes_hex":"0000000000000000000000000000f0bf9a9999999999993f9a9999999999b9bfa4703d0ad7a3b03f9a9999999999a93f00000000000000009a9999999999b93f7b14ae47e17a943f7b14ae47e17aa43f000000000000f0bf0000000000000000000000000000d03f9a9999999999b9bf000000000000d0bf0000000000000080","complete":true,"requested_bytes":128,"returned_bytes":128},"subject":{"architecture":"arm64","digest":{"sha256":"f01e8c0b7e2676b2961950b0e48efdd2589af218e4b36de7d39f5d1c76852bfb"},"format":"mach-o","local_path":"/private/tmp/paintfcp-rea.b3j9Te/MSE18V2-arm64","name":"MSE18V2-arm64"}}],"scenarios":[],"unknowns":[]}
\ No newline at end of file
diff --git a/docs/analysis/short-jaw-air.txt b/docs/analysis/short-jaw-air.txt
new file mode 100644
index 0000000..9f87e2a
--- /dev/null
+++ b/docs/analysis/short-jaw-air.txt
@@ -0,0 +1,371 @@
+Source AIR SHA256: f97705b88f5491057df2117aef457ffca84e11020574cd57742b0670db4484d8
+Line numbers refer to the complete original metal-objdump text. Static IR; original shader not executed.
+
+583: define internal fastcc <2 x float> @_ZL25mssFaceShapePinCoordinateDv2_fPU11MTLconstantK16MSSFaceThinPointi18MSSFaceShapeParamsS_(<2 x float> %0, %struct.MSSFaceThinPoint addrspace(2)* nocapture readonly %1, i32 %2, float %3, float %4, float %5, float %6, float %7, float %8, float %9, float %10, float %11, float %12, <2 x float> %13) unnamed_addr #2 {
+584: %15 = alloca [17 x <3 x i32>], align 16
+585: %16 = alloca [16 x <2 x i32>], align 8
+586: %17 = alloca [4 x <2 x i32>], align 8
+587: %18 = alloca [6 x <3 x i32>], align 16
+588: %19 = alloca [6 x <2 x i32>], align 8
+589: %20 = alloca [3 x <2 x i32>], align 8
+590: %21 = alloca [11 x i32], align 4
+591: %22 = alloca [11 x <2 x float>], align 8
+592: %23 = add nsw i32 %2, 75
+593: %24 = sext i32 %23 to i64
+594: %25 = getelementptr inbounds %struct.MSSFaceThinPoint, %struct.MSSFaceThinPoint addrspace(2)* %1, i64 %24, i32 0
+595: %26 = load <2 x float>, <2 x float> addrspace(2)* %25, align 8, !tbaa !62
+596: %27 = add nsw i32 %2, 59
+597: %28 = sext i32 %27 to i64
+598: %29 = getelementptr inbounds %struct.MSSFaceThinPoint, %struct.MSSFaceThinPoint addrspace(2)* %1, i64 %28, i32 0
+599: %30 = load <2 x float>, <2 x float> addrspace(2)* %29, align 8, !tbaa !62
+600: %31 = add nsw i32 %2, 67
+601: %32 = sext i32 %31 to i64
+602: %33 = getelementptr inbounds %struct.MSSFaceThinPoint, %struct.MSSFaceThinPoint addrspace(2)* %1, i64 %32, i32 0
+603: %34 = load <2 x float>, <2 x float> addrspace(2)* %33, align 8, !tbaa !62
+604: %35 = add nsw i32 %2, 74
+605: %36 = sext i32 %35 to i64
+606: %37 = getelementptr inbounds %struct.MSSFaceThinPoint, %struct.MSSFaceThinPoint addrspace(2)* %1, i64 %36, i32 0
+607: %38 = load <2 x float>, <2 x float> addrspace(2)* %37, align 8, !tbaa !62
+608: %39 = add nsw i32 %2, 60
+609: %40 = sext i32 %39 to i64
+610: %41 = getelementptr inbounds %struct.MSSFaceThinPoint, %struct.MSSFaceThinPoint addrspace(2)* %1, i64 %40, i32 0
+611: %42 = load <2 x float>, <2 x float> addrspace(2)* %41, align 8, !tbaa !62
+612: %43 = fadd fast <2 x float> %42, %38
+613: %44 = fmul fast <2 x float> %43,
+614: %45 = fsub fast <2 x float> %26, %30
+615: %46 = fmul fast <2 x float> %45, %13
+616: %47 = tail call fast float @air.dot.v2f32(<2 x float> %46, <2 x float> %46) #3
+617: %48 = tail call fast float @air.fast_sqrt.f32(float %47) #3
+618: %49 = fsub fast <2 x float> %34, %44
+619: %50 = fmul fast <2 x float> %49, %13
+620: %51 = tail call fast float @air.dot.v2f32(<2 x float> %50, <2 x float> %50) #3
+621: %52 = tail call fast float @air.fast_sqrt.f32(float %51) #3
+622: %53 = fmul fast float %52, 2.000000e+00
+623: %54 = tail call fast float @air.fast_fmax.f32(float %48, float %53) #3
+624: %55 = tail call fast float @air.fast_fmax.f32(float %54, float 1.000000e+00) #3
+625: %56 = fdiv fast float 2.800000e+02, %55
+626: %57 = tail call fast float @air.fast_clamp.f32(float %56, float 1.000000e+00, float 2.250000e+00) #3
+627: %58 = fmul fast float %57, %3
+628: %59 = fmul fast float %57, %4
+629: %60 = fmul fast float %57, %5
+630: %61 = fmul fast float %57, %6
+631: %62 = fmul fast float %57, %7
+632: %63 = fmul fast float %57, %8
+633: %64 = fmul fast float %57, %9
+634: %65 = tail call fast float @air.fast_fabs.f32(float %58) #3
+635: %66 = fcmp fast ogt float %65, 0x3EB0C6F7A0000000
+
+899: 251: ; preds = %212, %200
+900: %252 = phi <2 x float> [ %248, %212 ], [ %201, %200 ]
+901: %253 = tail call fast float @air.fast_fabs.f32(float %61) #3
+902: %254 = fcmp fast ogt float %253, 0x3EB0C6F7A0000000
+903: br i1 %254, label %255, label %309
+904:
+905: 255: ; preds = %251
+906: %256 = bitcast [6 x <3 x i32>]* %18 to i8*
+907: call void @llvm.lifetime.start.p0i8(i64 96, i8* nonnull %256) #8
+908: %257 = getelementptr inbounds [6 x <3 x i32>], [6 x <3 x i32>]* %18, i64 0, i64 0
+909: store <3 x i32> , <3 x i32>* %257, align 16
+910: %258 = getelementptr inbounds [6 x <3 x i32>], [6 x <3 x i32>]* %18, i64 0, i64 1
+911: store <3 x i32> , <3 x i32>* %258, align 16
+912: %259 = getelementptr inbounds [6 x <3 x i32>], [6 x <3 x i32>]* %18, i64 0, i64 2
+913: store <3 x i32> , <3 x i32>* %259, align 16
+914: %260 = getelementptr inbounds [6 x <3 x i32>], [6 x <3 x i32>]* %18, i64 0, i64 3
+915: store <3 x i32> , <3 x i32>* %260, align 16
+916: %261 = getelementptr inbounds [6 x <3 x i32>], [6 x <3 x i32>]* %18, i64 0, i64 4
+917: store <3 x i32> , <3 x i32>* %261, align 16
+918: %262 = getelementptr inbounds [6 x <3 x i32>], [6 x <3 x i32>]* %18, i64 0, i64 5
+919: store <3 x i32> , <3 x i32>* %262, align 16
+920: %263 = insertelement <2 x float> undef, float %61, i64 0
+921: %264 = shufflevector <2 x float> %263, <2 x float> undef, <2 x i32> zeroinitializer
+922: br label %266
+923:
+924: 265: ; preds = %305
+925: call void @llvm.lifetime.end.p0i8(i64 96, i8* nonnull %256) #8
+926: br label %309
+927:
+928: 266: ; preds = %305, %255
+929: %267 = phi <2 x float> [ %252, %255 ], [ %306, %305 ]
+930: %268 = phi i32 [ 0, %255 ], [ %307, %305 ]
+931: %269 = zext i32 %268 to i64
+932: %270 = getelementptr inbounds [6 x <3 x i32>], [6 x <3 x i32>]* %18, i64 0, i64 %269
+933: %271 = load <3 x i32>, <3 x i32>* %270, align 16
+934: %272 = extractelement <3 x i32> %271, i64 0
+935: %273 = add nsw i32 %272, %2
+936: %274 = sext i32 %273 to i64
+937: %275 = getelementptr inbounds %struct.MSSFaceThinPoint, %struct.MSSFaceThinPoint addrspace(2)* %1, i64 %274, i32 0
+938: %276 = load <2 x float>, <2 x float> addrspace(2)* %275, align 8, !tbaa !62
+939: %277 = extractelement <3 x i32> %271, i64 1
+940: %278 = add nsw i32 %277, %2
+941: %279 = sext i32 %278 to i64
+942: %280 = getelementptr inbounds %struct.MSSFaceThinPoint, %struct.MSSFaceThinPoint addrspace(2)* %1, i64 %279, i32 0
+943: %281 = load <2 x float>, <2 x float> addrspace(2)* %280, align 8, !tbaa !62
+944: %282 = fsub fast <2 x float> %276, %281
+945: %283 = tail call fast float @air.dot.v2f32(<2 x float> %282, <2 x float> %282) #3
+946: %284 = tail call fast float @air.fast_sqrt.f32(float %283) #3
+947: %285 = fcmp fast ugt float %284, 0x3F1A36E2E0000000
+948: br i1 %285, label %286, label %305
+949:
+950: 286: ; preds = %266
+951: %287 = extractelement <3 x i32> %271, i64 2
+952: %288 = add nsw i32 %287, %2
+953: %289 = sext i32 %288 to i64
+954: %290 = getelementptr inbounds %struct.MSSFaceThinPoint, %struct.MSSFaceThinPoint addrspace(2)* %1, i64 %289, i32 0
+955: %291 = load <2 x float>, <2 x float> addrspace(2)* %290, align 8, !tbaa !62
+956: %292 = fsub fast <2 x float> %291, %276
+957: %293 = fsub fast <2 x float> %267, %276
+958: %294 = tail call fast float @air.dot.v2f32(<2 x float> %293, <2 x float> %293) #3
+959: %295 = tail call fast float @air.fast_sqrt.f32(float %294) #3
+960: %296 = fdiv fast float %295, %284
+961: %297 = fsub fast float 1.000000e+00, %296
+962: %298 = tail call fast float @air.fast_clamp.f32(float %297, float 0.000000e+00, float 1.000000e+00) #3
+963: %299 = insertelement <2 x float> undef, float %298, i64 0
+964: %300 = shufflevector <2 x float> %299, <2 x float> undef, <2 x i32> zeroinitializer
+965: %301 = fmul fast <2 x float> %292, %264
+966: %302 = fmul fast <2 x float> %301, %300
+967: %303 = fsub fast <2 x float> %267, %302
+968: %304 = tail call fast <2 x float> @air.fast_clamp.v2f32(<2 x float> %303, <2 x float> zeroinitializer, <2 x float> ) #3
+969: br label %305
+970:
+971: 305: ; preds = %286, %266
+972: %306 = phi <2 x float> [ %304, %286 ], [ %267, %266 ]
+973: %307 = add nuw nsw i32 %268, 1
+974: %308 = icmp eq i32 %307, 6
+975: br i1 %308, label %265, label %266, !llvm.loop !67
+976:
+977: 309: ; preds = %265, %251
+978: %310 = phi <2 x float> [ %306, %265 ], [ %252, %251 ]
+979: %311 = tail call fast float @air.fast_fabs.f32(float %62) #3
+980: %312 = fcmp fast ogt float %311, 0x3EB0C6F7A0000000
+981: br i1 %312, label %313, label %372
+982:
+983: 313: ; preds = %309
+984: %314 = bitcast [6 x <2 x i32>]* %19 to i8*
+985: call void @llvm.lifetime.start.p0i8(i64 48, i8* nonnull %314) #8
+986: %315 = getelementptr inbounds [6 x <2 x i32>], [6 x <2 x i32>]* %19, i64 0, i64 0
+
+1127: 422: ; preds = %383, %372
+1128: %423 = phi <2 x float> [ %419, %383 ], [ %373, %372 ]
+1129: %424 = tail call fast float @air.fast_fabs.f32(float %64) #3
+1130: %425 = fcmp fast ogt float %424, 0x3EB0C6F7A0000000
+1131: br i1 %425, label %426, label %585
+1132:
+1133: 426: ; preds = %422
+1134: %427 = bitcast [11 x i32]* %21 to i8*
+1135: call void @llvm.lifetime.start.p0i8(i64 44, i8* nonnull %427) #8
+1136: %428 = getelementptr inbounds [11 x i32], [11 x i32]* %21, i64 0, i64 0
+1137: store i32 62, i32* %428, align 4
+1138: %429 = getelementptr inbounds [11 x i32], [11 x i32]* %21, i64 0, i64 1
+1139: store i32 63, i32* %429, align 4
+1140: %430 = getelementptr inbounds [11 x i32], [11 x i32]* %21, i64 0, i64 2
+1141: store i32 64, i32* %430, align 4
+1142: %431 = getelementptr inbounds [11 x i32], [11 x i32]* %21, i64 0, i64 3
+1143: store i32 65, i32* %431, align 4
+1144: %432 = getelementptr inbounds [11 x i32], [11 x i32]* %21, i64 0, i64 4
+1145: store i32 66, i32* %432, align 4
+1146: %433 = getelementptr inbounds [11 x i32], [11 x i32]* %21, i64 0, i64 5
+1147: store i32 67, i32* %433, align 4
+1148: %434 = getelementptr inbounds [11 x i32], [11 x i32]* %21, i64 0, i64 6
+1149: store i32 68, i32* %434, align 4
+1150: %435 = getelementptr inbounds [11 x i32], [11 x i32]* %21, i64 0, i64 7
+1151: store i32 69, i32* %435, align 4
+1152: %436 = getelementptr inbounds [11 x i32], [11 x i32]* %21, i64 0, i64 8
+1153: store i32 70, i32* %436, align 4
+1154: %437 = getelementptr inbounds [11 x i32], [11 x i32]* %21, i64 0, i64 9
+1155: store i32 71, i32* %437, align 4
+1156: %438 = getelementptr inbounds [11 x i32], [11 x i32]* %21, i64 0, i64 10
+1157: store i32 72, i32* %438, align 4
+1158: %439 = add nsw i32 %2, 46
+1159: %440 = sext i32 %439 to i64
+1160: %441 = getelementptr inbounds %struct.MSSFaceThinPoint, %struct.MSSFaceThinPoint addrspace(2)* %1, i64 %440, i32 0
+1161: %442 = load <2 x float>, <2 x float> addrspace(2)* %441, align 8, !tbaa !62
+1162: %443 = add nsw i32 %2, 48
+1163: %444 = sext i32 %443 to i64
+1164: %445 = getelementptr inbounds %struct.MSSFaceThinPoint, %struct.MSSFaceThinPoint addrspace(2)* %1, i64 %444, i32 0
+1165: %446 = load <2 x float>, <2 x float> addrspace(2)* %445, align 8, !tbaa !62
+1166: %447 = bitcast [11 x <2 x float>]* %22 to i8*
+1167: call void @llvm.lifetime.start.p0i8(i64 88, i8* nonnull %447) #8
+1168: br label %492
+1169:
+1170: 448: ; preds = %492
+1171: %449 = fsub fast <2 x float> %442, %446
+1172: %450 = extractelement <2 x float> %423, i64 1
+1173: %451 = extractelement <2 x float> %423, i64 0
+1174: %452 = getelementptr inbounds [11 x <2 x float>], [11 x <2 x float>]* %22, i64 0, i64 10
+1175: %453 = load <2 x float>, <2 x float>* %452, align 8, !tbaa !62
+1176: br label %454
+1177:
+1178: 454: ; preds = %484, %448
+1179: %455 = phi <2 x float> [ %453, %448 ], [ %460, %484 ]
+1180: %456 = phi i8 [ 0, %448 ], [ %485, %484 ]
+1181: %457 = phi i32 [ 0, %448 ], [ %486, %484 ]
+1182: %458 = zext i32 %457 to i64
+1183: %459 = getelementptr inbounds [11 x <2 x float>], [11 x <2 x float>]* %22, i64 0, i64 %458
+1184: %460 = load <2 x float>, <2 x float>* %459, align 8, !tbaa !62
+1185: %461 = extractelement <2 x float> %455, i64 1
+1186: %462 = extractelement <2 x float> %460, i64 1
+1187: %463 = fcmp fast ule float %462, %450
+1188: %464 = fcmp fast ogt float %461, %450
+1189: %465 = xor i1 %463, %464
+1190: br i1 %465, label %484, label %466
+1191:
+1192: 466: ; preds = %454
+1193: %467 = fsub fast float %461, %462
+1194: %468 = tail call fast float @air.fast_fabs.f32(float %467) #3
+1195: %469 = fcmp fast ugt float %468, 0x3EB0C6F7A0000000
+1196: %470 = fcmp fast olt float %467, 0.000000e+00
+1197: %471 = select fast i1 %470, float 0xBEB0C6F7A0000000, float 0x3EB0C6F7A0000000
+1198: %472 = select i1 %469, float %467, float %471
+1199: %473 = extractelement <2 x float> %455, i64 0
+1200: %474 = extractelement <2 x float> %460, i64 0
+1201: %475 = fsub fast float %473, %474
+1202: %476 = fsub fast float %450, %462
+1203: %477 = fmul fast float %475, %476
+1204: %478 = fdiv fast float %477, %472
+1205: %479 = fadd fast float %478, %474
+1206: %480 = fcmp fast olt float %451, %479
+1207: br i1 %480, label %481, label %484
+1208:
+1209: 481: ; preds = %466
+1210: %482 = and i8 %456, 1
+1211: %483 = xor i8 %482, 1
+1212: br label %484
+1213:
+1214: 484: ; preds = %481, %466, %454
+1215: %485 = phi i8 [ %483, %481 ], [ %456, %466 ], [ %456, %454 ]
+1216: %486 = add nuw nsw i32 %457, 1
+1217: %487 = icmp eq i32 %486, 11
+1218: br i1 %487, label %488, label %454, !llvm.loop !70
+1219:
+1220: 488: ; preds = %484
+1221: %489 = getelementptr inbounds [11 x <2 x float>], [11 x <2 x float>]* %22, i64 0, i64 0
+1222: %490 = and i8 %485, 1
+1223: %491 = load <2 x float>, <2 x float>* %489, align 8, !tbaa !62
+1224: br label %506
+1225:
+1226: 492: ; preds = %492, %426
+1227: %493 = phi i32 [ 0, %426 ], [ %502, %492 ]
+1228: %494 = zext i32 %493 to i64
+1229: %495 = getelementptr inbounds [11 x i32], [11 x i32]* %21, i64 0, i64 %494
+1230: %496 = load i32, i32* %495, align 4, !tbaa !53
+1231: %497 = add nsw i32 %496, %2
+1232: %498 = sext i32 %497 to i64
+1233: %499 = getelementptr inbounds %struct.MSSFaceThinPoint, %struct.MSSFaceThinPoint addrspace(2)* %1, i64 %498, i32 0
+1234: %500 = load <2 x float>, <2 x float> addrspace(2)* %499, align 8, !tbaa !62
+1235: %501 = getelementptr inbounds [11 x <2 x float>], [11 x <2 x float>]* %22, i64 0, i64 %494
+1236: store <2 x float> %500, <2 x float>* %501, align 8, !tbaa !62
+1237: %502 = add nuw nsw i32 %493, 1
+1238: %503 = icmp eq i32 %502, 11
+1239: br i1 %503, label %448, label %492, !llvm.loop !71
+1240:
+1241: 504: ; preds = %531
+1242: %505 = icmp eq i8 %490, 0
+1243: br i1 %505, label %559, label %540
+1244:
+1245: 506: ; preds = %531, %488
+1246: %507 = phi i32 [ 0, %488 ], [ %513, %531 ]
+1247: %508 = phi <2 x float> [ %491, %488 ], [ %538, %531 ]
+1248: %509 = phi float [ 1.000000e+01, %488 ], [ %537, %531 ]
+1249: %510 = zext i32 %507 to i64
+1250: %511 = getelementptr inbounds [11 x <2 x float>], [11 x <2 x float>]* %22, i64 0, i64 %510
+1251: %512 = load <2 x float>, <2 x float>* %511, align 8, !tbaa !62
+1252: %513 = add nuw nsw i32 %507, 1
+1253: %514 = trunc i32 %513 to i8
+1254: %515 = urem i8 %514, 11
+1255: %516 = zext i8 %515 to i64
+1256: %517 = getelementptr inbounds [11 x <2 x float>], [11 x <2 x float>]* %22, i64 0, i64 %516
+1257: %518 = load <2 x float>, <2 x float>* %517, align 8, !tbaa !62
+1258: %519 = fsub fast <2 x float> %518, %512
+1259: %520 = tail call fast float @air.dot.v2f32(<2 x float> %519, <2 x float> %519) #3
+1260: %521 = fcmp fast ugt float %520, 0x3EB0C6F7A0000000
+1261: br i1 %521, label %522, label %531
+1262:
+1263: 522: ; preds = %506
+1264: %523 = fsub fast <2 x float> %423, %512
+1265: %524 = tail call fast float @air.dot.v2f32(<2 x float> %523, <2 x float> %519) #3
+1266: %525 = fdiv fast float %524, %520
+1267: %526 = tail call fast float @air.fast_clamp.f32(float %525, float 0.000000e+00, float 1.000000e+00) #3
+1268: %527 = insertelement <2 x float> undef, float %526, i64 0
+1269: %528 = shufflevector <2 x float> %527, <2 x float> undef, <2 x i32> zeroinitializer
+1270: %529 = fmul fast <2 x float> %528, %519
+1271: %530 = fadd fast <2 x float> %529, %512
+1272: br label %531
+1273:
+1274: 531: ; preds = %522, %506
+1275: %532 = phi <2 x float> [ %530, %522 ], [ %512, %506 ]
+1276: %533 = fsub fast <2 x float> %423, %532
+1277: %534 = tail call fast float @air.dot.v2f32(<2 x float> %533, <2 x float> %533) #3
+1278: %535 = tail call fast float @air.fast_sqrt.f32(float %534) #3
+1279: %536 = fcmp fast olt float %535, %509
+1280: %537 = select i1 %536, float %535, float %509
+1281: %538 = select i1 %536, <2 x float> %532, <2 x float> %508
+1282: %539 = icmp eq i32 %513, 11
+1283: br i1 %539, label %504, label %506, !llvm.loop !72
+1284:
+1285: 540: ; preds = %504
+1286: %541 = fsub fast <2 x float> , %449
+1287: %542 = tail call fast float @air.dot.v2f32(<2 x float> %541, <2 x float> %541) #3
+1288: %543 = tail call fast float @air.fast_sqrt.f32(float %542) #3
+1289: %544 = fcmp fast ugt float %543, 0x3F1A36E2E0000000
+1290: br i1 %544, label %545, label %583
+1291:
+1292: 545: ; preds = %540
+1293: %546 = tail call fast float @air.dot.v2f32(<2 x float> zeroinitializer, <2 x float> zeroinitializer) #3
+1294: %547 = tail call fast float @air.fast_sqrt.f32(float %546) #3
+1295: %548 = fdiv fast float %547, %543
+1296: %549 = fsub fast float 1.000000e+00, %548
+1297: %550 = tail call fast float @air.fast_clamp.f32(float %549, float 0.000000e+00, float 1.000000e+00) #3
+1298: %551 = insertelement <2 x float> undef, float %550, i64 0
+1299: %552 = shufflevector <2 x float> %551, <2 x float> undef, <2 x i32> zeroinitializer
+1300: %553 = insertelement <2 x float> undef, float %64, i64 0
+1301: %554 = shufflevector <2 x float> %553, <2 x float> undef, <2 x i32> zeroinitializer
+1302: %555 = fmul fast <2 x float> %449, %554
+1303: %556 = fmul fast <2 x float> %555, %552
+1304: %557 = fsub fast <2 x float> %423, %556
+1305: %558 = tail call fast <2 x float> @air.fast_clamp.v2f32(<2 x float> %557, <2 x float> zeroinitializer, <2 x float> ) #3
+1306: br label %583
+1307:
+1308: 559: ; preds = %504
+1309: %560 = tail call fast float @air.dot.v2f32(<2 x float> %449, <2 x float> %449) #3
+1310: %561 = tail call fast float @air.fast_sqrt.f32(float %560) #3
+1311: %562 = fcmp fast olt float %537, %561
+1312: br i1 %562, label %563, label %583
+1313:
+1314: 563: ; preds = %559
+1315: %564 = fsub fast <2 x float> , %449
+1316: %565 = tail call fast float @air.dot.v2f32(<2 x float> %564, <2 x float> %564) #3
+1317: %566 = tail call fast float @air.fast_sqrt.f32(float %565) #3
+1318: %567 = fcmp fast ugt float %566, 0x3F1A36E2E0000000
+1319: br i1 %567, label %568, label %583
+1320:
+1321: 568: ; preds = %563
+1322: %569 = fsub fast <2 x float> %423, %538
+1323: %570 = tail call fast float @air.dot.v2f32(<2 x float> %569, <2 x float> %569) #3
+1324: %571 = tail call fast float @air.fast_sqrt.f32(float %570) #3
+1325: %572 = fdiv fast float %571, %566
+1326: %573 = fsub fast float 1.000000e+00, %572
+1327: %574 = tail call fast float @air.fast_clamp.f32(float %573, float 0.000000e+00, float 1.000000e+00) #3
+1328: %575 = insertelement <2 x float> undef, float %574, i64 0
+1329: %576 = shufflevector <2 x float> %575, <2 x float> undef, <2 x i32> zeroinitializer
+1330: %577 = insertelement <2 x float> undef, float %64, i64 0
+1331: %578 = shufflevector <2 x float> %577, <2 x float> undef, <2 x i32> zeroinitializer
+1332: %579 = fmul fast <2 x float> %449, %578
+1333: %580 = fmul fast <2 x float> %579, %576
+1334: %581 = fsub fast <2 x float> %423, %580
+1335: %582 = tail call fast <2 x float> @air.fast_clamp.v2f32(<2 x float> %581, <2 x float> zeroinitializer, <2 x float> ) #3
+1336: br label %583
+1337:
+1338: 583: ; preds = %568, %563, %559, %545, %540
+1339: %584 = phi <2 x float> [ %423, %559 ], [ %558, %545 ], [ %423, %540 ], [ %582, %568 ], [ %423, %563 ]
+1340: call void @llvm.lifetime.end.p0i8(i64 88, i8* nonnull %447) #8
+1341: call void @llvm.lifetime.end.p0i8(i64 44, i8* nonnull %427) #8
+1342: br label %585
+1343:
+1344: 585: ; preds = %583, %422
+1345: %586 = phi <2 x float> [ %584, %583 ], [ %423, %422 ]
+1346: %587 = tail call fast float @air.fast_fabs.f32(float %10) #3
+1347: %588 = fcmp fast ogt float %587, 0x3EB0C6F7A0000000
+1348: br i1 %588, label %589, label %613
+
+4634: !27 = !{i32 8, !"air.buffer", !"air.location_index", i32 7, i32 1, !"air.read", !"air.struct_type_info", !28, !"air.arg_type_size", i32 40, !"air.arg_type_align_size", i32 4, !"air.arg_type_name", !"MSSFaceShapeParams", !"air.arg_name", !"faceShapeParams"}
+4635: !28 = !{i32 0, i32 4, i32 0, !"float", !"shoulian", i32 4, i32 4, i32 0, !"float", !"zhailian", i32 8, i32 4, i32 0, !"float", !"vlian", i32 12, i32 4, i32 0, !"float", !"xiaegu", i32 16, i32 4, i32 0, !"float", !"quangu", i32 20, i32 4, i32 0, !"float", !"xiaba", i32 24, i32 4, i32 0, !"float", !"duanlian", i32 28, i32 4, i32 0, !"float", !"xiaotou", i32 32, i32 4, i32 0, !"float", !"xiaotoufanwei", i32 36, i32 4, i32 0, !"float", !"dayan"}
diff --git a/docs/analysis/short-jaw-report.json b/docs/analysis/short-jaw-report.json
new file mode 100644
index 0000000..645f81e
--- /dev/null
+++ b/docs/analysis/short-jaw-report.json
@@ -0,0 +1,177 @@
+{
+ "authority": "Static shipped-artifact analysis; original shader not executed. Public Vision76 region mapping reused from slim-pose-report.json.",
+ "target_arm64_sha256": "f01e8c0b7e2676b2961950b0e48efdd2589af218e4b36de7d39f5d1c76852bfb",
+ "air_sha256": "f97705b88f5491057df2117aef457ffca84e11020574cd57742b0670db4484d8",
+ "evidence_ids": {
+ "cpu_constants": "ev_d1b8a49fc62e384710bac234967c3e8cbf7c5127081c9b2c6f6a0bbd80b80967",
+ "cpu_assembly": "ev_8bc31392c07788d2482546121297c3f177d527282275ef21fd3e5edef9921544"
+ },
+ "common": {
+ "uv": "full-image normalized, y down; production stores y up and must convert points and q together",
+ "gain": "clamp(280/max(length((P75-P59)*pixelSize),2*length((P67-(P74+P60)/2)*pixelSize),1),1,2.25)",
+ "original_order": [
+ "thin17",
+ "narrow16",
+ "vFace4",
+ "jaw6",
+ "cheek6",
+ "chin3",
+ "shortPolygon",
+ "smallHead",
+ "bigEye"
+ ],
+ "enable_threshold": "abs(effectiveRatio)>1e-6",
+ "per_pin_or_field_result": "clamp(q-newOffset,0,1)"
+ },
+ "jaw": {
+ "struct_field": "xiaegu offset12 / helper parameter6",
+ "ui_range": [
+ 0,
+ 1
+ ],
+ "cpu_scale": 0.05,
+ "constant_address": "0x10005f9a8",
+ "pins_origin_radiusNeighbour_directionAnchor": [
+ [
+ 72,
+ 71,
+ 29
+ ],
+ [
+ 62,
+ 63,
+ 29
+ ],
+ [
+ 71,
+ 70,
+ 29
+ ],
+ [
+ 63,
+ 64,
+ 29
+ ],
+ [
+ 70,
+ 71,
+ 29
+ ],
+ [
+ 64,
+ 63,
+ 29
+ ]
+ ],
+ "radius": "length(Porigin-Pneighbour) in UV; skip radius<=.0001",
+ "direction": "P29-Porigin",
+ "weight": "clamp(1-length(q-Porigin)/radius,0,1)",
+ "step": "q=clamp(q-direction*clamp(jaw,0,1)*.05*gain*weight,0,1)",
+ "execution": "6 sequential q updates; each radius uses the second triplet index, unlike thin/chin which use the direction anchor distance",
+ "air_lines": [
+ 899,
+ 986
+ ]
+ },
+ "short": {
+ "struct_field": "duanlian offset24 / helper parameter9",
+ "ui_range": [
+ -1,
+ 1
+ ],
+ "cpu_scale": 0.25,
+ "constant_address": "0x10005f9e0",
+ "cpu_clamp_min_address": "0x10005f9d0 = -1",
+ "cpu_scaled_min_address": "0x10005f9f0 = -.25",
+ "polygon_indices": [
+ 62,
+ 63,
+ 64,
+ 65,
+ 66,
+ 67,
+ 68,
+ 69,
+ 70,
+ 71,
+ 72
+ ],
+ "polygon_closure": "P72->P62",
+ "axis": "P46-P48 (nose crest region)",
+ "radius": "length(axis) in UV; skip radius<=.0001",
+ "inside": "even-odd point-in-polygon; weight1",
+ "outside": "d=minimum distance to11 closed segments; weight=clamp(1-d/radius,0,1), only when d1e-6 then project t=clamp(dot(q-A,B-A)/dot(B-A,B-A),0,1); else choose A",
+ "ray_crossing_denominator": "dy if abs(dy)>1e-6 else signed1e-6",
+ "air_lines": [
+ 1127,
+ 1340
+ ]
+ },
+ "implementation": {
+ "source": "beatygril/beatygril/Plugin/PGFaceProcessor.m",
+ "order": [
+ "thin17",
+ "existing analytic narrow/V/cheek/small-head body",
+ "jaw6",
+ "chin3",
+ "shortPolygon",
+ "existing analytic bigEye"
+ ],
+ "control_coefficients": {
+ "jaw": 0.05,
+ "short": 0.25
+ },
+ "strength": "Original CPU clamps a common shape strength. Product fixes that multiplier at1 and maps it to no user parameter: PGAmount/s.amount is smoothing strength only and is not used by thin/chin/jaw/short. All4 share the same pixel-size gain. Each shape uses its own control; PGMix is applied independently to the total output later by the host adapter. No new common shape-strength UI is introduced.",
+ "sampling": "All sequential coordinates are evaluated within one CIWarpKernel per face; image and all4 masks share the same method.",
+ "missing_or_invalid76": "Preserve legacy face/archive and other5 analytic controls; skip thin/chin/jaw/short exactly.",
+ "unchanged": "Existing thin17/chin3 index order, constants and formulas remain unchanged.",
+ "source_sha256": "e4122ca95f7ba41bc925b4cc4bf2bf88a72044ea41880cd66acd9c8278a7daf4"
+ },
+ "limits": [
+ "No original target app/shader executed; these are static CPU/AIR observations.",
+ "The complete vendor chain is not reconstructed: narrow/V/cheek/small-head/eye algorithms and their ordering remain project approximations. Isolated jaw/short plus recovered thin/chin ordering is tested.",
+ "No new explicit yaw, pitch, roll or landmark-quality attenuation is inferred. Profile behavior follows detected 2D landmarks, as in the recovered branches.",
+ "Physical PNG checks bypass the host IOSurface adapter. Original vendor pixel equivalence and Motion/FCP1.1.2 runtime acceptance remain unverified.",
+ "Independent public Vision CGImage and CIImage detections differ slightly. Tests allow .025 normalized location difference; eye/temple displacement tests measure the actual GPU inverse field."
+ ],
+ "verification": {
+ "core": {
+ "exit_code": 0,
+ "beauty_checks": 49,
+ "face_checks": 116,
+ "slim_checks": 796,
+ "total": 961
+ },
+ "real_frames_asan": {
+ "exit_code": 0,
+ "checks": 886,
+ "frames": [
+ "public NASA astronaut",
+ "local user upright/rolled0s",
+ "local user profile4s"
+ ],
+ "diagnostics": []
+ },
+ "max_uv_oracle_error": 1.09828022e-05,
+ "physical_checks": "Public Vision76 lower-contour response, pupil/upper-contour positions beyond support unchanged; separate CGImage Vision point locations cross-check the CIImage production path.",
+ "local_logs": [
+ "build/analysis-jaw-short/core-tests.txt",
+ "build/analysis-jaw-short/real-frames-asan.txt"
+ ],
+ "app_built": false,
+ "host_operated": false
+ },
+ "pre_fix_difference": {
+ "source": "PGFaceProcessor.m; PGBeautyRenderer only does appearance",
+ "jaw": "axis-aligned bbox jawBand, .20 multiplier, no radiusNeighbour or lip anchor",
+ "short": "axis-aligned bbox lower field, .16 multiplier, no lower-contour polygon or nose axis",
+ "bounds": "Difference in algorithm is established. It does not by itself prove why a specific installed screenshot looks like temple movement; coordinate/isolated-control localization must be checked in actual image output.",
+ "processor_sha256": "fe4f2a6fc28799cc9b81832baaa2d6739320bde9def175ecc8a4494d004d6b28"
+ },
+ "air_excerpts": {
+ "path": "docs/analysis/short-jaw-air.txt",
+ "sha256": "45ef87253c5d2591d78531b59b1c8672e10b84615457faabd4dc7f0e9dcdbcac"
+ }
+}
diff --git a/docs/analysis/短脸与下颌骨分析.md b/docs/analysis/短脸与下颌骨分析.md
new file mode 100644
index 0000000..92b17d4
--- /dev/null
+++ b/docs/analysis/短脸与下颌骨分析.md
@@ -0,0 +1,39 @@
+# 短脸与下颌骨局部变形
+
+本次将短脸与下颌骨从包围框近似场替换为目标产物中恢复的76点局部场。几何实现在`PGFaceProcessor.m`;`PGBeautyRenderer.m`负责外观,不是这次位置问题的修改点。
+
+证据是目标ARM64二进制的只读CPU汇编、常量字节与已有Apple metal-objdump AIR。未运行原版APP或shader,未操作Motion/FCP。CPU证据见`rea-jaw-short.json`,带原始行号的GPU IR见`short-jaw-air.txt`,推导及来源摘要见`short-jaw-report.json`。
+
+## 确认的规则
+
+两项共用像素尺寸增益:
+
+`gain = clamp(280 / max(|(P75-P59)*imageSize|, 2*|(P67-(P74+P60)/2)*imageSize|, 1), 1, 2.25)`。
+
+距离为UV时,点与采样坐标统一采用全图归一化、Y向下;像素增益另用实际渲染宽高计算。产品内部76点采用Y向上,因此在同一个kernel中统一转换,未改宿主图像方向。
+
+下颌骨按顺序使用六组`(origin, radiusNeighbour, directionAnchor)`:
+
+`(72,71,29), (62,63,29), (71,70,29), (63,64,29), (70,71,29), (64,63,29)`。
+
+半径是前两点的相邻轮廓距离,方向是`P29-origin`。每步令`weight=clamp(1-|q-origin|/radius,0,1)`,再依次更新`q=clamp(q-direction*ratio*weight,0,1)`。`ratio=clamp(jaw,0,1)*0.05*gain`。不能把半径替换为到嘴锚点的距离,也不能将六步合并为在同一原始坐标上求和。
+
+短脸使用`P62...P72`构成的11边闭合下半脸区域。内部权重为1;外部权重按到最近线段的UV距离线性下降,在距离达到`|P46-P48|`时为0。位移轴为`P46-P48`(公开Vision的鼻梁点区域),`ratio=clamp(short,-1,1)*0.25*gain`,采样更新`q=clamp(q-axis*ratio*weight,0,1)`。正值的可见运动沿鼻梁方向收短,负值延长。半径不超过`0.0001`时跳过;线段投影的平方长度门限为`0.000001`。
+
+CPU字节证据ID:`ev_d1b8a49fc62e384710bac234967c3e8cbf7c5127081c9b2c6f6a0bbd80b80967`。`0x10005f9a8`的double值为`0.05`;`0x10005f9e0`为`0.25`,`0x10005f9d0`和`0x10005f9f0`分别是短脸下限`-1`与缩放后下限`-0.25`。汇编ID:`ev_8bc31392c07788d2482546121297c3f177d527282275ef21fd3e5edef9921544`。结构字段`xiaegu`偏移12、`duanlian`偏移24,结合CPU写入与AIR读取顺序确认映射。
+
+## 产品改动及边界
+
+移除旧`jaw*0.20*jawBand`和`-shortFace*0.16*lower`。保留瘦脸17步、下巴3步的点位、系数和数学,新的局部顺序为`jaw6 → chin3 → short polygon`,仍在同一个CIWarpKernel内更新采样坐标,对每张输入图像或蒙版,每张脸仅进行一次重采样。图像、磨皮/肤色/牙齿/眼睛四张蒙版调用同一变形路径。没有完整合法76点时,这四项恢复算法安全直通;旧记录、其他参数和离线归档继续可读。
+
+原版完整顺序为`thin → narrow → V → jaw → cheek → chin → short → small-head → eyes`。产品其余五项仍采用原有近似body/眼睛场:body先于jaw,故组合全链不宣称原版等价。原版CPU另接受共同形变强度;本产品该乘数固定为1,未映射到任何用户参数。`PGAmount`/`s.amount`仅表示磨皮强度,不参与thin/chin/jaw/short比例;四项共用同一像素尺寸gain。用户分别调节各项脸型值,最终`PGMix`由宿主适配器独立混合总输出。没有新增共同形变强度控制,以保持磨皮、美白和脸型卡片各自的含义。这里没有证实或添加3D姿态识别、yaw衰减或五官恢复锁定。
+
+旧算法与原版不同是确定事实;单凭静态代码,不能替某张已安装宿主画面的“太阳穴变化”断言唯一原因。下述位置验证针对真实PNG,宿主适配和原生交互由父任务验收。
+
+## 验证
+
+`scripts/test.sh`:Beauty49、Face116、Slim796,共961项通过。旧394项thin/chin测试保留;新增独立double oracle从上述AIR规则写成,比较实际GPU坐标场,最大UV误差`1.09828022e-05`。覆盖非零extent、横竖画幅、侧脸投影与roll、正负短脸、小脸增益上限、相邻轮廓半径反例、组合顺序、缺失/非法76点和四张蒙版一致性。
+
+ASan专项以NASA公开肖像及本地用户正面/侧脸帧运行:886项通过,无ASan诊断。独立CGImage Vision请求对照了瞳孔、嘴唇、鼻梁和上下轮廓索引,防止与生产CIImage路径共用一次Y翻转假设。实际GPU采样场确认下半脸有响应,处于支持域外的瞳孔和上部轮廓位置不响应。
+
+用户帧、完整坐标和测试构建只留在忽略的`build/`目录,不进入证据文件。上述不代表原版逐像素一致,也未替代新版本Motion/FCP原生验收;本次未构建APP、安装或提交。
diff --git a/docs/verification/v1.1.2/automated-tests.json b/docs/verification/v1.1.2/automated-tests.json
new file mode 100644
index 0000000..a661aed
--- /dev/null
+++ b/docs/verification/v1.1.2/automated-tests.json
@@ -0,0 +1,24 @@
+{
+ "version": "1.1.2",
+ "build": "11",
+ "total": 1535,
+ "counts": {
+ "beauty": 49,
+ "face": 116,
+ "thinChinJawShort": 796,
+ "hostAdapterASan": 335,
+ "oscASan": 98,
+ "inspectorASan": 141
+ },
+ "allPassed": true,
+ "extraRealFramesASan": {
+ "checks": 886,
+ "includedRepeatedBaseChecks": true,
+ "addedToTotal": false
+ },
+ "originCombinations": 28,
+ "originTagsExactlyByteIdentical": true,
+ "maxByteDifferenceVsIndependentUprightReference": 2,
+ "meanByteDifferenceLimit": 0.005,
+ "nativeCombinedBuildQA": "pending"
+}
diff --git a/docs/verification/v1.1.2/core-tests.txt b/docs/verification/v1.1.2/core-tests.txt
new file mode 100644
index 0000000..c690328
--- /dev/null
+++ b/docs/verification/v1.1.2/core-tests.txt
@@ -0,0 +1,5 @@
+Beauty renderer: 49 checks, 0 failures (Metal).
+2026-10-21 23:46:40.676 face-tests[24719:5743987] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
+Face processing: 116 checks passed
+2026-10-21 23:46:52.402 slim-tests[24738:5744142] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
+PASS: 796 slim alignment checks; maximum UV oracle error 1.09828022e-05
diff --git a/docs/verification/v1.1.2/host-adapter-asan.txt b/docs/verification/v1.1.2/host-adapter-asan.txt
new file mode 100644
index 0000000..eb138dd
--- /dev/null
+++ b/docs/verification/v1.1.2/host-adapter-asan.txt
@@ -0,0 +1,33 @@
+2026-10-21 23:54:44.774 host-tests[25302:5759794] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
+Default IOSurface maximum byte difference: 0
+Alternate destination tag maximum byte difference: 0
+Anatomy default 0/0 max=0 mean=0.0000000 eye=0/0 chin=0 inverted=0
+Anatomy default 0/1 max=0 mean=0.0000000 eye=0/0 chin=0 inverted=0
+Anatomy default 1/0 max=0 mean=0.0000000 eye=0/0 chin=0 inverted=0
+Anatomy default 1/1 max=0 mean=0.0000000 eye=0/0 chin=0 inverted=0
+Anatomy big-eye 0/0 max=2 mean=0.0008917 eye=33510/33880 chin=0 inverted=0
+Anatomy big-eye 0/1 max=2 mean=0.0008917 eye=33510/33880 chin=0 inverted=0
+Anatomy big-eye 1/0 max=2 mean=0.0008917 eye=33510/33880 chin=0 inverted=0
+Anatomy big-eye 1/1 max=2 mean=0.0008917 eye=33510/33880 chin=0 inverted=0
+Anatomy chin-positive 0/0 max=1 mean=0.0001459 eye=0/0 chin=116812 inverted=0
+Anatomy chin-positive 0/1 max=1 mean=0.0001459 eye=0/0 chin=116812 inverted=0
+Anatomy chin-positive 1/0 max=1 mean=0.0001459 eye=0/0 chin=116812 inverted=0
+Anatomy chin-positive 1/1 max=1 mean=0.0001459 eye=0/0 chin=116812 inverted=0
+Anatomy chin-negative 0/0 max=1 mean=0.0000849 eye=0/0 chin=60426 inverted=0
+Anatomy chin-negative 0/1 max=1 mean=0.0000849 eye=0/0 chin=60426 inverted=0
+Anatomy chin-negative 1/0 max=1 mean=0.0000849 eye=0/0 chin=60426 inverted=0
+Anatomy chin-negative 1/1 max=1 mean=0.0000849 eye=0/0 chin=60426 inverted=0
+Anatomy short-positive 0/0 max=2 mean=0.0011072 eye=0/0 chin=285953 inverted=0
+Anatomy short-positive 0/1 max=2 mean=0.0011072 eye=0/0 chin=285953 inverted=0
+Anatomy short-positive 1/0 max=2 mean=0.0011072 eye=0/0 chin=285953 inverted=0
+Anatomy short-positive 1/1 max=2 mean=0.0011072 eye=0/0 chin=285953 inverted=0
+Anatomy short-negative 0/0 max=2 mean=0.0008583 eye=0/0 chin=225648 inverted=0
+Anatomy short-negative 0/1 max=2 mean=0.0008583 eye=0/0 chin=225648 inverted=0
+Anatomy short-negative 1/0 max=2 mean=0.0008583 eye=0/0 chin=225648 inverted=0
+Anatomy short-negative 1/1 max=2 mean=0.0008583 eye=0/0 chin=225648 inverted=0
+Anatomy jaw 0/0 max=1 mean=0.0001974 eye=0/0 chin=12951 inverted=0
+Anatomy jaw 0/1 max=1 mean=0.0001974 eye=0/0 chin=12951 inverted=0
+Anatomy jaw 1/0 max=1 mean=0.0001974 eye=0/0 chin=12951 inverted=0
+Anatomy jaw 1/1 max=1 mean=0.0001974 eye=0/0 chin=12951 inverted=0
+Legacy fallback upright comparison: max=2 mean=0.0010357
+Host adapter: 335 checks, 0 failures
diff --git a/docs/verification/v1.1.2/independent-review.json b/docs/verification/v1.1.2/independent-review.json
new file mode 100644
index 0000000..2ed5468
--- /dev/null
+++ b/docs/verification/v1.1.2/independent-review.json
@@ -0,0 +1,10 @@
+{
+ "status": "passed",
+ "productionSourceSHA256": "e4122ca95f7ba41bc925b4cc4bf2bf88a72044ea41880cd66acd9c8278a7daf4",
+ "review": "Independent read-only review against recovered CPU constants and AIR: jaw six triples/radius/sequence, short polygon/axis/sign/thresholds, one landmark Y conversion, 56 argument layout, missing-topology pass-through. No blocking defect found.",
+ "extraChecks": {
+ "AIRProductCrossingCases": 30000,
+ "axisYConversionIdentities": 1000
+ },
+ "scope": "Five remaining approximate shape controls and full combined-chain equivalence remain outside this repair. Sampled anatomical upper-contour/pupil points do not prove every temple pixel under all poses is unchanged. Native combined-build QA pending."
+}
diff --git a/docs/verification/v1.1.2/inspector-asan.txt b/docs/verification/v1.1.2/inspector-asan.txt
new file mode 100644
index 0000000..080b3ca
--- /dev/null
+++ b/docs/verification/v1.1.2/inspector-asan.txt
@@ -0,0 +1 @@
+Inspector: 141 checks, 0 failures
diff --git a/docs/verification/v1.1.2/osc-asan.txt b/docs/verification/v1.1.2/osc-asan.txt
new file mode 100644
index 0000000..e814356
--- /dev/null
+++ b/docs/verification/v1.1.2/osc-asan.txt
@@ -0,0 +1,21 @@
+2026-10-21 23:54:43.574 osc-tests[25289:5759689] PGFaceOSC point-write desired=(0.25,0.75) setter=(0.25,0.75) readback=(0.25,0.75) mode=normalized verified=1
+2026-10-21 23:54:43.583 osc-tests[25289:5759689] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(80,180) surface=320x240 origin=0
+2026-10-21 23:54:44.244 osc-tests[25289:5759689] PGFaceOSC point-write desired=(0.8,0.3) setter=(1152,-432) readback=(0.8,0.3) mode=centered-pixels verified=1
+2026-10-21 23:54:44.244 osc-tests[25289:5759689] PGFaceOSC coordinates selection=(0.7,0.4) objectBounds=(-1920,-1080,3840,2160) canvas=(152,78) surface=320x240 origin=0
+2026-10-21 23:54:44.321 osc-tests[25289:5759689] PGFaceOSC point-write desired=(0.8,0.3) setter=(0.8,0.3) readback=(0.12345,0.87654) mode=normalized verified=0
+2026-10-21 23:54:44.321 osc-tests[25289:5759689] PGFaceOSC point-write desired=(0.75,0.25) setter=(0.75,0.25) readback=(0.75,0.25) mode=normalized verified=1
+2026-10-21 23:54:44.321 osc-tests[25289:5759689] PGFaceOSC coordinates selection=(0.75,0.25) objectBounds=(-100,-50,200,100) canvas=(160,60) surface=320x240 origin=0
+2026-10-21 23:54:44.402 osc-tests[25289:5759689] PGFaceOSC point-write desired=(0.625,0.375) setter=(0.625,0.375) readback=(0.625,0.375) mode=normalized verified=1
+2026-10-21 23:54:44.402 osc-tests[25289:5759689] PGFaceOSC coordinates selection=(0.625,0.375) objectBounds=(-100,-50,200,100) canvas=(240,60) surface=320x240 origin=0
+2026-10-21 23:54:44.483 osc-tests[25289:5759689] PGFaceOSC point-write desired=(0.25,0.75) setter=(0.25,0.75) readback=(0.25,0.75) mode=normalized verified=1
+2026-10-21 23:54:44.483 osc-tests[25289:5759689] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(144,180) surface=320x240 origin=0
+2026-10-21 23:54:44.561 osc-tests[25289:5759689] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
+2026-10-21 23:54:44.561 osc-tests[25289:5759689] PGFaceOSC coordinates selection=(0,1) objectBounds=(-100,-50,200,100) canvas=(100,140) surface=320x240 origin=0
+2026-10-21 23:54:44.646 osc-tests[25289:5759689] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(nan,nan) surface=320x240 origin=0
+2026-10-21 23:54:44.721 osc-tests[25289:5759689] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
+2026-10-21 23:54:44.721 osc-tests[25289:5759689] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(inf,nan) surface=320x240 origin=0
+2026-10-21 23:54:44.806 osc-tests[25289:5759689] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
+2026-10-21 23:54:44.807 osc-tests[25289:5759689] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(80,nan) surface=320x240 origin=0
+2026-10-21 23:54:44.889 osc-tests[25289:5759689] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
+2026-10-21 23:54:44.889 osc-tests[25289:5759689] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
+OSC adapter: 98 checks, 0 failures
diff --git a/docs/verification/v1.1.2/real-frames-asan.txt b/docs/verification/v1.1.2/real-frames-asan.txt
new file mode 100644
index 0000000..745dd96
--- /dev/null
+++ b/docs/verification/v1.1.2/real-frames-asan.txt
@@ -0,0 +1,2 @@
+2026-10-21 23:46:52.888 slim-tests-asan[24731:5744056] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
+PASS: 886 slim alignment checks; maximum UV oracle error 1.09828022e-05
diff --git a/docs/验证记录.md b/docs/验证记录.md
index 45fc09d..c4fb5d7 100644
--- a/docs/验证记录.md
+++ b/docs/验证记录.md
@@ -111,3 +111,9 @@ Motion:独立普通视频项目,源PTS0、1920×1080/25fps/10秒,媒体通
1.1.1(10)完整1046项回归通过(49美化、116人脸、394瘦脸/下巴、248宿主、98 OSC、141检查器),后三套启用ASan。正式编译、公证与原生Motion/FCP交互验收进行中,尚未把自动检查等同于原生通过。
1.1.1中间正式安装版在FCP原生确认API门控修复:read/writeBefore均0,action内均1,控件启用,重置和37%数值提交有效。原生日志与结论见`analysis/面板交互连接修复.md`。当前继续处理用户新报的短脸/下颌骨位置偏差,不将中间版标记为最终原生验收通过;临时APP、旧APP备份和公证ZIP已清理,保留唯一正式1.1.1等待合并修复。
+
+## 1.1.2 短脸与下颌骨修复(构建前)
+
+替换短脸和下颌骨的旧脸框近似,恢复目标76点的11边下半脸区域与六组局部采样,沿用单次图像重采样和图像/四张蒙版共用坐标场。四项已恢复控制缺完整76点时安全直通,其余五项和全链组合仍有近似边界。分析报告、CPU证据和AIR摘要见analysis/短脸与下颌骨分析.md。
+
+1535项全部通过:49美化、116人脸、796瘦脸/下巴/短脸/下颌骨、335宿主适配ASan、98 OSC ASan、141检查器ASan。宿主独立物理行序用例扩展到短脸正负与下颌骨,28种控制/原点组合响应在预期下半脸,瞳孔区差异为0,坐标标签切换逐字节一致;独立upright参考最大差2/255,均差<0.005/255。额外真实正面/侧脸ASan专项886项通过,含重复基础用例,不相加。已核对生产源码与冻结摘要一致。合并版尚待正式构建和原生验收。
diff --git a/tests/face_tests.m b/tests/face_tests.m
index 1f37a52..44ae9e4 100644
--- a/tests/face_tests.m
+++ b/tests/face_tests.m
@@ -323,14 +323,14 @@ int main(int argc, const char *argv[]) {
*controls[i] = 1;
PGFaceFrame *warped = [processor processImage:image settings:changed time:kCMTimeZero error:&error];
BOOL changedPixels=![Pixels(context, warped.image) isEqualToData:base];
- BOOL requiresPoints=(i==0 || i==4);
- Check(warped != nil && (requiresPoints ? !changedPixels : changedPixels), requiresPoints ? @"thin/chin safely skip a face with no indexed landmarks" : [@"geometry control changes pixels: " stringByAppendingString:names[i]]);
+ BOOL requiresPoints=(i==0 || i==2 || i==4 || i==6);
+ Check(warped != nil && (requiresPoints ? !changedPixels : changedPixels), requiresPoints ? @"recovered thin/chin/short/jaw skip a face with no indexed landmarks" : [@"geometry control changes pixels: " stringByAppendingString:names[i]]);
Check(CGRectEqualToRect(warped.image.extent, image.extent), @"warp preserves image extent");
}
settings.faceShort = 0.8;
NSData *positive = Pixels(context, [processor processImage:image settings:settings time:kCMTimeZero error:&error].image);
settings.faceShort = -0.8;
- Check(![positive isEqualToData:Pixels(context, [processor processImage:image settings:settings time:kCMTimeZero error:&error].image)], @"signed short-face control has distinct directions");
+ Check([positive isEqualToData:base] && [positive isEqualToData:Pixels(context, [processor processImage:image settings:settings time:kCMTimeZero error:&error].image)], @"both signed short-face values safely skip missing76; direction is tested with full geometry");
settings = PGBeautyDefaults(); settings.showFaceBoxes = 1;
frame = [processor processImage:image settings:settings time:kCMTimeZero error:&error];
Check(Channel(context, frame.overlayImage, 26, 64, 3) > 0, @"face outlines provide separate final-composite overlay");
diff --git a/tests/host_adapter_tests.m b/tests/host_adapter_tests.m
index c85ad80..fd1c1c2 100644
--- a/tests/host_adapter_tests.m
+++ b/tests/host_adapter_tests.m
@@ -328,14 +328,24 @@ static void PortraitAnatomyTests(NSString *fixture, CIImage *portrait) {
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 lowerFace = CGRectNull;
+ for (NSUInteger point = 62; point <= 72; point++) {
+ CGPoint p = face.landmarks.allPoints.normalizedPoints[point];
+ CGFloat x = (box.origin.x+p.x*box.size.width)*width;
+ CGFloat y = (1-box.origin.y-p.y*box.size.height)*height;
+ lowerFace = CGRectUnion(lowerFace, CGRectMake(x, y, 1, 1));
+ }
+ lowerFace = CGRectInset(lowerFace, -fw*.15, -fh*.15);
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 *names = @[@"default", @"big-eye", @"chin-positive", @"chin-negative"];
- for (NSUInteger effect = 0; effect < 4; effect++) {
+ const UInt32 controls[] = {0, PGBigEye, PGChin, PGChin, PGShort, PGShort, PGJaw};
+ const double values[] = {0, 1, 1, -1, 1, -1, 1};
+ NSArray *names = @[@"default", @"big-eye", @"chin-positive", @"chin-negative", @"short-positive", @"short-negative", @"jaw"];
+ for (NSUInteger effect = 0; effect < names.count; effect++) {
PGBeautySettings settings = PGBeautyDefaults();
if (effect == 1) settings.faceBigEye = 1;
- else if (effect > 1) settings.faceChin = values[effect];
+ else if (effect == 2 || effect == 3) settings.faceChin = values[effect];
+ else if (effect == 4 || effect == 5) settings.faceShort = values[effect];
+ else if (effect == 6) settings.faceJaw = 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;
@@ -361,9 +371,11 @@ static void PortraitAnatomyTests(NSString *fixture, CIImage *portrait) {
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);
+ double lowerMass = ChangeMass(canonical, actual, width, lowerFace);
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);
+ (effect < 4 ? cm > 100 && cm > 4*(lm+rm) && um == 0 :
+ lowerMass > 100 && lowerMass > 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);
diff --git a/tests/slim_tests.m b/tests/slim_tests.m
index cdfe27f..fccbde2 100644
--- a/tests/slim_tests.m
+++ b/tests/slim_tests.m
@@ -1,6 +1,7 @@
#import
#import
#import
+#import
#import "PGFaceProcessor.h"
#include
@@ -24,7 +25,8 @@ static NSDictionary *Geometry(double yaw,double roll) {
NSMutableArray *p=[NSMutableArray new];
for(NSUInteger i=0;i<76;i++)[p addObject:@[@.5,@.5]];
p[6]=@[@.34,@.65];p[13]=@[@.66,@.65];
- p[47]=@[@.5,@.60];p[48]=@[@.5,@.52];p[49]=@[@.5,@.45];p[52]=@[@.5,@.42];
+ p[46]=@[@.5,@.68];p[47]=@[@.5,@.60];p[48]=@[@.5,@.52];p[49]=@[@.5,@.45];p[52]=@[@.5,@.42];
+ p[29]=@[@.5,@.39];
p[36]=@[@.54,@.36];p[37]=@[@.5,@.35];p[38]=@[@.46,@.36];
for(NSUInteger i=0;i<17;i++) {
double t=M_PI+M_PI*i/16;
@@ -32,7 +34,7 @@ static NSDictionary *Geometry(double yaw,double roll) {
}
for(NSUInteger i=0;i<76;i++) {
CGPoint a=ReadPoint(p[i]);
- double depth=(i==47 || i==48 || i==49 || i==52)?.18:.03;
+ double depth=(i==46 || i==47 || i==48 || i==49 || i==52)?.18:.03;
double x=(a.x-.5)*cos(yaw)+depth*sin(yaw),y=a.y-.5;
p[i]=WritePoint(CGPointMake(.5+x*cos(roll)-y*sin(roll),.5+x*sin(roll)+y*cos(roll)));
}
@@ -76,6 +78,58 @@ static CGPoint ChinOracle(CGPoint pixel,NSDictionary *g,CGRect e,double thin,dou
}
return CGPointMake(q.x,1-q.y);
}
+
+// Independent double-precision reconstruction of AIR's jaw and short-face
+// branches. This does not call or inspect the production kernel/parameter table.
+// Jaw's support uses the adjacent contour edge, not the much longer lip vector.
+// Short-face uses a filled lower-contour polygon and its distance envelope.
+static double ReferenceSizeGain(CGPoint p[76],CGRect e) {
+ double span=hypot((p[75].x-p[59].x)*e.size.width,(p[75].y-p[59].y)*e.size.height);
+ double vertical=2*hypot((p[67].x-(p[74].x+p[60].x)/2)*e.size.width,(p[67].y-(p[74].y+p[60].y)/2)*e.size.height);
+ return Clamp(280/fmax(1,fmax(span,vertical)),1,2.25);
+}
+static CGPoint ReferencePoint(CGPoint p[76],NSUInteger index) {return CGPointMake(p[index].x,1-p[index].y);}
+static double ReferencePolygonWeight(CGPoint q,CGPoint p[76]) {
+ double radius=hypot(p[46].x-p[48].x,p[46].y-p[48].y);
+ if(radius<=.0001)return 0;
+ BOOL inside=NO;double minimum=10;
+ for(NSUInteger i=0;i<11;i++){
+ CGPoint a=ReferencePoint(p,62+i),b=ReferencePoint(p,62+(i+1)%11);
+ if((a.y>q.y)!=(b.y>q.y)){
+ double divisor=b.y-a.y;if(fabs(divisor)<=1e-6)divisor=divisor<0?-1e-6:1e-6;
+ if(q.x1e-6?Clamp(((q.x-a.x)*dx+(q.y-a.y)*dy)/squared,0,1):0;
+ minimum=fmin(minimum,hypot(q.x-a.x-t*dx,q.y-a.y-t*dy));
+ }
+ return inside?1:Clamp(1-minimum/radius,0,1);
+}
+static CGPoint JawShortOracle(CGPoint pixel,NSDictionary *g,CGRect e,PGBeautySettings s) {
+ CGPoint p[76];for(NSUInteger i=0;i<76;i++)p[i]=ReadPoint(g[@"points76"][i]);
+ double gain=ReferenceSizeGain(p,e);CGPoint q=Oracle(pixel,g,e,s.faceThin);q.y=1-q.y;
+ const unsigned int origin[]={72,62,71,63,70,64},neighbour[]={71,63,70,64,71,63};
+ double jaw=Clamp(s.faceJaw,0,1)*.05*gain;
+ if(fabs(jaw)>1e-6)for(NSUInteger i=0;i<6;i++){
+ CGPoint a=ReferencePoint(p,origin[i]),b=ReferencePoint(p,neighbour[i]),target=ReferencePoint(p,29);
+ double radius=hypot(b.x-a.x,b.y-a.y);if(radius<=.0001)continue;
+ double influence=Clamp(1-hypot(q.x-a.x,q.y-a.y)/radius,0,1);
+ q.x=Clamp(q.x-(target.x-a.x)*jaw*influence,0,1);q.y=Clamp(q.y-(target.y-a.y)*jaw*influence,0,1);
+ }
+ double chin=Clamp(s.faceChin,-1,1)*(s.faceChin>0?.04:.02)*gain;
+ if(fabs(chin)>1e-6)for(NSUInteger i=0;i<3;i++){
+ CGPoint a=ReferencePoint(p,68-i),target=ReferencePoint(p,38-i);double radius=hypot(target.x-a.x,target.y-a.y);
+ if(radius<=.0001)continue;double influence=Clamp(1-hypot(q.x-a.x,q.y-a.y)/radius,0,1);
+ q.x=Clamp(q.x-(target.x-a.x)*chin*influence,0,1);q.y=Clamp(q.y-(target.y-a.y)*chin*influence,0,1);
+ }
+ double shortRatio=Clamp(s.faceShort,-1,1)*.25*gain;
+ if(fabs(shortRatio)>1e-6){
+ double influence=ReferencePolygonWeight(q,p);
+ q.x=Clamp(q.x-(p[46].x-p[48].x)*shortRatio*influence,0,1);
+ q.y=Clamp(q.y-(p[48].y-p[46].y)*shortRatio*influence,0,1);
+ }
+ return CGPointMake(q.x,1-q.y);
+}
static CIImage *CoordinateImage(CGRect extent) {
NSUInteger w=extent.size.width,h=extent.size.height;
NSMutableData *d=[NSMutableData dataWithLength:w*h*4*sizeof(float)];float *p=d.mutableBytes;
@@ -119,6 +173,22 @@ static void NumericSettingsCase(CIContext *c,PGFaceProcessor *p,CGRect e,NSDicti
static void NumericCase(CIContext *c,PGFaceProcessor *p,CGRect e,NSDictionary *g,double amount,NSString *name) {
NumericSettingsCase(c,p,e,g,amount,0,name);
}
+static void NumericLocalCase(CIContext *c,PGFaceProcessor *processor,CGRect extent,NSDictionary *geometry,PGBeautySettings settings,NSString *name) {
+ CIImage *input=CoordinateImage(extent),*output=[processor warpImage:input geometry:@[geometry] settings:settings extent:extent];
+ Check(output!=nil && CGRectEqualToRect(output.extent,extent),[name stringByAppendingString:@" extent"]);
+ NSData *data=Pixels(c,output);const float *field=data.bytes;NSUInteger width=extent.size.width,height=extent.size.height;
+ double maximum=0,movement=0;
+ for(NSUInteger row=3;row=.00006)fprintf(stderr,"%s jaw/short oracle error %.9g\n",name.UTF8String,maximum);
+ Check(maximum<.00006,[name stringByAppendingString:@" actual GPU field vs independently decoded AIR double oracle"]);
+ Check(movement>.0001,[name stringByAppendingString:@" has nonzero covered samples"]);
+}
static CGPoint Displacement(CIContext *c,CIImage *ramp,CIImage *warped,CGPoint uv) {
CGRect e=ramp.extent;CGRect point=CGRectMake(floor(e.origin.x+uv.x*e.size.width),floor(e.origin.y+uv.y*e.size.height),1,1);
float a[4],b[4];
@@ -148,6 +218,85 @@ static void AnatomicalChecks(CIContext *c,PGFaceProcessor *p,NSDictionary *g,CGR
Check(outside && eyesUnchanged,@"both physical pupils outside chin supports remain unchanged");
}
}
+static void JawShortAnatomicalChecks(CIContext *context,PGFaceProcessor *processor,NSDictionary *geometry,CGRect extent) {
+ CGPoint points[76];for(NSUInteger i=0;i<76;i++)points[i]=ReadPoint(geometry[@"points76"][i]);
+ CIImage *ramp=CoordinateImage(extent);PGBeautySettings settings=PGBeautyDefaults();settings.faceJaw=1;
+ CIImage *jaw=[processor warpImage:ramp geometry:@[geometry] settings:settings extent:extent];
+ double jawMove=0;NSUInteger origin[]={72,62,71,63,70,64},neighbour[]={71,63,70,64,71,63};
+ for(NSUInteger i=0;i<6;i++){
+ CGPoint movement=Displacement(context,ramp,jaw,points[origin[i]]);jawMove=fmax(jawMove,hypot(movement.x,movement.y));
+ double x=(points[29].x-points[origin[i]].x)*extent.size.width,y=(points[29].y-points[origin[i]].y)*extent.size.height;
+ Check(movement.x*x+movement.y*y<.001,@"jaw inverse sampling moves opposite the physical contour-to-lip direction");
+ }
+ Check(jawMove>.1,@"jaw control acts on a detected lower-jaw contour, not a guessed temple band");
+ // These are anatomical regions reported by public Vision, not positions
+ // picked by the production support formula: pupils and upper contour ends.
+ for(NSNumber *index in @[@6,@13,@59,@75]){
+ CGPoint point=points[index.unsignedIntegerValue];BOOL clear=YES;
+ for(NSUInteger i=0;i<6;i++)if(hypot(point.x-points[origin[i]].x,point.y-points[origin[i]].y).99,@"public lower-contour centroid lies inside the short-face region");
+ double axisX=(points[46].x-points[48].x)*extent.size.width,axisY=(points[46].y-points[48].y)*extent.size.height;
+ for(int sign=-1;sign<=1;sign+=2){
+ settings=PGBeautyDefaults();settings.faceShort=sign;CIImage *shortFace=[processor warpImage:ramp geometry:@[geometry] settings:settings extent:extent];
+ CGPoint displacement=Displacement(context,ramp,shortFace,lower);
+ Check(hypot(displacement.x,displacement.y)>.1,@"short-face changes the physical lower face");
+ Check(sign*(displacement.x*axisX+displacement.y*axisY)<0,@"short-face signed source sampling follows the nose-crest axis; positive visible motion shortens upward");
+ for(NSNumber *index in @[@6,@13,@59,@75]){
+ CGPoint point=points[index.unsignedIntegerValue],motion=Displacement(context,ramp,shortFace,point);
+ Check(ReferencePolygonWeight(CGPointMake(point.x,1-point.y),points)==0 && hypot(motion.x,motion.y)<.002,@"physical pupil/temple beyond short-face distance envelope is unaffected");
+ }
+ }
+}
+static void JawShortSupportChecks(CIContext *context,PGFaceProcessor *processor) {
+ NSDictionary *g=Geometry(0,0);CGPoint points[76];for(NSUInteger i=0;i<76;i++)points[i]=ReadPoint(g[@"points76"][i]);
+ CGPoint probe=CGPointMake((points[72].x+points[29].x)/2,(points[72].y+points[29].y)/2);
+ const unsigned int origins[]={72,62,71,63,70,64},neighbours[]={71,63,70,64,71,63};BOOL outside=YES;
+ for(NSUInteger i=0;i<6;i++)if(hypot(probe.x-points[origins[i]].x,probe.y-points[origins[i]].y)<=hypot(points[origins[i]].x-points[neighbours[i]].x,points[origins[i]].y-points[neighbours[i]].y)+.005)outside=NO;
+ Check(outside,@"radius discriminator lies beyond all adjacent-contour jaw radii");
+ Check(hypot(probe.x-points[72].x,probe.y-points[72].y)<.8*hypot(points[29].x-points[72].x,points[29].y-points[72].y),@"same point would be inside the incorrect lip-distance jaw radius");
+ CGRect e=CGRectMake(-19,23,640,360);CIImage *ramp=CoordinateImage(e);PGBeautySettings settings=PGBeautyDefaults();settings.faceJaw=1;
+ CIImage *out=[processor warpImage:ramp geometry:@[g] settings:settings extent:e];CGPoint d=Displacement(context,ramp,out,probe);
+ Check(hypot(d.x,d.y)<.002,@"jaw uses neighbouring-contour support rather than the lip-anchor distance");
+ NSMutableDictionary *small=[g mutableCopy];NSMutableArray *scaled=[NSMutableArray new];
+ for(NSArray *point in g[@"points76"])[scaled addObject:@[@(.5+([point[0]doubleValue]-.5)*.18),@(.5+([point[1]doubleValue]-.5)*.18)]];small[@"points76"]=scaled;
+ for(NSUInteger i=0;i<76;i++)points[i]=ReadPoint(scaled[i]);
+ Check(ReferenceSizeGain(points,e)==2.25,@"small-face numerical case reaches the recovered gain cap");
+ NumericLocalCase(context,processor,e,small,settings,@"small face jaw max gain");settings.faceJaw=0;settings.faceShort=1;
+ NumericLocalCase(context,processor,e,small,settings,@"small face short max gain");
+ for(NSUInteger i=0;i<76;i++)points[i]=ReadPoint(g[@"points76"][i]);Check(ReferenceSizeGain(points,CGRectMake(0,0,1600,1600))==1,@"large-face numerical case reaches gain floor");
+ // Four full process masks must use exactly the same new warp as the image.
+ [processor setTrackingData:Archive(g) forTarget:0];settings=PGBeautyDefaults();settings.trackingEnabled=1;
+ PGFaceFrame *neutral=[processor processImage:ramp settings:settings time:kCMTimeZero error:nil];
+ for(NSUInteger mode=0;mode<3;mode++){
+ PGBeautySettings shape=settings;shape.faceJaw=mode!=1?1:0;shape.faceShort=mode!=0?-.65:0;
+ PGFaceFrame *frame=[processor processImage:ramp settings:shape time:kCMTimeZero error:nil];
+ Check([Pixels(context,frame.image)isEqualToData:Pixels(context,[processor warpImage:ramp geometry:@[g] settings:shape extent:e])],@"process image uses the new common jaw/short field");
+ NSArray *before=@[neutral.skinMask,neutral.colorMask,neutral.teethMask,neutral.eyeMask],*after=@[frame.skinMask,frame.colorMask,frame.teethMask,frame.eyeMask];
+ for(NSUInteger i=0;i<4;i++)Check([Pixels(context,after[i])isEqualToData:Pixels(context,[processor warpImage:before[i] geometry:@[g] settings:shape extent:e])],@"every image support mask shares the jaw/short coordinate field");
+ }
+}
+static void IndependentImageLandmarkCheck(NSString *path,NSDictionary *geometry) {
+ CGImageSourceRef source=CGImageSourceCreateWithURL((__bridge CFURLRef)[NSURL fileURLWithPath:path],NULL);
+ CGImageRef image=source?CGImageSourceCreateImageAtIndex(source,0,NULL):NULL;if(source)CFRelease(source);
+ VNDetectFaceLandmarksRequest *request=[VNDetectFaceLandmarksRequest new];request.revision=VNDetectFaceLandmarksRequestRevision3;request.constellation=VNRequestFaceLandmarksConstellation76Points;
+ NSError *error=nil;VNImageRequestHandler *handler=image?[[VNImageRequestHandler alloc]initWithCGImage:image options:@{}]:nil;
+ BOOL ok=[handler performRequests:@[request] error:&error];if(image)CGImageRelease(image);
+ VNFaceObservation *face=request.results.firstObject;VNFaceLandmarkRegion2D *all=face.landmarks.allPoints;
+ Check(ok && !error && all.pointCount==76,@"independent public CGImage detection provides76 for physical PNG orientation check");
+ for(NSNumber *index in @[@6,@13,@29,@46,@48,@59,@62,@67,@72,@75]){
+ CGPoint local=all.normalizedPoints[index.unsignedIntegerValue];
+ CGPoint full=CGPointMake(face.boundingBox.origin.x+local.x*face.boundingBox.size.width,face.boundingBox.origin.y+local.y*face.boundingBox.size.height);
+ CGPoint product=ReadPoint(geometry[@"points76"][index.unsignedIntegerValue]);
+ // Separate CGImage and CIImage Vision requests have small conversion
+ // differences. A Y flip, reordered topology or bbox-local point cannot
+ // satisfy this tolerance. Physical PNG top-row is (1-y)*height.
+ Check(hypot(full.x-product.x,full.y-product.y)<.025,@"independent CGImage anatomy agrees on pupils, lips, crest and lower contour in the PNG coordinate system");
+ }
+}
int main(int argc,const char *argv[]) {@autoreleasepool {
CIContext *c=[CIContext contextWithMTLDevice:MTLCreateSystemDefaultDevice() options:@{kCIContextWorkingColorSpace:NSNull.null,kCIContextOutputColorSpace:NSNull.null}];
@@ -169,6 +318,19 @@ int main(int argc,const char *argv[]) {@autoreleasepool {
NumericSettingsCase(c,p,extents[2],Geometry(-.6,-.2),1,-1,@"thin plus negative chin");
NumericSettingsCase(c,p,extents[0],Geometry(0,0),0,.5,@"half positive chin");
NumericSettingsCase(c,p,extents[0],Geometry(0,0),0,-.5,@"half negative chin");
+ for(NSUInteger e=0;e<4;e++)for(NSUInteger y=0;y<3;y++)for(NSUInteger r=0;r<3;r++)for(NSUInteger mode=0;mode<3;mode++){
+ PGBeautySettings local=PGBeautyDefaults();if(mode==0)local.faceJaw=1;else local.faceShort=mode==1?1:-1;
+ double angle=y==0?0:(y==1?1.4:-1.4);
+ NumericLocalCase(c,p,extents[e],Geometry(angle,roll[r]),local,
+ [NSString stringWithFormat:@"local size%lu yaw%lu roll%lu mode%lu",(unsigned long)e,(unsigned long)y,(unsigned long)r,(unsigned long)mode]);
+ }
+ PGBeautySettings combined=PGBeautyDefaults();combined.faceThin=.7;combined.faceJaw=.8;combined.faceChin=-.6;combined.faceShort=.75;
+ NumericLocalCase(c,p,extents[2],Geometry(.8,.45),combined,@"ordered thin jaw chin short positive");
+ combined.faceShort=-.75;combined.faceChin=.7;
+ NumericLocalCase(c,p,extents[1],Geometry(-.8,-.45),combined,@"ordered thin jaw chin short negative");
+ combined=PGBeautyDefaults();combined.faceJaw=.5;combined.faceShort=-.5;
+ NumericLocalCase(c,p,extents[1],Geometry(0,0),combined,@"half signed short plus jaw");
+ JawShortSupportChecks(c,p);
CIImage *image=CoordinateImage(extents[1]);NSData *source=Pixels(c,image);
PGBeautySettings s=PGBeautyDefaults();s.faceThin=1;s.trackingEnabled=1;NSError *error=nil;
NSDictionary *g=Geometry(.9,.15);[p setTrackingData:Archive(g) forTarget:0];
@@ -195,6 +357,10 @@ int main(int argc,const char *argv[]) {@autoreleasepool {
Check([Pixels(c,frame.image) isEqualToData:source],@"legacy/malformed indexed points never trigger a guessed symmetric thin warp");
PGBeautySettings chinOnly=s;chinOnly.faceThin=0;chinOnly.faceChin=1;
Check([Pixels(c,[p processImage:image settings:chinOnly time:kCMTimeZero error:&error].image) isEqualToData:source],@"legacy/malformed indexed points never trigger guessed bbox chin deformation");
+ PGBeautySettings shortOnly=s;shortOnly.faceThin=0;shortOnly.faceShort=-1;
+ Check([Pixels(c,[p processImage:image settings:shortOnly time:kCMTimeZero error:&error].image) isEqualToData:source],@"legacy/malformed indexed points skip recovered short-face field exactly");
+ PGBeautySettings jawOnly=s;jawOnly.faceThin=0;jawOnly.faceJaw=1;
+ Check([Pixels(c,[p processImage:image settings:jawOnly time:kCMTimeZero error:&error].image) isEqualToData:source],@"legacy/malformed indexed points skip recovered jaw field exactly");
PGBeautySettings other=s;other.faceNarrow=.5;
Check(![Pixels(c,[p processImage:image settings:other time:kCMTimeZero error:&error].image) isEqualToData:source],@"other controls remain available in legacy archives");
}
@@ -218,6 +384,8 @@ int main(int argc,const char *argv[]) {@autoreleasepool {
PGFaceProcessor *copy=[PGFaceProcessor new];[copy setTrackingData:[real trackingDataForTarget:0] forTarget:0];rs.trackingEnabled=1;
Check([Pixels(c,[copy processImage:portrait settings:rs time:kCMTimeZero error:&error].image) isEqualToData:Pixels(c,f.image)],@"real pose replays from persisted 76 points with identical pixels");
AnatomicalChecks(c,real,f.deformationGeometry.firstObject,portrait.extent);
+ JawShortAnatomicalChecks(c,real,f.deformationGeometry.firstObject,portrait.extent);
+ IndependentImageLandmarkCheck(@(argv[index]),f.deformationGeometry.firstObject);
}}
printf("PASS: %lu slim alignment checks; maximum UV oracle error %.9g\n",(unsigned long)checks,maxOracleError);return 0;
}}