ソースを参照

app: make UI jankless by making all updates truly atomic └(^o^ )X( ^o^)┘

darkfi 11 ヶ月 前
コミット
c4731e725d

+ 34 - 8
bin/app/script/traxator.py

@@ -136,10 +136,23 @@ class Vertex:
 @dataclass
 class PutDrawCall:
     epoch: int
+    batch_id: int
     timest: int
     dcs: [DrawCall]
     stats: [int]
 
+@dataclass
+class PutStartBatch:
+    epoch: int
+    batch_id: int
+    stat: int
+
+@dataclass
+class PutEndBatch:
+    epoch: int
+    batch_id: int
+    stat: int
+
 @dataclass
 class PutTex:
     epoch: int
@@ -214,6 +227,7 @@ def read_section(f):
     match c:
         case 0:
             epoch = serial.read_u32(cur)
+            batch_id = serial.read_u32(cur)
             timest = serial.read_u64(cur)
             dcs = serial.decode_arr(cur, read_dc)
             stats = []
@@ -221,16 +235,28 @@ def read_section(f):
                 stat = serial.read_u8(cur)
                 stats.append(stat)
                 #print(f"  stat={stat}")
-            #print(f"put_dcs epoch={epoch}, timest={timest}, dcs={dcs}, stats={stats}")
-            sect = PutDrawCall(epoch, timest, dcs, stats)
+            #print(f"put_dcs epoch={epoch}, batch_id={batch_id}, timest={timest}, dcs={dcs}, stats={stats}")
+            sect = PutDrawCall(epoch, batch_id, timest, dcs, stats)
         case 1:
+            epoch = serial.read_u32(cur)
+            batch_id = serial.read_u32(cur)
+            stat = serial.read_u8(cur)
+            #print(f"put_start_batch epoch={epoch}, batch_id={batch_id}, stat={stat}")
+            sect = PutStartBatch(epoch, batch_id, timest, dcs, stats)
+        case 2:
+            epoch = serial.read_u32(cur)
+            batch_id = serial.read_u32(cur)
+            stat = serial.read_u8(cur)
+            #print(f"put_end_batch epoch={epoch}, batch_id={batch_id}, stat={stat}")
+            sect = PutEndBatch(epoch, batch_id, timest, dcs, stats)
+        case 3:
             epoch = serial.read_u32(cur)
             tex = serial.read_u32(cur)
             tag = serial.decode_opt(cur, read_tag)
             stat = serial.read_u8(cur)
             #print(f"put_tex epoch={epoch}, tex={tex}, tag='{tag}', stat={stat}")
             sect = PutTex(epoch, tex, tag, stat)
-        case 2:
+        case 4:
             epoch = serial.read_u32(cur)
             verts = serial.decode_arr(cur, read_vert)
             buf = serial.read_u32(cur)
@@ -239,7 +265,7 @@ def read_section(f):
             stat = serial.read_u8(cur)
             #print(f"put_verts epoch={epoch}, buf={buf}, tag='{tag}', buftype={buftype}, stat={stat}")
             sect = PutVerts(epoch, verts, buf, tag, buftype, stat)
-        case 3:
+        case 5:
             epoch = serial.read_u32(cur)
             idxs = serial.decode_arr(cur, serial.read_u16)
             buf = serial.read_u32(cur)
@@ -248,14 +274,14 @@ def read_section(f):
             stat = serial.read_u8(cur)
             #print(f"put_idxs epoch={epoch}, buf={buf}, tag='{tag}', buftype={buftype}, stat={stat}")
             sect = PutIdxs(epoch, idxs, buf, tag, buftype, stat)
-        case 4:
+        case 6:
             epoch = serial.read_u32(cur)
             buf = serial.read_u32(cur)
             tag = serial.decode_opt(cur, read_tag)
             stat = serial.read_u8(cur)
             #print(f"del_tex epoch={epoch}, buf={buf}, tag='{tag}', stat={stat}")
             sect = DelTex(epoch, buf, tag, stat)
-        case 5:
+        case 7:
             epoch = serial.read_u32(cur)
             buf = serial.read_u32(cur)
             tag = serial.decode_opt(cur, read_tag)
@@ -263,11 +289,11 @@ def read_section(f):
             stat = serial.read_u8(cur)
             #print(f"del_buf epoch={epoch}, buf={buf}, tag='{tag}', buftype={buftype}, stat={stat}")
             sect = DelBuf(epoch, buf, tag, buftype, stat)
-        case 6:
+        case 8:
             dc = serial.read_u64(cur)
             #print(f"set_curr dc={dc}")
             sect = SetCurr(dc)
-        case 7:
+        case 9:
             idx = serial.read_u64(cur)
             #print(f"set_instr idx={idx}")
             sect = SetInstr(idx)

+ 3 - 2
bin/app/src/app/mod.rs

@@ -183,7 +183,7 @@ impl App {
     /// Begins the draw of the tree, and then starts the UI procs.
     pub async fn start(self: Arc<Self>, event_pub: GraphicsEventPublisherPtr, epoch: EpochIndex) {
         d!("Starting app epoch={epoch}");
-        let mut atom = PropertyAtomicGuard::new();
+        let mut atom = PropertyAtomicGuard::none();
 
         let window_node = self.sg_root.clone().lookup_node("/window").unwrap();
         let prop = window_node.get_property("screen_size").unwrap();
@@ -221,9 +221,10 @@ impl App {
     }
 
     async fn trigger_draw(&self) {
+        let atom = &mut self.render_api.make_guard();
         let window_node = self.sg_root.clone().lookup_node("/window").expect("no window attached!");
         match window_node.pimpl() {
-            Pimpl::Window(win) => win.draw().await,
+            Pimpl::Window(win) => win.draw(atom).await,
             _ => panic!("wrong pimpl"),
         }
     }

+ 48 - 36
bin/app/src/app/schema/chat.rs

@@ -29,6 +29,7 @@ use crate::{
         App,
     },
     expr::{self, Compiler},
+    gfx::make_render_guard,
     plugin::darkirc,
     prop::{
         Property, PropertyAtomicGuard, PropertyBool, PropertyFloat32, PropertyStr, PropertySubType,
@@ -192,7 +193,7 @@ pub async fn make(
         0,
     )
     .unwrap();
-    let atom = &mut PropertyAtomicGuard::new();
+    let atom = &mut PropertyAtomicGuard::none();
 
     let mut cc = Compiler::new();
 
@@ -288,17 +289,18 @@ pub async fn make(
     let sg_root = app.sg_root.clone();
     let layer_node2 = layer_node.clone();
     let chatview_is_visible = PropertyBool::wrap(&layer_node, Role::App, "is_visible", 0).unwrap();
+    let render_api = app.render_api.clone();
     let goback = async move || {
         info!(target: "app::chat", "clicked back");
-        let atom = &mut PropertyAtomicGuard::new();
+        let mut atom = make_render_guard(&render_api);
 
         let editz_node = layer_node2.clone().lookup_node("/content/editz").unwrap();
         editz_node.call_method("unfocus", vec![]).await.unwrap();
 
         let menu_node = sg_root.clone().lookup_node("/window/menu_layer").unwrap();
-        menu_node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
+        menu_node.set_property_bool(&mut atom, Role::App, "is_visible", true).unwrap();
 
-        chatview_is_visible.set(atom, false);
+        chatview_is_visible.set(&mut atom, false);
     };
 
     let (slot, recvr) = Slot::new("back_clicked");
@@ -502,7 +504,7 @@ pub async fn make(
         1.00, 0.30, 0.00, 1.
     ];
     for c in nick_colors {
-        prop.push_f32(Role::App, c).unwrap();
+        prop.clone().push_f32(atom, Role::App, c).unwrap();
     }
 
     let prop = node.get_property("hi_bg_color").unwrap();
@@ -838,17 +840,19 @@ pub async fn make(
     let editz_text2 = editz_text.clone();
     let channel2 = format!("#{channel}");
     let sg_root = app.sg_root.clone();
+    let render_api = app.render_api.clone();
     let sendmsg = move || {
         let editz_text = editz_text2.clone();
         let channel = channel2.clone();
         let sg_root = sg_root.clone();
         let chatview_node = chatview_node.clone();
+        let render_api = render_api.clone();
         async move {
-            let atom = &mut PropertyAtomicGuard::new();
+            let mut atom = make_render_guard(&render_api);
 
             let mut text = editz_text.get();
             info!(target: "app::chat", "Send '{text}' to channel: {channel}");
-            editz_text.set(atom, "");
+            editz_text.set(&mut atom, "");
 
             let Some(darkirc) = sg_root.clone().lookup_node("/plugin/darkirc") else {
                 error!(target: "app::chat", "DarkIrc plugin has not been loaded");
@@ -858,7 +862,7 @@ pub async fn make(
             if text.starts_with("/nick") {
                 let nick = text.split_whitespace().nth(1).unwrap_or("anon");
                 info!(target: "app::chat", "Setting nick to: {nick}");
-                darkirc.set_property_str(atom, Role::App, "nick", nick).unwrap();
+                darkirc.set_property_str(&mut atom, Role::App, "nick", nick).unwrap();
 
                 let msg = format!("You are now known as <{nick}>");
                 let id: [u8; 32] = rand::random();
@@ -954,6 +958,7 @@ pub async fn make(
     let (slot, recvr) = Slot::new("emoji_clicked");
     let chatedit_node2 = chatedit_node.clone();
     node.register("click", slot).unwrap();
+    let render_api = app.render_api.clone();
     let listen_click = app.ex.spawn(async move {
         let mut panel_height = if cfg!(target_os = "android") {
             let keyb_height = android_keyboard_height();
@@ -968,7 +973,7 @@ pub async fn make(
 
         while let Ok(_) = recvr.recv().await {
             info!(target: "app::chat", "clicked emoji");
-            let atom = &mut PropertyAtomicGuard::new();
+            let mut atom = make_render_guard(&render_api);
 
             if cfg!(target_os = "android") {
                 let keyb_height = android_keyboard_height();
@@ -982,11 +987,11 @@ pub async fn make(
 
                 assert!(!emoji_close_is_visible.get());
                 assert!(emoji_h_prop.get() < 0.001);
-                emoji_btn_is_visible.set(atom, false);
-                emoji_close_is_visible.set(atom, true);
-                emoji_h_prop.set(atom, panel_height as f32);
+                emoji_btn_is_visible.set(&mut atom, false);
+                emoji_close_is_visible.set(&mut atom, true);
+                emoji_h_prop.set(&mut atom, panel_height as f32);
                 //for i in 1..=20 {
-                //    emoji_h_prop.set(atom, (20 * i) as f32);
+                //    emoji_h_prop.set(&mut atom, (20 * i) as f32);
                 //    msleep(10).await;
                 //}
             } else {
@@ -994,11 +999,11 @@ pub async fn make(
 
                 assert!(emoji_close_is_visible.get());
                 assert!(emoji_h_prop.get() > 0.);
-                emoji_btn_is_visible.set(atom, true);
-                emoji_close_is_visible.set(atom, false);
-                emoji_h_prop.set(atom, 0. as f32);
+                emoji_btn_is_visible.set(&mut atom, true);
+                emoji_close_is_visible.set(&mut atom, false);
+                emoji_h_prop.set(&mut atom, 0. as f32);
                 //for i in 1..=20 {
-                //    emoji_h_prop.set(atom, (400 - 20 * i) as f32);
+                //    emoji_h_prop.set(&mut atom, (400 - 20 * i) as f32);
                 //    msleep(10).await;
                 //}
             }
@@ -1041,13 +1046,14 @@ pub async fn make(
     let (slot, recvr) = Slot::new("nickcmd_clicked");
     node.register("click", slot).unwrap();
     let editz_text2 = editz_text.clone();
+    let render_api = app.render_api.clone();
     let listen_click = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
             info!(target: "app::chat", "clicked /nick");
-            let atom = &mut PropertyAtomicGuard::new();
+            let mut atom = make_render_guard(&render_api);
             // This will autohide this popup due to ending in a space.
             // Setting the property will retrigger the logic whether to show popup.
-            editz_text2.set(atom, "/nick ");
+            editz_text2.set(&mut atom, "/nick ");
         }
     });
     app.tasks.lock().unwrap().push(listen_click);
@@ -1306,11 +1312,12 @@ pub async fn make(
     node.register("click", slot).unwrap();
     let actions_is_visible2 = actions_is_visible.clone();
     let editz_select_text2 = editz_select_text.clone();
+    let render_api = app.render_api.clone();
     let listen_click = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
             info!(target: "app::chat", "clicked copy");
-            let atom = &mut PropertyAtomicGuard::new();
-            actions_is_visible2.set(atom, false);
+            let mut atom = make_render_guard(&render_api);
+            actions_is_visible2.set(&mut atom, false);
             let select_text = editz_select_text2.get_str(0).unwrap();
             miniquad::window::clipboard_set(&select_text);
         }
@@ -1333,9 +1340,10 @@ pub async fn make(
     node.register("click", slot).unwrap();
     let actions_is_visible2 = actions_is_visible.clone();
     let chatedit_node2 = chatedit_node.clone();
+    let render_api = app.render_api.clone();
     let listen_click = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
-            let atom = &mut PropertyAtomicGuard::new();
+            let mut atom = make_render_guard(&render_api);
             if let Some(text) = miniquad::window::clipboard_get() {
                 info!(target: "app::chat", "clicked paste: {text}");
                 let mut data = vec![];
@@ -1344,7 +1352,7 @@ pub async fn make(
             } else {
                 info!(target: "app::chat", "clicked paste but clip is empty");
             }
-            actions_is_visible2.set(atom, false);
+            actions_is_visible2.set(&mut atom, false);
         }
     });
     app.tasks.lock().unwrap().push(listen_click);
@@ -1396,10 +1404,11 @@ pub async fn make(
     let (slot, recvr) = Slot::new("reqpasta");
     chatedit_node.register("paste_request", slot).unwrap();
     let pasta_is_visible2 = pasta_is_visible.clone();
+    let render_api = app.render_api.clone();
     let listen_click = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
-            let atom = &mut PropertyAtomicGuard::new();
-            pasta_is_visible2.set(atom, true);
+            let mut atom = make_render_guard(&render_api);
+            pasta_is_visible2.set(&mut atom, true);
         }
     });
     app.tasks.lock().unwrap().push(listen_click);
@@ -1481,9 +1490,10 @@ pub async fn make(
     node.register("click", slot).unwrap();
     let chatedit_node2 = chatedit_node.clone();
     let pasta_is_visible2 = pasta_is_visible.clone();
+    let render_api = app.render_api.clone();
     let listen_click = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
-            let atom = &mut PropertyAtomicGuard::new();
+            let mut atom = make_render_guard(&render_api);
             if let Some(text) = miniquad::window::clipboard_get() {
                 info!(target: "app::chat", "clicked paste: {text}");
                 let mut data = vec![];
@@ -1492,7 +1502,7 @@ pub async fn make(
             } else {
                 info!(target: "app::chat", "clicked paste but clip is empty");
             }
-            pasta_is_visible2.set(atom, false);
+            pasta_is_visible2.set(&mut atom, false);
         }
     });
     app.tasks.lock().unwrap().push(listen_click);
@@ -1501,18 +1511,19 @@ pub async fn make(
     {
         let editz_select_sub = editz_select_text.subscribe_modify();
         let pasta_is_visible2 = pasta_is_visible.clone();
+        let render_api = app.render_api.clone();
         let editz_select_task = app.ex.spawn(async move {
             while let Ok(_) = editz_select_sub.receive().await {
-                let atom = &mut PropertyAtomicGuard::new();
+                let mut atom = make_render_guard(&render_api);
                 if editz_select_text.is_null(0).unwrap() {
                     info!(target: "app::chat", "selection changed: null");
-                    actions_is_visible.set(atom, false);
-                    pasta_is_visible2.set(atom, false);
+                    actions_is_visible.set(&mut atom, false);
+                    pasta_is_visible2.set(&mut atom, false);
                 } else {
                     let select_text = editz_select_text.get_str(0).unwrap();
                     info!(target: "app::chat", "selection changed: {select_text}");
-                    actions_is_visible.set(atom, true);
-                    pasta_is_visible2.set(atom, false);
+                    actions_is_visible.set(&mut atom, true);
+                    pasta_is_visible2.set(&mut atom, false);
                 }
             }
         });
@@ -1520,10 +1531,11 @@ pub async fn make(
     }
 
     let editz_text_sub = editz_text.prop().subscribe_modify();
+    let render_api = app.render_api.clone();
     let editz_text_task = app.ex.spawn(async move {
         while let Ok(_) = editz_text_sub.receive().await {
-            let atom = &mut PropertyAtomicGuard::new();
-            pasta_is_visible.set(atom, false);
+            let mut atom = make_render_guard(&render_api);
+            pasta_is_visible.set(&mut atom, false);
 
             let text = editz_text.get();
             debug!(target: "app::chat", "text changed: {text}");
@@ -1533,11 +1545,11 @@ pub async fn make(
             // Only show popup for "/ni", "/nick", but not for: "", "/nick ", "/nick foo"
             if !text.is_empty() && "/nick".starts_with(&text) && text.len() <= "/nick".len() {
                 if !cmd_hint_is_visible.get() {
-                    cmd_hint_is_visible.set(atom, true);
+                    cmd_hint_is_visible.set(&mut atom, true);
                 }
             } else {
                 if cmd_hint_is_visible.get() {
-                    cmd_hint_is_visible.set(atom, false);
+                    cmd_hint_is_visible.set(&mut atom, false);
                 }
             }
         }

+ 7 - 5
bin/app/src/app/schema/menu.rs

@@ -22,6 +22,7 @@ use crate::{
         App,
     },
     expr,
+    gfx::make_render_guard,
     prop::{PropertyAtomicGuard, PropertyBool, PropertyFloat32, Role},
     scene::{SceneNodePtr, Slot},
     ui::{Button, Layer, ShapeVertex, Shortcut, Text, VectorArt, VectorShape},
@@ -69,7 +70,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
         0,
     )
     .unwrap();
-    let atom = &mut PropertyAtomicGuard::new();
+    let atom = &mut PropertyAtomicGuard::none();
 
     // Main view
     let layer_node = create_layer("menu_layer");
@@ -253,12 +254,13 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
             PropertyBool::wrap(&chatview_node, Role::App, "is_visible", 0).unwrap();
         let menu_is_visible = PropertyBool::wrap(&layer_node, Role::App, "is_visible", 0).unwrap();
 
+        let render_api = app.render_api.clone();
         let select_channel = move || {
-            let atom = &mut PropertyAtomicGuard::new();
+            let mut atom = make_render_guard(&render_api);
             info!(target: "app::menu", "clicked: {channel}!");
-            chatview_is_visible.set(atom, true);
-            menu_is_visible.set(atom, false);
-            set_normal_color(atom);
+            chatview_is_visible.set(&mut atom, true);
+            menu_is_visible.set(&mut atom, false);
+            set_normal_color(&mut atom);
         };
 
         let select_channel2 = select_channel.clone();

+ 8 - 5
bin/app/src/app/schema/mod.rs

@@ -26,6 +26,7 @@ use crate::{
         App,
     },
     expr::{self, Compiler},
+    gfx::make_render_guard,
     prop::{PropertyAtomicGuard, Role},
     scene::{SceneNodePtr, Slot},
     shape,
@@ -130,7 +131,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
     cc.add_const_f32("NETSTATUS_ICON_SIZE", NETSTATUS_ICON_SIZE);
     cc.add_const_f32("SETTINGS_ICON_SIZE", SETTINGS_ICON_SIZE);
 
-    let atom = &mut PropertyAtomicGuard::new();
+    let atom = &mut PropertyAtomicGuard::none();
 
     let node = create_shortcut("zoom_out_shortcut");
     node.set_property_str(atom, Role::App, "key", "ctrl+-").unwrap();
@@ -140,6 +141,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
     node.register("shortcut", slot).unwrap();
     let window_scale = app.sg_root.clone().lookup_node("/setting/scale").unwrap();
     let window_scale2 = window_scale.clone();
+    let render_api = app.render_api.clone();
     let listen_zoom = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
             let scale = 0.9 * window_scale2.get_property_f32("value").unwrap();
@@ -152,8 +154,8 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
                 scale.encode(&mut file).unwrap();
             }
 
-            let atom = &mut PropertyAtomicGuard::new();
-            window_scale2.set_property_f32(atom, Role::User, "value", scale).unwrap();
+            let mut atom = make_render_guard(&render_api);
+            window_scale2.set_property_f32(&mut atom, Role::User, "value", scale).unwrap();
         }
     });
     app.tasks.lock().unwrap().push(listen_zoom);
@@ -167,6 +169,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
     let (slot, recvr) = Slot::new("zoom_in_pressed");
     node.register("shortcut", slot).unwrap();
     let window_scale2 = window_scale.clone();
+    let render_api = app.render_api.clone();
     let listen_zoom = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
             let scale = 1.1 * window_scale2.get_property_f32("value").unwrap();
@@ -179,8 +182,8 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
                 scale.encode(&mut file).unwrap();
             }
 
-            let atom = &mut PropertyAtomicGuard::new();
-            window_scale2.set_property_f32(atom, Role::User, "value", scale).unwrap();
+            let mut atom = make_render_guard(&render_api);
+            window_scale2.set_property_f32(&mut atom, Role::User, "value", scale).unwrap();
         }
     });
     app.tasks.lock().unwrap().push(listen_zoom);

+ 138 - 23
bin/app/src/gfx/mod.rs

@@ -50,6 +50,7 @@ use trax::get_trax;
 
 use crate::{
     error::{Error, Result},
+    prop::{BatchGuardId, PropertyAtomicGuard},
     GOD,
 };
 
@@ -166,7 +167,7 @@ impl RenderApi {
     }
 
     fn next_epoch(&self) -> EpochIndex {
-        self.epoch.fetch_add(1, Ordering::SeqCst) + 1
+        self.epoch.fetch_add(1, Ordering::Relaxed) + 1
     }
 
     fn send(&self, method: GraphicsMethod) -> EpochIndex {
@@ -185,7 +186,7 @@ impl RenderApi {
         data: Vec<u8>,
         tag: DebugTag,
     ) -> (GfxTextureId, EpochIndex) {
-        let gfx_texture_id = NEXT_TEXTURE_ID.fetch_add(1, Ordering::SeqCst);
+        let gfx_texture_id = NEXT_TEXTURE_ID.fetch_add(1, Ordering::Relaxed);
 
         let method = GraphicsMethod::NewTexture((width, height, data, gfx_texture_id, tag));
         let epoch = self.send(method);
@@ -214,7 +215,7 @@ impl RenderApi {
         verts: Vec<Vertex>,
         tag: DebugTag,
     ) -> (GfxBufferId, EpochIndex) {
-        let gfx_buffer_id = NEXT_BUFFER_ID.fetch_add(1, Ordering::SeqCst);
+        let gfx_buffer_id = NEXT_BUFFER_ID.fetch_add(1, Ordering::Relaxed);
 
         let method = GraphicsMethod::NewVertexBuffer((verts, gfx_buffer_id, tag));
         let epoch = self.send(method);
@@ -227,7 +228,7 @@ impl RenderApi {
         indices: Vec<u16>,
         tag: DebugTag,
     ) -> (GfxBufferId, EpochIndex) {
-        let gfx_buffer_id = NEXT_BUFFER_ID.fetch_add(1, Ordering::SeqCst);
+        let gfx_buffer_id = NEXT_BUFFER_ID.fetch_add(1, Ordering::Relaxed);
 
         let method = GraphicsMethod::NewIndexBuffer((indices, gfx_buffer_id, tag));
         let epoch = self.send(method);
@@ -255,10 +256,36 @@ impl RenderApi {
         self.send_with_epoch(method, epoch);
     }
 
-    pub fn replace_draw_calls(&self, timest: Timestamp, dcs: Vec<(DcId, GfxDrawCall)>) {
-        let method = GraphicsMethod::ReplaceDrawCalls { timest, dcs };
+    pub fn replace_draw_calls(
+        &self,
+        batch_id: BatchGuardId,
+        timest: Timestamp,
+        dcs: Vec<(DcId, GfxDrawCall)>,
+    ) {
+        let method = GraphicsMethod::ReplaceDrawCalls { batch_id, timest, dcs };
+        self.send(method);
+    }
+
+    fn start_batch(&self, batch_id: BatchGuardId) {
+        let method = GraphicsMethod::StartBatch(batch_id);
         self.send(method);
     }
+    fn end_batch(&self, batch_id: BatchGuardId) {
+        let method = GraphicsMethod::EndBatch(batch_id);
+        self.send(method);
+    }
+
+    pub fn make_guard(&self) -> PropertyAtomicGuard {
+        let r = self.clone();
+        let start_batch = Box::new(move |bid| r.start_batch(bid));
+        let r = self.clone();
+        let end_batch = Box::new(move |bid| r.end_batch(bid));
+        PropertyAtomicGuard::new(start_batch, end_batch)
+    }
+}
+
+pub fn make_render_guard(render_api: &RenderApi) -> PropertyAtomicGuard {
+    render_api.make_guard()
 }
 
 #[derive(Clone, Debug)]
@@ -652,7 +679,9 @@ pub enum GraphicsMethod {
     NewVertexBuffer((Vec<Vertex>, GfxBufferId, DebugTag)),
     NewIndexBuffer((Vec<u16>, GfxBufferId, DebugTag)),
     DeleteBuffer((GfxBufferId, DebugTag, u8)),
-    ReplaceDrawCalls { timest: Timestamp, dcs: Vec<(DcId, GfxDrawCall)> },
+    ReplaceDrawCalls { batch_id: BatchGuardId, timest: Timestamp, dcs: Vec<(DcId, GfxDrawCall)> },
+    StartBatch(BatchGuardId),
+    EndBatch(BatchGuardId),
 }
 
 impl std::fmt::Debug for GraphicsMethod {
@@ -663,7 +692,11 @@ impl std::fmt::Debug for GraphicsMethod {
             Self::NewVertexBuffer(_) => write!(f, "NewVertexBuffer"),
             Self::NewIndexBuffer(_) => write!(f, "NewIndexBuffer"),
             Self::DeleteBuffer(_) => write!(f, "DeleteBuffer"),
-            Self::ReplaceDrawCalls { timest: _, dcs: _ } => write!(f, "ReplaceDrawCalls"),
+            Self::ReplaceDrawCalls { batch_id: bid, timest: _, dcs: _ } => {
+                write!(f, "ReplaceDrawCalls({bid})")
+            }
+            Self::StartBatch(bid) => write!(f, "StartBatch({bid})"),
+            Self::EndBatch(bid) => write!(f, "EndBatch({bid})"),
         }
     }
 }
@@ -798,6 +831,7 @@ struct Stage {
     pipeline: Pipeline,
     white_texture: miniquad::TextureId,
     draw_calls: HashMap<DcId, DrawCall>,
+    batches: HashMap<BatchGuardId, Vec<GraphicsMethod>>,
 
     textures: HashMap<GfxTextureId, miniquad::TextureId>,
     buffers: HashMap<GfxBufferId, miniquad::BufferId>,
@@ -894,6 +928,7 @@ impl Stage {
                 0,
                 DrawCall { instrs: vec![], dcs: vec![], z_index: 0, timest: 0 },
             )]),
+            batches: HashMap::new(),
 
             textures: HashMap::new(),
             buffers: HashMap::new(),
@@ -921,9 +956,47 @@ impl Stage {
                 self.method_new_index_buffer(indices, *gbuff_id)
             }
             GraphicsMethod::DeleteBuffer((gbuff_id, _, _)) => self.method_delete_buffer(*gbuff_id),
-            GraphicsMethod::ReplaceDrawCalls { timest, dcs } => {
+            GraphicsMethod::ReplaceDrawCalls { batch_id, timest, dcs } => {
+                t!("Commit dc to {batch_id}");
+                let batch = self.batches.get_mut(batch_id).unwrap();
                 let dcs = std::mem::take(dcs);
-                self.method_replace_draw_calls(*timest, dcs)
+                batch.push(GraphicsMethod::ReplaceDrawCalls {
+                    batch_id: *batch_id,
+                    timest: *timest,
+                    dcs,
+                });
+                if DEBUG_TRAX {
+                    get_trax().lock().put_stat(0);
+                }
+                Ok(())
+            }
+            GraphicsMethod::StartBatch(batch_id) => {
+                t!("Start batch {batch_id}");
+                if !self.batches.insert(*batch_id, vec![]).is_none() {
+                    panic!("Batch {batch_id} already open!")
+                }
+                if DEBUG_TRAX {
+                    get_trax().lock().put_stat(0);
+                }
+                Ok(())
+            }
+            GraphicsMethod::EndBatch(batch_id) => {
+                t!("End batch {batch_id}");
+                let batch = self.batches.remove(batch_id).unwrap();
+                for mut method in batch {
+                    let res = match &mut method {
+                        GraphicsMethod::ReplaceDrawCalls { batch_id: _, timest, dcs } => {
+                            let dcs = std::mem::take(dcs);
+                            self.method_replace_draw_calls(*timest, dcs)
+                        }
+                        _ => panic!("unexpected method in batch!"),
+                    };
+                    if let Err(err) = res {
+                        e!("process_method(method={method:?}) failed with err: {err:?}");
+                        panic!("process_method failed!")
+                    }
+                }
+                Ok(())
             }
         };
         if let Err(err) = res {
@@ -1112,8 +1185,14 @@ impl Stage {
             GraphicsMethod::DeleteBuffer((gbuff_id, tag, buftype)) => {
                 trax.del_buf(epoch, *gbuff_id, *tag, *buftype);
             }
-            GraphicsMethod::ReplaceDrawCalls { timest, dcs } => {
-                trax.put_dcs(epoch, *timest, dcs);
+            GraphicsMethod::ReplaceDrawCalls { batch_id, timest, dcs } => {
+                trax.put_dcs(epoch, *batch_id, *timest, dcs);
+            }
+            GraphicsMethod::StartBatch(batch_id) => {
+                trax.put_start_batch(epoch, *batch_id);
+            }
+            GraphicsMethod::EndBatch(batch_id) => {
+                trax.put_end_batch(epoch, *batch_id);
             }
         };
     }
@@ -1140,7 +1219,7 @@ struct PruneMethodHeap {
     /// Deleted objects
     del: Vec<GraphicsMethod>,
     /// Draw calls
-    dcs: HashMap<DcId, (Timestamp, GfxDrawCall)>,
+    dcs: HashMap<DcId, (BatchGuardId, Timestamp, GfxDrawCall)>,
 
     epoch: EpochIndex,
 }
@@ -1190,25 +1269,34 @@ impl PruneMethodHeap {
                     self.del.push(method);
                 }
             }
-            GraphicsMethod::ReplaceDrawCalls { timest, dcs } => {
-                self.method_replace_draw_calls(timest, dcs)
+            GraphicsMethod::ReplaceDrawCalls { batch_id, timest, dcs } => {
+                self.method_replace_draw_calls(batch_id, timest, dcs)
             }
+            // Discard batches since we will apply everything all at once anyway
+            // once the screen is switched on.
+            GraphicsMethod::StartBatch(_) => {}
+            GraphicsMethod::EndBatch(_) => {}
         }
     }
 
-    fn method_replace_draw_calls(&mut self, timest: Timestamp, dcs: Vec<(DcId, GfxDrawCall)>) {
+    fn method_replace_draw_calls(
+        &mut self,
+        batch_id: BatchGuardId,
+        timest: Timestamp,
+        dcs: Vec<(DcId, GfxDrawCall)>,
+    ) {
         for (key, val) in dcs {
             match self.dcs.get_mut(&key) {
                 Some(old_val) => {
                     // Only replace the draw call if it is more recent
-                    if old_val.0 < timest {
-                        *old_val = (timest, val);
+                    if old_val.1 < timest {
+                        *old_val = (batch_id, timest, val);
                     } else {
                         trace!(target: "gfx::pruner", "Rejected stale draw_call {key}: {val:?}");
                     }
                 }
                 None => {
-                    self.dcs.insert(key, (timest, val));
+                    self.dcs.insert(key, (batch_id, timest, val));
                 }
             }
         }
@@ -1216,6 +1304,7 @@ impl PruneMethodHeap {
 
     /// Collect everything now the screen is on
     fn recv_all(&mut self) -> Vec<GraphicsMethod> {
+        // Inhale that smoke deep
         let mut meth = Vec::with_capacity(
             self.new_buf.len() + self.new_tex.len() + self.del.len() + self.dcs.len(),
         );
@@ -1224,8 +1313,12 @@ impl PruneMethodHeap {
         meth.extend(new_buf.into_values());
         meth.extend(new_tex.into_values());
         meth.append(&mut self.del);
-        for (dc_id, (timest, dc)) in std::mem::take(&mut self.dcs) {
-            meth.push(GraphicsMethod::ReplaceDrawCalls { timest, dcs: vec![(dc_id, dc)] });
+        for (dc_id, (batch_id, timest, dc)) in std::mem::take(&mut self.dcs) {
+            meth.push(GraphicsMethod::ReplaceDrawCalls {
+                batch_id,
+                timest,
+                dcs: vec![(dc_id, dc)],
+            });
         }
         meth
     }
@@ -1233,9 +1326,18 @@ impl PruneMethodHeap {
 
 impl EventHandler for Stage {
     fn update(&mut self) {
+        // todo: trax is all messed up in this func
+
         let methods = std::mem::take(&mut *self.method_queue.lock());
 
         if self.egl_ctx_is_disabled() {
+            // Immediately apply any pending batches when the screen is switched off
+            let batch_ids: Vec<_> = self.batches.keys().cloned().collect();
+            for batch_id in batch_ids {
+                self.process_method(GraphicsMethod::EndBatch(batch_id));
+            }
+            self.batches.clear();
+
             // Screen is off so collect all methods into the pruner
             self.pruner.drain(methods);
             self.screen_was_off = true;
@@ -1248,14 +1350,26 @@ impl EventHandler for Stage {
         if self.screen_was_off {
             self.screen_was_off = false;
         } else {
+            let methods = self.pruner.recv_all();
+            assert!(methods.is_empty() || self.batches.is_empty());
             // Process all cached methods by the pruner from while the screen was off.
-            for method in self.pruner.recv_all() {
+            for method in methods {
                 // Stale methods will be dropped by pruner, so they will not be caught by trax
                 // while the screen is off.
                 if DEBUG_TRAX {
                     self.trax_method(self.epoch, &method);
                 }
-                self.process_method(method);
+                // We discard batches here but process_method uses them so implement this
+                // workaround.
+                match method {
+                    GraphicsMethod::ReplaceDrawCalls { batch_id: _, timest, dcs } => {
+                        if let Err(err) = self.method_replace_draw_calls(timest, dcs) {
+                            e!("process_method for ReplaceDrawCalls failed err: {err:?}");
+                            panic!("process_method failed!")
+                        }
+                    }
+                    _ => self.process_method(method),
+                }
                 if DEBUG_TRAX {
                     get_trax().lock().flush();
                 }
@@ -1387,6 +1501,7 @@ pub fn run_gui() {
         high_dpi: true,
         window_resizable: true,
         platform: miniquad::conf::Platform {
+            linux_x11_gl: miniquad::conf::LinuxX11Gl::EGLWithGLXFallback,
             linux_backend: miniquad::conf::LinuxBackend::WaylandWithX11Fallback,
             #[cfg(target_os = "android")]
             blocking_event_loop: true,

+ 34 - 16
bin/app/src/gfx/trax.rs

@@ -22,7 +22,7 @@ use parking_lot::Mutex as SyncMutex;
 use std::{fs::File, sync::OnceLock};
 
 use super::{DebugTag, GfxBufferId, GfxDrawCall, GfxTextureId, Vertex};
-use crate::EpochIndex;
+use crate::{prop::BatchGuardId, EpochIndex};
 
 macro_rules! d { ($($arg:tt)*) => { debug!(target: "gfx::trax", $($arg)*); } }
 
@@ -47,17 +47,36 @@ impl Trax {
         self.file.set_len(0).unwrap();
     }
 
-    pub fn put_dcs(&mut self, epoch: EpochIndex, timest: u64, dcs: &Vec<(u64, GfxDrawCall)>) {
-        d!("put_dcs({epoch}, {timest}, {dcs:?})");
+    pub fn put_dcs(
+        &mut self,
+        epoch: EpochIndex,
+        batch_id: BatchGuardId,
+        timest: u64,
+        dcs: &Vec<(u64, GfxDrawCall)>,
+    ) {
+        d!("put_dcs({epoch}, {batch_id}, {timest}, {dcs:?})");
         0u8.encode(&mut self.buf).unwrap();
         epoch.encode(&mut self.buf).unwrap();
+        batch_id.encode(&mut self.buf).unwrap();
         timest.encode(&mut self.buf).unwrap();
         dcs.encode(&mut self.buf).unwrap();
     }
 
+    pub fn put_start_batch(&mut self, epoch: EpochIndex, batch_id: BatchGuardId) {
+        d!("put_start_batch({epoch}, {batch_id})");
+        1u8.encode(&mut self.buf).unwrap();
+        batch_id.encode(&mut self.buf).unwrap();
+    }
+
+    pub fn put_end_batch(&mut self, epoch: EpochIndex, batch_id: BatchGuardId) {
+        d!("put_end_batch({epoch}, {batch_id})");
+        2u8.encode(&mut self.buf).unwrap();
+        batch_id.encode(&mut self.buf).unwrap();
+    }
+
     pub fn put_tex(&mut self, epoch: EpochIndex, tex: GfxTextureId, tag: DebugTag) {
         d!("put_tex({epoch}, {tex}, {tag:?})");
-        1u8.encode(&mut self.buf).unwrap();
+        3u8.encode(&mut self.buf).unwrap();
         epoch.encode(&mut self.buf).unwrap();
         tex.encode(&mut self.buf).unwrap();
         tag.encode(&mut self.buf).unwrap();
@@ -71,7 +90,7 @@ impl Trax {
         buftype: u8,
     ) {
         d!("put_verts({epoch}, ..., {buf}, {tag:?}, {buftype})");
-        2u8.encode(&mut self.buf).unwrap();
+        4u8.encode(&mut self.buf).unwrap();
         epoch.encode(&mut self.buf).unwrap();
         verts.encode(&mut self.buf).unwrap();
         buf.encode(&mut self.buf).unwrap();
@@ -87,29 +106,23 @@ impl Trax {
         buftype: u8,
     ) {
         d!("put_idxs({epoch}, ..., {buf}, {tag:?}, {buftype})");
-        3u8.encode(&mut self.buf).unwrap();
+        5u8.encode(&mut self.buf).unwrap();
         epoch.encode(&mut self.buf).unwrap();
         idxs.encode(&mut self.buf).unwrap();
         buf.encode(&mut self.buf).unwrap();
         tag.encode(&mut self.buf).unwrap();
         buftype.encode(&mut self.buf).unwrap();
     }
-
-    pub fn put_stat(&mut self, code: u8) {
-        d!("put_stat({code})");
-        code.encode(&mut self.buf).unwrap();
-    }
-
     pub fn del_tex(&mut self, epoch: EpochIndex, tex: GfxTextureId, tag: DebugTag) {
         d!("del_tex({epoch}, {tex}, {tag:?})");
-        4u8.encode(&mut self.buf).unwrap();
+        6u8.encode(&mut self.buf).unwrap();
         epoch.encode(&mut self.buf).unwrap();
         tex.encode(&mut self.buf).unwrap();
         tag.encode(&mut self.buf).unwrap();
     }
     pub fn del_buf(&mut self, epoch: EpochIndex, buf: GfxBufferId, tag: DebugTag, buftype: u8) {
         d!("del_buf({epoch}, {buf}, {tag:?}, {buftype})");
-        5u8.encode(&mut self.buf).unwrap();
+        7u8.encode(&mut self.buf).unwrap();
         epoch.encode(&mut self.buf).unwrap();
         buf.encode(&mut self.buf).unwrap();
         tag.encode(&mut self.buf).unwrap();
@@ -118,17 +131,22 @@ impl Trax {
 
     pub fn set_curr(&mut self, dc: u64) {
         d!("set_curr({dc})");
-        6u8.encode(&mut self.buf).unwrap();
+        8u8.encode(&mut self.buf).unwrap();
         dc.encode(&mut self.buf).unwrap();
         self.flush();
     }
     pub fn set_instr(&mut self, idx: usize) {
         d!("set_instr({idx})");
-        7u8.encode(&mut self.buf).unwrap();
+        9u8.encode(&mut self.buf).unwrap();
         idx.encode(&mut self.buf).unwrap();
         self.flush();
     }
 
+    pub fn put_stat(&mut self, code: u8) {
+        d!("put_stat({code})");
+        code.encode(&mut self.buf).unwrap();
+    }
+
     pub fn flush(&mut self) {
         d!("flush");
         let buf = std::mem::take(&mut self.buf);

+ 12 - 11
bin/app/src/main.rs

@@ -150,17 +150,6 @@ impl God {
         let bg_runtime = AsyncRuntime::new(bg_ex.clone(), "bg");
         bg_runtime.start();
 
-        #[cfg(feature = "enable-netdebug")]
-        {
-            let sg_root = sg_root.clone();
-            let ex = bg_ex.clone();
-            let zmq_task = bg_ex.spawn(async {
-                let zmq_rpc = ZeroMQAdapter::new(sg_root, ex).await;
-                zmq_rpc.run().await;
-            });
-            bg_runtime.push_task(zmq_task);
-        }
-
         let fg_runtime = AsyncRuntime::new(fg_ex.clone(), "fg");
 
         let (method_send, method_recv) = async_channel::unbounded();
@@ -182,6 +171,18 @@ impl God {
         });
         fg_runtime.push_task(app_task);
 
+        #[cfg(feature = "enable-netdebug")]
+        {
+            let sg_root = sg_root.clone();
+            let ex = bg_ex.clone();
+            let render_api = render_api.clone();
+            let zmq_task = bg_ex.spawn(async {
+                let zmq_rpc = ZeroMQAdapter::new(sg_root, render_api, ex).await;
+                zmq_rpc.run().await;
+            });
+            bg_runtime.push_task(zmq_task);
+        }
+
         #[cfg(feature = "enable-plugins")]
         {
             let ex = bg_ex.clone();

+ 13 - 10
bin/app/src/net.rs

@@ -24,6 +24,7 @@ use zeromq::{Socket, SocketRecv, SocketSend};
 use crate::{
     error::{Error, Result},
     expr::SExprCode,
+    gfx::{make_render_guard, RenderApi},
     prop::{PropertyAtomicGuard, PropertyType, Role},
     scene::{SceneNodeId, SceneNodePtr, ScenePath},
     ExecutorPtr,
@@ -75,6 +76,7 @@ pub struct ZeroMQAdapter {
     slot_recvr: Option<mpsc::Receiver<(Vec<u8>, Vec<u8>)>>,
     */
     sg_root: SceneNodePtr,
+    render_api: RenderApi,
     _ex: ExecutorPtr,
 
     zmq_rep: Mutex<zeromq::RepSocket>,
@@ -82,7 +84,7 @@ pub struct ZeroMQAdapter {
 }
 
 impl ZeroMQAdapter {
-    pub async fn new(sg_root: SceneNodePtr, ex: ExecutorPtr) -> Arc<Self> {
+    pub async fn new(sg_root: SceneNodePtr, render_api: RenderApi, ex: ExecutorPtr) -> Arc<Self> {
         let mut zmq_rep = zeromq::RepSocket::new();
         zmq_rep.bind("tcp://0.0.0.0:9484").await.unwrap();
 
@@ -91,6 +93,7 @@ impl ZeroMQAdapter {
 
         Arc::new(Self {
             sg_root,
+            render_api,
             _ex: ex,
             zmq_rep: Mutex::new(zmq_rep),
             _zmq_pub: Mutex::new(zmq_pub),
@@ -243,40 +246,40 @@ impl ZeroMQAdapter {
                     self.sg_root.clone().lookup_node(node_path).ok_or(Error::NodeNotFound)?;
                 let prop = node.get_property(&prop_name).ok_or(Error::PropertyNotFound)?;
 
-                let atom = &mut PropertyAtomicGuard::new();
+                let mut atom = make_render_guard(&self.render_api);
 
                 match prop_type {
                     PropertyType::Null => {
-                        prop.set_null(atom, Role::User, prop_i)?;
+                        prop.set_null(&mut atom, Role::User, prop_i)?;
                     }
                     PropertyType::Bool => {
                         let val = bool::decode(&mut cur).unwrap();
-                        prop.set_bool(atom, Role::User, prop_i, val)?;
+                        prop.set_bool(&mut atom, Role::User, prop_i, val)?;
                     }
                     PropertyType::Uint32 => {
                         let val = u32::decode(&mut cur).unwrap();
-                        prop.set_u32(atom, Role::User, prop_i, val)?;
+                        prop.set_u32(&mut atom, Role::User, prop_i, val)?;
                     }
                     PropertyType::Float32 => {
                         let val = f32::decode(&mut cur).unwrap();
-                        prop.set_f32(atom, Role::User, prop_i, val)?;
+                        prop.set_f32(&mut atom, Role::User, prop_i, val)?;
                     }
                     PropertyType::Str => {
                         let val = String::decode(&mut cur).unwrap();
-                        prop.set_str(atom, Role::User, prop_i, val)?;
+                        prop.set_str(&mut atom, Role::User, prop_i, val)?;
                     }
                     PropertyType::Enum => {
                         let val = String::decode(&mut cur).unwrap();
-                        prop.set_enum(atom, Role::User, prop_i, val)?;
+                        prop.set_enum(&mut atom, Role::User, prop_i, val)?;
                     }
                     PropertyType::SceneNodeId => {
                         let val = SceneNodeId::decode(&mut cur).unwrap();
-                        prop.set_node_id(atom, Role::User, prop_i, val)?;
+                        prop.set_node_id(&mut atom, Role::User, prop_i, val)?;
                     }
                     PropertyType::SExpr => {
                         let val = SExprCode::decode(&mut cur).unwrap();
                         debug!(target: "req", "  received code {:?}", val);
-                        prop.set_expr(atom, Role::User, prop_i, val)?;
+                        prop.set_expr(&mut atom, Role::User, prop_i, val)?;
                     }
                 }
             }

+ 4 - 4
bin/app/src/plugin/darkirc.rs

@@ -40,7 +40,7 @@ use std::{
 
 use crate::{
     error::{Error, Result},
-    prop::{PropertyAtomicGuard, PropertyStr, Role},
+    prop::{BatchGuardPtr, PropertyAtomicGuard, PropertyStr, Role},
     scene::{MethodCallSub, Pimpl, SceneNode, SceneNodeType, SceneNodeWeak},
     ui::{
         chatview::{MessageId, Timestamp},
@@ -267,7 +267,7 @@ impl DarkIrc {
         };
 
         if let Ok(prev_nick) = std::fs::read_to_string(nick_filename()) {
-            nick.set(&mut PropertyAtomicGuard::new(), prev_nick);
+            nick.set(&mut PropertyAtomicGuard::none(), prev_nick);
         }
 
         let self_ = Arc::new(Self {
@@ -496,7 +496,7 @@ impl DarkIrc {
         self.p2p.broadcast(&EventPut(event)).await;
     }
 
-    async fn apply_settings(self_: Arc<Self>) {
+    async fn apply_settings(self_: Arc<Self>, _: BatchGuardPtr) {
         self_.settings.save_settings();
 
         let p2p_settings = self_.p2p.settings();
@@ -525,7 +525,7 @@ impl DarkIrc {
             ex.spawn(async move { while Self::process_send(&me2, &method_sub).await {} });
 
         let mut on_modify = OnModify::new(ex.clone(), self.node.clone(), me.clone());
-        async fn save_nick(self_: Arc<DarkIrc>) {
+        async fn save_nick(self_: Arc<DarkIrc>, _batch: BatchGuardPtr) {
             let _ = std::fs::write(nick_filename(), self_.nick.get());
         }
         on_modify.when_change(self.nick.prop(), save_nick);

+ 2 - 2
bin/app/src/plugin/mod.rs

@@ -37,7 +37,7 @@ pub struct PluginSettings {
 }
 impl PluginSettings {
     pub fn add_setting(&self, name: &str, default: PropertyValue) -> Option<SceneNodePtr> {
-        let atom = &mut PropertyAtomicGuard::new();
+        let atom = &mut PropertyAtomicGuard::none();
         let node = match default {
             PropertyValue::Bool(b) => {
                 let mut node = SceneNode::new(name, SceneNodeType::Setting);
@@ -94,7 +94,7 @@ impl PluginSettings {
 
     // For all settings, copy the value from sled into the setting node's value property
     pub fn load_settings(&self) {
-        let atom = &mut PropertyAtomicGuard::new();
+        let atom = &mut PropertyAtomicGuard::none();
         for setting_node in self.setting_root.get_children().iter() {
             if setting_node.typ != SceneNodeType::Setting {
                 continue

+ 52 - 3
bin/app/src/prop/guard.rs

@@ -16,8 +16,15 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
+use std::sync::{
+    atomic::{AtomicU32, Ordering},
+    Arc,
+};
+
 use super::{ModifyAction, PropertyPtr, Role};
 
+static BATCH_ID: AtomicU32 = AtomicU32::new(0);
+
 /// This schedules all property updates to happen at the end of the scope.
 /// We can therefore have fine-grained control about when property updates are
 /// propagated to the rest of the scenegraph.
@@ -39,12 +46,21 @@ use super::{ModifyAction, PropertyPtr, Role};
 /// This also has the unintended side-effect of making draws much faster since they aren't
 /// interrupted halfway through by extra compute.
 pub struct PropertyAtomicGuard {
+    pub batch_id: BatchGuardId,
     updates: Vec<(PropertyPtr, Role, ModifyAction)>,
+    end_batch: Option<BatchGuardCb>,
+    parent: Option<BatchGuardPtr>,
 }
 
 impl PropertyAtomicGuard {
-    pub fn new() -> Self {
-        Self { updates: vec![] }
+    pub fn new(start_batch: BatchGuardCb, end_batch: BatchGuardCb) -> Self {
+        let batch_id = BATCH_ID.fetch_add(1, Ordering::Relaxed);
+        start_batch(batch_id);
+        Self { batch_id, updates: vec![], end_batch: Some(end_batch), parent: None }
+    }
+
+    pub fn none() -> Self {
+        Self::new(Box::new(|_| {}), Box::new(|_| {}))
     }
 
     pub(super) fn add(&mut self, prop: PropertyPtr, role: Role, action: ModifyAction) {
@@ -54,8 +70,41 @@ impl PropertyAtomicGuard {
 
 impl Drop for PropertyAtomicGuard {
     fn drop(&mut self) {
+        let guard = Arc::new(BatchGuard {
+            id: self.batch_id,
+            end_batch: self.end_batch.take(),
+            _parent: self.parent.take(),
+        });
         for (prop, role, action) in std::mem::take(&mut self.updates) {
-            prop.on_modify.notify((role, action));
+            prop.on_modify.notify((role, action, guard.clone()));
         }
     }
 }
+
+pub type BatchGuardId = u32;
+type BatchGuardCb = Box<dyn FnOnce(BatchGuardId) + Send + Sync>;
+pub type BatchGuardPtr = Arc<BatchGuard>;
+
+pub struct BatchGuard {
+    pub id: BatchGuardId,
+    end_batch: Option<BatchGuardCb>,
+    _parent: Option<BatchGuardPtr>,
+}
+
+impl BatchGuard {
+    pub fn spawn(self: &Arc<Self>) -> PropertyAtomicGuard {
+        PropertyAtomicGuard {
+            batch_id: self.id,
+            updates: vec![],
+            end_batch: Some(Box::new(|_| {})),
+            parent: Some(self.clone()),
+        }
+    }
+}
+
+impl Drop for BatchGuard {
+    fn drop(&mut self) {
+        let end_batch = self.end_batch.take().unwrap();
+        end_batch(self.id);
+    }
+}

+ 91 - 33
bin/app/src/prop/mod.rs

@@ -30,7 +30,7 @@ use crate::{
 };
 
 mod guard;
-pub use guard::PropertyAtomicGuard;
+pub use guard::{BatchGuardId, BatchGuardPtr, PropertyAtomicGuard};
 mod wrap;
 pub use wrap::{
     PropertyBool, PropertyColor, PropertyDimension, PropertyFloat32, PropertyRect, PropertyStr,
@@ -203,7 +203,7 @@ pub enum ModifyAction {
     Push(usize),
 }
 
-type ModifyPublisher = PublisherPtr<(Role, ModifyAction)>;
+type ModifyPublisher = PublisherPtr<(Role, ModifyAction, BatchGuardPtr)>;
 
 pub type PropertyPtr = Arc<Property>;
 pub type PropertyWeak = Weak<Property>;
@@ -364,11 +364,13 @@ impl Property {
     // Set
 
     /// This will clear all values, resetting them to the default
-    pub fn clear_values(self: Arc<Self>, role: Role) {
-        let vals = &mut self.vals.lock().unwrap();
-        vals.clear();
-        vals.resize(self.array_len, PropertyValue::Unset);
-        self.on_modify.notify((role, ModifyAction::Clear));
+    pub fn clear_values(self: Arc<Self>, atom: &mut PropertyAtomicGuard, role: Role) {
+        {
+            let vals = &mut self.vals.lock().unwrap();
+            vals.clear();
+            vals.resize(self.array_len, PropertyValue::Unset);
+        }
+        atom.add(self, role, ModifyAction::Clear);
     }
 
     fn set_raw_value(&self, i: usize, val: PropertyValue) -> Result<()> {
@@ -564,39 +566,66 @@ impl Property {
         cache[i] = val;
         Ok(())
     }
-    pub fn set_cache_f32(&self, role: Role, i: usize, val: f32) -> Result<()> {
+    pub fn set_cache_f32(
+        self: Arc<Self>,
+        atom: &mut PropertyAtomicGuard,
+        role: Role,
+        i: usize,
+        val: f32,
+    ) -> Result<()> {
         self.set_cache(i, PropertyValue::Float32(val))?;
-        self.on_modify.notify((role, ModifyAction::SetCache(vec![i])));
+        atom.add(self, role, ModifyAction::SetCache(vec![i]));
         Ok(())
     }
-    pub fn set_cache_u32(&self, role: Role, i: usize, val: u32) -> Result<()> {
+    pub fn set_cache_u32(
+        self: Arc<Self>,
+        atom: &mut PropertyAtomicGuard,
+        role: Role,
+        i: usize,
+        val: u32,
+    ) -> Result<()> {
         self.set_cache(i, PropertyValue::Uint32(val))?;
-        self.on_modify.notify((role, ModifyAction::SetCache(vec![i])));
+        atom.add(self, role, ModifyAction::SetCache(vec![i]));
         Ok(())
     }
 
-    pub fn set_cache_f32_multi(&self, role: Role, changes: Vec<(usize, f32)>) -> Result<()> {
+    pub fn set_cache_f32_multi(
+        self: Arc<Self>,
+        atom: &mut PropertyAtomicGuard,
+        role: Role,
+        changes: Vec<(usize, f32)>,
+    ) -> Result<()> {
         let mut idxs = vec![];
         for (idx, val) in changes {
             self.set_cache(idx, PropertyValue::Float32(val))?;
             idxs.push(idx);
         }
-        self.on_modify.notify((role, ModifyAction::SetCache(idxs)));
+        atom.add(self, role, ModifyAction::SetCache(idxs));
         Ok(())
     }
-    pub fn set_cache_u32_range(&self, role: Role, changes: Vec<(usize, u32)>) -> Result<()> {
+    pub fn set_cache_u32_range(
+        self: Arc<Self>,
+        atom: &mut PropertyAtomicGuard,
+        role: Role,
+        changes: Vec<(usize, u32)>,
+    ) -> Result<()> {
         let mut idxs = vec![];
         for (idx, val) in changes {
             self.set_cache(idx, PropertyValue::Uint32(val))?;
             idxs.push(idx);
         }
-        self.on_modify.notify((role, ModifyAction::SetCache(idxs)));
+        atom.add(self, role, ModifyAction::SetCache(idxs));
         Ok(())
     }
 
     // Push
 
-    fn push_value(&self, role: Role, value: PropertyValue) -> Result<usize> {
+    fn push_value(
+        self: Arc<Self>,
+        atom: &mut PropertyAtomicGuard,
+        role: Role,
+        value: PropertyValue,
+    ) -> Result<usize> {
         if self.is_bounded() {
             return Err(Error::PropertyIsBounded)
         }
@@ -606,33 +635,62 @@ impl Property {
         vals.push(value);
         drop(vals);
 
-        self.on_modify.notify((role, ModifyAction::Push(i)));
+        atom.add(self, role, ModifyAction::Push(i));
         Ok(i)
     }
 
-    pub fn push_null(&self, role: Role) -> Result<usize> {
-        self.push_value(role, PropertyValue::Null)
+    pub fn push_null(self: Arc<Self>, atom: &mut PropertyAtomicGuard, role: Role) -> Result<usize> {
+        self.push_value(atom, role, PropertyValue::Null)
     }
-
-    pub fn push_bool(&self, role: Role, val: bool) -> Result<usize> {
-        self.push_value(role, PropertyValue::Bool(val))
+    pub fn push_bool(
+        self: Arc<Self>,
+        atom: &mut PropertyAtomicGuard,
+        role: Role,
+        val: bool,
+    ) -> Result<usize> {
+        self.push_value(atom, role, PropertyValue::Bool(val))
     }
-    pub fn push_u32(&self, role: Role, val: u32) -> Result<usize> {
+    pub fn push_u32(
+        self: Arc<Self>,
+        atom: &mut PropertyAtomicGuard,
+        role: Role,
+        val: u32,
+    ) -> Result<usize> {
         // TODO: none of these push calls are enforcing constraints that are required
         // see the set_XX calls.
-        self.push_value(role, PropertyValue::Uint32(val))
+        self.push_value(atom, role, PropertyValue::Uint32(val))
     }
-    pub fn push_f32(&self, role: Role, val: f32) -> Result<usize> {
-        self.push_value(role, PropertyValue::Float32(val))
+    pub fn push_f32(
+        self: Arc<Self>,
+        atom: &mut PropertyAtomicGuard,
+        role: Role,
+        val: f32,
+    ) -> Result<usize> {
+        self.push_value(atom, role, PropertyValue::Float32(val))
     }
-    pub fn push_str<S: Into<String>>(&self, role: Role, val: S) -> Result<usize> {
-        self.push_value(role, PropertyValue::Str(val.into()))
+    pub fn push_str<S: Into<String>>(
+        self: Arc<Self>,
+        atom: &mut PropertyAtomicGuard,
+        role: Role,
+        val: S,
+    ) -> Result<usize> {
+        self.push_value(atom, role, PropertyValue::Str(val.into()))
     }
-    pub fn push_enum<S: Into<String>>(&self, role: Role, val: S) -> Result<usize> {
-        self.push_value(role, PropertyValue::Enum(val.into()))
+    pub fn push_enum<S: Into<String>>(
+        self: Arc<Self>,
+        atom: &mut PropertyAtomicGuard,
+        role: Role,
+        val: S,
+    ) -> Result<usize> {
+        self.push_value(atom, role, PropertyValue::Enum(val.into()))
     }
-    pub fn push_node_id(&self, role: Role, val: SceneNodeId) -> Result<usize> {
-        self.push_value(role, PropertyValue::SceneNodeId(val))
+    pub fn push_node_id(
+        self: Arc<Self>,
+        atom: &mut PropertyAtomicGuard,
+        role: Role,
+        val: SceneNodeId,
+    ) -> Result<usize> {
+        self.push_value(atom, role, PropertyValue::SceneNodeId(val))
     }
 
     // Get
@@ -775,7 +833,7 @@ impl Property {
 
     // Subs
 
-    pub fn subscribe_modify(&self) -> Subscription<(Role, ModifyAction)> {
+    pub fn subscribe_modify(&self) -> Subscription<(Role, ModifyAction, BatchGuardPtr)> {
         self.on_modify.clone().subscribe()
     }
 

+ 9 - 3
bin/app/src/prop/wrap.rs

@@ -293,14 +293,20 @@ impl PropertyRect {
         Ok(Self { prop, role })
     }
 
-    pub fn eval(&self, parent_rect: &Rectangle) -> Result<()> {
+    pub fn eval(&self, atom: &mut PropertyAtomicGuard, parent_rect: &Rectangle) -> Result<()> {
         self.eval_with(
+            atom,
             (0..4).collect(),
             vec![("w".to_string(), parent_rect.w), ("h".to_string(), parent_rect.h)],
         )
     }
 
-    pub fn eval_with(&self, range: Vec<usize>, extras: Vec<(String, f32)>) -> Result<()> {
+    pub fn eval_with(
+        &self,
+        atom: &mut PropertyAtomicGuard,
+        range: Vec<usize>,
+        extras: Vec<(String, f32)>,
+    ) -> Result<()> {
         let mut globals = vec![];
 
         for dep in self.prop.get_depends() {
@@ -330,7 +336,7 @@ impl PropertyRect {
             let v = machine.call()?.as_f32()?;
             changes.push((i, v));
         }
-        self.prop.set_cache_f32_multi(self.role, changes).unwrap();
+        self.prop().set_cache_f32_multi(atom, self.role, changes).unwrap();
         Ok(())
     }
 

+ 2 - 2
bin/app/src/ui/button.rs

@@ -77,9 +77,9 @@ impl UIObject for Button {
         &self,
         parent_rect: Rectangle,
         _trace_id: u32,
-        _atom: &mut PropertyAtomicGuard,
+        atom: &mut PropertyAtomicGuard,
     ) -> Option<DrawUpdate> {
-        let _ = self.rect.eval(&parent_rect);
+        let _ = self.rect.eval(atom, &parent_rect);
         None
     }
 

+ 60 - 49
bin/app/src/ui/chatedit.rs

@@ -40,8 +40,8 @@ use crate::{
     },
     mesh::MeshBuilder,
     prop::{
-        PropertyAtomicGuard, PropertyBool, PropertyColor, PropertyFloat32, PropertyPtr,
-        PropertyRect, PropertyStr, PropertyUint32, Role,
+        BatchGuardId, BatchGuardPtr, PropertyAtomicGuard, PropertyBool, PropertyColor,
+        PropertyFloat32, PropertyPtr, PropertyRect, PropertyStr, PropertyUint32, Role,
     },
     scene::{MethodCallSub, Pimpl, SceneNodePtr, SceneNodeWeak},
     text2::{self, Editor},
@@ -492,14 +492,15 @@ impl ChatEdit {
         mesh.append(verts, indices);
     }
 
-    async fn change_focus(self: Arc<Self>) {
+    async fn change_focus(self: Arc<Self>, batch: BatchGuardPtr) {
         if !self.is_active.get() {
             return
         }
         t!("Focus changed");
 
+        let atom = &mut batch.spawn();
         // Cursor visibility will change so just redraw everything lol
-        self.redraw().await;
+        self.redraw(atom).await;
     }
 
     async fn handle_shortcut(
@@ -549,7 +550,7 @@ impl ChatEdit {
             _ => return false,
         }
 
-        self.redraw().await;
+        self.redraw(atom).await;
         true
     }
 
@@ -678,7 +679,7 @@ impl ChatEdit {
 
         self.apply_cursor_scrolling(atom).await;
         self.pause_blinking();
-        self.redraw().await;
+        self.redraw(atom).await;
 
         return true
     }
@@ -788,7 +789,7 @@ impl ChatEdit {
 
     async fn handle_touch_move(&self, mut touch_pos: Point) -> bool {
         //t!("handle_touch_move({touch_pos:?})");
-        let atom = &mut PropertyAtomicGuard::new();
+        let atom = &mut self.render_api.make_guard();
         // We must update with non relative touch_pos bcos when doing vertical scrolling
         // we will modify the scroll, which is used by abs_to_local(), which is used
         // to then calculate the max scroll. So it ends up jumping around.
@@ -806,7 +807,7 @@ impl ChatEdit {
                 } else {
                     self.abs_to_local(&mut touch_pos);
                     self.start_touch_select(touch_pos, atom).await;
-                    self.redraw_select().await;
+                    self.redraw_select(atom.batch_id).await;
                 }
                 d!("touch state: StartSelect -> Select");
                 self.touch_info.lock().state = TouchStateAction::Select;
@@ -852,7 +853,7 @@ impl ChatEdit {
                 editor.set_selection(select.start, select.end);
                 drop(editor);
 
-                self.redraw_select().await;
+                self.redraw_select(atom.batch_id).await;
             }
             TouchStateAction::ScrollVert { start_pos, scroll_start } => {
                 let y_dist = start_pos.y - touch_pos.y;
@@ -862,7 +863,7 @@ impl ChatEdit {
                     return true
                 }
                 self.scroll.set(atom, scroll);
-                self.redraw_scroll().await;
+                self.redraw_scroll(atom.batch_id).await;
             }
             TouchStateAction::SetCursorPos => {
                 // TBH I can't even see the cursor under my thumb so I'll just
@@ -872,7 +873,7 @@ impl ChatEdit {
         }
         true
     }
-    async fn handle_touch_end(&self, mut touch_pos: Point) -> bool {
+    async fn handle_touch_end(&self, atom: &mut PropertyAtomicGuard, mut touch_pos: Point) -> bool {
         //t!("handle_touch_end({touch_pos:?})");
         self.abs_to_local(&mut touch_pos);
 
@@ -880,8 +881,8 @@ impl ChatEdit {
         match state {
             TouchStateAction::Inactive => return false,
             TouchStateAction::Started { pos: _, instant: _ } | TouchStateAction::SetCursorPos => {
-                self.touch_set_cursor_pos(touch_pos).await;
-                self.redraw().await;
+                self.touch_set_cursor_pos(atom, touch_pos).await;
+                self.redraw(atom).await;
             }
             _ => {}
         }
@@ -891,7 +892,7 @@ impl ChatEdit {
         true
     }
 
-    async fn touch_set_cursor_pos(&self, touch_pos: Point) {
+    async fn touch_set_cursor_pos(&self, atom: &mut PropertyAtomicGuard, touch_pos: Point) {
         t!("touch_set_cursor_pos({touch_pos:?})");
 
         let mut editor = self.lock_editor().await;
@@ -900,7 +901,7 @@ impl ChatEdit {
         drop(editor);
 
         self.pause_blinking();
-        self.finish_select(&mut PropertyAtomicGuard::new());
+        self.finish_select(atom);
     }
 
     fn finish_select(&self, atom: &mut PropertyAtomicGuard) {
@@ -937,16 +938,15 @@ impl ChatEdit {
         self.cursor_is_visible.store(true, Ordering::Relaxed);
     }
 
-    async fn redraw(&self) {
-        let atom = &mut PropertyAtomicGuard::new();
+    async fn redraw(&self, atom: &mut PropertyAtomicGuard) {
         let trace_id = rand::random();
         let timest = unixtime();
         let draw_update = self.make_draw_calls(trace_id, atom).await;
-        self.render_api.replace_draw_calls(timest, draw_update.draw_calls);
+        self.render_api.replace_draw_calls(atom.batch_id, timest, draw_update.draw_calls);
     }
 
     /// Called when scroll changes. Moves content up or down. Nothing more.
-    async fn redraw_scroll(&self) {
+    async fn redraw_scroll(&self, batch_id: BatchGuardId) {
         let timest = unixtime();
         let rect = self.rect.get();
         let scroll = self.scroll.get();
@@ -975,18 +975,18 @@ impl ChatEdit {
                 GfxDrawCall::new(phone_sel_instrs, vec![], 1, "chatedit_phone_sel_scroll"),
             ),
         ];
-        self.render_api.replace_draw_calls(timest, draw_main);
+        self.render_api.replace_draw_calls(batch_id, timest, draw_main);
     }
 
-    async fn redraw_cursor(&self) {
+    async fn redraw_cursor(&self, batch_id: BatchGuardId) {
         let timest = unixtime();
         let instrs = self.get_cursor_instrs().await;
         let draw_calls =
             vec![(self.cursor_dc_key, GfxDrawCall::new(instrs, vec![], 2, "curs_redr"))];
-        self.render_api.replace_draw_calls(timest, draw_calls);
+        self.render_api.replace_draw_calls(batch_id, timest, draw_calls);
     }
 
-    async fn redraw_select(&self) {
+    async fn redraw_select(&self, batch_id: BatchGuardId) {
         let timest = unixtime();
         let sel_instrs = self.regen_select_mesh().await;
         let phone_sel_instrs = self.regen_phone_select_handle_mesh().await;
@@ -997,7 +997,7 @@ impl ChatEdit {
                 GfxDrawCall::new(phone_sel_instrs, vec![], 1, "chatedit_phone_sel_redraw_sel"),
             ),
         ];
-        self.render_api.replace_draw_calls(timest, draw_calls);
+        self.render_api.replace_draw_calls(batch_id, timest, draw_calls);
     }
 
     async fn get_cursor_instrs(&self) -> Vec<GfxDrawInstruction> {
@@ -1104,6 +1104,7 @@ impl ChatEdit {
         // First we evaluate the width based off the parent dimensions
         self.rect
             .eval_with(
+                atom,
                 vec![2],
                 vec![
                     ("parent_w".to_string(), parent_rect.w),
@@ -1128,6 +1129,7 @@ impl ChatEdit {
         // Finally calculate the position
         self.rect
             .eval_with(
+                atom,
                 vec![0, 1],
                 vec![
                     ("parent_w".to_string(), parent_rect.w),
@@ -1238,9 +1240,9 @@ impl ChatEdit {
             panic!("self destroyed before insert_text_method_task was stopped!");
         };
 
-        let atom = &mut PropertyAtomicGuard::new();
+        let atom = &mut self_.render_api.make_guard();
         self_.insert(&text, atom).await;
-        self_.redraw().await;
+        self_.redraw(atom).await;
         true
     }
 
@@ -1337,8 +1339,12 @@ impl ChatEdit {
 
 impl Drop for ChatEdit {
     fn drop(&mut self) {
-        self.render_api
-            .replace_draw_calls(unixtime(), vec![(self.text_dc_key, Default::default())]);
+        let atom = self.render_api.make_guard();
+        self.render_api.replace_draw_calls(
+            atom.batch_id,
+            unixtime(),
+            vec![(self.text_dc_key, Default::default())],
+        );
     }
 }
 
@@ -1387,18 +1393,20 @@ impl UIObject for ChatEdit {
 
         // When text has been changed.
         // Cursor and selection might be invalidated.
-        async fn reset(self_: Arc<ChatEdit>) {
-            let atom = &mut PropertyAtomicGuard::new();
+        async fn reset(self_: Arc<ChatEdit>, batch: BatchGuardPtr) {
+            let atom = &mut batch.spawn();
             //self_.select_text.set_null(Role::Internal, 0).unwrap();
             self_.scroll.set(atom, 0.);
-            self_.redraw().await;
+            self_.redraw(atom).await;
         }
-        async fn redraw(self_: Arc<ChatEdit>) {
-            self_.redraw().await;
+        async fn redraw(self_: Arc<ChatEdit>, batch: BatchGuardPtr) {
+            let atom = &mut batch.spawn();
+            self_.redraw(atom).await;
         }
-        async fn set_text(self_: Arc<ChatEdit>) {
+        async fn set_text(self_: Arc<ChatEdit>, batch: BatchGuardPtr) {
             self_.lock_editor().await.on_text_prop_changed().await;
-            self_.redraw().await;
+            let atom = &mut batch.spawn();
+            self_.redraw(atom).await;
         }
 
         on_modify.when_change(self.rect.prop(), redraw);
@@ -1423,7 +1431,7 @@ impl UIObject for ChatEdit {
         on_modify.when_change(self.z_index.prop(), redraw);
         on_modify.when_change(self.debug.prop(), redraw);
 
-        async fn regen_cursor(self_: Arc<ChatEdit>) {
+        async fn regen_cursor(self_: Arc<ChatEdit>, _batch: BatchGuardPtr) {
             // Free the cache
             *self_.cursor_mesh.lock() = None;
         }
@@ -1452,7 +1460,8 @@ impl UIObject for ChatEdit {
 
                 // Invert the bool
                 self_.cursor_is_visible.fetch_not(Ordering::Relaxed);
-                self_.redraw_cursor().await;
+                let atom = &mut self_.render_api.make_guard();
+                self_.redraw_cursor(atom.batch_id).await;
             }
         });
 
@@ -1523,7 +1532,7 @@ impl UIObject for ChatEdit {
             repeater.key_down(PressedKey::Char(key), repeat)
         };
 
-        let atom = &mut PropertyAtomicGuard::new();
+        let atom = &mut self.render_api.make_guard();
 
         if mods.ctrl || mods.alt || mods.logo {
             if repeat {
@@ -1540,7 +1549,7 @@ impl UIObject for ChatEdit {
         t!("Key {:?} has {} actions", key, actions);
         let key_str = key.to_string().repeat(actions as usize);
         self.insert(&key_str, atom).await;
-        self.redraw().await;
+        self.redraw(atom).await;
         true
     }
 
@@ -1566,7 +1575,7 @@ impl UIObject for ChatEdit {
             t!("Key {:?} has {} actions", key, actions);
         }
 
-        let atom = &mut PropertyAtomicGuard::new();
+        let atom = &mut self.render_api.make_guard();
 
         let mut is_handled = false;
         for _ in 0..actions {
@@ -1598,7 +1607,7 @@ impl UIObject for ChatEdit {
             return false
         }
 
-        let atom = &mut PropertyAtomicGuard::new();
+        let atom = &mut self.render_api.make_guard();
 
         // clicking inside box will:
         // 1. make it active
@@ -1627,7 +1636,7 @@ impl UIObject for ChatEdit {
         self.mouse_btn_held.store(true, Ordering::Relaxed);
 
         self.pause_blinking();
-        self.redraw().await;
+        self.redraw(atom).await;
         true
     }
 
@@ -1653,7 +1662,7 @@ impl UIObject for ChatEdit {
             return false
         }
 
-        let atom = &mut PropertyAtomicGuard::new();
+        let atom = &mut self.render_api.make_guard();
 
         // if active and selection_active, then use x to modify the selection.
         // also implement scrolling when cursor is to the left or right
@@ -1679,9 +1688,9 @@ impl UIObject for ChatEdit {
 
         self.pause_blinking();
         self.apply_cursor_scrolling(atom).await;
-        self.redraw_scroll().await;
-        self.redraw_cursor().await;
-        self.redraw_select().await;
+        self.redraw_scroll(atom.batch_id).await;
+        self.redraw_cursor(atom.batch_id).await;
+        self.redraw_select(atom.batch_id).await;
         true
     }
 
@@ -1690,13 +1699,13 @@ impl UIObject for ChatEdit {
             return false
         }
 
-        let atom = &mut PropertyAtomicGuard::new();
+        let atom = &mut self.render_api.make_guard();
 
         let mut scroll = self.scroll.get() - wheel_pos.y * self.scroll_speed.get();
         scroll = scroll.clamp(0., self.max_scroll());
         t!("handle_mouse_wheel({wheel_pos:?}) [scroll={scroll}]");
         self.scroll.set(atom, scroll);
-        self.redraw_scroll().await;
+        self.redraw_scroll(atom.batch_id).await;
 
         true
     }
@@ -1711,10 +1720,12 @@ impl UIObject for ChatEdit {
             return false
         }
 
+        let atom = &mut self.render_api.make_guard();
+
         match phase {
             TouchPhase::Started => self.handle_touch_start(touch_pos).await,
             TouchPhase::Moved => self.handle_touch_move(touch_pos).await,
-            TouchPhase::Ended => self.handle_touch_end(touch_pos).await,
+            TouchPhase::Ended => self.handle_touch_end(atom, touch_pos).await,
             TouchPhase::Cancelled => false,
         }
     }

+ 41 - 24
bin/app/src/ui/chatview/mod.rs

@@ -40,8 +40,8 @@ use page::MessageBuffer;
 use crate::{
     gfx::{GfxDrawCall, GfxDrawInstruction, Point, Rectangle, RenderApi},
     prop::{
-        PropertyAtomicGuard, PropertyBool, PropertyColor, PropertyFloat32, PropertyRect,
-        PropertyUint32, Role,
+        BatchGuardId, BatchGuardPtr, PropertyAtomicGuard, PropertyBool, PropertyColor,
+        PropertyFloat32, PropertyRect, PropertyUint32, Role,
     },
     scene::{MethodCallSub, Pimpl, SceneNodeWeak},
     text::TextShaperPtr,
@@ -340,7 +340,7 @@ impl ChatView {
     }
 
     /// Mark line as selected
-    async fn select_line(&self, mut y: f32) {
+    async fn select_line(&self, batch_id: BatchGuardId, mut y: f32) {
         let trace_id = rand::random();
         t!("select_line({y}) [trace_id={trace_id}]");
         // The cursor is inside the rect. We just have to find which line it clicked.
@@ -356,7 +356,7 @@ impl ChatView {
         let mut msgbuf = self.msgbuf.lock().await;
         msgbuf.select_line(y).await;
 
-        self.redraw_cached(&mut msgbuf, trace_id).await;
+        self.redraw_cached(batch_id, &mut msgbuf, trace_id).await;
     }
 
     fn end_touch_phase(&self, touch_y: f32) {
@@ -454,7 +454,8 @@ impl ChatView {
             }
         }
 
-        self.redraw_cached(&mut msgbuf, trace_id).await;
+        let atom = self.render_api.make_guard();
+        self.redraw_cached(atom.batch_id, &mut msgbuf, trace_id).await;
         self.bgload_cv.notify();
     }
     async fn handle_insert_unconf_line(
@@ -473,7 +474,8 @@ impl ChatView {
         let mut msgbuf = self.msgbuf.lock().await;
         let Some(privmsg) = msgbuf.insert_privmsg(timest, msg_id, nick, text) else { return };
         privmsg.confirmed = false;
-        self.redraw_cached(&mut msgbuf, trace_id).await;
+        let atom = self.render_api.make_guard();
+        self.redraw_cached(atom.batch_id, &mut msgbuf, trace_id).await;
         self.bgload_cv.notify();
     }
 
@@ -588,7 +590,8 @@ impl ChatView {
         }
         t!("do_redraw = {do_redraw} [trace_id={trace_id}]");
         if do_redraw {
-            self.redraw_cached(&mut msgbuf, trace_id).await;
+            let atom = self.render_api.make_guard();
+            self.redraw_cached(atom.batch_id, &mut msgbuf, trace_id).await;
         }
     }
 
@@ -607,7 +610,7 @@ impl ChatView {
         }
 
         // 2/3 of time spent here  ~3.3ms
-        self.redraw_cached(&mut msgbuf, trace_id).await;
+        self.redraw_cached(atom.batch_id, &mut msgbuf, trace_id).await;
 
         self.scroll.set(atom, scroll);
         self.bgload_cv.notify();
@@ -679,7 +682,12 @@ impl ChatView {
         instrs
     }
 
-    async fn redraw_cached(&self, msgbuf: &mut MessageBuffer, trace_id: u32) {
+    async fn redraw_cached(
+        &self,
+        batch_id: BatchGuardId,
+        msgbuf: &mut MessageBuffer,
+        trace_id: u32,
+    ) {
         t!("ChatView::redraw_cached() [trace_id={trace_id}]");
         let timest = unixtime();
         let rect = self.rect.get();
@@ -692,21 +700,21 @@ impl ChatView {
         let draw_calls =
             vec![(self.dc_key, GfxDrawCall::new(instrs, vec![], self.z_index.get(), "chatview"))];
 
-        self.render_api.replace_draw_calls(timest, draw_calls);
+        self.render_api.replace_draw_calls(batch_id, timest, draw_calls);
         t!("ChatView::redraw_cached() DONE [trace_id={trace_id}]");
     }
 
     /// Invalidates cache and redraws everything
-    async fn redraw_all(&self) {
+    async fn redraw_all(&self, atom: &mut PropertyAtomicGuard) {
         let trace_id = rand::random();
         t!("ChatView::redraw_all() [trace_id={trace_id}]");
         let parent_rect = self.parent_rect.lock().unwrap().clone();
-        self.rect.eval(&parent_rect).expect("unable to eval rect");
+        self.rect.eval(atom, &parent_rect).expect("unable to eval rect");
 
         let mut msgbuf = self.msgbuf.lock().await;
         msgbuf.adjust_params();
         msgbuf.clear_meshes();
-        self.redraw_cached(&mut msgbuf, trace_id).await;
+        self.redraw_cached(atom.batch_id, &mut msgbuf, trace_id).await;
         t!("ChatView::redraw_all() DONE [trace_id={trace_id}]");
     }
 }
@@ -744,7 +752,7 @@ impl UIObject for ChatView {
                     // Should not happen
                     panic!("self destroyed before motion_task was stopped!");
                 };
-                let atom = &mut PropertyAtomicGuard::new();
+                let atom = &mut self_.render_api.make_guard();
                 self_.handle_movement(atom).await;
                 cv.reset();
             }
@@ -766,17 +774,18 @@ impl UIObject for ChatView {
 
         let mut on_modify = OnModify::new(ex, self.node.clone(), me.clone());
 
-        async fn reload_view(self_: Arc<ChatView>) {
-            let atom = &mut PropertyAtomicGuard::new();
+        async fn reload_view(self_: Arc<ChatView>, batch: BatchGuardPtr) {
+            let atom = &mut batch.spawn();
             self_.scrollview(self_.scroll.get(), atom).await;
         }
         on_modify.when_change(self.scroll.prop(), reload_view);
 
-        async fn redraw(self_: Arc<ChatView>) {
+        async fn redraw(self_: Arc<ChatView>, batch: BatchGuardPtr) {
             if !self_.rect.has_cached() {
                 return
             }
-            self_.redraw_all().await;
+            let atom = &mut batch.spawn();
+            self_.redraw_all(atom).await;
         }
 
         //on_modify.when_change(self.baseline.prop(), redraw);
@@ -815,7 +824,7 @@ impl UIObject for ChatView {
         t!("ChatView::draw({:?}, {trace_id})", self.node.upgrade().unwrap());
 
         *self.parent_rect.lock() = Some(parent_rect.clone());
-        self.rect.eval(&parent_rect).ok()?;
+        self.rect.eval(atom, &parent_rect).ok()?;
         let rect = self.rect.get();
 
         let mut msgbuf = self.msgbuf.lock().await;
@@ -877,8 +886,10 @@ impl UIObject for ChatView {
             return false
         }
 
+        let atom = self.render_api.make_guard();
+
         if ENABLE_SELECT {
-            self.select_line(mouse_pos.y).await;
+            self.select_line(atom.batch_id, mouse_pos.y).await;
         }
         self.mouse_btn_held.store(true, Ordering::Relaxed);
         true
@@ -911,7 +922,8 @@ impl UIObject for ChatView {
         }
 
         if ENABLE_SELECT {
-            self.select_line(mouse_pos.y).await;
+            let atom = &mut self.render_api.make_guard();
+            self.select_line(atom.batch_id, mouse_pos.y).await;
         }
         false
     }
@@ -939,7 +951,7 @@ impl UIObject for ChatView {
 
         let rect = self.rect.get();
         t!("handle_touch({phase:?}, {id},{id},  {touch_pos:?})");
-        let atom = &mut PropertyAtomicGuard::new();
+        let atom = &mut self.render_api.make_guard();
 
         let touch_y = touch_pos.y;
 
@@ -1012,7 +1024,7 @@ impl UIObject for ChatView {
                 // We are in selection mode so don't scroll the screen until touch phase ends.
                 if is_select_mode == Some(true) {
                     if ENABLE_SELECT {
-                        self.select_line(touch_y).await;
+                        self.select_line(atom.batch_id, touch_y).await;
                     }
                     return true
                 }
@@ -1036,6 +1048,11 @@ impl UIObject for ChatView {
 
 impl Drop for ChatView {
     fn drop(&mut self) {
-        self.render_api.replace_draw_calls(unixtime(), vec![(self.dc_key, Default::default())]);
+        let atom = self.render_api.make_guard();
+        self.render_api.replace_draw_calls(
+            atom.batch_id,
+            unixtime(),
+            vec![(self.dc_key, Default::default())],
+        );
     }
 }

+ 19 - 12
bin/app/src/ui/emoji_picker/mod.rs

@@ -28,7 +28,9 @@ use std::sync::{
 
 use crate::{
     gfx::{GfxDrawCall, GfxDrawInstruction, Point, Rectangle, RenderApi},
-    prop::{PropertyAtomicGuard, PropertyFloat32, PropertyRect, PropertyUint32, Role},
+    prop::{
+        BatchGuardPtr, PropertyAtomicGuard, PropertyFloat32, PropertyRect, PropertyUint32, Role,
+    },
     scene::{Pimpl, SceneNodeWeak},
     util::unixtime,
     ExecutorPtr,
@@ -182,8 +184,7 @@ impl EmojiPicker {
         }
     }
 
-    fn redraw(&self) {
-        let atom = &mut PropertyAtomicGuard::new();
+    fn redraw(&self, atom: &mut PropertyAtomicGuard) {
         let trace_id = rand::random();
         let timest = unixtime();
         t!("redraw({:?}) [timest={timest}, trace_id={trace_id}]", self.node.upgrade().unwrap());
@@ -193,7 +194,7 @@ impl EmojiPicker {
             error!(target: "ui::emoji_picker", "Emoji picker failed to draw");
             return
         };
-        self.render_api.replace_draw_calls(timest, draw_update.draw_calls);
+        self.render_api.replace_draw_calls(atom.batch_id, timest, draw_update.draw_calls);
         t!("redraw DONE [trace_id={trace_id}]");
     }
 
@@ -203,7 +204,7 @@ impl EmojiPicker {
         _trace_id: u32,
         atom: &mut PropertyAtomicGuard,
     ) -> Option<DrawUpdate> {
-        if let Err(e) = self.rect.eval(&parent_rect) {
+        if let Err(e) = self.rect.eval(atom, &parent_rect) {
             warn!(target: "ui::emoji_picker", "Rect eval failed: {e}");
             return None
         }
@@ -264,8 +265,9 @@ impl UIObject for EmojiPicker {
     async fn start(self: Arc<Self>, ex: ExecutorPtr) {
         let me = Arc::downgrade(&self);
 
-        async fn redraw(self_: Arc<EmojiPicker>) {
-            self_.redraw();
+        async fn redraw(self_: Arc<EmojiPicker>, batch: BatchGuardPtr) {
+            let atom = &mut batch.spawn();
+            self_.redraw(atom);
         }
 
         let mut on_modify = OnModify::new(ex, self.node.clone(), me.clone());
@@ -302,14 +304,14 @@ impl UIObject for EmojiPicker {
             return false
         }
         t!("handle_mouse_wheel()");
-        let atom = &mut PropertyAtomicGuard::new();
+        let atom = &mut self.render_api.make_guard();
 
         let mut scroll = self.scroll.get();
         scroll -= self.mouse_scroll_speed.get() * wheel_pos.y;
         scroll = scroll.clamp(0., self.max_scroll());
         self.scroll.set(atom, scroll);
 
-        self.redraw();
+        self.redraw(atom);
 
         true
     }
@@ -332,7 +334,7 @@ impl UIObject for EmojiPicker {
             return false
         }
 
-        let atom = &mut PropertyAtomicGuard::new();
+        let atom = &mut self.render_api.make_guard();
 
         let rect = self.rect.get();
         let pos = touch_pos - Point::new(rect.x, rect.y);
@@ -365,7 +367,7 @@ impl UIObject for EmojiPicker {
                             let mut scroll = touch_info.start_scroll + y_diff;
                             scroll = scroll.clamp(0., self.max_scroll());
                             self.scroll.set(atom, scroll);
-                            self.redraw();
+                            self.redraw(atom);
                         }
                     } else {
                         return false
@@ -393,6 +395,11 @@ impl UIObject for EmojiPicker {
 
 impl Drop for EmojiPicker {
     fn drop(&mut self) {
-        self.render_api.replace_draw_calls(unixtime(), vec![(self.dc_key, Default::default())]);
+        let atom = self.render_api.make_guard();
+        self.render_api.replace_draw_calls(
+            atom.batch_id,
+            unixtime(),
+            vec![(self.dc_key, Default::default())],
+        );
     }
 }

+ 22 - 12
bin/app/src/ui/image.rs

@@ -28,7 +28,7 @@ use crate::{
         RenderApi,
     },
     mesh::{MeshBuilder, MeshInfo, COLOR_WHITE},
-    prop::{PropertyAtomicGuard, PropertyRect, PropertyStr, PropertyUint32, Role},
+    prop::{BatchGuardPtr, PropertyAtomicGuard, PropertyRect, PropertyStr, PropertyUint32, Role},
     scene::{Pimpl, SceneNodeWeak},
     util::unixtime,
     ExecutorPtr,
@@ -88,11 +88,11 @@ impl Image {
         Pimpl::Image(self_)
     }
 
-    async fn reload(self: Arc<Self>) {
+    async fn reload(self: Arc<Self>, batch: BatchGuardPtr) {
         let texture = self.load_texture();
         *self.texture.lock() = Some(texture);
 
-        self.clone().redraw().await;
+        self.clone().redraw(batch).await;
     }
 
     fn load_texture(&self) -> ManagedTexturePtr {
@@ -122,17 +122,18 @@ impl Image {
         self.render_api.new_texture(width, height, bmp, gfxtag!("img"))
     }
 
-    async fn redraw(self: Arc<Self>) {
+    async fn redraw(self: Arc<Self>, batch: BatchGuardPtr) {
         let trace: DrawTrace = rand::random();
         let timest = unixtime();
         t!("redraw({:?}) [trace={trace}]", self.node.upgrade().unwrap());
         let Some(parent_rect) = self.parent_rect.lock().clone() else { return };
 
-        let Some(draw_update) = self.get_draw_calls(parent_rect).await else {
+        let atom = &mut batch.spawn();
+        let Some(draw_update) = self.get_draw_calls(atom, parent_rect).await else {
             error!(target: "ui::image", "Image failed to draw");
             return
         };
-        self.render_api.replace_draw_calls(timest, draw_update.draw_calls);
+        self.render_api.replace_draw_calls(batch.id, timest, draw_update.draw_calls);
         t!("redraw() DONE [trace={trace}]");
     }
 
@@ -146,10 +147,14 @@ impl Image {
         mesh.alloc(&self.render_api)
     }
 
-    async fn get_draw_calls(&self, parent_rect: Rectangle) -> Option<DrawUpdate> {
-        self.rect.eval(&parent_rect).ok()?;
+    async fn get_draw_calls(
+        &self,
+        atom: &mut PropertyAtomicGuard,
+        parent_rect: Rectangle,
+    ) -> Option<DrawUpdate> {
+        self.rect.eval(atom, &parent_rect).ok()?;
         let rect = self.rect.get();
-        self.uv.eval(&rect).ok()?;
+        self.uv.eval(atom, &rect).ok()?;
 
         let mesh = self.regen_mesh();
         let texture = self.texture.lock().clone().expect("Node missing texture_id!");
@@ -208,16 +213,21 @@ impl UIObject for Image {
         &self,
         parent_rect: Rectangle,
         trace: DrawTrace,
-        _atom: &mut PropertyAtomicGuard,
+        atom: &mut PropertyAtomicGuard,
     ) -> Option<DrawUpdate> {
         t!("Image::draw() [trace={trace}]");
         *self.parent_rect.lock() = Some(parent_rect);
-        self.get_draw_calls(parent_rect).await
+        self.get_draw_calls(atom, parent_rect).await
     }
 }
 
 impl Drop for Image {
     fn drop(&mut self) {
-        self.render_api.replace_draw_calls(unixtime(), vec![(self.dc_key, Default::default())]);
+        let atom = self.render_api.make_guard();
+        self.render_api.replace_draw_calls(
+            atom.batch_id,
+            unixtime(),
+            vec![(self.dc_key, Default::default())],
+        );
     }
 }

+ 5 - 5
bin/app/src/ui/layer.rs

@@ -24,7 +24,7 @@ use std::sync::Arc;
 
 use crate::{
     gfx::{GfxDrawCall, GfxDrawInstruction, Point, Rectangle, RenderApi},
-    prop::{PropertyAtomicGuard, PropertyBool, PropertyRect, PropertyUint32, Role},
+    prop::{BatchGuardPtr, PropertyAtomicGuard, PropertyBool, PropertyRect, PropertyUint32, Role},
     scene::{Pimpl, SceneNodePtr, SceneNodeWeak},
     util::{i18n::I18nBabelFish, unixtime},
     ExecutorPtr,
@@ -83,18 +83,18 @@ impl Layer {
         get_children_ordered(&node)
     }
 
-    async fn redraw(self: Arc<Self>) {
-        let atom = &mut PropertyAtomicGuard::new();
+    async fn redraw(self: Arc<Self>, batch: BatchGuardPtr) {
         let trace_id = rand::random();
         let timest = unixtime();
         t!("Layer::redraw({:?}) [trace_id={trace_id}]", self.node.upgrade().unwrap());
         let Some(parent_rect) = self.parent_rect.lock().clone() else { return };
 
+        let atom = &mut batch.spawn();
         let Some(draw_update) = self.get_draw_calls(parent_rect, trace_id, atom).await else {
             error!(target: "ui::layer", "Layer failed to draw [trace_id={trace_id}]");
             return
         };
-        self.render_api.replace_draw_calls(timest, draw_update.draw_calls);
+        self.render_api.replace_draw_calls(batch.id, timest, draw_update.draw_calls);
         t!(
             "Layer::redraw({:?}) DONE [timest={timest}, trace_id={trace_id}]",
             self.node.upgrade().unwrap()
@@ -107,7 +107,7 @@ impl Layer {
         trace_id: u32,
         atom: &mut PropertyAtomicGuard,
     ) -> Option<DrawUpdate> {
-        self.rect.eval(&parent_rect).ok()?;
+        self.rect.eval(atom, &parent_rect).ok()?;
         let rect = self.rect.get();
         t!("Layer::get_draw_calls() [rect={rect:?}, dc={}, trace_id={trace_id}]", self.dc_key);
 

+ 12 - 9
bin/app/src/ui/mod.rs

@@ -23,7 +23,7 @@ use std::sync::{Arc, Weak};
 
 use crate::{
     gfx::{GfxDrawCall, Point, Rectangle},
-    prop::{ModifyAction, PropertyAtomicGuard, PropertyPtr, Role},
+    prop::{BatchGuardPtr, ModifyAction, PropertyAtomicGuard, PropertyPtr, Role},
     scene::{Pimpl, SceneNode as SceneNode3, SceneNodePtr, SceneNodeWeak},
     util::i18n::I18nBabelFish,
     ExecutorPtr,
@@ -129,8 +129,11 @@ impl<T: Send + Sync + 'static> OnModify<T> {
         Self { ex, node, me, tasks: vec![] }
     }
 
-    pub fn when_change<F>(&mut self, prop: PropertyPtr, f: impl Fn(Arc<T>) -> F + Send + 'static)
-    where
+    pub fn when_change<F>(
+        &mut self,
+        prop: PropertyPtr,
+        f: impl Fn(Arc<T>, BatchGuardPtr) -> F + Send + 'static,
+    ) where
         F: std::future::Future<Output = ()> + Send + 'static,
     {
         let mut on_modify_subs = vec![(Arc::downgrade(&prop), None, prop.subscribe_modify())];
@@ -146,24 +149,24 @@ impl<T: Send + Sync + 'static> OnModify<T> {
                 for (i, (prop_weak, prop_i, on_modify_sub)) in on_modify_subs.iter().enumerate() {
                     let recv = on_modify_sub.receive();
                     poll_queues.push(async move {
-                        let (role, action) = recv.await.ok()?;
-                        Some((i, prop_weak, prop_i, role, action))
+                        let (role, action, batch_guard) = recv.await.ok()?;
+                        Some((i, prop_weak, prop_i, role, action, batch_guard))
                     });
                 }
 
-                let Some(Some((idx, prop_weak, prop_i, role, action))) = poll_queues.next().await else {
+                let Some(Some((idx, prop_weak, prop_i, role, action, batch_guard))) = poll_queues.next().await else {
                     e!("Property {:?} on_modify pipe is broken", prop);
                     return
                 };
 
                 // Skip internal messages from ourselves or explicitly marked ignored
-                if (idx == 0 && role == Role::Internal) || role == Role::Ignored{
+                if (idx == 0 && role == Role::Internal) || role == Role::Ignored {
                     continue
                 }
                 if let Some(prop_i) = prop_i {
                     match action {
                         ModifyAction::Set(i) => if *prop_i != i { continue },
-                        ModifyAction::SetCache(idxs) => if !idxs.contains(prop_i) { continue },
+                        ModifyAction::SetCache(idxs) => if !idxs.contains(prop_i) { continue }
                         _ => continue
                     }
                 }
@@ -184,7 +187,7 @@ impl<T: Send + Sync + 'static> OnModify<T> {
                 };
 
                 //debug!(target: "app", "property modified");
-                f(self_).await;
+                f(self_, batch_guard).await;
             }
         });
         self.tasks.push(task);

+ 20 - 10
bin/app/src/ui/text.rs

@@ -24,8 +24,8 @@ use std::sync::Arc;
 use crate::{
     gfx::{gfxtag, GfxDrawCall, GfxDrawInstruction, Rectangle, RenderApi},
     prop::{
-        PropertyAtomicGuard, PropertyBool, PropertyColor, PropertyFloat32, PropertyRect,
-        PropertyStr, PropertyUint32, Role,
+        BatchGuardPtr, PropertyAtomicGuard, PropertyBool, PropertyColor, PropertyFloat32,
+        PropertyRect, PropertyStr, PropertyUint32, Role,
     },
     scene::{Pimpl, SceneNodeWeak},
     text2::{self, TEXT_CTX},
@@ -136,22 +136,27 @@ impl Text {
         text2::render_layout_with_opts(&layout, debug_opts, &self.render_api, gfxtag!("text"))
     }
 
-    async fn redraw(self: Arc<Self>) {
+    async fn redraw(self: Arc<Self>, batch: BatchGuardPtr) {
         let trace: DrawTrace = rand::random();
         let timest = unixtime();
         t!("Text::redraw({:?}) [trace={trace}]", self.node.upgrade().unwrap());
         let Some(parent_rect) = self.parent_rect.lock().clone() else { return };
 
-        let Some(draw_update) = self.get_draw_calls(parent_rect).await else {
+        let atom = &mut batch.spawn();
+        let Some(draw_update) = self.get_draw_calls(atom, parent_rect).await else {
             error!(target: "ui::text", "Text failed to draw [trace={trace}]");
             return
         };
-        self.render_api.replace_draw_calls(timest, draw_update.draw_calls);
+        self.render_api.replace_draw_calls(batch.id, timest, draw_update.draw_calls);
         t!("Text::redraw() DONE [trace={trace}]");
     }
 
-    async fn get_draw_calls(&self, parent_rect: Rectangle) -> Option<DrawUpdate> {
-        self.rect.eval(&parent_rect).ok()?;
+    async fn get_draw_calls(
+        &self,
+        atom: &mut PropertyAtomicGuard,
+        parent_rect: Rectangle,
+    ) -> Option<DrawUpdate> {
+        self.rect.eval(atom, &parent_rect).ok()?;
         let rect = self.rect.get();
 
         let mut instrs = vec![GfxDrawInstruction::Move(rect.pos())];
@@ -196,11 +201,11 @@ impl UIObject for Text {
         &self,
         parent_rect: Rectangle,
         trace: DrawTrace,
-        _atom: &mut PropertyAtomicGuard,
+        atom: &mut PropertyAtomicGuard,
     ) -> Option<DrawUpdate> {
         t!("Text::draw({:?}) [trace={trace}]", self.node.upgrade().unwrap());
         *self.parent_rect.lock() = Some(parent_rect);
-        self.get_draw_calls(parent_rect).await
+        self.get_draw_calls(atom, parent_rect).await
     }
 
     fn set_i18n(&self, i18n_fish: &I18nBabelFish) {
@@ -210,6 +215,11 @@ impl UIObject for Text {
 
 impl Drop for Text {
     fn drop(&mut self) {
-        self.render_api.replace_draw_calls(unixtime(), vec![(self.dc_key, Default::default())]);
+        let atom = self.render_api.make_guard();
+        self.render_api.replace_draw_calls(
+            atom.batch_id,
+            unixtime(),
+            vec![(self.dc_key, Default::default())],
+        );
     }
 }

+ 20 - 9
bin/app/src/ui/vector_art/mod.rs

@@ -23,7 +23,7 @@ use std::sync::Arc;
 
 use crate::{
     gfx::{gfxtag, GfxDrawCall, GfxDrawInstruction, GfxDrawMesh, Rectangle, RenderApi},
-    prop::{PropertyAtomicGuard, PropertyBool, PropertyRect, PropertyUint32, Role},
+    prop::{BatchGuardPtr, PropertyAtomicGuard, PropertyBool, PropertyRect, PropertyUint32, Role},
     scene::{Pimpl, SceneNodeWeak},
     util::unixtime,
     ExecutorPtr,
@@ -87,17 +87,18 @@ impl VectorArt {
         format!("{:?}", self.node.upgrade().unwrap())
     }
 
-    async fn redraw(self: Arc<Self>) {
+    async fn redraw(self: Arc<Self>, batch: BatchGuardPtr) {
         let trace = rand::random();
         let timest = unixtime();
         trace!(target: "ui::vector_art", "VectorArt::redraw({}) [trace={trace}]", self.node_path());
         let Some(parent_rect) = self.parent_rect.lock().clone() else { return };
 
-        let Some(draw_update) = self.get_draw_calls(parent_rect, trace).await else {
+        let atom = &mut batch.spawn();
+        let Some(draw_update) = self.get_draw_calls(atom, parent_rect, trace).await else {
             error!(target: "ui::vector_art", "Mesh failed to draw [trace={trace}]");
             return
         };
-        self.render_api.replace_draw_calls(timest, draw_update.draw_calls);
+        self.render_api.replace_draw_calls(batch.id, timest, draw_update.draw_calls);
     }
 
     fn get_draw_instrs(&self) -> Vec<GfxDrawInstruction> {
@@ -119,8 +120,13 @@ impl VectorArt {
         vec![GfxDrawInstruction::Move(rect.pos()), GfxDrawInstruction::Draw(mesh)]
     }
 
-    async fn get_draw_calls(&self, parent_rect: Rectangle, trace: DrawTrace) -> Option<DrawUpdate> {
-        if let Err(e) = self.rect.eval(&parent_rect) {
+    async fn get_draw_calls(
+        &self,
+        atom: &mut PropertyAtomicGuard,
+        parent_rect: Rectangle,
+        trace: DrawTrace,
+    ) -> Option<DrawUpdate> {
+        if let Err(e) = self.rect.eval(atom, &parent_rect) {
             warn!(target: "ui::vector_art", "Rect eval failure: {e} [trace={trace}]");
             return None
         }
@@ -161,16 +167,21 @@ impl UIObject for VectorArt {
         &self,
         parent_rect: Rectangle,
         trace: DrawTrace,
-        _atom: &mut PropertyAtomicGuard,
+        atom: &mut PropertyAtomicGuard,
     ) -> Option<DrawUpdate> {
         t!("VectorArt::draw({}) [trace={trace}]", self.node_path());
         *self.parent_rect.lock() = Some(parent_rect);
-        self.get_draw_calls(parent_rect, trace).await
+        self.get_draw_calls(atom, parent_rect, trace).await
     }
 }
 
 impl Drop for VectorArt {
     fn drop(&mut self) {
-        self.render_api.replace_draw_calls(unixtime(), vec![(self.dc_key, Default::default())]);
+        let atom = self.render_api.make_guard();
+        self.render_api.replace_draw_calls(
+            atom.batch_id,
+            unixtime(),
+            vec![(self.dc_key, Default::default())],
+        );
     }
 }

+ 18 - 15
bin/app/src/ui/win.rs

@@ -28,7 +28,9 @@ use crate::{
         GraphicsEventMouseMoveSub, GraphicsEventMouseWheelSub, GraphicsEventPublisherPtr,
         GraphicsEventTouchSub, Point, Rectangle, RenderApi,
     },
-    prop::{PropertyAtomicGuard, PropertyDimension, PropertyFloat32, PropertyStr, Role},
+    prop::{
+        BatchGuardPtr, PropertyAtomicGuard, PropertyDimension, PropertyFloat32, PropertyStr, Role,
+    },
     scene::{Pimpl, SceneNodePtr, SceneNodeWeak},
     util::{i18n::I18nBabelFish, unixtime},
     ExecutorPtr,
@@ -116,17 +118,17 @@ impl Window {
                 };
 
                 d!("Window resized {size:?}");
-                let atom = &mut PropertyAtomicGuard::new();
-
-                // Now update the properties
-                screen_size2.set(atom, size);
 
                 let Some(self_) = me2.upgrade() else {
                     // Should not happen
                     panic!("self destroyed before modify_task was stopped!");
                 };
 
-                self_.draw().await;
+                let atom = &mut self_.render_api.make_guard();
+                // Now update the properties
+                screen_size2.set(atom, size);
+
+                self_.draw(atom).await;
             }
         });
 
@@ -168,11 +170,13 @@ impl Window {
         let me2 = me.clone();
         let touch_task = ex.spawn(async move { while Self::process_touch(&me2, &ev_sub).await {} });
 
-        async fn reload_locale(self_: Arc<Window>) {
-            self_.reload_locale().await;
+        async fn reload_locale(self_: Arc<Window>, batch: BatchGuardPtr) {
+            let atom = &mut batch.spawn();
+            self_.reload_locale(atom).await;
         }
-        async fn redraw(self_: Arc<Window>) {
-            self_.draw().await;
+        async fn redraw(self_: Arc<Window>, batch: BatchGuardPtr) {
+            let atom = &mut batch.spawn();
+            self_.draw(atom).await;
         }
 
         let mut on_modify = OnModify::new(ex.clone(), self.node.clone(), me.clone());
@@ -436,8 +440,7 @@ impl Window {
         }
     }
 
-    pub async fn draw(&self) {
-        let atom = &mut PropertyAtomicGuard::new();
+    pub async fn draw(&self, atom: &mut PropertyAtomicGuard) {
         let trace_id = rand::random();
         let timest = unixtime();
 
@@ -468,12 +471,12 @@ impl Window {
         draw_calls.push((0, dc));
         //t!("  => {:?}", draw_calls);
 
-        self.render_api.replace_draw_calls(timest, draw_calls);
+        self.render_api.replace_draw_calls(atom.batch_id, timest, draw_calls);
 
         t!("Window::draw() - replaced draw call [timest={timest}, trace_id={trace_id}]");
     }
 
-    async fn reload_locale(&self) {
+    async fn reload_locale(&self, atom: &mut PropertyAtomicGuard) {
         /*
         let i18n_src = indoc::indoc! {"
             hello-world = Hello, world!
@@ -494,6 +497,6 @@ impl Window {
             obj.set_i18n(&i18n_fish);
         }
         // Just redraw everything lol
-        self.draw().await;
+        self.draw(atom).await;
     }
 }