Kimi K3 ସହ Lulucat Notesର rendering pipelineକୁ ପୁନଃନିର୍ମାଣ କରିବା
ଆମ Core Graphics tile pipeline ଛଅଟି ଛୋଟ ପଦକ୍ଷେପରେ Metal point-sprite pipelineରେ ପରିଣତ ହେଲା; ପ୍ରତ୍ୟେକ ପଦକ୍ଷେପକୁ ଏକ ବାସ୍ତବ iPadରେ ଯାଞ୍ଚ କରାଗଲା। Fireworksରେ Kimi K3 ସହ pair-programming କରି କୋଡ୍ ଲେଖାଗଲା।
ଗତ ସପ୍ତାହେ, Lulucat Notesରେ lasso selectionକୁ ଟାଣିଲେ ଏକ ସ୍ପଷ୍ଟ ତରଙ୍ଗ ଦେଖାଯାଉଥିଲା: ଏକେ frameରେ କିଛି screen tile selectionକୁ ନୂଆ ସ୍ଥାନରେ ଦେଖାଉଥିଲେ, ଅନ୍ୟମାନେ ଏଯାବତ୍ ପୁରୁଣା ସ୍ଥାନରେ ଦେଖାଉଥିଲେ। ଆମେ ସମଗ୍ର rendering pipeline — Core Graphics bitmap plus CATiledLayer —କୁ Metalରେ ବଦଳାଇଲୁ; ଛଅଟି ଛୋଟ ପଦକ୍ଷେପରେ, ପରବର୍ତ୍ତୀ ପଦକ୍ଷେପ ପୂର୍ବରୁ ପ୍ରତ୍ୟେକଟିକୁ ଏକ ବାସ୍ତବ iPadରେ ଯାଞ୍ଚ କରି।
ଲେଖକତ୍ୱ ବିଷୟରେ ଆଉ ଗୋଟିଏ କଥା, ଯାହା ଏହି ପୋଷ୍ଟର ଦ୍ୱିତୀୟାର୍ଧ ପାଇଁ ଗୁରୁତ୍ୱପୂର୍ଣ୍ଣ: କୋଡ୍ Fireworksରେ ଚାଲୁଥିବା Moonshotର open model Kimi K3 ସହ pair-programming କରି ଲେଖାଗଲା। ମଣିଷ ଦିଗ ଦେଲା, ନିଷ୍ପତ୍ତି ନେଲା ଓ ପରୀକ୍ଷା କଲା; model ପ୍ରାୟ ପ୍ରତ୍ୟେକ line ଲେଖିଲା।
ପାଇପ୍ଲାଇନ୍
ପୁରୁଣା pipelineରେ ଦୁଇ ପ୍ରକାରର drawing ଥିଲା, ଯେଉଁମାନେ ଧୀରେ ଧୀରେ ଅଲଗା ହୋଇଯାଉଥିଲେ: ସସ୍ତା display ପାଇଁ strokeଗୁଡ଼ିକୁ bitmapରେ bake କରାଯାଉଥିଲା, ଏବଂ tileଗୁଡ଼ିକୁ ଅଧିକ detail ଦରକାର ହେଲେ vector ଭାବରେ ପୁଣି ଅଙ୍କାଯାଉଥିଲା। ନୂଆ pipelineର ଠିକ୍ ଗୋଟିଏ ଧାରଣା ଅଛି: ink ଭଳି ଦେଖାଯାଉଥିବା ପ୍ରତ୍ୟେକ ଜିନିଷ ଗୋଟିଏ point sprite। ଗୋଟିଏ pen stroke, ଗୋଟିଏ highlighter sweep ଏବଂ ଗୋଟିଏ eraser dab ସମାନ 32-byte vertex — position, diameter, color — ଯାହା ସେହି shader pair ଦ୍ୱାରା strokeର arc length ଉପରେ ଗୋଟିଏ point ବ୍ୟବଧାନରେ ସଜାଯାଇଥିବା GPU-rasterized circle ଭାବେ ଅଙ୍କାଯାଏ।
![]()
ଗୋଟିଏ point ବ୍ୟବଧାନରେ circleଗୁଡ଼ିକର ଏହି ଶୃଙ୍ଖଳ mathematically perfect capsuleଠାରୁ ପ୍ରାୟ 0.075 points ବିଚଳିତ ହୁଏ — ଆମ canvas densityରେ ଗୋଟିଏ pixelର ପାଞ୍ଚମ ଭାଗ। ବଦଳରେ, ତିନୋଟି tool ଗୋଟିଏ code pathରେ ମିଶିଯାଏ, ଏବଂ GPU ସେହି କାମ କରେ ଯେଉଁଥିରେ ସେ ସବୁଠାରୁ ଭଲ।
ସେହି ଧାରଣାକୁ ଘିରି architecture ସରଳ। Committed ink ଗୋଟିଏ 4096² textureରେ ରହେ। UIScrollView ରହିଛି, କିନ୍ତୁ କେବଳ ଗୋଟିଏ gesture engine ଭାବେ: ତାହାର contentOffset ଏବଂ zoomScale ପ୍ରତ୍ୟେକ frameରେ ଗୋଟିଏ viewport uniformକୁ ଦିଆଯାଏ, ତେଣୁ panning ଏବଂ zooming କିଛି ଲେଖେ ନାହିଁ। ପ୍ରତ୍ୟେକ frameରେ ପାଞ୍ଚଟି draw ହୁଏ:
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
Edits ସିଧାସଳଖ textureକୁ ଲେଖେ। ଗୋଟିଏ stroke commit କଲେ ତାହାର stampଗୁଡ଼ିକ append ହୁଏ। Erasing, deleting, moving ଏବଂ undoing ଗୋଟିଏ scissor rectangle ପଛରେ ପ୍ରଭାବିତ regionକୁ replay କରେ: regionକୁ clear କର, ତାହାକୁ intersect କରୁଥିବା strokeଗୁଡ଼ିକୁ ପୁଣି ଅଙ୍କ, ସରିଲା। Partial erasure ପୂର୍ବ ପୋଷ୍ଟର ownership semanticsକୁ ରଖେ — ଗୋଟିଏ erasure ସେହି strokeର ଯାହାରୁ ଏହା ink ହଟାଏ — ପ୍ରତ୍ୟେକ erased strokeକୁ ଗୋଟିଏ scratch textureରେ ଅଙ୍କି, destination-out blending (
ଏହି ସବୁର ଆରମ୍ଭ କରିଥିବା selectionକୁ ଏବେ point sprites ଭାବରେ ମଧ୍ୟ ଅଙ୍କାଯାଏ। ତାହାକୁ drag କଲେ ଗୋଟିଏ uniform offset update ହୁଏ। ଶୂନ୍ୟ texture write, ଶୂନ୍ୟ tile invalidation — ତରଙ୍ଗଟି structurally ଚାଲିଗଲା, କେବଳ କମି ନାହିଁ।
ରକ୍ଷକ: ଗୋଟିଏ pixel diff
ଆମେ Core Graphics rendererକୁ delete କରିନାହୁଁ। ତାହାକୁ offline reference implementation ଭାବେ ରଖିଛୁ, ଏବଂ ପ୍ରତ୍ୟେକ Metal ପରିବର୍ତ୍ତନକୁ deviceରେ capture କରାଯାଇଥିବା ବାସ୍ତବ stroke data ଉପରେ ତାହା ସହ pixel comparison ପାସ୍ କରିବାକୁ ପଡ଼େ। Acceptance criterion “identical pixels” ନୁହେଁ — anti-aliased edge ଧାରରେ ଦୁଇଟି ସଠିକ୍ rasterizer କିଛି gray levelରେ ଯଥାର୍ଥ ଭାବେ ଅଲଗା ହୋଇପାରେ। Gateଟି structural: କୌଣସି missing ink ନାହିଁ, କୌଣସି offset ନାହିଁ, କୌଣସି color drift ନାହିଁ, ଏବଂ inkଠାରୁ ଦୂରେ କେଉଁଠି ମଧ୍ୟ ବଡ଼ ପାର୍ଥକ୍ୟ ନାହିଁ।
![]()
ଏହି harness offline renderer ତିଆରି କରିବା ସମୟରେ ଆମେ ଭେଟିଥିବା ପାଞ୍ଚଟି bug ମଧ୍ୟରୁ ଚାରୋଟିକୁ ଧରିଲା; ସବୁକୁ outputକୁ pixel level ପର୍ଯ୍ୟନ୍ତ crop କରି ନିର୍ଣ୍ଣୟ କରାଗଲା: Swift/Metal struct stride mismatch (28 bytes ବନାମ 32, କାରଣ Metal float4କୁ 16ରେ align କରେ — screen ରଙ୍ଗୀନ blockରେ ଭରିଗଲା), [[point_size]]କୁ fragment shaderରେ varying ଭାବରେ ପଢ଼ିହେଉ ନଥିଲା (ପ୍ରତ୍ୟେକ stamp square ହୋଇଯାଉଥିଲା), ଗୋଟିଏ command bufferରେ ଦୁଇଟି render encoder ସହଅବସ୍ଥାନ କରୁଥିଲା (ସବୁ କଳା), ଏବଂ ଗୋଟିଏ missing start dab ଯାହା ଦ୍ରୁତ strokeର ପ୍ରଥମ ମିଲିମିଟରକୁ ଅଦୃଶ୍ୟ ରଖିଥିଲା।
ଛଅଟି ପଦକ୍ଷେପ, ଗୋଟିଏ rewrite ନୁହେଁ
Migration plan ଛଅଟି independently shippable ପଦକ୍ଷେପର ଥିଲା: pixel diff ପାସ୍ କରୁଥିବା offline renderer; ଶୂନ୍ୟ visual ପରିବର୍ତ୍ତନ ସହ display shell; GPUରେ live stroke; GPUରେ selection; ସିଧା textureକୁ ଲେଖୁଥିବା mutations; high-zoom vector redraw (ଦ୍ୱିତୀୟ ପଦକ୍ଷେପରେ ମିଶାଯାଇଥିଲା, କାରଣ “zero visual change” ଏହାକୁ ଦାବି କରୁଥିଲା)। ପ୍ରତ୍ୟେକ ପଦକ୍ଷେପର ଶେଷରେ desk ଉପରେ ଥିବା iPad Proରେ ଜଣେ ମଣିଷ — simulator ନୁହେଁ, screenshot diff ନୁହେଁ — ଲେଖୁଥିଲେ, ମୋଛୁଥିଲେ, zoom କରୁଥିଲେ ଏବଂ drag କରୁଥିଲେ।
ପ୍ରତ୍ୟେକ automated check ଯାହାକୁ ଧରିପାରିନଥିଲା, deviceଟି ସେହି ତିନୋଟି bugକୁ ଧରିଲା। 100% zoomଠାରୁ ଅଧିକରେ stroke ଦୁଇଥର ଅଙ୍କାଯାଉଥିଲା — ତଳେ soft texture, ଉପରେ sharp sprite — ଯାହା ହାଲୁକା blur ଭଳି ଦେଖାଯାଉଥିଲା ଏବଂ ମଣିଷ ତାହାକୁ କେତେକ ସେକେଣ୍ଡରେ ଧରିପାରୁଥିଲା। ଗୋଟିଏ stroke commit କଲେ ଗୋଟିଏ frame ପାଇଁ flicker ହେଉଥିଲା, କାରଣ ପୁରୁଣା overlay texture update ସହ ସମୟ ମେଳ ନଥିବାଭଳି cross-fade ହେଉଥିଲା। ଏବଂ 170% zoomଠାରୁ ଅଧିକରେ ପ୍ରତ୍ୟେକ note ଅଦୃଶ୍ୟ ହୋଇଗଲା: visibility-culling rectangle ନିଜର scaled coordinate spaceରେ contentOffset ବ୍ୟବହାର କରୁଥିଲା, ତେଣୁ zoom କରିବା ସମୟରେ ଏହା strokeଠାରୁ ଦୂରକୁ ସରିଯାଉଥିଲା। ଏହି ତିନୋଟି one-line-to-one-function fix ଥିଲା, ଏବଂ ଆମେ ପୂର୍ବରୁ ଲେଖିପାରୁଥିବା କୌଣସି testରେ ସେମାନଙ୍କ ମଧ୍ୟରୁ କିଛି ଥିଲା ନାହିଁ, କାରଣ କ’ଣ ଖୋଜିବାକୁ ହେବ ତାହା ଆମେ ଜାଣିନଥିଲୁ। UI-first consumer app ପାଇଁ ମଣିଷକୁ loopରେ ରଖିବାର କାରଣ ଏହା: “feel” କିପରି ଭାଙ୍ଗେ, ତାହାର ସମସ୍ତ ଉପାୟ ତାଲିକା କରିହେବ ନାହିଁ।
Kimi K3 ସହ କାମ କରିବା କିପରି
ପ୍ରଥମେ, ଦ୍ରୁତ। “discuss, write, build, install, look” loop କେତେକ ମିନିଟ୍ରେ ଚାଲୁଥିଲା, ଏବଂ ଶୀଘ୍ର ଉତ୍ତର ଦେଉଥିବା model ଗୋଟିଏ ଦିନରେ ଆପଣ କେତେ loop ଚଳାଇପାରିବେ ତାହା ବଦଳାଇଦିଏ।
ଦ୍ୱିତୀୟ, ଏହା over-engineer କରେ ନାହିଁ। ଏହି codebase ସ୍ପଷ୍ଟ house rules ଉପରେ ଚାଲେ — launch ପୂର୍ବରୁ backward-compatibility scaffolding ନାହିଁ, complexity କେବଳ device ଏହା ଆବଶ୍ୟକ ବୋଲି ପ୍ରମାଣ କଲେ — ଏବଂ K3 ମନେ ପକାଇବା ଛାଡ଼ି ସେଗୁଡ଼ିକୁ ମାନେ। ଏହା “ପରେ ପାଇଁ” spatial index ଯୋଡ଼ିଲା ନାହିଁ, ପ୍ରତ୍ୟେକ callକୁ defensive checkରେ ଘେରିଲା ନାହିଁ, speculatively abstract କଲା ନାହିଁ। ଏହାକୁ prompt କରିବା ଏମିତି ଜଣେ ଦକ୍ଷ ସହକର୍ମୀ ସହ କାମ କରିବା ଭଳି, ଯିଏ house rules ପଢ଼ିଛି ଏବଂ ସତରେ ସେଗୁଡ଼ିକୁ ବିଶ୍ୱାସ କରେ।
ତୃତୀୟ, ଏହାକୁ tool ଦିଅନ୍ତୁ, ଏହା ଉତ୍ସାହରେ ବ୍ୟବହାର କରେ। ଆମେ image utility — view, pixel regionକୁ crop, resize — ଯୋଡ଼ିଲୁ, ଏବଂ model ଉପରୋକ୍ତ ପାଞ୍ଚଟି harness bug ନିର୍ଣ୍ଣୟ କରିବା ପାଇଁ ନିଜ renderer outputକୁ proactive ଭାବରେ crop କରିବା ଆରମ୍ଭ କଲା। Tool ଥିବାରୁ ଏହାକୁ ଦେଖିବାକୁ ମନେ ପଡ଼ିଲା।
ଅନ୍ୟ ଅଧା: K3 ଏହି କାହାଣୀର ଅଧିକାଂଶ bug ଲେଖିଥିଲା, ସେହି coordinate-space bug ସହିତ ଯାହା noteଗୁଡ଼ିକୁ ଅଦୃଶ୍ୟ କରିଦେଇଥିଲା। ଏହାର ସୀମା ବାସ୍ତବ। କାମକୁ ସୁରକ୍ଷିତ କରିଥିଲା model ସବୁବେଳେ ଠିକ୍ ହେବା ନୁହେଁ; rendering driftକୁ harness ଧରୁଥିଲା ଏବଂ feelକୁ ମଣିଷ ଧରୁଥିଲା। ତଥାପି, ପ୍ରତିଦିନର କାମରେ, ଆମେ ବ୍ୟବହାର କରୁଥିବା frontier closed model — Opus-class systems — ଠାରୁ ଏହାକୁ ଭରସାଯୋଗ୍ୟ ଭାବେ ଅଲଗା କରିପାରୁନଥିଲି। କିଛି ଦିଗରେ ଏହା ସ୍ପଷ୍ଟ ଭାବେ ଭଲ ଥିଲା: ଅଧିକ ଦ୍ରୁତ, ଏବଂ codebaseକୁ defensive designରେ ଭରିଦେବାର ପ୍ରବୃତ୍ତି ବହୁତ କମ୍।
ଏଠାରୁ ଆମେ କିପରି ନିର୍ମାଣ କରିବାକୁ ଚାହୁଁଛୁ
ଆମେ big-spec agentic development ସହିତ ସମାପ୍ତ — ସେହି ଶୈଳୀ ଯେଉଁଥିରେ modelକୁ ଗୋଟିଏ ବଡ଼ specification ଦେଇ ଯାହା ତିଆରି ହୁଏ ତାହାକୁ ଗ୍ରହଣ କରାଯାଏ। ବିଫଳତାର ରୂପ ଖରାପ code ନୁହେଁ; ଏହା ଏପରି code ଯାହାକୁ କେହି ବୁଝେ ନାହିଁ।
ଏଠାରେ ଯାହା କାମ କଲା, ଏବଂ ଆମେ ଯାହା ରଖିବୁ: ଛୋଟ ପଦକ୍ଷେପ, ପ୍ରତ୍ୟେକଟି ଆରମ୍ଭ ପୂର୍ବରୁ ଆଲୋଚିତ, ତିଆରି ହେବା ପୂର୍ବରୁ ମଣିଷ ଦ୍ୱାରା ବୁଝାଯାଇଥିବା, ଏବଂ ଯେଉଁ deviceରେ ଏହା ରହିବ ସେହି deviceରେ ଯାଞ୍ଚିତ। Modelର କାମ ଦ୍ରୁତ, ସଠିକ୍ ଏବଂ ଅନିଶ୍ଚିତତା ବିଷୟରେ ସତ୍ୟବାଦୀ ହେବା। ମଣିଷର କାମ judgment, taste ଏବଂ e2e verification — ବିଶେଷକରି UI-first consumer software ପାଇଁ, ଯେଉଁଠାରେ specification “ଠିକ୍” କିପରି ଲାଗେ ତାହା କହିପାରେ ନାହିଁ। Fireworksରେ Kimi K3 ଠିକ୍ ଏହି loop ପାଇଁ ଭଲ ଭାବରେ ଗଢ଼ାଯାଇଥିବା ପରି ଲାଗେ: loopକୁ ଟାଇଟ୍ ରଖିବାକୁ ଯଥେଷ୍ଟ ଦ୍ରୁତ, ପଦକ୍ଷେପଗୁଡ଼ିକୁ ଛୋଟ ଓ ସଫା ରଖିବାକୁ ଯଥେଷ୍ଟ ବୁଦ୍ଧିମାନ।
ତରଙ୍ଗଟି ଚାଲିଗଲା, pipeline ଦୁଇଟି ଧାରଣାର ବଦଳରେ ଗୋଟିଏ ଧାରଣା ହେଲା, ଏବଂ ଆମକୁ ସେଠାକୁ ନେଇଥିବା ପ୍ରକ୍ରିୟା ରହିବ।