Skip to content
Open
Show file tree
Hide file tree
Changes from 73 commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
4b43e77
Funnel tgfx GPU backend calls through pag::Devices to decouple libpag…
leiyue123 Aug 25, 2026
511719d
Add Metal backend implementation and per-backend baseline isolation a…
leiyue123 Aug 25, 2026
ce62cb1
Extend pag::MtlTextureInfo with pixel format and add Metal id<MTLEven…
leiyue123 Aug 25, 2026
80a39a8
Add Metal platform surface API for iOS and macOS via MetalGPUDrawable…
leiyue123 Aug 25, 2026
59c82ec
Add Metal-specific PAGSurface / pagx::PAGSurface test coverage and fi…
leiyue123 Aug 25, 2026
beb3562
Un-gate PAGBlendTest.Blend from OpenGL and add three PAGBlendMetalTes…
leiyue123 Aug 25, 2026
ef63903
Support Metal backend in PAGView on iOS and macOS.
leiyue123 Sep 1, 2026
e61559c
Automatically raise iOS deployment target to 13.0 for Metal builds.
leiyue123 Sep 1, 2026
a5d166d
Fix PAGView Metal backend: drawableSize, device retry, and public cla…
leiyue123 Sep 1, 2026
8eec508
Fix macOS PAGView Metal rendering by enabling layer backing.
leiyue123 Sep 1, 2026
b088a47
Document Metal backend generation in the iOS README.
leiyue123 Sep 1, 2026
e88c5e2
Update the iOS PAGViewer demo to build with the Metal backend.
leiyue123 Sep 1, 2026
deaf39b
Document Metal backend generation in the macOS README.
leiyue123 Sep 1, 2026
924467d
Fix autotest backend naming and align argument handling with tgfx.
leiyue123 Sep 1, 2026
4cffcae
Skip baseline image generation in the autotest CI pipeline.
leiyue123 Sep 1, 2026
885092e
Mark PAGView Metal support as complete in the backend decoupling doc.
leiyue123 Sep 1, 2026
5b193c4
Upgrade tgfx and adapt filter uniform block bindings to shader-stage …
leiyue123 Sep 3, 2026
c777d2d
Merge remote-tracking branch 'origin/main' into feature/thunderllei_g…
leiyue123 Sep 3, 2026
00880fd
Fix code formatting in PAGView Metal implementation.
leiyue123 Sep 3, 2026
2239e2c
Fall back gracefully when main branch lacks per-backend UpdateBaselin…
leiyue123 Sep 3, 2026
d23cc88
Exclude Devices_Metal.mm from non-Apple builds to avoid missing cc1ob…
leiyue123 Sep 3, 2026
cb4749d
Declare tgfx task-threading APIs on the PAG module type for TGFXBind.
leiyue123 Sep 3, 2026
fed0c27
Declare all PAGSurface factory methods unconditionally in public head…
leiyue123 Sep 9, 2026
0f0abcc
Derive BackendSemaphore initialized state from backend and handle so …
leiyue123 Sep 9, 2026
3397269
Remove MTLCreateSystemDefaultDevice from PAGView so MetalWindow lazil…
leiyue123 Sep 9, 2026
3dd83c1
Capture the owning device from external textures so Metal rejects dra…
leiyue123 Sep 9, 2026
4473734
Retain the MTLDevice in MetalExternalDeviceRef manually since libpag …
leiyue123 Sep 9, 2026
f6a1a9c
Derive the async worker device from the external texture on Metal so …
leiyue123 Sep 17, 2026
891acfb
Walk back to the owning device from the wrapped surface so pagx rende…
leiyue123 Sep 17, 2026
1db81ac
Require the owning device when wrapping a tgfx Surface so pagx render…
leiyue123 Sep 17, 2026
fd36e5c
Read the Metal pixel format from the real MTLTexture so readback and …
leiyue123 Sep 17, 2026
45a1a61
Dispatch semaphore forwarding by backend with explicit switch so unsu…
leiyue123 Sep 22, 2026
56be752
Add pure logic tests for BackendSemaphore cross backend guards and To…
leiyue123 Sep 22, 2026
54d2f0d
Reject combining non OpenGL backends at configure time and count back…
leiyue123 Sep 22, 2026
73be597
Handle setFrameSize and backing property changes on macOS so the draw…
leiyue123 Sep 22, 2026
64f470a
Document that makeSnapshot is unreliable on the Metal backend because…
leiyue123 Sep 22, 2026
7e013eb
Serialize drawable size updates with the render flush on iOS and docu…
leiyue123 Sep 22, 2026
653e2cb
Restore the iOS demo to the OpenGL defaults and raise the Metal deplo…
leiyue123 Sep 22, 2026
1e65b15
Add a backend accessor to BackendSemaphore to match BackendTexture an…
leiyue123 Sep 22, 2026
0f94f5a
Replace the flat Metal semaphore accessors with a public MtlEventInfo…
leiyue123 Sep 22, 2026
d9a9133
Rename the C semaphore initializer to init_metal to match initMetal a…
leiyue123 Sep 22, 2026
bc1add3
Drop the redundant CAMetalLayer forward declarations already provided…
leiyue123 Sep 22, 2026
fdf3e1f
Document the main thread and resize requirements of FromMTKView.
leiyue123 Sep 22, 2026
8e36ac3
Document that FromMetalLayer and FromMTKView require the caller to ke…
leiyue123 Sep 22, 2026
4c5d5ba
Fix the fabricated ownership claim in the MetalGPUDrawable comment an…
leiyue123 Sep 22, 2026
42d7390
Remove FromMTKView whose thread and device injection constraints outw…
leiyue123 Sep 22, 2026
45bf4a5
Add the missing MTLDevice adoption note to the macOS FromMetalLayer d…
leiyue123 Sep 22, 2026
f769936
Document that FromMetalLayer callers must maintain the layer drawable…
leiyue123 Sep 22, 2026
d3104fe
Create the Metal window eagerly in FromLayer so FromMetalLayer fails …
leiyue123 Sep 22, 2026
37c3fcf
Keep the TGFX task thread methods out of the public PAG interface by …
leiyue123 Sep 28, 2026
815ef70
Fix stale symbol references and wrong API basis in comments and align…
leiyue123 Sep 28, 2026
9576dd0
Share the Metal layer setup between the platform PAGViews rename the …
leiyue123 Sep 28, 2026
ac97041
Deduplicate the decoder compatibility flag move test includes to the …
leiyue123 Sep 28, 2026
9ebdbea
Raise the default macOS deployment target to 11.0 for the Xcode 27 li…
leiyue123 Sep 28, 2026
9dcc3be
Merge branch 'main' into feature/thunderllei_gpu_backend
leiyue123 Sep 28, 2026
e1923d1
Move the per backend test infrastructure out of this PR so CI keeps u…
leiyue123 Sep 28, 2026
48ffdcc
Drop the internal investigation and design docs from the PR.
leiyue123 Sep 28, 2026
0f40e78
Remove references to the local backend decoupling doc from source com…
leiyue123 Sep 28, 2026
f02c5dc
Resolve the tgfx dependency conflict in favor of the main branch revi…
leiyue123 Sep 28, 2026
7ddff1a
Adopt the tgfx device deduplication so Metal surfaces on the same MTL…
leiyue123 Sep 28, 2026
abb1d39
Accept the baseline changes caused by the tgfx background blur fix.
leiyue123 Sep 28, 2026
b6ec8e8
Make the default device lock reentrant so code that fetches the defau…
leiyue123 Sep 29, 2026
eaddca9
Raise the Metal iOS deployment target to 15.0 to match the Xcode 27 S…
leiyue123 Sep 29, 2026
f80d2c0
Update the Chinese iOS readme to the 15.0 Metal deployment target.
leiyue123 Sep 29, 2026
52a97fa
Update the mac readmes to the 12.0 deployment floor.
leiyue123 Sep 29, 2026
e6ef33f
Silence the CAEAGLLayer deprecation warning inside the public iOS hea…
leiyue123 Sep 29, 2026
44eec4a
Define GLES_SILENCE_DEPRECATION locally in the iOS GL touching files …
leiyue123 Sep 29, 2026
7712322
Silence the CAEAGLLayer deprecation warnings in the iOS private heade…
leiyue123 Sep 29, 2026
04de461
Declare a single scene manifest in the iOS demo so the Xcode 27 runti…
leiyue123 Sep 29, 2026
47b1739
Adopt the UIScene configuration callbacks in the iOS demo AppDelegate…
leiyue123 Sep 29, 2026
1140b42
Complete the scene lifecycle migration by declaring the AppDelegate a…
leiyue123 Sep 29, 2026
d079847
Split the iOS demo scene lifecycle into a dedicated SceneDelegate to …
leiyue123 Sep 29, 2026
8871f97
Remove the unused tlsLockedDevice record that failed the web build wi…
leiyue123 Sep 30, 2026
3029423
Fix the sync mode deadlock and duplicate resizes and restore the cach…
leiyue123 Oct 8, 2026
71c176a
Merge branch 'refs/heads/main' into feature/thunderllei_gpu_backend
leiyue123 Oct 9, 2026
0cc9eeb
Add macOS and iOS Metal compile jobs to the build workflow
leiyue123 Oct 9, 2026
3d4133f
Split surface APIs into per backend categories fix Metal pixel buffer…
leiyue123 Oct 13, 2026
1ddf02f
Reject backend mismatched texture descriptions at resource factory en…
leiyue123 Oct 13, 2026
3fa7d40
Export the generated umbrella header and compile only the selected ba…
leiyue123 Oct 13, 2026
73ef4e6
Merge branch 'main' into feature/thunderllei_gpu_backend
leiyue123 Oct 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 112 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
cmake_minimum_required(VERSION 3.13)

# The iOS toolchain (third_party/vendor_tools/ios.toolchain.cmake) reads DEPLOYMENT_TARGET while
# configuring the project and derives both the deployment target and the compiler target triple
# from it. Apple toolchains reject iOS deployment targets below 15.0 (Xcode 26/27 SDKs), and
# the tgfx Metal backend has its own >= 13.0 floor that 15.0 satisfies, so Metal builds must
# pre-set the minimum before configuration. This only applies to iOS builds; macOS keeps its own
# default. Callers can override with -DDEPLOYMENT_TARGET=... explicitly.
if (PAG_USE_METAL AND NOT DEFINED DEPLOYMENT_TARGET AND PLATFORM MATCHES "^(OS|SIMULATOR)")
set(DEPLOYMENT_TARGET "15.0" CACHE STRING "Minimum iOS deployment target" FORCE)
endif ()

project(PAG)

if (APPLE)
Expand All @@ -25,6 +36,10 @@ set(CMAKE_CXX_VISIBILITY_PRESET hidden)

# Options for building libpag
option(PAG_USE_OPENGL "Allow use of OpenGL as GPU backend" ON)
option(PAG_USE_METAL "Use Metal as the GPU backend on Apple platforms" OFF)
option(PAG_USE_VULKAN "Use Vulkan as the GPU backend" OFF)
option(PAG_USE_D3D12 "Use D3D12 as the GPU backend on Windows" OFF)
option(PAG_USE_WEBGPU "Use WebGPU as the GPU backend on Web" OFF)
option(PAG_USE_SWIFTSHADER "Allow build with the SwiftShader library" OFF)
option(PAG_USE_ANGLE "Allow build with the ANGLE library" OFF)
option(PAG_USE_QT "Allow build with QT frameworks" OFF)
Expand Down Expand Up @@ -87,6 +102,62 @@ if (MACOS AND CLION_IDE AND NOT HasParent)
option(PAG_BUILD_TESTS "Build libpag tests" ON)
endif ()

# Non-GL backends (Metal / Vulkan / D3D12 / WebGPU) are mutually exclusive with each other and
# with OpenGL. If any non-GL backend is enabled, disable OpenGL and all GL-only variants
# (QT / SwiftShader / ANGLE). This mirrors the mutual-exclusion contract enforced in
# third_party/tgfx/CMakeLists.txt, and must run BEFORE the existing QT / SwiftShader mutual-
# exclusion block so the "if (PAG_USE_QT OR PAG_USE_SWIFTSHADER OR PAG_USE_ANGLE) set(PAG_USE_OPENGL ON)"
# fixup further down does not accidentally re-enable GL.
#
# The non-GL backends must not be combined: libpag passes each PAG_USE_* to tgfx unchanged, but
# tgfx resolves them with its own priority chain (Vulkan > D3D12 > Metal > WebGPU) and silently
# disables the losers. A combination would leave libpag compiling backend glue (Devices_*.cpp)
# for a backend tgfx never built, failing at link time with undefined symbols.
set(PAG_NON_GL_BACKENDS_COUNT 0)
foreach (backend PAG_USE_METAL PAG_USE_VULKAN PAG_USE_D3D12 PAG_USE_WEBGPU)
if (${backend})
math(EXPR PAG_NON_GL_BACKENDS_COUNT "${PAG_NON_GL_BACKENDS_COUNT} + 1")
endif ()
endforeach ()
if (PAG_NON_GL_BACKENDS_COUNT GREATER 1)
message(FATAL_ERROR
"Only one non-OpenGL backend may be enabled at a time "
"(PAG_USE_METAL / PAG_USE_VULKAN / PAG_USE_D3D12 / PAG_USE_WEBGPU).")
endif ()

if (PAG_USE_METAL OR PAG_USE_VULKAN OR PAG_USE_D3D12 OR PAG_USE_WEBGPU)
foreach (gl_variant PAG_USE_OPENGL PAG_USE_QT PAG_USE_SWIFTSHADER PAG_USE_ANGLE)
if (${gl_variant})
message(WARNING "${gl_variant} is ignored because a non-OpenGL backend is enabled.")
endif ()
endforeach ()
set(PAG_USE_OPENGL OFF)
set(PAG_USE_QT OFF)
set(PAG_USE_SWIFTSHADER OFF)
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 行处并修正版本号。

# simulator on arm64 only. Raise the deployment target automatically when Metal is selected so
# `gen_ios -DPAG_USE_METAL=ON` needs no extra flags (as documented in ios/README.md). Without this
# the toolchain's iOS default (9.0) flows into tgfx and trips its >= 13.0 check. Must run before
# tgfx is added via add_subdirectory(). Metal also only exists on Apple platforms — reject it
# elsewhere up front instead of failing later with a pile of undefined symbols.
if (PAG_USE_METAL AND NOT APPLE)
message(FATAL_ERROR "PAG_USE_METAL is only supported on Apple platforms.")
endif ()
# The Metal backend supports the iOS simulator on arm64 only.
if (PAG_USE_METAL AND IOS)
set(CMAKE_XCODE_ATTRIBUTE_EXCLUDED_ARCHS[sdk=iphonesimulator*] "x86_64")
endif ()

if (NOT PAG_USE_OPENGL AND NOT PAG_USE_METAL AND NOT PAG_USE_VULKAN
AND NOT PAG_USE_D3D12 AND NOT PAG_USE_WEBGPU)
message(FATAL_ERROR
"At least one GPU backend must be enabled "
"(PAG_USE_OPENGL / PAG_USE_METAL / PAG_USE_VULKAN / PAG_USE_D3D12 / PAG_USE_WEBGPU).")
endif ()

if (PAG_USE_QT)
set(PAG_USE_SWIFTSHADER OFF)
set(PAG_USE_ANGLE OFF)
Expand Down Expand Up @@ -120,6 +191,26 @@ if (PAG_USE_QT OR PAG_USE_SWIFTSHADER OR PAG_USE_ANGLE)
set(PAG_USE_OPENGL ON)
endif ()

# Expose the selected tgfx backend to libpag sources. The Devices_* implementation files dispatch
# on this macro to pick the right tgfx backend headers; kept in sync with the TGFX_USE_* variables
# passed to tgfx further down. libpag defines its own copy instead of relying on tgfx's PRIVATE
# compile definitions, so tgfx can change its visibility policy without breaking libpag.
if (PAG_USE_OPENGL)
list(APPEND PAG_DEFINES TGFX_USE_OPENGL)
endif ()
if (PAG_USE_METAL)

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] 非 GL 后端之间没有互斥,且 libpag 与 tgfx 的选择优先级相反 → 链接期 undefined symbol

证据链:

  1. :110-115 的互斥块只把 PAG_USE_OPENGL / QT / SwiftShader / ANGLE 置 OFF,Metal / Vulkan / D3D12 / WebGPU 彼此无互斥(而 :104-106 的注释却声称 "mutually exclusive with each other");
  2. 此处是 5 个独立的 if(不是 elseif),可同时定义多个 TGFX_USE_*;
  3. 两个开关都会透传给 tgfx(:722-723 / :750-751 / :788-789);
  4. tgfx CMakeLists.txt:98-136 是带优先级的 if/elseif 链:VULKAN > D3D12 > METAL > WEBGPU,命中 Vulkan 后显式 set(TGFX_USE_METAL OFF);且 :98-100 允许 macOS 开 Vulkan,不会被平台校验挡掉;
  5. tgfx 作为子目录,其 set(... OFF) 只影响自身 directory scope,libpag 的 PAG_DEFINES 已在此处定型,不会被回写;
  6. Devices_Metal.mm 仍会被编译(:260-265 只在 NOT APPLE 时排除)。

于是 macOS 上 -DPAG_USE_METAL=ON -DPAG_USE_VULKAN=ON → tgfx 只编 Vulkan 后端,libpag 仍编 Devices_Metal.mm → 链接期 undefined symbol: tgfx::MetalDevice::Make()。而 Devices_Backend.cpp:28 的哨兵是 #if defined(TGFX_USE_OPENGL) || defined(TGFX_USE_METAL),Metal 有定义即命中第一分支 → 不报 #error → 恰好放过了它自称要消灭的失败形式(该文件注释原文:「instead of a late link-time undefined-symbol failure」)。

放大器是 :157-160 的注释:libpag 故意「定义自己的一份宏而不依赖 tgfx 的 PRIVATE 定义」——这正是让两侧后端选择产生分歧的机制。

建议::110 处补非 GL 后端之间的 FATAL_ERROR 互斥,且优先级与 tgfx 同序;同时把 Devices_Backend.cpp 的哨兵改为逐后端独立 #if + 计数校验,否则它对「多后端同时定义」永久失效。

适用范围说明:GL 与 Metal 之间互斥是有效的(:110-111 确实生效),不存在重复符号,那个方向是误报。本条只针对非 GL 后端之间,失效形式是 undefined symbol 而非重复定义。

另一处 Minor::110-115 全段无任何 message 调用,静默丢弃用户显式传入的 -DPAG_USE_QT=ON;而紧邻的 :117-122「一个后端都没开」却是 FATAL_ERROR,同段逻辑对两种误配置强度不对称。

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 54d2f0d 中修复:按建议补了非 GL 后端之间的互斥——CMake configure 阶段统计 PAG_NON_GL_BACKENDS_COUNT,大于 1 即 FATAL_ERROR(CMakeLists.txt:116-125),互斥时机在 tgfx add_subdirectory 与 QT/SwiftShader fixup 之前;同时对非 GL 后端开启时被忽略的 GL 系开关改为显式 WARNING 提示。Devices_Metal.mm / TGFXCast_Metal.mm 在非 Apple 平台从源列表剔除(commit d23cc88),Linux/Web 不再受 .mm 影响。

list(APPEND PAG_DEFINES TGFX_USE_METAL)
endif ()
if (PAG_USE_VULKAN)
list(APPEND PAG_DEFINES TGFX_USE_VULKAN)
endif ()
if (PAG_USE_D3D12)
list(APPEND PAG_DEFINES TGFX_USE_D3D12)
endif ()
if (PAG_USE_WEBGPU)
list(APPEND PAG_DEFINES TGFX_USE_WEBGPU)
endif ()

if (PAG_USE_FFAVC)
set(PAG_USE_LIBAVC OFF)
endif ()
Expand Down Expand Up @@ -201,6 +292,13 @@ file(GLOB_RECURSE SRC_FILES
src/base/*.*
src/codec/*.*
src/rendering/*.*)
# Devices_Metal.mm is an Objective-C++ file; it only has content under TGFX_USE_METAL. On non-Apple
# platforms the GCC toolchain lacks the Objective-C++ frontend (`cc1objplus`) and would fail even
# compiling an empty .mm. Drop it from the source list outside of Apple.
if (NOT APPLE)
list(FILTER SRC_FILES EXCLUDE REGEX "src/rendering/gpu/Devices_Metal\\.mm$")
list(FILTER SRC_FILES EXCLUDE REGEX "src/base/utils/TGFXCast_Metal\\.mm$")
endif ()
list(APPEND PAG_FILES ${SRC_FILES})

if (PAG_BUILD_PAGX)
Expand Down Expand Up @@ -465,8 +563,7 @@ elseif (IOS)
find_library(OpenGLES_LIBS OpenGLES REQUIRED)
list(APPEND PAG_SHARED_LIBS ${OpenGLES_LIBS})
list(APPEND PAG_DEFINES GLES_SILENCE_DEPRECATION)
else ()
# Uses Metal instead.
elseif (PAG_USE_METAL)
find_library(Metal_LIBS Metal REQUIRED)
list(APPEND PAG_SHARED_LIBS ${Metal_LIBS})
endif ()
Expand Down Expand Up @@ -508,8 +605,7 @@ elseif (MACOS)
find_library(OpenGL_LIBS OpenGL REQUIRED)
list(APPEND PAG_SHARED_LIBS ${OpenGL_LIBS})
list(APPEND PAG_DEFINES GL_SILENCE_DEPRECATION)
else ()
# Uses Metal instead.
elseif (PAG_USE_METAL)
find_library(Metal_LIBS Metal REQUIRED)
list(APPEND PAG_SHARED_LIBS ${Metal_LIBS})
endif ()
Expand Down Expand Up @@ -665,6 +761,10 @@ elseif (HAS_CUSTOM_TGFX_DIR)
message("TGFX_DIR: ${TGFX_DIR}")
list(APPEND PAG_INCLUDES ${TGFX_DIR}/include)
set(TGFX_USE_OPENGL ${PAG_USE_OPENGL})
set(TGFX_USE_METAL ${PAG_USE_METAL})
set(TGFX_USE_VULKAN ${PAG_USE_VULKAN})
set(TGFX_USE_D3D12 ${PAG_USE_D3D12})
set(TGFX_USE_WEBGPU ${PAG_USE_WEBGPU})
set(TGFX_USE_QT ${PAG_USE_QT})
set(TGFX_USE_SWIFTSHADER ${PAG_USE_SWIFTSHADER})
set(TGFX_USE_ANGLE ${PAG_USE_ANGLE})
Expand All @@ -689,6 +789,10 @@ else ()
if (EXISTS ${TGFX_CACHE_DIR})
message("TGFX_CACHE_DIR: ${TGFX_CACHE_DIR}")
list(APPEND TGFX_OPTIONS "-DTGFX_USE_OPENGL=${PAG_USE_OPENGL}")
list(APPEND TGFX_OPTIONS "-DTGFX_USE_METAL=${PAG_USE_METAL}")
list(APPEND TGFX_OPTIONS "-DTGFX_USE_VULKAN=${PAG_USE_VULKAN}")
list(APPEND TGFX_OPTIONS "-DTGFX_USE_D3D12=${PAG_USE_D3D12}")
list(APPEND TGFX_OPTIONS "-DTGFX_USE_WEBGPU=${PAG_USE_WEBGPU}")
list(APPEND TGFX_OPTIONS "-DTGFX_USE_QT=${PAG_USE_QT}")
list(APPEND TGFX_OPTIONS "-DTGFX_USE_SWIFTSHADER=${PAG_USE_SWIFTSHADER}")
list(APPEND TGFX_OPTIONS "-DTGFX_USE_ANGLE=${PAG_USE_ANGLE}")
Expand Down Expand Up @@ -723,6 +827,10 @@ else ()
else ()
message("TGFX_DIR: ${TGFX_DIR}")
set(TGFX_USE_OPENGL ${PAG_USE_OPENGL})
set(TGFX_USE_METAL ${PAG_USE_METAL})
set(TGFX_USE_VULKAN ${PAG_USE_VULKAN})
set(TGFX_USE_D3D12 ${PAG_USE_D3D12})
set(TGFX_USE_WEBGPU ${PAG_USE_WEBGPU})
set(TGFX_USE_QT ${PAG_USE_QT})
set(TGFX_USE_SWIFTSHADER ${PAG_USE_SWIFTSHADER})
set(TGFX_USE_ANGLE ${PAG_USE_ANGLE})
Expand Down
2 changes: 1 addition & 1 deletion DEPS
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
{
"url": "${PAG_GROUP}/tgfx.git",
"commit": "062b864b1425de23fe5a63fbb67137974a4af215",
"commit": "200dffffa6f65f5b5fa3a2e6ae585a970e831b40",
"dir": "third_party/tgfx"
},
{
Expand Down
14 changes: 14 additions & 0 deletions include/pag/c/pag_backend_semaphore.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,18 @@ PAG_EXPORT void pag_backend_semaphore_init_gl(pag_backend_semaphore* semaphore,

PAG_EXPORT void* pag_backend_semaphore_get_gl_sync(pag_backend_semaphore* semaphore);

/**
* Initialize the semaphore with a Metal id<MTLEvent> and a timeline signal value. Passing a
* null event leaves the semaphore uninitialized.
*/
PAG_EXPORT void pag_backend_semaphore_init_metal(pag_backend_semaphore* semaphore,
void* mtlEvent, unsigned long long value);

/**
* Copies the underlying id<MTLEvent> handle and timeline value into eventInfo. Returns false and
* leaves eventInfo untouched if the semaphore was not initialized for the Metal backend.
*/
PAG_EXPORT bool pag_backend_semaphore_get_mtl_event(pag_backend_semaphore* semaphore,
pag_mtl_event_info* eventInfo);

PAG_C_PLUS_PLUS_END_GUARD
8 changes: 8 additions & 0 deletions include/pag/c/pag_backend_texture.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ PAG_C_PLUS_PLUS_BEGIN_GUARD
PAG_EXPORT pag_backend_texture* pag_backend_texture_create_from_gl_texture_info(
pag_gl_texture_info textureInfo, int width, int height);

/**
* Creates a new pag_backend_texture from a Metal id<MTLTexture>. Release it by pag_release.
* The MTLTexture pointer inside textureInfo is stored as an opaque handle; libpag will not
* release it.
*/
PAG_EXPORT pag_backend_texture* pag_backend_texture_create_from_mtl_texture_info(
pag_mtl_texture_info textureInfo, int width, int height);

PAG_EXPORT bool pag_backend_texture_get_gl_texture_info(pag_backend_texture* texture,
pag_gl_texture_info* textureInfo);

Expand Down
6 changes: 6 additions & 0 deletions include/pag/c/pag_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ typedef struct pag_mtl_texture_info {
void* texture = nullptr;
} pag_mtl_texture_info;

// Sync info for an id<MTLEvent>. Always timeline-style (event pointer + signal value).
typedef struct pag_mtl_event_info {
void* event = nullptr;
unsigned long long value = 0;
} pag_mtl_event_info;

typedef struct pag_point {
float x;
float y;
Expand Down
47 changes: 44 additions & 3 deletions include/pag/gpu.h
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,22 @@ struct MtlTextureInfo {
void* texture = nullptr;
};

/**
* Types for interacting with Metal sync objects created externally to PAG. Holds an id<MTLEvent>
* and its timeline signal value as opaque handles.
*/
struct MtlEventInfo {
/**

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] format 默认 70(RGBA8Unorm) 与 Apple 惯例相反,导致回读与高级混合静默红蓝互换

根因:tgfx 里 proxy 与 resource 并存两套 format,回读路径用的是「用户传入」那份。

返回什么 出处
RenderTargetProxy::format() 用户传入的 MtlTextureInfo::format TextureRenderTargetProxy.h:41-43 → return _format;
RenderTarget::format()(资源) 真实 MTLTexture 的 pixelFormat RenderTarget.h:101-103 → getRenderTexture()->format()

而 RenderContext::renderTarget 是 proxy(RenderContext.h:100)。

失效点 1 — 回读红蓝互换(置信度高):Surface.cpp:219-221(asyncReadPixels)取 renderContext->renderTarget->format(),即 proxy 那份用户值;同步 readPixels(:239-249)内部就是调它。终点是 libpag PAGSurface.cpp:188-205,而 PAGSurfaceImpl.mm:210(makeSnapshot)/:224(copyPixelsTo:) 把 dstInfo 写死 BGRA_8888。用户传 BGRA8Unorm 纹理却未设 format 时:GPU→buffer 是原始 BGRA 字节,srcInfo 声称 RGBA,dst 又是 BGRA → 转换器多做一次 R/B 交换 → 输出红蓝互换,无任何日志。

失效点 2 — 高级混合上屏(置信度中高,请确认):OpsCompositor.h:148 的 renderTarget 同样是 proxy,OpsCompositor.cpp:1126-1128 按 renderTarget->format()(用户值)分配 dst 拷贝中间纹理,而 MetalCommandEncoder.mm:73-99 的 copyTextureToTexture() 是裸 blit 无格式转换 → 源真实 BGRA、中间纹理按 RGBA 分配、原样搬字节。具体表现为互换还是被 Metal 异格式 blit validation 拒绝未实机验证。 这条恰好命中本 PR 新增的 PAGBlendMetalTest,但因 CI 不跑 Metal 永不暴露。

触发条件:传 BGRA8Unorm 纹理且未显式设 format;显式设 80 则两处都正常。而默认值恰在错的一侧——iOS/macOS 纹理绝大多数是 BGRA8Unorm,本 PR 自己在 ios/PAGView.mm:202、mac/PAGView.mm:191 设的都是 MTLPixelFormatBGRA8Unorm;MakeFrom(BackendTexture) + 回读又是离屏渲染最主流的组合。

另:本处文档说 "must set this field so tgfx can bind the texture correctly",两个论断都与实现相反——不设也能正确绑定,采样绑定路径也不看它(MetalTexture.mm:107 用 ToPixelFormat(metalTexture.pixelFormat))。

建议:把 TGFXCast.cpp 的 Metal 分支移到 .mm,用 ((__bridge id<MTLTexture>)mtlInfo.texture).pixelFormat 覆盖用户传值,从根上消除出错可能。

两个无效的修法(评审中验证过):① 删字段 → 见 :133 那条,会固化 bug;② 默认值改 0 换干净失败 → MetalDefines.h:35-52 的 MetalPixelFormatToPixelFormat() 结尾是 default: return RGBA_8888,在读用户 format 的这条路径上永不返回 Unknown,ProxyProvider.cpp:577-579 的早退不可达。
另:70 这个值逐字继承自 tgfx MetalTypes.h(连注释都一致),不是本 PR 自创的魔数,问题只在默认值方向与文档写反。

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.

采纳了"从真实 MTLTexture 覆盖"的方向并更进一步——commit fd36e5c 直接删除了 format 字段:tgfx 侧一律用 ((__bridge id<MTLTexture>)handle).pixelFormat 读取真实格式,不再存在用户传值这条路径,默认值方向错误、回读红蓝互换、高级混合 blit 三类问题从根上消除。头文件中"must set this field"的文档也随字段删除一并移除。回读路径已通过 Metal 后端与 GL 后端的逐用例像素对比验证(含 BGRA8Unorm 外部纹理回读)。

* Pointer to id<MTLEvent>.
*/
void* event = nullptr;

/**
* The timeline signal value paired with the event.
*/
uint64_t value = 0;
};

/**
* Types for interacting with Vulkan resources created externally to PAG. Holds the VkImage as a
* void*.
Expand Down Expand Up @@ -312,19 +328,44 @@ class PAG_API BackendSemaphore {
public:
BackendSemaphore();

bool isInitialized() const {
return _isInitialized;
/**
* Returns true if the backend semaphore has been initialized.
*/
bool isInitialized() const;

/**
* Returns the backend API of this semaphore.
*/
Backend backend() const {
return _backend;
}

void initGL(void* sync);

void* glSync() const;

/**
* Initializes the semaphore with a Metal id<MTLEvent> and a signal value. The event pointer is
* treated as an opaque handle — libpag never releases it. Metal's cross-queue synchronization
* always uses timeline (signaled/waited by counter value) semantics on MTLEvent, so both the
* event and its value are required. Passing a null event leaves the semaphore uninitialized.
*/
void initMetal(const MtlEventInfo& info);

/**
* If the backend API is Metal, copies a snapshot of the MtlEventInfo struct into the passed in
* pointer and returns true. Otherwise, returns false if the backend API is not Metal.
*/
bool getMtlEventInfo(MtlEventInfo* info) const;

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 的这一变更。

Backend _backend = Backend::MOCK;
union {

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] BackendSemaphore ABI 破坏:sizeof 24→32,_isInitialized 偏移 16→24,而 getter 是 inline

union 从 {void* _glSync}(8B)扩到含 MtlEventInfo(16B),把后续 bool _isInitialized 整体后移 8 字节。实测(clang++ 17 / arm64 复刻改动前后布局):

BackendSemaphore : old sizeof=24  _isInitialized@16  |  new sizeof=32  _isInitialized@24

isInitialized() 在 :323-325 是头文件内 inline 定义,会被内联进调用方二进制。老 App(按偏移 16 读)+ 新 libpag(写偏移 24)混用时:isInitialized() 读到的是 union 尾部字节而非标志位;且老 App 在栈上按 24 字节分配、新 libpag 按 32 字节写入 → 越界写。libpag 在 Apple 平台以 framework / CocoaPods 预编译二进制分发,正是最容易头库版本不一致的场景(纯源码构建不受影响)。

建议(可让这侧 ABI 破坏完全消失):删掉 _isInitialized,照搬 tgfx 的形态。 锁定版 tgfx 的 BackendSemaphore(Backend.h:385-393)私有区只有 _backend + union,根本没有这个成员;isInitialized() 在 :346 仅声明、out-of-line,实现在 Backend.cpp:256-273 按 switch(_backend) + handle 非空推导。

改完收益是复合的:

  • sizeof 变 _backend(4)+pad(4)+union(16) = 24,与改动前完全相同,_backend@0 / union@8 偏移也不变;
  • 自动修掉 GPUBackend.cpp:170-178 的「initMetal(nullptr, v) 不复位」问题(初始化状态直接由 event != nullptr 推导);
  • 与「补 backend() 访问器」「MtlEventInfo 提升为公开 struct」的改动天然合并。

那个多余的 bool 正是偏移漂移与 nullptr 不复位两个问题的共同根因。

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 0f0abcc 中按推荐方案修复:删除 _isInitialized 成员,初始化状态由 _backend + union 内 handle 推导,isInitialized() 改为 out-of-line 实现(按 tgfx Backend.cpp 的形态)。sizeof 恢复为 24、_backend@0 / union@8 偏移与改动前一致,ABI 破坏消除;initMetal(nullptr, v) 不复位的问题也随状态推导一并修复。后续 0f94f5a 将 Metal 访问器整合为公开的 MtlEventInfo struct + getMtlEventInfo(),并补了 backend() 访问器。

void* _glSync;
MtlEventInfo _mtlInfo;
};
bool _isInitialized;
};
} // namespace pag
8 changes: 4 additions & 4 deletions include/pag/pag.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ struct Rect;
class Context;
class Surface;
class ImageInfo;
class GLDevice;
class Device;
} // namespace tgfx

namespace pag {
Expand Down Expand Up @@ -1271,7 +1271,7 @@ class PAG_API PAGSurface {
std::shared_ptr<std::mutex> rootLocker = nullptr;
std::shared_ptr<Drawable> drawable = nullptr;
bool externalContext = false;
void* glRestorer = nullptr;
void* stateGuard = nullptr;

bool draw(RenderCache* cache, std::shared_ptr<Graphic> graphic, BackendSemaphore* signalSemaphore,
bool autoClear = true);
Expand Down Expand Up @@ -1645,7 +1645,7 @@ class PAG_API PAGDecoder {
int _numFrames = 0;
float _frameRate = 30.0f;
float maxFrameRate = 30.0f;
std::shared_ptr<tgfx::GLDevice> sharedDevice = nullptr;
std::shared_ptr<tgfx::Device> sharedDevice = nullptr;
int lastReadIndex = -1;
tgfx::ImageInfo* lastImageInfo = nullptr;
uint32_t lastContentVersion = 0;
Expand All @@ -1663,7 +1663,7 @@ class PAG_API PAGDecoder {
int numFrames);

PAGDecoder(std::shared_ptr<PAGComposition> composition, int width, int height, int numFrames,
float frameRate, float maxFrameRate, std::shared_ptr<tgfx::GLDevice> sharedDevice);
float frameRate, float maxFrameRate, std::shared_ptr<tgfx::Device> sharedDevice);

bool readFrameInternal(int index, std::shared_ptr<BitmapBuffer> bitmap);
bool renderFrame(std::shared_ptr<PAGComposition> composition, int index,
Expand Down
8 changes: 6 additions & 2 deletions include/pagx/tgfx.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,13 @@ std::unique_ptr<tgfx::Recording> Record(tgfx::Context* context,
* Creates a PAGSurface wrapping an existing tgfx::Surface, sharing ownership. Use this when the
* host already has a tgfx::Surface and wants to render a PAGScene into it via Record() without
* creating a separate render target.
* @param device the tgfx::Device the surface was created from. The device is required because a
* tgfx::Surface does not carry its owning device back-reference, and rendering
* must lock that exact device.
* @param surface the tgfx::Surface to wrap.
* @return a PAGSurface sharing ownership, or nullptr if surface is null.
* @return a PAGSurface sharing ownership, or nullptr if device or surface is null.
*/
std::shared_ptr<PAGSurface> MakeFrom(const std::shared_ptr<tgfx::Surface>& surface);
std::shared_ptr<PAGSurface> MakeFrom(const std::shared_ptr<tgfx::Device>& device,
const std::shared_ptr<tgfx::Surface>& surface);

} // namespace pagx
6 changes: 6 additions & 0 deletions ios/PAGViewer.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
F2BEA1332B032AEC00256135 /* libpag.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = F2BEA12E2B0321F400256135 /* libpag.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
F2C7AD5527A9202C00DEBEC6 /* rotation.jpg in Resources */ = {isa = PBXBuildFile; fileRef = F2C7AD5327A91FF600DEBEC6 /* rotation.jpg */; };
F2ED88A31FCBEDF100555707 /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = F2ED88A21FCBEDF100555707 /* AppDelegate.mm */; };
A1C0FFF02D5111A000000002 /* SceneDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = A1C0FFEF2D5111A000000001 /* SceneDelegate.mm */; };
F2ED88A91FCBEDF100555707 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F2ED88A71FCBEDF100555707 /* Main.storyboard */; };
F2ED88AB1FCBEDF100555707 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F2ED88AA1FCBEDF100555707 /* Assets.xcassets */; };
F2ED88AE1FCBEDF100555707 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F2ED88AC1FCBEDF100555707 /* LaunchScreen.storyboard */; };
Expand Down Expand Up @@ -175,6 +176,8 @@
F2ED889E1FCBEDF100555707 /* PAGViewer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PAGViewer.app; sourceTree = BUILT_PRODUCTS_DIR; };
F2ED88A11FCBEDF100555707 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
F2ED88A21FCBEDF100555707 /* AppDelegate.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AppDelegate.mm; sourceTree = "<group>"; };
A1C0FFEE2D5111A000000001 /* SceneDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SceneDelegate.h; sourceTree = "<group>"; };
A1C0FFEF2D5111A000000001 /* SceneDelegate.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SceneDelegate.mm; sourceTree = "<group>"; };
F2ED88A81FCBEDF100555707 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
F2ED88AA1FCBEDF100555707 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
F2ED88AD1FCBEDF100555707 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
Expand Down Expand Up @@ -310,6 +313,8 @@
4DC5D301239FD95D007DFA8C /* assets */,
F2ED88A11FCBEDF100555707 /* AppDelegate.h */,
F2ED88A21FCBEDF100555707 /* AppDelegate.mm */,
A1C0FFEE2D5111A000000001 /* SceneDelegate.h */,
A1C0FFEF2D5111A000000001 /* SceneDelegate.mm */,
F2ED88AA1FCBEDF100555707 /* Assets.xcassets */,
5478FF75278EFE3700999C9C /* Info.plist */,
F2ED88A71FCBEDF100555707 /* Main.storyboard */,
Expand Down Expand Up @@ -460,6 +465,7 @@
files = (
F2ED88B11FCBEDF100555707 /* main.mm in Sources */,
F2ED88A31FCBEDF100555707 /* AppDelegate.mm in Sources */,
A1C0FFF02D5111A000000002 /* SceneDelegate.mm in Sources */,
4DE99A5227BFC16F0003AA0A /* ViewController.mm in Sources */,
4DE99A4E27BFC16F0003AA0A /* BackgroundView.m in Sources */,
);
Expand Down
Loading
Loading