use glam::{Vec2, Vec3, Vec4};
use std::collections::HashMap;
use crate::glyph::RenderLayer;
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct GlyphInstance {
pub position: [f32; 3],
pub scale: [f32; 2],
pub rotation: f32,
pub color: [f32; 4],
pub emission: f32,
pub glow_color: [f32; 3],
pub glow_radius: f32,
pub uv_offset: [f32; 2],
pub uv_size: [f32; 2],
pub _pad: [f32; 2],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum BlendMode {
Alpha,
Additive,
Multiply,
Screen,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct BatchKey {
pub layer: RenderLayerOrd,
pub blend: BlendMode,
pub atlas_page: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct RenderLayerOrd(pub u8);
impl RenderLayerOrd {
pub fn from_layer(layer: RenderLayer) -> Self {
Self(match layer {
RenderLayer::Background => 0,
RenderLayer::World => 1,
RenderLayer::Entity => 2,
RenderLayer::Particle => 3,
RenderLayer::Overlay => 4,
RenderLayer::UI => 5,
})
}
}
impl BatchKey {
pub fn new(layer: RenderLayer, blend: BlendMode, atlas_page: u8) -> Self {
Self { layer: RenderLayerOrd::from_layer(layer), blend, atlas_page }
}
pub fn default_for_layer(layer: RenderLayer) -> Self {
let blend = match layer {
RenderLayer::Overlay | RenderLayer::Particle => BlendMode::Additive,
_ => BlendMode::Alpha,
};
Self::new(layer, blend, 0)
}
}
impl PartialOrd for BatchKey {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for BatchKey {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.layer.cmp(&other.layer)
.then(self.blend.cmp(&other.blend))
.then(self.atlas_page.cmp(&other.atlas_page))
}
}
#[derive(Debug)]
pub struct GlyphBatch {
pub key: BatchKey,
pub instances: Vec<GlyphInstance>,
}
impl GlyphBatch {
pub fn new(key: BatchKey) -> Self {
Self { key, instances: Vec::with_capacity(64) }
}
pub fn clear(&mut self) { self.instances.clear(); }
pub fn push(&mut self, inst: GlyphInstance) { self.instances.push(inst); }
pub fn len(&self) -> usize { self.instances.len() }
pub fn is_empty(&self) -> bool { self.instances.is_empty() }
pub fn as_bytes(&self) -> &[u8] {
bytemuck::cast_slice(&self.instances)
}
pub fn sort_back_to_front(&mut self) {
self.instances.sort_by(|a, b| {
b.position[2].partial_cmp(&a.position[2]).unwrap_or(std::cmp::Ordering::Equal)
});
}
pub fn sort_front_to_back(&mut self) {
self.instances.sort_by(|a, b| {
a.position[2].partial_cmp(&b.position[2]).unwrap_or(std::cmp::Ordering::Equal)
});
}
}
pub struct PendingGlyph {
pub key: BatchKey,
pub instance: GlyphInstance,
pub depth: f32, }
pub struct GlyphBatcher {
pending: Vec<PendingGlyph>,
batches: Vec<GlyphBatch>,
stats: BatchStats,
}
#[derive(Default, Debug, Clone)]
pub struct BatchStats {
pub total_glyphs: usize,
pub batch_count: usize,
pub alpha_glyphs: usize,
pub additive_glyphs: usize,
}
impl GlyphBatcher {
pub fn new() -> Self {
Self {
pending: Vec::with_capacity(4096),
batches: Vec::with_capacity(16),
stats: BatchStats::default(),
}
}
pub fn begin(&mut self) {
self.pending.clear();
self.stats = BatchStats::default();
}
pub fn push(&mut self, key: BatchKey, instance: GlyphInstance, depth: f32) {
self.pending.push(PendingGlyph { key, instance, depth });
}
pub fn push_default(&mut self, layer: RenderLayer, instance: GlyphInstance, depth: f32) {
let key = BatchKey::default_for_layer(layer);
self.push(key, instance, depth);
}
pub fn finish(&mut self) {
self.stats.total_glyphs = self.pending.len();
self.pending.sort_by(|a, b| {
a.key.cmp(&b.key)
.then(b.depth.partial_cmp(&a.depth).unwrap_or(std::cmp::Ordering::Equal))
});
self.batches.clear();
let mut current_key: Option<BatchKey> = None;
for item in &self.pending {
match &item.blend_type() {
BlendMode::Alpha | BlendMode::Multiply | BlendMode::Screen => {
self.stats.alpha_glyphs += 1;
}
BlendMode::Additive => {
self.stats.additive_glyphs += 1;
}
}
if current_key != Some(item.key) {
self.batches.push(GlyphBatch::new(item.key));
current_key = Some(item.key);
}
self.batches.last_mut().unwrap().instances.push(item.instance);
}
self.stats.batch_count = self.batches.len();
}
pub fn batches(&self) -> &[GlyphBatch] {
&self.batches
}
pub fn stats(&self) -> &BatchStats { &self.stats }
pub fn instance_count(&self) -> usize {
self.batches.iter().map(|b| b.len()).sum()
}
}
impl PendingGlyph {
fn blend_type(&self) -> BlendMode { self.key.blend }
}
impl Default for GlyphBatcher {
fn default() -> Self { Self::new() }
}
impl GlyphInstance {
pub fn build(
position: Vec3,
scale: Vec2,
rotation: f32,
color: Vec4,
emission: f32,
glow_color: Vec3,
glow_radius: f32,
uv_offset: Vec2,
uv_size: Vec2,
) -> Self {
Self {
position: position.into(),
scale: scale.into(),
rotation,
color: color.into(),
emission,
glow_color: glow_color.into(),
glow_radius,
uv_offset: uv_offset.into(),
uv_size: uv_size.into(),
_pad: [0.0; 2],
}
}
pub fn simple(position: Vec3, uv_offset: Vec2, uv_size: Vec2) -> Self {
Self::build(
position,
Vec2::ONE,
0.0,
Vec4::ONE,
0.0,
Vec3::ZERO,
0.0,
uv_offset,
uv_size,
)
}
pub fn glowing(position: Vec3, color: Vec4, emission: f32, glow_radius: f32,
uv_offset: Vec2, uv_size: Vec2) -> Self {
Self::build(
position,
Vec2::ONE,
0.0,
color,
emission,
Vec3::new(color.x, color.y, color.z),
glow_radius,
uv_offset,
uv_size,
)
}
}
#[derive(Default)]
pub struct AtlasUploadTracker {
dirty_pages: std::collections::HashSet<u8>,
}
impl AtlasUploadTracker {
pub fn mark_dirty(&mut self, page: u8) { self.dirty_pages.insert(page); }
pub fn is_dirty(&self, page: u8) -> bool { self.dirty_pages.contains(&page) }
pub fn clear(&mut self) { self.dirty_pages.clear(); }
pub fn dirty_pages(&self) -> impl Iterator<Item = u8> + '_ {
self.dirty_pages.iter().copied()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_instance(z: f32) -> GlyphInstance {
GlyphInstance::simple(Vec3::new(0.0, 0.0, z), Vec2::ZERO, Vec2::new(0.1, 0.1))
}
#[test]
fn batcher_groups_by_key() {
let mut batcher = GlyphBatcher::new();
batcher.begin();
let world_key = BatchKey::new(RenderLayer::World, BlendMode::Alpha, 0);
let ui_key = BatchKey::new(RenderLayer::UI, BlendMode::Alpha, 0);
batcher.push(world_key, make_instance(0.0), 0.0);
batcher.push(world_key, make_instance(1.0), 1.0);
batcher.push(ui_key, make_instance(0.0), 0.0);
batcher.finish();
assert_eq!(batcher.batches().len(), 2, "Expected 2 batches");
assert_eq!(batcher.instance_count(), 3);
}
#[test]
fn batches_sorted_by_layer() {
let mut batcher = GlyphBatcher::new();
batcher.begin();
batcher.push_default(RenderLayer::UI, make_instance(0.0), 0.0);
batcher.push_default(RenderLayer::World, make_instance(0.0), 0.0);
batcher.finish();
let batches = batcher.batches();
assert!(batches[0].key.layer < batches[1].key.layer);
}
#[test]
fn stats_correct() {
let mut batcher = GlyphBatcher::new();
batcher.begin();
batcher.push_default(RenderLayer::World, make_instance(0.0), 0.0);
batcher.push_default(RenderLayer::World, make_instance(1.0), 1.0);
batcher.push_default(RenderLayer::Particle, make_instance(0.0), 0.0);
batcher.finish();
assert_eq!(batcher.stats().total_glyphs, 3);
}
#[test]
fn glyph_instance_size_is_84() {
assert_eq!(std::mem::size_of::<GlyphInstance>(), 84);
}
#[test]
fn sort_back_to_front_orders_by_descending_z() {
let key = BatchKey::new(RenderLayer::World, BlendMode::Alpha, 0);
let mut batch = GlyphBatch::new(key);
batch.push(make_instance(0.0));
batch.push(make_instance(5.0));
batch.push(make_instance(2.0));
batch.sort_back_to_front();
let zs: Vec<f32> = batch.instances.iter().map(|i| i.position[2]).collect();
assert!(zs[0] >= zs[1] && zs[1] >= zs[2]);
}
}