diff --git a/hui-examples/boilerplate.rs b/hui-examples/boilerplate.rs
index 9c0d1ab..2c23df9 100644
--- a/hui-examples/boilerplate.rs
+++ b/hui-examples/boilerplate.rs
@@ -60,7 +60,7 @@ pub fn ui<T>(
           let mut frame = display.draw();
           frame.clear_color_srgb(0.5, 0.5, 0.5, 1.);
 
-          hui.begin();
+          hui.begin_frame();
 
           let size = UVec2::from(display.get_framebuffer_dimensions()).as_vec2();
           draw(&mut hui, size, &mut result);
diff --git a/hui-examples/examples/align_test.rs b/hui-examples/examples/align_test.rs
index bd33738..ee7d4d7 100644
--- a/hui-examples/examples/align_test.rs
+++ b/hui-examples/examples/align_test.rs
@@ -36,7 +36,7 @@ fn main() {
 
         let resolution = UVec2::from(display.get_framebuffer_dimensions()).as_vec2();
 
-        hui.begin();
+        hui.begin_frame();
 
         let z = instant.elapsed().as_secs_f32().sin().powi(2);
 
diff --git a/hui-examples/examples/text_weird.rs b/hui-examples/examples/text_weird.rs
index 7bfc292..66e850a 100644
--- a/hui-examples/examples/text_weird.rs
+++ b/hui-examples/examples/text_weird.rs
@@ -45,7 +45,7 @@ fn main() {
 
         let resolution = UVec2::from(display.get_framebuffer_dimensions()).as_vec2();
 
-        hui.begin();
+        hui.begin_frame();
 
         hui.add(Container {
           size: (Size::Relative(1.), Size::Relative(1.)).into(),
diff --git a/hui-glium/src/lib.rs b/hui-glium/src/lib.rs
index ccafb3b..b9fef49 100644
--- a/hui-glium/src/lib.rs
+++ b/hui-glium/src/lib.rs
@@ -1,11 +1,27 @@
 use std::rc::Rc;
 use glam::Vec2;
 use glium::{
-  backend::{Context, Facade}, implement_vertex, index::PrimitiveType, texture::{RawImage2d, Texture2d}, uniform, uniforms::{MagnifySamplerFilter, MinifySamplerFilter, Sampler, SamplerBehavior, SamplerWrapFunction}, Api, Blend, DrawParameters, IndexBuffer, Program, Surface, VertexBuffer
-};
-use hui::{
-  draw::{TextureAtlasMeta, UiDrawCall, UiVertex}, UiInstance
+  backend::{Context, Facade},
+  index::PrimitiveType,
+  texture::{RawImage2d, Texture2d},
+  uniforms::{
+    MagnifySamplerFilter,
+    MinifySamplerFilter,
+    Sampler,
+    SamplerBehavior,
+    SamplerWrapFunction
+  },
+  Api,
+  Blend,
+  DrawParameters,
+  IndexBuffer,
+  Program,
+  Surface,
+  VertexBuffer,
+  implement_vertex,
+  uniform,
 };
+use hui::UiInstance;
 
 const VERTEX_SHADER_GLES3: &str = include_str!("../shaders/vertex.es.vert");
 const FRAGMENT_SHADER_GLES3: &str = include_str!("../shaders/fragment.es.frag");
diff --git a/hui-painter/Cargo.toml b/hui-painter/Cargo.toml
index 2849fa0..aa520d1 100644
--- a/hui-painter/Cargo.toml
+++ b/hui-painter/Cargo.toml
@@ -8,7 +8,7 @@ rust-version = "1.85"
 version = "0.1.0-alpha.6"
 edition = "2024"
 license = "GPL-3.0-or-later"
-publish = false # TODO: change to true once ready
+publish = true
 include = [
   "src/**/*.rs",
   "Cargo.toml",
diff --git a/hui-painter/src/backend.rs b/hui-painter/src/backend.rs
index fe68314..643fdf9 100644
--- a/hui-painter/src/backend.rs
+++ b/hui-painter/src/backend.rs
@@ -1,3 +1,114 @@
-// pub struct BackendData<'a> {
-//   pub
-// }
\ No newline at end of file
+use crate::{
+  paint::{buffer::PaintBuffer, command::{PaintCommand, PaintRoot}},
+  texture::TextureAtlasBackendData,
+  PainterInstance,
+};
+
+pub struct Presentatation {
+  current_buffer: PaintBuffer,
+  cur_hash: Option<u64>,
+  prev_hash: Option<u64>,
+  version_counter: u64,
+}
+
+impl Presentatation {
+  pub fn new() -> Self {
+    Self {
+      current_buffer: PaintBuffer::new(),
+      cur_hash: None,
+      prev_hash: None,
+      version_counter: 0,
+    }
+  }
+
+  /// If the paint command has changed since the last draw call, draw it and return true.\
+  /// Otherwise, returns false.
+  pub fn draw(&mut self, painter: &mut PainterInstance, cmd: &impl PaintRoot) -> bool {
+    self.prev_hash = self.cur_hash;
+    self.cur_hash = Some(cmd.cache_hash());
+
+    if self.prev_hash == self.cur_hash {
+      return false;
+    }
+
+    self.current_buffer.clear();
+    cmd.paint_root(painter, &mut self.current_buffer);
+
+    self.version_counter = self.version_counter.wrapping_add(1);
+
+    true
+  }
+
+  /// Get the current paint buffer
+  pub fn buffer(&self) -> &PaintBuffer {
+    &self.current_buffer
+  }
+
+  /// Get the complete backend data for the current presentation
+  ///
+  /// It contains the current paint buffer and the hash of the presentation\
+  /// Unlike the `TextureAtlasBackendData`, the version is non-incremental
+  pub fn backend_data(&self) -> PresentatationBackendData {
+    PresentatationBackendData {
+      buffer: &self.current_buffer,
+      version: self.version_counter,
+      hash: self.cur_hash.unwrap_or(0),
+    }
+  }
+}
+
+impl Default for Presentatation {
+  fn default() -> Self {
+    Self::new()
+  }
+}
+
+/// Backend data for the Presentation
+#[derive(Clone, Copy)]
+pub struct PresentatationBackendData<'a> {
+  /// The current paint buffer
+  pub buffer: &'a PaintBuffer,
+
+  /// The version of the presentation
+  ///
+  /// This is incremented every time the buffer hash changes
+  pub version: u64,
+
+  /// Unique hash of current paint buffer commands
+  pub hash: u64,
+}
+
+#[derive(Clone, Copy)]
+pub struct BackendData<'a> {
+  pub presentation: PresentatationBackendData<'a>,
+  pub atlas: TextureAtlasBackendData<'a>,
+}
+
+impl PainterInstance {
+  pub fn backend_data<'a>(&'a self, presentation: &'a Presentatation) -> BackendData<'a> {
+    BackendData {
+      presentation: presentation.backend_data(),
+      atlas: self.textures.backend_data(),
+    }
+  }
+}
+
+// pub trait HasPainter {
+//   fn painter(&self) -> &PainterInstance;
+//   fn painter_mut(&self) -> &mut PainterInstance;
+// }
+
+// pub trait PresentFrontend: HasPainter {
+//   fn commands(&self) -> &dyn PaintCommand;
+
+//   fn present(&self, backend: &mut dyn PresentBackend) {
+//     backend.presentation().draw(
+//       self.painter_mut(),
+//       self.commands(),
+//     );
+//   }
+// }
+
+pub trait RenderBackend {
+  fn presentation(&self) -> &mut Presentatation;
+}
diff --git a/hui-painter/src/lib.rs b/hui-painter/src/lib.rs
index 56c8e2a..758884d 100644
--- a/hui-painter/src/lib.rs
+++ b/hui-painter/src/lib.rs
@@ -38,20 +38,4 @@ impl PainterInstance {
   pub fn fonts_mut(&mut self) -> &mut FontManager {
     &mut self.fonts
   }
-
-  // pub fn atlas(&self) -> &TextureAtlas {
-  //   &self.atlas
-  // }
-
-  // pub fn atlas_mut(&mut self) -> &mut TextureAtlas {
-  //   &mut self.atlas
-  // }
-
-  // pub fn fonts(&self) -> &FontManager {
-  //   &self.fonts
-  // }
-
-  // pub fn fonts_mut(&mut self) -> &mut FontManager {
-  //   &mut self.fonts
-  // }
 }
diff --git a/hui-painter/src/paint/command/text.rs b/hui-painter/src/paint/command/text.rs
index 58b76a2..b6ba8bf 100644
--- a/hui-painter/src/paint/command/text.rs
+++ b/hui-painter/src/paint/command/text.rs
@@ -6,7 +6,10 @@ use crate::{
   paint::{
     buffer::{PaintBuffer, Vertex},
     command::PaintCommand,
-  }, text::FontHandle, PainterInstance
+  },
+  text::FontHandle,
+  util::hash_vec4,
+  PainterInstance,
 };
 
 // TODO align, multichunk etc
@@ -145,9 +148,23 @@ impl PaintCommand for PaintText {
 
   fn cache_hash(&self) -> u64 {
     let mut hasher = rustc_hash::FxHasher::default();
+
+    // cache font/size/color
     self.text.font.hash(&mut hasher);
     hasher.write_u32(self.text.size.to_bits());
-    hasher.write(self.text.text.as_bytes());
+    hash_vec4(&mut hasher, self.text.color);
+
+    // cache text content
+    match self.text.text {
+      Cow::Owned(ref s) => hasher.write(s.as_bytes()),
+      Cow::Borrowed(s) => {
+        // since the lifetime is 'static, the str is guaranteed to never change
+        // so we can safely compare the ptr + len instead of the content
+        hasher.write_usize(s.as_ptr() as usize);
+        hasher.write_usize(s.len());
+      }
+    }
+
     hasher.finish()
   }
 }
diff --git a/hui-painter/src/texture.rs b/hui-painter/src/texture.rs
index 261f273..001da2b 100644
--- a/hui-painter/src/texture.rs
+++ b/hui-painter/src/texture.rs
@@ -128,6 +128,7 @@ impl TextureAllocation {
   }
 }
 
+#[derive(Clone, Copy)]
 pub struct TextureAtlasBackendData<'a> {
   pub data: &'a [u8],
   pub size: UVec2,
diff --git a/hui/Cargo.toml b/hui/Cargo.toml
index e15eda8..b1c0897 100644
--- a/hui/Cargo.toml
+++ b/hui/Cargo.toml
@@ -38,12 +38,6 @@ default = ["el_all", "image", "builtin_font", "derive"]
 ## Enable derive macros
 derive = ["dep:hui-derive"]
 
-## Enable image loading support using the `image` crate
-image = ["dep:image"]
-
-## Enable the built-in font (ProggyTiny, adds *35kb* to the executable)
-builtin_font = []
-
 #! #### Built-in elements:
 
 ## Enable all built-in elements
diff --git a/hui/src/instance.rs b/hui/src/instance.rs
index cfb4b72..59a085e 100644
--- a/hui/src/instance.rs
+++ b/hui/src/instance.rs
@@ -1,8 +1,5 @@
 use hui_painter::{
-  paint::{buffer::PaintBuffer, command::{PaintCommand, PaintList, PaintRoot}},
-  text::FontHandle,
-  texture::{SourceTextureFormat, TextureAtlasBackendData, TextureHandle},
-  PainterInstance,
+  backend::{BackendData, Presentatation}, paint::{buffer::PaintBuffer, command::{PaintCommand, PaintList, PaintRoot}}, text::FontHandle, texture::{SourceTextureFormat, TextureAtlasBackendData, TextureHandle}, PainterInstance
 };
 use crate::{
   element::{MeasureContext, ProcessContext, UiElement},
@@ -21,17 +18,12 @@ use crate::{
 /// (Please note that it's possible to render multiple UI "roots" using a single instance)
 pub struct UiInstance {
   painter: PainterInstance,
-  prev_draw_command_hash: Option<u64>,
-  cur_draw_command_hash: Option<u64>,
-  draw_commands: PaintList,
-  paint_buffer: PaintBuffer,
+  paint_commands: PaintList,
   stateful_state: StateRepo,
   events: EventQueue,
   input: UiInputState,
   signal: SignalStore,
   font_stack: FontStack,
-  /// True if in the middle of a laying out a frame
-  state: bool,
 }
 
 impl UiInstance {
@@ -41,16 +33,12 @@ impl UiInstance {
   pub fn new() -> Self {
     UiInstance {
       painter: PainterInstance::new(),
-      prev_draw_command_hash: None,
-      cur_draw_command_hash: None,
-      draw_commands: PaintList::default(),
-      paint_buffer: PaintBuffer::new(),
+      paint_commands: PaintList::default(),
       font_stack: FontStack::new(),
       stateful_state: StateRepo::new(),
       events: EventQueue::new(),
       input: UiInputState::new(),
       signal: SignalStore::new(),
-      state: false,
     }
   }
 
@@ -88,6 +76,8 @@ impl UiInstance {
   /// # Panics:
   /// - If the file exists but contains invalid image data\
   ///   (this will change to a soft error in the future)
+  ///
+  /// Deprecated.
   #[cfg(feature = "image")]
   #[deprecated]
   pub fn add_image_file_path(&mut self, path: impl AsRef<std::path::Path>) -> Result<TextureHandle, std::io::Error> {
@@ -148,7 +138,6 @@ impl UiInstance {
   /// ## Panics:
   /// If called while the UI is not active (call [`UiInstance::begin`] first)
   pub fn add(&mut self, element: impl UiElement, rect: impl Into<Rect>) {
-    assert!(self.state, "must call UiInstance::begin before adding elements");
     let rect: Rect = rect.into();
     let layout = LayoutInfo {
       position: rect.position,
@@ -169,79 +158,28 @@ impl UiInstance {
       measure: &measure,
       state: &mut self.stateful_state,
       layout: &layout,
-      paint_target: &mut self.draw_commands,
+      paint_target: &mut self.paint_commands,
       input: self.input.ctx(),
       signal: &mut self.signal,
       current_font,
     });
   }
 
-  /// Prepare the UI for layout and processing\
-  /// You must call this function at the beginning of the frame, before adding any elements\
+  /// Reset the state from the previous frame, and prepare the UI for layout and processing\
+  /// You must call this function at the start of the frame, before adding any elements\
   ///
   /// ## Panics:
   /// If called twice in a row (for example, if you forget to call [`UiInstance::end`])\
   /// This is an indication of a bug in your code and should be fixed.
-  pub fn begin(&mut self) {
-    //check and update current state
-    assert!(!self.state, "must call UiInstance::end before calling UiInstance::begin again");
-    self.state = true;
-
+  pub fn begin_frame(&mut self) {
     //first, drain and process the event queue
     self.input.update_state(&mut self.events);
 
     //then, reset the (remaining) signals
     self.signal.clear();
 
-    // Compute the hash of the current commands
-    self.prev_draw_command_hash = Some(self.draw_commands.cache_hash());
-
     // Clear the draw commands
-    self.draw_commands.clear();
-
-    //then, reset the draw commands
-    // std::mem::swap(&mut self.prev_draw_commands, &mut self.draw_commands);
-    // self.draw_commands.commands.clear();
-    // self.draw_call_modified = false;
-
-    //reset atlas modification flag
-    // self.atlas.reset_modified();
-  }
-
-  /// End the frame and prepare the UI for rendering\
-  /// You must call this function at the end of the frame, before rendering the UI
-  ///
-  /// ## Panics:
-  /// If called without calling [`UiInstance::begin`] first. (or if called twice)\
-  /// This is an indication of a bug in your code and should be fixed.
-  pub fn end(&mut self) {
-    //check and update current state
-    assert!(self.state, "must call UiInstance::begin before calling UiInstance::end");
-    self.state = false;
-
-    //check if the draw commands have been modified
-    if let Some(prev_hash) = self.prev_draw_command_hash {
-      let cur_hash = self.draw_commands.cache_hash();
-      self.cur_draw_command_hash = Some(cur_hash);
-      if cur_hash == prev_hash {
-        return
-      }
-    }
-
-    //if they have, rebuild the draw call and set the modified flag
-    self.paint_buffer.clear();
-    self.draw_commands.paint_root(&mut self.painter, &mut self.paint_buffer);
-  }
-
-  /// Get data intended for rendering backend
-  ///
-  /// You should call this function *before* calling [`UiInstance::begin`] or after calling [`UiInstance::end`]\
-  ///
-  /// This function should only be used by the rendering backend.\
-  /// You should not call this directly unless you're implementing a custom render backend
-  /// or have a very specific usecase (not using one)
-  fn backend_data(&self) -> (&TextureAtlasBackendData, &PaintBuffer) {
-    (&self.painter, &self.paint_buffer)
+    self.paint_commands.clear();
   }
 
   /// Push a platform event to the UI event queue
@@ -256,9 +194,6 @@ impl UiInstance {
   /// You should not call this directly unless you're implementing a custom platform backend
   /// or have a very specific usecase (not using one)
   pub fn push_event(&mut self, event: UiEvent) {
-    if self.state {
-      log::warn!("UiInstance::push_event called while in the middle of a frame, this is probably a mistake");
-    }
     self.events.push(event);
   }
 
@@ -275,6 +210,11 @@ impl UiInstance {
   pub fn process_signals<T: Signal + 'static>(&mut self, f: impl FnMut(T)) {
     self.signal.drain::<T>().for_each(f);
   }
+
+  /// Get the paint commands needed to render the UI
+  pub fn paint_command(&self) -> &impl PaintCommand {
+    &self.paint_commands
+  }
 }
 
 impl Default for UiInstance {