Polymorphic Blueprint
Open in Telegram
Advanced topics of software engineering [Runglish/English]. { Sometimes quite ridiculously in-depth } Author → @eleev Copyright © 2024-2026 Astemir Eleev. #swift #ios #apple #wwdc #metal #llvm #programming #typesystems #compiler #systemdesign #fp #dsa
Show more1 588
Subscribers
+324 hours
+17 days
+930 days
Posts Archive
#metal #physics #gpu #highperf #ui
Metal Graph-Node Editor.
Мне нужно было как то репрезентить, редактировать, добавлять и удалять элементы сцены. Я не придумал ничего луче, чем написать прямо на GPU весь high-perf UI для нод-редактора, аля blueprints (который уже deprecated) в Unreal Engine. Well... микро версия, пока. Почему не кит или суи? Да просто бороться с тем, с чем можно не иметь дела крутое состояние. Написав UI сразу на Metal я открыл hichless опыт и возможность в 120 FPS рендерить тысячи нодов и десятки тысяч ребер графа, анимированных и с не менее fluid UX.
Это все не без ряда ухищрений достигается, чуть-чуть комплекснее, чем закоммитить транзакцию на рендер сервер на следующем пасе.
@contravariance
#metal #physics #gpu #highperf
Metal Physics Engine.
Мне было мало микро-yolo-poc простого басика и rubber-joint physics для утенка. По этому я взял некоторые богатство GPU Gems от Nvidia и написал полноценный движок для joint физики и physically based water simulation.
Заодно realtime soft shadows with ambient occlusion, а еще ряд фич кастомного рендера и физ движка.
Один GPU инженер не знакомый с тонкостями чипов Apple, был весьма впечатлен потенциалом железа. Сами подумайте: не самый новы чип A18 Pro в своей совокупности комьюта превосходит PS4, у вас в кармане (PS4: 1,84 TFLOPS vs iPhone 16 Pro (A18 Pro) 2,15–2,3 TFLOPS). Мы не говорим про М или более новые версии.
Если все же будет интересно, тут некоторые фичи из симуляции: • Realtime soft shadows • Ambient occlusion • Shadow-water-wave refraction • New perspective 2d to 3d floating optical projection of duck • Self-shading on 3d floating projection physics movement • Procedural pool • Custom tile pattern generator with persisting hash-seeds • Water wave physics • Custom Water Postprocess Pipeline • Laplacian damped wave equation • Pool borders bounciness / interaction • Duck’s physics body interaction • Floating buoyancy physics • New wave heat map generation debug • Procedural pool bezel-border: a custom rounded rect SDF • Duck buoyancy movement with injected elongated impulses ahead/behind motion • New water refraction system • Spring-joint system redux • Adjustable center of mass, rotation, size • New joint physics state heat map debug overlay • New spring physics state heat map debug overlay • Specular perspective transform light for the duck • Shadowmaped highmap to more accurately shade / project specular light@contravariance
#metal #physics #engine #simulation #ui
Footnote: Metal Spring Joint Physics Engine. Rubber duck physics.
Движок работает на кастомном multipass рендере, через compute kernel.
Как нибудь покажу полную реплику порталирования, но не средствами CoreAnimation ака _UIPortalView / CAPortalView, а ... из игры Portal (Valve) в 2D, на Metal 🎸конечно.
@contravariance
#metal #ui #highperf #pool
Hey, Pool App! I like your duck
p.s. приложение хорошо нашумело, там есть наши ребята @lovedeathstartups
Я для утенка подогнал новый басик, ну и утенка апгрейднул :D
Все процедурно на Metal, утенок - единственный импорируемый асет, растер управляемый кастомным spring joint animation engine: кастомная итерполяция текселей на джоинты.
Все круто, но самое утомительное это подобрать во всей системе параметры... то утенок был надувным, то желейным...
@contravariance
#metal #ui #highperf #gpu
Vol I. Footnote II: Geometry Subdivision Rolling Window, GPU Buffer Reuse, 15 Parallel Video Streams, Low-Freq-High-Perf.
@contravariance
#metal #ui #highperf #gpu
Vol I. Footnote. Reverse Cylindric Roll Transform. Shadow Casting + Soft Shading.
@constravariance
#metal #ui #highperf #gpu
Metal High Performance Video Feed. 15 Streams, Shadow Casting, Cylindric Roll Transformations. Vol I.
@lazy_var сделал крутые посты по цилиндрическому ролу для произвольных scrollable UI компонент, одним из самых перформант способов доступных легальным способом на Apple OSs.
Я покажу, как выглядит построенный from ground up весь пайплайн high-performance feed с тем же цилинтрическим ролом, но на GPU через трио vertex, tesselation и fragment шейдерах, на кастомном не менее high perf рендере. И не просто статический контент... кто то говорит, что больше 2-3 потоков видео не запустить, декодеры yada-yada... это и правда и нет.
Как говориться, hold my beer... 15 потоков видео на high-perf фид ленте утилизируя весь компьют и практически не нагружая main thread. Практически zero UIKit и ровно zero SwiftUI.
У коллекции на голом Metal есть реюз (как у UICollectionView), rolling tesselation window - кто говорил что DSA не нужны в мобилке - это окно тесселяции, там где происходит цилиндрический бендинг я делаю сабдивижен геометрии, а там где cells физически всегда плоские - тесселяция всегда отключена - простой triangle strip (можно понимать, как два треугольника и общим index buffer - экономия на паре вертексов на каждый vertex draw call) - экономия за счет vertex winding order.
Что по видео фиду? Можно lossless компрессить поток на комьют кернеле, ресемлировать на фрагментом стейдже, в общем 15 видео потоков на GPU занимают в пике... чуть больше 300Мб. В среднем - 150-180. А ну еще это в 120FPS Pro Motion работает. Минимальный таргет iOS 16+ - мне там некоторые части метала нужны были. Видео feeds не нужно делать полностью из ... непрерывно воспроизводимого видео, но это не про продукт - а про deep tech.
Dynamic Island теперь ... кастит коническую тень на видео фид. Все честно считается, а debug button кастит тень и через CoreAnimation синкнут с elastic easing functions связывая все в единое целом.
Там миллион параметров торчит наружу, еще больше в API;
Один крутой инженер из Nvidia говорит, что фид ленты 92% бигтехов и больших команд никогда не смогут сделать перформант. Не потому, что инженеры не такие какие то - нет они умные и опытные, мне есть чему учиться у вас всех - просто инженеры сковывают себя рамками и борятся с тем, с чем можно просто не иметь дела. Можно подумать, при чем тут Nvidia :]
...
Может сделаю вторую часть, Vol I тут скорее номинально для того, чтобы показать, что тема и материал - весьма глубокий и обширный. Stay tuned.
@contravariance
#metal #ui #cg #computergraphics #rendering #gpu
Multipass Rendering. Ping-Pong Render. Vol II. Double Feedback Loop. Part II.
В Vol I вся сложность жила в математике одного шейдера. В Frost Renderer примечательный момент - там, где обычно используют preloaded normal/noise текстуры (typical bump-mapping подход), можно вместо этого генерировать Worley (cellular) noise прямо во фрагментном шейдере, без единого байта текстуры на диске:
static float worley(float2 uv, float scale) {
float2 p = uv * scale;
float2 cell = floor(p);
float2 f = fract(p);
float minDist = 1.0;
for (int y = -1; y <= 1; y++)
for (int x = -1; x <= 1; x++) {
float2 point = hash(cell + float2(x,y));
minDist = min(minDist, length(float2(x,y) + point - f));
}
return minDist;
}
Это не микро оптимизация - это устранение целого класса зависимостей: no asset pipeline, no texture loading, no VRAM footprint под noise maps, no licensing вопросов на текстуры. Трейдофф - несколько дополнительных ALU операций на пиксель (3×3 cell lookup), что на практике дешевле, чем bandwidth на текстурный fetch с произвольным noise-семплом, особенно если noise нужен на нескольких разных масштабах одновременно (как в случае с fiber-стрики и deep-freeze peaks читают один и тот же procedural noise на разных scale-параметрах, без необходимости держать несколько preloaded текстур разных размеров).
Как то была дискуссия, как с меньшими трейдофами задизайнить погодные эффекты в Weather App. Был аргумент, что эффективнее все запечь в alpha-blended .hevc x264-265 (а зависимости от наличия декодеров на конкретном SoC). Аргумент очевидно валидный, если не учесть одно но - если нам надо как то взаимодействовать с этими эффектами. Запечь уже не получиться все (либо вовсе ничего) - по этому полностью процедурный подход все же имеет свою ценность в ряде случаев.
Когда есть функция шума, нужно еще одно свойство, чтобы эффект выглядел правдоподобно. Слово анизотропный вы могли встречать в контексте anisotropic filtering - это другой, несвязанный концепт (про сэмплинг текстур под углом), просто термин совпадает. Здесь речь про анизотропный шум: изотропный (isotropic) шум выглядит одинаково во всех направлениях - нет выделенной оси, визуально читается как равномерный. Анизотропный - разный вдоль разных осей: структура вдоль одного направления отличается от перпендикулярного. Так обычно растут кристаллы изморози - направленно, а не равномерно. Конкретный метод может быть сколь угодно простым/сложным, важен принцип и выделенный фрагментный композитинг.Видео как backgroundTexture: zero-copy через CVMetalTextureCache В Vol I
background был статичной текстурой. Frost Renderer поддерживает live video feed как background - это требует моста между AVFoundation pixel buffers и Metal текстурами без копирования байтов на CPU:
CVMetalTextureCacheCreateTextureFromImage(
nil, textureCache, pixelBuffer, nil,
.bgra8Unorm, width, height, 0, &cvTexture
)
let texture = CVMetalTextureGetTexture(cvTexture!)! // NIT: simplification
CVPixelBuffer (decoder output) и MTLTexture (GPU-sampleable resource) указывают на общую IOSurface-backed память - CVMetalTextureCacheCreateTextureFromImage не копирует пиксели, а создаёт Metal совместимый view поверх уже существующей видеопамяти. Ключевая deталь, без которой это не работает - kCVPixelBufferMetalCompatibilityKey: true в pixelBufferAttributes при создании AVPlayerItemVideoOutput: без этого флага CVMetalTextureCacheCreateTextureFromImage может вернуть nil без явной ошибки, и дебажить такое - отдельное удовольствие.
Самое примечательное, что этот рендер не только весьма легковестный, но и отлично интегрируется в любой интерактивный или/и анимированный UI flow.@contravariance
#metal #ui #cg #computergraphics #rendering #gpu
Multipass Rendering. Ping-Pong Render. Vol II. Double Feedback Loop. Part I.
Тот же метод, но два независимых feedback-буфера (A, B) + один non-feedback composite pass (C). Эффект выглядит сложнее, но парадоксально на фрагментный пайплайн накладывается существенно меньший комьют даже с двойным feedback-буфером, потому что update-пассы A и B тривиальны (decay + write), а вся сложность ушла в noise generation внутри compose pass (С - composite pass), а не в feedback loops (A и B).
В первой части мы разобрали ping-pong как паттерн для одной эволюционирующей текстуры - rubber cloth slider, где state - это позиция анкера, затухающая к центру. Frost Renderer: эффект изморози на стекле, который скретчится касанием и нарастает обратно слоями, non-uniformly. Тот же фундамент, но три буфера вместо одного - и именно это даёт качественно новый результат.
Зачем три буфера, если идея та же?
Один ping-pong буфer кодирует одну скорость эволюции состояния. Rubber cloth релаксировал к центру с одной константой затухания - этого достаточно для одной физической величины.
Frost-эффект визуально состоит из двух фаз, которые обязаны жить с разными temporal constants:
•
Buffer A - быстрая фаза: скретч-trail, который тает за доли секунды (decay ~0.97 per frame)
• Buffer B - медленная фаза: глубокая заморозка, которая держится секундами (decay ~0.995 per frame)
Один скаляр не может одновременно затухать с двумя разными константами. Если попытаться воспроизвести обе фазы в одной текстуре (например, в разных каналах RGBA одного буфера), update-шейдер все равно должен применить разные decay-формулы к разным каналам в одном write - технически возможно, но это смешивает две независимые системы состояния в одну текстуру, что усложняет дальнейшую композицию (нельзя, например, передать одну фазу в другой шейдер без нарезки каналов) и убирает архитектурную гибкость иметь раздельные pixel formats, resolution, или update rate под каждую фазу при необходимости.
Архитектура: 3х read/write loops и один финальныый composite pass
Frame N:
• Pass 0: Buffer A update (читает A[N-1], пишет A[N]) → fast scratch decay
• Pass 1: Buffer B update (читает B[N-1], пишет B[N]) → slow freeze decay
• Pass 2:Final composite (читает A[N], B[N], background, normal maps) → drawable
Два независимых ping-pong пара (A читает/пишет себя, B читает/пишет себя - они не зависят друг от друга на стадии update), и только в финальном compose-пассе их результаты сходятся вместе. Это важный архитектурный нюасн: Buffer A и Buffer B не знают о существовании друг друга до самого последнего пасса - каждый эволюционирует как полностью независимая DP-цепочка.
Что объединяет рендер из прошлой части с frost render? Тот же DP-фреймворк, но теперь это не одна рекуррентная переменная, а система из двух независимых рекуррентных переменных, которые объединяются только на стадии "вывода" (compose), не на стадии "обновления" (update). Это прямая аналогия с тем, как в DP можно держать несколько независимых таблиц состояний (например, dp1[i], dp2[i] с разными переходами) и комбинировать их только в финальной формуле.
~0.5ms render frame, A18, release mode, aarch64 Swift 6.3.1, iOS 26.5)@contravariance
#metal #ui #cg #computergraphics #rendering #gpu
Multipass Rendering. Ping-Pong Render. Vol II. Double Feedback Loop. Part II.
В Vol I вся сложность жила в математике одного шейдера. В Frost Renderer примечательный момент - там, где обычно используют preloaded normal/noise текстуры (typical bump-mapping подход), можно вместо этого генерировать Worley (cellular) noise прямо во фрагментном шейдере, без единого байта текстуры на диске:
static float worley(float2 uv, float scale) {
float2 p = uv * scale;
float2 cell = floor(p);
float2 f = fract(p);
float minDist = 1.0;
for (int y = -1; y <= 1; y++)
for (int x = -1; x <= 1; x++) {
float2 point = hash(cell + float2(x,y));
minDist = min(minDist, length(float2(x,y) + point - f));
}
return minDist;
}
Это не микро оптимизация - это устранение целого класса зависимостей: no asset pipeline, no texture loading, no VRAM footprint под noise maps, no licensing вопросов на текстуры. Трейдофф - несколько дополнительных ALU операций на пиксель (3×3 cell lookup), что на практике дешевле, чем bandwidth на текстурный fetch с произвольным noise-семплом, особенно если noise нужен на нескольких разных масштабах одновременно (как в случае с fiber-стрики и deep-freeze peaks читают один и тот же procedural noise на разных scale-параметрах, без необходимости держать несколько preloaded текстур разных размеров).
Как то была дискуссия, как с меньшими трейдофами задизайнить погодные эффекты в Weather App. Был аргумент, что эффективнее все запечь в alpha-blended .hevc x264-265 (а зависимости от наличия декодеров на конкретном SoC). Аргумент очевидно валидный, если не учесть одно но - если нам надо как то взаимодействовать с этими эффектами. Запечь уже не получиться все (либо вовсе ничего) - по этому полностью процедурный подход все же имеет свою ценность в ряде случаев.
Когда есть функция шума, нужно еще одно свойство, чтобы эффект выглядел правдоподобно. Слово "анизотропный" вы могли встречать в контексте anisotropic filtering - это другой, несвязанный концепт (про сэмплинг текстур под углом), просто термин совпадает. Здесь речь про анизотропный шум: изотропный (isotropic) шум выглядит одинаково во всех направлениях - нет выделенной оси, визуально читается как равномерный. Анизотропный - разный вдоль разных осей: структура вдоль одного направления отличается от перпендикулярного. Так обычно растут кристаллы изморози - направленно, а не равномерно. Конкретная реализация может быть сколь угодно простой или сложной - важен сам принцип направленности.Видео как backgroundTexture: zero-copy через CVMetalTextureCache В Vol I background был статичной текстурой. Frost Renderer поддерживает live video feed как background - это требует моста между AVFoundation pixel buffers и Metal текстурами без копирования байтов на CPU:
CVMetalTextureCacheCreateTextureFromImage(
nil, textureCache, pixelBuffer, nil,
.bgra8Unorm, width, height, 0, &cvTexture
)
let texture = CVMetalTextureGetTexture(cvTexture!)! // NIT: simplification
CVPixelBuffer (decoder output) и MTLTexture (GPU-sampleable resource) указывают на общую IOSurface-backed память - CVMetalTextureCacheCreateTextureFromImage не копирует пиксели, а создаёт Metal совместимый view поверх уже существующей видеопамяти. Ключевая deталь, без которой это не работает - kCVPixelBufferMetalCompatibilityKey: true в pixelBufferAttributes при создании AVPlayerItemVideoOutput: без этого флага CVMetalTextureCacheCreateTextureFromImage может вернуть nil без явной ошибки, и дебажить такое - отдельное удовольствие.
Верхнеуровнево это вся техника - никакой магии. Точнее она в общей композиции всех частей вместе.@contravariance
#metal #ui #cg #computergraphics #rendering #gpu
Multipass Rendering. Ping-Pong Render. Vol II. Double Feedback Loop. Part I.
Тот же метод, но два независимых feedback-буфера (A, B) + один non-feedback composite pass (C). Эффект выглядит сложнее, но парадоксально на фрагментный пайплайн накладывается существенно меньший комьют даже с двойным feedback-буфером, потому что update-пассы A и B тривиальны (decay + write), а вся сложность ушла в noise generation внутри compose pass (С - composite pass), а не в feedback loops (A и B).
В первой части мы разобрали ping-pong как паттерн для одной эволюционирующей текстуры - rubber cloth slider, где state - это позиция анкера, затухающая к центру. Frost Renderer: эффект изморози на стекле, который скретчится касанием и нарастает обратно слоями, non-uniformly. Тот же фундамент, но три буфера вместо одного - и именно это даёт качественно новый результат.Зачем три буфера, если идея та же? Один ping-pong буфer кодирует одну скорость эволюции состояния. Rubber cloth релаксировал к центру с одной константой затухания - этого достаточно для одной физической величины. Frost-эффект визуально состоит из двух фаз, которые обязаны жить с разными temporal constants: •
Buffer A - быстрая фаза: скретч-trail, который тает за доли секунды (decay ~0.97 per frame)
• Buffer B - медленная фаза: глубокая заморозка, которая держится секундами (decay ~0.995 per frame)
Один скаляр не может одновременно затухать с двумя разными константами. Если попытаться воспроизвести обе фазы в одной текстуре (например, в разных каналах RGBA одного буфера), update-шейдер все равно должен применить разные decay-формулы к разным каналам в одном write - технически возможно, но это смешивает две независимые системы состояния в одну текстуру, что усложняет дальнейшую композицию (нельзя, например, передать одну фазу в другой шейдер без нарезки каналов) и убирает архитектурную гибкость иметь раздельные pixel formats, resolution, или update rate под каждую фазу при необходимости.
Архитектура: 3х read/write loops и один финальныый composite pass
Frame N:
• Pass 0: Buffer A update (читает A[N-1], пишет A[N]) → fast scratch decay
• Pass 1: Buffer B update (читает B[N-1], пишет B[N]) → slow freeze decay
• Pass 2: Final composite (читает A[N], B[N], background, normal maps) → drawable
Два независимых ping-pong пара (A читает/пишет себя, B читает/пишет себя - они не зависят друг от друга на стадии update), и только в финальном compose-пассе их результаты сходятся вместе. Это важный архитектурный нюасн: Buffer A и Buffer B не знают о существовании друг друга до самого последнего пасса - каждый эволюционирует как полностью независимая DP-цепочка.
Что объединяет рендер из прошлой части с frost render? Тот же DP фреймворк, но теперь это не одна рекуррентная переменная, а система из двух независимых рекуррентных переменных, которые объединяются только на стадии "вывода" (compose), не на стадии "обновления" (update). Это прямая аналогия с тем, как в DP можно держать несколько независимых таблиц состояний (например, dp1[i], dp2[i] с разными переходами) и комбинировать их только в финальной формуле.
~0.5ms render frame, A18, release mode, aarch64 Swift 6.3.1, iOS 26.5@contravariance
#metal #ui #cg #computergraphics #rendering #gpu
Multipass Rendering. Ping-Pong Render. Vol II. Double Feedback Loop. Part II.
В Vol I вся сложность жила в математике одного шейдера. В Frost Renderer примечательный момент - там, где обычно используют preloaded normal/noise текстуры (typical bump-mapping подход), можно вместо этого генерировать Worley (cellular) noise прямо во фрагментном шейдере, без единого байта текстуры на диске:
static float worley(float2 uv, float scale) {
float2 p = uv * scale;
float2 cell = floor(p);
float2 f = fract(p);
float minDist = 1.0;
for (int y = -1; y <= 1; y++)
for (int x = -1; x <= 1; x++) {
float2 point = hash(cell + float2(x,y));
minDist = min(minDist, length(float2(x,y) + point - f));
}
return minDist;
}
Это не микро оптимизация - это устранение целого класса зависимостей: no asset pipeline, no texture loading, no VRAM footprint под noise maps, no licensing вопросов на текстуры. Трейдофф - несколько дополнительных ALU операций на пиксель (3×3 cell lookup), что на практике дешевле, чем bandwidth на текстурный fetch с произвольным noise-семплом, особенно если noise нужен на нескольких разных масштабах одновременно (как в случае с fiber-стрики и deep-freeze peaks читают один и тот же procedural noise на разных scale-параметрах, без необходимости держать несколько preloaded текстур разных размеров).
Как то была дискуссия, как с меньшими трейдофами задизайнить погодные эффекты в Weather App. Был аргумент, что эффективнее все запечь в alpha-blended.hevcx264-265(а зависимости от наличия декодеров на конкретном SoC). Аргумент очевидно валидный, если не учесть одно но - если нам надо как то взаимодействовать с этими эффектами. Запечь уже не получиться все (либо вовсе ничего) - по этому полностью процедурный подход все же имеет свою ценность в ряде случаев.
Когда есть функция шума, нужно еще одно свойство, чтобы эффект выглядел правдоподобно. Слово "анизотропный" вы могли встречать в контексте anisotropic filtering - это другой, несвязанный концепт (про сэмплинг текстур под углом), просто термин совпадает. Здесь речь про анизотропный шум: изотропный (isotropic) шум выглядит одинаково во всех направлениях - нет выделенной оси, визуально читается как равномерный. Анизотропный - разный вдоль разных осей: структура вдоль одного направления отличается от перпендикулярного. Так обычно растут кристаллы изморози - направленно, а не равномерно. Конкретная реализация может быть сколь угодно простой или сложной - важен сам принцип направленности.Видео как backgroundTexture: zero-copy через
CVMetalTextureCache
В Vol I background был статичной текстурой. Frost Renderer поддерживает live video feed как background - это требует моста между AVFoundation pixel buffers и Metal текстурами без копирования байтов на CPU:
CVMetalTextureCacheCreateTextureFromImage(
nil, textureCache, pixelBuffer, nil,
.bgra8Unorm, width, height, 0, &cvTexture
)
let texture = CVMetalTextureGetTexture(cvTexture!)! // NIT: simplification
CVPixelBuffer (decoder output) и MTLTexture (GPU-sampleable resource) указывают на общую IOSurface-backed память - CVMetalTextureCacheCreateTextureFromImage не копирует пиксели, а создаёт Metal совместимый view поверх уже существующей видеопамяти. Ключевая deталь, без которой это не работает - kCVPixelBufferMetalCompatibilityKey: true в pixelBufferAttributes при создании AVPlayerItemVideoOutput: без этого флага CVMetalTextureCacheCreateTextureFromImage может вернуть nil без явной ошибки, и дебажить такое - отдельное удовольствие.
Верхнеуровнево это вся техника - никакой магии. Точнее она в общей композиции всех частей вместе.@contravariance
#metal #ui #cg #rendering #gpu
Multipass Rendering. Ping-Pong Render. Vol I.
Это серия из минимум двух частей про advanced computer graphics & GPU programming. Я вам покажу в этом и следующем посте две принципиально разные симуляции, но их обьеденяет один концептуально идентичный рендер; рендер построен вокруг идеи некоторой feedback текстуры - буффера, которые можно чейнить для композитинга более сложных фрагментных и вертексных стейджей GPU пайплайна.
Идея feedback texture заключается в рендер таргете, который становится инпутом для следующего фрейма. Концептуально это некоторая форма динамического программирования на GPU. Ну и кто сказал, что алгосы (DSA) не пригодятся :D В компьютерной графике это feedback loop - паттерн рендеринга, где output каждого фрейма зависит от state предыдущего фрейма; вычисленный результат идет обратно на вход следующей итерации, создавая непрерывную эволюцию данных во времени.Две текстуры A и B, свапаются каждый фрейм. Фрейм N рендерин в A, в то время пока читает из B - инпут последнего фрейма. Фрем N+1 рендерит в B пока читает А. GPU никогда не читает и не пишет в одну текстуру в одном пасе - это нелегальная операция в Metal - explicit validation error (для compute kernels через read_write текстуры - другая история, там UB реальна). От сюда и название - ping-pong - или два буфера симулируют одну персистентную текстуру, которая мутирует в каждым кадром.
Ping-pong как форма multipass - технически это частный случай multipass rendering: несколько проходов, каждый со своим render target. Но не каждый multipass - ping-pong. Ping-pong это multipass с temporal feedback: специфика именно в том, что output одного фрейма становится input следующего, а не просто "несколько проходов в одном фрейме".Это знание открывает принципиально новый мир лоу-лвл GPU дизайна. Round-trip между CPU и GPU даже на гетерогенной архитектуре это недешевое удовольствие. GPU мейнтейнит свою область памяти, где весь стейт относительно paint trails, decaying scratches, relaxing cloth anchors etc живет в двух преаллоцированных фрейм-буферах, без CPU-related фидбека стейта на каждый кадр (storageMode: .private). CPU передаёт только малый uniform buffer (touch up/down, drag, frame, velocity, parameters) - а весь объемный state (текстуры) остаётся резидентным в GPU между кадрами - CPU вообще не видит и не трогает байты текстур. На самом GPU мы можешь отдельно компьютить эволюцию состояния симуляции (текстура A), а в отдельной шейдере рендерить результат стейта (текстура B). Архитектурно перенося хранение, обновление состояния аля view model, но во фреймбуфере и не покидая GPU, и отображая результат аля presentation/view слой, но уже в соседнем фреймбуфере и опять же не покидая GPU. В такой модели мы не только держим данные, о состоянии и рендеринге максимально близко - максимально нивелируя многие проблемы data locality между update/render стадиями - но мы так же переиспользуем богатую GPU инфраструктуру в виде семплеров, блендинг фунций и RGBA текселей. Трейдоффом выступает ограничениями точности текстурного формата - GPU и Metal не всесильны тут. Ping-ponging так же подвержена ошибкам в порядке swap’инга буферов, которая выглядит как race condition, но детерминирована - в рамках одного MTLCommandBuffer с правильным swap race condition невозможна (не memory hazard) - Metal гарантирует порядок энкодинга команд в буфере. Лейаут unimorf'ов нужно синхронизировать между GPU и CPU, не самые очевидные способы unit тестирования. Хотя, вопреки распространненному заблуждению, шейдеры юнит тестятся и весьма успешно (offline Metal compute dispatch, или просто golden-image diffing).
Ping-pong - это swap указателей, а не копирование данных. Реализация через read/write/swap() - swap-метод просто обменивает ссылки между двумя буфера без копирования. Самой свап-операции тоже нет физического memmove, только переключение указателей буферов.• Stateful Rendering with the Ping-Pong Technique • MTLStorageMode.private • Setting resource storage modes @contravariance
#wwdc26 #swiftui #typechecker #swift
@ContentBuilder с бекпортом до iOS 13.0+ | iPadOS 13.0+ | Mac Catalyst 13.0+ | macOS 10.15+ | tvOS 13.0+ | visionOS 1.0+ | watchOS 6.0+
ContentBuilder = ViewBuilder алиас прозрачен для constraint solver'а, судя по всему это не про subtyping trick. Магия в conditional conformance на результирующих типах (TupleContent и подобных), плюс сдвиг от overload resolution на constraint стороне buildBlock к conformance lookup на стороне результирующего типа. Именно поэтому проблема комбинаторного взрыва операторов сюда не переносится.
Вместе с @ContentBuilder вводятся ряд других, конкретных билдеров (они в основом без бекпорта):
• CompositorContentBuilder
• KeyframeTrackContentBuilder
• ToolbarContentBuilder
• TabContentBuilder
• ImmersiveSpaceContentBuilder
...
Тред Славы Пестова про constraint solver это пока то, что в своей самой зачаточной форме попадет в Swift 6.4, в отдельных патчах. В Swift 6.3 вошли оптимизации disjunction selection и constraint solver arena usage. Более продвинутые и существенно менее тривиальные изменения constraint solver'a такие как clause learning, Boolean formula satisfiability и non-chronological backtracking.В остальном, все что было на даб-дабе про type ambiguity resolution это в большей степени про архитектуру SwiftUI, его дизайн токены и отдельные изменения. • How does compiler compile SwiftUI code? • What is wrong with type inference? • Replacing the Type Checker • swift/docs/TypeChecker.md • A Type System From Scratch – Robert Widmann @contravariance
#wwdc26 #uikit #apidiff
Тем временем новые UIKit API (ObjC API Diff: iOS 26.2 → iOS 27 (beta))
@contravariance
#swift #concurrency #compiler
Swift 6.4 cancellation shields: от семантики до самых недр компилятора, с полным трейсом всех изменений с фронтенда до билтинов.
• Что такое cancellation shields?
• Некоторые детали того, как работал флаг кооперативной отмены задач и как он теперь работает?
• Какие задачи решает cancellation shields?
• Возможно сподвигнет чуток глубже в cooperative cancellation model погрузиться - Swift Concurrency уже без малого +5 лет - имхо пора давно осваивать.
• Материал не для новичков, но может мотивировать размотать весь клубок связных концепций.
• Swift 6.4: Concurrency. Demystifying Cancellation Shields. Vol I.
• Swift 6.4: Concurrency. Demystifying Cancellation Shields. Vol II.
• Swift 6.4: Concurrency. Demystifying Cancellation Shields. Vol III.
В одной из следующих серий: Task Tree и подкапотные механизмы Swift Concurrency. Разберем, как на самом деле пропагируются контекст задач, изоляция и TaskLocal значения. Поговорим о том, почему в Swift 6.4 компилятор изменил низкоуровневую реализацию intrusive tree design и почему интрузивный дизайн - это один из самых эффективных способов построения рекурсивных моделей данных без лишних аллокаций с учетом cache locality.
А пока - happy dub-dub! 🥳
@contravariance
#swift #concurrency #compiler
Swift 6.4: Concurrency. Demystifying Cancellation Shields. Vol III.
IRGen понижает (via lowering phase) эти Builtins не в LLVM intrinsic, а в runtime calls:
case BuiltinValueKind::TaskCancellationShieldPush:
out.add(emitBuiltinTaskCancellationShieldPush(IGF));
return;
case BuiltinValueKind::TaskCancellationShieldPop:
return emitBuiltinTaskCancellationShieldPop(IGF);
А сами emitters вызывают runtime function pointers:
llvm::Value *irgen::emitBuiltinTaskCancellationShieldPush(IRGenFunction &IGF) {
auto *call =
IGF.Builder.CreateCall(IGF.IGM.getTaskCancellationShieldPushFunctionPointer(), {});
call->setDoesNotThrow();
call->setCallingConv(IGF.IGM.SwiftCC);
return call;
}
void irgen::emitBuiltinTaskCancellationShieldPop(IRGenFunction &IGF) {
auto *call =
IGF.Builder.CreateCall(IGF.IGM.getTaskCancellationShieldPopFunctionPointer(), {});
call->setDoesNotThrow();
call->setCallingConv(IGF.IGM.SwiftCC);
}
Runtime symbols:
swift_task_cancellationShieldPush
swift_task_cancellationShieldPop
swift_task_hasActiveCancellationShield
И уже в concurrency runtime это приземляется в AsyncTask::cancellationShieldPush() / AsyncTask::cancellationShieldPop(), где в ActiveTaskStatus ставится или снимается флаг:
HasActiveTaskCancellationShield = 0x10000
Фактически shield state живет в task status flags, а isCancelled теперь умеет учитывать этот флаг:
bool isCancelled(bool ignoreShield = false) const {
return (Flags & IsCancelled) &&
(ignoreShield || !(Flags & HasActiveTaskCancellationShield));
}
• /swiftlang/swift-evolution/blob/main/proposals/0504-task-cancellation-shields.md
• /swiftlang/swift/blob/release/6.3/stdlib/public/Concurrency/TaskCancellation.swift#L126-L130
• /swiftlang/swift/blob/release/6.4.x/stdlib/public/Concurrency/TaskCancellation.swift#L200-L258
• /swiftlang/swift/blob/release/6.4.x/stdlib/public/Concurrency/TaskCancellation.swift#L376-L386
• /swiftlang/swift/blob/release/6.4.x/stdlib/public/Concurrency/TaskCancellation.swift#L470-L480
• /swiftlang/swift/blob/release/6.4.x/stdlib/public/Concurrency/TaskCancellation.swift#L488-L517
• /swiftlang/swift/blob/release/6.4.x/include/swift/Basic/Features.def#L281
• /swiftlang/swift/blob/release/6.4.x/include/swift/AST/Builtins.def#L1265-L1273
• /swiftlang/swift/blob/release/6.4.x/lib/AST/Builtins.cpp#L2484-L2490
• /swiftlang/swift/blob/release/6.4.x/lib/IRGen/GenBuiltin.cpp#L1602-L1606
• /swiftlang/swift/blob/release/6.4.x/lib/IRGen/GenConcurrency.cpp#L371-L383
• /swiftlang/swift/blob/release/6.4.x/include/swift/Runtime/RuntimeFunctions.def#L2745-L2768
• /swiftlang/swift/blob/release/6.4.x/include/swift/Runtime/Concurrency.h#L607-L625
• /swiftlang/swift/blob/release/6.4.x/include/swift/Runtime/Concurrency.h#L744-L759
• /swiftlang/swift/blob/release/6.4.x/stdlib/public/Concurrency/Task.cpp#L1761-L1769
• /swiftlang/swift/blob/release/6.4.x/stdlib/public/Concurrency/Task.cpp#L1853-L1874
• /swiftlang/swift/blob/release/6.4.x/stdlib/public/Concurrency/TaskPrivate.h#L489-L566
• /swiftlang/swift/blob/release/6.4.x/stdlib/public/Concurrency/TaskPrivate.h#L1343-L1375
• /swiftlang/swift/blob/release/6.4.x/stdlib/public/Concurrency/TaskStatus.cpp#L858-L866
• /swiftlang/swift/blob/release/6.4.x/test/SILGen/builtins.swift#L1012-L1023
@contravariance#swift #concurrency #compiler
Swift 6.4: Concurrency. Demystifying Cancellation Shields. Vol II.
Важно помнить простую эвристику: если нужен actual cancellation state конкретного task handle, смотрим через taskHandle.isCancelled или UnsafeCurrentTask.isCancelled. Если нужен кооперативный флаг отмены текущего execution context, с учетом shielded lexical scope, смотрим через Task.isCancelled.
• Получить handle на текущую задачу можно через withUnsafeCurrentTask { }.
• Получить статус наличия cancellation shield у текущей задачи в текущем лексическом scope можно через Task.hasActiveCancellationShield.
• Для unsafe handle есть аналогичный UnsafeCurrentTask.hasActiveCancellationShield.
@available(SwiftStdlib 6.4, *)
extension Task where Success == Never, Failure == Never {
@available(SwiftStdlib 6.4, *)
@_alwaysEmitIntoClient
public static var hasActiveCancellationShield: Bool {
@_alwaysEmitIntoClient
get {
unsafe withUnsafeCurrentTask { task in
unsafe task?.hasActiveCancellationShield ?? false
}
}
}
}
На компиляторном уровне это выглядит так: $BuiltinTaskCancellationShield - это feature gate, а сами примитивы лежат как Swift Builtins: Builtin.taskCancellationShieldPush() и Builtin.taskCancellationShieldPop().
Они объявлены в compiler builtin table:
BUILTIN_MISC_OPERATION(TaskCancellationShieldPush, "taskCancellationShieldPush", "", Special)
BUILTIN_MISC_OPERATION(TaskCancellationShieldPop, "taskCancellationShieldPop", "", Special)
Дальше lib/AST/Builtins.cpp задает им сигнатуры:
static ValueDecl *getTaskCancellationShieldPush(ASTContext &ctx, Identifier id) {
return getBuiltinFunction(ctx, id, _thin, _parameters(), _int(1));
}
static ValueDecl *getTaskCancellationShieldPop(ASTContext &ctx, Identifier id) {
return getBuiltinFunction(ctx, id, _thin, _parameters(), _void);
}
push возвращает Builtin.Int1 / Bool, потому что nested shield не устанавливает второй shield. Если shield уже активен, push вернет false, и этот scope не должен делать pop.
В TaskCancellation.swift это видно прямо:
let didInstallShield = Builtin.taskCancellationShieldPush()
defer {
if Bool(didInstallShield) {
Builtin.taskCancellationShieldPop()
}
}
@contravariance#swift #concurrency #compiler
Swift 6.4: Concurrency. Demystifying Cancellation Shields. Vol I.
Пост расчитан на ваше понимание cooperative cancellation concurrency model, умение мануально связывать unstructed tasks и пропагировать cancellation state. Полный трейс всех 19 компиляторных референсов приведен в Vol III - вы можете этот rabbit hole с фронтенда до билтинов пройтись.Concurrency примечательно и точечно реинженирят - помимо очевидных и видимых изменений, в 6.4 уже вошли многие менее очевидные. Например, task tree изменил свою модель с intrusive single linked list на intrusive doubly linked list, но об этом отдельно. Cancellation propagation в кооперативной модели конкурентности Swift Concurrency претерпел изменения. Если раньше инвариант cancellation был терминальным и монотонным, то есть если флаг был флипнут, то анфлипнуть его снова уже не было возможности, он мог быть установлен в состояние отмены (cancellation state == true) ровно один раз.
@available(SwiftStdlib 5.1, *)
extension Task {
@_transparent public var isCancelled: Bool {
_taskIsCancelled(_task)
}
}
@available(SwiftStdlib 5.1, *)
extension Task where Success == Never, Failure == Never {
public static var isCancelled: Bool {
unsafe withUnsafeCurrentTask { task in
unsafe task?.isCancelled ?? false
}
}
}
Мы можем это состояние проверить как через instance task handle, так и через тип Task, e.g. task.isCancelled vs Task.isCancelled. До введения cancellation shields, если говорить именно о текущей задаче, семантической разницы почти не было, были в основном лексические различия.
/// After the value of this property becomes `true`, it remains `true` indefinitely.
/// There is no way to uncancel a task.
Теперь же инвариант isCancelled, через cancellation shield (SE-0504), будет слегка по-разному работать через instance vs type: source of truth vs lexical scope shielding.
@_transparent
public var isCancelled: Bool {
// This is @available(SwiftStdlib 6.4, *) but can't use SwiftStdlib in transparent function
if #available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999, *) {
let ignoreTaskCancellationShield: UInt64 = 0x1
return _taskIsCancelledWithFlags(_task, flags: ignoreTaskCancellationShield)
} else {
return _taskIsCancelled(_task)
}
}
public static var isCancelled: Bool {
unsafe withUnsafeCurrentTask { task in
if #available(SwiftStdlib 6.4, *) {
unsafe task?._isCancelled(ignoreTaskCancellationShield: false) ?? false
} else {
unsafe task?.isCancelled ?? false
}
}
}
То есть shield-aware поведение появляется именно для Task.isCancelled и Task.checkCancellation(). Для taskHandle.isCancelled все остается по-прежнему: instance property не контекстуальна и игнорирует shield.
/// ### Instance property isCancelled ignores Task Cancellation Shields
///
/// The instance property `task.isCancelled`
/// is not contextual and therefore does not respect cancellation shields.
/// If a task was cancelled and is executing with an active cancellation shield,
/// these properties will return the _actual_ cancellation status of the specific task.
Это сделано для возможности изолировать (e.g. шилдить) отдельные лексические скоупы от кооперативной отмены. Изоляция тут плохое слово, потому что isolation имеет совершенно иной семантический и компилятивный смысл. Cancellation shield не “отменяет отмену” (масло маслянное) и не делает task uncancelled. Он только временно запрещает коду внутри скоупа наблюдать cancellation state.
@contravariance