Просмотр исходного кода

app/gfx: split up the large mod.rs into submods

jkds 6 месяцев назад
Родитель
Сommit
45bdc0c7d8
4 измененных файлов с 749 добавлено и 657 удалено
  1. 324 0
      bin/app/src/gfx/api.rs
  2. 145 0
      bin/app/src/gfx/ev.rs
  3. 14 657
      bin/app/src/gfx/mod.rs
  4. 266 0
      bin/app/src/gfx/prune.rs

+ 324 - 0
bin/app/src/gfx/api.rs

@@ -0,0 +1,324 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2026 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::sync::{
+    atomic::{AtomicU32, Ordering},
+    Arc,
+};
+
+use super::{
+    anim::Frame as AnimFrame, AnimId, BufferId, DebugTag, DrawCall, TextureFormat, TextureId,
+    Vertex, NEXT_ANIM_ID, NEXT_BUFFER_ID, NEXT_TEXTURE_ID,
+};
+use crate::{
+    prop::{BatchGuardId, PropertyAtomicGuard},
+    util::unixtime,
+};
+
+pub type EpochIndex = u32;
+type DcId = u64;
+
+pub type ManagedTexturePtr = Arc<ManagedTexture>;
+pub type ManagedBufferPtr = Arc<ManagedBuffer>;
+pub type ManagedSeqAnimPtr = Arc<ManagedSeqAnim>;
+
+/// Auto-deletes texture on drop
+pub struct ManagedTexture {
+    pub(super) id: TextureId,
+    pub(super) epoch: u32,
+    render_api: RenderApi,
+    pub(super) tag: DebugTag,
+}
+
+impl Drop for ManagedTexture {
+    fn drop(&mut self) {
+        self.render_api.delete_unmanaged_texture(self.id, self.epoch, self.tag);
+    }
+}
+
+impl std::fmt::Debug for ManagedTexture {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("ManagedTexture").field("id", &self.id).finish()
+    }
+}
+
+/// Auto-deletes buffer on drop
+pub struct ManagedBuffer {
+    pub(super) id: BufferId,
+    pub(super) epoch: u32,
+    render_api: RenderApi,
+    pub(super) tag: DebugTag,
+    pub(super) buftype: u8,
+}
+
+impl Drop for ManagedBuffer {
+    fn drop(&mut self) {
+        self.render_api.delete_unmanaged_buffer(self.id, self.epoch, self.tag, self.buftype);
+    }
+}
+
+impl std::fmt::Debug for ManagedBuffer {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("ManagedBuffer").field("id", &self.id).finish()
+    }
+}
+
+pub struct ManagedSeqAnim {
+    frames_len: usize,
+    pub id: AnimId,
+    epoch: u32,
+    render_api: RenderApi,
+    tag: DebugTag,
+}
+
+impl ManagedSeqAnim {
+    pub fn update(&self, frame_idx: usize, frame: AnimFrame) {
+        assert!(frame_idx < self.frames_len);
+        self.render_api.update_unmanaged_anim(self.id, frame_idx, frame, self.epoch, self.tag);
+    }
+}
+
+impl Drop for ManagedSeqAnim {
+    fn drop(&mut self) {
+        self.render_api.delete_unmanaged_anim(self.id, self.epoch, self.tag);
+    }
+}
+
+impl std::fmt::Debug for ManagedSeqAnim {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("ManagedSeqAnim").field("id", &self.id).finish()
+    }
+}
+
+#[derive(Clone)]
+pub struct RenderApi {
+    /// We are abusing async_channel since it's cloneable whereas std::sync::mpsc is shit.
+    method_send: async_channel::Sender<(EpochIndex, GraphicsMethod)>,
+    /// Keep track of the current UI epoch
+    epoch: Arc<AtomicU32>,
+}
+
+impl RenderApi {
+    pub fn new(method_send: async_channel::Sender<(EpochIndex, GraphicsMethod)>) -> Self {
+        Self { method_send, epoch: Arc::new(AtomicU32::new(0)) }
+    }
+
+    pub(super) fn next_epoch(&self) -> EpochIndex {
+        self.epoch.fetch_add(1, Ordering::Relaxed) + 1
+    }
+
+    fn send(&self, method: GraphicsMethod) -> EpochIndex {
+        let epoch = self.epoch.load(Ordering::Relaxed);
+        self.send_with_epoch(method, epoch);
+        epoch
+    }
+    fn send_with_epoch(&self, method: GraphicsMethod, epoch: EpochIndex) {
+        let _ = self.method_send.try_send((epoch, method)).unwrap();
+    }
+
+    fn new_unmanaged_texture(
+        &self,
+        width: u16,
+        height: u16,
+        data: Vec<u8>,
+        fmt: TextureFormat,
+        tag: DebugTag,
+    ) -> (TextureId, EpochIndex) {
+        let gfx_texture_id = NEXT_TEXTURE_ID.fetch_add(1, Ordering::Relaxed);
+
+        let method = GraphicsMethod::NewTexture((width, height, data, fmt, gfx_texture_id, tag));
+        let epoch = self.send(method);
+
+        (gfx_texture_id, epoch)
+    }
+
+    pub fn new_texture(
+        &self,
+        width: u16,
+        height: u16,
+        data: Vec<u8>,
+        fmt: TextureFormat,
+        tag: DebugTag,
+    ) -> ManagedTexturePtr {
+        let (id, epoch) = self.new_unmanaged_texture(width, height, data, fmt, tag);
+        Arc::new(ManagedTexture { id, epoch, render_api: self.clone(), tag })
+    }
+
+    fn delete_unmanaged_texture(&self, texture: TextureId, epoch: EpochIndex, tag: DebugTag) {
+        let method = GraphicsMethod::DeleteTexture((texture, tag));
+        self.send_with_epoch(method, epoch);
+    }
+
+    fn new_unmanaged_vertex_buffer(
+        &self,
+        verts: Vec<Vertex>,
+        tag: DebugTag,
+    ) -> (BufferId, EpochIndex) {
+        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);
+
+        (gfx_buffer_id, epoch)
+    }
+
+    fn new_unmanaged_index_buffer(
+        &self,
+        indices: Vec<u16>,
+        tag: DebugTag,
+    ) -> (BufferId, EpochIndex) {
+        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);
+
+        (gfx_buffer_id, epoch)
+    }
+
+    pub fn new_vertex_buffer(&self, verts: Vec<Vertex>, tag: DebugTag) -> ManagedBufferPtr {
+        let (id, epoch) = self.new_unmanaged_vertex_buffer(verts, tag);
+        Arc::new(ManagedBuffer { id, epoch, render_api: self.clone(), tag, buftype: 0 })
+    }
+    pub fn new_index_buffer(&self, indices: Vec<u16>, tag: DebugTag) -> ManagedBufferPtr {
+        let (id, epoch) = self.new_unmanaged_index_buffer(indices, tag);
+        Arc::new(ManagedBuffer { id, epoch, render_api: self.clone(), tag, buftype: 1 })
+    }
+
+    fn delete_unmanaged_buffer(
+        &self,
+        buffer: BufferId,
+        epoch: EpochIndex,
+        tag: DebugTag,
+        buftype: u8,
+    ) {
+        let method = GraphicsMethod::DeleteBuffer((buffer, tag, buftype));
+        self.send_with_epoch(method, epoch);
+    }
+
+    fn new_unmanaged_anim(
+        &self,
+        frames_len: usize,
+        oneshot: bool,
+        tag: DebugTag,
+    ) -> (AnimId, EpochIndex) {
+        let gfx_anim_id = NEXT_ANIM_ID.fetch_add(1, Ordering::Relaxed);
+
+        let method = GraphicsMethod::NewSeqAnim { id: gfx_anim_id, frames_len, oneshot, tag };
+        let epoch = self.send(method);
+
+        (gfx_anim_id, epoch)
+    }
+
+    pub fn new_anim(&self, frames_len: usize, oneshot: bool, tag: DebugTag) -> ManagedSeqAnimPtr {
+        let (id, epoch) = self.new_unmanaged_anim(frames_len, oneshot, tag);
+        Arc::new(ManagedSeqAnim { frames_len, id, epoch, render_api: self.clone(), tag })
+    }
+
+    pub fn update_unmanaged_anim(
+        &self,
+        anim: AnimId,
+        frame_idx: usize,
+        frame: AnimFrame,
+        epoch: EpochIndex,
+        tag: DebugTag,
+    ) {
+        let method = GraphicsMethod::UpdateSeqAnim { id: anim, frame_idx, frame, tag };
+        self.send_with_epoch(method, epoch);
+    }
+
+    fn delete_unmanaged_anim(&self, anim: AnimId, epoch: EpochIndex, tag: DebugTag) {
+        let method = GraphicsMethod::DeleteSeqAnim((anim, tag));
+        self.send_with_epoch(method, epoch);
+    }
+
+    pub fn replace_draw_calls(&self, batch_id: BatchGuardId, dcs: Vec<(DcId, DrawCall)>) {
+        let method = GraphicsMethod::ReplaceGfxDrawCalls { batch_id, dcs };
+        self.send(method);
+
+        // I'm not sure whether we need this. Anyway its not fully reliable either since
+        // we have no guarantee that when `Stage::update()` whether this method is ready
+        // in the receiver.
+        #[cfg(target_os = "android")]
+        miniquad::window::schedule_update();
+    }
+
+    fn start_batch(&self, batch_id: BatchGuardId, tag: DebugTag) {
+        let method = GraphicsMethod::StartBatch { batch_id, tag };
+        self.send(method);
+    }
+    fn end_batch(&self, batch_id: BatchGuardId) {
+        let timest = unixtime();
+        let method = GraphicsMethod::EndBatch { batch_id, timest };
+        self.send(method);
+
+        // Force an update
+        #[cfg(target_os = "android")]
+        miniquad::window::schedule_update();
+    }
+
+    pub fn make_guard(&self, debug_str: Option<&'static str>) -> PropertyAtomicGuard {
+        let r = self.clone();
+        let start_batch = Box::new(move |bid| r.start_batch(bid, debug_str));
+        let r = self.clone();
+        let end_batch = Box::new(move |bid| r.end_batch(bid));
+        PropertyAtomicGuard::new(start_batch, end_batch)
+    }
+}
+
+#[derive(Clone)]
+pub enum GraphicsMethod {
+    NewTexture((u16, u16, Vec<u8>, TextureFormat, TextureId, DebugTag)),
+    DeleteTexture((TextureId, DebugTag)),
+    NewVertexBuffer((Vec<Vertex>, BufferId, DebugTag)),
+    NewIndexBuffer((Vec<u16>, BufferId, DebugTag)),
+    DeleteBuffer((BufferId, DebugTag, u8)),
+    NewSeqAnim { id: AnimId, frames_len: usize, oneshot: bool, tag: DebugTag },
+    UpdateSeqAnim { id: AnimId, frame_idx: usize, frame: AnimFrame, tag: DebugTag },
+    DeleteSeqAnim((AnimId, DebugTag)),
+    ReplaceGfxDrawCalls { batch_id: BatchGuardId, dcs: Vec<(DcId, DrawCall)> },
+    StartBatch { batch_id: BatchGuardId, tag: DebugTag },
+    EndBatch { batch_id: BatchGuardId, timest: u64 },
+    Noop,
+}
+
+impl std::fmt::Debug for GraphicsMethod {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::NewTexture(_) => write!(f, "NewTexture"),
+            Self::DeleteTexture(_) => write!(f, "DeleteTexture"),
+            Self::NewVertexBuffer(_) => write!(f, "NewVertexBuffer"),
+            Self::NewIndexBuffer(_) => write!(f, "NewIndexBuffer"),
+            Self::DeleteBuffer(_) => write!(f, "DeleteBuffer"),
+            Self::NewSeqAnim { .. } => write!(f, "NewSeqAnim"),
+            Self::UpdateSeqAnim { .. } => write!(f, "UpdateSeqAnim"),
+            Self::DeleteSeqAnim(_) => write!(f, "DeleteSeqAnim"),
+            Self::ReplaceGfxDrawCalls { batch_id: bid, dcs: _ } => {
+                write!(f, "ReplaceGfxDrawCalls({bid})")
+            }
+            Self::StartBatch { batch_id, tag } => write!(f, "StartBatch({batch_id}, {tag:?})"),
+            Self::EndBatch { batch_id, timest } => write!(f, "EndBatch({batch_id}, {timest})"),
+            Self::Noop => write!(f, "Noop"),
+        }
+    }
+}
+
+impl Default for GraphicsMethod {
+    fn default() -> Self {
+        GraphicsMethod::Noop
+    }
+}

+ 145 - 0
bin/app/src/gfx/ev.rs

@@ -0,0 +1,145 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2026 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use miniquad::{KeyCode, KeyMods, MouseButton, TouchPhase};
+use std::sync::Arc;
+
+use super::{Dimension, Point};
+
+struct EventChannel<T> {
+    sender: async_channel::Sender<T>,
+    recvr: async_channel::Receiver<T>,
+}
+
+impl<T> EventChannel<T> {
+    fn new() -> Self {
+        let (sender, recvr) = async_channel::unbounded();
+        Self { sender, recvr }
+    }
+
+    fn notify(&self, ev: T) {
+        self.sender.try_send(ev).unwrap();
+    }
+
+    fn clone_recvr(&self) -> async_channel::Receiver<T> {
+        self.recvr.clone()
+    }
+}
+
+pub type GraphicsEventPublisherPtr = Arc<GraphicsEventPublisher>;
+
+pub struct GraphicsEventPublisher {
+    resize: EventChannel<Dimension>,
+    key_down: EventChannel<(KeyCode, KeyMods, bool)>,
+    key_up: EventChannel<(KeyCode, KeyMods)>,
+    chr: EventChannel<(char, KeyMods, bool)>,
+    mouse_btn_down: EventChannel<(MouseButton, Point)>,
+    mouse_btn_up: EventChannel<(MouseButton, Point)>,
+    mouse_move: EventChannel<Point>,
+    mouse_wheel: EventChannel<Point>,
+    touch: EventChannel<(TouchPhase, u64, Point)>,
+}
+
+pub type GraphicsEventResizeSub = async_channel::Receiver<Dimension>;
+pub type GraphicsEventKeyDownSub = async_channel::Receiver<(KeyCode, KeyMods, bool)>;
+pub type GraphicsEventKeyUpSub = async_channel::Receiver<(KeyCode, KeyMods)>;
+pub type GraphicsEventCharSub = async_channel::Receiver<(char, KeyMods, bool)>;
+pub type GraphicsEventMouseButtonDownSub = async_channel::Receiver<(MouseButton, Point)>;
+pub type GraphicsEventMouseButtonUpSub = async_channel::Receiver<(MouseButton, Point)>;
+pub type GraphicsEventMouseMoveSub = async_channel::Receiver<Point>;
+pub type GraphicsEventMouseWheelSub = async_channel::Receiver<Point>;
+pub type GraphicsEventTouchSub = async_channel::Receiver<(TouchPhase, u64, Point)>;
+
+impl GraphicsEventPublisher {
+    pub fn new() -> Arc<Self> {
+        Arc::new(Self {
+            resize: EventChannel::new(),
+            key_down: EventChannel::new(),
+            key_up: EventChannel::new(),
+            chr: EventChannel::new(),
+            mouse_btn_down: EventChannel::new(),
+            mouse_btn_up: EventChannel::new(),
+            mouse_move: EventChannel::new(),
+            mouse_wheel: EventChannel::new(),
+            touch: EventChannel::new(),
+        })
+    }
+
+    pub(super) fn notify_resize(&self, screen_size: Dimension) {
+        self.resize.notify(screen_size);
+    }
+    pub(super) fn notify_key_down(&self, key: KeyCode, mods: KeyMods, repeat: bool) {
+        let ev = (key, mods, repeat);
+        self.key_down.notify(ev);
+    }
+    pub(super) fn notify_key_up(&self, key: KeyCode, mods: KeyMods) {
+        let ev = (key, mods);
+        self.key_up.notify(ev);
+    }
+    pub(super) fn notify_char(&self, chr: char, mods: KeyMods, repeat: bool) {
+        let ev = (chr, mods, repeat);
+        self.chr.notify(ev);
+    }
+    pub(super) fn notify_mouse_btn_down(&self, button: MouseButton, mouse_pos: Point) {
+        let ev = (button, mouse_pos);
+        self.mouse_btn_down.notify(ev);
+    }
+    pub(super) fn notify_mouse_btn_up(&self, button: MouseButton, mouse_pos: Point) {
+        let ev = (button, mouse_pos);
+        self.mouse_btn_up.notify(ev);
+    }
+
+    pub(super) fn notify_mouse_move(&self, mouse_pos: Point) {
+        self.mouse_move.notify(mouse_pos);
+    }
+    pub(super) fn notify_mouse_wheel(&self, wheel_pos: Point) {
+        self.mouse_wheel.notify(wheel_pos);
+    }
+    pub(super) fn notify_touch(&self, phase: TouchPhase, id: u64, touch_pos: Point) {
+        let ev = (phase, id, touch_pos);
+        self.touch.notify(ev);
+    }
+
+    pub fn subscribe_resize(&self) -> GraphicsEventResizeSub {
+        self.resize.clone_recvr()
+    }
+    pub fn subscribe_key_down(&self) -> GraphicsEventKeyDownSub {
+        self.key_down.clone_recvr()
+    }
+    pub fn subscribe_key_up(&self) -> GraphicsEventKeyUpSub {
+        self.key_up.clone_recvr()
+    }
+    pub fn subscribe_char(&self) -> GraphicsEventCharSub {
+        self.chr.clone_recvr()
+    }
+    pub fn subscribe_mouse_btn_down(&self) -> GraphicsEventMouseButtonDownSub {
+        self.mouse_btn_down.clone_recvr()
+    }
+    pub fn subscribe_mouse_btn_up(&self) -> GraphicsEventMouseButtonUpSub {
+        self.mouse_btn_up.clone_recvr()
+    }
+    pub fn subscribe_mouse_move(&self) -> GraphicsEventMouseMoveSub {
+        self.mouse_move.clone_recvr()
+    }
+    pub fn subscribe_mouse_wheel(&self) -> GraphicsEventMouseWheelSub {
+        self.mouse_wheel.clone_recvr()
+    }
+    pub fn subscribe_touch(&self) -> GraphicsEventTouchSub {
+        self.touch.clone_recvr()
+    }
+}

+ 14 - 657
bin/app/src/gfx/mod.rs

@@ -40,7 +40,21 @@ use std::{
 
 pub mod anim;
 use anim::{Frame as AnimFrame, GfxSeqAnim};
+mod api;
+pub use api::{
+    EpochIndex, GraphicsMethod, ManagedBuffer, ManagedBufferPtr, ManagedSeqAnim, ManagedSeqAnimPtr,
+    ManagedTexture, ManagedTexturePtr, RenderApi,
+};
+mod ev;
+pub use ev::{
+    GraphicsEventCharSub, GraphicsEventKeyDownSub, GraphicsEventKeyUpSub,
+    GraphicsEventMouseButtonDownSub, GraphicsEventMouseButtonUpSub, GraphicsEventMouseMoveSub,
+    GraphicsEventMouseWheelSub, GraphicsEventPublisher, GraphicsEventPublisherPtr,
+    GraphicsEventResizeSub, GraphicsEventTouchSub,
+};
 mod favico;
+mod prune;
+use prune::PruneMethodHeap;
 mod linalg;
 pub use linalg::{Dimension, Point, Rectangle};
 mod shader;
@@ -106,257 +120,6 @@ static NEXT_BUFFER_ID: AtomicU32 = AtomicU32::new(0);
 static NEXT_TEXTURE_ID: AtomicU32 = AtomicU32::new(0);
 static NEXT_ANIM_ID: AtomicU32 = AtomicU32::new(0);
 
-pub type ManagedTexturePtr = Arc<ManagedTexture>;
-
-/// Auto-deletes texture on drop
-pub struct ManagedTexture {
-    id: TextureId,
-    epoch: u32,
-    render_api: RenderApi,
-    tag: DebugTag,
-}
-
-impl Drop for ManagedTexture {
-    fn drop(&mut self) {
-        self.render_api.delete_unmanaged_texture(self.id, self.epoch, self.tag);
-    }
-}
-
-impl std::fmt::Debug for ManagedTexture {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.debug_struct("ManagedTexture").field("id", &self.id).finish()
-    }
-}
-
-pub type ManagedBufferPtr = Arc<ManagedBuffer>;
-
-/// Auto-deletes buffer on drop
-pub struct ManagedBuffer {
-    id: BufferId,
-    epoch: u32,
-    render_api: RenderApi,
-    tag: DebugTag,
-    buftype: u8,
-}
-
-impl Drop for ManagedBuffer {
-    fn drop(&mut self) {
-        self.render_api.delete_unmanaged_buffer(self.id, self.epoch, self.tag, self.buftype);
-    }
-}
-
-impl std::fmt::Debug for ManagedBuffer {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.debug_struct("ManagedBuffer").field("id", &self.id).finish()
-    }
-}
-
-pub type ManagedSeqAnimPtr = Arc<ManagedSeqAnim>;
-
-pub struct ManagedSeqAnim {
-    frames_len: usize,
-    pub id: AnimId,
-    epoch: u32,
-    render_api: RenderApi,
-    tag: DebugTag,
-}
-
-impl ManagedSeqAnim {
-    pub fn update(&self, frame_idx: usize, frame: AnimFrame) {
-        assert!(frame_idx < self.frames_len);
-        self.render_api.update_unmanaged_anim(self.id, frame_idx, frame, self.epoch, self.tag);
-    }
-}
-
-impl Drop for ManagedSeqAnim {
-    fn drop(&mut self) {
-        self.render_api.delete_unmanaged_anim(self.id, self.epoch, self.tag);
-    }
-}
-
-impl std::fmt::Debug for ManagedSeqAnim {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.debug_struct("ManagedSeqAnim").field("id", &self.id).finish()
-    }
-}
-
-pub type EpochIndex = u32;
-
-#[derive(Clone)]
-pub struct RenderApi {
-    /// We are abusing async_channel since it's cloneable whereas std::sync::mpsc is shit.
-    method_send: async_channel::Sender<(EpochIndex, GraphicsMethod)>,
-    /// Keep track of the current UI epoch
-    epoch: Arc<AtomicU32>,
-}
-
-impl RenderApi {
-    pub fn new(method_send: async_channel::Sender<(EpochIndex, GraphicsMethod)>) -> Self {
-        Self { method_send, epoch: Arc::new(AtomicU32::new(0)) }
-    }
-
-    fn next_epoch(&self) -> EpochIndex {
-        self.epoch.fetch_add(1, Ordering::Relaxed) + 1
-    }
-
-    fn send(&self, method: GraphicsMethod) -> EpochIndex {
-        let epoch = self.epoch.load(Ordering::Relaxed);
-        self.send_with_epoch(method, epoch);
-        epoch
-    }
-    fn send_with_epoch(&self, method: GraphicsMethod, epoch: EpochIndex) {
-        let _ = self.method_send.try_send((epoch, method)).unwrap();
-    }
-
-    fn new_unmanaged_texture(
-        &self,
-        width: u16,
-        height: u16,
-        data: Vec<u8>,
-        fmt: TextureFormat,
-        tag: DebugTag,
-    ) -> (TextureId, EpochIndex) {
-        let gfx_texture_id = NEXT_TEXTURE_ID.fetch_add(1, Ordering::Relaxed);
-
-        let method = GraphicsMethod::NewTexture((width, height, data, fmt, gfx_texture_id, tag));
-        let epoch = self.send(method);
-
-        (gfx_texture_id, epoch)
-    }
-
-    pub fn new_texture(
-        &self,
-        width: u16,
-        height: u16,
-        data: Vec<u8>,
-        fmt: TextureFormat,
-        tag: DebugTag,
-    ) -> ManagedTexturePtr {
-        let (id, epoch) = self.new_unmanaged_texture(width, height, data, fmt, tag);
-        Arc::new(ManagedTexture { id, epoch, render_api: self.clone(), tag })
-    }
-
-    fn delete_unmanaged_texture(&self, texture: TextureId, epoch: EpochIndex, tag: DebugTag) {
-        let method = GraphicsMethod::DeleteTexture((texture, tag));
-        self.send_with_epoch(method, epoch);
-    }
-
-    fn new_unmanaged_vertex_buffer(
-        &self,
-        verts: Vec<Vertex>,
-        tag: DebugTag,
-    ) -> (BufferId, EpochIndex) {
-        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);
-
-        (gfx_buffer_id, epoch)
-    }
-
-    fn new_unmanaged_index_buffer(
-        &self,
-        indices: Vec<u16>,
-        tag: DebugTag,
-    ) -> (BufferId, EpochIndex) {
-        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);
-
-        (gfx_buffer_id, epoch)
-    }
-
-    pub fn new_vertex_buffer(&self, verts: Vec<Vertex>, tag: DebugTag) -> ManagedBufferPtr {
-        let (id, epoch) = self.new_unmanaged_vertex_buffer(verts, tag);
-        Arc::new(ManagedBuffer { id, epoch, render_api: self.clone(), tag, buftype: 0 })
-    }
-    pub fn new_index_buffer(&self, indices: Vec<u16>, tag: DebugTag) -> ManagedBufferPtr {
-        let (id, epoch) = self.new_unmanaged_index_buffer(indices, tag);
-        Arc::new(ManagedBuffer { id, epoch, render_api: self.clone(), tag, buftype: 1 })
-    }
-
-    fn delete_unmanaged_buffer(
-        &self,
-        buffer: BufferId,
-        epoch: EpochIndex,
-        tag: DebugTag,
-        buftype: u8,
-    ) {
-        let method = GraphicsMethod::DeleteBuffer((buffer, tag, buftype));
-        self.send_with_epoch(method, epoch);
-    }
-
-    fn new_unmanaged_anim(
-        &self,
-        frames_len: usize,
-        oneshot: bool,
-        tag: DebugTag,
-    ) -> (AnimId, EpochIndex) {
-        let gfx_anim_id = NEXT_ANIM_ID.fetch_add(1, Ordering::Relaxed);
-
-        let method = GraphicsMethod::NewSeqAnim { id: gfx_anim_id, frames_len, oneshot, tag };
-        let epoch = self.send(method);
-
-        (gfx_anim_id, epoch)
-    }
-
-    pub fn new_anim(&self, frames_len: usize, oneshot: bool, tag: DebugTag) -> ManagedSeqAnimPtr {
-        let (id, epoch) = self.new_unmanaged_anim(frames_len, oneshot, tag);
-        Arc::new(ManagedSeqAnim { frames_len, id, epoch, render_api: self.clone(), tag })
-    }
-
-    pub fn update_unmanaged_anim(
-        &self,
-        anim: AnimId,
-        frame_idx: usize,
-        frame: AnimFrame,
-        epoch: EpochIndex,
-        tag: DebugTag,
-    ) {
-        let method = GraphicsMethod::UpdateSeqAnim { id: anim, frame_idx, frame, tag };
-        self.send_with_epoch(method, epoch);
-    }
-
-    fn delete_unmanaged_anim(&self, anim: AnimId, epoch: EpochIndex, tag: DebugTag) {
-        let method = GraphicsMethod::DeleteSeqAnim((anim, tag));
-        self.send_with_epoch(method, epoch);
-    }
-
-    pub fn replace_draw_calls(&self, batch_id: BatchGuardId, dcs: Vec<(DcId, DrawCall)>) {
-        let method = GraphicsMethod::ReplaceGfxDrawCalls { batch_id, dcs };
-        self.send(method);
-
-        // I'm not sure whether we need this. Anyway its not fully reliable either since
-        // we have no guarantee that when `Stage::update()` whether this method is ready
-        // in the receiver.
-        #[cfg(target_os = "android")]
-        miniquad::window::schedule_update();
-    }
-
-    fn start_batch(&self, batch_id: BatchGuardId, tag: DebugTag) {
-        let method = GraphicsMethod::StartBatch { batch_id, tag };
-        self.send(method);
-    }
-    fn end_batch(&self, batch_id: BatchGuardId) {
-        let timest = unixtime();
-        let method = GraphicsMethod::EndBatch { batch_id, timest };
-        self.send(method);
-
-        // Force an update
-        #[cfg(target_os = "android")]
-        miniquad::window::schedule_update();
-    }
-
-    pub fn make_guard(&self, debug_str: Option<&'static str>) -> PropertyAtomicGuard {
-        let r = self.clone();
-        let start_batch = Box::new(move |bid| r.start_batch(bid, debug_str));
-        let r = self.clone();
-        let end_batch = Box::new(move |bid| r.end_batch(bid));
-        PropertyAtomicGuard::new(start_batch, end_batch)
-    }
-}
-
 #[derive(Clone, Debug)]
 pub struct DrawMesh {
     pub vertex_buffer: ManagedBufferPtr,
@@ -844,172 +607,6 @@ impl<'a> RenderContext<'a> {
 type Timestamp = u64;
 type DcId = u64;
 
-#[derive(Clone)]
-pub enum GraphicsMethod {
-    NewTexture((u16, u16, Vec<u8>, TextureFormat, TextureId, DebugTag)),
-    DeleteTexture((TextureId, DebugTag)),
-    NewVertexBuffer((Vec<Vertex>, BufferId, DebugTag)),
-    NewIndexBuffer((Vec<u16>, BufferId, DebugTag)),
-    DeleteBuffer((BufferId, DebugTag, u8)),
-    NewSeqAnim { id: AnimId, frames_len: usize, oneshot: bool, tag: DebugTag },
-    UpdateSeqAnim { id: AnimId, frame_idx: usize, frame: AnimFrame, tag: DebugTag },
-    DeleteSeqAnim((AnimId, DebugTag)),
-    ReplaceGfxDrawCalls { batch_id: BatchGuardId, dcs: Vec<(DcId, DrawCall)> },
-    StartBatch { batch_id: BatchGuardId, tag: DebugTag },
-    EndBatch { batch_id: BatchGuardId, timest: Timestamp },
-    Noop,
-}
-
-impl std::fmt::Debug for GraphicsMethod {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        match self {
-            Self::NewTexture(_) => write!(f, "NewTexture"),
-            Self::DeleteTexture(_) => write!(f, "DeleteTexture"),
-            Self::NewVertexBuffer(_) => write!(f, "NewVertexBuffer"),
-            Self::NewIndexBuffer(_) => write!(f, "NewIndexBuffer"),
-            Self::DeleteBuffer(_) => write!(f, "DeleteBuffer"),
-            Self::NewSeqAnim { .. } => write!(f, "NewSeqAnim"),
-            Self::UpdateSeqAnim { .. } => write!(f, "UpdateSeqAnim"),
-            Self::DeleteSeqAnim(_) => write!(f, "DeleteSeqAnim"),
-            Self::ReplaceGfxDrawCalls { batch_id: bid, dcs: _ } => {
-                write!(f, "ReplaceGfxDrawCalls({bid})")
-            }
-            Self::StartBatch { batch_id, tag } => write!(f, "StartBatch({batch_id}, {tag:?})"),
-            Self::EndBatch { batch_id, timest } => write!(f, "EndBatch({batch_id}, {timest})"),
-            Self::Noop => write!(f, "Noop"),
-        }
-    }
-}
-
-impl Default for GraphicsMethod {
-    fn default() -> Self {
-        GraphicsMethod::Noop
-    }
-}
-
-struct EventChannel<T> {
-    sender: async_channel::Sender<T>,
-    recvr: async_channel::Receiver<T>,
-}
-
-impl<T> EventChannel<T> {
-    fn new() -> Self {
-        let (sender, recvr) = async_channel::unbounded();
-        Self { sender, recvr }
-    }
-
-    fn notify(&self, ev: T) {
-        self.sender.try_send(ev).unwrap();
-    }
-
-    fn clone_recvr(&self) -> async_channel::Receiver<T> {
-        self.recvr.clone()
-    }
-}
-
-pub type GraphicsEventPublisherPtr = Arc<GraphicsEventPublisher>;
-
-pub struct GraphicsEventPublisher {
-    resize: EventChannel<Dimension>,
-    key_down: EventChannel<(KeyCode, KeyMods, bool)>,
-    key_up: EventChannel<(KeyCode, KeyMods)>,
-    chr: EventChannel<(char, KeyMods, bool)>,
-    mouse_btn_down: EventChannel<(MouseButton, Point)>,
-    mouse_btn_up: EventChannel<(MouseButton, Point)>,
-    mouse_move: EventChannel<Point>,
-    mouse_wheel: EventChannel<Point>,
-    touch: EventChannel<(TouchPhase, u64, Point)>,
-}
-
-pub type GraphicsEventResizeSub = async_channel::Receiver<Dimension>;
-pub type GraphicsEventKeyDownSub = async_channel::Receiver<(KeyCode, KeyMods, bool)>;
-pub type GraphicsEventKeyUpSub = async_channel::Receiver<(KeyCode, KeyMods)>;
-pub type GraphicsEventCharSub = async_channel::Receiver<(char, KeyMods, bool)>;
-pub type GraphicsEventMouseButtonDownSub = async_channel::Receiver<(MouseButton, Point)>;
-pub type GraphicsEventMouseButtonUpSub = async_channel::Receiver<(MouseButton, Point)>;
-pub type GraphicsEventMouseMoveSub = async_channel::Receiver<Point>;
-pub type GraphicsEventMouseWheelSub = async_channel::Receiver<Point>;
-pub type GraphicsEventTouchSub = async_channel::Receiver<(TouchPhase, u64, Point)>;
-
-impl GraphicsEventPublisher {
-    pub fn new() -> Arc<Self> {
-        Arc::new(Self {
-            resize: EventChannel::new(),
-            key_down: EventChannel::new(),
-            key_up: EventChannel::new(),
-            chr: EventChannel::new(),
-            mouse_btn_down: EventChannel::new(),
-            mouse_btn_up: EventChannel::new(),
-            mouse_move: EventChannel::new(),
-            mouse_wheel: EventChannel::new(),
-            touch: EventChannel::new(),
-        })
-    }
-
-    fn notify_resize(&self, screen_size: Dimension) {
-        self.resize.notify(screen_size);
-    }
-    fn notify_key_down(&self, key: KeyCode, mods: KeyMods, repeat: bool) {
-        let ev = (key, mods, repeat);
-        self.key_down.notify(ev);
-    }
-    fn notify_key_up(&self, key: KeyCode, mods: KeyMods) {
-        let ev = (key, mods);
-        self.key_up.notify(ev);
-    }
-    fn notify_char(&self, chr: char, mods: KeyMods, repeat: bool) {
-        let ev = (chr, mods, repeat);
-        self.chr.notify(ev);
-    }
-    fn notify_mouse_btn_down(&self, button: MouseButton, mouse_pos: Point) {
-        let ev = (button, mouse_pos);
-        self.mouse_btn_down.notify(ev);
-    }
-    fn notify_mouse_btn_up(&self, button: MouseButton, mouse_pos: Point) {
-        let ev = (button, mouse_pos);
-        self.mouse_btn_up.notify(ev);
-    }
-
-    fn notify_mouse_move(&self, mouse_pos: Point) {
-        self.mouse_move.notify(mouse_pos);
-    }
-    fn notify_mouse_wheel(&self, wheel_pos: Point) {
-        self.mouse_wheel.notify(wheel_pos);
-    }
-    fn notify_touch(&self, phase: TouchPhase, id: u64, touch_pos: Point) {
-        let ev = (phase, id, touch_pos);
-        self.touch.notify(ev);
-    }
-
-    pub fn subscribe_resize(&self) -> GraphicsEventResizeSub {
-        self.resize.clone_recvr()
-    }
-    pub fn subscribe_key_down(&self) -> GraphicsEventKeyDownSub {
-        self.key_down.clone_recvr()
-    }
-    pub fn subscribe_key_up(&self) -> GraphicsEventKeyUpSub {
-        self.key_up.clone_recvr()
-    }
-    pub fn subscribe_char(&self) -> GraphicsEventCharSub {
-        self.chr.clone_recvr()
-    }
-    pub fn subscribe_mouse_btn_down(&self) -> GraphicsEventMouseButtonDownSub {
-        self.mouse_btn_down.clone_recvr()
-    }
-    pub fn subscribe_mouse_btn_up(&self) -> GraphicsEventMouseButtonUpSub {
-        self.mouse_btn_up.clone_recvr()
-    }
-    pub fn subscribe_mouse_move(&self) -> GraphicsEventMouseMoveSub {
-        self.mouse_move.clone_recvr()
-    }
-    pub fn subscribe_mouse_wheel(&self) -> GraphicsEventMouseWheelSub {
-        self.mouse_wheel.clone_recvr()
-    }
-    pub fn subscribe_touch(&self) -> GraphicsEventTouchSub {
-        self.touch.clone_recvr()
-    }
-}
-
 struct Stage {
     ctx: Box<dyn RenderingBackend>,
     #[cfg(target_os = "android")]
@@ -1486,246 +1083,6 @@ impl Stage {
     }
 }
 
-struct PendingAnim {
-    new_method: GraphicsMethod,
-    updates: HashMap<usize, GraphicsMethod>,
-}
-
-/// This is used to process the method queue while the screen is off to avoid the queue
-/// becoming congested and using up all the memory.
-/// Will drop alloc/delete pairs, and merge draw calls together.
-struct PruneMethodHeap {
-    /// Newly allocated buffers while screen was off
-    new_buf: HashMap<BufferId, GraphicsMethod>,
-    /// Newly allocated textures while screen was off
-    new_tex: HashMap<TextureId, GraphicsMethod>,
-    /// Deleted objects
-    del: Vec<GraphicsMethod>,
-
-    new_anim: HashMap<AnimId, PendingAnim>,
-    /// Existing anim updates
-    anim_updates: HashMap<AnimId, HashMap<usize, GraphicsMethod>>,
-    /// Existing anim deletes
-    anim_deletes: HashSet<AnimId>,
-
-    epoch: EpochIndex,
-
-    textures: *const HashMap<TextureId, miniquad::TextureId>,
-    buffers: *const HashMap<BufferId, miniquad::BufferId>,
-    anims: *const HashMap<AnimId, GfxSeqAnim>,
-    dropped_batches: *mut HashSet<BatchGuardId>,
-}
-
-impl PruneMethodHeap {
-    fn new(epoch: EpochIndex) -> Self {
-        Self {
-            new_buf: HashMap::new(),
-            new_tex: HashMap::new(),
-            del: vec![],
-            new_anim: HashMap::new(),
-            anim_updates: HashMap::new(),
-            anim_deletes: HashSet::new(),
-            epoch,
-            textures: std::ptr::null(),
-            buffers: std::ptr::null(),
-            anims: std::ptr::null(),
-            dropped_batches: std::ptr::null_mut(),
-        }
-    }
-
-    #[instrument(skip_all, target = "gfx::pruner")]
-    fn drain(&mut self, method_recv: &async_channel::Receiver<(EpochIndex, GraphicsMethod)>) {
-        // Process as many methods as we can
-        while let Ok((epoch, method)) = method_recv.try_recv() {
-            if epoch < self.epoch {
-                // Discard old rubbish
-                t!(
-                    "Discard method with old epoch: {epoch} curr: {} [method={method:?}]",
-                    self.epoch
-                );
-                continue
-            }
-            assert_eq!(epoch, self.epoch);
-            self.process_method(method);
-        }
-    }
-
-    fn process_method(&mut self, mut method: GraphicsMethod) {
-        match &method {
-            GraphicsMethod::NewTexture((_, _, _, _, gtex_id, _)) => {
-                if DEBUG_GFXAPI {
-                    t!("Prune method: new_texture(..., {gtex_id})");
-                }
-                self.new_tex.insert(*gtex_id, std::mem::take(&mut method));
-            }
-            GraphicsMethod::DeleteTexture((gtex_id, _)) => {
-                if DEBUG_GFXAPI {
-                    t!("Prune method: delete_texture(..., {gtex_id})");
-                }
-                if self.new_tex.remove(&gtex_id).is_none() {
-                    if !self.textures().contains_key(&gtex_id) {
-                        panic!("delete_texture missing ID {gtex_id} in pruner")
-                    }
-                    let method = std::mem::take(&mut method);
-                    self.del.push(method);
-                } else if DEBUG_GFXAPI {
-                    t!("Discard ellided texture {gtex_id}");
-                }
-            }
-            GraphicsMethod::NewVertexBuffer((_, gbuff_id, _)) => {
-                if DEBUG_GFXAPI {
-                    t!("Prune method: new_vertex_buffer(..., {gbuff_id})");
-                }
-                self.new_buf.insert(*gbuff_id, std::mem::take(&mut method));
-            }
-            GraphicsMethod::NewIndexBuffer((_, gbuff_id, _)) => {
-                if DEBUG_GFXAPI {
-                    t!("Prune method: new_index_buffer(..., {gbuff_id})");
-                }
-                self.new_buf.insert(*gbuff_id, std::mem::take(&mut method));
-            }
-            GraphicsMethod::DeleteBuffer((gbuff_id, _, _)) => {
-                if DEBUG_GFXAPI {
-                    t!("Prune method: delete_buffer(..., {gbuff_id})");
-                }
-                if self.new_buf.remove(&gbuff_id).is_none() {
-                    if !self.buffers().contains_key(&gbuff_id) {
-                        panic!("delete_buffer missing ID {gbuff_id} in pruner")
-                    }
-                    let method = std::mem::take(&mut method);
-                    self.del.push(method);
-                } else if DEBUG_GFXAPI {
-                    t!("Discard ellided buffer {gbuff_id}");
-                }
-            }
-            GraphicsMethod::NewSeqAnim { id, .. } => {
-                self.new_anim.insert(
-                    *id,
-                    PendingAnim {
-                        updates: HashMap::new(),
-                        new_method: std::mem::take(&mut method),
-                    },
-                );
-            }
-
-            GraphicsMethod::UpdateSeqAnim { id, frame_idx, .. } => {
-                if let Some(pending) = self.new_anim.get_mut(id) {
-                    pending.updates.insert(*frame_idx, method);
-                } else if self.anims().contains_key(id) {
-                    self.anim_updates.entry(*id).or_default().insert(*frame_idx, method);
-                } else {
-                    panic!("UpdateSeqAnim for unknown anim {id}");
-                }
-            }
-
-            GraphicsMethod::DeleteSeqAnim((id, _)) => {
-                if self.new_anim.remove(id).is_some() {
-                } else if self.anims().contains_key(id) {
-                    self.anim_deletes.insert(*id);
-                    self.anim_updates.remove(id);
-                } else {
-                    panic!("DeleteSeqAnim for unknown anim {id}");
-                }
-            }
-            GraphicsMethod::ReplaceGfxDrawCalls { .. } => {}
-            // Discard batches since we will apply everything all at once anyway
-            // once the screen is switched on.
-            GraphicsMethod::StartBatch { batch_id, tag } => {
-                t!("Pruner drop start batch {batch_id} debug={tag:?}");
-                if !self.dropped_batches().insert(*batch_id) {
-                    panic!("dropped batch {batch_id} already exits!");
-                }
-            }
-            GraphicsMethod::EndBatch { batch_id, timest: _ } => {
-                t!("Pruner drop end batch {batch_id}");
-                // Should have already been dropped previously
-                assert!(self.dropped_batches().contains(batch_id));
-            }
-            GraphicsMethod::Noop => panic!("noop"),
-        }
-    }
-
-    fn textures(&self) -> &HashMap<TextureId, miniquad::TextureId> {
-        assert!(!self.textures.is_null());
-        unsafe { &*self.textures }
-    }
-    fn buffers(&self) -> &HashMap<BufferId, miniquad::BufferId> {
-        assert!(!self.buffers.is_null());
-        unsafe { &*self.buffers }
-    }
-    fn anims(&self) -> &HashMap<AnimId, GfxSeqAnim> {
-        assert!(!self.anims.is_null());
-        unsafe { &*self.anims }
-    }
-    fn dropped_batches(&mut self) -> &mut HashSet<BatchGuardId> {
-        assert!(!self.dropped_batches.is_null());
-        unsafe { &mut *self.dropped_batches }
-    }
-
-    /// 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.new_anim.len() +
-                self.anim_updates.len() +
-                self.anim_deletes.len(),
-        );
-
-        self.drain_resources(&mut meth);
-        self.drain_anims(&mut meth);
-
-        meth
-    }
-
-    fn drain_resources(&mut self, meth: &mut Vec<GraphicsMethod>) {
-        let new_buf = std::mem::take(&mut self.new_buf);
-        let new_tex = std::mem::take(&mut self.new_tex);
-        meth.extend(new_buf.into_values());
-        meth.extend(new_tex.into_values());
-        meth.append(&mut self.del);
-    }
-
-    fn drain_anims(&mut self, meth: &mut Vec<GraphicsMethod>) {
-        self.drain_pending_anims(meth);
-        self.drain_live_anims(meth);
-    }
-
-    fn drain_pending_anims(&mut self, meth: &mut Vec<GraphicsMethod>) {
-        for (_id, pending) in std::mem::take(&mut self.new_anim) {
-            meth.push(pending.new_method);
-
-            let mut updates: Vec<_> = pending.updates.into_iter().collect();
-            updates.sort_by_key(|(idx, _)| *idx);
-            for (_, update) in updates {
-                meth.push(update);
-            }
-        }
-    }
-
-    fn drain_live_anims(&mut self, meth: &mut Vec<GraphicsMethod>) {
-        let mut updates: Vec<_> = self
-            .anim_updates
-            .drain()
-            .flat_map(|(anim_id, frame_updates)| {
-                let mut sorted: Vec<_> = frame_updates.into_iter().collect();
-                sorted.sort_by_key(|(idx, _)| *idx);
-                sorted.into_iter().map(move |(idx, update)| (anim_id, idx, update))
-            })
-            .collect();
-        updates.sort_by_key(|(anim_id, idx, _)| (*anim_id, *idx));
-        for (_, _, update) in updates {
-            meth.push(update);
-        }
-
-        for id in std::mem::take(&mut self.anim_deletes) {
-            meth.push(GraphicsMethod::DeleteSeqAnim((id, None)));
-        }
-    }
-}
-
 #[derive(Copy, Clone, Debug, PartialEq)]
 enum ScreenState {
     // Screen is on as normal

+ 266 - 0
bin/app/src/gfx/prune.rs

@@ -0,0 +1,266 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2026 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::collections::{HashMap, HashSet};
+
+use super::{
+    anim::GfxSeqAnim, AnimId, BufferId, EpochIndex, GraphicsMethod, TextureId, DEBUG_GFXAPI,
+};
+use crate::prop::BatchGuardId;
+
+macro_rules! t { ($($arg:tt)*) => { trace!(target: "gfx::prune", $($arg)*) }; }
+
+struct PendingAnim {
+    new_method: GraphicsMethod,
+    updates: HashMap<usize, GraphicsMethod>,
+}
+
+/// This is used to process the method queue while the screen is off to avoid the queue
+/// becoming congested and using up all the memory.
+/// Will drop alloc/delete pairs, and merge draw calls together.
+pub struct PruneMethodHeap {
+    /// Newly allocated buffers while screen was off
+    new_buf: HashMap<BufferId, GraphicsMethod>,
+    /// Newly allocated textures while screen was off
+    new_tex: HashMap<TextureId, GraphicsMethod>,
+    /// Deleted objects
+    del: Vec<GraphicsMethod>,
+
+    new_anim: HashMap<AnimId, PendingAnim>,
+    /// Existing anim updates
+    anim_updates: HashMap<AnimId, HashMap<usize, GraphicsMethod>>,
+    /// Existing anim deletes
+    anim_deletes: HashSet<AnimId>,
+
+    epoch: EpochIndex,
+
+    pub textures: *const HashMap<TextureId, miniquad::TextureId>,
+    pub buffers: *const HashMap<BufferId, miniquad::BufferId>,
+    pub anims: *const HashMap<AnimId, GfxSeqAnim>,
+    pub dropped_batches: *mut HashSet<BatchGuardId>,
+}
+
+impl PruneMethodHeap {
+    pub fn new(epoch: EpochIndex) -> Self {
+        Self {
+            new_buf: HashMap::new(),
+            new_tex: HashMap::new(),
+            del: vec![],
+            new_anim: HashMap::new(),
+            anim_updates: HashMap::new(),
+            anim_deletes: HashSet::new(),
+            epoch,
+            textures: std::ptr::null(),
+            buffers: std::ptr::null(),
+            anims: std::ptr::null(),
+            dropped_batches: std::ptr::null_mut(),
+        }
+    }
+
+    #[instrument(skip_all, target = "gfx::pruner")]
+    pub fn drain(&mut self, method_recv: &async_channel::Receiver<(EpochIndex, GraphicsMethod)>) {
+        // Process as many methods as we can
+        while let Ok((epoch, method)) = method_recv.try_recv() {
+            if epoch < self.epoch {
+                // Discard old rubbish
+                t!(
+                    "Discard method with old epoch: {epoch} curr: {} [method={method:?}]",
+                    self.epoch
+                );
+                continue
+            }
+            assert_eq!(epoch, self.epoch);
+            self.process_method(method);
+        }
+    }
+
+    fn process_method(&mut self, mut method: GraphicsMethod) {
+        match &method {
+            GraphicsMethod::NewTexture((_, _, _, _, gtex_id, _)) => {
+                if DEBUG_GFXAPI {
+                    t!("Prune method: new_texture(..., {gtex_id})");
+                }
+                self.new_tex.insert(*gtex_id, std::mem::take(&mut method));
+            }
+            GraphicsMethod::DeleteTexture((gtex_id, _)) => {
+                if DEBUG_GFXAPI {
+                    t!("Prune method: delete_texture(..., {gtex_id})");
+                }
+                if self.new_tex.remove(&gtex_id).is_none() {
+                    if !self.textures().contains_key(&gtex_id) {
+                        panic!("delete_texture missing ID {gtex_id} in pruner")
+                    }
+                    let method = std::mem::take(&mut method);
+                    self.del.push(method);
+                } else if DEBUG_GFXAPI {
+                    t!("Discard ellided texture {gtex_id}");
+                }
+            }
+            GraphicsMethod::NewVertexBuffer((_, gbuff_id, _)) => {
+                if DEBUG_GFXAPI {
+                    t!("Prune method: new_vertex_buffer(..., {gbuff_id})");
+                }
+                self.new_buf.insert(*gbuff_id, std::mem::take(&mut method));
+            }
+            GraphicsMethod::NewIndexBuffer((_, gbuff_id, _)) => {
+                if DEBUG_GFXAPI {
+                    t!("Prune method: new_index_buffer(..., {gbuff_id})");
+                }
+                self.new_buf.insert(*gbuff_id, std::mem::take(&mut method));
+            }
+            GraphicsMethod::DeleteBuffer((gbuff_id, _, _)) => {
+                if DEBUG_GFXAPI {
+                    t!("Prune method: delete_buffer(..., {gbuff_id})");
+                }
+                if self.new_buf.remove(&gbuff_id).is_none() {
+                    if !self.buffers().contains_key(&gbuff_id) {
+                        panic!("delete_buffer missing ID {gbuff_id} in pruner")
+                    }
+                    let method = std::mem::take(&mut method);
+                    self.del.push(method);
+                } else if DEBUG_GFXAPI {
+                    t!("Discard ellided buffer {gbuff_id}");
+                }
+            }
+            GraphicsMethod::NewSeqAnim { id, .. } => {
+                self.new_anim.insert(
+                    *id,
+                    PendingAnim {
+                        updates: HashMap::new(),
+                        new_method: std::mem::take(&mut method),
+                    },
+                );
+            }
+
+            GraphicsMethod::UpdateSeqAnim { id, frame_idx, .. } => {
+                if let Some(pending) = self.new_anim.get_mut(id) {
+                    pending.updates.insert(*frame_idx, method);
+                } else if self.anims().contains_key(id) {
+                    self.anim_updates.entry(*id).or_default().insert(*frame_idx, method);
+                } else {
+                    panic!("UpdateSeqAnim for unknown anim {id}");
+                }
+            }
+
+            GraphicsMethod::DeleteSeqAnim((id, _)) => {
+                if self.new_anim.remove(id).is_some() {
+                } else if self.anims().contains_key(id) {
+                    self.anim_deletes.insert(*id);
+                    self.anim_updates.remove(id);
+                } else {
+                    panic!("DeleteSeqAnim for unknown anim {id}");
+                }
+            }
+            GraphicsMethod::ReplaceGfxDrawCalls { .. } => {}
+            // Discard batches since we will apply everything all at once anyway
+            // once the screen is switched on.
+            GraphicsMethod::StartBatch { batch_id, tag } => {
+                t!("Pruner drop start batch {batch_id} debug={tag:?}");
+                if !self.dropped_batches().insert(*batch_id) {
+                    panic!("dropped batch {batch_id} already exits!");
+                }
+            }
+            GraphicsMethod::EndBatch { batch_id, timest: _ } => {
+                t!("Pruner drop end batch {batch_id}");
+                // Should have already been dropped previously
+                assert!(self.dropped_batches().contains(batch_id));
+            }
+            GraphicsMethod::Noop => panic!("noop"),
+        }
+    }
+
+    fn textures(&self) -> &HashMap<TextureId, miniquad::TextureId> {
+        assert!(!self.textures.is_null());
+        unsafe { &*self.textures }
+    }
+    fn buffers(&self) -> &HashMap<BufferId, miniquad::BufferId> {
+        assert!(!self.buffers.is_null());
+        unsafe { &*self.buffers }
+    }
+    fn anims(&self) -> &HashMap<AnimId, GfxSeqAnim> {
+        assert!(!self.anims.is_null());
+        unsafe { &*self.anims }
+    }
+    fn dropped_batches(&mut self) -> &mut HashSet<BatchGuardId> {
+        assert!(!self.dropped_batches.is_null());
+        unsafe { &mut *self.dropped_batches }
+    }
+
+    /// Collect everything now the screen is on
+    pub 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.new_anim.len() +
+                self.anim_updates.len() +
+                self.anim_deletes.len(),
+        );
+
+        self.drain_resources(&mut meth);
+        self.drain_anims(&mut meth);
+
+        meth
+    }
+
+    fn drain_resources(&mut self, meth: &mut Vec<GraphicsMethod>) {
+        let new_buf = std::mem::take(&mut self.new_buf);
+        let new_tex = std::mem::take(&mut self.new_tex);
+        meth.extend(new_buf.into_values());
+        meth.extend(new_tex.into_values());
+        meth.append(&mut self.del);
+    }
+
+    fn drain_anims(&mut self, meth: &mut Vec<GraphicsMethod>) {
+        self.drain_pending_anims(meth);
+        self.drain_live_anims(meth);
+    }
+
+    fn drain_pending_anims(&mut self, meth: &mut Vec<GraphicsMethod>) {
+        for (_id, pending) in std::mem::take(&mut self.new_anim) {
+            meth.push(pending.new_method);
+
+            let mut updates: Vec<_> = pending.updates.into_iter().collect();
+            updates.sort_by_key(|(idx, _)| *idx);
+            for (_, update) in updates {
+                meth.push(update);
+            }
+        }
+    }
+
+    fn drain_live_anims(&mut self, meth: &mut Vec<GraphicsMethod>) {
+        let mut updates: Vec<_> = self
+            .anim_updates
+            .drain()
+            .flat_map(|(anim_id, frame_updates)| {
+                let mut sorted: Vec<_> = frame_updates.into_iter().collect();
+                sorted.sort_by_key(|(idx, _)| *idx);
+                sorted.into_iter().map(move |(idx, update)| (anim_id, idx, update))
+            })
+            .collect();
+        updates.sort_by_key(|(anim_id, idx, _)| (*anim_id, *idx));
+        for (_, _, update) in updates {
+            meth.push(update);
+        }
+
+        for id in std::mem::take(&mut self.anim_deletes) {
+            meth.push(GraphicsMethod::DeleteSeqAnim((id, None)));
+        }
+    }
+}