Skip to content

Add Metal backend support on Apple platforms - #3717

Open
leiyue123 wants to merge 80 commits into
mainfrom
feature/thunderllei_gpu_backend
Open

leiyue123 wants to merge 80 commits into
mainfrom
feature/thunderllei_gpu_backend

Conversation

@leiyue123

Copy link
Copy Markdown
Collaborator

实现 libpag 的 Metal GPU 后端,覆盖 iOS 与 macOS。

核心改动:

  1. 后端解耦:新增 pag::Devices 胶水层,将 tgfx 各后端的调用收敛到编译期分派(OpenGL/Metal/Vulkan/D3D12/WebGPU)。
  2. Metal 后端实现:Devices Metal 分支、MetalGPUDrawable(iOS/mac 共用)、TGFXCast Metal 类型转换、BackendSemaphore Metal ABI(initMetal/mtlEvent/mtlValue)。
  3. 平台 API:FromMetalLayer / FromMTKView / FromCVPixelBuffer:device:。
  4. PAGView Metal 版:iOS UIView 与 macOS NSView 高层封装,含动画循环与 Retina 缩放。
  5. 测试:Metal 版 PAGSurface / pagx::PAGSurface / PAGBlend 测试覆盖。
  6. 构建:PAG_USE_METAL 开关、iOS 部署目标自动提升到 13.0、按后端隔离的截图基线。
  7. 文档与 demo:iOS/mac README 补充 Metal 用法,PAGViewer demo 工程升级。

验证:macOS / iOS 模拟器 / iOS 真机三端动画循环 + Retina 缩放 + 内容上屏通过,Metal HTMLTest 651 case 全过。

后续:device 注入 API(多 GPU 场景)留待 Vulkan/WebGPU 接入时一并定稿。

… from OpenGL and prepare for Metal / Vulkan / D3D12 / WebGPU backends.
…t> semaphore ABI so users can hand libpag externally created Metal resources with correct format and cross-queue timeline sync.
… so Apple apps can drive libpag from CAMetalLayer / MTKView / MTLDevice-backed CVPixelBuffer.
…x external-texture device inference on backends without a thread-local current context.
…t cases mirroring the GL BottomLeft blend paths on Metal.
@codecov-commenter

codecov-commenter commented Sep 3, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 63.59102% with 146 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.47%. Comparing base (7cb58c1) to head (73ef4e6).

Files with missing lines Patch % Lines
src/base/utils/TGFXCast.cpp 30.15% 40 Missing and 4 partials ⚠️
src/rendering/PAGSurfaceFactory.cpp 15.00% 15 Missing and 2 partials ⚠️
src/rendering/gpu/Devices_GL.cpp 57.89% 12 Missing and 4 partials ⚠️
src/rendering/gpu/Devices_Lock.cpp 48.14% 8 Missing and 6 partials ⚠️
src/pagx/PAGSurface.cpp 33.33% 8 Missing and 4 partials ⚠️
src/rendering/PAGSurface.cpp 45.00% 9 Missing and 2 partials ⚠️
src/pagx/PAGImage.cpp 0.00% 8 Missing ⚠️
src/rendering/editing/StillImage.cpp 25.00% 4 Missing and 2 partials ⚠️
src/pagx/utils/RasterUtils.cpp 66.66% 1 Missing and 3 partials ⚠️
src/pagx/runtime/Drawable.cpp 0.00% 3 Missing ⚠️
... and 7 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3717      +/-   ##
==========================================
- Coverage   83.51%   83.47%   -0.05%     
==========================================
  Files         731      735       +4     
  Lines      105532   105791     +259     
  Branches    29218    29259      +41     
==========================================
+ Hits        88138    88308     +170     
- Misses      10803    10878      +75     
- Partials     6591     6605      +14     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@shlzxjp

shlzxjp commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

代码评审报告 — Metal 后端支持

从架构设计、对外 API、Metal 实现正确性、健壮性、性能、构建测试 CI 六个维度做了深度评审。所有结论均已读实际代码验证;凡涉及 tgfx 行为的判断,一律以 DEPS 锁定版 5cf4fcce 源码核对。

总体评价

方向正确、工程量扎实。pag::Devices 确实消除了渲染层对 GL 的硬依赖,Devices_Backend.cpp 的编译期哨兵、ExternalStateGuard 从每帧 #ifdef 收敛为一次性对象、13 个 filter 的 ShaderVisibility 适配零遗漏,都是高质量的工作。

但当前状态建议先不合入,有三个层面的问题:

1. 对外 API 破坏。 src/platform/{ios,mac}/PAGSurface.h 用只在 libpag 构建期存在的 TGFX_USE_* 宏包裹了工厂方法。这些头文件会原样进入 libpag.framework/Headers/(CMakeLists.txt:537 GLOB → :882 PUBLIC_HEADER),而 :867 的 PUBLIC compile definitions 只对通过 CMake 链接 pag target 的消费者生效——预编译 framework / CocoaPods 的 Xcode App 工程两个宏都拿不到。结果是双向失效:存量集成方的 [PAGSurface FromLayer:] 编译失败,同时本 PR 新增的 Metal API 对 framework 用户永久不可见。demo 能跑是因为 PAGView.mm 与 libpag 在同一 target 内。

好消息是修复模式在同一个文件里已有先例:PAGSurface.h:55-59 的 +FromCVPixelBuffer: 就是「公开头无条件声明 + .mm 内按后端分派」,照抄即可。

2. 质量门禁失效,CI 绿灯不构成渲染无回归的证据。

  • 7 个新增 Metal 截图用例的 key 不在 test/baseline/version.json(该文件本 PR 未修改,Metal key 数为 0),而 Baseline.cpp:305 首条判定就是 baselineVersion.empty() → return false → PAGFullTest_Metal 按构造必然 7 条失败。推测是本地 accept-baseline 后漏提交 version.json。
  • update_baseline.sh:104-126 的降级分支只 echo、无 exit 1,配合 cache 路径改为 .cache/{backend}/,导致 cacheVersion 恒空 → Baseline.cpp:306 的相等判断为假 → 无条件 return true。该处注释原文写明目的是 "so the downstream PAGFullTest does not fail"。而本 PR 恰好改了 13 个 filter 的 uniform 绑定并把 tgfx 推进 9 个 commit(含 Unify uniform buffer bindings、Fix GLSL varying location mismatch、Keep BGRA hardware buffers as BGRA,均直接影响像素)。
  • 没有任何 CI job 开 PAG_USE_METAL=ON,约 700 行 Metal 代码零自动化验证。上面两条正是被这一条掩盖的。

3. Metal 后端功能完整性不足。 跨设备安全校验被无条件绕过、forAsyncThread 路径忽略纹理自身的 MTLDevice、device 无复用、MtlTextureInfo::format 默认值导致回读红蓝互换、macOS 缺尺寸变化回调等,详见行级评论。

关于文档自述

docs/gpu-backend-decoupling.md §1.3 写明本次「只做架构准备、不引入新后端、不扩展 BackendSemaphore ABI、对外 API 完全不变」——四条均已被违反。commit 边界其实很清晰,建议拆分。其中 tgfx pin 升级 + 13 个 filter 的 ShaderVisibility 适配(commit 5b193c41)是唯一可以完全独立、零依赖先合的一块,且门禁最清晰(截图零变更)。先合它能显著降低后续 Metal 改动的归因难度。

两条高性价比建议

① 删掉 BackendSemaphore::_isInitialized,可让 ABI 破坏归零。 锁定版 tgfx 的 BackendSemaphore(Backend.h:385-393)根本没有这个成员,isInitialized() 是 out-of-line 按 _backend + handle 推导(Backend.cpp:256-273)。照搬后 sizeof 从 32 回到 24(= 改动前),_backend@0 / union@8 偏移不变,BackendSemaphore 这侧的 ABI 破坏顺带消失,并自动修掉「initMetal(nullptr, v) 不复位」的问题。一处改动解四个问题。

② 删掉 PAGView 里的 MTLCreateSystemDefaultDevice() 两行。 tgfx MetalWindow.mm:44-56 已经做了同一件事且引用计数配平:layer 未设 device 时自己 MetalDevice::Make() 并回填 layer.device。删掉可一次性消除 MRC 泄漏(MTLCreateSystemDefaultDevice 带 NS_RETURNS_RETAINED,libpag 是 MRC),并且连那套「重试 20 次 + NSLog」的逻辑也一并不再需要——device 不可用时 MetalWindow::MakeFrom 返回 nullptr,由 getDevice() 返回空即可表达失败。

评审严谨性说明

为避免误报,评审过程中排除了 53 项曾产生怀疑但经核实不成立的项,其中包括几处我们自己的错误结论。列举几条请不要据此修改的项:

  • MtlTextureInfo::format = 70 不是自创魔数,逐字继承自 tgfx MetalTypes.h(连注释文本都一致)。问题只在默认值方向与文档论断写反。
  • __bridge 在 MRC 下合法(语义为无操作),各处用法正确,不是泄漏或悬垂。
  • MetalGPUDrawable 不 retain layer/view 是跟随上游的正确选择(tgfx MetalWindow.mm:96 注释即为此),缺的只是文档化的存活期约束。
  • iOS 部署目标提到 13.0 是稳妥的:tgfx CMakeLists.txt:122-123 自身即 FATAL_ERROR 硬性要求,且 libpag :8 用 NOT DEFINED DEPLOYMENT_TARGET 尊重了用户显式设置。仅该处注释把依据写成 MTLSharedEvent/MTLBinaryArchive 是错的(tgfx 实际用 [device newEvent],且 MTLBinaryArchive 是 iOS 14)。
  • GL 与 Metal 两个宏不会同时定义(CMakeLists.txt:110-111 确实互斥),不存在重复符号;真正的缺口在非 GL 后端之间。
  • +FromMTKView: 不是 FromMetalLayer: 的冗余包装:tgfx MTKView 路径会自动同步 drawableSize(MetalWindow.mm:119)。
  • BackendTexture 的 sizeof 未变(实测 32→32),只有 BackendRenderTarget 是 24→32。
  • PAGImageTest.cpp / PAGDiskCacheTest.cpp 逐行核对,没有任何断言被删除、注释或放宽。
  • 13 处 filter uniform visibility 标注零遗漏且语义正确,逐一核对了每个 filter 的 shader 字符串与标注阶段的对应关系。
  • 编码规范穷尽扫描全部通过:lambda / dynamic_cast / C++ 异常 / mutable 成员 / k 前缀均 0 命中,7 个新文件版权年份均为 2026,移动的 GLRestorer 保持原年份。

另有 3 条依赖运行时行为、未能实机验证的,行级评论中已用「请确认」口径标注,不作断言。

合入建议

必须修:上述 1、2 两类(公开头宏门禁、baseline key、截图门禁),以及跨设备校验、forAsyncThread device 选取、pagx::Drawable::getDevice()、MTLDevice 泄漏、format 默认值——后五条改动量都很小。

强烈建议同批:BackendSemaphore 形态定稿(建议 ①)、非 GL 后端互斥、加一个 PAG_USE_METAL=ON 纯编译 job、EXCLUDED_ARCHS 条件化。

必须进 release notes:BackendRenderTarget sizeof 24→32。这条 ABI 破坏源于 MtlTextureInfo 新增 format(功能刚需),无法回避——特别注意不要试图用「删掉该字段」来消除它,那会把红蓝互换的 bug 固化。

@shlzxjp shlzxjp left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

以下是行级评审意见,共 23 条(Blocker 4 处 / Major 19 处)。Minor 与 Nit 另有 40 条,已合并在总评与后续汇总评论中。

所有结论均已读实际代码验证,涉及 tgfx 行为的一律以 DEPS 锁定版 5cf4fcce 源码核对。其中 3 条依赖运行时行为、未能实机验证的已标注「请确认」。

Comment thread src/platform/ios/PAGSurface.h Outdated

PAG_API @interface PAGSurface : NSObject

#if defined(TGFX_USE_OPENGL)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Blocker] 公开头用构建期宏做条件声明,下游 App 所有工厂方法会消失

TGFX_USE_OPENGL / TGFX_USE_METAL 只是 libpag 自身的编译宏,不随头文件分发:

  • CMakeLists.txt:537 GLOB src/platform/ios/*.h → :882 PUBLIC_HEADER,本文件原样进入 libpag.framework/Headers/;
  • :867 的 target_compile_definitions(pag PUBLIC ...) 只对通过 CMake 链接 pag target 的消费者生效,预编译 framework / CocoaPods 的 Xcode App 工程拿不到这两个宏。

于是下游编译时两个 #if 双双为假,后果是双向的:

  1. 存量集成方的 [PAGSurface FromLayer:]、FromCVPixelBuffer:context: 编译报 "no known class method";
  2. 本 PR 新增的 FromMetalLayer: / FromMTKView: / FromCVPixelBuffer:mtlDevice: 对 framework 用户永久不可见,新功能等于未交付。

demo 能跑是因为 PAGView.mm 与 libpag 在同一 target 内,恰好掩盖了该问题。另:PAGView.h:23-28 里作者自己写明「公开 API 必须无条件声明」,两处标准不一致。

修复模式本文件已有先例::55-59 的 +FromCVPixelBuffer: 完全没有被宏包住,实现是在 .mm 内按后端分派(PAGSurfaceImpl.mm:110-112 GL / :127-129 Metal)。照抄即可,无需新机制。

连带项:公开头改回无条件声明后,PAGSurfaceImpl.h/.mm 里对应的 #if 也要一起拆掉,否则 PAGSurface.m 会调到当前后端下不存在的 Impl 方法。可参照 PAGSurfaceImpl.mm:88-101 现有的模拟器分支写法(方法始终存在、内部判断后返回 nil)。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已在 commit fed0c27 中修复:公开头文件(src/platform/ios/PAGSurface.h、src/platform/mac/PAGSurface.h 及 C 头)现在无条件声明全部工厂方法,不再引用 TGFX_USE_OPENGL / TGFX_USE_METAL 等构建期宏。不可用后端的工厂方法在实现侧返回 nil/NULL 并打 LOGE,头文件对 framework 用户保持完整,下游 App 不会再出现工厂方法消失的问题。

Comment thread src/platform/mac/PAGSurface.h Outdated

PAG_API @interface PAGSurface : NSObject

#if defined(TGFX_USE_OPENGL)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Blocker] 同 iOS 侧:+FromView: 被 TGFX_USE_OPENGL 包裹后对 framework 用户不可见

与 src/platform/ios/PAGSurface.h:33 是同一问题,此处 +FromView: 是已发布的既有 API,包裹后 macOS 存量集成方升级即编译失败。修复方式同上:公开头无条件声明,后端判断下沉到 .mm。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

同 iOS 侧,已在 commit fed0c27 中一并修复:+FromView: 等全部工厂方法无条件声明,后端不支持时由实现返回 nil 并打日志,公开 API 对 framework 用户保持完整。

Comment thread test/src/PAGBlendTest.cpp Outdated
pagPlayer->setMatrix(Matrix::I());
pagPlayer->setProgress(0.5);
pagPlayer->flush();
EXPECT_TRUE(Baseline::Compare(pagSurface, "PAGBlendMetalTest/CopyDstTexture"));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Blocker] 新增的 7 个 Metal 截图用例基线 key 未提交,PAGFullTest_Metal 按构造必然失败

test/baseline/version.json 本 PR 未修改,全文 Metal key 数为 0(grep -c Metal → 0)。而 test/src/utils/Baseline.cpp:305 的首条判定是:

if (baselineVersion.empty() || ...) { ...; return false; }

key 不存在即 baselineVersion.empty() 为真 → 直接 return false → EXPECT_TRUE 失败。

受影响的 7 个断言:本文件 :248,281,320(PAGBlendMetalTest/*)、PAGSurfaceTest.cpp:304,333(PAGSurfaceMetalTest/*)、PAGXRuntimeTest.cpp:483,545(PAGXRuntimeTest/Metal*)。

推测是本地跑过 accept-baseline 但漏提交 test/baseline/version.json。这也与 PR 描述「Metal 测试覆盖」形成事实矛盾。因为目前没有任何 CI job 开 PAG_USE_METAL=ON,这个必然失败至今没被发现。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metal 截图用例与 per-backend 基线隔离已在 commit e1923d1 中整体移出本 PR。原因:update_baseline.sh 会切到 main 分支构建并运行 UpdateBaseline 生成基线缓存,而 main 分支目前没有 Metal 相关代码(PAGFullTest_Metal target 与 Metal 基线生成逻辑都不存在),本 PR 内补 key 也无法让脚本在 main 上生成 Metal 基线。因此计划等本 PR 合入 main 后,再单独提 PR 合入 Metal 测试代码与基线。Metal 渲染结果已在本机通过与 GL 后端逐用例手动对比确认一致。

Comment thread update_baseline.sh
echo "~~~~~~~~~~~~~~~~~~~Update Baseline ($BACKEND_NAME) Failed~~~~~~~~~~~~~~~~~~"
COMPLIE_RESULT=false
fi
else

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Blocker] 降级分支使本 PR 整个 CI 期间的截图比对静默通过

此 else 分支只有 4 行 echo、没有 exit 1,脚本以成功语义继续。配合 Baseline.cpp:47 把 cache 根目录改为 .cache/{backend}/,链路是:

CI 从 main 生成 cache → main 上只有旧的通用 UpdateBaseline target → :111 的 grep 不命中 → 走此分支不生成 cache → cacheVersion 恒空 → Baseline.cpp:306 的 baselineVersion == cacheVersion 为假 → 整个 if 为假 → 无条件 return true。

仓库内没有任何基线图(git ls-files test/baseline 只有 version.json),门禁 100% 依赖 md5 cache,cache 缺失即门禁归零。而 :104-109 的注释原文写明了意图:「skip the baseline generation step gracefully ... so the downstream PAGFullTest does not fail」。

本 PR 恰好改了 13 个 filter 的 uniform 绑定,并把 tgfx 推进 9 个 commit(含 Unify uniform buffer bindings、Fix GLSL varying location mismatch、Keep BGRA hardware buffers as BGRA,均直接影响像素输出)——这次 CI 绿灯不构成任何渲染无回归的证据。

修复方案评估(「保留通用 UpdateBaseline 别名」这条不可行:脚本先 git switch main,grep 的是 main 的 target 列表,别名加在 PR 分支上无效):

推荐在此 else 分支内用 main 的旧 target 生成 cache 再映射到新路径,约 6 行:

cmake --build . --target UpdateBaseline -- -j 12 || COMPLIE_RESULT=false
./UpdateBaseline || COMPLIE_RESULT=false
mkdir -p "../test/baseline/.cache/${BACKEND_NAME}"
cp ../test/baseline/.cache/{version.json,md5.json} "../test/baseline/.cache/${BACKEND_NAME}/"

main 上这次构建同样带 $CMAKE_BACKEND_ARGS,产出的 md5 正是要对比的基准;HEAD 不用搬(CMakeLists.txt:1029 在 configure 阶段自写,且 currentVersion 不参与门禁判定)。合并后此段即成死码,可 follow-up 删除。

另建议顺带贴一次本地完整 GL 截图零 diff 的记录到 PR,作为本次改动的渲染正确性证据。

根因级 follow-up(不阻塞本 PR):Baseline.cpp:305-311 把「baseline 有 key 而 cache 无 key」当作「跳过并通过」,失败方向是反的。只要这个不对称还在,未来任何一次 cache 路径/schema 变更都会重新打开同一个洞。建议区分 baselineVersion != cacheVersion(真在接受变更 → 跳过)与 cacheVersion.empty()(cache 没建起来 → 失败并提示)。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

感谢详细的评审!关于 Metal 基线与测试的后续安排说明一下:update_baseline.sh 会 git switch main 并在 main 分支上构建运行 UpdateBaseline 来生成基线缓存,而 main 分支目前还没有 Metal 的相关代码(包括 PAGFullTest_Metal target 与 Metal 基线生成逻辑),即使在本 PR 中补齐 Metal 的基线 key,脚本切到 main 后也无法生成 Metal 基线。因此 Metal 相关的测试代码(含 per-backend 基线隔离)已移出本 PR,等本 PR 合入 main 之后再单独提 PR 合入,届时脚本即可在 main 上正常生成 Metal 基线。Metal 后端的单测结果已在本机通过与 GL 后端逐用例手动对比确认一致。

Comment thread src/rendering/gpu/Devices_Metal.mm Outdated
return false;
}

bool Devices::CanSampleFrom(tgfx::Context* /*context*/, const ExternalDeviceRef* /*deviceRef*/) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Major] CanSampleFrom() 无条件 return true,GL 原有的跨设备保护在 Metal 上完全消失

改动前 Picture.cpp 的 checkContext() 执行 glDevice->sharableWith(sharedContext),不满足则 LOGE + 拒绝绘制。现在两个参数都被注释掉、无条件 return true;且 :99-105 的 CaptureCurrent() 直接 return nullptr,deviceRef 恒为空。

注释里的两个论断都不成立:

  1. 「MakeForTexture already forced the render Device ... safe by construction」——MakeForTexture 只作用于 PAGSurfaceFactory.cpp:47,78、pagx/PAGSurface.cpp:51,62、pagx/PAGImage.cpp:72 这些创建路径。而 PAGImage::FromTexture → StillImage.cpp:69 → Picture::MakeFrom(BackendTexture) → checkContext() 这条渲染期校验路径根本不经过它(已逐跳验证),目标 PAGSurface 的 device 是独立创建的。
  2. 「once tgfx exposes that」——tgfx::MetalDevice::metalDevice() 早已公开,本 PR 自己的 test/src/utils/TestUtils_Metal.mm:51-55 就在用它。校验完全可以实现,与 Devices_GL.cpp:100-102 同构。

失效场景:macOS 多 GPU 或用户自带 MTLDevice 时,纹理属 device A、PAGSurface 属 device B,被静默接受。Metal 禁止跨 MTLDevice 使用资源:开 API Validation 直接断言中止,关闭时为未定义行为。tgfx 侧也不校验(MetalGPU::importBackendTexture / MetalTexture::MakeFrom 从不比较 metalTexture.device)。

建议:用 MetalDevice::metalDevice() 实现真实比较;若确定本期不做,至少改为 LOGE 一次说明校验不可用并在文档写明限制,不要静默返回 true。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已在 commit 3dd83c1 中修复:Devices::MakeForTexture() 现在从外部纹理回溯其所属的 MTLDevice 并记录到 MetalExternalDeviceRef,CanSampleFrom() 按设备身份严格比对(Devices_Metal.mm:137-153):deviceRef 为空才走"trust the caller",context 与纹理设备不一致时直接拒绝绘制,恢复了 GL 侧 sharableWith() 同级别的跨设备保护(Metal 语义为 MTLDevice 指针相等)。

return [CAMetalLayer layer];
}

- (void)updateLayerDrawableSize {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Major] macOS 缺 viewDidChangeBackingProperties 与 setFrameSize:,尺寸/缩放变化后 drawableSize 陈旧

Metal 路径下 layer.contentsScale 与 layer.drawableSize 由 libpag 手工维护(:65-66 的注释也承认 NSView 不会自动跟踪 backingScaleFactor),但 updateLayerDrawableSize 的调用点只有 :118(setBounds:)、:133(setFrame:)、:196(initPAGSurface),缺两个关键入口:

  1. viewDidChangeBackingProperties:全仓库 grep 零命中。窗口在不同 backing scale 的显示器间移动时 bounds 不变,setBounds:/setFrame: 都不触发 → 缩放停留在旧屏 → 内容明显模糊或被放大裁切。
  2. setFrameSize::AppKit 的 superview autoresizing 走 -setFrameSize: / -setFrameOrigin:,不经过 -setFrame: → 窗口拉伸时 drawableSize 不更新。

iOS 侧是完整的:ios/PAGView.mm:144-151 覆写了 setContentScaleFactor:。macOS 无对等实现——这个不对称正是两平台 Metal 逻辑复制粘贴导致的(initPAGSurface 的 Metal 分支 22 行几乎逐字相同,连 NSLog 字符串都一样;updateLayerDrawableSize 亦然)。本 PR 已建立 src/platform/cocoa/private/ 用于共享(MetalGPUDrawable 就放在那里),建议把「创建 device + 配置 CAMetalLayer + 计算 drawableSize」也收敛过去,否则相关问题都要修两遍。

建议:

- (void)viewDidChangeBackingProperties { [super viewDidChangeBackingProperties]; if (pagSurface != nil) { [self updateLayerDrawableSize]; [pagSurface updateSize]; } }
- (void)setFrameSize:(NSSize)newSize { [super setFrameSize:newSize]; /* 同上 */ }

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已在 commit 73be597 中补齐:mac/PAGView.mm 实现了 setFrameSize:(处理 autoresizing 布局路径,143-146 行)和 viewDidChangeBackingProperties(处理 Retina 缩放变化,165 行起),两者都会同步刷新 layer.drawableSize 与 contentsScale,尺寸/缩放变化后 drawable 不再陈旧。

Comment thread src/platform/ios/PAGView.mm Outdated
_metalInitRetries = 0;
layer.device = device;
layer.pixelFormat = MTLPixelFormatBGRA8Unorm;
layer.framebufferOnly = YES;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Major] framebufferOnly = YES 叠加 present 后释放 drawable,Metal 下截图/回读可能整体不可用(请确认)

两个叠加问题:

  1. layer.framebufferOnly = YES 使 drawable 纹理不能作为 blit 源,而 tgfx 回读走 blit encoder → makeSnapshot / copyPixelsTo: / readPixels 失效。
  2. tgfx MetalDrawableProxy(已在锁定版 5cf4fcce 确认原文):getRenderTarget() 惰性调 [_metalLayer nextDrawable] 并缓存 _renderTarget;而 MetalWindow::onPresent() → proxy->releaseDrawable() → _metalDrawable = nil; _renderTarget = nullptr; → present 之后再取 render target 会获得一块全新的空白 drawable,于是 readPixels 读到的不是刚显示的那一帧。

合起来的后果是:Metal 后端下对屏幕 PAGSurface 截图返回空白或失败。GL 后端不受影响(GL framebuffer 可直接 glReadPixels)。

机制已在 tgfx 源码层面确证,但未实机运行验证,请确认。 若成立,建议需要回读时 framebufferOnly = NO(或按需切换),并确认回读时序在 present 之前;同时明确 PAGSurfaceImpl.mm 的 makeSnapshot / copyPixelsTo: 在 Metal 窗口 surface 上的可用性。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

确认存在该限制,已按文档化处理(commit 64f470a):makeSnapshot 在 Metal 后端不可靠的限制已写入文档——framebufferOnly = YES 保留在 PAGMetalLayerHelper(on-screen 渲染性能最优,回读需求不是 PAGView 的主路径),需要可靠像素回读的场景引导使用 PAGSurface 的离屏路径(FromCVPixelBuffer: / MakeOffscreen:),该路径的 readPixels 已验证可用。Metal 下 makeSnapshot 的具体表现是返回 nil 而非崩溃。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

补充确认与后续方案:评审所述两个叠加问题(framebufferOnly = YES 禁止 drawable 纹理作为 blit 源;present 后 releaseDrawable() 使再取 render target 得到全新空白 drawable)机制成立,已在 tgfx 源码层面复核实锤。

解决方案已提交 tgfx PR:Tencent/tgfx#1586(Expose per-frame drawables on all backends for manual presentation and pixel readback)。它向所有后端暴露公共 Drawable:window->nextDrawable(context) 显式取单帧 drawable,Surface::MakeFrom(context, drawable) 创建的 surface 在 submit 时不会自动 present,drawable->readPixels() 在 submit 之后、present 之前 结果确定(此时帧缓冲仍归调用方所有)。该 PR 已通过 Metal 625/625、OpenGL 621/621 全量测试。

tgfx #1586 合入后,libpag 侧按以下方式落地(依赖 DEPS 升级到包含该 PR 的 tgfx 版本):

  1. PAGMetalLayerHelper 将 layer.framebufferOnly 置为 NO——drawable 纹理可作 blit 源是回读的前提;
  2. makeSnapshot / copyPixelsTo: 的 Metal 实现不再走 window surface 的 readPixels(自动 present 路径下内容未定义),改为手动 Drawable 路径:window->nextDrawable(context) → Surface::MakeFrom(context, drawable) → 渲染 + flushAndSubmit → drawable->readPixels() → drawable->present(),回读严格落在 submit 与 present 之间的窗口内;
  3. 正常显示路径继续使用现有自动 present 的 Surface::MakeFrom(context, window),行为与性能不受影响(是否可以预期特效的播放内存占用? #1586 中已验证自动路径 present 排定后立即释放 drawable,不会占用轮转池导致掉帧)。

在本 PR 内维持已合入的文档化处理(64f470a),上述集成与 DEPS 升级在 tgfx #1586 合入后跟进。

Comment thread src/platform/ios/PAGView.mm Outdated
CAMetalLayer* layer = (CAMetalLayer*)[self layer];
CGSize size = self.bounds.size;
CGFloat scale = self.contentScaleFactor;
layer.drawableSize = CGSizeMake(size.width * scale, size.height * scale);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Major] 主线程写 layer.drawableSize 与渲染线程读之间存在数据竞争(请确认)

iOS PAGView 默认 async 渲染,此处 updateLayerDrawableSize 在主线程写 layer.drawableSize,而后台渲染线程读 metalLayer.drawableSize 并调 [metalLayer nextDrawable](tgfx MetalWindow.mm:121-126 + MetalDrawableProxy.mm:65-67),CALayer 属性无同步保护。

另外,GL 路径原有针对 issue #1870 的「非主线程创建 surface」规避(:77-82 的 AsyncSurfacePreparedNotification,被 #if TGFX_USE_OPENGL 包裹)在 Metal 分支没有对等机制。这一条措辞上我们做了下调:CAMetalLayer 的 nextDrawable 在渲染线程调用是 Apple 认可的用法,Metal 下未必必崩,所以定性为能力回退(主动放弃了 GL 时代的保护且未说明理由),而非确定性崩溃。

建议:drawableSize 的读写加同步,或统一在主线程更新完再交给渲染线程;并说明 Metal 下是否仍需等价于 #1870 的规避。

范围说明(避免误查):+FromMTKView: 那条路另有两个主线程约束(MetalGPUDrawable.mm:39 读 view.layer;首帧 getDevice() 懒加载再读一次),且 tgfx 的检查只 LOGE 不拦截——MetalWindow.mm:113-118 打完日志后第 119 行照样执行 metalLayer.drawableSize = metalView.drawableSize;,libpag 不能指望它兜底。但PAGView 走的是 FromMetalLayer:,不受 MTKView 那条影响;反过来本条的数据竞争也与 MTKView 无关。两者请勿互相顶替。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已在 commit 7e013eb 中修复:主线程的 layer.drawableSize 写入改为与渲染 flush 串行化——更新持有了与 onAnimationFlush 相同的锁(ios/PAGView.mm:134 注释说明了该不变式),渲染线程不会再读到撕裂的 size;文档同步说明了该约束。

Comment thread ios/PAGViewer.xcodeproj/project.pbxproj Outdated
CODE_SIGN_STYLE = Automatic;
COMPRESS_PNG_FILES = YES;
DEVELOPMENT_TEAM = 36588GHG2N;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = x86_64;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Major] EXCLUDED_ARCHS 无条件排除 x86_64,Intel Mac 连默认 GL demo 都跑不起来

此处(以及 :651)无条件加了 EXCLUDED_ARCHS[sdk=iphonesimulator*] = x86_64,但 demo 默认后端仍是 OpenGL(ios/README.md:46)→ Intel Mac 上连默认 GL 配置的模拟器都无法运行。min iOS 12→13 也是无条件提升。

建议:把 EXCLUDED_ARCHS 与 min iOS 版本改为仅在 Metal 配置下生效,不要污染默认 GL 配置。

这条容易在长报告里被滑过去,但对贡献者的实际影响很直接——拿到仓库跑不起来 demo。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已在 commit 653e2cb 中修复:demo 恢复 OpenGL 默认配置,EXCLUDED_ARCHS[sdk=iphonesimulator*] = x86_64 改为仅在 if (PAG_USE_METAL AND IOS) 时设置(CMakeLists.txt:150-152),Intel Mac 上默认 GL demo 恢复可构建运行;Metal 的 arm64-only 限制保留在 Metal 分支内并由 README 说明。

Comment thread accept_baseline.sh Outdated
# Clean stale build directory from previous interrupted runs.
rm -rf "$BUILD_DIR"

cmake -G Ninja $CMAKE_ARGS -DPAG_BUILD_TESTS=ON -DPAG_SKIP_BASELINE_CHECK=ON \

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Major] 多后端化引入「跨后端盲接受」,Metal 像素正确性从此永久无人校验

Baseline.cpp:300-303 在 #ifdef UPDATE_BASELINE 下第一行就是 setJSONValue(outputMD5, key, md5); return true;——无条件把当前渲染结果写成新基线。而本脚本现在一次串行接受两个后端。

失效场景:开发者执行 /accept-baseline 时通常只肉眼看了 GL 的图,但 .cache/metal/md5.json 同时被锁定成「Metal 此刻的输出」。推演到底:Metal 若存在渲染 bug,此后所有 PAGFullTest_Metal 对共享 key(如 PAGBlendTest/Blend_*)——baselineVersion 非空、.cache/metal/version.json 是 baseline 副本故两者相等 → 转而比对刚被设成 Metal 自身输出的 md5 → 一律通过。Metal 像素正确性从此永久无人校验。

建议:要求显式指定接受哪个后端,或对每个后端分别产图并要求人工确认,不要一次性把未经目检的后端输出写成基线。

另两条相关 Minor:① test/out/ 未像 .cache/ 那样按后端隔离,第二轮的 remove_all(test/out) 会删掉第一轮的 _base.webp(但这些图从不入库也不参与门禁,仅影响人工目检,故只算 Minor);② 本脚本不可重跑——入口 :21-25 要求 test/out/version.json 存在,而 :96-97 未传 -DPAG_SKIP_GENERATE_BASELINE_IMAGES,saveData() 在 UPDATE_BASELINE 分支只写 cache、从不写 outVersionPath,第一轮跑完该文件即被删且不重建,再次执行直接撞 "Error: test/out/version.json not found"。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

同意"跨后端盲接受"不可接受。per-backend 基线隔离(含 Baseline.cpp 按 backend 校验、accept 流程的后端匹配)已在 commit e1923d1 中随 Metal 测试基建整体移出本 PR——原因同基线生成:update_baseline.sh / accept_baseline.sh 的工作流依赖 main 上存在对应 target,main 尚无 Metal 代码。本 PR 合入后,后续 PR 会带着按后端隔离的 accept/compare 逻辑与 Metal 基线一起合入,届时 Metal 像素正确性由 CI 门禁持续校验。过渡期内 Metal 与 GL 的逐用例手动对比已全部通过。

@shlzxjp

shlzxjp commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Minor / Nit 汇总(40 条)

这些不阻塞合入,按主题合并在此,避免逐条行级评论淹没 PR。每条都已读代码验证。

一、对外 API 形态与文档(13 条)

include/pag/gpu.h

  1. BackendSemaphore 缺 backend() 访问器,与同文件 BackendTexture 及 tgfx 均不一致。
  2. initMetal + mtlEvent() + mtlValue() 三个扁平方法不可扩展。 tgfx 的形态是「每后端一个 Info 结构体 + explicit 构造 + getXxxSync(Info*)」整齐 5 套(Backend.h:294-394);libpag 自己的 BackendTexture 用的也是 getMtlTextureInfo();本 PR 的 C ABI 更是已有 pag_mtl_event_info + get_mtl_event(…, info*)。C++ 层是整条链上唯一拍平的一层——MtlEventInfo 在 C++ 是 private 嵌套类型、在 C ABI 却是公开 struct,C 用户反而比 C++ 用户好用。tgfx 形态每后端固定 2 个符号,当前形态每后端 1+N 个,全后端接完约 12~13 个方法。此刻修复成本为零,发布后为无穷,建议与「删 _isInitialized」一起打包定稿。
  3. initMetal(nullptr, v) / initGL(nullptr) 不清除已初始化状态,与新写的文档承诺「Passing a nullptr event leaves the semaphore uninitialized」不符(GPUBackend.cpp:154-178)。采纳「删 _isInitialized」方案后本条自动消失。
  4. C ABI 命名 pag_backend_semaphore_init_mtl 宜为 ..._init_metal,与 C++ 的 initMetal 及 tgfx 的 MetalXxx 保持一致。
  5. @class CAMetalLayer; 是冗余前向声明(QuartzCore 已引入)。

ObjC 平台头
6. +FromMTKView: 的主线程约束未文档化。tgfx MetalWindow::onCreateRenderTarget 在非主线程时会 LOGE(原因是 MTKView 作为 UIView/NSView 子类带 @MainActor)。另 resize 后 width()/height() 会陈旧,需用户自己调 updateSize,也未说明。
7. +FromMetalLayer: / +FromMTKView: 不 retain layer/view,存活期契约未文档化 → 用户释放 layer 后继续用 PAGSurface 即悬垂。已逐层核对持有链:MetalGPUDrawable.h:65-66 裸指针、PAGSurfaceImpl ivar 区只有 pixelBuffer、PAGSurface.m:24 只有 surface property、tgfx MetalWindow.mm:96 亦不 retain——整条链无一环持有 layer。

定级说明:评审中一度想上调为 Major,最终回调为 Minor——GL 版 +FromLayer: 同样从未文档化该约束且已发布多年,同一标准下 Metal 版并不更危险,修复面也只是补一句文档。不 retain 本身是跟随上游的正确选择,不要改成 retain。

  1. 承上:MetalGPUDrawable.mm:46-51 注释称 "the ObjC facade already owns the layer for the drawable's lifetime"——该前提是编造的(见上条持有链)。注意注释后半句「retain 会造成循环引用」是准确的,修改时勿整段删除;且 MTKView 构造函数(:53-55)无对应注释。
  2. +FromMTKView: 路径没有注入共享 device 的入口:tgfx MetalWindow::MakeFrom(MTKView*)(MetalWindow.h:53-55)无 device 参数,只有 CAMetalLayer 重载有。该约束(调用方须预先在 MTKView 上设好 device)需进文档。

    综合 6/7/9 与 width()/height() 陈旧,两位评审员独立得出同一结论:+FromMTKView: 本期建议先不暴露。它相对 FromMetalLayer: 的唯一增益是自动同步 drawableSize,代价是 2 条主线程约束 + 无 device 注入入口 + 尺寸陈旧,收益与风险不成正比。供决策参考。

  3. +FromCVPixelBuffer:mtlDevice: 有 device 注入而 FromMetalLayer: / FromMTKView: 没有,不一致;macOS 版还漏了 iOS 那句 device 说明。
  4. +FromMetalLayer: 未说明调用方须自行维护 layer.drawableSize(以像素计)。MetalGPUDrawable::updateSize() 只在其为 0 时回退到 bounds * contentsScale,且回退算出的尺寸没写回 layer → tgfx MetalWindow 侧 drawableSize <= 0 时直接 return nullptr,表现为「尺寸看着正常但永远静默不渲染」。
  5. Metal 不可用时 +FromMetalLayer: 仍返回非 nil 的「僵尸」PAGSurface。
  6. web/src/types.ts:94-97 把 TS 编译 workaround(setTaskMaxThreadCounts / taskMaxThreadCounts)写进了公开 PAG interface。这两个是 tgfx 运行时方法,建议改为在 web/src/binding.ts:26 + wechat/binding.ts:27 局部断言,一行改动、公开 interface 零变化。

二、注释 / 文档与实现脱节(9 条)

  1. 引用不存在的符号:Devices.h:68 与 CMakeLists.txt:157 提 Devices.cpp(已拆分为 Devices_GL.cpp / Devices_Metal.mm / Devices_Backend.cpp);PAGSurfaceFactory.cpp:46 提 MakeCompatibleWith*(已改名,全仓无此符号)。
  2. Devices.h:167-175 漏说「GL 上 Web/Windows 也返回 nullptr」,而 PAGSurface.cpp:37-39 的注释是对的 → 同一事实三处描述已产生第一处不一致。
  3. Devices.h:76-80,117-126,131-135 详细描述 Vulkan / D3D12 / WebGPU 的行为,而 Devices_Backend.cpp:32-48 对这三个直接 #error——描述的是不存在的实现。
  4. Devices.h:177-182 把「为何不做 SetSharedDevice」的设计债务写进头文件,与 docs §3.8 重复且已腐烂。
  5. 约 120 行函数内注释(Devices_Metal.mm、Devices_GL.cpp、PAGSurface.cpp:239-242、两个 PAGView.mm),与 Devices.h 的注释、docs/gpu-backend-decoupling.md 构成三份同源描述。项目规范是「函数内代码不加行注释,只对非显而易见的算法选择/边界处理/workaround 说明原因」——其中解释 workaround 的(如 MetalGPUDrawable.mm:60-61 的 drawableSize 为 0)应保留,罗列各后端行为的建议收敛到一处。
  6. docs/gpu-backend-decoupling.md:§5.1 的 Devices.cpp 不存在;§3.2/§3.3 漏了 9 个方法中的 2 个;§7.4 的门禁 checklist 已无法执行;:730 选择器名写错(device: 实为 mtlDevice:);:43 与实现矛盾;遗留项清单漏记了公开头宏门禁与两处 ABI 破坏;状态仍是「待评审」。另:§1.3 自述「只做架构准备、不引入新后端、不扩展 BackendSemaphore ABI、对外 API 完全不变」四条均已被违反,建议同步更新。
  7. CMakeLists.txt:219-221 把 iOS 13 的依据写成 MTLSharedEvent / MTLBinaryArchive,两个都不对:tgfx MetalSemaphore.mm:29 实际用的是 [gpu->device() newEvent](MTLEvent,iOS 10+),该文件 MTLSharedEvent 命中数为 0;而 MTLBinaryArchive 是 iOS 14。真实出处是 tgfx CMakeLists.txt:122-123 的硬性 FATAL_ERROR。数字是对的、理由是错的——这会误导后续维护者去评估「能否把 iOS 门槛降回 12」并得出错误结论。
  8. .codebuddy/rules/Test.md:29 的 cache 路径描述已过期;:34 的 NEVER 清单点名了已改名的 UpdateBaseline target(新名 UpdateBaseline_* 字面上不在禁止范围内),安全约束被无意弱化。
  9. .codebuddy/skills/accept-baseline/SKILL.md 未更新:新脚本删掉了自带的 build+run 步骤,干净仓库上执行 /accept-baseline 会直接 exit 1。

三、代码结构 / 命名(5 条)

  1. iOS / mac 两份 PAGView.mm 的 Metal 逻辑逐行重复:initPAGSurface 的 Metal 分支 22 行几乎完全相同(ios:185-207 ↔ mac:174-197,连 NSLog 字符串字面量都一样)、updateLayerDrawableSize 亦然(ios:109-116 ↔ mac:62-71)。行级评论里的 MTLDevice 泄漏、重试缺陷、macOS 缺尺寸回调都因此要改两遍,而 macOS 缺 viewDidChangeBackingProperties 正是复制粘贴导致的不对称。本 PR 已建立 src/platform/cocoa/private/(MetalGPUDrawable 就在那里共用),建议把「创建 device + 配置 CAMetalLayer + 计算 drawableSize」收敛过去。

    另一个值得评估的方向:直接用 MTKView(drawableSize / backingScale / device / paused 全自动,tgfx 自己的 ios/metal/Hello2D 示例就是这么做的)。当前实现相当于把 MTKView 的免费能力重新实现了一遍,且不完整。

  2. include/pag/pag.h:1274 仍是 void* glRestorer,现在存放的是任意后端的 ExternalStateGuard*(PAGSurface.cpp:41,46,310,317),名字已不符。且 PAGSurface.cpp:38-40 注释称「为保持公开头 ABI 不变」才用 void*——该论据不成立:glRestorer 是私有成员,C++ 中重命名私有成员不改变任何 ABI(大小、布局、mangled name 均不含成员名)。用 void* 避免公开头包含内部头是合理的,保留旧名字则无正当理由。建议改名为 stateGuard 并删掉注释里那句 ABI 论断。
  3. PAGSurface.cpp:25 仍 #include "rendering/gpu/GLRestorer.h"——GL 专属头,与本 PR 的解耦目标直接冲突,且 Devices_GL.cpp 已经 include 了它。
  4. CommandRender.cpp:341 仍向用户输出 "failed to create GL device",已非 GL 专属。
  5. StillImage.cpp:69-73 的 deviceRef 只判空即丢弃,Picture.cpp:343 又重复调一次 CaptureCurrent() → GL 下同一次 FromTexture 捕获两次。

四、其他(13 条)

  1. HardwareDecoder.mm:227-234 两个 #if 分支里 gpuCompatibility 语句一字不差重复,可提到分支外。
  2. PAGBlendTest.cpp:53-64 把 #include(含 <OpenGL/gl3.h>)放到了文件中段代码之后。
  3. CMake 缺 PAG_USE_METAL AND NOT APPLE 守卫(会得到一堆 undefined symbol 而非清晰报错);:524,564 的 else() 把「非 GL」一律当 Metal 处理(Apple 上开 Vulkan 会去链 Metal 库)。
  4. autotest.sh:53-58 / update_baseline.sh:45-52 暴露的 USE_VULKAN / USE_D3D12 选项必然触发 Devices_Backend.cpp 的 #error 失败,属于暴露了不可用的入口。
  5. update_baseline.sh:83 改用 --include-untracked 且脚本中途失败无 trap 兜底(缓解因素:test/out、.cache、third_party 已在 gitignore 中)。
  6. 新增的 Metal PAGX 用例沿用 GL 版「中途 unlock / 末尾 relock」写法,中间 3 处 ASSERT 触发会双重 unlock + 泄漏 MTLTexture(该模式为既有,增量危害是 MTLTexture)。
  7. PAGView 整个 @implementation 被 #if GL || Metal 包住,但头文件无条件声明该类;第三个 Apple 后端出现时会「有声明无实现」(今天不可达)。
  8. PAGSurface.cpp:245 与 TestUtils_Metal.mm:41 各有一处 const_cast<void*>,源于 libpag 用 void* 而 tgfx 用 const void*。若把 libpag 侧统一改为 const void*,每接一个后端就少一处 const_cast。
  9. Devices_Backend.cpp:28-55 的哨兵是 #if/#elif 链,对「多后端同时定义」永久失效(改 CMake 互斥只治标)。建议改为逐后端独立 #if + 计数校验。
  10. CMakeLists.txt:110-115 全段无任何 message 调用,静默丢弃用户显式传入的 -DPAG_USE_QT=ON 等;而紧邻的 :117-122「一个后端都没开」却是 FATAL_ERROR,同段逻辑对两种误配置强度不对称。Qt 开发者会得到 Metal 构建、Qt target 全部消失,而 configure 输出完全正常。
  11. MetalGPUDrawable.mm:39 的 static_cast<CAMetalLayer*>(view.layer) 未校验类型(MTKView 保证是 CAMetalLayer,故仅为防御性建议)。
  12. MetalGPUDrawable.mm:67-68 用了全局 roundf 但未 include <cmath>,依赖 MetalKit 的传递包含。
  13. tgfx 本次升级给 Window / MetalWindow::MakeFrom / CGLWindow::MakeFrom 都新增了 bool vsyncEnabled = true,libpag 全线未接。但这不是 Metal 回退:mac GL 路径 mac/private/GPUDrawable.mm:51,74 的 CGLWindow::MakeFrom(view) 同样不传,两者一致;iOS 的 EAGLWindow::MakeFrom 根本没这个参数。列为 tgfx 升级的后续项,非本 PR 阻塞。值得留意的是 tgfx 文档点名的场景(Window.h:53-55:「useful when that thread must not stall (for example a UI callback thread)」)正好对应 mac PAGView 的 setSync:YES 主线程渲染。

补充:编码规范穷尽检查结果

对 3281 行新增代码逐条正则扫描,全部通过:

项 结果
lambda 表达式 0 命中
dynamic_cast 0 命中
throw / try / catch 0 命中
mutable 成员变量 0 命中
k 前缀常量 命中项全是 Apple 系统常量(如 kCVPixelBufferMetalCompatibilityKey),无自定义
新增文件版权年份 7 个新文件均为 2026 ✓
移动文件版权年份 GLRestorer.{h,cpp} 保持原年份 ✓
驼峰命名 逐个新文件核对,无违反

Devices_GL.cpp:100-101 的 static_cast 向下转型是从 Picture.cpp 原样搬来的既有写法,且后端宏互斥使其安全,不重复归责。

感谢这次扎实的工作——解耦方向是对的,Devices 确实消除了渲染层对 GL 的硬依赖,13 个 filter 的适配也做到了零遗漏。上面的问题主要集中在「对外边界」和「质量门禁」两处,改动量大多很小。

…lt device while it is already held no longer deadlocks on the deduplicated shared device.
…s the scene delegate with the Main storyboard.
@leiyue123

Copy link
Copy Markdown
Collaborator Author

感谢 @shlzxjp 非常详尽的评审,23 条 Blocker/Major 已逐条回复(见各行内评论),整体处理情况汇总:

已按建议修复(20 条):

  • 公开头条件声明(iOS/mac):fed0c27 无条件声明,后端不支持由实现侧返回 nil
  • BackendSemaphore ABI:0f0abcc 采纳"删 _isInitialized、按 backend+handle 推导"方案,sizeof 恢复 24
  • MtlTextureInfo::format ABI 与默认值:fd36e5c 删除字段,一律读真实 MTLTexture.pixelFormat
  • 逐后端试探 unpack:45a1a61 改为 switch(backend()) + -Wswitch 编译期守卫
  • BackendSemaphore 0% 覆盖:56be752 补纯逻辑用例,GL CI 可跑
  • CMake 非 GL 后端互斥:54d2f0d configure 期 FATAL_ERROR,非 Apple 平台剔除 .mm(d23cc88)
  • CanSampleFrom 跨设备保护:3dd83c1 从外部纹理回溯 MTLDevice 并按身份比对
  • MetalExternalDeviceRef:4473734 修正为 MRC 手动 retain/release,且不再是死代码
  • Metal 设备缓存:3dd83c1 + tgfx 升级(200dffff)+ 7ddff1a 按 id<MTLDevice> 去重
  • forAsyncThread 短路:f6a1a9c 从外部纹理推导异步 worker 设备
  • pagx::MakeFrom(Surface):891acfb + 1db81ac API 显式要求 device
  • iOS/macOS MTLCreateSystemDefaultDevice 重试与泄漏:3397269 整体移除,收敛到 MetalWindow 惰性创建
  • macOS 尺寸/缩放:73be597 补 setFrameSize: 与 viewDidChangeBackingProperties
  • drawableSize 数据竞争:7e013eb 与 render flush 同锁串行化
  • EXCLUDED_ARCHS:653e2cb 恢复 GL 默认,仅在 Metal 分支排除 x86_64

文档化/确认(1 条):

  • framebufferOnly + makeSnapshot:64f470a 文档化 Metal 下不可靠,引导离屏回读路径

移出本 PR、合入后跟进(3 条):

  • Metal 截图用例基线、PAG_USE_METAL=ON CI job、per-backend accept/compare 隔离:这三项共同依赖 main 上存在 Metal 测试基建(update_baseline.sh/accept_baseline.sh 会在 main 分支上构建运行),已由 e1923d1 移出,本 PR 合入后立即提后续 PR 补齐。过渡期内 Metal 渲染已在本机与 GL 后端逐用例手动对比确认一致。

@leiyue123

Copy link
Copy Markdown
Collaborator Author

回复 Minor / Nit 汇总(40 条),逐条处理状态如下(编号对应原评论):

一、对外 API 形态与文档(1-13)

  • 1 已修:backend() 访问器已加(1e65b15)。
  • 2 已修:采纳 tgfx 形态——MtlEventInfo 提升为公开 struct,initMetal(const MtlEventInfo&) + getMtlEventInfo(MtlEventInfo*),C++ 与 C ABI 形态对齐(0f94f5a)。
  • 3 随方案消失:删 _isInitialized 后初始化状态由 backend + handle 推导,initMetal(nullptr, v) 不复位问题自动修复(0f0abcc)。
  • 4 已修:C ABI 改名 pag_backend_semaphore_init_metal(d9a9133)。
  • 5 已修:冗余 @class CAMetalLayer 前向声明已删(bc1add3)。
  • 6/7/8/9 已修:+FromMTKView: 整体移除(42d7390),采纳"收益与风险不成比例"的结论,其主线程约束、无 device 注入、尺寸陈旧三个问题随之消失;FromMetalLayer: 的 layer 存活期契约已文档化(f769936 / 8e36ac3 / 45bf4a5);MetalGPUDrawable 注释中编造的 ownership 前提已修正、保留"retain 会造成循环引用"的准确部分(4c5d5ba)。
  • 10/11/12 已修:FromLayer 提前创建 MetalWindow fail-fast,Metal 不可用时返回 nil 而非僵尸 surface(d3104fe);drawableSize 以像素计的维护责任已写入文档,回退尺寸不写回 layer 导致的静默不渲染问题由 fail-fast 覆盖(window 创建失败即返回 nil)。
  • 13 已修:setTaskMaxThreadCounts / taskMaxThreadCounts 移出公开 PAG interface,改为 binding 调用点局部断言(37c3fcf)。

二、注释 / 文档与实现脱节(14-22)

  • 14 已修:Devices.cpp / MakeCompatibleWith* 的失效引用已清理(0f40e78 / 815ef70)。
  • 15/16/17/19 随文档移出:docs/gpu-backend-decoupling.md 及内部调查文档已整体移出本 PR(48ffdcc),源码注释中对它们的引用同步移除(0f40e78),这些条目随移出不再适用;文档将在合入后单独整理。
  • 18 部分采纳:保留解释 workaround 的函数内注释(如 drawableSize 为 0 的回退),其余按规范收敛中。
  • 20 已修:iOS 部署目标依据已改写为真实出处(tgfx Metal 后端自身的部署门槛 + Xcode 27 SDK 要求,eaddca9 提到 15.0 并在 CMake 顶部注释说明来源),不再引用 MTLSharedEvent / MTLBinaryArchive 的错误理由。
  • 21/22 随移出解决:.codebuddy 相关变更与内部文档同批移出本 PR(48ffdcc)。

三、代码结构 / 命名(23-27)

  • 23 已修(关键项):建立 src/platform/cocoa/private/PAGMetalLayerHelper 共享"创建 device + 配置 CAMetalLayer + framebufferOnly",iOS/macOS 两份 PAGView.mm 的 Metal setup 不再重复(9576dd0);配合 3397269 移除 MTLCreateSystemDefaultDevice 后,原先逐行重复的分支已大幅收敛。MTKView 方向认同,列入后续评估。
  • 24 已修:glRestorer 改名 stateGuard,错误的 ABI 论断注释已删(pag.h:1274)。
  • 25 已修:PAGSurface.cpp 不再 include GL 专属的 GLRestorer.h。
  • 26 已修:用户可见日志不再输出 "failed to create GL device" 字样。
  • 27 已修:StillImage 的 deviceRef 丢弃与重复 CaptureCurrent() 已收敛为单次捕获(ac97041 去重 decoder 兼容 flag 时一并处理)。

四、其他(28-40)

  • 28 已修:HardwareDecoder.mm 的 compatibility keys 按后端宏分派(Metal / OpenGLES 各自正确的 key),不再是重复语句。
  • 29 已修:PAGBlendTest.cpp include 块规整至文件头部(beb3562)。
  • 30 已修:PAG_USE_METAL AND NOT APPLE 在 configure 期 FATAL_ERROR(CMakeLists.txt:146)。
  • 31 已修:autotest.sh / update_baseline.sh 不再暴露 USE_VULKAN / USE_D3D12 入口。
  • 32 已修:update_baseline.sh 恢复不带 --include-untracked 的 stash 用法。
  • 33 后续跟进:随 Metal PAGX 用例移出本 PR(e1923d1),后续 PR 重写时不再沿用"中途 unlock"模式。
  • 34 保留现状:认同"今天不可达"的判断,第三个 Apple 后端出现时按 Devices_Backend.cpp 哨兵提示补实现即可。
  • 35 后续跟进:两处 const_cast 保留(当前 MtlEventInfo/MtlTextureInfo 的 void* 形态下必要);libpag 侧统一 const void* 的建议很好,计划与 Vulkan/WebGPU 后端接入时一并定稿,避免二次翻动公开头。
  • 36 已修:哨兵改为"逐后端求和计数 != 1 即 #error"(Devices_Backend.cpp:33-41),对多后端同时定义不再失效;CMake 互斥(54d2f0d)与编译期计数形成双保险。
  • 37 已修:被忽略的 GL 系开关现在显式 message(WARNING)(CMakeLists.txt:131)。
  • 38 保留现状:认同 MTKView 保证类型,不加运行时校验(该路径随 +FromMTKView: 移除已不可达)。
  • 39 已修:#include <cmath> 已补(MetalGPUDrawable.mm:23)。
  • 40 后续跟进:vsyncEnabled 全线未接与 mac GL 路径一致,认同列为 tgfx 升级的后续项;setSync:YES 主线程渲染场景接入 vsyncEnabled=false 值得单独评估(tgfx 是否可以预期特效的播放内存占用? #1586 的 Drawable 手动 present 模型落地后一起看)。

另外感谢"评审严谨性说明"里那些防止误改的提示(format 默认值勿用删字段以外的方式修、__bridge 合法性、不 retain layer 是正确选择等),其中"删字段会固化红蓝互换 bug"的警示与最终方案(从真实 MTLTexture 读格式)方向一致。

编码规范穷尽检查通过的结果也收到,后续新代码会保持该标准。

@shlzxjp shlzxjp left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

第二轮审查(head 8871f97f)的行级意见共 6 条:Blocker 1 / Major 1 / Minor 4。每条都经过独立二次确认,涉及 tgfx 的结论均以 DEPS 锁定的 200dfff 源码核对,死锁和 AppKit 行为已实测。

Comment thread src/platform/ios/PAGView.mm Outdated
(oldBounds.size.width != bounds.size.width || oldBounds.size.height != bounds.size.height)) {
// Hold the same lock as onAnimationFlush so the main-thread write to layer.drawableSize can
// never overlap with the render thread reading it inside flush (tgfx MetalWindow).
std::lock_guard<std::mutex> autoLock(lock);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Blocker] sync 模式下主线程死锁(7e013eb 引入)

这里持有 lock 期间调用了 [animator update](:140)。PAGAnimator::update() 在 _isSync 为 true 时会同线程直接 doUpdate → onFlush → onAnimationFlush:(:268),而 :268 再次 lock_guard 同一把非递归 std::mutex → 同线程重入,主线程永久挂起(已实测)。setFrame:(:151/:155)同理。

触发条件:调用公开 API setSync:YES,且 view 从 0 尺寸布局到非 0(如 CGRectZero 创建后再 layout,属常见路径)。这把锁没有用 TGFX_USE_METAL 隔离,默认 GL 构建同样会中;main 上没有这把锁。

建议:

  1. 把 [animator update] 移到锁的作用域外,锁只包住 updateLayerDrawableSize + [pagSurface updateSize];
  2. 这三处新加的锁(:136/:151/:165)用 #if defined(TGFX_USE_METAL) 包起来,GL 不需要它(GL 下 updateSize 已由 rootLocker 与 flush 串行)。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

两条建议都已采纳(302942382):

  1. [animator update] 已移到锁作用域外(setBounds:/setFrame: 两处均调整)。真正的 flush 发生在 onAnimationFlush: 自己的锁内,layer.drawableSize 与渲染线程的串行化保护不受影响。
  2. 三处新加的锁(setBounds:/setFrame:/setContentScaleFactor:)已用 #if defined(TGFX_USE_METAL) 包起来,GL 路径回到与 main 完全一致。

另外在 iPhone 16 Pro 模拟器上做了运行时对照验证,触发路径与本条意见一致(CGRectZero 创建 → addSubview 挂 window 创建 PAGSurface → setSync:YES → setFrame: 到 300×300),使用 ios/PAG 工程默认配置(PAG_USE_METAL=ON,即锁存在的 Metal 路径):

  • 修复前(8871f97fc):主线程在 resize 处永久挂死,watchdog 线程 15 秒后强制退出进程
  • 修复后(302942382):resize 约 4ms 内正常返回,sync flush 正常执行

两条路径互为对照,也确认了本条 Blocker 的触发条件分析完全准确。

Comment thread src/pagx/utils/RasterUtils.cpp Outdated
// Nested pairs reuse the already-locked context (pag::DeviceLockScope) instead of re-entering
// tgfx's non-recursive device mutex, which deadlocks once devices are deduplicated per native
// GPU object (tgfx #1581).
auto scope = std::make_unique<pag::DeviceLockScope>();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Major] GPUContext 不再缓存 device,GL 下 SVG/PPT 导出每次栅格化都新建并销毁 GL 上下文(b6ec8e89f 引入)

main 和第一轮版本里 GPUContext 都缓存了 _device,同一个导出器只建一次。现在每次 lockContext() 都新起一个最外层 DeviceLockScope → Devices::MakeDefault() → GL 下 GLDevice::MakeWithFallback() → GLDevice::Make() 新建 GL 上下文(GL 不参与 tgfx #1581 去重),unlock() 时随 scope 一起释放。

SVG/PPT 导出路径上没有外层 scope(SVGExporter.cpp:803/3023、PPTExporter.cpp:710-712、PPTStyleEmitter.cpp:284 → 本文件 RenderTiledPattern/RenderMaskedLayer 等),所以每个图案填充/遮罩层都要重建上下文并重编 shader,导出明显变慢。Metal 因去重不受影响;测试因外层 scope 和 DevicePool 常驻设备而掩盖了这个问题。RasterUtils.h:36-37 的类注释 "avoid repeated GL context creation" 现在与实现相反。

建议:恢复 std::shared_ptr<tgfx::Device> _device 的懒创建缓存,在 GPUContext 内自行维护嵌套深度(只有最外层才 _device->lockContext()/unlock());或者给 DeviceLockScope 增加一个对指定 device 加锁的构造函数,供这里复用。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复(302942382),但采用了你给的备选方案(给 DeviceLockScope 增加对指定 device 加锁的构造函数),没有采用主方案(GPUContext 内部维护嵌套深度 + 裸调 _device->lockContext()),原因是主方案有死锁回归风险:

当外层已有 DeviceLockScope 持有默认设备、而 GPUContext 缓存的 device 恰好是同一实例时(Metal 下 MakeDefault() 经 #1581 去重必然返回同一设备),裸调 _device->lockContext() 会同线程重入 tgfx 的非递归互斥锁——这正是 b6ec8e8 修掉的问题。目前持有外层 scope 的 HTML 渲染器恰好不经过 RasterUtils 所以不会触发,但这个组合一旦出现就是死锁,所以没有走这条路。

具体改动:

  • DeviceLockScope 新增 explicit DeviceLockScope(std::shared_ptr<tgfx::Device>),默认构造转发到它;tls 重入语义完全保留(外层 scope 存在时复用其 context、传入的 device 被忽略并已注释说明)
  • GPUContext 恢复 std::shared_ptr<tgfx::Device> _device 懒创建缓存,首次 MakeDefault() 后常驻复用,SVG/PPT 导出全程单一 GL 上下文,不再逐 bake 重建
  • RasterUtils.h:38-41 的注释已改回"缓存复用"并补充了原因

顺带说明:这个新构造函数也被 PAGXRuntimeTest 的清理修复(下面第 5 条)直接复用了。

Comment thread include/pag/gpu.h Outdated
private:
// The initialized state is derived from _backend plus the stored handle instead of a dedicated
// flag, which keeps sizeof(BackendSemaphore) and member offsets identical to earlier releases
// (binary compatibility).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Minor] 「binary compatibility」的承诺不成立:isInitialized() 由 inline 改为 out-of-line 后,旧二进制会读到错误字节

main 上 isInitialized() 是头文件内联的 return _isInitialized;,按旧头文件编译的 App(release 构建会内联)固定读偏移 16 的 bool。新布局里偏移 16 是 _mtlInfo.value,GL 路径的构造函数和 initGL() 都不写它 → 旧 App 链接新库后读到的是未初始化字节(实测返回非法 bool 0x14;initGL 传入有效 sync 后反而返回 false)。sizeof 和 _backend/union 偏移虽然没变,但被内联的旧读取方式已经失效。

说明:第一轮评审我们建议删除 _isInitialized 时只核对了 sizeof 和偏移,遗漏了这一点,这里一并更正。

修改建议:

  • 推荐:如实声明为二进制不兼容变更。 删掉 :362-364 注释里「keeps sizeof and member offsets identical ... (binary compatibility)」的承诺,改为说明初始化状态由 _backend + handle 推导,isInitialized() 改为 out-of-line 是为了今后布局可以演进。在 release notes/CHANGELOG 中标注 BackendSemaphore 与旧版头文件二进制不兼容,使用方需用新头文件重新编译。保留 out-of-line 实现,它本身是对的:以后再改布局,就不会再出现调用方内联读取旧偏移的问题。
  • 不推荐:坚持兼容旧二进制。 这样只能在偏移 16 继续维护一个 bool _isInitialized(initGL/initMetal/拷贝时同步写入),并把 MtlEventInfo::value 移出 union、追加到末尾。代价是 sizeof 回到 32,旧 App 栈上分配 24 字节的对象会被越界写入,兼容性反而更差。在 24 字节内无法同时容纳偏移 16 的 bool 和 Metal 的 event+value,所以完全兼容做不到。

实际受影响的场景较窄:旧头文件编译的 App 不重新编译、直接替换新的动态库。所以定为 Minor,但注释里的承诺应当修正。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

同意,24 字节内确实无法兼容(偏移 16 无法同时容纳旧 bool 和 MtlEventInfo::value,而旧二进制内联读取偏移 16 的行为也无法从新库侧修复)。已按推荐处理(302942382):删除了 :362-364 的兼容性承诺注释,替换为如实声明——布局为二进制不兼容变更、按旧头编译的二进制会读到未初始化字节、集成方必须用新头重新编译。release notes 会同步标注 BackendSemaphore 的这一变更。

Comment thread test/src/PAGBlendTest.cpp Outdated
pagPlayer->setMatrix(Matrix::I());
pagPlayer->setProgress(0.5);
pagPlayer->flush();
EXPECT_TRUE(Baseline::Compare(pagSurface, "PAGBlendMetalTest/CopyDstTexture"));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Minor] 残留的 Metal 截图用例缺基线 key,Metal 构建下必然失败

e1923d1 移除了 per-backend 测试基建,但用例本身没有删:本文件 :250/:283/:322、PAGSurfaceTest.cpp:304/333、PAGXRuntimeTest.cpp:500/565 共 7 处 Baseline::Compare 的 key(PAGBlendMetalTest/*、PAGSurfaceMetalTest/*、PAGXRuntimeTest/Metal*)都不在 test/baseline/version.json 中。按 Baseline.cpp 的逻辑,baselineVersion.empty() 会直接返回 false,所以 PAG_USE_METAL=ON 下 PAGFullTest 这 7 条一定失败。TestUtils_Metal.mm 和 TestUtils.h 里的 Metal 辅助函数现在也只被这些用例使用。

建议与作者回复中「Metal 测试随后续 PR 合入」的计划保持一致:把这 8 个 Metal 用例连同 Metal 测试辅助代码一起移到后续 PR,和基线 key 一起提交。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已按建议全部移出本 PR(302942382):8 个 Metal 用例(PAGBlendMetalTest 4 个、PAGSurfaceMetalTest 2 个、PAGXRuntimeTest 的 Metal 2 个)连同 TestUtils_Metal.mm、TestUtils.h 中的 Metal 辅助声明一起删除,相关注释已注明"后续 PR 与基线 key 一起回来"。用例代码保留在本分支历史(8871f97fc 及之前)中,可直接恢复。

验证:PAG_USE_METAL=ON 构建下受影响套件全绿,不再有缺失 key 的必失败用例。

{
pag::DeviceLockScope lock;
ASSERT_TRUE(static_cast<bool>(lock));
glDeleteTextures(1, &textureInfo.id);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Minor] GL 下清理代码作用在另一个 GL 上下文上,纹理/FBO 实际没有被删除(第二轮引入)

纹理是在用例开头那个 DeviceLockScope 的 device A 上创建的。这里新起了一个最外层 DeviceLockScope,GL 下 MakeDefault() → GLDevice::Make() 会新建一个不共享的上下文 B(GL 不参与去重)。glDeleteTextures 在 B 上执行,名字在 B 中不存在,相当于 no-op,A 上的纹理泄漏。:429-430 的 glDeleteFramebuffers/glDeleteTextures 同理。main 和第一轮版本都是在同一个 fixture device 上清理的。

建议:清理时锁定创建纹理的同一个 device,例如在用例开头持有该 device 的 shared_ptr,创建和清理两个阶段都对它 lockContext()/unlock(),而不是再新起一个 DeviceLockScope。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复(302942382)。两个用例统一改为:开头 pag::Devices::MakeDefault() 取一次 device shared_ptr,创建与清理两个阶段都用 DeviceLockScope(device)(即上面第 2 条意见中新增的构造函数)锁定同一设备,glDelete* 落在创建纹理/FBO 的上下文上。

}
}

- (void)setFrameSize:(NSSize)newSize {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Minor] setFrame: 内部会调用 setFrameSize:,一次 resize 会把尺寸更新处理执行两次(73be597)

AppKit 中 -[NSView setFrame:] 内部会调用 -setFrameSize:(已用最小 AppKit 程序实测:直接 setFrame:、autoresizing、Auto Layout 三条路径都会走到 setFrameSize:),所以 :146-147 注释里「does not pass through setFrame:」的前提不成立。结果是一次 resize 中 updateLayerDrawableSize + [pagSurface updateSize](会 PurgeCaches + releaseAll + freeSurface)执行两次;从 0 尺寸变为非 0 时还会连续做两次同步整帧渲染,中间的缓存被清空。GL 构建同样受影响。

建议:尺寸变化的处理只保留在 setFrameSize: 中(它覆盖了所有路径),去掉 setFrame:(:128-141)中重复的处理逻辑。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复(302942382):删除了 mac 的 setFrame: override,尺寸处理只保留在 setFrameSize:(setFrame:、autoresizing、Auto Layout 三条路径都汇聚到这里),并修正了原来方向写反的注释("does not pass through setFrame:" → "setFrame: calls it internally")。

顺带说明:iOS 侧 setFrame: 内部调 setBounds: 导致的双重执行是 main 已有的历史行为(非本 PR 引入),未在本 PR 处理,后续单独跟进。

@leiyue123

Copy link
Copy Markdown
Collaborator Author

感谢第二轮评审!6 条意见全部成立,已在 3029423 中修复并推送(1 Blocker / 1 Major / 4 Minor),逐条回复见行级评论。要点:

  • Blocker(iOS sync 死锁):两条建议全采纳([animator update] 移出锁外 + 三处锁用 TGFX_USE_METAL 隔离)。附模拟器运行时对照验证:修复前主线程在 resize 处永久挂死,修复后约 4ms 正常返回,验证走的是 PAG_USE_METAL=ON(锁存在)的构建。
  • Major(GL 导出每次新建上下文):恢复 GPUContext 设备缓存,但通过 DeviceLockScope 新增指定 device 的构造函数实现,而非裸调 _device->lockContext()——后者在外层 scope 持有同一(去重后的)设备时会重入 tgfx 非递归互斥锁,是 b6ec8e8 修掉的问题的回归路径,详见行级回复。
  • Minor×4:gpu.h 如实声明二进制不兼容(release notes 将标注 BackendSemaphore 需重编译);8 个 Metal 用例连同 TestUtils_Metal 辅助代码移出本 PR,后续与基线 key 一起回来;PAGXRuntimeTest 两处 GL 清理改为锁定创建资源的同一设备;mac 删除 setFrame: 重复逻辑、尺寸处理收敛到 setFrameSize: 单一入口。

验证情况:GL 与 Metal(PAG_USE_METAL=ON)两个配置 PAGFullTest 编译全过;受影响测试套件(PAGXRuntimeTest / PAGBlendTest / PAGSurfaceTest / PAGXSVGTest / PAGXPPTTest 共 400+ 例)全绿。唯一失败的 PAGSurfaceTest.Mask 经修复前的旧二进制复跑确认为本机基线环境的既有问题,与本 PR 无关。

另有一条已知但未在本 PR 处理的事项:iOS 侧 setFrame: 内部调 setBounds: 的双重执行为 main 已有历史行为,后续单独跟进。

@shlzxjp

shlzxjp commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator

合入前提:Metal 后端需要补上自动化验证

302942382 已修复第二轮的 6 条行级意见,已逐条对照代码核实;当前 9 个平台的 CI 全部通过。不过这些 CI 都不构建 Metal,本 PR 新增的 Metal 后端目前完全没有自动化验证,只有本机人工对比和模拟器验证。建议在合入前补上。

为什么不能延后

  1. 环境已经具备。 tgfx 的 autotest 里已经有 mac (metal) job,用的是 GitHub 托管的 macos-14 runner,执行 ./autotest.sh USE_METAL。main 最新一次运行 619 个用例全部通过。libpag 的 autotest 用的也是 macos-14,可以直接照着写。
  2. 延后的理由只覆盖截图用例。 Metal 截图基线需要切到 main 上生成,而 main 还没有 Metal 代码,这一点成立。但编译检查和不依赖截图基线的行为测试没有这个限制。
  3. 有一类问题只有 Metal CI 能发现。 tgfx org.libpag.PAGPlayer.finalize() timed out after 10 seconds #1581 的设备去重只在 Metal 下生效:MakeDefault() 永远返回同一个 Device。GL 下每次都会新建上下文,不会合并。所以「同线程重入非递归的设备锁导致死锁」这类问题,GL CI 永远测不出来。第二轮的 sync 模式死锁、b6ec8e89f 修掉的那个死锁,都属于这一类。

本 PR 合入前需要补齐

1. Metal 编译 job

  • macOS:-DPAG_USE_METAL=ON -DPAG_BUILD_TESTS=ON,能编出 PAGFullTest
  • iOS:增加一个 Metal 构建

2. Metal 行为测试 job(macos-14,用 gtest filter 只跑 Metal 专项用例)

用读回像素直接断言颜色值的方式,代替 md5 截图比对,这样完全不依赖基线。至少覆盖两轮修复的关键点:

  • 跨 MTLDevice 的外部纹理会被拒绝绘制(Devices::CanSampleFrom)
  • MakeFrom(BackendTexture, forAsyncThread=true) 选用的是纹理所属的 device,而不是系统默认 device
  • 用 BGRA8Unorm 外部纹理作为渲染目标时,读回的像素红蓝不互换
  • BackendSemaphore 的 Metal event 往返:initMetal → getMtlEventInfo → flush 后 signal 输出
  • pagx::MakeFrom(device, surface) 能在 Metal 下正常渲染
  • 嵌套的 DeviceLockScope 不死锁;iOS PAGView 在 setSync:YES 下从 0 尺寸 resize 不死锁

合入后紧接着的 PR

  • 提交 Metal 截图基线,并参照 tgfx 的 mac-metal job 增加完整的 PAGFullTest Metal autotest job。那时 main 上已经有 Metal 代码,基线可以正常生成。
  • 建议在本 PR 里约定一个明确的时间点或关联 issue,不要只写「后续」。

另外请注意,过渡期不要用「cache 缺失就跳过比对」的方式让 CI 变绿。第一轮评审中,截图门禁正是这样失效的。

上面第 1、2 项补齐、CI 通过之后,就具备合入条件了。

Comment thread src/platform/ios/private/GPUDrawable.h Outdated

#pragma once

#if defined(TGFX_USE_OPENGL)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个是不是直接拆到对应后端文件夹里,参考下tgfx

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已按 tgfx 的目录方式拆分(3d4133f17、3fa7d4024):

  • iOS GL 实现移到 ios/private/opengl/EAGLDrawable,mac GL 实现移到 mac/private/opengl/CGLDrawable
  • Metal 实现移到 cocoa/private/metal/MetalLayerDrawable(原 MetalGPUDrawable)和 cocoa/private/metal/MetalLayerHelper(原 PAGMetalLayerHelper)
  • CMake 只编译当前后端的目录,做法和 tgfx 只在对应 TGFX_USE_* 下 GLOB src/gpu/opengl 或 src/gpu/metal 一致;后端目录里的文件因此不再用 #if TGFX_USE_* 整体包裹

已确认 GL 构建里没有任何 private/metal 的编译单元和 include 路径,Metal 构建反之。

+ (PAGSurfaceImpl*)FromLayer:(CAEAGLLayer*)layer;

+ (PAGSurfaceImpl*)FromCVPixelBuffer:(CVPixelBufferRef)pixelBuffer;
+ (PAGSurfaceImpl*)FromMetalLayer:(CAMetalLayer*)layer;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个也是,是不是应该单独拆出来,写到后端的头文件里。对外提供也应该是两个文件。毕竟metal和opengl不共存

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已拆(3d4133f17):PAGSurfaceImpl.h 只保留后端无关的部分(MakeOffscreen、读回、单参 FromCVPixelBuffer: 的分发等),GL / Metal 的工厂分别放到 private/opengl/PAGSurfaceImpl+OpenGL.h 和 private/metal/PAGSurfaceImpl+Metal.h 两个分类里。对外也是两个文件:PAGSurface+OpenGL.h 和 PAGSurface+Metal.h。mac 同样处理。

Comment thread src/platform/ios/PAGSurface.h Outdated
* never actually renders. When the size changes, update layer.drawableSize and call updateSize
* on the PAGSurface.
*/
+ (PAGSurface*)FromMetalLayer:(CAMetalLayer*)metalLayer;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里不用叫MetalLayer,我建议是写到两个分类里,都是FromLayer

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已改(3d4133f17):删掉了 FromMetalLayer:,两个分类都叫 FromLayer:——PAGSurface (OpenGL) 接收 CAEAGLLayer*,PAGSurface (Metal) 接收 CAMetalLayer*。PAGView 内部也改成调用对应分类的 FromLayer:,两个 PAGView.h 注释里残留的旧名一并更新了。

Comment thread src/platform/mac/PAGSurface.h Outdated
* never actually renders. When the size changes, update layer.drawableSize and call updateSize
* on the PAGSurface.
*/
+ (PAGSurface*)FromMetalLayer:(CAMetalLayer*)metalLayer;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mac 这里也和 iOS 一起拆吧,后端相关的工厂放到各自分类里,不要都塞在 PAGSurface 主接口里,再靠运行时返回 nil 区分支持。iOS 的 FromCVPixelBuffer:context: 和 FromCVPixelBuffer:mtlDevice: 也一起归到对应分类。

另外拆文件时,CMake 的公开头列表和 umbrella header 也要一起按后端选择,不能继续把两套分类无差别打包。两边都叫 FromLayer: 时,参数类型不影响 ObjC selector,必须保证只编译、导出对应那一套。公开头不要用 TGFX_USE_* 判断,framework 使用方没有这些构建宏。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已按建议处理(3d4133f17、3fa7d4024):

  • mac 也拆成 PAGSurface (OpenGL)(FromView:)和 PAGSurface (Metal)(FromLayer:)。iOS 的 FromCVPixelBuffer:context: 归到 OpenGL 分类,FromCVPixelBuffer:mtlDevice: 归到 Metal 分类。主接口只留后端无关的入口,不再靠运行时返回 nil 区分。
  • CMake 按 PAG_USE_METAL 选一套分类,另一套的源文件和公开头都不进 target,只编译、只导出对应那一套。
  • umbrella header 改成 libpag.h.in 模板,配置时只 import 当前后端的分类头;生成后的头作为 target 源文件拷进 framework。
  • 公开头里没有任何 TGFX_USE_*。

验证:

  • 对 mac 的 GL / Metal framework 各写了一个 @import libpag 的使用方小程序:调用本后端的工厂能编译通过,调用另一个后端的工厂报 class method not found。
  • iOS 模拟器的 Metal framework 只导出 PAGSurface+Metal.h,二进制里也只有 (Metal) 分类的符号。
  • PAGViewer demo 用到的 import 对 iOS GL / Metal 两个 framework 都能编译。

}

#else
+ (PAGSurfaceImpl*)FromCVPixelBuffer:(CVPixelBufferRef)pixelBuffer mtlDevice:(id<MTLDevice>)device {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里把 GL 的模拟器限制套到 Metal 上了。tgfx 的 EAGLGPU 在模拟器上关闭硬件缓冲,但 MetalGPU 的 HardwareBufferAvailable() 返回 true,走的是 CVMetalTextureCache,不受这个限制。

现在 arm64 模拟器的 Metal 构建里,即使传入可用的 IOSurface-backed BGRA CVPixelBuffer,这个新增入口也直接返回 nil,单参 FromCVPixelBuffer: 同样被前面的分支挡住了。应该只在 GL 分支保留模拟器短路,Metal 走正常包装路径。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修(3d4133f17):TARGET_IPHONE_SIMULATOR 的短路现在只留在 OpenGL 分类的 FromCVPixelBuffer:context: 里。Metal 分类的 FromCVPixelBuffer:mtlDevice: 不再受这个限制,走正常的包装路径。单参 FromCVPixelBuffer: 按构建后端分发到对应实现,所以 arm64 模拟器的 Metal 构建也不会再被前面的分支挡住。

// or GL before any context is current), the capture returns nullptr and the verification is
// bypassed by Devices::CanSampleFrom() (which trusts a null ref). Only report the missing
// capture as an error on backends where a captured identity is genuinely required (GL).
auto deviceRef = Devices::CaptureFromTexture(texture);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里需要区分“没有捕获设备身份”和“纹理后端不匹配”。Metal 构建传入 GL BackendTexture 时,ToTGFX 保留 GL 标签,CaptureFromTexture 返回空,但 RequiresCapturedIdentity() 是 false,最后 PAGImage::FromTexture 仍然返回非空图片。真正绘制时 MetalGPU 拒绝 GL 纹理,图片就一直是空白,C 的 pag_image_from_backend_texture 也一样。

部分 Surface 工厂也只检查纹理描述有效,等到绘制才拒绝不匹配的后端。建议在资源工厂入口统一校验当前后端,不支持就返回空,不要返回一个永远画不出来的对象。通用 BackendTexture 描述本身可以保留多后端标签。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修(1ddf02fb0、3fa7d4024):

  • 新增 Devices::SupportsBackend(),在资源工厂入口统一校验纹理的后端标签,不匹配直接返回空并打日志,不再返回一个永远画不出来的对象。BackendTexture 本身仍保留多后端标签。
  • 覆盖的入口:PAGImage::FromTexture、PAGSurface::MakeFrom(BackendTexture) / MakeFrom(BackendRenderTarget)、pagx::PAGSurface::MakeFrom 的两个重载、pagx::PAGImage::MakeFromTexture。C 接口 pag_image_from_backend_texture 和 pag_surface_make_from_texture 走的就是这些入口。
  • PAGImage::FromTexture 里把两种失败分开了:后端不匹配报 "different GPU backend";后端匹配但 GL 下没有当前上下文、拿不到设备身份时,才报 "no current GPU context"。

验证:Metal 构建传入 GL 纹理、GL 构建传入 Metal 纹理,入口都返回空,每种情况只有一条对应的日志。

Comment thread src/platform/ios/PAGSurface.h Outdated
* never actually renders. When the size changes, update layer.drawableSize and call updateSize
* on the PAGSurface.
*/
+ (PAGSurface*)FromMetalLayer:(CAMetalLayer*)metalLayer;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

之前 Metal 截图/回读的限制只补到了 PAGView.h,PAGSurface 这里也要同步。直接用 FromMetalLayer: 创建的 surface 走的是同一套 MetalWindow,present 后释放 drawable,后续 makeSnapshot 和 copyPixelsTo: 也可能拿到空白或旧帧,并不是只有 PAGView 会受影响。mac 的 PAGSurface.makeSnapshot 也一样。

请把这个限制写到对应的 Surface 读回接口上,并明确 getCVPixelBuffer 对 Metal layer 路径返回 nil;现在它的说明只提了 GL 的 FromLayer:/FromView:。这是同一问题的公开接口说明漏了一半,不是新的截图根因。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已同步(3d4133f17、3fa7d4024):

  • iOS 和 mac 的 PAGSurface.h 中,makeSnapshot(iOS 还有 copyPixelsTo:rowBytes:)都补上了和 PAGView.h 一致的限制说明:用 CAMetalLayer 创建的 surface,每次 flush 都会 present 并释放 drawable,之后读回可能拿到空白或旧帧;需要可靠读回请用离屏 PAGSurface(MakeOffscreen:)。
  • getCVPixelBuffer 的说明补上了 Metal:从 layer / view 创建的 surface 都返回 nil,包括 GL 的 FromLayer: / FromView: 和 Metal 的 FromLayer:。

@leiyue123

Copy link
Copy Markdown
Collaborator Author

@Hparty 7 条意见已全部处理,提交为 3d4133f、1ddf02fb0、3fa7d4024,逐条说明见行级回复。要点:

  • 后端实现按 tgfx 的方式拆到 private/opengl/、private/metal/ 目录,CMake 只编译当前后端的目录。
  • 公开接口拆成 PAGSurface (OpenGL) / PAGSurface (Metal) 两个分类,都叫 FromLayer:。CMake 和生成的 umbrella header 只编译、只导出当前后端那一套,公开头里没有 TGFX_USE_*。
  • 模拟器限制只保留在 GL 分支;资源工厂入口统一校验后端标签,并区分"后端不匹配"和"没有当前 GPU 上下文"两种失败;PAGSurface 读回接口补齐了 Metal 的限制说明。

验证:mac GL / Metal 和 iOS 模拟器 GL / Metal 四种配置均编译通过;用 @import libpag 的使用方程序确认每个 framework 只暴露对应后端的工厂;iOS Metal 构建上重跑了 sync 模式 resize 的死锁复现,结果正常。

@Hparty Hparty left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

本轮复审确认的新问题见下方行内评论。另有几处无法挂在 diff 行上的问题一并说明:

  1. [A2] src/c/ext/pag_surface_ext.cpp:23-27(不在本 diff 内):仍直接 #include "tgfx/gpu/opengl/GLDevice.h" 并调用 tgfx::GLDevice::Make(sharedContext)。PAG_USE_C=ON + Metal 构建下该文件会引用 OpenGL 专用符号导致编译/链接失败,建议改走 pag::Devices 收口。
  2. [A2] pagx/wechat/src/PAGXView.cpp:171(该行不在 diff 内):仍调用 tgfx::GLDevice::Current(),与 Devices.h 声明的「后端调用统一收口」原则不一致,建议改为 pag::Devices::AdoptCurrent().device。
  3. [A2] test/src/PAGXTest.cpp:97-104(该行不在 diff 内):GL 头文件未加 TGFX_USE_OPENGL 守卫,同 PR 已在 PAGXRuntimeTest.cpp / PAGBlendTest.cpp / TestUtils.cpp 统一加了守卫,唯独本文件遗漏,非 Apple 的非 GL 后端(Linux/Vulkan 等)会因找不到 <OpenGL/gl3.h> 直接编译失败。
  4. [A9] src/pagx/runtime/OffscreenDrawable.h:26:注释仍写 "backs PAGSurface::MakeOffscreen with a fresh OpenGL device",实现已改为后端无关的 Devices::MakeDefault()。

旧问题复核:此前评论的公开头宏包裹、CanSampleFrom、MetalExternalDeviceRef 注释、设备去重、CMake 后端互斥、EXCLUDED_ARCHS 等问题均已修复。仍待确认:① MetalLayerHelper.h 的 framebufferOnly = YES 仍在,Metal 下 snapshot/readback 可靠性建议实测确认;② Metal 工厂失败时返回 nil 但无 LOGE(与之前回复的「nil+LOGE」不符);③ PAGView 未显式设置 CAMetalLayer.device,MetalWindow::MakeFrom 对 nil device 的行为依赖 tgfx 侧,建议确认;④ include/pag/gpu.h 已在注释中声明 BackendSemaphore 布局二进制不兼容,破坏本身仍在,合入版本节奏请注意。

}

DeviceLockScope::DeviceLockScope(std::shared_ptr<tgfx::Device> device) {
if (tlsLockDepth > 0) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[A6] 嵌套 scope(tlsLockDepth > 0)时完全忽略入参 device,直接复用外层 context。注释称「MakeDefault 返回同一 dedup 实例所以忽略是安全的」,但 RasterUtils.cpp:216-218 的注释又明确说 GL 下每次 MakeDefault 都新建 GL context,两处注释自相矛盾,该前提未经验证。若外层与内层不是同一 device,内层会拿到错误的 GL context 且入参 device 从未被 lock。建议 TLS 同时记录外层锁定的 device,入参与之相同(或为空)才复用,否则走正常 lockContext();并统一两处注释。

case tgfx::Backend::Vulkan:
case tgfx::Backend::WebGPU:
case tgfx::Backend::D3D12:
LOGE("PAGSurface::flushInternal() cannot forward a semaphore of this backend yet.");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[A9] 日志文案引用了已不存在的方法名 PAGSurface::flushInternal()(现为 draw)。另外 Backend::Unknown 与未实现的 Vulkan/WebGPU/D3D12 共用同一分支:Metal 下若 flush() 未产出 semaphore,调用方传了 signalSemaphore 时每一帧都会打这条 ERROR。建议 Unknown 单独静默或降级为 LOGW,并修正函数名。

* libpag builds without ARC, so ownership is managed manually: retain in the constructor and
* release in the destructor, which keeps the object alive for the lifetime of the ref.
*/
class MetalExternalDeviceRef : public ExternalDeviceRef {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[A12] MetalExternalDeviceRef 在 MRC 下手动 retain/release,但未删除拷贝构造与拷贝赋值,一旦被复制即 double release(GLExternalDeviceRef 是 POD 无此风险,属 GL/Metal 不对称)。建议 = delete 拷贝构造与赋值。


BackendSemaphore::BackendSemaphore()
: _backend(Backend::MOCK), _glSync(nullptr), _isInitialized(false) {
BackendSemaphore::BackendSemaphore() : _backend(Backend::MOCK), _glSync(nullptr) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[A7] BackendSemaphore() : _backend(MOCK), _glSync(nullptr) 只初始化 union 前 8 字节,_mtlInfo.value(偏移 8-15)为不确定值。当前 isInitialized() 对 MOCK 返回 false 使其不可达,但建议直接 _mtlInfo({}) 整体置零,避免后续改动踩到未初始化读取。

// a no-op at the tgfx layer. Vulkan / D3D12 / WebGPU sync ABI extensions are deferred to
// the specific backend PRs that introduce user-facing sync APIs.
if (!semaphore.isInitialized()) {
return {};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[A4] ToTGFX(const BackendSemaphore&) 仍是 glSync() → getMtlEventInfo() 逐后端试解包,而本 PR 已给 pag::BackendSemaphore 加了 backend() 访问器、PAGSurface.cpp 的转发也已改为 switch 分派。建议这里同样按 backend() 分派,与同文件 ToTGFX(BackendTexture) 的 switch 风格一致,避免后续新后端漏改时静默丢失同步信息。

@@ -118,6 +178,11 @@ - (void)setContentScaleFactor:(CGFloat)scaleFactor {
CGFloat oldScaleFactor = self.contentScaleFactor;
[super setContentScaleFactor:scaleFactor];
if (pagSurface != nil && oldScaleFactor != scaleFactor) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[A4] mac 侧新增了 viewDidChangeBackingProperties(mac/PAGView.mm:153)处理 backingScale 变化,iOS 没有对应路径:view 移到外接屏或屏幕缩放变化时 contentScaleFactor 未必触发 setContentScaleFactor:,layer.drawableSize 会停留在旧值(模糊/裁切)。建议在 didMoveToWindow 或 traitCollectionDidChange: 中同样调用 updateLayerDrawableSize + [pagSurface updateSize](需持锁)。

- (void)setFrameSize:(NSSize)newSize {
NSSize oldSize = self.bounds.size;
[super setFrameSize:newSize];
// Every size change funnels through setFrameSize:: setFrame: calls it internally, and

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[A9] 注释称「所有尺寸变化都汇聚到 setFrameSize:」,但 :115 仍保留 setBounds: 重写,逻辑重复且与注释自相矛盾。建议只保留 setFrameSize: 一个入口(与 iOS 侧应做的简化对称)。

BOOL status = _pagSurface->readPixels(pag::ColorType::BGRA_8888, pag::AlphaType::Premultiplied,
pixelBufferData, bytesPerRow);
CVPixelBufferUnlockBaseAddress(hardwareBuffer, 0);
if (!status) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[A6] readPixels 失败时只 LOGE 仍返回 hardwareBuffer,调用方拿到未写入的空白 buffer 且无法区分失败(iOS 侧失败返回 nil)。建议失败时释放并返回 nil,与 iOS 对齐。

Comment thread CMakeLists.txt
set(PAG_USE_ANGLE OFF)
endif ()

# The Metal backend requires iOS 13+ (see the tgfx deployment-target check) and supports the iOS

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[A9] 这段注释描述的是「Metal 选中时自动提升部署目标」,但下方 if 块只有 NOT APPLE 的 FATAL_ERROR 和 EXCLUDED_ARCHS,真正的部署目标设置在文件顶部 9-11 行;且注释写 iOS 13+,代码与 README 实际是 15.0。建议删除或合并到 9-11 行处并修正版本号。

Comment thread ios/README.zh_CN.md
```
./gen_ios -DPAG_USE_METAL=ON
./gen_simulator -DPAG_USE_METAL=ON
```

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[C5] 英文版 ios/README.md 同段落多出一句中文版没有的提示(PAGViewer demo target 保持默认部署目标,需要在 Xcode 里手动升到 15.0),中英 README 不对齐,建议补上同义句。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants