إعادة بناء خط تصيير Lulucat Notes باستخدام Kimi K3
تحوّل خط أنابيب البلاطات في Core Graphics إلى خط أنابيب Metal للطوابع النقطية في ست خطوات صغيرة، جرى التحقق من كل خطوة منها على جهاز iPad حقيقي. كُتب الكود بالبرمجة الثنائية مع Kimi K3 على Fireworks.
في الأسبوع الماضي، أدى سحب تحديد باللاسو عبر Lulucat Notes إلى موجة مرئية: أظهرت بعض بلاطات الشاشة التحديد في موضعه الجديد، بينما ظلت أخرى تعرض الموضع القديم في الإطار نفسه. استبدلنا خط أنابيب التصيير بأكمله — صورة Core Graphics النقطية مع CATiledLayer — بـ Metal، في ست خطوات صغيرة، جرى التحقق من كل واحدة منها على جهاز iPad حقيقي قبل بدء التالية.
كُتب الكود بالبرمجة الثنائية مع Kimi K3، النموذج المفتوح من Moonshot الذي يعمل على Fireworks. قاد الإنسان العمل، واتخذ القرارات، وأجرى الاختبارات؛ وكتب النموذج تقريبًا كل سطر.
خط الأنابيب
كان في خط الأنابيب القديم نوعان من الرسم يبتعدان تدريجيًا عن بعضهما: كانت الضربات تُخبز في صورة نقطية لعرضها بتكلفة منخفضة، وتُعاد رسمها كمتجهات كلما احتاجت البلاطات إلى تفاصيل أكثر. أما خط الأنابيب الجديد فله فكرة واحدة تمامًا: كل ما يشبه الحبر هو طابع نقطي. فضربة القلم، ومسحة التمييز، ولمسة الممحاة كلها رأس واحد حجمه 32 بايت — موضع، وقطر، ولون — يرسمه زوج المظللات نفسه على هيئة دوائر تُرسترها وحدة معالجة الرسوميات، متباعدة بمقدار نقطة واحدة على طول قوس الضربة.
![]()
عند تباعد نقطة واحدة، تنحرف سلسلة الدوائر عن كبسولة مثالية رياضيًا بنحو 0.075 نقطة — أي خُمس بكسل عند كثافة لوحة الرسم لدينا. وفي المقابل، تختزل الأدوات الثلاث إلى مسار واحد في الكود، وتفعل وحدة معالجة الرسوميات ما تجيده.
حول هذه الفكرة، تصبح البنية بسيطة. يعيش الحبر المثبّت في نسيج واحد بحجم 4096². وتبقى UIScrollView موجودة، لكن بعد تخفيض دورها إلى محرك إيماءات خالص: يغذي contentOffset وzoomScale متغير إطار العرض في كل إطار، ولذلك لا يكتب التحريك والتكبير شيئًا. يتكون كل إطار من خمسة رسوم:
flowchart TB
subgraph frame["Every frame: five draws"]
direction TB
paper["1 · paper blit"] --> ink["2 · committed ink"]
ink --> live["3 · live stroke"]
live --> sel["4 · selection"]
sel --> dash["5 · lasso dashes"]
end
commit["stroke commit<br/>append stamps"] --> tex[("ink texture<br/>4096² render target")]
replay["regional replay<br/>erase · delete · move · undo"] --> tex
tex -. "sampled or re-drawn" .-> ink
تكتب التعديلات في النسيج مباشرةً. ويضيف تثبيت ضربة طوابعها. أما المحو والحذف والنقل والتراجع، فتعيد تشغيل المنطقة المتأثرة خلف مستطيل قص: تمسح المنطقة، ثم تعيد رسم الضربات التي تتقاطع معها، وانتهى الأمر. ويحافظ المحو الجزئي على دلالات الملكية الواردة في المقالة السابقة — فالمحو يتبع الضربة التي يزيل حبرها — وذلك برسم كل ضربة ممحاة في نسيج مؤقت، وطرح مسارات محوها الخاصة باستخدام مزج destination-out (
والتحديد الذي بدأ كل هذا يُرسم الآن بالطوابع النقطية أيضًا. يؤدي سحبه إلى تحديث إزاحة موحدة واحدة. لا كتابات في النسيج، ولا إبطال للبلاطات — لقد اختفت الموجة بنيويًا، لا بمجرد تخفيف أثرها.
الحارس: مقارنة بكسلية
لم نحذف مصيّر Core Graphics. بل خفّضنا دوره إلى تطبيق مرجعي غير متصل، ويجب على كل تغيير في Metal أن يمر بمقارنة بكسلات معه، باستخدام بيانات ضربات حقيقية التُقطت على الجهاز. وليس معيار القبول هو «بكسلات متطابقة» — فمن الطبيعي أن يختلف مصيّران صحيحان ببضع درجات رمادية على امتداد الحواف المضادة للتعرّج. أما بوابة القبول فهي بنيوية: لا حبر مفقود، ولا إزاحة، ولا انحراف في اللون، ولا فرق كبير في أي موضع بعيد عن الحبر.
![]()
اكتشف هذا الاختبار أربعًا من الأخطاء الخمسة التي واجهتنا أثناء بناء المصيّر غير المتصل، وقد شُخّصت كلها باقتصاص الناتج إلى مستوى البكسل: عدم تطابق خطوة بنية Swift/Metal (28 بايتًا مقابل 32، لأن Metal يحاذي float4 إلى 16 — فامتلأت الشاشة بكتل لونية)، وعدم إمكانية قراءة [[point_size]] كـ varying في مظلل الأجزاء (فخرج كل طابع مربعًا)، ووجود مشفّري رسم على مخزن أوامر واحد في الوقت نفسه (فأصبح كل شيء أسود)، وفقدان لمسة البداية، مما جعل المليمتر الأول من الضربات السريعة غير مرئي.
ست خطوات، لا إعادة كتابة واحدة
كانت خطة الترحيل ست خطوات يمكن شحن كل منها على حدة: مصيّر غير متصل يمر بفرق البكسلات؛ غلاف عرض بلا أي تغيير بصري؛ ضربة حية على GPU؛ تحديد على GPU؛ تعديلات تكتب في النسيج مباشرةً؛ وإعادة رسم متجهية عند التكبير العالي (أُدرجت في الخطوة الثانية، لأن «صفر تغيير بصري» كان يتطلب ذلك). انتهت كل خطوة بإنسان يكتب ويمحو ويكبّر ويسحب على iPad Pro الموجود على المكتب — لا بمحاكي ولا بفرق لقطات شاشة.
التقط الجهاز ثلاثة أخطاء أغفلتها كل الفحوص الآلية. عند تكبير يتجاوز 100%، كانت الضربات تُرسم مرتين — نسيج ناعم في الأسفل، وطوابع حادة فوقه — فبدت كضباب خفيف لاحظه الإنسان خلال ثوانٍ. وكان تثبيت ضربة يومض لإطار واحد، لأن التراكب القديم كان يتلاشى تدريجيًا خارج التزامن مع تحديث النسيج. وفوق تكبير 170%، اختفت كل الملاحظات: كان مستطيل قصّ إخفاء الرؤية يستخدم contentOffset في فضاء الإحداثيات المقيّس، ولذلك كان يبتعد عن الضربات كلما كبّرت. كانت الإصلاحات الثلاثة كلها عبارة عن تغييرات من سطر واحد في دالة واحدة، ولم يكن أي منها موجودًا في اختبار كان يمكننا كتابته مسبقًا، لأننا لم نكن نعرف ما الذي ينبغي البحث عنه. في تطبيق استهلاكي تكون واجهته أولًا، لهذا يبقى الإنسان داخل الحلقة.
كيف تبدو تجربة العمل مع Kimi K3
سريع، قبل كل شيء. كانت حلقة «ناقش، اكتب، ابنِ، ثبّت، انظر» تستغرق دقائق، والنموذج الذي يجيب بسرعة يغيّر عدد الحلقات التي يمكنك تحمّلها في اليوم.
ثانيًا، لا يفرط في الهندسة. تعمل قاعدة الكود هذه وفق قواعد صريحة خاصة بنا — لا بنية للتوافق العكسي قبل الإطلاق، ولا تعقيد إلا عندما يثبت الجهاز ضرورته — ويتبعها K3 من دون تذكير. لم يضف فهارس مكانية «لوقت لاحق»، ولم يلف كل استدعاء في فحوص دفاعية، ولم يجرّد الأشياء تجريدًا استباقيًا. ويشبه توجيه التعليمات إليه العملَ مع زميل كفء قرأ قواعد العمل فعلًا ويؤمن بها.
ثالثًا، أعطه أدوات وسيستخدمها بحماس. وصلنا أدوات صور — للعرض، والاقتصاص إلى منطقة بكسلات، وتغيير الحجم — وبدأ النموذج يقتص ناتج مصيّره بنفسه على نحو استباقي لتشخيص أخطاء الاختبار الخمسة المذكورة أعلاه. وقد ذكّرته الأداة بأن ينظر.
أما النصف الآخر من الصورة فهو أن K3 كتب معظم الأخطاء الواردة في هذه القصة، بما فيها خطأ فضاء الإحداثيات الذي جعل الملاحظات تختفي. حدوده حقيقية. وما جعل العمل آمنًا لم يكن صواب النموذج دائمًا؛ بل كان الاختبار يلتقط انحراف التصيير، والإنسان يلتقط الإحساس. ومع ذلك، لم أكن أستطيع يومًا بعد يوم التمييز بينه وبين النماذج المغلقة الرائدة التي نستخدمها أيضًا — أنظمة من فئة Opus. وكان أفضل بوضوح على بعض المحاور: أسرع، وأقل ميلًا بكثير إلى حشو قاعدة الكود بتصميم دفاعي.
كيف نريد أن نبني من الآن فصاعدًا
انتهينا من التطوير الوكيلي ذي المواصفات الكبيرة — الأسلوب الذي تسلّم فيه النموذج مواصفة كبيرة وتقبل كل ما يخرج منها. وضع الفشل ليس كودًا سيئًا؛ بل كود لا يفهمه أحد.
ما نجح هنا، وما سنحافظ عليه: خطوات صغيرة، تُناقش كل واحدة منها قبل أن تبدأ، ويفهمها الإنسان قبل بنائها، ويُتحقق منها على الجهاز الذي ستعيش عليه. وظيفة النموذج أن يكون سريعًا ودقيقًا وصريحًا بشأن عدم اليقين. ووظيفة الإنسان هي الحكم والذوق والتحقق الشامل من البداية إلى النهاية — خصوصًا في البرمجيات الاستهلاكية التي تكون واجهتها أولًا، حيث لا تستطيع المواصفة وصف ما يعنيه أن يكون الشيء «صحيحًا» من حيث الإحساس. واتضح أن Kimi K3 على Fireworks مناسب تمامًا لهذه الحلقة: سريع بما يكفي لإبقاء الحلقة ضيقة، وذكي بما يكفي لإبقاء الخطوات صغيرة ونظيفة.
اختفت الموجة، وأصبح خط الأنابيب فكرة واحدة بدلًا من فكرتين، أما العملية التي أوصلتنا إلى هنا فستبقى.